diff --git a/.eslintrc.js b/.eslintrc.js
new file mode 100644
index 0000000000000000000000000000000000000000..acec42340a90ecf6ffb1c9c49437bc5829407216
--- /dev/null
+++ b/.eslintrc.js
@@ -0,0 +1,36 @@
+module.exports = {
+  extends: "eslint:recommended",
+  env: { "browser": true },
+  globals: {
+    "$": false,
+    "jQuery": false
+  },
+  rules: {
+    // errors
+    "block-scoped-var": "error",
+    "func-names": "error",
+    "no-loop-func": "error",
+    "no-param-reassign": "error",
+    "no-shadow": "error",
+    "no-unused-expressions": "error",
+
+    // warnings
+    "dot-notation": "warn",
+    "eqeqeq": ["warn", "smart"],
+    "guard-for-in": "warn",
+    "key-spacing": ["warn", { "beforeColon": false, "afterColon": true }],
+    "no-lonely-if": "warn",
+    "no-console": ["warn", { "allow": ["warn", "error"] }],
+    "no-unneeded-ternary": "warn",
+
+    // fixed automatically
+    "block-spacing": ["warn", "always"],
+    "comma-spacing": ["warn", { "before": false, "after": true }],
+    "indent": ["error", 2],
+    "keyword-spacing": ["warn", { "before": true, "after": true }],
+    "linebreak-style": ["error", "unix"],
+    "no-multi-spaces": "warn",
+    "semi-spacing": ["warn", { "before": false, "after": true }],
+    "space-infix-ops": "warn"
+  }
+};
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
deleted file mode 100644
index 6a677c43e20de8374702d7a884a344253df23b8f..0000000000000000000000000000000000000000
--- a/.github/pull_request_template.md
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-###### TO-DO
-- [x] Make pull request
-- [ ] If you want, you can make a to-do list for your PR like this
-
-<!-- Don't forget to add labels and explain them as you see fit -->
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 7104109c8e8a5dc7f567d5191a4cbc2a8f0e7b63..31d9ca3d1cc4f1fb472863f46a698bd10d006c79 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,7 +5,8 @@ ChangeLog
 .\#*
 TAGS
 *~
-/vendor
 composer.lock
+composer.phar
 import/solrmarc.log
-/solr
\ No newline at end of file
+/solr
+node_modules
diff --git a/.travis.yml b/.travis.yml
index 0e880fe262d87f2c0f81c4315fedb098b86fbed7..cbbbd1f96d82c7c270fa059ecb7895acc943d64b 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -2,23 +2,20 @@ sudo: false
 language: php
 php:
   - 5.4
+  - 5.5
+  - 5.6
+  - 7.0
 
 env:
   - VUFIND_HOME=$PWD VUFIND_LOCAL_DIR=$PWD/local
 
 before_script:
-  # Note: locked phpcs to 2.3.4 due to a bug in 2.4.0; can relax restriction after next release.
-  - pear install pear/PHP_CodeSniffer-2.3.4
   - pear install pear/Http_Request2
   - pear channel-discover pear.phing.info
   - pear install phing/phing
-  - composer global require fabpot/php-cs-fixer
-  - export PATH="$HOME/.composer/vendor/bin:$PATH"
   - phpenv config-rm xdebug.ini
   - phpenv rehash
+  - npm install -g eslint@"<3.0.0"
 
 script:
-  - phing composer
-  - phpunit --stderr --configuration module/VuFind/tests/phpunit.xml
-  - phpcs --standard=PEAR --ignore=*/config/*,*/tests/* --extensions=php $PWD/module
-  - phing php-cs-fixer-dryrun
+  - phing composer phpunitfast phpcs-console php-cs-fixer-dryrun eslint
diff --git a/Gruntfile.js b/Gruntfile.js
new file mode 100644
index 0000000000000000000000000000000000000000..8ba8ebea8005db00c962840329b30becbacbd992
--- /dev/null
+++ b/Gruntfile.js
@@ -0,0 +1,172 @@
+module.exports = function(grunt) {
+  require('jit-grunt')(grunt); // Just in time library loading
+
+  var fs = require('fs');
+
+  function getLoadPaths(file) {
+    var config;
+    var parts = file.split('/');
+    parts.pop(); // eliminate filename
+
+    // initialize search path with directory containing LESS file
+    var retVal = [];
+    retVal.push(parts.join('/'));
+
+    // Iterate through theme.config.php files collecting parent themes in search path:
+    while (config = fs.readFileSync("themes/" + parts[1] + "/theme.config.php", "UTF-8")) {
+      var matches = config.match(/["']extends["']\s*=>\s*['"](\w+)['"]/);
+
+      // "extends" set to "false" or missing entirely? We've hit the end of the line:
+      if (matches === null || matches[1] === 'false') {
+        break;
+      }
+
+      parts[1] = matches[1];
+      retVal.push(parts.join('/') + '/');
+    }
+    return retVal;
+  }
+
+  var fontAwesomePath = '"../../bootstrap3/css/fonts"';
+
+  grunt.initConfig({
+    // LESS compilation
+    less: {
+      compile: {
+        options: {
+          paths: getLoadPaths,
+          compress: true,
+          modifyVars: {
+            'fa-font-path': fontAwesomePath
+          }
+        },
+        files: [{
+          expand: true,
+          src: "themes/*/less/compiled.less",
+          rename: function (dest, src) {
+            return src.replace('/less/', '/css/').replace('.less', '.css');
+          }
+        }]
+      }
+    },
+    // SASS compilation
+    scss: {
+      sass: {
+        options: {
+          style: 'compress'
+        }
+      }
+    },
+    // Convert LESS to SASS, mostly for development team use
+    lessToSass: {
+      convert: {
+        files: [
+          {
+            expand: true,
+            cwd: 'themes/bootstrap3/less',
+            src: ['*.less', 'components/*.less'],
+            ext: '.scss',
+            dest: 'themes/bootstrap3/scss'
+          },
+          {
+            expand: true,
+            cwd: 'themes/bootprint3/less',
+            src: ['*.less'],
+            ext: '.scss',
+            dest: 'themes/bootprint3/scss'
+          }
+        ],
+        options: {
+          replacements: [
+            { // Replace ; in include with ,
+              pattern: /(\s+)@include ([^\(]+)\(([^\)]+)\);/gi,
+              replacement: function mixinCommas(match, space, $1, $2) {
+                return space + '@include ' + $1 + '(' + $2.replace(/;/g, ',') + ');';
+              },
+              order: 3
+            },
+            { // Inline &:extends converted
+              pattern: /&:extend\(([^\)]+)\)/gi,
+              replacement: '@extend $1',
+              order: 3
+            },
+            { // Inline variables not default
+              pattern: / !default; }/gi,
+              replacement: '; }',
+              order: 3
+            },
+            {  // VuFind: Correct paths
+              pattern: 'vendor/bootstrap/bootstrap',
+              replacement: 'vendor/bootstrap',
+              order: 4
+            },
+            {
+              pattern: '$fa-font-path: "../../../fonts" !default;\n',
+              replacement: '',
+              order: 4
+            },
+            {
+              pattern: '@import "vendor/font-awesome/font-awesome";',
+              replacement: '$fa-font-path: ' + fontAwesomePath + ';\n@import "vendor/font-awesome/font-awesome";',
+              order: 4
+            },
+            { // VuFind: Bootprint fixes
+              pattern: '@import "bootstrap";\n@import "variables";',
+              replacement: '@import "variables", "bootstrap";',
+              order: 4
+            },
+            {
+              pattern: '$brand-primary: #619144 !default;',
+              replacement: '$brand-primary: #619144;',
+              order: 4
+            }
+          ]
+        }
+      }
+    },
+    watch: {
+      options: {
+        atBegin: true
+      },
+      less: {
+        files: 'themes/*/less/**/*.less',
+        tasks: ['less']
+      },
+      scss: {
+        files: 'themes/*/scss/**/*.scss',
+        tasks: ['scss']
+      }
+    }
+  });
+  grunt.registerMultiTask('scss', function sassScan() {
+    var sassConfig = {},
+      path = require('path'),
+      themeList = fs.readdirSync(path.resolve('themes')).filter(function (theme) {
+        return fs.existsSync(path.resolve('themes/' + theme + '/scss/compiled.scss'));
+      });
+
+    for (var i in themeList) {
+      var config = {
+        options: {
+          outputStyle: 'compressed'
+        },
+        files: [{
+          expand: true,
+          cwd: path.join('themes', themeList[i], 'scss'),
+          src: ['compiled.scss'],
+          dest: path.join('themes', themeList[i], 'css'),
+          ext: '.css'
+        }]
+      };
+      for (var key in this.data.options) {
+        config.options[key] = this.data.options[key] + '';
+      }
+      config.options.includePaths = getLoadPaths('themes/' + themeList[i] + '/scss/compiled.scss');
+
+      sassConfig[themeList[i]] = config;
+    }
+
+    grunt.config.set('sass', sassConfig);
+    grunt.task.run('sass');
+  });
+};
diff --git a/README.md b/README.md
index c19f52c492b53caa716f0e3ad1d4efdc890c120d..8a418ab30b65f1c09475d577ae0627d8ac3d109f 100644
--- a/README.md
+++ b/README.md
@@ -9,9 +9,12 @@ records.  To learn more, visit https://vufind.org.
 
 Installation
 ------------
-
 See online documentation at https://vufind.org/wiki/installation
 
+Contributing
+------------
+See our [developers handbook](https://vufind.org/wiki/development) for more information.
+
 Testing
 -------
 
diff --git a/build.xml b/build.xml
index b85aba585e6fe1b5ef53b5dbbe48663fb822c411..c55115c867d98454bb966987bba879f7f1faf562 100644
--- a/build.xml
+++ b/build.xml
@@ -7,7 +7,6 @@
   <property name="apacheconfdir" value="/etc/apache2/conf.d" />
   <property name="apachectl" value="false" /><!-- set to apachectl path to spin up Apache instance -->
   <property name="seleniumjar" value="false" /><!-- set to Selenium jar path to spin up Selenium instance -->
-  <property name="nodepath" value="" /><!-- set to node.js modules path to use Zombie.js testing -->
   <!-- command for extra cleanup during shutdown; e.g. to change cache ownership after testing w/ Apache so deletion won't fail: -->
   <property name="extra_shutdown_cleanup" value="false" />
   <property name="vufindurl" value="http://localhost/vufind" />
@@ -21,12 +20,15 @@
   <property name="pgsqlhost" value="localhost" />
   <property name="pgsqlrootuser" value="postgres" />
   <property name="phpunit_extra_params" value="" />
-  <property name="mink_driver" value="zombiejs" /><!-- may also be set to selenium -->
+  <property name="composer_extra_params" value="" />
+  <property name="mink_driver" value="selenium" />
+  <property name="selenium_browser" value="firefox" />
   <property name="snooze_multiplier" value="1" /><!-- can be used to slow down tests (selenium only) -->
+  <property name="solr_startup_sleep" value="0" />
   <property name="php-cs-fixers" value="no_blank_lines_before_namespaces,function_call_space,trailing_spaces,unused_use,lowercase_keywords,encoding,parenthesis,php_closing_tag,visibility,duplicate_semicolon,extra_empty_lines,no_blank_lines_after_class_opening,no_empty_lines_after_phpdocs,operators_spaces,spaces_before_semicolon,ternary_spaces,concat_with_spaces,short_array_syntax,phpdoc_no_access,remove_leading_slash_use,eof_ending" />
 
 
-  <property name="version" value="3.0.3" />
+  <property name="version" value="3.1.3" />
 
   <!-- We only need the -p switch if the password is non-blank -->
   <if><not><equals arg1="${mysqlrootpass}" arg2="" /></not><then>
@@ -64,39 +66,54 @@
     <phingcall target="phpmd"/>
     <phingcall target="pdepend"/>
     <phingcall target="phploc"/>
+    <phingcall target="eslint-report"/>
   </target>
 
   <!-- Report rule violations with PHPMD (mess detector) -->
   <target name="phpmd">
-    <exec command="phpmd ${srcdir}/module xml ${srcdir}/tests/phpmd.xml --exclude ${srcdir}/module/VuFind/tests,${srcdir}/module/VuDL/tests,${srcdir}/module/VuFindSearch/tests --reportfile ${builddir}/reports/phpmd.xml" />
+    <exec command="${srcdir}/vendor/bin/phpmd ${srcdir}/module xml ${srcdir}/tests/phpmd.xml --exclude ${srcdir}/module/VuFind/tests,${srcdir}/module/VuDL/tests,${srcdir}/module/VuFindSearch/tests --reportfile ${builddir}/reports/phpmd.xml" />
   </target>
 
   <!-- Measure project with phploc -->
   <target name="phploc">
-    <exec command="phploc --log-csv ${builddir}/reports/phploc.csv ${srcdir}/module" />
+    <exec command="${srcdir}/vendor/bin/phploc --log-csv ${builddir}/reports/phploc.csv ${srcdir}/module" />
   </target>
 
   <!-- PHP_Depend code analysis -->
   <target name="pdepend">
-    <exec command="pdepend --jdepend-xml=${builddir}/reports/jdepend.xml --jdepend-chart=${builddir}/reports/dependencies.svg --overview-pyramid=${builddir}/reports/pdepend-pyramid.svg ${srcdir}/module" />
+    <exec command="${srcdir}/vendor/bin/pdepend --jdepend-xml=${builddir}/reports/jdepend.xml --jdepend-chart=${builddir}/reports/dependencies.svg --overview-pyramid=${builddir}/reports/pdepend-pyramid.svg ${srcdir}/module" />
   </target>
 
   <!-- PHP copy-and-paste detection -->
   <target name="phpcpd">
-    <exec command="phpcpd --log-pmd ${builddir}/reports/pmd-cpd.xml --exclude tests ${srcdir}/module" />
+    <exec command="${srcdir}/vendor/bin/phpcpd --log-pmd ${builddir}/reports/pmd-cpd.xml --exclude tests ${srcdir}/module" />
   </target>
 
   <!-- PHP CodeSniffer -->
   <target name="phpcs">
-    <exec command="phpcs --standard=PEAR --ignore=*/config/*,*/tests/* --extensions=php --report=checkstyle ${srcdir}/module &gt; ${builddir}/reports/checkstyle.xml" escape="false" />
+    <exec command="${srcdir}/vendor/bin/phpcs --standard=PEAR --ignore=*/config/*,*/tests/* --extensions=php --report=checkstyle ${srcdir}/module &gt; ${builddir}/reports/checkstyle.xml" escape="false" />
+  </target>
+  <target name="phpcs-console">
+    <exec command="${srcdir}/vendor/bin/phpcs --standard=PEAR --ignore=*/config/*,*/tests/* --extensions=php ${srcdir}/module" escape="false" passthru="true" checkreturn="true" />
   </target>
 
   <!-- php-cs-fixer (first task applies fixes, second task simply checks if they are needed) -->
   <target name="php-cs-fixer">
-    <exec command="php-cs-fixer fix ${srcdir}/module --fixers=${php-cs-fixers} --verbose" passthru="true" escape="false" />
+    <exec command="${srcdir}/vendor/bin/php-cs-fixer fix ${srcdir}/module --fixers=${php-cs-fixers} --verbose" passthru="true" escape="false" />
   </target>
   <target name="php-cs-fixer-dryrun">
-    <exec command="php-cs-fixer fix ${srcdir}/module --fixers=${php-cs-fixers} --dry-run --verbose --diff" passthru="true" escape="false" checkreturn="true" />
+    <exec command="${srcdir}/vendor/bin/php-cs-fixer fix ${srcdir}/module --fixers=${php-cs-fixers} --dry-run --verbose --diff" passthru="true" escape="false" checkreturn="true" />
+  </target>
+
+  <!-- ESLint -->
+  <target name="eslint">
+    <exec command="eslint -c ${srcdir}/.eslintrc.js ${srcdir}/themes/bootstrap3/js/*.js" escape="false" checkreturn="true" passthru="true" />
+  </target>
+  <target name="eslint-fix">
+    <exec command="eslint -c ${srcdir}/.eslintrc.js ${srcdir}/themes/bootstrap3/js/*.js --fix" escape="false" passthru="true" />
+  </target>
+  <target name="eslint-report">
+    <exec command="eslint -c ${srcdir}/.eslintrc.js ${srcdir}/themes/bootstrap3/js/*.js --format checkstyle -o ${builddir}/reports/eslint.xml" escape="false" />
   </target>
 
   <!-- PHP API Documentation -->
@@ -112,18 +129,18 @@
 
   <!-- PHPUnit -->
   <target name="phpunit" description="Run tests">
-    <exec dir="${srcdir}/module/VuFind/tests" command="NODE_PATH=${nodepath} VUFIND_MINK_DRIVER=${mink_driver} VUFIND_SNOOZE_MULTIPLIER=${snooze_multiplier} VUFIND_LOCAL_DIR=${srcdir}/local VUFIND_URL=${vufindurl} phpunit -dzend.enable_gc=0 --log-junit ${builddir}/reports/phpunit.xml --coverage-clover ${builddir}/reports/coverage/clover.xml --coverage-html ${builddir}/reports/coverage/ ${phpunit_extra_params}" passthru="true" checkreturn="true" />
+    <exec dir="${srcdir}/module/VuFind/tests" command="VUFIND_MINK_DRIVER=${mink_driver} VUFIND_SELENIUM_BROWSER=${selenium_browser} VUFIND_SNOOZE_MULTIPLIER=${snooze_multiplier} VUFIND_LOCAL_DIR=${srcdir}/local VUFIND_URL=${vufindurl} ${srcdir}/vendor/bin/phpunit -dzend.enable_gc=0 --log-junit ${builddir}/reports/phpunit.xml --coverage-clover ${builddir}/reports/coverage/clover.xml --coverage-html ${builddir}/reports/coverage/ ${phpunit_extra_params}" passthru="true" checkreturn="true" />
   </target>
 
   <!-- PHPUnit without logging output -->
   <target name="phpunitfast" description="Run tests">
-    <exec dir="${srcdir}/module/VuFind/tests" command="NODE_PATH=${nodepath} VUFIND_MINK_DRIVER=${mink_driver} VUFIND_SNOOZE_MULTIPLIER=${snooze_multiplier} VUFIND_LOCAL_DIR=${srcdir}/local VUFIND_URL=${vufindurl} phpunit -dzend.enable_gc=0 ${phpunit_extra_params}" passthru="true" checkreturn="true" />
+    <exec dir="${srcdir}/module/VuFind/tests" command="VUFIND_MINK_DRIVER=${mink_driver} VUFIND_SELENIUM_BROWSER=${selenium_browser} VUFIND_SNOOZE_MULTIPLIER=${snooze_multiplier} VUFIND_LOCAL_DIR=${srcdir}/local VUFIND_URL=${vufindurl} ${srcdir}/vendor/bin/phpunit -dzend.enable_gc=0 ${phpunit_extra_params}" passthru="true" checkreturn="true" />
   </target>
 
   <target name="composer" description="Install dependencies with Composer">
     <httpget url="https://getcomposer.org/composer.phar" sslVerifyPeer="false" dir="${srcdir}" />
     <echo message="Installing dependencies..." />
-    <exec command="php ${srcdir}/composer.phar install" passthru="true" checkreturn="true" />
+    <exec command="php ${srcdir}/composer.phar install ${composer_extra_params}" passthru="true" checkreturn="true" />
   </target>
 
   <!-- Install and Activate VuFind -->
@@ -196,6 +213,17 @@
     <exec command="VUFIND_HOME=${srcdir} ${srcdir}/solr.sh restart" outputProperty="LASTOUTPUT" />
     <echo message="${LASTOUTPUT}" />
 
+    <if>
+      <equals arg1="0" arg2="${solr_startup_sleep}" />
+      <then>
+        <!-- do nothing -->
+      </then>
+      <else>
+        <echo message="Waiting ${solr_startup_sleep} seconds for Solr to be ready..." />
+        <exec command="sleep ${solr_startup_sleep}" />
+      </else>
+    </if>
+
     <!-- import marc test records into vufind index (note: the marc test records have prefix "testsample#") -->
     <exec escape="false" command="find ${srcdir}/tests/data -name '*.mrc' -printf '%p,'" outputProperty="buglist" />
     <foreach list="${buglist}" param="filename" delimiter="," target="importrec" />
@@ -272,7 +300,9 @@
     <mkdir dir="${builddir}/export/vufind/usr/local/vufind" />
 
     <!-- load the relevant files into the work area -->
-    <phingcall target="composer" />
+    <phingcall target="composer">
+      <property name="composer_extra_params" value="--no-dev" />
+    </phingcall>
     <exec command="git archive HEAD --format=tar | tar -x -C ${builddir}/export/vufind/usr/local/vufind" />
     <copy todir = "${builddir}/export/vufind/usr/local/vufind/vendor">
       <fileset dir="${srcdir}/vendor" defaultexcludes="false" />
@@ -328,6 +358,15 @@
         <exec command="echo building=\&quot;${BASENAME}\&quot; &gt;&gt; ${srcdir}/local/import/marc-${BASENAME}.properties" />
         <exec command="sed -e &quot;s!marc_local.properties!marc-${BASENAME}.properties!&quot; ${srcdir}/local/import/import.properties &gt; ${srcdir}/local/import/import-${BASENAME}.properties" />
 
+        <!-- if there is a file-specific import configuration, load it now: -->
+        <if>
+          <available file="${filename}.properties" />
+          <then>
+            <echo message="Found custom import configs for ${filename}." />
+            <exec command="cat ${filename}.properties &gt;&gt; ${srcdir}/local/import/marc-${BASENAME}.properties" />
+          </then>
+        </if>
+
         <!-- perform the import -->
         <exec command="VUFIND_HOME=${srcdir} VUFIND_LOCAL_DIR=${srcdir}/local ${srcdir}/import-marc.sh -p ${srcdir}/local/import/import-${BASENAME}.properties ${filename}" outputProperty="LASTOUTPUT" />
         <echo message="${LASTOUTPUT}" />
diff --git a/composer.json b/composer.json
index e99bfc825b402e3c29b295ba00ae9538bd9d4d6b..f6ea1158571f0f49aad77eefa6cd5e90c0b8e3f1 100644
--- a/composer.json
+++ b/composer.json
@@ -8,26 +8,23 @@
         }
     ],
     "license": "GPL-2.0",
-    "repositories": [
-        {
-            "type": "vcs",
-            "url": "https://github.com/zendframework/zendservice_amazon"
-        }
-    ],
     "require": {
         "aferrandini/phpqrcode": "1.0.1",
         "jasig/phpcas": "1.3.3",
         "cap60552/php-sip2": "1.0.0",
         "los/losrecaptcha": "1.0.0",
         "ahand/mobileesp": "dev-master",
+        "matthiasmullie/minify": "1.3.35",
         "ocramius/proxy-manager": "1.0.2",
         "oyejorge/less.php": "1.7.0.9",
         "pear/file_marc": "1.1.2",
         "pear/validate_ispn": "dev-master",
         "serialssolutions/summon": "1.0.0",
         "symfony/yaml": "2.7.6",
-        "vufind-org/vufindcode": "1.0.2",
-        "vufind-org/vufindhttp": "2.0.0",
+        "vufind-org/vufindcode": "1.0.3",
+        "vufind-org/vufindharvest": "2.1.0",
+        "vufind-org/vufindhttp": "2.1.1",
+        "yajra/laravel-pdo-via-oci8": "1.1.1",
         "sabre/vobject": "^3.4",
         "zendframework/zendframework": "2.4.6",
         "zendframework/zendrest": "2.0.2",
@@ -37,6 +34,11 @@
     "require-dev": {
         "behat/mink": "1.7.0",
         "behat/mink-selenium2-driver": "1.3.0",
-        "behat/mink-zombie-driver": "1.3.0"
+        "friendsofphp/php-cs-fixer": "1.11.6",
+        "phploc/phploc": "2.0.6",
+        "phpmd/phpmd": "1.5.0",
+        "phpunit/phpunit": "4.8.27",
+        "sebastian/phpcpd": "2.0.0",
+        "squizlabs/php_codesniffer": "2.6.0"
     }
 }
diff --git a/config/vufind/DAIA.ini b/config/vufind/DAIA.ini
index d0f1fca96c99ca5b793ee34e796f8acc221fd41d..97b0cfe2e0c3a3dddf97d6318c402369fca6b862 100644
--- a/config/vufind/DAIA.ini
+++ b/config/vufind/DAIA.ini
@@ -14,10 +14,21 @@
 ; daiaResponseFormat = json
 ;
 
+; This section contains settings applying to DAIA and its deriving classes such as
+; PAIA.
+;[General]
+; Set the time to live (ttl) for cached data (default is 30 seconds).
+;cacheLifetime = 90
+
 [DAIA]
 ; The base URL for the DAIA webservice.
 baseUrl = [your DAIA server base url]
 
+; Set a DAIA specific timeout in seconds to be used for DAIA http requests (defaults
+; to Zend defaults or as defined in
+; vendor/vufind-org/vufindhttp/src/VuFindHttp/HttpService.php)
+;timeout = 30
+
 ; The prefix prepended to the VuFind record Id resulting in the document URI
 ; used for the DAIA request (default = ppn:) (the prefix usually defines the
 ; field which the DAIA server uses for the loookup - e.g. ppn: or isbn:).
@@ -34,7 +45,7 @@ baseUrl = [your DAIA server base url]
 ; If daiaContentTypes are set, the DAIA driver checks the Content-Type
 ; line in the DAIA response HTTP header for the configured values. If
 ; daiaContentTypes is not set, Content-Type HTTP header is NOT checked.
-; 
+;
 ; expected Content-Types for DAIA XML format:
 ; (separate multiple values by commas, for example:
 ; daiaContentTypes['xml'] = "application/xml, text/xml"
@@ -44,3 +55,18 @@ daiaContentTypes['xml'] = "application/xml"
 ; (separate multiple values by commas, for example:
 ; daiaContentTypes['json'] = "application/json, application/javascript"
 daiaContentTypes['json'] = "application/json"
+
+; Enable caching for DAIA items (default is false).
+;daiaCache = false
+
+; DAIA does not support placing holds (this functionality is covered by PAIA) but is
+; able to provide a link to the OPAC to perform such an action. Regarding placing
+; holds/recalls such link is usually given as href for an unavailable service.
+; Uncomment the below section Holds with the setting 'function' to show a link to
+; the OPAC (if it was provided in the DAIA response) instead of a VuFind
+; Holds-Button.
+; If PAIA is used in combination with the DAIA driver, handling holds etc. should be
+; left to the logic in the PAIA driver - thus keep this section commented out if you
+; are using PAIA as well.
+; [Holds]
+; function = getHoldLink
diff --git a/config/vufind/Demo.ini b/config/vufind/Demo.ini
index 1033570e85018a50409e339499e031974bc2b630..0c20010ecfd4ce4e494c2db873da398d949ef930 100644
--- a/config/vufind/Demo.ini
+++ b/config/vufind/Demo.ini
@@ -72,4 +72,12 @@ getHoldDefaultRequiredDate = 50
 placeHold = 50
 placeILLRequest = 50
 placeStorageRetrievalRequest = 50
-renewMyItems = 50
\ No newline at end of file
+renewMyItems = 50
+
+; Uncomment the following lines to override default config.ini password settings
+; (see [Authorization] section).
+;[changePassword]
+;minLength = 4
+;maxLength = 20
+;pattern = "alphanumeric"
+;hint = "Your optional custom hint can go here."
\ No newline at end of file
diff --git a/config/vufind/EDS.ini b/config/vufind/EDS.ini
index 7a0d8de1a5ea05ecdff5c3adba929c8801879ae5..7576b15dde6fd98b7abda7dd05ec15d2a18b953e 100644
--- a/config/vufind/EDS.ini
+++ b/config/vufind/EDS.ini
@@ -198,3 +198,17 @@ user_name = [USERNAME]
 password  = [PASSWORD]
 profile   = [PROFILE]
 organization_id = "VuFind 2.x from MyUniversity"
+
+; This section controls what happens when a record title in a search result list
+; is clicked. VuFind can either embed the full result directly in the list using
+; AJAX or can display it at its own separate URL as a full HTML page.
+; full - separate page (default)
+; tabs - embedded using tabs (see record/tabs.phtml)
+; accordion - embedded using an accordion (see record/accordion.phtml)
+; NOTE: To turn this feature on for favorite lists, see the lists_view setting
+; in the [Social] section of config.ini.
+; NOTE: This feature is incompatible with SyndeticsPlus content; please use
+;       regular Syndetics if necessary.
+[List]
+view=full
+
diff --git a/config/vufind/Horizon.ini b/config/vufind/Horizon.ini
index 0aaaeae69f89a01e3985cca42bcc0e08bc08c898..f118bed49b85607f26c0aada5eaf321aa4aead92 100644
--- a/config/vufind/Horizon.ini
+++ b/config/vufind/Horizon.ini
@@ -7,11 +7,11 @@ database    = mydatabase
 
 [Statuses]
 ; custom item statuses - A coma-separated list of value pairs. Supported values are
-; available:1, available:0, reserve:N, reserve:Y, and duedate:0. duedate:0 is only 
-; used to designate that an item does not have a due date (e.g. it is lost). By 
+; available:1, available:0, reserve:N, reserve:Y, and duedate:0. duedate:0 is only
+; used to designate that an item does not have a due date (e.g. it is lost). By
 ; default all other item statuses have a duedate, so nothing else is needed
 i   = "available:1", "reserve:N"
 h   = "available:0", "reserve:N"
 rb  = "available:0", "reserve:Y"
 l   = "available:0", "reserve:N", "duedate:0"
-m   = "available:0", "reserve:N", "duedate:0"  
+m   = "available:0", "reserve:N", "duedate:0"
diff --git a/config/vufind/HorizonXMLAPI.ini b/config/vufind/HorizonXMLAPI.ini
index e7ab57d67e917231e60419340a69cccf35d123cd..b4293a02f061965db3d3fd618e401d25beb593ef 100644
--- a/config/vufind/HorizonXMLAPI.ini
+++ b/config/vufind/HorizonXMLAPI.ini
@@ -13,7 +13,7 @@ dateformat = "m/d/Y"
 
 ; This OPTIONAL setting is used to override the pickup location codes and names retrieved
 ; from the ILS database.
-; IMPORTANT: The locationCode is case sensitive and MUST match the case of the 
+; IMPORTANT: The locationCode is case sensitive and MUST match the case of the
 ; code in the Horizon location table.
 ;[pickUpLocations]
 ;locationCode = "locationName"
@@ -24,7 +24,7 @@ dateformat = "m/d/Y"
 ; during hold form processing. Most users should not need to change this setting.
 HMACKeys = item_id:id:level
 
-; notify - The method by which users are notified when their hold / request is 
+; notify - The method by which users are notified when their hold / request is
 ; available. Must correspond with a Horizon system setting.
 notify = "e-mail"
 
@@ -35,7 +35,7 @@ defaultRequiredDate = 0:1:0
 
 ; This OPTIONAL setting is used to override the default pickup location for borrowers
 ; that is retrieved from the borrower record in the ILS database.
-; This MUST match one of the locations listed in pickUpLocations above or the 
+; This MUST match one of the locations listed in pickUpLocations above or the
 ; pickup_location_sort table in the ILS. This setting is case sensitive.
 ;defaultPickUpLocation = "locationCode"
 
@@ -46,8 +46,8 @@ extraHoldFields = pickUpLocation
 
 [Statuses]
 ; custom item statuses - A coma-separated list of value pairs. Supported values are
-; available:1, available:0, reserve:N, reserve:Y, and duedate:0. duedate:0 is only 
-; used to designate that an item does not have a due date (e.g. it is lost). By 
+; available:1, available:0, reserve:N, reserve:Y, and duedate:0. duedate:0 is only
+; used to designate that an item does not have a due date (e.g. it is lost). By
 ; default all other item statuses have a duedate, so nothing else is needed
 i  = "available:1", "reserve:N"
 h  = "available:0", "reserve:N"
diff --git a/config/vufind/KohaILSDI.ini b/config/vufind/KohaILSDI.ini
new file mode 100755
index 0000000000000000000000000000000000000000..d4561db68119886fb3cceb4d8a9876f5d39eb951
--- /dev/null
+++ b/config/vufind/KohaILSDI.ini
@@ -0,0 +1,55 @@
+; KohaILSDI Driver Config
+
+; This driver differs that it uses the ISL-DI API instead of direct database calls
+; It does however, fallback to direct database calls to enhance functionaility
+; that is not available in in the ILS-DI API.
+
+; You must enable ILS-DI in the Web services preferences in Koha
+
+[Catalog]
+; database host, port, user, password, database
+host        = localhost
+port        = 3306
+username    = mysqlusername
+password    = mysqlpassword
+database    = koha
+
+; Url to the ILS-DI API
+url         	= http://library.myuniversity.edu/cgi-bin/koha/ilsdi.pl
+
+;; In addition you can set 'renewals_enabled' and
+;; 'cancel_holds_enabled' in config.ini to 'true' using this driver.
+;; I would also recommend you set 'holds_mode' to '"holds"', as this
+;; driver does not handle recalls.
+
+[Holds]
+; HMACKeys - A list of hold form element names that will be analyzed for consistency
+; during hold form processing. Most users should not need to change this setting.
+; Comment this line to disable VuFind integrated reservations.
+HMACKeys = item_id:id:level
+
+; defaultRequiredDate - A colon-separated list used to set the default "not required
+; after" date for holds in the format days:months:years
+; e.g. 0:1:0 will set a "not required after" date of 1 month from the current date
+defaultRequiredDate = 0:1:0
+
+; extraHoldFields - A colon-separated list used to display extra visible fields in the
+; place holds form. Supported values are "comments", "requiredByDate" and
+; "pickUpLocation"
+extraHoldFields = comments:pickUpLocation:requiredByDate
+
+; A Pick Up Location Code used to pre-select the pick up location drop
+; down list and provide a default option if others are not
+; available. The default of 'false' will force users to pick a pickup
+; location. By setting this to a Koha location code (e.g. '"MAIN"'),
+; Vufind will default to that location.
+defaultPickUpLocation = "MAIN"
+
+; branchcodes for libraries avalaible as pickup locations
+pickupLocations[] = MAIN
+
+[Other]
+; Locations for Always Available by Default
+;availableLocations[] = REFERENCEAREA
+;availableLocations[] = INTERNET
+;availableLocations[] = ONLINE
diff --git a/config/vufind/LBS4.ini b/config/vufind/LBS4.ini
index 333ff993b36b0cd6db700af6bb62b7fc6025aacf..7e39ba18744f6f5c5784e53a5c1b916952451fe1 100644
--- a/config/vufind/LBS4.ini
+++ b/config/vufind/LBS4.ini
@@ -6,10 +6,18 @@
 opaciln  = "000"
 ; the fileset number for the catalog
 opacfno  = "1"
-; URL where loan requests are build from 
+; URL where loan requests are build from
 opcloan  = "https://your.opac.com/loan/LNG=DU/REQ"
 sybpath  = "/opt/sybase"
 sybase   = "INTERFACE"
 username = "XXXXXXX"
 password = "XXXXXXX"
 database = "lbsdb"
+
+; For more information, see DAIA.ini
+; [DAIA]
+baseUrl = https://daia.myuniversity.edu
+daiaidprefix = ppn:
+; daiaResponseFormat = json
+;
+
diff --git a/config/vufind/NoILS.ini b/config/vufind/NoILS.ini
index cea0ca4cf33424f63ec67307a6e4088239592a89..25a793100b8da1cc295b7e26ce1833d6dcd5de07 100644
--- a/config/vufind/NoILS.ini
+++ b/config/vufind/NoILS.ini
@@ -7,13 +7,18 @@ hideLogin = false
 ; none = do not show holdings info in Holdings Tab (see Site/hideHoldingsTabWhenEmpty
 ;        setting in config.ini to completely disable the Holdings Tab in this case)
 ; marc = Use information in the Marc Record Mapped from [MarcHoldings]
-; custom = use the options in the [Holdings] section below 
+; custom = use the options in the [Holdings] section below
 useHoldings = none
 ;useStatus - Whether or not to use the show item statuses
 ; none = do not show status information
 ; marc = Use information in the Marc Record Mapped from [MarcStatus]
-; custom = use the options in the [Status] section below 
+; custom = use the options in the [Status] section below
 useStatus = none
+;idPrefix - Optional - Prefix added to Solr record IDs managed by this instance of
+; the NoILS driver; needed when using this driver in combination with the
+; MultiBackend driver. When used, the value should usually match one of the keys
+; in the [Drivers] section of MultiBackend.ini followed by a dot.
+;idPrefix = "instance1."
 
 [MarcHoldings]
 ; Used if useHoldings is set to "marc"
diff --git a/config/vufind/PAIA.ini b/config/vufind/PAIA.ini
new file mode 100644
index 0000000000000000000000000000000000000000..38f6e661c4da13f6f16f45ad0a3338f0ea0a3184
--- /dev/null
+++ b/config/vufind/PAIA.ini
@@ -0,0 +1,54 @@
+; The PAIA driver extends the DAIA driver and uses the settings for the DAIA driver.
+; Either copy & paste the settings from DAIA.ini into this PAIA.ini or reference your
+; correctly configured DAIA.ini via [Parent_Config]
+;[Parent_Config]
+;relative_path = DAIA.ini
+
+; PAIA configuration
+[PAIA]
+; base URL of the PAIA server WITH trailing slash
+baseUrl = ""
+
+; Set a PAIA specific timeout in seconds to be used for PAIA http requests (defaults
+; to Zend defaults or as defined in
+; vendor/vufind-org/vufindhttp/src/VuFindHttp/HttpService.php)
+;timeout = 30
+
+; Enable caching for PAIA items (default is false). TTL for cached data will be the
+; same as for DAIA cache (see cacheLifetime setting in DAIA.ini).
+;paiaCache = false
+
+; Driver configuration, usually you can leave it untouched
+
+; Without customization the PAIA driver will offer to place a recall for items with
+; unavailable service loan but set href for loan. The recall will be performed via
+; PAIA request.
+; The pre-defined HMACKeys (id:item_id:doc_id) should suffice to place a recall. No
+; extra fields are allowed (if you need those you might be able to cover this
+; functionality in a custom driver by using PAIA confirm/conditions).
+[Holds]
+; HMACKeys - A list of form element names that will be analyzed for consistency
+; during form processing. Most users should not need to change this setting.
+HMACKeys = "id:item_id:doc_id"
+
+; Without customization the PAIA driver will offer to place a storageretrievalrequest
+; for items with available service loan and set href for loan. The
+; storageretrievalrequest will be performed via PAIA request (technically the same as
+; for recall, but with different frontend templates etc.).
+; The pre-defined HMACKeys (id:item_id:doc_id) should suffice to request an item. No
+; extra fields are allowed (if you need those you might be able to cover this
+; functionality in a custom driver by using PAIA confirm/conditions).
+[StorageRetrievalRequests]
+; HMACKeys - A list of form element names that will be analyzed for consistency
+; during form processing. Most users should not need to change this setting.
+HMACKeys = id:item_id:doc_id
+
+; The PAIA driver supports renewals in MyResearch views. The renewal will be
+; performed via PAIA renew.
+; The pre-defined HMACKeys (id:item_id:doc_id) should suffice to renew an item. No
+; extra fields are allowed (if you need those you might be able to cover this
+; functionality in a custom driver by using PAIA confirm/conditions).
+[Renewals]
+; HMACKeys - A list of form element names that will be analyzed for consistency
+; during form processing. Most users should not need to change this setting.
+HMACKeys = "id:item_id:doc_id"
diff --git a/config/vufind/Pazpar2.ini b/config/vufind/Pazpar2.ini
index f26980c75c1a1c5afc9af633f9ad81a146d1d1dc..7013f8cf695d18d2d31d09e47f1ba96005071dfa 100644
--- a/config/vufind/Pazpar2.ini
+++ b/config/vufind/Pazpar2.ini
@@ -33,8 +33,8 @@ max_query_time  = 60
 ; NOT CURRENTLY SUPPORTED
 
 ; This section defines the sort options available on Pazpar2 search results.
-; Values on the left of the equal sign are Pazpar2 API sort values.  Values 
-; on the right of the equal sign are text that will be run through the 
+; Values on the left of the equal sign are Pazpar2 API sort values.  Values
+; on the right of the equal sign are text that will be run through the
 ; translation module and displayed on screen.
 [Sorting]
 relevance   = sort_relevance
diff --git a/config/vufind/Polaris.ini b/config/vufind/Polaris.ini
index 77d4e088217bdf0b6d96b46627b18c5a76aa2a94..a2802f30c5d9e9f571fdee915f8d009152444a19 100644
--- a/config/vufind/Polaris.ini
+++ b/config/vufind/Polaris.ini
@@ -16,11 +16,11 @@ HMACKeys = item_id:holdtype:level
 defaultRequiredDate = 0:1:0
 
 ; extraHoldFields - A colon-separated list used to display extra visible fields in the
-; place holds form. Supported values are "comments", "requiredByDate" and 
-; "pickUpLocation"  
+; place holds form. Supported values are "comments", "requiredByDate" and
+; "pickUpLocation"
 extraHoldFields = pickUpLocation
 
 ; A Pick Up Location Code used to pre-select the pick up location drop down list and
-; provide a default option if others are not available. Must correspond with one of 
+; provide a default option if others are not available. Must correspond with one of
 ; the Location IDs returned by getPickUpLocations()
 defaultPickUpLocation = "15"
\ No newline at end of file
diff --git a/config/vufind/Primo.ini b/config/vufind/Primo.ini
index 556089cc9648ae9e50b5c6ed599c1c3001d6a18c..c127b1c799b4d9569ac80746a9f6fef3fcf84d34 100644
--- a/config/vufind/Primo.ini
+++ b/config/vufind/Primo.ini
@@ -15,11 +15,11 @@ case_sensitive_bools = true
 ; HTTP timeout
 timeout = 30
 
-; URL of the Primo server. For hosted services typically of the form 
-; http://XYZ.hosted.exlibrisgroup.com:1701 where XYZ is the ID of the Primo 
-; instance (e.g. primoapac01). Use http://mlplus.hosted.exlibrisgroup.com for 
+; URL of the Primo server. For hosted services typically of the form
+; http://XYZ.hosted.exlibrisgroup.com:1701 where XYZ is the ID of the Primo
+; instance (e.g. primoapac01). Use http://mlplus.hosted.exlibrisgroup.com for
 ; MetaLib Plus.
-; You may also enter the full URL including a path to the brief search if necessary. 
+; You may also enter the full URL including a path to the brief search if necessary.
 url = "http://XYZ.hosted.exlibrisgroup.com:1701"
 
 ; This section controls the result limit options for search results. default_limit
diff --git a/config/vufind/RecordCache.ini b/config/vufind/RecordCache.ini
index b010e3b9abbb7c408783d90acc6aefa06046f080..884e9b676035edcd96ce2e857d909297a1ee8b18 100644
--- a/config/vufind/RecordCache.ini
+++ b/config/vufind/RecordCache.ini
@@ -3,12 +3,12 @@
 ; currently only one:
 ;
 ; operatingMode : Set the operating mode of the cache. Valid settings are
-; "disabled" (default setting), "primary" if you want to try to load records from 
-; the cache first, or "fallback" if you want to try to load records only if the 
+; "disabled" (default setting), "primary" if you want to try to load records from
+; the cache first, or "fallback" if you want to try to load records only if the
 ; regular source fails.
 ;
-; Settings can optionally be defined separately for favorites in the [Favorite] 
-; section. Settings from [Default] section are used in any other context and also in 
+; Settings can optionally be defined separately for favorites in the [Favorite]
+; section. Settings from [Default] section are used in any other context and also in
 ; favorites if they don't exist in the [Favorite] section.
 ;
 ; IMPORTANT NOTE: Not all search backends are compatible with caching. If record
diff --git a/config/vufind/Sierra.ini b/config/vufind/Sierra.ini
index 2ba1dba57703b4cf525c5876e0fff61c36407670..547958430b526ce220874ee53fe9f377e86074bd 100644
--- a/config/vufind/Sierra.ini
+++ b/config/vufind/Sierra.ini
@@ -1,5 +1,5 @@
 [Catalog]
-dna_url = sierra-db.library.edu 
+dna_url = sierra-db.library.edu
 dna_port = 1032
 dna_db = iii
 dna_user = username
diff --git a/config/vufind/Summon.ini b/config/vufind/Summon.ini
index bfaac502de6280ecd40a35c033e6c2e36ba4391c..910d04f484948cfe07e4ac5aad5b9ba06f6fce61 100644
--- a/config/vufind/Summon.ini
+++ b/config/vufind/Summon.ini
@@ -134,9 +134,11 @@ Discipline = "Subject Area"
 SubjectTerms = Topic
 Language = Language
 PublicationDate = "adv_search_year"  ; share year string w/advanced search page
+;DatabaseName = "Database"
+;SourceName = "Provider"
 
 ; These facets will be shown above search results if the TopFacets recommendations
-; module is used, as opposed to the [Facets] section, which is shown to the side 
+; module is used, as opposed to the [Facets] section, which is shown to the side
 ; of search results when the SideFacets module is used.
 ;
 ; NOTE: This section is not used by default -- see default_top_recommend setting
@@ -150,6 +152,14 @@ SubjectTerms = "Suggested Topics"
 ; button. This can get configured with the showMore settings.
 ; You can use the * to set a new default setting.
 showMore[*] = 6
+
+; Show more facets in a lightbox (paginated, no limit)
+; If false, facets expand in side bar to show facets up to the above limit
+; If "more", facets expand and offer an option at the bottom to open the lightbox
+; If true, facets immediately open in the lightbox
+showMoreInLightbox[*] = more
+;lightboxLimit = 50 ; page size for the lightbox
+
 ; Or you can set a facet specific value by using the facet name as index.
 ;showMore['ContentType'] = 10
 ; Rows and columns for table used by top facets
@@ -216,9 +226,9 @@ orFacets = *
 
 ; This section shows which search types will display in the basic search box at
 ; the top of Summon pages.  The name of each setting below corresponds with an
-; index defined in the Summon API.  The value of each setting is the text to 
-; display on screen.  All on-screen text will be run through the translator, so 
-; be sure to update language files if necessary.  The order of these settings 
+; index defined in the Summon API.  The value of each setting is the text to
+; display on screen.  All on-screen text will be run through the translator, so
+; be sure to update language files if necessary.  The order of these settings
 ; will be maintained in the drop-down list in the UI.
 ;
 ; Note: The search type of "AllFields" is a special case that searches all
@@ -248,9 +258,9 @@ TableOfContents     = adv_search_toc
 
 ; This section defines the sort options available on Summon search results.
 ; Values on the left of the equal sign are either the reserved term "relevance"
-; or the name of a Summon index to use for sorting; asc and desc modifiers may be 
-; used in combination with index names, but not relevance.  Values on the right 
-; of the equal sign are text that will be run through the translation module and 
+; or the name of a Summon index to use for sorting; asc and desc modifiers may be
+; used in combination with index names, but not relevance.  Values on the right
+; of the equal sign are text that will be run through the translation module and
 ; displayed on screen.
 [Sorting]
 relevance = sort_relevance
@@ -273,3 +283,16 @@ next_prev_navigation = false
 ;[Views]
 ;list = List
 ;grid = Grid
+
+; This section controls what happens when a record title in a search result list
+; is clicked. VuFind can either embed the full result directly in the list using
+; AJAX or can display it at its own separate URL as a full HTML page.
+; full - separate page (default)
+; tabs - embedded using tabs (see record/tabs.phtml)
+; accordion - embedded using an accordion (see record/accordion.phtml)
+; NOTE: To turn this feature on for favorite lists, see the lists_view setting
+; in the [Social] section of config.ini.
+; NOTE: This feature is incompatible with SyndeticsPlus content; please use
+;       regular Syndetics if necessary.
+[List]
+view=full
diff --git a/config/vufind/Symphony.ini b/config/vufind/Symphony.ini
index 03e90e007e8574d3836e1bf2bf4251bbe4291e48..c65dc48028f1aad4d0a8f629893c95509df356dc 100644
--- a/config/vufind/Symphony.ini
+++ b/config/vufind/Symphony.ini
@@ -9,8 +9,8 @@ baseURL = http://localhost:8080/symws
 clientID = VuFind
 ; Installations using the 'Always Require Authentication' option
 ; must provide a valid login and password for anonymous operations.
-;user = 
-;password = 
+;user =
+;password =
 ; You can provide options to be provided to SoapClients here, e.g.
 ;soapOptions[proxy_host] = proxy.example.edu
 
@@ -54,13 +54,13 @@ showStaffNotes = true
 ;showFeeType - Determines the type of fees that are shown.
 ;              Supported values are: UNPAID_FEES, PAID_FEES, ALL_FEES
 showFeeType = ALL_FEES
-;usernameField - Determines from which field from Symphony to populate the 
+;usernameField - Determines from which field from Symphony to populate the
 ;                patron's username in Vufind's user DB. ILS_username_field must be
 ;                to "id" in config.ini for this to work.
 ;                Supported values are: userKey, userID
 usernameField = userID
 ;userProfileGroupField - Determines from which field to populate the "Group" in "My Profile".
-;                        Supported values are: GROUP_ID, USER_PROFILE_ID, 
+;                        Supported values are: GROUP_ID, USER_PROFILE_ID,
 ;                                              PATRON_LIBRARY_ID, DEPARTMENT
 userProfileGroupField = USER_PROFILE_ID
 
@@ -78,11 +78,11 @@ HMACKeys = item_id:holdtype:level
 defaultRequiredDate = 0:1:1
 
 ; extraHoldFields - A colon-separated list used to display extra visible fields in the
-; place holds form. Supported values are "comments", "requiredByDate" and 
-; "pickUpLocation"  
+; place holds form. Supported values are "comments", "requiredByDate" and
+; "pickUpLocation"
 extraHoldFields = comments:requiredByDate:pickUpLocation
 
 ; A Pick Up Location Code used to pre-select the pick up location drop down list and
-; provide a default option if others are not available. Must correspond with one of 
+; provide a default option if others are not available. Must correspond with one of
 ; the Location IDs returned by getPickUpLocations()
 ;defaultPickUpLocation = MAIN
diff --git a/config/vufind/Unicorn.ini b/config/vufind/Unicorn.ini
index 711347353f5cf0670e620214e45a3e2a3815a61c..be40f05f5a33ae31a96fde4f19e1d2c4e34b5604 100644
--- a/config/vufind/Unicorn.ini
+++ b/config/vufind/Unicorn.ini
@@ -6,7 +6,7 @@
 ; specifying url this way is more convenient
 url = http://your-sirsi-web-server/pathto/driver.pl
 ; Unicorn/Symphony returns the fines amounts in cents,
-; set this parameter to "true" to leave the fines 
+; set this parameter to "true" to leave the fines
 ; amounts in cents instead of dollars, or to "false"
 ; to convert the amounts to dollars
 leaveFinesAmountsInCents = true
@@ -39,11 +39,11 @@ HMACKeys = item_id
 defaultRequiredDate = 0:1:0
 
 ; extraHoldFields - A colon-separated list used to display extra visible fields in the
-; place holds form. Supported values are "comments", "requiredByDate" and 
-; "pickUpLocation"  
+; place holds form. Supported values are "comments", "requiredByDate" and
+; "pickUpLocation"
 extraHoldFields = requiredByDate:pickUpLocation:comments
 ;
-; The following are lists of Location Codes and Item Types 
+; The following are lists of Location Codes and Item Types
 ; for items that are NOT AVAILABLE even if they are NOT checked out.
 ; The values on the right side of "=" is the status message to display.
 ;
diff --git a/config/vufind/Virtua.ini b/config/vufind/Virtua.ini
index 6af4bdca53837a2cee4a3ce3302c787f4a6775c7..9c5d365ec147c975b68e45b8a41a00bf9a3bf76e 100644
--- a/config/vufind/Virtua.ini
+++ b/config/vufind/Virtua.ini
@@ -3,7 +3,7 @@
 host        = virtuadb.your.library.server
 ; Your iportal server
 webhost     = virtuaweb.your.library.server
-; Your iportal cgi token string 
+; Your iportal cgi token string
 ; Example, from "http://webvirtua.libexample.edu/cgi-bin/gw/chameleon"
 ; you get "cgi-bin/gw", default is "cgi-bin"
 cgi_token   = "cgi-bin"
@@ -18,5 +18,5 @@ service     = VTLS01
 ; Login details
 user        = username
 password    = password
-; System language (en -> English, pt -> Portuguese, fr -> French; default = en) 
+; System language (en -> English, pt -> Portuguese, fr -> French; default = en)
 language    = en
\ No newline at end of file
diff --git a/config/vufind/Voyager.ini b/config/vufind/Voyager.ini
index 197509fda4843a1c930a5292d9aeb4cf8b474c24..49f0b8027216106a4ddd8f48617cdd2485ce07ee 100644
--- a/config/vufind/Voyager.ini
+++ b/config/vufind/Voyager.ini
@@ -42,6 +42,15 @@ login_field = LAST_NAME
 ; fund tree to filter out unwanted values.  Leave it commented out to get all funds.
 ;parent_fund = 0
 
+; This section controls call slip behavior (storage retrieval requests in VuFind).
+[StorageRetrievalRequests]
+; Colon-separated list of call slip statuses (see CALL_SLIP_STATUS_TYPE table in
+; Voyager's database) that control whether a call slip is displayed. Only slips
+; matching statuses in this list will be displayed. Current Voyager versions (as of
+; May 2016) don't immediately delete a call slip from the database when it has been
+; processed and converted to a hold, which may confuse users.
+;display_statuses = 1:2:3:5:8:9
+
 ; Settings for controlling how holdings are displayed
 [Holdings]
 ; How purchase history is displayed. Supported values are:
diff --git a/config/vufind/VoyagerRestful.ini b/config/vufind/VoyagerRestful.ini
index 527b90be11ebff7051d1340eb942c8012a4395d3..bc960bce839c5235cc736be5238516db95a61a3f 100644
--- a/config/vufind/VoyagerRestful.ini
+++ b/config/vufind/VoyagerRestful.ini
@@ -218,6 +218,13 @@ disableAvailabilityCheckForRequestGroups = "15:19:21:32"
 ;helpText = "Help text for all languages."
 ;helpText[en-gb] = "Help text for English language."
 
+; Colon-separated list of call slip statuses (see CALL_SLIP_STATUS_TYPE table in
+; Voyager's database) that control whether a call slip is displayed. Only slips
+; matching statuses in this list will be displayed. Current Voyager versions (as of
+; May 2016) don't immediately delete a call slip from the database when it has been
+; processed and converted to a hold, which may confuse users.
+;display_statuses = 1:2:3:5:8:9
+
 ; This section controls UB (Universal Borrowing, ILL in VuFind) behavior. To enable,
 ; uncomment (at minimum) the HMACKeys and extraFields settings below. See also
 ; section ILLRequestSources for mapping between patron ID's and UB libraries.
@@ -292,3 +299,8 @@ show_statuses = "/lost|missing|claim/i"
 ; PIN change parameters. The default limits are taken from the interface documentation.
 ;minLength = 5
 ;maxLength = 12
+; See the password_pattern/password_hint settings in the [Authentication] section
+; of config.ini for notes on these settings. When set here, these will override the
+; config.ini defaults when Voyager is used for authentication.
+;pattern = "alphanumeric"
+;hint = "Your optional custom hint can go here."
\ No newline at end of file
diff --git a/config/vufind/VuDL.ini b/config/vufind/VuDL.ini
deleted file mode 100644
index ebebf062ca184e8effbc7e835aefaefc82deac63..0000000000000000000000000000000000000000
--- a/config/vufind/VuDL.ini
+++ /dev/null
@@ -1,51 +0,0 @@
-[General]
-title = "VuDL";
-
-[Fedora]
-adminUser = fedoraAdmin
-adminPass = fedoraAdmin
-page_length = 16
-url_base = http://link.to.vudl.fedora:port/fedora/objects/
-query_url = http://link.to.vudl.fedora:port/fedora/risearch
-root_id = root:id
-
-[Licenses]
-creativecommons.org = CC
-
-; This section determines which fields from
-; Solr are presented in the VuDL sidebar
-[Details]
-; field = Title
-author,author2        = Author
-dc_collection_str_mv  = Collection
-dc_contributor_str_mv = Contributor
-description           = Description
-first_indexed         = Date Added
-format                = Format
-language              = Language
-publishDate           = Publish Year
-publisher             = Publisher
-series                = Series
-dc_source_str_mv      = Source 
-title                 = Title
-title_alt             = Alternate Title
-topic                 = Topic
-
-[Routes]
-tiff  = page
-flac = audio
-mp3 = audio
-mpeg = audio
-octet-stream = audio
-ogg = audio
-x-flac = audio
-mp4 = video
-ogv = video
-webmv = video
-pdf = download
-msword = download
-
-[Access]
-ip_range[] = 127.0.0.1
-ip_range[] = "::1"
-proxy_url = "http://ezproxy.myuniversity.edu/login?url="
diff --git a/config/vufind/WorldCat.ini b/config/vufind/WorldCat.ini
index 33dc8d29a9f3b6e71f34236012730c32ae1c6fc2..d8fce1f8e448e3471a8ee6405af189b8bd0be1fb 100644
--- a/config/vufind/WorldCat.ini
+++ b/config/vufind/WorldCat.ini
@@ -9,9 +9,9 @@ default_sort         = relevance
 ; This section shows which search types will display in the basic search box at
 ; the top of WorldCat pages.  The name of each setting below corresponds with one
 ; or more indices defined in the WorldCat API (multiple values are separated by
-; colons).  The value of each setting is the text to display on screen.  All 
-; on-screen text will be run through the translator, so be sure to update language 
-; files if necessary.  The order of these settings will be maintained in the 
+; colons).  The value of each setting is the text to display on screen.  All
+; on-screen text will be run through the translator, so be sure to update language
+; files if necessary.  The order of these settings will be maintained in the
 ; drop-down list in the UI.
 ;
 ; For a complete list of legal values, see the SRU Explain page here:
@@ -38,8 +38,8 @@ srw.se                  = adv_search_series
 srw.yr                  = adv_search_year
 
 ; This section defines the sort options available on WorldCat search results.
-; Values on the left of the equal sign are WorldCat API sort values.  Values 
-; on the right of the equal sign are text that will be run through the 
+; Values on the left of the equal sign are WorldCat API sort values.  Values
+; on the right of the equal sign are text that will be run through the
 ; translation module and displayed on screen.
 [Sorting]
 relevance   = sort_relevance
@@ -56,3 +56,16 @@ Title       = sort_title
 next_prev_navigation = false
 
 related[] = "WorldCatSimilar"
+
+; This section controls what happens when a record title in a search result list
+; is clicked. VuFind can either embed the full result directly in the list using
+; AJAX or can display it at its own separate URL as a full HTML page.
+; full - separate page (default)
+; tabs - embedded using tabs (see record/tabs.phtml)
+; accordion - embedded using an accordion (see record/accordion.phtml)
+; NOTE: To turn this feature on for favorite lists, see the lists_view setting
+; in the [Social] section of config.ini.
+; NOTE: This feature is incompatible with SyndeticsPlus content; please use
+;       regular Syndetics if necessary.
+[List]
+view=full
diff --git a/config/vufind/XCNCIP2.ini b/config/vufind/XCNCIP2.ini
index 76be7e34c7e0a19adf82f32dbb85d604325efe35..f94e3f5ce613accd4967fd236e5fd53e9196cd5d 100644
--- a/config/vufind/XCNCIP2.ini
+++ b/config/vufind/XCNCIP2.ini
@@ -5,16 +5,16 @@ url         = http://myuniversity.edu:8080/ncipv2/NCIPResponder
 ; Your library's Agency ID (ILSDefaultAgency setting in driver_config.properties):
 agency      = "My University"
 
-; Pickup location definitions: CSV file 
+; Pickup location definitions: CSV file
 ;
 ;     Format: [agency],[locationID],[locationDisplay]
 ;
-;     e.g., 
+;     e.g.,
 ;         (for consortium=false)
 ;         My University,1,Main Circulation Desk
 ;         My University,2,Stacks
 ;
-;     e.g., 
+;     e.g.,
 ;         (for consortium=true)
 ;         Agency1,1,Agency1 - Main Circulation Desk
 ;         Agency1,2,Agency1 - Stacks
diff --git a/config/vufind/author-classification.ini b/config/vufind/author-classification.ini
index 50d5eb6fb6253ab5f5ee4c5dfeb6d975b517ca02..3708e210bcf8850b1b44b176e8cd49d2d8ab9b66 100644
--- a/config/vufind/author-classification.ini
+++ b/config/vufind/author-classification.ini
@@ -11,3 +11,581 @@ nonCreativeRoles =  anl,app,arc,asg,asn,att,auc,aqt,ant,bnd,bdd,blw,bkd,bkp,bsl,
 ;firstAuthorRoles = bearbeiter,bearb,komponist,komp,urheber,verfasser,verf
 ;secondAuthorRoles = herausgeber,hg,hrsg,illustrator,ill,mitarbeiter,mitarb,übersetzer,übers
 ;nonCreativeRoles = begründer,begr,redakteur,red
+
+; This section maps codes used in the [AuthorRoles] lists to fuller forms. You may
+; use pipe characters (|) to separate multiple synonyms for the same code. Synonyms
+; serve several important purposes:
+;
+; 1. They allow the [AuthorRoles] lists to be shorter and more readable. When
+; evaluating the lists, these synonyms will always be taken into account.
+;
+; 2. By default, the synonym list will be used to map alternate forms to standard
+; codes during index time to allow standardized display and translation. Note that
+; synonyms on the right side of the equal sign will be matched in a case-insensitive,
+; punctuation-independent way. However, when values are mapped to codes, the values
+; on the left side of the equal sign will be stored in the Solr index exactly as
+; presented here, case and all. This allows fuzzy matching to catch the most matches
+; while yielding a very precise code for exact translation lookup. IMPORTANT: If you
+; wish to index raw, unnormalized values instead, the indexing configuration can be
+; adjusted to disable this mapping behavior; see $VUFIND_HOME/import/marc.properties
+; for more details.
+;
+; 3. For the purposes of identifying "unknown relators," all keys and values in this
+; list are examined. Any relator term not found anywhere in this section is
+; considered "unknown" and treated accordingly.
+[RelatorSynonyms]
+abr = "Abridger"
+acp = "Art copyist"
+act = "Actor"
+adi = "Art director"
+adp = "Adapter"
+aft = "Author of afterword, colophon, etc."
+anl = "Analyst"
+anm = "Animator"
+ann = "Annotator"
+ant = "Bibliographic antecedent"
+ape = "Appellee"
+apl = "Appellant"
+app = "Applicant"
+aqt = "Author in quotations or text abstracts"
+arc = "Architect"
+ard = "Artistic director"
+arr = "Arranger"
+art = "Artist"
+asg = "Assignee"
+asn = "Associated name"
+ato = "Autographer"
+att = "Attributed name"
+auc = "Auctioneer"
+aud = "Author of dialog"
+aui = "Author of introduction, etc."
+aus = "Screenwriter"
+aut = "Author"
+bdd = "Binding designer"
+bjd = "Bookjacket designer"
+bkd = "Book designer"
+bkp = "Book producer"
+blw = "Blurb writer"
+bnd = "Binder"
+bpd = "Bookplate designer"
+brd = "Broadcaster"
+brl = "Braille embosser"
+bsl = "Bookseller"
+cas = "Caster"
+ccp = "Conceptor"
+chr = "Choreographer"
+clb = "Collaborator"
+cli = "Client"
+cll = "Calligrapher"
+clr = "Colorist"
+clt = "Collotyper"
+cmm = "Commentator"
+cmp = "Composer"
+cmt = "Compositor"
+cnd = "Conductor"
+cng = "Cinematographer"
+cns = "Censor"
+coe = "Contestant-appellee"
+col = "Collector"
+com = "Compiler"
+con = "Conservator"
+cor = "Collection registrar"
+cos = "Contestant"
+cot = "Contestant-appellant"
+cou = "Court governed"
+cov = "Cover designer"
+cpc = "Copyright claimant"
+cpe = "Complainant-appellee"
+cph = "Copyright holder"
+cpl = "Complainant"
+cpt = "Complainant-appellant"
+cre = "Creator"
+crp = "Correspondent"
+crr = "Corrector"
+crt = "Court reporter"
+csl = "Consultant"
+csp = "Consultant to a project"
+cst = "Costume designer"
+ctb = "Contributor"
+cte = "Contestee-appellee"
+ctg = "Cartographer"
+ctr = "Contractor"
+cts = "Contestee"
+ctt = "Contestee-appellant"
+cur = "Curator"
+cwt = "Commentator for written text"
+dbp = "Distribution place"
+dfd = "Defendant"
+dfe = "Defendant-appellee"
+dft = "Defendant-appellant"
+dgg = "Degree granting institution"
+dgs = "Degree supervisor"
+dir = "Conductor"
+dis = "Dissertant"
+dln = "Delineator"
+dnc = "Dancer"
+dnr = "Donor"
+dpc = "Depicted"
+dpt = "Depositor"
+drm = "Draftsman"
+drt = "Director"
+dsr = "Designer"
+dst = "Distributor"
+dtc = "Data contributor"
+dte = "Dedicatee"
+dtm = "Data manager"
+dto = "Dedicator"
+dub = "Dubious author"
+edc = "Editor of compilation"
+edm = "Editor of moving image work"
+edt = "Editor"
+egr = "Engraver"
+elg = "Electrician"
+elt = "Electrotyper"
+eng = "Engineer"
+enj = "Enacting jurisdiction"
+etr = "Etcher"
+evp = "Event place"
+exp = "Expert"
+fac = "Facsimilist"
+fds = "Film distributor"
+fld = "Field director"
+flm = "Film editor"
+fmd = "Film director"
+fmk = "Filmmaker"
+fmo = "Former owner"
+fmp = "Film producer"
+fnd = "Funder"
+fpy = "First party"
+frg = "Forger"
+gis = "Geographic information specialist"
+grt = "Graphic technician"
+his = "Host institution"
+hnr = "Honoree"
+hst = "Host"
+Ill = "Illustrator"
+ill = "Illustrator"
+ilu = "Illuminator"
+ins = "Inscriber"
+inv = "Inventor"
+isb = "Issuing body"
+itr = "Instrumentalist"
+ive = "Interviewee"
+ivr = "Interviewer"
+jud = "Judge"
+jug = "Jurisdiction governed"
+kad = "Cadential author"
+lbr = "Laboratory"
+lbt = "Librettist"
+ldr = "Laboratory director"
+led = "Lead"
+lee = "Libelee-appellee"
+lel = "Libelee"
+len = "Lender"
+let = "Libelee-appellant"
+lgd = "Lighting designer"
+lie = "Libelant-appellee"
+lil = "Libelant"
+lit = "Libelant-appellant"
+lsa = "Landscape architect"
+lse = "Licensee"
+lso = "Licensor"
+ltg = "Lithographer"
+lyr = "Lyricist"
+mcp = "Music copyist"
+mdc = "Metadata contact"
+med = "Medium"
+mfp = "Manufacture place"
+mfr = "Manufacturer"
+mod = "Moderator"
+mon = "Monitor"
+mrb = "Marbler"
+mrk = "Markup editor"
+msd = "Musical director"
+mte = "Metal-engraver"
+mtk = "Minute taker"
+mus = "Musician"
+nrt = "Narrator"
+opn = "Opponent"
+org = "Originator"
+orm = "Organizer"
+osp = "Onscreen presenter"
+oth = "Other"
+own = "Owner"
+pan = "Panelist"
+pat = "Patron"
+pbd = "Publishing director"
+pbl = "Publisher"
+pdr = "Project director"
+pfr = "Proofreader"
+pht = "Photographer"
+plt = "Platemaker"
+pma = "Permitting agency"
+pmn = "Production manager"
+pop = "Printer of plates"
+ppm = "Papermaker"
+ppt = "Puppeteer"
+pra = "Praeses"
+prc = "Process contact"
+prd = "Production personnel"
+pre = "Presenter"
+prf = "Performer"
+prg = "Programmer"
+prm = "Printmaker"
+prn = "Production company"
+pro = "Producer"
+prp = "Production place"
+prs = "Production designer"
+prt = "Printer"
+prv = "Provider"
+pta = "Patent applicant"
+pte = "Plaintiff-appellee"
+ptf = "Plaintiff"
+pth = "Patent holder"
+ptt = "Plaintiff-appellant"
+pup = "Publication place"
+rbr = "Rubricator"
+rcd = "Recordist"
+rce = "Recording engineer"
+rcp = "Addressee"
+rdd = "Radio director"
+red = "Redactor"
+ren = "Renderer"
+res = "Researcher"
+rev = "Reviewer"
+rpc = "Radio producer"
+rps = "Repository"
+rpt = "Reporter"
+rpy = "Responsible party"
+rse = "Respondent-appellee"
+rsg = "Restager"
+rsp = "Respondent"
+rsr = "Restorationist"
+rst = "Respondent-appellant"
+rth = "Research team head"
+rtm = "Research team member"
+sad = "Scientific advisor"
+sce = "Scenarist"
+scl = "Sculptor"
+scr = "Scribe"
+sds = "Sound designer"
+sec = "Secretary"
+sgd = "Stage director"
+sgn = "Signer"
+sht = "Supporting host"
+sll = "Seller"
+sng = "Singer"
+spk = "Speaker"
+spn = "Sponsor"
+spy = "Second party"
+srv = "Surveyor"
+std = "Set designer"
+stg = "Setting"
+stl = "Storyteller"
+stm = "Stage manager"
+stn = "Standards body"
+str = "Stereotyper"
+tcd = "Technical director"
+tch = "Teacher"
+ths = "Thesis advisor"
+tld = "Television director"
+tlp = "Television producer"
+trc = "Transcriber"
+trl = "Translator"
+tyd = "Type designer"
+tyg = "Typographer"
+uvp = "University place"
+vac = "Voice actor"
+vdg = "Videographer"
+voc = "Vocalist"
+wac = "Writer of added commentary"
+wal = "Writer of added lyrics"
+wam = "Writer of accompanying material"
+wat = "Writer of added text"
+wdc = "Woodcutter"
+wde = "Wood engraver"
+win = "Writer of introduction"
+wit = "Witness"
+wpr = "Writer of preface"
+wst = "Writer of supplementary textual content"
+
+; This section is an example of relator synonyms with German and English terms
+; combined. Mapping of German terms is based on the DNB's RDA Arbeitshilfe AH-017
+; (revision 1.5 dated 2016-07-27). You can uncomment it and remove the above
+; English-only section as needed.
+;[RelatorSynonyms]
+;abr = "Abridger|Kürzender"
+;acp = "Art copyist"
+;act = "Actor|Schauspieler|SchauspielerIn"
+;adi = "Art director"
+;adp = "Adapter"
+;aft = "Author of afterword, colophon, etc.|Verfasser eines Nachworts|VerfasserIn eines Nachworts"
+;anl = "Analyst"
+;anm = "Animator|Trickfilmzeichner|TrickfilmzeichnerIn"
+;ann = "Annotator|annotierende Person"
+;ant = "Bibliographic antecedent"
+;ape = "Appellee|Berufungsbeklagter|Revisionsbeklagter"
+;apl = "Appellant|Berufungskläger|BerufungsklägerIn|Revisionskläger|RevisionsklägerIn"
+;app = "Applicant"
+;aqt = "Author in quotations or text abstracts"
+;arc = "Architect|Architekt|ArchitektIn"
+;ard = "Artistic director"
+;arr = "Arranger|Arrangeur|ArrangeurIn"
+;art = "Artist|Künstler|KünstlerIn"
+;asg = "Assignee"
+;asn = "Associated name"
+;ato = "Autographer|Unterzeichner|UnterzeichnerIn"
+;att = "Attributed name"
+;auc = "Auctioneer"
+;aud = "Author of dialog"
+;aui = "Author of introduction, etc.|Verfasser eines Geleitwortes|VerfasserIn eines Geleitwortes"
+;aus = "Screenwriter|Drehbuchautor|DrehbuchautorIn"
+;aut = "Author|Verfasser|VerfasserIn"
+;bdd = "Binding designer"
+;bjd = "Bookjacket designer"
+;bkd = "Book designer|Buchgestalter|BuchgestalterIn"
+;bkp = "Book producer"
+;blw = "Blurb writer"
+;bnd = "Binder|Buchbinder|BuchbinderIn"
+;bpd = "Bookplate designer"
+;brd = "Broadcaster|Sender"
+;brl = "Braille embosser|Brailleschriftpräger|BrailleschriftprägerIn"
+;bsl = "Bookseller"
+;cas = "Caster|Formgießer|FormgießerIn"
+;ccp = "Conceptor"
+;chr = "Choreographer|Choreograph|ChoreographIn"
+;clb = "Collaborator"
+;cli = "Client"
+;cll = "Calligrapher|Kalligraf|KalligrafIn"
+;clr = "Colorist|Colorist|ColoristIn"
+;clt = "Collotyper|Lichtdrucker|LichtdruckerIn"
+;cmm = "Commentator|Kommentator|KommentatorIn"
+;cmp = "Composer|Komponist|KomponistIn"
+;cmt = "Compositor"
+;cnd = "Conductor|Dirigent|DirigentIn"
+;cng = "Cinematographer|Bildregisseur|BildregisseurIn"
+;cns = "Censor|Zensor|ZensorIn"
+;coe = "Contestant-appellee"
+;col = "Collector|Sammler|SammlerIn"
+;com = "Compiler|Zusammenstellender"
+;con = "Conservator"
+;cor = "Collection registrar|Registrar"
+;cos = "Contestant"
+;cot = "Contestant-appellant"
+;cou = "Court governed|Durch Verfahrensvorschriften geregeltes Gericht"
+;cov = "Cover designer"
+;cpc = "Copyright claimant"
+;cpe = "Complainant-appellee"
+;cph = "Copyright holder"
+;cpl = "Complainant"
+;cpt = "Complainant-appellant"
+;cre = "Creator|Geistiger Schöpfer|GeistigeR SchöpferIn|Buchkünstler|BuchkünstlerIn|Berichterstatter|BerichterstatterIn"
+;crp = "Correspondent"
+;crr = "Corrector"
+;crt = "Court reporter|Gerichtsstenograf|GerichtsstenografIn"
+;csl = "Consultant|Berater|BeraterIn"
+;csp = "Consultant to a project"
+;cst = "Costume designer|Kostümbildner|KostümbildnerIn"
+;ctb = "Contributor|Mitwirkender|Letterer|Maskenbildner|MaskenbildnerIn|On-Screen-Teilnehmer|On-Screen-TeilnehmerIn|Orchesterleiter|OrchesterleiterIn|Chorleiter|ChorleiterIn|Softwareentwickler|SoftwareentwicklerIn|Special-effects-Provider|Visual-effects-Provider|Verfasser eines Postscriptums|VerfasserIn eines Postscriptums"
+;cte = "Contestee-appellee"
+;ctg = "Cartographer|Kartograf|KartografIn"
+;ctr = "Contractor|Vertragspartner|VertragspartnerIn"
+;cts = "Contestee"
+;ctt = "Contestee-appellant"
+;cur = "Curator|Kurator|KuratorIn"
+;cwt = "Commentator for written text"
+;dbp = "Distribution place"
+;dfd = "Defendant|Angeklagter|Beklagter"
+;dfe = "Defendant-appellee"
+;dft = "Defendant-appellant"
+;dgg = "Degree granting institution|Grad-verleihende Institution"
+;dgs = "Degree supervisor|Akademischer Betreuer|AkademischeR BetreuerIn"
+;dir = "Conductor"
+;dis = "Dissertant"
+;dln = "Delineator"
+;dnc = "Dancer|Tänzer|TänzerIn"
+;dnr = "Donor|Stifter|StifterIn"
+;dpc = "Depicted"
+;dpt = "Depositor|Leihgeber|LeihgeberIn"
+;drm = "Draftsman|Technischer Zeichner|Technischer ZeichnerIn"
+;drt = "Director|Regisseur|RegisseurIn"
+;dsr = "Designer|DesignerIn"
+;dst = "Distributor|Vertrieb"
+;dtc = "Data contributor"
+;dte = "Dedicatee|Widmungsempfänger|WidmungsempfängerIn"
+;dtm = "Data manager"
+;dto = "Dedicator|Widmender"
+;dub = "Dubious author"
+;edc = "Editor of compilation"
+;edm = "Editor of moving image work|Cutter|CutterIn"
+;edt = "Editor|Herausgeber|HerausgeberIn"
+;egr = "Engraver|Stecher|StecherIn"
+;elg = "Electrician"
+;elt = "Electrotyper"
+;eng = "Engineer"
+;enj = "Enacting jurisdiction|Normerlassende Gebietskörperschaft"
+;etr = "Etcher|Radierer"
+;evp = "Event place"
+;exp = "Expert"
+;fac = "Facsimilist"
+;fds = "Film distributor|Filmvertrieb"
+;fld = "Field director"
+;flm = "Film editor"
+;fmd = "Film director|Filmregisseur|FilmregisseurIn"
+;fmk = "Filmmaker|Filmemacher|FilmemacherIn"
+;fmo = "Former owner|früherer Eigentümer|frühereR EigentümerIn"
+;fmp = "Film producer|Filmproduzent|FilmproduzentIn"
+;fnd = "Funder"
+;fpy = "First party"
+;frg = "Forger"
+;gis = "Geographic information specialist"
+;grt = "Graphic technician"
+;hg = "Publisher"
+;his = "Host institution|Gastgebende Institution"
+;hnr = "Honoree|Gefeierter"
+;hst = "Host|Gastgeber"
+;Ill = "Illustrator"
+;ill = "Illustrator|Illustrator|IllustratorIn"
+;ilu = "Illuminator|Buchmaler|BuchmalerIn"
+;ins = "Inscriber|Beschriftende Person"
+;inv = "Inventor|Erfinder|ErfinderIn"
+;isb = "Issuing body|Herausgebendes Organ"
+;itr = "Instrumentalist|Instrumentalmusiker|InstrumentalmusikerIn"
+;ive = "Interviewee|Interviewter"
+;ivr = "Interviewer|InterviewerIn"
+;jud = "Judge|Richter|RichterIn"
+;jug = "Jurisdiction governed|Geregelte Gebietskörperschaft"
+;kad = "Cadential author"
+;lbr = "Laboratory"
+;lbt = "Librettist|Librettist|LibrettistIn"
+;ldr = "Laboratory director"
+;led = "Lead"
+;lee = "Libelee-appellee"
+;lel = "Libelee"
+;len = "Lender"
+;let = "Libelee-appellant"
+;lgd = "Lighting designer"
+;lie = "Libelant-appellee"
+;lil = "Libelant"
+;lit = "Libelant-appellant"
+;lsa = "Landscape architect|Landschaftsarchitekt|LandschaftsarchitektIn"
+;lse = "Licensee"
+;lso = "Licensor"
+;ltg = "Lithographer|Lithograph|LithographIn"
+;lyr = "Lyricist|Textdichter|TextdichterIn"
+;mcp = "Music copyist"
+;mdc = "Metadata contact"
+;med = "Medium"
+;mfp = "Manufacture place"
+;mfr = "Manufacturer|Hersteller|HerstellerIn"
+;mod = "Moderator|ModeratorIn"
+;mon = "Monitor"
+;mrb = "Marbler"
+;mrk = "Markup editor"
+;msd = "Musical director|Musikalischer Leiter|Musikalischer LeiterIn"
+;mte = "Metal-engraver"
+;mtk = "Minute taker|Protokollant|ProtokollantIn"
+;mus = "Musician"
+;nrt = "Narrator|Erzähler|ErzählerIn"
+;opn = "Opponent"
+;org = "Originator"
+;orm = "Organizer|Veranstalter|VeranstalterIn"
+;osp = "Onscreen presenter|On-screen Präsentator|On-screen PräsentatorIn"
+;oth = "Other|Sonstige Person, Familie oder Körperschaft, die mit einem Exemplar in Verbindung steht|Sonstige Person, Familie oder Körperschaft, die mit einem Werk in Verbindung steht|Begründer des Werks|BegründerIn des Werks"
+;own = "Owner|Eigentümer|EigentümerIn|gegenwärtiger Eigentümer|gegenwärtigeR EigentümerIn"
+;pan = "Panelist|Diskussionsteilnehmer|DiskussionsteilnehmerIn"
+;pat = "Patron|Auftraggeber|AuftraggeberIn"
+;pbd = "Publishing director|Chefredakteur|ChefredakteurIn"
+;pbl = "Publisher|Verlag"
+;pdr = "Project director"
+;pfr = "Proofreader"
+;pht = "Photographer|Fotograf|FotografIn"
+;plt = "Platemaker|Druckformhersteller|DruckformherstellerIn"
+;pma = "Permitting agency"
+;pmn = "Production manager"
+;pop = "Printer of plates"
+;ppm = "Papermaker|Papiermacher|PapiermacherIn"
+;ppt = "Puppeteer|Puppenspieler|PuppenspielerIn"
+;pra = "Praeses"
+;prc = "Process contact"
+;prd = "Production personnel"
+;pre = "Presenter|Präsentator|PräsentatorIn"
+;prf = "Performer|Ausführender"
+;prg = "Programmer|Programmierer|ProgrammiererIn"
+;prm = "Printmaker|Druckgrafiker|DruckgrafikerIn"
+;prn = "Production company|Produktionsfirma"
+;pro = "Producer|Produzent|ProduzentIn"
+;prp = "Production place"
+;prs = "Production designer|Szenenbildner|SzenenbildnerIn"
+;prt = "Printer|Drucker|DruckerIn"
+;prv = "Provider"
+;pta = "Patent applicant"
+;pte = "Plaintiff-appellee"
+;ptf = "Plaintiff|Zivilkläger|ZivilklägerIn"
+;pth = "Patent holder"
+;ptt = "Plaintiff-appellant"
+;pup = "Publication place"
+;rbr = "Rubricator"
+;rcd = "Recordist|Tonmeister|TonmeisterIn"
+;rce = "Recording engineer|Toningenieur|ToningenieurIn"
+;rcp = "Addressee|Adressat"
+;rdd = "Radio director|Hörfunkregisseur|HörfunkregisseurIn"
+;red = "Redactor|Red"
+;ren = "Renderer"
+;res = "Researcher|Forscher|ForscherIn"
+;rev = "Reviewer"
+;rpc = "Radio producer|Hörfunkproduzent|HörfunkproduzentIn"
+;rps = "Repository"
+;rpt = "Reporter"
+;rpy = "Responsible party"
+;rse = "Respondent-appellee"
+;rsg = "Restager"
+;rsp = "Respondent|RespondentIn"
+;rsr = "Restorationist|Restaurator|RestauratorIn"
+;rst = "Respondent-appellant"
+;rth = "Research team head"
+;rtm = "Research team member"
+;sad = "Scientific advisor"
+;sce = "Scenarist"
+;scl = "Sculptor|Bildhauer|BildhauerIn"
+;scr = "Scribe"
+;sds = "Sound designer"
+;sec = "Secretary"
+;sgd = "Stage director|Bühnenregisseur|BühnenregisseurIn"
+;sgn = "Signer"
+;sht = "Supporting host"
+;sll = "Seller|Verkäufer|VerkäuferIn"
+;sng = "Singer|Sänger|SängerIn"
+;spk = "Speaker|Redner|RednerIn"
+;spn = "Sponsor|SponsorIn"
+;spy = "Second party"
+;srv = "Surveyor|Landvermesser|LandvermesserIn"
+;std = "Set designer"
+;stg = "Setting"
+;stl = "Storyteller|Geschichtenerzähler|GeschichtenerzählerIn"
+;stm = "Stage manager"
+;stn = "Standards body"
+;str = "Stereotyper"
+;tcd = "Technical director"
+;tch = "Teacher|Lehrer|LehrerIn"
+;ths = "Thesis advisor"
+;tld = "Television director|Fernsehregisseur|FernsehregisseurIn"
+;tlp = "Television producer|Fernsehproduzent|FernsehproduzentIn"
+;trc = "Transcriber|Transkribierer|TranskribiererIn"
+;trl = "Translator|Ãœbersetzer|ÃœbersetzerIn"
+;tyd = "Type designer"
+;tyg = "Typographer"
+;uvp = "University place"
+;vac = "Voice actor|Synchronsprecher|SynchronsprecherIn"
+;vdg = "Videographer|Bildregisseur|BildregisseurIn"
+;voc = "Vocalist"
+;wac = "Writer of added commentary|Kommentarverfasser|KommentarverfasserIn"
+;wal = "Writer of added lyrics|Verfasser von zusätzlichen Lyrics|VerfasserIn von zusätzlichen Lyrics"
+;wam = "Writer of accompanying material"
+;wat = "Writer of added text|Verfasser von Zusatztexten|VerfasserIn von Zusatztexten"
+;wdc = "Woodcutter"
+;wde = "Wood engraver"
+;win = "Writer of introduction|Verfasser einer Einleitung|VerfasserIn einer Einleitung"
+;wit = "Witness"
+;wpr = "Writer of preface|Verfasser eines Vorworts|VerfasserIn eines Vorworts"
+;wst = "Writer of supplementary textual content|Verfasser von ergänzendem Text|VerfasserIn von ergänzendem Text"
diff --git a/config/vufind/authority.ini b/config/vufind/authority.ini
index 3a50ff6888bd9c9b6ca02caec92e63e42895f25f..39416f005519bbce38f8cde59e3bbd114cd7605c 100644
--- a/config/vufind/authority.ini
+++ b/config/vufind/authority.ini
@@ -23,6 +23,16 @@ heading   = "Heading"
 [Facets]
 source             = "Authority File"
 record_type        = "Record Type"
+field_of_activity    = "Field of activity"
+occupation         = "Occupation"
+gender             = "Gender"
+language           = "Language"
+;birth_date          = "Date of birth"
+;birth_place         = "Place of birth"
+;death_date          = "Date of death"
+;death_place         = "Place of death"
+country            = "Associated country"
+related_place       = "Other associated place"
 form_facet_str_mv  = "Form"
 general_facet_str_mv  = "General"
 chronological_facet_str_mv  = "Chronological"
diff --git a/config/vufind/authsearchspecs.yaml b/config/vufind/authsearchspecs.yaml
index 3c52061d1d7f4d793321b2dcd81f57e848ad6ee1..e86f852ba2a86e7acf51e0b056aa23943152cf99 100644
--- a/config/vufind/authsearchspecs.yaml
+++ b/config/vufind/authsearchspecs.yaml
@@ -11,23 +11,7 @@ AllFields:
     - allfields
   DismaxParams:
     - [bq, (record_type:"Personal Name" OR record_type:"Corporate Name")^500]
-  QueryFields:
-    heading_keywords:
-      - [onephrase, 1000]
-      - [and, 750]
-      - [or, 10]
-    use_for_keywords:
-      - [onephrase, 500]
-      - [and, 400]
-      - [or, 5]
-    see_also_keywords:
-      - [onephrase, 130]
-      - [and, 100]
-      - [or, 2]
-    allfields:
-      - [onephrase, 50]
-      - [and, 10]
-      - [or, ~]
+  DismaxHandler: edismax
 
 Heading:
   DismaxFields:
@@ -36,27 +20,11 @@ Heading:
     - see_also_keywords^100
   DismaxParams:
     - [bq, (record_type:"Personal Name" OR record_type:"Corporate Name")^500]
-  QueryFields:
-    heading_keywords:
-      - [onephrase, 1000]
-      - [and, 750]
-      - [or, 10]
-    use_for_keywords:
-      - [onephrase, 500]
-      - [and, 400]
-      - [or, 5]
-    see_also_keywords:
-      - [onephrase, 130]
-      - [and, 100]
-      - [or, 2]
+  DismaxHandler: edismax
 
 MainHeading:
   DismaxFields:
     - heading_keywords^750
   DismaxParams:
     - [bq, (record_type:"Personal Name" OR record_type:"Corporate Name")^500]
-  QueryFields:
-    heading_keywords:
-      - [onephrase, 1000]
-      - [and, 750]
-      - [or, 10]
+  DismaxHandler: edismax
diff --git a/config/vufind/combined.ini b/config/vufind/combined.ini
index be505b32765140ef79757b38c1a4329f02b6fdfb..977e88247fc383bea510976e730a82550afdbf48 100644
--- a/config/vufind/combined.ini
+++ b/config/vufind/combined.ini
@@ -31,9 +31,9 @@
 ; hiddenFilter = One or more hidden filters to apply to search results displayed in
 ;                the column.
 ;          Use multiple "hiddenfilter[] = ..." lines if multiple filters are needed.
-;          Hidden filters can be used in conjunction with search tab filters 
+;          Hidden filters can be used in conjunction with search tab filters
 ;          ([SearchTabsFilters] in config.ini). In this case, make sure the filters
-;          are equal in both configurations to make the correct tab active when 
+;          are equal in both configurations to make the correct tab active when
 ;          clicking more results from the combined view.
 ; shard = Limit results to one or more shards (use names from searches.ini, not
 ;         URLs). Use multiple "shard[] = ..." lines if multiple shards are needed.
diff --git a/config/vufind/config.ini b/config/vufind/config.ini
index 87ec3126e2804bdfe89d27dc96e6130d6670c19e..214229bab573da7409fba00fe4752cb26818977c 100644
--- a/config/vufind/config.ini
+++ b/config/vufind/config.ini
@@ -33,6 +33,22 @@ title           = "Library Catalog"
 ;   bootprint3 = bootstrap3 theme with more attractive default styling applied
 ;                (named after the earlier, now-deprecated blueprint theme)
 theme           = bootprint3
+
+; Automatic asset minification and concatenation setting. When active, HeadScript
+; and HeadLink will concatenate and minify all viable files to reduce requests and
+; load times. This setting is off by default.
+;
+; This configuration takes the form of a semi-colon separated list of
+; environment:configuration pairs where "environment" is a possible APPLICATION_ENV
+; value (e.g. 'production' or 'development') or '*'/no prefix for all contexts.
+; Possible values for 'configuration' within each environment are 'js', 'css',
+; 'off'/false, 'on'/true/'*'. This allows global enabling/disabling of the pipeline
+; or separate configurations for different types of resources. Multiple configuration
+; values may be comma-separated -- e.g. 'js,css'.
+;
+; Example: "development:off; production:js,css"
+;asset_pipeline = "production:js"
+
 ; Uncomment the following line to use a different default theme for mobile devices.
 ; You may not wish to use this setting if you are using one of the Bootstrap-based
 ; standard themes since they support responsive design. Available mobile theme:
@@ -92,6 +108,12 @@ admin_enabled = false
 sidebarOnLeft = false
 ; Invert the sidebarOnLeft setting for right-to-left languages?
 mirrorSidebarInRTL = true
+; Put search result thumbnails on the left (true) or right (false)
+resultThumbnailsOnLeft = true
+; Put favorites list thumbnails on the left (true) or right (false)
+listThumbnailsOnLeft = true
+; Show thumbnail on opposite side in right-to-left languages?
+mirrorThumbnailsRTL = true
 ; Handle menu as an offcanvas slider at mobile sizes (in bootstrap3-based themes)
 offcanvas = false
 ; Show (true) / Hide (false) Book Bag - Default is Hide.
@@ -103,7 +125,7 @@ showBulkOptions = false
 ; Should users be allowed to save searches in their accounts?
 allowSavedSearches = true
 ; Generator value to display in an HTML header <meta> tag:
-generator = "VuFind 3.0.3"
+generator = "VuFind 3.1.3"
 
 ; This section allows you to configure the mechanism used for storing user
 ; sessions.  Available types: File, Memcache, Database.
@@ -140,13 +162,14 @@ lifetime                    = 3600 ; Session lasts for 1 hour
 
 ; Please set the ILS that VuFind will interact with.
 ;
-; Available drivers: Aleph, Amicus, ClaviusSQL, Evergreen, Horizon (basic database
-;       access only), HorizonXMLAPI (more features via API), Innovative, Koha, LBS4,
-;       MultiBackend (to chain together multiple drivers in a consortial setting),
-;       NewGenLib, NoILS (for users without an ILS, or to disable ILS functionality
-;       during maintenance), Polaris, Unicorn (which also applies to SirsiDynix
-;       Symphony), Virtua, Voyager (for Voyager 6+), VoyagerRestful (for Voyager 7+
-;       w/ RESTful web services), XCNCIP2 (for XC NCIP Tookit v2.x)
+; Available drivers: Aleph, Amicus, ClaviusSQL, DAIA, Evergreen, Horizon (basic db
+;       access only), HorizonXMLAPI (more features via API), Innovative, Koha,
+;       KohaILSDI, LBS4, MultiBackend (to chain together multiple drivers in a
+;       consortial setting), NewGenLib, NoILS (for users without an ILS, or to
+;       disable ILS functionality during maintenance), PAIA, Polaris, Unicorn (which
+;       also applies to SirsiDynix Symphony), Virtua, Voyager (for Voyager 6+),
+;       VoyagerRestful (for Voyager 7+ w/ RESTful web services), XCNCIP2 (for XC
+;       NCIP Tookit v2.x)
 ; Note: Unicorn users should visit the vufind-unicorn project for more details:
 ;       http://code.google.com/p/vufind-unicorn/
 ; Note: DAIA supports XML or JSON results (since release 2.4).
@@ -314,6 +337,13 @@ ils_encryption_key = false
 ; this).
 ;minimum_password_length = 4
 ;maximum_password_length = 32
+; Specify default limit of accepted characters in the password. Allowed values
+; are "numeric", "alphanumeric" or a regular expression
+;password_pattern = "(?=.*\d)(?=.*[a-z])(?=.*[A-Z])"
+; Specify default hint about what the password may contain when using a regexp
+; pattern. May be text or a translation key. The "numeric" and "alphanumeric"
+; patterns have translated default hints.
+;password_hint = "Include both upper and lowercase letters and at least one number."
 
 ; Uncomment this line to switch on "privacy mode" in which no user information
 ; will be stored in the database. Note that this is incompatible with social
@@ -368,7 +398,8 @@ ils_encryption_key = false
 ; "stats" core here; if no URL is specified, [Index]/url below will be used.
 ;solr            = http://localhost:8080/solr
 
-; When using the File mode, specify a directory for saving stat files here:
+; When using the File mode, specify a directory for saving stat files here.
+; NOTE: As currently implemented, File mode is NOT RECOMMENDED FOR USE IN PRODUCTION.
 ;file            = /usr/local/vufind/local/logs
 
 ; When displaying search statistics in the Admin module, do you want to see a single
@@ -450,10 +481,7 @@ database          = mysql://root@localhost/vufind
 
 ; LDAP is optional.  This section only needs to exist if the
 ; Authentication Method is set to LDAP.  When LDAP is active,
-; host, port, basedn and username are required.  The remaining
-; settings are optional, mapping fields in your LDAP schema
-; to fields in VuFind's database -- the more you fill in, the more
-; data will be imported from LDAP into VuFind.
+; host, port, basedn and username are required.
 ;[LDAP]
 ; Prefix the host with ldaps:// to use LDAPS; omit the prefix for standard
 ; LDAP with TLS.
@@ -461,6 +489,12 @@ database          = mysql://root@localhost/vufind
 ;port            = 389       ; LDAPS usually uses port 636 instead
 ;basedn          = "o=myuniversity.edu"
 ;username        = uid
+; separator string for mapping multi-valued ldap-fields to a user attribute
+; if no separator is given, only the first value is mapped to the given attribute
+;separator = ';'
+; Optional settings to map fields in your LDAP schema to fields in the user table
+; in VuFind's database -- the more you fill in, the more data will be imported
+; from LDAP into VuFind:
 ;firstname       = givenname
 ;lastname        = sn
 ;email           = mail
@@ -676,6 +710,12 @@ authors         = Wikipedia
 ; http://code.google.com/apis/books/branding.html before using Google Book Search.
 ;previews       = Google,OpenLibrary,HathiTrust
 
+; This setting controls whether or not cover images are linked to previews when
+; available. Legal settings are false (never link), * (always link; default), or
+; a comma-separated list of templates in which linking should occur (see coversize
+; above for a list of legal values).
+;linkPreviewsToCovers = *
+
 ; Possible HathiRights options = pd,ic,op,orph,und,umall,ic-world,nobody,pdus,cc-by,cc-by-nd,
 ; cc-by-nc-nd,cc-by-nc,cc-by-nc-sa,cc-by-sa,orphcand,cc-zero,und-world,icus
 ; Default is "pd,ic-world" if unset here.
@@ -699,11 +739,34 @@ authors         = Wikipedia
 ; recommendation module in searches.ini for more information)
 ;europeanaAPI = INSERTKEY
 
-; If this is set, a new map tab will show on the record page for records which
-; have long_lat data (see import/marc_local.properties for more information).
-; The setting specifies the type of map; currently, the only supported value is
-; "google"
-;recordMap = google
+; Geographic Display
+; recordMap can be set to either 'google' or 'openlayers';
+; 'google' will only display point features
+; 'openlayers' will display point and rectangle features. Default setting
+; (see import/marc_local.properties for more information).
+; Map Tab Options:
+; mapLabels:  leave empty, file:filename, or driver
+;      Leave it empty – no map labels will be displayed (default)
+;      file:filename - specify a file name after the colon for the
+;             coordinate/label lookup file. Coordinates in file must
+;             be specified as WENS.
+;      driver - Use the getCoordinateLabels method of the record driver to fetch labels;
+;               by default this relies on the long_lat_label field in Solr,
+;               but you can override the behavior with custom record driver code.
+;               The field must be the same length as the number of coordinate sets.
+;               Coordinates will be matched to labels on an ordered basis such that
+;               label[0] will be assigned for coordinate[0] and so forth.
+; displayCoords: true or false. Default is false. (Only for recordMap = openlayers)
+;                If displayCoords is true, then the coordinate values from
+;                coordinate field will be displayed before the map label in the label popup.
+;recordMap = openlayers
+;mapLabels = file:geosearch_test_lookup.txt
+;displayCoords = true
+
+; If you set recordMap = google, then Google requires that you obtain an API key.
+; For more information on obtaining an API key, see:
+; https://developers.google.com/maps/documentation/javascript/get-api-key#key
+;googleMapApiKey = "your-key-here"
 
 ; This section controls the behavior of the cover generator when makeDynamicCovers
 ; above is non-false.
@@ -801,6 +864,8 @@ url = "https://api.vlb.de/api/v1/cover/"
 ; set use_ssl to true if you serve your site over ssl and you
 ; use SyndeticsPlus to avoid insecure content browser warnings
 ; (or if you just prefer ssl)
+; NOTE: SyndeticsPlus is incompatible with the tabs/accordion [List] views in
+;       searches.ini. Do not turn it on if you are using these optional features.
 [Syndetics]
 use_ssl = false
 plus = false
@@ -1039,6 +1104,11 @@ skip_numeric = true
 ;file           = /var/log/vufind.log:alert,error,notice,debug
 ;email          = alerts@myuniversity.edu:alert-5,error-5
 
+; Get URL from https://YOURSLACK.slack.com/apps/manage/custom-integrations
+;slack = #channel_name:alert,error
+;slackurl = https://hooks.slack.com/services/your-private-details
+;slackname = "VuFind Log" ; username messages are posted under
+
 ; This section can be used to specify a "parent configuration" from which
 ; the current configuration file will inherit.  You can chain multiple
 ; configurations together if you wish.
@@ -1188,12 +1258,24 @@ preferred_service = "loan"
 ; preferred_service settings to be ignored.
 show_full_status = false
 
+; You can set this to the name of an alphabetic browse handler (see the
+; [AlphaBrowse_Types] section) in order to link call numbers displayed on the
+; holdings tab and in status messages to a specific browse list. Set to false
+; to disable call number linking.
+callnumber_handler = false
+
 ; This section controls the behavior of the Record module.
 [Record]
 ; Set this to true in order to enable "next" and "previous" links to navigate
 ; through the current result set from within the record view.
 next_prev_navigation = false
 
+; Set this to true in order to enable "first" and "last" links to navigate
+; through the content result set from within the record view. Note, this
+; may cause slow behavior with some installations. The option will only work
+; when next_prev_navigation is also set to true.
+first_last_navigation = false
+
 ; Setting this to true will cause VuFind to skip the results page and
 ; proceed directly to the record page when a search has only one hit.
 jump_to_single_search_result = false
@@ -1411,7 +1493,7 @@ treeSearchLimit = 100
 ;secretKey = "https://www.google.com/recaptcha/admin/create"
 ; Valid theme values: dark, light
 ;theme      = light
-; Valid forms values: changePassword, email, feedback, newAccount, passwordRecovery, sms
+; Valid forms values: changePassword, email, feedback, newAccount, passwordRecovery, sms, userComments
 ; Use * for all supported forms
 ;forms = changePassword, email, newAccount, passwordRecovery, sms
 
@@ -1425,9 +1507,27 @@ comments = enabled
 ; create. If you change this to a more restrictive option, it is your responsibility
 ; to update the user_list database table to update the status of existing lists.
 lists = enabled
+; The following two settings are equivalent to default_limit / limit_options in
+; searches.ini, but used to control the page sizes of lists of favorites:
+lists_default_limit   = 20
+;lists_limit_options   = 10,20,40,60,80,100
+; This section controls what happens when a record title in a favorites list
+; is clicked. VuFind can either embed the full result directly in the list using
+; AJAX or can display it at its own separate URL as a full HTML page.
+; See the [List] section of searches.ini for all available options.
+lists_view=full
 ; Tags may be "enabled" or "disabled" (default = "enabled")
 ; When disabling tags, don't forget to also turn off tag search in searches.ini.
 tags = enabled
 ; This controls the maximum length of a single tag; it should correspond with the
 ; field size in the tags database table.
 max_tag_length = 64
+; This controls whether tags are case-sensitive (true) or always forced to be
+; represented as lowercase strings (false -- the default).
+case_sensitive_tags = false
+; If this setting is set to false, users will not be presented with a search
+; drop-down or advanced search link when searching/viewing tags. This is recommended
+; when using a multi-backend system (e.g. Solr + Summon + WorldCat). If set to
+; true, the standard Solr search options and advanced search link will be shown
+; in the tag screens; this is recommended when using a Solr-only configuration.
+show_solr_options_in_tag_search = false
diff --git a/config/vufind/facets.ini b/config/vufind/facets.ini
index d7df062bcdd771c93e22ea6dad75f4c555232fd8..5d7096df24fa2a5c88bc6cec49baaf408292ca26 100644
--- a/config/vufind/facets.ini
+++ b/config/vufind/facets.ini
@@ -45,9 +45,9 @@ dateRange[] = publishDate
 ; How hierarchical facets are sorted. Default is result count, but alternative ways
 ; can be specified:
 ; top = Sort the top level list alphabetically, others by result count (useful e.g.
-;       for a large number of building facets where top level is organization and 
-;       second level the library branch) 
-; all = Sort all levels alphabetically 
+;       for a large number of building facets where top level is organization and
+;       second level the library branch)
+; all = Sort all levels alphabetically
 ;hierarchicalFacetSortOptions[building] = top
 ; How hierarchical facet values are displayed in the records:
 ; single = Display only the deepest level (default)
@@ -74,12 +74,21 @@ dateRange[] = publishDate
 facet_limit = 30
 ; Override facet_limit on a per-field basis using this array:
 ;facet_limit_by_field[format] = 50
+
 ; By default, the side facets will only show 6 facets and then the "show more"
 ; button. This can get configured with the showMore settings.
 ; You can use the * to set a new default setting.
 showMore[*] = 6
 ; Or you can set a facet specific value by using the facet name as index.
 ;showMore['format'] = 10
+
+; Show more facets in a lightbox (paginated, no limit)
+; If false, facets expand in side bar to show facets up to the above limit
+; If "more", facets expand and offer an option at the bottom to open the lightbox
+; If true, facets immediately open in the lightbox
+showMoreInLightbox[*] = more
+;lightboxLimit = 50 ; page size for the lightbox
+
 ; Rows and columns for table used by top facets
 top_rows = 2
 top_cols = 3
@@ -169,7 +178,7 @@ special_facets   = "illustrated,daterange"
 ; e.g. sortKey{{{_:::_}}}facetValue{{{_:::_}}}displayText
 ; Per-field delimiters can be set here following a pipe after the facet name.
 ; e.g. "author_id_str|:::"
-; If no delimiter is set, the default delimiter (set above) will be used.  
+; If no delimiter is set, the default delimiter (set above) will be used.
 ;delimited_facets[] = author_id_str
 ;delimited_facets[] = "author_id_str|:::"
 
diff --git a/config/vufind/httpd-vufind.conf b/config/vufind/httpd-vufind.conf
index 24f039453b78c9aa4df8714b6543b1ccd4f58bf7..ddbb03a287824c630bafcf56dc6a3a9fc1da8cfe 100644
--- a/config/vufind/httpd-vufind.conf
+++ b/config/vufind/httpd-vufind.conf
@@ -13,6 +13,19 @@ AliasMatch ^/vufind/themes/([0-9a-zA-Z-_]*)/js/(.*)$ /usr/local/vufind/themes/$1
   AllowOverride All
 </Directory>
 
+# Configuration for public cache (used for asset pipeline minification)
+AliasMatch ^/vufind/cache/(.*)$ /usr/local/vufind/local/cache/public/$1
+<Directory /usr/local/vufind/local/cache/public/>
+  <IfModule !mod_authz_core.c>
+    Order allow,deny
+    Allow from all
+  </IfModule>
+  <IfModule mod_authz_core.c>
+    Require all granted
+  </IfModule>
+  AllowOverride All
+</Directory>
+
 # Configuration for general VuFind base:
 Alias /vufind /usr/local/vufind/public
 <Directory /usr/local/vufind/public/>
@@ -38,11 +51,6 @@ Alias /vufind /usr/local/vufind/public
 <Location /vufind>
   RewriteEngine On
 
-  # If using VuDL, uncomment the following line, fill in your appropriate Fedora
-  # server and port, and make sure that Apache mod_proxy and mod_proxy_http are
-  # enabled.
-  #RewriteRule ^files/(.*)/(.*) http://your-fedora-server/fedora/objects/$1/datastreams/$2/content [P]
-
   RewriteCond %{REQUEST_FILENAME} -s [OR]
   RewriteCond %{REQUEST_FILENAME} -l [OR]
   RewriteCond %{REQUEST_FILENAME} -d
diff --git a/config/vufind/permissions.ini b/config/vufind/permissions.ini
index 44ed60c13aeb872803ebef19d6c09bd59d19e9d0..e480e9d1e45b86ba2bcb47c2630198e3ce29a48c 100644
--- a/config/vufind/permissions.ini
+++ b/config/vufind/permissions.ini
@@ -5,7 +5,7 @@
 ; section contains several keys:
 ;
 ; require    - Set to 'ALL' to require all conditions in the section to be met in
-;              order to grant the permission(s). Set to 'ANY' to allow any one or 
+;              order to grant the permission(s). Set to 'ANY' to allow any one or
 ;              more of the conditions to grant the permission(s). Defaults to 'ALL'
 ;              if unset. Note that this rule is used for combining the output of
 ;              permission provider services. When a single permission provider
@@ -21,7 +21,7 @@
 ; ipRange     - Grant the permission to the single IP adresse or to the range.
 ;               Accepts a single IP adresse or a range with a minus character without
 ;               blanks as separator. Also partial addresses can be used (e.g. 192.168
-;               denotes 192.168.0.0-192.168.255.255) and IPv6 addresses are also 
+;               denotes 192.168.0.0-192.168.255.255) and IPv6 addresses are also
 ;               supported (unless PHP is compiled with IPv6 disabled).
 ; ipRegEx     - Grant the permission to IP addresses matching the provided regular
 ;               expression(s). Accepts a string or an array; if an array is passed,
@@ -45,6 +45,9 @@
 ;               rule that checks an attribute.
 ; username    - Grant the permission to logged-in users whose usernames match the
 ;               specified value(s). Accepts a string or an array.
+; user        - Grant the permissions to logged in users whose user attribute match
+;               the given regular expression pattern. For valid pattern syntax see
+;               http://php.net/manual/de/reference.pcre.pattern.syntax.php.
 ;
 ; Example configuration (grants the "sample.permission" permission to users named
 ; admin1 or admin2, or anyone coming from the IP addresses 1.2.3.4 or 1.2.3.5):
@@ -57,7 +60,12 @@
 ; ipRange[] = "1.2.3.4"
 ; ipRange[] = "1.2.3.7-1.2.5.254"
 ; permission = sample.permission
-;
+
+; Example configuration (grants the "sample.permission" permission to users 
+; who are from myCollege or who is a studentmajor (.*studentmajor.*):
+; user[] = "college myCollege"
+; user[] = "major .*studentmajor.*"
+
 ; List of permissions that you may wish to configure:
 ;
 ; access.AdminModule - Controls access to the admin panel (if enabled in config.ini)
@@ -91,7 +99,7 @@ permission = access.StaffViewTab
 
 ; Examples for Shibboleth
 ;
-; Only users that have either common-lib-terms and entityid from idp1 or 
+; Only users that have either common-lib-terms and entityid from idp1 or
 ; member and entityid from idp2 may have access to EITModule
 ;[shibboleth.EITModule1]
 ;shibboleth[] = "entityid https://testidp1.example.org/idp/shibboleth"
diff --git a/config/vufind/reservessearchspecs.yaml b/config/vufind/reservessearchspecs.yaml
index 5c28da4db4001dec8f686551578b5dc802637d6f..9c29dffc27b0cac07864d8df750d452924be86f7 100644
--- a/config/vufind/reservessearchspecs.yaml
+++ b/config/vufind/reservessearchspecs.yaml
@@ -8,40 +8,19 @@ AllFields:
     - course^400
     - instructor^400
     - department^200
-  QueryFields:
-    course:
-      - [onephrase, 1000]
-      - [and, 750]
-      - [or, 10] 
-    instructor:
-      - [onephrase, 1000]
-      - [and, 750]
-      - [or, 10] 
-    department:
-      - [onephrase, 1000]
-      - [and, 750]
-      - [or, 10]
+  DismaxHandler: edismax
+
 Instructor:
   DismaxFields:
     - instructor^400
-  QueryFields:
-    instructor:
-      - [onephrase, 1000]
-      - [and, 750]
-      - [or, 10] 
+  DismaxHandler: edismax
+
 Course:
   DismaxFields:
     - course^400
-  QueryFields:
-    course:
-      - [onephrase, 1000]
-      - [and, 750]
-      - [or, 10]   
+  DismaxHandler: edismax
+
 Department:
   DismaxFields:
     - department^400
-  QueryFields:
-    department:
-      - [onephrase, 1000]
-      - [and, 750]
-      - [or, 10]
+  DismaxHandler: edismax
diff --git a/config/vufind/searches.ini b/config/vufind/searches.ini
index bcce585cb64103fcf27430f4c0f0039b09b388cf..f00e92a5ca6703d48628ac5800a22faf040e98eb 100644
--- a/config/vufind/searches.ini
+++ b/config/vufind/searches.ini
@@ -51,6 +51,7 @@ case_sensitive_ranges = true
 ; [NoResultsRecommendations] sections below.
 ; See the comments above those sections for details on legal settings.  You may
 ; repeat these lines to load multiple recommendations.
+;default_top_recommend[] = MapSelection ; see [MapSelection] below for details
 default_top_recommend[] = TopFacets:ResultsTop
 default_top_recommend[] = SpellingSuggestions
 ;default_top_recommend[] = VisualFacets:Visual_Settings
@@ -120,6 +121,7 @@ Author              = Author
 Subject             = Subject
 CallNumber          = "Call Number"
 ISN                 = "ISBN/ISSN"
+;Coordinate        = Coordinates
 tag                 = Tag
 
 ; This section defines which search options will be included on the advanced
@@ -136,6 +138,8 @@ publisher           = adv_search_publisher
 Series              = adv_search_series
 year                = adv_search_year
 toc                 = adv_search_toc
+;Coordinate        = Coordinates
+
 
 ; This section defines the sort options available on standard search results.
 ; Values on the left of the equal sign are either the reserved term "relevance"
@@ -194,15 +198,19 @@ CallNumber = callnumber-sort
 ;       Display results from the DPLA catalog. Provide a boolean to have the sidebar
 ;       collapsed or open on page load.
 ; EuropeanaResults:[url]:[requestParam]:[limit]:[unwanted data providers]
-;       Display search results from Europeana.eu API. [url] is the base search URL
-;       default "api.europeana.eu/api/opensearch.rss" [requestParam] parameter name
-;       for passing lookup value in url, default is "searchTerms" [limit] defaults to
-;       5, is the number of result items to display [unwanted data providers] comma
-;       separated list of dataproviders to ignore results from; useful for excluding
-;       own results that are also in Europeana. An API key must be set in config.ini
-;       (see europeanaAPI setting in [Content] section).
-; EuropeanaResultsDeferred: [url]:[requestParam]:[limit]:[unwanted data providers]
-;       See EuropeanaResults, but this version uses AJAX.
+;       Display search results from Europeana.eu API.
+;       Parameters (all are optional):
+;         [url] = base search URL, default api.europeana.eu/api/v2/opensearch.rss
+;         [requestParam] = parameter name for passing lookup value in url, default is
+;             "searchTerms"
+;         [limit] = the number of result items to display (defaults to 5)
+;         [unwanted data providers] = comma separated list of dataproviders to ignore
+;             results from; useful for excluding your own results that are also in
+;             Europeana.
+;       An API key must be set in config.ini (see europeanaAPI setting in [Content]
+;       section).
+; EuropeanaResultsDeferred:[url]:[requestParam]:[limit]:[unwanted data providers]
+;       See EuropeanaResults, but this version uses AJAX for asynchronous loading.
 ; ExpandFacets:[ini section]:[ini name]
 ;       Display facets listed in the specified section of the specified ini file;
 ;       if [ini name] is left out, it defaults to "facets."  Rather than using
@@ -317,6 +325,13 @@ CallNumber = callnumber-sort
 ;       AuthorityRecommend:__resultlimit__:50 then authority recommendations will
 ;       only display on result screens displaying fewer than 50 hits; by default,
 ;       recommendations will always display). Filtering is optional.
+; MapSelection:[ini section]:[ini name]
+;       Enable geographic searching capability via OpenLayers3 API by activating
+;       this module. Records must be indexed using the geographic search and display
+;       fields. See the marc_local.properties file for more information on indexing.
+;       Default settings and more comments may be found in the  [MapSelection]
+;       section in this file. The section name and ini file name loaded by the
+;       module may be overridden through the [ini section]/[ini name] parameters.
 ; PubDateVisAjax:[zooming]:[facet field 1]:[facet field 2]:...:[facet field n]
 ;       Display a visualization of publication dates for each of the specified facet
 ;       fields.  This is designed for a field containing four-digit years.  Zooming
@@ -493,6 +508,7 @@ Author = "Solr:Author:author,author2"
 Subject = "Solr:Subject:topic,genre,geographic,era"
 CallNumber = "SolrCN"
 ISN = "Solr:ISN:isbn,issn"
+Coordinate = "None"
 tag = "Tag"
 
 ; When snippets are enabled, this section can be used to display captions based on
@@ -549,6 +565,17 @@ container_title = "Journal Title"
 ;grid = Grid
 ;visual = Visual
 
+; This section controls what happens when a record title in a search result list
+; is clicked. VuFind can either embed the full result directly in the list using
+; AJAX or can display it at its own separate URL as a full HTML page.
+; full - separate page (default)
+; tabs - embedded using tabs (see record/ajaxview-tabs.phtml)
+; accordion - embedded using an accordion (see record/ajaxview-accordion.phtml)
+; NOTE: This feature is incompatible with SyndeticsPlus content; please use
+;       regular Syndetics if necessary.
+[List]
+view=full
+
 ; This section allows for adding hidden filters. Each filter will be translated
 ; to format 'key:"value"' and added by Solr.php as a hidden filter (a facet that
 ; is always applied but is not seen by the user).  This is useful if you use a
@@ -595,3 +622,38 @@ container_title = "Journal Title"
 ; Priority order (descending) for record sources (record ID prefixes separated
 ; from the actual record by period, e.g. testsrc.12345)
 ;sources = alli,testsrc
+
+; This section defines the default parameters for the geographic search
+; functionality found in the MapSelection recommendation module.
+; To enable this feature, uncomment the default_top_recommend[] = MapSelection
+; in the default recommendations section. To set the configuration settings 
+; for this feature, adjust the parameters below.
+[MapSelection]
+; This defines the coordinates of a search region that will be highlighted when
+; the user clicks the "Geographic Search" link next to the VuFind search box.
+; This should ideally cover a large area of the map where most/all of your
+; geographic points are located. If your dataset is not concentrated in one 
+; geographic area, it is advised that you pick a default area, and do not use
+; the entire extent of the map for searching (otherwise the search may be slow).
+; The default coordinates specified below are in decimal degrees, and are 
+; ordered as WENS (west, east, north, south). Ranges of valid values are:
+; -180 to 180 (longitude) and -85 to 85 (latitude). Note, to search from and to
+; the international date line, use west = -179 and east = -180. 
+default_coordinates = "-95, 30, 72, 15"
+; height: Height in pixels of the map selection interface.
+height = 320
+
+; This section defines settings used to fetch similar records.
+[MoreLikeThis]
+; Boolean value indicating whether the newer MoreLikeThis query handler should be
+; used instead of the traditional MoreLikeThis component (default). Only the
+; MoreLikeThis query handler supports sharded indexes, but as of this writing, the
+; traditional component offers more nuanced relevance ranking. Results from these
+; methods may differ.
+;useMoreLikeThisHandler = true
+; If the MoreLikeThis handler is used, this setting can be used to adjust its
+; behavior. See https://cwiki.apache.org/confluence/display/solr/Other+Parsers#OtherParsers-MoreLikeThisQueryParser
+; for more information regarding the possible parameters.
+;params = "qf=title,title_short,callnumber-label,topic,language,author,publishDate mintf=1 mindf=1";
+; This setting can be used to limit the maximum number of suggestions. Default is 5.
+;count = 5
diff --git a/config/vufind/searchspecs.yaml b/config/vufind/searchspecs.yaml
index e945c79ad982bd74ef8ab3ba1fb11e78f923a8d4..5e94d5cf055dab578a701f352ea878066470e5b9 100644
--- a/config/vufind/searchspecs.yaml
+++ b/config/vufind/searchspecs.yaml
@@ -148,6 +148,19 @@
 # [uppercase] - Convert string to uppercase
 #
 # See the CallNumber search below for an example of custom munging in action.
+#
+#-----------------------------------------------------------------------------------
+#
+# Note that you may create a "@parent_yaml" entry at the very top of the file to
+# inherit sections from another file. For example:
+#
+# @parent_yaml: "/path/to/my/file.yaml"
+#
+# Only sections not found in this file will be loaded in from the parent file.
+# In some complex scenarios, this can be a useful way of sharing settings
+# between multiple configured VuFind instances. You can create a chain of parent
+# files if necessary.
+#
 #-----------------------------------------------------------------------------------
 
 # These searches use Dismax when possible:
@@ -161,51 +174,13 @@ Author:
     - author_corporate
     - author_variant
     - author2_variant
-  QueryFields:
-    author:
-      - [onephrase, 350]
-      - [and, 200]
-      - [or, 100]
-    author_fuller:
-      - [onephrase, 200]
-      - [and, 100]
-      - [or, 50]
-    author2:
-      - [onephrase, 100]
-      - [and, 50]
-      - [or, ~]
-    author2_fuller:
-      - [onephrase, 100]
-      - [and, 50]
-      - [or, ~]
-    author_additional:
-      - [onephrase, 100]
-      - [and, 50]
-      - [or, ~]
-    author_corporate:
-      - [onephrase, 100]
-      - [and, 50]
-      - [or, ~]
-    author_variant:
-      - [onephrase, 100]
-      - [and, 50]
-      - [or, ~]
-    author2_variant:
-      - [onephrase, 100]
-      - [and, 50]
-      - [or, ~]
+  DismaxHandler: edismax
 
 ISN:
   DismaxFields:
     - isbn
     - issn
-  QueryFields:
-    issn:
-      - [and, 100]
-      - [or, ~]
-    isbn:
-      - [and, 100]
-      - [or, ~]
+  DismaxHandler: edismax
 
 Subject:
   DismaxFields:
@@ -214,34 +189,16 @@ Subject:
     - geographic^50
     - genre^50
     - era
-  QueryFields:
-    topic_unstemmed:
-      - [onephrase, 350]
-      - [and, 150]
-      - [or, ~]
-    topic:
-      - [onephrase, 300]
-      - [and, 100]
-      - [or, ~]
-    geographic:
-      - [onephrase, 300]
-      - [and, 100]
-      - [or, ~]
-    genre:
-      - [onephrase, 300]
-      - [and, 100]
-      - [or, ~]
-    era:
-      - [and, 100]
-      - [or, ~]
+  DismaxHandler: edismax
 #  ExactSettings:
 #    DismaxFields:
 #      - topic_unstemmed^150
-#    QueryFields:
-#      - topic_unstemmed:
-#        - [onephrase, 350]
-#        - [and, 150]
-#        - [or, ~]
+
+
+Coordinate:
+  DismaxFields:
+    - long_lat_display
+  DismaxHandler: edismax
 
 # This field definition is a compromise that supports both journal-level and
 # article-level data.  The disadvantage is that hits in article titles will
@@ -260,40 +217,11 @@ JournalTitle:
     - title_old
     - series^100
     - series2
-  QueryFields:
-    title_short:
-      - [onephrase, 500]
-    title_full_unstemmed:
-      - [onephrase, 450]
-      - [and, 400]
-    title_full:
-      - [onephrase, 400]
-    title:
-      - [onephrase, 300]
-      - [and, 250]
-    container_title:
-      - [onephrase, 275]
-      - [and, 225]
-    title_alt:
-      - [and, 200]
-    title_new:
-      - [and, 100]
-    title_old:
-      - [and, ~]
-    series:
-      - [onephrase, 100]
-      - [and, 50]
-    series2:
-      - [onephrase, 50]
-      - [and , ~]
+  DismaxHandler: edismax
   FilterQuery: "format:Journal OR format:Article"
 #  ExactSettings:
 #    DismaxFields:
 #      - title_full_unstemmed^450
-#    QueryFields:
-#      - title_full_unstemmed:
-#        - [onephrase, 450]
-#        - [and, 400]
 #    FilterQuery: "format:Journal OR format:Article"
 
 Title:
@@ -307,50 +235,16 @@ Title:
     - title_old
     - series^100
     - series2
-  QueryFields:
-    title_short:
-      - [onephrase, 500]
-    title_full_unstemmed:
-      - [onephrase, 450]
-      - [and, 400]
-    title_full:
-      - [onephrase, 400]
-    title:
-      - [onephrase, 300]
-      - [and, 250]
-    title_alt:
-      - [and, 200]
-    title_new:
-      - [and, 100]
-    title_old:
-      - [and, ~]
-    series:
-      - [onephrase, 100]
-      - [and, 50]
-    series2:
-      - [onephrase, 50]
-      - [and , ~]
+  DismaxHandler: edismax
 #  ExactSettings:
 #    DismaxFields:
 #      - title_full_unstemmed^450
-#    QueryFields:
-#      - title_full_unstemmed:
-#        - [onephrase, 450]
-#        - [and, 400]
 
 Series:
   DismaxFields:
     - series^100
     - series2
-  QueryFields:
-    series:
-      - [onephrase, 500]
-      - [and, 200]
-      - [or, 100]
-    series2:
-      - [onephrase, 50]
-      - [and, 50]
-      - [or, ~]
+  DismaxHandler: edismax
 
 AllFields:
   DismaxFields:
@@ -376,64 +270,8 @@ AllFields:
     - description
     - isbn
     - issn
-  QueryFields:
-    0:
-      0:
-        - OR
-        - 50
-      title_short:
-        - [onephrase, 750]
-      title_full_unstemmed:
-        - [onephrase, 600]
-        - [and, 500]
-      title_full:
-        - [onephrase, 400]
-      title:
-        - [onephrase, 300]
-        - [and, 250]
-      title_alt:
-        - [and, 200]
-      title_new:
-        - [and, 100]
-    series:
-      - [and, 50]
-    series2:
-      - [and, 30]
-    author:
-      - [onephrase, 300]
-      - [and, 250]
-    author_fuller:
-      - [onephrase, 150]
-      - [and, 125]
-    author2:
-      - [and, 50]
-    author_additional:
-      - [and, 50]
-    contents:
-      - [and, 10]
-    topic_unstemmed:
-      - [onephrase, 550]
-      - [and, 500]
-    topic:
-      - [onephrase, 500]
-    geographic:
-      - [onephrase, 300]
-    genre:
-      - [onephrase, 300]
-    allfields_unstemmed:
-      - [or, 10]
-    fulltext_unstemmed:
-      - [or, 10]
-    allfields:
-      - [or, ~]
-    fulltext:
-      - [or, ~]
-    description:
-      - [or, ~]
-    isbn:
-      - [onephrase, ~]
-    issn:
-      - [onephrase, ~]
+    - long_lat_display
+  DismaxHandler: edismax
 #  ExactSettings:
 #    DismaxFields:
 #      - title_full_unstemmed^600
@@ -442,21 +280,6 @@ AllFields:
 #      - fulltext_unstemmed^10
 #      - isbn
 #      - issn
-#    QueryFields:
-#      title_full_unstemmed:
-#        - [onephrase, 600]
-#        - [and, 500]
-#      topic_unstemmed:
-#        - [onephrase, 550]
-#        - [and, 500]
-#      allfields_unstemmed:
-#        - [or, 10]
-#      fulltext_unstemmed:
-#        - [or, 10]
-#      isbn:
-#        - [onephrase, ~]
-#      issn:
-#        - [onephrase, ~]
 
 # These are advanced searches that never use Dismax:
 id:
diff --git a/config/vufind/websearchspecs.yaml b/config/vufind/websearchspecs.yaml
index 1ba5e789eda284d37b75c5c1a33e6b358cd92a44..5e3eac9c9627a8622eca67457cf6d601448ec102 100644
--- a/config/vufind/websearchspecs.yaml
+++ b/config/vufind/websearchspecs.yaml
@@ -14,40 +14,4 @@ AllFields:
     - url_keywords^50
     - fulltext_unstemmed^10
     - fulltext
-  QueryFields:
-    title_unstemmed:
-      - [onephrase, 1200]
-      - [and, 800]
-      - [or, 15]
-    title:
-      - [onephrase, 1000]
-      - [and, 750]
-      - [or, 10]
-    description_unstemmed:
-      - [onephrase, 400]
-      - [and, 350]
-      - [or, ~]
-    description:
-      - [onephrase, 350]
-      - [and, 300]
-      - [or, ~]
-    keywords_unstemmed:
-      - [onephrase, 300]
-      - [and, 250]
-      - [or, ~]
-    keywords:
-      - [onephrase, 250]
-      - [and, 200]
-      - [or, ~]
-    url_keywords:
-      - [onephrase, 100]
-      - [and, 50]
-      - [or, ~]
-    fulltext_unstemmed:
-      - [onephrase, 50]
-      - [and, 10]
-      - [or, ~]
-    fulltext:
-      - [onephrase, 25]
-      - [and, 5]
-      - [or, ~]
+  DismaxHandler: edismax
diff --git a/config/vufind/website.ini b/config/vufind/website.ini
index 5baf601fb3df0549321fc361cfafd0e71d591ff7..03f9f3a73773404f7f5106c34fcf792d1e792663 100644
--- a/config/vufind/website.ini
+++ b/config/vufind/website.ini
@@ -40,4 +40,4 @@ facet_limit = 30
 ; the [Index] section of config.ini.
 ;[Index]
 ;url             = http://localhost:8080/solr
-;default_core    = website 
+;default_core    = website
diff --git a/harvest/batch-delete.bat b/harvest/batch-delete.bat
index bff832c87d285d93056c616521fdd45bf5fb9398..217e6db08cbf4bb482ff39e63778831a37180164 100644
--- a/harvest/batch-delete.bat
+++ b/harvest/batch-delete.bat
@@ -9,16 +9,16 @@ goto end
 
 rem Make sure VUFIND_HOME is set:
 if not "!%VUFIND_HOME%!"=="!!" goto vufindhomefound
-rem VUFIND_HOME not set -- try to call vufind.bat to 
+rem VUFIND_HOME not set -- try to call env.bat to 
 rem fix the problem before we give up completely
-if exist %0\..\..\vufind.bat goto usevufindbat
-rem If vufind.bat doesn't exist, the user hasn't run the installer yet.
-echo ERROR: vufind.bat does not exist -- could not set up environment.
+if exist %0\..\..\env.bat goto useenvbat
+rem If env.bat doesn't exist, the user hasn't run the installer yet.
+echo ERROR: env.bat does not exist -- could not set up environment.
 echo Please run install.php to correct this problem.
 goto end
-:usevufindbat
+:useenvbat
 cd %0\..\..
-call vufind > nul
+call env > nul
 cd %0\..
 if not "!%VUFIND_HOME%!"=="!!" goto vufindhomefound
 echo You need to set the VUFIND_HOME environmental variable before running this script.
diff --git a/harvest/batch-import-marc-auth.bat b/harvest/batch-import-marc-auth.bat
index a96595d710dbab4a7be6e18a34adb9a12a026b73..d9de642c667584d2a268322fb31ec06f69d542e7 100644
--- a/harvest/batch-import-marc-auth.bat
+++ b/harvest/batch-import-marc-auth.bat
@@ -9,16 +9,16 @@ goto end
 
 rem Make sure VUFIND_HOME is set:
 if not "!%VUFIND_HOME%!"=="!!" goto vufindhomefound
-rem VUFIND_HOME not set -- try to call vufind.bat to 
+rem VUFIND_HOME not set -- try to call env.bat to 
 rem fix the problem before we give up completely
-if exist %0\..\..\vufind.bat goto usevufindbat
-rem If vufind.bat doesn't exist, the user hasn't run the installer yet.
-echo ERROR: vufind.bat does not exist -- could not set up environment.
+if exist %0\..\..\env.bat goto useenvbat
+rem If env.bat doesn't exist, the user hasn't run the installer yet.
+echo ERROR: env.bat does not exist -- could not set up environment.
 echo Please run install.php to correct this problem.
 goto end
-:usevufindbat
+:useenvbat
 cd %0\..\..
-call vufind > nul
+call env > nul
 cd %0\..
 if not "!%VUFIND_HOME%!"=="!!" goto vufindhomefound
 echo You need to set the VUFIND_HOME environmental variable before running this script.
diff --git a/harvest/batch-import-marc.bat b/harvest/batch-import-marc.bat
index 5a1ff9bc8e82c87b0cdbfdf5ebb5ac60ff501471..852f1b7d3454c909649ea33d85c43a1f9eb1a931 100644
--- a/harvest/batch-import-marc.bat
+++ b/harvest/batch-import-marc.bat
@@ -9,16 +9,16 @@ goto end
 
 rem Make sure VUFIND_HOME is set:
 if not "!%VUFIND_HOME%!"=="!!" goto vufindhomefound
-rem VUFIND_HOME not set -- try to call vufind.bat to 
+rem VUFIND_HOME not set -- try to call env.bat to 
 rem fix the problem before we give up completely
-if exist %0\..\..\vufind.bat goto usevufindbat
-rem If vufind.bat doesn't exist, the user hasn't run the installer yet.
-echo ERROR: vufind.bat does not exist -- could not set up environment.
+if exist %0\..\..\env.bat goto useenvbat
+rem If env.bat doesn't exist, the user hasn't run the installer yet.
+echo ERROR: env.bat does not exist -- could not set up environment.
 echo Please run install.php to correct this problem.
 goto end
-:usevufindbat
+:useenvbat
 cd %0\..\..
-call vufind > nul
+call env > nul
 cd %0\..
 if not "!%VUFIND_HOME%!"=="!!" goto vufindhomefound
 echo You need to set the VUFIND_HOME environmental variable before running this script.
diff --git a/harvest/batch-import-xsl.bat b/harvest/batch-import-xsl.bat
index b17eed3560fc29abfe5f35b3a040c4a8506063b6..e9ab09e396cf00c6759ba1e93e2cfcb9fb0d36de 100644
--- a/harvest/batch-import-xsl.bat
+++ b/harvest/batch-import-xsl.bat
@@ -9,16 +9,16 @@ goto end
 
 rem Make sure VUFIND_HOME is set:
 if not "!%VUFIND_HOME%!"=="!!" goto vufindhomefound
-rem VUFIND_HOME not set -- try to call vufind.bat to 
+rem VUFIND_HOME not set -- try to call env.bat to 
 rem fix the problem before we give up completely
-if exist %0\..\..\vufind.bat goto usevufindbat
-rem If vufind.bat doesn't exist, the user hasn't run the installer yet.
-echo ERROR: vufind.bat does not exist -- could not set up environment.
+if exist %0\..\..\env.bat goto useenvbat
+rem If env.bat doesn't exist, the user hasn't run the installer yet.
+echo ERROR: env.bat does not exist -- could not set up environment.
 echo Please run install.php to correct this problem.
 goto end
-:usevufindbat
+:useenvbat
 cd %0\..\..
-call vufind > nul
+call env > nul
 cd %0\..
 if not "!%VUFIND_HOME%!"=="!!" goto vufindhomefound
 echo You need to set the VUFIND_HOME environmental variable before running this script.
diff --git a/harvest/harvest_oai.php b/harvest/harvest_oai.php
index 7fbfba07088d04f084852f56f9d753c926ee2e43..c08364b9fcf94402d81a9afb56f86cc21f39f9ce 100644
--- a/harvest/harvest_oai.php
+++ b/harvest/harvest_oai.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Harvest_Tools
diff --git a/harvest/merge-marc.php b/harvest/merge-marc.php
index 98700a9af73100ec27c4fd9ee642738076a8ee74..87335875b6d233c36c5ecdea24a34e43c108862b 100644
--- a/harvest/merge-marc.php
+++ b/harvest/merge-marc.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Harvest_Tools
diff --git a/harvest/oai.ini b/harvest/oai.ini
index 38a98d02c054f7e662872c99efa53ae0ff7c424f..54f494b1800d515d6b4cdf5058080ba248f83f20 100644
--- a/harvest/oai.ini
+++ b/harvest/oai.ini
@@ -19,6 +19,9 @@
 ; dateGranularity = auto
 ; harvestedIdLog = harvest.log
 ; verbose = false
+; autosslca = true
+; sslcapath = "/etc/ssl/certs" ; e.g. for Debian systems
+; sslcafile = "/etc/pki/tls/cert.pem" ; e.g. for CentOS systems
 ; sslverifypeer = true
 ; sanitize = true
 ; badXMLLog = bad.log
@@ -103,6 +106,14 @@
 ; harvesting; this may be useful for troubleshooting purposes, but it defaults to
 ; false.
 ;
+; autosslca will attempt to autodetect your SSL certificate authority.
+;
+; sslcafile can be used to specify the path to an SSL certificate authority
+; file (e.g. /etc/pki/tls/cert.pem on CentOS/RedHat systems).
+;
+; sslcapath can be used to specify the path to an SSL certificate authority
+; directory (e.g. /etc/ssl/certs on Debian systems).
+;
 ; sslverifypeer may be set to false to disable SSL certificate checking; it defaults
 ; to true, and changing the setting is not recommended.
 ;
diff --git a/import-marc-auth.bat b/import-marc-auth.bat
index 217dce3eda47c1928a8a37df7851f0792e62e434..bcef07eb90b4bc81267465b5dc99d4e0ce3c9c3f 100644
--- a/import-marc-auth.bat
+++ b/import-marc-auth.bat
@@ -9,15 +9,15 @@ goto end
 
 rem Make sure we know where the VuFind home directory lives:
 if not "!%VUFIND_HOME%!"=="!!" goto vufindhomefound
-rem VUFIND_HOME not set -- try to call vufind.bat to 
+rem VUFIND_HOME not set -- try to call env.bat to 
 rem fix the problem before we give up completely
-if exist vufind.bat goto usevufindbat
-rem If vufind.bat doesn't exist, the user hasn't run the installer yet.
-echo ERROR: vufind.bat does not exist -- could not set up environment.
-echo Please run install.php to correct this problem.
+if exist env.bat goto useenvbat
+rem If env.bat doesn't exist, the user hasn't run the installer yet.
+echo ERROR: env.bat does not exist -- could not set up environment.
+echo Please run "php install.php" to correct this problem.
 goto end
-:usevufindbat
-call vufind > nul
+:useenvbat
+call env > nul
 if not "!%VUFIND_HOME%!"=="!!" goto vufindhomefound
 echo You need to set the VUFIND_HOME environmental variable before running this script.
 goto end
diff --git a/import-marc.bat b/import-marc.bat
index 418eda0650b7fc60a8499f9e669250df80e2893c..5b2257f1686b30921ffdc62b3126143961b840fb 100644
--- a/import-marc.bat
+++ b/import-marc.bat
@@ -53,12 +53,12 @@ set INDEX_OPTIONS=-Xms512m -Xmx512m -DentityExpansionLimit=0
 rem ##################################################
 rem # Set SOLRCORE
 rem ##################################################
-if not "!%SOLRCORE%!"=="!!" goto solrcorefound
-set SOLRCORE=biblio
-:solrcorefound
+if "!%SOLRCORE%!"=="!!" goto solrcorenotfound
+set EXTRA_SOLRMARC_SETTINGS=%EXTRA_SOLRMARC_SETTINGS%  -Dsolr.core.name=%SOLRCORE%
+:solrcorenotfound
 
 rem ##################################################
-rem # Set SOLR_HOME
+rem # Set VUFIND_HOME
 rem ##################################################
 if not "!%VUFIND_HOME%!"=="!!" goto vufindhomefound
 rem VUFIND_HOME not set -- try to call env.bat to 
@@ -74,16 +74,6 @@ if not "!%VUFIND_HOME%!"=="!!" goto vufindhomefound
 echo You need to set the VUFIND_HOME environmental variable before running this script.
 goto end
 :vufindhomefound
-if "!%SOLR_HOME%!"=="!!" goto solrhomenotfound
-set EXTRA_SOLRMARC_SETTINGS=%EXTRA_SOLRMARC_SETTINGS% -Dsolr.path=%SOLR_HOME% -Dsolr.solr.home=%SOLR_HOME% -Dsolrmarc.solr.war.path=%SOLR_HOME%/jetty/webapps/solr.war
-:solrhomenotfound
-
-rem ##################################################
-rem # Set SOLRMARC_HOME
-rem ##################################################
-if "!%SOLRMARC_HOME%!"=="!!" goto solrmarchomenotfound
-set EXTRA_SOLRMARC_SETTINGS=%EXTRA_SOLRMARC_SETTINGS% -Dsolrmarc.path=%SOLRMARC_HOME%
-:solrmarchomenotfound
 
 rem #####################################################
 rem # Build java command
@@ -109,12 +99,12 @@ set PROPERTIES_FILE=%VUFIND_HOME%\import\import.properties
 rem ##################################################
 rem # Set Command Options
 rem ##################################################
-set JAR_FILE=%VUFIND_HOME%\import\SolrMarc.jar
+for %%a in (%VUFIND_HOME%\import\solrmarc_core_*.jar) do set JAR_FILE=%%a
 
 rem #####################################################
 rem # Execute Importer
 rem #####################################################
-set RUN_CMD=%JAVA% %INDEX_OPTIONS% -Duser.timezone=UTC -Dsolr.core.name=%SOLRCORE% %EXTRA_SOLRMARC_SETTINGS% -jar %JAR_FILE% %PROPERTIES_FILE% %1
+set RUN_CMD=%JAVA% %INDEX_OPTIONS% -Duser.timezone=UTC %EXTRA_SOLRMARC_SETTINGS% -jar %JAR_FILE% %PROPERTIES_FILE% -solrj %VUFIND_HOME%\solr\vendor\dist\solrj-lib %1
 echo Now Importing %1 ...
 echo %RUN_CMD%
 %RUN_CMD%
diff --git a/import-marc.sh b/import-marc.sh
index 389b79317bc1e7478b6159be18056645b17a84b2..9569b084c896203e1057176bb355421a879bfcc3 100755
--- a/import-marc.sh
+++ b/import-marc.sh
@@ -5,8 +5,6 @@
 #
 # VUFIND_HOME
 #   Path to the vufind installation
-# SOLRMARC_HOME
-#   Path to the solrmarc installation
 # JAVA_HOME
 #   Path to the java
 # INDEX_OPTIONS
@@ -58,9 +56,9 @@ fi
 ##################################################
 # Set SOLRCORE
 ##################################################
-if [ -z "$SOLRCORE" ]
+if [ ! -z "$SOLRCORE" ]
 then
-  SOLRCORE="biblio"
+  EXTRA_SOLRMARC_SETTINGS="$EXTRA_SOLRMARC_SETTINGS -Dsolr.core.name=$SOLRCORE"
 fi
 
 
@@ -73,24 +71,6 @@ then
 fi
 
 
-##################################################
-# Use SOLR_HOME if set
-##################################################
-if [ ! -z "$SOLR_HOME" ]
-then
-  EXTRA_SOLRMARC_SETTINGS="$EXTRA_SOLRMARC_SETTINGS -Dsolr.path=$SOLR_HOME -Dsolr.solr.home=$SOLR_HOME -Dsolrmarc.solr.war.path=$SOLR_HOME/jetty/webapps/solr.war"
-fi
-
-
-##################################################
-# Set SOLRMARC_HOME
-##################################################
-if [ ! -z "$SOLRMARC_HOME" ]
-then
-  EXTRA_SOLRMARC_SETTINGS="$EXTRA_SOLRMARC_SETTINGS -Dsolrmarc.path=$VUFIND_HOME/import"
-fi
-
-
 #####################################################
 # Build java command
 #####################################################
@@ -117,7 +97,7 @@ fi
 ##################################################
 # Set Command Options
 ##################################################
-JAR_FILE="$VUFIND_HOME/import/SolrMarc.jar"
+for i in $VUFIND_HOME/import/solrmarc_core_*.jar; do JAR_FILE="$i"; done
 
 #####################################################
 # Verify that JAR_FILE exists
@@ -139,7 +119,7 @@ MARC_FILE=`basename $1`
 # Execute Importer
 #####################################################
 
-RUN_CMD="$JAVA $INDEX_OPTIONS -Duser.timezone=UTC -Dsolr.core.name=$SOLRCORE $EXTRA_SOLRMARC_SETTINGS -jar $JAR_FILE $PROPERTIES_FILE $MARC_PATH/$MARC_FILE"
+RUN_CMD="$JAVA $INDEX_OPTIONS -Duser.timezone=UTC $EXTRA_SOLRMARC_SETTINGS -jar $JAR_FILE $PROPERTIES_FILE -solrj $VUFIND_HOME/solr/vendor/dist/solrj-lib $MARC_PATH/$MARC_FILE"
 echo "Now Importing $1 ..."
 # solrmarc writes log messages to stderr, write RUN_CMD to the same place
 echo "`date '+%h %d, %H:%M:%S'` $RUN_CMD" >&2
diff --git a/import/SolrMarc.jar b/import/SolrMarc.jar
deleted file mode 100644
index 20e04b969614bd0f25bf17e47392eb1860a77936..0000000000000000000000000000000000000000
Binary files a/import/SolrMarc.jar and /dev/null differ
diff --git a/import/VuFindIndexer-README.txt b/import/VuFindIndexer-README.txt
deleted file mode 100644
index 6d83555cab9f15f65282daf6093c75dbc3f2b474..0000000000000000000000000000000000000000
--- a/import/VuFindIndexer-README.txt
+++ /dev/null
@@ -1,8 +0,0 @@
-VuFindIndexer.jar is a SolrMarc component.  If you wish to customize this file,
-you can rebuild SolrMarc from source.  See the SolrMarc project homepage:
-
-        http://code.google.com/p/solrmarc/
-
-You may also prefer to localize using BeanShell scripts or Mixins; see:
-
-        https://vufind.org/wiki/solrmarc
\ No newline at end of file
diff --git a/import/VuFindIndexer.jar b/import/VuFindIndexer.jar
deleted file mode 100644
index 61f130bb4a7ab85960cd8cafef3651c4f25580bc..0000000000000000000000000000000000000000
Binary files a/import/VuFindIndexer.jar and /dev/null differ
diff --git a/import/import-xsl.php b/import/import-xsl.php
index fad85e7f535d5144681e282560b534b9cb6c483b..db1230ce550add43d150deeb8ec1da0005bb7fb0 100644
--- a/import/import-xsl.php
+++ b/import/import-xsl.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Utilities
diff --git a/import/import.properties b/import/import.properties
index 8549a0ac3664fe854db608241ddc548f8d443e78..28a465ccb2124540b860632e814af55e1bd54236 100644
--- a/import/import.properties
+++ b/import/import.properties
@@ -1,25 +1,15 @@
-# Properties for the Java import program
-# $Id: vufind_config.properties $
+# Properties for the SolrMarc import program
 
 # IMPORTANT NOTE FOR WINDOWS USERS:
 #      Use forward slashes, not back slashes (i.e.  c:/vufind/..., not c:\vufind\...)
 
-# solrmarc.custom.jar.path - Jar containing custom java code to use in indexing. 
-# If solr.indexer below is defined (other than the default of org.solrmarc.index.SolrIndexer)
-# you MUST define this value to be the Jar containing the class listed there. 
-solrmarc.custom.jar.path=VuFindIndexer.jar|lib
-
-# Path to your solr instance
-solr.path = REMOTE
+# Solr settings
 solr.core.name = biblio
-solr.indexer = org.solrmarc.index.VuFindIndexer
 solr.indexer.properties = marc.properties, marc_local.properties
-
-#optional URL of running solr search engine to cause updates to be recognized.
 solr.hosturl = http://localhost:8080/solr/biblio/update
 
-#where to look for properties files, translation maps, and custom scripts
-#note that . refers to the directory where the jarfile for SolrMarc is located.
+# where to look for properties files, translation maps, and custom scripts
+# note that . refers to the directory where the jarfile for SolrMarc is located.
 solrmarc.path = /usr/local/vufind/import
 
 # Path to your marc file
diff --git a/import/import_auth.properties b/import/import_auth.properties
index c81f0c34b2e89de65918b79bc18fc4ca61d39efa..f2b66f6216d02c1db04dcd990d43cdcd8be3d360 100644
--- a/import/import_auth.properties
+++ b/import/import_auth.properties
@@ -1,25 +1,15 @@
-# Properties for the Java import program (used for importing authority records)
-# $Id: import_auth.properties $
+# Properties for the SolrMarc import program (used for importing authority records)
 
 # IMPORTANT NOTE FOR WINDOWS USERS:
 #      Use forward slashes, not back slashes (i.e.  c:/vufind/..., not c:\vufind\...)
 
-# solrmarc.custom.jar.path - Jar containing custom java code to use in indexing. 
-# If solr.indexer below is defined (other than the default of org.solrmarc.index.SolrIndexer)
-# you MUST define this value to be the Jar containing the class listed there. 
-solrmarc.custom.jar.path=VuFindIndexer.jar|lib
-
-# Path to your solr instance
-solr.path = REMOTE
+# Solr settings
 solr.core.name = authority
-solr.indexer = org.solrmarc.index.VuFindIndexer
 solr.indexer.properties = marc_auth.properties
-
-#optional URL of running solr search engine to cause updates to be recognized.
 solr.hosturl = http://localhost:8080/solr/authority/update
 
-#where to look for properties files, translation maps, and custom scripts
-#note that . refers to the directory where the jarfile for SolrMarc is located.
+# where to look for properties files, translation maps, and custom scripts
+# note that . refers to the directory where the jarfile for SolrMarc is located.
 solrmarc.path = /usr/local/vufind/import
 
 # Path to your marc file
diff --git a/import/index_java/.gitignore b/import/index_java/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..ae3c1726048cd06b9a143e0376ed46dd9b9a8d53
--- /dev/null
+++ b/import/index_java/.gitignore
@@ -0,0 +1 @@
+/bin/
diff --git a/import/index_java/src/org/solrmarc/index/UpdateDateTracker.java b/import/index_java/src/org/solrmarc/index/UpdateDateTracker.java
new file mode 100644
index 0000000000000000000000000000000000000000..ac8c6b796dedbf0dd8b6e8835f118805adc8bebf
--- /dev/null
+++ b/import/index_java/src/org/solrmarc/index/UpdateDateTracker.java
@@ -0,0 +1,184 @@
+package org.solrmarc.index;
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+import java.sql.*;
+import java.text.SimpleDateFormat;
+
+public class UpdateDateTracker
+{
+    private Connection db;
+    private String core;
+    private String id;
+    private SimpleDateFormat iso8601 = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
+
+    private Timestamp firstIndexed;
+    private Timestamp lastIndexed;
+    private Timestamp lastRecordChange;
+    private Timestamp deleted;
+
+    PreparedStatement insertSql;
+    PreparedStatement selectSql;
+    PreparedStatement updateSql;
+
+    /* Private support method: create a row in the change_tracker table.
+     */
+    private void createRow(Timestamp newRecordChange) throws SQLException
+    {
+        // Save new values to the object:
+        java.util.Date rightNow = new java.util.Date();
+        firstIndexed = lastIndexed = new Timestamp(rightNow.getTime());
+        lastRecordChange = newRecordChange;
+        
+        // Save new values to the database:
+        insertSql.setString(1, core);
+        insertSql.setString(2, id);
+        insertSql.setTimestamp(3, firstIndexed);
+        insertSql.setTimestamp(4, lastIndexed);
+        insertSql.setTimestamp(5, lastRecordChange);
+        insertSql.executeUpdate();
+    }
+
+    /* Private support method: read a row from the change_tracker table.
+     */
+    private boolean readRow() throws SQLException
+    {
+        selectSql.setString(1, core);
+        selectSql.setString(2, id);
+        ResultSet result = selectSql.executeQuery();
+        
+        // No results?  Free resources and return false:
+        if (!result.first()) {
+            result.close();
+            return false;
+        }
+        
+        // If we got this far, we have results -- load them into the object:
+        firstIndexed = result.getTimestamp(1);
+        lastIndexed = result.getTimestamp(2);
+        lastRecordChange = result.getTimestamp(3);
+        deleted = result.getTimestamp(4);
+
+        // Free resources and report success:
+        result.close();
+        return true;
+    }
+
+    /* Private support method: update a row in the change_tracker table.
+     */
+    private void updateRow(Timestamp newRecordChange) throws SQLException
+    {
+        // Save new values to the object:
+        java.util.Date rightNow = new java.util.Date();
+        lastIndexed = new Timestamp(rightNow.getTime());
+        // If first indexed is null, we're restoring a deleted record, so
+        // we need to treat it as new -- we'll use the current time.
+        if (firstIndexed == null) {
+            firstIndexed = lastIndexed;
+        }
+        lastRecordChange = newRecordChange;
+
+        // Save new values to the database:
+        updateSql.setTimestamp(1, firstIndexed);
+        updateSql.setTimestamp(2, lastIndexed);
+        updateSql.setTimestamp(3, lastRecordChange);
+        updateSql.setNull(4, java.sql.Types.NULL);
+        updateSql.setString(5, core);
+        updateSql.setString(6, id);
+        updateSql.executeUpdate();
+    }
+
+    /* Constructor:
+     */
+    public UpdateDateTracker(Connection dbConnection) throws SQLException
+    {
+        db = dbConnection;
+        insertSql = db.prepareStatement(
+            "INSERT INTO change_tracker(core, id, first_indexed, last_indexed, last_record_change) " +
+            "VALUES(?, ?, ?, ?, ?);");
+        selectSql = db.prepareStatement(
+            "SELECT first_indexed, last_indexed, last_record_change, deleted " +
+            "FROM change_tracker WHERE core = ? AND id = ?;",
+            ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY);
+        updateSql = db.prepareStatement("UPDATE change_tracker " +
+            "SET first_indexed = ?, last_indexed = ?, last_record_change = ?, deleted = ? " +
+            "WHERE core = ? AND id = ?;");
+    }
+
+    /* Finalizer
+     */
+    protected void finalize() throws SQLException, Throwable
+    {
+        insertSql.close();
+        selectSql.close();
+        updateSql.close();
+        super.finalize();
+    }
+
+    /* Get the first indexed date (IMPORTANT: index() must be called before this method)
+     */
+    public String getFirstIndexed()
+    {
+        return iso8601.format(new java.util.Date(firstIndexed.getTime()));
+    }
+
+    /* Get the last indexed date (IMPORTANT: index() must be called before this method)
+     */
+    public String getLastIndexed()
+    {
+        return iso8601.format(new java.util.Date(lastIndexed.getTime()));
+    }
+
+    /* Update the database to indicate that the record has just been received by the indexer:
+     */
+    public void index(String selectedCore, String selectedId, java.util.Date recordChange) throws SQLException
+    {
+        // If core and ID match the values currently in the class, we have already
+        // indexed the record and do not need to repeat ourselves!
+        if (selectedCore.equals(core) && selectedId.equals(id)) {
+            return;
+        }
+
+        // If we made it this far, we need to update the database, so let's store
+        // the current core/ID pair we are operating on:
+        core = selectedCore;
+        id = selectedId;
+
+        // Convert incoming java.util.Date to a Timestamp:
+        Timestamp newRecordChange = new Timestamp(recordChange.getTime());
+
+        // No row?  Create one!
+        if (!readRow()) {
+            createRow(newRecordChange);
+        // Row already exists?  See if it needs to be updated:
+        } else {
+            // Are we restoring a previously deleted record, or was the stored 
+            // record change date before current record change date?  Either way,
+            // we need to update the table!
+            //
+            // Note that we check for a time difference of at least one second in
+            // order to count as a change.  Because dates are stored with second
+            // precision, some of the date conversions have been known to create
+            // minor inaccuracies in the millisecond range, which used to cause
+            // false positives.
+            if (deleted != null || 
+                Math.abs(lastRecordChange.getTime() - newRecordChange.getTime()) > 999) {
+                updateRow(newRecordChange);
+            }
+        }
+    }
+}
diff --git a/import/index_java/src/org/solrmarc/index/VuFindIndexer.java b/import/index_java/src/org/solrmarc/index/VuFindIndexer.java
new file mode 100644
index 0000000000000000000000000000000000000000..9fdb43b5f1552b337179b49bc0b0483cd98b0e66
--- /dev/null
+++ b/import/index_java/src/org/solrmarc/index/VuFindIndexer.java
@@ -0,0 +1,2762 @@
+package org.solrmarc.index;
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+import java.io.File;
+import java.io.FileReader;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.BufferedReader;
+import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
+import java.lang.StringBuilder;
+import java.text.ParseException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedHashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.sql.*;
+import java.text.SimpleDateFormat;
+
+import org.apache.log4j.Logger;
+import org.marc4j.marc.ControlField;
+import org.marc4j.marc.DataField;
+import org.marc4j.marc.Record;
+import org.marc4j.marc.Subfield;
+import org.marc4j.marc.VariableField;
+import org.solrmarc.callnum.DeweyCallNumber;
+import org.solrmarc.callnum.LCCallNumber;
+import org.solrmarc.tools.CallNumUtils;
+import org.solrmarc.tools.SolrMarcIndexerException;
+import org.solrmarc.tools.Utils;
+import org.ini4j.Ini;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+
+import org.w3c.dom.Document;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+/**
+ *
+ * @author Robert Haschart
+ * @version $Id: VuFindIndexer.java 224 2008-11-05 19:33:21Z asnagy $
+ *
+ */
+public class VuFindIndexer extends SolrIndexer
+{
+    // Initialize logging category
+    static Logger logger = Logger.getLogger(VuFindIndexer.class.getName());
+
+    // Initialize VuFind database connection (null until explicitly activated)
+    private Connection vufindDatabase = null;
+    private UpdateDateTracker tracker = null;
+
+    // the SimpleDateFormat class is not Thread-safe the below line were changes to be not static 
+    // which given the rest of the design of SolrMarc will make them work correctly.
+    private SimpleDateFormat marc005date = new SimpleDateFormat("yyyyMMddHHmmss.S");
+    private SimpleDateFormat marc008date = new SimpleDateFormat("yyMMdd");
+
+    private static final Pattern COORDINATES_PATTERN = Pattern.compile("^([eEwWnNsS])(\\d{3})(\\d{2})(\\d{2})");
+    private static final Pattern HDMSHDD_PATTERN = Pattern.compile("^([eEwWnNsS])(\\d+(\\.\\d+)?)");
+    private static final Pattern PMDD_PATTERN = Pattern.compile("^([+-])(\\d+(\\.\\d+)?)");
+
+    private static ConcurrentHashMap<String, Ini> configCache = new ConcurrentHashMap<String, Ini>();
+    private ConcurrentHashMap<String, String> relatorSynonymLookup = new ConcurrentHashMap<String, String>();
+    private Set<String> knownRelators = new LinkedHashSet<String>();
+
+    // Shutdown flag:
+    private boolean shuttingDown = false;
+
+    // VuFind-specific configs:
+    private Properties vuFindConfigs = null;
+
+    /**
+     * Default constructor
+     * @param propertiesMapFile the {@code x_index.properties} file mapping solr
+     *  field names to values in the marc records
+     * @param propertyDirs array of directories holding properties files
+     * @throws Exception if {@code SolrIndexer} constructor threw an exception.
+     */
+    public VuFindIndexer(final String propertiesMapFile, final String[] propertyDirs)
+            throws FileNotFoundException, IOException, ParseException {
+        super(propertiesMapFile, propertyDirs);
+        try {
+            vuFindConfigs = Utils.loadProperties(propertyDirs, "vufind.properties");
+        } catch (IllegalArgumentException e) {
+            // If the properties load failed, don't worry about it -- we'll use defaults.
+        }
+    }
+
+    /**
+     * Log an error message and throw a fatal exception.
+     * @param msg message to log
+     */
+    private void dieWithError(String msg)
+    {
+        logger.error(msg);
+        throw new SolrMarcIndexerException(SolrMarcIndexerException.EXIT, msg);
+    }
+
+    /**
+     * Given the base name of a configuration file, locate the full path.
+     * @param filename base name of a configuration file
+     */
+    private File findConfigFile(String filename)
+    {
+        // Find VuFind's home directory in the environment; if it's not available,
+        // try using a relative path on the assumption that we are currently in
+        // VuFind's import subdirectory:
+        String vufindHome = System.getenv("VUFIND_HOME");
+        if (vufindHome == null) {
+            vufindHome = "..";
+        }
+
+        // Check for VuFind 2.0's local directory environment variable:
+        String vufindLocal = System.getenv("VUFIND_LOCAL_DIR");
+
+        // Get the relative VuFind path from the properties file, defaulting to
+        // the 2.0-style config/vufind if necessary.
+        String relativeConfigPath = Utils.getProperty(
+            vuFindConfigs, "vufind.config.relative_path", "config/vufind"
+        );
+
+        // Try several different locations for the file -- VuFind 2 local dir,
+        // VuFind 2 base dir, VuFind 1 base dir.
+        File file;
+        if (vufindLocal != null) {
+            file = new File(vufindLocal + "/" + relativeConfigPath + "/" + filename);
+            if (file.exists()) {
+                return file;
+            }
+        }
+        file = new File(vufindHome + "/" + relativeConfigPath + "/" + filename);
+        if (file.exists()) {
+            return file;
+        }
+        file = new File(vufindHome + "/web/conf/" + filename);
+        return file;
+    }
+
+    /**
+     * Sanitize a VuFind configuration setting.
+     * @param str configuration setting
+     */
+    private String sanitizeConfigSetting(String str)
+    {
+        // Drop comments if necessary:
+        int pos = str.indexOf(';');
+        if (pos >= 0) {
+            str = str.substring(0, pos).trim();
+        }
+
+        // Strip wrapping quotes if necessary (the ini reader won't do this for us):
+        if (str.startsWith("\"")) {
+            str = str.substring(1, str.length());
+        }
+        if (str.endsWith("\"")) {
+            str = str.substring(0, str.length() - 1);
+        }
+        return str;
+    }
+
+    /**
+     * Load an ini file.
+     * @param filename name of {@code .ini} file
+     */
+    public Ini loadConfigFile(String filename)
+    {
+        // Retrieve the file if it is not already cached.
+        if (!configCache.containsKey(filename)) {
+            Ini ini = new Ini();
+            try {
+                ini.load(new FileReader(findConfigFile(filename)));
+                configCache.putIfAbsent(filename, ini);
+            } catch (Throwable e) {
+                dieWithError("Unable to access " + filename);
+            }
+        }
+        return configCache.get(filename);
+    }
+
+    /**
+     * Get a section from a VuFind configuration file.
+     * @param filename configuration file name
+     * @param section section name within the file
+     */
+    public Map<String, String> getConfigSection(String filename, String section)
+    {
+        // Grab the ini file.
+        Ini ini = loadConfigFile(filename);
+        Map<String, String> retVal = ini.get(section);
+
+        String parent = ini.get("Parent_Config", "path");
+        while (parent != null) {
+            Ini parentIni = loadConfigFile(parent);
+            Map<String, String> parentSection = parentIni.get(section);
+            for (String key : parentSection.keySet()) {
+                if (!retVal.containsKey(key)) {
+                    retVal.put(key, parentSection.get(key));
+                }
+            }
+            parent = parentIni.get("Parent_Config", "path");
+        }
+
+        // Check to see if we need to worry about an override file:
+        String override = ini.get("Extra_Config", "local_overrides");
+        if (override != null) {
+            Map<String, String> overrideSection = loadConfigFile(override).get(section);
+            for (String key : overrideSection.keySet()) {
+                retVal.put(key, overrideSection.get(key));
+            }
+        }
+        return retVal;
+    }
+
+    /**
+     * Get a setting from a VuFind configuration file.
+     * @param filename configuration file name
+     * @param section section name within the file
+     * @param setting setting name within the section
+     */
+    public String getConfigSetting(String filename, String section, String setting)
+    {
+        String retVal = null;
+
+        // Grab the ini file.
+        Ini ini = loadConfigFile(filename);
+
+        // Check to see if we need to worry about an override file:
+        String override = ini.get("Extra_Config", "local_overrides");
+        if (override != null) {
+            Ini overrideIni = loadConfigFile(override);
+            retVal = overrideIni.get(section, setting);
+            if (retVal != null) {
+                return sanitizeConfigSetting(retVal);
+            }
+        }
+
+        // Try to find the requested setting:
+        retVal = ini.get(section, setting);
+
+        //  No setting?  Check for a parent configuration:
+        while (retVal == null) {
+            String parent = ini.get("Parent_Config", "path");
+            if (parent !=  null) {
+                try {
+                    ini.load(new FileReader(new File(parent)));
+                } catch (Throwable e) {
+                    dieWithError("Unable to access " + parent);
+                }
+                retVal = ini.get(section, setting);
+            } else {
+                break;
+            }
+        }
+
+        // Return the processed setting:
+        return retVal == null ? null : sanitizeConfigSetting(retVal);
+    }
+
+    /**
+     * Connect to the VuFind database if we do not already have a connection.
+     */
+    private void connectToDatabase()
+    {
+        // Already connected?  Do nothing further!
+        if (vufindDatabase != null) {
+            return;
+        }
+
+        String dsn = getConfigSetting("config.ini", "Database", "database");
+
+        try {
+            // Parse key settings from the PHP-style DSN:
+            String username = "";
+            String password = "";
+            String classname = "invalid";
+            String prefix = "invalid";
+            if (dsn.substring(0, 8).equals("mysql://")) {
+                classname = "com.mysql.jdbc.Driver";
+                prefix = "mysql";
+            } else if (dsn.substring(0, 8).equals("pgsql://")) {
+                classname = "org.postgresql.Driver";
+                prefix = "postgresql";
+            }
+
+            Class.forName(classname).newInstance();
+            String[] parts = dsn.split("://");
+            if (parts.length > 1) {
+                parts = parts[1].split("@");
+                if (parts.length > 1) {
+                    dsn = prefix + "://" + parts[1];
+                    parts = parts[0].split(":");
+                    username = parts[0];
+                    if (parts.length > 1) {
+                        password = parts[1];
+                    }
+                }
+            }
+
+            // Connect to the database:
+            vufindDatabase = DriverManager.getConnection("jdbc:" + dsn, username, password);
+        } catch (Throwable e) {
+            dieWithError("Unable to connect to VuFind database");
+        }
+
+        Runtime.getRuntime().addShutdownHook(new VuFindShutdownThread(this));
+    }
+
+    private void disconnectFromDatabase()
+    {
+        if (vufindDatabase != null) {
+            try {
+                vufindDatabase.close();
+            } catch (SQLException e) {
+                System.err.println("Unable to disconnect from VuFind database");
+                logger.error("Unable to disconnect from VuFind database");
+            }
+        }
+    }
+
+    public void shutdown()
+    {
+        disconnectFromDatabase();
+        shuttingDown = true;
+    }
+
+    class VuFindShutdownThread extends Thread
+    {
+        private VuFindIndexer indexer;
+
+        public VuFindShutdownThread(VuFindIndexer i)
+        {
+            indexer = i;
+        }
+
+        public void run()
+        {
+            indexer.shutdown();
+        }
+    }
+
+    /**
+     * Establish UpdateDateTracker object if not already available.
+     */
+    private void loadUpdateDateTracker() throws java.sql.SQLException
+    {
+        if (tracker == null) {
+            connectToDatabase();
+            tracker = new UpdateDateTracker(vufindDatabase);
+        }
+    }
+
+    /**
+     * Support method for getLatestTransaction.
+     * @return Date extracted from 005 (or very old date, if unavailable)
+     */
+    private java.util.Date normalize005Date(String input)
+    {
+        // Normalize "null" strings to a generic bad value:
+        if (input == null) {
+            input = "null";
+        }
+
+        // Try to parse the date; default to "millisecond 0" (very old date) if we can't
+        // parse the data successfully.
+        java.util.Date retVal;
+        try {
+            retVal = marc005date.parse(input);
+        } catch(java.text.ParseException e) {
+            retVal = new java.util.Date(0);
+        }
+        return retVal;
+    }
+
+    /**
+     * Support method for getLatestTransaction.
+     * @return Date extracted from 008 (or very old date, if unavailable)
+     */
+    private java.util.Date normalize008Date(String input)
+    {
+        // Normalize "null" strings to a generic bad value:
+        if (input == null || input.length() < 6) {
+            input = "null";
+        }
+
+        // Try to parse the date; default to "millisecond 0" (very old date) if we can't
+        // parse the data successfully.
+        java.util.Date retVal;
+        try {
+            retVal = marc008date.parse(input.substring(0, 6));
+        } catch(java.lang.StringIndexOutOfBoundsException e) {
+            retVal = new java.util.Date(0);
+        } catch(java.text.ParseException e) {
+            retVal = new java.util.Date(0);
+        }
+        return retVal;
+    }
+
+    /**
+     * Extract the latest transaction date from the MARC record.  This is useful
+     * for detecting when a record has changed since the last time it was indexed.
+     *
+     * @param record MARC record
+     * @return Latest transaction date.
+     */
+    public java.util.Date getLatestTransaction(Record record) {
+        // First try the 005 -- this is most likely to have a precise transaction date:
+        Set<String> dates = getFieldList(record, "005");
+        if (dates != null) {
+            Iterator<String> dateIter = dates.iterator();
+            if (dateIter.hasNext()) {
+                return normalize005Date(dateIter.next());
+            }
+        }
+
+        // No luck with 005?  Try 008 next -- less precise, but better than nothing:
+        dates = getFieldList(record, "008");
+        if (dates != null) {
+            Iterator<String> dateIter = dates.iterator();
+            if (dateIter.hasNext()) {
+                return normalize008Date(dateIter.next());
+            }
+        }
+
+        // If we got this far, we couldn't find a valid value; return an arbitrary date:
+        return new java.util.Date(0);
+    }
+
+    /**
+     * Get all available publishers from the record.
+     *
+     * @param  record MARC record
+     * @return set of publishers
+     */
+    public Set<String> getPublishers(final Record record) {
+        Set<String> publishers = new LinkedHashSet<String>();
+
+        // First check old-style 260b name:
+        List<VariableField> list260 = record.getVariableFields("260");
+        for (VariableField vf : list260)
+        {
+            DataField df = (DataField) vf;
+            String currentString = "";
+            for (Subfield current : df.getSubfields('b')) {
+                currentString = currentString.trim().concat(" " + current.getData()).trim();
+            }
+            if (currentString.length() > 0) {
+                publishers.add(currentString);
+            }
+        }
+
+        // Now track down relevant RDA-style 264b names; we only care about
+        // copyright and publication names (and ignore copyright names if
+        // publication names are present).
+        Set<String> pubNames = new LinkedHashSet<String>();
+        Set<String> copyNames = new LinkedHashSet<String>();
+        List<VariableField> list264 = record.getVariableFields("264");
+        for (VariableField vf : list264)
+        {
+            DataField df = (DataField) vf;
+            String currentString = "";
+            for (Subfield current : df.getSubfields('b')) {
+                currentString = currentString.trim().concat(" " + current.getData()).trim();
+            }
+            if (currentString.length() > 0) {
+                char ind2 = df.getIndicator2();
+                switch (ind2)
+                {
+                    case '1':
+                        pubNames.add(currentString);
+                        break;
+                    case '4':
+                        copyNames.add(currentString);
+                        break;
+                }
+            }
+        }
+        if (pubNames.size() > 0) {
+            publishers.addAll(pubNames);
+        } else if (copyNames.size() > 0) {
+            publishers.addAll(copyNames);
+        }
+
+        return publishers;
+    }
+
+    /**
+     * Get all available dates from the record.
+     *
+     * @param  record MARC record
+     * @return set of dates
+     */
+    public Set<String> getDates(final Record record) {
+        Set<String> dates = new LinkedHashSet<String>();
+
+        // First check old-style 260c date:
+        List<VariableField> list260 = record.getVariableFields("260");
+        for (VariableField vf : list260) {
+            DataField df = (DataField) vf;
+            List<Subfield> currentDates = df.getSubfields('c');
+            for (Subfield sf : currentDates) {
+                String currentDateStr = Utils.cleanDate(sf.getData());
+                if (currentDateStr != null) dates.add(currentDateStr);
+            }
+        }
+
+        // Now track down relevant RDA-style 264c dates; we only care about
+        // copyright and publication dates (and ignore copyright dates if
+        // publication dates are present).
+        Set<String> pubDates = new LinkedHashSet<String>();
+        Set<String> copyDates = new LinkedHashSet<String>();
+        List<VariableField> list264 = record.getVariableFields("264");
+        for (VariableField vf : list264) {
+            DataField df = (DataField) vf;
+            List<Subfield> currentDates = df.getSubfields('c');
+            for (Subfield sf : currentDates) {
+                String currentDateStr = Utils.cleanDate(sf.getData());
+                char ind2 = df.getIndicator2();
+                switch (ind2)
+                {
+                    case '1':
+                        if (currentDateStr != null) pubDates.add(currentDateStr);
+                        break;
+                    case '4':
+                        if (currentDateStr != null) copyDates.add(currentDateStr);
+                        break;
+                }
+            }
+        }
+        if (pubDates.size() > 0) {
+            dates.addAll(pubDates);
+        } else if (copyDates.size() > 0) {
+            dates.addAll(copyDates);
+        }
+
+        return dates;
+    }
+
+    /**
+     * Get the earliest publication date from the record.
+     *
+     * @param  record MARC record
+     * @return earliest date
+     */
+    public String getFirstDate(final Record record) {
+        String result = null;
+        Set<String> dates = getDates(record);
+        for(String current: dates) {
+            if (result == null || Integer.parseInt(current) < Integer.parseInt(result)) {
+                result = current;
+            }
+        }
+        return result;
+    }
+
+    /**
+     * Determine Record Format(s)
+     *
+     * @param  record MARC record
+     * @return set of record formats
+     */
+    public Set<String> getFormat(final Record record){
+        Set<String> result = new LinkedHashSet<String>();
+        String leader = record.getLeader().toString();
+        char leaderBit;
+        ControlField fixedField = (ControlField) record.getVariableField("008");
+        DataField title = (DataField) record.getVariableField("245");
+        String formatString;
+        char formatCode = ' ';
+        char formatCode2 = ' ';
+        char formatCode4 = ' ';
+
+        // check if there's an h in the 245
+        if (title != null) {
+            if (title.getSubfield('h') != null){
+                if (title.getSubfield('h').getData().toLowerCase().contains("[electronic resource]")) {
+                    result.add("Electronic");
+                    return result;
+                }
+            }
+        }
+
+        // check the 007 - this is a repeating field
+        List<VariableField> fields = record.getVariableFields("007");
+        Iterator<VariableField> fieldsIter = fields.iterator();
+        if (fields != null) {
+            // TODO: update loop to for(:) syntax, but problem with type casting.
+            ControlField formatField;
+            while(fieldsIter.hasNext()) {
+                formatField = (ControlField) fieldsIter.next();
+                formatString = formatField.getData().toUpperCase();
+                formatCode = formatString.length() > 0 ? formatString.charAt(0) : ' ';
+                formatCode2 = formatString.length() > 1 ? formatString.charAt(1) : ' ';
+                formatCode4 = formatString.length() > 4 ? formatString.charAt(4) : ' ';
+                switch (formatCode) {
+                    case 'A':
+                        switch(formatCode2) {
+                            case 'D':
+                                result.add("Atlas");
+                                break;
+                            default:
+                                result.add("Map");
+                                break;
+                        }
+                        break;
+                    case 'C':
+                        switch(formatCode2) {
+                            case 'A':
+                                result.add("TapeCartridge");
+                                break;
+                            case 'B':
+                                result.add("ChipCartridge");
+                                break;
+                            case 'C':
+                                result.add("DiscCartridge");
+                                break;
+                            case 'F':
+                                result.add("TapeCassette");
+                                break;
+                            case 'H':
+                                result.add("TapeReel");
+                                break;
+                            case 'J':
+                                result.add("FloppyDisk");
+                                break;
+                            case 'M':
+                            case 'O':
+                                result.add("CDROM");
+                                break;
+                            case 'R':
+                                // Do not return - this will cause anything with an
+                                // 856 field to be labeled as "Electronic"
+                                break;
+                            default:
+                                result.add("Software");
+                                break;
+                        }
+                        break;
+                    case 'D':
+                        result.add("Globe");
+                        break;
+                    case 'F':
+                        result.add("Braille");
+                        break;
+                    case 'G':
+                        switch(formatCode2) {
+                            case 'C':
+                            case 'D':
+                                result.add("Filmstrip");
+                                break;
+                            case 'T':
+                                result.add("Transparency");
+                                break;
+                            default:
+                                result.add("Slide");
+                                break;
+                        }
+                        break;
+                    case 'H':
+                        result.add("Microfilm");
+                        break;
+                    case 'K':
+                        switch(formatCode2) {
+                            case 'C':
+                                result.add("Collage");
+                                break;
+                            case 'D':
+                                result.add("Drawing");
+                                break;
+                            case 'E':
+                                result.add("Painting");
+                                break;
+                            case 'F':
+                                result.add("Print");
+                                break;
+                            case 'G':
+                                result.add("Photonegative");
+                                break;
+                            case 'J':
+                                result.add("Print");
+                                break;
+                            case 'L':
+                                result.add("Drawing");
+                                break;
+                            case 'O':
+                                result.add("FlashCard");
+                                break;
+                            case 'N':
+                                result.add("Chart");
+                                break;
+                            default:
+                                result.add("Photo");
+                                break;
+                        }
+                        break;
+                    case 'M':
+                        switch(formatCode2) {
+                            case 'F':
+                                result.add("VideoCassette");
+                                break;
+                            case 'R':
+                                result.add("Filmstrip");
+                                break;
+                            default:
+                                result.add("MotionPicture");
+                                break;
+                        }
+                        break;
+                    case 'O':
+                        result.add("Kit");
+                        break;
+                    case 'Q':
+                        result.add("MusicalScore");
+                        break;
+                    case 'R':
+                        result.add("SensorImage");
+                        break;
+                    case 'S':
+                        switch(formatCode2) {
+                            case 'D':
+                                result.add("SoundDisc");
+                                break;
+                            case 'S':
+                                result.add("SoundCassette");
+                                break;
+                            default:
+                                result.add("SoundRecording");
+                                break;
+                        }
+                        break;
+                    case 'V':
+                        switch(formatCode2) {
+                            case 'C':
+                                result.add("VideoCartridge");
+                                break;
+                            case 'D':
+                                switch(formatCode4) {
+                                    case 'S':
+                                        result.add("BRDisc");
+                                        break;
+                                    case 'V':
+                                    default:
+                                        result.add("VideoDisc");
+                                        break;
+                                }
+                                break;
+                            case 'F':
+                                result.add("VideoCassette");
+                                break;
+                            case 'R':
+                                result.add("VideoReel");
+                                break;
+                            default:
+                                result.add("Video");
+                                break;
+                        }
+                        break;
+                }
+            }
+            if (!result.isEmpty()) {
+                return result;
+            }
+        }
+
+        // check the Leader at position 6
+        leaderBit = leader.charAt(6);
+        switch (Character.toUpperCase(leaderBit)) {
+            case 'C':
+            case 'D':
+                result.add("MusicalScore");
+                break;
+            case 'E':
+            case 'F':
+                result.add("Map");
+                break;
+            case 'G':
+                result.add("Slide");
+                break;
+            case 'I':
+                result.add("SoundRecording");
+                break;
+            case 'J':
+                result.add("MusicRecording");
+                break;
+            case 'K':
+                result.add("Photo");
+                break;
+            case 'M':
+                result.add("Electronic");
+                break;
+            case 'O':
+            case 'P':
+                result.add("Kit");
+                break;
+            case 'R':
+                result.add("PhysicalObject");
+                break;
+            case 'T':
+                result.add("Manuscript");
+                break;
+        }
+        if (!result.isEmpty()) {
+            return result;
+        }
+
+        // check the Leader at position 7
+        leaderBit = leader.charAt(7);
+        switch (Character.toUpperCase(leaderBit)) {
+            // Monograph
+            case 'M':
+                if (formatCode == 'C') {
+                    result.add("eBook");
+                } else {
+                    result.add("Book");
+                }
+                break;
+            // Component parts
+            case 'A':
+                result.add("BookComponentPart");
+                break;
+            case 'B':
+                result.add("SerialComponentPart");
+                break;
+            // Serial
+            case 'S':
+                // Look in 008 to determine what type of Continuing Resource
+                formatCode = fixedField.getData().toUpperCase().charAt(21);
+                switch (formatCode) {
+                    case 'N':
+                        result.add("Newspaper");
+                        break;
+                    case 'P':
+                        result.add("Journal");
+                        break;
+                    default:
+                        result.add("Serial");
+                        break;
+                }
+        }
+
+        // Nothing worked!
+        if (result.isEmpty()) {
+            result.add("Unknown");
+        }
+
+        return result;
+    }
+
+    /**
+     * Get call numbers of a specific type.
+     * 
+     * <p>{@code fieldSpec} is of form {@literal 098abc:099ab}, does not accept subfield ranges.
+     *
+     *
+     * @param record  current MARC record
+     * @param fieldSpec  which MARC fields / subfields need to be analyzed
+     * @param callTypeSf  subfield containing call number type, single character only
+     * @param callType  literal call number code
+     * @param result  a collection to gather the call numbers
+     * @return collection of call numbers, same object as {@code result}
+     */
+    public static Collection<String> getCallNumberByTypeCollector(
+            Record record, String fieldSpec, String callTypeSf, String callType, Collection<String> result) {
+        for (String tag : fieldSpec.split(":")) {
+            // Check to ensure tag length is at least 3 characters
+            if (tag.length() < 3) {
+                //TODO: Should this go to a log? Better message for a bad tag in a field spec?
+                System.err.println("Invalid tag specified: " + tag);
+                continue;
+            }
+            String dfTag = tag.substring(0, 3);
+            String sfSpec = null;
+            if (tag.length() > 3) {
+                    sfSpec = tag.substring(3);
+            }
+
+            // do all fields for this tag
+            for (VariableField vf : record.getVariableFields(dfTag)) {
+                // Assume tag represents a DataField
+                DataField df = (DataField) vf;
+                boolean callTypeMatch = false;
+                
+                // Assume call type subfield could repeat
+                for (Subfield typeSf : df.getSubfields(callTypeSf)) {
+                    if (callTypeSf.indexOf(typeSf.getCode()) != -1 && typeSf.getData().equals(callType)) {
+                        callTypeMatch = true;
+                    }
+                }
+                System.err.println("callTypeMatch after loop: " + callTypeMatch);
+                if (callTypeMatch) {
+                    result.add(df.getSubfieldsAsString(sfSpec));
+                }
+            } // end loop over variable fields
+        } // end loop over fieldSpec
+        return result;
+    }
+    
+
+    /**
+     * Get call numbers of a specific type.
+     * 
+     * <p>{@code fieldSpec} is of form {@literal 098abc:099ab}, does not accept subfield ranges.
+     *
+     * @param record  current MARC record
+     * @param fieldSpec  which MARC fields / subfields need to be analyzed
+     * @param callTypeSf  subfield containing call number type, single character only
+     * @param callType  literal call number code
+     * @return set of call numbers
+     */
+    public static Set<String> getCallNumberByType(Record record, String fieldSpec, String callTypeSf, String callType) {
+        return (Set<String>) getCallNumberByTypeCollector(record, fieldSpec, callTypeSf, callType,
+                new LinkedHashSet<String>());
+    }
+
+    /**
+     * Get call numbers of a specific type.
+     * 
+     * <p>{@code fieldSpec} is of form {@literal 098abc:099ab}, does not accept subfield ranges.
+     *
+     * @param record  current MARC record
+     * @param fieldSpec  which MARC fields / subfields need to be analyzed
+     * @param callTypeSf  subfield containing call number type, single character only
+     * @param callType  literal call number code
+     * @return list of call numbers
+     */
+    public static List<String> getCallNumberByTypeAsList(Record record, String fieldSpec, String callTypeSf, String callType) {
+        return (List<String>) getCallNumberByTypeCollector(record, fieldSpec, callTypeSf, callType,
+                new ArrayList<String>());
+    }
+    
+    /**
+     * Extract the full call number from a record, stripped of spaces
+     * @param record MARC record
+     * @return Call number label
+     * @deprecated Obsolete as of VuFind 2.4.
+     *          This method exists only to support the VuFind call number search, version <= 2.3.
+     *          As of VuFind 2.4, the munging for call number search in handled entirely in Solr.
+     */
+    @Deprecated
+    public String getFullCallNumber(final Record record) {
+
+        return(getFullCallNumber(record, "099ab:090ab:050ab"));
+    }
+
+    /**
+     * Extract the full call number from a record, stripped of spaces
+     * @param record MARC record
+     * @param fieldSpec taglist for call number fields
+     * @return Call number label
+     * @deprecated Obsolete as of VuFind 2.4.
+     *          This method exists only to support the VuFind call number search, version <= 2.3.
+     *          As of VuFind 2.4, the munging for call number search in handled entirely in Solr.
+     */
+    @Deprecated
+    public String getFullCallNumber(final Record record, String fieldSpec) {
+
+        String val = getFirstFieldVal(record, fieldSpec);
+
+        if (val != null) {
+            return val.toUpperCase().replaceAll(" ", "");
+        } else {
+            return val;
+        }
+    }
+
+    /**
+     * Extract the call number label from a record
+     * @param record MARC record
+     * @return Call number label
+     */
+    public String getCallNumberLabel(final Record record) {
+
+        return getCallNumberLabel(record, "090a:050a");
+    }
+
+    /**
+     * Extract the call number label from a record
+     * @param record MARC record
+     * @param fieldSpec taglist for call number fields
+     * @return Call number label
+     */
+    public String getCallNumberLabel(final Record record, String fieldSpec) {
+
+        String val = getFirstFieldVal(record, fieldSpec);
+
+        if (val != null) {
+            int dotPos = val.indexOf(".");
+            if (dotPos > 0) {
+                val = val.substring(0, dotPos);
+            }
+            return val.toUpperCase();
+        } else {
+            return val;
+        }
+    }
+
+    /**
+     * Extract the subject component of the call number
+     *
+     * Can return null
+     *
+     * @param record MARC record
+     * @return Call number subject letters
+     */
+    public String getCallNumberSubject(final Record record) {
+
+        return(getCallNumberSubject(record, "090a:050a"));
+    }
+
+    /**
+     * Extract the subject component of the call number
+     *
+     * Can return null
+     *
+     * @param record current MARC record
+     * @return Call number subject letters
+     */
+    public String getCallNumberSubject(final Record record, String fieldSpec) {
+
+        String val = getFirstFieldVal(record, fieldSpec);
+
+        if (val != null) {
+            String [] callNumberSubject = val.toUpperCase().split("[^A-Z]+");
+            if (callNumberSubject.length > 0)
+            {
+                return callNumberSubject[0];
+            }
+        }
+        return(null);
+    }
+
+    /**
+     * Normalize a single LC call number
+     * @param record current MARC record
+     * @return String Normalized LCCN
+     */
+    public String getFullCallNumberNormalized(final Record record) {
+
+        return(getFullCallNumberNormalized(record, "099ab:090ab:050ab"));
+    }
+
+    /**
+     * Normalize a single LC call number
+     * @param record current MARC record
+     * @param fieldSpec which MARC fields / subfields need to be analyzed
+     * @return String Normalized LC call number
+     */
+    public String getFullCallNumberNormalized(final Record record, String fieldSpec) {
+
+        // TODO: is the null fieldSpec still an issue?
+        if (fieldSpec != null) {
+            String cn = getFirstFieldVal(record, fieldSpec);
+            return (new LCCallNumber(cn)).getShelfKey();
+        }
+        // If we got this far, we couldn't find a valid value:
+        return null;
+    }
+
+    /**
+     * Determine if a record is illustrated.
+     *
+     * @param  LC call number
+     * @return "Illustrated" or "Not Illustrated"
+     */
+    public String isIllustrated(Record record) {
+        String leader = record.getLeader().toString();
+
+        // Does the leader indicate this is a "language material" that might have extra
+        // illustration details in the fixed fields?
+        if (leader.charAt(6) == 'a') {
+            String currentCode = "";         // for use in loops below
+
+            // List of 008/18-21 codes that indicate illustrations:
+            String illusCodes = "abcdefghijklmop";
+
+            // Check the illustration characters of the 008:
+            ControlField fixedField = (ControlField) record.getVariableField("008");
+            if (fixedField != null) {
+                String fixedFieldText = fixedField.getData().toLowerCase();
+                for (int i = 18; i <= 21; i++) {
+                    if (i < fixedFieldText.length()) {
+                        currentCode = fixedFieldText.substring(i, i + 1);
+                        if (illusCodes.contains(currentCode)) {
+                            return "Illustrated";
+                        }
+                    }
+                }
+            }
+
+            // Now check if any 006 fields apply:
+            List<VariableField> fields = record.getVariableFields("006");
+            Iterator<VariableField> fieldsIter = fields.iterator();
+            if (fields != null) {
+                while(fieldsIter.hasNext()) {
+                    fixedField = (ControlField) fieldsIter.next();
+                    String fixedFieldText = fixedField.getData().toLowerCase();
+                    for (int i = 1; i <= 4; i++) {
+                         if (i < fixedFieldText.length()) {
+                            currentCode = fixedFieldText.substring(i, i + 1);
+                            if (illusCodes.contains(currentCode)) {
+                                return "Illustrated";
+                            }
+                        }
+                    }
+                }
+            }
+        }
+
+        // Now check for interesting strings in 300 subfield b:
+        List<VariableField> fields = record.getVariableFields("300");
+        Iterator<VariableField> fieldsIter = fields.iterator();
+        if (fields != null) {
+            DataField physical;
+            while(fieldsIter.hasNext()) {
+                physical = (DataField) fieldsIter.next();
+                List<Subfield> subfields = physical.getSubfields('b');
+                for (Subfield sf: subfields) {
+                    String desc = sf.getData().toLowerCase();
+                    if (desc.contains("ill.") || desc.contains("illus.")) {
+                        return "Illustrated";
+                    }
+                }
+            }
+        }
+
+        // If we made it this far, we found no sign of illustrations:
+        return "Not Illustrated";
+    }
+
+
+    /**
+     * Normalize LC numbers for sorting purposes (use only the first valid number!).
+     * Will return first call number found if none pass validation,
+     * or empty string if no call numbers.
+     *
+     * @param  record current MARC record
+     * @param  fieldSpec which MARC fields / subfields need to be analyzed
+     * @return sortable shelf key of the first valid LC number encountered, 
+     *         otherwise shelf key of the first call number found.
+     */
+    public String getLCSortable(Record record, String fieldSpec) {
+        // Loop through the specified MARC fields:
+        Set<String> input = getFieldList(record, fieldSpec);
+        String firstCall = "";
+        for (String current : input) {
+            // If this is a valid LC number, return the sortable shelf key:
+            LCCallNumber callNum = new LCCallNumber(current);
+            if (callNum.isValid()) {
+                return callNum.getShelfKey();   // RETURN first valid
+            }
+            if (firstCall.length() == 0) {
+                firstCall = current;
+            }
+        }
+
+        // If we made it this far, did not find a valid LC number, so use what we have:
+        return new LCCallNumber(firstCall).getShelfKey();
+    }
+
+    /**
+     * Get sort key for first LC call number, identified by call type.
+     * 
+     * <p>{@code fieldSpec} is of form {@literal 098abc:099ab}, does not accept subfield ranges.
+     *
+     *
+     * @param record  current MARC record
+     * @param fieldSpec  which MARC fields / subfields need to be analyzed
+     * @param callTypeSf  subfield containing call number type, single character only
+     * @param callType  literal call number code
+     * @return sort key for first identified LC call number
+     */
+    public String getLCSortableByType(
+            Record record, String fieldSpec, String callTypeSf, String callType) {
+        String sortKey = null;
+        for (String tag : fieldSpec.split(":")) {
+            // Check to ensure tag length is at least 3 characters
+            if (tag.length() < 3) {
+                //TODO: Should this go to a log? Better message for a bad tag in a field spec?
+                System.err.println("Invalid tag specified: " + tag);
+                continue;
+            }
+            String dfTag = tag.substring(0, 3);
+            String sfSpec = null;
+            if (tag.length() > 3) {
+                    sfSpec = tag.substring(3);
+            }
+
+            // do all fields for this tag
+            for (VariableField vf : record.getVariableFields(dfTag)) {
+                // Assume tag represents a DataField
+                DataField df = (DataField) vf;
+                boolean callTypeMatch = false;
+                
+                // Assume call type subfield could repeat
+                for (Subfield typeSf : df.getSubfields(callTypeSf)) {
+                    if (callTypeSf.indexOf(typeSf.getCode()) != -1 && typeSf.getData().equals(callType)) {
+                        callTypeMatch = true;
+                    }
+                }
+                // take the first call number coded as LC
+                if (callTypeMatch) {
+                    sortKey = new LCCallNumber(df.getSubfieldsAsString(sfSpec)).getShelfKey();
+                    break;
+                }
+            } // end loop over variable fields
+        } // end loop over fieldSpec
+        return sortKey;
+    }
+
+    /**
+     * Extract a numeric portion of the Dewey decimal call number
+     *
+     * Can return null
+     *
+     * @param record current MARC record
+     * @param fieldSpec which MARC fields / subfields need to be analyzed
+     * @param precisionStr a decimal number (represented in string format) showing the
+     *  desired precision of the returned number; i.e. 100 to round to nearest hundred,
+     *  10 to round to nearest ten, 0.1 to round to nearest tenth, etc.
+     * @return Set containing requested numeric portions of Dewey decimal call numbers
+     */
+    public Set<String> getDeweyNumber(Record record, String fieldSpec, String precisionStr) {
+        // Initialize our return value:
+        Set<String> result = new LinkedHashSet<String>();
+
+        // Precision comes in as a string, but we need to convert it to a float:
+        float precision = Float.parseFloat(precisionStr);
+
+        // Loop through the specified MARC fields:
+        Set<String> input = getFieldList(record, fieldSpec);
+        for (String current: input) {
+            DeweyCallNumber callNum = new DeweyCallNumber(current);
+            if (callNum.isValid()) {
+                // Convert the numeric portion of the call number into a float:
+                float currentVal = Float.parseFloat(callNum.getClassification());
+                
+                // Round the call number value to the specified precision:
+                Float finalVal = new Float(Math.floor(currentVal / precision) * precision);
+                
+                // Convert the rounded value back to a string (with leading zeros) and save it:
+                // TODO: Provide different conversion to remove CallNumUtils dependency
+                result.add(CallNumUtils.normalizeFloat(finalVal.toString(), 3, -1));
+            }
+        }
+
+        // If we found no call number matches, return null; otherwise, return our results:
+        if (result.isEmpty())
+            return null;
+        return result;
+    }
+
+    /**
+     * Normalize Dewey numbers for searching purposes (uppercase/stripped spaces)
+     *
+     * Can return null
+     *
+     * @param record current MARC record
+     * @param fieldSpec which MARC fields / subfields need to be analyzed
+     * @return Set containing normalized Dewey numbers extracted from specified fields.
+     */
+    public Set<String> getDeweySearchable(Record record, String fieldSpec) {
+        // Initialize our return value:
+        Set<String> result = new LinkedHashSet<String>();
+
+        // Loop through the specified MARC fields:
+        Set<String> input = getFieldList(record, fieldSpec);
+        Iterator<String> iter = input.iterator();
+        while (iter.hasNext()) {
+            // Get the current string to work on:
+            String current = iter.next();
+
+            // Add valid strings to the set, normalizing them to be all uppercase
+            // and free from whitespace.
+            DeweyCallNumber callNum = new DeweyCallNumber(current);
+            if (callNum.isValid()) {
+                result.add(callNum.toString().toUpperCase().replaceAll(" ", ""));
+            }
+        }
+
+        // If we found no call numbers, return null; otherwise, return our results:
+        if (result.isEmpty())
+            return null;
+        return result;
+    }
+
+    /**
+     * Normalize Dewey numbers for sorting purposes (use only the first valid number!)
+     *
+     * Can return null
+     *
+     * @param record current MARC record
+     * @param fieldSpec which MARC fields / subfields need to be analyzed
+     * @return String containing the first valid Dewey number encountered, normalized
+     *         for sorting purposes.
+     */
+    public String getDeweySortable(Record record, String fieldSpec) {
+        // Loop through the specified MARC fields:
+        Set<String> input = getFieldList(record, fieldSpec);
+        Iterator<String> iter = input.iterator();
+        while (iter.hasNext()) {
+            // Get the current string to work on:
+            String current = iter.next();
+
+            // If this is a valid Dewey number, return the sortable shelf key:
+            DeweyCallNumber callNum = new DeweyCallNumber(current);
+            if (callNum.isValid()) {
+                return callNum.getShelfKey();
+            }
+        }
+
+        // If we made it this far, we didn't find a valid sortable Dewey number:
+        return null;
+    }
+
+    /**
+     * Get sort key for first Dewey call number, identified by call type.
+     * 
+     * <p>{@code fieldSpec} is of form {@literal 098abc:099ab}, does not accept subfield ranges.
+     *
+     *
+     * @param record  current MARC record
+     * @param fieldSpec  which MARC fields / subfields need to be analyzed
+     * @param callTypeSf  subfield containing call number type, single character only
+     * @param callType  literal call number code
+     * @return sort key for first identified Dewey call number
+     */
+    public static String getDeweySortableByType(
+            Record record, String fieldSpec, String callTypeSf, String callType) {
+        String sortKey = null;
+        for (String tag : fieldSpec.split(":")) {
+            // Check to ensure tag length is at least 3 characters
+            if (tag.length() < 3) {
+                //TODO: Should this go to a log? Better message for a bad tag in a field spec?
+                System.err.println("Invalid tag specified: " + tag);
+                continue;
+            }
+            String dfTag = tag.substring(0, 3);
+            String sfSpec = null;
+            if (tag.length() > 3) {
+                    sfSpec = tag.substring(3);
+            }
+
+            // do all fields for this tag
+            for (VariableField vf : record.getVariableFields(dfTag)) {
+                // Assume tag represents a DataField
+                DataField df = (DataField) vf;
+                boolean callTypeMatch = false;
+                
+                // Assume call type subfield could repeat
+                for (Subfield typeSf : df.getSubfields(callTypeSf)) {
+                    if (callTypeSf.indexOf(typeSf.getCode()) != -1 && typeSf.getData().equals(callType)) {
+                        callTypeMatch = true;
+                    }
+                }
+                // take the first call number coded as Dewey
+                if (callTypeMatch) {
+                    sortKey = new DeweyCallNumber(df.getSubfieldsAsString(sfSpec)).getShelfKey();
+                    break;
+                }
+            } // end loop over variable fields
+        } // end loop over fieldSpec
+        return sortKey;
+    }
+
+    
+    /**
+     * Normalize Dewey numbers for AlphaBrowse sorting purposes (use all numbers!)
+     *
+     * Can return null
+     *
+     * @param record current MARC record
+     * @param fieldSpec which MARC fields / subfields need to be analyzed
+     * @return List containing normalized Dewey numbers extracted from specified fields.
+     */
+    public List<String> getDeweySortables(Record record, String fieldSpec) {
+        // Initialize our return value:
+        List<String> result = new LinkedList<String>();
+
+        // Loop through the specified MARC fields:
+        Set<String> input = getFieldList(record, fieldSpec);
+        Iterator<String> iter = input.iterator();
+        while (iter.hasNext()) {
+            // Get the current string to work on:
+            String current = iter.next();
+
+            // gather all sort keys, even if number is not valid
+            DeweyCallNumber callNum = new DeweyCallNumber(current);
+            result.add(callNum.getShelfKey());
+        }
+
+        // If we found no call numbers, return null; otherwise, return our results:
+        if (result.isEmpty())
+            return null;
+        return result;
+    }
+
+    /**
+     * The following several methods are designed to get latitude and longitude
+     * coordinates.
+     * Records can have multiple coordinates sets of points and/or rectangles.
+     * Points are represented by coordinate sets where N=S E=W.
+     *
+     * code adapted from xrosecky - Moravian Library
+     * https://github.com/moravianlibrary/VuFind-2.x/blob/master/import/index_scripts/geo.bsh
+     * and incorporates VuFind location.bsh functionality for GoogleMap display.
+     */
+
+    /**
+     * Convert MARC coordinates into location_geo format.
+     *
+     * @param  Record record
+     * @return List   geo_coordinates
+     */
+    public List<String> getAllCoordinates(Record record) {
+        List<String> geo_coordinates = new ArrayList<String>();
+        List<VariableField> list034 = record.getVariableFields("034");
+        if (list034 != null) {
+            for (VariableField vf : list034) {
+                DataField df = (DataField) vf;
+                String d = df.getSubfield('d').getData();
+                String e = df.getSubfield('e').getData();
+                String f = df.getSubfield('f').getData();
+                String g = df.getSubfield('g').getData();
+                //System.out.println("raw Coords: "+d+" "+e+" "+f+" "+g);
+
+                // Check to see if there are only 2 coordinates
+                // If so, copy them into the corresponding coordinate fields
+                if ((d !=null && (e == null || e.trim().equals(""))) && (f != null && (g==null || g.trim().equals("")))) {
+                    e = d;
+                    g = f;
+                }
+                if ((e !=null && (d == null || d.trim().equals(""))) && (g != null && (f==null || f.trim().equals("")))) {
+                    d = e;
+                    f = g;
+                }
+
+                // Check and convert coordinates to +/- decimal degrees
+                Double west = convertCoordinate(d);
+                Double east = convertCoordinate(e);
+                Double north = convertCoordinate(f);
+                Double south = convertCoordinate(g);
+
+                // New Format for indexing coordinates in Solr 5.0 - minX, maxX, maxY, minY
+                // Note - storage in Solr follows the WENS order, but display is WSEN order
+                String result = String.format("ENVELOPE(%s,%s,%s,%s)", new Object[] { west, east, north, south });
+
+                if (validateCoordinates(west, east, north, south)) {
+                    geo_coordinates.add(result);
+                }
+            }
+        }
+        return geo_coordinates;
+    }
+
+    /**
+     * Get point coordinates for GoogleMap display.
+     *
+     * @param  Record record
+     * @return List   coordinates
+     */
+    public List<String> getPointCoordinates(Record record) {
+        List<String> coordinates = new ArrayList<String>();
+        List<VariableField> list034 = record.getVariableFields("034");
+        if (list034 != null) {
+            for (VariableField vf : list034) {
+                DataField df = (DataField) vf;
+                String d = df.getSubfield('d').getData();
+                String e = df.getSubfield('e').getData();
+                String f = df.getSubfield('f').getData();
+                String g = df.getSubfield('g').getData();
+
+                // Check to see if there are only 2 coordinates
+                if ((d !=null && (e == null || e.trim().equals(""))) && (f != null && (g==null || g.trim().equals("")))) {
+                    Double long_val = convertCoordinate(d);
+                    Double lat_val = convertCoordinate(f);
+                    String longlatCoordinate = Double.toString(long_val) + ',' + Double.toString(lat_val);
+                    coordinates.add(longlatCoordinate);
+                }
+                if ((e !=null && (d == null || d.trim().equals(""))) && (g != null && (f==null || f.trim().equals("")))) {
+                    Double long_val = convertCoordinate(e);
+                    Double lat_val = convertCoordinate(g);
+                    String longlatCoordinate = Double.toString(long_val) + ',' + Double.toString(lat_val);
+                    coordinates.add(longlatCoordinate);
+                }
+                // Check if N=S and E=W
+                if (d.equals(e) && f.equals(g)) {
+                    Double long_val = convertCoordinate(d);
+                    Double lat_val = convertCoordinate(f);
+                    String longlatCoordinate = Double.toString(long_val) + ',' + Double.toString(lat_val);
+                    coordinates.add(longlatCoordinate);
+                }
+            }
+        }
+        return coordinates;
+    }
+
+    /**
+     * Get all available coordinates from the record.
+     *
+     * @param  Record record
+     * @return List   geo_coordinates
+     */
+    public List<String> getDisplayCoordinates(Record record) {
+        List<String> geo_coordinates = new ArrayList<String>();
+        List<VariableField> list034 = record.getVariableFields("034");
+        if (list034 != null) {
+            for (VariableField vf : list034) {
+                DataField df = (DataField) vf;
+                String west = df.getSubfield('d').getData();
+                String east = df.getSubfield('e').getData();
+                String north = df.getSubfield('f').getData();
+                String south = df.getSubfield('g').getData();
+                String result = String.format("%s %s %s %s", new Object[] { west, east, north, south });
+                if (west != null || east != null || north != null || south != null) {
+                    geo_coordinates.add(result);
+                }
+            }
+        }
+        return geo_coordinates;
+    }
+
+    /**
+     * Check coordinate type HDMS HDD or +/-DD.
+     *
+     * @param  String coordinateStr
+     * @return Double coordinate
+     */
+    protected Double convertCoordinate(String coordinateStr) {
+        Double coordinate = Double.NaN;
+        Matcher HDmatcher = HDMSHDD_PATTERN.matcher(coordinateStr);
+        Matcher PMDmatcher = PMDD_PATTERN.matcher(coordinateStr);
+        if (HDmatcher.matches()) {
+            String hemisphere = HDmatcher.group(1).toUpperCase();
+            Double degrees = Double.parseDouble(HDmatcher.group(2));
+            // Check for HDD or HDMS
+            if (hemisphere.equals("N") || hemisphere.equals("S")) {
+                if (degrees > 90) {
+                    String hdmsCoordinate = hemisphere+"0"+HDmatcher.group(2);
+                    coordinate = coordinateToDecimal(hdmsCoordinate);
+                } else {
+                    coordinate = Double.parseDouble(HDmatcher.group(2));
+                    if (hemisphere.equals("S")) {
+                        coordinate *= -1;
+                    }
+                }
+            }
+            if (hemisphere.equals("E") || hemisphere.equals("W")) {
+                if (degrees > 180) {
+                    String hdmsCoordinate = HDmatcher.group(0);
+                    coordinate = coordinateToDecimal(hdmsCoordinate);
+                } else {
+                    coordinate = Double.parseDouble(HDmatcher.group(2));
+                    if (hemisphere.equals("W")) {
+                        coordinate *= -1;
+                    }
+                }
+            }
+            return coordinate;
+        } else if (PMDmatcher.matches()) {
+            String hemisphere = PMDmatcher.group(1);
+            coordinate = Double.parseDouble(PMDmatcher.group(2));
+            if (hemisphere.equals("-")) {
+                coordinate *= -1;
+            }
+            return coordinate;
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * Convert HDMS coordinates to decimal degrees.
+     *
+     * @param  String coordinateStr
+     * @return Double coordinate
+     */
+    protected Double coordinateToDecimal(String coordinateStr) {
+        Matcher matcher = COORDINATES_PATTERN.matcher(coordinateStr);
+        if (matcher.matches()) {
+            String hemisphere = matcher.group(1).toUpperCase();
+            int degrees = Integer.parseInt(matcher.group(2));
+            int minutes = Integer.parseInt(matcher.group(3));
+            int seconds = Integer.parseInt(matcher.group(4));
+            double coordinate = degrees + (minutes / 60.0) + (seconds / 3600.0);
+            if (hemisphere.equals("W") || hemisphere.equals("S")) {
+                coordinate *= -1;
+            }
+            return coordinate;
+        }
+        return null;
+    }
+
+    /**
+     * Check decimal degree coordinates to make sure they are valid.
+     *
+     * @param  Double west, east, north, south
+     * @return boolean
+     */
+    protected boolean validateCoordinates(Double west, Double east, Double north, Double south) {
+        if (west == null || east == null || north == null || south == null) {
+            return false;
+        }
+        if (west > 180.0 || west < -180.0 || east > 180.0 || east < -180.0) {
+            return false;
+        }
+        if (north > 90.0 || north < -90.0 || south > 90.0 || south < -90.0) {
+            return false;
+        }
+        if (north < south || west > east) {
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * THIS FUNCTION HAS BEEN DEPRECATED.
+     * Determine the longitude and latitude of the items location.
+     *
+     * @param  record current MARC record
+     * @return string of form "longitude, latitude"
+     */
+    public String getLongLat(Record record) {
+        // Check 034 subfield d and f
+        List<VariableField> fields = record.getVariableFields("034");
+        Iterator<VariableField> fieldsIter = fields.iterator();
+        if (fields != null) {
+            DataField physical;
+            while(fieldsIter.hasNext()) {
+                physical = (DataField) fieldsIter.next();
+                String val = null;
+
+                List<Subfield> subfields_d = physical.getSubfields('d');
+                Iterator<Subfield> subfieldsIter_d = subfields_d.iterator();
+                if (subfields_d != null) {
+                    while (subfieldsIter_d.hasNext()) {
+                        val = subfieldsIter_d.next().getData().trim();
+                        if (!val.matches("-?\\d+(.\\d+)?")) {
+                            return null;
+                        }
+                    }
+                }
+                List<Subfield> subfields_f = physical.getSubfields('f');
+                Iterator<Subfield> subfieldsIter_f = subfields_f.iterator();
+                if (subfields_f != null) {
+                    while (subfieldsIter_f.hasNext()) {
+                        String val2 = subfieldsIter_f.next().getData().trim();
+                        if (!val2.matches("-?\\d+(.\\d+)?")) {
+                            return null;
+                        }
+                        val = val + ',' + val2;
+                    }
+                }
+                return val;
+            }
+        }
+        //otherwise return null
+        return null;
+    }
+
+    /**
+     * Update the index date in the database for the specified core/ID pair.  We
+     * maintain a database of "first/last indexed" times separately from Solr to
+     * allow the history of our indexing activity to be stored permanently in a
+     * fashion that can survive even a total Solr rebuild.
+     */
+    public UpdateDateTracker updateTracker(String core, String id, java.util.Date latestTransaction)
+    {
+        // Update the database (if necessary):
+        try {
+            // Initialize date tracker if not already initialized:
+            loadUpdateDateTracker();
+
+            tracker.index(core, id, latestTransaction);
+        } catch (java.sql.SQLException e) {
+            // If we're in the process of shutting down, an error is expected:
+            if (!shuttingDown) {
+                dieWithError("Unexpected database error");
+            }
+        }
+
+        // Send back the tracker object so the caller can use it (helpful for
+        // use in BeanShell scripts).
+        return tracker;
+    }
+
+    /**
+     * Get the "first indexed" date for the current record.  (This is the first
+     * time that SolrMarc ever encountered this particular record).
+     *
+     * @param record current MARC record
+     * @param fieldSpec fields / subfields to be analyzed
+     * @param core core name
+     * @return ID string
+     */
+    public String getFirstIndexed(Record record, String fieldSpec, String core) {
+        // Update the database, then send back the first indexed date:
+        updateTracker(core, getFirstFieldVal(record, fieldSpec), getLatestTransaction(record));
+        return tracker.getFirstIndexed();
+    }
+
+    /**
+     * Get the "first indexed" date for the current record.  (This is the first
+     * time that SolrMarc ever encountered this particular record).
+     *
+     * @param record current MARC record
+     * @param fieldSpec fields / subfields to be analyzed
+     * @return ID string
+     */
+    public String getFirstIndexed(Record record, String fieldSpec) {
+        return getFirstIndexed(record, fieldSpec, "biblio");
+    }
+
+    /**
+     * Get the "first indexed" date for the current record.  (This is the first
+     * time that SolrMarc ever encountered this particular record).
+     *
+     * @param record current MARC record
+     * @return ID string
+     */
+    public String getFirstIndexed(Record record) {
+        return getFirstIndexed(record, "001", "biblio");
+    }
+
+    /**
+     * Get the "last indexed" date for the current record.  (This is the last time
+     * the record changed from SolrMarc's perspective).
+     *
+     * @param record current MARC record
+     * @param fieldSpec fields / subfields to be analyzed
+     * @param core core name
+     * @return ID string
+     */
+    public String getLastIndexed(Record record, String fieldSpec, String core) {
+        // Update the database, then send back the last indexed date:
+        updateTracker(core, getFirstFieldVal(record, fieldSpec), getLatestTransaction(record));
+        return tracker.getLastIndexed();
+    }
+
+    /**
+     * Get the "last indexed" date for the current record.  (This is the last time
+     * the record changed from SolrMarc's perspective).
+     *
+     * @param record current MARC record
+     * @param fieldSpec fields / subfields to analyze
+     * @return ID string
+     */
+    public String getLastIndexed(Record record, String fieldSpec) {
+        return getLastIndexed(record, fieldSpec, "biblio");
+    }
+
+    /**
+     * Get the "last indexed" date for the current record.  (This is the last time
+     * the record changed from SolrMarc's perspective).
+     *
+     * @param record current MARC record
+     * @return ID string
+     */
+    public String getLastIndexed(Record record) {
+        return getLastIndexed(record, "001", "biblio");
+    }
+
+    /**
+     * Load configurations for the full text parser.  Return an array containing the
+     * parser type in the first element and the parser configuration in the second
+     * element.
+     *
+     * @return String[]
+     */
+    public String[] getFulltextParserSettings()
+    {
+        String parserType = getConfigSetting(
+            "fulltext.ini", "General", "parser"
+        );
+        if (null != parserType) {
+            parserType = parserType.toLowerCase();
+        }
+
+        // Is Aperture active?
+        String aperturePath = getConfigSetting(
+            "fulltext.ini", "Aperture", "webcrawler"
+        );
+        if ((null == parserType && null != aperturePath)
+            || (null != parserType && parserType.equals("aperture"))
+        ) {
+            String[] array = { "aperture", aperturePath };
+            return array;
+        }
+
+        // Is Tika active?
+        String tikaPath = getConfigSetting(
+            "fulltext.ini", "Tika", "path"
+        );
+        if ((null == parserType && null != tikaPath)
+            || (null != parserType && parserType.equals("tika"))
+        ) {
+            String[] array = { "tika", tikaPath };
+            return array;
+        }
+
+        // No recognized parser found:
+        String[] array = { "none", null };
+        return array;
+    }
+
+    /**
+     * Extract full-text from the documents referenced in the tags
+     *
+     * @param Record record current MARC record
+     * @param String field spec to search for URLs
+     * @param String only harvest files matching this extension (null for all)
+     * @return String The full-text
+     */
+    public String getFulltext(Record record, String fieldSpec, String extension) {
+        String result = "";
+
+        // Get the web crawler settings (and return no text if it is unavailable)
+        String[] parserSettings = getFulltextParserSettings();
+        if (parserSettings[0].equals("none")) {
+            return null;
+        }
+
+        // Loop through the specified MARC fields:
+        Set<String> fields = getFieldList(record, fieldSpec);
+        Iterator<String> fieldsIter = fields.iterator();
+        if (fields != null) {
+            while(fieldsIter.hasNext()) {
+                // Get the current string to work on (and sanitize spaces):
+                String current = fieldsIter.next().replaceAll(" ", "%20");
+                // Filter by file extension
+                if (extension == null || current.endsWith(extension)) {
+                    // Load the parser output for each tag into a string
+                    result = result + harvestWithParser(current, parserSettings);
+                }
+            }
+        }
+        // return string to SolrMarc
+        return result;
+    }
+
+    /**
+     * Extract full-text from the documents referenced in the tags
+     *
+     * @param Record record current MARC record
+     * @param String field spec to search for URLs
+     * @return String The full-text
+     */
+    public String getFulltext(Record record, String fieldSpec) {
+        return getFulltext(record, fieldSpec, null);
+    }
+
+    /**
+     * Extract full-text from the documents referenced in the tags
+     *
+     * @param Record record current MARC record
+     * @return String The full-text
+     */
+    public String getFulltext(Record record) {
+        return getFulltext(record, "856u", null);
+    }
+
+    /**
+     * Clean up XML data generated by Aperture
+     *
+     * @param f file to clean
+     * @return a fixed version of the file
+     */
+    public File sanitizeApertureOutput(File f) throws IOException
+    {
+        //clean up the aperture xml output
+        File tempFile = File.createTempFile("buffer", ".tmp");
+        FileOutputStream fw = new FileOutputStream(tempFile);
+        OutputStreamWriter writer = new OutputStreamWriter(fw, "UTF8");
+
+        //delete this control character from the File and save
+        FileReader fr = new FileReader(f);
+        BufferedReader br = new BufferedReader(fr);
+        while (br.ready()) {
+            writer.write(sanitizeFullText(br.readLine()));
+        }
+        writer.close();
+        br.close();
+        fr.close();
+
+        return tempFile;
+    }
+
+    /**
+     * Clean up bad characters in the full text.
+     *
+     * @param text text to clean
+     * @return cleaned text
+     */
+    public String sanitizeFullText(String text)
+    {
+        String badChars = "[^\\u0009\\u000A\\u000D\\u0020-\\uD7FF\\uE000-\\uFFFD\\u10000-\\u10FFFF]+";
+        return text.replaceAll(badChars, " ");
+    }
+
+    /**
+     * Harvest the contents of a document file (PDF, Word, etc.) using Aperture.
+     * This method will only work if Aperture is properly configured in the
+     * fulltext.ini file.  Without proper configuration, this will simply return an
+     * empty string.
+     *
+     * @param url the url extracted from the MARC tag.
+     * @param aperturePath The path to Aperture
+     * @return full-text extracted from url
+     */
+    public String harvestWithAperture(String url, String aperturePath) {
+        String plainText = "";
+        // Create temp file.
+        File f = null;
+        try {
+            f = File.createTempFile("apt", ".txt");
+        } catch (Throwable e) {
+            dieWithError("Unable to create temporary file for full text harvest.");
+        }
+
+        // Delete temp file when program exits.
+        f.deleteOnExit();
+
+        // Construct the command to call Aperture
+        String cmd = aperturePath + " -o " + f.getAbsolutePath().toString()  + " -x " + url;
+
+        // Call Aperture
+        //System.out.println("Loading fulltext from " + url + ". Please wait ...");
+        try {
+            Process p = Runtime.getRuntime().exec(cmd);
+            
+            // Debugging output
+            /*
+            BufferedReader stdInput = new BufferedReader(new
+                InputStreamReader(p.getInputStream()));
+            String s;
+            while ((s = stdInput.readLine()) != null) {
+                System.out.println(s);
+            }
+            */
+            
+            // Wait for Aperture to finish
+            p.waitFor();
+        } catch (Throwable e) {
+            logger.error("Problem executing Aperture -- " + e.getMessage());
+        }
+
+        // Parse Aperture XML output
+        Document xmlDoc = null;
+        try {
+            DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
+            DocumentBuilder db = dbf.newDocumentBuilder();
+            File tempFile = sanitizeApertureOutput(f);
+            xmlDoc = db.parse(tempFile);
+            NodeList nl = xmlDoc.getElementsByTagName("plainTextContent");
+            if(nl != null && nl.getLength() > 0) {
+                Node node = nl.item(0);
+                if (node.getNodeType() == Node.ELEMENT_NODE) {
+                    plainText = plainText + node.getTextContent();
+                }
+            }
+
+            // we'll hold onto the temp file if it failed to parse for debugging;
+            // only set it up to be deleted if we've made it this far successfully.
+            tempFile.deleteOnExit();
+        } catch (Throwable e) {
+            logger.error("Problem parsing Aperture XML -- " + e.getMessage());
+        }
+
+        return plainText;
+    }
+
+    /**
+     * Harvest the contents of a document file (PDF, Word, etc.) using Tika.
+     * This method will only work if Tika is properly configured in the fulltext.ini
+     * file.  Without proper configuration, this will simply return an empty string.
+     *
+     * @param url the url extracted from the MARC tag.
+     * @param scraperPath path to Tika
+     * @return the full-text
+     */
+    public String harvestWithTika(String url, String scraperPath) {
+
+        // Construct the command
+        String cmd = "java -jar " + scraperPath + " -t -eUTF8 " + url;
+
+        StringBuilder stringBuilder= new StringBuilder();
+
+        // Call our scraper
+        //System.out.println("Loading fulltext from " + url + ". Please wait ...");
+        try {
+            Process p = Runtime.getRuntime().exec(cmd);
+            BufferedReader stdInput = new BufferedReader(new
+                InputStreamReader(p.getInputStream(), "UTF8"));
+
+            // We'll build the string from the command output
+            String s;
+            while ((s = stdInput.readLine()) != null) {
+                stringBuilder.append(s);
+            }
+        } catch (Throwable e) {
+            logger.error("Problem with Tika -- " + e.getMessage());
+        }
+
+        return sanitizeFullText(stringBuilder.toString());
+    }
+
+    /**
+     * Harvest the contents of a document file (PDF, Word, etc.) using the active parser.
+     *
+     * @param url the URL extracted from the MARC tag.
+     * @param settings configuration settings from {@code getFulltextParserSettings}.
+     * @return the full-text
+     */
+    public String harvestWithParser(String url, String[] settings) {
+        if (settings[0].equals("aperture")) {
+            return harvestWithAperture(url, settings[1]);
+        } else if (settings[0].equals("tika")) {
+            return harvestWithTika(url, settings[1]);
+        }
+        return null;
+    }
+
+    /**
+     * Get access to the Logger object.
+     *
+     * @return Logger
+     */
+    public Logger getLogger()
+    {
+        return logger;
+    }
+
+    /**
+     * Extract all valid relator terms from a list of subfields using a whitelist.
+     * @param subfields        List of subfields to check
+     * @param permittedRoles   Whitelist to check against
+     * @param indexRawRelators Should we index relators raw, as found
+     * in the MARC (true) or index mapped versions (false)?
+     * @return Set of valid relator terms
+     */
+    public Set<String> getValidRelatorsFromSubfields(List<Subfield> subfields, List<String> permittedRoles, Boolean indexRawRelators)
+    {
+        Set<String> relators = new LinkedHashSet<String>();
+        for (int j = 0; j < subfields.size(); j++) {
+            String raw = subfields.get(j).getData();
+            String current = normalizeRelatorString(raw);
+            if (permittedRoles.contains(current)) {
+                relators.add(indexRawRelators ? raw : mapRelatorStringToCode(current));
+            }
+        }
+        return relators;
+    }
+
+    /**
+     * Is this relator term unknown to author-classification.ini?
+     * @param current relator to check
+     * @return True if unknown
+     */
+    public Boolean isUnknownRelator(String current)
+    {
+        // If we haven't loaded known relators yet, do so now:
+        if (knownRelators.size() == 0) {
+            Map<String, String> all = getConfigSection("author-classification.ini", "RelatorSynonyms");
+            for (String key : all.keySet()) {
+                knownRelators.add(normalizeRelatorString(key));
+                for (String synonym: all.get(key).split("\\|")) {
+                    knownRelators.add(normalizeRelatorString(synonym));
+                }
+            }
+        }
+        return !knownRelators.contains(normalizeRelatorString(current));
+    }
+
+    /**
+     * Extract all valid relator terms from a list of subfields using a whitelist.
+     * @param subfields      List of subfields to check
+     * @return Set of valid relator terms
+     */
+    public Set<String> getUnknownRelatorsFromSubfields(List<Subfield> subfields)
+    {
+        Set<String> relators = new LinkedHashSet<String>();
+        for (int j = 0; j < subfields.size(); j++) {
+            String current = subfields.get(j).getData().trim();
+            if (current.length() > 0 && isUnknownRelator(current)) {
+                logger.info("Unknown relator: " + current);
+                relators.add(current);
+            }
+        }
+        return relators;
+    }
+
+    /**
+     * Extract all values that meet the specified relator requirements.
+     * @param authorField           Field to analyze
+     * @param noRelatorAllowed      Array of tag names which are allowed to be used with
+     * no declared relator.
+     * @param relatorConfig         The setting in author-classification.ini which
+     * defines which relator terms are acceptable (or a colon-delimited list)
+     * @param unknownRelatorAllowed Array of tag names whose relators should be indexed 
+     * even if they are not listed in author-classification.ini.
+     * @param indexRawRelators      Set to "true" to index relators raw, as found
+     * in the MARC or "false" to index mapped versions.
+     * @return Set
+     */
+    public Set<String> getValidRelators(DataField authorField,
+        String[] noRelatorAllowed, String relatorConfig,
+        String[] unknownRelatorAllowed, String indexRawRelators
+    ) {
+        // get tag number from Field
+        String tag = authorField.getTag();
+        List<Subfield> subfieldE = authorField.getSubfields('e');
+        List<Subfield> subfield4 = authorField.getSubfields('4');
+
+        Set<String> relators = new LinkedHashSet<String>();
+
+        // if no relator is found, check to see if the current tag is in the "no
+        // relator allowed" list.
+        if (subfieldE.size() == 0 && subfield4.size() == 0) {
+            if (Arrays.asList(noRelatorAllowed).contains(tag)) {
+                relators.add("");
+            }
+        } else {
+            // If we got this far, we need to figure out what type of relation they have
+            List permittedRoles = normalizeRelatorStringList(Arrays.asList(loadRelatorConfig(relatorConfig)));
+            relators.addAll(getValidRelatorsFromSubfields(subfieldE, permittedRoles, indexRawRelators.toLowerCase().equals("true")));
+            relators.addAll(getValidRelatorsFromSubfields(subfield4, permittedRoles, indexRawRelators.toLowerCase().equals("true")));
+            if (Arrays.asList(unknownRelatorAllowed).contains(tag)) {
+                Set<String> unknown = getUnknownRelatorsFromSubfields(subfieldE);
+                if (unknown.size() == 0) {
+                    unknown = getUnknownRelatorsFromSubfields(subfield4);
+                }
+                relators.addAll(unknown);
+            }
+        }
+        return relators;
+    }
+
+    /**
+     * Parse a SolrMarc fieldspec into a map of tag name to set of subfield strings
+     * (note that we need to map to a set rather than a single string, because the
+     * same tag may repeat with different subfields to extract different sections
+     * of the same field into distinct values).
+     *
+     * @param tagList The field specification to parse
+     * @return HashMap
+     */
+    protected HashMap<String, Set<String>> getParsedTagList(String tagList)
+    {
+        String[] tags = tagList.split(":");//convert string input to array
+        HashMap<String, Set<String>> tagMap = new HashMap<String, Set<String>>();
+        //cut tags array up into key/value pairs in hash map
+        Set<String> currentSet;
+        for(int i = 0; i < tags.length; i++){
+            String tag = tags[i].substring(0, 3);
+            if (!tagMap.containsKey(tag)) {
+                currentSet = new LinkedHashSet<String>();
+                tagMap.put(tag, currentSet);
+            } else {
+                currentSet = tagMap.get(tag);
+            }
+            currentSet.add(tags[i].substring(3));
+        }
+        return tagMap;
+    }
+
+    /**
+     * Filter values retrieved using tagList to include only those whose relator
+     * values are acceptable. Used for separating different types of authors.
+     *
+     * @param record               The record (fed in automatically)
+     * @param tagList              The field specification to read
+     * @param acceptWithoutRelator Colon-delimited list of tags whose values should
+     * be accepted even if no relator subfield is defined
+     * @param relatorConfig        The setting in author-classification.ini which
+     * defines which relator terms are acceptable (or a colon-delimited list)
+     * @param acceptUnknownRelators Colon-delimited list of tags whose relators
+     * should be indexed even if they are not listed in author-classification.ini.
+     * @param indexRawRelators      Set to "true" to index relators raw, as found
+     * in the MARC or "false" to index mapped versions.
+     * @param firstOnly            Return first result only?
+     * @return List result
+     */
+    public List<String> getAuthorsFilteredByRelator(Record record, String tagList,
+        String acceptWithoutRelator, String relatorConfig,
+        String acceptUnknownRelators, String indexRawRelators, Boolean firstOnly
+    ) {
+        List<String> result = new LinkedList<String>();
+        String[] noRelatorAllowed = acceptWithoutRelator.split(":");
+        String[] unknownRelatorAllowed = acceptUnknownRelators.split(":");
+        HashMap<String, Set<String>> parsedTagList = getParsedTagList(tagList);
+        List fields = getFieldSetMatchingTagList(record, tagList);
+        Iterator fieldsIter = fields.iterator();
+        if (fields != null){
+            DataField authorField;
+            while (fieldsIter.hasNext()){
+                authorField = (DataField) fieldsIter.next();
+                // add all author types to the result set; if we have multiple relators, repeat the authors
+                for (String iterator: getValidRelators(authorField, noRelatorAllowed, relatorConfig, unknownRelatorAllowed, indexRawRelators)) {
+                    for (String subfields : parsedTagList.get(authorField.getTag())) {
+                        String current = getDataFromVariableField(authorField, "["+subfields+"]", " ", false);
+                        // TODO: we may eventually be able to use this line instead,
+                        // but right now it's not handling separation between the
+                        // subfields correctly, so it's commented out until that is
+                        // fixed.
+                        //String current = authorField.getSubfieldsAsString(subfields);
+                        if (null != current) {
+                            result.add(current);
+                            if (firstOnly) {
+                                return result;
+                            }
+                        }
+                    }
+                }
+            }
+        }
+        return result;
+    }
+
+    /**
+     * Filter values retrieved using tagList to include only those whose relator
+     * values are acceptable. Used for separating different types of authors.
+     *
+     * @param record               The record (fed in automatically)
+     * @param tagList              The field specification to read
+     * @param acceptWithoutRelator Colon-delimited list of tags whose values should
+     * be accepted even if no relator subfield is defined
+     * @param relatorConfig        The setting in author-classification.ini which
+     * defines which relator terms are acceptable (or a colon-delimited list)
+     * @return List result
+     */
+    public List<String> getAuthorsFilteredByRelator(Record record, String tagList,
+        String acceptWithoutRelator, String relatorConfig
+    ) {
+        // default firstOnly to false!
+        return getAuthorsFilteredByRelator(
+            record, tagList, acceptWithoutRelator, relatorConfig,
+            acceptWithoutRelator, "false", false
+        );
+    }
+
+    /**
+     * Filter values retrieved using tagList to include only those whose relator
+     * values are acceptable. Used for separating different types of authors.
+     *
+     * @param record               The record (fed in automatically)
+     * @param tagList              The field specification to read
+     * @param acceptWithoutRelator Colon-delimited list of tags whose values should
+     * be accepted even if no relator subfield is defined
+     * @param relatorConfig        The setting in author-classification.ini which
+     * defines which relator terms are acceptable (or a colon-delimited list)
+     * @return List result
+     * @param acceptUnknownRelators Colon-delimited list of tags whose relators
+     * should be indexed even if they are not listed in author-classification.ini.
+     */
+    public List<String> getAuthorsFilteredByRelator(Record record, String tagList,
+        String acceptWithoutRelator, String relatorConfig,
+        String acceptUnknownRelators
+    ) {
+        // default firstOnly to false!
+        return getAuthorsFilteredByRelator(
+            record, tagList, acceptWithoutRelator, relatorConfig,
+            acceptUnknownRelators, "false", false
+        );
+    }
+
+    /**
+     * Filter values retrieved using tagList to include only those whose relator
+     * values are acceptable. Used for separating different types of authors.
+     *
+     * @param record               The record (fed in automatically)
+     * @param tagList              The field specification to read
+     * @param acceptWithoutRelator Colon-delimited list of tags whose values should
+     * be accepted even if no relator subfield is defined
+     * @param relatorConfig        The setting in author-classification.ini which
+     * defines which relator terms are acceptable (or a colon-delimited list)
+     * @return List result
+     * @param acceptUnknownRelators Colon-delimited list of tags whose relators
+     * should be indexed even if they are not listed in author-classification.ini.
+     * @param indexRawRelators      Set to "true" to index relators raw, as found
+     * in the MARC or "false" to index mapped versions.
+     */
+    public List<String> getAuthorsFilteredByRelator(Record record, String tagList,
+        String acceptWithoutRelator, String relatorConfig,
+        String acceptUnknownRelators, String indexRawRelators
+    ) {
+        // default firstOnly to false!
+        return getAuthorsFilteredByRelator(
+            record, tagList, acceptWithoutRelator, relatorConfig,
+            acceptUnknownRelators, indexRawRelators, false
+        );
+    }
+
+    /**
+     * If the provided relator is included in the synonym list, convert it back to
+     * a code (for better standardization/translation).
+     *
+     * @param relator Relator code to check
+     * @return Code version, if found, or raw string if no match found.
+     */
+    public String mapRelatorStringToCode(String relator)
+    {
+        String normalizedRelator = normalizeRelatorString(relator);
+        return relatorSynonymLookup.containsKey(normalizedRelator)
+            ? relatorSynonymLookup.get(normalizedRelator) : relator;
+    }
+
+    /**
+     * Filter values retrieved using tagList to include only those whose relator
+     * values are acceptable. Used for separating different types of authors.
+     *
+     * @param record                The record (fed in automatically)
+     * @param tagList               The field specification to read
+     * @param acceptWithoutRelator  Colon-delimited list of tags whose values should
+     * be accepted even if no relator subfield is defined
+     * @param relatorConfig         The setting in author-classification.ini which
+     * defines which relator terms  are acceptable (or a colon-delimited list)
+     * @param acceptUnknownRelators Colon-delimited list of tags whose relators
+     * should be indexed even if they are not listed in author-classification.ini.
+     * @param indexRawRelators      Set to "true" to index relators raw, as found
+     * in the MARC or "false" to index mapped versions.
+     * @return String
+     */
+    public String getFirstAuthorFilteredByRelator(Record record, String tagList,
+        String acceptWithoutRelator, String relatorConfig,
+        String acceptUnknownRelators, String indexRawRelators
+    ) {
+        List<String> result = getAuthorsFilteredByRelator(
+            record, tagList, acceptWithoutRelator, relatorConfig,
+            acceptUnknownRelators, indexRawRelators, true
+        );
+        for (String s : result) {
+            return s;
+        }
+        return null;
+    }
+
+    /**
+     * Filter values retrieved using tagList to include only those whose relator
+     * values are acceptable. Used for separating different types of authors.
+     *
+     * @param record                The record (fed in automatically)
+     * @param tagList               The field specification to read
+     * @param acceptWithoutRelator  Colon-delimited list of tags whose values should
+     * be accepted even if no relator subfield is defined
+     * @param relatorConfig         The setting in author-classification.ini which
+     * defines which relator terms  are acceptable (or a colon-delimited list)
+     * @return String
+     */
+    public String getFirstAuthorFilteredByRelator(Record record, String tagList,
+        String acceptWithoutRelator, String relatorConfig
+    ) {
+        return getFirstAuthorFilteredByRelator(
+            record, tagList, acceptWithoutRelator, relatorConfig,
+            acceptWithoutRelator, "false"
+        );
+    }
+
+    /**
+     * Filter values retrieved using tagList to include only those whose relator
+     * values are acceptable. Used for separating different types of authors.
+     *
+     * @param record                The record (fed in automatically)
+     * @param tagList               The field specification to read
+     * @param acceptWithoutRelator  Colon-delimited list of tags whose values should
+     * be accepted even if no relator subfield is defined
+     * @param relatorConfig         The setting in author-classification.ini which
+     * defines which relator terms  are acceptable (or a colon-delimited list)
+     * @param acceptUnknownRelators Colon-delimited list of tags whose relators
+     * should be indexed even if they are not listed in author-classification.ini.
+     * @return String
+     */
+    public String getFirstAuthorFilteredByRelator(Record record, String tagList,
+        String acceptWithoutRelator, String relatorConfig,
+        String acceptUnknownRelators
+    ) {
+        return getFirstAuthorFilteredByRelator(
+            record, tagList, acceptWithoutRelator, relatorConfig,
+            acceptUnknownRelators, "false"
+        );
+    }
+
+    /**
+     * Filter values retrieved using tagList to include only those whose relator
+     * values are acceptable. Used for saving relators of authors separated by different
+     * types.
+     *
+     * @param record               The record (fed in automatically)
+     * @param tagList              The field specification to read
+     * @param acceptWithoutRelator Colon-delimited list of tags whose values should
+     * be accepted even if no relator subfield is defined
+     * @param relatorConfig        The setting in author-classification.ini which
+     * defines which relator terms are acceptable (or a colon-delimited list)
+     * @param acceptUnknownRelators Colon-delimited list of tags whose relators
+     * should be indexed even if they are not listed in author-classification.ini.
+     * @param indexRawRelators      Set to "true" to index relators raw, as found
+     * in the MARC or "false" to index mapped versions.
+     * @param firstOnly            Return first result only?
+     * @return List result
+     */
+    public List getRelatorsFilteredByRelator(Record record, String tagList,
+        String acceptWithoutRelator, String relatorConfig,
+        String acceptUnknownRelators, String indexRawRelators, Boolean firstOnly
+    ) {
+        List result = new LinkedList();
+        String[] noRelatorAllowed = acceptWithoutRelator.split(":");
+        String[] unknownRelatorAllowed = acceptUnknownRelators.split(":");
+        HashMap<String, Set<String>> parsedTagList = getParsedTagList(tagList);
+        List fields = getFieldSetMatchingTagList(record, tagList);
+        Iterator fieldsIter = fields.iterator();
+        if (fields != null){
+            DataField authorField;
+            while (fieldsIter.hasNext()){
+                authorField = (DataField) fieldsIter.next();
+                //add all author types to the result set
+                result.addAll(getValidRelators(authorField, noRelatorAllowed, relatorConfig, unknownRelatorAllowed, indexRawRelators));
+            }
+        }
+        return result;
+    }
+
+    /**
+     * Filter values retrieved using tagList to include only those whose relator
+     * values are acceptable. Used for saving relators of authors separated by different
+     * types.
+     *
+     * @param record               The record (fed in automatically)
+     * @param tagList              The field specification to read
+     * @param acceptWithoutRelator Colon-delimited list of tags whose values should
+     * be accepted even if no relator subfield is defined
+     * @param relatorConfig        The setting in author-classification.ini which
+     * defines which relator terms are acceptable (or a colon-delimited list)
+     * @param acceptUnknownRelators Colon-delimited list of tags whose relators
+     * should be indexed even if they are not listed in author-classification.ini.
+     * @param indexRawRelators      Set to "true" to index relators raw, as found
+     * in the MARC or "false" to index mapped versions.
+     * @return List result
+     */
+    public List getRelatorsFilteredByRelator(Record record, String tagList,
+        String acceptWithoutRelator, String relatorConfig,
+        String acceptUnknownRelators, String indexRawRelators
+    ) {
+        // default firstOnly to false!
+        return getRelatorsFilteredByRelator(
+            record, tagList, acceptWithoutRelator, relatorConfig,
+            acceptUnknownRelators, indexRawRelators, false
+        );
+    }
+
+    /**
+     * Filter values retrieved using tagList to include only those whose relator
+     * values are acceptable. Used for saving relators of authors separated by different
+     * types.
+     *
+     * @param record               The record (fed in automatically)
+     * @param tagList              The field specification to read
+     * @param acceptWithoutRelator Colon-delimited list of tags whose values should
+     * be accepted even if no relator subfield is defined
+     * @param relatorConfig        The setting in author-classification.ini which
+     * defines which relator terms are acceptable (or a colon-delimited list)
+     * @param acceptUnknownRelators Colon-delimited list of tags whose relators
+     * should be indexed even if they are not listed in author-classification.ini.
+     * @return List result
+     */
+    public List getRelatorsFilteredByRelator(Record record, String tagList,
+        String acceptWithoutRelator, String relatorConfig,
+        String acceptUnknownRelators
+    ) {
+        // default firstOnly to false!
+        return getRelatorsFilteredByRelator(
+            record, tagList, acceptWithoutRelator, relatorConfig,
+            acceptUnknownRelators, "false", false
+        );
+    }
+
+    /**
+     * Filter values retrieved using tagList to include only those whose relator
+     * values are acceptable. Used for saving relators of authors separated by different
+     * types.
+     *
+     * @param record               The record (fed in automatically)
+     * @param tagList              The field specification to read
+     * @param acceptWithoutRelator Colon-delimited list of tags whose values should
+     * be accepted even if no relator subfield is defined
+     * @param relatorConfig        The setting in author-classification.ini which
+     * defines which relator terms are acceptable (or a colon-delimited list)
+     * @return List result
+     */
+    public List getRelatorsFilteredByRelator(Record record, String tagList,
+        String acceptWithoutRelator, String relatorConfig
+    ) {
+        // default firstOnly to false!
+        return getRelatorsFilteredByRelator(
+            record, tagList, acceptWithoutRelator, relatorConfig,
+            acceptWithoutRelator, "false", false
+        );
+    }
+
+    /**
+     * This method fetches relator definitions from ini file and casts them to an
+     * array. If a colon-delimited string is passed in, this will be directly parsed
+     * instead of resorting to .ini loading.
+     *
+     * @param setting Setting to load from .ini or colon-delimited list.
+     * @return String[]
+     */
+    protected String[] loadRelatorConfig(String setting){
+        StringBuilder relators = new StringBuilder();
+
+        // check for pipe-delimited string
+        String[] relatorSettings = setting.split("\\|");
+        for (String relatorSetting: relatorSettings) {
+            // check for colon-delimited string
+            String[] relatorArray = relatorSetting.split(":");
+            if (relatorArray.length > 1) {
+                for (int i = 0; i < relatorArray.length; i++) {
+                    relators.append(relatorArray[i]).append(",");
+                }
+            } else {
+                relators.append(getConfigSetting(
+                    "author-classification.ini", "AuthorRoles", relatorSetting
+                )).append(",");
+            }
+        }
+
+        return relators.toString().split(",");
+    }
+
+    /**
+     * Normalizes a relator string and returns a list containing the normalized
+     * relator plus any configured synonyms.
+     *
+     * @param relator Relator term to normalize
+     * @return List of strings
+     */
+    public List<String> normalizeRelatorAndAddSynonyms(String relator)
+    {
+        List<String> newList = new ArrayList<String>();
+        String normalized = normalizeRelatorString(relator);
+        newList.add(normalized);
+        String synonyms = getConfigSetting(
+            "author-classification.ini", "RelatorSynonyms", relator
+        );
+        if (null != synonyms && synonyms.length() > 0) {
+            for (String synonym: synonyms.split("\\|")) {
+                String normalizedSynonym = normalizeRelatorString(synonym);
+                relatorSynonymLookup.put(normalizedSynonym, relator);
+                newList.add(normalizedSynonym);
+            }
+        }
+        return newList;
+    }
+
+    /**
+     * Normalizes the strings in a list.
+     *
+     * @param stringList List of strings to be normalized
+     * @return Normalized List of strings 
+     */
+    protected List<String> normalizeRelatorStringList(List<String> stringList)
+    {
+        List<String> newList = new ArrayList<String>();
+        for (String relator: stringList) {
+            newList.addAll(normalizeRelatorAndAddSynonyms(relator));
+        }
+        return newList;
+    }
+
+    /**
+     * Normalizes a string
+     *
+     * @param string String to be normalized
+     * @return string
+     */
+    protected String normalizeRelatorString(String string)
+    {
+        return string
+            .trim()
+            .toLowerCase()
+            .replaceAll("\\p{Punct}+", "");    //POSIX character class Punctuation: One of !"#$%&'()*+,-./:;<=>?@[\]^_`{|}~
+    }
+
+    /**
+     * Filter values retrieved using tagList to include only those whose relator
+     * values are acceptable. Used for separating different types of authors.
+     *
+     * @param record               The record (fed in automatically)
+     * @param tagList              The field specification to read
+     * @param acceptWithoutRelator Colon-delimited list of tags whose values should
+     * be accepted even if no relator subfield is defined
+     * @param relatorConfig        The setting in author-classification.ini which
+     * defines which relator terms are acceptable (or a colon-delimited list)
+     * @param acceptUnknownRelators Colon-delimited list of tags whose relators
+     * should be indexed even if they are not listed in author-classification.ini.
+     * @param indexRawRelators      Set to "true" to index relators raw, as found
+     * in the MARC or "false" to index mapped versions.
+     * @return List result
+     */
+    public List<String> getAuthorInitialsFilteredByRelator(Record record,
+        String tagList, String acceptWithoutRelator, String relatorConfig,
+        String acceptUnknownRelators, String indexRawRelators
+    ) {
+        List<String> authors = getAuthorsFilteredByRelator(
+            record, tagList, acceptWithoutRelator, relatorConfig,
+            acceptUnknownRelators, indexRawRelators
+        );
+        List<String> result = new LinkedList<String>();
+        for (String author : authors) {
+            result.add(processInitials(author));
+        }
+        return result;
+    }
+
+    /**
+     * Filter values retrieved using tagList to include only those whose relator
+     * values are acceptable. Used for separating different types of authors.
+     *
+     * @param record               The record (fed in automatically)
+     * @param tagList              The field specification to read
+     * @param acceptWithoutRelator Colon-delimited list of tags whose values should
+     * be accepted even if no relator subfield is defined
+     * @param relatorConfig        The setting in author-classification.ini which
+     * defines which relator terms are acceptable (or a colon-delimited list)
+     * @return List result
+     */
+    public List<String> getAuthorInitialsFilteredByRelator(Record record,
+        String tagList, String acceptWithoutRelator, String relatorConfig
+    ) {
+        return getAuthorInitialsFilteredByRelator(
+            record, tagList, acceptWithoutRelator, relatorConfig,
+            acceptWithoutRelator, "false"
+        );
+    }
+
+    /**
+     * Filter values retrieved using tagList to include only those whose relator
+     * values are acceptable. Used for separating different types of authors.
+     *
+     * @param record               The record (fed in automatically)
+     * @param tagList              The field specification to read
+     * @param acceptWithoutRelator Colon-delimited list of tags whose values should
+     * be accepted even if no relator subfield is defined
+     * @param relatorConfig        The setting in author-classification.ini which
+     * defines which relator terms are acceptable (or a colon-delimited list)
+     * @param acceptUnknownRelators Colon-delimited list of tags whose relators
+     * should be indexed even if they are not listed in author-classification.ini.
+     * @return List result
+     */
+    public List<String> getAuthorInitialsFilteredByRelator(Record record,
+        String tagList, String acceptWithoutRelator, String relatorConfig,
+        String acceptUnknownRelators
+    ) {
+        return getAuthorInitialsFilteredByRelator(
+            record, tagList, acceptWithoutRelator, relatorConfig,
+            acceptUnknownRelators, "false"
+        );
+    }
+    
+    /**
+     * Takes a name and cuts it into initials
+     * @param authorName e.g. Yeats, William Butler
+     * @return initials e.g. w b y wb
+     */
+    protected String processInitials(String authorName) {
+        Boolean isPersonalName = false;
+        // we guess that if there is a comma before the end - this is a personal name
+        if ((authorName.indexOf(',') > 0) 
+            && (authorName.indexOf(',') < authorName.length()-1)) {
+            isPersonalName = true;
+        }
+        // get rid of non-alphabet chars but keep hyphens and accents 
+        authorName = authorName.replaceAll("[^\\p{L} -]", "").toLowerCase();
+        String[] names = authorName.split(" "); //split into tokens on spaces
+        // if this is a personal name we'll reorganise to put lastname at the end
+        String result = "";
+        if (isPersonalName) {
+            String lastName = names[0]; 
+            for (int i = 0; i < names.length-1; i++) {
+                names[i] = names[i+1];
+            }
+            names[names.length-1] = lastName;
+        }
+        // put all the initials together in a space separated string
+        for (String name : names) {
+            if (name.length() > 0) {
+                String initial = name.substring(0,1);
+                // if there is a hyphenated name, use both initials
+                int pos = name.indexOf('-');
+                if (pos > 0 && pos < name.length() - 1) {
+                    String extra = name.substring(pos+1, pos+2);
+                    initial = initial + " " + extra;
+                }
+                result += " " + initial; 
+            }
+        }
+        // grab all initials and stick them together
+        String smushAll = result.replaceAll(" ", "");
+        // if it's a long personal name, get all but the last initials as well
+        // e.g. wb for william butler yeats
+        if (names.length > 2 && isPersonalName) {
+            String smushPers = result.substring(0,result.length()-1).replaceAll(" ","");
+            result = result + " " + smushPers;
+        }
+        // now we have initials separate and together
+        if (!result.trim().equals(smushAll)) {
+            result += " " + smushAll; 
+        }
+        result = result.trim();
+        return result;
+    }
+
+    /**
+     * Normalize trailing punctuation. This mimics the functionality built into VuFind's
+     * textFacet field type, so that you can get equivalent values when indexing into
+     * a string field. (Useful for docValues support).
+     *
+     * Can return null
+     *
+     * @param record current MARC record
+     * @param fieldSpec which MARC fields / subfields need to be analyzed
+     * @return Set containing normalized values
+     */
+    public Set<String> normalizeTrailingPunctuation(Record record, String fieldSpec) {
+        // Initialize our return value:
+        Set<String> result = new LinkedHashSet<String>();
+
+        // Loop through the specified MARC fields:
+        Set<String> input = getFieldList(record, fieldSpec);
+        Pattern pattern = Pattern.compile("(?<!\b[A-Z])[.\\s]*$");
+        for (String current: input) {
+            result.add(pattern.matcher(current).replaceAll(""));
+        }
+
+        // If we found no matches, return null; otherwise, return our results:
+        return result.isEmpty() ? null : result;
+    }
+}
\ No newline at end of file
diff --git a/import/index_scripts/author.bsh b/import/index_scripts/author.bsh
index bfe5f7cd12c4b985ea20b65ee67ed78dbe5f4f65..4bbbc395959b8098fada43068f0f57f05ae01739 100644
--- a/import/index_scripts/author.bsh
+++ b/import/index_scripts/author.bsh
@@ -3,47 +3,119 @@ import org.marc4j.marc.DataField;
 import org.solrmarc.index.UpdateDateTracker;
 import org.ini4j.Ini;
 import java.util.Arrays;
+import java.util.ArrayList;
+import java.util.Map;
 
 // define the base level indexer so that its methods can be called from the script.
 // note that the SolrIndexer code will set this value before the script methods are called.
 org.solrmarc.index.SolrIndexer indexer = null;
 
+HashMap relatorSynonymLookup = new HashMap();
+Set knownRelators = new LinkedHashSet();
+
+/**
+ * Extract all valid relator terms from a list of subfields using a whitelist.
+ * @param subfields        List of subfields to check
+ * @param permittedRoles   Whitelist to check against
+ * @param indexRawRelators Should we index relators raw, as found
+ * in the MARC (true) or index mapped versions (false)?
+ * @return Set of valid relator terms
+ */
+public Set getValidRelatorsFromSubfields(List subfields, List permittedRoles, Boolean indexRawRelators)
+{
+    Set relators = new LinkedHashSet();
+    for (int j = 0; j < subfields.size(); j++) {
+        String raw = subfields.get(j).getData();
+        String current = normalizeRelatorString(raw);
+        if (permittedRoles.contains(current)) {
+            relators.add(indexRawRelators ? raw : mapRelatorStringToCode(current));
+        }
+    }
+    return relators;
+}
+
+/**
+ * Is this relator term unknown to author-classification.ini?
+ * @param current relator to check
+ * @return True if unknown
+ */
+public Boolean isUnknownRelator(String current)
+{
+    // If we haven't loaded known relators yet, do so now:
+    if (knownRelators.size() == 0) {
+        Map all = indexer.getConfigSection("author-classification.ini", "RelatorSynonyms");
+        for (String key : all.keySet()) {
+            knownRelators.add(normalizeRelatorString(key));
+            for (String synonym: all.get(key).split("\\|")) {
+                knownRelators.add(normalizeRelatorString(synonym));
+            }
+        }
+    }
+    return !knownRelators.contains(normalizeRelatorString(current));
+}
+
+/**
+ * Extract all valid relator terms from a list of subfields using a whitelist.
+ * @param subfields      List of subfields to check
+ * @return Set of valid relator terms
+ */
+public Set getUnknownRelatorsFromSubfields(List subfields)
+{
+    Set relators = new LinkedHashSet();
+    for (int j = 0; j < subfields.size(); j++) {
+        String current = subfields.get(j).getData().trim();
+        if (current.length() > 0 && isUnknownRelator(current)) {
+            indexer.getLogger().info("Unknown relator: " + current);
+            relators.add(current);
+        }
+    }
+    return relators;
+}
+
 /**
- * Check if a particular Datafield meets the specified relator requirements.
- * @param authorField      Field to analyze
- * @param noRelatorAllowed Array of tag names which are allowed to be used with
+ * Extract all values that meet the specified relator requirements.
+ * @param authorField           Field to analyze
+ * @param noRelatorAllowed      Array of tag names which are allowed to be used with
  * no declared relator.
- * @param relatorConfig    The setting in author-classification.ini which
+ * @param relatorConfig         The setting in author-classification.ini which
  * defines which relator terms are acceptable (or a colon-delimited list)
- * @return Boolean
+ * @param unknownRelatorAllowed Array of tag names whose relators should be indexed
+ * even if they are not listed in author-classification.ini.
+ * @param indexRawRelators      Set to "true" to index relators raw, as found
+ * in the MARC or "false" to index mapped versions.
+ * @return Set
  */
-public Boolean authorHasAppropriateRelator(DataField authorField,
-    String[] noRelatorAllowed, String relatorConfig
+public Set getValidRelators(DataField authorField,
+    String[] noRelatorAllowed, String relatorConfig,
+    String[] unknownRelatorAllowed, String indexRawRelators
 ) {
     // get tag number from Field
     String tag = authorField.getTag();
-    List subfieldE = normalizeRelatorSubfieldList(authorField.getSubfields('e'));
-    List subfield4 = normalizeRelatorSubfieldList(authorField.getSubfields('4'));
+    List subfieldE = authorField.getSubfields('e');
+    List subfield4 = authorField.getSubfields('4');
+
+    Set relators = new LinkedHashSet();
 
     // if no relator is found, check to see if the current tag is in the "no
     // relator allowed" list.
     if (subfieldE.size() == 0 && subfield4.size() == 0) {
-        return Arrays.asList(noRelatorAllowed).contains(tag);
-    }
-
-    // If we got this far, we need to figure out what type of relation they have
-    List permittedRoles = normalizeRelatorStringList(Arrays.asList(loadRelatorConfig(relatorConfig)));
-    for (int j = 0; j < subfield4.size(); j++) {
-        if (permittedRoles.contains(subfield4.get(j).getData())) {
-            return true;
+        if (Arrays.asList(noRelatorAllowed).contains(tag)) {
+            relators.add("");
         }
-    }
-    for (int j = 0; j < subfieldE.size(); j++) {
-        if (permittedRoles.contains(subfieldE.get(j).getData())) {
-            return true;
+    } else {
+        // If we got this far, we need to figure out what type of relation they have
+        List permittedRoles = normalizeRelatorStringList(Arrays.asList(loadRelatorConfig(relatorConfig)));
+        relators.addAll(getValidRelatorsFromSubfields(subfieldE, permittedRoles, indexRawRelators.toLowerCase().equals("true")));
+        relators.addAll(getValidRelatorsFromSubfields(subfield4, permittedRoles, indexRawRelators.toLowerCase().equals("true")));
+        if (Arrays.asList(unknownRelatorAllowed).contains(tag)) {
+            Set unknown = getUnknownRelatorsFromSubfields(subfieldE);
+            if (unknown.size() == 0) {
+                unknown = getUnknownRelatorsFromSubfields(subfield4);
+            }
+            relators.addAll(unknown);
         }
     }
-    return false;
+    return relators;
 }
 
 /**
@@ -84,14 +156,20 @@ public HashMap getParsedTagList(String tagList)
  * be accepted even if no relator subfield is defined
  * @param relatorConfig        The setting in author-classification.ini which
  * defines which relator terms are acceptable (or a colon-delimited list)
+ * @param acceptUnknownRelators Colon-delimited list of tags whose relators
+ * should be indexed even if they are not listed in author-classification.ini.
+ * @param indexRawRelators      Set to "true" to index relators raw, as found
+ * in the MARC or "false" to index mapped versions.
  * @param firstOnly            Return first result only?
  * @return List result
  */
 public List getAuthorsFilteredByRelator(Record record, String tagList,
-    String acceptWithoutRelator, String relatorConfig, Boolean firstOnly
+    String acceptWithoutRelator, String relatorConfig,
+    String acceptUnknownRelators, String indexRawRelators, Boolean firstOnly
 ) {
     List result = new LinkedList();
     String[] noRelatorAllowed = acceptWithoutRelator.split(":");
+    String[] unknownRelatorAllowed = acceptUnknownRelators.split(":");
     HashMap parsedTagList = getParsedTagList(tagList);
     List fields = indexer.getFieldSetMatchingTagList(record, tagList);
     Iterator fieldsIter = fields.iterator();
@@ -99,8 +177,8 @@ public List getAuthorsFilteredByRelator(Record record, String tagList,
         DataField authorField;
         while (fieldsIter.hasNext()){
             authorField = (DataField) fieldsIter.next();
-            //add all author types to the result set
-            if (authorHasAppropriateRelator(authorField, noRelatorAllowed, relatorConfig)) {
+            // add all author types to the result set; if we have multiple relators, repeat the authors
+            for (String iterator: getValidRelators(authorField, noRelatorAllowed, relatorConfig, unknownRelatorAllowed, indexRawRelators)) {
                 for (String subfields : parsedTagList.get(authorField.getTag())) {
                     String current = indexer.getDataFromVariableField(authorField, "["+subfields+"]", " ", false);
                     // TODO: we may eventually be able to use this line instead,
@@ -138,7 +216,8 @@ public List getAuthorsFilteredByRelator(Record record, String tagList,
 ) {
     // default firstOnly to false!
     return getAuthorsFilteredByRelator(
-        record, tagList, acceptWithoutRelator, relatorConfig, false
+        record, tagList, acceptWithoutRelator, relatorConfig,
+        acceptWithoutRelator, "false", false
     );
 }
 
@@ -152,13 +231,85 @@ public List getAuthorsFilteredByRelator(Record record, String tagList,
  * be accepted even if no relator subfield is defined
  * @param relatorConfig        The setting in author-classification.ini which
  * defines which relator terms are acceptable (or a colon-delimited list)
+ * @return List result
+ * @param acceptUnknownRelators Colon-delimited list of tags whose relators
+ * should be indexed even if they are not listed in author-classification.ini.
+ */
+public List getAuthorsFilteredByRelator(Record record, String tagList,
+    String acceptWithoutRelator, String relatorConfig,
+    String acceptUnknownRelators
+) {
+    // default firstOnly to false!
+    return getAuthorsFilteredByRelator(
+        record, tagList, acceptWithoutRelator, relatorConfig,
+        acceptUnknownRelators, "false", false
+    );
+}
+
+/**
+ * Filter values retrieved using tagList to include only those whose relator
+ * values are acceptable. Used for separating different types of authors.
+ *
+ * @param record               The record (fed in automatically)
+ * @param tagList              The field specification to read
+ * @param acceptWithoutRelator Colon-delimited list of tags whose values should
+ * be accepted even if no relator subfield is defined
+ * @param relatorConfig        The setting in author-classification.ini which
+ * defines which relator terms are acceptable (or a colon-delimited list)
+ * @return List result
+ * @param acceptUnknownRelators Colon-delimited list of tags whose relators
+ * should be indexed even if they are not listed in author-classification.ini.
+ * @param indexRawRelators      Set to "true" to index relators raw, as found
+ * in the MARC or "false" to index mapped versions.
+ */
+public List getAuthorsFilteredByRelator(Record record, String tagList,
+    String acceptWithoutRelator, String relatorConfig,
+    String acceptUnknownRelators, String indexRawRelators
+) {
+    // default firstOnly to false!
+    return getAuthorsFilteredByRelator(
+        record, tagList, acceptWithoutRelator, relatorConfig,
+        acceptUnknownRelators, indexRawRelators, false
+    );
+}
+
+/**
+ * If the provided relator is included in the synonym list, convert it back to
+ * a code (for better standardization/translation).
+ *
+ * @param relator Relator code to check
+ * @return Code version, if found, or raw string if no match found.
+ */
+public String mapRelatorStringToCode(String relator)
+{
+    String normalizedRelator = normalizeRelatorString(relator);
+    return relatorSynonymLookup.containsKey(normalizedRelator)
+        ? relatorSynonymLookup.get(normalizedRelator) : relator;
+}
+
+/**
+ * Filter values retrieved using tagList to include only those whose relator
+ * values are acceptable. Used for separating different types of authors.
+ *
+ * @param record                The record (fed in automatically)
+ * @param tagList               The field specification to read
+ * @param acceptWithoutRelator  Colon-delimited list of tags whose values should
+ * be accepted even if no relator subfield is defined
+ * @param relatorConfig         The setting in author-classification.ini which
+ * defines which relator terms  are acceptable (or a colon-delimited list)
+ * @param acceptUnknownRelators Colon-delimited list of tags whose relators
+ * should be indexed even if they are not listed in author-classification.ini.
+ * @param indexRawRelators      Set to "true" to index relators raw, as found
+ * in the MARC or "false" to index mapped versions.
  * @return String
  */
 public String getFirstAuthorFilteredByRelator(Record record, String tagList,
-    String acceptWithoutRelator, String relatorConfig
+    String acceptWithoutRelator, String relatorConfig,
+    String acceptUnknownRelators, String indexRawRelators
 ) {
     List result = getAuthorsFilteredByRelator(
-        record, tagList, acceptWithoutRelator, relatorConfig, true
+        record, tagList, acceptWithoutRelator, relatorConfig,
+        acceptUnknownRelators, indexRawRelators, true
     );
     for (String s : result) {
         return s;
@@ -166,6 +317,51 @@ public String getFirstAuthorFilteredByRelator(Record record, String tagList,
     return null;
 }
 
+/**
+ * Filter values retrieved using tagList to include only those whose relator
+ * values are acceptable. Used for separating different types of authors.
+ *
+ * @param record                The record (fed in automatically)
+ * @param tagList               The field specification to read
+ * @param acceptWithoutRelator  Colon-delimited list of tags whose values should
+ * be accepted even if no relator subfield is defined
+ * @param relatorConfig         The setting in author-classification.ini which
+ * defines which relator terms  are acceptable (or a colon-delimited list)
+ * @return String
+ */
+public String getFirstAuthorFilteredByRelator(Record record, String tagList,
+    String acceptWithoutRelator, String relatorConfig
+) {
+    return getFirstAuthorFilteredByRelator(
+        record, tagList, acceptWithoutRelator, relatorConfig,
+        acceptWithoutRelator, "false"
+    );
+}
+
+/**
+ * Filter values retrieved using tagList to include only those whose relator
+ * values are acceptable. Used for separating different types of authors.
+ *
+ * @param record                The record (fed in automatically)
+ * @param tagList               The field specification to read
+ * @param acceptWithoutRelator  Colon-delimited list of tags whose values should
+ * be accepted even if no relator subfield is defined
+ * @param relatorConfig         The setting in author-classification.ini which
+ * defines which relator terms  are acceptable (or a colon-delimited list)
+ * @param acceptUnknownRelators Colon-delimited list of tags whose relators
+ * should be indexed even if they are not listed in author-classification.ini.
+ * @return String
+ */
+public String getFirstAuthorFilteredByRelator(Record record, String tagList,
+    String acceptWithoutRelator, String relatorConfig,
+    String acceptUnknownRelators
+) {
+    return getFirstAuthorFilteredByRelator(
+        record, tagList, acceptWithoutRelator, relatorConfig,
+        acceptUnknownRelators, "false"
+    );
+}
+
 /**
  * Filter values retrieved using tagList to include only those whose relator
  * values are acceptable. Used for saving relators of authors separated by different
@@ -177,15 +373,20 @@ public String getFirstAuthorFilteredByRelator(Record record, String tagList,
  * be accepted even if no relator subfield is defined
  * @param relatorConfig        The setting in author-classification.ini which
  * defines which relator terms are acceptable (or a colon-delimited list)
+ * @param acceptUnknownRelators Colon-delimited list of tags whose relators
+ * should be indexed even if they are not listed in author-classification.ini.
+ * @param indexRawRelators      Set to "true" to index relators raw, as found
+ * in the MARC or "false" to index mapped versions.
  * @param firstOnly            Return first result only?
  * @return List result
  */
 public List getRelatorsFilteredByRelator(Record record, String tagList,
-    String acceptWithoutRelator, String relatorConfig, Boolean firstOnly,
-    String defaultRelator
+    String acceptWithoutRelator, String relatorConfig,
+    String acceptUnknownRelators, String indexRawRelators, Boolean firstOnly
 ) {
     List result = new LinkedList();
     String[] noRelatorAllowed = acceptWithoutRelator.split(":");
+    String[] unknownRelatorAllowed = acceptUnknownRelators.split(":");
     HashMap parsedTagList = getParsedTagList(tagList);
     List fields = indexer.getFieldSetMatchingTagList(record, tagList);
     Iterator fieldsIter = fields.iterator();
@@ -194,35 +395,66 @@ public List getRelatorsFilteredByRelator(Record record, String tagList,
         while (fieldsIter.hasNext()){
             authorField = (DataField) fieldsIter.next();
             //add all author types to the result set
-            if (authorHasAppropriateRelator(authorField, noRelatorAllowed, relatorConfig)) {
-                List subfieldE = normalizeRelatorSubfieldList(authorField.getSubfields('e'));
-                List subfield4 = normalizeRelatorSubfieldList(authorField.getSubfields('4'));
-    
-                // get the first non-empty subfield
-                String relator = defaultRelator;
-
-                // try subfield E first
-                for (int j = 0; j < subfieldE.size(); j++) {
-                    if (!subfieldE.get(j).getData().isEmpty()) {
-                        relator = subfieldE.get(j).getData();
-                        continue;
-                    }
-                }
-                // try subfield 4 now and overwrite relator as subfield 4 is most important
-                for (int j = 0; j < subfield4.size(); j++) {
-                    if (!subfield4.get(j).getData().isEmpty()) {
-                        relator = subfield4.get(j).getData();
-                        continue;
-                    }
-                }
-
-                result.add(relator);
-            }
+            result.addAll(getValidRelators(authorField, noRelatorAllowed, relatorConfig, unknownRelatorAllowed, indexRawRelators));
         }
     }
     return result;
 }
 
+/**
+ * Filter values retrieved using tagList to include only those whose relator
+ * values are acceptable. Used for saving relators of authors separated by different
+ * types.
+ *
+ * @param record               The record (fed in automatically)
+ * @param tagList              The field specification to read
+ * @param acceptWithoutRelator Colon-delimited list of tags whose values should
+ * be accepted even if no relator subfield is defined
+ * @param relatorConfig        The setting in author-classification.ini which
+ * defines which relator terms are acceptable (or a colon-delimited list)
+ * @param acceptUnknownRelators Colon-delimited list of tags whose relators
+ * should be indexed even if they are not listed in author-classification.ini.
+ * @param indexRawRelators      Set to "true" to index relators raw, as found
+ * in the MARC or "false" to index mapped versions.
+ * @return List result
+ */
+public List getRelatorsFilteredByRelator(Record record, String tagList,
+    String acceptWithoutRelator, String relatorConfig,
+    String acceptUnknownRelators, String indexRawRelators
+) {
+    // default firstOnly to false!
+    return getRelatorsFilteredByRelator(
+        record, tagList, acceptWithoutRelator, relatorConfig,
+        acceptUnknownRelators, indexRawRelators, false
+    );
+}
+
+/**
+ * Filter values retrieved using tagList to include only those whose relator
+ * values are acceptable. Used for saving relators of authors separated by different
+ * types.
+ *
+ * @param record               The record (fed in automatically)
+ * @param tagList              The field specification to read
+ * @param acceptWithoutRelator Colon-delimited list of tags whose values should
+ * be accepted even if no relator subfield is defined
+ * @param relatorConfig        The setting in author-classification.ini which
+ * defines which relator terms are acceptable (or a colon-delimited list)
+ * @param acceptUnknownRelators Colon-delimited list of tags whose relators
+ * should be indexed even if they are not listed in author-classification.ini.
+ * @return List result
+ */
+public List getRelatorsFilteredByRelator(Record record, String tagList,
+    String acceptWithoutRelator, String relatorConfig,
+    String acceptUnknownRelators
+) {
+    // default firstOnly to false!
+    return getRelatorsFilteredByRelator(
+        record, tagList, acceptWithoutRelator, relatorConfig,
+        acceptUnknownRelators, "false", false
+    );
+}
+
 /**
  * Filter values retrieved using tagList to include only those whose relator
  * values are acceptable. Used for saving relators of authors separated by different
@@ -241,7 +473,8 @@ public List getRelatorsFilteredByRelator(Record record, String tagList,
 ) {
     // default firstOnly to false!
     return getRelatorsFilteredByRelator(
-        record, tagList, acceptWithoutRelator, relatorConfig, false, ""
+        record, tagList, acceptWithoutRelator, relatorConfig,
+        acceptWithoutRelator, "false", false
     );
 }
 
@@ -253,7 +486,7 @@ public List getRelatorsFilteredByRelator(Record record, String tagList,
  * @param setting Setting to load from .ini or colon-delimited list.
  * @return String[]
  */
-public String[] loadRelatorConfig(String setting) {
+public String[] loadRelatorConfig(String setting){
     StringBuilder relators = new StringBuilder();
 
     // check for pipe-delimited string
@@ -276,36 +509,43 @@ public String[] loadRelatorConfig(String setting) {
 }
 
 /**
- * Normalizes the strings in a list.
+ * Normalizes a relator string and returns a list containing the normalized
+ * relator plus any configured synonyms.
  *
- * @param stringList List of strings to be normalized
- * @return stringList Normalized List of strings 
+ * @param relator Relator term to normalize
+ * @return List of strings
  */
-public List normalizeRelatorStringList(List stringList)
+public List normalizeRelatorAndAddSynonyms(String relator)
 {
-    for (int j = 0; j < stringList.size(); j++) {
-        stringList.set(
-            j,
-            normalizeRelatorString(stringList.get(j))
-        );
+    List newList = new ArrayList();
+    String normalized = normalizeRelatorString(relator);
+    newList.add(normalized);
+    String synonyms = indexer.getConfigSetting(
+        "author-classification.ini", "RelatorSynonyms", relator
+    );
+    if (null != synonyms && synonyms.length() > 0) {
+        for (String synonym: synonyms.split("\\|")) {
+            String normalizedSynonym = normalizeRelatorString(synonym);
+            relatorSynonymLookup.put(normalizedSynonym, relator);
+            newList.add(normalizedSynonym);
+        }
     }
-    return stringList;
+    return newList;
 }
 
 /**
- * Normalizes the strings in a list of subfields.
+ * Normalizes the strings in a list.
  *
- * @param subfieldList List of subfields to be normalized
- * @return subfieldList Normalized List of subfields
+ * @param stringList List of strings to be normalized
+ * @return Normalized List of strings 
  */
-public List normalizeRelatorSubfieldList(List subfieldList)
+public List normalizeRelatorStringList(List stringList)
 {
-    for (int j = 0; j < subfieldList.size(); j++) {
-        subfieldList.get(j).setData(
-            normalizeRelatorString(subfieldList.get(j).getData())
-        );
+    List newList = new ArrayList();
+    for (String relator: stringList) {
+        newList.addAll(normalizeRelatorAndAddSynonyms(relator));
     }
-    return subfieldList;
+    return newList;
 }
 
 /**
@@ -332,20 +572,72 @@ public String normalizeRelatorString(String string)
  * be accepted even if no relator subfield is defined
  * @param relatorConfig        The setting in author-classification.ini which
  * defines which relator terms are acceptable (or a colon-delimited list)
- * @param firstOnly            Return first result only?
+ * @param acceptUnknownRelators Colon-delimited list of tags whose relators
+ * should be indexed even if they are not listed in author-classification.ini.
+ * @param indexRawRelators      Set to "true" to index relators raw, as found
+ * in the MARC or "false" to index mapped versions.
  * @return List result
  */
-public List getAuthorInitialsFilteredByRelator(Record record, String tagList,
-    String acceptWithoutRelator, String relatorConfig
+public List getAuthorInitialsFilteredByRelator(Record record,
+    String tagList, String acceptWithoutRelator, String relatorConfig,
+    String acceptUnknownRelators, String indexRawRelators
 ) {
-    List authors = getAuthorsFilteredByRelator(record, tagList, acceptWithoutRelator, relatorConfig);
+    List authors = getAuthorsFilteredByRelator(
+        record, tagList, acceptWithoutRelator, relatorConfig,
+        acceptUnknownRelators, indexRawRelators
+    );
     List result = new LinkedList();
     for (String author : authors) {
-        result.add(this.processInitials(author));
+        result.add(processInitials(author));
     }
     return result;
 }
 
+/**
+ * Filter values retrieved using tagList to include only those whose relator
+ * values are acceptable. Used for separating different types of authors.
+ *
+ * @param record               The record (fed in automatically)
+ * @param tagList              The field specification to read
+ * @param acceptWithoutRelator Colon-delimited list of tags whose values should
+ * be accepted even if no relator subfield is defined
+ * @param relatorConfig        The setting in author-classification.ini which
+ * defines which relator terms are acceptable (or a colon-delimited list)
+ * @return List result
+ */
+public List getAuthorInitialsFilteredByRelator(Record record,
+    String tagList, String acceptWithoutRelator, String relatorConfig
+) {
+    return getAuthorInitialsFilteredByRelator(
+        record, tagList, acceptWithoutRelator, relatorConfig,
+        acceptWithoutRelator, "false"
+    );
+}
+
+/**
+ * Filter values retrieved using tagList to include only those whose relator
+ * values are acceptable. Used for separating different types of authors.
+ *
+ * @param record               The record (fed in automatically)
+ * @param tagList              The field specification to read
+ * @param acceptWithoutRelator Colon-delimited list of tags whose values should
+ * be accepted even if no relator subfield is defined
+ * @param relatorConfig        The setting in author-classification.ini which
+ * defines which relator terms are acceptable (or a colon-delimited list)
+ * @param acceptUnknownRelators Colon-delimited list of tags whose relators
+ * should be indexed even if they are not listed in author-classification.ini.
+ * @return List result
+ */
+public List getAuthorInitialsFilteredByRelator(Record record,
+    String tagList, String acceptWithoutRelator, String relatorConfig,
+    String acceptUnknownRelators
+) {
+    return getAuthorInitialsFilteredByRelator(
+        record, tagList, acceptWithoutRelator, relatorConfig,
+        acceptUnknownRelators, "false"
+    );
+}
+
 /**
  * Takes a name and cuts it into initials
  * @param authorName e.g. Yeats, William Butler
@@ -397,4 +689,4 @@ public String processInitials(String authorName) {
     }
     result = result.trim();
     return result;
-}
\ No newline at end of file
+}
diff --git a/import/index_scripts/dewey.bsh b/import/index_scripts/dewey.bsh
index 4bef34e3ef9e2724eef7a7b1ed4f1cc11a7d82a8..50edb41552ab3c45c591f290400b40cc7c909247 100644
--- a/import/index_scripts/dewey.bsh
+++ b/import/index_scripts/dewey.bsh
@@ -10,6 +10,9 @@ import org.solrmarc.callnum.DeweyCallNumber;
 import org.solrmarc.index.SolrIndexer;
 import org.solrmarc.tools.CallNumUtils;
 
+// The beanshell script class will initialize this variable to pointer to the singleton SolrIndexer class.
+SolrIndexer indexer;
+
 /**
  * Extract a numeric portion of the Dewey decimal call number
  *
@@ -30,7 +33,7 @@ public Set getDeweyNumber(Record record, String fieldSpec, String precisionStr)
     float precision = Float.parseFloat(precisionStr);
 
     // Loop through the specified MARC fields:
-    Set input = SolrIndexer.getFieldList(record, fieldSpec);
+    Set input = indexer.getFieldList(record, fieldSpec);
     for (String current: input) {
         DeweyCallNumber callNum = new DeweyCallNumber(current);
         if (callNum.isValid()) {
@@ -66,7 +69,7 @@ public Set getDeweySearchable(Record record, String fieldSpec) {
     Set result = new LinkedHashSet();
 
     // Loop through the specified MARC fields:
-    Set input = SolrIndexer.getFieldList(record, fieldSpec);
+    Set input = indexer.getFieldList(record, fieldSpec);
     Iterator iter = input.iterator();
     while (iter.hasNext()) {
         // Get the current string to work on:
@@ -98,7 +101,7 @@ public Set getDeweySearchable(Record record, String fieldSpec) {
  */
 public String getDeweySortable(Record record, String fieldSpec) {
     // Loop through the specified MARC fields:
-    Set input = SolrIndexer.getFieldList(record, fieldSpec);
+    Set input = indexer.getFieldList(record, fieldSpec);
     Iterator iter = input.iterator();
     while (iter.hasNext()) {
         // Get the current string to work on:
@@ -180,7 +183,7 @@ public List getDeweySortables(Record record, String fieldSpec) {
     List result = new LinkedList();
 
     // Loop through the specified MARC fields:
-    Set input = SolrIndexer.getFieldList(record, fieldSpec);
+    Set input = indexer.getFieldList(record, fieldSpec);
     Iterator iter = input.iterator();
     while (iter.hasNext()) {
         // Get the current string to work on:
diff --git a/import/index_scripts/location.bsh b/import/index_scripts/location.bsh
index c382de853038938d15fef7ef760e3e14de071d65..9787855982e204f06e26ba59b8c63746b5b1e757 100644
--- a/import/index_scripts/location.bsh
+++ b/import/index_scripts/location.bsh
@@ -1,15 +1,232 @@
 /**
- * Custom latitude/longitude script.
+ * Custom script to get latitude and longitude coordinates.
+ * Records can have multiple coordinates sets
+ * of points and/or rectangles.
+ * Points are represented by coordinate sets where N=S E=W.
  *
- * This can be used to override built-in SolrMarc custom functions.  If you change
- * this script, you will need to activate it in import/marc_local.properties before
- * it will be applied during indexing.
+ * code adapted from xrosecky - Moravian Library
+ * https://github.com/moravianlibrary/VuFind-2.x/blob/master/import/index_scripts/geo.bsh
+ * and incorporates VuFind location.bsh functionality for GoogleMap display.
+ *
+ */
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import org.marc4j.marc.*;
+
+private static final Pattern COORDINATES_PATTERN = Pattern.compile("^([eEwWnNsS])(\\d{3})(\\d{2})(\\d{2})");
+private static final Pattern HDMSHDD_PATTERN = Pattern.compile("^([eEwWnNsS])(\\d+(\\.\\d+)?)");
+private static final Pattern PMDD_PATTERN = Pattern.compile("^([+-])(\\d+(\\.\\d+)?)");
+
+/**
+ * Convert MARC coordinates into location_geo format.
+ *
+ * @param  Record record
+ * @return List   geo_coordinates
+ */
+public List getAllCoordinates(Record record) {
+    List geo_coordinates = new ArrayList();
+    List list034 = record.getVariableFields("034");
+    if (list034 != null) {
+        for (VariableField vf : list034) {
+            DataField df = (DataField) vf;
+            String d = df.getSubfield('d').getData();
+            String e = df.getSubfield('e').getData();
+            String f = df.getSubfield('f').getData();
+            String g = df.getSubfield('g').getData();
+            //System.out.println("raw Coords: "+d+" "+e+" "+f+" "+g);
+
+            // Check to see if there are only 2 coordinates
+            // If so, copy them into the corresponding coordinate fields
+            if ((d !=null && (e == null || e.trim().equals(""))) && (f != null && (g==null || g.trim().equals("")))) {
+                e = d;
+                g = f;
+            }
+            if ((e !=null && (d == null || d.trim().equals(""))) && (g != null && (f==null || f.trim().equals("")))) {
+                d = e;
+                f = g;
+            }
+
+            // Check and convert coordinates to +/- decimal degrees
+            Double west = convertCoordinate(d);
+            Double east = convertCoordinate(e);
+            Double north = convertCoordinate(f);
+            Double south = convertCoordinate(g);
+
+            // New Format for indexing coordinates in Solr 5.0 - minX, maxX, maxY, minY
+            // Note - storage in Solr follows the WENS order, but display is WSEN order
+            String result = String.format("ENVELOPE(%s,%s,%s,%s)", new Object[] { west, east, north, south });
+
+            if (validateCoordinates(west, east, north, south)) {
+                    geo_coordinates.add(result);
+            }
+        }
+    }
+    return geo_coordinates;
+}
+
+/**
+ * Get point coordinates for GoogleMap display.
+ *
+ * @param  Record record
+ * @return List   coordinates
+ */
+public List getPointCoordinates(Record record) {
+    List coordinates = new ArrayList();
+    List list034 = record.getVariableFields("034");
+    if (list034 != null) {
+        for (VariableField vf : list034) {
+            DataField df = (DataField) vf;
+            String d = df.getSubfield('d').getData();
+            String e = df.getSubfield('e').getData();
+            String f = df.getSubfield('f').getData();
+            String g = df.getSubfield('g').getData();
+
+            // Check to see if there are only 2 coordinates
+            if ((d !=null && (e == null || e.trim().equals(""))) && (f != null && (g==null || g.trim().equals("")))) {
+                Double long_val = convertCoordinate(d);
+                Double lat_val = convertCoordinate(f);
+                String longlatCoordinate = Double.toString(long_val) + ',' + Double.toString(lat_val);
+                coordinates.add(longlatCoordinate);
+            }
+            if ((e !=null && (d == null || d.trim().equals(""))) && (g != null && (f==null || f.trim().equals("")))) {
+                Double long_val = convertCoordinate(e);
+                Double lat_val = convertCoordinate(g);
+                String longlatCoordinate = Double.toString(long_val) + ',' + Double.toString(lat_val);
+                coordinates.add(longlatCoordinate);
+            }
+            // Check if N=S and E=W
+            if (d.equals(e) && f.equals(g)) {
+                Double long_val = convertCoordinate(d);
+                Double lat_val = convertCoordinate(f);
+                String longlatCoordinate = Double.toString(long_val) + ',' + Double.toString(lat_val);
+                coordinates.add(longlatCoordinate);
+            }
+        }
+    }
+    return coordinates;
+}
+
+/**
+ * Get all available coordinates from the record.
+ *
+ * @param  Record record
+ * @return List   geo_coordinates
+ */
+public List getDisplayCoordinates(Record record) {
+    List geo_coordinates = new ArrayList();
+    List list034 = record.getVariableFields("034");
+    if (list034 != null) {
+        for (VariableField vf : list034) {
+            DataField df = (DataField) vf;
+            String west = df.getSubfield('d').getData();
+            String east = df.getSubfield('e').getData();
+            String north = df.getSubfield('f').getData();
+            String south = df.getSubfield('g').getData();
+            String result = String.format("%s %s %s %s", new Object[] { west, east, north, south });
+            if (west != null || east != null || north != null || south != null) {
+                geo_coordinates.add(result);
+            }
+        }
+    }
+    return geo_coordinates;
+}
+
+/**
+ * Check coordinate type HDMS HDD or +/-DD.
+ *
+ * @param  String coordinateStr
+ * @return Double coordinate
+ */
+public Double convertCoordinate(String coordinateStr) {
+    Double coordinate = Double.NaN;
+    Matcher HDmatcher = HDMSHDD_PATTERN.matcher(coordinateStr);
+    Matcher PMDmatcher = PMDD_PATTERN.matcher(coordinateStr);
+    if (HDmatcher.matches()) {
+        String hemisphere = HDmatcher.group(1).toUpperCase();
+        Double degrees = Double.parseDouble(HDmatcher.group(2));
+        // Check for HDD or HDMS
+        if (hemisphere.equals("N") || hemisphere.equals("S")) {
+            if (degrees > 90) {
+                String hdmsCoordinate = hemisphere+"0"+HDmatcher.group(2);
+                coordinate = coordinateToDecimal(hdmsCoordinate);
+            } else {
+                coordinate = Double.parseDouble(HDmatcher.group(2));
+                if (hemisphere.equals("S")) {
+                    coordinate *= -1;
+                }
+            }
+        }
+        if (hemisphere.equals("E") || hemisphere.equals("W")) {
+            if (degrees > 180) {
+                String hdmsCoordinate = HDmatcher.group(0);
+                coordinate = coordinateToDecimal(hdmsCoordinate);
+            } else {
+                coordinate = Double.parseDouble(HDmatcher.group(2));
+                if (hemisphere.equals("W")) {
+                    coordinate *= -1;
+                }
+            }
+        }
+        return coordinate;
+    } else if (PMDmatcher.matches()) {
+        String hemisphere = PMDmatcher.group(1);
+        coordinate = Double.parseDouble(PMDmatcher.group(2));
+        if (hemisphere.equals("-")) {
+            coordinate *= -1;
+        }
+        return coordinate;
+    } else {
+        return null;
+    }
+}
+
+/**
+ * Convert HDMS coordinates to decimal degrees.
+ *
+ * @param  String coordinateStr
+ * @return Double coordinate
  */
-import org.marc4j.marc.Record;
-import org.marc4j.marc.ControlField;
-import org.marc4j.marc.DataField;
+public Double coordinateToDecimal(String coordinateStr) {
+    Matcher matcher = COORDINATES_PATTERN.matcher(coordinateStr);
+    if (matcher.matches()) {
+        String hemisphere = matcher.group(1).toUpperCase();
+        int degrees = Integer.parseInt(matcher.group(2));
+        int minutes = Integer.parseInt(matcher.group(3));
+        int seconds = Integer.parseInt(matcher.group(4));
+        double coordinate = degrees + (minutes / 60.0) + (seconds / 3600.0);
+        if (hemisphere.equals("W") || hemisphere.equals("S")) {
+            coordinate *= -1;
+        }
+        return coordinate;
+    }
+    return null;
+}
 
 /**
+ * Check decimal degree coordinates to make sure they are valid.
+ *
+ * @param  Double west, east, north, south
+ * @return boolean
+ */
+public boolean validateCoordinates(Double west, Double east, Double north, Double south) {
+    if (west == null || east == null || north == null || south == null) {
+        return false;
+    }
+    if (west > 180.0 || west < -180.0 || east > 180.0 || east < -180.0) {
+        return false;
+    }
+    if (north > 90.0 || north < -90.0 || south > 90.0 || south < -90.0) {
+        return false;
+    }
+    if (north < south || west > east) {
+        return false;
+    }
+    return true;
+}
+
+/**
+ * THIS FUNCTION HAS BEEN DEPRECATED.
  * Determine the longitude and latitude of the items location.
  *
  * @param  Record    record
@@ -46,10 +263,9 @@ public String getLongLat(Record record) {
                     val = val + ',' + val2;
                 }
             }
-        return val;
+            return val;
         }
     }
     //otherwise return null
     return null;
-}
-
+}
\ No newline at end of file
diff --git a/import/lib/bsh-2.0b5.jar b/import/lib/bsh-2.0b5.jar
new file mode 100644
index 0000000000000000000000000000000000000000..e326510007373f0a5f96acf121e97fb58c4fc8ed
Binary files /dev/null and b/import/lib/bsh-2.0b5.jar differ
diff --git a/import/lib/fast-classpath-scanner-1.91.4.jar b/import/lib/fast-classpath-scanner-1.91.4.jar
new file mode 100644
index 0000000000000000000000000000000000000000..45684e86863d56f8b30e04247c6a63372b4e4a12
Binary files /dev/null and b/import/lib/fast-classpath-scanner-1.91.4.jar differ
diff --git a/import/lib/java-cup-11b-runtime.jar b/import/lib/java-cup-11b-runtime.jar
new file mode 100644
index 0000000000000000000000000000000000000000..b248460839d0ec0bfc4326e157860e563ec1dd08
Binary files /dev/null and b/import/lib/java-cup-11b-runtime.jar differ
diff --git a/import/lib/jopt-simple-5.0.2.jar b/import/lib/jopt-simple-5.0.2.jar
new file mode 100644
index 0000000000000000000000000000000000000000..2c7a0872d2f164d78ae877922ea15af92baf8290
Binary files /dev/null and b/import/lib/jopt-simple-5.0.2.jar differ
diff --git a/import/lib/log4j-1.2.17.jar b/import/lib/log4j-1.2.17.jar
new file mode 100644
index 0000000000000000000000000000000000000000..1d425cf7d7e25f81be64d32c406ff66cfb6c4766
Binary files /dev/null and b/import/lib/log4j-1.2.17.jar differ
diff --git a/import/lib/marc4j-2.8.0.jar b/import/lib/marc4j-2.8.0.jar
new file mode 100644
index 0000000000000000000000000000000000000000..ed784eedf5e45571fdc74073acaaa68d525c1ede
Binary files /dev/null and b/import/lib/marc4j-2.8.0.jar differ
diff --git a/import/lib/miglayout15-swing.jar b/import/lib/miglayout15-swing.jar
new file mode 100644
index 0000000000000000000000000000000000000000..2d6a62f1bfcaf5282cc3496371e2a9e1fafaac7c
Binary files /dev/null and b/import/lib/miglayout15-swing.jar differ
diff --git a/import/lib/slf4j-log4j12-1.7.16.jar b/import/lib/slf4j-log4j12-1.7.16.jar
new file mode 100644
index 0000000000000000000000000000000000000000..a6aef80e9eed61f717d2f7c2655bebd2b3290b68
Binary files /dev/null and b/import/lib/slf4j-log4j12-1.7.16.jar differ
diff --git a/import/lib_local/README.md b/import/lib_local/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..f5ff428ebd44c5d00040192aa5efd080adf973e5
--- /dev/null
+++ b/import/lib_local/README.md
@@ -0,0 +1,3 @@
+This folder contains dependencies specific to VuFind's custom indexing functions.
+
+Core SolrMarc dependencies are found in the regular lib directory.
diff --git a/import/lib/ini4j-0.5.2-SNAPSHOT.jar b/import/lib_local/ini4j-0.5.2-SNAPSHOT.jar
similarity index 100%
rename from import/lib/ini4j-0.5.2-SNAPSHOT.jar
rename to import/lib_local/ini4j-0.5.2-SNAPSHOT.jar
diff --git a/import/lib/mysql-connector-java-5.0.8-bin.jar b/import/lib_local/mysql-connector-java-5.0.8-bin.jar
similarity index 100%
rename from import/lib/mysql-connector-java-5.0.8-bin.jar
rename to import/lib_local/mysql-connector-java-5.0.8-bin.jar
diff --git a/import/lib/postgresql-9.1-902.jdbc4.jar b/import/lib_local/postgresql-9.1-902.jdbc4.jar
similarity index 100%
rename from import/lib/postgresql-9.1-902.jdbc4.jar
rename to import/lib_local/postgresql-9.1-902.jdbc4.jar
diff --git a/import/log4j.properties b/import/log4j.properties
index 2fd730aefb4db06c51f734864c85c57451644f5d..59a6adaa0a6a6dd61f33bd791bf8132720983604 100644
--- a/import/log4j.properties
+++ b/import/log4j.properties
@@ -2,13 +2,23 @@
 #
 # $Id: log4j.properties 396 2009-02-13 15:24:07Z rh9ec@virginia.edu $
 
-log4j.rootLogger=debug, stdout, file
+log4j.rootLogger=info, stdout, file
 
 # Application logging level
 # 	Valid options are TRACE,DEBUG,INFO,WARN,ERROR,FATAL
-log4j.logger.org.solrmarc.marc.MarcImporter=INFO
+log4j.logger.org.solrmarc=DEBUG
+log4j.logger.org.solrmarc.index.extractor.impl.java.JavaValueExtractorUtils=INFO
+log4j.logger.org.solrmarc.index.utils.FastClasspathUtils=INFO
 log4j.logger.org.solrmarc.marc.MarcHandler=INFO
-log4j.logger.org.solrmarc.tools.Utils=INFO
+log4j.logger.org.solrmarc.tools.PropertyUtils=INFO
+log4j.logger.org.solrmarc.tools.DataUtil=INFO
+log4j.logger.org.solrmarc.index.mapping.impl.TranslationMappingFactory=INFO
+log4j.logger.org.solrmarc.driver.ThreadedIndexer=INFO
+log4j.logger.org.solrmarc.driver.ChunkIndexerWorker=INFO
+log4j.logger.org.solrmarc.driver.IndexDriver=DEBUG
+log4j.logger.org.solrmarc.index.indexer.FullConditionalParser=INFO
+log4j.logger.org.solrmarc.index.indexer.VerboseSymbolFactory=INFO
+log4j.logger.org.apache.solr=INFO
 
 # stdout appender
 # Output the file name and line number to the console
@@ -23,7 +33,7 @@ log4j.appender.stdout.target=System.out
 # with a max file size of 100KB
 # and keep 1 previous log file
 log4j.appender.file=org.apache.log4j.RollingFileAppender
-log4j.appender.file.File=${one-jar.home.dir}solrmarc.log
+log4j.appender.file.File=${solrmarc.jar.dir}/solrmarc.log
 log4j.appender.file.MaxFileSize=100KB
 log4j.appender.file.MaxBackupIndex=1
 log4j.appender.file.layout=org.apache.log4j.PatternLayout
diff --git a/import/marc.properties b/import/marc.properties
index 95de9d0d30bdecdce20c497311fc62646ef5eecb..b743ac762026721b50db21b6e022c67539937077 100644
--- a/import/marc.properties
+++ b/import/marc.properties
@@ -17,6 +17,31 @@ allfields = custom, getAllSearchableFieldsAsSet(100, 900)
 language = 008[35-37]:041a:041d:041h:041j, language_map.properties
 format = custom, getFormat, format_map.properties
 
+# All of the *FilteredByRelator() indexing functions take the same list of
+# parameters:
+#
+# 1. The field specification for retrieving author names
+# 2. A colon-delimited list of tags whose contents should be accepted even if no
+#    relator term is specified.
+# 3. Either a colon-delimited list of relator terms whose corresponding authors
+#    should be indexed into the field, or a setting name from the [AuthorRoles]
+#    section of author-classification.ini containing such a list.
+# 4. A colon-delimited list of tags whose contents should be accepted if an
+#    unknown (not defined in author-classification.ini's [RelatorSynonyms] section)
+#    relator term is encountered. You can set this to "none" to ignore all unknown
+#    relator terms. Optional; defaults to the same list as parameter 2.
+# 5. Whether to index raw relator terms instead of mapping them according to
+#    [RelatorSynonyms] in author-classification.ini. Default value is 'false' (do
+#    not index raw values).
+#
+# It is VERY IMPORTANT to pass identical values for all 5 parameters to all related
+# fields. Thus, author/author_variant/author_fuller/author_role/author_sort should
+# all receive the same parameters, as should the groupings of
+# author2/author2_variant/author2_fuller/author2_role and
+# author_corporate/author_corporate_role. If the parameters are not kept in sync,
+# the index may contain mismatched author names and relator values.
+#
+# see $VUFIND_HOME/config/vufind/author-classification.ini for relator definitions
 author                = custom, getAuthorsFilteredByRelator(100abcd:700abcd,100,firstAuthorRoles)
 author_variant        = custom, getAuthorInitialsFilteredByRelator(100a:700a,100,firstAuthorRoles)
 author_fuller         = custom, getAuthorsFilteredByRelator(100q:700q,100,firstAuthorRoles)
@@ -39,6 +64,7 @@ title_alt = 100t:130adfgklnpst:240a:246a:505t:700t:710t:711t:730adfgklnpst:740a
 title_old = 780ast
 title_new = 785ast
 title_sort = custom, getSortableTitle
+#title_sort = 245ab, join(" "), titleSortLower
 series = 440ap:800abcdfpqt:830ap
 series2 = 490a
 
@@ -84,7 +110,7 @@ dewey-raw = 082a:083a
 
 # Extract the numeric portion of the OCLC number using a pattern map:
 oclc_num = 035a, (pattern_map.oclc_num)
-pattern_map.oclc_num.pattern_0 = \\([Oo][Cc][Oo][Ll][Cc]\\)[^0-9]*[0]*([0-9]+)=>$1
+pattern_map.oclc_num.pattern_0 = [(][Oo][Cc][Oo][Ll][Cc][)][^0-9]*[0]*([0-9]+)=>$1
 pattern_map.oclc_num.pattern_1 = ocm[0]*([0-9]+)[ ]*[0-9]*=>$1
 pattern_map.oclc_num.pattern_2 = ocn[0]*([0-9]+).*=>$1
 pattern_map.oclc_num.pattern_3 = on[0]*([0-9]+).*=>$1
diff --git a/import/marc_auth.properties b/import/marc_auth.properties
index cd6cf6e98b9ea985837f0611c32e42c38aa0ff05..0513aeb8a416d94ecdc1f78ef2c381e660e72d2a 100644
--- a/import/marc_auth.properties
+++ b/import/marc_auth.properties
@@ -21,3 +21,15 @@ heading = custom, getAllSubfields(100:110:111, " ")
 use_for = custom, getAllSubfields(400:410:411, " ")
 see_also = custom, getAllSubfields(500:510:511, " ")
 scope_note = custom, getAllSubfields(665:663:360, " ")
+
+# RDA fields
+birth_date = 046f, first
+death_date = 046g, first
+birth_place = 370a, first
+death_place = 370b, first
+country = 370c
+related_place = 370f
+field_of_activity = 372a
+occupation = 374a
+gender = 375a
+language = 377a, language_map.properties
diff --git a/import/marc_local.properties b/import/marc_local.properties
index 6306b933e568a5f42086703ab688122aa06ee819..a5bc4cdf01687bbb60a9bd16916041d5a4cd50e6 100644
--- a/import/marc_local.properties
+++ b/import/marc_local.properties
@@ -54,9 +54,15 @@
 #       https://vufind.org/wiki/indexing:full_text_tools
 #fulltext = custom, getFulltext(856u, pdf)
 
-# Uncomment the following line if you want to index latitude/longitude data for
-# Google Map recommendations:
-#long_lat = custom, getLongLat
+# Uncomment the following if you want to use the OpenLayers3 Geographic Search
+# and Google Map or OpenLayers3 Geo-Display functionality
+# See searches.ini for configuration options for Geographic Searching.
+# See config.ini for configuration options for Geo-Display.
+#location_geo = custom, getAllCoordinates
+#long_lat = custom, getPointCoordinates
+#long_lat_display = custom, getDisplayCoordinates
+#long_lat_label = 034z
+
 
 # Uncomment the following lines if you are indexing journal article data that uses
 # the 773 field to describe the journal containing the article.  These settings
diff --git a/import/solrmarc_core_3.0.6.jar b/import/solrmarc_core_3.0.6.jar
new file mode 100644
index 0000000000000000000000000000000000000000..1c2bbf20c8f6f980b6fbb25750e65c57386515da
Binary files /dev/null and b/import/solrmarc_core_3.0.6.jar differ
diff --git a/import/webcrawl.php b/import/webcrawl.php
index bfa504881949bec2a844e22d2fe74ff2039b8e06..24857961c8ab76bf9a16fadac73d3df26c7e347e 100644
--- a/import/webcrawl.php
+++ b/import/webcrawl.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Utilities
diff --git a/import/xsl/nlm_ojs.xsl b/import/xsl/nlm_ojs.xsl
index a535a03b3128a76071067fe95d0453821f5bd583..62c41fefa7cbcd1422630daf1037ca77b40435b1 100644
--- a/import/xsl/nlm_ojs.xsl
+++ b/import/xsl/nlm_ojs.xsl
@@ -62,7 +62,7 @@
                     </field>
                 </xsl:if>
 
-                <!-- Article endPage !! Only enable this part of the code if you have defined "container_end_page" in  ./solr/biblio/conf/schema.xml -> <field name="container_end_page" type="text" indexed="true" stored="true"/> !!
+                <!-- Article endPage !! Only enable this part of the code if you have defined "container_end_page" in  ./solr/vufind/biblio/conf/schema.xml -> <field name="container_end_page" type="text" indexed="true" stored="true"/> !!
 
                 <xsl:if test="//nlm:lpage[normalize-space()]">
                         <field name="container_end_page">
diff --git a/import/xsl/vudl_FOXML.xsl b/import/xsl/vudl_FOXML.xsl
index 9f6434b0f697e5724fc5d7eecfbec9e5884cf9a2..c1eb61013d900989a6c2539bf6c2c72629232858 100644
--- a/import/xsl/vudl_FOXML.xsl
+++ b/import/xsl/vudl_FOXML.xsl
@@ -13,7 +13,7 @@ GNU General Public License for more details.
 
 You should have received a copy of the GNU General Public License
 along with this program; if not, write to the Free Software
-Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 -->
 <xsl:stylesheet version="1.0"
     xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
diff --git a/install.php b/install.php
index 81bf01ac97164a26d576793b6956b05e69e437b2..78ffaaaec0041bb813d029cc5673666be356e927 100644
--- a/install.php
+++ b/install.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Installer
@@ -372,7 +372,7 @@ function validateModule($module)
 {
     $regex = '/^[a-zA-Z][0-9a-zA-Z_]*$/';
     $illegalModules = array(
-        'VuDL', 'VuFind', 'VuFindAdmin', 'VuFindConsole', 'VuFindDevTools',
+        'VuFind', 'VuFindAdmin', 'VuFindConsole', 'VuFindDevTools',
         'VuFindLocalTemplate', 'VuFindSearch', 'VuFindTest', 'VuFindTheme',
     );
     if (in_array($module, $illegalModules)) {
diff --git a/languages/CreatorRoles/ca.ini b/languages/CreatorRoles/ca.ini
index a9d5436ea038cee63f082dc958ea7be35b40ad0d..0a7a50c0cf2b78048ee892aecdf1265b7446b36d 100644
--- a/languages/CreatorRoles/ca.ini
+++ b/languages/CreatorRoles/ca.ini
@@ -7,8 +7,11 @@ aut = "Autor"
 Beiträger = "Col·laborador"
 bsl = "Llibreter"
 chr = "Coreògraf"
+con = "Conservador"
 csl = "Consultor"
 ctb = "Col·laborador"
+ctg = "Cartògraf"
+cur = "Preservador"
 dnc = "Ballarí"
 elg = "Electricista"
 eng = "Ingenier"
@@ -17,6 +20,7 @@ Hrsg = "Editor"
 hrsg = "Editor"
 Ill = "Illustrador"
 ill = "Illustrador"
+ivr = "Entrevistador"
 jud = "Jutge"
 komm = "Commentarista"
 Komment = "Commentarista"
@@ -24,6 +28,7 @@ Komp = "Compositor"
 komp = "Compositor"
 lbr = "Laboratori"
 lse = "Llicenciat"
+mod = "Moderador"
 mus = "Músic"
 nrt = "Narrador"
 opn = "Oponent"
@@ -36,6 +41,7 @@ prod = "Productor"
 res = "Investigador"
 rev = "Revisor"
 rps = "Repositori"
+sad = "Assesor científic"
 scl = "Escultor"
 sec = "Secretari"
 sgn = "Signant"
@@ -48,6 +54,9 @@ textverf = "Autor"
 trl = "Traductor"
 tänzer = "Ballarí"
 vac = "Doblador"
+Verstorb = "Mort"
+verstorb = "Mort"
+voc = "Vocalista"
 vorl = "Esborrany"
 wit = "Testimoni"
 Ãœbers = "Traductor"
diff --git a/languages/CreatorRoles/el.ini b/languages/CreatorRoles/el.ini
new file mode 100644
index 0000000000000000000000000000000000000000..ecf6f1d57785d991462066d499996c2d4fee0d07
--- /dev/null
+++ b/languages/CreatorRoles/el.ini
@@ -0,0 +1,311 @@
+acp = "Αντιγραφέας έργων τέχνης"
+act = "Ηθοποιός"
+adi = "Καλλιτεχνικός διευθυντής"
+adp = "Διασκευαστής"
+Adressat = "Παραλήπτης"
+adressat = "Παραλήπτης"
+aft = "Συγγραφέας του επιλόγου, κολοφώνα, κ.λπ."
+angebl hrsg = "Αμφίβολος εκδότης"
+angebl komp = "Αμφίβολος συγγραφέας / συνθέτης"
+Angebl Verf = "Αμφίβολος συγγραφέας"
+angebl verf = "Αμφίβολος συγγραφέας"
+angebl übers = "Αμφίβολος μεταφραστής"
+animation = "Κινούμενα σχέδια"
+anl = "Αναλυτής"
+ann = "Σχολιαστής (χειρόγραφων σημειώσεων σε έντυπο)"
+ant = "Βιβλιογραφικός πρόδρομος"
+ape = "Εναγόμενος"
+apl = "Εφεσείων"!"
+app = "Υποψήφιος"
+aqt = "Συγγραφέας αναφορών ή επιτομών"
+arc = "Αρχιτέκτονας"
+ard = "Καλλιτεχνικός διευθυντής"
+arr = "Διασκευαστής (μουσικής σύνθεσης)"
+art = "Καλλιτέχνης"
+asg = "Ανάδοχος Εκτύπωσης"
+asn = "Σχετικό/Συνδεόμενο Όνομα"
+ato = "Υπογράφων"
+att = "Αποδιδόμενο όνομα"
+auc = "Δημοπράτης"
+aud = "Συγγραφέας διαλόγων"
+Auftraggeber = "Πελάτης"
+aui = "Συγγραφέας εισαγωγής κ.λπ."
+aus = "Σεναριογράφος"
+aut = "Συγγραφέας"
+bdd = "Σχεδιαστής Βιβλιοδεσίας"
+Bearb = "Εκδότης"
+bearb = "Εκδότης"
+Begr = "Χρηματοδότης"
+begr = "Χρηματοδότης"
+Beiträger = "Συντελεστής"
+beiträger = "Λογοτεχνικός συντελεστής"
+beiträger k = "Καλλιτεχνικός συντελεστής"
+beiträger m = "Μουσικός συντελεστής"
+bjd = "Σχεδιαστής καλύμματος"
+bkd = "Σχεδιαστής βιβλίου"
+bkp = "Παραγωγός βιβλίου"
+blw = "Συγγραφέας του οπισθόφυλλου"
+bnd = "Βιβλιοδέτης"
+bpd = "Σχεδιαστής βιβλιοσήμου"
+brd = "Εκφωνητής"
+bsl = "Βιβλιοπώλης"
+ccp = "Ο συλλαμβάνων την πρωτότυπη ιδέα"
+choreinstud = "Πρόβα χορωδίας"
+choreogr = "Χορογράφος"
+chr = "Χορογράφος"
+clb = "Συνεργάτης"
+cli = "Πελάτης"
+cll = "Καλλιγράφος"
+clr = "Χρωματιστής (colorist)"
+clt = "Καλοτύπος"
+cmm = "Σχολιαστής"
+cmp = "Συνθέτης"
+cmt = "Στοιχειοθέτης"
+cnd = "Διευθυντής ορχήστρας"
+cng = "Κινηματογραφιστής"
+cns = "Λογοκριτής"
+coe = "Αντίδικος-Εφεσίβλητος"
+col = "Συλλέκτης"
+com = "Συντάκτης"
+con = "Έφορος-Συντηρητής"
+cos = "Αντίδικος"
+cot = "Αντίδικος-Εκκαλών"
+cov = "Σχεδιαστής εξωφύλλου"
+cpc = "Διεκδικητής πνευματικών δικαιωμάτων"
+cpe = "Ενάγων-appellee"
+cph = "Κάτοχος πνευματικών δικαιωμάτων"
+cpl = "Ενάγων"
+cpt = "Ενάγων-appellant"
+cre = "Δημιουργός"
+crp = "Αλληλογράφος"
+crr = "Διορθωτής"
+crt = "Δικαστικός ανταποκριτής"
+csl = "Σύμβουλος"
+csp = "Σύμβουλος σε ένα έργο"
+cst = "Ενδυματολόγος"
+ctb = "Συντελεστής"
+cte = "Contestee-Εφεσίβλητος"
+ctg = "Χαρτογράφος"
+ctr = "Ανάδοχος"
+cts = "Διάδικος"
+ctt = "Contestee-Εκκαλών"
+cur = "Έφορος"
+cwt = "Σχολιαστής για γραπτό κείμενο"
+darst = "Ερμηνευτής"
+dbp = "Τόπος διανομής"
+dfd = "Εναγόμενος"
+dfe = "Εναγόμενος-Εφεσίβλητος"
+dft = "Εναγόμενος-Εκκαλών"
+dgg = "Εκχωρητής βαθμού"
+dir = "Διευθυντής ορχήστρας"
+dis = "Ο εκπονών διατριβή"
+dnc = "Χορευτής"
+dnr = "Δωρητής"
+dpc = "Απεικονιζόμενος"
+dpt = "Καταθέτης"
+drehbuch = "Σεναριογράφος"
+drm = "Σχεδιαστής"
+drt = "Διευθυντής"
+dsr = "Σχεδιαστής"
+dst = "Διανομέας"
+dtc = "Συντελεστής δεδομένων"
+dte = "Αποδέκτης αφιέρωσης"
+dtm = "Διαχειριστής δεδομένων"
+dto = "Αφιερωτής"
+dub = "Αμφίβολος συγγραφέας"
+edc = "Επιμελητής σύνταξης"
+edm = "Υπεύθυνος μοντάζ"
+edt = "Επιμελητής έκδοσης"
+egr = "Χαράκτης"
+elg = "Ηλεκτρολόγος σκηνής"
+elt = "Κατασκευαστής κλισέ της ηλεκτροτυπίας"
+eng = "Μηχανικός"
+etr = "Οξυγράφος"
+evp = "Τόπος εκδήλωσης"
+exp = "Εμπειρογνώμονας"
+fac = "Κατασκευαστής πανομοιότυπων"
+fds = "Διανομέας κινηματογραφικής ταινίας"
+fld = "Διευθυντής εργασιών πεδίου"
+flm = "Επιμελητής κινηματογραφικής ταινίας"
+fmd = "Σκηνοθέτης"
+fmk = "Παραγωγός ανεξάρτητης/προσωπικής κινηματογραφικής ταινίας"
+fmo = "Προηγούμενος ιδιοκτήτης"
+fmp = "Παραγωγός κινηματογραφικής ταινίας"
+fnd = "Χρηματοδότης"
+Forts = "Παράταση"
+fotogr = "Φωτογράφος"
+frg = "Παραχαράκτης"
+gis = "Ειδικός γεωγραφικών πληροφοριών"
+grt = "Τεχνίτης γραφικών"
+hg = "Εκδότης"
+his = "Το φιλοξενόν ίδρυμα"
+hnr = "Τιμώμενος"
+Hrsg = "Εκδότης"
+hrsg = "Εκδότης"
+hst = "Οικοδεσπότης παρουσιαστής"
+Ill = "Εικονογράφος"
+ill = "Εικονογράφος"
+ilu = "Διακοσμητής χειρογράφου"
+ins = "Αφιερωτής (Επιγράφων)"
+inszenierung = "Υπεύθυνος σκηνής"
+interpr = "Διερμηνέας"
+interpret = "Διερμηνέας"
+interviewer = "Συνεντευξιαστής"
+interviewter = "Συνεντευξιαζόμενος"
+inv = "Εφευρέτης"
+isb = "Φορέας έκδοσης"
+itr = "Οργανοπαίκτης"
+ive = "Συνεντευξιαζόμενος"
+ivr = "Συνεντευξιαστής"
+jud = "Δικαστής"
+kamera = "Κάμερα"
+kartograph = "Χαρτογράφος"
+komm = "Σχολιαστής"
+Komment = "Σχολιαστής"
+Komp = "Συνθέτης"
+komp = "Συνθέτης"
+Korresp = "Αλληλογράφος"
+kostüm = "Ενδυματολόγος"
+lbr = "Εργαστήριο"
+lbt = "Λιμπρετίστας"
+led = "Επικεφαλής"
+len = "Δανειστής"
+lsa = "Αρχιτέκτονας τοπίων"
+lse = "Κάτοχος διακαιώματος εκτύπωσης"
+lso = "Χορηγός δικαιώματος εκτύπωσης"
+ltg = "Λιθογράφος"
+lyr = "Στιχουργός"
+med = "Μέσο"
+mfp = "Τόπος κατασκευής"
+mfr = "Κατασκευαστής"
+mitarb = "Συνεργάτης"
+mod = "Μεσολαβητής"
+mon = "Επόπτης"
+mrb = "Μαρμαροτεχνίτης"
+msd = "Μουσικός διευθυντής"
+mte = "Χαράκτης μετάλλου"
+mtk = "Πρακτικογράφος"
+mus = "Μουσικός"
+mutmassl Verf = "Πιθανός συγγραφέας"
+mutmassl VerfKomp = "Πιθανός συγγραφέας / composer"
+mutmaßl hrsg = "Πιθανός εκδότης"
+Mutmaßl Verf = "Πιθανός συγγραφέας"
+mutmaßl verf = "Πιθανός συγγραφέας"
+mutmaßl übers = "Πιθανός μεταφραστής"
+Nachr = "Συγγραφέας του επιλόγου, κολοφώνα, κ.λπ."
+nrt = "Αφηγητής"
+opn = "Αντίπαλος"
+org = "Δημιουργός"
+orm = "Διοργανωτής συνεδρίασης"
+osp = "Παρουσιαστής"
+oth = "Άλλος"
+own = "Ιδιοκτήτης"
+pat = "Υποστηρικτής"
+pbd = "Διευθυντής έκδοσης"
+pbl = "Εκδότης"
+pdr = "Διευθυντής έργου"
+pfr = "Διορθωτής τυπογραφικών δοκιμίων"
+pht = "Φωτογράφος"
+plt = "Κατασκευαστής τυπογραφικών πλακών"
+pma = "Φορέας έκδοσης άδειας"
+pmn = "Διευθυντής παραγωγής"
+pop = "Τυπογράφος πλακών"
+ppm = "Χαρτοποιός"
+ppt = "Κουκλοπαίχτης"
+prc = "Διαδικασία επαφής"
+prd = "Προσωπικό παραγωγής"
+pre = "Παρουσιαστής"
+prf = "Ερμηνευτής"
+prg = "Προγραμματιστής"
+prn = "Εταιρία παραγωγής"
+pro = "Παραγωγός"
+prod = "Παραγωγός"
+prp = "Τόπος παραγωγής"
+prs = "Σχεδιαστής παραγωγής"
+prt = "Εκτυπωτής"
+prv = "Πάροχος"
+pta = "Υποψήφιος για Δίπλωμα ευρεσιτεχνίας"
+ptf = "Ενάγων"
+pth = "Κάτοχος ευρεσιτεχνίας"
+pup = "Τόπος έκδοσης"
+rbr = "Ερυθρογράφος"
+rcd = "Καταγραφέας"
+rce = "Μηχανικός καταγραφής"
+rcp = "Παραλήπτης"
+rdd = "Ραδιοφωνικός διευθυντής"
+realisation = "Πραγματοποίηση"
+Red = "Συντάκτης"
+red = "Συντάκτης"
+regie = "Διευθυντής"
+reporter = "Ανταποκριτής"
+res = "Ερευνητής"
+resp = "Εναγόμενος"
+rev = "Κριτικός"
+rpc = "Ραδιοφωνικός παραγωγός"
+rps = "Αποθετήριο"
+rpt = "Ανταποκριτής"
+rpy = "Υπεύθυνος"
+rse = "Εναγόμενος-appellee"
+rsp = "Εναγόμενος"
+rsr = "Υπεύθυνος ανακαίνισης"
+rst = "Εναγόμενος-appellant"
+rth = "Κεφαλή ερευνητικής ομάδας"
+rtm = "Μέλος ερευνητικής ομάδας"
+sad = "Επιστημονικός σύμβουλος"
+Sammler = "Συλλέκτης"
+sammler = "Συλλέκτης"
+sce = "Σεναριογράφος"
+Schreiber = "Συγγραφέας"
+scl = "Γλύπτης"
+scr = "Γραφέας"
+sds = "Ηχολήπτης"
+sec = "Γραμματέας"
+sgd = "Διευθυντής σκηνής"
+sgn = "Υπογράφων"
+sht = "Φιλοξενία/Υποστήριξη"
+sll = "Πωλητής"
+sng = "Τραγουδιστής"
+spk = "Εκφωνητής"
+spn = "Χορηγός"
+sprecher = "Εκφωνητής"
+srv = "Επιθεωρητής"
+std = "Σχεδιαστής σκηνικών"
+stecher = "Χαράκτης"
+stl = "Αφηγητής"
+stm = "Διευθυντής σκηνής"
+stn = "Σώμα προτύπων"
+str = "Τσιγκογράφος"
+tcd = "Τεχνικός Διευθυντής (παραγωγής)"
+tch = "Δάσκαλος"
+textverf = "Συγγραφέας"
+ths = "Εισηγητής διατριβής"
+tld = "Τηλεοπτικός σκηνοθέτης"
+tlp = "Τηλεοπτικός παραγωγός"
+trc = "Αντιγραφέας"
+trl = "Μεταφραστής"
+tyd = "Σχεδιαστής τυπογραφικών στοιχείων"
+tyg = "Τυπογράφος"
+tänzer = "Χορευτής"
+uvp = "Τοποθεσία Πανεπιστημίου"
+vac = "Ηθοποιός που δανείζει τη φωνή του"
+vdg = "Χειριστής τηλεοπτικής κάμερας"
+Verstorb = "Αποθανών"
+verstorb = "Αποθανών"
+voc = "Αοιδός"
+vorl = "Προσχέδιο"
+Vorr = "Συγγραφέας εισαγωγής, κ.λπ."
+wac = "Συγγραφέας πρόσθετου σχολιασμού"
+wal = "Συγγραφέας των πρόσθετων στίχων"
+wam = "Συγγραφέας του συνοδευτικού υλικού"
+wat = "Συγγραφέας του πρόσθετου κειμένου"
+wdc = "Υλοτόμος"
+wde = "Ξυλογράφος"
+widmungsempfänger = "Αυτός στον  οποίο αφιερώνεται κάτι"
+win = "Συγγραφέας της εισαγωγής"
+wit = "Μάρτυρας"
+wpr = "Συγγραφέας του προλόγου"
+wst = "Συγγραφέας των συμπληρωματικών περιεχομένων κειμένου"
+zeichner = "Σχεδιαστής"
+zensor = "Λογοκριτής"
+Übers = "Μεταφραστής"
+übers = "Μεταφραστής"
diff --git a/languages/CreatorRoles/en-gb.ini b/languages/CreatorRoles/en-gb.ini
new file mode 100644
index 0000000000000000000000000000000000000000..cadd95abc80299bd06a41f89de2a892f98142dc5
--- /dev/null
+++ b/languages/CreatorRoles/en-gb.ini
@@ -0,0 +1 @@
+@parent_ini = "CreatorRoles/en.ini"
diff --git a/languages/CreatorRoles/eu.ini b/languages/CreatorRoles/eu.ini
new file mode 100644
index 0000000000000000000000000000000000000000..305db6c33bff37c50963e3c331e4eefd1747bcd9
--- /dev/null
+++ b/languages/CreatorRoles/eu.ini
@@ -0,0 +1,346 @@
+abr = "Biltzailea"
+acp = "Arte kopiatzailea"
+act = "Aktorea"
+adi = "Arte zuzendaria"
+adp = "Egokitzailea"
+Adressat = "Jasotzailea"
+adressat = "jasotzailea"
+aft = "Amaieraren egilea"
+angebl hrsg = "Zalantzazko argitaratzailea"
+angebl komp = "Zalantzazko egilea / konpositorea"
+Angebl Verf = "Zalantzazko egilea"
+angebl verf = "Zalantzazko egilea"
+angebl übers = "Zalantzazko itzultzailea"
+animation = "Animazioa"
+anl = "Analista"
+anm = "Animatzailea"
+ann = "Ohargilea"
+ant = "Aurrekari bibliografikoa"
+ape = "Apelatua"
+apl = "Apelatzen duena"
+app = "Eskatzailea"
+aqt = "Laburpen egilea"
+arc = "Arkitektoa"
+ard = "Zezendari artistikoa"
+arr = "Moldatzailea"
+art = "Artista"
+asg = "Lagapen-hartzailea"
+asn = "Lotutako izena"
+ato = "Autografoa"
+att = "Esleitutako izena"
+auc = "Enkantea egilea"
+aud = "Hizketa egilea"
+Auftraggeber = "Bezeroa"
+aui = "Introdukzio egilea, etabar."
+aus = "Gidoilaria"
+aut = "Egilea"
+bdd = "Koadernaketa diseinatzailea"
+Bearb = "Argitaratzailea"
+bearb = "argitaratzailea"
+Begr = "Babeslea"
+begr = "babeslea"
+Beiträger = "Laguntzailea"
+beiträger = "Literatura-laguntzailea"
+beiträger k = "Arte-lagutnzailea"
+beiträger m = "Musika-laguntzailea"
+bjd = "Liburu-azal diseinatzailea"
+bkd = "Liburu diseinatzailea"
+bkp = "Liburu ekoizlea"
+blw = "Publizitate idazlea"
+bnd = "Koadernatzailea"
+bpd = "Ex libris diseinatzailea"
+brd = "Esataria"
+brl = "Braille estanpatzailea"
+bsl = "Liburu-saltzailea"
+bühnenbild = "Set diseinatzailea"
+cas = "Emailea"
+ccp = "Kontzeptista"
+choreinstud = "Koru saiakera"
+choreogr = "Koreografoa"
+chr = "Koreografoa"
+clb = "Kolaboratzailea"
+cli = "Bezeroa"
+cll = "Kaligrafia egilea"
+clr = "Kolorista"
+clt = "Koloidazlea"
+cmm = "Komentarista"
+cmp = "Konpositorea"
+cmt = "Konpositorea"
+cnd = "Gidaria"
+cng = "Zinematografoa"
+cns = "Zentsorea"
+coe = "Parte-hartzailea"
+col = "Biltzailea"
+com = "Konpilatzailea"
+con = "Kontserbatzailea"
+cor = "Bilduma erregistratzailea"
+cos = "Lehiakidea"
+cot = "Parte-hartzailea"
+cou = "Auzitegi gobernatua"
+cov = "Azal diseinatzailea"
+cpc = "Copyright eskatzailea"
+cpe = "Apelaziora demandatua"
+cph = "Copyright edukitzailea"
+cpl = "Kereilaria"
+cpt = "Sortzailea"
+cre = "Sortzailea"
+crp = "Korrespontsala"
+crr = "Zuzentzailea"
+crt = "Auzitegi-berriemailea"
+csl = "Kontsultaria"
+csp = "Proiektu baterako kontsultaria"
+cst = "Jantzien disenatzailea"
+ctb = "Laguntzailea"
+cte = "Lehiaketaren irabazlea"
+ctg = "Kartografo"
+ctr = "Kontraktorea"
+cts = "Lehiaketaren irabazlea"
+ctt = "Lehiaketaren apelatua"
+cur = "Curator"
+cwt = "Testu idatzien iruzkintzailea"
+darst = "Interpretatzailea"
+dbp = "Banaketaren lekua"
+dfd = "Akusatua"
+dfe = "Akusatuaren apelatua"
+dft = "Akusatuaren apelatzailea"
+dgg = "Graduko titulua ematen duen erakundea"
+dgs = "Graduko aztertzailea"
+dir = "Gidaria"
+dis = "Mintzalaria"
+dln = "Delineatzailea"
+dnc = "Dantzaria"
+dnr = "Emailea"
+dpc = "Ordezkatua"
+dpt = "Jasotzailea"
+drehbuch = "Gidoilaria"
+drm = "Marrazkilaria"
+drt = "Zuzendaria"
+dsr = "Diseinatzailea"
+dst = "Laguntzailea"
+dtc = "Datu laguntzailea"
+dte = "Dedikatua"
+dtm = "Datu arduraduna"
+dto = "Dedikatzailea"
+dub = "Zalantzazko egilea"
+edc = "Bildumaren argitaratzailea"
+edm = "Irudi mugimenduan lanaren argitaratzailea"
+edt = "Argitaratzailea"
+egr = "Grabatzailea"
+elg = "Argiketaria"
+elt = "Elektrotipoen egilea"
+eng = "Ingeniaria"
+enj = "Eskumenaren promulgazioa"
+etr = "Akuaforte grabatzailea"
+evp = "Ekitaldiaren lekua"
+exp = "Aditua"
+fac = "Faksimile egilea"
+fds = "Filmen banatzailea"
+fld = "Sailaren zuzendaria"
+flm = "Filmaren argitaratzailea"
+fmd = "Filmaren zuzendaria"
+fmk = "Filmaren egilear"
+fmo = "Lehengo jabea"
+fmp = "Filmaren produktorea"
+fnd = "Babeslea"
+Forts = "Jarraipena"
+fotogr = "Argazkilaria"
+fpy = "Lehenengo alderdia"
+frg = "Faltsutzailea"
+gis = "Informazio geografikoaren aditua"
+grt = "Tekniko grafikoa"
+hg = "Argitaratzailea"
+his = "Erakunde anfitrioia"
+hnr = "Omendua"
+Hrsg = "Argitaratzailea"
+hrsg = "Argitaratzailea"
+hst = "Anfitrioia"
+Ill = "Ilustratzailea"
+ill = "Ilustratzailea"
+ilu = "Argitzailea"
+ins = "Izena eman duena"
+inszenierung = "Eszenaren arduraduna"
+interpr = "Interpretatzailea"
+interpret = "Interpretatzailea"
+interviewer = "Elkarrizketatzailea"
+interviewter = "Elkarrizketatua"
+inv = "Asmatzailea"
+isb = "Igortze erakundea"
+itr = "Jotzailea"
+ive = "Elkarrizketatua"
+ivr = "Elkarrizketatzailea"
+jud = "Epailea"
+jug = "Jurisdikzio gobernatua"
+kad = "Kadentzia egilea"
+kamera = "Kamara"
+kartograph = "Kartografoa"
+komm = "Iruzkintzailea"
+Komment = "Iruzkintzailea"
+Komp = "Konpositorea"
+komp = "Konpositorea"
+Korresp = "Korrespontsala"
+kostüm = "Jantzien diseinatzailea"
+lbr = "Laborategia"
+lbt = "Libretista"
+ldr = "Laborategiaren zuzendaria"
+led = "Liderra"
+lee = "Difamazioaren apelatua"
+lel = "Difamatua"
+len = "Mailegatzailea"
+let = "Difamazioaren apelatzailea"
+lgd = "Lighting designer"
+lie = "Beltzailearen-apelatua"
+lil = "Beltzailea"
+lit = "Beltzailearen-apelatzailea"
+lsa = "Pasaia-arkitektua"
+lse = "Baimenduna"
+lso = "Baimena emailea"
+ltg = "Litografoa"
+lyr = "Kanta-idazlea"
+mcp = "Musika kopista"
+mdc = "Metadata kontaktua"
+med = "Euskarria"
+mfp = "Fabrikazio lekua"
+mfr = "Fabrikatzailea"
+mitarb = "Laguntzailea"
+mod = "Moderatzailea"
+moderation = "Moderazioa"
+mon = "Monitorea"
+mrb = "Marmolgilea"
+mrk = "Markup editorea"
+msd = "Musika zuzendaria"
+mte = "Metal-grabatzailea"
+mtk = "Minutu hartzailea"
+mus = "Musikaria"
+mutmassl Verf = "Egile putatiboa"
+mutmassl VerfKomp = "Egile putatiboa / konpositorea"
+mutmaßl hrsg = "Argitaratzaile putatiboa"
+Mutmaßl Verf = "Egile putatiboa"
+mutmaßl verf = "egile putatiboa"
+mutmaßl übers = "Itzultzaile putatiboa"
+Nachr = "Amaieraren egile putatiboa"
+nrt = "Kontalaria"
+opn = "Aurkaria"
+org = "Sortzailea"
+orm = "Antolatzailea"
+osp = "Onscreen aurkezlea"
+oth = "Beste bat"
+own = "Jabea"
+pan = "Panelista"
+pat = "Patroia"
+pbd = "Argitalpenaren zuzendaria"
+pbl = "Argitaratzailea"
+pdr = "Proiektuaren zuzendaria"
+pfr = "Froga irakurlea"
+pht = "Argazkilaria"
+plt = "Plantxa egilea"
+pma = "Baimen agentzia"
+pmn = "Produkzio arduraduna"
+pop = "Plantxa imprimatzailea"
+ppm = "Papera egilea"
+ppt = "Txotxongilolaria"
+pra = "Errektorea"
+prc = "Prozesuaren kontaktua"
+prd = "Produkzio langileak"
+pre = "Aurkezlea"
+prf = "Interpretatzailea"
+prg = "Programatzailea"
+prm = "Inprimaketaren egilea"
+prn = "Produkzio enpresa"
+pro = "Produktorea"
+prod = "Produktorea"
+prp = "Produkzio lekua"
+prs = "Produkzio diseinatzailea"
+prt = "Inprimatzailea"
+prv = "Hornitzailea"
+präses = "Errektorea"
+pta = "Patente aplikatzailea"
+pte = "Auzi-jartzailearen apelatua"
+ptf = "Auzi-jartzailea"
+pth = "Patente edukitzailea"
+ptt = "Auzi-jartzailearen apelatzailea"
+pup = "Argitaratze lekua"
+rbr = "Sinatzailea"
+rcd = "Grabatzailea"
+rce = "Grabazioaren ingeniaria"
+rcp = "Jasotzailea"
+rdd = "Radio zuzendaria"
+realisation = "Errealizazioa"
+Red = "Erredaktorea"
+red = "Erredaktorea"
+regie = "Zuzendaria"
+ren = "Ordezkaria"
+reporter = "Erreportaria"
+res = "Ikertzailea"
+resp = "Demandatua"
+rev = "Aztertzailea"
+rpc = "Radio produktorea"
+rps = "Biltegia"
+rpt = "Erreportaria"
+rpy = "Ardeldiko arduraduna"
+rse = "Demandatua-apelatua"
+rsg = "Eszenan ipintzen duena"
+rsp = "Demandatua"
+rsr = "Birgaitzailea"
+rst = "Demandatua-apelatzailea"
+rth = "Ikerketa taldearen arduraduna"
+rtm = "Ikerketa taldearen bazkidea"
+sad = "Aholkulari zientifikoa"
+Sammler = "Biltzailea"
+sammler = "Biltzailea"
+sce = "Eszenan ipintzen duena"
+Schreiber = "Idazlea"
+scl = "Eskultorea"
+scr = "Eskribaua"
+sds = "Soinu diseinatzailea"
+sec = "Idazkaritza"
+sgd = "Sail zuzendaria"
+sgn = "Sinatzailea"
+sht = "Laguntzen duen anfitrioa"
+sll = "Kantaria"
+sng = "Kantaria"
+spk = "Hizlaria"
+spn = "Babeslea"
+sprecher = "Hizlaria"
+spy = "Bigarren alderdia/zatia"
+srv = "Ikuskatzailea"
+std = "Set diseinatzailea"
+stecher = "Grabatzailea"
+stg = "Baldintza"
+stl = "Kontaria"
+stm = "Eszena arduraduna"
+stn = "Arauen erakundea"
+str = "Estereotipoa"
+tcd = "Irakaslea"
+tch = "Irakaslea"
+textverf = "Egilea"
+ths = "Tesi aholkularia"
+tld = "Telebistaren arduraduna"
+tlp = "Telebistaren produktorea"
+trc = "Transkribatzailea"
+trl = "Itzultzailea"
+tyd = "Tipo diseinatzailea"
+tyg = "Tipografoa"
+tänzer = "Dantzaria"
+uvp = "Unibertsitate lekua"
+vac = "Ahots aktorea"
+vdg = "Bideografoa"
+Verstorb = "Hildakoa"
+verstorb = "Hildakoa"
+voc = "Kantaria"
+vorl = "Zirriborroa"
+Vorr = "Sarreraren egilea"
+wac = "Iruzkin gehigarriaren idazlea"
+wal = "Musika-letra gehigarriaren idazlea"
+wam = "Material gehigarriaren idazlea"
+wat = "Testu gehigarriaren idazlea"
+wdc = "Egurgilea"
+wde = "Egur grabatzailea"
+widmungsempfänger = "Dedikatua"
+win = "Sarreraren idazlea"
+wit = "Lekukoa"
+wpr = "Prefazioaren idalea"
+wst = "Testu-eduki gehigarriaren idazlea"
+zeichner = "Txostengilea"
+zensor = "Zentsorea"
+Ãœbers = "Itzultzailea"
+übers = "Itzultzailea"
diff --git a/languages/CreatorRoles/fi.ini b/languages/CreatorRoles/fi.ini
index 09a25e07e6e616534dbd8653d3a59d50a4100fa3..5baf93b816b24c92d3c93abd7f1da6509843d2ae 100644
--- a/languages/CreatorRoles/fi.ini
+++ b/languages/CreatorRoles/fi.ini
@@ -20,6 +20,7 @@ cnd = "Johtaja"
 cng = "Kuvaaja"
 com = "Kokoaja"
 cph = "Tekijänoikeuden omistaja"
+cst = "Puvustaja"
 ctb = "Avustaja"
 ctg = "Kartoittaja"
 cwt = "Tekstin kommentaattori"
@@ -31,7 +32,9 @@ dst = "Jakaja"
 dub = "Kyseenalainen tekijä"
 edt = "Toimittaja"
 egr = "Kaivertaja"
+exp = "Ekspertti"
 fac = "Jäljenteen toteuttaja"
+fds = "Levittäjä"
 flm = "Leikkaaja"
 fmp = "Tuottaja"
 fnd = "Rahoittaja"
@@ -43,6 +46,7 @@ ive = "Haastateltava"
 ivr = "Haastattelija"
 lbr = "Laboratorio"
 lbt = "Libretisti"
+lgd = "Valosuunnittelija"
 lyr = "Sanoittaja"
 mdc = "Metadatakontakti"
 mus = "Muusikko"
@@ -52,14 +56,17 @@ own = "Omistaja"
 pbd = "Päätoimittaja"
 pbl = "Julkaisija"
 pht = "Valokuvaaja"
+pmn = "Tuotantopäällikkö"
 prd = "Tuotantohenkilökunta"
 prf = "Esittäjä"
 prn = "Tuotantoyhtiö"
 pro = "Tuottaja"
+prs = "Tuotannon suunnittelija"
 prt = "Painaja"
 rce = "Äänittäjä"
 rev = "Kriitikko"
 rpy = "Vastaava toimittaja"
+sds = "Äänisuunnittelija"
 sng = "Laulaja"
 spk = "Puhuja"
 std = "Lavastaja"
@@ -67,5 +74,6 @@ tch = "Opettaja"
 trl = "Kääntäjä"
 voc = "Laulusolisti"
 wam = "Liiteaineiston tekijä"
+wst = "Täydentävän sisällön kirjoittaja"
 Übers = "Kääntäjä"
 übers = "Kääntäjä"
diff --git a/languages/CreatorRoles/fr.ini b/languages/CreatorRoles/fr.ini
new file mode 100644
index 0000000000000000000000000000000000000000..960a443840cfd0120058fc534860ab6ac4e947d6
--- /dev/null
+++ b/languages/CreatorRoles/fr.ini
@@ -0,0 +1,340 @@
+abr = "Abréviateur"
+acp = "Copiste d'art"
+act = "Acteur"
+adi = "Directeur artistique"
+adp = "Adaptateur"
+Adressat = "Destinataire"
+adressat = "Destinataire"
+aft = "Auteur de la postface, de l'achevé d'imprimer, etc."
+angebl hrsg = "Éditeur douteux"
+angebl komp = "Auteur douteux"
+Angebl Verf = "Auteur douteux"
+angebl verf = "Auteur douteux"
+angebl übers = "Traducateur douteux"
+animation = "Animateur"
+anl = "Analyste"
+anm = "Animateur"
+ann = "Annotateur"
+ant = "Antécédent bibliographique"
+ape = "Appelé"
+apl = "Appelant"
+app = "Candidat"
+aqt = "Auteur mentionné dans une citation ou des extraits de textes"
+arc = "Architecte"
+ard = "Directeur artistique"
+arr = "Arrangeur"
+art = "Artiste"
+asg = "Cessionnaire"
+asn = "Nom associé"
+ato = "Autographeur"
+att = "Nom attribué"
+auc = "Commissaire-priseur"
+aud = "Auteur du dialogue"
+Auftraggeber = "Client"
+aui = "Auteur de l'introduction, etc."
+aus = "Auteur d'un scénario, etc."
+aut = "Auteur"
+bdd = "Concepteur de reliures"
+Bearb = "Éditeur"
+bearb = "Éditeur"
+Begr = "Financeur"
+begr = "Financeur"
+Beiträger = "Contributeur"
+beiträger = "Contributeur littéraire"
+beiträger k = "Contributeur artistique"
+beiträger m = "Contributeur musical"
+bjd = "Concepteur de jaquettes"
+bkd = "Créateur de couverture"
+bkp = "Producteur de livres"
+blw = "Auteur de la quatrième de couverture"
+bnd = "Relieur"
+bpd = "Concepteur d'ex-libris"
+brd = "Diffuseur"
+brl = "Encodeur braille"
+bsl = "Libraire"
+bühnenbild = "Set designer"
+cas = "Casteur"
+ccp = "Concepteur"
+choreinstud = "Répétiteur de chorale"
+choreogr = "Chorégraphe"
+chr = "Chorégraphe"
+clb = "Collaborateur"
+cli = "Client"
+cll = "Calligraphiste"
+clr = "Coloriste"
+clt = "Responsable de la phototypie"
+cmm = "Commentateur"
+cmp = "Compositeur"
+cmt = "Compositeur (Imprimerie)"
+cnd = "Chef d'orchestre"
+cng = "Directeur de la photographie"
+cns = "Censeur"
+coe = "Contestant-intimé"
+col = "Collectionneur"
+com = "Compilateur"
+con = "Conservateur"
+cor = "Déspositaire de la collection"
+cos = "Contestant"
+cot = "Contestant-appelant"
+cov = "Concepteur de pages couvertures"
+cpc = "Demandeur du droit d'auteur"
+cpe = "Plaignant-intimé"
+cph = "Titulaire du droit d'auteur"
+cpl = "Plaignant"
+cpt = "Plaignant-appelant"
+cre = "Créateur"
+crp = "Correspondant"
+crr = "Réviseur"
+csl = "Expert-conseil"
+csp = "Consultant sur le projet"
+cst = "Costumier"
+ctb = "Collaborateur"
+cte = "Contesté-intimé"
+ctg = "Cartographe"
+ctr = "Contractant"
+cts = "Contesté"
+ctt = "Contesté-appelant"
+cur = "Conservateur d'exposition"
+cwt = "Commentateur d'un texte écrit"
+darst = "Performer"
+dbp = "Distribution des places"
+dfd = "Défendeur"
+dfe = "Défendeur-intimé"
+dft = "Défendeur-appelant"
+dgg = "Institution émettrice d'un diplôme"
+dgs = "superviseur de diplôme"
+dir = "Conducteur"
+dis = "Doctorant"
+dln = "Dessinateur"
+dnc = "Danseur"
+dnr = "Donateur"
+dpc = "Entité illustrée"
+dpt = "Déposant"
+drehbuch = "Scénariste"
+drm = "Dessinateur"
+drt = "Directeur"
+dsr = "Designer"
+dst = "Distributeur"
+dtc = "Contributeur de données"
+dte = "Dédicataire"
+dtm = "Gestionnaire de données"
+dto = "Dédicataire"
+dub = "Auteur douteux"
+edc = "Éditeur de la compilation"
+edm = "Éditeur de l'image animée"
+edt = "Éditeur intellectuel"
+egr = "Graveur"
+elg = "Électricien"
+elt = "Galvanotypeur"
+eng = "Ingénieur"
+enj = "Juridiction compétente"
+etr = "Aquafortiste"
+evp = "Lieu de l'évènement"
+exp = "Expert"
+fac = "Copiste"
+fds = "Distributeur"
+fld = "Directeur local"
+flm = "Monteur de films"
+fmd = "Réalisateur"
+fmk = "Réalisateur"
+fmo = "Ancien propriétaire"
+fmp = "Producteur de film"
+fnd = "Bailleur de fonds"
+fotogr = "Photographe"
+fpy = "Première partie"
+frg = "Faussaire"
+gis = "Spécialiste de l'information géographique"
+grt = "Graphiste"
+hg = "Éditeur"
+his = "Institution hôte"
+hnr = "Personne honorée"
+Hrsg = "Éditeur"
+hrsg = "Éditeur"
+hst = "Hôte"
+Ill = "Illustrateur"
+ill = "Illustrateur"
+ilu = "Enlumineur"
+ins = "Graveur"
+inszenierung = "Gestionnaire de plateau"
+interpr = "Interprète"
+interpret = "Interprète"
+interviewer = "Interviewer"
+interviewter = "Personne interviewé"
+inv = "Inventeur"
+isb = "Organisme émetteur"
+itr = "Instrumentiste"
+ive = "Personne interrogée"
+ivr = "Interviewer"
+jud = "Juge"
+kad = "Auteur cadentiel"
+kamera = "Caméraman"
+kartograph = "Cartographe"
+komm = "Commentateur"
+Komment = "Commentateur"
+Komp = "Compositeur"
+komp = "Compositeur"
+Korresp = "Correspondant"
+kostüm = "Costumier"
+lbr = "Laboratoire"
+lbt = "Librettiste"
+ldr = "Directeur de laboratoire"
+led = "Chef"
+lee = "Partie adverse-intimé"
+lel = "Partie adverse"
+len = "Prêteur"
+let = "Partie adverse-appelant"
+lgd = "Éclairagiste"
+lie = "Requérant-intimé"
+lil = "Requérant"
+lit = "Requérant-appelant"
+lsa = "Architecte-paysagiste"
+lse = "Porteur de licence"
+lso = "Donneur de licence"
+ltg = "Lithographe"
+lyr = "Parolier"
+mcp = "Copiste de musique"
+mdc = "Agent de liaison sur les métadonnées"
+med = "Média"
+mfp = "Lieu de production"
+mfr = "Fabricant"
+mitarb = "Collaborateur"
+mod = "Animateur de débat"
+moderation = "Modérateur"
+mon = "Moniteur"
+mrk = "Éditeur de balisage"
+msd = "Directeur musical"
+mte = "Graveur sur métal"
+mtk = "Prise de minutes"
+mus = "Musicien"
+mutmassl Verf = "Auteur supposé"
+mutmassl VerfKomp = "Auteur / compositeur supposé"
+mutmaßl hrsg = "Éditeur supposé"
+Mutmaßl Verf = "Auteur supposé"
+mutmaßl verf = "Auteur supposé"
+mutmaßl übers = "Traducteur supposé"
+Nachr = "Auteur de l'épilogue, colophon, etc."
+nrt = "Narrateur"
+opn = "Opposant"
+org = "Auteur"
+orm = "Organisateur de réunion"
+osp = "Présentateur"
+oth = "Autre"
+own = "Propriétaire"
+pan = "Panelliste"
+pat = "Client"
+pbd = "Directeur de la publication"
+pbl = "Éditeur"
+pdr = "Directeur de projet"
+pfr = "Relecteur"
+pht = "Photographe"
+plt = "Clicheur"
+pma = "Agence de réglementation"
+pmn = "Directeur de production"
+pop = "Imprimeur de planches"
+ppm = "Papetier"
+ppt = "Marionnettiste"
+pra = "Président"
+prc = "Agent de liaison du processus"
+prd = "Personnel de la réalisation"
+pre = "Présentateur"
+prf = "Interprète"
+prg = "Programmeur"
+prm = "Graveur, (arts visuels)"
+prn = "Compagnie de production"
+pro = "Producteur"
+prod = "Producteur"
+prp = "Lieu de production"
+prs = "Designer"
+prt = "Imprimeur"
+prv = "Fournisseur"
+präses = "Président"
+pta = "Demandeur de brevet"
+pte = "Demandeur-intimé"
+ptf = "Demandeur"
+pth = "Titulaire de brevet"
+ptt = "Demandeur-appelant"
+pup = "Lieur d'édition"
+rcd = "Enregistreur"
+rce = "Ingénieur du son"
+rcp = "Destinataire"
+rdd = "Directeur radio"
+realisation = "Réalisation ##"
+Red = "Rédacteur"
+red = "Rédacteur"
+regie = "Directeur"
+ren = "Perspectiviste"
+reporter = "Reporter"
+res = "Chercheur"
+resp = "Personne interrogée"
+rev = "Critique"
+rpc = "Producteur radio"
+rps = "Service d'archives"
+rpt = "Reporteur"
+rpy = "Partie responsable"
+rse = "Répondant-intimé"
+rsg = "Metteur en scène"
+rsp = "Répondant"
+rsr = "Restaurateur"
+rst = "Répondant-appelant"
+rth = "Chef d'équipe de chercheurs"
+rtm = "Membre d'équipe de chercheurs"
+sad = "Conseiller scientifique"
+Sammler = "Collectionneur"
+sammler = "Collectionneur"
+sce = "Scénariste"
+Schreiber = "Auteur"
+scl = "Sculpteur"
+scr = "Scribe"
+sds = "Concepteur du son"
+sec = "Secrétaire"
+sgd = "Directeur de plateau"
+sgn = "Signataire"
+sht = "Organisme de soutien"
+sll = "Vendeur"
+sng = "Chanteur"
+spk = "Intervenant"
+spn = "Commanditaire"
+sprecher = "Speaker"
+spy = "Deuxième partie"
+srv = "Arpenteur"
+std = "Décorateur de scène"
+stecher = "Graveur"
+stg = "Mise en place"
+stl = "Conteur"
+stm = "Régisseur de plateau"
+stn = "Organisme de normalisation"
+str = "Stéréotypeur"
+tcd = "Directeur technique"
+tch = "Professeur"
+textverf = "Auteur"
+ths = "Directeur de thèse"
+tld = "Directeur de télévision"
+tlp = "Producteur télévisé"
+trc = "Transcripteur"
+trl = "Traducteur"
+tyd = "Concepteur de caractères typographiques"
+tyg = "Typographe"
+tänzer = "Danseur"
+uvp = "Université"
+vac = "Acteur vocal"
+vdg = "Vidéaste"
+Verstorb = "Décédé"
+verstorb = "Décédé"
+voc = "Vocaliste"
+vorl = "Esquisse"
+Vorr = "Auteur de l'introduction, etc."
+wac = "Auteur du commentaire"
+wal = "Auteur des paroles ajoutées"
+wam = "Auteur du matériel d'appoint"
+wat = "Auteur du texte ajouté"
+wdc = "Xylographe"
+wde = "Graveur sur bois de bout"
+widmungsempfänger = "Dédicataire"
+win = "Auteur de l'introduction"
+wit = "Témoin"
+wpr = "Préfacier"
+wst = "Auteur du supplément textuel"
+zeichner = "Illustrateur"
+zensor = "Censeur"
+Ãœbers = "Traducteur"
+übers = "Traducteur"
diff --git a/languages/CreatorRoles/it.ini b/languages/CreatorRoles/it.ini
index 11baf050f8490acc603d67a0dad62bfeb05208ac..26ce96bdae9f99d8a5a4211a110b400172803f51 100644
--- a/languages/CreatorRoles/it.ini
+++ b/languages/CreatorRoles/it.ini
@@ -1,6 +1,7 @@
 abr = "Autore del riassunto"
 acp = "Copista d'arte"
 act = "Attore"
+adi = "Regista"
 adp = "Adattatore"
 Adressat = "Destinatario"
 adressat = "Destinatario"
@@ -18,6 +19,7 @@ ant = "Antecedente bibliografico"
 ape = "Appellato"
 apl = "Appellante"
 app = "Richiedente"
+aqt = "Autore in citazioni o abstract"
 arc = "Architetto"
 ard = "Direttore artistico"
 arr = "Arrangiatore"
@@ -38,6 +40,9 @@ bearb = "Curatore"
 Begr = "Finanziatore"
 begr = "Finanziatore"
 Beiträger = "Contributore"
+beiträger = "Contributo letterario"
+beiträger k = "Contributo artistico"
+beiträger m = "Contributo musicale"
 bjd = "Designer della sovraccoperta"
 bkd = "Designer del libro"
 bkp = "Produttore del libro"
@@ -47,8 +52,10 @@ bpd = "Designer dell'ex-libris"
 brd = "Annunciatore"
 brl = "Goffratore braille"
 bsl = "Libraio"
+bühnenbild = "Ideatore del set"
 cas = "Fonditore"
 ccp = "Ideatore"
+choreinstud = "Direttore del coro"
 choreogr = "Coreografo"
 chr = "Coreografo"
 clb = "Collaboratore"
@@ -60,19 +67,162 @@ cmm = "Commentatore"
 cmp = "Compositore"
 cmt = "Compositore tipografico"
 cnd = "Direttore d'orchestra"
+cng = "Direttore della fotografia"
 cns = "Censore"
+coe = "Contestant-appellee"
 col = "Raccoglitore"
 com = "Compilatore"
 con = "Conservatore"
+cor = "Gestore collezione"
+cos = "Contestante"
+cot = "Contestante - appellante"
+cou = "Court governed"
+cov = "Designer della copertina"
+cpc = "Rivendicatore del copyright"
+cpe = "Complainant-appellee"
+cph = "Titolare del copyright"
+cpl = "Ricorrente"
+cpt = "Ricorrente-appellante"
 cre = "Creatore"
+crp = "Corrispondente"
 crr = "Correttore"
+crt = "Stenografo"
+csl = "Consulente"
+csp = "Consulente per un progetto"
+cst = "Costumista"
+ctb = "Contributore"
+cte = "Contestee-appellee"
 ctg = "Cartografo"
+ctr = "Terzista"
+cts = "Contestee"
+ctt = "Contestee-appellant"
 cur = "Curatore"
+cwt = "Commentatore del testo scritto"
+darst = "Revisore"
+dbp = "Luogo di distribuzione"
+dfd = "Imputato"
+dfe = "Imputato-convenuto"
+dft = "Imputato-appellante'"
+dgg = "Istituto che emette la laurea"
+dgs = "Supervisore di laurea"
+dir = "Direttore"
+dis = "Laureando"
+dln = "Delineatore"
+dnc = "Ballerinor"
+dnr = "Donatore"
+dpc = "Raffigurato"
+dpt = "Depositante"
+drehbuch = "Sceneggiatore"
+drm = "Disegnatore"
+drt = "Direttore"
+dsr = "Designer"
+dst = "Distributore"
+dtc = "Contributore in dati"
+dte = "Persona a cui è dedicato"
+dtm = "Data manager"
+dto = "Dedicante"
+dub = "Autore dubbio"
+edc = "Redattore della compilazione"
+edm = "Redattore del montaggio"
+edt = "Redattore"
+egr = "Incisore"
+elg = "Elettricista"
+elt = "Elettrodattilografo"
+eng = "Ingegnere"
+enj = "Giurisdizione"
+etr = "Acquafortista"
+evp = "Luogo dell'evento"
+exp = "Esperto"
+fac = "Facsimilista"
+fds = "Distributore del film"
+fld = "Direttore del sito"
+flm = "Montatore"
+fmd = "Direttore del film"
+fmk = "Regista"
+fmo = "Precedente proprietario"
+fmp = "Produttore del film"
+fnd = "Finanziatore"
+Forts = "Continuazione"
+fotogr = "Fotografo"
+fpy = "First party"
+frg = "Falsario"
+gis = "Specialista in informazione geografica"
+grt = "Tecnico grafico"
+hg = "Editore"
+his = "Istituzione ospitante"
+hnr = "Celebrato"
+Hrsg = "Editore"
+hrsg = "Editore"
+hst = "Ospite"
+Ill = "Illustratore"
+ill = "Illustratore"
+ilu = "Illuminatore"
+ins = "Scrittore di epigrafi"
+inszenierung = "Direttore artistico"
+interpr = "Interprete"
+interpret = "Interprete"
+interviewer = "Intervistatore"
+interviewter = "Intervistato"
+inv = "Inventore"
+isb = "Società emittente"
+itr = "Strumentista"
+ive = "Intervistato"
+ivr = "Intervistatore"
+jud = "Giudice"
+jug = "Giurisdizione governata"
+kad = "Autore cadenze"
+kamera = "Macchina da presa"
+kartograph = "Cartografo"
+komm = "Commentatore"
+Komment = "Commentatore"
+Komp = "Compositore"
+komp = "Compositore"
+Korresp = "Correspondente"
+kostüm = "Costumista"
+lbr = "Laboratorio"
+lbt = "Librettista"
+ldr = "Direttore di laboratorio"
+led = "Capo"
+lee = "Libelee-appellee"
+lel = "Libelee"
+len = "Prestatore"
+let = "Libelee-appellant"
+lgd = "Designer dell'illuminazione"
+lie = "Libelista-convenuto"
+lil = "Libelista"
+lit = "Libelista-appellante"
+lsa = "Architetto del paesaggio"
+lse = "Titolare della licenza"
+lso = "Firmatario della licenza"
+ltg = "Litografo"
+lyr = "Paroliere"
+mcp = "Copista di musica"
+mdc = "Contatto dei metadati"
+med = "Mezzo"
+mfp = "Luogo di manifattura"
+mfr = "Fabbricante"
 mitarb = "Collaboratore"
 mod = "Moderatore"
+moderation = "Moderazione"
+mon = "Controllore"
+mrb = "Marmorizzatore"
+mrk = "Editore della marcatura"
+msd = "Direttore musicale"
+mte = "Incisore sul metallo"
+mtk = "Segretario"
 mus = "Musicista"
+mutmassl Verf = "Autore presunto"
+mutmassl VerfKomp = "Autore / compositore presunto"
+mutmaßl hrsg = "Editore presunto"
+Mutmaßl Verf = "Autore presunto"
+mutmaßl verf = "Autore presunto"
+mutmaßl übers = "Traduttore presunto"
+Nachr = "Autore della postfazione, colophon, etc."
 nrt = "Narratore"
+opn = "Opponente"
+org = "Originatore"
 orm = "Organizzatore"
+osp = "Presentatore sullo schermo"
 oth = "Altro"
 own = "Proprietario"
 pan = "Partecipante a una tavola rotonda"
@@ -88,6 +238,7 @@ pmn = "Direttore di produzione"
 pop = "Stampatore delle tavole"
 ppm = "Cartaro"
 ppt = "Burattinaio"
+pra = "Sintesi"
 prc = "Responsabile del procedimento"
 prd = "Personale di produzione"
 pre = "Presentatore"
@@ -98,7 +249,10 @@ prn = "Società di produzione"
 pro = "Produttore"
 prod = "Produttore"
 prp = "Luogo di produzione"
+prs = "Progettista di produzione"
 prt = "Stampatore"
+prv = "Fornitore"
+präses = "Sintesi"
 pta = "Richiedente del brevetto"
 pte = "Parte civile-appellata"
 ptf = "Parte civile"
@@ -110,25 +264,78 @@ rcd = "Finico"
 rce = "Ingegnere del suono"
 rcp = "Destinatario"
 rdd = "Regista radiofonico"
+realisation = "Realizazione"
 Red = "Redattore"
 red = "Redattore"
+regie = "Direttore"
+ren = "Renderista"
+reporter = "Reporter"
+res = "Ricercatore"
+resp = "Respondent"
 rev = "Revisore"
+rpc = "Produttore radio"
+rps = "Deposito"
+rpt = "Reporter"
+rpy = "Responsabile"
+rse = "Responsabile-convenuto"
+rsg = "Riallestitore"
+rsp = "Respondent"
+rsr = "Restauratore"
+rst = "Respondent-appellant"
+rth = "Capo team ricerca"
+rtm = "Membro team ricerca"
+sad = "Informatore scientifico"
+Sammler = "Collezionista"
+sammler = "Collezionista"
+sce = "Scenaggiatore"
+Schreiber = "Scrittore"
 scl = "Scultore"
+scr = "Scriba"
+sds = "Designer del suono"
+sec = "Segretario"
+sgd = "Direttore di scena"
+sgn = "Firmatario"
+sht = "Fornitore di supporto"
 sll = "Venditore"
 sng = "Cantante"
+spk = "Oratore"
+spn = "Sponsor"
+sprecher = "Oratore"
+spy = "Second party"
+srv = "Supervisore"
+std = "Scenografo"
+stecher = "Incisore"
+stg = "Ambientazione"
+stl = "Narratore"
+stm = "Direttore artistico"
+stn = "Ente di standardizzazione"
+str = "Stereotipista"
+tcd = "Direttore tecnico"
 tch = "Insegnante"
 textverf = "Autore"
 ths = "Relatore della tesi"
+tld = "Direttore televisivo"
+tlp = "Produttore televisivo"
+trc = "Transcrittore"
 trl = "Traduttore"
+tyd = "Designer dei caratteri"
 tyg = "Tipografo"
 tänzer = "Danzatore"
+uvp = "Sede dell'Università"
+vac = "Doppiatore"
+vdg = "Videografico"
 Verstorb = "Deceduto"
 verstorb = "Deceduto"
 voc = "Vocalista"
 vorl = "Bozza"
 Vorr = "Autore dell'introduzione etc."
+wac = "Scrittore dei commenti aggiunti"
+wal = "Scrittore delle liriche aggiunte"
+wam = "Scrittore del materiale di accompagnamento"
+wat = "Scrittore del testo aggiunto"
 wdc = "Incisore su legno"
 wde = "Xilografo"
+widmungsempfänger = "Persona a cui è dedicato"
 win = "Autore dell'introduzione"
 wit = "Testimome"
 wpr = "Autore della prefazione"
diff --git a/languages/CreatorRoles/ja.ini b/languages/CreatorRoles/ja.ini
new file mode 100644
index 0000000000000000000000000000000000000000..8b1b36aa63c1e29a4bee8da0f632020f97691ca3
--- /dev/null
+++ b/languages/CreatorRoles/ja.ini
@@ -0,0 +1,337 @@
+abr = "要約者"
+acp = "芸術コピーライター"
+act = "俳優"
+adi = "アートディレクター"
+adp = "アダプター"
+Adressat = "受信人"
+adressat = "受信人"
+aft = "あとがき、奥付の著者等"
+angebl hrsg = "疑わしい出版者"
+angebl komp = "疑わしい著者 / 作曲者"
+Angebl Verf = "疑わしい著者"
+angebl verf = "疑わしい著者"
+angebl übers = "疑わしい著者"
+animation = "アニメーション"
+anl = "アナリスト"
+anm = "アニメータ-"
+ann = "注釈者"
+ant = "書誌的先例"
+ape = "被控訴人"
+apl = " 上訴人"
+app = " 応募者"
+aqt = "引用や要約の著者"
+arc = "建築家"
+ard = "芸術デイレクター"
+arr = "アレンジャー"
+art = "芸術家"
+asg = "管財人"
+asn = "関連名"
+ato = "署名者"
+att = "属性名"
+auc = "競売人"
+aud = "対話著者"
+Auftraggeber = "クライアント"
+aui = "解説の著者, 等."
+aus = "映画脚本家"
+aut = "著者"
+bdd = "製本デザイナー"
+Bearb = "編集者"
+bearb = "編集者"
+Begr = "創立者"
+begr = "創立者"
+Beiträger = "寄稿家"
+beiträger = "文学寄稿家"
+beiträger k = "芸術寄稿家"
+beiträger m = "音楽寄稿家"
+bjd = "フックカバーデザイナー"
+bkd = "装幀デザイナー"
+bkp = "本制作者"
+blw = "カバー推薦広告作者"
+bnd = "製本技術者"
+bpd = "蔵書票デザイナー"
+brd = "放送キャスター"
+brl = "点字"
+bsl = "書店"
+bühnenbild = " 舞台装置家"
+cas = "キャスター"
+ccp = "コンセプター"
+choreinstud = "合唱リハーサル"
+choreogr = "振付師"
+chr = "振付師"
+clb = "共編者"
+cli = "クライアント"
+cll = "書道家"
+clr = "カラリスト"
+clt = "コロタイプ印刷家"
+cmm = "コメンテーター"
+cmp = "作曲家"
+cmt = "作曲家"
+cnd = "指揮者"
+cng = "撮影監督"
+cns = "検閲"
+coe = "異議申立人-上訴人"
+col = "収集家"
+com = "編纂者"
+con = "修復家"
+cor = "コレクション登録者"
+cos = "異議申立人"
+cot = "異議申立人-上訴人"
+cou = "裁判所が裁定した"
+cov = "ブックカバーデザイナー"
+cpc = "著作権の原告"
+cpe = "原告-被上訴人"
+cph = "著作権者"
+cpl = "異議申立人"
+cpt = "異議申立人-被上訴人"
+cre = "クリエーター"
+crp = "寄稿家"
+crr = "校正者"
+crt = "裁判所 記録官"
+csl = "コンサルタント"
+csp = "プロジェクトのコンサルタント"
+cst = "衣装デザイナー"
+ctb = "寄与者"
+cte = "異議申立人-上訴人"
+ctg = "製図家"
+ctr = "契約者"
+cts = "異議申立人"
+ctt = "異議申立人-上訴人"
+cur = "キュレーター"
+cwt = "書かれたテキストへのコメンテーター"
+darst = "パフォーマー"
+dbp = "配布地"
+dfd = "被告人"
+dfe = "被告人-上訴人"
+dft = "被告人-上訴人"
+dgg = "学位授与機関"
+dgs = "学位監督者"
+dir = "指揮者"
+dln = "見取り図を書く人"
+dnc = "舞踏家"
+dnr = "提供者"
+dpc = "描写された"
+dpt = "デポジター"
+drehbuch = "スクリーンライター"
+drm = "製図技師"
+drt = "デイレクター"
+dsr = "デザイナー"
+dst = "配布者"
+dte = "著書を献本された人"
+dtm = "データマネージャー"
+dto = "献呈者"
+dub = "疑わしい著者"
+edc = "編纂者"
+edm = "動画編集者"
+edt = "編集者"
+egr = "彫版工"
+elg = "電気工事士"
+elt = "電気製版工"
+eng = "エンジニア"
+enj = "法律を実行する"
+etr = "銅版画家"
+evp = "事件現場"
+exp = "専門家"
+fac = "書写を行う人"
+fds = "映画配給者"
+fld = "フイールドディレクター"
+flm = "映画編集者"
+fmd = "映画監督"
+fmk = "映画制作者"
+fmo = "旧所有者"
+fmp = "映画プロデューサー"
+fnd = "資金提供者"
+Forts = "継続"
+fotogr = "写真家"
+fpy = "当事者"
+frg = "偽造者"
+gis = "GISスペシャリスト"
+grt = "グラフィック技術者"
+hg = "出版者"
+his = "引受機関"
+hnr = "受賞者"
+Hrsg = "出版者"
+hrsg = "出版者"
+hst = "ホスト"
+Ill = "イラストレータ-"
+ill = "イラストレーター"
+ilu = "照明係"
+ins = "贈呈者"
+inszenierung = "舞台主任"
+interpr = "通訳"
+interpret = "通訳"
+interviewer = "インタビューアー"
+interviewter = "インタビューター"
+inv = "発明家"
+isb = "発行機関"
+itr = "器楽家"
+ive = "インタビューイー"
+ivr = "インタビューアー"
+jud = "裁判官"
+jug = "司法によって規定された"
+kad = "最終著者"
+kamera = "カメラ"
+kartograph = "製図家"
+komm = "評論家"
+Komment = "評論家"
+Komp = "作曲家"
+komp = "作曲家"
+Korresp = "寄稿家"
+kostüm = "衣装デザイナー"
+lbr = "実験室"
+lbt = "台本作者"
+ldr = "実験室主任"
+led = "先例"
+lee = "被告-被上訴人"
+lel = "被告"
+let = "被告-被上訴人"
+lie = "被告-被上訴人"
+lil = "被告"
+lit = "被告-被上訴人"
+lsa = "景観建築家"
+lse = "実施権者"
+lso = "ライセンサー"
+ltg = "石版工"
+lyr = "歌詞作者"
+mcp = "実演用楽譜を用意する人"
+mdc = "メタデータにコンタクトする"
+med = "中間"
+mfp = "生産地"
+mfr = "生産者"
+mitarb = "共演者"
+mod = "モデレーター"
+mon = "モニター"
+mrb = "大理石工"
+mrk = "法案編集者"
+msd = "音楽デイレクター"
+mte = "銅版画家"
+mtk = "議事録作成者"
+mus = "音楽家"
+mutmassl Verf = "推定著者"
+mutmassl VerfKomp = "推定著者 / 作曲家"
+mutmaßl hrsg = "推定出版者"
+Mutmaßl Verf = "推定著者"
+mutmaßl verf = "推定著者"
+mutmaßl übers = "推定翻訳者"
+Nachr = "あとがきと奥付の著者, 等"
+nrt = "ナレーター"
+opn = "対戦相手"
+org = "創設者"
+orm = "組織者"
+osp = "オンスクリーンプレゼンター"
+oth = "他の"
+own = "所有者"
+pan = "パネリスト"
+pat = "パトロン"
+pbd = "出版デイレクター"
+pbl = "出版者"
+pdr = "プロジェクトデイレクター"
+pfr = "校正する"
+pht = "写真家"
+plt = "地図制作者"
+pma = "認可団体"
+pmn = "製作責任者"
+pop = "地図印刷社"
+ppm = "製紙工"
+ppt = "人形遣い"
+pra = "æ ¡æ­£"
+prc = "プロセスにコンタクトする"
+prd = "制作スタッフ"
+pre = "プレゼンター"
+prf = "パーフォーマー"
+prg = "プログラマー"
+prm = "印刷会社"
+prn = "制作会社"
+pro = "プロデューサー"
+prod = "プロデューサー"
+prp = "生産地"
+prs = "生産デザイナー"
+prt = "出版者"
+prv = "プロバイダー"
+präses = "校正"
+pta = "特許申請者"
+pte = "原告-上訴人"
+ptf = "原告"
+pth = "特許所有者"
+ptt = "原告-上訴人"
+pup = "出版地面"
+rcd = "記録係"
+rce = "録音技術者"
+rcp = "受信人"
+rdd = "ラジオデイレクター"
+realisation = "実現"
+Red = "編集者"
+red = "編集者"
+regie = "ディレクター"
+ren = "レンダラー"
+reporter = "調査者"
+res = "調査者"
+resp = "被告"
+rev = "批評家"
+rpc = "ラジオ放送プロデューサー"
+rps = "レポジトリ"
+rpt = "報告者"
+rpy = "責任のある当事者"
+rse = "原告-上訴人"
+rsp = "被告"
+rst = "被告人-上訴人"
+rth = "調査チーム主任"
+rtm = "調査チームメンバー"
+sad = "科学アドバイザー"
+Sammler = "収集家"
+sammler = "収集家"
+sce = "シナリオライター"
+Schreiber = "執筆者"
+scl = "彫刻家"
+scr = "写字生"
+sds = "音響デザイナー"
+sec = "秘書"
+sgd = "舞台演出家"
+sgn = "手話通訳者"
+sht = "ホストをサポートする"
+sll = "販売者"
+sng = "歌手"
+spk = "話者"
+spn = "スポンサー"
+sprecher = "話者"
+spy = "契約当事者"
+srv = "測量士"
+std = "舞台装置デザィナー"
+stecher = "版画家"
+stg = "設定"
+stl = "ストーリーテラー"
+stm = "舞台演出家"
+stn = "標準化団体"
+str = "Stereotyper"
+tcd = "テクニカル ディレクター"
+tch = "教師"
+textverf = "著者"
+ths = "論文の指導者"
+tld = "テレビディレクター"
+tlp = "テレビプロデューサー"
+trc = "字幕作成者"
+trl = "翻訳家"
+tyd = "活字デザイナー"
+tyg = "印刷工"
+tänzer = "舞踏家"
+vac = "声優"
+vdg = "映像作家"
+Verstorb = "æ•…"
+verstorb = "æ•…"
+voc = "声楽家"
+vorl = "原稿"
+Vorr = "解説の著者[等]"
+wac = "追加コメントの著者"
+wal = "追加詩文の著者"
+wam = "追加資料の著者"
+wat = "追録の著者"
+wdc = "木こり"
+wde = "木彫家"
+widmungsempfänger = "献呈者"
+win = "解説の著者"
+wit = "目撃者"
+wpr = "序説の著者"
+wst = "付録のテキストの著者"
+zeichner = "構成者"
+zensor = "検閲"
+Übers = "翻訳者"
+übers = "翻訳者"
diff --git a/languages/CreatorRoles/nl.ini b/languages/CreatorRoles/nl.ini
index 783061ca7868c173968d199c59e572874c1f09ca..fb8b1a2d0fcc1543152b8c7111eb4725ebe775f5 100644
--- a/languages/CreatorRoles/nl.ini
+++ b/languages/CreatorRoles/nl.ini
@@ -1,6 +1,17 @@
 abr = "Afkorting"
+acp = "Artistiek Kopiist"
 act = "Acteur"
+adi = "Art director"
 adp = "Adapter"
+Adressat = "Geadresseerde"
+adressat = "Geadresseerde"
+aft = "Auteur van het nawoord, colofon, enz."
+angebl hrsg = "Twijfelachtige uitgever"
+angebl komp = "Twijfelachtige auteur / componist"
+Angebl Verf = "Twijfelachtige auteur"
+angebl verf = "Twijfelachtige auteur"
+angebl übers = "Twijfelachtige vertaler"
+animation = "Animatie"
 anl = "Analist"
 anm = "Animator"
 ann = "Annotator"
@@ -19,10 +30,19 @@ ato = "Autographer"
 att = "Toegewezen naam"
 auc = "Veilingmeester"
 aud = "Auteur van het dialoogvenster"
+Auftraggeber = "Opdrachtgever"
 aui = "Auteur van inleiding, enz."
 aus = "Scenarioschrijver"
 aut = "Auteur"
 bdd = "Boekbinder"
+Bearb = "Editor"
+bearb = "Editor"
+Begr = "Financier"
+begr = "Financier"
+Beiträger = "Medewerker"
+beiträger = "Literair Medewerker"
+beiträger k = "Artistiek Medewerker"
+beiträger m = "Muzikale Medewerker"
 bjd = "Ontwerper wikkel"
 bkd = "Boek ontwerper"
 bkp = "Boek producent"
@@ -32,8 +52,11 @@ bpd = "Ontwerper ex-libris"
 brd = "Omroeper"
 brl = "Brailleprinter"
 bsl = "Boekverkoper"
+bühnenbild = "Decorontwerper"
 cas = "Caster"
 ccp = "Conceptor"
+choreinstud = "Koor repetitie"
+choreogr = "Choreograaf"
 chr = "Choreograaf"
 clb = "Medewerker"
 cli = "Klant"
@@ -75,6 +98,7 @@ cts = "Contestee"
 ctt = "Contestee-appellant"
 cur = "Curator"
 cwt = "Commentator voor geschreven tekst"
+darst = "Artiest"
 dbp = "Plaats van distributie"
 dfd = "Beklaagde"
 dfe = "Beklaagde-appellee"
@@ -88,6 +112,7 @@ dnc = "Danser"
 dnr = "Donor"
 dpc = "Afgebeeld"
 dpt = "Bewaargever"
+drehbuch = "Scenarioschrijver"
 drm = "Tekenaar"
 drt = "Director"
 dsr = "Ontwerper"
@@ -117,17 +142,27 @@ fmk = "Filmmaker"
 fmo = "Vorige eigenaar"
 fmp = "Film producer"
 fnd = "Sponsor"
+Forts = "Voortzetting"
+fotogr = "Fotograaf"
 fpy = "Betrokken partij"
 frg = "Vervalser"
 gis = "Geografische informatie specialist"
 grt = "Grafisch technicus"
+hg = "Uitgever"
 his = "Gastinstelling"
 hnr = "Honoree"
+Hrsg = "Uitgever"
+hrsg = "Uitgever"
 hst = "Gastheer"
 Ill = "Tekenaar"
 ill = "Tekenaar"
 ilu = "Illuminator"
 ins = "Inscriber"
+inszenierung = "Regisseur"
+interpr = "Vertolker"
+interpret = "Vertolker"
+interviewer = "Interviewer"
+interviewter = "Geïnterviewde"
 inv = "Uitvinder"
 isb = "Uitgevende instelling"
 itr = "Instrumentalist"
@@ -135,6 +170,15 @@ ive = "Geïnterviewde"
 ivr = "Interviewer"
 jud = "Rechter"
 jug = "Bevoegde instantie"
+kad = "Cadentiale auteur"
+kamera = "Camera"
+kartograph = "Cartograaf"
+komm = "Commentator"
+Komment = "Commentator"
+Komp = "Componist"
+komp = "Componist"
+Korresp = "Correspondent"
+kostüm = "Kostuum ontwerper"
 lbr = "Labo"
 lbt = "Librettist"
 ldr = "Labo directeur"
@@ -157,7 +201,9 @@ mdc = "Metadata contact"
 med = "Medium"
 mfp = "Productielocatie"
 mfr = "Fabrikant"
+mitarb = "Medewerker"
 mod = "Moderator"
+moderation = "Matiging"
 mon = "Monitor"
 mrb = "Marmeerder"
 mrk = "Opmaak editor"
@@ -165,6 +211,13 @@ msd = "Muziekdirecteur"
 mte = "Metaal-graveur"
 mtk = "Notulist"
 mus = "Musicus"
+mutmassl Verf = "Vermeend auteur"
+mutmassl VerfKomp = "Vermeend auteur / componist"
+mutmaßl hrsg = "Vermeend uitgever"
+Mutmaßl Verf = "Vermeend auteur"
+mutmaßl verf = "Vermeend auteur"
+mutmaßl übers = "Vermeend vertaler"
+Nachr = "Auteur van het nawoord, colofon, enz."
 nrt = "Verteller"
 opn = "Tegenstander"
 org = "Opsteller"
@@ -194,10 +247,12 @@ prg = "Programmeur"
 prm = "Printmaker"
 prn = "Productiebedrijf"
 pro = "Producer"
+prod = "Producent"
 prp = "Productieplaats"
 prs = "Productie ontwerper"
 prt = "Printer"
 prv = "Leverancier"
+präses = "Voorzitter"
 pta = "Patent aanvrager"
 pte = "Plaintiff-appellee"
 ptf = "Plaintiff"
@@ -209,9 +264,14 @@ rcd = "Geluidsrecorder"
 rce = "Geluidstechnicus"
 rcp = "Geadresseerde"
 rdd = "Radio director"
+realisation = "Realisatie"
+Red = "Redacteur"
 red = "Redacteur"
+regie = "Regisseur"
 ren = "Renderer"
+reporter = "Verslaggever"
 res = "Onderzoeker"
+resp = "Respondent"
 rev = "Reviewer"
 rpc = "Radio producer"
 rps = "Bewaarplaats"
@@ -225,7 +285,10 @@ rst = "Respondent-appellant"
 rth = "Hoofd van onderzoeksgroep"
 rtm = "Lid onderzoeksgroep"
 sad = "Wetenschappelijk adviseur"
+Sammler = "Verzamelaar"
+sammler = "Verzamelaar"
 sce = "Scenarist"
+Schreiber = "Schrijver"
 scl = "Beeldhouwer"
 scr = "Schriftgeleerde"
 sds = "Geluidsdesigner"
@@ -237,9 +300,11 @@ sll = "Verkoper"
 sng = "Zanger"
 spk = "Spreker"
 spn = "Sponsor"
+sprecher = "Spreker"
 spy = "Tweede partij"
 srv = "Landmeter"
 std = "Decorontwerper"
+stecher = "Graveur"
 stg = "Omgeving"
 stl = "Verteller"
 stm = "Stage manager"
@@ -247,6 +312,7 @@ stn = "Normalisatie-instituut"
 str = "Stereotyper"
 tcd = "Technisch directeur"
 tch = "Lesgever"
+textverf = "Auteur"
 ths = "Thesis begeleider"
 tld = "Televisiedirecteur"
 tlp = "Televisieproducent"
@@ -254,17 +320,27 @@ trc = "Transcriber"
 trl = "Vertaler"
 tyd = "Letterontwerper"
 tyg = "Typograaf"
+tänzer = "Danser"
 uvp = "Universiteitslocatie"
 vac = "Stemacteur"
 vdg = "Videograaf"
+Verstorb = "Overleden"
+verstorb = "Overleden"
 voc = "Vocalist"
+vorl = "Ontwerp"
+Vorr = "Auteur van de introductie, enz."
 wac = "Schrijver van toegevoegde commentaar"
 wal = "Schrijver van toegevoegde lyrics"
 wam = "Schrijver van begeleidend materiaal"
 wat = "Schrijver van toegevoegde tekst"
 wdc = "Woodcutter"
 wde = "Houthakker"
+widmungsempfänger = "Opgedragen aan"
 win = "Schrijver van inleiding"
 wit = "Getuige"
 wpr = "Scrhijver van voorwoord"
 wst = "Schrijver van aanvullende tekstuele inhoud"
+zeichner = "Tekenaar"
+zensor = "Censor"
+Ãœbers = "Vertaler"
+übers = "Vertaler"
diff --git a/languages/CreatorRoles/pl.ini b/languages/CreatorRoles/pl.ini
new file mode 100644
index 0000000000000000000000000000000000000000..50a99f7312b154484cb3ffd8c2be0540d7e51f23
--- /dev/null
+++ b/languages/CreatorRoles/pl.ini
@@ -0,0 +1,346 @@
+abr = "Skrót"
+acp = "Kopista"
+act = "Aktor"
+adi = "Dyrektor artystyczny"
+adp = "Adaptator"
+Adressat = "Adresat"
+adressat = "Adresat"
+aft = "Autor posłowia, kolofonu itd."
+angebl hrsg = "Prawdopodobny wydawca"
+angebl komp = "Prawdopodobny autor / kompozytor"
+Angebl Verf = "Prawdopodobny autor"
+angebl verf = "Prawdopodobny autor"
+angebl übers = "Prawdopodobny tłumacz"
+animation = "Animacja"
+anl = "Analityk"
+anm = "Animator"
+ann = "Adnotacja"
+ant = "Bibliograficzny poprzednik"
+ape = "Pozwany"
+apl = "Apelant"
+app = "Wnioskodawca"
+aqt = "Autor cytatu lub streszczenia tekstu"
+arc = "Architekt"
+ard = "Dyrektor artystyczny"
+arr = "Aranżer"
+art = "Artysta"
+asg = "Następca prawny"
+asn = "Przynależna nazwa"
+ato = "Autograf"
+att = "Przynależna nazwa"
+auc = "Licytator"
+aud = "Autor dialogu"
+Auftraggeber = "Klient"
+aui = "Autor wstępu, przedmowy itd."
+aus = "Autor scenariusza"
+aut = "Autor"
+bdd = "Projektant oprawy"
+Bearb = "Edytor"
+bearb = "Edytor"
+Begr = "Założyciel / Twórca"
+begr = "Założyciel / Twórca"
+Beiträger = "Współpracownik"
+beiträger = "Współpracownik literacki"
+beiträger k = "Współpracownik artystyczny"
+beiträger m = "Współpracownik muzyczny"
+bjd = "Projektant obwoluty"
+bkd = "Projektant książki"
+bkp = "Producent książki"
+blw = "Autor notatki"
+bnd = "Introligator"
+bpd = "Projektant ekslibris"
+brd = "Spiker radiowy"
+brl = "Wytaczarz Braille'a"
+bsl = "Księgarz"
+bühnenbild = "Scenograf"
+cas = "Odlewacz"
+ccp = "Projektant koncepcyjny"
+choreinstud = "Próba chóru"
+choreogr = "Choreograf"
+chr = "Choreograf"
+clb = "Współpracownik"
+cli = "Klient"
+cll = "Kaligraf"
+clr = "Kolorysta"
+clt = "Światłodruk"
+cmm = "Komentator"
+cmp = "Kompozytor"
+cmt = "Zecer / składacz"
+cnd = "Dyrygent"
+cng = "Operator filmowy"
+cns = "Cenzor"
+coe = "Wniosek / Apel"
+col = "Kolekcjoner"
+com = "Kompilator"
+con = "Konserwator zabytków"
+cor = "Kurator"
+cos = "Uczestnik"
+cot = "Wnioskodawca / Kandydat"
+cou = "Ustanowione sÄ…dowo"
+cov = "Projektant okładki"
+cpc = "Należne prawa autorskie"
+cpe = "Pozwany w postępowaniu odwoławczym"
+cph = "Właściciel praw autorskich"
+cpl = "Składający zażalenie"
+cpt = "WnoszÄ…cy o rewizjÄ™, sprawdzenie"
+cre = "Twórca"
+crp = "Korespondent"
+crr = "Korektor"
+crt = "Korespondent sÄ…dowy"
+csl = "Doradca"
+csp = "Doradca projektu"
+cst = "Projektant kostiumów"
+ctb = "Współpracownik"
+cte = "Konkurs"
+ctg = "Kartograf"
+ctr = "Strona umowy"
+cts = "Kandydatura"
+ctt = "Kandydat"
+cur = "Kurator"
+cwt = "Komentator"
+darst = "Wykonawca"
+dbp = "Miejsce wydania"
+dfd = "Oskarżony"
+dfe = "Oskarżenie"
+dft = "Oskarżony"
+dgg = "Organizacja wydawająca stopień naukowy"
+dgs = "Promotor doktoranta"
+dir = "Dyrygent"
+dis = "Doktorant"
+dln = "Traser"
+dnc = "Tancerz"
+dnr = "Dawca"
+dpc = "Przedstawiony"
+dpt = "Inwestor"
+drehbuch = "Scenarzysta"
+drm = "Rysownik"
+drt = "Dyrektor"
+dsr = "Projektant"
+dst = "Dystrybutor"
+dtc = "Kontrybutor danych"
+dte = "Ofiarowany"
+dtm = "Administrator danych"
+dto = "Dedykator"
+dub = "Rzekomy, prawdopodobny autor"
+edc = "Redaktor kompilacji, zbioru"
+edm = "Montażysta"
+edt = "Redaktor"
+egr = "Grawer"
+elg = "Elektryk"
+elt = "Galwanotechnik"
+eng = "Inżynier"
+enj = "SÄ…downictwo ustawodawcza"
+etr = "Akwaforcista"
+evp = "Miejsce zdarzenia"
+exp = "Ekspert"
+fac = "Reproducent"
+fds = "Dystrybutor filmów"
+fld = "Kierownik"
+flm = "Montażysta"
+fmd = "Reżyser"
+fmk = "Producent filmowy"
+fmo = "Były właściciel"
+fmp = "Producent filmowy"
+fnd = "Inwestor"
+Forts = "Kontynuacja"
+fotogr = "Fotograf"
+fpy = "Pierwsza strona umowy"
+frg = "Fałszerz"
+gis = "Specjalista informacji geograficznej"
+grt = "Technik graficzny"
+hg = "Wydawca"
+his = "Organizator"
+hnr = "Uhonorowany"
+Hrsg = "Wydawca"
+hrsg = "Wydawca"
+hst = "Organizator"
+Ill = "Ilustrator"
+ill = "Ilustrator"
+ilu = "Iluminator (malarstwo książkowe)"
+ins = "Ofiarodawca"
+inszenierung = "Inscenizacja"
+interpr = "TÅ‚umacz"
+interpret = "TÅ‚umacz"
+interviewer = "Dziennikarz"
+interviewter = "Rozmówca"
+inv = "Wynalazca"
+isb = "Organ wydajÄ…cy"
+itr = "Instrumentalista"
+ive = "Rozmówca"
+ivr = "Dziennikarz"
+jud = "Sędzia"
+jug = "Główne sądownictwo"
+kad = "Autor kadencji"
+kamera = "Kamera"
+kartograph = "Kartograf"
+komm = "Komentator"
+Komment = "Komentator"
+Komp = "Kompozytor"
+komp = "Kompozytor"
+Korresp = "Korespondent"
+kostüm = "Projektant kostiumów"
+lbr = "Laboratorium"
+lbt = "Librecista, autor libretta"
+ldr = "Dyrektor laboratorium"
+led = "Kierownictwo"
+lee = "Odwołanie"
+lel = "Odwołanie"
+len = "Pożyczający, kredytobiorca"
+let = "Składający wniosek o odwołanie"
+lgd = "Projektant oświetlenia"
+lie = "Składający wniosek o odwołanie"
+lil = "Składający wniosek o odwołanie"
+lit = "Składający wniosek o odwołanie"
+lsa = "Ruralista"
+lse = "Licencjobiorca"
+lso = "Licencjodawca"
+ltg = "Litograf"
+lyr = "Tekściarz"
+mcp = "Reproducent muzyki"
+mdc = "Kontakt do metadanych"
+med = "Nośnik"
+mfp = "Miejsce produkcji"
+mfr = "Producent"
+mitarb = "Współpracownik"
+mod = "Moderator"
+moderation = "Moderacja"
+mon = "Obserwator"
+mrb = "Kamieniarz marmurowy"
+mrk = "Edytor języka znaczników"
+msd = "Dyrektor muzyczny"
+mte = "Grawer do metalu"
+mtk = "Protokolant"
+mus = "Muzyk"
+mutmassl Verf = "Domniemany autor"
+mutmassl VerfKomp = "Domniemany autor / kompozytor"
+mutmaßl hrsg = "Domniemany wydawca"
+Mutmaßl Verf = "Domniemany autor"
+mutmaßl verf = "Domniemany autor"
+mutmaßl übers = "Domniemany tłumacz"
+Nachr = "Autor posłowia, kolofonu itd."
+nrt = "Narrator"
+opn = "Przeciwnik"
+org = "Twórca"
+orm = "Organizator"
+osp = "Prezenter"
+oth = "Inni"
+own = "Właściciel"
+pan = "Panelista"
+pat = "Patron"
+pbd = "Dyrektor wydawnictwa"
+pbl = "Wydawca"
+pdr = "Dyrektor projektu"
+pfr = "Korektor"
+pht = "Fotograf"
+plt = "Fleksograf"
+pma = "Agencja zezwalajÄ…ca"
+pmn = "Kierownik produkcji"
+pop = "Producent płyt drukarskich"
+ppm = "Czerpacz"
+ppt = "Lalkarz"
+pra = "prezes, Vorstand"
+prc = "process contact" (Kontakt z procesem) ------------------------------> What does it mean? ciÄ…g, kontynuacja kontaktu"
+prd = "Pracownicy produkcji"
+pre = "Prezenter"
+prf = "Wykonawca"
+prg = "Programista"
+prm = "Grafik"
+prn = "Firma produkcyjna"
+pro = "Producent"
+prod = "Producent"
+prp = "Miejsce produkcji"
+prs = "Projektant produkcji"
+prt = "Drukarz"
+prv = "Dostawca"
+präses = "Preses" s.o. prezes"
+pta = "Wnioskodawca patentowy"
+pte = "Plaintiff-appellee"   ------------------------------> What does it mean? powód *osoba"
+ptf = "Powód"
+pth = "Właściciel patentu"
+ptt = "Plaintiff-appellant"   ------------------------------> s.o."
+pup = "Miejsce publikacji"
+rbr = "Rubrykator"
+rcd = "Producent nagrań"
+rce = "Inżynier dźwięku"
+rcp = "Adresat"
+rdd = "Dyrektor radia, radiofonii"
+realisation = "Realizacja"
+Red = "Redaktor"
+red = "Redaktor"
+regie = "Dyrektor"
+ren = "Grafik trójwymiarów"
+reporter = "Reporter"
+res = "Badacz"
+resp = "Pozwany"
+rev = "Recenzent"
+rpc = "Producent radiofoniczny"
+rps = "Repozytorium"
+rpt = "Reporter"
+rpy = "Strona odpowiedzialna"
+rse = "Respondent-appellee"   ------------------------------> s.o."
+rsg = "Reżyser wznowienia sztuki teatralnej"
+rsp = "Oskarżony"
+rsr = "Restaurator"
+rst = "Respondent-appellant"   ------------------------------> s.o."
+rth = "Kierownik zespołu badaczy"
+rtm = "Członek zespołu badaczy"
+sad = "Doradca naukowy"
+Sammler = "Kolekcjoner"
+sammler = "Kolekcjoner"
+sce = "Scenarzysta"
+Schreiber = "Pisarz"
+scl = "Rzeźbiarz"
+scr = "Autor"
+sds = "Projektant dźwięku"
+sec = "Sekretarz"
+sgd = "Reżyser"
+sgn = "Sygnatariusz"
+sht = "WspierajÄ…cy organizator"
+sll = "Sprzedawca"
+sng = "Åšpiewak"
+spk = "Mówca"
+spn = "Sponsor"
+sprecher = "Mówca"
+spy = "Druga strona umowy"
+srv = "Rzeczoznawca"
+std = "Scenograf"
+stecher = "Grawer"
+stg = "Sceneria"
+stl = "Nowelista"
+stm = "Inscenizator"
+stn = "Organizacja normalizacyjna"
+str = "Technolog odlewni"
+tcd = "Techniczny dyrektor"
+tch = "Nauczyciel"
+textverf = "Autor"
+ths = "Promotor doktoranta"
+tld = "Dyrektor telewizyjny"
+tlp = "Producent telewizyjny"
+trc = "Transkryptor"
+trl = "TÅ‚umacz"
+tyd = "Projektant czcionki"
+tyg = "Typograf"
+tänzer = "Tancerz"
+uvp = "Miejsce uniwersytetu"
+vac = "Aktor podkładający głos" > lektor"
+vdg = "Kamerzysta"
+Verstorb = "Zmarły"
+verstorb = "Zmarły"
+voc = "Wokalista"
+vorl = "Wzór"
+Vorr = "Autor wstępu itd."
+wac = "Autor dodatkowego komentarza"
+wal = "Autor dodatkowych słów piosenki"
+wam = "Autor dołączonych materiałów"
+wat = "Autor dodatkowego tekstu"
+wdc = "Drwal"
+wde = "Grawer drewna"
+widmungsempfänger = "Ofiarowany, odbiorca dedykacji"
+win = "Autor wstępu"
+wit = "Åšwiadek"
+wpr = "Autor wstępu"
+wst = "Autor uzupełniającego tekstu"
+zeichner = "Kreślarz"
+zensor = "Cenzor"
+Ãœbers = "TÅ‚umacz"
+übers = "Tłumacz"
diff --git a/languages/CreatorRoles/sv.ini b/languages/CreatorRoles/sv.ini
index c825fcab2734e36802d525c0a959cea449ef7f1a..357a6d5eac816cfcdcb82a1c29b25d1e8dd82037 100644
--- a/languages/CreatorRoles/sv.ini
+++ b/languages/CreatorRoles/sv.ini
@@ -20,6 +20,7 @@ cnd = "Dirigent"
 cng = "Filmfotograf"
 com = "Sammanställare"
 cph = "Upphovsrättsinnehavare"
+cst = "Kostymdesigner"
 ctb = "Bidragsgivare"
 ctg = "Kartograf"
 cwt = "Författare till textkommentar"
@@ -31,7 +32,9 @@ dst = "Distributör"
 dub = "Författare (tvivelaktig)"
 edt = "Utgivare, redaktör, sammanställare"
 egr = "Gravör"
+exp = "Expert"
 fac = "Framställare av faksimil"
+fds = "Distributör"
 flm = "Utgivare av film"
 fmp = "Producent"
 fnd = "Finansiär"
@@ -41,6 +44,7 @@ ill = "Illustratör"
 ive = "Intervjuobjekt"
 ivr = "Intervjuare"
 lbr = "Laboratorie"
+lgd = "Ljusdesigner"
 lyr = "Författare till sångtext"
 mdc = "Metadataansvarig"
 mus = "Musiker"
@@ -50,14 +54,17 @@ own = "Ägare"
 pbd = "Chefredaktör, huvudredaktör"
 pbl = "Utgivare"
 pht = "Fotograf"
+pmn = "Produktionschef"
 prd = "Produktionspersonal"
 prf = "Framförande person, spelare, musiker, etc."
 prn = "Produktionsbolag"
 pro = "Filmproducent"
+prs = "Produktionsdesigner"
 prt = "Tryckare"
 rce = "Inspelningstekniker"
 rev = "Recensent"
 rpy = "Ansvarig utgivare"
+sds = "Ljuddesigner"
 sng = "SÃ¥ngare"
 spk = "Röst, tal"
 std = "Scenograf"
@@ -65,5 +72,6 @@ tch = "Lärare"
 trl = "Översättare"
 voc = "Vokalist"
 wam = "Författare till material som medföljer AV-media"
+wst = "Författare till kompletterande material"
 Übers = "Översättare"
 übers = "Översättare"
diff --git a/languages/HoldingStatus/el.ini b/languages/HoldingStatus/el.ini
new file mode 100644
index 0000000000000000000000000000000000000000..39b5771dbb501ca93a51bac37ab6036a2e2ceb60
--- /dev/null
+++ b/languages/HoldingStatus/el.ini
@@ -0,0 +1,4 @@
+service_available_presentation = "Χρήση εντός της Βιβλιοθήκης αποκλειστικά"
+service_loan = "Δανεισμό"
+service_presentation = "Χρήση εντός της Βιβλιοθήκης"
+services_available_html = "Διαθέσιμο για %%list%%"
diff --git a/languages/HoldingStatus/en-gb.ini b/languages/HoldingStatus/en-gb.ini
new file mode 100644
index 0000000000000000000000000000000000000000..39818546856f2a1d501e0cb76fdd0973d832b88e
--- /dev/null
+++ b/languages/HoldingStatus/en-gb.ini
@@ -0,0 +1 @@
+@parent_ini = "HoldingStatus/en.ini"
diff --git a/languages/HoldingStatus/eu.ini b/languages/HoldingStatus/eu.ini
new file mode 100644
index 0000000000000000000000000000000000000000..419fc32efbd063fb8c913482e6578eb55a9840cb
--- /dev/null
+++ b/languages/HoldingStatus/eu.ini
@@ -0,0 +1,4 @@
+service_available_presentation = "Erabilgarri bakarrik liburutegian"
+service_loan = "Mailegua"
+service_presentation = "Liburutegian erabilita"
+services_available_html = "Erabilgarri %%list%%-etarako"
diff --git a/languages/HoldingStatus/fr.ini b/languages/HoldingStatus/fr.ini
new file mode 100644
index 0000000000000000000000000000000000000000..83d8975e0828d0fd3a9b91308b3856494409149b
--- /dev/null
+++ b/languages/HoldingStatus/fr.ini
@@ -0,0 +1,4 @@
+service_available_presentation = "Utilisation dans la bibliothèque seulement"
+service_loan = "En prêt"
+service_presentation = "Consultation sur place"
+services_available_html = "Disponible pour %%list%%"
diff --git a/languages/HoldingStatus/ja.ini b/languages/HoldingStatus/ja.ini
new file mode 100644
index 0000000000000000000000000000000000000000..620f96c32a535369432f5f70e45e3ad58bf417f2
--- /dev/null
+++ b/languages/HoldingStatus/ja.ini
@@ -0,0 +1,4 @@
+service_available_presentation = "図書館専用のみ"
+service_loan = "貸出"
+service_presentation = "図書館専用"
+services_available_html = "使用可能 %%list%%"
diff --git a/languages/HoldingStatus/pl.ini b/languages/HoldingStatus/pl.ini
new file mode 100644
index 0000000000000000000000000000000000000000..9f1e90cb78c22761afdb33cbb97614e35bd58a7f
--- /dev/null
+++ b/languages/HoldingStatus/pl.ini
@@ -0,0 +1,4 @@
+service_available_presentation = "Wyłącznie do użytku w bibliotece"
+service_loan = "Egzemplarz"
+service_presentation = "Do użytku w bibliotece"
+services_available_html = "Dostępne dla %%list%%"
diff --git a/languages/ar.ini b/languages/ar.ini
index dfee4257035d8ed4c372cbd50c73d8f4eae6fc80..a34366ca8c82cbdd822560d6a45e8c73cb15566b 100644
--- a/languages/ar.ini
+++ b/languages/ar.ini
@@ -4,6 +4,7 @@
 Abstract = "مستخلص"
 Access = "وصول"
 Access URL = "URL الوصول"
+access_denied = "تم رفض الوصول."
 Accession Number = "رقم الأكسشن"
 Account = "حساب"
 Add = "إضافة"
@@ -60,8 +61,10 @@ An error occurred during execution; please try again later. = "لقد حدث خ
 AND = "Ùˆ"
 anonymous_tags = "وسوم مجهولة"
 APA Citation = "APA استشهاد"
+applied_filter = "مرشح مطبّق"
 Article = "مقال"
 Ask a Librarian = "إسأل أخصائي مكتبات"
+Associated country = "بلد مرتبط"
 Audience = "جمهور"
 Audio = "سمعي"
 authentication_error_admin = "لا يمكننا تسجيل دخولك في الوقت الحالي. يرجى الاتصال بمدير نظامك للمساعدة."
@@ -76,6 +79,7 @@ Author Browse = "استعراض المؤلف"
 Author Notes = "ملاحظات المؤلف"
 Author Results for = "نتائج البحث عن المؤلف"
 Author Search Results = "نتائج بحث المؤلف"
+Authority File = "ملف استناد"
 Authors = "المؤلفون"
 Authors Related to Your Search = "المؤلفون المتعلقون ببحثك"
 Auto configuration is currently disabled = "تم إيقاف التهيئة التلقائية حاليا"
@@ -239,6 +243,8 @@ Create New Password = "إنشاء كلمة مرور جديدة"
 Created = "تم إنشاؤه"
 Database = "قاعدة البيانات"
 Date = "التاريخ"
+Date of birth = "تاريخ الميلاد"
+Date of death = "تاريخ الوفاه"
 date_day_placeholder = "اليوم"
 date_from = "من"
 date_month_placeholder = "الشهر"
@@ -255,6 +261,7 @@ delete_list = "حذف القائمة"
 delete_page = "حذف الصفحة"
 delete_selected = "حذف المحدد"
 delete_selected_favorites = "حذف المفضلات المحددة"
+delete_tag = "وسم الحذف"
 delete_tags = "حذف الوسوم"
 delete_tags_by = "حذف الوسوم بواسطة"
 Department = "القسم"
@@ -266,6 +273,7 @@ Displaying the top = "عرض الأعلى"
 Document Inspector = "فاحص الوثائق"
 Document Type = "نوع الوثيقة"
 DOI = "DOI"
+Draw Search Box = "أرسم صندوق البحث"
 Due = "مستحق"
 Due Date = "تاريخ الاستحقاق"
 DVD = "قرص فيديو رقمي"
@@ -356,6 +364,7 @@ Favorites = "المفضلة"
 Fee = "رسم"
 Feedback = "تغذية راجعة"
 feedback_name = "الاسم"
+Field of activity = "حقل النشاط"
 File Description = "وصف الملف"
 Filter = "المرشح"
 filter_tags = "ترشيح الوسوم"
@@ -379,7 +388,9 @@ From = "من"
 Full description = "وصف كامل"
 Full text is not displayed to guests = "لا يتم عرض النص الكامل للضيوف."
 fulltext_limit = "قصر على المقالات ذات النص الكامل متاح"
+Gender = "الجنس"
 Genre = "نوع المادة"
+Geographic Search = "بحث جغرافي"
 Geographic Terms = "مصطلحات جغرافية"
 Geography = "المنطقة"
 Get full text = "احصل على النص الكامل"
@@ -679,6 +690,7 @@ Number = "رقم"
 number_decimal_point = "."
 number_thousands_separator = ","
 OAI Server = "OAI خادم"
+Occupation = "الوظيفة"
 of = "من"
 old_password = "كلمة المرور القديمة"
 On Reserve = "في الحجز"
@@ -692,6 +704,7 @@ operator_exact = "هو (بالضبط)"
 OR = "أو"
 or create a new list = "أو قم بإنشاء قائمة جديدة"
 original = "أصلي"
+Other associated place = "مكان آخر مرتبط"
 Other Authors = "مؤلفون آخرون"
 Other Editions = "طبعات أخرى"
 Other Libraries = "مكتبات أخرى"
@@ -704,6 +717,8 @@ password_error_invalid = "كلمة المرور الجديدة غير صحيحة
 password_error_not_unique = "لم يتم تغيير كلمة المرور"
 password_maximum_length = "الحد الأقصى لطول كلمة المرور هو %%maxlength%% أحرف"
 password_minimum_length = "الحد الأدنى لطول كلمة المرور هو%%minlength%% أحرف"
+password_only_alphanumeric = "أرقام وحروف من A-Z فقط"
+password_only_numeric = "أرقام فقط"
 Passwords do not match = "كلمة المرور غير متطابقة"
 Past = "Past"
 PDF Full Text = "PDF النص الكامل"
@@ -715,6 +730,8 @@ Physical Description = "وصف مادي"
 Physical Object = "كيان مادي"
 pick_up_location = "موقع الالتقاط"
 Place a Hold = "أحجز النسخة"
+Place of birth = "مكان الميلاد"
+Place of death = "مكان الوفاه"
 Playing Time = "وقت التشغيل"
 Please check back soon = "يرجى التحقق مرة أخرى قريبا"
 Please contact the Library Reference Department for assistance = "يرجى الاتصال بقسم مرجعية المكتبة للمساعدة"
@@ -761,6 +778,7 @@ Recall This = "إستدعي هذه النسخة"
 recaptcha_not_passed = "CAPTCHA لم تمر"
 Record Citations = "استشهادات التسجيلة"
 Record Count = "عد التسجيلة"
+Record Type = "نوع التسجيلة"
 Recover Account = "استعادة الحساب"
 recovery_by_email = "استعادة بواسطة البريد الالكتروني"
 recovery_by_username = "استعادة بواسطة اسم المستخدم"
@@ -805,6 +823,7 @@ Requests = "طلبات"
 Reserves = "الحجز الأكاديمي"
 Reserves Search = "لحث الحجز الأكاديمي"
 Reserves Search Results = "نتائج البحث في الحجز الأكاديمي"
+result_count = "نتيجة %%count%%"
 Results = "نتائج"
 results = "نتائج"
 Results for = "نتائج لـ"
@@ -856,6 +875,8 @@ Serial = "دورية"
 Series = "سلاسل"
 Set = "ضبط"
 Showing = "يعرض"
+sidebar_close = "أقفل الشريط الجانبي"
+sidebar_expand = "أفتح الشريط الجانبي"
 Similar Items = "مواد مشابهة"
 Skip to content = "تخطي إلى المحتوى"
 skip_confirm = "هل أنت متأكد من أنك تريد تخطي هذه الخطوة؟"
@@ -869,10 +890,12 @@ sms_success = "تم إرسال الرسالة."
 Software = "برمجيات"
 Sorry, but the help you requested is unavailable in your language. = "عذراً، لكن المساعدة التي قمت بطلبها غير متوفرة بلغتك."
 Sort = "فرز بـ"
+sort_alphabetic = "أبجديا"
 sort_author = "المؤلف"
 sort_author_author = "أبجديا"
 sort_author_relevance = "الشعبية"
 sort_callnumber = "رقم الطلب"
+sort_count = "عدد النتائج"
 sort_relevance = "الصلة"
 sort_title = "العنوان"
 sort_year = "التاريخ تنازليا"
@@ -1017,9 +1040,6 @@ View this record in EBSCOhost = "عرض هذه التسجيلة في EBSCOhost"
 visual_facet_parent = "من"
 Volume = "المجلد"
 Volume Holdings = "مقتنيات المجلد"
-vudl_access_denied = "تم رفض الوصول."
-vudl_tab_docs = "وثائق"
-vudl_tab_pages = "الصفحات"
 VuFind Configuration = "VuFind تهيئة"
 vufind_upgrade_fail = "لا يمكننا ترقية VuFind في الوقت الحالي"
 Warning: These citations may not always be 100% accurate = "تحذير: قد لا تكون هذه الاستشهادات دائما دقيقة بنسبة 100%"
diff --git a/languages/bn.ini b/languages/bn.ini
index 5811c885768a0fbdff464eb3ea85f4278634dd2d..38c56a834286bb503071ef118ed6d5f6ad323d54 100644
--- a/languages/bn.ini
+++ b/languages/bn.ini
@@ -1,8 +1,11 @@
+; -- Note: This file represents Bengali as spoken in India; we may eventually need a separate bn-bd.ini for Bangladesh --
+; -- Translations courtesy of Dr. Parthasarathi Mukhopadhyay and Mr. Debabrata Barman
 ; For future reference:
 ;Bengali = বাংলা
 Abstract = "সার সংক্ষেপ"
 Access = "প্রবেশাধিকার"
 Access URL = "URL ব্যবহার করুন"
+access_denied = "পরিষেবা বন্ধ।"
 Accession Number = "পরিগ্রহন সংখ্যা"
 Account = "অ্যাকাউন্ট"
 Add = "যুক্ত করুন"
@@ -59,8 +62,10 @@ An error occurred during execution; please try again later. = "চালান
 AND = "এবং"
 anonymous_tags = "অনামা ট্যাগ "
 APA Citation = "APA সাইটেশন"
+applied_filter = "ব্যবহারিক ফিল্টার"
 Article = "প্রবন্ধ"
 Ask a Librarian = "গ্রন্থাগারিককে জিজ্ঞাসা করুন"
+Associated country = "সম্পর্কিত দেশ"
 Audience = "ব্যবহারকারী"
 Audio = "অডিও"
 authentication_error_admin = "এই সময়ে আপনার লগ ইন অনুমোদন নেই । সহায়তার জন্য আপনার সিস্টেম প্রশাসকের সঙ্গে যোগাযোগ করুন ।"
@@ -75,6 +80,7 @@ Author Browse = "লেখক আনুযায়ী ব্রাউজ করু
 Author Notes = "লেখক সম্পর্কিত টীকা"
 Author Results for = "অনুসন্ধিত লেখক"
 Author Search Results = "লেখক অনুসন্ধানের ফলাফল"
+Authority File = "প্রাধিকার ফাইল"
 Authors = "লেখক"
 Authors Related to Your Search = "আপনার অনুসন্ধান করা লেখক"
 Auto configuration is currently disabled = "অটো কনফিগারেশন বর্তমানে নিষ্ক্রিয় করা আছে"
@@ -95,6 +101,7 @@ Be the first to leave a comment = "প্রথমজন হিসাবে ম
 Be the first to tag this record = "প্রথমজন হিসাবে ট্যাগ করুন"
 Bibliographic Details = "গ্রন্থ-পঞ্জীর বিবরন"
 Bibliography = "গ্রন্থ-পঞ্জী"
+Blu-ray Disc = "ব্লু-রে ডিস্ক"
 Book = "গ্রন্থ"
 Book Bag = "গ্রন্থ সম্ভার"
 Book Chapter = "গ্রন্থের অধ্যায়"
@@ -225,6 +232,7 @@ Copies = "প্রতিলিপিগুলি"
 Copy = "প্রতিলিপি"
 Copyright = "গ্রহস্বত্ব"
 Corporate Author = "সংস্থা লেখক"
+Corporate Authors = "সংস্থা লেখক"
 Country = "দেশ"
 Course = "পাঠ্যক্রম"
 Course Reserves = "পাঠ্যক্রম আরক্ষিত"
@@ -236,6 +244,8 @@ Create New Password = "নতুন পাসওয়ার্ড তৈরী ক
 Created = "তৈরী হয়েছে"
 Database = "উপাত্তকোষ"
 Date = "তারিখ"
+Date of birth = "জন্মতারিখ"
+Date of death = "মৃত্যুর তারিখ"
 date_day_placeholder = "দিন"
 date_from = "থেকে"
 date_month_placeholder = "মাস"
@@ -252,6 +262,7 @@ delete_list = "তালিকাটি মুছুন"
 delete_page = "পৃষ্ঠাটি মুছুন"
 delete_selected = "নির্বাচিতগুলি মুছুন"
 delete_selected_favorites = "পছন্দের তালিকাগুলি মুছুন"
+delete_tag = "ট্যাগ মুছুন"
 delete_tags = "ট্যাগগুলি মুছুন"
 delete_tags_by = "ট্যাগ  অনুযায়ী মুছুন"
 Department = "বিভাগ"
@@ -263,6 +274,7 @@ Displaying the top = "শীর্ষে প্রদর্শিত"
 Document Inspector = "প্রলেখ পরিদর্শক"
 Document Type = "প্রলেখের ধরণ"
 DOI = "ডিওআই"
+Draw Search Box = "অনুসন্ধান ক্ষেত্র সংযোজন করুন"
 Due = "বাকি"
 Due Date = "তারিখ বাকি আছে"
 DVD = "ডিভিডি"
@@ -353,6 +365,7 @@ Favorites = "উপাদানটি সংরক্ষন করা হয়ে
 Fee = "খরচ"
 Feedback = "প্রতিক্রিয়া পাঠান"
 feedback_name = "নাম"
+Field of activity = "বিষয় দক্ষতা"
 File Description = "ফাইলের বিবরণ"
 Filter = "ফিল্টার"
 filter_tags = "ফিল্টার ট্যাগ করুন"
@@ -376,7 +389,9 @@ From = "থেকে"
 Full description = "সম্পূর্ণ বিবরণ"
 Full text is not displayed to guests = "সম্পূর্ণ পাঠ অতিথি ব্যবহারকারীকে প্রদর্শন করা হয় না।"
 fulltext_limit = "কিছু নির্দ্দিস্ট প্রবন্ধের সম্পূর্ণ পাঠ পাওয়া যাবে"
+Gender = "লিঙ্গ"
 Genre = "প্রলেখ প্রকারভেদ"
+Geographic Search = "ভৌগলিক অনুসন্ধান"
 Geographic Terms = "ভৌগলিক শব্দাবলী"
 Geography = "ভূগল"
 Get full text = "সম্পূর্ণ পাঠ পাওয়ার জন্য"
@@ -384,6 +399,7 @@ Get RSS Feed = "আরএসএস প্রতিক্রিয়া পাও
 Globe = "গোলাকার"
 Go = "যান"
 Go to Standard View = "প্রমাণক দেখতে যান"
+go_to_list = "তালিকা দেখুন"
 google_map_cluster = "গুচ্ছ"
 google_map_cluster_points = "আলোচ্য বিষয় গুচ্ছ"
 Grid = "গ্রিড"
@@ -441,6 +457,7 @@ hold_success = "আপনার আনুরোধটি সফল হয়েছ
 Holdings = "হোল্ডিংস"
 Holdings at Other Libraries = "অন্যান্য গ্রন্থাগারে হোল্ডিংসগুলি"
 Holdings details from = "হোল্ডিংসের বিবরণ"
+Holdings_notes = "হোল্ডিং টীকা"
 Holds = "হোল্ড করুন"
 Holds and Recalls = "তলব তথ্য এবং হোল্ড গুলি"
 Home = "প্রথম পাতা"
@@ -469,11 +486,13 @@ ill_request_pick_up_library = "গ্রন্থাগার পিকাপ 
 ill_request_pick_up_location = "পিকাপের স্থান"
 ill_request_place_fail_missing = "আপনার আনুরোধটি ব্যর্থ। কিছু ডাটা পাওয়া যাচ্ছে না। আরও সহায়তার জন্য সঞ্চারন ডেস্কে যোগাযোগ করুন"
 ill_request_place_success = "আপনার আনুরোধটি সম্পূর্ন"
+ill_request_place_success_html = "আপনার আনুরোধটি সম্পূর্ন হয়েছে। <a href="%%url%%">আন্তঃগ্রন্থাগার ঋণ অনুরোধ</a>."
 ill_request_place_text = "অনুগ্রহ করে একটি আন্তঃগ্রন্থাগার ঋণ উপস্থাপন করুন"
 ill_request_processed = "সম্পূর্নটি প্রক্রিয়া"
 ill_request_profile_html = "আন্তঃগ্রন্থাগার ঋণ অনুরোধের জন্য তথ্য , অনুগ্রহ করে ঠিক করুন <a href="%%url%%">Library Catalog Profile</a>."
 ill_request_submit_text = "অনুরোধ করুন"
 Illustrated = "ছবিযুক্ত"
+ils_connection_failed = "অটোমেশন সফটওয়্যারের সঙ্গে সম্পর্ক ছিন্ন হয়েছে। আপনার অ্যাকাউন্ট সম্পর্কিত তথ্য উপলব্ধ নয়। সমাধানের জন্য আপনার গ্রন্থাগারে যোগাযোগ করুন।"
 ils_offline_holdings_message = "হোল্ডিংস এবং উপাদানটির প্রাপ্যতা বর্তমানে অনুপলব্ধ। আপনার সবরকম অসুবিধার জন্য আমরা দুঃখিত এবং অন্যান্য সহায়তার জন্য আমাদের সাথে যোগাযোগ করুনঃ"
 ils_offline_home_message = "আপনার অ্যাকাউন্টের বিশদ বিবরণ এবং জীবন্ত উপাদানটির তথ্য এই সময়ে অনুপলব্ধ হতে পারে। আপনার সবরকম অসুবিধার জন্য আমরা দুঃখিত এবং অন্যান্য সহায়তার জন্য আমাদের সাথে যোগাযোগ করুনঃ"
 ils_offline_login_message = "আপনার অ্যাকাউন্টের বিশদ বিবরণ এই সময়ে অনুপলব্ধ হতে পারে। আপনার সবরকম অসুবিধার জন্য আমরা দুঃখিত এবং অন্যান্য সহায়তার জন্য আমাদের সাথে যোগাযোগ করুনঃ"
@@ -503,6 +522,7 @@ ISBN/ISSN = "আইসবিএন/আইএসএসএন"
 ISSN = "আইএসএসএন"
 Issue = "প্রদান করা"
 Item Description = "উপাদানের বিবরণ"
+Item Notes = "উপাদান টীকা"
 Item removed from favorites = "পছন্দের তালিকা থেকে উপাদানটি বাদ দিন"
 Item removed from list = "তালিকা থেকে উপাদানটি বাদ দিন"
 Items = "উপাদানগুলি"
@@ -564,6 +584,7 @@ login_disabled = "লগইন এই সময়ে অসম্ভব"
 login_target = "গ্রন্থাগার"
 Logout = "লগআউট করুন"
 Main Author = "প্রধান লেখক"
+Main Authors = "প্রধান লেখক"
 Major Categories = "মুখ্য শ্রেণী"
 Manage Tags = "ট্যাগ নিয়ন্ত্রণ করুন"
 Manuscript = "পাণ্ডুলিপি"
@@ -586,6 +607,7 @@ More catalog results = "আরও গ্রন্থতালিকার ফল
 More options = "আরও অপশন"
 More Summon results = "আরও Summon ফলাফল"
 More Topics = "আরও বিষয় দেখুন"
+more_authors_abbrev = "অন্যান্য"
 more_info_toggle = "প্রদর্শন করুন/আরও তথ্য আড়াল করুন"
 more_topics = "%%count%% more topics"
 Most Recent Received Issues = "সম্প্রতিককালে গৃহীত সংখ্যাটি"
@@ -619,11 +641,14 @@ No reviews were found for this record = "এই নথির জন্য কো
 No Tags = "কোনো ট্যাগ নেই"
 no_description = "বর্ণনা উপলবদ্ধ নয়।"
 no_items_selected = "কোন উপাদান নির্বাচিত হয় নি"
+nohit_active_filters = "এক বা একাধিক ফিল্টার এই অনুসন্ধানটির সঙ্গে যুক্ত আছে। প্রলেখ সংখ্যা বেশি পেতে ফিল্টার অপসারণ করুন।"
+nohit_change_tab = "আপনার অনুসন্ধানটি "%%activeTab%%" ট্যাবে সীমিত আছে। প্রলেখ সংখ্যা বেশি পেতে অন্যান্য ট্যাব ব্যবহার করুন।"
 nohit_filters = "এই অনুসন্ধানের জন্য ফিল্টার প্রয়োগ করুন:"
 nohit_heading = "ফলাফল নেই!"
 nohit_no_filters = "কোন ফিল্টার এই অনুসন্ধানে প্রয়োগ করা হয় নি"
 nohit_parse_error = "আপনার অনুসন্ধানের প্রশ্নটি একটি সমস্যা করবে বলে মনে হয়। বাক্য-গঠন চেক করুন। যদি আপনি বিস্তৃত বৈশিষ্ট্য প্রয়োগ করার চেষ্টা না করেন, তাহলে উদ্ধারচিহ্নের ভিতরে প্রশ্নটি করলে সাহায্য পেতে পারেন"
 nohit_prefix = "অনুসন্ধানের সাথে সম্পর্ক যুক্ত "
+nohit_query_without_filters = "এই অনুসন্ধান থেকে সমস্ত ফিল্টার গুলি মুছুন।"
 nohit_spelling = "সম্ভবত আপনার কিছু বানান বৈচিত্র প্রয়োগ চেষ্টা করা উচিত"
 nohit_suffix = "কোনো প্রলেখ নেই।"
 nohit_suggest = "আপনি কিছু শব্দ মুছে ফেলে আপনার  অনুসন্ধান ফ্রেজ পরিমার্জন করুন বা আপনার বানান পরীক্ষা করার চেষ্টা করতে পারেন."
@@ -660,11 +685,13 @@ note_785_5 = "অংশ আনুযায়ী শোষিত"
 note_785_6 = "বিভক্ত করুন"
 note_785_7 = "একীভূত / স্বরূপ"
 note_785_8 = "পরিবর্তিত"
+note_787 = "অন্যান্য সম্পর্ক সমূহ"
 Notes = "টীকা"
 Number = "সংখ্যা"
 number_decimal_point = "."
 number_thousands_separator = ","
 OAI Server = "ওএআই সার্ভার"
+Occupation = "পেশা"
 of = "এর"
 old_password = "পুরোনো পাসওয়ার্ড"
 On Reserve = "আরক্ষিত"
@@ -678,6 +705,7 @@ operator_exact = "রয়েছে (একদম ঠিক)"
 OR = "অথবা"
 or create a new list = "অথবা নতুন তালিকা তৈরী করুন"
 original = "আসল"
+Other associated place = "অন্যান্য সম্পর্কিত স্থান"
 Other Authors = "অন্যান্য লেখক"
 Other Editions = "অন্যান্য সংস্করন"
 Other Libraries = "অন্যান্য গ্রন্থাগার"
@@ -690,6 +718,8 @@ password_error_invalid = "নতুন পাসওয়ার্ডটি অ
 password_error_not_unique = "পাসওয়ার্ড পরিবর্তন করা হয়নি"
 password_maximum_length = "Maximum password length is %%maxlength%% characters"
 password_minimum_length = "Minimum password length is %%minlength%% characters"
+password_only_alphanumeric = "সংখ্যা এবং বর্ণ (A-Z)"
+password_only_numeric = "শুধুমাত্র সংখ্যা"
 Passwords do not match = "পাসওয়ার্ড মিলছে না"
 Past = "অতীত"
 PDF Full Text = "পিডিএফ এ সম্পূর্ন পাঠ"
@@ -701,6 +731,8 @@ Physical Description = "দৈহিক বর্ননা"
 Physical Object = "দৈহিক বিষয়"
 pick_up_location = "পিকাপের স্থান"
 Place a Hold = "এই স্থানে হোল্ড করুন"
+Place of birth = "জন্মস্থান"
+Place of death = "মৃত্যুর স্থান"
 Playing Time = "খেলার সময়"
 Please check back soon = "শীঘ্রই আবার চেক করুন"
 Please contact the Library Reference Department for assistance = "সহায়তার জন্য গ্রন্থাগার রেফারেন্স বিভাগের সাথে যোগাযোগ করুন"
@@ -747,6 +779,7 @@ Recall This = "মনে করুন"
 recaptcha_not_passed = "CAPTCHA অনুত্তীর্ণ"
 Record Citations = "সাইটেশন নথি"
 Record Count = "নথির সংখ্যা"
+Record Type = "নথির প্রকার"
 Recover Account = "অ্যাকাউন্ট পুনরুদ্ধার করুন"
 recovery_by_email = "ইমেল অণুযায়ী উদ্ধার করুন"
 recovery_by_username = "ব্যবহারকারীর নাম অণুযায়ী উদ্ধার করুন"
@@ -791,6 +824,7 @@ Requests = "অনুরোধ"
 Reserves = "অরক্ষিত"
 Reserves Search = "আরক্ষিত অনুসন্ধান"
 Reserves Search Results = "আরক্ষিত অনুসন্ধান ফলাফল"
+result_count = "%%count%% ফলাফল"
 Results = "ফলাফল"
 results = "ফলাফল"
 Results for = "ফলাফলের জন্য"
@@ -842,6 +876,8 @@ Serial = "সাময়িক পত্র"
 Series = "মালা"
 Set = "সেট"
 Showing = "প্রদর্শন"
+sidebar_close = "সাইডবার সংকোচন"
+sidebar_expand = "সাইডবার প্রসারণ"
 Similar Items = "অনুরূপ উপাদানগুলি"
 Skip to content = "বিষয়বস্তু এড়িয়ে যান"
 skip_confirm = "আপনি কি এই ধাপটি এড়িয়ে যেতে চান?"
@@ -855,10 +891,12 @@ sms_success = "বার্তা পাঠানো হয়েছে।"
 Software = "সফটওয়্যার"
 Sorry, but the help you requested is unavailable in your language. = "দুঃখিত, সাহায্যের অনুরোধটি আপনার ভাষায় অনুপলব্ধ।"
 Sort = "শ্রেণী করন"
+sort_alphabetic = "বর্ণানুক্রমিক"
 sort_author = "লেখক"
 sort_author_author = "বর্ণানুক্রমিক"
 sort_author_relevance = "জনপ্রিয়তা"
 sort_callnumber = "ডাকসংখ্যা"
+sort_count = "ফলাফল গণনা"
 sort_relevance = "প্রাসঙ্গিকতা"
 sort_title = "আখ্যা"
 sort_year = "তারিখ অধোগামী অনুযায়ী সাজান"
@@ -894,6 +932,7 @@ storage_retrieval_request_invalid_pickup = "একটি ভুল পিকা
 storage_retrieval_request_issue = "তারিখ"
 storage_retrieval_request_place_fail_missing = "আপনার আনুরোধটি ব্যর্থ। কিছু ডাটা পাওয়া যাচ্ছে না। আরও সহায়তার জন্য সঞ্চারন ডেস্কে যোগাযোগ করুন"
 storage_retrieval_request_place_success = "আপনার অনুরোধটি সফল"
+storage_retrieval_request_place_success_html = "আপনার আনুরোধটি সম্পূর্ন হয়েছে। <a href="%%url%%">সঞ্চিত তথ্যোদ্ধার অনুরোধ</a>।"
 storage_retrieval_request_place_text = "একটি সংগ্রহস্থানে সঞ্চিত তথ্যোদ্ধার অণুরোধটি রাখুন"
 storage_retrieval_request_processed = "প্রক্রিয়াটি সম্পন্ন"
 storage_retrieval_request_profile_html = "সঞ্চিত তথ্যোদ্ধার অণুরোধের জন্য তথ্য, অনুগ্রহ করে স্থাপণ করুন আপনার <a href="%%url%%">Library Catalog Profile</a>."
@@ -919,6 +958,7 @@ summon_database_recommendations = "এখানে আপনি অতিরি
 Supplements = "সম্পূরক অংশ"
 Supplied by Amazon = "অ্যামাজন দ্বারা সরবরাহকৃত"
 Switch view to = "অন্যটিতে গিয়ে দেখুন"
+switchquery_fuzzy = "ফাজি (fuzzy) অনুসন্ধানের প্রয়োগ একই বানানের অন্যান্য শব্দ গুলি প্রদর্শন করবে"
 switchquery_intro = "আপনি আপনার অনুসন্ধান জিজ্ঞাসাগুলি সমন্বয়ের মাধ্যমে আরও ফলাফল পেতে পারেন"
 switchquery_lowercasebools = "আপনি যদি বুলিয়ান অপারেটর ব্যবহার করেন, তাহলে অবশ্যয় বড় হরফ ব্যবহার করুন"
 switchquery_truncatechar = "আপনার অনুসন্ধান জিজ্ঞাসা কমান এবং আপনার ফলাফল প্রসারিত করুন"
@@ -1001,9 +1041,6 @@ View this record in EBSCOhost = "নথিগুলো EBSCOhost এ দেখ
 visual_facet_parent = "থেকে"
 Volume = "খণ্ড"
 Volume Holdings = "খণ্ড সংগ্রহ"
-vudl_access_denied = "পরিষেবা বন্ধ।"
-vudl_tab_docs = "নথি"
-vudl_tab_pages = "পৃষ্ঠাগুলি"
 VuFind Configuration = "VuFind কনফিগারেশন"
 vufind_upgrade_fail = "আমরা এই সময়ে VuFind আপগ্রেড করতে পারছি না"
 Warning: These citations may not always be 100% accurate = "সতর্কবাণী: সাইটেশন সবসময় 100% নির্ভুল হতে পারে না"
diff --git a/languages/ca.ini b/languages/ca.ini
index 24eb96fd4da0204322dc89f1faf3d2bd456cc5b1..5e26cd72314aa0290b0b00a1c55debcdf6a6ee98 100644
--- a/languages/ca.ini
+++ b/languages/ca.ini
@@ -15,6 +15,7 @@
 Abstract = "Resum"
 Access = "Accés"
 Access URL = "URL d'accés"
+access_denied = "Accés no autoritzat."
 Accession Number = "Número de registre"
 Account = "Compte"
 Add = "Afegir"
@@ -71,8 +72,10 @@ An error occurred during execution; please try again later. = "S'ha produït un
 AND = "i"
 anonymous_tags = "Etiqueta anònima"
 APA Citation = "Cita APA"
+applied_filter = "Filtre aplicat"
 Article = "Article"
 Ask a Librarian = "Pregunteu al bibliotecari"
+Associated country = "Païs associat"
 Audience = "Destinataris"
 Audio = "Audio"
 authentication_error_admin = "No podem iniciar la vostra sessió en aquests moments. Contacteu amb l’adminitrador del sistema."
@@ -87,6 +90,7 @@ Author Browse = "Explorar autor"
 Author Notes = "Notes de l'autor"
 Author Results for = "Resultats d'autor per"
 Author Search Results = "Resultats a la cerca d'autor"
+Authority File = "Fitxer d'autoritat"
 Authors = "Autors"
 Authors Related to Your Search = "Autors relacionats a la cerca"
 Auto configuration is currently disabled = "Auto configuració deactivate"
@@ -250,6 +254,8 @@ Create New Password = "Nova Contrasenya"
 Created = "Creat"
 Database = "Base de dades"
 Date = "Data"
+Date of birth = "Data de naixement"
+Date of death = "Data de defunció"
 date_day_placeholder = "D"
 date_from = "Des de"
 date_month_placeholder = "M"
@@ -266,6 +272,7 @@ delete_list = "Eliminar llista"
 delete_page = "Esborrar pàgina"
 delete_selected = "Eliminar seleccionats"
 delete_selected_favorites = "Eliminar favorits seleccionats"
+delete_tag = "Esborrar etiqueta"
 delete_tags = "Esborrar etiquetes"
 delete_tags_by = "Esborrar etiquetes per"
 Department = "Department"
@@ -360,13 +367,14 @@ fav_email_fail = "Disculpeu, hi ha hagut un error. Els vostres favorits no s’h
 fav_email_missing = "S’han perdut algunes dades. Els vostres favorits no s’han enviat per correu electrònic."
 fav_email_success = "Els vostres favorits s’han enviat per correu electrònic."
 fav_export = "Exportar favorits"
-fav_list_delete = "La vostra llista de favorits s’ha eliminat"
-fav_list_delete_cancel = "Aquesta llista no s’ha eliminat"
+fav_list_delete = "La vostra llista de favorits s’ha eliminat."
+fav_list_delete_cancel = "Aquesta llista no s’ha eliminat."
 fav_list_delete_fail = "Disculpeu, hi ha hagut un error. La vostra llista no s’ha eliminat."
 Favorites = "Favorits"
 Fee = "Quota"
 Feedback = "Feedback"
 feedback_name = "Nom"
+Field of activity = "Camp d'activitat"
 File Description = "Descripció del fitxer"
 Filter = "Filtre"
 filter_tags = "Filtre d'etiquetes"
@@ -390,7 +398,9 @@ From = "Des de"
 Full description = "Descripció completa"
 Full text is not displayed to guests = "Text complet no disponible per a convidats."
 fulltext_limit = "Limitar a articles amb text complet disponible"
+Gender = "Gènere"
 Genre = "Gènere"
+Geographic Search = "Cerca geogràfica"
 Geographic Terms = "Termes Geogràfics"
 Geography = "Geografia"
 Get full text = "Obtenir text complet"
@@ -690,6 +700,7 @@ Number = "Número"
 number_decimal_point = "."
 number_thousands_separator = ","
 OAI Server = "Servidor OAI"
+Occupation = "Ocupació"
 of = "de"
 old_password = "Constrasenya antiga"
 On Reserve = "Reservat"
@@ -703,6 +714,7 @@ operator_exact = "es (exacte)"
 OR = "OR"
 or create a new list = "o crear una nova llista"
 original = "Original"
+Other associated place = "Altre lloc associat"
 Other Authors = "Altres autors"
 Other Editions = "Altres edicions"
 Other Libraries = "Altres biblioteques"
@@ -715,6 +727,8 @@ password_error_invalid = "Nova contrassenya no vàlida (ex. conté caracters no
 password_error_not_unique = "La constrasenya no s'ha canviat"
 password_maximum_length = "Longitut màxima de la contrasenya %%maxlength%% caracters"
 password_minimum_length = "El password ha tenor almenys %%minlength%% caracters"
+password_only_alphanumeric = "Números i lletres A-Z només"
+password_only_numeric = "Només números"
 Passwords do not match = "Les contrasenyes no coincideixen"
 Past = "Anterior"
 PDF Full Text = "PDF a text complet"
@@ -726,6 +740,8 @@ Physical Description = "Descripció física"
 Physical Object = "Objecte físic"
 pick_up_location = "Biblioteca de recollida"
 Place a Hold = "Fer una reserva"
+Place of birth = "Lloc de naixement"
+Place of death = "Lloc de defunció"
 Playing Time = "Temps de reproducció"
 Please check back soon = "Si us plau, retorneu aviat el préstec"
 Please contact the Library Reference Department for assistance = "Si us plau, per rebre ajuda contacteu amb el Departament de Referència de la Biblioteca"
@@ -772,6 +788,7 @@ Recall This = "Reclamar aquest"
 recaptcha_not_passed = "CAPTCHA invàlid"
 Record Citations = "Cites del registre"
 Record Count = "Número de registres"
+Record Type = "Tipus de registre"
 Recover Account = "S'ha oblidat el seu compte/contrasenya?"
 recovery_by_email = "Recuperar per mail"
 recovery_by_username = "Recuperar per usuari"
@@ -867,6 +884,8 @@ Serial = "Publicació Periòdica"
 Series = "Periòdiques"
 Set = "Conjunt"
 Showing = "Mostrar"
+sidebar_close = "Amagar lateral"
+sidebar_expand = "Expanish literal"
 Similar Items = "Ítems similars"
 Skip to content = "Anar al contingut"
 skip_confirm = "Vol ignorar aquest pas?"
@@ -880,10 +899,12 @@ sms_success = "Missatge enviat."
 Software = "Software"
 Sorry, but the help you requested is unavailable in your language. = "Disculpeu, però l’ajuda sol·licitada no està disponible en el vostre idioma."
 Sort = "Ordenar"
+sort_alphabetic = "Alfabèticament"
 sort_author = "Autor"
 sort_author_author = "Alfabèticament"
 sort_author_relevance = "Popularitat"
 sort_callnumber = "Topogràfic"
+sort_count = "Total resultats"
 sort_relevance = "Importància"
 sort_title = "Títol"
 sort_year = "Data Descendent"
@@ -1028,9 +1049,6 @@ View this record in EBSCOhost = "Veure aquest registre a EBSCOhost"
 visual_facet_parent = "Des de"
 Volume = "Volum"
 Volume Holdings = "Fons del volum"
-vudl_access_denied = "Accés no autoritzat."
-vudl_tab_docs = "Docs"
-vudl_tab_pages = "Pàgines"
 VuFind Configuration = "Configuració de VuFind"
 vufind_upgrade_fail = "No es pot actualitzar Vufind en aquests moments"
 Warning: These citations may not always be 100% accurate = "Atenció: Aquestes cites poden no estar 100% correctes"
diff --git a/languages/cs.ini b/languages/cs.ini
index 5b12c3cfcefde06d554228979e34db65c9bc52d8..b125da83c07d4623c5ab08dfef2707eac33a0d7c 100644
--- a/languages/cs.ini
+++ b/languages/cs.ini
@@ -2,6 +2,7 @@
 Abstract = "Abstrakt"
 Access = "Přístup"
 Access URL = "Přístupová URL adresa"
+access_denied = "Přístup odepřen."
 Accession Number = "Přístupové číslo"
 Account = "Účet"
 Add = "Přidat"
@@ -58,8 +59,10 @@ An error occurred during execution; please try again later. = "Vyskytla se chyba
 AND = "AND"
 anonymous_tags = "Anonymní tagy"
 APA Citation = "Citace podle APA"
+applied_filter = "Použitý filtr"
 Article = "Článek"
 Ask a Librarian = "Zeptejte se knihovníka"
+Associated country = "Související země"
 Audience = "Uživatelské určení"
 Audio = "Zvuková nahrávka"
 authentication_error_admin = "Při přihlášení došlo k chybě. Kontaktujte správce systému."
@@ -74,6 +77,7 @@ Author Browse = "Prohlížení mezi autory"
 Author Notes = "Poznámky autora"
 Author Results for = "Výsledky hledání pro autora"
 Author Search Results = "Výsledky hledání pro autora"
+Authority File = "Databáze autorit"
 Authors = "Autoři"
 Authors Related to Your Search = "Autoři relevantní k vašemu hledání"
 Auto configuration is currently disabled = "Automatická konfigurace je nyní vypnuta"
@@ -237,6 +241,8 @@ Create New Password = "Vytvořit nové heslo"
 Created = "Vytvořeno"
 Database = "Databáze"
 Date = "Datum"
+Date of birth = "Datum narození"
+Date of death = "Datum úmrtí"
 date_day_placeholder = "d"
 date_from = "Od"
 date_month_placeholder = "m"
@@ -253,6 +259,7 @@ delete_list = "odstranit seznam"
 delete_page = "Odstranit stránku"
 delete_selected = "smazat vybrané"
 delete_selected_favorites = "Smazat vybrané oblíbené"
+delete_tag = "Odstranit tag"
 delete_tags = "Odstranit tagy"
 delete_tags_by = "Odstranit tagy uživatele"
 Department = "Oddělení"
@@ -264,6 +271,7 @@ Displaying the top = "Zobrazuji nejrelevantnější výsledky"
 Document Inspector = "Inspekce dokumentu"
 Document Type = "Druh dokumentu"
 DOI = "DOI"
+Draw Search Box = "Zobrazit vyhledávací pole"
 Due = "do"
 Due Date = "Půjčeno do"
 DVD = "DVD"
@@ -354,6 +362,7 @@ Favorites = "Oblíbené"
 Fee = "Poplatek"
 Feedback = "Váš názor"
 feedback_name = "Jméno"
+Field of activity = "Oblast působení"
 File Description = "Popis souboru"
 Filter = "Filtr"
 filter_tags = "Filtrovat tagy"
@@ -377,7 +386,9 @@ From = "Od"
 Full description = "Celý popis"
 Full text is not displayed to guests = "Nepřihlášeným uživatelům se plný text nezobrazuje"
 fulltext_limit = "Omezit pouze na články s dostupným plným textem"
+Gender = "Pohlaví"
 Genre = "Žánr"
+Geographic Search = "Geografické vyhledávání"
 Geographic Terms = "Geografický termín"
 Geography = "Geografie"
 Get full text = "Získat plný text"
@@ -677,6 +688,7 @@ Number = "Číslo"
 number_decimal_point = ","
 number_thousands_separator = " "
 OAI Server = "OAI Server"
+Occupation = "Povolání"
 of = "z"
 old_password = "Staré heslo"
 On Reserve = "Rezervováno"
@@ -690,6 +702,7 @@ operator_exact = "rovná se"
 OR = "OR"
 or create a new list = "nebo vytvořit nový seznam"
 original = "Originál"
+Other associated place = "Další související místo"
 Other Authors = "Další autoři"
 Other Editions = "Další vydání"
 Other Libraries = "Další knihovny"
@@ -702,6 +715,8 @@ password_error_invalid = "Nové heslo není platné (např. obsahuje nepovolené
 password_error_not_unique = "Heslo nebylo změněno"
 password_maximum_length = "Heslo může být dlouhé maximálně %%maxlength%% znaků"
 password_minimum_length = "Heslo musí být dlouhé alespoň %%minlength%% znaků"
+password_only_alphanumeric = "Pouze čísla a písmena A-Z"
+password_only_numeric = "Pouze čísla"
 Passwords do not match = "Hesla se neshodují"
 Past = "Minulost"
 PDF Full Text = "Plný text ve formátu PDF"
@@ -713,6 +728,8 @@ Physical Description = "Fyzický popis"
 Physical Object = "Fyzický předmět"
 pick_up_location = "Místo vyzvednutí"
 Place a Hold = "Požadavek"
+Place of birth = "Místo narození"
+Place of death = "Místo úmrtí"
 Playing Time = "Doba přehrávání"
 Please check back soon = "Prosím, zkuste to znovu později"
 Please contact the Library Reference Department for assistance = "Pro pomoc se obraťte na pracovníky knihovny:"
@@ -759,6 +776,7 @@ Recall This = "Rezervovat"
 recaptcha_not_passed = "Kód CAPTCHA nesouhlasí."
 Record Citations = "Citace záznamu"
 Record Count = "Počet záznamů"
+Record Type = "Typ záznamu"
 Recover Account = "Obnovit účet"
 recovery_by_email = "Obnovit pomocí emailu"
 recovery_by_username = "Obnovit pomocí uživatelského jména"
@@ -803,6 +821,7 @@ Requests = "Požadavky"
 Reserves = "Rezervace"
 Reserves Search = "Vyhledávání rezervací"
 Reserves Search Results = "Výsledky vyhledávání rezervací"
+result_count = "%%count%% výsledků"
 Results = "Výsledky"
 results = "výsledků"
 Results for = "výsledků pro"
@@ -854,6 +873,8 @@ Serial = "Seriál"
 Series = "Edice"
 Set = "Nastavit"
 Showing = "Zobrazuji"
+sidebar_close = "Skrýt možnosti"
+sidebar_expand = "Zobrazit možnosti"
 Similar Items = "Podobné jednotky"
 Skip to content = "Přeskočit na obsah"
 skip_confirm = "Opravdu chcete přeskočit tento krok?"
@@ -867,10 +888,12 @@ sms_success = "SMS úspěšně odeslána."
 Software = "Program"
 Sorry, but the help you requested is unavailable in your language. = "Omlouváme se, ale nápověda není k dispozici ve vašem jazyce."
 Sort = "Seřadit podle"
+sort_alphabetic = "AbecednÄ›"
 sort_author = "Autor"
 sort_author_author = "AbecednÄ›"
 sort_author_relevance = "Oblíbenost"
 sort_callnumber = "Signatury"
+sort_count = "Počet výsledků"
 sort_relevance = "Relevance"
 sort_title = "Název"
 sort_year = "Podle data sestupnÄ›"
@@ -1015,9 +1038,6 @@ View this record in EBSCOhost = "Zobrazit záznam v databázi EBSCOhost"
 visual_facet_parent = "Od"
 Volume = "Část"
 Volume Holdings = "Jednotky"
-vudl_access_denied = "Přístup odepřen."
-vudl_tab_docs = "Dokumenty"
-vudl_tab_pages = "Strany"
 VuFind Configuration = "Nastavení VuFindu"
 vufind_upgrade_fail = "Nyní nelze VuFind aktualizovat"
 Warning: These citations may not always be 100% accurate = "Upozornění: Tyto citace jsou generovány automaticky. Nemusí být zcela správně podle citačních pravidel."
diff --git a/languages/cy.ini b/languages/cy.ini
index 004c67619fdf36471d85a4ec5bc358910fcfb617..a2e7b917ffe6111087f5c7136957d26b0a9cf530 100644
--- a/languages/cy.ini
+++ b/languages/cy.ini
@@ -4,6 +4,7 @@
 Abstract = "Crynodeb"
 Access = "Mynediad"
 Access URL = "URL mynediad"
+access_denied = "Mynediad wedi'i wrthod."
 Accession Number = "Cyfeirnod"
 Account = "Cyfrif"
 Add = "Ychwanegu"
@@ -60,8 +61,10 @@ An error occurred during execution; please try again later. = "Bu gwall wrth gyf
 AND = "AND"
 anonymous_tags = "Tagiau Anhysbys"
 APA Citation = "Dyfyniad APA"
+applied_filter = "Hidlydd Cymwysedig"
 Article = "Erthygl"
 Ask a Librarian = "Gofynnwch i Lyfrgellydd"
+Associated country = "Gwlad gysylltiedig"
 Audience = "Cynulleidfa"
 Audio = "Sain"
 authentication_error_admin = "Ni fedrwn eich mewngofnodi yr adeg hwn. Cysylltwch â gweinyddwr eich system am gymorth"
@@ -76,6 +79,7 @@ Author Browse = "Pori Awduron"
 Author Notes = "Nodiadau'r Awdur"
 Author Results for = "Canlyniadau ar gyfer Awdur"
 Author Search Results = "Canlyniadau Chwilio am Awdur"
+Authority File = "Ffeil Awdurdod"
 Authors = "Awduron"
 Authors Related to Your Search = "Awduron yn berthnasol i'ch chwiliad"
 Auto configuration is currently disabled = "Ar hyn o bryd, mae awto-osod wedi'i analluogi"
@@ -239,6 +243,8 @@ Create New Password = "Creu Cyfrinair Newydd"
 Created = "Wedi'i Greu"
 Database = "Cronfa ddata"
 Date = "Dyddiad"
+Date of birth = "Dyddiad geni"
+Date of death = "Dyddiad marwolaeth"
 date_day_placeholder = "D"
 date_from = "o"
 date_month_placeholder = "M"
@@ -255,6 +261,7 @@ delete_list = "Dileu Rhestr"
 delete_page = "Dileu Tudalen"
 delete_selected = "Dileu'r Detholiad"
 delete_selected_favorites = "Dileu Ffefrynnau Dethol"
+delete_tag = "Dileu Tag"
 delete_tags = "Dileu Tagiau"
 delete_tags_by = "Dileu tagiau yn ôl"
 Department = "Adran"
@@ -266,6 +273,7 @@ Displaying the top = "Arddangos y brig"
 Document Inspector = "Archwilydd Dogfen"
 Document Type = "Math o Ddogfen"
 DOI = "DOI"
+Draw Search Box = "Llunio Blwch Chwilio"
 Due = "Erbyn"
 Due Date = "Dyddiad Cau"
 DVD = "DVD"
@@ -349,13 +357,14 @@ fav_email_fail = "Mae'n ddrwg gennym, mae gwall wedi digwydd. Ni e-bostiwyd eich
 fav_email_missing = "Roedd peth data ar goll. Ni e-bostiwyd eich ffefryn(nau)."
 fav_email_success = "E-bostiwyd eich ffefryn(nau) fel y gofynnwyd."
 fav_export = "Allfudo Ffefrynnau"
-fav_list_delete = "Dilëwyd eich rhestr ffefrynnau"
-fav_list_delete_cancel = "Ni ddilëwyd y rhestr hon"
+fav_list_delete = "Dilëwyd eich rhestr ffefrynnau."
+fav_list_delete_cancel = "Ni ddilëwyd y rhestr hon."
 fav_list_delete_fail = "Mae'n ddrwg gennym, mae gwall wedi digwydd. Ni ddilëwyd eich rhestr."
 Favorites = "Ffefrynnau"
 Fee = "Fee"
 Feedback = "Adborth"
 feedback_name = "Enw"
+Field of activity = "Maes gweithgarwch"
 File Description = "Disgrifiad Ffeil"
 Filter = "Hidlo"
 filter_tags = "Hidlo Tagiau"
@@ -379,7 +388,9 @@ From = "o"
 Full description = "Disgrifiad llawn"
 Full text is not displayed to guests = "Ni ddangosir y testun llawn i westeion."
 fulltext_limit = "Cyfyngu i erthyglau â'r testun llawn ar gael"
+Gender = "Rhywedd"
 Genre = "Genre"
+Geographic Search = "Chwiliad daearyddol"
 Geographic Terms = "Termau Daearyddol"
 Geography = "Daearyddiaeth"
 Get full text = "Cael y testun llawn"
@@ -679,6 +690,7 @@ Number = "Rhifau"
 number_decimal_point = "."
 number_thousands_separator = ","
 OAI Server = "Gweinydd OAI"
+Occupation = "Galwedigaeth"
 of = "o"
 old_password = "Hen Gyfrinair"
 On Reserve = "Ar Gadw"
@@ -692,6 +704,7 @@ operator_exact = "yn (yn union)"
 OR = "OR"
 or create a new list = "neu greu rhestr newydd"
 original = "Gwreiddiol"
+Other associated place = "Lle cysylltiedig arall"
 Other Authors = "Awduron Eraill"
 Other Editions = "Rhifynnau Eraill"
 Other Libraries = "Llyfrgelloedd Eraill"
@@ -704,6 +717,8 @@ password_error_invalid = "Mae'r cyfrinair newydd yn annilys (e.e. mae'n cynnwys
 password_error_not_unique = "Ni newidiwyd y cyfrinair"
 password_maximum_length = "Ni all y cyfrinair fod yn hirach na %%maxlength%% nod"
 password_minimum_length = "Rhaid i'r cyfrinair gynnwys o leiaf %%minlength%% nod"
+password_only_alphanumeric = "Rhifau a llythrennau, A-Z yn unig"
+password_only_numeric = "Rhifau yn unig"
 Passwords do not match = "Nid yw'r cyfrineiriau yr un peth"
 Past = "Gorffennol"
 PDF Full Text = "Testun PDF llawn"
@@ -715,6 +730,8 @@ Physical Description = "Disgrifiad Corfforoll"
 Physical Object = "Gwrthrych Ffisegol"
 pick_up_location = "Casglu o'r Llyfrgell"
 Place a Hold = "Gwneud Cais"
+Place of birth = "Lleoliad geni"
+Place of death = "Lleoliad marwolaeth"
 Playing Time = "Amser Chwarae"
 Please check back soon = "Gwiriwch eto yn fuan"
 Please contact the Library Reference Department for assistance = "Cysylltwch â'r Ddesg Fenthyca am gymorth"
@@ -761,6 +778,7 @@ Recall This = "Adalw hwn"
 recaptcha_not_passed = "Methwyd y prawf CAPTCHA"
 Record Citations = "Cofnodi Dyfyniadau"
 Record Count = "Nifer y Cofnodion"
+Record Type = "Math Cofnod"
 Recover Account = "Adfer Cyfrif"
 recovery_by_email = "Adfer trwy e-bost"
 recovery_by_username = "Adfer trwy enw defnyddiwr"
@@ -805,6 +823,7 @@ Requests = "Ceisiadau"
 Reserves = "Ar Gadw"
 Reserves Search = "Chwilio Ar Gadw"
 Reserves Search Results = "Canlyniadau Chwilio Ar Gadw"
+result_count = "%%count%% canlyniadau"
 Results = "Canlyniadau"
 results = "canlyniadau"
 Results for = "Canlyniadau ar gyfer"
@@ -856,6 +875,8 @@ Serial = "Cyfresol"
 Series = "Cyfres"
 Set = "Set"
 Showing = "Dangos"
+sidebar_close = "Cwympo bar ochr"
+sidebar_expand = "Ehangu bar ochr"
 Similar Items = "Eitemau Tebyg"
 Skip to content = "Neidio i'r cynnwys"
 skip_confirm = "Ydych chi'n siŵr eich bod am hepgor y cam hwn?"
@@ -869,10 +890,12 @@ sms_success = "Neges wedi'i anfon"
 Software = "Meddalwedd"
 Sorry, but the help you requested is unavailable in your language. = "Ymddiheuriadau, ond nid yw'r cymorth a geisiwyd ar gael yn eich iaith"
 Sort = "Sortio"
+sort_alphabetic = "Trefn y wyddor"
 sort_author = "Awdur"
 sort_author_author = "Trefn y wyddor"
 sort_author_relevance = "Poblogrwydd"
 sort_callnumber = "Rhif Galw"
+sort_count = "Cyfri Canlyniadau"
 sort_relevance = "Perthnasedd"
 sort_title = "Teitl"
 sort_year = "Dyddiad Trefn Ddisgynnol"
@@ -1017,9 +1040,6 @@ View this record in EBSCOhost = "Gweld y cofnod hwn mewn EBSCOhost"
 visual_facet_parent = "o"
 Volume = "Sain"
 Volume Holdings = "Daliadau Sain"
-vudl_access_denied = "Mynediad wedi'i wrthod."
-vudl_tab_docs = "Dogfennau"
-vudl_tab_pages = "Tudalennau"
 VuFind Configuration = "Ffurfwedd iFind"
 vufind_upgrade_fail = "Ni allwn uwchraddio VuFind ar hyn o bryd"
 Warning: These citations may not always be 100% accurate = "Rhybudd: Mae'n bosib nad yw'r dyfyniadau hyn bob amser yn 100% cywir"
diff --git a/languages/da.ini b/languages/da.ini
index 865826dc1c1c9a88cd4f0f83951f43177e2f9a2d..614aba3980b5803b54713f895ac65d5edfbc9add 100644
--- a/languages/da.ini
+++ b/languages/da.ini
@@ -252,8 +252,8 @@ fav_email_fail = "Vi beklager. Der er opstået en fejl. Dine favoritter blev ikk
 fav_email_missing = "”Der er sket en fejl. Dine favoritter blev ikke sendt”"
 fav_email_success = "Dine favoritter blev sendt"
 fav_export = "Eksportér favoritter"
-fav_list_delete = "Din favoritliste blev slettet"
-fav_list_delete_cancel = "Denne liste blev ikke slettet"
+fav_list_delete = "Din favoritliste blev slettet."
+fav_list_delete_cancel = "Denne liste blev ikke slettet."
 fav_list_delete_fail = "Vi beklager. Der er sket en fejl. Den liste blev ikke slettet."
 Favorites = "Favoritter"
 Fee = "Betaling"
@@ -571,6 +571,7 @@ sms_success = "Besked sendt."
 Software = "Software"
 Sorry, but the help you requested is unavailable in your language. = "Vi beklager, men den hjælp du har bedt om er ikke tilgængelig på dit sprog."
 Sort = "Sortér"
+sort_alphabetic = "Alfabetisk"
 sort_author = "Forfatter"
 sort_author_author = "Alfabetisk"
 sort_author_relevance = "Popularitet"
diff --git a/languages/de.ini b/languages/de.ini
index a1a1fdcdafcf8ef7a32315e4cedc5fef262dd72b..8c47e55fdd77f3f6ffa33d2030f49bc9c6d24392 100644
--- a/languages/de.ini
+++ b/languages/de.ini
@@ -1,8 +1,9 @@
-; For future reference:
-;German = Deutsch
+; For future reference:
+;German = Deutsch
 Abstract = "Abstract"
 Access = "Zugangseinschränkungen"
 Access URL = "Zugangs-URL"
+access_denied = "Zugriff nicht erlaubt."
 Accession Number = "Dokumentencode"
 Account = "Konto"
 Add = "Hinzufügen"
@@ -59,8 +60,10 @@ An error occurred during execution; please try again later. = "Ein Fehler ist au
 AND = "UND"
 anonymous_tags = "Anonyme Tags"
 APA Citation = "APA Zitierstil"
+applied_filter = "Aktivierter Filter"
 Article = "Artikel"
 Ask a Librarian = "Fachauskunft der Bibliothek"
+Associated country = "Zugehöriges Land"
 Audience = "Zielpublikum"
 Audio = "Audio"
 authentication_error_admin = "Sie können sich momentan nicht einloggen. Bitte kontaktieren Sie den Systemadministrator für weitere Hilfe."
@@ -75,6 +78,7 @@ Author Browse = "Verfasser - Browsen"
 Author Notes = "Verfasserangaben"
 Author Results for = "gefundene Verfasser"
 Author Search Results = "Verfasser Suchresultate"
+Authority File = "Normdatei"
 Authors = "Autoren"
 Authors Related to Your Search = "Verfasser die mit Ihrer Suche im Zusammenhang stehen."
 Auto configuration is currently disabled = "Autokonfiguration ist momentan nicht aktiviert"
@@ -238,6 +242,8 @@ Create New Password = "Neues Passwort erstellen"
 Created = "Erstellt"
 Database = "Datenbank"
 Date = "Datum"
+Date of birth = "Geburtsdatum"
+Date of death = "Todestag"
 date_day_placeholder = "T"
 date_from = "Von"
 date_month_placeholder = "M"
@@ -254,6 +260,7 @@ delete_list = "Liste löschen"
 delete_page = "Seite löschen"
 delete_selected = "Auswahl löschen"
 delete_selected_favorites = "Ausgewählte Favoriten löschen"
+delete_tag = "Tag löschen"
 delete_tags = "Tags löschen"
 delete_tags_by = "Tags löschen nach"
 Department = "Abteilung"
@@ -265,6 +272,7 @@ Displaying the top = "Angezeigt werden die ersten"
 Document Inspector = "Dokumentprüfer"
 Document Type = "Publikationsart"
 DOI = "DOI"
+Draw Search Box = "Suchbereich definieren"
 Due = "Rückgabe bis"
 Due Date = "Rückgabedatum"
 DVD = "DVD"
@@ -348,13 +356,14 @@ fav_email_fail = "Leider ist ein Fehler aufgetreten. Ihre ausgewählten Favorite
 fav_email_missing = "Fehlende Angaben. Ihre ausgewählten Favoriten wurden nicht per E-Mail versendet."
 fav_email_success = "Ihre ausgewählten Favoriten wurden erfolgreich per E-Mail versendet."
 fav_export = "Favoriten exportieren"
-fav_list_delete = "Ihre Favoritenliste wurde gelöscht"
-fav_list_delete_cancel = "Die Liste wurde nicht gelöscht"
+fav_list_delete = "Ihre Favoritenliste wurde gelöscht."
+fav_list_delete_cancel = "Die Liste wurde nicht gelöscht."
 fav_list_delete_fail = "Leider ist ein Fehler aufgetreten. Ihre Liste wurde nicht gelöscht."
 Favorites = "Favoriten"
 Fee = "Gebühr"
 Feedback = "Feedback"
 feedback_name = "Name"
+Field of activity = "Tätigkeitsbereich"
 File Description = "Dateibeschreibung"
 Filter = "Filter"
 filter_tags = "Tags filtern"
@@ -378,7 +387,9 @@ From = "Von"
 Full description = "Ausführliche Beschreibung"
 Full text is not displayed to guests = "Volltext ist im Gastzugang nicht verfügbar."
 fulltext_limit = "Eingrenzen auf Datensätze mit Volltextzugriff"
+Gender = "Geschlecht"
 Genre = "Genre"
+Geographic Search = "Geografische Suche"
 Geographic Terms = "Geografische Kategorien"
 Geography = "Geographie"
 Get full text = "Volltext"
@@ -560,7 +571,7 @@ list_access_denied = "Sie sind nicht berechtigt diese Liste anzusehen."
 list_edit_name_required = "Name für die Liste fehlt!"
 list_item_delete = "Eintrag aus der Liste löschen"
 load_tag_error = "Fehler: Laden der Tags fehlgeschlagen"
-Loading = "wird geladen..."
+Loading = "Wird geladen"
 Local Login = "Lokale Anmeldung"
 local_login_desc = "Geben Sie ihren Benutzernamen und ihr Passwort für diese Webseite ein."
 Located = "Standort"
@@ -679,6 +690,7 @@ Number = "Nummer"
 number_decimal_point = ","
 number_thousands_separator = "."
 OAI Server = "OAI-Server"
+Occupation = "Beruf"
 of = "von"
 old_password = "Bisheriges Passwort"
 On Reserve = "Vorgemerkt"
@@ -692,6 +704,7 @@ operator_exact = "ist gleich"
 OR = "ODER"
 or create a new list = "oder erstellen Sie eine neue Liste"
 original = "Original"
+Other associated place = "Weiterer zugehöriger Ort"
 Other Authors = "Weitere Verfasser"
 Other Editions = "Weitere Ausgaben"
 Other Libraries = "Weitere Bibliotheken"
@@ -704,6 +717,8 @@ password_error_invalid = "Ungültiges neues Passwort (enthält z.B. ungültige Z
 password_error_not_unique = "Das Passwort wurde nicht geändert"
 password_maximum_length = "Die maximale Länge des Passowrts beträgt %%maxlength%% Zeichen"
 password_minimum_length = "Die minimale Länge des Passowrts beträgt %%minlength%% Zeichen"
+password_only_alphanumeric = "Nur Zahlen und Buchstaben A-Z"
+password_only_numeric = "Nur Zahlen"
 Passwords do not match = "Die Passwörter stimmen nicht überein"
 Past = "seit"
 PDF Full Text = "PDF-Volltext"
@@ -715,6 +730,8 @@ Physical Description = "Beschreibung"
 Physical Object = "Objekt"
 pick_up_location = "Abholung in Bibliothek"
 Place a Hold = "Bestellen"
+Place of birth = "Geburtsort"
+Place of death = "Sterbeort"
 Playing Time = "Spieldauer"
 Please check back soon = "Bitte versuchen Sie es bald nocheinmal"
 Please contact the Library Reference Department for assistance = "Bitte kontaktieren Sie ihre Bibliothek für weitere Hilfe"
@@ -761,6 +778,7 @@ Recall This = "Vormerken"
 recaptcha_not_passed = "CAPTCHA nicht korrekt eingegeben"
 Record Citations = "Zitate"
 Record Count = "Satzanzahl"
+Record Type = "Datenart"
 Recover Account = "Zugang zum Konto wiederherstellen"
 recovery_by_email = "Über die E-Mail zurücksetzen"
 recovery_by_username = "Über den Benutzernamen zurücksetzen"
@@ -805,6 +823,7 @@ Requests = "Anfragen"
 Reserves = "Vormerkungen"
 Reserves Search = "Suche Vormerkungen"
 Reserves Search Results = "Suchergebnisse Vormerkungen"
+result_count = "%%count%% Treffer"
 Results = "Treffer"
 results = "Treffer"
 Results for = "Ergebnis für"
@@ -856,6 +875,8 @@ Serial = "Schriftenreihe"
 Series = "Schriftenreihe"
 Set = "Wechseln"
 Showing = "Treffer"
+sidebar_close = "Seitenleiste ausblenden"
+sidebar_expand = "Seitenleiste einblenden"
 Similar Items = "Ähnliche Einträge"
 Skip to content = "Weiter zum Inhalt"
 skip_confirm = "Wollen Sie diesen Schritt wirklich überspringen?"
@@ -869,10 +890,12 @@ sms_success = "Nachricht verschickt."
 Software = "Software"
 Sorry, but the help you requested is unavailable in your language. = "Die Hilfeseiten sind nicht verfügbar in Ihrer Sprache."
 Sort = "Sortieren"
+sort_alphabetic = "Alphabetisch"
 sort_author = "Verfasser"
 sort_author_author = "Alphabetisch"
 sort_author_relevance = "Häufigkeit"
 sort_callnumber = "Signatur"
+sort_count = "Anzahl Treffer"
 sort_relevance = "Relevanz"
 sort_title = "Titel"
 sort_year = "Nach Datum, absteigend"
@@ -1017,9 +1040,6 @@ View this record in EBSCOhost = "Diesen Datensatz in EBSCOhost ansehen"
 visual_facet_parent = "Von"
 Volume = "Jahrgang"
 Volume Holdings = "Bandangaben"
-vudl_access_denied = "Zugriff nicht erlaubt."
-vudl_tab_docs = "Dokumente"
-vudl_tab_pages = "Seiten"
 VuFind Configuration = "VuFind-Konfiguration"
 vufind_upgrade_fail = "Vufind kann momentan nicht upgegradet werden"
 Warning: These citations may not always be 100% accurate = "Achtung: Diese Zitate sind unter Umständen nicht zu 100% korrekt"
diff --git a/languages/el.ini b/languages/el.ini
index 3282e7c589a38ce31dec2ff3420ef359714981ca..fe78601d270129777828b53b750a234fd08fc3cb 100644
--- a/languages/el.ini
+++ b/languages/el.ini
@@ -4,6 +4,7 @@
 Abstract = "Περίληψη"
 Access = "Πρόσβαση"
 Access URL = "Σύνδεσμος πρόσβασης"
+access_denied = "Δεν επιτρέπεται η πρόσβαση."
 Accession Number = "Αριθμός Καταχώρησης"
 Account = "Λογαριασμός"
 Add = "Προσθήκη"
@@ -60,8 +61,10 @@ An error occurred during execution; please try again later. = "Παρουσιά
 AND = "AND"
 anonymous_tags = "Ανώνυμες ετικέτες"
 APA Citation = "Παραπομπή APA"
+applied_filter = "Εφαρμοσμένο φίλτρο"
 Article = "Άρθρο"
 Ask a Librarian = "Ερώτηση σε βιβλιοθηκονόμο"
+Associated country = "Σχετιζόμενη χώρα"
 Audience = "Κοινό"
 Audio = "Ηχητικό"
 authentication_error_admin = "Η σύνδεση δεν είναι δυνατή. Απευθυνθείτε στον διαχειριστή του συστήματος"
@@ -76,6 +79,7 @@ Author Browse = "Περιήγηση ανά συγγραφέα"
 Author Notes = "Σημειώσεις συγγραφέα"
 Author Results for = "Αποτελέσματα για συγγραφέα"
 Author Search Results = "Αποτελέσματα αναζήτησης συγγραφέα"
+Authority File = "Αρχείο καθιερωμένων όρων"
 Authors = "Συγγραφείς"
 Authors Related to Your Search = "Συγγραφείς σχετική με την αναζήτηση"
 Auto configuration is currently disabled = "Η αυτόματη παραμετροποίηση είναι απενεργοποιημένη"
@@ -96,6 +100,7 @@ Be the first to leave a comment = "καταχωρήστε σχόλιο πρώτ
 Be the first to tag this record = "Καταχωρήστε ετικέτα πρώτοι"
 Bibliographic Details = "Λεπτομέρειες βιβλιογραφικής εγγραφής"
 Bibliography = "Βιβλιογραφία"
+Blu-ray Disc = "Δίσκος Blu-ray"
 Book = "Βιβλίο"
 Book Bag = "Καλάθι βιβλίων"
 Book Chapter = "Κεφάλαιο βιβλίου"
@@ -186,6 +191,7 @@ citation_singlepage_abbrev = "p."
 citation_volume_abbrev = "Vol."
 Cite this = "Εμφάνιση παραπομπής"
 City = "Πόλη"
+Clear = "Καθαρισμός"
 clear_tag_filter = "Καθαρισμός φίλτρων"
 close = "κλείσιμο"
 Code = "Κώδικας"
@@ -225,6 +231,7 @@ Copies = "Αντίγραφα"
 Copy = "Αντίγραφο"
 Copyright = "Copyright"
 Corporate Author = "Corporate συγγραφέας"
+Corporate Authors = "Συλλογικά Όργανα"
 Country = "Χώρα"
 Course = "Μάθημα"
 Course Reserves = "Δεσμευμένα για μαθήματα"
@@ -236,6 +243,8 @@ Create New Password = "Δημιουργία νέου κωδικού"
 Created = "Δημιουργήθηκε"
 Database = "Βάση δεδομένων"
 Date = "Ημερομηνία"
+Date of birth = "Ημερομηνία γέννησης"
+Date of death = "Ημερομηνία θανάτου"
 date_day_placeholder = "Η"
 date_from = "Από"
 date_month_placeholder = "Μ"
@@ -252,6 +261,7 @@ delete_list = "Διαγραφή λίστας"
 delete_page = "Διαγραφή σελίδας"
 delete_selected = "Διαγραφή επιλεγμένων"
 delete_selected_favorites = "Διαγραφή επιλεγμένων αγαπημένων"
+delete_tag = "Διαγραφή ετικέτας"
 delete_tags = "Διαγραφή ετικετών"
 delete_tags_by = "Διαγραφή ετικετών με"
 Department = "Τμήμα"
@@ -263,6 +273,7 @@ Displaying the top = "Εμφανίζονται τα πρώτα"
 Document Inspector = "Επισκόπηση εγγράφου"
 Document Type = "Τύπος εγγράφου"
 DOI = "DOI"
+Draw Search Box = "Σχεδιάστε την περιοχή αναζήτησης"
 Due = "Επιστροφή"
 Due Date = "Ημερομηνία επιστροφής"
 DVD = "DVD"
@@ -353,6 +364,7 @@ Favorites = "Αγαπημένα"
 Fee = "Χρέωση"
 Feedback = "σχόλιά"
 feedback_name = "Όνομα"
+Field of activity = "Πεδίο δραστηριότητας"
 File Description = "Περιγραφή αρχείου"
 Filter = "Φίλτρο"
 filter_tags = "Φιλτράρισμα ετικετών"
@@ -376,7 +388,9 @@ From = "από"
 Full description = "Πλήρης περιγραφή"
 Full text is not displayed to guests = "Οι επισκέπτες δεν έχουν πρόσβαση στο πλήρες κείμενο."
 fulltext_limit = "Μόνο τα άρθρα με πλήρες κείμενο"
+Gender = "Γένος"
 Genre = "Είδος"
+Geographic Search = "Γεωγραφική αναζήτηση"
 Geographic Terms = "Γεωγραφικοί όροι"
 Geography = "Γεωγραφία"
 Get full text = "Λήψη πλήρους κειμένου"
@@ -384,6 +398,7 @@ Get RSS Feed = "Λήψη RSS"
 Globe = "Ουράνια σφαίρα"
 Go = "Έναρξη"
 Go to Standard View = "Κανονική προβολή"
+go_to_list = "Μετάβαση στη λίστα"
 google_map_cluster = "Σύμπλεγμα (Google Maps)"
 google_map_cluster_points = "Σημεία ομαδοποίησης"
 Grid = "Πλέγμα"
@@ -568,6 +583,7 @@ login_disabled = "Η είσοδος στο σύστημα είναι απενε
 login_target = "Βιβλιοθήκη"
 Logout = "Έξοδος"
 Main Author = "Κύριος συγγραφέας"
+Main Authors = "Κύριοι συγγραφείς"
 Major Categories = "Κύριες κατηγορίες"
 Manage Tags = "Διαχείριση ετικετών"
 Manuscript = "Χειρόγραφο"
@@ -590,6 +606,7 @@ More catalog results = "Περισσότερα αποτελέσματα κατα
 More options = "Περισσότερες επιλογές"
 More Summon results = "Περισσότερα αποτελέσματα (Summon)"
 More Topics = "Περισσότερα θέματα"
+more_authors_abbrev = "κ.ά."
 more_info_toggle = "Εμφάνιση / απόκρυψη περισσότερων πληροφοριών."
 more_topics = "%%count%% περισσότερα θέματα"
 Most Recent Received Issues = "πιο πρόσφατα τεύχη"
@@ -623,11 +640,14 @@ No reviews were found for this record = "Δεν υπάρχουν reviews για
 No Tags = "Δεν υπάρχουν"
 no_description = "Η περιγραφή δεν είναι διαθέσιμη"
 no_items_selected = "Δεν επιλέχθηκαν τεκμήρια"
+nohit_active_filters = "Ένα ή περισσότερα φίλτρα έχουν εφαρμοστεί σε αυτή την αναζήτηση. Αν τα φίλτρα αναζήτησης αφαιρεθούν, θα μπορέσετε να ανακτήσετε περισσότερα αποτελέσματα."
+nohit_change_tab = "Κάνετε αναζήτηση στην καρτέλα "%%activeTab%%". Μπορεί να βρείτε σχετικά αποτελέσματα σε μια από τις άλλες καρτέλες:"
 nohit_filters = "Ενεργά φίλτρα αναζήτησης:"
 nohit_heading = "Κανένα αποτέλεσμα!"
 nohit_no_filters = "Δεν υπάρχουν ενεργά φίλτρα για την αναζήτηση."
 nohit_parse_error = "Δεν βρέθηκαν αποτελέσματα. Ελέγξτε τη σύνταξη ή βάλτε τη φράση αναζήτησης σε εισαγωγικά"
 nohit_prefix = "Για την αναζήτησή σας"
+nohit_query_without_filters = "Αφαίρεση όλων των φίλτρων αναζήτησης."
 nohit_spelling = "Ελέγξτε την ορθογραφία"
 nohit_suffix = "δε βρέθηκαν αποτελέσματα"
 nohit_suggest = "Δοκιμάστε εναλλακτική αναζήτηση με λιγότερες λέξεις ή ελέγξτε την ορθογραφία"
@@ -664,11 +684,13 @@ note_785_5 = "Απορροφάται μερικά από"
 note_785_6 = "Διαχωρίζεται σε"
 note_785_7 = "Συνενώθηκε με / Μορφές"
 note_785_8 = "Αναίρεση αλλαγής σε"
+note_787 = "Άλλη συσχέτιση"
 Notes = "Σημειώσεις"
 Number = "Αριθμός"
 number_decimal_point = ","
 number_thousands_separator = "."
 OAI Server = "Εξυπηρετητής OAI"
+Occupation = "Επάγγελμα"
 of = "από"
 old_password = "Τρέχων κωδικός"
 On Reserve = "Δεσμευμένο"
@@ -682,6 +704,7 @@ operator_exact = "είναι (ακριβώς)"
 OR = "OR"
 or create a new list = "ή δημιουργήστε νέα λίστα"
 original = "Αρχική μορφή"
+Other associated place = "Άλλος συναφής τόπος"
 Other Authors = "Άλλοι συγγραφείς"
 Other Editions = "Άλλες εκδόσεις"
 Other Libraries = "Άλλες βιβλιοθήκες"
@@ -694,6 +717,8 @@ password_error_invalid = "Ο νέος κωδικός πρόσβασης δεν 
 password_error_not_unique = "Ο κωδικός δεν άλλαξε"
 password_maximum_length = "Ο μέγιστος αριθμός χαρακτήρων για τον κωδικό είναι %%maxlength%%"
 password_minimum_length = "Ο ελάχιστος αριθμός χαρακτήρων για τον κωδικό είναι %%minlength%%"
+password_only_alphanumeric = "Αριθμοί και γράμματα A-Z μόνο"
+password_only_numeric = "Αριθμοί μόνο"
 Passwords do not match = "Οι κωδικοί δε συμφωνούν"
 Past = "Προηγούμενες"
 PDF Full Text = "Πλήρες κείμενο PDF"
@@ -705,6 +730,8 @@ Physical Description = "Φυσική περιγραφή"
 Physical Object = "Φυσικό αντικείμενο"
 pick_up_location = "Βιβλιοθήκη παραλαβής"
 Place a Hold = "Κάντε κράτηση"
+Place of birth = "Τόπος γέννησης"
+Place of death = "Τόπος θανάτου"
 Playing Time = "Διάρκεια"
 Please check back soon = "Ελέγξτε πάλι αργότερα"
 Please contact the Library Reference Department for assistance = "Απευθυνθείτε στο προσωπικό της βιβλιοθήκης για βοήθεια"
@@ -751,6 +778,7 @@ Recall This = "Κάντε ανάκληση"
 recaptcha_not_passed = "Λανθασμένο CAPTCHA"
 Record Citations = "Αναφορές εγγραφής"
 Record Count = "Αριθμός εγγραφών"
+Record Type = "Τύπος εγγραφής"
 Recover Account = "Ανάκτηση λογαριασμού"
 recovery_by_email = "Ανάκτηση μέσω email"
 recovery_by_username = "Ανάκτηση μέσω ονόματος χρήστη"
@@ -795,6 +823,7 @@ Requests = "Αιτήσεις"
 Reserves = "Καρατήσεις"
 Reserves Search = "Αναζήτηση κρατήσεων"
 Reserves Search Results = "Αποτελέσματα αναζήτησης κρατήσεων"
+result_count = "%%count%% αποτελέσματα"
 Results = "Αποτελέσματα"
 results = "Αποτελέσματα"
 Results for = "Αποτελέσματα για"
@@ -846,6 +875,8 @@ Serial = "Περιοδική έκδοση"
 Series = "Σειρά"
 Set = "Εφαρμογή"
 Showing = "Εμφανίζονται"
+sidebar_close = "Σύμπτυξη πλαϊνής μπάρας"
+sidebar_expand = "Ανάπτυξη πλαϊνής μπάρας"
 Similar Items = "Παρόμοια τεκμήρια"
 Skip to content = "Μετάβαση στο περιεχόμενο"
 skip_confirm = "Σίγουρα θέλετε να παραλείψετε αυτό το βήμα;"
@@ -859,10 +890,12 @@ sms_success = "Αποστολή μυνήματος επιτυχής"
 Software = "Λογισμικό"
 Sorry, but the help you requested is unavailable in your language. = "Το θέμα της βοήθειας που ψάχνετε δεν είναι διαθέσιμο για τη γλώσσα σας."
 Sort = "Ταξινόμηση"
+sort_alphabetic = "Αλφαβητικά"
 sort_author = "Με συγγραφέα"
 sort_author_author = "Αλφαβητικά"
 sort_author_relevance = "Με δημοφιλία"
 sort_callnumber = "Με ταξινομικό"
+sort_count = "Σύνολο αποτελεσμάτων"??"
 sort_relevance = "Με σχετικότητα"
 sort_title = "Με τίτλο"
 sort_year = "Με ημερομηνία (φθ.)"
@@ -924,6 +957,7 @@ summon_database_recommendations = "Πρόσθετες πηγές:"
 Supplements = "Παραρτήματα"
 Supplied by Amazon = "Παρέχεται από το Amazon"
 Switch view to = "Αλλαγή προβολής σε"
+switchquery_fuzzy = "Εκτελώντας μια ασαφή αναζήτηση (fuzzy search) μπορείτε να βρείτε όρους με συναφή ορθογραφία"
 switchquery_intro = "Η τροποποίηση των όρων αναζήτησης μπορεί να δώσει περισσότερα αποτελέσματα."
 switchquery_lowercasebools = "Για να χρησιμοποιήσετε λογικούς τελεστές θα πρέπει να τους εισάγετε με κεφαλαία"
 switchquery_truncatechar = "Χρησιμοποιήστε λιγότερους όρους αναζήτησης για περισσότερα αποτελέσματα"
@@ -1006,9 +1040,6 @@ View this record in EBSCOhost = "Προβολή αυτής της εγγραφή
 visual_facet_parent = "από"
 Volume = "Τόμος"
 Volume Holdings = "Τεκμήρια τόμου"
-vudl_access_denied = "Δεν επιτρέπεται η πρόσβαση."
-vudl_tab_docs = "Έγγραφα"
-vudl_tab_pages = "Σελίδες"
 VuFind Configuration = "Ρυθμίσεις VuFind"
 vufind_upgrade_fail = "Αυτή τη στιγμή δεν είναι δυνατή η αναβάθμιση του VuFind"
 Warning: These citations may not always be 100% accurate = "Πρόσοχή: Οι παραπομπές μπορεί να μην είναι 100% ακριβείς"
diff --git a/languages/en.ini b/languages/en.ini
index f3b280de00dea8f563bb10402c03dba250e8e15e..6fc1a171ba9f0160e222abf23c6e31bc0993406e 100644
--- a/languages/en.ini
+++ b/languages/en.ini
@@ -3,6 +3,7 @@
 Abstract = "Abstract"
 Access = "Access"
 Access URL = "Access URL"
+access_denied = "Access denied."
 Accession Number = "Accession Number"
 Account = "Account"
 Add = "Add"
@@ -59,8 +60,10 @@ An error occurred during execution; please try again later. = "An error occurred
 AND = "AND"
 anonymous_tags = "Anonymous Tags"
 APA Citation = "APA Citation"
+applied_filter = "Applied Filter"
 Article = "Article"
 Ask a Librarian = "Ask a Librarian"
+Associated country = "Associated country"
 Audience = "Audience"
 Audio = "Audio"
 authentication_error_admin = "We cannot log you in at this time. Please contact your system administrator for assistance."
@@ -75,6 +78,7 @@ Author Browse = "Author Browse"
 Author Notes = "Author Notes"
 Author Results for = "Author Results for"
 Author Search Results = "Author Search Results"
+Authority File = "Authority File"
 Authors = "Authors"
 Authors Related to Your Search = "Authors Related to Your Search"
 Auto configuration is currently disabled = "Auto configuration is currently disabled"
@@ -238,6 +242,8 @@ Create New Password = "Create New Password"
 Created = "Created"
 Database = "Database"
 Date = "Date"
+Date of birth = "Date of birth"
+Date of death = "Date of death"
 date_day_placeholder = "D"
 date_from = "From"
 date_month_placeholder = "M"
@@ -254,6 +260,7 @@ delete_list = "Delete List"
 delete_page = "Delete Page"
 delete_selected = "Delete Selected"
 delete_selected_favorites = "Delete Selected Favorites"
+delete_tag = "Delete Tag"
 delete_tags = "Delete Tags"
 delete_tags_by = "Delete Tags By"
 Department = "Department"
@@ -265,6 +272,7 @@ Displaying the top = "Displaying the top"
 Document Inspector = "Document Inspector"
 Document Type = "Document Type"
 DOI = "DOI"
+Draw Search Box = "Draw Search Box"
 Due = "Due"
 Due Date = "Due Date"
 DVD = "DVD"
@@ -348,13 +356,14 @@ fav_email_fail = "Sorry, an error has occurred. Your favorite(s) were not emaile
 fav_email_missing = "Some data was missing. Your favorite(s) were not emailed."
 fav_email_success = "Your favorite(s) were emailed as requested."
 fav_export = "Export Favorites"
-fav_list_delete = "List has been deleted"
-fav_list_delete_cancel = "This list was not deleted"
+fav_list_delete = "List has been deleted."
+fav_list_delete_cancel = "This list was not deleted."
 fav_list_delete_fail = "Sorry, an error has occurred. Your list was not deleted."
 Favorites = "Saved Items"
 Fee = "Fee"
 Feedback = "Feedback"
 feedback_name = "Name"
+Field of activity = "Field of activity"
 File Description = "File Description"
 Filter = "Filter"
 filter_tags = "Filter Tags"
@@ -378,7 +387,9 @@ From = "From"
 Full description = "Full description"
 Full text is not displayed to guests = "Full text is not displayed to guests."
 fulltext_limit = "Limit to articles with full text available"
+Gender = "Gender"
 Genre = "Genre"
+Geographic Search = "Geographic Search"
 Geographic Terms = "Geographic Terms"
 Geography = "Geography"
 Get full text = "Get full text"
@@ -431,6 +442,7 @@ hold_error_blocked = "You do not have sufficient privileges to place a hold on t
 hold_error_fail = "Your request failed. Please contact the circulation desk for further assistance"
 hold_invalid_pickup = "An invalid pick up location was entered. Please try again"
 hold_invalid_request_group = "An invalid hold request group was entered. Please try again"
+hold_items_available = "Cannot place a hold because items are available."
 hold_login = "for hold and recall information"
 hold_place = "Place Request"
 hold_place_fail_missing = "Your request failed. Some data was missing. Please contact the circulation desk for further assistance"
@@ -679,6 +691,7 @@ Number = "Number"
 number_decimal_point = "."
 number_thousands_separator = ","
 OAI Server = "OAI Server"
+Occupation = "Occupation"
 of = "of"
 old_password = "Old Password"
 On Reserve = "On Reserve"
@@ -692,6 +705,7 @@ operator_exact = "is (exact)"
 OR = "OR"
 or create a new list = "or create a new list"
 original = "Original"
+Other associated place = "Other associated place"
 Other Authors = "Other Authors"
 Other Editions = "Other Editions"
 Other Libraries = "Other Libraries"
@@ -704,6 +718,8 @@ password_error_invalid = "New password is invalid (e.g. contains invalid charact
 password_error_not_unique = "Password was not changed"
 password_maximum_length = "Maximum password length is %%maxlength%% characters"
 password_minimum_length = "Minimum password length is %%minlength%% characters"
+password_only_alphanumeric = "Numbers and letters A-Z only"
+password_only_numeric = "Numbers only"
 Passwords do not match = "Passwords do not match"
 Past = "Past"
 PDF Full Text = "PDF Full Text"
@@ -715,6 +731,8 @@ Physical Description = "Physical Description"
 Physical Object = "Physical Object"
 pick_up_location = "Pickup Location"
 Place a Hold = "Place a Hold"
+Place of birth = "Place of birth"
+Place of death = "Place of death"
 Playing Time = "Playing Time"
 Please check back soon = "Please check back soon"
 Please contact the Library Reference Department for assistance = "Please contact the Library Reference Department for assistance"
@@ -761,6 +779,7 @@ Recall This = "Recall This"
 recaptcha_not_passed = "CAPTCHA not passed"
 Record Citations = "Record Citations"
 Record Count = "Record Count"
+Record Type = "Record Type"
 Recover Account = "Recover Account"
 recovery_by_email = "Recover by email"
 recovery_by_username = "Recover by username"
@@ -805,6 +824,7 @@ Requests = "Requests"
 Reserves = "Reserves"
 Reserves Search = "Reserves Search"
 Reserves Search Results = "Reserves Search Results"
+result_count = "%%count%% results"
 Results = "Results"
 results = "results"
 Results for = "Results for"
@@ -856,6 +876,8 @@ Serial = "Serial"
 Series = "Series"
 Set = "Set"
 Showing = "Showing"
+sidebar_close = "Collapse Sidebar"
+sidebar_expand = "Expand Sidebar"
 Similar Items = "Similar Items"
 Skip to content = "Skip to content"
 skip_confirm = "Are you sure you want to skip this step?"
@@ -869,10 +891,12 @@ sms_success = "Message sent."
 Software = "Software"
 Sorry, but the help you requested is unavailable in your language. = "Sorry, but the help you requested is unavailable in your language."
 Sort = "Sort"
+sort_alphabetic = "Alphabetical"
 sort_author = "Author"
 sort_author_author = "Alphabetical"
 sort_author_relevance = "Popularity"
 sort_callnumber = "Call Number"
+sort_count = "Result Count"
 sort_relevance = "Relevance"
 sort_title = "Title"
 sort_year = "Date Descending"
@@ -1017,9 +1041,6 @@ View this record in EBSCOhost = "View this record in EBSCOhost"
 visual_facet_parent = "From"
 Volume = "Volume"
 Volume Holdings = "Volume Holdings"
-vudl_access_denied = "Access denied."
-vudl_tab_docs = "Docs"
-vudl_tab_pages = "Pages"
 VuFind Configuration = "VuFind Configuration"
 vufind_upgrade_fail = "We cannot upgrade VuFind at this time"
 Warning: These citations may not always be 100% accurate = "Warning: These citations may not always be 100% accurate"
diff --git a/languages/es.ini b/languages/es.ini
index 7d875252c155be2f46936717b0ed144e9b85f312..dc37f0512a5c71afa0be975038794fde25ce7aef 100644
--- a/languages/es.ini
+++ b/languages/es.ini
@@ -4,6 +4,7 @@
 Abstract = "Resumen"
 Access = "Acceso"
 Access URL = "URL de Acceso"
+access_denied = "Acceso Denegado"
 Accession Number = "Número de Acceso"
 Account = "Cuenta"
 Add = "Agregar"
@@ -60,8 +61,10 @@ An error occurred during execution; please try again later. = "Un error ha ocurr
 AND = "Y"
 anonymous_tags = "Etiquetas Anónimas"
 APA Citation = "Cita APA"
+applied_filter = "Filtro Aplicado"
 Article = "Artículo"
 Ask a Librarian = "Consulte a un Bibliotecario"
+Associated country = "País asociado"
 Audience = "Público"
 Audio = "Audiom"
 authentication_error_admin = "No podemos iniciar su sesión en este momento. Por favor contacte a su Administrador del Sistema."
@@ -76,6 +79,7 @@ Author Browse = "Autor alfabético"
 Author Notes = "Notas de Autor"
 Author Results for = "Resultados por Autor"
 Author Search Results = "Resultados de Búsqueda por Autor"
+Authority File = "Archivo de autoridad"
 Authors = "Autores"
 Authors Related to Your Search = "Autores Relacionados"
 Auto configuration is currently disabled = "Configuración automática deshabilitada"
@@ -239,6 +243,8 @@ Create New Password = "Crear Nueva Contraseña"
 Created = "Creado"
 Database = "Base de Datos"
 Date = "Fecha"
+Date of birth = "Fecha de nacimiento"
+Date of death = "Fecha de fallecimiento"
 date_day_placeholder = "D"
 date_from = "De"
 date_month_placeholder = "M"
@@ -255,6 +261,7 @@ delete_list = "Borrar Lista"
 delete_page = "Borrar Página"
 delete_selected = "Borrar Seleccionados"
 delete_selected_favorites = "Borrar Favoritos Seleccionados"
+delete_tag = "Borrar etiqueta"
 delete_tags = "Borrar Etiquetas"
 delete_tags_by = "Borrar Etiquetas por"
 Department = "Departamento"
@@ -266,6 +273,7 @@ Displaying the top = "Mostrando la parte superior"
 Document Inspector = "Inspector de documentos"
 Document Type = "Tipo de Documento"
 DOI = "DOI"
+Draw Search Box = "Dibujar cajón de búqueda"
 Due = "Vencimiento"
 Due Date = "Fecha de Vencimiento"
 DVD = "DVD"
@@ -349,13 +357,14 @@ fav_email_fail = "Lo sentimos, un error ha ocurrido. Sus favoritos no fueron env
 fav_email_missing = "Faltaron algunos datos. Sus favoritos no fueron enviados por correo electrónico."
 fav_email_success = "Sus favoritos fueron enviados por correo electrónico conforme a lo solicitado."
 fav_export = "Exportar favoritos"
-fav_list_delete = "Su lista favorita ha sido borrada"
-fav_list_delete_cancel = "Esta lista no fue borrada"
+fav_list_delete = "Su lista favorita ha sido borrada."
+fav_list_delete_cancel = "Esta lista no fue borrada."
 fav_list_delete_fail = "Lo sentimos, un error ha ocurrido. Su lista no ha sido borrada."
 Favorites = "Favoritos"
 Fee = "Cuota"
 Feedback = "Comentarios"
 feedback_name = "Nombre"
+Field of activity = "Campo de actividad"
 File Description = "Descripción del Archivo"
 Filter = "Filtro"
 filter_tags = "Filtro de Etiquetas"
@@ -379,7 +388,9 @@ From = "De"
 Full description = "Descripción completa"
 Full text is not displayed to guests = "El texto completo no se muestra a los invitados"
 fulltext_limit = "Limitar a artículos con texto completo disponible"
+Gender = "Género"
 Genre = "Género"
+Geographic Search = "Búsqueda Geográfica"
 Geographic Terms = "Términos Geográficos"
 Geography = "Geografía"
 Get full text = "Enlace del recurso"
@@ -679,6 +690,7 @@ Number = "Número"
 number_decimal_point = "."
 number_thousands_separator = ","
 OAI Server = "Servidor OAI"
+Occupation = "Ocupación"
 of = "de"
 old_password = "Contraseña anterior"
 On Reserve = "En Reserva"
@@ -692,6 +704,7 @@ operator_exact = "es (exacto)"
 OR = "O"
 or create a new list = "O cree una lista nueva"
 original = "Original"
+Other associated place = "Otro lugar asociado"
 Other Authors = "Otros Autores"
 Other Editions = "Otras Ediciones"
 Other Libraries = "Otras Bibliotecas"
@@ -704,6 +717,8 @@ password_error_invalid = "Nueva contraseña es inválida (p.e. contiene caracter
 password_error_not_unique = "La contraseña no se ha cambiado"
 password_maximum_length = "Longitud máxima de la contraeña son %%maxlength%% caracteres"
 password_minimum_length = "Longitud mínima de la contraeña son %%minlength%% caracteres"
+password_only_alphanumeric = "Sólo números y letras A-Z"
+password_only_numeric = "Sólo números"
 Passwords do not match = "La Contraseña no coincide"
 Past = "Pasado"
 PDF Full Text = "Texto Completo PDF"
@@ -715,6 +730,8 @@ Physical Description = "Descripción Física"
 Physical Object = "Objeto Físico"
 pick_up_location = "Ubicación de entrega"
 Place a Hold = "Hacer reserva"
+Place of birth = "Lugar de nacimiento"
+Place of death = "Lugar de fallecimiento"
 Playing Time = "Tiempo de Juego"
 Please check back soon = "Por favor, vuelva pronto"
 Please contact the Library Reference Department for assistance = "Por favor contacte al Referencista para asistencia"
@@ -761,6 +778,7 @@ Recall This = "Recordar esto"
 recaptcha_not_passed = "CAPTCHA inválida"
 Record Citations = "Registro de Citas"
 Record Count = "Conteo de registro"
+Record Type = "Tipo de registro"
 Recover Account = "Recuperar cuenta"
 recovery_by_email = "Recuperar por correo electrónico"
 recovery_by_username = "Recuperar por nombre de usuario"
@@ -805,6 +823,7 @@ Requests = "Solicitar"
 Reserves = "Reservas"
 Reserves Search = "Buscar Reservas"
 Reserves Search Results = "Resultado de Búsquedas de Reservas"
+result_count = "%%count%% resultados"
 Results = "Resultados"
 results = "Resultados"
 Results for = "Resultados para"
@@ -856,6 +875,8 @@ Serial = "Seriadas"
 Series = "Series"
 Set = "Ingresar"
 Showing = "Mostrando"
+sidebar_close = "Contraer barra lateral"
+sidebar_expand = "Expander barra lateral"
 Similar Items = "Ejemplares similares"
 Skip to content = "Saltar al contenido"
 skip_confirm = "Seguro que quiere omitir este paso"
@@ -869,10 +890,12 @@ sms_success = "Mensaje enviado."
 Software = "Software"
 Sorry, but the help you requested is unavailable in your language. = "Lo sentimos, la ayuda solicitada no está dispobible en su idioma."
 Sort = "Ordenar"
+sort_alphabetic = "Alfabético"
 sort_author = "Autor"
 sort_author_author = "Alfabético"
 sort_author_relevance = "Popularidad"
 sort_callnumber = "Número de clasificación"
+sort_count = "Resultados del conteo"
 sort_relevance = "Relevancia"
 sort_title = "Título"
 sort_year = "Fecha Descendente"
@@ -1017,9 +1040,6 @@ View this record in EBSCOhost = "Ver este registro en EBSCOhost"
 visual_facet_parent = "De"
 Volume = "Volumen"
 Volume Holdings = "Volumen de existencias"
-vudl_access_denied = "Acceso Denegado"
-vudl_tab_docs = "Documentos"
-vudl_tab_pages = "Páginas"
 VuFind Configuration = "Configuración de VuFind"
 vufind_upgrade_fail = "No podemos actualizar VuFind en este momento"
 Warning: These citations may not always be 100% accurate = "Precaución: Estas citas no son 100% exactas"
diff --git a/languages/eu.ini b/languages/eu.ini
index 02cf73a1d736bf6f59c862da2c7b7a97eaed34ba..cabf58dd132ff99f788e5bcdd3811ad53124bec9 100644
--- a/languages/eu.ini
+++ b/languages/eu.ini
@@ -623,8 +623,12 @@
 ;Zuni = Zuni
 Abstract = "Laburpena"
 Access = "Sartu"
+Access URL = "URL sarbidea"
+access_denied = "Ez dago sartzerik"
+Accession Number = "Sartzeko zenbakia"
 Account = "Kontua"
 Add = "Erantsi"
+Add a Library Card = "Gehitu liburutegirako txartela"
 Add a Note = "Ohar bat erantsi"
 Add Tag = "Etiketa erantsi"
 Add Tags = "Etiketak erantsi"
@@ -664,16 +668,23 @@ Advanced Search = "Bilaketa aurreratua"
 advSearchError_noRights = "Baimenik gabe"
 advSearchError_notAdvanced = "Errorea"
 advSearchError_notFound = "Ez da aurkitu"
+ajaxview_label_information = "Argibidea"
+ajaxview_label_tools = "Tresnak"
 All = "Guztiak"
 All Fields = "Eremu guztiak"
 All Pages Loaded = "Orri guztiak kargatuta"
 All Text = "Testu osoa"
 alphabrowse_matches = "Emaitzak"
+alphabrowselink_html = "Bilatu %%index%% hasiera honekin <a href="%%url%%">%%from%%</a>."
 An error has occurred = "Errore bat gertatu da"
+An error occurred during execution; please try again later. = "Errorea izan zen egikaritu zen bitartean; mesedez saiatu berriro."
 AND = "ETA"
 anonymous_tags = "Etiketa anonimoak"
 APA Citation = "APA aipamena"
+applied_filter = "Iragazkia aplikatuta"
+Article = "Artikulua"
 Ask a Librarian = "Galdetu liburuzainari"
+Associated country = "Herri elkartua"
 Audience = "Hartzaileak"
 Audio = "Entzunezkoa"
 authentication_error_admin = "Momentu honetan ezin dugu zure saioa ireki. Mesedez, administratzailearekin harremanetan jarri."
@@ -688,12 +699,14 @@ Author Browse = "Egilea arakatu"
 Author Notes = "Notas de Autor"
 Author Results for = "Egile-bilaketaren emaitzak"
 Author Search Results = "Egile-bilaketaren emaitzak"
+Authority File = "Autoritate fitxategia"
 Authors = "Egileak"
 Authors Related to Your Search = "zure egile-bilaketaren antzekoak"
 Auto configuration is currently disabled = "Autokonfigurazioa desgaituta dago momentu honetan"
 auto_configure_description = "Instalazioa berria bada, agian zuzen ezazu arazoa VuFind's Auto Configure tresna erabiliz."
 auto_configure_disabled = "Autokofigurazioa desgaituta dago."
 auto_configure_title = "Auto Configure"
+Availability = "Erabilgarritasuna"
 Available = "Eskuragarri"
 Available Functionality = "Funtzionalitate erabilgarri"
 Awards = "Sariak"
@@ -707,8 +720,10 @@ Be the first to leave a comment = "Izan zaitez lehena ohar bat uzten"
 Be the first to tag this record = "Izan zaitez lehena erregistro honi etiketa jartzen"
 Bibliographic Details = "Xehetasun bibliografikoak"
 Bibliography = "Bibliografia"
+Blu-ray Disc = "Blu-ray Diskoa"
 Book = "Liburua"
 Book Bag = "Liburu-poltsa"
+Book Chapter = "Liburu kapitulua"
 Book Cover = "Azala"
 bookbag_confirm_empty = "Seguro que quiere vaciar su Mochila?"
 bookbag_delete = "Borrar Itemes seleccionados de la Mochila"
@@ -725,6 +740,7 @@ bookbag_save = "Guardar Itemes seleccionados de la Mochila"
 bookbag_save_selected = "Guardar Seleccionados"
 Bookmark = "Sare sozialak"
 Books = "Liburuak"
+Borrowing Location = "Maileguaren kokapena"
 Braille = "Braille"
 Brief View = "Bista laburra"
 Browse = "Arakatu"
@@ -783,6 +799,8 @@ Checked Out = "Maileguan"
 Checked Out Items = "Mailegatutako aleak"
 Checkedout = "Maileguan"
 Chicago Citation = "Chicago Style aipamena"
+child_record_count = "%%count%% erregistroak"
+child_records = "Edukiak/piezak"
 Choose a Category to Begin Browsing = "Aukeratu kategoria bat bilatzen hasteko"
 Choose a Column to Begin Browsing = "Katalogoa arakatzen hasteko zutabe bat aukeratu"
 Choose a List = "Aukera ezazu zerrenda bat"
@@ -793,6 +811,7 @@ citation_singlepage_abbrev = "orr."
 citation_volume_abbrev = "Lib."
 Cite this = "Erreferentzia bihurtu"
 City = "Hiria"
+Clear = "Garbitu"
 clear_tag_filter = "Ezabatu iragazkia"
 close = "Itxi"
 Code = "Kodea"
@@ -810,6 +829,8 @@ Company/Entity = "Konpainia/Entitatea"
 Configuration = "Konfigurazioa"
 confirm_delete = "Ziur zaude honako hau ezabatu nahi duzula?"
 confirm_delete_brief = "Ezabatu item-a?"
+confirm_delete_library_card_brief = "Liburutegirako txartela ezabatu?"
+confirm_delete_library_card_text = "Ziur zaude liburutegirako txartel hau ezabatzeaz?"
 confirm_delete_list_brief = "Ezabatu zerrenda?"
 confirm_delete_list_text = "Ziur zaude honako zerrenda hau ezabatu nahi duzula?"
 confirm_delete_tags_brief = "Baietsi etiketen ezabapena"
@@ -824,10 +845,13 @@ confirm_storage_retrieval_request_cancel_all_text = "Zure biltegiratzea berresku
 confirm_storage_retrieval_request_cancel_selected_text = "Aukeratutako biltegiratzea berreskuratzeko eskaerak ezeztatu nahi dituzu?"
 conjunction_or = "edo"
 Contents = "Edukia"
+Contributing Source = "Ekarpen-iturria"
 Contributors = "Egileak"
 Copies = "Kopiak"
 Copy = "Alea"
+Copyright = "Copyright"
 Corporate Author = "Erakunde egilea"
+Corporate Authors = "Egile korporatiboa"
 Country = "Herria"
 Course = "Ikastaroa"
 Course Reserves = "Erreserba egin ezazu"
@@ -839,6 +863,8 @@ Create New Password = "Sortu pasahitza berria"
 Created = "Sortua"
 Database = "Datu-basea"
 Date = "Data"
+Date of birth = "Jaiotze data"
+Date of death = "Heriotza data"
 date_day_placeholder = "D"
 date_from = "De"
 date_month_placeholder = "M"
@@ -855,6 +881,7 @@ delete_list = "Zerrenda ezabatu"
 delete_page = "Ezabatu orria"
 delete_selected = "Aukeratutakoak ezabatu"
 delete_selected_favorites = "Gogokoenak ezabatu"
+delete_tag = "Ezabatu etiketa"
 delete_tags = "Ezabatu etiketak"
 delete_tags_by = "Ezabatu honetarako etiketak"
 Department = "Departamentua"
@@ -864,11 +891,15 @@ Detailed View = "Bista xehatua"
 Details = "Xehetasunak"
 Displaying the top = "Goiko aldea aurkezten ari da"
 Document Inspector = "Dokumentu Inspektorea"
+Document Type = "Dokumentu mota"
+DOI = "DOI"
+Draw Search Box = "Marraztu Bilatze Kaxa"
 Due = "Noiz arte"
 Due Date = "Itzulketa data"
 DVD = "DVD"
 eBook = "eBook"
 Edit = "Editatu"
+Edit Library Card = "Editatu Liburutegirako Txartela"
 Edit this Advanced Search = "Bilaketa editatu"
 edit_list = "Zerrenda editatu"
 edit_list_fail = "Sentitzen dugu, ez zaude baimenduta editatzeko"
@@ -897,6 +928,8 @@ Email this = "Bidali"
 Email this Search = "Emaitzak posta elektronikoz bidali"
 email_failure = "Errorea mezua bidaltzean"
 email_link = "Lotura"
+email_maximum_recipients_note = "Gehienez %%max%% jasotzaile baimentzen dira."
+email_multiple_recipients_note = "Jasotzaile batzuk erants ditzakezu komaz bereiztuak."
 email_selected = "Aukeratutakoak bidali"
 email_selected_favorites = "Aukeratutakoak bidali"
 email_sending = "Posta elektronikoa bidaltzen"
@@ -944,13 +977,15 @@ fav_email_fail = "Sentitzen dugu, zure gogokoenak ez dira posta elektronikoz bid
 fav_email_missing = "Datu batzuk falta dira. Zure gogokoenak ez dira posta elektronikoz bidali."
 fav_email_success = "Zure gogokoenak posta elektonikoz bidali dira."
 fav_export = "Gogokoenak esportatu"
-fav_list_delete = "Zure gogoen zerrenda ezabatu da"
-fav_list_delete_cancel = "Zerrenda hori ez da ezabatu"
+fav_list_delete = "Zure gogoen zerrenda ezabatu da."
+fav_list_delete_cancel = "Zerrenda hori ez da ezabatu."
 fav_list_delete_fail = "Errorea gertatu da. Ez dugu zure zerrenda ezabatu."
 Favorites = "Gogokoenak"
 Fee = "Kuota"
 Feedback = "Feedback"
 feedback_name = "Feedback-Izena"
+Field of activity = "Jarduera-eremua"
+File Description = "Fitxategi deskribapena"
 Filter = "Iragazkia"
 filter_tags = "Iragazteko etiketak"
 filter_wildcard = "Edozein"
@@ -965,6 +1000,7 @@ First = "Lehengoa"
 First Name = "Izena"
 fix_metadata = "Bai, konpondu metadata; Itxarongo dut"
 for search = "bilaketa honetara"
+Forgot Password = "Pasahitza ahaztuta"
 Form Submitted! = "Inprimakia bidalita!"
 Format = "Formatua"
 found = "Aurkituta"
@@ -972,7 +1008,9 @@ From = "Nork"
 Full description = "Deskribapen osoa"
 Full text is not displayed to guests = "Testu osoa ez da erakusten erabiltzaile gonbidatuei"
 fulltext_limit = "Testu osoko artikuluei mugatu"
+Gender = "Generoa"
 Genre = "Generoa"
+Geographic Search = "Bilaketa geografikoa"
 Geographic Terms = "Termino geografikoak"
 Geography = "Geografia"
 Get full text = "Testu osoa"
@@ -980,6 +1018,7 @@ Get RSS Feed = "RSS"
 Globe = "Global"
 Go = "Joan"
 Go to Standard View = "Ikuspegi arruntara joan"
+go_to_list = "Joan zerrendara"
 google_map_cluster = "Grupo"
 google_map_cluster_points = "Cluster Puntuak"
 Grid = "Sareta"
@@ -990,6 +1029,7 @@ Has Illustrations = "Irudiduna"
 Help = "Laguntza"
 Help with Advanced Search = "Bilaketa aurreratuan lagundu"
 Help with Search Operators = "Eragileak nola erabili"
+help_page_missing = "Laguntza orrialdea ez da existitzen."
 hierarchy_hide_tree = "Ezkutatu Hierarkia Osoa"
 hierarchy_show_tree = "Erakutsi Hierarkia Osoa"
 hierarchy_tree = "Testuingurua"
@@ -1083,6 +1123,7 @@ in = "non"
 In This Collection = "Bilduma honetan"
 in_collection_label = "Bilduma:"
 include_synonyms = "Erabili sinonimoak emaitzak zabaltzeko"
+Index Terms = "Indize terminoak"
 Indexes = "Indizeak"
 information = "Informazioa"
 Institution = "Erakundea"
@@ -1109,9 +1150,11 @@ items_added_to_bookbag = "Itemes agregados a su Mochila"
 items_already_in_bookbag = "ítemes están en su Mochila o podrían no estar añadidos."
 Journal = "Aldizkaria"
 Journal Articles = "Aldizkariko-artikuluak"
+Journal Info = "Aldizkariari buruzko informazioa"
 Journal Title = "Aldizkari izenburua"
 Journals = "Aldizkariak"
 Jump to = "Jauzi hona"
+just_cataloged = "Katalogatu berriak"
 Keyword = "Gako-hitza"
 Keyword Filter = "Gako-hitzaren iragazkia"
 Kit = "Kit"
@@ -1121,7 +1164,19 @@ Last = "Azkena"
 Last Modified = "Azken aldaketa"
 Last Name = "Abizena"
 less = "gutxiago"
+libphonenumber_invalid = "Telefono zenbakia ez da zuzena"
+libphonenumber_invalidcountry = "Herriko aurrezenbakia ez da zuzena"
+libphonenumber_invalidregion = "Herrialdeko aurrezenbakia ez da zuzena:"
+libphonenumber_notanumber = "The string supplied did not seem to be a phone number"
+libphonenumber_toolong = "Sartutako testu katea luzegia da telefono zenbakia izateko"
+libphonenumber_tooshort = "Sartutako testu katea motzegia da telefono zenbakia izateko"
+libphonenumber_tooshortidd = "Telefono zenbakia motzegia da IDD-en atzean"
 Library = "Liburutegia"
+Library Card = "Liburutegirako Txartela"
+Library Card Deleted = "Liburutegirako Txartela Ezabatuta"
+Library Card Name = "Liburutegirako Txartelaren Izena"
+Library Cards = "Liburutegirako Txartelak"
+Library Cards Disabled = "Liburutegirako Txartelak Desgaituta"
 Library Catalog Password = "Pasahitza"
 Library Catalog Profile = "Profila"
 Library Catalog Record = "Katalogoaren erregistroa"
@@ -1148,6 +1203,7 @@ login_disabled = "Login ez dago erabilgarri momentu honetan."
 login_target = "Liburutegia"
 Logout = "Irten"
 Main Author = "Egile nagusia"
+Main Authors = "Egile Nagusiak"
 Major Categories = "Kategoria nagusiak"
 Manage Tags = "Kudeatu etiketak"
 Manuscript = "Eskuizkribua"
@@ -1157,6 +1213,7 @@ map_results_label = "En esta ubicación:"
 Maps = "Mapak"
 Media Format = "Euskarri-formatua"
 medium = "Ertaina"
+MeSH Terms = "MeSH Terminoak"
 Message = "Mezua"
 Message From Sender = "Igorlearen mezua"
 Metadata Prefix = "Metadata Aurrizki"
@@ -1169,7 +1226,9 @@ More catalog results = "Emaitza gehiago"
 More options = "Aukera gehiago"
 More Summon results = "Eskatutako emaitza gehiago"
 More Topics = "Gai gehiago"
+more_authors_abbrev = "et al."
 more_info_toggle = "Erakutsi/Ezkutatu info. gehiago."
+more_topics = "%%count%% gai gehiago"
 Most Recent Received Issues = "Oharra"
 Multiple Call Numbers = "Klasifikazio zenbaki bat baino gehiago"
 Multiple Locations = "Ale bat baino gehiago"
@@ -1201,11 +1260,14 @@ No reviews were found for this record = "Erregistro honek ez du iruzkinik"
 No Tags = "Etiketarik gabe"
 no_description = "Deskribapenik ez dago."
 no_items_selected = "Ez duzu dokumenturik aukeratu"
+nohit_active_filters = "Fazeta iragazki bat edo gehiago erabili dira bilaketa honetarako. Iragazkirik ezabatu nahi baduzu, emaitza gehiago izango dituzu."
+nohit_change_tab = "Bilatu duzu "%%activeTab%%"-en. Nahi baduzu, bila dezakezu beste batzuetan:"
 nohit_filters = "Iragazkirik gabe"
 nohit_heading = "Ez dago emaitzarik"
 nohit_no_filters = "Ez da inongo iragazkirik jarri."
 nohit_parse_error = "Kontsultarekin arazoak daude. Saiatu berriro."
 nohit_prefix = "Ez dago emaitzarik"
+nohit_query_without_filters = "Ezabatu iragazki guztiak"
 nohit_spelling = "Ez dago iradokizunik"
 nohit_suffix = ". Berriz bila dezakezu datuak aldatuz."
 nohit_suggest = "Esaldia berrikusi."
@@ -1215,12 +1277,15 @@ Not On Reserve = "Ez dago erreserban"
 not_applicable = "n/a"
 Note = "Oharra"
 note_760 = "Serie nagusia"
+note_762 = "Azpiserie"
 note_765 = "Jatorrizko hizkuntzaren sarrera"
+note_767 = "Itzulpena"
 note_770 = "Gehigarri bereziaren oharra"
 note_772 = "Erregistro nagusiaren sarrera"
 note_773 = "Item nagusiaren sarrera"
 note_774 = "Unitate konstituitzailea"
 note_775 = "Beste argitalpenaren sarrera"
+note_776 = "Formulario gehigarria"
 note_777 = "Faszikuluaren sarrera"
 note_780_0 = "Aurretikoaren sarrera"
 note_780_1 = "Neurri batean jarraitzen du"
@@ -1237,15 +1302,21 @@ note_785_3 = "Ordeztuta parte batean"
 note_785_4 = "Xurgatuta"
 note_785_5 = "Xurgatuta parte batean"
 note_785_6 = "Zatituta"
+note_785_7 = "Bat eginda honekin / Formularioak"
 note_785_8 = "Aldatu berriro"
+note_787 = "Beste erlazio bat"
 Notes = "Oharrak"
 Number = "Zenbakia"
+number_decimal_point = ","
+number_thousands_separator = "."
 OAI Server = "OAI Zerbitzaria"
+Occupation = "Lanbidea"
 of = "--"
 old_password = "Aurreko pasahitza"
 On Reserve = "Erreserbatua"
 On Reserve - Ask at Circulation Desk = "Erreserbatua"
 on_reserve = "Reservas - Pregunte en Circulación"
+on_topic = "%%count%% item(ak) gai honi buruz"
 Online Access = "Sarrera elektronikoa"
 online_resources = "Testu osoa"
 operator_contains = "du"
@@ -1253,13 +1324,21 @@ operator_exact = "da (zehatza)"
 OR = "EDO"
 or create a new list = "edo sor ezazu zerrenda berri bat"
 original = "Jatorrizkoa"
+Other associated place = "Beste lotutako leku bat"
 Other Authors = "Beste egile batzuk"
 Other Editions = "Beste argitalpen batzuk"
 Other Libraries = "Beste liburutegi batzuk"
 Other Sources = "Beste baliabide batzuk"
+Page not found. = "Orrialdea ez da aurkitu."
 Password = "Erabiltzaile-zenbakia edo pasahitza"
 Password Again = "Idatzi berriz pasahitza"
 Password cannot be blank = "Pasahitza ezin da bete gabe utzi"
+password_error_invalid = "Pasahitza berria ez da zuzena(ad. karaktereak ez dira zuzenak)"
+password_error_not_unique = "Pasahitza ez da aldatu"
+password_maximum_length = "Pasahitzarako gehienezko luzapena da %%maxlength%% karaktere"
+password_minimum_length = "Pasahitzarako gutxieneko luzapena da %%minlength%% karaktere"
+password_only_alphanumeric = "Pasahitzak bakarrik A-Z onartzen du"
+password_only_numeric = "Bakarrik zenbakiak"
 Passwords do not match = "Pasahitza ez da egokia"
 Past = "Azken"
 PDF Full Text = "PDF testu osoa"
@@ -1271,6 +1350,8 @@ Physical Description = "Deskribapen fisikoa"
 Physical Object = "Objektu fisikoa"
 pick_up_location = "Emate kokapena"
 Place a Hold = "Erreserbatu"
+Place of birth = "Jaiotze data"
+Place of death = "Heriotza data"
 Playing Time = "Iraupena"
 Please check back soon = "Zuzena den egiaztatu"
 Please contact the Library Reference Department for assistance = "Jar zaitez liburutegiarekin harremanetan"
@@ -1290,16 +1371,21 @@ Private = "Pribatua"
 Production Credits = "Kredituak"
 Profile = "Profila"
 profile_update = "Eskatutakoaren arabera aktualizatu da"
+pronounced = "Ebakitua"
 Provider = "Hornitzailea"
 Public = "Publikoa"
 Publication = "Argitalpena"
+Publication Date = "Argitaratze data"
 Publication Frequency = "Argitaratze maiztasuna"
 Publication Information = "Argitalpenari buruzko informazioa"
 Publication Type = "Argitalpen mota"
+Publication Year = "Argitaratze urtea"
 Publication_Place = "Argitaratze lekua"
 Published = "Argitaratua"
 Published in = "Argitaratua izan da"
 Publisher = "Argitaratzailea"
+Publisher Information = "Argitaratzaile Informazioa"
+Publisher Permissions = "Argitaratzaile Baimenak"
 QR Code = "QR Kodea"
 qrcode_hide = "Ezkutatu QR Kodea"
 qrcode_show = "Erakutsi QR Kodea"
@@ -1312,6 +1398,8 @@ Recall This = "Jakinarazi"
 recaptcha_not_passed = "CAPTCHA ez da gainditu"
 Record Citations = "Dokumentuaren aipamena"
 Record Count = "Erregistro-zenbatzea"
+Record Type = "Erregistro mota"
+Recover Account = "Berrezkuratu kontua"
 recovery_by_email = "Berreskuratu posta elektronikoren bidez"
 recovery_by_username = "Berreskuratu erabiltzaile izenaren bidez"
 recovery_disabled = "Pasahitzaren berreskuratzea ezinezkoa da"
@@ -1355,7 +1443,9 @@ Requests = "Eskaerak"
 Reserves = "Erreserbak"
 Reserves Search = "Erresenben bilaketa"
 Reserves Search Results = "Signaturaren araberako bilaketaren emaitza"
+result_count = "%%count%% emaitzak"
 Results = "Emaitzak"
+results = "emaitzak"
 Results for = "Emaitzak"
 Results per page = "Orren araberako emaitza"
 Resumption Token = "Berrekitzeko Token-a"
@@ -1387,6 +1477,7 @@ search_NOT = "BAINA EZ"
 search_OR = "EDO"
 search_save_success = "Búsqueda guardada con éxito"
 search_unsave_success = "Búsqueda guardada ha sido eliminada con éxito"
+seconds_abbrev = "s"
 see all = "Guztiak ikusi"
 See also = "Ikus ere"
 Select this record = "Aukeratu erregistro hau"
@@ -1404,7 +1495,10 @@ Serial = "Aldizkako argitalpena"
 Series = "Saila"
 Set = "Aukera"
 Showing = "Erakusten"
+sidebar_close = "Itxi albokobarra"
+sidebar_expand = "Ireki albokobarra"
 Similar Items = "Antzeko izenburuak"
+Skip to content = "Joan edukira"
 skip_confirm = "Pauso hau sahiestu nahi duzula ziur zaude?"
 skip_fix_metadata = "Ez konpondu metadata momentu honetan."
 skip_step = "Sahiestu pauso hau"
@@ -1416,15 +1510,18 @@ sms_success = "Mezua bidalia."
 Software = "Software"
 Sorry, but the help you requested is unavailable in your language. = "Eskatutako laguntza ez dago zure hizkuntzan"
 Sort = "Antolatu"
+sort_alphabetic = "Alfabetikoa"
 sort_author = "Egilea"
 sort_author_author = "Alfabetikoa"
 sort_author_relevance = "Onarpena"
 sort_callnumber = "Sailkapena"
+sort_count = "Emaitzen kontaketa"
 sort_relevance = "Garrantzia"
 sort_title = "Izenburua"
 sort_year = "Berrienatik atzera"
 sort_year asc = "Zaharrenetik aurrera"
 Source = "Baliabidea"
+Source Title = "Baliabidearen titulua"
 spell_expand_alt = "Hedatu"
 spell_suggest = "Iradokizunak"
 Staff View = "MARC erregistroa"
@@ -1466,6 +1563,7 @@ storage_retrieval_request_year = "Urtea"
 Subcollection = "Azpibilduma"
 Subject = "Gaia"
 Subject Area = "Jakintza adarra"
+Subject Geographic = "Gai geografikoa"
 Subject Recommendations = "Gomendatutako gaiak"
 Subject Terms = "Gai terminoak"
 Subject(s) = "Gai(ak)"
@@ -1479,8 +1577,10 @@ summon_database_recommendations = "Errekurtso gehiago hemen aurki ditzakezu:"
 Supplements = "Gehigarriak"
 Supplied by Amazon = "Hornitzailea, Amazon"
 Switch view to = "Ikuspegia aldatu"
+switchquery_fuzzy = "Fuzzy bilaketa egiten, berdin letreiatzen diren terminoak bila ditzakezu "
 switchquery_intro = "Emaitza gehiago nahi baduzu, doitu zure kontsulta."
 switchquery_lowercasebools = "Operadore booleanoak letra larriz idatzi behar dira"
+switchquery_truncatechar = "Moztu zure bilaketa kontsulta emaitza gehiago izateko"
 switchquery_unwantedbools = "Y, O eta NO hitzak idatzi behar dituzu; erabili komatxoak"
 switchquery_unwantedquotes = "Komatxoak ezabatzeak bilaketa zabalagoa onartuko du"
 switchquery_wildcard = "Zernahitarako karaktereak erabil daitezke, hitz-aldaerak aurkitzeko"
@@ -1514,6 +1614,7 @@ Title not available = "Dokumentu hau ez dago liburutegian"
 Title View = "Izenburuaren bista"
 title_hold_place = "Egin eskaera izenbururako"
 To = "Nori"
+Too Many Email Recipients = "Jasotzaile gehiegi"
 too_many_favorites = "Lista muy larga para desplegar. Intente con sus favoritos en más de una lista o delimitar con etiquetas."
 too_many_new_items = "Demasiados nuevos ítemes para desplegar en una sola lista. Intente limitando su búsqueda."
 too_many_reserves = "Demasiadas reservas de cursos para desplegar en una sola lista. Intente limitando su búsqueda."
@@ -1528,10 +1629,12 @@ total_resources = "Baliabideen guztizkoa"
 total_saved_items = "Gordetako elementuen guztizkoa"
 total_tags = "Etiketen guztizkoa"
 total_users = "Erabiltzaileen guztizkoa"
+Transliterated Title = "Titulu letraldatua"
 tree_search_limit_reached_html = "Zure bilaketak aurkitu zituen erakusteko emaitza gehiegi. Erakusten dira bakarrik lehenengo <b>%%limit%%</b> items. Emaitza gehiago ikusteko, sakatu <a id="fullSearchLink" href="%%url%%" target="_blank">here.</a>"
 unique_tags = "Etiketa bakarrak"
 University Library = "Liburutegi unibertsitarioa"
 Unknown = "Ezezaguna"
+unrecognized_facet_label = "Beste batzuk"
 Upgrade VuFind = "Eguneratu VuFind"
 upgrade_description = "VuFind aurreko bertsio baterako eguneratzen ari bazara, karga dezakezu zure aurreko konfigurazioa tresna hau erabiliz."
 URL = "URL"
@@ -1540,6 +1643,7 @@ Use instead = "Honen ordez erabili"
 User Account = "Erabiltzaile"
 Username = "Izen eta 2 abizen"
 Username cannot be blank = "Erabiltzaile izena ezin da bete gabe utzi"
+Username is already in use in another library card = "Erabiltzaile izena beste txartel batean dago erabilita"
 VHS = "VHS"
 Video = "Bideoa"
 Video Clips = "Video Clips"
@@ -1556,9 +1660,6 @@ View this record in EBSCOhost = "Ikusi hau EBSCOhost-en"
 visual_facet_parent = "Nork"
 Volume = "Bolumena"
 Volume Holdings = "Liburukiak"
-vudl_access_denied = "Ez dago sartzerik"
-vudl_tab_docs = "Dok."
-vudl_tab_pages = "Orriak"
 VuFind Configuration = "Vufind-en konfigurazioa"
 vufind_upgrade_fail = "VuFind ezin da eguneratu momentu honetan"
 Warning: These citations may not always be 100% accurate = "Kontuz: berrikusi erreferentzia hauek erabili aurretik"
@@ -1577,6 +1678,7 @@ You do not have any fines = "Ez daukazu isunik"
 You do not have any holds or recalls placed = "Ez daukazu erreserbarik"
 You do not have any interlibrary loan requests placed = "Ez duzu eskatu liburutegi arteko mailegurik"
 You do not have any items checked out = "Ez daukazu mailegurik"
+You do not have any library cards = "Ez duzu liburutegirako txartelik"
 You do not have any saved resources = "Ez daukazu gordetako erregistrorik"
 You do not have any storage retrieval requests placed = "Ez duzu eskatu biltegiratze berreskuratzerik"
 You must be logged in first = "Lehenik saioa ireki behar duzu"
@@ -1591,5 +1693,6 @@ Your Lists = "Zure zerrendak"
 Your Profile = "Zure profila"
 Your search terms = "Bilatu dituzun hitzak"
 Your Tags = "Zure etiketak"
+your_match_would_be_here = "Zure emaitza hemen izan daiteke."
 Zip = "Posta kodea"
 zoom = "Zoom"
diff --git a/languages/fi.ini b/languages/fi.ini
index 90f1fa714a74c6621df4045a2d878c81d1731e53..0ce6e619cc538fed22433aedeee075f118020e1a 100644
--- a/languages/fi.ini
+++ b/languages/fi.ini
@@ -2,6 +2,7 @@
 Abstract = "Abstrakti"
 Access = "Pääsy"
 Access URL = "Linkki aineistoon"
+access_denied = "Pääsy estetty."
 Accession Number = "Hankintanumero"
 Account = "Tili"
 Add = "Lisää"
@@ -58,8 +59,10 @@ An error occurred during execution; please try again later. = "Virhe toimintoa s
 AND = "JA"
 anonymous_tags = "Tuntemattomien tagit"
 APA Citation = "APA-viite"
+applied_filter = "Käytetty suodatus"
 Article = "Artikkeli"
 Ask a Librarian = "Kysy kirjastosta"
+Associated country = "Liittyvä maa"
 Audience = "Yleisö"
 Audio = "Ääni"
 authentication_error_admin = "Sisäänkirjautuminen epäonnistui. Ota yhteyttä järjestelmän ylläpitäjään."
@@ -74,6 +77,7 @@ Author Browse = "Tekijäselaus"
 Author Notes = "Tekijähuomautukset"
 Author Results for = "Tekijätulokset haulle"
 Author Search Results = "Tekijähaun tulokset"
+Authority File = "Auktoriteetin lähde"
 Authors = "Tekijät"
 Authors Related to Your Search = "Hakuun liittyvät tekijät"
 Auto configuration is currently disabled = "Automaattinen asennus on pois päältä"
@@ -235,8 +239,16 @@ Create a List = "Lisää uusi"
 Create New Account = "Uusi tili"
 Create New Password = "Uusi salasana"
 Created = "Luotu"
+CreatorRoles::abr = "Lyhentäjä"
+CreatorRoles::acp = "Art copyist"
+CreatorRoles::adi = "Lavastaja"
+CreatorRoles::anl = "Analyytikko"
+CreatorRoles::arc = "Arkkitehti"
+CreatorRoles::bnd = "Sitoja"
 Database = "Tietokanta"
 Date = "Päiväys"
+Date of birth = "Syntymäaika"
+Date of death = "Kuolinaika"
 date_day_placeholder = "P"
 date_from = "Alkaen"
 date_month_placeholder = "K"
@@ -253,6 +265,7 @@ delete_list = "Poista lista"
 delete_page = "Poista sivu"
 delete_selected = "Poista valitut"
 delete_selected_favorites = "Poista valitut suosikit"
+delete_tag = "Poista tagi"
 delete_tags = "Poista tagit"
 delete_tags_by = "Poista tagit käyttäjältä"
 Department = "Osasto"
@@ -264,6 +277,7 @@ Displaying the top = "Näytetään ensimmäiset"
 Document Inspector = "Dokumentin tutkija"
 Document Type = "Dokumentin tyyppi"
 DOI = "DOI"
+Draw Search Box = "Valitse alue"
 Due = "Eräpäivä"
 Due Date = "Eräpäivä"
 DVD = "DVD"
@@ -347,13 +361,14 @@ fav_email_fail = "Tapahtui virhe. Suosikkejasi ei lähetetty sähköpostilla."
 fav_email_missing = "Puuttuvat tiedot. Suosikkejasi ei lähetetty sähköpostilla."
 fav_email_success = "Suosikit lähetetty sähköpostilla."
 fav_export = "Vie suosikit"
-fav_list_delete = "Suosikkilista poistettu"
+fav_list_delete = "Suosikkilista poistettu."
 fav_list_delete_cancel = "Listaa ei poistettu."
 fav_list_delete_fail = "Tapahtui virhe. Listaa ei poistettu."
 Favorites = "Suosikit"
 Fee = "Maksu"
 Feedback = "Palaute"
 feedback_name = "Nimi"
+Field of activity = "Toimiala"
 File Description = "Tiedoston kuvaus"
 Filter = "Suodatin"
 filter_tags = "Suodata tageja"
@@ -377,7 +392,9 @@ From = "Lähettäjä"
 Full description = "Täydet tiedot"
 Full text is not displayed to guests = "Kokotekstiä ei näytetä vieraille."
 fulltext_limit = "Rajaa kokotekstiartikkeleihin"
+Gender = "Sukupuoli"
 Genre = "Genre"
+Geographic Search = "Maantieteellinen haku"
 Geographic Terms = "Maantieteelliset termit"
 Geography = "Maantieteellinen"
 Get full text = "Hae kokoteksti"
@@ -430,6 +447,7 @@ hold_error_blocked = "Varaaminen ei ole mahdollista, koska olet lainauskiellossa
 hold_error_fail = "Pyyntö epäonnistui. Ota yhteyttä kirjaston asiakaspalveluun."
 hold_invalid_pickup = "Valittu noutopaikka on virheellinen. Yritä uudestaan."
 hold_invalid_request_group = "Valittu varausryhmä on virheellinen. Yritä uudestaan."
+hold_items_available = "Varaaminen ei ole mahdollista, koska niteitä on saatavissa."
 hold_login = "tehdäksesi varauspyynnön"
 hold_place = "Tee varaus"
 hold_place_fail_missing = "Pyyntö epäonnistui puuttuvien tietojen vuoksi. Ota yhteyttä kirjaston asiakaspalveluun."
@@ -677,6 +695,7 @@ Number = "Numero"
 number_decimal_point = ","
 number_thousands_separator = " "
 OAI Server = "OAI-palvelin"
+Occupation = "Ammatti"
 of = "yhteensä"
 old_password = "Vanha salasana"
 On Reserve = "Rajatussa käytössä"
@@ -690,6 +709,7 @@ operator_exact = "on (täsmälleen)"
 OR = "TAI"
 or create a new list = "tai luo uusi lista"
 original = "Alkuperäinen"
+Other associated place = "Muu liittyvä paikka"
 Other Authors = "Muut tekijät"
 Other Editions = "Muut painokset"
 Other Libraries = "Muut kirjastot"
@@ -702,6 +722,8 @@ password_error_invalid = "Uusi salasana on virheellinen (esim. sisältää kiell
 password_error_not_unique = "Salasanaa ei muutettu"
 password_maximum_length = "Salasanan enimmäispituus on %%maxlength%% merkkiä"
 password_minimum_length = "Salasanan vähimmäispituus on %%minlength%% merkkiä"
+password_only_alphanumeric = "Vain numerot ja kirjaimet a-z"
+password_only_numeric = "Vain numerot"
 Passwords do not match = "Salasanat eivät täsmää"
 Past = "Viimeiset"
 PDF Full Text = "PDF-kokoteksti"
@@ -713,6 +735,8 @@ Physical Description = "Ulkoasu"
 Physical Object = "Fyysinen objekti"
 pick_up_location = "Noutopaikka"
 Place a Hold = "Tee varaus"
+Place of birth = "Syntymäpaikka"
+Place of death = "Kuolinpaikka"
 Playing Time = "Kesto"
 Please check back soon = "Tarkista myöhemmin uudelleen"
 Please contact the Library Reference Department for assistance = "Saadaksesi apua ota yhteyttä kirjaston asiakaspalveluun"
@@ -759,6 +783,7 @@ Recall This = "Tee varaus"
 recaptcha_not_passed = "CAPTCHA-tarkistus virheellinen"
 Record Citations = "Tietueen sitaatit"
 Record Count = "Tietueiden lukumäärä"
+Record Type = "Tietueen tyyppi"
 Recover Account = "Palauta tili"
 recovery_by_email = "Palautus sähköpostiosoitteen avulla"
 recovery_by_username = "Palautus käyttäjätunnuksen avulla"
@@ -803,6 +828,7 @@ Requests = "Varauksia"
 Reserves = "Rajatut"
 Reserves Search = "Rajattujen haku"
 Reserves Search Results = "Rajattujen haun tulokset"
+result_count = "%%count%% tulosta"
 Results = "Tulokset"
 results = "tuloksesta"
 Results for = "Tulokset haulle"
@@ -854,6 +880,8 @@ Serial = "Kausijulkaisu"
 Series = "Sarja"
 Set = "Aseta"
 Showing = "Näytetään"
+sidebar_close = "Pienennä sivupalkki"
+sidebar_expand = "Laajenna sivupalkki"
 Similar Items = "Samankaltaisia teoksia"
 Skip to content = "Siirry sisältöön"
 skip_confirm = "Haluatko varmasti ohittaa tämän vaiheen?"
@@ -867,10 +895,12 @@ sms_success = "Viesti lähetetty."
 Software = "Tietokoneohjelma"
 Sorry, but the help you requested is unavailable in your language. = "Suomenkielistä ohjetta ei ole saatavissa."
 Sort = "Järjestä"
+sort_alphabetic = "Aakkosjärjestys"
 sort_author = "Tekijä"
 sort_author_author = "Aakkosellinen"
 sort_author_relevance = "Relevanssi"
 sort_callnumber = "Luokka"
+sort_count = "Lukumäärä"
 sort_relevance = "Relevanssi"
 sort_title = "Nimeke"
 sort_year = "Aika (uusimmat ensin)"
@@ -1015,9 +1045,6 @@ View this record in EBSCOhost = "Näytä tämä tietue EBSCOhost:ssa"
 visual_facet_parent = "Päätasolta"
 Volume = "Volyymi"
 Volume Holdings = "Numerot saatavissa"
-vudl_access_denied = "Pääsy estetty."
-vudl_tab_docs = "Dokum."
-vudl_tab_pages = "Sivuja"
 VuFind Configuration = "VuFind-asetukset"
 vufind_upgrade_fail = "VuFindin päivitys ei onnistu juuri nyt"
 Warning: These citations may not always be 100% accurate = "Varoitus: Nämä viitteet eivät aina ole täysin luotettavia"
diff --git a/languages/fr.ini b/languages/fr.ini
index fe24a9061d1581224171efa79d1495928f7d36a2..96c1392f4bf8a74439634722aeb901bd20341ee8 100644
--- a/languages/fr.ini
+++ b/languages/fr.ini
@@ -2,9 +2,13 @@
 ;French = Français
 Abstract = "Résumé"
 Access = "Accès"
+Access URL = "URL d'accès"
+access_denied = "Accès interdit."
+Accession Number = "Numéro d'inventaire"
 Account = "Compte lecteur"
 Add = "Ajouter"
-Add a Note = "Ajouter un commentaire"
+Add a Library Card = "Ajouter une carte de bibliothèque"
+Add a Note = "Ajouter une observation"
 Add Tag = "Ajouter un tag"
 Add Tags = "Ajouter des tags"
 Add to another list = "Ajouter à une autre liste"
@@ -26,10 +30,10 @@ add_tag_success = "Tag enregistré"
 Address = "Adresse"
 adv_search_all = "Tous les champs"
 adv_search_author = "Auteur"
-adv_search_callnumber = "Identifiant"
+adv_search_callnumber = "Cote"
 adv_search_filters = "Filtre appliqué"
 adv_search_isn = "ISBN/ISSN"
-adv_search_journaltitle = "Titre de journal"
+adv_search_journaltitle = "Titre de revue"
 adv_search_label = "Chercher"
 adv_search_publisher = "Éditeur"
 adv_search_select_all = "Tout sélectionner"
@@ -40,24 +44,29 @@ adv_search_toc = "Table des matières"
 adv_search_year = "Année de publication"
 Advanced = "Recherche avancée"
 Advanced Search = "Recherche avancée"
-advSearchError_noRights = "Je suis désolé, mais vous n'avez pas le droit de changer cette recheche. Peut-étre que la session n'est plus valable ?"
-advSearchError_notAdvanced = "La recherche choisi ne peut pas être éditée."
-advSearchError_notFound = "La recheche demandée n'existe pas."
+advSearchError_noRights = "Désolé, mais vous n'avez pas le droit de changer cette recherche. Peut-être que la session n'est plus valable ?"
+advSearchError_notAdvanced = "La recherche choisie ne peut pas être éditée."
+advSearchError_notFound = "La recherche demandée n'existe pas."
+ajaxview_label_information = "Information"
+ajaxview_label_tools = "Outils"
 All = "Tout"
 All Fields = "Tous les champs"
 All Pages Loaded = "Toutes les pages chargées"
-All Text = "tout le texte"
+All Text = "Tout le texte"
 alphabrowse_matches = "Résultats"
+alphabrowselink_html = "Parcourir par %%index%% à partir de <a href="%%url%%">%%from%%</a>."
 An error has occurred = "Une erreur est survenue"
 An error occurred during execution; please try again later. = "Erreur lors de l'exécution de la commande. Veuillez réessayer plus tard."
 AND = "ET"
 anonymous_tags = "Tags anonymes"
-APA Citation = "Citation APA"
+APA Citation = "Style de citation APA"
+applied_filter = "Appliquer le filtre"
 Article = "Article"
 Ask a Librarian = "Contacter un bibliothécaire"
+Associated country = "Pays associé"
 Audience = "Public"
 Audio = "Audio"
-authentication_error_admin = "Vous ne pouvez pas vous connecter en ce moment. Veuilles contacter votre adminitrateur pour du support"
+authentication_error_admin = "Vous ne pouvez pas vous connecter en ce moment. Veuillez contacter votre administrateur pour obtenir de l'aide"
 authentication_error_blank = "Ces champs ne peuvent être vides."
 authentication_error_creation_blocked = "Vous n'avez pas les autorisations nécessaires pour créer un compte."
 authentication_error_denied = "Accès refusé"
@@ -67,14 +76,16 @@ authentication_error_technical = "Connexion impossible pour le moment. Veuillez
 Author = "Auteur"
 Author Browse = "Parcourir par auteur"
 Author Notes = "Notes sur l'auteur"
-Author Results for = "Resultats d'auteurs"
-Author Search Results = "Resultats de recherce auteurs"
+Author Results for = "Résultats d'auteurs"
+Author Search Results = "Résultats de recherche auteurs"
+Authority File = "Fichier d'autorité"
 Authors = "Auteurs"
-Authors Related to Your Search = "Auteurs liés à votre recherche"
-Auto configuration is currently disabled = "La configuration automatique est désactivée"
+Authors Related to Your Search = "Auteurs en relation avec votre recherche"
+Auto configuration is currently disabled = "La configuration automatique est actuellement désactivée"
 auto_configure_description = "Dans le cas d'une nouvelle installation, vous devriez pouvoir corriger le problème en utilisant l'outil de configuration automatique."
 auto_configure_disabled = "La configuration automatique est désactivée."
 auto_configure_title = "Configuration automatique"
+Availability = "Disponibilité"
 Available = "Disponible"
 Available Functionality = "Fonctions disponibles"
 Awards = "Récompenses"
@@ -82,28 +93,29 @@ Back to Record = "Retourner à la notice"
 Back to Search Results = "Retourner aux résultats de recherche"
 Backtrace = "Historique"
 Bag = "Panier"
-Balance = "Balance du compte"
+Balance = "Solde du compte"
 basic_search_keep_filters = "Je garde les filtres."
 Be the first to leave a comment = "Soyez le premier à ajouter un commentaire"
 Be the first to tag this record = "Soyez le premier à ajouter un tag"
 Bibliographic Details = "Détails bibliographiques"
 Bibliography = "Bibliographie"
+Blu-ray Disc = "Disque blu-ray"
 Book = "Livre"
 Book Bag = "Panier de livres"
-Book Chapter = "Chapitre d'ouvrage"
+Book Chapter = "Chapitre de livre"
 Book Cover = "Couverture de livre"
 bookbag_confirm_empty = "Êtes-vous sûr de vouloir vider le panier ?"
-bookbag_delete = "Delete Selected Book Bag Items"
-bookbag_delete_selected = "Supprimer la sélection"
-bookbag_email = "Envoyer les éléments sélectionnés par mail"
-bookbag_email_selected = "Envoyer les éléments sélectionnés par mail"
-bookbag_export = "Exporter les éléments sélectionnés"
+bookbag_delete = "Supprimer les éléments du panier sélectionnés"
+bookbag_delete_selected = "Supprimer les éléments sélectionnés"
+bookbag_email = "Envoyer par courriel les éléments du panier sélectionnés"
+bookbag_email_selected = "Envoyer les éléments sélectionnés"
+bookbag_export = "Exporter les éléments du panier sélectionnés"
 bookbag_export_selected = "Exporter les éléments sélectionnés"
 bookbag_full = "Plein"
 bookbag_full_msg = "Votre panier est plein"
 bookbag_is_empty = "Votre panier est vide"
-bookbag_print_selected = "Print Selected"
-bookbag_save = "Sauvegarder les éléments sélectionnés"
+bookbag_print_selected = "Imprimer les éléments sélectionnés"
+bookbag_save = "Sauvegarder les éléments du panier sélectionnés"
 bookbag_save_selected = "Sauvegarder les éléments sélectionnés"
 Bookmark = "Signet"
 Books = "Livres"
@@ -123,38 +135,38 @@ browse_format = "Format"
 browse_lcc = "Classification de la Bibliothèque du Congrès (LCC)"
 browse_publishDate = "Année de publication"
 browse_title = "Titre"
-browse_topic = "Matière"
+browse_topic = "Sujet"
 bulk_email_success = "Les éléments ont été envoyés"
 bulk_email_title = "Notices du catalogue de la bibliothèque"
-bulk_error_missing = "Certains éléments étaient absents. La requête n'a pu être exécutée correctement."
+bulk_error_missing = "Certains éléments étaient absents. La requête n'a pu être exécutée correctement."
 bulk_export_not_supported = "Les éléments sélectionnés ne peuvent être exportés par lot."
-bulk_fail = "Désolé, une erreur est survenue. Veuillez réessayer."
-bulk_noitems_advice = "Aucun élément sélectionné. Cochez au moins une base et essayer à nouveau."
-bulk_save_error = "Certains éléments étaient absents. Vos notices n'ont pas été sauvées."
-bulk_save_success = "Vos notices ont été sauvées avec succès"
+bulk_fail = "Hélas une erreur est survenue. Veuillez réessayer."
+bulk_noitems_advice = "Aucun élément n'est sélectionné. Cochez au moins un élément et essayez à nouveau."
+bulk_save_error = "Certains éléments étaient absents. Vos notices n'ont pas été sauvegardées."
+bulk_save_success = "Vos notices ont été sauvegardées avec succès"
 By = "par"
 by = "par"
-By Alphabetical = "par alphabet"
+By Alphabetical = "par ordre alphabétique"
 By Author = "par auteur"
 By Call Number = "par cote"
 By Course = "par cours"
 By Department = "par département"
 By Era = "par période"
 By Genre = "par genre"
-By Instructor = "par agrégé"
+By Instructor = "par professeur"
 By Popularity = "par popularité"
 By Recent = "plus récent"
 By Region = "par région"
 By Title = "par titre"
 By Topic = "par sujet"
 Call Number = "Cote"
-callnumber_abbrev = "Classification #"
+callnumber_abbrev = "Cote :"
 Cannot find record = "Notice introuvable"
 Cannot find similar records = "Pas de notices similaires trouvées"
 Cassette = "Cassette"
-cat_establish_account = "Afin d'accéder à votre profil, veuillez saisir les informations suivantes :"
+cat_establish_account = "Afin de créer un compte d'utilisateur, veuillez saisir les informations suivantes :"
 cat_password_abbrev = "Mot de passe dans le catalogue"
-cat_username_abbrev = "Nom d'utilisateur dans le catalogue"
+cat_username_abbrev = "Nom d'utilisateur du catalogue"
 Catalog Login = "Connexion au catalogue"
 Catalog Results = "Résultats du catalogue"
 catalog_login_desc = "Entrez vos codes d'accès au catalogue"
@@ -166,25 +178,26 @@ Checked Out = "Emprunté"
 Checked Out Items = "Documents empruntés"
 Checkedout = "Documents empruntés"
 Chicago Citation = "Style de citation Chicago"
-child_record_count = "%%count%% enregistrements"
+child_record_count = "%%count%% notices"
 child_records = "Contenu"
-Choose a Category to Begin Browsing = "Choisir une catégorie pour commencer"
-Choose a Column to Begin Browsing = "Choisissez une colonne pour parcourir"
+Choose a Category to Begin Browsing = "Choisir une catégorie pour commencer à parcourir"
+Choose a Column to Begin Browsing = "Choisir une colonne pour parcourir"
 Choose a List = "Choisissez une liste"
 choose_login_method = "Choisissez une méthode de connexion"
 citation_issue_abbrev = "n° "
 citation_multipage_abbrev = "pp."
 citation_singlepage_abbrev = "p."
 citation_volume_abbrev = "Vol."
-Cite this = "Citer ceci"
+Cite this = "Citer"
 City = "Ville"
+Clear = "Effacer"
 clear_tag_filter = "Supprimer les filtres"
 close = "fermer"
 Code = "Code"
 Collection = "Collection"
 Collection Browse = "Parcourir les collections"
-Collection Items = "Parcourir les éléments"
-collection_disambiguation = "Plusieurs collections correspondantes trouvées"
+Collection Items = "Éléments de la collection"
+collection_disambiguation = "Plusieurs collections correspondantes ont été trouvées"
 collection_empty = "Pas d'éléments à afficher."
 collection_view_record = "Voir la notice"
 Collections = "Collections"
@@ -193,11 +206,13 @@ comment_error_save = "Erreur: Enregistrement du commentaire impossible"
 Comments = "Commentaires"
 Company/Entity = "Entreprise/Institution"
 Configuration = "Configuration"
-confirm_delete = "Êtes-vous sûr de vouloir supprimer ceci ?"
+confirm_delete = "Êtes-vous sûr de vouloir supprimer cet élément ?"
 confirm_delete_brief = "Supprimer l'élément ?"
+confirm_delete_library_card_brief = "Supprimer la carte de bibliothèque ?"
+confirm_delete_library_card_text = "Êtes-vous sûr de vouloir supprimer cette carte de bibliothèque ?"
 confirm_delete_list_brief = "Supprimer la liste ?"
 confirm_delete_list_text = "Êtes-vous sûr de vouloir supprimer cette liste ?"
-confirm_delete_tags_brief = "Supprimer les mots-clés"
+confirm_delete_tags_brief = "Supprimer les tags"
 confirm_dialog_no = "Annuler"
 confirm_dialog_yes = "Confirmer"
 confirm_hold_cancel_all_text = "Êtes-vous sûr de vouloir annuler toutes vos réservations en cours ?"
@@ -205,29 +220,34 @@ confirm_hold_cancel_selected_text = "Êtes-vous sûr de vouloir annuler les rés
 confirm_ill_request_cancel_all_text = "Souhaitez-vous annuler toutes vos demandes en cours de prêt entre bibliothèques ?"
 confirm_ill_request_cancel_selected_text = "Souhaitez-vous annuler les demandes de prêt entre bibliothèques sélectionnées ?"
 confirm_new_password = "Confirmer le nouveau mot de passe"
-confirm_storage_retrieval_request_cancel_all_text = "Souhaitez-vous annuler toutes vos demandes de communications en magasin ?"
-confirm_storage_retrieval_request_cancel_selected_text = "souaitez-vous annuler les demandes communications en magasin sélectionnées ?"
+confirm_storage_retrieval_request_cancel_all_text = "Souhaitez-vous annuler toutes vos demandes de communication en magasin ?"
+confirm_storage_retrieval_request_cancel_selected_text = "Souhaitez-vous annuler les demandes de communication en magasin sélectionnées ?"
 conjunction_or = "ou"
 Contents = "Contenu"
+Contributing Source = "Sources"
 Contributors = "autres auteurs"
-Copies = "Copies"
-Copy = "Copie"
-Corporate Author = "Collectif d'auteurs"
+Copies = "Exemplaires"
+Copy = "Exemplaire"
+Copyright = "Copyright"
+Corporate Author = "Collectivité auteur"
+Corporate Authors = "Collectivités auteurs"
 Country = "Pays"
 Course = "Cours"
-Course Reserves = "Exemplaires réservés"
-course_reserves_empty_list = "Aucun exemplaire réservé n'a été trouvé."
+Course Reserves = "Exemplaires mis en réserve pour un cours"
+course_reserves_empty_list = "Aucun exemplaire mis en réserve n'a été trouvé."
 Cover Image = "Image de couverture de livre"
 Create a List = "Créer une liste"
-Create New Account = "Ouvrir un nouveau compte"
+Create New Account = "Créer un nouveau compte"
 Create New Password = "Créer un nouveau mot de passe"
 Created = "Créé le "
 Database = "Base de données"
 Date = "Date"
+Date of birth = "Date de naissance"
+Date of death = "Date de décès"
 date_day_placeholder = "J"
 date_from = "De"
 date_month_placeholder = "M"
-date_to = "A"
+date_to = "À"
 date_year_placeholder = "A"
 Days = "Jours"
 Debug Information = "Debug informations"
@@ -236,12 +256,13 @@ Delete = "Supprimer"
 delete_all = "Tout supprimer"
 delete_comment_failure = "Suppression impossible."
 delete_comment_success = "Commentaire supprimé."
-delete_list = "Détruire la liste"
+delete_list = "Supprimer la liste"
 delete_page = "Supprimer la page"
-delete_selected = "Détruire les éléments sélectionnés"
-delete_selected_favorites = "Détruitre les favoris sélectionnés"
-delete_tags = "Supprimer les mots-clés"
-delete_tags_by = "Supprimer les mots-clés par"
+delete_selected = "Supprimer les éléments sélectionnés"
+delete_selected_favorites = "Supprimer les favoris sélectionnés"
+delete_tag = "Supprimer le tag"
+delete_tags = "Supprimer les tags"
+delete_tags_by = "Supprimer les tags par"
 Department = "Département"
 Description = "Description"
 Desired Username = "Nom d'utilisateur souhaité"
@@ -249,21 +270,24 @@ Detailed View = "Vue détaillée"
 Details = "Détails"
 Displaying the top = "Affichage ascendant"
 Document Inspector = "Inspecteur de document"
+Document Type = "Type de document"
 DOI = "DOI"
+Draw Search Box = "Dessiner la boîte de recherche"
 Due = "Délais"
 Due Date = "Délais d'emprunt"
 DVD = "DVD"
 eBook = "eBook"
-Edit = "Éditer"
+Edit = "Modifier"
+Edit Library Card = "Modifier la carte de bibliothèque"
 Edit this Advanced Search = "Modifier cette recherche"
 edit_list = "Modifier la liste"
-edit_list_fail = "Désolé, vous n'êtes pas autorisé à éditer cette liste"
-edit_list_success = "La lise a été mise à jour avec succès."
+edit_list_fail = "Hélas vous n'êtes pas autorisé à modifier cette liste"
+edit_list_success = "La liste a été mise à jour avec succès."
 Edition = "Édition"
 eds_expander_fulltext = "Chercher aussi à l'intérieur des articles"
 eds_expander_thesaurus = "Appliquer les termes associés"
 eds_limiter_FC = "Seulement le catalogue"
-eds_limiter_FC1 = "Seulement le répertoire institutionnel"
+eds_limiter_FC1 = "Seulement le dépôt institutionnel"
 eds_limiter_FM6 = "Audio disponible"
 eds_limiter_FR = "Référence disponible"
 eds_limiter_FT = "Texte intégral"
@@ -276,26 +300,26 @@ eds_mode_smart = "Recherche intelligente"
 eds_modes_and_expanders = "Modes de recherche et rebonds"
 Electronic = "Électronique"
 Email = "Courriel"
-Email Address = "adresse email"
+Email Address = "Adresse de courriel"
 Email address is invalid = "Cette adresse de courriel n'est pas valide"
 Email Record = "Envoyer la notice par courriel"
-Email this = "Envoyer ceci"
-Email this Search = "Envoyer cette recherche"
+Email this = "Envoyer par courriel"
+Email this Search = "Envoyer cette recherche par courriel"
 email_failure = "Erreur - Le message n'a pu être envoyé"
 email_link = "Lien"
 email_maximum_recipients_note = "Vous pouvez saisir au maximum %%max%% adresses."
 email_multiple_recipients_note = "Séparez plusieurs adresses de courriel par des virgules."
 email_selected = "Envoyer la sélection par courriel"
-email_selected_favorites = "Envoyer les favoris par courriel"
+email_selected_favorites = "Envoyer par courriel les favoris sélectionnés"
 email_sending = "Envoi en cours..."
 email_subject = "Sujet"
 email_success = "Message envoyé avec succès"
 Empty = "Vide"
-Empty Book Bag = "Vider la panier"
+Empty Book Bag = "Vider le panier"
 Enable Auto Config = "Activer la configuration automatique"
 End Page = "Dernière page"
 Era = "Période"
-error_inconsistent_parameters = "Désolé, une erreur est survenue. De mauvais paramètres ont été détectés."
+error_inconsistent_parameters = "Hélas une erreur est survenue. De mauvais paramètres ont été détectés."
 error_page_parameter_list_heading = "Paramètres de la requête"
 Exception = "Exception"
 Excerpt = "Extrait"
@@ -324,35 +348,37 @@ export_unsupported_format = "Format d'export non supporté"
 FAQs = "FAQ"
 fav_delete = "Supprimer les favoris sélectionnés"
 fav_delete_deleting = "Vos favoris sont supprimés."
-fav_delete_fail = "Désolé, une erreur est survenue, vos favoris n'ont pas été supprimés."
-fav_delete_missing = "Certaines informations étaient manquantes.Vos favoris n'ont pas été supprimés."
+fav_delete_fail = "Hélas une erreur est survenue, vos favoris n'ont pas été supprimés."
+fav_delete_missing = "Certaines informations étaient manquantes. Vos favoris n'ont pas été supprimés."
 fav_delete_success = "Vos favoris ont été supprimés."
-fav_delete_warn = "Vous êtes sur le point de supprimer ces favoris de toutes vos listes. Si vous souhitez les retirer d'une seule liste veuillez la sélectionner avant de demander la suppression."
-fav_email_fail = "Désolé, une erreur est survenue. Vos favoris n'ont pas été envoyés."
+fav_delete_warn = "Vous êtes sur le point de supprimer ces favoris de toutes vos listes. Si vous souhaitez les retirer d'une seule liste, veuillez la sélectionner avant de demander la suppression."
+fav_email_fail = "Hélas une erreur est survenue. Vos favoris n'ont pas été envoyés."
 fav_email_missing = "Certains éléments sont manquants. Vos favoris n'ont pas été envoyés."
 fav_email_success = "Vos favoris ont été envoyés par courriel."
 fav_export = "Exporter les favoris"
-fav_list_delete = "Votre liste de favoris a été supprimée"
-fav_list_delete_cancel = "La liste n'a pas été supprimée"
-fav_list_delete_fail = "Désolé, une erreur est survenue. Votre liste n'a pas été supprimée."
+fav_list_delete = "Votre liste de favoris a été supprimée."
+fav_list_delete_cancel = "La liste n'a pas été supprimée."
+fav_list_delete_fail = "Hélas une erreur est survenue. Votre liste n'a pas été supprimée."
 Favorites = "Favoris"
-Fee = "Tarifs"
-Feedback = "Commentaire"
+Fee = "Frais"
+Feedback = "Remarques"
 feedback_name = "Nom"
+Field of activity = "Domaine d'activité"
+File Description = "Description du fichier"
 Filter = "Filtre"
-filter_tags = "Filtrer les mots-clés"
+filter_tags = "Filtrer les tags"
 filter_wildcard = "Tous"
 Find = "Rechercher"
 Find More = "Autres modes de recherche"
-Find New Items = "Notices récentes"
+Find New Items = "Chercher des nouveautés"
 Finding Aid = "Aide pour la recherche"
 Fine = "Amendes"
-fine_limit_patron = "Vous avez atteint la limite et ne pouvez plus renouveler de prêt."
+fine_limit_patron = "Vous avez atteint la limite des amendes et ne pouvez plus renouveler d'emprunt."
 Fines = "Amendes"
 First = "Premier"
 First Name = "Prénom"
-fix_metadata = "Oui, pour corriger les métadonnées ; J'attendrai"
-for search = "pour la requête"
+fix_metadata = "Oui, corriger les métadonnées ; J'attendrai"
+for search = "pour la recherche"
 Forgot Password = "Mot de passe oublié"
 Form Submitted! = "Formulaire envoyé !"
 Format = "Format"
@@ -361,18 +387,21 @@ From = "De"
 Full description = "Description complète"
 Full text is not displayed to guests = "Le texte intégral n'est pas affiché aux usagers non authentifiés."
 fulltext_limit = "Limiter aux articles avec plein texte accessible"
+Gender = "Genre"
 Genre = "Genre"
+Geographic Search = "Recherche géographique"
 Geographic Terms = "Termes géographiques"
 Geography = "Géographie"
 Get full text = "Accéder au texte intégral"
-Get RSS Feed = "S'inscrire aux flux RSS"
+Get RSS Feed = "S'abonner aux flux RSS"
 Globe = "Globe"
 Go = "Go"
 Go to Standard View = "Retour à la liste des résultats"
+go_to_list = "Aller à la liste"
 google_map_cluster = "Cluster"
 google_map_cluster_points = "Cluster Points"
 Grid = "Grille"
-Group = "Group"
+Group = "Groupe"
 group_AND = "Tous les groupes"
 group_OR = "Un des groupes"
 Has Illustrations = "Illustré"
@@ -383,41 +412,41 @@ help_page_missing = "La page d'aide demandée n'existe pas."
 hierarchy_hide_tree = "Replier l'arborescence"
 hierarchy_show_tree = "Déplier l'arborescence"
 hierarchy_tree = "Contexte"
-hierarchy_tree_error = "Désolé, chargement de l'arborscence impossible"
+hierarchy_tree_error = "Hélas le chargement de l'arborescence n'a pas été possible"
 hierarchy_view_context = "Voir le contexte"
 History = "Historique"
 history_delete = "Effacer"
 history_delete_link = "Effacer"
 history_empty_search = "Quelque chose (recherche vide)"
-history_limits = "Limite"
-history_no_searches = "Il n'existe aucune recherche dans votre dossier."
-history_purge = "Supprimer les recherches de la session qui n'ont pas été archivées."
-history_recent_searches = "Recherches sauvegardées."
+history_limits = "Filtre"
+history_no_searches = "Il n'existe aucune recherche dans votre historique."
+history_purge = "Supprimer les recherches de la session qui n'ont pas été sauvegardées."
+history_recent_searches = "Recherches récentes."
 history_results = "Résultats"
 history_save = "Sauvegarder ?"
 history_save_link = "Sauvegarder"
-history_saved_searches = "Recherches enregistrées"
+history_saved_searches = "Recherches sauvegardées"
 history_search = "Recherche"
 history_time = "Date"
 hold_available = "Disponible pour le retrait"
 hold_cancel = "Annuler la réservation"
 hold_cancel_all = "Annuler toutes les réservations"
-hold_cancel_fail = "Votre demande n'a pas été annulée. Veuillez contacter le bureau d'accueil pour plus d'informations."
+hold_cancel_fail = "Votre réservation n'a pas été annulée. Veuillez contacter la bibliothèque pour plus d'informations."
 hold_cancel_selected = "Annuler les réservations sélectionnées"
-hold_cancel_success = "Votre demande a bien été annulée"
-hold_cancel_success_items = "La (les) demandes(s) a (ont) bien été annulé(e)s"
+hold_cancel_success = "Votre réservation a bien été annulée"
+hold_cancel_success_items = "La ou les réservations ont bien été annulées"
 hold_date_invalid = "Veuillez saisir une date valide"
 hold_date_past = "Veuillez saisir une date dans le futur"
-hold_empty_selection = "Aucune réservation sélectionnée"
+hold_empty_selection = "Aucune réservation n'est sélectionnée"
 hold_error_blocked = "Vous n'avez pas les autorisations nécessaires pour réserver ce document."
-hold_error_fail = "Votre demande a échoué. Veuillez contacter le bureau d'accueil pour plus d'informations."
-hold_invalid_pickup = "L'établissement de retrait saisi est incorrect. Veuillez essayer à nouveau"
-hold_invalid_request_group = "Le groupe utilisé pour la demande de réservation n'est pas validée. Veuillez réessayer."
-hold_login = "Pour les informations de réservation"
-hold_place = "Effectuer une réservation"
-hold_place_fail_missing = "Votre demande a échoué. Il manquait certaines données. Veuillez contacter le bureau d'accueil pour plus d'informations"
+hold_error_fail = "Votre demande a échoué. Veuillez contacter la bibliothèque pour plus d'informations."
+hold_invalid_pickup = "Le lieu de retrait saisi est incorrect. Veuillez réessayer"
+hold_invalid_request_group = "Le groupe utilisé pour la demande de réservation est incorrect. Veuillez réessayer."
+hold_login = "Pour les informations sur les réservations"
+hold_place = "Réserver"
+hold_place_fail_missing = "Votre demande a échoué. Il manquait certaines données. Veuillez contacter la bibliothèque pour plus d'informations"
 hold_place_success_html = "La réservation a bien été enregistrée. <a href="%%url%%">Vos réservations et rappels</a>."
-hold_profile_html = "Pour les réservations et prolongations, veuiller créer un <a href="%%url%%">compte utilisateur</a>."
+hold_profile_html = "Pour les réservations et prolongations, veuillez créer un <a href="%%url%%">profil dans le catalogue de la bibliothèque</a>."
 hold_queue_position = "Place dans la file d'attente"
 hold_request_group = "Demande de"
 hold_requested_group = "Demandé par"
@@ -427,8 +456,8 @@ Holdings = "Exemplaires"
 Holdings at Other Libraries = "Exemplaires dans les autres bibliothèques"
 Holdings details from = "Informations d'exemplaires de"
 Holdings_notes = "Notes"
-Holds = "Emprunts"
-Holds and Recalls = "Emprunts et rappels"
+Holds = "Réservations"
+Holds and Recalls = "Réservations et rappels"
 Home = "Page d'accueil"
 home_browse = "Parcourir par"
 HTML Full Text = "Texte intégral HTML"
@@ -436,35 +465,35 @@ Identifier = "Identifiant"
 ill_request_available = "Disponible pour le retrait"
 ill_request_cancel = "Annuler la demande de prêt entre bibliothèques."
 ill_request_cancel_all = "Annuler toutes les demandes de prêt entre bibliothèques."
-ill_request_cancel_fail = "Votre demande n'a pas été annulée. Veuillez contacter le bureau d'accueil pour plus d'informations."
+ill_request_cancel_fail = "Votre demande n'a pas été annulée. Veuillez contacter la bibliothèque pour plus d'informations."
 ill_request_cancel_selected = "Annuler les demandes de prêt entre bibliothèques sélectionnées."
 ill_request_cancel_success = "Votre demande a bien été annulée"
-ill_request_cancel_success_items = "La (les) demandes(s) a (ont) bien été annulé(e)s"
+ill_request_cancel_success_items = "La ou les demandes ont bien été annulées"
 ill_request_canceled = "Annulé"
 ill_request_check_text = "Vérifier les demandes de prêt entre bibliothèques."
 ill_request_comments = "Commentaires"
 ill_request_date_invalid = "Veuillez saisir une date valide"
 ill_request_date_past = "Veuillez saisir une date dans le futur"
-ill_request_empty_selection = "Aucune demande de prêt entre bibliothèques sélectionnée."
-ill_request_error_blocked = "Vous n'avez pas les autorisations suffisantes pour placer une demande de prêt entre bibliothèques sur ce document."
-ill_request_error_fail = "Votre demande a échoué. Veuillez contacter le bureau d'accueil pour plus d'informations."
-ill_request_error_technical = "Votre requête a échoué en raison d'un problème technique. Veuillez vous rendre au bureau de prêt pour plus de précisions."
-ill_request_error_unknown_patron_source = "La bibliothèque de l'usager n'est pas reconnue dans les requêtes de prêt entre bibliothèques."
-ill_request_invalid_pickup = "L'établissement de retrait saisi est incorrect. Veuillez essayer à nouveau"
+ill_request_empty_selection = "Aucune demande de prêt entre bibliothèques n'est sélectionnée."
+ill_request_error_blocked = "Vous n'avez pas les autorisations suffisantes pour faire une demande de prêt entre bibliothèques pour ce document."
+ill_request_error_fail = "Votre demande a échoué. Veuillez contacter la bibliothèque pour plus d'informations."
+ill_request_error_technical = "Votre requête a échoué en raison d'un problème technique. Veuillez contacter votre bibliothèque pour plus de précisions."
+ill_request_error_unknown_patron_source = "La bibliothèque de l'usager n'est pas reconnue dans les demandes de prêt entre bibliothèques."
+ill_request_invalid_pickup = "Le lieu de retrait saisi est incorrect. Veuillez essayer à nouveau"
 ill_request_pick_up_library = "Bibliothèque de retrait"
 ill_request_pick_up_location = "Lieu de retrait"
-ill_request_place_fail_missing = "Votre demande a échoué. Il manquait certaines données. Veuillez contacter le bureau d'accueil pour plus d'informations"
-ill_request_place_success = "La réservation a bien été enregistrée."
-ill_request_place_success_html = "La réservation a bien été enregistrée. <a href="%%url%%">Demandes de prêt entre bibliothèques</a>."
+ill_request_place_fail_missing = "Votre demande a échoué. Il manquait certaines données. Veuillez contacter la bibliothèque pour plus d'informations"
+ill_request_place_success = "La demande a bien été enregistrée."
+ill_request_place_success_html = "La demande a bien été enregistrée. <a href="%%url%%">Demandes de prêt entre bibliothèques</a>."
 ill_request_place_text = "Faire une demande de prêt entre bibliothèques"
 ill_request_processed = "Traité"
 ill_request_profile_html = "Pour les informations sur les demandes de prêt entre bibliothèques, merci de créer un <a href="%%url%%">profil dans le catalogue de la bibliothèque</a>."
 ill_request_submit_text = "Faire une demande"
 Illustrated = "Illustré"
 ils_connection_failed = "Notre système de gestion de bibliothèque est en cours de maintenance."
-ils_offline_holdings_message = "Les réservations et la disponibilité des documents sont pour le moment indisponibles. Veuillez nous excuser pour la gêne occassionée et nous contacter pour toute assistance :"
-ils_offline_home_message = "Vos informations personnelles seront indisponibles dans cet intervalle. Veuillez nous excuser pour la gêne occassionée et nous contacter pour toute assistance :"
-ils_offline_login_message = "Vos informations personnelles seront indisponibles dans cet intervalle. Veuillez nous excuser pour la gêne occassionée et nous contacter pour toute assistance :"
+ils_offline_holdings_message = "Les réservations et la disponibilité des documents sont momentanément indisponibles. Veuillez nous excuser pour la gêne occasionnée et nous contacter pour toute assistance :"
+ils_offline_home_message = "Votre compte personnel et les données sur vos emprunts seront indisponibles pendant ce temps. Veuillez nous excuser pour la gêne occasionnée et nous contacter pour toute assistance :"
+ils_offline_login_message = "Votre compte personnel sera indisponible pendant ce temps. Veuillez nous excuser pour la gêne occasionnée et nous contacter pour toute assistance :"
 ils_offline_status = "Notre système de gestion de bibliothèque est en cours de maintenance."
 ils_offline_title = "Opération de maintenance en cours"
 Import Record = "Importer les notices"
@@ -473,12 +502,13 @@ in = "dans"
 In This Collection = "Dans cette collection"
 in_collection_label = "Dans la collection:"
 include_synonyms = "Étendre la recherche en utilisant les synonymes"
+Index Terms = "Termes de l'index"
 Indexes = "Index"
 information = "Information"
 Institution = "Institution"
 Institutional Login = "Accéder à votre institution"
-institutional_login_desc = "Entrez votre identifiant 'campus' et votre mot de passe."
-Instructor = "Instructeur"
+institutional_login_desc = "Entrez votre identifiant institutionnel et votre mot de passe."
+Instructor = "Professeur"
 Interlibrary Loan Requests = "Demandes de prêt entre bibliothèques"
 Internet = "Internet"
 Invalid Patron Login = "Identifiant lecteur invalide"
@@ -488,20 +518,22 @@ Invalid Sender Email Address = "Adresse de l'expéditeur invalide"
 ISBN = "ISBN"
 ISBN/ISSN = "ISBN/ISSN"
 ISSN = "ISSN"
-Issue = "Edition"
-Item Description = "Description d'exemplaire"
+Issue = "Numéro"
+Item Description = "Description"
 Item Notes = "Notes"
-Item removed from favorites = "Élément retiré des favoris"
-Item removed from list = "Élément retiré de la liste"
+Item removed from favorites = "Notice retirée des favoris"
+Item removed from list = "Notice retirée de la liste"
 Items = "Éléments"
 items = "notices"
 items_added_to_bookbag = "Notice(s) ajoutée(s) à votre panier"
 items_already_in_bookbag = "La ou les notices sont déjà dans votre panier ou n'ont pu être ajoutées."
-Journal = "Périodique"
-Journal Articles = "Article de périodique"
-Journal Title = "Titre de périodique"
-Journals = "Périodiques"
+Journal = "Revue"
+Journal Articles = "Article de revue"
+Journal Info = "Informations sur la revue"
+Journal Title = "Titre de revue"
+Journals = "Revues"
 Jump to = "Aller à"
+just_cataloged = "Catalogué récemment"
 Keyword = "Mots-clés"
 Keyword Filter = "Filtre par mots-clés"
 Kit = "Kit"
@@ -511,18 +543,30 @@ Last = "Dernier"
 Last Modified = "Dernière modification"
 Last Name = "Nom"
 less = "moins"
+libphonenumber_invalid = "Numéro de téléphone invalide"
+libphonenumber_invalidcountry = "Code pays invalide"
+libphonenumber_invalidregion = "Code region invalide : "
+libphonenumber_notanumber = "La valeur entrée ne semble pas être un numéro de téléphone"
+libphonenumber_toolong = "La valeur entrée est trop longue pour un numéro de téléphone"
+libphonenumber_tooshort = "La valeur entrée est trop courte pour un numéro de téléphone"
+libphonenumber_tooshortidd = "Numéro de téléphone trop court après l'indicatif téléphonique international"
 Library = "Bibliothèque"
-Library Catalog Password = "Mot de passe du catalogue de la bibliothèque"
-Library Catalog Profile = "Compte lecteur sur catalogue de la bibliothèque"
+Library Card = "Carte de bibliothèque"
+Library Card Deleted = "Carte de bibliothèque supprimée"
+Library Card Name = "Nom de la carte de bibliothèque"
+Library Cards = "Cartes de bibliothèque"
+Library Cards Disabled = "Cartes de bibliothèque désactivées"
+Library Catalog Password = "Mot de passe pour le catalogue de la bibliothèque"
+Library Catalog Profile = "Profil dans le catalogue de la bibliothèque"
 Library Catalog Record = "Notice du catalogue de la bibliothèque"
 Library Catalog Search = "Recherche dans le catalogue de la bibliothèque"
-Library Catalog Search Result = "Résultat de la recherche sur le catalogue"
+Library Catalog Search Result = "Résultat de la recherche dans le catalogue de la bibliothèque"
 Library Catalog Username = "Nom d'utilisateur du catalogue de la bibliothèque"
 Library Web Search = "Recherche sur le site de la bibliothèque"
 lightbox_error = "Erreur: impossible d'ouvrir la pop-up"
 Limit To = "Limiter à"
 List = "Liste"
-List Tags = "Liste des mots-clés"
+List Tags = "Liste des tags"
 list_access_denied = "Vous n'avez pas les droits pour consulter cette liste."
 list_edit_name_required = "Le nom de la liste est obligatoire."
 load_tag_error = "Erreur: Chargement des tags impossible"
@@ -533,32 +577,35 @@ Located = "Localisé"
 Location = "Localisation"
 Log Out = "Se déconnecter"
 Login = "Connexion"
-Login for full access = "Connectez vous pour un accès complet."
+Login for full access = "Connectez-vous pour un accès complet."
 login_disabled = "Connexion impossible pour le moment."
 login_target = "Bibliothèque"
 Logout = "Se déconnecter"
 Main Author = "Auteur principal"
+Main Authors = "Auteurs principaux"
 Major Categories = "Catégories principales"
-Manage Tags = "Gérer les mots-clés"
+Manage Tags = "Gérer les tags"
 Manuscript = "Manuscrit"
 Map = "Carte"
 Map View = "Voir sur une carte"
 map_results_label = "À cette adresse :"
 Maps = "Cartes"
 Media Format = "Format de média"
-medium = "Moyen"
+medium = "Support"
+MeSH Terms = "Termes MeSH"
 Message = "Message"
-Message From Sender = "Message de l'émetteur "
+Message From Sender = "Message de l'expéditeur "
 Metadata Prefix = "Préfixe des métadonnées"
 Microfilm = "Microfilm"
-MLA Citation = "Citation MLA"
-mobile_link = "Vous semblez naviguer depuis un appareil mobile, basculer vers la version mobile ?"
+MLA Citation = "Style de citation MLA"
+mobile_link = "Vous semblez naviguer depuis un appareil mobile, voulez-vous basculer vers l'interface mobile ?"
 Monograph Title = "Titre de monographie"
 more = "plus"
-More catalog results = "Plus de resultats du catalogue"
+More catalog results = "Plus de résultats du catalogue"
 More options = "Plus d'options"
 More Summon results = "Plus de résultats issus de Summon"
 More Topics = "Plus de sujets"
+more_authors_abbrev = "et autres"
 more_info_toggle = "Afficher/masquer les informations supplémentaires."
 more_topics = "%%count%% sujets supplémentaires"
 Most Recent Received Issues = "Les éditions les plus récentes"
@@ -566,61 +613,64 @@ Multiple Call Numbers = "Cotes multiples"
 Multiple Locations = "Localisations multiples"
 Musical Score = "Partition"
 My Favorites = "Mes favoris"
-My Fines = "Mes frais"
-My Holds = "Mes empruntes"
+My Fines = "Mes amendes"
+My Holds = "Mes réservations"
 My Profile = "Mon profil"
-Narrow Search = "Affiner la recherche"
+Narrow Search = "Restreindre la recherche"
 navigate_back = "Retour"
 Need Help? = "Besoin d'aide ?"
-New Item Feed = "Nouveautés du flux RSS"
+New Item Feed = "Flux RSS des nouveautés"
 New Item Search = "Recherche des nouveautés"
-New Item Search Results = "Resultats de la recherche des nouveautés"
+New Item Search Results = "Résultats de la recherche des nouveautés"
 New Items = "Nouveautés"
-New Title = "Nouveaux titres"
+New Title = "Nouveau titre"
 new_password = "Nouveau mot de passe"
-new_password_success = "Mise à jour du mot de passe effectuée avec succès"
+new_password_success = "Mot de passe mis à jour avec succès"
 Newspaper = "Journal"
 Next = "Suivant"
 No citations are available for this record = "Pas de citation disponible pour cette notice"
 No Cover Image = "Pas d'image"
 No dependency problems found = "Pas de problème de dépendances"
 No excerpts were found for this record. = "Pas d'extraits trouvés pour cette notice."
-No library account = "Pas de compte de bibliothèque"
-No new item information is currently available. = "Pas de nouvelle information disponbile sur cet exemplaire."
+No library account = "Pas de compte lecteur"
+No new item information is currently available. = "L'information sur les nouveautés n'est pas disponible pour le moment."
 No Preference = "Pas de préférence"
-No reviews were found for this record = "Pas de commentaire trouvé sur cette notice"
+No reviews were found for this record = "Pas de commentaire trouvé pour cette notice"
 No Tags = "Pas de tags"
-no_description = "Aucune description disponible."
-no_items_selected = "Aucun élément sélectionné"
-nohit_filters = "Filtres appliqués à cette recheche"
+no_description = "Aucune description n'est disponible."
+no_items_selected = "Aucun élément n'est sélectionné"
+nohit_active_filters = "Au moins un filtre a été appliqué à cette recherche. Supprimez ce filtre pour obtenir plus de résultats."
+nohit_change_tab = "Vous avez recherché dans l'onglet "%%activeTab%%". Vous trouverez peut-être des résultats supplémentaires dans les autres onglets :"
+nohit_filters = "Filtres appliqués à cette recherche"
 nohit_heading = "Aucun résultat."
 nohit_no_filters = "Aucun filtre n'a été appliqué sur cette recherche."
-nohit_parse_error = "Il semblerait qu'il y ait une erreur dans votre recherche. Vérifiez la syntaxe. Si vous ne souhaitez pas utiliser de fonction avancée, mettre votre recherche entre guillemets peut aider."
+nohit_parse_error = "Il semblerait qu'il y ait une erreur dans votre recherche. Vérifiez la syntaxe. Si vous ne souhaitez pas utiliser de fonction avancée, vous pouvez essayez de mettre votre recherche entre guillemets."
 nohit_prefix = "Votre recherche"
+nohit_query_without_filters = "Supprimer tous les filtres de cette recherche."
 nohit_spelling = "Essayez une orthographe différente."
 nohit_suffix = "ne correspond à aucune ressource."
 nohit_suggest = "Vérifiez votre recherche en supprimant des mots-clés et en vérifiant l'orthographe."
 NOT = "PAS"
-Not Illustrated = "Non illustré"
+Not Illustrated = "Sans illustrations"
 Not On Reserve = "Non réservé"
 not_applicable = "n/a"
 Note = "Note"
-note_760 = "Série principale"
-note_762 = "Sous-série"
+note_760 = "Collection principale"
+note_762 = "Sous-collection"
 note_765 = "Traduit de"
-note_767 = "Traduction"
-note_770 = "A pour supplément"
-note_772 = "Supplément de"
+note_767 = "Traduit sous le titre"
+note_770 = "A pour supplément ou numéro spécial"
+note_772 = "Supplément à"
 note_773 = "Fait partie de"
 note_774 = "Est constitué de"
-note_775 = "Autres éditions disponibles"
+note_775 = "Autres éditions"
 note_776 = "Autre format"
-note_777 = "Livré avec"
+note_777 = "Publié avec"
 note_780_0 = "Fait suite à"
 note_780_1 = "Fait suite après scission à"
 note_780_2 = "Remplace"
 note_780_3 = "Remplace en partie"
-note_780_4 = "Fusion de ... et ..."
+note_780_4 = "Fusion de... et de..."
 note_780_5 = "A absorbé"
 note_780_6 = "A absorbé en partie"
 note_780_7 = "Scission de"
@@ -630,50 +680,57 @@ note_785_2 = "Remplacé par"
 note_785_3 = "Remplacé en partie par"
 note_785_4 = "Absorbé par"
 note_785_5 = "Absorbé en partie par"
-note_785_6 = "Scindé en ... et ..."
-note_785_7 = "Fusionné avec ... et devient ..."
+note_785_6 = "Scindé en... et ..."
+note_785_7 = "Fusionné avec... pour devenir..."
 note_785_8 = "Redevient"
 note_787 = "Autre relation"
 Notes = "Notes"
-Number = "Nombre"
+Number = "Numéro"
 number_decimal_point = ","
 number_thousands_separator = " "
 OAI Server = "Serveur OAI"
+Occupation = "Activité"
 of = "de"
 old_password = "Ancien mot de passe"
-On Reserve = "Réservé"
-On Reserve - Ask at Circulation Desk = "Réservé - demandez au guichet d'emprunt"
-on_reserve = "Réservé - Faire une demande au bureau de prêt"
+On Reserve = "Mis en réserve"
+On Reserve - Ask at Circulation Desk = "Mis en réserve - Contactez la bibliothèque"
+on_reserve = "Mis en réserve - Faire une demande à la bibliothèque"
 on_topic = "%%count%% élément(s) sur ce sujet"
 Online Access = "Accès en ligne"
 online_resources = "Texte intégral"
 operator_contains = "contient"
 operator_exact = "est (exactement)"
 OR = "OU"
-or create a new list = "ou créer une liste"
+or create a new list = "ou créer une nouvelle liste"
 original = "Original"
+Other associated place = "Autres lieux associés"
 Other Authors = "Autres auteurs"
 Other Editions = "Autres éditions"
 Other Libraries = "Autres bibliothèques"
 Other Sources = "Autres sources"
-Page not found. = "Page non trouvée."
+Page not found. = "Page introuvable."
 Password = "Mot de passe"
-Password Again = "confirmer le mot de passe"
+Password Again = "Confirmer le mot de passe"
 Password cannot be blank = "Veuillez saisir un mot de passe"
+password_error_invalid = "Nouveau mot de passe non valide (contient p. ex. des caractères interdits)"
 password_error_not_unique = "Le mot de passe n'a pas été changé"
-password_maximum_length = "Taille maximum du mot de passe : %%maxlength%% caractères"
-password_minimum_length = "Taille minimum du mot de passe : %%minlength%% caractères"
-Passwords do not match = "Les mots de passes ne correspondent pas"
+password_maximum_length = "Longueur maximum du mot de passe : %%maxlength%% caractères"
+password_minimum_length = "Longueur minimum du mot de passe : %%minlength%% caractères"
+password_only_alphanumeric = "Chiffres et lettres A-Z seulement"
+password_only_numeric = "Chiffres seulement"
+Passwords do not match = "Les mots de passe ne correspondent pas"
 Past = "Depuis"
 PDF Full Text = "Texte intégral en PDF"
 peer_reviewed = "Validé par les pairs"
 peer_reviewed_limit = "Limiter aux articles de revues à comité de lecture"
 Phone Number = "Numéro de téléphone"
 Photo = "Photo"
-Physical Description = "Description physique"
+Physical Description = "Description matérielle"
 Physical Object = "Objet physique"
-pick_up_location = "Bibliothèque de retrait"
-Place a Hold = "Commander"
+pick_up_location = "Lieu de retrait"
+Place a Hold = "Réserver"
+Place of birth = "Lieu de naissance"
+Place of death = "Lieu de décès"
 Playing Time = "Durée"
 Please check back soon = "Veuillez réessayer plus tard"
 Please contact the Library Reference Department for assistance = "Veuillez contacter votre bibliothèque pour obtenir de l'assistance"
@@ -682,7 +739,7 @@ Please upgrade your browser. = "Veuillez mettre à jour votre navigateur."
 Posted by = "Enregistré par"
 posted_on = "le"
 Preferences = "Préférences"
-Preferred Library = "Bibliothèque préféré"
+Preferred Library = "Bibliothèque favorite"
 Prev = "Précédent"
 Preview = "Prévisualiser"
 Preview from = "Prévisualiser depuis"
@@ -697,31 +754,36 @@ pronounced = "prononcé"
 Provider = "Fournisseur"
 Public = "Publique"
 Publication = "Publication"
+Publication Date = "Date de publication"
 Publication Frequency = "Fréquence de publication"
-Publication Information = "Information d'édition"
-Publication Type = "Type d'édition"
+Publication Information = "Information sur la publication"
+Publication Type = "Type de publication"
+Publication Year = "Année de publication"
 Publication_Place = "Lieu de publication"
 Published = "Publié"
 Published in = "Publié dans"
 Publisher = "Éditeur"
+Publisher Information = "Informations sur l'éditeur"
+Publisher Permissions = "Autorisations de l'éditeur"
 QR Code = "QR Code"
 qrcode_hide = "Cacher le QR Code"
 qrcode_show = "Afficher le QR Code"
 query time = "Temps de recherche"
 random_recommendation_title = "Documents aléatoires parmi les résultats"
-Range = "Durée"
-Range slider = "Slider"
+Range = "Étendue"
+Range slider = "Curseur"
 Read the full review online... = "Lire l'avis complet en ligne..."
-Recall This = "Réserver"
+Recall This = "Rappeler"
 recaptcha_not_passed = "L'identification du CAPTCHA a échoué"
 Record Citations = "Citations de notices"
-Record Count = "Nombre d'enregistrements"
+Record Count = "Nombre de notices"
+Record Type = "Type de notice"
 Recover Account = "Récupérer le compte"
 recovery_by_email = "Récupérer par courriel"
-recovery_by_username = "Récupérer par identifiant"
+recovery_by_username = "Récupérer par nom d'utilisateur"
 recovery_disabled = "Récupération de mot de passe non activée"
-recovery_email_notification = "Un demande de récupération de mot de passe vient d'être faite pour votre compte avec la bibliothèque %%library%%."
-recovery_email_sent = "Les instructions de réinitialisation du mot de passe ont été envoyée à l'adresse de courriel associée à ce compte."
+recovery_email_notification = "Une demande de récupération de mot de passe vient d'être faite pour votre compte avec la bibliothèque %%library%%."
+recovery_email_sent = "Les instructions de réinitialisation du mot de passe ont été envoyées à l'adresse de courriel associée à ce compte."
 recovery_email_subject = "Récupération de compte VuFind"
 recovery_email_url_pretext = "Veuillez visiter cette page pour définir un nouveau mot de passe : %%url%%"
 recovery_expired_hash = "Le lien de récupération n'est plus valide"
@@ -733,35 +795,36 @@ recovery_user_not_found = "Nous n'avons pu identifier votre compte"
 Refine Results = "Affiner les résultats"
 Region = "Région"
 Related Author = "Auteurs similaires"
-Related Items = "Exemplaires similaires"
+Related Items = "Documents similaires"
 Related Subjects = "Sujets similaires"
 Remove Filters = "Effacer les filtres"
 Remove from Book Bag = "Retirer du panier"
-renew_all = "Renouveler tous les éléments"
+renew_all = "Prolonger tous les prêts"
 renew_determine_fail = "Nous n'avons pu déterminer si ce prêt peut être prolongé. Veuillez contacter un membre de l'équipe."
-renew_empty_selection = "Aucun élément sélectionné"
-renew_error = "Nous n'avons pas pu prolonger ce(s) prêt(s). Veuillez contacter un membre de l'équipe"
+renew_empty_selection = "Aucun élément n'est sélectionné"
+renew_error = "Nous n'avons pas pu prolonger ce(s) prêt(s). Veuillez contacter la bibliothèque"
 renew_fail = "Ce prêt n'a pu être prolongé"
 renew_item = "Prolonger un prêt"
 renew_item_due = "Documents à rendre dans les 24 heures"
-renew_item_limit = "Ce document a atteint le nombre maximal de prolongations de prêts"
+renew_item_limit = "Pour ce document le nombre maximal de prolongations de prêt est atteint"
 renew_item_no = "Le prêt de ce document ne peut être prolongé"
 renew_item_overdue = "Document en retard"
 renew_item_requested = "Ce document a été réservé par un autre usager"
 renew_select_box = "Prolonger le prêt de ce document"
 renew_selected = "Prolonger le prêt des documents sélectionnés"
 renew_success = "Prolongation effectuée"
-Renewed = "Renouvelé"
+Renewed = "Prolongé"
 Request full text = "Demander le texte intégral"
 request_in_transit = "En transit vers le site de retrait."
-request_place_text = "Placer une réservation"
+request_place_text = "Faire une réservation"
 request_submit_text = "Valider la réservation"
-Requests = "Requêtes"
-Reserves = "Réservations"
-Reserves Search = "Rechercher des réservations"
-Reserves Search Results = "Resultats de la recherche des réservations"
+Requests = "Demandes"
+Reserves = "Réserves"
+Reserves Search = "Rechercher dans les réserves"
+Reserves Search Results = "Résultats de la recherche dans les réserves"
+result_count = "%%count%% résultats"
 Results = "Résultats"
-results = "résultas"
+results = "résultats"
 Results for = "Résultats de"
 Results per page = "Résultats par page"
 Resumption Token = "Jeton (Resumption Token)"
@@ -773,19 +836,19 @@ save_search = "Enregistrer la recherche"
 save_search_remove = "Effacer la recherche"
 Saved in = "Enregistré dans"
 scholarly_limit = "Limiter à des articles scientifiques"
-Scroll to Load More = "Défiler pour charger plus d'éléments"
+Scroll to Load More = "Faire défiler pour charger plus d'éléments"
 Search = "Rechercher"
 Search For = "Rechercher"
-Search For Items on Reserve = "Rechercher des exemplaires réservés"
+Search For Items on Reserve = "Rechercher des exemplaires mis en réserve"
 Search History = "Historique de recherche"
 Search Home = "Recherche"
 Search Mode = "Mode de recherche"
 Search Options = "Options de recherche"
-Search Results = "Resultats de la recherche"
+Search Results = "Résultats de la recherche"
 search results of = "Résultats de recherche de "
 Search Tips = "Astuces pour la recherche"
 Search Tools = "Outils de recherche"
-Search Type = "Recherche par type"
+Search Type = "Type de recherche"
 search_AND = "Tous les termes"
 search_groups = "Groupes de recherches"
 search_match = "Correspondances"
@@ -794,24 +857,26 @@ search_OR = "Un des termes"
 search_save_success = "Recherche enregistrée avec succès."
 search_unsave_success = "Recherche supprimée avec succès."
 seconds_abbrev = "s"
-see all = "voir les "
+see all = "voir tous les "
 See also = "Voir aussi"
-Select this record = "Sélectionner cet enregistrement"
+Select this record = "Sélectionner cette notice"
 Select your carrier = "Choisissez votre fournisseur de téléphonie"
 select_page = "Choisir une page"
-select_pickup_location = "Sélectionner le lieu de retrait"
-select_request_group = "Sélectionner le group de requête"
+select_pickup_location = "Choisir le lieu de retrait"
+select_request_group = "Sélectionner le groupe de demandes"
 Selected = "Sélectionné"
 Send = "Envoyer"
-Send us your feedback! = "Un commentaire ?"
+Send us your feedback! = "Envoyez-nous vos remarques !"
 send_an_email_copy = "Envoyer une copie à cette adresse"
 send_email_copy_to_me = "M'envoyer une copie"
 Sensor Image = "Image du capteur"
-Serial = "Collection"
+Serial = "Publication en série"
 Series = "Collection"
 Set = "Changer"
 Showing = "Résultat(s)"
-Similar Items = "Exemplaires similaires"
+sidebar_close = "Cacher la barre latérale"
+sidebar_expand = "Afficher la barre latérale"
+Similar Items = "Documents similaires"
 Skip to content = "Aller au contenu"
 skip_confirm = "Êtes-vous sûr de vouloir passer cette étape ?"
 skip_fix_metadata = "Ne pas corriger les métadonnées pour le moment."
@@ -822,186 +887,191 @@ sms_phone_number = "Numéro de téléphone (10 chiffres)"
 sms_sending = "Envoi en cours..."
 sms_success = "Message envoyé."
 Software = "Logiciel"
-Sorry, but the help you requested is unavailable in your language. = "Desolé, mais le support demandé n'existe pas dans cette langue."
-Sort = "Tri"
+Sorry, but the help you requested is unavailable in your language. = "Désolé, mais l'aide demandée n'existe pas dans cette langue."
+Sort = "Trier"
+sort_alphabetic = "Alphabétique"
 sort_author = "Auteur"
 sort_author_author = "Alphabétique"
 sort_author_relevance = "Pertinence"
-sort_callnumber = "Identifiant"
-sort_relevance = "Relevance"
+sort_callnumber = "Cote"
+sort_count = "Nombre de résultats"
+sort_relevance = "Pertinence"
 sort_title = "Titre"
-sort_year = "Date (décroissant)"
-sort_year asc = "Date (croissant)"
+sort_year = "Date (décroissante)"
+sort_year asc = "Date (croissante)"
 Source = "Source"
-spell_expand_alt = "Recherche plus"
+Source Title = "Titre de la source"
+spell_expand_alt = "Recherche élargie"
 spell_suggest = "Recherches alternatives"
-Staff View = "Affichage Marc"
+Staff View = "Affichage MARC"
 Start a new Advanced Search = "Nouvelle recherche avancée"
 Start a new Basic Search = "Nouvelle recherche simple"
 Start Page = "Page d'accueil"
 starting from = "à partir de"
 Status = "Statut"
-status_unknown_message = "Statut temps réel indisponible"
-Storage Retrieval Requests = "Demandes de communications en magasin"
+status_unknown_message = "Statut en temps réel indisponible"
+Storage Retrieval Requests = "Demandes de communication en magasin"
 storage_retrieval_request_available = "Disponible pour le retrait"
-storage_retrieval_request_cancel = "Annuler la demande de communications en magasin"
-storage_retrieval_request_cancel_all = "Annuler toutes les demandes de communications en magasin"
-storage_retrieval_request_cancel_fail = "Votre demande n'a pas été annulée. Veuillez contacter le bureau d'accueil pour plus d'informations."
-storage_retrieval_request_cancel_selected = "Annuler les demandes de communications en magasin sélectionnées"
+storage_retrieval_request_cancel = "Annuler la demande de communication en magasin"
+storage_retrieval_request_cancel_all = "Annuler toutes les demandes de communication en magasin"
+storage_retrieval_request_cancel_fail = "Votre demande n'a pas été annulée. Veuillez contacter la bibliothèque pour plus d'informations."
+storage_retrieval_request_cancel_selected = "Annuler les demandes de communication en magasin sélectionnées"
 storage_retrieval_request_cancel_success = "Votre demande a bien été annulée"
-storage_retrieval_request_cancel_success_items = "La (les) demandes(s) a (ont) bien été annulé(e)s"
+storage_retrieval_request_cancel_success_items = "La ou les demandes ont bien été annulées"
 storage_retrieval_request_canceled = "Annulé"
-storage_retrieval_request_check_text = "Consulter les demandes de communications en magasin"
+storage_retrieval_request_check_text = "Consulter les demandes de communication en magasin"
 storage_retrieval_request_comments = "Commentaires"
 storage_retrieval_request_date_invalid = "Veuillez saisir une date valide"
 storage_retrieval_request_date_past = "Veuillez saisir une date dans le futur"
-storage_retrieval_request_empty_selection = "Aucune demande de communications en magasin sélectionnée"
-storage_retrieval_request_error_blocked = "Vous n'êtes pas autorisé à faire une demande de communications en magasin sur ce document"
-storage_retrieval_request_error_fail = "Votre demande a échoué. Veuillez contacter le bureau d'accueil pour plus d'informations."
-storage_retrieval_request_invalid_pickup = "L'établissement de retrait saisi est incorrect. Veuillez essayer à nouveau"
+storage_retrieval_request_empty_selection = "Aucune demande de communication en magasin n'est sélectionnée"
+storage_retrieval_request_error_blocked = "Vous n'êtes pas autorisé-e à faire une demande de communication en magasin sur ce document"
+storage_retrieval_request_error_fail = "Votre demande a échoué. Veuillez contacter la bibliothèque pour plus d'informations."
+storage_retrieval_request_invalid_pickup = "Le lieu de retrait saisi est incorrect. Veuillez essayer à nouveau"
 storage_retrieval_request_issue = "Date"
-storage_retrieval_request_place_fail_missing = "Votre demande a échoué. Il manquait certaines données. Veuillez contacter le bureau d'accueil pour plus d'informations"
-storage_retrieval_request_place_success = "La réservation a bien été enregistrée."
-storage_retrieval_request_place_success_html = "La réservation a bien été enregistrée. <a href="%%url%%">Demandes de communications en magasin</a>."
-storage_retrieval_request_place_text = "Faire une demande de communications en magasin"
+storage_retrieval_request_place_fail_missing = "Votre demande a échoué. Il manquait certaines données. Veuillez contacter la bibliothèque pour plus d'informations"
+storage_retrieval_request_place_success = "La demande de communication en magasin a bien été enregistrée."
+storage_retrieval_request_place_success_html = "La demande de communication en magasin a bien été enregistrée. <a href="%%url%%">Demandes de communication en magasin</a>."
+storage_retrieval_request_place_text = "Demander la communication d'un document en magasin"
 storage_retrieval_request_processed = "Traité"
-storage_retrieval_request_profile_html = "Pöur les informations sur les demandes de communications en magasin, merci de créer un <a href="%%url%%">profil dans le catalogue de la bibliothèque</a>."
+storage_retrieval_request_profile_html = "Pour les informations sur les demandes de communication en magasin, merci de créer un <a href="%%url%%">profil dans le catalogue de la bibliothèque</a>."
 storage_retrieval_request_reference = "Référence"
 storage_retrieval_request_selected_item = "Document sélectionné"
-storage_retrieval_request_submit_text = "Effectuer une réservation"
+storage_retrieval_request_submit_text = "Demander la communication d'un document en magasin"
 storage_retrieval_request_volume = "Volume"
 storage_retrieval_request_year = "Année"
 Subcollection = "Sous-collection"
 Subject = "Sujet"
 Subject Area = "Domaine"
-Subject Recommendations = "Recommandations Thématiques"
-Subject Terms = "Sujet"
+Subject Geographic = "Sujet géographique"
+Subject Recommendations = "Recommandations thématiques"
+Subject Terms = "Mots-clés sujets"
 Subject(s) = "Sujet(s)"
 Subjects = "Sujets"
 Submit = "Envoyer"
 Submitting = "Envoi"
 Suggested Topics = "Suggestion de sujets"
-Summary = "Sommaire"
-Summon Results = "Sommaire des resultats"
+Summary = "Résumé"
+Summon Results = "Résultats dans Summon"
 summon_database_recommendations = "Vous pourrez trouver des informations supplémentaires ici :"
 Supplements = "Suppléments"
 Supplied by Amazon = "Mis à disposition par Amazon"
 Switch view to = "Basculer vers la vue"
+switchquery_fuzzy = "Effectuer une recherche floue retournera les résultats avec des orthographes proches"
 switchquery_intro = "Vous pourriez obtenir plus de résultats en ajustant votre recherche"
-switchquery_lowercasebools = "Si vous souhaitez utiliser des booléens, ils doivent être en MAJUSCULE."
+switchquery_lowercasebools = "Si vous souhaitez utiliser des opérateurs booléens, ils doivent être en MAJUSCULES."
 switchquery_truncatechar = "Réduisez votre équation de recherche pour augmenter le nombre de résultats."
-switchquery_unwantedbools = "Les mots AND, OR et NOT peuvent fausser la rechercher, essayez d'ajouter des guillemets"
+switchquery_unwantedbools = "Les mots AND, OR et NOT peuvent fausser la recherche; essayez d'ajouter des guillemets"
 switchquery_unwantedquotes = "Supprimer les guillemets pour élargir la recherche"
 switchquery_wildcard = "Ajouter un symbole de troncature (wildcard) permet d'inclure plus de variations."
 System Unavailable = "Système indisponible"
 Table of Contents = "Table des matières"
 Table of Contents unavailable = "Table des matières indisponible"
 Tag = "Tag"
-Tag Management = "Gestion des mots-clés"
-tag_delete_filter = "Vous utilisez le filtre suivant - Nom d'utilisateur : %username%, Mot-clé : %tag%, Ressource : %resource%"
-tag_delete_warning = "Attention ! Vous êtes sur le point de supprimer %count% mots-clés"
-tag_filter_empty = "Aucun mot-clé disponible pour ce filtre"
+Tag Management = "Gestion des tags"
+tag_delete_filter = "Vous utilisez le filtre suivant - Nom d'utilisateur : %username%, Tag : %tag%, Ressource : %resource%"
+tag_delete_warning = "Attention ! Vous êtes sur le point de supprimer %count% tags"
+tag_filter_empty = "Aucun tag disponible pour ce filtre"
 Tags = "Tags"
-tags_deleted = "%count% mot(s)-clé(s) supprimé(s)"
+tags_deleted = "%count% tag(s) supprimé(s)"
 test_fail = "Échec"
-test_fix = "Fix"
+test_fix = "Corriger"
 test_ok = "OK"
 Text this = "Envoyer par SMS"
-Thank you for your feedback. = "Merci pour votre commentaire."
-That email address is already used = "Cette adresse de courriel est déjà enregistrée"
-That username is already taken = "Cet identifiant est déjà utilisé"
+Thank you for your feedback. = "Merci pour vos remarques."
+That email address is already used = "Cette adresse de courriel est déjà utilisée"
+That username is already taken = "Ce nom d'utilisateur est déjà utilisé"
 The record you selected is not part of any of your lists. = "La notice sélectionnée ne fait pas partie d'une de vos listes"
 The record you selected is not part of the selected list. = "La notice sélectionnée ne fait pas partie de la liste sélectionnée"
-The system is currently unavailable due to system maintenance = "Pour des raisons de maintenance, le système est actuellement indisponible"
+The system is currently unavailable due to system maintenance = "Pour des raisons de maintenance, le système est momentanément indisponible"
 Theme = "Thème"
 This email was sent from = "Ce courriel a été envoyé par"
 This field is required = "Ce champ est obligatoire"
-This item is already part of the following list/lists = "Ce exemplaire fait déjà partie des listes suivantes"
+This item is already part of the following list/lists = "Cette notice fait déjà partie des listes suivantes"
 This result not is displayed to guests = "Ce résultat n'est pas affiché aux utilisateurs non authentifiés."
 Title = "Titre"
 Title not available = "Ce titre est indisponible"
 Title View = "Vue Titre"
-title_hold_place = "Placer une requête au niveau du titre"
-To = "A"
-Too Many Email Recipients = "Il y a trop des adresses de courriel."
+title_hold_place = "Faire une demande au niveau du titre"
+To = "À"
+Too Many Email Recipients = "Il y a trop d'adresses de courriel."
 too_many_favorites = "La liste est trop longue pour être affichée en une fois. Essayez de réorganiser vos favoris en plusieurs listes ou de limiter à l'aide des tags."
 too_many_new_items = "Il y a trop de nouvelles notices pour les afficher en une seule fois. Essayez de préciser votre recherche."
-too_many_reserves = "Il y a trop de documents réservés pour les afficher en une seule fois. Essayez de préciser votre recherche."
-top_facet_additional_prefix = "Supplémentaire "
+too_many_reserves = "Il y a trop de documents mis en réserve pour les afficher en une seule fois. Essayez de préciser votre recherche."
+top_facet_additional_prefix = "En outre "
 top_facet_suffix = "... dans votre recherche."
-Topic = "Matière"
-Topics = "Matières"
-Total Balance Due = "Montant total dû"
+Topic = "Sujet"
+Topics = "Sujets"
+Total Balance Due = "Solde débiteur total"
 total_comments = "Nombre total de commentaires"
 total_lists = "Nombre total de listes"
 total_resources = "Nombre total de ressources"
 total_saved_items = "Nombre total d'éléments sauvegardés"
-total_tags = "Nombre total de mots-clés"
+total_tags = "Nombre total de tags"
 total_users = "Nombre total d'utilisateurs"
+Transliterated Title = "Titre translittéré"
 tree_search_limit_reached_html = "Trop de réponses pour l'affichage arborescent. Seuls les <b>%%limit%%</b> premiers éléments sont affichés. Pour une recherche complète, cliquez <a id="fullSearchLink" href="%%url%%" target="_blank">ici.</a>"
-unique_tags = "Mots-clés uniques"
+unique_tags = "Tags uniques"
 University Library = "Bibliothèque universitaire"
 Unknown = "Inconnue"
 unrecognized_facet_label = "Autre"
 Upgrade VuFind = "Mettre à jour VuFind"
-upgrade_description = "Si vous effectuez une mise à jour depuis une ancienne version de Vufind, vous pouvez charger vos anciens paramètre à l'aide de cet outil."
+upgrade_description = "Si vous effectuez une mise à jour depuis une ancienne version de VuFind, vous pouvez charger vos anciens paramètres à l'aide de cet outil."
 URL = "URL"
 Use for = "Employé pour"
 Use instead = "Employer"
 User Account = "Compte d'utilisateur"
 Username = "Nom d'utilisateur"
-Username cannot be blank = "Il faut saisir un identifiant"
+Username cannot be blank = "Il faut saisir un nom d'utilisateur"
+Username is already in use in another library card = "Ce nom d'utilisateur est déjà utilisé pour une autre carte"
 VHS = "VHS"
 Video = "Vidéo"
 Video Clips = "vidéo-clips"
 Videos = "Vidéos"
 view already selected = "Voir les éléments déjà sélectionnés"
-View Book Bag = "Consuler le panier"
+View Book Bag = "Consulter le panier"
 View Full Collection = "Voir la collection complète"
 View Full Record = "Voir la notice complète"
 View in EDS = "Voir dans EDS"
-View online: Full view Book Preview from the Hathi Trust = "Disponible en texte intégral sur the Hathi Trust"
+View online: Full view Book Preview from the Hathi Trust = "Disponible en ligne en texte intégral sur the Hathi Trust"
 View Record = "Voir la notice"
 View Records = "Voir les notices"
-View this record in EBSCOhost = "Voir cet enregistrement sur EBSCOhost"
+View this record in EBSCOhost = "Voir cette notice dans EBSCOhost"
 visual_facet_parent = "De"
 Volume = "Volume"
 Volume Holdings = "Indications des volumes"
-vudl_access_denied = "Accés interdit."
-vudl_tab_docs = "Documents"
-vudl_tab_pages = "Pages"
 VuFind Configuration = "Configuration de VuFind"
 vufind_upgrade_fail = "Mise à jour impossible pour le moment"
-Warning: These citations may not always be 100% accurate = "Avis: Ces citations peuvent ne pas être correctes à 100%"
-wcterms_broader = "Mot-clés ultérieurs"
+Warning: These citations may not always be 100% accurate = "Attention : ces citations peuvent ne pas être correctes à 100%"
+wcterms_broader = "Mot-clés plus larges"
 wcterms_exact = "Mot-clés exacts"
-wcterms_narrower = "Mot-clés approchés"
+wcterms_narrower = "Mot-clés approchant"
 Web = "Web"
-What am I looking at = "De quoi ai-je besoin ?"
+What am I looking at = "Comment cela fonctionne-t-il ?"
 widen_prefix = "Veuillez élargir votre recherche"
 wiki_link = "Informations fournies par Wikipedia"
 with filters = "avec filtres"
 with_selected = "avec la sélection"
 Year of Publication = "Année de publication"
 Yesterday = "Hier"
-You do not have any fines = "Vous n'avez pas de frais"
-You do not have any holds or recalls placed = "Vous n'avez pas de réservation ou rappel"
+You do not have any fines = "Vous n'avez pas d'amende"
+You do not have any holds or recalls placed = "Vous n'avez pas de réservation ni de rappel"
 You do not have any interlibrary loan requests placed = "Vous n'avez aucune demande de prêt entre bibliothèques en cours"
 You do not have any items checked out = "Vous n'avez aucun emprunt"
+You do not have any library cards = "Vous n'avez aucune carte de bibliothèque"
 You do not have any saved resources = "Vous n'avez pas encore sauvegardé de données"
-You do not have any storage retrieval requests placed = "Vous n'avez aucune demande de communications en magasin en cours"
-You must be logged in first = "Il faut se connecter dabord"
+You do not have any storage retrieval requests placed = "Vous n'avez aucune demande de communication en magasin en cours"
+You must be logged in first = "Il faut se connecter d'abord"
 Your Account = "Votre compte"
 Your book bag is empty = "Votre panier est vide"
 Your Checked Out Items = "Vos emprunts"
 Your Comment = "Votre commentaire"
 Your Favorites = "Vos favoris"
-Your Fines = "Vos frais"
+Your Fines = "Vos amendes"
 Your Holds and Recalls = "Vos réservations et rappels"
 Your Lists = "Vos listes"
 Your Profile = "Votre profil"
 Your search terms = "Vos termes de recherche"
 Your Tags = "Vos tags"
 your_match_would_be_here = "Votre correspondance devrait être ici."
-Zip = "Zip"
+Zip = "Code postal"
 zoom = "Zoom"
diff --git a/languages/ga.ini b/languages/ga.ini
index f2e822e81a4e1c561653c18609938b0acc133596..ec5cd5647ec2ec09ebbfe694e09bb2605f84e94e 100644
--- a/languages/ga.ini
+++ b/languages/ga.ini
@@ -239,8 +239,8 @@ fav_email_fail = "Ár leithscéal, ach tharla earráid. Níor seoladh do cheaná
 fav_email_missing = "Bhí roinnt sonraí in easnamh. Níor seoladh do cheaná(i)n ar ríomhphost."
 fav_email_success = "Seoladh do cheaná(i)n ar ríomhphost de réir d’iarratais."
 fav_export = "Easpórtáil Ceanáin"
-fav_list_delete = "Scriosadh do liosta ceanán"
-fav_list_delete_cancel = "Níor scriosadh an liosta seo"
+fav_list_delete = "Scriosadh do liosta ceanán."
+fav_list_delete_cancel = "Níor scriosadh an liosta seo."
 fav_list_delete_fail = "Ár leithscéal, ach tharla earráid. Níor scriosadh do liosta."
 Favorites = "Ceanáin"
 Fee = "Táille"
@@ -558,6 +558,7 @@ sms_success = "Teachtaireacht seolta."
 Software = "Bogearraí"
 Sorry, but the help you requested is unavailable in your language. = "Ár leithscéal, ach níl an chabhair a d'iarr tú ar fáil i do theanga."
 Sort = "Sórtáil"
+sort_alphabetic = "Ord aibítre"
 sort_author = "Údar"
 sort_author_author = "Ord aibítre"
 sort_author_relevance = "Éileamh"
diff --git a/languages/he.ini b/languages/he.ini
index 7ec6fd9abe4df2226a5bac852265cff9abce8a8a..ec0d77142d351d3585ebd383587f23d74cd2f099 100644
--- a/languages/he.ini
+++ b/languages/he.ini
@@ -652,6 +652,7 @@ sms_success = "הודעה נשלחה."
 Software = "תכנה"
 Sorry, but the help you requested is unavailable in your language. = "מצטערים, אבל העזרה שביקשת אינה זמינה בשפה שביקשת"
 Sort = "מיון"
+sort_alphabetic = "אלפביתי"
 sort_author = "מחבר"
 sort_author_author = "מחבר לפי אלפבת"
 sort_author_relevance = "פופולריות"
@@ -747,8 +748,6 @@ View Records = "ראה רשומות"
 visual_facet_parent = "מ"
 Volume = "כרך"
 Volume Holdings = "מלאי כרכים"
-vudl_tab_docs = "מסמכים"
-vudl_tab_pages = "עמודים"
 VuFind Configuration = "הגדרות ויופיינד"
 vufind_upgrade_fail = "אין אפשרות לשדרג אתת VuFind בזמן זה"
 Warning: These citations may not always be 100% accurate = "אזהרה: ציטוטים אלה לעיתים לא מדויקים ב 100%"
diff --git a/languages/it.ini b/languages/it.ini
index f3ce36cb654a8bdd941d0331fe674db6fadf03ac..28478dc12ac0667c9588af6a8d3c676fd114076f 100644
--- a/languages/it.ini
+++ b/languages/it.ini
@@ -3,6 +3,7 @@
 Abstract = "Abstract"
 Access = "Accesso"
 Access URL = "URL di accesso"
+access_denied = "Accesso negato."
 Accession Number = "Accession Number"
 Account = "Account"
 Add = "Aggiungi"
@@ -59,8 +60,10 @@ An error occurred during execution; please try again later. = "Errore durante l'
 AND = "AND"
 anonymous_tags = "Tag anonimi"
 APA Citation = "Citazione APA"
+applied_filter = "Filtro applicato"
 Article = "Articolo"
 Ask a Librarian = "Chiedi al bibliotecario"
+Associated country = "Paese associato"
 Audience = "Pubblico"
 Audio = "Reg. sonora musicale"
 authentication_error_admin = "Non puoi loggarti in questo momento. Per assistenza, contatta il tuo amministratore di sistema."
@@ -75,6 +78,7 @@ Author Browse = "Lista (autore)"
 Author Notes = "Note sull'autore"
 Author Results for = "Risultati per autore"
 Author Search Results = "Risultati ricerca autore"
+Authority File = "Authority File"
 Authors = "Autori"
 Authors Related to Your Search = "Autori collegati alla tua ricerca"
 Auto configuration is currently disabled = "L'auto-configurazione attualmente è disabilitata"
@@ -238,6 +242,8 @@ Create New Password = "Crea una nuova Password"
 Created = "Creato"
 Database = "Database"
 Date = "Data"
+Date of birth = "Data di nascita"
+Date of death = "Data di morte"
 date_day_placeholder = "D"
 date_from = "Da"
 date_month_placeholder = "M"
@@ -254,6 +260,7 @@ delete_list = "Elimina la lista"
 delete_page = "Cancella la pagina"
 delete_selected = "Elimina i selezionati"
 delete_selected_favorites = "Elimina i preferiti selezionati"
+delete_tag = "Cancella Tag"
 delete_tags = "Cancella i tag"
 delete_tags_by = "Cancella tag per"
 Department = "Dipartimento"
@@ -265,6 +272,7 @@ Displaying the top = "Displaying the top"
 Document Inspector = "Analizzatore documentale"
 Document Type = "Tipo di documento"
 DOI = "DOI"
+Draw Search Box = "Definisci riquadro ricerca"
 Due = "Scadere"
 Due Date = "Data di scadenza"
 DVD = "DVD"
@@ -348,13 +356,14 @@ fav_email_fail = "Ops, si è verificato un errore. I tuoi preferiti non sono sta
 fav_email_missing = "Alcuni dati erano mancanti; i tuoi preferiti non sono stati inviati."
 fav_email_success = "I tuoi preferiti sono stati inviati all'indirizzo specificato."
 fav_export = "Esporta i preferiti"
-fav_list_delete = "la lista dei tuoi preferiti è stata eliminata"
-fav_list_delete_cancel = "Questa lista non è stata eliminata"
+fav_list_delete = "la lista dei tuoi preferiti è stata eliminata."
+fav_list_delete_cancel = "Questa lista non è stata eliminata."
 fav_list_delete_fail = "Ops, si è verificato un errore. La tua lista non è stata eliminata."
 Favorites = "Elementi salvati"
 Fee = "Tariffa"
 Feedback = "Feedback"
 feedback_name = "Nome"
+Field of activity = "Campo di attività"
 File Description = "Descrizione del file"
 Filter = "Filtra"
 filter_tags = "Filtra i tag"
@@ -378,7 +387,9 @@ From = "Da"
 Full description = "Descrizione completa"
 Full text is not displayed to guests = "Il Full text non è disponibile agli ospiti."
 fulltext_limit = "Limita agli articoli per cui il full text è disponibile"
+Gender = "Genere"
 Genre = "Genere"
+Geographic Search = "Ricerca gerografica"
 Geographic Terms = "Termini geografici"
 Geography = "Geografia"
 Get full text = "Testo"
@@ -678,6 +689,7 @@ Number = "Numero"
 number_decimal_point = ","
 number_thousands_separator = "."
 OAI Server = "OAI Server"
+Occupation = "Occupazione"
 of = "di"
 old_password = "Vecchia Password"
 On Reserve = "Riservato"
@@ -691,6 +703,7 @@ operator_exact = "= (esatto)"
 OR = "OR"
 or create a new list = "oppure crea una nuova lista"
 original = "Originale"
+Other associated place = "Altro luogo associato"
 Other Authors = "Altri autori"
 Other Editions = "Altre edizioni"
 Other Libraries = "Altre biblioteche"
@@ -703,6 +716,8 @@ password_error_invalid = "La nuova password non è valida (contiene caratteri no
 password_error_not_unique = "La password non è stata cambiata"
 password_maximum_length = "La lunghezza massima della password è di %%maxlength%% caratteri"
 password_minimum_length = "La lunghezza minima della password è di %%maxlength%% caratteri"
+password_only_alphanumeric = "Solo numeri e lettere A-Z"
+password_only_numeric = "Solo numeri"
 Passwords do not match = "Le password non coincidono"
 Past = "Ultimi"
 PDF Full Text = "PDF Full Text"
@@ -714,6 +729,8 @@ Physical Description = "Descrizione fisica"
 Physical Object = "Oggetto fisico"
 pick_up_location = "Punto di ritiro"
 Place a Hold = "Richiedi"
+Place of birth = "Luogo di nascita"
+Place of death = "Luogo di morte"
 Playing Time = "Durata"
 Please check back soon = "Per favore ricontrolla tra un pò"
 Please contact the Library Reference Department for assistance = "Per assistenza contattare il banco reference"
@@ -760,6 +777,7 @@ Recall This = "Prenota"
 recaptcha_not_passed = "CAPTCHA non corretto"
 Record Citations = "Citazioni del record"
 Record Count = "Conteggio dei record"
+Record Type = "Tipo record"
 Recover Account = "Recupera account"
 recovery_by_email = "Rigenera attraverso email"
 recovery_by_username = "Rigenera per username"
@@ -804,6 +822,7 @@ Requests = "Richieste"
 Reserves = "Reserve"
 Reserves Search = "Reserves Search"
 Reserves Search Results = "Reserves Search Results"
+result_count = "%%count%% risultati"
 Results = "Resultati"
 results = "risultati"
 Results for = "Risultati per"
@@ -855,6 +874,8 @@ Serial = "Periodico"
 Series = "Serie"
 Set = "Set"
 Showing = "Mostra"
+sidebar_close = "Compatta barra"
+sidebar_expand = "Espandi barra"
 Similar Items = "Documenti analoghi"
 Skip to content = "Salta al contenuto"
 skip_confirm = "Sei sicuro di saltare questo passaggio?"
@@ -868,10 +889,12 @@ sms_success = "SMS inviato."
 Software = "Software"
 Sorry, but the help you requested is unavailable in your language. = "Ci scusiamo ma l'aiuto non è disponibile nella tua lingua."
 Sort = "Ordina"
+sort_alphabetic = "Alfabetico"
 sort_author = "Autore"
 sort_author_author = "Alfabetico"
 sort_author_relevance = "Popolarità"
 sort_callnumber = "Collocazione"
+sort_count = "Conta risultati"
 sort_relevance = "Rilevanza"
 sort_title = "Titolo"
 sort_year = "Data (discendente)"
@@ -1016,9 +1039,6 @@ View this record in EBSCOhost = "Vedi questo record in EBSCOhost"
 visual_facet_parent = "Da"
 Volume = "Volume"
 Volume Holdings = "Copie del volume"
-vudl_access_denied = "Accesso negato."
-vudl_tab_docs = "Documenti"
-vudl_tab_pages = "Pagine"
 VuFind Configuration = "Configurazione VuFind"
 vufind_upgrade_fail = "Al momento non possiamo aggiornare VuFind"
 Warning: These citations may not always be 100% accurate = "Attenzione: Queste citazioni potrebbero non essere precise al 100%"
diff --git a/languages/ja.ini b/languages/ja.ini
index a4f06380869b7f4454af7423f67666277aacab09..4e8cc5e2e9055fa58d5706ead13e5ae82f0d5035 100644
--- a/languages/ja.ini
+++ b/languages/ja.ini
@@ -4,6 +4,7 @@
 Abstract = "抄録"
 Access = "アクセス"
 Access URL = "アクセスURL"
+access_denied = "アクセスできません。"
 Accession Number = "受入番号"
 Account = "アカウント"
 Add = "追加"
@@ -60,8 +61,10 @@ An error occurred during execution; please try again later. = "処理中にエ
 AND = "AND"
 anonymous_tags = "匿名タグ"
 APA Citation = "APA引用形式"
+applied_filter = "適用されたフィルター"
 Article = "è«–æ–‡"
 Ask a Librarian = "図書館員に聞く"
+Associated country = "関係国"
 Audience = "Audience"
 Audio = "オーディオ"
 authentication_error_admin = "現在、ログインできません。システム管理者にご連絡ください。"
@@ -76,6 +79,7 @@ Author Browse = "著者ブラウズ"
 Author Notes = "著者紹介"
 Author Results for = "Author Results for"
 Author Search Results = "著者検索結果"
+Authority File = "典拠ファイル"
 Authors = "著者"
 Authors Related to Your Search = "関連資料の著者"
 Auto configuration is currently disabled = "自動設定機能は無効です"
@@ -96,6 +100,7 @@ Be the first to leave a comment = "このレコードへの初めてのコメン
 Be the first to tag this record = "このレコードへの初めてのタグを付けませんか"
 Bibliographic Details = "書誌詳細"
 Bibliography = "書誌"
+Blu-ray Disc = "ブルーレイディスク"
 Book = "図書"
 Book Bag = "ブックカート"
 Book Chapter = "図書の章"
@@ -186,6 +191,7 @@ citation_singlepage_abbrev = "p."
 citation_volume_abbrev = "Vol."
 Cite this = "この資料を引用"
 City = "都市"
+Clear = "クリア"
 clear_tag_filter = "フィルターのクリア"
 close = "閉じる"
 Code = "コード"
@@ -225,6 +231,7 @@ Copies = "部数"
 Copy = "所蔵"
 Copyright = "著作権"
 Corporate Author = "団体著者"
+Corporate Authors = "共著者"
 Country = "国"
 Course = "講義"
 Course Reserves = "講義資料"
@@ -236,6 +243,8 @@ Create New Password = "新パスワードの作成"
 Created = "作成"
 Database = "データベース"
 Date = "日付"
+Date of birth = "出生日"
+Date of death = "死亡日"
 date_day_placeholder = "D"
 date_from = "From"
 date_month_placeholder = "M"
@@ -252,6 +261,7 @@ delete_list = "リストを削除"
 delete_page = "ページの削除"
 delete_selected = "選択アイテムを削除"
 delete_selected_favorites = "選択したお気に入りを削除"
+delete_tag = "削除タグ"
 delete_tags = "タグの削除"
 delete_tags_by = "次の条件でタグを削除"
 Department = "部局"
@@ -263,6 +273,7 @@ Displaying the top = "先頭に表示"
 Document Inspector = "ドキュメント検査機能"
 Document Type = "文書タイプ"
 DOI = "DOI"
+Draw Search Box = "検索ボックスをドローします。"
 Due = "返却日"
 Due Date = "返却日"
 DVD = "DVD"
@@ -353,6 +364,7 @@ Favorites = "お気に入り"
 Fee = "料金"
 Feedback = "ご意見"
 feedback_name = "お名前"
+Field of activity = "アクテイビティのフイールド"
 File Description = "ファイル記述"
 Filter = "フィルター"
 filter_tags = "タグのフィルタリング"
@@ -376,7 +388,9 @@ From = "From"
 Full description = "詳細記述"
 Full text is not displayed to guests = "フルテキストはゲストには表示されません。"
 fulltext_limit = "全文が利用できる論文に限定する"
+Gender = "ジェンダー"
 Genre = "ジャンル"
+Geographic Search = "GIS検索"
 Geographic Terms = "地理項目"
 Geography = "地理区分"
 Get full text = "全文の入手"
@@ -384,6 +398,7 @@ Get RSS Feed = "RSSフィード"
 Globe = "地球儀"
 Go = "実行"
 Go to Standard View = "標準画面へ移行"
+go_to_list = "リストへ飛ぶ"
 google_map_cluster = "クラスタ"
 google_map_cluster_points = "クラスター・ポイント"
 Grid = "グリッド"
@@ -568,6 +583,7 @@ login_disabled = "現在、ログインできません。"
 login_target = "図書館"
 Logout = "ログアウト"
 Main Author = "第一著者"
+Main Authors = "主要な著者"
 Major Categories = "主要カテゴリ"
 Manage Tags = "タグの管理"
 Manuscript = "手稿"
@@ -590,6 +606,7 @@ More catalog results = "目録システム検索結果をさらに表示"
 More options = "オプションを追加"
 More Summon results = "Summon検索結果をさらに表示"
 More Topics = "さらなるトピック"
+more_authors_abbrev = "ç­‰"
 more_info_toggle = "詳細情報の表示/非表示"
 more_topics = "その他のトピック %%count%%件"
 Most Recent Received Issues = "最新受領資料"
@@ -623,11 +640,14 @@ No reviews were found for this record = "このレコードに書評はありま
 No Tags = "タグなし"
 no_description = "記述は利用できません。"
 no_items_selected = "アイテムが選択されていません"
+nohit_active_filters = "一つ以上のファッセット・フイルターがこの検索に適用されます.もしフィルターを削除すると, より多くの結果が得られるかもしれません。"
+nohit_change_tab = "%%activeTab%%を検索しています。他のタブのひとつで何か見つかるかも知れません。"
 nohit_filters = "現在この検索に適用されているフィルター:"
 nohit_heading = "該当資料なし"
 nohit_no_filters = "この検索に適用されているフィルターはありません。"
 nohit_parse_error = "検索クエリに問題があると思われます。検索文法をチェックしてください。特に高度な検索機能を使用していない場合は、検索語を引用符で括ってみると良いかもしれません。"
 nohit_prefix = "指定した検索語"
+nohit_query_without_filters = "この検索からすべてのフイルターを削除します。"
 nohit_spelling = "少々検索語を変更した方が良いでしょう。"
 nohit_suffix = "に合致する資料はありませんでした。"
 nohit_suggest = "検索語の一部を削除したり、スペルをチェックしたりして、検索語句を変えて再度実行してみてください。"
@@ -664,11 +684,13 @@ note_785_5 = "吸収後誌(パート)"
 note_785_6 = "分割後誌"
 note_785_7 = "合併後タイトル"
 note_785_8 = "誌名変更"
+note_787 = "他の関係"
 Notes = "注記"
 Number = "番号"
 number_decimal_point = "."
 number_thousands_separator = ","
 OAI Server = "OAIサーバー"
+Occupation = "職業"
 of = "/"
 old_password = "旧パスワード"
 On Reserve = "予約の有無"
@@ -682,6 +704,7 @@ operator_exact = "完全一致"
 OR = "OR"
 or create a new list = "あるいは、新規リストを作成する"
 original = "オリジナル"
+Other associated place = "他の関連する場所"
 Other Authors = "その他の著者"
 Other Editions = "その他の版"
 Other Libraries = "他の図書館"
@@ -694,6 +717,8 @@ password_error_invalid = "新パスワードは不正です(不正な文字が
 password_error_not_unique = "パスワードは変更されませんでした"
 password_maximum_length = "パスワードは最大%%maxlength%%文字までです。"
 password_minimum_length = "パスワードは最低%%minlength%%文字必要です。"
+password_only_alphanumeric = "番号および A-Zの文字のみ"
+password_only_numeric = "番号のみ"
 Passwords do not match = "パスワードが間違っています"
 Past = "過去"
 PDF Full Text = "PDFフルテキスト"
@@ -705,6 +730,8 @@ Physical Description = "物理的記述"
 Physical Object = "物体"
 pick_up_location = "受取図書館"
 Place a Hold = "予約する"
+Place of birth = "出生地"
+Place of death = "死去地"
 Playing Time = "演奏時間"
 Please check back soon = "しばらくお待ちください"
 Please contact the Library Reference Department for assistance = "詳しくは図書館の管理者にご連絡ください"
@@ -751,6 +778,7 @@ Recall This = "返却請求する"
 recaptcha_not_passed = "CAPTCHAが一致しません"
 Record Citations = "レコードの引用形"
 Record Count = "レコード数"
+Record Type = "レコードのタイプ"
 Recover Account = "アカウントの復旧"
 recovery_by_email = "メールアドレスによる再設定"
 recovery_by_username = "ユーザ名による再設定"
@@ -795,6 +823,7 @@ Requests = "リクエスト"
 Reserves = "予約"
 Reserves Search = "予約資料検索"
 Reserves Search Results = "予約資料検索結果"
+result_count = "%%count%% 結果"
 Results = "結果"
 results = "結果"
 Results for = "検索語:"
@@ -846,6 +875,8 @@ Serial = "逐次刊行物"
 Series = "シリーズ"
 Set = "セット"
 Showing = "検索結果"
+sidebar_close = "サイドバーが壊れた"
+sidebar_expand = "拡張サイドバー"
 Similar Items = "類似資料"
 Skip to content = "コンテンツを見る"
 skip_confirm = "本当にこのステップを省略しますか?"
@@ -859,10 +890,12 @@ sms_success = "メッセージを送信しました。"
 Software = "ソフトウェア"
 Sorry, but the help you requested is unavailable in your language. = "申し訳ありませんが、指定された言語のヘルプはありません。"
 Sort = "ソート"
+sort_alphabetic = "アルファベット順"
 sort_author = "著者順"
 sort_author_author = "アルファベット順"
 sort_author_relevance = "人気順"
 sort_callnumber = "請求記号順"
+sort_count = "集計結果"
 sort_relevance = "適合順"
 sort_title = "タイトル順"
 sort_year = "出版年降順"
@@ -924,6 +957,7 @@ summon_database_recommendations = "次のデータベースからも検索でき
 Supplements = "補足資料"
 Supplied by Amazon = "Amazon提供"
 Switch view to = "表示画面切替"
+switchquery_fuzzy = "あいまい検索を実行すると似通った綴りの用語を検索されるかも知れません"
 switchquery_intro = "検索結果が意図したものでない場合は、検索語を変更してもう一度検索してください。"
 switchquery_lowercasebools = "論理演算子はすべて大文字で指定してください。"
 switchquery_truncatechar = "検索結果を増やすには検索語を短くしてください"
@@ -1006,9 +1040,6 @@ View this record in EBSCOhost = "EBSCOhostのレコードを表示"
 visual_facet_parent = "From"
 Volume = "å·»"
 Volume Holdings = "所蔵巻号"
-vudl_access_denied = "アクセスできません。"
-vudl_tab_docs = "ドキュメント"
-vudl_tab_pages = "ページ"
 VuFind Configuration = "VuFindの設定"
 vufind_upgrade_fail = "現在、VuFindをアップグレードできません"
 Warning: These citations may not always be 100% accurate = "警告: この引用は必ずしも正確ではありません"
diff --git a/languages/nl.ini b/languages/nl.ini
index dd608652b25026274ec6439196e5c2fcde616e10..c5eeb428610eda88bf1429a38982cd97a3e11c98 100644
--- a/languages/nl.ini
+++ b/languages/nl.ini
@@ -4,6 +4,7 @@
 Abstract = "Samenvatting"
 Access = "Toegang"
 Access URL = "URL naar item"
+access_denied = "Toegang geweigerd"
 Accession Number = "Archiefnummer"
 Account = "Account"
 Add = "Toevoegen"
@@ -60,8 +61,10 @@ An error occurred during execution; please try again later. = "Er trad een fout
 AND = "AND"
 anonymous_tags = "Anonieme tags"
 APA Citation = "APA Citatie"
+applied_filter = "Toegepaste filter"
 Article = "Artikel"
 Ask a Librarian = "Vraag het een bibliothecaris"
+Associated country = "Geassocieerd land"
 Audience = "publiek"
 Audio = "Audio"
 authentication_error_admin = "We kunnen je nu niet inloggen. Contacteer jouw systeembeheerder voor hulp."
@@ -76,6 +79,7 @@ Author Browse = "Blader door de auteurs"
 Author Notes = "Auteur aantekeningen"
 Author Results for = "Auteurs-resultaten voor"
 Author Search Results = "Auteur zoekresultaten"
+Authority File = "Autoriteit bestand"
 Authors = "Auteurs"
 Authors Related to Your Search = "Auteurs verwant aan jouw zoekopdracht"
 Auto configuration is currently disabled = "Automatische configuratie is momenteel uitgeschakeld"
@@ -239,6 +243,8 @@ Create New Password = "Maak Nieuw Wachtwoord Aan"
 Created = "Aangemaakt"
 Database = "Databank"
 Date = "Datum"
+Date of birth = "Geboortedatum"
+Date of death = "Datum van overlijden"
 date_day_placeholder = "D"
 date_from = "Van"
 date_month_placeholder = "M"
@@ -255,6 +261,7 @@ delete_list = "Lijst wissen"
 delete_page = "Verwijder pagina"
 delete_selected = "Geselecteerde wissen"
 delete_selected_favorites = "Geselecteerde favorieten wissen"
+delete_tag = "Verwijder etiket"
 delete_tags = "Verwijder tags"
 delete_tags_by = "Verwijder tags door"
 Department = "Departement"
@@ -266,6 +273,7 @@ Displaying the top = "De bovenkant wordt afgebeeld"
 Document Inspector = "Document Controleur"
 Document Type = "Document Type"
 DOI = "DOI"
+Draw Search Box = "Teken Zoek-vak"
 Due = "Inleverdatum"
 Due Date = "Inleverdatum"
 DVD = "DVD"
@@ -349,13 +357,14 @@ fav_email_fail = "Sorry, er is een fout opgetreden. Jouw favoriet(en) werd(en) n
 fav_email_missing = "De gegevens waren niet volledig. Jouw favoriet(en) werd(en) niet verzonden."
 fav_email_success = "Jouw favoriet(en) werd(en) verzonden zoals je vroeg."
 fav_export = "Exporteer Favorieten"
-fav_list_delete = "Jouw favorietenlijst werd gewist"
-fav_list_delete_cancel = "Deze lijst werd niet gewist"
+fav_list_delete = "Jouw favorietenlijst werd gewist."
+fav_list_delete_cancel = "Deze lijst werd niet gewist."
 fav_list_delete_fail = "Sorry, er is een fout opgetreden. Jouw lijst werd niet gewist."
 Favorites = "Favorieten"
 Fee = "Bijdrage"
 Feedback = "Feedback"
 feedback_name = "Naam"
+Field of activity = "Werkterrein"
 File Description = "Bestandsbeschrijving"
 Filter = "Filter"
 filter_tags = "Filter tags"
@@ -379,7 +388,9 @@ From = "Van"
 Full description = "Volledige beschrijving"
 Full text is not displayed to guests = "Full text wordt niet getoond aan gasten"
 fulltext_limit = "Beperken tot artikels beschikbaar in Full Text"
+Gender = "Geslacht"
 Genre = "Genre"
+Geographic Search = "Geografische zoekopdracht"
 Geographic Terms = "Geografische termen"
 Geography = "Geografie"
 Get full text = "Volledige tekst"
@@ -679,6 +690,7 @@ Number = "Nummer"
 number_decimal_point = ","
 number_thousands_separator = "."
 OAI Server = "OAI Server"
+Occupation = "Beroep"
 of = "van"
 old_password = "Oude wachtwoord"
 On Reserve = "Gereserveerd"
@@ -692,6 +704,7 @@ operator_exact = "is (exact)"
 OR = "OR"
 or create a new list = "of maak een nieuwe lijst"
 original = "origineel"
+Other associated place = "Andere geassocieerde plaats"
 Other Authors = "Andere auteurs"
 Other Editions = "Andere edities"
 Other Libraries = "Andere bibliotheken"
@@ -704,6 +717,8 @@ password_error_invalid = "Het wachtwoord is ongeldig (bv. bevat ongeldige karakt
 password_error_not_unique = "Het wachtwoord werd niet aangepast"
 password_maximum_length = "Het wachtwoord bevat maximaal %%maxlength%% karakters"
 password_minimum_length = "Het wachtwoord bevat minimaal %%minlength%% karakters"
+password_only_alphanumeric = "Enkel nummers en letters A-Z"
+password_only_numeric = "Enkel nummers"
 Passwords do not match = "Wachtwoorden komen niet overeen"
 Past = "Vorige"
 PDF Full Text = "PDF Full text"
@@ -715,6 +730,8 @@ Physical Description = "Fysieke beschrijving"
 Physical Object = "Fysiek object"
 pick_up_location = "Afhaalpunt"
 Place a Hold = "Plaats een reservatie"
+Place of birth = "Geboorteplaats"
+Place of death = "Plaats van overlijden"
 Playing Time = "Speelduur"
 Please check back soon = "Controleer het binnenkort nog eens"
 Please contact the Library Reference Department for assistance = "Contacteer de vakspecialist van de bibliotheek"
@@ -761,6 +778,7 @@ Recall This = "Hou dit vast"
 recaptcha_not_passed = "Onjuiste CAPTCHA"
 Record Citations = "Record citatie"
 Record Count = "Aantal records"
+Record Type = "Document type"
 Recover Account = "Herstel jouw account"
 recovery_by_email = "Herstel met behulp van jouw email"
 recovery_by_username = "Herstel met behulp van jouw gebruikersnaam"
@@ -805,6 +823,7 @@ Requests = "Aanvragen"
 Reserves = "Reserveringen"
 Reserves Search = "Zoekopdracht Reserveringen"
 Reserves Search Results = "Zoekresultaten voor Reserveringen"
+result_count = "%%count%% resultaten"
 Results = "Resultaten"
 results = "resultaten"
 Results for = "Resultaten voor"
@@ -856,6 +875,8 @@ Serial = "Serie"
 Series = "Reeks"
 Set = "Aanpassen"
 Showing = "Toon"
+sidebar_close = "Zijbalk inklappen"
+sidebar_expand = "Zijbalk uitklappen"
 Similar Items = "Gelijkaardige items"
 Skip to content = "Ga door naar de inhoud"
 skip_confirm = "Weet je zeker dat je deze stap wil overslaan?"
@@ -869,10 +890,12 @@ sms_success = "Bericht is verzonden."
 Software = "Software"
 Sorry, but the help you requested is unavailable in your language. = "Sorry, de hulp die je vraagt, is niet beschikbaar in jouw taal"
 Sort = "Sorteren"
+sort_alphabetic = "Alfabetisch"
 sort_author = "Auteur"
 sort_author_author = "Alfabetisch"
 sort_author_relevance = "populariteit"
 sort_callnumber = "Plaatsingsnummer"
+sort_count = "Aantal Resultaten"
 sort_relevance = "Relevantie"
 sort_title = "Titel"
 sort_year = "Datum Aflopend"
@@ -1017,9 +1040,6 @@ View this record in EBSCOhost = "Bekijk dit record in EBSCOhost"
 visual_facet_parent = "Van"
 Volume = "Volume"
 Volume Holdings = "Volume Bezit"
-vudl_access_denied = "Toegang geweigerd"
-vudl_tab_docs = "Documenten"
-vudl_tab_pages = "Pagina's"
 VuFind Configuration = "VuFind Configuratie"
 vufind_upgrade_fail = "We kunnen op dit moment VUFind niet bijwerken"
 Warning: These citations may not always be 100% accurate = "Let op: Deze citaties zijn niet altijd 100% accuraat"
diff --git a/languages/pl.ini b/languages/pl.ini
index d34f9405ab46db2d554a987af6c551ea5cf549a7..fe6402ae98779c5b1cc5ffec578d148a78327c56 100644
--- a/languages/pl.ini
+++ b/languages/pl.ini
@@ -65,6 +65,7 @@
 Abstract = "Streszczenie"
 Access = "Ograniczenie dostępu"
 Access URL = "URL dostępu"
+access_denied = "Dostęp niedozwolony."
 Accession Number = "Numer dostępu"
 Account = "Konto czytelnika"
 Add = "Dołączyć"
@@ -121,8 +122,10 @@ An error occurred during execution; please try again later. = "Wystąpił błąd
 AND = "I"
 anonymous_tags = "anonimowa etykieta"
 APA Citation = "Styl cytowania APA"
+applied_filter = "Zastosowane filtry"
 Article = "Artykuł"
 Ask a Librarian = "Zapytaj bibliotekarza"
+Associated country = "ZwiÄ…zany kraj"
 Audience = "Publiczność"
 Audio = "Audio"
 authentication_error_admin = "Obecnie nie możesz się zalogować. Skontaktuj się z administratorem sieci."
@@ -137,6 +140,7 @@ Author Browse = "Przeglądanie według autora"
 Author Notes = "Dane autora"
 Author Results for = "Znalezieni autorzy"
 Author Search Results = "Rezultaty wyszukiwania według autorów"
+Authority File = "Kartoteka haseł wzorcowych"
 Authors = "Autorzy"
 Authors Related to Your Search = "Autorzy zrelacjonowani z wyszukiwaniem."
 Auto configuration is currently disabled = "Autokonfiguracja jest nieaktywna."
@@ -157,6 +161,7 @@ Be the first to leave a comment = "Napisz pierwszy komentarz"
 Be the first to tag this record = "Dołącz pierwszą etykiete"
 Bibliographic Details = "Opis bibliograficzny"
 Bibliography = "Bibliografia"
+Blu-ray Disc = "Dysk Blu-ray"
 Book = "Książka"
 Book Bag = "Lista podręczna"
 Book Chapter = "Rozdział"
@@ -247,6 +252,7 @@ citation_singlepage_abbrev = "s."
 citation_volume_abbrev = "r."
 Cite this = "Cytować"
 City = "Miasto"
+Clear = "Opróżnić, opróżnij"
 clear_tag_filter = "Usuń filtry"
 close = "Zamknąć"
 Code = "Kod błędu"
@@ -286,6 +292,7 @@ Copies = "Egzemplarze"
 Copy = "Egzemplarz"
 Copyright = "Prawo autorskie"
 Corporate Author = "Korporacja"
+Corporate Authors = "organizacja autorów"
 Country = "Kraj"
 Course = "Semestr"
 Course Reserves = "Aparaty semestralne"
@@ -297,6 +304,8 @@ Create New Password = "Stwórz nowe hasło"
 Created = "Utwórzone"
 Database = "Baza danych"
 Date = "Data"
+Date of birth = "Data urodzenia"
+Date of death = "Data śmierci"
 date_day_placeholder = "D"
 date_from = "od"
 date_month_placeholder = "M"
@@ -313,6 +322,7 @@ delete_list = "Usunąć listę"
 delete_page = "Usuń strone"
 delete_selected = "Usuń zaznaczone elementy"
 delete_selected_favorites = "Usuń zaznaczone uluobione książki"
+delete_tag = "Etykieta usunięcia"
 delete_tags = "Usuń etykiety"
 delete_tags_by = "Usuń etykiety według"
 Department = "Oddział"
@@ -324,6 +334,7 @@ Displaying the top = "Widoczne sÄ… pierwsze rezultaty"
 Document Inspector = "Kontroler dokumentów"
 Document Type = "Typ dokumentu"
 DOI = "DOI"
+Draw Search Box = "Zaznacz zasięg wyszukiwania"
 Due = "do"
 Due Date = "Termin zwrotu"
 DVD = "DVD"
@@ -414,6 +425,7 @@ Favorites = "Ulubione książki"
 Fee = "Opłata"
 Feedback = "Feedback"
 feedback_name = "Nazwisko"
+Field of activity = "Zakres działalności"
 File Description = "Opis pliku"
 Filter = "filtr"
 filter_tags = "Filtruj etykiety"
@@ -437,7 +449,9 @@ From = "od"
 Full description = "Szczegółowa specyfikacja"
 Full text is not displayed to guests = "Goście nie mają uprawnień do oglądania artykułów pełnotekstowych."
 fulltext_limit = "Ogranicz do artykułów pełnotekstowych."
+Gender = "Płeć"
 Genre = "Gatunek"
+Geographic Search = "Wyszukiwanie geograficzne"
 Geographic Terms = "Hasła geograficzne"
 Geography = "Geografia"
 Get full text = "Dokumenty pełnotekstowe"
@@ -445,6 +459,7 @@ Get RSS Feed = "Abonuj RSS"
 Globe = "Globus"
 Go = "Wykonaj"
 Go to Standard View = "do wersji standartowej"
+go_to_list = "Idź do listy"
 google_map_cluster = "Skupienie"
 google_map_cluster_points = "Aglomeracja"
 Grid = "do wersji Grid"
@@ -629,6 +644,7 @@ login_disabled = "Obecnie nie możesz się zalogować."
 login_target = "Biblioteka"
 Logout = "Logout"
 Main Author = "1. autor"
+Main Authors = "Główni autorzy"
 Major Categories = "Kategorie główne"
 Manage Tags = "ZarzÄ…dzaj etykietami"
 Manuscript = "Rękopis"
@@ -651,6 +667,7 @@ More catalog results = "Więcej rezultatów"
 More options = "Dalsze opcje"
 More Summon results = "Więcej cytatów"
 More Topics = "Więcej tematów"
+more_authors_abbrev = "i wsp."
 more_info_toggle = "Pokaż więcej/mniej."
 more_topics = "%%count%% więcej tematów"
 Most Recent Received Issues = "Najnowsze wydania"
@@ -684,11 +701,14 @@ No reviews were found for this record = "Ten zapis nie ma recenzji."
 No Tags = "Nie ma etykietki"
 no_description = "Nie ma specyfikacji."
 no_items_selected = "Nic nie wybrałeś"
+nohit_active_filters = "Jeden lub więcej filtrów został zastosowany do wyszukiwania. Jeśli usuniesz filtry, możesz otrzymać więcej rezultatów."
+nohit_change_tab = "Wyszukiwałeś w etykiecie "%%activeTab%%". Może znajdziesz coś w jednej z innych etykiet:"
 nohit_filters = "Zastosowane filtry"
 nohit_heading = "Nie ma rezultatów!"
 nohit_no_filters = "W tym wyszukiwaniu nie zastosowałeś filtrów."
 nohit_parse_error = "Wystąpił problem przy wyszukiwaniu - sprawdź składnię. Jeśli nie używasz kompleksowej składni wyszukiwania, napisz parametry w cudzysłowie."
 nohit_prefix = "Dla wyszukiwania"
+nohit_query_without_filters = "Usuń filtry z tego wyszukiwania."
 nohit_spelling = "Spróbuj zastosować inną pisownię."
 nohit_suffix = "Nie znaleziono tożsamości."
 nohit_suggest = "Możesz polepszyć wyszukiwanie usuwając słowa oraz sprawdzając ortografię."
@@ -725,11 +745,13 @@ note_785_5 = "Częściowo oddane w"
 note_785_6 = "Rozdzielone w"
 note_785_7 = "Połączone z / formularzem"
 note_785_8 = "Znowu pod tytułem"
+note_787 = "Inne powiÄ…zanie"
 Notes = "Komentarze"
 Number = "Numer"
 number_decimal_point = ","
 number_thousands_separator = "."
 OAI Server = "Server OAI"
+Occupation = "Zawód"
 of = "od"
 old_password = "Stare hasło"
 On Reserve = "Zarezerwowane"
@@ -743,6 +765,7 @@ operator_exact = "jest (dokładnie)"
 OR = "ALBO"
 or create a new list = "albo utwórz nową listę"
 original = "Oryginał"
+Other associated place = "Inne zwiÄ…zane miejsce"
 Other Authors = "Kolejni autorzy"
 Other Editions = "Kolejne wydania"
 Other Libraries = "Kolejne biblioteki"
@@ -755,6 +778,8 @@ password_error_invalid = "Nowe hasło jest nieważne (n.p. zawiera nieprawidłow
 password_error_not_unique = "Hasło nie zostało zmienione"
 password_maximum_length = "Maksymalna długość hasła to %%maxlength%% znaków"
 password_minimum_length = "Minimalna długość hasła to %%minlength%% znaków"
+password_only_alphanumeric = "Tylko numery oraz literki A-Z"
+password_only_numeric = "Tylko numery"
 Passwords do not match = "Hasła nie są identyczne"
 Past = "od"
 PDF Full Text = "PDF pełnotekstowe"
@@ -766,6 +791,8 @@ Physical Description = "Opis fizyczny"
 Physical Object = "Objekt"
 pick_up_location = "Odbierz w bibliotece"
 Place a Hold = "Zamów"
+Place of birth = "Miejsce urodzenia"
+Place of death = "Miejsce śmierci"
 Playing Time = "Czas trwania"
 Please check back soon = "Spróbuj jeszcze raz"
 Please contact the Library Reference Department for assistance = "Skontaktuj się z biblioteką aby otrzymać dalsze informacje."
@@ -812,6 +839,7 @@ Recall This = "Odwołaj"
 recaptcha_not_passed = "CAPTCHA nie zweryfikowany"
 Record Citations = "Cytaty zapisu"
 Record Count = "Ilość zapisów"
+Record Type = "Typ zapisu"
 Recover Account = "Odzyskaj konto"
 recovery_by_email = "Odzyskaj przez email"
 recovery_by_username = "Odzyskaj przez nazwę użytkownika"
@@ -856,6 +884,7 @@ Requests = "Zamówienia"
 Reserves = "Rezerwacje"
 Reserves Search = "Wyszukiwanie rezerwacji"
 Reserves Search Results = "Rezultaty rezerwacji"
+result_count = "%%count%% Rezultatów"
 Results = "Rezultaty"
 results = "Rezultaty"
 Results for = "Rezultaty dla"
@@ -907,6 +936,8 @@ Serial = "Wydawnictwo ciągłe"
 Series = "Seria"
 Set = "Zmień"
 Showing = "Rezultaty"
+sidebar_close = "Złóż fiszkę"
+sidebar_expand = "Rozszerz fiszkÄ™"
 Similar Items = "Podobne zapisy"
 Skip to content = "Przejdź do treści"
 skip_confirm = "Czy chcesz naprawdę ominąć ten krok?"
@@ -920,10 +951,12 @@ sms_success = "Wiadomość wysłana"
 Software = "Oprogramowanie"
 Sorry, but the help you requested is unavailable in your language. = "Strony pomocy nie są dostępne w twoim języku."
 Sort = "Sortuj"
+sort_alphabetic = "Alfabetycznie"
 sort_author = "Autor"
 sort_author_author = "Alfabetycznie"
 sort_author_relevance = "Częstotliwość"
 sort_callnumber = "Sygnatura"
+sort_count = "Liczba rezultatów"
 sort_relevance = "Ważność"
 sort_title = "Tytuł"
 sort_year = "Według najnowszych"
@@ -985,6 +1018,7 @@ summon_database_recommendations = "Inne możliwe źródła:"
 Supplements = "Suplementy"
 Supplied by Amazon = "Dostarczone przez Amazon"
 Switch view to = "Zmień do wersji"
+switchquery_fuzzy = "Wyszukiwanie rozmyte (Fuzzy Search) może wykazać rezultaty dla podobnych haseł."
 switchquery_intro = "Możesz otrzymać więcej wyników. Zmodyfikuj wyszukiwanie."
 switchquery_lowercasebools = "Próbujesz użyć operatorów wyszukiwania? Napisz je z dużej litery."
 switchquery_truncatechar = "Zredukuj wyszukiwane hasła aby rozszerzyć swoje wyniki."
@@ -1067,9 +1101,6 @@ View this record in EBSCOhost = "Zobacz zapis w EBSCOhost"
 visual_facet_parent = "od"
 Volume = "Tom"
 Volume Holdings = "Dane tomów"
-vudl_access_denied = "Dostęp niedozwolony."
-vudl_tab_docs = "Dokumenty"
-vudl_tab_pages = "Strony"
 VuFind Configuration = "Konfiguracja VuFind"
 vufind_upgrade_fail = "Vufind obecnie nie może dokonać aktualizacji."
 Warning: These citations may not always be 100% accurate = "Uwaga: Te cytaty mogą odróżniać się od wytycznej twojego fakultetu."
diff --git a/languages/pt-br.ini b/languages/pt-br.ini
index 1b931536a0674e2e67080e5da3045b0bfd3511f0..f28b74135b22cbdf344dbe2c32ef23614c7302b2 100644
--- a/languages/pt-br.ini
+++ b/languages/pt-br.ini
@@ -1,8 +1,9 @@
-; For future reference:
-;Brazilian Portugese = "Português (Brasil)"
+; For future reference:
+;Brazilian Portugese = "Português (Brasil)"
 Abstract = "Resumo"
 Access = "Acesso"
 Access URL = "Acessar a URL"
+access_denied = "Access não permitido."
 Accession Number = "Número de chamada"
 Account = "Conta"
 Add = "Adicionar"
@@ -59,8 +60,10 @@ An error occurred during execution; please try again later. = "Ocorreu um erro d
 AND = "E"
 anonymous_tags = "Tags anônimas"
 APA Citation = "Citação norma APA"
+applied_filter = "Filtro Aplicado"
 Article = "Atigo"
 Ask a Librarian = "Serviço de Referência"
+Associated country = "País associado"
 Audience = "Audiência"
 Audio = "Audio"
 authentication_error_admin = "Não é possível fazer login no momento. Contate o administrador do sistema para obter ajuda."
@@ -75,6 +78,7 @@ Author Browse = "Navegar por Autor"
 Author Notes = "Notas de Autor"
 Author Results for = "Busca de Autor para"
 Author Search Results = "Resultados da busca por Autor"
+Authority File = "Arquivo de autoridade"
 Authors = "Autores"
 Authors Related to Your Search = "Autores relacionados com a sua busca"
 Auto configuration is currently disabled = "A configuração automática está desativada"
@@ -238,6 +242,8 @@ Create New Password = "Criar Nova Senha"
 Created = "Criado"
 Database = "Base de Dados"
 Date = "Data"
+Date of birth = "Data de nascimento"
+Date of death = "Data de morte"
 date_day_placeholder = "D"
 date_from = "De"
 date_month_placeholder = "M"
@@ -254,6 +260,7 @@ delete_list = "Apagar lista"
 delete_page = "Deletar página"
 delete_selected = "Apagar selec."
 delete_selected_favorites = "Apagar os Favoritos selecionados"
+delete_tag = "Rótulo removido"
 delete_tags = "Deletar tags"
 delete_tags_by = "Deletar tags por"
 Department = "Departamento"
@@ -265,6 +272,7 @@ Displaying the top = "Primeiros resultados"
 Document Inspector = "Inspetor de documentos"
 Document Type = "Tipo de documento"
 DOI = "DOI"
+Draw Search Box = "Desenhar Caixa de pesquisa"
 Due = "Fim do empréstimo"
 Due Date = "Data fim do empréstimo"
 DVD = "DVD"
@@ -348,13 +356,14 @@ fav_email_fail = "Desculpe, ocorreu um erro. Favorito(s) não enviado(s) por ema
 fav_email_missing = "Alguns dados foram perdidos. Favorito(s) não enviado(s) por email."
 fav_email_success = "Favorito(s) enviado(s) por email conforme solicitado."
 fav_export = "Exportar Favoritos"
-fav_list_delete = "Lista de favoritos apagada"
-fav_list_delete_cancel = "Lista não apagada"
+fav_list_delete = "Lista de favoritos apagada."
+fav_list_delete_cancel = "Lista não apagada."
 fav_list_delete_fail = "Desculpe, ocorreu um erro. Lista não eliminada."
 Favorites = "Itens Guardados"
 Fee = "Multas"
 Feedback = "Comentário"
 feedback_name = "Nome"
+Field of activity = "Campo de atividade"
 File Description = "Descrição de arquivo"
 Filter = "Filtro"
 filter_tags = "Tags de filtro"
@@ -378,7 +387,9 @@ From = "De"
 Full description = "ver descrição completa"
 Full text is not displayed to guests = "Texto completo não é apresentado para usuários convidados (não logados)."
 fulltext_limit = "Limitar a artigos com texto completo disponível"
+Gender = "Gênero"
 Genre = "Gênero"
+Geographic Search = "Busca Geográfica"
 Geographic Terms = "Termos Geográficos"
 Geography = "Geografia"
 Get full text = "Obter o texto integral"
@@ -678,6 +689,7 @@ Number = "Número"
 number_decimal_point = "."
 number_thousands_separator = ","
 OAI Server = "Servidor OAI"
+Occupation = "Ocupação"
 of = "de"
 old_password = "Senha Antiga"
 On Reserve = "Reservado"
@@ -691,6 +703,7 @@ operator_exact = "é (exato)"
 OR = "OU"
 or create a new list = "ou criar uma nova lista"
 original = "Original"
+Other associated place = "Outro local associado"
 Other Authors = "Outros Autores"
 Other Editions = "Outras Edições"
 Other Libraries = "Outras Bibliotecas"
@@ -703,6 +716,8 @@ password_error_invalid = "Nova senha é inválida (ex: contêm caracteres invál
 password_error_not_unique = "Senha não foi alterada"
 password_maximum_length = "O tamanho máximo da senha é %%maxlength%% caracteres"
 password_minimum_length = "O tamanho mínimo da senha é %%minlength%% caracteres"
+password_only_alphanumeric = "Use somente números e letras de a até z"
+password_only_numeric = "Somente números"
 Passwords do not match = "As senhas não coincidem."
 Past = "Últimos"
 PDF Full Text = "Texto Completo em Formato PDF"
@@ -714,6 +729,8 @@ Physical Description = "Descrição Física"
 Physical Object = "Objeto físico"
 pick_up_location = "Local de levantamento"
 Place a Hold = "Localização"
+Place of birth = "Local de nascimento"
+Place of death = "Local de morte"
 Playing Time = "Duração"
 Please check back soon = "Por favor, volte em breve"
 Please contact the Library Reference Department for assistance = "Por favor contate o Serviço de Referência da Biblioteca para mais ajuda"
@@ -760,6 +777,7 @@ Recall This = "Reservar"
 recaptcha_not_passed = "Código CAPTCHA não coincidiu"
 Record Citations = "Citações do registro"
 Record Count = "Número de Registros"
+Record Type = "Tipo de registro"
 Recover Account = "Recuperar Conta"
 recovery_by_email = "Recuperar por email"
 recovery_by_username = "Recuperar pela identificação do usuário"
@@ -804,6 +822,7 @@ Requests = "Pedidos"
 Reserves = "Reservas"
 Reserves Search = "Busca de Reservas"
 Reserves Search Results = "Resultados da Busca de Reservas"
+result_count = "%%count%% resultados"
 Results = "Resultados"
 results = "resultados"
 Results for = "Resultados para"
@@ -855,6 +874,8 @@ Serial = "Periódico"
 Series = "coleção"
 Set = "Definir"
 Showing = "A mostrar"
+sidebar_close = "ocultar barra lateral"
+sidebar_expand = "Expandir barra lateral"
 Similar Items = "Registros relacionados"
 Skip to content = "Pular para o conteúdo"
 skip_confirm = "Tem certeza de que quer pular esta etapa?"
@@ -868,10 +889,12 @@ sms_success = "Mensagem enviada."
 Software = "Software"
 Sorry, but the help you requested is unavailable in your language. = "Desculpe, a ajuda que solicitou não está disponível em seu idioma."
 Sort = "Ordenar"
+sort_alphabetic = "Ordem alfabética"
 sort_author = "Autor"
 sort_author_author = "Ordem alfabética"
 sort_author_relevance = "Popularidade"
 sort_callnumber = "Área"
+sort_count = "Contagem de resultados"
 sort_relevance = "Relevância"
 sort_title = "Título"
 sort_year = "Data Descendente"
@@ -1016,9 +1039,6 @@ View this record in EBSCOhost = "Visualizar este registro na base de dados BSCOh
 visual_facet_parent = "De"
 Volume = "Volume"
 Volume Holdings = "Existências do Volume"
-vudl_access_denied = "Access não permitido."
-vudl_tab_docs = "Documentos"
-vudl_tab_pages = "Páginas"
 VuFind Configuration = "Configuração VuFind"
 vufind_upgrade_fail = "Não é possível atualizar o VuFind de momento"
 Warning: These citations may not always be 100% accurate = "Nota: a formatação da citação pode não corresponder 100% ao definido pela respectiva norma"
diff --git a/languages/pt.ini b/languages/pt.ini
index 06833fff89111469f08d7c8b5bddfdff8bcf9567..90da6d9c90b6950a45e99b8edad09f27a7b4671a 100644
--- a/languages/pt.ini
+++ b/languages/pt.ini
@@ -3,6 +3,7 @@
 ;note_785_7 = "Juntado com" ; incomplete translation (en.ini was later revised)
 Abstract = "Resumo"
 Access = "Acesso"
+access_denied = "Acesso negado."
 Account = "Conta"
 Add = "Adicionar"
 Add a Note = "Adicionar uma nota"
@@ -333,8 +334,8 @@ fav_email_fail = "Desculpe, ocorreu um erro. Favorito (s) não enviado(s) por em
 fav_email_missing = "Alguns dados foram perdidos. Favorito (s) não enviado(s) por email."
 fav_email_success = "Favorito(s) enviado(s) por email conforme solicitado."
 fav_export = "Exportar Favoritos"
-fav_list_delete = "Lista de favoritos apagada"
-fav_list_delete_cancel = "Lista não apagada"
+fav_list_delete = "Lista de favoritos apagada."
+fav_list_delete_cancel = "Lista não apagada."
 fav_list_delete_fail = "Desculpe, ocorreu um erro. Lista não eliminada."
 Favorites = "Favoritos"
 Fee = "Multas"
@@ -824,6 +825,7 @@ sms_success = "Mensagem enviada."
 Software = "Software"
 Sorry, but the help you requested is unavailable in your language. = "Desculpe, a ajuda que solicitou não está disponível em Português."
 Sort = "Ordenar"
+sort_alphabetic = "Ordem alfabética"
 sort_author = "Autor"
 sort_author_author = "Ordem alfabética"
 sort_author_relevance = "Popularidade"
@@ -967,9 +969,6 @@ View this record in EBSCOhost = "Veja este registoo no EBSCOhost"
 visual_facet_parent = "De"
 Volume = "Volume"
 Volume Holdings = "Existências do Volume"
-vudl_access_denied = "Acesso negado."
-vudl_tab_docs = "Documentos"
-vudl_tab_pages = "Páginas"
 VuFind Configuration = "Configuração VuFind"
 vufind_upgrade_fail = "Não é possível atualizar o VuFind de momento"
 Warning: These citations may not always be 100% accurate = "Nota: a formatação da citação pode não corresponder 100% ao definido pela respectiva norma"
diff --git a/languages/ru.ini b/languages/ru.ini
index 51142d844581ffba17ca58c20c61db87c33aea87..134d1f77527cf0eb69cf2598fc65432b32799c86 100644
--- a/languages/ru.ini
+++ b/languages/ru.ini
@@ -20,6 +20,7 @@
 Abstract = "Краткий обзор"
 Access = "Доступ"
 Access URL = "Доступ через URL"
+access_denied = "Доступ запрещен."
 Accession Number = "Номер доступа"
 Account = "Акаунт"
 Add = "Добавить"
@@ -364,7 +365,7 @@ fav_email_fail = "Произошла ошибка. Ваши избранные 
 fav_email_missing = "Пропущены некоторые данные. Ваши избранные не были указаны по Email."
 fav_email_success = "Ваши избранные были указаны по Email."
 fav_export = "Экспорт избранного"
-fav_list_delete = "Список избранного был удален"
+fav_list_delete = "Список избранного был удален."
 fav_list_delete_cancel = "Этот список не удален."
 fav_list_delete_fail = "Произошла ошибка. Ваш список удален."
 Favorites = "Избранное"
@@ -883,6 +884,7 @@ sms_success = "Сообщение отправлено."
 Software = "Программное обеспечение"
 Sorry, but the help you requested is unavailable in your language. = "Запрошення справка недоступна на Вашем языке."
 Sort = "Сортировка"
+sort_alphabetic = "Алфавитный"
 sort_author = "Автор"
 sort_author_author = "Алфавитный"
 sort_author_relevance = "Популярность"
@@ -1031,9 +1033,6 @@ View this record in EBSCOhost = "Просмотр записи в EBSCOhost"
 visual_facet_parent = "от"
 Volume = "Том"
 Volume Holdings = "Объемы фондов"
-vudl_access_denied = "Доступ запрещен."
-vudl_tab_docs = "докум."
-vudl_tab_pages = "стран."
 VuFind Configuration = "Конфигурация VuFind"
 vufind_upgrade_fail = "Невозможно обновлять версию VuFind в это время"
 Warning: These citations may not always be 100% accurate = "Предупреждение: эти цитированмия не могут быть всегда правильны на 100%"
diff --git a/languages/sl.ini b/languages/sl.ini
index 0a4fa17528117a0532deecb4faa1ffec6540478a..29519ba195a96909466cc91b126c1b65a75e0db6 100644
--- a/languages/sl.ini
+++ b/languages/sl.ini
@@ -1,5 +1,6 @@
 ;note_785_7 = "Združeno z" ; incomplete translation (en.ini was later revised)
 Access = "Dostop"
+access_denied = "Dostop zavrnjen."
 Account = "Račun"
 Add = "Dodaj"
 Add a Note = "Dodaj opombo"
@@ -288,8 +289,8 @@ fav_email_fail = "Oprostite, pojavila se je napaka. Vaše priljubljene niso posl
 fav_email_missing = "Manjkajo nekateri podatki. Vaše priljubljene niso poslane po emailu."
 fav_email_success = "Vaše priljubljene so poslane po emailu.."
 fav_export = "Izvozi priljubljene"
-fav_list_delete = "Izbran je bil seznam"
-fav_list_delete_cancel = "Ta seznam ni bil izbrisan"
+fav_list_delete = "Izbran je bil seznam."
+fav_list_delete_cancel = "Ta seznam ni bil izbrisan."
 fav_list_delete_fail = "Oprostite, pojavila se je napaka. Vaš seznam ni bil izbrisan."
 Favorites = "Priljubljene"
 Fee = "ÄŒlanarina/zamudnina"
@@ -690,6 +691,7 @@ sms_success = "Sporočilo poslano."
 Software = "Software"
 Sorry, but the help you requested is unavailable in your language. = "Oprostite, pomoč, ni na voljo v vašem jeziku."
 Sort = "Razvrsti"
+sort_alphabetic = "Abecedno"
 sort_author = "PO AvtorJU"
 sort_author_author = "Abecedno"
 sort_author_relevance = "Po priljubljenosti"
@@ -804,9 +806,6 @@ View Records = "poglej zadetke"
 visual_facet_parent = "Iz"
 Volume = "Zvezek"
 Volume Holdings = "Zaloga"
-vudl_access_denied = "Dostop zavrnjen."
-vudl_tab_docs = "Dokumenti"
-vudl_tab_pages = "Strani"
 VuFind Configuration = "VuFind nastavitve"
 vufind_upgrade_fail = "Trenutno ne moremo nadgraditi Vufinda"
 Warning: These citations may not always be 100% accurate = "Opozorilo: Ti citati niso vedno 100% točni"
diff --git a/languages/sv.ini b/languages/sv.ini
index 1b6b75eb58b67360f597b67271fe3aec2b6b9b57..66953ba17390030dfd95717c119c3dd452d56fd3 100644
--- a/languages/sv.ini
+++ b/languages/sv.ini
@@ -2,6 +2,7 @@
 Abstract = "Abstrakt"
 Access = "Tillgång"
 Access URL = "Länk till materialet"
+access_denied = "Ã…tkomst nekad"
 Accession Number = "Accessionsnummer"
 Account = "Konto"
 Add = "Lägg till"
@@ -58,8 +59,10 @@ An error occurred during execution; please try again later. = "Utförandet missl
 AND = "OCH"
 anonymous_tags = "Anonyma taggar"
 APA Citation = "APA-referens"
+applied_filter = "Filter i bruk"
 Article = "Artikel"
 Ask a Librarian = "Fråga biblioteket"
+Associated country = "Associerat land"
 Audience = "Publik"
 Audio = "Ljudupptagning"
 authentication_error_admin = "Inloggningen misslyckades. Kontakta systemadministratören."
@@ -74,6 +77,7 @@ Author Browse = "Bläddra bland upphovsmän"
 Author Notes = "Anmärkningar om upphovsman"
 Author Results for = "Upphovsmän hittade med sökningen"
 Author Search Results = "Sökresultat med upphovsmän"
+Authority File = "Auktoritetskälla"
 Authors = "Författarna"
 Authors Related to Your Search = "Upphovsmän relaterade till sökningen"
 Auto configuration is currently disabled = "Auto configuration is currently disabled"
@@ -235,8 +239,11 @@ Create a List = "Skapa en lista"
 Create New Account = "Skapa ett nytt konto"
 Create New Password = "Skapa ett nytt lösenord"
 Created = "Skapad"
+CreatorRoles::bnd = "Bokbindare"
 Database = "Databas"
 Date = "Datum"
+Date of birth = "Födelsedatum"
+Date of death = "Dödsdatum"
 date_day_placeholder = "D"
 date_from = "Från och med"
 date_month_placeholder = "M"
@@ -253,6 +260,7 @@ delete_list = "Radera listan"
 delete_page = "Radera sidan"
 delete_selected = "Radera valda"
 delete_selected_favorites = "Radera valda favoriter"
+delete_tag = "Radera taggen"
 delete_tags = "Radera taggar"
 delete_tags_by = "Radera taggar av"
 Department = "Avdelning"
@@ -264,6 +272,7 @@ Displaying the top = "Visar de första"
 Document Inspector = "Document Inspector"
 Document Type = "Dokument typ"
 DOI = "DOI"
+Draw Search Box = "Välj region"
 Due = "Förfaller"
 Due Date = "Förfallodag"
 DVD = "DVD"
@@ -354,6 +363,7 @@ Favorites = "Favoriter"
 Fee = "Avgift"
 Feedback = "Respons"
 feedback_name = "Ditt namn"
+Field of activity = "Verksamhetsområde"
 File Description = "Filbeskrivning"
 Filter = "Filter"
 filter_tags = "Filtrera taggar"
@@ -377,7 +387,9 @@ From = "Från"
 Full description = "Full beskrivning"
 Full text is not displayed to guests = "Fulltext visas inte till gäster."
 fulltext_limit = "Begränsa till fulltextartiklar"
+Gender = "Kön"
 Genre = "Genre"
+Geographic Search = "Geografisk sökning"
 Geographic Terms = "Geografiska termer"
 Geography = "Geografisk"
 Get full text = "Hämta fulltext"
@@ -428,8 +440,9 @@ hold_date_past = "Ange ett datum som infaller senare än idag"
 hold_empty_selection = "Inga reserveringar valda"
 hold_error_blocked = "Reservering är inte möjlig, eftersom du har låneförbud eller redan har en likvärdig reservering."
 hold_error_fail = "Begäran misslyckades. Kontakta kundtjänst."
-hold_invalid_pickup = "Reservering kan inte göras eftersom det finns exemplar tillgängliga för utlåning i biblioteket."
-hold_invalid_request_group = "Det valda avhämtningsstället är felaktigt. Försök igen."
+hold_invalid_pickup = "Det valda avhämtningsstället är felaktigt. Försök igen."
+hold_invalid_request_group = "Det valda gruppet är felaktigt. Försök igen."
+hold_items_available = "Reservering kan inte göras eftersom det finns exemplar tillgängliga för utlåning i biblioteket."
 hold_login = "för att kontrollera reserveringsmöjlighet"
 hold_place = "Reservera"
 hold_place_fail_missing = "Reservering misslyckades p.g.a. att uppgifter saknas. Kontakta kundtjänst."
@@ -677,6 +690,7 @@ Number = "Nummer"
 number_decimal_point = ","
 number_thousands_separator = " "
 OAI Server = "OAI Servern"
+Occupation = "Yrke"
 of = "av"
 old_password = "Gamla lösenordet"
 On Reserve = "I begränsad användning"
@@ -690,6 +704,7 @@ operator_exact = "är (exakt)"
 OR = "ELLER"
 or create a new list = "eller skapa en ny lista"
 original = "Original"
+Other associated place = "Annan associerad plats"
 Other Authors = "Övriga upphovsmän"
 Other Editions = "Andra upplagor"
 Other Libraries = "Andra bibliotek"
@@ -702,6 +717,8 @@ password_error_invalid = "Nytt lösenord är ogiltigt (t.ex. innehåller ogiltig
 password_error_not_unique = "Lösenordet inte ändrat"
 password_maximum_length = "Högsta längd på lösenord är %%maxlength%% tecken"
 password_minimum_length = "Minsta längd på lösenord är %%minlength%% tecken"
+password_only_alphanumeric = "Bara siffror och bokstäver a-z"
+password_only_numeric = "Bara siffror"
 Passwords do not match = "Lösenorden stämmer inte"
 Past = "Senaste"
 PDF Full Text = "PDF-fulltext"
@@ -713,6 +730,8 @@ Physical Description = "Fysisk beskrivning"
 Physical Object = "Fysiskt objekt"
 pick_up_location = "Avhämtningsställe"
 Place a Hold = "Reservera"
+Place of birth = "Födelseort"
+Place of death = "Dödsort"
 Playing Time = "Speltid"
 Please check back soon = "Kontrollera på nytt om en stund"
 Please contact the Library Reference Department for assistance = "Kontakta kundtjänst för hjälp"
@@ -759,6 +778,7 @@ Recall This = "Reservera denna"
 recaptcha_not_passed = "CAPTCHA inte klarat"
 Record Citations = "Hänvisningar gällande denna post"
 Record Count = "Antal poster"
+Record Type = "Posttyp"
 Recover Account = "Ã…tervinna kontot"
 recovery_by_email = "Hämta via e-post"
 recovery_by_username = "Hämta med användarnamn"
@@ -803,6 +823,7 @@ Requests = "Reserveringar"
 Reserves = "Material i begränsad användning"
 Reserves Search = "Sök material i begränsad användning"
 Reserves Search Results = "Sökresultat för material i begränsad användning"
+result_count = "%%count%% resultat"
 Results = "Resultat"
 results = "resultat"
 Results for = "Resultat för sökningen"
@@ -854,6 +875,8 @@ Serial = "Periodisk publikation"
 Series = "Serie"
 Set = "Justera"
 Showing = "Visas"
+sidebar_close = "Kollaps sidpanelen"
+sidebar_expand = "Expandera sidpanelen"
 Similar Items = "Liknande verk"
 Skip to content = "Hoppa till innehåll"
 skip_confirm = "Är du säker på att du vill hoppa över detta steg?"
@@ -867,10 +890,12 @@ sms_success = "Meddelandet har skickats."
 Software = "Datorprogram"
 Sorry, but the help you requested is unavailable in your language. = "Denna hjälptext finns inte på svenska"
 Sort = "Sortera"
+sort_alphabetic = "Alfabetiskt"
 sort_author = "Upphovsman"
 sort_author_author = "Alfabetiskt"
 sort_author_relevance = "Relevans"
 sort_callnumber = "Signum"
+sort_count = "Antal"
 sort_relevance = "Relevans"
 sort_title = "Titel"
 sort_year = "Tid (nyaste först)"
@@ -1015,9 +1040,6 @@ View this record in EBSCOhost = "Visa denna post i EBSCOhost"
 visual_facet_parent = "Från"
 Volume = "Volym"
 Volume Holdings = "Tillgängliga nummer"
-vudl_access_denied = "Ã…tkomst nekad"
-vudl_tab_docs = "Dokumenter"
-vudl_tab_pages = "Sidor"
 VuFind Configuration = "VuFind-inställningar"
 vufind_upgrade_fail = "Vi kan inte uppgradera VuFind vid denna tidpunkt"
 Warning: These citations may not always be 100% accurate = "Varning: dessa hänvisningar är inte alltid fullständigt riktiga"
diff --git a/languages/tr.ini b/languages/tr.ini
index e95212538f40c822efbd26d84729da6b196eddd3..b7014ed6849f2fa05660805209e584721777993b 100644
--- a/languages/tr.ini
+++ b/languages/tr.ini
@@ -14,6 +14,7 @@
 Abstract = "Özet"
 Access = "EriÅŸim"
 Access URL = "EriÅŸim Adresi URL"
+access_denied = "Giriþ engellendi."
 Accession Number = "Aksesiyon No"
 Account = "Hesap"
 Add = "Ekle"
@@ -70,8 +71,10 @@ An error occurred during execution; please try again later. = "Yürütme sıras
 AND = "VE"
 anonymous_tags = "Anonim Etiketler"
 APA Citation = "APA Alıntı"
+applied_filter = "Uygulanmış Flitre"
 Article = "Makale"
 Ask a Librarian = "Kütüphaneciye Sor"
+Associated country = "Ä°lgli Ãœlke"
 Audience = "Ä°zleyiciler"
 Audio = "Ä°ÅŸitsel"
 authentication_error_admin = "Yönetici Girişi Onaylanmadı"
@@ -86,6 +89,7 @@ Author Browse = "Yazara Göre Listele"
 Author Notes = "Yazar Notları"
 Author Results for = "Yazar Sonuçları"
 Author Search Results = "Yazar Taraması Sonucu"
+Authority File = "Otorite Dosyası"
 Authors = "Yazarlar"
 Authors Related to Your Search = "Taramanızla ilgili Yazarlar"
 Auto configuration is currently disabled = "Otomatik yapılandırma şu anda devre dışı"
@@ -249,6 +253,8 @@ Create New Password = "Yeni Åžifre OluÅŸtur"
 Created = "OluÅŸturma Tarihi"
 Database = "Veritabanı"
 Date = "Tarih"
+Date of birth = "DoÄŸum Tarihi"
+Date of death = "Ölüm Tarihi"
 date_day_placeholder = "G"
 date_from = "den"
 date_month_placeholder = "A"
@@ -265,6 +271,7 @@ delete_list = "Listeyi Sil"
 delete_page = "Sayfayý Sil"
 delete_selected = "Seçilmişleri Sil"
 delete_selected_favorites = "Seçilmiş Favorileri Sil"
+delete_tag = "SilinmiÅŸ Etiket"
 delete_tags = "Etiketleri Sil"
 delete_tags_by = "Etiketleri Sil(ilgili)"
 Department = "Materyalin BulunduÄŸu Yerler"
@@ -276,6 +283,7 @@ Displaying the top = "Üst gösteriliyor"
 Document Inspector = "Doküman Denetcisi"
 Document Type = "Doküman Türü"
 DOI = "DOI"
+Draw Search Box = "Arama Kutusu Düzenle"
 Due = "Ä°ade Tarihi"
 Due Date = "Ä°ade tarihi"
 DVD = "DVD"
@@ -359,13 +367,14 @@ fav_email_fail = "Bir hata oluştu. Favorileriniz email ile gönderilemedi."
 fav_email_missing = "Bazı bilgiler eksik. Favorileriniz email ile gönderilemedi."
 fav_email_success = "Favorileriniz isteğiniz üzere email ile gönderildi."
 fav_export = "Favorileri Aktar"
-fav_list_delete = "Favori listeniz silindi"
-fav_list_delete_cancel = "Bu liste silinmedi"
+fav_list_delete = "Favori listeniz silindi."
+fav_list_delete_cancel = "Bu liste silinmedi."
 fav_list_delete_fail = "Bir hata oluÅŸtu. Listeniz silinemedi."
 Favorites = "Favorilerim"
 Fee = "Gecikme Cezası"
 Feedback = "Görüsleriniz"
 feedback_name = "Adiniz"
+Field of activity = "Aktivite Alanı"
 File Description = "Dosya tanımı"
 Filter = "Filtre"
 filter_tags = "Etiketleri Flitrele"
@@ -389,7 +398,9 @@ From = "den"
 Full description = "Ful tanımlama"
 Full text is not displayed to guests = "Tam metin misafirler için gösterilmez."
 fulltext_limit = "Tam Metin Limiti"
+Gender = "Cinsiyet"
 Genre = "Tür"
+Geographic Search = "CoÄŸrafik Tarama"
 Geographic Terms = "CoÄŸrafik Terimler"
 Geography = "CoÄŸrafi"
 Get full text = "Tam Metin EriÅŸim"
@@ -689,6 +700,7 @@ Number = "Numara"
 number_decimal_point = "."
 number_thousands_separator = ","
 OAI Server = "OAI Sunucu"
+Occupation = "Meslek"
 of = "arası kayıtlar."
 old_password = "Eski Åžifre"
 On Reserve = "Rezerve"
@@ -702,6 +714,7 @@ operator_exact = "tam"
 OR = "YADA"
 or create a new list = "Yeni liste oluÅŸtur"
 original = "Orjinal"
+Other associated place = "DiÄŸer Ä°liÅŸkili Yer"
 Other Authors = "DiÄŸer Yazarlar"
 Other Editions = "DiÄŸer Edisyonlar"
 Other Libraries = "Diğer Kütüphaneler"
@@ -714,6 +727,8 @@ password_error_invalid = "Yeni Şifre geçersizi (geçersiz karakterler içeriyo
 password_error_not_unique = "Åžifre deÄŸiÅŸmedi"
 password_maximum_length = "Maksimum ÅŸifre uzunluÄŸu %%maxlength%% karakter"
 password_minimum_length = "Minimum ÅŸifre uzunluÄŸu %%minlength%% karakter"
+password_only_alphanumeric = "Sayılar ve Harfler Sadece A-Z"
+password_only_numeric = "Sadece Sayılar"
 Passwords do not match = "Åžifre eÅŸleÅŸmedi"
 Past = "Geçen"
 PDF Full Text = "PDF Tam Metin"
@@ -725,6 +740,8 @@ Physical Description = "Fiziksel Özellikler"
 Physical Object = "Fiziksel Nesne"
 pick_up_location = "Bulunduğu Kütüphane"
 Place a Hold = "Rezerve"
+Place of birth = "DoÄŸum Yeri"
+Place of death = "Ölüm Yeri"
 Playing Time = "Oyun Zamanı"
 Please check back soon = "Daha sonra tekrar kontrol ediniz"
 Please contact the Library Reference Department for assistance = "Sistem sorumlusuna e-posta yolla"
@@ -771,6 +788,7 @@ Recall This = "Rezerve"
 recaptcha_not_passed = "CAPTCHA kodu uygun deÄŸil"
 Record Citations = "Alıntılar"
 Record Count = "Kayıt Sayısı"
+Record Type = "Kayıt Türü"
 Recover Account = "Hesabı Kurtar"
 recovery_by_email = "Eposta ile kurtar"
 recovery_by_username = "Kullanıcı adı ile kurtar"
@@ -815,6 +833,7 @@ Requests = "Ä°stekler"
 Reserves = "Rezerve"
 Reserves Search = "Rezerve Tara"
 Reserves Search Results = "Rezerve Tarama Sonucu"
+result_count = "%%count%% sonuçlar"
 Results = "Sonuçlar"
 results = "sonuçlar"
 Results for = "Results for"
@@ -866,6 +885,8 @@ Serial = "Süreli"
 Series = "Seri Bilgileri"
 Set = "Set"
 Showing = "Gösterilen"
+sidebar_close = "Kenar Çubuğunu Daralt"
+sidebar_expand = "Kenar Çubuğunu Genişlet"
 Similar Items = "Benzer Materyaller"
 Skip to content = "İçeriği atla"
 skip_confirm = "Bu adımı geçmek istediğinizden eminmisiniz?"
@@ -879,10 +900,12 @@ sms_success = "SMS Gönderildi"
 Software = "Yazılım"
 Sorry, but the help you requested is unavailable in your language. = "İstemiş olduğunuz yardım kendi dilinizde mevcut değildir."
 Sort = "Sırala"
+sort_alphabetic = "Alfabetik"
 sort_author = "Yazar"
 sort_author_author = "Yazarı Yazara Göre Sırala"
 sort_author_relevance = "Yazar ilgililiğine göre sırala"
 sort_callnumber = "Yer Numarası"
+sort_count = "Sonuç Sayısı"
 sort_relevance = "Ä°lgili"
 sort_title = "Materyal Adı"
 sort_year = "Tarih-Azalan"
@@ -1027,9 +1050,6 @@ View this record in EBSCOhost = "Bu Kaydı EBSCOhostda Görüntüle"
 visual_facet_parent = "den"
 Volume = "Volüm"
 Volume Holdings = "Cilt EriÅŸim Bilgileri"
-vudl_access_denied = "Giriþ engellendi."
-vudl_tab_docs = "Dokümanlar"
-vudl_tab_pages = "Sayfalar"
 VuFind Configuration = "Vufind Konfigürasyonu"
 vufind_upgrade_fail = "Vufind'i şu anda güncelleyemiyoruz."
 Warning: These citations may not always be 100% accurate = "Uyarı: Bu alıntı herzaman %100 doğru olmayabilir."
diff --git a/languages/zh-cn.ini b/languages/zh-cn.ini
index 41bd16a909d4b1f7f57e27b73829b83c12793b17..26c7c0168a7363b56514b7d65e2183cfb202a8c4 100644
--- a/languages/zh-cn.ini
+++ b/languages/zh-cn.ini
@@ -558,6 +558,7 @@ sms_success = "邮件已送出."
 Software = "软件"
 Sorry, but the help you requested is unavailable in your language. = "抱歉,您请求帮助您的语言不可用."
 Sort = "排序"
+sort_alphabetic = "拼音"
 sort_author = "作者排序"
 sort_author_author = "作者字母排序"
 sort_author_relevance = "作者相关性排序"
diff --git a/languages/zh.ini b/languages/zh.ini
index 52914e9455643ce28f7f4c6c20a08765a49ddeab..ca7f776600d15382ccfcc51ff2abafb0690d78f8 100644
--- a/languages/zh.ini
+++ b/languages/zh.ini
@@ -558,6 +558,7 @@ sms_success = "郵件已送出."
 Software = "軟件"
 Sorry, but the help you requested is unavailable in your language. = "抱歉,您請求幫助您的語言不可用."
 Sort = "排序"
+sort_alphabetic = "拼音"
 sort_author = "作者排序"
 sort_author_author = "作者字母排序"
 sort_author_relevance = "作者相關性排序"
diff --git a/module/VuDL/config/module.config.php b/module/VuDL/config/module.config.php
deleted file mode 100644
index fc41092e9d9eae9e516a871e63b616a9eba991ba..0000000000000000000000000000000000000000
--- a/module/VuDL/config/module.config.php
+++ /dev/null
@@ -1,98 +0,0 @@
-<?php
-namespace VuDL\Module\Configuration;
-
-$config = [
-    'controllers' => [
-        'invokables' => [
-            'vudl' => 'VuDL\Controller\VudlController'
-        ],
-    ],
-    'service_manager' => [
-        'factories' => [
-            'VuDL\Connection\Manager' => 'VuDL\Factory::getConnectionManager',
-            'VuDL\Connection\Fedora' => 'VuDL\Factory::getConnectionFedora',
-            'VuDL\Connection\Solr' => 'VuDL\Factory::getConnectionSolr',
-        ],
-    ],
-    'vufind' => [
-        'plugin_managers' => [
-            'recorddriver' => [
-                'factories' => [
-                    'solrvudl' => 'VuDL\Factory::getRecordDriver',
-                ],
-            ],
-        ],
-    ],
-    'router' => [
-        'routes' => [
-            'files' => [
-                'type' => 'Zend\Mvc\Router\Http\Segment',
-                'options' => [
-                    'route'    => '/files/:id/:type'
-                ]
-            ],
-            'vudl-about' => [
-                'type' => 'Zend\Mvc\Router\Http\Literal',
-                'options' => [
-                    'route'    => '/VuDL/About',
-                    'defaults' => [
-                        'controller' => 'VuDL',
-                        'action'     => 'About',
-                    ]
-                ]
-            ],
-            'vudl-default-collection' => [
-                'type' => 'Zend\Mvc\Router\Http\Segment',
-                'options' => [
-                    'route'    => '/Collection[/]',
-                    'defaults' => [
-                        'controller' => 'VuDL',
-                        'action'     => 'Collections'
-                    ]
-                ]
-            ],
-            'vudl-grid' => [
-                'type' => 'Zend\Mvc\Router\Http\Segment',
-                'options' => [
-                    'route'    => '/Grid/:id',
-                    'defaults' => [
-                        'controller' => 'VuDL',
-                        'action'     => 'Grid'
-                    ]
-                ]
-            ],
-            'vudl-home' => [
-                'type' => 'Zend\Mvc\Router\Http\Segment',
-                'options' => [
-                    'route'    => '/VuDL/Home[/]',
-                    'defaults' => [
-                        'controller' => 'VuDL',
-                        'action'     => 'Home',
-                    ]
-                ]
-            ],
-            'vudl-record' => [
-                'type' => 'Zend\Mvc\Router\Http\Segment',
-                'options' => [
-                    'route'    => '/Item/:id',
-                    'defaults' => [
-                        'controller' => 'VuDL',
-                        'action'     => 'Record'
-                    ]
-                ]
-            ],
-            'vudl-sibling' => [
-                'type' => 'Zend\Mvc\Router\Http\Segment',
-                'options' => [
-                    'route'    => '/Vudl/Sibling/',
-                    'defaults' => [
-                        'controller' => 'VuDL',
-                        'action'     => 'Sibling'
-                    ]
-                ]
-            ],
-        ]
-    ],
-];
-
-return $config;
diff --git a/module/VuDL/src/VuDL/Connection/AbstractBase.php b/module/VuDL/src/VuDL/Connection/AbstractBase.php
deleted file mode 100644
index d0920b48a51d6bac6dd21c0f762978afbcc3a09b..0000000000000000000000000000000000000000
--- a/module/VuDL/src/VuDL/Connection/AbstractBase.php
+++ /dev/null
@@ -1,203 +0,0 @@
-<?php
-/**
- * VuDL connection base class (defines some methods to talk to VuDL sources)
- *
- * PHP version 5
- *
- * Copyright (C) Villanova University 2010.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License version 2,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
- *
- * @category VuFind
- * @package  Controller
- * @author   Chris Hallberg <challber@villanova.edu>
- * @license  http://opensource.org/licenses/gpl-3.0.php GNU General Public License
- * @link     https://vufind.org/wiki/
- */
-namespace VuDL\Connection;
-use VuFindHttp\HttpServiceInterface,
-    VuFindSearch\ParamBag;
-
-/**
- * VuDL connection base class
- *
- * @category VuFind
- * @package  Controller
- * @author   Chris Hallberg <challber@villanova.edu>
- * @license  http://opensource.org/licenses/gpl-3.0.php GNU General Public License
- * @link     https://vufind.org/wiki/
- */
-class AbstractBase implements \VuFindHttp\HttpServiceAwareInterface
-{
-    use \VuFindHttp\HttpServiceAwareTrait;
-
-    /**
-     * VuDL config
-     *
-     * @var \Zend\Config\Config
-     */
-    protected $config = null;
-
-    /**
-     * Parent List data cache
-     *
-     * @var array
-     */
-    protected $parentLists = [];
-
-    /**
-     * Constructor
-     *
-     * @param \Zend\Config\Config $config config
-     */
-    public function __construct($config)
-    {
-        $this->config = $config;
-    }
-
-    /**
-     * Get root id from config
-     *
-     * @return string
-     */
-    protected function getRootId()
-    {
-        return isset($this->config->General->root_id)
-            ? $this->config->General->root_id
-            : null;
-    }
-
-    /**
-     * Get VuDL detail fields.
-     *
-     * @return array
-     */
-    protected function getDetailsList()
-    {
-        return isset($this->config->Details)
-            ? $this->config->Details->toArray()
-            : [];
-    }
-
-    /**
-     * Get Fedora Page Length.
-     *
-     * @return string
-     */
-    public function getPageLength()
-    {
-        return isset($this->config->General->page_length)
-            ? $this->config->General->page_length
-            : 16;
-    }
-
-    /**
-     * Format details properly into the correct keys
-     *
-     * @param array $record Record object
-     *
-     * @return string
-     */
-    protected function formatDetails($record)
-    {
-        // Format details
-        // Get config for which details we want
-        $detailsList = $this->getDetailsList();
-        if (empty($detailsList)) {
-            throw new \Exception('Missing [Details] in VuDL.ini');
-        }
-        $details = [];
-        foreach ($detailsList as $key => $title) {
-            $keys = explode(',', $key);
-            $field = false;
-            for ($i = 0;$i < count($keys);$i++) {
-                if (isset($record[$keys[$i]])) {
-                    $field = $keys[$i];
-                    break;
-                }
-            }
-            if (false === $field) {
-                continue;
-            }
-            if (count($keys) == 1) {
-                if (isset($record[$keys[0]])) {
-                    $details[$field] = [
-                        'title' => $title,
-                        'value' => $record[$keys[0]]
-                    ];
-                }
-            } else {
-                $value = [];
-                foreach ($keys as $k) {
-                    if (isset($record[$k])) {
-                        if (is_array($record[$k])) {
-                            $value = array_merge($value, $record[$k]);
-                        } else {
-                            $value[] = $record[$k];
-                        }
-                    }
-                }
-                $details[$field] = [
-                    'title' => $title,
-                    'value' => $value
-                ];
-            }
-        }
-        return $details;
-    }
-
-    /**
-     * A method to search from the root id down to the current record
-     *   creating multiple breadcrumb paths along the way
-     *
-     * @param array  $tree Array of parents by id with title and array of children
-     * @param string $id   Target id to stop at
-     *
-     * @return array Array of arrays with parents in order
-     */
-    protected function traceParents($tree, $id)
-    {
-        // BFS from top (root id) to target $id
-        $queue = [
-            [
-                'id' => $this->getRootId(),
-                'path' => []
-            ]
-        ];
-        $ret = [];
-        while (!empty($queue)) {
-            $current = array_shift($queue);
-            $record = $tree[$current['id']];
-            $path = $current['path'];
-            if ($current['id'] != $this->getRootId()) {
-                $path[$current['id']] = $record['title'];
-            }
-            foreach ($record['children'] as $cid) {
-                // At target
-                if ($cid == $id) {
-                    array_push($ret, $path);
-                } else { // Add to queue for more
-                    array_push(
-                        $queue,
-                        [
-                            'id' => $cid,
-                            'path' => $path
-                        ]
-                    );
-                }
-            }
-        }
-        return $ret;
-    }
-}
diff --git a/module/VuDL/src/VuDL/Connection/Fedora.php b/module/VuDL/src/VuDL/Connection/Fedora.php
deleted file mode 100644
index bdf922d346983dc5cadc41794f28b2b11754d354..0000000000000000000000000000000000000000
--- a/module/VuDL/src/VuDL/Connection/Fedora.php
+++ /dev/null
@@ -1,489 +0,0 @@
-<?php
-/**
- * VuDL to Fedora connection class (defines some methods to talk to Fedora)
- *
- * PHP version 5
- *
- * Copyright (C) Villanova University 2010.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License version 2,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
- *
- * @category VuFind
- * @package  Controller
- * @author   Chris Hallberg <challber@villanova.edu>
- * @license  http://opensource.org/licenses/gpl-3.0.php GNU General Public License
- * @link     https://vufind.org/wiki/
- */
-namespace VuDL\Connection;
-use VuFindHttp\HttpServiceInterface,
-    VuFindSearch\ParamBag;
-
-/**
- * VuDL-Fedora connection class
- *
- * @category VuFind
- * @package  Controller
- * @author   Chris Hallberg <challber@villanova.edu>
- * @license  http://opensource.org/licenses/gpl-3.0.php GNU General Public License
- * @link     https://vufind.org/wiki/
- */
-class Fedora extends AbstractBase
-{
-    /**
-     * Datastreams data cache
-     *
-     * @var array
-     */
-    protected $datastreams = [];
-
-    /**
-     * Get Fedora Base URL.
-     *
-     * @return string
-     */
-    public function getBase()
-    {
-        return isset($this->config->Fedora->url_base)
-            ? $this->config->Fedora->url_base
-            : null;
-    }
-
-    /**
-     * Returns an array of classes for this object
-     *
-     * @param string $id record id
-     *
-     * @return array
-     */
-    public function getClasses($id)
-    {
-        $data = $this->getDatastreamContent($id, 'RELS-EXT');
-        $matches = [];
-        preg_match_all(
-            '/rdf:resource="info:fedora\/vudl-system:([^"]+)/',
-            $data,
-            $matches
-        );
-        return $matches[1];
-    }
-
-    /**
-     * Returns file contents of the structmap, our most common call
-     *
-     * @param string $id  Record id
-     * @param bool   $xml Return data as SimpleXMLElement?
-     *
-     * @return string|\SimpleXMLElement
-     */
-    public function getDatastreams($id, $xml = false)
-    {
-        if (!isset($this->datastreams[$id])) {
-            $this->datastreams[$id] = $this->getDatastreamContent(
-                $id,
-                '/datastreams?format=xml',
-                true
-            );
-        }
-        if ($xml) {
-            return simplexml_load_string($this->datastreams[$id]);
-        } else {
-            return $this->datastreams[$id];
-        }
-    }
-
-    /**
-     * Return the content of a datastream.
-     *
-     * @param string $id         Record id
-     * @param string $stream     Name of stream to retrieve
-     * @param bool   $justStream Do not append /content and return from url as is
-     *
-     * @return string
-     */
-    public function getDatastreamContent($id, $stream, $justStream = false)
-    {
-        if ($justStream) {
-            $url = $this->getBase() . $id . $stream;
-        } else {
-            $url = $this->getBase() . $id . '/datastreams/' . $stream . '/content';
-        }
-        return file_get_contents($url);
-    }
-
-    /**
-     * Return the headers of a datastream.
-     *
-     * @param string $id     Record id
-     * @param string $stream Name of stream to retrieve
-     *
-     * @return string
-     */
-    public function getDatastreamHeaders($id, $stream)
-    {
-        return get_headers(
-            $this->getBase() . $id . '/datastreams/' . $stream . '/content'
-        );
-    }
-
-    /**
-     * Get details for the sidebar on a record.
-     *
-     * @param string $id     ID to retrieve
-     * @param bool   $format Send result through formatDetails?
-     *
-     * @return string
-     */
-    public function getDetails($id, $format = false)
-    {
-        $dc = [];
-        preg_match_all(
-            '/<[^\/]*dc:([^ >]+)>([^<]+)/',
-            $this->getDatastreamContent($id, 'DC'),
-            $dc
-        );
-        $details = [];
-        foreach ($dc[2] as $i => $detail) {
-            $details[$dc[1][$i]] = $detail;
-        }
-        if ($format) {
-            return $this->formatDetails($details);
-        }
-        return $details;
-    }
-
-    /**
-     * Get an HTTP client
-     *
-     * @param string $url URL for client to access
-     *
-     * @return \Zend\Http\Client
-     */
-    public function getHttpClient($url)
-    {
-        if ($this->httpService) {
-            return $this->httpService->createClient($url);
-        }
-        return new \Zend\Http\Client($url);
-    }
-
-    /**
-     * Get an item's label
-     *
-     * @param string $id Record's id
-     *
-     * @return string
-     */
-    public function getLabel($id)
-    {
-        $query = 'select $memberTitle from <#ri> '
-            . 'where $member <dc:identifier> \'' . $id . '\' '
-            . 'and $member <fedora-model:label> $memberTitle';
-        $response = $this->query($query);
-        $list = explode("\n", $response->getBody());
-        return $list[1];
-    }
-
-    /**
-     * Tuple call to return and parse a list of members...
-     *
-     * @param string $root ...for this id
-     *
-     * @return array of members in order
-     */
-    public function getMemberList($root)
-    {
-        $query = 'select $memberPID $memberTitle from <#ri> '
-            . 'where $member <fedora-rels-ext:isMemberOf> <info:fedora/'
-            . $root . '> '
-            . 'and $member <fedora-model:label> $memberTitle '
-            . 'and $member <dc:identifier> $memberPID';
-        $response = $this->query($query);
-        $list = explode("\n", $response->getBody());
-        $items = [];
-        for ($i = 1;$i < count($list);$i++) {
-            if (empty($list[$i])) {
-                continue;
-            }
-            list($id,) = explode(',', $list[$i], 2);
-            $items[] = $id;
-        }
-        return $items;
-    }
-
-    /**
-     * Get the last modified date from Solr
-     *
-     * @param string $id ID to look up
-     *
-     * @return array
-     * @throws \Exception
-     */
-    public function getModDate($id)
-    {
-        $query = 'select $lastModDate from <#ri> '
-            . 'where $member '
-            . '<info:fedora/fedora-system:def/view#lastModifiedDate> '
-            . '$lastModDate '
-            . 'and $member <dc:identifier> \'' . $id . '\'';
-        $response = $this->query($query);
-        $list = explode("\n", $response->getBody());
-        return $list[1];
-    }
-
-    /**
-     * Returns file contents of the structmap, our most common call
-     *
-     * @param string $root record id
-     *
-     * @return array of ids
-     */
-    public function getOrderedMembers($root)
-    {
-        $query = 'select $memberPID $memberTitle $sequence $member from <#ri> '
-            . 'where $member <fedora-rels-ext:isMemberOf> <info:fedora/'
-            . $root . '> '
-            . 'and $member <http://vudl.org/relationships#sequence> $sequence '
-            . 'and $member <fedora-model:label> $memberTitle '
-            . 'and $member <dc:identifier> $memberPID';
-        $response = $this->query($query);
-        $list = explode("\n", $response->getBody());
-        if (count($list) > 2) {
-            $items = [];
-            $sequenced = true;
-            for ($i = 1;$i < count($list);$i++) {
-                if (empty($list[$i])) {
-                    continue;
-                }
-                list($id, $title, $sequence,) = explode(',', $list[$i], 4);
-                list($seqID, $seq) = explode('#', $sequence);
-                if ($seqID != $root) {
-                    $sequenced = false;
-                    break;
-                }
-                $items[] = [
-                    'seq' => $seq,
-                    'id' => $id
-                ];
-            }
-            if ($sequenced) {
-                usort(
-                    $items,
-                    function ($a, $b) {
-                        return intval($a['seq']) - intval($b['seq']);
-                    }
-                );
-                return array_map(
-                    function ($op) {
-                        return $op['id'];
-                    },
-                    $items
-                );
-            }
-        }
-        // No sequence? Title sort.
-        $query = 'select $memberPID $memberTitle from <#ri> '
-            . 'where $member <fedora-rels-ext:isMemberOf> <info:fedora/'
-            . $root . '> '
-            . 'and $member <fedora-model:label> $memberTitle '
-            . 'and $member <dc:identifier> $memberPID '
-            . 'order by $memberTitle';
-        $response = $this->query($query);
-        $list = explode("\n", $response->getBody());
-        $items = [];
-        for ($i = 1;$i < count($list);$i++) {
-            if (empty($list[$i])) {
-                continue;
-            }
-            list($id, $title, ) = explode(',', $list[$i], 3);
-            $items[] = $id;
-        }
-        return $items;
-    }
-
-    /**
-     * Tuple call to return and parse a list of parents...
-     *
-     * @param string $id ...for this id
-     *
-     * @return array of parents in order from top-down
-     */
-    public function getParentList($id)
-    {
-        if (isset($this->parentLists[$id])) {
-            return $this->parentLists[$id];
-        }
-        // Walk to get all parents to root
-        $query = 'select $child $parent $parentTitle from <#ri> '
-                . 'where walk ('
-                        . '<info:fedora/' . $id . '> '
-                        . '<fedora-rels-ext:isMemberOf> '
-                        . '$parent '
-                    . 'and $child <fedora-rels-ext:isMemberOf> $parent) '
-                . 'and $parent <fedora-model:label> $parentTitle';
-        // Parse out relationships
-        $response = $this->query($query);
-        $list = explode("\n", trim($response->getBody(), "\n"));
-        $tree = [];
-        for ($i = 1;$i < count($list);$i++) {
-            list($child, $parent, $title) = explode(',', substr($list[$i], 12), 3);
-            $parent = substr($parent, 12);
-            if (!isset($tree[$parent])) {
-                $tree[$parent] = [
-                    'children' => [],
-                    'title' => $title
-                ];
-            }
-            $tree[$parent]['children'][] = $child;
-        }
-        $ret = $this->traceParents($tree, $id);
-        // Store in cache
-        $this->parentLists[$id] = $ret;
-        return $ret;
-    }
-
-    /**
-     * Get Fedora Query URL.
-     *
-     * @return string
-     */
-    public function getQueryURL()
-    {
-        return isset($this->config->Fedora->query_url)
-            ? $this->config->Fedora->query_url
-            : null;
-    }
-
-    /**
-     * Get collapsable XML for an id
-     *
-     * @param object        $record   Record data
-     * @param View\Renderer $renderer View renderer to get techinfo template
-     *
-     * @return html string
-     */
-    public function getTechInfo($record = null, $renderer = null)
-    {
-        if ($record == null) {
-            return false;
-        }
-        $ret = [];
-        // OCR
-        if (isset($record['ocr-dirty'])) {
-            $record['ocr-dirty'] = htmlentities(
-                $this->getDatastreamContent(
-                    $record['id'],
-                    'OCR-DIRTY'
-                )
-            );
-        }
-        // Technical Information
-        if (isset($record['master-md'])) {
-            $record['techinfo'] = $this->getDatastreamContent(
-                $record['id'],
-                'MASTER-MD'
-            );
-            $info = $this->getSizeAndTypeInfo($record['techinfo']);
-            $ret['size']     = $info['size'];
-            $ret['type'] = $info['type'];
-        }
-        if ($renderer != null) {
-            $ret['div'] = $renderer
-                ->render('vudl/techinfo.phtml', ['record' => $record]);
-        }
-        return $ret;
-    }
-
-    /**
-     * Get size/type information out of the technical metadata.
-     *
-     * @param string $techInfo Technical metadata
-     *
-     * @return array
-     */
-    protected function getSizeAndTypeInfo($techInfo)
-    {
-        $data = $type = [];
-        preg_match('/<size[^>]*>([^<]*)/', $techInfo, $data);
-        preg_match('/mimetype="([^"]*)/', $techInfo, $type);
-        $size_index = 0;
-        if (count($data) > 1) {
-            $bytes = intval($data[1]);
-            $sizes = ['bytes','KB','MB'];
-            while ($size_index < count($sizes) - 1 && $bytes > 1024) {
-                $bytes /= 1024;
-                $size_index++;
-            }
-            return [
-                'size' => round($bytes, 1) . ' ' . $sizes[$size_index],
-                'type' => $type[1]
-            ];
-        }
-        return [];
-    }
-
-    /**
-     * Get copyright URL and compare it to special cases from VuDL.ini
-     *
-     * @param array $id          record id
-     * @param array $setLicenses ids are strings to match urls to,
-     *  the values are abbreviations. Parsed in details.phtml later.
-     *
-     * @return array
-     */
-    public function getCopyright($id, $setLicenses)
-    {
-        $check = $this->getDatastreamHeaders($id, 'LICENSE');
-        if (!strpos($check[0], '404')) {
-            $xml = $this->getDatastreamContent($id, 'LICENSE');
-            preg_match('/xlink:href="(.*?)"/', $xml, $license);
-            $license = $license[1];
-            foreach ($setLicenses as $tell => $value) {
-                if (strpos($license, $tell)) {
-                    return [$license, $value];
-                }
-            }
-            return [$license, false];
-        }
-        return null;
-    }
-
-    /**
-     * Consolidation of Zend Client calls
-     *
-     * @param string $query   Query for call
-     * @param array  $options Additional options
-     *
-     * @return Response
-     */
-    protected function query($query, $options = [])
-    {
-        $data = [
-            'type'  => 'tuples',
-            'flush' => false,
-            'lang'  => 'itql',
-            'format' => 'CSV',
-            'query' => $query
-        ];
-        foreach ($options as $key => $value) {
-            $data[$key] = $value;
-        }
-        $client = $this->getHttpClient($this->getQueryURL());
-        $client->setMethod('POST');
-        $client->setAuth(
-            $this->config->Fedora->adminUser, $this->config->Fedora->adminPass
-        );
-        $client->setParameterPost($data);
-        return $client->send();
-    }
-}
diff --git a/module/VuDL/src/VuDL/Connection/Manager.php b/module/VuDL/src/VuDL/Connection/Manager.php
deleted file mode 100644
index 9a9e6d54cbe67617fcfa6936d2eeaf2761cd2567..0000000000000000000000000000000000000000
--- a/module/VuDL/src/VuDL/Connection/Manager.php
+++ /dev/null
@@ -1,112 +0,0 @@
-<?php
-/**
- * VuDL connection management class (goes through connections in priority order)
- *
- * PHP version 5
- *
- * Copyright (C) Villanova University 2010.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License version 2,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
- *
- * @category VuFind
- * @package  Controller
- * @author   Chris Hallberg <challber@villanova.edu>
- * @license  http://opensource.org/licenses/gpl-3.0.php GNU General Public License
- * @link     https://vufind.org/wiki/
- */
-namespace VuDL\Connection;
-
-/**
- * VuDL connection manager
- *
- * @category VuFind
- * @package  Controller
- * @author   Chris Hallberg <challber@villanova.edu>
- * @license  http://opensource.org/licenses/gpl-3.0.php GNU General Public License
- * @link     https://vufind.org/wiki/
- */
-class Manager
-{
-    /**
-     * Array of classes to try in order
-     */
-    protected $priority;
-
-    /**
-     * Cache of class objects
-     */
-    protected $connections;
-
-    /**
-     * Used to load class objects
-     */
-    protected $serviceLocator;
-
-    /**
-     * Constructor
-     *
-     * @param array          $priority Order we want to try connectors in
-     * @param ServiceLocator $sm       Object to load everything from
-     */
-    public function __construct($priority, $sm)
-    {
-        $this->priority = $priority;
-        $this->connections = [];
-        $this->serviceLocator = $sm;
-    }
-
-    /**
-     * Get a class object from a classname, save in cache
-     *
-     * @param string $className Class we want to load
-     *
-     * @return object
-     */
-    protected function get($className)
-    {
-        if (!isset($this->connections[$className])) {
-            $this->connections[$className] = $this->serviceLocator
-                ->get("VuDL\\Connection\\$className");
-        }
-        return $this->connections[$className];
-    }
-
-    /**
-     * Try to call a function in each successive class
-     * according to priority
-     *
-     * @param string $methodName The function we want to call
-     * @param array  $params     The params to pass to the func
-     *
-     * @return mixed
-     */
-    public function __call($methodName, $params)
-    {
-        $index = 0;
-        while ($index < count($this->priority)) {
-            $object = $this->get($this->priority[$index]);
-            if (method_exists($object, $methodName)) {
-                $ret = call_user_func_array([$object, $methodName], $params);
-                if (!is_null($ret)) {
-                    //var_dump($methodName.' - '.$this->priority[$index]);
-                    return $ret;
-                }
-            }
-            $index ++;
-        }
-        throw new \Exception(
-            'VuDL Connection Failed to resolved method "' . $methodName . '"'
-        );
-    }
-}
diff --git a/module/VuDL/src/VuDL/Connection/Solr.php b/module/VuDL/src/VuDL/Connection/Solr.php
deleted file mode 100644
index e221fda9b7f2f7f6f97aab7a87ca93fd7c343692..0000000000000000000000000000000000000000
--- a/module/VuDL/src/VuDL/Connection/Solr.php
+++ /dev/null
@@ -1,356 +0,0 @@
-<?php
-/**
- * VuDL to Solr connection class (defines some methods to talk to Solr)
- *
- * PHP version 5
- *
- * Copyright (C) Villanova University 2010.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License version 2,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
- *
- * @category VuFind
- * @package  Controller
- * @author   Chris Hallberg <challber@villanova.edu>
- * @license  http://opensource.org/licenses/gpl-3.0.php GNU General Public License
- * @link     https://vufind.org/wiki/
- */
-namespace VuDL\Connection;
-use VuFindSearch\ParamBag;
-
-/**
- * VuDL-Fedora connection class
- *
- * @category VuFind
- * @package  Controller
- * @author   Chris Hallberg <challber@villanova.edu>
- * @license  http://opensource.org/licenses/gpl-3.0.php GNU General Public License
- * @link     https://vufind.org/wiki/
- */
-class Solr extends AbstractBase
-{
-    /**
-     * Connector class to Solr
-     *
-     * @var \VuFindSearch\Backend\Solr\Connector
-     */
-    protected $solr;
-
-    /**
-     * Constructor
-     *
-     * @param \Zend\Config\Config           $config  config
-     * @param \VuFind\Search\BackendManager $backend backend manager
-     */
-    public function __construct($config, $backend)
-    {
-        $this->config = $config;
-        $this->solr = $backend->getConnector();
-    }
-
-    /**
-     * Returns an array of classes for this object
-     *
-     * @param string $id record id
-     *
-     * @return array
-     */
-    public function getClasses($id)
-    {
-        // Get record
-        $response = $this->search(
-            new ParamBag(
-                [
-                    'q'  => 'id:"' . $id . '"',
-                    'fl' => 'modeltype_str_mv',
-                ]
-            )
-        );
-        $record = json_decode($response);
-        if ($record->response->numFound > 0) {
-            return array_map(
-                function ($op) {
-                    return substr($op, 12);
-                },
-                $record->response->docs[0]->modeltype_str_mv
-            );
-        }
-        return null;
-    }
-
-    /**
-     * Get details from Solr
-     *
-     * @param string  $id     ID to look up
-     * @param boolean $format Run result through formatDetails?
-     *
-     * @return array
-     * @throws \Exception
-     */
-    public function getDetails($id, $format)
-    {
-        if ($response = $this->search(
-            new ParamBag(
-                [
-                    'q' => 'id:"' . $id . '"'
-                ]
-            )
-        )) {
-            $record = json_decode($response);
-            if ($format) {
-                return $this->formatDetails((Array) $record->response->docs[0]);
-            }
-            return (Array) $record->response->docs[0];
-        }
-        return null;
-    }
-
-    /**
-     * Get an item's label
-     *
-     * @param string $id Record's id
-     *
-     * @return string
-     */
-    public function getLabel($id)
-    {
-        $labelField = 'dc_title_str';
-        $response = $this->search(
-            new ParamBag(
-                [
-                    'q'     => 'id:"' . $id . '"',
-                    'fl'    => $labelField,
-                ]
-            )
-        );
-        $details = json_decode($response);
-        // If we have results
-        if ($details->response->numFound > 0) {
-            return $details->response->docs[0]->$labelField;
-        }
-        return null;
-    }
-
-    /**
-     * Tuple call to return and parse a list of members...
-     *
-     * @param string $root ...for this id
-     *
-     * @return array of members in order
-     */
-    public function getMemberList($root)
-    {
-        // Get members
-        $response = $this->search(
-            new ParamBag(
-                [
-                    'q'  => 'relsext.isMemberOf:"' . $root . '"',
-                    'fl' => 'id,hierarchy_top_title',
-                    'rows' => 100,
-                ]
-            )
-        );
-        $children = json_decode($response);
-        // If we have results
-        if ($children->response->numFound > 0) {
-            return array_map(
-                function ($n) {
-                    return [
-                        'id' => $n->id,
-                        'title' => $n->hierarchy_top_title,
-                    ];
-                },
-                $children->response->docs
-            );
-        }
-        return [];
-    }
-
-    /**
-     * Get the last modified date from Solr
-     *
-     * @param string $id ID to look up
-     *
-     * @return array
-     * @throws \Exception
-     */
-    public function getModDate($id)
-    {
-        $modfield = 'fgs.lastModifiedDate';
-        if ($response = $this->search(
-            new ParamBag(
-                [
-                    'q'     => 'id:"' . $id . '"',
-                    'fl'    => $modfield,
-                ]
-            )
-        )) {
-            $record = json_decode($response);
-            $date = $record->response->docs[0]->$modfield;
-            return $date[0];
-        }
-        return null;
-    }
-
-    /**
-     * Returns file contents of the structmap, our most common call
-     *
-     * @param string $id         record id
-     * @param array  $extra_sort extra fields to sort on
-     *
-     * @return string $id
-     */
-    public function getOrderedMembers($id, $extra_sort = [])
-    {
-        // Try to find members in order
-        $seqField = 'sequence_' . str_replace(':', '_', $id) . '_str';
-        $sort = [$seqField . ' asc'];
-        foreach ($extra_sort as $sf) {
-            $sort[] = $sf;
-        }
-        $response = $this->search(
-            new ParamBag(
-                [
-                    'q'  => 'relsext.isMemberOf:"' . $id . '"',
-                    'sort'  => implode(',', $sort),
-                    'fl' => 'id',
-                    'rows' => 99999,
-                ]
-            )
-        );
-        $data = json_decode($response);
-        // If we didn't find anything, do a standard members search
-        if ($data->response->numFound == 0) {
-            return null;
-        } else {
-            return array_map(
-                function ($n) {
-                    return $n->id;
-                },
-                $data->response->docs
-            );
-        }
-    }
-
-    /**
-     * Tuple call to return and parse a list of parents...
-     *
-     * @param string $id ...for this id
-     *
-     * @return array of parents in order from top-down
-     */
-    public function getParentList($id)
-    {
-        // Cache
-        if (isset($this->parentLists[$id])) {
-            return $this->parentLists[$id];
-        }
-        $queue = [$id];
-        $tree = [];
-        while (!empty($queue)) {
-            $current = array_shift($queue);
-            if ($current == $this->getRootId()) {
-                continue;
-            }
-            $response = $this->search(
-                new ParamBag(
-                    [
-                        'q'     => 'id:"' . $current . '"',
-                        'fl'    => 'hierarchy_parent_id,hierarchy_parent_title',
-                    ]
-                )
-            );
-            $data = json_decode($response);
-            if ($current == $id && $data->response->numFound == 0) {
-                return null;
-            }
-            // Get info on our record
-            $parents = $data->response->docs[0];
-            foreach ($parents->hierarchy_parent_id as $i => $cid) {
-                array_push($queue, $cid);
-                if (!isset($tree[$cid])) {
-                    $tree[$cid] = [
-                        'children' => [],
-                        'title' => $parents->hierarchy_parent_title[$i]
-                    ];
-                }
-                if (!in_array($current, $tree[$cid]['children'])) {
-                    $tree[$cid]['children'][] = $current;
-                }
-            }
-        }
-        $ret = $this->traceParents($tree, $id);
-        // Store in cache
-        $this->parentLists[$id] = $ret;
-        return $ret;
-    }
-
-    /**
-     * Get copyright URL and compare it to special cases from VuDL.ini
-     *
-     * @param array $id          record id
-     * @param array $setLicenses ids are strings to match urls to,
-     *  the values are abbreviations. Parsed in details.phtml later.
-     *
-     * @return array
-     */
-    public function getCopyright($id, $setLicenses)
-    {
-        $licenseField = 'license.mdRef';
-        $response = $this->search(
-            new ParamBag(
-                [
-                    'q'     => 'id:"' . $id . '"',
-                    'fl'    => $licenseField
-                ]
-            )
-        );
-        $data = json_decode($response);
-        $docs = $data->response->docs[0];
-        if ($data->response->numFound == 0 || !isset($docs->$licenseField)) {
-            return null;
-        }
-        $license = $docs->$licenseField;
-        $license = $license[0];
-        foreach ($setLicenses as $tell => $value) {
-            if (strpos($license, $tell)) {
-                return [$license, $value];
-            }
-        }
-        return [$license, false];
-    }
-
-    /**
-     * Perform a search with clean params
-     *
-     * @param ParamBag $paramBag The params you'd normally send to solr
-     *
-     * @return json
-     */
-    protected function search($paramBag)
-    {
-        // Remove global filters from the Solr connector
-        $map = $this->solr->getMap();
-        $params = $map->getParameters('select', 'appends');
-        $map->setParameters('select', 'appends', []);
-        // Turn off grouping
-        $paramBag->set('group', 'false');
-        $paramBag->add('wt', 'json');
-        // Search
-        $response = $this->solr->search($paramBag);
-        // Reapply the global filters
-        $map->setParameters('select', 'appends', $params->getArrayCopy());
-
-        return $response;
-    }
-}
diff --git a/module/VuDL/src/VuDL/Controller/AbstractVuDL.php b/module/VuDL/src/VuDL/Controller/AbstractVuDL.php
deleted file mode 100644
index 01f1cd1f83fc814c4b08b9b13d7ad84734af1150..0000000000000000000000000000000000000000
--- a/module/VuDL/src/VuDL/Controller/AbstractVuDL.php
+++ /dev/null
@@ -1,109 +0,0 @@
-<?php
-/**
- * VuDL controller base class (defines some methods that can be shared by other
- * controllers).
- *
- * PHP version 5
- *
- * Copyright (C) Villanova University 2010.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License version 2,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
- *
- * @category VuFind
- * @package  Controller
- * @author   Demian Katz <demian.katz@villanova.edu>
- * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
- * @link     https://vufind.org/wiki/
- */
-namespace VuDL\Controller;
-
-/**
- * VuDL controller base class (defines some methods that can be shared by other
- * controllers).
- *
- * @category VuFind
- * @package  Controller
- * @author   Chris Hallberg <challber@villanova.edu>
- * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
- * @link     https://vufind.org/wiki/
- */
-class AbstractVuDL extends \VuFind\Controller\AbstractBase
-{
-    /**
-     * VuDL config
-     *
-     * @var \Zend\Config\Config
-     */
-    protected $vuDLConfig = null;
-
-    /**
-     * Create a new ViewModel with title.
-     *
-     * @param array $params Parameters to pass to ViewModel constructor.
-     *
-     * @return ViewModel
-     */
-    protected function createViewModel($params = null)
-    {
-        $view = parent::createViewModel($params);
-        $view->title = $this->getVuDLConfig()->General->title;
-        return $view;
-    }
-
-    /**
-     * Get the VuDL configuration object.
-     *
-     * @return \Zend\Config\Config
-     */
-    protected function getVuDLConfig()
-    {
-        if (null === $this->vuDLConfig) {
-            $this->vuDLConfig = $this->getServiceLocator()
-                ->get('VuFind\Config')->get('VuDL');
-        }
-        return $this->vuDLConfig;
-    }
-    
-    /**
-     * Get VuDL Licenses.
-     *
-     * @return array
-     */
-    protected function getConnector()
-    {
-        return $this->getServiceLocator()->get('VuDL\Connection\Manager');
-    }
-    
-    /**
-     * Get VuDL Licenses.
-     *
-     * @return array
-     */
-    protected function getLicenses()
-    {
-        $cfg = $this->getVuDLConfig();
-        return isset($cfg->Licenses) ? $cfg->Licenses->toArray() : [];
-    }
-
-    /**
-     * Get VuDL Routes.
-     *
-     * @return array
-     */
-    protected function getVuDLRoutes()
-    {
-        $cfg = $this->getVuDLConfig();
-        return isset($cfg->Routes) ? $cfg->Routes->toArray() : [];
-    }
-}
diff --git a/module/VuDL/src/VuDL/Controller/VudlController.php b/module/VuDL/src/VuDL/Controller/VudlController.php
deleted file mode 100644
index 50a05e492e8d9c94817bf30a4b954658b9d1c5e2..0000000000000000000000000000000000000000
--- a/module/VuDL/src/VuDL/Controller/VudlController.php
+++ /dev/null
@@ -1,452 +0,0 @@
-<?php
-/**
- * VuDLController Module Controller
- *
- * PHP Version 5
- *
- * Copyright (C) Villanova University 2011.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License version 2,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
- *
- * @category VuFind
- * @package  Controller
- * @author   Chris Hallberg <challber@villanova.edu>
- * @author   David Lacy <david.lacy@villanova.edu>
- * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
- * @link     https://vufind.org Main Site
- */
-namespace VuDL\Controller;
-use VuFind\Exception\RecordMissing as RecordMissingException,
-    VuFindSearch\ParamBag;
-
-/**
- * This controller is for the viewing of the digital library files.
- *
- * @category VuFind
- * @package  Controller
- * @author   Chris Hallberg <challber@villanova.edu>
- * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
- * @link     https://vufind.org Main Site
- */
-class VudlController extends AbstractVuDL
-{
-    /**
-     * Retrieve the object cache.
-     *
-     * @return object
-     */
-    protected function getCache()
-    {
-        return $this->getServiceLocator()->get('VuFind\CacheManager')
-            ->getCache('object');
-    }
-
-    /**
-     * Returns the root id for any parent this item may have
-     * ie. If we're requesting a specific page, return the book
-     *
-     * @param string $id record id
-     *
-     * @return string $id
-     */
-    protected function getRoot($id)
-    {
-        $parents = array_reverse($this->getConnector()->getParentList($id));
-        foreach (array_keys($parents[0]) as $i) {
-            if (in_array(
-                'ResourceCollection',
-                $this->getConnector()->getClasses($i)
-            )) {
-                return $i;
-            }
-        }
-        return $id;
-    }
-
-    /**
-     * Returns the page number of the child in a parent
-     *
-     * @param string $parent parent record id
-     * @param string $child  child record id
-     *
-     * @return string $id
-     */
-    protected function getPage($parent, $child)
-    {
-        // GET LISTS
-        $lists = $this->getConnector()->getOrderedMembers($parent);
-        // GET LIST ITEMS
-        foreach ($lists as $list => $list_data) {
-            $items = $this->getConnector()->getOrderedMembers($list_data);
-            foreach ($items as $i => $id) {
-                if ($id == $child) {
-                    return [$list, $i];
-                }
-            }
-        }
-    }
-
-    /**
-     * Generate an array of all child pages and their information/images
-     *
-     * @param string $root       record id to search under
-     * @param string $start      page/doc to start with for the return
-     * @param int    $pageLength page length (leave null to use default)
-     *
-     * @return associative array of the lists with their members
-     */
-    protected function getOutline($root, $start = 0, $pageLength = null)
-    {
-        $cache = (strtolower($this->params()->fromQuery('cache')) == 'no');
-
-        $generator = new \VuDL\OutlineGenerator(
-            $this->getConnector(), $this->url(), $this->getVuDLRoutes(),
-            $cache ? $this->getCache() : false
-        );
-        return $generator->getOutline($root, $start, $pageLength);
-    }
-
-    /**
-     * Get the technical metadata for a record from POST
-     *
-     * @return array
-     */
-    protected function getTechInfo()
-    {
-        return $this->getConnector()->getTechInfo(
-            $this->params()->fromPost(),
-            $this->getViewRenderer()
-        );
-    }
-
-    /**
-     * Ajax function for the VuDL view
-     * Return JSON encoding of pages
-     *
-     * @return json string of pages
-     */
-    public function ajaxAction()
-    {
-        $method = (String) $this->params()->fromQuery('method');
-        return $this->jsonReturn($this->$method());
-    }
-
-    /**
-     * Format data for return as JSON
-     *
-     * @param string $data Data to be encoded
-     *
-     * @return json string
-     */
-    protected function jsonReturn($data)
-    {
-        $output = ['data' => $data, 'status' => 'OK'];
-        $response = $this->getResponse();
-        $headers = $response->getHeaders();
-        $headers->addHeaderLine(
-            'Content-type', 'application/javascript'
-        );
-        $headers->addHeaderLine(
-            'Cache-Control', 'no-cache, must-revalidate'
-        );
-        $headers->addHeaderLine(
-            'Expires', 'Mon, 26 Jul 1997 05:00:00 GMT'
-        );
-        $response->setContent(json_encode($output));
-        return $response;
-    }
-
-    /**
-     * Returns the outline for the next offset block of records
-     *
-     * @return associative array
-     */
-    public function pageAjax()
-    {
-        $id = $this->params()->fromQuery('record');
-        $start = $this->params()->fromQuery('start');
-        $end = $this->params()->fromQuery('end');
-        $data = [
-            'outline' => $this->getOutline($id, $start, $end - $start),
-            'start'  => (int)$start
-        ];
-        $data['outline'] = current($data['outline']['lists']);
-        if (isset($data['outline'])) {
-            $data['length'] = count($data['outline']);
-        } else {
-            $data['length'] = 0;
-        }
-        return $data;
-    }
-
-    /**
-     * Template view swapping, this function returns the html from the template
-     *
-     * @return json string of page
-     */
-    public function viewLoad()
-    {
-        $renderer = $this->getViewRenderer();
-        $data = $this->params()->fromPost();
-        if ($data == null) {
-            $id = $this->params()->fromQuery('id');
-            $list = [];
-            preg_match_all(
-                '/dsid="([^"]+)"/',
-                strtolower($this->getConnector()->getDatastreams($id)),
-                $list
-            );
-            $data = array_flip($list[1]);
-            $data['id'] = $id;
-        }
-        $data['keys'] = array_keys($data);
-        try {
-            $view = $renderer->render(
-                'vudl/views/' . $data['filetype'] . '.phtml',
-                $data
-            );
-        } catch(\Exception $e) {
-            $view = $renderer->render(
-                'vudl/views/download.phtml',
-                $data
-            );
-        }
-        return $view;
-    }
-
-    /**
-     * Display record in VuDL from Fedora
-     *
-     * @return View Object
-     */
-    public function recordAction()
-    {
-        // Target id
-        $id = $this->params()->fromRoute('id');
-        if ($id == null) {
-            return $this->forwardTo('VuDL', 'Home');
-        }
-
-        $classes = $this->getConnector()->getClasses($id);
-        if (in_array('FolderCollection', $classes)) {
-            return $this->forwardTo('Collection', 'Home', ['id' => $id]);
-        }
-        $view = $this->createViewModel();
-
-        // Check if we're a ResourceObject || find parent
-        $root = $this->getRoot($id);
-        list($currList, $currPage) = $this->getPage($root, $id);
-        $view->initPage = $root == $id ? 0 : $currPage;
-        $view->initList = $currList ?: 0;
-        $view->id = $root;
-
-        try {
-            $driver = $this->getRecordLoader()->load($root);
-        } catch(\Exception $e) {
-        }
-        if (isset($driver) && $driver->isProtected()) {
-            return $this->forwardTo('VuDL', 'Denied', ['id' => $id]);
-        }
-
-        // File information / description
-        $fileDetails = $this->getConnector()->getDetails($root, true);
-
-        // Copyright information
-        list($fileDetails['license'], $fileDetails['special_license'])
-            = $this->getConnector()->getCopyright($root, $this->getLicenses());
-
-        $view->details = $fileDetails;
-
-        // Get ids for all files
-        $outline = $this->getOutline(
-            $root,
-            max(0, $view->initPage - ($this->getConnector()->getPageLength() / 2))
-        );
-
-        // Send the data for the first pages
-        // (Original, Large, Medium, Thumbnail srcs) and THE DOCUMENTS
-        $view->outline = $outline;
-        $parents = $this->getConnector()->getParentList($root);
-        //$keys = array_keys($parents);
-        //$view->hierarchyID = end($keys);
-        $view->parents = $parents;
-        if ($id != $root) {
-            $view->parentID = $root;
-        }
-        $view->pagelength = $this->getConnector()->getPageLength();
-        return $view;
-    }
-
-    /**
-     * Forward home action to browse
-     *
-     * @return forward
-     */
-    public function homeAction()
-    {
-        $view = $this->createViewModel();
-        $config = $this->getConfig('vudl');
-        $children = $this->getConnector()->getMemberList($config->General->root_id);
-        foreach ($children as $item) {
-            $outline[] = [
-                'id' => $item['id'],
-                'img' => $this->url()->fromRoute(
-                    'files',
-                    [
-                        'id'   => $item['id'],
-                        'type' => 'THUMBNAIL'
-                    ]
-                ),
-                'label' => $item['title'][0]
-            ];
-        }
-        $view->thumbnails = $outline;
-        return $view;
-    }
-
-    /**
-     * Display record in VuDL from Fedora as a grid
-     *
-     * @return View Object
-     */
-    public function gridAction()
-    {
-        $view = $this->createViewModel();
-        // Target id
-        $id = $this->params()->fromRoute('id');
-
-        // Check if we're a ResourceObject || find parent
-        $root = $this->getRoot($id);
-        $view->page = $root == $id ? 0 : $this->getPage($root, $id);
-        $view->id = $root;
-
-        // File information / description
-        $fileDetails = $this->getConnector()->getDetails($root, true);
-        $view->details = $fileDetails;
-
-        // Get ids for all files
-        $outline = $this->getOutline($root, 0);
-
-        // Send the data for the first pages
-        // (Original, Large, Medium, Thumbnail srcs) and THE DOCUMENTS
-        $view->outline = $outline;
-        $parents = $this->getConnector()->getParentList($root);
-        //$keys = array_keys($parents);
-        //$view->hierarchyID = end($keys);
-        $view->parents = $parents;
-        return $view;
-    }
-
-    /**
-     * About page
-     *
-     * @return View Object
-     */
-    public function aboutAction()
-    {
-        $view = $this->createViewModel();
-        $connector = $this->getServiceLocator()->get('VuFind\Search\BackendManager')
-            ->get('Solr')->getConnector();
-        $queries = [
-            'modeltype_str_mv:"vudl-system:FolderCollection"',
-            'modeltype_str_mv:"vudl-system:ResourceCollection"',
-            // TODO: make these work:
-            //'modeltype_str_mv:"vudl-system:ImageData"',
-            //'modeltype_str_mv:"vudl-system:PDFData"',
-        ];
-        $response = '';
-        foreach ($queries as $q) {
-            $params = new \VuFindSearch\ParamBag(['q' => $q]);
-            $response .= $connector->search($params);
-        }
-        $result = [];
-        preg_match_all('/"ngroups">([^<]*)/', $response, $result);
-        $view->totals = [
-            'folders' => intval($result[1][0]),
-            'resources' => intval($result[1][1]),
-            // TODO: make these work:
-            //'images'=>intval($result[1][2]),
-            //'pdfs'=>intval($result[1][3])
-        ];
-        return $view;
-    }
-
-    /**
-     * Copyright page
-     *
-     * @return View Object
-     */
-    public function copyrightAction()
-    {
-        $view = $this->createViewModel();
-        return $view;
-    }
-
-    /**
-     * Access denied screen.
-     *
-     * @return mixed
-     */
-    protected function deniedAction()
-    {
-        $view = $this->createViewModel();
-        $view->id = $this->params()->fromRoute('id');
-        return $view;
-    }
-
-    /**
-     * Redirect to the appropriate sibling.
-     *
-     * @return mixed
-     */
-    protected function siblingAction()
-    {
-        $params = $this->params()->fromQuery();
-        $members = $this->getConnector()->getOrderedMembers($params['trail']);
-        if (count($members) < 2) {
-            //return $this->redirect()
-                //->toRoute('Collection', 'Home', array('id'=>$params['trail']));
-        }
-        $index = -1;
-        foreach ($members as $i => $member) {
-            if ($member == $params['id']) {
-                $index = $i + count($members);
-                break;
-            }
-        }
-        if ($index == -1) {
-            return $this->redirect()
-                ->toRoute('collection', ['id' => $params['trail']]);
-        } elseif (isset($params['prev'])) {
-            return $this->redirect()->toRoute(
-                'vudl-record', ['id' => $members[($index - 1) % count($members)]]
-            );
-        } else {
-            return $this->redirect()->toRoute(
-                'vudl-record', ['id' => $members[($index + 1) % count($members)]]
-            );
-        }
-    }
-
-    /**
-     * Redirect to the appropriate sibling.
-     *
-     * @return View Object
-     */
-    protected function collectionsAction()
-    {
-        return $this
-            ->forwardTo('Collection', 'Home', ['id' => $this->getRootId()]);
-    }
-}
diff --git a/module/VuDL/src/VuDL/Factory.php b/module/VuDL/src/VuDL/Factory.php
deleted file mode 100644
index a2b4ab351889b5930b68c716eb83a698fdcccb0c..0000000000000000000000000000000000000000
--- a/module/VuDL/src/VuDL/Factory.php
+++ /dev/null
@@ -1,106 +0,0 @@
-<?php
-/**
- * Factory for VuDL resources.
- *
- * PHP version 5
- *
- * Copyright (C) Villanova University 2014.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License version 2,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
- *
- * @category VuFind
- * @package  VuDL
- * @author   Demian Katz <demian.katz@villanova.edu>
- * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
- * @link     https://vufind.org/wiki/
- */
-namespace VuDL;
-use Zend\ServiceManager\ServiceManager;
-
-/**
- * Factory for VuDL resources.
- *
- * @category VuFind
- * @package  VuDL
- * @author   Demian Katz <demian.katz@villanova.edu>
- * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
- * @link     https://vufind.org/wiki/
- *
- * @codeCoverageIgnore
- */
-class Factory
-{
-    /**
-     * Construct the Connection Manager service.
-     *
-     * @param ServiceManager $sm Service manager.
-     *
-     * @return Fedora
-     */
-    public static function getConnectionManager(ServiceManager $sm)
-    {
-        return new \VuDL\Connection\Manager(
-            ['Solr', 'Fedora'], $sm
-        );
-    }
-        
-    /**
-     * Construct the Connection Fedora service.
-     *
-     * @param ServiceManager $sm Service manager.
-     *
-     * @return Fedora
-     */
-    public static function getConnectionFedora(ServiceManager $sm)
-    {
-        return new \VuDL\Connection\Fedora(
-            $sm->get('VuFind\Config')->get('VuDL')
-        );
-    }
-        
-    /**
-     * Construct the Connection Solr service.
-     *
-     * @param ServiceManager $sm Service manager.
-     *
-     * @return Fedora
-     */
-    public static function getConnectionSolr(ServiceManager $sm)
-    {
-        return new \VuDL\Connection\Solr(
-            $sm->get('VuFind\Config')->get('VuDL'),
-            $sm->get('VuFind\Search\BackendManager')->get('Solr')
-        );
-    }
-
-    /**
-     * Construct the VuDL record driver.
-     *
-     * @param ServiceManager $sm Service manager.
-     *
-     * @return RecordDriver\SolrVudl
-     */
-    public static function getRecordDriver(ServiceManager $sm)
-    {
-        $driver = new RecordDriver\SolrVudl(
-            $sm->getServiceLocator()->get('VuFind\Config')->get('config'),
-            null,
-            $sm->getServiceLocator()->get('VuFind\Config')->get('searches')
-        );
-        $driver->setVuDLConfig(
-            $sm->getServiceLocator()->get('VuFind\Config')->get('VuDL')
-        );
-        return $driver;
-    }
-}
diff --git a/module/VuDL/src/VuDL/OutlineGenerator.php b/module/VuDL/src/VuDL/OutlineGenerator.php
deleted file mode 100644
index 00b03095b949d11dd6c7c4824ae27311eb9c5209..0000000000000000000000000000000000000000
--- a/module/VuDL/src/VuDL/OutlineGenerator.php
+++ /dev/null
@@ -1,296 +0,0 @@
-<?php
-/**
- * VuDL outline generator
- *
- * PHP version 5
- *
- * Copyright (C) Villanova University 2010.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License version 2,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
- *
- * @category VuFind
- * @package  VuDL
- * @author   Chris Hallberg <challber@villanova.edu>
- * @license  http://opensource.org/licenses/gpl-3.0.php GNU General Public License
- * @link     https://vufind.org/wiki/
- */
-namespace VuDL;
-use Zend\Mvc\Controller\Plugin\Url as UrlHelper;
-
-/**
- * VuDL outline generator
- *
- * @category VuFind
- * @package  VuDL
- * @author   Chris Hallberg <challber@villanova.edu>
- * @license  http://opensource.org/licenses/gpl-3.0.php GNU General Public License
- * @link     https://vufind.org/wiki/
- */
-class OutlineGenerator
-{
-    /**
-     * VuDL connection manager
-     *
-     * @var connector
-     */
-    protected $connector;
-
-    /**
-     * URL helper
-     *
-     * @var UrlHelper
-     */
-    protected $url;
-
-    /**
-     * VuDL route configuration
-     *
-     * @var array
-     */
-    protected $routes;
-
-    /**
-     * Cache object
-     *
-     * @var object
-     */
-    protected $cache;
-
-    /**
-     * Queues
-     *
-     * @var array
-     */
-    protected $queue;
-
-    /**
-     * Modification date information
-     *
-     * @var array
-     */
-    protected $moddate;
-
-    /**
-     * Outline currently under construction
-     *
-     * @var array
-     */
-    protected $outline;
-
-    /**
-     * Constructor
-     *
-     * @param Fedora      $connector VuDL connection manager
-     * @param UrlHelper   $url       URL helper
-     * @param array       $routes    VuDL route configuration
-     * @param object|bool $cache     Cache object (or false to disable caching)
-     */
-    public function __construct(Connection\Manager $connector, UrlHelper $url,
-        $routes = [], $cache = false
-    ) {
-        $this->connector = $connector;
-        $this->url = $url;
-        $this->routes = $routes;
-        $this->cache = $cache;
-    }
-
-    /**
-     * Compares the cache date against a given date. If given date is newer,
-     * return false in order to refresh cache. Else return cache!
-     *
-     * @param string      $key     Unique key of cache object
-     * @param string|Date $moddate Date to test cache time freshness
-     *
-     * @return cache object or false
-     */
-    protected function getCache($key, $moddate = null)
-    {
-        if ($this->cache && $cache_item = $this->cache->getItem($key)) {
-            if ($moddate == null || (isset($cache_item['moddate'])
-                && date_create($cache_item['moddate']) >= date_create($moddate))
-            ) {
-                return $cache_item['outline'];
-            }
-        }
-        return false;
-    }
-
-    /**
-     * Save cache object with date to test for freshness
-     *
-     * @param string $key  Unique key of cache object
-     * @param object $data Object to save
-     *
-     * @return cache object or false
-     */
-    protected function setCache($key, $data)
-    {
-        if ($this->cache) {
-            $this->cache->setItem(
-                $key,
-                [
-                    'moddate' => date(DATE_ATOM),
-                    'outline' => $data
-                ]
-            );
-            return $data;
-        }
-        return false;
-    }
-
-    /**
-     * Load information on lists within the specified record.
-     *
-     * @param string $root record ID
-     *
-     * @return void
-     */
-    protected function loadLists($root)
-    {
-        // Reset the state of the class:
-        $this->queue = $this->moddate = [];
-        $this->outline = [
-            'counts' => [],
-            'names' => [],
-            'lists' => []
-        ];
-        // Get lists
-        $lists = $this->connector->getOrderedMembers($root);
-        // Get list items
-        foreach ($lists as $i => $list_id) {
-            // Get list name
-            $this->outline['names'][] = $this->connector->getLabel($list_id);
-            $this->moddate[$i] = $this->connector->getModDate($list_id);
-            $items = $this->connector->getOrderedMembers($list_id);
-            $this->queue[$i] = $items;
-        }
-    }
-
-    /**
-     * Build a single item within the outline.
-     *
-     * @param string $id ID of record to retrieve
-     *
-     * @return array
-     */
-    protected function buildItem($id)
-    {
-        // Else, get all the data and save it to the cache
-        $list = [];
-        // Get the file type
-        $file = $this->connector->getDatastreams($id);
-        preg_match_all(
-            '/dsid="([^"]+)"[^>]*mimeType="([^"]+)/',
-            $file,
-            $list
-        );
-        $masterIndex = array_search('MASTER', $list[1]);
-        $mimetype = $masterIndex ? $list[2][$masterIndex] : 'N/A';
-        if (!$masterIndex) {
-            $type = 'page';
-        } else {
-            $type = substr(
-                $list[2][$masterIndex],
-                strpos($list[2][$masterIndex], '/') + 1
-            );
-        }
-        $details = $this->connector->getDetails($id, false);
-        return [
-            'id' => $id,
-            'fulltype' => $type,
-            'mimetype' => $mimetype,
-            'filetype' => isset($this->routes[$type])
-                ? $this->routes[$type]
-                : $type,
-            'label' => isset($details['title'])
-                ? $details['title']
-                : $id,
-            'datastreams' => $list[1],
-            'mimetypes' => $list[2]
-        ];
-    }
-
-    /**
-     * Load all pages and docs.
-     *
-     * @param string $start      page/doc to start with for the return
-     * @param int    $pageLength page length (leave null to use default)
-     *
-     * @return void
-     */
-    protected function loadPagesAndDocs($start, $pageLength)
-    {
-        // Set default page length if necessary
-        if ($pageLength == null) {
-            $pageLength = $this->connector->getPageLength();
-        }
-
-        // Get data on all pages and docs
-        foreach ($this->queue as $parent => $items) {
-            $this->outline['counts'][$parent] = count($items);
-            if (count($items) < $start) {
-                continue;
-            }
-            $this->outline['lists'][$parent] = [];
-            for ($i = $start;$i < $start + $pageLength;$i++) {
-                if ($i >= count($items)) {
-                    break;
-                }
-                $id = $items[$i];
-                // If there's a cache of this page...
-                $item = $this->getCache(md5($id), $this->moddate[$parent]);
-                if (!$item) {
-                    $item = $this->buildItem($id);
-                    $this->setCache(md5($id), $item);
-                }
-                $this->outline['lists'][$parent][$i] = $item;
-            }
-        }
-    }
-
-    /**
-     * Add URLs to the outline.
-     *
-     * @return void
-     */
-    protected function injectUrls()
-    {
-        foreach ($this->outline['lists'] as $key => $list) {
-            foreach ($list as $id => $item) {
-                foreach ($item['datastreams'] as $ds) {
-                    $routeParams = ['id' => $item['id'], 'type' => $ds];
-                    $this->outline['lists'][$key][$id][strtolower($ds)]
-                        = $this->url->fromRoute('files', $routeParams);
-                }
-            }
-        }
-    }
-
-    /**
-     * Generate an array of all child pages and their information/images
-     *
-     * @param string $root       record id to search under
-     * @param string $start      page/doc to start with for the return
-     * @param int    $pageLength page length (leave null to use default)
-     *
-     * @return associative array of the lists with their members
-     */
-    public function getOutline($root, $start = 0, $pageLength = null)
-    {
-        $this->loadLists($root);
-        $this->loadPagesAndDocs($start, $pageLength);
-        $this->injectUrls();
-        return $this->outline;
-    }
-}
diff --git a/module/VuDL/src/VuDL/RecordDriver/SolrVudl.php b/module/VuDL/src/VuDL/RecordDriver/SolrVudl.php
deleted file mode 100644
index c62b32059d9ab71e01d86a06e422ff57f7fdcf00..0000000000000000000000000000000000000000
--- a/module/VuDL/src/VuDL/RecordDriver/SolrVudl.php
+++ /dev/null
@@ -1,197 +0,0 @@
-<?php
-/**
- * Model for VuDL records in Solr.
- *
- * PHP version 5
- *
- * Copyright (C) Villanova University 2010.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License version 2,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
- *
- * @category VuFind
- * @package  RecordDrivers
- * @author   Demian Katz <demian.katz@villanova.edu>
- * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
- * @link     https://vufind.org/wiki/
- */
-namespace VuDL\RecordDriver;
-
-/**
- * Model for VuDL records in Solr.
- *
- * @category VuFind
- * @package  RecordDrivers
- * @author   Demian Katz <demian.katz@villanova.edu>
- * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
- * @link     https://vufind.org/wiki/
- */
-class SolrVudl extends \VuFind\RecordDriver\SolrDefault
-{
-    /**
-     * Full VuDL record
-     *
-     * @var \SimpleXML
-     */
-    protected $fullRecord = null;
-
-    /**
-     * VuDL config
-     *
-     * @var \Zend\Config\Config
-     */
-    protected $vuDLConfig = null;
-
-    /**
-     * Set module.config.php
-     *
-     * @param \Zend\Config\Config $vc Configuration
-     *
-     * @return void
-     */
-    public function setVuDLConfig(\Zend\Config\Config $vc)
-    {
-        $this->vuDLConfig = $vc;
-    }
-
-    /**
-     * Parse the full record, if required, and return it.
-     *
-     * @return object
-     */
-    public function getFullRecord()
-    {
-        if (is_null($this->fullRecord)) {
-            $xml = $this->fields['fullrecord'];
-            $this->fullRecord = simplexml_load_string($xml);
-        }
-        return $this->fullRecord;
-    }
-
-    /**
-     * Returns one of three things: a full URL to a thumbnail preview of the record
-     * if an image is available in an external system; an array of parameters to
-     * send to VuFind's internal cover generator if no fixed URL exists; or false
-     * if no thumbnail can be generated.
-     *
-     * @param string $size Size of thumbnail (small, medium or large -- small is
-     * default).
-     *
-     * @return string|array|bool
-     */
-    public function getThumbnail($size = 'small')
-    {
-        return [
-            'route' => 'files',
-            'routeParams' => [
-                'id' => $this->getUniqueID(),
-                'type' => 'THUMBNAIL'
-            ]
-        ];
-        // We are currently storing only one size of thumbnail; we'll use this for
-        // small and medium sizes in the interface, flagging "large" as unavailable
-        // for now.
-        if ($size == 'large') {
-            return false;
-        }
-        $result = $this->getFullRecord();
-        $thumb = isset($result->thumbnail) ? trim($result->thumbnail) : false;
-        return empty($thumb) ? false : $thumb;
-    }
-
-    /**
-     * Return an array of associative URL arrays with one or more of the following
-     * keys:
-     *
-     * <li>
-     *   <ul>desc: URL description text to display (optional)</ul>
-     *   <ul>url: fully-formed URL (required if 'route' is absent)</ul>
-     *   <ul>route: VuFind route to build URL with (required if 'url' is absent)</ul>
-     *   <ul>routeParams: Parameters for route (optional)</ul>
-     *   <ul>queryString: Query params to append after building route (optional)</ul>
-     * </li>
-     *
-     * @return array
-     */
-    public function getURLs()
-    {
-        if ($this->isCollection()) {
-            return [];
-        } else {
-            $retval = [
-                [
-                    'route' => 'vudl-record',
-                    'routeParams' => [
-                        'id' => $this->getUniqueID()
-                    ]
-                ]
-            ];
-            if ($this->isProtected()) {
-                $retval[0]['prefix'] = $this->getProxyUrl();
-            }
-            return $retval;
-        }
-    }
-
-    /**
-     * Does this record need to be protected from public access?
-     *
-     * @return bool
-     */
-    public function isProtected()
-    {
-        // Is license_str set?
-        if (!isset($this->fields['license_str'])) {
-            return false;
-        }
-        // Check IP range
-        $userIP = $_SERVER['REMOTE_ADDR'];
-        $range = isset($this->vuDLConfig->Access->ip_range)
-            ? $this->vuDLConfig->Access->ip_range->toArray() : [];
-        foreach ($range as $ip) {
-            if (strpos($userIP, $ip) === 0) {
-                return false;
-            }
-        }
-        return true;
-    }
-
-    /**
-     * Get the proxy prefix for protecting this record.
-     *
-     * @return string
-     */
-    protected function getProxyURL()
-    {
-        return isset($this->vuDLConfig->Access->proxy_url)
-            ? $this->vuDLConfig->Access->proxy_url : '';
-    }
-
-    /**
-     * Get all subject headings associated with this record.  Each heading is
-     * returned as an array of chunks, increasing from least specific to most
-     * specific.
-     *
-     * @return array
-     */
-    public function getAllSubjectHeadings()
-    {
-        $retVal = [];
-        if (isset($this->fields['topic'])) {
-            foreach ($this->fields['topic'] as $current) {
-                $retVal[] = explode(' -- ', $current);
-            }
-        }
-        return $retVal;
-    }
-}
diff --git a/module/VuDL/src/VuDL/View/Helper/Bootstrap3/VuDL.php b/module/VuDL/src/VuDL/View/Helper/Bootstrap3/VuDL.php
deleted file mode 100644
index eae49269f0f8654d8c3892cd6dfd3bfe4a27bfbe..0000000000000000000000000000000000000000
--- a/module/VuDL/src/VuDL/View/Helper/Bootstrap3/VuDL.php
+++ /dev/null
@@ -1,75 +0,0 @@
-<?php
-/**
- * VuDL view helper
- *
- * PHP version 5
- *
- * Copyright (C) Villanova University 2010.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License version 2,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
- *
- * @category VuFind
- * @package  View_Helpers
- * @author   Demian Katz <demian.katz@villanova.edu>
- * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
- * @link     https://vufind.org/wiki/
- */
-namespace VuDL\View\Helper\Bootstrap3;
-
-/**
- * VuDL view helper
- *
- * @category VuFind
- * @package  View_Helpers
- * @author   Demian Katz <demian.katz@villanova.edu>
- * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
- * @link     https://vufind.org/wiki/
- */
-class VuDL extends \Zend\View\Helper\AbstractHelper
-{
-    /**
-     * Format technical information.
-     *
-     * @param string $techInfo Input
-     *
-     * @return string
-     */
-    public function formatTechInfo($techInfo)
-    {
-        $old = [
-            '/<(\/[^>]+)>/',
-            '/<([^>]+)>/',
-            '/\/&gt;/',
-            '/&lt;\/div&gt;/',
-            '/<div>\s*<\/div>/',
-            '/(?<=<div>)([^<]+)<div>/',
-            '/<div>/'
-        ];
-        $new = [
-            '&lt;\1&gt;</div>',
-            '<div>&lt;\1&gt;',
-            '/&gt;</div>',
-            '</div>',
-            '</div>',
-            '<a class="xmlt" onClick="'
-                . 'var p=this.parentNode;'
-                . "p.className=p.className.indexOf('collapsed')<0"
-                . " ? 'xml collapsed'"
-                . " : 'xml'"
-                . '">\1</a><div>',
-            '<div class="xml">'
-        ];
-        return preg_replace($old, $new, $techInfo);
-    }
-}
diff --git a/module/VuDL/tests/unit-tests/src/VuDLTest/Connection/FedoraTest.php b/module/VuDL/tests/unit-tests/src/VuDLTest/Connection/FedoraTest.php
deleted file mode 100644
index db5e451680ad7cb9704b9e7c5079a8ce5aadf88d..0000000000000000000000000000000000000000
--- a/module/VuDL/tests/unit-tests/src/VuDLTest/Connection/FedoraTest.php
+++ /dev/null
@@ -1,84 +0,0 @@
-<?php
-/**
- * VuDL Fedora Test Class
- *
- * PHP version 5
- *
- * Copyright (C) Villanova University 2010.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License version 2,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
- *
- * @category VuFind
- * @package  Tests
- * @author   Demian Katz <demian.katz@villanova.edu>
- * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
- * @link     https://vufind.org/wiki/development:testing:unit_tests Wiki
- */
-namespace VuDLTest;
-
-/**
- * VuDL Fedora Test Class
- *
- * @category VuFind
- * @package  Tests
- * @author   Demian Katz <demian.katz@villanova.edu>
- * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
- * @link     https://vufind.org/wiki/development:testing:unit_tests Wiki
- */
-class FedoraTest extends \VuFindTest\Unit\TestCase
-{
-    public function testAllWithMock()
-    {
-        $subject = $this->getMock(
-            '\VuDL\Connection\Fedora',
-            ['getDatastreamContent', 'getDatastreamHeaders'],
-            [(object) [
-                'Fedora' => (object) [
-                    'url_base' => 'http://jsontest.com/',
-                    'query_url' => 'QUERY',
-                    'adminUser' => 'ADMIN',
-                    'adminPass' => 'ADMINPASS'
-                ]
-            ]]
-        );
-        $subject->method('getDatastreamContent')->will(
-            $this->onConsecutiveCalls(
-                '<hasModel xmlns="info:fedora/fedora-system:def/model#" rdf:resource="info:fedora/vudl-system:CLASS1"/><hasModel xmlns="info:fedora/fedora-system:def/model#" rdf:resource="info:fedora/vudl-system:CLASS2"/>',
-                '<xml><a><b id="c"></b></a></xml>',
-                'xlink:href="test_passed"',
-                '<dc:title>T</dc:title><dc:id>ID</dc:id>'
-            )
-        );
-        $subject->method('getDatastreamHeaders')->will(
-            $this->onConsecutiveCalls(
-                ['HTTP/1.1 200 OK'],
-                ['HTTP/1.1 404 EVERYTHING IS WRONG']
-            )
-        );
-
-        $this->assertEquals('http://jsontest.com/', $subject->getBase());
-
-        $this->assertEquals(['CLASS1','CLASS2'], $subject->getClasses('id'));
-
-        $this->assertEquals('<xml><a><b id="c"></b></a></xml>', $subject->getDatastreams('id'));
-        $this->assertEquals('SimpleXMLElement', get_class($subject->getDatastreams('id', true)));
-
-        $this->assertEquals(['test_passed', 'fake'], $subject->getCopyright('id', ['passed' => 'fake']));
-
-        $this->assertEquals(['title' => 'T','id' => 'ID'], $subject->getDetails('id'));
-        // Detail formatting tested in Solr
-
-        $this->assertEquals('Zend\Http\Client', get_class($subject->getHttpClient('url')));
-    }
-}
diff --git a/module/VuDL/tests/unit-tests/src/VuDLTest/Connection/ManagerTest.php b/module/VuDL/tests/unit-tests/src/VuDLTest/Connection/ManagerTest.php
deleted file mode 100644
index 2a76118761e2e862b9304aca65eeeeca76bb8017..0000000000000000000000000000000000000000
--- a/module/VuDL/tests/unit-tests/src/VuDLTest/Connection/ManagerTest.php
+++ /dev/null
@@ -1,135 +0,0 @@
-<?php
-/**
- * VuDL Connection Manager Test Class
- *
- * PHP version 5
- *
- * Copyright (C) Villanova University 2010.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License version 2,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
- *
- * @category VuFind
- * @package  Tests
- * @author   Demian Katz <demian.katz@villanova.edu>
- * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
- * @link     https://vufind.org/wiki/development:testing:unit_tests Wiki
- */
-namespace VuDLTest;
-use VuDL\Connection\Manager;
-
-/**
- * VuDL Connection Manager Test Class
- *
- * @category VuFind
- * @package  Tests
- * @author   Demian Katz <demian.katz@villanova.edu>
- * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
- * @link     https://vufind.org/wiki/development:testing:unit_tests Wiki
- */
-class ManagerTest extends \VuFindTest\Unit\TestCase
-{
-    protected function getTestSubject()
-    {
-        return new \VuDL\Connection\Manager(['First', 'Second'], new FakeServiceLocator());
-    }
-
-    public function testManager()
-    {
-        $subject = $this->getTestSubject();
-        $this->assertEquals('First',  $subject->infoInFirst());
-        $this->assertEquals('First 41',  $subject->paramPassing(41));
-        $this->assertEquals('Second', $subject->nullInFirst());
-        $this->assertEquals('Second', $subject->missingMethodInFirst());
-    }
-
-    public function testAllNull()
-    {
-        $subject = $this->getTestSubject();
-        try {
-            $subject->allNull();
-        } catch(\Exception $e) {
-            $this->assertEquals(
-                'VuDL Connection Failed to resolved method "allNull"',
-                $e->getMessage()
-            );
-            return;
-        }
-
-        $this->fail('Exception not thrown for all null response');
-    }
-
-    public function testMissingMethod()
-    {
-        $subject = $this->getTestSubject();
-        try {
-            $subject->missingMethod();
-        } catch(\Exception $e) {
-            $this->assertEquals(
-                'VuDL Connection Failed to resolved method "missingMethod"',
-                $e->getMessage()
-            );
-            return;
-        }
-
-        $this->fail('Exception not thrown for missing method');
-    }
-}
-
-class FakeServiceLocator
-{
-    public function get($class)
-    {
-        if($class == "VuDL\\Connection\\First") {
-            return new First();
-        } else {
-            return new Second();
-        }
-    }
-}
-
-class First
-{
-    public function infoInFirst()
-    {
-        return 'First';
-    }
-    public function paramPassing($n)
-    {
-        return 'First ' . $n;
-    }
-    public function nullInFirst()
-    {
-        return null;
-    }
-    public function allNull()
-    {
-        return null;
-    }
-}
-
-class Second
-{
-    public function nullInFirst()
-    {
-        return 'Second';
-    }
-    public function missingMethodInFirst()
-    {
-        return 'Second';
-    }
-    public function allNull()
-    {
-        return null;
-    }
-}
diff --git a/module/VuDL/tests/unit-tests/src/VuDLTest/Connection/SolrTest.php b/module/VuDL/tests/unit-tests/src/VuDLTest/Connection/SolrTest.php
deleted file mode 100644
index 2a9d00dd28d9275570db67c1c619840357797ae9..0000000000000000000000000000000000000000
--- a/module/VuDL/tests/unit-tests/src/VuDLTest/Connection/SolrTest.php
+++ /dev/null
@@ -1,199 +0,0 @@
-<?php
-/**
- * VuDL Solr Test Class
- *
- * PHP version 5
- *
- * Copyright (C) Villanova University 2010.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License version 2,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
- *
- * @category VuFind
- * @package  Tests
- * @author   Demian Katz <demian.katz@villanova.edu>
- * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
- * @link     https://vufind.org/wiki/development:testing:unit_tests Wiki
- */
-namespace VuDLTest;
-
-/**
- * VuDL Solr Test Class
- *
- * @category VuFind
- * @package  Tests
- * @author   Demian Katz <demian.katz@villanova.edu>
- * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
- * @link     https://vufind.org/wiki/development:testing:unit_tests Wiki
- */
-class SolrTest extends \VuFindTest\Unit\TestCase
-{
-    public function testMissingDetails()
-    {
-        $subject = new \VuDL\Connection\Solr(
-            (object) ['General' => (object) ['page_length' => 8]],
-            new FakeBackend(['{"response":{"docs":[{"author":"1,2"}]}}'])
-        );
-
-        $this->assertEquals(8, $subject->getPageLength());
-
-        try {
-            $subject->getDetails('id', true);
-        } catch(\Exception $e) {
-            $this->assertEquals(
-                'Missing [Details] in VuDL.ini',
-                $e->getMessage()
-            );
-            return;
-        }
-
-        $this->fail('Exception not thrown for empty VuDL.ini details');
-    }
-
-    public function testAllWithMock()
-    {
-        $subject = new \VuDL\Connection\Solr(
-            (object) [
-                'General' => (object) ['root_id' => 'ROOT'],
-                'Details' => new FakeConfig(['author,author2' => 'Author','series' => 'Series','bacon,eggs' => 'Yum','unused' => ':('])
-            ],
-            new FakeBackend(
-                [
-                    '{"response":{"numFound":0}}',
-                    '{"response":{"numFound":1,"docs":[{"modeltype_str_mv":["123456789012CLASS_ONE","123456789012CLASS_TWO"]}]}}',
-
-                    false,
-                    '{"response":{"docs":[{"author":["A1","A2"],"series":"S1"}]}}',
-                    '{"response":{"docs":[{"author":["A1","A2"],"series":"S1","bacon":"MORE"}]}}',
-
-                    '{"response":{"numFound":0}}',
-                    '{"response":{"numFound":1,"docs":[{"dc_title_str":"LABEL"}]}}',
-
-                    '{"response":{"numFound":0}}',
-                    '{"response":{"numFound":1,"docs":[{"id":"ID", "hierarchy_top_title":"TOP"}]}}',
-
-                    false,
-                    '{"response":{"numFound":1,"docs":[{"fgs.lastModifiedDate":["DATE"]}]}}',
-
-                    '{"response":{"numFound":0}}',
-                    '{"response":{"numFound":1,"docs":[{"id":"ID1"},{"id":"ID2"}]}}',
-
-                    '{"response":{"numFound":0}}',
-                    '{"response":{"numFound":1,"docs":[{"hierarchy_parent_id":["id2","id4"],"hierarchy_parent_title":["title2","title4"]}]}}',
-                    '{"response":{"numFound":1,"docs":[{"hierarchy_parent_id":["id3"],"hierarchy_parent_title":["title3"]}]}}',
-                    '{"response":{"numFound":1,"docs":[{"hierarchy_parent_id":["ROOT"],"hierarchy_parent_title":["ROOT"]}]}}',
-                    '{"response":{"numFound":1,"docs":[{"hierarchy_parent_id":["ROOT"],"hierarchy_parent_title":["ROOT"]}]}}',
-
-                    '{"response":{"numFound":0,"docs":[0]}}',
-                    '{"response":{"numFound":1,"docs":[{"license.mdRef":["vuABC"]}]}}',
-                    '{"response":{"numFound":1,"docs":[{"license.mdRef":["vuABC"]}]}}'
-                ]
-            )
-        );
-
-        $this->assertEquals(null, $subject->getClasses('id'));
-        $this->assertEquals(["CLASS_ONE", "CLASS_TWO"], $subject->getClasses('id'));
-
-        $this->assertEquals(null, $subject->getDetails('id', false));
-        $this->assertEquals(["author" => ["A1","A2"],"series" => "S1"], $subject->getDetails('id', false));
-        $this->assertEquals(
-            [
-            "author" => ["title" => "Author", "value" => ["A1","A2"]],
-            "bacon" => ["title" => "Yum", "value" => ["MORE"]],
-            "series" => ["title" => "Series", "value" => "S1"],
-            ], $subject->getDetails('id', true)
-        );
-
-        $this->assertEquals(null, $subject->getLabel('id'));
-        $this->assertEquals("LABEL", $subject->getLabel('id'));
-
-        $this->assertEquals([], $subject->getMemberList('root'));
-        $this->assertEquals([['id' => 'ID','title' => 'TOP']], $subject->getMemberList('root'));
-
-        $this->assertEquals(null, $subject->getModDate('id'));
-        $this->assertEquals("DATE", $subject->getModDate('id'));
-
-        $this->assertEquals(null, $subject->getOrderedMembers('id'));
-        $this->assertEquals(["ID1", "ID2"], $subject->getOrderedMembers('id', ['fake_filter']));
-
-        $this->assertEquals(null, $subject->getParentList('id1'));
-        $this->assertEquals([['id4' => 'title4'], ['id3' => 'title3','id2' => 'title2']], $subject->getParentList('id1'));
-        // Cache test
-        $this->assertEquals([['id4' => 'title4'], ['id3' => 'title3','id2' => 'title2']], $subject->getParentList('id1'));
-
-        $this->assertEquals(null, $subject->getCopyright('id', []));
-        $this->assertEquals(["vuABC", "WTFPL"], $subject->getCopyright('id', ['A' => 'WTFPL']));
-        $this->assertEquals(["vuABC", false], $subject->getCopyright('id', ['X' => 'WTFPL']));
-
-        $this->assertEquals(16, $subject->getPageLength());
-    }
-}
-
-class FakeBackend
-{
-    protected $returnList;
-
-    public function __construct($returns)
-    {
-        $this->returnList = $returns;
-    }
-
-    public function getConnector()
-    {
-        return new FakeSolr($this->returnList);
-    }
-}
-
-class FakeSolr
-{
-    protected $callNumber = 0;
-    protected $returnList;
-
-    public function __construct($returns)
-    {
-        $this->returnList = $returns;
-    }
-
-    public function getMap()
-    {
-        return new FakeMap();
-    }
-
-    public function search()
-    {
-        return $this->returnList[$this->callNumber++];
-    }
-}
-
-class FakeMap
-{
-    public function __call($method, $args)
-    {
-        return new \ArrayObject();
-    }
-}
-
-class FakeConfig
-{
-    protected $value;
-
-    public function __construct($v)
-    {
-        $this->value = $v;
-    }
-
-    public function toArray()
-    {
-        return $this->value;
-    }
-}
diff --git a/module/VuFind/Module.php b/module/VuFind/Module.php
index b49a7515551e77d282b0663aebe343e36a957281..5fba39ce42bf7e1c5b9f9cf7259ba99357b0ce3c 100644
--- a/module/VuFind/Module.php
+++ b/module/VuFind/Module.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Module
diff --git a/module/VuFind/config/module.config.php b/module/VuFind/config/module.config.php
index 0c4d4ae08e3252daca145b7856945a0b17ace4a9..e727075792b9772e5cbbf2ecceb9503d1532c011 100644
--- a/module/VuFind/config/module.config.php
+++ b/module/VuFind/config/module.config.php
@@ -140,6 +140,7 @@ $config = [
             'VuFind\ContentExcerptsPluginManager' => 'VuFind\Service\Factory::getContentExcerptsPluginManager',
             'VuFind\ContentReviewsPluginManager' => 'VuFind\Service\Factory::getContentReviewsPluginManager',
             'VuFind\CookieManager' => 'VuFind\Service\Factory::getCookieManager',
+            'VuFind\Cover\Router' => 'VuFind\Service\Factory::getCoverRouter',
             'VuFind\DateConverter' => 'VuFind\Service\Factory::getDateConverter',
             'VuFind\DbAdapter' => 'VuFind\Service\Factory::getDbAdapter',
             'VuFind\DbAdapterFactory' => 'VuFind\Service\Factory::getDbAdapterFactory',
@@ -233,6 +234,7 @@ $config = [
             'session'        => ['id', 'session_id_seq'],
             'tags'           => ['id', 'tags_id_seq'],
             'user'           => ['id', 'user_id_seq'],
+            'user_card'      => ['id', 'user_card_id_seq'],
             'user_list'      => ['id', 'user_list_id_seq'],
             'user_resource'  => ['id', 'user_resource_id_seq'],
         ],
@@ -335,6 +337,8 @@ $config = [
                 'abstract_factories' => ['VuFind\Db\Table\PluginFactory'],
                 'factories' => [
                     'resource' => 'VuFind\Db\Table\Factory::getResource',
+                    'resourcetags' => 'VuFind\Db\Table\Factory::getResourceTags',
+                    'tags' => 'VuFind\Db\Table\Factory::getTags',
                     'user' => 'VuFind\Db\Table\Factory::getUser',
                     'userlist' => 'VuFind\Db\Table\Factory::getUserList',
                 ],
@@ -343,10 +347,8 @@ $config = [
                     'comments' => 'VuFind\Db\Table\Comments',
                     'oairesumption' => 'VuFind\Db\Table\OaiResumption',
                     'record' => 'VuFind\Db\Table\Record',
-                    'resourcetags' => 'VuFind\Db\Table\ResourceTags',
                     'search' => 'VuFind\Db\Table\Search',
                     'session' => 'VuFind\Db\Table\Session',
-                    'tags' => 'VuFind\Db\Table\Tags',
                     'userresource' => 'VuFind\Db\Table\UserResource',
                     'userstats' => 'VuFind\Db\Table\UserStats',
                     'userstatsfields' => 'VuFind\Db\Table\UserStatsFields',
@@ -385,8 +387,11 @@ $config = [
                     'demo' => 'VuFind\ILS\Driver\Factory::getDemo',
                     'horizon' => 'VuFind\ILS\Driver\Factory::getHorizon',
                     'horizonxmlapi' => 'VuFind\ILS\Driver\Factory::getHorizonXMLAPI',
+                    'lbs4' => 'VuFind\ILS\Driver\Factory::getLBS4',
                     'multibackend' => 'VuFind\ILS\Driver\Factory::getMultiBackend',
                     'noils' => 'VuFind\ILS\Driver\Factory::getNoILS',
+                    'paia' => 'VuFind\ILS\Driver\Factory::getPAIA',
+                    'kohailsdi' => 'VuFind\ILS\Driver\Factory::getKohaILSDI',
                     'unicorn' => 'VuFind\ILS\Driver\Factory::getUnicorn',
                     'voyager' => 'VuFind\ILS\Driver\Factory::getVoyager',
                     'voyagerrestful' => 'VuFind\ILS\Driver\Factory::getVoyagerRestful',
@@ -397,7 +402,6 @@ $config = [
                     'evergreen' => 'VuFind\ILS\Driver\Evergreen',
                     'innovative' => 'VuFind\ILS\Driver\Innovative',
                     'koha' => 'VuFind\ILS\Driver\Koha',
-                    'lbs4' => 'VuFind\ILS\Driver\LBS4',
                     'newgenlib' => 'VuFind\ILS\Driver\NewGenLib',
                     'polaris' => 'VuFind\ILS\Driver\Polaris',
                     'sample' => 'VuFind\ILS\Driver\Sample',
@@ -419,6 +423,8 @@ $config = [
                     'europeanaresults' => 'VuFind\Recommend\Factory::getEuropeanaResults',
                     'expandfacets' => 'VuFind\Recommend\Factory::getExpandFacets',
                     'favoritefacets' => 'VuFind\Recommend\Factory::getFavoriteFacets',
+                    'mapselection' => 'VuFind\Recommend\Factory::getMapSelection',
+                    'resultgooglemapajax' => 'VuFind\Recommend\Factory::getResultGoogleMapAjax',
                     'sidefacets' => 'VuFind\Recommend\Factory::getSideFacets',
                     'randomrecommend' => 'VuFind\Recommend\Factory::getRandomRecommend',
                     'summonbestbets' => 'VuFind\Recommend\Factory::getSummonBestBets',
@@ -440,7 +446,6 @@ $config = [
                     'openlibrarysubjectsdeferred' => 'VuFind\Recommend\OpenLibrarySubjectsDeferred',
                     'pubdatevisajax' => 'VuFind\Recommend\PubDateVisAjax',
                     'removefilters' => 'VuFind\Recommend\RemoveFilters',
-                    'resultgooglemapajax' => 'VuFind\Recommend\ResultGoogleMapAjax',
                     'spellingsuggestions' => 'VuFind\Recommend\SpellingSuggestions',
                     'summonbestbetsdeferred' => 'VuFind\Recommend\SummonBestBetsDeferred',
                     'summondatabasesdeferred' => 'VuFind\Recommend\SummonDatabasesDeferred',
@@ -607,6 +612,8 @@ $config = [
         // driver is not defined here, it will inherit configuration from a configured
         // parent class.  The defaultTab setting may be used to specify the default
         // active tab; if null, the value from the relevant .ini file will be used.
+        // You can also specify which tabs are loaded in the background when arriving
+        // at a record tabs view with backgroundLoadedTabs as a list of tab indexes.
         'recorddriver_tabs' => [
             'VuFind\RecordDriver\EDS' => [
                 'tabs' => [
@@ -651,6 +658,7 @@ $config = [
                     'Details' => 'StaffViewArray',
                 ],
                 'defaultTab' => null,
+                // 'backgroundLoadedTabs' => ['UserComments', 'Details']
             ],
             'VuFind\RecordDriver\SolrMarc' => [
                 'tabs' => [
@@ -705,6 +713,7 @@ $config = [
                 'ipRegEx' => 'VuFind\Role\PermissionProvider\Factory::getIpRegEx',
                 'serverParam' => 'VuFind\Role\PermissionProvider\Factory::getServerParam',
                 'shibboleth' => 'VuFind\Role\PermissionProvider\Factory::getShibboleth',
+                'user' => 'VuFind\Role\PermissionProvider\Factory::getUser',
                 'username' => 'VuFind\Role\PermissionProvider\Factory::getUsername',
             ],
             'invokables' => [
@@ -738,8 +747,8 @@ $dynamicRoutes = [
 
 // Define static routes -- Controller/Action strings
 $staticRoutes = [
-    'Alphabrowse/Home', 'Author/Home', 'Author/Search',
-    'Authority/Home', 'Authority/Record', 'Authority/Search',
+    'Alphabrowse/Home', 'Author/FacetList', 'Author/Home', 'Author/Search',
+    'Authority/FacetList', 'Authority/Home', 'Authority/Record', 'Authority/Search',
     'Browse/Author', 'Browse/Dewey', 'Browse/Era', 'Browse/Genre', 'Browse/Home',
     'Browse/LCC', 'Browse/Region', 'Browse/Tag', 'Browse/Topic', 'Cart/doExport',
     'Cart/Email', 'Cart/Export', 'Cart/Home', 'Cart/MyResearchBulk',
@@ -768,17 +777,17 @@ $staticRoutes = [
     'Primo/Advanced', 'Primo/Home', 'Primo/Search',
     'QRCode/Show', 'QRCode/Unavailable',
     'OAI/Server', 'Pazpar2/Home', 'Pazpar2/Search', 'Records/Home',
-    'Search/Advanced', 'Search/Email', 'Search/History', 'Search/Home',
-    'Search/NewItem', 'Search/OpenSearch', 'Search/Reserves', 'Search/Results',
-    'Search/Suggest',
-    'Summon/Advanced', 'Summon/Home', 'Summon/Search',
+    'Search/Advanced', 'Search/Email', 'Search/FacetList', 'Search/History',
+    'Search/Home', 'Search/NewItem', 'Search/OpenSearch', 'Search/Reserves',
+    'Search/ReservesFacetList', 'Search/Results', 'Search/Suggest',
+    'Summon/Advanced', 'Summon/FacetList', 'Summon/Home', 'Summon/Search',
     'Tag/Home',
     'Upgrade/Home', 'Upgrade/FixAnonymousTags', 'Upgrade/FixDuplicateTags',
     'Upgrade/FixConfig', 'Upgrade/FixDatabase', 'Upgrade/FixMetadata',
     'Upgrade/GetDBCredentials', 'Upgrade/GetDbEncodingPreference',
     'Upgrade/GetSourceDir', 'Upgrade/GetSourceVersion', 'Upgrade/Reset',
     'Upgrade/ShowSQL',
-    'Web/Home', 'Web/Results',
+    'Web/Home', 'Web/FacetList', 'Web/Results',
     'Worldcat/Advanced', 'Worldcat/Home', 'Worldcat/Search'
 ];
 
diff --git a/module/VuFind/sql/migrations/pgsql/3.0/004-modify-seach-columns.sql b/module/VuFind/sql/migrations/pgsql/3.0/004-modify-seach-columns.sql
index 2920ec1f36c5529b394e8ac5d74febc526620b29..4f7b6fcecb8a1716d6ac131c874fba4ba9cd0e63 100644
--- a/module/VuFind/sql/migrations/pgsql/3.0/004-modify-seach-columns.sql
+++ b/module/VuFind/sql/migrations/pgsql/3.0/004-modify-seach-columns.sql
@@ -3,5 +3,7 @@
 --
 
 ALTER TABLE "search"
-  ALTER COLUMN created TYPE timestamp NOT NULL SET DEFAULT '1970-01-01 00:00:00',
-  ADD COLUMN checksum int DEFAULT NULL;
+   ALTER COLUMN created TYPE timestamp,
+   ALTER COLUMN created SET NOT NULL,
+   ALTER COLUMN created SET DEFAULT '1970-01-01 00:00:00',
+   ADD COLUMN checksum int DEFAULT NULL;
diff --git a/module/VuFind/sql/mysql.sql b/module/VuFind/sql/mysql.sql
index e0e45e1d496211711b2d21bdbb1cd297f61f126b..9029d54fa5fb9bd72bc8e3b9efcb9b7c59c2e5ba 100644
--- a/module/VuFind/sql/mysql.sql
+++ b/module/VuFind/sql/mysql.sql
@@ -155,7 +155,7 @@ CREATE TABLE `tags` (
   `id` int(11) NOT NULL AUTO_INCREMENT,
   `tag` varchar(64) NOT NULL DEFAULT '',
   PRIMARY KEY (`id`)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE utf8_bin;
 /*!40101 SET character_set_client = @saved_cs_client */;
 
 --
diff --git a/module/VuFind/src/VuFind/Auth/AbstractBase.php b/module/VuFind/src/VuFind/Auth/AbstractBase.php
index dbd40e8a1a60544ef614f403d87bfb883e631e6d..95c0d82ba00db2d7965313cb5d8920645c774cca 100644
--- a/module/VuFind/src/VuFind/Auth/AbstractBase.php
+++ b/module/VuFind/src/VuFind/Auth/AbstractBase.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Authentication
@@ -95,6 +95,17 @@ abstract class AbstractBase implements \VuFind\Db\Table\DbTableAwareInterface,
         // By default, do no checking.
     }
 
+    /**
+     * Reset any internal status; this is essentially an event hook which most auth
+     * modules can ignore. See ChoiceAuth for a use case example.
+     *
+     * @return void
+     */
+    public function resetState()
+    {
+        // By default, do no checking.
+    }
+
     /**
      * Set configuration.
      *
@@ -263,6 +274,19 @@ abstract class AbstractBase implements \VuFind\Db\Table\DbTableAwareInterface,
         return false;
     }
 
+    /**
+     * Return a canned password policy hint when available
+     *
+     * @param string $pattern Current policy pattern
+     *
+     * @return string
+     */
+    protected function getCannedPasswordPolicyHint($pattern)
+    {
+        return (in_array($pattern, ['numeric', 'alphanumeric']))
+            ? 'password_only_' . $pattern : null;
+    }
+
     /**
      * Password policy for a new password (e.g. minLength, maxLength)
      *
@@ -280,6 +304,17 @@ abstract class AbstractBase implements \VuFind\Db\Table\DbTableAwareInterface,
             $policy['maxLength']
                 = $config->Authentication->maximum_password_length;
         }
+        if (isset($config->Authentication->password_pattern)) {
+            $policy['pattern']
+                = $config->Authentication->password_pattern;
+        }
+        if (isset($config->Authentication->password_hint)) {
+            $policy['hint'] = $config->Authentication->password_hint;
+        } else {
+            $policy['hint'] = $this->getCannedPasswordPolicyHint(
+                isset($policy['pattern']) ? $policy['pattern'] : null
+            );
+        }
         return $policy;
     }
 
@@ -325,5 +360,32 @@ abstract class AbstractBase implements \VuFind\Db\Table\DbTableAwareInterface,
                 )
             );
         }
+        if (!empty($policy['pattern'])) {
+            $valid = true;
+            if ($policy['pattern'] == 'numeric') {
+                if (!ctype_digit($password)) {
+                    $valid = false;
+                }
+            } elseif ($policy['pattern'] == 'alphanumeric') {
+                if (preg_match('/[^\da-zA-Z]/', $password)) {
+                    $valid = false;
+                }
+            } else {
+                $result = preg_match(
+                    "/({$policy['pattern']})/", $password, $matches
+                );
+                if ($result === false) {
+                    throw new \Exception(
+                        'Invalid regexp in password pattern: ' . $policy['pattern']
+                    );
+                }
+                if (!$result || $matches[1] != $password) {
+                    $valid = false;
+                }
+            }
+            if (!$valid) {
+                throw new AuthException($this->translate('password_error_invalid'));
+            }
+        }
     }
 }
diff --git a/module/VuFind/src/VuFind/Auth/CAS.php b/module/VuFind/src/VuFind/Auth/CAS.php
index a6c76571738c1cf847e28ae736b06c7a0368372e..f50f61bd565f895f500f1a639332f06900e23a31 100644
--- a/module/VuFind/src/VuFind/Auth/CAS.php
+++ b/module/VuFind/src/VuFind/Auth/CAS.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Authentication
@@ -45,7 +45,7 @@ class CAS extends AbstractBase
     /**
      * Already Setup phpCAS
      *
-     * @var boolean
+     * @var bool
      */
     protected $phpCASSetup = false;
 
diff --git a/module/VuFind/src/VuFind/Auth/ChoiceAuth.php b/module/VuFind/src/VuFind/Auth/ChoiceAuth.php
index bec8dade3b8638e46e21702bca5799d998aede17..7c37b91a081b86be452ed21c46dc8ae9f3dba7f5 100644
--- a/module/VuFind/src/VuFind/Auth/ChoiceAuth.php
+++ b/module/VuFind/src/VuFind/Auth/ChoiceAuth.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Authentication
@@ -138,6 +138,17 @@ class ChoiceAuth extends AbstractBase
         $this->setStrategyFromRequest($request);
     }
 
+    /**
+     * Reset any internal status; this is essentially an event hook which most auth
+     * modules can ignore. See ChoiceAuth for a use case example.
+     *
+     * @return void
+     */
+    public function resetState()
+    {
+        $this->strategy = false;
+    }
+
     /**
      * Attempt to authenticate the current user.  Throws exception if login fails.
      *
diff --git a/module/VuFind/src/VuFind/Auth/Database.php b/module/VuFind/src/VuFind/Auth/Database.php
index f7485f17f65aa84eadd636782918ea4814e40f93..bf546b4f3785f543a89db462b55515c29badc768 100644
--- a/module/VuFind/src/VuFind/Auth/Database.php
+++ b/module/VuFind/src/VuFind/Auth/Database.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Authentication
diff --git a/module/VuFind/src/VuFind/Auth/Facebook.php b/module/VuFind/src/VuFind/Auth/Facebook.php
index 3bd2e4598c1af95bf176ac3dec2208885823e9c8..bf58a9c3d61615b605f7f6a0b2a5bc3874727bd2 100644
--- a/module/VuFind/src/VuFind/Auth/Facebook.php
+++ b/module/VuFind/src/VuFind/Auth/Facebook.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Authentication
diff --git a/module/VuFind/src/VuFind/Auth/Factory.php b/module/VuFind/src/VuFind/Auth/Factory.php
index 6b63bd98d7df86555639c9962f4555d7cf35bd6a..bf6740eed94173e3d3f27dfae0c67a3769e55b0b 100644
--- a/module/VuFind/src/VuFind/Auth/Factory.php
+++ b/module/VuFind/src/VuFind/Auth/Factory.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Authentication
diff --git a/module/VuFind/src/VuFind/Auth/ILS.php b/module/VuFind/src/VuFind/Auth/ILS.php
index 8cb5d0f6c361af46b103e7a262a59cff47fea314..5a0a05ebed69583985da9dc9071384a6e4562f9e 100644
--- a/module/VuFind/src/VuFind/Auth/ILS.php
+++ b/module/VuFind/src/VuFind/Auth/ILS.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Authentication
@@ -140,7 +140,7 @@ class ILS extends AbstractBase
         try {
             return false !== $this->getCatalog()->checkFunction(
                 'changePassword',
-                ['patron' => $this->getLoggedInPatron()]
+                ['patron' => $this->authenticator->getStoredCatalogCredentials()]
             );
         } catch (ILSException $e) {
             return false;
@@ -155,7 +155,13 @@ class ILS extends AbstractBase
     public function getPasswordPolicy()
     {
         $policy = $this->getCatalog()->getPasswordPolicy($this->getLoggedInPatron());
-        return $policy !== false ? $policy : parent::getPasswordPolicy();
+        if ($policy === false) {
+            return parent::getPasswordPolicy();
+        }
+        if (isset($policy['pattern']) && empty($policy['hint'])) {
+            $policy['hint'] = $this->getCannedPasswordPolicyHint($policy['pattern']);
+        }
+        return $policy;
     }
 
     /**
@@ -267,7 +273,7 @@ class ILS extends AbstractBase
      *
      * @throws AuthException
      *
-     * @return array Patron
+     * @return array|null Patron or null if no credentials exist
      */
     protected function getLoggedInPatron()
     {
diff --git a/module/VuFind/src/VuFind/Auth/ILSAuthenticator.php b/module/VuFind/src/VuFind/Auth/ILSAuthenticator.php
index ef26a071d3512b6832e35181bb4ec6aa55c7e017..caedb859a5ef5d36cd91c5568c4614fd9214c238 100644
--- a/module/VuFind/src/VuFind/Auth/ILSAuthenticator.php
+++ b/module/VuFind/src/VuFind/Auth/ILSAuthenticator.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Authentication
@@ -72,6 +72,28 @@ class ILSAuthenticator
         $this->catalog = $catalog;
     }
 
+    /**
+     * Get stored catalog credentials for the current user.
+     *
+     * Returns associative array of cat_username and cat_password if they are
+     * available, false otherwise. This method does not verify that the credentials
+     * are valid.
+     *
+     * @return array|bool
+     */
+    public function getStoredCatalogCredentials()
+    {
+        // Fail if no username is found, but allow a missing password (not every ILS
+        // requires a password to connect).
+        if (($user = $this->auth->isLoggedIn()) && !empty($user->cat_username)) {
+            return [
+                'cat_username' => $user->cat_username,
+                'cat_password' => $user->cat_password
+            ];
+        }
+        return false;
+    }
+
     /**
      * Log the current user into the catalog using stored credentials; if this
      * fails, clear the user's stored credentials so they can enter new, corrected
@@ -85,9 +107,7 @@ class ILSAuthenticator
     {
         // Fail if no username is found, but allow a missing password (not every ILS
         // requires a password to connect).
-        if (($user = $this->auth->isLoggedIn())
-            && isset($user->cat_username) && !empty($user->cat_username)
-        ) {
+        if (($user = $this->auth->isLoggedIn()) && !empty($user->cat_username)) {
             // Do we have a previously cached ILS account?
             if (isset($this->ilsAccount[$user->cat_username])) {
                 return $this->ilsAccount[$user->cat_username];
diff --git a/module/VuFind/src/VuFind/Auth/InvalidArgumentException.php b/module/VuFind/src/VuFind/Auth/InvalidArgumentException.php
index 98a9f9a1204e8ae44b7a297bb78ccfcb4b58d57a..7f72239bdc2869a2a4ba58256c64151b4d462bfa 100644
--- a/module/VuFind/src/VuFind/Auth/InvalidArgumentException.php
+++ b/module/VuFind/src/VuFind/Auth/InvalidArgumentException.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Exceptions
diff --git a/module/VuFind/src/VuFind/Auth/LDAP.php b/module/VuFind/src/VuFind/Auth/LDAP.php
index e006e17f15490afc75e8064f90fe2d56ee08d45d..d40cbb6557fd0407e925d3b4830e09f1ecba6793 100644
--- a/module/VuFind/src/VuFind/Auth/LDAP.php
+++ b/module/VuFind/src/VuFind/Auth/LDAP.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Authentication
@@ -277,8 +277,19 @@ class LDAP extends AbstractBase
                 foreach ($fields as $field) {
                     $configValue = $this->getSetting($field);
                     if ($data[$i][$j] == $configValue && !empty($configValue)) {
-                        $value = $data[$i][$configValue][0];
-                        $this->debug("found $field = $value");
+                        $value = $data[$i][$configValue];
+                        $separator = $this->config->LDAP->separator;
+                        // if no separator is given map only the first value
+                        if (isset($separator)) {
+                            $tmp = [];
+                            for ($k = 0; $k < $value["count"]; $k++) {
+                                $tmp[] = $value[$k];
+                            }
+                            $value = implode($separator, $tmp);
+                        } else {
+                            $value = $value[0];
+                        }
+                        
                         if ($field != "cat_password") {
                             $user->$field = ($value === null) ? '' : $value;
                         } else {
diff --git a/module/VuFind/src/VuFind/Auth/Manager.php b/module/VuFind/src/VuFind/Auth/Manager.php
index da5dc330c3dd314e53822d924eeded2c7fa29ce1..69c93ae22d8d21ffb0e4c21e060be94bbbd72758 100644
--- a/module/VuFind/src/VuFind/Auth/Manager.php
+++ b/module/VuFind/src/VuFind/Auth/Manager.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Authentication
@@ -226,9 +226,10 @@ class Manager implements \ZfcRbac\Identity\IdentityProviderInterface
      */
     public function supportsPasswordChange($authMethod = null)
     {
-        if ($this->getAuth($authMethod)->supportsPasswordChange()) {
-            return isset($this->config->Authentication->change_password)
-                && $this->config->Authentication->change_password;
+        if (isset($this->config->Authentication->change_password)
+            && $this->config->Authentication->change_password
+        ) {
+            return $this->getAuth($authMethod)->supportsPasswordChange();
         }
         return false;
     }
@@ -557,6 +558,7 @@ class Manager implements \ZfcRbac\Identity\IdentityProviderInterface
         if (!$this->getAuth()->getSessionInitiator(null)
             && !$this->csrf->isValid($request->getPost()->get('csrf'))
         ) {
+            $this->getAuth()->resetState();
             throw new AuthException('authentication_error_technical');
         }
 
diff --git a/module/VuFind/src/VuFind/Auth/MultiAuth.php b/module/VuFind/src/VuFind/Auth/MultiAuth.php
index 18de614f4749d9591eee303ee74d0f4ab0a6fe8a..b643e3240d7a56269174c5b5e0269edb457fccd7 100644
--- a/module/VuFind/src/VuFind/Auth/MultiAuth.php
+++ b/module/VuFind/src/VuFind/Auth/MultiAuth.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Authentication
@@ -130,9 +130,11 @@ class MultiAuth extends AbstractBase
     public function setConfig($config)
     {
         parent::setConfig($config);
-        $this->methods = array_map(
-            'trim', explode(',', $config->MultiAuth->method_order)
-        );
+        if (isset($config->MultiAuth->method_order)) {
+            $this->methods = array_map(
+                'trim', explode(',', $config->MultiAuth->method_order)
+            );
+        }
         if (isset($config->MultiAuth->filters)
             && strlen($config->MultiAuth->filters)
         ) {
diff --git a/module/VuFind/src/VuFind/Auth/MultiILS.php b/module/VuFind/src/VuFind/Auth/MultiILS.php
index be1acf2c74e2a66cf99a633a131929f1a4ff72db..8084e63b39e3dc2dd7eacac10c4e7fc217e21ef4 100644
--- a/module/VuFind/src/VuFind/Auth/MultiILS.php
+++ b/module/VuFind/src/VuFind/Auth/MultiILS.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Authentication
diff --git a/module/VuFind/src/VuFind/Auth/PluginFactory.php b/module/VuFind/src/VuFind/Auth/PluginFactory.php
index 4198a81414d00c82efff3a71677c07325e67426d..602c66e8c9b6d642d577d2f7da395d9241a46392 100644
--- a/module/VuFind/src/VuFind/Auth/PluginFactory.php
+++ b/module/VuFind/src/VuFind/Auth/PluginFactory.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Authentication
diff --git a/module/VuFind/src/VuFind/Auth/PluginManager.php b/module/VuFind/src/VuFind/Auth/PluginManager.php
index 9079804d8997e08aaeafb957e53c106577a02940..d4a4cc01d9b4d5b212cc386d752e1a89ab3a3bf8 100644
--- a/module/VuFind/src/VuFind/Auth/PluginManager.php
+++ b/module/VuFind/src/VuFind/Auth/PluginManager.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Authentication
diff --git a/module/VuFind/src/VuFind/Auth/SIP2.php b/module/VuFind/src/VuFind/Auth/SIP2.php
index f0789bd8f9625052dd47151036affc909cefae6e..542fce8a65ebe76ba1ac0fd43606faa328da50bb 100644
--- a/module/VuFind/src/VuFind/Auth/SIP2.php
+++ b/module/VuFind/src/VuFind/Auth/SIP2.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Authentication
diff --git a/module/VuFind/src/VuFind/Auth/Shibboleth.php b/module/VuFind/src/VuFind/Auth/Shibboleth.php
index 9a948e56a881a837a6c7d699aa5651df6009894f..874e59e2ea83768dff76f8f55c33b213419be0d7 100644
--- a/module/VuFind/src/VuFind/Auth/Shibboleth.php
+++ b/module/VuFind/src/VuFind/Auth/Shibboleth.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Authentication
@@ -211,7 +211,10 @@ class Shibboleth extends AbstractBase
         if (isset($config->Shibboleth->logout)
             && !empty($config->Shibboleth->logout)
         ) {
-            $url = $config->Shibboleth->logout . '?return=' . urlencode($url);
+            $append = (strpos($config->Shibboleth->logout, '?') !== false) ? '&'
+                : '?';
+            $url = $config->Shibboleth->logout . $append . 'return='
+                . urlencode($url);
         }
 
         // Send back the redirect URL (possibly modified):
diff --git a/module/VuFind/src/VuFind/Autocomplete/AutocompleteInterface.php b/module/VuFind/src/VuFind/Autocomplete/AutocompleteInterface.php
index c6e3f8c27aee0d121840dcbe563bc658e6c1d069..247a2108bc3bd273d17753440f5201650c7af3ed 100644
--- a/module/VuFind/src/VuFind/Autocomplete/AutocompleteInterface.php
+++ b/module/VuFind/src/VuFind/Autocomplete/AutocompleteInterface.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Autocomplete
diff --git a/module/VuFind/src/VuFind/Autocomplete/Factory.php b/module/VuFind/src/VuFind/Autocomplete/Factory.php
index 10b3bb371a6cb55900d55eac136d046c32e3173b..0d73fc2ee4b15da2bdfbd0baabb42ed5a666e445 100644
--- a/module/VuFind/src/VuFind/Autocomplete/Factory.php
+++ b/module/VuFind/src/VuFind/Autocomplete/Factory.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Autocomplete
diff --git a/module/VuFind/src/VuFind/Autocomplete/None.php b/module/VuFind/src/VuFind/Autocomplete/None.php
index 5dcec0056a5a0507ac14a032ef480b40fa45e402..1be913e29597b591cb147f82be820b5905db7dc0 100644
--- a/module/VuFind/src/VuFind/Autocomplete/None.php
+++ b/module/VuFind/src/VuFind/Autocomplete/None.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Autocomplete
diff --git a/module/VuFind/src/VuFind/Autocomplete/OCLCIdentities.php b/module/VuFind/src/VuFind/Autocomplete/OCLCIdentities.php
index 1424b40a2db58c56671453179d37548b853663c5..36a8c5ee5e14685f5dd437ced52a192a6b8077de 100644
--- a/module/VuFind/src/VuFind/Autocomplete/OCLCIdentities.php
+++ b/module/VuFind/src/VuFind/Autocomplete/OCLCIdentities.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Autocomplete
diff --git a/module/VuFind/src/VuFind/Autocomplete/PluginFactory.php b/module/VuFind/src/VuFind/Autocomplete/PluginFactory.php
index 1e89fcb72303c1cf2fda591839156fd1bfa8ba1e..1c0a1c5a030f0a4b33bb82d2b612d1cda2b54679 100644
--- a/module/VuFind/src/VuFind/Autocomplete/PluginFactory.php
+++ b/module/VuFind/src/VuFind/Autocomplete/PluginFactory.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Autocomplete
diff --git a/module/VuFind/src/VuFind/Autocomplete/PluginManager.php b/module/VuFind/src/VuFind/Autocomplete/PluginManager.php
index f42c01ce3e995430f64431b5c94048c586cf0cbf..ceffd0844121e342fc6ef92e290483e23887f452 100644
--- a/module/VuFind/src/VuFind/Autocomplete/PluginManager.php
+++ b/module/VuFind/src/VuFind/Autocomplete/PluginManager.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Autocomplete
diff --git a/module/VuFind/src/VuFind/Autocomplete/Solr.php b/module/VuFind/src/VuFind/Autocomplete/Solr.php
index 51c0341949e0c2ad7a0665fd3c1e8d46409b57be..281b6d0f833d3a8efeb3d7272c6fb4ebca3df108 100644
--- a/module/VuFind/src/VuFind/Autocomplete/Solr.php
+++ b/module/VuFind/src/VuFind/Autocomplete/Solr.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Autocomplete
diff --git a/module/VuFind/src/VuFind/Autocomplete/SolrAuth.php b/module/VuFind/src/VuFind/Autocomplete/SolrAuth.php
index 8e9330d267b4ab3c26d9e06857d104b200bf865f..9d7e86a1be1d2b58cd420c66fa4aa7e3ec7f9eb6 100644
--- a/module/VuFind/src/VuFind/Autocomplete/SolrAuth.php
+++ b/module/VuFind/src/VuFind/Autocomplete/SolrAuth.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Autocomplete
diff --git a/module/VuFind/src/VuFind/Autocomplete/SolrCN.php b/module/VuFind/src/VuFind/Autocomplete/SolrCN.php
index ea9bad3484e5671cca4282e24206d7301e95731d..580337dc83e026d008290e41ea0f197c4b02be1d 100644
--- a/module/VuFind/src/VuFind/Autocomplete/SolrCN.php
+++ b/module/VuFind/src/VuFind/Autocomplete/SolrCN.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Autocomplete
diff --git a/module/VuFind/src/VuFind/Autocomplete/SolrReserves.php b/module/VuFind/src/VuFind/Autocomplete/SolrReserves.php
index cf13e537b48246892ffa1cd0b9db0db36aba1434..b047c11b2d33fc0e232ecc6fb64bdc896d933dd2 100644
--- a/module/VuFind/src/VuFind/Autocomplete/SolrReserves.php
+++ b/module/VuFind/src/VuFind/Autocomplete/SolrReserves.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Autocomplete
diff --git a/module/VuFind/src/VuFind/Autocomplete/Tag.php b/module/VuFind/src/VuFind/Autocomplete/Tag.php
index b068e10ed0fad0388273e503a861a2336f27a17d..fde1921715b5f02f907cb0de1e9194f8b5c95091 100644
--- a/module/VuFind/src/VuFind/Autocomplete/Tag.php
+++ b/module/VuFind/src/VuFind/Autocomplete/Tag.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Autocomplete
diff --git a/module/VuFind/src/VuFind/Bootstrapper.php b/module/VuFind/src/VuFind/Bootstrapper.php
index 1d139917e6930cab24f6687206179640e47d43c5..5d521d2399f71a52a21e731faa3cf130f8da187e 100644
--- a/module/VuFind/src/VuFind/Bootstrapper.php
+++ b/module/VuFind/src/VuFind/Bootstrapper.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Bootstrap
@@ -391,30 +391,26 @@ class Bootstrapper
     }
 
     /**
-     * Set up custom 404 status based on exception type.
+     * Set up custom HTTP status based on exception information.
      *
      * @return void
      */
-    protected function initExceptionBased404s()
+    protected function initExceptionBasedHttpStatuses()
     {
-        // 404s not needed in console mode:
+        // HTTP statuses not needed in console mode:
         if (Console::isConsole()) {
             return;
         }
 
         $callback = function ($e) {
             $exception = $e->getParam('exception');
-            if (is_object($exception)) {
-                if ($exception instanceof \VuFind\Exception\RecordMissing) {
-                    // TODO: it might be better to solve this problem by using a
-                    // custom RouteNotFoundStrategy.
-                    $response = $e->getResponse();
-                    if (!$response) {
-                        $response = new HttpResponse();
-                        $e->setResponse($response);
-                    }
-                    $response->setStatusCode(404);
+            if ($exception instanceof \VuFind\Exception\HttpStatusInterface) {
+                $response = $e->getResponse();
+                if (!$response) {
+                    $response = new HttpResponse();
+                    $e->setResponse($response);
                 }
+                $response->setStatusCode($exception->getHttpStatus());
             }
         };
         $this->events->attach('dispatch.error', $callback);
diff --git a/module/VuFind/src/VuFind/Cache/KeyGeneratorTrait.php b/module/VuFind/src/VuFind/Cache/KeyGeneratorTrait.php
new file mode 100644
index 0000000000000000000000000000000000000000..490c886e16aa0ccbd8d804e7e7c22f739d3f0621
--- /dev/null
+++ b/module/VuFind/src/VuFind/Cache/KeyGeneratorTrait.php
@@ -0,0 +1,76 @@
+<?php
+/**
+ * VuFind Cache Key Generator Trait
+ *
+ * PHP version 5
+ *
+ * Copyright (C) Leipzig University Library 2016.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ * @category VuFind
+ * @package  Cache
+ * @author   André Lahmann <lahmann@ub.uni-leipzig.de>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     https://vufind.org/wiki/development:architecture:caching
+ */
+namespace VuFind\Cache;
+
+/**
+ * VuFind Cache Key Generator Trait
+ *
+ * Provides functions for generating uniform cache keys.
+ *
+ * @category VuFind
+ * @package  Cache
+ * @author   André Lahmann <lahmann@ub.uni-leipzig.de>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     https://vufind.org/wiki/development:architecture:caching
+ */
+trait KeyGeneratorTrait
+{
+    /**
+     * Method to ensure uniform cache keys for cached VuFind objects.
+     *
+     * @param string|null $suffix Optional suffix that will get appended to the
+     * object class name calling getCacheKey()
+     *
+     * @return string
+     */
+    protected function getCacheKey($suffix = null)
+    {
+        // Build the raw key combining the calling classname with an optional suffix
+        $key = get_class($this) . (!empty($suffix) ? '_' . $suffix : '');
+
+        // Test the build key
+        if ($this->cache
+            && !preg_match($this->cache->getOptions()->getKeyPattern(), $key)
+        ) {
+            // The key violates the currently set StorageAdapter key_pattern. Our
+            // best guess is to remove any characters that do not match the only
+            // default key_pattern for Zend\Cache\StorageAdapters: the filesystem
+            // adapter (default key_pattern "/^[a-z0-9_\+\-]*$/Di").
+            // Any other custom pattern is assumed as less restrictive, thus the
+            // transformed key should match the custom pattern.
+            $key = preg_replace(
+                "/([^a-z0-9_\+\-])+/Di",
+                "", $key
+            );
+        }
+
+        // If we are in production mode reduce the rawkey to md5 hash which will
+        // keep the key length at handy 32hex
+        return APPLICATION_ENV == 'production' ? md5($key) : $key;
+    }
+}
diff --git a/module/VuFind/src/VuFind/Cache/Manager.php b/module/VuFind/src/VuFind/Cache/Manager.php
index 0174002070f9d5466e4f2d3064107d4df2ff25c1..7dcd720a2db4eee499c377ac438ea6f7c5b30df6 100644
--- a/module/VuFind/src/VuFind/Cache/Manager.php
+++ b/module/VuFind/src/VuFind/Cache/Manager.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Cache
@@ -92,6 +92,7 @@ class Manager
         foreach (['config', 'cover', 'language', 'object'] as $cache) {
             $this->createFileCache($cache, $cacheBase . $cache . 's');
         }
+        $this->createFileCache('public', $cacheBase . 'public');
 
         // Set up search specs cache based on config settings:
         $searchCacheType = isset($searchConfig->Cache->type)
diff --git a/module/VuFind/src/VuFind/Cache/Storage/Adapter/NoCacheAdapter.php b/module/VuFind/src/VuFind/Cache/Storage/Adapter/NoCacheAdapter.php
index 6392d776eb59eabdccab52afea5ef0d8e0351404..5c7ea1245b47693ed2985862c4bfc1520c39d4a6 100644
--- a/module/VuFind/src/VuFind/Cache/Storage/Adapter/NoCacheAdapter.php
+++ b/module/VuFind/src/VuFind/Cache/Storage/Adapter/NoCacheAdapter.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Cache
diff --git a/module/VuFind/src/VuFind/Cart.php b/module/VuFind/src/VuFind/Cart.php
index 5df5d4f08cccc30954e55bc745f12d405b11d141..3195c10216dd81402f1e35cb2ca5f59bff363a91 100644
--- a/module/VuFind/src/VuFind/Cart.php
+++ b/module/VuFind/src/VuFind/Cart.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Cart
@@ -306,6 +306,16 @@ class Cart
         return $this->cookieManager->getDomain();
     }
 
+    /**
+     * Get cookie path ('/' if unset).
+     *
+     * @return string
+     */
+    public function getCookiePath()
+    {
+        return $this->cookieManager->getPath();
+    }
+
     /**
      * Process parameters and return the cart content.
      *
diff --git a/module/VuFind/src/VuFind/Config/AccountCapabilities.php b/module/VuFind/src/VuFind/Config/AccountCapabilities.php
index 6ffa6d750b044c3003fcc9dd63922c759f6484dd..b741f7bc8d81fad9d7b226958d1bfb7ed65a973d 100644
--- a/module/VuFind/src/VuFind/Config/AccountCapabilities.php
+++ b/module/VuFind/src/VuFind/Config/AccountCapabilities.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Controller
diff --git a/module/VuFind/src/VuFind/Config/Locator.php b/module/VuFind/src/VuFind/Config/Locator.php
index 3c3bf70e70e72378917968b13ba3242a236c48a6..1f92f623de354dc448d7ea74977137b09a3df3ba 100644
--- a/module/VuFind/src/VuFind/Config/Locator.php
+++ b/module/VuFind/src/VuFind/Config/Locator.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Config
diff --git a/module/VuFind/src/VuFind/Config/PluginFactory.php b/module/VuFind/src/VuFind/Config/PluginFactory.php
index aa90eb80454ee0899afdf4c4f005a05e0a2c0a81..60b36d01f5bcf6b181ddcf79d9bbf63e8168233e 100644
--- a/module/VuFind/src/VuFind/Config/PluginFactory.php
+++ b/module/VuFind/src/VuFind/Config/PluginFactory.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  ServiceManager
diff --git a/module/VuFind/src/VuFind/Config/PluginManager.php b/module/VuFind/src/VuFind/Config/PluginManager.php
index 4d50ce77beb034d5ea33bf9da05ad80be62fdea3..f486f2c7ddfce81afce3f9a7f2bb4b29c40927ad 100644
--- a/module/VuFind/src/VuFind/Config/PluginManager.php
+++ b/module/VuFind/src/VuFind/Config/PluginManager.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  ServiceManager
diff --git a/module/VuFind/src/VuFind/Config/Reader/CacheDecorator.php b/module/VuFind/src/VuFind/Config/Reader/CacheDecorator.php
index 851d67ba4eceab8d1f5df8c08b18906f31daa9bb..3f5243e3899e31319291234137b7ba18df76f53c 100644
--- a/module/VuFind/src/VuFind/Config/Reader/CacheDecorator.php
+++ b/module/VuFind/src/VuFind/Config/Reader/CacheDecorator.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Config
@@ -95,7 +95,7 @@ class CacheDecorator implements ReaderInterface
      *
      * @param string $string String
      *
-     * @return array|boolean
+     * @return array|bool
      */
     public function fromString($string)
     {
diff --git a/module/VuFind/src/VuFind/Config/SearchSpecsReader.php b/module/VuFind/src/VuFind/Config/SearchSpecsReader.php
index 379af8dd6adda0915611f22e4ec9c6335365eeb2..d772c346e30ba51cc73be9f42cb18387234205c6 100644
--- a/module/VuFind/src/VuFind/Config/SearchSpecsReader.php
+++ b/module/VuFind/src/VuFind/Config/SearchSpecsReader.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Config
@@ -74,39 +74,82 @@ class SearchSpecsReader
     {
         // Load data if it is not already in the object's cache:
         if (!isset($this->searchSpecs[$filename])) {
-            // Connect to searchspecs cache:
-            $cache = (null !== $this->cacheManager)
-                ? $this->cacheManager->getCache('searchspecs') : false;
+            $this->searchSpecs[$filename] = $this->getFromPaths(
+                Locator::getBaseConfigPath($filename),
+                Locator::getLocalConfigPath($filename)
+            );
+        }
 
-            // Determine full configuration file path:
-            $fullpath = Locator::getBaseConfigPath($filename);
-            $local = Locator::getLocalConfigPath($filename);
+        return $this->searchSpecs[$filename];
+    }
 
-            // Generate cache key:
-            $cacheKey = $filename . '-'
-                . (file_exists($fullpath) ? filemtime($fullpath) : 0);
-            if (!empty($local)) {
-                $cacheKey .= '-local-' . filemtime($local);
+    /**
+     * Given core and local filenames, retrieve the searchspecs data.
+     *
+     * @param string $defaultFile Full path to file containing default YAML
+     * @param string $customFile  Full path to file containing local customizations
+     * (may be null if no local file exists).
+     *
+     * @return array
+     */
+    protected function getFromPaths($defaultFile, $customFile = null)
+    {
+        // Connect to searchspecs cache:
+        $cache = (null !== $this->cacheManager)
+            ? $this->cacheManager->getCache('searchspecs') : false;
+
+        // Generate cache key:
+        $cacheKey = basename($defaultFile) . '-'
+            . (file_exists($defaultFile) ? filemtime($defaultFile) : 0);
+        if (!empty($customFile)) {
+            $cacheKey .= '-local-' . filemtime($customFile);
+        }
+        $cacheKey = md5($cacheKey);
+
+        // Generate data if not found in cache:
+        if ($cache === false || !($results = $cache->getItem($cacheKey))) {
+            $results = $this->parseYaml($customFile, $defaultFile);
+            if ($cache !== false) {
+                $cache->setItem($cacheKey, $results);
             }
-            $cacheKey = md5($cacheKey);
+        }
 
-            // Generate data if not found in cache:
-            if ($cache === false || !($results = $cache->getItem($cacheKey))) {
-                $results = file_exists($fullpath)
-                    ? Yaml::parse(file_get_contents($fullpath)) : [];
-                if (!empty($local)) {
-                    $localResults = Yaml::parse(file_get_contents($local));
-                    foreach ($localResults as $key => $value) {
-                        $results[$key] = $value;
-                    }
-                }
-                if ($cache !== false) {
-                    $cache->setItem($cacheKey, $results);
+        return $results;
+    }
+
+    /**
+     * Process a YAML file (and its parent, if necessary).
+     *
+     * @param string $file          YAML file to load (will evaluate to empty array
+     * if file does not exist).
+     * @param string $defaultParent Parent YAML file from which $file should
+     * inherit (unless overridden by a specific directive in $file). None by
+     * default.
+     *
+     * @return array
+     */
+    protected function parseYaml($file, $defaultParent = null)
+    {
+        // First load current file:
+        $results = (!empty($file) && file_exists($file))
+            ? Yaml::parse(file_get_contents($file)) : [];
+
+        // Override default parent with explicitly-defined parent, if present:
+        if (isset($results['@parent_yaml'])) {
+            $defaultParent = $results['@parent_yaml'];
+            // Swallow the directive after processing it:
+            unset($results['@parent_yaml']);
+        }
+
+        // Now load in missing sections from parent, if applicable:
+        if (null !== $defaultParent) {
+            foreach ($this->parseYaml($defaultParent) as $section => $contents) {
+                if (!isset($results[$section])) {
+                    $results[$section] = $contents;
                 }
             }
-            $this->searchSpecs[$filename] = $results;
         }
 
-        return $this->searchSpecs[$filename];
+        return $results;
     }
 }
diff --git a/module/VuFind/src/VuFind/Config/Upgrade.php b/module/VuFind/src/VuFind/Config/Upgrade.php
index d5a65bb26cf2f0ab56209d099ec40a33d9bc6242..e013148a35545bc4015760a7297c79406d3c974f 100644
--- a/module/VuFind/src/VuFind/Config/Upgrade.php
+++ b/module/VuFind/src/VuFind/Config/Upgrade.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Config
diff --git a/module/VuFind/src/VuFind/Config/Version.php b/module/VuFind/src/VuFind/Config/Version.php
index 87247fe006698b164f5aeb930d7ba92411fb7628..58d62ca1a4908d1d1d805d5e8fa25604e58696c9 100644
--- a/module/VuFind/src/VuFind/Config/Version.php
+++ b/module/VuFind/src/VuFind/Config/Version.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Controller
diff --git a/module/VuFind/src/VuFind/Config/Writer.php b/module/VuFind/src/VuFind/Config/Writer.php
index 234c70ba2f5b83cc57e926728e3bc1d84d12d14d..16eac6f366d3e9281da86cb7213460e4453c8eeb 100644
--- a/module/VuFind/src/VuFind/Config/Writer.php
+++ b/module/VuFind/src/VuFind/Config/Writer.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Config
diff --git a/module/VuFind/src/VuFind/Connection/OpenLibrary.php b/module/VuFind/src/VuFind/Connection/OpenLibrary.php
index b8074fdb3e3b0f865f3a2b4205512721efb1a399..e3e371ae9b364689c19aab750ad5ae3c47fbb4dd 100644
--- a/module/VuFind/src/VuFind/Connection/OpenLibrary.php
+++ b/module/VuFind/src/VuFind/Connection/OpenLibrary.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  OpenLibrary
diff --git a/module/VuFind/src/VuFind/Connection/Oracle.php b/module/VuFind/src/VuFind/Connection/Oracle.php
index fc75bafd4b35348598acb5a4ed71c821172c0a1b..24054d5ad6666580220cd53fa6f27a0d666e414d 100644
--- a/module/VuFind/src/VuFind/Connection/Oracle.php
+++ b/module/VuFind/src/VuFind/Connection/Oracle.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Oracle
diff --git a/module/VuFind/src/VuFind/Connection/Wikipedia.php b/module/VuFind/src/VuFind/Connection/Wikipedia.php
index c8b55e04ea1c637c4d4e1553b381362a6679a4af..cb1c13eb611710ad724af3d66eef3734009e3fc5 100644
--- a/module/VuFind/src/VuFind/Connection/Wikipedia.php
+++ b/module/VuFind/src/VuFind/Connection/Wikipedia.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Connection
diff --git a/module/VuFind/src/VuFind/Connection/WorldCatUtils.php b/module/VuFind/src/VuFind/Connection/WorldCatUtils.php
index eb4663d3fcbb1020d6f1502c892d3d3a3c8996da..de76a864932faa1b1911088574dd50b87f775669 100644
--- a/module/VuFind/src/VuFind/Connection/WorldCatUtils.php
+++ b/module/VuFind/src/VuFind/Connection/WorldCatUtils.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  WorldCat
@@ -124,7 +124,7 @@ class WorldCatUtils implements \Zend\Log\LoggerAwareInterface
      *
      * @param string $current Name chunk to examine.
      *
-     * @return boolean        Should we use this as a name?
+     * @return bool           Should we use this as a name?
      */
     protected function isUsefulNameChunk($current)
     {
diff --git a/module/VuFind/src/VuFind/Content/AbstractAmazon.php b/module/VuFind/src/VuFind/Content/AbstractAmazon.php
index baeaf43d79771fa1c2b18baed239a9bcd2d80b8b..4cb9b28daca4d2d9625096fc0118e664ff937cff 100644
--- a/module/VuFind/src/VuFind/Content/AbstractAmazon.php
+++ b/module/VuFind/src/VuFind/Content/AbstractAmazon.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Content
diff --git a/module/VuFind/src/VuFind/Content/AbstractBase.php b/module/VuFind/src/VuFind/Content/AbstractBase.php
index cda2b1216a9b06d0d888bd8da7664f3aac28cfe3..78dcb20bd44f4d9593ba7ff490145821ffada3f6 100644
--- a/module/VuFind/src/VuFind/Content/AbstractBase.php
+++ b/module/VuFind/src/VuFind/Content/AbstractBase.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Content
diff --git a/module/VuFind/src/VuFind/Content/AbstractCover.php b/module/VuFind/src/VuFind/Content/AbstractCover.php
index ac6501c918a632567fadd26ecd54f882411a44f3..fd9ee46af21f53ec503c2de8576dd87f7b2b912b 100644
--- a/module/VuFind/src/VuFind/Content/AbstractCover.php
+++ b/module/VuFind/src/VuFind/Content/AbstractCover.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Content
diff --git a/module/VuFind/src/VuFind/Content/AbstractSyndetics.php b/module/VuFind/src/VuFind/Content/AbstractSyndetics.php
index 370a772fde50cc7f888aa425d820f8d0b3001a82..6da5c441838da66bc22e8326f466ac124ee79bd7 100644
--- a/module/VuFind/src/VuFind/Content/AbstractSyndetics.php
+++ b/module/VuFind/src/VuFind/Content/AbstractSyndetics.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Content
diff --git a/module/VuFind/src/VuFind/Content/AuthorNotes/Factory.php b/module/VuFind/src/VuFind/Content/AuthorNotes/Factory.php
index c86136a54f59615e93a4e6c89228565b309287d3..599459bce8e5906b705d89b17f7d79102d0898ca 100644
--- a/module/VuFind/src/VuFind/Content/AuthorNotes/Factory.php
+++ b/module/VuFind/src/VuFind/Content/AuthorNotes/Factory.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Content
diff --git a/module/VuFind/src/VuFind/Content/AuthorNotes/PluginManager.php b/module/VuFind/src/VuFind/Content/AuthorNotes/PluginManager.php
index b45875445778f1303ec4371d8ba4f2c9579be2ee..67c52c12434869fc6d5f004ee040e151b086f767 100644
--- a/module/VuFind/src/VuFind/Content/AuthorNotes/PluginManager.php
+++ b/module/VuFind/src/VuFind/Content/AuthorNotes/PluginManager.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Content
diff --git a/module/VuFind/src/VuFind/Content/AuthorNotes/Syndetics.php b/module/VuFind/src/VuFind/Content/AuthorNotes/Syndetics.php
index 78b6b7d09138a612fdfc23341b13b32d34ecc7ef..521427ec21c4130dc4a04c9ac0f5ae34ff95d10d 100644
--- a/module/VuFind/src/VuFind/Content/AuthorNotes/Syndetics.php
+++ b/module/VuFind/src/VuFind/Content/AuthorNotes/Syndetics.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Content
diff --git a/module/VuFind/src/VuFind/Content/Covers/Amazon.php b/module/VuFind/src/VuFind/Content/Covers/Amazon.php
index bfcc0369ffbce293c222fc19c139747d0d561955..1896efda924aa423e7320c273e0ff6bb4fda7f44 100644
--- a/module/VuFind/src/VuFind/Content/Covers/Amazon.php
+++ b/module/VuFind/src/VuFind/Content/Covers/Amazon.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Content
diff --git a/module/VuFind/src/VuFind/Content/Covers/Booksite.php b/module/VuFind/src/VuFind/Content/Covers/Booksite.php
index 32b47ff5586f589da325b7dda655b635947044b5..787e4fe217a7159ae71dd8f018a50ccc5d08aafe 100644
--- a/module/VuFind/src/VuFind/Content/Covers/Booksite.php
+++ b/module/VuFind/src/VuFind/Content/Covers/Booksite.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Content
diff --git a/module/VuFind/src/VuFind/Content/Covers/Buchhandel.php b/module/VuFind/src/VuFind/Content/Covers/Buchhandel.php
index e9e33dfb7437d26c280125a9dcf8bb9f974a103f..d75ab2bd2863d95cb7547c07da4207ed388f3523 100644
--- a/module/VuFind/src/VuFind/Content/Covers/Buchhandel.php
+++ b/module/VuFind/src/VuFind/Content/Covers/Buchhandel.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Content
diff --git a/module/VuFind/src/VuFind/Content/Covers/ContentCafe.php b/module/VuFind/src/VuFind/Content/Covers/ContentCafe.php
index 32b96a841f68d1b232b33d0d5b66665b69998d97..7a39eff5c4bb20b6d1ba800c408b676fa1fa7da1 100644
--- a/module/VuFind/src/VuFind/Content/Covers/ContentCafe.php
+++ b/module/VuFind/src/VuFind/Content/Covers/ContentCafe.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Content
diff --git a/module/VuFind/src/VuFind/Content/Covers/Factory.php b/module/VuFind/src/VuFind/Content/Covers/Factory.php
index d6834ba6b086da4df2c9e4ea7ecb407cb46365eb..9d62e7fa63c4d34b030fa9f763da49c516ffd135 100644
--- a/module/VuFind/src/VuFind/Content/Covers/Factory.php
+++ b/module/VuFind/src/VuFind/Content/Covers/Factory.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Content
diff --git a/module/VuFind/src/VuFind/Content/Covers/Google.php b/module/VuFind/src/VuFind/Content/Covers/Google.php
index 7d35d4062a7901af97bb78ff1a6513d499a030f4..3242303bba65a705a964e56a8a924de4ed635a94 100644
--- a/module/VuFind/src/VuFind/Content/Covers/Google.php
+++ b/module/VuFind/src/VuFind/Content/Covers/Google.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Content
@@ -78,48 +78,27 @@ class Google extends \VuFind\Content\AbstractCover
      */
     public function getUrl($key, $size, $ids)
     {
-        // Don't bother trying if we can't read JSON:
-        if (!is_callable('json_decode')) {
+        // Don't bother trying if we can't read JSON or ISBN is missing:
+        if (!is_callable('json_decode') || !isset($ids['isbn'])) {
             return false;
         }
-        if (!isset($ids['isbn'])) {
-            return false;
-        }
-        $isbn = $ids['isbn']->get13();
-
-        // Construct the request URL:
-        $url = 'http://books.google.com/books?jscmd=viewapi&' .
-               'bibkeys=ISBN:' . $isbn . '&callback=addTheCover';
 
-        // Make the HTTP request:
+        // Construct the request URL and make the HTTP request:
+        $url = 'https://books.google.com/books?jscmd=viewapi&' .
+               'bibkeys=ISBN:' . $ids['isbn']->get13() . '&callback=addTheCover';
         $result = $this->getHttpClient($url)->send();
 
-        // Was the request successful?
-        if ($result->isSuccess()) {
-            // grab the response:
-            $json = $result->getBody();
-
-            // extract the useful JSON from the response:
-            $count = preg_match('/^[^{]*({.*})[^}]*$/', $json, $matches);
-            if ($count < 1) {
-                return false;
-            }
-            $json = $matches[1];
-
+        // If the request was successful and we can extract a valid response...
+        if ($result->isSuccess()
+            && preg_match('/^[^{]*({.*})[^}]*$/', $result->getBody(), $matches)
+        ) {
             // convert \x26 or \u0026 to &
-            $json = str_replace(["\\x26", "\\u0026"], "&", $json);
-
-            // decode the object:
-            $json = json_decode($json, true);
-
-            // convert a flat object to an array -- probably unnecessary, but
-            // retained just in case the response format changes:
-            if (isset($json['thumbnail_url'])) {
-                $json = [$json];
-            }
+            $json = json_decode(
+                str_replace(['\\x26', '\\u0026'], '&', $matches[1]), true
+            );
 
             // find the first thumbnail URL and process it:
-            foreach ($json as $current) {
+            foreach ((array)$json as $current) {
                 if (isset($current['thumbnail_url'])) {
                     return $current['thumbnail_url'];
                 }
diff --git a/module/VuFind/src/VuFind/Content/Covers/LibraryThing.php b/module/VuFind/src/VuFind/Content/Covers/LibraryThing.php
index 822aefd9501b091740cfec65206387c7fbe2dcf7..2ec6d2f3a81f87cf58ff090f3113c1c2119a1575 100644
--- a/module/VuFind/src/VuFind/Content/Covers/LibraryThing.php
+++ b/module/VuFind/src/VuFind/Content/Covers/LibraryThing.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Content
diff --git a/module/VuFind/src/VuFind/Content/Covers/OpenLibrary.php b/module/VuFind/src/VuFind/Content/Covers/OpenLibrary.php
index c2fa97b5450f1fedfc6850766daa5dfdbdf7e854..d15098b8e009e57663e62402ec3f2221ba3003c4 100644
--- a/module/VuFind/src/VuFind/Content/Covers/OpenLibrary.php
+++ b/module/VuFind/src/VuFind/Content/Covers/OpenLibrary.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Content
diff --git a/module/VuFind/src/VuFind/Content/Covers/PluginManager.php b/module/VuFind/src/VuFind/Content/Covers/PluginManager.php
index 6b18b5f97b36c233c05afc0af2f43261d0a2526d..d09f6e0d1031f3dbfca731e30188eeb8c1bc4976 100644
--- a/module/VuFind/src/VuFind/Content/Covers/PluginManager.php
+++ b/module/VuFind/src/VuFind/Content/Covers/PluginManager.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Content
diff --git a/module/VuFind/src/VuFind/Content/Covers/Summon.php b/module/VuFind/src/VuFind/Content/Covers/Summon.php
index f9a09c4813b374efa0bc8587a8519e048fbda285..a08ae44ce3fa0885dfd194999e99521a978d7998 100644
--- a/module/VuFind/src/VuFind/Content/Covers/Summon.php
+++ b/module/VuFind/src/VuFind/Content/Covers/Summon.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Content
diff --git a/module/VuFind/src/VuFind/Content/Covers/Syndetics.php b/module/VuFind/src/VuFind/Content/Covers/Syndetics.php
index 2256de1c58b6711e13f203ee51200121b4c8e173..5f7095ba4d712954bc52d6db5ba0cf7493e5e2f0 100644
--- a/module/VuFind/src/VuFind/Content/Covers/Syndetics.php
+++ b/module/VuFind/src/VuFind/Content/Covers/Syndetics.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Content
diff --git a/module/VuFind/src/VuFind/Content/Excerpts/Factory.php b/module/VuFind/src/VuFind/Content/Excerpts/Factory.php
index a5f868ce12503b53c79cf276a401b97af5ad70da..cc34ec49ae602f1624c900ec1eba4dbe62916970 100644
--- a/module/VuFind/src/VuFind/Content/Excerpts/Factory.php
+++ b/module/VuFind/src/VuFind/Content/Excerpts/Factory.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Content
diff --git a/module/VuFind/src/VuFind/Content/Excerpts/PluginManager.php b/module/VuFind/src/VuFind/Content/Excerpts/PluginManager.php
index 2613fd7fb3a1f062d1ef3ee91db45173345b2f2e..10e0be3e3a1ffcd9823bc1f5c44b0e99fd2f5557 100644
--- a/module/VuFind/src/VuFind/Content/Excerpts/PluginManager.php
+++ b/module/VuFind/src/VuFind/Content/Excerpts/PluginManager.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Content
diff --git a/module/VuFind/src/VuFind/Content/Excerpts/Syndetics.php b/module/VuFind/src/VuFind/Content/Excerpts/Syndetics.php
index 2b6be1caf4680578007894858cc8b0c28bef1a59..07ffb6564ce4af45fa7402b8a48c64f5ae24cc0d 100644
--- a/module/VuFind/src/VuFind/Content/Excerpts/Syndetics.php
+++ b/module/VuFind/src/VuFind/Content/Excerpts/Syndetics.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Content
diff --git a/module/VuFind/src/VuFind/Content/Factory.php b/module/VuFind/src/VuFind/Content/Factory.php
index 699917552f9dc3ad04be8167a1667031a87bbbc2..63e874de7bd1d9b4cbe78881a3a9b23aec2f62b4 100644
--- a/module/VuFind/src/VuFind/Content/Factory.php
+++ b/module/VuFind/src/VuFind/Content/Factory.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Content
diff --git a/module/VuFind/src/VuFind/Content/Loader.php b/module/VuFind/src/VuFind/Content/Loader.php
index 3b65695773dbf4b0e32d7782258e688648877beb..4ccc2b63396c246cec5fdcfa7aa847104836aea2 100644
--- a/module/VuFind/src/VuFind/Content/Loader.php
+++ b/module/VuFind/src/VuFind/Content/Loader.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Content
diff --git a/module/VuFind/src/VuFind/Content/PluginManager.php b/module/VuFind/src/VuFind/Content/PluginManager.php
index c40b9061d397ae2bb7a4f5ac86f985a620b55d59..3277fd31118a3738f7c479d006fb71afc616e9bd 100644
--- a/module/VuFind/src/VuFind/Content/PluginManager.php
+++ b/module/VuFind/src/VuFind/Content/PluginManager.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Content
diff --git a/module/VuFind/src/VuFind/Content/Reviews/Amazon.php b/module/VuFind/src/VuFind/Content/Reviews/Amazon.php
index 0e208946a19f3be0b2040a73a1f5e13d6b13797d..40d746f76517cbf91ced442cfe0d9788f88c4f00 100644
--- a/module/VuFind/src/VuFind/Content/Reviews/Amazon.php
+++ b/module/VuFind/src/VuFind/Content/Reviews/Amazon.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Content
diff --git a/module/VuFind/src/VuFind/Content/Reviews/AmazonEditorial.php b/module/VuFind/src/VuFind/Content/Reviews/AmazonEditorial.php
index 393efca8b5a067525ed80300d1dbc6845a43c46f..d9fca065a6d02c39d77e6266ac0040ee36077a61 100644
--- a/module/VuFind/src/VuFind/Content/Reviews/AmazonEditorial.php
+++ b/module/VuFind/src/VuFind/Content/Reviews/AmazonEditorial.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Content
diff --git a/module/VuFind/src/VuFind/Content/Reviews/Booksite.php b/module/VuFind/src/VuFind/Content/Reviews/Booksite.php
index 573fee4098a67d0ddf6f445e95cf8a2e38b2f47c..e494071220109648bced42936097a0b0a76e1bb2 100644
--- a/module/VuFind/src/VuFind/Content/Reviews/Booksite.php
+++ b/module/VuFind/src/VuFind/Content/Reviews/Booksite.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Content
diff --git a/module/VuFind/src/VuFind/Content/Reviews/Factory.php b/module/VuFind/src/VuFind/Content/Reviews/Factory.php
index e791a326ffdae5c18f29887dd05abb16e938196a..17f516442b2dc882700e759943e1fbc0ed8be013 100644
--- a/module/VuFind/src/VuFind/Content/Reviews/Factory.php
+++ b/module/VuFind/src/VuFind/Content/Reviews/Factory.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Content
diff --git a/module/VuFind/src/VuFind/Content/Reviews/Guardian.php b/module/VuFind/src/VuFind/Content/Reviews/Guardian.php
index 65ce19ee57d1c38281718e61f61f7506deb38655..c0afc13ee1e20129b59abbf6302d1c943fd58897 100644
--- a/module/VuFind/src/VuFind/Content/Reviews/Guardian.php
+++ b/module/VuFind/src/VuFind/Content/Reviews/Guardian.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Content
diff --git a/module/VuFind/src/VuFind/Content/Reviews/PluginManager.php b/module/VuFind/src/VuFind/Content/Reviews/PluginManager.php
index 793dd587dc551f54dd902c0253c64a464164e7a1..c21ebdab35573ab2cee3c3d1b9376ce2600d2aba 100644
--- a/module/VuFind/src/VuFind/Content/Reviews/PluginManager.php
+++ b/module/VuFind/src/VuFind/Content/Reviews/PluginManager.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Content
diff --git a/module/VuFind/src/VuFind/Content/Reviews/Syndetics.php b/module/VuFind/src/VuFind/Content/Reviews/Syndetics.php
index fcb8ff6884f8420ff49f2f8ece1b92b0a7df6727..a65e2c03faa50d9248f14a7131af48f57d8fe8c9 100644
--- a/module/VuFind/src/VuFind/Content/Reviews/Syndetics.php
+++ b/module/VuFind/src/VuFind/Content/Reviews/Syndetics.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Content
diff --git a/module/VuFind/src/VuFind/Controller/AbstractBase.php b/module/VuFind/src/VuFind/Controller/AbstractBase.php
index dbf4da32eaf88580624bc9303f79c7e9d22ea83e..d5c19c2a06bd125106437880ccfafb2c5f457b74 100644
--- a/module/VuFind/src/VuFind/Controller/AbstractBase.php
+++ b/module/VuFind/src/VuFind/Controller/AbstractBase.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Controller
@@ -460,10 +460,10 @@ class AbstractBase extends AbstractActionController
      * Check to see if a form was submitted from its post value
      * Also validate the Captcha, if it's activated
      *
-     * @param string  $submitElement Name of the post field of the submit button
-     * @param boolean $useRecaptcha  Are we using captcha in this situation?
+     * @param string $submitElement Name of the post field of the submit button
+     * @param bool   $useRecaptcha  Are we using captcha in this situation?
      *
-     * @return boolean
+     * @return bool
      */
     protected function formWasSubmitted($submitElement = 'submit',
         $useRecaptcha = false
@@ -525,6 +525,17 @@ class AbstractBase extends AbstractActionController
         return $this->getServiceLocator()->get('VuFind\Search\Memory');
     }
 
+    /**
+     * Are comments enabled?
+     *
+     * @return bool
+     */
+    protected function commentsEnabled()
+    {
+        $check = $this->getServiceLocator()->get('VuFind\AccountCapabilities');
+        return $check->getCommentSetting() !== 'disabled';
+    }
+
     /**
      * Are lists enabled?
      *
@@ -626,4 +637,15 @@ class AbstractBase extends AbstractActionController
     {
         $this->followup()->clear('url');
     }
+
+    /**
+     * Get the tab configuration for this controller.
+     *
+     * @return array
+     */
+    protected function getRecordTabConfig()
+    {
+        $cfg = $this->getServiceLocator()->get('Config');
+        return $cfg['vufind']['recorddriver_tabs'];
+    }
 }
diff --git a/module/VuFind/src/VuFind/Controller/AbstractRecord.php b/module/VuFind/src/VuFind/Controller/AbstractRecord.php
index 44b3e3b292c3f4c669f90e742c81a5bbe12fd241..7ea90a10dba3ca1796b86e05966640ce39671f53 100644
--- a/module/VuFind/src/VuFind/Controller/AbstractRecord.php
+++ b/module/VuFind/src/VuFind/Controller/AbstractRecord.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Controller
@@ -26,7 +26,8 @@
  * @link     https://vufind.org/wiki/development:plugins:controllers Wiki
  */
 namespace VuFind\Controller;
-use VuFind\Exception\Mail as MailException,
+use VuFind\Exception\Forbidden as ForbiddenException,
+    VuFind\Exception\Mail as MailException,
     VuFind\RecordDriver\AbstractBase as AbstractRecordDriver;
 
 /**
@@ -104,8 +105,20 @@ class AbstractRecord extends AbstractBase
      */
     public function addcommentAction()
     {
+        // Make sure comments are enabled:
+        if (!$this->commentsEnabled()) {
+            throw new ForbiddenException('Comments disabled');
+        }
+
+        $recaptchaActive = $this->recaptcha()->active('userComments');
+
         // Force login:
         if (!($user = $this->getUser())) {
+            // Validate CAPTCHA before redirecting to login:
+            if (!$this->formWasSubmitted('comment', $recaptchaActive)) {
+                return $this->redirectToRecord('', 'UserComments');
+            }
+
             // Remember comment since POST data will be lost:
             return $this->forceLogin(
                 null, ['comment' => $this->params()->fromPost('comment')]
@@ -119,6 +132,11 @@ class AbstractRecord extends AbstractBase
         $comment = $this->params()->fromPost('comment');
         if (empty($comment)) {
             $comment = $this->followup()->retrieveAndClear('comment');
+        } else {
+            // Validate CAPTCHA now only if we're not coming back post-login:
+            if (!$this->formWasSubmitted('comment', $recaptchaActive)) {
+                return $this->redirectToRecord('', 'UserComments');
+            }
         }
 
         // At this point, we should have a comment to save; if we do not,
@@ -145,6 +163,11 @@ class AbstractRecord extends AbstractBase
      */
     public function deletecommentAction()
     {
+        // Make sure comments are enabled:
+        if (!$this->commentsEnabled()) {
+            throw new ForbiddenException('Comments disabled');
+        }
+
         // Force login:
         if (!($user = $this->getUser())) {
             return $this->forceLogin();
@@ -168,7 +191,7 @@ class AbstractRecord extends AbstractBase
     {
         // Make sure tags are enabled:
         if (!$this->tagsEnabled()) {
-            throw new \Exception('Tags disabled');
+            throw new ForbiddenException('Tags disabled');
         }
 
         // Force login:
@@ -203,7 +226,7 @@ class AbstractRecord extends AbstractBase
     {
         // Make sure tags are enabled:
         if (!$this->tagsEnabled()) {
-            throw new \Exception('Tags disabled');
+            throw new ForbiddenException('Tags disabled');
         }
 
         // Force login:
@@ -311,7 +334,7 @@ class AbstractRecord extends AbstractBase
     {
         // Fail if lists are disabled:
         if (!$this->listsEnabled()) {
-            throw new \Exception('Lists disabled');
+            throw new ForbiddenException('Lists disabled');
         }
 
         // Process form submission:
@@ -591,14 +614,15 @@ class AbstractRecord extends AbstractBase
     }
 
     /**
-     * Get the tab configuration for this controller.
+     * Alias to getRecordTabConfig for backward compatibility.
+     *
+     * @deprecated use getRecordTabConfig instead
      *
      * @return array
      */
     protected function getTabConfiguration()
     {
-        $cfg = $this->getServiceLocator()->get('Config');
-        return $cfg['vufind']['recorddriver_tabs'];
+        return $this->getRecordTabConfig();
     }
 
     /**
@@ -612,11 +636,14 @@ class AbstractRecord extends AbstractBase
         $request = $this->getRequest();
         $rtpm = $this->getServiceLocator()->get('VuFind\RecordTabPluginManager');
         $details = $rtpm->getTabDetailsForRecord(
-            $driver, $this->getTabConfiguration(), $request,
+            $driver, $this->getRecordTabConfig(), $request,
             $this->fallbackDefaultTab
         );
         $this->allTabs = $details['tabs'];
         $this->defaultTab = $details['default'] ? $details['default'] : false;
+        $this->backgroundTabs = $rtpm->getBackgroundTabNames(
+            $driver, $this->getRecordTabConfig()
+        );
     }
 
     /**
@@ -646,6 +673,19 @@ class AbstractRecord extends AbstractBase
         return $this->allTabs;
     }
 
+    /**
+     * Get names of tabs to be loaded in the background.
+     *
+     * @return array
+     */
+    protected function getBackgroundTabs()
+    {
+        if (null === $this->backgroundTabs) {
+            $this->loadTabDetails();
+        }
+        return $this->backgroundTabs;
+    }
+
     /**
      * Is the result scroller active?
      *
@@ -686,6 +726,7 @@ class AbstractRecord extends AbstractBase
         $view->tabs = $this->getAllTabs();
         $view->activeTab = strtolower($tab);
         $view->defaultTab = strtolower($this->getDefaultTab());
+        $view->backgroundTabs = $this->getBackgroundTabs();
         $view->loadInitialTabWithAjax
             = isset($config->Site->loadInitialTabWithAjax)
             ? (bool) $config->Site->loadInitialTabWithAjax : false;
@@ -696,6 +737,10 @@ class AbstractRecord extends AbstractBase
             $view->scrollData = $this->resultScroller()->getScrollData($driver);
         }
 
+        $view->callnumberHandler = isset($config->Item_Status->callnumber_handler)
+            ? $config->Item_Status->callnumber_handler
+            : false;
+
         $view->setTemplate($ajax ? 'record/ajaxtab' : 'record/view');
         return $view;
     }
diff --git a/module/VuFind/src/VuFind/Controller/AbstractSearch.php b/module/VuFind/src/VuFind/Controller/AbstractSearch.php
index 392d5f1a9c2951dbf5ff1e486e06f873dfa81ea7..6ce2ba82bb7bc6323c664d172f788aaa3c4e632f 100644
--- a/module/VuFind/src/VuFind/Controller/AbstractSearch.php
+++ b/module/VuFind/src/VuFind/Controller/AbstractSearch.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Controller
@@ -696,4 +696,63 @@ class AbstractSearch extends AbstractBase
 
         return $formatted;
     }
+
+    /**
+     * Returns a list of all items associated with one facet for the lightbox
+     *
+     * Parameters:
+     * facet        The facet to retrieve
+     * searchParams Facet search params from $results->getUrlQuery()->getParams()
+     *
+     * @return mixed
+     */
+    public function facetListAction()
+    {
+        $this->disableSessionWrites();  // avoid session write timing bug
+        // Get results
+        $results = $this->getResultsManager()->get($this->searchClassId);
+        $params = $results->getParams();
+        $params->initFromRequest($this->getRequest()->getQuery());
+        // Get parameters
+        $facet = $this->params()->fromQuery('facet');
+        $page = (int) $this->params()->fromQuery('facetpage', 1);
+        $options = $results->getOptions();
+        $facetSortOptions = $options->getFacetSortOptions();
+        $sort = $this->params()->fromQuery('facetsort', null);
+        if ($sort === null || !in_array($sort, array_keys($facetSortOptions))) {
+            $sort = empty($facetSortOptions)
+                ? 'count'
+                : current(array_keys($facetSortOptions));
+        }
+        $config = $this->getServiceLocator()->get('VuFind\Config')
+            ->get($options->getFacetsIni());
+        $limit = isset($config->Results_Settings->lightboxLimit)
+            ? $config->Results_Settings->lightboxLimit
+            : 50;
+        $limit = $this->params()->fromQuery('facetlimit', $limit);
+        $facets = $results->getPartialFieldFacets(
+            [$facet], false, $limit, $sort, $page,
+            $this->params()->fromQuery('facetop', 'AND') == 'OR'
+        );
+        $list = $facets[$facet]['data']['list'];
+        $params->activateAllFacets();
+        $facetLabel = $params->getFacetLabel($facet);
+
+        $view = $this->createViewModel(
+            [
+                'data' => $list,
+                'exclude' => $this->params()->fromQuery('facetexclude', 0),
+                'facet' => $facet,
+                'facetLabel' => $facetLabel,
+                'operator' => $this->params()->fromQuery('facetop', 'AND'),
+                'page' => $page,
+                'results' => $results,
+                'anotherPage' => $facets[$facet]['more'],
+                'sort' => $sort,
+                'sortOptions' => $facetSortOptions
+            ]
+        );
+        $view->setTemplate('search/facet-list');
+        return $view;
+    }
 }
diff --git a/module/VuFind/src/VuFind/Controller/AjaxController.php b/module/VuFind/src/VuFind/Controller/AjaxController.php
index f6644e95922f6a4aab910a189f07006360a0218c..2295756472bdcac3f58f9ef209241138fe02fc3f 100644
--- a/module/VuFind/src/VuFind/Controller/AjaxController.php
+++ b/module/VuFind/src/VuFind/Controller/AjaxController.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Controller
@@ -224,7 +224,10 @@ class AjaxController extends AbstractBase
                 // If a full status display has been requested, append the HTML:
                 if ($showFullStatus) {
                     $current['full_status'] = $renderer->render(
-                        'ajax/status-full.phtml', ['statusItems' => $record]
+                        'ajax/status-full.phtml', [
+                            'statusItems' => $record,
+                            'callnumberHandler' => $this->getCallnumberHandler()
+                         ]
                     );
                 }
                 $current['record_number'] = array_search($current['id'], $ids);
@@ -295,7 +298,7 @@ class AjaxController extends AbstractBase
                 $list = $transList;
             }
             // All values mode?  Return comma-separated values:
-            return implode(', ', $list);
+            return implode(",\t", $list);
         } else {
             // Message mode?  Return the specified message, translated to the
             // appropriate language.
@@ -303,6 +306,26 @@ class AjaxController extends AbstractBase
         }
     }
 
+    /**
+     * Based on settings and the number of callnumbers, return callnumber handler
+     * Use callnumbers before pickValue is run.
+     *
+     * @param array  $list           Array of callnumbers.
+     * @param string $displaySetting config.ini setting -- first, all or msg
+     *
+     * @return string
+     */
+    protected function getCallnumberHandler($list = null, $displaySetting = null)
+    {
+        if ($displaySetting == 'msg' && count($list) > 1) {
+            return false;
+        }
+        $config = $this->getConfig();
+        return isset($config->Item_Status->callnumber_handler)
+            ? $config->Item_Status->callnumber_handler
+            : false;
+    }
+
     /**
      * Reduce an array of service names to a human-readable string.
      *
@@ -376,6 +399,10 @@ class AjaxController extends AbstractBase
             }
         }
 
+        $callnumberHandler = $this->getCallnumberHandler(
+            $callNumbers, $callnumberSetting
+        );
+
         // Determine call number string based on findings:
         $callNumber = $this->pickValue(
             $callNumbers, $callnumberSetting, 'Multiple Call Numbers'
@@ -406,7 +433,8 @@ class AjaxController extends AbstractBase
             'reserve_message' => $record[0]['reserve'] == 'Y'
                 ? $this->translate('on_reserve')
                 : $this->translate('Not On Reserve'),
-            'callnumber' => htmlentities($callNumber, ENT_COMPAT, 'UTF-8')
+            'callnumber' => htmlentities($callNumber, ENT_COMPAT, 'UTF-8'),
+            'callnumber_handler' => $callnumberHandler
         ];
     }
 
@@ -449,6 +477,9 @@ class AjaxController extends AbstractBase
         foreach ($locations as $location => $details) {
             $locationCallnumbers = array_unique($details['callnumbers']);
             // Determine call number string based on findings:
+            $callnumberHandler = $this->getCallnumberHandler(
+                $locationCallnumbers, $callnumberSetting
+            );
             $locationCallnumbers = $this->pickValue(
                 $locationCallnumbers, $callnumberSetting, 'Multiple Call Numbers'
             );
@@ -462,7 +493,8 @@ class AjaxController extends AbstractBase
                 'callnumbers' =>
                     htmlentities($locationCallnumbers, ENT_COMPAT, 'UTF-8'),
                 'status_unknown' => isset($details['status_unknown'])
-                    ? $details['status_unknown'] : false
+                    ? $details['status_unknown'] : false,
+                'callnumber_handler' => $callnumberHandler
             ];
             $locationList[] = $locationInfo;
         }
@@ -719,7 +751,7 @@ class AjaxController extends AbstractBase
             $tagList[] = [
                 'tag'   => $tag->tag,
                 'cnt'   => $tag->cnt,
-                'is_me' => $tag->is_me == 1 ? true : false
+                'is_me' => !empty($tag->is_me)
             ];
         }
 
@@ -735,6 +767,51 @@ class AjaxController extends AbstractBase
         return $view;
     }
 
+    /**
+     * Get record for integrated list view.
+     *
+     * @return \Zend\Http\Response
+     */
+    protected function getRecordDetailsAjax()
+    {
+        $driver = $this->getRecordLoader()->load(
+            $this->params()->fromQuery('id'),
+            $this->params()->fromQuery('source')
+        );
+        $viewtype = preg_replace(
+            '/\W/', '',
+            trim(strtolower($this->params()->fromQuery('type')))
+        );
+        $request = $this->getRequest();
+        $config = $this->getServiceLocator()->get('Config');
+        $sconfig = $this->getServiceLocator()->get('VuFind\Config')->get('searches');
+
+        $recordTabPlugin = $this->getServiceLocator()
+            ->get('VuFind\RecordTabPluginManager');
+        $details = $recordTabPlugin
+            ->getTabDetailsForRecord(
+                $driver,
+                $config['vufind']['recorddriver_tabs'],
+                $request,
+                'Information'
+            );
+
+        $rtpm = $this->getServiceLocator()->get('VuFind\RecordTabPluginManager');
+        $html = $this->getViewRenderer()
+            ->render(
+                "record/ajaxview-" . $viewtype . ".phtml",
+                [
+                    'defaultTab' => $details['default'],
+                    'driver' => $driver,
+                    'tabs' => $details['tabs'],
+                    'backgroundTabs' => $rtpm->getBackgroundTabNames(
+                        $driver, $this->getRecordTabConfig()
+                    )
+                ]
+            );
+        return $this->output($html, self::STATUS_OK);
+    }
+
     /**
      * Get map data on search results and output in JSON
      *
@@ -985,6 +1062,15 @@ class AjaxController extends AbstractBase
      */
     protected function commentRecordAjax()
     {
+        // Make sure comments are enabled:
+        if (!$this->commentsEnabled()) {
+            return $this->output(
+                $this->translate('Comments disabled'),
+                self::STATUS_ERROR,
+                403
+            );
+        }
+
         $user = $this->getUser();
         if ($user === false) {
             return $this->output(
@@ -1004,6 +1090,16 @@ class AjaxController extends AbstractBase
             );
         }
 
+        $useCaptcha = $this->recaptcha()->active('userComments');
+        $this->recaptcha()->setErrorMode('none');
+        if (!$this->formWasSubmitted('comment', $useCaptcha)) {
+            return $this->output(
+                $this->translate('recaptcha_not_passed'),
+                self::STATUS_ERROR,
+                403
+            );
+        }
+
         $table = $this->getTable('Resource');
         $resource = $table->findResource(
             $id, $this->params()->fromPost('source', DEFAULT_SEARCH_BACKEND)
@@ -1020,6 +1116,15 @@ class AjaxController extends AbstractBase
      */
     protected function deleteRecordCommentAjax()
     {
+        // Make sure comments are enabled:
+        if (!$this->commentsEnabled()) {
+            return $this->output(
+                $this->translate('Comments disabled'),
+                self::STATUS_ERROR,
+                403
+            );
+        }
+
         $user = $this->getUser();
         if ($user === false) {
             return $this->output(
@@ -1193,7 +1298,7 @@ class AjaxController extends AbstractBase
         $this->disableSessionWrites();  // avoid session write timing bug
         $id = $this->params()->fromQuery('id');
         $pickupLib = $this->params()->fromQuery('pickupLib');
-        if (empty($id) || empty($pickupLib)) {
+        if (null === $id || null === $pickupLib) {
             return $this->output(
                 $this->translate('bulk_error_missing'),
                 self::STATUS_ERROR,
@@ -1245,7 +1350,7 @@ class AjaxController extends AbstractBase
         $this->disableSessionWrites();  // avoid session write timing bug
         $id = $this->params()->fromQuery('id');
         $requestGroupId = $this->params()->fromQuery('requestGroupId');
-        if (empty($id) || empty($requestGroupId)) {
+        if (null === $id || null === $requestGroupId) {
             return $this->output(
                 $this->translate('bulk_error_missing'),
                 self::STATUS_ERROR,
diff --git a/module/VuFind/src/VuFind/Controller/AuthorController.php b/module/VuFind/src/VuFind/Controller/AuthorController.php
index b4e9fbf843b980f0b33f6a0f4541b048ff2f25c9..96746db8f622221423a58bc806e1c3a36fb81945 100644
--- a/module/VuFind/src/VuFind/Controller/AuthorController.php
+++ b/module/VuFind/src/VuFind/Controller/AuthorController.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Controller
@@ -38,6 +38,21 @@ namespace VuFind\Controller;
  */
 class AuthorController extends AbstractSearch
 {
+    /**
+     * Returns a list of all items associated with one facet for the lightbox
+     *
+     * Parameters:
+     * facet        The facet to retrieve
+     * searchParams Facet search params from $results->getUrlQuery()->getParams()
+     *
+     * @return mixed
+     */
+    public function facetListAction()
+    {
+        $this->searchClassId = 'SolrAuthor';
+        return parent::facetListAction();
+    }
+
     /**
      * Sets the configuration for displaying author results
      *
diff --git a/module/VuFind/src/VuFind/Controller/AuthorityController.php b/module/VuFind/src/VuFind/Controller/AuthorityController.php
index a7303409d671192c05a71f173f46c3e054a5cdc3..6959b990acac76093e189e07c0a15318306d5dfd 100644
--- a/module/VuFind/src/VuFind/Controller/AuthorityController.php
+++ b/module/VuFind/src/VuFind/Controller/AuthorityController.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Controller
diff --git a/module/VuFind/src/VuFind/Controller/BrowseController.php b/module/VuFind/src/VuFind/Controller/BrowseController.php
index cd966ba0dbde41a383a31e857bcc48a9cb143f93..e27045bb1363eca50f0f374e5df63519b68da7b1 100644
--- a/module/VuFind/src/VuFind/Controller/BrowseController.php
+++ b/module/VuFind/src/VuFind/Controller/BrowseController.php
@@ -26,6 +26,7 @@
  * @link     https://vufind.org/wiki/development:plugins:controllers Wiki
  */
 namespace VuFind\Controller;
+use VuFind\Exception\Forbidden as ForbiddenException;
 
 /**
  * BrowseController Class
@@ -283,7 +284,7 @@ class BrowseController extends AbstractBase
     public function tagAction()
     {
         if (!$this->tagsEnabled()) {
-            throw new \Exception('Tags disabled.');
+            throw new ForbiddenException('Tags disabled.');
         }
 
         $this->setCurrentAction('Tag');
diff --git a/module/VuFind/src/VuFind/Controller/CartController.php b/module/VuFind/src/VuFind/Controller/CartController.php
index dfadc07a704b279acc68ab8013969ed9aaf03065..d3ad3fe8a9bbe487a4f1df5b48f76f6ac199fce8 100644
--- a/module/VuFind/src/VuFind/Controller/CartController.php
+++ b/module/VuFind/src/VuFind/Controller/CartController.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Controller
@@ -26,7 +26,8 @@
  * @link     https://vufind.org Main Site
  */
 namespace VuFind\Controller;
-use VuFind\Exception\Mail as MailException;
+use VuFind\Exception\Forbidden as ForbiddenException,
+    VuFind\Exception\Mail as MailException;
 
 /**
  * Book Bag / Bulk Action Controller
@@ -338,7 +339,10 @@ class CartController extends AbstractBase
             $msg = [
                 'translate' => false, 'html' => true,
                 'msg' => $this->getViewRenderer()->render(
-                    'cart/export-success.phtml', ['url' => $url]
+                    'cart/export-success.phtml', [
+                        'url' => $url,
+                        'exportType' => $export->getBulkExportType($format)
+                    ]
                 )
             ];
             return $this->redirectToSource('success', $msg);
@@ -403,7 +407,7 @@ class CartController extends AbstractBase
     {
         // Fail if lists are disabled:
         if (!$this->listsEnabled()) {
-            throw new \Exception('Lists disabled');
+            throw new ForbiddenException('Lists disabled');
         }
 
         // Load record information first (no need to prompt for login if we just
diff --git a/module/VuFind/src/VuFind/Controller/CollectionController.php b/module/VuFind/src/VuFind/Controller/CollectionController.php
index 3a098f2bf516518c347f3062badb509f271836da..a0233529ab007f335f498858b0d8b18f34dc4af9 100644
--- a/module/VuFind/src/VuFind/Controller/CollectionController.php
+++ b/module/VuFind/src/VuFind/Controller/CollectionController.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Controller
@@ -59,7 +59,7 @@ class CollectionController extends AbstractRecord
      *
      * @return array
      */
-    protected function getTabConfiguration()
+    protected function getRecordTabConfig()
     {
         $cfg = $this->getServiceLocator()->get('Config');
         return $cfg['vufind']['recorddriver_collection_tabs'];
diff --git a/module/VuFind/src/VuFind/Controller/CollectionsController.php b/module/VuFind/src/VuFind/Controller/CollectionsController.php
index 325c13fee83d0109db176cab50f915019e42264f..d62386910fcced39457d344d180097bc0953f941 100644
--- a/module/VuFind/src/VuFind/Controller/CollectionsController.php
+++ b/module/VuFind/src/VuFind/Controller/CollectionsController.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Controller
@@ -178,7 +178,8 @@ class CollectionsController extends AbstractBase
         $result = $searchObject->getFullFieldFacets(
             [$browseField], false, 150000, 'index'
         );
-        $result = $result[$browseField]['data']['list'];
+        $result = isset($result[$browseField]['data']['list'])
+            ? $result[$browseField]['data']['list'] : [];
 
         $delimiter = $this->getBrowseDelimiter();
         foreach ($result as $rkey => $collection) {
diff --git a/module/VuFind/src/VuFind/Controller/CombinedController.php b/module/VuFind/src/VuFind/Controller/CombinedController.php
index 281449c730833cdec4c64288ece3e6eea117880a..50832b8e8bfadb7758eb021833d43e78eb5b96c0 100644
--- a/module/VuFind/src/VuFind/Controller/CombinedController.php
+++ b/module/VuFind/src/VuFind/Controller/CombinedController.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Controller
diff --git a/module/VuFind/src/VuFind/Controller/ConfirmController.php b/module/VuFind/src/VuFind/Controller/ConfirmController.php
index 58afe3086734b105ca8a628c3837937e00a87a65..7bf76a42fafe228bd007ccd3dc5fc53dae1e5716 100644
--- a/module/VuFind/src/VuFind/Controller/ConfirmController.php
+++ b/module/VuFind/src/VuFind/Controller/ConfirmController.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Controller
diff --git a/module/VuFind/src/VuFind/Controller/EITController.php b/module/VuFind/src/VuFind/Controller/EITController.php
index 2eaea7c981277a99427afa67b31de95e283ebc77..d77b3113113c6aa9fab0038d57916d098e67b7bc 100644
--- a/module/VuFind/src/VuFind/Controller/EITController.php
+++ b/module/VuFind/src/VuFind/Controller/EITController.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Controller
diff --git a/module/VuFind/src/VuFind/Controller/EITrecordController.php b/module/VuFind/src/VuFind/Controller/EITrecordController.php
index 6baa09931145cee628668194b4f262eea6c490a6..f5c523484687e3afb7d4944053dc84f85a6d8834 100644
--- a/module/VuFind/src/VuFind/Controller/EITrecordController.php
+++ b/module/VuFind/src/VuFind/Controller/EITrecordController.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Controller
diff --git a/module/VuFind/src/VuFind/Controller/EdsController.php b/module/VuFind/src/VuFind/Controller/EdsController.php
index 05e970f9499de8aeaafb46fac29da1037943ecfa..8c4053ab033b1deea65f1488856e1f0d1d1d8fc3 100644
--- a/module/VuFind/src/VuFind/Controller/EdsController.php
+++ b/module/VuFind/src/VuFind/Controller/EdsController.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Controller
diff --git a/module/VuFind/src/VuFind/Controller/EdsrecordController.php b/module/VuFind/src/VuFind/Controller/EdsrecordController.php
index 21db4a3137ff2b19156f2ac92124354608dd42c4..559dfb06f2b992bddd7756e6fca711f332480af2 100644
--- a/module/VuFind/src/VuFind/Controller/EdsrecordController.php
+++ b/module/VuFind/src/VuFind/Controller/EdsrecordController.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Controller
diff --git a/module/VuFind/src/VuFind/Controller/ErrorController.php b/module/VuFind/src/VuFind/Controller/ErrorController.php
index d63525af1e29ecd13cf9634460775ee32d153cdf..5ba9f87c3a21bcc6117013c60bbfe7d36886d6fb 100644
--- a/module/VuFind/src/VuFind/Controller/ErrorController.php
+++ b/module/VuFind/src/VuFind/Controller/ErrorController.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Controller
diff --git a/module/VuFind/src/VuFind/Controller/Factory.php b/module/VuFind/src/VuFind/Controller/Factory.php
index 7453f8dd931b029239441a61a470d49cf931ceab..44c6f963ced36dc3136139853f3f24297e7e31f5 100644
--- a/module/VuFind/src/VuFind/Controller/Factory.php
+++ b/module/VuFind/src/VuFind/Controller/Factory.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  View_Helpers
diff --git a/module/VuFind/src/VuFind/Controller/HelpController.php b/module/VuFind/src/VuFind/Controller/HelpController.php
index 1096e0f8d9dc640df18217c38dfb15936db3d7a3..320dee1df69fe87b17dfea38069b5e7c56207ec6 100644
--- a/module/VuFind/src/VuFind/Controller/HelpController.php
+++ b/module/VuFind/src/VuFind/Controller/HelpController.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Controller
diff --git a/module/VuFind/src/VuFind/Controller/HierarchyController.php b/module/VuFind/src/VuFind/Controller/HierarchyController.php
index 3794aa6e2f84604fc203b4cb2aaf4adf8dc2a0fc..23b59619058f6dd31ab361d2404e95544a8eb45a 100644
--- a/module/VuFind/src/VuFind/Controller/HierarchyController.php
+++ b/module/VuFind/src/VuFind/Controller/HierarchyController.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Controller
diff --git a/module/VuFind/src/VuFind/Controller/HoldsTrait.php b/module/VuFind/src/VuFind/Controller/HoldsTrait.php
index 5312d88412ddee55ff128efeed5719332a98a3d0..b3cfa79a82ee46a04369b6d17c90f0e3e3567f0b 100644
--- a/module/VuFind/src/VuFind/Controller/HoldsTrait.php
+++ b/module/VuFind/src/VuFind/Controller/HoldsTrait.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Controller
diff --git a/module/VuFind/src/VuFind/Controller/ILLRequestsTrait.php b/module/VuFind/src/VuFind/Controller/ILLRequestsTrait.php
index 063f32a322e7c10525d8d89b3e6e4c2b6405a802..eaa943d0416008bac083a0b7101617c59bbb7345 100644
--- a/module/VuFind/src/VuFind/Controller/ILLRequestsTrait.php
+++ b/module/VuFind/src/VuFind/Controller/ILLRequestsTrait.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Controller
diff --git a/module/VuFind/src/VuFind/Controller/IndexController.php b/module/VuFind/src/VuFind/Controller/IndexController.php
index 3eb671d24b07f8ea66cac72aa5461d49ea18565f..f5a10651c3a83090a7445897e030754cd37fe963 100644
--- a/module/VuFind/src/VuFind/Controller/IndexController.php
+++ b/module/VuFind/src/VuFind/Controller/IndexController.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Controller
diff --git a/module/VuFind/src/VuFind/Controller/InstallController.php b/module/VuFind/src/VuFind/Controller/InstallController.php
index 96f9be03e568c1b86bf9b9bed544c3230a4e0909..04397d971f7146cf9f43b6b38eda2841e50f0348 100644
--- a/module/VuFind/src/VuFind/Controller/InstallController.php
+++ b/module/VuFind/src/VuFind/Controller/InstallController.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Controller
diff --git a/module/VuFind/src/VuFind/Controller/LibGuidesController.php b/module/VuFind/src/VuFind/Controller/LibGuidesController.php
index e1d2677e55257aae638ac0f639af06b3e680745b..18384f9f3a2a36c6dd8ecfbf758b92af28c4c690 100644
--- a/module/VuFind/src/VuFind/Controller/LibGuidesController.php
+++ b/module/VuFind/src/VuFind/Controller/LibGuidesController.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Controller
diff --git a/module/VuFind/src/VuFind/Controller/LibraryCardsController.php b/module/VuFind/src/VuFind/Controller/LibraryCardsController.php
index 37dc5f6eae66198ce7e1ca76d3512e37d67c6ed8..0d447863b5161ccea066378dc361e0327c96696a 100644
--- a/module/VuFind/src/VuFind/Controller/LibraryCardsController.php
+++ b/module/VuFind/src/VuFind/Controller/LibraryCardsController.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Controller
diff --git a/module/VuFind/src/VuFind/Controller/MissingrecordController.php b/module/VuFind/src/VuFind/Controller/MissingrecordController.php
index 390125089f5fb0ff2821b208236dd55c49db9a4e..9998aab0dac920b6d62d3f81cd4e58469e5094d8 100644
--- a/module/VuFind/src/VuFind/Controller/MissingrecordController.php
+++ b/module/VuFind/src/VuFind/Controller/MissingrecordController.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Controller
diff --git a/module/VuFind/src/VuFind/Controller/MyResearchController.php b/module/VuFind/src/VuFind/Controller/MyResearchController.php
index 21d48a5bfa269b8d1d631a67345cdec475b38499..60cffadf461a169c57cdf51745e44be87875e5d9 100644
--- a/module/VuFind/src/VuFind/Controller/MyResearchController.php
+++ b/module/VuFind/src/VuFind/Controller/MyResearchController.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Controller
@@ -28,10 +28,12 @@
 namespace VuFind\Controller;
 
 use VuFind\Exception\Auth as AuthException,
+    VuFind\Exception\Forbidden as ForbiddenException,
     VuFind\Exception\Mail as MailException,
     VuFind\Exception\ListPermission as ListPermissionException,
     VuFind\Exception\RecordMissing as RecordMissingException,
-    VuFind\Search\RecommendListener, Zend\Stdlib\Parameters;
+    VuFind\Search\RecommendListener, Zend\Stdlib\Parameters,
+    Zend\View\Model\ViewModel;
 
 /**
  * Controller for the user account area.
@@ -295,7 +297,7 @@ class MyResearchController extends AbstractBase
         $sessId = $this->getServiceLocator()->get('VuFind\SessionManager')->getId();
         $row = $searchTable->getOwnedRowById($searchId, $sessId, $userId);
         if (empty($row)) {
-            throw new \Exception('Access denied.');
+            throw new ForbiddenException('Access denied.');
         }
         $row->saved = $saved ? 1 : 0;
         $row->user_id = $userId;
@@ -312,7 +314,7 @@ class MyResearchController extends AbstractBase
         // Fail if saved searches are disabled.
         $check = $this->getServiceLocator()->get('VuFind\AccountCapabilities');
         if ($check->getSavedSearchSetting() === 'disabled') {
-            throw new \Exception('Saved searches disabled.');
+            throw new ForbiddenException('Saved searches disabled.');
         }
 
         $user = $this->getUser();
@@ -643,7 +645,7 @@ class MyResearchController extends AbstractBase
     {
         // Fail if lists are disabled:
         if (!$this->listsEnabled()) {
-            throw new \Exception('Lists disabled');
+            throw new ForbiddenException('Lists disabled');
         }
 
         // Check for "delete item" request; parameter may be in GET or POST depending
@@ -775,7 +777,7 @@ class MyResearchController extends AbstractBase
     {
         // Fail if lists are disabled:
         if (!$this->listsEnabled()) {
-            throw new \Exception('Lists disabled');
+            throw new ForbiddenException('Lists disabled');
         }
 
         // User must be logged in to edit list:
@@ -816,7 +818,7 @@ class MyResearchController extends AbstractBase
     {
         // Fail if lists are disabled:
         if (!$this->listsEnabled()) {
-            throw new \Exception('Lists disabled');
+            throw new ForbiddenException('Lists disabled');
         }
 
         // Get requested list ID:
@@ -1316,7 +1318,7 @@ class MyResearchController extends AbstractBase
                     $view->hash = $hash;
                     $view->username = $user->username;
                     $view->useRecaptcha
-                        = $this->recaptcha()->active('passwordRecovery');
+                        = $this->recaptcha()->active('changePassword');
                     $view->setTemplate('myresearch/newpassword');
                     return $view;
                 }
@@ -1326,6 +1328,26 @@ class MyResearchController extends AbstractBase
         return $this->forwardTo('MyResearch', 'Login');
     }
 
+    /**
+     * Reset the new password form and return the modified view. When a user has
+     * already been loaded from an existing hash, this resets the hash and updates
+     * the form so that the user can try again.
+     *
+     * @param mixed     $userFromHash User loaded from database, or false if none.
+     * @param ViewModel $view         View object
+     *
+     * @return ViewModel
+     */
+    protected function resetNewPasswordForm($userFromHash, ViewModel $view)
+    {
+        if ($userFromHash) {
+            $userFromHash->updateHash();
+            $view->username = $userFromHash->username;
+            $view->hash = $userFromHash->verify_hash;
+        }
+        return $view;
+    }
+
     /**
      * Handling submission of a new password for a user.
      *
@@ -1351,7 +1373,8 @@ class MyResearchController extends AbstractBase
         $view->useRecaptcha = $this->recaptcha()->active('changePassword');
         // Check reCaptcha
         if (!$this->formWasSubmitted('submit', $view->useRecaptcha)) {
-            return $view;
+            $this->setUpAuthenticationFromRequest();
+            return $this->resetNewPasswordForm($userFromHash, $view);
         }
         // Missing or invalid hash
         if (false == $userFromHash) {
@@ -1362,10 +1385,7 @@ class MyResearchController extends AbstractBase
         } elseif ($userFromHash->username !== $post->username) {
             $this->flashMessenger()
                 ->addMessage('authentication_error_invalid', 'error');
-            $userFromHash->updateHash();
-            $view->username = $userFromHash->username;
-            $view->hash = $userFromHash->verify_hash;
-            return $view;
+            return $this->resetNewPasswordForm($userFromHash, $view);
         }
         // Verify old password if we're logged in
         if ($this->getUser()) {
@@ -1452,7 +1472,11 @@ class MyResearchController extends AbstractBase
      */
     protected function setUpAuthenticationFromRequest()
     {
-        $method = trim($this->params()->fromQuery('auth_method'));
+        $method = trim(
+            $this->params()->fromQuery(
+                'auth_method', $this->params()->fromPost('auth_method')
+            )
+        );
         if (!empty($method)) {
             $this->getAuthManager()->setAuthMethod($method);
         }
diff --git a/module/VuFind/src/VuFind/Controller/Pazpar2Controller.php b/module/VuFind/src/VuFind/Controller/Pazpar2Controller.php
index 9a1e62e06599574e441e7cc52f8c20f164e12f48..f6d6fbe938e6340f96e8edaae52ddeda836f1bab 100644
--- a/module/VuFind/src/VuFind/Controller/Pazpar2Controller.php
+++ b/module/VuFind/src/VuFind/Controller/Pazpar2Controller.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Controller
diff --git a/module/VuFind/src/VuFind/Controller/Pazpar2recordController.php b/module/VuFind/src/VuFind/Controller/Pazpar2recordController.php
index 7aab4e97646e5f827fba5ea2f4b786fa4c289ee2..7ef8067c38671e81cd6a321caa6a7a9efcbd1741 100644
--- a/module/VuFind/src/VuFind/Controller/Pazpar2recordController.php
+++ b/module/VuFind/src/VuFind/Controller/Pazpar2recordController.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Controller
diff --git a/module/VuFind/src/VuFind/Controller/Plugin/AbstractRequestBase.php b/module/VuFind/src/VuFind/Controller/Plugin/AbstractRequestBase.php
index a7c67fab69002761cceae32b9272c9aabeb2e9a2..adf71e0df94f901c1c243fb8998e70521212071d 100644
--- a/module/VuFind/src/VuFind/Controller/Plugin/AbstractRequestBase.php
+++ b/module/VuFind/src/VuFind/Controller/Plugin/AbstractRequestBase.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Controller_Plugins
@@ -123,7 +123,7 @@ abstract class AbstractRequestBase extends AbstractPlugin
      *
      * @param array $linkData An array of keys to check
      *
-     * @return boolean|array
+     * @return bool|array
      */
     public function validateRequest($linkData)
     {
diff --git a/module/VuFind/src/VuFind/Controller/Plugin/DbUpgrade.php b/module/VuFind/src/VuFind/Controller/Plugin/DbUpgrade.php
index 662270cdbeca3b54b5966ec118c6a9a23a3b807b..43e594e9c7203b25bbbd6e591ceb7cb43496160e 100644
--- a/module/VuFind/src/VuFind/Controller/Plugin/DbUpgrade.php
+++ b/module/VuFind/src/VuFind/Controller/Plugin/DbUpgrade.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Controller_Plugins
@@ -190,6 +190,89 @@ class DbUpgrade extends AbstractPlugin
         return $retVal;
     }
 
+    /**
+     * Retrieve (and statically cache) table status information.
+     *
+     * @return array
+     */
+    public function getTableStatus()
+    {
+        static $status = false;
+        if (!$status) {
+            $status = $this->getAdapter()
+                ->query('SHOW TABLE STATUS', DbAdapter::QUERY_MODE_EXECUTE)
+                ->toArray();
+        }
+        return $status;
+    }
+
+    /**
+     * Check whether the actual table collation matches the expected table
+     * collation; return false if there is no problem, the name of the desired
+     * collation otherwise.
+     *
+     * @param array $table Information about a table (from getTableStatus())
+     *
+     * @return bool|string
+     */
+    protected function getCollationProblemsForTable($table)
+    {
+        // For now, we'll only detect problems in utf8-encoded tables; if the
+        // user has a Latin1 database, they probably have more complex issues to
+        // work through anyway.
+        preg_match_all(
+            '/CHARSET=utf8 COLLATE (\w+)/', $this->dbCommands[$table['Name']][0],
+            $matches
+        );
+        if (isset($matches[1][0])
+            && strtolower($matches[1][0]) != strtolower($table['Collation'])
+        ) {
+            return $matches[1][0];
+        }
+        return false;
+    }
+
+    /**
+     * Get information on incorrectly collated tables/columns. Return value is
+     * associative array of table name => correct collation value.
+     *
+     * @throws \Exception
+     * @return array
+     */
+    public function getCollationProblems()
+    {
+        // Load details:
+        $retVal = [];
+        foreach ($this->getTableStatus() as $current) {
+            if ($problem = $this->getCollationProblemsForTable($current)) {
+                $retVal[$current['Name']] = $problem;
+            }
+        }
+        return $retVal;
+    }
+
+    /**
+     * Fix collation problems based on the output of getCollationProblems().
+     *
+     * @param array $tables Output of getCollationProblems()
+     * @param bool  $logsql Should we return the SQL as a string rather than
+     * execute it?
+     *
+     * @throws \Exception
+     * @return string       SQL if $logsql is true, empty string otherwise
+     */
+    public function fixCollationProblems($tables, $logsql = false)
+    {
+        $sqlcommands = '';
+        foreach ($tables as $table => $newCollation) {
+            // Adjust default table collation:
+            $sql = "ALTER TABLE `$table` CONVERT TO CHARACTER SET utf8 "
+                . "COLLATE $newCollation;";
+            $sqlcommands .= $this->query($sql, $logsql);
+        }
+        return $sqlcommands;
+    }
+
     /**
      * Get information on incorrectly encoded tables/columns.
      *
@@ -198,16 +281,12 @@ class DbUpgrade extends AbstractPlugin
      */
     public function getEncodingProblems()
     {
-        // Get table summary:
-        $sql = "SHOW TABLE STATUS";
-        $results = $this->getAdapter()->query($sql, DbAdapter::QUERY_MODE_EXECUTE);
-
         // Load details:
         $retVal = [];
-        foreach ($results as $current) {
-            if (strtolower(substr($current->Collation, 0, 6)) == 'latin1') {
-                $retVal[$current->Name]
-                    = $this->getEncodingProblemsForTable($current->Name);
+        foreach ($this->getTableStatus() as $current) {
+            if (strtolower(substr($current['Collation'], 0, 6)) == 'latin1') {
+                $retVal[$current['Name']]
+                    = $this->getEncodingProblemsForTable($current['Name']);
             }
         }
 
diff --git a/module/VuFind/src/VuFind/Controller/Plugin/Factory.php b/module/VuFind/src/VuFind/Controller/Plugin/Factory.php
index 15b2c3c4413bfa804be580f2aa24c95019489286..ba55d930e825b2728bc31b71c8d061987a4f18fd 100644
--- a/module/VuFind/src/VuFind/Controller/Plugin/Factory.php
+++ b/module/VuFind/src/VuFind/Controller/Plugin/Factory.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  View_Helpers
diff --git a/module/VuFind/src/VuFind/Controller/Plugin/Favorites.php b/module/VuFind/src/VuFind/Controller/Plugin/Favorites.php
index cca3d2d2297fdf106b6782bb1be4092dc40d4f0a..5e3e4e5befc609232f4fbc2d267d13e66001d301 100644
--- a/module/VuFind/src/VuFind/Controller/Plugin/Favorites.php
+++ b/module/VuFind/src/VuFind/Controller/Plugin/Favorites.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Controller_Plugins
diff --git a/module/VuFind/src/VuFind/Controller/Plugin/Followup.php b/module/VuFind/src/VuFind/Controller/Plugin/Followup.php
index bc9a0d7505c3d1ac79fd2be9d033f8aa82d08e93..c47d40e6ce44498b15725e1556f5663252e3f340 100644
--- a/module/VuFind/src/VuFind/Controller/Plugin/Followup.php
+++ b/module/VuFind/src/VuFind/Controller/Plugin/Followup.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Controller_Plugins
diff --git a/module/VuFind/src/VuFind/Controller/Plugin/Holds.php b/module/VuFind/src/VuFind/Controller/Plugin/Holds.php
index 65947ee8f5299ead30d1112ff46cfdda042499fc..b8ce7380ec049f8beb95634b7f49c4d0bfe8844a 100644
--- a/module/VuFind/src/VuFind/Controller/Plugin/Holds.php
+++ b/module/VuFind/src/VuFind/Controller/Plugin/Holds.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Controller_Plugins
@@ -58,14 +58,29 @@ class Holds extends AbstractRequestBase
                 // Build OPAC URL
                 $ilsDetails['cancel_link']
                     = $catalog->getCancelHoldLink($ilsDetails);
+            } else if (isset($ilsDetails['cancel_details'])) {
+                // The ILS driver provided cancel details up front. If the
+                // details are an empty string (flagging lack of support), we
+                // should unset it to prevent confusion; otherwise, we'll leave it
+                // as-is.
+                if ('' === $ilsDetails['cancel_details']) {
+                    unset($ilsDetails['cancel_details']);
+                } else {
+                    $this->rememberValidId($ilsDetails['cancel_details']);
+                }
             } else {
-                // Form Details
+                // Default case: ILS supports cancel but we need to look up
+                // details:
                 $cancelDetails = $catalog->getCancelHoldDetails($ilsDetails);
                 if ($cancelDetails !== '') {
                     $ilsDetails['cancel_details'] = $cancelDetails;
                     $this->rememberValidId($ilsDetails['cancel_details']);
                 }
             }
+        } else {
+            // Cancelling holds disabled? Make sure no details get passed back:
+            unset($ilsDetails['cancel_link']);
+            unset($ilsDetails['cancel_details']);
         }
 
         return $ilsDetails;
diff --git a/module/VuFind/src/VuFind/Controller/Plugin/ILLRequests.php b/module/VuFind/src/VuFind/Controller/Plugin/ILLRequests.php
index 75241a8e240f2938096a99f89b281dd93974eb4f..6a6d3172bda810e1ac7e0b87a5fdc7b12861712a 100644
--- a/module/VuFind/src/VuFind/Controller/Plugin/ILLRequests.php
+++ b/module/VuFind/src/VuFind/Controller/Plugin/ILLRequests.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Controller_Plugins
diff --git a/module/VuFind/src/VuFind/Controller/Plugin/NewItems.php b/module/VuFind/src/VuFind/Controller/Plugin/NewItems.php
index c075dbbe566a532632cb8c18ba1b55193fe9936d..ab4f9be985656498049001f1a4b00966c08164b4 100644
--- a/module/VuFind/src/VuFind/Controller/Plugin/NewItems.php
+++ b/module/VuFind/src/VuFind/Controller/Plugin/NewItems.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Controller_Plugins
diff --git a/module/VuFind/src/VuFind/Controller/Plugin/Recaptcha.php b/module/VuFind/src/VuFind/Controller/Plugin/Recaptcha.php
index 347a27ee0c21f3b0b8b8acf40e2ec7b187f66814..b5c15efc686cda999b797b41efb20a8f8a77ec37 100644
--- a/module/VuFind/src/VuFind/Controller/Plugin/Recaptcha.php
+++ b/module/VuFind/src/VuFind/Controller/Plugin/Recaptcha.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Controller_Plugins
@@ -90,7 +90,7 @@ class Recaptcha extends AbstractPlugin
      */
     public function setErrorMode($mode)
     {
-        if (in_array($mode, ['flash', 'throw'])) {
+        if (in_array($mode, ['flash', 'throw', 'none'])) {
             $this->errorMode = $mode;
             return true;
         }
@@ -110,7 +110,7 @@ class Recaptcha extends AbstractPlugin
     /**
      * Pull the captcha field from POST and check them for accuracy
      *
-     * @return boolean
+     * @return bool
      */
     public function validate()
     {
@@ -125,7 +125,7 @@ class Recaptcha extends AbstractPlugin
             $response = false;
         }
         $captchaPassed = $response && $response->isValid();
-        if (!$captchaPassed) {
+        if (!$captchaPassed && $this->errorMode != 'none') {
             if ($this->errorMode == 'flash') {
                 $this->getController()->flashMessenger()
                     ->addMessage('recaptcha_not_passed', 'error');
@@ -141,7 +141,7 @@ class Recaptcha extends AbstractPlugin
      *
      * @param string $domain The specific config term are we checking; ie. "sms"
      *
-     * @return boolean
+     * @return bool
      */
     public function active($domain = false)
     {
diff --git a/module/VuFind/src/VuFind/Controller/Plugin/Renewals.php b/module/VuFind/src/VuFind/Controller/Plugin/Renewals.php
index 4cb7d3359076f4dd884ada2a698d55d6311e5066..3ec476ce84a0a6b8decc3040a4943801dd351d3f 100644
--- a/module/VuFind/src/VuFind/Controller/Plugin/Renewals.php
+++ b/module/VuFind/src/VuFind/Controller/Plugin/Renewals.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Controller_Plugins
diff --git a/module/VuFind/src/VuFind/Controller/Plugin/Reserves.php b/module/VuFind/src/VuFind/Controller/Plugin/Reserves.php
index 3a672ea50bbac5a08af5a2a80b0f18878af585b4..cbd33a53db63d535d4b07f2051bbbc47687b4879 100644
--- a/module/VuFind/src/VuFind/Controller/Plugin/Reserves.php
+++ b/module/VuFind/src/VuFind/Controller/Plugin/Reserves.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Controller_Plugins
diff --git a/module/VuFind/src/VuFind/Controller/Plugin/ResultScroller.php b/module/VuFind/src/VuFind/Controller/Plugin/ResultScroller.php
index fde08a082a25c3b513be7c2218e30627c200ea80..04396779c4da5fd5f9984177fbb06cbfbda16f22 100644
--- a/module/VuFind/src/VuFind/Controller/Plugin/ResultScroller.php
+++ b/module/VuFind/src/VuFind/Controller/Plugin/ResultScroller.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Controller_Plugins
@@ -88,7 +88,10 @@ class ResultScroller extends AbstractPlugin
         $this->data->searchId = $searchObject->getSearchId();
         $this->data->page = $searchObject->getParams()->getPage();
         $this->data->limit = $searchObject->getParams()->getLimit();
+        $this->data->sort = $searchObject->getParams()->getSort();
         $this->data->total = $searchObject->getResultTotal();
+        $this->data->firstlast = $searchObject->getOptions()
+            ->supportsFirstLastNavigation();
 
         // save the IDs of records on the current page to the session
         // so we can "slide" from one record to the next/previous records
@@ -98,6 +101,8 @@ class ResultScroller extends AbstractPlugin
         // clear the previous/next page
         unset($this->data->prevIds);
         unset($this->data->nextIds);
+        unset($this->data->firstId);
+        unset($this->data->lastId);
 
         return (bool)$this->data->currIds;
     }
@@ -286,6 +291,125 @@ class ResultScroller extends AbstractPlugin
         return $retVal;
     }
 
+    /**
+     * Return a modified results array for the case where we need to retrieve data
+     * from the the first page of results
+     *
+     * @param array                       $retVal     Return values (in progress)
+     * @param \VuFind\Search\Base\Results $lastSearch Representation of last search
+     *
+     * @return array
+     */
+    protected function scrollToFirstRecord($retVal, $lastSearch)
+    {
+        // Set page in session to First Page
+        $this->data->page = 1;
+        // update the search URL in the session
+        $lastSearch->getParams()->setPage($this->data->page);
+        $this->rememberSearch($lastSearch);
+
+        // update current, next and prev Ids
+        $this->data->currIds = $this->fetchPage($lastSearch, $this->data->page);
+        $this->data->nextIds = $this->fetchPage($lastSearch, $this->data->page + 1);
+        $this->data->prevIds = null;
+
+        // now we can set the previous/next record
+        $retVal['previousRecord'] = null;
+        $retVal['nextRecord'] = isset($this->data->currIds[1])
+            ? $this->data->currIds[1] : null;
+        // cover extremely unlikely edge case -- page size of 1:
+        if (null === $retVal['nextRecord'] && isset($this->data->nextIds[0])) {
+            $retVal['nextRecord'] = $this->data->nextIds[0];
+        }
+
+        // recalculate the current position
+        $retVal['currentPosition'] = 1;
+
+        // and we're done
+        return $retVal;
+    }
+
+    /**
+     * Return a modified results array for the case where we need to retrieve data
+     * from the the last page of results
+     *
+     * @param array                       $retVal     Return values (in progress)
+     * @param \VuFind\Search\Base\Results $lastSearch Representation of last search
+     *
+     * @return array
+     */
+    protected function scrollToLastRecord($retVal, $lastSearch)
+    {
+        // Set page in session to Last Page
+        $this->data->page = $this->getLastPageNumber();
+        // update the search URL in the session
+        $lastSearch->getParams()->setPage($this->data->page);
+        $this->rememberSearch($lastSearch);
+
+        // update current, next and prev Ids
+        $this->data->currIds = $this->fetchPage($lastSearch, $this->data->page);
+        $this->data->prevIds = $this->fetchPage($lastSearch, $this->data->page - 1);
+        $this->data->nextIds = null;
+
+        // recalculate the current position
+        $retVal['currentPosition'] = $this->data->total;
+
+        // now we can set the previous/next record
+        $retVal['nextRecord'] = null;
+        if (count($this->data->currIds) > 1) {
+            $pos = count($this->data->currIds) - 2;
+            $retVal['previousRecord'] = $this->data->currIds[$pos];
+        } else if (count($this->data->prevIds) > 0) {
+            $prevPos = count($this->data->prevIds) - 1;
+            $retVal['previousRecord'] = $this->data->prevIds[$prevPos];
+        }
+
+        // and we're done
+        return $retVal;
+    }
+
+    /**
+     * Get the ID of the first record in the result set.
+     *
+     * @param \VuFind\Search\Base\Results $lastSearch Representation of last search
+     *
+     * @return string
+     */
+    protected function getFirstRecordId($lastSearch)
+    {
+        if (!isset($this->data->firstId)) {
+            $firstPage = $this->fetchPage($lastSearch, 1);
+            $this->data->firstId = $firstPage[0];
+        }
+        return $this->data->firstId;
+    }
+
+    /**
+     * Calculate the last page number in the result set.
+     *
+     * @return int
+     */
+    protected function getLastPageNumber()
+    {
+        return ceil($this->data->total / $this->data->limit);
+    }
+
+    /**
+     * Get the ID of the last record in the result set.
+     *
+     * @param \VuFind\Search\Base\Results $lastSearch Representation of last search
+     *
+     * @return string
+     */
+    protected function getLastRecordId($lastSearch)
+    {
+        if (!isset($this->data->lastId)) {
+            $results = $this->fetchPage($lastSearch, $this->getLastPageNumber());
+            $this->data->lastId = array_pop($results);
+        }
+        return $this->data->lastId;
+    }
+
     /**
      * Get the previous/next record in the last search
      * result set relative to the current one, also return
@@ -301,6 +425,7 @@ class ResultScroller extends AbstractPlugin
     public function getScrollData($driver)
     {
         $retVal = [
+            'firstRecord' => null, 'lastRecord' => null,
             'previousRecord' => null, 'nextRecord' => null,
             'currentPosition' => null, 'resultTotal' => null
         ];
@@ -322,6 +447,12 @@ class ResultScroller extends AbstractPlugin
             $retVal['resultTotal']
                 = isset($this->data->total) ? $this->data->total : 0;
 
+            // Set first and last record IDs
+            if ($this->data->firstlast) {
+                $retVal['firstRecord'] = $this->getFirstRecordId($lastSearch);
+                $retVal['lastRecord'] = $this->getLastRecordId($lastSearch);
+            }
+
             // build a full ID string using the driver:
             $id = $driver->getSourceIdentifier() . '|' . $driver->getUniqueId();
 
@@ -361,7 +492,6 @@ class ResultScroller extends AbstractPlugin
                             ->scrollToPreviousPage($retVal, $lastSearch, $pos);
                     }
                 }
-
                 // if there is something on the next page
                 if (!empty($this->data->nextIds)) {
                     // check if current record is on the next page
@@ -371,6 +501,14 @@ class ResultScroller extends AbstractPlugin
                         return $this->scrollToNextPage($retVal, $lastSearch, $pos);
                     }
                 }
+                if ($this->data->firstlast) {
+                    if ($id == $retVal['firstRecord']) {
+                        return $this->scrollToFirstRecord($retVal, $lastSearch);
+                    }
+                    if ($id == $retVal['lastRecord']) {
+                        return $this->scrollToLastRecord($retVal, $lastSearch);
+                    }
+                }
             }
         }
         return $retVal;
@@ -387,7 +525,7 @@ class ResultScroller extends AbstractPlugin
      */
     protected function fetchPage($searchObject, $page = null)
     {
-        if (!is_null($page)) {
+        if (null !== $page) {
             $searchObject->getParams()->setPage($page);
             $searchObject->performAndProcessSearch();
         }
@@ -421,6 +559,7 @@ class ResultScroller extends AbstractPlugin
                 // The saved search does not remember its original limit;
                 // we should reapply it from the session data:
                 $search->getParams()->setLimit($this->data->limit);
+                $search->getParams()->setSort($this->data->sort);
                 return $search;
             }
         }
diff --git a/module/VuFind/src/VuFind/Controller/Plugin/StorageRetrievalRequests.php b/module/VuFind/src/VuFind/Controller/Plugin/StorageRetrievalRequests.php
index caf463c3f84fd8a40738b2070337b0343d50946a..c1f1bf1a2bf7fd7dac7e92850a4d01d0590a623d 100644
--- a/module/VuFind/src/VuFind/Controller/Plugin/StorageRetrievalRequests.php
+++ b/module/VuFind/src/VuFind/Controller/Plugin/StorageRetrievalRequests.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Controller_Plugins
diff --git a/module/VuFind/src/VuFind/Controller/PrimoController.php b/module/VuFind/src/VuFind/Controller/PrimoController.php
index 785b64166bca6707fc73942f01e77aa29c1b0627..adfb41dfd3db6f5ebe7b389ea13b16dee8f23f2a 100644
--- a/module/VuFind/src/VuFind/Controller/PrimoController.php
+++ b/module/VuFind/src/VuFind/Controller/PrimoController.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Controller
diff --git a/module/VuFind/src/VuFind/Controller/PrimorecordController.php b/module/VuFind/src/VuFind/Controller/PrimorecordController.php
index d1b8110a5f6bc8028205fbfff47abcf9acdc67b1..a1e53fd789d7e39c6cbd13c9bc88439bc2ca97df 100644
--- a/module/VuFind/src/VuFind/Controller/PrimorecordController.php
+++ b/module/VuFind/src/VuFind/Controller/PrimorecordController.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Controller
diff --git a/module/VuFind/src/VuFind/Controller/RecordController.php b/module/VuFind/src/VuFind/Controller/RecordController.php
index b828da497bb02618254ba9f42d5832b90436543e..05b2c6cebcab1561300c9c45c6261bc4c85752b8 100644
--- a/module/VuFind/src/VuFind/Controller/RecordController.php
+++ b/module/VuFind/src/VuFind/Controller/RecordController.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Controller
diff --git a/module/VuFind/src/VuFind/Controller/RecordsController.php b/module/VuFind/src/VuFind/Controller/RecordsController.php
index 7097cc2da39b93eaaa6efcd1e9a85b98a7089c76..1cdd6d6c222f8f48131aa2d48d58d7a8e2e7881c 100644
--- a/module/VuFind/src/VuFind/Controller/RecordsController.php
+++ b/module/VuFind/src/VuFind/Controller/RecordsController.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Controller
diff --git a/module/VuFind/src/VuFind/Controller/SearchController.php b/module/VuFind/src/VuFind/Controller/SearchController.php
index c995ab221a905a04d81021f51f59c221d7a56059..e32847da14fc7e44c9eae78a375f495f8e6c0891 100644
--- a/module/VuFind/src/VuFind/Controller/SearchController.php
+++ b/module/VuFind/src/VuFind/Controller/SearchController.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Controller
@@ -411,6 +411,17 @@ class SearchController extends AbstractSearch
         );
     }
 
+    /**
+     * Show facet list for Solr-driven reserves.
+     *
+     * @return mixed
+     */
+    public function reservesfacetlistAction()
+    {
+        $this->searchClassId = 'SolrReserves';
+        return $this->facetListAction();
+    }
+
     /**
      * Show search form for Solr-driven reserves.
      *
@@ -507,6 +518,10 @@ class SearchController extends AbstractSearch
             $query->set('type', 'tag');
         }
         if ($this->params()->fromQuery('type') == 'tag') {
+            // Because we're coming in from a search, we want to do a fuzzy
+            // tag search, not an exact search like we would when linking to a
+            // specific tag name.
+            $query = $this->getRequest()->getQuery()->set('fuzzy', 'true');
             return $this->forwardTo('Tag', 'Home');
         }
 
diff --git a/module/VuFind/src/VuFind/Controller/StorageRetrievalRequestsTrait.php b/module/VuFind/src/VuFind/Controller/StorageRetrievalRequestsTrait.php
index facb8243e59e767e70b6de36711090c23f428e90..e76997c3c392f95b7f9d99804079bd36471bcf0d 100644
--- a/module/VuFind/src/VuFind/Controller/StorageRetrievalRequestsTrait.php
+++ b/module/VuFind/src/VuFind/Controller/StorageRetrievalRequestsTrait.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Controller
diff --git a/module/VuFind/src/VuFind/Controller/SummonController.php b/module/VuFind/src/VuFind/Controller/SummonController.php
index f8294fb5bcdfd0757abe1a640540ae119cb17030..95fbb8c9875f5a725bd6df7abf7e0c4b559e3739 100644
--- a/module/VuFind/src/VuFind/Controller/SummonController.php
+++ b/module/VuFind/src/VuFind/Controller/SummonController.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Controller
diff --git a/module/VuFind/src/VuFind/Controller/SummonrecordController.php b/module/VuFind/src/VuFind/Controller/SummonrecordController.php
index 7d5bc02f4216f47af1f920afacf349d89191cb2b..f173a437134c2410acc1d7978a21bf399bf6d609 100644
--- a/module/VuFind/src/VuFind/Controller/SummonrecordController.php
+++ b/module/VuFind/src/VuFind/Controller/SummonrecordController.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Controller
diff --git a/module/VuFind/src/VuFind/Controller/TagController.php b/module/VuFind/src/VuFind/Controller/TagController.php
index e12730e24bec776d7093737158df874ebd2ccb5d..a4a8d2b6e83e686b2f4462ee8824a2a166a9ba7f 100644
--- a/module/VuFind/src/VuFind/Controller/TagController.php
+++ b/module/VuFind/src/VuFind/Controller/TagController.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Controller
@@ -26,6 +26,7 @@
  * @link     https://vufind.org Main Site
  */
 namespace VuFind\Controller;
+use VuFind\Exception\Forbidden as ForbiddenException;
 
 /**
  * Tag Controller
@@ -55,7 +56,7 @@ class TagController extends AbstractSearch
     public function homeAction()
     {
         if (!$this->tagsEnabled()) {
-            throw new \Exception('Tags disabled');
+            throw new ForbiddenException('Tags disabled');
         }
         return $this->resultsAction();
     }
diff --git a/module/VuFind/src/VuFind/Controller/UpgradeController.php b/module/VuFind/src/VuFind/Controller/UpgradeController.php
index b9ad30bb4ec9e0a6594893790143924ccd35fdc0..eb2a7a5db3e3d062dd5b387af136386c0d1719bc 100644
--- a/module/VuFind/src/VuFind/Controller/UpgradeController.php
+++ b/module/VuFind/src/VuFind/Controller/UpgradeController.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Controller
@@ -324,62 +324,89 @@ class UpgradeController extends AbstractBase
     }
 
     /**
-     * Upgrade the database.
+     * Attempt to perform a MySQL upgrade; return either a string containing SQL
+     * (if we are in "log SQL" mode), an empty string (if we are successful but
+     * not logging SQL) or a Zend Framework object representing forward/redirect
+     * (if we need to obtain user input).
+     *
+     * @param \Zend\Db\Adapter\Adapter $adapter Database adapter
      *
      * @return mixed
      */
-    public function fixdatabaseAction()
+    protected function upgradeMySQL($adapter)
     {
         $sql = '';
 
-        try {
-            // Set up the helper with information from our SQL file:
-            $this->dbUpgrade()
-                ->setAdapter($this->getServiceLocator()->get('VuFind\DbAdapter'))
-                ->loadSql(APPLICATION_PATH . '/module/VuFind/sql/mysql.sql');
-
-            // Check for missing tables.  Note that we need to finish dealing with
-            // missing tables before we proceed to the missing columns check, or else
-            // the missing tables will cause fatal errors during the column test.
-            $missingTables = $this->dbUpgrade()->getMissingTables();
-            if (!empty($missingTables)) {
-                // Only manipulate DB if we're not in logging mode:
-                if (!$this->logsql) {
-                    if (!$this->hasDatabaseRootCredentials()) {
-                        return $this->forwardTo('Upgrade', 'GetDbCredentials');
-                    }
-                    $this->dbUpgrade()->setAdapter($this->getRootDbAdapter());
-                    $this->session->warnings->append(
-                        "Created missing table(s): " . implode(', ', $missingTables)
-                    );
+        // Set up the helper with information from our SQL file:
+        $this->dbUpgrade()
+            ->setAdapter($adapter)
+            ->loadSql(APPLICATION_PATH . '/module/VuFind/sql/mysql.sql');
+
+        // Check for missing tables.  Note that we need to finish dealing with
+        // missing tables before we proceed to the missing columns check, or else
+        // the missing tables will cause fatal errors during the column test.
+        $missingTables = $this->dbUpgrade()->getMissingTables();
+        if (!empty($missingTables)) {
+            // Only manipulate DB if we're not in logging mode:
+            if (!$this->logsql) {
+                if (!$this->hasDatabaseRootCredentials()) {
+                    return $this->forwardTo('Upgrade', 'GetDbCredentials');
                 }
-                $sql .= $this->dbUpgrade()
-                    ->createMissingTables($missingTables, $this->logsql);
+                $this->dbUpgrade()->setAdapter($this->getRootDbAdapter());
+                $this->session->warnings->append(
+                    "Created missing table(s): " . implode(', ', $missingTables)
+                );
             }
-
-            // Check for missing columns.
-            $mT = $this->logsql ? $missingTables : [];
-            $missingCols = $this->dbUpgrade()->getMissingColumns($mT);
-            if (!empty($missingCols)) {
-                // Only manipulate DB if we're not in logging mode:
-                if (!$this->logsql) {
-                    if (!$this->hasDatabaseRootCredentials()) {
-                        return $this->forwardTo('Upgrade', 'GetDbCredentials');
-                    }
-                    $this->dbUpgrade()->setAdapter($this->getRootDbAdapter());
-                    $this->session->warnings->append(
-                        "Added column(s) to table(s): "
-                        . implode(', ', array_keys($missingCols))
-                    );
+            $sql .= $this->dbUpgrade()
+                ->createMissingTables($missingTables, $this->logsql);
+        }
+
+        // Check for missing columns.
+        $mT = $this->logsql ? $missingTables : [];
+        $missingCols = $this->dbUpgrade()->getMissingColumns($mT);
+        if (!empty($missingCols)) {
+            // Only manipulate DB if we're not in logging mode:
+            if (!$this->logsql) {
+                if (!$this->hasDatabaseRootCredentials()) {
+                    return $this->forwardTo('Upgrade', 'GetDbCredentials');
                 }
-                $sql .= $this->dbUpgrade()
-                    ->createMissingColumns($missingCols, $this->logsql);
+                $this->dbUpgrade()->setAdapter($this->getRootDbAdapter());
+                $this->session->warnings->append(
+                    "Added column(s) to table(s): "
+                    . implode(', ', array_keys($missingCols))
+                );
+            }
+            $sql .= $this->dbUpgrade()
+                ->createMissingColumns($missingCols, $this->logsql);
+        }
+
+        // Check for modified columns.
+        $mC = $this->logsql ? $missingCols : [];
+        $modifiedCols = $this->dbUpgrade()->getModifiedColumns($mT, $mC);
+        if (!empty($modifiedCols)) {
+            // Only manipulate DB if we're not in logging mode:
+            if (!$this->logsql) {
+                if (!$this->hasDatabaseRootCredentials()) {
+                    return $this->forwardTo('Upgrade', 'GetDbCredentials');
+                }
+                $this->dbUpgrade()->setAdapter($this->getRootDbAdapter());
+                $this->session->warnings->append(
+                    "Modified column(s) in table(s): "
+                    . implode(', ', array_keys($modifiedCols))
+                );
             }
+            $sql .= $this->dbUpgrade()
+                ->updateModifiedColumns($modifiedCols, $this->logsql);
+        }
 
-            // Check for modified columns.
-            $mC = $this->logsql ? $missingCols : [];
-            $modifiedCols = $this->dbUpgrade()->getModifiedColumns($mT, $mC);
-            if (!empty($modifiedCols)) {
+        // Check for encoding problems.
+        $encProblems = $this->dbUpgrade()->getEncodingProblems();
+        if (!empty($encProblems)) {
+            if (!isset($this->session->dbChangeEncoding)) {
+                return $this->forwardTo('Upgrade', 'GetDbEncodingPreference');
+            }
+
+            if ($this->session->dbChangeEncoding) {
                 // Only manipulate DB if we're not in logging mode:
                 if (!$this->logsql) {
                     if (!$this->hasDatabaseRootCredentials()) {
@@ -387,72 +414,91 @@ class UpgradeController extends AbstractBase
                     }
                     $this->dbUpgrade()->setAdapter($this->getRootDbAdapter());
                     $this->session->warnings->append(
-                        "Modified column(s) in table(s): "
-                        . implode(', ', array_keys($modifiedCols))
+                        "Modified encoding settings in table(s): "
+                        . implode(', ', array_keys($encProblems))
                     );
                 }
                 $sql .= $this->dbUpgrade()
-                    ->updateModifiedColumns($modifiedCols, $this->logsql);
+                    ->fixEncodingProblems($encProblems, $this->logsql);
+                $this->setDbEncodingConfiguration('utf8');
+            } else {
+                // User has requested that we skip encoding conversion:
+                $this->setDbEncodingConfiguration('latin1');
             }
+        }
 
-            // Check for encoding problems.
-            $encProblems = $this->dbUpgrade()->getEncodingProblems();
-            if (!empty($encProblems)) {
-                if (!isset($this->session->dbChangeEncoding)) {
-                    return $this->forwardTo('Upgrade', 'GetDbEncodingPreference');
+        // Check for collation problems.
+        $colProblems = $this->dbUpgrade()->getCollationProblems();
+        if (!empty($colProblems)) {
+            if (!$this->logsql) {
+                if (!$this->hasDatabaseRootCredentials()) {
+                    return $this->forwardTo('Upgrade', 'GetDbCredentials');
                 }
+                $this->dbUpgrade()->setAdapter($this->getRootDbAdapter());
+            }
+            $sql .= $this->dbUpgrade()
+                ->fixCollationProblems($colProblems, $this->logsql);
+            $this->session->warnings->append(
+                "Modified collation(s) in table(s): "
+                . implode(', ', array_keys($colProblems))
+            );
+        }
+
+        // Don't keep DB credentials in session longer than necessary:
+        unset($this->session->dbRootUser);
+        unset($this->session->dbRootPass);
 
-                if ($this->session->dbChangeEncoding) {
-                    // Only manipulate DB if we're not in logging mode:
-                    if (!$this->logsql) {
-                        if (!$this->hasDatabaseRootCredentials()) {
-                            return $this->forwardTo('Upgrade', 'GetDbCredentials');
-                        }
-                        $this->dbUpgrade()->setAdapter($this->getRootDbAdapter());
-                        $this->session->warnings->append(
-                            "Modified encoding settings in table(s): "
-                            . implode(', ', array_keys($encProblems))
-                        );
+        return $sql;
+    }
+
+    /**
+     * Upgrade the database.
+     *
+     * @return mixed
+     */
+    public function fixdatabaseAction()
+    {
+        try {
+            // If we haven't already tried it, attempt a structure update:
+            if (!isset($this->session->sql)) {
+                // If this is a MySQL connection, we can do an automatic upgrade;
+                // if VuFind is using a different database, we have to prompt the
+                // user to check the migrations directory and upgrade manually.
+                $adapter = $this->getServiceLocator()->get('VuFind\DbAdapter');
+                $platform = $adapter->getDriver()->getDatabasePlatformName();
+                if (strtolower($platform) == 'mysql') {
+                    $upgradeResult = $this->upgradeMySQL($adapter);
+                    if (!is_string($upgradeResult)) {
+                        return $upgradeResult;
                     }
-                    $sql .= $this->dbUpgrade()
-                        ->fixEncodingProblems($encProblems, $this->logsql);
-                    $this->setDbEncodingConfiguration('utf8');
+                    $this->session->sql = $upgradeResult;
                 } else {
-                    // User has requested that we skip encoding conversion:
-                    $this->setDbEncodingConfiguration('latin1');
+                    $this->session->sql = '';
+                    $this->session->warnings->append(
+                        'Automatic database upgrade not supported for ' . $platform
+                        . '. Check for manual migration scripts in the '
+                        . '$VUFIND_HOME/module/VuFind/sql/migrations directory.'
+                    );
                 }
             }
 
-            // Don't keep DB credentials in session longer than necessary:
-            unset($this->session->dbRootUser);
-            unset($this->session->dbRootPass);
+            // Now that database structure is addressed, we can fix database
+            // content -- the checks below should be platform-independent.
 
             // Check for legacy tag bugs:
             $resourceTagsTable = $this->getTable('ResourceTags');
             $anonymousTags = $resourceTagsTable->getAnonymousCount();
             if ($anonymousTags > 0 && !isset($this->cookie->skipAnonymousTags)) {
                 $this->getRequest()->getQuery()->set('anonymousCnt', $anonymousTags);
-                return $this->forwardTo('Upgrade', 'FixAnonymousTags');
+                return $this->redirect()->toRoute('upgrade-fixanonymoustags');
             }
             $dupeTags = $this->getTable('Tags')->getDuplicates();
             if (count($dupeTags) > 0 && !isset($this->cookie->skipDupeTags)) {
-                return $this->forwardTo('Upgrade', 'FixDuplicateTags');
+                return $this->redirect()->toRoute('upgrade-fixduplicatetags');
             }
 
             // Clean up the "VuFind" source, if necessary.
             $this->fixVuFindSourceInDatabase();
-
-            // Add checksums to all saved searches but catch exceptions (e.g. in case
-            // column checksum does not exist yet because of sqllog).
-            try {
-                $this->fixSearchChecksumsInDatabase();
-            } catch (\Exception $e) {
-                $this->session->warnings->append(
-                    'Could not fix checksums in table search - maybe column ' .
-                    'checksum is missing? Exception thrown with ' .
-                    'message: ' . $e->getMessage()
-                );
-            }
         } catch (\Exception $e) {
             $this->flashMessenger()->addMessage(
                 'Database upgrade failed: ' . $e->getMessage(), 'error'
@@ -460,12 +506,23 @@ class UpgradeController extends AbstractBase
             return $this->forwardTo('Upgrade', 'Error');
         }
 
+        // Add checksums to all saved searches but catch exceptions (e.g. in case
+        // column checksum does not exist yet because of sqllog).
+        try {
+            $this->fixSearchChecksumsInDatabase();
+        } catch (\Exception $e) {
+            $this->session->warnings->append(
+                'Could not fix checksums in table search - maybe column ' .
+                'checksum is missing? Exception thrown with ' .
+                'message: ' . $e->getMessage()
+            );
+        }
+
         $this->cookie->databaseOkay = true;
-        if ($this->logsql) {
-            $this->session->sql = $sql;
+        if (!empty($this->session->sql)) {
             return $this->forwardTo('Upgrade', 'ShowSql');
         }
-        return $this->forwardTo('Upgrade', 'Home');
+        return $this->redirect()->toRoute('upgrade-home');
     }
 
     /**
@@ -478,7 +535,7 @@ class UpgradeController extends AbstractBase
         $continue = $this->params()->fromPost('continue', 'nope');
         if ($continue == 'Next') {
             unset($this->session->sql);
-            return $this->forwardTo('Upgrade', 'Home');
+            return $this->redirect()->toRoute('upgrade-home');
         }
 
         return $this->createViewModel(['sql' => $this->session->sql]);
diff --git a/module/VuFind/src/VuFind/Controller/WebController.php b/module/VuFind/src/VuFind/Controller/WebController.php
index fbce09b498c19c19b36ca0f98d27f30c3cb87fa4..11089c3ce22fa26c172bb2d67cafe86906db9379 100644
--- a/module/VuFind/src/VuFind/Controller/WebController.php
+++ b/module/VuFind/src/VuFind/Controller/WebController.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Controller
diff --git a/module/VuFind/src/VuFind/Controller/Widget/Solr/FacetInterface.php b/module/VuFind/src/VuFind/Controller/Widget/Solr/FacetInterface.php
index d244b4dbd88c40fcbe9e842bf0bca8a2e4325a16..efed73c8592e0c1c6d07a7ab4fdee1a4f9936305 100644
--- a/module/VuFind/src/VuFind/Controller/Widget/Solr/FacetInterface.php
+++ b/module/VuFind/src/VuFind/Controller/Widget/Solr/FacetInterface.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Controller
diff --git a/module/VuFind/src/VuFind/Controller/Widget/WidgetInterface.php b/module/VuFind/src/VuFind/Controller/Widget/WidgetInterface.php
index 91045cb9ab3098510a34e19358e219a8bf3ba335..e2a17aa3b619742547317fd54dadbb00500cef93 100644
--- a/module/VuFind/src/VuFind/Controller/Widget/WidgetInterface.php
+++ b/module/VuFind/src/VuFind/Controller/Widget/WidgetInterface.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Controller
diff --git a/module/VuFind/src/VuFind/Controller/WorldcatController.php b/module/VuFind/src/VuFind/Controller/WorldcatController.php
index 7aafc0abaa17ef742bca090811087bfdf8b8f9f6..86dc102675bfe6b4a743df165af5e092fb6cded0 100644
--- a/module/VuFind/src/VuFind/Controller/WorldcatController.php
+++ b/module/VuFind/src/VuFind/Controller/WorldcatController.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Controller
diff --git a/module/VuFind/src/VuFind/Controller/WorldcatrecordController.php b/module/VuFind/src/VuFind/Controller/WorldcatrecordController.php
index b6cfaad46956ffe833862f8acd44af78a8004904..a3249ac11e4e5e754c7c5a622e74903b04106acf 100644
--- a/module/VuFind/src/VuFind/Controller/WorldcatrecordController.php
+++ b/module/VuFind/src/VuFind/Controller/WorldcatrecordController.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Controller
diff --git a/module/VuFind/src/VuFind/Cookie/Container.php b/module/VuFind/src/VuFind/Cookie/Container.php
index cf456b2feb510d4c39b75fb273e980225fa9d6c7..5de312f2a63a077cf14123c49252d4f4030cd39b 100644
--- a/module/VuFind/src/VuFind/Cookie/Container.php
+++ b/module/VuFind/src/VuFind/Cookie/Container.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Cookie
diff --git a/module/VuFind/src/VuFind/Cookie/CookieManager.php b/module/VuFind/src/VuFind/Cookie/CookieManager.php
index d9f5e2191cc50fbbd9b31bd6b97d0a9a39b91c63..05a9d0008f0e29e2b4e854eef0a32dd7dd697ccc 100644
--- a/module/VuFind/src/VuFind/Cookie/CookieManager.php
+++ b/module/VuFind/src/VuFind/Cookie/CookieManager.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Cookie
diff --git a/module/VuFind/src/VuFind/Cover/CachingProxy.php b/module/VuFind/src/VuFind/Cover/CachingProxy.php
index 8611dac0fe6d100b7811b91ea0af9f7483b20315..6c2c8255b2cfcac3ba6900c356ca3ff0df44275c 100644
--- a/module/VuFind/src/VuFind/Cover/CachingProxy.php
+++ b/module/VuFind/src/VuFind/Cover/CachingProxy.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Cover_Generator
diff --git a/module/VuFind/src/VuFind/Cover/Generator.php b/module/VuFind/src/VuFind/Cover/Generator.php
index aa859f70473a889208fdd09d35e7baa083d8dbd8..cb6d76b62322884cdaa0e5e0d4ac402afb2b04eb 100644
--- a/module/VuFind/src/VuFind/Cover/Generator.php
+++ b/module/VuFind/src/VuFind/Cover/Generator.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Cover_Generator
diff --git a/module/VuFind/src/VuFind/Cover/Loader.php b/module/VuFind/src/VuFind/Cover/Loader.php
index 0f788a794abac88707007ebf7b48d625437236f1..14f549b5d11e9e3b8717aab261eacb4ae053af8d 100644
--- a/module/VuFind/src/VuFind/Cover/Loader.php
+++ b/module/VuFind/src/VuFind/Cover/Loader.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Cover_Generator
diff --git a/module/VuFind/src/VuFind/Cover/Router.php b/module/VuFind/src/VuFind/Cover/Router.php
new file mode 100644
index 0000000000000000000000000000000000000000..b9f7293a66a2e8964a1edcb1e24a22d6de9a7880
--- /dev/null
+++ b/module/VuFind/src/VuFind/Cover/Router.php
@@ -0,0 +1,86 @@
+<?php
+/**
+ * Cover image router
+ *
+ * PHP version 5
+ *
+ * Copyright (C) Villanova University 2016.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ * @category VuFind
+ * @package  Cover_Generator
+ * @author   Demian Katz <demian.katz@villanova.edu>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     https://vufind.org/wiki/configuration:external_content Wiki
+ */
+namespace VuFind\Cover;
+use VuFind\RecordDriver\AbstractBase as RecordDriver;
+
+/**
+ * Cover image router
+ *
+ * @category VuFind
+ * @package  Cover_Generator
+ * @author   Demian Katz <demian.katz@villanova.edu>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     https://vufind.org/wiki/configuration:external_content Wiki
+ */
+class Router
+{
+    /**
+     * Base URL for dynamic cover images.
+     *
+     * @var string
+     */
+    protected $dynamicUrl;
+
+    /**
+     * Constructor
+     *
+     * @param string $url Base URL for dynamic cover images.
+     */
+    public function __construct($url)
+    {
+        $this->dynamicUrl = $url;
+    }
+
+    /**
+     * Generate a thumbnail URL (return false if unsupported).
+     *
+     * @param RecordDriver $driver Record driver
+     * @param string       $size   Size of thumbnail (small, medium or large --
+     * small is default).
+     *
+     * @return string|bool
+     */
+    public function getUrl(RecordDriver $driver, $size = 'small')
+    {
+        // Try to build thumbnail:
+        $thumb = $driver->tryMethod('getThumbnail', [$size]);
+
+        // No thumbnail?  Return false:
+        if (empty($thumb)) {
+            return false;
+        }
+
+        // Array?  It's parameters to send to the cover generator:
+        if (is_array($thumb)) {
+            return $this->dynamicUrl . '?' . http_build_query($thumb);
+        }
+
+        // Default case -- return fixed string:
+        return $thumb;
+    }
+}
diff --git a/module/VuFind/src/VuFind/Crypt/HMAC.php b/module/VuFind/src/VuFind/Crypt/HMAC.php
index cc087d41c9474fe4961e8be6b7235d6bc3f58802..13936143f8c606faf2d47078c6392ce05ec03d83 100644
--- a/module/VuFind/src/VuFind/Crypt/HMAC.php
+++ b/module/VuFind/src/VuFind/Crypt/HMAC.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Crypt
diff --git a/module/VuFind/src/VuFind/Crypt/RC4.php b/module/VuFind/src/VuFind/Crypt/RC4.php
index 31b1d08475b0dd5cc83012124bfa0dedb1283e7f..bf500bb54abebc154ee347ab09f65fea2f606118 100644
--- a/module/VuFind/src/VuFind/Crypt/RC4.php
+++ b/module/VuFind/src/VuFind/Crypt/RC4.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Crypt
diff --git a/module/VuFind/src/VuFind/Date/Converter.php b/module/VuFind/src/VuFind/Date/Converter.php
index 8ba15b5730df8e7185ff035b9539cec36cf0ddf6..dbd01ddb97930e75aabe37f75333ed59d1d690f9 100644
--- a/module/VuFind/src/VuFind/Date/Converter.php
+++ b/module/VuFind/src/VuFind/Date/Converter.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Date
diff --git a/module/VuFind/src/VuFind/Db/AdapterFactory.php b/module/VuFind/src/VuFind/Db/AdapterFactory.php
index bad40183d4eb6e30aae24348e7badb1ab1f82581..edf0afd76263ddf3869d9122c9b4f9bb93941379 100644
--- a/module/VuFind/src/VuFind/Db/AdapterFactory.php
+++ b/module/VuFind/src/VuFind/Db/AdapterFactory.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Db
diff --git a/module/VuFind/src/VuFind/Db/Row/ChangeTracker.php b/module/VuFind/src/VuFind/Db/Row/ChangeTracker.php
index 7db79e418152fc1fdde289ddba5ac1478b306be5..4bcc1e1898ab6ccbc9a9e764682740bbaa1af26c 100644
--- a/module/VuFind/src/VuFind/Db/Row/ChangeTracker.php
+++ b/module/VuFind/src/VuFind/Db/Row/ChangeTracker.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Db_Row
diff --git a/module/VuFind/src/VuFind/Db/Row/Comments.php b/module/VuFind/src/VuFind/Db/Row/Comments.php
index 47023c65b336dc4da3ff3b34ae207c24d6e11d10..97ebc8055951b2723abb904ecfd75e634b6f165b 100644
--- a/module/VuFind/src/VuFind/Db/Row/Comments.php
+++ b/module/VuFind/src/VuFind/Db/Row/Comments.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Db_Row
diff --git a/module/VuFind/src/VuFind/Db/Row/OaiResumption.php b/module/VuFind/src/VuFind/Db/Row/OaiResumption.php
index bfab79d54cc1cc08f03dec339bda2e77a41d9483..7fac85d9ece6c8a643a2e5b25e4619ad79043b9f 100644
--- a/module/VuFind/src/VuFind/Db/Row/OaiResumption.php
+++ b/module/VuFind/src/VuFind/Db/Row/OaiResumption.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Db_Row
diff --git a/module/VuFind/src/VuFind/Db/Row/PrivateUser.php b/module/VuFind/src/VuFind/Db/Row/PrivateUser.php
index 40034e3d9f9a08a32b3550cf3eee6c9b0b9f7c3a..9c2a634b38f94e5c0c789fe7cdec5583a26b490a 100644
--- a/module/VuFind/src/VuFind/Db/Row/PrivateUser.php
+++ b/module/VuFind/src/VuFind/Db/Row/PrivateUser.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Db_Row
diff --git a/module/VuFind/src/VuFind/Db/Row/Record.php b/module/VuFind/src/VuFind/Db/Row/Record.php
index 87dddec83b0f97abe1115069c12a65684929e08e..a4b91fde65a1cfd804fb100ad307dc1af0604431 100644
--- a/module/VuFind/src/VuFind/Db/Row/Record.php
+++ b/module/VuFind/src/VuFind/Db/Row/Record.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Db_Row
diff --git a/module/VuFind/src/VuFind/Db/Row/Resource.php b/module/VuFind/src/VuFind/Db/Row/Resource.php
index fcedd6c33622db297559e7360761ab1df76794eb..8d936d65ca43d4a52c256ace3e632e6f26774d4c 100644
--- a/module/VuFind/src/VuFind/Db/Row/Resource.php
+++ b/module/VuFind/src/VuFind/Db/Row/Resource.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Db_Row
@@ -95,8 +95,8 @@ class Resource extends RowGateway implements \VuFind\Db\Table\DbTableAwareInterf
     /**
      * Remove a tag from the current resource.
      *
-     * @param string              $tagText The tag to save.
-     * @param \VuFind\Db\Row\User $user    The user posting the tag.
+     * @param string              $tagText The tag to delete.
+     * @param \VuFind\Db\Row\User $user    The user deleting the tag.
      * @param string              $list_id The list associated with the tag
      * (optional).
      *
@@ -107,12 +107,16 @@ class Resource extends RowGateway implements \VuFind\Db\Table\DbTableAwareInterf
         $tagText = trim($tagText);
         if (!empty($tagText)) {
             $tags = $this->getDbTable('Tags');
-            $tag = $tags->getByText($tagText);
-
-            $linker = $this->getDbTable('ResourceTags');
-            $linker->destroyLinks(
-                $this->id, $user->id, $list_id, $tag->id
-            );
+            $tagIds = [];
+            foreach ($tags->getByText($tagText, false, false) as $tag) {
+                $tagIds[] = $tag->id;
+            }
+            if (!empty($tagIds)) {
+                $linker = $this->getDbTable('ResourceTags');
+                $linker->destroyLinks(
+                    $this->id, $user->id, $list_id, $tagIds
+                );
+            }
         }
     }
 
diff --git a/module/VuFind/src/VuFind/Db/Row/ResourceTags.php b/module/VuFind/src/VuFind/Db/Row/ResourceTags.php
index eec3b7c70533fa7ac45c8ab490a74f91202d24eb..c0693cb47a9c3f79ea82c7b73688df03cbb44667 100644
--- a/module/VuFind/src/VuFind/Db/Row/ResourceTags.php
+++ b/module/VuFind/src/VuFind/Db/Row/ResourceTags.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Db_Row
diff --git a/module/VuFind/src/VuFind/Db/Row/RowGateway.php b/module/VuFind/src/VuFind/Db/Row/RowGateway.php
index e04dcc9a6c0fdc28ab84318cf550eaea0e11e39e..dc66a6a24b7ac8960ba73e649e94af92bb33fcab 100644
--- a/module/VuFind/src/VuFind/Db/Row/RowGateway.php
+++ b/module/VuFind/src/VuFind/Db/Row/RowGateway.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Db_Row
diff --git a/module/VuFind/src/VuFind/Db/Row/Search.php b/module/VuFind/src/VuFind/Db/Row/Search.php
index 9ade445b405a4928d19c5f766c47e0b6ff634304..51c43a4c0bcd9ed88da1f1920c5844bd5fdc1f65 100644
--- a/module/VuFind/src/VuFind/Db/Row/Search.php
+++ b/module/VuFind/src/VuFind/Db/Row/Search.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Db_Row
diff --git a/module/VuFind/src/VuFind/Db/Row/Session.php b/module/VuFind/src/VuFind/Db/Row/Session.php
index 8a30fdc49b4a96fb8a52bbb00d4c1690915c9f7d..1fb9a4c39bc1c40a884c993eb0a8991d333ec466 100644
--- a/module/VuFind/src/VuFind/Db/Row/Session.php
+++ b/module/VuFind/src/VuFind/Db/Row/Session.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Db_Row
diff --git a/module/VuFind/src/VuFind/Db/Row/Tags.php b/module/VuFind/src/VuFind/Db/Row/Tags.php
index 0566ae054c29d3b73a1bf8c67c5c40bf1b019517..4fb70da2cc00e5162d3ec37d274148b1a23412fa 100644
--- a/module/VuFind/src/VuFind/Db/Row/Tags.php
+++ b/module/VuFind/src/VuFind/Db/Row/Tags.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Db_Row
@@ -93,7 +93,7 @@ class Tags extends RowGateway implements \VuFind\Db\Table\DbTableAwareInterface
             if ($offset > 0) {
                 $select->offset($offset);
             }
-            if (!is_null($limit)) {
+            if (null !== $limit) {
                 $select->limit($limit);
             }
         };
diff --git a/module/VuFind/src/VuFind/Db/Row/User.php b/module/VuFind/src/VuFind/Db/Row/User.php
index 5519daff5549f9e91798cc4a37454df419b1985e..d72c030e31a4d77119c739614d651912839c68b8 100644
--- a/module/VuFind/src/VuFind/Db/Row/User.php
+++ b/module/VuFind/src/VuFind/Db/Row/User.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Db_Row
@@ -140,9 +140,11 @@ class User extends RowGateway implements \VuFind\Db\Table\DbTableAwareInterface,
      */
     public function getCatPassword()
     {
-        return $this->passwordEncryptionEnabled()
-            ? $this->encryptOrDecrypt($this->cat_pass_enc, false)
-            : (isset($this->cat_password) ? $this->cat_password : null);
+        if ($this->passwordEncryptionEnabled()) {
+            return isset($this->cat_pass_enc)
+                ? $this->encryptOrDecrypt($this->cat_pass_enc, false) : null;
+        }
+        return isset($this->cat_password) ? $this->cat_password : null;
     }
 
     /**
@@ -229,51 +231,8 @@ class User extends RowGateway implements \VuFind\Db\Table\DbTableAwareInterface,
     public function getTags($resourceId = null, $listId = null,
         $source = DEFAULT_SEARCH_BACKEND
     ) {
-        $userId = $this->id;
-        $callback = function ($select) use ($userId, $resourceId, $listId, $source) {
-            $select->columns(
-                [
-                    'id' => new Expression(
-                        'min(?)', ['tags.id'],
-                        [Expression::TYPE_IDENTIFIER]
-                    ),
-                    'tag',
-                    'cnt' => new Expression(
-                        'COUNT(DISTINCT(?))', ['rt.resource_id'],
-                        [Expression::TYPE_IDENTIFIER]
-                    )
-                ]
-            );
-            $select->join(
-                ['rt' => 'resource_tags'], 'tags.id = rt.tag_id', []
-            );
-            $select->join(
-                ['r' => 'resource'], 'rt.resource_id = r.id', []
-            );
-            $select->join(
-                ['ur' => 'user_resource'], 'r.id = ur.resource_id', []
-            );
-            $select->group(['tag'])
-                ->order(['tag']);
-
-            $select->where->equalTo('ur.user_id', $userId)
-                ->equalTo('rt.user_id', $userId)
-                ->equalTo(
-                    'ur.list_id', 'rt.list_id',
-                    Predicate::TYPE_IDENTIFIER, Predicate::TYPE_IDENTIFIER
-                )
-                ->equalTo('r.source', $source);
-
-            if (!is_null($resourceId)) {
-                $select->where->equalTo('r.record_id', $resourceId);
-            }
-            if (!is_null($listId)) {
-                $select->where->equalTo('rt.list_id', $listId);
-            }
-        };
-
-        $table = $this->getDbTable('Tags');
-        return $table->select($callback);
+        return $this->getDbTable('Tags')
+            ->getForUser($this->id, $resourceId, $listId, $source);
     }
 
     /**
@@ -653,6 +612,8 @@ class User extends RowGateway implements \VuFind\Db\Table\DbTableAwareInterface,
             $list = $table->getExisting($current->id);
             $list->delete($this, true);
         }
+        $resourceTags = $this->getDbTable('ResourceTags');
+        $resourceTags->destroyLinks(null, $this->id);
 
         // Remove the user itself:
         return parent::delete();
diff --git a/module/VuFind/src/VuFind/Db/Row/UserCard.php b/module/VuFind/src/VuFind/Db/Row/UserCard.php
index bac9471ab39f5aab08d2239f2e0c6ad24e8d55ed..56c4daff2afaa378676b407ffcc2013a6d6a4f5e 100644
--- a/module/VuFind/src/VuFind/Db/Row/UserCard.php
+++ b/module/VuFind/src/VuFind/Db/Row/UserCard.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Db_Row
diff --git a/module/VuFind/src/VuFind/Db/Row/UserList.php b/module/VuFind/src/VuFind/Db/Row/UserList.php
index c25aeaed17762dbb91d5ff89c64ee7fd422a05d2..ea1ba710352d0f3a0647267f761ad4aa1122cdad 100644
--- a/module/VuFind/src/VuFind/Db/Row/UserList.php
+++ b/module/VuFind/src/VuFind/Db/Row/UserList.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Db_Row
diff --git a/module/VuFind/src/VuFind/Db/Row/UserResource.php b/module/VuFind/src/VuFind/Db/Row/UserResource.php
index f05832128928c8062c525c2b2ef8607d0ce12a5d..03546b0d44b31e554370d83aaa7a124d5e39c1e9 100644
--- a/module/VuFind/src/VuFind/Db/Row/UserResource.php
+++ b/module/VuFind/src/VuFind/Db/Row/UserResource.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Db_Row
diff --git a/module/VuFind/src/VuFind/Db/Table/ChangeTracker.php b/module/VuFind/src/VuFind/Db/Table/ChangeTracker.php
index 91561fa9b7250d1be18de9deb70db4f34b48674a..f0eae0978e78b7e179291f19ad8eb0d17688a66e 100644
--- a/module/VuFind/src/VuFind/Db/Table/ChangeTracker.php
+++ b/module/VuFind/src/VuFind/Db/Table/ChangeTracker.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Db_Table
diff --git a/module/VuFind/src/VuFind/Db/Table/Comments.php b/module/VuFind/src/VuFind/Db/Table/Comments.php
index a6b691b5e0a2135fe2db0fd952c9444241571648..a75089e2eb1ffbc944af7acea3553fc3497f8a49 100644
--- a/module/VuFind/src/VuFind/Db/Table/Comments.php
+++ b/module/VuFind/src/VuFind/Db/Table/Comments.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Db_Table
diff --git a/module/VuFind/src/VuFind/Db/Table/DbTableAwareInterface.php b/module/VuFind/src/VuFind/Db/Table/DbTableAwareInterface.php
index 158acdb9ebf810d727f5ebbd92a515681c48eca2..098e0af852aead2d07fb9ceffd3b0fd372e8785b 100644
--- a/module/VuFind/src/VuFind/Db/Table/DbTableAwareInterface.php
+++ b/module/VuFind/src/VuFind/Db/Table/DbTableAwareInterface.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Db_Table
diff --git a/module/VuFind/src/VuFind/Db/Table/DbTableAwareTrait.php b/module/VuFind/src/VuFind/Db/Table/DbTableAwareTrait.php
index 121f385b0f5d1fc62fc36a4c62cd60f089ceb914..14e9d1f5d17024267fabf83d073a97d912b50502 100644
--- a/module/VuFind/src/VuFind/Db/Table/DbTableAwareTrait.php
+++ b/module/VuFind/src/VuFind/Db/Table/DbTableAwareTrait.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Db_Table
diff --git a/module/VuFind/src/VuFind/Db/Table/Factory.php b/module/VuFind/src/VuFind/Db/Table/Factory.php
index 0369a3c7d2375b9fc95613b239b9404048af165c..389455d6e65e2ad83a9c01f81f6cec2497a7666b 100644
--- a/module/VuFind/src/VuFind/Db/Table/Factory.php
+++ b/module/VuFind/src/VuFind/Db/Table/Factory.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Db_Table
@@ -53,6 +53,36 @@ class Factory
         return new Resource($sm->getServiceLocator()->get('VuFind\DateConverter'));
     }
 
+    /**
+     * Construct the Tags table.
+     *
+     * @param ServiceManager $sm Service manager.
+     *
+     * @return User
+     */
+    public static function getTags(ServiceManager $sm)
+    {
+        $config = $sm->getServiceLocator()->get('VuFind\Config')->get('config');
+        $caseSensitive = isset($config->Social->case_sensitive_tags)
+            && $config->Social->case_sensitive_tags;
+        return new Tags($caseSensitive);
+    }
+
+    /**
+     * Construct the ResourceTags table.
+     *
+     * @param ServiceManager $sm Service manager.
+     *
+     * @return User
+     */
+    public static function getResourceTags(ServiceManager $sm)
+    {
+        $config = $sm->getServiceLocator()->get('VuFind\Config')->get('config');
+        $caseSensitive = isset($config->Social->case_sensitive_tags)
+            && $config->Social->case_sensitive_tags;
+        return new ResourceTags($caseSensitive);
+    }
+
     /**
      * Construct the User table.
      *
diff --git a/module/VuFind/src/VuFind/Db/Table/Gateway.php b/module/VuFind/src/VuFind/Db/Table/Gateway.php
index f8a17dc2a2c10bf13280db7a4763ec5a118fc81f..7caccd9b582a38a85ae15fc588c6723e453457da 100644
--- a/module/VuFind/src/VuFind/Db/Table/Gateway.php
+++ b/module/VuFind/src/VuFind/Db/Table/Gateway.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Db_Table
diff --git a/module/VuFind/src/VuFind/Db/Table/OaiResumption.php b/module/VuFind/src/VuFind/Db/Table/OaiResumption.php
index 38590babc665e753892755ed56c8fd177de85668..d90831ef9e74b05a00ee629fd354c50b2644e99a 100644
--- a/module/VuFind/src/VuFind/Db/Table/OaiResumption.php
+++ b/module/VuFind/src/VuFind/Db/Table/OaiResumption.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Db_Table
diff --git a/module/VuFind/src/VuFind/Db/Table/PluginFactory.php b/module/VuFind/src/VuFind/Db/Table/PluginFactory.php
index b2fb04c5111ca6220788be67aa79be886e84ff42..ad2484f4969a3910a6935c8ed10c60b76d683295 100644
--- a/module/VuFind/src/VuFind/Db/Table/PluginFactory.php
+++ b/module/VuFind/src/VuFind/Db/Table/PluginFactory.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Db_Table
diff --git a/module/VuFind/src/VuFind/Db/Table/PluginManager.php b/module/VuFind/src/VuFind/Db/Table/PluginManager.php
index 331747eaa7c3d6742a3a474cd837e97e51392953..2ac45b87643d1a0b4b1fd52b5dd1136da6be0cf6 100644
--- a/module/VuFind/src/VuFind/Db/Table/PluginManager.php
+++ b/module/VuFind/src/VuFind/Db/Table/PluginManager.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Db_Table
diff --git a/module/VuFind/src/VuFind/Db/Table/Record.php b/module/VuFind/src/VuFind/Db/Table/Record.php
index 7d373bb333763688fc4ea17631d9ac9c32018d01..ac614c4b5eb2cf3dda6a47c404429d7ccd7f3588 100644
--- a/module/VuFind/src/VuFind/Db/Table/Record.php
+++ b/module/VuFind/src/VuFind/Db/Table/Record.php
@@ -19,7 +19,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Db_Table
diff --git a/module/VuFind/src/VuFind/Db/Table/Resource.php b/module/VuFind/src/VuFind/Db/Table/Resource.php
index b58214e0d409218e21af8fbb1da2ea35735faf48..796c643693d92f36939e16280e237cb973fce25d 100644
--- a/module/VuFind/src/VuFind/Db/Table/Resource.php
+++ b/module/VuFind/src/VuFind/Db/Table/Resource.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Db_Table
diff --git a/module/VuFind/src/VuFind/Db/Table/ResourceTags.php b/module/VuFind/src/VuFind/Db/Table/ResourceTags.php
index c7c2be073c15f59572058575cc400d65162aa25f..d2a4b60efb74894db8df88f70d42328253f66451 100644
--- a/module/VuFind/src/VuFind/Db/Table/ResourceTags.php
+++ b/module/VuFind/src/VuFind/Db/Table/ResourceTags.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Db_Table
@@ -39,12 +39,22 @@ use Zend\Db\Sql\Expression;
  */
 class ResourceTags extends Gateway
 {
+    /**
+     * Are tags case sensitive?
+     *
+     * @var bool
+     */
+    protected $caseSensitive;
+
     /**
      * Constructor
+     *
+     * @param bool $caseSensitive Are tags case sensitive?
      */
-    public function __construct()
+    public function __construct($caseSensitive = false)
     {
         parent::__construct('resource_tags', 'VuFind\Db\Row\ResourceTags');
+        $this->caseSensitive = $caseSensitive;
     }
 
     /**
@@ -64,12 +74,12 @@ class ResourceTags extends Gateway
         $callback = function ($select) use ($resource, $tag, $user, $list) {
             $select->where->equalTo('resource_id', $resource)
                 ->equalTo('tag_id', $tag);
-            if (!is_null($list)) {
+            if (null !== $list) {
                 $select->where->equalTo('list_id', $list);
             } else {
                 $select->where->isNull('list_id');
             }
-            if (!is_null($user)) {
+            if (null !== $user) {
                 $select->where->equalTo('user_id', $user);
             } else {
                 $select->where->isNull('user_id');
@@ -82,13 +92,13 @@ class ResourceTags extends Gateway
             $result = $this->createRow();
             $result->resource_id = $resource;
             $result->tag_id = $tag;
-            if (!is_null($list)) {
+            if (null !== $list) {
                 $result->list_id = $list;
             }
-            if (!is_null($user)) {
+            if (null !== $user) {
                 $result->user_id = $user;
             }
-            if (!is_null($posted)) {
+            if (null !== $posted) {
                 $result->posted = $posted;
             }
             $result->save();
@@ -153,9 +163,13 @@ class ResourceTags extends Gateway
             $select->join(
                 ['t' => 'tags'], 'resource_tags.tag_id = t.id', []
             );
-            $select->where->equalTo('t.tag', $tag)
-                ->where->equalTo('resource_tags.user_id', $userId);
-            if (!is_null($listId)) {
+            if ($this->caseSensitive) {
+                $select->where->equalTo('t.tag', $tag);
+            } else {
+                $select->where->literal('lower(t.tag) = lower(?)', [$tag]);
+            }
+            $select->where->equalTo('resource_tags.user_id', $userId);
+            if (null !== $listId) {
                 $select->where->equalTo('resource_tags.list_id', $listId);
             }
         };
@@ -191,7 +205,7 @@ class ResourceTags extends Gateway
         $stats = (array)$result->current();
         if ($extended) {
             $stats['unique'] = count($this->getUniqueTags());
-            $stats['anonymous'] = count($this->getAnonymousCount());
+            $stats['anonymous'] = $this->getAnonymousCount();
         }
         return $stats;
     }
@@ -204,8 +218,8 @@ class ResourceTags extends Gateway
      * @param string       $user     ID of user removing links
      * @param string       $list     ID of list to unlink (null for ALL matching
      * tags, 'none' for tags not in a list, true for tags only found in a list)
-     * @param string       $tag      ID of tag to unlink (null for ALL matching
-     * tags)
+     * @param string|array $tag      ID or array of IDs of tag(s) to unlink (null
+     * for ALL matching tags)
      *
      * @return void
      */
@@ -213,13 +227,13 @@ class ResourceTags extends Gateway
     {
         $callback = function ($select) use ($resource, $user, $list, $tag) {
             $select->where->equalTo('user_id', $user);
-            if (!is_null($resource)) {
+            if (null !== $resource) {
                 if (!is_array($resource)) {
                     $resource = [$resource];
                 }
                 $select->where->in('resource_id', $resource);
             }
-            if (!is_null($list)) {
+            if (null !== $list) {
                 if (true === $list) {
                     // special case -- if $list is set to boolean true, we
                     // want to only delete tags that are associated with lists.
@@ -232,8 +246,12 @@ class ResourceTags extends Gateway
                     $select->where->equalTo('list_id', $list);
                 }
             }
-            if (!is_null($tag)) {
-                $select->where->equalTo('tag_id', $tag);
+            if (null !== $tag) {
+                if (is_array($tag)) {
+                    $select->where->in('tag_id', $tag);
+                } else {
+                    $select->where->equalTo('tag_id', $tag);
+                }
             }
         };
 
@@ -328,13 +346,13 @@ class ResourceTags extends Gateway
                 'resource_tags.resource_id = r.id',
                 ["title" => "title"]
             );
-            if (!is_null($userId)) {
+            if (null !== $userId) {
                 $select->where->equalTo('resource_tags.user_id', $userId);
             }
-            if (!is_null($resourceId)) {
+            if (null !== $resourceId) {
                 $select->where->equalTo('resource_tags.resource_id', $resourceId);
             }
-            if (!is_null($tagId)) {
+            if (null !== $tagId) {
                 $select->where->equalTo('resource_tags.tag_id', $tagId);
             }
             $select->group(['resource_id', 'title']);
@@ -354,7 +372,6 @@ class ResourceTags extends Gateway
      */
     public function getUniqueTags($userId = null, $resourceId = null, $tagId = null)
     {
-
         $callback = function ($select) use ($userId, $resourceId, $tagId) {
             $select->columns(
                 [
@@ -383,19 +400,22 @@ class ResourceTags extends Gateway
             $select->join(
                 ['t' => 'tags'],
                 'resource_tags.tag_id = t.id',
-                ["tag" => "tag"]
+                [
+                    'tag' =>
+                        $this->caseSensitive ? 'tag' : new Expression('lower(tag)')
+                ]
             );
-            if (!is_null($userId)) {
+            if (null !== $userId) {
                 $select->where->equalTo('resource_tags.user_id', $userId);
             }
-            if (!is_null($resourceId)) {
+            if (null !== $resourceId) {
                 $select->where->equalTo('resource_tags.resource_id', $resourceId);
             }
-            if (!is_null($tagId)) {
+            if (null !== $tagId) {
                 $select->where->equalTo('resource_tags.tag_id', $tagId);
             }
             $select->group(['tag_id', 'tag']);
-            $select->order(['tag']);
+            $select->order([new Expression('lower(tag)')]);
         };
         return $this->select($callback);
     }
@@ -441,13 +461,13 @@ class ResourceTags extends Gateway
                 'resource_tags.user_id = u.id',
                 ["username" => "username"]
             );
-            if (!is_null($userId)) {
+            if (null !== $userId) {
                 $select->where->equalTo('resource_tags.user_id', $userId);
             }
-            if (!is_null($resourceId)) {
+            if (null !== $resourceId) {
                 $select->where->equalTo('resource_tags.resource_id', $resourceId);
             }
-            if (!is_null($tagId)) {
+            if (null !== $tagId) {
                 $select->where->equalTo('resource_tags.tag_id', $tagId);
             }
             $select->group(['user_id', 'username']);
@@ -456,6 +476,27 @@ class ResourceTags extends Gateway
         return $this->select($callback);
     }
 
+    /**
+     * Given an array for sorting database results, make sure the tag field is
+     * sorted in a case-insensitive fashion.
+     *
+     * @param array $order Order settings
+     *
+     * @return array
+     */
+    protected function formatTagOrder($order)
+    {
+        if (empty($order)) {
+            return $order;
+        }
+        $newOrder = [];
+        foreach ((array)$order as $current) {
+            $newOrder[] = $current == 'tag'
+                ? new Expression('lower(tag)') : $current;
+        }
+        return $newOrder;
+    }
+
     /**
      * Get Resource Tags
      *
@@ -481,7 +522,10 @@ class ResourceTags extends Gateway
         $select->join(
             ['t' => 'tags'],
             'resource_tags.tag_id = t.id',
-            ["tag" => "tag"]
+            [
+                'tag' =>
+                    $this->caseSensitive ? 'tag' : new Expression('lower(tag)')
+            ]
         );
         $select->join(
             ['u' => 'user'],
@@ -502,7 +546,7 @@ class ResourceTags extends Gateway
         if (null !== $tagId) {
             $select->where->equalTo('resource_tags.tag_id', $tagId);
         }
-        $select->order($order);
+        $select->order($this->formatTagOrder($order));
 
         if (null !== $page) {
             $select->limit($limit);
diff --git a/module/VuFind/src/VuFind/Db/Table/Search.php b/module/VuFind/src/VuFind/Db/Table/Search.php
index a397492448a7c53e53a9c9415bf7a8e2f8a18eaa..aa7d516245fc9087c9520a52e7d98c62dea13846 100644
--- a/module/VuFind/src/VuFind/Db/Table/Search.php
+++ b/module/VuFind/src/VuFind/Db/Table/Search.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Db_Table
@@ -31,6 +31,7 @@ namespace VuFind\Db\Table;
 use minSO;
 use Zend\Db\Adapter\ParameterContainer;
 use Zend\Db\TableGateway\Feature;
+use Zend\Db\Sql\Expression;
 
 /**
  * Table Definition for search
@@ -71,7 +72,7 @@ class Search extends Gateway
             }
             $eventFeature = new Feature\EventFeature();
             $eventFeature->getEventManager()->attach(
-                Feature\EventFeature::EVENT_PRE_INSERT, [$this, 'onPreInsert']
+                Feature\EventFeature::EVENT_PRE_INITIALIZE, [$this, 'onPreInsert']
             );
             $this->featureSet->addFeature($eventFeature);
         }
@@ -80,7 +81,7 @@ class Search extends Gateway
     }
 
     /**
-     * Customize the Insert object to include extra metadata about the
+     * Customize the database object to include extra metadata about the
      * search_object field so that it will be written correctly. This is
      * triggered only when we're interacting with PostgreSQL; MySQL works fine
      * without the extra hint.
@@ -131,6 +132,64 @@ class Search extends Gateway
         return $this->select($callback);
     }
 
+    /**
+     * Delete expired searches. Allows setting of 'from' and 'to' ID's so that rows
+     * can be deleted in small batches.
+     *
+     * @param int $daysOld Age in days of an "expired" search.
+     * @param int $idFrom  Lowest id of rows to delete.
+     * @param int $idTo    Highest id of rows to delete.
+     *
+     * @return int Number of rows deleted
+     */
+    public function deleteExpired($daysOld = 2, $idFrom = null, $idTo = null)
+    {
+        // Determine the expiration date:
+        $expireDate = date('Y-m-d H:i:s', time() - $daysOld * 24 * 60 * 60);
+        $callback = function ($select) use ($expireDate, $idFrom, $idTo) {
+            $where = $select->where->lessThan('created', $expireDate)
+                ->equalTo('saved', 0);
+            if (null !== $idFrom) {
+                $where->and->greaterThanOrEqualTo('id', $idFrom);
+            }
+            if (null !== $idTo) {
+                $where->and->lessThanOrEqualTo('id', $idTo);
+            }
+        };
+        return $this->delete($callback);
+    }
+
+    /**
+     * Get the lowest id and highest id for expired searches.
+     *
+     * @param int $daysOld Age in days of an "expired" search.
+     *
+     * @return array|bool Array of lowest id and highest id or false if no expired
+     * records found
+     */
+    public function getExpiredIdRange($daysOld = 2)
+    {
+        // Determine the expiration date:
+        $expireDate = date('Y-m-d H:i:s', time() - $daysOld * 24 * 60 * 60);
+        $callback = function ($select) use ($expireDate) {
+            $select->where->lessThan('created', $expireDate)->equalTo('saved', 0);
+        };
+        $select = $this->getSql()->select();
+        $select->columns(
+            [
+                'id' => new Expression('1'), // required for TableGateway
+                'minId' => new Expression('MIN(id)'),
+                'maxId' => new Expression('MAX(id)'),
+            ]
+        );
+        $select->where($callback);
+        $result = $this->selectWith($select)->current();
+        if (null === $result->minId) {
+            return false;
+        }
+        return [$result->minId, $result->maxId];
+    }
+
     /**
      * Get a query representing expired searches (this can be passed
      * to select() or delete() for further processing).
diff --git a/module/VuFind/src/VuFind/Db/Table/Session.php b/module/VuFind/src/VuFind/Db/Table/Session.php
index 3c555b30efb5817bf2c3a87143a14e51bdc0969d..c6449ad23e4fa9fecb5c057175183bc41acecb61 100644
--- a/module/VuFind/src/VuFind/Db/Table/Session.php
+++ b/module/VuFind/src/VuFind/Db/Table/Session.php
@@ -5,6 +5,7 @@
  * PHP version 5
  *
  * Copyright (C) Villanova University 2010.
+ * Copyright (C) The National Library of Finland 2016.
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License version 2,
@@ -17,16 +18,18 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Db_Table
  * @author   Demian Katz <demian.katz@villanova.edu>
+ * @author   Ere Maijala <ere.maijala@helsinki.fi>
  * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
  * @link     https://vufind.org Main Page
  */
 namespace VuFind\Db\Table;
 use VuFind\Exception\SessionExpired as SessionExpiredException;
+use Zend\Db\Sql\Expression;
 
 /**
  * Table Definition for session
@@ -34,6 +37,7 @@ use VuFind\Exception\SessionExpired as SessionExpiredException;
  * @category VuFind
  * @package  Db_Table
  * @author   Demian Katz <demian.katz@villanova.edu>
+ * @author   Ere Maijala <ere.maijala@helsinki.fi>
  * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
  * @link     https://vufind.org Main Site
  */
@@ -139,6 +143,61 @@ class Session extends Gateway
         $this->delete($callback);
     }
 
+    /**
+     * Delete expired sessions. Allows setting of 'from' and 'to' ID's so that rows
+     * can be deleted in small batches.
+     *
+     * @param int $daysOld Age in days of an "expired" session.
+     * @param int $idFrom  Lowest id of rows to delete.
+     * @param int $idTo    Highest id of rows to delete.
+     *
+     * @return int Number of rows deleted
+     */
+    public function deleteExpired($daysOld = 2, $idFrom = null, $idTo = null)
+    {
+        $expireDate = time() - $daysOld * 24 * 60 * 60;
+        $callback = function ($select) use ($expireDate, $idFrom, $idTo) {
+            $where = $select->where->lessThan('last_used', $expireDate);
+            if (null !== $idFrom) {
+                $where->and->greaterThanOrEqualTo('id', $idFrom);
+            }
+            if (null !== $idTo) {
+                $where->and->lessThanOrEqualTo('id', $idTo);
+            }
+        };
+        return $this->delete($callback);
+    }
+
+    /**
+     * Get the lowest id and highest id for expired sessions.
+     *
+     * @param int $daysOld Age in days of an "expired" session.
+     *
+     * @return array|bool Array of lowest id and highest id or false if no expired
+     * records found
+     */
+    public function getExpiredIdRange($daysOld = 2)
+    {
+        $expireDate = time() - $daysOld * 24 * 60 * 60;
+        $callback = function ($select) use ($expireDate) {
+            $select->where->lessThan('last_used', $expireDate);
+        };
+        $select = $this->getSql()->select();
+        $select->columns(
+            [
+                'id' => new Expression('1'), // required for TableGateway
+                'minId' => new Expression('MIN(id)'),
+                'maxId' => new Expression('MAX(id)'),
+            ]
+        );
+        $select->where($callback);
+        $result = $this->selectWith($select)->current();
+        if (null === $result->minId) {
+            return false;
+        }
+        return [$result->minId, $result->maxId];
+    }
+
     /**
      * Get a query representing expired sessions (this can be passed
      * to select() or delete() for further processing).
diff --git a/module/VuFind/src/VuFind/Db/Table/Tags.php b/module/VuFind/src/VuFind/Db/Table/Tags.php
index 9b1a7ae535d67ed43873976336aa252373339cd9..1992d7a3c5db381767a293a0db46b9b6bc1e61e0 100644
--- a/module/VuFind/src/VuFind/Db/Table/Tags.php
+++ b/module/VuFind/src/VuFind/Db/Table/Tags.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Db_Table
@@ -26,7 +26,9 @@
  * @link     https://vufind.org Main Site
  */
 namespace VuFind\Db\Table;
-use Zend\Db\Sql\Expression, Zend\Db\Sql\Select;
+use Zend\Db\Sql\Expression,
+    Zend\Db\Sql\Predicate\Predicate,
+    Zend\Db\Sql\Select;
 
 /**
  * Table Definition for tags
@@ -39,36 +41,56 @@ use Zend\Db\Sql\Expression, Zend\Db\Sql\Select;
  */
 class Tags extends Gateway
 {
+    /**
+     * Are tags case sensitive?
+     *
+     * @var bool
+     */
+    protected $caseSensitive;
+
     /**
      * Constructor
+     *
+     * @param bool $caseSensitive Are tags case sensitive?
      */
-    public function __construct()
+    public function __construct($caseSensitive = false)
     {
         parent::__construct('tags', 'VuFind\Db\Row\Tags');
+        $this->caseSensitive = $caseSensitive;
     }
 
     /**
      * Get the row associated with a specific tag string.
      *
-     * @param string $tag    Tag to look up.
-     * @param bool   $create Should we create the row if it does not exist?
+     * @param string $tag       Tag to look up.
+     * @param bool   $create    Should we create the row if it does not exist?
+     * @param bool   $firstOnly Should we return the first matching row (true)
+     * or the entire result set (in case of multiple matches)?
      *
-     * @return \VuFind\Db\Row\Tags|null Matching row if found or created, null
-     * otherwise.
+     * @return mixed Matching row/result set if found or created, null otherwise.
      */
-    public function getByText($tag, $create = true)
+    public function getByText($tag, $create = true, $firstOnly = true)
     {
-        $result = $this->select(['tag' => $tag])->current();
-        if (empty($result) && $create) {
-            $result = $this->createRow();
-            $result->tag = $tag;
-            $result->save();
+        $cs = $this->caseSensitive;
+        $callback = function ($select) use ($tag, $cs) {
+            if ($cs) {
+                $select->where->equalTo('tag', $tag);
+            } else {
+                $select->where->literal('lower(tag) = lower(?)', [$tag]);
+            }
+        };
+        $result = $this->select($callback);
+        if (count($result) == 0 && $create) {
+            $row = $this->createRow();
+            $row->tag = $cs ? $tag : mb_strtolower($tag, 'UTF8');
+            $row->save();
+            return $firstOnly ? $row : [$row];
         }
-        return $result;
+        return $firstOnly ? $result->current() : $result;
     }
 
     /**
-     * Get the tags the match a string
+     * Get the tags that match a string
      *
      * @param string $text  Tag to look up.
      * @param string $sort  Sort/search parameter
@@ -84,6 +106,67 @@ class Tags extends Gateway
         return $this->getTagList($sort, $limit, $callback);
     }
 
+    /**
+     * Get all resources associated with the provided tag query.
+     *
+     * @param string $q      Search query
+     * @param string $source Record source (optional limiter)
+     * @param string $sort   Resource field to sort on (optional)
+     * @param int    $offset Offset for results
+     * @param int    $limit  Limit for results (null for none)
+     * @param bool   $fuzzy  Are we doing an exact or fuzzy search?
+     *
+     * @return array
+     */
+    public function resourceSearch($q, $source = null, $sort = null,
+        $offset = 0, $limit = null, $fuzzy = true
+    ) {
+        $cb = function ($select) use ($q, $source, $sort, $offset, $limit, $fuzzy) {
+            $select->columns(
+                [
+                    new Expression(
+                        'DISTINCT(?)', ['resource.id'],
+                        [Expression::TYPE_IDENTIFIER]
+                    ),
+                ]
+            );
+            $select->join(
+                ['rt' => 'resource_tags'],
+                'tags.id = rt.tag_id',
+                []
+            );
+            $select->join(
+                ['resource' => 'resource'],
+                'rt.resource_id = resource.id',
+                '*'
+            );
+            if ($fuzzy) {
+                $select->where->literal('lower(tags.tag) like lower(?)', [$q]);
+            } else if (!$this->caseSensitive) {
+                $select->where->literal('lower(tags.tag) = lower(?)', [$q]);
+            } else {
+                $select->where->equalTo('tags.tag', $q);
+            }
+
+            if (!empty($source)) {
+                $select->where->equalTo('source', $source);
+            }
+
+            if (!empty($sort)) {
+                Resource::applySort($select, $sort);
+            }
+
+            if ($offset > 0) {
+                $select->offset($offset);
+            }
+            if (null !== $limit) {
+                $select->limit($limit);
+            }
+        };
+
+        return $this->select($cb);
+    }
+
     /**
      * Get tags associated with the specified resource.
      *
@@ -116,8 +199,10 @@ class Tags extends Gateway
                         ['subq' => $subq],
                         'tags.id = subq.tag_id',
                         [
+                            // is_me will either be null (not owned) or the ID
+                            // of the tag (owned by the current user).
                             'is_me' => new Expression(
-                                'MAX(?)', ['subq.is_me'],
+                                'MAX(?)', ['subq.tag_id'],
                                 [Expression::TYPE_IDENTIFIER]
                             )
                         ],
@@ -127,7 +212,9 @@ class Tags extends Gateway
                 // SELECT (do not add table prefixes)
                 $select->columns(
                     [
-                        'id', 'tag',
+                        'id',
+                        'tag' => $this->caseSensitive
+                            ? 'tag' : new Expression('lower(tag)'),
                         'cnt' => new Expression(
                             'COUNT(DISTINCT(?))', ["rt.user_id"],
                             [Expression::TYPE_IDENTIFIER]
@@ -144,9 +231,9 @@ class Tags extends Gateway
                 $select->group(['tags.id', 'tag']);
 
                 if ($sort == 'count') {
-                    $select->order(['cnt DESC', 'tags.tag']);
+                    $select->order(['cnt DESC', new Expression('lower(tags.tag)')]);
                 } else if ($sort == 'tag') {
-                    $select->order(['tags.tag']);
+                    $select->order([new Expression('lower(tags.tag)')]);
                 }
 
                 if ($limit > 0) {
@@ -156,16 +243,77 @@ class Tags extends Gateway
                     $select->where->isNotNull('rt.list_id');
                 } else if ($list === false) {
                     $select->where->isNull('rt.list_id');
-                } else if (!is_null($list)) {
+                } else if (null !== $list) {
                     $select->where->equalTo('rt.list_id', $list);
                 }
-                if (!is_null($user)) {
+                if (null !== $user) {
                     $select->where->equalTo('rt.user_id', $user);
                 }
             }
         );
     }
 
+    /**
+     * Get a list of all tags generated by the user in favorites lists.  Note that
+     * the returned list WILL NOT include tags attached to records that are not
+     * saved in favorites lists.
+     *
+     * @param string $userId     User ID to look up.
+     * @param string $resourceId Filter for tags tied to a specific resource (null
+     * for no filter).
+     * @param int    $listId     Filter for tags tied to a specific list (null for no
+     * filter).
+     * @param string $source     Filter for tags tied to a specific record source.
+     *
+     * @return \Zend\Db\ResultSet\AbstractResultSet
+     */
+    public function getForUser($userId, $resourceId = null, $listId = null,
+        $source = DEFAULT_SEARCH_BACKEND
+    ) {
+        $callback = function ($select) use ($userId, $resourceId, $listId, $source) {
+            $select->columns(
+                [
+                    'id' => new Expression(
+                        'min(?)', ['tags.id'],
+                        [Expression::TYPE_IDENTIFIER]
+                    ),
+                    'tag' => $this->caseSensitive
+                        ? 'tag' : new Expression('lower(tag)'),
+                    'cnt' => new Expression(
+                        'COUNT(DISTINCT(?))', ['rt.resource_id'],
+                        [Expression::TYPE_IDENTIFIER]
+                    )
+                ]
+            );
+            $select->join(
+                ['rt' => 'resource_tags'], 'tags.id = rt.tag_id', []
+            );
+            $select->join(
+                ['r' => 'resource'], 'rt.resource_id = r.id', []
+            );
+            $select->join(
+                ['ur' => 'user_resource'], 'r.id = ur.resource_id', []
+            );
+            $select->group(['tag'])->order([new Expression('lower(tag)')]);
+
+            $select->where->equalTo('ur.user_id', $userId)
+                ->equalTo('rt.user_id', $userId)
+                ->equalTo(
+                    'ur.list_id', 'rt.list_id',
+                    Predicate::TYPE_IDENTIFIER, Predicate::TYPE_IDENTIFIER
+                )
+                ->equalTo('r.source', $source);
+
+            if (null !== $resourceId) {
+                $select->where->equalTo('r.record_id', $resourceId);
+            }
+            if (null !== $listId) {
+                $select->where->equalTo('rt.list_id', $listId);
+            }
+        };
+        return $this->select($callback);
+    }
+
     /**
      * Get a subquery used for flagging tag ownership (see getForResource).
      *
@@ -178,7 +326,7 @@ class Tags extends Gateway
     protected function getIsMeSubquery($id, $source, $userToCheck)
     {
         $sub = new Select('resource_tags');
-        $sub->columns(['tag_id', 'is_me' => new Expression("1")])
+        $sub->columns(['tag_id'])
             ->join(
                 // Convert record_id to resource_id
                 ['r' => 'resource'],
@@ -210,7 +358,9 @@ class Tags extends Gateway
         $callback = function ($select) use ($sort, $limit, $extra_where) {
             $select->columns(
                 [
-                    'id', 'tag',
+                    'id',
+                    'tag' => $this->caseSensitive
+                        ? 'tag' : new Expression('lower(tag)'),
                     'cnt' => new Expression(
                         'COUNT(DISTINCT(?))', ['resource_tags.resource_id'],
                         [Expression::TYPE_IDENTIFIER]
@@ -230,14 +380,14 @@ class Tags extends Gateway
             $select->group(['tags.id', 'tags.tag']);
             switch ($sort) {
             case 'alphabetical':
-                $select->order(['tags.tag', 'cnt DESC']);
+                $select->order([new Expression('lower(tags.tag)'), 'cnt DESC']);
                 break;
             case 'popularity':
-                $select->order(['cnt DESC', 'tags.tag']);
+                $select->order(['cnt DESC', new Expression('lower(tags.tag)')]);
                 break;
             case 'recent':
                 $select->order(
-                    ['posted DESC', 'cnt DESC', 'tags.tag']
+                    ['posted DESC', 'cnt DESC', new Expression('lower(tags.tag)')]
                 );
                 break;
             }
@@ -279,7 +429,7 @@ class Tags extends Gateway
 
     /**
      * Get a list of duplicate tags (this should never happen, but past bugs
-     * have introduced problems).
+     * and the introduction of case-insensitive tags have introduced problems).
      *
      * @return mixed
      */
@@ -288,7 +438,9 @@ class Tags extends Gateway
         $callback = function ($select) {
             $select->columns(
                 [
-                    'tag',
+                    'tag' => new Expression(
+                        'MIN(?)', ['tag'], [Expression::TYPE_IDENTIFIER]
+                    ),
                     'cnt' => new Expression(
                         'COUNT(?)', ['tag'], [Expression::TYPE_IDENTIFIER]
                     ),
@@ -297,8 +449,10 @@ class Tags extends Gateway
                     )
                 ]
             );
-            $select->group('tag');
-            $select->having->greaterThan('cnt', 1);
+            $select->group(
+                $this->caseSensitive ? 'tag' : new Expression('lower(tag)')
+            );
+            $select->having('COUNT(tag) > 1');
         };
         return $this->select($callback);
     }
@@ -345,7 +499,7 @@ class Tags extends Gateway
     protected function fixDuplicateTag($tag)
     {
         // Make sure this really is a duplicate.
-        $result = $this->select(['tag' => $tag]);
+        $result = $this->getByText($tag, false, false);
         if (count($result) < 2) {
             return;
         }
diff --git a/module/VuFind/src/VuFind/Db/Table/User.php b/module/VuFind/src/VuFind/Db/Table/User.php
index 158475e98c3b1628ca4390080bc9c5f4ce110ed6..f09907a5ba182f39b50d27c95ee3d431292067a4 100644
--- a/module/VuFind/src/VuFind/Db/Table/User.php
+++ b/module/VuFind/src/VuFind/Db/Table/User.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Db_Table
diff --git a/module/VuFind/src/VuFind/Db/Table/UserCard.php b/module/VuFind/src/VuFind/Db/Table/UserCard.php
index 263a8702386c314ce970f45333e60c9d68870b7e..590117406ff013c04540e2e46f6ea67f579b8f26 100644
--- a/module/VuFind/src/VuFind/Db/Table/UserCard.php
+++ b/module/VuFind/src/VuFind/Db/Table/UserCard.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Db_Table
diff --git a/module/VuFind/src/VuFind/Db/Table/UserList.php b/module/VuFind/src/VuFind/Db/Table/UserList.php
index 2fd8cd44f4c497e4192873a651434bc4dae6e13f..ced6dce5e776791aba1b128a956b328d15926f4f 100644
--- a/module/VuFind/src/VuFind/Db/Table/UserList.php
+++ b/module/VuFind/src/VuFind/Db/Table/UserList.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Db_Table
diff --git a/module/VuFind/src/VuFind/Db/Table/UserResource.php b/module/VuFind/src/VuFind/Db/Table/UserResource.php
index 9eb40c7e14971c3380effa5b493766120abe5886..00e4d946fc74ea1a1fac4c43262d6d1de17447ae 100644
--- a/module/VuFind/src/VuFind/Db/Table/UserResource.php
+++ b/module/VuFind/src/VuFind/Db/Table/UserResource.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Db_Table
diff --git a/module/VuFind/src/VuFind/Db/Table/UserStats.php b/module/VuFind/src/VuFind/Db/Table/UserStats.php
index 86ca25bc84023765654dbb55fc572ed4b55a6190..ea4203eb69204adb1d14962e9dd073a1513e7117 100644
--- a/module/VuFind/src/VuFind/Db/Table/UserStats.php
+++ b/module/VuFind/src/VuFind/Db/Table/UserStats.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Db_Table
@@ -51,9 +51,9 @@ class UserStats extends Gateway
     /**
      * Returns the list of most popular browsers with use counts
      *
-     * @param boolean $withVersions Include browser version numbers?
+     * @param bool $withVersions Include browser version numbers?
      * True = versions (Firefox 12.0) False = names only (Firefox).
-     * @param integer $limit        How many to return
+     * @param int  $limit        How many to return
      *
      * @return array
      */
diff --git a/module/VuFind/src/VuFind/Db/Table/UserStatsFields.php b/module/VuFind/src/VuFind/Db/Table/UserStatsFields.php
index 832a43888cec475ce60edf131b552172623cc445..3a833e0872191fa0ecf9e594da876b3db49f47e9 100644
--- a/module/VuFind/src/VuFind/Db/Table/UserStatsFields.php
+++ b/module/VuFind/src/VuFind/Db/Table/UserStatsFields.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Db_Table
diff --git a/module/VuFind/src/VuFind/Exception/Auth.php b/module/VuFind/src/VuFind/Exception/Auth.php
index c0e7746105174d8f532f6dbb220e69dcfde1b777..6c55a69edba5eabe035dcff0346ac60bf9546627 100644
--- a/module/VuFind/src/VuFind/Exception/Auth.php
+++ b/module/VuFind/src/VuFind/Exception/Auth.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Exceptions
diff --git a/module/VuFind/src/VuFind/Exception/Date.php b/module/VuFind/src/VuFind/Exception/Date.php
index b8ce90a29cafe008e924b82c562f5ca9c74b337f..a7315f109fc037f477689356cd4fbe0c7a733cf3 100644
--- a/module/VuFind/src/VuFind/Exception/Date.php
+++ b/module/VuFind/src/VuFind/Exception/Date.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Exceptions
diff --git a/module/VuFind/src/VuFind/Exception/FileAccess.php b/module/VuFind/src/VuFind/Exception/FileAccess.php
index 16e884e8682bc1c3b4c550f03e2603edd6bcc45d..3802a74cfd82f6faac87dc1eea6c40532b0f9b1a 100644
--- a/module/VuFind/src/VuFind/Exception/FileAccess.php
+++ b/module/VuFind/src/VuFind/Exception/FileAccess.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Exceptions
diff --git a/module/VuFind/src/VuFind/Exception/Forbidden.php b/module/VuFind/src/VuFind/Exception/Forbidden.php
index fe1c96f89c541aeaf48b2e411eea5de571bb827d..42245d3777b427e2a762025ad420d87ba20bbe28 100644
--- a/module/VuFind/src/VuFind/Exception/Forbidden.php
+++ b/module/VuFind/src/VuFind/Exception/Forbidden.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Exceptions
@@ -36,6 +36,15 @@ namespace VuFind\Exception;
  * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
  * @link     https://vufind.org/wiki/development Wiki
  */
-class Forbidden extends \Exception
+class Forbidden extends \Exception implements HttpStatusInterface
 {
+    /**
+     * Get HTTP status associated with this exception.
+     *
+     * @return int
+     */
+    public function getHttpStatus()
+    {
+        return 403;
+    }
 }
diff --git a/module/VuFind/src/VuFind/Exception/HttpStatusInterface.php b/module/VuFind/src/VuFind/Exception/HttpStatusInterface.php
new file mode 100644
index 0000000000000000000000000000000000000000..821d7d16769256df5a9540cd4d5a0e546eaabeca
--- /dev/null
+++ b/module/VuFind/src/VuFind/Exception/HttpStatusInterface.php
@@ -0,0 +1,49 @@
+<?php
+/**
+ * Interface for exceptions that should trigger specific HTTP status codes
+ * when unhandled.
+ *
+ * PHP version 5
+ *
+ * Copyright (C) Villanova University 2011.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ * @category VuFind
+ * @package  Exceptions
+ * @author   Demian Katz <demian.katz@villanova.edu>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     https://vufind.org/wiki/development Wiki
+ */
+namespace VuFind\Exception;
+
+/**
+ * Interface for exceptions that should trigger specific HTTP status codes
+ * when unhandled.
+ *
+ * @category VuFind
+ * @package  Exceptions
+ * @author   Demian Katz <demian.katz@villanova.edu>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     https://vufind.org/wiki/development Wiki
+ */
+interface HttpStatusInterface
+{
+    /**
+     * Get HTTP status associated with this exception.
+     *
+     * @return int
+     */
+    public function getHttpStatus();
+}
diff --git a/module/VuFind/src/VuFind/Exception/ILS.php b/module/VuFind/src/VuFind/Exception/ILS.php
index 525870d375fa8a74cac594a56b18faa2c148e7c3..84393627782ece565057e91cf63048406a1d6728 100644
--- a/module/VuFind/src/VuFind/Exception/ILS.php
+++ b/module/VuFind/src/VuFind/Exception/ILS.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Exceptions
diff --git a/module/VuFind/src/VuFind/Exception/LibraryCard.php b/module/VuFind/src/VuFind/Exception/LibraryCard.php
index 045b5a4d505d156b0c0d6ec5558f4571d11fd633..f0d7bbf22f0376ba30b558478176b21d8a87e25e 100644
--- a/module/VuFind/src/VuFind/Exception/LibraryCard.php
+++ b/module/VuFind/src/VuFind/Exception/LibraryCard.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Exceptions
diff --git a/module/VuFind/src/VuFind/Exception/ListPermission.php b/module/VuFind/src/VuFind/Exception/ListPermission.php
index 337ad6056a1fecbe0b79e95cda85c53ae71a9b69..b84168341d146bc34d83a7b02a5f491373fbf742 100644
--- a/module/VuFind/src/VuFind/Exception/ListPermission.php
+++ b/module/VuFind/src/VuFind/Exception/ListPermission.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Exceptions
@@ -36,6 +36,6 @@ namespace VuFind\Exception;
  * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
  * @link     https://vufind.org/wiki/development Wiki
  */
-class ListPermission extends \Exception
+class ListPermission extends Forbidden
 {
 }
diff --git a/module/VuFind/src/VuFind/Exception/LoginRequired.php b/module/VuFind/src/VuFind/Exception/LoginRequired.php
index 4f44b4e7d8e0e08d780824151ea9cf6b16761ca6..59e5ca594f1059b70f81099c2f9346599242d5a1 100644
--- a/module/VuFind/src/VuFind/Exception/LoginRequired.php
+++ b/module/VuFind/src/VuFind/Exception/LoginRequired.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Exceptions
@@ -36,6 +36,6 @@ namespace VuFind\Exception;
  * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
  * @link     https://vufind.org/wiki/development Wiki
  */
-class LoginRequired extends \Exception
+class LoginRequired extends Forbidden
 {
 }
diff --git a/module/VuFind/src/VuFind/Exception/Mail.php b/module/VuFind/src/VuFind/Exception/Mail.php
index d57f6ea054788d707dc528f53f9c7c959a9b93a7..727cc037fda3380cda0dc0e782608253d9779da9 100644
--- a/module/VuFind/src/VuFind/Exception/Mail.php
+++ b/module/VuFind/src/VuFind/Exception/Mail.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Exceptions
diff --git a/module/VuFind/src/VuFind/Exception/MissingField.php b/module/VuFind/src/VuFind/Exception/MissingField.php
index d208169c618631e6707b39e1e57a0fbd6d979b89..cd9e532591c2a8e373c4e50c9ba172a1bd02c2a7 100644
--- a/module/VuFind/src/VuFind/Exception/MissingField.php
+++ b/module/VuFind/src/VuFind/Exception/MissingField.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Exceptions
diff --git a/module/VuFind/src/VuFind/Exception/PasswordSecurity.php b/module/VuFind/src/VuFind/Exception/PasswordSecurity.php
index 54616f80db0ee7faba33724411cf94f588fec2c7..198404a3adbff0925309162d4c9eea23d47239f9 100644
--- a/module/VuFind/src/VuFind/Exception/PasswordSecurity.php
+++ b/module/VuFind/src/VuFind/Exception/PasswordSecurity.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Exceptions
diff --git a/module/VuFind/src/VuFind/Exception/RecordMissing.php b/module/VuFind/src/VuFind/Exception/RecordMissing.php
index 89b0d9f36dacd1d87a9201b9c17d8846cec3215c..598312f32e14219c38eb06c39a035565f18be170 100644
--- a/module/VuFind/src/VuFind/Exception/RecordMissing.php
+++ b/module/VuFind/src/VuFind/Exception/RecordMissing.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Exceptions
@@ -36,6 +36,15 @@ namespace VuFind\Exception;
  * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
  * @link     https://vufind.org/wiki/development Wiki
  */
-class RecordMissing extends \Exception
+class RecordMissing extends \Exception implements HttpStatusInterface
 {
+    /**
+     * Get HTTP status associated with this exception.
+     *
+     * @return int
+     */
+    public function getHttpStatus()
+    {
+        return 404;
+    }
 }
diff --git a/module/VuFind/src/VuFind/Exception/SessionExpired.php b/module/VuFind/src/VuFind/Exception/SessionExpired.php
index b933ba7c4fcf053e218cb737917fba3d6a7ef0ca..dee31eeb4675ae7bd1c486a23207e7b37df1cb2a 100644
--- a/module/VuFind/src/VuFind/Exception/SessionExpired.php
+++ b/module/VuFind/src/VuFind/Exception/SessionExpired.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Exceptions
diff --git a/module/VuFind/src/VuFind/Export.php b/module/VuFind/src/VuFind/Export.php
index 57b472b5d6eb1662523a9fe20d07947a5570105e..256492b8438cb3557d1fd3d292d68792d982b6bc 100644
--- a/module/VuFind/src/VuFind/Export.php
+++ b/module/VuFind/src/VuFind/Export.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Export
diff --git a/module/VuFind/src/VuFind/Feed/Writer/Extension/DublinCore/Entry.php b/module/VuFind/src/VuFind/Feed/Writer/Extension/DublinCore/Entry.php
index 3ba164fa196bd05944ce0dff58370eb0f609c1b5..2277a6ebcbf8e92fe17a4a21b5d57a1e43b458a3 100644
--- a/module/VuFind/src/VuFind/Feed/Writer/Extension/DublinCore/Entry.php
+++ b/module/VuFind/src/VuFind/Feed/Writer/Extension/DublinCore/Entry.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Feed_Plugins
diff --git a/module/VuFind/src/VuFind/Feed/Writer/Extension/DublinCore/Renderer/Entry.php b/module/VuFind/src/VuFind/Feed/Writer/Extension/DublinCore/Renderer/Entry.php
index 2736f4ee382f69cc56b0ca244b8d3dd7d8bdc663..cc1af3a5525fc512e0db88716713fb640e1a3681 100644
--- a/module/VuFind/src/VuFind/Feed/Writer/Extension/DublinCore/Renderer/Entry.php
+++ b/module/VuFind/src/VuFind/Feed/Writer/Extension/DublinCore/Renderer/Entry.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Feed_Plugins
diff --git a/module/VuFind/src/VuFind/Feed/Writer/Extension/OpenSearch/Feed.php b/module/VuFind/src/VuFind/Feed/Writer/Extension/OpenSearch/Feed.php
index 03be6c270af1ac5d977eb15f6f300609e06cf4f4..3d5d6a5399aa9dcc2824e44e1d6e7f3c1213bef1 100644
--- a/module/VuFind/src/VuFind/Feed/Writer/Extension/OpenSearch/Feed.php
+++ b/module/VuFind/src/VuFind/Feed/Writer/Extension/OpenSearch/Feed.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Feed_Plugins
diff --git a/module/VuFind/src/VuFind/Feed/Writer/Extension/OpenSearch/Renderer/Feed.php b/module/VuFind/src/VuFind/Feed/Writer/Extension/OpenSearch/Renderer/Feed.php
index c5e0897e3e464265425f5e28e04874a4cd2908d1..ccec269077c4d27892c179a3e05fcf389dfca5a0 100644
--- a/module/VuFind/src/VuFind/Feed/Writer/Extension/OpenSearch/Renderer/Feed.php
+++ b/module/VuFind/src/VuFind/Feed/Writer/Extension/OpenSearch/Renderer/Feed.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Feed_Plugins
diff --git a/module/VuFind/src/VuFind/Harvester/OAI.php b/module/VuFind/src/VuFind/Harvester/OAI.php
deleted file mode 100644
index 2a807a6fd7ebc893e5b48ee12498d597f92f5f95..0000000000000000000000000000000000000000
--- a/module/VuFind/src/VuFind/Harvester/OAI.php
+++ /dev/null
@@ -1,1044 +0,0 @@
-<?php
-/**
- * OAI-PMH Harvest Tool
- *
- * PHP version 5
- *
- * Copyright (c) Demian Katz 2010.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License version 2,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
- *
- * @category VuFind
- * @package  Harvest_Tools
- * @author   Demian Katz <demian.katz@villanova.edu>
- * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
- * @link     https://vufind.org/wiki/indexing:oai-pmh Wiki
- */
-namespace VuFind\Harvester;
-use Zend\Console\Console;
-
-/**
- * OAI Class
- *
- * This class harvests records via OAI-PMH using settings from oai.ini.
- *
- * @category VuFind
- * @package  Harvest_Tools
- * @author   Demian Katz <demian.katz@villanova.edu>
- * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
- * @link     https://vufind.org/wiki/indexing:oai-pmh Wiki
- */
-class OAI
-{
-    /**
-     * HTTP client
-     *
-     * @var \Zend\Http\Client
-     */
-    protected $client;
-
-    /**
-     * HTTP client's timeout
-     *
-     * @var int
-     */
-    protected $timeout = 60;
-
-    /**
-     * Combine harvested records (per OAI chunk size) into one (collection) file?
-     *
-     * @var bool
-     */
-    protected $combineRecords = false;
-
-    /**
-     * The wrapping XML tag to be used if combinedRecords is set to true
-     *
-     * @var string
-     */
-    protected $combineRecordsTag = '<collection>';
-
-    /**
-     * URL to harvest from
-     *
-     * @var string
-     */
-    protected $baseURL;
-
-    /**
-     * Target set(s) to harvest (null for all records)
-     *
-     * @var string|array
-     */
-    protected $set = null;
-
-    /**
-     * Metadata type to harvest
-     *
-     * @var string
-     */
-    protected $metadataPrefix = 'oai_dc';
-
-    /**
-     * OAI prefix to strip from ID values
-     *
-     * @var string
-     */
-    protected $idPrefix = '';
-
-    /**
-     * Regular expression searches
-     *
-     * @var array
-     */
-    protected $idSearch = [];
-
-    /**
-     * Replacements for regular expression matches
-     *
-     * @var array
-     */
-    protected $idReplace = [];
-
-    /**
-     * Directory for storing harvested files
-     *
-     * @var string
-     */
-    protected $basePath;
-
-    /**
-     * File for tracking last harvest date
-     *
-     * @var string
-     */
-    protected $lastHarvestFile;
-
-    /**
-     * File for tracking last harvest state (for continuing interrupted
-     * connection).
-     *
-     * @var string
-     */
-    protected $lastStateFile;
-
-    /**
-     * Harvest end date (null for no specific end)
-     *
-     * @var string
-     */
-    protected $harvestEndDate;
-
-    /**
-     * Harvest start date (null for no specific start)
-     *
-     * @var string
-     */
-    protected $startDate = null;
-
-    /**
-     * Date granularity ('auto' to autodetect)
-     *
-     * @var string
-     */
-    protected $granularity = 'auto';
-
-    /**
-     * Tag to use for injecting IDs into XML (false for none)
-     *
-     * @var string|bool
-     */
-    protected $injectId = false;
-
-    /**
-     * Tag to use for injecting setSpecs (false for none)
-     *
-     * @var string|bool
-     */
-    protected $injectSetSpec = false;
-
-    /**
-     * Tag to use for injecting set names (false for none)
-     *
-     * @var string|bool
-     */
-    protected $injectSetName = false;
-
-    /**
-     * Tag to use for injecting datestamp (false for none)
-     *
-     * @var string|bool
-     */
-    protected $injectDate = false;
-
-    /**
-     * List of header elements to copy into body
-     *
-     * @var array
-     */
-    protected $injectHeaderElements = [];
-
-    /**
-     * Associative array of setSpec => setName
-     *
-     * @var array
-     */
-    protected $setNames = [];
-
-    /**
-     * Filename for logging harvested IDs (false for none)
-     *
-     * @var string|bool
-     */
-    protected $harvestedIdLog = false;
-
-    /**
-     * Should we display debug output?
-     *
-     * @var bool
-     */
-    protected $verbose = false;
-
-    /**
-     * Should we sanitize XML?
-     *
-     * @var bool
-     */
-    protected $sanitize = false;
-
-    /**
-     * Filename for logging bad XML responses (false for none)
-     *
-     * @var string|bool
-     */
-    protected $badXMLLog = false;
-
-    /**
-     * Username for HTTP basic authentication (false for none)
-     *
-     * @var string|bool
-     */
-    protected $httpUser = false;
-
-    /**
-     * Password for HTTP basic authentication (false for none)
-     *
-     * @var string|bool
-     */
-    protected $httpPass = false;
-
-    /**
-     * As we harvest records, we want to track the most recent date encountered
-     * so we can set a start point for the next harvest.  (Unix timestamp format)
-     *
-     * @var int
-     */
-    protected $endDate = 0;
-
-    /**
-     * Constructor.
-     *
-     * @param string            $target   Target directory for harvest.
-     * @param array             $settings OAI-PMH settings from oai.ini.
-     * @param \Zend\Http\Client $client   HTTP client
-     * @param string            $from     Harvest start date (omit to use
-     * last_harvest.txt)
-     * @param string            $until    Harvest end date (optional)
-     */
-    public function __construct($target, $settings, \Zend\Http\Client $client,
-        $from = null, $until = null
-    ) {
-        // Store client:
-        $this->client = $client;
-
-        // Disable SSL verification if requested:
-        if (isset($settings['sslverifypeer']) && !$settings['sslverifypeer']) {
-            $this->client->setOptions(['sslverifypeer' => false]);
-        }
-
-        // Don't time out during harvest!!
-        set_time_limit(0);
-
-        // Set up base directory for harvested files:
-        $this->setBasePath($target);
-
-        // Check if there is a file containing a start date:
-        $this->lastHarvestFile = $this->basePath . 'last_harvest.txt';
-        $this->lastStateFile = $this->basePath . 'last_state.txt';
-
-        // Set up start/end dates:
-        $this->setStartDate(empty($from) ? $this->loadLastHarvestedDate() : $from);
-        $this->setEndDate($until);
-
-        // Save configuration:
-        $this->setConfig($target, $settings);
-
-        // Load set names if we're going to need them:
-        if ($this->injectSetName) {
-            $this->loadSetNames();
-        }
-
-        // Autoload granularity if necessary:
-        if ($this->granularity == 'auto') {
-            $this->loadGranularity();
-        }
-    }
-
-    /**
-     * Set an end date for the harvest (only harvest records BEFORE this date).
-     *
-     * @param string $date End date (YYYY-MM-DD format).
-     *
-     * @return void
-     */
-    public function setEndDate($date)
-    {
-        $this->harvestEndDate = $date;
-    }
-    /**
-     * Set a start date for the harvest (only harvest records AFTER this date).
-     *
-     * @param string $date Start date (YYYY-MM-DD format).
-     *
-     * @return void
-     */
-    public function setStartDate($date)
-    {
-        $this->startDate = $date;
-    }
-
-    /**
-     * Harvest all available documents.
-     *
-     * @return void
-     */
-    public function launch()
-    {
-        // Normalize sets setting to an array:
-        $sets = (array)$this->set;
-        if (empty($sets)) {
-            $sets = [null];
-        }
-
-        // Load last state, if applicable (used to recover from server failure).
-        if (file_exists($this->lastStateFile)) {
-            $this->write("Found {$this->lastStateFile}; attempting to resume.\n");
-            list($resumeSet, $resumeToken, $this->startDate)
-                = explode("\t", file_get_contents($this->lastStateFile));
-        }
-    
-        // Loop through all of the selected sets:
-        foreach ($sets as $set) {
-            // If we're resuming and there are multiple sets, find the right one.
-            if (isset($resumeToken) && $resumeSet != $set) {
-                continue;
-            }
-
-            // If we have a token to resume from, pick up there now...
-            if (isset($resumeToken)) {
-                $token = $resumeToken;
-                unset($resumeToken);
-            } else {
-                // ...otherwise, start harvesting at the requested date:
-                $token = $this->getRecordsByDate(
-                    $this->startDate, $set, $this->harvestEndDate
-                );
-            }
-
-            // Keep harvesting as long as a resumption token is provided:
-            while ($token !== false) {
-                // Save current state in case we need to resume later:
-                file_put_contents(
-                    $this->lastStateFile, "$set\t$token\t{$this->startDate}"
-                );
-                $token = $this->getRecordsByToken($token);
-            }
-        }
-
-        // If we made it this far, all was successful, so we should clean up
-        // the "last state" file.
-        if (file_exists($this->lastStateFile)) {
-            unlink($this->lastStateFile);
-        }
-    }
-
-    /**
-     * Set up directory structure for harvesting (support method for constructor).
-     *
-     * @param string $target The OAI-PMH target directory to create.
-     *
-     * @return void
-     */
-    protected function setBasePath($target)
-    {
-        // Get the base VuFind path:
-        if (strlen(LOCAL_OVERRIDE_DIR) > 0) {
-            $home = LOCAL_OVERRIDE_DIR;
-        } else {
-            $home = realpath(APPLICATION_PATH . '/..');
-        }
-
-        // Build the full harvest path:
-        $this->basePath = $home . '/harvest/' . $target . '/';
-
-        // Create the directory if it does not already exist:
-        if (!is_dir($this->basePath)) {
-            if (!mkdir($this->basePath)) {
-                throw new \Exception(
-                    "Problem creating directory {$this->basePath}."
-                );
-            }
-        }
-    }
-
-    /**
-     * Retrieve the date from the "last harvested" file and use it as our start
-     * date if it is available.
-     *
-     * @return string
-     */
-    protected function loadLastHarvestedDate()
-    {
-        return (file_exists($this->lastHarvestFile))
-            ? trim(current(file($this->lastHarvestFile))) : null;
-    }
-
-    /**
-     * Normalize a date to a Unix timestamp.
-     *
-     * @param string $date Date (ISO-8601 or YYYY-MM-DD HH:MM:SS)
-     *
-     * @return integer     Unix timestamp (or false if $date invalid)
-     */
-    protected function normalizeDate($date)
-    {
-        // Remove timezone markers -- we don't want PHP to outsmart us by adjusting
-        // the time zone!
-        $date = str_replace(['T', 'Z'], [' ', ''], $date);
-
-        // Translate to a timestamp:
-        return strtotime($date);
-    }
-
-    /**
-     * Save a date to the "last harvested" file.
-     *
-     * @param string $date Date to save.
-     *
-     * @return void
-     */
-    protected function saveLastHarvestedDate($date)
-    {
-        file_put_contents($this->lastHarvestFile, $date);
-    }
-
-    /**
-     * Make an OAI-PMH request.  Die if there is an error; return a SimpleXML object
-     * on success.
-     *
-     * @param string $verb   OAI-PMH verb to execute.
-     * @param array  $params GET parameters for ListRecords method.
-     *
-     * @return object        SimpleXML-formatted response.
-     */
-    protected function sendRequest($verb, $params = [])
-    {
-        // Debug:
-        if ($this->verbose) {
-            $this->write(
-                "Sending request: verb = {$verb}, params = " . print_r($params, true)
-            );
-        }
-
-        // Set up retry loop:
-        while (true) {
-            // Set up the request:
-            $this->client->resetParameters();
-            $this->client->setUri($this->baseURL);
-            $this->client->setOptions(['timeout' => $this->timeout]);
-
-            // Set authentication, if necessary:
-            if ($this->httpUser && $this->httpPass) {
-                $this->client->setAuth($this->httpUser, $this->httpPass);
-            }
-
-            // Load request parameters:
-            $query = $this->client->getRequest()->getQuery();
-            $query->set('verb', $verb);
-            foreach ($params as $key => $value) {
-                $query->set($key, $value);
-            }
-
-            // Perform request and die on error:
-            $result = $this->client->setMethod('GET')->send();
-            if ($result->getStatusCode() == 503) {
-                $delayHeader = $result->getHeaders()->get('Retry-After');
-                $delay = is_object($delayHeader)
-                    ? $delayHeader->getDeltaSeconds() : 0;
-                if ($delay > 0) {
-                    if ($this->verbose) {
-                        $this->writeLine(
-                            "Received 503 response; waiting {$delay} seconds..."
-                        );
-                    }
-                    sleep($delay);
-                }
-            } else if (!$result->isSuccess()) {
-                throw new \Exception('HTTP Error ' . $result->getStatusCode());
-            } else {
-                // If we didn't get an error, we can leave the retry loop:
-                break;
-            }
-        }
-
-        // If we got this far, there was no error -- send back response.
-        return $this->processResponse($result->getBody());
-    }
-
-    /**
-     * Log a bad XML response.
-     *
-     * @param string $xml Bad XML
-     *
-     * @return void
-     */
-    protected function logBadXML($xml)
-    {
-        $file = fopen($this->basePath . $this->badXMLLog, 'a');
-        if (!$file) {
-            throw new \Exception("Problem opening {$this->badXMLLog}.");
-        }
-        fputs($file, $xml . "\n\n");
-        fclose($file);
-    }
-
-    /**
-     * Sanitize XML.
-     *
-     * @param string $xml XML to sanitize
-     *
-     * @return string
-     */
-    protected function sanitizeXML($xml)
-    {
-        // Sanitize the XML if requested:
-        $regex = '/[^\x{0009}\x{000a}\x{000d}\x{0020}-\x{D7FF}\x{E000}-\x{FFFD}]+/u';
-        $newXML = trim(preg_replace($regex, ' ', $xml, -1, $count));
-
-        if ($count > 0 && $this->badXMLLog) {
-            $this->logBadXML($xml);
-        }
-
-        return $newXML;
-    }
-
-    /**
-     * Process an OAI-PMH response into a SimpleXML object.  Die if an error is
-     * detected.
-     *
-     * @param string $xml OAI-PMH response XML.
-     *
-     * @return object     SimpleXML-formatted response.
-     */
-    protected function processResponse($xml)
-    {
-        // Sanitize if necessary:
-        if ($this->sanitize) {
-            $xml = $this->sanitizeXML($xml);
-        }
-
-        // Parse the XML (newer versions of LibXML require a special flag for
-        // large documents, and responses may be quite large):
-        $flags = LIBXML_VERSION >= 20900 ? LIBXML_PARSEHUGE : 0;
-        $result = simplexml_load_string($xml, null, $flags);
-        if (!$result) {
-            throw new \Exception("Problem loading XML: {$xml}");
-        }
-
-        // Detect errors and die if one is found:
-        if ($result->error) {
-            $attribs = $result->error->attributes();
-
-            // If this is a bad resumption token error and we're trying to
-            // restore a prior state, we should clean up.
-            if ($attribs['code'] == 'badResumptionToken'
-                && file_exists($this->lastStateFile)
-            ) {
-                unlink($this->lastStateFile);
-                throw new \Exception(
-                    "Token expired; removing last_state.txt. Please restart harvest."
-                );
-            }
-            throw new \Exception(
-                "OAI-PMH error -- code: {$attribs['code']}, " .
-                "value: {$result->error}"
-            );
-        }
-
-        // If we got this far, we have a valid response:
-        return $result;
-    }
-
-    /**
-     * Get the filename for a specific record ID.
-     *
-     * @param string $id  ID of record to save.
-     * @param string $ext File extension to use.
-     *
-     * @return string     Full path + filename.
-     */
-    protected function getFilename($id, $ext)
-    {
-        return $this->basePath . time() . '_' .
-            preg_replace('/[^\w]/', '_', $id) . '.' . $ext;
-    }
-
-    /**
-     * Create a tracking file to record the deletion of a record.
-     *
-     * @param string|array $ids ID(s) of deleted record(s).
-     *
-     * @return void
-     */
-    protected function saveDeletedRecords($ids)
-    {
-        $ids = (array)$ids; // make sure input is array format
-        $filename = $this->getFilename($ids[0], 'delete');
-        file_put_contents($filename, implode("\n", $ids));
-    }
-
-    /**
-     * Save a record to disk.
-     *
-     * @param string $id     ID of record to save.
-     * @param object $record Record to save (in SimpleXML format).
-     *
-     * @return void
-     */
-    protected function getRecordXML($id, $record)
-    {
-        if (!isset($record->metadata)) {
-            throw new \Exception("Unexpected missing record metadata.");
-        }
-
-        // Extract the actual metadata from inside the <metadata></metadata> tags;
-        // there is probably a cleaner way to do this, but this simple method avoids
-        // the complexity of dealing with namespaces in SimpleXML:
-        $xml = trim($record->metadata->asXML());
-        preg_match('/^<metadata([^\>]*)>/', $xml, $extractedNs);
-        $xml = preg_replace('/(^<metadata[^\>]*>)|(<\/metadata>$)/m', '', $xml);
-
-        // If we are supposed to inject any values, do so now inside the first
-        // tag of the file:
-        $insert = '';
-        if (!empty($this->injectId)) {
-            $insert .= "<{$this->injectId}>" . htmlspecialchars($id) .
-                "</{$this->injectId}>";
-        }
-        if (!empty($this->injectDate)) {
-            $insert .= "<{$this->injectDate}>" .
-                htmlspecialchars((string)$record->header->datestamp) .
-                "</{$this->injectDate}>";
-        }
-        if (!empty($this->injectSetSpec)) {
-            if (isset($record->header->setSpec)) {
-                foreach ($record->header->setSpec as $current) {
-                    $insert .= "<{$this->injectSetSpec}>" .
-                        htmlspecialchars((string)$current) .
-                        "</{$this->injectSetSpec}>";
-                }
-            }
-        }
-        if (!empty($this->injectSetName)) {
-            if (isset($record->header->setSpec)) {
-                foreach ($record->header->setSpec as $current) {
-                    $name = $this->setNames[(string)$current];
-                    $insert .= "<{$this->injectSetName}>" .
-                        htmlspecialchars($name) .
-                        "</{$this->injectSetName}>";
-                }
-            }
-        }
-        if (!empty($this->injectHeaderElements)) {
-            foreach ($this->injectHeaderElements as $element) {
-                if (isset($record->header->$element)) {
-                    $insert .= $record->header->$element->asXML();
-                }
-            }
-        }
-        if (!empty($insert)) {
-            $xml = preg_replace('/>/', '>' . $insert, $xml, 1);
-        }
-        $xml = $this->fixNamespaces(
-            $xml, $record->getDocNamespaces(),
-            isset($extractedNs[1]) ? $extractedNs[1] : ''
-        );
-
-        return trim($xml);
-    }
-
-    /**
-     * Save a record to disk.
-     *
-     * @param string $id  Record ID to use for filename generation.
-     * @param string $xml XML to save.
-     *
-     * @return void
-     */
-    protected function saveFile($id, $xml)
-    {
-        // Save our XML:
-        file_put_contents($this->getFilename($id, 'xml'), trim($xml));
-    }
-
-    /**
-     * Support method for saveRecord() -- fix namespaces in the top tag of the XML
-     * document to compensate for bugs in the SimpleXML library.
-     *
-     * @param string $xml  XML document to clean up
-     * @param array  $ns   Namespaces to check
-     * @param string $attr Attributes extracted from the <metadata> tag
-     *
-     * @return string
-     */
-    protected function fixNamespaces($xml, $ns, $attr = '')
-    {
-        foreach ($ns as $key => $val) {
-            if (!empty($key)
-                && strstr($xml, $key . ':') && !strstr($xml, 'xmlns:' . $key)
-                && !strstr($attr, 'xmlns:' . $key)
-            ) {
-                $attr .= ' xmlns:' . $key . '="' . $val . '"';
-            }
-        }
-        if (!empty($attr)) {
-            $xml = preg_replace('/>/', $attr . '>', $xml, 1);
-        }
-        return $xml;
-    }
-
-    /**
-     * Load date granularity from the server.
-     *
-     * @return void
-     */
-    protected function loadGranularity()
-    {
-        $this->write("Autodetecting date granularity... ");
-        $response = $this->sendRequest('Identify');
-        $this->granularity = (string)$response->Identify->granularity;
-        $this->writeLine("found {$this->granularity}.");
-    }
-
-    /**
-     * Load set list from the server.
-     *
-     * @return void
-     */
-    protected function loadSetNames()
-    {
-        $this->write("Loading set list... ");
-
-        // On the first pass through the following loop, we want to get the
-        // first page of sets without using a resumption token:
-        $params = [];
-
-        // Grab set information until we have it all (at which point we will
-        // break out of this otherwise-infinite loop):
-        while (true) {
-            // Process current page of results:
-            $response = $this->sendRequest('ListSets', $params);
-            if (isset($response->ListSets->set)) {
-                foreach ($response->ListSets->set as $current) {
-                    $spec = (string)$current->setSpec;
-                    $name = (string)$current->setName;
-                    if (!empty($spec)) {
-                        $this->setNames[$spec] = $name;
-                    }
-                }
-            }
-
-            // Is there a resumption token?  If so, continue looping; if not,
-            // we're done!
-            if (isset($response->ListSets->resumptionToken)
-                && !empty($response->ListSets->resumptionToken)
-            ) {
-                $params['resumptionToken']
-                    = (string)$response->ListSets->resumptionToken;
-            } else {
-                $this->writeLine("found " . count($this->setNames));
-                return;
-            }
-        }
-    }
-
-    /**
-     * Extract the ID from a record object (support method for processRecords()).
-     *
-     * @param object $record SimpleXML record.
-     *
-     * @return string        The ID value.
-     */
-    protected function extractID($record)
-    {
-        // Normalize to string:
-        $id = (string)$record->header->identifier;
-
-        // Strip prefix if found:
-        if (substr($id, 0, strlen($this->idPrefix)) == $this->idPrefix) {
-            $id = substr($id, strlen($this->idPrefix));
-        }
-
-        // Apply regular expression matching:
-        if (!empty($this->idSearch)) {
-            $id = preg_replace($this->idSearch, $this->idReplace, $id);
-        }
-
-        // Return final value:
-        return $id;
-    }
-
-    /**
-     * Save harvested records to disk and track the end date.
-     *
-     * @param object $records SimpleXML records.
-     *
-     * @return void
-     */
-    protected function processRecords($records)
-    {
-        $this->writeLine('Processing ' . count($records) . " records...");
-
-        // Array for tracking successfully harvested IDs:
-        $harvestedIds = [];
-
-        // Array for tracking deleted IDs and string for tracking inner HTML
-        // (both of these variables are used only when in 'combineRecords' mode):
-        $deletedIds = [];
-        $innerXML = '';
-
-        // Loop through the records:
-        foreach ($records as $record) {
-            // Die if the record is missing its header:
-            if (empty($record->header)) {
-                throw new \Exception("Unexpected missing record header.");
-            }
-
-            // Get the ID of the current record:
-            $id = $this->extractID($record);
-
-            // Save the current record, either as a deleted or as a regular file:
-            $attribs = $record->header->attributes();
-            if (strtolower($attribs['status']) == 'deleted') {
-                if ($this->combineRecords) {
-                    $deletedIds[] = $id;
-                } else {
-                    $this->saveDeletedRecords($id);
-                }
-            } else {
-                if ($this->combineRecords) {
-                    $innerXML .= $this->getRecordXML($id, $record);
-                } else {
-                    $this->saveFile($id, $this->getRecordXML($id, $record));
-                }
-                $harvestedIds[] = $id;
-            }
-
-            // If the current record's date is newer than the previous end date,
-            // remember it for future reference:
-            $date = $this->normalizeDate($record->header->datestamp);
-            if ($date && $date > $this->endDate) {
-                $this->endDate = $date;
-            }
-        }
-
-        if ($this->combineRecords) {
-            if (!empty($harvestedIds)) {
-                $this->saveFile($harvestedIds[0], $this->getCombinedXML($innerXML));
-            }
-
-            if (!empty($deletedIds)) {
-                $this->saveDeletedRecords($deletedIds);
-            }
-        }
-
-        // Do we have IDs to log and a log filename?  If so, log them:
-        if (!empty($this->harvestedIdLog) && !empty($harvestedIds)) {
-            $file = fopen($this->basePath . $this->harvestedIdLog, 'a');
-            if (!$file) {
-                throw new \Exception("Problem opening {$this->harvestedIdLog}.");
-            }
-            fputs($file, implode(PHP_EOL, $harvestedIds));
-            fclose($file);
-        }
-    }
-
-    /**
-     * Support method for building combined XML document.
-     *
-     * @param string $innerXML XML for inside of document.
-     *
-     * @return string
-     */
-    protected function getCombinedXML($innerXML)
-    {
-        // Determine start and end tags from configuration:
-        $start = $this->combineRecordsTag;
-        $tmp = explode(' ', $start);
-        $end = '</' . str_replace(['<', '>'], '', $tmp[0]) . '>';
-
-        // Assemble the document:
-        return $start . $innerXML . $end;
-    }
-
-    /**
-     * Harvest records using OAI-PMH.
-     *
-     * @param array $params GET parameters for ListRecords method.
-     *
-     * @return mixed        Resumption token if provided, false if finished
-     */
-    protected function getRecords($params)
-    {
-        // Make the OAI-PMH request:
-        $response = $this->sendRequest('ListRecords', $params);
-
-        // Save the records from the response:
-        if ($response->ListRecords->record) {
-            $this->processRecords($response->ListRecords->record);
-        }
-
-        // If we have a resumption token, keep going; otherwise, we're done -- save
-        // the end date.
-        if (isset($response->ListRecords->resumptionToken)
-            && !empty($response->ListRecords->resumptionToken)
-        ) {
-            return $response->ListRecords->resumptionToken;
-        } else if ($this->endDate > 0) {
-            $dateFormat = ($this->granularity == 'YYYY-MM-DD') ?
-                'Y-m-d' : 'Y-m-d\TH:i:s\Z';
-            $this->saveLastHarvestedDate(date($dateFormat, $this->endDate));
-        }
-        return false;
-    }
-
-    /**
-     * Harvest records via OAI-PMH using date and set.
-     *
-     * @param string $from  Harvest start date (null for no specific start).
-     * @param string $set   Set to harvest (null for all records).
-     * @param string $until Harvest end date (null for no specific end).
-     *
-     * @return mixed        Resumption token if provided, false if finished
-     */
-    protected function getRecordsByDate($from = null, $set = null, $until = null)
-    {
-        $params = ['metadataPrefix' => $this->metadataPrefix];
-        if (!empty($from)) {
-            $params['from'] = $from;
-        }
-        if (!empty($set)) {
-            $params['set'] = $set;
-        }
-        if (!empty($until)) {
-            $params['until'] = $until;
-        }
-        return $this->getRecords($params);
-    }
-
-    /**
-     * Harvest records via OAI-PMH using resumption token.
-     *
-     * @param string $token Resumption token.
-     *
-     * @return mixed        Resumption token if provided, false if finished
-     */
-    protected function getRecordsByToken($token)
-    {
-        return $this->getRecords(['resumptionToken' => (string)$token]);
-    }
-
-    /**
-     * Set configuration (support method for constructor).
-     *
-     * @param string $target   Target directory for harvest.
-     * @param array  $settings Configuration
-     *
-     * @return void
-     */
-    protected function setConfig($target, $settings)
-    {
-        // Set up base URL:
-        if (empty($settings['url'])) {
-            throw new \Exception("Missing base URL for {$target}.");
-        }
-        $this->baseURL = $settings['url'];
-
-        // Settings that may be mapped directly from $settings to class properties:
-        $mappableSettings = [
-            'set', 'metadataPrefix', 'idPrefix', 'idSearch', 'idReplace',
-            'harvestedIdLog', 'injectId', 'injectSetSpec', 'injectSetName',
-            'injectDate', 'injectHeaderElements', 'verbose', 'sanitize', 'badXMLLog',
-            'httpUser', 'httpPass', 'timeout', 'combineRecords', 'combineRecordsTag',
-        ];
-        foreach ($mappableSettings as $current) {
-            if (isset($settings[$current])) {
-                $this->$current = $settings[$current];
-            }
-        }
-
-        // Special case: $settings value does not match property value (for
-        // readability):
-        if (isset($settings['dateGranularity'])) {
-            $this->granularity = $settings['dateGranularity'];
-        }
-
-        // Normalize injectHeaderElements to an array:
-        if (!is_array($this->injectHeaderElements)) {
-            $this->injectHeaderElements = [$this->injectHeaderElements];
-        }
-    }
-
-    /**
-     * Write a string to the Console.
-     *
-     * @param string $str String to write.
-     *
-     * @return void
-     */
-    protected function write($str)
-    {
-        // Bypass output when testing:
-        if (defined('VUFIND_PHPUNIT_RUNNING')) {
-            return;
-        }
-        Console::write($str);
-    }
-
-    /**
-     * Write a string w/newline to the Console.
-     *
-     * @param string $str String to write.
-     *
-     * @return void
-     */
-    protected function writeLine($str)
-    {
-        // Bypass output when testing:
-        if (defined('VUFIND_PHPUNIT_RUNNING')) {
-            return;
-        }
-        Console::writeLine($str);
-    }
-}
diff --git a/module/VuFind/src/VuFind/Hierarchy/Driver/AbstractBase.php b/module/VuFind/src/VuFind/Hierarchy/Driver/AbstractBase.php
index 493bb29f0d99218055074aa79ee69d47dbe9738c..ebaefd8288fefcd39e21882e123932b6b1db9860 100644
--- a/module/VuFind/src/VuFind/Hierarchy/Driver/AbstractBase.php
+++ b/module/VuFind/src/VuFind/Hierarchy/Driver/AbstractBase.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Hierarchy
diff --git a/module/VuFind/src/VuFind/Hierarchy/Driver/ConfigurationBased.php b/module/VuFind/src/VuFind/Hierarchy/Driver/ConfigurationBased.php
index 3a926cad6a305a7d996a8b0ac7c0e1097cae3c01..e2415e17e52d95c55a27adbdd9e9ff5f6915df7b 100644
--- a/module/VuFind/src/VuFind/Hierarchy/Driver/ConfigurationBased.php
+++ b/module/VuFind/src/VuFind/Hierarchy/Driver/ConfigurationBased.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Hierarchy_Drivers
diff --git a/module/VuFind/src/VuFind/Hierarchy/Driver/Factory.php b/module/VuFind/src/VuFind/Hierarchy/Driver/Factory.php
index 7d73ed0b7875c714f7b774afde1f91e635547552..0fc036ab206c84b163ba37fdffc4d834966dd581 100644
--- a/module/VuFind/src/VuFind/Hierarchy/Driver/Factory.php
+++ b/module/VuFind/src/VuFind/Hierarchy/Driver/Factory.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Hierarchy_Drivers
diff --git a/module/VuFind/src/VuFind/Hierarchy/Driver/PluginManager.php b/module/VuFind/src/VuFind/Hierarchy/Driver/PluginManager.php
index 89bfbe2637091e5d4a5f2e4ce973b6569c3a1061..53fbe1ef4bb1668135193af6584894b16255a72c 100644
--- a/module/VuFind/src/VuFind/Hierarchy/Driver/PluginManager.php
+++ b/module/VuFind/src/VuFind/Hierarchy/Driver/PluginManager.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Hierarchy_Drivers
diff --git a/module/VuFind/src/VuFind/Hierarchy/TreeDataFormatter/AbstractBase.php b/module/VuFind/src/VuFind/Hierarchy/TreeDataFormatter/AbstractBase.php
index 942870fb5b92a50e1a31082d540a04d462449fb1..e396315a1e8dee630a8a833558131bf87599a84e 100644
--- a/module/VuFind/src/VuFind/Hierarchy/TreeDataFormatter/AbstractBase.php
+++ b/module/VuFind/src/VuFind/Hierarchy/TreeDataFormatter/AbstractBase.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  HierarchyTree_DataFormatter
diff --git a/module/VuFind/src/VuFind/Hierarchy/TreeDataFormatter/Json.php b/module/VuFind/src/VuFind/Hierarchy/TreeDataFormatter/Json.php
index f576db3faaee12434fbf23dbae231c548a49f3b1..5f2645b2c0bcc0c312436d11634fc6ac450cb053 100644
--- a/module/VuFind/src/VuFind/Hierarchy/TreeDataFormatter/Json.php
+++ b/module/VuFind/src/VuFind/Hierarchy/TreeDataFormatter/Json.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  HierarchyTree_DataFormatter
diff --git a/module/VuFind/src/VuFind/Hierarchy/TreeDataFormatter/PluginManager.php b/module/VuFind/src/VuFind/Hierarchy/TreeDataFormatter/PluginManager.php
index 8d6fe543e971e7657bd59b06114afa2a94235fb4..f244f866fa04dbaec4c2aceafc813475eeb9c11c 100644
--- a/module/VuFind/src/VuFind/Hierarchy/TreeDataFormatter/PluginManager.php
+++ b/module/VuFind/src/VuFind/Hierarchy/TreeDataFormatter/PluginManager.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  HierarchyTree_DataSource
diff --git a/module/VuFind/src/VuFind/Hierarchy/TreeDataFormatter/Xml.php b/module/VuFind/src/VuFind/Hierarchy/TreeDataFormatter/Xml.php
index 3793596a8b0169a2ccd6dcf43250f23930278249..2aecded13bf0b0f6a9d820f9e31788bc7c84a02f 100644
--- a/module/VuFind/src/VuFind/Hierarchy/TreeDataFormatter/Xml.php
+++ b/module/VuFind/src/VuFind/Hierarchy/TreeDataFormatter/Xml.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  HierarchyTree_DataFormatter
diff --git a/module/VuFind/src/VuFind/Hierarchy/TreeDataSource/AbstractBase.php b/module/VuFind/src/VuFind/Hierarchy/TreeDataSource/AbstractBase.php
index 96460fabd6530fd34a09ef0a67a196f623d71f41..8036b03fcfce05865d7ff6dca7c7089afbeb9727 100644
--- a/module/VuFind/src/VuFind/Hierarchy/TreeDataSource/AbstractBase.php
+++ b/module/VuFind/src/VuFind/Hierarchy/TreeDataSource/AbstractBase.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  HierarchyTree_DataSource
diff --git a/module/VuFind/src/VuFind/Hierarchy/TreeDataSource/Factory.php b/module/VuFind/src/VuFind/Hierarchy/TreeDataSource/Factory.php
index e083b3441999c56fd156455a4432965d49274424..96581b5b01b1d1719a88b1d384deb9000b8861b9 100644
--- a/module/VuFind/src/VuFind/Hierarchy/TreeDataSource/Factory.php
+++ b/module/VuFind/src/VuFind/Hierarchy/TreeDataSource/Factory.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  HierarchyTree_DataSource
diff --git a/module/VuFind/src/VuFind/Hierarchy/TreeDataSource/PluginManager.php b/module/VuFind/src/VuFind/Hierarchy/TreeDataSource/PluginManager.php
index 459988a0d4edeb24eee89cb7c5762f6895e29daf..e96325f81bddbbe806b31977bc91dc533a54f7d8 100644
--- a/module/VuFind/src/VuFind/Hierarchy/TreeDataSource/PluginManager.php
+++ b/module/VuFind/src/VuFind/Hierarchy/TreeDataSource/PluginManager.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  HierarchyTree_DataSource
diff --git a/module/VuFind/src/VuFind/Hierarchy/TreeDataSource/Solr.php b/module/VuFind/src/VuFind/Hierarchy/TreeDataSource/Solr.php
index cc6b82ef680c4a3705e339934262aa7d3586415f..3b6c965fea97bc7672be5869637ad08d4a18b348 100644
--- a/module/VuFind/src/VuFind/Hierarchy/TreeDataSource/Solr.php
+++ b/module/VuFind/src/VuFind/Hierarchy/TreeDataSource/Solr.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  HierarchyTree_DataSource
diff --git a/module/VuFind/src/VuFind/Hierarchy/TreeDataSource/XMLFile.php b/module/VuFind/src/VuFind/Hierarchy/TreeDataSource/XMLFile.php
index 4367c84c2ecd70002a164ea460e67aecbf10871d..f6b40040d570e8bcc0af93c7273955b02dcb0e01 100644
--- a/module/VuFind/src/VuFind/Hierarchy/TreeDataSource/XMLFile.php
+++ b/module/VuFind/src/VuFind/Hierarchy/TreeDataSource/XMLFile.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  HierarchyTree_DataSource
diff --git a/module/VuFind/src/VuFind/Hierarchy/TreeRenderer/AbstractBase.php b/module/VuFind/src/VuFind/Hierarchy/TreeRenderer/AbstractBase.php
index 62604bc76543984209c03b2d30ed4726e88b66c6..18361597ab216003701cae3e056f122224d8b890 100644
--- a/module/VuFind/src/VuFind/Hierarchy/TreeRenderer/AbstractBase.php
+++ b/module/VuFind/src/VuFind/Hierarchy/TreeRenderer/AbstractBase.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  HierarchyTree_Renderer
diff --git a/module/VuFind/src/VuFind/Hierarchy/TreeRenderer/Factory.php b/module/VuFind/src/VuFind/Hierarchy/TreeRenderer/Factory.php
index 857ca5ebd61dc085b3fa4390207fd0e750480315..650de94b91ded74753aa4192df2972ccd28122e5 100644
--- a/module/VuFind/src/VuFind/Hierarchy/TreeRenderer/Factory.php
+++ b/module/VuFind/src/VuFind/Hierarchy/TreeRenderer/Factory.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  HierarchyTree_Renderer
diff --git a/module/VuFind/src/VuFind/Hierarchy/TreeRenderer/JSTree.php b/module/VuFind/src/VuFind/Hierarchy/TreeRenderer/JSTree.php
index 34329bffaadd1de3e76ca7086c36a45fbf8c064e..07ddc43a75ff590207f3d579cebec625275c1965 100644
--- a/module/VuFind/src/VuFind/Hierarchy/TreeRenderer/JSTree.php
+++ b/module/VuFind/src/VuFind/Hierarchy/TreeRenderer/JSTree.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  HierarchyTree_Renderer
diff --git a/module/VuFind/src/VuFind/Hierarchy/TreeRenderer/PluginManager.php b/module/VuFind/src/VuFind/Hierarchy/TreeRenderer/PluginManager.php
index 1e7ec4aad4800c19121865015f8826e722cb4a6c..4ea337b134451953328cf07d3192ac218bf92bb0 100644
--- a/module/VuFind/src/VuFind/Hierarchy/TreeRenderer/PluginManager.php
+++ b/module/VuFind/src/VuFind/Hierarchy/TreeRenderer/PluginManager.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  HierarchyTree_Renderer
diff --git a/module/VuFind/src/VuFind/I18n/ExtendedIniNormalizer.php b/module/VuFind/src/VuFind/I18n/ExtendedIniNormalizer.php
index bb19e0bc5f26c81553da5c2450d4686e8fa83448..951d5795a9eddbb252672bfc0579f2bf1358ee25 100644
--- a/module/VuFind/src/VuFind/I18n/ExtendedIniNormalizer.php
+++ b/module/VuFind/src/VuFind/I18n/ExtendedIniNormalizer.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Translator
diff --git a/module/VuFind/src/VuFind/I18n/TranslatableString.php b/module/VuFind/src/VuFind/I18n/TranslatableString.php
index 738e6f932b8ede2b04d189c4bea4f7a1ac7497a8..51db34fa907d4a3c008425ec2d090091d1562de0 100644
--- a/module/VuFind/src/VuFind/I18n/TranslatableString.php
+++ b/module/VuFind/src/VuFind/I18n/TranslatableString.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Translator
diff --git a/module/VuFind/src/VuFind/I18n/TranslatableStringInterface.php b/module/VuFind/src/VuFind/I18n/TranslatableStringInterface.php
index 58fdd5f6f9ec4a2521bcbf00e1b4b375ebd7519e..9445e85f66f2418a5f728c41c8012cce0cac6e8b 100644
--- a/module/VuFind/src/VuFind/I18n/TranslatableStringInterface.php
+++ b/module/VuFind/src/VuFind/I18n/TranslatableStringInterface.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Translator
diff --git a/module/VuFind/src/VuFind/I18n/Translator/Loader/ExtendedIni.php b/module/VuFind/src/VuFind/I18n/Translator/Loader/ExtendedIni.php
index b0b72e0bf192fc0446f6549cefa3856eb1bf0bdd..f5e43ecde4a0199ebed09344b054d6d21a638a4e 100644
--- a/module/VuFind/src/VuFind/I18n/Translator/Loader/ExtendedIni.php
+++ b/module/VuFind/src/VuFind/I18n/Translator/Loader/ExtendedIni.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Translator
diff --git a/module/VuFind/src/VuFind/I18n/Translator/Loader/ExtendedIniReader.php b/module/VuFind/src/VuFind/I18n/Translator/Loader/ExtendedIniReader.php
index d93371440bd09e1b1f0a672d97fe23876a01c5b8..93679eecda8082fb9d3c5e4dfb6e400ecbb7504e 100644
--- a/module/VuFind/src/VuFind/I18n/Translator/Loader/ExtendedIniReader.php
+++ b/module/VuFind/src/VuFind/I18n/Translator/Loader/ExtendedIniReader.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Translator
diff --git a/module/VuFind/src/VuFind/I18n/Translator/TranslatorAwareInterface.php b/module/VuFind/src/VuFind/I18n/Translator/TranslatorAwareInterface.php
index 3a0cf95fb531a753a93a31bd03ecdb9f734a7857..84957e5bac85041612fe3297bae2cf9ef00fad19 100644
--- a/module/VuFind/src/VuFind/I18n/Translator/TranslatorAwareInterface.php
+++ b/module/VuFind/src/VuFind/I18n/Translator/TranslatorAwareInterface.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Translator
diff --git a/module/VuFind/src/VuFind/I18n/Translator/TranslatorAwareTrait.php b/module/VuFind/src/VuFind/I18n/Translator/TranslatorAwareTrait.php
index b99d2d6986a8445bef0c6eef4531ded082583883..faff89552ef9f5bcb952c335f326aca7ee547c93 100644
--- a/module/VuFind/src/VuFind/I18n/Translator/TranslatorAwareTrait.php
+++ b/module/VuFind/src/VuFind/I18n/Translator/TranslatorAwareTrait.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Translator
diff --git a/module/VuFind/src/VuFind/ILS/Connection.php b/module/VuFind/src/VuFind/ILS/Connection.php
index f2963c352182ba7be0aff8ed8133d3233f26999c..8cc116c4add3b4b58247d0bea0324ddee43d4c18 100644
--- a/module/VuFind/src/VuFind/ILS/Connection.php
+++ b/module/VuFind/src/VuFind/ILS/Connection.php
@@ -20,7 +20,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  ILS_Drivers
diff --git a/module/VuFind/src/VuFind/ILS/Driver/AbstractBase.php b/module/VuFind/src/VuFind/ILS/Driver/AbstractBase.php
index 563afe39c4d0c94ead7b07ed19881e0d70ab556f..683c70cdba23cdfcc1f9ea4d7e95a0bc10a5f5e8 100644
--- a/module/VuFind/src/VuFind/ILS/Driver/AbstractBase.php
+++ b/module/VuFind/src/VuFind/ILS/Driver/AbstractBase.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  ILS_Drivers
@@ -26,7 +26,8 @@
  * @link     https://vufind.org/wiki/development:plugins:ils_drivers Wiki
  */
 namespace VuFind\ILS\Driver;
-use Zend\Cache\Storage\StorageInterface;
+use Zend\Cache\Storage\StorageInterface,
+    VuFind\Cache\KeyGeneratorTrait;
 
 /**
  * Default ILS driver base class.
@@ -41,6 +42,8 @@ use Zend\Cache\Storage\StorageInterface;
  */
 abstract class AbstractBase implements DriverInterface
 {
+    use KeyGeneratorTrait;
+
     /**
      * Cache for storing ILS data temporarily (e.g. patron blocks)
      *
@@ -89,21 +92,6 @@ abstract class AbstractBase implements DriverInterface
         $this->config = $config;
     }
 
-    /**
-     * Add instance-specific context to a cache key suffix to ensure that
-     * multiple drivers don't accidentally share values in the cache.
-     * This implementation works anywhere but can be overridden with something more
-     * performant.
-     *
-     * @param string $key Cache key suffix
-     *
-     * @return string
-     */
-    protected function formatCacheKey($key)
-    {
-        return get_class($this) . '-' . md5(json_encode($this->config) . "|$key");
-    }
-
     /**
      * Helper function for fetching cached data.
      * Data is cached for up to $this->cacheLifetime seconds so that it would be
@@ -120,7 +108,7 @@ abstract class AbstractBase implements DriverInterface
             return null;
         }
 
-        $fullKey = $this->formatCacheKey($key);
+        $fullKey = $this->getCacheKey($key);
         $item = $this->cache->getItem($fullKey);
         if (null !== $item) {
             // Return value if still valid:
@@ -153,6 +141,22 @@ abstract class AbstractBase implements DriverInterface
             'time' => time(),
             'entry' => $entry
         ];
-        $this->cache->setItem($this->formatCacheKey($key), $item);
+        $this->cache->setItem($this->getCacheKey($key), $item);
+    }
+
+    /**
+     * Helper function for removing cached data.
+     *
+     * @param string $key Cache entry key
+     *
+     * @return void
+     */
+    protected function removeCachedData($key)
+    {
+        // Don't write to cache if we don't have a cache!
+        if (null === $this->cache) {
+            return;
+        }
+        $this->cache->removeItem($this->getCacheKey($key));
     }
 }
diff --git a/module/VuFind/src/VuFind/ILS/Driver/Aleph.php b/module/VuFind/src/VuFind/ILS/Driver/Aleph.php
index bb446f72ca8e920ffde21e6c6215d8592d33be61..fe69cf8b4c78a0d8bae4b7e5693f82a38bffc225 100644
--- a/module/VuFind/src/VuFind/ILS/Driver/Aleph.php
+++ b/module/VuFind/src/VuFind/ILS/Driver/Aleph.php
@@ -23,7 +23,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  ILS_Drivers
diff --git a/module/VuFind/src/VuFind/ILS/Driver/Amicus.php b/module/VuFind/src/VuFind/ILS/Driver/Amicus.php
index a363f2e4bdfd1f79220b24b67b90c78f7fa6838a..3f2adfe9eb8b227824157c110ade6f2b796320c0 100644
--- a/module/VuFind/src/VuFind/ILS/Driver/Amicus.php
+++ b/module/VuFind/src/VuFind/ILS/Driver/Amicus.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  ILS_Drivers
@@ -178,9 +178,9 @@ class Amicus extends AbstractBase implements TranslatorAwareInterface
      * If there is no on loan items it returns 0.
      * Used in getHolding and getStatus functions
      *
-     * @param integer $copyId The copy id number to check.
+     * @param int $copyId The copy id number to check.
      *
-     * @return integer             Number of on loan items.
+     * @return int        Number of on loan items.
      */
     protected function sacaStatus($copyId)
     {
@@ -216,9 +216,9 @@ class Amicus extends AbstractBase implements TranslatorAwareInterface
      * If the difference is greater than 50 days it will return one special message
      * If not it returns the due date
      *
-     * @param integer $copyId The copy id number to check.
+     * @param int $copyId The copy id number to check.
      *
-     * @return string             String with special message or due date.
+     * @return string     String with special message or due date.
      */
     protected function sacaFecha($copyId)
     {
@@ -249,9 +249,9 @@ class Amicus extends AbstractBase implements TranslatorAwareInterface
      * Function that returns the numbers of holds for a copy id number given.
      * If there is no holds it returns 0.
      *
-     * @param integer $holdingId The copy id number to check.
+     * @param int $holdingId The copy id number to check.
      *
-     * @return integer             Integer with the number of holds.
+     * @return int           Integer with the number of holds.
      */
     protected function sacaReservas($holdingId)
     {
diff --git a/module/VuFind/src/VuFind/ILS/Driver/ClaviusSQL.php b/module/VuFind/src/VuFind/ILS/Driver/ClaviusSQL.php
index 44ad5f9a88e96f4d806170b9c2bb7ecd3a3c663a..e0918ccba49c8654fd123b8ffff1fe9d24cb7752 100644
--- a/module/VuFind/src/VuFind/ILS/Driver/ClaviusSQL.php
+++ b/module/VuFind/src/VuFind/ILS/Driver/ClaviusSQL.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  ILS_Drivers
@@ -84,7 +84,7 @@ class ClaviusSQL extends AbstractBase
     /**
      * How many days is new document hidden in catalog
      *
-     * @var integer
+     * @var int
      */
     protected $hideNewItemsDays;
 
diff --git a/module/VuFind/src/VuFind/ILS/Driver/DAIA.php b/module/VuFind/src/VuFind/ILS/Driver/DAIA.php
index f76e83471926585fe0968354b92dc3a39b89d293..784cdaee4db7b53a7f2f43fa82e7ca29389efd4c 100644
--- a/module/VuFind/src/VuFind/ILS/Driver/DAIA.php
+++ b/module/VuFind/src/VuFind/ILS/Driver/DAIA.php
@@ -20,7 +20,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  ILS_Drivers
@@ -59,6 +59,20 @@ class DAIA extends AbstractBase implements
      */
     protected $baseUrl;
 
+    /**
+     * Timeout in seconds to be used for DAIA http requests
+     *
+     * @var string
+     */
+    protected $daiaTimeout = null;
+
+    /**
+     * Flag to switch on/off caching for DAIA items
+     *
+     * @var bool
+     */
+    protected $daiaCacheEnabled = false;
+
     /**
      * DAIA query identifier prefix
      *
@@ -76,7 +90,7 @@ class DAIA extends AbstractBase implements
     /**
      * Flag to enable multiple DAIA-queries
      *
-     * @var boolean
+     * @var bool
      */
     protected $multiQuery = false;
 
@@ -135,6 +149,10 @@ class DAIA extends AbstractBase implements
         } else {
             throw new ILSException('DAIA/baseUrl configuration needs to be set.');
         }
+        // use DAIA specific timeout setting for http requests if configured
+        if ((isset($this->config['DAIA']['timeout']))) {
+            $this->daiaTimeout = $this->config['DAIA']['timeout'];
+        }
         if (isset($this->config['DAIA']['daiaResponseFormat'])) {
             $this->daiaResponseFormat = strtolower(
                 $this->config['DAIA']['daiaResponseFormat']
@@ -159,6 +177,35 @@ class DAIA extends AbstractBase implements
         } else {
             $this->debug('No ContentTypes for response defined. Accepting any.');
         }
+        if (isset($this->config['DAIA']['daiaCache'])) {
+            $this->daiaCacheEnabled = $this->config['DAIA']['daiaCache'];
+        } else {
+            $this->debug('Caching not enabled, disabling it by default.');
+        }
+        if (isset($this->config['General'])
+            && isset($this->config['General']['cacheLifetime'])
+        ) {
+            $this->cacheLifetime = $this->config['General']['cacheLifetime'];
+        } else {
+            $this->debug(
+                'Cache lifetime not set, using VuFind\ILS\Driver\AbstractBase ' .
+                'default value.'
+            );
+        }
+    }
+
+    /**
+     * DAIA specific override of method to ensure uniform cache keys for cached
+     * VuFind objects.
+     *
+     * @param string|null $suffix Optional suffix that will get appended to the
+     * object class name calling getCacheKey()
+     *
+     * @return string
+     */
+    protected function getCacheKey($suffix = null)
+    {
+        return parent::getCacheKey(md5($this->baseURL) . $suffix);
     }
 
     /**
@@ -208,6 +255,15 @@ class DAIA extends AbstractBase implements
      */
     public function getStatus($id)
     {
+        // check ids for existing availability data in cache and skip these ids
+        if ($this->daiaCacheEnabled
+            && $item = $this->getCachedData($this->generateURI($id))
+        ) {
+            if ($item != null) {
+                return $item;
+            }
+        }
+
         // let's retrieve the DAIA document by URI
         try {
             $rawResult = $this->doHTTPRequest($this->generateURI($id));
@@ -216,7 +272,12 @@ class DAIA extends AbstractBase implements
             $doc = $this->extractDaiaDoc($id, $rawResult);
             if (!is_null($doc)) {
                 // parse the extracted DAIA document and return the status info
-                return $this->parseDaiaDoc($id, $doc);
+                $data = $this->parseDaiaDoc($id, $doc);
+                // cache the status information
+                if ($this->daiaCacheEnabled) {
+                    $this->putCachedData($this->generateURI($id), $data);
+                }
+                return $data;
             }
         } catch (ILSException $e) {
             $this->debug($e->getMessage());
@@ -247,41 +308,66 @@ class DAIA extends AbstractBase implements
     {
         $status = [];
 
-        try {
-            if ($this->multiQuery) {
-                // perform one DAIA query with multiple URIs
-                $rawResult = $this
-                    ->doHTTPRequest($this->generateMultiURIs($ids));
-                // the id used in VuFind can differ from the document-URI
-                // (depending on how the URI is generated)
-                foreach ($ids as $id) {
-                    // it is assumed that each DAIA document has a unique URI,
-                    // so get the document with the corresponding id
-                    $doc = $this->extractDaiaDoc($id, $rawResult);
-                    if (!is_null($doc)) {
-                        // a document with the corresponding id exists, which
-                        // means we got status information for that record
-                        $status[] = $this->parseDaiaDoc($id, $doc);
-                    }
-                    unset($doc);
+        // check cache for given ids and skip these ids if availability data is found
+        foreach ($ids as $key => $id) {
+            if ($this->daiaCacheEnabled
+                && $item = $this->getCachedData($this->generateURI($id))
+            ) {
+                if ($item != null) {
+                    $status[] = $item;
+                    unset($ids[$key]);
                 }
-            } else {
-                // multiQuery is not supported, so retrieve DAIA documents one by
-                // one
-                foreach ($ids as $id) {
-                    $rawResult = $this->doHTTPRequest($this->generateURI($id));
-                    // extract the DAIA document for the current id from the
-                    // HTTPRequest's result
-                    $doc = $this->extractDaiaDoc($id, $rawResult);
-                    if (!is_null($doc)) {
-                        // parse the extracted DAIA document and save the status
-                        // info
-                        $status[] = $this->parseDaiaDoc($id, $doc);
+            }
+        }
+
+        // only query DAIA service if we have some ids left
+        if (count($ids) > 0) {
+            try {
+                if ($this->multiQuery) {
+                    // perform one DAIA query with multiple URIs
+                    $rawResult = $this
+                        ->doHTTPRequest($this->generateMultiURIs($ids));
+                    // the id used in VuFind can differ from the document-URI
+                    // (depending on how the URI is generated)
+                    foreach ($ids as $id) {
+                        // it is assumed that each DAIA document has a unique URI,
+                        // so get the document with the corresponding id
+                        $doc = $this->extractDaiaDoc($id, $rawResult);
+                        if (!is_null($doc)) {
+                            // a document with the corresponding id exists, which
+                            // means we got status information for that record
+                            $data = $this->parseDaiaDoc($id, $doc);
+                            // cache the status information
+                            if ($this->daiaCacheEnabled) {
+                                $this->putCachedData($this->generateURI($id), $data);
+                            }
+                            $status[] = $data;
+                        }
+                        unset($doc);
+                    }
+                } else {
+                    // multiQuery is not supported, so retrieve DAIA documents one by
+                    // one
+                    foreach ($ids as $id) {
+                        $rawResult = $this->doHTTPRequest($this->generateURI($id));
+                        // extract the DAIA document for the current id from the
+                        // HTTPRequest's result
+                        $doc = $this->extractDaiaDoc($id, $rawResult);
+                        if (!is_null($doc)) {
+                            // parse the extracted DAIA document and save the status
+                            // info
+                            $data = $this->parseDaiaDoc($id, $doc);
+                            // cache the status information
+                            if ($this->daiaCacheEnabled) {
+                                $this->putCachedData($this->generateURI($id), $data);
+                            }
+                            $status[] = $data;
+                        }
                     }
                 }
+            } catch (ILSException $e) {
+                $this->debug($e->getMessage());
             }
-        } catch (ILSException $e) {
-            $this->debug($e->getMessage());
         }
         return $status;
     }
@@ -373,7 +459,7 @@ class DAIA extends AbstractBase implements
         try {
             $result = $this->httpService->get(
                 $this->baseUrl,
-                $params, null, $http_headers
+                $params, $this->daiaTimeout, $http_headers
             );
         } catch (\Exception $e) {
             throw new ILSException(
@@ -430,7 +516,7 @@ class DAIA extends AbstractBase implements
      *
      * @return string     URI of the DAIA document
      *
-     * @see http://gbv.github.io/daiaspec/daia.html#query-api
+     * @see http://gbv.github.io/daia/daia.html#query-parameters
      */
     protected function generateURI($id)
     {
@@ -445,7 +531,7 @@ class DAIA extends AbstractBase implements
      *
      * @return string   Combined URIs (delimited by "|")
      *
-     * @see http://gbv.github.io/daiaspec/daia.html#query-api
+     * @see http://gbv.github.io/daia/daia.html#query-parameters
      */
     protected function generateMultiURIs($ids)
     {
@@ -657,6 +743,8 @@ class DAIA extends AbstractBase implements
             foreach ($daiaArray['item'] as $item) {
                 $result_item = [];
                 $result_item['id'] = $id;
+                // custom DAIA field
+                $result_item['doc_id'] = $doc_id;
                 $result_item['item_id'] = $item['id'];
                 // custom DAIA field used in getHoldLink()
                 $result_item['ilslink']
@@ -671,9 +759,17 @@ class DAIA extends AbstractBase implements
                 // get callnumber
                 $result_item['callnumber'] = $this->getItemCallnumber($item);
                 // get location
-                $result_item['location'] = $this->getItemLocation($item);
+                $result_item['location'] = $this->getItemDepartment($item);
+                // custom DAIA field
+                $result_item['locationid'] = $this->getItemDepartmentId($item);
                 // get location link
-                $result_item['locationhref'] = $this->getItemLocationLink($item);
+                $result_item['locationhref'] = $this->getItemDepartmentLink($item);
+                // custom DAIA field
+                $result_item['storage'] = $this->getItemStorage($item);
+                // custom DAIA field
+                $result_item['storageid'] = $this->getItemStorageId($item);
+                // custom DAIA field
+                $result_item['storagehref'] = $this->getItemStorageLink($item);
                 // status and availability will be calculated in own function
                 $result_item = $this->getItemStatus($item) + $result_item;
                 // add result_item to the result array
@@ -696,9 +792,10 @@ class DAIA extends AbstractBase implements
         $availability = false;
         $status = ''; // status cannot be null as this will crash the translator
         $duedate = null;
-        $availableLink = '';
+        $serviceLink = '';
         $queue = '';
         $item_notes = [];
+        $item_limitation_types = [];
         $services = [];
 
         if (isset($item['available'])) {
@@ -720,11 +817,11 @@ class DAIA extends AbstractBase implements
                     // openaccess
                     $availability = true;
                     if ($available['service'] == 'loan'
-                        && isset($available['service']['href'])
+                        && isset($available['href'])
                     ) {
                         // save the link to the ils if we have a href for loan
                         // service
-                        $availableLink = $available['service']['href'];
+                        $serviceLink = $available['href'];
                     }
                 }
 
@@ -732,7 +829,11 @@ class DAIA extends AbstractBase implements
                 if (isset($available['limitation'])) {
                     $item_notes = array_merge(
                         $item_notes,
-                        $this->getItemLimitation($available['limitation'])
+                        $this->getItemLimitationContent($available['limitation'])
+                    );
+                    $item_limitation_types = array_merge(
+                        $item_limitation_types,
+                        $this->getItemLimitationTypes($available['limitation'])
                     );
                 }
 
@@ -758,21 +859,28 @@ class DAIA extends AbstractBase implements
                     )
                 ) {
                     if ($unavailable['service'] == 'loan'
-                        && isset($unavailable['service']['href'])
+                        && isset($unavailable['href'])
                     ) {
                         //save the link to the ils if we have a href for loan service
+                        $serviceLink = $unavailable['href'];
                     }
 
                     // use limitation element for status string
                     if (isset($unavailable['limitation'])) {
                         $item_notes = array_merge(
                             $item_notes,
-                            $this->getItemLimitation($unavailable['limitation'])
+                            $this->getItemLimitationContent(
+                                $unavailable['limitation']
+                            )
+                        );
+                        $item_limitation_types = array_merge(
+                            $item_limitation_types,
+                            $this->getItemLimitationTypes($unavailable['limitation'])
                         );
                     }
                 }
                 // attribute expected is mandatory for unavailable element
-                if (isset($unavailable['expected'])) {
+                if (!empty($unavailable['expected'])) {
                     try {
                         $duedate = $this->dateConverter
                             ->convertToDisplayDate(
@@ -796,119 +904,280 @@ class DAIA extends AbstractBase implements
             }
         }
 
-        /*'availability' => '0',
-        'status' => '',  // string - needs to be computed from availability info
-        'duedate' => '', // if checked_out else null
-        'returnDate' => '', // false if not recently returned(?)
-        'requests_placed' => '', // total number of placed holds
-        'is_holdable' => false, // place holding possible?*/
+        /*'returnDate' => '', // false if not recently returned(?)*/
 
-        if (!empty($availableLink)) {
-            $return['ilslink'] = $availableLink;
+        if (!empty($serviceLink)) {
+            $return['ilslink'] = $serviceLink;
         }
 
         $return['item_notes']      = $item_notes;
-        $return['status']          = $status;
+        $return['status']          = $this->getStatusString($item);
         $return['availability']    = $availability;
         $return['duedate']         = $duedate;
         $return['requests_placed'] = $queue;
         $return['services']        = $this->getAvailableItemServices($services);
 
+        // In this DAIA driver implementation addLink and is_holdable are assumed
+        // Boolean as patron based availability requires either a patron-id or -type.
+        // This should be handled in a custom DAIA driver
+        $return['addLink']     = $this->checkIsRecallable($item);
+        $return['is_holdable'] = $this->checkIsRecallable($item);
+        $return['holdtype']    = $this->getHoldType($item);
+
+        // Check if we the item is available for storage retrieval request if it is
+        // not holdable.
+        $return['addStorageRetrievalRequestLink'] = !$return['is_holdable']
+            ? $this->checkIsStorageRetrievalRequest($item) : false;
+
+        // add a custom Field to allow passing custom DAIA data to the frontend in
+        // order to use it for more precise display of availability
+        $return['customData']      = $this->getCustomData($item);
+
+        $return['limitation_types'] = $item_limitation_types;
+        
         return $return;
     }
 
     /**
-     * Returns the value for "number" in VuFind getStatus/getHolding array
+     * Helper function to allow custom data in status array.
      *
-     * @param array $item    Array with DAIA item data
-     * @param int   $counter Integer counting items as alternative return value
+     * @param array $item Array with DAIA item data
      *
-     * @return mixed
+     * @return array
      */
-    protected function getItemNumber($item, $counter)
+    protected function getCustomData($item)
     {
-        return $counter;
+        return [];
     }
 
     /**
-     * Returns the value for "barcode" in VuFind getStatus/getHolding array
+     * Helper function to return an appropriate status string for current item.
      *
      * @param array $item Array with DAIA item data
      *
      * @return string
      */
-    protected function getItemBarcode($item)
+    protected function getStatusString($item)
     {
-        return '1';
+        // status cannot be null as this will crash the translator
+        return '';
     }
 
     /**
-     * Returns the value for "reserve" in VuFind getStatus/getHolding array
+     * Helper function to determine if item is recallable.
+     * DAIA does not genuinly allow distinguishing between holdable and recallable
+     * items. This could be achieved by usage of limitations but this would not be
+     * shared functionality between different DAIA implementations (thus should be
+     * implemented in custom drivers). Therefore this returns whether an item
+     * is recallable based on unavailable services and the existence of an href.
      *
      * @param array $item Array with DAIA item data
      *
-     * @return string
+     * @return bool
      */
-    protected function getItemReserveStatus($item)
+    protected function checkIsRecallable($item)
     {
-        return 'N';
+        // This basic implementation checks the item for being unavailable for loan
+        // and presentation but with an existing href (as a flag for further action).
+        $services = ['available' => [], 'unavailable' => []];
+        $href = false;
+        if (isset($item['available'])) {
+            // check if item is loanable or presentation
+            foreach ($item['available'] as $available) {
+                if (isset($available['service'])
+                    && in_array($available['service'], ['loan', 'presentation'])
+                ) {
+                    $services['available'][] = $available['service'];
+                }
+            }
+        }
+
+        if (isset($item['unavailable'])) {
+            foreach ($item['unavailable'] as $unavailable) {
+                if (isset($unavailable['service'])
+                    && in_array($unavailable['service'], ['loan', 'presentation'])
+                ) {
+                    $services['unavailable'][] = $unavailable['service'];
+                    // attribute href is used to determine whether item is recallable
+                    // or not
+                    $href = isset($unavailable['href']) ? true : $href;
+                }
+            }
+        }
+
+        // Check if we have at least one service unavailable and a href field is set
+        // (either as flag or as actual value for the next action).
+        return ($href && count(
+            array_diff($services['unavailable'], $services['available'])
+        ));
     }
 
     /**
-     * Returns the value for "callnumber" in VuFind getStatus/getHolding array
+     * Helper function to determine if the item is available as storage retrieval.
+     *
+     * @param array $item Array with DAIA item data
+     *
+     * @return bool
+     */
+    protected function checkIsStorageRetrievalRequest($item)
+    {
+        // This basic implementation checks the item for being available for loan
+        // and presentation but with an existing href (as a flag for further action).
+        $services = ['available' => [], 'unavailable' => []];
+        $href = false;
+        if (isset($item['available'])) {
+            // check if item is loanable or presentation
+            foreach ($item['available'] as $available) {
+                if (isset($available['service'])
+                    && in_array($available['service'], ['loan', 'presentation'])
+                ) {
+                    $services['available'][] = $available['service'];
+                    // attribute href is used to determine whether item is
+                    // requestable or not
+                    $href = isset($available['href']) ? true : $href;
+                }
+            }
+        }
+
+        if (isset($item['unavailable'])) {
+            foreach ($item['unavailable'] as $unavailable) {
+                if (isset($unavailable['service'])
+                    && in_array($unavailable['service'], ['loan', 'presentation'])
+                ) {
+                    $services['unavailable'][] = $unavailable['service'];
+                }
+            }
+        }
+
+        // Check if we have at least one service unavailable and a href field is set
+        // (either as flag or as actual value for the next action).
+        return ($href && count(
+            array_diff($services['available'], $services['unavailable'])
+        ));
+    }
+
+    /**
+     * Helper function to determine the holdtype availble for current item.
+     * DAIA does not genuinly allow distinguishing between holdable and recallable
+     * items. This could be achieved by usage of limitations but this would not be
+     * shared functionality between different DAIA implementations (thus should be
+     * implemented in custom drivers). Therefore getHoldType always returns recall.
+     *
+     * @param array $item Array with DAIA item data
+     *
+     * @return string 'recall'|null
+     */
+    protected function getHoldType($item)
+    {
+        // return holdtype (hold, recall or block if patron is not allowed) for item
+        return $this->checkIsRecallable($item) ? 'recall' : null;
+    }
+
+    /**
+     * Returns the evaluated value of the provided limitation element
+     *
+     * @param array $limitations Array with DAIA limitation data
+     *
+     * @return array
+     */
+    protected function getItemLimitation($limitations)
+    {
+        $itemLimitation = [];
+        foreach ($limitations as $limitation) {
+            // return the first limitation with content set
+            if (isset($limitation['content'])) {
+                $itemLimitation[] = $limitation['content'];
+            }
+        }
+        return $itemLimitation;
+    }
+
+    /**
+     * Returns the value of item.department.content (e.g. to be used in VuFind
+     * getStatus/getHolding array as location)
      *
      * @param array $item Array with DAIA item data
      *
      * @return string
      */
-    protected function getItemCallnumber($item)
+    protected function getItemDepartment($item)
     {
-        return isset($item['label']) && !empty($item['label'])
-            ? $item['label']
+        return isset($item['department']) && isset($item['department']['content'])
+        && !empty($item['department']['content'])
+            ? $item['department']['content']
             : 'Unknown';
     }
 
     /**
-     * Returns the value for "location" in VuFind getStatus/getHolding array
+     * Returns the value of item.department.id (e.g. to be used in VuFind
+     * getStatus/getHolding array as location)
      *
      * @param array $item Array with DAIA item data
      *
      * @return string
      */
-    protected function getItemLocation($item)
+    protected function getItemDepartmentId($item)
     {
-        $location = '';
+        return isset($item['department']) && isset($item['department']['id'])
+            ? $item['department']['id'] : '';
+    }
 
-        if (isset($item['department'])
-            && isset($item['department']['content'])
-        ) {
-            $location .= (empty($location)
-                ? $item['department']['content']
-                : ' - ' . $item['department']['content']);
-        }
+    /**
+     * Returns the value of item.department.href (e.g. to be used in VuFind
+     * getStatus/getHolding array for linking the location)
+     *
+     * @param array $item Array with DAIA item data
+     *
+     * @return string
+     */
+    protected function getItemDepartmentLink($item)
+    {
+        return isset($item['department']['href'])
+            ? $item['department']['href'] : false;
+    }
 
-        if (isset($item['storage'])
-            && isset($item['storage']['content'])
-        ) {
-            $location .= (empty($location)
-                ? $item['storage']['content']
-                : ' - ' . $item['storage']['content']);
-        }
+    /**
+     * Returns the value of item.storage.content (e.g. to be used in VuFind
+     * getStatus/getHolding array as location)
+     *
+     * @param array $item Array with DAIA item data
+     *
+     * @return string
+     */
+    protected function getItemStorage($item)
+    {
+        return isset($item['storage']) && isset($item['storage']['content'])
+        && !empty($item['storage']['content'])
+            ? $item['storage']['content']
+            : 'Unknown';
+    }
 
-        return (empty($location) ? 'Unknown' : $location);
+    /**
+     * Returns the value of item.storage.id (e.g. to be used in VuFind
+     * getStatus/getHolding array as location)
+     *
+     * @param array $item Array with DAIA item data
+     *
+     * @return string
+     */
+    protected function getItemStorageId($item)
+    {
+        return isset($item['storage']) && isset($item['storage']['id'])
+            ? $item['storage']['id'] : '';
     }
 
     /**
-     * Returns the value for "location" href in VuFind getStatus/getHolding array
+     * Returns the value of item.storage.href (e.g. to be used in VuFind
+     * getStatus/getHolding array for linking the location)
      *
      * @param array $item Array with DAIA item data
      *
      * @return string
      */
-    protected function getItemLocationLink($item)
+    protected function getItemStorageLink($item)
     {
-        return isset($item['storage']['href'])
-            ? $item['storage']['href'] : false;
+        return isset($item['storage']) && isset($item['storage']['href'])
+            ? $item['storage']['href'] : '';
     }
 
     /**
@@ -918,16 +1187,86 @@ class DAIA extends AbstractBase implements
      *
      * @return array
      */
-    protected function getItemLimitation($limitations)
+    protected function getItemLimitationContent($limitations)
     {
-        $itemLimitation = [];
+        $itemLimitationContent = [];
         foreach ($limitations as $limitation) {
-            // return the limitations with content set
+            // return the first limitation with content set
             if (isset($limitation['content'])) {
-                $itemLimitation[] = $limitation['content'];
+                $itemLimitationContent[] = $limitation['content'];
             }
         }
-        return $itemLimitation;
+        return $itemLimitationContent;
+    }
+
+    /**
+     * Returns the evaluated values of the provided limitations element
+     *
+     * @param array $limitations Array with DAIA limitation data
+     *
+     * @return array
+     */
+    protected function getItemLimitationTypes($limitations)
+    {
+        $itemLimitationTypes = [];
+        foreach ($limitations as $limitation) {
+            // return the first limitation with content set
+            if (isset($limitation['id'])) {
+                $itemLimitationTypes[] = $limitation['id'];
+            }
+        }
+        return $itemLimitationTypes;
+    }
+
+    /**
+     * Returns the value for "number" in VuFind getStatus/getHolding array
+     *
+     * @param array $item    Array with DAIA item data
+     * @param int   $counter Integer counting items as alternative return value
+     *
+     * @return mixed
+     */
+    protected function getItemNumber($item, $counter)
+    {
+        return $counter;
+    }
+
+    /**
+     * Returns the value for "location" in VuFind getStatus/getHolding array
+     *
+     * @param array $item Array with DAIA item data
+     *
+     * @return string
+     */
+    protected function getItemBarcode($item)
+    {
+        return '1';
+    }
+
+    /**
+     * Returns the value for "reserve" in VuFind getStatus/getHolding array
+     *
+     * @param array $item Array with DAIA item data
+     *
+     * @return string
+     */
+    protected function getItemReserveStatus($item)
+    {
+        return 'N';
+    }
+
+    /**
+     * Returns the value for "callnumber" in VuFind getStatus/getHolding array
+     *
+     * @param array $item Array with DAIA item data
+     *
+     * @return string
+     */
+    protected function getItemCallnumber($item)
+    {
+        return isset($item['label']) && !empty($item['label'])
+            ? $item['label']
+            : 'Unknown';
     }
 
     /**
@@ -952,7 +1291,7 @@ class DAIA extends AbstractBase implements
         }
         return array_intersect(['loan', 'presentation'], $availableServices);
     }
-    
+
     /**
      * Logs content of message elements in DAIA response for debugging
      *
diff --git a/module/VuFind/src/VuFind/ILS/Driver/Demo.php b/module/VuFind/src/VuFind/ILS/Driver/Demo.php
index 4878e89d813c784a52dce97ee6e703ab057f9ee4..520b5172024a4b2f1734507009090e396210f46e 100644
--- a/module/VuFind/src/VuFind/ILS/Driver/Demo.php
+++ b/module/VuFind/src/VuFind/ILS/Driver/Demo.php
@@ -22,7 +22,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  ILS_Drivers
@@ -586,14 +586,7 @@ class Demo extends AbstractBase
      */
     public function getStatuses($ids)
     {
-        // Random Seed
-        srand(time());
-
-        $status = [];
-        foreach ($ids as $id) {
-            $status[] = $this->getStatus($id);
-        }
-        return $status;
+        return array_map([$this, 'getStatus'], $ids);
     }
 
     /**
@@ -1055,8 +1048,8 @@ class Demo extends AbstractBase
     /**
      * Get request groups
      *
-     * @param integer $bibId  BIB ID
-     * @param array   $patron Patron information returned by the patronLogin
+     * @param int   $bibId  BIB ID
+     * @param array $patron Patron information returned by the patronLogin
      * method.
      *
      * @return array  False if request groups not in use or an array of
@@ -1949,10 +1942,9 @@ class Demo extends AbstractBase
             ];
         }
         if ($function == 'changePassword') {
-            return [
-                'minLength' => 4,
-                'maxLength' => 20
-            ];
+            return isset($this->config['changePassword'])
+                ? $this->config['changePassword']
+                : ['minLength' => 4, 'maxLength' => 20];
         }
         return [];
     }
diff --git a/module/VuFind/src/VuFind/ILS/Driver/DriverInterface.php b/module/VuFind/src/VuFind/ILS/Driver/DriverInterface.php
index 6374c9afc16d998abc3b01fb7f6a43d06813b015..dc3ba6f6304fc4650311d50a387a220d8c7c19cd 100644
--- a/module/VuFind/src/VuFind/ILS/Driver/DriverInterface.php
+++ b/module/VuFind/src/VuFind/ILS/Driver/DriverInterface.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  ILS_Drivers
diff --git a/module/VuFind/src/VuFind/ILS/Driver/Evergreen.php b/module/VuFind/src/VuFind/ILS/Driver/Evergreen.php
index 3f035f180bcd54e8f217b44f581e750e35db0a8a..f546054e10b5b3fccc90b25a9a8a5530677bd674 100644
--- a/module/VuFind/src/VuFind/ILS/Driver/Evergreen.php
+++ b/module/VuFind/src/VuFind/ILS/Driver/Evergreen.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  ILS_Drivers
diff --git a/module/VuFind/src/VuFind/ILS/Driver/Factory.php b/module/VuFind/src/VuFind/ILS/Driver/Factory.php
index dbff5f4b0b759de7a32f3d8358756723eb90db94..8d6cdd8a21a76821be74ff9ff1b560220b768e7c 100644
--- a/module/VuFind/src/VuFind/ILS/Driver/Factory.php
+++ b/module/VuFind/src/VuFind/ILS/Driver/Factory.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  ILS_Drivers
@@ -65,7 +65,27 @@ class Factory
      */
     public static function getDAIA(ServiceManager $sm)
     {
-        return new DAIA(
+        $daia = new DAIA(
+            $sm->getServiceLocator()->get('VuFind\DateConverter')
+        );
+
+        $daia->setCacheStorage(
+            $sm->getServiceLocator()->get('VuFind\CacheManager')->getCache('object')
+        );
+
+        return $daia;
+    }
+
+    /**
+     * Factory for LBS4 driver.
+     *
+     * @param ServiceManager $sm Service manager.
+     *
+     * @return LBS4
+     */
+    public static function getLBS4(ServiceManager $sm)
+    {
+        return new LBS4(
             $sm->getServiceLocator()->get('VuFind\DateConverter')
         );
     }
@@ -142,6 +162,39 @@ class Factory
         return new NoILS($sm->getServiceLocator()->get('VuFind\RecordLoader'));
     }
 
+    /**
+     * Factory for PAIA driver.
+     *
+     * @param ServiceManager $sm Service manager.
+     *
+     * @return PAIA
+     */
+    public static function getPAIA(ServiceManager $sm)
+    {
+        $paia = new PAIA(
+            $sm->getServiceLocator()->get('VuFind\DateConverter'),
+            $sm->getServiceLocator()->get('VuFind\SessionManager')
+        );
+
+        $paia->setCacheStorage(
+            $sm->getServiceLocator()->get('VuFind\CacheManager')->getCache('object')
+        );
+
+        return $paia;
+    }
+
+    /**
+     * Factory for KohaILSDI driver.
+     *
+     * @param ServiceManager $sm Service manager.
+     *
+     * @return KohaILSDI
+     */
+    public static function getKohaILSDI(ServiceManager $sm)
+    {
+        return new KohaILSDI($sm->getServiceLocator()->get('VuFind\DateConverter'));
+    }
+
     /**
      * Factory for Unicorn driver.
      *
diff --git a/module/VuFind/src/VuFind/ILS/Driver/Horizon.php b/module/VuFind/src/VuFind/ILS/Driver/Horizon.php
index 4012048c433fc0baf36b0189a2fb80bb20faccb4..8f78419ed81ece404708617fe44ee3e60a3b9336 100644
--- a/module/VuFind/src/VuFind/ILS/Driver/Horizon.php
+++ b/module/VuFind/src/VuFind/ILS/Driver/Horizon.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  ILS_Drivers
diff --git a/module/VuFind/src/VuFind/ILS/Driver/HorizonXMLAPI.php b/module/VuFind/src/VuFind/ILS/Driver/HorizonXMLAPI.php
index a6d06f9d4ee0985f194f21d31208713689b87389..d95af141dd0470dd4a88e3b34d08f8508b99255f 100644
--- a/module/VuFind/src/VuFind/ILS/Driver/HorizonXMLAPI.php
+++ b/module/VuFind/src/VuFind/ILS/Driver/HorizonXMLAPI.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  ILS_Drivers
diff --git a/module/VuFind/src/VuFind/ILS/Driver/Innovative.php b/module/VuFind/src/VuFind/ILS/Driver/Innovative.php
index 70898b0bb8bfcbfaf55ce8632e93229cb9b6df57..4f20df3c3253976704403dd245ef57cd5ce6d6cf 100644
--- a/module/VuFind/src/VuFind/ILS/Driver/Innovative.php
+++ b/module/VuFind/src/VuFind/ILS/Driver/Innovative.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  ILS_Drivers
diff --git a/module/VuFind/src/VuFind/ILS/Driver/Koha.php b/module/VuFind/src/VuFind/ILS/Driver/Koha.php
index 69c14a422c1a540681270599b286f9bc5c9a2739..cd364683a0c92d25b12b775b5ae6471fbd0c47a1 100644
--- a/module/VuFind/src/VuFind/ILS/Driver/Koha.php
+++ b/module/VuFind/src/VuFind/ILS/Driver/Koha.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  ILS_Drivers
diff --git a/module/VuFind/src/VuFind/ILS/Driver/KohaILSDI.php b/module/VuFind/src/VuFind/ILS/Driver/KohaILSDI.php
new file mode 100644
index 0000000000000000000000000000000000000000..81410180af61e2b6ff30cbb4c18fb865c9d2aa2c
--- /dev/null
+++ b/module/VuFind/src/VuFind/ILS/Driver/KohaILSDI.php
@@ -0,0 +1,1634 @@
+<?php
+/**
+ * KohaILSDI ILS Driver
+ *
+ * PHP version 5
+ *
+ * Copyright (C) Alex Sassmannshausen, PTFS Europe 2014.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ * @category VuFind
+ * @package  ILS_Drivers
+ * @author   Alex Sassmannshausen <alex.sassmannshausen@ptfs-europe.com>
+ * @author   Tom Misilo <misilot@fit.edu>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     https://vufind.org/wiki/development:plugins:ils_drivers Wiki
+ */
+namespace VuFind\ILS\Driver;
+use PDO, PDOException;
+use VuFind\Exception\ILS as ILSException;
+use VuFindHttp\HttpServiceInterface;
+use Zend\Log\LoggerInterface;
+use VuFind\Exception\Date as DateException;
+
+/**
+ * VuFind Driver for Koha, using web APIs (ILSDI)
+ *
+ * Minimum Koha Version: 3.18.6
+ *
+ * @category VuFind
+ * @package  ILS_Drivers
+ * @author   Alex Sassmannshausen <alex.sassmannshausen@ptfs-europe.com>
+ * @author   Tom Misilo <misilot@fit.edu>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     https://vufind.org/wiki/development:plugins:ils_drivers Wiki
+ */
+class KohaILSDI extends \VuFind\ILS\Driver\AbstractBase implements
+    \VuFindHttp\HttpServiceAwareInterface, \Zend\Log\LoggerAwareInterface
+{
+    /**
+     * Web services host
+     *
+     * @var string
+     */
+    protected $host;
+
+    /**
+     * ILS base URL
+     *
+     * @var string
+     */
+    protected $ilsBaseUrl;
+
+    /**
+     * Location codes
+     *
+     * @var array
+     */
+    protected $locations;
+
+    /**
+     * Codes of locations available for pickup
+     *
+     * @var array
+     */
+    protected $pickupEnableBranchcodes;
+
+    /**
+     * Codes of locations always should be available
+     *   - For example reference material or material
+     *     not for loan
+     *
+     * @var array
+     */
+    protected $availableLocationsDefault;
+
+    /**
+     * Default location code
+     *
+     * @var string
+     */
+    protected $defaultLocation;
+
+    /**
+     * Database connection
+     *
+     * @var string
+     */
+    protected $db;
+
+    /**
+     * Logger Status
+     *
+     * @var LoggerInterface
+     */
+    protected $logger = false;
+
+    /**
+     * Set the logger
+     *
+     * @param LoggerInterface $logger Logger to use.
+     *
+     * @return void
+     */
+    public function setLogger(LoggerInterface $logger)
+    {
+        $this->logger = $logger;
+    }
+
+    /**
+     * Show a debug message.
+     *
+     * @param string $msg Debug message.
+     *
+     * @return void
+     */
+    protected function debug($msg)
+    {
+        if ($this->logger) {
+            $this->logger->debug($msg);
+        }
+    }
+
+    /**
+     * HTTP service
+     *
+     * @var \VuFindHttp\HttpServiceInterface
+     */
+    protected $httpService = null;
+
+    /**
+     * Set the HTTP service to be used for HTTP requests.
+     *
+     * @param HttpServiceInterface $service HTTP service
+     *
+     * @return void
+     */
+    public function setHttpService(HttpServiceInterface $service)
+    {
+        $this->httpService = $service;
+    }
+
+    /**
+     * Date converter object
+     *
+     * @var \VuFind\Date\Converter
+     */
+    protected $dateConverter;
+
+    /**
+     * Initialize the driver.
+     *
+     * Validate configuration and perform all resource-intensive tasks needed to
+     * make the driver active.
+     *
+     * @throws ILSException
+     * @return void
+     */
+    public function init()
+    {
+        if (empty($this->config)) {
+            throw new ILSException('Configuration needs to be set.');
+        }
+
+        // Base for API address
+        $this->host = isset($this->config['Catalog']['host']) ?
+            $this->config['Catalog']['host'] : "localhost";
+
+        // Storing the base URL of ILS
+        $this->ilsBaseUrl = isset($this->config['Catalog']['url'])
+            ? $this->config['Catalog']['url'] : "";
+
+        // Default location defined in 'KohaILSDI.ini'
+        $this->defaultLocation
+            = isset($this->config['Holds']['defaultPickUpLocation'])
+            ? $this->config['Holds']['defaultPickUpLocation'] : null;
+
+        $this->pickupEnableBranchcodes
+            = isset($this->config['Holds']['pickupLocations'])
+            ? $this->config['Holds']['pickupLocations'] : [];
+
+        // Locations that should default to available, defined in 'KohaILSDI.ini'
+        $this->availableLocationsDefault
+            = isset($this->config['Other']['availableLocations'])
+            ? $this->config['Other']['availableLocations'] : [];
+
+        // Create a dateConverter
+        $this->dateConverter = new \VuFind\Date\Converter;
+
+        $this->debug("Config Summary:");
+        $this->debug("DB Host: " . $this->host);
+        $this->debug("ILS URL: " . $this->ilsBaseUrl);
+        $this->debug("Locations: " . $this->locations);
+        $this->debug("Default Location: " . $this->defaultLocation);
+    }
+
+    /**
+     * Initialize the DB driver.
+     *
+     * Validate configuration and perform all resource-intensive tasks needed to
+     * make the driver active.
+     *
+     * @throws ILSException
+     * @return void
+     */
+    protected function initDb()
+    {
+        if (empty($this->config)) {
+            throw new ILSException('Configuration needs to be set.');
+        }
+
+        //Connect to MySQL
+        try {
+            $this->db = new PDO(
+                'mysql:host=' . $this->host .
+                ';port=' . $this->config['Catalog']['port'] .
+                ';dbname=' . $this->config['Catalog']['database'],
+                $this->config['Catalog']['username'],
+                $this->config['Catalog']['password'],
+                [PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8']
+            );
+
+            // Throw PDOExceptions if something goes wrong
+            $this->db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
+            //Return result set like mysql_fetch_assoc()
+            $this->db->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
+            // set communication enoding to utf8
+            $this->db->exec("SET NAMES utf8");
+        } catch (PDOException $e) {
+            $this->debug('Connection failed: ' . $e->getMessage());
+            throw new ILSException($e->getMessage);
+        }
+
+        $this->debug('Connected to DB');
+    }
+
+    /**
+     * Get Field
+     *
+     * Check $contents is not "", return it; else return $default.
+     *
+     * @param string $contents string to be checked
+     * @param string $default  value to return if $contents is ""
+     *
+     * @return $contents or $default
+     */
+    protected function getField($contents, $default = "Unknown")
+    {
+        if ((string) $contents != "") {
+            return (string) $contents;
+        } else {
+            return $default;
+        }
+    }
+
+    /**
+     * Make Request
+     *
+     * Makes a request to the Koha ILSDI API
+     *
+     * @param string $api_query   Query string for request
+     * @param string $http_method HTTP method (default = GET)
+     *
+     * @throws ILSException
+     * @return obj
+     */
+    protected function makeRequest($api_query, $http_method = "GET")
+    {
+        //$url = $this->host . $this->api_path . $api_query;
+
+        $url = $this->ilsBaseUrl . "?service=" . $api_query;
+
+        $this->debug("URL: '$url'");
+
+        $http_headers = [
+            "Accept: text/xml",
+            "Accept-encoding: plain",
+        ];
+
+        try {
+            $client = $this->httpService->createClient($url);
+
+            $client->setMethod($http_method);
+            $client->setHeaders($http_headers);
+            $result = $client->send();
+        } catch (\Exception $e) {
+            $this->debug("Result is invalid.");
+            throw new ILSException($e->getMessage());
+        }
+
+        if (!$result->isSuccess()) {
+            $this->debug("Result is invalid.");
+            throw new ILSException('HTTP error');
+        }
+        $answer = $result->getBody();
+        //$answer = str_replace('xmlns=', 'ns=', $answer);
+        $result = simplexml_load_string($answer);
+        if (!$result) {
+            $this->debug("XML is not valid, URL: $url");
+
+            throw new ILSException(
+                "XML is not valid, URL: $url method: $method answer: $answer."
+            );
+        }
+        return $result;
+    }
+
+    /**
+     * Make Ilsdi Request Array
+     *
+     * Makes a request to the Koha ILSDI API
+     *
+     * @param string $service     Called function (GetAvailability,
+     *                                             GetRecords,
+     *                                             GetAuthorityRecords,
+     *                                             LookupPatron,
+     *                                             AuthenticatePatron,
+     *                                             GetPatronInfo,
+     *                                             GetPatronStatus,
+     *                                             GetServices,
+     *                                             RenewLoan,
+     *                                             HoldTitle,
+     *                                             HoldItem,
+     *                                             CancelHold)
+     * @param array  $params      Key is parameter name, value is parameter value
+     * @param string $http_method HTTP method (default = GET)
+     *
+     * @throws ILSException
+     * @return obj
+     */
+    protected function makeIlsdiRequest($service, $params, $http_method = "GET")
+    {
+        $start = microtime(true);
+        $url = $this->ilsBaseUrl . "?service=" . $service;
+        foreach ($params as $paramname => $paramvalue) {
+            $url .= "&$paramname=" . urlencode($paramvalue);
+        }
+
+        $this->debug("URL: '$url'");
+
+        $http_headers = [
+            "Accept: text/xml",
+            "Accept-encoding: plain",
+        ];
+
+        try {
+            $client = $this->httpService->createClient($url);
+            $client->setMethod($http_method);
+            $client->setHeaders($http_headers);
+            $result = $client->send();
+        } catch (\Exception $e) {
+            $this->debug("Result is invalid.");
+            throw new ILSException($e->getMessage());
+        }
+
+        if (!$result->isSuccess()) {
+            $this->debug("Result is invalid.");
+            throw new ILSException('HTTP error');
+        }
+        $end = microtime(true);
+        $time1 = $end - $start;
+        $start = microtime(true);
+        $result = simplexml_load_string($result->getBody());
+        if (!$result) {
+            $this->debug("XML is not valid, URL: $url");
+
+            throw new ILSException(
+                "XML is not valid, URL: $url"
+            );
+        }
+        $end = microtime(true);
+        $time2 = $end - $start;
+        echo "\t$time1 - $time2";
+        return $result;
+    }
+
+    /**
+     * To Koha Date
+     *
+     * Turns a display date into a date format expected by Koha.
+     *
+     * @param string $display_date Date to be converted
+     *
+     * @throws ILSException
+     * @return string $koha_date
+     */
+    protected function toKohaDate($display_date)
+    {
+        $koha_date = "";
+
+        // Convert last interest date from format to Koha format
+        $koha_date = $this->dateConverter->convertFromDisplayDate(
+            "Y-m-d", $display_date
+        );
+
+        $checkTime =  $this->dateConverter->convertFromDisplayDate(
+            "U", $display_date
+        );
+        if (!is_numeric($checkTime)) {
+            throw new DateException('Result should be numeric');
+        }
+
+        if (time() > $checkTime) {
+            // Hold Date is in the past
+            throw new DateException('hold_date_past');
+        }
+        return $koha_date;
+    }
+
+    /**
+     * Public Function which retrieves renew, hold and cancel settings from the
+     * driver ini file.
+     *
+     * @param string $function The name of the feature to be checked
+     *
+     * @return array An array with key-value pairs.
+     */
+    public function getConfig($function)
+    {
+        $functionConfig = "";
+        if (isset($this->config[$function])) {
+            $functionConfig = $this->config[$function];
+        } else {
+            $functionConfig = false;
+        }
+        return $functionConfig;
+    }
+
+    /**
+     * Get Pick Up Locations
+     *
+     * This is responsible for gettting a list of valid library locations for
+     * holds / recall retrieval
+     *
+     * @param array $patron      Patron information returned by the patronLogin
+     * method.
+     * @param array $holdDetails Optional array, only passed in when getting a list
+     * in the context of placing a hold; contains most of the same values passed to
+     * placeHold, minus the patron data.    May be used to limit the pickup options
+     * or may be ignored.  The driver must not add new options to the return array
+     * based on this data or other areas of VuFind may behave incorrectly.
+     *
+     * @throws ILSException
+     * @return array An array of associative arrays with locationID and
+     * locationDisplay keys
+     *
+     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+     */
+    public function getPickUpLocations($patron = false, $holdDetails = null)
+    {
+        if (!$this->locations) {
+            if (!$this->db) {
+                $this->initDb();
+            }
+            $branchcodes = "'" . implode(
+                "','", $this->pickupEnableBranchcodes
+            ) . "'";
+            $sql = "SELECT branchcode as locationID,
+                       branchname as locationDisplay
+                    FROM branches
+                    WHERE branchcode IN ($branchcodes)";
+            try {
+                $sqlSt = $this->db->prepare($sql);
+                $sqlSt->execute();
+                $this->locations = $sqlSt->fetchAll();
+            } catch (PDOException $e) {
+                $this->debug('Connection failed: ' . $e->getMessage());
+                throw new ILSException($e->getMessage());
+            }
+        }
+        return $this->locations;
+
+            // we get them from the API
+            // FIXME: Not yet possible: API incomplete.
+            // TODO: When API: pull locations dynamically from API.
+            /* $response = $this->makeRequest("organizations/branch"); */
+            /* $locations_response_array = $response->OrganizationsGetRows; */
+            /* foreach ($locations_response_array as $location_response) { */
+            /*     $locations[] = array( */
+            /*         'locationID'      => $location_response->OrganizationID, */
+            /*         'locationDisplay' => $location_response->Name, */
+            /*     ); */
+            /* } */
+    }
+
+    /**
+     * Get Default Pick Up Location
+     *
+     * Returns the default pick up location set in KohaILSDI.ini
+     *
+     * @param array $patron      Patron information returned by the patronLogin
+     * method.
+     * @param array $holdDetails Optional array, only passed in when getting a list
+     * in the context of placing a hold; contains most of the same values passed to
+     * placeHold, minus the patron data.    May be used to limit the pickup options
+     * or may be ignored.
+     *
+     * @return string The default pickup location for the patron.
+     *
+     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+     */
+    public function getDefaultPickUpLocation($patron = false, $holdDetails = null)
+    {
+        return $this->defaultLocation;
+    }
+
+    /**
+     * Place Hold
+     *
+     * Attempts to place a hold or recall on a particular item and returns
+     * an array with result details or throws an exception on failure of support
+     * classes
+     *
+     * @param array $holdDetails An array of item and patron data
+     *
+     * @throws ILSException
+     * @return mixed An array of data on the request including
+     * whether or not it was successful and a system message (if available)
+     */
+    public function placeHold($holdDetails)
+    {
+        $rsvLst             = [];
+        $patron             = $holdDetails['patron'];
+        $patron_id          = $patron['id'];
+        $request_location   = isset($patron['ip']) ? $patron['ip'] : "127.0.0.1";
+        $bib_id             = $holdDetails['id'];
+        $item_id            = $holdDetails['item_id'];
+        $pickup_location    = !empty($holdDetails['pickUpLocation'])
+            ? $holdDetails['pickUpLocation'] : $this->defaultLocation;
+        $level              = isset($holdDetails['level'])
+            && !empty($holdDetails['level']) ? $holdDetails['level'] : "item";
+
+        try {
+            //$needed_before_date = $this->toKohaDate($holdDetails['requiredBy']);
+            $dateObject = \DateTime::createFromFormat(
+                "j. n. Y", $holdDetails['requiredBy']
+            );
+            if (is_object($dateObject)) {
+                $needed_before_date = $dateObject->format("Y-m-d");
+            }
+        } catch (\Exception $e) {
+            return [
+                "success" => false,
+                "sysMessage" => "It seems you entered an invalid expiration date."
+            ];
+        }
+
+        $this->debug("patron: " . $patron);
+        $this->debug("patron_id: " . $patron_id);
+        $this->debug("request_location: " . $request_location);
+        $this->debug("item_id: " . $item_id);
+        $this->debug("bib_id: " . $bib_id);
+        $this->debug("pickup loc: " . $pickup_location);
+        $this->debug("Needed before date: " . $needed_before_date);
+        $this->debug("Level: " . $level);
+
+        if ($level == "title") {
+            $rqString = "HoldTitle&patron_id=$patron_id&bib_id=$bib_id"
+                . "&request_location=$request_location"
+                . "&pickup_location=$pickup_location"
+                . "&pickup_expiry_date=$needed_before_date";
+        } else {
+            $rqString = "HoldItem&patron_id=$patron_id&bib_id=$bib_id"
+                . "&item_id=$item_id"
+                . "&pickup_location=$pickup_location"
+                . "&needed_before_date=$needed_before_date"
+                . "&pickup_expiry_date=$needed_before_date";
+        }
+
+        $rsp = $this->makeRequest($rqString);
+
+        //TODO - test this new functionality
+        /*
+        if ( $level == "title" ) {
+        	$rsp2 = $this->makeIlsdiRequest("HoldTitle",
+        			array("patron_id" => $patron_id,
+        				  "bib_id" => $bib_id,
+        				  "request_location" => $request_location,
+        				  "pickup_location" => $pickup_location,
+        				  "pickup_expiry_date" => $needed_before_date,
+        				  "needed_before_date" => $needed_before_date
+        			));
+        } else {
+        	$rsp2 = $this->makeIlsdiRequest("HoldItem",
+        			array("patron_id" => $patron_id,
+        				  "bib_id" => $bib_id,
+        				  "item_id" => $item_id,
+        				  "pickup_location" => $pickup_location,
+        				  "pickup_expiry_date" => $needed_before_date,
+        				  "needed_before_date" => $needed_before_date
+        			));
+        }
+        */
+        $this->debug("Title: " . $rsp->{'title'});
+        $this->debug("Pickup Location: " . $rsp->{'pickup_location'});
+        $this->debug("Code: " . $rsp->{'code'});
+
+        if ($rsp->{'code'} != "") {
+            return [
+                "success"    => false,
+                "sysMessage" => $this->getField($rsp->{'code'})
+                                   . $holdDetails['level'],
+            ];
+        }
+        return [
+            "success"    => true,
+            //"sysMessage" => $message,
+        ];
+    }
+
+    /**
+     * Get Holding
+     *
+     * This is responsible for retrieving the holding information of a certain
+     * record.
+     *
+     * @param string $id     The record id to retrieve the holdings for
+     * @param array  $patron Patron data
+     *
+     * @throws \VuFind\Exception\Date
+     * @throws ILSException
+     * @return array         On success, an associative array with the following
+     * keys: id, availability (boolean), status, location, reserve, callnumber,
+     * duedate, number, barcode.
+     */
+    public function getHolding($id, array $patron = null)
+    {
+
+        $this->debug(
+            "Function getHolding($id, "
+               . implode(",", (array)$patron)
+               . ") called"
+        );
+
+        $started = microtime(true);
+
+        $holding = [];
+        $available = true;
+        $duedate = $status = '';
+        $loc = $shelf = '';
+        $reserves = "N";
+
+        $sql = "select i.itemnumber as ITEMNO, i.location,
+            COALESCE(av.lib_opac,av.lib,av.authorised_value,i.location) AS LOCATION,
+            i.holdingbranch as HLDBRNCH, i.homebranch as HOMEBRANCH,
+            i.reserves as RESERVES, i.itemcallnumber as CALLNO, i.barcode as BARCODE,
+            i.copynumber as COPYNO, i.notforloan as NOTFORLOAN,
+            i.itemnotes as PUBLICNOTES, b.frameworkcode as DOCTYPE,
+            t.frombranch as TRANSFERFROM, t.tobranch as TRANSFERTO,
+            i.itemlost as ITEMLOST, i.itemlost_on AS LOSTON
+            from items i join biblio b on i.biblionumber = b.biblionumber
+            left outer join
+                (SELECT itemnumber, frombranch, tobranch from branchtransfers
+                where datearrived IS NULL) as t USING (itemnumber)
+            left join authorised_values as av on i.location = av.authorised_value
+            where i.biblionumber = :id AND av.category = 'LOC'
+            order by i.itemnumber DESC";
+        $sqlReserves = "select count(*) as RESERVESCOUNT from reserves "
+            . "WHERE biblionumber = :id AND found IS NULL";
+        $sqlWaitingReserve = "select count(*) as WAITING from reserves "
+            . "WHERE itemnumber = :item_id and found = 'W'";
+        $sqlHoldings = "SELECT ExtractValue(( SELECT marcxml FROM biblioitems "
+            . "WHERE biblionumber = :id), "
+            . "'//datafield[@tag=\"866\"]/subfield[@code=\"a\"]') AS MFHD;";
+
+        if (!$this->db) {
+            $this->initDb();
+        }
+        try {
+            $itemSqlStmt = $this->db->prepare($sql);
+            $itemSqlStmt->execute([':id' => $id]);
+            $sqlStmtReserves = $this->db->prepare($sqlReserves);
+            $sqlStmtWaitingReserve = $this->db->prepare($sqlWaitingReserve);
+            $sqlStmtReserves->execute([':id' => $id]);
+            $sqlStmtHoldings = $this->db->prepare($sqlHoldings);
+            $sqlStmtHoldings->execute([':id' => $id]);
+        } catch (PDOException $e) {
+            $this->debug('Connection failed: ' . $e->getMessage());
+            throw new ILSException($e->getMessage());
+        }
+
+        $this->debug("Rows count: " . $itemSqlStmt->rowCount());
+
+        $notes = $sqlStmtHoldings->fetch();
+        $reservesRow = $sqlStmtReserves->fetch();
+        $reservesCount = $reservesRow["RESERVESCOUNT"];
+
+        foreach ($itemSqlStmt->fetchAll() as $rowItem) {
+            $inum = $rowItem['ITEMNO'];
+            $sqlStmtWaitingReserve->execute([':item_id' => $inum]);
+            $waitingReserveRow = $sqlStmtWaitingReserve->fetch();
+            $waitingReserve = $waitingReserveRow["WAITING"];
+            $sql = "select date_due as DUEDATE from issues where itemnumber = :inum";
+            switch ($rowItem['NOTFORLOAN']) {
+            case 0:
+                // If the item is available for loan, then check its current
+                // status
+                $issueSqlStmt = $this->db->prepare($sql);
+                $issueSqlStmt->execute([':inum' => $inum]);
+                $rowIssue = $issueSqlStmt->fetch();
+                if ($rowIssue) {
+                    $available = false;
+                    $status = 'Checked out';
+                    $duedate = $rowIssue['DUEDATE'];
+                } else {
+                    $available = true;
+                    $status = 'Available';
+                    // No due date for an available item
+                    $duedate = '';
+                }
+                break;
+            case 1: // The item is not available for loan
+            default:
+                $available = false;
+                $status = 'Not for loan';
+                $duedate = '';
+                break;
+            }
+            /*
+             * If the Item is in any of locations defined by
+             * availableLocations[] in the KohaILSDI.ini file
+             * the item is considered available
+             */
+
+            if (in_array($rowItem['LOCATION'], $this->availableLocationsDefault)) {
+                $available = true;
+                $duedate = '';
+                $status = 'Available';
+            }
+
+            // If Item is Lost or Missing, provide that status
+            if ($rowItem['ITEMLOST'] > 0) {
+                $available = false;
+                $duedate = $rowItem['LOSTON'];
+                $status = 'Lost/Missing';
+            }
+
+            $duedate_formatted = date_format(new \DateTime($duedate), "m/d/Y");
+
+            //Retrieving the full branch name
+            if ($rowItem['HLDBRNCH'] == null) {
+                if ($rowItem['HOMEBRANCH'] == null) {
+                    $loc = "Unknown";
+                } else {
+                    $loc = $rowItem['LOCATION'];
+                }
+            } else {
+                $loc = $rowItem['LOCATION'];
+            }
+
+            if ($loc != "Unknown") {
+                $sqlBranch = "select branchname as BNAME
+                              from branches
+                              where branchcode = :branch";
+                $branchSqlStmt = $this->db->prepare($sqlBranch);
+                $branchSqlStmt->execute([':branch' => $loc]);
+                $row = $branchSqlStmt->fetch();
+                if ($row) {
+                    $loc = $row['BNAME'];
+                }
+            }
+
+            $onTransfer = false;
+            if (($rowItem["TRANSFERFROM"] != null)
+                && ($rowItem["TRANSFERTO"] != null)
+            ) {
+                $branchSqlStmt->execute([':branch' => $rowItem["TRANSFERFROM"]]);
+                $rowFrom = $branchSqlStmt->fetch();
+                $transferfrom = $rowFrom
+                    ? $rowFrom["BNAME"] : $rowItem["TRANSFERFROM"];
+                $branchSqlStmt->execute([':branch' => $rowItem["TRANSFERTO"]]);
+                $rowTo = $branchSqlStmt->fetch();
+                $transferto = $rowTo ? $rowTo["BNAME"] : $rowItem["TRANSFERTO"];
+                $status = "Na cest? z $transferfrom do $transferto";
+                $available = false;
+                $onTransfer = true;
+            }
+
+            if ($rowItem['DOCTYPE'] == "PE") {
+                $rowItem['COPYNO'] = $rowItem['PERIONAME'];
+            }
+            if ($waitingReserve) {
+                $available = false;
+                $status = "Waiting";
+                $waiting = true;
+            } else {
+                $waiting = false;
+            }
+            $holding[] = [
+                'id'           => $id,
+                'availability' => (string) $available,
+                'item_id'      => $rowItem['ITEMNO'],
+                'status'       => $status,
+                'location'     => $loc,
+                'item_notes'  => (null == $rowItem['PUBLICNOTES']
+                    ? null : [ $rowItem['PUBLICNOTES'] ]),
+                'notes'        => $notes["MFHD"],
+                //'reserve'      => (null == $rowItem['RESERVES'])
+                //    ? 'N' : $rowItem['RESERVES'],
+                'reserve'      => 'N',
+                'callnumber'   =>
+                    ((null == $rowItem['CALLNO']) || ($rowItem['DOCTYPE'] == "PE"))
+                        ? '' : $rowItem['CALLNO'],
+                'duedate'      => ($onTransfer || $waiting)
+                    ? '' : (string) $duedate_formatted,
+                'barcode'      => (null == $rowItem['BARCODE'])
+                    ? 'Unknown' : $rowItem['BARCODE'],
+                'number'       => (null == $rowItem['COPYNO'])
+                    ? '' : $rowItem['COPYNO'],
+                'requests_placed' => $reservesCount ? $reservesCount : 0,
+                'frameworkcode' => $rowItem['DOCTYPE'],
+            ];
+
+        }
+
+        //file_put_contents('holding.txt', print_r($holding,TRUE), FILE_APPEND);
+
+        $this->debug(
+            "Processing finished, rows processed: "
+            . count($holding) . ", took " . (microtime(true) - $started) .
+            " sec"
+        );
+
+        return $holding;
+    }
+
+    /**
+     * This method queries the ILS for new items
+     *
+     * Comment for $fundID: (use a value returned by getFunds, or exclude for no
+     * limit); note that ?fund? may be a misnomer ? if funds are not an
+     * appropriate way to limit your new item results, you can return a different
+     * set of values from getFunds. The important thing is that this parameter
+     * supports an ID returned by getFunds, whatever that may mean.
+     *
+     * @param unknown $page    - page number of results to retrieve (starts at 1)
+     * @param unknown $limit   - the size of each page of results to retrieve
+     * @param unknown $daysOld - the maxi age of records to retrieve in days  -max 30
+     * @param string  $fundId  - optional fund ID to use for limiting results
+     *
+     * @return array provides a count and the results of new items.
+     */
+    public function getNewItems($page, $limit, $daysOld, $fundId = null)
+    {
+
+        $this->debug("getNewItems called $page|$limit|$daysOld|$fundId");
+
+        $items = [];
+        $daysOld = min(abs(intval($daysOld)), 30);
+        $sql = "SELECT distinct biblionumber as id
+                FROM items
+                WHERE itemlost = 0
+                   and dateaccessioned > DATE_ADD(CURRENT_TIMESTAMP,
+                      INTERVAL -$daysOld day)
+                ORDER BY dateaccessioned DESC";
+
+        if (!$this->db) {
+            $this->initDb();
+        }
+
+        $this->debug($sql);
+
+        $itemSqlStmt = $this->db->prepare($sql);
+        $itemSqlStmt->execute();
+
+        $rescount = 0;
+        foreach ($itemSqlStmt->fetchAll() as $rowItem) {
+            $items[] =  [
+                'id' => $rowItem['id']
+            ];
+            $rescount++;
+        }
+
+        $this->debug($rescount . " fetched");
+
+        $results = array_slice($items, ($page - 1) * $limit, ($page * $limit) - 1);
+        return ['count' => $rescount, 'results' => $results];
+    }
+
+    /**
+     * Get Hold Link
+     *
+     * The goal for this method is to return a URL to a "place hold" web page on
+     * the ILS OPAC. This is used for ILSs that do not support an API or method
+     * to place Holds.
+     *
+     * @param string $id      The id of the bib record
+     * @param array  $details Item details from getHoldings return array
+     *
+     * @return string         URL to ILS's OPAC's place hold screen.
+     *
+     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+     */
+    /*public function getHoldLink($id, $details)
+    {
+        // Web link of the ILS for placing hold on the item
+        return $this->ilsBaseUrl . "/cgi-bin/koha/opac-reserve.pl?biblionumber=$id";
+    }*/
+
+    /**
+     * Get Patron Fines
+     *
+     * This is responsible for retrieving all fines by a specific patron.
+     *
+     * @param array $patron The patron array from patronLogin
+     *
+     * @throws \VuFind\Exception\Date
+     * @throws ILSException
+     * @return mixed        Array of the patron's fines on success.
+     */
+    public function getMyFines($patron)
+    {
+        $id = 0;
+        $transactionLst = [];
+        $row = $sql = $sqlStmt = '';
+        try {
+            $id = $patron['id'];
+            $sql = "SELECT al.amount*100 as amount, " .
+                   "al.amountoutstanding*100 as balance, al.accounttype as fine, " .
+                   "al.date as createdat, items.biblionumber as id, " .
+                   "al.description as title " .
+                   "FROM `accountlines` al " .
+                   "LEFT JOIN items USING (itemnumber) " .
+                   "WHERE al.borrowernumber = :id ";
+            if (!$this->db) {
+                $this->initDB();
+            }
+            $sqlStmt = $this->db->prepare($sql);
+            $sqlStmt->execute([':id' => $id]);
+            foreach ($sqlStmt->fetchAll() as $row) {
+                switch ($row['fine'])
+                {
+                case 'A':
+                    $fineValue = "Account Management Fee";
+                    break;
+                case 'C':
+                    $fineValue = "Credit";
+                    break;
+                case "CR":
+                    $fineValue = "Credit for Returning Lost Book";
+                    break;
+                case "Copie":
+                    $fineValue = "Copier Fee";
+                    break;
+                case 'F':
+                    $fineValue = "Overdue Fine";
+                    break;
+                case "FFOR":
+                    $fineValue = "Forgiven Overdue Fine";
+                    break;
+                case "FOR":
+                    $fineValue = "Forgiven";
+                    break;
+                case "FU":
+                    $fineValue = "Overdue Fine";
+                    break;
+                case 'L':
+                    $fineValue = "Lost Item";
+                    break;
+                case "LR":
+                    $fineValue = "Lost and Returned";
+                    break;
+                case 'M':
+                    $fineValue = "Sundry";
+                    break;
+                case 'N':
+                    $fineValue = "New Card";
+                    break;
+                case 'O':
+                    $fineValue = "Overdue Fine";
+                    break;
+                case "Pay":
+                    $fineValue = "Payment";
+                    break;
+                case "REF":
+                    $fineValue = "Refund";
+                    break;
+                case "Rent":
+                    $fineValue = "Rental Fee";
+                    break;
+                case "Rep":
+                    $fineValue = "Replacement";
+                    break;
+                case "Res":
+                    $fineValue = "Reserve Charge";
+                    break;
+                case 'W':
+                    $fineValue = "Charge Written Off";
+                    break;
+                default:
+                    $fineValue = "Unknown Charge";
+                    break;
+                }
+ 
+                $transactionLst[] = [
+                           'amount'     => $row['amount'],
+                           'checkout'   => "N/A",
+                           'title'      => $row['title'],
+                           'fine'       => $fineValue,
+                           'balance'    => $row['balance'],
+                           'createdate' => $row['createdat'],
+                           'duedate' => date_format(
+                               new \DateTime($row['duedate']), "m/d/Y"
+                           ),
+                           'duedate'    => "N/A",
+                           'id'         => isset($row['id']) ? $row['id'] : -1,
+                ];
+            }
+            return $transactionLst;
+        }
+        catch (PDOException $e) {
+            throw new ILSException($e->getMessage());
+        }
+    }
+
+    /**
+     * Get Patron Fines
+     *
+     * This is responsible for retrieving all fines by a specific patron.
+     *
+     * @param array $patron The patron array from patronLogin
+     *
+     * @throws \VuFind\Exception\Date
+     * @throws ILSException
+     * @return mixed        Array of the patron's fines on success.
+     */
+    public function getMyFinesILS($patron)
+    {
+        $id = $patron['id'];
+        $fineLst = [];
+
+        $rsp = $this->makeRequest(
+            "GetPatronInfo&patron_id=$id" . "&show_contact=0&show_fines=1"
+        );
+
+        $this->debug("ID: " . $rsp->{'borrowernumber'});
+        $this->debug("Chrgs: " . $rsp->{'charges'});
+
+        foreach ($rsp->{'fines'}->{'fine'} as $fine) {
+            $fineLst[] = [
+                'amount'     => 100 * $this->getField($fine->{'amount'}),
+                // FIXME: require accountlines.itemnumber -> issues.issuedate data
+                'checkout'   => "N/A",
+                'fine'       => $this->getField($fine->{'description'}),
+                'balance'    => 100 * $this->getField($fine->{'amountoutstanding'}),
+                'createdate' => $this->getField($fine->{'date'}),
+                // FIXME: require accountlines.itemnumber -> issues.date_due data.
+                'duedate'    => "N/A",
+                // FIXME: require accountlines.itemnumber -> items.biblionumber data
+                'id'         => "N/A",
+            ];
+        }
+        return $fineLst;
+    }
+
+    /**
+     * Get Patron Holds
+     *
+     * This is responsible for retrieving all holds by a specific patron.
+     *
+     * @param array $patron The patron array from patronLogin
+     *
+     * @throws \VuFind\Exception\Date
+     * @throws ILSException
+     * @return array        Array of the patron's holds on success.
+     */
+    public function getMyHolds($patron)
+    {
+        $id = $patron['id'];
+        $holdLst = [];
+
+        $rsp = $this->makeRequest(
+            "GetPatronInfo&patron_id=$id" . "&show_contact=0&show_holds=1"
+        );
+
+        $this->debug("ID: " . $rsp->{'borrowernumber'});
+
+        foreach ($rsp->{'holds'}->{'hold'} as $hold) {
+            $holdLst[] = [
+                'id'       => $this->getField($hold->{'biblionumber'}),
+                'location' => $this->getField($hold->{'branchname'}),
+                // FIXME: require exposure of reserves.expirationdate
+                'expire'   => "N/A",
+                'create'   => date_format(
+                    new \DateTime($this->getField($hold->{'reservedate'})), "m/d/Y"
+                ),
+                'position' => $this->getField($hold->{'priority'}),
+                'title' => $this->getField($hold->{'title'}),
+                'available' => ($this->getField($hold->{'found'}) == "W"),
+                'reserve_id' => $this->getField($hold->{'reserve_id'}),
+            ];
+        }
+        return $holdLst;
+    }
+
+    /**
+     * Get Cancel Hold Details
+     *
+     * In order to cancel a hold, Koha requires the patron details and
+     * an item ID. This function returns the item id as a string. This
+     * value is then used by the CancelHolds function.
+     *
+     * @param array $holdDetails An array of item data
+     *
+     * @return string Data for use in a form field
+     */
+    public function getCancelHoldDetails($holdDetails)
+    {
+        return $holdDetails['reserve_id'];
+    }
+
+    /**
+     * Cancel Holds
+     *
+     * Attempts to Cancel a hold or recall on a particular item. The
+     * data in $cancelDetails['details'] is determined by getCancelHoldDetails().
+     *
+     * @param array $cancelDetails An array of item and patron data
+     *
+     * @return array               An array of data on each request including
+     * whether or not it was successful and a system message (if available)
+     */
+    public function cancelHolds($cancelDetails)
+    {
+        $retVal         = ['count' => 0, 'items' => []];
+        $details        = $cancelDetails['details'];
+        $patron_id      = $cancelDetails['patron']['id'];
+        $request_prefix = "CancelHold&patron_id=" . $patron_id . "&item_id=";
+
+        foreach ($details as $cancelItem) {
+            $rsp = $this->makeRequest($request_prefix . $cancelItem);
+            if ($rsp->{'code'} != "Canceled") {
+                $retVal['items'][$cancelItem] = [
+                    'success'    => false,
+                    'status'     => 'hold_cancel_fail',
+                    'sysMessage' => $this->getField($rsp->{'code'}),
+                ];
+            } else {
+                $retVal['count']++;
+                $retVal['items'][$cancelItem] = [
+                    'success' => true,
+                    'status' => 'hold_cancel_success',
+                ];
+            }
+        }
+        return $retVal;
+    }
+
+    /**
+     * Get Patron Profile
+     *
+     * This is responsible for retrieving the profile for a specific patron.
+     *
+     * @param array $patron The patron array
+     *
+     * @throws ILSException
+     * @return array        Array of the patron's profile data on success.
+     */
+    public function getMyProfile($patron)
+    {
+        $id = $patron['id'];
+        $profile = [];
+
+        $rsp = $this->makeRequest(
+            "GetPatronInfo&patron_id=$id" . "&show_contact=1"
+        );
+
+        $this->debug("Code: " . $rsp->{'code'});
+        $this->debug("Cardnumber: " . $rsp->{'cardnumber'});
+
+        if ($rsp->{'code'} != 'PatronNotFound') {
+            $profile = [
+                'firstname' => $this->getField($rsp->{'firstname'}),
+                'lastname'  => $this->getField($rsp->{'surname'}),
+                'address1'  => $this->getField($rsp->{'address'}),
+                'address2'  => $this->getField($rsp->{'address2'}),
+                'zip'       => $this->getField($rsp->{'zipcode'}),
+                'phone'     => $this->getField($rsp->{'phone'}),
+                'group'     => $this->getField($rsp->{'categorycode'}),
+            ];
+            return $profile;
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * Get Patron Transactions
+     *
+     * This is responsible for retrieving all transactions (i.e. checked out items)
+     * by a specific patron.
+     *
+     * @param array $patron The patron array from patronLogin
+     *
+     * @throws \VuFind\Exception\Date
+     * @throws ILSException
+     * @return array        Array of the patron's transactions on success.
+     */
+    public function getMyTransactions($patron)
+    {
+        echo "<!--";
+        $id = $patron['id'];
+        $transactionLst = [];
+        $start = microtime(true);
+        $rsp = $this->makeRequest(
+            "GetPatronInfo&patron_id=$id" . "&show_contact=0&show_loans=1"
+        );
+        $end = microtime(true);
+        $requestTimes[] = $end - $start;
+
+        $this->debug("ID: " . $rsp->{'borrowernumber'});
+
+        foreach ($rsp->{'loans'}->{'loan'} as $loan) {
+            $start = microtime(true);
+            $rsp2 = $this->makeIlsdiRequest(
+                "GetServices", [
+                    "patron_id" => $id,
+                    "item_id" => $this->getField($loan->{'itemnumber'})
+                ]
+            );
+            $end = microtime(true);
+            $requestTimes[] = $end - $start;
+            $renewable = false;
+            foreach ($rsp2->{'AvailableFor'} as $service) {
+                if ($this->getField((string)$service) == "loan renewal") {
+                    $renewable = true;
+                }
+            }
+
+            $transactionLst[] = [
+                'duedate'   => date_format(
+                    new \DateTime(
+                        $this->getField($loan->{'date_due'})
+                    ),
+                    "m/d/Y"
+                ),
+                'id'        => $this->getField($loan->{'biblionumber'}),
+                'item_id'   => $this->getField($loan->{'itemnumber'}),
+                'barcode'   => $this->getField($loan->{'barcode'}),
+                'renew'     => $this->getField($loan->{'renewals'}, '0'),
+                'renewable' => $renewable,
+            ];
+        }
+        foreach ($requestTimes as $time) {
+            echo "\n$time\n";
+        }
+        echo "-->";
+        return $transactionLst;
+    }
+
+    /**
+     * Get Renew Details
+     *
+     * In order to renew an item, Koha requires the patron details and
+     * an item id. This function returns the item id as a string which
+     * is then used as submitted form data in checkedOut.php. This
+     * value is then extracted by the RenewMyItems function.
+     *
+     * @param array $checkOutDetails An array of item data
+     *
+     * @return string Data for use in a form field
+     */
+    public function getRenewDetails($checkOutDetails)
+    {
+        return $checkOutDetails['item_id'];
+    }
+
+    /**
+     * Renew My Items
+     *
+     * Function for attempting to renew a patron's items.  The data in
+     * $renewDetails['details'] is determined by getRenewDetails().
+     *
+     * @param array $renewDetails An array of data required for
+     * renewing items including the Patron ID and an array of renewal
+     * IDS
+     *
+     * @return array An array of renewal information keyed by item ID
+     */
+    public function renewMyItems($renewDetails)
+    {
+        $retVal         = ['blocks' => false, 'details' => []];
+        $details        = $renewDetails['details'];
+        $patron_id      = $renewDetails['patron']['id'];
+        $request_prefix = "RenewLoan&patron_id=" . $patron_id . "&item_id=";
+
+        foreach ($details as $renewItem) {
+            $rsp = $this->makeRequest($request_prefix . $renewItem);
+            if ($rsp->{'success'} != '0') {
+                list($date, $time)
+                    = explode(" ", $this->getField($rsp->{'date_due'}));
+                $retVal['details'][$renewItem] = [
+                    "success"  => true,
+                    "new_date" => $date,
+                    "new_time" => $time,
+                    "item_id"  => $renewItem,
+                ];
+            } else {
+                $retVal['details'][$renewItem] = [
+                    "success"    => false,
+                    "new_date"   => false,
+                    "item_id"    => $renewItem,
+                    //"sysMessage" => $this->getField($rsp->{'error'}),
+                ];
+            }
+        }
+        return $retVal;
+    }
+
+    /**
+     * Get Purchase History
+     *
+     * This is responsible for retrieving the acquisitions history data for the
+     * specific record (usually recently received issues of a serial).
+     *
+     * @param string $id The record id to retrieve the info for
+     *
+     * @throws ILSException
+     * @return array An array with the acquisitions data on success.
+     *
+     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+     */
+    public function getPurchaseHistory($id)
+    {
+        try {
+            if (!$this->db) {
+                $this->initDb();
+            }
+            $sql = "SELECT b.title, b.biblionumber,
+                       MAX(CONCAT(s.publisheddate, ' / ',s.serialseq))
+                         AS 'date and enumeration'
+                    FROM serial s
+                    LEFT JOIN biblio b USING (biblionumber)
+                    WHERE s.STATUS=2 and b.biblionumber = :id
+                    GROUP BY b.biblionumber
+                    ORDER BY s.publisheddate DESC";
+
+            $sqlStmt = $this->db->prepare($sql);
+            $sqlStmt->execute(['id' => $id]);
+
+            $result = [];
+            foreach ($sqlStmt->fetchAll() as $rowItem) {
+                $result[] = ['issue' => $rowItem["date and enumeration"]];
+            }
+        } catch (PDOException $e) {
+            throw new ILSException($e->getMessage());
+        }
+        return $result;
+    }
+
+    /**
+     * Get Status
+     *
+     * This is responsible for retrieving the status information of a certain
+     * record.
+     *
+     * @param string $id The record id to retrieve the holdings for
+     *
+     * @throws ILSException
+     * @return mixed     On success, an associative array with the following keys:
+     * id, availability (boolean), status, location, reserve, callnumber.
+     */
+    public function getStatus($id)
+    {
+        return $this->getHolding($id);
+    }
+
+    /**
+     * Get Statuses
+     *
+     * This is responsible for retrieving the status information for a
+     * collection of records.
+     *
+     * @param array $idLst The array of record ids to retrieve the status for
+     *
+     * @throws ILSException
+     * @return array       An array of getStatus() return values on success.
+     */
+    public function getStatuses($idLst)
+    {
+        $this->debug("IDs:" . implode(',', $idLst));
+
+        $statusLst = [];
+        foreach ($idLst as $id) {
+            $statusLst[] = $this->getStatus($id);
+        }
+        return $statusLst;
+    }
+
+    /**
+     * Get suppressed records.
+     *
+     * @throws ILSException
+     * @return array ID numbers of suppressed records in the system.
+     */
+    public function getSuppressedRecords()
+    {
+        try {
+            if (!$this->db) {
+                $this->initDb();
+            }
+            $sql = "SELECT biblio.biblionumber AS biblionumber
+                      FROM biblioitems
+                      JOIN biblio USING (biblionumber)
+                      WHERE ExtractValue(
+                         marcxml, '//datafield[@tag=\"942\"]/subfield[@code=\"n\"]' )
+                      IN ('Y', '1')";
+            $sqlStmt = $this->db->prepare($sql);
+            $sqlStmt->execute();
+            $result = [];
+            foreach ($sqlStmt->fetchAll() as $rowItem) {
+                $result[] = $rowItem["biblionumber"];
+            }
+        } catch (PDOException $e) {
+            throw new ILSException($e->getMessage());
+        }
+        return $result;
+    }
+
+    /**
+     * Get Departments
+     *
+     * @throws ILSException
+     * @return array An associative array with key = ID, value = dept. name.
+     */
+    public function getDepartments()
+    {
+        $deptList = [];
+
+        $sql = "SELECT DISTINCT department as abv, lib_opac AS DEPARTMENT
+                 FROM courses
+                 INNER JOIN `authorised_values`
+                    ON courses.department = `authorised_values`.`authorised_value`";
+        try {
+            if (!$this->db) {
+                $this->initDb();
+            }
+            $sqlStmt = $this->db->prepare($sql);
+            $sqlStmt->execute();
+            $result = [];
+            foreach ($sqlStmt->fetchAll() as $rowItem) {
+                $deptList[$rowItem["abv"]] = $rowItem["DEPARTMENT"];
+            }
+        } catch (PDOException $e) {
+            throw new ILSException($e->getMessage());
+        }
+        return $deptList;
+    }
+
+    /**
+     * Get Instructors
+     *
+     * @throws ILSException
+     * @return array An associative array with key = ID, value = name.
+     */
+    public function getInstructors()
+    {
+        $instList = [];
+
+        $sql = "SELECT DISTINCT borrowernumber,
+                       CONCAT(firstname, ' ', surname) AS name
+                 FROM course_instructors
+                 LEFT JOIN borrowers USING(borrowernumber)";
+
+        try {
+            if (!$this->db) {
+                $this->initDb();
+            }
+            $sqlStmt = $this->db->prepare($sql);
+            $sqlStmt->execute();
+            $result = [];
+            foreach ($sqlStmt->fetchAll() as $rowItem) {
+                $instList[$rowItem["borrowernumber"]] = $rowItem["name"];
+            }
+        } catch (PDOException $e) {
+            throw new ILSException($e->getMessage());
+        }
+        return $instList;
+    }
+
+    /**
+     * Get Courses
+     *
+     * @throws ILSException
+     * @return array An associative array with key = ID, value = name.
+     */
+    public function getCourses()
+    {
+        $courseList = [];
+
+        $sql = "SELECT course_id,
+                CONCAT (course_number, ' - ', course_name) AS course
+                 FROM courses
+                 WHERE enabled = 1";
+        try {
+            if (!$this->db) {
+                $this->initDb();
+            }
+            $sqlStmt = $this->db->prepare($sql);
+            $sqlStmt->execute();
+            $result = [];
+            foreach ($sqlStmt->fetchAll() as $rowItem) {
+                $courseList[$rowItem["course_id"]] = $rowItem["course"];
+            }
+        } catch (PDOException $e) {
+            throw new ILSException($e->getMessage());
+        }
+        return $courseList;
+    }
+
+    /**
+     * Find Reserves
+     *
+     * Obtain information on course reserves.
+     *
+     * This version of findReserves was contributed by Matthew Hooper and includes
+     * support for electronic reserves (though eReserve support is still a work in
+     * progress).
+     *
+     * @param string $course ID from getCourses (empty string to match all)
+     * @param string $inst   ID from getInstructors (empty string to match all)
+     * @param string $dept   ID from getDepartments (empty string to match all)
+     *
+     * @throws ILSException
+     * @return array An array of associative arrays representing reserve items.
+     */
+    public function findReserves($course, $inst, $dept)
+    {
+        $recordList = [];
+        $reserveWhere = [];
+        $bindParams = [];
+        if ($course != '') {
+            $reserveWhere[] = "COURSE_ID = :course";
+            $bindParams[':course'] = $course;
+        }
+        if ($inst != '') {
+            $reserveWhere[] = "INSTRUCTOR_ID = :inst";
+            $bindParams[':inst'] = $inst;
+        }
+        if ($dept != '') {
+            $reserveWhere[] = "DEPARTMENT_ID = :dept";
+            $bindParams[':dept'] = $dept;
+        }
+        $reserveWhere = empty($reserveWhere) ?
+            "" : "where (" . implode(' AND ', $reserveWhere) . ")";
+
+        $sql = "SELECT biblionumber AS `BIB_ID`,
+                       courses.course_id AS COURSE_ID,
+                       course_instructors.borrowernumber as INSTRUCTOR_ID,
+                       courses.department AS DEPARTMENT_ID
+                FROM courses
+                INNER JOIN `authorised_values`
+                   ON courses.department = `authorised_values`.`authorised_value`
+                INNER JOIN `course_reserves` USING (course_id)
+                INNER JOIN `course_items` USING (ci_id)
+                INNER JOIN `items` USING (itemnumber)
+                INNER JOIN `course_instructors` USING (course_id)
+                INNER JOIN `borrowers` USING (borrowernumber)
+                WHERE courses.enabled = 'yes'" . $reserveWhere;
+
+        try {
+            $sqlStmt = $this->db->prepare($sql, $bindParams);
+            $sqlStmt->execute();
+            $result = [];
+            foreach ($sqlStmt->fetchAll() as $rowItem) {
+                $result[] = $rowItem;
+            }
+        } catch (PDOException $e) {
+            throw new ILSException($e->getMessage());
+        }
+        return $result;
+    }
+
+    /**
+     * Patron Login
+     *
+     * This is responsible for authenticating a patron against the catalog.
+     *
+     * @param string $username The patron username
+     * @param string $password The patron's password
+     *
+     * @throws ILSException
+     * @return mixed          Associative array of patron info on successful login,
+     * null on unsuccessful login.
+     */
+    public function patronLogin($username, $password)
+    {
+        $patron = [];
+
+        //       $idObj = $this->makeRequest(
+        //         "AuthenticatePatron" . "&username=" . $username
+        //       . "&password=" . $password
+        // );
+        $idObj = $this->makeRequest(
+            "LookupPatron" . "&id=" . urlencode($username)
+            . "&id_type=userid"
+        );
+
+        $this->debug("username: " . $username);
+        $this->debug("Code: " . $idObj->{'code'});
+        $this->debug("ID: " . $idObj->{'id'});
+
+        $id = $this->getField($idObj->{'id'}, 0);
+        if ($id) {
+            $rsp = $this->makeRequest(
+                "GetPatronInfo&patron_id=$id&show_contact=1"
+            );
+            $profile = [
+                'id'           => $this->getField($idObj->{'id'}),
+                'firstname'    => $this->getField($rsp->{'firstname'}),
+                'lastname'     => $this->getField($rsp->{'lastname'}),
+                'cat_username' => $username,
+                'cat_password' => $password,
+                'email'        => $this->getField($rsp->{'email'}),
+                'major'        => null,
+                'college'      => null,
+            ];
+            return $profile;
+        } else {
+            return null;
+        }
+    }
+}
diff --git a/module/VuFind/src/VuFind/ILS/Driver/LBS4.php b/module/VuFind/src/VuFind/ILS/Driver/LBS4.php
index 75397199c3de8d272580f191638b7f520a4fe52b..84b5cf02ca2d986350be06fafa4940a3f6da2add 100644
--- a/module/VuFind/src/VuFind/ILS/Driver/LBS4.php
+++ b/module/VuFind/src/VuFind/ILS/Driver/LBS4.php
@@ -15,7 +15,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  ILS_Drivers
@@ -36,10 +36,20 @@ use VuFind\I18n\Translator\TranslatorAwareInterface;
  * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
  * @link     https://vufind.org/wiki/development:plugins:ils_drivers Wiki
  */
-class LBS4 extends AbstractBase implements TranslatorAwareInterface
+class LBS4 extends DAIA implements TranslatorAwareInterface
 {
     use \VuFind\I18n\Translator\TranslatorAwareTrait;
 
+    /**
+     * Constructor
+     *
+     * @param \VuFind\Date\Converter $converter Date converter
+     */
+    public function __construct(\VuFind\Date\Converter $converter)
+    {
+        $this->dateConverter = $converter;
+    }
+
     /**
      * Database connection
      *
@@ -79,6 +89,8 @@ class LBS4 extends AbstractBase implements TranslatorAwareInterface
      */
     public function init()
     {
+        parent::init(); // initialize DAIA parent
+
         if (isset($this->config['Catalog']['opaciln'])) {
             $this->opaciln = $this->config['Catalog']['opaciln'];
         }
@@ -88,11 +100,9 @@ class LBS4 extends AbstractBase implements TranslatorAwareInterface
         if (isset($this->config['Catalog']['opcloan'])) {
             $this->opcloan = $this->config['Catalog']['opcloan'];
         }
-        if (function_exists("sybase_pconnect")
-            && isset($this->config['Catalog']['database'])
-        ) {
+        if (isset($this->config['Catalog']['database'])) {
             putenv("SYBASE=" . $this->config['Catalog']['sybpath']);
-            $this->db = sybase_pconnect(
+            $this->db = sybase_connect(
                 $this->config['Catalog']['sybase'],
                 $this->config['Catalog']['username'],
                 $this->config['Catalog']['password']
@@ -119,352 +129,6 @@ class LBS4 extends AbstractBase implements TranslatorAwareInterface
         return isset($this->config[$function]) ? $this->config[$function] : false;
     }
 
-    /**
-     * Get Status
-     *
-     * This is responsible for retrieving the status information of a certain
-     * record.
-     *
-     * @param string $ppn The record id to retrieve the holdings for
-     *
-     * @throws ILSException
-     * @return mixed     On success, an associative array with the following keys:
-     * ppn, availability (boolean), status, location, reserve, callnumber.
-     */
-    public function getStatus($ppn)
-    {
-        $sybid = substr($ppn, 0, -1); //strip checksum
-        $sql = "select o.loan_indication, o.signature, v.loan_status"
-             . " from ous_copy_cache o, volume v"
-             . " where o.iln=" . $this->opaciln
-             . " and o.ppn=" . $sybid
-             . " and o.epn *= v.epn"; //outer join
-        try {
-            $sqlStmt = sybase_query($sql);
-            $number = 0;
-            while ($row = sybase_fetch_row($sqlStmt)) {
-                $number++;
-                $loan_indi  = $row[0];
-                $label = substr($row[1], 4);
-                $locid = substr($row[1], 0, 3);
-                $location = $this->translate($this->opaciln . "/" . $locid);
-                $loan_status  = $row[2];
-
-                $reserve = 'N';
-                $status = $this->getStatusText($loan_indi, $loan_status);
-                $available = true;
-
-                if ($loan_indi == 7) {
-                    $available = false; //not for loan
-                } else if ($loan_indi == 8) {
-                    $available = false; //missed items
-                } else if ($loan_indi == 9) {
-                    $available = false; //not ready yet
-                }
-                if ($loan_status == 5) {
-                    $available = false;
-                } else if ($loan_status == 4) {
-                    $available = false;
-                }
-
-                $holding[] = [
-                    'id'             => $ppn,
-                    'availability'   => $available,
-                    'status'         => $status,
-                    'location'       => $location,
-                    'reserve'        => $reserve,
-                    'callnumber'     => $label,
-                ];
-            }
-        } catch (\Exception $e) {
-            throw new ILSException($e->getMessage());
-        }
-        return $holding;
-    }
-
-    /**
-     * Get status text
-     *
-     * @param string $indi   Indicator value
-     * @param string $status status as retrieved from db
-     *
-     * @return string the message to be displayed
-     */
-    protected function getStatusText($indi, $status)
-    {
-        if ($indi == 0 && $status == 0) {
-            $text = 'Available';
-        } else if ($indi == 0 && $status == 4) {
-            $text = 'On Reserve';
-        } else if ($indi == 0 && $status == 5) {
-            $text = 'Checked Out';
-        } else if ($indi == 3) {
-            $text = 'Presence';
-        } else {
-            $text = 'Not Available';
-        }
-        return $text;
-    }
-
-    /**
-     * Get Holding
-     *
-     * This is responsible for retrieving the holding information of a certain
-     * record.
-     *
-     * @param string $ppn    The record id to retrieve the holdings for
-     * @param array  $patron Patron data
-     *
-     * @return array         On success, an associative array with the following
-     * keys: id, availability (boolean), status, location, reserve, callnumber,
-     * duedate, number, barcode.
-     *
-     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
-     */
-    public function getHolding($ppn, array $patron = null)
-    {
-        $sybid = substr($ppn, 0, -1); //strip checksum
-        $sql = "select o.epn, o.loan_indication"
-             . ", v.volume_bar, v.loan_status"
-             . ", v.volume_number, o.signature"
-             . ", o.holding, o.type_of_material_copy"
-             . " from ous_copy_cache o, volume v, titles_copy t"
-             . " where o.iln=" . $this->opaciln
-             . " and o.ppn=" . $sybid
-             . " and o.epn *= v.epn"//outer join
-             . " and t.epn = o.epn"
-             . " and t.iln = o.iln"
-             . " and t.fno = o.fno"
-             . " order by o.signature";
-        try {
-            $sqlStmt = sybase_query($sql);
-            $holding = [];
-            while ($row = sybase_fetch_row($sqlStmt)) {
-                $epn   = $row[0];
-                $loan_indi  = (string)$row[1];
-                $volbar = $row[2];
-                $loan_status = (string)$row[3];
-                $volnum = $row[4];
-
-                //library location identifier is a callnumber prefix
-                $locid = substr($row[5], 0, 3);
-
-                //suppress multiple callnumbers, comma separated items
-                $callnumber = current(explode(',', substr($row[5], 4)));
-
-                if ($locid != '') {
-                    $location = $this->opaciln . "/" . $locid;
-                }
-                if ($row[6] != '') {
-                    $summary = [$row[6]];
-                }
-                $material = $row[7];
-
-                $check = false;
-                $reserve = 'N';
-                $is_holdable = false;
-
-                $storage = $this->getStorage($loan_indi, $locid, $callnumber);
-                $status = $this->getStatusText($loan_indi, $loan_status);
-                $note = $this->getNote($loan_indi, $locid, $callnumber);
-
-                if (empty($storage)) {
-                    $check = $this->checkHold($loan_indi, $material);
-                } else if (empty($volbar)) {
-                    $volbar = $locid;
-                }
-
-                $available = true;
-                if ($loan_status == '') {
-                    $available = false;
-                } else if ($loan_status == 4) {
-                    $available = false;
-                    $reserve = 'Y';
-                } else if ($loan_status == 5) {
-                    $available = false;
-                    $duedate = $this->getLoanexpire($volnum);
-                    $is_holdable = true;
-                } else if ($loan_indi > 6) {
-                    $available = false;
-                }
-
-                $holding[] = [
-                    'id'             => $ppn,
-                    'availability'   => $available,
-                    'status'         => $status,
-                    'location'       => $location,
-                    'reserve'        => $reserve,
-                    'callnumber'     => $callnumber,
-                    'duedate'        => $duedate,
-                    'number'         => $volbar,
-                    'barcode'        => $volbar,
-                    'notes'          => [$note],
-                    'summary'        => $summary,
-                    'is_holdable'    => $is_holdable,
-                    'item_id'        => $epn,
-                    'check'          => $check,
-                    'storageRetrievalRequestLink' => $storage,
-                    'checkStorageRetrievalRequest' => !empty($storage),
-                    'material'       => $material,
-                ];
-            }
-        } catch (\Exception $e) {
-            throw new ILSException($e->getMessage());
-        }
-        return $holding;
-    }
-
-    /**
-     * Test whether holds needs to be checked
-     *
-     * @param string $loanindi The loan indicator
-     * @param string $material The material code
-     *
-     * @return bool
-     */
-    protected function checkHold($loanindi, $material)
-    {
-        if ($loanindi == 0) {
-            if (substr($material, 0, 2) == 'Ab') {
-                return true;
-            }
-        } else if ($loanindi == 3) {
-            return true;
-        } else if ($loanindi == 6) {
-            return true;
-        } else {
-            return false;
-        }
-    }
-
-    /**
-     * Get Notes
-     *
-     * This is responsible for retrieving library specific
-     * notes for a record. You may want to override this.
-     *
-     * @param string $loanind    The library loan indicator
-     * @param string $locid      The library location identifier
-     * @param array  $callnumber The callnumber of the item
-     *
-     * @return string On success, a string to be displayed near the item
-     *
-     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
-     */
-    protected function getNote($loanind, $locid, $callnumber)
-    {
-        if ($loanind == 0 && $locid == '000') {
-            $note = $this->translate("Textbook Collection");
-        } else if ($loanind == 1) {
-            $note = $this->translate("Short loan");//Short time loan?
-        } else if ($loanind == 2) {
-            $note = "Interlibrary Loan";
-        } else if ($loanind == 3) {
-            $note = $this->translate("Presence");
-        } else if ($loanind == 8) {
-            $note = $this->translate("Missed");
-        } else if ($loanind == 9) {
-            $note = $this->translate("In Progress");
-        }
-        return $note;
-    }
-
-    /**
-     * Get Storage
-     *
-     * This is responsible for retrieving library specific
-     * storage urls if available, i.e. bibmap links.
-     *
-     * You may want to override this.
-     *
-     * @param string $locid      The library location identifier
-     * @param array  $callnumber The callnumber of the item
-     *
-     * @return string On success, a url string to be displayed as
-     * storage link.
-     *
-     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
-     */
-    protected function getStorage($locid, $callnumber)
-    {
-        return false;
-    }
-
-    /**
-     * Get Loanexpire
-     *
-     * This is responsible for retrieving loan expiration dates
-     * for an item.
-     *
-     * @param string $vol The volume number
-     *
-     * @return string On success, a string to be displayed as
-     *                loan expiration date.
-     */
-    protected function getLoanexpire($vol)
-    {
-        $sql = "select expiry_date_loan from loans_requests"
-             . " where volume_number=" . $vol . "";
-        try {
-            $sqlStmt = sybase_query($sql);
-            $result = false;
-            if ($row = sybase_fetch_row($sqlStmt)) {
-                $result = $row[0];
-            }
-            if ($result) {
-                return substr($result, 0, 12);
-            }
-        } catch (\Exception $e) {
-            throw new ILSException($e->getMessage());
-        }
-        return false;
-    }
-
-    /**
-     * Get Hold Link
-     *
-     * The goal for this method is to return a URL to a "place hold" web page on
-     * the ILS OPAC. This is used for ILSs that do not support an API or method
-     * to place Holds.
-     *
-     * @param string $id      The id of the bib record
-     * @param array  $details Item details from getHoldings return array
-     *
-     * @return string         URL to ILS's OPAC's place hold screen.
-     */
-    public function getHoldLink($id, $details)
-    {
-        if (isset($details['item_id'])) {
-            $epn = $details['item_id'];
-            $hold = $this->opcloan . "?MTR=mon"
-                        . "&BES=" . $this->opacfno
-                        . "&EPN=" . $this->prfz($epn);
-            return $hold;
-        }
-        return $this->opcloan . "?MTR=mon" . "&BES=" . $this->opacfno
-               . "&EPN=" . $this->prfz($id);
-    }
-
-    /**
-     * Get Statuses
-     *
-     * This is responsible for retrieving the status information for a
-     * collection of records.
-     *
-     * @param array $ids The array of record ids to retrieve the status for
-     *
-     * @throws ILSException
-     * @return array       An array of getStatus() return values on success.
-     */
-    public function getStatuses($ids)
-    {
-        $items = [];
-        foreach ($ids as $id) {
-            $items[] = $this->getStatus($id);
-        }
-        return $items;
-    }
-
     /**
      * Patron Login
      *
diff --git a/module/VuFind/src/VuFind/ILS/Driver/MultiBackend.php b/module/VuFind/src/VuFind/ILS/Driver/MultiBackend.php
index 5dd56520f21f3b0f5a4e4424881b1bfb66e5ccd6..19dc073a2fabbf9656b8ad44ff79431267ea4e15 100644
--- a/module/VuFind/src/VuFind/ILS/Driver/MultiBackend.php
+++ b/module/VuFind/src/VuFind/ILS/Driver/MultiBackend.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  ILSdrivers
@@ -701,8 +701,8 @@ class MultiBackend extends AbstractBase
     /**
      * Get request groups
      *
-     * @param integer $id     BIB ID
-     * @param array   $patron Patron information returned by the patronLogin
+     * @param int   $id     BIB ID
+     * @param array $patron Patron information returned by the patronLogin
      * method.
      *
      * @return array  An array of associative arrays with requestGroupId and
@@ -1044,7 +1044,8 @@ class MultiBackend extends AbstractBase
         if ($driver
             && $this->methodSupported($driver, 'placeILLRequest', compact($details))
         ) {
-            $details = $this->stripIdPrefixes($details, $source, ['id']);
+            // Patron is not stripped so that the correct library can be determined
+            $details = $this->stripIdPrefixes($details, $source, ['id'], ['patron']);
             return $driver->placeILLRequest($details);
         }
         throw new ILSException('No suitable backend driver found');
@@ -1178,7 +1179,7 @@ class MultiBackend extends AbstractBase
         }
         if (!$source) {
             try {
-                $patron = $this->ilsAuth->storedCatalogLogin();
+                $patron = $this->ilsAuth->getStoredCatalogCredentials();
                 if ($patron && isset($patron['cat_username'])) {
                     $source = $this->getSource($patron['cat_username']);
                 }
@@ -1425,12 +1426,13 @@ class MultiBackend extends AbstractBase
     * array or array of arrays
     * @param string $source       Source code
     * @param array  $modifyFields Fields to be modified in the array
+    * @param array  $ignoreFields Fields to be ignored during recursive processing
     *
     * @return mixed     Modified array or empty/null if that input was
     *                   empty/null
     */
     protected function stripIdPrefixes($data, $source,
-        $modifyFields = ['id', 'cat_username']
+        $modifyFields = ['id', 'cat_username'], $ignoreFields = []
     ) {
         if (!isset($data) || empty($data)) {
             return $data;
@@ -1439,6 +1441,9 @@ class MultiBackend extends AbstractBase
 
         foreach ($array as $key => $value) {
             if (is_array($value)) {
+                if (in_array($key, $ignoreFields)) {
+                    continue;
+                }
                 $array[$key] = $this->stripIdPrefixes(
                     $value, $source, $modifyFields
                 );
diff --git a/module/VuFind/src/VuFind/ILS/Driver/NewGenLib.php b/module/VuFind/src/VuFind/ILS/Driver/NewGenLib.php
index 35ec430300ded1c6f0485dc0700ef853d225d1a0..0f08ced0c6b0bcedff6e515b801399ec0236d184 100644
--- a/module/VuFind/src/VuFind/ILS/Driver/NewGenLib.php
+++ b/module/VuFind/src/VuFind/ILS/Driver/NewGenLib.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  ILS_Drivers
diff --git a/module/VuFind/src/VuFind/ILS/Driver/NoILS.php b/module/VuFind/src/VuFind/ILS/Driver/NoILS.php
index c72dc3923d95f10de48830dd8880cfe186319483..523ff210a03930419671cd6ce0d6f475f9e27887 100644
--- a/module/VuFind/src/VuFind/ILS/Driver/NoILS.php
+++ b/module/VuFind/src/VuFind/ILS/Driver/NoILS.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  ILS_Drivers
@@ -91,6 +91,17 @@ class NoILS extends AbstractBase implements TranslatorAwareInterface
         return isset($this->config[$function]) ? $this->config[$function] : false;
     }
 
+    /**
+     * Get the ID prefix from the configuration, if set.
+     *
+     * @return string
+     */
+    protected function getIdPrefix()
+    {
+        return isset($this->config['settings']['idPrefix'])
+            ? $this->config['settings']['idPrefix'] : null;
+    }
+
     /**
      * Get a Solr record.
      *
@@ -100,7 +111,9 @@ class NoILS extends AbstractBase implements TranslatorAwareInterface
      */
     protected function getSolrRecord($id)
     {
-        return $this->recordLoader->load($id);
+        // Add idPrefix condition
+        $idPrefix = $this->getIdPrefix();
+        return $this->recordLoader->load(strlen($idPrefix) ? $idPrefix . $id : $id);
     }
 
     /**
@@ -246,6 +259,14 @@ class NoILS extends AbstractBase implements TranslatorAwareInterface
             $result = $recordDriver->tryMethod(
                 'getFormattedMarcDetails', [$field, $marcStatus]
             );
+            // If the details coming back from the record driver include the
+            // ID prefix, strip it off!
+            $idPrefix = $this->getIdPrefix();
+            if (isset($result[0]['id']) && strlen($idPrefix)
+                && $idPrefix === substr($result[0]['id'], 0, strlen($idPrefix))
+            ) {
+                $result[0]['id'] = substr($result[0]['id'], strlen($idPrefix));
+            }
             return empty($result) ? [] : $result;
         }
         return [];
diff --git a/module/VuFind/src/VuFind/ILS/Driver/PAIA.php b/module/VuFind/src/VuFind/ILS/Driver/PAIA.php
new file mode 100644
index 0000000000000000000000000000000000000000..31712d4e294b582f0541dfaeeeeeb46cef36900d
--- /dev/null
+++ b/module/VuFind/src/VuFind/ILS/Driver/PAIA.php
@@ -0,0 +1,1776 @@
+<?php
+/**
+ * PAIA ILS Driver for VuFind to get patron information
+ *
+ * PHP version 5
+ *
+ * Copyright (C) Oliver Goldschmidt, Magda Roos, Till Kinstler, André Lahmann 2013,
+ * 2014, 2015.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ *
+ * @category VuFind
+ * @package  ILS_Drivers
+ * @author   Oliver Goldschmidt <o.goldschmidt@tuhh.de>
+ * @author   Magdalena Roos <roos@gbv.de>
+ * @author   Till Kinstler <kinstler@gbv.de>
+ * @author   André Lahmann <lahmann@ub.uni-leipzig.de>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     https://vufind.org/wiki/development:plugins:ils_drivers Wiki
+ */
+namespace VuFind\ILS\Driver;
+use VuFind\Exception\ILS as ILSException;
+
+/**
+ * PAIA ILS Driver for VuFind to get patron information
+ *
+ * Holding information is obtained by DAIA, so it's not necessary to implement those
+ * functions here; we just need to extend the DAIA driver.
+ *
+ * @category VuFind
+ * @package  ILS_Drivers
+ * @author   Oliver Goldschmidt <o.goldschmidt@tuhh.de>
+ * @author   Magdalena Roos <roos@gbv.de>
+ * @author   Till Kinstler <kinstler@gbv.de>
+ * @author   André Lahmann <lahmann@ub.uni-leipzig.de>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     https://vufind.org/wiki/development:plugins:ils_drivers Wiki
+ */
+class PAIA extends DAIA
+{
+    /**
+     * URL of PAIA service
+     *
+     * @var
+     */
+    protected $paiaURL;
+
+    /**
+     * Timeout in seconds to be used for PAIA http requests
+     *
+     * @var
+     */
+    protected $paiaTimeout = null;
+
+    /**
+     * Flag to switch on/off caching for PAIA items
+     *
+     * @var bool
+     */
+    protected $paiaCacheEnabled = false;
+
+    /**
+     * Session containing PAIA login information
+     *
+     * @var \Zend\Session\Container
+     */
+    protected $session;
+
+    /**
+     * SessionManager
+     *
+     * @var \VuFind\SessionManager
+     */
+    protected $sessionManager;
+
+    /**
+     * PAIA status strings
+     *
+     * @var array
+     */
+    protected static $statusStrings = [
+        '0' => 'no relation',
+        '1' => 'reserved',
+        '2' => 'ordered',
+        '3' => 'held',
+        '4' => 'provided',
+        '5' => 'rejected',
+    ];
+
+    /**
+     * PAIA constructor.
+     *
+     * @param \VuFind\Date\Converter       $converter      Date converter
+     * @param \Zend\Session\SessionManager $sessionManager Session Manager
+     */
+    public function __construct(\VuFind\Date\Converter $converter,
+        \Zend\Session\SessionManager $sessionManager
+    ) {
+        parent::__construct($converter);
+        $this->sessionManager = $sessionManager;
+    }
+
+    /**
+     * PAIA specific override of method to ensure uniform cache keys for cached
+     * VuFind objects.
+     *
+     * @param string|null $suffix Optional suffix that will get appended to the
+     * object class name calling getCacheKey()
+     *
+     * @return string
+     */
+    protected function getCacheKey($suffix = null)
+    {
+        return \VuFind\ILS\Driver\AbstractBase::getCacheKey(
+            md5($this->baseUrl . $this->paiaURL) . $suffix
+        );
+    }
+
+    /**
+     * Get the session container (constructing it on demand if not already present)
+     *
+     * @return SessionContainer
+     */
+    protected function getSession()
+    {
+        // SessionContainer not defined yet? Build it now:
+        if (null === $this->session) {
+            $this->session = new \Zend\Session\Container(
+                'PAIA', $this->sessionManager
+            );
+        }
+        return $this->session;
+    }
+
+    /**
+     * Get the session scope
+     *
+     * @return array Array of the Session scope
+     */
+    protected function getScope()
+    {
+        return $this->getSession()->scope;
+    }
+
+    /**
+     * Initialize the driver.
+     *
+     * Validate configuration and perform all resource-intensive tasks needed to
+     * make the driver active.
+     *
+     * @throws ILSException
+     * @return void
+     */
+    public function init()
+    {
+        parent::init();
+
+        if (!(isset($this->config['PAIA']['baseUrl']))) {
+            throw new ILSException('PAIA/baseUrl configuration needs to be set.');
+        }
+        $this->paiaURL = $this->config['PAIA']['baseUrl'];
+
+        // use PAIA specific timeout setting for http requests if configured
+        if ((isset($this->config['PAIA']['timeout']))) {
+            $this->paiaTimeout = $this->config['PAIA']['timeout'];
+        }
+
+        // do we have caching enabled for PAIA
+        if (isset($this->config['PAIA']['paiaCache'])) {
+            $this->paiaCacheEnabled = $this->config['PAIA']['paiaCache'];
+        } else {
+            $this->debug('Caching not enabled, disabling it by default.');
+        }
+    }
+
+    // public functions implemented to satisfy Driver Interface
+
+    /*
+    These methods are not implemented in the PAIA driver as they are probably
+    not necessary in PAIA context:
+    - findReserves
+    - getCancelHoldLink
+    - getConsortialHoldings
+    - getCourses
+    - getDepartments
+    - getHoldDefaultRequiredDate
+    - getInstructors
+    - getOfflineMode
+    - getSuppressedAuthorityRecords
+    - getSuppressedRecords
+    - hasHoldings
+    - loginIsHidden
+    - renewMyItemsLink
+    - supportsMethod
+    */
+
+    /**
+     * This method cancels a list of holds for a specific patron.
+     *
+     * @param array $cancelDetails An associative array with two keys:
+     *      patron   array returned by the driver's patronLogin method
+     *      details  an array of strings returned by the driver's
+     *               getCancelHoldDetails method
+     *
+     * @return array Associative array containing:
+     *      count   The number of items successfully cancelled
+     *      items   Associative array where key matches one of the item_id
+     *              values returned by getMyHolds and the value is an
+     *              associative array with these keys:
+     *                success    Boolean true or false
+     *                status     A status message from the language file
+     *                           (required – VuFind-specific message,
+     *                           subject to translation)
+     *                sysMessage A system supplied failure message
+     */
+    public function cancelHolds($cancelDetails)
+    {
+        $it = $cancelDetails['details'];
+        $items = [];
+        foreach ($it as $item) {
+            $items[] = ['item' => stripslashes($item)];
+        }
+        $patron = $cancelDetails['patron'];
+        $post_data = ["doc" => $items];
+
+        try {
+            $array_response = $this->paiaPostAsArray(
+                'core/' . $patron['cat_username'] . '/cancel', $post_data
+            );
+        } catch (ILSException $e) {
+            $this->debug($e->getMessage());
+            return [
+                'success' => false,
+                'status' => $e->getMessage(),
+            ];
+        }
+
+        $details = [];
+
+        if (isset($array_response['error'])) {
+            $details[] = [
+                'success' => false,
+                'status' => $array_response['error_description'],
+                'sysMessage' => $array_response['error']
+            ];
+        } else {
+            $count = 0;
+            $elements = $array_response['doc'];
+            foreach ($elements as $element) {
+                $item_id = $element['item'];
+                if ($element['error']) {
+                    $details[$item_id] = [
+                        'success' => false,
+                        'status' => $element['error'],
+                        'sysMessage' => 'Cancel request rejected'
+                    ];
+                } else {
+                    $details[$item_id] = [
+                        'success' => true,
+                        'status' => 'Success',
+                        'sysMessage' => 'Successfully cancelled'
+                    ];
+                    $count++;
+
+                    // DAIA cache cannot be cleared for particular item as PAIA only
+                    // operates with specific item URIs and the DAIA cache is setup
+                    // by doc URIs (containing items with URIs)
+                }
+            }
+
+            // If caching is enabled for PAIA clear the cache as at least for one
+            // item cancel was successfull and therefore the status changed.
+            // Otherwise the changed status will not be shown before the cache
+            // expires.
+            if ($this->paiaCacheEnabled) {
+                $this->removeCachedData($patron['cat_username']);
+            }
+        }
+        $returnArray = ['count' => $count, 'items' => $details];
+
+        return $returnArray;
+    }
+
+    /**
+     * Public Function which changes the password in the library system
+     * (not supported prior to VuFind 2.4)
+     *
+     * @param array $details Array with patron information, newPassword and
+     *                       oldPassword.
+     *
+     * @return array An array with patron information.
+     */
+    public function changePassword($details)
+    {
+        $post_data = [
+            "patron"       => $details['patron']['cat_username'],
+            "username"     => $details['patron']['cat_username'],
+            "old_password" => $details['oldPassword'],
+            "new_password" => $details['newPassword']
+        ];
+
+        try {
+            $array_response = $this->paiaPostAsArray(
+                'auth/change', $post_data
+            );
+        } catch (ILSException $e) {
+            $this->debug($e->getMessage());
+            return [
+                'success' => false,
+                'status' => $e->getMessage(),
+            ];
+        }
+
+        $details = [];
+
+        if (isset($array_response['error'])) {
+            // on error
+            $details = [
+                'success'    => false,
+                'status'     => $array_response['error'],
+                'sysMessage' =>
+                    isset($array_response['error'])
+                        ? $array_response['error'] : ' ' .
+                    isset($array_response['error_description'])
+                        ? $array_response['error_description'] : ' '
+            ];
+        } elseif ($array_response['patron'] === $post_data['patron']) {
+            // on success patron_id is returned
+            $details = [
+                'success' => true,
+                'status' => 'Successfully changed'
+            ];
+        } else {
+            $details = [
+                'success' => false,
+                'status' => 'Failure changing password',
+                'sysMessage' => serialize($array_response)
+            ];
+        }
+        return $details;
+    }
+
+    /**
+     * This method returns a string to use as the input form value for
+     * cancelling each hold item. (optional, but required if you
+     * implement cancelHolds). Not supported prior to VuFind 1.2
+     *
+     * @param array $checkOutDetails One of the individual item arrays returned by
+     *                               the getMyHolds method
+     *
+     * @return string  A string to use as the input form value for cancelling
+     *                 each hold item; you can pass any data that is needed
+     *                 by your ILS to identify the hold – the output of this
+     *                 method will be used as part of the input to the
+     *                 cancelHolds method.
+     */
+    public function getCancelHoldDetails($checkOutDetails)
+    {
+        return($checkOutDetails['cancel_details']);
+    }
+
+    /**
+     * Get Default Pick Up Location
+     *
+     * @param array $patron      Patron information returned by the patronLogin
+     * method.
+     * @param array $holdDetails Optional array, only passed in when getting a list
+     * in the context of placing a hold; contains most of the same values passed to
+     * placeHold, minus the patron data.  May be used to limit the pickup options
+     * or may be ignored.
+     *
+     * @return string       The default pickup location for the patron.
+     */
+    public function getDefaultPickUpLocation($patron = null, $holdDetails = null)
+    {
+        return false;
+    }
+
+    /**
+     * Get Funds
+     *
+     * Return a list of funds which may be used to limit the getNewItems list.
+     *
+     * @return array An associative array with key = fund ID, value = fund name.
+     */
+    public function getFunds()
+    {
+        // If you do not want or support such limits, just return an empty
+        // array here and the limit control on the new item search screen
+        // will disappear.
+        return [];
+    }
+
+    /**
+     * Cancel Storage Retrieval Request
+     *
+     * Attempts to Cancel a Storage Retrieval Request on a particular item. The
+     * data in $cancelDetails['details'] is determined by
+     * getCancelStorageRetrievalRequestDetails().
+     *
+     * @param array $cancelDetails An array of item and patron data
+     *
+     * @return array               An array of data on each request including
+     * whether or not it was successful and a system message (if available)
+     */
+    public function cancelStorageRetrievalRequests($cancelDetails)
+    {
+        // Not yet implemented
+        return [];
+    }
+
+    /**
+     * Get Cancel Storage Retrieval Request Details
+     *
+     * In order to cancel a hold, Voyager requires the patron details an item ID
+     * and a recall ID. This function returns the item id and recall id as a string
+     * separated by a pipe, which is then submitted as form data in Hold.php. This
+     * value is then extracted by the CancelHolds function.
+     *
+     * @param array $details An array of item data
+     *
+     * @return string Data for use in a form field
+     */
+    public function getCancelStorageRetrievalRequestDetails($details)
+    {
+        // Not yet implemented
+        return '';
+    }
+
+    /**
+     * Get Patron ILL Requests
+     *
+     * This is responsible for retrieving all ILL requests by a specific patron.
+     *
+     * @param array $patron The patron array from patronLogin
+     *
+     * @return mixed        Array of the patron's ILL requests
+     *
+     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+     */
+    public function getMyILLRequests($patron)
+    {
+        // Not yet implemented
+        return [];
+    }
+
+    /**
+     * Check if ILL request available
+     *
+     * This is responsible for determining if an item is requestable
+     *
+     * @param string $id     The Bib ID
+     * @param array  $data   An Array of item data
+     * @param patron $patron An array of patron data
+     *
+     * @return bool True if request is valid, false if not
+     *
+     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+     */
+    public function checkILLRequestIsValid($id, $data, $patron)
+    {
+        // Not yet implemented
+        return false;
+    }
+    /**
+     * Place ILL Request
+     *
+     * Attempts to place an ILL request on a particular item and returns
+     * an array with result details
+     *
+     * @param array $details An array of item and patron data
+     *
+     * @return mixed An array of data on the request including
+     * whether or not it was successful and a system message (if available)
+     */
+    public function placeILLRequest($details)
+    {
+        // Not yet implemented
+        return [];
+    }
+
+    /**
+     * Get ILL Pickup Libraries
+     *
+     * This is responsible for getting information on the possible pickup libraries
+     *
+     * @param string $id     Record ID
+     * @param array  $patron Patron
+     *
+     * @return bool|array False if request not allowed, or an array of associative
+     * arrays with libraries.
+     *
+     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+     */
+    public function getILLPickupLibraries($id, $patron)
+    {
+        // Not yet implemented
+        return false;
+    }
+
+    /**
+     * Get ILL Pickup Locations
+     *
+     * This is responsible for getting a list of possible pickup locations for a
+     * library
+     *
+     * @param string $id        Record ID
+     * @param string $pickupLib Pickup library ID
+     * @param array  $patron    Patron
+     *
+     * @return bool|array False if request not allowed, or an array of locations.
+     *
+     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+     */
+    public function getILLPickupLocations($id, $pickupLib, $patron)
+    {
+        // Not yet implemented
+        return false;
+    }
+
+    /**
+     * Cancel ILL Request
+     *
+     * Attempts to Cancel an ILL request on a particular item. The
+     * data in $cancelDetails['details'] is determined by
+     * getCancelILLRequestDetails().
+     *
+     * @param array $cancelDetails An array of item and patron data
+     *
+     * @return array               An array of data on each request including
+     * whether or not it was successful and a system message (if available)
+     */
+    public function cancelILLRequests($cancelDetails)
+    {
+        // Not yet implemented
+        return [];
+    }
+
+    /**
+     * Get Cancel ILL Request Details
+     *
+     * @param array $details An array of item data
+     *
+     * @return string Data for use in a form field
+     */
+    public function getCancelILLRequestDetails($details)
+    {
+        // Not yet implemented
+        return '';
+    }
+
+    /**
+     * Get Patron Fines
+     *
+     * This is responsible for retrieving all fines by a specific patron.
+     *
+     * @param array $patron The patron array from patronLogin
+     *
+     * @return mixed Array of the patron's fines on success
+     */
+    public function getMyFines($patron)
+    {
+        $fees = $this->paiaGetAsArray(
+            'core/' . $patron['cat_username'] . '/fees'
+        );
+
+        // PAIA simple data type money: a monetary value with currency (format
+        // [0-9]+\.[0-9][0-9] [A-Z][A-Z][A-Z]), for instance 0.80 USD.
+        $feeConverter = function ($fee) {
+            $paiaCurrencyPattern = "/^([0-9]+\.[0-9][0-9]) ([A-Z][A-Z][A-Z])$/";
+            if (preg_match($paiaCurrencyPattern, $fee, $feeMatches)) {
+                // VuFind expects fees in PENNIES
+                return ($feeMatches[1] * 100);
+            }
+            return $fee;
+        };
+
+        $results = [];
+        if (isset($fees['fee'])) {
+            foreach ($fees['fee'] as $fee) {
+                $result = [
+                    // fee.amount 	1..1 	money 	amount of a single fee
+                    'amount'      => $feeConverter($fee['amount']),
+                    'checkout'    => '',
+                    // fee.feetype 	0..1 	string 	textual description of the type
+                    // of service that caused the fee
+                    'fine'    => (isset($fee['feetype']) ? $fee['feetype'] : null),
+                    'balance' => $feeConverter($fee['amount']),
+                    // fee.date 	0..1 	date 	date when the fee was claimed
+                    'createdate'  => (isset($fee['date'])
+                        ? $this->convertDate($fee['date']) : null),
+                    'duedate' => '',
+                    // fee.edition 	0..1 	URI 	edition that caused the fee
+                    'id' => (isset($fee['edition'])
+                        ? $this->getAlternativeItemId($fee['edition']) : ''),
+                ];
+                // custom PAIA fields can get added in getAdditionalFeeData
+                $results[] = $result + $this->getAdditionalFeeData($fee, $patron);
+            }
+        }
+        return $results;
+    }
+
+    /**
+     * Gets additional array fields for the item.
+     * Override this method in your custom PAIA driver if necessary.
+     *
+     * @param array $fee    The fee array from PAIA
+     * @param array $patron The patron array from patronLogin
+     *
+     * @return array Additional fee data for the item
+     */
+    protected function getAdditionalFeeData($fee, $patron = null)
+    {
+        $additionalData = [];
+        // Add the item title using the about field,
+        // but only if this fee is caused by some item
+        if (isset($fee['item'])) {
+            $additionalData['title'] = $fee['about'];
+        }
+
+        // custom PAIA fields
+        // fee.about 	0..1 	string 	textual information about the fee
+        // fee.item 	0..1 	URI 	item that caused the fee
+        // fee.feeid 	0..1 	URI 	URI of the type of service that
+        // caused the fee
+        $additionalData['feeid']      = (isset($fee['feeid'])
+            ? $fee['feeid'] : null);
+        $additionalData['about']      = (isset($fee['about'])
+            ? $fee['about'] : null);
+        $additionalData['item']       = (isset($fee['item'])
+            ? $fee['item'] : null);
+        $additionalData['title']      = (isset($fee['title'])
+            ? $fee['title'] : null);
+
+        return $additionalData;
+    }
+
+    /**
+     * Get Patron Holds
+     *
+     * This is responsible for retrieving all holds by a specific patron.
+     *
+     * @param array $patron The patron array from patronLogin
+     *
+     * @return mixed Array of the patron's holds on success.
+     */
+    public function getMyHolds($patron)
+    {
+        // filters for getMyHolds are:
+        // status = 1 - reserved (the document is not accessible for the patron yet,
+        //              but it will be)
+        //          4 - provided (the document is ready to be used by the patron)
+        $filter = ['status' => [1, 4]];
+        // get items-docs for given filters
+        $items = $this->paiaGetItems($patron, $filter);
+
+        return $this->mapPaiaItems($items, 'myHoldsMapping');
+    }
+
+    /**
+     * Get Patron Profile
+     *
+     * This is responsible for retrieving the profile for a specific patron.
+     *
+     * @param array $patron The patron array
+     *
+     * @return array Array of the patron's profile data on success,
+     */
+    public function getMyProfile($patron)
+    {
+        //todo: read VCard if avaiable in patron info
+        //todo: make fields more configurable
+        if (is_array($patron)) {
+            return [
+                'firstname'  => $patron['firstname'],
+                'lastname'   => $patron['lastname'],
+                'address1'   => null,
+                'address2'   => null,
+                'city'       => null,
+                'country'    => null,
+                'zip'        => null,
+                'phone'      => null,
+                'group'      => null,
+                // PAIA specific custom values
+                'expires'    => isset($patron['expires'])
+                    ? $this->convertDate($patron['expires']) : null,
+                'statuscode' => isset($patron['status']) ? $patron['status'] : null,
+                'canWrite'   => in_array('write_items', $this->getScope()),
+            ];
+        }
+        return [];
+    }
+
+    /**
+     * Get Patron Transactions
+     *
+     * This is responsible for retrieving all transactions (i.e. checked out items)
+     * by a specific patron.
+     *
+     * @param array $patron The patron array from patronLogin
+     *
+     * @return array Array of the patron's transactions on success,
+     */
+    public function getMyTransactions($patron)
+    {
+        // filters for getMyTransactions are:
+        // status = 3 - held (the document is on loan by the patron)
+        $filter = ['status' => [3]];
+        // get items-docs for given filters
+        $items = $this->paiaGetItems($patron, $filter);
+
+        return $this->mapPaiaItems($items, 'myTransactionsMapping');
+    }
+
+    /**
+     * Get Patron StorageRetrievalRequests
+     *
+     * This is responsible for retrieving all storage retrieval requests
+     * by a specific patron.
+     *
+     * @param array $patron The patron array from patronLogin
+     *
+     * @return array Array of the patron's storage retrieval requests on success,
+     */
+    public function getMyStorageRetrievalRequests($patron)
+    {
+        // filters for getMyStorageRetrievalRequests are:
+        // status = 2 - ordered (the document is ordered by the patron)
+        $filter = ['status' => [2]];
+        // get items-docs for given filters
+        $items = $this->paiaGetItems($patron, $filter);
+
+        return $this->mapPaiaItems($items, 'myStorageRetrievalRequestsMapping');
+    }
+
+    /**
+     * This method queries the ILS for new items
+     *
+     * @param string $page    page number of results to retrieve (counting starts @1)
+     * @param string $limit   the size of each page of results to retrieve
+     * @param string $daysOld the maximum age of records to retrieve in days (max 30)
+     * @param string $fundID  optional fund ID to use for limiting results
+     *
+     * @return array An associative array with two keys: 'count' (the number of items
+     * in the 'results' array) and 'results' (an array of associative arrays, each
+     * with a single key: 'id', a record ID).
+     */
+    public function getNewItems($page, $limit, $daysOld, $fundID)
+    {
+        return [];
+    }
+
+    /**
+     * Get Pick Up Locations
+     *
+     * This is responsible for gettting a list of valid library locations for
+     * holds / recall retrieval
+     *
+     * @param array $patron      Patron information returned by the patronLogin
+     *                           method.
+     * @param array $holdDetails Optional array, only passed in when getting a list
+     * in the context of placing a hold; contains most of the same values passed to
+     * placeHold, minus the patron data.  May be used to limit the pickup options
+     * or may be ignored.  The driver must not add new options to the return array
+     * based on this data or other areas of VuFind may behave incorrectly.
+     *
+     * @return array        An array of associative arrays with locationID and
+     * locationDisplay keys
+     */
+    public function getPickUpLocations($patron = null, $holdDetails = null)
+    {
+        // How to get valid PickupLocations for a PICA LBS?
+        return [];
+    }
+
+    /**
+     * This method returns a string to use as the input form value for renewing
+     * each hold item. (optional, but required if you implement the
+     * renewMyItems method) Not supported prior to VuFind 1.2
+     *
+     * @param array $checkOutDetails One of the individual item arrays returned by
+     *                               the getMyTransactions method
+     *
+     * @return string A string to use as the input form value for renewing
+     *                each item; you can pass any data that is needed by your
+     *                ILS to identify the transaction to renew – the output
+     *                of this method will be used as part of the input to the
+     *                renewMyItems method.
+     */
+    public function getRenewDetails($checkOutDetails)
+    {
+        return($checkOutDetails['renew_details']);
+    }
+
+    /**
+     * Get the callnumber of this item
+     *
+     * @param array $doc Array of PAIA item.
+     *
+     * @return String
+     */
+    protected function getCallNumber($doc)
+    {
+        return isset($doc['label']) ? $doc['label'] : null;
+    }
+
+    /**
+     * Patron Login
+     *
+     * This is responsible for authenticating a patron against the catalog.
+     *
+     * @param string $username The patron's username
+     * @param string $password The patron's login password
+     *
+     * @return mixed          Associative array of patron info on successful login,
+     * null on unsuccessful login.
+     *
+     * @throws ILSException
+     */
+    public function patronLogin($username, $password)
+    {
+        if ($username == '' || $password == '') {
+            throw new ILSException('Invalid Login, Please try again.');
+        }
+
+        $session = $this->getSession();
+
+        // if we already have a session with access_token and patron id, try to get
+        // patron info with session data
+        if (isset($session->expires) && $session->expires > time()) {
+            try {
+                return $this->enrichUserDetails(
+                    $this->paiaGetUserDetails($session->patron),
+                    $password
+                );
+            } catch (ILSException $e) {
+                $this->debug('Session expired, login again', ['info' => 'info']);
+            }
+        }
+        try {
+            if ($this->paiaLogin($username, $password)) {
+                return $this->enrichUserDetails(
+                    $this->paiaGetUserDetails($session->patron),
+                    $password
+                );
+            }
+        } catch (ILSException $e) {
+            throw new ILSException($e->getMessage());
+        }
+    }
+
+    /**
+     * PAIA helper function to map session data to return value of patronLogin()
+     *
+     * @param array  $details  Patron details returned by patronLogin
+     * @param string $password Patron cataloge password
+     *
+     * @return mixed
+     */
+    protected function enrichUserDetails($details, $password)
+    {
+        $session = $this->getSession();
+
+        $details['cat_username'] = $session->patron;
+        $details['cat_password'] = $password;
+        return $details;
+    }
+
+    /**
+     * Returns an array with PAIA confirmations based on the given holdDetails which
+     * will be used for a request.
+     * Currently two condition types are supported:
+     *  - http://purl.org/ontology/paia#StorageCondition to select a document
+     *    location -- mapped to pickUpLocation
+     *  - http://purl.org/ontology/paia#FeeCondition to confirm or select a document
+     *    service causing a fee -- not mapped yet
+     *
+     * @param array $holdDetails An array of item and patron data
+     *
+     * @return array
+     */
+    protected function getConfirmations($holdDetails)
+    {
+        $confirmations = [];
+        if (isset($holdDetails['pickUpLocation'])) {
+            $confirmations['http://purl.org/ontology/paia#StorageCondition']
+                = [$holdDetails['pickUpLocation']];
+        }
+        return $confirmations;
+    }
+
+    /**
+     * Place Hold
+     *
+     * Attempts to place a hold or recall on a particular item and returns
+     * an array with result details
+     *
+     * Make a request on a specific record
+     *
+     * @param array $holdDetails An array of item and patron data
+     *
+     * @return mixed An array of data on the request including
+     * whether or not it was successful and a system message (if available)
+     */
+    public function placeHold($holdDetails)
+    {
+        $item = $holdDetails['item_id'];
+        $patron = $holdDetails['patron'];
+
+        $doc = [];
+        $doc['item'] = stripslashes($item);
+        if ($confirm = $this->getConfirmations($holdDetails)) {
+            $doc["confirm"] = $confirm;
+        }
+        $post_data['doc'][] = $doc;
+
+        try {
+            $array_response = $this->paiaPostAsArray(
+                'core/' . $patron['cat_username'] . '/request', $post_data
+            );
+        } catch (ILSException $e) {
+            $this->debug($e->getMessage());
+            return [
+                'success' => false,
+                'sysMessage' => $e->getMessage(),
+            ];
+        }
+
+        $details = [];
+        if (isset($array_response['error'])) {
+            $details = [
+                'success' => false,
+                'sysMessage' => $array_response['error_description']
+            ];
+        } else {
+            $elements = $array_response['doc'];
+            foreach ($elements as $element) {
+                if (isset($element['error'])) {
+                    $details = [
+                        'success' => false,
+                        'sysMessage' => $element['error']
+                    ];
+                } else {
+                    $details = [
+                        'success' => true,
+                        'sysMessage' => 'Successfully requested'
+                    ];
+                    // if caching is enabled for DAIA remove the cached data for the
+                    // current item otherwise the changed status will not be shown
+                    // before the cache expires
+                    if ($this->daiaCacheEnabled) {
+                        $this->removeCachedData($holdDetails['doc_id']);
+                    }
+                }
+            }
+        }
+        return $details;
+    }
+
+    /**
+     * Place a Storage Retrieval Request
+     *
+     * Attempts to place a request on a particular item and returns
+     * an array with result details.
+     *
+     * @param array $details An array of item and patron data
+     *
+     * @return mixed An array of data on the request including
+     * whether or not it was successful and a system message (if available)
+     */
+    public function placeStorageRetrievalRequest($details)
+    {
+        // Making a storage retrieval request is the same in PAIA as placing a Hold
+        return $this->placeHold($details);
+    }
+
+    /**
+     * This method renews a list of items for a specific patron.
+     *
+     * @param array $details - An associative array with two keys:
+     *      patron - array returned by patronLogin method
+     *      details - array of values returned by the getRenewDetails method
+     *                identifying which items to renew
+     *
+     * @return array - An associative array with two keys:
+     *     blocks - An array of strings specifying why a user is blocked from
+     *              renewing (false if no blocks)
+     *     details - Not set when blocks exist; otherwise, an array of
+     *               associative arrays (keyed by item ID) with each subarray
+     *               containing these keys:
+     *                  success – Boolean true or false
+     *                  new_date – string – A new due date
+     *                  new_time – string – A new due time
+     *                  item_id – The item id of the renewed item
+     *                  sysMessage – A system supplied renewal message (optional)
+     */
+    public function renewMyItems($details)
+    {
+        $it = $details['details'];
+        $items = [];
+        foreach ($it as $item) {
+            $items[] = ['item' => stripslashes($item)];
+        }
+        $patron = $details['patron'];
+        $post_data = ["doc" => $items];
+
+        try {
+            $array_response = $this->paiaPostAsArray(
+                'core/' . $patron['cat_username'] . '/renew', $post_data
+            );
+        } catch (ILSException $e) {
+            $this->debug($e->getMessage());
+            return [
+                'success' => false,
+                'sysMessage' => $e->getMessage(),
+            ];
+        }
+
+        $details = [];
+
+        if (isset($array_response['error'])) {
+            $details[] = [
+                'success' => false,
+                'sysMessage' => $array_response['error_description']
+            ];
+        } else {
+            $elements = $array_response['doc'];
+            foreach ($elements as $element) {
+                // VuFind can only assign the response to an id - if none is given
+                // (which is possible) simply skip this response element
+                if (isset($element['item'])) {
+                    if (isset($element['error'])) {
+                        $details[$element['item']] = [
+                            'success' => false,
+                            'sysMessage' => $element['error']
+                        ];
+                    } elseif ($element['status'] == '3') {
+                        $details[$element['item']] = [
+                            'success'  => true,
+                            'new_date' => isset($element['endtime'])
+                                ? $this->convertDatetime($element['endtime']) : '',
+                            'item_id'  => 0,
+                            'sysMessage' => 'Successfully renewed'
+                        ];
+                    } else {
+                        $details[$element['item']] = [
+                            'success'  => false,
+                            'item_id'  => 0,
+                            'new_date' => isset($element['endtime'])
+                                ? $this->convertDatetime($element['endtime']) : '',
+                            'sysMessage' => 'Request rejected'
+                        ];
+                    }
+                }
+
+                // DAIA cache cannot be cleared for particular item as PAIA only
+                // operates with specific item URIs and the DAIA cache is setup
+                // by doc URIs (containing items with URIs)
+            }
+
+            // If caching is enabled for PAIA clear the cache as at least for one
+            // item renew was successfull and therefore the status changed. Otherwise
+            // the changed status will not be shown before the cache expires.
+            if ($this->paiaCacheEnabled) {
+                $this->removeCachedData($patron['cat_username']);
+            }
+        }
+        $returnArray = ['blocks' => false, 'details' => $details];
+        return $returnArray;
+    }
+
+    /*
+     * PAIA functions
+     */
+
+    /**
+     * PAIA support method to return strings for PAIA service status values
+     *
+     * @param string $status PAIA service status
+     *
+     * @return string Describing PAIA service status
+     */
+    protected function paiaStatusString($status)
+    {
+        return isset(self::$statusStrings[$status])
+            ? self::$statusStrings[$status] : '';
+    }
+
+    /**
+     * PAIA support method for PAIA core method 'items' returning only those
+     * documents containing the given service status.
+     *
+     * @param array $patron Array with patron information
+     * @param array $filter Array of properties identifying the wanted items
+     *
+     * @return array|mixed Array of documents containing the given filter properties
+     */
+    protected function paiaGetItems($patron, $filter = [])
+    {
+        // check for existing data in cache
+        if ($this->paiaCacheEnabled) {
+            $itemsResponse = $this->getCachedData($patron['cat_username']);
+        }
+
+        if (!isset($itemsResponse) || $itemsResponse == null) {
+            $itemsResponse = $this->paiaGetAsArray(
+                'core/' . $patron['cat_username'] . '/items'
+            );
+            if ($this->paiaCacheEnabled) {
+                $this->putCachedData($patron['cat_username'], $itemsResponse);
+            }
+        }
+
+        if (isset($itemsResponse['doc'])) {
+            if (count($filter)) {
+                $filteredItems = [];
+                foreach ($itemsResponse['doc'] as $doc) {
+                    $filterCounter = 0;
+                    foreach ($filter as $filterKey => $filterValue) {
+                        if (isset($doc[$filterKey])
+                            && in_array($doc[$filterKey], (array)$filterValue)
+                        ) {
+                            $filterCounter++;
+                        }
+                    }
+                    if ($filterCounter == count($filter)) {
+                        $filteredItems[] = $doc;
+                    }
+                }
+                return $filteredItems;
+            } else {
+                return $itemsResponse;
+            }
+        } else {
+            $this->debug(
+                "No documents found in PAIA response. Returning empty array."
+            );
+        }
+        return [];
+    }
+
+    /**
+     * PAIA support method to retrieve needed ItemId in case PAIA-response does not
+     * contain it
+     *
+     * @param string $id itemId
+     *
+     * @return string $id
+     */
+    protected function getAlternativeItemId($id)
+    {
+        return $id;
+    }
+
+    /**
+     * PAIA support function to implement ILS specific parsing of user_details
+     *
+     * @param string $patron        User id
+     * @param array  $user_response Array with PAIA response data
+     *
+     * @return array
+     */
+    protected function paiaParseUserDetails($patron, $user_response)
+    {
+        $username = trim($user_response['name']);
+        if (count(explode(',', $username)) == 2) {
+            $nameArr = explode(',', $username);
+            $firstname = $nameArr[1];
+            $lastname = $nameArr[0];
+        } else {
+            $nameArr = explode(' ', $username);
+            $firstname = $nameArr[0];
+            $lastname = '';
+            array_shift($nameArr);
+            foreach ($nameArr as $value) {
+                $lastname .= ' ' . $value;
+            }
+            $lastname = trim($lastname);
+        }
+
+        // TODO: implement parsing of user details according to types set
+        // (cf. https://github.com/gbv/paia/issues/29)
+
+        $user = [];
+        $user['id']        = $patron;
+        $user['firstname'] = $firstname;
+        $user['lastname']  = $lastname;
+        $user['email']     = (isset($user_response['email'])
+            ? $user_response['email'] : '');
+        $user['major']     = null;
+        $user['college']   = null;
+        // add other information from PAIA - we don't want anything to get lost
+        // while parsing
+        foreach ($user_response as $key => $value) {
+            if (!isset($user[$key])) {
+                $user[$key] = $value;
+            }
+        }
+        return $user;
+    }
+
+    /**
+     * PAIA helper function to allow customization of mapping from PAIA response to
+     * VuFind ILS-method return values.
+     *
+     * @param array  $items   Array of PAIA items to be mapped
+     * @param string $mapping String identifying a custom mapping-method
+     *
+     * @return array
+     */
+    protected function mapPaiaItems($items, $mapping)
+    {
+        if (is_callable([$this, $mapping])) {
+            return $this->$mapping($items);
+        }
+
+        $this->debug('Could not call method: ' . $mapping . '() .');
+        return [];
+    }
+
+    /**
+     * This PAIA helper function allows custom overrides for mapping of PAIA response
+     * to getMyHolds data structure.
+     *
+     * @param array $items Array of PAIA items to be mapped.
+     *
+     * @return array
+     */
+    protected function myHoldsMapping($items)
+    {
+        $results = [];
+
+        foreach ($items as $doc) {
+            $result = [];
+
+            // item (0..1) URI of a particular copy
+            $result['item_id'] = (isset($doc['item']) ? $doc['item'] : '');
+
+            $result['cancel_details']
+                = (isset($doc['cancancel']) && $doc['cancancel'])
+                ? $result['item_id'] : '';
+
+            // edition (0..1) URI of a the document (no particular copy)
+            // hook for retrieving alternative ItemId in case PAIA does not
+            // the needed id
+            $result['id'] = (isset($doc['edition'])
+                ? $this->getAlternativeItemId($doc['edition']) : '');
+
+            $result['type'] = $this->paiaStatusString($doc['status']);
+
+            // storage (0..1) textual description of location of the document
+            $result['location'] = (isset($doc['storage']) ? $doc['storage'] : null);
+
+            // queue (0..1) number of waiting requests for the document or item
+            $result['position'] =  (isset($doc['queue']) ? $doc['queue'] : null);
+
+            // only true if status == 4
+            $result['available'] = false;
+
+            // about (0..1) textual description of the document
+            $result['title'] = (isset($doc['about']) ? $doc['about'] : null);
+
+            // PAIA custom field
+            // label (0..1) call number, shelf mark or similar item label
+            $result['callnumber'] = $this->getCallNumber($doc);
+
+            /*
+             * meaning of starttime and endtime depends on status:
+             *
+             * status | starttime
+             *        | endtime
+             * -------+--------------------------------
+             * 0      | -
+             *        | -
+             * 1      | when the document was reserved
+             *        | when the reserved document is expected to be available
+             * 2      | when the document was ordered
+             *        | when the ordered document is expected to be available
+             * 3      | when the document was lend
+             *        | when the loan period ends or ended (due)
+             * 4      | when the document is provided
+             *        | when the provision will expire
+             * 5      | when the request was rejected
+             *        | -
+             */
+
+            $result['create'] = (isset($doc['starttime'])
+                ? $this->convertDatetime($doc['starttime']) : '');
+
+            if ($doc['status'] == '4') {
+                $result['expire'] = (isset($doc['endtime'])
+                    ? $this->convertDatetime($doc['endtime']) : '');
+            } else {
+                $result['duedate'] = (isset($doc['endtime'])
+                    ? $this->convertDatetime($doc['endtime']) : '');
+            }
+
+            // status: provided (the document is ready to be used by the patron)
+            $result['available'] = $doc['status'] == 4 ? true : false;
+
+            // Optional VuFind fields
+            /*
+            $result['reqnum'] = null;
+            $result['volume'] =  null;
+            $result['publication_year'] = null;
+            $result['isbn'] = null;
+            $result['issn'] = null;
+            $result['oclc'] = null;
+            $result['upc'] = null;
+            */
+
+            $results[] = $result;
+
+        }
+        return $results;
+    }
+
+    /**
+     * This PAIA helper function allows custom overrides for mapping of PAIA response
+     * to getMyStorageRetrievalRequests data structure.
+     *
+     * @param array $items Array of PAIA items to be mapped.
+     *
+     * @return array
+     */
+    protected function myStorageRetrievalRequestsMapping($items)
+    {
+        $results = [];
+
+        foreach ($items as $doc) {
+            $result = [];
+
+            // item (0..1) URI of a particular copy
+            $result['item_id'] = (isset($doc['item']) ? $doc['item'] : '');
+
+            $result['cancel_details']
+                = (isset($doc['cancancel']) && $doc['cancancel'])
+                ? $result['item_id'] : '';
+
+            // edition (0..1) URI of a the document (no particular copy)
+            // hook for retrieving alternative ItemId in case PAIA does not
+            // the needed id
+            $result['id'] = (isset($doc['edition'])
+                ? $this->getAlternativeItemId($doc['edition']) : '');
+
+            $result['type'] = $this->paiaStatusString($doc['status']);
+
+            // storage (0..1) textual description of location of the document
+            $result['location'] = (isset($doc['storage']) ? $doc['storage'] : null);
+
+            // queue (0..1) number of waiting requests for the document or item
+            $result['position'] =  (isset($doc['queue']) ? $doc['queue'] : null);
+
+            // only true if status == 4
+            $result['available'] = false;
+
+            // about (0..1) textual description of the document
+            $result['title'] = (isset($doc['about']) ? $doc['about'] : null);
+
+            // PAIA custom field
+            // label (0..1) call number, shelf mark or similar item label
+            $result['callnumber'] = $this->getCallNumber($doc);
+
+            $result['create'] = (isset($doc['starttime'])
+                ? $this->convertDatetime($doc['starttime']) : '');
+
+            // Optional VuFind fields
+            /*
+            $result['reqnum'] = null;
+            $result['volume'] =  null;
+            $result['publication_year'] = null;
+            $result['isbn'] = null;
+            $result['issn'] = null;
+            $result['oclc'] = null;
+            $result['upc'] = null;
+            */
+
+            $results[] = $result;
+
+        }
+        return $results;
+    }
+
+    /**
+     * This PAIA helper function allows custom overrides for mapping of PAIA response
+     * to getMyTransactions data structure.
+     *
+     * @param array $items Array of PAIA items to be mapped.
+     *
+     * @return array
+     */
+    protected function myTransactionsMapping($items)
+    {
+        $results = [];
+
+        foreach ($items as $doc) {
+            $result = [];
+            // canrenew (0..1) whether a document can be renewed (bool)
+            $result['renewable'] = (isset($doc['canrenew'])
+                ? $doc['canrenew'] : false);
+
+            // item (0..1) URI of a particular copy
+            $result['item_id'] = (isset($doc['item']) ? $doc['item'] : '');
+
+            $result['renew_details']
+                = (isset($doc['canrenew']) && $doc['canrenew'])
+                ? $result['item_id'] : '';
+
+            // edition (0..1)  URI of a the document (no particular copy)
+            // hook for retrieving alternative ItemId in case PAIA does not
+            // the needed id
+            $result['id'] = (isset($doc['edition'])
+                ? $this->getAlternativeItemId($doc['edition']) : '');
+
+            // requested (0..1) URI that was originally requested
+
+            // about (0..1) textual description of the document
+            $result['title'] = (isset($doc['about']) ? $doc['about'] : null);
+
+            // queue (0..1) number of waiting requests for the document or item
+            $result['request'] = (isset($doc['queue']) ? $doc['queue'] : null);
+
+            // renewals (0..1) number of times the document has been renewed
+            $result['renew'] = (isset($doc['renewals']) ? $doc['renewals'] : null);
+
+            // reminder (0..1) number of times the patron has been reminded
+            $result['reminder'] = (
+                isset($doc['reminder']) ? $doc['reminder'] : null
+            );
+
+            // custom PAIA field
+            // starttime (0..1) date and time when the status began
+            $result['startTime'] = (isset($doc['starttime'])
+                ? $this->convertDatetime($doc['starttime']) : '');
+
+            // endtime (0..1) date and time when the status will expire
+            $result['dueTime'] = (isset($doc['endtime'])
+                ? $this->convertDatetime($doc['endtime']) : '');
+
+            // duedate (0..1) date when the current status will expire (deprecated)
+            $result['duedate'] = (isset($doc['duedate'])
+                ? $this->convertDate($doc['duedate']) : '');
+
+            // cancancel (0..1) whether an ordered or provided document can be
+            // canceled
+
+            // error (0..1) error message, for instance if a request was rejected
+            $result['message'] = (isset($doc['error']) ? $doc['error'] : '');
+
+            // storage (0..1) textual description of location of the document
+            $result['borrowingLocation'] = (isset($doc['storage'])
+                ? $doc['storage'] : '');
+
+            // storageid (0..1) location URI
+
+            // PAIA custom field
+            // label (0..1) call number, shelf mark or similar item label
+            $result['callnumber'] = $this->getCallNumber($doc);
+
+            // Optional VuFind fields
+            /*
+            $result['barcode'] = null;
+            $result['dueStatus'] = null;
+            $result['renewLimit'] = "1";
+            $result['volume'] = null;
+            $result['publication_year'] = null;
+            $result['isbn'] = null;
+            $result['issn'] = null;
+            $result['oclc'] = null;
+            $result['upc'] = null;
+            $result['institution_name'] = null;
+            */
+
+            $results[] = $result;
+        }
+
+        return $results;
+    }
+
+    /**
+     * Post something to a foreign host
+     *
+     * @param string $file         POST target URL
+     * @param string $data_to_send POST data
+     * @param string $access_token PAIA access token for current session
+     *
+     * @return string POST response
+     * @throws ILSException
+     */
+    protected function paiaPostRequest($file, $data_to_send, $access_token = null)
+    {
+        // json-encoding
+        $postData = json_encode($data_to_send);
+
+        $http_headers = [];
+        if (isset($access_token)) {
+            $http_headers['Authorization'] = 'Bearer ' . $access_token;
+        }
+
+        try {
+            $result = $this->httpService->post(
+                $this->paiaURL . $file,
+                $postData,
+                'application/json; charset=UTF-8',
+                $this->paiaTimeout,
+                $http_headers
+            );
+        } catch (\Exception $e) {
+            throw new ILSException($e->getMessage());
+        }
+
+        if (!$result->isSuccess()) {
+            // log error for debugging
+            $this->debug(
+                'HTTP status ' . $result->getStatusCode() .
+                ' received'
+            );
+        }
+        // return any result as error-handling is done elsewhere
+        return ($result->getBody());
+    }
+
+    /**
+     * GET data from foreign host
+     *
+     * @param string $file         GET target URL
+     * @param string $access_token PAIA access token for current session
+     *
+     * @return bool|string
+     * @throws ILSException
+     */
+    protected function paiaGetRequest($file, $access_token)
+    {
+        $http_headers = [
+            'Authorization' => 'Bearer ' . $access_token,
+            'Content-type' => 'application/json; charset=UTF-8',
+        ];
+
+        try {
+            $result = $this->httpService->get(
+                $this->paiaURL . $file,
+                [], $this->paiaTimeout, $http_headers
+            );
+        } catch (\Exception $e) {
+            throw new ILSException($e->getMessage());
+        }
+
+        if (!$result->isSuccess()) {
+            // log error for debugging
+            $this->debug(
+                'HTTP status ' . $result->getStatusCode() .
+                ' received'
+            );
+        }
+        // return any result as error-handling is done elsewhere
+        return ($result->getBody());
+    }
+
+    /**
+     * Helper function for PAIA to uniformely parse JSON
+     *
+     * @param string $file JSON data
+     *
+     * @return mixed
+     * @throws ILSException
+     */
+    protected function paiaParseJsonAsArray($file)
+    {
+        $responseArray = json_decode($file, true);
+
+        if (isset($responseArray['error'])) {
+            throw new ILSException(
+                $responseArray['error'],
+                $responseArray['code']
+            );
+        }
+
+        return $responseArray;
+    }
+
+    /**
+     * Retrieve file at given URL and return it as json_decoded array
+     *
+     * @param string $file GET target URL
+     *
+     * @return array|mixed
+     * @throws ILSException
+     */
+    protected function paiaGetAsArray($file)
+    {
+        $responseJson = $this->paiaGetRequest(
+            $file,
+            $this->getSession()->access_token
+        );
+
+        try {
+            $responseArray = $this->paiaParseJsonAsArray($responseJson);
+        } catch (ILSException $e) {
+            $this->debug($e->getCode() . ':' . $e->getMessage());
+            return [];
+        }
+
+        return $responseArray;
+    }
+
+    /**
+     * Post something at given URL and return it as json_decoded array
+     *
+     * @param string $file POST target URL
+     * @param array  $data POST data
+     *
+     * @return array|mixed
+     * @throws ILSException
+     */
+    protected function paiaPostAsArray($file, $data)
+    {
+        $responseJson = $this->paiaPostRequest(
+            $file,
+            $data,
+            $this->getSession()->access_token
+        );
+
+        try {
+            $responseArray = $this->paiaParseJsonAsArray($responseJson);
+        } catch (ILSException $e) {
+            $this->debug($e->getCode() . ':' . $e->getMessage());
+            /* TODO: do not return empty array, this causes eventually confusion */
+            return [];
+        }
+
+        return $responseArray;
+    }
+
+    /**
+     * PAIA authentication function
+     *
+     * @param string $username Username
+     * @param string $password Password
+     *
+     * @return mixed Associative array of patron info on successful login,
+     * null on unsuccessful login, PEAR_Error on error.
+     * @throws ILSException
+     */
+    protected function paiaLogin($username, $password)
+    {
+        // perform full PAIA auth and get patron info
+        $post_data = [
+            "username"   => $username,
+            "password"   => $password,
+            "grant_type" => "password",
+            "scope"      => "read_patron read_fees read_items write_items " .
+                "change_password"
+        ];
+        $responseJson = $this->paiaPostRequest('auth/login', $post_data);
+
+        try {
+            $responseArray = $this->paiaParseJsonAsArray($responseJson);
+        } catch (ILSException $e) {
+            if ($e->getMessage() === 'access_denied') {
+                return false;
+            }
+            throw new ILSException(
+                $e->getCode() . ':' . $e->getMessage()
+            );
+        }
+
+        if (!isset($responseArray['access_token'])) {
+            throw new ILSException(
+                'Unknown error! Access denied.'
+            );
+        } elseif (!isset($responseArray['patron'])) {
+            throw new ILSException(
+                'Login credentials accepted, but got no patron ID?!?'
+            );
+        } else {
+            // at least access_token and patron got returned which is sufficient for
+            // us, now save all to session
+            $session = $this->getSession();
+
+            $session->patron
+                = isset($responseArray['patron'])
+                ? $responseArray['patron'] : null;
+            $session->access_token
+                = isset($responseArray['access_token'])
+                ? $responseArray['access_token'] : null;
+            $session->scope
+                = isset($responseArray['scope'])
+                ? explode(' ', $responseArray['scope']) : null;
+            $session->expires
+                = isset($responseArray['expires_in'])
+                ? (time() + ($responseArray['expires_in'])) : null;
+
+            return true;
+        }
+    }
+
+    /**
+     * Support method for paiaLogin() -- load user details into session and return
+     * array of basic user data.
+     *
+     * @param array $patron patron ID
+     *
+     * @return array
+     * @throws ILSException
+     */
+    protected function paiaGetUserDetails($patron)
+    {
+        $responseJson = $this->paiaGetRequest(
+            'core/' . $patron, $this->getSession()->access_token
+        );
+
+        try {
+            $responseArray = $this->paiaParseJsonAsArray($responseJson);
+        } catch (ILSException $e) {
+            throw new ILSException(
+                $e->getMessage(), $e->getCode()
+            );
+        }
+        return $this->paiaParseUserDetails($patron, $responseArray);
+    }
+
+    /**
+     * Check if storage retrieval request available
+     *
+     * This is responsible for determining if an item is requestable
+     *
+     * @param string $id     The Bib ID
+     * @param array  $data   An Array of item data
+     * @param patron $patron An array of patron data
+     *
+     * @return bool True if request is valid, false if not
+     *
+     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+     */
+    public function checkStorageRetrievalRequestIsValid($id, $data, $patron)
+    {
+        return $this->checkRequestIsValid($id, $data, $patron);
+    }
+
+    /**
+     * Check if hold or recall available
+     *
+     * This is responsible for determining if an item is requestable
+     *
+     * @param string $id     The Bib ID
+     * @param array  $data   An Array of item data
+     * @param patron $patron An array of patron data
+     *
+     * @return bool True if request is valid, false if not
+     *
+     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+     */
+    public function checkRequestIsValid($id, $data, $patron)
+    {
+        // TODO: make this more configurable
+        if (isset($patron['status']) && $patron['status']  == 0
+            && isset($patron['expires']) && $patron['expires'] > date('Y-m-d')
+            && in_array('write_items', $this->getScope())
+        ) {
+            return true;
+        }
+        return false;
+    }
+}
diff --git a/module/VuFind/src/VuFind/ILS/Driver/PluginFactory.php b/module/VuFind/src/VuFind/ILS/Driver/PluginFactory.php
index 2be238eebacf2f56b2260082d15c422d238f3540..9b15b1d223b46a29dc670061c7d6c979ad318cdf 100644
--- a/module/VuFind/src/VuFind/ILS/Driver/PluginFactory.php
+++ b/module/VuFind/src/VuFind/ILS/Driver/PluginFactory.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  ILS_Drivers
diff --git a/module/VuFind/src/VuFind/ILS/Driver/PluginManager.php b/module/VuFind/src/VuFind/ILS/Driver/PluginManager.php
index 6e466c094ce1016ee9cdb6cf23d123398be3c391..60f16b4b9b97f77761cbaf46e6b682630c7b347a 100644
--- a/module/VuFind/src/VuFind/ILS/Driver/PluginManager.php
+++ b/module/VuFind/src/VuFind/ILS/Driver/PluginManager.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  ILS_Drivers
diff --git a/module/VuFind/src/VuFind/ILS/Driver/Polaris.php b/module/VuFind/src/VuFind/ILS/Driver/Polaris.php
index c2232399760c2ae9710f4ff4072fd880708bb661..835d9edf704122c576e22b7e30045cfb60286b0c 100644
--- a/module/VuFind/src/VuFind/ILS/Driver/Polaris.php
+++ b/module/VuFind/src/VuFind/ILS/Driver/Polaris.php
@@ -16,7 +16,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  ILS_Drivers
diff --git a/module/VuFind/src/VuFind/ILS/Driver/Sample.php b/module/VuFind/src/VuFind/ILS/Driver/Sample.php
index 1d9e523ed92f1eccfb7c633e9fc1f770c4c6c85b..e472b7c0a3ddde5fd4621aef1bcdc12853930c92 100644
--- a/module/VuFind/src/VuFind/ILS/Driver/Sample.php
+++ b/module/VuFind/src/VuFind/ILS/Driver/Sample.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  ILS_Drivers
diff --git a/module/VuFind/src/VuFind/ILS/Driver/Sierra.php b/module/VuFind/src/VuFind/ILS/Driver/Sierra.php
index 521055ca4af3939f5d5e30e81e7822eb59d9088e..c14a819ff18e1036de28604a52a0a1b2a3d51d7b 100644
--- a/module/VuFind/src/VuFind/ILS/Driver/Sierra.php
+++ b/module/VuFind/src/VuFind/ILS/Driver/Sierra.php
@@ -107,7 +107,10 @@ class Sierra extends AbstractBase implements TranslatorAwareInterface
             . "FROM sierra_view.bib_view "
             . "LEFT JOIN sierra_view.bib_record_item_record_link ON "
             . "(bib_view.id = bib_record_item_record_link.bib_record_id) "
-            . "WHERE bib_view.record_num = $1;";
+            . "INNER JOIN sierra_view.item_view ON "
+            . "(bib_record_item_record_link.item_record_id = item_view.id) "
+            . "WHERE bib_view.record_num = $1 "
+            . "AND item_view.is_suppressed = false;";
         $record_ids = pg_query_params(
             $this->db, $get_record_ids_query, [$this->idStrip($id)]
         );
diff --git a/module/VuFind/src/VuFind/ILS/Driver/Symphony.php b/module/VuFind/src/VuFind/ILS/Driver/Symphony.php
index 64d9c6fabde752df13d40338cd3c109fa0881a53..e34f7f06b7de727c846d89d2e8b98ba0575a7fb8 100644
--- a/module/VuFind/src/VuFind/ILS/Driver/Symphony.php
+++ b/module/VuFind/src/VuFind/ILS/Driver/Symphony.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  ILS_Drivers
diff --git a/module/VuFind/src/VuFind/ILS/Driver/Unicorn.php b/module/VuFind/src/VuFind/ILS/Driver/Unicorn.php
index 429f40e4f35d1447bff9055adb50f913d58b4dc8..9a4a969cec8eb3a58b0cd5519bf23e69cff6ea2f 100644
--- a/module/VuFind/src/VuFind/ILS/Driver/Unicorn.php
+++ b/module/VuFind/src/VuFind/ILS/Driver/Unicorn.php
@@ -15,7 +15,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  ILS_Drivers
diff --git a/module/VuFind/src/VuFind/ILS/Driver/Virtua.php b/module/VuFind/src/VuFind/ILS/Driver/Virtua.php
index ec816b65964f521e63de5afa8b003a60e6c18121..2f56a89a8e20dda275f7fd397111c7f359fa40c6 100644
--- a/module/VuFind/src/VuFind/ILS/Driver/Virtua.php
+++ b/module/VuFind/src/VuFind/ILS/Driver/Virtua.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  ILS_Drivers
diff --git a/module/VuFind/src/VuFind/ILS/Driver/Voyager.php b/module/VuFind/src/VuFind/ILS/Driver/Voyager.php
index 8caeea5b92f74b45e738b58b7d64f1bd76c27699..4d786a048df21739590b8eeb9aaa3723757d04b8 100644
--- a/module/VuFind/src/VuFind/ILS/Driver/Voyager.php
+++ b/module/VuFind/src/VuFind/ILS/Driver/Voyager.php
@@ -5,7 +5,7 @@
  * PHP version 5
  *
  * Copyright (C) Villanova University 2007.
- * Copyright (C) The National Library of Finland 2014.
+ * Copyright (C) The National Library of Finland 2014-2016.
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License version 2,
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  ILS_Drivers
@@ -29,7 +29,7 @@
  * @link     https://vufind.org/wiki/development:plugins:ils_drivers Wiki
  */
 namespace VuFind\ILS\Driver;
-use File_MARC, PDO, PDOException,
+use File_MARC, Yajra\Pdo\Oci8, PDO, PDOException,
     VuFind\Exception\Date as DateException,
     VuFind\Exception\ILS as ILSException,
     VuFind\I18n\Translator\TranslatorAwareInterface,
@@ -57,7 +57,7 @@ class Voyager extends AbstractBase
     /**
      * Database connection
      *
-     * @var PDO
+     * @var Oci8
      */
     protected $db;
 
@@ -157,8 +157,8 @@ class Voyager extends AbstractBase
                  ')' .
                ')';
         try {
-            $this->db = new PDO(
-                "oci:dbname=$tns",
+            $this->db = new Oci8(
+                "oci:dbname=$tns;charset=US_ASCII",
                 $this->config['Catalog']['user'],
                 $this->config['Catalog']['password']
             );
@@ -1218,10 +1218,7 @@ class Voyager extends AbstractBase
             $bindBarcode = strtolower(utf8_decode($barcode));
             $compareLogin = mb_strtolower($login, 'UTF-8');
 
-            $this->debugSQL(__FUNCTION__, $sql, [':barcode' => $bindBarcode]);
-            $sqlStmt = $this->db->prepare($sql);
-            $sqlStmt->bindParam(':barcode', $bindBarcode, PDO::PARAM_STR);
-            $sqlStmt->execute();
+            $sqlStmt = $this->executeSQL($sql, [':barcode' => $bindBarcode]);
             // For some reason barcode is not unique, so evaluate all resulting
             // rows just to be safe
             while ($row = $sqlStmt->fetch(PDO::FETCH_ASSOC)) {
@@ -1794,6 +1791,18 @@ class Voyager extends AbstractBase
             'BIB_TEXT.BIB_ID = CALL_SLIP.BIB_ID'
         ];
 
+        if (!empty($this->config['StorageRetrievalRequests']['display_statuses'])) {
+            $statuses = preg_replace(
+                '/[^:\d]*/',
+                '',
+                $this->config['StorageRetrievalRequests']['display_statuses']
+            );
+            if ($statuses) {
+                $sqlWhere[] = 'CALL_SLIP.STATUS IN ('
+                    . str_replace(':', ',', $statuses) . ')';
+            }
+        }
+
         // Order by
         $sqlOrderBy = [
             "to_char(CALL_SLIP.DATE_REQUESTED, 'YYYY-MM-DD HH24:MI:SS')"
@@ -1887,9 +1896,7 @@ class Voyager extends AbstractBase
 
         $sql = $this->buildSqlFromArray($sqlArray);
         try {
-            $sqlStmt = $this->db->prepare($sql['string']);
-            $this->debugSQL(__FUNCTION__, $sql['string'], $sql['bind']);
-            $sqlStmt->execute($sql['bind']);
+            $sqlStmt = $this->executeSQL($sql);
             while ($sqlRow = $sqlStmt->fetch(PDO::FETCH_ASSOC)) {
                 $list[] = $this->processMyStorageRetrievalRequestsData($sqlRow);
             }
diff --git a/module/VuFind/src/VuFind/ILS/Driver/VoyagerRestful.php b/module/VuFind/src/VuFind/ILS/Driver/VoyagerRestful.php
index 1a89aab9a4154432e32767edddcda6d63d3d91fc..293258fe4b6ed37df6cf670dc350650ee3e02c09 100644
--- a/module/VuFind/src/VuFind/ILS/Driver/VoyagerRestful.php
+++ b/module/VuFind/src/VuFind/ILS/Driver/VoyagerRestful.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  ILS_Drivers
@@ -330,7 +330,7 @@ class VoyagerRestful extends Voyager implements \VuFindHttp\HttpServiceAwareInte
      *
      * @return string
      */
-    protected function formatCacheKey($key)
+    protected function getCacheKey($key = null)
     {
         // Override the base class formatting with Voyager-specific details
         // to ensure proper caching in a MultiBackend environment.
@@ -725,8 +725,7 @@ class VoyagerRestful extends Voyager implements \VuFindHttp\HttpServiceAwareInte
             }
 
             try {
-                $sqlStmt = $this->db->prepare($sql);
-                $sqlStmt->execute($params);
+                $sqlStmt = $this->executeSQL($sql, $params);
             } catch (PDOException $e) {
                 throw new ILSException($e->getMessage());
             }
@@ -841,11 +840,11 @@ class VoyagerRestful extends Voyager implements \VuFindHttp\HttpServiceAwareInte
     /**
      * Get request groups
      *
-     * @param integer $bibId  BIB ID
-     * @param array   $patron Patron information returned by the patronLogin
+     * @param int   $bibId  BIB ID
+     * @param array $patron Patron information returned by the patronLogin
      * method.
      *
-     * @return array  False if request groups not in use or an array of
+     * @return array False if request groups not in use or an array of
      * associative arrays with id and name keys
      */
     public function getRequestGroups($bibId, $patron)
@@ -882,7 +881,7 @@ class VoyagerRestful extends Voyager implements \VuFindHttp\HttpServiceAwareInte
             }
 
             $items = [];
-            foreach ($results->hold as $hold) {
+            foreach ($results->$request as $hold) {
                 foreach ($hold->items->item as $item) {
                     $items[(string)$item->item_id] = 1;
                 }
@@ -1004,9 +1003,7 @@ class VoyagerRestful extends Voyager implements \VuFindHttp\HttpServiceAwareInte
         $sql = $this->buildSqlFromArray($sqlArray);
 
         try {
-            $this->debugSQL(__FUNCTION__, $sql['string'], $sql['bind']);
-            $sqlStmt = $this->db->prepare($sql['string']);
-            $sqlStmt->execute($sql['bind']);
+            $sqlStmt = $this->executeSQL($sql);
         } catch (PDOException $e) {
             throw new ILSException($e->getMessage());
         }
@@ -1617,8 +1614,8 @@ EOT;
     /**
      * Check whether the given patron has the given bib record on loan.
      *
-     * @param integer $patronId Patron ID
-     * @param integer $bibId    BIB ID
+     * @param int $patronId Patron ID
+     * @param int $bibId    BIB ID
      *
      * @return bool
      */
@@ -1661,9 +1658,7 @@ EOT;
         $sql = $this->buildSqlFromArray($sqlArray);
 
         try {
-            $sqlStmt = $this->db->prepare($sql['string']);
-            $this->debugSQL(__FUNCTION__, $sql['string'], $sql['bind']);
-            $sqlStmt->execute($sql['bind']);
+            $sqlStmt = $this->executeSQL($sql);
             $sqlRow = $sqlStmt->fetch(PDO::FETCH_ASSOC);
             return $sqlRow['CNT'] > 0;
         } catch (PDOException $e) {
@@ -1674,8 +1669,8 @@ EOT;
     /**
      * Check whether items exist for the given BIB ID
      *
-     * @param integer $bibId          BIB ID
-     * @param integer $requestGroupId Request group ID or null
+     * @param int $bibId          BIB ID
+     * @param int $requestGroupId Request group ID or null
      *
      * @return bool
      */
@@ -1724,9 +1719,7 @@ EOT;
 
         $sql = $this->buildSqlFromArray($sqlArray);
         try {
-            $sqlStmt = $this->db->prepare($sql['string']);
-            $this->debugSQL(__FUNCTION__, $sql['string'], $sql['bind']);
-            $sqlStmt->execute($sql['bind']);
+            $sqlStmt = $this->executeSQL($sql);
             $sqlRow = $sqlStmt->fetch(PDO::FETCH_ASSOC);
             return $sqlRow['CNT'] > 0;
         } catch (PDOException $e) {
@@ -1737,8 +1730,8 @@ EOT;
     /**
      * Check whether there are items available for loan for the given BIB ID
      *
-     * @param integer $bibId          BIB ID
-     * @param integer $requestGroupId Request group ID or null
+     * @param int $bibId          BIB ID
+     * @param int $requestGroupId Request group ID or null
      *
      * @return bool
      */
@@ -1799,9 +1792,7 @@ EOT;
             ' where avail.STATUS=1'; // 1 = not charged
 
         try {
-            $sqlStmt = $this->db->prepare($outersql);
-            $this->debugSQL(__FUNCTION__, $outersql, $sql['bind']);
-            $sqlStmt->execute($sql['bind']);
+            $sqlStmt = $this->executeSQL($outersql, $sql['bind']);
             $sqlRow = $sqlStmt->fetch(PDO::FETCH_ASSOC);
             return $sqlRow['CNT'] > 0;
         } catch (PDOException $e) {
@@ -3271,9 +3262,23 @@ EOT;
                 $this->sanitizePIN($details['oldPassword']), ENT_COMPAT, 'UTF-8'
             )
         );
+
         if ($oldPIN === '') {
             // Voyager requires the PIN code to be set even if it was empty
             $oldPIN = '     ';
+
+            // In this case we have to check that the user didn't previously have a
+            // PIN code since Voyager doesn't validate the 'empty' old PIN
+            $sql = "SELECT PATRON_PIN FROM {$this->dbName}.PATRON WHERE"
+                . ' PATRON_ID=:id';
+            $sqlStmt = $this->executeSQL($sql, ['id' => $patron['id']]);
+            if (!($row = $sqlStmt->fetch(PDO::FETCH_ASSOC))
+                || null !== $row['PATRON_PIN']
+            ) {
+                return [
+                    'success' => false, 'status' => 'authentication_error_invalid'
+                ];
+            }
         }
         $newPIN = trim(
             htmlspecialchars(
diff --git a/module/VuFind/src/VuFind/ILS/Driver/XCNCIP2.php b/module/VuFind/src/VuFind/ILS/Driver/XCNCIP2.php
index 787e4382b31fcf1829148bc2fea01272bf2455ae..2b9624664f81b20c691af9920fea42af5894027a 100644
--- a/module/VuFind/src/VuFind/ILS/Driver/XCNCIP2.php
+++ b/module/VuFind/src/VuFind/ILS/Driver/XCNCIP2.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  ILS_Drivers
@@ -45,7 +45,7 @@ class XCNCIP2 extends AbstractBase implements \VuFindHttp\HttpServiceAwareInterf
     /**
      * Is this a consortium? Default: false
      *
-     * @var boolean
+     * @var bool
      */
     protected $consortium = false;
 
@@ -1757,6 +1757,11 @@ class XCNCIP2 extends AbstractBase implements \VuFindHttp\HttpServiceAwareInterf
         if (!is_null($patron_agency_id)) {
             $ret .=
                    '<ns1:InitiationHeader>' .
+                        '<ns1:FromAgencyId>' .
+                            '<ns1:AgencyId>' .
+                                htmlspecialchars($patron_agency_id) .
+                            '</ns1:AgencyId>' .
+                        '</ns1:FromAgencyId>' .
                         '<ns1:ToAgencyId>' .
                             '<ns1:AgencyId>' .
                                 htmlspecialchars($patron_agency_id) .
diff --git a/module/VuFind/src/VuFind/ILS/HoldSettings.php b/module/VuFind/src/VuFind/ILS/HoldSettings.php
index a67c0f00bb9cc7a7e0a4bdc9e4272aaf022fa609..7051528179931f558eb992e01c175fb0675f0e2b 100644
--- a/module/VuFind/src/VuFind/ILS/HoldSettings.php
+++ b/module/VuFind/src/VuFind/ILS/HoldSettings.php
@@ -20,7 +20,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  ILS_Drivers
diff --git a/module/VuFind/src/VuFind/ILS/Logic/Holds.php b/module/VuFind/src/VuFind/ILS/Logic/Holds.php
index ff4d71ea4a987a5d57e54ffe8a360886c16b759c..07f65072b3ecc3b3de4ba0d4bde45b230be868f3 100644
--- a/module/VuFind/src/VuFind/ILS/Logic/Holds.php
+++ b/module/VuFind/src/VuFind/ILS/Logic/Holds.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  ILS_Logic
diff --git a/module/VuFind/src/VuFind/ILS/Logic/TitleHolds.php b/module/VuFind/src/VuFind/ILS/Logic/TitleHolds.php
index 75a0e74be36d98c235450982790d823ef93d7e5e..191baaed745d9b54eb5385d1555dccb887fbdfe5 100644
--- a/module/VuFind/src/VuFind/ILS/Logic/TitleHolds.php
+++ b/module/VuFind/src/VuFind/ILS/Logic/TitleHolds.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  ILS_Logic
diff --git a/module/VuFind/src/VuFind/ImageLoader.php b/module/VuFind/src/VuFind/ImageLoader.php
index 8aec50f026ed62d4cbd1c813483af3ee5059c24e..1c5ddb3fae20a9715ea318b773e1f3b37dfe80b3 100644
--- a/module/VuFind/src/VuFind/ImageLoader.php
+++ b/module/VuFind/src/VuFind/ImageLoader.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Cover_Generator
diff --git a/module/VuFind/src/VuFind/Log/Logger.php b/module/VuFind/src/VuFind/Log/Logger.php
index 297099ac31b7644b7dab6ea67ce007f0e764e47b..9b97e58e1f4b76d43a7ad4c378477f5055cb8615 100644
--- a/module/VuFind/src/VuFind/Log/Logger.php
+++ b/module/VuFind/src/VuFind/Log/Logger.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Error_Logging
@@ -119,6 +119,36 @@ class Logger extends BaseLogger implements ServiceLocatorAwareInterface
             $this->addWriters($writer, $filters);
         }
 
+        // Activate slack logging, if applicable:
+        if (isset($config->Logging->slack) && isset($config->Logging->slackurl)) {
+            $options = [];
+            // Get config
+            list($channel, $error_types) = explode(':', $config->Logging->slack);
+            if ($error_types == null) {
+                $error_types = $channel;
+                $channel = null;
+            }
+            if ($channel) {
+                $options['channel'] = $channel;
+            }
+            if (isset($config->Logging->slackname)) {
+                $options['name'] = $config->Logging->slackname;
+            }
+            $filters = explode(',', $error_types);
+            // Make Writers
+            $writer = new Writer\Slack(
+                $config->Logging->slackurl,
+                $this->getServiceLocator()->get('VuFind\Http')->createClient(),
+                $options
+            );
+            $writer->setContentType('application/json');
+            $formatter = new \Zend\Log\Formatter\Simple(
+                "*%priorityName%*: %message%"
+            );
+            $writer->setFormatter($formatter);
+            $this->addWriters($writer, $filters);
+        }
+
         // Null (no-op) writer to avoid errors
         if (count($this->writers) == 0) {
             $nullWriter = 'Zend\Log\Writer\Noop';
@@ -269,6 +299,24 @@ class Logger extends BaseLogger implements ServiceLocatorAwareInterface
         return parent::log($priority, $message, $extra);
     }
 
+    /**
+     * Given an exception, return a severity level for logging purposes.
+     *
+     * @param \Exception $error Exception to analyze
+     *
+     * @return int
+     */
+    protected function getSeverityFromException($error)
+    {
+        // Treat unexpected or 5xx errors as more severe than 4xx errors.
+        if ($error instanceof \VuFind\Exception\HttpStatusInterface
+            && in_array($error->getHttpStatus(), [403, 404])
+        ) {
+            return BaseLogger::WARN;
+        }
+        return BaseLogger::CRIT;
+    }
+
     /**
      * Log an exception triggered by ZF2 for administrative purposes.
      *
@@ -281,7 +329,12 @@ class Logger extends BaseLogger implements ServiceLocatorAwareInterface
     {
         // We need to build a variety of pieces so we can supply
         // information at five different verbosity levels:
-        $baseError = $error->getMessage();
+        $baseError = get_class($error) . ' : ' . $error->getMessage();
+        $prev = $error->getPrevious();
+        while ($prev) {
+            $baseError .= ' ; ' . get_class($prev) . ' : ' . $prev->getMessage();
+            $prev = $prev->getPrevious();
+        }
         $referer = $server->get('HTTP_REFERER', 'none');
         $basicServer
             = '(Server: IP = ' . $server->get('REMOTE_ADDR') . ', '
@@ -329,7 +382,7 @@ class Logger extends BaseLogger implements ServiceLocatorAwareInterface
             5 => $baseError . $detailedServer . $detailedBacktrace
         ];
 
-        $this->log(BaseLogger::CRIT, $errorDetails);
+        $this->log($this->getSeverityFromException($error), $errorDetails);
     }
 
     /**
diff --git a/module/VuFind/src/VuFind/Log/LoggerAwareTrait.php b/module/VuFind/src/VuFind/Log/LoggerAwareTrait.php
index a65ca6e5c1963a45b0e4f1b534493397a45441c8..7bb4ed59bcefba8fff11f7a1c9924d5fcf4155c2 100644
--- a/module/VuFind/src/VuFind/Log/LoggerAwareTrait.php
+++ b/module/VuFind/src/VuFind/Log/LoggerAwareTrait.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Error_Logging
diff --git a/module/VuFind/src/VuFind/Log/Writer/Db.php b/module/VuFind/src/VuFind/Log/Writer/Db.php
index d76e1b35441a248fd2c86fd26629a3e6cea1d7f2..c467468935fcd5dfb0bb70754c691767e5079a52 100644
--- a/module/VuFind/src/VuFind/Log/Writer/Db.php
+++ b/module/VuFind/src/VuFind/Log/Writer/Db.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Error_Logging
@@ -38,24 +38,7 @@ namespace VuFind\Log\Writer;
  */
 class Db extends \Zend\Log\Writer\Db
 {
-    /**
-     * Holds the verbosity level
-     *
-     * @var int
-     */
-    protected $verbosity = 1;
-
-    /**
-     * Set verbosity
-     *
-     * @param integer $verb verbosity setting
-     *
-     * @return void
-     */
-    public function setVerbosity($verb)
-    {
-        $this->verbosity = $verb;
-    }
+    use VerbosityTrait;
 
     /**
      * Write a message to the log.
@@ -67,12 +50,7 @@ class Db extends \Zend\Log\Writer\Db
      */
     protected function doWrite(array $event)
     {
-        // Apply verbosity filter:
-        if (is_array($event['message'])) {
-            $event['message'] = $event['message'][$this->verbosity];
-        }
-
-        // Call parent method:
-        return parent::doWrite($event);
+        // Apply verbosity, Call parent method:
+        return parent::doWrite($this->applyVerbosity($event));
     }
 }
diff --git a/module/VuFind/src/VuFind/Log/Writer/Mail.php b/module/VuFind/src/VuFind/Log/Writer/Mail.php
index f9fa68817193888bbde5453a06cc7c99e5445aee..853fbc03ae607357f6dd5075ce775af5535c0629 100644
--- a/module/VuFind/src/VuFind/Log/Writer/Mail.php
+++ b/module/VuFind/src/VuFind/Log/Writer/Mail.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Error_Logging
@@ -38,25 +38,8 @@ namespace VuFind\Log\Writer;
  */
 class Mail extends \Zend\Log\Writer\Mail
 {
-    /**
-     * Holds the verbosity level
-     *
-     * @var int
-     */
-    protected $verbosity = 1;
-
-    /**
-     * Set verbosity
-     *
-     * @param integer $verb verbosity setting
-     *
-     * @return void
-     */
-    public function setVerbosity($verb)
-    {
-        $this->verbosity = $verb;
-    }
-
+    use VerbosityTrait;
+    
     /**
      * Write a message to the log.
      *
@@ -67,12 +50,7 @@ class Mail extends \Zend\Log\Writer\Mail
      */
     protected function doWrite(array $event)
     {
-        // Apply verbosity filter:
-        if (is_array($event['message'])) {
-            $event['message'] = $event['message'][$this->verbosity];
-        }
-
-        // Call parent method:
-        return parent::doWrite($event);
+        // Apply verbosity, Call parent method:
+        return parent::doWrite($this->applyVerbosity($event));
     }
 }
diff --git a/module/VuFind/src/VuFind/Log/Writer/Post.php b/module/VuFind/src/VuFind/Log/Writer/Post.php
new file mode 100644
index 0000000000000000000000000000000000000000..178825edbeffd1d6eb7f5c8543ebf89279442d15
--- /dev/null
+++ b/module/VuFind/src/VuFind/Log/Writer/Post.php
@@ -0,0 +1,124 @@
+<?php
+/**
+ * HTTP POST log writer
+ *
+ * PHP version 5
+ *
+ * Copyright (C) Villanova University 2010.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ * @category VuFind
+ * @package  Error_Logging
+ * @author   Chris Hallberg <challber@villanova.edu>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     https://vufind.org Main Site
+ */
+namespace VuFind\Log\Writer;
+use Zend\Http\Client;
+
+/**
+ * This class extends the Zend Logging to sent POST messages over HTTP
+ *
+ * @category VuFind
+ * @package  Error_Logging
+ * @author   Chris Hallberg <challber@villanova.edu>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     https://vufind.org Main Site
+ */
+class Post extends \Zend\Log\Writer\AbstractWriter
+{
+    use VerbosityTrait;
+
+    /**
+     * Holds the verbosity level
+     *
+     * @var int
+     */
+    protected $url = null;
+
+    /**
+     * Pre-configured http client
+     *
+     * @var \Zend\Http\Client
+     */
+    protected $client = null;
+
+    /**
+     * Content type
+     *
+     * @var string
+     */
+    protected $contentType = 'application/x-www-form-urlencoded';
+
+    /**
+     * Constructor
+     *
+     * @param string $url    URL to open as a stream
+     * @param Client $client Pre-configured http client
+     */
+    public function __construct($url, Client $client)
+    {
+        $this->url = $url;
+        $this->client = $client;
+    }
+
+    /**
+     * Set verbosity
+     *
+     * @param int $type content type string
+     *
+     * @return void
+     */
+    public function setContentType($type)
+    {
+        $this->contentType = $type;
+    }
+
+    /**
+     * Get data for raw body
+     *
+     * @param array $event event data
+     *
+     * @return string
+     */
+    protected function getBody($event)
+    {
+        return ['message' => $this->formatter->format($event) . PHP_EOL];
+    }
+
+    /**
+     * Write a message to the log.
+     *
+     * @param array $event event data
+     *
+     * @return void
+     * @throws \Zend\Log\Exception\RuntimeException
+     */
+    protected function doWrite(array $event)
+    {
+        // Apply verbosity filter:
+        if (is_array($event['message'])) {
+            $event['message'] = $event['message'][$this->verbosity];
+        }
+
+        // Create request
+        $this->client->setUri($this->url);
+        $this->client->setMethod('POST');
+        $this->client->setEncType($this->contentType);
+        $this->client->setRawBody($this->getBody($this->applyVerbosity($event)));
+        // Send
+        $response = $this->client->send();
+    }
+}
diff --git a/module/VuFind/src/VuFind/Log/Writer/Slack.php b/module/VuFind/src/VuFind/Log/Writer/Slack.php
new file mode 100644
index 0000000000000000000000000000000000000000..26284522ac531f036b80f59d411c7afa74fa0e6f
--- /dev/null
+++ b/module/VuFind/src/VuFind/Log/Writer/Slack.php
@@ -0,0 +1,108 @@
+<?php
+/**
+ * HTTP POST log writer for Slack
+ *
+ * PHP version 5
+ *
+ * Copyright (C) Villanova University 2010.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ * @category VuFind
+ * @package  Error_Logging
+ * @author   Chris Hallberg <challber@villanova.edu>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     https://vufind.org Main Site
+ */
+namespace VuFind\Log\Writer;
+use Zend\Http\Client;
+
+/**
+ * This class extends the Zend Logging to send errors to Slack
+ *
+ * @category VuFind
+ * @package  Error_Logging
+ * @author   Chris Hallberg <challber@villanova.edu>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     https://vufind.org Main Site
+ */
+class Slack extends Post
+{
+    /**
+     * The slack channel that should receive messages.
+     *
+     * @var string
+     */
+    protected $channel = '#vufind_log';
+
+    /**
+     * The slack username messages are posted under.
+     *
+     * @var string
+     */
+    protected $username = 'VuFind Log';
+
+    /**
+     * Icons that appear at the start of log messages in Slack, by severity
+     *
+     * @var array
+     */
+    protected $messageIcons = [
+        ':fire: :fire: :fire: ', // EMERG
+        ':rotating_light: ',     // ALERT
+        ':red_circle: ',         // CRIT
+        ':exclamation: ',        // ERR
+        ':warning: ',            // WARN
+        ':speech_balloon: ',     // NOTICE
+        ':information_source: ', // INFO
+        ':beetle: '              // DEBUG
+    ];
+
+    /**
+     * Constructor
+     *
+     * @param string $url     URL to open as a stream
+     * @param Client $client  Pre-configured http client
+     * @param array  $options Optional settings (may contain 'channel' for the
+     * Slack channel to use and/or 'name' for the username messages are posted under)
+     */
+    public function __construct($url, Client $client, array $options = [])
+    {
+        if (isset($options['channel'])) {
+            $this->channel = $options['channel'];
+        }
+        if (isset($options['name'])) {
+            $this->username = $options['name'];
+        }
+        parent::__construct($url, $client);
+    }
+
+    /**
+     * Get data for raw body
+     *
+     * @param array $event event data
+     *
+     * @return string
+     */
+    protected function getBody($event)
+    {
+        $data = [
+            'channel' => $this->channel,
+            'username' => $this->username,
+            'text' => $this->messageIcons[$event['priority']]
+                . $this->formatter->format($event) . PHP_EOL
+        ];
+        return json_encode($data);
+    }
+}
diff --git a/module/VuFind/src/VuFind/Log/Writer/Stream.php b/module/VuFind/src/VuFind/Log/Writer/Stream.php
index b274f94c38dac0771f366d153dd26b52dfba3510..eca9096fb4947bfadc454342d7290591a1250231 100644
--- a/module/VuFind/src/VuFind/Log/Writer/Stream.php
+++ b/module/VuFind/src/VuFind/Log/Writer/Stream.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Error_Logging
@@ -38,24 +38,7 @@ namespace VuFind\Log\Writer;
  */
 class Stream extends \Zend\Log\Writer\Stream
 {
-    /**
-     * Holds the verbosity level
-     *
-     * @var int
-     */
-    protected $verbosity = 1;
-
-    /**
-     * Set verbosity
-     *
-     * @param integer $verb verbosity setting
-     *
-     * @return void
-     */
-    public function setVerbosity($verb)
-    {
-        $this->verbosity = $verb;
-    }
+    use VerbosityTrait;
 
     /**
      * Write a message to the log.
@@ -67,12 +50,7 @@ class Stream extends \Zend\Log\Writer\Stream
      */
     protected function doWrite(array $event)
     {
-        // Apply verbosity filter:
-        if (is_array($event['message'])) {
-            $event['message'] = $event['message'][$this->verbosity];
-        }
-
-        // Call parent method:
-        return parent::doWrite($event);
+        // Apply verbosity, Call parent method:
+        return parent::doWrite($this->applyVerbosity($event));
     }
 }
diff --git a/module/VuDL/tests/unit-tests/src/VuDLTest/Connection/AbstractBaseTest.php b/module/VuFind/src/VuFind/Log/Writer/VerbosityTrait.php
similarity index 56%
rename from module/VuDL/tests/unit-tests/src/VuDLTest/Connection/AbstractBaseTest.php
rename to module/VuFind/src/VuFind/Log/Writer/VerbosityTrait.php
index b03c1b40a4d60824fc1b1720e60f3236eceb3f55..e2cb2ab14959de0c6f767357a3026aea5f3d68b7 100644
--- a/module/VuDL/tests/unit-tests/src/VuDLTest/Connection/AbstractBaseTest.php
+++ b/module/VuFind/src/VuFind/Log/Writer/VerbosityTrait.php
@@ -1,10 +1,10 @@
 <?php
 /**
- * VuDL Abstract Base Test Class
+ * Trait to add configurable verbosity settings to loggers
  *
  * PHP version 5
  *
- * Copyright (C) Villanova University 2010.
+ * Copyright (C) Villanova University 2016.
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License version 2,
@@ -20,51 +20,56 @@
  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
  *
  * @category VuFind
- * @package  Tests
+ * @package  View_Helpers
  * @author   Demian Katz <demian.katz@villanova.edu>
  * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
  * @link     https://vufind.org/wiki/development:testing:unit_tests Wiki
  */
-namespace VuDLTest;
+namespace VuFind\Log\Writer;
 
 /**
- * VuDL Solr Manager Test Class
+ * Trait to add configurable verbosity settings to loggers
  *
  * @category VuFind
- * @package  Tests
+ * @package  View_Helpers
  * @author   Demian Katz <demian.katz@villanova.edu>
  * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
  * @link     https://vufind.org/wiki/development:testing:unit_tests Wiki
  */
-class AbstractBaseTest extends \VuFindTest\Unit\TestCase
+trait VerbosityTrait
 {
-    public function testConstructorAndSet()
-    {
-        $subject = new \VuDL\Connection\AbstractBase([]);
-        $subject->setHttpService(new FakeInterface());
-    }
-}
-
-class FakeInterface implements \VuFindHttp\HttpServiceInterface
-{
-    public function proxify(\Zend\Http\Client $client, array $options = [])
-    {
-
-    }
-    public function get($url, array $params = [], $timeout = null)
-    {
+    /**
+     * Holds the verbosity level
+     *
+     * @var int
+     */
+    protected $verbosity = 1;
 
-    }
-    public function post($url, $body = null, $type = 'application/octet-stream', $timeout = null)
+    /**
+     * Set verbosity
+     *
+     * @param int $verb verbosity setting
+     *
+     * @return void
+     */
+    public function setVerbosity($verb)
     {
-
+        $this->verbosity = $verb;
     }
-    public function postForm($url, array $params = [], $timeout = null)
-    {
 
-    }
-    public function createClient($url, $method = \Zend\Http\Request::METHOD_GET, $timeout = null)
+    /**
+     * Apply verbosity setting to message.
+     *
+     * @param array $event event data
+     *
+     * @return array
+     */
+    protected function applyVerbosity(array $event)
     {
-
+        // Apply verbosity filter:
+        if (is_array($event['message'])) {
+            $event['message'] = $event['message'][$this->verbosity];
+        }
+        return $event;
     }
 }
diff --git a/module/VuFind/src/VuFind/Mailer/Factory.php b/module/VuFind/src/VuFind/Mailer/Factory.php
index 86cf227e30aa3729fdaaf55b387ed82ee07719a2..996c3e4d9d0ee6078e754607cc15f72f2cf8ee07 100644
--- a/module/VuFind/src/VuFind/Mailer/Factory.php
+++ b/module/VuFind/src/VuFind/Mailer/Factory.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Mailer
diff --git a/module/VuFind/src/VuFind/Mailer/Mailer.php b/module/VuFind/src/VuFind/Mailer/Mailer.php
index 118c8fc5a12d3e7a4f8d6e2149d1583020c7910a..d149369a801fc21ffd34ea514315b14c7138ec3e 100644
--- a/module/VuFind/src/VuFind/Mailer/Mailer.php
+++ b/module/VuFind/src/VuFind/Mailer/Mailer.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Mailer
diff --git a/module/VuFind/src/VuFind/Net/IpAddressUtils.php b/module/VuFind/src/VuFind/Net/IpAddressUtils.php
index 304cc149cd6685da00c7c4efcfbaef6a5d850792..5c997e6158b2eb9e523eb8b8d02c1be68714b021 100644
--- a/module/VuFind/src/VuFind/Net/IpAddressUtils.php
+++ b/module/VuFind/src/VuFind/Net/IpAddressUtils.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Authorization
@@ -41,8 +41,8 @@ class IpAddressUtils
     /**
      * Normalize an IP address or a beginning of it to an IPv6 address
      *
-     * @param string  $ip  IP Address
-     * @param boolean $end Whether to make a partial address  an "end of range"
+     * @param string $ip  IP Address
+     * @param bool   $end Whether to make a partial address  an "end of range"
      * address
      *
      * @return string|false Packed in_addr representation if successful, false
diff --git a/module/VuFind/src/VuFind/OAI/Server.php b/module/VuFind/src/VuFind/OAI/Server.php
index e8726872a21033d43163da48e7a9b01f5e6c671c..a161e5dceed85c6e3985be4a2fed78808150430d 100644
--- a/module/VuFind/src/VuFind/OAI/Server.php
+++ b/module/VuFind/src/VuFind/OAI/Server.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  OAI_Server
diff --git a/module/VuFind/src/VuFind/OAI/Server/Auth.php b/module/VuFind/src/VuFind/OAI/Server/Auth.php
index 5906a971070b838ab71b2f514746e96b37d4dae4..826bf57f8b5853ce41a2f2340cd5246a01af6899 100644
--- a/module/VuFind/src/VuFind/OAI/Server/Auth.php
+++ b/module/VuFind/src/VuFind/OAI/Server/Auth.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  OAI_Server
diff --git a/module/VuFind/src/VuFind/QRCode/Loader.php b/module/VuFind/src/VuFind/QRCode/Loader.php
index dcc1b43e4ec7c0ed617de76df0d8f97d4de009e5..b6dd7667e353dfbbad61bd8c44dac6fd00d7ee5c 100644
--- a/module/VuFind/src/VuFind/QRCode/Loader.php
+++ b/module/VuFind/src/VuFind/QRCode/Loader.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  QRCode_Generator
diff --git a/module/VuFind/src/VuFind/Recommend/AbstractFacets.php b/module/VuFind/src/VuFind/Recommend/AbstractFacets.php
index 951bb32ddfa175071d53576d82ef0ce46fd3b892..4dbca413a88096adeeb483e8a825d455c77a7a11 100644
--- a/module/VuFind/src/VuFind/Recommend/AbstractFacets.php
+++ b/module/VuFind/src/VuFind/Recommend/AbstractFacets.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Recommendations
diff --git a/module/VuFind/src/VuFind/Recommend/AbstractSummonRecommend.php b/module/VuFind/src/VuFind/Recommend/AbstractSummonRecommend.php
index 1cb4fe4412187fe71040cb385ea25eaa2ef79509..8afb149ca35d470390e634c47a1e5dc0c550097f 100644
--- a/module/VuFind/src/VuFind/Recommend/AbstractSummonRecommend.php
+++ b/module/VuFind/src/VuFind/Recommend/AbstractSummonRecommend.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Recommendations
diff --git a/module/VuFind/src/VuFind/Recommend/AbstractSummonRecommendDeferred.php b/module/VuFind/src/VuFind/Recommend/AbstractSummonRecommendDeferred.php
index ba9283535708ac3b9f3ef37f025ed2e528108f1c..b40561eafbfc3beefcfcd9b48acd0cfc5b8d817a 100644
--- a/module/VuFind/src/VuFind/Recommend/AbstractSummonRecommendDeferred.php
+++ b/module/VuFind/src/VuFind/Recommend/AbstractSummonRecommendDeferred.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Recommendations
diff --git a/module/VuFind/src/VuFind/Recommend/AlphaBrowseLink.php b/module/VuFind/src/VuFind/Recommend/AlphaBrowseLink.php
index a3482ccdb990469248cff1917f2773e3e78f54ee..f6dcb1532061a49ecda3931b1b38269ac7ed88d4 100644
--- a/module/VuFind/src/VuFind/Recommend/AlphaBrowseLink.php
+++ b/module/VuFind/src/VuFind/Recommend/AlphaBrowseLink.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Recommendations
diff --git a/module/VuFind/src/VuFind/Recommend/AuthorFacets.php b/module/VuFind/src/VuFind/Recommend/AuthorFacets.php
index 5f625a92e29a3d1b57e02a112758a47dc09e1530..7fee194aa87c8fd669e075cce3418ba23131770f 100644
--- a/module/VuFind/src/VuFind/Recommend/AuthorFacets.php
+++ b/module/VuFind/src/VuFind/Recommend/AuthorFacets.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Recommendations
diff --git a/module/VuFind/src/VuFind/Recommend/AuthorInfo.php b/module/VuFind/src/VuFind/Recommend/AuthorInfo.php
index c2c2d68032443cfe114e101bf10bfb82fb3e2e6a..020823d06c8d9871e9fe609fed77432a984ccdd0 100644
--- a/module/VuFind/src/VuFind/Recommend/AuthorInfo.php
+++ b/module/VuFind/src/VuFind/Recommend/AuthorInfo.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Recommendations
diff --git a/module/VuFind/src/VuFind/Recommend/AuthorityRecommend.php b/module/VuFind/src/VuFind/Recommend/AuthorityRecommend.php
index 5b69e7f5c257b1ea27735ad2d09c215375b48c35..8f9b57e403ad9732d1a5c7b1951dda83abfc2fcf 100644
--- a/module/VuFind/src/VuFind/Recommend/AuthorityRecommend.php
+++ b/module/VuFind/src/VuFind/Recommend/AuthorityRecommend.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Recommendations
diff --git a/module/VuFind/src/VuFind/Recommend/CatalogResults.php b/module/VuFind/src/VuFind/Recommend/CatalogResults.php
index c5ecf39647855c0326b977710c209c3761f9ebb0..30db19bfbbe590e956e03f800981a7e5a60a5f49 100644
--- a/module/VuFind/src/VuFind/Recommend/CatalogResults.php
+++ b/module/VuFind/src/VuFind/Recommend/CatalogResults.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Recommendations
diff --git a/module/VuFind/src/VuFind/Recommend/CollectionSideFacets.php b/module/VuFind/src/VuFind/Recommend/CollectionSideFacets.php
index 1246e9fec61a7061c6ee7edfa540a19024627569..b92fdcc1200825ffef89174bbbe6dec097a856fe 100644
--- a/module/VuFind/src/VuFind/Recommend/CollectionSideFacets.php
+++ b/module/VuFind/src/VuFind/Recommend/CollectionSideFacets.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Recommendations
diff --git a/module/VuFind/src/VuFind/Recommend/DPLATerms.php b/module/VuFind/src/VuFind/Recommend/DPLATerms.php
index 6f193739620fd04b19fb6a9ae4c19b0f9247922d..0e89abae4232f0c4807cc9f9121e72119d182324 100644
--- a/module/VuFind/src/VuFind/Recommend/DPLATerms.php
+++ b/module/VuFind/src/VuFind/Recommend/DPLATerms.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Recommendations
diff --git a/module/VuFind/src/VuFind/Recommend/Deprecated.php b/module/VuFind/src/VuFind/Recommend/Deprecated.php
index b4224e819d68cff3a6a5f103811d4da5e34d20f5..0fa9eb5b6e562d391834636c7eea059a7aa8b772 100644
--- a/module/VuFind/src/VuFind/Recommend/Deprecated.php
+++ b/module/VuFind/src/VuFind/Recommend/Deprecated.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Recommendations
diff --git a/module/VuFind/src/VuFind/Recommend/EuropeanaResults.php b/module/VuFind/src/VuFind/Recommend/EuropeanaResults.php
index b6e627fe1b65f0ca5c96ba67f461053c3bbf1df5..c906684f38e42b5f353b557449857a24959f3ac5 100644
--- a/module/VuFind/src/VuFind/Recommend/EuropeanaResults.php
+++ b/module/VuFind/src/VuFind/Recommend/EuropeanaResults.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Recommendations
@@ -32,7 +32,7 @@ use Zend\Feed\Reader\Reader as FeedReader;
 /**
  * EuropeanaResults Recommendations Module
  *
- * This class provides recommendations by using the WorldCat Terminologies API.
+ * This class provides recommendations by using the Europeana API.
  *
  * @category VuFind
  * @package  Recommendations
@@ -139,7 +139,7 @@ class EuropeanaResults implements RecommendInterface,
         // Parse out parameters:
         $params = explode(':', $settings);
         $this->baseUrl = (isset($params[0]) && !empty($params[0]))
-            ? $params[0] : 'api.europeana.eu/api/opensearch.rss';
+            ? $params[0] : 'api.europeana.eu/api/v2/opensearch.rss';
         $this->requestParam = (isset($params[1]) && !empty($params[1]))
             ? $params[1] : 'searchTerms';
         $this->limit = isset($params[2]) && is_numeric($params[2])
@@ -231,7 +231,7 @@ class EuropeanaResults implements RecommendInterface,
             if (!empty($link)) {
                 $resultsProcessed[] = [
                     'title' => $value->getTitle(),
-                    'link' => substr($link, 0, strpos($link, '.srw')) . '.html',
+                    'link' => $link,
                     'enclosure' => $value->getEnclosure()['url']
                 ];
             }
diff --git a/module/VuFind/src/VuFind/Recommend/EuropeanaResultsDeferred.php b/module/VuFind/src/VuFind/Recommend/EuropeanaResultsDeferred.php
index d9ed603dff57451a71273261837e2e1c53c1be93..768335db2a7462775525d4d87a2c9e3629b4a89d 100644
--- a/module/VuFind/src/VuFind/Recommend/EuropeanaResultsDeferred.php
+++ b/module/VuFind/src/VuFind/Recommend/EuropeanaResultsDeferred.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Recommendations
diff --git a/module/VuFind/src/VuFind/Recommend/Factory.php b/module/VuFind/src/VuFind/Recommend/Factory.php
index 457cb58e739a4add9da7f4f534a46200d886b260..23b56b4aec8ca2a05232012058903d40b5824c30 100644
--- a/module/VuFind/src/VuFind/Recommend/Factory.php
+++ b/module/VuFind/src/VuFind/Recommend/Factory.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Recommendations
@@ -182,6 +182,21 @@ class Factory
         );
     }
 
+    /**
+     * Factory for MapSelection module.
+     *
+     * @param ServiceManager $sm Service manager.
+     *
+     * @return MapSelection
+     */
+    public function getMapSelection(ServiceManager $sm)
+    {
+        $config = $sm->getServiceLocator()->get('Vufind\Config');
+        $backend = $sm->getServiceLocator()->get('VuFind\Search\BackendManager');
+        $solr = $backend->get('Solr');
+        return new MapSelection($config, $solr);
+    }
+
     /**
      * Factory for Random Recommendations.
      *
@@ -197,6 +212,21 @@ class Factory
         );
     }
 
+    /**
+     * Factory for ResultGoogleMapAjax Recommendations.
+     *
+     * @param ServiceManager $sm Service manager.
+     *
+     * @return ResultGoogleMapAjax
+     */
+    public static function getResultGoogleMapAjax(ServiceManager $sm)
+    {
+        $config = $sm->getServiceLocator()->get('VuFind\Config')->get('config');
+        $key = isset($config->Content->googleMapApiKey)
+            ? $config->Content->googleMapApiKey : null;
+        return new ResultGoogleMapAjax($key);
+    }
+
     /**
      * Factory for SideFacets module.
      *
diff --git a/module/VuFind/src/VuFind/Recommend/FavoriteFacets.php b/module/VuFind/src/VuFind/Recommend/FavoriteFacets.php
index f3b325f4c2efbeba0ca9be98740bbc9223c00788..525264622d453569f63bb669f9b0157e26fa56ed 100644
--- a/module/VuFind/src/VuFind/Recommend/FavoriteFacets.php
+++ b/module/VuFind/src/VuFind/Recommend/FavoriteFacets.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Recommendations
diff --git a/module/VuFind/src/VuFind/Recommend/Libraryh3lp.php b/module/VuFind/src/VuFind/Recommend/Libraryh3lp.php
index d3e4fc0b8a8e863b9ec34941932454b4adbd5f2f..f42319204cadbb5e4235bde2eddefab51687f17f 100644
--- a/module/VuFind/src/VuFind/Recommend/Libraryh3lp.php
+++ b/module/VuFind/src/VuFind/Recommend/Libraryh3lp.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Recommendations
diff --git a/module/VuFind/src/VuFind/Recommend/MapSelection.php b/module/VuFind/src/VuFind/Recommend/MapSelection.php
new file mode 100644
index 0000000000000000000000000000000000000000..31a33758bacc5a94368f6ef68f07acd2b76df9fc
--- /dev/null
+++ b/module/VuFind/src/VuFind/Recommend/MapSelection.php
@@ -0,0 +1,633 @@
+<?php
+/**
+ * MapSelection Recommendations Module
+ *
+ * PHP version 5
+ *
+ * Copyright (C) Villanova University 2010.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ * @category VuFind2
+ * @package  Recommendations
+ * @author   Vaclav Rosecky <xrosecky@gmail.com>
+ * @author   Leila Gonzales <lmg@agiweb.org>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     http://vufind.org/wiki/vufind2:recommendation_modules Wiki
+ */
+namespace VuFind\Recommend;
+
+/**
+ * MapSelection Recommendations Module
+ *
+ * @category VuFind2
+ * @package  Recommendations
+ * @author   Vaclav Rosecky <xrosecky@gmail.com>
+ * @author   Leila Gonzales <lmg@agiweb.org>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     http://vufind.org/wiki/vufind2:recommendation_modules Wiki
+ */
+class MapSelection implements \VuFind\Recommend\RecommendInterface
+{
+    /**
+     * Default coordinates. Order is WENS
+     *
+     * @var array
+     */
+    protected $defaultCoordinates = [];
+
+    /**
+     * The geoField variable name
+     *
+     * @var string
+     */
+    protected $geoField = 'location_geo';
+
+    /**
+     * Height of search map pane
+     *
+     * @var string
+     */
+    protected $height;
+
+    /**
+     * Selected coordinates
+     *
+     * @var string
+     */
+    protected $selectedCoordinates = null;
+
+    /**
+     * Search parameters
+     *
+     * @var string
+     */
+    protected $searchParams = null;
+
+    /**
+     * Search object
+     *
+     * @var string
+     */
+    protected $searchObject;
+
+    /**
+     * Search Results coordinates
+     *
+     * @var array
+     */
+    protected $searchResultCoords = [];
+
+    /**
+     * Bbox search box coordinates
+     *
+     * @var array
+     */
+    protected $bboxSearchCoords = [];
+
+    /**
+     * Configuration loader
+     *
+     * @var \VuFind\Config\PluginManager
+     */
+    protected $configLoader;
+
+    /**
+     * Solr search loader
+     *
+     * @var \VuFind\Search\BackendManager
+     */
+    protected $solr;
+
+    /**
+     * Query Builder object
+     *
+     * @var \VuFind\Search\BackendManager
+     */
+    protected $queryBuilder;
+
+    /**
+     * Solr connector Object
+     *
+     * @var \VuFind\Search\BackendManager
+     */
+    protected $solrConnector;
+
+    /**
+     * Query Object
+     *
+     * @var \VuFind\Search\BackendManager
+     */
+    protected $searchQuery;
+
+    /**
+     * Backend Parameters / Search Filters
+     *
+     * @var \VuFind\Search\BackendManager
+     */
+    protected $searchFilters;
+
+    /**
+     * Constructor
+     *
+     * @param \VuFind\Config\PluginManager  $configLoader Configuration loader
+     * @param \VuFind\Search\BackendManager $solr         Search interface
+     */
+    public function __construct(\VuFind\Config\PluginManager $configLoader, $solr)
+    {
+        $this->configLoader = $configLoader;
+        $this->solr = $solr;
+        $this->queryBuilder = $solr->getQueryBuilder();
+        $this->solrConnector = $solr->getConnector();
+    }
+
+    /**
+     * SetConfig
+     *
+     * Store the configuration of the recommendation module.
+     *
+     * @param string $settings Settings from searches.ini.
+     *
+     * @return void
+     */
+    public function setConfig($settings)
+    {
+        $settings = explode(':', $settings);
+        $mainSection = empty($settings[0]) ? 'MapSelection' : $settings[0];
+        $iniFile = empty($settings[1]) ? 'searches' : $settings[1];
+        $config = $this->configLoader->get($iniFile);
+        if (isset($config->$mainSection)) {
+            $entries = $config->$mainSection;
+            if (isset($entries->default_coordinates)) {
+                $this->defaultCoordinates = explode(
+                    ',', $entries->default_coordinates
+                );
+            }
+            if (isset($entries->height)) {
+                $this->height = $entries->height;
+            }
+        }
+    }
+
+    /**
+     * Init
+     *
+     * Called at the end of the Search Params objects' initFromRequest() method.
+     * This method is responsible for setting search parameters needed by the
+     * recommendation module and for reading any existing search parameters that may
+     * be needed.
+     *
+     * @param \VuFind\Search\Solr\Params $params  Search parameter object
+     * @param \Zend\StdLib\Parameters    $request Parameter object representing user
+     * request.
+     *
+     * @return void
+     */
+    public function init($params, $request)
+    {
+    }
+
+    /**
+     * Process
+     *
+     * Called after the Search Results object has performed its main search.  This
+     * may be used to extract necessary information from the Search Results object
+     * or to perform completely unrelated processing.
+     *
+     * @param \VuFind\Search\Base\Results $results Search results object
+     *
+     * @return void
+     */
+    public function process($results)
+    {
+        $reorder_coords = [];
+        $filters = $results->getParams()->getFilters();
+        foreach ($filters as $key => $value) {
+            if ($key == $this->geoField) {
+                $match = [];
+                if (preg_match(
+                    '/Intersects\(ENVELOPE\((.*), (.*), (.*), (.*)\)\)/',
+                    $value[0], $match
+                )
+                ) {
+                    array_push(
+                        $this->bboxSearchCoords,
+                        (float)$match[1], (float)$match[2],
+                        (float)$match[3], (float)$match[4]
+                    );
+                    // Need to reorder coords from WENS to WSEN
+                    array_push(
+                        $reorder_coords,
+                        (float)$match[1], (float)$match[4],
+                        (float)$match[2], (float)$match[3]
+                    );
+                    $this->selectedCoordinates = $reorder_coords;
+                }
+                $this->searchParams = $results->getUrlQuery()->removeFacet(
+                    $this->geoField, $value[0], false
+                );
+            }
+        }
+        if ($this->searchParams == null) {
+            $this->searchParams = $results->getUrlQuery()->getParams(false);
+        }
+        $this->searchFilters = $results->getParams()->getBackendParameters();
+        $this->searchQuery = $results->getParams()->getQuery();
+    }
+
+    /**
+     * GetSelectedCoordinates
+     *
+     * Return coordinates selected by user
+     *
+     * @return array of floats
+     */
+    public function getSelectedCoordinates()
+    {
+        return $this->selectedCoordinates;
+    }
+
+    /**
+     * GetDefaultCoordinates
+     *
+     * Return default coordinates from configuration
+     *
+     * @return array of floats
+     */
+    public function getDefaultCoordinates()
+    {
+        return $this->defaultCoordinates;
+    }
+
+    /**
+     * GetHeight
+     *
+     * Return height of map in pixels
+     *
+     * @return number
+     */
+    public function getHeight()
+    {
+        return $this->height;
+    }
+
+    /**
+     * GetSearchParams
+     *
+     * Return search params without filter for geographic search
+     *
+     * @return string
+     */
+    public function getSearchParams()
+    {
+        return $this->searchParams;
+    }
+
+    /**
+     * GetSearchParams no question mark at end
+     *
+     * Return search params without leading question mark and colon.
+     * Copied from ResultGoogleMapAjax.php and chngd name to add NoQ.LMG
+     *
+     * @return string
+     */
+    public function getSearchParamsNoQ()
+    {
+        // Get search parameters and return them minus the leading ?:
+           return substr($this->searchObject->getUrlQuery()->getParams(false), 1);
+    }
+
+    /**
+     * GetGeoField
+     *
+     * Return Solr field to use for geographic search
+     *
+     * @return string
+     */
+    public function getGeoField()
+    {
+        return $this->geoField;
+    }
+
+    /**
+     * Get geo field values for all search results
+     *
+     * @return array
+     */
+    public function getSearchResultCoordinates()
+    {
+        $result = [];
+        $params = $this->searchFilters;
+        // Check to makes sure we have a geographic search
+        if (strpos($params->get('fq')[0], $this->geoField) !== false) {
+            $params->mergeWith($this->queryBuilder->build($this->searchQuery));
+            $params->set('fl', 'id, ' . $this->geoField . ', title');
+            $params->set('wt', 'json');
+            $params->set('rows', '10000000'); // set to return all results
+            $response = json_decode($this->solrConnector->search($params));
+            foreach ($response->response->docs as $current) {
+                $result[] = [
+                    $current->id, $current->{$this->geoField}, $current->title
+                ];
+            }
+        }
+        return $result;
+    }
+
+    /**
+     * Convert coordinates to 360 degree grid
+     *
+     * @param array $coordinates coordinates for conversion
+     *
+     * @return array
+     */
+    public function coordinatesToGrid($coordinates)
+    {
+        $gridCoords = [];
+        list($coordW, $coordE, $coordN, $coordS) = $coordinates;
+        if ($coordE == (float)-0) {
+            $coordE = (float)0;
+        }
+        // Convert coordinates to 360 degree grid
+        if ($coordE < $coordW && $coordE < 0) {
+            $coordE = 360 + $coordE;
+        }
+        if ($coordW < 0 && $coordW >= -180) {
+            $coordW = 360 + $coordW;
+            $coordE = 360 + $coordE;
+        }
+        $gridCoords = [$coordW, $coordE, $coordN, $coordS];
+        return $gridCoords;
+    }
+
+    /**
+     * Convert coordinates to longitude latitude grid
+     *
+     * @param array $centerPt coordinates for conversion
+     *
+     * @return array
+     */
+    public function centerToLongLat($centerPt)
+    {
+        $LongLatCoords = [];
+        list($coordWE, $coordSN) = $centerPt;
+        // convert coordinate to 180 degree grid
+        if ($coordWE > 180) {
+            $coordWE = $coordWE - 360;
+        }
+        $LongLatCoords = [$coordWE, $coordSN];
+        return $LongLatCoords;
+    }
+
+    /**
+     * Calculated the center of search box and coordinate overlap
+     *
+     * @param array $bboxCoords  search box coordinates
+     * @param array $coordinates coordinates for conversion
+     *
+     * @return array
+     */
+    public function getCenterFromBboxCoordIntersect($bboxCoords, $coordinates)
+    {
+        $centerCoordBbox = [];
+        list($bboxW, $bboxE, $bboxN, $bboxS) = $bboxCoords;
+        list($coordW, $coordE, $coordN, $coordS) = $coordinates;
+        $bboxLon = range(floor($bboxW), ceil($bboxE));
+        $bboxLat = range(floor($bboxS), ceil($bboxN));
+        $coordLon = range(floor($coordW), ceil($coordE));
+        $coordLat = range(floor($coordS), ceil($coordN));
+        $cbLon = array_intersect($coordLon, $bboxLon);
+        $cbLat = array_intersect($coordLat, $bboxLat);
+        $centerCoordBbox = [
+            min($cbLon), max($cbLon), min($cbLat), max($cbLat)
+        ];
+        return $centerCoordBbox;
+    }
+
+    /**
+     * Check to see if coordinate and bbox intersect
+     *
+     * @param array $bboxCoords searchbox coordinates
+     * @param array $coordinate result record coordinates
+     *
+     * @return bool
+     */
+    public function coordBboxIntersect($bboxCoords, $coordinate)
+    {
+        $coordIntersect = false;
+        list($bboxW, $bboxE, $bboxN, $bboxS) = $bboxCoords;
+        list($coordW, $coordE, $coordN, $coordS) = $coordinate;
+        //Does coordinate fall within search box
+        if ((($coordW >= $bboxW && $coordW <= $bboxE)
+            || ($coordE >= $bboxW && $coordE <= $bboxE))
+            && (($coordS >= $bboxS && $coordS <= $bboxN)
+            || ($coordN >= $bboxS && $coordN <= $bboxN))
+        ) {
+            $coordIntersect = true;
+        }
+        // Does searchbox fall within coordinate
+        if ((($bboxW >= $coordW && $bboxW <= $coordE)
+            || ($bboxE >= $coordW && $bboxE <= $coordE))
+            && (($bboxS >= $coordS && $bboxS <= $coordN)
+            || ($bboxN >= $coordS && $bboxN <= $coordN))
+        ) {
+            $coordIntersect = true;
+        }
+        // Does searchbox span coordinate
+        if ((($coordE >= $bboxW && $coordE <= $bboxE)
+            && ($coordW >= $bboxW && $coordW <= $bboxE))
+            && ($coordN > $bboxN && $coordS < $bboxS)
+        ) {
+            $coordIntersect = true;
+        }
+        // Does coordinate span searchbox
+        if (($coordW < $bboxW && $coordE > $bboxE)
+            && (($coordS >= $bboxS && $coordS <= $bboxN)
+            && ($coordN >= $bboxS && $coordN <= $bboxN))
+        ) {
+            $coordIntersect = true;
+        }
+        return $coordIntersect;
+    }
+
+    /**
+     * Calculate center point of coordinate set
+     *
+     * @param array $coordinate centerPoint coordinate
+     *
+     * @return array
+     */
+    public function calculateCenterPoint($coordinate)
+    {
+        $centerCoord = [];
+        list($coordW, $coordE, $coordN, $coordS) = $coordinate;
+        // Calculate center point
+        $centerWE = (($coordW - $coordE) / 2) + $coordE;
+        $centerSN = (($coordN - $coordS) / 2) + $coordS;
+        // Return WENS coordinates even though W=E and N=S
+        $centerCoord = [$centerWE, $centerWE, $centerSN, $centerSN];
+        return $centerCoord;
+    }
+
+    /**
+     * Create array of geo features that are in search box
+     *
+     * Return search results record data
+     *
+     * @param string $recordId    record ID
+     * @param string $recordCoord record coordinates
+     * @param string $title       record title
+     * @param array  $bboxCoords  search box coordinates
+     *
+     * @return array
+     */
+    public function createGeoFeature($recordId, $recordCoord, $title, $bboxCoords)
+    {
+        $recId = $recordId;
+        $recCoord = $recordCoord;
+        $recTitle = $title;
+        list($bboxW, $bboxE, $bboxN, $bboxS) = $bboxCoords;
+        $centerData = [];
+        $match = [];
+        if (preg_match('/ENVELOPE\((.*),(.*),(.*),(.*)\)/', $recCoord, $match)) {
+            // Convert coordinates to 360 degree grid
+            $floats = array_map('floatval', $match);
+            $matchCoords = [$floats[1], $floats[2], $floats[3], $floats[4]];
+            $gridCoords = $this->coordinatesToGrid($matchCoords);
+            list($coordW, $coordE, $coordN, $coordS) = $gridCoords;
+
+            // Adjust coordinates on grid if necessary based on search box
+            if ($bboxW > 180 && ($coordW > 0 && $coordW < 180)) {
+                $coordW = 360 + $coordW;
+                $coordE = 360 + $coordE;
+            }
+            if ($bboxE > 180 && ($coordE > 0 && $coordE < 180)) {
+                $coordE = 360 + $coordE;
+            }
+            //Does coordinate fall within search box
+            if ($this->coordBboxIntersect(
+                $bboxCoords, [$coordW, $coordE, $coordN, $coordS]
+            )
+            ) {
+                // Calculate center point
+                $centerPt = $this->calculateCenterPoint(
+                    [$coordW, $coordE, $coordN, $coordS]
+                );
+                // Does center point intersect search box?
+                if ($this->coordBboxIntersect($bboxCoords, $centerPt)) {
+                    // Convert center point to long lat cooridnate
+                    $ctrLongLat = $this->centerToLongLat(
+                        [$centerPt[0], $centerPt[2]]
+                    );
+                    $centerData = [
+                        $recId, $ctrLongLat[0], $ctrLongLat[1], $recTitle
+                    ];
+                } else {
+                    // Recalculate center point
+                    $centerCoordBbox = $this->getCenterFromBboxCoordIntersect(
+                        [$bboxW, $bboxE, $bboxN, $bboxS],
+                        [$coordW, $coordE, $coordN, $coordS]
+                    );
+                    // Calculate new center point
+                    $newCtr = $this->calculateCenterPoint($centerCoordBbox);
+                    // Does new center point intersect search box?
+                    if ($this->coordBboxIntersect($bboxCoords, $newCtr)) {
+                        // Convert new center point to long lat cooridnate
+                        $ctrLongLat = $this->centerToLongLat(
+                            [$newCtr[0], $newCtr[2]]
+                        );
+                        $centerData = [
+                            $recId, $ctrLongLat[0], $ctrLongLat[1], $recTitle
+                        ];
+                    } else {
+                        // Make center point center of search box
+                        $bboxCtr = $this->calculateCenterPoint(
+                            [$bboxW, $bboxE, $bboxN, $bboxS]
+                        );
+                        $ctrLongLat = $this->centerToLongLat(
+                            [$bboxCtr[0],$bboxCtr[2]]
+                        );
+                        $centerData = [
+                            $recId, $ctrLongLat[0], $ctrLongLat[1], $recTitle
+                        ];
+                    }
+
+                }
+            }
+        }
+        return $centerData;
+    }
+
+    /**
+     * Process search result record coordinate values
+     *
+     * Return search results record coordinates and process for
+     * display on search map.
+     *
+     * @return array
+     */
+    public function getMapResultCoordinates()
+    {
+        $centerCoords = [];
+        $rawCoordIds = [];
+        $centerCoordIds = [];
+        // Both coordinate variables are in WENS order //
+        $rawCoords = $this->getSearchResultCoordinates();
+        // Convert bbox coords to 360 grid  //
+        $bboxCoords = $this->coordinatesToGrid($this->bboxSearchCoords);
+        list($bboxW, $bboxE, $bboxN, $bboxS) = $bboxCoords;
+        foreach ($rawCoords as $idCoords) {
+            foreach ($idCoords[1] as $coord) {
+                $recId = $idCoords[0];
+                $rawCoordIds[] = $recId;
+                $title = $idCoords[2];
+                $centerPoint = $this->createGeoFeature(
+                    $recId, $coord, $title, $bboxCoords
+                );
+                if ($centerPoint) {
+                    $centerCoordIds[] = $centerPoint[0];
+                    $centerCoords[] = $centerPoint;
+                    break;
+                }
+            }
+        }
+        //Solr search includes close-by geo features
+        //Check and add these if there are any
+        $addIds = array_merge(
+            array_diff($rawCoordIds, $centerCoordIds),
+            array_diff($centerCoordIds, $rawCoordIds)
+        );
+        //Remove duplicate ids
+        $addIds = array_unique($addIds);
+        if (count($addIds) > 0) {
+            $bboxCenter = $this->calculateCenterPoint(
+                [$bboxW, $bboxE, $bboxN, $bboxS]
+            );
+            $centerLongLat = $this->centerToLongLat([$bboxCenter[0],$bboxCenter[2]]);
+            foreach ($addIds as $coordId) {
+                foreach ($rawCoords as $idCoords) {
+                    if ($coordId == $idCoords[0]) {
+                        $title = $idCoords[2];
+                        $centerCoords[] = [$coordId,
+                            $centerLongLat[0],
+                            $centerLongLat[1],
+                            $title
+                        ];
+                    }
+                }
+            }
+        }
+        return $centerCoords;
+    }
+}
diff --git a/module/VuFind/src/VuFind/Recommend/OpenLibrarySubjects.php b/module/VuFind/src/VuFind/Recommend/OpenLibrarySubjects.php
index 7a5cfdd9fd6bac163c48eb8cfedb0665cad669a3..7a62aa79bd05ca85ba6b9ac569cc42660ee9ae59 100644
--- a/module/VuFind/src/VuFind/Recommend/OpenLibrarySubjects.php
+++ b/module/VuFind/src/VuFind/Recommend/OpenLibrarySubjects.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Recommendations
diff --git a/module/VuFind/src/VuFind/Recommend/OpenLibrarySubjectsDeferred.php b/module/VuFind/src/VuFind/Recommend/OpenLibrarySubjectsDeferred.php
index 8bd8bae0aa0361348f012c26659c0e307da8e958..c3a6121f946dabb411756ab82c9813cdbafd08dd 100644
--- a/module/VuFind/src/VuFind/Recommend/OpenLibrarySubjectsDeferred.php
+++ b/module/VuFind/src/VuFind/Recommend/OpenLibrarySubjectsDeferred.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Recommendations
diff --git a/module/VuFind/src/VuFind/Recommend/PluginFactory.php b/module/VuFind/src/VuFind/Recommend/PluginFactory.php
index 1d89f4f32c980b3f942813f4f08f986257ca37ea..098b64fb81b201150c5199b9e0ad626f022aab27 100644
--- a/module/VuFind/src/VuFind/Recommend/PluginFactory.php
+++ b/module/VuFind/src/VuFind/Recommend/PluginFactory.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Recommendations
diff --git a/module/VuFind/src/VuFind/Recommend/PluginManager.php b/module/VuFind/src/VuFind/Recommend/PluginManager.php
index 8b07ba166b3d6c64a575593c049d8c0ff4908df0..4a1270d29491a051d9aaccd967977e74c6743a8b 100644
--- a/module/VuFind/src/VuFind/Recommend/PluginManager.php
+++ b/module/VuFind/src/VuFind/Recommend/PluginManager.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Recommendations
diff --git a/module/VuFind/src/VuFind/Recommend/PubDateVisAjax.php b/module/VuFind/src/VuFind/Recommend/PubDateVisAjax.php
index f36f3beda180a2e90838d36b41ab7ab767b6ad3e..c8967c35fdfe304df16416da15c034d91a98e8f2 100644
--- a/module/VuFind/src/VuFind/Recommend/PubDateVisAjax.php
+++ b/module/VuFind/src/VuFind/Recommend/PubDateVisAjax.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Recommendations
diff --git a/module/VuFind/src/VuFind/Recommend/RandomRecommend.php b/module/VuFind/src/VuFind/Recommend/RandomRecommend.php
index ad8558e9a33493b0135abfad7a3128d617ebdd21..f5211f96fa779af7c6adc668b91385acfe591624 100644
--- a/module/VuFind/src/VuFind/Recommend/RandomRecommend.php
+++ b/module/VuFind/src/VuFind/Recommend/RandomRecommend.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Recommendations
diff --git a/module/VuFind/src/VuFind/Recommend/RecommendInterface.php b/module/VuFind/src/VuFind/Recommend/RecommendInterface.php
index 54353f8a8df0d98665939a6201c76583a37e6486..8d63bf12c52d6e86cd49f2017bfdbdc945affeec 100644
--- a/module/VuFind/src/VuFind/Recommend/RecommendInterface.php
+++ b/module/VuFind/src/VuFind/Recommend/RecommendInterface.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Recommendations
diff --git a/module/VuFind/src/VuFind/Recommend/RemoveFilters.php b/module/VuFind/src/VuFind/Recommend/RemoveFilters.php
index 58f176e62b3149a5fb6e8891b414fb6c9b7f449e..9253f8bd17d4f9da7f290b8d37e8579e1b51e841 100644
--- a/module/VuFind/src/VuFind/Recommend/RemoveFilters.php
+++ b/module/VuFind/src/VuFind/Recommend/RemoveFilters.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Recommendations
diff --git a/module/VuFind/src/VuFind/Recommend/ResultGoogleMapAjax.php b/module/VuFind/src/VuFind/Recommend/ResultGoogleMapAjax.php
index d9d6862e3713b3aececb4a9b487f9177850b6fae..833c4f687c894a5b1f23ff6f7f525070e47b14dd 100644
--- a/module/VuFind/src/VuFind/Recommend/ResultGoogleMapAjax.php
+++ b/module/VuFind/src/VuFind/Recommend/ResultGoogleMapAjax.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Recommendations
@@ -50,6 +50,23 @@ class ResultGoogleMapAjax implements RecommendInterface
      */
     protected $searchObject;
 
+    /**
+     * Google Maps API key.
+     *
+     * @var string
+     */
+    protected $googleMapApiKey;
+
+    /**
+     * Constructor
+     *
+     * @param string $key API key
+     */
+    public function __construct($key)
+    {
+        $this->googleMapApiKey = $key;
+    }
+
     /**
      * Store the configuration of the recommendation module.
      *
@@ -93,6 +110,16 @@ class ResultGoogleMapAjax implements RecommendInterface
         $this->searchObject = $results;
     }
 
+    /**
+     * Get the Google Maps API key.
+     *
+     * @return string
+     */
+    public function getGoogleMapApiKey()
+    {
+        return $this->googleMapApiKey;
+    }
+
     /**
      * Get search parameters
      *
diff --git a/module/VuFind/src/VuFind/Recommend/SearchObject.php b/module/VuFind/src/VuFind/Recommend/SearchObject.php
index 8593194979fa31e3cfd016fd740e74b89c87f87c..c4d6520ad1e5c16c045df807f226e8495c4ea2bc 100644
--- a/module/VuFind/src/VuFind/Recommend/SearchObject.php
+++ b/module/VuFind/src/VuFind/Recommend/SearchObject.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Recommendations
diff --git a/module/VuFind/src/VuFind/Recommend/SideFacets.php b/module/VuFind/src/VuFind/Recommend/SideFacets.php
index 03f014f449a17251bf51ab09b443abc1c0530767..30f96a99ef5054c82d9707bf98919006c37df404 100644
--- a/module/VuFind/src/VuFind/Recommend/SideFacets.php
+++ b/module/VuFind/src/VuFind/Recommend/SideFacets.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Recommendations
@@ -84,6 +84,13 @@ class SideFacets extends AbstractFacets
      */
     protected $checkboxFacets = [];
 
+    /**
+     * Settings controlling how lightbox is used for facet display.
+     *
+     * @var bool|string
+     */
+    protected $showInLightboxSettings = [];
+
     /**
      * Settings controlling how many values to display before "show more."
      *
@@ -193,6 +200,10 @@ class SideFacets extends AbstractFacets
             $this->showMoreSettings
                 = $config->Results_Settings->showMore->toArray();
         }
+        if (isset($config->Results_Settings->showMoreInLightbox)) {
+            $this->showInLightboxSettings
+                = $config->Results_Settings->showMoreInLightbox->toArray();
+        }
 
         // Collapsed facets:
         if (isset($config->Results_Settings->collapsedFacets)) {
@@ -348,12 +359,13 @@ class SideFacets extends AbstractFacets
 
     /**
      * Return the list of facets configured to be collapsed
+     * defaults to 6
      *
      * @param string $facetName Name of the facet to get
      *
      * @return int
      */
-    public function getShowMoreSetting($facetName = '*')
+    public function getShowMoreSetting($facetName)
     {
         // Look for either facet-specific configuration or else a configured
         // default. If neither is found, initialize return value to 0.
@@ -367,6 +379,27 @@ class SideFacets extends AbstractFacets
         return (isset($val) && $val > 0) ? $val : 6;
     }
 
+    /**
+     * Return settings for showing more results in the lightbox
+     *
+     * @param string $facetName Name of the facet to get
+     *
+     * @return int
+     */
+    public function getShowInLightboxSetting($facetName)
+    {
+        // Look for either facet-specific configuration or else a configured
+        // default.
+        if (isset($this->showInLightboxSettings[$facetName])) {
+            return $this->showInLightboxSettings[$facetName];
+        } elseif (isset($this->showInLightboxSettings['*'])) {
+            return $this->showInLightboxSettings['*'];
+        }
+
+        // No config found; use default behavior:
+        return 'more';
+    }
+
     /**
      * Get the list of filters to display
      *
diff --git a/module/VuFind/src/VuFind/Recommend/SpellingSuggestions.php b/module/VuFind/src/VuFind/Recommend/SpellingSuggestions.php
index ba9feb2973a4c9f3ceebc4f5b4e008cf73f6242f..23675c4f2b3d1c5c0615c0afad46fc6f36f12a43 100644
--- a/module/VuFind/src/VuFind/Recommend/SpellingSuggestions.php
+++ b/module/VuFind/src/VuFind/Recommend/SpellingSuggestions.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Recommendations
diff --git a/module/VuFind/src/VuFind/Recommend/SummonBestBets.php b/module/VuFind/src/VuFind/Recommend/SummonBestBets.php
index 03aec504a0e2797800fff46c9850babc22cddc9b..a82ff34d0f579b58701ba2c750482705b8c28ee6 100644
--- a/module/VuFind/src/VuFind/Recommend/SummonBestBets.php
+++ b/module/VuFind/src/VuFind/Recommend/SummonBestBets.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Recommendations
diff --git a/module/VuFind/src/VuFind/Recommend/SummonBestBetsDeferred.php b/module/VuFind/src/VuFind/Recommend/SummonBestBetsDeferred.php
index f52ea407855ba6488bf55919d49fe73b95540a5c..7ef745c020f309d1af9bde39a7bbdf28fa50d8e1 100644
--- a/module/VuFind/src/VuFind/Recommend/SummonBestBetsDeferred.php
+++ b/module/VuFind/src/VuFind/Recommend/SummonBestBetsDeferred.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Recommendations
diff --git a/module/VuFind/src/VuFind/Recommend/SummonDatabases.php b/module/VuFind/src/VuFind/Recommend/SummonDatabases.php
index d8747b82b349c687bb92fcd2f99fc626384161c8..f36eb697c0d66559f9ebb1c643c6d9af84665291 100644
--- a/module/VuFind/src/VuFind/Recommend/SummonDatabases.php
+++ b/module/VuFind/src/VuFind/Recommend/SummonDatabases.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Recommendations
diff --git a/module/VuFind/src/VuFind/Recommend/SummonDatabasesDeferred.php b/module/VuFind/src/VuFind/Recommend/SummonDatabasesDeferred.php
index d73fcd2b82aef6fd0c70ad8f9639806f7831e71f..a25f9a92dec618cf5b0232aa1c79fcbf53594b0b 100644
--- a/module/VuFind/src/VuFind/Recommend/SummonDatabasesDeferred.php
+++ b/module/VuFind/src/VuFind/Recommend/SummonDatabasesDeferred.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Recommendations
diff --git a/module/VuFind/src/VuFind/Recommend/SummonResults.php b/module/VuFind/src/VuFind/Recommend/SummonResults.php
index 2eae5db20006c8f2a2e9ff409de3b4b4bd5a3411..9ee4ea7abde1c2fc0d8fe909610cdbbebf884ede 100644
--- a/module/VuFind/src/VuFind/Recommend/SummonResults.php
+++ b/module/VuFind/src/VuFind/Recommend/SummonResults.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Recommendations
diff --git a/module/VuFind/src/VuFind/Recommend/SummonResultsDeferred.php b/module/VuFind/src/VuFind/Recommend/SummonResultsDeferred.php
index d8638036cdf83407162af11488beef5214876eb5..bc42487418f77c5d8bb1c82c25ea0bd5c06282fa 100644
--- a/module/VuFind/src/VuFind/Recommend/SummonResultsDeferred.php
+++ b/module/VuFind/src/VuFind/Recommend/SummonResultsDeferred.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Recommendations
diff --git a/module/VuFind/src/VuFind/Recommend/SummonTopics.php b/module/VuFind/src/VuFind/Recommend/SummonTopics.php
index bf541a4a77fe8d970ea4cc72633397ba9b4b928f..8c815cb28bb83cb57675e0990f61fd06ff1c8917 100644
--- a/module/VuFind/src/VuFind/Recommend/SummonTopics.php
+++ b/module/VuFind/src/VuFind/Recommend/SummonTopics.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Recommendations
diff --git a/module/VuFind/src/VuFind/Recommend/SwitchQuery.php b/module/VuFind/src/VuFind/Recommend/SwitchQuery.php
index 3038a852b7705830d07a4383e342dd9d0a97f87f..41d3b91bf627d93949f8e7996d97e2d42bbbdfbd 100644
--- a/module/VuFind/src/VuFind/Recommend/SwitchQuery.php
+++ b/module/VuFind/src/VuFind/Recommend/SwitchQuery.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Recommendations
diff --git a/module/VuFind/src/VuFind/Recommend/SwitchTab.php b/module/VuFind/src/VuFind/Recommend/SwitchTab.php
index ceba51557d47b11de46818651dd540dfa0d31f83..ef705b54024d2563f8e4346c5bfed26be24cc332 100644
--- a/module/VuFind/src/VuFind/Recommend/SwitchTab.php
+++ b/module/VuFind/src/VuFind/Recommend/SwitchTab.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Recommendations
diff --git a/module/VuFind/src/VuFind/Recommend/SwitchType.php b/module/VuFind/src/VuFind/Recommend/SwitchType.php
index a990be7b525efeff4fc3f488fb302d2cd7165113..b448d68db5c8db0513ae78902ef372a702ac94d8 100644
--- a/module/VuFind/src/VuFind/Recommend/SwitchType.php
+++ b/module/VuFind/src/VuFind/Recommend/SwitchType.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Recommendations
diff --git a/module/VuFind/src/VuFind/Recommend/TopFacets.php b/module/VuFind/src/VuFind/Recommend/TopFacets.php
index 180247079cc9062c97775baafd26cb3c6587c5dd..ba055512c759e9a5f7e2de2be601063c30dc7ddf 100644
--- a/module/VuFind/src/VuFind/Recommend/TopFacets.php
+++ b/module/VuFind/src/VuFind/Recommend/TopFacets.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Recommendations
diff --git a/module/VuFind/src/VuFind/Recommend/VisualFacets.php b/module/VuFind/src/VuFind/Recommend/VisualFacets.php
index 8e6a1794db38017bf5759437cbc2552864020dcd..9e119facc6ffae616f15b599ed59e6b21f937be8 100644
--- a/module/VuFind/src/VuFind/Recommend/VisualFacets.php
+++ b/module/VuFind/src/VuFind/Recommend/VisualFacets.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Recommendations
diff --git a/module/VuFind/src/VuFind/Recommend/WebResults.php b/module/VuFind/src/VuFind/Recommend/WebResults.php
index f9f13d57d9e937aeea9373e2a2cc166eb11a5262..296100943b58a2902519a3409adecd1c008dd15c 100644
--- a/module/VuFind/src/VuFind/Recommend/WebResults.php
+++ b/module/VuFind/src/VuFind/Recommend/WebResults.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Recommendations
diff --git a/module/VuFind/src/VuFind/Recommend/WorldCatIdentities.php b/module/VuFind/src/VuFind/Recommend/WorldCatIdentities.php
index 0c4b5c7a7746eb22bfe7d7b2fe84299390e89ba1..49f51b835d92196e67ad579401771de67ab82c02 100644
--- a/module/VuFind/src/VuFind/Recommend/WorldCatIdentities.php
+++ b/module/VuFind/src/VuFind/Recommend/WorldCatIdentities.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Recommendations
diff --git a/module/VuFind/src/VuFind/Record/Cache.php b/module/VuFind/src/VuFind/Record/Cache.php
index bfa9a4310dbdbbd0852a75c377adf41568972cdd..154dc0d30b395cdc824712e527b36e8e7182ef93 100644
--- a/module/VuFind/src/VuFind/Record/Cache.php
+++ b/module/VuFind/src/VuFind/Record/Cache.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Record
@@ -237,7 +237,7 @@ class Cache implements \Zend\Log\LoggerAwareInterface
      *
      * @param string $source Record source
      *
-     * @return boolean
+     * @return bool
      */
     public function isCachable($source)
     {
diff --git a/module/VuFind/src/VuFind/Record/Cache/RecordCacheAwareInterface.php b/module/VuFind/src/VuFind/Record/Cache/RecordCacheAwareInterface.php
index e3b27de7b763224f3f1f7f1f0e5984c02d99632b..a68c5d89f54607c3b775937bee964312d830751d 100644
--- a/module/VuFind/src/VuFind/Record/Cache/RecordCacheAwareInterface.php
+++ b/module/VuFind/src/VuFind/Record/Cache/RecordCacheAwareInterface.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Record
diff --git a/module/VuFind/src/VuFind/Record/Cache/RecordCacheAwareTrait.php b/module/VuFind/src/VuFind/Record/Cache/RecordCacheAwareTrait.php
index 7790e13a00bd7214c94785329e6cb3cd0a5dba03..57f3150ead406cabc1771057a5d19f9956c31933 100644
--- a/module/VuFind/src/VuFind/Record/Cache/RecordCacheAwareTrait.php
+++ b/module/VuFind/src/VuFind/Record/Cache/RecordCacheAwareTrait.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Record
diff --git a/module/VuFind/src/VuFind/Record/Loader.php b/module/VuFind/src/VuFind/Record/Loader.php
index 68580fe7e113c9f47697bba5164b99f4e4ec58fc..64aaf7264c0a40bba692c56a821707dc18a369d8 100644
--- a/module/VuFind/src/VuFind/Record/Loader.php
+++ b/module/VuFind/src/VuFind/Record/Loader.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Record
diff --git a/module/VuFind/src/VuFind/Record/Router.php b/module/VuFind/src/VuFind/Record/Router.php
index 972f1aeddd22eb855b7cd8ffb242c210e3c266ee..da967271bf3f3694432d505fd8a000973004404e 100644
--- a/module/VuFind/src/VuFind/Record/Router.php
+++ b/module/VuFind/src/VuFind/Record/Router.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Record
diff --git a/module/VuFind/src/VuFind/RecordDriver/AbstractBase.php b/module/VuFind/src/VuFind/RecordDriver/AbstractBase.php
index 6b071112cdb8d6967e2c59f105d305e0a27a018b..bc04d7bd132eec8df6b97fcef09b0a613a775921 100644
--- a/module/VuFind/src/VuFind/RecordDriver/AbstractBase.php
+++ b/module/VuFind/src/VuFind/RecordDriver/AbstractBase.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  RecordDrivers
diff --git a/module/VuFind/src/VuFind/RecordDriver/EDS.php b/module/VuFind/src/VuFind/RecordDriver/EDS.php
index 88960b9b20a0ea3a08130924cef70efc9e7699c3..c5c2689bd7323c59105cf6b55c4a1fd6e5fa1fe8 100644
--- a/module/VuFind/src/VuFind/RecordDriver/EDS.php
+++ b/module/VuFind/src/VuFind/RecordDriver/EDS.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  RecordDrivers
diff --git a/module/VuFind/src/VuFind/RecordDriver/EIT.php b/module/VuFind/src/VuFind/RecordDriver/EIT.php
index a239ad26e96104177afef4fb6671de50d5aec347..009d4d53f3b6a258a4b3d9043bb40ba4ee92377e 100644
--- a/module/VuFind/src/VuFind/RecordDriver/EIT.php
+++ b/module/VuFind/src/VuFind/RecordDriver/EIT.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  RecordDrivers
diff --git a/module/VuFind/src/VuFind/RecordDriver/Factory.php b/module/VuFind/src/VuFind/RecordDriver/Factory.php
index dd2bb0a48c3c404fdc9482c1822463d4a88d1298..f31badeefcfb274c2a27723349d11aa6f22e05e1 100644
--- a/module/VuFind/src/VuFind/RecordDriver/Factory.php
+++ b/module/VuFind/src/VuFind/RecordDriver/Factory.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  RecordDrivers
diff --git a/module/VuFind/src/VuFind/RecordDriver/LibGuides.php b/module/VuFind/src/VuFind/RecordDriver/LibGuides.php
index 696dd9ddff057c3a4e868917dcccff2d1696a85c..c7cfecc561c461ba1a2bad46a072a576d1835687 100644
--- a/module/VuFind/src/VuFind/RecordDriver/LibGuides.php
+++ b/module/VuFind/src/VuFind/RecordDriver/LibGuides.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  RecordDrivers
diff --git a/module/VuFind/src/VuFind/RecordDriver/Missing.php b/module/VuFind/src/VuFind/RecordDriver/Missing.php
index a7b911ac749300339fbe0ca1a6a38c8c1eadab8e..fb45356f4785fc75a88c808bdd895060c7ca1d71 100644
--- a/module/VuFind/src/VuFind/RecordDriver/Missing.php
+++ b/module/VuFind/src/VuFind/RecordDriver/Missing.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  RecordDrivers
diff --git a/module/VuFind/src/VuFind/RecordDriver/Pazpar2.php b/module/VuFind/src/VuFind/RecordDriver/Pazpar2.php
index cfd52206b8cf28eaeed3f0b157bad733f8e7657a..c630c4b8d8609721f551a5edd1eb60efd4bcf4e1 100644
--- a/module/VuFind/src/VuFind/RecordDriver/Pazpar2.php
+++ b/module/VuFind/src/VuFind/RecordDriver/Pazpar2.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  RecordDrivers
diff --git a/module/VuFind/src/VuFind/RecordDriver/PluginFactory.php b/module/VuFind/src/VuFind/RecordDriver/PluginFactory.php
index 577ceef10ae9f74d275fcf969e2d032af3fdb2ee..f42e7968c4a4fec1b9166b27ce76cfa504841b5f 100644
--- a/module/VuFind/src/VuFind/RecordDriver/PluginFactory.php
+++ b/module/VuFind/src/VuFind/RecordDriver/PluginFactory.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  RecordDrivers
diff --git a/module/VuFind/src/VuFind/RecordDriver/PluginManager.php b/module/VuFind/src/VuFind/RecordDriver/PluginManager.php
index 5d6731a3f61de9b1af24b8c51f24f5763130d596..31a36761fdab9ee1a5db110d18205b9ed2fe075c 100644
--- a/module/VuFind/src/VuFind/RecordDriver/PluginManager.php
+++ b/module/VuFind/src/VuFind/RecordDriver/PluginManager.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  RecordDrivers
diff --git a/module/VuFind/src/VuFind/RecordDriver/Primo.php b/module/VuFind/src/VuFind/RecordDriver/Primo.php
index 6345355eb4da38a4944667d65b4009419cf779b0..3641155241c924bd18b0aa685c379593b3657ba5 100644
--- a/module/VuFind/src/VuFind/RecordDriver/Primo.php
+++ b/module/VuFind/src/VuFind/RecordDriver/Primo.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  RecordDrivers
diff --git a/module/VuFind/src/VuFind/RecordDriver/Response/PublicationDetails.php b/module/VuFind/src/VuFind/RecordDriver/Response/PublicationDetails.php
index 6a0df9ff6af96ce5aa29a7f6cc0922d6ab512cb2..4f3c2bdcfe767f2a99784f4e97aa10e660cf28d6 100644
--- a/module/VuFind/src/VuFind/RecordDriver/Response/PublicationDetails.php
+++ b/module/VuFind/src/VuFind/RecordDriver/Response/PublicationDetails.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  RecordDrivers
diff --git a/module/VuFind/src/VuFind/RecordDriver/SolrAuth.php b/module/VuFind/src/VuFind/RecordDriver/SolrAuth.php
index 82d3bea3fc734d713a630713758bfad52cfdd403..bcce5575dfcd19311fc827917bb9dfb179d1564a 100644
--- a/module/VuFind/src/VuFind/RecordDriver/SolrAuth.php
+++ b/module/VuFind/src/VuFind/RecordDriver/SolrAuth.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  RecordDrivers
diff --git a/module/VuFind/src/VuFind/RecordDriver/SolrDefault.php b/module/VuFind/src/VuFind/RecordDriver/SolrDefault.php
index 10fdca54af42916b9babf0a92a939648405371f7..85ceedb4e016bd1497956d7dcaa1c08085c7822c 100644
--- a/module/VuFind/src/VuFind/RecordDriver/SolrDefault.php
+++ b/module/VuFind/src/VuFind/RecordDriver/SolrDefault.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  RecordDrivers
@@ -1797,14 +1797,14 @@ class SolrDefault extends AbstractBase
     }
 
     /**
-     * Get longitude/latitude text (or false if not available).
+     * Get longitude/latitude values (or empty array if not available).
      *
-     * @return string|bool
+     * @return array
      */
     public function getLongLat()
     {
         return isset($this->fields['long_lat'])
-            ? $this->fields['long_lat'] : false;
+            ? $this->fields['long_lat'] : [];
     }
 
     /**
@@ -1914,4 +1914,37 @@ class SolrDefault extends AbstractBase
             && !empty($this->fields['hierarchy_parent_id'])
             ? $this->fields['hierarchy_parent_id'][0] : '';
     }
+
+    /**
+     * Get the bbox-geo variable.
+     *
+     * @return array
+     */
+    public function getGeoLocation()
+    {
+        return isset($this->fields['location_geo'])
+            ? $this->fields['location_geo'] : [];
+    }
+
+    /**
+     * Get the map display (lat/lon) coordinates
+     *
+     * @return array
+     */
+    public function getDisplayCoordinates()
+    {
+        return isset($this->fields['long_lat_display'])
+            ? $this->fields['long_lat_display'] : [];
+    }
+
+    /**
+     * Get the map display (lat/lon) labels
+     *
+     * @return array
+     */
+    public function getCoordinateLabels()
+    {
+        return isset($this->fields['long_lat_label'])
+            ? $this->fields['long_lat_label'] : [];
+    }
 }
diff --git a/module/VuFind/src/VuFind/RecordDriver/SolrMarc.php b/module/VuFind/src/VuFind/RecordDriver/SolrMarc.php
index bc54a57ec40e60306ae254c7c28820877e519155..9e788f26e8012c7ea77c5c6961ef77e8d58b990f 100644
--- a/module/VuFind/src/VuFind/RecordDriver/SolrMarc.php
+++ b/module/VuFind/src/VuFind/RecordDriver/SolrMarc.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  RecordDrivers
@@ -509,7 +509,7 @@ class SolrMarc extends SolrDefault
         // Loop through all subfields, collecting results that match the whitelist;
         // note that it is important to retain the original MARC order here!
         $allSubfields = $currentField->getSubfields();
-        if (count($allSubfields) > 0) {
+        if (!empty($allSubfields)) {
             foreach ($allSubfields as $currentSubfield) {
                 if (in_array($currentSubfield->getCode(), $subfields)) {
                     // Grab the current subfield value and act on it if it is
@@ -523,7 +523,7 @@ class SolrMarc extends SolrDefault
         }
 
         // Send back the data in a different format depending on $concat mode:
-        return $concat ? [implode($separator, $matches)] : $matches;
+        return $concat && $matches ? [implode($separator, $matches)] : $matches;
     }
 
     /**
diff --git a/module/VuFind/src/VuFind/RecordDriver/SolrMarcRemote.php b/module/VuFind/src/VuFind/RecordDriver/SolrMarcRemote.php
index f28c94c2e582d331a91ac5e88c4d2a05296d1a6b..961bb3ab1b8406cdc08d3ec27f5fbb2b0774c61d 100644
--- a/module/VuFind/src/VuFind/RecordDriver/SolrMarcRemote.php
+++ b/module/VuFind/src/VuFind/RecordDriver/SolrMarcRemote.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  RecordDrivers
diff --git a/module/VuFind/src/VuFind/RecordDriver/SolrReserves.php b/module/VuFind/src/VuFind/RecordDriver/SolrReserves.php
index 377de504573db1d85fa60ed1ae6715adc7eb7790..9aff7940a94583e9e4b94fdc831f5a6d0ded584b 100644
--- a/module/VuFind/src/VuFind/RecordDriver/SolrReserves.php
+++ b/module/VuFind/src/VuFind/RecordDriver/SolrReserves.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  RecordDrivers
diff --git a/module/VuFind/src/VuFind/RecordDriver/SolrWeb.php b/module/VuFind/src/VuFind/RecordDriver/SolrWeb.php
index 1bd543be2119a25de55f914f008447d092b96ecd..57e6a75a98f7cb88e5d9af3e9d704cc628840533 100644
--- a/module/VuFind/src/VuFind/RecordDriver/SolrWeb.php
+++ b/module/VuFind/src/VuFind/RecordDriver/SolrWeb.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  RecordDrivers
diff --git a/module/VuFind/src/VuFind/RecordDriver/Summon.php b/module/VuFind/src/VuFind/RecordDriver/Summon.php
index edd572bc7339b95957285ec9353073d125e5a616..e229322991fa06c132ce99e8cc9ecacf4004e6b8 100644
--- a/module/VuFind/src/VuFind/RecordDriver/Summon.php
+++ b/module/VuFind/src/VuFind/RecordDriver/Summon.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  RecordDrivers
diff --git a/module/VuFind/src/VuFind/RecordDriver/WorldCat.php b/module/VuFind/src/VuFind/RecordDriver/WorldCat.php
index 3614b2ccb038d258b52ebfd624042e0e7a86d6b0..a9d58936a8ccee7ac8c795201f44e0e71269a363 100644
--- a/module/VuFind/src/VuFind/RecordDriver/WorldCat.php
+++ b/module/VuFind/src/VuFind/RecordDriver/WorldCat.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  RecordDrivers
diff --git a/module/VuFind/src/VuFind/RecordTab/AbstractBase.php b/module/VuFind/src/VuFind/RecordTab/AbstractBase.php
index cad28200f320f43fe0833a734f4930059a0dc594..ec17fa281ddd9f11ac14303bd1658704e93f3696 100644
--- a/module/VuFind/src/VuFind/RecordTab/AbstractBase.php
+++ b/module/VuFind/src/VuFind/RecordTab/AbstractBase.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  RecordTabs
diff --git a/module/VuFind/src/VuFind/RecordTab/AbstractContent.php b/module/VuFind/src/VuFind/RecordTab/AbstractContent.php
index e9c7d86a6e70f1fa04c8e8b4d4cdd88f2fcdd4d5..d9f86a45114305970a2aa4df63e81bf15eaad10c 100644
--- a/module/VuFind/src/VuFind/RecordTab/AbstractContent.php
+++ b/module/VuFind/src/VuFind/RecordTab/AbstractContent.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  RecordTabs
diff --git a/module/VuFind/src/VuFind/RecordTab/CollectionHierarchyTree.php b/module/VuFind/src/VuFind/RecordTab/CollectionHierarchyTree.php
index 2a8339af8faaed14c213bdbbbd5b519678b1ba55..6fc8338a5bd32a2c1d6a46870cc66659d2f89028 100644
--- a/module/VuFind/src/VuFind/RecordTab/CollectionHierarchyTree.php
+++ b/module/VuFind/src/VuFind/RecordTab/CollectionHierarchyTree.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  RecordTabs
diff --git a/module/VuFind/src/VuFind/RecordTab/CollectionList.php b/module/VuFind/src/VuFind/RecordTab/CollectionList.php
index 0f91ba75eccde8009af3b8a15e7cc283109fd972..2fc8f4782ed47185c33ab09b59cdf0d6aee30f1a 100644
--- a/module/VuFind/src/VuFind/RecordTab/CollectionList.php
+++ b/module/VuFind/src/VuFind/RecordTab/CollectionList.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  RecordTabs
diff --git a/module/VuFind/src/VuFind/RecordTab/Description.php b/module/VuFind/src/VuFind/RecordTab/Description.php
index fd589b6632fd3661b8bbb14a66fff0d8ae10042d..af5b32e8f13c76a7c5ec4198e6ddd65e3b4718f9 100644
--- a/module/VuFind/src/VuFind/RecordTab/Description.php
+++ b/module/VuFind/src/VuFind/RecordTab/Description.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  RecordTabs
diff --git a/module/VuFind/src/VuFind/RecordTab/Excerpt.php b/module/VuFind/src/VuFind/RecordTab/Excerpt.php
index c7bd0e7514d117e61ed6192521143faa4f3e35f4..2b8008dd4eac365df532659ba9e981baedaa971d 100644
--- a/module/VuFind/src/VuFind/RecordTab/Excerpt.php
+++ b/module/VuFind/src/VuFind/RecordTab/Excerpt.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  RecordTabs
diff --git a/module/VuFind/src/VuFind/RecordTab/Factory.php b/module/VuFind/src/VuFind/RecordTab/Factory.php
index 9288dfb474699b92a90d91d95d4c92724b0354dc..f37187e7cc78f947df08acd900dae1939cb0310d 100644
--- a/module/VuFind/src/VuFind/RecordTab/Factory.php
+++ b/module/VuFind/src/VuFind/RecordTab/Factory.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  RecordDrivers
@@ -177,8 +177,18 @@ class Factory
     public static function getMap(ServiceManager $sm)
     {
         $config = $sm->getServiceLocator()->get('VuFind\Config')->get('config');
-        $enabled = isset($config->Content->recordMap);
-        return new Map($enabled);
+        $mapType = isset($config->Content->recordMap)
+            ? $config->Content->recordMap : null;
+        $options = [];
+        $optionFields = [
+            'displayCoords', 'mapLabels', 'googleMapApiKey'
+        ];
+        foreach ($optionFields as $field) {
+            if (isset($config->Content->$field)) {
+                $options[$field] = $config->Content->$field;
+            }
+        }
+        return new Map($mapType, $options);
     }
 
     /**
@@ -252,6 +262,13 @@ class Factory
     public static function getUserComments(ServiceManager $sm)
     {
         $capabilities = $sm->getServiceLocator()->get('VuFind\AccountCapabilities');
-        return new UserComments('enabled' === $capabilities->getCommentSetting());
+        $config = $sm->getServiceLocator()->get('VuFind\Config')->get('config');
+        $useRecaptcha = isset($config->Captcha) && isset($config->Captcha->forms)
+            && (trim($config->Captcha->forms) === '*'
+            || strpos($config->Captcha->forms, 'userComments'));
+        return new UserComments(
+            'enabled' === $capabilities->getCommentSetting(),
+            $useRecaptcha
+        );
     }
 }
diff --git a/module/VuFind/src/VuFind/RecordTab/HierarchyTree.php b/module/VuFind/src/VuFind/RecordTab/HierarchyTree.php
index eb8aa9a28eca3fe818ab8ce9b90c88a2a5d80992..4b6d3e025e1c4317cce411bb10171aba209d0943 100644
--- a/module/VuFind/src/VuFind/RecordTab/HierarchyTree.php
+++ b/module/VuFind/src/VuFind/RecordTab/HierarchyTree.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  RecordTabs
diff --git a/module/VuFind/src/VuFind/RecordTab/HoldingsILS.php b/module/VuFind/src/VuFind/RecordTab/HoldingsILS.php
index 647f780a629ea8a1642a9c82386387beb70a282a..8670d3bc121c228b613fb76ae64c3dbcd708cd63 100644
--- a/module/VuFind/src/VuFind/RecordTab/HoldingsILS.php
+++ b/module/VuFind/src/VuFind/RecordTab/HoldingsILS.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  RecordTabs
diff --git a/module/VuFind/src/VuFind/RecordTab/HoldingsWorldCat.php b/module/VuFind/src/VuFind/RecordTab/HoldingsWorldCat.php
index 3bcf8e83ba1be945aae5037b836addf00490502a..e346088115e53d86834a67f16a995f97d5ec07f8 100644
--- a/module/VuFind/src/VuFind/RecordTab/HoldingsWorldCat.php
+++ b/module/VuFind/src/VuFind/RecordTab/HoldingsWorldCat.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  RecordTabs
diff --git a/module/VuFind/src/VuFind/RecordTab/Map.php b/module/VuFind/src/VuFind/RecordTab/Map.php
index d8c22526f6f0d0792e0e7e55f2fbde28f3a8df57..58193373d31ceb86a6b7b23e5481fd143bba652a 100644
--- a/module/VuFind/src/VuFind/RecordTab/Map.php
+++ b/module/VuFind/src/VuFind/RecordTab/Map.php
@@ -17,11 +17,12 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  RecordTabs
  * @author   Demian Katz <demian.katz@villanova.edu>
+ * @author   Leila Gonzales <lmg@agiweb.org>
  * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
  * @link     https://vufind.org/wiki/development:plugins:record_tabs Wiki
  */
@@ -33,26 +34,67 @@ namespace VuFind\RecordTab;
  * @category VuFind
  * @package  RecordTabs
  * @author   Demian Katz <demian.katz@villanova.edu>
+ * @author   Leila Gonzales <lmg@agiweb.org>
  * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
  * @link     https://vufind.org/wiki/development:plugins:record_tabs Wiki
  */
 class Map extends AbstractBase
 {
     /**
-     * Is this module enabled in the configuration?
+     * What type of map interface should be used?
+     *
+     * @var string
+     */
+    protected $mapType = null;
+
+    /**
+     * Should we display coordinates as part of labels?
      *
      * @var bool
      */
-    protected $enabled;
+    protected $displayCoords = false;
+
+    /**
+     * Map labels setting from config.ini.
+     *
+     * @var string
+     */
+    protected $mapLabels = null;
+
+    /**
+     * Google Maps API key.
+     *
+     * @var string
+     */
+    protected $googleMapApiKey = null;
 
     /**
      * Constructor
      *
-     * @param bool $enabled Is this module enabled in the configuration?
+     * @param string $mapType Map provider (valid options: 'google' or 'openlayers';
+     * null to disable this feature)
+     * @param array  $options Additional settings
      */
-    public function __construct($enabled = true)
+    public function __construct($mapType = null, $options = [])
     {
-        $this->enabled = $enabled;
+        switch (trim(strtolower($mapType))) {
+        case 'google':
+            // Confirm API key, then fall through to 'openlayers' case for
+            // other standard behavior:
+            if (empty($options['googleMapApiKey'])) {
+                throw new \Exception('Google API key must be set in config.ini');
+            }
+            $this->googleMapApiKey = $options['googleMapApiKey'];
+        case 'openlayers':
+            $this->mapType = trim(strtolower($mapType));
+            $legalOptions = ['displayCoords', 'mapLabels'];
+            foreach ($legalOptions as $option) {
+                if (isset($options[$option])) {
+                    $this->$option = $options[$option];
+                }
+            }
+            break;
+        }
     }
 
     /**
@@ -62,7 +104,7 @@ class Map extends AbstractBase
      */
     public function supportsAjax()
     {
-        // No, Google script magic required
+        // No, magic required
         return false;
     }
 
@@ -76,6 +118,43 @@ class Map extends AbstractBase
         return 'Map View';
     }
 
+    /**
+     * Get the map type for determining template to use.
+     *
+     * @return string
+     */
+    public function getMapType()
+    {
+        return $this->mapType;
+    }
+
+    /**
+     * Get the Google Maps API key.
+     *
+     * @return string
+     */
+    public function getGoogleMapApiKey()
+    {
+        return $this->googleMapApiKey;
+    }
+
+    /**
+     * Is this tab active?
+     *
+     * @return bool
+     */
+    public function isActive()
+    {
+        if ($this->mapType == 'openlayers') {
+            $geocoords = $this->getRecordDriver()->tryMethod('getGeoLocation');
+            return !empty($geocoords);
+        } else if ($this->mapType == 'google') {
+            $longLat = $this->getRecordDriver()->tryMethod('getLongLat');
+            return !empty($longLat);
+        }
+        return false;
+    }
+
     /**
      * Get the JSON needed to display the record on a Google map.
      *
@@ -87,28 +166,166 @@ class Map extends AbstractBase
         if (empty($longLat)) {
             return json_encode([]);
         }
-        $longLat = explode(',', $longLat);
-        $markers = [
-            [
-                'title' => (string) $this->getRecordDriver()->getBreadcrumb(),
-                'lon' => $longLat[0],
-                'lat' => $longLat[1]
-            ]
-        ];
+        $markers = [];
+        $mapDisplayLabels = $this->getMapLabels();
+        foreach ($longLat as $key => $value) {
+            $coordval = explode(',', $value);
+            $label = isset($mapDisplayLabels[$key])
+                ? $mapDisplayLabels[$key] : '';
+            $markers[] = [
+                [
+                    'title' => $label,
+                    'lon' => $coordval[0],
+                    'lat' => $coordval[1]
+                ]
+            ];
+        }
         return json_encode($markers);
     }
 
     /**
-     * Is this tab active?
+     * Get the bbox-geo coordinates.
      *
-     * @return bool
+     * @return array
      */
-    public function isActive()
+    public function getGeoLocationCoords()
     {
-        if (!$this->enabled) {
-            return false;
+        $geoCoords = $this->getRecordDriver()->tryMethod('getGeoLocation');
+        if (empty($geoCoords)) {
+            return [];
         }
-        $longLat = $this->getRecordDriver()->tryMethod('getLongLat');
-        return !empty($longLat);
+        $coordarray = [];
+        /* Extract coordinates from location_geo field */
+        foreach ($geoCoords as $key => $value) {
+            $match = [];
+            if (preg_match('/ENVELOPE\((.*),(.*),(.*),(.*)\)/', $value, $match)) {
+                $lonW = (float)$match[1];
+                $lonE = (float)$match[2];
+                $latN = (float)$match[3];
+                $latS = (float)$match[4];
+                // Display as point or polygon?
+                if (($lonE == $lonW) && ($latN == $latS)) {
+                    $shape = 2;
+                } else {
+                    $shape = 4;
+                }
+                // Coordinates ordered for ol3 display as WSEN
+                array_push($coordarray, [$lonW, $latS, $lonE, $latN, $shape]);
+            }
+        }
+        return $coordarray;
+    }
+
+    /**
+     * Get the map display coordinates.
+     *
+     * @return array
+     */
+    public function getDisplayCoords()
+    {
+        $label_coords = [];
+        $coords = $this->getRecordDriver()->tryMethod('getDisplayCoordinates');
+        foreach ($coords as $val) {
+            $coord = explode(' ', $val);
+            $labelW = $coord[0];
+            $labelE = $coord[1];
+            $labelN = $coord[2];
+            $labelS = $coord[3];
+            /* Create coordinate label for map display */
+            if (($labelW == $labelE) && ($labelN == $labelS)) {
+                $labelcoord = $labelS . ' ' . $labelE;
+            } else {
+                /* Coordinate order is min to max on lat and long axes */
+                $labelcoord = $labelS . ' ' . $labelN . ' ' .
+                $labelW . ' ' . $labelE;
+            }
+            array_push($label_coords, $labelcoord);
+        }
+        return $label_coords;
+    }
+
+    /**
+     * Get the map labels.
+     *
+     * @return array
+     */
+    public function getMapLabels()
+    {
+        $labels = [];
+        $mapLabelData = explode(':', $this->mapLabels);
+        if ($mapLabelData[0] == 'driver') {
+            $labels = $this->getRecordDriver()->tryMethod('getCoordinateLabels');
+            return $labels;
+        }
+        if ($mapLabelData[0] == 'file') {
+            $coords = $this->getRecordDriver()->tryMethod('getDisplayCoordinates');
+            /* read lookup file into array */
+            $label_lookup = [];
+            $file = \VuFind\Config\Locator::getConfigPath($mapLabelData[1]);
+            if (file_exists($file)) {
+                $fp = fopen($file, 'r');
+                while (($line = fgetcsv($fp, 0, "\t")) !== false) {
+                    if (count($line) > 1) {
+                        $label_lookup[$line[0]] = $line[1];
+                    }
+                }
+                fclose($fp);
+            }
+            $labels = [];
+            if (null !== $coords) {
+                foreach ($coords as $val) {
+                    /* Collapse spaces to make combined coordinate string to match
+                        against lookup table coordinate */
+                    $coordmatch = implode('', explode(' ', $val));
+                    /* See if coordinate string matches lookup
+                        table coordinates and if so return label */
+                    $labelname = isset($label_lookup[$coordmatch])
+                        ? $label_lookup[$coordmatch] : '';
+                    array_push($labels, $labelname);
+                }
+            }
+            return $labels;
+        }
+    }
+
+    /**
+     * Construct the map coordinates and labels array.
+     *
+     * @return array
+     */
+    public function getMapTabData()
+    {
+        $geoCoords = $this->getGeoLocationCoords();
+        if (empty($geoCoords)) {
+            return [];
+        }
+        $mapTabData = [];
+        $mapDisplayCoords = [];
+        $mapDisplayLabels = [];
+        if ($this->displayCoords) {
+             $mapDisplayCoords = $this->getDisplayCoords();
+        }
+        if (isset($this->mapLabels)) {
+            $mapDisplayLabels = $this->getMapLabels();
+        }
+        // Pass coordinates, display coordinates, and labels
+        foreach ($geoCoords as $key => $value) {
+            $mapCoords = '';
+            $mapLabel = '';
+            if ($this->displayCoords) {
+                $mapCoords = $mapDisplayCoords[$key];
+            }
+            if (isset($this->mapLabels)) {
+                $mapLabel = $mapDisplayLabels[$key];
+            }
+            array_push(
+                $mapTabData, [
+                    $geoCoords[$key][0], $geoCoords[$key][1],
+                    $geoCoords[$key][2], $geoCoords[$key][3],
+                    $geoCoords[$key][4], $mapLabel, $mapCoords
+                    ]
+            );
+        }
+        return $mapTabData;
     }
 }
diff --git a/module/VuFind/src/VuFind/RecordTab/PluginFactory.php b/module/VuFind/src/VuFind/RecordTab/PluginFactory.php
index 54dd120f883ac9f2656dd20c63ac0403ac5f1eab..9029d442cd2e89f2860d70bc4b9d86b23546d962 100644
--- a/module/VuFind/src/VuFind/RecordTab/PluginFactory.php
+++ b/module/VuFind/src/VuFind/RecordTab/PluginFactory.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  RecordTabs
diff --git a/module/VuFind/src/VuFind/RecordTab/PluginManager.php b/module/VuFind/src/VuFind/RecordTab/PluginManager.php
index 950022ca459e40376b84708393eb1f436a702afd..28792dafdbfb55e994fe7a4ec3914ecbced62863 100644
--- a/module/VuFind/src/VuFind/RecordTab/PluginManager.php
+++ b/module/VuFind/src/VuFind/RecordTab/PluginManager.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  RecordTabs
@@ -95,6 +95,21 @@ class PluginManager extends \VuFind\ServiceManager\AbstractPluginManager
         return $this->getConfigByClass($driver, $config, 'tabs', []);
     }
 
+    /**
+     * Get an array of tabs names configured to load via AJAX in the background
+     *
+     * @param AbstractRecordDriver $driver Record driver
+     * @param array                $config Tab configuration (associative array
+     * including 'tabs' array mapping driver class => tab service name)
+     *
+     * @return array
+     */
+    public function getBackgroundTabNames(AbstractRecordDriver $driver,
+        array $config
+    ) {
+        return $this->getConfigByClass($driver, $config, 'backgroundLoadedTabs', []);
+    }
+
     /**
      * Get a default tab by looking up the provided record driver in the tab
      * configuration array.
diff --git a/module/VuFind/src/VuFind/RecordTab/Preview.php b/module/VuFind/src/VuFind/RecordTab/Preview.php
index 4b5a5a7feb4366bb1e2029e90949355fb82f31ea..d9b6d9ce78ce7ef824df8e9834e4ed639574cceb 100644
--- a/module/VuFind/src/VuFind/RecordTab/Preview.php
+++ b/module/VuFind/src/VuFind/RecordTab/Preview.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  RecordTabs
diff --git a/module/VuFind/src/VuFind/RecordTab/Reviews.php b/module/VuFind/src/VuFind/RecordTab/Reviews.php
index c67530ce869f5701574cfdbe9577b1e7bf6b4728..66176a13a3b5464dd2c1e67edf57097c5c58fc9d 100644
--- a/module/VuFind/src/VuFind/RecordTab/Reviews.php
+++ b/module/VuFind/src/VuFind/RecordTab/Reviews.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  RecordTabs
diff --git a/module/VuFind/src/VuFind/RecordTab/SimilarItemsCarousel.php b/module/VuFind/src/VuFind/RecordTab/SimilarItemsCarousel.php
index daeb53d76a3ac1d694cc102a2a5044a737f2f77c..4ddf208106685bdb9e5416f4c12df765d636f6d7 100644
--- a/module/VuFind/src/VuFind/RecordTab/SimilarItemsCarousel.php
+++ b/module/VuFind/src/VuFind/RecordTab/SimilarItemsCarousel.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  RecordTabs
diff --git a/module/VuFind/src/VuFind/RecordTab/StaffViewArray.php b/module/VuFind/src/VuFind/RecordTab/StaffViewArray.php
index 43461195813da69bdb7bc425a2337f83aba74f5c..b87c5168459b70482e025092cd9e21a52fcd104d 100644
--- a/module/VuFind/src/VuFind/RecordTab/StaffViewArray.php
+++ b/module/VuFind/src/VuFind/RecordTab/StaffViewArray.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  RecordTabs
diff --git a/module/VuFind/src/VuFind/RecordTab/StaffViewMARC.php b/module/VuFind/src/VuFind/RecordTab/StaffViewMARC.php
index 7c69033341f360005dd38e646dfbf82b49f66908..8b3b5c0d1376e1d3f6a3993da011d8116d6672bc 100644
--- a/module/VuFind/src/VuFind/RecordTab/StaffViewMARC.php
+++ b/module/VuFind/src/VuFind/RecordTab/StaffViewMARC.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  RecordTabs
diff --git a/module/VuFind/src/VuFind/RecordTab/TOC.php b/module/VuFind/src/VuFind/RecordTab/TOC.php
index 00e4f1b53de79239329138460fdfc26face1e943..76bc7621b3161382768746b8a27de8f03ee95ed6 100644
--- a/module/VuFind/src/VuFind/RecordTab/TOC.php
+++ b/module/VuFind/src/VuFind/RecordTab/TOC.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  RecordTabs
diff --git a/module/VuFind/src/VuFind/RecordTab/TabInterface.php b/module/VuFind/src/VuFind/RecordTab/TabInterface.php
index 25ad1a96a7ec083116a142e9a7118265e68c8fad..08edaebcb43f8626c065c64d32aebebacea4886c 100644
--- a/module/VuFind/src/VuFind/RecordTab/TabInterface.php
+++ b/module/VuFind/src/VuFind/RecordTab/TabInterface.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  RecordTabs
diff --git a/module/VuFind/src/VuFind/RecordTab/UserComments.php b/module/VuFind/src/VuFind/RecordTab/UserComments.php
index f9d47d6cdb3fc5002a5a63a2ae2f59c340ed2a27..8921c63e8d59b0e1b32ff5dda2efbf8693a22c05 100644
--- a/module/VuFind/src/VuFind/RecordTab/UserComments.php
+++ b/module/VuFind/src/VuFind/RecordTab/UserComments.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  RecordTabs
@@ -45,14 +45,33 @@ class UserComments extends AbstractBase
      */
     protected $enabled;
 
+    /**
+     * Should we use ReCaptcha?
+     *
+     * @var bool
+     */
+    protected $useRecaptcha;
+
     /**
      * Constructor
      *
      * @param bool $enabled is this tab enabled?
+     * @param bool $urc     use recaptcha?
      */
-    public function __construct($enabled = true)
+    public function __construct($enabled = true, $urc = false)
     {
         $this->enabled = $enabled;
+        $this->useRecaptcha = $urc;
+    }
+
+    /**
+     * Is Recaptcha active?
+     *
+     * @return bool
+     */
+    public function isRecaptchaActive()
+    {
+        return $this->useRecaptcha;
     }
 
     /**
diff --git a/module/VuFind/src/VuFind/Related/Deprecated.php b/module/VuFind/src/VuFind/Related/Deprecated.php
index 1abe90fc85a8709c15bc0a8c72a19ed806693fab..5a98193fde57fa2eb413c305ba98e9f57a0de9d6 100644
--- a/module/VuFind/src/VuFind/Related/Deprecated.php
+++ b/module/VuFind/src/VuFind/Related/Deprecated.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Related_Records
diff --git a/module/VuFind/src/VuFind/Related/Factory.php b/module/VuFind/src/VuFind/Related/Factory.php
index 9d81d43a4acf9826308380e698fec08e61f0fcfb..f09d4e5816fdc3fee7f019f47d23d79e10c495a9 100644
--- a/module/VuFind/src/VuFind/Related/Factory.php
+++ b/module/VuFind/src/VuFind/Related/Factory.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Related_Records
diff --git a/module/VuFind/src/VuFind/Related/PluginFactory.php b/module/VuFind/src/VuFind/Related/PluginFactory.php
index a5798235ea37848ff39a3448b580d581ad71fc49..f31afdb99d67eaed3be3b4e46b42e0660cfaadbf 100644
--- a/module/VuFind/src/VuFind/Related/PluginFactory.php
+++ b/module/VuFind/src/VuFind/Related/PluginFactory.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Related_Records
diff --git a/module/VuFind/src/VuFind/Related/PluginManager.php b/module/VuFind/src/VuFind/Related/PluginManager.php
index 72f3835f7806a48004c939df52e1f9e607ee10ae..f57d380fabdb2593e6c1bac70a15cb627c2eab83 100644
--- a/module/VuFind/src/VuFind/Related/PluginManager.php
+++ b/module/VuFind/src/VuFind/Related/PluginManager.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Related_Records
diff --git a/module/VuFind/src/VuFind/Related/RelatedInterface.php b/module/VuFind/src/VuFind/Related/RelatedInterface.php
index 6bae160f7ab1f00ab3795836f11d9f3887439c82..29b98ad1d6a70100db1dacd76515e2bd170b8c1b 100644
--- a/module/VuFind/src/VuFind/Related/RelatedInterface.php
+++ b/module/VuFind/src/VuFind/Related/RelatedInterface.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Related_Records
diff --git a/module/VuFind/src/VuFind/Related/Similar.php b/module/VuFind/src/VuFind/Related/Similar.php
index 69ce81f014b72464824b6364731f2cb2b0499ebd..3751134007513d6fc3bede0cf77e8374bf3b577a 100644
--- a/module/VuFind/src/VuFind/Related/Similar.php
+++ b/module/VuFind/src/VuFind/Related/Similar.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Related_Records
diff --git a/module/VuFind/src/VuFind/Related/WorldCatSimilar.php b/module/VuFind/src/VuFind/Related/WorldCatSimilar.php
index 2e21321db40902f729ff9af689cc4e39f2782cbd..13d07a47f2b84dc3154fa39123b7e4fdd8f2691a 100644
--- a/module/VuFind/src/VuFind/Related/WorldCatSimilar.php
+++ b/module/VuFind/src/VuFind/Related/WorldCatSimilar.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Related_Records
diff --git a/module/VuFind/src/VuFind/Reserves/CsvReader.php b/module/VuFind/src/VuFind/Reserves/CsvReader.php
index 95c809b483f5b6fd07e2a8193e31a9e146d4a45e..00e6ebd8efdd0e2d15a3d5b133c259ef5479e3dc 100644
--- a/module/VuFind/src/VuFind/Reserves/CsvReader.php
+++ b/module/VuFind/src/VuFind/Reserves/CsvReader.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Reserves
diff --git a/module/VuFind/src/VuFind/Resolver/Connection.php b/module/VuFind/src/VuFind/Resolver/Connection.php
index 34c78f66a3250357eec26c46e1ffc18869de9ad3..609f7c5b91228508cc9f497b74f8316c0068fac8 100644
--- a/module/VuFind/src/VuFind/Resolver/Connection.php
+++ b/module/VuFind/src/VuFind/Resolver/Connection.php
@@ -20,7 +20,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Resolver_Drivers
diff --git a/module/VuFind/src/VuFind/Resolver/Driver/Demo.php b/module/VuFind/src/VuFind/Resolver/Driver/Demo.php
index 2b4c0a101c949c0feeb4ea54413b43c6423861cf..43ce20ba4289a8803ebe6d80dedf574ba9f5b3e4 100644
--- a/module/VuFind/src/VuFind/Resolver/Driver/Demo.php
+++ b/module/VuFind/src/VuFind/Resolver/Driver/Demo.php
@@ -19,7 +19,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Resolver_Drivers
diff --git a/module/VuFind/src/VuFind/Resolver/Driver/DriverInterface.php b/module/VuFind/src/VuFind/Resolver/Driver/DriverInterface.php
index fb73f629308b0e2c832397c867ca6c58589618b4..7f243ec690dd72f2613330a38e9fe99ecf09e8f8 100644
--- a/module/VuFind/src/VuFind/Resolver/Driver/DriverInterface.php
+++ b/module/VuFind/src/VuFind/Resolver/Driver/DriverInterface.php
@@ -20,7 +20,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Resolver_Drivers
diff --git a/module/VuFind/src/VuFind/Resolver/Driver/Ezb.php b/module/VuFind/src/VuFind/Resolver/Driver/Ezb.php
index c8a5fc618dd13bf4a306b68c78d482bd50e529af..ad375be428e1113af9ec42cda196c132ad84e786 100644
--- a/module/VuFind/src/VuFind/Resolver/Driver/Ezb.php
+++ b/module/VuFind/src/VuFind/Resolver/Driver/Ezb.php
@@ -22,7 +22,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Resolver_Drivers
diff --git a/module/VuFind/src/VuFind/Resolver/Driver/Factory.php b/module/VuFind/src/VuFind/Resolver/Driver/Factory.php
index 913ab79280a656f85e3565d88e65212afbc6516e..1293fa49d3fd578930bca4b13484916dd2d06a7e 100644
--- a/module/VuFind/src/VuFind/Resolver/Driver/Factory.php
+++ b/module/VuFind/src/VuFind/Resolver/Driver/Factory.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Resolver_Drivers
diff --git a/module/VuFind/src/VuFind/Resolver/Driver/PluginFactory.php b/module/VuFind/src/VuFind/Resolver/Driver/PluginFactory.php
index 86a9b33dff8b25098078c6f8266c3f37d82536ce..1fc12e15c37fc1f50cfa9a9f9ec8b9d13953e2ad 100644
--- a/module/VuFind/src/VuFind/Resolver/Driver/PluginFactory.php
+++ b/module/VuFind/src/VuFind/Resolver/Driver/PluginFactory.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Resolver_Drivers
diff --git a/module/VuFind/src/VuFind/Resolver/Driver/PluginManager.php b/module/VuFind/src/VuFind/Resolver/Driver/PluginManager.php
index b79a3401de206bd50a85c4db3524b5a4bba1417d..a003c23f6d3b42392f785d4afd2a6094d04637fd 100644
--- a/module/VuFind/src/VuFind/Resolver/Driver/PluginManager.php
+++ b/module/VuFind/src/VuFind/Resolver/Driver/PluginManager.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Resolver_Drivers
diff --git a/module/VuFind/src/VuFind/Resolver/Driver/Redi.php b/module/VuFind/src/VuFind/Resolver/Driver/Redi.php
index 89dfc5ffedac9ba0ce3e94e1eb56c5a1f340adc4..7d90616ac5e27bf0f5ab476c0ee343ea4278ff8c 100644
--- a/module/VuFind/src/VuFind/Resolver/Driver/Redi.php
+++ b/module/VuFind/src/VuFind/Resolver/Driver/Redi.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Resolver_Drivers
diff --git a/module/VuFind/src/VuFind/Resolver/Driver/Sfx.php b/module/VuFind/src/VuFind/Resolver/Driver/Sfx.php
index d5aaeaffdc124eedc302fec5b10974b1ad5d4e80..d2063132907bc821060ce7a0793b2b532369ab14 100644
--- a/module/VuFind/src/VuFind/Resolver/Driver/Sfx.php
+++ b/module/VuFind/src/VuFind/Resolver/Driver/Sfx.php
@@ -20,7 +20,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Resolver_Drivers
diff --git a/module/VuFind/src/VuFind/Resolver/Driver/Threesixtylink.php b/module/VuFind/src/VuFind/Resolver/Driver/Threesixtylink.php
index 45c1d51b04c45f577ac97ccf25e610d87d58126a..791dd6ba5e8ae91cec61734a43e3bed8f83d4665 100644
--- a/module/VuFind/src/VuFind/Resolver/Driver/Threesixtylink.php
+++ b/module/VuFind/src/VuFind/Resolver/Driver/Threesixtylink.php
@@ -19,7 +19,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Resolver_Drivers
diff --git a/module/VuFind/src/VuFind/Role/DynamicRoleProvider.php b/module/VuFind/src/VuFind/Role/DynamicRoleProvider.php
index a389f54fed4937461bc1c171275d22d50e2f94db..510451e646101ed0a53ddda6514e898857729bce 100644
--- a/module/VuFind/src/VuFind/Role/DynamicRoleProvider.php
+++ b/module/VuFind/src/VuFind/Role/DynamicRoleProvider.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Authorization
diff --git a/module/VuFind/src/VuFind/Role/DynamicRoleProviderFactory.php b/module/VuFind/src/VuFind/Role/DynamicRoleProviderFactory.php
index ffa17f280ef563ee00205c6b9306c18c44460768..7f03083eac55ce3917d6aaf3e7b95f098c15578d 100644
--- a/module/VuFind/src/VuFind/Role/DynamicRoleProviderFactory.php
+++ b/module/VuFind/src/VuFind/Role/DynamicRoleProviderFactory.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Authorization
diff --git a/module/VuFind/src/VuFind/Role/PermissionProvider/Factory.php b/module/VuFind/src/VuFind/Role/PermissionProvider/Factory.php
index f7aa828055acce82108b5c5232bc23d7402d206c..7a73d0db4e10fd511c9a52a22dbf8aa10ee9e347 100644
--- a/module/VuFind/src/VuFind/Role/PermissionProvider/Factory.php
+++ b/module/VuFind/src/VuFind/Role/PermissionProvider/Factory.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Authorization
@@ -108,4 +108,18 @@ class Factory
             $sm->getServiceLocator()->get('ZfcRbac\Service\AuthorizationService')
         );
     }
+    
+    /**
+     * Factory for User
+     *
+     * @param ServiceManager $sm Service manager.
+     *
+     * @return User
+     */
+    public static function getUser(ServiceManager $sm)
+    {
+        return new User(
+            $sm->getServiceLocator()->get('ZfcRbac\Service\AuthorizationService')
+        );
+    }
 }
diff --git a/module/VuFind/src/VuFind/Role/PermissionProvider/IpRange.php b/module/VuFind/src/VuFind/Role/PermissionProvider/IpRange.php
index d63834ea9b947e50928cbbed4e3f286c940032da..27c2bb9bc048b62f0b67ac8a9137747305e1ac3c 100644
--- a/module/VuFind/src/VuFind/Role/PermissionProvider/IpRange.php
+++ b/module/VuFind/src/VuFind/Role/PermissionProvider/IpRange.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Authorization
diff --git a/module/VuFind/src/VuFind/Role/PermissionProvider/IpRegEx.php b/module/VuFind/src/VuFind/Role/PermissionProvider/IpRegEx.php
index 595c08b6995439f684f9107db1cea1410164e21d..a6550021cbaf54fa13e44ab38b4ed588eb7dc0df 100644
--- a/module/VuFind/src/VuFind/Role/PermissionProvider/IpRegEx.php
+++ b/module/VuFind/src/VuFind/Role/PermissionProvider/IpRegEx.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Authorization
diff --git a/module/VuFind/src/VuFind/Role/PermissionProvider/PermissionProviderInterface.php b/module/VuFind/src/VuFind/Role/PermissionProvider/PermissionProviderInterface.php
index 797b2411ef2b88f96ecb067fa2b5b92714bd0938..72ae133737b7ac8edfecd3ec0ae31e97edb15761 100644
--- a/module/VuFind/src/VuFind/Role/PermissionProvider/PermissionProviderInterface.php
+++ b/module/VuFind/src/VuFind/Role/PermissionProvider/PermissionProviderInterface.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Authorization
diff --git a/module/VuFind/src/VuFind/Role/PermissionProvider/PluginManager.php b/module/VuFind/src/VuFind/Role/PermissionProvider/PluginManager.php
index d79d4793ae80e05d20ad673e3ff4d1c0dc68d2cb..499b0c414eac1589a1cc4a84196a3c5bd4c2d891 100644
--- a/module/VuFind/src/VuFind/Role/PermissionProvider/PluginManager.php
+++ b/module/VuFind/src/VuFind/Role/PermissionProvider/PluginManager.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Authorization
diff --git a/module/VuFind/src/VuFind/Role/PermissionProvider/Role.php b/module/VuFind/src/VuFind/Role/PermissionProvider/Role.php
index 6cd8fce0d667e19a3154611402ab34a11b8e7cd7..9c817e4ff71d87f90dccadc4fafb31da80b9a8cc 100644
--- a/module/VuFind/src/VuFind/Role/PermissionProvider/Role.php
+++ b/module/VuFind/src/VuFind/Role/PermissionProvider/Role.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Authorization
diff --git a/module/VuFind/src/VuFind/Role/PermissionProvider/ServerParam.php b/module/VuFind/src/VuFind/Role/PermissionProvider/ServerParam.php
index da2ef78600e12a16e35f48a028c191aac54424a4..b8c7cfc1b9dff0b75b0e622eb9e4bbd27260136c 100644
--- a/module/VuFind/src/VuFind/Role/PermissionProvider/ServerParam.php
+++ b/module/VuFind/src/VuFind/Role/PermissionProvider/ServerParam.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Authorization
@@ -111,7 +111,7 @@ class ServerParam implements PermissionProviderInterface,
      *
      * @param string $option Option
      *
-     * @return boolean true if a server param matches, false if not
+     * @return bool true if a server param matches, false if not
      */
     protected function checkServerParam($option)
     {
diff --git a/module/VuFind/src/VuFind/Role/PermissionProvider/Shibboleth.php b/module/VuFind/src/VuFind/Role/PermissionProvider/Shibboleth.php
index 2291403385c048fb2f163e9c47f814e0e2db95ee..9cb677aaf33b17b65a97028622693db6c528778f 100644
--- a/module/VuFind/src/VuFind/Role/PermissionProvider/Shibboleth.php
+++ b/module/VuFind/src/VuFind/Role/PermissionProvider/Shibboleth.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Authorization
diff --git a/module/VuFind/src/VuFind/Role/PermissionProvider/User.php b/module/VuFind/src/VuFind/Role/PermissionProvider/User.php
new file mode 100644
index 0000000000000000000000000000000000000000..2e8fa94246b537d103e1bb37f83d16cb825f03f2
--- /dev/null
+++ b/module/VuFind/src/VuFind/Role/PermissionProvider/User.php
@@ -0,0 +1,103 @@
+<?php
+/**
+ * User permission provider for VuFind.
+ *
+ * PHP version 5
+ *
+ * Copyright (C) Villanova University 2007.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ * @category VuFind2
+ * @package  Authorization
+ * @author   Markus Beh <markus.beh@ub.uni-freiburg.de>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     http://www.vufind.org  Main Page
+ */
+namespace VuFind\Role\PermissionProvider;
+use ZfcRbac\Service\AuthorizationService;
+
+/**
+ * LDAP permission provider for VuFind.
+ * based on permission provider Username.php
+ *
+ * @category VuFind2
+ * @package  Authorization
+ * @author   Demian Katz <demian.katz@villanova.edu>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     http://www.vufind.org  Main Page
+ */
+class User implements PermissionProviderInterface,
+    \Zend\Log\LoggerAwareInterface
+{
+    use \VuFind\Log\LoggerAwareTrait;
+
+    /**
+     * Authorization object
+     *
+     * @var AuthorizationService
+     */
+    protected $auth;
+
+    /**
+     * Constructor
+     *
+     * @param AuthorizationService $authorization Authorization service
+     */
+    public function __construct(AuthorizationService $authorization)
+    {
+        $this->auth = $authorization;
+    }
+
+    /**
+     * Return an array of roles which may be granted the permission based on
+     * the options.
+     *
+     * @param mixed $options Options provided from configuration.
+     *
+     * @return array
+     */
+    public function getPermissions($options)
+    {
+        // If no user is logged in, or the user doesn't match the passed-in
+        // whitelist, we can't grant the permission to any roles.
+        if (!($user = $this->auth->getIdentity())) {
+            return [];
+        }
+
+        // which user attribute has to match which pattern to get permissions?
+        $criteria = [];
+        foreach ((array)$options as $option) {
+            $parts = explode(' ', $option, 2);
+            if (count($parts) < 2) {
+                $this->logError("configuration option '{$option}' invalid");
+                return [];
+            } else {
+                list($attribute, $pattern) = $parts;
+
+                // check user attribute values against the pattern
+                if (! preg_match('/^\/.*\/$/', $pattern)) {
+                    $pattern = '/' . $pattern . '/';
+                }
+
+                if (preg_match($pattern, $user[$attribute])) {
+                    return ['loggedin'];
+                }
+            }
+        }
+
+        //no matches found, so the user don't get any permissions
+        return [];
+    }
+}
diff --git a/module/VuFind/src/VuFind/Role/PermissionProvider/Username.php b/module/VuFind/src/VuFind/Role/PermissionProvider/Username.php
index 7f762f5c9c90c0637d83cb23b074776640a9430c..6021c12083e124ad2fd2e82d14b68a92668fa803 100644
--- a/module/VuFind/src/VuFind/Role/PermissionProvider/Username.php
+++ b/module/VuFind/src/VuFind/Role/PermissionProvider/Username.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Authorization
diff --git a/module/VuFind/src/VuFind/Route/RouteGenerator.php b/module/VuFind/src/VuFind/Route/RouteGenerator.php
index 5071b017c00c458e5aa72e420017367e40be8255..9d857a378af85ac4e5ca96014b0b3f5a2e5e82a2 100644
--- a/module/VuFind/src/VuFind/Route/RouteGenerator.php
+++ b/module/VuFind/src/VuFind/Route/RouteGenerator.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Route
diff --git a/module/VuFind/src/VuFind/SMS/AbstractBase.php b/module/VuFind/src/VuFind/SMS/AbstractBase.php
index eae5f4954c51d0221da923caee4c85b12e134fa1..c8e72036650a0af8c6f2030d5da08656e65c42f7 100644
--- a/module/VuFind/src/VuFind/SMS/AbstractBase.php
+++ b/module/VuFind/src/VuFind/SMS/AbstractBase.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  SMS
diff --git a/module/VuFind/src/VuFind/SMS/Clickatell.php b/module/VuFind/src/VuFind/SMS/Clickatell.php
index d528d9366becad4d06301b0fbe1552e6e9282e55..a2f43afa7cfae1cd80b9568f7496d367451aaff3 100644
--- a/module/VuFind/src/VuFind/SMS/Clickatell.php
+++ b/module/VuFind/src/VuFind/SMS/Clickatell.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  SMS
diff --git a/module/VuFind/src/VuFind/SMS/Factory.php b/module/VuFind/src/VuFind/SMS/Factory.php
index d8dee0ee31e0ae2ad7d50dfaf4dd3f7018952ebe..9d8975e51550f65463255ff294e22e1841f19823 100644
--- a/module/VuFind/src/VuFind/SMS/Factory.php
+++ b/module/VuFind/src/VuFind/SMS/Factory.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  SMS
diff --git a/module/VuFind/src/VuFind/SMS/Mailer.php b/module/VuFind/src/VuFind/SMS/Mailer.php
index 7f7640d601afcb9dff33f75149e24ebd3d65ba3e..8c26f39d135c692d15233d8bfc139e7a1b2b3099 100644
--- a/module/VuFind/src/VuFind/SMS/Mailer.php
+++ b/module/VuFind/src/VuFind/SMS/Mailer.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  SMS
diff --git a/module/VuFind/src/VuFind/SMS/SMSInterface.php b/module/VuFind/src/VuFind/SMS/SMSInterface.php
index 7a76acc034dcb3974ba2a72cb24de92783df9b93..5aeaee3c1eb03121af4b04d543ee4622086b7ca0 100644
--- a/module/VuFind/src/VuFind/SMS/SMSInterface.php
+++ b/module/VuFind/src/VuFind/SMS/SMSInterface.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  SMS
diff --git a/module/VuFind/src/VuFind/Search/BackendManager.php b/module/VuFind/src/VuFind/Search/BackendManager.php
index 5ea478fe9f0ba0c3ef619ff377db157c0860d4bf..ba350513ca1e0e360423d859e70b6810cdb61dcb 100644
--- a/module/VuFind/src/VuFind/Search/BackendManager.php
+++ b/module/VuFind/src/VuFind/Search/BackendManager.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
@@ -126,7 +126,7 @@ class BackendManager implements SharedListenerAggregateInterface
      *
      * @param string $name Backend name
      *
-     * @return boolean
+     * @return bool
      */
     public function has($name)
     {
diff --git a/module/VuFind/src/VuFind/Search/Base/Options.php b/module/VuFind/src/VuFind/Search/Base/Options.php
index e9a15f310bb0f6305e113aaca41b5e8f0cec3a32..468106e7f0da218d510c29d6befe9a0d30652e49 100644
--- a/module/VuFind/src/VuFind/Search/Base/Options.php
+++ b/module/VuFind/src/VuFind/Search/Base/Options.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search_Base
@@ -50,6 +50,13 @@ abstract class Options implements TranslatorAwareInterface
      */
     protected $sortOptions = [];
 
+    /**
+     * Available sort options for facets
+     *
+     * @var array
+     */
+    protected $facetSortOptions = [];
+
     /**
      * Overall default sort option
      *
@@ -232,6 +239,13 @@ abstract class Options implements TranslatorAwareInterface
      */
     protected $facetsIni = 'facets';
 
+    /**
+     * Active list view option (see [List] in searches.ini).
+     *
+     * @var string
+     */
+    protected $listviewOption = "full";
+
     /**
      * Configuration loader
      *
@@ -246,6 +260,13 @@ abstract class Options implements TranslatorAwareInterface
      */
     protected $resultLimit = -1;
 
+    /**
+     * Is the first/last navigation scroller enabled?
+     *
+     * @var bool
+     */
+    protected $firstlastNavigation = false;
+
     /**
      * Constructor
      *
@@ -424,6 +445,16 @@ abstract class Options implements TranslatorAwareInterface
         return $this->sortOptions;
     }
 
+    /**
+     * Get an array of sort options for facets.
+     *
+     * @return array
+     */
+    public function getFacetSortOptions()
+    {
+        return $this->facetSortOptions;
+    }
+
     /**
      * Get the default sort option for the specified search handler.
      *
@@ -659,6 +690,16 @@ abstract class Options implements TranslatorAwareInterface
         return $this->autocompleteEnabled;
     }
 
+    /**
+     * Get a string of the listviewOption (full or tab).
+     *
+     * @return string
+     */
+    public function getListViewOption()
+    {
+        return $this->listviewOption;
+    }
+
     /**
      * Return the route name for the search results action.
      *
@@ -691,6 +732,17 @@ abstract class Options implements TranslatorAwareInterface
         return false;
     }
 
+    /**
+     * Return the route name for the facet list action. Returns false to cover
+     * unimplemented support.
+     *
+     * @return string|bool
+     */
+    public function getFacetListAction()
+    {
+        return false;
+    }
+
     /**
      * Does this search option support the cart/book bag?
      *
@@ -856,4 +908,14 @@ abstract class Options implements TranslatorAwareInterface
         $vars = array_keys($vars);
         return $vars;
     }
+
+    /**
+     * Should we include first/last options in result scroller navigation?
+     *
+     * @return bool
+     */
+    public function supportsFirstLastNavigation()
+    {
+        return $this->firstlastNavigation;
+    }
 }
diff --git a/module/VuFind/src/VuFind/Search/Base/Params.php b/module/VuFind/src/VuFind/Search/Base/Params.php
index f3e1ae91e19420b7dd51c5973fc63f606c7341f5..35b5e6954f9c1cfccfab1fd817c4a1ade77655c5 100644
--- a/module/VuFind/src/VuFind/Search/Base/Params.php
+++ b/module/VuFind/src/VuFind/Search/Base/Params.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search_Base
@@ -367,7 +367,7 @@ class Params implements ServiceLocatorAwareInterface
      * @param \Zend\StdLib\Parameters $request Parameter object representing user
      * request.
      *
-     * @return boolean True if search settings were found, false if not.
+     * @return bool True if search settings were found, false if not.
      */
     protected function initBasicSearch($request)
     {
@@ -1687,15 +1687,20 @@ class Params implements ServiceLocatorAwareInterface
     }
 
     /**
-     * Translate a string if a translator is available.
+     * Translate a string (or string-castable object)
      *
-     * @param string $msg Message to translate
+     * @param string|object|array $target  String to translate or an array of text
+     * domain and string to translate
+     * @param array               $tokens  Tokens to inject into the translated
+     * string
+     * @param string              $default Default value to use if no translation is
+     * found (null for no default).
      *
      * @return string
      */
-    public function translate($msg)
+    public function translate($target, $tokens = [], $default = null)
     {
-        return $this->getOptions()->translate($msg);
+        return $this->getOptions()->translate($target, $tokens, $default);
     }
 
     /**
diff --git a/module/VuFind/src/VuFind/Search/Base/Results.php b/module/VuFind/src/VuFind/Search/Base/Results.php
index f693db99242dd278de2a91a097680eb632aa4a2f..a2b917e5586f11c103378edddb552800a7b80549 100644
--- a/module/VuFind/src/VuFind/Search/Base/Results.php
+++ b/module/VuFind/src/VuFind/Search/Base/Results.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search_Base
@@ -621,4 +621,52 @@ abstract class Results implements ServiceLocatorAwareInterface
             [$this->getOptions(), 'translate'], func_get_args()
         );
     }
+
+    /**
+     * Get complete facet counts for several index fields
+     *
+     * @param array  $facetfields  name of the Solr fields to return facets for
+     * @param bool   $removeFilter Clear existing filters from selected fields (true)
+     * or retain them (false)?
+     * @param int    $limit        A limit for the number of facets returned, this
+     * may be useful for very large amounts of facets that can break the JSON parse
+     * method because of PHP out of memory exceptions (default = -1, no limit).
+     * @param string $facetSort    A facet sort value to use (null to retain current)
+     *
+     * @return array an array with the facet values for each index field
+     */
+    public function getFullFieldFacets($facetfields, $removeFilter = true,
+        $limit = -1, $facetSort = null
+    ) {
+        if (!method_exists($this, 'getPartialFieldFacets')) {
+            throw new \Exception('getPartialFieldFacets not implemented');
+        }
+        $page = 1;
+        $facets = [];
+        do {
+            $facetpage = $this->getPartialFieldFacets(
+                $facetfields, $removeFilter, $limit, $facetSort, $page
+            );
+            $nextfields = [];
+            foreach ($facetfields as $field) {
+                if (!empty($facetpage[$field]['data']['list'])) {
+                    if (!isset($facets[$field])) {
+                        $facets[$field] = $facetpage[$field];
+                        $facets[$field]['more'] = false;
+                    } else {
+                        $facets[$field]['data']['list'] = array_merge(
+                            $facets[$field]['data']['list'],
+                            $facetpage[$field]['data']['list']
+                        );
+                    }
+                    if ($facetpage[$field]['more'] !== false) {
+                        $nextfields[] = $field;
+                    }
+                }
+            }
+            $facetfields = $nextfields;
+            $page++;
+        } while ($limit == -1 && !empty($facetfields));
+        return $facets;
+    }
 }
diff --git a/module/VuFind/src/VuFind/Search/Combined/Options.php b/module/VuFind/src/VuFind/Search/Combined/Options.php
index f4330f23b5464f0710343d5b499ca98d1a852c45..35748991640ec710f116225227b3ab2dc4b91cc2 100644
--- a/module/VuFind/src/VuFind/Search/Combined/Options.php
+++ b/module/VuFind/src/VuFind/Search/Combined/Options.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search_Base
diff --git a/module/VuFind/src/VuFind/Search/Combined/Params.php b/module/VuFind/src/VuFind/Search/Combined/Params.php
index 543e7d91a48a0cf7bdae253a4731b5a72304fd03..36e99db4ae9ce418008246f9db7400858a579921 100644
--- a/module/VuFind/src/VuFind/Search/Combined/Params.php
+++ b/module/VuFind/src/VuFind/Search/Combined/Params.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search_SolrAuth
diff --git a/module/VuFind/src/VuFind/Search/Combined/Results.php b/module/VuFind/src/VuFind/Search/Combined/Results.php
index 0846d84e022dfe97f4c0bc1be7f36f21e2ae3745..0fcd25636810f9f590d7ea622ea7d71230fc6fa3 100644
--- a/module/VuFind/src/VuFind/Search/Combined/Results.php
+++ b/module/VuFind/src/VuFind/Search/Combined/Results.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search_Base
diff --git a/module/VuFind/src/VuFind/Search/EDS/Options.php b/module/VuFind/src/VuFind/Search/EDS/Options.php
index 2a28e84b206df3ec43cb0dd03c5646bfeafdcc48..7fe292e463b181db1e9b2c9d27369dacf47d6e5d 100644
--- a/module/VuFind/src/VuFind/Search/EDS/Options.php
+++ b/module/VuFind/src/VuFind/Search/EDS/Options.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  EBSCO
@@ -47,42 +47,49 @@ class Options extends \VuFind\Search\Base\Options
 
     /**
      * Default search mode options
+     *
      * @var string
      */
     protected $defaultMode = 'all';
 
     /**
      * The set search mode
+     *
      * @var string
      */
     protected $searchMode;
 
     /**
      * Default expanders to apply
+     *
      * @var array
      */
     protected $defaultExpanders = [];
 
     /**
      * Available expander options
+     *
      * @var unknown
      */
     protected $expanderOptions = [];
 
     /**
      * Available limiter options
+     *
      * @var unknown
     */
     protected $limiterOptions = [];
 
     /**
-     * Wheither or not to return available facets with the search response
+     * Whether or not to return available facets with the search response
+     *
      * @var unknown
      */
     protected $includeFacets = 'y';
 
     /**
      * Available Search Options from the API
+     *
      * @var array
      */
     protected $apiInfo;
@@ -372,6 +379,11 @@ class Options extends \VuFind\Search\Base\Options
             $this->defaultView = 'list|' . $searchSettings->General->default_view;
         }
 
+        // Load list view for result (controls AJAX embedding vs. linking)
+        if (isset($searchSettings->List->view)) {
+            $this->listviewOption = $searchSettings->List->view;
+        }
+
         if (isset($searchSettings->Advanced_Facet_Settings->special_facets)) {
             $this->specialAdvancedFacets
                 = $searchSettings->Advanced_Facet_Settings->special_facets;
diff --git a/module/VuFind/src/VuFind/Search/EDS/Params.php b/module/VuFind/src/VuFind/Search/EDS/Params.php
index bd13a8dde6c9dabe7505992088905294f1ca428a..0c9935df69f662bc519dd01cdaa3a4fb26705d74 100644
--- a/module/VuFind/src/VuFind/Search/EDS/Params.php
+++ b/module/VuFind/src/VuFind/Search/EDS/Params.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  EBSCO
diff --git a/module/VuFind/src/VuFind/Search/EDS/QueryAdapter.php b/module/VuFind/src/VuFind/Search/EDS/QueryAdapter.php
index eaa938ad94777664072db69e03e5627d83fc9c6f..4c2548321efe761a1cf890338a13489b1e762907 100644
--- a/module/VuFind/src/VuFind/Search/EDS/QueryAdapter.php
+++ b/module/VuFind/src/VuFind/Search/EDS/QueryAdapter.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  EBSCO
diff --git a/module/VuFind/src/VuFind/Search/EDS/Results.php b/module/VuFind/src/VuFind/Search/EDS/Results.php
index 3852c3816c5bb8455eac8a9e0735a60cfec7a515..eb5c8776dc1840963e76203aebfccaf9120e84b2 100644
--- a/module/VuFind/src/VuFind/Search/EDS/Results.php
+++ b/module/VuFind/src/VuFind/Search/EDS/Results.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  EBSCO
diff --git a/module/VuFind/src/VuFind/Search/EIT/Options.php b/module/VuFind/src/VuFind/Search/EIT/Options.php
index f9d4182f5b2fcb56376d71fb09fd8cb59e00f16e..ca4cf69ac141a0aff05c92fa2b65a5cc6d950f45 100644
--- a/module/VuFind/src/VuFind/Search/EIT/Options.php
+++ b/module/VuFind/src/VuFind/Search/EIT/Options.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search_EIT
diff --git a/module/VuFind/src/VuFind/Search/EIT/Params.php b/module/VuFind/src/VuFind/Search/EIT/Params.php
index 297c0d46b4e05f9fa5b3a7dc18f3e78752194c1c..18aee0e88c646af977476b9c4bb66a3cde781c9a 100644
--- a/module/VuFind/src/VuFind/Search/EIT/Params.php
+++ b/module/VuFind/src/VuFind/Search/EIT/Params.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search_EIT
diff --git a/module/VuFind/src/VuFind/Search/EIT/Results.php b/module/VuFind/src/VuFind/Search/EIT/Results.php
index c988312a44856850f9af35501224527fa137d13b..b9754f1edb75500cc073e2fb6f7666dbd6248666 100644
--- a/module/VuFind/src/VuFind/Search/EIT/Results.php
+++ b/module/VuFind/src/VuFind/Search/EIT/Results.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search_EBSCO
diff --git a/module/VuFind/src/VuFind/Search/EmptySet/Options.php b/module/VuFind/src/VuFind/Search/EmptySet/Options.php
index 3993ff814331f82c01c65775e519bf32b8001af7..14944ba3f81cd6182f9856e37d0490fd18e837bd 100644
--- a/module/VuFind/src/VuFind/Search/EmptySet/Options.php
+++ b/module/VuFind/src/VuFind/Search/EmptySet/Options.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search_Tags
diff --git a/module/VuFind/src/VuFind/Search/EmptySet/Params.php b/module/VuFind/src/VuFind/Search/EmptySet/Params.php
index 4e4687a22e8d2980310903014d0f69356a0155d9..94f83d8abf36760095e2edfb986ad3de068347c3 100644
--- a/module/VuFind/src/VuFind/Search/EmptySet/Params.php
+++ b/module/VuFind/src/VuFind/Search/EmptySet/Params.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search_Tags
diff --git a/module/VuFind/src/VuFind/Search/EmptySet/Results.php b/module/VuFind/src/VuFind/Search/EmptySet/Results.php
index 875d0bb0935a10ff8331f23fc66ec1ab2ade5e0c..8719fafd1dafb8f320a1801320c88cd53d7f2dc5 100644
--- a/module/VuFind/src/VuFind/Search/EmptySet/Results.php
+++ b/module/VuFind/src/VuFind/Search/EmptySet/Results.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search_EmptySet
diff --git a/module/VuFind/src/VuFind/Search/Factory/AbstractSolrBackendFactory.php b/module/VuFind/src/VuFind/Search/Factory/AbstractSolrBackendFactory.php
index 01360e80bb8dc02eb66891c696fb957fcc18cecd..e0987fc71eb2dd63baea854232c53d647256628b 100644
--- a/module/VuFind/src/VuFind/Search/Factory/AbstractSolrBackendFactory.php
+++ b/module/VuFind/src/VuFind/Search/Factory/AbstractSolrBackendFactory.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
@@ -42,6 +42,7 @@ use VuFind\Search\Solr\HierarchicalFacetListener;
 use VuFindSearch\Backend\BackendInterface;
 use VuFindSearch\Backend\Solr\LuceneSyntaxHelper;
 use VuFindSearch\Backend\Solr\QueryBuilder;
+use VuFindSearch\Backend\Solr\SimilarBuilder;
 use VuFindSearch\Backend\Solr\HandlerMap;
 use VuFindSearch\Backend\Solr\Connector;
 use VuFindSearch\Backend\Solr\Backend;
@@ -156,6 +157,7 @@ abstract class AbstractSolrBackendFactory implements FactoryInterface
     {
         $backend = new Backend($connector);
         $backend->setQueryBuilder($this->createQueryBuilder());
+        $backend->setSimilarBuilder($this->createSimilarBuilder());
         if ($this->logger) {
             $backend->setLogger($this->logger);
         }
@@ -372,6 +374,18 @@ abstract class AbstractSolrBackendFactory implements FactoryInterface
         return $builder;
     }
 
+    /**
+     * Create the similar records query builder.
+     *
+     * @return SimilarBuilder
+     */
+    protected function createSimilarBuilder()
+    {
+        return new SimilarBuilder(
+            $this->config->get($this->searchConfig), $this->uniqueKey
+        );
+    }
+
     /**
      * Load the search specs.
      *
diff --git a/module/VuFind/src/VuFind/Search/Factory/EITBackendFactory.php b/module/VuFind/src/VuFind/Search/Factory/EITBackendFactory.php
index 3de97bf327427c088a86d3ad52497a37af24f09d..c9b2f0596fce21141cf3d8abd863ed43564f7111 100644
--- a/module/VuFind/src/VuFind/Search/Factory/EITBackendFactory.php
+++ b/module/VuFind/src/VuFind/Search/Factory/EITBackendFactory.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFind/src/VuFind/Search/Factory/EdsBackendFactory.php b/module/VuFind/src/VuFind/Search/Factory/EdsBackendFactory.php
index d57c5c208901470e034e693dc3830b9da6edea41..b67a1564a83b8f31617e819911dee6401652b5e4 100644
--- a/module/VuFind/src/VuFind/Search/Factory/EdsBackendFactory.php
+++ b/module/VuFind/src/VuFind/Search/Factory/EdsBackendFactory.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFind/src/VuFind/Search/Factory/LibGuidesBackendFactory.php b/module/VuFind/src/VuFind/Search/Factory/LibGuidesBackendFactory.php
index 803f9d456fcc77e771a9cb208f9e06b55b542cb5..8f571376ee55a4016ebe2f8767183fc72a66df21 100644
--- a/module/VuFind/src/VuFind/Search/Factory/LibGuidesBackendFactory.php
+++ b/module/VuFind/src/VuFind/Search/Factory/LibGuidesBackendFactory.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFind/src/VuFind/Search/Factory/Pazpar2BackendFactory.php b/module/VuFind/src/VuFind/Search/Factory/Pazpar2BackendFactory.php
index ec16b01c14dee22690a99a009027908c7d170943..12286c35cd702f890de78ccbc32cc7db491486ec 100644
--- a/module/VuFind/src/VuFind/Search/Factory/Pazpar2BackendFactory.php
+++ b/module/VuFind/src/VuFind/Search/Factory/Pazpar2BackendFactory.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFind/src/VuFind/Search/Factory/PrimoBackendFactory.php b/module/VuFind/src/VuFind/Search/Factory/PrimoBackendFactory.php
index 49019e14c8a26688607db3bac96db52675b83907..67d9e280136275e1832f15447d34b81c8e2f0001 100644
--- a/module/VuFind/src/VuFind/Search/Factory/PrimoBackendFactory.php
+++ b/module/VuFind/src/VuFind/Search/Factory/PrimoBackendFactory.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFind/src/VuFind/Search/Factory/SolrAuthBackendFactory.php b/module/VuFind/src/VuFind/Search/Factory/SolrAuthBackendFactory.php
index ac5f4b4aab71110dcb3a4358f912ff97511e6674..15c624ac676a34f582a99ea832cf76f9afbf0a36 100644
--- a/module/VuFind/src/VuFind/Search/Factory/SolrAuthBackendFactory.php
+++ b/module/VuFind/src/VuFind/Search/Factory/SolrAuthBackendFactory.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFind/src/VuFind/Search/Factory/SolrDefaultBackendFactory.php b/module/VuFind/src/VuFind/Search/Factory/SolrDefaultBackendFactory.php
index f4f3938f175f46ef95bb14fd009f1f95d8ddc1d0..3a4a0a34ec910c977b265fea8b60b878378b7544 100644
--- a/module/VuFind/src/VuFind/Search/Factory/SolrDefaultBackendFactory.php
+++ b/module/VuFind/src/VuFind/Search/Factory/SolrDefaultBackendFactory.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFind/src/VuFind/Search/Factory/SolrReservesBackendFactory.php b/module/VuFind/src/VuFind/Search/Factory/SolrReservesBackendFactory.php
index b2178803d40906dfffacc4bb020bb8582a58a5aa..4d3dc436094cc4e6c963a233a05437b86942a640 100644
--- a/module/VuFind/src/VuFind/Search/Factory/SolrReservesBackendFactory.php
+++ b/module/VuFind/src/VuFind/Search/Factory/SolrReservesBackendFactory.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFind/src/VuFind/Search/Factory/SolrStatsBackendFactory.php b/module/VuFind/src/VuFind/Search/Factory/SolrStatsBackendFactory.php
index df8d1acd34350fdff18846cb9c07cd62ae45c20d..439744f2d69ba35039885baea0a322d4ed369a74 100644
--- a/module/VuFind/src/VuFind/Search/Factory/SolrStatsBackendFactory.php
+++ b/module/VuFind/src/VuFind/Search/Factory/SolrStatsBackendFactory.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFind/src/VuFind/Search/Factory/SolrWebBackendFactory.php b/module/VuFind/src/VuFind/Search/Factory/SolrWebBackendFactory.php
index 53c01095320a603508172fac077153a714fd5ef5..e8123f1b540b45dbce9cd3644e210b05f7cfcd41 100644
--- a/module/VuFind/src/VuFind/Search/Factory/SolrWebBackendFactory.php
+++ b/module/VuFind/src/VuFind/Search/Factory/SolrWebBackendFactory.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFind/src/VuFind/Search/Factory/SummonBackendFactory.php b/module/VuFind/src/VuFind/Search/Factory/SummonBackendFactory.php
index ebd898c7cc2abbe8f50ef16d6cb91ee837ba4874..4dc9b5a07a8dc4e71ba7861e55c0e5c8456a5b5a 100644
--- a/module/VuFind/src/VuFind/Search/Factory/SummonBackendFactory.php
+++ b/module/VuFind/src/VuFind/Search/Factory/SummonBackendFactory.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFind/src/VuFind/Search/Factory/WorldCatBackendFactory.php b/module/VuFind/src/VuFind/Search/Factory/WorldCatBackendFactory.php
index b3ecdbc8e5a564977afd2bac549aa14d69c0e5e0..b904866bab0816a05a87777f90bbf532c9e3e93c 100644
--- a/module/VuFind/src/VuFind/Search/Factory/WorldCatBackendFactory.php
+++ b/module/VuFind/src/VuFind/Search/Factory/WorldCatBackendFactory.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFind/src/VuFind/Search/Favorites/Options.php b/module/VuFind/src/VuFind/Search/Favorites/Options.php
index d6cbba63b5baff399f09c6a0fcf0637a8a053060..f7ad8daf1d601d5bba0be34f140043aa9c4f0e3f 100644
--- a/module/VuFind/src/VuFind/Search/Favorites/Options.php
+++ b/module/VuFind/src/VuFind/Search/Favorites/Options.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search_Favorites
@@ -52,6 +52,16 @@ class Options extends \VuFind\Search\Base\Options
             'title' => 'sort_title', 'author' => 'sort_author',
             'year DESC' => 'sort_year', 'year' => 'sort_year asc'
         ];
+        $config = $configLoader->get('config');
+        if (isset($config->Social->lists_default_limit)) {
+            $this->defaultLimit = $config->Social->lists_default_limit;
+        }
+        if (isset($config->Social->lists_limit_options)) {
+            $this->limitOptions = explode(',', $config->Social->lists_limit_options);
+        }
+        if (isset($config->Social->lists_view)) {
+            $this->listviewOption = $config->Social->lists_view;
+        }
     }
 
     /**
diff --git a/module/VuFind/src/VuFind/Search/Favorites/Params.php b/module/VuFind/src/VuFind/Search/Favorites/Params.php
index 835a6f4345b7c344a89ef456d0cf6182a1865cc3..ccde0805c561f1c83bcec64c151ae951590caecf 100644
--- a/module/VuFind/src/VuFind/Search/Favorites/Params.php
+++ b/module/VuFind/src/VuFind/Search/Favorites/Params.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search_Favorites
diff --git a/module/VuFind/src/VuFind/Search/Favorites/Results.php b/module/VuFind/src/VuFind/Search/Favorites/Results.php
index 161c7aa568291709a997c80aa91930b4b15968b6..24c942b4a8a4184d3a3bd80e4463267409b75ac3 100644
--- a/module/VuFind/src/VuFind/Search/Favorites/Results.php
+++ b/module/VuFind/src/VuFind/Search/Favorites/Results.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search_Favorites
diff --git a/module/VuFind/src/VuFind/Search/LibGuides/Options.php b/module/VuFind/src/VuFind/Search/LibGuides/Options.php
index e8e173b907e429728d278f7331af6fcd857a8339..bba8ba0e9b385fb4c68bc58f3b4442d240069b48 100644
--- a/module/VuFind/src/VuFind/Search/LibGuides/Options.php
+++ b/module/VuFind/src/VuFind/Search/LibGuides/Options.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search_LibGuides
diff --git a/module/VuFind/src/VuFind/Search/LibGuides/Params.php b/module/VuFind/src/VuFind/Search/LibGuides/Params.php
index 1127a19241f3ddfcb8c9a85d2aa8a4a2aa7fba8c..92909647a59deab79290277573c9787e7f431b4f 100644
--- a/module/VuFind/src/VuFind/Search/LibGuides/Params.php
+++ b/module/VuFind/src/VuFind/Search/LibGuides/Params.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search_LibGuides
diff --git a/module/VuFind/src/VuFind/Search/LibGuides/Results.php b/module/VuFind/src/VuFind/Search/LibGuides/Results.php
index 51a2511c78beb44264806106e05c6ed9fd96de2c..a59f2e3c7d291d5279e309b305a98b0b5352cf85 100644
--- a/module/VuFind/src/VuFind/Search/LibGuides/Results.php
+++ b/module/VuFind/src/VuFind/Search/LibGuides/Results.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search_LibGuides
diff --git a/module/VuFind/src/VuFind/Search/Memory.php b/module/VuFind/src/VuFind/Search/Memory.php
index b4f5ab03df876051b6b4c46b7080f4c06c8736f7..077f9f860897070ee2493783220aed7b385f9306 100644
--- a/module/VuFind/src/VuFind/Search/Memory.php
+++ b/module/VuFind/src/VuFind/Search/Memory.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFind/src/VuFind/Search/Minified.php b/module/VuFind/src/VuFind/Search/Minified.php
index e189de0dcef87f8019746a2d309f5b39a961f6e6..2854a61b185ba4c0b0e1999eadd94894514f0b32 100644
--- a/module/VuFind/src/VuFind/Search/Minified.php
+++ b/module/VuFind/src/VuFind/Search/Minified.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFind/src/VuFind/Search/MixedList/Options.php b/module/VuFind/src/VuFind/Search/MixedList/Options.php
index 512668159085218655738d307bbe23c5cb445421..855eee542fac06eb11f1f51a6ce4e7d3ef7a5fef 100644
--- a/module/VuFind/src/VuFind/Search/MixedList/Options.php
+++ b/module/VuFind/src/VuFind/Search/MixedList/Options.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search_MixedList
diff --git a/module/VuFind/src/VuFind/Search/MixedList/Params.php b/module/VuFind/src/VuFind/Search/MixedList/Params.php
index 20d4f82bdfe06fe54504750b71ecb09f70bfae91..e3e7948268b843c70ac161ee3c4ac289ee8621ae 100644
--- a/module/VuFind/src/VuFind/Search/MixedList/Params.php
+++ b/module/VuFind/src/VuFind/Search/MixedList/Params.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search_MixedList
@@ -55,12 +55,87 @@ class Params extends \VuFind\Search\Base\Params
      */
     protected function initSearch($request)
     {
-        $this->recordsToRequest = $request->get('id', []);
+        // Convert special 'id' parameter into a standard hidden filter:
+        $idParam = $request->get('id', []);
+        if (!empty($idParam)) {
+            $this->addHiddenFilter('ids:' . implode("\t", $idParam));
+        }
+    }
 
-        // We always want to display the entire list as one page:
+    /**
+     * Parse record ids from a filter value and set as the ID list.
+     *
+     * @param string $filterValue Filter value
+     *
+     * @return void
+     */
+    protected function setRecordIdsFromFilter($filterValue)
+    {
+        $this->recordsToRequest = explode("\t", $filterValue);
         $this->setLimit(count($this->recordsToRequest));
     }
 
+    /**
+     * Take a filter string and add it into the protected hidden filters
+     *   array checking for duplicates.
+     *
+     * Special case for 'ids': populate the ID list and remove from hidden filters.
+     *
+     * @param string $newFilter A filter string from url : "field:value"
+     *
+     * @return void
+     */
+    public function addHiddenFilter($newFilter)
+    {
+        list($field, $value) = $this->parseFilter($newFilter);
+        if ($field == 'ids') {
+            $this->setRecordIdsFromFilter($value);
+        } else {
+            parent::addHiddenFilter($newFilter);
+        }
+    }
+
+    /**
+     * Restore settings from a minified object found in the database.
+     *
+     * @param \VuFind\Search\Minified $minified Minified Search Object
+     *
+     * @return void
+     */
+    public function deminify($minified)
+    {
+        parent::deminify($minified);
+        if (isset($this->hiddenFilters['ids'][0])) {
+            $this->setRecordIdsFromFilter($this->hiddenFilters['ids'][0]);
+            unset($this->hiddenFilters['ids']);
+        }
+    }
+
+    /**
+     * Build a string for onscreen display.
+     *
+     * @return string
+     */
+    public function getDisplayQuery()
+    {
+        return $this->translate(
+            'result_count', ['%%count%%' => count($this->recordsToRequest)]
+        );
+    }
+
+    /**
+     * Return record ids as a hidden filter list so that it is properly stored when
+     * the search is represented as an URL or stored in the database.
+     *
+     * @return array
+     */
+    public function getHiddenFilters()
+    {
+        $filters = parent::getHiddenFilters();
+        $filters['ids'] = [implode("\t", $this->recordsToRequest)];
+        return $filters;
+    }
+
     /**
      * Get list of records to display.
      *
diff --git a/module/VuFind/src/VuFind/Search/MixedList/Results.php b/module/VuFind/src/VuFind/Search/MixedList/Results.php
index 76f544dadc7e2dd275ae99af8320b14878347512..582ed29f810464408b49bd953d2ab7a2fd1e37e7 100644
--- a/module/VuFind/src/VuFind/Search/MixedList/Results.php
+++ b/module/VuFind/src/VuFind/Search/MixedList/Results.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search_MixedList
diff --git a/module/VuFind/src/VuFind/Search/Options/Factory.php b/module/VuFind/src/VuFind/Search/Options/Factory.php
index 079782f6459c3169a236db8dd9985a2d2651d4bc..266e3cdbfeddb46a1605ff956a4400a36b31f549 100644
--- a/module/VuFind/src/VuFind/Search/Options/Factory.php
+++ b/module/VuFind/src/VuFind/Search/Options/Factory.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFind/src/VuFind/Search/Options/PluginFactory.php b/module/VuFind/src/VuFind/Search/Options/PluginFactory.php
index 0892d32ce6558f9bc8001e660946f21e1831d15f..e263c58186a51f20bae3fcef520b1bf5b36c1e64 100644
--- a/module/VuFind/src/VuFind/Search/Options/PluginFactory.php
+++ b/module/VuFind/src/VuFind/Search/Options/PluginFactory.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFind/src/VuFind/Search/Options/PluginManager.php b/module/VuFind/src/VuFind/Search/Options/PluginManager.php
index f26aa2efdda1041d651c5f01e8ca2dcbbdf938f7..75dcd6488b9d5584915f47ca1e14d7cb8f63699b 100644
--- a/module/VuFind/src/VuFind/Search/Options/PluginManager.php
+++ b/module/VuFind/src/VuFind/Search/Options/PluginManager.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFind/src/VuFind/Search/Params/PluginFactory.php b/module/VuFind/src/VuFind/Search/Params/PluginFactory.php
index 6c4eb3e79562e4a83708e3bd012b447a05bffca5..4caff16388b33735d515016a420079559afbed7d 100644
--- a/module/VuFind/src/VuFind/Search/Params/PluginFactory.php
+++ b/module/VuFind/src/VuFind/Search/Params/PluginFactory.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFind/src/VuFind/Search/Params/PluginManager.php b/module/VuFind/src/VuFind/Search/Params/PluginManager.php
index bf9f8dbabfcebf29a76d3550fa6c0f2b71c45ce6..fcace47adef1020e96ce5f29dd037cd34b926dd9 100644
--- a/module/VuFind/src/VuFind/Search/Params/PluginManager.php
+++ b/module/VuFind/src/VuFind/Search/Params/PluginManager.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFind/src/VuFind/Search/Pazpar2/Options.php b/module/VuFind/src/VuFind/Search/Pazpar2/Options.php
index 673a13395eef7ac8a882d7a5864228cbe098ed2b..1561404121cc8e6e416bd29004dae9a0f5c35727 100644
--- a/module/VuFind/src/VuFind/Search/Pazpar2/Options.php
+++ b/module/VuFind/src/VuFind/Search/Pazpar2/Options.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search_Pazpar2
diff --git a/module/VuFind/src/VuFind/Search/Pazpar2/Params.php b/module/VuFind/src/VuFind/Search/Pazpar2/Params.php
index 62128cd6ad12080995c1348d8b33d0fc470a5646..83ad96c7127de565098caff64aeefa1bf50f70d8 100644
--- a/module/VuFind/src/VuFind/Search/Pazpar2/Params.php
+++ b/module/VuFind/src/VuFind/Search/Pazpar2/Params.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search_Pazpar2
diff --git a/module/VuFind/src/VuFind/Search/Pazpar2/Results.php b/module/VuFind/src/VuFind/Search/Pazpar2/Results.php
index 5ca517b15e23e9f7e59dfda602a2d303da61dbf3..555c6258b1fa7eacad972649731d3b8ea22b68c9 100644
--- a/module/VuFind/src/VuFind/Search/Pazpar2/Results.php
+++ b/module/VuFind/src/VuFind/Search/Pazpar2/Results.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search_Pazpar2
diff --git a/module/VuFind/src/VuFind/Search/Primo/InjectOnCampusListener.php b/module/VuFind/src/VuFind/Search/Primo/InjectOnCampusListener.php
index 88dfa292da3148f6ae8b91ed7b5adf2f45522ce7..e75640b1ac293cc95a148f71cd73cb24b514c85a 100644
--- a/module/VuFind/src/VuFind/Search/Primo/InjectOnCampusListener.php
+++ b/module/VuFind/src/VuFind/Search/Primo/InjectOnCampusListener.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
@@ -57,7 +57,7 @@ class InjectOnCampusListener
     /**
      * Is user on campus or not?
      *
-     * @var boolean
+     * @var bool
      */
     protected $isOnCampus;
 
@@ -101,7 +101,7 @@ class InjectOnCampusListener
     /**
      * Determines, which value is needed for the onCampus parameter
      *
-     * @return boolean
+     * @return bool
      */
     protected function getOnCampus()
     {
diff --git a/module/VuFind/src/VuFind/Search/Primo/Options.php b/module/VuFind/src/VuFind/Search/Primo/Options.php
index 9d09b21449fd6628b5ea76ef86c3340e4226016e..0140dc41df9fc1569f499de30b1c8b280985a955 100644
--- a/module/VuFind/src/VuFind/Search/Primo/Options.php
+++ b/module/VuFind/src/VuFind/Search/Primo/Options.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search_Primo
diff --git a/module/VuFind/src/VuFind/Search/Primo/Params.php b/module/VuFind/src/VuFind/Search/Primo/Params.php
index 8de1045b1de1693dc4d9b29f64da6a5cb2285019..330a883f02229d8a58f65d1d9f3ea8c4691f030f 100644
--- a/module/VuFind/src/VuFind/Search/Primo/Params.php
+++ b/module/VuFind/src/VuFind/Search/Primo/Params.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search_Primo
diff --git a/module/VuFind/src/VuFind/Search/Primo/PrimoPermissionHandler.php b/module/VuFind/src/VuFind/Search/Primo/PrimoPermissionHandler.php
index c0b5d174284cab6124af76faabfde90d098d258c..8ef3535f2b4eaea1dd661724cdff018b2efbbd46 100644
--- a/module/VuFind/src/VuFind/Search/Primo/PrimoPermissionHandler.php
+++ b/module/VuFind/src/VuFind/Search/Primo/PrimoPermissionHandler.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
@@ -94,7 +94,7 @@ class PrimoPermissionHandler
      *
      * @param string $code Code to approve against config file
      *
-     * @return boolean
+     * @return bool
      */
     public function instCodeExists($code)
     {
@@ -118,7 +118,7 @@ class PrimoPermissionHandler
     /**
      * Check if the user has permission
      *
-     * @return boolean
+     * @return bool
      */
     public function hasPermission()
     {
diff --git a/module/VuFind/src/VuFind/Search/Primo/Results.php b/module/VuFind/src/VuFind/Search/Primo/Results.php
index cc558e1e40f10ca83af4f955a20d13804b5e49d2..c6f2d2eec8c109e7a1bfb9fcafdb784312fbac82 100644
--- a/module/VuFind/src/VuFind/Search/Primo/Results.php
+++ b/module/VuFind/src/VuFind/Search/Primo/Results.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search_Primo
diff --git a/module/VuFind/src/VuFind/Search/QueryAdapter.php b/module/VuFind/src/VuFind/Search/QueryAdapter.php
index 2df9783e1530adfb401acc3129cf83537497e545..034ed22a5a642aead018b6204ff8f8f013a2bef5 100644
--- a/module/VuFind/src/VuFind/Search/QueryAdapter.php
+++ b/module/VuFind/src/VuFind/Search/QueryAdapter.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search_Solr
diff --git a/module/VuFind/src/VuFind/Search/RecommendListener.php b/module/VuFind/src/VuFind/Search/RecommendListener.php
index 2e1709e8c7a2e31b9ffa3d56c0a3071454b73ac3..c0043c342598855b50a454673229562cc8825135 100644
--- a/module/VuFind/src/VuFind/Search/RecommendListener.php
+++ b/module/VuFind/src/VuFind/Search/RecommendListener.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFind/src/VuFind/Search/Results/Factory.php b/module/VuFind/src/VuFind/Search/Results/Factory.php
index a3d17d9b1ec86f80bd4ba2ec1b9e67d23b3fcedb..ff723c194597747cd4062dbd80c362b76fac1c17 100644
--- a/module/VuFind/src/VuFind/Search/Results/Factory.php
+++ b/module/VuFind/src/VuFind/Search/Results/Factory.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFind/src/VuFind/Search/Results/PluginFactory.php b/module/VuFind/src/VuFind/Search/Results/PluginFactory.php
index a05a6d34ad415b27e0f036d6a436b812a0b23165..14f53b00a9f69ce8df27dfe909d401c5f305c1be 100644
--- a/module/VuFind/src/VuFind/Search/Results/PluginFactory.php
+++ b/module/VuFind/src/VuFind/Search/Results/PluginFactory.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFind/src/VuFind/Search/Results/PluginManager.php b/module/VuFind/src/VuFind/Search/Results/PluginManager.php
index 50745cb19e9b8f902509f2bee8a57f9e5eb51530..2341fef78c47eb37141cc036716dadddfdedca0d 100644
--- a/module/VuFind/src/VuFind/Search/Results/PluginManager.php
+++ b/module/VuFind/src/VuFind/Search/Results/PluginManager.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFind/src/VuFind/Search/SearchRunner.php b/module/VuFind/src/VuFind/Search/SearchRunner.php
index 11c9a82b20ae5522af79af73386a51e319d7f958..eec7df35e9549f7ebf436fcf426446bbd1d64a8f 100644
--- a/module/VuFind/src/VuFind/Search/SearchRunner.php
+++ b/module/VuFind/src/VuFind/Search/SearchRunner.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFind/src/VuFind/Search/SearchTabsHelper.php b/module/VuFind/src/VuFind/Search/SearchTabsHelper.php
index 1e15fd959d9fa32e2dfc66f3d33633d72ce98cae..6a4fb95d6c86709b6de3d9ece37323b2a4ee61a2 100644
--- a/module/VuFind/src/VuFind/Search/SearchTabsHelper.php
+++ b/module/VuFind/src/VuFind/Search/SearchTabsHelper.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  View_Helpers
@@ -150,7 +150,7 @@ class SearchTabsHelper extends \Zend\View\Helper\AbstractHelper
      * @param string $hiddenFilters Hidden filters
      * @param string $configFilters Filters from filter configuration
      *
-     * @return boolean
+     * @return bool
      */
     public function filtersMatch($class, $hiddenFilters, $configFilters)
     {
diff --git a/module/VuFind/src/VuFind/Search/Solr/AbstractErrorListener.php b/module/VuFind/src/VuFind/Search/Solr/AbstractErrorListener.php
index 64539dbb276700b3c4a191dcca426c1e975d74a2..446a3f45133bbc495b334d1ea7b2f96566f9714b 100644
--- a/module/VuFind/src/VuFind/Search/Solr/AbstractErrorListener.php
+++ b/module/VuFind/src/VuFind/Search/Solr/AbstractErrorListener.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
@@ -90,7 +90,7 @@ abstract class AbstractErrorListener
      *
      * @param BackendInterface $backend Backend instance
      *
-     * @return boolean
+     * @return bool
      */
     public function listenForBackend(BackendInterface $backend)
     {
diff --git a/module/VuFind/src/VuFind/Search/Solr/DeduplicationListener.php b/module/VuFind/src/VuFind/Search/Solr/DeduplicationListener.php
index f48222ba8b99e64a9da07ad4205ca35fcf1b0e37..a2a34cc42b9bb0d440c6744be2ed7d2f1f11428d 100644
--- a/module/VuFind/src/VuFind/Search/Solr/DeduplicationListener.php
+++ b/module/VuFind/src/VuFind/Search/Solr/DeduplicationListener.php
@@ -19,7 +19,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFind/src/VuFind/Search/Solr/FilterFieldConversionListener.php b/module/VuFind/src/VuFind/Search/Solr/FilterFieldConversionListener.php
index c865d3d0be3598bd662c57c95764fe841358eb07..0e89b2cdda6ab341fc61920dfe26d149910c55f8 100644
--- a/module/VuFind/src/VuFind/Search/Solr/FilterFieldConversionListener.php
+++ b/module/VuFind/src/VuFind/Search/Solr/FilterFieldConversionListener.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFind/src/VuFind/Search/Solr/HideFacetValueListener.php b/module/VuFind/src/VuFind/Search/Solr/HideFacetValueListener.php
index 8b00b34afa7d0903c346b9cdeb1327d99639eaa6..ef47bdbdbfe85738f7d47316de8a0974807f62bc 100644
--- a/module/VuFind/src/VuFind/Search/Solr/HideFacetValueListener.php
+++ b/module/VuFind/src/VuFind/Search/Solr/HideFacetValueListener.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFind/src/VuFind/Search/Solr/HierarchicalFacetHelper.php b/module/VuFind/src/VuFind/Search/Solr/HierarchicalFacetHelper.php
index 05f1ada3f78032aadc148b1570fe9a467f0958eb..7f555923a3ed680c6f91a1e85a3542b3356f6ff9 100644
--- a/module/VuFind/src/VuFind/Search/Solr/HierarchicalFacetHelper.php
+++ b/module/VuFind/src/VuFind/Search/Solr/HierarchicalFacetHelper.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
@@ -244,7 +244,7 @@ class HierarchicalFacetHelper
      *
      * @param array $list Facet list
      *
-     * @return boolean Whether any items are applied (for recursive calls)
+     * @return bool Whether any items are applied (for recursive calls)
      */
     protected function updateAppliedChildrenStatus($list)
     {
diff --git a/module/VuFind/src/VuFind/Search/Solr/HierarchicalFacetListener.php b/module/VuFind/src/VuFind/Search/Solr/HierarchicalFacetListener.php
index 713fad232136707b8cedb2b9571f86c4d3313238..0f974ae07bccb2c3a4851526d4055ae4cdf5e197 100644
--- a/module/VuFind/src/VuFind/Search/Solr/HierarchicalFacetListener.php
+++ b/module/VuFind/src/VuFind/Search/Solr/HierarchicalFacetListener.php
@@ -19,7 +19,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFind/src/VuFind/Search/Solr/InjectConditionalFilterListener.php b/module/VuFind/src/VuFind/Search/Solr/InjectConditionalFilterListener.php
index 4b29711f06b516ae2eec3abe51f58aa6b6e4929c..15ea7983688e67bf41e3f5c27f913cf0d219fc1c 100644
--- a/module/VuFind/src/VuFind/Search/Solr/InjectConditionalFilterListener.php
+++ b/module/VuFind/src/VuFind/Search/Solr/InjectConditionalFilterListener.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFind/src/VuFind/Search/Solr/InjectHighlightingListener.php b/module/VuFind/src/VuFind/Search/Solr/InjectHighlightingListener.php
index de4c9176b40b9822495cba4365e264efbea4e8a7..181e5e6bfeef2d35a35c48ecbfe65abd9afbd904 100644
--- a/module/VuFind/src/VuFind/Search/Solr/InjectHighlightingListener.php
+++ b/module/VuFind/src/VuFind/Search/Solr/InjectHighlightingListener.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFind/src/VuFind/Search/Solr/InjectSpellingListener.php b/module/VuFind/src/VuFind/Search/Solr/InjectSpellingListener.php
index 6a8a0fe46f3415cef25fbd452260b0f4c9fbb25f..ee6662a75dc6bd64eae486b3765739adf51f6bc5 100644
--- a/module/VuFind/src/VuFind/Search/Solr/InjectSpellingListener.php
+++ b/module/VuFind/src/VuFind/Search/Solr/InjectSpellingListener.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
@@ -134,8 +134,9 @@ class InjectSpellingListener
                     );
 
                     // Turn on spellcheck.q generation in query builder:
-                    $this->backend->getQueryBuilder()
-                        ->setCreateSpellingQuery(true);
+                    $this->backend->getQueryBuilder()->setCreateSpellingQuery(true);
+                } else {
+                    $this->backend->getQueryBuilder()->setCreateSpellingQuery(false);
                 }
             }
         }
diff --git a/module/VuFind/src/VuFind/Search/Solr/MultiIndexListener.php b/module/VuFind/src/VuFind/Search/Solr/MultiIndexListener.php
index d86c96fde6f5990830dd4b90d4a88517f7895ce4..64aefab9ca7b564d7b15c1d3fcf210152183f285 100644
--- a/module/VuFind/src/VuFind/Search/Solr/MultiIndexListener.php
+++ b/module/VuFind/src/VuFind/Search/Solr/MultiIndexListener.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFind/src/VuFind/Search/Solr/Options.php b/module/VuFind/src/VuFind/Search/Solr/Options.php
index 02b6504cdd933fb027b9132523e5b938703e4a80..6855645189e91a24835f076624333cd46820ea11 100644
--- a/module/VuFind/src/VuFind/Search/Solr/Options.php
+++ b/module/VuFind/src/VuFind/Search/Solr/Options.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search_Solr
@@ -38,6 +38,16 @@ namespace VuFind\Search\Solr;
  */
 class Options extends \VuFind\Search\Base\Options
 {
+    /**
+     * Available sort options for facets
+     *
+     * @var array
+     */
+    protected $facetSortOptions = [
+        'count' => 'sort_count',
+        'index' => 'sort_alphabetic'
+    ];
+
     /**
      * Hierarchical facets
      *
@@ -142,6 +152,10 @@ class Options extends \VuFind\Search\Base\Options
         } else {
             $this->viewOptions = ['list' => 'List'];
         }
+        // Load list view for result (controls AJAX embedding vs. linking)
+        if (isset($searchSettings->List->view)) {
+            $this->listviewOption = $searchSettings->List->view;
+        }
 
         // Load facet preferences
         $facetSettings = $configLoader->get($this->facetsIni);
@@ -184,6 +198,13 @@ class Options extends \VuFind\Search\Base\Options
             $this->spellcheck = $config->Spelling->enabled;
         }
 
+        // Turn on first/last navigation if configured:
+        if (isset($config->Record->first_last_navigation)
+            && $config->Record->first_last_navigation
+        ) {
+            $this->firstlastNavigation = true;
+        }
+
         // Turn on highlighting if the user has requested highlighting or snippet
         // functionality:
         $highlight = !isset($searchSettings->General->highlighting)
@@ -250,6 +271,17 @@ class Options extends \VuFind\Search\Base\Options
         return 'search-advanced';
     }
 
+    /**
+     * Return the route name for the facet list action. Returns false to cover
+     * unimplemented support.
+     *
+     * @return string|bool
+     */
+    public function getFacetListAction()
+    {
+        return 'search-facetlist';
+    }
+
     /**
      * Get the relevance sort override for empty searches.
      *
diff --git a/module/VuFind/src/VuFind/Search/Solr/Params.php b/module/VuFind/src/VuFind/Search/Solr/Params.php
index 06df69db698501987534ab3b0732dbbfecc82eb5..9689a2d893bd900369351fcbacfd608bccfff1ee 100644
--- a/module/VuFind/src/VuFind/Search/Solr/Params.php
+++ b/module/VuFind/src/VuFind/Search/Solr/Params.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search_Solr
diff --git a/module/VuFind/src/VuFind/Search/Solr/Results.php b/module/VuFind/src/VuFind/Search/Solr/Results.php
index bc6a1db1e3a10d334e0e2b15f6e1bc64d12894f5..dedf2cab7dda1e3474afbbb689d361f9303d1f34 100644
--- a/module/VuFind/src/VuFind/Search/Solr/Results.php
+++ b/module/VuFind/src/VuFind/Search/Solr/Results.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search_Solr
@@ -294,11 +294,13 @@ class Results extends \VuFind\Search\Base\Results
      * may be useful for very large amounts of facets that can break the JSON parse
      * method because of PHP out of memory exceptions (default = -1, no limit).
      * @param string $facetSort    A facet sort value to use (null to retain current)
+     * @param int    $page         1 based. Offsets results by limit.
+     * @param bool   $ored         Whether or not facet is an OR facet or not
      *
-     * @return array an array with the facet values for each index field
+     * @return array list facet values for each index field with label and more bool
      */
-    public function getFullFieldFacets($facetfields, $removeFilter = true,
-        $limit = -1, $facetSort = null
+    public function getPartialFieldFacets($facetfields, $removeFilter = true,
+        $limit = -1, $facetSort = null, $page = null, $ored = false
     ) {
         $clone = clone($this);
         $params = $clone->getParams();
@@ -306,11 +308,17 @@ class Results extends \VuFind\Search\Base\Results
         // Manipulate facet settings temporarily:
         $params->resetFacetConfig();
         $params->setFacetLimit($limit);
+        if (null !== $page && $limit != -1) {
+            $offset = ($page - 1) * $limit;
+            $params->setFacetOffset($offset);
+            // Return limit plus one so we know there's another page
+            $params->setFacetLimit($limit + 1);
+        }
         if (null !== $facetSort) {
             $params->setFacetSort($facetSort);
         }
         foreach ($facetfields as $facetName) {
-            $params->addFacet($facetName);
+            $params->addFacet($facetName, null, $ored);
 
             // Clear existing filters for the selected field if necessary:
             if ($removeFilter) {
@@ -335,8 +343,15 @@ class Results extends \VuFind\Search\Base\Results
 
         // Reformat into a hash:
         foreach ($result as $key => $value) {
-            unset($result[$key]);
-            $result[$key]['data'] = $value;
+            // Detect next page and crop results if necessary
+            $more = false;
+            if (isset($page) && count($value['list']) > 0
+                && count($value['list']) == $limit + 1
+            ) {
+                $more = true;
+                array_pop($value['list']);
+            }
+            $result[$key] = ['more' => $more, 'data' => $value];
         }
 
         // Send back data:
diff --git a/module/VuFind/src/VuFind/Search/Solr/SpellingProcessor.php b/module/VuFind/src/VuFind/Search/Solr/SpellingProcessor.php
index fb90d679311442c03c5d55971260f5149404f7b1..05532e44a2fe25e1fa3ec40d461cb81e0b63b3a6 100644
--- a/module/VuFind/src/VuFind/Search/Solr/SpellingProcessor.php
+++ b/module/VuFind/src/VuFind/Search/Solr/SpellingProcessor.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search_Solr
diff --git a/module/VuFind/src/VuFind/Search/Solr/V3/ErrorListener.php b/module/VuFind/src/VuFind/Search/Solr/V3/ErrorListener.php
index 33d3a4efa4908d47d9615b70ef97d4d29b0826f9..f27dd86a921b1c1a843f2f9788062315fb7ff4ce 100644
--- a/module/VuFind/src/VuFind/Search/Solr/V3/ErrorListener.php
+++ b/module/VuFind/src/VuFind/Search/Solr/V3/ErrorListener.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFind/src/VuFind/Search/Solr/V4/ErrorListener.php b/module/VuFind/src/VuFind/Search/Solr/V4/ErrorListener.php
index 82e9e3e2a9104e7e7086898272e248a5cd1ffe34..02845f34d9dc4fadb72d6ef8576622d655599c5c 100644
--- a/module/VuFind/src/VuFind/Search/Solr/V4/ErrorListener.php
+++ b/module/VuFind/src/VuFind/Search/Solr/V4/ErrorListener.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFind/src/VuFind/Search/SolrAuth/Options.php b/module/VuFind/src/VuFind/Search/SolrAuth/Options.php
index efaef88a72c64bd2b3482887c1d4c4205da37e3c..4db4a16228c4e5fc418e309169cdf43a684291ad 100644
--- a/module/VuFind/src/VuFind/Search/SolrAuth/Options.php
+++ b/module/VuFind/src/VuFind/Search/SolrAuth/Options.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search_SolrAuth
@@ -50,6 +50,17 @@ class Options extends \VuFind\Search\Solr\Options
         $this->spellcheck = false;
     }
 
+    /**
+     * Return the route name for the facet list action. Returns false to cover
+     * unimplemented support.
+     *
+     * @return string|bool
+     */
+    public function getFacetListAction()
+    {
+        return 'authority-facetlist';
+    }
+
     /**
      * Return the route name for the search results action.
      *
diff --git a/module/VuFind/src/VuFind/Search/SolrAuth/Params.php b/module/VuFind/src/VuFind/Search/SolrAuth/Params.php
index ff7b25bd3182d448c9e935304369ca523459bedc..231df92091baeac48247251bad2db3b2b41fe5e8 100644
--- a/module/VuFind/src/VuFind/Search/SolrAuth/Params.php
+++ b/module/VuFind/src/VuFind/Search/SolrAuth/Params.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search_SolrAuth
diff --git a/module/VuFind/src/VuFind/Search/SolrAuth/Results.php b/module/VuFind/src/VuFind/Search/SolrAuth/Results.php
index 0e99b327b3acc435801fb003e3468bf9567aa4e3..9b81dd415428eadc1d1e85e3eca6d39455735677 100644
--- a/module/VuFind/src/VuFind/Search/SolrAuth/Results.php
+++ b/module/VuFind/src/VuFind/Search/SolrAuth/Results.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search_SolrAuth
diff --git a/module/VuFind/src/VuFind/Search/SolrAuthor/Options.php b/module/VuFind/src/VuFind/Search/SolrAuthor/Options.php
index c84927faa023060da0b5ff79a15e2b4ba82c715a..240c8faa19ff0ee4a5b6e6d837189d86b4cd89aa 100644
--- a/module/VuFind/src/VuFind/Search/SolrAuthor/Options.php
+++ b/module/VuFind/src/VuFind/Search/SolrAuthor/Options.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search_SolrAuthor
@@ -51,6 +51,17 @@ class Options extends \VuFind\Search\Solr\Options
         $this->spellcheck = false;
     }
 
+    /**
+     * Return the route name for the facet list action. Returns false to cover
+     * unimplemented support.
+     *
+     * @return string|bool
+     */
+    public function getFacetListAction()
+    {
+        return 'author-facetlist';
+    }
+
     /**
      * Return the route name for the search results action.
      *
diff --git a/module/VuFind/src/VuFind/Search/SolrAuthor/Params.php b/module/VuFind/src/VuFind/Search/SolrAuthor/Params.php
index 719ea76b85b018ac4ad54b698c57e175f40fa848..65938234da242532093394bc6f76340f2e570454 100644
--- a/module/VuFind/src/VuFind/Search/SolrAuthor/Params.php
+++ b/module/VuFind/src/VuFind/Search/SolrAuthor/Params.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search_SolrAuthor
@@ -44,7 +44,7 @@ class Params extends \VuFind\Search\Solr\Params
      * @param \Zend\StdLib\Parameters $request Parameter object representing user
      * request.
      *
-     * @return boolean True if search settings were found, false if not.
+     * @return bool True if search settings were found, false if not.
      */
     protected function initBasicSearch($request)
     {
diff --git a/module/VuFind/src/VuFind/Search/SolrAuthor/Results.php b/module/VuFind/src/VuFind/Search/SolrAuthor/Results.php
index 705d0cbe5e22beaa4b4bfb75626ef29b5255caec..c75436b2e064ffde421be5b57cb76bdc55069d90 100644
--- a/module/VuFind/src/VuFind/Search/SolrAuthor/Results.php
+++ b/module/VuFind/src/VuFind/Search/SolrAuthor/Results.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search_SolrAuthor
diff --git a/module/VuFind/src/VuFind/Search/SolrAuthorFacets/Options.php b/module/VuFind/src/VuFind/Search/SolrAuthorFacets/Options.php
index f2b80f796313803490f1a9a8f37522757541cafd..0e38efe8dd3c6fb2aff8d0d828cdca0babe2d981 100644
--- a/module/VuFind/src/VuFind/Search/SolrAuthorFacets/Options.php
+++ b/module/VuFind/src/VuFind/Search/SolrAuthorFacets/Options.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search_SolrAuthorFacets
@@ -60,6 +60,18 @@ class Options extends \VuFind\Search\Solr\Options
         $this->spellcheck = false;
     }
 
+    /**
+     * Return the route name for the facet list action. Returns false to cover
+     * unimplemented support.
+     *
+     * @return string|bool
+     */
+    public function getFacetListAction()
+    {
+        // Not applicable here; we don't want to inherit the parent class' route.
+        return false;
+    }
+
     /**
      * Return the route name for the search results action.
      *
diff --git a/module/VuFind/src/VuFind/Search/SolrAuthorFacets/Params.php b/module/VuFind/src/VuFind/Search/SolrAuthorFacets/Params.php
index 6a86791b3acdfe816614031f46010e7ebb50e8c1..00b8ffd9152a1fbc592c8d3d0f8decfb32a5aff6 100644
--- a/module/VuFind/src/VuFind/Search/SolrAuthorFacets/Params.php
+++ b/module/VuFind/src/VuFind/Search/SolrAuthorFacets/Params.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search_SolrAuthorFacets
@@ -70,7 +70,7 @@ class Params extends \VuFind\Search\Solr\Params
      * @param \Zend\StdLib\Parameters $request Parameter object representing user
      * request.
      *
-     * @return boolean True if search settings were found, false if not.
+     * @return bool True if search settings were found, false if not.
      */
     protected function initBasicSearch($request)
     {
diff --git a/module/VuFind/src/VuFind/Search/SolrAuthorFacets/Results.php b/module/VuFind/src/VuFind/Search/SolrAuthorFacets/Results.php
index 1d1e7e2c0d38e80b59a8eb96abe97b947806fdec..d842355edd0303bfd1d5e453ba2cd0a1a475efb9 100644
--- a/module/VuFind/src/VuFind/Search/SolrAuthorFacets/Results.php
+++ b/module/VuFind/src/VuFind/Search/SolrAuthorFacets/Results.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search_SolrAuthorFacets
diff --git a/module/VuFind/src/VuFind/Search/SolrCollection/Options.php b/module/VuFind/src/VuFind/Search/SolrCollection/Options.php
index c1f50e5d2724fda72f452f912c4756bec7fd278f..37f671c31c2fb987fe7b8b39fa2b5782315079e8 100644
--- a/module/VuFind/src/VuFind/Search/SolrCollection/Options.php
+++ b/module/VuFind/src/VuFind/Search/SolrCollection/Options.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search_SolrAuthor
@@ -64,6 +64,18 @@ class Options extends \VuFind\Search\Solr\Options
         $this->defaultSort = key($this->sortOptions);
     }
 
+    /**
+     * Return the route name for the facet list action. Returns false to cover
+     * unimplemented support.
+     *
+     * @return string|bool
+     */
+    public function getFacetListAction()
+    {
+        // TODO: implement support for this if needed.
+        return false;
+    }
+
     /**
      * Load all recommendation settings from the relevant ini file.  Returns an
      * associative array where the key is the location of the recommendations (top
diff --git a/module/VuFind/src/VuFind/Search/SolrCollection/Params.php b/module/VuFind/src/VuFind/Search/SolrCollection/Params.php
index 6d9c4021ea078c663e81ded62512332d63933268..068ba1dc1b96a8ba196c4053de1fc538e6ff8c78 100644
--- a/module/VuFind/src/VuFind/Search/SolrCollection/Params.php
+++ b/module/VuFind/src/VuFind/Search/SolrCollection/Params.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search_SolrAuthor
diff --git a/module/VuFind/src/VuFind/Search/SolrCollection/Results.php b/module/VuFind/src/VuFind/Search/SolrCollection/Results.php
index 448328b94ccc089fe1cf9ef801aa50b5a47c8e09..fb6a977d47843d2cf6232a735e897f5bf1896577 100644
--- a/module/VuFind/src/VuFind/Search/SolrCollection/Results.php
+++ b/module/VuFind/src/VuFind/Search/SolrCollection/Results.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search_SolrAuthor
diff --git a/module/VuFind/src/VuFind/Search/SolrReserves/Options.php b/module/VuFind/src/VuFind/Search/SolrReserves/Options.php
index 94b6cd97d5351b87c9a209bbb488f1732527ca0e..47231216d96ed097afb5ffc7449c0ff5637b3516 100644
--- a/module/VuFind/src/VuFind/Search/SolrReserves/Options.php
+++ b/module/VuFind/src/VuFind/Search/SolrReserves/Options.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search_SolrReserves
@@ -52,6 +52,17 @@ class Options extends \VuFind\Search\Solr\Options
         parent::__construct($configLoader);
     }
 
+    /**
+     * Return the route name for the facet list action. Returns false to cover
+     * unimplemented support.
+     *
+     * @return string|bool
+     */
+    public function getFacetListAction()
+    {
+        return 'search-reservesfacetlist';
+    }
+
     /**
      * Return the route name for the search results action.
      *
diff --git a/module/VuFind/src/VuFind/Search/SolrReserves/Params.php b/module/VuFind/src/VuFind/Search/SolrReserves/Params.php
index c1da472130859237bb195782d43075b9993f461e..62248b08eea0a55d7db3dc288b72d09ff12deae0 100644
--- a/module/VuFind/src/VuFind/Search/SolrReserves/Params.php
+++ b/module/VuFind/src/VuFind/Search/SolrReserves/Params.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search_SolrReserves
diff --git a/module/VuFind/src/VuFind/Search/SolrReserves/Results.php b/module/VuFind/src/VuFind/Search/SolrReserves/Results.php
index c3ad33f5ce2244f2c6513eb7164fe4c78cd07581..c611199ccafba7b283456d0a212896d3067f42b0 100644
--- a/module/VuFind/src/VuFind/Search/SolrReserves/Results.php
+++ b/module/VuFind/src/VuFind/Search/SolrReserves/Results.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search_SolrReserves
diff --git a/module/VuFind/src/VuFind/Search/SolrWeb/Options.php b/module/VuFind/src/VuFind/Search/SolrWeb/Options.php
index 9c43ab00fa730d4a8ba3537c46f050a0a322c04b..33ab274b4405db496a8aa05f3384546de136b7f1 100644
--- a/module/VuFind/src/VuFind/Search/SolrWeb/Options.php
+++ b/module/VuFind/src/VuFind/Search/SolrWeb/Options.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search_SolrWeb
@@ -71,6 +71,17 @@ class Options extends \VuFind\Search\Solr\Options
         return false;
     }
 
+    /**
+     * Return the route name for the facet list action. Returns false to cover
+     * unimplemented support.
+     *
+     * @return string|bool
+     */
+    public function getFacetListAction()
+    {
+        return 'web-facetlist';
+    }
+
     /**
      * Does this search option support the cart/book bag?
      *
diff --git a/module/VuFind/src/VuFind/Search/SolrWeb/Params.php b/module/VuFind/src/VuFind/Search/SolrWeb/Params.php
index 399b9e592b18f6c1bea4553c1c84bf59d7d6949b..b028a5ba8d6a2d073cfe7a1b24130584d5c15e79 100644
--- a/module/VuFind/src/VuFind/Search/SolrWeb/Params.php
+++ b/module/VuFind/src/VuFind/Search/SolrWeb/Params.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search_SolrWeb
diff --git a/module/VuFind/src/VuFind/Search/SolrWeb/Results.php b/module/VuFind/src/VuFind/Search/SolrWeb/Results.php
index 2b106a759791b3f289f7e008b15f0b53b8759f4c..7c1c30d1da2018146f8856ae8c6ff270b399f96b 100644
--- a/module/VuFind/src/VuFind/Search/SolrWeb/Results.php
+++ b/module/VuFind/src/VuFind/Search/SolrWeb/Results.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search_SolrWeb
diff --git a/module/VuFind/src/VuFind/Search/Summon/Options.php b/module/VuFind/src/VuFind/Search/Summon/Options.php
index 4ba7d822148313489708ffe8f6cfffcd6cb0956d..9b4382871491123c5ce6e991832beed3d8bb61ba 100644
--- a/module/VuFind/src/VuFind/Search/Summon/Options.php
+++ b/module/VuFind/src/VuFind/Search/Summon/Options.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search_Summon
@@ -158,6 +158,10 @@ class Options extends \VuFind\Search\Base\Options
         } else {
             $this->viewOptions = ['list' => 'List'];
         }
+        // Load list view for result (controls AJAX embedding vs. linking)
+        if (isset($searchSettings->List->view)) {
+            $this->listviewOption = $searchSettings->List->view;
+        }
     }
 
     /**
@@ -181,6 +185,17 @@ class Options extends \VuFind\Search\Base\Options
         return 'summon-advanced';
     }
 
+    /**
+     * Return the route name for the facet list action. Returns false to cover
+     * unimplemented support.
+     *
+     * @return string|bool
+     */
+    public function getFacetListAction()
+    {
+        return 'summon-facetlist';
+    }
+
     /**
      * Get the relevance sort override for empty searches.
      *
diff --git a/module/VuFind/src/VuFind/Search/Summon/Params.php b/module/VuFind/src/VuFind/Search/Summon/Params.php
index 17b911c0c65ffc9dfe0cf1e433d559386448defe..9adcbbc847bf17b8de1d805dd7baa80f39b77a26 100644
--- a/module/VuFind/src/VuFind/Search/Summon/Params.php
+++ b/module/VuFind/src/VuFind/Search/Summon/Params.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search_Summon
@@ -82,6 +82,18 @@ class Params extends \VuFind\Search\Base\Params
         return parent::addFacet($parts[0], $newAlias, $ored);
     }
 
+    /**
+     * Reset the current facet configuration.
+     *
+     * @return void
+     */
+    public function resetFacetConfig()
+    {
+        parent::resetFacetConfig();
+        $this->dateFacetSettings = [];
+        $this->fullFacetSettings = [];
+    }
+
     /**
      * Get the full facet settings stored by addFacet -- these may include extra
      * parameters needed by the search results class.
@@ -135,11 +147,11 @@ class Params extends \VuFind\Search\Base\Params
         // Special case -- if we have a "holdings only" or "expand query" facet,
         // we want this to always appear, even on the "no results" screen, since
         // setting this facet actually EXPANDS rather than reduces the result set.
-        if (isset($facets['holdingsOnly'])) {
-            $facets['holdingsOnly']['alwaysVisible'] = true;
-        }
-        if (isset($facets['queryExpansion'])) {
-            $facets['queryExpansion']['alwaysVisible'] = true;
+        foreach ($facets as $i => $facet) {
+            list($field) = explode(':', $facet['filter']);
+            if ($field == 'holdingsOnly' || $field == 'queryExpansion') {
+                $facets[$i]['alwaysVisible'] = true;
+            }
         }
 
         // Return modified list:
diff --git a/module/VuFind/src/VuFind/Search/Summon/Results.php b/module/VuFind/src/VuFind/Search/Summon/Results.php
index e775eaece5b9af65b4032037994e721e50652944..827b720b9a494a8e30eecb774268e0fd1b469486 100644
--- a/module/VuFind/src/VuFind/Search/Summon/Results.php
+++ b/module/VuFind/src/VuFind/Search/Summon/Results.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search_Summon
@@ -322,4 +322,59 @@ class Results extends \VuFind\Search\Base\Results
     {
         return $this->topicRecommendations;
     }
+
+    /**
+     * Get complete facet counts for several index fields
+     *
+     * @param array  $facetfields  name of the Solr fields to return facets for
+     * @param bool   $removeFilter Clear existing filters from selected fields (true)
+     * or retain them (false)?
+     * @param int    $limit        A limit for the number of facets returned, this
+     * may be useful for very large amounts of facets that can break the JSON parse
+     * method because of PHP out of memory exceptions (default = -1, no limit).
+     * @param string $facetSort    A facet sort value to use (null to retain current)
+     * @param int    $page         1 based. Offsets results by limit.
+     *
+     * @return array an array with the facet values for each index field
+     */
+    public function getPartialFieldFacets($facetfields, $removeFilter = true,
+        $limit = -1, $facetSort = null, $page = null
+    ) {
+        $params = $this->getParams();
+        $query  = $params->getQuery();
+        // No limit not implemented with Summon: cause page loop
+        if ($limit == -1) {
+            if ($page === null) {
+                $page = 1;
+            }
+            $limit = 50;
+        }
+        $params->resetFacetConfig();
+        foreach ($facetfields as $facet) {
+            $params->addFacet($facet . ',or,' . $page . ',' . $limit);
+        }
+        $params = $params->getBackendParameters();
+        $collection = $this->getSearchService()->search(
+            'Summon', $query, 0, 0, $params
+        );
+
+        $facets = $collection->getFacets();
+        $ret = [];
+        foreach ($facets as $data) {
+            if (in_array($data['displayName'], $facetfields)) {
+                $formatted = $this->formatFacetData($data);
+                $list = $formatted['counts'];
+                $ret[$data['displayName']] = [
+                    'data' => [
+                        'label' => $data['displayName'],
+                        'list' => $list,
+                    ],
+                    'more' => null
+                ];
+            }
+        }
+
+        // Send back data:
+        return $ret;
+    }
 }
diff --git a/module/VuFind/src/VuFind/Search/Tags/Options.php b/module/VuFind/src/VuFind/Search/Tags/Options.php
index 10834f068d4d514fdbb7884ddc6ee8b4e24351db..665321e7a3130349de2e56626ba8c1827fc817fe 100644
--- a/module/VuFind/src/VuFind/Search/Tags/Options.php
+++ b/module/VuFind/src/VuFind/Search/Tags/Options.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search_Tags
@@ -38,6 +38,14 @@ namespace VuFind\Search\Tags;
  */
 class Options extends \VuFind\Search\Base\Options
 {
+    /**
+     * Should we load Solr search options for a more integrated search experience
+     * or omit them to prevent confusion in multi-backend environments?
+     *
+     * @var bool
+     */
+    protected $useSolrSearchOptions = false;
+
     /**
      * Constructor
      *
@@ -46,12 +54,42 @@ class Options extends \VuFind\Search\Base\Options
     public function __construct(\VuFind\Config\PluginManager $configLoader)
     {
         parent::__construct($configLoader);
-        $this->basicHandlers = ['tags' => 'Tag'];
+        $config = $configLoader->get('config');
+        if (isset($config->Social->show_solr_options_in_tag_search)
+            && $config->Social->show_solr_options_in_tag_search
+        ) {
+            $this->useSolrSearchOptions = true;
+        }
+        $searchSettings = $this->useSolrSearchOptions
+            ? $configLoader->get($this->searchIni) : null;
+        if (isset($searchSettings->Basic_Searches)) {
+            foreach ($searchSettings->Basic_Searches as $key => $value) {
+                $this->basicHandlers[$key] = $value;
+            }
+        } else {
+            $this->basicHandlers = ['tag' => 'Tag'];
+        }
+        $this->defaultHandler = 'tag';
         $this->defaultSort = 'title';
         $this->sortOptions = [
             'title' => 'sort_title', 'author' => 'sort_author',
             'year DESC' => 'sort_year', 'year' => 'sort_year asc'
         ];
+        // Load autocomplete preference:
+        if (isset($searchSettings->Autocomplete->enabled)) {
+            $this->autocompleteEnabled = $searchSettings->Autocomplete->enabled;
+        }
+    }
+
+    /**
+     * Return the route name of the action used for performing advanced searches.
+     * Returns false if the feature is not supported.
+     *
+     * @return string|bool
+     */
+    public function getAdvancedSearchAction()
+    {
+        return $this->useSolrSearchOptions ? 'search-advanced' : null;
     }
 
     /**
@@ -61,7 +99,7 @@ class Options extends \VuFind\Search\Base\Options
      */
     public function getSearchAction()
     {
-        return 'tag-home';
+        return 'search-results';
     }
 
     /**
diff --git a/module/VuFind/src/VuFind/Search/Tags/Params.php b/module/VuFind/src/VuFind/Search/Tags/Params.php
index f1b9fafb1cbfd5cffc35b0ba4bf5e7d0301f4a8f..cdbf1908095a5a8c31bb9f34702673f65b226d61 100644
--- a/module/VuFind/src/VuFind/Search/Tags/Params.php
+++ b/module/VuFind/src/VuFind/Search/Tags/Params.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search_Tags
@@ -38,4 +38,34 @@ namespace VuFind\Search\Tags;
  */
 class Params extends \VuFind\Search\Base\Params
 {
+    /**
+     * Is this a fuzzy search?
+     *
+     * @var bool
+     */
+    protected $fuzzy = false;
+
+    /**
+     * Is this a fuzzy search?
+     *
+     * @return bool
+     */
+    public function isFuzzyTagSearch()
+    {
+        return $this->fuzzy;
+    }
+
+    /**
+     * Pull the search parameters
+     *
+     * @param \Zend\StdLib\Parameters $request Parameter object representing user
+     * request.
+     *
+     * @return void
+     */
+    public function initFromRequest($request)
+    {
+        parent::initFromRequest($request);
+        $this->fuzzy = ('true' == $request->get('fuzzy', 'false'));
+    }
 }
diff --git a/module/VuFind/src/VuFind/Search/Tags/Results.php b/module/VuFind/src/VuFind/Search/Tags/Results.php
index fd69d91e2d632dbbe1163c7b23f5b25880f19007..5cb79bf923174b1a5c8724f575c3a4be1c20f726 100644
--- a/module/VuFind/src/VuFind/Search/Tags/Results.php
+++ b/module/VuFind/src/VuFind/Search/Tags/Results.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search_Tags
@@ -40,20 +40,35 @@ use VuFind\Search\Base\Results as BaseResults;
 class Results extends BaseResults
 {
     /**
-     * Support method for performAndProcessSearch -- perform a search based on the
-     * parameters passed to the object.
+     * Process a fuzzy tag query.
      *
-     * @return void
+     * @param string $q Raw query
+     *
+     * @return string
      */
-    protected function performSearch()
+    protected function formatFuzzyQuery($q)
+    {
+        // Change unescaped asterisks to percent signs to translate more common
+        // wildcard character into format used by database.
+        return preg_replace('/(?<!\\\\)\\*/', '%', $q);
+    }
+
+    /**
+     * Return resources associated with the user tag query.
+     *
+     * @param bool $fuzzy Is this a fuzzy query or an exact match?
+     *
+     * @return array
+     */
+    protected function performTagSearch($fuzzy)
     {
         $table = $this->getTable('Tags');
-        $tag = $table->getByText($this->getParams()->getDisplayQuery(), false);
-        if (!empty($tag)) {
-            $rawResults = $tag->getResources(null, $this->getParams()->getSort());
-        } else {
-            $rawResults = [];
-        }
+        $query = $fuzzy
+            ? $this->formatFuzzyQuery($this->getParams()->getDisplayQuery())
+            : $this->getParams()->getDisplayQuery();
+        $rawResults = $table->resourceSearch(
+            $query, null, $this->getParams()->getSort(), 0, null, $fuzzy
+        );
 
         // How many results were there?
         $this->resultTotal = count($rawResults);
@@ -61,20 +76,35 @@ class Results extends BaseResults
         // Apply offset and limit if necessary!
         $limit = $this->getParams()->getLimit();
         if ($this->resultTotal > $limit) {
-            $rawResults = $tag->getResources(
-                null, $this->getParams()->getSort(), $this->getStartRecord() - 1,
-                $limit
+            $rawResults = $table->resourceSearch(
+                $query, null, $this->getParams()->getSort(),
+                $this->getStartRecord() - 1, $limit, $fuzzy
             );
         }
 
+        return $rawResults->toArray();
+    }
+
+    /**
+     * Support method for performAndProcessSearch -- perform a search based on the
+     * parameters passed to the object.
+     *
+     * @return void
+     */
+    protected function performSearch()
+    {
+        // There are two possibilities here: either we are in "fuzzy" mode because
+        // we are coming in from a search, in which case we want to do a fuzzy
+        // search that supports wildcards, or else we are coming in from a tag
+        // link, in which case we want to do an exact match.
+        $results = $this->performTagSearch($this->getParams()->isFuzzyTagSearch());
+
         // Retrieve record drivers for the selected items.
-        $recordsToRequest = [];
-        foreach ($rawResults as $row) {
-            $recordsToRequest[]
-                = ['id' => $row->record_id, 'source' => $row->source];
-        }
+        $callback = function ($row) {
+            return ['id' => $row['record_id'], 'source' => $row['source']];
+        };
         $this->results = $this->getServiceLocator()->get('VuFind\RecordLoader')
-            ->loadBatch($recordsToRequest);
+            ->loadBatch(array_map($callback, $results));
     }
 
     /**
diff --git a/module/VuFind/src/VuFind/Search/UrlQueryHelper.php b/module/VuFind/src/VuFind/Search/UrlQueryHelper.php
index d88bfc0efe59a5a2b93cc27dc2ff757de5ab105d..a112d5c97ae545eb153a1f4da8c68dcd80116339 100644
--- a/module/VuFind/src/VuFind/Search/UrlQueryHelper.php
+++ b/module/VuFind/src/VuFind/Search/UrlQueryHelper.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFind/src/VuFind/Search/WorldCat/Options.php b/module/VuFind/src/VuFind/Search/WorldCat/Options.php
index fbc293ffcf67e387d3f0d502932bd2b581fbf548..f16ccd0fb614c30bca4a9f6e95084f7b5b15b511 100644
--- a/module/VuFind/src/VuFind/Search/WorldCat/Options.php
+++ b/module/VuFind/src/VuFind/Search/WorldCat/Options.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search_WorldCat
@@ -80,6 +80,10 @@ class Options extends \VuFind\Search\Base\Options
                 $this->defaultSortByHandler[$key] = $val;
             }
         }
+        // Load list view for result (controls AJAX embedding vs. linking)
+        if (isset($searchSettings->List->view)) {
+            $this->listviewOption = $searchSettings->List->view;
+        }
     }
 
     /**
diff --git a/module/VuFind/src/VuFind/Search/WorldCat/Params.php b/module/VuFind/src/VuFind/Search/WorldCat/Params.php
index f1aca3791d7bcd22e187ebf35feba34d61fbb82f..4725e954f61d1a90e3958f7a1fd5ff541dab615c 100644
--- a/module/VuFind/src/VuFind/Search/WorldCat/Params.php
+++ b/module/VuFind/src/VuFind/Search/WorldCat/Params.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search_WorldCat
diff --git a/module/VuFind/src/VuFind/Search/WorldCat/Results.php b/module/VuFind/src/VuFind/Search/WorldCat/Results.php
index 359277aec56b66a79278b4117157ab625357002f..c7a54f1b36f12000b8158a56af52225528ee5260 100644
--- a/module/VuFind/src/VuFind/Search/WorldCat/Results.php
+++ b/module/VuFind/src/VuFind/Search/WorldCat/Results.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search_WorldCat
diff --git a/module/VuFind/src/VuFind/Search/minSO.php b/module/VuFind/src/VuFind/Search/minSO.php
index f83c62ca63a2ad080c38a10f9fd0f9097272f232..aff83f8c8169f8910eb439c6e2784a4065af8b37 100644
--- a/module/VuFind/src/VuFind/Search/minSO.php
+++ b/module/VuFind/src/VuFind/Search/minSO.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFind/src/VuFind/Service/Factory.php b/module/VuFind/src/VuFind/Service/Factory.php
index 13ad96c8f6668dfb413fc2381bd9e9f4c94484eb..6593137e45e7fc47d4a1d3e4d9f0f81c8d5bbb23 100644
--- a/module/VuFind/src/VuFind/Service/Factory.php
+++ b/module/VuFind/src/VuFind/Service/Factory.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Service
@@ -205,6 +205,9 @@ class Factory
             && $config->Cookies->limit_by_path
         ) {
             $path = $sm->get('Request')->getBasePath();
+            if (empty($path)) {
+                $path = '/';
+            }
         }
         $secure = isset($config->Cookies->only_secure)
             ? $config->Cookies->only_secure
@@ -215,6 +218,20 @@ class Factory
         return new \VuFind\Cookie\CookieManager($_COOKIE, $path, $domain, $secure);
     }
 
+    /**
+     * Construct the cover router.
+     *
+     * @param ServiceManager $sm Service manager.
+     *
+     * @return \VuFind\Cover\Router
+     */
+    public static function getCoverRouter(ServiceManager $sm)
+    {
+        $base = $sm->get('ControllerPluginManager')->get('url')
+            ->fromRoute('cover-show');
+        return new \VuFind\Cover\Router($base);
+    }
+
     /**
      * Construct the date converter.
      *
@@ -518,7 +535,7 @@ class Factory
             : (isset($config->Captcha->privateKey)
                 ? $config->Captcha->privateKey
                 : '');
-        $recaptcha = new \LosReCaptcha\Service\ReCaptcha(
+        $recaptcha = new \VuFind\Service\ReCaptcha(
             $siteKey, $secretKey, ['ssl' => true]
         );
         if (isset($config->Captcha->theme)) {
diff --git a/module/VuFind/src/VuFind/Service/ReCaptcha.php b/module/VuFind/src/VuFind/Service/ReCaptcha.php
new file mode 100644
index 0000000000000000000000000000000000000000..2a8ff0925846f56f3428cceebad3facdde8868d0
--- /dev/null
+++ b/module/VuFind/src/VuFind/Service/ReCaptcha.php
@@ -0,0 +1,77 @@
+<?php
+/**
+ * Recaptcha service
+ *
+ * PHP version 5
+ *
+ * Copyright (C) Villanova University 2016.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ * @category VuFind
+ * @package  View_Helpers
+ * @author   Chris Hallberg <crhallberg@gmail.com>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     https://vufind.org/wiki/development Wiki
+ */
+namespace VuFind\Service;
+
+/**
+ * Recaptcha service
+ *
+ * @category VuFind
+ * @package  View_Helpers
+ * @author   Chris Hallberg <crhallberg@gmail.com>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     https://vufind.org/wiki/development Wiki
+ */
+class ReCaptcha extends \LosReCaptcha\Service\ReCaptcha
+{
+    /**
+     * Get the HTML code for the captcha
+     *
+     * This method uses the public key to fetch a recaptcha form.
+     *
+     * @param null|string $name Base name for recaptcha form elements
+     *
+     * @return string
+     *
+     * @throws \ZendService\ReCaptcha\Exception
+     */
+    public function getHtml($name = null)
+    {
+        // Get standard HTML
+        $html = parent::getHtml($name);
+
+        // Override placeholder div with richer version:
+        $div = '<div class="g-recaptcha" data-sitekey="' . $this->siteKey . '"';
+        foreach ($this->options as $key => $option) {
+            if ($key == 'lang') {
+                continue;
+            }
+            $div .= ' data-' . $key . '="' . $option . '"';
+        }
+        $div .= '>';
+        $divregex = '/<div[^>]*id=[\'"]recaptcha_widget[\'"][^>]*>/';
+
+        $scriptRegex = '|<script[^>]*></script>|';
+        $scriptReplacement = '<script>/*form magic*/</script>';
+
+        return preg_replace(
+            [$divregex, $scriptRegex],
+            [$div, $scriptReplacement],
+            $html
+        );
+    }
+}
diff --git a/module/VuFind/src/VuFind/ServiceManager/AbstractPluginFactory.php b/module/VuFind/src/VuFind/ServiceManager/AbstractPluginFactory.php
index 941915471890a2a091e2b4dd70bb1cf8ae51772d..f4cc0a7b5363709ab86c5c4697d5bc1b3c738623 100644
--- a/module/VuFind/src/VuFind/ServiceManager/AbstractPluginFactory.php
+++ b/module/VuFind/src/VuFind/ServiceManager/AbstractPluginFactory.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  ServiceManager
diff --git a/module/VuFind/src/VuFind/ServiceManager/AbstractPluginManager.php b/module/VuFind/src/VuFind/ServiceManager/AbstractPluginManager.php
index 12d4ccdff9e5033b0c3b4b8ac5f54ee6dd814284..94dc060fa382857b6214c46d36dd7e918771a31d 100644
--- a/module/VuFind/src/VuFind/ServiceManager/AbstractPluginManager.php
+++ b/module/VuFind/src/VuFind/ServiceManager/AbstractPluginManager.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  ServiceManager
diff --git a/module/VuFind/src/VuFind/ServiceManager/Initializer.php b/module/VuFind/src/VuFind/ServiceManager/Initializer.php
index b7ec20c32a7567205b9730d5c3ddba96c84c0e86..834b6983551c658ec25a950362e392ee3ef63011 100644
--- a/module/VuFind/src/VuFind/ServiceManager/Initializer.php
+++ b/module/VuFind/src/VuFind/ServiceManager/Initializer.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  ServiceManager
diff --git a/module/VuFind/src/VuFind/Session/AbstractBase.php b/module/VuFind/src/VuFind/Session/AbstractBase.php
index 98f3a1dbb7b7279b160c069868bcb469e3f25ada..0adaa6914c316d11248313499220ef5ef4c7433c 100644
--- a/module/VuFind/src/VuFind/Session/AbstractBase.php
+++ b/module/VuFind/src/VuFind/Session/AbstractBase.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Session_Handlers
@@ -58,6 +58,34 @@ abstract class AbstractBase implements SaveHandlerInterface,
      */
     protected $config = null;
 
+    /**
+     * Whether writes are disabled, i.e. any changes to the session are not written
+     * to the storage
+     *
+     * @var bool
+     */
+    protected $writesDisabled = false;
+
+    /**
+     * Enable session writing (default)
+     *
+     * @return void
+     */
+    public function enableWrites()
+    {
+        $this->writesDisabled = false;
+    }
+
+    /**
+     * Disable session writing, i.e. make it read-only
+     *
+     * @return void
+     */
+    public function disableWrites()
+    {
+        $this->writesDisabled = true;
+    }
+
     /**
      * Set configuration.
      *
@@ -145,4 +173,30 @@ abstract class AbstractBase implements SaveHandlerInterface,
         // Something to keep in mind though.
         return true;
     }
+
+    /**
+     * Write function that is called when session data is to be saved.
+     *
+     * @param string $sess_id The current session ID
+     * @param string $data    The session data to write
+     *
+     * @return bool
+     */
+    public function write($sess_id, $data)
+    {
+        if ($this->writesDisabled) {
+            return true;
+        }
+        return $this->saveSession($sess_id, $data);
+    }
+
+    /**
+     * A function that is called internally when session data is to be saved.
+     *
+     * @param string $sess_id The current session ID
+     * @param string $data    The session data to write
+     *
+     * @return bool
+     */
+    abstract protected function saveSession($sess_id, $data);
 }
diff --git a/module/VuFind/src/VuFind/Session/Database.php b/module/VuFind/src/VuFind/Session/Database.php
index 23a57bb56cf036bc2f5f8d6065e63e84aca54827..7320f68ffc829e031020b6acf8771f827f88d117 100644
--- a/module/VuFind/src/VuFind/Session/Database.php
+++ b/module/VuFind/src/VuFind/Session/Database.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Session_Handlers
@@ -59,20 +59,6 @@ class Database extends AbstractBase
         }
     }
 
-    /**
-     * Write function that is called when session data is to be saved.
-     *
-     * @param string $sess_id The current session ID
-     * @param string $data    The session data to write
-     *
-     * @return bool
-     */
-    public function write($sess_id, $data)
-    {
-        $this->getTable('Session')->writeSession($sess_id, $data);
-        return true;
-    }
-
     /**
      * The destroy handler, this is executed when a session is destroyed with
      * session_destroy() and takes the session id as its only parameter.
@@ -105,4 +91,18 @@ class Database extends AbstractBase
         $this->getTable('Session')->garbageCollect($sess_maxlifetime);
         return true;
     }
+
+    /**
+     * A function that is called internally when session data is to be saved.
+     *
+     * @param string $sess_id The current session ID
+     * @param string $data    The session data to write
+     *
+     * @return bool
+     */
+    protected function saveSession($sess_id, $data)
+    {
+        $this->getTable('Session')->writeSession($sess_id, $data);
+        return true;
+    }
 }
diff --git a/module/VuFind/src/VuFind/Session/File.php b/module/VuFind/src/VuFind/Session/File.php
index fbfa84da7ba6b7bcbd52fc204b1ff5c77ea523eb..b93936576425cbad5bed561dc4b92a5d5e44fb07 100644
--- a/module/VuFind/src/VuFind/Session/File.php
+++ b/module/VuFind/src/VuFind/Session/File.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Session_Handlers
@@ -100,32 +100,6 @@ class File extends AbstractBase
         return (string)file_get_contents($sess_file);
     }
 
-    /**
-     * Write function that is called when session data is to be saved.
-     *
-     * @param string $sess_id The current session ID
-     * @param string $data    The session data to write
-     *
-     * @return bool
-     */
-    public function write($sess_id, $data)
-    {
-        $sess_file = $this->getPath() . '/sess_' . $sess_id;
-        if ($fp = fopen($sess_file, "w")) {
-            $return = fwrite($fp, $data);
-            fclose($fp);
-            if ($return !== false) {
-                return true;
-            }
-        }
-        // If we got this far, something went wrong with the file output...
-        // It is tempting to throw an exception here, but this code is called
-        // outside of the context of exception handling, so all we can do is
-        // echo a message.
-        echo 'Cannot write session to ' . $sess_file . "\n";
-        return false;
-    }
-
     /**
      * The destroy handler, this is executed when a session is destroyed with
      * session_destroy() and takes the session id as its only parameter.
@@ -164,4 +138,30 @@ class File extends AbstractBase
         }
         return true;
     }
+
+    /**
+     * A function that is called internally when session data is to be saved.
+     *
+     * @param string $sess_id The current session ID
+     * @param string $data    The session data to write
+     *
+     * @return bool
+     */
+    protected function saveSession($sess_id, $data)
+    {
+        $sess_file = $this->getPath() . '/sess_' . $sess_id;
+        if ($fp = fopen($sess_file, "w")) {
+            $return = fwrite($fp, $data);
+            fclose($fp);
+            if ($return !== false) {
+                return true;
+            }
+        }
+        // If we got this far, something went wrong with the file output...
+        // It is tempting to throw an exception here, but this code is called
+        // outside of the context of exception handling, so all we can do is
+        // echo a message.
+        echo 'Cannot write session to ' . $sess_file . "\n";
+        return false;
+    }
 }
diff --git a/module/VuFind/src/VuFind/Session/ManagerFactory.php b/module/VuFind/src/VuFind/Session/ManagerFactory.php
index 16c7ac2e74d8fa8bd6399a329922fcad0efc92d7..4210d729bd32c02c2e7b46f29c256cec98f865a2 100644
--- a/module/VuFind/src/VuFind/Session/ManagerFactory.php
+++ b/module/VuFind/src/VuFind/Session/ManagerFactory.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Session_Handlers
@@ -91,7 +91,7 @@ class ManagerFactory implements \Zend\ServiceManager\FactoryInterface
      * handler: http://us.php.net/manual/en/function.session-set-save-handler.php
      *
      * This method sets that up.
-     * 
+     *
      * @param SessionManager $sessionManager Session manager instance
      *
      * @return void
@@ -128,12 +128,29 @@ class ManagerFactory implements \Zend\ServiceManager\FactoryInterface
         // Start up the session:
         $sessionManager->start();
 
+        // Verify that any existing session has the correct path to avoid using
+        // a cookie from a service higher up in the path hierarchy.
+        $storage = new \Zend\Session\Container('SessionState', $sessionManager);
+        if (null !== $storage->cookiePath) {
+            if ($storage->cookiePath != $sessionConfig->getCookiePath()) {
+                // Disable writes temporarily to keep the existing session intact
+                $sessionManager->getSaveHandler()->disableWrites();
+                // Regenerate session ID and reset the session data
+                $sessionManager->regenerateId(false);
+                session_unset();
+                $sessionManager->getSaveHandler()->enableWrites();
+                $storage->cookiePath = $sessionConfig->getCookiePath();
+            }
+        } else {
+            $storage->cookiePath = $sessionConfig->getCookiePath();
+        }
+
         // Check if we need to immediately stop it based on the settings object
         // (which may have been informed by a controller that sessions should not
         // be written as part of the current process):
         $settings = $sm->get('VuFind\Session\Settings');
         if ($settings->setSessionManager($sessionManager)->isWriteDisabled()) {
-            $sessionManager->writeClose();
+            $sessionManager->getSaveHandler()->disableWrites();
         } else {
             // If the session is not disabled, we should set up the normal
             // shutdown function:
diff --git a/module/VuFind/src/VuFind/Session/Memcache.php b/module/VuFind/src/VuFind/Session/Memcache.php
index ca035c508eafe652b16aa0b5d1a3e77d5aa24b4c..1071c6bcf930b8bc6665c15d4e215e9a3c188337 100644
--- a/module/VuFind/src/VuFind/Session/Memcache.php
+++ b/module/VuFind/src/VuFind/Session/Memcache.php
@@ -20,7 +20,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Session_Handlers
@@ -89,21 +89,6 @@ class Memcache extends AbstractBase
         return $this->getConnection()->get("vufind_sessions/{$sess_id}");
     }
 
-    /**
-     * Write function that is called when session data is to be saved.
-     *
-     * @param string $sess_id The current session ID
-     * @param string $data    The session data to write
-     *
-     * @return bool
-     */
-    public function write($sess_id, $data)
-    {
-        return $this->getConnection()->set(
-            "vufind_sessions/{$sess_id}", $data, 0, $this->lifetime
-        );
-    }
-
     /**
      * The destroy handler, this is executed when a session is destroyed with
      * session_destroy() and takes the session id as its only parameter.
@@ -120,4 +105,19 @@ class Memcache extends AbstractBase
         // Perform Memcache-specific cleanup:
         return $this->getConnection()->delete("vufind_sessions/{$sess_id}");
     }
+
+    /**
+     * A function that is called internally when session data is to be saved.
+     *
+     * @param string $sess_id The current session ID
+     * @param string $data    The session data to write
+     *
+     * @return bool
+     */
+    protected function saveSession($sess_id, $data)
+    {
+        return $this->getConnection()->set(
+            "vufind_sessions/{$sess_id}", $data, 0, $this->lifetime
+        );
+    }
 }
diff --git a/module/VuFind/src/VuFind/Session/PluginFactory.php b/module/VuFind/src/VuFind/Session/PluginFactory.php
index cc5fcfc4e20502b68606021785341494692b3ea7..55b19de2c1e3004bc774f4e3eef6458bf3403ecb 100644
--- a/module/VuFind/src/VuFind/Session/PluginFactory.php
+++ b/module/VuFind/src/VuFind/Session/PluginFactory.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Session_Handlers
diff --git a/module/VuFind/src/VuFind/Session/PluginManager.php b/module/VuFind/src/VuFind/Session/PluginManager.php
index a444794c91ec7e43370e104b6b791b06a1d832f8..65c603da1ca6a3182f1ddca3cfa6c0e843bf3f11 100644
--- a/module/VuFind/src/VuFind/Session/PluginManager.php
+++ b/module/VuFind/src/VuFind/Session/PluginManager.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Session_Handlers
diff --git a/module/VuFind/src/VuFind/Session/Settings.php b/module/VuFind/src/VuFind/Session/Settings.php
index b3cb072f47225b46f3c67480c5c876b262e9b513..42b67e682617f679164b8aee76c519832328e347 100644
--- a/module/VuFind/src/VuFind/Session/Settings.php
+++ b/module/VuFind/src/VuFind/Session/Settings.php
@@ -20,7 +20,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Session_Handlers
diff --git a/module/VuFind/src/VuFind/SimpleXML.php b/module/VuFind/src/VuFind/SimpleXML.php
index eaebd1c7be3b2d118ae58418bf09532804f73ed8..b750fd5e8c3aa27016f472674faf645d09e30dae 100644
--- a/module/VuFind/src/VuFind/SimpleXML.php
+++ b/module/VuFind/src/VuFind/SimpleXML.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  SimpleXML
diff --git a/module/VuFind/src/VuFind/Sitemap/AbstractFile.php b/module/VuFind/src/VuFind/Sitemap/AbstractFile.php
index c8c096f370e06e0bfd9572b11e89309c69582d1a..eac5e5537229f4a03f80f6d895a814e124b8d286 100644
--- a/module/VuFind/src/VuFind/Sitemap/AbstractFile.php
+++ b/module/VuFind/src/VuFind/Sitemap/AbstractFile.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Sitemap
diff --git a/module/VuFind/src/VuFind/Sitemap/Generator.php b/module/VuFind/src/VuFind/Sitemap/Generator.php
index 8cafb4838004d48ff268011f9ac143f60e129d3a..4cfb8b5d4e66435fc4b824551a1d829c92a75bb8 100644
--- a/module/VuFind/src/VuFind/Sitemap/Generator.php
+++ b/module/VuFind/src/VuFind/Sitemap/Generator.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Sitemap
diff --git a/module/VuFind/src/VuFind/Sitemap/Sitemap.php b/module/VuFind/src/VuFind/Sitemap/Sitemap.php
index 182b09e1ff124af9805ebe36d0481a7b23272df9..6585efe13e1e0eb7adfe838593ed30a5e91e31bc 100644
--- a/module/VuFind/src/VuFind/Sitemap/Sitemap.php
+++ b/module/VuFind/src/VuFind/Sitemap/Sitemap.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Sitemap
diff --git a/module/VuFind/src/VuFind/Sitemap/SitemapIndex.php b/module/VuFind/src/VuFind/Sitemap/SitemapIndex.php
index 25fc3f629ef5dbf8e948664ddac3fb42a5bcd529..98a088fe6b2362bb8398f2172c0e08dcd252e370 100644
--- a/module/VuFind/src/VuFind/Sitemap/SitemapIndex.php
+++ b/module/VuFind/src/VuFind/Sitemap/SitemapIndex.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Sitemap
diff --git a/module/VuFind/src/VuFind/Solr/Utils.php b/module/VuFind/src/VuFind/Solr/Utils.php
index efa8c114e8a6fe5fc4ef4e75fbd8544b5b66ae43..96c04a0ab96ea9196338e26b0c8522dbb928c985 100644
--- a/module/VuFind/src/VuFind/Solr/Utils.php
+++ b/module/VuFind/src/VuFind/Solr/Utils.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Solr
diff --git a/module/VuFind/src/VuFind/Solr/Writer.php b/module/VuFind/src/VuFind/Solr/Writer.php
index e73828744bd70f44305c5695d163d6883f43c346..d6afec54e4b61fb6b8ac6680c7e05607fa36af0a 100644
--- a/module/VuFind/src/VuFind/Solr/Writer.php
+++ b/module/VuFind/src/VuFind/Solr/Writer.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Solr
diff --git a/module/VuFind/src/VuFind/Statistics/AbstractBase.php b/module/VuFind/src/VuFind/Statistics/AbstractBase.php
index 9819d7034bb193a99bea95f8c880be8535d262d1..4ca00530e4e6fd6805a9a303587091d760d7f465 100644
--- a/module/VuFind/src/VuFind/Statistics/AbstractBase.php
+++ b/module/VuFind/src/VuFind/Statistics/AbstractBase.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Statistics
@@ -178,8 +178,8 @@ abstract class AbstractBase
     /**
      * Returns a count and most used list
      *
-     * @param integer $listLength How long the top list is
-     * @param bool    $bySource   Sort data by source?
+     * @param int  $listLength How long the top list is
+     * @param bool $bySource   Sort data by source?
      *
      * @return mixed
      */
diff --git a/module/VuFind/src/VuFind/Statistics/Driver/AbstractBase.php b/module/VuFind/src/VuFind/Statistics/Driver/AbstractBase.php
index 1732b4104f2d9d6823aebd661c2244bcfb1bfa16..63228f80d56770eaad4e20ae4be5681725778810 100644
--- a/module/VuFind/src/VuFind/Statistics/Driver/AbstractBase.php
+++ b/module/VuFind/src/VuFind/Statistics/Driver/AbstractBase.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Statistics
@@ -96,8 +96,8 @@ abstract class AbstractBase
     /**
      * Returns browser usage statistics
      *
-     * @param bool    $version Include the version numbers in the list
-     * @param integer $limit   How many items to return
+     * @param bool $version Include the version numbers in the list
+     * @param int  $limit   How many items to return
      *
      * @return array
      *
diff --git a/module/VuFind/src/VuFind/Statistics/Driver/Db.php b/module/VuFind/src/VuFind/Statistics/Driver/Db.php
index 3355ac4ce349e352d83ce516a4e0f4d8adb519f3..e8715af68b09c41bd49ceffa8fef36351c372a84 100644
--- a/module/VuFind/src/VuFind/Statistics/Driver/Db.php
+++ b/module/VuFind/src/VuFind/Statistics/Driver/Db.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Statistics
@@ -78,8 +78,8 @@ class Db extends AbstractBase implements \VuFind\Db\Table\DbTableAwareInterface
     /**
      * Returns browser usage statistics
      *
-     * @param bool    $version Include the version numbers in the list
-     * @param integer $limit   How many items to return
+     * @param bool $version Include the version numbers in the list
+     * @param int  $limit   How many items to return
      *
      * @return array
      */
diff --git a/module/VuFind/src/VuFind/Statistics/Driver/Factory.php b/module/VuFind/src/VuFind/Statistics/Driver/Factory.php
index b731b702da23fcadd4eee596fa8dc794c2b05b91..ff8c7b31a044cbec2e60b7e92bc4bdc6a2b81abd 100644
--- a/module/VuFind/src/VuFind/Statistics/Driver/Factory.php
+++ b/module/VuFind/src/VuFind/Statistics/Driver/Factory.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Statistics
diff --git a/module/VuFind/src/VuFind/Statistics/Driver/File.php b/module/VuFind/src/VuFind/Statistics/Driver/File.php
index 33901cd80dc97045167bcfae9d94bb50adfc899f..62707954a70cce1f88608e91f52d253ebeea413a 100644
--- a/module/VuFind/src/VuFind/Statistics/Driver/File.php
+++ b/module/VuFind/src/VuFind/Statistics/Driver/File.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Statistics
@@ -102,8 +102,8 @@ class File extends AbstractBase
     /**
      * Convert data array to XML
      *
-     * @param array   $data Associative array of data
-     * @param integer $tab  How far the string should be indented with tabs
+     * @param array $data Associative array of data
+     * @param int   $tab  How far the string should be indented with tabs
      *
      * @return string
      */
diff --git a/module/VuFind/src/VuFind/Statistics/Driver/PluginFactory.php b/module/VuFind/src/VuFind/Statistics/Driver/PluginFactory.php
index 3435e6d13fb3b77d451b4b66adfae797580ca19b..f68608acb453f753474fb4c5647a7ccc99d2f4df 100644
--- a/module/VuFind/src/VuFind/Statistics/Driver/PluginFactory.php
+++ b/module/VuFind/src/VuFind/Statistics/Driver/PluginFactory.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Statistics
diff --git a/module/VuFind/src/VuFind/Statistics/Driver/PluginManager.php b/module/VuFind/src/VuFind/Statistics/Driver/PluginManager.php
index f1465d8831b45a34e2fd533a08ba834cc4eb7a8d..ce032a9b3568b833581e2d47a8d56d3e05cf4eb1 100644
--- a/module/VuFind/src/VuFind/Statistics/Driver/PluginManager.php
+++ b/module/VuFind/src/VuFind/Statistics/Driver/PluginManager.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Statistics
diff --git a/module/VuFind/src/VuFind/Statistics/Driver/Solr.php b/module/VuFind/src/VuFind/Statistics/Driver/Solr.php
index c8388435ad03e7204d5f6d8ac00c219add97d88c..87da4d8aadcb54dea3d391e8cc62a21df551a311 100644
--- a/module/VuFind/src/VuFind/Statistics/Driver/Solr.php
+++ b/module/VuFind/src/VuFind/Statistics/Driver/Solr.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Statistics
@@ -120,8 +120,8 @@ class Solr extends AbstractBase
     /**
      * Returns browser usage statistics
      *
-     * @param bool    $version    Include the version numbers in the list
-     * @param integer $listLength How many items to return
+     * @param bool $version    Include the version numbers in the list
+     * @param int  $listLength How many items to return
      *
      * @return array
      */
diff --git a/module/VuFind/src/VuFind/Statistics/Record.php b/module/VuFind/src/VuFind/Statistics/Record.php
index 2165776d9ddff526178dc82764c3584025d141d6..8446a030739c29b2ff7ed34327f9b638239556dd 100644
--- a/module/VuFind/src/VuFind/Statistics/Record.php
+++ b/module/VuFind/src/VuFind/Statistics/Record.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Statistics
@@ -61,8 +61,8 @@ class Record extends AbstractBase
      * Returns a set of basic statistics including total records
      * and most commonly viewed records.
      *
-     * @param integer $listLength How long the top list is
-     * @param bool    $bySource   Sort record views by source?
+     * @param int  $listLength How long the top list is
+     * @param bool $bySource   Sort record views by source?
      *
      * @return array
      */
diff --git a/module/VuFind/src/VuFind/Statistics/Search.php b/module/VuFind/src/VuFind/Statistics/Search.php
index ddb545685f9e2bc634f35d5376b2a22681451091..7e1d9c89e689f3cfaba1e220d71b69829894ce63 100644
--- a/module/VuFind/src/VuFind/Statistics/Search.php
+++ b/module/VuFind/src/VuFind/Statistics/Search.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Statistics
@@ -62,8 +62,8 @@ class Search extends AbstractBase
      * Returns a set of basic statistics including total searches,
      * number of empty searches and most popular search terms.
      *
-     * @param integer $listLength Number of top searches to return
-     * @param bool    $bySource   Separate searches by search source?
+     * @param int  $listLength Number of top searches to return
+     * @param bool $bySource   Separate searches by search source?
      *
      * @return array
      */
diff --git a/module/VuFind/src/VuFind/Tags.php b/module/VuFind/src/VuFind/Tags.php
index 0cb60aca4255f58270f33e9ae4969c120c356b21..d36dd97d4c1c7a5f51a7106cfdfbbb44b7f179c4 100644
--- a/module/VuFind/src/VuFind/Tags.php
+++ b/module/VuFind/src/VuFind/Tags.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tags
diff --git a/module/VuFind/src/VuFind/View/Helper/AbstractLayoutClass.php b/module/VuFind/src/VuFind/View/Helper/AbstractLayoutClass.php
index 774ea9ab2b7d023fdf3d1e2d0ce191dc3198d8d4..af9b84e6c0bccbbb87a589a585c255a81516922e 100644
--- a/module/VuFind/src/VuFind/View/Helper/AbstractLayoutClass.php
+++ b/module/VuFind/src/VuFind/View/Helper/AbstractLayoutClass.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  View_Helpers
diff --git a/module/VuFind/src/VuFind/View/Helper/AbstractSearch.php b/module/VuFind/src/VuFind/View/Helper/AbstractSearch.php
index 2cd5e4f6300cd750d556a123c1f42e13bc7447eb..77df6b724e707f2234ca5cb35f119f3f7330637c 100644
--- a/module/VuFind/src/VuFind/View/Helper/AbstractSearch.php
+++ b/module/VuFind/src/VuFind/View/Helper/AbstractSearch.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  View_Helpers
diff --git a/module/VuFind/src/VuFind/View/Helper/Bootstrap3/Factory.php b/module/VuFind/src/VuFind/View/Helper/Bootstrap3/Factory.php
index 563cb6baf69ce4da923de95dd7f29d9e459b4976..fb2ad863b1e190711a6679eb2da4d99b937e61ce 100644
--- a/module/VuFind/src/VuFind/View/Helper/Bootstrap3/Factory.php
+++ b/module/VuFind/src/VuFind/View/Helper/Bootstrap3/Factory.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  View_Helpers
diff --git a/module/VuFind/src/VuFind/View/Helper/Bootstrap3/Flashmessages.php b/module/VuFind/src/VuFind/View/Helper/Bootstrap3/Flashmessages.php
index 1399a8bd60cedd0cab079684c4eace8d669b2309..1a4124993e6fb787b0a25ddfb370ba4dc1c0f07c 100644
--- a/module/VuFind/src/VuFind/View/Helper/Bootstrap3/Flashmessages.php
+++ b/module/VuFind/src/VuFind/View/Helper/Bootstrap3/Flashmessages.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  View_Helpers
diff --git a/module/VuFind/src/VuFind/View/Helper/Bootstrap3/Highlight.php b/module/VuFind/src/VuFind/View/Helper/Bootstrap3/Highlight.php
index ed29eb76adade31e828188a6c92015e445721ce2..f02155c4dafe45aebe4383b5a9024ad54d4010d7 100644
--- a/module/VuFind/src/VuFind/View/Helper/Bootstrap3/Highlight.php
+++ b/module/VuFind/src/VuFind/View/Helper/Bootstrap3/Highlight.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  View_Helpers
diff --git a/module/VuFind/src/VuFind/View/Helper/Bootstrap3/LayoutClass.php b/module/VuFind/src/VuFind/View/Helper/Bootstrap3/LayoutClass.php
index ab1724316df99cbe795116693089c8e143e1c94d..92cc9f527caf1134a644e40067e530d2c414eb44 100644
--- a/module/VuFind/src/VuFind/View/Helper/Bootstrap3/LayoutClass.php
+++ b/module/VuFind/src/VuFind/View/Helper/Bootstrap3/LayoutClass.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  View_Helpers
diff --git a/module/VuFind/src/VuFind/View/Helper/Bootstrap3/Recaptcha.php b/module/VuFind/src/VuFind/View/Helper/Bootstrap3/Recaptcha.php
index 1256ccf3249fb04cf85022134fe1e7c1de5f301b..4a33ee68efea0f3996d17a6787214df079562e51 100644
--- a/module/VuFind/src/VuFind/View/Helper/Bootstrap3/Recaptcha.php
+++ b/module/VuFind/src/VuFind/View/Helper/Bootstrap3/Recaptcha.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  View_Helpers
diff --git a/module/VuFind/src/VuFind/View/Helper/Bootstrap3/Search.php b/module/VuFind/src/VuFind/View/Helper/Bootstrap3/Search.php
index 98981ce2e609aa5d8762739da507678fbcec9dda..0cd60f9f0b4124ed42bc28ef0273ecf2c7eef0f9 100644
--- a/module/VuFind/src/VuFind/View/Helper/Bootstrap3/Search.php
+++ b/module/VuFind/src/VuFind/View/Helper/Bootstrap3/Search.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  View_Helpers
diff --git a/module/VuFind/src/VuFind/View/Helper/Root/AccountCapabilities.php b/module/VuFind/src/VuFind/View/Helper/Root/AccountCapabilities.php
index 21182b98d66dcce54dd204061c3c4e672674e665..b13a774656779548ce55b91d8c61b264bca66406 100644
--- a/module/VuFind/src/VuFind/View/Helper/Root/AccountCapabilities.php
+++ b/module/VuFind/src/VuFind/View/Helper/Root/AccountCapabilities.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  View_Helpers
diff --git a/module/VuFind/src/VuFind/View/Helper/Root/AddEllipsis.php b/module/VuFind/src/VuFind/View/Helper/Root/AddEllipsis.php
index 990de77c06c18feec4a19e62b5b6a6f7d0a4a37d..3a4f4dc432a153820e5e8ad1c06cc8020a034922 100644
--- a/module/VuFind/src/VuFind/View/Helper/Root/AddEllipsis.php
+++ b/module/VuFind/src/VuFind/View/Helper/Root/AddEllipsis.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  View_Helpers
diff --git a/module/VuFind/src/VuFind/View/Helper/Root/AddThis.php b/module/VuFind/src/VuFind/View/Helper/Root/AddThis.php
index 581040f71ab0726827f9264141ee06abca148bfa..fc7c85ad288a055941fba41787109be640ca75de 100644
--- a/module/VuFind/src/VuFind/View/Helper/Root/AddThis.php
+++ b/module/VuFind/src/VuFind/View/Helper/Root/AddThis.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  View_Helpers
diff --git a/module/VuFind/src/VuFind/View/Helper/Root/AlphaBrowse.php b/module/VuFind/src/VuFind/View/Helper/Root/AlphaBrowse.php
index 76f48db697190d84e957995036e1795d02a0d0e2..d0f83d6453a0957dabee6537e554060edf1df826 100644
--- a/module/VuFind/src/VuFind/View/Helper/Root/AlphaBrowse.php
+++ b/module/VuFind/src/VuFind/View/Helper/Root/AlphaBrowse.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  View_Helpers
diff --git a/module/VuFind/src/VuFind/View/Helper/Root/Auth.php b/module/VuFind/src/VuFind/View/Helper/Root/Auth.php
index 95f6eb59be2dc6f4163158c1de1fb473c688a030..3dba8a89222950a47db73a34aa92aa2c517b8da5 100644
--- a/module/VuFind/src/VuFind/View/Helper/Root/Auth.php
+++ b/module/VuFind/src/VuFind/View/Helper/Root/Auth.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  View_Helpers
diff --git a/module/VuFind/src/VuFind/View/Helper/Root/Browse.php b/module/VuFind/src/VuFind/View/Helper/Root/Browse.php
index cad9b2b01f3115d3e9a5f7b4c1f20b49f5ce2562..a1b2accb8d2625170f9d3fe901fafdfbdc25511f 100644
--- a/module/VuFind/src/VuFind/View/Helper/Root/Browse.php
+++ b/module/VuFind/src/VuFind/View/Helper/Root/Browse.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  View_Helpers
diff --git a/module/VuFind/src/VuFind/View/Helper/Root/Cart.php b/module/VuFind/src/VuFind/View/Helper/Root/Cart.php
index 2d63a06153c701cf8c56cbfc6c1410053a4ce05b..28c02aabc9cbb27b62892f419817e8e67e804594 100644
--- a/module/VuFind/src/VuFind/View/Helper/Root/Cart.php
+++ b/module/VuFind/src/VuFind/View/Helper/Root/Cart.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  View_Helpers
diff --git a/module/VuFind/src/VuFind/View/Helper/Root/Citation.php b/module/VuFind/src/VuFind/View/Helper/Root/Citation.php
index 6d0f81ef77a8108f0b1505100a78b6dad4f7bd4b..14550d90995bc84b3ea3167ac6afd72d0c13951a 100644
--- a/module/VuFind/src/VuFind/View/Helper/Root/Citation.php
+++ b/module/VuFind/src/VuFind/View/Helper/Root/Citation.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  View_Helpers
@@ -502,7 +502,7 @@ class Citation extends \Zend\View\Helper\AbstractHelper
      *
      * @param string $string String to test.
      *
-     * @return boolean       Does string end in punctuation?
+     * @return bool          Does string end in punctuation?
      */
     protected function isPunctuated($string)
     {
diff --git a/module/VuFind/src/VuFind/View/Helper/Root/ContentLoader.php b/module/VuFind/src/VuFind/View/Helper/Root/ContentLoader.php
index bd45237c943432172819426cda95f4b64e77bfdb..b8a70240b0db3d601f5b554303a48e2acf846e7a 100644
--- a/module/VuFind/src/VuFind/View/Helper/Root/ContentLoader.php
+++ b/module/VuFind/src/VuFind/View/Helper/Root/ContentLoader.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  View_Helpers
diff --git a/module/VuFind/src/VuFind/View/Helper/Root/Context.php b/module/VuFind/src/VuFind/View/Helper/Root/Context.php
index 25ba1f6053bf541b5691a153c4b006aacec8f7de..8381ef6c324da55c21a458b1ef29437f015fe404 100644
--- a/module/VuFind/src/VuFind/View/Helper/Root/Context.php
+++ b/module/VuFind/src/VuFind/View/Helper/Root/Context.php
@@ -19,7 +19,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  View_Helpers
diff --git a/module/VuFind/src/VuFind/View/Helper/Root/CurrentPath.php b/module/VuFind/src/VuFind/View/Helper/Root/CurrentPath.php
index 7c4ba1f9cc189f921fb8350c29b55ba110358530..3a89af1460281815ef4d88d6bbee5965dace6b40 100644
--- a/module/VuFind/src/VuFind/View/Helper/Root/CurrentPath.php
+++ b/module/VuFind/src/VuFind/View/Helper/Root/CurrentPath.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  View_Helpers
diff --git a/module/VuFind/src/VuFind/View/Helper/Root/DateTime.php b/module/VuFind/src/VuFind/View/Helper/Root/DateTime.php
index a15816c42e8f581df175a70fd820ae5ea3de064d..7f4ad0557d575deae644661ed9b1ed909ad4e280 100644
--- a/module/VuFind/src/VuFind/View/Helper/Root/DateTime.php
+++ b/module/VuFind/src/VuFind/View/Helper/Root/DateTime.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  View_Helpers
diff --git a/module/VuFind/src/VuFind/View/Helper/Root/DisplayLanguageOption.php b/module/VuFind/src/VuFind/View/Helper/Root/DisplayLanguageOption.php
index 5bcd8fcb27b08e21ff9b65369fdf4bd5473c402d..3db515ca754058a20b8a0288bb8634bfe1bdb35b 100644
--- a/module/VuFind/src/VuFind/View/Helper/Root/DisplayLanguageOption.php
+++ b/module/VuFind/src/VuFind/View/Helper/Root/DisplayLanguageOption.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  View_Helpers
diff --git a/module/VuFind/src/VuFind/View/Helper/Root/Export.php b/module/VuFind/src/VuFind/View/Helper/Root/Export.php
index 88c820531f46deeef3ae279c6a7cc22080e79b3d..a6f230d773a16539f1cb7d1f5634cee0504eefe8 100644
--- a/module/VuFind/src/VuFind/View/Helper/Root/Export.php
+++ b/module/VuFind/src/VuFind/View/Helper/Root/Export.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  View_Helpers
diff --git a/module/VuFind/src/VuFind/View/Helper/Root/Factory.php b/module/VuFind/src/VuFind/View/Helper/Root/Factory.php
index 161cc89d78741179c5a7402780b37c383ca35187..340d1b8154ffc28ba3e5121b5a6f8e13b59bdc5b 100644
--- a/module/VuFind/src/VuFind/View/Helper/Root/Factory.php
+++ b/module/VuFind/src/VuFind/View/Helper/Root/Factory.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  View_Helpers
@@ -202,6 +202,21 @@ class Factory
         return new Flashmessages($messenger);
     }
 
+    /**
+     * Construct the GeoCoords helper.
+     *
+     * @param ServiceManager $sm Service manager.
+     *
+     * @return GeoCoords
+     */
+    public static function getGeoCoords(ServiceManager $sm)
+    {
+        $config = $sm->getServiceLocator()->get('VuFind\Config')->get('searches');
+        $coords = isset($config->MapSelection->default_coordinates)
+            ? $config->MapSelection->default_coordinates : false;
+        return new GeoCoords($coords);
+    }
+
     /**
      * Construct the GoogleAnalytics helper.
      *
@@ -234,7 +249,9 @@ class Factory
         $customVars = isset($config->Piwik->custom_variables)
             ? $config->Piwik->custom_variables
             : false;
-        return new Piwik($url, $siteId, $customVars);
+        $request = $sm->getServiceLocator()->get('Request');
+        $router = $sm->getServiceLocator()->get('Router');
+        return new Piwik($url, $siteId, $customVars, $router, $request);
     }
 
     /**
@@ -367,9 +384,13 @@ class Factory
      */
     public static function getRecord(ServiceManager $sm)
     {
-        return new Record(
+        $helper = new Record(
             $sm->getServiceLocator()->get('VuFind\Config')->get('config')
         );
+        $helper->setCoverRouter(
+            $sm->getServiceLocator()->get('VuFind\Cover\Router')
+        );
+        return $helper;
     }
 
     /**
diff --git a/module/VuFind/src/VuFind/View/Helper/Root/Feedback.php b/module/VuFind/src/VuFind/View/Helper/Root/Feedback.php
index 7b7c7b8b7d86b68c63c08f7c965d19aaef10e12f..cbdf45338c668213091e9e6d241f21ba92acd335 100644
--- a/module/VuFind/src/VuFind/View/Helper/Root/Feedback.php
+++ b/module/VuFind/src/VuFind/View/Helper/Root/Feedback.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  View_Helpers
@@ -60,7 +60,7 @@ class Feedback extends \Zend\View\Helper\AbstractHelper
     /**
      * This will retrieve the config for whether or not the tab is enabled.
      *
-     * @return boolean
+     * @return bool
      */
     public function tabEnabled()
     {
diff --git a/module/VuFind/src/VuFind/View/Helper/Root/Flashmessages.php b/module/VuFind/src/VuFind/View/Helper/Root/Flashmessages.php
index 11b846d15f228c4a2fd797f638fc87658672b0a4..ac7ec7a7366253cbd8a5e80deb71f21e339c77a5 100644
--- a/module/VuFind/src/VuFind/View/Helper/Root/Flashmessages.php
+++ b/module/VuFind/src/VuFind/View/Helper/Root/Flashmessages.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  View_Helpers
diff --git a/module/VuFind/src/VuFind/View/Helper/Root/GeoCoords.php b/module/VuFind/src/VuFind/View/Helper/Root/GeoCoords.php
new file mode 100644
index 0000000000000000000000000000000000000000..ed372fab0bcaca39dff9dfc4920a7ff119f881da
--- /dev/null
+++ b/module/VuFind/src/VuFind/View/Helper/Root/GeoCoords.php
@@ -0,0 +1,115 @@
+<?php
+/**
+ * GeoCoords view helper
+ *
+ * PHP version 5
+ *
+ * Copyright (C) Villanova University 2010.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ * @category VuFind
+ * @package  View_Helpers
+ * @author   Leila Gonzales <lmg@agiweb.org>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     https://vufind.org Main Site
+ */
+namespace VuFind\View\Helper\Root;
+use VuFind\Search\Base\Options;
+
+/**
+ * GeoCoords view helper
+ *
+ * @category VuFind
+ * @package  View_Helpers
+ * @author   Leila Gonzales <lmg@agiweb.org>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     https://vufind.org/wiki/development Wiki
+ */
+class GeoCoords extends \Zend\View\Helper\AbstractHelper
+{
+    /**
+     * Is Map Search enabled?
+     *
+     * @var bool
+     */
+    protected $enabled;
+
+    /**
+     * Default coordinates 
+     *
+     * @var string
+     */
+    protected $coords;
+
+    /**
+     * Get geoField variable name
+     *
+     * @var string
+     */
+    protected $geoField = 'location_geo';
+
+    /**
+     * Constructor
+     *
+     * @param string $coords Default coordinates
+     */
+    public function __construct($coords)
+    {
+        $this->coords = $coords;
+    }
+
+    /**
+     * Check if the relevant recommendation module is enabled; if not, there is no
+     * point in generating a search link. Note that right now we are assuming it is
+     * set up as a default top recommendation; this may need to be made more
+     * flexible in future to account for more use cases.
+     *
+     * @param array $settings Recommendation settings
+     *
+     * @return bool
+     */
+    protected function recommendationEnabled($settings)
+    {
+        if (isset($settings['top']) && is_array($settings['top'])) {
+            foreach ($settings['top'] as $setting) {
+                $parts = explode(':', $setting);
+                if (strtolower($parts[0]) === 'mapselection') {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Get search URL if geo search is enabled for the specified search class ID,
+     * false if disabled.
+     *
+     * @param Options $options Search options
+     *
+     * @return string|bool
+     */
+    public function getSearchUrl(Options $options)
+    {
+        // If the relevant module is disabled, bail out now:
+        if (!$this->recommendationEnabled($options->getRecommendationSettings())) {
+            return false;
+        }
+        $urlHelper = $this->getView()->plugin('url');
+        return $urlHelper('search-results')
+            . '?filter[]=' . urlencode($this->geoField)
+            . ':Intersects(ENVELOPE(' . urlencode($this->coords) . '))';
+    }
+}
diff --git a/module/VuFind/src/VuFind/View/Helper/Root/GoogleAnalytics.php b/module/VuFind/src/VuFind/View/Helper/Root/GoogleAnalytics.php
index 326860040b504359008682096bc74c031616cd40..78ae8ccdbce137ccdda2aa95f159aed970c86895 100644
--- a/module/VuFind/src/VuFind/View/Helper/Root/GoogleAnalytics.php
+++ b/module/VuFind/src/VuFind/View/Helper/Root/GoogleAnalytics.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  View_Helpers
@@ -67,9 +67,11 @@ class GoogleAnalytics extends \Zend\View\Helper\AbstractHelper
     /**
      * Returns GA code (if active) or empty string if not.
      *
+     * @param string $customUrl override URL to send to Google Analytics
+     *
      * @return string
      */
-    public function __invoke()
+    public function __invoke($customUrl = false)
     {
         if (!$this->key) {
             return '';
@@ -77,9 +79,13 @@ class GoogleAnalytics extends \Zend\View\Helper\AbstractHelper
         if (!$this->universal) {
             $code = 'var key = "' . $this->key . '";' . "\n"
                 . "var _gaq = _gaq || [];\n"
-                . "_gaq.push(['_setAccount', key]);\n"
-                . "_gaq.push(['_trackPageview']);\n"
-                . "(function() {\n"
+                . "_gaq.push(['_setAccount', key]);\n";
+            if ($customUrl) {
+                $code .= "_gaq.push(['_trackPageview', '" . $customUrl . "']);\n";
+            } else {
+                $code .= "_gaq.push(['_trackPageview']);\n";
+            }
+            $code .= "(function() {\n"
                 . "var ga = document.createElement('script'); "
                 . "ga.type = 'text/javascript'; ga.async = true;\n"
                 . "ga.src = ('https:' == document.location.protocol ? "
diff --git a/module/VuFind/src/VuFind/View/Helper/Root/HelpText.php b/module/VuFind/src/VuFind/View/Helper/Root/HelpText.php
index dabb2044ecbbb15d6ca4e5fa76ee6f7321a9656e..e7bdbbbfd716d4154773b34557d6632521249dbb 100644
--- a/module/VuFind/src/VuFind/View/Helper/Root/HelpText.php
+++ b/module/VuFind/src/VuFind/View/Helper/Root/HelpText.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  View_Helpers
diff --git a/module/VuFind/src/VuFind/View/Helper/Root/Highlight.php b/module/VuFind/src/VuFind/View/Helper/Root/Highlight.php
index 1b32105b5995bf496b087e7f50708912a544f0ed..33a0b088ddd16621a59024391efe529cf44841ef 100644
--- a/module/VuFind/src/VuFind/View/Helper/Root/Highlight.php
+++ b/module/VuFind/src/VuFind/View/Helper/Root/Highlight.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  View_Helpers
diff --git a/module/VuFind/src/VuFind/View/Helper/Root/HistoryLabel.php b/module/VuFind/src/VuFind/View/Helper/Root/HistoryLabel.php
index bd55fe7ae4e46915777587ec4f26af01d8e385d7..3d1af23ba24db8c53e23ade9d495077273a79190 100644
--- a/module/VuFind/src/VuFind/View/Helper/Root/HistoryLabel.php
+++ b/module/VuFind/src/VuFind/View/Helper/Root/HistoryLabel.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  View_Helpers
diff --git a/module/VuFind/src/VuFind/View/Helper/Root/Ils.php b/module/VuFind/src/VuFind/View/Helper/Root/Ils.php
index 221f79477b5719a54926373d17a3cf33cded780f..eb3d913c26b9041678e2f11039587e1a69105120 100644
--- a/module/VuFind/src/VuFind/View/Helper/Root/Ils.php
+++ b/module/VuFind/src/VuFind/View/Helper/Root/Ils.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  View_Helpers
diff --git a/module/VuFind/src/VuFind/View/Helper/Root/JqueryValidation.php b/module/VuFind/src/VuFind/View/Helper/Root/JqueryValidation.php
index f37a60414ee887f5a4721d11f87c68b83bc0cf2a..c80c6c94f5bb8db004216024b5546ac379cf9374 100644
--- a/module/VuFind/src/VuFind/View/Helper/Root/JqueryValidation.php
+++ b/module/VuFind/src/VuFind/View/Helper/Root/JqueryValidation.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  View_Helpers
diff --git a/module/VuFind/src/VuFind/View/Helper/Root/JsTranslations.php b/module/VuFind/src/VuFind/View/Helper/Root/JsTranslations.php
index 27095b1765e6d33e6bfd5ef9985223015619dcb0..517ccffbabd1722bccf92873f542b559472d9188 100644
--- a/module/VuFind/src/VuFind/View/Helper/Root/JsTranslations.php
+++ b/module/VuFind/src/VuFind/View/Helper/Root/JsTranslations.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  View_Helpers
diff --git a/module/VuFind/src/VuFind/View/Helper/Root/KeepAlive.php b/module/VuFind/src/VuFind/View/Helper/Root/KeepAlive.php
index e7cd17e80a686077fdcc4d43cd2587f73337a4ec..2ab79a4be25c9f9b83be3743b908a7341f49288d 100644
--- a/module/VuFind/src/VuFind/View/Helper/Root/KeepAlive.php
+++ b/module/VuFind/src/VuFind/View/Helper/Root/KeepAlive.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  View_Helpers
diff --git a/module/VuFind/src/VuFind/View/Helper/Root/LocalizedNumber.php b/module/VuFind/src/VuFind/View/Helper/Root/LocalizedNumber.php
index bcf9059859b13b90b93200fc290ae911a15cca7d..64ab9e31152d8fe0d7c707603af9eaa4e3c4d221 100644
--- a/module/VuFind/src/VuFind/View/Helper/Root/LocalizedNumber.php
+++ b/module/VuFind/src/VuFind/View/Helper/Root/LocalizedNumber.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  View_Helpers
diff --git a/module/VuFind/src/VuFind/View/Helper/Root/OpenUrl.php b/module/VuFind/src/VuFind/View/Helper/Root/OpenUrl.php
index b9a4ad8d14ee6a71b91fd1f95f203d752a814089..6f1393deedc64b576f2b0ef3a79b4c96da4e0e96 100644
--- a/module/VuFind/src/VuFind/View/Helper/Root/OpenUrl.php
+++ b/module/VuFind/src/VuFind/View/Helper/Root/OpenUrl.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  View_Helpers
diff --git a/module/VuFind/src/VuFind/View/Helper/Root/Piwik.php b/module/VuFind/src/VuFind/View/Helper/Root/Piwik.php
index 7f15b4df004a9a7f133ec180da6b7c6b43888cf8..45266fe9fc22ae727bdec7ed06782940c323cff7 100644
--- a/module/VuFind/src/VuFind/View/Helper/Root/Piwik.php
+++ b/module/VuFind/src/VuFind/View/Helper/Root/Piwik.php
@@ -4,7 +4,7 @@
  *
  * PHP version 5
  *
- * Copyright (C) The National Library of Finland 2014.
+ * Copyright (C) The National Library of Finland 2014-2016.
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License version 2,
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  View_Helpers
@@ -59,15 +59,54 @@ class Piwik extends \Zend\View\Helper\AbstractHelper
      */
     protected $customVars;
 
+    /**
+     * Request object
+     *
+     * @var Zend\Http\PhpEnvironment\Request
+     */
+    protected $request;
+
+    /**
+     * Router object
+     *
+     * @var Zend\Mvc\Router\Http\RouteMatch
+     */
+    protected $router;
+
+    /**
+     * Whether the tracker was initialized from lightbox.
+     *
+     * @var bool
+     */
+    protected $lightbox;
+
+    /**
+     * Additional parameters
+     *
+     * @var array
+     */
+    protected $params;
+
+    /**
+     * A timestamp used to identify the init function to avoid name clashes when
+     * opening lightboxes.
+     *
+     * @var int
+     */
+    protected $timestamp;
+
     /**
      * Constructor
      *
-     * @param string|bool $url        Piwik address (false if disabled)
-     * @param int         $siteId     Piwik site ID
-     * @param bool        $customVars Whether to track additional information in
-     * custom variables
+     * @param string|bool                      $url        Piwik address
+     * (false if disabled)
+     * @param int                              $siteId     Piwik site ID
+     * @param bool                             $customVars Whether to track
+     * additional information in custom variables
+     * @param Zend\Mvc\Router\Http\RouteMatch  $router     Request
+     * @param Zend\Http\PhpEnvironment\Request $request    Request
      */
-    public function __construct($url, $siteId, $customVars)
+    public function __construct($url, $siteId, $customVars, $router, $request)
     {
         $this->url = $url;
         if ($url && substr($url, -1) != '/') {
@@ -75,22 +114,35 @@ class Piwik extends \Zend\View\Helper\AbstractHelper
         }
         $this->siteId = $siteId;
         $this->customVars = $customVars;
+        $this->router = $router;
+        $this->request = $request;
+        $this->timestamp = round(microtime(true) * 1000);
     }
 
     /**
      * Returns Piwik code (if active) or empty string if not.
      *
+     * @param array $params Parameters
+     *
      * @return string
      */
-    public function __invoke()
+    public function __invoke($params = null)
     {
         if (!$this->url) {
             return '';
         }
 
-        if ($results = $this->getSearchResults()) {
+        $this->params = $params;
+        if (isset($this->params['lightbox'])) {
+            $this->lightbox = $this->params['lightbox'];
+        }
+
+        $results = $this->getSearchResults();
+        if ($results && ($combinedResults = $this->getCombinedSearchResults())) {
+            $code = $this->trackCombinedSearch($results, $combinedResults);
+        } elseif ($results) {
             $code = $this->trackSearch($results);
-        } else if ($recordDriver = $this->getRecordDriver()) {
+        } elseif ($recordDriver = $this->getRecordDriver()) {
             $code = $this->trackRecordPage($recordDriver);
         } else {
             $code = $this->trackPageView();
@@ -109,7 +161,9 @@ class Piwik extends \Zend\View\Helper\AbstractHelper
      */
     protected function trackSearch($results)
     {
-        $customVars = $this->getSearchCustomVars($results);
+        $customVars = $this->lightbox
+            ? $this->getLightboxCustomVars()
+            : $this->getSearchCustomVars($results);
 
         $code = $this->getOpeningTrackingCode();
         $code .= $this->getCustomVarsCode($customVars);
@@ -119,6 +173,28 @@ class Piwik extends \Zend\View\Helper\AbstractHelper
         return $code;
     }
 
+    /**
+     * Track a Combined Search
+     *
+     * @param VuFind\Search\Base\Results $results         Search Results
+     * @param array                      $combinedResults Combined Search Results
+     *
+     * @return string Tracking Code
+     */
+    protected function trackCombinedSearch($results, $combinedResults)
+    {
+        $customVars = $this->lightbox
+            ? $this->getLightboxCustomVars()
+            : $this->getSearchCustomVars($results);
+
+        $code = $this->getOpeningTrackingCode();
+        $code .= $this->getCustomVarsCode($customVars);
+        $code .= $this->getTrackCombinedSearchCode($results, $combinedResults);
+        $code .= $this->getClosingTrackingCode();
+
+        return $code;
+    }
+
     /**
      * Track a Record View
      *
@@ -128,7 +204,9 @@ class Piwik extends \Zend\View\Helper\AbstractHelper
      */
     protected function trackRecordPage($recordDriver)
     {
-        $customVars = $this->getRecordPageCustomVars($recordDriver);
+        $customVars = $this->lightbox
+            ? $this->getLightboxCustomVars()
+            : $this->getRecordPageCustomVars($recordDriver);
 
         $code = $this->getOpeningTrackingCode();
         $code .= $this->getCustomVarsCode($customVars);
@@ -145,7 +223,9 @@ class Piwik extends \Zend\View\Helper\AbstractHelper
      */
     protected function trackPageView()
     {
-        $customVars = $this->getGenericCustomVars();
+        $customVars = $this->lightbox
+            ? $this->getLightboxCustomVars()
+            : $this->getGenericCustomVars();
 
         $code = $this->getOpeningTrackingCode();
         $code .= $this->getCustomVarsCode($customVars);
@@ -177,6 +257,29 @@ class Piwik extends \Zend\View\Helper\AbstractHelper
         return null;
     }
 
+    /**
+     * Get Combined Search Results if on a Results Page
+     *
+     * @return array|null Array of search results or null if not on a combined search
+     * page
+     */
+    protected function getCombinedSearchResults()
+    {
+        $viewModel = $this->getView()->plugin('view_model');
+        $current = $viewModel->getCurrent();
+        if (null === $current) {
+            return null;
+        }
+        $children = $current->getChildren();
+        if (isset($children[0])) {
+            $results = $children[0]->getVariable('combinedResults');
+            if (is_array($results)) {
+                return $results;
+            }
+        }
+        return null;
+    }
+
     /**
      * Get Record Driver if on a Record Page
      *
@@ -270,6 +373,16 @@ class Piwik extends \Zend\View\Helper\AbstractHelper
         ];
     }
 
+    /**
+     * Get Custom Variables for lightbox actions
+     *
+     * @return array Associative array of custom variables
+     */
+    protected function getLightboxCustomVars()
+    {
+        return [];
+    }
+
     /**
      * Get Custom Variables for a Generic Page View
      *
@@ -289,17 +402,38 @@ class Piwik extends \Zend\View\Helper\AbstractHelper
     {
         return <<<EOT
 
-function initVuFindPiwikTracker(){
+function initVuFindPiwikTracker{$this->timestamp}(){
     var VuFindPiwikTracker = Piwik.getTracker();
 
     VuFindPiwikTracker.setSiteId({$this->siteId});
     VuFindPiwikTracker.setTrackerUrl('{$this->url}piwik.php');
-    VuFindPiwikTracker.setCustomUrl(location.protocol + '//'
-        + location.host + location.pathname);
+    VuFindPiwikTracker.setCustomUrl('{$this->getCustomUrl()}');
 
 EOT;
     }
 
+    /**
+     * Get the custom URL of the Tracking Code
+     *
+     * @return string URL
+     */
+    protected function getCustomUrl()
+    {
+        $path = $this->request->getUri()->getPath();
+        $routeMatch = $this->router->match($this->request);
+        if ($routeMatch
+            && $routeMatch->getMatchedRouteName() == 'vufindrecord-ajaxtab'
+        ) {
+            // Replace 'AjaxTab' with tab name in record page URLs
+            $replace = 'AjaxTab';
+            $tab = $this->request->getPost('tab');
+            if (null !== ($pos = strrpos($path, $replace))) {
+                $path = substr_replace($path, $tab, $pos, $pos + strlen($replace));
+            }
+        }
+        return $path;
+    }
+
     /**
      * Get the Finalization Part of the Tracking Code
      *
@@ -311,11 +445,17 @@ EOT;
     VuFindPiwikTracker.enableLinkTracking();
 };
 (function(){
-var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
-    g.type='text/javascript'; g.defer=true; g.async=true;
-    g.src='{$this->url}piwik.js';
-    g.onload=initVuFindPiwikTracker;
-s.parentNode.insertBefore(g,s); })();
+    if (typeof Piwik === 'undefined') {
+        var d=document, g=d.createElement('script'),
+            s=d.getElementsByTagName('script')[0];
+        g.type='text/javascript'; g.defer=true; g.async=true;
+        g.src='{$this->url}piwik.js';
+        g.onload=initVuFindPiwikTracker{$this->timestamp};
+        s.parentNode.insertBefore(g,s);
+    } else {
+        initVuFindPiwikTracker{$this->timestamp}();
+    }
+})();
 EOT;
     }
 
@@ -363,10 +503,48 @@ EOT;
         $searchTerms = $escape($params->getDisplayQuery());
         $searchType = $escape($params->getSearchType());
         $resultCount = $results->getResultTotal();
+        $backendId = $results->getOptions()->getSearchClassId();
+
+        // Use trackSiteSearch *instead* of trackPageView in searches
+        return <<<EOT
+    VuFindPiwikTracker.trackSiteSearch(
+        '$backendId|$searchTerms', '$searchType', $resultCount
+    );
+
+EOT;
+    }
+
+    /**
+     * Get Site Search Tracking Code for Combined Search
+     *
+     * @param VuFind\Search\Base\Results $results         Search results
+     * @param array                      $combinedResults Combined Search Results
+     *
+     * @return string JavaScript Code Fragment
+     */
+    protected function getTrackCombinedSearchCode($results, $combinedResults)
+    {
+        $escape = $this->getView()->plugin('escapeHtmlAttr');
+        $params = $results->getParams();
+        $searchTerms = $escape($params->getDisplayQuery());
+        $searchType = $escape($params->getSearchType());
+        $resultCount = 0;
+        foreach ($combinedResults as $currentSearch) {
+            if (!empty($currentSearch['ajax'])) {
+                // Some results fetched via ajax, so report that we don't know the
+                // result count.
+                $resultCount = 'false';
+                break;
+            }
+            $resultCount += $currentSearch['view']->results
+                ->getResultTotal();
+        }
 
         // Use trackSiteSearch *instead* of trackPageView in searches
         return <<<EOT
-    VuFindPiwikTracker.trackSiteSearch('$searchTerms', '$searchType', $resultCount);
+    VuFindPiwikTracker.trackSiteSearch(
+        'Combined|$searchTerms', '$searchType', $resultCount
+    );
 
 EOT;
     }
diff --git a/module/VuFind/src/VuFind/View/Helper/Root/Printms.php b/module/VuFind/src/VuFind/View/Helper/Root/Printms.php
index 61a912a0ec3973b277d58ea61a8b93dcdbcb45ac..9a77195f0369582370e339cfe594ad591f9edb60 100644
--- a/module/VuFind/src/VuFind/View/Helper/Root/Printms.php
+++ b/module/VuFind/src/VuFind/View/Helper/Root/Printms.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  View_Helpers
diff --git a/module/VuFind/src/VuFind/View/Helper/Root/ProxyUrl.php b/module/VuFind/src/VuFind/View/Helper/Root/ProxyUrl.php
index 552003c484a3d3b7d8654c4ee015347c41474abb..cb87741da46caa9aeae01e173df921dac4d71110 100644
--- a/module/VuFind/src/VuFind/View/Helper/Root/ProxyUrl.php
+++ b/module/VuFind/src/VuFind/View/Helper/Root/ProxyUrl.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  View_Helpers
diff --git a/module/VuFind/src/VuFind/View/Helper/Root/Recaptcha.php b/module/VuFind/src/VuFind/View/Helper/Root/Recaptcha.php
index b26ab3dd75aca40c3e34a04a0b288ae2a56b81df..7a5d583b8bbe62618bc5fc15392f1b3c8be9725f 100644
--- a/module/VuFind/src/VuFind/View/Helper/Root/Recaptcha.php
+++ b/module/VuFind/src/VuFind/View/Helper/Root/Recaptcha.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  View_Helpers
@@ -76,7 +76,7 @@ class Recaptcha extends AbstractHelper
     public function __construct($rc, $config)
     {
         $this->recaptcha = $rc;
-        $this->active = isset($config->Captcha);
+        $this->active = isset($config->Captcha->forms);
     }
 
     /**
@@ -92,22 +92,26 @@ class Recaptcha extends AbstractHelper
     /**
      * Generate <div> with ReCaptcha from render.
      *
-     * @param boolean $useRecaptcha Boolean of active state, for compact templating
+     * @param bool $useRecaptcha Boolean of active state, for compact templating
+     * @param bool $wrapHtml     Include prefix and suffix?
      *
      * @return string $html
      */
-    public function html($useRecaptcha = true)
+    public function html($useRecaptcha = true, $wrapHtml = true)
     {
         if (!isset($useRecaptcha) || !$useRecaptcha) {
             return false;
         }
+        if (!$wrapHtml) {
+            return $this->recaptcha->getHtml();
+        }
         return $this->prefixHtml . $this->recaptcha->getHtml() . $this->suffixHtml;
     }
 
     /**
      * Return whether Captcha is active in the config
      *
-     * @return boolean
+     * @return bool
      */
     public function active()
     {
diff --git a/module/VuFind/src/VuFind/View/Helper/Root/Recommend.php b/module/VuFind/src/VuFind/View/Helper/Root/Recommend.php
index ff579a6a8bc85879c52136e87b5a58ce140ded7d..8d2d44c85540a4195790e73acf0d07c1553d88d8 100644
--- a/module/VuFind/src/VuFind/View/Helper/Root/Recommend.php
+++ b/module/VuFind/src/VuFind/View/Helper/Root/Recommend.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  View_Helpers
diff --git a/module/VuFind/src/VuFind/View/Helper/Root/Record.php b/module/VuFind/src/VuFind/View/Helper/Root/Record.php
index da217bf455c6798ef8402ffba37bb7c099ddb934..1054069909e0afc83b627f1cbad27fb5472f5673 100644
--- a/module/VuFind/src/VuFind/View/Helper/Root/Record.php
+++ b/module/VuFind/src/VuFind/View/Helper/Root/Record.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  View_Helpers
@@ -26,6 +26,7 @@
  * @link     https://vufind.org/wiki/development Wiki
  */
 namespace VuFind\View\Helper\Root;
+use VuFind\Cover\Router as CoverRouter;
 use Zend\View\Exception\RuntimeException, Zend\View\Helper\AbstractHelper;
 
 /**
@@ -46,6 +47,13 @@ class Record extends AbstractHelper
      */
     protected $contextHelper;
 
+    /**
+     * Cover router
+     *
+     * @var CoverRouter
+     */
+    protected $coverRouter = null;
+
     /**
      * Record driver
      *
@@ -70,6 +78,18 @@ class Record extends AbstractHelper
         $this->config = $config;
     }
 
+    /**
+     * Inject the cover router
+     *
+     * @param CoverRouter $router Cover router
+     *
+     * @return void
+     */
+    public function setCoverRouter($router)
+    {
+        $this->coverRouter = $router;
+    }
+
     /**
      * Render a template within a record driver folder.
      *
@@ -328,25 +348,6 @@ class Record extends AbstractHelper
         return $transEsc('Title not available');
     }
 
-    /**
-     * Get the name of the controller used by the record route.
-     *
-     * @return string
-     */
-    public function getController()
-    {
-        // Figure out controller using naming convention based on resource
-        // source:
-        $source = $this->driver->getSourceIdentifier();
-        if ($source == DEFAULT_SEARCH_BACKEND) {
-            // Default source is special case -- it uses the basic record
-            // controller.
-            return 'Record';
-        }
-        // All other controllers will correspond with the record source:
-        return ucwords(strtolower($source)) . 'record';
-    }
-
     /**
      * Render the link of the specified type.
      *
@@ -427,40 +428,114 @@ class Record extends AbstractHelper
     /**
      * Render a cover for the current record.
      *
-     * @param string $context Context of code being genarated
+     * @param string $context Context of code being generated
      * @param string $default The default size of the cover
      * @param string $link    The link for the anchor
      *
      * @return string
      */
     public function getCover($context, $default, $link = false)
+    {
+        $details = $this->getCoverDetails($context, $default, $link);
+        return $details['html'];
+    }
+
+    /**
+     * Should cover images be linked to previews (when applicable) in the provided
+     * template context?
+     *
+     * @param string $context Context of code being generated
+     *
+     * @return bool
+     */
+    protected function getPreviewCoverLinkSetting($context)
+    {
+        static $previewContexts = false;
+        if (false === $previewContexts) {
+            $previewContexts = isset($this->config->Content->linkPreviewsToCovers)
+                ? array_map(
+                    'trim',
+                    explode(',', $this->config->Content->linkPreviewsToCovers)
+                ) : ['*'];
+        }
+        return in_array('*', $previewContexts)
+            || in_array($context, $previewContexts);
+    }
+
+    /**
+     * Get the rendered cover plus some useful parameters.
+     *
+     * @param string $context Context of code being generated
+     * @param string $default The default size of the cover
+     * @param string $link    The link for the anchor
+     *
+     * @return array
+     */
+    public function getCoverDetails($context, $default, $link = false)
+    {
+        $details = compact('link', 'context') + [
+            'driver' => $this->driver, 'cover' => false, 'size' => false,
+            'linkPreview' => $this->getPreviewCoverLinkSetting($context),
+        ];
+        $preferredSize = $this->getCoverSize($context, $default);
+        if (empty($preferredSize)) {    // covers disabled entirely
+            $details['html'] = '';
+        } else {
+            // Find best option if more than one size is defined (e.g. small:medium)
+            foreach (explode(':', $preferredSize) as $size) {
+                if ($details['cover'] = $this->getThumbnail($size)) {
+                    $details['size'] = $size;
+                    break;
+                }
+            }
+
+            $details['html'] = $this->contextHelper->renderInContext(
+                'record/cover.phtml', $details
+            );
+        }
+        return $details;
+    }
+
+    /**
+     * Get the configured thumbnail size for record lists
+     *
+     * @param string $context Context of code being generated
+     * @param string $default The default size of the cover
+     *
+     * @return string
+     */
+    protected function getCoverSize($context, $default = 'medium')
     {
         if (isset($this->config->Content->coversize)
             && !$this->config->Content->coversize
         ) {
             // covers disabled entirely
-            $preferredSize = false;
-        } else {
-            // check for context-specific overrides
-            $preferredSize = isset($this->config->Content->coversize[$context])
-                ? $this->config->Content->coversize[$context] : $default;
-        }
-        if (empty($preferredSize)) {
-            return '';
+            return false;
         }
+        // check for context-specific overrides
+        return isset($this->config->Content->coversize[$context])
+            ? $this->config->Content->coversize[$context] : $default;
+    }
 
-        // Find best option if more than one size is defined (e.g. small:medium)
-        $cover = false;  // assume invalid until good size found below
-        foreach (explode(':', $preferredSize) as $size) {
-            if ($cover = $this->getThumbnail($size)) {
-                break;
-            }
+    /**
+     * Get the configured thumbnail alignment
+     *
+     * @param string $context telling the context asking, prepends the config key
+     *
+     * @return string
+     */
+    public function getThumbnailAlignment($context = 'result')
+    {
+        $view = $this->getView();
+        $configField = $context . 'ThumbnailsOnLeft';
+        $left = !isset($this->config->Site->$configField)
+            ? true : $this->config->Site->$configField;
+        $mirror = !isset($this->config->Site->mirrorThumbnailsRTL)
+            ? true : $this->config->Site->mirrorThumbnailsRTL;
+        if ($view->layout()->rtl && !$mirror) {
+            $left = !$left;
         }
-
-        $driver = $this->driver;    // for convenient use in compact()
-        return $this->contextHelper->renderInContext(
-            'record/cover.phtml', compact('cover', 'link', 'context', 'driver')
-        );
+        return $left ? 'left' : 'right';
     }
 
     /**
@@ -520,22 +595,9 @@ class Record extends AbstractHelper
      */
     public function getThumbnail($size = 'small')
     {
-        // Try to build thumbnail:
-        $thumb = $this->driver->tryMethod('getThumbnail', [$size]);
-
-        // No thumbnail?  Return false:
-        if (empty($thumb)) {
-            return false;
-        }
-
-        // Array?  It's parameters to send to the cover generator:
-        if (is_array($thumb)) {
-            $urlHelper = $this->getView()->plugin('url');
-            return $urlHelper('cover-show') . '?' . http_build_query($thumb);
-        }
-
-        // Default case -- return fixed string:
-        return $thumb;
+        return $this->coverRouter
+            ? $this->coverRouter->getUrl($this->driver, $size)
+            : false;
     }
 
     /**
diff --git a/module/VuFind/src/VuFind/View/Helper/Root/RecordLink.php b/module/VuFind/src/VuFind/View/Helper/Root/RecordLink.php
index a7d1adbe47713a4edf6672818663ac3e2920f84e..be65d2c35cf4d91e471a1aed8d93dc7bb0fa624e 100644
--- a/module/VuFind/src/VuFind/View/Helper/Root/RecordLink.php
+++ b/module/VuFind/src/VuFind/View/Helper/Root/RecordLink.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  View_Helpers
diff --git a/module/VuFind/src/VuFind/View/Helper/Root/Related.php b/module/VuFind/src/VuFind/View/Helper/Root/Related.php
index d90ca42447df03de11dcca3732350ca0f53a5320..a66d73f7ef86f9755bd3e4119f2fe62ad4138211 100644
--- a/module/VuFind/src/VuFind/View/Helper/Root/Related.php
+++ b/module/VuFind/src/VuFind/View/Helper/Root/Related.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  View_Helpers
diff --git a/module/VuFind/src/VuFind/View/Helper/Root/RenderArray.php b/module/VuFind/src/VuFind/View/Helper/Root/RenderArray.php
index b167140ec9670525a5d5fbf5faef02c8f6f973bb..89bbda6b678c657e720e3a77d12d468f21ac6f59 100644
--- a/module/VuFind/src/VuFind/View/Helper/Root/RenderArray.php
+++ b/module/VuFind/src/VuFind/View/Helper/Root/RenderArray.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  View_Helpers
diff --git a/module/VuFind/src/VuFind/View/Helper/Root/ResultFeed.php b/module/VuFind/src/VuFind/View/Helper/Root/ResultFeed.php
index f122a267c0d5d68f37254a518007cf941adc41c9..eecc7864725e423cc0f1f7987faad8470b085e9f 100644
--- a/module/VuFind/src/VuFind/View/Helper/Root/ResultFeed.php
+++ b/module/VuFind/src/VuFind/View/Helper/Root/ResultFeed.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  View_Helpers
diff --git a/module/VuFind/src/VuFind/View/Helper/Root/SafeMoneyFormat.php b/module/VuFind/src/VuFind/View/Helper/Root/SafeMoneyFormat.php
index 1e8d14a0c0f995bbec01b0682b80bfd2c8da34bd..b27fc9dfd0956746f6a7d8d035d07ff8ef371820 100644
--- a/module/VuFind/src/VuFind/View/Helper/Root/SafeMoneyFormat.php
+++ b/module/VuFind/src/VuFind/View/Helper/Root/SafeMoneyFormat.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  View_Helpers
diff --git a/module/VuFind/src/VuFind/View/Helper/Root/SearchBox.php b/module/VuFind/src/VuFind/View/Helper/Root/SearchBox.php
index 0a72cda3f622e74ac33129473365c851478b9ca0..a4fb93bc11c2c4841cd5b88ef2355c3defbe4fce 100644
--- a/module/VuFind/src/VuFind/View/Helper/Root/SearchBox.php
+++ b/module/VuFind/src/VuFind/View/Helper/Root/SearchBox.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  View_Helpers
diff --git a/module/VuFind/src/VuFind/View/Helper/Root/SearchMemory.php b/module/VuFind/src/VuFind/View/Helper/Root/SearchMemory.php
index fa227e3ec1b6feb8eced02c9417e3ceeaecf19ec..ab05d2a53ebf6fa6edad14134f5c41ed803b94bb 100644
--- a/module/VuFind/src/VuFind/View/Helper/Root/SearchMemory.php
+++ b/module/VuFind/src/VuFind/View/Helper/Root/SearchMemory.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  View_Helpers
diff --git a/module/VuFind/src/VuFind/View/Helper/Root/SearchOptions.php b/module/VuFind/src/VuFind/View/Helper/Root/SearchOptions.php
index 0625918c3f4784e1acb790c54e60b16487dcc94c..74d1989dd0ee8268633929319651866b3311c967 100644
--- a/module/VuFind/src/VuFind/View/Helper/Root/SearchOptions.php
+++ b/module/VuFind/src/VuFind/View/Helper/Root/SearchOptions.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  View_Helpers
diff --git a/module/VuFind/src/VuFind/View/Helper/Root/SearchParams.php b/module/VuFind/src/VuFind/View/Helper/Root/SearchParams.php
index 7723465191cc6e45e74f46428ad7fdb6abe659c1..33fa2cf003cc1c8e30873d50a89b309b2a3ec099 100644
--- a/module/VuFind/src/VuFind/View/Helper/Root/SearchParams.php
+++ b/module/VuFind/src/VuFind/View/Helper/Root/SearchParams.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  View_Helpers
diff --git a/module/VuFind/src/VuFind/View/Helper/Root/SearchTabs.php b/module/VuFind/src/VuFind/View/Helper/Root/SearchTabs.php
index fc68369757d7ab28bc21a94b4a135726b8b9496c..04735d9feeaf5486632843fe48557bb604da8d27 100644
--- a/module/VuFind/src/VuFind/View/Helper/Root/SearchTabs.php
+++ b/module/VuFind/src/VuFind/View/Helper/Root/SearchTabs.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  View_Helpers
diff --git a/module/VuFind/src/VuFind/View/Helper/Root/SortFacetList.php b/module/VuFind/src/VuFind/View/Helper/Root/SortFacetList.php
index e6947e844689fb498cdb66c495963ed94eb9bd90..cf17f6b55e78b5485dac4295e91cf6f61a279c30 100644
--- a/module/VuFind/src/VuFind/View/Helper/Root/SortFacetList.php
+++ b/module/VuFind/src/VuFind/View/Helper/Root/SortFacetList.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  View_Helpers
diff --git a/module/VuFind/src/VuFind/View/Helper/Root/Summon.php b/module/VuFind/src/VuFind/View/Helper/Root/Summon.php
index 170ab98a43458379f9db254b9c58d7ec8ac2fbec..8ae68f035c7cb3cf32e937666a27b8e2d3a7f3f1 100644
--- a/module/VuFind/src/VuFind/View/Helper/Root/Summon.php
+++ b/module/VuFind/src/VuFind/View/Helper/Root/Summon.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Summon
diff --git a/module/VuFind/src/VuFind/View/Helper/Root/SyndeticsPlus.php b/module/VuFind/src/VuFind/View/Helper/Root/SyndeticsPlus.php
index 22472beafd26c7feeff3f5d1c038b0ac0da238d5..7de84f752eb08f78c8b52568841d5562423e2ac8 100644
--- a/module/VuFind/src/VuFind/View/Helper/Root/SyndeticsPlus.php
+++ b/module/VuFind/src/VuFind/View/Helper/Root/SyndeticsPlus.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  View_Helpers
diff --git a/module/VuFind/src/VuFind/View/Helper/Root/SystemEmail.php b/module/VuFind/src/VuFind/View/Helper/Root/SystemEmail.php
index 04c5234b81f5fffe889ebd8703ba32adddea59bf..021fe52e0c38e68f7db535ac97d5f0ef562362af 100644
--- a/module/VuFind/src/VuFind/View/Helper/Root/SystemEmail.php
+++ b/module/VuFind/src/VuFind/View/Helper/Root/SystemEmail.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  View_Helpers
diff --git a/module/VuFind/src/VuFind/View/Helper/Root/TransEsc.php b/module/VuFind/src/VuFind/View/Helper/Root/TransEsc.php
index 56a8a5b51cbdb1a4c1525d3cdc9fbd81e057dc4f..ce96b795007412d70fd112eb3f470eb892703d0f 100644
--- a/module/VuFind/src/VuFind/View/Helper/Root/TransEsc.php
+++ b/module/VuFind/src/VuFind/View/Helper/Root/TransEsc.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  View_Helpers
diff --git a/module/VuFind/src/VuFind/View/Helper/Root/Translate.php b/module/VuFind/src/VuFind/View/Helper/Root/Translate.php
index 3fbbef0c204292ac02c513097f579e5db2770fc7..1ed735e5e29483bec3737f5fd6c6cd5895755bba 100644
--- a/module/VuFind/src/VuFind/View/Helper/Root/Translate.php
+++ b/module/VuFind/src/VuFind/View/Helper/Root/Translate.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  View_Helpers
diff --git a/module/VuFind/src/VuFind/View/Helper/Root/Truncate.php b/module/VuFind/src/VuFind/View/Helper/Root/Truncate.php
index 00259943a34b8fbd95591d8769dff1da77996577..058228e77e59df75023338878c3a31a97a2e95c0 100644
--- a/module/VuFind/src/VuFind/View/Helper/Root/Truncate.php
+++ b/module/VuFind/src/VuFind/View/Helper/Root/Truncate.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  View_Helpers
diff --git a/module/VuFind/src/VuFind/View/Helper/Root/UserList.php b/module/VuFind/src/VuFind/View/Helper/Root/UserList.php
index 85f5c1fe823f113efd2d0740cd493141b00895bc..702bc0c52504a611c6068f9dae303843f9a0db9f 100644
--- a/module/VuFind/src/VuFind/View/Helper/Root/UserList.php
+++ b/module/VuFind/src/VuFind/View/Helper/Root/UserList.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  View_Helpers
diff --git a/module/VuFind/src/VuFind/View/Helper/Root/UserTags.php b/module/VuFind/src/VuFind/View/Helper/Root/UserTags.php
index 2f07d1ea8b1006d2d99bbb89fa40d7de4a633f0a..b0fbf7ac81caf3d4c687876fc07f82d2e3f47b7a 100644
--- a/module/VuFind/src/VuFind/View/Helper/Root/UserTags.php
+++ b/module/VuFind/src/VuFind/View/Helper/Root/UserTags.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  View_Helpers
diff --git a/module/VuFind/src/VuFind/View/Helper/jQueryMobile/MobileMenu.php b/module/VuFind/src/VuFind/View/Helper/jQueryMobile/MobileMenu.php
index e67be37b0c30421cfcdfe8a8802968c210b04654..a3c4d8d99a02bbac2203d54f29111fc2556463c0 100644
--- a/module/VuFind/src/VuFind/View/Helper/jQueryMobile/MobileMenu.php
+++ b/module/VuFind/src/VuFind/View/Helper/jQueryMobile/MobileMenu.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  View_Helpers
diff --git a/module/VuFind/src/VuFind/XSLT/Import/VuFind.php b/module/VuFind/src/VuFind/XSLT/Import/VuFind.php
index 4862649f466a7a3ea3a0ee1cabfda80476c28ce7..421f2a3ec742b25cfffb45520b3993072edc87e8 100644
--- a/module/VuFind/src/VuFind/XSLT/Import/VuFind.php
+++ b/module/VuFind/src/VuFind/XSLT/Import/VuFind.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Import_Tools
diff --git a/module/VuFind/src/VuFind/XSLT/Import/VuFindSitemap.php b/module/VuFind/src/VuFind/XSLT/Import/VuFindSitemap.php
index d812e2f9110d08204e556bc86d4ab4eb6fd59d81..723ab95c14e855e8494fa891ddd81ac8f1fa71b6 100644
--- a/module/VuFind/src/VuFind/XSLT/Import/VuFindSitemap.php
+++ b/module/VuFind/src/VuFind/XSLT/Import/VuFindSitemap.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Import_Tools
diff --git a/module/VuFind/src/VuFind/XSLT/Importer.php b/module/VuFind/src/VuFind/XSLT/Importer.php
index 3f146ecd05051ac4c731ac30d2126b102f30b564..4219a26686a8d02cb17d35cdaa7c561ef6ffecbd 100644
--- a/module/VuFind/src/VuFind/XSLT/Importer.php
+++ b/module/VuFind/src/VuFind/XSLT/Importer.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  XSLT
diff --git a/module/VuFind/src/VuFind/XSLT/Processor.php b/module/VuFind/src/VuFind/XSLT/Processor.php
index 716cd9283b7938179b84d873815aff969cbae57b..8c532add0fa6bf88e4e8c3bca92b9b2d8da490d3 100644
--- a/module/VuFind/src/VuFind/XSLT/Processor.php
+++ b/module/VuFind/src/VuFind/XSLT/Processor.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  XSLT
diff --git a/module/VuFind/src/VuFindTest/RecordDriver/TestHarness.php b/module/VuFind/src/VuFindTest/RecordDriver/TestHarness.php
index 8699bab13d0d555f97d1c46e6f5f022e6b911537..3710d7aa60bf71c7fc4ca35d4dbfab27c25f5cb0 100644
--- a/module/VuFind/src/VuFindTest/RecordDriver/TestHarness.php
+++ b/module/VuFind/src/VuFindTest/RecordDriver/TestHarness.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  RecordDrivers
diff --git a/module/VuFind/src/VuFindTest/Search/TestHarness/Options.php b/module/VuFind/src/VuFindTest/Search/TestHarness/Options.php
index 8ad16aa05212ae3560ba84cb265a41387dfc228b..43e54aac847d098a417694168c6f7103133cc05d 100644
--- a/module/VuFind/src/VuFindTest/Search/TestHarness/Options.php
+++ b/module/VuFind/src/VuFindTest/Search/TestHarness/Options.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search_Base
@@ -40,6 +40,23 @@ namespace VuFindTest\Search\TestHarness;
  */
 class Options extends \VuFind\Search\Base\Options
 {
+    /**
+     * Constructor
+     *
+     * @param \VuFind\Config\PluginManager $configLoader Config loader
+     */
+    public function __construct(\VuFind\Config\PluginManager $configLoader)
+    {
+        parent::__construct($configLoader);
+        // Turn on first/last navigation if configured:
+        $config = $configLoader->get('config');
+        if (isset($config->Record->first_last_navigation)
+            && $config->Record->first_last_navigation
+        ) {
+            $this->firstlastNavigation = true;
+        }
+    }
+
     /**
      * Return the route name for the search results action.
      *
diff --git a/module/VuFind/src/VuFindTest/Search/TestHarness/Params.php b/module/VuFind/src/VuFindTest/Search/TestHarness/Params.php
index 90c052dfedf21d1079ebcb956cc790c78b42f357..8cabb13eba3642f233029c131be0d65131c22366 100644
--- a/module/VuFind/src/VuFindTest/Search/TestHarness/Params.php
+++ b/module/VuFind/src/VuFindTest/Search/TestHarness/Params.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search_Base
diff --git a/module/VuFind/src/VuFindTest/Search/TestHarness/Results.php b/module/VuFind/src/VuFindTest/Search/TestHarness/Results.php
index a79e3e2eac2ad122f5e10c3774cd1a24a9c3752d..374690efd25096bd2a0298f7a726970f68afd266 100644
--- a/module/VuFind/src/VuFindTest/Search/TestHarness/Results.php
+++ b/module/VuFind/src/VuFindTest/Search/TestHarness/Results.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search_Base
@@ -100,7 +100,10 @@ class Results extends \VuFind\Search\Base\Results
             if ($i > $this->resultTotal) {
                 break;
             }
-            $this->results[] = $this->getMockRecordDriver($i);
+            // Append sort type to ID to simulate sort order; default sort is
+            // null, so most tests will just get numbers.
+            $sort = $this->getParams()->getSort();
+            $this->results[] = $this->getMockRecordDriver($sort . $i);
         }
     }
 
diff --git a/module/VuFind/src/VuFindTest/Unit/DbTestCase.php b/module/VuFind/src/VuFindTest/Unit/DbTestCase.php
index 3570cd7f7afcb83ede23b9655008d13e514f7835..5d0608d40982b2bbd9cdf5431230d2c8247e7670 100644
--- a/module/VuFind/src/VuFindTest/Unit/DbTestCase.php
+++ b/module/VuFind/src/VuFindTest/Unit/DbTestCase.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/src/VuFindTest/Unit/ILSDriverTestCase.php b/module/VuFind/src/VuFindTest/Unit/ILSDriverTestCase.php
index c70d94130820b101c8abd40bcd785b28c5304018..8cee1886fd5afa0d5fbab8a709985f1448529eed 100644
--- a/module/VuFind/src/VuFindTest/Unit/ILSDriverTestCase.php
+++ b/module/VuFind/src/VuFindTest/Unit/ILSDriverTestCase.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/src/VuFindTest/Unit/MinkTestCase.php b/module/VuFind/src/VuFindTest/Unit/MinkTestCase.php
index 218a7bf13d664b671dd4e77a037379876ff614c0..6c6a3907d514e8095072b11a9adeb431b945c59a 100644
--- a/module/VuFind/src/VuFindTest/Unit/MinkTestCase.php
+++ b/module/VuFind/src/VuFindTest/Unit/MinkTestCase.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
@@ -27,7 +27,7 @@
  * @link     https://vufind.org/wiki/development:testing:unit_tests Wiki
  */
 namespace VuFindTest\Unit;
-use Behat\Mink\Driver\ZombieDriver, Behat\Mink\Session,
+use Behat\Mink\Driver\Selenium2Driver, Behat\Mink\Session,
     Behat\Mink\Element\Element,
     VuFind\Config\Locator as ConfigLocator,
     VuFind\Config\Writer as ConfigWriter;
@@ -75,7 +75,6 @@ abstract class MinkTestCase extends DbTestCase
     {
         foreach ($configs as $file => $settings) {
             $this->changeConfigFile($file, $settings, in_array($file, $replace));
-            $this->modifiedConfigs[] = $file;
         }
     }
 
@@ -93,14 +92,17 @@ abstract class MinkTestCase extends DbTestCase
     {
         $file = $configName . '.ini';
         $local = ConfigLocator::getLocalConfigPath($file, null, true);
-        if (file_exists($local)) {
-            // File exists? Make a backup!
-            copy($local, $local . '.bak');
-        } else {
-            // File doesn't exist? Make a baseline version.
-            copy(ConfigLocator::getBaseConfigPath($file), $local);
-        }
+        if (!in_array($configName, $this->modifiedConfigs)) {
+            if (file_exists($local)) {
+                // File exists? Make a backup!
+                copy($local, $local . '.bak');
+            } else {
+                // File doesn't exist? Make a baseline version.
+                copy(ConfigLocator::getBaseConfigPath($file), $local);
+            }
 
+            $this->modifiedConfigs[] = $configName;
+        }
         // If we're replacing the existing file, wipe it out now:
         if ($replace) {
             file_put_contents($local, '');
@@ -115,31 +117,6 @@ abstract class MinkTestCase extends DbTestCase
         $writer->save();
     }
 
-    /**
-     * Are we using the Zombie.js driver?
-     *
-     * @return bool
-     */
-    protected function isZombieDriver()
-    {
-        return getenv('VUFIND_MINK_DRIVER') !== 'selenium';
-    }
-
-    /**
-     * Assert an HTTP status code (if supported).
-     *
-     * @param int $code Expected status code.
-     *
-     * @return void
-     */
-    protected function assertHttpStatus($code)
-    {
-        // This assertion is not supported by Selenium.
-        if ($this->isZombieDriver()) {
-            $this->assertEquals(200, $this->getMinkSession()->getStatusCode());
-        }
-    }
-
     /**
      * Sleep if necessary.
      *
@@ -149,14 +126,11 @@ abstract class MinkTestCase extends DbTestCase
      */
     protected function snooze($secs = 1)
     {
-        // Sleep is not necessary for Zombie.js.
-        if (!$this->isZombieDriver()) {
-            $snoozeMultiplier = intval(getenv('VUFIND_SNOOZE_MULTIPLIER'));
-            if ($snoozeMultiplier < 1) {
-                $snoozeMultiplier = 1;
-            }
-            sleep($secs * $snoozeMultiplier);
+        $snoozeMultiplier = intval(getenv('VUFIND_SNOOZE_MULTIPLIER'));
+        if ($snoozeMultiplier < 1) {
+            $snoozeMultiplier = 1;
         }
+        sleep($secs * $snoozeMultiplier);
     }
 
     /**
@@ -168,22 +142,19 @@ abstract class MinkTestCase extends DbTestCase
      */
     protected function checkVisibility(Element $element)
     {
-        // Zombie.js does not support visibility checks; just assume true.
-        return $this->isZombieDriver() ? true : $element->isVisible();
+        return $element->isVisible();
     }
 
     /**
      * Get the Mink driver, initializing it if necessary.
      *
-     * @return ZombieDriver
+     * @return Selenium2Driver
      */
     protected function getMinkDriver()
     {
-        return !$this->isZombieDriver()
-            ? new \Behat\Mink\Driver\Selenium2Driver('firefox')
-            : new ZombieDriver(
-                new \Behat\Mink\Driver\NodeJS\Server\ZombieServer()
-            );
+        $env = getenv('VUFIND_SELENIUM_BROWSER');
+        $browser = $env ? $env : 'firefox';
+        return new Selenium2Driver($browser);
     }
 
     /**
@@ -249,6 +220,7 @@ abstract class MinkTestCase extends DbTestCase
                 rename($backup, $local);
             }
         }
+        $this->modifiedConfigs = [];
     }
 
     /**
@@ -310,13 +282,10 @@ abstract class MinkTestCase extends DbTestCase
      */
     public function setUp()
     {
-        // Give up if we're not running in CI or Zombie.js is unavailable:
+        // Give up if we're not running in CI:
         if (!$this->continuousIntegrationRunning()) {
             return $this->markTestSkipped('Continuous integration not running.');
         }
-        if (strlen(getenv('NODE_PATH')) == 0) {
-            return $this->markTestSkipped('NODE_PATH setting missing.');
-        }
 
         // Reset the modified configs list.
         $this->modifiedConfigs = [];
diff --git a/module/VuFind/src/VuFindTest/Unit/RecommendDeferredTestCase.php b/module/VuFind/src/VuFindTest/Unit/RecommendDeferredTestCase.php
index a834a1de4aeeb60623480cae8e299526ef644858..771bc7a0a481b6064a4e844861362ff16e26220e 100644
--- a/module/VuFind/src/VuFindTest/Unit/RecommendDeferredTestCase.php
+++ b/module/VuFind/src/VuFindTest/Unit/RecommendDeferredTestCase.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/src/VuFindTest/Unit/TestCase.php b/module/VuFind/src/VuFindTest/Unit/TestCase.php
index 4c213b1cd9695554e0b95269743cde37c02f89bd..dff552c20875a24c4d8847e2965d531cb01f1268 100644
--- a/module/VuFind/src/VuFindTest/Unit/TestCase.php
+++ b/module/VuFind/src/VuFindTest/Unit/TestCase.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/src/VuFindTest/Unit/UserCreationTrait.php b/module/VuFind/src/VuFindTest/Unit/UserCreationTrait.php
index 9f1188abcf5c5f3155027496e27316c1ee833482..81aa71ed705f93c2c6e2a14c25a1c4fc63e7acc0 100644
--- a/module/VuFind/src/VuFindTest/Unit/UserCreationTrait.php
+++ b/module/VuFind/src/VuFindTest/Unit/UserCreationTrait.php
@@ -19,7 +19,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/src/VuFindTest/Unit/ViewHelperTestCase.php b/module/VuFind/src/VuFindTest/Unit/ViewHelperTestCase.php
index 5a30a2438488dd1efd7bad8dc3c6adab406096e9..bbbc68549c1cb67c2dc6c27643661be943708d34 100644
--- a/module/VuFind/src/VuFindTest/Unit/ViewHelperTestCase.php
+++ b/module/VuFind/src/VuFindTest/Unit/ViewHelperTestCase.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/fixtures/configs/buildxml-2.5/build.xml b/module/VuFind/tests/fixtures/configs/buildxml-2.5/build.xml
index 5fb120c36022ed942be3babb02780611f8512e7d..7ea4b899cb5edc8c01a6aa172953e452322e5980 100644
--- a/module/VuFind/tests/fixtures/configs/buildxml-2.5/build.xml
+++ b/module/VuFind/tests/fixtures/configs/buildxml-2.5/build.xml
@@ -65,7 +65,7 @@
 
   <!-- Report rule violations with PHPMD (mess detector) -->
   <target name="phpmd">
-    <exec command="phpmd ${srcdir}/module xml ${srcdir}/tests/phpmd.xml --exclude ${srcdir}/module/VuFind/tests,${srcdir}/module/VuDL/tests,${srcdir}/module/VuFindSearch/tests --reportfile ${builddir}/reports/phpmd.xml" />
+    <exec command="phpmd ${srcdir}/module xml ${srcdir}/tests/phpmd.xml --exclude ${srcdir}/module/VuFind/tests,${srcdir}/module/VuFindSearch/tests --reportfile ${builddir}/reports/phpmd.xml" />
   </target>
 
   <!-- Measure project with phploc -->
diff --git a/module/VuFind/tests/fixtures/configs/yaml/core.yaml b/module/VuFind/tests/fixtures/configs/yaml/core.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..6662da1d1c2c2596955134fdecc36ad746915154
--- /dev/null
+++ b/module/VuFind/tests/fixtures/configs/yaml/core.yaml
@@ -0,0 +1,4 @@
+top:
+  foo: "bar"
+bottom:
+  goo: "gar"
\ No newline at end of file
diff --git a/module/VuFind/tests/fixtures/configs/yaml/local.yaml b/module/VuFind/tests/fixtures/configs/yaml/local.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..5dbebb0f4d97cfcec662d23980fca8fadb39e21c
--- /dev/null
+++ b/module/VuFind/tests/fixtures/configs/yaml/local.yaml
@@ -0,0 +1,4 @@
+top:
+  foo: "xyzzy"
+middle:
+  moo: "cow"
\ No newline at end of file
diff --git a/module/VuFind/tests/fixtures/paia/response/changePassword.json b/module/VuFind/tests/fixtures/paia/response/changePassword.json
new file mode 100644
index 0000000000000000000000000000000000000000..3a1981450d90f7300f76160ad92cd93c28b6cef5
--- /dev/null
+++ b/module/VuFind/tests/fixtures/paia/response/changePassword.json
@@ -0,0 +1,15 @@
+HTTP/1.1 200 OK
+Content-Type: application/json; charset=UTF-8
+Date: Mon, 20 Jun 2016 10:20:48 GMT
+Server: vzg-paia/2.2-RC3 (Wed Jun 01 17:38:24 CEST 2016)
+Cache-Control: no-cache, no-store
+Access-Control-Allow-Origin: *
+Access-Control-Expose-Headers: X-OAuth-Scopes, X-Accepted-OAuth-Scopes
+Access-Control-Allow-Headers: Content-Type, Authorization
+Pragma: no-cache
+X-OAuth-Scopes: change_password read_items read_fees read_patron
+X-Accepted-OAuth-Scopes: change_password
+
+{
+  "patron": "08301001001"
+}
diff --git a/module/VuFind/tests/fixtures/paia/response/fees.json b/module/VuFind/tests/fixtures/paia/response/fees.json
new file mode 100644
index 0000000000000000000000000000000000000000..010e18a05fd5e65aaa0bd05ae66603b27a564017
--- /dev/null
+++ b/module/VuFind/tests/fixtures/paia/response/fees.json
@@ -0,0 +1,51 @@
+HTTP/1.1 200 OK
+Server: vzg-paia/2.2-RC3 (Wed Jun 01 17:38:24 CEST 2016)
+Date: Mon, 20 Jun 2016 11:30:05 GMT
+Cache-Control: no-cache
+Access-Control-Allow-Origin: *
+Access-Control-Expose-Headers: X-OAuth-Scopes, X-Accepted-OAuth-Scopes
+Access-Control-Allow-Headers: Content-Type, Authorization
+Pragma: no-cache
+X-OAuth-Scopes: read_availability change_password write_items read_items read_fees read_patron
+X-Accepted-OAuth-Scopes: read_fees
+Content-Type: application/json; charset=UTF-8
+
+{
+  "amount": "7.40 EUR",
+  "fee": [{
+    "feetypeid": "de-830:fee-type:2",
+    "amount": "1.60 EUR",
+    "date": "2016-06-07T11:19:11+02:00",
+    "feetype": "Vormerkgebuehr",
+    "about": "Open source licensing : software freedom and intellectual property law ; [open source licensees are free to: use open source software for any purpose, make and distribute copies, create and distribute derivative works, access and use the source code, com / Rosen, Lawrence (c 2005)",
+    "item": "http://uri.gbv.de/document/opac-de-830:bar:830$28295402"
+  }, {
+    "feetypeid": "de-830:fee-type:2",
+    "amount": "0.80 EUR",
+    "date": "2016-05-23T15:31:34+02:00",
+    "feetype": "Vormerkgebuehr",
+    "about": "Zend framework in action / Allen, Rob (2009)",
+    "item": "http://uri.gbv.de/document/opac-de-830:bar:830$28323471"
+  }, {
+    "feetypeid": "de-830:fee-type:3",
+    "amount": "3.00 EUR",
+    "date": "2016-05-24T02:31:30+02:00",
+    "feetype": "Säumnisgebühr",
+    "about": "Unsere historischen Gärten / Lutze, Margot (1986)",
+    "item": "http://uri.gbv.de/document/opac-de-830:bar:830$24476416"
+  }, {
+    "feetypeid": "de-830:fee-type:3",
+    "amount": "1.00 EUR",
+    "date": "2016-06-17T02:31:48+02:00",
+    "feetype": "Säumnisgebühr",
+    "about": "Triumphe des Backsteins = Triumphs of brick / (1992)",
+    "item": "http://uri.gbv.de/document/opac-de-830:bar:830$33204941"
+  }, {
+    "feetypeid": "de-830:fee-type:3",
+    "amount": "1.00 EUR",
+    "date": "2016-05-24T02:31:30+02:00",
+    "feetype": "Säumnisgebühr",
+    "about": "Lehrbuch der Botanik / Strasburger, Eduard (2008)",
+    "item": "http://uri.gbv.de/document/opac-de-830:bar:830$26461872"
+  }]
+}
diff --git a/module/VuFind/tests/fixtures/paia/response/items.json b/module/VuFind/tests/fixtures/paia/response/items.json
new file mode 100644
index 0000000000000000000000000000000000000000..15df98c4ee2191c2a76a3390b890b05dce04a80b
--- /dev/null
+++ b/module/VuFind/tests/fixtures/paia/response/items.json
@@ -0,0 +1,87 @@
+HTTP/1.1 200 OK
+Server: vzg-paia/2.2-RC3 (Wed Jun 01 17:38:24 CEST 2016)
+Date: Mon, 20 Jun 2016 10:38:21 GMT
+Cache-Control: no-cache
+Access-Control-Allow-Origin: *
+Access-Control-Expose-Headers: X-OAuth-Scopes, X-Accepted-OAuth-Scopes
+Access-Control-Allow-Headers: Content-Type, Authorization
+Pragma: no-cache
+X-OAuth-Scopes: change_password read_items read_fees read_patron
+X-Accepted-OAuth-Scopes: read_patron
+Content-Type: application/json; charset=UTF-8
+
+{
+  "doc": [{
+    "queue": 0,
+    "canrenew": false,
+    "cancancel": false,
+    "storageid": "de-830:desk:23",
+    "label": "34:3409-6983",
+    "reminder": 0,
+    "edition": "http://uri.gbv.de/document/opac-de-830:ppn:040445623",
+    "status": 4,
+    "starttime": "2016-06-17",
+    "storage": "Test-Theke",
+    "about": "Praktikum über Entwurf und Manipulation von Datenbanken : SQL/DS (IBM), UDS (Siemens) und MEMODAX / Vossen, Gottfried (1986)",
+    "renewals": 0,
+    "item": "http://uri.gbv.de/document/opac-de-830:bar:830$34096983"
+  }, {
+    "queue": 0,
+    "canrenew": false,
+    "cancancel": false,
+    "storageid": "de-830:desk:1",
+    "label": "24:2426-0127",
+    "reminder": 0,
+    "edition": "http://uri.gbv.de/document/opac-de-830:ppn:020966334",
+    "endtime": "2016-05-23",
+    "status": 2,
+    "starttime": "2016-04-25T08:50:41+02:00",
+    "storage": "Ausleihe",
+    "about": "Gold / Kettell, Brian (1982)",
+    "renewals": 0,
+    "item": "http://uri.gbv.de/document/opac-de-830:bar:830$24260127"
+  },{
+    "queue": 0,
+    "canrenew": false,
+    "cancancel": false,
+    "storageid": "de-830:desk:1",
+    "label": "28:2834-2436",
+    "reminder": 1,
+    "edition": "http://uri.gbv.de/document/opac-de-830:ppn:58891861X",
+    "endtime": "2016-06-15",
+    "status": 3,
+    "starttime": "2013-11-15",
+    "storage": "Ausleihe",
+    "about": "Theoretische Informatik : mit 22 Tabellen und 78 Aufgaben / Hoffmann, Dirk W. (2009)",
+    "renewals": 12,
+    "item": "http://uri.gbv.de/document/opac-de-830:bar:830$28342436"
+  }, {
+    "queue": 0,
+    "canrenew": false,
+    "cancancel": false,
+    "storageid": "de-830:desk:612",
+    "label": "22:2227-8001",
+    "reminder": 0,
+    "edition": "http://uri.gbv.de/document/opac-de-830:ppn:659228084",
+    "endtime": "2016-07-14",
+    "status": 3,
+    "starttime": "2011-12-22",
+    "storage": "Ausleihe",
+    "about": "Linked Open Library Data : bibliographische Daten und ihre Zugänglichkeit im Web der Daten ; Innovationspreis 2011 / Fürste, Fabian M. (2011)",
+    "renewals": 9,
+    "item": "http://uri.gbv.de/document/opac-de-830:bar:830$22278001"
+  }, {
+    "queue": 0,
+    "canrenew": false,
+    "cancancel": true,
+    "storageid": "de-830:desk:613",
+    "label": "28:2829-5402",
+    "edition": "http://uri.gbv.de/document/opac-de-830:ppn:391260316",
+    "endtime": "2016-06-15",
+    "status": 1,
+    "starttime": "2016-06-15T13:22:27+02:00",
+    "storage": "Ausleihe",
+    "about": "Open source licensing : software freedom and intellectual property law ; [open source licensees are free to: use open source software for any purpose, make and distribute copies, create and distribute derivative works, access and use the source code, com / Rosen, Lawrence (c 2005)",
+    "item": "http://uri.gbv.de/document/opac-de-830:bar:830$28295402"
+  } ]
+}
diff --git a/module/VuFind/tests/fixtures/paia/response/login.json b/module/VuFind/tests/fixtures/paia/response/login.json
new file mode 100644
index 0000000000000000000000000000000000000000..ac3fc2676eed354792e7145b3f704887a2b92190
--- /dev/null
+++ b/module/VuFind/tests/fixtures/paia/response/login.json
@@ -0,0 +1,18 @@
+HTTP/1.1 200 OK
+Server: vzg-paia/2.2-RC3 (Wed Jun 01 17:38:24 CEST 2016)
+Date: Mon, 20 Jun 2016 13:35:01 GMT
+Cache-Control: no-cache, no-store
+Access-Control-Allow-Origin: *
+Access-Control-Expose-Headers: X-OAuth-Scopes, X-Accepted-OAuth-Scopes
+Access-Control-Allow-Headers: Content-Type, Authorization
+Pragma: no-cache
+Content-Type: application/json; charset=UTF-8
+
+{
+  "access_token": "AAASECRETBBB",
+  "expires_in": 3600,
+  "scope": "read_availability change_password write_items read_items read_fees read_patron",
+  "token_type": "Bearer",
+  "patron": "08301001001"
+}
+
diff --git a/module/VuFind/tests/fixtures/paia/response/login_bad.json b/module/VuFind/tests/fixtures/paia/response/login_bad.json
new file mode 100644
index 0000000000000000000000000000000000000000..e5379d8795c346125479dec12b384df21ea7ddf7
--- /dev/null
+++ b/module/VuFind/tests/fixtures/paia/response/login_bad.json
@@ -0,0 +1,14 @@
+HTTP/1.1 200 OK
+Content-Type: application/json; charset=UTF-8
+Server: vzg-paia/2.2-RC3 (Wed Jun 01 17:38:24 CEST 2016)
+Date: Mon, 20 Jun 2016 13:35:01 GMT
+Cache-Control: no-cache, no-store
+Access-Control-Allow-Origin: *
+Access-Control-Expose-Headers: X-OAuth-Scopes, X-Accepted-OAuth-Scopes
+Access-Control-Allow-Headers: Content-Type, Authorization
+Pragma: no-cache
+
+{
+  "error": "access_denied",
+  "error_description": "invalid patron or password"
+}
diff --git a/module/VuFind/tests/fixtures/paia/response/patron.json b/module/VuFind/tests/fixtures/paia/response/patron.json
new file mode 100644
index 0000000000000000000000000000000000000000..819320fdb9b61ed68950c43ff3a8f715ab72c483
--- /dev/null
+++ b/module/VuFind/tests/fixtures/paia/response/patron.json
@@ -0,0 +1,22 @@
+HTTP/1.1 200 OK
+Server: vzg-paia/2.2-RC3 (Wed Jun 01 17:38:24 CEST 2016)
+Date: Mon, 20 Jun 2016 10:38:21 GMT
+Cache-Control: no-cache
+Access-Control-Allow-Origin: *
+Access-Control-Expose-Headers: X-OAuth-Scopes, X-Accepted-OAuth-Scopes
+Access-Control-Allow-Headers: Content-Type, Authorization
+Pragma: no-cache
+X-OAuth-Scopes: change_password read_items read_fees read_patron
+X-Accepted-OAuth-Scopes: read_patron
+Content-Type: application/json; charset=UTF-8
+
+{
+  "name": " Nobody Nothing",
+  "expires": "9999-12-31",
+  "email": "nobody@vufind.org",
+  "status": 0,
+  "address": "No Street at all 8, D-21073 Hamburg",
+  "type": ["de-830:user-type:20"]
+}
+
+
diff --git a/module/VuFind/tests/fixtures/paia/response/patron_expired.json b/module/VuFind/tests/fixtures/paia/response/patron_expired.json
new file mode 100644
index 0000000000000000000000000000000000000000..acf8c6316729a22e56bcf695a7c13e0e2930341e
--- /dev/null
+++ b/module/VuFind/tests/fixtures/paia/response/patron_expired.json
@@ -0,0 +1,22 @@
+HTTP/1.1 200 OK
+Server: vzg-paia/2.2-RC3 (Wed Jun 01 17:38:24 CEST 2016)
+Date: Mon, 20 Jun 2016 10:38:21 GMT
+Cache-Control: no-cache
+Access-Control-Allow-Origin: *
+Access-Control-Expose-Headers: X-OAuth-Scopes, X-Accepted-OAuth-Scopes
+Access-Control-Allow-Headers: Content-Type, Authorization
+Pragma: no-cache
+X-OAuth-Scopes: change_password read_items read_fees read_patron
+X-Accepted-OAuth-Scopes: read_patron
+Content-Type: application/json; charset=UTF-8
+
+{
+  "name": " Nobody Nothing",
+  "expires": "2015-12-31",
+  "email": "nobody@vufind.org",
+  "status": 0,
+  "address": "No Street at all 8, D-21073 Hamburg",
+  "type": ["de-830:user-type:20"]
+}
+
+
diff --git a/module/VuFind/tests/fixtures/paia/response/patron_locked.json b/module/VuFind/tests/fixtures/paia/response/patron_locked.json
new file mode 100644
index 0000000000000000000000000000000000000000..1392f029859b76428f12050069972e6e3c6b2d66
--- /dev/null
+++ b/module/VuFind/tests/fixtures/paia/response/patron_locked.json
@@ -0,0 +1,22 @@
+HTTP/1.1 200 OK
+Server: vzg-paia/2.2-RC3 (Wed Jun 01 17:38:24 CEST 2016)
+Date: Mon, 20 Jun 2016 10:38:21 GMT
+Cache-Control: no-cache
+Access-Control-Allow-Origin: *
+Access-Control-Expose-Headers: X-OAuth-Scopes, X-Accepted-OAuth-Scopes
+Access-Control-Allow-Headers: Content-Type, Authorization
+Pragma: no-cache
+X-OAuth-Scopes: change_password read_items read_fees read_patron
+X-Accepted-OAuth-Scopes: read_patron
+Content-Type: application/json; charset=UTF-8
+
+{
+  "name": " Nobody Nothing",
+  "expires": "9999-12-31",
+  "email": "nobody@vufind.org",
+  "status": 9,
+  "address": "No Street at all 8, D-21073 Hamburg",
+  "type": ["de-830:user-type:20"]
+}
+
+
diff --git a/module/VuFind/tests/fixtures/paia/response/renew_error.json b/module/VuFind/tests/fixtures/paia/response/renew_error.json
new file mode 100644
index 0000000000000000000000000000000000000000..e1d95e98308c0bf1241ef445936f20396f0bf949
--- /dev/null
+++ b/module/VuFind/tests/fixtures/paia/response/renew_error.json
@@ -0,0 +1,17 @@
+HTTP/1.1 200 OK
+Server: vzg-paia/2.2-RC3 (Wed Jun 01 17:38:24 CEST 2016)
+Date: Mon, 20 Jun 2016 11:21:15 GMT
+Cache-Control: no-cache
+Access-Control-Allow-Origin: *
+Access-Control-Expose-Headers: X-OAuth-Scopes, X-Accepted-OAuth-Scopes
+Access-Control-Allow-Headers: Content-Type, Authorization
+Pragma: no-cache
+X-OAuth-Scopes: change_password read_items read_fees read_patron
+X-Accepted-OAuth-Scopes: write_items
+Content-Type: application/json; charset=UTF-8
+
+{
+  "error": "insufficient_scope",
+  "code": 403,
+  "error_description": "The access token was accepted but it lacks permission for the request"
+}
\ No newline at end of file
diff --git a/module/VuFind/tests/fixtures/paia/response/renew_ok.json b/module/VuFind/tests/fixtures/paia/response/renew_ok.json
new file mode 100644
index 0000000000000000000000000000000000000000..be60b443e02c225819e08740d5c4a5f54d2bbb06
--- /dev/null
+++ b/module/VuFind/tests/fixtures/paia/response/renew_ok.json
@@ -0,0 +1,30 @@
+HTTP/1.1 200 OK
+Server: vzg-paia/2.2-RC3 (Wed Jun 01 17:38:24 CEST 2016)
+Date: Mon, 20 Jun 2016 11:23:46 GMT
+Cache-Control: no-cache
+Access-Control-Allow-Origin: *
+Access-Control-Expose-Headers: X-OAuth-Scopes, X-Accepted-OAuth-Scopes
+Access-Control-Allow-Headers: Content-Type, Authorization
+Pragma: no-cache
+X-OAuth-Scopes: read_availability change_password write_items read_items read_fees read_patron
+X-Accepted-OAuth-Scopes: write_items
+Content-Type: application/json; charset=UTF-8
+
+{
+  "doc": [{
+    "requested": "http://uri.gbv.de/document/opac-de-830:bar:830$22061137",
+    "queue": 0,
+    "cancancel": false,
+    "storageid": "de-830:desk:1",
+    "label": "22:2206-1137",
+    "reminder": 0,
+    "edition": "http://uri.gbv.de/document/opac-de-830:ppn:048430196",
+    "endtime": "2016-07-18",
+    "status": 3,
+    "starttime": "2016-06-07",
+    "storage": "Ausleihe",
+    "about": "Verteilte Datenbanken /  (1991)",
+    "renewals": 1,
+    "item": "http://uri.gbv.de/document/opac-de-830:bar:830$22061137"
+  }]
+}
diff --git a/module/VuFind/tests/fixtures/paia/response/storageretrieval.json b/module/VuFind/tests/fixtures/paia/response/storageretrieval.json
new file mode 100644
index 0000000000000000000000000000000000000000..31cfed23463a048f883b9e82af861a1ffb36d2f1
--- /dev/null
+++ b/module/VuFind/tests/fixtures/paia/response/storageretrieval.json
@@ -0,0 +1,29 @@
+HTTP/1.1 200 OK
+Server: vzg-paia/2.2-RC3 (Wed Jun 01 17:38:24 CEST 2016)
+Date: Mon, 20 Jun 2016 11:26:47 GMT
+Cache-Control: no-cache
+Access-Control-Allow-Origin: *
+Access-Control-Expose-Headers: X-OAuth-Scopes, X-Accepted-OAuth-Scopes
+Access-Control-Allow-Headers: Content-Type, Authorization
+Pragma: no-cache
+X-OAuth-Scopes: read_availability change_password write_items read_items read_fees read_patron
+X-Accepted-OAuth-Scopes: write_items
+Content-Type: application/json; charset=UTF-8
+
+{
+  "doc": [{
+    "queue": 0,
+    "cancancel": false,
+    "storageid": "de-830:desk:1",
+    "label": "24:2401-4292",
+    "reminder": 0,
+    "edition": "http://uri.gbv.de/document/opac-de-830:ppn:221142312",
+    "endtime": "2016-07-18",
+    "status": 2,
+    "starttime": "2016-06-20T13:26:46+02:00",
+    "storage": "Ausleihe",
+    "about": "Konzepte von Datenbanken / Kuhlen, Rainer (1979)",
+    "renewals": 0,
+    "item": "http://uri.gbv.de/document/opac-de-830:bar:830$24014292"
+  }]
+}
\ No newline at end of file
diff --git a/module/VuFind/tests/integration-tests/src/VuFindTest/Auth/DatabaseTest.php b/module/VuFind/tests/integration-tests/src/VuFindTest/Auth/DatabaseTest.php
index 1b22a17b1690d718e2522bd7572050fda6b6a53f..8a6523ce38a5a085c23126b84e6988504a78b93d 100644
--- a/module/VuFind/tests/integration-tests/src/VuFindTest/Auth/DatabaseTest.php
+++ b/module/VuFind/tests/integration-tests/src/VuFindTest/Auth/DatabaseTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/integration-tests/src/VuFindTest/Auth/ILSTest.php b/module/VuFind/tests/integration-tests/src/VuFindTest/Auth/ILSTest.php
index 8b79a3ee73f61e034e04fbb989b6b0836147452e..5e4d8957072808656d9d5b2ec0d471f61035b8fb 100644
--- a/module/VuFind/tests/integration-tests/src/VuFindTest/Auth/ILSTest.php
+++ b/module/VuFind/tests/integration-tests/src/VuFindTest/Auth/ILSTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/integration-tests/src/VuFindTest/Auth/ShibbolethTest.php b/module/VuFind/tests/integration-tests/src/VuFindTest/Auth/ShibbolethTest.php
index c85cc73720f798d10e44fabcbdc4b20ca45142d4..345128d9e06343a6190dab81ff0ca367f5941cd1 100644
--- a/module/VuFind/tests/integration-tests/src/VuFindTest/Auth/ShibbolethTest.php
+++ b/module/VuFind/tests/integration-tests/src/VuFindTest/Auth/ShibbolethTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/integration-tests/src/VuFindTest/Connection/SolrAuthTest.php b/module/VuFind/tests/integration-tests/src/VuFindTest/Connection/SolrAuthTest.php
index 631f95efa991a647bcdb2e7ffc739ba529eb948e..5aa35e166b711899921aba5c0ef2a668e04dc4f1 100644
--- a/module/VuFind/tests/integration-tests/src/VuFindTest/Connection/SolrAuthTest.php
+++ b/module/VuFind/tests/integration-tests/src/VuFindTest/Connection/SolrAuthTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/integration-tests/src/VuFindTest/Connection/SolrTest.php b/module/VuFind/tests/integration-tests/src/VuFindTest/Connection/SolrTest.php
index 45b91bfcc089f2908dbb0b8655a4a6680f72a74b..bd26cea59ccae40f6652571d573549d45c831c14 100644
--- a/module/VuFind/tests/integration-tests/src/VuFindTest/Connection/SolrTest.php
+++ b/module/VuFind/tests/integration-tests/src/VuFindTest/Connection/SolrTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/integration-tests/src/VuFindTest/Db/Table/ChangeTrackerTest.php b/module/VuFind/tests/integration-tests/src/VuFindTest/Db/Table/ChangeTrackerTest.php
index fe32af5adfc788ec14cd42894d79f624f79d3270..f3064c1563044fed0de8a0c26ee4c06aea358203 100644
--- a/module/VuFind/tests/integration-tests/src/VuFindTest/Db/Table/ChangeTrackerTest.php
+++ b/module/VuFind/tests/integration-tests/src/VuFindTest/Db/Table/ChangeTrackerTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/integration-tests/src/VuFindTest/Mink/AccountActionsTest.php b/module/VuFind/tests/integration-tests/src/VuFindTest/Mink/AccountActionsTest.php
index ce672237a9a1521f89f3e7acd58baac494714d95..ec4e43e9ab5a80ce822a991918b492d4b64ec603 100644
--- a/module/VuFind/tests/integration-tests/src/VuFindTest/Mink/AccountActionsTest.php
+++ b/module/VuFind/tests/integration-tests/src/VuFindTest/Mink/AccountActionsTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
@@ -78,6 +78,7 @@ class AccountActionsTest extends \VuFindTest\Unit\MinkTestCase
         $this->findCss($page, '#loginOptions a')->click();
         $this->snooze();
         $this->findCss($page, '.modal-body .createAccountLink')->click();
+        $this->snooze();
         $this->fillInAccountForm($page);
         $this->findCss($page, '.modal-body .btn.btn-primary')->click();
         $this->snooze();
@@ -122,8 +123,8 @@ class AccountActionsTest extends \VuFindTest\Unit\MinkTestCase
         $this->findCss($page, '#loginOptions a')->click();
         $this->fillInLoginForm($page, 'username1', 'test');
         $this->findCss($page, '.modal-body .btn.btn-primary')->click();
-        $this->assertLightboxWarning($page, 'Invalid login -- please try again.');
         $this->snooze();
+        $this->assertLightboxWarning($page, 'Invalid login -- please try again.');
 
         // Now log in successfully:
         $this->fillInLoginForm($page, 'username1', 'good');
diff --git a/module/VuFind/tests/integration-tests/src/VuFindTest/Mink/AdvancedSearchTest.php b/module/VuFind/tests/integration-tests/src/VuFindTest/Mink/AdvancedSearchTest.php
index b513ab3e54ee5f984713915ebf5ae1d3e57dc2eb..d1ba6ad504600a4853c7d90a1049c3c695f8ed1e 100644
--- a/module/VuFind/tests/integration-tests/src/VuFindTest/Mink/AdvancedSearchTest.php
+++ b/module/VuFind/tests/integration-tests/src/VuFindTest/Mink/AdvancedSearchTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/integration-tests/src/VuFindTest/Mink/BasicTest.php b/module/VuFind/tests/integration-tests/src/VuFindTest/Mink/BasicTest.php
index 52f6cf83ca68adc6a2311ccedf2a007805a5f564..e009d10801d3425509b6f5ac72198e9c0562c9ac 100644
--- a/module/VuFind/tests/integration-tests/src/VuFindTest/Mink/BasicTest.php
+++ b/module/VuFind/tests/integration-tests/src/VuFindTest/Mink/BasicTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
@@ -47,7 +47,6 @@ class BasicTest extends \VuFindTest\Unit\MinkTestCase
     {
         $session = $this->getMinkSession();
         $session->visit($this->getVuFindUrl() . '/Search/Home');
-        $this->assertHttpStatus(200);
         $page = $session->getPage();
         $this->assertTrue(false !== strstr($page->getContent(), 'VuFind'));
     }
@@ -105,4 +104,24 @@ class BasicTest extends \VuFindTest\Unit\MinkTestCase
             $this->findCss($page, 'footer .help-link')->getHTML()
         );
     }
+
+    /**
+     * Test lightbox jump links
+     *
+     * @return void
+     */
+    public function testLightboxJumps()
+    {
+        $session = $this->getMinkSession();
+        $session->visit($this->getVuFindUrl() . '/Search/Home');
+        $page = $session->getPage();
+        // Open Search tips lightbox
+        $this->findCss($page, 'footer .help-link')->click();
+        $this->snooze();
+        // Click a jump link
+        $this->findCss($page, '.modal-body .HelpMenu a')->click();
+        // Make sure we're still in the Search Tips
+        $this->snooze();
+        $this->findCss($page, '.modal-body .HelpMenu');
+    }
 }
diff --git a/module/VuFind/tests/integration-tests/src/VuFindTest/Mink/BulkTest.php b/module/VuFind/tests/integration-tests/src/VuFindTest/Mink/BulkTest.php
index 3312d27e7e8b43dac7d3e7e7e0c03b029f20b73d..50c4236fe70a2a055b1be74d39ae63373f16badb 100644
--- a/module/VuFind/tests/integration-tests/src/VuFindTest/Mink/BulkTest.php
+++ b/module/VuFind/tests/integration-tests/src/VuFindTest/Mink/BulkTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/integration-tests/src/VuFindTest/Mink/CallnumberBrowseTest.php b/module/VuFind/tests/integration-tests/src/VuFindTest/Mink/CallnumberBrowseTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..eb0e9309637673b33ea83a1dbc937c085495e82d
--- /dev/null
+++ b/module/VuFind/tests/integration-tests/src/VuFindTest/Mink/CallnumberBrowseTest.php
@@ -0,0 +1,276 @@
+<?php
+/**
+ * Mink search actions test class.
+ *
+ * PHP version 5
+ *
+ * Copyright (C) Villanova University 2011.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ * @category VuFind
+ * @package  Tests
+ * @author   Demian Katz <demian.katz@villanova.edu>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     https://vufind.org Main Page
+ */
+namespace VuFindTest\Mink;
+
+/**
+ * Mink search actions test class.
+ *
+ * @category VuFind
+ * @package  Tests
+ * @author   Demian Katz <demian.katz@villanova.edu>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     https://vufind.org Main Page
+ */
+class CallnumberBrowseTest extends \VuFindTest\Unit\MinkTestCase
+{
+    protected $id = 'testdeweybrowse';
+
+    /**
+     * Standard setup method.
+     *
+     * @return void
+     */
+    public function setUp()
+    {
+        // Give up if we're not running in CI:
+        if (!$this->continuousIntegrationRunning()) {
+            return $this->markTestSkipped('Continuous integration not running.');
+        }
+    }
+
+    /**
+     * Search for the specified query.
+     *
+     * @param string $query Search term(s)
+     *
+     * @return \Behat\Mink\Element\Element
+     */
+    protected function performSearch($query, $page = false)
+    {
+        $session = $this->getMinkSession();
+        $session->visit($this->getVuFindUrl() . '/Search/Home');
+        $page = $session->getPage();
+        $this->findCss($page, '.searchForm [name="lookfor"]')->setValue($query);
+        $this->findCss($page, '.btn.btn-primary')->click();
+        $this->snooze();
+        return $page;
+    }
+
+    /**
+     * Set config for callnumber tests
+     * Sets callnumber_handler to false
+     *
+     * @param string $nos  multiple_call_nos setting
+     * @param string $locs multiple_locations setting
+     *
+     * @return void
+     */
+    protected function changeCallnumberSettings($nos, $locs, $full = false)
+    {
+        $this->changeConfigs(
+            [
+                'config' => [
+                    'Catalog' => ['driver' => 'Sample'],
+                    'Item_Status' => [
+                        'multiple_call_nos' => $nos,
+                        'multiple_locations' => $locs,
+                        'callnumber_handler' => false,
+                        'show_full_status' => $full
+                    ]
+                ]
+            ]
+        );
+    }
+
+    /**
+     * Checks if a link has the correct callnumber and setting
+     *
+     * @param \Behat\Mink\Element\Element $link link element
+     * @param string                      $type dewey or lcc
+     *
+     * @return void
+     */
+    protected function checkLink($link, $type)
+    {
+        $this->assertTrue(is_object($link));
+        $href = $link->getAttribute('href');
+        $this->assertContains($type, $href);
+        $this->assertNotEquals('', $link->getText());
+        $this->assertContains($link->getText(), $href);
+    }
+
+    protected function setupMultipleCallnumbers()
+    {
+        $this->changeConfigs([
+            'config' => [
+                'Catalog' => ['driver' => 'Demo']
+            ],
+            'Demo' => [
+                'Holdings' => [
+                    $this->id => json_encode([
+                        ['callnumber' => 'CallNumberOne', 'location' => 'Villanova'],
+                        ['callnumber' => 'CallNumberTwo', 'location' => 'Villanova'],
+                        ['callnumber' => 'CallNumberThree', 'location' => 'Phobos'],
+                        ['callnumber' => 'CallNumberFour', 'location' => 'Phobos']
+                    ])
+                ]
+            ]
+        ]);
+    }
+
+    /**
+     * Sets callnumber_handler to true
+     *
+     * @param string                      $type        dewey or lcc
+     * @param \Behat\Mink\Element\Element $page        page element
+     * @param bool                        $expectLinks links on multiple?
+     *
+     * @return void
+     */
+    protected function activateAndTestLinks($type, $page, $expectLinks)
+    {
+        // Single callnumbers (Sample)
+        $this->changeConfigs([
+            'config' => [
+                'Catalog' => ['driver' => 'Sample'],
+                'Item_Status' => ['callnumber_handler' => $type]
+            ]
+        ]);
+        $this->getMinkSession()->reload();
+        $this->snooze();
+        $link = $page->find('css', '.callnumber a,.groupCallnumber a,.fullCallnumber a');
+        $this->checkLink($link, $type);
+
+        // Multiple callnumbers
+        $this->setupMultipleCallnumbers();
+        $this->getMinkSession()->reload();
+        $this->snooze();
+        $link = $page->find('css', '.callnumber a,.groupCallnumber a,.fullCallnumber a');
+        if ($expectLinks) {
+            $this->checkLink($link, $type);
+            // TODO
+            // if 'all'
+            // - refresh until multiple
+            // - test multiple
+            // else
+        } else {
+            $this->assertTrue(is_null($link));
+        }
+    }
+
+    /**
+     * Sets callnumber_handler to true
+     *
+     * @param string $nos         multiple_call_nos setting
+     * @param string $locs        multiple_locations setting
+     * @param bool   $expectLinks whether or not links are expected for multiple callnumbers in this config
+     *
+     * @return void
+     */
+    protected function validateSetting($nos = 'first', $locs = 'msg', $expectLinks = true, $full = false)
+    {
+        $this->changeCallnumberSettings($nos, $locs, $full);
+        $page = $this->performSearch('id:' . $this->id);
+        // No link
+        $link = $page->find('css', '.callnumber a,.groupCallnumber a,.fullCallnumber a');
+        $this->assertTrue(is_null($link));
+        // With dewey links
+        $this->activateAndTestLinks('dewey', $page, $expectLinks);
+        // With lcc links
+        $this->activateAndTestLinks('lcc', $page, $expectLinks);
+    }
+
+    /**
+     * Test with multiple_call_nos set to first
+     * and multiple_locations set to msg
+     *
+     * @return void
+     */
+    public function testFirstAndMsg()
+    {
+        $this->changeConfigs([
+            'config' => ['Item_Status' => ['show_full_status' => false]]
+        ]);
+        $this->validateSetting('first');
+    }
+
+    /**
+     * Test with multiple_call_nos set to first
+     * and multiple_locations set to msg
+     *
+     * @return void
+     */
+    public function testAllAndMsg()
+    {
+        $this->validateSetting('all');
+    }
+
+    /**
+     * Test with multiple_call_nos set to first
+     * and multiple_locations set to msg
+     *
+     * @return void
+     */
+    public function testMsgAndMsg()
+    {
+        $this->validateSetting('msg', 'msg', false);
+    }
+
+    /**
+     * Test with multiple_call_nos set to first
+     * and multiple_locations set to group
+     *
+     * @return void
+     */
+    public function testFirstAndGroup()
+    {
+        $this->validateSetting('first', 'group');
+    }
+
+    /**
+     * Test with multiple_call_nos set to all
+     * and multiple_locations set to msg
+     *
+     * @return void
+     */
+    public function testAllAndGroup()
+    {
+        $this->validateSetting('all', 'group');
+    }
+
+    /**
+     * Test with multiple_call_nos set to msg
+     * and multiple_locations set to group
+     *
+     * @return void
+     */
+    public function testMsgAndGroup()
+    {
+        $this->validateSetting('msg', 'group', false);
+    }
+
+    /**
+     * Test with show_full_status set to true
+     *
+     * @return void
+     */
+    public function testStatusFull()
+    {
+        $this->validateSetting('first', 'msg', true, true);
+    }
+}
diff --git a/module/VuFind/tests/integration-tests/src/VuFindTest/Mink/CartTest.php b/module/VuFind/tests/integration-tests/src/VuFindTest/Mink/CartTest.php
index c8af0023f0afa4e9a63d0b7259dfd00198efe523..eaade7d8a92f611b601b6f56100c9bb256b61ce0 100644
--- a/module/VuFind/tests/integration-tests/src/VuFindTest/Mink/CartTest.php
+++ b/module/VuFind/tests/integration-tests/src/VuFindTest/Mink/CartTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
@@ -64,6 +64,21 @@ class CartTest extends \VuFindTest\Unit\MinkTestCase
         return $session->getPage();
     }
 
+    /**
+     * Get a reference to a standard search results page.
+     *
+     * @param string $id Record ID to load.
+     *
+     * @return Element
+     */
+    protected function getRecordPage($id)
+    {
+        $session = $this->getMinkSession();
+        $path = '/Record/' . urlencode($id);
+        $session->visit($this->getVuFindUrl() . $path);
+        return $session->getPage();
+    }
+
     /**
      * Click the "add to cart" button with nothing selected; fail if this does
      * not display an appropriate message.
@@ -92,6 +107,34 @@ class CartTest extends \VuFindTest\Unit\MinkTestCase
         $this->fail('Too many retries on check for error message.');
     }
 
+    /**
+     * Click the "add to cart" button with duplicate IDs selected; fail if this does
+     * not display an appropriate message.
+     *
+     * @param Element $page       Page element
+     * @param Element $updateCart Add to cart button
+     *
+     * @return void
+     */
+    protected function tryAddingDuplicatesToCart(Element $page, Element $updateCart)
+    {
+        // This test is a bit timing-sensitive, so introduce a retry loop before
+        // completely failing.
+        for ($clickRetry = 0; $clickRetry <= 4; $clickRetry++) {
+            $updateCart->click();
+            $content = $page->find('css', '.popover-content');
+            if (is_object($content)) {
+                $this->assertEquals(
+                    '0 item(s) added to your Book Bag 2 item(s) are either '
+                    . 'already in your Book Bag or could not be added',
+                    $content->getText()
+                );
+                return;
+            }
+        }
+        $this->fail('Too many retries on check for error message.');
+    }
+
     /**
      * Add the current page of results to the cart.
      *
@@ -128,23 +171,16 @@ class CartTest extends \VuFindTest\Unit\MinkTestCase
      *
      * @return Element
      */
-    protected function setUpGenericCartTest()
+    protected function setUpGenericCartTest($extraConfigs = [])
     {
         // Activate the cart:
-        $this->changeConfigs(
-            ['config' =>
-                [
-                    'Site' => ['showBookBag' => true, 'theme' => 'bootprint3'],
-                    'Mail' => ['testOnly' => 1],
-                ],
-            ]
-        );
+        $extraConfigs['config']['Site'] = ['showBookBag' => true];
+        $this->changeConfigs($extraConfigs);
 
         $page = $this->getSearchResultsPage();
 
         // Click "add" without selecting anything.
         $updateCart = $this->findCss($page, '#updateCart');
-        $this->tryAddingNothingToCart($page, $updateCart);
 
         // Now actually select something:
         $this->addCurrentPageToCart($page, $updateCart);
@@ -152,6 +188,7 @@ class CartTest extends \VuFindTest\Unit\MinkTestCase
 
         // Open the cart and empty it:
         $this->openCartLightbox($page);
+        $this->snooze();
 
         return $page;
     }
@@ -215,8 +252,123 @@ class CartTest extends \VuFindTest\Unit\MinkTestCase
     {
         $cartSelectAll = $page->find('css', '.modal-dialog .checkbox-select-all');
         $cartSelectAll->check();
+        $this->snooze();
     }
 
+    /**
+     * Test that adding nothing to the cart triggers an appropriate message.
+     *
+     * @return void
+     */
+    public function testAddingNothing()
+    {
+        // Activate the cart:
+        $this->changeConfigs(['config' => ['Site' => ['showBookBag' => true]]]);
+
+        $page = $this->getSearchResultsPage();
+
+        // Click "add" without selecting anything.
+        $updateCart = $this->findCss($page, '#updateCart');
+        $this->tryAddingNothingToCart($page, $updateCart);
+    }
+
+    /**
+     * Test that adding the same records to the cart multiple times triggers an
+     * appropriate message.
+     *
+     * @return void
+     */
+    public function testAddingDuplicates()
+    {
+         // Activate the cart:
+        $this->changeConfigs(['config' => ['Site' => ['showBookBag' => true]]]);
+
+        $page = $this->getSearchResultsPage();
+
+        // Now select the same things twice:
+        $updateCart = $this->findCss($page, '#updateCart');
+        $this->addCurrentPageToCart($page, $updateCart);
+        $this->assertEquals('2', $this->findCss($page, '#cartItems strong')->getText());
+        $this->tryAddingDuplicatesToCart($page, $updateCart);
+        $this->assertEquals('2', $this->findCss($page, '#cartItems strong')->getText());
+   }
+
+    /**
+     * Test that the cart limit is enforced from search results.
+     *
+     * @return void
+     */
+    public function testOverfillingCart()
+    {
+         // Activate the cart:
+        $this->changeConfigs(
+            ['config' => ['Site' => ['showBookBag' => true, 'bookBagMaxSize' => 1]]]
+        );
+
+        $page = $this->getSearchResultsPage();
+
+        // Now select the same things twice:
+        $updateCart = $this->findCss($page, '#updateCart');
+        $this->addCurrentPageToCart($page, $updateCart);
+        $this->assertEquals('1', $this->findCss($page, '#cartItems strong')->getText());
+   }
+
+    /**
+     * Test that the cart limit is enforced from record pages.
+     *
+     * @return void
+     */
+    public function testOverfillingCartFromRecordPage()
+    {
+         // Activate the cart:
+        $this->changeConfigs(
+            ['config' => ['Site' => ['showBookBag' => true, 'bookBagMaxSize' => 1]]]
+        );
+
+        $page = $this->getRecordPage('testsample1');
+
+        // Test that we can toggle the cart item back and forth:
+        $cartItems = $this->findCss($page, '#cartItems');
+        $add = $this->findCss($page, '.cart-add');
+        $remove = $this->findCss($page, '.cart-remove');
+        $add->click();
+        $this->assertEquals('1 items (Full)', $cartItems->getText());
+        $remove->click();
+        $this->assertEquals('0 items', $cartItems->getText());
+        $add->click();
+        $this->assertEquals('1 items (Full)', $cartItems->getText());
+
+        // Now move to another page and try to add a second item -- it should
+        // not be added due to cart limit:
+        $page = $this->getRecordPage('testsample2');
+        $cartItems = $this->findCss($page, '#cartItems');
+        $add = $this->findCss($page, '.cart-add');
+        $add->click();
+        $this->assertEquals('1 items (Full)', $cartItems->getText());
+   }
+
+    /**
+     * Test that the record "add to cart" button functions.
+     *
+     * @return void
+     */
+    public function testAddingMultipleRecordsFromRecordPage()
+    {
+         // Activate the cart:
+        $this->changeConfigs(
+            ['config' => ['Site' => ['showBookBag' => true]]]
+        );
+
+        // Test that we can add multiple records:
+        for ($x = 1; $x <= 3; $x++) {
+            $page = $this->getRecordPage('testsample' . $x);
+            $this->findCss($page, '.cart-add')->click();
+            $this->assertEquals(
+                $x . ' items', $this->findCss($page, '#cartItems')->getText()
+            );
+        }
+   }
+
     /**
      * Test that we can put items in the cart and then remove them with the
      * delete control.
@@ -275,6 +427,34 @@ class CartTest extends \VuFindTest\Unit\MinkTestCase
         $this->assertEquals('0', $this->findCss($page, '#cartItems strong')->getText());
     }
 
+    /**
+     * Test that we can put items in the cart and then remove them outside of
+     * the lightbox.
+     *
+     * @return void
+     */
+    public function testFillAndEmptyCartWithoutLightbox()
+    {
+        // Turn on limit by path setting; there used to be a bug where cookie
+        // paths were set inconsistently between JS and server-side code. This
+        // test should catch any regressions in that area.
+        $page = $this->setUpGenericCartTest(
+            ['config' => ['Cookies' => ['limit_by_path' => 1]]]
+        );
+
+        // Go to the cart page and activate the "empty" control:
+        $session = $this->getMinkSession();
+        $session->visit($this->getVuFindUrl() . '/Cart');
+        $empty = $this->findCss($page, '#cart-empty-label');
+        $empty->click();
+        $emptyConfirm = $this->findCss($page, '#cart-confirm-empty');
+        $emptyConfirm->click();
+
+        // Confirm that the cart has truly been emptied:
+        $this->snooze(); // wait for display to update
+        $this->assertEquals('0', $this->findCss($page, '#cartItems strong')->getText());
+    }
+
     /**
      * Test that the email control works.
      *
@@ -282,7 +462,9 @@ class CartTest extends \VuFindTest\Unit\MinkTestCase
      */
     public function testCartEmail()
     {
-        $page = $this->setUpGenericCartTest();
+        $page = $this->setUpGenericCartTest(
+            ['config' => ['Mail' => ['testOnly' => 1]]]
+        );
         $button = $this->findCss($page, '.cart-controls button[name=email]');
 
         // First try clicking without selecting anything:
@@ -383,10 +565,53 @@ class CartTest extends \VuFindTest\Unit\MinkTestCase
         // Do the export:
         $submit = $this->findCss($page, '.modal-body input[name=submit]');
         $submit->click();
+        $this->snooze();
         $result = $this->findCss($page, '.modal-body .alert .text-center .btn');
         $this->assertEquals('Download File', $result->getText());
     }
 
+    /**
+     * Test that the export control works when redirecting to a third-party site.
+     *
+     * @return void
+     */
+    public function testCartExportToThirdParty()
+    {
+        $page = $this->setUpGenericCartTest(
+            [
+                'config' => [
+                    'Export' => [
+                        'Google' => 'record,bulk',
+                    ],
+                ],
+                'export' => [
+                    'Google' => [
+                        'requiredMethods[]' => 'getTitle',
+                        'redirectUrl' => 'https://www.google.com',
+                        'headers[]' => 'Content-type: text/plain; charset=utf-8',
+                    ],
+                ],
+            ]
+        );
+        $button = $this->findCss($page, '.cart-controls button[name=export]');
+
+        // Go to export option list:
+        $this->selectAllItemsInCart($page);
+        $button->click();
+
+        // Select EndNote option
+        $select = $this->findCss($page, '#format');
+        $select->selectOption('Google');
+
+        // Do the export:
+        $submit = $this->findCss($page, '.modal-body input[name=submit]');
+        $submit->click();
+        $this->snooze();
+        $this->assertEquals(
+            'https://www.google.com/', $this->getMinkSession()->getCurrentUrl()
+        );
+    }
+
     /**
      * Test that the print control works.
      *
diff --git a/module/VuFind/tests/integration-tests/src/VuFindTest/Mink/ChoiceAuthTest.php b/module/VuFind/tests/integration-tests/src/VuFindTest/Mink/ChoiceAuthTest.php
index 709737acbdb96cce3ab1234149202fbfd1a11e79..1947cc81bf609a17fa468302b1c1dc5b42396690 100644
--- a/module/VuFind/tests/integration-tests/src/VuFindTest/Mink/ChoiceAuthTest.php
+++ b/module/VuFind/tests/integration-tests/src/VuFindTest/Mink/ChoiceAuthTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/integration-tests/src/VuFindTest/Mink/CombinedSearchTest.php b/module/VuFind/tests/integration-tests/src/VuFindTest/Mink/CombinedSearchTest.php
index 838489b0cee61130ee120d64ea4c64cf74abce5d..f217ff5176c6c3b21424078b38c51b71a75528b1 100644
--- a/module/VuFind/tests/integration-tests/src/VuFindTest/Mink/CombinedSearchTest.php
+++ b/module/VuFind/tests/integration-tests/src/VuFindTest/Mink/CombinedSearchTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/integration-tests/src/VuFindTest/Mink/FavoritesTest.php b/module/VuFind/tests/integration-tests/src/VuFindTest/Mink/FavoritesTest.php
index 351124e37b8f9a185df53a5493a526e0c605b465..a404209b31e32639816487579992a023e0ecc6df 100644
--- a/module/VuFind/tests/integration-tests/src/VuFindTest/Mink/FavoritesTest.php
+++ b/module/VuFind/tests/integration-tests/src/VuFindTest/Mink/FavoritesTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
@@ -391,6 +391,13 @@ class FavoritesTest extends \VuFindTest\Unit\MinkTestCase
      */
     protected function setupBulkTest()
     {
+        $this->changeConfigs(
+            ['config' =>
+                [
+                    'Mail' => ['testOnly' => 1],
+                ],
+            ]
+        );
         // Go home
         $session = $this->getMinkSession();
         $path = '/Search/Home';
diff --git a/module/VuFind/tests/integration-tests/src/VuFindTest/Mink/FeedbackTest.php b/module/VuFind/tests/integration-tests/src/VuFindTest/Mink/FeedbackTest.php
index 292cf57dd88f85ee6b0451014697ac9d19bcc339..7842bae9b99b0458bdf68c4be9037d584f285358 100644
--- a/module/VuFind/tests/integration-tests/src/VuFindTest/Mink/FeedbackTest.php
+++ b/module/VuFind/tests/integration-tests/src/VuFindTest/Mink/FeedbackTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/integration-tests/src/VuFindTest/Mink/IlsActionsTest.php b/module/VuFind/tests/integration-tests/src/VuFindTest/Mink/IlsActionsTest.php
index 3b6da18d319424d2aeca70517c9e72901619edb2..bbc92f31e0d19531da08beeb5c7541cb93777bc9 100644
--- a/module/VuFind/tests/integration-tests/src/VuFindTest/Mink/IlsActionsTest.php
+++ b/module/VuFind/tests/integration-tests/src/VuFindTest/Mink/IlsActionsTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/integration-tests/src/VuFindTest/Mink/LinkResolverTest.php b/module/VuFind/tests/integration-tests/src/VuFindTest/Mink/LinkResolverTest.php
index 826526bd2fd9e13f5d00059c521f1cb337966f2e..7d565501f5b6e3169a613946327b31be28553899 100644
--- a/module/VuFind/tests/integration-tests/src/VuFindTest/Mink/LinkResolverTest.php
+++ b/module/VuFind/tests/integration-tests/src/VuFindTest/Mink/LinkResolverTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/integration-tests/src/VuFindTest/Mink/ListViewsTest.php b/module/VuFind/tests/integration-tests/src/VuFindTest/Mink/ListViewsTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..69fbd8a79ab22ecd9944a19419fe10f00d4c2b1c
--- /dev/null
+++ b/module/VuFind/tests/integration-tests/src/VuFindTest/Mink/ListViewsTest.php
@@ -0,0 +1,242 @@
+<?php
+/**
+ * List views (i.e. tabs/accordion) test class.
+ *
+ * PHP version 5
+ *
+ * Copyright (C) Villanova University 2011.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ * @category VuFind2
+ * @package  Tests
+ * @author   Demian Katz <demian.katz@villanova.edu>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     http://www.vufind.org  Main Page
+ */
+namespace VuFindTest\Mink;
+use Behat\Mink\Element\Element;
+
+/**
+ * List views (i.e. tabs/accordion) test class.
+ *
+ * @category VuFind2
+ * @package  Tests
+ * @author   Demian Katz <demian.katz@villanova.edu>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     http://www.vufind.org  Main Page
+ */
+class ListViewsTest extends \VuFindTest\Unit\MinkTestCase
+{
+    use \VuFindTest\Unit\UserCreationTrait;
+
+    /**
+     * Standard setup method.
+     *
+     * @return mixed
+     */
+    public static function setUpBeforeClass()
+    {
+        return static::failIfUsersExist();
+    }
+
+    /**
+     * Standard setup method.
+     *
+     * @return void
+     */
+    public function setUp()
+    {
+        // Give up if we're not running in CI:
+        if (!$this->continuousIntegrationRunning()) {
+            return $this->markTestSkipped('Continuous integration not running.');
+        }
+    }
+
+    /**
+     * Perform a search and return the page after submitting the form.
+     *
+     * @return Element
+     */
+    protected function gotoSearch()
+    {
+        $session = $this->getMinkSession();
+        $session->visit($this->getVuFindUrl() . '/Search/Home');
+        $page = $session->getPage();
+        $this->findCss($page, '.searchForm [name="lookfor"]')
+            ->setValue('id:testdeweybrowse');
+        $this->findCss($page, '.btn.btn-primary')->click();
+        $this->snooze();
+        return $page;
+    }
+
+    /**
+     * Perform a search and return the page after submitting the form and
+     * clicking the first record.
+     *
+     * @return Element
+     */
+    protected function gotoRecord()
+    {
+        $page = $this->gotoSearch();
+        $this->findCss($page, '.result a.title')->click();
+        $this->snooze();
+        return $page;
+    }
+
+    /**
+     * Test that we can save a favorite from tab mode.
+     *
+     * @return void
+     */
+    public function testFavoritesInTabMode()
+    {
+        // Change the theme:
+        $this->changeConfigs(
+            ['searches' => ['List' => ['view' => 'tabs']]]
+        );
+
+        $page = $this->gotoRecord();
+
+        // Click save inside the tools tab
+        $this->findCss($page, '#tools_cd588d8723d65ca0ce9439e79755fa0a')->click();
+        $this->findCss($page, '#tools_cd588d8723d65ca0ce9439e79755fa0a-content .save-record')->click();
+        // Make an account
+        $this->findCss($page, '.modal-body .createAccountLink')->click();
+        $this->fillInAccountForm($page);
+        $this->findCss($page, '.modal-body .btn.btn-primary')->click();
+        $this->snooze();
+        $this->findCss($page, '#save_list');
+        // Save to list
+        $this->findCss($page, '.modal-body .btn.btn-primary')->click();
+        $this->snooze();
+        $this->findCss($page, '#modal .close')->click();
+        $this->snooze();
+        // Check saved items status
+        $this->findCss($page, '#information_cd588d8723d65ca0ce9439e79755fa0a-content .savedLists ul');
+    }
+
+    /**
+     * Test that we can save a favorite from accordion mode.
+     *
+     * @return void
+     */
+    public function testFavoritesInAccordionMode()
+    {
+        // Change the theme:
+        $this->changeConfigs(
+            ['searches' => ['List' => ['view' => 'accordion']]]
+        );
+
+        $page = $this->gotoRecord();
+
+        // Click save inside the tools tab
+        $this->findCss($page, '#tools_cd588d8723d65ca0ce9439e79755fa0a')->click();
+        $this->findCss($page, '#tools_cd588d8723d65ca0ce9439e79755fa0a-content .save-record')->click();
+        // Login
+        $this->fillInLoginForm($page, 'username1', 'test');
+        $this->submitLoginForm($page);
+        // Make list
+        $this->findCss($page, '#make-list')->click();
+        $this->snooze();
+        $this->findCss($page, '#list_title')->setValue('Test List');
+        $this->findCss($page, '#list_desc')->setValue('Just. THE BEST.');
+        $this->findCss($page, '.modal-body .btn.btn-primary')->click();
+        $this->snooze();
+        // Save to list
+        $this->findCss($page, '.modal-body .btn.btn-primary')->click();
+        $this->snooze();
+        $this->findCss($page, '#modal .close')->click();
+        $this->snooze();
+        // Check saved items status
+        // Not visible, but still exists
+        $this->findCss($page, '#information_cd588d8723d65ca0ce9439e79755fa0a-content .savedLists ul');
+    }
+
+    /**
+     * Test localStorage saving from tab mode.
+     *
+     * @return void
+     */
+    protected function localStorageDance()
+    {
+        $page = $this->gotoRecord();
+        $session = $this->getMinkSession();
+
+        // Reload the page to close all results
+        $session->reload();
+        // Did our saved one open automatically?
+        $this->findCss($page, '.result.embedded');
+
+        // Close it
+        $this->findCss($page, '.result a.title')->click();
+        // Did our result stay closed?
+        $session->reload();
+        $result = $page->find('css', '.result.embedded');
+        $this->assertFalse(is_object($result));
+
+        // Open it
+        $this->findCss($page, '.result a.title')->click();
+        $this->snooze();
+        // Search for anything else
+        $session->visit($this->getVuFindUrl() . '/Search/Home');
+        $page = $session->getPage();
+        $this->findCss($page, '.searchForm [name="lookfor"]')
+            ->setValue('anything else');
+        $this->findCss($page, '.btn.btn-primary')->click();
+        // Come back
+        $page = $this->gotoSearch();
+        // Did our result close after not being being in the last search?
+        $result = $page->find('css', '.result.embedded');
+        $this->assertFalse(is_object($result));
+    }
+
+    /**
+     * Test localStorage saving from tab mode.
+     *
+     * @return void
+     */
+    public function testSavedOpenInTabsMode()
+    {
+        // Change the theme:
+        $this->changeConfigs(
+            ['searches' => ['List' => ['view' => 'tabs']]]
+        );
+        $this->localStorageDance();
+    }
+
+    /**
+     * Test localStorage saving from accordion mode.
+     *
+     * @return void
+     */
+    public function testSavedOpenInAccordionMode()
+    {
+        // Change the theme:
+        $this->changeConfigs(
+            ['searches' => ['List' => ['view' => 'accordion']]]
+        );
+        $this->localStorageDance();
+    }
+
+    /**
+     * Standard teardown method.
+     *
+     * @return void
+     */
+    public static function tearDownAfterClass()
+    {
+        static::removeUsers(['username1']);
+    }
+}
diff --git a/module/VuFind/tests/integration-tests/src/VuFindTest/Mink/RecordActionsTest.php b/module/VuFind/tests/integration-tests/src/VuFindTest/Mink/RecordActionsTest.php
index 3794601b02eaba402a43d41dc3b9bf0ed59451cd..708d57f8bbcc6bce2fadfaf1d59c06f1d133f873 100644
--- a/module/VuFind/tests/integration-tests/src/VuFindTest/Mink/RecordActionsTest.php
+++ b/module/VuFind/tests/integration-tests/src/VuFindTest/Mink/RecordActionsTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
@@ -84,7 +84,8 @@ class RecordActionsTest extends \VuFindTest\Unit\MinkTestCase
      *
      * @return void
      */
-    protected function makeAccount($page, $username) {
+    protected function makeAccount($page, $username)
+    {
         $this->findCss($page, '.modal-body .createAccountLink')->click();
         $this->snooze();
         $this->fillInAccountForm(
@@ -123,7 +124,7 @@ class RecordActionsTest extends \VuFindTest\Unit\MinkTestCase
         // Create new account
         $this->makeAccount($page, 'username1');
         // Make sure page updated for login
-        $page = $this->gotoRecord();
+        // $page = $this->gotoRecord();
         $this->findCss($page, '.record-tabs .usercomments')->click();
         $this->assertEquals(// Can Comment?
             'Add your comment',
@@ -145,7 +146,7 @@ class RecordActionsTest extends \VuFindTest\Unit\MinkTestCase
     }
 
     /**
-     * Test adding comments on records.
+     * Test adding tags on records.
      *
      * @return void
      */
@@ -173,7 +174,7 @@ class RecordActionsTest extends \VuFindTest\Unit\MinkTestCase
         $this->findCss($page, '.logoutOptions a.logout')->click();
         $this->snooze();
         // Login
-        $page = $this->gotoRecord(); // redirects to search home???
+        // $page = $this->gotoRecord();
         $this->findCss($page, '.tag-record')->click();
         $this->snooze();
         $this->fillInLoginForm($page, 'username2', 'test');
@@ -223,7 +224,7 @@ class RecordActionsTest extends \VuFindTest\Unit\MinkTestCase
         $this->fillInLoginForm($page, 'username1', 'test');
         $this->findCss($page, '.modal-body .btn.btn-primary')->click();
         $this->snooze();
-        $page = $this->gotoRecord();
+        // $page = $this->gotoRecord();
         // Check selected == 0
         $this->assertNull($page->find('css', '.tagList .tag.selected'));
         $this->findCss($page, '.tagList .tag');
@@ -241,6 +242,41 @@ class RecordActionsTest extends \VuFindTest\Unit\MinkTestCase
         $this->findCss($page, '.logoutOptions a.logout')->click();
     }
 
+    /**
+     * Test adding case sensitive tags on records.
+     *
+     * @return void
+     */
+    public function testAddSensitiveTag()
+    {
+        // Change the theme:
+        $this->changeConfigs(
+            [
+                'config' => [
+                    'Site' => ['theme' => 'bootstrap3'],
+                    'Social' => ['case_sensitive_tags' => 'true']
+                ]
+            ]
+        );
+        // Login
+        $page = $this->gotoRecord();
+        $this->findCss($page, '.tag-record')->click();
+        $this->snooze();
+        $this->fillInLoginForm($page, 'username2', 'test');
+        $this->submitLoginForm($page);
+        // Add tags
+        $this->findCss($page, '.modal #addtag_tag')->setValue('one ONE "new tag" ONE "THREE 4"');
+        $this->findCss($page, '.modal-body .btn.btn-primary')->click();
+        $this->snooze();
+        $success = $this->findCss($page, '.modal-body .alert-success');
+        $this->assertEquals('Tags Saved', $success->getText());
+        $this->findCss($page, '.modal .close')->click();
+        // Count tags
+        $this->snooze();
+        $tags = $page->findAll('css', '.tagList .tag');
+        $this->assertEquals(6, count($tags));
+    }
+
     /**
      * Test record view email.
      *
diff --git a/module/VuFind/tests/integration-tests/src/VuFindTest/Mink/RecordTest.php b/module/VuFind/tests/integration-tests/src/VuFindTest/Mink/RecordTest.php
index 7e99ff4583957c1bf269e42f41562c9a5027d76d..557b2d3604877fc6559f6e93ac61ad7580a6183b 100644
--- a/module/VuFind/tests/integration-tests/src/VuFindTest/Mink/RecordTest.php
+++ b/module/VuFind/tests/integration-tests/src/VuFindTest/Mink/RecordTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
@@ -53,7 +53,6 @@ class RecordTest extends \VuFindTest\Unit\MinkTestCase
         );
         $session = $this->getMinkSession();
         $session->visit($url);
-        $this->assertHttpStatus(200);
         $page = $session->getPage();
         $staffViewTab = $this->findCss($page, '.record-tabs .details');
         $this->assertEquals('Staff View', $staffViewTab->getText());
@@ -64,6 +63,38 @@ class RecordTest extends \VuFindTest\Unit\MinkTestCase
         $this->assertEquals('LEADER', substr($staffViewTable->getText(), 0, 6));
     }
 
+    /**
+     * Test that we can start on a hashed URL and then move back to the default
+     * tab from there.
+     *
+     * @param string $id       ID to load
+     * @param bool   $encodeId Should we URL encode the ID?
+     *
+     * @return void
+     */
+    protected function tryLoadingTabHashAndReturningToDefault($id, $encodeId = true)
+    {
+        // special test for going back to default tab from non-default URL
+        $url = $this->getVuFindUrl(
+            '/Record/' . ($encodeId ? rawurlencode($id) : $id) . '/Holdings#details'
+        );
+        $session = $this->getMinkSession();
+        $session->visit($url);
+        $page = $session->getPage();
+        $this->snooze();
+        $staffViewTable = $this->findCss($page, '.record-tabs .details-tab table.citation');
+        $this->assertEquals('LEADER', substr($staffViewTable->getText(), 0, 6));
+        $page = $session->getPage();
+        $staffViewTab = $this->findCss($page, '.record-tabs .holdings');
+        $this->assertEquals('Holdings', $staffViewTab->getText());
+        $staffViewTab->click();
+        $this->snooze();
+        $holdingsTabHeader = $this->findCss($page, '.record-tabs .holdings-tab h3');
+        $this->assertEquals('3rd Floor Main Library', $holdingsTabHeader->getText());
+        list($baseUrl) = explode('#', $url);
+        $this->assertEquals($baseUrl, $session->getCurrentUrl());
+    }
+
     /**
      * Test that record tabs work with a "normal" ID.
      *
@@ -72,6 +103,7 @@ class RecordTest extends \VuFindTest\Unit\MinkTestCase
     public function testRecordTabsOnNormalId()
     {
         $this->tryRecordTabsOnId('testsample1');
+        $this->tryLoadingTabHashAndReturningToDefault('testsample2');
     }
 
     /**
@@ -82,6 +114,9 @@ class RecordTest extends \VuFindTest\Unit\MinkTestCase
     public function testRecordTabsOnSpacedId()
     {
         $this->tryRecordTabsOnId('dot.dash-underscore__3.space suffix');
+        $this->tryLoadingTabHashAndReturningToDefault(
+            'dot.dash-underscore__3.space suffix'
+        );
     }
 
     /**
@@ -94,5 +129,23 @@ class RecordTest extends \VuFindTest\Unit\MinkTestCase
         // Skip encoding on this one, because Zend Framework doesn't URL encode
         // plus signs in route segments!
         $this->tryRecordTabsOnId('theplus+andtheminus-', false);
+        $this->tryLoadingTabHashAndReturningToDefault(
+            'theplus+andtheminus-', false
+        );
+    }
+
+    /**
+     * Test that tabs work correctly with loadInitialTabWithAjax turned on.
+     *
+     * @return void
+     */
+    public function testLoadInitialTabWithAjax()
+    {
+        $this->changeConfigs(
+            ['config' => ['Site' => ['loadInitialTabWithAjax' => 1]]]
+        );
+        $this->tryRecordTabsOnId('testsample1');
+        $this->tryLoadingTabHashAndReturningToDefault('testsample2');
+
     }
 }
diff --git a/module/VuFind/tests/integration-tests/src/VuFindTest/Mink/SearchActionsTest.php b/module/VuFind/tests/integration-tests/src/VuFindTest/Mink/SearchActionsTest.php
index 13a298c798ef99f0996d83f65f6ab5d2fe41134d..9985a75807e38bbba0d3b5158045c019151382c5 100644
--- a/module/VuFind/tests/integration-tests/src/VuFindTest/Mink/SearchActionsTest.php
+++ b/module/VuFind/tests/integration-tests/src/VuFindTest/Mink/SearchActionsTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
@@ -200,6 +200,246 @@ class SearchActionsTest extends \VuFindTest\Unit\MinkTestCase
         );
     }
 
+    /**
+     * Helper function for facets lists
+     *
+     * @param \Behat\Mink\Element\Element $page            Mink page object
+     * @param int                         $limit           Configured lightbox length
+     * @param bool                        $exclusionActive Is facet exclusion on?
+     *
+     * @return void
+     */
+    protected function facetListProcedure($page, $limit, $exclusionActive = false)
+    {
+        $this->snooze();
+        $items = $page->findAll('css', '#modal #facet-list-count .js-facet-item');
+        $this->assertEquals($limit, count($items));
+        $excludes = $page
+            ->findAll('css', '#modal #facet-list-count .badge .fa-times');
+        $this->assertEquals($exclusionActive ? $limit : 0, count($excludes));
+        // more
+        $this->findCss($page, '#modal .js-facet-next-page')->click();
+        $this->snooze();
+        $items = $page->findAll('css', '#modal #facet-list-count .js-facet-item');
+        $this->assertEquals($limit * 2, count($items));
+        $this->assertEquals(
+            'Weird IDs 9 '
+            . 'Fiction 7 '
+            . 'The Study Of P|pes 1 '
+            . 'The Study and Scor_ng of Dots.and-Dashes:Colons 1 '
+            . 'The Study of "Important" Things 1 '
+            . 'The Study of %\'s? 1 '
+            . 'The Study of +\'s? 1 '
+            . 'The Study of @Twitter #test 1 '
+            . 'more ...',
+            $this->findCss($page, '#modal #facet-list-count')->getText()
+        );
+        $excludes = $page
+            ->findAll('css', '#modal #facet-list-count .badge .fa-times');
+        $this->assertEquals($exclusionActive ? $limit * 2 : 0, count($excludes));
+
+        // sort by title
+        $this->findCss($page, '[data-sort="index"]')->click();
+        $this->snooze();
+        $items = $page->findAll('css', '#modal #facet-list-index .js-facet-item');
+        $this->assertEquals($limit, count($items)); // reset number of items
+        $this->assertEquals(
+            'Fiction 7 '
+            . 'The Study Of P|pes 1 '
+            . 'The Study and Scor_ng of Dots.and-Dashes:Colons 1 '
+            . 'The Study of "Important" Things 1 '
+            . 'more ...',
+            $this->findCss($page, '#modal #facet-list-index')->getText()
+        );
+        $excludes = $page
+            ->findAll('css', '#modal #facet-list-index .badge .fa-times');
+        $this->assertEquals($exclusionActive ? $limit : 0, count($excludes));
+        // sort by index again
+        $this->findCss($page, '[data-sort="count"]')->click();
+        $this->snooze();
+        $items = $page->findAll('css', '#modal #facet-list-count .js-facet-item');
+        $this->assertEquals($limit * 2, count($items)); // maintain number of items
+        // When exclusion is active, the result count is outside of the link tag:
+        $expectedLinkText = $exclusionActive ? 'Weird IDs' : 'Weird IDs 9';
+        $weirdIDs = $this->findAndAssertLink(
+            $page->findById('modal'), $expectedLinkText
+        );
+        $this->assertEquals($expectedLinkText, $weirdIDs->getText());
+        // apply US facet
+        $weirdIDs->click();
+        $this->snooze();
+    }
+
+    /**
+     * Test expanding facets into the lightbox
+     *
+     * @return void
+     */
+    public function testFacetLightbox()
+    {
+        $limit = 4;
+        $this->changeConfigs(
+            [
+                'facets' => [
+                    'Results_Settings' => [
+                        'showMoreInLightbox[*]' => true,
+                        'lightboxLimit' => $limit
+                    ]
+                ]
+            ]
+        );
+        $page = $this->performSearch('building:weird_ids.mrc');
+        // Open the geographic facet
+        $genreMore = $this->findCss($page, '#more-narrowGroupHidden-genre_facet');
+        $genreMore->click();
+        $this->facetListProcedure($page, $limit);
+        $genreMore->click();
+        $this->findCss($page, '#modal .js-facet-item.active')->click();
+        // remove facet
+        $this->snooze();
+        $this->assertNull($page->find('css', '.list-group.filters'));
+    }
+
+    /**
+     * Test expanding facets into the lightbox
+     *
+     * @return void
+     */
+    public function testFacetLightboxMoreSetting()
+    {
+        $limit = 4;
+        $this->changeConfigs(
+            [
+                'facets' => [
+                    'Results_Settings' => [
+                        'showMoreInLightbox[*]' => 'more',
+                        'lightboxLimit' => $limit
+                    ]
+                ]
+            ]
+        );
+        $page = $this->performSearch('building:weird_ids.mrc');
+        // Open the geographic facet
+        $genreMore = $this->findCss($page, '#more-narrowGroupHidden-genre_facet');
+        $genreMore->click();
+        $this->findCss($page, '.narrowGroupHidden-genre_facet[data-lightbox]')->click();
+        $this->facetListProcedure($page, $limit);
+        $genreMore->click();
+        $this->findCss($page, '.narrowGroupHidden-genre_facet[data-lightbox]')->click();
+        $this->findCss($page, '#modal .js-facet-item.active')->click();
+        // remove facet
+        $this->snooze();
+        $this->assertNull($page->find('css', '.list-group.filters'));
+    }
+
+    /**
+     * Test that exclusion works properly deep in lightbox results.
+     *
+     * @return void
+     */
+    public function testFacetLightboxExclusion()
+    {
+        $limit = 4;
+        $this->changeConfigs(
+            [
+                'facets' => [
+                    'Results_Settings' => [
+                        'showMoreInLightbox[*]' => true,
+                        'lightboxLimit' => $limit,
+                        'exclude' => '*',
+                    ]
+                ]
+            ]
+        );
+        $page = $this->performSearch('building:weird_ids.mrc');
+        // Open the geographic facet
+        $genreMore = $this->findCss($page, '#more-narrowGroupHidden-genre_facet');
+        $genreMore->click();
+        $this->facetListProcedure($page, $limit, true);
+        $this->assertEquals(1, count($page->find('css', '.list-group.filters')));
+    }
+
+    /**
+     * Support method to click a hierarchical facet.
+     *
+     * @param \Behat\Mink\Element\Element $page Mink page object
+     *
+     * @return void
+     */
+    protected function clickHierarchyFacet($page)
+    {
+        $this->findCss($page, '#j1_1.jstree-closed .jstree-icon');
+        $session = $this->getMinkSession();
+        $session->executeScript("$('#j1_1.jstree-closed .jstree-icon').click();");
+        $this->findCss($page, '#j1_1.jstree-open .jstree-icon');
+        $this->findCss($page, '#j1_2 a')->click();
+        $this->snooze();
+        $filter = $this->findCss($page, '.filters .list-group-item.active');
+        $this->assertEquals('hierarchy: 1/level1a/level2a/', $filter->getText());
+        $this->findCss($page, '#j1_2 .fa-check');
+    }
+
+    /**
+     * Test that hierarchy facets work properly.
+     *
+     * @return void
+     */
+    public function testHierarchicalFacets()
+    {
+        $this->changeConfigs(
+            [
+                'facets' => [
+                    'Results' => [
+                        'hierarchical_facet_str_mv' => 'hierarchy'
+                    ],
+                    'SpecialFacets' => [
+                        'hierarchical[]' => 'hierarchical_facet_str_mv'
+                    ]
+                ]
+            ]
+        );
+        $page = $this->performSearch('building:"hierarchy.mrc"');
+        $this->clickHierarchyFacet($page);
+    }
+
+    /**
+     * Test that we can persist uncollapsed state of collapsed facets
+     *
+     * @return void
+     */
+    public function testCollapseStatePersistence()
+    {
+        $this->changeConfigs(
+            [
+                'facets' => [
+                    'Results' => [
+                        'hierarchical_facet_str_mv' => 'hierarchy'
+                    ],
+                    'Results_Settings' => [
+                        'collapsedFacets' => '*'
+                    ],
+                    'SpecialFacets' => [
+                        'hierarchical[]' => 'hierarchical_facet_str_mv'
+                    ]
+                ]
+            ]
+        );
+        $page = $this->performSearch('building:"hierarchy.mrc"');
+        // Uncollapse format so we can check if it is still open after reload:
+        $this->findCss($page, '#side-panel-format .collapsed')->click();
+        // Uncollapse hierarchical facet so we can click it:
+        $this->findCss($page, '#side-panel-hierarchical_facet_str_mv .collapsed')->click();
+        $this->clickHierarchyFacet($page);
+
+        // We have now reloaded the page. Let's toggle format off and on to confirm
+        // that it was opened, and let's also toggle building on to confirm that
+        // it was not alread opened.
+        $this->findCss($page, '#side-panel-format .title')->click(); // off
+        $this->snooze(); // wait for animation
+        $this->findCss($page, '#side-panel-format .collapsed')->click(); // on
+        $this->findCss($page, '#side-panel-building .collapsed')->click(); // on
+    }
+
     /**
      * Standard teardown method.
      *
diff --git a/module/VuFind/tests/integration-tests/src/VuFindTest/View/Helper/Root/ResultFeedTest.php b/module/VuFind/tests/integration-tests/src/VuFindTest/View/Helper/Root/ResultFeedTest.php
index 53c20cb501e1639d47db537c99397ca019fc7752..f8b754ead542a9c915e015a9a127f9ba0ccce492 100644
--- a/module/VuFind/tests/integration-tests/src/VuFindTest/View/Helper/Root/ResultFeedTest.php
+++ b/module/VuFind/tests/integration-tests/src/VuFindTest/View/Helper/Root/ResultFeedTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/phpunit.xml b/module/VuFind/tests/phpunit.xml
index 7fa7d4964ec5c276799f9e2acbcb6ca2fbe893af..4bd62bcdb5cc1d5fcda988ae1e7ff9e5b84d7ba6 100644
--- a/module/VuFind/tests/phpunit.xml
+++ b/module/VuFind/tests/phpunit.xml
@@ -11,7 +11,6 @@
   <filter>
     <whitelist addUncoveredFilesFromWhitelist="true">
       <directory suffix=".php">../src/VuFind</directory>
-      <directory suffix=".php">../../VuDL/src</directory>
       <directory suffix=".php">../../VuFindAdmin/src</directory>
       <directory suffix=".php">../../VuFindConsole/src</directory>
       <directory suffix=".php">../../VuFindDevTools/src</directory>
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Auth/ChoiceAuthTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Auth/ChoiceAuthTest.php
index dc1a3aa942168df2c3f744855f1873cc58637b6a..99f78d8e24ef7cfa900f27d215960f2f226a78ad 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/Auth/ChoiceAuthTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Auth/ChoiceAuthTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Auth/DatabaseUnitTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Auth/DatabaseUnitTest.php
index d19b2b3f2715c8634b2f5c2cd7f73d855a198120..8f85a97d747d9f271f8d26164f3a087a1aae65c8 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/Auth/DatabaseUnitTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Auth/DatabaseUnitTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Auth/ILSAuthenticatorTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Auth/ILSAuthenticatorTest.php
index 485686a693be9d7845580f74a0d8a07a4d173651..c5202ab848bc644938fba372fc42d171972601ab 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/Auth/ILSAuthenticatorTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Auth/ILSAuthenticatorTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Auth/LDAPTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Auth/LDAPTest.php
index c154a3020f666151b4b2b190e4d818583a4f9799..748a2e79a237b200deb6ceff81110b99871a1425 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/Auth/LDAPTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Auth/LDAPTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Auth/ManagerTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Auth/ManagerTest.php
index e4cac9004281e2d5534b977b4108834de9174a0a..abbc3ba7a015935abe0ec9a58a95aa3be6f2f28a 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/Auth/ManagerTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Auth/ManagerTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Auth/MultiAuthTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Auth/MultiAuthTest.php
index f94a66077c341ee854394047049607f7c16119a0..bd31556b6b58e318a3cf1f7dcb05fbfdf08ab07c 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/Auth/MultiAuthTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Auth/MultiAuthTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Auth/PluginManagerTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Auth/PluginManagerTest.php
index 36debc8720ae388a80eece80d399ab693f5559a7..6bc5439a7355fc0b2da9e1a3497a09db57f4ffcd 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/Auth/PluginManagerTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Auth/PluginManagerTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Auth/SIP2Test.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Auth/SIP2Test.php
index 04773d6e473069af4775271232bb7a6aa675abe8..374a634d2f462bf4b480a33cd4e2207d7228510a 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/Auth/SIP2Test.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Auth/SIP2Test.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Autocomplete/PluginManagerTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Autocomplete/PluginManagerTest.php
index 9d959ee1db6acf712d0ecc07e3b17db15ad23956..b7f329a1cd1ecf0ce6299e556859936b844eea7f 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/Autocomplete/PluginManagerTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Autocomplete/PluginManagerTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Autocomplete/TagTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Autocomplete/TagTest.php
index a7775f2a0b138d705497946101302ade00a3d933..17cd49b6f5ea05456879789038c5d4977944c408 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/Autocomplete/TagTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Autocomplete/TagTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Cache/Storage/Adapter/NoCacheAdapterTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Cache/Storage/Adapter/NoCacheAdapterTest.php
index 07c6a284206d6192075b0e52ec30c805e2fc0599..8d516fc7749aa6bf1bfb0f501a274f0ab1bba297 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/Cache/Storage/Adapter/NoCacheAdapterTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Cache/Storage/Adapter/NoCacheAdapterTest.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Cache
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/CartTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/CartTest.php
index eb82a1656678e1e3111d22927b74248b7fb0252f..87473d3225077560621b9bab335f77c979ca9cdb 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/CartTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/CartTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Config/PluginFactoryTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Config/PluginFactoryTest.php
index a68618fdd94afee035f1e5f88bb0061566b54686..1e1d4b742f16d0bab7df88084252edc8bc12a781 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/Config/PluginFactoryTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Config/PluginFactoryTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Config/Reader/CacheDecoratorTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Config/Reader/CacheDecoratorTest.php
index 4f5f95e4ef2f9e49e73b39bc1f30f09595dd00b7..6f881748799f2f269b8b6b1ef61d6ec856f5e11b 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/Config/Reader/CacheDecoratorTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Config/Reader/CacheDecoratorTest.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Config/SearchSpecsReaderTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Config/SearchSpecsReaderTest.php
index 8f4808ceb6e77d4e1029a502e939cddf1676c36e..52d8c0f294722b12b7c986f5f04618488bf2e542 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/Config/SearchSpecsReaderTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Config/SearchSpecsReaderTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
@@ -26,7 +26,7 @@
  * @link     https://vufind.org/wiki/development:testing:unit_tests Wiki
  */
 namespace VuFindTest\Config;
-use VuFind\Config\SearchSpecsReader;
+use VuFind\Config\Locator, VuFind\Config\SearchSpecsReader;
 
 /**
  * Config SearchSpecsReader Test Class
@@ -40,6 +40,49 @@ use VuFind\Config\SearchSpecsReader;
  */
 class SearchSpecsReaderTest extends \VuFindTest\Unit\TestCase
 {
+    /**
+     * Flag -- did writing config files fail?
+     *
+     * @var bool
+     */
+    protected static $writeFailed = false;
+
+    /**
+     * Array of files to clean up after test.
+     *
+     * @var array
+     */
+    protected static $filesToDelete = [];
+
+    /**
+     * Standard setup method.
+     *
+     * @return void
+     */
+    public static function setUpBeforeClass()
+    {
+        // Create test files:
+        $parentPath = Locator::getLocalConfigPath('top.yaml', null, true);
+        $parent = "top: foo";
+        $childPath = Locator::getLocalConfigPath('middle.yaml', null, true);
+        $child = "@parent_yaml: $parentPath\nmiddle: bar";
+        $grandchildPath = Locator::getLocalConfigPath('bottom.yaml', null, true);
+        $grandchild = "@parent_yaml: $childPath\nbottom: baz";
+
+        // Fail if we are unable to write files:
+        if (null === $parentPath || null === $childPath || null === $grandchildPath
+            || !file_put_contents($parentPath, $parent)
+            || !file_put_contents($childPath, $child)
+            || !file_put_contents($grandchildPath, $grandchild)
+        ) {
+            self::$writeFailed = true;
+            return;
+        }
+
+        // Mark for cleanup:
+        self::$filesToDelete = [$parentPath, $childPath, $grandchildPath];
+    }
+
     /**
      * Test loading of a YAML file.
      *
@@ -68,4 +111,84 @@ class SearchSpecsReaderTest extends \VuFindTest\Unit\TestCase
         $specs = $reader->get('notreallyasearchspecs.yaml');
         $this->assertEquals([], $specs);
     }
+
+    /**
+     * Test direct loading of two single files.
+     *
+     * @return void
+     */
+    public function testYamlLoad()
+    {
+        $reader = new SearchSpecsReader();
+        $core = __DIR__ . '/../../../../fixtures/configs/yaml/core.yaml';
+        $local = __DIR__ . '/../../../../fixtures/configs/yaml/local.yaml';
+        $this->assertEquals(
+            [
+                'top' => ['foo' => 'bar'],
+                'bottom' => ['goo' => 'gar'],
+            ],
+            $this->callMethod($reader, 'getFromPaths', [$core])
+        );
+        $this->assertEquals(
+            [
+                'top' => ['foo' => 'xyzzy'],
+                'middle' => ['moo' => 'cow'],
+            ],
+            $this->callMethod($reader, 'getFromPaths', [$local])
+        );
+    }
+
+    /**
+     * Test merging of two files.
+     *
+     * @return void
+     */
+    public function testYamlMerge()
+    {
+        $reader = new SearchSpecsReader();
+        $core = __DIR__ . '/../../../../fixtures/configs/yaml/core.yaml';
+        $local = __DIR__ . '/../../../../fixtures/configs/yaml/local.yaml';
+        $this->assertEquals(
+            [
+                'top' => ['foo' => 'xyzzy'],
+                'middle' => ['moo' => 'cow'],
+                'bottom' => ['goo' => 'gar'],
+            ],
+            $this->callMethod($reader, 'getFromPaths', [$core, $local])
+        );
+    }
+
+    /**
+     * Test @parent_yaml directive.
+     *
+     * @return void
+     */
+    public function testParentYaml()
+    {
+        if (self::$writeFailed) {
+            $this->markTestSkipped('Could not write test configurations.');
+        }
+        $reader = new SearchSpecsReader();
+        $core = Locator::getLocalConfigPath('middle.yaml', null, true);
+        $local = Locator::getLocalConfigPath('bottom.yaml', null, true);
+        $this->assertEquals(
+            [
+                'top' => 'foo',
+                'middle' => 'bar',
+                'bottom' => 'baz',
+            ],
+            $this->callMethod($reader, 'getFromPaths', [$core, $local])
+        );
+    }
+
+    /**
+     * Standard teardown method.
+     *
+     * @return void
+     */
+    public static function tearDownAfterClass()
+    {
+        // Clean up test files:
+        array_map('unlink', self::$filesToDelete);
+    }
 }
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Config/UpgradeTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Config/UpgradeTest.php
index 84fb5f2a6945eb4c3159d469a343f0d880c0ab94..27f77e9333c54630661bc819f77c9ab4aa24bf25 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/Config/UpgradeTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Config/UpgradeTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Config/VersionTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Config/VersionTest.php
index 8ede10c81779b7c9e710ded1c1c742a1dbe88fde..755825ab06d7ebaa5cd7ad92648ee73082b27306 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/Config/VersionTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Config/VersionTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Config/WriterTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Config/WriterTest.php
index 3e2bcc3155de8408ecefaf64c4baa9e70d114746..04849e68abb05c32680e97714c45386f5c0048ec 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/Config/WriterTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Config/WriterTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Connection/WikipediaTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Connection/WikipediaTest.php
index 067712cc533577aed148926f3582d15623e1e9a9..b78bdfc8f5da564ddb450bd24d388828ad5d532b 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/Connection/WikipediaTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Connection/WikipediaTest.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Connection/WorldCatUtilsTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Connection/WorldCatUtilsTest.php
index 9efe930cf1346b2e9d70eb865a398aeeb059772e..a5b13207d37668ee9ef2bab3b2db61ba42bc1ec1 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/Connection/WorldCatUtilsTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Connection/WorldCatUtilsTest.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Content/AuthorNotes/PluginManagerTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Content/AuthorNotes/PluginManagerTest.php
index f38ada4b6a28845b5f59d5016ce7447caa275855..9b2c7a52ec97c7b54bd7cda463b9d335ef692537 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/Content/AuthorNotes/PluginManagerTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Content/AuthorNotes/PluginManagerTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Content/Covers/AmazonTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Content/Covers/AmazonTest.php
index 58cfbaef9e5935f29d532a7ae47250b0523827ee..a65e99dcd44db2e2ef7fa68fd1a9036234a1d255 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/Content/Covers/AmazonTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Content/Covers/AmazonTest.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Content/Covers/BooksiteTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Content/Covers/BooksiteTest.php
index 5faa1db76d3e535c93560a84c55c3b735a860422..80a80a5853230bffd5c7a14c2f15225419342bac 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/Content/Covers/BooksiteTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Content/Covers/BooksiteTest.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Content/Covers/BuchhandelTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Content/Covers/BuchhandelTest.php
index 0823cb99d0f3dc1f3e209085e18912b7d7c9f0bd..cc0d1892a2e3dd7dd9788a3aa7d458dddef1443e 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/Content/Covers/BuchhandelTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Content/Covers/BuchhandelTest.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Content/Covers/ContentCafeTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Content/Covers/ContentCafeTest.php
index fa1087411420b3a888a246bd2f4e93d36de163e7..8a2c3b23d646cdd3231f1a47b80412cb70747b20 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/Content/Covers/ContentCafeTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Content/Covers/ContentCafeTest.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Content/Covers/LibraryThingTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Content/Covers/LibraryThingTest.php
index 1c5c2aba88a983986997f58dc0c500e30659af48..99c626ce9e5c8c64f4f6cb49647d7c8fad4e4017 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/Content/Covers/LibraryThingTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Content/Covers/LibraryThingTest.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Content/Covers/OpenLibraryTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Content/Covers/OpenLibraryTest.php
index 31e88ef12ff5b71345858a208b6a6aedf8787a5b..4de1be7fcd3ba36b0bf963458b342fc9b79fbb51 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/Content/Covers/OpenLibraryTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Content/Covers/OpenLibraryTest.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Content/Covers/PluginManagerTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Content/Covers/PluginManagerTest.php
index 0d2f9d8ca8d66289669f13ef858714126c6ef0c1..88ded6380e81d51f36746fc89f5fefa096bc5abf 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/Content/Covers/PluginManagerTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Content/Covers/PluginManagerTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Content/Covers/SummonTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Content/Covers/SummonTest.php
index 63e280ddbe2b936f908fe2398221a2b5575caa4b..f25c145d81e155cefd716b389a7a3479749029f4 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/Content/Covers/SummonTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Content/Covers/SummonTest.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Content/Excerpts/PluginManagerTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Content/Excerpts/PluginManagerTest.php
index 35b6e91d1f13dc2e162a0437ea43120f5f4b2482..4870d565fd1ff58fc2a2737bff872902195251a2 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/Content/Excerpts/PluginManagerTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Content/Excerpts/PluginManagerTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Content/PluginManagerTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Content/PluginManagerTest.php
index a983ed900cfcf126d5f93033287d01bd7dbe548c..c4654d323ae14ee7786ce3fe2ef51b241a21f52c 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/Content/PluginManagerTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Content/PluginManagerTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Content/Reviews/PluginManagerTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Content/Reviews/PluginManagerTest.php
index 534170d4352875fc01c5d436df7725a2d32d29e0..f80c4d04e80e68561547c6f3ed0298018a3e95e6 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/Content/Reviews/PluginManagerTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Content/Reviews/PluginManagerTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Controller/Plugin/FollowupTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Controller/Plugin/FollowupTest.php
index 41302f944683a8f15337a74ed735242f99beb5a4..d10b2f9a934e6d748d1721e1874b486bc1ddd1ed 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/Controller/Plugin/FollowupTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Controller/Plugin/FollowupTest.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Controller/Plugin/NewItemsTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Controller/Plugin/NewItemsTest.php
index 5c9d6a1d9a965ef69576b4c7a0adba17f698e7f4..13c46d81d4dd92edc9ae43c0531af7256d7895bb 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/Controller/Plugin/NewItemsTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Controller/Plugin/NewItemsTest.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Controller/Plugin/ResultScrollerTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Controller/Plugin/ResultScrollerTest.php
index 0bf9127024d3d95cb17733e2a74e59345985d3c6..2a7c3953639e0737c271388efc31ef4d29c258e5 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/Controller/Plugin/ResultScrollerTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Controller/Plugin/ResultScrollerTest.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
@@ -54,12 +54,31 @@ class ResultScrollerTest extends TestCase
         $results = $this->getMockResults();
         $this->assertFalse($plugin->init($results));
         $expected = [
+            'firstRecord' => null, 'lastRecord' => null,
             'previousRecord' => null, 'nextRecord' => null,
             'currentPosition' => null, 'resultTotal' => null
         ];
         $this->assertEquals($expected, $plugin->getScrollData($results->getMockRecordDriver(1)));
     }
 
+    /**
+     * Test scrolling on single-record set
+     *
+     * @return void
+     */
+    public function testScrollingOnSingleRecord()
+    {
+        $results = $this->getMockResults(1, 10, 1);
+        $plugin = $this->getMockResultScroller($results);
+        $this->assertTrue($plugin->init($results));
+        $expected = [
+            'firstRecord' => 'Solr|1', 'lastRecord' => 'Solr|1',
+            'previousRecord' => null, 'nextRecord' => null,
+            'currentPosition' => 1, 'resultTotal' => 1
+        ];
+        $this->assertEquals($expected, $plugin->getScrollData($results->getMockRecordDriver(1)));
+    }
+
     /**
      * Test scrolling for a record in the middle of the page
      *
@@ -71,6 +90,100 @@ class ResultScrollerTest extends TestCase
         $plugin = $this->getMockResultScroller($results);
         $this->assertTrue($plugin->init($results));
         $expected = [
+            'firstRecord' => 'Solr|1', 'lastRecord' => 'Solr|10',
+            'previousRecord' => 'Solr|4', 'nextRecord' => 'Solr|6',
+            'currentPosition' => 5, 'resultTotal' => 10
+        ];
+        $this->assertEquals($expected, $plugin->getScrollData($results->getMockRecordDriver(5)));
+    }
+
+    /**
+     * Test scrolling to the first record in a set.
+     *
+     * @return void
+     */
+    public function testScrollingToFirstRecord()
+    {
+        $results = $this->getMockResults(5, 2, 10);
+        $plugin = $this->getMockResultScroller($results);
+        $this->assertTrue($plugin->init($results));
+        $expected = [
+            'firstRecord' => 'Solr|1', 'lastRecord' => 'Solr|10',
+            'previousRecord' => null, 'nextRecord' => 'Solr|2',
+            'currentPosition' => 1, 'resultTotal' => 10
+        ];
+        $this->assertEquals($expected, $plugin->getScrollData($results->getMockRecordDriver(1)));
+    }
+
+    /**
+     * Test scrolling to the first record in a set (with page size set to 1).
+     *
+     * @return void
+     */
+    public function testScrollingToFirstRecordWithPageSize1()
+    {
+        $results = $this->getMockResults(10, 1, 10);
+        $plugin = $this->getMockResultScroller($results);
+        $this->assertTrue($plugin->init($results));
+        $expected = [
+            'firstRecord' => 'Solr|1', 'lastRecord' => 'Solr|10',
+            'previousRecord' => null, 'nextRecord' => 'Solr|2',
+            'currentPosition' => 1, 'resultTotal' => 10
+        ];
+        $this->assertEquals($expected, $plugin->getScrollData($results->getMockRecordDriver(1)));
+    }
+
+    /**
+     * Test scrolling to the last record in a set (with multiple records on the
+     * last page of results).
+     *
+     * @return void
+     */
+    public function testScrollingToLastRecord()
+    {
+        $results = $this->getMockResults(1, 2, 10);
+        $plugin = $this->getMockResultScroller($results);
+        $this->assertTrue($plugin->init($results));
+        $expected = [
+            'firstRecord' => 'Solr|1', 'lastRecord' => 'Solr|10',
+            'previousRecord' => 'Solr|9', 'nextRecord' => null,
+            'currentPosition' => 10, 'resultTotal' => 10
+        ];
+        $this->assertEquals($expected, $plugin->getScrollData($results->getMockRecordDriver(10)));
+    }
+
+    /**
+     * Test scrolling to the last record in a set (with only one record on the
+     * last page of results).
+     *
+     * @return void
+     */
+    public function testScrollingToLastRecordAcrossPageBoundaries()
+    {
+        $results = $this->getMockResults(1, 2, 9);
+        $plugin = $this->getMockResultScroller($results);
+        $this->assertTrue($plugin->init($results));
+        $expected = [
+            'firstRecord' => 'Solr|1', 'lastRecord' => 'Solr|9',
+            'previousRecord' => 'Solr|8', 'nextRecord' => null,
+            'currentPosition' => 9, 'resultTotal' => 9
+        ];
+        $this->assertEquals($expected, $plugin->getScrollData($results->getMockRecordDriver(9)));
+    }
+
+    /**
+     * Test that first/last results can be disabled (this is the same as the
+     * testScrollingInMiddleOfPage() test, but with first/last setting off).
+     *
+     * @return void
+     */
+    public function testDisabledFirstLast()
+    {
+        $results = $this->getMockResults(1, 10, 10, false);
+        $plugin = $this->getMockResultScroller($results);
+        $this->assertTrue($plugin->init($results));
+        $expected = [
+            'firstRecord' => null, 'lastRecord' => null,
             'previousRecord' => 'Solr|4', 'nextRecord' => 'Solr|6',
             'currentPosition' => 5, 'resultTotal' => 10
         ];
@@ -88,6 +201,7 @@ class ResultScrollerTest extends TestCase
         $plugin = $this->getMockResultScroller($results);
         $this->assertTrue($plugin->init($results));
         $expected = [
+            'firstRecord' => 'Solr|1', 'lastRecord' => 'Solr|10',
             'previousRecord' => null, 'nextRecord' => 'Solr|2',
             'currentPosition' => 1, 'resultTotal' => 10
         ];
@@ -105,6 +219,7 @@ class ResultScrollerTest extends TestCase
         $plugin = $this->getMockResultScroller($results);
         $this->assertTrue($plugin->init($results));
         $expected = [
+            'firstRecord' => 'Solr|1', 'lastRecord' => 'Solr|10',
             'previousRecord' => 'Solr|9', 'nextRecord' => null,
             'currentPosition' => 10, 'resultTotal' => 10
         ];
@@ -122,6 +237,7 @@ class ResultScrollerTest extends TestCase
         $plugin = $this->getMockResultScroller($results);
         $this->assertTrue($plugin->init($results));
         $expected = [
+            'firstRecord' => 'Solr|1', 'lastRecord' => 'Solr|17',
             'previousRecord' => 'Solr|16', 'nextRecord' => null,
             'currentPosition' => 17, 'resultTotal' => 17
         ];
@@ -139,12 +255,23 @@ class ResultScrollerTest extends TestCase
         $plugin = $this->getMockResultScroller($results);
         $this->assertTrue($plugin->init($results));
         $expected = [
+            'firstRecord' => 'Solr|1', 'lastRecord' => 'Solr|30',
             'previousRecord' => 'Solr|10', 'nextRecord' => 'Solr|12',
             'currentPosition' => 11, 'resultTotal' => 30
         ];
         $this->assertEquals($expected, $plugin->getScrollData($results->getMockRecordDriver(11)));
     }
 
+    /**
+     * Get a configuration array to turn on first/last setting.
+     *
+     * @return array
+     */
+    protected function getFirstLastConfig()
+    {
+        return ['Record' => ['first_last_navigation' => true]];
+    }
+
     /**
      * Test scrolling at end of middle page.
      *
@@ -156,28 +283,59 @@ class ResultScrollerTest extends TestCase
         $plugin = $this->getMockResultScroller($results);
         $this->assertTrue($plugin->init($results));
         $expected = [
+            'firstRecord' => 'Solr|1', 'lastRecord' => 'Solr|30',
             'previousRecord' => 'Solr|19', 'nextRecord' => 'Solr|21',
             'currentPosition' => 20, 'resultTotal' => 30
         ];
         $this->assertEquals($expected, $plugin->getScrollData($results->getMockRecordDriver(20)));
     }
 
+    /**
+     * Test scrolling at end of middle page with sorting.
+     *
+     * @return void
+     */
+    public function testScrollingAtEndOfMiddlePageWithSorting()
+    {
+        $results = $this->getMockResults(2, 10, 30, true, 'sorted');
+        $plugin = $this->getMockResultScroller($results);
+        $this->assertTrue($plugin->init($results));
+        $expected = [
+            'firstRecord' => 'Solr|sorted1', 'lastRecord' => 'Solr|sorted30',
+            'previousRecord' => 'Solr|sorted19', 'nextRecord' => 'Solr|sorted21',
+            'currentPosition' => 20, 'resultTotal' => 30
+        ];
+        $this->assertEquals($expected, $plugin->getScrollData(
+            $results->getMockRecordDriver('sorted20'))
+        );
+    }
+
     /**
      * Get mock search results
      *
-     * @param int $page  Current page number
-     * @param int $limit Page size
-     * @param int $total Total size of fake result set
+     * @param int    $page      Current page number
+     * @param int    $limit     Page size
+     * @param int    $total     Total size of fake result set
+     * @param bool   $firstLast Turn on first/last config?
+     * @param string $sort      Sort type (null for default)
      *
      * @return \VuFind\Search\Base\Results
      */
-    protected function getMockResults($page = 1, $limit = 20, $total = 0)
-    {
+    protected function getMockResults($page = 1, $limit = 20, $total = 0,
+        $firstLast = true, $sort = null
+    ) {
         $pm = $this->getMockBuilder('VuFind\Config\PluginManager')->disableOriginalConstructor()->getMock();
+        $config = new \Zend\Config\Config(
+            $firstLast ? $this->getFirstLastConfig() : []
+        );
+        $pm->expects($this->any())->method('get')->will($this->returnValue($config));
         $options = new \VuFindTest\Search\TestHarness\Options($pm);
         $params = new \VuFindTest\Search\TestHarness\Params($options, $pm);
         $params->setPage($page);
         $params->setLimit($limit);
+        if (null !== $sort) {
+            $params->setSort($sort, true);
+        }
         $results = new \VuFindTest\Search\TestHarness\Results($params, $total);
         return $results;
     }
@@ -185,13 +343,15 @@ class ResultScrollerTest extends TestCase
     /**
      * Get mock result scroller
      *
-     * @param \VuFind\Search\Base\Results restoreLastSearch results (null to ignore)
-     * @param array                                                                  $methods Methods to mock
+     * @param \VuFind\Search\Base\Results $results restoreLastSearch results
+     * (null to ignore)
+     * @param array                       $methods Methods to mock
      *
      * @return ResultScroller
      */
-    protected function getMockResultScroller($results = null, $methods = ['restoreLastSearch', 'rememberSearch'])
-    {
+    protected function getMockResultScroller($results = null,
+        $methods = ['restoreLastSearch', 'rememberSearch']
+    ) {
         $mock = $this->getMock(
             'VuFind\Controller\Plugin\ResultScroller', $methods, [new Container('test')]
         );
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Cookie/ContainerTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Cookie/ContainerTest.php
index 05de17b599cdf9f03c1b1d267d26f8cfb4ad152a..519fbe65feab0742f7225b4d2316478951e0a2d3 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/Cookie/ContainerTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Cookie/ContainerTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Cover/LoaderTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Cover/LoaderTest.php
index 8ee9f29387978e777c80d49ae489a17880f5a8c3..0eb780295573b42b33531bfbf1b962044a557a8b 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/Cover/LoaderTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Cover/LoaderTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Cover/RouterTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Cover/RouterTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..e2d97760fcf6dbbeed8b33a4f8a0fbce96c44744
--- /dev/null
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Cover/RouterTest.php
@@ -0,0 +1,104 @@
+<?php
+/**
+ * Cover Router Test Class
+ *
+ * PHP version 5
+ *
+ * Copyright (C) Villanova University 2016.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ * @category VuFind
+ * @package  Tests
+ * @author   Demian Katz <demian.katz@villanova.edu>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     https://vufind.org/wiki/development:testing:unit_tests Wiki
+ */
+namespace VuFindTest\Cover;
+use VuFind\Cover\Router, VuFindTest\RecordDriver\TestHarness;
+
+/**
+ * Cover Router Test Class
+ *
+ * @category VuFind
+ * @package  Tests
+ * @author   Demian Katz <demian.katz@villanova.edu>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     https://vufind.org/wiki/development:testing:unit_tests Wiki
+ */
+class RouterTest extends \VuFindTest\Unit\TestCase
+{
+    /**
+     * Get a fake record driver
+     *
+     * @param array $data Test data
+     *
+     * @return TestHarness
+     */
+    protected function getDriver($data)
+    {
+        $driver = new TestHarness();
+        $driver->setRawData($data);
+        return $driver;
+    }
+
+    /**
+     * Get a router to test
+     *
+     * @return Router
+     */
+    protected function getRouter()
+    {
+        return new Router('https://vufind.org/cover');
+    }
+
+    /**
+     * Test a record driver with no thumbnail data.
+     *
+     * @return void
+     */
+    public function testUnsupportedThumbnail()
+    {
+        $this->assertFalse(
+            $this->getRouter()->getUrl($this->getDriver([]))
+        );
+    }
+
+    /**
+     * Test a record driver with static thumbnail data.
+     *
+     * @return void
+     */
+    public function testStaticUrl()
+    {
+        $url = 'http://foo/bar';
+        $this->assertEquals(
+            $url, $this->getRouter()->getUrl($this->getDriver(['Thumbnail' => $url]))
+        );
+    }
+
+    /**
+     * Test a record driver with dynamic thumbnail data.
+     *
+     * @return void
+     */
+    public function testDynamicUrl()
+    {
+        $params = ['foo' => 'bar'];
+        $this->assertEquals(
+            'https://vufind.org/cover?foo=bar',
+            $this->getRouter()->getUrl($this->getDriver(['Thumbnail' => $params]))
+        );
+    }
+}
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Crypt/HMACTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Crypt/HMACTest.php
index ea7687fd1b7b2ef4bf9bb93677c3e71014d99521..6bfc5f08c3a11b6ecac0417523ff142e40b853ce 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/Crypt/HMACTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Crypt/HMACTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Crypt/RC4Test.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Crypt/RC4Test.php
index 3226f41295680fe65209bcc3e2ff42d01cddd87d..8b9d06266c5803b4a49a872c1f33df5076ad9757 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/Crypt/RC4Test.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Crypt/RC4Test.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Date/ConverterTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Date/ConverterTest.php
index b65a5db880b4ab5d71c09a77a62ec10ca92a20ac..65849a77803e52707388a69e7cb03deafd7e0a4a 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/Date/ConverterTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Date/ConverterTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Db/Table/PluginManagerTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Db/Table/PluginManagerTest.php
index 591b5e38aa743a978d7dc43af3f7bb60d1e67ce9..d67afbfe1206c1432170b3b45329682ae97c2fc4 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/Db/Table/PluginManagerTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Db/Table/PluginManagerTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Db/Table/UserListTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Db/Table/UserListTest.php
index 1eb65cc9dd2d3f3ccc22c8b350f64e4551482d39..2ad8d58afb39f702b93294fb964dae51546145d9 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/Db/Table/UserListTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Db/Table/UserListTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/ExportTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/ExportTest.php
index 70e1b63a71d1a0b70c69522e552f1c2fa789127c..856753b19aeb805a8be5064a161a77e498c6522b 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/ExportTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/ExportTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Harvester/OAITest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Harvester/OAITest.php
deleted file mode 100644
index cd4e3445550bd9b88b8ebbf6b3e6ab9770bcdf7b..0000000000000000000000000000000000000000
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/Harvester/OAITest.php
+++ /dev/null
@@ -1,303 +0,0 @@
-<?php
-
-/**
- * OAI-PMH harvester unit test.
- *
- * PHP version 5
- *
- * Copyright (C) Villanova University 2010.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License version 2,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
- *
- * @category Search
- * @package  Service
- * @author   David Maus <maus@hab.de>
- * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
- * @link     https://vufind.org/wiki/development
- */
-namespace VuFindTest\Harvester;
-
-use VuFind\Harvester\OAI;
-
-/**
- * OAI-PMH harvester unit test.
- *
- * PHP version 5
- *
- * Copyright (C) Villanova University 2010.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License version 2,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
- *
- * @category Search
- * @package  Service
- * @author   David Maus <maus@hab.de>
- * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
- * @link     https://vufind.org/wiki/development
- */
-class OAITest extends \VuFindTest\Unit\TestCase
-{
-    /**
-     * Test configuration.
-     *
-     * @return void
-     */
-    public function testConfig()
-    {
-        $config = [
-            'url' => 'http://localhost',
-            'set' => 'myset',
-            'metadataPrefix' => 'fakemdprefix',
-            'idPrefix' => 'fakeidprefix',
-            'idSearch' => 'search',
-            'idReplace' => 'replace',
-            'harvestedIdLog' => '/my/harvest.log',
-            'injectId' => 'idtag',
-            'injectSetSpec' => 'setspectag',
-            'injectDate' => 'datetag',
-            'injectHeaderElements' => 'headertag',
-            'dateGranularity' => 'mygranularity',
-            'verbose' => true,
-            'sanitize' => true,
-            'badXMLLog' => '/my/xml.log',
-        ];
-        $oai = new OAI('test', $config, $this->getMockClient());
-
-        // Special cases where config key != class property:
-        $this->assertEquals(
-            $config['url'], $this->getProperty($oai, 'baseURL')
-        );
-        $this->assertEquals(
-            $config['dateGranularity'], $this->getProperty($oai, 'granularity')
-        );
-
-        // Special case where value is transformed:
-        $this->assertEquals(
-            [$config['injectHeaderElements']],
-            $this->getProperty($oai, 'injectHeaderElements')
-        );
-
-        // Unset special cases in preparation for generic loop below:
-        unset($config['url']);
-        unset($config['dateGranularity']);
-        unset($config['injectHeaderElements']);
-
-        // Generic case for remaining configs:
-        foreach ($config as $key => $value) {
-            $this->assertEquals($value, $this->getProperty($oai, $key));
-        }
-    }
-
-    /**
-     * Test the injectSetName configuration.
-     *
-     * @return void
-     */
-    public function testInjectSetNameConfig()
-    {
-        $client = $this->getMockClient();
-        $response = $client->send();
-        $response->expects($this->any())
-            ->method('isSuccess')
-            ->will($this->returnValue(true));
-        $response->expects($this->any())
-            ->method('getBody')
-            ->will($this->returnValue('<?xml version="1.0"?><OAI-PMH xmlns="http://www.openarchives.org/OAI/2.0/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.openarchives.org/OAI/2.0/ http://www.openarchives.org/OAI/2.0/OAI-PMH.xsd"><responseDate>2013-10-11T10:06:06Z</responseDate><request verb="ListSets" metadataPrefix="oai_dc" resumptionToken="" submit="Go">http://vu61162/vufind3/OAI/Server</request><ListSets><set><setSpec>Audio (Music)</setSpec><setName>Audio (Music)</setName></set><set><setSpec>Audio (Non-Music)</setSpec><setName>Audio (Non-Music)</setName></set></ListSets></OAI-PMH>'));
-        $config = [
-            'url' => 'http://localhost',
-            'injectSetName' => 'setnametag',
-            'verbose' => true,
-            'dateGranularity' => 'mygranularity',
-        ];
-        $oai = new OAI('test', $config, $client);
-        $this->assertEquals(
-            $config['injectSetName'], $this->getProperty($oai, 'injectSetName')
-        );
-        $this->assertEquals(
-            [
-                'Audio (Music)' => 'Audio (Music)',
-                'Audio (Non-Music)' => 'Audio (Non-Music)'
-            ], $this->getProperty($oai, 'setNames')
-        );
-    }
-
-    /**
-     * Test the sslverifypeer configuration.
-     *
-     * @return void
-     */
-    public function testSSLVerifyPeer()
-    {
-        $client = $this->getMockClient();
-        $client->expects($this->once())
-            ->method('setOptions')
-            ->with($this->equalTo(['sslverifypeer' => false]));
-        $config = [
-            'url' => 'http://localhost',
-            'sslverifypeer' => false,
-            'dateGranularity' => 'mygranularity',
-        ];
-        $oai = new OAI('test', $config, $client);
-    }
-
-    /**
-     * Test date autodetection.
-     *
-     * @return void
-     */
-    public function testDateAutodetect()
-    {
-        $client = $this->getMockClient();
-        $response = $client->send();
-        $response->expects($this->any())
-            ->method('isSuccess')
-            ->will($this->returnValue(true));
-        $response->expects($this->any())
-            ->method('getBody')
-            ->will($this->returnValue($this->getIdentifyResponse()));
-        $config = [
-            'url' => 'http://localhost',
-            'verbose' => true,
-        ];
-        $oai = new OAI('test', $config, $client);
-        $this->assertEquals(
-            'YYYY-MM-DDThh:mm:ssZ', $this->getProperty($oai, 'granularity')
-        );
-    }
-
-    /**
-     * Test date autodetection w/503 retry.
-     *
-     * @return void
-     */
-    public function testDateAutodetectWith503Retry()
-    {
-        $client = $this->getMockClient();
-        $response = $client->send();
-        $response->expects($this->any())
-            ->method('isSuccess')
-            ->will($this->returnValue(true));
-        $response->expects($this->at(1))
-            ->method('getStatusCode')
-            ->will($this->returnValue(503));
-        $response->expects($this->any())
-            ->method('getBody')
-            ->will($this->returnValue($this->getIdentifyResponse()));
-        $header = $this->getMock('Zend\Http\Header\RetryAfter');
-        $header->expects($this->once())
-            ->method('getDeltaSeconds')
-            ->will($this->returnValue(1));
-        $headers = $response->getHeaders();
-        $headers->expects($this->any())
-            ->method('get')
-            ->with($this->equalTo('Retry-After'))
-            ->will($this->returnValue($header));
-        $config = [
-            'url' => 'http://localhost',
-            'verbose' => true,
-        ];
-        $oai = new OAI('test', $config, $client);
-        $this->assertEquals(
-            'YYYY-MM-DDThh:mm:ssZ', $this->getProperty($oai, 'granularity')
-        );
-    }
-
-    /**
-     * Test HTTP error detection.
-     *
-     * @return void
-     *
-     * @expectedException        Exception
-     * @expectedExceptionMessage HTTP Error
-     */
-    public function testHTTPErrorDetection()
-    {
-        $client = $this->getMockClient();
-        $response = $client->send();
-        $response->expects($this->any())
-            ->method('isSuccess')
-            ->will($this->returnValue(false));
-        $config = [
-            'url' => 'http://localhost',
-            'verbose' => true,
-        ];
-        $oai = new OAI('test', $config, $client);
-    }
-
-    /**
-    /**
-     * Test that a missing URL throws an exception.
-     *
-     * @return void
-     *
-     * @expectedException        Exception
-     * @expectedExceptionMessage Missing base URL for test.
-     */
-    public function testMissingURLThrowsException()
-    {
-        $oai = new OAI('test', [], $this->getMockClient());
-    }
-
-    // Internal API
-
-    /**
-     * Get a sample Identify response
-     *
-     * @return string
-     */
-    protected function getIdentifyResponse()
-    {
-        return '<?xml version="1.0"?><OAI-PMH xmlns="http://www.openarchives.org/OAI/2.0/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.openarchives.org/OAI/2.0/ http://www.openarchives.org/OAI/2.0/OAI-PMH.xsd"><responseDate>2013-10-11T11:12:04Z</responseDate><request verb="Identify" submit="Go">http://fake/my/OAI/Server</request><Identify><repositoryName>myuniversity University VuFind</repositoryName><baseURL>http://fake/my/OAI/Server</baseURL><protocolVersion>2.0</protocolVersion><earliestDatestamp>2000-01-01T00:00:00Z</earliestDatestamp><deletedRecord>transient</deletedRecord><granularity>YYYY-MM-DDThh:mm:ssZ</granularity><adminEmail>libtech@myuniversity.edu</adminEmail><description><oai-identifier xmlns="http://www.openarchives.org/OAI/2.0/oai-identifier" xsi:schemaLocation="http://www.openarchives.org/OAI/2.0/oai-identifier http://www.openarchives.org/OAI/2.0/oai-identifier.xsd"><scheme>oai</scheme><repositoryIdentifier>fake.myuniversity.edu</repositoryIdentifier><delimiter>:</delimiter><sampleIdentifier>oai:fake.myuniversity.edu:123456</sampleIdentifier></oai-identifier></description></Identify></OAI-PMH>';
-    }
-
-    /**
-     * Get a fake HTTP client
-     *
-     * @return \Zend\Http\Client
-     */
-    protected function getMockClient()
-    {
-        $query = $this->getMock('Zend\Stdlib\Parameters');
-        $request = $this->getMock('Zend\Http\Request');
-        $request->expects($this->any())
-            ->method('getQuery')
-            ->will($this->returnValue($query));
-        $headers = $this->getMock('Zend\Http\Headers');
-        $response = $this->getMock('Zend\Http\Response');
-        $response->expects($this->any())
-            ->method('getHeaders')
-            ->will($this->returnValue($headers));
-        $client = $this->getMock('Zend\Http\Client');
-        $client->expects($this->any())
-            ->method('getRequest')
-            ->will($this->returnValue($request));
-        $client->expects($this->any())
-            ->method('setMethod')
-            ->will($this->returnValue($client));
-        $client->expects($this->any())
-            ->method('send')
-            ->will($this->returnValue($response));
-        return $client;
-    }
-}
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Hierarchy/Driver/PluginManagerTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Hierarchy/Driver/PluginManagerTest.php
index 5f217350b727a3ca45037e8e0cfe7aa5505b89db..bd4cebbdbea0d0dcd3bc13622688f0cbc214ff7d 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/Hierarchy/Driver/PluginManagerTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Hierarchy/Driver/PluginManagerTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Hierarchy/TreeDataSource/PluginManagerTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Hierarchy/TreeDataSource/PluginManagerTest.php
index 2015e2f36a886dbeb7c15d8d24ff36a291889fe1..78a0fc391642a96c7ee867f2623bf56f61d90e97 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/Hierarchy/TreeDataSource/PluginManagerTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Hierarchy/TreeDataSource/PluginManagerTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Hierarchy/TreeRenderer/PluginManagerTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Hierarchy/TreeRenderer/PluginManagerTest.php
index 33cfaa02f239971d951f6d7ee089ab0f68ce8c18..d105bef9b35b230d8aa04b83592b03c45ac7ded2 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/Hierarchy/TreeRenderer/PluginManagerTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Hierarchy/TreeRenderer/PluginManagerTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/I18n/ExtendedIniNormalizerTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/I18n/ExtendedIniNormalizerTest.php
index 9be33285f20d7ed9e8f62084f76a8ab676784955..882ef20efa9fec18f45b08cd57397bc2be0cdeee 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/I18n/ExtendedIniNormalizerTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/I18n/ExtendedIniNormalizerTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/I18n/Translator/Loader/ExtendedIniReaderTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/I18n/Translator/Loader/ExtendedIniReaderTest.php
index d5b8a6285bd4634c6fb4155fe5bdf967055b8232..fd11881a9c62b2c0d1e37349e70b4247b0d48ec8 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/I18n/Translator/Loader/ExtendedIniReaderTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/I18n/Translator/Loader/ExtendedIniReaderTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/I18n/Translator/Loader/ExtendedIniTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/I18n/Translator/Loader/ExtendedIniTest.php
index 0f14b4bd6ea0ac27940d2864cd74a5e87713dc8e..cda7cacca4763d63c2291ade3bc57e329236185f 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/I18n/Translator/Loader/ExtendedIniTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/I18n/Translator/Loader/ExtendedIniTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/ILS/Driver/AlephTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/ILS/Driver/AlephTest.php
index c5a712e4e2cf9ef7af6c1fcbb9232a2d14a0fff5..ee3191ff7cb1f61a5f5387e877d57a15dad10518 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/ILS/Driver/AlephTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/ILS/Driver/AlephTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/ILS/Driver/AmicusTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/ILS/Driver/AmicusTest.php
index 23dac84a3cde9ecd12ebf8a4526dc618f0ebb332..cd56a798a0fa53ab50ccfb99d652898f82253164 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/ILS/Driver/AmicusTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/ILS/Driver/AmicusTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/ILS/Driver/ClaviusSQLTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/ILS/Driver/ClaviusSQLTest.php
index 050afca49be0f2f47d1f193e9acdd61bd7812b95..d7e58d4df4d75a1f62d37982a46ade1dd63ec0fe 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/ILS/Driver/ClaviusSQLTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/ILS/Driver/ClaviusSQLTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/ILS/Driver/DAIATest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/ILS/Driver/DAIATest.php
index 780d877421500849aed63fdf03c6059b38fddf48..4d83a8d70ec9f31bb58a18190b1d8b45701bed95 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/ILS/Driver/DAIATest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/ILS/Driver/DAIATest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
@@ -54,15 +54,26 @@ class DAIATest extends \VuFindTest\Unit\ILSDriverTestCase
                 'requests_placed' => '',
                 'id' => "027586081",
                 'item_id' => "http://uri.gbv.de/document/opac-de-000:epn:711134758",
-                'ilslink' => "http://opac.example-library.edu/DB=1/PPNSET?PPN=027586081",
+                'ilslink' => "http://opac.example-library.edu/loan/REQ?EPN=711134758",
                 'number' => 1,
                 'barcode' => "1",
                 'reserve' => "N",
                 'callnumber' => "ABC 12",
-                'location' => "Example Library for DAIA Tests - Abteilung III",
-                'locationhref' => false,
+                'location' => 'Example Library for DAIA Tests',
+                'locationid' => 'http://uri.gbv.de/organization/isil/DE-000',
+                'locationhref' => 'http://www.example-library.edu',
+                'storage' => 'Abteilung III',
+                'storageid' => '',
+                'storagehref' => '',
                 'item_notes' => [],
-                'services' => ['loan', 'presentation']
+                'services' => ['loan', 'presentation'],
+                'is_holdable' => false,
+                'addLink' => false,
+                'holdtype' => null,
+                'addStorageRetrievalRequestLink' => true,
+                'customData' => [],
+                'limitation_types' => [],
+                'doc_id' => 'http://uri.gbv.de/document/opac-de-000:ppn:027586081'
             ],
         1 =>
             [
@@ -77,10 +88,21 @@ class DAIATest extends \VuFindTest\Unit\ILSDriverTestCase
                 'barcode' => "1",
                 'reserve' => "N",
                 'callnumber' => "DEF 34",
-                'location' => "Example Library for DAIA Tests - Abteilung III",
-                'locationhref' => false,
+                'location' => 'Example Library for DAIA Tests',
+                'locationid' => 'http://uri.gbv.de/organization/isil/DE-000',
+                'locationhref' => 'http://www.example-library.edu',
+                'storage' => 'Abteilung III',
+                'storageid' => '',
+                'storagehref' => '',
                 'item_notes' => ['mit Zustimmung', 'nur Kopie'],
-                'services' => ['loan', 'presentation']
+                'services' => ['loan', 'presentation'],
+                'is_holdable' => false,
+                'addLink' => false,
+                'holdtype' => null,
+                'addStorageRetrievalRequestLink' => false,
+                'customData' => [],
+                'limitation_types' => [],
+                'doc_id' => 'http://uri.gbv.de/document/opac-de-000:ppn:027586081'
             ],
         2 =>
             [
@@ -95,10 +117,21 @@ class DAIATest extends \VuFindTest\Unit\ILSDriverTestCase
                 'barcode' => "1",
                 'reserve' => "N",
                 'callnumber' => "GHI 56",
-                'location' => "Example Library for DAIA Tests - Abteilung III",
-                'locationhref' => false,
+                'location' => 'Example Library for DAIA Tests',
+                'locationid' => 'http://uri.gbv.de/organization/isil/DE-000',
+                'locationhref' => 'http://www.example-library.edu',
+                'storage' => 'Abteilung III',
+                'storageid' => '',
+                'storagehref' => '',
                 'item_notes' => [],
-                'services' => []
+                'services' => [],
+                'is_holdable' => false,
+                'addLink' => false,
+                'holdtype' => null,
+                'addStorageRetrievalRequestLink' => false,
+                'customData' => [],
+                'limitation_types' => [],
+                'doc_id' => 'http://uri.gbv.de/document/opac-de-000:ppn:027586081'
             ],
     ];
 
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/ILS/Driver/DemoTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/ILS/Driver/DemoTest.php
index 38d08a8b767f5ecbc20551695f21602e75950513..46223719c980d7d74e058d1981f8d89ea033d89e 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/ILS/Driver/DemoTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/ILS/Driver/DemoTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/ILS/Driver/EvergreenTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/ILS/Driver/EvergreenTest.php
index 64baff7763e7c31b4bc5cf16381fdb3983fd5bf0..094880a90740421c842fd23f229c25927d77e2ad 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/ILS/Driver/EvergreenTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/ILS/Driver/EvergreenTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/ILS/Driver/HorizonTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/ILS/Driver/HorizonTest.php
index 6e40a8ed70db3ffbaf59c60e3eebd3f1df289d1d..bb0ef44810e5cd1dcec0349b64bc7acc283b2ce1 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/ILS/Driver/HorizonTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/ILS/Driver/HorizonTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/ILS/Driver/HorizonXMLAPITest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/ILS/Driver/HorizonXMLAPITest.php
index 40cc7289a84b627a5a2d4e262ecaae4b27d43d6d..3ec7bca47f347473d704ded2f508b0d5c0fc6f3b 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/ILS/Driver/HorizonXMLAPITest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/ILS/Driver/HorizonXMLAPITest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/ILS/Driver/InnovativeTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/ILS/Driver/InnovativeTest.php
index f219fdb05b7d4369b11a59b56a4b5f4288be328e..b884bba151c3c400721712d6ca266074ac43857d 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/ILS/Driver/InnovativeTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/ILS/Driver/InnovativeTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/ILS/Driver/KohaTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/ILS/Driver/KohaTest.php
index ba72afd4f9b718ee61f8dd464358c6ba6106cc6b..d763a5fdf299a82127c68d496dd5019bc7e31685 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/ILS/Driver/KohaTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/ILS/Driver/KohaTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/ILS/Driver/MultiBackendTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/ILS/Driver/MultiBackendTest.php
index ff03cb55c522430877a4707d77bb9d5807719fc3..a83d8828850ab18c0e82fd040237a100d04852ff 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/ILS/Driver/MultiBackendTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/ILS/Driver/MultiBackendTest.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
@@ -2321,6 +2321,11 @@ class MultiBackendTest extends \VuFindTest\Unit\TestCase
                 ->will(
                     $this->returnValue($this->getPatron('username', $userSource))
                 );
+            $mockAuth->expects($this->any())
+                ->method('getStoredCatalogCredentials')
+                ->will(
+                    $this->returnValue($this->getPatron('username', $userSource))
+                );
         }
         return $mockAuth;
     }
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/ILS/Driver/NewGenLibTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/ILS/Driver/NewGenLibTest.php
index f07fb4899e05134471477fc449d15def85ada5c8..8eb6ca46a7a4ddbb11bf79f15f713716a0232dd0 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/ILS/Driver/NewGenLibTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/ILS/Driver/NewGenLibTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/ILS/Driver/NoILSTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/ILS/Driver/NoILSTest.php
index c61033b6d185b12d0c123a7de7bbe325e4e8ca90..584b47e7d05c579a464d0d7efe0c5b87e3c6eb7b 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/ILS/Driver/NoILSTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/ILS/Driver/NoILSTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/ILS/Driver/PAIATest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/ILS/Driver/PAIATest.php
new file mode 100644
index 0000000000000000000000000000000000000000..1af911b12ca7d3219767a0869cd3bef3d498b3bd
--- /dev/null
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/ILS/Driver/PAIATest.php
@@ -0,0 +1,628 @@
+<?php
+/**
+ * ILS driver test
+ *
+ * PHP version 5
+ *
+ * Copyright (C) Villanova University 2011.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ *
+ * @category VuFind
+ * @package  Tests
+ * @author   Demian Katz <demian.katz@villanova.edu>
+ * @author   Oliver Goldschmidt <o.goldschmidt@tuhh.de>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     https://vufind.org Main Page
+ */
+namespace VuFindTest\ILS\Driver;
+use VuFind\ILS\Driver\PAIA;
+
+use Zend\Http\Client\Adapter\Test as TestAdapter;
+use Zend\Http\Response as HttpResponse;
+
+use InvalidArgumentException;
+
+/**
+ * ILS driver test
+ *
+ * @category VuFind
+ * @package  Tests
+ * @author   Demian Katz <demian.katz@villanova.edu>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     https://vufind.org Main Page
+ */
+class PAIATest extends \VuFindTest\Unit\ILSDriverTestCase
+{
+    protected $validConfig = [
+        'DAIA' =>
+            [
+                'baseUrl'            => 'http://daia.gbv.de/',
+            ],
+        'PAIA' =>
+            [
+                'baseUrl'            => 'http://paia.gbv.de/',
+            ]
+    ];
+
+    protected $patron = [
+        'id' => '08301001001',
+        'firstname' => 'Nobody',
+        'lastname' => 'Nothing',
+        'email' => 'nobody@vufind.org',
+        'major' => null,
+        'college' => null,
+        'name' => ' Nobody Nothing',
+        'expires' => '9999-12-31',
+        'status' => 0,
+        'address' => 'No street at all 8, D-21073 Hamburg',
+        'type' => [
+            0 => 'de-830:user-type:2'
+        ],
+        'cat_username' => '08301001001',
+        'cat_password' => 'NOPASSWORD'
+    ];
+
+    protected $patron_bad = [
+        'id' => '08301001011',
+        'firstname' => 'Invalid',
+        'lastname' => 'Nobody',
+        'email' => 'nobody_invalid@vufind.org',
+        'major' => null,
+        'college' => null,
+        'name' => ' Nobody Nothing',
+        'expires' => '9999-12-31',
+        'status' => 9,
+        'address' => 'No street at all 8, D-21073 Hamburg',
+        'type' => [
+            0 => 'de-830:user-type:2'
+        ],
+        'cat_username' => '08301001011',
+        'cat_password' => 'NOPASSWORD'
+    ];
+
+    protected $patron_expired = [
+        'id' => '08301001111',
+        'firstname' => 'Expired',
+        'lastname' => 'Nobody',
+        'email' => 'nobody_expired@vufind.org',
+        'major' => null,
+        'college' => null,
+        'name' => ' Nobody Nothing',
+        'expires' => '2015-12-31',
+        'status' => 0,
+        'address' => 'No street at all 8, D-21073 Hamburg',
+        'type' => [
+            0 => 'de-830:user-type:2'
+        ],
+        'cat_username' => '08301001111',
+        'cat_password' => 'NOPASSWORD'
+    ];
+
+    protected $feeTestResult = [
+        0 =>
+            [
+                'amount' => 160.0,
+                'checkout' => '',
+                'fine' => 'Vormerkgebuehr',
+                'balance' => 160.0,
+                'createdate' => '06-07-2016',
+                'duedate' => '',
+                'id' => '',
+                'title' => null,
+                'feeid' => null,
+                'about' => 'Open source licensing : software freedom and intellectual property law ; [open source licensees are free to: use open source software for any purpose, make and distribute copies, create and distribute derivative works, access and use the source code, com / Rosen, Lawrence (c 2005)',
+                'item' => 'http://uri.gbv.de/document/opac-de-830:bar:830$28295402'
+            ],
+        1 =>
+            [
+                'amount' => 80.0,
+                'checkout' => '',
+                'fine' => 'Vormerkgebuehr',
+                'balance' => 80.0,
+                'createdate' => '05-23-2016',
+                'duedate' => '',
+                'id' => '',
+                'title' => null,
+                'feeid' => null,
+                'about' => 'Zend framework in action / Allen, Rob (2009)',
+                'item' => 'http://uri.gbv.de/document/opac-de-830:bar:830$28323471'
+            ],
+        2 =>
+            [
+                'amount' => 300.0,
+                'checkout' => '',
+                'fine' => 'Säumnisgebühr',
+                'balance' => 300.0,
+                'createdate' => '05-23-2016',
+                'duedate' => '',
+                'id' => '',
+                'title' => null,
+                'feeid' => null,
+                'about' => 'Unsere historischen Gärten / Lutze, Margot (1986)',
+                'item' => 'http://uri.gbv.de/document/opac-de-830:bar:830$24476416'
+            ],
+        3 =>
+            [
+                'amount' => 100.0,
+                'checkout' => '',
+                'fine' => 'Säumnisgebühr',
+                'balance' => 100.0,
+                'createdate' => '06-16-2016',
+                'duedate' => '',
+                'id' => '',
+                'title' => null,
+                'feeid' => null,
+                'about' => 'Triumphe des Backsteins = Triumphs of brick / (1992)',
+                'item' => 'http://uri.gbv.de/document/opac-de-830:bar:830$33204941'
+            ],
+        4 =>
+            [
+                'amount' => 100.0,
+                'checkout' => '',
+                'fine' => 'Säumnisgebühr',
+                'balance' => 100.0,
+                'createdate' => '05-23-2016',
+                'duedate' => '',
+                'id' => '',
+                'title' => null,
+                'feeid' => null,
+                'about' => 'Lehrbuch der Botanik / Strasburger, Eduard (2008)',
+                'item' => 'http://uri.gbv.de/document/opac-de-830:bar:830$26461872'
+            ],
+    ];
+
+    protected $holdsTestResult = [
+        0 =>
+            [
+                'item_id' => 'http://uri.gbv.de/document/opac-de-830:bar:830$34096983',
+                'cancel_details' => '',
+                'id' => 'http://uri.gbv.de/document/opac-de-830:ppn:040445623',
+                'type' => 'provided',
+                'location' => 'Test-Theke',
+                'position' => 0,
+                'available' => true,
+                'title' => 'Praktikum über Entwurf und Manipulation von Datenbanken : SQL/DS (IBM), UDS (Siemens) und MEMODAX / Vossen, Gottfried (1986)',
+                'callnumber' => '34:3409-6983',
+                'create' => '06-17-2016',
+                'expire' => '',
+            ],
+        1 =>
+            [
+                'item_id' => 'http://uri.gbv.de/document/opac-de-830:bar:830$28295402',
+                'cancel_details' => 'http://uri.gbv.de/document/opac-de-830:bar:830$28295402',
+                'id' => 'http://uri.gbv.de/document/opac-de-830:ppn:391260316',
+                'type' => 'reserved',
+                'location' => 'Ausleihe',
+                'position' => 0,
+                'available' => false,
+                'title' => 'Open source licensing : software freedom and intellectual property law ; [open source licensees are free to: use open source software for any purpose, make and distribute copies, create and distribute derivative works, access and use the source code, com / Rosen, Lawrence (c 2005)',
+                'callnumber' => '28:2829-5402',
+                'create' => '06-15-2016',
+                'duedate' => '06-15-2016',
+            ],
+    ];
+
+    protected $requestsTestResult = [
+        0 =>
+            [
+                'item_id' => 'http://uri.gbv.de/document/opac-de-830:bar:830$24260127',
+                'cancel_details' => '',
+                'id' => 'http://uri.gbv.de/document/opac-de-830:ppn:020966334',
+                'type' => 'ordered',
+                'location' => 'Ausleihe',
+                'position' => 0,
+                'available' => false,
+                'title' => 'Gold / Kettell, Brian (1982)',
+                'callnumber' => '24:2426-0127',
+                'create' => '04-25-2016',
+            ],
+    ];
+
+    protected $transactionsTestResult = [
+        0 =>
+            [
+                'item_id' => 'http://uri.gbv.de/document/opac-de-830:bar:830$28342436',
+                'id' => 'http://uri.gbv.de/document/opac-de-830:ppn:58891861X',
+                'title' => 'Theoretische Informatik : mit 22 Tabellen und 78 Aufgaben / Hoffmann, Dirk W. (2009)',
+                'callnumber' => '28:2834-2436',
+                'renewable' => false,
+                'renew_details' => '',
+                'request' => 0,
+                'renew' => 12,
+                'reminder' => 1,
+                'startTime' => '11-15-2013',
+                'dueTime' => '06-15-2016',
+                'duedate' => '',
+                'message' => '',
+                'borrowingLocation' => 'Ausleihe',
+            ],
+        1 =>
+            [
+                'renewable' => false,
+                'item_id' => 'http://uri.gbv.de/document/opac-de-830:bar:830$22278001',
+                'renew_details' => '',
+                'id' => 'http://uri.gbv.de/document/opac-de-830:ppn:659228084',
+                'title' => 'Linked Open Library Data : bibliographische Daten und ihre Zugänglichkeit im Web der Daten ; Innovationspreis 2011 / Fürste, Fabian M. (2011)',
+                'request' => 0,
+                'renew' => 9,
+                'reminder' => 0,
+                'startTime' => '12-22-2011',
+                'dueTime' => '07-14-2016',
+                'duedate' => '',
+                'message' => '',
+                'borrowingLocation' => 'Ausleihe',
+                'callnumber' => '22:2227-8001',
+            ]
+    ];
+
+    protected $renewTestResult = [
+        'blocks' => false,
+        'details' => [
+            'http://uri.gbv.de/document/opac-de-830:bar:830$22061137' => [
+                'success' => true,
+                'new_date' => "07-18-2016",
+                'item_id' => 0,
+                'sysMessage' => "Successfully renewed"
+            ]
+        ]
+    ];
+
+    protected $storageRetrievalTestResult = [
+        'success' => true,
+        'sysMessage' => 'Successfully requested'
+    ];
+
+    protected $pwchangeTestResult = [
+        'success' => true,
+        'status' => "Successfully changed"
+    ];
+
+    protected $profileTestResult = [
+        'firstname' => "Nobody",
+        'lastname' => "Nothing",
+        'address1' => NULL,
+        'address2' => NULL,
+        'city' => NULL,
+        'country' => NULL,
+        'zip' => NULL,
+        'phone' => NULL,
+        'group' => NULL,
+        'expires' => "12-31-9999",
+        'statuscode' => 0,
+        'canWrite' => true
+    ];
+
+    /******************* Test cases ***************/
+    /*
+     ok changePassword
+     ok checkRequestIsValid
+     ok checkStorageRetrievalRequestIsValid
+     ok getMyProfile
+     ok getMyFines
+     ok getMyHolds
+     ok getMyTransactions
+     ok getRenewDetails
+     ok getMyStorageRetrievalRequests
+     ok placeHold
+     ok renewMyItems
+     ok placeStorageRetrievalRequest
+     */
+
+    /**
+     * Constructor
+     */
+    public function __construct()
+    {
+        $this->driver = $this->createConnector();
+    }
+
+    /**
+     * Test
+     *
+     * @return void
+     */
+    public function testChangePassword()
+    {
+        $changePasswordTestdata = [
+            "patron" => [
+                "cat_username" => "08301001001"
+             ],
+             "oldPassword" => "oldsecret",
+             "newPassword" => "newsecret"
+        ];
+
+        $conn = $this->createConnector('changePassword.json');
+        $conn->setConfig($this->validConfig);
+        $conn->init();
+        $result = $conn->changePassword($changePasswordTestdata);
+        $this->assertEquals($this->pwchangeTestResult, $result);
+    }
+
+    /**
+     * Test
+     *
+     * @return void
+     */
+    public function testFees()
+    {
+        $conn = $this->createConnector('fees.json');
+        $conn->setConfig($this->validConfig);
+        $conn->init();
+        $result = $conn->getMyFines($this->patron);
+
+        $this->assertEquals($this->feeTestResult, $result);
+    }
+
+    /**
+     * Test
+     *
+     * @return void
+     */
+    public function testHolds()
+    {
+        $conn = $this->createConnector('items.json');
+        $conn->setConfig($this->validConfig);
+        $conn->init();
+        $result = $conn->getMyHolds($this->patron);
+
+        $this->assertEquals($this->holdsTestResult, $result);
+    }
+
+    /**
+     * Test
+     *
+     * @return void
+     */
+    public function testRequests()
+    {
+        $conn = $this->createConnector('items.json');
+        $conn->setConfig($this->validConfig);
+        $conn->init();
+        $result = $conn->getMyStorageRetrievalRequests($this->patron);
+
+        $this->assertEquals($this->requestsTestResult, $result);
+    }
+
+    /**
+     * Test
+     *
+     * @return void
+     */
+    public function testTransactions()
+    {
+        $conn = $this->createConnector('items.json');
+        $conn->setConfig($this->validConfig);
+        $conn->init();
+        $result = $conn->getMyTransactions($this->patron);
+
+        $this->assertEquals($this->transactionsTestResult, $result);
+    }
+
+    /**
+     * Test
+     *
+     * @return void
+     */
+    public function testProfile()
+    {
+        $conn = $this->createMockConnector('patron.json');
+        $result = $conn->getMyProfile($this->patron);
+
+        $this->assertEquals($this->profileTestResult, $result);
+    }
+
+    /**
+     * Test
+     *
+     * @return void
+     */
+    public function testValidRequest()
+    {
+        $conn = $this->createMockConnector('patron.json');
+
+        $result = $conn->checkRequestIsValid(
+            'http://paia.gbv.de/', [], $this->patron
+        );
+        $resultStorageRetrieval = $conn->checkStorageRetrievalRequestIsValid(
+            'http://paia.gbv.de/', [], $this->patron
+        );
+        $result_bad = $conn->checkRequestIsValid(
+            'http://paia.gbv.de/', [], $this->patron_bad
+        );
+        $resultStorage_bad = $conn->checkStorageRetrievalRequestIsValid(
+            'http://paia.gbv.de/', [], $this->patron_bad
+        );
+        $result_expired = $conn->checkRequestIsValid(
+            'http://paia.gbv.de/', [], $this->patron_expired
+        );
+        $resultStorage_expired = $conn->checkStorageRetrievalRequestIsValid(
+            'http://paia.gbv.de/', [], $this->patron_expired
+        );
+
+        $this->assertEquals(true, $result);
+        $this->assertEquals(true, $resultStorageRetrieval);
+        $this->assertEquals(false, $result_bad);
+        $this->assertEquals(false, $resultStorage_bad);
+        $this->assertEquals(false, $result_expired);
+        $this->assertEquals(false, $resultStorage_expired);
+    }
+    /**
+     * Test
+     *
+     * @return void
+     */
+    public function testRenewDetails()
+    {
+        $conn = $this->createConnector('');
+        $conn->setConfig($this->validConfig);
+        $conn->init();
+        $result = $conn->getRenewDetails($this->transactionsTestResult[1]);
+
+        $this->assertEquals('', $result);
+    }
+
+    /**
+     * Test
+     *
+     * @return void
+     */
+    public function testPlaceHold()
+    {
+        $sr_request = [
+            "item_id"     => "http://uri.gbv.de/document/opac-de-830:bar:830$24014292",
+            "patron" => [
+                "cat_username" => "08301001001"
+            ]
+        ];
+
+        $conn = $this->createConnector('storageretrieval.json');
+        $conn->setConfig($this->validConfig);
+        $conn->init();
+        $result = $conn->placeHold($sr_request);
+        $this->assertEquals($this->storageRetrievalTestResult, $result);
+    }
+
+    /**
+     * Test
+     *
+     * @return void
+     */
+    public function testPlaceStorageRetrievalRequest()
+    {
+        $sr_request = [
+            "item_id"     => "http://uri.gbv.de/document/opac-de-830:bar:830$24014292",
+            "patron" => [
+                "cat_username" => "08301001001"
+            ]
+        ];
+
+        $conn = $this->createConnector('storageretrieval.json');
+        $conn->setConfig($this->validConfig);
+        $conn->init();
+        $result = $conn->placeStorageRetrievalRequest($sr_request);
+        $this->assertEquals($this->storageRetrievalTestResult, $result);
+    }
+
+    /**
+     * Test
+     *
+     * @return void
+     */
+    public function testRenew()
+    {
+        $renew_request = [
+            "details" => [
+                "item"     => "http://uri.gbv.de/document/opac-de-830:bar:830$22061137"
+            ],
+            "patron" => [
+                "cat_username" => "08301001001"
+            ]
+        ];
+
+        $conn = $this->createConnector('renew_ok.json');
+        $conn->setConfig($this->validConfig);
+        $conn->init();
+        $result = $conn->renewMyItems($renew_request);
+
+        $this->assertEquals($this->renewTestResult, $result);
+
+    /* TODO: make me work
+        $conn_fail = $this->createConnector('renew_error.json');
+        $connfail->setConfig($this->validConfig);
+        $conn_fail->init();
+        $result_fail = $conn_fail->renewMyItems($renew_request);
+
+        $this->assertEquals($this->failedRenewTestResult, $result_fail);
+    */
+    }
+
+    /**
+     * Create connector with fixture file.
+     *
+     * @param string $fixture Fixture file
+     *
+     * @return Connector
+     *
+     * @throws InvalidArgumentException Fixture file does not exist
+     */
+    protected function createConnector($fixture = null)
+    {
+        $adapter = new TestAdapter();
+        if ($fixture) {
+            $file = realpath(
+                __DIR__ .
+                '/../../../../../../tests/fixtures/paia/response/' . $fixture
+            );
+            if (!is_string($file) || !file_exists($file) || !is_readable($file)) {
+                throw new InvalidArgumentException(
+                    sprintf('Unable to load fixture file: %s ', $file)
+                );
+            }
+            $response = file_get_contents($file);
+            $responseObj = HttpResponse::fromString($response);
+            $adapter->setResponse($responseObj);
+        }
+        $service = new \VuFindHttp\HttpService();
+        $service->setDefaultAdapter($adapter);
+        $conn = new PAIA(
+            new \VuFind\Date\Converter(),
+            new \Zend\Session\SessionManager()
+        );
+        $conn->setHttpService($service);
+        return $conn;
+    }
+
+    /**
+     * Create connector with fixture file.
+     *
+     * @param string $fixture Fixture file
+     *
+     * @return Connector
+     *
+     * @throws InvalidArgumentException Fixture file does not exist
+     */
+    protected function createMockConnector($fixture = null)
+    {
+        $adapter = new TestAdapter();
+        if ($fixture) {
+            $file = realpath(
+                __DIR__ .
+                '/../../../../../../tests/fixtures/paia/response/' . $fixture
+            );
+            if (!is_string($file) || !file_exists($file) || !is_readable($file)) {
+                throw new InvalidArgumentException(
+                    sprintf('Unable to load fixture file: %s ', $file)
+                );
+            }
+            $response = file_get_contents($file);
+            $responseObj = HttpResponse::fromString($response);
+            $adapter->setResponse($responseObj);
+        }
+        $service = new \VuFindHttp\HttpService();
+        $service->setDefaultAdapter($adapter);
+        $conn = $this->getMockBuilder('VuFind\ILS\Driver\PAIA')
+            ->setConstructorArgs([ new \VuFind\Date\Converter(),
+                new \Zend\Session\SessionManager()
+            ])
+            ->setMethods(['getScope'])
+            ->getMock();
+        $conn->expects($this->any())->method('getScope')
+            ->will($this->returnValue([ 'write_items' ]));
+        $conn->setHttpService($service);
+        $conn->setConfig($this->validConfig);
+        $conn->init();
+        return $conn;
+    }
+}
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/ILS/Driver/PluginManagerTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/ILS/Driver/PluginManagerTest.php
index 7871bb5f1b66aad51d42da4f89e0900877a00ebf..391c3a00e82e13049336dfa0a8a93a9ab67514f0 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/ILS/Driver/PluginManagerTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/ILS/Driver/PluginManagerTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/ILS/Driver/PolarisTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/ILS/Driver/PolarisTest.php
index fcc528282b9765e09ce2de38144b45eebecdc546..874b61c48ecaeb5176b3e01d2f65630e14ba8391 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/ILS/Driver/PolarisTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/ILS/Driver/PolarisTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/ILS/Driver/SampleTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/ILS/Driver/SampleTest.php
index cffc8bb15feac9b7575fd73589f73e6509a0598b..7111c3fa86f50ca62b666427938ff7dec40dd43b 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/ILS/Driver/SampleTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/ILS/Driver/SampleTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/ILS/Driver/SymphonyTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/ILS/Driver/SymphonyTest.php
index d389751771e45bb3a4a914c96df0a3acd8eff1a2..88afb6c84c92bd5d2d8f898a07fcc33a716217a0 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/ILS/Driver/SymphonyTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/ILS/Driver/SymphonyTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/ILS/Driver/UnicornTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/ILS/Driver/UnicornTest.php
index 90492bc4f15141d8b480a73ee6088da456762b6f..cc24dee02b44b4ba98a0a08c44ad668fa40cf22f 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/ILS/Driver/UnicornTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/ILS/Driver/UnicornTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/ILS/Driver/VirtuaTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/ILS/Driver/VirtuaTest.php
index 26a448708981356d99f50eb3830716847394b1f6..5e67b6222b978ce3fd9b8eb9dd90b837460831fe 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/ILS/Driver/VirtuaTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/ILS/Driver/VirtuaTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/ILS/Driver/VoyagerRestfulTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/ILS/Driver/VoyagerRestfulTest.php
index 81e3c9e75d985920d392b72f5b64bd41fa393149..ba32ce436471cc4e37e30199268a97321ec59ee2 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/ILS/Driver/VoyagerRestfulTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/ILS/Driver/VoyagerRestfulTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/ILS/Driver/VoyagerTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/ILS/Driver/VoyagerTest.php
index ee6dda329c14fa0f7edbe3d97569166ec91a9a9a..018363a1e2a0c822b13c71ad423d3c24b8e1f369 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/ILS/Driver/VoyagerTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/ILS/Driver/VoyagerTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/ILS/Driver/XCNCIP2Test.php b/module/VuFind/tests/unit-tests/src/VuFindTest/ILS/Driver/XCNCIP2Test.php
index d0f0be0091b6dba863da40cd3e7a79dc0a160b0b..1ef3834f39c67fb05c2b4e2e0160733a482156e6 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/ILS/Driver/XCNCIP2Test.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/ILS/Driver/XCNCIP2Test.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Log/LoggerTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Log/LoggerTest.php
index 9b33b1a1ff09cce867fb638578625a59b4e622c6..76e765994dcdcf7aeb1be79ae118b13ae63498a3 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/Log/LoggerTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Log/LoggerTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
@@ -57,8 +57,8 @@ Array
     [REQUEST_URI] => /foo/bar
 )
 CONTEXT;
-            return $a[1] === 'test'
-                && $a[2] === 'test(Server: IP = 1.2.3.4, Referer = none, User Agent = Fake browser, Host = localhost:80, Request URI = /foo/bar)'
+            return $a[1] === 'Exception : test'
+                && $a[2] === 'Exception : test(Server: IP = 1.2.3.4, Referer = none, User Agent = Fake browser, Host = localhost:80, Request URI = /foo/bar)'
                 && false !== strpos($a[3], $a[2])
                 && false !== strpos($a[3], 'Backtrace:')
                 && false !== strpos($a[3], 'line')
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Mailer/MailerTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Mailer/MailerTest.php
index 11231b38d471ae8703359f920eab38e0c1e18f75..0d7319b426ac1732b076299ed28c8f84d546153d 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/Mailer/MailerTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Mailer/MailerTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Net/IpAddressUtilsTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Net/IpAddressUtilsTest.php
index e64b30ef4546373b5c4bb9c9dbd2e257a1a3f95d..a4c6ba9c288f0a67f1222d891c9c268d29b0328f 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/Net/IpAddressUtilsTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Net/IpAddressUtilsTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/OAI/ServerTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/OAI/ServerTest.php
index 51279eeef62e189144603e1780b834b4fb3f76ec..b1edff494fdcbc71c8be78e25bfc7448039052a9 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/OAI/ServerTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/OAI/ServerTest.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category Search
  * @package  Service
@@ -33,23 +33,6 @@ use VuFind\OAI\Server;
 /**
  * OAI-PMH server unit test.
  *
- * PHP version 5
- *
- * Copyright (C) Villanova University 2010.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License version 2,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
- *
  * @category Search
  * @package  Service
  * @author   David Maus <maus@hab.de>
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/QRCode/LoaderTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/QRCode/LoaderTest.php
index 3993d6763a944c3ea9d8430966b53efa644a40eb..737e0d3e8652b73f81a35bb553e29696156a4ef9 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/QRCode/LoaderTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/QRCode/LoaderTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Recommend/CollectionSideFacetsTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Recommend/CollectionSideFacetsTest.php
index 86ef08b80dd1c374ba790c1cc20f3d6aa6e2075c..a32069a75542b9bffe266633e969300ddedeb0d0 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/Recommend/CollectionSideFacetsTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Recommend/CollectionSideFacetsTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Recommend/EuropeanaResultsDeferredTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Recommend/EuropeanaResultsDeferredTest.php
index 29bc331bbeb2648d4b89bb8740e4c2e5f138943a..69c637449e36a6512a53e226db1d832c555dc34f 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/Recommend/EuropeanaResultsDeferredTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Recommend/EuropeanaResultsDeferredTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Recommend/ExpandFacetsTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Recommend/ExpandFacetsTest.php
index e1fc13b9ad72df3b8fcb8665a65169621d83f8d3..749ba70b36a7f538e9bd5fdff3299a54aa8d6bc9 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/Recommend/ExpandFacetsTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Recommend/ExpandFacetsTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Recommend/FacetCloudTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Recommend/FacetCloudTest.php
index 0e471c8709690260b6dd8ca87628332dd4bfe4e4..d32819c3d7c7f1a528ba093f9844c212f8f0951d 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/Recommend/FacetCloudTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Recommend/FacetCloudTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Recommend/FavoriteFacetsTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Recommend/FavoriteFacetsTest.php
index 0354ac88ed4f6dee62fccc941f62b7fad33c45b4..d0e3a8d7cf969ccf370f31fca662360925f2a31f 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/Recommend/FavoriteFacetsTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Recommend/FavoriteFacetsTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Recommend/OpenLibrarySubjectsDeferredTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Recommend/OpenLibrarySubjectsDeferredTest.php
index 562bdbad5e13dc7a095f56d25758df358a66b385..7eac330748e36dce791bda1aae71da1a696ed792 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/Recommend/OpenLibrarySubjectsDeferredTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Recommend/OpenLibrarySubjectsDeferredTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Recommend/PluginManagerTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Recommend/PluginManagerTest.php
index a2b0142a126dee99dda6a15acfbe6b27523c1f57..56fd205dd0dfa5c46fb5f126bb72fdc7acd6363d 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/Recommend/PluginManagerTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Recommend/PluginManagerTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Recommend/RandomRecommendTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Recommend/RandomRecommendTest.php
index 58cb074816f28e993a837210c653f1f85da84db3..df7e823d363ac21f5b35ed14ae848aea54960a93 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/Recommend/RandomRecommendTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Recommend/RandomRecommendTest.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Recommend/SideFacetsTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Recommend/SideFacetsTest.php
index 97432ee5e4bdf59b6d538f4a47a95ea2e188828c..1f93997559026819626cfdc4847acb81945337a2 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/Recommend/SideFacetsTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Recommend/SideFacetsTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Recommend/SummonBestBetsDeferredTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Recommend/SummonBestBetsDeferredTest.php
index 55a613cc2773db4631694485132d4dbec1f211b8..91972c247cbe955fce68c7f4e9e9e9c2744b81ad 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/Recommend/SummonBestBetsDeferredTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Recommend/SummonBestBetsDeferredTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Recommend/SummonDatabasesDeferredTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Recommend/SummonDatabasesDeferredTest.php
index 160f50dcd0f332af62a82d172d99e86bdada4ae4..ae0a5d751ed3f35208528d92e446df6fbc599ba7 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/Recommend/SummonDatabasesDeferredTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Recommend/SummonDatabasesDeferredTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Recommend/SummonResultsDeferredTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Recommend/SummonResultsDeferredTest.php
index 18ebde640a002b6e54e81daa4b88c9471e1bf326..6e14b1a74dd0f649435aaf61de33b82ee5b1360f 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/Recommend/SummonResultsDeferredTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Recommend/SummonResultsDeferredTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Recommend/SwitchQueryTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Recommend/SwitchQueryTest.php
index 85ccfbb4cb26e6d17a6428f456ceb73abe63d9cf..d9ec2632b615be31c503123677c6f018a5f189be 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/Recommend/SwitchQueryTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Recommend/SwitchQueryTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Record/CacheTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Record/CacheTest.php
index 6f521314f8e87a31d58008b52fb06ae883231e14..bebf604c43c9cf87d05c22c4d4326e7809f473f5 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/Record/CacheTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Record/CacheTest.php
@@ -19,7 +19,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Record/LoaderTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Record/LoaderTest.php
index 2223d60c1538f748c3ce59fa5c6d598d6f856079..0406874f133bad42f1169885d6ceb14deac12848 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/Record/LoaderTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Record/LoaderTest.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Record/RouterTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Record/RouterTest.php
index e8e760ca6e2ff8093529f6820f75e372493f64d2..fa405428915169704297ed23eb661b0474754429 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/Record/RouterTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Record/RouterTest.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/RecordDriver/PluginManagerTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/RecordDriver/PluginManagerTest.php
index 903ad42b7690ad14c5b1bb3e61cfd830835673a6..fa79725ceef269280005e96156a9083c2b8e68e1 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/RecordDriver/PluginManagerTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/RecordDriver/PluginManagerTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/RecordDriver/Response/PublicationDetailsTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/RecordDriver/Response/PublicationDetailsTest.php
index 4d0fb8b2a8daf2b70ff638ec4668436724f2ac5e..c40859a6edaf2169e98d6d5d877cce75497107e0 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/RecordDriver/Response/PublicationDetailsTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/RecordDriver/Response/PublicationDetailsTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/RecordDriver/SolrDefaultTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/RecordDriver/SolrDefaultTest.php
index 986106739039860d940cd0edcbc110e02359854f..6ebdcf2735add745b6f130749201c2428c44e3e4 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/RecordDriver/SolrDefaultTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/RecordDriver/SolrDefaultTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/RecordDriver/SolrMarcTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/RecordDriver/SolrMarcTest.php
index 48e899a44bac927088fb148591bea575af2cc251..248750950754f81654513f96eb8a90124125e5eb 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/RecordDriver/SolrMarcTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/RecordDriver/SolrMarcTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/RecordTab/PluginManagerTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/RecordTab/PluginManagerTest.php
index 0afe37a7134ff34a7bbfa43ad702681f1d32d63c..bd1877dff58c4a4d7a785e4369d40858d952001c 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/RecordTab/PluginManagerTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/RecordTab/PluginManagerTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Related/PluginManagerTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Related/PluginManagerTest.php
index 706121efc8727792b086301b258595340afe1b58..2eef50e1130576a05ad22ccc9f6bfee6aaf68613 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/Related/PluginManagerTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Related/PluginManagerTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Related/SimilarTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Related/SimilarTest.php
index 630c09c2b5d96f2efaaa07127a44f829573c9b3a..5b627aef27af46f5dbb1d13901f05783f4874d83 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/Related/SimilarTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Related/SimilarTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Related/WorldCatSimilarTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Related/WorldCatSimilarTest.php
index aeee1a5c6461e77f4a5cac6968b85a4b4c7f2cf8..19b67cf1b98f4a7b1f7c9289f9f7f9a431c7fafe 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/Related/WorldCatSimilarTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Related/WorldCatSimilarTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Reserves/CsvReaderTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Reserves/CsvReaderTest.php
index 7465da0749e765abb901328f1601cd1675a950c5..062644f74306105759143d8b96aae6e9249ea3e1 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/Reserves/CsvReaderTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Reserves/CsvReaderTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Resolver/Driver/PluginManagerTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Resolver/Driver/PluginManagerTest.php
index 4bb6450e91633d4249fa7967436f04b8bf09fa16..0bf83a09dd5eb44cbc8cdfc8ee2eec2c19938e8a 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/Resolver/Driver/PluginManagerTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Resolver/Driver/PluginManagerTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Resolver/Driver/RediTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Resolver/Driver/RediTest.php
index 5b8a98c22fadfa993478e8157d2462881548c063..fd520b6a1c390e1d8fa8c9cd8d57b9e19f7d5248 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/Resolver/Driver/RediTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Resolver/Driver/RediTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Role/DynamicRoleProviderTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Role/DynamicRoleProviderTest.php
index e802e7ef285b44ff1053c28ba68d0335b60d6e35..76455cf40f7b5a9846f76c5a46ea6388178bc7cc 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/Role/DynamicRoleProviderTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Role/DynamicRoleProviderTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Role/PermissionProvider/PluginManagerTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Role/PermissionProvider/PluginManagerTest.php
index 273ff71b589a4ccf0ce170ea91ec9afdb4b8aed8..faf6d89679bf72f431150d2f1b1448dc4cf6e81a 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/Role/PermissionProvider/PluginManagerTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Role/PermissionProvider/PluginManagerTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Role/PermissionProvider/ServerParamTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Role/PermissionProvider/ServerParamTest.php
index f36bffef09b705063901aab654d5f455a80a43f3..43171a8ee6017db3ff17f1f76592d649ae7e85c0 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/Role/PermissionProvider/ServerParamTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Role/PermissionProvider/ServerParamTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Role/PermissionProvider/ShibbolethTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Role/PermissionProvider/ShibbolethTest.php
index 3df70ee30901d390288f420eac4e7c00f78a2a44..660ce4ab1668f9d91503d39800fd2a1dcce0bfdd 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/Role/PermissionProvider/ShibbolethTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Role/PermissionProvider/ShibbolethTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Role/PermissionProvider/UserTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Role/PermissionProvider/UserTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..43aa098d9333b2a148e2b248c66eaccb61b98552
--- /dev/null
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Role/PermissionProvider/UserTest.php
@@ -0,0 +1,156 @@
+<?php
+/**
+ * PermissionProvider User Test Class
+ *
+ * PHP version 5
+ *
+ * Copyright (C) Villanova University 2010.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ * @category VuFind
+ * @package  Tests
+ * @author   Markus Beh <markus.beh@ub.uni-freiburg.de>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     https://vufind.org/wiki/development:testing:unit_tests Wiki
+ */
+namespace VuFindTest\Role\PermissionProvider;
+
+use ZfcRbac\Service\AuthorizationService;
+
+/**
+ * PermissionProvider User Test Class
+ *
+ * @category VuFind
+ * @package  Tests
+ * @author   Markus Beh <markus.beh@ub.uni-freiburg.de>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     https://vufind.org/wiki/development:testing:unit_tests Wiki
+ */
+class UserTest extends \VuFindTest\Unit\TestCase
+{
+    protected $userValueMap = [
+        'testuser1' =>
+        [
+                ['username','mbeh'],
+                ['email','markus.beh@ub.uni-freiburg.de'],
+                ['college', 'Albert Ludwigs Universität Freiburg']
+        ]
+        ,
+        'testuser2' =>
+        [
+                ['username','mbeh2'],
+                ['email','markus.beh@ub.uni-freiburg.de'],
+                ['college', 'Villanova University'],
+                ['major', 'alumni']
+        ]
+    ];
+
+    /**
+     * Test single option with matching string
+     *
+     * @return void
+     */
+    public function testGetPermissions()
+    {
+        $this->check(
+            'testuser1',
+            ['college .*Freiburg'],
+            ['loggedin']
+        );
+
+        $this->check(
+            'testuser2',
+            ["college .*Freiburg"],
+            []
+        );
+    }
+
+    /**
+     * Test an invalid configuration
+     *
+     * @return void
+     */
+    public function testBadConfig()
+    {
+        $this->check(
+            'testuser1',
+            ['college'],
+            []
+        );
+    }
+
+    /**
+     * Convenience method for executing similiar tests
+     *
+     * @param string $testuser Name of testuser
+     * @param array  $options  Options like settings in permissions.ini
+     * @param array  $roles    Roles to return if match
+     *
+     * @return void
+     */
+    protected function check($testuser, $options, $roles)
+    {
+        $this->testuser
+            = (isset($this->userValueMap[$testuser]))
+            ? $testuser
+            : 'testuser1';
+
+        $auth = $this->getMockAuthorizationService();
+        $this->permissionProvider
+            = new \VuFind\Role\PermissionProvider\User($auth);
+
+        $this->assertEquals(
+            $roles,
+            $this->permissionProvider->getPermissions($options)
+        );
+    }
+
+    /**
+     * Get a mock authorization service object
+     *
+     * @return AuthorizationService
+     */
+    protected function getMockAuthorizationService()
+    {
+        $authorizationService
+            = $this->getMockBuilder('ZfcRbac\Service\AuthorizationService')
+                ->disableOriginalConstructor()
+                ->getMock();
+        $authorizationService
+            ->method('getIdentity')
+            ->will($this->returnValue($this->getMockUser()));
+
+        return $authorizationService;
+    }
+
+    /**
+     * Get a mock user object
+     *
+     * @return UserRow
+     */
+    protected function getMockUser()
+    {
+        $user = $this->getMockBuilder('VuFind\Db\Row\User')
+            ->disableOriginalConstructor()
+            ->getMock();
+        $user->method('__get')
+            ->will($this->returnValueMap($this->userValueMap[$this->testuser]));
+        $user->method('offsetGet')
+            ->will($this->returnValueMap($this->userValueMap[$this->testuser]));
+
+        return $user;
+    }
+
+}
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/SMS/ClickatellTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/SMS/ClickatellTest.php
index 4b47be05bc03eb1f96b86a306a7e92c0447c3091..29bbbf23964fc72f09a37aa76fd978fad84ed15b 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/SMS/ClickatellTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/SMS/ClickatellTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Search/BackendManagerTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Search/BackendManagerTest.php
index c5166b5433c1a61e817e49b779db975701e444c4..5ff43c7e8f18acb3581823daf7e696e7de0fa3e1 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/Search/BackendManagerTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Search/BackendManagerTest.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Search/Base/ParamsTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Search/Base/ParamsTest.php
index f2f7729517a288dcb2e62a62e077dcb88a92cd20..727aaa12de5b959b476d0e48b903ad9c538205ec 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/Search/Base/ParamsTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Search/Base/ParamsTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Search/MemoryTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Search/MemoryTest.php
index 28ea54b45b342065eae907d9509a01b6a30321f1..cbad1b53f6f7f3962254df0fcfa6754da6ac2367 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/Search/MemoryTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Search/MemoryTest.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Search/Options/PluginManagerTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Search/Options/PluginManagerTest.php
index 9f06b0fe12303d8c9bc12c923d0c3ccc4346452d..8bde975351c7756792aa08419245ef1423da5f65 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/Search/Options/PluginManagerTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Search/Options/PluginManagerTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Search/Params/PluginManagerTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Search/Params/PluginManagerTest.php
index fdabd49a9231f05a65484eadf835b0a45ab9262f..7922e568eeb151baa4e711221c25e931e1622b88 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/Search/Params/PluginManagerTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Search/Params/PluginManagerTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Search/Primo/OnCampusListenerTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Search/Primo/OnCampusListenerTest.php
index d21bd0b4f3fad4228fb793891360cf2cd21a9952..2f9971b24e825f5e4551de769349f4ffffa615d8 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/Search/Primo/OnCampusListenerTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Search/Primo/OnCampusListenerTest.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Search/Primo/PrimoPermissionHandlerTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Search/Primo/PrimoPermissionHandlerTest.php
index d0923d89d30262eafdb5d5e61d80c93670b0f7e6..29d7093d7243d7ffeced300d4a461265d0269e12 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/Search/Primo/PrimoPermissionHandlerTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Search/Primo/PrimoPermissionHandlerTest.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Search/QueryAdapterTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Search/QueryAdapterTest.php
index 27013794e31c35514996e0e290a3aab6c33c2f97..9ab9c5e3c92b54618a6b34ff5312907a83bf19ba 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/Search/QueryAdapterTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Search/QueryAdapterTest.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Search/Results/PluginManagerTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Search/Results/PluginManagerTest.php
index 76c78a3ca041018d85463f7089e65ccc9a775aba..7b825458c66e1ae5b0df5fa61c4310f027a23df7 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/Search/Results/PluginManagerTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Search/Results/PluginManagerTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Search/SearchTabsHelperTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Search/SearchTabsHelperTest.php
index 9200f3451db3e85234b5f87b394578df8240bde1..4059477ccec9b05091861f87e0b09db1865d724e 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/Search/SearchTabsHelperTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Search/SearchTabsHelperTest.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Search/Solr/ConditionalFilterListenerTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Search/Solr/ConditionalFilterListenerTest.php
index 6ace272cd44d3254d64358e45e4e27e637953994..e6ddeacf7d3281bf748900726086aeb7cb6099a2 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/Search/Solr/ConditionalFilterListenerTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Search/Solr/ConditionalFilterListenerTest.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Search/Solr/FilterFieldConversionListenerTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Search/Solr/FilterFieldConversionListenerTest.php
index 5be83bac4affdc7e4239a7effd46b470e8f39f1e..06653b6552644267d424876b387a14f2bb98b413 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/Search/Solr/FilterFieldConversionListenerTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Search/Solr/FilterFieldConversionListenerTest.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Search/Solr/HideFacetValueListenerTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Search/Solr/HideFacetValueListenerTest.php
index d0041fbca9376d907c5e6499a67b85e5ab9ef88f..c9cca9fa82968b07d8eb7bc1584342d27baa1270 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/Search/Solr/HideFacetValueListenerTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Search/Solr/HideFacetValueListenerTest.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Search/Solr/HierarchicalFacetHelperTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Search/Solr/HierarchicalFacetHelperTest.php
index 4b6e1d295d544be4c2fcf472a471b37149dd12a6..741d0226bcfda527175c7fa2cdf1219b950c877a 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/Search/Solr/HierarchicalFacetHelperTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Search/Solr/HierarchicalFacetHelperTest.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Search/Solr/MultiIndexListenerTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Search/Solr/MultiIndexListenerTest.php
index 49b8b62eaee3b15a9115e12bffcdaf9e4a5e9a4a..86f245d869945f71dddf902a5b8dff8bb012bb3a 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/Search/Solr/MultiIndexListenerTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Search/Solr/MultiIndexListenerTest.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Search/Solr/SpellingProcessorTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Search/Solr/SpellingProcessorTest.php
index a72e4ab9c710bc00762acdc11475a2a7ebac94ef..a639d2f350b432422084e80e0bf08215e8cc737c 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/Search/Solr/SpellingProcessorTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Search/Solr/SpellingProcessorTest.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Search/Solr/V3/ErrorListenerTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Search/Solr/V3/ErrorListenerTest.php
index 047827a646c2a6adad3627381519ebecfb34b7d1..6e8920b52ac7ec1d3afa1a6d8142eb611c21ac73 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/Search/Solr/V3/ErrorListenerTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Search/Solr/V3/ErrorListenerTest.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Search/Solr/V4/ErrorListenerTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Search/Solr/V4/ErrorListenerTest.php
index 5167f893c87cd594dfee5b0cfdfbf47578116e28..b50ffea986fbeb13c8c71a5c75f50008610c0537 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/Search/Solr/V4/ErrorListenerTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Search/Solr/V4/ErrorListenerTest.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Session/PluginManagerTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Session/PluginManagerTest.php
index 7116e47a86738096581c7539a9172ae541189b27..0fb4100a32d354a3d942b0aaf671b80d4e9c7994 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/Session/PluginManagerTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Session/PluginManagerTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/SimpleXMLTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/SimpleXMLTest.php
index 749b69b4e6e5d279d6afead9704513002102fba8..65b8f4c14450dd530948847ec6c989ca06509570 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/SimpleXMLTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/SimpleXMLTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Sitemap/SitemapIndexTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Sitemap/SitemapIndexTest.php
index bda34edab100d8f1fbd7809b6f347b6a030d2eef..75382ef7628f02e9ccf9d852ebffd224a6ef4b02 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/Sitemap/SitemapIndexTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Sitemap/SitemapIndexTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Sitemap/SitemapTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Sitemap/SitemapTest.php
index 055d7865d847fe56acaff9fd3b2d45f0c0947f25..ec24d5490fa9fd5821528d8f2c717d66704f8393 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/Sitemap/SitemapTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Sitemap/SitemapTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Solr/UtilsTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Solr/UtilsTest.php
index f304c92ff24b207f6da17e9f1b02d8dc01b0d26a..139169c31c03f2940946d5e93ed85021bab756fe 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/Solr/UtilsTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Solr/UtilsTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Solr/WriterTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Solr/WriterTest.php
index 3992131f47b36056e4e3e9d42aa64d0ef98b648a..3ee5363536499ea7991cced454a0304fa975c940 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/Solr/WriterTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Solr/WriterTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Statistics/Driver/PluginManagerTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Statistics/Driver/PluginManagerTest.php
index c3b8b88b6280b723f2b38bbb75e3b13bcb6e0ba1..d7637770909c9cdef42193a2ec86b83d6ec64181 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/Statistics/Driver/PluginManagerTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Statistics/Driver/PluginManagerTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/TagsTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/TagsTest.php
index e826e3557379b1cdd3572b1bef84a41882329013..b630d3f3402c54f319706a3ff554a770687db265 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/TagsTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/TagsTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/View/Helper/Root/CartTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/View/Helper/Root/CartTest.php
index 87940a4b111c62e8404ccbde41d9311c2178cce5..63ce5593b3229d34cf0d5b9aabe769b19b858686 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/View/Helper/Root/CartTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/View/Helper/Root/CartTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/View/Helper/Root/CitationTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/View/Helper/Root/CitationTest.php
index 606c07291745c7603ea70c83ec8c60f8bc510d5f..f88d76f240be2f62e5edca3221075e3b5d99f89c 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/View/Helper/Root/CitationTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/View/Helper/Root/CitationTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/View/Helper/Root/GoogleAnalyticsTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/View/Helper/Root/GoogleAnalyticsTest.php
index 5a69b7207f708424174dfe2d098bd0a76a74defe..4281c00141d2a22953078ed44ed7fec776787de1 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/View/Helper/Root/GoogleAnalyticsTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/View/Helper/Root/GoogleAnalyticsTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/View/Helper/Root/OpenUrlTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/View/Helper/Root/OpenUrlTest.php
index f0a1a6f1e0664adca04fae3888aba053fcffef36..6cd82aec1ce42b5021028cb4bfc6e7f974fef4ec 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/View/Helper/Root/OpenUrlTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/View/Helper/Root/OpenUrlTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/View/Helper/Root/RecordTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/View/Helper/Root/RecordTest.php
index 371744574c254a9e5a11a3799732e498b5b26ba2..32434ba4980ac5431759f638493f3ce6406512ca 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/View/Helper/Root/RecordTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/View/Helper/Root/RecordTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
@@ -207,23 +207,6 @@ class RecordTest extends \PHPUnit_Framework_TestCase
         );
     }
 
-    /**
-     * Test getController.
-     *
-     * @return void
-     */
-    public function testGetController()
-    {
-        // Default (Solr) case:
-        $driver = new \VuFindTest\RecordDriver\TestHarness();
-        $record = $this->getRecord($driver);
-        $this->assertEquals('Record', $record->getController());
-
-        // Custom source case:
-        $driver->setSourceIdentifier('Foo');
-        $this->assertEquals('Foorecord', $record->getController());
-    }
-
     /**
      * Test getPreviews.
      *
@@ -401,7 +384,7 @@ class RecordTest extends \PHPUnit_Framework_TestCase
         // Hard-coded thumbnail:
         $driver = new \VuFindTest\RecordDriver\TestHarness();
         $driver->setRawData(['Thumbnail' => ['bar' => 'baz']]);
-        $record = $this->getRecord($driver, [], null, 'cover-show');
+        $record = $this->getRecord($driver);
         $this->assertEquals('http://foo/bar?bar=baz', $record->getThumbnail());
     }
 
@@ -535,6 +518,7 @@ class RecordTest extends \PHPUnit_Framework_TestCase
             ->will($this->returnValue($this->getMockResolver()));
         $config = is_array($config) ? new \Zend\Config\Config($config) : $config;
         $record = new Record($config);
+        $record->setCoverRouter(new \VuFind\Cover\Router('http://foo/bar'));
         $record->setView($view);
         return $record->__invoke($driver);
     }
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/View/Helper/Root/SafeMoneyFormatTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/View/Helper/Root/SafeMoneyFormatTest.php
index 877ecf7c92ec1f3c7e66ce98dca617c5abc5f982..afda8fa117bcbfbb5f882fa848e4552a95ae951e 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/View/Helper/Root/SafeMoneyFormatTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/View/Helper/Root/SafeMoneyFormatTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/View/Helper/Root/TranslateTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/View/Helper/Root/TranslateTest.php
index 495e4d75c19a462b836f847dbda10020a7ae0ee1..466e5067de2a4139fcac7d4e86ba01874d6779f2 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/View/Helper/Root/TranslateTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/View/Helper/Root/TranslateTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/View/Helper/jQueryMobile/MobileMenuTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/View/Helper/jQueryMobile/MobileMenuTest.php
index 294c8c6bd33ff71c5b8bc6aa6cd6613f22f7eaa3..13af03b02b3958abb13d46c69879fe9231f55bb9 100644
--- a/module/VuFind/tests/unit-tests/src/VuFindTest/View/Helper/jQueryMobile/MobileMenuTest.php
+++ b/module/VuFind/tests/unit-tests/src/VuFindTest/View/Helper/jQueryMobile/MobileMenuTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFind/xsl/record-marc.xsl b/module/VuFind/xsl/record-marc.xsl
index ea4ae78dbf0e9c16217b3f44246462ac0bcdb4e0..1ac1d3afb021ba9d39e6efe4c1dce995e13840f9 100644
--- a/module/VuFind/xsl/record-marc.xsl
+++ b/module/VuFind/xsl/record-marc.xsl
@@ -16,7 +16,7 @@
           <td width="5%"/>
           <td width="*"/>
         </tr>
-        <tr>
+        <tr class="marc-row-LEADER">
           <th>LEADER</th>
           <td colspan="3"><xsl:value-of select="//marc:leader"/></td>
         </tr>
@@ -25,7 +25,7 @@
   </xsl:template>
 
   <xsl:template match="//marc:controlfield">
-      <tr>
+      <tr class="marc-row-{@tag}">
         <th>
           <xsl:value-of select="@tag"/>
         </th>
@@ -34,7 +34,7 @@
   </xsl:template>
 
   <xsl:template match="//marc:datafield">
-      <tr>
+      <tr class="marc-row-{@tag}">
         <th>
           <xsl:value-of select="@tag"/>
         </th>
diff --git a/module/VuFindAdmin/Module.php b/module/VuFindAdmin/Module.php
index 127a8a4565a077d5a23e52a87a2deea3ed004970..91d6e58637425f8103942a95e09bb68304d00776 100644
--- a/module/VuFindAdmin/Module.php
+++ b/module/VuFindAdmin/Module.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Module
diff --git a/module/VuFindAdmin/src/VuFindAdmin/Controller/AbstractAdmin.php b/module/VuFindAdmin/src/VuFindAdmin/Controller/AbstractAdmin.php
index f3cda8fdd0c002a74570568c5ba1f4ba4bd4ce13..af23318cc2dcb67ba4f93fe60bd97f8d94076f9c 100644
--- a/module/VuFindAdmin/src/VuFindAdmin/Controller/AbstractAdmin.php
+++ b/module/VuFindAdmin/src/VuFindAdmin/Controller/AbstractAdmin.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Controller
diff --git a/module/VuFindAdmin/src/VuFindAdmin/Controller/AdminController.php b/module/VuFindAdmin/src/VuFindAdmin/Controller/AdminController.php
index 7e48bc5b447b610b48c529c4d3f84fc91778a5b3..6a92e21145f2a3fc23076e5fe828327625ceb83a 100644
--- a/module/VuFindAdmin/src/VuFindAdmin/Controller/AdminController.php
+++ b/module/VuFindAdmin/src/VuFindAdmin/Controller/AdminController.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Controller
diff --git a/module/VuFindAdmin/src/VuFindAdmin/Controller/ConfigController.php b/module/VuFindAdmin/src/VuFindAdmin/Controller/ConfigController.php
index 5dd13c68c5235aa81c1440f0e85f3477c6f89974..0640ecafc9f7dfabc34118e444a12db30d335cb1 100644
--- a/module/VuFindAdmin/src/VuFindAdmin/Controller/ConfigController.php
+++ b/module/VuFindAdmin/src/VuFindAdmin/Controller/ConfigController.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Controller
diff --git a/module/VuFindAdmin/src/VuFindAdmin/Controller/MaintenanceController.php b/module/VuFindAdmin/src/VuFindAdmin/Controller/MaintenanceController.php
index d02a9af441171a6f22a7944d47dd11a5f3969be9..672c3bfb0b0ca7b97d276d3678a7b6a6104d6c0d 100644
--- a/module/VuFindAdmin/src/VuFindAdmin/Controller/MaintenanceController.php
+++ b/module/VuFindAdmin/src/VuFindAdmin/Controller/MaintenanceController.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Controller
diff --git a/module/VuFindAdmin/src/VuFindAdmin/Controller/SocialstatsController.php b/module/VuFindAdmin/src/VuFindAdmin/Controller/SocialstatsController.php
index 86449a89bca69ff9fa69c6414ddb66cc94735590..067ee3bd2fa6989c162facabc55c44ec0996918d 100644
--- a/module/VuFindAdmin/src/VuFindAdmin/Controller/SocialstatsController.php
+++ b/module/VuFindAdmin/src/VuFindAdmin/Controller/SocialstatsController.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Controller
diff --git a/module/VuFindAdmin/src/VuFindAdmin/Controller/StatisticsController.php b/module/VuFindAdmin/src/VuFindAdmin/Controller/StatisticsController.php
index 113a13f499a29e784daa5e979217b4e018cb87ca..676923e2d8f872eeeb47ed904524641d13e610d7 100644
--- a/module/VuFindAdmin/src/VuFindAdmin/Controller/StatisticsController.php
+++ b/module/VuFindAdmin/src/VuFindAdmin/Controller/StatisticsController.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Controller
diff --git a/module/VuFindAdmin/src/VuFindAdmin/Controller/TagsController.php b/module/VuFindAdmin/src/VuFindAdmin/Controller/TagsController.php
index 74fc67722295d5b8907796a1d3874f976580e43c..b5e26167315369dda30dd12c7f743daf4008daeb 100644
--- a/module/VuFindAdmin/src/VuFindAdmin/Controller/TagsController.php
+++ b/module/VuFindAdmin/src/VuFindAdmin/Controller/TagsController.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Controller
diff --git a/module/VuFindAdmin/tests/unit-tests/src/VuFindTest/Controller/SocialstatsControllerTest.php b/module/VuFindAdmin/tests/unit-tests/src/VuFindTest/Controller/SocialstatsControllerTest.php
index b26812ccad996cacffff7b134892a0ed6bc4a757..182ecd0f1c3e31f71e4d87daa8a34e458a776302 100644
--- a/module/VuFindAdmin/tests/unit-tests/src/VuFindTest/Controller/SocialstatsControllerTest.php
+++ b/module/VuFindAdmin/tests/unit-tests/src/VuFindTest/Controller/SocialstatsControllerTest.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFindConsole/Module.php b/module/VuFindConsole/Module.php
index 8b21d20f506d6d81170362eff5b1d0539b57883d..016f39db18ab8eb9e4ccf0f386a0754a1c2dc693 100644
--- a/module/VuFindConsole/Module.php
+++ b/module/VuFindConsole/Module.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Module
diff --git a/module/VuFindConsole/src/VuFindConsole/Controller/AbstractBase.php b/module/VuFindConsole/src/VuFindConsole/Controller/AbstractBase.php
index 022e32a2d7b0b56df7cbd21398c155c1f079066c..dfc34ed0c95f40eb354571135a06504004d8bd7d 100644
--- a/module/VuFindConsole/src/VuFindConsole/Controller/AbstractBase.php
+++ b/module/VuFindConsole/src/VuFindConsole/Controller/AbstractBase.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Controller
diff --git a/module/VuFindConsole/src/VuFindConsole/Controller/GenerateController.php b/module/VuFindConsole/src/VuFindConsole/Controller/GenerateController.php
index f55b45e42441df5c4cb5514cd2f2986a34c21ca6..7058b9800231de3e57dc622c56269ed6b01b6956 100644
--- a/module/VuFindConsole/src/VuFindConsole/Controller/GenerateController.php
+++ b/module/VuFindConsole/src/VuFindConsole/Controller/GenerateController.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Controller
@@ -194,14 +194,14 @@ class GenerateController extends AbstractBase
         $mainConfig = $this->getServiceLocator()->get('Config');
         foreach ($mainConfig['router']['routes'] as $key => $val) {
             if (isset($val['options']['route'])
-                && substr($val['options']['route'], -12) == '[:id[/:tab]]'
+                && substr($val['options']['route'], -14) == '[:id[/[:tab]]]'
             ) {
                 $newRoute = $key . '-' . strtolower($action);
                 if (isset($mainConfig['router']['routes'][$newRoute])) {
                     Console::writeLine($newRoute . ' already exists; skipping.');
                 } else {
                     $val['options']['route'] = str_replace(
-                        '[:id[/:tab]]', "[:id]/$action", $val['options']['route']
+                        '[:id[/[:tab]]]', "[:id]/$action", $val['options']['route']
                     );
                     $val['options']['defaults']['action'] = $action;
                     $config['router']['routes'][$newRoute] = $val;
diff --git a/module/VuFindConsole/src/VuFindConsole/Controller/HarvestController.php b/module/VuFindConsole/src/VuFindConsole/Controller/HarvestController.php
index 9aeefc2dcc92a96d6b4edc1f5fe27e60087cc0ef..bddc17f97346d2b3bb380c72cecb5c6043c843bf 100644
--- a/module/VuFindConsole/src/VuFindConsole/Controller/HarvestController.php
+++ b/module/VuFindConsole/src/VuFindConsole/Controller/HarvestController.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Controller
@@ -26,7 +26,7 @@
  * @link     https://vufind.org/wiki/development:plugins:controllers Wiki
  */
 namespace VuFindConsole\Controller;
-use VuFind\Harvester\OAI, Zend\Console\Console;
+use VuFindHarvest\OaiPmh\HarvesterConsoleRunner, Zend\Console\Console;
 
 /**
  * This controller handles various command-line tools
@@ -39,6 +39,31 @@ use VuFind\Harvester\OAI, Zend\Console\Console;
  */
 class HarvestController extends AbstractBase
 {
+    /**
+     * Get the base directory for harvesting OAI-PMH data.
+     *
+     * @return string
+     */
+    protected function getHarvestRoot()
+    {
+        // Get the base VuFind path:
+        if (strlen(LOCAL_OVERRIDE_DIR) > 0) {
+            $home = LOCAL_OVERRIDE_DIR;
+        } else {
+            $home = realpath(APPLICATION_PATH . '/..');
+        }
+
+        // Build the full harvest path:
+        $dir = $home . '/harvest/';
+
+        // Create the directory if it does not already exist:
+        if (!is_dir($dir) && !mkdir($dir)) {
+            throw new \Exception("Problem creating directory {$dir}.");
+        }
+
+        return $dir;
+    }
+
     /**
      * Harvest OAI-PMH records.
      *
@@ -48,56 +73,22 @@ class HarvestController extends AbstractBase
     {
         $this->checkLocalSetting();
 
-        // Parse switches:
-        $this->consoleOpts->addRules(
-            ['from-s' => 'Harvest start date', 'until-s' => 'Harvest end date']
-        );
-        $from = $this->consoleOpts->getOption('from');
-        $until = $this->consoleOpts->getOption('until');
-
-        // Read Config files
-        $configFile = \VuFind\Config\Locator::getConfigPath('oai.ini', 'harvest');
-        $oaiSettings = @parse_ini_file($configFile, true);
-        if (empty($oaiSettings)) {
-            Console::writeLine("Please add OAI-PMH settings to oai.ini.");
-            return $this->getFailureResponse();
-        }
-
-        // If first command line parameter is set, see if we can limit to just the
-        // specified OAI harvester:
-        $argv = $this->consoleOpts->getRemainingArgs();
-        if (isset($argv[0])) {
-            if (isset($oaiSettings[$argv[0]])) {
-                $oaiSettings = [$argv[0] => $oaiSettings[$argv[0]]];
-            } else {
-                Console::writeLine("Could not load settings for {$argv[0]}.");
-                return $this->getFailureResponse();
-            }
+        // Get default options, add the default --ini setting if missing:
+        $opts = HarvesterConsoleRunner::getDefaultOptions();
+        if (!$opts->getOption('ini')) {
+            $ini = \VuFind\Config\Locator::getConfigPath('oai.ini', 'harvest');
+            $opts->addArguments(['--ini=' . $ini]);
         }
 
-        // Loop through all the settings and perform harvests:
-        $processed = 0;
-        foreach ($oaiSettings as $target => $settings) {
-            if (!empty($target) && !empty($settings)) {
-                Console::writeLine("Processing {$target}...");
-                try {
-                    $client = $this->getServiceLocator()->get('VuFind\Http')
-                        ->createClient();
-                    $harvest = new OAI($target, $settings, $client, $from, $until);
-                    $harvest->launch();
-                } catch (\Exception $e) {
-                    Console::writeLine($e->getMessage());
-                    return $this->getFailureResponse();
-                }
-                $processed++;
-            }
-        }
+        // Get the default VuFind HTTP client:
+        $client = $this->getServiceLocator()->get('VuFind\Http')->createClient();
 
-        // All done.
-        Console::writeLine(
-            "Completed without errors -- {$processed} source(s) processed."
+        // Run the job!
+        $runner = new HarvesterConsoleRunner(
+            $opts, $client, $this->getHarvestRoot()
         );
-        return $this->getSuccessResponse();
+        return $runner->run()
+            ? $this->getSuccessResponse() : $this->getFailureResponse();
     }
 
     /**
diff --git a/module/VuFindConsole/src/VuFindConsole/Controller/ImportController.php b/module/VuFindConsole/src/VuFindConsole/Controller/ImportController.php
index 3637db6ebf132a6e1c6363960ab06da4c847f587..91a9473e313810d4c1f4145063c03f383699cbf0 100644
--- a/module/VuFindConsole/src/VuFindConsole/Controller/ImportController.php
+++ b/module/VuFindConsole/src/VuFindConsole/Controller/ImportController.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Controller
@@ -88,8 +88,7 @@ class ImportController extends AbstractBase
             );
             Console::writeLine("");
             Console::writeLine(
-                "Note: See vudl.properties and ojs.properties "
-                . "for configuration examples."
+                "Note: See ojs.properties for configuration examples."
             );
             return $this->getFailureResponse();
         }
diff --git a/module/VuFindConsole/src/VuFindConsole/Controller/LanguageController.php b/module/VuFindConsole/src/VuFindConsole/Controller/LanguageController.php
index a2dad4ed8612316ccd098e35424a97bf381a1ffe..ba600720d038d1b4e3ddcc96251caeac96029bc6 100644
--- a/module/VuFindConsole/src/VuFindConsole/Controller/LanguageController.php
+++ b/module/VuFindConsole/src/VuFindConsole/Controller/LanguageController.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Controller
diff --git a/module/VuFindConsole/src/VuFindConsole/Controller/UtilController.php b/module/VuFindConsole/src/VuFindConsole/Controller/UtilController.php
index 65ec5dfab5f8211deb1ba87247b2770834bd287c..8611b72c1f8b11af77901dd6a06b99f7c4d7f9dc 100644
--- a/module/VuFindConsole/src/VuFindConsole/Controller/UtilController.php
+++ b/module/VuFindConsole/src/VuFindConsole/Controller/UtilController.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Controller
@@ -453,6 +453,39 @@ class UtilController extends AbstractBase
         return $this->getSuccessResponse();
     }
 
+    /**
+     * Display help for the search or session expiration actions
+     *
+     * @param string $rows Plural name of records to delete
+     *
+     * @return \Zend\Console\Response
+     */
+    protected function expirationHelp($rows)
+    {
+        Console::writeLine("Expire old $rows in the database.");
+        Console::writeLine('');
+        Console::writeLine(
+            'Optional parameters: [--batch=size] [--sleep=time] [age]'
+        );
+        Console::writeLine('');
+        Console::writeLine(
+            '  batch: number of records to delete in a single batch'
+            . ' (default 1000)'
+        );
+        Console::writeLine(
+            '  sleep: milliseconds to sleep between batches (default 100)'
+        );
+
+        Console::writeLine(
+            "  age: the age (in days) of $rows to expire (default 2)"
+        );
+        Console::writeLine('');
+        Console::writeLine(
+            "By default, $rows more than 2 days old will be removed."
+        );
+        return $this->getFailureResponse();
+    }
+
     /**
      * Command-line tool to clear unwanted entries
      * from search history database table.
@@ -464,21 +497,13 @@ class UtilController extends AbstractBase
         $this->consoleOpts->addRules(
             [
                 'h|help' => 'Get help',
+                'batch=i' => 'Batch size',
+                'sleep=i' => 'Sleep interval between batches'
             ]
         );
 
-        if ($this->consoleOpts->getOption('h')
-            || $this->consoleOpts->getOption('help')
-        ) {
-            Console::writeLine('Expire old searches in the database.');
-            Console::writeLine('');
-            Console::writeLine(
-                'Optional parameter: the age (in days) of searches to expire;'
-            );
-            Console::writeLine(
-                'by default, searches more than 2 days old will be removed.'
-            );
-            return $this->getFailureResponse();
+        if ($this->consoleOpts->getOption('h')) {
+            return $this->expirationHelp('searches');
         }
 
         return $this->expire(
@@ -499,21 +524,13 @@ class UtilController extends AbstractBase
         $this->consoleOpts->addRules(
             [
                 'h|help' => 'Get help',
+                'batch=i' => 'Batch size',
+                'sleep=i' => 'Sleep interval between batches'
             ]
         );
 
-        if ($this->consoleOpts->getOption('h')
-            || $this->consoleOpts->getOption('help')
-        ) {
-            Console::writeLine('Expire old sessions in the database.');
-            Console::writeLine('');
-            Console::writeLine(
-                'Optional parameter: the age (in days) of sessions to expire;'
-            );
-            Console::writeLine(
-                'by default, sessions more than 2 days old will be removed.'
-            );
-            return $this->getFailureResponse();
+        if ($this->consoleOpts->getOption('h')) {
+            return $this->expirationHelp('sessions');
         }
 
         return $this->expire(
@@ -797,7 +814,7 @@ class UtilController extends AbstractBase
     /**
      * Abstract delete method.
      *
-     * @param string $table         Table to operate on.
+     * @param string $tableName     Table to operate on.
      * @param string $successString String for reporting success.
      * @param string $failString    String for reporting failure.
      * @param int    $minAge        Minimum age allowed for expiration (also used
@@ -805,7 +822,7 @@ class UtilController extends AbstractBase
      *
      * @return mixed
      */
-    protected function expire($table, $successString, $failString, $minAge = 2)
+    protected function expire($tableName, $successString, $failString, $minAge = 2)
     {
         // Get command-line arguments
         $argv = $this->consoleOpts->getRemainingArgs();
@@ -813,6 +830,11 @@ class UtilController extends AbstractBase
         // Use command line value as expiration age, or default to $minAge.
         $daysOld = isset($argv[0]) ? intval($argv[0]) : $minAge;
 
+        // Use command line values for batch size and sleep time if specified.
+        $options = $this->consoleOpts->getArguments();
+        $batchSize = isset($options['batch']) ? $options['batch'] : 1000;
+        $sleepTime = isset($options['sleep']) ? $options['sleep'] : 100;
+
         // Abort if we have an invalid expiration age.
         if ($daysOld < 2) {
             Console::writeLine(
@@ -824,23 +846,51 @@ class UtilController extends AbstractBase
             return $this->getFailureResponse();
         }
 
-        // Delete the expired searches--this cleans up any junk left in the database
-        // from old search histories that were not
-        // caught by the session garbage collector.
-        $search = $this->getTable($table);
-        if (!method_exists($search, 'getExpiredQuery')) {
-            throw new \Exception($table . ' does not support getExpiredQuery()');
+        // Delete the expired rows--this cleans up any junk left in the database
+        // e.g. from old searches or sessions that were not caught by the session
+        // garbage collector.
+        $table = $this->getTable($tableName);
+        if (!method_exists($table, 'getExpiredIdRange')) {
+            throw new \Exception("$tableName does not support getExpiredIdRange()");
         }
-        $query = $search->getExpiredQuery($daysOld);
-        if (($count = count($search->select($query))) == 0) {
-            Console::writeLine($failString);
+        if (!method_exists($table, 'deleteExpired')) {
+            throw new \Exception("$tableName does not support deleteExpired()");
+        }
+
+        $idRange = $table->getExpiredIdRange($daysOld);
+        if (false === $idRange) {
+            $this->timestampedMessage($failString);
             return $this->getSuccessResponse();
         }
-        $search->delete($query);
-        Console::writeLine(str_replace('%%count%%', $count, $successString));
+
+        // Delete records in batches
+        for ($batch = $idRange[0]; $batch <= $idRange[1]; $batch += $batchSize) {
+            $count = $table->deleteExpired(
+                $daysOld, $batch, $batch + $batchSize - 1
+            );
+            $this->timestampedMessage(
+                str_replace('%%count%%', $count, $successString)
+            );
+            // Be nice to others and wait between batches
+            if ($batch + $batchSize <= $idRange[1]) {
+                usleep($sleepTime * 1000);
+            }
+        }
         return $this->getSuccessResponse();
     }
 
+    /**
+     * Print a message with a time stamp to the console
+     *
+     * @param string $msg Message
+     *
+     * @return void
+     */
+    protected function timestampedMessage($msg)
+    {
+        Console::writeLine('[' . date('Y-m-d H:i:s') . '] ' . $msg);
+    }
+
     /**
      * Convert hash algorithms
      * Expected parameters: oldmethod:oldkey (or none) newmethod:newkey
diff --git a/module/VuFindConsole/src/VuFindConsole/Mvc/Router/ConsoleRouter.php b/module/VuFindConsole/src/VuFindConsole/Mvc/Router/ConsoleRouter.php
index a67eb6ff78933184b3f34f469b2f74e0496859ae..a1503295c2f43cc279388cccadf6ee171a08dc73 100644
--- a/module/VuFindConsole/src/VuFindConsole/Mvc/Router/ConsoleRouter.php
+++ b/module/VuFindConsole/src/VuFindConsole/Mvc/Router/ConsoleRouter.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Mvc_Router
diff --git a/module/VuFindConsole/tests/unit-tests/src/VuFindTest/Mvc/Router/ConsoleRouterTest.php b/module/VuFindConsole/tests/unit-tests/src/VuFindTest/Mvc/Router/ConsoleRouterTest.php
index 60fa9f717fd664371d62e1a9fc3147ca8197f20f..34dac835f5d7a461dab995b924e15ae416e6ba18 100644
--- a/module/VuFindConsole/tests/unit-tests/src/VuFindTest/Mvc/Router/ConsoleRouterTest.php
+++ b/module/VuFindConsole/tests/unit-tests/src/VuFindTest/Mvc/Router/ConsoleRouterTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFindDevTools/Module.php b/module/VuFindDevTools/Module.php
index 0bc2dcda3ef3c53effc33edfb00e0d30c096718c..377e9494b75d29e2e8f3a3871f7b22951f876b75 100644
--- a/module/VuFindDevTools/Module.php
+++ b/module/VuFindDevTools/Module.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Module
diff --git a/module/VuFindDevTools/tests/unit-tests/src/VuFindTest/Controller/DevtoolsControllerTest.php b/module/VuFindDevTools/tests/unit-tests/src/VuFindTest/Controller/DevtoolsControllerTest.php
index 2b3b7e7062b5d808b4649f07431d7260c9abbd1c..292f92dbb5ac8125f974484b6aa053a28e645d3e 100644
--- a/module/VuFindDevTools/tests/unit-tests/src/VuFindTest/Controller/DevtoolsControllerTest.php
+++ b/module/VuFindDevTools/tests/unit-tests/src/VuFindTest/Controller/DevtoolsControllerTest.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFindDevTools/tests/unit-tests/src/VuFindTest/LanguageHelperTest.php b/module/VuFindDevTools/tests/unit-tests/src/VuFindTest/LanguageHelperTest.php
index 3db6b8576ba02ef2105b5e2496d91f9f3f5c7e2d..f64aa53e340e674993056e87731eb3a2fc0af8ea 100644
--- a/module/VuFindDevTools/tests/unit-tests/src/VuFindTest/LanguageHelperTest.php
+++ b/module/VuFindDevTools/tests/unit-tests/src/VuFindTest/LanguageHelperTest.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFindLocalTemplate/Module.php b/module/VuFindLocalTemplate/Module.php
index 7395af8ad14378d033355210a9587519c7295c86..10afcf2716bb94c193ac47cb8ad1f9acacecd56c 100644
--- a/module/VuFindLocalTemplate/Module.php
+++ b/module/VuFindLocalTemplate/Module.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Module
diff --git a/module/VuFindSearch/Module.php b/module/VuFindSearch/Module.php
index a890e39dc247a553215916f46117d32bafbd197c..819807ca6e57f01cd4af32c88ce577cc17860757 100644
--- a/module/VuFindSearch/Module.php
+++ b/module/VuFindSearch/Module.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
@@ -46,7 +46,7 @@ namespace VuFindSearch;
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFindSearch/src/VuFindSearch/Backend/AbstractBackend.php b/module/VuFindSearch/src/VuFindSearch/Backend/AbstractBackend.php
index 12cdfa57404445bfff3cc32c9e56040870dbbf7b..8a9961464ba835e350f806ac6ee2cbd053ab639b 100644
--- a/module/VuFindSearch/src/VuFindSearch/Backend/AbstractBackend.php
+++ b/module/VuFindSearch/src/VuFindSearch/Backend/AbstractBackend.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFindSearch/src/VuFindSearch/Backend/AbstractHandlerMap.php b/module/VuFindSearch/src/VuFindSearch/Backend/AbstractHandlerMap.php
index 6eb6562354fda0862773bd87eb0230518ae41965..627b3ec10b6005848c58c5b9dd9a87500cb08a43 100644
--- a/module/VuFindSearch/src/VuFindSearch/Backend/AbstractHandlerMap.php
+++ b/module/VuFindSearch/src/VuFindSearch/Backend/AbstractHandlerMap.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFindSearch/src/VuFindSearch/Backend/BackendInterface.php b/module/VuFindSearch/src/VuFindSearch/Backend/BackendInterface.php
index 5089e55ade514eb78084cee8d8a966bad2bea0fd..64d0df1e74fce1eba027cdb6011a4c2b2f349dbe 100644
--- a/module/VuFindSearch/src/VuFindSearch/Backend/BackendInterface.php
+++ b/module/VuFindSearch/src/VuFindSearch/Backend/BackendInterface.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
@@ -62,8 +62,8 @@ interface BackendInterface
      * Perform a search and return record collection.
      *
      * @param AbstractQuery $query  Search query
-     * @param integer       $offset Search offset
-     * @param integer       $limit  Search limit
+     * @param int           $offset Search offset
+     * @param int           $limit  Search limit
      * @param ParamBag      $params Search backend parameters
      *
      * @return \VuFindSearch\Response\RecordCollectionInterface
diff --git a/module/VuFindSearch/src/VuFindSearch/Backend/EDS/Backend.php b/module/VuFindSearch/src/VuFindSearch/Backend/EDS/Backend.php
index cd107a545106eb3fedafcb0d2b9936038736518e..0c739c570dac853068ac1917fb1d0a4d5850699c 100644
--- a/module/VuFindSearch/src/VuFindSearch/Backend/EDS/Backend.php
+++ b/module/VuFindSearch/src/VuFindSearch/Backend/EDS/Backend.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
@@ -102,7 +102,7 @@ class Backend extends AbstractBackend
     /**
      * Whether or not to use IP Authentication for communication with the EDS API
      *
-     * @var boolean
+     * @var bool
      */
     protected $ipAuth = false;
 
@@ -187,8 +187,8 @@ class Backend extends AbstractBackend
      * Perform a search and return record collection.
      *
      * @param AbstractQuery $query  Search query
-     * @param integer       $offset Search offset
-     * @param integer       $limit  Search limit
+     * @param int           $offset Search offset
+     * @param int           $limit  Search limit
      * @param ParamBag      $params Search backend parameters
      *
      *@return \VuFindSearch\Response\RecordCollectionInterface
@@ -468,7 +468,7 @@ class Backend extends AbstractBackend
      * Obtain the session token from the Session container. If it doesn't exist,
      * generate a new one.
      *
-     * @param boolean $isInvalid If a session token is invalid, generate a new one
+     * @param bool $isInvalid If a session token is invalid, generate a new one
      * regardless of what is in the session container
      *
      * @return string
diff --git a/module/VuFindSearch/src/VuFindSearch/Backend/EDS/Base.php b/module/VuFindSearch/src/VuFindSearch/Backend/EDS/Base.php
index 766edca587a2f0516050050f7983df48a22ae329..d4d10ad132fd977e5f12ad290a45d54f4818d1da 100644
--- a/module/VuFindSearch/src/VuFindSearch/Backend/EDS/Base.php
+++ b/module/VuFindSearch/src/VuFindSearch/Backend/EDS/Base.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category EBSCOIndustries
  * @package  EBSCO
diff --git a/module/VuFindSearch/src/VuFindSearch/Backend/EDS/Exception.php b/module/VuFindSearch/src/VuFindSearch/Backend/EDS/Exception.php
index 61bc5a3faf04b67fb9feeab558095e8c51c4188e..b347cae4e4f12b49d55811e97a41e1ef740ea29f 100644
--- a/module/VuFindSearch/src/VuFindSearch/Backend/EDS/Exception.php
+++ b/module/VuFindSearch/src/VuFindSearch/Backend/EDS/Exception.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category EBSCOIndustries
  * @package  EBSCO
diff --git a/module/VuFindSearch/src/VuFindSearch/Backend/EDS/QueryBuilder.php b/module/VuFindSearch/src/VuFindSearch/Backend/EDS/QueryBuilder.php
index ba859663a4cf8832c2a711d058f03e963539ec27..fb2611633e51b405a6cba3dc9fbc118ae582fac5 100644
--- a/module/VuFindSearch/src/VuFindSearch/Backend/EDS/QueryBuilder.php
+++ b/module/VuFindSearch/src/VuFindSearch/Backend/EDS/QueryBuilder.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFindSearch/src/VuFindSearch/Backend/EDS/Response/RecordCollection.php b/module/VuFindSearch/src/VuFindSearch/Backend/EDS/Response/RecordCollection.php
index b394f872ac28fe5d00308dc636092b99311e15c6..badf94d65a3eb612190c12285919ed39dd06f5bc 100644
--- a/module/VuFindSearch/src/VuFindSearch/Backend/EDS/Response/RecordCollection.php
+++ b/module/VuFindSearch/src/VuFindSearch/Backend/EDS/Response/RecordCollection.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFindSearch/src/VuFindSearch/Backend/EDS/Response/RecordCollectionFactory.php b/module/VuFindSearch/src/VuFindSearch/Backend/EDS/Response/RecordCollectionFactory.php
index bdbce2fe669c0dbef1de1575731276a6d3ee0f86..46b5b8eaae0965767cc4ce65c4029f4ebfe0a262 100644
--- a/module/VuFindSearch/src/VuFindSearch/Backend/EDS/Response/RecordCollectionFactory.php
+++ b/module/VuFindSearch/src/VuFindSearch/Backend/EDS/Response/RecordCollectionFactory.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFindSearch/src/VuFindSearch/Backend/EDS/SearchRequestModel.php b/module/VuFindSearch/src/VuFindSearch/Backend/EDS/SearchRequestModel.php
index 7e13efb325e67c02f7a1894c6e7d82536b5e2e97..7cbf63bc11a23cbbb118ab8d249f809f13c57938 100644
--- a/module/VuFindSearch/src/VuFindSearch/Backend/EDS/SearchRequestModel.php
+++ b/module/VuFindSearch/src/VuFindSearch/Backend/EDS/SearchRequestModel.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category EBSCOIndustries
  * @package  EBSCO
@@ -112,7 +112,7 @@ class SearchRequestModel
     /**
      * Whether or not to highlight the search term in the results.
      *
-     * @var boolean
+     * @var bool
      */
     protected $highlight;
 
@@ -271,7 +271,7 @@ class SearchRequestModel
      * @param string $valueToCheck    Value to check the ending characters of
      * @param string $valueToCheckFor Characters to check for
      *
-     * @return boolean
+     * @return bool
      */
     protected static function endsWith($valueToCheck, $valueToCheckFor)
     {
diff --git a/module/VuFindSearch/src/VuFindSearch/Backend/EDS/Zend2.php b/module/VuFindSearch/src/VuFindSearch/Backend/EDS/Zend2.php
index c047b1d24c92211ded03756f49057191da4a8b2a..899f01527f52e781f0bf4a32cef06a090421cc53 100644
--- a/module/VuFindSearch/src/VuFindSearch/Backend/EDS/Zend2.php
+++ b/module/VuFindSearch/src/VuFindSearch/Backend/EDS/Zend2.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category EBSCOIndustries
  * @package  EBSCO
@@ -47,6 +47,7 @@ class Zend2 extends EdsApi_REST_Base implements LoggerAwareInterface
 
      /**
      * The HTTP Request object to execute EDS API transactions
+      *
      * @var Zend2HttpClient
      */
     protected $client;
diff --git a/module/VuFindSearch/src/VuFindSearch/Backend/EIT/Backend.php b/module/VuFindSearch/src/VuFindSearch/Backend/EIT/Backend.php
index 2b905e280fe7c30e83983ad9265ebb0f27865ad2..3713b8b201a90a8222e6890ed483795134b8a22c 100644
--- a/module/VuFindSearch/src/VuFindSearch/Backend/EIT/Backend.php
+++ b/module/VuFindSearch/src/VuFindSearch/Backend/EIT/Backend.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
@@ -81,8 +81,8 @@ class Backend extends AbstractBackend
      * Perform a search and return record collection.
      *
      * @param AbstractQuery $query  Search query
-     * @param integer       $offset Search offset
-     * @param integer       $limit  Search limit
+     * @param int           $offset Search offset
+     * @param int           $limit  Search limit
      * @param ParamBag      $params Search backend parameters
      *
      * @return RecordCollectionInterface
diff --git a/module/VuFindSearch/src/VuFindSearch/Backend/EIT/Connector.php b/module/VuFindSearch/src/VuFindSearch/Backend/EIT/Connector.php
index e52f57ebf113081229fc0febfd3d47e2b54e1280..5c04d244518b1f3b8cbf3bec1a3f7d31fd43faf6 100644
--- a/module/VuFindSearch/src/VuFindSearch/Backend/EIT/Connector.php
+++ b/module/VuFindSearch/src/VuFindSearch/Backend/EIT/Connector.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Connection
@@ -103,8 +103,8 @@ class Connector implements \Zend\Log\LoggerAwareInterface
      * Execute a search.
      *
      * @param ParamBag $params Parameters
-     * @param integer  $offset Search offset
-     * @param integer  $limit  Search limit
+     * @param int      $offset Search offset
+     * @param int      $limit  Search limit
      *
      * @return array
      */
diff --git a/module/VuFindSearch/src/VuFindSearch/Backend/EIT/QueryBuilder.php b/module/VuFindSearch/src/VuFindSearch/Backend/EIT/QueryBuilder.php
index 37788362a1d8592c8136a6ebf7656c1d7d00ef10..af72d7e048be7ce415b496b453c5b630e9fcacc4 100644
--- a/module/VuFindSearch/src/VuFindSearch/Backend/EIT/QueryBuilder.php
+++ b/module/VuFindSearch/src/VuFindSearch/Backend/EIT/QueryBuilder.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFindSearch/src/VuFindSearch/Backend/EIT/Response/XML/RecordCollection.php b/module/VuFindSearch/src/VuFindSearch/Backend/EIT/Response/XML/RecordCollection.php
index 309a05386cce26abfdaa736e7930e87580d5f70b..27b7dd4a83dd073306008f4d443764517622a3a4 100644
--- a/module/VuFindSearch/src/VuFindSearch/Backend/EIT/Response/XML/RecordCollection.php
+++ b/module/VuFindSearch/src/VuFindSearch/Backend/EIT/Response/XML/RecordCollection.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFindSearch/src/VuFindSearch/Backend/EIT/Response/XML/RecordCollectionFactory.php b/module/VuFindSearch/src/VuFindSearch/Backend/EIT/Response/XML/RecordCollectionFactory.php
index e81b411e5eb425d99bbde45d702fb89a23b5cfe3..9c664eb06934c51d6cb000c99322470d69db845f 100644
--- a/module/VuFindSearch/src/VuFindSearch/Backend/EIT/Response/XML/RecordCollectionFactory.php
+++ b/module/VuFindSearch/src/VuFindSearch/Backend/EIT/Response/XML/RecordCollectionFactory.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFindSearch/src/VuFindSearch/Backend/Exception/BackendException.php b/module/VuFindSearch/src/VuFindSearch/Backend/Exception/BackendException.php
index 7fa816f6bf47602d1b75d484f52c4855da8d08cd..b4cc48f9c845eb55c1ecadaae40dd85bced61ca0 100644
--- a/module/VuFindSearch/src/VuFindSearch/Backend/Exception/BackendException.php
+++ b/module/VuFindSearch/src/VuFindSearch/Backend/Exception/BackendException.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
@@ -75,7 +75,7 @@ class BackendException extends RuntimeException
      *
      * @param string $tag Tag
      *
-     * @return boolean
+     * @return bool
      */
     public function hasTag($tag)
     {
diff --git a/module/VuFindSearch/src/VuFindSearch/Backend/Exception/HttpErrorException.php b/module/VuFindSearch/src/VuFindSearch/Backend/Exception/HttpErrorException.php
index 9848ff3e82ce57f20a346f4bd6fdbcc607037214..1ba4267818765822cf595d017ae1caa1cc840313 100644
--- a/module/VuFindSearch/src/VuFindSearch/Backend/Exception/HttpErrorException.php
+++ b/module/VuFindSearch/src/VuFindSearch/Backend/Exception/HttpErrorException.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFindSearch/src/VuFindSearch/Backend/Exception/RemoteErrorException.php b/module/VuFindSearch/src/VuFindSearch/Backend/Exception/RemoteErrorException.php
index 39ee5399706be2f577c7e884bb34ab684c74a469..4d3f0c60a40b04e224b6f7396de8eacd66b15ec1 100644
--- a/module/VuFindSearch/src/VuFindSearch/Backend/Exception/RemoteErrorException.php
+++ b/module/VuFindSearch/src/VuFindSearch/Backend/Exception/RemoteErrorException.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFindSearch/src/VuFindSearch/Backend/Exception/RequestErrorException.php b/module/VuFindSearch/src/VuFindSearch/Backend/Exception/RequestErrorException.php
index e35374509bc96c547ce75b34c04ff2e1d0c13cdb..e9aae62f7dfec1facfd98ef0853201305f05d56c 100644
--- a/module/VuFindSearch/src/VuFindSearch/Backend/Exception/RequestErrorException.php
+++ b/module/VuFindSearch/src/VuFindSearch/Backend/Exception/RequestErrorException.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFindSearch/src/VuFindSearch/Backend/LibGuides/Backend.php b/module/VuFindSearch/src/VuFindSearch/Backend/LibGuides/Backend.php
index e495718c04e141b3f196dd580b13cc2a20c0e302..f1a0dc992b49465fa1fd454db534898a175f475d 100644
--- a/module/VuFindSearch/src/VuFindSearch/Backend/LibGuides/Backend.php
+++ b/module/VuFindSearch/src/VuFindSearch/Backend/LibGuides/Backend.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
@@ -95,8 +95,8 @@ class Backend extends AbstractBackend
      * Perform a search and return record collection.
      *
      * @param AbstractQuery $query  Search query
-     * @param integer       $offset Search offset
-     * @param integer       $limit  Search limit
+     * @param int           $offset Search offset
+     * @param int           $limit  Search limit
      * @param ParamBag      $params Search backend parameters
      *
      * @return RecordCollectionInterface
diff --git a/module/VuFindSearch/src/VuFindSearch/Backend/LibGuides/Connector.php b/module/VuFindSearch/src/VuFindSearch/Backend/LibGuides/Connector.php
index 7c10749367c855d9623ce9565dccbb5d5f61e996..ed682f18ae74d32a556856bccb5639ce28d35bc1 100644
--- a/module/VuFindSearch/src/VuFindSearch/Backend/LibGuides/Connector.php
+++ b/module/VuFindSearch/src/VuFindSearch/Backend/LibGuides/Connector.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFindSearch/src/VuFindSearch/Backend/LibGuides/QueryBuilder.php b/module/VuFindSearch/src/VuFindSearch/Backend/LibGuides/QueryBuilder.php
index 458f8bce44d06e0c8a264032cbaf55b820712f8a..e2bffeefa86e6a1c1a1618099d0f0bd2d55eef93 100644
--- a/module/VuFindSearch/src/VuFindSearch/Backend/LibGuides/QueryBuilder.php
+++ b/module/VuFindSearch/src/VuFindSearch/Backend/LibGuides/QueryBuilder.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFindSearch/src/VuFindSearch/Backend/LibGuides/Response/RecordCollection.php b/module/VuFindSearch/src/VuFindSearch/Backend/LibGuides/Response/RecordCollection.php
index 09723998d1b34bdb5ff8c8f6bd8b64cff5fac936..45fe90e83539e4a91245853ca8cdd4fb59882156 100644
--- a/module/VuFindSearch/src/VuFindSearch/Backend/LibGuides/Response/RecordCollection.php
+++ b/module/VuFindSearch/src/VuFindSearch/Backend/LibGuides/Response/RecordCollection.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFindSearch/src/VuFindSearch/Backend/LibGuides/Response/RecordCollectionFactory.php b/module/VuFindSearch/src/VuFindSearch/Backend/LibGuides/Response/RecordCollectionFactory.php
index 7d90452c391717c5fab49325ac45ec5301807a0d..ed199ac3f9bc378ce5d3ab032300606811c54fbe 100644
--- a/module/VuFindSearch/src/VuFindSearch/Backend/LibGuides/Response/RecordCollectionFactory.php
+++ b/module/VuFindSearch/src/VuFindSearch/Backend/LibGuides/Response/RecordCollectionFactory.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFindSearch/src/VuFindSearch/Backend/Pazpar2/Backend.php b/module/VuFindSearch/src/VuFindSearch/Backend/Pazpar2/Backend.php
index 19b69df7ee30ed9ccc4bd9cae5b2673f4c52fd62..6fb0796bee8fc7fd1c05f638ed0079245150ddd4 100644
--- a/module/VuFindSearch/src/VuFindSearch/Backend/Pazpar2/Backend.php
+++ b/module/VuFindSearch/src/VuFindSearch/Backend/Pazpar2/Backend.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
@@ -123,8 +123,8 @@ class Backend extends AbstractBackend
      * Perform a search and return record collection.
      *
      * @param AbstractQuery $query  Search query
-     * @param integer       $offset Search offset
-     * @param integer       $limit  Search limit
+     * @param int           $offset Search offset
+     * @param int           $limit  Search limit
      * @param ParamBag      $params Search backend parameters
      *
      * @return RecordCollectionInterface
diff --git a/module/VuFindSearch/src/VuFindSearch/Backend/Pazpar2/Connector.php b/module/VuFindSearch/src/VuFindSearch/Backend/Pazpar2/Connector.php
index 2afab398a5ef99e746d84b3a3b728c109ad90372..ac7aa37d6580d061ceda796c0c2545cff14d9e08 100644
--- a/module/VuFindSearch/src/VuFindSearch/Backend/Pazpar2/Connector.php
+++ b/module/VuFindSearch/src/VuFindSearch/Backend/Pazpar2/Connector.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Connection
diff --git a/module/VuFindSearch/src/VuFindSearch/Backend/Pazpar2/QueryBuilder.php b/module/VuFindSearch/src/VuFindSearch/Backend/Pazpar2/QueryBuilder.php
index 2f03656d5e678a137f784f34335f1c77264f25ee..47ee9ca3b9822e14463e6c7ddc7d52779519043c 100644
--- a/module/VuFindSearch/src/VuFindSearch/Backend/Pazpar2/QueryBuilder.php
+++ b/module/VuFindSearch/src/VuFindSearch/Backend/Pazpar2/QueryBuilder.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFindSearch/src/VuFindSearch/Backend/Pazpar2/Response/Record.php b/module/VuFindSearch/src/VuFindSearch/Backend/Pazpar2/Response/Record.php
index c75edd08ee5e8908dd120363efc2dde1ce48e6e5..6caa851c9ca46f4000154cf30a8853f74e5a65fa 100644
--- a/module/VuFindSearch/src/VuFindSearch/Backend/Pazpar2/Response/Record.php
+++ b/module/VuFindSearch/src/VuFindSearch/Backend/Pazpar2/Response/Record.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFindSearch/src/VuFindSearch/Backend/Pazpar2/Response/RecordCollection.php b/module/VuFindSearch/src/VuFindSearch/Backend/Pazpar2/Response/RecordCollection.php
index 7a2d1db61e071b1d74029dc5917f9114d6f50425..e507cf82221142b20ba36d1dbabff9ec9229d97e 100644
--- a/module/VuFindSearch/src/VuFindSearch/Backend/Pazpar2/Response/RecordCollection.php
+++ b/module/VuFindSearch/src/VuFindSearch/Backend/Pazpar2/Response/RecordCollection.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFindSearch/src/VuFindSearch/Backend/Pazpar2/Response/RecordCollectionFactory.php b/module/VuFindSearch/src/VuFindSearch/Backend/Pazpar2/Response/RecordCollectionFactory.php
index b52fbd2f361f14e8358a4d46fb45a9f185d8d321..2a5c6b4e6762ed88c6882a771013a63a80b7422f 100644
--- a/module/VuFindSearch/src/VuFindSearch/Backend/Pazpar2/Response/RecordCollectionFactory.php
+++ b/module/VuFindSearch/src/VuFindSearch/Backend/Pazpar2/Response/RecordCollectionFactory.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFindSearch/src/VuFindSearch/Backend/Primo/Backend.php b/module/VuFindSearch/src/VuFindSearch/Backend/Primo/Backend.php
index 21ea14699b16d3563e607b529d9b9af63e68fd89..7d80edbc45afd8e1131fb881a9191fb414501f5f 100644
--- a/module/VuFindSearch/src/VuFindSearch/Backend/Primo/Backend.php
+++ b/module/VuFindSearch/src/VuFindSearch/Backend/Primo/Backend.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
@@ -85,8 +85,8 @@ class Backend extends AbstractBackend
      * Perform a search and return record collection.
      *
      * @param AbstractQuery $query  Search query
-     * @param integer       $offset Search offset
-     * @param integer       $limit  Search limit
+     * @param int           $offset Search offset
+     * @param int           $limit  Search limit
      * @param ParamBag      $params Search backend parameters
      *
      * @return RecordCollectionInterface
diff --git a/module/VuFindSearch/src/VuFindSearch/Backend/Primo/Connector.php b/module/VuFindSearch/src/VuFindSearch/Backend/Primo/Connector.php
index 45e573fe201a8f86202bc4c02f24492ce76eb153..a4c4ade7f841f120938eb00c989fb069177f1870 100644
--- a/module/VuFindSearch/src/VuFindSearch/Backend/Primo/Connector.php
+++ b/module/VuFindSearch/src/VuFindSearch/Backend/Primo/Connector.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFindSearch/src/VuFindSearch/Backend/Primo/QueryBuilder.php b/module/VuFindSearch/src/VuFindSearch/Backend/Primo/QueryBuilder.php
index 3f02b1bf028c841359ee85aff1f098e08a25e19b..7cb3aa649203874eca607c0349c04952198b4872 100644
--- a/module/VuFindSearch/src/VuFindSearch/Backend/Primo/QueryBuilder.php
+++ b/module/VuFindSearch/src/VuFindSearch/Backend/Primo/QueryBuilder.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFindSearch/src/VuFindSearch/Backend/Primo/Response/RecordCollection.php b/module/VuFindSearch/src/VuFindSearch/Backend/Primo/Response/RecordCollection.php
index 6f7a7fc717652c1bc7155d976af8b544c6a0a397..0ecb31a9e8398bb0d3b9270977a150952ac39364 100644
--- a/module/VuFindSearch/src/VuFindSearch/Backend/Primo/Response/RecordCollection.php
+++ b/module/VuFindSearch/src/VuFindSearch/Backend/Primo/Response/RecordCollection.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFindSearch/src/VuFindSearch/Backend/Primo/Response/RecordCollectionFactory.php b/module/VuFindSearch/src/VuFindSearch/Backend/Primo/Response/RecordCollectionFactory.php
index 771b7cb6b2cc74e8b0335fec031069ab5ac9b594..25b83040a3573dca365cd5bf962cef2f24c3cc47 100644
--- a/module/VuFindSearch/src/VuFindSearch/Backend/Primo/Response/RecordCollectionFactory.php
+++ b/module/VuFindSearch/src/VuFindSearch/Backend/Primo/Response/RecordCollectionFactory.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFindSearch/src/VuFindSearch/Backend/SRU/Connector.php b/module/VuFindSearch/src/VuFindSearch/Backend/SRU/Connector.php
index f666aeebcd1b7bc6f14a2683c8e35c6f0871976d..b63786426f2bd9bf0f765eb70c48a8bf4f813742 100644
--- a/module/VuFindSearch/src/VuFindSearch/Backend/SRU/Connector.php
+++ b/module/VuFindSearch/src/VuFindSearch/Backend/SRU/Connector.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  SRU
diff --git a/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Backend.php b/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Backend.php
index 9b5c7e2e50fd32f6b15212e25c0d8d15bb72e077..d2247ddcfbaacbc3374259a22255530465bcaf7c 100644
--- a/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Backend.php
+++ b/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Backend.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
@@ -73,6 +73,13 @@ class Backend extends AbstractBackend
      */
     protected $queryBuilder = null;
 
+    /**
+     * Similar records query builder.
+     *
+     * @var SimilarBuilder
+     */
+    protected $similarBuilder = null;
+
     /**
      * Constructor.
      *
@@ -90,8 +97,8 @@ class Backend extends AbstractBackend
      * Perform a search and return record collection.
      *
      * @param AbstractQuery $query  Search query
-     * @param integer       $offset Search offset
-     * @param integer       $limit  Search limit
+     * @param int           $offset Search offset
+     * @param int           $limit  Search limit
      * @param ParamBag      $params Search backend parameters
      *
      * @return RecordCollectionInterface
@@ -116,7 +123,7 @@ class Backend extends AbstractBackend
      * Get Random records
      *
      * @param AbstractQuery $query  Search query
-     * @param integer       $limit  Search limit
+     * @param int           $limit  Search limit
      * @param ParamBag      $params Search backend parameters
      *
      * @return RecordCollectionInterface
@@ -211,6 +218,7 @@ class Backend extends AbstractBackend
         $params = $params ?: new ParamBag();
         $this->injectResponseWriter($params);
 
+        $params->mergeWith($this->getSimilarBuilder()->build($id, $params));
         $response   = $this->connector->similar($id, $params);
         $collection = $this->createRecordCollection($response);
         $this->injectSourceIdentifier($collection);
@@ -304,6 +312,33 @@ class Backend extends AbstractBackend
         return $this->queryBuilder;
     }
 
+    /**
+     * Set the similar records query builder.
+     *
+     * @param SimilarBuilder $similarBuilder Similar builder
+     *
+     * @return void
+     */
+    public function setSimilarBuilder(SimilarBuilder $similarBuilder)
+    {
+        $this->similarBuilder = $similarBuilder;
+    }
+
+    /**
+     * Return similar records query builder.
+     *
+     * Lazy loads an empty default SimilarBuilder if none was set.
+     *
+     * @return SimilarBuilder
+     */
+    public function getSimilarBuilder()
+    {
+        if (!$this->similarBuilder) {
+            $this->similarBuilder = new SimilarBuilder();
+        }
+        return $this->similarBuilder;
+    }
+
     /**
      * Return the record collection factory.
      *
diff --git a/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Connector.php b/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Connector.php
index d2d96a381bf0d77b10e84a1c7ded7f70431713f4..b0e8adf61e307f150ad4806960acba9b108950a3 100644
--- a/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Connector.php
+++ b/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Connector.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
@@ -66,7 +66,7 @@ class Connector implements \Zend\Log\LoggerAwareInterface
      *
      * @see self::query()
      *
-     * @var integer
+     * @var int
      */
     const MAX_GET_URL_LENGTH = 2048;
 
@@ -185,23 +185,20 @@ class Connector implements \Zend\Log\LoggerAwareInterface
     /**
      * Return records similar to a given record specified by id.
      *
-     * Uses MoreLikeThis Request Handler
+     * Uses MoreLikeThis Request Component or MoreLikeThis Handler
      *
-     * @param string   $id     Id of given record
+     * @param string   $id     ID of given record (not currently used, but
+     * retained for backward compatibility / extensibility).
      * @param ParamBag $params Parameters
      *
      * @return string
+     *
+     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
      */
-    public function similar($id, ParamBag $params = null)
+    public function similar($id, ParamBag $params)
     {
-        $params = $params ?: new ParamBag();
-        $params
-            ->set('q', sprintf('%s:"%s"', $this->uniqueKey, addcslashes($id, '"')));
-        $params->set('qt', 'morelikethis');
-
         $handler = $this->map->getHandler(__FUNCTION__);
         $this->map->prepare(__FUNCTION__, $params);
-
         return $this->query($handler, $params);
     }
 
diff --git a/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Document/AbstractDocument.php b/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Document/AbstractDocument.php
index 3c5278a280faaa0470588c0756cd4d5e17d1c788..6c42383ca7fcca16d6195ecc161f1dcd8d284990 100644
--- a/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Document/AbstractDocument.php
+++ b/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Document/AbstractDocument.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Document/CommitDocument.php b/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Document/CommitDocument.php
index 5027c4fa939637cb6df98723f6836dc98526efa3..27b7f1e35904ff0990ab2a179ced3e314b7fd23e 100644
--- a/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Document/CommitDocument.php
+++ b/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Document/CommitDocument.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
@@ -44,14 +44,14 @@ class CommitDocument extends AbstractDocument
     /**
      * Value for commitWithin attribute
      *
-     * @var integer
+     * @var int
      */
     protected $commitWithin;
 
     /**
      * Constructor.
      *
-     * @param integer $commitWithin commitWithin attribute value
+     * @param int $commitWithin commitWithin attribute value
      *
      * @return void
      */
diff --git a/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Document/DeleteDocument.php b/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Document/DeleteDocument.php
index 7045035b8b0d80c5d11828710734ce65f13b215c..2eb386ae439eda19cb68f70923d858691ebaeeaa 100644
--- a/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Document/DeleteDocument.php
+++ b/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Document/DeleteDocument.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Document/OptimizeDocument.php b/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Document/OptimizeDocument.php
index 7df461ffe46e6861826563ffde329cbc5c17bb31..523ad174bad4f194e7c87ca936444b877ede4058 100644
--- a/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Document/OptimizeDocument.php
+++ b/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Document/OptimizeDocument.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
@@ -44,14 +44,14 @@ class OptimizeDocument extends AbstractDocument
     /**
      * Value for waitFlush attribute
      *
-     * @var boolean
+     * @var bool
      */
     protected $waitFlush;
 
     /**
      * Value for waitSearch attribute
      *
-     * @var boolean
+     * @var bool
      */
     protected $waitSearcher;
 
diff --git a/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Document/RawXMLDocument.php b/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Document/RawXMLDocument.php
index 1e2dba18c1eea33695614e97ffc6e1a39ba94d82..5e05267d5a0cce92ab0fbe13356049c601403a75 100644
--- a/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Document/RawXMLDocument.php
+++ b/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Document/RawXMLDocument.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Document/UpdateDocument.php b/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Document/UpdateDocument.php
index 9b580390233f587d2327f6b20f63dc0155aa67e4..65927e789796fc7382f23390120e6a402dc231be 100644
--- a/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Document/UpdateDocument.php
+++ b/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Document/UpdateDocument.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFindSearch/src/VuFindSearch/Backend/Solr/HandlerMap.php b/module/VuFindSearch/src/VuFindSearch/Backend/Solr/HandlerMap.php
index f2c3bed87aa15a9f7a4b06cb6fe948ca40160985..9cfc3e4d1b0ae45ae8b0974c0d9793d8c0072e7c 100644
--- a/module/VuFindSearch/src/VuFindSearch/Backend/Solr/HandlerMap.php
+++ b/module/VuFindSearch/src/VuFindSearch/Backend/Solr/HandlerMap.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFindSearch/src/VuFindSearch/Backend/Solr/LuceneSyntaxHelper.php b/module/VuFindSearch/src/VuFindSearch/Backend/Solr/LuceneSyntaxHelper.php
index b8df9a3caa90a923d7a23d450d005d43a7d32cdc..a508351c94937f875acbc92276b26dbdcabd3d15 100644
--- a/module/VuFindSearch/src/VuFindSearch/Backend/Solr/LuceneSyntaxHelper.php
+++ b/module/VuFindSearch/src/VuFindSearch/Backend/Solr/LuceneSyntaxHelper.php
@@ -6,6 +6,7 @@
  * PHP version 5
  *
  * Copyright (C) Villanova University 2010.
+ * Copyright (C) The National Library of Finland 2016.
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License version 2,
@@ -18,13 +19,14 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
  * @author   Andrew S. Nagy <vufind-tech@lists.sourceforge.net>
  * @author   David Maus <maus@hab.de>
  * @author   Demian Katz <demian.katz@villanova.edu>
+ * @author   Ere Maijala <ere.maijala@helsinki.fi>
  * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
  * @link     https://vufind.org
  */
@@ -38,6 +40,7 @@ namespace VuFindSearch\Backend\Solr;
  * @author   Andrew S. Nagy <vufind-tech@lists.sourceforge.net>
  * @author   David Maus <maus@hab.de>
  * @author   Demian Katz <demian.katz@villanova.edu>
+ * @author   Ere Maijala <ere.maijala@helsinki.fi>
  * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
  * @link     https://vufind.org
  */
@@ -267,6 +270,71 @@ class LuceneSyntaxHelper
         return trim(preg_replace_callback($regs, $callback, $string));
     }
 
+    /**
+     * Extract search terms from a query string for spell checking.
+     *
+     * This will only handle the most often used simple cases.
+     *
+     * @param string $query Query string
+     *
+     * @return string
+     */
+    public function extractSearchTerms($query)
+    {
+        $result = [];
+        $inQuotes = false;
+        $collected = '';
+        $discardParens = 0;
+        // Discard local parameters
+        $query = preg_replace('/\{!.+?\}/', '', $query);
+        // Discard fuzziness and proximity indicators
+        $query = preg_replace('/\~[^\s]*/', '', $query);
+        $query = preg_replace('/\^[^\s]*/', '', $query);
+        $lastCh = '';
+        foreach (str_split($query) as $ch) {
+            // Handle quotes (everything in quotes is considered part of search
+            // terms)
+            if ($ch == '"' && $lastCh != '\\') {
+                $inQuotes = !$inQuotes;
+            }
+            if (!$inQuotes) {
+                // Discard closing parenthesis for previously discarded opening ones
+                // to keep balance
+                if ($ch == ')' && $discardParens > 0) {
+                    --$discardParens;
+                    continue;
+                }
+                // Flush to result array on word break
+                if ($ch == ' ' && $collected !== '') {
+                    $result[] = $collected;
+                    $collected = '';
+                    continue;
+                }
+                // If we encounter ':', discard preceding string as it's a field name
+                if ($ch == ':') {
+                    // Take into account any opening parenthesis we discard here
+                    $discardParens += substr_count($collected, '(');
+                    $collected = '';
+                    continue;
+                }
+            }
+            $collected .= $ch;
+            $lastCh = $ch;
+        }
+        // Flush final collected string
+        if ($collected !== '') {
+            $result[] = $collected;
+        }
+        // Discard any preceding pluses or minuses
+        $result = array_map(
+            function ($s) {
+                return ltrim($s, '+-');
+            },
+            $result
+        );
+        return implode(' ', $result);
+    }
+
     /**
      * Are there any case-sensitive Boolean operators configured?
      *
diff --git a/module/VuFindSearch/src/VuFindSearch/Backend/Solr/QueryBuilder.php b/module/VuFindSearch/src/VuFindSearch/Backend/Solr/QueryBuilder.php
index 29f228bad3b4fdc659b564714abf7cc34f35d7a9..0c9d8e95019b4bc65feb530ae0a59f7c332d30e0 100644
--- a/module/VuFindSearch/src/VuFindSearch/Backend/Solr/QueryBuilder.php
+++ b/module/VuFindSearch/src/VuFindSearch/Backend/Solr/QueryBuilder.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
@@ -120,10 +120,13 @@ class QueryBuilder implements QueryBuilderInterface
     {
         $params = new ParamBag();
 
-        // Add spelling query if applicable -- note that we mus set this up before
+        // Add spelling query if applicable -- note that we must set this up before
         // we process the main query in order to avoid unwanted extra syntax:
         if ($this->createSpellingQuery) {
-            $params->set('spellcheck.q', $query->getAllTerms());
+            $params->set(
+                'spellcheck.q',
+                $this->getLuceneHelper()->extractSearchTerms($query->getAllTerms())
+            );
         }
 
         if ($query instanceof QueryGroup) {
@@ -151,19 +154,24 @@ class QueryBuilder implements QueryBuilderInterface
                         $params->set('hl.q', $oldString);
                     }
                 }
-            } else {
-                if ($handler->hasDismax()) {
-                    $params->set('qf', implode(' ', $handler->getDismaxFields()));
-                    $params->set('qt', $handler->getDismaxHandler());
-                    foreach ($handler->getDismaxParams() as $param) {
-                        $params->add(reset($param), next($param));
-                    }
-                    if ($handler->hasFilterQuery()) {
-                        $params->add('fq', $handler->getFilterQuery());
-                    }
-                } else {
-                    $string = $handler->createSimpleQueryString($string);
+            } else if ($handler->hasDismax()) {
+                // If we're using extended dismax, we'll miss out on the question
+                // mark fix in createAdvancedInnerSearchString(), so we should
+                // apply it here. If other query munges arise that are valuable
+                // to both dismax and edismax, we should add a wrapper function
+                // around them and call it from here instead of this one very
+                // specific check.
+                $string = $this->fixTrailingQuestionMarks($string);
+                $params->set('qf', implode(' ', $handler->getDismaxFields()));
+                $params->set('qt', $handler->getDismaxHandler());
+                foreach ($handler->getDismaxParams() as $param) {
+                    $params->add(reset($param), next($param));
+                }
+                if ($handler->hasFilterQuery()) {
+                    $params->add('fq', $handler->getFilterQuery());
                 }
+            } else {
+                $string = $handler->createSimpleQueryString($string);
             }
         }
         $params->set('q', $string);
@@ -359,6 +367,38 @@ class QueryBuilder implements QueryBuilderInterface
         }
     }
 
+    /**
+     * If the query ends in a non-escaped question mark, the user may not really
+     * intend to use the question mark as a wildcard -- let's account for that
+     * possibility.
+     *
+     * @param string $string Search query to adjust
+     *
+     * @return string
+     */
+    protected function fixTrailingQuestionMarks($string)
+    {
+        $multiword = preg_match('/[^\s]\s+[^\s]/', $string);
+        $callback = function ($matches) use ($multiword) {
+            // Make sure all question marks are properly escaped (first unescape
+            // any that are already escaped to prevent double-escapes, then escape
+            // all of them):
+            $s = $matches[1];
+            $escaped = str_replace('?', '\?', str_replace('\?', '?', $s));
+            $s = "($s) OR ($escaped)";
+            if ($multiword) {
+                $s = "($s) ";
+            }
+            return $s;
+        };
+        // Use a lookahead to skip matches found within quoted phrases.
+        $lookahead = '(?=(?:[^\"]*+\"[^\"]*+\")*+[^\"]*+$)';
+        $string = preg_replace_callback(
+            '/([^\s]+\?)(\s|$)' . $lookahead . '/', $callback, $string
+        );
+        return rtrim($string);
+    }
+
     /**
      * Return advanced inner search string based on input and handler.
      *
@@ -383,17 +423,8 @@ class QueryBuilder implements QueryBuilderInterface
             return $string;
         }
 
-        // If the query ends in a non-escaped question mark, the user may not really
-        // intend to use the question mark as a wildcard -- let's account for that
-        // possibility
-        if (substr($string, -1) == '?' && substr($string, -2) != '\?') {
-            // Make sure all question marks are properly escaped (first unescape
-            // any that are already escaped to prevent double-escapes, then escape
-            // all of them):
-            $strippedQuery
-                = str_replace('?', '\?', str_replace('\?', '?', $string));
-            $string = "({$string}) OR (" . $strippedQuery . ")";
-        }
+        // Account for trailing question marks:
+        $string = $this->fixTrailingQuestionMarks($string);
 
         return $handler
             ? $handler->createAdvancedQueryString($string, false) : $string;
diff --git a/module/VuFindSearch/src/VuFindSearch/Backend/Solr/QueryBuilderInterface.php b/module/VuFindSearch/src/VuFindSearch/Backend/Solr/QueryBuilderInterface.php
index 279d9933cb1e70aefdadd7fa4376211137f8dc90..275bfa9eafb3c9803d5832f50014f775336404ab 100644
--- a/module/VuFindSearch/src/VuFindSearch/Backend/Solr/QueryBuilderInterface.php
+++ b/module/VuFindSearch/src/VuFindSearch/Backend/Solr/QueryBuilderInterface.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Record/SerializableRecord.php b/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Record/SerializableRecord.php
index 2784c6872a037774cf335a30d897d7989414eacf..3b3fe2eaa0db8b22488998c16c49a479f04bfd9b 100644
--- a/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Record/SerializableRecord.php
+++ b/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Record/SerializableRecord.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Record/SerializableRecordInterface.php b/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Record/SerializableRecordInterface.php
index c7219b9c104fa924dbace69a4c4be00cee33319a..bc2255688726d7fc9b9297bcd6d280f6805fff31 100644
--- a/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Record/SerializableRecordInterface.php
+++ b/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Record/SerializableRecordInterface.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Response/Json/Facets.php b/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Response/Json/Facets.php
index a06cdf9f446a1735e8203d527bcdb2ec1165dec0..f5f39b7ca4873bc764d0ae4ad7ae0a31a479cc62 100644
--- a/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Response/Json/Facets.php
+++ b/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Response/Json/Facets.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Response/Json/NamedList.php b/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Response/Json/NamedList.php
index b46587d3edcd049da44df43fbd4cb0d286846eec..8d2314e054c06477f73d832551240dbf316e1779 100644
--- a/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Response/Json/NamedList.php
+++ b/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Response/Json/NamedList.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
@@ -132,7 +132,7 @@ class NamedList implements Countable, Iterator
     /**
      * Return true if the iterator is at a valid position.
      *
-     * @return boolean
+     * @return bool
      */
     public function valid()
     {
diff --git a/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Response/Json/Record.php b/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Response/Json/Record.php
index 1e7e3390fc41cf9a52f1d23b95aef8a98f611303..2370f011961ba7084e776010ce1a9583ed32422f 100644
--- a/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Response/Json/Record.php
+++ b/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Response/Json/Record.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Response/Json/RecordCollection.php b/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Response/Json/RecordCollection.php
index 5a0626ea47e694ce307e48b2b9ba023eb8516310..39c9e2a6f590aa14fc60be556b441d733ea235c1 100644
--- a/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Response/Json/RecordCollection.php
+++ b/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Response/Json/RecordCollection.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Response/Json/RecordCollectionFactory.php b/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Response/Json/RecordCollectionFactory.php
index aea5294390849bda85044f834a0482ec081777f2..8378112f19524a805ee546fcc73b6815d197b4f1 100644
--- a/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Response/Json/RecordCollectionFactory.php
+++ b/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Response/Json/RecordCollectionFactory.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Response/Json/Spellcheck.php b/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Response/Json/Spellcheck.php
index 6fc60cb26b7416896a9b8a354cadf30ce3bc1224..a6d6bff7e774396b1bdebfd213dec7a42501f2ac 100644
--- a/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Response/Json/Spellcheck.php
+++ b/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Response/Json/Spellcheck.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
@@ -161,7 +161,7 @@ class Spellcheck implements IteratorAggregate, Countable
      *
      * @param string $term Term to check
      *
-     * @return boolean
+     * @return bool
      */
     protected function contains($term)
     {
diff --git a/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Response/Json/Terms.php b/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Response/Json/Terms.php
index 2faa4806e9315ffb81cbbcad9aaa182360ad7b70..e9a640ad3d3932547f66806304501ec7c2e7ecd1 100644
--- a/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Response/Json/Terms.php
+++ b/module/VuFindSearch/src/VuFindSearch/Backend/Solr/Response/Json/Terms.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFindSearch/src/VuFindSearch/Backend/Solr/SearchHandler.php b/module/VuFindSearch/src/VuFindSearch/Backend/Solr/SearchHandler.php
index d4c0977228caedb8ccf3d435dc0506c3a6c4736d..9ff6012e2b306bcb0ff153915cac6fc5245b2e28 100644
--- a/module/VuFindSearch/src/VuFindSearch/Backend/Solr/SearchHandler.php
+++ b/module/VuFindSearch/src/VuFindSearch/Backend/Solr/SearchHandler.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFindSearch/src/VuFindSearch/Backend/Solr/SimilarBuilder.php b/module/VuFindSearch/src/VuFindSearch/Backend/Solr/SimilarBuilder.php
new file mode 100644
index 0000000000000000000000000000000000000000..b9a47fcd2257f76efd22f7b9a04f43aab7a2b800
--- /dev/null
+++ b/module/VuFindSearch/src/VuFindSearch/Backend/Solr/SimilarBuilder.php
@@ -0,0 +1,136 @@
+<?php
+
+/**
+ * SOLR SimilarBuilder.
+ *
+ * PHP version 5
+ *
+ * Copyright (C) Villanova University 2010.
+ * Copyright (C) The National Library of Finland 2016.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ * @category VuFind
+ * @package  Search
+ * @author   Andrew S. Nagy <vufind-tech@lists.sourceforge.net>
+ * @author   David Maus <maus@hab.de>
+ * @author   Demian Katz <demian.katz@villanova.edu>
+ * @author   Ere Maijala <ere.maijala@helsinki.fi>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     https://vufind.org
+ */
+namespace VuFindSearch\Backend\Solr;
+
+use VuFindSearch\ParamBag;
+
+/**
+ * SOLR SimilarBuilder.
+ *
+ * @category VuFind
+ * @package  Search
+ * @author   Andrew S. Nagy <vufind-tech@lists.sourceforge.net>
+ * @author   David Maus <maus@hab.de>
+ * @author   Demian Katz <demian.katz@villanova.edu>
+ * @author   Ere Maijala <ere.maijala@helsinki.fi>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     https://vufind.org
+ */
+class SimilarBuilder implements SimilarBuilderInterface
+{
+    /**
+     * Solr field used to store unique identifier
+     *
+     * @var string
+     */
+    protected $uniqueKey;
+
+    /**
+     * Whether to use MoreLikeThis Handler instead of the traditional MoreLikeThis
+     * component.
+     *
+     * @var bool
+     */
+    protected $useHandler = false;
+
+    /**
+     * MoreLikeThis Handler parameters
+     *
+     * @var string
+     */
+    protected $handlerParams = '';
+
+    /**
+     * Number of similar records to retrieve
+     *
+     * @var int
+     */
+    protected $count = 5;
+
+    /**
+     * Constructor.
+     *
+     * @param \Zend\Config\Config $searchConfig Search config
+     * @param string              $uniqueKey    Solr field used to store unique
+     * identifier
+     *
+     * @return void
+     */
+    public function __construct(\Zend\Config\Config $searchConfig = null,
+        $uniqueKey = 'id'
+    ) {
+        $this->uniqueKey = $uniqueKey;
+        if (isset($searchConfig->MoreLikeThis)) {
+            $mlt = $searchConfig->MoreLikeThis;
+            if (isset($mlt->useMoreLikeThisHandler)
+                && $mlt->useMoreLikeThisHandler
+            ) {
+                $this->useHandler = true;
+                $this->handlerParams = isset($mlt->params) ? $mlt->params : '';
+            }
+            if (isset($mlt->count)) {
+                $this->count = $mlt->count;
+            }
+        }
+    }
+
+    /// Public API
+
+    /**
+     * Return SOLR search parameters based on a record Id and params.
+     *
+     * @param string $id Record Id
+     *
+     * @return ParamBag
+     */
+    public function build($id)
+    {
+        $params = new ParamBag();
+        if ($this->useHandler) {
+            $mltParams = $this->handlerParams
+                ? $this->handlerParams
+                : 'qf=title,title_short,callnumber-label,topic,language,author,'
+                    . 'publishDate mintf=1 mindf=1';
+            $params->set('q', sprintf('{!mlt %s}%s', $mltParams, $id));
+        } else {
+            $params->set(
+                'q', sprintf('%s:"%s"', $this->uniqueKey, addcslashes($id, '"'))
+            );
+            $params->set('qt', 'morelikethis');
+        }
+        if (null === $params->get('rows')) {
+            $params->set('rows', $this->count);
+        }
+        return $params;
+    }
+}
diff --git a/module/VuDL/Module.php b/module/VuFindSearch/src/VuFindSearch/Backend/Solr/SimilarBuilderInterface.php
similarity index 53%
rename from module/VuDL/Module.php
rename to module/VuFindSearch/src/VuFindSearch/Backend/Solr/SimilarBuilderInterface.php
index 3c93fb2e3fdd8d0772c07e6e4636b1864e65866a..1397e45cd96b70f7146ddf40604cd43acc555d85 100644
--- a/module/VuDL/Module.php
+++ b/module/VuFindSearch/src/VuFindSearch/Backend/Solr/SimilarBuilderInterface.php
@@ -1,10 +1,12 @@
 <?php
+
 /**
- * VuDL interface module.
+ * SOLR SimilarBuilder interface definition.
  *
  * PHP version 5
  *
  * Copyright (C) Villanova University 2010.
+ * Copyright (C) The National Library of Finland 2016.
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License version 2,
@@ -17,50 +19,41 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
- * @package  Module
+ * @package  Search
+ * @author   Andrew S. Nagy <vufind-tech@lists.sourceforge.net>
+ * @author   David Maus <maus@hab.de>
  * @author   Demian Katz <demian.katz@villanova.edu>
+ * @author   Ere Maijala <ere.maijala@helsinki.fi>
  * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
- * @link     https://vufind.org/wiki/development
+ * @link     https://vufind.org
  */
-namespace VuDL;
+namespace VuFindSearch\Backend\Solr;
+
+use VuFindSearch\ParamBag;
 
 /**
- * VuDL interface module.
+ * SOLR SimilarBuilder interface definition.
  *
  * @category VuFind
- * @package  Module
+ * @package  Search
+ * @author   Andrew S. Nagy <vufind-tech@lists.sourceforge.net>
+ * @author   David Maus <maus@hab.de>
  * @author   Demian Katz <demian.katz@villanova.edu>
+ * @author   Ere Maijala <ere.maijala@helsinki.fi>
  * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
- * @link     https://vufind.org/wiki/development
+ * @link     https://vufind.org
  */
-class Module
+interface SimilarBuilderInterface
 {
     /**
-     * Get module configuration
+     * Build SOLR query based on VuFind query object.
      *
-     * @return array
-     */
-    public function getConfig()
-    {
-        return include __DIR__ . '/config/module.config.php';
-    }
-
-    /**
-     * Get autoloader configuration
+     * @param string $id Record id
      *
-     * @return array
+     * @return ParamBag
      */
-    public function getAutoloaderConfig()
-    {
-        return [
-            'Zend\Loader\StandardAutoloader' => [
-                'namespaces' => [
-                    __NAMESPACE__ => __DIR__ . '/src/' . __NAMESPACE__,
-                ],
-            ],
-        ];
-    }
+    public function build($id);
 }
diff --git a/module/VuFindSearch/src/VuFindSearch/Backend/Summon/Backend.php b/module/VuFindSearch/src/VuFindSearch/Backend/Summon/Backend.php
index 6fdc1c55764e15592d5314c9334054e10ebddabb..42fd418f52c0c483b00570a9c30d8bb743fc8e92 100644
--- a/module/VuFindSearch/src/VuFindSearch/Backend/Summon/Backend.php
+++ b/module/VuFindSearch/src/VuFindSearch/Backend/Summon/Backend.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
@@ -92,8 +92,8 @@ class Backend extends AbstractBackend implements RetrieveBatchInterface
      * Perform a search and return record collection.
      *
      * @param AbstractQuery $query  Search query
-     * @param integer       $offset Search offset
-     * @param integer       $limit  Search limit
+     * @param int           $offset Search offset
+     * @param int           $limit  Search limit
      * @param ParamBag      $params Search backend parameters
      *
      * @return RecordCollectionInterface
diff --git a/module/VuFindSearch/src/VuFindSearch/Backend/Summon/QueryBuilder.php b/module/VuFindSearch/src/VuFindSearch/Backend/Summon/QueryBuilder.php
index 3db41a9d5b1c41e1fe3c0f7f566a16e041c433f2..64eda65801effe9a8f9260b3e59bed0672313544 100644
--- a/module/VuFindSearch/src/VuFindSearch/Backend/Summon/QueryBuilder.php
+++ b/module/VuFindSearch/src/VuFindSearch/Backend/Summon/QueryBuilder.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFindSearch/src/VuFindSearch/Backend/Summon/Response/RecordCollection.php b/module/VuFindSearch/src/VuFindSearch/Backend/Summon/Response/RecordCollection.php
index 8d2b51bd4fd661be2109efc31c6eaf17b2018dce..261262cacfc89a8960b7549762c5104638f959ae 100644
--- a/module/VuFindSearch/src/VuFindSearch/Backend/Summon/Response/RecordCollection.php
+++ b/module/VuFindSearch/src/VuFindSearch/Backend/Summon/Response/RecordCollection.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFindSearch/src/VuFindSearch/Backend/Summon/Response/RecordCollectionFactory.php b/module/VuFindSearch/src/VuFindSearch/Backend/Summon/Response/RecordCollectionFactory.php
index 7115427fc73cf566a3192f1bde9a22bf5576c9d6..77f11892b9a30dc33744804e59ada85f8bf01365 100644
--- a/module/VuFindSearch/src/VuFindSearch/Backend/Summon/Response/RecordCollectionFactory.php
+++ b/module/VuFindSearch/src/VuFindSearch/Backend/Summon/Response/RecordCollectionFactory.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFindSearch/src/VuFindSearch/Backend/WorldCat/Backend.php b/module/VuFindSearch/src/VuFindSearch/Backend/WorldCat/Backend.php
index 1c64462b5859110afd18d0ed2d99bbe504065bbd..d0ea632f6e3a98df315e2e5e7ad80f6f18725757 100644
--- a/module/VuFindSearch/src/VuFindSearch/Backend/WorldCat/Backend.php
+++ b/module/VuFindSearch/src/VuFindSearch/Backend/WorldCat/Backend.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
@@ -85,8 +85,8 @@ class Backend extends AbstractBackend
      * Perform a search and return record collection.
      *
      * @param AbstractQuery $query  Search query
-     * @param integer       $offset Search offset
-     * @param integer       $limit  Search limit
+     * @param int           $offset Search offset
+     * @param int           $limit  Search limit
      * @param ParamBag      $params Search backend parameters
      *
      * @return RecordCollectionInterface
diff --git a/module/VuFindSearch/src/VuFindSearch/Backend/WorldCat/Connector.php b/module/VuFindSearch/src/VuFindSearch/Backend/WorldCat/Connector.php
index 407315695bb8d6e6c169e4cccc6ee101f35c57ef..207baa86840de28daa1c6e65490191f258d07c6f 100644
--- a/module/VuFindSearch/src/VuFindSearch/Backend/WorldCat/Connector.php
+++ b/module/VuFindSearch/src/VuFindSearch/Backend/WorldCat/Connector.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  WorldCat
@@ -121,8 +121,8 @@ class Connector extends \VuFindSearch\Backend\SRU\Connector
      * Execute a search.
      *
      * @param ParamBag $params Parameters
-     * @param integer  $offset Search offset
-     * @param integer  $limit  Search limit
+     * @param int      $offset Search offset
+     * @param int      $limit  Search limit
      *
      * @return string
      */
diff --git a/module/VuFindSearch/src/VuFindSearch/Backend/WorldCat/QueryBuilder.php b/module/VuFindSearch/src/VuFindSearch/Backend/WorldCat/QueryBuilder.php
index f1dece49e347a92636f334a8d6bf04569dda9f85..94b288d2269d6863b5e54a05220b1007913ec75c 100644
--- a/module/VuFindSearch/src/VuFindSearch/Backend/WorldCat/QueryBuilder.php
+++ b/module/VuFindSearch/src/VuFindSearch/Backend/WorldCat/QueryBuilder.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFindSearch/src/VuFindSearch/Backend/WorldCat/Response/XML/Record.php b/module/VuFindSearch/src/VuFindSearch/Backend/WorldCat/Response/XML/Record.php
index 42dbe73669108d669cf08d88e945c491d07d81c6..8e1c89a7ba4ca70ef1a048b08fae55dcdcb48d3d 100644
--- a/module/VuFindSearch/src/VuFindSearch/Backend/WorldCat/Response/XML/Record.php
+++ b/module/VuFindSearch/src/VuFindSearch/Backend/WorldCat/Response/XML/Record.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFindSearch/src/VuFindSearch/Backend/WorldCat/Response/XML/RecordCollection.php b/module/VuFindSearch/src/VuFindSearch/Backend/WorldCat/Response/XML/RecordCollection.php
index 386d88f95690f4bac20ac2e57d6a678e4e3a48b8..7b9f0e6139dfd412509724325dcf7d15f60f617e 100644
--- a/module/VuFindSearch/src/VuFindSearch/Backend/WorldCat/Response/XML/RecordCollection.php
+++ b/module/VuFindSearch/src/VuFindSearch/Backend/WorldCat/Response/XML/RecordCollection.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFindSearch/src/VuFindSearch/Backend/WorldCat/Response/XML/RecordCollectionFactory.php b/module/VuFindSearch/src/VuFindSearch/Backend/WorldCat/Response/XML/RecordCollectionFactory.php
index 24800ba9848d04c5b8b367e011d8e6192844e33e..fb2e6ab810912514220b06b29def488579895a2a 100644
--- a/module/VuFindSearch/src/VuFindSearch/Backend/WorldCat/Response/XML/RecordCollectionFactory.php
+++ b/module/VuFindSearch/src/VuFindSearch/Backend/WorldCat/Response/XML/RecordCollectionFactory.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFindSearch/src/VuFindSearch/Exception/ExceptionInterface.php b/module/VuFindSearch/src/VuFindSearch/Exception/ExceptionInterface.php
index a8bcc9ad5aa8fe560c0d2db502874566bcc48dea..14701b521db538585073001801f8c3b5c6192874 100644
--- a/module/VuFindSearch/src/VuFindSearch/Exception/ExceptionInterface.php
+++ b/module/VuFindSearch/src/VuFindSearch/Exception/ExceptionInterface.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFindSearch/src/VuFindSearch/Exception/InvalidArgumentException.php b/module/VuFindSearch/src/VuFindSearch/Exception/InvalidArgumentException.php
index 70508716c414dadd956f41304751390fd43802e5..374a3ce91bc68f3a4df7ed99bfa26ad3aa5c9576 100644
--- a/module/VuFindSearch/src/VuFindSearch/Exception/InvalidArgumentException.php
+++ b/module/VuFindSearch/src/VuFindSearch/Exception/InvalidArgumentException.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFindSearch/src/VuFindSearch/Exception/RuntimeException.php b/module/VuFindSearch/src/VuFindSearch/Exception/RuntimeException.php
index 96f98db62e458327ed7f6c060c2f391245594963..a6f86dc11f0b3e55006843e4be7480bca1837b15 100644
--- a/module/VuFindSearch/src/VuFindSearch/Exception/RuntimeException.php
+++ b/module/VuFindSearch/src/VuFindSearch/Exception/RuntimeException.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFindSearch/src/VuFindSearch/Feature/RandomInterface.php b/module/VuFindSearch/src/VuFindSearch/Feature/RandomInterface.php
index f8501a0ca5fde9af5e4d183f6dd710d095390a10..c8cb9723dd0762ba085680c8734050c7a615c411 100644
--- a/module/VuFindSearch/src/VuFindSearch/Feature/RandomInterface.php
+++ b/module/VuFindSearch/src/VuFindSearch/Feature/RandomInterface.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
@@ -46,7 +46,7 @@ interface RandomInterface
      * Return random records.
      *
      * @param AbstractQuery $query  Search query
-     * @param integer       $limit  Search limit
+     * @param int           $limit  Search limit
      * @param ParamBag      $params Search backend parameters
      *
      * @return \VuFindSearch\Response\RecordCollectionInterface
diff --git a/module/VuFindSearch/src/VuFindSearch/Feature/RetrieveBatchInterface.php b/module/VuFindSearch/src/VuFindSearch/Feature/RetrieveBatchInterface.php
index b49c84179d6b98883360bed5501daeb080a42974..240bf8f936b690a6bd98d3092f2a2ee717832d4d 100644
--- a/module/VuFindSearch/src/VuFindSearch/Feature/RetrieveBatchInterface.php
+++ b/module/VuFindSearch/src/VuFindSearch/Feature/RetrieveBatchInterface.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFindSearch/src/VuFindSearch/Feature/SimilarInterface.php b/module/VuFindSearch/src/VuFindSearch/Feature/SimilarInterface.php
index d152f0d010a30d5e555d242a2bad702071890e46..2a2d29a28ba7c8744d7bcfb854da7979d4d19897 100644
--- a/module/VuFindSearch/src/VuFindSearch/Feature/SimilarInterface.php
+++ b/module/VuFindSearch/src/VuFindSearch/Feature/SimilarInterface.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFindSearch/src/VuFindSearch/ParamBag.php b/module/VuFindSearch/src/VuFindSearch/ParamBag.php
index c32cf56f0ae2aceaa9b1ca9c6e19f5a18ab2e56b..8a9a06142dc12c6d807642a077d0884d6be8ea25 100644
--- a/module/VuFindSearch/src/VuFindSearch/ParamBag.php
+++ b/module/VuFindSearch/src/VuFindSearch/ParamBag.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
@@ -82,7 +82,7 @@ class ParamBag
      * @param string $name  Parameter name
      * @param string $value Parameter value
      *
-     * @return boolean
+     * @return bool
      */
     public function contains($name, $value)
     {
diff --git a/module/VuFindSearch/src/VuFindSearch/Query/AbstractQuery.php b/module/VuFindSearch/src/VuFindSearch/Query/AbstractQuery.php
index e82bd8156e040e1aab5f4e2225e359be60cc3379..62e8f73f2e0548a341572de0eaa53f17caa7bf35 100644
--- a/module/VuFindSearch/src/VuFindSearch/Query/AbstractQuery.php
+++ b/module/VuFindSearch/src/VuFindSearch/Query/AbstractQuery.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFindSearch/src/VuFindSearch/Query/Query.php b/module/VuFindSearch/src/VuFindSearch/Query/Query.php
index eb9ee259be69a7ebf57a1fd74c8aa4e639f3343d..6c92e181d813cde6aa5d58aacbf1e370de9f1904 100644
--- a/module/VuFindSearch/src/VuFindSearch/Query/Query.php
+++ b/module/VuFindSearch/src/VuFindSearch/Query/Query.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
@@ -149,8 +149,9 @@ class Query extends AbstractQuery
      */
     public function containsTerm($needle)
     {
-        // Escape slashes in $needle to avoid regular expression errors:
-        $needle = str_replace('/', '\/', $needle);
+        // Escape characters with special meaning in regular expressions to avoid
+        // errors:
+        $needle = preg_quote($needle, '/');
 
         return (bool)preg_match("/\b$needle\b/u", $this->getString());
     }
@@ -177,7 +178,7 @@ class Query extends AbstractQuery
     {
         // Escape $from so it is regular expression safe (just in case it
         // includes any weird punctuation -- unlikely but possible):
-        $from = addcslashes($from, '\^$.[]|()?*+{}/');
+        $from = preg_quote($from, '/');
 
         // If our "from" pattern contains non-word characters, we can't use word
         // boundaries for matching.  We want to try to use word boundaries when
diff --git a/module/VuFindSearch/src/VuFindSearch/Query/QueryGroup.php b/module/VuFindSearch/src/VuFindSearch/Query/QueryGroup.php
index 9906c84c976c894fb3638c410027144bd6d6e3be..35eb6645cae4f6a12152b365f5070a3a8709e780 100644
--- a/module/VuFindSearch/src/VuFindSearch/Query/QueryGroup.php
+++ b/module/VuFindSearch/src/VuFindSearch/Query/QueryGroup.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
@@ -69,7 +69,7 @@ class QueryGroup extends AbstractQuery
     /**
      * Is the query group negated?
      *
-     * @var boolean
+     * @var bool
      */
     protected $negation;
 
@@ -229,7 +229,7 @@ class QueryGroup extends AbstractQuery
     /**
      * Return true if group is an exclusion group.
      *
-     * @return boolean
+     * @return bool
      */
     public function isNegated()
     {
diff --git a/module/VuFindSearch/src/VuFindSearch/Query/QueryInterface.php b/module/VuFindSearch/src/VuFindSearch/Query/QueryInterface.php
index 618c5590b864f85af129054e007d189107b26fbe..c9b48abe601b2a2fa8f8b16a44a2b80675ee1bc3 100644
--- a/module/VuFindSearch/src/VuFindSearch/Query/QueryInterface.php
+++ b/module/VuFindSearch/src/VuFindSearch/Query/QueryInterface.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFindSearch/src/VuFindSearch/Response/AbstractRecordCollection.php b/module/VuFindSearch/src/VuFindSearch/Response/AbstractRecordCollection.php
index b2fcf9f15f9678819f023998d24296da83b8f703..0ac5ae10d0ae06feaa21185e83a7be70d26a064d 100644
--- a/module/VuFindSearch/src/VuFindSearch/Response/AbstractRecordCollection.php
+++ b/module/VuFindSearch/src/VuFindSearch/Response/AbstractRecordCollection.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
@@ -165,7 +165,7 @@ abstract class AbstractRecordCollection implements RecordCollectionInterface
     /**
      * Return true if current collection index is valid.
      *
-     * @return boolean
+     * @return bool
      */
     public function valid()
     {
diff --git a/module/VuFindSearch/src/VuFindSearch/Response/RecordCollectionFactoryInterface.php b/module/VuFindSearch/src/VuFindSearch/Response/RecordCollectionFactoryInterface.php
index 13b239bc2a66abb1434f52e0b8efbc9b652b065b..d6d46e250ff8d13410d93f445db14f5b24afc7e8 100644
--- a/module/VuFindSearch/src/VuFindSearch/Response/RecordCollectionFactoryInterface.php
+++ b/module/VuFindSearch/src/VuFindSearch/Response/RecordCollectionFactoryInterface.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category Search
  * @package  Service
diff --git a/module/VuFindSearch/src/VuFindSearch/Response/RecordCollectionInterface.php b/module/VuFindSearch/src/VuFindSearch/Response/RecordCollectionInterface.php
index 691724271af7f2c20634aaf02cb33a9f67e94c26..ef08511ee593637f2ed44f5608c8a218d235bf1b 100644
--- a/module/VuFindSearch/src/VuFindSearch/Response/RecordCollectionInterface.php
+++ b/module/VuFindSearch/src/VuFindSearch/Response/RecordCollectionInterface.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category Search
  * @package  Service
diff --git a/module/VuFindSearch/src/VuFindSearch/Response/RecordInterface.php b/module/VuFindSearch/src/VuFindSearch/Response/RecordInterface.php
index 4f4d7ffd8bae2f1c552fb9221aaa9b8251866e60..d9a66fc356910eda41d3e836712652ba45dd028d 100644
--- a/module/VuFindSearch/src/VuFindSearch/Response/RecordInterface.php
+++ b/module/VuFindSearch/src/VuFindSearch/Response/RecordInterface.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category Search
  * @package  Service
diff --git a/module/VuFindSearch/src/VuFindSearch/Service.php b/module/VuFindSearch/src/VuFindSearch/Service.php
index fb118b6495b551a878bbf4aba277b7529a7e8af5..8fc4fe86b7d117c7c2868253550c91ff5025379d 100644
--- a/module/VuFindSearch/src/VuFindSearch/Service.php
+++ b/module/VuFindSearch/src/VuFindSearch/Service.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
@@ -87,8 +87,8 @@ class Service
      *
      * @param string              $backend Search backend identifier
      * @param Query\AbstractQuery $query   Search query
-     * @param integer             $offset  Search offset
-     * @param integer             $limit   Search limit
+     * @param int                 $offset  Search offset
+     * @param int                 $limit   Search limit
      * @param ParamBag            $params  Search backend parameters
      *
      * @return RecordCollectionInterface
@@ -196,7 +196,7 @@ class Service
      *
      * @param string              $backend Search backend identifier
      * @param Query\AbstractQuery $query   Search query
-     * @param integer             $limit   Search limit
+     * @param int                 $limit   Search limit
      * @param ParamBag            $params  Search backend parameters
      *
      * @return RecordCollectionInterface
diff --git a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/AbstractHandlerMapTest.php b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/AbstractHandlerMapTest.php
index d9ec68e105692c6819a43865ad5e6e1b71f8e471..570eb5551a4dd9c270d31a2b6106944a0918b56a 100644
--- a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/AbstractHandlerMapTest.php
+++ b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/AbstractHandlerMapTest.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/EDS/BackendTest.php b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/EDS/BackendTest.php
index d220a3c1a889d6f7e1e0bcc631d05304fa5998ed..b5e01ac1e35ad4f8d4983faf3d691056008bec4a 100644
--- a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/EDS/BackendTest.php
+++ b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/EDS/BackendTest.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/EDS/QueryBuilderTest.php b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/EDS/QueryBuilderTest.php
index f307944cc6609cedea1088c94ce49f44e1a1bf13..e88d4154a07e803a677c1806aac0a34ae70575c4 100644
--- a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/EDS/QueryBuilderTest.php
+++ b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/EDS/QueryBuilderTest.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/EDS/Response/RecordCollectionFactoryTest.php b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/EDS/Response/RecordCollectionFactoryTest.php
index 074d59449ba516e7b44261cc84821656d0860f76..f91c11918944d059ab645f896fdd78148c5fda00 100644
--- a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/EDS/Response/RecordCollectionFactoryTest.php
+++ b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/EDS/Response/RecordCollectionFactoryTest.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/EDS/Response/RecordCollectionTest.php b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/EDS/Response/RecordCollectionTest.php
index 1920836271dd41ff73b7f572fd5c6cabd99f24bf..47732f573ea5393445f4a2e4d9ed8435aa8b8727 100644
--- a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/EDS/Response/RecordCollectionTest.php
+++ b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/EDS/Response/RecordCollectionTest.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/EIT/BackendTest.php b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/EIT/BackendTest.php
index 8d1789a167411601506cfcc1ff2f21bb5ae0fc74..504a1b632b8711af6c257104e8c5b95ccf8552c8 100644
--- a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/EIT/BackendTest.php
+++ b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/EIT/BackendTest.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/EIT/QueryBuilderTest.php b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/EIT/QueryBuilderTest.php
index 6ccc2c8be64af385480c89684b3ffbcb315e2f1c..f14d9ae673d1fb7cb8e0246e508a3642a8f1bb13 100644
--- a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/EIT/QueryBuilderTest.php
+++ b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/EIT/QueryBuilderTest.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/EIT/Response/XML/RecordCollectionFactoryTest.php b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/EIT/Response/XML/RecordCollectionFactoryTest.php
index 0cf0907aa2a60ced11dbf9c26e3af3afbd2f9e55..b7ba078f7da6786cb48c24a4644e3de9e8327149 100644
--- a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/EIT/Response/XML/RecordCollectionFactoryTest.php
+++ b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/EIT/Response/XML/RecordCollectionFactoryTest.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/EIT/Response/XML/RecordCollectionTest.php b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/EIT/Response/XML/RecordCollectionTest.php
index edf001477a3becdbff5d4a69c411c402d91d0130..b761feffc347e175b26b3e50528a42014cf39520 100644
--- a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/EIT/Response/XML/RecordCollectionTest.php
+++ b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/EIT/Response/XML/RecordCollectionTest.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/LibGuides/BackendTest.php b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/LibGuides/BackendTest.php
index af807699cd210246cf6d85482890bb4a46a410cd..1a92ff7ee17f5c51576d972e899df7ecaa399b44 100644
--- a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/LibGuides/BackendTest.php
+++ b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/LibGuides/BackendTest.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/LibGuides/QueryBuilderTest.php b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/LibGuides/QueryBuilderTest.php
index 4d6b732a3993150e4a4f5faa44f8dc25f97944c4..f52be91b602f8ad204d58d975b4c5413d3856e9b 100644
--- a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/LibGuides/QueryBuilderTest.php
+++ b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/LibGuides/QueryBuilderTest.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/LibGuides/Response/RecordCollectionFactoryTest.php b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/LibGuides/Response/RecordCollectionFactoryTest.php
index 3891fcfcd53ad1288a657051139c526833bddc61..519a7f5e38626b019112cfcc239814bfad50f87a 100644
--- a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/LibGuides/Response/RecordCollectionFactoryTest.php
+++ b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/LibGuides/Response/RecordCollectionFactoryTest.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/LibGuides/Response/RecordCollectionTest.php b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/LibGuides/Response/RecordCollectionTest.php
index 161a3a6859ff6b56d315333f229c8f706bbdfa51..3a0fec7e81cfe0fe81d5a3f9fad58526fed45902 100644
--- a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/LibGuides/Response/RecordCollectionTest.php
+++ b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/LibGuides/Response/RecordCollectionTest.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Pazpar2/BackendTest.php b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Pazpar2/BackendTest.php
index 603fc0de8cbf9e25a9e6dfe04b08526ded8c8585..7f5d0fc23407b637f73432a592b3fcffd193aefb 100644
--- a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Pazpar2/BackendTest.php
+++ b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Pazpar2/BackendTest.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Primo/BackendTest.php b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Primo/BackendTest.php
index 6930617970a48797c324d9553d0386362b80b729..0de237bb03fe90848d7b7a5203f9aa029dffa0f4 100644
--- a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Primo/BackendTest.php
+++ b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Primo/BackendTest.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Primo/ConnectorTest.php b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Primo/ConnectorTest.php
index cf73cf71d4ba4f7cb0e9721d0db321f261c970d4..a36e58485e55ba2d01ff4f5e888099888dd5174e 100644
--- a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Primo/ConnectorTest.php
+++ b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Primo/ConnectorTest.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Primo/QueryBuilderTest.php b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Primo/QueryBuilderTest.php
index 97992254a565b80d4865dca15c92a8ba5a083f7d..080d421e27bf04c886bb783d741a8ea70eff2e71 100644
--- a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Primo/QueryBuilderTest.php
+++ b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Primo/QueryBuilderTest.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Primo/Response/RecordCollectionFactoryTest.php b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Primo/Response/RecordCollectionFactoryTest.php
index e6263a7ddf30145be99412189f078508fa2939e7..57bbcd70b1f941f26bdcc656480bd21a94225992 100644
--- a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Primo/Response/RecordCollectionFactoryTest.php
+++ b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Primo/Response/RecordCollectionFactoryTest.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Primo/Response/RecordCollectionTest.php b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Primo/Response/RecordCollectionTest.php
index c386fc88070586a1e9eb5b9d17a53d79203026ba..3feb618e2cdffecee337bf57a04f05a12c41f6cd 100644
--- a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Primo/Response/RecordCollectionTest.php
+++ b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Primo/Response/RecordCollectionTest.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/BackendTest.php b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/BackendTest.php
index 5ce5128f7df1249ba6d902a6d0d36f28850e21b1..b49adf79a0c69b912782d392f06bcf74ed277745 100644
--- a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/BackendTest.php
+++ b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/BackendTest.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/ConnectorTest.php b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/ConnectorTest.php
index 873747e04cb66e29538b3ee466800ade2d143d15..7f8b7f516271e5edac1ee0e99cd6e83829d7b937 100644
--- a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/ConnectorTest.php
+++ b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/ConnectorTest.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/Document/CommitDocumentTest.php b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/Document/CommitDocumentTest.php
index f7b9d23739758f158b916e7522095c2a9a41f296..ce07259ba25a94e7c296982578874a7156ebff32 100644
--- a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/Document/CommitDocumentTest.php
+++ b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/Document/CommitDocumentTest.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/Document/DeleteDocumentTest.php b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/Document/DeleteDocumentTest.php
index 3eb70de70704ea89f122c11407e75eb8fd6a9387..862764d51be601358a353641cf4f3910cb677718 100644
--- a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/Document/DeleteDocumentTest.php
+++ b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/Document/DeleteDocumentTest.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/Document/OptimizeDocumentTest.php b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/Document/OptimizeDocumentTest.php
index 2133c28158425763a60706585a0e457d3fdc67af..d05715917f304703cac28630fa2ed0e6289f6673 100644
--- a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/Document/OptimizeDocumentTest.php
+++ b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/Document/OptimizeDocumentTest.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/Document/RawXMLDocumentTest.php b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/Document/RawXMLDocumentTest.php
index e0e22cf057b70ea23bf6303f6467f2f5dc2e3a91..0e352327b7d60976c280a2e959c1ba223d952dc8 100644
--- a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/Document/RawXMLDocumentTest.php
+++ b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/Document/RawXMLDocumentTest.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/Document/UpdateDocumentTest.php b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/Document/UpdateDocumentTest.php
index bb174a1c7815817d5fa98488d80bb1a5cf12f400..16696c5d3daa642da4c75dac43ed10909ac19a2b 100644
--- a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/Document/UpdateDocumentTest.php
+++ b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/Document/UpdateDocumentTest.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/HandlerMapTest.php b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/HandlerMapTest.php
index 148e7ee3de82b63188cda2ad8d0c213c0fe41111..18ec025e71c45d5caa11b6036e0eefaa2faef43c 100644
--- a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/HandlerMapTest.php
+++ b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/HandlerMapTest.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/LuceneSyntaxHelperTest.php b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/LuceneSyntaxHelperTest.php
index 23fab9e15b97a1bde3acb531a9463d88c9938f41..356094b739e8fa75c4c0285b630cda30e279c47b 100644
--- a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/LuceneSyntaxHelperTest.php
+++ b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/LuceneSyntaxHelperTest.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
@@ -342,4 +342,38 @@ class LuceneSyntaxHelperTest extends \VuFindTest\Unit\TestCase
             $expected, $lh->normalizeSearchString($input)
         );
     }
+
+    /**
+     * Test search term extraction
+     *
+     * @return void
+     */
+    public function testExtractSearchTerms()
+    {
+        $lh = new LuceneSyntaxHelper(false, false);
+        $tests = [
+            'keyword' => 'keyword',
+            'two keywords' => 'two keywords',
+            'index:keyword' => 'keyword',
+            'index:keyword anotherkeyword' => 'keyword anotherkeyword',
+            'index:keyword anotherindex:anotherkeyword' => 'keyword anotherkeyword',
+            '(index:keyword)' => 'keyword',
+            'index:(keyword1 keyword2)' => '(keyword1 keyword2)',
+            '{!local params}keyword' => 'keyword',
+            'keyword~' => 'keyword',
+            'keyword~0.8' => 'keyword',
+            'keyword keyword2^20' => 'keyword keyword2',
+            '"keyword keyword2 keyword3"~2' => '"keyword keyword2 keyword3"',
+            '"kw1 kw2 kw3"~2 kw4^200' => '"kw1 kw2 kw3" kw4',
+            '+keyword -keyword2^20' => 'keyword keyword2',
+            'index:+keyword index2:-keyword2^20' => 'keyword keyword2',
+            'index:[start TO end]' => '[start TO end]',
+            'index:{start TO end}' => '{start TO end}',
+            'es\\"caped field:test' => 'es\\"caped test'
+        ];
+        foreach ($tests as $input => $expected)
+        $this->assertEquals(
+            $expected, $lh->extractSearchTerms($input)
+        );
+    }
 }
diff --git a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/QueryBuilderTest.php b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/QueryBuilderTest.php
index 8e12024ca03d1448090e8c5be62f57d1bca17a52..f966c234a59b5d3bef4faff3f3c00d92f8894ee9 100644
--- a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/QueryBuilderTest.php
+++ b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/QueryBuilderTest.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
@@ -102,24 +102,64 @@ class QueryBuilderTest extends \VuFindTest\Unit\TestCase
     }
 
     /**
-     * Test generation with a query handler
+     * Return array of [test query, expected result] arrays.
      *
-     * @return void
+     * @return array
      */
-    public function testQueryHandler()
+    protected function getQuestionTests()
     {
-        // Set up an array of expected inputs and outputs:
         // @codingStandardsIgnoreStart
-        $tests = [
-            ['this?', '((this?) OR (this\?))'],// trailing question mark
+        return [
+            ['this?', '(this?) OR (this\?)'], // trailing question mark
+            ['this? that', '((this?) OR (this\?)) that'], // question mark after first word
+            ['start this? that', 'start ((this?) OR (this\?)) that'], // question mark after the middle word
+            ['start AND this? AND that', 'start AND ((this?) OR (this\?)) AND that'], // question mark with boolean operators
+            ['start t?his that', 'start t?his that'], // question mark as a wildcard in the middle of a word
+            ['start? this?', '((start?) OR (start\?)) ((this?) OR (this\?))'], // multiple ? terms
+            ['this? that? this?', '((this?) OR (this\?)) ((that?) OR (that\?)) ((this?) OR (this\?))'], // repeating ? term
+            ['"this? that?"', '"this? that?"'], // ? terms inside quoted phrase
         ];
         // @codingStandardsIgnoreEnd
+    }
 
+    /**
+     * Test generation with a query handler
+     *
+     * @return void
+     */
+    public function testQueryHandler()
+    {
+        // Set up an array of expected inputs and outputs:
+        $tests = $this->getQuestionTests();
         $qb = new QueryBuilder(
             [
                 'test' => []
             ]
         );
+        foreach ($tests as $test) {
+            list($input, $output) = $test;
+            $q = new Query($input, 'test');
+            $response = $qb->build($q);
+            $processedQ = $response->get('q');
+            $this->assertEquals('(' . $output . ')', $processedQ[0]);
+        }
+    }
+
+    /**
+     * Test generation with a query handler with edismax
+     *
+     * @return void
+     */
+    public function testQueryHandlerWithEdismax()
+    {
+        // Set up an array of expected inputs and outputs:
+        $tests = $this->getQuestionTests();
+
+        $qb = new QueryBuilder(
+            [
+                'test' => ['DismaxHandler' => 'edismax', 'DismaxFields' => ['foo']]
+            ]
+        );
         foreach ($tests as $test) {
             list($input, $output) = $test;
             $q = new Query($input, 'test');
diff --git a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/Record/SerializableRecordTest.php b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/Record/SerializableRecordTest.php
index 4a7ecf00843b88f9e2acbf6b831e377eee4ca4a4..96502d7bf2543dd441ebc529e43f35a0ae7da23c 100644
--- a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/Record/SerializableRecordTest.php
+++ b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/Record/SerializableRecordTest.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/Response/Json/FacetsTest.php b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/Response/Json/FacetsTest.php
index 1e54dd0637799b7889ff171c5aaa6e403c472eb5..5ed80ee0133233924eb1beacd1320a7619fb4b20 100644
--- a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/Response/Json/FacetsTest.php
+++ b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/Response/Json/FacetsTest.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/Response/Json/NamedListTest.php b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/Response/Json/NamedListTest.php
index 1527c25c77f084556ba40b7824973371894bee08..9c988ff0fa091c6694eb365a046e4f07ff84072a 100644
--- a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/Response/Json/NamedListTest.php
+++ b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/Response/Json/NamedListTest.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/Response/Json/RecordCollectionFactoryTest.php b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/Response/Json/RecordCollectionFactoryTest.php
index 70c14abb0e7032e7f1e8e4beb48206e9b3271666..b0d6917766051bd64c5110109b983795ad406e5d 100644
--- a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/Response/Json/RecordCollectionFactoryTest.php
+++ b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/Response/Json/RecordCollectionFactoryTest.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/Response/Json/RecordCollectionTest.php b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/Response/Json/RecordCollectionTest.php
index 1f9d366f7f39de415f7a8716db761537dc9839e6..e33db1d255c297c53fadf0c216dede9cfbc6da67 100644
--- a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/Response/Json/RecordCollectionTest.php
+++ b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/Response/Json/RecordCollectionTest.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/Response/Json/SpellcheckTest.php b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/Response/Json/SpellcheckTest.php
index 9bfdda26a5f8b25a879de07ebf057a702b6ec9e2..1910336b25fa1dcdaee35cdc6ca4cf9099d511a0 100644
--- a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/Response/Json/SpellcheckTest.php
+++ b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/Response/Json/SpellcheckTest.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/Response/Json/TermsTest.php b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/Response/Json/TermsTest.php
index 33f0b105af4c013731516de7d6a76ac82e0a774d..6bacfc600ef4936e8374b5c4f77be0a69663e7ec 100644
--- a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/Response/Json/TermsTest.php
+++ b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/Response/Json/TermsTest.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/SearchHandlerTest.php b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/SearchHandlerTest.php
index 25d4de15591f79494d842cb5b7e2424488ebde09..cecbd0fdef01edd5d77ed562a0db8bffb635e461 100644
--- a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/SearchHandlerTest.php
+++ b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/SearchHandlerTest.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/SimilarBuilderTest.php b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/SimilarBuilderTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..1f07a0694f5b97986c8ce9ea065af23f29de5237
--- /dev/null
+++ b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Solr/SimilarBuilderTest.php
@@ -0,0 +1,114 @@
+<?php
+
+/**
+ * Unit tests for SOLR similar records query builder
+ *
+ * PHP version 5
+ *
+ * Copyright (C) Villanova University 2010.
+ * Copyright (C) The National Library of Finland 2016.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ * @category VuFind
+ * @package  Search
+ * @author   David Maus <maus@hab.de>
+ * @author   Ere Maijala <ere.maijala@helsinki.fi>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     https://vufind.org
+ */
+namespace VuFindTest\Backend\Solr;
+
+use VuFindSearch\Backend\Solr\SimilarBuilder;
+
+/**
+ * Unit tests for SOLR similar records query builder
+ *
+ * @category VuFind
+ * @package  Search
+ * @author   David Maus <maus@hab.de>
+ * @author   Ere Maijala <ere.maijala@helsinki.fi>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     https://vufind.org
+ */
+class SimilarBuilderTest extends \VuFindTest\Unit\TestCase
+{
+    /**
+     * Test builder with default params.
+     *
+     * @return void
+     */
+    public function testDefaultParams()
+    {
+        $sb = new SimilarBuilder();
+        $response = $sb->build('testrecord');
+        $rows = $response->get('rows');
+        $this->assertEquals(5, $rows[0]);
+        $q = $response->get('q');
+        $this->assertEquals('id:"testrecord"', $q[0]);
+        $qt = $response->get('qt');
+        $this->assertEquals('morelikethis', $qt[0]);
+    }
+
+    /**
+     * Test builder with alternative id field.
+     *
+     * @return void
+     */
+    public function testAlternativeIdField()
+    {
+        $sb = new SimilarBuilder(null, 'key');
+        $response = $sb->build('testrecord');
+        $q = $response->get('q');
+        $this->assertEquals('key:"testrecord"', $q[0]);
+    }
+
+    /**
+     * Test builder with different configurations.
+     *
+     * @return void
+     */
+    public function testMltConfig()
+    {
+        $config = [
+            'MoreLikeThis' => [
+                'count' => 10
+            ]
+        ];
+        $sb = new SimilarBuilder(new \Zend\Config\Config($config));
+        $response = $sb->build('testrecord');
+        $rows = $response->get('rows');
+        $this->assertEquals(10, $rows[0]);
+
+        $config['MoreLikeThis']['useMoreLikeThisHandler'] = true;
+        $sb = new SimilarBuilder(new \Zend\Config\Config($config));
+        $response = $sb->build('testrecord');
+        $rows = $response->get('rows');
+        $this->assertEquals(10, $rows[0]);
+        $q = $response->get('q');
+        $this->assertEquals(
+            '{!mlt qf=title,title_short,callnumber-label,topic,language,author,'
+            . 'publishDate mintf=1 mindf=1}testrecord',
+            $q[0]
+        );
+        $qt = $response->get('qt');
+        $this->assertEquals(null, $qt);
+
+        $config['MoreLikeThis']['params'] = 'qf=title,topic';
+        $sb = new SimilarBuilder(new \Zend\Config\Config($config));
+        $response = $sb->build('testrecord');
+        $q = $response->get('q');
+        $this->assertEquals('{!mlt qf=title,topic}testrecord', $q[0]);
+    }
+}
diff --git a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Summon/BackendTest.php b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Summon/BackendTest.php
index 431c634ade982e72b6d9b1cbbe9d49f83d3ae4de..cdd350d7382dbd3d2232bbda29774f2e0e055d01 100644
--- a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Summon/BackendTest.php
+++ b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Summon/BackendTest.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Summon/QueryBuilderTest.php b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Summon/QueryBuilderTest.php
index 2d814dd6a49b7c319bb647c131a3919de75ba550..1e6c5d5e6a6699ee3f120350d28be382cf3ba485 100644
--- a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Summon/QueryBuilderTest.php
+++ b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Summon/QueryBuilderTest.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Summon/Response/RecordCollectionFactoryTest.php b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Summon/Response/RecordCollectionFactoryTest.php
index 14b691fd1a9c86465610f87966be86b2b6f03af1..65d363568a5556a63d68f29276e73161d6874a7c 100644
--- a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Summon/Response/RecordCollectionFactoryTest.php
+++ b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Summon/Response/RecordCollectionFactoryTest.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Summon/Response/RecordCollectionTest.php b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Summon/Response/RecordCollectionTest.php
index ed4efd5ac0981b089347ab0bd588e94f11009fa6..87ad7a3018c9be102c2b101cd4e8d57d96c0af1a 100644
--- a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Summon/Response/RecordCollectionTest.php
+++ b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/Summon/Response/RecordCollectionTest.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/WorldCat/BackendTest.php b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/WorldCat/BackendTest.php
index 13aabdb31799ddf915d60cf4217ec3129a723d6d..2b9e8b29f04eee66acc56805bbfccefdae62715f 100644
--- a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/WorldCat/BackendTest.php
+++ b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/WorldCat/BackendTest.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/WorldCat/ConnectorTest.php b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/WorldCat/ConnectorTest.php
index e651f75b6e7e62f41b25ba8b3d1046e5b63985fc..6c1870124d787875b5fa43b6da5113a4eac0f839 100644
--- a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/WorldCat/ConnectorTest.php
+++ b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/WorldCat/ConnectorTest.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/WorldCat/QueryBuilderTest.php b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/WorldCat/QueryBuilderTest.php
index 7846887a41fe54290d7bd62c6882847df5aa3d30..90be8a707517c669afb7e4c55dfa4bdeac7f0d08 100644
--- a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/WorldCat/QueryBuilderTest.php
+++ b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/WorldCat/QueryBuilderTest.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/WorldCat/Response/XML/RecordCollectionFactoryTest.php b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/WorldCat/Response/XML/RecordCollectionFactoryTest.php
index ea91098a328848248d030e67d4df2c46978b3285..9f5334f65bc69fee104f856b1d82a8b2fec92117 100644
--- a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/WorldCat/Response/XML/RecordCollectionFactoryTest.php
+++ b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Backend/WorldCat/Response/XML/RecordCollectionFactoryTest.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/ParamBagTest.php b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/ParamBagTest.php
index 558416983217e4d08b717fa5a9ac304b7ddede94..482b69f42141edb6e35538435966eee867ac01e4 100644
--- a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/ParamBagTest.php
+++ b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/ParamBagTest.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Query/QueryGroupTest.php b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Query/QueryGroupTest.php
index 32ac27bedd92dd0e199e016f7fe818ac85b98b89..8b91ac0200d5738bb1bb8c84d68a1c3b258d5b99 100644
--- a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Query/QueryGroupTest.php
+++ b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Query/QueryGroupTest.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Query/QueryTest.php b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Query/QueryTest.php
index a035877b080233224838ab002506ea7d89c4dcf1..7678581b8fb0f81521d5d124a2c566512359e22e 100644
--- a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Query/QueryTest.php
+++ b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/Query/QueryTest.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
@@ -49,11 +49,16 @@ class QueryTest extends PHPUnit_Framework_TestCase
      */
     public function testContainsTerm()
     {
-        $q = new Query('test query');
+        $q = new Query('test query we<(ird and/or');
 
-        // Should contain both actual terms:
+        // Should contain all actual terms (even those containing regex chars):
         $this->assertTrue($q->containsTerm('test'));
         $this->assertTrue($q->containsTerm('query'));
+        $this->assertTrue($q->containsTerm('we<(ird'));
+        // A slash can be a word boundary but also a single term
+        $this->assertTrue($q->containsTerm('and'));
+        $this->assertTrue($q->containsTerm('or'));
+        $this->assertTrue($q->containsTerm('and/or'));
 
         // Should not contain a non-present term:
         $this->assertFalse($q->containsTerm('garbage'));
@@ -62,6 +67,23 @@ class QueryTest extends PHPUnit_Framework_TestCase
         $this->assertFalse($q->containsTerm('tes'));
     }
 
+    /**
+     * Test replaceTerm() method
+     *
+     * @return void
+     */
+    public function testReplaceTerm()
+    {
+        $q = new Query('test query we<(ird and/or');
+        $q->replaceTerm('we<(ird', 'we>(ird');
+        $q->replaceTerm('and/or', 'and-or');
+        $this->assertEquals('test query we>(ird and-or', $q->getString());
+
+        $q = new Query('test query we<(ird and/or');
+        $q->replaceTerm('and', 'not');
+        $this->assertEquals('test query we<(ird not/or', $q->getString());
+    }
+
     /**
      * Test setHandler() method
      *
diff --git a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/SearchServiceTest.php b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/SearchServiceTest.php
index 792cbb26263fcb840b9eac15730e7ab8eeb05df1..c55501c9f42eb158970687de96fae0dd1288be2d 100644
--- a/module/VuFindSearch/tests/unit-tests/src/VuFindTest/SearchServiceTest.php
+++ b/module/VuFindSearch/tests/unit-tests/src/VuFindTest/SearchServiceTest.php
@@ -18,7 +18,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Search
diff --git a/module/VuFindTheme/Module.php b/module/VuFindTheme/Module.php
index 3df7a794cb31268e629df6af7a912834e76a5f50..55f5d0a4952e441f56d624833e0e4c54db352eaf 100644
--- a/module/VuFindTheme/Module.php
+++ b/module/VuFindTheme/Module.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Theme
diff --git a/module/VuFindTheme/src/VuFindTheme/Initializer.php b/module/VuFindTheme/src/VuFindTheme/Initializer.php
index 2ec816b5edd5d0d5263d319da4a7b80db7171c7e..563625e4747c7c05c152850799b76c6b3ec14d0b 100644
--- a/module/VuFindTheme/src/VuFindTheme/Initializer.php
+++ b/module/VuFindTheme/src/VuFindTheme/Initializer.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Theme
diff --git a/module/VuFindTheme/src/VuFindTheme/InjectTemplateListener.php b/module/VuFindTheme/src/VuFindTheme/InjectTemplateListener.php
index 79a340be9914b2bdc5ee99ba05202b99dc7e381a..b074c98ec320f9d01d90107e85a98ab0a18302fd 100644
--- a/module/VuFindTheme/src/VuFindTheme/InjectTemplateListener.php
+++ b/module/VuFindTheme/src/VuFindTheme/InjectTemplateListener.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Theme
diff --git a/module/VuFindTheme/src/VuFindTheme/LessCompiler.php b/module/VuFindTheme/src/VuFindTheme/LessCompiler.php
index 17c4b3830cd8afa17c946ea6185d92821df0e1cb..68f2e6eaa29c56aa8964f0e5e52d88b178ae4192 100644
--- a/module/VuFindTheme/src/VuFindTheme/LessCompiler.php
+++ b/module/VuFindTheme/src/VuFindTheme/LessCompiler.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Theme
diff --git a/module/VuFindTheme/src/VuFindTheme/Mobile.php b/module/VuFindTheme/src/VuFindTheme/Mobile.php
index b0a92e48c89fef43c942a337f08abb2a376eb952..ce148f2b820f53b854296136a7eaee0e200e589b 100644
--- a/module/VuFindTheme/src/VuFindTheme/Mobile.php
+++ b/module/VuFindTheme/src/VuFindTheme/Mobile.php
@@ -23,7 +23,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Theme
diff --git a/module/VuFindTheme/src/VuFindTheme/ResourceContainer.php b/module/VuFindTheme/src/VuFindTheme/ResourceContainer.php
index 4dedc575e30f51cd438b16f0801ef08b903bd81f..c0573975002d964d9318cde443855ba3ebdc4cf0 100644
--- a/module/VuFindTheme/src/VuFindTheme/ResourceContainer.php
+++ b/module/VuFindTheme/src/VuFindTheme/ResourceContainer.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Theme
@@ -238,7 +238,7 @@ class ResourceContainer
      *
      * @param string $file Filename to check
      *
-     * @return boolean
+     * @return bool
      */
     protected function dynamicallyParsed($file)
     {
@@ -255,7 +255,7 @@ class ResourceContainer
      *
      * @param string $file Filename to remove
      *
-     * @return boolean
+     * @return bool
      */
     protected function removeCSS($file)
     {
diff --git a/module/VuFindTheme/src/VuFindTheme/ThemeInfo.php b/module/VuFindTheme/src/VuFindTheme/ThemeInfo.php
index 2501a05a7fba1e87e2b50ae925f31ba0ee430d90..43e0731c11643c2c683ce64cd67ef2af1dcac214 100644
--- a/module/VuFindTheme/src/VuFindTheme/ThemeInfo.php
+++ b/module/VuFindTheme/src/VuFindTheme/ThemeInfo.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Theme
diff --git a/module/VuFindTheme/src/VuFindTheme/View/Helper/ConcatTrait.php b/module/VuFindTheme/src/VuFindTheme/View/Helper/ConcatTrait.php
new file mode 100644
index 0000000000000000000000000000000000000000..db948a36f30879645efd1d41731f4f5010583a50
--- /dev/null
+++ b/module/VuFindTheme/src/VuFindTheme/View/Helper/ConcatTrait.php
@@ -0,0 +1,328 @@
+<?php
+/**
+ * Trait to add asset pipeline functionality (concatenation / minification) to
+ * a HeadLink/HeadScript-style view helper.
+ *
+ * PHP version 5
+ *
+ * Copyright (C) Villanova University 2016.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ *
+ * @category VuFind
+ * @package  View_Helpers
+ * @author   Demian Katz <demian.katz@villanova.edu>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     https://vufind.org/wiki/development:testing:unit_tests Wiki
+ */
+namespace VuFindTheme\View\Helper;
+use VuFindTheme\ThemeInfo;
+
+/**
+ * Trait to add asset pipeline functionality (concatenation / minification) to
+ * a HeadLink/HeadScript-style view helper.
+ *
+ * @category VuFind
+ * @package  View_Helpers
+ * @author   Demian Katz <demian.katz@villanova.edu>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     https://vufind.org/wiki/development:testing:unit_tests Wiki
+ */
+trait ConcatTrait
+{
+    /**
+     * Returns true if file should not be included in the compressed concat file
+     *
+     * @param stdClass $item Element object
+     *
+     * @return bool
+     */
+    abstract protected function isExcludedFromConcat($item);
+
+    /**
+     * Get the folder name and file extension
+     *
+     * @return string
+     */
+    abstract protected function getFileType();
+
+    /**
+     * Get the file path from the element object
+     *
+     * @param stdClass $item Element object
+     *
+     * @return string
+     */
+    abstract protected function getResourceFilePath($item);
+
+    /**
+     * Set the file path of the element object
+     *
+     * @param stdClass $item Element object
+     * @param string   $path New path string
+
+     * @return void
+     */
+    abstract protected function setResourceFilePath($item, $path);
+
+    /**
+     * Get the minifier that can handle these file types
+     *
+     * @return minifying object like \MatthiasMullie\Minify\JS
+     */
+    abstract protected function getMinifier();
+
+    /**
+     * Set the file path of the link object
+     *
+     * @param stdClass $item Link element object
+     *
+     * @return string
+     */
+    public function getType($item)
+    {
+        return 'default';
+    }
+
+    /**
+     * Should we use the asset pipeline to join files together and minify them?
+     *
+     * @var bool
+     */
+    protected $usePipeline = false;
+
+    /**
+     * Array of resource items by type, contains key as well
+     *
+     * @var array
+     */
+    protected $groups = [];
+
+    /**
+     * Future order of the concatenated file
+     *
+     * @var number
+     */
+    protected $concatIndex = null;
+
+    /**
+     * Check if config is enamled for this file type
+     *
+     * @param string|bool $config Config for current application environment
+     *
+     * @return bool
+     */
+    protected function enabledInConfig($config)
+    {
+        if ($config === false || $config == 'off') {
+            return false;
+        }
+        if ($config == '*' || $config == 'on'
+            || $config == 'true' || $config === true
+        ) {
+            return true;
+        }
+        $settings = array_map('trim', explode(',', $config));
+        return in_array($this->getFileType(), $settings);
+    }
+
+    /**
+     * Initialize class properties related to concatenation of resources.
+     * All of the elements to be concatenated into ($this->concatItems)
+     * and those that need to remain on their own ($this->otherItems).
+     *
+     * @return void
+     */
+    protected function filterItems()
+    {
+        $this->groups = [];
+        $groupTypes = [];
+
+        $this->getContainer()->ksort();
+
+        foreach ($this as $key => $item) {
+            if ($this->isExcludedFromConcat($item)) {
+                $this->groups[] = [
+                    'other' => true,
+                    'item' => $item
+                ];
+                $groupTypes[] = 'other';
+                continue;
+            }
+
+            $details = $this->themeInfo->findContainingTheme(
+                $this->getFileType() . '/' . $this->getResourceFilePath($item),
+                ThemeInfo::RETURN_ALL_DETAILS
+            );
+
+            $type = $this->getType($item);
+            $index = array_search($type, $groupTypes);
+            if ($index === false) {
+                $this->groups[] = [
+                    'items' => [$item],
+                    'key' => $details['path'] . filemtime($details['path'])
+                ];
+                $groupTypes[] = $type;
+            } else {
+                $this->groups[$index]['items'][] = $item;
+                $this->groups[$index]['key'] .=
+                    $details['path'] . filemtime($details['path']);
+            }
+        }
+
+        return count($groupTypes) > 0;
+    }
+
+    /**
+     * Get the path to the directory where we can cache files generated by
+     * this trait.
+     *
+     * @return string
+     */
+    protected function getResourceCacheDir()
+    {
+        return $this->themeInfo->getBaseDir() . '/../local/cache/public/';
+    }
+
+    /**
+     * Using the concatKey, return the path of the concatenated file.
+     * Generate if it does not yet exist.
+     *
+     * @param array $group Object containing 'key' and stdobj file 'items'
+     *
+     * @return string
+     */
+    protected function getConcatenatedFilePath($group)
+    {
+        $urlHelper = $this->getView()->plugin('url');
+
+        // Don't recompress individual files
+        if (count($group['items']) === 1) {
+            $path = $this->getResourceFilePath($group['items'][0]);
+            $details = $this->themeInfo->findContainingTheme(
+                $this->getFileType() . '/' . $path,
+                ThemeInfo::RETURN_ALL_DETAILS
+            );
+            return $urlHelper('home') . 'themes/' . $details['theme']
+                . '/' . $this->getFileType() . '/' . $path;
+        }
+        // Locate/create concatenated asset file
+        $filename = md5($group['key']) . '.min.' . $this->getFileType();
+        $concatPath = $this->getResourceCacheDir() . $filename;
+        if (!file_exists($concatPath)) {
+            $minifier = $this->getMinifier();
+            foreach ($group['items'] as $item) {
+                $details = $this->themeInfo->findContainingTheme(
+                    $this->getFileType() . '/' . $this->getResourceFilePath($item),
+                    ThemeInfo::RETURN_ALL_DETAILS
+                );
+                $minifier->add($details['path']);
+            }
+            $minifier->minify($concatPath);
+        }
+
+        return $urlHelper('home') . 'cache/' . $filename;
+    }
+
+    /**
+     * Process and return items in index order
+     *
+     * @param string|int $indent Amount of whitespace/string to use for indention
+     *
+     * @return string
+     */
+    protected function outputInOrder($indent)
+    {
+        // Some of this logic was copied from HeadScript; it does not all apply
+        // when incorporated into HeadLink, but it has no harmful side effects.
+        $indent = (null !== $indent)
+            ? $this->getWhitespace($indent)
+            : $this->getIndent();
+
+        if ($this->view) {
+            $useCdata = $this->view->plugin('doctype')->isXhtml();
+        } else {
+            $useCdata = $this->useCdata;
+        }
+
+        $escapeStart = ($useCdata) ? '//<![CDATA[' : '//<!--';
+        $escapeEnd   = ($useCdata) ? '//]]>' : '//-->';
+
+        $output = [];
+        foreach ($this->groups as $group) {
+            if (isset($group['other'])) {
+                $output[] = $this->itemToString(
+                    $group['item'], $indent, $escapeStart, $escapeEnd
+                );
+            } else {
+                // Note that we  use parent::itemToString() below instead of
+                // $this->itemToString() to bypass VuFind logic that determines
+                // file paths within the theme (not appropriate for concatenated
+                // files, which are stored in a theme-independent cache).
+                $path = $this->getConcatenatedFilePath($group);
+                $item = $this->setResourceFilePath($group['items'][0], $path);
+                $output[] = parent::itemToString(
+                    $item, $indent, $escapeStart, $escapeEnd
+                );
+            }
+        }
+
+        return $indent . implode(
+            $this->escape($this->getSeparator()) . $indent, $output
+        );
+    }
+
+    /**
+     * Can we use the asset pipeline?
+     *
+     * @return bool
+     */
+    protected function isPipelineActive()
+    {
+        if ($this->usePipeline) {
+            $cacheDir = $this->getResourceCacheDir();
+            if (!is_writable($cacheDir)) {
+                $this->usePipeline = false;
+                error_log("Cannot write to $cacheDir; disabling asset pipeline.");
+            }
+        }
+        return $this->usePipeline;
+    }
+
+    /**
+     * Render link elements as string
+     * Customized to minify and concatenate
+     *
+     * @param string|int $indent Amount of whitespace or string to use for indention
+     *
+     * @return string
+     */
+    public function toString($indent = null)
+    {
+        // toString must not throw exception
+        try {
+            if (!$this->isPipelineActive() || !$this->filterItems()
+                || count($this) == 1
+            ) {
+                return parent::toString($indent);
+            }
+
+            return $this->outputInOrder($indent);
+        } catch (\Exception $e) {
+            error_log($e->getMessage());
+        }
+
+        return '';
+    }
+}
diff --git a/module/VuFindTheme/src/VuFindTheme/View/Helper/Factory.php b/module/VuFindTheme/src/VuFindTheme/View/Helper/Factory.php
index 859e7fcc0c7d0bffe6e26370b4ab7f387a48a9e3..093c00663424cd46ca2b9d12ab4ffa0c6d4b57e1 100644
--- a/module/VuFindTheme/src/VuFindTheme/View/Helper/Factory.php
+++ b/module/VuFindTheme/src/VuFindTheme/View/Helper/Factory.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  View_Helpers
@@ -41,6 +41,36 @@ use Zend\ServiceManager\ServiceManager;
  */
 class Factory
 {
+    /**
+     * Split config and return prefixed setting with current environment.
+     *
+     * @param ServiceManager $sm Service manager.
+     *
+     * @return string|bool
+     */
+    protected static function getPipelineConfig(ServiceManager $sm)
+    {
+        $config = $sm->getServiceLocator()->get('VuFind\Config')->get('config');
+        $default = false;
+        if (isset($config['Site']['asset_pipeline'])) {
+            $settings = array_map(
+                'trim',
+                explode(';', $config['Site']['asset_pipeline'])
+            );
+            foreach ($settings as $setting) {
+                $parts = array_map('trim', explode(':', $setting));
+                if (APPLICATION_ENV === $parts[0]) {
+                    return $parts[1];
+                } else if (count($parts) == 1) {
+                    $default = $parts[0];
+                } else if ($parts[0] === '*') {
+                    $default = $parts[1];
+                }
+            }
+        }
+        return $default;
+    }
+
     /**
      * Construct the HeadLink helper.
      *
@@ -51,7 +81,8 @@ class Factory
     public static function getHeadLink(ServiceManager $sm)
     {
         return new HeadLink(
-            $sm->getServiceLocator()->get('VuFindTheme\ThemeInfo')
+            $sm->getServiceLocator()->get('VuFindTheme\ThemeInfo'),
+            Factory::getPipelineConfig($sm)
         );
     }
 
@@ -65,7 +96,8 @@ class Factory
     public static function getHeadScript(ServiceManager $sm)
     {
         return new HeadScript(
-            $sm->getServiceLocator()->get('VuFindTheme\ThemeInfo')
+            $sm->getServiceLocator()->get('VuFindTheme\ThemeInfo'),
+            Factory::getPipelineConfig($sm)
         );
     }
 
@@ -107,7 +139,8 @@ class Factory
     public static function getInlineScript(ServiceManager $sm)
     {
         return new InlineScript(
-            $sm->getServiceLocator()->get('VuFindTheme\ThemeInfo')
+            $sm->getServiceLocator()->get('VuFindTheme\ThemeInfo'),
+            Factory::getPipelineConfig($sm)
         );
     }
 
diff --git a/module/VuFindTheme/src/VuFindTheme/View/Helper/HeadLink.php b/module/VuFindTheme/src/VuFindTheme/View/Helper/HeadLink.php
index 6a393b6738ae00a08c1e57e6dafa9097f1c86ede..0fbd23405da3c75e28618051df5bdcb4a373d4bc 100644
--- a/module/VuFindTheme/src/VuFindTheme/View/Helper/HeadLink.php
+++ b/module/VuFindTheme/src/VuFindTheme/View/Helper/HeadLink.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  View_Helpers
@@ -39,6 +39,8 @@ use VuFindTheme\ThemeInfo;
  */
 class HeadLink extends \Zend\View\Helper\HeadLink
 {
+    use ConcatTrait;
+
     /**
      * Theme information service
      *
@@ -49,12 +51,24 @@ class HeadLink extends \Zend\View\Helper\HeadLink
     /**
      * Constructor
      *
-     * @param ThemeInfo $themeInfo Theme information service
+     * @param ThemeInfo   $themeInfo Theme information service
+     * @param string|bool $plconfig  Config for current application environment
      */
-    public function __construct(ThemeInfo $themeInfo)
+    public function __construct(ThemeInfo $themeInfo, $plconfig = false)
     {
         parent::__construct();
         $this->themeInfo = $themeInfo;
+        $this->usePipeline = $this->enabledInConfig($plconfig);
+    }
+
+    /**
+     * Folder name and file extension for trait
+     *
+     * @return string
+     */
+    protected function getFileType()
+    {
+        return 'css';
     }
 
     /**
@@ -85,15 +99,12 @@ class HeadLink extends \Zend\View\Helper\HeadLink
     /**
      * Compile a less file to css and add to css folder
      *
-     * @param string $file                  Path to less file
-     * @param string $media                 Media type
-     * @param string $conditionalStylesheet Load condition for file
+     * @param string $file Path to less file
      *
-     * @return void
+     * @return string
      */
-    public function addLessStylesheet($file, $media = 'all',
-        $conditionalStylesheet = false
-    ) {
+    public function addLessStylesheet($file)
+    {
         $relPath = 'less/' . $file;
         $urlHelper = $this->getView()->plugin('url');
         $currentTheme = $this->themeInfo->findContainingTheme($relPath);
@@ -122,15 +133,76 @@ class HeadLink extends \Zend\View\Helper\HeadLink
                     'output' => str_replace('.less', '.css', $file)
                 ]
             );
-            $this->prependStylesheet(
-                $cssDirectory . $css_file_name, $media, $conditionalStylesheet
-            );
+            return $cssDirectory . $css_file_name;
         } catch (\Exception $e) {
             error_log($e->getMessage());
             list($fileName, ) = explode('.', $file);
-            $this->prependStylesheet(
-                $urlHelper('home') . "themes/{$currentTheme}/css/{$fileName}.css"
-            );
+            return $urlHelper('home') . "themes/{$currentTheme}/css/{$fileName}.css";
         }
     }
+
+    /**
+     * Returns true if file should not be included in the compressed concat file
+     * Required by ConcatTrait
+     *
+     * @param stdClass $item Link element object
+     *
+     * @return bool
+     */
+    protected function isExcludedFromConcat($item)
+    {
+        return !isset($item->rel) || $item->rel != 'stylesheet'
+            || strpos($item->href, '://');
+    }
+
+    /**
+     * Get the file path from the link object
+     * Required by ConcatTrait
+     *
+     * @param stdClass $item Link element object
+     *
+     * @return string
+     */
+    protected function getResourceFilePath($item)
+    {
+        return $item->href;
+    }
+
+    /**
+     * Set the file path of the link object
+     * Required by ConcatTrait
+     *
+     * @param stdClass $item Link element object
+     * @param string   $path New path string
+     *
+     * @return stdClass
+     */
+    protected function setResourceFilePath($item, $path)
+    {
+        $item->href = $path;
+        return $item;
+    }
+
+    /**
+     * Get the file type
+     *
+     * @param stdClass $item Link element object
+     *
+     * @return string
+     */
+    public function getType($item)
+    {
+        return isset($item->media) ? $item->media : 'all';
+    }
+
+    /**
+     * Get the minifier that can handle these file types
+     * Required by ConcatTrait
+     *
+     * @return \MatthiasMullie\Minify\JS
+     */
+    protected function getMinifier()
+    {
+        return new \MatthiasMullie\Minify\CSS();
+    }
 }
diff --git a/module/VuFindTheme/src/VuFindTheme/View/Helper/HeadScript.php b/module/VuFindTheme/src/VuFindTheme/View/Helper/HeadScript.php
index 6525db861b4323c7f357e17d4d3055587bac9b09..cd6a519a89158e1195fd112b9098c4fc171b6e85 100644
--- a/module/VuFindTheme/src/VuFindTheme/View/Helper/HeadScript.php
+++ b/module/VuFindTheme/src/VuFindTheme/View/Helper/HeadScript.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  View_Helpers
@@ -39,6 +39,8 @@ use VuFindTheme\ThemeInfo;
  */
 class HeadScript extends \Zend\View\Helper\HeadScript
 {
+    use ConcatTrait;
+
     /**
      * Theme information service
      *
@@ -49,12 +51,24 @@ class HeadScript extends \Zend\View\Helper\HeadScript
     /**
      * Constructor
      *
-     * @param ThemeInfo $themeInfo Theme information service
+     * @param ThemeInfo   $themeInfo Theme information service
+     * @param string|bool $plconfig  Config for current application environment
      */
-    public function __construct(ThemeInfo $themeInfo)
+    public function __construct(ThemeInfo $themeInfo, $plconfig = false)
     {
         parent::__construct();
         $this->themeInfo = $themeInfo;
+        $this->usePipeline = $this->enabledInConfig($plconfig);
+    }
+
+    /**
+     * Folder name and file extension for trait
+     *
+     * @return string
+     */
+    protected function getFileType()
+    {
+        return 'js';
     }
 
     /**
@@ -86,4 +100,58 @@ class HeadScript extends \Zend\View\Helper\HeadScript
 
         return parent::itemToString($item, $indent, $escapeStart, $escapeEnd);
     }
+
+    /**
+     * Returns true if file should not be included in the compressed concat file
+     * Required by ConcatTrait
+     *
+     * @param stdClass $item Script element object
+     *
+     * @return bool
+     */
+    protected function isExcludedFromConcat($item)
+    {
+        return empty($item->attributes['src'])
+            || isset($item->attributes['conditional'])
+            || strpos($item->attributes['src'], '://');
+    }
+
+    /**
+     * Get the file path from the script object
+     * Required by ConcatTrait
+     *
+     * @param stdClass $item Script element object
+     *
+     * @return string
+     */
+    protected function getResourceFilePath($item)
+    {
+        return $item->attributes['src'];
+    }
+
+    /**
+     * Set the file path of the script object
+     * Required by ConcatTrait
+     *
+     * @param stdClass $item Script element object
+     * @param string   $path New path string
+     *
+     * @return stdClass
+     */
+    protected function setResourceFilePath($item, $path)
+    {
+        $item->attributes['src'] = $path;
+        return $item;
+    }
+
+    /**
+     * Get the minifier that can handle these file types
+     * Required by ConcatTrait
+     *
+     * @return \MatthiasMullie\Minify\JS
+     */
+    protected function getMinifier()
+    {
+        return new \MatthiasMullie\Minify\JS();
+    }
 }
diff --git a/module/VuFindTheme/src/VuFindTheme/View/Helper/HeadThemeResources.php b/module/VuFindTheme/src/VuFindTheme/View/Helper/HeadThemeResources.php
index b496a6468ca70e794a37dfc0d6b026497ebe12b1..acb1a40d449d914575c2a78cf1f4749d5e54b093 100644
--- a/module/VuFindTheme/src/VuFindTheme/View/Helper/HeadThemeResources.php
+++ b/module/VuFindTheme/src/VuFindTheme/View/Helper/HeadThemeResources.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  View_Helpers
@@ -68,6 +68,27 @@ class HeadThemeResources extends \Zend\View\Helper\AbstractHelper
         $this->addScripts();
     }
 
+    /**
+     * Given a colon-delimited configuration string, break it apart, making sure
+     * that URLs in the first position are not inappropriately split.
+     *
+     * @param string $current Setting to parse
+     *
+     * @return array
+     */
+    protected function parseSetting($current)
+    {
+        $parts = explode(':', $current);
+        // Special case: don't explode URLs:
+        if (($parts[0] === 'http' || $parts[0] === 'https')
+            && '//' === substr($parts[1], 0, 2)
+        ) {
+            $protocol = array_shift($parts);
+            $parts[0] = $protocol . ':' . $parts[0];
+        }
+        return $parts;
+    }
+
     /**
      * Add meta tags to header.
      *
@@ -101,7 +122,7 @@ class HeadThemeResources extends \Zend\View\Helper\AbstractHelper
         // Load CSS (make sure we prepend them in the appropriate order; theme
         // resources should load before extras added by individual templates):
         foreach (array_reverse($this->container->getCss()) as $current) {
-            $parts = explode(':', $current);
+            $parts = $this->parseSetting($current);
             $headLink()->prependStylesheet(
                 trim($parts[0]),
                 isset($parts[1]) ? trim($parts[1]) : 'all',
@@ -112,9 +133,9 @@ class HeadThemeResources extends \Zend\View\Helper\AbstractHelper
         // Compile and load LESS (make sure we prepend them in the appropriate order
         // theme resources should load before extras added by individual templates):
         foreach (array_reverse($this->container->getLessCss()) as $current) {
-            $parts = explode(':', $current);
-            $headLink()->addLessStylesheet(
-                trim($parts[0]),
+            $parts = $this->parseSetting($current);
+            $headLink()->prependStylesheet(
+                $headLink()->addLessStylesheet(trim($parts[0])),
                 isset($parts[1]) ? trim($parts[1]) : 'all',
                 isset($parts[2]) ? trim($parts[2]) : false
             );
@@ -141,7 +162,7 @@ class HeadThemeResources extends \Zend\View\Helper\AbstractHelper
         // Load Javascript (same ordering considerations as CSS, above):
         $headScript = $this->getView()->plugin('headscript');
         foreach (array_reverse($this->container->getJs()) as $current) {
-            $parts =  explode(':', $current);
+            $parts =  $this->parseSetting($current);
             $headScript()->prependFile(
                 trim($parts[0]),
                 'text/javascript',
diff --git a/module/VuFindTheme/src/VuFindTheme/View/Helper/ImageLink.php b/module/VuFindTheme/src/VuFindTheme/View/Helper/ImageLink.php
index 2b64a0b76451cdc29fd0645c91b2cafdbe9ef07f..6d7c8004dcf78862a207340bac632a64d432de10 100644
--- a/module/VuFindTheme/src/VuFindTheme/View/Helper/ImageLink.php
+++ b/module/VuFindTheme/src/VuFindTheme/View/Helper/ImageLink.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  View_Helpers
diff --git a/module/VuFindTheme/src/VuFindTheme/View/Helper/InlineScript.php b/module/VuFindTheme/src/VuFindTheme/View/Helper/InlineScript.php
index c0497a4d7212b34140a90621681ccb1bbadb6aea..ec9fc2596ff2e35065b6979b9fd90fcb028d21b1 100644
--- a/module/VuFindTheme/src/VuFindTheme/View/Helper/InlineScript.php
+++ b/module/VuFindTheme/src/VuFindTheme/View/Helper/InlineScript.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  View_Helpers
@@ -40,6 +40,7 @@ class InlineScript extends HeadScript
 {
     /**
      * Registry key for placeholder
+     *
      * @var string
      */
     protected $regKey = 'Zend_View_Helper_InlineScript';
diff --git a/module/VuFindTheme/src/VuFindTheme/View/Helper/MobileUrl.php b/module/VuFindTheme/src/VuFindTheme/View/Helper/MobileUrl.php
index 5781425e71ddac329c82ed34922a8779c7602eec..7cb72877a8ab611a134b47facff6a7e4f026ef64 100644
--- a/module/VuFindTheme/src/VuFindTheme/View/Helper/MobileUrl.php
+++ b/module/VuFindTheme/src/VuFindTheme/View/Helper/MobileUrl.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  View_Helpers
diff --git a/module/VuFindTheme/tests/unit-tests/src/VuFindTest/LessCompilerTest.php b/module/VuFindTheme/tests/unit-tests/src/VuFindTest/LessCompilerTest.php
index 5b06edfed6d8ab65e27479ea34f74fb62b9e492d..fdb265750d8cc98a24ef68af13bf43c5c1d555c3 100644
--- a/module/VuFindTheme/tests/unit-tests/src/VuFindTest/LessCompilerTest.php
+++ b/module/VuFindTheme/tests/unit-tests/src/VuFindTest/LessCompilerTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFindTheme/tests/unit-tests/src/VuFindTest/ThemeInfoTest.php b/module/VuFindTheme/tests/unit-tests/src/VuFindTest/ThemeInfoTest.php
index 2fbb1331825abd2c844da9b5a3ed46c05064d5b8..76eab05b64b4b7efbfe8e563fe377f5b904f348f 100644
--- a/module/VuFindTheme/tests/unit-tests/src/VuFindTest/ThemeInfoTest.php
+++ b/module/VuFindTheme/tests/unit-tests/src/VuFindTest/ThemeInfoTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFindTheme/tests/unit-tests/src/VuFindTest/ThemeInjectTemplateListenerTest.php b/module/VuFindTheme/tests/unit-tests/src/VuFindTest/ThemeInjectTemplateListenerTest.php
index 584f1b8563e4d8d59880e3a18d799500211368cb..8e59fc159d55b2d6bb41c4dc9a000562ef0cd8c7 100644
--- a/module/VuFindTheme/tests/unit-tests/src/VuFindTest/ThemeInjectTemplateListenerTest.php
+++ b/module/VuFindTheme/tests/unit-tests/src/VuFindTest/ThemeInjectTemplateListenerTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFindTheme/tests/unit-tests/src/VuFindTest/ThemeMobileTest.php b/module/VuFindTheme/tests/unit-tests/src/VuFindTest/ThemeMobileTest.php
index 89ee24181b35900c73bf49fd37b372ecf5c3ea36..74ba2f2156df026e5e7e39982a113f7f854e69c8 100644
--- a/module/VuFindTheme/tests/unit-tests/src/VuFindTest/ThemeMobileTest.php
+++ b/module/VuFindTheme/tests/unit-tests/src/VuFindTest/ThemeMobileTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFindTheme/tests/unit-tests/src/VuFindTest/ThemeResourceContainerTest.php b/module/VuFindTheme/tests/unit-tests/src/VuFindTest/ThemeResourceContainerTest.php
index a3f24c73cbe4569b13dd594fdcf468ee6bf7b9c2..f0abc5a2c41ff101f57dbc1d076fbdd8e1aa534b 100644
--- a/module/VuFindTheme/tests/unit-tests/src/VuFindTest/ThemeResourceContainerTest.php
+++ b/module/VuFindTheme/tests/unit-tests/src/VuFindTest/ThemeResourceContainerTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
diff --git a/module/VuFindTheme/tests/unit-tests/src/VuFindTest/View/Helper/HeadThemeResourcesTest.php b/module/VuFindTheme/tests/unit-tests/src/VuFindTest/View/Helper/HeadThemeResourcesTest.php
index 8921c1ab4f9003694d5e98f02c1fd2436268a27a..67615920c126872dce35309fa43c96ff90f6cda4 100644
--- a/module/VuFindTheme/tests/unit-tests/src/VuFindTest/View/Helper/HeadThemeResourcesTest.php
+++ b/module/VuFindTheme/tests/unit-tests/src/VuFindTest/View/Helper/HeadThemeResourcesTest.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Tests
@@ -51,6 +51,25 @@ class HeadThemeResourcesTest extends \VuFindTest\Unit\TestCase
         $helper->__invoke();
     }
 
+    /**
+     * Test configuration parsing.
+     *
+     * @return void
+     */
+    public function testConfigParsing()
+    {
+        $helper = new HeadThemeResources($this->getResourceContainer());
+        $tests = [
+            'foo:bar:baz' => ['foo', 'bar', 'baz'],
+            'http://foo/bar:baz:xyzzy' => ['http://foo/bar', 'baz', 'xyzzy']
+        ];
+        foreach ($tests as $test => $expected) {
+            $this->assertEquals(
+                $expected, $this->callMethod($helper, 'parseSetting', [$test])
+            );
+        }
+    }
+
     /**
      * Get a populated resource container for testing.
      *
diff --git a/package.json b/package.json
index eee218bcd74514511dbe1379679617b194d749b8..f61c89e97efc9b3fbe5f812715a9da0711ad6711 100644
--- a/package.json
+++ b/package.json
@@ -1,10 +1,16 @@
 {
-  "name": "vufind2",
-  "version": "1.0.0",
-  "description": "theme related processing",
-  "main": "gruntfile.js",
+  "name": "vufind",
+  "version": "3.1.2",
+  "description": "Dev tools to handle css preprocessing, js magic, and compression",
+  "repository": {
+    "type": "git",
+    "url": "http://github.com/vufind-org/vufind"
+  },
+  "bugs": {
+    "url": "https://vufind.org/jira"
+  },
   "dependencies": {
-    "grunt": "~0.4.5",
+    "grunt": "^0.4.5",
     "grunt-sass": "^1.0.0",
     "grunt-contrib-less": "1.3.0",
     "grunt-less-to-sass": "0.0.10",
diff --git a/packages/DEBIAN/changelog b/packages/DEBIAN/changelog
index 06dce5e387af115f9d83a202d0b134fbda5306c1..5bb619c7b69753c69189ce6ba328bb5368cbc148 100644
--- a/packages/DEBIAN/changelog
+++ b/packages/DEBIAN/changelog
@@ -1,3 +1,27 @@
+vufind 3.1.3 distribution; urgency=low
+
+  * VuFind 3.1.3 release (see http://vufind.org/wiki/changelog for details) 
+
+ -- maintainer VuFind Project Administration Team <vufind-admins@lists.sourceforge.net>  Mo 13 Mar 2017 08:44:17 UTC
+
+vufind 3.1.2 distribution; urgency=low
+
+  * VuFind 3.1.2 release (see http://vufind.org/wiki/changelog for details) 
+
+ -- maintainer VuFind Project Administration Team <vufind-admins@lists.sourceforge.net>  Mo 16 Jan 2017 07:21:54 UTC
+
+vufind 3.1.1 distribution; urgency=low
+
+  * VuFind 3.1.1 release (see http://vufind.org/wiki/changelog for details) 
+
+ -- maintainer VuFind Project Administration Team <vufind-admins@lists.sourceforge.net>  Mo 31 Oct 2016 08:44:19 UTC
+
+vufind 3.1 distribution; urgency=low
+
+  * VuFind 3.1 release (see http://vufind.org/wiki/changelog for details) 
+
+ -- maintainer VuFind Project Administration Team <vufind-admins@lists.sourceforge.net>  Mo 26 Sep 2016 10:30:43 UTC
+
 vufind 3.0.3 distribution; urgency=low
 
   * VuFind 3.0.3 release (see http://vufind.org/wiki/changelog for details) 
diff --git a/packages/DEBIAN/control b/packages/DEBIAN/control
index 059764350cc8d0b5fb97766472d0dfbfa932ea55..afe834be47c28b80403c9b2e267693c24ee4e018 100644
--- a/packages/DEBIAN/control
+++ b/packages/DEBIAN/control
@@ -1,5 +1,5 @@
 Package: vufind
-Version: 3.0.3
+Version: 3.1.3
 Section: World Wide Web
 Priority: Optional
 Architecture: all
diff --git a/solr.sh b/solr.sh
index 5b9233ba803db97fb2fb1127f47cd4253db81426..2a4956dc0dbead52f653eeea05cb095f20b63e95 100755
--- a/solr.sh
+++ b/solr.sh
@@ -36,7 +36,7 @@ usage()
 # Set VUFIND_HOME
 if [ -z "$VUFIND_HOME" ]
 then
-  VUFIND_HOME=`dirname $0`
+  VUFIND_HOME=$(dirname "$0")
 fi
 
 if [ -z "$SOLR_HOME" ]
@@ -65,4 +65,4 @@ then
 fi
 
 export SOLR_LOGS_DIR=$SOLR_LOGS_DIR
-$SOLR_BIN/solr $1 -p $SOLR_PORT -s $SOLR_HOME -m $SOLR_HEAP -a "-Dsolr.log=$SOLR_LOGS_DIR"
+"$SOLR_BIN/solr" "$1" -p "$SOLR_PORT" -s "$SOLR_HOME" -m "$SOLR_HEAP" -a "-Dsolr.log=$SOLR_LOGS_DIR"
diff --git a/tests/data/author_relators.mrc b/tests/data/author_relators.mrc
index 4b54f97cb0a034a25913a5284647d8afe7c7db8e..ed1135d2e11e0863303c9ff837f1d2dfdb629f69 100644
--- a/tests/data/author_relators.mrc
+++ b/tests/data/author_relators.mrc
@@ -1 +1 @@
-02850cam a2200769   4500003000700000005001700007007000300024008004100027010001700068016002100085020003000106035002500136040003100161041001300192041002000205050003200225240002500257245028200282246003000564246003000594250001100624260003400635300001500669490004900684500008700733591005300820689007100873689002200944689001100966700008800977700008101065700004501146830006501191935000901256935001001265001001301275982002501288981003501313981002801348969001301376983001901389969000601408900002601414900003101440900002601471900001601497900001801513900002401531900001701555900002501572900002101597900003001618900001701648900004201665900001601707900003001723900003301753900003801786900003701824900002501861900003201886900003701918900001701955951000701972852005601979980004502035DE-57620111002185455.0tu030415s2002    xx             00 0 san c  a  2002291169  a(OCoLC)314248261  z8171102127981-7110-212-7  a(DE-599)BSZ104272066  aDE-576bgercDE-576erakwb0 asanaeng07aSanskritaengl. 0aBL1140.4.V572.E 2002 (BL3)+10aViṣṇu-purāṇa10aŚrīviṣṇupurāṇam :b(a system of Hindu mythology and tradition) = The Viṣṇu Purāṇam /cMaharṣidvaipāyanavyāsapraṇītaṃ. Sanskrit text and English translation with various notes derived from other texts by H. H. Wilson. Ed. and rev. by  K. L. Joshi11aThe Viṣṇu Purāṇam3 aThe Viṣṇu Purāṇam  a1. ed.  aDelhi :bParimal Publ.,c2002  a48, 568 S.1 aParimala Saṃskṛta granthamālā ; v63  aEnglish and Sanskrit - Erroneously ascribed to Vyāsa. - Hindu mythological text.  aISBN formal falsch in d. Vorl. *** 580: TUUB/sze00Du0(DE-588)4287488-90(DE-576)2108171512gndtViṣṇu-purāṇa01Af2gndaKommentar0 5DE-5761 aWilson, Horace H.d1786 - 1860eÜbers.0(DE-588)11739775X0(DE-576)1656002684trl1 aJośī, KanhaiyālālaeHrsg.0(DE-588)12389073X0(DE-576)1679856984edt0 aVyāsaeAngebe Verf.0(DE-576)165948167 0aParimala Saṃskṛta granthamālāv63w(DE-576)017853168  azkor  bdruck0000183626-0  a(DE-15)0008958536x1  a(DE-15)Indol Ivf 4x1ccurrent  a(DE-15)41A-2004-1576x1  bIndolx1  a(DE-15)1298010  cB  aHayman Wilson, Horace  aWilson, John Horace Hayman  aWilson, Horace Hayman  aWilson, ...  aWilson, H. H.  aJoshi, Kanhaiya Lal  aJoshi, K. L.  aShastri, Joshi K. L.  aJoshi, Kaniyalal  aKanhaiyālāla Jośī  aVeda Vyāsa  aKṛṣṇadvaipāyaṇa Vedavyāsa  aBedabyāsa  aKrisna Dwaipayan Vedabyas  aKrishna Dvaipāyana Vyāsa  aKṛṣṇadvaipāyaṇa Vyāsa  aKṛṣṇadvaipāyaṇavyāsa  aKrishnadwaipayanvyas  aKrishnadvaipāyana Vyāsa  aKṛṣṇa Dvaipāyana Vyāsa  aDvaipāyana  bZZ  aDE-15x425390888:201403052111z2004-08-17T00:00:00Z  a104272066b0c201403052111d20141003203002878cam a2200769   4500003000700000005001700007007000300024008004100027010001700068016002100085020003000106035002500136040003100161041001300192041002000205050003200225240002500257245030300282246003000585246003000615250001100645260003400656300001500690490004900705500008700754591005300841689007100894689002200965689001100987700008800998700008101086700005201167830006501219935000901284935001001293001001301303982002501316981003501341981002801376969001301404983001901417969000601436900002601442900003101468900002601499900001601525900001801541900002401559900001701583900002501600900002101625900003001646900001701676900004201693900001601735900003001751900003301781900003801814900003701852900002501889900003201914900003701946900001701983951000702000852005602007980004502063DE-57620111002185455.0tu030415s2002    xx             00 0 san c  a  2002291169  a(OCoLC)314248261  z8171102127981-7110-212-7  a(DE-599)BSZ104272066  aDE-576bgercDE-576erakwb0 asanaeng07aSanskritaengl. 0aBL1140.4.V572.E 2002 (BL3)+10aViṣṇu-purāṇa10aŚrīviṣṇupurāṇam :b(a system of Hindu mythology and tradition) = The Viṣṇu Purāṇam (with dubious author)/cMaharṣidvaipāyanavyāsapraṇītaṃ. Sanskrit text and English translation with various notes derived from other texts by H. H. Wilson. Ed. and rev. by  K. L. Joshi11aThe Viṣṇu Purāṇam3 aThe Viṣṇu Purāṇam  a1. ed.  aDelhi :bParimal Publ.,c2002  a48, 568 S.1 aParimala Saṃskṛta granthamālā ; v63  aEnglish and Sanskrit - Erroneously ascribed to Vyāsa. - Hindu mythological text.  aISBN formal falsch in d. Vorl. *** 580: TUUB/sze00Du0(DE-588)4287488-90(DE-576)2108171512gndtViṣṇu-purāṇa01Af2gndaKommentar0 5DE-5761 aWilson, Horace H.d1786 - 1860eÜbers.0(DE-588)11739775X0(DE-576)1656002684trl1 aJośī, KanhaiyālālaeHrsg.0(DE-588)12389073X0(DE-576)1679856984edt0 aVyāsaedubious author0(DE-576)1659481674dub 0aParimala Saṃskṛta granthamālāv63w(DE-576)017853168  azkor  bdruck0000183626-1  a(DE-15)0008958536x1  a(DE-15)Indol Ivf 4x1ccurrent  a(DE-15)41A-2004-1576x1  bIndolx1  a(DE-15)1298010  cB  aHayman Wilson, Horace  aWilson, John Horace Hayman  aWilson, Horace Hayman  aWilson, ...  aWilson, H. H.  aJoshi, Kanhaiya Lal  aJoshi, K. L.  aShastri, Joshi K. L.  aJoshi, Kaniyalal  aKanhaiyālāla Jośī  aVeda Vyāsa  aKṛṣṇadvaipāyaṇa Vedavyāsa  aBedabyāsa  aKrisna Dwaipayan Vedabyas  aKrishna Dvaipāyana Vyāsa  aKṛṣṇadvaipāyaṇa Vyāsa  aKṛṣṇadvaipāyaṇavyāsa  aKrishnadwaipayanvyas  aKrishnadvaipāyana Vyāsa  aKṛṣṇa Dvaipāyana Vyāsa  aDvaipāyana  bZZ  aDE-15x425390888:201403052111z2004-08-17T00:00:00Z  a104272066b0c201403052111d20141003203005846cam a2201489  a4500003000700000005001700007007000300024008004100027022001400068035002500082040003100107041001300138084013700151084015600288084013700444100007600581245015300657246004800810260004400858260003600902500010100938591003301039700005101072700007901123710007401202710007901276935001001355936028401365936025901649936025401908001001302162983001702175984000702192983001802199969000702217983002302224971000702247983002002254970000802274900003102282900002702313900002302340900002302363900002102386900002702407900002802434900001502462900002302477900001802500900002302518900002302541900002202564900001802586900002202604900002302626900002602649900002402675900002602699900002002725900002602745900001502771900001902786900002402805900002502829900002402854900002502878900002102903900002602924900002002950900002502970900002602995900002203021900002603043900002403069900001903093900002303112900002003135900002603155900002603181900002403207900002503231900002303256900002403279900001503303900002503318900002503343900002303368900002203391900002803413900002503441900002803466900002103494900002203515900002603537900002603563900002503589900001903614900002503633900002103658900002803679900002803707900001703735900001703752900001503769900002303784900002703807900001703834900003503851900001703886900002103903910003403924910003403958910003803992910001104030910001104041910004404052910001104096910004304107910001804150910001604168951001004184852003204194852003204226852003304258852003404291980003104325DE-57620090324212109.0tu870129n19uuuuuuxx             00 0 ger c  a1617-4445  a(DE-599)BSZ012485284  aDE-576bgercDE-576erakwb0 ageraeng  aHI 32712rvk9H9HG - HN9HI9HI 1130 - HI 40009HI 1350 - HI 39759HI 3240 - HI 37959HI 3269 - HI 35839HI 3270 - HI 32859HI 3271  aHI 32802rvk9H9HG - HN9HI9HI 1130 - HI 40009HI 1350 - HI 39759HI 3240 - HI 37959HI 3269 - HI 35839HI 3270 - HI 32859HI 3280 - HI 32859HI 3280  aHI 32932rvk9H9HG - HN9HI9HI 1130 - HI 40009HI 1350 - HI 39759HI 3240 - HI 37959HI 3269 - HI 35839HI 3290 - HI 32939HI 32931 aShakespeare, Williamd1564 - 16160(DE-588)1186137230(DE-576)20911023610aEnglisch-deutsche Studienausgabe der Dramen Shakespeares /cunter dem Patronat der Deutschen Shakespeare-Gesellschaft hrsg. von Rüdiger Ahrens ...9 aEnglisch-deutsche Studienausgabe der Dramen  aTübingen :bStauffenburg-Verl.,c19XX  aTübingen :bFrancke,canfangs  aAnfangs unter dem Patronat der Deutschen Shakespeare-Gesellschaft West hrsg. von Werner Habicht   a720 ff. bvb; 5090: L1UB/sred1 aHabicht, WernereHrsg.0(DE-576)1612562364edt1 aAhrens, Rüdigerd1939-eHrsg.0(DE-588)1208429390(DE-576)16003826X4edt2 aDeutsche Shakespeare-Gesellschaft0(DE-588)37097-60(DE-576)1903505042 aDeutsche Shakespeare-Gesellschaft West0(DE-588)38940-70(DE-576)190362154  bdruckrvaHI 3271bTeilsammlungen; AuszügekAnglistik. AmerikanistikkEnglische LiteraturkRenaissance, Elisabethanische Zeit (1500-1640)kLiteraturgeschichtekEinzelne Autoren und DenkmälerkAutoren und Denkmäler SkShakespeare, WilliamkPrimärliteraturkTeilsammlungen; AuszügervaHI 3280bDramenkAnglistik. AmerikanistikkEnglische LiteraturkRenaissance, Elisabethanische Zeit (1500-1640)kLiteraturgeschichtekEinzelne Autoren und DenkmälerkAutoren und Denkmäler SkShakespeare, WilliamkPrimärliteraturkEinzelwerkekDramenrvaHI 3293bEinzelwerkekAnglistik. AmerikanistikkEnglische LiteraturkRenaissance, Elisabethanische Zeit (1500-1640)kLiteraturgeschichtekEinzelne Autoren und DenkmälerkAutoren und Denkmäler SkShakespeare, WilliamkÜbersetzungenkEinzelwerke0000652212-0  a(DE-14)11453  cMB  a(DE-15)184896  cMB  a(DE-Ch1)1000092343  cMB  a(DE-L152)111197  cTIT  aBacon-Shakespeare, Francis  aChekchapiyera, William  aPseudo-Shakespeare  aSaixpēr, Uilliam  aSaixpēros, ...  aŠakisbīr, Williyam  aS'egsi-pe-yar, Wi-li-am  aSekhspír  aŠekspir, Uil'jam  aŠekspir, U.  aŠekspir, Vilijam  aŠekspir, Vil'jam  aŠekspir, Viljem  aŠekspir, V.  aŠekspir, Wiliam  aŠekspir, William  aŠekspiras, Viljamas  aŠek'spiri, Uiliam  aŠekspīrs, Viljams  aŠekspyras, V.  aShaekespeare, William  aShakespear  aShakespear, W.  aShakespear, Wilhelm  aShakespear, Willhelm  aShakespear, William  aShake-Spear, William  aShakespeare, ...  aShakespeare, Gwilherm  aShakespeare, W.  aShakespeare, Wilhelm  aShakespeare, Willhelm  aShakespearowy, W.  aShake-Speare, William  aShakespere, William  aShakspear, ...  aShakspear, William  aShakspeare, ...  aShakspeare, Guglielmo  aShakspeare, Guillaume  aShakspeare, William  aShak-Speare, William  aShakspere, William  aShashibiya, William  aShashibiya  aSheikusupia, Uiriamu  aSheḳspir, Ṿiliam  aŠhekspir, Viliam  aShex'pir, Wil'yam  aŠiksbīr, Wīlīam  aŠiksbīr, Willyam  aŠikspīr, Wīlīam  aSzekspir, Wiliam  aSzekspir, William  aSheḳspir, Ṿiliyam  aSchakespeare, William  aŠaksbīr, Wiliyam  aShakspere, ...  aSchakespear, William  aSchakespear, ...  aŠaḳisper, Veilliyam  aŠaḳisper, Veilliyem  aŠaḳisper  aŠeḳisper  aŠekesper  aŠekesper, Wilyam  aShakespeare, Guillermo  a莎士比亚  aشېكېسپېر, ۋىليام  a沙士比亞  aشېكېسپېر  aShakespeare-Gesellschaft West  ag:Deutschland, Bundesrepublik  aDeutsche Shakespeare-Gesellschaft  a4:nach  a4:vorg  aDeutschen Shakespeare-Gesellschaft e.V.  a4:nauv  aDeutsche Shakespeare-Gesellschaft West  av:abgespalten  aZ:1963-1993  bXA-GB  aDE-14z2014-03-16T01:53:03Z  aDE-15z2008-12-06T12:10:36Z  aDE-Ch1z2010-03-25T10:49:50Z  aDE-L152z2009-05-13T12:13:11Z  a012485284b0c20140306183405900cam a2201489  a4500003000700000005001700007007000300024008004100027022001400068035002500082040003100107041001300138084013700151084015600288084013700444100007600581245019000657246004800847260004400895260003600939500010100975591003301076700005101109700007901160710008601239710008401325935001001409936028401419936025901703936025401962001001302216983001702229984000702246983001802253969000702271983002302278971000702301983002002308970000802328900003102336900002702367900002302394900002302417900002102440900002702461900002802488900001502516900002302531900001802554900002302572900002302595900002202618900001802640900002202658900002302680900002602703900002402729900002602753900002002779900002602799900001502825900001902840900002402859900002502883900002402908900002502932900002102957900002602978900002003004900002503024900002603049900002203075900002603097900002403123900001903147900002303166900002003189900002603209900002603235900002403261900002503285900002303310900002403333900001503357900002503372900002503397900002303422900002203445900002803467900002503495900002803520900002103548900002203569900002603591900002603617900002503643900001903668900002503687900002103712900002803733900002803761900001703789900001703806900001503823900002303838900002703861900001703888900003503905900001703940900002103957910003403978910003404012910003804046910001104084910001104095910004404106910001104150910004304161910001804204910001604222951001004238852003204248852003204280852003304312852003404345980003104379DE-57620090324212109.0tu870129n19uuuuuuxx             00 0 ger c  a1617-4445  a(DE-599)BSZ012485284  aDE-576bgercDE-576erakwb0 ageraeng  aHI 32712rvk9H9HG - HN9HI9HI 1130 - HI 40009HI 1350 - HI 39759HI 3240 - HI 37959HI 3269 - HI 35839HI 3270 - HI 32859HI 3271  aHI 32802rvk9H9HG - HN9HI9HI 1130 - HI 40009HI 1350 - HI 39759HI 3240 - HI 37959HI 3269 - HI 35839HI 3270 - HI 32859HI 3280 - HI 32859HI 3280  aHI 32932rvk9H9HG - HN9HI9HI 1130 - HI 40009HI 1350 - HI 39759HI 3240 - HI 37959HI 3269 - HI 35839HI 3290 - HI 32939HI 32931 aShakespeare, Williamd1564 - 16160(DE-588)1186137230(DE-576)20911023610aEnglisch-deutsche Studienausgabe der Dramen Shakespeares (with patron and other organization) /cunter dem Patronat der Deutschen Shakespeare-Gesellschaft hrsg. von Rüdiger Ahrens ...9 aEnglisch-deutsche Studienausgabe der Dramen  aTübingen :bStauffenburg-Verl.,c19XX  aTübingen :bFrancke,canfangs  aAnfangs unter dem Patronat der Deutschen Shakespeare-Gesellschaft West hrsg. von Werner Habicht   a720 ff. bvb; 5090: L1UB/sred1 aHabicht, WernereHrsg.0(DE-576)1612562364edt1 aAhrens, Rüdigerd1939-eHrsg.0(DE-588)1208429390(DE-576)16003826X4edt2 aDeutsche Shakespeare-Gesellschaftepatron0(DE-588)37097-60(DE-576)190350504pat2 aOther Non Creative Organizationeother0(DE-588)38940-70(DE-576)1903621544oth  bdruckrvaHI 3271bTeilsammlungen; AuszügekAnglistik. AmerikanistikkEnglische LiteraturkRenaissance, Elisabethanische Zeit (1500-1640)kLiteraturgeschichtekEinzelne Autoren und DenkmälerkAutoren und Denkmäler SkShakespeare, WilliamkPrimärliteraturkTeilsammlungen; AuszügervaHI 3280bDramenkAnglistik. AmerikanistikkEnglische LiteraturkRenaissance, Elisabethanische Zeit (1500-1640)kLiteraturgeschichtekEinzelne Autoren und DenkmälerkAutoren und Denkmäler SkShakespeare, WilliamkPrimärliteraturkEinzelwerkekDramenrvaHI 3293bEinzelwerkekAnglistik. AmerikanistikkEnglische LiteraturkRenaissance, Elisabethanische Zeit (1500-1640)kLiteraturgeschichtekEinzelne Autoren und DenkmälerkAutoren und Denkmäler SkShakespeare, WilliamkÜbersetzungenkEinzelwerke0000652212-1  a(DE-14)11453  cMB  a(DE-15)184896  cMB  a(DE-Ch1)1000092343  cMB  a(DE-L152)111197  cTIT  aBacon-Shakespeare, Francis  aChekchapiyera, William  aPseudo-Shakespeare  aSaixpēr, Uilliam  aSaixpēros, ...  aŠakisbīr, Williyam  aS'egsi-pe-yar, Wi-li-am  aSekhspír  aŠekspir, Uil'jam  aŠekspir, U.  aŠekspir, Vilijam  aŠekspir, Vil'jam  aŠekspir, Viljem  aŠekspir, V.  aŠekspir, Wiliam  aŠekspir, William  aŠekspiras, Viljamas  aŠek'spiri, Uiliam  aŠekspīrs, Viljams  aŠekspyras, V.  aShaekespeare, William  aShakespear  aShakespear, W.  aShakespear, Wilhelm  aShakespear, Willhelm  aShakespear, William  aShake-Spear, William  aShakespeare, ...  aShakespeare, Gwilherm  aShakespeare, W.  aShakespeare, Wilhelm  aShakespeare, Willhelm  aShakespearowy, W.  aShake-Speare, William  aShakespere, William  aShakspear, ...  aShakspear, William  aShakspeare, ...  aShakspeare, Guglielmo  aShakspeare, Guillaume  aShakspeare, William  aShak-Speare, William  aShakspere, William  aShashibiya, William  aShashibiya  aSheikusupia, Uiriamu  aSheḳspir, Ṿiliam  aŠhekspir, Viliam  aShex'pir, Wil'yam  aŠiksbīr, Wīlīam  aŠiksbīr, Willyam  aŠikspīr, Wīlīam  aSzekspir, Wiliam  aSzekspir, William  aSheḳspir, Ṿiliyam  aSchakespeare, William  aŠaksbīr, Wiliyam  aShakspere, ...  aSchakespear, William  aSchakespear, ...  aŠaḳisper, Veilliyam  aŠaḳisper, Veilliyem  aŠaḳisper  aŠeḳisper  aŠekesper  aŠekesper, Wilyam  aShakespeare, Guillermo  a莎士比亚  aشېكېسپېر, ۋىليام  a沙士比亞  aشېكېسپېر  aShakespeare-Gesellschaft West  ag:Deutschland, Bundesrepublik  aDeutsche Shakespeare-Gesellschaft  a4:nach  a4:vorg  aDeutschen Shakespeare-Gesellschaft e.V.  a4:nauv  aDeutsche Shakespeare-Gesellschaft West  av:abgespalten  aZ:1963-1993  bXA-GB  aDE-14z2014-03-16T01:53:03Z  aDE-15z2008-12-06T12:10:36Z  aDE-Ch1z2010-03-25T10:49:50Z  aDE-L152z2009-05-13T12:13:11Z  a012485284b0c20140306183403210cam a2200829  a4500003000700000005001700007007000300024008004100027020003000068022001400098035002500112040003100137041000800168084011800176100007100294245011800365246002600483260004500509260004000554500015300594591003200747689006600779689002600845689007000871689001100941700008000952700007601032700008601108700006301194700008601257866002001343935001001363936018101373020001801554001001301572983001801585969000701603900002001610900002901630900002101659900002001680900002101700900002701721900002101748900003201769900002101801900002401822900001601846900001901862900002401881900002101905900002801926950002101954950002001975950002101995950001902016950001502035950001402050950001702064950002102081950003202102950002102134950002402155950001602179950001902195950002402214950002102238951001002259951001002269852005602279980004502335DE-57620150128133128.0tu850101n19uuuuuuxx             00 0 eng c  a082230702290-8223-0702-2  a1532-0928  a(DE-599)BSZ002376105  aDE-576bgercDE-576erakwb0 aeng  aHL 23822rvk9H9HG - HN9HL9HL 1070 - HL 49909HL 1600 - HL 49559HL 2300 - HL 25259HL 2380 - HL 23859HL 23821 aCarlyle, Thomasd1795 - 18810(DE-588)1185191310(DE-576)30168114714aThe collected letters of Thomas and Jane Welsh Carlyle :bDuke-Edinburgh edition /csenior eds.: Ian Campbell ...9 aThe collected letters  aDurham [u.a.] :bDuke Univ. Press,c19XX  aDurham, NC :bUniv. Press,canfangs  aAnfangs mit der Ang.: General ed.: Charles Richard Sanders. - Dazwischen: Senior ed.: Kenneth J. Fielding. - Vols. 13 - 24 ed. by Clyde de L. Ryals   a580:FRUB/Seng ; 720 ff. bvb00Dp0(DE-588)1185191310(DE-576)3016811472gndaCarlyle, Thomas01Af2gndaBriefsammlung02Dp0(DE-588)1188703350(DE-576)2102300452gndaCarlyle, Jane Welsh0 5DE-5761 aCarlyle, Jane Welshd1801 - 18660(DE-588)1188703350(DE-576)2102300454aut1 aCampbell, Iand1942-eHrsg.0(DE-588)10272059920(DE-576)3726038904edt1 aRyals, Clyde de L.d1928 - 1998eHrsg.0(DE-588)12189620X0(DE-576)2929443064edt1 aFielding, Kenneth J.d1924-eHrsg.0(DE-576)1609408694edt1 aSanders, Charles Richardd1904-eHrsg.0(DE-588)1220767290(DE-576)2140401194edt 0aVol. 1 (1970) -  bdruckrvaHL 2382bBriefekAnglistik. AmerikanistikkEnglische Literaturk19. Jahrhundert (1770/1800-1890/1900)kLiteraturgeschichtekEinzelne AutorenkAutoren CkCarlyle, ThomaskBriefe  a97808223070200001732009-0  a(DE-15)390383  cMB  aFielding, K. J.  aFielding, Kenneth Joshua  aCarlisle, Thomas  aKarlejl', Tomas  aCarlyle, Tommaso  aRyals, Clyde de Loache  aCarlyle, Jane B.  aCarlyle, Jane Baillie Welsh  aCarlyle, Jane W.  aWelsh Carlyle, Jane  aWelsh, Jane  aWelsh, Jane B.  aWelsh, Jane Carlyle  aCampbell, Ian M.  aCampbell, Ian MacDonald  aCarlisle, Thomas  aKarlejl', Tomas  aCarlyle, Tommaso  aSchriftsteller  aHistoriker  aPhilosoph  aÜbersetzer  aCarlyle, Jane B.  aCarlyle, Jane Baillie Welsh  aCarlyle, Jane W.  aWelsh Carlyle, Jane  aWelsh, Jane  aWelsh, Jane B.  aWelsh, Jane Carlyle  aSchriftstellerin  aXA-GB  bXA-GB  aDE-15x017759153:201501290334z2005-04-14T00:00:00Z  a002376105b0c201501290334d20150208163703230cam a2200829  a4500003000700000005001700007007000300024008004100027020003000068022001400098035002500112040003100137041000800168084011800176100007100294245011800365246002600483260004500509260004000554500015300594591003200747689006600779689002600845689007000871689001100941700010000952700008101052700009301133700006701226700007001293866002001363935001001383936018101393020001801574001001301592983001801605969000701623900002001630900002901650900002101679900002001700900002101720900002701741900002101768900003201789900002101821900002401842900001601866900001901882900002401901900002101925900002801946950002101974950002001995950002102015950001902036950001502055950001402070950001702084950002102101950003202122950002102154950002402175950001602199950001902215950002402234950002102258951001002279951001002289852005602299980004502355DE-57620150128133128.0tu850101n19uuuuuuxx             00 0 eng c  a082230702290-8223-0702-2  a1532-0928  a(DE-599)BSZ002376105  aDE-576bgercDE-576erakwb0 aeng  aHL 23822rvk9H9HG - HN9HL9HL 1070 - HL 49909HL 1600 - HL 49559HL 2300 - HL 25259HL 2380 - HL 23859HL 23821 aCarlyle, Thomasd1795 - 18810(DE-588)1185191310(DE-576)30168114714aThe collected letters of Thomas and Jane Welsh Carlyle :bDuke-Edinburgh edition /csenior eds.: Ian Campbell ...9 aThe collected letters  aDurham [u.a.] :bDuke Univ. Press,c19XX  aDurham, NC :bUniv. Press,canfangs  aAnfangs mit der Ang.: General ed.: Charles Richard Sanders. - Dazwischen: Senior ed.: Kenneth J. Fielding. - Vols. 13 - 24 ed. by Clyde de L. Ryals   a580:FRUB/Seng ; 720 ff. bvb00Dp0(DE-588)1185191310(DE-576)3016811472gndaCarlyle, Thomas01Af2gndaBriefsammlung02Dp0(DE-588)1188703350(DE-576)2102300452gndaCarlyle, Jane Welsh0 5DE-5761 aSecond Author, Wrong Relatortermd1801 - 1866eautor0(DE-588)1188703350(DE-576)2102300454aut1 aEditor, Uppercased1942-eEDITOR0(DE-588)10272059920(DE-576)3726038904EDT1 aEditor, Whitespaced1928 - 1998efilm editor0(DE-588)12189620X0(DE-576)2929443064 flm1 aEditor, Underscored1924-efilm_editor0(DE-576)1609408694flm1 aEmpty, Relatore d1904-0(DE-588)1220767290(DE-576)2140401194  0aVol. 1 (1970) -  bdruckrvaHL 2382bBriefekAnglistik. AmerikanistikkEnglische Literaturk19. Jahrhundert (1770/1800-1890/1900)kLiteraturgeschichtekEinzelne AutorenkAutoren CkCarlyle, ThomaskBriefe  a97808223070200001732009-1  a(DE-15)390383  cMB  aFielding, K. J.  aFielding, Kenneth Joshua  aCarlisle, Thomas  aKarlejl', Tomas  aCarlyle, Tommaso  aRyals, Clyde de Loache  aCarlyle, Jane B.  aCarlyle, Jane Baillie Welsh  aCarlyle, Jane W.  aWelsh Carlyle, Jane  aWelsh, Jane  aWelsh, Jane B.  aWelsh, Jane Carlyle  aCampbell, Ian M.  aCampbell, Ian MacDonald  aCarlisle, Thomas  aKarlejl', Tomas  aCarlyle, Tommaso  aSchriftsteller  aHistoriker  aPhilosoph  aÜbersetzer  aCarlyle, Jane B.  aCarlyle, Jane Baillie Welsh  aCarlyle, Jane W.  aWelsh Carlyle, Jane  aWelsh, Jane  aWelsh, Jane B.  aWelsh, Jane Carlyle  aSchriftstellerin  aXA-GB  bXA-GB  aDE-15x017759153:201501290334z2005-04-14T00:00:00Z  a002376105b0c201501290334d20150208163703261cam a2200829  a4500003000700000005001700007007000300024008004100027020003000068022001400098035002500112040003100137041000800168084011800176100007100294245011800365246002600483260004500509260004000554500015300594591003200747689006600779689002600845689007000871689001100941700010000952700008701052700009701139700009301236700006501329866002001394935001001414936018101424020001801605001001301623983001801636969000701654900002001661900002901681900002101710900002001731900002101751900002701772900002101799900003201820900002101852900002401873900001601897900001901913900002401932900002101956900002801977950002102005950002002026950002102046950001902067950001502086950001402101950001702115950002102132950003202153950002102185950002402206950001602230950001902246950002402265950002102289951001002310951001002320852005602330980004502386DE-57620150128133128.0tu850101n19uuuuuuxx             00 0 eng c  a082230702290-8223-0702-2  a1532-0928  a(DE-599)BSZ002376105  aDE-576bgercDE-576erakwb0 aeng  aHL 23822rvk9H9HG - HN9HL9HL 1070 - HL 49909HL 1600 - HL 49559HL 2300 - HL 25259HL 2380 - HL 23859HL 23821 aCarlyle, Thomasd1795 - 18810(DE-588)1185191310(DE-576)30168114714aThe collected letters of Thomas and Jane Welsh Carlyle :bDuke-Edinburgh edition /csenior eds.: Ian Campbell ...9 aThe collected letters  aDurham [u.a.] :bDuke Univ. Press,c19XX  aDurham, NC :bUniv. Press,canfangs  aAnfangs mit der Ang.: General ed.: Charles Richard Sanders. - Dazwischen: Senior ed.: Kenneth J. Fielding. - Vols. 13 - 24 ed. by Clyde de L. Ryals   a580:FRUB/Seng ; 720 ff. bvb00Dp0(DE-588)1185191310(DE-576)3016811472gndaCarlyle, Thomas01Af2gndaBriefsammlung02Dp0(DE-588)1188703350(DE-576)2102300452gndaCarlyle, Jane Welsh0 5DE-5761 aSecond Author, Wrong Relatortermd1801 - 1866eautor0(DE-588)1188703350(DE-576)2102300454aut1 aDifferent, Relatord1942-efilm editor0(DE-588)10272059920(DE-576)3726038904fld1 aDouble, Secondary Authord1928 - 1998etranslator0(DE-588)12189620X0(DE-576)2929443064trl1 aDouble, Secondary Authord1928 - 1998eeditor0(DE-588)12189620X0(DE-576)2929443064edt1 aConflicting, Relatorsd1924-eeditor0(DE-576)1609408694cre 0aVol. 1 (1970) -  bdruckrvaHL 2382bBriefekAnglistik. AmerikanistikkEnglische Literaturk19. Jahrhundert (1770/1800-1890/1900)kLiteraturgeschichtekEinzelne AutorenkAutoren CkCarlyle, ThomaskBriefe  a97808223070200001732009-2  a(DE-15)390383  cMB  aFielding, K. J.  aFielding, Kenneth Joshua  aCarlisle, Thomas  aKarlejl', Tomas  aCarlyle, Tommaso  aRyals, Clyde de Loache  aCarlyle, Jane B.  aCarlyle, Jane Baillie Welsh  aCarlyle, Jane W.  aWelsh Carlyle, Jane  aWelsh, Jane  aWelsh, Jane B.  aWelsh, Jane Carlyle  aCampbell, Ian M.  aCampbell, Ian MacDonald  aCarlisle, Thomas  aKarlejl', Tomas  aCarlyle, Tommaso  aSchriftsteller  aHistoriker  aPhilosoph  aÜbersetzer  aCarlyle, Jane B.  aCarlyle, Jane Baillie Welsh  aCarlyle, Jane W.  aWelsh Carlyle, Jane  aWelsh, Jane  aWelsh, Jane B.  aWelsh, Jane Carlyle  aSchriftstellerin  aXA-GB  bXA-GB  aDE-15x017759153:201501290334z2005-04-14T00:00:00Z  a002376105b0c201501290334d201502081637
\ No newline at end of file
+02850cam a2200769   4500003000700000005001700007007000300024008004100027010001700068016002100085020003000106035002500136040003100161041001300192041002000205050003200225240002500257245028200282246003000564246003000594250001100624260003400635300001500669490004900684500008700733591005300820689007100873689002200944689001100966700008800977700008101065700004501146830006501191935000901256935001001265001001301275982002501288981003501313981002801348969001301376983001901389969000601408900002601414900003101440900002601471900001601497900001801513900002401531900001701555900002501572900002101597900003001618900001701648900004201665900001601707900003001723900003301753900003801786900003701824900002501861900003201886900003701918900001701955951000701972852005601979980004502035DE-57620111002185455.0tu030415s2002    xx             00 0 san c  a  2002291169  a(OCoLC)314248261  z8171102127981-7110-212-7  a(DE-599)BSZ104272066  aDE-576bgercDE-576erakwb0 asanaeng07aSanskritaengl. 0aBL1140.4.V572.E 2002 (BL3)+10aViṣṇu-purāṇa10aŚrīviṣṇupurāṇam :b(a system of Hindu mythology and tradition) = The Viṣṇu Purāṇam /cMaharṣidvaipāyanavyāsapraṇītaṃ. Sanskrit text and English translation with various notes derived from other texts by H. H. Wilson. Ed. and rev. by  K. L. Joshi11aThe Viṣṇu Purāṇam3 aThe Viṣṇu Purāṇam  a1. ed.  aDelhi :bParimal Publ.,c2002  a48, 568 S.1 aParimala Saṃskṛta granthamālā ; v63  aEnglish and Sanskrit - Erroneously ascribed to Vyāsa. - Hindu mythological text.  aISBN formal falsch in d. Vorl. *** 580: TUUB/sze00Du0(DE-588)4287488-90(DE-576)2108171512gndtViṣṇu-purāṇa01Af2gndaKommentar0 5DE-5761 aWilson, Horace H.d1786 - 1860eÜbers.0(DE-588)11739775X0(DE-576)1656002684trl1 aJośī, KanhaiyālālaeHrsg.0(DE-588)12389073X0(DE-576)1679856984edt0 aVyāsaeAngebe Verf.0(DE-576)165948167 0aParimala Saṃskṛta granthamālāv63w(DE-576)017853168  azkor  bdruck0000183626-0  a(DE-15)0008958536x1  a(DE-15)Indol Ivf 4x1ccurrent  a(DE-15)41A-2004-1576x1  bIndolx1  a(DE-15)1298010  cB  aHayman Wilson, Horace  aWilson, John Horace Hayman  aWilson, Horace Hayman  aWilson, ...  aWilson, H. H.  aJoshi, Kanhaiya Lal  aJoshi, K. L.  aShastri, Joshi K. L.  aJoshi, Kaniyalal  aKanhaiyālāla Jośī  aVeda Vyāsa  aKṛṣṇadvaipāyaṇa Vedavyāsa  aBedabyāsa  aKrisna Dwaipayan Vedabyas  aKrishna Dvaipāyana Vyāsa  aKṛṣṇadvaipāyaṇa Vyāsa  aKṛṣṇadvaipāyaṇavyāsa  aKrishnadwaipayanvyas  aKrishnadvaipāyana Vyāsa  aKṛṣṇa Dvaipāyana Vyāsa  aDvaipāyana  bZZ  aDE-15x425390888:201403052111z2004-08-17T00:00:00Z  a104272066b0c201403052111d20141003203002878cam a2200769   4500003000700000005001700007007000300024008004100027010001700068016002100085020003000106035002500136040003100161041001300192041002000205050003200225240002500257245030300282246003000585246003000615250001100645260003400656300001500690490004900705500008700754591005300841689007100894689002200965689001100987700008800998700008101086700005201167830006501219935000901284935001001293001001301303982002501316981003501341981002801376969001301404983001901417969000601436900002601442900003101468900002601499900001601525900001801541900002401559900001701583900002501600900002101625900003001646900001701676900004201693900001601735900003001751900003301781900003801814900003701852900002501889900003201914900003701946900001701983951000702000852005602007980004502063DE-57620111002185455.0tu030415s2002    xx             00 0 san c  a  2002291169  a(OCoLC)314248261  z8171102127981-7110-212-7  a(DE-599)BSZ104272066  aDE-576bgercDE-576erakwb0 asanaeng07aSanskritaengl. 0aBL1140.4.V572.E 2002 (BL3)+10aViṣṇu-purāṇa10aŚrīviṣṇupurāṇam :b(a system of Hindu mythology and tradition) = The Viṣṇu Purāṇam (with dubious author)/cMaharṣidvaipāyanavyāsapraṇītaṃ. Sanskrit text and English translation with various notes derived from other texts by H. H. Wilson. Ed. and rev. by  K. L. Joshi11aThe Viṣṇu Purāṇam3 aThe Viṣṇu Purāṇam  a1. ed.  aDelhi :bParimal Publ.,c2002  a48, 568 S.1 aParimala Saṃskṛta granthamālā ; v63  aEnglish and Sanskrit - Erroneously ascribed to Vyāsa. - Hindu mythological text.  aISBN formal falsch in d. Vorl. *** 580: TUUB/sze00Du0(DE-588)4287488-90(DE-576)2108171512gndtViṣṇu-purāṇa01Af2gndaKommentar0 5DE-5761 aWilson, Horace H.d1786 - 1860eÜbers.0(DE-588)11739775X0(DE-576)1656002684trl1 aJośī, KanhaiyālālaeHrsg.0(DE-588)12389073X0(DE-576)1679856984edt0 aVyāsaedubious author0(DE-576)1659481674dub 0aParimala Saṃskṛta granthamālāv63w(DE-576)017853168  azkor  bdruck0000183626-1  a(DE-15)0008958536x1  a(DE-15)Indol Ivf 4x1ccurrent  a(DE-15)41A-2004-1576x1  bIndolx1  a(DE-15)1298010  cB  aHayman Wilson, Horace  aWilson, John Horace Hayman  aWilson, Horace Hayman  aWilson, ...  aWilson, H. H.  aJoshi, Kanhaiya Lal  aJoshi, K. L.  aShastri, Joshi K. L.  aJoshi, Kaniyalal  aKanhaiyālāla Jośī  aVeda Vyāsa  aKṛṣṇadvaipāyaṇa Vedavyāsa  aBedabyāsa  aKrisna Dwaipayan Vedabyas  aKrishna Dvaipāyana Vyāsa  aKṛṣṇadvaipāyaṇa Vyāsa  aKṛṣṇadvaipāyaṇavyāsa  aKrishnadwaipayanvyas  aKrishnadvaipāyana Vyāsa  aKṛṣṇa Dvaipāyana Vyāsa  aDvaipāyana  bZZ  aDE-15x425390888:201403052111z2004-08-17T00:00:00Z  a104272066b0c201403052111d20141003203005846cam a2201489  a4500003000700000005001700007007000300024008004100027022001400068035002500082040003100107041001300138084013700151084015600288084013700444100007600581245015300657246004800810260004400858260003600902500010100938591003301039700005101072700007901123710007401202710007901276935001001355936028401365936025901649936025401908001001302162983001702175984000702192983001802199969000702217983002302224971000702247983002002254970000802274900003102282900002702313900002302340900002302363900002102386900002702407900002802434900001502462900002302477900001802500900002302518900002302541900002202564900001802586900002202604900002302626900002602649900002402675900002602699900002002725900002602745900001502771900001902786900002402805900002502829900002402854900002502878900002102903900002602924900002002950900002502970900002602995900002203021900002603043900002403069900001903093900002303112900002003135900002603155900002603181900002403207900002503231900002303256900002403279900001503303900002503318900002503343900002303368900002203391900002803413900002503441900002803466900002103494900002203515900002603537900002603563900002503589900001903614900002503633900002103658900002803679900002803707900001703735900001703752900001503769900002303784900002703807900001703834900003503851900001703886900002103903910003403924910003403958910003803992910001104030910001104041910004404052910001104096910004304107910001804150910001604168951001004184852003204194852003204226852003304258852003404291980003104325DE-57620090324212109.0tu870129n19uuuuuuxx             00 0 ger c  a1617-4445  a(DE-599)BSZ012485284  aDE-576bgercDE-576erakwb0 ageraeng  aHI 32712rvk9H9HG - HN9HI9HI 1130 - HI 40009HI 1350 - HI 39759HI 3240 - HI 37959HI 3269 - HI 35839HI 3270 - HI 32859HI 3271  aHI 32802rvk9H9HG - HN9HI9HI 1130 - HI 40009HI 1350 - HI 39759HI 3240 - HI 37959HI 3269 - HI 35839HI 3270 - HI 32859HI 3280 - HI 32859HI 3280  aHI 32932rvk9H9HG - HN9HI9HI 1130 - HI 40009HI 1350 - HI 39759HI 3240 - HI 37959HI 3269 - HI 35839HI 3290 - HI 32939HI 32931 aShakespeare, Williamd1564 - 16160(DE-588)1186137230(DE-576)20911023610aEnglisch-deutsche Studienausgabe der Dramen Shakespeares /cunter dem Patronat der Deutschen Shakespeare-Gesellschaft hrsg. von Rüdiger Ahrens ...9 aEnglisch-deutsche Studienausgabe der Dramen  aTübingen :bStauffenburg-Verl.,c19XX  aTübingen :bFrancke,canfangs  aAnfangs unter dem Patronat der Deutschen Shakespeare-Gesellschaft West hrsg. von Werner Habicht   a720 ff. bvb; 5090: L1UB/sred1 aHabicht, WernereHrsg.0(DE-576)1612562364edt1 aAhrens, Rüdigerd1939-eHrsg.0(DE-588)1208429390(DE-576)16003826X4edt2 aDeutsche Shakespeare-Gesellschaft0(DE-588)37097-60(DE-576)1903505042 aDeutsche Shakespeare-Gesellschaft West0(DE-588)38940-70(DE-576)190362154  bdruckrvaHI 3271bTeilsammlungen; AuszügekAnglistik. AmerikanistikkEnglische LiteraturkRenaissance, Elisabethanische Zeit (1500-1640)kLiteraturgeschichtekEinzelne Autoren und DenkmälerkAutoren und Denkmäler SkShakespeare, WilliamkPrimärliteraturkTeilsammlungen; AuszügervaHI 3280bDramenkAnglistik. AmerikanistikkEnglische LiteraturkRenaissance, Elisabethanische Zeit (1500-1640)kLiteraturgeschichtekEinzelne Autoren und DenkmälerkAutoren und Denkmäler SkShakespeare, WilliamkPrimärliteraturkEinzelwerkekDramenrvaHI 3293bEinzelwerkekAnglistik. AmerikanistikkEnglische LiteraturkRenaissance, Elisabethanische Zeit (1500-1640)kLiteraturgeschichtekEinzelne Autoren und DenkmälerkAutoren und Denkmäler SkShakespeare, WilliamkÜbersetzungenkEinzelwerke0000652212-0  a(DE-14)11453  cMB  a(DE-15)184896  cMB  a(DE-Ch1)1000092343  cMB  a(DE-L152)111197  cTIT  aBacon-Shakespeare, Francis  aChekchapiyera, William  aPseudo-Shakespeare  aSaixpēr, Uilliam  aSaixpēros, ...  aŠakisbīr, Williyam  aS'egsi-pe-yar, Wi-li-am  aSekhspír  aŠekspir, Uil'jam  aŠekspir, U.  aŠekspir, Vilijam  aŠekspir, Vil'jam  aŠekspir, Viljem  aŠekspir, V.  aŠekspir, Wiliam  aŠekspir, William  aŠekspiras, Viljamas  aŠek'spiri, Uiliam  aŠekspīrs, Viljams  aŠekspyras, V.  aShaekespeare, William  aShakespear  aShakespear, W.  aShakespear, Wilhelm  aShakespear, Willhelm  aShakespear, William  aShake-Spear, William  aShakespeare, ...  aShakespeare, Gwilherm  aShakespeare, W.  aShakespeare, Wilhelm  aShakespeare, Willhelm  aShakespearowy, W.  aShake-Speare, William  aShakespere, William  aShakspear, ...  aShakspear, William  aShakspeare, ...  aShakspeare, Guglielmo  aShakspeare, Guillaume  aShakspeare, William  aShak-Speare, William  aShakspere, William  aShashibiya, William  aShashibiya  aSheikusupia, Uiriamu  aSheḳspir, Ṿiliam  aŠhekspir, Viliam  aShex'pir, Wil'yam  aŠiksbīr, Wīlīam  aŠiksbīr, Willyam  aŠikspīr, Wīlīam  aSzekspir, Wiliam  aSzekspir, William  aSheḳspir, Ṿiliyam  aSchakespeare, William  aŠaksbīr, Wiliyam  aShakspere, ...  aSchakespear, William  aSchakespear, ...  aŠaḳisper, Veilliyam  aŠaḳisper, Veilliyem  aŠaḳisper  aŠeḳisper  aŠekesper  aŠekesper, Wilyam  aShakespeare, Guillermo  a莎士比亚  aشېكېسپېر, ۋىليام  a沙士比亞  aشېكېسپېر  aShakespeare-Gesellschaft West  ag:Deutschland, Bundesrepublik  aDeutsche Shakespeare-Gesellschaft  a4:nach  a4:vorg  aDeutschen Shakespeare-Gesellschaft e.V.  a4:nauv  aDeutsche Shakespeare-Gesellschaft West  av:abgespalten  aZ:1963-1993  bXA-GB  aDE-14z2014-03-16T01:53:03Z  aDE-15z2008-12-06T12:10:36Z  aDE-Ch1z2010-03-25T10:49:50Z  aDE-L152z2009-05-13T12:13:11Z  a012485284b0c20140306183405900cam a2201489  a4500003000700000005001700007007000300024008004100027022001400068035002500082040003100107041001300138084013700151084015600288084013700444100007600581245019000657246004800847260004400895260003600939500010100975591003301076700005101109700007901160710008601239710008401325935001001409936028401419936025901703936025401962001001302216983001702229984000702246983001802253969000702271983002302278971000702301983002002308970000802328900003102336900002702367900002302394900002302417900002102440900002702461900002802488900001502516900002302531900001802554900002302572900002302595900002202618900001802640900002202658900002302680900002602703900002402729900002602753900002002779900002602799900001502825900001902840900002402859900002502883900002402908900002502932900002102957900002602978900002003004900002503024900002603049900002203075900002603097900002403123900001903147900002303166900002003189900002603209900002603235900002403261900002503285900002303310900002403333900001503357900002503372900002503397900002303422900002203445900002803467900002503495900002803520900002103548900002203569900002603591900002603617900002503643900001903668900002503687900002103712900002803733900002803761900001703789900001703806900001503823900002303838900002703861900001703888900003503905900001703940900002103957910003403978910003404012910003804046910001104084910001104095910004404106910001104150910004304161910001804204910001604222951001004238852003204248852003204280852003304312852003404345980003104379DE-57620090324212109.0tu870129n19uuuuuuxx             00 0 ger c  a1617-4445  a(DE-599)BSZ012485284  aDE-576bgercDE-576erakwb0 ageraeng  aHI 32712rvk9H9HG - HN9HI9HI 1130 - HI 40009HI 1350 - HI 39759HI 3240 - HI 37959HI 3269 - HI 35839HI 3270 - HI 32859HI 3271  aHI 32802rvk9H9HG - HN9HI9HI 1130 - HI 40009HI 1350 - HI 39759HI 3240 - HI 37959HI 3269 - HI 35839HI 3270 - HI 32859HI 3280 - HI 32859HI 3280  aHI 32932rvk9H9HG - HN9HI9HI 1130 - HI 40009HI 1350 - HI 39759HI 3240 - HI 37959HI 3269 - HI 35839HI 3290 - HI 32939HI 32931 aShakespeare, Williamd1564 - 16160(DE-588)1186137230(DE-576)20911023610aEnglisch-deutsche Studienausgabe der Dramen Shakespeares (with patron and other organization) /cunter dem Patronat der Deutschen Shakespeare-Gesellschaft hrsg. von Rüdiger Ahrens ...9 aEnglisch-deutsche Studienausgabe der Dramen  aTübingen :bStauffenburg-Verl.,c19XX  aTübingen :bFrancke,canfangs  aAnfangs unter dem Patronat der Deutschen Shakespeare-Gesellschaft West hrsg. von Werner Habicht   a720 ff. bvb; 5090: L1UB/sred1 aHabicht, WernereHrsg.0(DE-576)1612562364edt1 aAhrens, Rüdigerd1939-eHrsg.0(DE-588)1208429390(DE-576)16003826X4edt2 aDeutsche Shakespeare-Gesellschaftepatron0(DE-588)37097-60(DE-576)190350504pat2 aOther Non Creative Organizationeother0(DE-588)38940-70(DE-576)1903621544oth  bdruckrvaHI 3271bTeilsammlungen; AuszügekAnglistik. AmerikanistikkEnglische LiteraturkRenaissance, Elisabethanische Zeit (1500-1640)kLiteraturgeschichtekEinzelne Autoren und DenkmälerkAutoren und Denkmäler SkShakespeare, WilliamkPrimärliteraturkTeilsammlungen; AuszügervaHI 3280bDramenkAnglistik. AmerikanistikkEnglische LiteraturkRenaissance, Elisabethanische Zeit (1500-1640)kLiteraturgeschichtekEinzelne Autoren und DenkmälerkAutoren und Denkmäler SkShakespeare, WilliamkPrimärliteraturkEinzelwerkekDramenrvaHI 3293bEinzelwerkekAnglistik. AmerikanistikkEnglische LiteraturkRenaissance, Elisabethanische Zeit (1500-1640)kLiteraturgeschichtekEinzelne Autoren und DenkmälerkAutoren und Denkmäler SkShakespeare, WilliamkÜbersetzungenkEinzelwerke0000652212-1  a(DE-14)11453  cMB  a(DE-15)184896  cMB  a(DE-Ch1)1000092343  cMB  a(DE-L152)111197  cTIT  aBacon-Shakespeare, Francis  aChekchapiyera, William  aPseudo-Shakespeare  aSaixpēr, Uilliam  aSaixpēros, ...  aŠakisbīr, Williyam  aS'egsi-pe-yar, Wi-li-am  aSekhspír  aŠekspir, Uil'jam  aŠekspir, U.  aŠekspir, Vilijam  aŠekspir, Vil'jam  aŠekspir, Viljem  aŠekspir, V.  aŠekspir, Wiliam  aŠekspir, William  aŠekspiras, Viljamas  aŠek'spiri, Uiliam  aŠekspīrs, Viljams  aŠekspyras, V.  aShaekespeare, William  aShakespear  aShakespear, W.  aShakespear, Wilhelm  aShakespear, Willhelm  aShakespear, William  aShake-Spear, William  aShakespeare, ...  aShakespeare, Gwilherm  aShakespeare, W.  aShakespeare, Wilhelm  aShakespeare, Willhelm  aShakespearowy, W.  aShake-Speare, William  aShakespere, William  aShakspear, ...  aShakspear, William  aShakspeare, ...  aShakspeare, Guglielmo  aShakspeare, Guillaume  aShakspeare, William  aShak-Speare, William  aShakspere, William  aShashibiya, William  aShashibiya  aSheikusupia, Uiriamu  aSheḳspir, Ṿiliam  aŠhekspir, Viliam  aShex'pir, Wil'yam  aŠiksbīr, Wīlīam  aŠiksbīr, Willyam  aŠikspīr, Wīlīam  aSzekspir, Wiliam  aSzekspir, William  aSheḳspir, Ṿiliyam  aSchakespeare, William  aŠaksbīr, Wiliyam  aShakspere, ...  aSchakespear, William  aSchakespear, ...  aŠaḳisper, Veilliyam  aŠaḳisper, Veilliyem  aŠaḳisper  aŠeḳisper  aŠekesper  aŠekesper, Wilyam  aShakespeare, Guillermo  a莎士比亚  aشېكېسپېر, ۋىليام  a沙士比亞  aشېكېسپېر  aShakespeare-Gesellschaft West  ag:Deutschland, Bundesrepublik  aDeutsche Shakespeare-Gesellschaft  a4:nach  a4:vorg  aDeutschen Shakespeare-Gesellschaft e.V.  a4:nauv  aDeutsche Shakespeare-Gesellschaft West  av:abgespalten  aZ:1963-1993  bXA-GB  aDE-14z2014-03-16T01:53:03Z  aDE-15z2008-12-06T12:10:36Z  aDE-Ch1z2010-03-25T10:49:50Z  aDE-L152z2009-05-13T12:13:11Z  a012485284b0c20140306183403210cam a2200829  a4500003000700000005001700007007000300024008004100027020003000068022001400098035002500112040003100137041000800168084011800176100007100294245011800365246002600483260004500509260004000554500015300594591003200747689006600779689002600845689007000871689001100941700008000952700007601032700008601108700006301194700008601257866002001343935001001363936018101373020001801554001001301572983001801585969000701603900002001610900002901630900002101659900002001680900002101700900002701721900002101748900003201769900002101801900002401822900001601846900001901862900002401881900002101905900002801926950002101954950002001975950002101995950001902016950001502035950001402050950001702064950002102081950003202102950002102134950002402155950001602179950001902195950002402214950002102238951001002259951001002269852005602279980004502335DE-57620150128133128.0tu850101n19uuuuuuxx             00 0 eng c  a082230702290-8223-0702-2  a1532-0928  a(DE-599)BSZ002376105  aDE-576bgercDE-576erakwb0 aeng  aHL 23822rvk9H9HG - HN9HL9HL 1070 - HL 49909HL 1600 - HL 49559HL 2300 - HL 25259HL 2380 - HL 23859HL 23821 aCarlyle, Thomasd1795 - 18810(DE-588)1185191310(DE-576)30168114714aThe collected letters of Thomas and Jane Welsh Carlyle :bDuke-Edinburgh edition /csenior eds.: Ian Campbell ...9 aThe collected letters  aDurham [u.a.] :bDuke Univ. Press,c19XX  aDurham, NC :bUniv. Press,canfangs  aAnfangs mit der Ang.: General ed.: Charles Richard Sanders. - Dazwischen: Senior ed.: Kenneth J. Fielding. - Vols. 13 - 24 ed. by Clyde de L. Ryals   a580:FRUB/Seng ; 720 ff. bvb00Dp0(DE-588)1185191310(DE-576)3016811472gndaCarlyle, Thomas01Af2gndaBriefsammlung02Dp0(DE-588)1188703350(DE-576)2102300452gndaCarlyle, Jane Welsh0 5DE-5761 aCarlyle, Jane Welshd1801 - 18660(DE-588)1188703350(DE-576)2102300454aut1 aCampbell, Iand1942-eHrsg.0(DE-588)10272059920(DE-576)3726038904edt1 aRyals, Clyde de L.d1928 - 1998eHrsg.0(DE-588)12189620X0(DE-576)2929443064edt1 aFielding, Kenneth J.d1924-eHrsg.0(DE-576)1609408694edt1 aSanders, Charles Richardd1904-eHrsg.0(DE-588)1220767290(DE-576)2140401194edt 0aVol. 1 (1970) -  bdruckrvaHL 2382bBriefekAnglistik. AmerikanistikkEnglische Literaturk19. Jahrhundert (1770/1800-1890/1900)kLiteraturgeschichtekEinzelne AutorenkAutoren CkCarlyle, ThomaskBriefe  a97808223070200001732009-0  a(DE-15)390383  cMB  aFielding, K. J.  aFielding, Kenneth Joshua  aCarlisle, Thomas  aKarlejl', Tomas  aCarlyle, Tommaso  aRyals, Clyde de Loache  aCarlyle, Jane B.  aCarlyle, Jane Baillie Welsh  aCarlyle, Jane W.  aWelsh Carlyle, Jane  aWelsh, Jane  aWelsh, Jane B.  aWelsh, Jane Carlyle  aCampbell, Ian M.  aCampbell, Ian MacDonald  aCarlisle, Thomas  aKarlejl', Tomas  aCarlyle, Tommaso  aSchriftsteller  aHistoriker  aPhilosoph  aÜbersetzer  aCarlyle, Jane B.  aCarlyle, Jane Baillie Welsh  aCarlyle, Jane W.  aWelsh Carlyle, Jane  aWelsh, Jane  aWelsh, Jane B.  aWelsh, Jane Carlyle  aSchriftstellerin  aXA-GB  bXA-GB  aDE-15x017759153:201501290334z2005-04-14T00:00:00Z  a002376105b0c201501290334d20150208163703230cam a2200829  a4500003000700000005001700007007000300024008004100027020003000068022001400098035002500112040003100137041000800168084011800176100007100294245011800365246002600483260004500509260004000554500015300594591003200747689006600779689002600845689007000871689001100941700010000952700008101052700009301133700006701226700007001293866002001363935001001383936018101393020001801574001001301592983001801605969000701623900002001630900002901650900002101679900002001700900002101720900002701741900002101768900003201789900002101821900002401842900001601866900001901882900002401901900002101925900002801946950002101974950002001995950002102015950001902036950001502055950001402070950001702084950002102101950003202122950002102154950002402175950001602199950001902215950002402234950002102258951001002279951001002289852005602299980004502355DE-57620150128133128.0tu850101n19uuuuuuxx             00 0 eng c  a082230702290-8223-0702-2  a1532-0928  a(DE-599)BSZ002376105  aDE-576bgercDE-576erakwb0 aeng  aHL 23822rvk9H9HG - HN9HL9HL 1070 - HL 49909HL 1600 - HL 49559HL 2300 - HL 25259HL 2380 - HL 23859HL 23821 aCarlyle, Thomasd1795 - 18810(DE-588)1185191310(DE-576)30168114714aThe collected letters of Thomas and Jane Welsh Carlyle :bDuke-Edinburgh edition /csenior eds.: Ian Campbell ...9 aThe collected letters  aDurham [u.a.] :bDuke Univ. Press,c19XX  aDurham, NC :bUniv. Press,canfangs  aAnfangs mit der Ang.: General ed.: Charles Richard Sanders. - Dazwischen: Senior ed.: Kenneth J. Fielding. - Vols. 13 - 24 ed. by Clyde de L. Ryals   a580:FRUB/Seng ; 720 ff. bvb00Dp0(DE-588)1185191310(DE-576)3016811472gndaCarlyle, Thomas01Af2gndaBriefsammlung02Dp0(DE-588)1188703350(DE-576)2102300452gndaCarlyle, Jane Welsh0 5DE-5761 aSecond Author, Wrong Relatortermd1801 - 1866eautor0(DE-588)1188703350(DE-576)2102300454aut1 aEditor, Uppercased1942-eEDITOR0(DE-588)10272059920(DE-576)3726038904EDT1 aEditor, Whitespaced1928 - 1998efilm editor0(DE-588)12189620X0(DE-576)2929443064 flm1 aEditor, Underscored1924-efilm_editor0(DE-576)1609408694flm1 aEmpty, Relatore d1904-0(DE-588)1220767290(DE-576)2140401194  0aVol. 1 (1970) -  bdruckrvaHL 2382bBriefekAnglistik. AmerikanistikkEnglische Literaturk19. Jahrhundert (1770/1800-1890/1900)kLiteraturgeschichtekEinzelne AutorenkAutoren CkCarlyle, ThomaskBriefe  a97808223070200001732009-1  a(DE-15)390383  cMB  aFielding, K. J.  aFielding, Kenneth Joshua  aCarlisle, Thomas  aKarlejl', Tomas  aCarlyle, Tommaso  aRyals, Clyde de Loache  aCarlyle, Jane B.  aCarlyle, Jane Baillie Welsh  aCarlyle, Jane W.  aWelsh Carlyle, Jane  aWelsh, Jane  aWelsh, Jane B.  aWelsh, Jane Carlyle  aCampbell, Ian M.  aCampbell, Ian MacDonald  aCarlisle, Thomas  aKarlejl', Tomas  aCarlyle, Tommaso  aSchriftsteller  aHistoriker  aPhilosoph  aÜbersetzer  aCarlyle, Jane B.  aCarlyle, Jane Baillie Welsh  aCarlyle, Jane W.  aWelsh Carlyle, Jane  aWelsh, Jane  aWelsh, Jane B.  aWelsh, Jane Carlyle  aSchriftstellerin  aXA-GB  bXA-GB  aDE-15x017759153:201501290334z2005-04-14T00:00:00Z  a002376105b0c201501290334d20150208163703261cam a2200829  a4500003000700000005001700007007000300024008004100027020003000068022001400098035002500112040003100137041000800168084011800176100007100294245011800365246002600483260004500509260004000554500015300594591003200747689006600779689002600845689007000871689001100941700010000952700008701052700009701139700009301236700006501329866002001394935001001414936018101424020001801605001001301623983001801636969000701654900002001661900002901681900002101710900002001731900002101751900002701772900002101799900003201820900002101852900002401873900001601897900001901913900002401932900002101956900002801977950002102005950002002026950002102046950001902067950001502086950001402101950001702115950002102132950003202153950002102185950002402206950001602230950001902246950002402265950002102289951001002310951001002320852005602330980004502386DE-57620150128133128.0tu850101n19uuuuuuxx             00 0 eng c  a082230702290-8223-0702-2  a1532-0928  a(DE-599)BSZ002376105  aDE-576bgercDE-576erakwb0 aeng  aHL 23822rvk9H9HG - HN9HL9HL 1070 - HL 49909HL 1600 - HL 49559HL 2300 - HL 25259HL 2380 - HL 23859HL 23821 aCarlyle, Thomasd1795 - 18810(DE-588)1185191310(DE-576)30168114714aThe collected letters of Thomas and Jane Welsh Carlyle :bDuke-Edinburgh edition /csenior eds.: Ian Campbell ...9 aThe collected letters  aDurham [u.a.] :bDuke Univ. Press,c19XX  aDurham, NC :bUniv. Press,canfangs  aAnfangs mit der Ang.: General ed.: Charles Richard Sanders. - Dazwischen: Senior ed.: Kenneth J. Fielding. - Vols. 13 - 24 ed. by Clyde de L. Ryals   a580:FRUB/Seng ; 720 ff. bvb00Dp0(DE-588)1185191310(DE-576)3016811472gndaCarlyle, Thomas01Af2gndaBriefsammlung02Dp0(DE-588)1188703350(DE-576)2102300452gndaCarlyle, Jane Welsh0 5DE-5761 aSecond Author, Wrong Relatortermd1801 - 1866eautor0(DE-588)1188703350(DE-576)2102300454aut1 aDifferent, Relatord1942-efilm editor0(DE-588)10272059920(DE-576)3726038904fld1 aDouble, Secondary Authord1928 - 1998etranslator0(DE-588)12189620X0(DE-576)2929443064trl1 aDouble, Secondary Authord1928 - 1998eeditor0(DE-588)12189620X0(DE-576)2929443064edt1 aConflicting, Relatorsd1924-eeditor0(DE-576)1609408694cre 0aVol. 1 (1970) -  bdruckrvaHL 2382bBriefekAnglistik. AmerikanistikkEnglische Literaturk19. Jahrhundert (1770/1800-1890/1900)kLiteraturgeschichtekEinzelne AutorenkAutoren CkCarlyle, ThomaskBriefe  a97808223070200001732009-2  a(DE-15)390383  cMB  aFielding, K. J.  aFielding, Kenneth Joshua  aCarlisle, Thomas  aKarlejl', Tomas  aCarlyle, Tommaso  aRyals, Clyde de Loache  aCarlyle, Jane B.  aCarlyle, Jane Baillie Welsh  aCarlyle, Jane W.  aWelsh Carlyle, Jane  aWelsh, Jane  aWelsh, Jane B.  aWelsh, Jane Carlyle  aCampbell, Ian M.  aCampbell, Ian MacDonald  aCarlisle, Thomas  aKarlejl', Tomas  aCarlyle, Tommaso  aSchriftsteller  aHistoriker  aPhilosoph  aÜbersetzer  aCarlyle, Jane B.  aCarlyle, Jane Baillie Welsh  aCarlyle, Jane W.  aWelsh Carlyle, Jane  aWelsh, Jane  aWelsh, Jane B.  aWelsh, Jane Carlyle  aSchriftstellerin  aXA-GB  bXA-GB  aDE-15x017759153:201501290334z2005-04-14T00:00:00Z  a002376105b0c201501290334d20150208163703239cam a2200829  a4500003000700000005001700007007000300024008004100027020003000068022001400098035002500112040003100137041000800168084011800176100007100294245011800365246002600483260004500509260004000554500015300594591003200747689006600779689002600845689007000871689001100941700009700952700008201049700009201131700008801223700006101311866002001372935001001392936018101402020001801583001001301601983001801614969000701632900002001639900002901659900002101688900002001709900002101729900002701750900002101777900003201798900002101830900002401851900001601875900001901891900002401910900002101934900002801955950002101983950002002004950002102024950001902045950001502064950001402079950001702093950002102110950003202131950002102163950002402184950001602208950001902224950002402243950002102267951001002288951001002298852005602308980004502364DE-57620150128133128.0tu850101n19uuuuuuxx             00 0 eng c  a082230702290-8223-0702-2  a1532-0928  a(DE-599)BSZ002376105  aDE-576bgercDE-576erakwb0 aeng  aHL 23822rvk9H9HG - HN9HL9HL 1070 - HL 49909HL 1600 - HL 49559HL 2300 - HL 25259HL 2380 - HL 23859HL 23821 aCarlyle, Thomasd1795 - 18810(DE-588)1185191310(DE-576)30168114714aThe collected letters of Thomas and Jane Welsh Carlyle :bDuke-Edinburgh edition /csenior eds.: Ian Campbell ...9 aThe collected letters  aDurham [u.a.] :bDuke Univ. Press,c19XX  aDurham, NC :bUniv. Press,canfangs  aAnfangs mit der Ang.: General ed.: Charles Richard Sanders. - Dazwischen: Senior ed.: Kenneth J. Fielding. - Vols. 13 - 24 ed. by Clyde de L. Ryals   a580:FRUB/Seng ; 720 ff. bvb00Dp0(DE-588)1185191310(DE-576)3016811472gndaCarlyle, Thomas01Af2gndaBriefsammlung02Dp0(DE-588)1188703350(DE-576)2102300452gndaCarlyle, Jane Welsh0 5DE-5761 aSecond Author, Wrong Relatortermd1801 - 1866eAuthor.0(DE-588)1188703350(DE-576)2102300451 aDifferent, Relatord1942-efilm editor0(DE-588)10272059920(DE-576)3726038901 aDouble, Secondary Authord1928 - 1998etranslator0(DE-588)12189620X0(DE-576)2929443061 aDouble, Secondary Authord1928 - 1998eeditor0(DE-588)12189620X0(DE-576)2929443061 aConflicting, Relatorsd1924-eCreator0(DE-576)160940869 0aVol. 1 (1970) -  bdruckrvaHL 2382bBriefekAnglistik. AmerikanistikkEnglische Literaturk19. Jahrhundert (1770/1800-1890/1900)kLiteraturgeschichtekEinzelne AutorenkAutoren CkCarlyle, ThomaskBriefe  a97808223070200001732009-3  a(DE-15)390383  cMB  aFielding, K. J.  aFielding, Kenneth Joshua  aCarlisle, Thomas  aKarlejl', Tomas  aCarlyle, Tommaso  aRyals, Clyde de Loache  aCarlyle, Jane B.  aCarlyle, Jane Baillie Welsh  aCarlyle, Jane W.  aWelsh Carlyle, Jane  aWelsh, Jane  aWelsh, Jane B.  aWelsh, Jane Carlyle  aCampbell, Ian M.  aCampbell, Ian MacDonald  aCarlisle, Thomas  aKarlejl', Tomas  aCarlyle, Tommaso  aSchriftsteller  aHistoriker  aPhilosoph  aÜbersetzer  aCarlyle, Jane B.  aCarlyle, Jane Baillie Welsh  aCarlyle, Jane W.  aWelsh Carlyle, Jane  aWelsh, Jane  aWelsh, Jane B.  aWelsh, Jane Carlyle  aSchriftstellerin  aXA-GB  bXA-GB  aDE-15x017759153:201501290334z2005-04-14T00:00:00Z  a002376105b0c201501290334d20150208163701034cam a2200277  a4500001001000000003000700010005001700017007000300034008004100037035002500078040002900103041001300132245007800145264006800223264005200291336002600343337004600369338002500415500009300440591002200533689006200555689001100617700010900628935000900737935001000746017791359DE-57620151016084517.0tu890531s1926    xx             00 0 ger c  a(DE-599)BSZ017791359  aDE-576bgercDE-576erda0 ageraara10aNeue Huḏailiten-Diwane /cherausgegeben und übersetzt von Joseph Hell 1aHannover :bOrient-Buchhandlung Heinz Lafaire K.-G.,c1926-193331aLeipzig :bOtto Harrassowitz Verlag,c1926-1933  aTextbtxt2rdacontent  aohne Hilfsmittel zu benutzenbn2rdamedia  aBandbnc2rdacarrier  aBand 2, 1933 im Otto Harrassowitz Verlag, Leipzig erschienen. Text deutsch und arabisch.  a5550: FRUB08/Sred00Dg0(DE-588)4107038-00(DE-576)2094198572gndaHudhailiten0 5DE-5761 aHell, Josephd1875 - 1950eHerausgeberIneÜbersetzerIn0(DE-588)11668433X0(DE-576)16136862X4edt4trl  azkor  bdruck01154cam a2200313  a4500001001200000003000700012005001700019007000300036008004100039035002500080040002900105041001300134100004800147110003800195245009300233264006800326264005200394336002600446337004600472338002500518500009300543591002200636689006200658689001100720700005000731710004000781935000900821935001000830017791359-1DE-57620151016084517.0tu890531s1926    xx             00 0 ger c  a(DE-599)BSZ017791359  aDE-576bgercDE-576erda0 ageraara1 aAuthor, Primaryd1795 - 18814aut4ive4dsr1 aCorporate, Primary4orm4spn4his10aFake Record 1 with multiple relators/cin Marc 100, 110, 700 and 710; only in subfield 4 1aHannover :bOrient-Buchhandlung Heinz Lafaire K.-G.,c1926-193331aLeipzig :bOtto Harrassowitz Verlag,c1926-1933  aTextbtxt2rdacontent  aohne Hilfsmittel zu benutzenbn2rdamedia  aBandbnc2rdacarrier  aBand 2, 1933 im Otto Harrassowitz Verlag, Leipzig erschienen. Text deutsch und arabisch.  a5550: FRUB08/Sred00Dg0(DE-588)4107038-00(DE-576)2094198572gndaHudhailiten0 5DE-5761 aAuthor, Secondaryd1875 - 19504edt4pro4res1 aCorporate, Secondary4dgg4hnr4isb  azkor  bdruck01266cam a2200313  a4500001001200000003000700012005001700019007000300036008004100039035002500080040002900105041001300134100006400147110006900211245009300280264006800373264005200441336002600493337004600519338002500565500009300590591002200683689006200705689001100767700007700778710007800855935000900933935001000942017791359-2DE-57620151016084517.0tu890531s1926    xx             00 0 ger c  a(DE-599)BSZ017791359  aDE-576bgercDE-576erda0 ageraara1 aAuthor, Primaryd1795 - 1881eauthoreintervieweeedesigner1 aCorporate, Primaryeorganizeresponsoring bodyehost institution10aFake Record 2 with multiple relators/cin Marc 100, 110, 700 and 710; only in subfield e 1aHannover :bOrient-Buchhandlung Heinz Lafaire K.-G.,c1926-193331aLeipzig :bOtto Harrassowitz Verlag,c1926-1933  aTextbtxt2rdacontent  aohne Hilfsmittel zu benutzenbn2rdamedia  aBandbnc2rdacarrier  aBand 2, 1933 im Otto Harrassowitz Verlag, Leipzig erschienen. Text deutsch und arabisch.  a5550: FRUB08/Sred00Dg0(DE-588)4107038-00(DE-576)2094198572gndaHudhailiten0 5DE-5761 aAuthor, Secondaryd1875 - 1950eeditorial directoreproducereresearcher1 aCorporate, Secondaryedegree granting institutionehonoureeeissuing body  azkor  bdruck01332cam a2200313  a4500001001200000003000700012005001700019007000300036008004100039035002500080040002900105041001300134100007900147110008400226245009900310264006800409264005200477336002600529337004600555338002500601500009300626591002200719689006200741689001100803700009200814710009300906935000900999935001001008017791359-3DE-57620151016084517.0tu890531s1926    xx             00 0 ger c  a(DE-599)BSZ017791359  aDE-576bgercDE-576erda0 ageraara1 aAuthor, Primaryd1795 - 1881eauthor4auteinterviewee4iveedesigner4dsr1 aCorporate, Primaryeorganizer4ormesponsoring body4spnehost institution4his10aFake Record 3 with multiple relators/cin Marc 100, 110, 700 and 710; both in subfield e and 4 1aHannover :bOrient-Buchhandlung Heinz Lafaire K.-G.,c1926-193331aLeipzig :bOtto Harrassowitz Verlag,c1926-1933  aTextbtxt2rdacontent  aohne Hilfsmittel zu benutzenbn2rdamedia  aBandbnc2rdacarrier  aBand 2, 1933 im Otto Harrassowitz Verlag, Leipzig erschienen. Text deutsch und arabisch.  a5550: FRUB08/Sred00Dg0(DE-588)4107038-00(DE-576)2094198572gndaHudhailiten0 5DE-5761 aAuthor, Secondaryd1875 - 1950eeditorial director4edteproducer4proeresearcher4res1 aCorporate, Secondaryedegree granting institution4dggehonouree4hnreissuing body4isb  azkor  bdruck01246cam a2200313  a4500001001200000003000700012005001700019007000300036008004100039035002500080040002900105041001300134100006400147110006100211245010700272264006800379264005200447336002600499337004600525338002500571500009300596591002200689689006200711689001100773700006500784710006400849935000900913935001000922017791359-4DE-57620151016084517.0tu890531s1926    xx             00 0 ger c  a(DE-599)BSZ017791359  aDE-576bgercDE-576erda0 ageraara1 aAuthor, Primaryd1795 - 1881eauthoreinterviewee4ive4dsr1 aCorporate, Primaryeorganizeresponsoring body4spn4his10aFake Record 4 with multiple relators/cin Marc 100, 110, 700 and 710; some in subfield e and some in 4 1aHannover :bOrient-Buchhandlung Heinz Lafaire K.-G.,c1926-193331aLeipzig :bOtto Harrassowitz Verlag,c1926-1933  aTextbtxt2rdacontent  aohne Hilfsmittel zu benutzenbn2rdamedia  aBandbnc2rdacarrier  aBand 2, 1933 im Otto Harrassowitz Verlag, Leipzig erschienen. Text deutsch und arabisch.  a5550: FRUB08/Sred00Dg0(DE-588)4107038-00(DE-576)2094198572gndaHudhailiten0 5DE-5761 aAuthor, Secondaryd1875 - 1950eeditorial director4pro4res1 aCorporate, Secondaryedegree granting institution4hnr4isb  azkor  bdruck01282cam a2200313  a4500001001200000003000700012005001700019007000300036008004100039035002500080040002900105041001300134100006800147110007100215245010000286264006800386264005200454336002600506337004600532338002500578500009300603591002200696689006200718689001100780700006900791710008900860935000900949935001000958017791359-5DE-57620151016084517.0tu890531s1926    xx             00 0 ger c  a(DE-599)BSZ017791359  aDE-576bgercDE-576erda0 ageraara1 aAuthor, Primaryd1795 - 1881eVerfassereInterviewtereDesigner1 aCorporate, PrimaryeVeranstaltereSponsoreGastgebende Institution10aFake Record 5 with multiple German relators/cin Marc 100, 110, 700 and 710; only in subfield e 1aHannover :bOrient-Buchhandlung Heinz Lafaire K.-G.,c1926-193331aLeipzig :bOtto Harrassowitz Verlag,c1926-1933  aTextbtxt2rdacontent  aohne Hilfsmittel zu benutzenbn2rdamedia  aBandbnc2rdacarrier  aBand 2, 1933 im Otto Harrassowitz Verlag, Leipzig erschienen. Text deutsch und arabisch.  a5550: FRUB08/Sred00Dg0(DE-588)4107038-00(DE-576)2094198572gndaHudhailiten0 5DE-5761 aAuthor, Secondaryd1875 - 1950eHerausgebereProduzenteForscher1 aCorporate, SecondaryeGrad-verleihende InstitutioneGefeiertereHerausgebendes Organ  azkor  bdruck01348cam a2200313  a4500001001200000003000700012005001700019007000300036008004100039035002500080040002900105041001300134100008300147110008600230245010600316264006800422264005200490336002600542337004600568338002500614500009300639591002200732689006200754689001100816700008400827710010400911935000901015935001001024017791359-6DE-57620151016084517.0tu890531s1926    xx             00 0 ger c  a(DE-599)BSZ017791359  aDE-576bgercDE-576erda0 ageraara1 aAuthor, Primaryd1795 - 1881eVerfasser4auteInterviewter4iveeDesigner4dsr1 aCorporate, PrimaryeVeranstalter4ormeSponsor4spneGastgebende Institution4his10aFake Record 6 with multiple German relators/cin Marc 100, 110, 700 and 710; both in subfield e and 4 1aHannover :bOrient-Buchhandlung Heinz Lafaire K.-G.,c1926-193331aLeipzig :bOtto Harrassowitz Verlag,c1926-1933  aTextbtxt2rdacontent  aohne Hilfsmittel zu benutzenbn2rdamedia  aBandbnc2rdacarrier  aBand 2, 1933 im Otto Harrassowitz Verlag, Leipzig erschienen. Text deutsch und arabisch.  a5550: FRUB08/Sred00Dg0(DE-588)4107038-00(DE-576)2094198572gndaHudhailiten0 5DE-5761 aAuthor, Secondaryd1875 - 1950eHerausgeber4edteProduzent4proeForscher4res1 aCorporate, SecondaryeGrad-verleihende Institution4dggeGefeierter4hnreHerausgebendes Organ4isb  azkor  bdruck01312cam a2200313  a4500001001200000003000700012005001700019007000300036008004100039035002500080040002900105041001300134100007200147110007500219245011600294264006800410264005200478336002600530337004600556338002500602500009300627591002200720689006200742689001100804700007500815710008900890935000900979935001000988017791359-7DE-57620151016084517.0tu890531s1926    xx             00 0 ger c  a(DE-599)BSZ017791359  aDE-576bgercDE-576erda0 ageraara1 aAuthor, Primaryd1795 - 1881eVerfasserIneInterviewteReDesignerIn1 aCorporate, PrimaryeVeranstalterIneSponsorIneGastgebende Institution10aFake Record 7 with multiple German, gender neutral relators/cin Marc 100, 110, 700 and 710; only in subfield e 1aHannover :bOrient-Buchhandlung Heinz Lafaire K.-G.,c1926-193331aLeipzig :bOtto Harrassowitz Verlag,c1926-1933  aTextbtxt2rdacontent  aohne Hilfsmittel zu benutzenbn2rdamedia  aBandbnc2rdacarrier  aBand 2, 1933 im Otto Harrassowitz Verlag, Leipzig erschienen. Text deutsch und arabisch.  a5550: FRUB08/Sred00Dg0(DE-588)4107038-00(DE-576)2094198572gndaHudhailiten0 5DE-5761 aAuthor, Secondaryd1875 - 1950eHerausgeberIneProduzentIneForscherIn1 aCorporate, SecondaryeGrad-verleihende InstitutioneGefeierteReHerausgebendes Organ  azkor  bdruck01378cam a2200313  a4500001001200000003000700012005001700019007000300036008004100039035002500080040002900105041001300134100008700147110009000234245012200324264006800446264005200514336002600566337004600592338002500638500009300663591002200756689006200778689001100840700009000851710010400941935000901045935001001054017791359-8DE-57620151016084517.0tu890531s1926    xx             00 0 ger c  a(DE-599)BSZ017791359  aDE-576bgercDE-576erda0 ageraara1 aAuthor, Primaryd1795 - 1881eVerfasserIn4auteInterviewteR4iveeDesignerIn4dsr1 aCorporate, PrimaryeVeranstalterIn4ormeSponsorIn4spneGastgebende Institution4his10aFake Record 8 with multiple German, gender neutral relators/cin Marc 100, 110, 700 and 710; both in subfield e and 4 1aHannover :bOrient-Buchhandlung Heinz Lafaire K.-G.,c1926-193331aLeipzig :bOtto Harrassowitz Verlag,c1926-1933  aTextbtxt2rdacontent  aohne Hilfsmittel zu benutzenbn2rdamedia  aBandbnc2rdacarrier  aBand 2, 1933 im Otto Harrassowitz Verlag, Leipzig erschienen. Text deutsch und arabisch.  a5550: FRUB08/Sred00Dg0(DE-588)4107038-00(DE-576)2094198572gndaHudhailiten0 5DE-5761 aAuthor, Secondaryd1875 - 1950eHerausgeberIn4edteProduzentIn4proeForscherIn4res1 aCorporate, SecondaryeGrad-verleihende Institution4dggeGefeierteR4hnreHerausgebendes Organ4isb  azkor  bdruck01540cam a2200313  a4500001001200000003000700012005001700019007000300036008004100039035002500080040002900105041001300134100011400147110009000261245016300351264006800514264005200582336002600634337004600660338002500706500009300731591002200824689006200846689001100908700018400919710010401103935000901207935001001216017791359-9DE-57620151016084517.0tu890531s1926    xx             00 0 ger c  a(DE-599)BSZ017791359  aDE-576bgercDE-576erda0 ageraara1 aAuthor, Primaryd1795 - 1881eVerfasserIn4auteInterviewteR4iveeDesignerIn4dsreBegründer des Werks4oth1 aCorporate, PrimaryeVeranstalterIn4ormeSponsorIn4spneGastgebende Institution4his10aFake Record 9 with multiple German, gender neutral relators including relators with generic code oth/cin Marc 100, 110, 700 and 710; both in subfield e and 4 1aHannover :bOrient-Buchhandlung Heinz Lafaire K.-G.,c1926-193331aLeipzig :bOtto Harrassowitz Verlag,c1926-1933  aTextbtxt2rdacontent  aohne Hilfsmittel zu benutzenbn2rdamedia  aBandbnc2rdacarrier  aBand 2, 1933 im Otto Harrassowitz Verlag, Leipzig erschienen. Text deutsch und arabisch.  a5550: FRUB08/Sred00Dg0(DE-588)4107038-00(DE-576)2094198572gndaHudhailiten0 5DE-5761 aAuthor, Secondaryd1875 - 1950eHerausgeberIn4edteProduzentIn4proeForscherIn4reseSonstige Person, Familie oder Körperschaft, die mit einem Exemplar in Verbindung steht4oth1 aCorporate, SecondaryeGrad-verleihende Institution4dggeGefeierteR4hnreHerausgebendes Organ4isb  azkor  bdruck01501cam a2200313  a4500001001300000003000700013005001700020007000300037008004100040035002500081040002900106041001300135100011400148110009000262245019600352264006800548264005200616336002600668337004600694338002500740500009300765591002200858689006200880689001100942700011100953710010401064935000901168935001001177017791359-10DE-57620151016084517.0tu890531s1926    xx             00 0 ger c  a(DE-599)BSZ017791359  aDE-576bgercDE-576erda0 ageraara1 aAuthor, Primaryd1795 - 1881eVerfasserIn4auteInterviewteR4iveeDesignerIn4dsreBegründer des Werks4oth1 aCorporate, PrimaryeVeranstalterIn4ormeSponsorIn4spneGastgebende Institution4his10aFake Record 10 with multiple German, gender neutral relators, most relator codes for Secondary Author being mapped to relator code ctbcin Marc 100, 110, 700 and 710; both in subfield e and 4 1aHannover :bOrient-Buchhandlung Heinz Lafaire K.-G.,c1926-193331aLeipzig :bOtto Harrassowitz Verlag,c1926-1933  aTextbtxt2rdacontent  aohne Hilfsmittel zu benutzenbn2rdamedia  aBandbnc2rdacarrier  aBand 2, 1933 im Otto Harrassowitz Verlag, Leipzig erschienen. Text deutsch und arabisch.  a5550: FRUB08/Sred00Dg0(DE-588)4107038-00(DE-576)2094198572gndaHudhailiten0 5DE-5761 aAuthor, Secondaryd1875 - 1950eHerausgeberIn4edteMitwirkendeR4ctbeLetterer4ctbeMaskenbildnerIn4ctb1 aCorporate, SecondaryeGrad-verleihende Institution4dggeGefeierteR4hnreHerausgebendes Organ4isb  azkor  bdruck01495cam a2200313  a4500001001300000003000700013005001700020007000300037008004100040035002500081040002900106041001300135100010900148110011200257245015000369264006800519264005200587336002600639337004600665338002500711500009300736591002200829689006200851689001100913700011200924710012601036935000901162935001001171017791359-11DE-57620151016084517.0tu890531s1926    xx             00 0 ger c  a(DE-599)BSZ017791359  aDE-576bgercDE-576erda0 ageraara1 aAuthor, Primaryd1795 - 1881eVerfasserIn4auteInterviewteR4iveeDesignerIn4dsreUnknownRelator14xx11 aCorporate, PrimaryeVeranstalterIn4ormeSponsorIn4spneGastgebende Institution4hiseUnknownRelator24xx210aFake Record 11 with multiple German, gender neutral relators including unknown relators/cin Marc 100, 110, 700 and 710; both in subfield e and 4 1aHannover :bOrient-Buchhandlung Heinz Lafaire K.-G.,c1926-193331aLeipzig :bOtto Harrassowitz Verlag,c1926-1933  aTextbtxt2rdacontent  aohne Hilfsmittel zu benutzenbn2rdamedia  aBandbnc2rdacarrier  aBand 2, 1933 im Otto Harrassowitz Verlag, Leipzig erschienen. Text deutsch und arabisch.  a5550: FRUB08/Sred00Dg0(DE-588)4107038-00(DE-576)2094198572gndaHudhailiten0 5DE-5761 aAuthor, Secondaryd1875 - 1950eHerausgeberIn4edteProduzentIn4proeForscherIn4reseUnknownRelator34xx31 aCorporate, SecondaryeGrad-verleihende Institution4dggeGefeierteR4hnreHerausgebendes Organ4isbeUnknownRelator44xx4  azkor  bdruck
\ No newline at end of file
diff --git a/tests/data/geo.mrc b/tests/data/geo.mrc
new file mode 100644
index 0000000000000000000000000000000000000000..42f35e0e55684076d1095dc77fac3758c6666e54
--- /dev/null
+++ b/tests/data/geo.mrc
@@ -0,0 +1 @@
+00362naaa 2200109zu 4500001000800000005001500008008004100023034007400064034007000138100001700208245002700225  2000120160513104939730000s1973    coAa          0  0  EL  d0 aad+129.95348029e+129.95348029f-55.29356577g-55.29356577zSite 3740 aad+92.58856498e+92.58856498f+35.0285559g+35.0285559zSite 4511 aHurt, Millie10aTest Publication 2000100364naaa 2200109zu 4500001000800000005001500008008004100023034007000064034007200134100002100206245002700227  2000220160513104939730000s1973    coAa          0  0  EL  d0 aad+10.05908843e+10.05908843f-5.75031466g-5.75031466zSite 4400 aad+46.45283977e+46.45283977f+31.50720239g+31.50720239zSite 4471 aWinrow, Sanjuana10aTest Publication 2000200360naaa 2200109zu 4500001000800000005001500008008004100023034007200064034007200136100001500208245002700223  2000320160513104939730000s1973    coAa          0  0  EL  d0 aad-35.26826575e-35.26826575f+27.26620523g+27.26620523zSite 4000 aad-15.10285454e-15.10285454f+14.80524368g+14.80524368zSite 1631 aRudd, Jann10aTest Publication 2000300613naaa 2200145zu 4500001000800000005001500008008004100023034007200064034007200136034006600208034007400274034007200348100002000420245002700440  2000420160513104939730000s1973    coAa          0  0  EL  d0 aad-56.86690649e-56.86690649f+15.77554895g+15.77554895zSite 4790 aad+159.74574799e+159.74574799f-20.9661451g-20.9661451zSite 4340 aad+7.9676261e+7.9676261f+6.62658415g+6.62658415zSite 4480 aad+135.37478775e+135.37478775f+46.27024658g+46.27024658zSite 5530 aad-105.9279514e-105.9279514f+35.25445142g+35.25445142zSite 3061 aWalston, Verlie10aTest Publication 2000400526naaa 2200133zu 4500001000800000005001500008008004100023034007200064034007200136034006600208034007200274100001900346245002700365  2000520160513104939730000s1973    coAa          0  0  EL  d0 aad+83.63104693e+83.63104693f+21.46845472g+21.46845472zSite 3140 aad+156.17691966e+156.17691966f-9.22226407g-9.22226407zSite 1400 aad-7.6873641e-7.6873641f+49.3997425g+49.3997425zSite 1050 aad-85.11852582e-85.11852582f-32.54764684g-32.54764684zSite 2661 aPakele, Marina10aTest Publication 2000500365naaa 2200109zu 4500001000800000005001500008008004100023034007400064034007000138100002000208245002700228  2000620160513104939730000s1973    coAa          0  0  EL  d0 aadE119.53888243eE119.53888243fN62.91327942gN62.91327942zSite 2550 aadE67.8057745eE67.8057745fN36.44414008gN36.44414008zSite 5461 aMcglasson, Rich10aTest Publication 2000600627naaa 2200145zu 4500001000800000005001500008008004100023034007400064034007400138034007200212034007400284034007400358100002200432245002700454  2000720160513104939730000s1973    coAa          0  0  EL  d0 aadE121.59566053eE121.59566053fN23.86811097gN23.86811097zSite 3610 aadE151.51785078eE151.51785078fN13.91875447gN13.91875447zSite 1050 aadW86.72014286eW86.72014286fS10.73770388gS10.73770388zSite 4880 aadE134.89543887eE134.89543887fN25.94241103gN25.94241103zSite 2060 aadE161.78292166eE161.78292166fN10.15327259gN10.15327259zSite 3501 aRottman, Serafina10aTest Publication 2000700621naaa 2200145zu 4500001000800000005001500008008004100023034007400064034007400138034007200212034007200284034007200356100002000428245002700448  2000820160513104939730000s1973    coAa          0  0  EL  d0 aadW103.57340238eW103.57340238fS22.78804289gS22.78804289zSite 2170 aadW175.68166135eW175.68166135fS69.41361345gS69.41361345zSite 4970 aadE48.51599542eE48.51599542fS58.33629524gS58.33629524zSite 3550 aadE65.04196038eE65.04196038fN32.99243302gN32.99243302zSite 4860 aadW111.45384173eW111.45384173fS7.22546191gS7.22546191zSite 1131 aHaberle, Manuel10aTest Publication 2000800616naaa 2200145zu 4500001000800000005001500008008004100023034007000064034007000134034007400204034007200278034007000350100002300420245002700443  2000920160513104939730000s1973    coAa          0  0  EL  d0 aadE1.55144883eE1.55144883fS15.91268613gS15.91268613zSite 4710 aadE0.46771867eE0.46771867fS53.88776866gS53.88776866zSite 1390 aadE120.96526676eE120.96526676fN22.40092028gN22.40092028zSite 2390 aadW43.86881638eW43.86881638fS29.38831583gS29.38831583zSite 1970 aadW158.26944205eW158.26944205fS0.7378453gS0.7378453zSite 3941 aRuvalcaba, Letisha10aTest Publication 2000900326naaa 2200109zu 4500001000800000005001500008008004100023034005400064034005400118100001700172245002700189  2001020160513104939730000s1973    coAa          0  0  EL  d0 aadE0870709eE0870709fS065613gS065613zSite 1200 aadE1242258eE1242258fS110713gS110713zSite 2231 aBogard, Noma10aTest Publication 2001000525naaa 2200145zu 4500001000800000005001500008008004100023034005400064034005400118034005400172034005400226034005400280100001800334245002700352  2001120160513104939730000s1973    coAa          0  0  EL  d0 aadE0025331eE0025331fS493930gS493930zSite 3010 aadE0013038eE0013038fN444623gN444623zSite 1560 aadW0483141eW0483141fS452235gS452235zSite 4630 aadW0703406eW0703406fS373129gS373129zSite 3630 aadW1172630eW1172630fN661503gN661503zSite 3931 aPutman, Davis10aTest Publication 2001100526naaa 2200145zu 4500001000800000005001500008008004100023034005400064034005400118034005400172034005400226034005400280100001900334245002700353  2001220160513104939730000s1973    coAa          0  0  EL  d0 aadE1741450eE1741450fN154310gN154310zSite 5490 aadW0570821eW0570821fS415337gS415337zSite 2360 aadE1773647eE1773647fS330258gS330258zSite 4620 aadE0710915eE0710915fN400408gN400408zSite 3550 aadW0401638eW0401638fN093759gN093759zSite 5471 aBucher, Margit10aTest Publication 2001200528naaa 2200145zu 4500001000800000005001500008008004100023034005400064034005400118034005400172034005400226034005400280100002100334245002700355  2001320160513104939730000s1973    coAa          0  0  EL  d0 aadW1511513eW1511513fS055042gS055042zSite 1050 aadW1465931eW1465931fS261609gS261609zSite 4840 aadW0790538eW0790538fS512156gS512156zSite 5440 aadW1415343eW1415343fS185901gS185901zSite 4230 aadW1304431eW1304431fS502042gS502042zSite 1291 aDobyns, Wilfredo10aTest Publication 2001300463naaa 2200133zu 4500001000800000005001500008008004100023034005400064034005400118034005400172034005400226100002200280245002700302  2001420160513104939730000s1973    coAa          0  0  EL  d0 aadE1742335eE1742335fN191726gN191726zSite 2130 aadW1570705eW1570705fN435341gN435341zSite 5090 aadW1020233eW1020233fN203933gN203933zSite 2910 aadW1290413eW1290413fS691417gS691417zSite 2251 aMilbourne, Milton10aTest Publication 2001400328naaa 2200109zu 4500001000800000005001500008008004100023034005400064034005400118100001900172245002700191  2001520160513104939730000s1973    coAa          0  0  EL  d0 aadW0755517eW0755517fN151326gN151326zSite 3270 aadW0932259eW0932259fN020021gN020021zSite 1521 aMirsky, Dionna10aTest Publication 2001500536naaa 2200133zu 4500001000800000005001500008008004100023034007400064034007400138034007100212034007200283100002000355245002700375  2001620160513104939730000s1973    coAa          0  0  EL  d0 aad+100.64194149e+150.64194149f+76.29679349g+46.29679349zSite 3250 aad+140.41485857e+150.41485857f-59.57999928g-69.57999928zSite 1980 aad-14.23933962e-9.23933962f-27.72727724g-57.72727724zSite 3170 aad-175.1864435e-172.1864435f-10.03581345g-20.03581345zSite 1721 aInnocent, Leisa10aTest Publication 2001600615naaa 2200145zu 4500001000800000005001500008008004100023034006800064034007100132034007200203034007200275034007200347100002300419245002700442  2001720160513104939730000s1973    coAa          0  0  EL  d0 aad-165.6004414e-115.6004414f-14.509653g-24.509653zSite 2190 aad-172.2069375e-132.2069375f+10.76133978g+5.76133978zSite 2500 aad-162.8898108e-142.8898108f-15.07237354g-25.07237354zSite 5040 aad+71.03685744e+81.03685744f-66.09831516g-76.09831516zSite 1810 aad-160.1490595e-140.1490595f+68.15731704g+58.15731704zSite 4201 aCornforth, Desiree10aTest Publication 2001700613naaa 2200145zu 4500001000800000005001500008008004100023034007000064034007200134034007000206034007200276034007300348100001900421245002700440  2001820160513104939730000s1973    coAa          0  0  EL  d0 aad-91.5895638e-61.5895638f+74.14618546g+44.14618546zSite 1140 aad+64.41336345e+74.41336345f+79.74325176g+49.74325176zSite 1270 aad-7.44081494e-2.44081494f+68.18607023g+58.18607023zSite 1210 aad-170.1177443e-130.1177443f-63.43626229g-73.43626229zSite 4440 aad+95.39073945e+145.39073945f+42.45583955g+22.45583955zSite 2461 aKarter, Marvis10aTest Publication 2001800441naaa 2200121zu 4500001000800000005001500008008004100023034006800064034007000132034007200202100001800274245002700292  2001920160513104939730000s1973    coAa          0  0  EL  d0 aad+10.5670584e+15.5670584f-21.6083379g-41.6083379zSite 2070 aad+41.53787448e+51.53787448f+77.9847143g+57.9847143zSite 5480 aad+129.0839142e+139.0839142f+78.85096117g+68.85096117zSite 2721 aClaro, Bobbie10aTest Publication 2001900620naaa 2200145zu 4500001000800000005001500008008004100023034007400064034006900138034007200207034007400279034007400353100002000427245002700447  2002020160513104939730000s1973    coAa          0  0  EL  d0 aadW163.70412537eW143.70412537fN23.98046062gN13.98046062zSite 2460 aadW37.45857716eW27.45857716fN11.7990193gN6.7990193zSite 4110 aadW92.34562344eW62.34562344fS25.81496714gS55.81496714zSite 4880 aadE144.53537717eE174.53537717fS28.13599649gS58.13599649zSite 5160 aadW177.82278613eW117.82278613fS32.33211767gS62.33211767zSite 2831 aBeckett, Tamala10aTest Publication 2002000364naaa 2200109zu 4500001000800000005001500008008004100023034007400064034007200138100001700210245002700227  2002120160513104939730000s1973    coAa          0  0  EL  d0 aadE114.22094565eE144.22094565fS37.22983935gS77.22983935zSite 4160 aadW167.75647806eW147.75647806fN8.67140253gN3.67140253zSite 3961 aLam, Cristen10aTest Publication 2002100528naaa 2200133zu 4500001000800000005001500008008004100023034007200064034007200136034007200208034007000280100001700350245002700367  2002220160513104939730000s1973    coAa          0  0  EL  d0 aadW97.44010063eW67.44010063fN69.57202273gN39.57202273zSite 3650 aadW79.40943037eW59.40943037fN63.76345804gN43.76345804zSite 2300 aadW103.46816014eW73.46816014fS9.60188743gS14.60188743zSite 2350 aadW16.35276413eW11.35276413fS4.27200773gS9.27200773zSite 2451 aDalke, Isiah10aTest Publication 2002200618naaa 2200145zu 4500001000800000005001500008008004100023034007400064034007200138034007400210034007200284034007400356100001500430245002700445  2002320160513104939730000s1973    coAa          0  0  EL  d0 aadE127.23972147eE137.23972147fN63.12317768gN43.12317768zSite 2080 aadE129.15557267eE149.15557267fN45.7443194gN25.7443194zSite 1020 aadW151.73434552eW101.73434552fS55.73496457gS75.73496457zSite 1080 aadE54.94955124eE64.94955124fS34.71609174gS64.71609174zSite 5370 aadE106.81774118eE156.81774118fS29.05905321gS59.05905321zSite 4111 aOram, Donn10aTest Publication 2002300367naaa 2200109zu 4500001000800000005001500008008004100023034007400064034007100138100002100209245002700230  2002420160513104939730000s1973    coAa          0  0  EL  d0 aadE129.86022987eE149.86022987fN51.68020143gN31.68020143zSite 4760 aadE5.86815575eE15.86815575fN60.95617804gN40.95617804zSite 1761 aRupp, Georgeanna10aTest Publication 2002400450naaa 2200121zu 4500001000800000005001500008008004100023034007400064034007200138034007200210100001900282245002700301  2002520160513104939730000s1973    coAa          0  0  EL  d0 aadW156.11673622eW106.11673622fN78.32587133gN68.32587133zSite 3370 aadE47.92289733eE57.92289733fN77.17739015gN67.17739015zSite 1040 aadW32.89273609eW22.89273609fS40.70960814gS60.70960814zSite 4531 aWalter, Jasper10aTest Publication 2002500532naaa 2200133zu 4500001000800000005001500008008004100023034007300064034007400137034007300211034006800284100001900352245002700371  2002620160513104939730000s1973    coAa          0  0  EL  d0 aadE96.70798372eE146.70798372fN54.00011075gN34.00011075zSite 2710 aadE145.51684204eE165.51684204fN27.98976019gN17.98976019zSite 2660 aadW106.32772299eW76.32772299fN46.24201475gN26.24201475zSite 2860 aadE20.1511411eE25.1511411fN6.07910182gN1.07910182zSite 2421 aHultgren, Josh10aTest Publication 2002600328naaa 2200109zu 4500001000800000005001500008008004100023034005400064034005400118100001900172245002700191  2002720160513104939730000s1973    coAa          0  0  EL  d0 aadW0170403eW0120403fN755211gN455211zSite 4560 aadW1740209eW1340209fN794749gN694749zSite 3851 aGalasso, Tatum10aTest Publication 2002700333naaa 2200109zu 4500001000800000005001500008008004100023034005400064034005400118100002400172245002700196  2002820160513104939730000s1973    coAa          0  0  EL  d0 aadE0710106eE0810106fS111308gS211308zSite 5310 aadE0132703eE0182703fN541236gN341236zSite 3481 aCantrell, Cornelius10aTest Publication 2002800399naaa 2200121zu 4500001000800000005001500008008004100023034005400064034005400118034005400172100002400226245002700250  2002920160513104939730000s1973    coAa          0  0  EL  d0 aadW1693838eW1193838fN122531gN072531zSite 4150 aadE0844331eE1044331fS693723gS793723zSite 1320 aadE0062611eE0162611fN681512gN381512zSite 5031 aGreenblatt, Sherill10aTest Publication 2002900327naaa 2200109zu 4500001000800000005001500008008004100023034005400064034005400118100001800172245002700190  2003020160513104939730000s1973    coAa          0  0  EL  d0 aadE1053451eE1553451fS183256gS383256zSite 2870 aadE0830412eE1030412fS174644gS374644zSite 1801 aPerine, Alisa10aTest Publication 2003000393naaa 2200121zu 4500001000800000005001500008008004100023034005400064034005400118034005400172100001800226245002700244  2003120160513104939730000s1973    coAa          0  0  EL  d0 aadE0202657eE0252657fS470042gS770042zSite 2360 aadW1594422eW1094422fS562802gS762802zSite 4950 aadW0254325eW0204325fS591429gS691429zSite 3241 aSuen, Lanette10aTest Publication 2003100460naaa 2200133zu 4500001000800000005001500008008004100023034005400064034005400118034005400172034005400226100001900280245002700299  2003220160513104939730000s1973    coAa          0  0  EL  d0 aadW1331944eW0931944fN402114gN202114zSite 3060 aadW1780423eW1700423fN584511gN284511zSite 4340 aadW1600712eW1500712fN454326gN254326zSite 3810 aadW1752336eW1652336fN731737gN631737zSite 5211 aIverson, Beryl10aTest Publication 2003200459naaa 2200133zu 4500001000800000005001500008008004100023034005400064034005400118034005400172034005400226100001800280245002700298  2003320160513104939730000s1973    coAa          0  0  EL  d0 aadE1423151eE1523151fS521748gS721748zSite 2580 aadW1691508eW1391508fS131306gS231306zSite 5220 aadE1345235eE1745235fS630056gS730056zSite 3540 aadW1640324eW1140324fS330005gS630005zSite 1441 aBoyland, Bebe10aTest Publication 2003300394naaa 2200121zu 4500001000800000005001500008008004100023034005400064034005400118034005400172100001900226245002700245  2003420160513104939730000s1973    coAa          0  0  EL  d0 aadE1590126eE1690126fS341136gS641136zSite 3030 aadW1054117eW0754117fS354552gS754552zSite 3730 aadW0994125eW0694125fS434119gS634119zSite 3551 aArechiga, Moon10aTest Publication 2003400457naaa 2200133zu 4500001000800000005001500008008004100023034005400064034005400118034005400172034005400226100001600280245002700296  2003520160513104939730000s1973    coAa          0  0  EL  d0 aadW1405617eW1205617fS555856gS755856zSite 4800 aadE1073429eE1573429fS022601gS072601zSite 3730 aadE0913431eE1011343fS422933gS622933zSite 5360 aadE1131855eE1231855fS682514gS782514zSite 3241 aRozar, Josh10aTest Publication 2003500521naaa 2200133zu 4500001000800000005001500008008004100023034006800064034007200132034006800204034007000272100001800342245002700360  2003620160513104939730000s1973    coAa          0  0  EL  d0 aad-169.415256e-169.415256f+29.8815696g+29.8815696zSite 5600 aad-151.5560527e-151.5560527f-36.13307132g-36.13307132zSite 2020 aad-87.4479458e-87.4479458f+9.11816125g+9.11816125zSite 5400 aad-149.4183832e-149.4183832f+29.1541857g+29.1541857zSite 5571 aGuynn, Mayola10aTest Publication 2003600366naaa 2200109zu 4500001000800000005001500008008004100023034007200064034007200136100002100208245002700229  2003720160513104939730000s1973    coAa          0  0  EL  d0 aad+86.55263228e+86.55263228f-28.95862432g-28.95862432zSite 1680 aad-149.3464366e-129.3464366f+27.28202333g+17.28202333zSite 1341 aEngelhard, Marco10aTest Publication 2003700366naaa 2200109zu 4500001000800000005001500008008004100023034007400064034007200138100001900210245002700229  2003820160513104939730000s1973    coAa          0  0  EL  d0 aad+118.90296884e+118.90296884f+27.65389383g+27.65389383zSite 3970 aad-135.3498909e-95.34989087f+60.71524151g+40.71524151zSite 2231 aBornstein, Dia10aTest Publication 2003800363naaa 2200109zu 4500001000800000005001500008008004100023034007000064034007200134100002000206245002700226  2003920160513104939730000s1973    coAa          0  0  EL  d0 aad+15.7081159e+15.7081159f+10.51847859g+10.51847859zSite 1540 aad-65.34590317e-45.34590317f-16.22908424g-36.22908424zSite 4341 aMathisen, Dayle10aTest Publication 2003900356naaa 2200109zu 4500001000800000005001500008008004100023034007000064034007000134100001500204245002700219  2004020160513104939730000s1973    coAa          0  0  EL  d0 aad+120.07557262e+120.07557262f-7.8300017g-7.8300017zSite 4600 aad-78.33685724e-58.33685724f-20.7677353g-40.7677353zSite 3351 aOrtiz, Tia10aTest Publication 2004000370naaa 2200109zu 4500001000800000005001500008008004100023034007400064034007200138100002300210245002700233  2004120160513104939730000s1973    coAa          0  0  EL  d0 aad+179.11624407e+179.11624407f+49.45597061g+49.45597061zSite 1290 aad+18.65135762e+23.65135762f+75.67582839g+65.67582839zSite 3251 aSteffensen, Bianca10aTest Publication 2004100365naaa 2200109zu 4500001000800000005001500008008004100023034007200064034007200136100002000208245002700228  2004220160513104939730000s1973    coAa          0  0  EL  d0 aad-69.44473478e-69.44473478f+16.37800971g+16.37800971zSite 5180 aad-31.50294667e-21.50294667f-31.33882964g-61.33882964zSite 3511 aLaurich, Stuart10aTest Publication 2004200369naaa 2200109zu 4500001000800000005001500008008004100023034007400064034007200138100002200210245002700232  2004320160513104939730000s1973    coAa          0  0  EL  d0 aad+154.29238217e+154.29238217f-50.92756335g-50.92756335zSite 2970 aad-66.00381134e-46.00381134f-41.48033844g-61.48033844zSite 1781 aBeveridge, Albina10aTest Publication 2004300367naaa 2200109zu 4500001000800000005001500008008004100023034007000064034007400134100002200208245002700230  2004420160513104939730000s1973    coAa          0  0  EL  d0 aadE24.1857034eE24.1857034fS66.87278415gS66.87278415zSite 1290 aadW166.03284101eW156.03284101fN65.01324719gN35.01324719zSite 3991 aLetsinger, Tamiko10aTest Publication 2004400366naaa 2200109zu 4500001000800000005001500008008004100023034007000064034007400134100002100208245002700229  2004520160513104939730000s1973    coAa          0  0  EL  d0 aadE25.0293899eE25.0293899fS12.28047561gS12.28047561zSite 5060 aadW169.45170238eW119.45170238fN38.14274122gN18.14274122zSite 1251 aCounter, Modesto10aTest Publication 2004500531naaa 2200133zu 4500001000800000005001500008008004100023034007200064034007200136034007200208034007300280100001700353245002700370  2004620160513104939730000s1973    coAa          0  0  EL  d0 aadE86.99902059eE86.99902059fS41.27856855gS41.27856855zSite 3990 aadE133.71880822eE143.71880822fN8.13138602gN3.13138602zSite 4030 aadE171.49599516eE171.49599516fS56.3556026gS56.3556026zSite 1530 aadE119.63111094eE169.63111094fN11.62690851gN6.62690851zSite 1201 aValois, Arie10aTest Publication 2004600528naaa 2200133zu 4500001000800000005001500008008004100023034006800064034007200132034007200204034007400276100001700350245002700367  2004720160513104939730000s1973    coAa          0  0  EL  d0 aadW23.6918351eW23.6918351fN5.50055924gN5.50055924zSite 4950 aadW71.72678006eW51.72678006fN80.68048573gN72.68048573zSite 4190 aadW72.47081023eW72.47081023fS13.50689659gS13.50689659zSite 5050 aadW159.51582506eW109.51582506fS12.01041499gS22.01041499zSite 3211 aGiard, Dodie10aTest Publication 2004700364naaa 2200109zu 4500001000800000005001500008008004100023034007200064034007200136100001900208245002700227  2004820160513104939730000s1973    coAa          0  0  EL  d0 aadW98.39221287eW98.39221287fN20.00645186gN20.00645186zSite 1400 aadW35.22129012eW25.22129012fS11.27828226gS21.27828226zSite 1821 aDryer, Verlene10aTest Publication 2004800365naaa 2200109zu 4500001000800000005001500008008004100023034007400064034007400138100001600212245002700228  2004920160513104939730000s1973    coAa          0  0  EL  d0 aadW126.87139449eW126.87139449fS18.74530167gS18.74530167zSite 4470 aadW159.94018465eW109.94018465fS57.54337933gS67.54337933zSite 4601 aKohn, Clora10aTest Publication 2004900366naaa 2200109zu 4500001000800000005001500008008004100023034007200064034007200136100002100208245002700229  2005020160513104939730000s1973    coAa          0  0  EL  d0 aadW159.88627958eW109.88627958fS3.02307754gS8.02307754zSite 3560 aadE109.5707309eE119.5707309fS48.38262915gS78.38262915zSite 5401 aSutter, Katheryn10aTest Publication 2005000358naaa 2200109zu 4500001000800000005001500008008004100023034007000064034007000134100001700204245002700221  2005120160513104939730000s1973    coAa          0  0  EL  d0 aadE19.87166027eE24.87166027fS35.8561532gS75.8561532zSite 3760 aadE71.3886803eE81.3886803fS25.17975703gS55.17975703zSite 1071 aWalberg, Pia10aTest Publication 2005100590naaa 2200157zu 4500001000800000005001500008008004100023034005400064034005400118034005400172034005400226034005400280034005400334100001700388245002700405  2005220160513104939730000s1973    coAa          0  0  EL  d0 aadE0475840eE0475840fN315244gN315244zSite 3620 aadE0250120eE0250120fS051344gS051344zSite 4610 aadE0392838eE0392838fN123637gN123637zSite 3470 aadW0041819eW0041819fN670212gN670212zSite 3540 aadW0514641eW0514641fS325055gS325055zSite 5520 aadW0532607eW0432607fN401312gN201312zSite 2221 aHove, Armand10aTest Publication 2005200329naaa 2200109zu 4500001000800000005001500008008004100023034005400064034005400118100002000172245002700192  2005320160513104939730000s1973    coAa          0  0  EL  d0 aadW0670504eW0670504fS394806gS394806zSite 3640 aadW0380213eW0280213fN411500gN211500zSite 1091 aWestray, Shizue10aTest Publication 2005300326naaa 2200109zu 4500001000800000005001500008008004100023034005400064034005400118100001700172245002700189  2005420160513104939730000s1973    coAa          0  0  EL  d0 aadW0254536eW0254536fS244501gS244501zSite 2570 aadW0934339eW0634339fN451934gN251934zSite 5111 aOros, Cierra10aTest Publication 2005400330naaa 2200109zu 4500001000800000005001500008008004100023034005400064034005400118100002100172245002700193  2005520160513104939730000s1973    coAa          0  0  EL  d0 aadW1652138eW1652138fN004809gN004809zSite 3750 aadE0435513eE0535513fN510216gN310216zSite 2421 aBlacker, Minerva10aTest Publication 2005500329naaa 2200109zu 4500001000800000005001500008008004100023034005400064034005400118100002000172245002700192  2005620160513104939730000s1973    coAa          0  0  EL  d0 aadW1043920eW1043920fS011057gS011057zSite 2450 aadW0234238eW0184238fN682608gN382608zSite 4221 aLaurich, Stuart10aTest Publication 2005600328naaa 2200109zu 4500001000800000005001500008008004100023034005400064034005400118100001900172245002700191  2005720160513104939730000s1973    coAa          0  0  EL  d0 aadW1722040eW1722040fS352841gS352841zSite 2820 aadW1665858eW1565858fN683913gN383913zSite 1871 aKarter, Marvis10aTest Publication 2005700264naaa 2200097zu 4500001000800000005001500008008004100023034005400064100002100118245002700139  2005820160513104939730000s1973    coAa          0  0  EL  d0 aadW0981303eW0981303fN194221gN194221zSite 1021 aFavela, Lorretta10aTest Publication 2005800262naaa 2200097zu 4500001000800000005001500008008004100023034005400064100001900118245002700137  2005920160513104939730000s1973    coAa          0  0  EL  d0 aadW1612904eW1412904fS130045gS230045zSite 4881 aHultgren, Josh10aTest Publication 2005900327naaa 2200109zu 4500001000800000005001500008008004100023034005400064034005400118100001800172245002700190  2006020160513104939730000s1973    coAa          0  0  EL  d0 aadW0960543eW0960543fS052755gS052755zSite 5170 aadW1062423eW0762423fN181516gN081516zSite 1581 aMain, Neville10aTest Publication 2006000326naaa 2200109zu 4500001000800000005001500008008004100023034005400064034005400118100001700172245002700189  2006120160513104939730000s1973    coAa          0  0  EL  d0 aadW1681205eW1681205fN335749gN335749zSite 1440 aadW1684935eW1384935fS551611gS751611zSite 2101 aShaw, Denice10aTest Publication 2006100278naaa 2200097zu 4500001000800000005001500008008004100023034007000064100001900134245002700153  2006220160513104939730000s1973    coAa          0  0  EL  d0 aad+72.62860542e+72.62860542f-50.0640401g-50.0640401zSite 3861 aJasik, Micaela10aTest Publication 2006200280naaa 2200097zu 4500001000800000005001500008008004100023034007000064100002100134245002700155  2006320160513104939730000s1973    coAa          0  0  EL  d0 aad+79.53785174e+79.53785174f+6.56562911g+6.56562911zSite 1381 aRupp, Georgeanna10aTest Publication 2006300277naaa 2200097zu 4500001000800000005001500008008004100023034007200064100001600136245002700152  2006420160513104939730000s1973    coAa          0  0  EL  d0 aad+91.40655977e+91.40655977f-68.68188538g-68.68188538zSite 3761 aSears, Meri10aTest Publication 2006400280naaa 2200097zu 4500001000800000005001500008008004100023034007000064100002100134245002700155  2006520160513104939730000s1973    coAa          0  0  EL  d0 aad+90.5240303e+90.5240303f+16.31382075g+16.31382075zSite 2991 aWinrow, Sanjuana10aTest Publication 2006500279naaa 2200097zu 4500001000800000005001500008008004100023034007000064100002000134245002700154  2006620160513104939730000s1973    coAa          0  0  EL  d0 aad+51.37763177e+51.37763177f+0.65207263g+0.65207263zSite 4471 aSutter, Karolyn10aTest Publication 2006600280naaa 2200097zu 4500001000800000005001500008008004100023034007200064100001900136245002700155  2006720160513104939730000s1973    coAa          0  0  EL  d0 aad+173.34512334e+173.34512334f+28.0049181g+28.0049181zSite 1541 aArechiga, Moon10aTest Publication 2006700277naaa 2200097zu 4500001000800000005001500008008004100023034007200064100001600136245002700152  2006820160513104939730000s1973    coAa          0  0  EL  d0 aad-15.99213608e-15.99213608f+13.65438911g+13.65438911zSite 4251 aSaar, Danna10aTest Publication 2006800280naaa 2200097zu 4500001000800000005001500008008004100023034007200064100001900136245002700155  2006920160513104939730000s1973    coAa          0  0  EL  d0 aad+137.98717735e+137.98717735f-65.0442013g-65.0442013zSite 1001 aMirabito, Shin10aTest Publication 2006900278naaa 2200097zu 4500001000800000005001500008008004100023034007200064100001700136245002700153  2007020160513104939730000s1973    coAa          0  0  EL  d0 aad-117.4711533e-117.4711533f+38.23863124g+38.23863124zSite 3651 aWalberg, Pia10aTest Publication 2007000282naaa 2200097zu 4500001000800000005001500008008004100023034007200064100002100136245002700157  2007120160513104939730000s1973    coAa          0  0  EL  d0 aad+95.03142217e+95.03142217f-14.54137057g-14.54137057zSite 2361 aBalicki, Nicolas10aTest Publication 2007100276naaa 2200097zu 4500001000800000005001500008008004100023034007200064100001500136245002700151  2007220160513104939730000s1973    coAa          0  0  EL  d0 aad+34.55902657e+34.55902657f-27.96661882g-27.96661882zSite 5541 aOram, Donn10aTest Publication 2007200279naaa 2200097zu 4500001000800000005001500008008004100023034007200064100001800136245002700154  2007320160513104939730000s1973    coAa          0  0  EL  d0 aad+125.34051527e+125.34051527f+9.81883652g+9.81883652zSite 1211 aMurden, Claud10aTest Publication 2007300279naaa 2200097zu 4500001000800000005001500008008004100023034007400064100001600138245002700154  2007420160513104939730000s1973    coAa          0  0  EL  d0 aad+110.92377605e+110.92377605f+31.06593861g+31.06593861zSite 4421 aPerla, Jong10aTest Publication 2007400284naaa 2200097zu 4500001000800000005001500008008004100023034007400064100002100138245002700159  2007520160513104939730000s1973    coAa          0  0  EL  d0 aad+111.98924724e+111.98924724f+52.12174162g+52.12174162zSite 1501 aDobyns, Wilfredo10aTest Publication 2007500282naaa 2200097zu 4500001000800000005001500008008004100023034007000064100002300134245002700157  2007620160513104939730000s1973    coAa          0  0  EL  d0 aad+155.34616323e+155.34616323f-7.8061926g-7.8061926zSite 3231 aGrizzell, Griselda10aTest Publication 2007600282naaa 2200097zu 4500001000800000005001500008008004100023034007200064100002100136245002700157  2007720160513104939730000s1973    coAa          0  0  EL  d0 aad-45.37116744e-45.37116744f-45.10756474g-45.10756474zSite 1551 aSharrow, Sherron10aTest Publication 2007700279naaa 2200097zu 4500001000800000005001500008008004100023034007200064100001800136245002700154  2007820160513104939730000s1973    coAa          0  0  EL  d0 aad-125.8758505e-125.8758505f+37.66553345g+37.66553345zSite 4951 aOverall, Lula10aTest Publication 2007800280naaa 2200097zu 4500001000800000005001500008008004100023034007200064100001900136245002700155  2007920160513104939730000s1973    coAa          0  0  EL  d0 aad-125.7131481e-125.7131481f-65.01937325g-65.01937325zSite 2721 aHersh, Sheldon10aTest Publication 2007900278naaa 2200097zu 4500001000800000005001500008008004100023034007200064100001700136245002700153  2008020160513104939730000s1973    coAa          0  0  EL  d0 aad-167.5160645e-167.5160645f+48.54645955g+48.54645955zSite 4591 aFouche, Hsiu10aTest Publication 2008000281naaa 2200097zu 4500001000800000005001500008008004100023034007400064100001800138245002700156  2008120160513104939730000s1973    coAa          0  0  EL  d0 aadE118.55522501eE118.55522501fS59.40043873gS59.40043873zSite 4161 aPerine, Alisa10aTest Publication 2008100280naaa 2200097zu 4500001000800000005001500008008004100023034007200064100001900136245002700155  2008220160513104939730000s1973    coAa          0  0  EL  d0 aadE19.95358751eE19.95358751fN66.64677012gN66.64677012zSite 4031 aWalter, Jasper10aTest Publication 2008200285naaa 2200097zu 4500001000800000005001500008008004100023034007200064100002400136245002700160  2008320160513104939730000s1973    coAa          0  0  EL  d0 aadW61.22077604eW61.22077604fS26.01320081gS26.01320081zSite 3771 aGreenblatt, Sherill10aTest Publication 2008300283naaa 2200097zu 4500001000800000005001500008008004100023034007200064100002200136245002700158  2008420160513104939730000s1973    coAa          0  0  EL  d0 aadW38.52101948eW38.52101948fN42.22226461gN42.22226461zSite 5481 aCrafts, Claudette10aTest Publication 2008400284naaa 2200097zu 4500001000800000005001500008008004100023034007400064100002100138245002700159  2008520160513104939730000s1973    coAa          0  0  EL  d0 aadE132.70789703eE132.70789703fN38.04866994gN38.04866994zSite 1881 aCounter, Modesto10aTest Publication 2008500281naaa 2200097zu 4500001000800000005001500008008004100023034007200064100002000136245002700156  2008620160513104939730000s1973    coAa          0  0  EL  d0 aadW89.35020317eW89.35020317fN47.93266336gN47.93266336zSite 3091 aCrowder, Carlee10aTest Publication 2008600280naaa 2200097zu 4500001000800000005001500008008004100023034007200064100001900136245002700155  2008720160513104939730000s1973    coAa          0  0  EL  d0 aadW125.32764205eW125.32764205fS4.19255958gS4.19255958zSite 3411 aJone, Federico10aTest Publication 2008700282naaa 2200097zu 4500001000800000005001500008008004100023034007200064100002100136245002700157  2008820160513104939730000s1973    coAa          0  0  EL  d0 aadW168.93025338eW168.93025338fS6.02872258gS6.02872258zSite 2641 aSchock, Lisandra10aTest Publication 2008800282naaa 2200097zu 4500001000800000005001500008008004100023034007000064100002300134245002700157  2008920160513104939730000s1973    coAa          0  0  EL  d0 aadW98.77347908eW98.77347908fS42.6798184gS42.6798184zSite 1191 aSpadaro, Geraldine10aTest Publication 2008900278naaa 2200097zu 4500001000800000005001500008008004100023034007000064100001900134245002700153  2009020160513104939730000s1973    coAa          0  0  EL  d0 aadE85.8591623eE85.8591623fN66.00524174gN66.00524174zSite 3251 aSwanson, Mario10aTest Publication 2009000286naaa 2200097zu 4500001000800000005001500008008004100023034007400064100002300138245002700161  2009120160513104939730000s1973    coAa          0  0  EL  d0 aadE137.11729029eE137.11729029fN39.84518323gN39.84518323zSite 1601 aCornforth, Desiree10aTest Publication 2009100276naaa 2200097zu 4500001000800000005001500008008004100023034007000064100001700134245002700151  2009220160513104939730000s1973    coAa          0  0  EL  d0 aadW65.3799254eW65.3799254fS46.40296668gS46.40296668zSite 3221 aLam, Cristen10aTest Publication 2009200283naaa 2200097zu 4500001000800000005001500008008004100023034007400064100002000138245002700158  2009320160513104939730000s1973    coAa          0  0  EL  d0 aadE176.53377147eE176.53377147fN22.77971971gN22.77971971zSite 3131 aFrankum, Thomas10aTest Publication 2009300284naaa 2200097zu 4500001000800000005001500008008004100023034007200064100002300136245002700159  2009420160513104939730000s1973    coAa          0  0  EL  d0 aadW27.45737117eW27.45737117fS55.67694086gS55.67694086zSite 3631 aMagallon, Christia10aTest Publication 2009400282naaa 2200097zu 4500001000800000005001500008008004100023034007400064100001900138245002700157  2009520160513104939730000s1973    coAa          0  0  EL  d0 aadE175.12324693eE175.12324693fS38.39888377gS38.39888377zSite 4401 aIverson, Beryl10aTest Publication 2009500274naaa 2200097zu 4500001000800000005001500008008004100023034007000064100001500134245002700149  2009620160513104939730000s1973    coAa          0  0  EL  d0 aadW23.4142272eW23.4142272fN29.80880028gN29.80880028zSite 3131 aOrtiz, Tia10aTest Publication 2009600276naaa 2200097zu 4500001000800000005001500008008004100023034007000064100001700134245002700151  2009720160513104939730000s1973    coAa          0  0  EL  d0 aadW18.33091801eW18.33091801fS6.31305738gS6.31305738zSite 3991 aBogard, Noma10aTest Publication 2009700281naaa 2200097zu 4500001000800000005001500008008004100023034007200064100002000136245002700156  2009820160513104939730000s1973    coAa          0  0  EL  d0 aadW72.31173246eW72.31173246fS46.75721273gS46.75721273zSite 3311 aWalston, Verlie10aTest Publication 2009800281naaa 2200097zu 4500001000800000005001500008008004100023034007200064100002000136245002700156  2009920160513104939730000s1973    coAa          0  0  EL  d0 aadW48.97356546eW48.97356546fN36.74731237gN36.74731237zSite 3291 aInnocent, Leisa10aTest Publication 2009900277naaa 2200097zu 4500001000800000005001500008008004100023034007200064100001600136245002700152  2010020160513104939730000s1973    coAa          0  0  EL  d0 aadW91.61595667eW91.61595667fN11.85462394gN11.85462394zSite 2661 aKohn, Clora10aTest Publication 2010000280naaa 2200097zu 4500001000800000005001500008008004100023034007200064100001900136245002700155  2010120160513104939730000s1973    coAa          0  0  EL  d0 aadW99.81880793eW99.81880793fN56.31339558gN56.31339558zSite 1271 aLinney, Samual10aTest Publication 2010100283naaa 2200097zu 4500001000800000005001500008008004100023034007400064100002000138245002700158  2010220160513104939730000s1973    coAa          0  0  EL  d0 aadW125.07883932eW125.07883932fS45.00178881gS45.00178881zSite 2591 aMerrifield, Kym10aTest Publication 2010200262naaa 2200097zu 4500001000800000005001500008008004100023034005400064100001900118245002700137  2010320160513104939730000s1973    coAa          0  0  EL  d0 aadE0362614eE0362614fS011543gS011543zSite 1811 aBucher, Margit10aTest Publication 2010300264naaa 2200097zu 4500001000800000005001500008008004100023034005400064100002100118245002700139  2010420160513104939730000s1973    coAa          0  0  EL  d0 aadE0181242eE0181242fS291354gS291354zSite 2271 aSteenberg, Shila10aTest Publication 2010400264naaa 2200097zu 4500001000800000005001500008008004100023034005400064100002100118245002700139  2010520160513104939730000s1973    coAa          0  0  EL  d0 aadE0983727eE0983727fN682744gN682744zSite 5311 aBlacker, Minerva10aTest Publication 2010500261naaa 2200097zu 4500001000800000005001500008008004100023034005400064100001800118245002700136  2010620160513104939730000s1973    coAa          0  0  EL  d0 aadE0963319eE0963319fN151338gN151338zSite 1301 aHanley, Karla10aTest Publication 2010600264naaa 2200097zu 4500001000800000005001500008008004100023034005400064100002100118245002700139  2010720160513104939730000s1973    coAa          0  0  EL  d0 aadW0753627eW0753627fN265359gN265359zSite 3661 aMackson, Yolanda10aTest Publication 2010700263naaa 2200097zu 4500001000800000005001500008008004100023034005400064100002000118245002700138  2010820160513104939730000s1973    coAa          0  0  EL  d0 aadE1792143eE1792143fS752257gS752257zSite 5231 aHarrill, Mariko10aTest Publication 2010800265naaa 2200097zu 4500001000800000005001500008008004100023034005400064100002200118245002700140  2010920160513104939730000s1973    coAa          0  0  EL  d0 aadW0953048eW0953048fS170735gS170735zSite 4021 aBehringer, Anitra10aTest Publication 2010900258naaa 2200097zu 4500001000800000005001500008008004100023034005400064100001500118245002700133  2011020160513104939730000s1973    coAa          0  0  EL  d0 aadW1184933eW1184933fN152720gN152720zSite 3511 aRowse, Ida10aTest Publication 2011000264naaa 2200097zu 4500001000800000005001500008008004100023034005400064100002100118245002700139  2011120160513104939730000s1973    coAa          0  0  EL  d0 aadW1374144eW1374144fS081546gS081546zSite 1531 aSutter, Katheryn10aTest Publication 2011100263naaa 2200097zu 4500001000800000005001500008008004100023034005400064100002000118245002700138  2011220160513104939730000s1973    coAa          0  0  EL  d0 aadW1705954eW1705954fS082612gS082612zSite 3541 aBeckett, Tamala10aTest Publication 2011200259naaa 2200097zu 4500001000800000005001500008008004100023034005400064100001600118245002700134  2011320160513104939730000s1973    coAa          0  0  EL  d0 aadE0783553eE0783553fS255532gS255532zSite 4171 aBetty, Eryn10aTest Publication 2011300265naaa 2200097zu 4500001000800000005001500008008004100023034005400064100002200118245002700140  2011420160513104939730000s1973    coAa          0  0  EL  d0 aadE1284246eE1284246fS020216gS020216zSite 2121 aLetsinger, Tamiko10aTest Publication 2011400267naaa 2200097zu 4500001000800000005001500008008004100023034005400064100002400118245002700142  2011520160513104939730000s1973    coAa          0  0  EL  d0 aadE0521421eE0521421fS022222gS022222zSite 4701 aCantrell, Cornelius10aTest Publication 2011500260naaa 2200097zu 4500001000800000005001500008008004100023034005400064100001700118245002700135  2011620160513104939730000s1973    coAa          0  0  EL  d0 aadE0331639eE0331639fS283049gS283049zSite 4771 aLeyva, Marie10aTest Publication 2011600265naaa 2200097zu 4500001000800000005001500008008004100023034005400064100002200118245002700140  2011720160513104939730000s1973    coAa          0  0  EL  d0 aadE0760744eE0760744fN480042gN480042zSite 4401 aBeveridge, Albina10aTest Publication 2011700261naaa 2200097zu 4500001000800000005001500008008004100023034005400064100001800118245002700136  2011820160513104939730000s1973    coAa          0  0  EL  d0 aadW0712539eW0712539fN295730gN295730zSite 5271 aBramhall, Ria10aTest Publication 2011800260naaa 2200097zu 4500001000800000005001500008008004100023034005400064100001700118245002700135  2011920160513104939730000s1973    coAa          0  0  EL  d0 aadE1681553eE1681553fN524510gN524510zSite 1391 aHurt, Millie10aTest Publication 2011900260naaa 2200097zu 4500001000800000005001500008008004100023034005400064100001700118245002700135  2012020160513104939730000s1973    coAa          0  0  EL  d0 aadW0140130eW0140130fS010120gS010120zSite 4641 aWulff, Nilsa10aTest Publication 2012000263naaa 2200097zu 4500001000800000005001500008008004100023034005400064100002000118245002700138  2012120160513104939730000s1973    coAa          0  0  EL  d0 aadW0782218eW0782218fS753724gS753724zSite 5041 aFirth, Theressa10aTest Publication 2012100263naaa 2200097zu 4500001000800000005001500008008004100023034005400064100002000118245002700138  2012220160513104939730000s1973    coAa          0  0  EL  d0 aadW1330422eW1330422fN034324gN034324zSite 4511 aWorster, Marita10aTest Publication 2012200265naaa 2200097zu 4500001000800000005001500008008004100023034005400064100002200118245002700140  2012320160513104939730000s1973    coAa          0  0  EL  d0 aadW0843206eW0843206fS553424gS553424zSite 2961 aOlveda, Jenniffer10aTest Publication 2012300263naaa 2200097zu 4500001000800000005001500008008004100023034005400064100002000118245002700138  2012420160513104939730000s1973    coAa          0  0  EL  d0 aadW1582630eW1582630fS142429gS142429zSite 2791 aMessner, Leanna10aTest Publication 2012400275naaa 2200097zu 4500001000800000005001500008008004100023034006900064100001700133245002700150  2012520160513104939730000s1973    coAa          0  0  EL  d0 aad-12.49085466e-7.49085466f+9.71187917g+4.71187917zSite 1731 aBevill, Sook10aTest Publication 2012500282naaa 2200097zu 4500001000800000005001500008008004100023034007200064100002100136245002700157  2012620160513104939730000s1973    coAa          0  0  EL  d0 aad+71.12605143e+81.12605143f+20.40214021g+10.40214021zSite 1211 aSchock, Lisandra10aTest Publication 2012600281naaa 2200097zu 4500001000800000005001500008008004100023034007400064100001800138245002700156  2012720160513104939730000s1973    coAa          0  0  EL  d0 aad+104.20929332e+154.20929332f+23.15670674g+13.15670674zSite 2831 aAbner, Shelli10aTest Publication 2012700282naaa 2200097zu 4500001000800000005001500008008004100023034007200064100002100136245002700157  2012820160513104939730000s1973    coAa          0  0  EL  d0 aad-130.3358613e-90.33586126f+54.37067475g+34.37067475zSite 3061 aNecessary, Dante10aTest Publication 2012800277naaa 2200097zu 4500001000800000005001500008008004100023034007300064100001500137245002700152  2012920160513104939730000s1973    coAa          0  0  EL  d0 aad+89.51162859e+109.51162859f-22.88853952g-42.88853952zSite 1271 aAmyx, Lyle10aTest Publication 2012900281naaa 2200097zu 4500001000800000005001500008008004100023034007400064100001800138245002700156  2013020160513104939730000s1973    coAa          0  0  EL  d0 aad+120.94552421e+130.94552421f+80.21089126g+77.21089126zSite 5191 aGranda, Isiah10aTest Publication 2013000280naaa 2200097zu 4500001000800000005001500008008004100023034007000064100002100134245002700155  2013120160513104939730000s1973    coAa          0  0  EL  d0 aad+166.31983556e+176.31983556f-64.114475g-74.114475zSite 1291 aFavela, Lorretta10aTest Publication 2013100277naaa 2200097zu 4500001000800000005001500008008004100023034007200064100001600136245002700152  2013220160513104939730000s1973    coAa          0  0  EL  d0 aad-33.35817709e-23.35817709f-26.04958019g-56.04958019zSite 3441 aDaub, Lloyd10aTest Publication 2013200278naaa 2200097zu 4500001000800000005001500008008004100023034007200064100001700136245002700153  2013320160513104939730000s1973    coAa          0  0  EL  d0 aad+100.28119821e+150.28119821f-39.5929073g-79.5929073zSite 2811 aFines, Gregg10aTest Publication 2013300279naaa 2200097zu 4500001000800000005001500008008004100023034006800064100002200132245002700154  2013420160513104939730000s1973    coAa          0  0  EL  d0 aad-98.724204e-38.72420401f+40.4924143g+20.4924143zSite 4941 aWalcott, Porfirio10aTest Publication 2013400280naaa 2200097zu 4500001000800000005001500008008004100023034007300064100001800137245002700155  2013520160513104939730000s1973    coAa          0  0  EL  d0 aad-170.45022257e-156.4502226f+25.08138959g+15.08138959zSite 4151 aGranda, Isiah10aTest Publication 2013500281naaa 2200097zu 4500001000800000005001500008008004100023034007300064100001900137245002700156  2013620160513104939730000s1973    coAa          0  0  EL  d0 aad+93.74890612e+143.74890612f-13.32263614g-23.32263614zSite 2561 aCutter, Annika10aTest Publication 2013600279naaa 2200097zu 4500001000800000005001500008008004100023034007200064100001800136245002700154  2013720160513104939730000s1973    coAa          0  0  EL  d0 aad-66.87358455e-46.87358455f+70.98897026g+50.98897026zSite 5431 aHanley, Karla10aTest Publication 2013700276naaa 2200097zu 4500001000800000005001500008008004100023034007000064100001700134245002700151  2013820160513104939730000s1973    coAa          0  0  EL  d0 aad-172.9265117e-162.9265117f-1.47082029g-6.47082029zSite 1691 aDalke, Isiah10aTest Publication 2013800281naaa 2200097zu 4500001000800000005001500008008004100023034007000064100002200134245002700156  2013920160513104939730000s1973    coAa          0  0  EL  d0 aad+33.61230501e+43.61230501f-4.12575355g-9.12575355zSite 3771 aBehringer, Anitra10aTest Publication 2013900281naaa 2200097zu 4500001000800000005001500008008004100023034007200064100002000136245002700156  2014020160513104939730000s1973    coAa          0  0  EL  d0 aad+75.10477259e+95.10477259f+73.86871739g+53.86871739zSite 2831 aWilhoite, Kathi10aTest Publication 2014000282naaa 2200097zu 4500001000800000005001500008008004100023034007400064100001900138245002700157  2014120160513104939730000s1973    coAa          0  0  EL  d0 aad+125.92523664e+135.92523664f-15.86550515g-25.86550515zSite 1841 aJasik, Micaela10aTest Publication 2014100283naaa 2200097zu 4500001000800000005001500008008004100023034007400064100002000138245002700158  2014220160513104939730000s1973    coAa          0  0  EL  d0 aad+112.65337661e+122.65337661f+78.04277975g+68.04277975zSite 5271 aStrine, Tabitha10aTest Publication 2014200276naaa 2200097zu 4500001000800000005001500008008004100023034006900064100001800133245002700151  2014320160513104939730000s1973    coAa          0  0  EL  d0 aad-10.83945484e-5.83945484f-30.6046605g-60.6046605zSite 1311 aOverall, Lula10aTest Publication 2014300279naaa 2200097zu 4500001000800000005001500008008004100023034007000064100002000134245002700154  2014420160513104939730000s1973    coAa          0  0  EL  d0 aad-15.53775937e-10.53775937f-46.3793589g-66.3793589zSite 2041 aMathisen, Dayle10aTest Publication 2014400281naaa 2200097zu 4500001000800000005001500008008004100023034007200064100002000136245002700156  2014520160513104939730000s1973    coAa          0  0  EL  d0 aad-147.1524264e-127.1524264f-70.97214091g-75.97214091zSite 4281 aWilhoite, Kathi10aTest Publication 2014500278naaa 2200097zu 4500001000800000005001500008008004100023034007200064100001700136245002700153  2014620160513104939730000s1973    coAa          0  0  EL  d0 aad+73.14760572e+93.14760572f-44.44305202g-64.44305202zSite 4881 aFines, Gregg10aTest Publication 2014600280naaa 2200097zu 4500001000800000005001500008008004100023034007400064100001700138245002700155  2014720160513104939730000s1973    coAa          0  0  EL  d0 aadE143.91680691eE153.91680691fN69.05928016gN59.05928016zSite 2301 aKinnan, Enda10aTest Publication 2014700281naaa 2200097zu 4500001000800000005001500008008004100023034007000064100002200134245002700156  2014820160513104939730000s1973    coAa          0  0  EL  d0 aadE109.2953057eE159.2953057fN75.6021786gN72.6021786zSite 2351 aHartfield, Shelba10aTest Publication 2014800282naaa 2200097zu 4500001000800000005001500008008004100023034007300064100002000137245002700157  2014920160513104939730000s1973    coAa          0  0  EL  d0 aadE144.81897837eE164.81897837fN18.62668817gN8.62668817zSite 2871 aSkeens, Cordell10aTest Publication 2014900278naaa 2200097zu 4500001000800000005001500008008004100023034007200064100001700136245002700153  2015020160513104939730000s1973    coAa          0  0  EL  d0 aadW115.49585203eW85.49585203fN19.56564384gN9.56564384zSite 4151 aHove, Armand10aTest Publication 2015000280naaa 2200097zu 4500001000800000005001500008008004100023034007200064100001900136245002700155  2015120160513104939730000s1973    coAa          0  0  EL  d0 aadW32.63289449eW22.63289449fS55.38513226gS75.38513226zSite 1201 aHaugh, Winford10aTest Publication 2015100279naaa 2200097zu 4500001000800000005001500008008004100023034007300064100001700137245002700154  2015220160513104939730000s1973    coAa          0  0  EL  d0 aadE87.77332672eE107.77332672fS54.25951534gS74.25951534zSite 3431 aValois, Arie10aTest Publication 2015200279naaa 2200097zu 4500001000800000005001500008008004100023034006800064100002200132245002700154  2015320160513104939730000s1973    coAa          0  0  EL  d0 aadW32.8377449eW22.8377449fS3.09135927gS8.09135927zSite 4141 aMcclaran, Kathern10aTest Publication 2015300277naaa 2200097zu 4500001000800000005001500008008004100023034007000064100001800134245002700152  2015420160513104939730000s1973    coAa          0  0  EL  d0 aadE34.4627201eE44.4627201fN23.51119899gN13.51119899zSite 3351 aHeinrich, See10aTest Publication 2015400282naaa 2200097zu 4500001000800000005001500008008004100023034007200064100002100136245002700157  2015520160513104939730000s1973    coAa          0  0  EL  d0 aadE11.25085415eE16.25085415fN62.44255328gN42.44255328zSite 3801 aSteenberg, Shila10aTest Publication 2015500278naaa 2200097zu 4500001000800000005001500008008004100023034007200064100001700136245002700153  2015620160513104939730000s1973    coAa          0  0  EL  d0 aadE35.02387108eE45.02387108fN51.72426854gN31.72426854zSite 1231 aNiccum, Alex10aTest Publication 2015600284naaa 2200097zu 4500001000800000005001500008008004100023034007400064100002100138245002700159  2015720160513104939730000s1973    coAa          0  0  EL  d0 aadE115.38134036eE145.38134036fS21.96462606gS41.96462606zSite 3801 aRussom, Clorinda10aTest Publication 2015700278naaa 2200097zu 4500001000800000005001500008008004100023034006600064100002300130245002700153  2015820160513104939730000s1973    coAa          0  0  EL  d0 aadW79.5685706eW59.5685706fS3.2186524gS8.2186524zSite 4461 aMontanye, Criselda10aTest Publication 2015800280naaa 2200097zu 4500001000800000005001500008008004100023034007200064100001900136245002700155  2015920160513104939730000s1973    coAa          0  0  EL  d0 aadE78.87843097eE98.87843097fS24.09348392gS44.09348392zSite 3721 aGalasso, Tatum10aTest Publication 2015900283naaa 2200097zu 4500001000800000005001500008008004100023034007400064100002000138245002700158  2016020160513104939730000s1973    coAa          0  0  EL  d0 aadW179.09659396eW119.09659396fS39.32213026gS79.32213026zSite 4631 aLiptak, Caitlin10aTest Publication 2016000282naaa 2200097zu 4500001000800000005001500008008004100023034007400064100001900138245002700157  2016120160513104939730000s1973    coAa          0  0  EL  d0 aadW163.27020238eW113.27020238fS36.75458726gS76.75458726zSite 1621 aGaetano, Dedra10aTest Publication 2016100261naaa 2200097zu 4500001000800000005001500008008004100023034005400064100001800118245002700136  2016220160513104939730000s1973    coAa          0  0  EL  d0 aadE1114626eE1214626fN752639gN452639zSite 4161 aTorian, Belva10aTest Publication 2016200262naaa 2200097zu 4500001000800000005001500008008004100023034005400064100001900118245002700137  2016320160513104939730000s1973    coAa          0  0  EL  d0 aadE0285018eE0335018fN271418gN171418zSite 2191 aMirabito, Shin10aTest Publication 2016300266naaa 2200097zu 4500001000800000005001500008008004100023034005400064100002300118245002700141  2016420160513104939730000s1973    coAa          0  0  EL  d0 aadE0330550eE0430550fN471910gN271910zSite 1651 aSpadaro, Geraldine10aTest Publication 2016400265naaa 2200097zu 4500001000800000005001500008008004100023034005400064100002200118245002700140  2016520160513104939730000s1973    coAa          0  0  EL  d0 aadW0501321eW0401321fN053851gN003851zSite 4651 aHartfield, Shelba10aTest Publication 2016500259naaa 2200097zu 4500001000800000005001500008008004100023034005400064100001600118245002700134  2016620160513104939730000s1973    coAa          0  0  EL  d0 aadE1541338eE1641338fS053823gS103823zSite 2621 aDaub, Lloyd10aTest Publication 2016600262naaa 2200097zu 4500001000800000005001500008008004100023034005400064100001900118245002700137  2016720160513104939730000s1973    coAa          0  0  EL  d0 aadW0722141eW0522141fS223403gS423403zSite 5141 aCutter, Annika10aTest Publication 2016700264naaa 2200097zu 4500001000800000005001500008008004100023034005400064100002100118245002700139  2016820160513104939730000s1973    coAa          0  0  EL  d0 aadW0053928eW0003928fS472308gS772308zSite 1971 aErickson, Joette10aTest Publication 2016800264naaa 2200097zu 4500001000800000005001500008008004100023034005400064100002100118245002700139  2016920160513104939730000s1973    coAa          0  0  EL  d0 aadW0675444eW0475444fN571945gN271945zSite 2791 aSharrow, Sherron10aTest Publication 2016900261naaa 2200097zu 4500001000800000005001500008008004100023034005400064100001800118245002700136  2017020160513104939730000s1973    coAa          0  0  EL  d0 aadE1064421eE1564421fN402522gN202522zSite 1701 aVidrio, Sofia10aTest Publication 2017000261naaa 2200097zu 4500001000800000005001500008008004100023034005400064100001800118245002700136  2017120160513104939730000s1973    coAa          0  0  EL  d0 aadW1254938eW0854938fN511735gN311735zSite 5101 aHeinrich, See10aTest Publication 2017100261naaa 2200097zu 4500001000800000005001500008008004100023034005400064100001800118245002700136  2017220160513104939730000s1973    coAa          0  0  EL  d0 aadW1344729eW0944729fN051424gN001424zSite 5111 aBoyland, Bebe10aTest Publication 2017200263naaa 2200097zu 4500001000800000005001500008008004100023034005400064100002000118245002700138  2017320160513104939730000s1973    coAa          0  0  EL  d0 aadE0932835eE1432835fS175621gS375621zSite 2151 aStrine, Tabitha10aTest Publication 2017300262naaa 2200097zu 4500001000800000005001500008008004100023034005400064100001900118245002700137  2017420160513104939730000s1973    coAa          0  0  EL  d0 aadE1701912eE1751912fS073104gS123104zSite 1931 aBornstein, Dia10aTest Publication 2017400266naaa 2200097zu 4500001000800000005001500008008004100023034005400064100002300118245002700141  2017520160513104939730000s1973    coAa          0  0  EL  d0 aadW1362047eW0962047fS250039gS550039zSite 4111 aGrizzell, Griselda10aTest Publication 2017500264naaa 2200097zu 4500001000800000005001500008008004100023034005400064100002100118245002700139  2017620160513104939730000s1973    coAa          0  0  EL  d0 aadW0545835eW0445835fS450010gS650010zSite 5091 aFalzone, Natasha10aTest Publication 2017600259naaa 2200097zu 4500001000800000005001500008008004100023034005400064100001600118245002700134  2017720160513104939730000s1973    coAa          0  0  EL  d0 aadE1455446eE1555446fS373045gS773045zSite 3521 aJaco, Angle10aTest Publication 20177
\ No newline at end of file
diff --git a/tests/data/geo.mrc.properties b/tests/data/geo.mrc.properties
new file mode 100644
index 0000000000000000000000000000000000000000..e5d632f91c36bb66fbc9c31a820dfe56632ad1df
--- /dev/null
+++ b/tests/data/geo.mrc.properties
@@ -0,0 +1,6 @@
+id = 001, (pattern_map.id_prefix), first
+pattern_map.id_prefix.pattern_0 = (.+)=>geo$1
+location_geo = custom, getAllCoordinates
+long_lat = custom, getPointCoordinates
+long_lat_display = custom, getDisplayCoordinates
+long_lat_label = 034z
diff --git a/tests/data/hierarchy.mrc b/tests/data/hierarchy.mrc
new file mode 100644
index 0000000000000000000000000000000000000000..143164a5cb1c0526183bcb110a5d29bd2302267d
--- /dev/null
+++ b/tests/data/hierarchy.mrc
@@ -0,0 +1 @@
+01425cas a22003371a 45  001001200000005001700012008004100029010003600070022001400106032001700120035001600137037006600153040012800219049001700347050001800364245002400382260004500406300002700451310001900478362005700497500002200554500006900576515003100645650004900676650003600725650003200761710006000793780006300853785010800916999006301024hiersample120091117105557.0840328d19831987nyufx1p   o   0   a0eng d  a   84643014 //r88zsn 84011556 0 a0748-1985  a002051bUSPS  aocm10569413  bHuman Sciences Press, Inc., 72 5th Ave., New York, N.Y. 10011  aKSWcKSWdCLUdAIPdNSTdDLCdNSDdNSTdDLCdHULdNSTdNSDdNLMdNSTdNSDdNSTdNSDdDLCdNSTdNSDdNSTdCLUdNSTdOCLdPVU  aPVUP[SERIAL]00aAC489.R3bJ6800aHierarchy Example 1  a[New York] :bThe Institute,c1983-1987.  a5 v. :bill. ;c26 cm.  aTwo no. a year0 aVol. 1, no. 1 (fall 1983)-v. 5, no. 4 (winter 1987).  aTitle from cover.  aVols. for <spring 1985-> published by Human Sciences Press, Inc.  aVol. 1 has only one issue. 0aRational-emotive psychotherapyvPeriodicals. 0aCognitive therapyvPeriodicals. 2aPsychotherapyvperiodicals.2 aInstitute for Rational-Emotive Therapy (New York, N.Y.)00tRational livingx0034-0049w(OCLC)1763461w(DLC)  7200064700tJournal of rational-emotive and cognitive-behavior therapyx0894-9085w(DLC)  88648219w(OCoLC)1630781400a0/level1a/b1/level1a/level2a/c2/level1a/level2a/level3a/01182cas a22002891a 45  001001200000005001700012007001400029008004100043010002300084022001400107030001100121035001600132040008300148050001800231245002400249260004900273300002800322362005000350550005000400550013200450580006300582650003200645710006000677710003500737785008500772999003500857hiersample220091117105643.0hd bfa   b|c|751101d19661983nyufr1p       0   a0eng    a   72000647 //r8520 a0034-0049  aATLVBX  aocm01763461  aDLCcMULdNSDdDLCdRCSdAIPdDLCdNSDdNSTdDLCdAIPdNSTdDLCdNSTdNSDdNST00aAA790.A1bR3500aHierarchy Example 2  a[New York] :bInstitute for Rational Living.  a18 v. :bill. ;c27 cm.0 aVol. 1 (Feb. 1966)-v.18, no. 1 (spring 1983).  aJournal of the Institute for Rational Living.  aIssued by: Institute for Rational Living, Feb. 1966-spring 1982; Institute for Rational-Emotive Therapy, fall 1982-spring 1983.  aContinued in 1983 by: Journal of rational emotive therapy. 0aMental healthvPeriodicals.2 aInstitute for Rational-Emotive Therapy (New York, N.Y.)2 aInstitute for Rational Living.10tJournal of rational emotive therapyx0748-1985w(DLC)  84643014w(OCoLC)1056941300a0/level1a/b1/level1a/level2b/01026cas a22002891a 45  001001200000005001700012008004100029010003000070022001400100035001600114037005600130040003300186050002100219245002400240260004800264300002800312310001400340362003300354362004000387500002200427650003200449650003500481650004200516785009900558936001600657999006300673hiersample320091117104735.0820311d19831998nyuqr1p   o   0   a0eng d  a   83-644252 zsn82-2030 0 a0731-7158  aocm08235783  bHaworth Press, 28 E. 22nd St., New York, N.Y. 10010  aNSDcNSDdDLCdAIPdNLMdNST0 aAC455.2.P73bP8900aHierarchy Example 3  a[New York, N.Y.] :bHaworth Press,c[c1983-  a17 v. :bill. ;c23 cm.  aQuarterly0 aVol. 1, no. 1 (spring 1983)-1 aCeased with: Vol. 17, no. 4 (1998).  aTitle from cover. 2aPsychotherapyvperiodicals. 2aPrivate Practicevperiodicals. 0aPsychotherapyxPracticevPeriodicals.00tJournal of psychotherapy in independent practicex1522-9580w(DLC)sn 98005091w(OCoLC)40524650  aSummer 198400a0/level1a/b1/level1a/level2a/c2/level1a/level2a/level3b/01331cas a22003491a 45  001001200000005001700012007001600029008004100045010001700086022001400103030001100117032001700128035001600145037005800161040006300219090001700282245002400299260003200323300001700355310001400372362003100386500002200417650006400439650003400503506006600537530004200603710002600645856009600671856011700767936003400884999006300918hiersample420091117105437.0cr  an ---uuuuu840713c19859999nyuqr1p       0   a0eng d  asn 84006638 0 a0748-4518  aJQCRE6  a755730bUSPS  aocm10945712  bPlenum Pub. Corp., 233 Spring St., New York, NY 10013  aNSDcNSDdFUGdNSDdNSTdIULdNSTdHULdNSTdEYMdNSTdHUL  aAV6001b.J7200aHierarchy Example 4  aNew York :bPlenum,cc1985-  av. ;c23 cm.  aQuarterly0 aVol. 1, no. 1 (Mar. 1985)-  aTitle from cover. 0aCriminal justice, Administration ofxResearchvPeriodicals. 0aCrimexResearchvPeriodicals.  aElectronic access restricted to Villanova University patrons.  aAlso available on the World Wide Web.2 aLINK (Online service)41zOnline version [v. 15 (1999)-present]uhttp://springerlink.metapress.com/link.asp?id=10258641zOff-campus accessuhttp://openurl.villanova.edu:9003/sfx_local?sid=sfx:e_collection&issn=0748-4518&genre=journal  aVol. 1, no. 4 (Dec. 1985) LIC00a0/level1a/b1/level1a/level2b/c2/level1a/level2b/level3c/01653cas a22003971a 45  001001200000005001700012007001500029007001400044008004100058010002200099022001400121030001100135032001700146035001600163040007900179050001700258245002800275260004300303300002500346362002400371506006600395530004200461550008700503555004200590500010300632650004300735700003600778700003500814700004600849710004900895710003400944856012100978856011701099936002401216999001501240hiersample520091117092424.0cr mn ---|||||hd bda   b|c|750901c19379999ncuqr1p       0   a0eng d  a   40012649 //r86  a0022-3387  aJPRPAU  a281800bUSPS  aocm01588544  aMULcMULdNSDdDLCdNSDdDLCdm.c.dRCSdAIPdNSDdAIPdOCLdDLCdm/cdNST00aAF1001b.J6604aThe Hierarchy Example 5  aDurham, N.C. :bDuke University Press.  av. :bill. ;c23 cm.0 aVol. 1 (Mar. 1937)-  aElectronic access restricted to Villanova University patrons.  aAlso available on the World Wide Web.  aIssued in cooperation with the Duke University Parapsychology Laboratory, 194 -50.  aVols. 1-16, 1937-52, in v. 17, no. 3.  aEditors: Mar. 1937-<Dec. 1938> J. B. Rhine, C. E. Stuart (with W. McDougall, Mar. 1937-Sept. 1938) 0aParapsychologyxResearchvPeriodicals.1 aMcDougall, William,d1871-1938.1 aStuart, Charles Edward,d1907-1 aRhine, J. B.q(Joseph Banks),d1895-1980.2 aDuke University.bParapsychology Laboratory.2 aProQuest Psychology Journals.41zOnline version [v. 59 (1995)-present]uhttp://proquest.umi.com/pqdweb?pmid=000029287&clientId=3260&RQT=318&VName=PQD41zOff-campus accessuhttp://openurl.villanova.edu:9003/sfx_local?sid=sfx:e_collection&issn=0022-3387&genre=journal  aMar. 1937-Dec. 195800a0/level1a/01536cas a22003371a 45  001001200000005001700012008004100029010003700070022001400107035001600121040003800137041002300175050001500198245002400213260010900237300002000346310002500366321002200391362005700413500002200470546004100492550017300533580007100706650003800777650003000815710004700845710007500892780008100967785008701048999006301135hiersample620091117105533.0751101d19571970inumr1p      o0   a0eng    aa  53008626 //r853zsc 80001624   a0095-9057  aocm01772601  aDLCcMULdNSDdDLCdm/cdHULdNST0 aengafreageraita0 aAA1b.J97500aHierarchy Example 6  aBloomington, Ind. :bGraduate Institute for Mathematics and Mechanics, Indiana University,cc1957-c1970.  a14 v. ;c26 cm.  aMonthly,b-June 1970  aBimonthly,b1957-0 aVol. 6, no. 1 (Jan. 1957)-v. 19, no. 12 (June 1970).  aTitle from cover.  aEnglish, French, German and Italian.  aVols. for 1957-   published by the Graduate Institute for Mathematics and Mechanics, Indiana University;     -June 1970 by the Dept. of Mathematics, Indiana University.  aContinued in July 1970 by: Indiana University mathematics journal. 0aMechanics, AnalyticvPeriodicals. 0aMathematicsvPeriodicals.2 aIndiana University.bDept. of Mathematics.2 aIndiana University.bGraduate Institute for Mathematics and Mechanics.00tJournal of rational mechanics and analysisw(DLC)sf 85001244w(OCoLC)226307110tIndiana University mathematics journalx0022-2518w(DLC)  75640369w(OCoLC)175301900a0/level1a/b1/level1a/level2a/c2/level1a/level2a/level3d/01452cas a22003851a 45  001001200000005001700012007001500029007001400044008004100058010001700099022001400116030001100130032001700141035001600158037006000174040005900234050001400293210001600307245002400323260004200347300002500389310002200414321002400436362002200460506006600482500004700548530004200595650002900637700003800666710003400704856012200738856011700860936002600977999006301003hiersample720091117102216.0cr mn ---|||||hd bfa   b|c|741112c19369999maubr1p       0   a0eng    a   38003075   a0022-3980  aJOPSAM  a282400bUSPS  aocm01782317  bJournal Press, 2 Commercial St., Provincetown, MA 02657  aDLCcDLCdOCLdDLCdNSDdm.c.dOCLdRCSdAIPdOCLdNSD00aAF1b.J671 aJ. psychol.04aHierarchy Example 7  aProvincetown, Mass. :bJournal Press.  av. :bill. ;c25 cm.  aBimonthly,b1965-  aQuarterly,b1935-640 aVol. 1 (1935/36)-  aElectronic access restricted to Villanova University patrons.  aEditor: 1935/36-<Oct. 1937,> C. Murchison.  aAlso available on the World Wide Web. 0aPsychologyvPeriodicals.1 aMurchison, Carl Allanmore,d1887-2 aProQuest Psychology Journals.41zOnline version [v. 128 (1994)-present]uhttp://proquest.umi.com/pqdweb?pmid=000014346&clientId=3260&RQT=318&VName=PQD41zOff-campus accessuhttp://openurl.villanova.edu:9003/sfx_local?sid=sfx:e_collection&issn=0022-3980&genre=journal  aJan. 1975 (surrogate)00a0/level1a/b1/level1a/level2b/c2/level1a/level2b/level3e/01226cas a22003131a 45  001001200000005001700012007001500029008004100044010002200085022001400107030001100121035001600132040007300148050001400221245002400235260004200259300002500301362002400326506006600350500003000416530004200446650004100488650005200529700001800581710003600599856009700635856011700732999006300849hiersample820091117102549.0cr an         741112c19569999enkbrzp       0   a0eng    a   57003141 //r830 a0022-3999  aJPCRAT  aocm01782774  aDLCcDLCdNSDdOCLdNSDdDLCdSERdOCLdRCSdDLCdOCLdAIPdOCLdNSD00aAC52b.J600aHierarchy Example 8  aLondon ;aNew York :bPergamon Press.  av. :bill. ;c26 cm.0 aVol. 1 (Feb. 1956)-  aElectronic access restricted to Villanova University patrons.  aEditor: 1956-   D. Leigh.  aAlso available on the World Wide Web. 2aPsychosomatic Medicinevperiodicals. 0aMedicine, PsychosomaticxResearchvPeriodicals.1 aLeigh, Denis.2 aScienceDirect (Online service).41zOnline version [v. 44 (1998)-present]uhttp://www.sciencedirect.com/science/journal/0022399941zOff-campus accessuhttp://openurl.villanova.edu:9003/sfx_local?sid=sfx:e_collection&issn=0022-3999&genre=journal00a0/level1z/b1/level1z/level2z/c2/level1z/level2z/level3z/02079cas a2200445 a 4500001001200000005001700012006001900029007001500048008004100063010001700104035002300121040005000144022002800194037008800222050001300310049000900323245002800332260007000360310002800430321003600458362003100494500005200525500003300577500009500610506006600705538003600771550012000807650003700927650002200964650002900986710005101015710005701066710002501123776009601148780007501244856012201319856011701441994001201558999006301570hiersample920091117160642.0m        d        cr mnu||||||||741112c19659999pauqx pss     0   a0eng d  a  2006212058  a(OCoLC)ocm39109327  aFGAcFGAdOCLdOCLCQdFQMdNSDdEYMdHULdPVU0 a1559-8519y0022-449921  bSociety for the Scientific Study of Sexuality, PO Box 416, Allentown, PA 18105-041614aAQ5b.J6  aPVUM14aThe Hierarchy Example 9  aNew York, N.Y. :bSociety for the Scientific Study of Sex,c1965-  a4 issues yearly,b1967-  aThree issues yearly,b1965-19660 aVol. 1, no. 1 (Mar. 1965)-  aTitle from cover (JSTOR, viewed Apr. 18, 2007).  aPlace of publication varies.  aLatest issue consulted: Vol. 43, issue 4 (Nov. 2006) (SSSS website, viewed Apr. 18, 2007).  aElectronic access restricted to Villanova University patrons.  aMode of access: World Wide Web.  aIssued by: Society for the Scientific Study of Sex, 1965-1994; Society for the Scientific Study of Sexuality, 1995- 0aSexologyxResearchvPeriodicals.12aSexvPeriodicals.22aPsychiatryvPeriodicals.2 aSociety for the Scientific Study of Sex (U.S.)2 aSociety for the Scientific Study of Sexuality (U.S.)2 aJSTOR (Organization)08iAlso issued in print:tJournal of sex researchx0022-4499w(DLC)   68130414w(OCoLC)178336500tAdvances in sex research (Online)w(DLC)  2007234124w(OCoLC)12320877540zOnline version [JSTOR: v. 1 (1965)-present ; latest 5 years unavailable]uhttp://www.jstor.org/journals/00224499.html40zOff-campus accessuhttp://openurl.villanova.edu:9003/sfx_local?sid=sfx:e_collection&issn=0022-4499&genre=journal  aC0bPVU00a0/level1z/b1/level1z/level2y/c2/level1z/level2y/level3g/01156cas a22003131a 45  001001300000005001700013007001600030008004100046010001700087022001400104030001100118035001600129037001000145040005300155050001800208090001700226245002500243260005100268300001700319362002400336500003500360506006600395530004200461650004600503710002000549856009300569856011700662999006300779hiersample1020091117093002.0cr  bn ---|||||751002c19709999njufr1p       0   a0eng    a   75643076 0 a0047-2662  aJPHPAE  aocm02240975  b07716  aDLCcDLCdNSDdDLCdOCLdRCSdAIPdNSDdOCLdNST0 aAF204.5b.J68  aAF204.5b.J600aHierarchy Example 10  a[Atlantic Highlands, N.J. :bHumanities Press]  av. ;c23 cm.0 aVol. 1 (fall 1970)-  aAt head of title <1974->: JPP.  aElectronic access restricted to Villanova University patrons.  aAlso available on the World Wide Web. 0aPhenomenological psychologyvPeriodicals.2 aIngenta (Firm).41zOnline version [v. 1 (1970/71)-present]uhttp://www.ingentaconnect.com/content/brill/jpp41zOff-campus accessuhttp://openurl.villanova.edu:9003/sfx_local?sid=sfx:e_collection&issn=0047-2662&genre=journal00a0/level1z/b1/level1z/level2y/c2/level1z/level2y/level3g/
\ No newline at end of file
diff --git a/tests/data/hierarchy.mrc.properties b/tests/data/hierarchy.mrc.properties
new file mode 100644
index 0000000000000000000000000000000000000000..317c14184f372c40979bf01b85efc9c724ad10ed
--- /dev/null
+++ b/tests/data/hierarchy.mrc.properties
@@ -0,0 +1,3 @@
+# The 999 field is being used to set up a test hierarchy here; this is not a
+# recommended practice, simply an easy way to set up a fake example.
+hierarchical_facet_str_mv = 999a:999b:999c
\ No newline at end of file
diff --git a/themes/bootprint3/less/bootprint.less b/themes/bootprint3/less/bootprint.less
index b42b1deb138b3f13e06b72923d6c05b891522be7..cbf1d366e54db64a000275104c790a33a3aa633c 100644
--- a/themes/bootprint3/less/bootprint.less
+++ b/themes/bootprint3/less/bootprint.less
@@ -1,274 +1,222 @@
 @import "bootstrap";
 @import "variables";
 @import "icons";
+@import "search";
+
+@brand-primary: #619144; // a11y overrides @brand-primary in sass
+@active-orange: #E70;
 
 /* --- Bootstrap MODS ---*/
 body {
-  background:@brand-primary;
-  font-size:13px;
-  & a,.btn-link {color:#06C}
-  & a:hover,.btn-link:hover {color:#09F}
-  @media (max-width: 767px) {
-    padding:6px;
-    header {
-      margin-top:0;
-    }
-    .label {
-      font-size:85%;
-    }
-  }
+  background: @brand-primary;
+  font-size: 13px
 }
-@media (min-width: 768px) {
-  .badge {
-    font-size:85%;
-    margin-top:1px;
-  }
-  .label { padding-top:.3em; }
-  .modal-dialog { width: 650px; }
+.container {
+  background: #FFF;
+  padding: 0;
 }
-.btn {
-  padding:3px 5px 2px;
-  &.btn-default {
-    background:@gray-lighter;
-    background-image:linear-gradient(#FFF, @nav-tabs-border-color);
-    border:1px solid @gray-light;
-    color:@gray-darker;
-    text-shadow:0 1px 0 #FFF;
-    &:hover {
-      border:1px solid @gray-dark;
-      color:@gray-darker;
-      text-shadow:none;
-    }
-  }
+.main .container { padding: 0 4px 18px; }
+
+a,
+.btn-link {
+  color: #06C;
+  &:hover { color: #09F; }
 }
-.btn-danger, .btn-danger:hover  {border-color:darken(@brand-danger,  12%);font-weight:bold;}
-.btn-info,   .btn-info:hover    {border-color:darken(@brand-info,    12%);font-weight:bold;}
-.btn-primary,.btn-primary:hover {border-color:darken(@brand-primary, 12%);font-weight:bold;}
-.btn-success,.btn-success:hover {border-color:darken(@brand-success, 12%);font-weight:bold;}
-.btn-warning,.btn-warning:hover {border-color:darken(@brand-warning, 12%);font-weight:bold;}
-h2 {margin:0 8px 8px}
-input[type="radio"], input[type="checkbox"] {
-  margin:0 auto;
-  margin-top:2px;
-  padding:0 2px;
+.alert { padding: 8px; }
+.btn { padding: 3px 5px 2px; }
+.btn.btn-default {
+  background: @gray-lighter;
+  background-image: linear-gradient(#FFF, @nav-tabs-border-color);
+  border: 1px solid @gray;
+  color: @gray-darker;
+  text-shadow: 0 1px 0 #FFF;
 }
-.nav > li > a {
-  padding:5px 10px;
+.btn:not(.btn-default) { font-weight:bold; }
+.btn-danger,
+.btn-danger:hover  { border-color:darken(@brand-danger,  12%); }
+.btn-info,
+.btn-info:hover    { border-color:darken(@brand-info,    12%); }
+.btn-primary,
+.btn-primary:hover { border-color:darken(@brand-primary, 12%); }
+.btn-success,
+.btn-success:hover { border-color:darken(@brand-success, 12%); }
+.btn-warning,
+.btn-warning:hover { border-color:darken(@brand-warning, 12%); }
+
+#commentList .comment:nth-child(even) { background: @gray-lighter; }
+#dateVisColorSettings { stroke: @brand-primary; }
+#hierarchyRecord { background: #FFF; }
+h2 { margin: 0 8px 8px; }
+input[type=radio],
+input[type=checkbox] {
+  margin: 2px auto 0;
+  padding: 0 2px;
 }
-.nav-pills {display:table;margin:0 auto}
-.navbar {min-height:1px;}
+.nav > li > a { padding: 5px 10px; }
+.nav-pills {
+  display: table;
+  margin: 0 auto;
+}
+.navbar { min-height: 1px; }
 .navbar-form {
-  margin-top:5px;
-  margin-bottom:5px;
+  margin-top: 5px;
+  margin-bottom: 5px;
 }
 .pagination {
-  display:table;
-  margin:18px auto;
-  & > li {
-    & > a,& > span {
-      padding:4px 12px 3px 12px;
-    }
-  }
-  & > .active {
-    & > a, & > span, & > a:hover, & > span:hover, & > a:focus, & > span:focus {
-      background:@brand-primary;
-      border-color:@brand-primary;
-    }
-  }
+  display: table;
+  margin: 18px auto;
 }
-.panel-heading {
-  padding:0;
-  a {
-    cursor:pointer;
-    display:inline-block;
-    padding:6px;
-    width:100%;
-  }
+.pagination > li > a,
+.pagination > li > span { padding: 4px 12px 3px; }
+.pagination > .active > a,
+.pagination > .active > a:focus,
+.pagination > .active > a:hover,
+.pagination > .active > span,
+.pagination > .active > span:focus,
+.pagination > .active > span:hover {
+  background: @brand-primary;
+  border-color: @brand-primary;
 }
-.row {
-  &:not(.top-row) {
-    padding:6px 4px;
-    margin:0 -4px;
-  }
-  &.result {
-    &:nth-child(even) {
-      background:@gray-lighter;
-    }
-    &:last-child {
-      margin-bottom:1em;
-    }
-  }
-  & > p {padding:0 1em;}
+.panel-heading { padding: 0; }
+.panel-heading a {
+  cursor: pointer;
+  display: inline-block;
+  padding: 6px;
+  width: 100%;
 }
-.search .row {
-  padding:2px 0;
-}
-.sub-breadcrumb {
-  padding:0 5px;
+.row:not(.top-row) {
+  padding: 6px 4px;
+  margin: 0 -4px;
 }
+.row > p { padding: 0 1em; }
+.sub-breadcrumb { padding: 0 5px; }
 .tab-content {
   padding: 6px 8px;
-  border: 1px solid @nav-tabs-border-color;
+  border: 1px solid @gray-lighter;
   border-top: 0;
 }
 
-/* --- Layout --- */
-.container {
-  background:#FFF;
-  padding:0;
+@media (max-width: 767px) {
+  body { padding: 6px; }
+  header { margin-top: 0; }
+  .label { font-size: 85%; }
 }
-header {
-  margin-top:18px;
-  .fa.fa-bars {font-size:21px}
-  .navbar {
-    border-radius:5px 5px 0 0;
-    padding:0 10px;
-    .searchForm {display:none !important}
-    .navbar-brand {
-      background-image:url('../../images/vufind_logo.png');
-      background-position: center center;
-      background-repeat: no-repeat;
-      background-size: contain;
-      color:transparent;
-      height:65px;
-      margin-top:5px;
-      width:170px;
-      &:hover,&:active,&:focus {
-        color:transparent;
-      }
-    }
-    .navbar-brand.lang-ar {background-image:url('../../images/vufind_logo_ar.png');}
-    .navbar-nav > li > a {
-      padding:12px 6px;
-      @media (max-width: 767px) {
-        padding:8px 24px;
-      }
-    }
-    .navbar-right {
-      margin-top:12px;
-      @media (max-width: 767px) {
-        margin:0;
-      }
-    }
-  }
-  .searchbox {
-    background:linear-gradient(to bottom, #FFF, #EEE);
-    display:block !important;
-    .tab-content {
-      border:0;
-      .navbar-text {
-        margin:5px 10px 5px 0;
-      }
-    }
+@media (min-width: 768px) {
+  .badge {
+    font-size: 85%;
+    margin-top: 1px;
   }
+  .label { padding-top: .3em; }
+  .modal-dialog { width: 650px; }
+}
+
+/* --- Header --- */
+header { margin-top: 18px; }
+header .fa.fa-bars { font-size: 21px; }
+header .navbar {
+  border-radius: 5px 5px 0 0;
+  padding: 0 10px;
+  .searchForm { display: none !important; }
+  .navbar-brand {
+    height: 65px;
+    width: 170px;
+    margin-top: 5px;
+    color: transparent;
+    background-image: ~"url('../../bootprint3/images/vufind_logo.png')";
+    background-position: center center;
+    background-repeat: no-repeat;
+    background-size: contain;
+    &:active,
+    &:focus,
+    &:hover { color: transparent; }
+    &.lang-ar { background-image: ~"url('../../bootprint3/images/vufind_logo_ar.png')"; }
+  }
+  .navbar-nav > li > a { padding: 12px 6px; }
+  .navbar-right { margin-top: 12px; }
   @media (max-width: 767px) {
-    #header-collapse .navbar-right li {
-      text-align:right;
-    }
-    .searchForm_type {
-      margin-top:2px;
-      margin-bottom:2px;
-    }
-  }
-  .breadcrumb {
-    border:1px solid #CCC;
-    border-radius:0;
-    border-width:1px 0;
-    font-size:12px;
-    margin-bottom:2px;
-    padding:7px 20px 5px;
+    .navbar-nav > li > a { padding: 8px 24px; }
+    .navbar-right { margin: 0; }
   }
 }
-.searchForm_lookfor,.searchForm_type {
-  border-color:@brand-primary;
+header .searchbox {
+  background: linear-gradient(to bottom, #FFF, #EEE);
+  display: block !important;
 }
-[name=searchForm] {
-  margin:6px 8px 8px;
-  padding:0;
-  & .btn-primary,& .form-control,
-  .template-dir-eds.template-name-advanced & .clear-btn,
-  .template-dir-search.template-name-advanced & .clear-btn {
-    font-size:14px;
-    height:32px;
-    padding:5px 8px;
-    &[multiple] {height:auto}
-  }
-  .search-query {
-    @media (min-width: 768px) {
-      width:400px;
-    }
-  }
-  .nav-tabs {
-    border-bottom:0;
-    padding:0 6px;
-    li {
-      a {
-        margin-bottom:-1px;
-        border-bottom:0;
-        padding-bottom:6px;
-        &:hover {
-          background:none;
-          border-color:transparent;
-          text-decoration:underline;
-        }
-      }
-      &.active a,&.active a:hover {
-        background:#FFF;
-        border-color:@brand-primary;
-        border-bottom:0;
-        text-decoration:none;
-        z-index:5;
-      }
-    }
+header .searchbox .tab-content { border: 0; }
+header .searchbox .tab-content .navbar-text { margin: 5px 10px 5px 0; }
+@media (max-width: 767px) {
+  header #header-collapse .navbar-right li { text-align: right; }
+  header .searchForm_type {
+    margin-top: 2px;
+    margin-bottom: 2px
   }
 }
-.main {
-  .container {
-    padding:0 4px 18px;
-  }
+header .breadcrumb {
+  border: 1px solid @gray-lighter;
+  border-radius: 0;
+  border-width: 1px 0;
+  font-size: 12px;
+  margin-bottom: 2px;
+  padding: 7px 20px 5px
 }
-footer {
-  margin-bottom:36px;
-  .container {
-    border-radius:0 0 5px 5px;
-    border-top:1px solid @nav-tabs-border-color;
-    padding-top:18px;
-  }
-  hr {display:none}
-  p {margin:0}
-  ul {padding-left:30px}
+
+/* --- Footer --- */
+footer { margin-bottom: 36px; }
+footer .container {
+  border-radius: 0 0 5px 5px;
+  border-top: 1px solid @nav-tabs-border-color;
+  padding-top: 18px;
 }
+footer hr { display: none; }
+footer p { margin: 0; }
+footer ul { padding-left: 30px; }
+
+/* --- Browse --- */
+[id^=list].list-group .col-sm-9 { margin: 0; }
 
 /* --- Offcanvas --- */
-body.offcanvas .sidebar .list-group {color: #000;}
-body.offcanvas .sidebar .list-group {color: #000;}
-body.offcanvas .offcanvas-toggle {
-  padding-bottom: 18px;
-  font-size: 16px;
-  background: #fff;
-  box-shadow: 0 0 2px #000;
-  color: @brand-primary;
+body.offcanvas {
+  .offcanvas-toggle {
+    padding-bottom: 18px;
+    font-size: 16px;
+    background: #fff;
+    box-shadow: 0 0 2px #000;
+    color: @brand-primary;
+  }
+  .sidebar .list-group { color: #000; }
+  &.active .sidebar { color: #FFF; }
+  &.active .offcanvas-toggle { box-shadow: none; }
+}
+
+/* --- Random Items (results view) --- */
+ul.random {
+  list-style: none;
+  padding: 0;
+  margin: 0;
+  text-align: justify;
 }
-body.offcanvas.active .sidebar {color: #FFF;}
-body.offcanvas.active .offcanvas-toggle {box-shadow: none;}
+ul.random li { padding-bottom: 10px; }
+ul.random li img { margin: 0 auto 1em; }
+ul.random.image,
+ul.random.mixed { text-align: center; }
+ul.random.image li img { margin: 0 auto; }
 
-/* --- PubDateVis --- */
-#dateVisColorSettings {
-  background-color: #fff; // background of box
-  fill: rgb(234,234,234); // fillColor
-  outline-color: #e8cfac; // selection color
-  stroke: @brand-primary; // color
+/* --- ReCaptcha --- */
+#custom_recaptcha_widget { display: table; }
+#custom_recaptcha_widget embed { display: none; }
+#custom_recaptcha_widget #recaptcha_image {
+  border: 1px solid #000;
+  padding: 6px;
+  margin: 1em 0;
+}
+#custom_recaptcha_widget #recaptcha_response_field { margin: 0 .5em; }
+#custom_recaptcha_widget > div > a {
+  display: inline-block;
+  float: left;
+  margin: 5px 10px 5px 0;
 }
 
 /* --- Record --- */
-#commentList {
-  .comment {
-    &:nth-child(even) {
-      background:@gray-lighter;
-    }
-  }
-}
-#hierarchyRecord {background:#FFF}
 .tagList button {
   margin-top: 0;
   padding-top: 0;
@@ -279,141 +227,65 @@ body.offcanvas.active .offcanvas-toggle {box-shadow: none;}
 .tagList button .fa-close { margin-top: 3px; }
 
 /* --- Search --- */
-.alert {padding:8px}
-.bulkActionButtons {margin-bottom:6px}
+.bulkActionButtons { margin-bottom: 6px; }
 .result {
-  & > .col-xs-1.checkbox {
-    width:auto;
-  }
-  .label {
-    display:inline-block;
-    margin-bottom:4px;
-  }
-  .left {text-align:center}
-  .savedLists {
-    margin:0 0 4px 0;
-    padding:4px 0 4px 6px;
-    ul {padding-left:18px;}
-  }
-  @media (max-width:767px) {
-    .search-controls .form-inline {
-      text-align:left;
-    }
-  }
-  @media (min-width:768px) {
-    & > .col-xs-1.checkbox {
-      padding:0;
-    }
-    & > .col-xs-11 {
-      width:95%;
-      padding:0;
-    }
+  padding: 1rem;
+  margin-left: -1.1rem;
+  &:nth-child(even) { background-color: @gray-lighter; }
+  &.embedded .getFull.expanded {
+    margin-top: -6px;
+    padding-top: .5rem;
+    padding-bottom: .5rem;
   }
 }
-.search-controls {
-  label {text-align:left}
-  @media (max-width:767px) {
-    margin:4px -4px;
-    padding:4px 0;
-  }
+.result > p {padding:0 1em;}
+.result .label {
+  display: inline-block;
+  margin-bottom: 4px;
 }
-
-/* --- Search Home --- */
-.searchHomeContent {
-  float:none;
-  margin:1em auto;
-  width:90%;
+.result .long-view .tab-content {background: #FFF;}
+.result .media { margin: 0; }
+.result .row { padding: 0; }
+.result .savedLists {
+  margin: 0 0 4px;
+  padding: 4px 0 4px 6px;
+}
+.result .savedLists ul { padding-left: 18px; }
+.search-controls label { text-align: left; }
+@media (max-width: 767px) {
+  .result .search-controls .form-inline { text-align: left; }
+  .search-controls {
+    margin: 4px -4px;
+    padding: 4px 0;
+  }
 }
-
-/* --- Advanced Search --- */
-#advSearchForm .search {margin:0}
-.group .match {margin-top: .5em;}
-.template-dir-eds.template-name-advanced .clear-btn,
-.template-dir-search.template-name-advanced .clear-btn {background-image: linear-gradient(#fff, #eee);}
 
 /* --- Sidebar --- */
-@active-orange: #E70;
 .sidebar {
-  .list-group {
-    margin-bottom:5px;
-    label.list-group-item {
-      padding-left:26px;
-      input[type=checkbox] {
-        margin-top:2px;
-      }
-    }
-  }
-  .list-group-item {
-    padding:7px 10px 6px;
-    &.active {
-      background:@active-orange;
-      border-color:@active-orange;
-      color:#FFF;
-      &:hover {background:@active-orange;border-color:@active-orange;}
-      .badge {color:@active-orange;}
-    }
-  }
-  & .slider-container {
-    margin:4px auto 10px;
-    width:95%;
-    .slider-handle {
-      background:#619144;
-      opacity:1;
-    }
-  }
-}
-.sidebar .list-group-item,.top-row {
-  .badge a {
-    color:#FFF;
-    &:hover {color:@brand-danger}
-  }
-}
+  .list-group { margin-bottom: 5px; }
+  .list-group label.list-group-item { padding-left: 26px; }
+  .list-group label.list-group-item input[type=checkbox] { margin-top: 2px; }
 
-/* --- Captcha --- */
-#custom_recaptcha_widget {
-  display:table;
-  embed { display:none; }
-  #recaptcha_image {
-    border:1px solid #000;
-    padding:6px;
-    margin:1em 0;
-  }
-  #recaptcha_response_field { margin:0 .5em; }
-  & > div > a {
-    display:inline-block;
-    float:left;
-    margin:5px 10px 5px 0;
-  }
+  .list-group-item { padding: 7px 10px 6px; }
+  .list-group-item.active { color: #fff; }
+  .list-group-item.active .badge { color: @active-orange; }
+  .list-group-item.active,
+  .list-group-item.active:hover {
+    background: @active-orange;
+    border-color: @active-orange;
+  }
+  .list-group-item .badge a { color: #fff; }
 }
-
-/* --- Random Items (results view) --- */
-ul.random {
-  list-style: none;
-  padding: 0;
-  margin: 0px;
-  text-align:justify;
-  li {
-    padding-bottom:10px;
-    img {
-      margin: 0 auto 1em auto;
-    }
-  }
-  &.image, &.mixed {
-    text-align: center;
-  }
-  &.image li img {
-    margin: 0 auto;
-  }
+.slider-container .slider-handle {
+  background: @brand-primary;
+  border: 1px solid @brand-primary;
+  &:hover,&:active,&:focus {
+    background: #FFF;
+    border-color: @gray-light;
+  }
+  &:active,&:focus { border-color: @brand-primary; }
 }
-
-/* --- Twitter Typeahead --- */
-.tt-dropdown-menu {
-  margin:2px;
+.top-row .badge a {
+  color: #fff;
+  &:hover { color: @brand-danger; }
 }
-
-/* --- Browse --- */
-[id^=list].list-group .col-sm-9 {margin:0}
-
-/* --- VuDL --- */
-.container > .row > .col-xs-12 { padding:0 }
-.vudl.row { padding:4px 0 }
\ No newline at end of file
diff --git a/themes/bootprint3/less/icons.less b/themes/bootprint3/less/icons.less
index 869df78ae7badf6a1c9f0cebdb6fa883c7c06b11..6ab6c37e0afffa613bea4688f4ff7b97c873027c 100644
--- a/themes/bootprint3/less/icons.less
+++ b/themes/bootprint3/less/icons.less
@@ -1,122 +1,123 @@
+@bp3-icon-path: '../../bootprint3/images/icons';
 .bp-icon {
-  background-position:center center;
-  background-repeat:no-repeat;
-  color:transparent;
-  content:'';
-  display:inline-block;
-  height:16px;
-  margin:0;
-  padding:0;
-  text-shadow:none;
-  vertical-align:text-bottom;
-  width:16px;
+  background-position: center center;
+  background-repeat: no-repeat;
+  color: transparent;
+  content: '';
+  display: inline-block;
+  height: 16px;
+  margin: 0;
+  padding: 0;
+  text-shadow: none;
+  vertical-align: text-bottom;
+  width: 16px;
 }
-.fa-x {background-image:url('../../images/icons/page_white.png'); &:extend(.bp-icon);}
-i.fa-archive {background-image:url('../../images/icons/package.png'); &:extend(.bp-icon);}
-i.fa-asterisk {background-image:url('../../images/icons/list.png'); &:extend(.bp-icon);}
-i.fa-atlas {background-image:url('../../images/icons/map.png'); &:extend(.bp-icon);}
-i.fa-bell {background-image:url('../../images/icons/bell.png'); &:extend(.bp-icon);}
-i.fa-book {background-image:url('../../images/icons/book.png'); &:extend(.bp-icon);}
-i.fa-bookbag-add {background-image:url('../../images/icons/bookbag_add.png'); &:extend(.bp-icon);}
-i.fa-bookbag-delete {background-image:url('../../images/icons/bookbag_delete.png'); &:extend(.bp-icon);}
-i.fa-bookbag-empty {background-image:url('../../images/icons/bookbag_empty.png'); &:extend(.bp-icon);}
-i.fa-bookmark {background-image:url('../../images/icons/bookmark_add.png'); &:extend(.bp-icon);}
-i.fa-braille {background-image:url('../../images/icons/page_red.png'); &:extend(.bp-icon);}
-i.fa-cancel-all-holds {background-image:url('../../images/icons/holdCancelAll.png'); &:extend(.bp-icon);}
-i.fa-cancel-all-storage-retrieval-requests {background-image:url('../../images/icons/holdCancelAll.png'); &:extend(.bp-icon);}
-i.fa-cancel-holds {background-image:url('../../images/icons/holdCancel.png'); &:extend(.bp-icon);}
-i.fa-cancel-storage-retrieval-requests {background-image:url('../../images/icons/holdCancel.png'); &:extend(.bp-icon);}
-i.fa-cdrom {background-image:url('../../images/icons/cd.png'); &:extend(.bp-icon);}
-i.fa-chart {background-image:url('../../images/icons/chart_bar.png'); &:extend(.bp-icon);}
-i.fa-chipcartridge {background-image:url('../../images/icons/server.png'); &:extend(.bp-icon);}
-i.fa-collage {background-image:url('../../images/icons/pictures.png'); &:extend(.bp-icon);}
-i.fa-close {background-image:url('../../images/icons/cross.png'); &:extend(.bp-icon);}
-i.fa-disccartridge {background-image:url('../../images/icons/cd.png'); &:extend(.bp-icon);}
-i.fa-drawing {background-image:url('../../images/icons/photo.png'); &:extend(.bp-icon);}
-i.fa-ebook {background-image:url('../../images/icons/book_addresses.png'); &:extend(.bp-icon);}
-i.fa-edit {background-image:url('../../images/icons/edit.png'); &:extend(.bp-icon);}
-i.fa-electronic {background-image:url('../../images/icons/mouse.png'); &:extend(.bp-icon);}
+.fa-x { background-image: ~"url('@{bp3-icon-path}/page_white.png')"; &:extend(.bp-icon); }
+i.fa-archive { background-image: ~"url('@{bp3-icon-path}/package.png')"; &:extend(.bp-icon); }
+i.fa-asterisk { background-image: ~"url('@{bp3-icon-path}/list.png')"; &:extend(.bp-icon); }
+i.fa-atlas { background-image: ~"url('@{bp3-icon-path}/map.png')"; &:extend(.bp-icon); }
+i.fa-bell { background-image: ~"url('@{bp3-icon-path}/bell.png')"; &:extend(.bp-icon); }
+i.fa-book { background-image: ~"url('@{bp3-icon-path}/book.png')"; &:extend(.bp-icon); }
+i.fa-bookbag-add { background-image: ~"url('@{bp3-icon-path}/bookbag_add.png')"; &:extend(.bp-icon); }
+i.fa-bookbag-delete { background-image: ~"url('@{bp3-icon-path}/bookbag_delete.png')"; &:extend(.bp-icon); }
+i.fa-bookbag-empty { background-image: ~"url('@{bp3-icon-path}/bookbag_empty.png')"; &:extend(.bp-icon); }
+i.fa-bookmark { background-image: ~"url('@{bp3-icon-path}/bookmark_add.png')"; &:extend(.bp-icon); }
+i.fa-braille { background-image: ~"url('@{bp3-icon-path}/page_red.png')"; &:extend(.bp-icon); }
+i.fa-cancel-all-holds { background-image: ~"url('@{bp3-icon-path}/holdCancelAll.png')"; &:extend(.bp-icon); }
+i.fa-cancel-all-storage-retrieval-requests { background-image: ~"url('@{bp3-icon-path}/holdCancelAll.png')"; &:extend(.bp-icon); }
+i.fa-cancel-holds { background-image: ~"url('@{bp3-icon-path}/holdCancel.png')"; &:extend(.bp-icon); }
+i.fa-cancel-storage-retrieval-requests { background-image: ~"url('@{bp3-icon-path}/holdCancel.png')"; &:extend(.bp-icon); }
+i.fa-cdrom { background-image: ~"url('@{bp3-icon-path}/cd.png')"; &:extend(.bp-icon); }
+i.fa-chart { background-image: ~"url('@{bp3-icon-path}/chart_bar.png')"; &:extend(.bp-icon); }
+i.fa-chipcartridge { background-image: ~"url('@{bp3-icon-path}/server.png')"; &:extend(.bp-icon); }
+i.fa-collage { background-image: ~"url('@{bp3-icon-path}/pictures.png')"; &:extend(.bp-icon); }
+i.fa-close { background-image: ~"url('@{bp3-icon-path}/cross.png')"; &:extend(.bp-icon); }
+i.fa-disccartridge { background-image: ~"url('@{bp3-icon-path}/cd.png')"; &:extend(.bp-icon); }
+i.fa-drawing { background-image: ~"url('@{bp3-icon-path}/photo.png')"; &:extend(.bp-icon); }
+i.fa-ebook { background-image: ~"url('@{bp3-icon-path}/book_addresses.png')"; &:extend(.bp-icon); }
+i.fa-edit { background-image: ~"url('@{bp3-icon-path}/edit.png')"; &:extend(.bp-icon); }
+i.fa-electronic { background-image: ~"url('@{bp3-icon-path}/mouse.png')"; &:extend(.bp-icon); }
 i.fa-email,
 i.fa-envelope,
-i.fa-envelope-o {background-image:url('../../images/icons/email.png'); &:extend(.bp-icon);}
-i.fa-exchange {background-image:url('../../images/icons/arrow_refresh.png'); &:extend(.bp-icon);}
-i.fa-external-link {background-image:url('../../images/icons/link_go.png'); &:extend(.bp-icon);}
-i.fa-filmstrip {background-image:url('../../images/icons/film.png'); &:extend(.bp-icon);}
-i.fa-flag {background-image:url('../../images/icons/flag_red.png'); &:extend(.bp-icon);}
-i.fa-flashcard {background-image:url('../../images/icons/table_lightening.png'); &:extend(.bp-icon);}
-i.fa-floppydisk {background-image:url('../../images/icons/disk.png'); &:extend(.bp-icon);}
-i.fa-globe {background-image:url('../../images/icons/world.png'); &:extend(.bp-icon);}
-i.fa-grid {background-image:url('../../images/icons/view_grid.png'); &:extend(.bp-icon);}
-i.fa-heart {background-image:url('../../images/icons/heart.png'); &:extend(.bp-icon);}
-i.fa-home {background-image:url('../../images/icons/house.png'); &:extend(.bp-icon);}
-i.fa-inbox {background-image:url('../../images/icons/box.png'); &:extend(.bp-icon);}
-i.fa-journal {background-image:url('../../images/icons/book.png'); &:extend(.bp-icon);}
-i.fa-kit {background-image:url('../../images/icons/briefcase.png'); &:extend(.bp-icon);}
-i.fa-leaf,.fa-sitemap {background-image:url('../../images/icons/treeCurrent.png'); &:extend(.bp-icon);}
-i.fa-list {background-image:url('../../images/icons/view_list.png'); &:extend(.bp-icon);}
-i.fa-list-alt,i.fa-export {background-image:url('../../images/icons/application_add.png'); &:extend(.bp-icon);}
-i.fa-lock {background-image:url('../../images/icons/lock.png'); &:extend(.bp-icon);}
-i.fa-manuscript {background-image:url('../../images/icons/script.png'); &:extend(.bp-icon);}
-i.fa-map {background-image:url('../../images/icons/map.png'); &:extend(.bp-icon);}
-i.fa-microfilm {background-image:url('../../images/icons/film.png'); &:extend(.bp-icon);}
-i.fa-minus-circle,i.fa-minus-sign {background-image:url('../../images/icons/delete.png'); &:extend(.bp-icon);}
-i.fa-mobile {background-image:url('../../images/icons/phone.png'); &:extend(.bp-icon);}
-i.fa-motionpicture {background-image:url('../../images/icons/television.png'); &:extend(.bp-icon);}
-i.fa-musicalscore {background-image:url('../../images/icons/music.png'); &:extend(.bp-icon);}
-i.fa-musicrecording {background-image:url('../../images/icons/music.png'); &:extend(.bp-icon);}
-i.fa-newspaper {background-image:url('../../images/icons/newspaper.png'); &:extend(.bp-icon);}
-i.fa-ok {background-image:url('../../images/icons/tick.png'); &:extend(.bp-icon);}
-i.fa-online {background-image:url('../../images/icons/computer.png'); &:extend(.bp-icon);}
-i.fa-painting {background-image:url('../../images/icons/paintbrush.png'); &:extend(.bp-icon);}
-i.fa-photo {background-image:url('../../images/icons/photo.png'); &:extend(.bp-icon);}
-i.fa-photonegative {background-image:url('../../images/icons/film.png'); &:extend(.bp-icon);}
-i.fa-physicalobject {background-image:url('../../images/icons/box.png'); &:extend(.bp-icon);}
-i.fa-plus {background-image:url('../../images/icons/add.png'); &:extend(.bp-icon);}
-i.fa-plus-circle {background-image:url('../../images/icons/add.png'); &:extend(.bp-icon);}
-i.fa-print {background-image:url('../../images/icons/printer.png'); &:extend(.bp-icon);}
-i.fa-qrcode {background-image:url('../../images/icons/qrcode.png'); &:extend(.bp-icon);}
-i.fa-remove {background-image:url('../../images/icons/delete.png'); &:extend(.bp-icon);}
-i.fa-renew {background-image:url('../../images/icons/renew.png'); &:extend(.bp-icon);}
-i.fa-renew-all {background-image:url('../../images/icons/renewAll.png'); &:extend(.bp-icon);}
-i.fa-report {background-image:url('../../images/icons/report.png'); &:extend(.bp-icon);}
-i.fa-rss {background-image:url('../../images/icons/feed.png'); &:extend(.bp-icon);}
-i.fa-save {background-image:url('../../images/icons/disk.png'); &:extend(.bp-icon);}
-i.fa-search {background-image:url('../../images/icons/magnifier.png'); &:extend(.bp-icon);}
-i.fa-sensorimage {background-image:url('../../images/icons/photo.png'); &:extend(.bp-icon);}
-i.fa-serial {background-image:url('../../images/icons/page_white_stack.png'); &:extend(.bp-icon);}
-i.fa-shopping-cart {background-image:url('../../images/icons/cart.png'); &:extend(.bp-icon);}
-i.fa-sign-in {background-image:url('../../images/icons/door_in.png'); &:extend(.bp-icon);}
-i.fa-sign-out {background-image:url('../../images/icons/door_out.png'); &:extend(.bp-icon);}
-i.fa-slide {background-image:url('../../images/icons/film.png'); &:extend(.bp-icon);}
-i.fa-software {background-image:url('../../images/icons/drive_cd.png'); &:extend(.bp-icon);}
-i.fa-soundcassette {background-image:url('../../images/icons/sound.png'); &:extend(.bp-icon);}
-i.fa-sounddisc {background-image:url('../../images/icons/cd.png'); &:extend(.bp-icon);}
-i.fa-soundrecording {background-image:url('../../images/icons/sound.png'); &:extend(.bp-icon);}
-i.fa-spinner {background-image:url('../../images/icons/ajax_loading.gif'); &:extend(.bp-icon);}
-i.fa-star {background-image:url('../../images/icons/star.png'); &:extend(.bp-icon);}
-i.fa-status-unknown {background-image:url('../../images/icons/bullet_orange.png'); &:extend(.bp-icon);}
-i.fa-suitcase {background-image:url('../../images/icons/bookbag.png'); &:extend(.bp-icon);}
-i.fa-tapecartridge {background-image:url('../../images/icons/drive.png'); &:extend(.bp-icon);}
-i.fa-tapecassette {background-image:url('../../images/icons/drive.png'); &:extend(.bp-icon);}
-i.fa-tapereel {background-image:url('../../images/icons/film.png'); &:extend(.bp-icon);}
-i.fa-transparency {background-image:url('../../images/icons/film.png'); &:extend(.bp-icon);}
+i.fa-envelope-o { background-image: ~"url('@{bp3-icon-path}/email.png')"; &:extend(.bp-icon); }
+i.fa-exchange { background-image: ~"url('@{bp3-icon-path}/arrow_refresh.png')"; &:extend(.bp-icon); }
+i.fa-external-link { background-image: ~"url('@{bp3-icon-path}/link_go.png')"; &:extend(.bp-icon); }
+i.fa-filmstrip { background-image: ~"url('@{bp3-icon-path}/film.png')"; &:extend(.bp-icon); }
+i.fa-flag { background-image: ~"url('@{bp3-icon-path}/flag_red.png')"; &:extend(.bp-icon); }
+i.fa-flashcard { background-image: ~"url('@{bp3-icon-path}/table_lightening.png')"; &:extend(.bp-icon); }
+i.fa-floppydisk { background-image: ~"url('@{bp3-icon-path}/disk.png')"; &:extend(.bp-icon); }
+i.fa-globe { background-image: ~"url('@{bp3-icon-path}/world.png')"; &:extend(.bp-icon); }
+i.fa-grid { background-image: ~"url('@{bp3-icon-path}/view_grid.png')"; &:extend(.bp-icon); }
+i.fa-heart { background-image: ~"url('@{bp3-icon-path}/heart.png')"; &:extend(.bp-icon); }
+i.fa-home { background-image: ~"url('@{bp3-icon-path}/house.png')"; &:extend(.bp-icon); }
+i.fa-inbox { background-image: ~"url('@{bp3-icon-path}/box.png')"; &:extend(.bp-icon); }
+i.fa-journal { background-image: ~"url('@{bp3-icon-path}/book.png')"; &:extend(.bp-icon); }
+i.fa-kit { background-image: ~"url('@{bp3-icon-path}/briefcase.png')"; &:extend(.bp-icon); }
+i.fa-leaf,.fa-sitemap { background-image: ~"url('@{bp3-icon-path}/treeCurrent.png')"; &:extend(.bp-icon); }
+i.fa-list { background-image: ~"url('@{bp3-icon-path}/view_list.png')"; &:extend(.bp-icon); }
+i.fa-list-alt,i.fa-export { background-image: ~"url('@{bp3-icon-path}/application_add.png')"; &:extend(.bp-icon); }
+i.fa-lock { background-image: ~"url('@{bp3-icon-path}/lock.png')"; &:extend(.bp-icon); }
+i.fa-manuscript { background-image: ~"url('@{bp3-icon-path}/script.png')"; &:extend(.bp-icon); }
+i.fa-map { background-image: ~"url('@{bp3-icon-path}/map.png')"; &:extend(.bp-icon); }
+i.fa-microfilm { background-image: ~"url('@{bp3-icon-path}/film.png')"; &:extend(.bp-icon); }
+i.fa-minus-circle,i.fa-minus-sign { background-image: ~"url('@{bp3-icon-path}/delete.png')"; &:extend(.bp-icon); }
+i.fa-mobile { background-image: ~"url('@{bp3-icon-path}/phone.png')"; &:extend(.bp-icon); }
+i.fa-motionpicture { background-image: ~"url('@{bp3-icon-path}/television.png')"; &:extend(.bp-icon); }
+i.fa-musicalscore { background-image: ~"url('@{bp3-icon-path}/music.png')"; &:extend(.bp-icon); }
+i.fa-musicrecording { background-image: ~"url('@{bp3-icon-path}/music.png')"; &:extend(.bp-icon); }
+i.fa-newspaper { background-image: ~"url('@{bp3-icon-path}/newspaper.png')"; &:extend(.bp-icon); }
+i.fa-ok { background-image: ~"url('@{bp3-icon-path}/tick.png')"; &:extend(.bp-icon); }
+i.fa-online { background-image: ~"url('@{bp3-icon-path}/computer.png')"; &:extend(.bp-icon); }
+i.fa-painting { background-image: ~"url('@{bp3-icon-path}/paintbrush.png')"; &:extend(.bp-icon); }
+i.fa-photo { background-image: ~"url('@{bp3-icon-path}/photo.png')"; &:extend(.bp-icon); }
+i.fa-photonegative { background-image: ~"url('@{bp3-icon-path}/film.png')"; &:extend(.bp-icon); }
+i.fa-physicalobject { background-image: ~"url('@{bp3-icon-path}/box.png')"; &:extend(.bp-icon); }
+i.fa-plus { background-image: ~"url('@{bp3-icon-path}/add.png')"; &:extend(.bp-icon); }
+i.fa-plus-circle { background-image: ~"url('@{bp3-icon-path}/add.png')"; &:extend(.bp-icon); }
+i.fa-print { background-image: ~"url('@{bp3-icon-path}/printer.png')"; &:extend(.bp-icon); }
+i.fa-qrcode { background-image: ~"url('@{bp3-icon-path}/qrcode.png')"; &:extend(.bp-icon); }
+i.fa-remove { background-image: ~"url('@{bp3-icon-path}/delete.png')"; &:extend(.bp-icon); }
+i.fa-renew { background-image: ~"url('@{bp3-icon-path}/renew.png')"; &:extend(.bp-icon); }
+i.fa-renew-all { background-image: ~"url('@{bp3-icon-path}/renewAll.png')"; &:extend(.bp-icon); }
+i.fa-report { background-image: ~"url('@{bp3-icon-path}/report.png')"; &:extend(.bp-icon); }
+i.fa-rss { background-image: ~"url('@{bp3-icon-path}/feed.png')"; &:extend(.bp-icon); }
+i.fa-save { background-image: ~"url('@{bp3-icon-path}/disk.png')"; &:extend(.bp-icon); }
+i.fa-search { background-image: ~"url('@{bp3-icon-path}/magnifier.png')"; &:extend(.bp-icon); }
+i.fa-sensorimage { background-image: ~"url('@{bp3-icon-path}/photo.png')"; &:extend(.bp-icon); }
+i.fa-serial { background-image: ~"url('@{bp3-icon-path}/page_white_stack.png')"; &:extend(.bp-icon); }
+i.fa-shopping-cart { background-image: ~"url('@{bp3-icon-path}/cart.png')"; &:extend(.bp-icon); }
+i.fa-sign-in { background-image: ~"url('@{bp3-icon-path}/door_in.png')"; &:extend(.bp-icon); }
+i.fa-sign-out { background-image: ~"url('@{bp3-icon-path}/door_out.png')"; &:extend(.bp-icon); }
+i.fa-slide { background-image: ~"url('@{bp3-icon-path}/film.png')"; &:extend(.bp-icon); }
+i.fa-software { background-image: ~"url('@{bp3-icon-path}/drive_cd.png')"; &:extend(.bp-icon); }
+i.fa-soundcassette { background-image: ~"url('@{bp3-icon-path}/sound.png')"; &:extend(.bp-icon); }
+i.fa-sounddisc { background-image: ~"url('@{bp3-icon-path}/cd.png')"; &:extend(.bp-icon); }
+i.fa-soundrecording { background-image: ~"url('@{bp3-icon-path}/sound.png')"; &:extend(.bp-icon); }
+i.fa-spinner { background-image: ~"url('@{bp3-icon-path}/ajax_loading.gif')"; &:extend(.bp-icon); }
+i.fa-star { background-image: ~"url('@{bp3-icon-path}/star.png')"; &:extend(.bp-icon); }
+i.fa-status-unknown { background-image: ~"url('@{bp3-icon-path}/bullet_orange.png')"; &:extend(.bp-icon); }
+i.fa-suitcase { background-image: ~"url('@{bp3-icon-path}/bookbag.png')"; &:extend(.bp-icon); }
+i.fa-tapecartridge { background-image: ~"url('@{bp3-icon-path}/drive.png')"; &:extend(.bp-icon); }
+i.fa-tapecassette { background-image: ~"url('@{bp3-icon-path}/drive.png')"; &:extend(.bp-icon); }
+i.fa-tapereel { background-image: ~"url('@{bp3-icon-path}/film.png')"; &:extend(.bp-icon); }
+i.fa-transparency { background-image: ~"url('@{bp3-icon-path}/film.png')"; &:extend(.bp-icon); }
 i.fa-trash,
-i.fa-trash-o {background-image:url('../../images/icons/bin.png'); &:extend(.bp-icon);}
-i.fa-tree {background-image:url('../../images/icons/treeCurrent.png'); &:extend(.bp-icon);}
-i.fa-tree-muted {background-image:url('../../images/icons/treeMuted.png'); &:extend(.bp-icon);}
-i.fa-unknown {background-image:url('../../images/icons/page_white.png'); &:extend(.bp-icon);}
-i.fa-usd {background-image:url('../../images/icons/money_dollar.png'); &:extend(.bp-icon);}
-i.fa-user {background-image:url('../../images/icons/user.png'); &:extend(.bp-icon);}
-i.fa-video{background-image:url('../../images/icons/television.png'); &:extend(.bp-icon);}
-i.fa-videocartridge {background-image:url('../../images/icons/television.png'); &:extend(.bp-icon);}
-i.fa-videocassette{background-image:url('../../images/icons/television.png'); &:extend(.bp-icon);}
-i.fa-videodisc{background-image:url('../../images/icons/cd.png'); &:extend(.bp-icon);}
-i.fa-videoreel{background-image:url('../../images/icons/film.png'); &:extend(.bp-icon);}
-i.fa-visual {background-image:url('../../images/icons/view_visual.png'); &:extend(.bp-icon);}
+i.fa-trash-o { background-image: ~"url('@{bp3-icon-path}/bin.png')"; &:extend(.bp-icon); }
+i.fa-tree { background-image: ~"url('@{bp3-icon-path}/treeCurrent.png')"; &:extend(.bp-icon); }
+i.fa-tree-muted { background-image: ~"url('@{bp3-icon-path}/treeMuted.png')"; &:extend(.bp-icon); }
+i.fa-unknown { background-image: ~"url('@{bp3-icon-path}/page_white.png')"; &:extend(.bp-icon); }
+i.fa-usd { background-image: ~"url('@{bp3-icon-path}/money_dollar.png')"; &:extend(.bp-icon); }
+i.fa-user { background-image: ~"url('@{bp3-icon-path}/user.png')"; &:extend(.bp-icon); }
+i.fa-video{background-image: ~"url('@{bp3-icon-path}/television.png')"; &:extend(.bp-icon); }
+i.fa-videocartridge { background-image: ~"url('@{bp3-icon-path}/television.png')"; &:extend(.bp-icon); }
+i.fa-videocassette{background-image: ~"url('@{bp3-icon-path}/television.png')"; &:extend(.bp-icon); }
+i.fa-videodisc{background-image: ~"url('@{bp3-icon-path}/cd.png')"; &:extend(.bp-icon); }
+i.fa-videoreel{background-image: ~"url('@{bp3-icon-path}/film.png')"; &:extend(.bp-icon); }
+i.fa-visual { background-image: ~"url('@{bp3-icon-path}/view_visual.png')"; &:extend(.bp-icon); }
 
 body.rtl {
-  i.fa-external-link {background-image:url('../../images/icons/link_go_rtl.png');}
-  i.fa-flag {background-image:url('../../images/icons/flag_red_rtl.png');}
+  i.fa-external-link { background-image: ~"url('@{bp3-icon-path}/link_go_rtl.png')"; }
+  i.fa-flag { background-image: ~"url('@{bp3-icon-path}/flag_red_rtl.png')"; }
 }
 
-#cart-empty-label i.fa-close {background-image:url('../../images/icons/briefcase.png'); &:extend(.bp-icon);}
\ No newline at end of file
+#cart-empty-label i.fa-close { background-image: ~"url('@{bp3-icon-path}/briefcase.png')"; &:extend(.bp-icon); }
diff --git a/themes/bootprint3/less/search.less b/themes/bootprint3/less/search.less
new file mode 100644
index 0000000000000000000000000000000000000000..90ee8ce35b44caf2364a22449beaddf605f4b753
--- /dev/null
+++ b/themes/bootprint3/less/search.less
@@ -0,0 +1,51 @@
+.searchHomeContent {
+  float: none;
+  margin: 1em auto;
+  width: 90%;
+}
+#advSearchForm .search { margin: 0; }
+.group .match { margin-top: .5em; }
+
+/* --- Search form --- */
+.searchForm_lookfor,
+.searchForm_type { border-color: @brand-primary; }
+[name=searchForm] {
+  margin: 6px 8px 8px;
+  padding: 0;
+
+  .clear-btn,
+  .btn-primary,
+  .form-control {
+    font-size: 14px;
+    height: 32px;
+    padding: 5px 8px;
+  }
+  .clear-btn,
+  .btn-primary[multiple],
+  .form-control[multiple] { height: auto; }
+  @media (min-width: 768px) {
+    .search-query { width: 400px; }
+  }
+  .nav-tabs {
+    border-bottom: 0;
+    padding: 0 6px;
+  }
+  .nav-tabs li a {
+    margin-bottom: -1px;
+    border-bottom: 0;
+    padding-bottom: 6px;
+    &:hover {
+      background: 0 0;
+      border-color: transparent;
+      text-decoration: underline;
+    }
+  }
+  .nav-tabs li.active a,
+  .nav-tabs li.active a:hover {
+    background: #FFF;
+    border-color: @brand-primary;
+    border-bottom: 0;
+    text-decoration: none;
+    z-index: 5;
+  }
+}
diff --git a/themes/bootprint3/less/variables.less b/themes/bootprint3/less/variables.less
index 30a97528d2770b3ab987a6bef9d1c9f33a81e71b..f4fdc3e51c027e32f1def9840bca8b14a4d3e10e 100644
--- a/themes/bootprint3/less/variables.less
+++ b/themes/bootprint3/less/variables.less
@@ -16,7 +16,7 @@
 
 @input-border-focus: @brand-primary;
 
-@legend-border-color: @gray-light;
+@legend-border-color: #777; // @gray-light
 
 @grid-gutter-width: 14px;
 
@@ -27,13 +27,13 @@
 @navbar-margin-bottom: 0px;
 @nav-link-padding    : 5px;
 
-@pagination-active-bg    : @brand-info;
-@pagination-active-border: @brand-info;
+@pagination-active-bg    : #5bc0de; // @brand-info
+@pagination-active-border: #5bc0de; // @brand-info
 
 @panel-body-padding: 5px;
 
 @breadcrumb-padding-vertical  : 6px;
 @breadcrumb-padding-horizontal: 20px;
 @breadcrumb-bg                : #FFF;
-@breadcrumb-color             : @gray-light;
-@breadcrumb-active-color      : @gray-dark;
\ No newline at end of file
+@breadcrumb-color             : #777; // @gray-light
+@breadcrumb-active-color      : #333; // @gray-dark
\ No newline at end of file
diff --git a/themes/bootprint3/scss/bootprint.scss b/themes/bootprint3/scss/bootprint.scss
new file mode 100644
index 0000000000000000000000000000000000000000..36d2efa7102a0c58b996244dc59090868d2207ee
--- /dev/null
+++ b/themes/bootprint3/scss/bootprint.scss
@@ -0,0 +1,290 @@
+@import "variables", "bootstrap";
+@import "icons";
+@import "search";
+
+$brand-primary: #619144; // a11y overrides $brand-primary in sass
+$active-orange: #E70 !default;
+
+/* --- Bootstrap MODS ---*/
+body {
+  background: $brand-primary;
+  font-size: 13px
+}
+.container {
+  background: #FFF;
+  padding: 0;
+}
+.main .container { padding: 0 4px 18px; }
+
+a,
+.btn-link {
+  color: #06C;
+  &:hover { color: #09F; }
+}
+.alert { padding: 8px; }
+.btn { padding: 3px 5px 2px; }
+.btn.btn-default {
+  background: $gray-lighter;
+  background-image: linear-gradient(#FFF, $nav-tabs-border-color);
+  border: 1px solid $gray;
+  color: $gray-darker;
+  text-shadow: 0 1px 0 #FFF;
+}
+.btn:not(.btn-default) { font-weight:bold; }
+.btn-danger,
+.btn-danger:hover  { border-color:darken($brand-danger,  12%); }
+.btn-info,
+.btn-info:hover    { border-color:darken($brand-info,    12%); }
+.btn-primary,
+.btn-primary:hover { border-color:darken($brand-primary, 12%); }
+.btn-success,
+.btn-success:hover { border-color:darken($brand-success, 12%); }
+.btn-warning,
+.btn-warning:hover { border-color:darken($brand-warning, 12%); }
+
+#commentList .comment:nth-child(even) { background: $gray-lighter; }
+#dateVisColorSettings { stroke: $brand-primary; }
+#hierarchyRecord { background: #FFF; }
+h2 { margin: 0 8px 8px; }
+input[type=radio],
+input[type=checkbox] {
+  margin: 2px auto 0;
+  padding: 0 2px;
+}
+.nav > li > a { padding: 5px 10px; }
+.nav-pills {
+  display: table;
+  margin: 0 auto;
+}
+.navbar { min-height: 1px; }
+.navbar-form {
+  margin-top: 5px;
+  margin-bottom: 5px;
+}
+.pagination {
+  display: table;
+  margin: 18px auto;
+}
+.pagination > li > a,
+.pagination > li > span { padding: 4px 12px 3px; }
+.pagination > .active > a,
+.pagination > .active > a:focus,
+.pagination > .active > a:hover,
+.pagination > .active > span,
+.pagination > .active > span:focus,
+.pagination > .active > span:hover {
+  background: $brand-primary;
+  border-color: $brand-primary;
+}
+.panel-heading { padding: 0; }
+.panel-heading a {
+  cursor: pointer;
+  display: inline-block;
+  padding: 6px;
+  width: 100%;
+}
+.row:not(.top-row) {
+  padding: 6px 4px;
+  margin: 0 -4px;
+}
+.row > p { padding: 0 1em; }
+.sub-breadcrumb { padding: 0 5px; }
+.tab-content {
+  padding: 6px 8px;
+  border: 1px solid $gray-lighter;
+  border-top: 0;
+}
+
+@media (max-width: 767px) {
+  body { padding: 6px; }
+  header { margin-top: 0; }
+  .label { font-size: 85%; }
+}
+@media (min-width: 768px) {
+  .badge {
+    font-size: 85%;
+    margin-top: 1px;
+  }
+  .label { padding-top: .3em; }
+  .modal-dialog { width: 650px; }
+}
+
+/* --- Header --- */
+header { margin-top: 18px; }
+header .fa.fa-bars { font-size: 21px; }
+header .navbar {
+  border-radius: 5px 5px 0 0;
+  padding: 0 10px;
+  .searchForm { display: none !important; }
+  .navbar-brand {
+    height: 65px;
+    width: 170px;
+    margin-top: 5px;
+    color: transparent;
+    background-image: unquote("url('../../bootprint3/images/vufind_logo.png')");
+    background-position: center center;
+    background-repeat: no-repeat;
+    background-size: contain;
+    &:active,
+    &:focus,
+    &:hover { color: transparent; }
+    &.lang-ar { background-image: unquote("url('../../bootprint3/images/vufind_logo_ar.png')"); }
+  }
+  .navbar-nav > li > a { padding: 12px 6px; }
+  .navbar-right { margin-top: 12px; }
+  @media (max-width: 767px) {
+    .navbar-nav > li > a { padding: 8px 24px; }
+    .navbar-right { margin: 0; }
+  }
+}
+header .searchbox {
+  background: linear-gradient(to bottom, #FFF, #EEE);
+  display: block !important;
+}
+header .searchbox .tab-content { border: 0; }
+header .searchbox .tab-content .navbar-text { margin: 5px 10px 5px 0; }
+@media (max-width: 767px) {
+  header #header-collapse .navbar-right li { text-align: right; }
+  header .searchForm_type {
+    margin-top: 2px;
+    margin-bottom: 2px
+  }
+}
+header .breadcrumb {
+  border: 1px solid $gray-lighter;
+  border-radius: 0;
+  border-width: 1px 0;
+  font-size: 12px;
+  margin-bottom: 2px;
+  padding: 7px 20px 5px
+}
+
+/* --- Footer --- */
+footer { margin-bottom: 36px; }
+footer .container {
+  border-radius: 0 0 5px 5px;
+  border-top: 1px solid $nav-tabs-border-color;
+  padding-top: 18px;
+}
+footer hr { display: none; }
+footer p { margin: 0; }
+footer ul { padding-left: 30px; }
+
+/* --- Browse --- */
+[id^=list].list-group .col-sm-9 { margin: 0; }
+
+/* --- Offcanvas --- */
+body.offcanvas {
+  .offcanvas-toggle {
+    padding-bottom: 18px;
+    font-size: 16px;
+    background: #fff;
+    box-shadow: 0 0 2px #000;
+    color: $brand-primary;
+  }
+  .sidebar .list-group { color: #000; }
+  &.active .sidebar { color: #FFF; }
+  &.active .offcanvas-toggle { box-shadow: none; }
+}
+
+/* --- Random Items (results view) --- */
+ul.random {
+  list-style: none;
+  padding: 0;
+  margin: 0;
+  text-align: justify;
+}
+ul.random li { padding-bottom: 10px; }
+ul.random li img { margin: 0 auto 1em; }
+ul.random.image,
+ul.random.mixed { text-align: center; }
+ul.random.image li img { margin: 0 auto; }
+
+/* --- ReCaptcha --- */
+#custom_recaptcha_widget { display: table; }
+#custom_recaptcha_widget embed { display: none; }
+#custom_recaptcha_widget #recaptcha_image {
+  border: 1px solid #000;
+  padding: 6px;
+  margin: 1em 0;
+}
+#custom_recaptcha_widget #recaptcha_response_field { margin: 0 .5em; }
+#custom_recaptcha_widget > div > a {
+  display: inline-block;
+  float: left;
+  margin: 5px 10px 5px 0;
+}
+
+/* --- Record --- */
+.tagList button {
+  margin-top: 0;
+  padding-top: 0;
+  padding-bottom: 4px;
+  font-size: 95%;
+  vertical-align: initial;
+}
+.tagList button .fa-close { margin-top: 3px; }
+
+/* --- Search --- */
+.bulkActionButtons { margin-bottom: 6px; }
+.result {
+  padding: 1rem;
+  margin-left: -1.1rem;
+  &:nth-child(even) { background-color: $gray-lighter; }
+  &.embedded .getFull.expanded {
+    margin-top: -6px;
+    padding-top: .5rem;
+    padding-bottom: .5rem;
+  }
+}
+.result > p {padding:0 1em;}
+.result .label {
+  display: inline-block;
+  margin-bottom: 4px;
+}
+.result .long-view .tab-content {background: #FFF;}
+.result .media { margin: 0; }
+.result .row { padding: 0; }
+.result .savedLists {
+  margin: 0 0 4px;
+  padding: 4px 0 4px 6px;
+}
+.result .savedLists ul { padding-left: 18px; }
+.search-controls label { text-align: left; }
+@media (max-width: 767px) {
+  .result .search-controls .form-inline { text-align: left; }
+  .search-controls {
+    margin: 4px -4px;
+    padding: 4px 0;
+  }
+}
+
+/* --- Sidebar --- */
+.sidebar {
+  .list-group { margin-bottom: 5px; }
+  .list-group label.list-group-item { padding-left: 26px; }
+  .list-group label.list-group-item input[type=checkbox] { margin-top: 2px; }
+
+  .list-group-item { padding: 7px 10px 6px; }
+  .list-group-item.active { color: #fff; }
+  .list-group-item.active .badge { color: $active-orange; }
+  .list-group-item.active,
+  .list-group-item.active:hover {
+    background: $active-orange;
+    border-color: $active-orange;
+  }
+  .list-group-item .badge a { color: #fff; }
+}
+.slider-container .slider-handle {
+  background: $brand-primary;
+  border: 1px solid $brand-primary;
+  &:hover,&:active,&:focus {
+    background: #FFF;
+    border-color: $gray-light;
+  }
+  &:active,&:focus { border-color: $brand-primary; }
+}
+.top-row .badge a {
+  color: #fff;
+  &:hover { color: $brand-danger; }
+}
diff --git a/themes/bootprint3/scss/compiled.scss b/themes/bootprint3/scss/compiled.scss
new file mode 100644
index 0000000000000000000000000000000000000000..9529db72a4dff183871de9d099eaf8d118cbe7fc
--- /dev/null
+++ b/themes/bootprint3/scss/compiled.scss
@@ -0,0 +1,14 @@
+@import "bootprint";
+
+/**
+ * This file prevents multiple instances of the Bootstrap library from
+ * being included by utilizing the theme inheritance feature of Vufind.
+ *
+ *    bootstrap.less <--- compiled.less
+ *          ^                    ^
+ *          |                    |
+ *          |                    X
+ *          |                    |
+ *    bootprint.less <--- compiled.less
+ *
+ */
\ No newline at end of file
diff --git a/themes/bootprint3/scss/compiled.scss.map b/themes/bootprint3/scss/compiled.scss.map
new file mode 100644
index 0000000000000000000000000000000000000000..ea19bd4d26db0bebe75f8c493e9ad4c1ea365c53
--- /dev/null
+++ b/themes/bootprint3/scss/compiled.scss.map
@@ -0,0 +1,7 @@
+{
+"version": 3,
+"mappings": "AAAA;;;;GAIG;AAAA,4EAA4E;AAAA,IAAI;EAAC,WAAW,EAAC,UAAU;EAAC,oBAAoB,EAAC,IAAI;EAAC,wBAAwB,EAAC,IAAI;;AAAC,IAAI;EAAC,MAAM,EAAC,CAAC;;AAAC,sGAA0F;EAAC,OAAO,EAAC,KAAK;;AAAC,8BAA2B;EAAC,OAAO,EAAC,YAAY;EAAC,cAAc,EAAC,QAAQ;;AAAC,qBAAqB;EAAC,OAAO,EAAC,IAAI;EAAC,MAAM,EAAC,CAAC;;AAAC,kBAAiB;EAAC,OAAO,EAAC,IAAI;;AAAC,CAAC;EAAC,gBAAgB,EAAC,WAAW;;AAAC,iBAAgB;EAAC,OAAO,EAAC,CAAC;;AAAC,WAAW;EAAC,aAAa,EAAC,UAAU;;AAAC,SAAQ;EAAC,WAAW,EAAC,IAAI;;AAAC,GAAG;EAAC,UAAU,EAAC,MAAM;;AAAC,EAAE;EAAC,SAAS,EAAC,GAAG;EAAC,MAAM,EAAC,OAAO;;AAAC,IAAI;EAAC,UAAU,EAAC,IAAI;EAAC,KAAK,EAAC,IAAI;;AAAC,KAAK;EAAC,SAAS,EAAC,GAAG;;AAAC,QAAO;EAAC,SAAS,EAAC,GAAG;EAAC,WAAW,EAAC,CAAC;EAAC,QAAQ,EAAC,QAAQ;EAAC,cAAc,EAAC,QAAQ;;AAAC,GAAG;EAAC,GAAG,EAAC,MAAM;;AAAC,GAAG;EAAC,MAAM,EAAC,OAAO;;AAAC,GAAG;EAAC,MAAM,EAAC,CAAC;;AAAC,cAAc;EAAC,QAAQ,EAAC,MAAM;;AAAC,MAAM;EAAC,MAAM,EAAC,QAAQ;;AAAC,EAAE;EAAC,UAAU,EAAC,WAAW;EAAC,MAAM,EAAC,CAAC;;AAAC,GAAG;EAAC,QAAQ,EAAC,IAAI;;AAAC,oBAAiB;EAAC,WAAW,EAAC,mBAAmB;EAAC,SAAS,EAAC,GAAG;;AAAC,yCAAqC;EAAC,KAAK,EAAC,OAAO;EAAC,IAAI,EAAC,OAAO;EAAC,MAAM,EAAC,CAAC;;AAAC,MAAM;EAAC,QAAQ,EAAC,OAAO;;AAAC,cAAa;EAAC,cAAc,EAAC,IAAI;;AAAC,4EAAyE;EAAC,kBAAkB,EAAC,MAAM;EAAC,MAAM,EAAC,OAAO;;AAAC,sCAAqC;EAAC,MAAM,EAAC,OAAO;;AAAC,iDAAgD;EAAC,MAAM,EAAC,CAAC;EAAC,OAAO,EAAC,CAAC;;AAAC,KAAK;EAAC,WAAW,EAAC,MAAM;;AAAC,2CAA0C;EAAC,UAAU,EAAC,UAAU;EAAC,OAAO,EAAC,CAAC;;AAAC,gGAA+F;EAAC,MAAM,EAAC,IAAI;;AAAC,oBAAoB;EAAC,kBAAkB,EAAC,SAAS;EAAC,UAAU,EAAC,WAAW;;AAAC,mGAAkG;EAAC,kBAAkB,EAAC,IAAI;;AAAC,QAAQ;EAAC,MAAM,EAAC,iBAAiB;EAAC,MAAM,EAAC,KAAK;EAAC,OAAO,EAAC,kBAAkB;;AAAC,MAAM;EAAC,MAAM,EAAC,CAAC;EAAC,OAAO,EAAC,CAAC;;AAAC,QAAQ;EAAC,QAAQ,EAAC,IAAI;;AAAC,QAAQ;EAAC,WAAW,EAAC,IAAI;;AAAC,KAAK;EAAC,eAAe,EAAC,QAAQ;EAAC,cAAc,EAAC,CAAC;;AAAC,MAAK;EAAC,OAAO,EAAC,CAAC;;AAAC,qFAAqF;AAAA,YAAY;EAAC,oBAAkB;IAAC,UAAU,EAAC,sBAAsB;IAAC,KAAK,EAAC,eAAe;IAAC,UAAU,EAAC,eAAe;IAAC,WAAW,EAAC,eAAe;;EAAC,YAAW;IAAC,eAAe,EAAC,SAAS;;EAAC,aAAa;IAAC,OAAO,EAAC,mBAAmB;;EAAC,iBAAiB;IAAC,OAAO,EAAC,oBAAoB;;EAAC,gDAA+C;IAAC,OAAO,EAAC,EAAE;;EAAC,eAAc;IAAC,MAAM,EAAC,cAAc;IAAC,iBAAiB,EAAC,KAAK;;EAAC,KAAK;IAAC,OAAO,EAAC,kBAAkB;;EAAC,OAAM;IAAC,iBAAiB,EAAC,KAAK;;EAAC,GAAG;IAAC,SAAS,EAAC,eAAe;;EAAC,SAAO;IAAC,OAAO,EAAC,CAAC;IAAC,MAAM,EAAC,CAAC;;EAAC,MAAK;IAAC,gBAAgB,EAAC,KAAK;;EAAC,OAAO;IAAC,OAAO,EAAC,IAAI;;EAAC,sCAA+B;IAAC,gBAAgB,EAAC,eAAe;;EAAC,yCAAuC;IAAC,MAAM,EAAC,cAAc;;EAAC,MAAM;IAAC,eAAe,EAAC,mBAAmB;;EAAC,oBAAmB;IAAC,gBAAgB,EAAC,eAAe;;EAAC,sCAAqC;IAAC,MAAM,EAAC,yBAAyB;AAAE,CAAC;EAAC,kBAAkB,EAAC,UAAU;EAAC,eAAe,EAAC,UAAU;EAAC,UAAU,EAAC,UAAU;;AAAC,iBAAgB;EAAC,kBAAkB,EAAC,UAAU;EAAC,eAAe,EAAC,UAAU;EAAC,UAAU,EAAC,UAAU;;AAAC,IAAI;EAAC,SAAS,EAAC,IAAI;EAAC,2BAA2B,EAAC,WAAa;;AAAC,IAAI;EAAC,WAAW,EAAC,2CAA2C;EAAC,SAAS,EAAC,IAAI;EAAC,WAAW,EAAC,UAAU;EAAC,KAAK,EAAC,IAAI;EAAC,gBAAgB,EAAC,IAAI;;AAAC,+BAA4B;EAAC,WAAW,EAAC,OAAO;EAAC,SAAS,EAAC,OAAO;EAAC,WAAW,EAAC,OAAO;;AAAC,CAAC;EAAC,KAAK,EAAC,OAAO;EAAC,eAAe,EAAC,IAAI;;AAAC,gBAAe;EAAC,KAAK,EAAC,OAAO;EAAC,eAAe,EAAC,SAAS;;AAAC,OAAO;EAAC,OAAO,EAAC,WAAW;EAAC,OAAO,EAAC,iCAAiC;EAAC,cAAc,EAAC,IAAI;;AAAC,MAAM;EAAC,MAAM,EAAC,CAAC;;AAAC,GAAG;EAAC,cAAc,EAAC,MAAM;;AAAC,uHAAqG;EAAC,OAAO,EAAC,KAAK;EAAC,SAAS,EAAC,IAAI;EAAC,MAAM,EAAC,IAAI;;AAAC,YAAY;EAAC,aAAa,EAAC,GAAG;;AAAC,cAAc;EAAC,OAAO,EAAC,GAAG;EAAC,WAAW,EAAC,UAAU;EAAC,gBAAgB,EAAC,IAAI;EAAC,MAAM,EAAC,cAAc;EAAC,aAAa,EAAC,GAAG;EAAC,kBAAkB,EAAC,mBAAmB;EAAC,aAAa,EAAC,mBAAmB;EAAC,UAAU,EAAC,mBAAmB;EAAC,OAAO,EAAC,YAAY;EAAC,SAAS,EAAC,IAAI;EAAC,MAAM,EAAC,IAAI;;AAAC,WAAW;EAAC,aAAa,EAAC,GAAG;;AAAC,EAAE;EAAC,UAAU,EAAC,IAAI;EAAC,aAAa,EAAC,IAAI;EAAC,MAAM,EAAC,CAAC;EAAC,UAAU,EAAC,cAAc;;AAAC,QAAQ;EAAC,QAAQ,EAAC,QAAQ;EAAC,KAAK,EAAC,GAAG;EAAC,MAAM,EAAC,GAAG;EAAC,MAAM,EAAC,IAAI;EAAC,OAAO,EAAC,CAAC;EAAC,QAAQ,EAAC,MAAM;EAAC,IAAI,EAAC,gBAAgB;EAAC,MAAM,EAAC,CAAC;;AAAC,mDAAkD;EAAC,QAAQ,EAAC,MAAM;EAAC,KAAK,EAAC,IAAI;EAAC,MAAM,EAAC,IAAI;EAAC,MAAM,EAAC,CAAC;EAAC,QAAQ,EAAC,OAAO;EAAC,IAAI,EAAC,IAAI;;AAAC,eAAe;EAAC,MAAM,EAAC,OAAO;;AAAC,oDAAyC;EAAC,WAAW,EAAC,OAAO;EAAC,WAAW,EAAC,GAAG;EAAC,WAAW,EAAC,GAAG;EAAC,KAAK,EAAC,OAAO;;AAAC,sQAA+O;EAAC,WAAW,EAAC,MAAM;EAAC,WAAW,EAAC,CAAC;EAAC,KAAK,EAAC,IAAI;;AAAC,yBAAoB;EAAC,UAAU,EAAC,IAAI;EAAC,aAAa,EAAC,GAAG;;AAAC,kIAAuH;EAAC,SAAS,EAAC,GAAG;;AAAC,yBAAoB;EAAC,UAAU,EAAC,GAAG;EAAC,aAAa,EAAC,GAAG;;AAAC,kIAAuH;EAAC,SAAS,EAAC,GAAG;;AAAC,OAAM;EAAC,SAAS,EAAC,IAAI;;AAAC,OAAM;EAAC,SAAS,EAAC,IAAI;;AAAC,OAAM;EAAC,SAAS,EAAC,IAAI;;AAAC,OAAM;EAAC,SAAS,EAAC,IAAI;;AAAC,OAAM;EAAC,SAAS,EAAC,IAAI;;AAAC,OAAM;EAAC,SAAS,EAAC,IAAI;;AAAC,CAAC;EAAC,MAAM,EAAC,OAAO;;AAAC,KAAK;EAAC,aAAa,EAAC,IAAI;EAAC,SAAS,EAAC,IAAI;EAAC,WAAW,EAAC,GAAG;EAAC,WAAW,EAAC,GAAG;;AAAC,yBAAwB;EAAC,KAAK;IAAC,SAAS,EAAC,MAAM;AAAE,aAAY;EAAC,SAAS,EAAC,GAAG;;AAAC,WAAU;EAAC,gBAAgB,EAAC,OAAO;EAAC,OAAO,EAAC,IAAI;;AAAC,UAAU;EAAC,UAAU,EAAC,IAAI;;AAAC,WAAW;EAAC,UAAU,EAAC,KAAK;;AAAC,YAAY;EAAC,UAAU,EAAC,MAAM;;AAAC,aAAa;EAAC,UAAU,EAAC,OAAO;;AAAC,YAAY;EAAC,WAAW,EAAC,MAAM;;AAAC,eAAe;EAAC,cAAc,EAAC,SAAS;;AAAC,eAAe;EAAC,cAAc,EAAC,SAAS;;AAAC,gBAAgB;EAAC,cAAc,EAAC,UAAU;;AAAC,WAAW;EAAC,KAAK,EAAC,IAAI;;AAAC,aAAa;EAAC,KAAK,EAAC,OAAO;;AAAC,0CAAyC;EAAC,KAAK,EAAC,OAAO;;AAAC,aAAa;EAAC,KAAK,EAAC,OAAO;;AAAC,0CAAyC;EAAC,KAAK,EAAC,OAAO;;AAAC,UAAU;EAAC,KAAK,EAAC,OAAO;;AAAC,oCAAmC;EAAC,KAAK,EAAC,OAAO;;AAAC,aAAa;EAAC,KAAK,EAAC,OAAO;;AAAC,0CAAyC;EAAC,KAAK,EAAC,OAAO;;AAAC,YAAY;EAAC,KAAK,EAAC,OAAO;;AAAC,wCAAuC;EAAC,KAAK,EAAC,OAAO;;AAAC,WAAW;EAAC,KAAK,EAAC,IAAI;EAAC,gBAAgB,EAAC,OAAO;;AAAC,sCAAqC;EAAC,gBAAgB,EAAC,OAAO;;AAAC,WAAW;EAAC,gBAAgB,EAAC,OAAO;;AAAC,sCAAqC;EAAC,gBAAgB,EAAC,OAAO;;AAAC,QAAQ;EAAC,gBAAgB,EAAC,OAAO;;AAAC,gCAA+B;EAAC,gBAAgB,EAAC,OAAO;;AAAC,WAAW;EAAC,gBAAgB,EAAC,OAAO;;AAAC,sCAAqC;EAAC,gBAAgB,EAAC,OAAO;;AAAC,UAAU;EAAC,gBAAgB,EAAC,OAAO;;AAAC,oCAAmC;EAAC,gBAAgB,EAAC,OAAO;;AAAC,YAAY;EAAC,cAAc,EAAC,GAAG;EAAC,MAAM,EAAC,WAAW;EAAC,aAAa,EAAC,cAAc;;AAAC,MAAK;EAAC,UAAU,EAAC,CAAC;EAAC,aAAa,EAAC,GAAG;;AAAC,0BAAuB;EAAC,aAAa,EAAC,CAAC;;AAAC,cAAc;EAAC,YAAY,EAAC,CAAC;EAAC,UAAU,EAAC,IAAI;;AAAC,YAAY;EAAC,YAAY,EAAC,CAAC;EAAC,UAAU,EAAC,IAAI;EAAC,MAAM,EAAC,CAAC;EAAC,WAAW,EAAC,IAAI;;AAAC,iBAAe;EAAC,OAAO,EAAC,YAAY;EAAC,YAAY,EAAC,GAAG;EAAC,aAAa,EAAC,GAAG;;AAAC,EAAE;EAAC,UAAU,EAAC,CAAC;EAAC,aAAa,EAAC,IAAI;;AAAC,MAAK;EAAC,WAAW,EAAC,UAAU;;AAAC,EAAE;EAAC,WAAW,EAAC,IAAI;;AAAC,EAAE;EAAC,WAAW,EAAC,CAAC;;AAAC,yBAAwB;EAAC,iBAAiB;IAAC,KAAK,EAAC,IAAI;IAAC,KAAK,EAAC,KAAK;IAAC,KAAK,EAAC,IAAI;IAAC,UAAU,EAAC,KAAK;IAAC,QAAQ,EAAC,MAAM;IAAC,aAAa,EAAC,QAAQ;IAAC,WAAW,EAAC,MAAM;;EAAC,iBAAiB;IAAC,WAAW,EAAC,KAAK;AAAE,sCAAqC;EAAC,MAAM,EAAC,IAAI;EAAC,aAAa,EAAC,eAAe;;AAAC,WAAW;EAAC,SAAS,EAAC,GAAG;EAAC,cAAc,EAAC,SAAS;;AAAC,UAAU;EAAC,OAAO,EAAC,QAAQ;EAAC,MAAM,EAAC,QAAQ;EAAC,SAAS,EAAC,OAAO;EAAC,WAAW,EAAC,cAAc;;AAAC,2EAAyE;EAAC,aAAa,EAAC,CAAC;;AAAC,sDAAoD;EAAC,OAAO,EAAC,KAAK;EAAC,SAAS,EAAC,GAAG;EAAC,WAAW,EAAC,UAAU;EAAC,KAAK,EAAC,IAAI;;AAAC,2EAAyE;EAAC,OAAO,EAAC,aAAa;;AAAC,0CAAyC;EAAC,aAAa,EAAC,IAAI;EAAC,YAAY,EAAC,CAAC;EAAC,YAAY,EAAC,cAAc;EAAC,WAAW,EAAC,CAAC;EAAC,UAAU,EAAC,KAAK;;AAAC,oNAA+M;EAAC,OAAO,EAAC,EAAE;;AAAC,8MAAyM;EAAC,OAAO,EAAC,aAAa;;AAAC,OAAO;EAAC,aAAa,EAAC,IAAI;EAAC,UAAU,EAAC,MAAM;EAAC,WAAW,EAAC,UAAU;;AAAC,oBAAiB;EAAC,WAAW,EAAC,6CAA6C;;AAAC,IAAI;EAAC,OAAO,EAAC,OAAO;EAAC,SAAS,EAAC,GAAG;EAAC,KAAK,EAAC,OAAO;EAAC,gBAAgB,EAAC,OAAO;EAAC,aAAa,EAAC,GAAG;;AAAC,GAAG;EAAC,OAAO,EAAC,OAAO;EAAC,SAAS,EAAC,GAAG;EAAC,KAAK,EAAC,IAAI;EAAC,gBAAgB,EAAC,IAAI;EAAC,aAAa,EAAC,GAAG;EAAC,UAAU,EAAC,kCAA+B;;AAAC,OAAO;EAAC,OAAO,EAAC,CAAC;EAAC,SAAS,EAAC,IAAI;EAAC,WAAW,EAAC,IAAI;EAAC,UAAU,EAAC,IAAI;;AAAC,GAAG;EAAC,OAAO,EAAC,KAAK;EAAC,OAAO,EAAC,KAAK;EAAC,MAAM,EAAC,OAAO;EAAC,SAAS,EAAC,IAAI;EAAC,WAAW,EAAC,UAAU;EAAC,UAAU,EAAC,SAAS;EAAC,SAAS,EAAC,UAAU;EAAC,KAAK,EAAC,IAAI;EAAC,gBAAgB,EAAC,OAAO;EAAC,MAAM,EAAC,cAAc;EAAC,aAAa,EAAC,GAAG;;AAAC,QAAQ;EAAC,OAAO,EAAC,CAAC;EAAC,SAAS,EAAC,OAAO;EAAC,KAAK,EAAC,OAAO;EAAC,WAAW,EAAC,QAAQ;EAAC,gBAAgB,EAAC,WAAW;EAAC,aAAa,EAAC,CAAC;;AAAC,eAAe;EAAC,UAAU,EAAC,KAAK;EAAC,UAAU,EAAC,MAAM;;AAAC,UAAU;EAAC,YAAY,EAAC,IAAI;EAAC,WAAW,EAAC,IAAI;EAAC,YAAY,EAAC,GAAG;EAAC,aAAa,EAAC,GAAG;;AAAC,yBAAwB;EAAC,UAAU;IAAC,KAAK,EAAC,KAAK;AAAE,yBAAwB;EAAC,UAAU;IAAC,KAAK,EAAC,KAAK;AAAE,0BAAyB;EAAC,UAAU;IAAC,KAAK,EAAC,KAAK;AAAE,gBAAgB;EAAC,YAAY,EAAC,IAAI;EAAC,WAAW,EAAC,IAAI;EAAC,YAAY,EAAC,GAAG;EAAC,aAAa,EAAC,GAAG;;AAAC,IAAI;EAAC,WAAW,EAAC,IAAI;EAAC,YAAY,EAAC,IAAI;;AAAC,0hBAA0hB;EAAC,QAAQ,EAAC,QAAQ;EAAC,UAAU,EAAC,GAAG;EAAC,YAAY,EAAC,GAAG;EAAC,aAAa,EAAC,GAAG;;AAAC,qIAAqI;EAAC,KAAK,EAAC,IAAI;;AAAC,UAAU;EAAC,KAAK,EAAC,IAAI;;AAAC,UAAU;EAAC,KAAK,EAAC,YAAY;;AAAC,UAAU;EAAC,KAAK,EAAC,YAAY;;AAAC,SAAS;EAAC,KAAK,EAAC,GAAG;;AAAC,SAAS;EAAC,KAAK,EAAC,YAAY;;AAAC,SAAS;EAAC,KAAK,EAAC,YAAY;;AAAC,SAAS;EAAC,KAAK,EAAC,GAAG;;AAAC,SAAS;EAAC,KAAK,EAAC,YAAY;;AAAC,SAAS;EAAC,KAAK,EAAC,YAAY;;AAAC,SAAS;EAAC,KAAK,EAAC,GAAG;;AAAC,SAAS;EAAC,KAAK,EAAC,YAAY;;AAAC,SAAS;EAAC,KAAK,EAAC,WAAW;;AAAC,eAAe;EAAC,KAAK,EAAC,IAAI;;AAAC,eAAe;EAAC,KAAK,EAAC,YAAY;;AAAC,eAAe;EAAC,KAAK,EAAC,YAAY;;AAAC,cAAc;EAAC,KAAK,EAAC,GAAG;;AAAC,cAAc;EAAC,KAAK,EAAC,YAAY;;AAAC,cAAc;EAAC,KAAK,EAAC,YAAY;;AAAC,cAAc;EAAC,KAAK,EAAC,GAAG;;AAAC,cAAc;EAAC,KAAK,EAAC,YAAY;;AAAC,cAAc;EAAC,KAAK,EAAC,YAAY;;AAAC,cAAc;EAAC,KAAK,EAAC,GAAG;;AAAC,cAAc;EAAC,KAAK,EAAC,YAAY;;AAAC,cAAc;EAAC,KAAK,EAAC,WAAW;;AAAC,cAAc;EAAC,KAAK,EAAC,IAAI;;AAAC,eAAe;EAAC,IAAI,EAAC,IAAI;;AAAC,eAAe;EAAC,IAAI,EAAC,YAAY;;AAAC,eAAe;EAAC,IAAI,EAAC,YAAY;;AAAC,cAAc;EAAC,IAAI,EAAC,GAAG;;AAAC,cAAc;EAAC,IAAI,EAAC,YAAY;;AAAC,cAAc;EAAC,IAAI,EAAC,YAAY;;AAAC,cAAc;EAAC,IAAI,EAAC,GAAG;;AAAC,cAAc;EAAC,IAAI,EAAC,YAAY;;AAAC,cAAc;EAAC,IAAI,EAAC,YAAY;;AAAC,cAAc;EAAC,IAAI,EAAC,GAAG;;AAAC,cAAc;EAAC,IAAI,EAAC,YAAY;;AAAC,cAAc;EAAC,IAAI,EAAC,WAAW;;AAAC,cAAc;EAAC,IAAI,EAAC,IAAI;;AAAC,iBAAiB;EAAC,WAAW,EAAC,IAAI;;AAAC,iBAAiB;EAAC,WAAW,EAAC,YAAY;;AAAC,iBAAiB;EAAC,WAAW,EAAC,YAAY;;AAAC,gBAAgB;EAAC,WAAW,EAAC,GAAG;;AAAC,gBAAgB;EAAC,WAAW,EAAC,YAAY;;AAAC,gBAAgB;EAAC,WAAW,EAAC,YAAY;;AAAC,gBAAgB;EAAC,WAAW,EAAC,GAAG;;AAAC,gBAAgB;EAAC,WAAW,EAAC,YAAY;;AAAC,gBAAgB;EAAC,WAAW,EAAC,YAAY;;AAAC,gBAAgB;EAAC,WAAW,EAAC,GAAG;;AAAC,gBAAgB;EAAC,WAAW,EAAC,YAAY;;AAAC,gBAAgB;EAAC,WAAW,EAAC,WAAW;;AAAC,gBAAgB;EAAC,WAAW,EAAC,CAAC;;AAAC,yBAAwB;EAAC,qIAAqI;IAAC,KAAK,EAAC,IAAI;;EAAC,UAAU;IAAC,KAAK,EAAC,IAAI;;EAAC,UAAU;IAAC,KAAK,EAAC,YAAY;;EAAC,UAAU;IAAC,KAAK,EAAC,YAAY;;EAAC,SAAS;IAAC,KAAK,EAAC,GAAG;;EAAC,SAAS;IAAC,KAAK,EAAC,YAAY;;EAAC,SAAS;IAAC,KAAK,EAAC,YAAY;;EAAC,SAAS;IAAC,KAAK,EAAC,GAAG;;EAAC,SAAS;IAAC,KAAK,EAAC,YAAY;;EAAC,SAAS;IAAC,KAAK,EAAC,YAAY;;EAAC,SAAS;IAAC,KAAK,EAAC,GAAG;;EAAC,SAAS;IAAC,KAAK,EAAC,YAAY;;EAAC,SAAS;IAAC,KAAK,EAAC,WAAW;;EAAC,eAAe;IAAC,KAAK,EAAC,IAAI;;EAAC,eAAe;IAAC,KAAK,EAAC,YAAY;;EAAC,eAAe;IAAC,KAAK,EAAC,YAAY;;EAAC,cAAc;IAAC,KAAK,EAAC,GAAG;;EAAC,cAAc;IAAC,KAAK,EAAC,YAAY;;EAAC,cAAc;IAAC,KAAK,EAAC,YAAY;;EAAC,cAAc;IAAC,KAAK,EAAC,GAAG;;EAAC,cAAc;IAAC,KAAK,EAAC,YAAY;;EAAC,cAAc;IAAC,KAAK,EAAC,YAAY;;EAAC,cAAc;IAAC,KAAK,EAAC,GAAG;;EAAC,cAAc;IAAC,KAAK,EAAC,YAAY;;EAAC,cAAc;IAAC,KAAK,EAAC,WAAW;;EAAC,cAAc;IAAC,KAAK,EAAC,IAAI;;EAAC,eAAe;IAAC,IAAI,EAAC,IAAI;;EAAC,eAAe;IAAC,IAAI,EAAC,YAAY;;EAAC,eAAe;IAAC,IAAI,EAAC,YAAY;;EAAC,cAAc;IAAC,IAAI,EAAC,GAAG;;EAAC,cAAc;IAAC,IAAI,EAAC,YAAY;;EAAC,cAAc;IAAC,IAAI,EAAC,YAAY;;EAAC,cAAc;IAAC,IAAI,EAAC,GAAG;;EAAC,cAAc;IAAC,IAAI,EAAC,YAAY;;EAAC,cAAc;IAAC,IAAI,EAAC,YAAY;;EAAC,cAAc;IAAC,IAAI,EAAC,GAAG;;EAAC,cAAc;IAAC,IAAI,EAAC,YAAY;;EAAC,cAAc;IAAC,IAAI,EAAC,WAAW;;EAAC,cAAc;IAAC,IAAI,EAAC,IAAI;;EAAC,iBAAiB;IAAC,WAAW,EAAC,IAAI;;EAAC,iBAAiB;IAAC,WAAW,EAAC,YAAY;;EAAC,iBAAiB;IAAC,WAAW,EAAC,YAAY;;EAAC,gBAAgB;IAAC,WAAW,EAAC,GAAG;;EAAC,gBAAgB;IAAC,WAAW,EAAC,YAAY;;EAAC,gBAAgB;IAAC,WAAW,EAAC,YAAY;;EAAC,gBAAgB;IAAC,WAAW,EAAC,GAAG;;EAAC,gBAAgB;IAAC,WAAW,EAAC,YAAY;;EAAC,gBAAgB;IAAC,WAAW,EAAC,YAAY;;EAAC,gBAAgB;IAAC,WAAW,EAAC,GAAG;;EAAC,gBAAgB;IAAC,WAAW,EAAC,YAAY;;EAAC,gBAAgB;IAAC,WAAW,EAAC,WAAW;;EAAC,gBAAgB;IAAC,WAAW,EAAC,CAAC;AAAE,yBAAwB;EAAC,qIAAqI;IAAC,KAAK,EAAC,IAAI;;EAAC,UAAU;IAAC,KAAK,EAAC,IAAI;;EAAC,UAAU;IAAC,KAAK,EAAC,YAAY;;EAAC,UAAU;IAAC,KAAK,EAAC,YAAY;;EAAC,SAAS;IAAC,KAAK,EAAC,GAAG;;EAAC,SAAS;IAAC,KAAK,EAAC,YAAY;;EAAC,SAAS;IAAC,KAAK,EAAC,YAAY;;EAAC,SAAS;IAAC,KAAK,EAAC,GAAG;;EAAC,SAAS;IAAC,KAAK,EAAC,YAAY;;EAAC,SAAS;IAAC,KAAK,EAAC,YAAY;;EAAC,SAAS;IAAC,KAAK,EAAC,GAAG;;EAAC,SAAS;IAAC,KAAK,EAAC,YAAY;;EAAC,SAAS;IAAC,KAAK,EAAC,WAAW;;EAAC,eAAe;IAAC,KAAK,EAAC,IAAI;;EAAC,eAAe;IAAC,KAAK,EAAC,YAAY;;EAAC,eAAe;IAAC,KAAK,EAAC,YAAY;;EAAC,cAAc;IAAC,KAAK,EAAC,GAAG;;EAAC,cAAc;IAAC,KAAK,EAAC,YAAY;;EAAC,cAAc;IAAC,KAAK,EAAC,YAAY;;EAAC,cAAc;IAAC,KAAK,EAAC,GAAG;;EAAC,cAAc;IAAC,KAAK,EAAC,YAAY;;EAAC,cAAc;IAAC,KAAK,EAAC,YAAY;;EAAC,cAAc;IAAC,KAAK,EAAC,GAAG;;EAAC,cAAc;IAAC,KAAK,EAAC,YAAY;;EAAC,cAAc;IAAC,KAAK,EAAC,WAAW;;EAAC,cAAc;IAAC,KAAK,EAAC,IAAI;;EAAC,eAAe;IAAC,IAAI,EAAC,IAAI;;EAAC,eAAe;IAAC,IAAI,EAAC,YAAY;;EAAC,eAAe;IAAC,IAAI,EAAC,YAAY;;EAAC,cAAc;IAAC,IAAI,EAAC,GAAG;;EAAC,cAAc;IAAC,IAAI,EAAC,YAAY;;EAAC,cAAc;IAAC,IAAI,EAAC,YAAY;;EAAC,cAAc;IAAC,IAAI,EAAC,GAAG;;EAAC,cAAc;IAAC,IAAI,EAAC,YAAY;;EAAC,cAAc;IAAC,IAAI,EAAC,YAAY;;EAAC,cAAc;IAAC,IAAI,EAAC,GAAG;;EAAC,cAAc;IAAC,IAAI,EAAC,YAAY;;EAAC,cAAc;IAAC,IAAI,EAAC,WAAW;;EAAC,cAAc;IAAC,IAAI,EAAC,IAAI;;EAAC,iBAAiB;IAAC,WAAW,EAAC,IAAI;;EAAC,iBAAiB;IAAC,WAAW,EAAC,YAAY;;EAAC,iBAAiB;IAAC,WAAW,EAAC,YAAY;;EAAC,gBAAgB;IAAC,WAAW,EAAC,GAAG;;EAAC,gBAAgB;IAAC,WAAW,EAAC,YAAY;;EAAC,gBAAgB;IAAC,WAAW,EAAC,YAAY;;EAAC,gBAAgB;IAAC,WAAW,EAAC,GAAG;;EAAC,gBAAgB;IAAC,WAAW,EAAC,YAAY;;EAAC,gBAAgB;IAAC,WAAW,EAAC,YAAY;;EAAC,gBAAgB;IAAC,WAAW,EAAC,GAAG;;EAAC,gBAAgB;IAAC,WAAW,EAAC,YAAY;;EAAC,gBAAgB;IAAC,WAAW,EAAC,WAAW;;EAAC,gBAAgB;IAAC,WAAW,EAAC,CAAC;AAAE,0BAAyB;EAAC,qIAAqI;IAAC,KAAK,EAAC,IAAI;;EAAC,UAAU;IAAC,KAAK,EAAC,IAAI;;EAAC,UAAU;IAAC,KAAK,EAAC,YAAY;;EAAC,UAAU;IAAC,KAAK,EAAC,YAAY;;EAAC,SAAS;IAAC,KAAK,EAAC,GAAG;;EAAC,SAAS;IAAC,KAAK,EAAC,YAAY;;EAAC,SAAS;IAAC,KAAK,EAAC,YAAY;;EAAC,SAAS;IAAC,KAAK,EAAC,GAAG;;EAAC,SAAS;IAAC,KAAK,EAAC,YAAY;;EAAC,SAAS;IAAC,KAAK,EAAC,YAAY;;EAAC,SAAS;IAAC,KAAK,EAAC,GAAG;;EAAC,SAAS;IAAC,KAAK,EAAC,YAAY;;EAAC,SAAS;IAAC,KAAK,EAAC,WAAW;;EAAC,eAAe;IAAC,KAAK,EAAC,IAAI;;EAAC,eAAe;IAAC,KAAK,EAAC,YAAY;;EAAC,eAAe;IAAC,KAAK,EAAC,YAAY;;EAAC,cAAc;IAAC,KAAK,EAAC,GAAG;;EAAC,cAAc;IAAC,KAAK,EAAC,YAAY;;EAAC,cAAc;IAAC,KAAK,EAAC,YAAY;;EAAC,cAAc;IAAC,KAAK,EAAC,GAAG;;EAAC,cAAc;IAAC,KAAK,EAAC,YAAY;;EAAC,cAAc;IAAC,KAAK,EAAC,YAAY;;EAAC,cAAc;IAAC,KAAK,EAAC,GAAG;;EAAC,cAAc;IAAC,KAAK,EAAC,YAAY;;EAAC,cAAc;IAAC,KAAK,EAAC,WAAW;;EAAC,cAAc;IAAC,KAAK,EAAC,IAAI;;EAAC,eAAe;IAAC,IAAI,EAAC,IAAI;;EAAC,eAAe;IAAC,IAAI,EAAC,YAAY;;EAAC,eAAe;IAAC,IAAI,EAAC,YAAY;;EAAC,cAAc;IAAC,IAAI,EAAC,GAAG;;EAAC,cAAc;IAAC,IAAI,EAAC,YAAY;;EAAC,cAAc;IAAC,IAAI,EAAC,YAAY;;EAAC,cAAc;IAAC,IAAI,EAAC,GAAG;;EAAC,cAAc;IAAC,IAAI,EAAC,YAAY;;EAAC,cAAc;IAAC,IAAI,EAAC,YAAY;;EAAC,cAAc;IAAC,IAAI,EAAC,GAAG;;EAAC,cAAc;IAAC,IAAI,EAAC,YAAY;;EAAC,cAAc;IAAC,IAAI,EAAC,WAAW;;EAAC,cAAc;IAAC,IAAI,EAAC,IAAI;;EAAC,iBAAiB;IAAC,WAAW,EAAC,IAAI;;EAAC,iBAAiB;IAAC,WAAW,EAAC,YAAY;;EAAC,iBAAiB;IAAC,WAAW,EAAC,YAAY;;EAAC,gBAAgB;IAAC,WAAW,EAAC,GAAG;;EAAC,gBAAgB;IAAC,WAAW,EAAC,YAAY;;EAAC,gBAAgB;IAAC,WAAW,EAAC,YAAY;;EAAC,gBAAgB;IAAC,WAAW,EAAC,GAAG;;EAAC,gBAAgB;IAAC,WAAW,EAAC,YAAY;;EAAC,gBAAgB;IAAC,WAAW,EAAC,YAAY;;EAAC,gBAAgB;IAAC,WAAW,EAAC,GAAG;;EAAC,gBAAgB;IAAC,WAAW,EAAC,YAAY;;EAAC,gBAAgB;IAAC,WAAW,EAAC,WAAW;;EAAC,gBAAgB;IAAC,WAAW,EAAC,CAAC;AAAE,KAAK;EAAC,gBAAgB,EAAC,WAAW;;AAAC,OAAO;EAAC,WAAW,EAAC,GAAG;EAAC,cAAc,EAAC,GAAG;EAAC,KAAK,EAAC,IAAI;EAAC,UAAU,EAAC,IAAI;;AAAC,EAAE;EAAC,UAAU,EAAC,IAAI;;AAAC,MAAM;EAAC,KAAK,EAAC,IAAI;EAAC,SAAS,EAAC,IAAI;EAAC,aAAa,EAAC,IAAI;;AAAC,0JAAiH;EAAC,OAAO,EAAC,GAAG;EAAC,WAAW,EAAC,UAAU;EAAC,cAAc,EAAC,GAAG;EAAC,UAAU,EAAC,cAAc;;AAAC,wBAAkB;EAAC,cAAc,EAAC,MAAM;EAAC,aAAa,EAAC,cAAc;;AAAC,oSAAmP;EAAC,UAAU,EAAC,CAAC;;AAAC,sBAAkB;EAAC,UAAU,EAAC,cAAc;;AAAC,aAAa;EAAC,gBAAgB,EAAC,IAAI;;AAAC,sNAA6K;EAAC,OAAO,EAAC,GAAG;;AAAC,eAAe;EAAC,MAAM,EAAC,cAAc;;AAAC,gNAAuK;EAAC,MAAM,EAAC,cAAc;;AAAC,oEAAuD;EAAC,mBAAmB,EAAC,GAAG;;AAAC,4CAAwC;EAAC,gBAAgB,EAAC,OAAO;;AAAC,+BAA2B;EAAC,gBAAgB,EAAC,OAAO;;AAAC,wBAAwB;EAAC,QAAQ,EAAC,MAAM;EAAC,KAAK,EAAC,IAAI;EAAC,OAAO,EAAC,YAAY;;AAAC,gDAA+C;EAAC,QAAQ,EAAC,MAAM;EAAC,KAAK,EAAC,IAAI;EAAC,OAAO,EAAC,UAAU;;AAAC,0YAAuT;EAAC,gBAAgB,EAAC,OAAO;;AAAC,6NAA2L;EAAC,gBAAgB,EAAC,OAAO;;AAAC,sZAAmU;EAAC,gBAAgB,EAAC,OAAO;;AAAC,kOAAgM;EAAC,gBAAgB,EAAC,OAAO;;AAAC,kXAA+R;EAAC,gBAAgB,EAAC,OAAO;;AAAC,mNAAiL;EAAC,gBAAgB,EAAC,OAAO;;AAAC,sZAAmU;EAAC,gBAAgB,EAAC,OAAO;;AAAC,kOAAgM;EAAC,gBAAgB,EAAC,OAAO;;AAAC,0YAAuT;EAAC,gBAAgB,EAAC,OAAO;;AAAC,6NAA2L;EAAC,gBAAgB,EAAC,OAAO;;AAAC,iBAAiB;EAAC,UAAU,EAAC,IAAI;EAAC,UAAU,EAAC,IAAI;;AAAC,oCAAmC;EAAC,iBAAiB;IAAC,KAAK,EAAC,IAAI;IAAC,aAAa,EAAC,MAAM;IAAC,UAAU,EAAC,MAAM;IAAC,kBAAkB,EAAC,wBAAwB;IAAC,MAAM,EAAC,cAAc;;EAAC,0BAAwB;IAAC,aAAa,EAAC,CAAC;;EAAC,kRAA6N;IAAC,WAAW,EAAC,MAAM;;EAAC,mCAAiC;IAAC,MAAM,EAAC,CAAC;;EAAC,gZAA2V;IAAC,WAAW,EAAC,CAAC;;EAAC,0YAAqV;IAAC,YAAY,EAAC,CAAC;;EAAC,sQAAmO;IAAC,aAAa,EAAC,CAAC;AAAE,QAAQ;EAAC,OAAO,EAAC,CAAC;EAAC,MAAM,EAAC,CAAC;EAAC,MAAM,EAAC,CAAC;EAAC,SAAS,EAAC,CAAC;;AAAC,MAAM;EAAC,OAAO,EAAC,KAAK;EAAC,KAAK,EAAC,IAAI;EAAC,OAAO,EAAC,CAAC;EAAC,aAAa,EAAC,IAAI;EAAC,SAAS,EAAC,MAAM;EAAC,WAAW,EAAC,OAAO;EAAC,KAAK,EAAC,IAAI;EAAC,MAAM,EAAC,CAAC;EAAC,aAAa,EAAC,cAAc;;AAAC,KAAK;EAAC,OAAO,EAAC,YAAY;EAAC,SAAS,EAAC,IAAI;EAAC,aAAa,EAAC,GAAG;EAAC,WAAW,EAAC,IAAI;;AAAC,oBAAoB;EAAC,kBAAkB,EAAC,UAAU;EAAC,eAAe,EAAC,UAAU;EAAC,UAAU,EAAC,UAAU;;AAAC,2CAA0C;EAAC,MAAM,EAAC,OAAO;EAAC,UAAU,EAAC,MAAM;EAAC,WAAW,EAAC,MAAM;;AAAC,kBAAkB;EAAC,OAAO,EAAC,KAAK;;AAAC,mBAAmB;EAAC,OAAO,EAAC,KAAK;EAAC,KAAK,EAAC,IAAI;;AAAC,8BAA6B;EAAC,MAAM,EAAC,IAAI;;AAAC,iFAA+E;EAAC,OAAO,EAAC,WAAW;EAAC,OAAO,EAAC,iCAAiC;EAAC,cAAc,EAAC,IAAI;;AAAC,MAAM;EAAC,OAAO,EAAC,KAAK;EAAC,WAAW,EAAC,GAAG;EAAC,SAAS,EAAC,IAAI;EAAC,WAAW,EAAC,UAAU;EAAC,KAAK,EAAC,IAAI;;AAAC,aAAa;EAAC,OAAO,EAAC,KAAK;EAAC,KAAK,EAAC,IAAI;EAAC,MAAM,EAAC,IAAI;EAAC,OAAO,EAAC,OAAO;EAAC,SAAS,EAAC,IAAI;EAAC,WAAW,EAAC,UAAU;EAAC,KAAK,EAAC,IAAI;EAAC,gBAAgB,EAAC,IAAI;EAAC,gBAAgB,EAAC,IAAI;EAAC,MAAM,EAAC,cAAc;EAAC,aAAa,EAAC,GAAG;EAAC,kBAAkB,EAAC,oCAAiC;EAAC,UAAU,EAAC,oCAAiC;EAAC,kBAAkB,EAAC,0DAA0D;EAAC,aAAa,EAAC,0DAA0D;EAAC,UAAU,EAAC,0DAA0D;;AAAC,mBAAmB;EAAC,YAAY,EAAC,OAAO;EAAC,OAAO,EAAC,CAAC;EAAC,kBAAkB,EAAC,oEAAgE;EAAC,UAAU,EAAC,oEAAgE;;AAAC,+BAA+B;EAAC,KAAK,EAAC,IAAI;EAAC,OAAO,EAAC,CAAC;;AAAC,mCAAmC;EAAC,KAAK,EAAC,IAAI;;AAAC,wCAAwC;EAAC,KAAK,EAAC,IAAI;;AAAC,yBAAyB;EAAC,MAAM,EAAC,CAAC;EAAC,gBAAgB,EAAC,WAAW;;AAAC,kFAAgF;EAAC,gBAAgB,EAAC,IAAI;EAAC,OAAO,EAAC,CAAC;;AAAC,yDAAwD;EAAC,MAAM,EAAC,WAAW;;AAAC,qBAAqB;EAAC,MAAM,EAAC,IAAI;;AAAC,oBAAoB;EAAC,kBAAkB,EAAC,IAAI;;AAAC,qDAAoD;EAAC,6IAA0I;IAAC,WAAW,EAAC,IAAI;;EAAC,wRAAiR;IAAC,WAAW,EAAC,IAAI;;EAAC,wRAAiR;IAAC,WAAW,EAAC,IAAI;AAAE,WAAW;EAAC,aAAa,EAAC,IAAI;;AAAC,iBAAgB;EAAC,QAAQ,EAAC,QAAQ;EAAC,OAAO,EAAC,KAAK;EAAC,UAAU,EAAC,IAAI;EAAC,aAAa,EAAC,IAAI;;AAAC,6BAA4B;EAAC,UAAU,EAAC,IAAI;EAAC,YAAY,EAAC,IAAI;EAAC,aAAa,EAAC,CAAC;EAAC,WAAW,EAAC,MAAM;EAAC,MAAM,EAAC,OAAO;;AAAC,wIAAqI;EAAC,QAAQ,EAAC,QAAQ;EAAC,WAAW,EAAC,KAAK;EAAC,UAAU,EAAC,MAAM;;AAAC,sCAAiC;EAAC,UAAU,EAAC,IAAI;;AAAC,+BAA8B;EAAC,QAAQ,EAAC,QAAQ;EAAC,OAAO,EAAC,YAAY;EAAC,YAAY,EAAC,IAAI;EAAC,aAAa,EAAC,CAAC;EAAC,cAAc,EAAC,MAAM;EAAC,WAAW,EAAC,MAAM;EAAC,MAAM,EAAC,OAAO;;AAAC,kEAA6D;EAAC,UAAU,EAAC,CAAC;EAAC,WAAW,EAAC,IAAI;;AAAC,iNAA4M;EAAC,MAAM,EAAC,WAAW;;AAAC,wHAAqH;EAAC,MAAM,EAAC,WAAW;;AAAC,oHAAiH;EAAC,MAAM,EAAC,WAAW;;AAAC,oBAAoB;EAAC,WAAW,EAAC,GAAG;EAAC,cAAc,EAAC,GAAG;EAAC,aAAa,EAAC,CAAC;EAAC,UAAU,EAAC,IAAI;;AAAC,4DAA2D;EAAC,YAAY,EAAC,CAAC;EAAC,aAAa,EAAC,CAAC;;AAAC,SAAS;EAAC,MAAM,EAAC,IAAI;EAAC,OAAO,EAAC,OAAO;EAAC,SAAS,EAAC,IAAI;EAAC,WAAW,EAAC,GAAG;EAAC,aAAa,EAAC,GAAG;;AAAC,eAAe;EAAC,MAAM,EAAC,IAAI;EAAC,WAAW,EAAC,IAAI;;AAAC,4CAA2C;EAAC,MAAM,EAAC,IAAI;;AAAC,4BAA4B;EAAC,MAAM,EAAC,IAAI;EAAC,OAAO,EAAC,OAAO;EAAC,SAAS,EAAC,IAAI;EAAC,WAAW,EAAC,GAAG;EAAC,aAAa,EAAC,GAAG;;AAAC,kCAAkC;EAAC,MAAM,EAAC,IAAI;EAAC,WAAW,EAAC,IAAI;;AAAC,kFAAiF;EAAC,MAAM,EAAC,IAAI;;AAAC,mCAAmC;EAAC,MAAM,EAAC,IAAI;EAAC,UAAU,EAAC,IAAI;EAAC,OAAO,EAAC,OAAO;EAAC,SAAS,EAAC,IAAI;EAAC,WAAW,EAAC,GAAG;;AAAC,SAAS;EAAC,MAAM,EAAC,IAAI;EAAC,OAAO,EAAC,OAAO;EAAC,SAAS,EAAC,IAAI;EAAC,WAAW,EAAC,SAAS;EAAC,aAAa,EAAC,GAAG;;AAAC,eAAe;EAAC,MAAM,EAAC,IAAI;EAAC,WAAW,EAAC,IAAI;;AAAC,4CAA2C;EAAC,MAAM,EAAC,IAAI;;AAAC,4BAA4B;EAAC,MAAM,EAAC,IAAI;EAAC,OAAO,EAAC,OAAO;EAAC,SAAS,EAAC,IAAI;EAAC,WAAW,EAAC,SAAS;EAAC,aAAa,EAAC,GAAG;;AAAC,kCAAkC;EAAC,MAAM,EAAC,IAAI;EAAC,WAAW,EAAC,IAAI;;AAAC,kFAAiF;EAAC,MAAM,EAAC,IAAI;;AAAC,mCAAmC;EAAC,MAAM,EAAC,IAAI;EAAC,UAAU,EAAC,IAAI;EAAC,OAAO,EAAC,OAAO;EAAC,SAAS,EAAC,IAAI;EAAC,WAAW,EAAC,SAAS;;AAAC,aAAa;EAAC,QAAQ,EAAC,QAAQ;;AAAC,2BAA2B;EAAC,aAAa,EAAC,MAAM;;AAAC,sBAAsB;EAAC,QAAQ,EAAC,QAAQ;EAAC,GAAG,EAAC,CAAC;EAAC,KAAK,EAAC,CAAC;EAAC,OAAO,EAAC,CAAC;EAAC,OAAO,EAAC,KAAK;EAAC,KAAK,EAAC,IAAI;EAAC,MAAM,EAAC,IAAI;EAAC,WAAW,EAAC,IAAI;EAAC,UAAU,EAAC,MAAM;EAAC,cAAc,EAAC,IAAI;;AAAC,mIAA2H;EAAC,KAAK,EAAC,IAAI;EAAC,MAAM,EAAC,IAAI;EAAC,WAAW,EAAC,IAAI;;AAAC,mIAA2H;EAAC,KAAK,EAAC,IAAI;EAAC,MAAM,EAAC,IAAI;EAAC,WAAW,EAAC,IAAI;;AAAC,yRAAgR;EAAC,KAAK,EAAC,OAAO;;AAAC,0BAA0B;EAAC,YAAY,EAAC,OAAO;EAAC,kBAAkB,EAAC,oCAAiC;EAAC,UAAU,EAAC,oCAAiC;;AAAC,gCAAgC;EAAC,YAAY,EAAC,OAAO;EAAC,kBAAkB,EAAC,qDAAiD;EAAC,UAAU,EAAC,qDAAiD;;AAAC,+BAA+B;EAAC,KAAK,EAAC,OAAO;EAAC,YAAY,EAAC,OAAO;EAAC,gBAAgB,EAAC,OAAO;;AAAC,mCAAmC;EAAC,KAAK,EAAC,OAAO;;AAAC,yRAAgR;EAAC,KAAK,EAAC,OAAO;;AAAC,0BAA0B;EAAC,YAAY,EAAC,OAAO;EAAC,kBAAkB,EAAC,oCAAiC;EAAC,UAAU,EAAC,oCAAiC;;AAAC,gCAAgC;EAAC,YAAY,EAAC,OAAO;EAAC,kBAAkB,EAAC,qDAAiD;EAAC,UAAU,EAAC,qDAAiD;;AAAC,+BAA+B;EAAC,KAAK,EAAC,OAAO;EAAC,YAAY,EAAC,OAAO;EAAC,gBAAgB,EAAC,OAAO;;AAAC,mCAAmC;EAAC,KAAK,EAAC,OAAO;;AAAC,qQAA4P;EAAC,KAAK,EAAC,OAAO;;AAAC,wBAAwB;EAAC,YAAY,EAAC,OAAO;EAAC,kBAAkB,EAAC,oCAAiC;EAAC,UAAU,EAAC,oCAAiC;;AAAC,8BAA8B;EAAC,YAAY,EAAC,OAAO;EAAC,kBAAkB,EAAC,qDAAiD;EAAC,UAAU,EAAC,qDAAiD;;AAAC,6BAA6B;EAAC,KAAK,EAAC,OAAO;EAAC,YAAY,EAAC,OAAO;EAAC,gBAAgB,EAAC,OAAO;;AAAC,iCAAiC;EAAC,KAAK,EAAC,OAAO;;AAAC,4CAA0C;EAAC,GAAG,EAAC,IAAI;;AAAC,oDAAkD;EAAC,GAAG,EAAC,CAAC;;AAAC,WAAW;EAAC,OAAO,EAAC,KAAK;EAAC,UAAU,EAAC,GAAG;EAAC,aAAa,EAAC,IAAI;EAAC,KAAK,EAAC,OAAO;;AAAC,yBAAwB;EAAC,wBAAwB;IAAC,OAAO,EAAC,YAAY;IAAC,aAAa,EAAC,CAAC;IAAC,cAAc,EAAC,MAAM;;EAAC,0BAA0B;IAAC,OAAO,EAAC,YAAY;IAAC,KAAK,EAAC,IAAI;IAAC,cAAc,EAAC,MAAM;;EAAC,iCAAiC;IAAC,OAAO,EAAC,YAAY;;EAAC,yBAAyB;IAAC,OAAO,EAAC,YAAY;IAAC,cAAc,EAAC,MAAM;;EAAC,iIAA+H;IAAC,KAAK,EAAC,IAAI;;EAAC,yCAAuC;IAAC,KAAK,EAAC,IAAI;;EAAC,2BAA2B;IAAC,aAAa,EAAC,CAAC;IAAC,cAAc,EAAC,MAAM;;EAAC,2CAA0C;IAAC,OAAO,EAAC,YAAY;IAAC,UAAU,EAAC,CAAC;IAAC,aAAa,EAAC,CAAC;IAAC,cAAc,EAAC,MAAM;;EAAC,uDAAsD;IAAC,YAAY,EAAC,CAAC;;EAAC,sFAAqF;IAAC,QAAQ,EAAC,QAAQ;IAAC,WAAW,EAAC,CAAC;;EAAC,iDAAiD;IAAC,GAAG,EAAC,CAAC;AAAE,sHAAmH;EAAC,UAAU,EAAC,CAAC;EAAC,aAAa,EAAC,CAAC;EAAC,WAAW,EAAC,GAAG;;AAAC,mDAAkD;EAAC,UAAU,EAAC,IAAI;;AAAC,4BAA4B;EAAC,WAAW,EAAC,IAAI;EAAC,YAAY,EAAC,IAAI;;AAAC,yBAAwB;EAAC,+BAA+B;IAAC,UAAU,EAAC,KAAK;IAAC,aAAa,EAAC,CAAC;IAAC,WAAW,EAAC,GAAG;AAAE,qDAAqD;EAAC,KAAK,EAAC,GAAG;;AAAC,yBAAwB;EAAC,8CAA8C;IAAC,WAAW,EAAC,GAAG;IAAC,SAAS,EAAC,IAAI;AAAE,yBAAwB;EAAC,8CAA8C;IAAC,WAAW,EAAC,GAAG;IAAC,SAAS,EAAC,IAAI;AAAE,IAAI;EAAC,OAAO,EAAC,YAAY;EAAC,aAAa,EAAC,CAAC;EAAC,WAAW,EAAC,MAAM;EAAC,UAAU,EAAC,MAAM;EAAC,cAAc,EAAC,MAAM;EAAC,YAAY,EAAC,YAAY;EAAC,MAAM,EAAC,OAAO;EAAC,gBAAgB,EAAC,IAAI;EAAC,MAAM,EAAC,qBAAqB;EAAC,WAAW,EAAC,MAAM;EAAC,OAAO,EAAC,OAAO;EAAC,SAAS,EAAC,IAAI;EAAC,WAAW,EAAC,UAAU;EAAC,aAAa,EAAC,GAAG;EAAC,mBAAmB,EAAC,IAAI;EAAC,gBAAgB,EAAC,IAAI;EAAC,eAAe,EAAC,IAAI;EAAC,WAAW,EAAC,IAAI;;AAAC,kGAA6F;EAAC,OAAO,EAAC,WAAW;EAAC,OAAO,EAAC,iCAAiC;EAAC,cAAc,EAAC,IAAI;;AAAC,kCAAgC;EAAC,KAAK,EAAC,IAAI;EAAC,eAAe,EAAC,IAAI;;AAAC,wBAAuB;EAAC,OAAO,EAAC,CAAC;EAAC,gBAAgB,EAAC,IAAI;EAAC,kBAAkB,EAAC,oCAAiC;EAAC,UAAU,EAAC,oCAAiC;;AAAC,sDAAoD;EAAC,MAAM,EAAC,WAAW;EAAC,OAAO,EAAC,GAAG;EAAC,MAAM,EAAC,iBAAiB;EAAC,kBAAkB,EAAC,IAAI;EAAC,UAAU,EAAC,IAAI;;AAAC,wCAAuC;EAAC,cAAc,EAAC,IAAI;;AAAC,YAAY;EAAC,KAAK,EAAC,IAAI;EAAC,gBAAgB,EAAC,IAAI;EAAC,YAAY,EAAC,IAAI;;AAAC,sCAAqC;EAAC,KAAK,EAAC,IAAI;EAAC,gBAAgB,EAAC,OAAO;EAAC,YAAY,EAAC,OAAO;;AAAC,kBAAkB;EAAC,KAAK,EAAC,IAAI;EAAC,gBAAgB,EAAC,OAAO;EAAC,YAAY,EAAC,OAAO;;AAAC,8EAA0E;EAAC,KAAK,EAAC,IAAI;EAAC,gBAAgB,EAAC,OAAO;EAAC,YAAY,EAAC,OAAO;;AAAC,oSAAsR;EAAC,KAAK,EAAC,IAAI;EAAC,gBAAgB,EAAC,OAAO;EAAC,YAAY,EAAC,OAAO;;AAAC,8EAA0E;EAAC,gBAAgB,EAAC,IAAI;;AAAC,oSAA4R;EAAC,gBAAgB,EAAC,IAAI;EAAC,YAAY,EAAC,IAAI;;AAAC,mBAAmB;EAAC,KAAK,EAAC,IAAI;EAAC,gBAAgB,EAAC,IAAI;;AAAC,oHAAgH;EAAC,KAAK,EAAC,IAAI;EAAC,gBAAgB,EAAC,IAAI;EAAC,YAAY,EAAC,OAAO;;AAAC,YAAY;EAAC,KAAK,EAAC,IAAI;EAAC,gBAAgB,EAAC,OAAO;EAAC,YAAY,EAAC,IAAI;;AAAC,sCAAqC;EAAC,KAAK,EAAC,IAAI;EAAC,gBAAgB,EAAC,OAAO;EAAC,YAAY,EAAC,OAAO;;AAAC,kBAAkB;EAAC,KAAK,EAAC,IAAI;EAAC,gBAAgB,EAAC,OAAO;EAAC,YAAY,EAAC,OAAO;;AAAC,8EAA0E;EAAC,KAAK,EAAC,IAAI;EAAC,gBAAgB,EAAC,OAAO;EAAC,YAAY,EAAC,OAAO;;AAAC,oSAAsR;EAAC,KAAK,EAAC,IAAI;EAAC,gBAAgB,EAAC,OAAO;EAAC,YAAY,EAAC,OAAO;;AAAC,8EAA0E;EAAC,gBAAgB,EAAC,IAAI;;AAAC,oSAA4R;EAAC,gBAAgB,EAAC,OAAO;EAAC,YAAY,EAAC,IAAI;;AAAC,mBAAmB;EAAC,KAAK,EAAC,OAAO;EAAC,gBAAgB,EAAC,IAAI;;AAAC,oHAAgH;EAAC,KAAK,EAAC,OAAO;EAAC,gBAAgB,EAAC,IAAI;EAAC,YAAY,EAAC,OAAO;;AAAC,YAAY;EAAC,KAAK,EAAC,IAAI;EAAC,gBAAgB,EAAC,OAAO;EAAC,YAAY,EAAC,IAAI;;AAAC,sCAAqC;EAAC,KAAK,EAAC,IAAI;EAAC,gBAAgB,EAAC,OAAO;EAAC,YAAY,EAAC,OAAO;;AAAC,kBAAkB;EAAC,KAAK,EAAC,IAAI;EAAC,gBAAgB,EAAC,OAAO;EAAC,YAAY,EAAC,OAAO;;AAAC,8EAA0E;EAAC,KAAK,EAAC,IAAI;EAAC,gBAAgB,EAAC,OAAO;EAAC,YAAY,EAAC,OAAO;;AAAC,oSAAsR;EAAC,KAAK,EAAC,IAAI;EAAC,gBAAgB,EAAC,OAAO;EAAC,YAAY,EAAC,OAAO;;AAAC,8EAA0E;EAAC,gBAAgB,EAAC,IAAI;;AAAC,oSAA4R;EAAC,gBAAgB,EAAC,OAAO;EAAC,YAAY,EAAC,IAAI;;AAAC,mBAAmB;EAAC,KAAK,EAAC,OAAO;EAAC,gBAAgB,EAAC,IAAI;;AAAC,oHAAgH;EAAC,KAAK,EAAC,OAAO;EAAC,gBAAgB,EAAC,IAAI;EAAC,YAAY,EAAC,OAAO;;AAAC,SAAS;EAAC,KAAK,EAAC,IAAI;EAAC,gBAAgB,EAAC,OAAO;EAAC,YAAY,EAAC,IAAI;;AAAC,gCAA+B;EAAC,KAAK,EAAC,IAAI;EAAC,gBAAgB,EAAC,OAAO;EAAC,YAAY,EAAC,OAAO;;AAAC,eAAe;EAAC,KAAK,EAAC,IAAI;EAAC,gBAAgB,EAAC,OAAO;EAAC,YAAY,EAAC,OAAO;;AAAC,qEAAiE;EAAC,KAAK,EAAC,IAAI;EAAC,gBAAgB,EAAC,OAAO;EAAC,YAAY,EAAC,OAAO;;AAAC,yQAA2P;EAAC,KAAK,EAAC,IAAI;EAAC,gBAAgB,EAAC,OAAO;EAAC,YAAY,EAAC,OAAO;;AAAC,qEAAiE;EAAC,gBAAgB,EAAC,IAAI;;AAAC,yQAAiQ;EAAC,gBAAgB,EAAC,OAAO;EAAC,YAAY,EAAC,IAAI;;AAAC,gBAAgB;EAAC,KAAK,EAAC,OAAO;EAAC,gBAAgB,EAAC,IAAI;;AAAC,qGAAiG;EAAC,KAAK,EAAC,OAAO;EAAC,gBAAgB,EAAC,IAAI;EAAC,YAAY,EAAC,OAAO;;AAAC,YAAY;EAAC,KAAK,EAAC,IAAI;EAAC,gBAAgB,EAAC,OAAO;EAAC,YAAY,EAAC,IAAI;;AAAC,sCAAqC;EAAC,KAAK,EAAC,IAAI;EAAC,gBAAgB,EAAC,OAAO;EAAC,YAAY,EAAC,OAAO;;AAAC,kBAAkB;EAAC,KAAK,EAAC,IAAI;EAAC,gBAAgB,EAAC,OAAO;EAAC,YAAY,EAAC,OAAO;;AAAC,8EAA0E;EAAC,KAAK,EAAC,IAAI;EAAC,gBAAgB,EAAC,OAAO;EAAC,YAAY,EAAC,OAAO;;AAAC,oSAAsR;EAAC,KAAK,EAAC,IAAI;EAAC,gBAAgB,EAAC,OAAO;EAAC,YAAY,EAAC,OAAO;;AAAC,8EAA0E;EAAC,gBAAgB,EAAC,IAAI;;AAAC,oSAA4R;EAAC,gBAAgB,EAAC,OAAO;EAAC,YAAY,EAAC,IAAI;;AAAC,mBAAmB;EAAC,KAAK,EAAC,OAAO;EAAC,gBAAgB,EAAC,IAAI;;AAAC,oHAAgH;EAAC,KAAK,EAAC,OAAO;EAAC,gBAAgB,EAAC,IAAI;EAAC,YAAY,EAAC,OAAO;;AAAC,WAAW;EAAC,KAAK,EAAC,IAAI;EAAC,gBAAgB,EAAC,OAAO;EAAC,YAAY,EAAC,IAAI;;AAAC,oCAAmC;EAAC,KAAK,EAAC,IAAI;EAAC,gBAAgB,EAAC,OAAO;EAAC,YAAY,EAAC,OAAO;;AAAC,iBAAiB;EAAC,KAAK,EAAC,IAAI;EAAC,gBAAgB,EAAC,OAAO;EAAC,YAAY,EAAC,OAAO;;AAAC,2EAAuE;EAAC,KAAK,EAAC,IAAI;EAAC,gBAAgB,EAAC,OAAO;EAAC,YAAY,EAAC,OAAO;;AAAC,2RAA6Q;EAAC,KAAK,EAAC,IAAI;EAAC,gBAAgB,EAAC,OAAO;EAAC,YAAY,EAAC,OAAO;;AAAC,2EAAuE;EAAC,gBAAgB,EAAC,IAAI;;AAAC,2RAAmR;EAAC,gBAAgB,EAAC,OAAO;EAAC,YAAY,EAAC,IAAI;;AAAC,kBAAkB;EAAC,KAAK,EAAC,OAAO;EAAC,gBAAgB,EAAC,IAAI;;AAAC,+GAA2G;EAAC,KAAK,EAAC,OAAO;EAAC,gBAAgB,EAAC,IAAI;EAAC,YAAY,EAAC,OAAO;;AAAC,SAAS;EAAC,KAAK,EAAC,OAAO;EAAC,WAAW,EAAC,MAAM;EAAC,aAAa,EAAC,CAAC;;AAAC,gGAA4F;EAAC,gBAAgB,EAAC,WAAW;EAAC,kBAAkB,EAAC,IAAI;EAAC,UAAU,EAAC,IAAI;;AAAC,6DAA0D;EAAC,YAAY,EAAC,WAAW;;AAAC,gCAA+B;EAAC,KAAK,EAAC,OAAO;EAAC,eAAe,EAAC,SAAS;EAAC,gBAAgB,EAAC,WAAW;;AAAC,4HAAyH;EAAC,KAAK,EAAC,IAAI;EAAC,eAAe,EAAC,IAAI;;AAAC,6BAA0B;EAAC,OAAO,EAAC,OAAO;EAAC,SAAS,EAAC,IAAI;EAAC,WAAW,EAAC,SAAS;EAAC,aAAa,EAAC,GAAG;;AAAC,6BAA0B;EAAC,OAAO,EAAC,OAAO;EAAC,SAAS,EAAC,IAAI;EAAC,WAAW,EAAC,GAAG;EAAC,aAAa,EAAC,GAAG;;AAAC,6BAA0B;EAAC,OAAO,EAAC,OAAO;EAAC,SAAS,EAAC,IAAI;EAAC,WAAW,EAAC,GAAG;EAAC,aAAa,EAAC,GAAG;;AAAC,UAAU;EAAC,OAAO,EAAC,KAAK;EAAC,KAAK,EAAC,IAAI;;AAAC,uBAAqB;EAAC,UAAU,EAAC,GAAG;;AAAC,6FAA2F;EAAC,KAAK,EAAC,IAAI;;AAAC,KAAK;EAAC,OAAO,EAAC,CAAC;EAAC,kBAAkB,EAAC,mBAAmB;EAAC,aAAa,EAAC,mBAAmB;EAAC,UAAU,EAAC,mBAAmB;;AAAC,QAAQ;EAAC,OAAO,EAAC,CAAC;;AAAC,SAAS;EAAC,OAAO,EAAC,IAAI;;AAAC,YAAY;EAAC,OAAO,EAAC,KAAK;;AAAC,cAAc;EAAC,OAAO,EAAC,SAAS;;AAAC,iBAAiB;EAAC,OAAO,EAAC,eAAe;;AAAC,WAAW;EAAC,QAAQ,EAAC,QAAQ;EAAC,MAAM,EAAC,CAAC;EAAC,QAAQ,EAAC,MAAM;EAAC,2BAA2B,EAAC,kBAAkB;EAAC,mBAAmB,EAAC,kBAAkB;EAAC,2BAA2B,EAAC,IAAI;EAAC,mBAAmB,EAAC,IAAI;EAAC,kCAAkC,EAAC,IAAI;EAAC,0BAA0B,EAAC,IAAI;;AAAC,MAAM;EAAC,OAAO,EAAC,YAAY;EAAC,KAAK,EAAC,CAAC;EAAC,MAAM,EAAC,CAAC;EAAC,WAAW,EAAC,GAAG;EAAC,cAAc,EAAC,MAAM;EAAC,UAAU,EAAC,UAAU;EAAC,UAAU,EAAC,YAAY;EAAC,YAAY,EAAC,qBAAqB;EAAC,WAAW,EAAC,qBAAqB;;AAAC,kBAAiB;EAAC,QAAQ,EAAC,QAAQ;;AAAC,sBAAsB;EAAC,OAAO,EAAC,CAAC;;AAAC,cAAc;EAAC,QAAQ,EAAC,QAAQ;EAAC,GAAG,EAAC,IAAI;EAAC,IAAI,EAAC,CAAC;EAAC,OAAO,EAAC,IAAI;EAAC,OAAO,EAAC,IAAI;EAAC,KAAK,EAAC,IAAI;EAAC,SAAS,EAAC,KAAK;EAAC,OAAO,EAAC,KAAK;EAAC,MAAM,EAAC,OAAO;EAAC,UAAU,EAAC,IAAI;EAAC,SAAS,EAAC,IAAI;EAAC,UAAU,EAAC,IAAI;EAAC,gBAAgB,EAAC,IAAI;EAAC,MAAM,EAAC,cAAc;EAAC,MAAM,EAAC,6BAA0B;EAAC,aAAa,EAAC,GAAG;EAAC,kBAAkB,EAAC,+BAA4B;EAAC,UAAU,EAAC,+BAA4B;EAAC,eAAe,EAAC,WAAW;;AAAC,yBAAyB;EAAC,KAAK,EAAC,CAAC;EAAC,IAAI,EAAC,IAAI;;AAAC,uBAAuB;EAAC,MAAM,EAAC,GAAG;EAAC,MAAM,EAAC,KAAK;EAAC,QAAQ,EAAC,MAAM;EAAC,gBAAgB,EAAC,OAAO;;AAAC,uBAAmB;EAAC,OAAO,EAAC,KAAK;EAAC,OAAO,EAAC,QAAQ;EAAC,KAAK,EAAC,IAAI;EAAC,WAAW,EAAC,MAAM;EAAC,WAAW,EAAC,UAAU;EAAC,KAAK,EAAC,IAAI;EAAC,WAAW,EAAC,MAAM;;AAAC,4DAAmD;EAAC,eAAe,EAAC,IAAI;EAAC,KAAK,EAAC,OAAO;EAAC,gBAAgB,EAAC,OAAO;;AAAC,oGAAsF;EAAC,KAAK,EAAC,IAAI;EAAC,eAAe,EAAC,IAAI;EAAC,OAAO,EAAC,CAAC;EAAC,gBAAgB,EAAC,OAAO;;AAAC,0GAA4F;EAAC,KAAK,EAAC,IAAI;;AAAC,0EAAiE;EAAC,eAAe,EAAC,IAAI;EAAC,gBAAgB,EAAC,WAAW;EAAC,gBAAgB,EAAC,IAAI;EAAC,MAAM,EAAC,2DAA2D;EAAC,MAAM,EAAC,WAAW;;AAAC,sBAAoB;EAAC,OAAO,EAAC,KAAK;;AAAC,SAAO;EAAC,OAAO,EAAC,CAAC;;AAAC,oBAAoB;EAAC,IAAI,EAAC,IAAI;EAAC,KAAK,EAAC,CAAC;;AAAC,mBAAmB;EAAC,IAAI,EAAC,CAAC;EAAC,KAAK,EAAC,IAAI;;AAAC,gBAAgB;EAAC,OAAO,EAAC,KAAK;EAAC,OAAO,EAAC,QAAQ;EAAC,SAAS,EAAC,IAAI;EAAC,WAAW,EAAC,UAAU;EAAC,KAAK,EAAC,IAAI;EAAC,WAAW,EAAC,MAAM;;AAAC,kBAAkB;EAAC,QAAQ,EAAC,KAAK;EAAC,IAAI,EAAC,CAAC;EAAC,KAAK,EAAC,CAAC;EAAC,MAAM,EAAC,CAAC;EAAC,GAAG,EAAC,CAAC;EAAC,OAAO,EAAC,GAAG;;AAAC,4BAA0B;EAAC,KAAK,EAAC,CAAC;EAAC,IAAI,EAAC,IAAI;;AAAC,qDAAoD;EAAC,UAAU,EAAC,CAAC;EAAC,aAAa,EAAC,UAAU;EAAC,aAAa,EAAC,YAAY;EAAC,OAAO,EAAC,EAAE;;AAAC,qEAAoE;EAAC,GAAG,EAAC,IAAI;EAAC,MAAM,EAAC,IAAI;EAAC,aAAa,EAAC,GAAG;;AAAC,yBAAwB;EAAC,4BAA4B;IAAC,IAAI,EAAC,IAAI;IAAC,KAAK,EAAC,CAAC;;EAAC,iCAAiC;IAAC,IAAI,EAAC,CAAC;IAAC,KAAK,EAAC,IAAI;AAAE,+BAA8B;EAAC,QAAQ,EAAC,QAAQ;EAAC,OAAO,EAAC,YAAY;EAAC,cAAc,EAAC,MAAM;;AAAC,6CAAwC;EAAC,QAAQ,EAAC,QAAQ;EAAC,KAAK,EAAC,IAAI;;AAAC,8OAAuN;EAAC,OAAO,EAAC,CAAC;;AAAC,sHAA2G;EAAC,WAAW,EAAC,IAAI;;AAAC,YAAY;EAAC,WAAW,EAAC,IAAI;;AAAC,qEAAmE;EAAC,KAAK,EAAC,IAAI;;AAAC,2EAAmE;EAAC,WAAW,EAAC,GAAG;;AAAC,0EAAwE;EAAC,aAAa,EAAC,CAAC;;AAAC,6BAA2B;EAAC,WAAW,EAAC,CAAC;;AAAC,oEAAkE;EAAC,0BAA0B,EAAC,CAAC;EAAC,uBAAuB,EAAC,CAAC;;AAAC,+FAA0F;EAAC,yBAAyB,EAAC,CAAC;EAAC,sBAAsB,EAAC,CAAC;;AAAC,uBAAqB;EAAC,KAAK,EAAC,IAAI;;AAAC,iEAA6D;EAAC,aAAa,EAAC,CAAC;;AAAC,+IAAsI;EAAC,0BAA0B,EAAC,CAAC;EAAC,uBAAuB,EAAC,CAAC;;AAAC,uEAAmE;EAAC,yBAAyB,EAAC,CAAC;EAAC,sBAAsB,EAAC,CAAC;;AAAC,oEAAmE;EAAC,OAAO,EAAC,CAAC;;AAAC,oCAAgC;EAAC,YAAY,EAAC,GAAG;EAAC,aAAa,EAAC,GAAG;;AAAC,uCAAmC;EAAC,YAAY,EAAC,IAAI;EAAC,aAAa,EAAC,IAAI;;AAAC,gCAAgC;EAAC,kBAAkB,EAAC,oCAAiC;EAAC,UAAU,EAAC,oCAAiC;;AAAC,yCAAyC;EAAC,kBAAkB,EAAC,IAAI;EAAC,UAAU,EAAC,IAAI;;AAAC,WAAW;EAAC,WAAW,EAAC,CAAC;;AAAC,cAAc;EAAC,YAAY,EAAC,SAAS;EAAC,mBAAmB,EAAC,CAAC;;AAAC,sBAAsB;EAAC,YAAY,EAAC,SAAS;;AAAC,qGAA2F;EAAC,OAAO,EAAC,KAAK;EAAC,KAAK,EAAC,IAAI;EAAC,KAAK,EAAC,IAAI;EAAC,SAAS,EAAC,IAAI;;AAAC,uCAAmC;EAAC,KAAK,EAAC,IAAI;;AAAC,kKAA+I;EAAC,UAAU,EAAC,IAAI;EAAC,WAAW,EAAC,CAAC;;AAAC,6DAA2D;EAAC,aAAa,EAAC,CAAC;;AAAC,uDAAqD;EAAC,uBAAuB,EAAC,GAAG;EAAC,sBAAsB,EAAC,GAAG;EAAC,0BAA0B,EAAC,CAAC;EAAC,yBAAyB,EAAC,CAAC;;AAAC,uDAAqD;EAAC,uBAAuB,EAAC,CAAC;EAAC,sBAAsB,EAAC,CAAC;EAAC,0BAA0B,EAAC,GAAG;EAAC,yBAAyB,EAAC,GAAG;;AAAC,0EAAsE;EAAC,aAAa,EAAC,CAAC;;AAAC,iKAAwJ;EAAC,0BAA0B,EAAC,CAAC;EAAC,yBAAyB,EAAC,CAAC;;AAAC,gFAA4E;EAAC,uBAAuB,EAAC,CAAC;EAAC,sBAAsB,EAAC,CAAC;;AAAC,oBAAoB;EAAC,OAAO,EAAC,KAAK;EAAC,KAAK,EAAC,IAAI;EAAC,YAAY,EAAC,KAAK;EAAC,eAAe,EAAC,QAAQ;;AAAC,8DAAyD;EAAC,KAAK,EAAC,IAAI;EAAC,OAAO,EAAC,UAAU;EAAC,KAAK,EAAC,EAAE;;AAAC,sCAAoC;EAAC,KAAK,EAAC,IAAI;;AAAC,gDAA8C;EAAC,IAAI,EAAC,IAAI;;AAAC,8OAA+N;EAAC,QAAQ,EAAC,QAAQ;EAAC,IAAI,EAAC,gBAAgB;EAAC,cAAc,EAAC,IAAI;;AAAC,YAAY;EAAC,QAAQ,EAAC,QAAQ;EAAC,OAAO,EAAC,KAAK;EAAC,eAAe,EAAC,QAAQ;;AAAC,2BAA2B;EAAC,KAAK,EAAC,IAAI;EAAC,YAAY,EAAC,CAAC;EAAC,aAAa,EAAC,CAAC;;AAAC,0BAA0B;EAAC,QAAQ,EAAC,QAAQ;EAAC,OAAO,EAAC,CAAC;EAAC,KAAK,EAAC,IAAI;EAAC,KAAK,EAAC,IAAI;EAAC,aAAa,EAAC,CAAC;;AAAC,gCAAgC;EAAC,OAAO,EAAC,CAAC;;AAAC,gHAAsG;EAAC,MAAM,EAAC,IAAI;EAAC,OAAO,EAAC,OAAO;EAAC,SAAS,EAAC,IAAI;EAAC,WAAW,EAAC,SAAS;EAAC,aAAa,EAAC,GAAG;;AAAC,kIAAwH;EAAC,MAAM,EAAC,IAAI;EAAC,WAAW,EAAC,IAAI;;AAAC,0SAAqR;EAAC,MAAM,EAAC,IAAI;;AAAC,gHAAsG;EAAC,MAAM,EAAC,IAAI;EAAC,OAAO,EAAC,OAAO;EAAC,SAAS,EAAC,IAAI;EAAC,WAAW,EAAC,GAAG;EAAC,aAAa,EAAC,GAAG;;AAAC,kIAAwH;EAAC,MAAM,EAAC,IAAI;EAAC,WAAW,EAAC,IAAI;;AAAC,0SAAqR;EAAC,MAAM,EAAC,IAAI;;AAAC,gEAA8D;EAAC,OAAO,EAAC,UAAU;;AAAC,yKAAuK;EAAC,aAAa,EAAC,CAAC;;AAAC,oCAAmC;EAAC,KAAK,EAAC,EAAE;EAAC,WAAW,EAAC,MAAM;EAAC,cAAc,EAAC,MAAM;;AAAC,kBAAkB;EAAC,OAAO,EAAC,OAAO;EAAC,SAAS,EAAC,IAAI;EAAC,WAAW,EAAC,MAAM;EAAC,WAAW,EAAC,CAAC;EAAC,KAAK,EAAC,IAAI;EAAC,UAAU,EAAC,MAAM;EAAC,gBAAgB,EAAC,IAAI;EAAC,MAAM,EAAC,cAAc;EAAC,aAAa,EAAC,GAAG;;AAAC,2BAA2B;EAAC,OAAO,EAAC,OAAO;EAAC,SAAS,EAAC,IAAI;EAAC,aAAa,EAAC,GAAG;;AAAC,2BAA2B;EAAC,OAAO,EAAC,OAAO;EAAC,SAAS,EAAC,IAAI;EAAC,aAAa,EAAC,GAAG;;AAAC,iFAAgF;EAAC,UAAU,EAAC,CAAC;;AAAC,2VAAuU;EAAC,0BAA0B,EAAC,CAAC;EAAC,uBAAuB,EAAC,CAAC;;AAAC,8BAA8B;EAAC,YAAY,EAAC,CAAC;;AAAC,oUAAgT;EAAC,yBAAyB,EAAC,CAAC;EAAC,sBAAsB,EAAC,CAAC;;AAAC,6BAA6B;EAAC,WAAW,EAAC,CAAC;;AAAC,gBAAgB;EAAC,QAAQ,EAAC,QAAQ;EAAC,SAAS,EAAC,CAAC;EAAC,WAAW,EAAC,MAAM;;AAAC,uBAAqB;EAAC,QAAQ,EAAC,QAAQ;;AAAC,8BAA0B;EAAC,WAAW,EAAC,IAAI;;AAAC,4FAAoF;EAAC,OAAO,EAAC,CAAC;;AAAC,8EAAyE;EAAC,YAAY,EAAC,IAAI;;AAAC,4EAAuE;EAAC,OAAO,EAAC,CAAC;EAAC,WAAW,EAAC,IAAI;;AAAC,IAAI;EAAC,aAAa,EAAC,CAAC;EAAC,YAAY,EAAC,CAAC;EAAC,UAAU,EAAC,IAAI;;AAAC,SAAO;EAAC,QAAQ,EAAC,QAAQ;EAAC,OAAO,EAAC,KAAK;;AAAC,aAAS;EAAC,QAAQ,EAAC,QAAQ;EAAC,OAAO,EAAC,KAAK;EAAC,OAAO,EAAC,GAAG;;AAAC,wCAA+B;EAAC,eAAe,EAAC,IAAI;EAAC,gBAAgB,EAAC,IAAI;;AAAC,sBAAkB;EAAC,KAAK,EAAC,IAAI;;AAAC,0DAAiD;EAAC,KAAK,EAAC,IAAI;EAAC,eAAe,EAAC,IAAI;EAAC,gBAAgB,EAAC,WAAW;EAAC,MAAM,EAAC,WAAW;;AAAC,0DAAkD;EAAC,gBAAgB,EAAC,IAAI;EAAC,YAAY,EAAC,OAAO;;AAAC,iBAAiB;EAAC,MAAM,EAAC,GAAG;EAAC,MAAM,EAAC,KAAK;EAAC,QAAQ,EAAC,MAAM;EAAC,gBAAgB,EAAC,OAAO;;AAAC,mBAAa;EAAC,SAAS,EAAC,IAAI;;AAAC,SAAS;EAAC,aAAa,EAAC,cAAc;;AAAC,cAAY;EAAC,KAAK,EAAC,IAAI;EAAC,aAAa,EAAC,IAAI;;AAAC,kBAAc;EAAC,YAAY,EAAC,GAAG;EAAC,WAAW,EAAC,UAAU;EAAC,MAAM,EAAC,qBAAqB;EAAC,aAAa,EAAC,WAAW;;AAAC,wBAAoB;EAAC,YAAY,EAAC,cAAc;;AAAC,2FAA6E;EAAC,KAAK,EAAC,IAAI;EAAC,gBAAgB,EAAC,IAAI;EAAC,MAAM,EAAC,cAAc;EAAC,mBAAmB,EAAC,WAAW;EAAC,MAAM,EAAC,OAAO;;AAAC,uBAAuB;EAAC,KAAK,EAAC,IAAI;EAAC,aAAa,EAAC,CAAC;;AAAC,4BAA0B;EAAC,KAAK,EAAC,IAAI;;AAAC,gCAA4B;EAAC,UAAU,EAAC,MAAM;EAAC,aAAa,EAAC,GAAG;;AAAC,kDAAgD;EAAC,GAAG,EAAC,IAAI;EAAC,IAAI,EAAC,IAAI;;AAAC,yBAAwB;EAAC,4BAA0B;IAAC,OAAO,EAAC,UAAU;IAAC,KAAK,EAAC,EAAE;;EAAC,gCAA4B;IAAC,aAAa,EAAC,CAAC;AAAE,gCAA4B;EAAC,YAAY,EAAC,CAAC;EAAC,aAAa,EAAC,GAAG;;AAAC,+HAAiH;EAAC,MAAM,EAAC,cAAc;;AAAC,yBAAwB;EAAC,gCAA4B;IAAC,aAAa,EAAC,cAAc;IAAC,aAAa,EAAC,WAAW;;EAAC,+HAAiH;IAAC,mBAAmB,EAAC,IAAI;AAAE,eAAa;EAAC,KAAK,EAAC,IAAI;;AAAC,mBAAe;EAAC,aAAa,EAAC,GAAG;;AAAC,oBAAgB;EAAC,WAAW,EAAC,GAAG;;AAAC,8FAAgF;EAAC,KAAK,EAAC,IAAI;EAAC,gBAAgB,EAAC,OAAO;;AAAC,iBAAe;EAAC,KAAK,EAAC,IAAI;;AAAC,sBAAkB;EAAC,UAAU,EAAC,GAAG;EAAC,WAAW,EAAC,CAAC;;AAAC,cAAc;EAAC,KAAK,EAAC,IAAI;;AAAC,mBAAiB;EAAC,KAAK,EAAC,IAAI;;AAAC,uBAAmB;EAAC,UAAU,EAAC,MAAM;EAAC,aAAa,EAAC,GAAG;;AAAC,yCAAuC;EAAC,GAAG,EAAC,IAAI;EAAC,IAAI,EAAC,IAAI;;AAAC,yBAAwB;EAAC,mBAAiB;IAAC,OAAO,EAAC,UAAU;IAAC,KAAK,EAAC,EAAE;;EAAC,uBAAmB;IAAC,aAAa,EAAC,CAAC;AAAE,mBAAmB;EAAC,aAAa,EAAC,CAAC;;AAAC,4BAAwB;EAAC,YAAY,EAAC,CAAC;EAAC,aAAa,EAAC,GAAG;;AAAC,mHAAqG;EAAC,MAAM,EAAC,cAAc;;AAAC,yBAAwB;EAAC,4BAAwB;IAAC,aAAa,EAAC,cAAc;IAAC,aAAa,EAAC,WAAW;;EAAC,mHAAqG;IAAC,mBAAmB,EAAC,IAAI;AAAE,wBAAsB;EAAC,OAAO,EAAC,IAAI;;AAAC,sBAAoB;EAAC,OAAO,EAAC,KAAK;;AAAC,wBAAwB;EAAC,UAAU,EAAC,IAAI;EAAC,uBAAuB,EAAC,CAAC;EAAC,sBAAsB,EAAC,CAAC;;AAAC,OAAO;EAAC,QAAQ,EAAC,QAAQ;EAAC,UAAU,EAAC,IAAI;EAAC,aAAa,EAAC,CAAC;EAAC,MAAM,EAAC,qBAAqB;;AAAC,yBAAwB;EAAC,OAAO;IAAC,aAAa,EAAC,GAAG;AAAE,yBAAwB;EAAC,cAAc;IAAC,KAAK,EAAC,IAAI;AAAE,gBAAgB;EAAC,UAAU,EAAC,OAAO;EAAC,aAAa,EAAC,GAAG;EAAC,YAAY,EAAC,GAAG;EAAC,UAAU,EAAC,qBAAqB;EAAC,UAAU,EAAC,sCAAmC;EAAC,0BAA0B,EAAC,KAAK;;AAAC,mBAAmB;EAAC,UAAU,EAAC,IAAI;;AAAC,yBAAwB;EAAC,gBAAgB;IAAC,KAAK,EAAC,IAAI;IAAC,UAAU,EAAC,CAAC;IAAC,UAAU,EAAC,IAAI;;EAAC,yBAAyB;IAAC,OAAO,EAAC,gBAAgB;IAAC,MAAM,EAAC,eAAe;IAAC,cAAc,EAAC,CAAC;IAAC,QAAQ,EAAC,kBAAkB;;EAAC,mBAAmB;IAAC,UAAU,EAAC,OAAO;;EAAC,8GAA4G;IAAC,YAAY,EAAC,CAAC;IAAC,aAAa,EAAC,CAAC;AAAE,yEAAwE;EAAC,UAAU,EAAC,KAAK;;AAAC,6DAA2D;EAAC,yEAAwE;IAAC,UAAU,EAAC,KAAK;AAAE,kIAAuH;EAAC,YAAY,EAAC,IAAI;EAAC,WAAW,EAAC,IAAI;;AAAC,yBAAwB;EAAC,kIAAuH;IAAC,YAAY,EAAC,CAAC;IAAC,WAAW,EAAC,CAAC;AAAE,kBAAkB;EAAC,OAAO,EAAC,IAAI;EAAC,YAAY,EAAC,OAAO;;AAAC,yBAAwB;EAAC,kBAAkB;IAAC,aAAa,EAAC,CAAC;AAAE,uCAAsC;EAAC,QAAQ,EAAC,KAAK;EAAC,KAAK,EAAC,CAAC;EAAC,IAAI,EAAC,CAAC;EAAC,OAAO,EAAC,IAAI;;AAAC,yBAAwB;EAAC,uCAAsC;IAAC,aAAa,EAAC,CAAC;AAAE,iBAAiB;EAAC,GAAG,EAAC,CAAC;EAAC,YAAY,EAAC,OAAO;;AAAC,oBAAoB;EAAC,MAAM,EAAC,CAAC;EAAC,aAAa,EAAC,CAAC;EAAC,YAAY,EAAC,OAAO;;AAAC,aAAa;EAAC,KAAK,EAAC,IAAI;EAAC,OAAO,EAAC,UAAU;EAAC,SAAS,EAAC,IAAI;EAAC,WAAW,EAAC,IAAI;EAAC,MAAM,EAAC,IAAI;;AAAC,wCAAuC;EAAC,eAAe,EAAC,IAAI;;AAAC,mBAAiB;EAAC,OAAO,EAAC,KAAK;;AAAC,yBAAwB;EAAC,4EAAuE;IAAC,WAAW,EAAC,IAAI;AAAE,cAAc;EAAC,QAAQ,EAAC,QAAQ;EAAC,KAAK,EAAC,KAAK;EAAC,YAAY,EAAC,GAAG;EAAC,OAAO,EAAC,QAAQ;EAAC,UAAU,EAAC,MAAM;EAAC,aAAa,EAAC,MAAM;EAAC,gBAAgB,EAAC,WAAW;EAAC,gBAAgB,EAAC,IAAI;EAAC,MAAM,EAAC,qBAAqB;EAAC,aAAa,EAAC,GAAG;;AAAC,oBAAoB;EAAC,OAAO,EAAC,CAAC;;AAAC,wBAAwB;EAAC,OAAO,EAAC,KAAK;EAAC,KAAK,EAAC,IAAI;EAAC,MAAM,EAAC,GAAG;EAAC,aAAa,EAAC,GAAG;;AAAC,oCAAkC;EAAC,UAAU,EAAC,GAAG;;AAAC,yBAAwB;EAAC,cAAc;IAAC,OAAO,EAAC,IAAI;AAAE,WAAW;EAAC,MAAM,EAAC,YAAY;;AAAC,oBAAgB;EAAC,WAAW,EAAC,IAAI;EAAC,cAAc,EAAC,IAAI;EAAC,WAAW,EAAC,IAAI;;AAAC,yBAAwB;EAAC,gCAAgC;IAAC,QAAQ,EAAC,MAAM;IAAC,KAAK,EAAC,IAAI;IAAC,KAAK,EAAC,IAAI;IAAC,UAAU,EAAC,CAAC;IAAC,gBAAgB,EAAC,WAAW;IAAC,MAAM,EAAC,CAAC;IAAC,UAAU,EAAC,IAAI;;EAAC,4FAAuF;IAAC,OAAO,EAAC,iBAAiB;;EAAC,yCAAqC;IAAC,WAAW,EAAC,IAAI;;EAAC,gGAAuF;IAAC,gBAAgB,EAAC,IAAI;AAAE,yBAAwB;EAAC,WAAW;IAAC,KAAK,EAAC,IAAI;IAAC,MAAM,EAAC,CAAC;;EAAC,gBAAc;IAAC,KAAK,EAAC,IAAI;;EAAC,oBAAgB;IAAC,WAAW,EAAC,MAAM;IAAC,cAAc,EAAC,MAAM;AAAE,YAAY;EAAC,WAAW,EAAC,IAAI;EAAC,YAAY,EAAC,IAAI;EAAC,OAAO,EAAC,QAAQ;EAAC,UAAU,EAAC,qBAAqB;EAAC,aAAa,EAAC,qBAAqB;EAAC,kBAAkB,EAAC,wEAAiE;EAAC,UAAU,EAAC,wEAAiE;EAAC,UAAU,EAAC,MAAM;EAAC,aAAa,EAAC,MAAM;;AAAC,yBAAwB;EAAC,wBAAwB;IAAC,OAAO,EAAC,YAAY;IAAC,aAAa,EAAC,CAAC;IAAC,cAAc,EAAC,MAAM;;EAAC,0BAA0B;IAAC,OAAO,EAAC,YAAY;IAAC,KAAK,EAAC,IAAI;IAAC,cAAc,EAAC,MAAM;;EAAC,iCAAiC;IAAC,OAAO,EAAC,YAAY;;EAAC,yBAAyB;IAAC,OAAO,EAAC,YAAY;IAAC,cAAc,EAAC,MAAM;;EAAC,iIAA+H;IAAC,KAAK,EAAC,IAAI;;EAAC,yCAAuC;IAAC,KAAK,EAAC,IAAI;;EAAC,2BAA2B;IAAC,aAAa,EAAC,CAAC;IAAC,cAAc,EAAC,MAAM;;EAAC,2CAA0C;IAAC,OAAO,EAAC,YAAY;IAAC,UAAU,EAAC,CAAC;IAAC,aAAa,EAAC,CAAC;IAAC,cAAc,EAAC,MAAM;;EAAC,uDAAsD;IAAC,YAAY,EAAC,CAAC;;EAAC,sFAAqF;IAAC,QAAQ,EAAC,QAAQ;IAAC,WAAW,EAAC,CAAC;;EAAC,iDAAiD;IAAC,GAAG,EAAC,CAAC;AAAE,yBAAwB;EAAC,wBAAwB;IAAC,aAAa,EAAC,GAAG;;EAAC,mCAAmC;IAAC,aAAa,EAAC,CAAC;AAAE,yBAAwB;EAAC,YAAY;IAAC,KAAK,EAAC,IAAI;IAAC,MAAM,EAAC,CAAC;IAAC,WAAW,EAAC,CAAC;IAAC,YAAY,EAAC,CAAC;IAAC,WAAW,EAAC,CAAC;IAAC,cAAc,EAAC,CAAC;IAAC,kBAAkB,EAAC,IAAI;IAAC,UAAU,EAAC,IAAI;AAAE,iCAA6B;EAAC,UAAU,EAAC,CAAC;EAAC,uBAAuB,EAAC,CAAC;EAAC,sBAAsB,EAAC,CAAC;;AAAC,sDAAkD;EAAC,aAAa,EAAC,CAAC;EAAC,uBAAuB,EAAC,GAAG;EAAC,sBAAsB,EAAC,GAAG;EAAC,0BAA0B,EAAC,CAAC;EAAC,yBAAyB,EAAC,CAAC;;AAAC,WAAW;EAAC,UAAU,EAAC,MAAM;EAAC,aAAa,EAAC,MAAM;;AAAC,kBAAkB;EAAC,UAAU,EAAC,MAAM;EAAC,aAAa,EAAC,MAAM;;AAAC,kBAAkB;EAAC,UAAU,EAAC,MAAM;EAAC,aAAa,EAAC,MAAM;;AAAC,YAAY;EAAC,UAAU,EAAC,MAAM;EAAC,aAAa,EAAC,MAAM;;AAAC,yBAAwB;EAAC,YAAY;IAAC,KAAK,EAAC,IAAI;IAAC,WAAW,EAAC,GAAG;IAAC,YAAY,EAAC,GAAG;AAAE,yBAAwB;EAAC,YAAY;IAAC,KAAK,EAAC,eAAe;IAAC,KAAK,EAAC,IAAI;;EAAC,aAAa;IAAC,KAAK,EAAC,gBAAgB;IAAC,KAAK,EAAC,KAAK;IAAC,YAAY,EAAC,IAAI;;EAAC,6BAA2B;IAAC,YAAY,EAAC,CAAC;AAAE,eAAe;EAAC,gBAAgB,EAAC,OAAO;EAAC,YAAY,EAAC,OAAO;;AAAC,6BAA6B;EAAC,KAAK,EAAC,IAAI;;AAAC,wEAAuE;EAAC,KAAK,EAAC,OAAO;EAAC,gBAAgB,EAAC,WAAW;;AAAC,4BAA4B;EAAC,KAAK,EAAC,IAAI;;AAAC,oCAAgC;EAAC,KAAK,EAAC,IAAI;;AAAC,sFAA6E;EAAC,KAAK,EAAC,OAAO;EAAC,gBAAgB,EAAC,IAAI;;AAAC,2IAA6H;EAAC,KAAK,EAAC,OAAO;EAAC,gBAAgB,EAAC,IAAI;;AAAC,iJAAmI;EAAC,KAAK,EAAC,IAAI;EAAC,gBAAgB,EAAC,OAAO;;AAAC,8BAA8B;EAAC,YAAY,EAAC,IAAI;;AAAC,0EAAyE;EAAC,gBAAgB,EAAC,IAAI;;AAAC,wCAAwC;EAAC,gBAAgB,EAAC,IAAI;;AAAC,8DAA6D;EAAC,YAAY,EAAC,OAAO;;AAAC,qIAAuH;EAAC,gBAAgB,EAAC,IAAI;EAAC,KAAK,EAAC,OAAO;;AAAC,yBAAwB;EAAC,yDAAqD;IAAC,KAAK,EAAC,IAAI;;EAAC,gIAAuH;IAAC,KAAK,EAAC,OAAO;IAAC,gBAAgB,EAAC,IAAI;;EAAC,0MAA4L;IAAC,KAAK,EAAC,OAAO;IAAC,gBAAgB,EAAC,IAAI;;EAAC,gNAAkM;IAAC,KAAK,EAAC,IAAI;IAAC,gBAAgB,EAAC,OAAO;AAAE,4BAA4B;EAAC,KAAK,EAAC,IAAI;;AAAC,kCAAkC;EAAC,KAAK,EAAC,OAAO;;AAAC,yBAAyB;EAAC,KAAK,EAAC,IAAI;;AAAC,gEAA+D;EAAC,KAAK,EAAC,OAAO;;AAAC,4LAAyL;EAAC,KAAK,EAAC,IAAI;;AAAC,eAAe;EAAC,gBAAgB,EAAC,IAAI;EAAC,YAAY,EAAC,OAAO;;AAAC,6BAA6B;EAAC,KAAK,EAAC,OAAO;;AAAC,wEAAuE;EAAC,KAAK,EAAC,IAAI;EAAC,gBAAgB,EAAC,WAAW;;AAAC,4BAA4B;EAAC,KAAK,EAAC,OAAO;;AAAC,oCAAgC;EAAC,KAAK,EAAC,OAAO;;AAAC,sFAA6E;EAAC,KAAK,EAAC,IAAI;EAAC,gBAAgB,EAAC,WAAW;;AAAC,2IAA6H;EAAC,KAAK,EAAC,IAAI;EAAC,gBAAgB,EAAC,OAAO;;AAAC,iJAAmI;EAAC,KAAK,EAAC,IAAI;EAAC,gBAAgB,EAAC,WAAW;;AAAC,8BAA8B;EAAC,YAAY,EAAC,IAAI;;AAAC,0EAAyE;EAAC,gBAAgB,EAAC,IAAI;;AAAC,wCAAwC;EAAC,gBAAgB,EAAC,IAAI;;AAAC,8DAA6D;EAAC,YAAY,EAAC,OAAO;;AAAC,qIAAuH;EAAC,gBAAgB,EAAC,OAAO;EAAC,KAAK,EAAC,IAAI;;AAAC,yBAAwB;EAAC,mEAAiE;IAAC,YAAY,EAAC,OAAO;;EAAC,yDAAyD;IAAC,gBAAgB,EAAC,OAAO;;EAAC,yDAAqD;IAAC,KAAK,EAAC,OAAO;;EAAC,gIAAuH;IAAC,KAAK,EAAC,IAAI;IAAC,gBAAgB,EAAC,WAAW;;EAAC,0MAA4L;IAAC,KAAK,EAAC,IAAI;IAAC,gBAAgB,EAAC,OAAO;;EAAC,gNAAkM;IAAC,KAAK,EAAC,IAAI;IAAC,gBAAgB,EAAC,WAAW;AAAE,4BAA4B;EAAC,KAAK,EAAC,OAAO;;AAAC,kCAAkC;EAAC,KAAK,EAAC,IAAI;;AAAC,yBAAyB;EAAC,KAAK,EAAC,OAAO;;AAAC,gEAA+D;EAAC,KAAK,EAAC,IAAI;;AAAC,4LAAyL;EAAC,KAAK,EAAC,IAAI;;AAAC,WAAW;EAAC,OAAO,EAAC,QAAQ;EAAC,aAAa,EAAC,IAAI;EAAC,UAAU,EAAC,IAAI;EAAC,gBAAgB,EAAC,IAAI;EAAC,aAAa,EAAC,GAAG;;AAAC,gBAAc;EAAC,OAAO,EAAC,YAAY;;AAAC,4BAAwB;EAAC,OAAO,EAAC,QAAQ;EAAC,OAAO,EAAC,KAAK;EAAC,KAAK,EAAC,IAAI;;AAAC,qBAAmB;EAAC,KAAK,EAAC,IAAI;;AAAC,WAAW;EAAC,OAAO,EAAC,YAAY;EAAC,YAAY,EAAC,CAAC;EAAC,MAAM,EAAC,MAAM;EAAC,aAAa,EAAC,GAAG;;AAAC,gBAAc;EAAC,OAAO,EAAC,MAAM;;AAAC,6CAAoC;EAAC,QAAQ,EAAC,QAAQ;EAAC,KAAK,EAAC,IAAI;EAAC,OAAO,EAAC,OAAO;EAAC,WAAW,EAAC,UAAU;EAAC,eAAe,EAAC,IAAI;EAAC,KAAK,EAAC,OAAO;EAAC,gBAAgB,EAAC,IAAI;EAAC,MAAM,EAAC,cAAc;EAAC,WAAW,EAAC,IAAI;;AAAC,qEAA4D;EAAC,WAAW,EAAC,CAAC;EAAC,yBAAyB,EAAC,GAAG;EAAC,sBAAsB,EAAC,GAAG;;AAAC,mEAA0D;EAAC,0BAA0B,EAAC,GAAG;EAAC,uBAAuB,EAAC,GAAG;;AAAC,oHAAiG;EAAC,OAAO,EAAC,CAAC;EAAC,KAAK,EAAC,OAAO;EAAC,gBAAgB,EAAC,IAAI;EAAC,YAAY,EAAC,IAAI;;AAAC,iMAAoK;EAAC,OAAO,EAAC,CAAC;EAAC,KAAK,EAAC,IAAI;EAAC,gBAAgB,EAAC,OAAO;EAAC,YAAY,EAAC,OAAO;EAAC,MAAM,EAAC,OAAO;;AAAC,6MAAgL;EAAC,KAAK,EAAC,IAAI;EAAC,gBAAgB,EAAC,IAAI;EAAC,YAAY,EAAC,IAAI;EAAC,MAAM,EAAC,WAAW;;AAAC,mDAA0C;EAAC,OAAO,EAAC,OAAO;EAAC,SAAS,EAAC,IAAI;EAAC,WAAW,EAAC,SAAS;;AAAC,2EAAkE;EAAC,yBAAyB,EAAC,GAAG;EAAC,sBAAsB,EAAC,GAAG;;AAAC,yEAAgE;EAAC,0BAA0B,EAAC,GAAG;EAAC,uBAAuB,EAAC,GAAG;;AAAC,mDAA0C;EAAC,OAAO,EAAC,OAAO;EAAC,SAAS,EAAC,IAAI;EAAC,WAAW,EAAC,GAAG;;AAAC,2EAAkE;EAAC,yBAAyB,EAAC,GAAG;EAAC,sBAAsB,EAAC,GAAG;;AAAC,yEAAgE;EAAC,0BAA0B,EAAC,GAAG;EAAC,uBAAuB,EAAC,GAAG;;AAAC,MAAM;EAAC,YAAY,EAAC,CAAC;EAAC,MAAM,EAAC,MAAM;EAAC,UAAU,EAAC,IAAI;EAAC,UAAU,EAAC,MAAM;;AAAC,SAAS;EAAC,OAAO,EAAC,MAAM;;AAAC,+BAA0B;EAAC,OAAO,EAAC,YAAY;EAAC,OAAO,EAAC,QAAQ;EAAC,gBAAgB,EAAC,IAAI;EAAC,MAAM,EAAC,cAAc;EAAC,aAAa,EAAC,IAAI;;AAAC,wCAAmC;EAAC,eAAe,EAAC,IAAI;EAAC,gBAAgB,EAAC,IAAI;;AAAC,qCAAgC;EAAC,KAAK,EAAC,KAAK;;AAAC,6CAAwC;EAAC,KAAK,EAAC,IAAI;;AAAC,qGAA0F;EAAC,KAAK,EAAC,IAAI;EAAC,gBAAgB,EAAC,IAAI;EAAC,MAAM,EAAC,WAAW;;AAAC,yCAAuC;EAAC,OAAO,EAAC,MAAM;EAAC,OAAO,EAAC,cAAc;EAAC,SAAS,EAAC,GAAG;EAAC,WAAW,EAAC,IAAI;EAAC,WAAW,EAAC,CAAC;EAAC,KAAK,EAAC,IAAI;EAAC,UAAU,EAAC,MAAM;EAAC,WAAW,EAAC,MAAM;EAAC,cAAc,EAAC,QAAQ;EAAC,aAAa,EAAC,KAAK;;AAAC,4BAA2B;EAAC,KAAK,EAAC,IAAI;EAAC,eAAe,EAAC,IAAI;EAAC,MAAM,EAAC,OAAO;;AAAC,YAAY;EAAC,OAAO,EAAC,IAAI;;AAAC,WAAW;EAAC,QAAQ,EAAC,QAAQ;EAAC,GAAG,EAAC,IAAI;;AAAC,cAAc;EAAC,gBAAgB,EAAC,IAAI;;AAAC,sDAAqD;EAAC,gBAAgB,EAAC,OAAO;;AAAC,cAAc;EAAC,gBAAgB,EAAC,OAAO;;AAAC,sDAAqD;EAAC,gBAAgB,EAAC,OAAO;;AAAC,cAAc;EAAC,gBAAgB,EAAC,OAAO;;AAAC,sDAAqD;EAAC,gBAAgB,EAAC,OAAO;;AAAC,8CAA4C;EAAC,gBAAgB,EAAC,OAAO;;AAAC,gDAA+C;EAAC,gBAAgB,EAAC,OAAO;;AAAC,cAAc;EAAC,gBAAgB,EAAC,OAAO;;AAAC,sDAAqD;EAAC,gBAAgB,EAAC,OAAO;;AAAC,aAAa;EAAC,gBAAgB,EAAC,OAAO;;AAAC,oDAAmD;EAAC,gBAAgB,EAAC,OAAO;;AAAC,MAAM;EAAC,OAAO,EAAC,YAAY;EAAC,SAAS,EAAC,IAAI;EAAC,OAAO,EAAC,OAAO;EAAC,SAAS,EAAC,IAAI;EAAC,WAAW,EAAC,IAAI;EAAC,KAAK,EAAC,IAAI;EAAC,WAAW,EAAC,CAAC;EAAC,cAAc,EAAC,MAAM;EAAC,WAAW,EAAC,MAAM;EAAC,UAAU,EAAC,MAAM;EAAC,gBAAgB,EAAC,IAAI;EAAC,aAAa,EAAC,IAAI;;AAAC,YAAY;EAAC,OAAO,EAAC,IAAI;;AAAC,WAAW;EAAC,QAAQ,EAAC,QAAQ;EAAC,GAAG,EAAC,IAAI;;AAAC,2CAAwC;EAAC,GAAG,EAAC,CAAC;EAAC,OAAO,EAAC,OAAO;;AAAC,4BAA2B;EAAC,KAAK,EAAC,IAAI;EAAC,eAAe,EAAC,IAAI;EAAC,MAAM,EAAC,OAAO;;AAAC,mEAA0D;EAAC,KAAK,EAAC,OAAO;EAAC,gBAAgB,EAAC,IAAI;;AAAC,yBAAuB;EAAC,KAAK,EAAC,KAAK;;AAAC,kCAA8B;EAAC,YAAY,EAAC,GAAG;;AAAC,4BAAsB;EAAC,WAAW,EAAC,GAAG;;AAAC,UAAU;EAAC,WAAW,EAAC,IAAI;EAAC,cAAc,EAAC,IAAI;EAAC,aAAa,EAAC,IAAI;EAAC,KAAK,EAAC,OAAO;EAAC,gBAAgB,EAAC,IAAI;;AAAC,6BAA4B;EAAC,KAAK,EAAC,OAAO;;AAAC,YAAY;EAAC,aAAa,EAAC,IAAI;EAAC,SAAS,EAAC,IAAI;EAAC,WAAW,EAAC,GAAG;;AAAC,eAAa;EAAC,gBAAgB,EAAC,OAAO;;AAAC,kDAAiD;EAAC,aAAa,EAAC,GAAG;EAAC,YAAY,EAAC,GAAG;EAAC,aAAa,EAAC,GAAG;;AAAC,qBAAqB;EAAC,SAAS,EAAC,IAAI;;AAAC,oCAAmC;EAAC,UAAU;IAAC,WAAW,EAAC,IAAI;IAAC,cAAc,EAAC,IAAI;;EAAC,kDAAiD;IAAC,YAAY,EAAC,IAAI;IAAC,aAAa,EAAC,IAAI;;EAAC,6BAA4B;IAAC,SAAS,EAAC,IAAI;AAAE,UAAU;EAAC,OAAO,EAAC,KAAK;EAAC,OAAO,EAAC,GAAG;EAAC,aAAa,EAAC,IAAI;EAAC,WAAW,EAAC,UAAU;EAAC,gBAAgB,EAAC,IAAI;EAAC,MAAM,EAAC,cAAc;EAAC,aAAa,EAAC,GAAG;EAAC,kBAAkB,EAAC,sBAAsB;EAAC,aAAa,EAAC,sBAAsB;EAAC,UAAU,EAAC,sBAAsB;;AAAC,oCAA+B;EAAC,WAAW,EAAC,IAAI;EAAC,YAAY,EAAC,IAAI;;AAAC,wDAAsD;EAAC,YAAY,EAAC,OAAO;;AAAC,mBAAmB;EAAC,OAAO,EAAC,GAAG;EAAC,KAAK,EAAC,IAAI;;AAAC,MAAM;EAAC,OAAO,EAAC,IAAI;EAAC,aAAa,EAAC,IAAI;EAAC,MAAM,EAAC,qBAAqB;EAAC,aAAa,EAAC,GAAG;;AAAC,SAAS;EAAC,UAAU,EAAC,CAAC;EAAC,KAAK,EAAC,OAAO;;AAAC,kBAAkB;EAAC,WAAW,EAAC,IAAI;;AAAC,uBAAkB;EAAC,aAAa,EAAC,CAAC;;AAAC,cAAU;EAAC,UAAU,EAAC,GAAG;;AAAC,sCAAqC;EAAC,aAAa,EAAC,IAAI;;AAAC,oDAAmD;EAAC,QAAQ,EAAC,QAAQ;EAAC,GAAG,EAAC,IAAI;EAAC,KAAK,EAAC,KAAK;EAAC,KAAK,EAAC,OAAO;;AAAC,cAAc;EAAC,gBAAgB,EAAC,OAAO;EAAC,YAAY,EAAC,OAAO;EAAC,KAAK,EAAC,OAAO;;AAAC,iBAAiB;EAAC,gBAAgB,EAAC,OAAO;;AAAC,0BAA0B;EAAC,KAAK,EAAC,OAAO;;AAAC,WAAW;EAAC,gBAAgB,EAAC,OAAO;EAAC,YAAY,EAAC,OAAO;EAAC,KAAK,EAAC,OAAO;;AAAC,cAAc;EAAC,gBAAgB,EAAC,OAAO;;AAAC,uBAAuB;EAAC,KAAK,EAAC,OAAO;;AAAC,cAAc;EAAC,gBAAgB,EAAC,OAAO;EAAC,YAAY,EAAC,OAAO;EAAC,KAAK,EAAC,OAAO;;AAAC,iBAAiB;EAAC,gBAAgB,EAAC,OAAO;;AAAC,0BAA0B;EAAC,KAAK,EAAC,OAAO;;AAAC,aAAa;EAAC,gBAAgB,EAAC,OAAO;EAAC,YAAY,EAAC,OAAO;EAAC,KAAK,EAAC,OAAO;;AAAC,gBAAgB;EAAC,gBAAgB,EAAC,OAAO;;AAAC,yBAAyB;EAAC,KAAK,EAAC,OAAO;;AAAC,uCAAoG;EAA5D,IAAI;IAAC,mBAAmB,EAAC,MAAM;EAAC,EAAE;IAAC,mBAAmB,EAAC,GAAG;AAAE,+BAA4F;EAA5D,IAAI;IAAC,mBAAmB,EAAC,MAAM;EAAC,EAAE;IAAC,mBAAmB,EAAC,GAAG;AAAE,SAAS;EAAC,QAAQ,EAAC,MAAM;EAAC,MAAM,EAAC,IAAI;EAAC,aAAa,EAAC,IAAI;EAAC,gBAAgB,EAAC,OAAO;EAAC,aAAa,EAAC,GAAG;EAAC,kBAAkB,EAAC,kCAA+B;EAAC,UAAU,EAAC,kCAA+B;;AAAC,aAAa;EAAC,KAAK,EAAC,IAAI;EAAC,KAAK,EAAC,CAAC;EAAC,MAAM,EAAC,IAAI;EAAC,SAAS,EAAC,IAAI;EAAC,WAAW,EAAC,IAAI;EAAC,KAAK,EAAC,IAAI;EAAC,UAAU,EAAC,MAAM;EAAC,gBAAgB,EAAC,OAAO;EAAC,kBAAkB,EAAC,kCAA+B;EAAC,UAAU,EAAC,kCAA+B;EAAC,kBAAkB,EAAC,cAAc;EAAC,aAAa,EAAC,cAAc;EAAC,UAAU,EAAC,cAAc;;AAAC,sDAAqD;EAAC,gBAAgB,EAAC,2LAAkL;EAAC,gBAAgB,EAAC,sLAA6K;EAAC,gBAAgB,EAAC,mLAA0K;EAAC,eAAe,EAAC,SAAS;;AAAC,oDAAmD;EAAC,iBAAiB,EAAC,uCAAuC;EAAC,YAAY,EAAC,uCAAuC;EAAC,SAAS,EAAC,uCAAuC;;AAAC,qBAAqB;EAAC,gBAAgB,EAAC,OAAO;;AAAC,uCAAuC;EAAC,gBAAgB,EAAC,2LAAkL;EAAC,gBAAgB,EAAC,sLAA6K;EAAC,gBAAgB,EAAC,mLAA0K;;AAAC,kBAAkB;EAAC,gBAAgB,EAAC,OAAO;;AAAC,oCAAoC;EAAC,gBAAgB,EAAC,2LAAkL;EAAC,gBAAgB,EAAC,sLAA6K;EAAC,gBAAgB,EAAC,mLAA0K;;AAAC,qBAAqB;EAAC,gBAAgB,EAAC,OAAO;;AAAC,uCAAuC;EAAC,gBAAgB,EAAC,2LAAkL;EAAC,gBAAgB,EAAC,sLAA6K;EAAC,gBAAgB,EAAC,mLAA0K;;AAAC,oBAAoB;EAAC,gBAAgB,EAAC,OAAO;;AAAC,sCAAsC;EAAC,gBAAgB,EAAC,2LAAkL;EAAC,gBAAgB,EAAC,sLAA6K;EAAC,gBAAgB,EAAC,mLAA0K;;AAAC,MAAM;EAAC,UAAU,EAAC,IAAI;;AAAC,kBAAkB;EAAC,UAAU,EAAC,CAAC;;AAAC,mBAAkB;EAAC,IAAI,EAAC,CAAC;EAAC,QAAQ,EAAC,MAAM;;AAAC,WAAW;EAAC,KAAK,EAAC,OAAO;;AAAC,aAAa;EAAC,OAAO,EAAC,KAAK;;AAAC,2BAA2B;EAAC,SAAS,EAAC,IAAI;;AAAC,kCAA+B;EAAC,YAAY,EAAC,IAAI;;AAAC,gCAA6B;EAAC,aAAa,EAAC,IAAI;;AAAC,sCAAoC;EAAC,OAAO,EAAC,UAAU;EAAC,cAAc,EAAC,GAAG;;AAAC,aAAa;EAAC,cAAc,EAAC,MAAM;;AAAC,aAAa;EAAC,cAAc,EAAC,MAAM;;AAAC,cAAc;EAAC,UAAU,EAAC,CAAC;EAAC,aAAa,EAAC,GAAG;;AAAC,WAAW;EAAC,YAAY,EAAC,CAAC;EAAC,UAAU,EAAC,IAAI;;AAAC,WAAW;EAAC,aAAa,EAAC,IAAI;EAAC,YAAY,EAAC,CAAC;;AAAC,+EAA6E;EAAC,QAAQ,EAAC,QAAQ;EAAC,OAAO,EAAC,KAAK;EAAC,OAAO,EAAC,SAAS;EAAC,aAAa,EAAC,IAAI;EAAC,gBAAgB,EAAC,IAAI;EAAC,MAAM,EAAC,cAAc;;AAAC,4BAA4B;EAAC,uBAAuB,EAAC,GAAG;EAAC,sBAAsB,EAAC,GAAG;;AAAC,2BAA2B;EAAC,aAAa,EAAC,CAAC;EAAC,0BAA0B,EAAC,GAAG;EAAC,yBAAyB,EAAC,GAAG;;AAAC,yCAAwC;EAAC,KAAK,EAAC,IAAI;;AAAC,2FAA0F;EAAC,KAAK,EAAC,IAAI;;AAAC,4GAAyG;EAAC,eAAe,EAAC,IAAI;EAAC,KAAK,EAAC,IAAI;EAAC,gBAAgB,EAAC,OAAO;;AAAC,sBAAsB;EAAC,KAAK,EAAC,IAAI;EAAC,UAAU,EAAC,IAAI;;AAAC,2FAAyF;EAAC,gBAAgB,EAAC,IAAI;EAAC,KAAK,EAAC,IAAI;EAAC,MAAM,EAAC,WAAW;;AAAC,sKAAoK;EAAC,KAAK,EAAC,OAAO;;AAAC,6JAA2J;EAAC,KAAK,EAAC,IAAI;;AAAC,qFAAmF;EAAC,OAAO,EAAC,CAAC;EAAC,KAAK,EAAC,IAAI;EAAC,gBAAgB,EAAC,OAAO;EAAC,YAAY,EAAC,OAAO;;AAAC,uhBAAmgB;EAAC,KAAK,EAAC,OAAO;;AAAC,uJAAqJ;EAAC,KAAK,EAAC,OAAO;;AAAC,wBAAwB;EAAC,KAAK,EAAC,OAAO;EAAC,gBAAgB,EAAC,OAAO;;AAAC,yDAAwD;EAAC,KAAK,EAAC,OAAO;;AAAC,2GAA0G;EAAC,KAAK,EAAC,OAAO;;AAAC,4IAAyI;EAAC,KAAK,EAAC,OAAO;EAAC,gBAAgB,EAAC,OAAO;;AAAC,iPAA4O;EAAC,KAAK,EAAC,IAAI;EAAC,gBAAgB,EAAC,OAAO;EAAC,YAAY,EAAC,OAAO;;AAAC,qBAAqB;EAAC,KAAK,EAAC,OAAO;EAAC,gBAAgB,EAAC,OAAO;;AAAC,mDAAkD;EAAC,KAAK,EAAC,OAAO;;AAAC,qGAAoG;EAAC,KAAK,EAAC,OAAO;;AAAC,gIAA6H;EAAC,KAAK,EAAC,OAAO;EAAC,gBAAgB,EAAC,OAAO;;AAAC,+NAA0N;EAAC,KAAK,EAAC,IAAI;EAAC,gBAAgB,EAAC,OAAO;EAAC,YAAY,EAAC,OAAO;;AAAC,wBAAwB;EAAC,KAAK,EAAC,OAAO;EAAC,gBAAgB,EAAC,OAAO;;AAAC,yDAAwD;EAAC,KAAK,EAAC,OAAO;;AAAC,2GAA0G;EAAC,KAAK,EAAC,OAAO;;AAAC,4IAAyI;EAAC,KAAK,EAAC,OAAO;EAAC,gBAAgB,EAAC,OAAO;;AAAC,iPAA4O;EAAC,KAAK,EAAC,IAAI;EAAC,gBAAgB,EAAC,OAAO;EAAC,YAAY,EAAC,OAAO;;AAAC,uBAAuB;EAAC,KAAK,EAAC,OAAO;EAAC,gBAAgB,EAAC,OAAO;;AAAC,uDAAsD;EAAC,KAAK,EAAC,OAAO;;AAAC,yGAAwG;EAAC,KAAK,EAAC,OAAO;;AAAC,wIAAqI;EAAC,KAAK,EAAC,OAAO;EAAC,gBAAgB,EAAC,OAAO;;AAAC,2OAAsO;EAAC,KAAK,EAAC,IAAI;EAAC,gBAAgB,EAAC,OAAO;EAAC,YAAY,EAAC,OAAO;;AAAC,wBAAwB;EAAC,UAAU,EAAC,CAAC;EAAC,aAAa,EAAC,GAAG;;AAAC,qBAAqB;EAAC,aAAa,EAAC,CAAC;EAAC,WAAW,EAAC,GAAG;;AAAC,MAAM;EAAC,aAAa,EAAC,IAAI;EAAC,gBAAgB,EAAC,IAAI;EAAC,MAAM,EAAC,qBAAqB;EAAC,aAAa,EAAC,GAAG;EAAC,kBAAkB,EAAC,6BAA0B;EAAC,UAAU,EAAC,6BAA0B;;AAAC,WAAW;EAAC,OAAO,EAAC,GAAG;;AAAC,cAAc;EAAC,OAAO,EAAC,SAAS;EAAC,aAAa,EAAC,qBAAqB;EAAC,uBAAuB,EAAC,GAAG;EAAC,sBAAsB,EAAC,GAAG;;AAAC,2CAAyC;EAAC,KAAK,EAAC,OAAO;;AAAC,YAAY;EAAC,UAAU,EAAC,CAAC;EAAC,aAAa,EAAC,CAAC;EAAC,SAAS,EAAC,IAAI;EAAC,KAAK,EAAC,OAAO;;AAAC,kHAAgG;EAAC,KAAK,EAAC,OAAO;;AAAC,aAAa;EAAC,OAAO,EAAC,SAAS;EAAC,gBAAgB,EAAC,OAAO;EAAC,UAAU,EAAC,cAAc;EAAC,0BAA0B,EAAC,GAAG;EAAC,yBAAyB,EAAC,GAAG;;AAAC,4DAAqD;EAAC,aAAa,EAAC,CAAC;;AAAC,8FAAuF;EAAC,YAAY,EAAC,KAAK;EAAC,aAAa,EAAC,CAAC;;AAAC,8IAAuI;EAAC,UAAU,EAAC,CAAC;EAAC,uBAAuB,EAAC,GAAG;EAAC,sBAAsB,EAAC,GAAG;;AAAC,0IAAmI;EAAC,aAAa,EAAC,CAAC;EAAC,0BAA0B,EAAC,GAAG;EAAC,yBAAyB,EAAC,GAAG;;AAAC,oFAA8E;EAAC,uBAAuB,EAAC,CAAC;EAAC,sBAAsB,EAAC,CAAC;;AAAC,yDAAuD;EAAC,gBAAgB,EAAC,CAAC;;AAAC,2BAAyB;EAAC,gBAAgB,EAAC,CAAC;;AAAC,uFAA2E;EAAC,aAAa,EAAC,CAAC;;AAAC,+GAAmG;EAAC,YAAY,EAAC,GAAG;EAAC,aAAa,EAAC,GAAG;;AAAC,wFAAiF;EAAC,uBAAuB,EAAC,GAAG;EAAC,sBAAsB,EAAC,GAAG;;AAAC,sUAAuS;EAAC,sBAAsB,EAAC,GAAG;EAAC,uBAAuB,EAAC,GAAG;;AAAC,swBAAusB;EAAC,sBAAsB,EAAC,GAAG;;AAAC,8vBAA+rB;EAAC,uBAAuB,EAAC,GAAG;;AAAC,qFAA8E;EAAC,0BAA0B,EAAC,GAAG;EAAC,yBAAyB,EAAC,GAAG;;AAAC,wTAAyR;EAAC,yBAAyB,EAAC,GAAG;EAAC,0BAA0B,EAAC,GAAG;;AAAC,0uBAA2qB;EAAC,yBAAyB,EAAC,GAAG;;AAAC,kuBAAmqB;EAAC,0BAA0B,EAAC,GAAG;;AAAC,gJAA6H;EAAC,UAAU,EAAC,cAAc;;AAAC,gHAAmG;EAAC,UAAU,EAAC,CAAC;;AAAC,sEAA+D;EAAC,MAAM,EAAC,CAAC;;AAAC,sxBAA+pB;EAAC,WAAW,EAAC,CAAC;;AAAC,0wBAAmpB;EAAC,YAAY,EAAC,CAAC;;AAAC,8gBAA+b;EAAC,aAAa,EAAC,CAAC;;AAAC,sgBAAub;EAAC,aAAa,EAAC,CAAC;;AAAC,0BAAwB;EAAC,MAAM,EAAC,CAAC;EAAC,aAAa,EAAC,CAAC;;AAAC,YAAY;EAAC,aAAa,EAAC,IAAI;;AAAC,mBAAmB;EAAC,aAAa,EAAC,CAAC;EAAC,aAAa,EAAC,GAAG;;AAAC,4BAA0B;EAAC,UAAU,EAAC,GAAG;;AAAC,2BAA2B;EAAC,aAAa,EAAC,CAAC;;AAAC,wHAA+G;EAAC,UAAU,EAAC,cAAc;;AAAC,0BAA0B;EAAC,UAAU,EAAC,CAAC;;AAAC,wDAAsD;EAAC,aAAa,EAAC,cAAc;;AAAC,cAAc;EAAC,YAAY,EAAC,IAAI;;AAAC,+BAA6B;EAAC,KAAK,EAAC,IAAI;EAAC,gBAAgB,EAAC,OAAO;EAAC,YAAY,EAAC,IAAI;;AAAC,+DAAyD;EAAC,gBAAgB,EAAC,IAAI;;AAAC,sCAAoC;EAAC,KAAK,EAAC,OAAO;EAAC,gBAAgB,EAAC,IAAI;;AAAC,8DAAwD;EAAC,mBAAmB,EAAC,IAAI;;AAAC,cAAc;EAAC,YAAY,EAAC,OAAO;;AAAC,+BAA6B;EAAC,KAAK,EAAC,IAAI;EAAC,gBAAgB,EAAC,OAAO;EAAC,YAAY,EAAC,OAAO;;AAAC,+DAAyD;EAAC,gBAAgB,EAAC,OAAO;;AAAC,sCAAoC;EAAC,KAAK,EAAC,OAAO;EAAC,gBAAgB,EAAC,IAAI;;AAAC,8DAAwD;EAAC,mBAAmB,EAAC,OAAO;;AAAC,cAAc;EAAC,YAAY,EAAC,OAAO;;AAAC,+BAA6B;EAAC,KAAK,EAAC,OAAO;EAAC,gBAAgB,EAAC,OAAO;EAAC,YAAY,EAAC,OAAO;;AAAC,+DAAyD;EAAC,gBAAgB,EAAC,OAAO;;AAAC,sCAAoC;EAAC,KAAK,EAAC,OAAO;EAAC,gBAAgB,EAAC,OAAO;;AAAC,8DAAwD;EAAC,mBAAmB,EAAC,OAAO;;AAAC,WAAW;EAAC,YAAY,EAAC,OAAO;;AAAC,4BAA0B;EAAC,KAAK,EAAC,OAAO;EAAC,gBAAgB,EAAC,OAAO;EAAC,YAAY,EAAC,OAAO;;AAAC,4DAAsD;EAAC,gBAAgB,EAAC,OAAO;;AAAC,mCAAiC;EAAC,KAAK,EAAC,OAAO;EAAC,gBAAgB,EAAC,OAAO;;AAAC,2DAAqD;EAAC,mBAAmB,EAAC,OAAO;;AAAC,cAAc;EAAC,YAAY,EAAC,OAAO;;AAAC,+BAA6B;EAAC,KAAK,EAAC,OAAO;EAAC,gBAAgB,EAAC,OAAO;EAAC,YAAY,EAAC,OAAO;;AAAC,+DAAyD;EAAC,gBAAgB,EAAC,OAAO;;AAAC,sCAAoC;EAAC,KAAK,EAAC,OAAO;EAAC,gBAAgB,EAAC,OAAO;;AAAC,8DAAwD;EAAC,mBAAmB,EAAC,OAAO;;AAAC,aAAa;EAAC,YAAY,EAAC,OAAO;;AAAC,8BAA4B;EAAC,KAAK,EAAC,OAAO;EAAC,gBAAgB,EAAC,OAAO;EAAC,YAAY,EAAC,OAAO;;AAAC,8DAAwD;EAAC,gBAAgB,EAAC,OAAO;;AAAC,qCAAmC;EAAC,KAAK,EAAC,OAAO;EAAC,gBAAgB,EAAC,OAAO;;AAAC,6DAAuD;EAAC,mBAAmB,EAAC,OAAO;;AAAC,iBAAiB;EAAC,QAAQ,EAAC,QAAQ;EAAC,OAAO,EAAC,KAAK;EAAC,MAAM,EAAC,CAAC;EAAC,OAAO,EAAC,CAAC;EAAC,QAAQ,EAAC,MAAM;;AAAC,8IAA0I;EAAC,QAAQ,EAAC,QAAQ;EAAC,GAAG,EAAC,CAAC;EAAC,IAAI,EAAC,CAAC;EAAC,MAAM,EAAC,CAAC;EAAC,MAAM,EAAC,IAAI;EAAC,KAAK,EAAC,IAAI;EAAC,MAAM,EAAC,CAAC;;AAAC,uBAAuB;EAAC,cAAc,EAAC,MAAM;;AAAC,sBAAsB;EAAC,cAAc,EAAC,GAAG;;AAAC,KAAK;EAAC,UAAU,EAAC,IAAI;EAAC,OAAO,EAAC,IAAI;EAAC,aAAa,EAAC,IAAI;EAAC,gBAAgB,EAAC,OAAO;EAAC,MAAM,EAAC,iBAAiB;EAAC,aAAa,EAAC,GAAG;EAAC,kBAAkB,EAAC,mCAAgC;EAAC,UAAU,EAAC,mCAAgC;;AAAC,gBAAgB;EAAC,YAAY,EAAC,IAAI;EAAC,YAAY,EAAC,mBAAgB;;AAAC,QAAQ;EAAC,OAAO,EAAC,IAAI;EAAC,aAAa,EAAC,GAAG;;AAAC,QAAQ;EAAC,OAAO,EAAC,GAAG;EAAC,aAAa,EAAC,GAAG;;AAAC,2BAA0B;EAAC,KAAK,EAAC,KAAK;EAAC,SAAS,EAAC,MAAM;EAAC,WAAW,EAAC,IAAI;EAAC,WAAW,EAAC,CAAC;EAAC,KAAK,EAAC,IAAI;EAAC,WAAW,EAAC,YAAY;EAAC,OAAO,EAAC,EAAE;EAAC,MAAM,EAAC,iBAAiB;;AAAC,0BAAyB;EAAC,KAAK,EAAC,IAAI;EAAC,eAAe,EAAC,IAAI;EAAC,MAAM,EAAC,OAAO;EAAC,OAAO,EAAC,EAAE;EAAC,MAAM,EAAC,iBAAiB;;AAAC,YAAY;EAAC,OAAO,EAAC,CAAC;EAAC,MAAM,EAAC,OAAO;EAAC,UAAU,EAAC,WAAW;EAAC,MAAM,EAAC,CAAC;EAAC,kBAAkB,EAAC,IAAI;;AAAC,WAAW;EAAC,QAAQ,EAAC,MAAM;;AAAC,MAAM;EAAC,OAAO,EAAC,IAAI;EAAC,QAAQ,EAAC,MAAM;EAAC,QAAQ,EAAC,KAAK;EAAC,GAAG,EAAC,CAAC;EAAC,KAAK,EAAC,CAAC;EAAC,MAAM,EAAC,CAAC;EAAC,IAAI,EAAC,CAAC;EAAC,OAAO,EAAC,IAAI;EAAC,0BAA0B,EAAC,KAAK;EAAC,OAAO,EAAC,CAAC;;AAAC,yBAAyB;EAAC,iBAAiB,EAAC,kBAAkB;EAAC,aAAa,EAAC,kBAAkB;EAAC,YAAY,EAAC,kBAAkB;EAAC,SAAS,EAAC,kBAAkB;EAAC,kBAAkB,EAAC,+BAA+B;EAAC,eAAe,EAAC,4BAA4B;EAAC,aAAa,EAAC,0BAA0B;EAAC,UAAU,EAAC,uBAAuB;;AAAC,uBAAuB;EAAC,iBAAiB,EAAC,eAAe;EAAC,aAAa,EAAC,eAAe;EAAC,YAAY,EAAC,eAAe;EAAC,SAAS,EAAC,eAAe;;AAAC,kBAAkB;EAAC,UAAU,EAAC,MAAM;EAAC,UAAU,EAAC,IAAI;;AAAC,aAAa;EAAC,QAAQ,EAAC,QAAQ;EAAC,KAAK,EAAC,IAAI;EAAC,MAAM,EAAC,IAAI;;AAAC,cAAc;EAAC,QAAQ,EAAC,QAAQ;EAAC,gBAAgB,EAAC,IAAI;EAAC,MAAM,EAAC,cAAc;EAAC,MAAM,EAAC,4BAAyB;EAAC,aAAa,EAAC,GAAG;EAAC,kBAAkB,EAAC,4BAAyB;EAAC,UAAU,EAAC,4BAAyB;EAAC,eAAe,EAAC,WAAW;EAAC,OAAO,EAAC,CAAC;;AAAC,eAAe;EAAC,QAAQ,EAAC,KAAK;EAAC,GAAG,EAAC,CAAC;EAAC,KAAK,EAAC,CAAC;EAAC,MAAM,EAAC,CAAC;EAAC,IAAI,EAAC,CAAC;EAAC,OAAO,EAAC,IAAI;EAAC,gBAAgB,EAAC,IAAI;;AAAC,oBAAoB;EAAC,OAAO,EAAC,CAAC;EAAC,MAAM,EAAC,gBAAgB;;AAAC,kBAAkB;EAAC,OAAO,EAAC,EAAE;EAAC,MAAM,EAAC,iBAAiB;;AAAC,aAAa;EAAC,OAAO,EAAC,IAAI;EAAC,aAAa,EAAC,iBAAiB;;AAAC,oBAAoB;EAAC,UAAU,EAAC,IAAI;;AAAC,YAAY;EAAC,MAAM,EAAC,CAAC;EAAC,WAAW,EAAC,UAAU;;AAAC,WAAW;EAAC,QAAQ,EAAC,QAAQ;EAAC,OAAO,EAAC,IAAI;;AAAC,aAAa;EAAC,OAAO,EAAC,IAAI;EAAC,UAAU,EAAC,KAAK;EAAC,UAAU,EAAC,iBAAiB;;AAAC,yBAAuB;EAAC,WAAW,EAAC,GAAG;EAAC,aAAa,EAAC,CAAC;;AAAC,oCAAkC;EAAC,WAAW,EAAC,IAAI;;AAAC,qCAAmC;EAAC,WAAW,EAAC,CAAC;;AAAC,wBAAwB;EAAC,QAAQ,EAAC,QAAQ;EAAC,GAAG,EAAC,OAAO;EAAC,KAAK,EAAC,IAAI;EAAC,MAAM,EAAC,IAAI;EAAC,QAAQ,EAAC,MAAM;;AAAC,yBAAwB;EAAC,aAAa;IAAC,KAAK,EAAC,KAAK;IAAC,MAAM,EAAC,SAAS;;EAAC,cAAc;IAAC,kBAAkB,EAAC,6BAA0B;IAAC,UAAU,EAAC,6BAA0B;;EAAC,SAAS;IAAC,KAAK,EAAC,KAAK;AAAE,yBAAwB;EAAC,SAAS;IAAC,KAAK,EAAC,KAAK;AAAE,QAAQ;EAAC,QAAQ,EAAC,QAAQ;EAAC,OAAO,EAAC,IAAI;EAAC,OAAO,EAAC,KAAK;EAAC,WAAW,EAAC,2CAA2C;EAAC,UAAU,EAAC,MAAM;EAAC,WAAW,EAAC,MAAM;EAAC,cAAc,EAAC,MAAM;EAAC,UAAU,EAAC,IAAI;EAAC,WAAW,EAAC,UAAU;EAAC,UAAU,EAAC,IAAI;EAAC,UAAU,EAAC,KAAK;EAAC,eAAe,EAAC,IAAI;EAAC,WAAW,EAAC,IAAI;EAAC,cAAc,EAAC,IAAI;EAAC,WAAW,EAAC,MAAM;EAAC,UAAU,EAAC,MAAM;EAAC,YAAY,EAAC,MAAM;EAAC,SAAS,EAAC,MAAM;EAAC,SAAS,EAAC,IAAI;EAAC,OAAO,EAAC,CAAC;EAAC,MAAM,EAAC,gBAAgB;;AAAC,WAAW;EAAC,OAAO,EAAC,EAAE;EAAC,MAAM,EAAC,iBAAiB;;AAAC,YAAY;EAAC,UAAU,EAAC,IAAI;EAAC,OAAO,EAAC,KAAK;;AAAC,cAAc;EAAC,WAAW,EAAC,GAAG;EAAC,OAAO,EAAC,KAAK;;AAAC,eAAe;EAAC,UAAU,EAAC,GAAG;EAAC,OAAO,EAAC,KAAK;;AAAC,aAAa;EAAC,WAAW,EAAC,IAAI;EAAC,OAAO,EAAC,KAAK;;AAAC,cAAc;EAAC,SAAS,EAAC,KAAK;EAAC,OAAO,EAAC,OAAO;EAAC,KAAK,EAAC,IAAI;EAAC,UAAU,EAAC,MAAM;EAAC,gBAAgB,EAAC,IAAI;EAAC,aAAa,EAAC,GAAG;;AAAC,cAAc;EAAC,QAAQ,EAAC,QAAQ;EAAC,KAAK,EAAC,CAAC;EAAC,MAAM,EAAC,CAAC;EAAC,YAAY,EAAC,WAAW;EAAC,YAAY,EAAC,KAAK;;AAAC,2BAA2B;EAAC,MAAM,EAAC,CAAC;EAAC,IAAI,EAAC,GAAG;EAAC,WAAW,EAAC,IAAI;EAAC,YAAY,EAAC,SAAS;EAAC,gBAAgB,EAAC,IAAI;;AAAC,gCAAgC;EAAC,MAAM,EAAC,CAAC;EAAC,KAAK,EAAC,GAAG;EAAC,aAAa,EAAC,IAAI;EAAC,YAAY,EAAC,SAAS;EAAC,gBAAgB,EAAC,IAAI;;AAAC,iCAAiC;EAAC,MAAM,EAAC,CAAC;EAAC,IAAI,EAAC,GAAG;EAAC,aAAa,EAAC,IAAI;EAAC,YAAY,EAAC,SAAS;EAAC,gBAAgB,EAAC,IAAI;;AAAC,6BAA6B;EAAC,GAAG,EAAC,GAAG;EAAC,IAAI,EAAC,CAAC;EAAC,UAAU,EAAC,IAAI;EAAC,YAAY,EAAC,aAAa;EAAC,kBAAkB,EAAC,IAAI;;AAAC,4BAA4B;EAAC,GAAG,EAAC,GAAG;EAAC,KAAK,EAAC,CAAC;EAAC,UAAU,EAAC,IAAI;EAAC,YAAY,EAAC,aAAa;EAAC,iBAAiB,EAAC,IAAI;;AAAC,8BAA8B;EAAC,GAAG,EAAC,CAAC;EAAC,IAAI,EAAC,GAAG;EAAC,WAAW,EAAC,IAAI;EAAC,YAAY,EAAC,SAAS;EAAC,mBAAmB,EAAC,IAAI;;AAAC,mCAAmC;EAAC,GAAG,EAAC,CAAC;EAAC,KAAK,EAAC,GAAG;EAAC,UAAU,EAAC,IAAI;EAAC,YAAY,EAAC,SAAS;EAAC,mBAAmB,EAAC,IAAI;;AAAC,oCAAoC;EAAC,GAAG,EAAC,CAAC;EAAC,IAAI,EAAC,GAAG;EAAC,UAAU,EAAC,IAAI;EAAC,YAAY,EAAC,SAAS;EAAC,mBAAmB,EAAC,IAAI;;AAAC,QAAQ;EAAC,QAAQ,EAAC,QAAQ;EAAC,GAAG,EAAC,CAAC;EAAC,IAAI,EAAC,CAAC;EAAC,OAAO,EAAC,IAAI;EAAC,OAAO,EAAC,IAAI;EAAC,SAAS,EAAC,KAAK;EAAC,OAAO,EAAC,GAAG;EAAC,WAAW,EAAC,2CAA2C;EAAC,UAAU,EAAC,MAAM;EAAC,WAAW,EAAC,MAAM;EAAC,cAAc,EAAC,MAAM;EAAC,UAAU,EAAC,IAAI;EAAC,WAAW,EAAC,UAAU;EAAC,UAAU,EAAC,IAAI;EAAC,UAAU,EAAC,KAAK;EAAC,eAAe,EAAC,IAAI;EAAC,WAAW,EAAC,IAAI;EAAC,cAAc,EAAC,IAAI;EAAC,WAAW,EAAC,MAAM;EAAC,UAAU,EAAC,MAAM;EAAC,YAAY,EAAC,MAAM;EAAC,SAAS,EAAC,MAAM;EAAC,SAAS,EAAC,IAAI;EAAC,gBAAgB,EAAC,IAAI;EAAC,eAAe,EAAC,WAAW;EAAC,MAAM,EAAC,cAAc;EAAC,MAAM,EAAC,4BAAyB;EAAC,aAAa,EAAC,GAAG;EAAC,kBAAkB,EAAC,6BAA0B;EAAC,UAAU,EAAC,6BAA0B;;AAAC,YAAY;EAAC,UAAU,EAAC,KAAK;;AAAC,cAAc;EAAC,WAAW,EAAC,IAAI;;AAAC,eAAe;EAAC,UAAU,EAAC,IAAI;;AAAC,aAAa;EAAC,WAAW,EAAC,KAAK;;AAAC,cAAc;EAAC,MAAM,EAAC,CAAC;EAAC,OAAO,EAAC,QAAQ;EAAC,SAAS,EAAC,IAAI;EAAC,gBAAgB,EAAC,OAAO;EAAC,aAAa,EAAC,iBAAiB;EAAC,aAAa,EAAC,WAAW;;AAAC,gBAAgB;EAAC,OAAO,EAAC,QAAQ;;AAAC,0CAAqC;EAAC,QAAQ,EAAC,QAAQ;EAAC,OAAO,EAAC,KAAK;EAAC,KAAK,EAAC,CAAC;EAAC,MAAM,EAAC,CAAC;EAAC,YAAY,EAAC,WAAW;EAAC,YAAY,EAAC,KAAK;;AAAC,iBAAe;EAAC,YAAY,EAAC,IAAI;;AAAC,uBAAqB;EAAC,YAAY,EAAC,IAAI;EAAC,OAAO,EAAC,EAAE;;AAAC,qBAAmB;EAAC,IAAI,EAAC,GAAG;EAAC,WAAW,EAAC,KAAK;EAAC,mBAAmB,EAAC,CAAC;EAAC,gBAAgB,EAAC,IAAI;EAAC,gBAAgB,EAAC,mBAAgB;EAAC,MAAM,EAAC,KAAK;;AAAC,2BAAyB;EAAC,OAAO,EAAC,GAAG;EAAC,MAAM,EAAC,GAAG;EAAC,WAAW,EAAC,KAAK;EAAC,mBAAmB,EAAC,CAAC;EAAC,gBAAgB,EAAC,IAAI;;AAAC,uBAAqB;EAAC,GAAG,EAAC,GAAG;EAAC,IAAI,EAAC,KAAK;EAAC,UAAU,EAAC,KAAK;EAAC,iBAAiB,EAAC,CAAC;EAAC,kBAAkB,EAAC,IAAI;EAAC,kBAAkB,EAAC,mBAAgB;;AAAC,6BAA2B;EAAC,OAAO,EAAC,GAAG;EAAC,IAAI,EAAC,GAAG;EAAC,MAAM,EAAC,KAAK;EAAC,iBAAiB,EAAC,CAAC;EAAC,kBAAkB,EAAC,IAAI;;AAAC,wBAAsB;EAAC,IAAI,EAAC,GAAG;EAAC,WAAW,EAAC,KAAK;EAAC,gBAAgB,EAAC,CAAC;EAAC,mBAAmB,EAAC,IAAI;EAAC,mBAAmB,EAAC,mBAAgB;EAAC,GAAG,EAAC,KAAK;;AAAC,8BAA4B;EAAC,OAAO,EAAC,GAAG;EAAC,GAAG,EAAC,GAAG;EAAC,WAAW,EAAC,KAAK;EAAC,gBAAgB,EAAC,CAAC;EAAC,mBAAmB,EAAC,IAAI;;AAAC,sBAAoB;EAAC,GAAG,EAAC,GAAG;EAAC,KAAK,EAAC,KAAK;EAAC,UAAU,EAAC,KAAK;EAAC,kBAAkB,EAAC,CAAC;EAAC,iBAAiB,EAAC,IAAI;EAAC,iBAAiB,EAAC,mBAAgB;;AAAC,4BAA0B;EAAC,OAAO,EAAC,GAAG;EAAC,KAAK,EAAC,GAAG;EAAC,kBAAkB,EAAC,CAAC;EAAC,iBAAiB,EAAC,IAAI;EAAC,MAAM,EAAC,KAAK;;AAAC,SAAS;EAAC,QAAQ,EAAC,QAAQ;;AAAC,eAAe;EAAC,QAAQ,EAAC,QAAQ;EAAC,QAAQ,EAAC,MAAM;EAAC,KAAK,EAAC,IAAI;;AAAC,uBAAqB;EAAC,OAAO,EAAC,IAAI;EAAC,QAAQ,EAAC,QAAQ;EAAC,kBAAkB,EAAC,oBAAoB;EAAC,aAAa,EAAC,oBAAoB;EAAC,UAAU,EAAC,oBAAoB;;AAAC,gEAAqD;EAAC,WAAW,EAAC,CAAC;;AAAC,qDAAoD;EAAC,uBAAqB;IAAC,kBAAkB,EAAC,kCAAkC;IAAC,eAAe,EAAC,+BAA+B;IAAC,aAAa,EAAC,6BAA6B;IAAC,UAAU,EAAC,0BAA0B;IAAC,2BAA2B,EAAC,MAAM;IAAC,wBAAwB,EAAC,MAAM;IAAC,mBAAmB,EAAC,MAAM;IAAC,mBAAmB,EAAC,MAAM;IAAC,gBAAgB,EAAC,MAAM;IAAC,WAAW,EAAC,MAAM;;EAAC,kEAA6D;IAAC,iBAAiB,EAAC,uBAAuB;IAAC,SAAS,EAAC,uBAAuB;IAAC,IAAI,EAAC,CAAC;;EAAC,iEAA4D;IAAC,iBAAiB,EAAC,wBAAwB;IAAC,SAAS,EAAC,wBAAwB;IAAC,IAAI,EAAC,CAAC;;EAAC,qGAA6F;IAAC,iBAAiB,EAAC,oBAAoB;IAAC,SAAS,EAAC,oBAAoB;IAAC,IAAI,EAAC,CAAC;AAAE,2EAAmE;EAAC,OAAO,EAAC,KAAK;;AAAC,yBAAuB;EAAC,IAAI,EAAC,CAAC;;AAAC,gDAA2C;EAAC,QAAQ,EAAC,QAAQ;EAAC,GAAG,EAAC,CAAC;EAAC,KAAK,EAAC,IAAI;;AAAC,uBAAqB;EAAC,IAAI,EAAC,IAAI;;AAAC,uBAAqB;EAAC,IAAI,EAAC,KAAK;;AAAC,2DAAsD;EAAC,IAAI,EAAC,CAAC;;AAAC,8BAA4B;EAAC,IAAI,EAAC,KAAK;;AAAC,+BAA6B;EAAC,IAAI,EAAC,IAAI;;AAAC,iBAAiB;EAAC,QAAQ,EAAC,QAAQ;EAAC,GAAG,EAAC,CAAC;EAAC,IAAI,EAAC,CAAC;EAAC,MAAM,EAAC,CAAC;EAAC,KAAK,EAAC,GAAG;EAAC,OAAO,EAAC,EAAE;EAAC,MAAM,EAAC,iBAAiB;EAAC,SAAS,EAAC,IAAI;EAAC,KAAK,EAAC,IAAI;EAAC,UAAU,EAAC,MAAM;EAAC,WAAW,EAAC,4BAAyB;EAAC,gBAAgB,EAAC,WAAa;;AAAC,sBAAsB;EAAC,gBAAgB,EAAC,+EAAyE;EAAC,gBAAgB,EAAC,0EAAoE;EAAC,gBAAgB,EAAC,2EAAqE;EAAC,iBAAiB,EAAC,QAAQ;EAAC,MAAM,EAAC,8GAA8G;;AAAC,uBAAuB;EAAC,IAAI,EAAC,IAAI;EAAC,KAAK,EAAC,CAAC;EAAC,gBAAgB,EAAC,+EAAyE;EAAC,gBAAgB,EAAC,0EAAoE;EAAC,gBAAgB,EAAC,2EAAqE;EAAC,iBAAiB,EAAC,QAAQ;EAAC,MAAM,EAAC,8GAA8G;;AAAC,gDAA+C;EAAC,OAAO,EAAC,CAAC;EAAC,KAAK,EAAC,IAAI;EAAC,eAAe,EAAC,IAAI;EAAC,OAAO,EAAC,EAAE;EAAC,MAAM,EAAC,iBAAiB;;AAAC,iJAA8I;EAAC,QAAQ,EAAC,QAAQ;EAAC,GAAG,EAAC,GAAG;EAAC,UAAU,EAAC,KAAK;EAAC,OAAO,EAAC,CAAC;EAAC,OAAO,EAAC,YAAY;;AAAC,uEAAsE;EAAC,IAAI,EAAC,GAAG;EAAC,WAAW,EAAC,KAAK;;AAAC,wEAAuE;EAAC,KAAK,EAAC,GAAG;EAAC,YAAY,EAAC,KAAK;;AAAC,0DAAyD;EAAC,KAAK,EAAC,IAAI;EAAC,MAAM,EAAC,IAAI;EAAC,WAAW,EAAC,CAAC;EAAC,WAAW,EAAC,KAAK;;AAAC,mCAAmC;EAAC,OAAO,EAAC,OAAO;;AAAC,mCAAmC;EAAC,OAAO,EAAC,OAAO;;AAAC,oBAAoB;EAAC,QAAQ,EAAC,QAAQ;EAAC,MAAM,EAAC,IAAI;EAAC,IAAI,EAAC,GAAG;EAAC,OAAO,EAAC,EAAE;EAAC,KAAK,EAAC,GAAG;EAAC,WAAW,EAAC,IAAI;EAAC,YAAY,EAAC,CAAC;EAAC,UAAU,EAAC,IAAI;EAAC,UAAU,EAAC,MAAM;;AAAC,uBAAuB;EAAC,OAAO,EAAC,YAAY;EAAC,KAAK,EAAC,IAAI;EAAC,MAAM,EAAC,IAAI;EAAC,MAAM,EAAC,GAAG;EAAC,WAAW,EAAC,MAAM;EAAC,MAAM,EAAC,cAAc;EAAC,aAAa,EAAC,IAAI;EAAC,MAAM,EAAC,OAAO;EAAC,gBAAgB,EAAC,OAAO;EAAC,gBAAgB,EAAC,WAAa;;AAAC,4BAA4B;EAAC,MAAM,EAAC,CAAC;EAAC,KAAK,EAAC,IAAI;EAAC,MAAM,EAAC,IAAI;EAAC,gBAAgB,EAAC,IAAI;;AAAC,iBAAiB;EAAC,QAAQ,EAAC,QAAQ;EAAC,IAAI,EAAC,GAAG;EAAC,KAAK,EAAC,GAAG;EAAC,MAAM,EAAC,IAAI;EAAC,OAAO,EAAC,EAAE;EAAC,WAAW,EAAC,IAAI;EAAC,cAAc,EAAC,IAAI;EAAC,KAAK,EAAC,IAAI;EAAC,UAAU,EAAC,MAAM;EAAC,WAAW,EAAC,4BAAyB;;AAAC,sBAAsB;EAAC,WAAW,EAAC,IAAI;;AAAC,oCAAmC;EAAC,iJAA8I;IAAC,KAAK,EAAC,IAAI;IAAC,MAAM,EAAC,IAAI;IAAC,UAAU,EAAC,KAAK;IAAC,SAAS,EAAC,IAAI;;EAAC,uEAAsE;IAAC,WAAW,EAAC,KAAK;;EAAC,wEAAuE;IAAC,YAAY,EAAC,KAAK;;EAAC,iBAAiB;IAAC,IAAI,EAAC,GAAG;IAAC,KAAK,EAAC,GAAG;IAAC,cAAc,EAAC,IAAI;;EAAC,oBAAoB;IAAC,MAAM,EAAC,IAAI;AAAE,srBAAmpB;EAAC,OAAO,EAAC,GAAG;EAAC,OAAO,EAAC,KAAK;;AAAC,kVAAiU;EAAC,KAAK,EAAC,IAAI;;AAAC,aAAa;EAAC,OAAO,EAAC,KAAK;EAAC,WAAW,EAAC,IAAI;EAAC,YAAY,EAAC,IAAI;;AAAC,WAAW;EAAC,KAAK,EAAC,gBAAgB;;AAAC,UAAU;EAAC,KAAK,EAAC,eAAe;;AAAC,KAAK;EAAC,OAAO,EAAC,eAAe;;AAAC,KAAK;EAAC,OAAO,EAAC,gBAAgB;;AAAC,UAAU;EAAC,UAAU,EAAC,MAAM;;AAAC,UAAU;EAAC,IAAI,EAAC,KAAK;EAAC,KAAK,EAAC,WAAW;EAAC,WAAW,EAAC,IAAI;EAAC,gBAAgB,EAAC,WAAW;EAAC,MAAM,EAAC,CAAC;;AAAC,OAAO;EAAC,OAAO,EAAC,eAAe;;AAAC,MAAM;EAAC,QAAQ,EAAC,KAAK;;AAAC,aAAiC;EAAnB,KAAK,EAAC,YAAY;AAAC,kDAA+C;EAAC,OAAO,EAAC,eAAe;;AAAC,kQAAuP;EAAC,OAAO,EAAC,eAAe;;AAAC,yBAAwB;EAAC,WAAW;IAAC,OAAO,EAAC,gBAAgB;;EAAC,gBAAgB;IAAC,OAAO,EAAC,gBAAgB;;EAAC,aAAa;IAAC,OAAO,EAAC,oBAAoB;;EAAC,4BAA2B;IAAC,OAAO,EAAC,qBAAqB;AAAE,yBAAwB;EAAC,iBAAiB;IAAC,OAAO,EAAC,gBAAgB;AAAE,yBAAwB;EAAC,kBAAkB;IAAC,OAAO,EAAC,iBAAiB;AAAE,yBAAwB;EAAC,wBAAwB;IAAC,OAAO,EAAC,uBAAuB;AAAE,gDAA8C;EAAC,WAAW;IAAC,OAAO,EAAC,gBAAgB;;EAAC,gBAAgB;IAAC,OAAO,EAAC,gBAAgB;;EAAC,aAAa;IAAC,OAAO,EAAC,oBAAoB;;EAAC,4BAA2B;IAAC,OAAO,EAAC,qBAAqB;AAAE,gDAA8C;EAAC,iBAAiB;IAAC,OAAO,EAAC,gBAAgB;AAAE,gDAA8C;EAAC,kBAAkB;IAAC,OAAO,EAAC,iBAAiB;AAAE,gDAA8C;EAAC,wBAAwB;IAAC,OAAO,EAAC,uBAAuB;AAAE,iDAA+C;EAAC,WAAW;IAAC,OAAO,EAAC,gBAAgB;;EAAC,gBAAgB;IAAC,OAAO,EAAC,gBAAgB;;EAAC,aAAa;IAAC,OAAO,EAAC,oBAAoB;;EAAC,4BAA2B;IAAC,OAAO,EAAC,qBAAqB;AAAE,iDAA+C;EAAC,iBAAiB;IAAC,OAAO,EAAC,gBAAgB;AAAE,iDAA+C;EAAC,kBAAkB;IAAC,OAAO,EAAC,iBAAiB;AAAE,iDAA+C;EAAC,wBAAwB;IAAC,OAAO,EAAC,uBAAuB;AAAE,0BAAyB;EAAC,WAAW;IAAC,OAAO,EAAC,gBAAgB;;EAAC,gBAAgB;IAAC,OAAO,EAAC,gBAAgB;;EAAC,aAAa;IAAC,OAAO,EAAC,oBAAoB;;EAAC,4BAA2B;IAAC,OAAO,EAAC,qBAAqB;AAAE,0BAAyB;EAAC,iBAAiB;IAAC,OAAO,EAAC,gBAAgB;AAAE,0BAAyB;EAAC,kBAAkB;IAAC,OAAO,EAAC,iBAAiB;AAAE,0BAAyB;EAAC,wBAAwB;IAAC,OAAO,EAAC,uBAAuB;AAAE,yBAAwB;EAAC,UAAU;IAAC,OAAO,EAAC,eAAe;AAAE,gDAA8C;EAAC,UAAU;IAAC,OAAO,EAAC,eAAe;AAAE,iDAA+C;EAAC,UAAU;IAAC,OAAO,EAAC,eAAe;AAAE,0BAAyB;EAAC,UAAU;IAAC,OAAO,EAAC,eAAe;AAAE,cAAc;EAAC,OAAO,EAAC,eAAe;;AAAC,YAAY;EAAC,cAAc;IAAC,OAAO,EAAC,gBAAgB;;EAAC,mBAAmB;IAAC,OAAO,EAAC,gBAAgB;;EAAC,gBAAgB;IAAC,OAAO,EAAC,oBAAoB;;EAAC,kCAAiC;IAAC,OAAO,EAAC,qBAAqB;AAAE,oBAAoB;EAAC,OAAO,EAAC,eAAe;;AAAC,YAAY;EAAC,oBAAoB;IAAC,OAAO,EAAC,gBAAgB;AAAE,qBAAqB;EAAC,OAAO,EAAC,eAAe;;AAAC,YAAY;EAAC,qBAAqB;IAAC,OAAO,EAAC,iBAAiB;AAAE,2BAA2B;EAAC,OAAO,EAAC,eAAe;;AAAC,YAAY;EAAC,2BAA2B;IAAC,OAAO,EAAC,uBAAuB;AAAE,YAAY;EAAC,aAAa;IAAC,OAAO,EAAC,eAAe;AAAE;;;EAG1z2G;AAAA,UAAyd;EAA9c,WAAW,EAAC,aAAa;EAAC,GAAG,EAAC,4CAA4C;EAAC,GAAG,EAAC,6VAAyV;EAAC,WAAW,EAAC,MAAM;EAAC,UAAU,EAAC,MAAM;AAAC,GAAG;EAAC,OAAO,EAAC,YAAY;EAAC,IAAI,EAAC,uCAAuC;EAAC,SAAS,EAAC,OAAO;EAAC,cAAc,EAAC,IAAI;EAAC,sBAAsB,EAAC,WAAW;EAAC,uBAAuB,EAAC,SAAS;;AAAC,MAAM;EAAC,SAAS,EAAC,YAAY;EAAC,WAAW,EAAC,KAAK;EAAC,cAAc,EAAC,IAAI;;AAAC,MAAM;EAAC,SAAS,EAAC,GAAG;;AAAC,MAAM;EAAC,SAAS,EAAC,GAAG;;AAAC,MAAM;EAAC,SAAS,EAAC,GAAG;;AAAC,MAAM;EAAC,SAAS,EAAC,GAAG;;AAAC,MAAM;EAAC,KAAK,EAAC,YAAY;EAAC,UAAU,EAAC,MAAM;;AAAC,MAAM;EAAC,YAAY,EAAC,CAAC;EAAC,WAAW,EAAC,YAAY;EAAC,eAAe,EAAC,IAAI;;AAAC,WAAS;EAAC,QAAQ,EAAC,QAAQ;;AAAC,MAAM;EAAC,QAAQ,EAAC,QAAQ;EAAC,IAAI,EAAC,aAAa;EAAC,KAAK,EAAC,YAAY;EAAC,GAAG,EAAC,WAAW;EAAC,UAAU,EAAC,MAAM;;AAAC,YAAY;EAAC,IAAI,EAAC,aAAa;;AAAC,UAAU;EAAC,OAAO,EAAC,gBAAgB;EAAC,MAAM,EAAC,gBAAgB;EAAC,aAAa,EAAC,IAAI;;AAAC,aAAa;EAAC,KAAK,EAAC,IAAI;;AAAC,cAAc;EAAC,KAAK,EAAC,KAAK;;AAAC,gBAAgB;EAAC,YAAY,EAAC,IAAI;;AAAC,iBAAiB;EAAC,WAAW,EAAC,IAAI;;AAAC,WAAW;EAAC,KAAK,EAAC,KAAK;;AAAC,UAAU;EAAC,KAAK,EAAC,IAAI;;AAAC,aAAa;EAAC,YAAY,EAAC,IAAI;;AAAC,cAAc;EAAC,WAAW,EAAC,IAAI;;AAAC,QAAQ;EAAC,iBAAiB,EAAC,0BAA0B;EAAC,SAAS,EAAC,0BAA0B;;AAAC,SAAS;EAAC,iBAAiB,EAAC,4BAA4B;EAAC,SAAS,EAAC,4BAA4B;;AAAC,0BAAoJ;EAAzH,EAAE;IAAC,iBAAiB,EAAC,YAAY;IAAC,SAAS,EAAC,YAAY;EAAC,IAAI;IAAC,iBAAiB,EAAC,cAAc;IAAC,SAAS,EAAC,cAAc;AAAE,kBAA4I;EAAzH,EAAE;IAAC,iBAAiB,EAAC,YAAY;IAAC,SAAS,EAAC,YAAY;EAAC,IAAI;IAAC,iBAAiB,EAAC,cAAc;IAAC,SAAS,EAAC,cAAc;AAAE,aAAa;EAAC,UAAU,EAAC,0DAA0D;EAAC,iBAAiB,EAAC,aAAa;EAAC,aAAa,EAAC,aAAa;EAAC,SAAS,EAAC,aAAa;;AAAC,cAAc;EAAC,UAAU,EAAC,0DAA0D;EAAC,iBAAiB,EAAC,cAAc;EAAC,aAAa,EAAC,cAAc;EAAC,SAAS,EAAC,cAAc;;AAAC,cAAc;EAAC,UAAU,EAAC,0DAA0D;EAAC,iBAAiB,EAAC,cAAc;EAAC,aAAa,EAAC,cAAc;EAAC,SAAS,EAAC,cAAc;;AAAC,mBAAmB;EAAC,UAAU,EAAC,oEAAoE;EAAC,iBAAiB,EAAC,YAAY;EAAC,aAAa,EAAC,YAAY;EAAC,SAAS,EAAC,YAAY;;AAAC,iBAAiB;EAAC,UAAU,EAAC,oEAAoE;EAAC,iBAAiB,EAAC,YAAY;EAAC,aAAa,EAAC,YAAY;EAAC,SAAS,EAAC,YAAY;;AAAC,mHAA+G;EAAC,MAAM,EAAC,IAAI;;AAAC,SAAS;EAAC,QAAQ,EAAC,QAAQ;EAAC,OAAO,EAAC,YAAY;EAAC,KAAK,EAAC,GAAG;EAAC,MAAM,EAAC,GAAG;EAAC,WAAW,EAAC,GAAG;EAAC,cAAc,EAAC,MAAM;;AAAC,0BAAyB;EAAC,QAAQ,EAAC,QAAQ;EAAC,IAAI,EAAC,CAAC;EAAC,KAAK,EAAC,IAAI;EAAC,UAAU,EAAC,MAAM;;AAAC,YAAY;EAAC,WAAW,EAAC,OAAO;;AAAC,YAAY;EAAC,SAAS,EAAC,GAAG;;AAAC,WAAW;EAAC,KAAK,EAAC,IAAI;;AAAC,gBAAgB;EAAC,OAAO,EAAC,OAAO;;AAAC,gBAAgB;EAAC,OAAO,EAAC,OAAO;;AAAC,iBAAiB;EAAC,OAAO,EAAC,OAAO;;AAAC,qBAAqB;EAAC,OAAO,EAAC,OAAO;;AAAC,gBAAgB;EAAC,OAAO,EAAC,OAAO;;AAAC,eAAe;EAAC,OAAO,EAAC,OAAO;;AAAC,iBAAiB;EAAC,OAAO,EAAC,OAAO;;AAAC,eAAe;EAAC,OAAO,EAAC,OAAO;;AAAC,eAAe;EAAC,OAAO,EAAC,OAAO;;AAAC,mBAAmB;EAAC,OAAO,EAAC,OAAO;;AAAC,aAAa;EAAC,OAAO,EAAC,OAAO;;AAAC,kBAAkB;EAAC,OAAO,EAAC,OAAO;;AAAC,gBAAgB;EAAC,OAAO,EAAC,OAAO;;AAAC,qDAAmD;EAAC,OAAO,EAAC,OAAO;;AAAC,sBAAsB;EAAC,OAAO,EAAC,OAAO;;AAAC,uBAAuB;EAAC,OAAO,EAAC,OAAO;;AAAC,oBAAoB;EAAC,OAAO,EAAC,OAAO;;AAAC,iBAAiB;EAAC,OAAO,EAAC,OAAO;;AAAC,+BAA8B;EAAC,OAAO,EAAC,OAAO;;AAAC,kBAAkB;EAAC,OAAO,EAAC,OAAO;;AAAC,eAAe;EAAC,OAAO,EAAC,OAAO;;AAAC,iBAAiB;EAAC,OAAO,EAAC,OAAO;;AAAC,kBAAkB;EAAC,OAAO,EAAC,OAAO;;AAAC,eAAe;EAAC,OAAO,EAAC,OAAO;;AAAC,mBAAmB;EAAC,OAAO,EAAC,OAAO;;AAAC,8BAA8B;EAAC,OAAO,EAAC,OAAO;;AAAC,4BAA4B;EAAC,OAAO,EAAC,OAAO;;AAAC,gBAAgB;EAAC,OAAO,EAAC,OAAO;;AAAC,wBAAwB;EAAC,OAAO,EAAC,OAAO;;AAAC,0CAAyC;EAAC,OAAO,EAAC,OAAO;;AAAC,kBAAkB;EAAC,OAAO,EAAC,OAAO;;AAAC,mBAAmB;EAAC,OAAO,EAAC,OAAO;;AAAC,eAAe;EAAC,OAAO,EAAC,OAAO;;AAAC,eAAe;EAAC,OAAO,EAAC,OAAO;;AAAC,qBAAqB;EAAC,OAAO,EAAC,OAAO;;AAAC,qBAAqB;EAAC,OAAO,EAAC,OAAO;;AAAC,sBAAsB;EAAC,OAAO,EAAC,OAAO;;AAAC,oBAAoB;EAAC,OAAO,EAAC,OAAO;;AAAC,iBAAiB;EAAC,OAAO,EAAC,OAAO;;AAAC,kBAAkB;EAAC,OAAO,EAAC,OAAO;;AAAC,cAAc;EAAC,OAAO,EAAC,OAAO;;AAAC,eAAe;EAAC,OAAO,EAAC,OAAO;;AAAC,eAAe;EAAC,OAAO,EAAC,OAAO;;AAAC,mBAAmB;EAAC,OAAO,EAAC,OAAO;;AAAC,gBAAgB;EAAC,OAAO,EAAC,OAAO;;AAAC,iBAAiB;EAAC,OAAO,EAAC,OAAO;;AAAC,eAAe;EAAC,OAAO,EAAC,OAAO;;AAAC,eAAe;EAAC,OAAO,EAAC,OAAO;;AAAC,iBAAiB;EAAC,OAAO,EAAC,OAAO;;AAAC,sBAAsB;EAAC,OAAO,EAAC,OAAO;;AAAC,qBAAqB;EAAC,OAAO,EAAC,OAAO;;AAAC,qBAAqB;EAAC,OAAO,EAAC,OAAO;;AAAC,uBAAuB;EAAC,OAAO,EAAC,OAAO;;AAAC,sBAAsB;EAAC,OAAO,EAAC,OAAO;;AAAC,wBAAwB;EAAC,OAAO,EAAC,OAAO;;AAAC,eAAe;EAAC,OAAO,EAAC,OAAO;;AAAC,qCAAoC;EAAC,OAAO,EAAC,OAAO;;AAAC,iBAAiB;EAAC,OAAO,EAAC,OAAO;;AAAC,uBAAuB;EAAC,OAAO,EAAC,OAAO;;AAAC,wDAAsD;EAAC,OAAO,EAAC,OAAO;;AAAC,iBAAiB;EAAC,OAAO,EAAC,OAAO;;AAAC,qBAAqB;EAAC,OAAO,EAAC,OAAO;;AAAC,iBAAiB;EAAC,OAAO,EAAC,OAAO;;AAAC,eAAe;EAAC,OAAO,EAAC,OAAO;;AAAC,2CAA0C;EAAC,OAAO,EAAC,OAAO;;AAAC,yBAAyB;EAAC,OAAO,EAAC,OAAO;;AAAC,yBAAyB;EAAC,OAAO,EAAC,OAAO;;AAAC,iBAAiB;EAAC,OAAO,EAAC,OAAO;;AAAC,wBAAwB;EAAC,OAAO,EAAC,OAAO;;AAAC,wBAAwB;EAAC,OAAO,EAAC,OAAO;;AAAC,mBAAmB;EAAC,OAAO,EAAC,OAAO;;AAAC,eAAe;EAAC,OAAO,EAAC,OAAO;;AAAC,gBAAgB;EAAC,OAAO,EAAC,OAAO;;AAAC,eAAe;EAAC,OAAO,EAAC,OAAO;;AAAC,kBAAkB;EAAC,OAAO,EAAC,OAAO;;AAAC,uBAAuB;EAAC,OAAO,EAAC,OAAO;;AAAC,uBAAuB;EAAC,OAAO,EAAC,OAAO;;AAAC,gBAAgB;EAAC,OAAO,EAAC,OAAO;;AAAC,uBAAuB;EAAC,OAAO,EAAC,OAAO;;AAAC,wBAAwB;EAAC,OAAO,EAAC,OAAO;;AAAC,sBAAsB;EAAC,OAAO,EAAC,OAAO;;AAAC,uBAAuB;EAAC,OAAO,EAAC,OAAO;;AAAC,uBAAuB;EAAC,OAAO,EAAC,OAAO;;AAAC,uBAAuB;EAAC,OAAO,EAAC,OAAO;;AAAC,0BAA0B;EAAC,OAAO,EAAC,OAAO;;AAAC,sBAAsB;EAAC,OAAO,EAAC,OAAO;;AAAC,qBAAqB;EAAC,OAAO,EAAC,OAAO;;AAAC,yBAAyB;EAAC,OAAO,EAAC,OAAO;;AAAC,yBAAyB;EAAC,OAAO,EAAC,OAAO;;AAAC,cAAc;EAAC,OAAO,EAAC,OAAO;;AAAC,qBAAqB;EAAC,OAAO,EAAC,OAAO;;AAAC,sBAAsB;EAAC,OAAO,EAAC,OAAO;;AAAC,mBAAmB;EAAC,OAAO,EAAC,OAAO;;AAAC,qBAAqB;EAAC,OAAO,EAAC,OAAO;;AAAC,yCAAwC;EAAC,OAAO,EAAC,OAAO;;AAAC,iBAAiB;EAAC,OAAO,EAAC,OAAO;;AAAC,mBAAmB;EAAC,OAAO,EAAC,OAAO;;AAAC,eAAe;EAAC,OAAO,EAAC,OAAO;;AAAC,gBAAgB;EAAC,OAAO,EAAC,OAAO;;AAAC,mBAAmB;EAAC,OAAO,EAAC,OAAO;;AAAC,6BAA6B;EAAC,OAAO,EAAC,OAAO;;AAAC,eAAe;EAAC,OAAO,EAAC,OAAO;;AAAC,eAAe;EAAC,OAAO,EAAC,OAAO;;AAAC,eAAe;EAAC,OAAO,EAAC,OAAO;;AAAC,cAAc;EAAC,OAAO,EAAC,OAAO;;AAAC,oBAAoB;EAAC,OAAO,EAAC,OAAO;;AAAC,mDAAkD;EAAC,OAAO,EAAC,OAAO;;AAAC,gBAAgB;EAAC,OAAO,EAAC,OAAO;;AAAC,mBAAmB;EAAC,OAAO,EAAC,OAAO;;AAAC,iBAAiB;EAAC,OAAO,EAAC,OAAO;;AAAC,kBAAkB;EAAC,OAAO,EAAC,OAAO;;AAAC,iBAAiB;EAAC,OAAO,EAAC,OAAO;;AAAC,qBAAqB;EAAC,OAAO,EAAC,OAAO;;AAAC,uBAAuB;EAAC,OAAO,EAAC,OAAO;;AAAC,kBAAkB;EAAC,OAAO,EAAC,OAAO;;AAAC,wBAAwB;EAAC,OAAO,EAAC,OAAO;;AAAC,iBAAiB;EAAC,OAAO,EAAC,OAAO;;AAAC,sBAAsB;EAAC,OAAO,EAAC,OAAO;;AAAC,mBAAmB;EAAC,OAAO,EAAC,OAAO;;AAAC,mBAAmB;EAAC,OAAO,EAAC,OAAO;;AAAC,4CAA2C;EAAC,OAAO,EAAC,OAAO;;AAAC,yBAAyB;EAAC,OAAO,EAAC,OAAO;;AAAC,0BAA0B;EAAC,OAAO,EAAC,OAAO;;AAAC,uBAAuB;EAAC,OAAO,EAAC,OAAO;;AAAC,cAAc;EAAC,OAAO,EAAC,OAAO;;AAAC,iCAAgC;EAAC,OAAO,EAAC,OAAO;;AAAC,mBAAmB;EAAC,OAAO,EAAC,OAAO;;AAAC,sBAAsB;EAAC,OAAO,EAAC,OAAO;;AAAC,wBAAwB;EAAC,OAAO,EAAC,OAAO;;AAAC,oBAAoB;EAAC,OAAO,EAAC,OAAO;;AAAC,kBAAkB;EAAC,OAAO,EAAC,OAAO;;AAAC,mBAAmB;EAAC,OAAO,EAAC,OAAO;;AAAC,0BAA0B;EAAC,OAAO,EAAC,OAAO;;AAAC,qBAAqB;EAAC,OAAO,EAAC,OAAO;;AAAC,wBAAwB;EAAC,OAAO,EAAC,OAAO;;AAAC,kBAAkB;EAAC,OAAO,EAAC,OAAO;;AAAC,iBAAiB;EAAC,OAAO,EAAC,OAAO;;AAAC,wBAAwB;EAAC,OAAO,EAAC,OAAO;;AAAC,iBAAiB;EAAC,OAAO,EAAC,OAAO;;AAAC,kBAAkB;EAAC,OAAO,EAAC,OAAO;;AAAC,gBAAgB;EAAC,OAAO,EAAC,OAAO;;AAAC,mBAAmB;EAAC,OAAO,EAAC,OAAO;;AAAC,qBAAqB;EAAC,OAAO,EAAC,OAAO;;AAAC,uBAAuB;EAAC,OAAO,EAAC,OAAO;;AAAC,kBAAkB;EAAC,OAAO,EAAC,OAAO;;AAAC,0CAAyC;EAAC,OAAO,EAAC,OAAO;;AAAC,iBAAiB;EAAC,OAAO,EAAC,OAAO;;AAAC,iBAAiB;EAAC,OAAO,EAAC,OAAO;;AAAC,sBAAsB;EAAC,OAAO,EAAC,OAAO;;AAAC,+BAA8B;EAAC,OAAO,EAAC,OAAO;;AAAC,gBAAgB;EAAC,OAAO,EAAC,OAAO;;AAAC,mBAAmB;EAAC,OAAO,EAAC,OAAO;;AAAC,eAAe;EAAC,OAAO,EAAC,OAAO;;AAAC,sBAAsB;EAAC,OAAO,EAAC,OAAO;;AAAC,uBAAuB;EAAC,OAAO,EAAC,OAAO;;AAAC,sBAAsB;EAAC,OAAO,EAAC,OAAO;;AAAC,oBAAoB;EAAC,OAAO,EAAC,OAAO;;AAAC,sBAAsB;EAAC,OAAO,EAAC,OAAO;;AAAC,4BAA4B;EAAC,OAAO,EAAC,OAAO;;AAAC,6BAA6B;EAAC,OAAO,EAAC,OAAO;;AAAC,0BAA0B;EAAC,OAAO,EAAC,OAAO;;AAAC,4BAA4B;EAAC,OAAO,EAAC,OAAO;;AAAC,gBAAgB;EAAC,OAAO,EAAC,OAAO;;AAAC,iBAAiB;EAAC,OAAO,EAAC,OAAO;;AAAC,gBAAgB;EAAC,OAAO,EAAC,OAAO;;AAAC,iBAAiB;EAAC,OAAO,EAAC,OAAO;;AAAC,oBAAoB;EAAC,OAAO,EAAC,OAAO;;AAAC,qBAAqB;EAAC,OAAO,EAAC,OAAO;;AAAC,kCAAiC;EAAC,OAAO,EAAC,OAAO;;AAAC,iCAAgC;EAAC,OAAO,EAAC,OAAO;;AAAC,gBAAgB;EAAC,OAAO,EAAC,OAAO;;AAAC,gBAAgB;EAAC,OAAO,EAAC,OAAO;;AAAC,mCAAkC;EAAC,OAAO,EAAC,OAAO;;AAAC,mCAAkC;EAAC,OAAO,EAAC,OAAO;;AAAC,oBAAoB;EAAC,OAAO,EAAC,OAAO;;AAAC,oCAAmC;EAAC,OAAO,EAAC,OAAO;;AAAC,iBAAiB;EAAC,OAAO,EAAC,OAAO;;AAAC,uDAAqD;EAAC,OAAO,EAAC,OAAO;;AAAC,kBAAkB;EAAC,OAAO,EAAC,OAAO;;AAAC,kBAAkB;EAAC,OAAO,EAAC,OAAO;;AAAC,wBAAwB;EAAC,OAAO,EAAC,OAAO;;AAAC,oBAAoB;EAAC,OAAO,EAAC,OAAO;;AAAC,gBAAgB;EAAC,OAAO,EAAC,OAAO;;AAAC,gBAAgB;EAAC,OAAO,EAAC,OAAO;;AAAC,gBAAgB;EAAC,OAAO,EAAC,OAAO;;AAAC,oBAAoB;EAAC,OAAO,EAAC,OAAO;;AAAC,2BAA2B;EAAC,OAAO,EAAC,OAAO;;AAAC,6BAA6B;EAAC,OAAO,EAAC,OAAO;;AAAC,sBAAsB;EAAC,OAAO,EAAC,OAAO;;AAAC,gBAAgB;EAAC,OAAO,EAAC,OAAO;;AAAC,qBAAqB;EAAC,OAAO,EAAC,OAAO;;AAAC,mBAAmB;EAAC,OAAO,EAAC,OAAO;;AAAC,qBAAqB;EAAC,OAAO,EAAC,OAAO;;AAAC,sBAAsB;EAAC,OAAO,EAAC,OAAO;;AAAC,kBAAkB;EAAC,OAAO,EAAC,OAAO;;AAAC,oCAAmC;EAAC,OAAO,EAAC,OAAO;;AAAC,0CAAyC;EAAC,OAAO,EAAC,OAAO;;AAAC,uCAAsC;EAAC,OAAO,EAAC,OAAO;;AAAC,mBAAmB;EAAC,OAAO,EAAC,OAAO;;AAAC,mBAAmB;EAAC,OAAO,EAAC,OAAO;;AAAC,uCAAsC;EAAC,OAAO,EAAC,OAAO;;AAAC,kCAAiC;EAAC,OAAO,EAAC,OAAO;;AAAC,2CAA0C;EAAC,OAAO,EAAC,OAAO;;AAAC,oBAAoB;EAAC,OAAO,EAAC,OAAO;;AAAC,qBAAqB;EAAC,OAAO,EAAC,OAAO;;AAAC,iCAAgC;EAAC,OAAO,EAAC,OAAO;;AAAC,kBAAkB;EAAC,OAAO,EAAC,OAAO;;AAAC,mBAAmB;EAAC,OAAO,EAAC,OAAO;;AAAC,sCAAqC;EAAC,OAAO,EAAC,OAAO;;AAAC,sBAAsB;EAAC,OAAO,EAAC,OAAO;;AAAC,mBAAmB;EAAC,OAAO,EAAC,OAAO;;AAAC,yBAAyB;EAAC,OAAO,EAAC,OAAO;;AAAC,uBAAuB;EAAC,OAAO,EAAC,OAAO;;AAAC,kBAAkB;EAAC,OAAO,EAAC,OAAO;;AAAC,sBAAsB;EAAC,OAAO,EAAC,OAAO;;AAAC,mBAAmB;EAAC,OAAO,EAAC,OAAO;;AAAC,iBAAiB;EAAC,OAAO,EAAC,OAAO;;AAAC,iBAAiB;EAAC,OAAO,EAAC,OAAO;;AAAC,kBAAkB;EAAC,OAAO,EAAC,OAAO;;AAAC,sBAAsB;EAAC,OAAO,EAAC,OAAO;;AAAC,qBAAqB;EAAC,OAAO,EAAC,OAAO;;AAAC,qBAAqB;EAAC,OAAO,EAAC,OAAO;;AAAC,oBAAoB;EAAC,OAAO,EAAC,OAAO;;AAAC,iBAAiB;EAAC,OAAO,EAAC,OAAO;;AAAC,sBAAsB;EAAC,OAAO,EAAC,OAAO;;AAAC,eAAe;EAAC,OAAO,EAAC,OAAO;;AAAC,mBAAmB;EAAC,OAAO,EAAC,OAAO;;AAAC,sBAAsB;EAAC,OAAO,EAAC,OAAO;;AAAC,4BAA4B;EAAC,OAAO,EAAC,OAAO;;AAAC,6BAA6B;EAAC,OAAO,EAAC,OAAO;;AAAC,0BAA0B;EAAC,OAAO,EAAC,OAAO;;AAAC,4BAA4B;EAAC,OAAO,EAAC,OAAO;;AAAC,qBAAqB;EAAC,OAAO,EAAC,OAAO;;AAAC,sBAAsB;EAAC,OAAO,EAAC,OAAO;;AAAC,mBAAmB;EAAC,OAAO,EAAC,OAAO;;AAAC,qBAAqB;EAAC,OAAO,EAAC,OAAO;;AAAC,kBAAkB;EAAC,OAAO,EAAC,OAAO;;AAAC,iBAAiB;EAAC,OAAO,EAAC,OAAO;;AAAC,iBAAiB;EAAC,OAAO,EAAC,OAAO;;AAAC,0CAAyC;EAAC,OAAO,EAAC,OAAO;;AAAC,mBAAmB;EAAC,OAAO,EAAC,OAAO;;AAAC,qBAAqB;EAAC,OAAO,EAAC,OAAO;;AAAC,sBAAsB;EAAC,OAAO,EAAC,OAAO;;AAAC,kBAAkB;EAAC,OAAO,EAAC,OAAO;;AAAC,iBAAiB;EAAC,OAAO,EAAC,OAAO;;AAAC,uCAAsC;EAAC,OAAO,EAAC,OAAO;;AAAC,qBAAqB;EAAC,OAAO,EAAC,OAAO;;AAAC,mBAAmB;EAAC,OAAO,EAAC,OAAO;;AAAC,wBAAwB;EAAC,OAAO,EAAC,OAAO;;AAAC,kBAAkB;EAAC,OAAO,EAAC,OAAO;;AAAC,kBAAkB;EAAC,OAAO,EAAC,OAAO;;AAAC,gBAAgB;EAAC,OAAO,EAAC,OAAO;;AAAC,kBAAkB;EAAC,OAAO,EAAC,OAAO;;AAAC,qBAAqB;EAAC,OAAO,EAAC,OAAO;;AAAC,iBAAiB;EAAC,OAAO,EAAC,OAAO;;AAAC,yBAAyB;EAAC,OAAO,EAAC,OAAO;;AAAC,mBAAmB;EAAC,OAAO,EAAC,OAAO;;AAAC,eAAe;EAAC,OAAO,EAAC,OAAO;;AAAC,+CAA8C;EAAC,OAAO,EAAC,OAAO;;AAAC,6EAA2E;EAAC,OAAO,EAAC,OAAO;;AAAC,yBAAyB;EAAC,OAAO,EAAC,OAAO;;AAAC,eAAe;EAAC,OAAO,EAAC,OAAO;;AAAC,oBAAoB;EAAC,OAAO,EAAC,OAAO;;AAAC,0CAAyC;EAAC,OAAO,EAAC,OAAO;;AAAC,mBAAmB;EAAC,OAAO,EAAC,OAAO;;AAAC,eAAe;EAAC,OAAO,EAAC,OAAO;;AAAC,sBAAsB;EAAC,OAAO,EAAC,OAAO;;AAAC,sBAAsB;EAAC,OAAO,EAAC,OAAO;;AAAC,oBAAoB;EAAC,OAAO,EAAC,OAAO;;AAAC,iBAAiB;EAAC,OAAO,EAAC,OAAO;;AAAC,uBAAuB;EAAC,OAAO,EAAC,OAAO;;AAAC,qBAAqB;EAAC,OAAO,EAAC,OAAO;;AAAC,2BAA2B;EAAC,OAAO,EAAC,OAAO;;AAAC,iBAAiB;EAAC,OAAO,EAAC,OAAO;;AAAC,qBAAqB;EAAC,OAAO,EAAC,OAAO;;AAAC,4BAA4B;EAAC,OAAO,EAAC,OAAO;;AAAC,iBAAiB;EAAC,OAAO,EAAC,OAAO;;AAAC,iBAAiB;EAAC,OAAO,EAAC,OAAO;;AAAC,8BAA8B;EAAC,OAAO,EAAC,OAAO;;AAAC,+BAA+B;EAAC,OAAO,EAAC,OAAO;;AAAC,4BAA4B;EAAC,OAAO,EAAC,OAAO;;AAAC,8BAA8B;EAAC,OAAO,EAAC,OAAO;;AAAC,gBAAgB;EAAC,OAAO,EAAC,OAAO;;AAAC,eAAe;EAAC,OAAO,EAAC,OAAO;;AAAC,iBAAiB;EAAC,OAAO,EAAC,OAAO;;AAAC,qBAAqB;EAAC,OAAO,EAAC,OAAO;;AAAC,mBAAmB;EAAC,OAAO,EAAC,OAAO;;AAAC,qBAAqB;EAAC,OAAO,EAAC,OAAO;;AAAC,qBAAqB;EAAC,OAAO,EAAC,OAAO;;AAAC,qBAAqB;EAAC,OAAO,EAAC,OAAO;;AAAC,sBAAsB;EAAC,OAAO,EAAC,OAAO;;AAAC,iBAAiB;EAAC,OAAO,EAAC,OAAO;;AAAC,uBAAuB;EAAC,OAAO,EAAC,OAAO;;AAAC,yBAAyB;EAAC,OAAO,EAAC,OAAO;;AAAC,mBAAmB;EAAC,OAAO,EAAC,OAAO;;AAAC,qBAAqB;EAAC,OAAO,EAAC,OAAO;;AAAC,uBAAuB;EAAC,OAAO,EAAC,OAAO;;AAAC,wBAAwB;EAAC,OAAO,EAAC,OAAO;;AAAC,+BAA+B;EAAC,OAAO,EAAC,OAAO;;AAAC,uBAAuB;EAAC,OAAO,EAAC,OAAO;;AAAC,kBAAkB;EAAC,OAAO,EAAC,OAAO;;AAAC,sDAAqD;EAAC,OAAO,EAAC,OAAO;;AAAC,kDAAiD;EAAC,OAAO,EAAC,OAAO;;AAAC,wDAAuD;EAAC,OAAO,EAAC,OAAO;;AAAC,+BAA8B;EAAC,OAAO,EAAC,OAAO;;AAAC,cAAc;EAAC,OAAO,EAAC,OAAO;;AAAC,iCAAgC;EAAC,OAAO,EAAC,OAAO;;AAAC,gCAA+B;EAAC,OAAO,EAAC,OAAO;;AAAC,8DAA2D;EAAC,OAAO,EAAC,OAAO;;AAAC,mDAAiD;EAAC,OAAO,EAAC,OAAO;;AAAC,8BAA6B;EAAC,OAAO,EAAC,OAAO;;AAAC,kCAAiC;EAAC,OAAO,EAAC,OAAO;;AAAC,eAAe;EAAC,OAAO,EAAC,OAAO;;AAAC,oBAAoB;EAAC,OAAO,EAAC,OAAO;;AAAC,yBAAyB;EAAC,OAAO,EAAC,OAAO;;AAAC,0BAA0B;EAAC,OAAO,EAAC,OAAO;;AAAC,0BAA0B;EAAC,OAAO,EAAC,OAAO;;AAAC,2BAA2B;EAAC,OAAO,EAAC,OAAO;;AAAC,2BAA2B;EAAC,OAAO,EAAC,OAAO;;AAAC,4BAA4B;EAAC,OAAO,EAAC,OAAO;;AAAC,oBAAoB;EAAC,OAAO,EAAC,OAAO;;AAAC,sBAAsB;EAAC,OAAO,EAAC,OAAO;;AAAC,yBAAyB;EAAC,OAAO,EAAC,OAAO;;AAAC,kBAAkB;EAAC,OAAO,EAAC,OAAO;;AAAC,eAAe;EAAC,OAAO,EAAC,OAAO;;AAAC,sBAAsB;EAAC,OAAO,EAAC,OAAO;;AAAC,uBAAuB;EAAC,OAAO,EAAC,OAAO;;AAAC,kBAAkB;EAAC,OAAO,EAAC,OAAO;;AAAC,yBAAyB;EAAC,OAAO,EAAC,OAAO;;AAAC,oBAAoB;EAAC,OAAO,EAAC,OAAO;;AAAC,iBAAiB;EAAC,OAAO,EAAC,OAAO;;AAAC,cAAc;EAAC,OAAO,EAAC,OAAO;;AAAC,oBAAoB;EAAC,OAAO,EAAC,OAAO;;AAAC,2BAA2B;EAAC,OAAO,EAAC,OAAO;;AAAC,iBAAiB;EAAC,OAAO,EAAC,OAAO;;AAAC,wBAAwB;EAAC,OAAO,EAAC,OAAO;;AAAC,0BAA0B;EAAC,OAAO,EAAC,OAAO;;AAAC,wBAAwB;EAAC,OAAO,EAAC,OAAO;;AAAC,0BAA0B;EAAC,OAAO,EAAC,OAAO;;AAAC,2BAA2B;EAAC,OAAO,EAAC,OAAO;;AAAC,gBAAgB;EAAC,OAAO,EAAC,OAAO;;AAAC,kBAAkB;EAAC,OAAO,EAAC,OAAO;;AAAC,kBAAkB;EAAC,OAAO,EAAC,OAAO;;AAAC,gBAAgB;EAAC,OAAO,EAAC,OAAO;;AAAC,mBAAmB;EAAC,OAAO,EAAC,OAAO;;AAAC,gBAAgB;EAAC,OAAO,EAAC,OAAO;;AAAC,qBAAqB;EAAC,OAAO,EAAC,OAAO;;AAAC,iBAAiB;EAAC,OAAO,EAAC,OAAO;;AAAC,iBAAiB;EAAC,OAAO,EAAC,OAAO;;AAAC,eAAe;EAAC,OAAO,EAAC,OAAO;;AAAC,sCAAqC;EAAC,OAAO,EAAC,OAAO;;AAAC,gBAAgB;EAAC,OAAO,EAAC,OAAO;;AAAC,iBAAiB;EAAC,OAAO,EAAC,OAAO;;AAAC,kBAAkB;EAAC,OAAO,EAAC,OAAO;;AAAC,cAAc;EAAC,OAAO,EAAC,OAAO;;AAAC,aAAa;EAAC,OAAO,EAAC,OAAO;;AAAC,gBAAgB;EAAC,OAAO,EAAC,OAAO;;AAAC,iBAAiB;EAAC,OAAO,EAAC,OAAO;;AAAC,oBAAoB;EAAC,OAAO,EAAC,OAAO;;AAAC,yBAAyB;EAAC,OAAO,EAAC,OAAO;;AAAC,+BAA+B;EAAC,OAAO,EAAC,OAAO;;AAAC,8BAA8B;EAAC,OAAO,EAAC,OAAO;;AAAC,sDAAqD;EAAC,OAAO,EAAC,OAAO;;AAAC,uBAAuB;EAAC,OAAO,EAAC,OAAO;;AAAC,qBAAqB;EAAC,OAAO,EAAC,OAAO;;AAAC,uBAAuB;EAAC,OAAO,EAAC,OAAO;;AAAC,uCAAsC;EAAC,OAAO,EAAC,OAAO;;AAAC,wBAAwB;EAAC,OAAO,EAAC,OAAO;;AAAC,wBAAwB;EAAC,OAAO,EAAC,OAAO;;AAAC,gBAAgB;EAAC,OAAO,EAAC,OAAO;;AAAC,0BAA0B;EAAC,OAAO,EAAC,OAAO;;AAAC,oBAAoB;EAAC,OAAO,EAAC,OAAO;;AAAC,iBAAiB;EAAC,OAAO,EAAC,OAAO;;AAAC,8DAA4D;EAAC,OAAO,EAAC,OAAO;;AAAC,kDAAiD;EAAC,OAAO,EAAC,OAAO;;AAAC,gBAAgB;EAAC,OAAO,EAAC,OAAO;;AAAC,iBAAiB;EAAC,OAAO,EAAC,OAAO;;AAAC,iBAAiB;EAAC,OAAO,EAAC,OAAO;;AAAC,wBAAwB;EAAC,OAAO,EAAC,OAAO;;AAAC,6BAA6B;EAAC,OAAO,EAAC,OAAO;;AAAC,sBAAsB;EAAC,OAAO,EAAC,OAAO;;AAAC,oBAAoB;EAAC,OAAO,EAAC,OAAO;;AAAC,eAAe;EAAC,OAAO,EAAC,OAAO;;AAAC,wBAAwB;EAAC,OAAO,EAAC,OAAO;;AAAC,yBAAyB;EAAC,OAAO,EAAC,OAAO;;AAAC,iBAAiB;EAAC,OAAO,EAAC,OAAO;;AAAC,iBAAiB;EAAC,OAAO,EAAC,OAAO;;AAAC,mBAAmB;EAAC,OAAO,EAAC,OAAO;;AAAC,cAAc;EAAC,OAAO,EAAC,OAAO;;AAAC,mBAAmB;EAAC,OAAO,EAAC,OAAO;;AAAC,gBAAgB;EAAC,OAAO,EAAC,OAAO;;AAAC,cAAc;EAAC,OAAO,EAAC,OAAO;;AAAC,gBAAgB;EAAC,OAAO,EAAC,OAAO;;AAAC,eAAe;EAAC,OAAO,EAAC,OAAO;;AAAC,gBAAgB;EAAC,OAAO,EAAC,OAAO;;AAAC,kBAAkB;EAAC,OAAO,EAAC,OAAO;;AAAC,yBAAyB;EAAC,OAAO,EAAC,OAAO;;AAAC,gBAAgB;EAAC,OAAO,EAAC,OAAO;;AAAC,uBAAuB;EAAC,OAAO,EAAC,OAAO;;AAAC,kBAAkB;EAAC,OAAO,EAAC,OAAO;;AAAC,qCAAoC;EAAC,OAAO,EAAC,OAAO;;AAAC,+BAA8B;EAAC,OAAO,EAAC,OAAO;;AAAC,eAAe;EAAC,OAAO,EAAC,OAAO;;AAAC,kBAAkB;EAAC,OAAO,EAAC,OAAO;;AAAC,qBAAqB;EAAC,OAAO,EAAC,OAAO;;AAAC,qBAAqB;EAAC,OAAO,EAAC,OAAO;;AAAC,mBAAmB;EAAC,OAAO,EAAC,OAAO;;AAAC,qBAAqB;EAAC,OAAO,EAAC,OAAO;;AAAC,sBAAsB;EAAC,OAAO,EAAC,OAAO;;AAAC,uBAAuB;EAAC,OAAO,EAAC,OAAO;;AAAC,4BAA4B;EAAC,OAAO,EAAC,OAAO;;AAAC,2EAAyE;EAAC,OAAO,EAAC,OAAO;;AAAC,gDAA+C;EAAC,OAAO,EAAC,OAAO;;AAAC,gDAA+C;EAAC,OAAO,EAAC,OAAO;;AAAC,gDAA+C;EAAC,OAAO,EAAC,OAAO;;AAAC,sBAAsB;EAAC,OAAO,EAAC,OAAO;;AAAC,eAAe;EAAC,OAAO,EAAC,OAAO;;AAAC,kBAAkB;EAAC,OAAO,EAAC,OAAO;;AAAC,mBAAmB;EAAC,OAAO,EAAC,OAAO;;AAAC,2GAAuG;EAAC,OAAO,EAAC,OAAO;;AAAC,yBAAyB;EAAC,OAAO,EAAC,OAAO;;AAAC,sDAAoD;EAAC,OAAO,EAAC,OAAO;;AAAC,gCAA+B;EAAC,OAAO,EAAC,OAAO;;AAAC,qBAAqB;EAAC,OAAO,EAAC,OAAO;;AAAC,cAAc;EAAC,OAAO,EAAC,OAAO;;AAAC,4EAA0E;EAAC,OAAO,EAAC,OAAO;;AAAC,wBAAwB;EAAC,OAAO,EAAC,OAAO;;AAAC,aAAa;EAAC,OAAO,EAAC,OAAO;;AAAC,oCAAmC;EAAC,OAAO,EAAC,OAAO;;AAAC,uCAAsC;EAAC,OAAO,EAAC,OAAO;;AAAC,2CAA0C;EAAC,OAAO,EAAC,OAAO;;AAAC,kBAAkB;EAAC,OAAO,EAAC,OAAO;;AAAC,sBAAsB;EAAC,OAAO,EAAC,OAAO;;AAAC,iBAAiB;EAAC,OAAO,EAAC,OAAO;;AAAC,oBAAoB;EAAC,OAAO,EAAC,OAAO;;AAAC,kBAAkB;EAAC,OAAO,EAAC,OAAO;;AAAC,oBAAoB;EAAC,OAAO,EAAC,OAAO;;AAAC,2BAA2B;EAAC,OAAO,EAAC,OAAO;;AAAC,eAAe;EAAC,OAAO,EAAC,OAAO;;AAAC,6CAA4C;EAAC,OAAO,EAAC,OAAO;;AAAC,cAAc;EAAC,OAAO,EAAC,OAAO;;AAAC,qBAAqB;EAAC,OAAO,EAAC,OAAO;;AAAC,eAAe;EAAC,OAAO,EAAC,OAAO;;AAAC,qBAAqB;EAAC,OAAO,EAAC,OAAO;;AAAC,iBAAiB;EAAC,OAAO,EAAC,OAAO;;AAAC,eAAe;EAAC,OAAO,EAAC,OAAO;;AAAC,sBAAsB;EAAC,OAAO,EAAC,OAAO;;AAAC,eAAe;EAAC,OAAO,EAAC,OAAO;;AAAC,qBAAqB;EAAC,OAAO,EAAC,OAAO;;AAAC,iBAAiB;EAAC,OAAO,EAAC,OAAO;;AAAC,wBAAwB;EAAC,OAAO,EAAC,OAAO;;AAAC,kBAAkB;EAAC,OAAO,EAAC,OAAO;;AAAC,wBAAwB;EAAC,OAAO,EAAC,OAAO;;AAAC,sBAAsB;EAAC,OAAO,EAAC,OAAO;;AAAC,kBAAkB;EAAC,OAAO,EAAC,OAAO;;AAAC,oBAAoB;EAAC,OAAO,EAAC,OAAO;;AAAC,oBAAoB;EAAC,OAAO,EAAC,OAAO;;AAAC,qBAAqB;EAAC,OAAO,EAAC,OAAO;;AAAC,uBAAuB;EAAC,OAAO,EAAC,OAAO;;AAAC,gBAAgB;EAAC,OAAO,EAAC,OAAO;;AAAC,oBAAoB;EAAC,OAAO,EAAC,OAAO;;AAAC,aAAa;EAAC,OAAO,EAAC,OAAO;;AAAC,qBAAqB;EAAC,OAAO,EAAC,OAAO;;AAAC,sBAAsB;EAAC,OAAO,EAAC,OAAO;;AAAC,wBAAwB;EAAC,OAAO,EAAC,OAAO;;AAAC,qBAAqB;EAAC,OAAO,EAAC,OAAO;;AAAC,oBAAoB;EAAC,OAAO,EAAC,OAAO;;AAAC,qBAAqB;EAAC,OAAO,EAAC,OAAO;;AAAC,iBAAiB;EAAC,OAAO,EAAC,OAAO;;AAAC,wBAAwB;EAAC,OAAO,EAAC,OAAO;;AAAC,qBAAqB;EAAC,OAAO,EAAC,OAAO;;AAAC,oBAAoB;EAAC,OAAO,EAAC,OAAO;;AAAC,kBAAkB;EAAC,OAAO,EAAC,OAAO;;AAAC,cAAc;EAAC,OAAO,EAAC,OAAO;;AAAC,kBAAkB;EAAC,OAAO,EAAC,OAAO;;AAAC,oBAAoB;EAAC,OAAO,EAAC,OAAO;;AAAC,aAAa;EAAC,OAAO,EAAC,OAAO;;AAAC,oDAAkD;EAAC,OAAO,EAAC,OAAO;;AAAC,mBAAmB;EAAC,OAAO,EAAC,OAAO;;AAAC,qBAAqB;EAAC,OAAO,EAAC,OAAO;;AAAC,yBAAyB;EAAC,OAAO,EAAC,OAAO;;AAAC,mBAAmB;EAAC,OAAO,EAAC,OAAO;;AAAC,mBAAmB;EAAC,OAAO,EAAC,OAAO;;AAAC,kBAAkB;EAAC,OAAO,EAAC,OAAO;;AAAC,iBAAiB;EAAC,OAAO,EAAC,OAAO;;AAAC,uBAAuB;EAAC,OAAO,EAAC,OAAO;;AAAC,sBAAsB;EAAC,OAAO,EAAC,OAAO;;AAAC,mBAAmB;EAAC,OAAO,EAAC,OAAO;;AAAC,oBAAoB;EAAC,OAAO,EAAC,OAAO;;AAAC,0BAA0B;EAAC,OAAO,EAAC,OAAO;;AAAC,kBAAkB;EAAC,OAAO,EAAC,OAAO;;AAAC,eAAe;EAAC,OAAO,EAAC,OAAO;;AAAC,sBAAsB;EAAC,OAAO,EAAC,OAAO;;AAAC,qBAAqB;EAAC,OAAO,EAAC,OAAO;;AAAC,sBAAsB;EAAC,OAAO,EAAC,OAAO;;AAAC,oBAAoB;EAAC,OAAO,EAAC,OAAO;;AAAC,gBAAgB;EAAC,OAAO,EAAC,OAAO;;AAAC,eAAe;EAAC,OAAO,EAAC,OAAO;;AAAC,kBAAkB;EAAC,OAAO,EAAC,OAAO;;AAAC,2CAA0C;EAAC,OAAO,EAAC,OAAO;;AAAC,0BAA0B;EAAC,OAAO,EAAC,OAAO;;AAAC,uBAAuB;EAAC,OAAO,EAAC,OAAO;;AAAC,sBAAsB;EAAC,OAAO,EAAC,OAAO;;AAAC,qBAAqB;EAAC,OAAO,EAAC,OAAO;;AAAC,sBAAsB;EAAC,OAAO,EAAC,OAAO;;AAAC,wBAAwB;EAAC,OAAO,EAAC,OAAO;;AAAC,wBAAwB;EAAC,OAAO,EAAC,OAAO;;AAAC,iBAAiB;EAAC,OAAO,EAAC,OAAO;;AAAC,qBAAqB;EAAC,OAAO,EAAC,OAAO;;AAAC,4BAA4B;EAAC,OAAO,EAAC,OAAO;;AAAC,sBAAsB;EAAC,OAAO,EAAC,OAAO;;AAAC,mBAAmB;EAAC,OAAO,EAAC,OAAO;;AAAC,iBAAiB;EAAC,OAAO,EAAC,OAAO;;AAAC,oBAAoB;EAAC,OAAO,EAAC,OAAO;;AAAC,qBAAqB;EAAC,OAAO,EAAC,OAAO;;AAAC,gCAA+B;EAAC,OAAO,EAAC,OAAO;;AAAC,kBAAkB;EAAC,OAAO,EAAC,OAAO;;AAAC,gBAAgB;EAAC,OAAO,EAAC,OAAO;;AAAC,iBAAiB;EAAC,OAAO,EAAC,OAAO;;AAAC,iBAAiB;EAAC,OAAO,EAAC,OAAO;;AAAC,sCAAqC;EAAC,OAAO,EAAC,OAAO;;AAAC,wBAAwB;EAAC,OAAO,EAAC,OAAO;;AAAC,mBAAmB;EAAC,OAAO,EAAC,OAAO;;AAAC,uBAAuB;EAAC,OAAO,EAAC,OAAO;;AAAC,6CAA4C;EAAC,OAAO,EAAC,OAAO;;AAAC,uDAAsD;EAAC,OAAO,EAAC,OAAO;;AAAC,6CAA4C;EAAC,OAAO,EAAC,OAAO;;AAAC,gDAA+C;EAAC,OAAO,EAAC,OAAO;;AAAC,8CAA6C;EAAC,OAAO,EAAC,OAAO;;AAAC,wBAAwB;EAAC,OAAO,EAAC,OAAO;;AAAC,mBAAmB;EAAC,OAAO,EAAC,OAAO;;AAAC,uBAAuB;EAAC,OAAO,EAAC,OAAO;;AAAC,yBAAyB;EAAC,OAAO,EAAC,OAAO;;AAAC,sBAAsB;EAAC,OAAO,EAAC,OAAO;;AAAC,wBAAwB;EAAC,OAAO,EAAC,OAAO;;AAAC,iBAAiB;EAAC,OAAO,EAAC,OAAO;;AAAC,yBAAyB;EAAC,OAAO,EAAC,OAAO;;AAAC,gBAAgB;EAAC,OAAO,EAAC,OAAO;;AAAC,wBAAwB;EAAC,OAAO,EAAC,OAAO;;AAAC,sBAAsB;EAAC,OAAO,EAAC,OAAO;;AAAC,kDAAiD;EAAC,OAAO,EAAC,OAAO;;AAAC,iDAAgD;EAAC,OAAO,EAAC,OAAO;;AAAC,gDAA+C;EAAC,OAAO,EAAC,OAAO;;AAAC,oBAAoB;EAAC,OAAO,EAAC,OAAO;;AAAC,8CAA6C;EAAC,OAAO,EAAC,OAAO;;AAAC,+CAA8C;EAAC,OAAO,EAAC,OAAO;;AAAC,0BAA0B;EAAC,OAAO,EAAC,OAAO;;AAAC,wBAAwB;EAAC,OAAO,EAAC,OAAO;;AAAC,uBAAuB;EAAC,OAAO,EAAC,OAAO;;AAAC,yBAAyB;EAAC,OAAO,EAAC,OAAO;;AAAC,uBAAuB;EAAC,OAAO,EAAC,OAAO;;AAAC,oBAAoB;EAAC,OAAO,EAAC,OAAO;;AAAC,qBAAqB;EAAC,OAAO,EAAC,OAAO;;AAAC,2BAA2B;EAAC,OAAO,EAAC,OAAO;;AAAC,aAAa;EAAC,OAAO,EAAC,OAAO;;AAAC,oBAAoB;EAAC,OAAO,EAAC,OAAO;;AAAC,sBAAsB;EAAC,OAAO,EAAC,OAAO;;AAAC,wBAAwB;EAAC,OAAO,EAAC,OAAO;;AAAC,+BAA+B;EAAC,OAAO,EAAC,OAAO;;AAAC,qBAAqB;EAAC,OAAO,EAAC,OAAO;;AAAC,sBAAsB;EAAC,OAAO,EAAC,OAAO;;AAAC,iBAAiB;EAAC,OAAO,EAAC,OAAO;;AAAC,iBAAiB;EAAC,OAAO,EAAC,OAAO;;AAAC,kBAAkB;EAAC,OAAO,EAAC,OAAO;;AAAC,gBAAgB;EAAC,OAAO,EAAC,OAAO;;AAAC,4BAA4B;EAAC,OAAO,EAAC,OAAO;;AAAC,oCAAmC;EAAC,OAAO,EAAC,OAAO;;AAAC,iBAAiB;EAAC,OAAO,EAAC,OAAO;;AAAC,gBAAgB;EAAC,OAAO,EAAC,OAAO;;AAAC,iBAAiB;EAAC,OAAO,EAAC,OAAO;;AAAC,0BAA0B;EAAC,OAAO,EAAC,OAAO;;AAAC,2BAA2B;EAAC,OAAO,EAAC,OAAO;;AAAC,2BAA2B;EAAC,OAAO,EAAC,OAAO;;AAAC,2BAA2B;EAAC,OAAO,EAAC,OAAO;;AAAC,mBAAmB;EAAC,OAAO,EAAC,OAAO;;AAAC,kBAAkB;EAAC,OAAO,EAAC,OAAO;;AAAC,oBAAoB;EAAC,OAAO,EAAC,OAAO;;AAAC,gBAAgB;EAAC,OAAO,EAAC,OAAO;;AAAC,cAAc;EAAC,OAAO,EAAC,OAAO;;AAAC,qBAAqB;EAAC,OAAO,EAAC,OAAO;;AAAC,uBAAuB;EAAC,OAAO,EAAC,OAAO;;AAAC,gBAAgB;EAAC,OAAO,EAAC,OAAO;;AAAC,gBAAgB;EAAC,OAAO,EAAC,OAAO;;AAAC,oBAAoB;EAAC,OAAO,EAAC,OAAO;;AAAC,oBAAoB;EAAC,OAAO,EAAC,OAAO;;AAAC,uBAAuB;EAAC,OAAO,EAAC,OAAO;;AAAC,eAAe;EAAC,OAAO,EAAC,OAAO;;AAAC,0BAA0B;EAAC,OAAO,EAAC,OAAO;;AAAC,mBAAmB;EAAC,OAAO,EAAC,OAAO;;AAAC,eAAe;EAAC,OAAO,EAAC,OAAO;;AAAC,uBAAuB;EAAC,OAAO,EAAC,OAAO;;AAAC,cAAc;EAAC,OAAO,EAAC,OAAO;;AAAC,uBAAuB;EAAC,OAAO,EAAC,OAAO;;AAAC,mBAAmB;EAAC,OAAO,EAAC,OAAO;;AAAC,iBAAiB;EAAC,OAAO,EAAC,OAAO;;AAAC,uBAAuB;EAAC,OAAO,EAAC,OAAO;;AAAC,yBAAyB;EAAC,OAAO,EAAC,OAAO;;AAAC,sBAAsB;EAAC,OAAO,EAAC,OAAO;;AAAC,wBAAwB;EAAC,OAAO,EAAC,OAAO;;AAAC,uBAAuB;EAAC,OAAO,EAAC,OAAO;;AAAC,0BAA0B;EAAC,OAAO,EAAC,OAAO;;AAAC,kBAAkB;EAAC,OAAO,EAAC,OAAO;;AAAC,oBAAoB;EAAC,OAAO,EAAC,OAAO;;AAAC,sBAAsB;EAAC,OAAO,EAAC,OAAO;;AAAC,kBAAkB;EAAC,OAAO,EAAC,OAAO;;AAAC,iBAAiB;EAAC,OAAO,EAAC,OAAO;;AAAC,qBAAqB;EAAC,OAAO,EAAC,OAAO;;AAAC,kBAAkB;EAAC,OAAO,EAAC,OAAO;;AAAC,iBAAiB;EAAC,OAAO,EAAC,OAAO;;AAAC,2BAA2B;EAAC,OAAO,EAAC,OAAO;;AAAC,yBAAyB;EAAC,OAAO,EAAC,OAAO;;AAAC,4BAA4B;EAAC,OAAO,EAAC,OAAO;;AAAC,gBAAgB;EAAC,OAAO,EAAC,OAAO;;AAAC,4BAA4B;EAAC,OAAO,EAAC,OAAO;;AAAC,+BAA+B;EAAC,OAAO,EAAC,OAAO;;AAAC,kBAAkB;EAAC,OAAO,EAAC,OAAO;;AAAC,sCAAsC;EAAC,OAAO,EAAC,OAAO;;AAAC,2EAA0E;EAAC,OAAO,EAAC,OAAO;;AAAC,gEAA8D;EAAC,OAAO,EAAC,OAAO;;AAAC,gBAAgB;EAAC,OAAO,EAAC,OAAO;;AAAC,kBAAkB;EAAC,OAAO,EAAC,OAAO;;AAAC,4CAA2C;EAAC,OAAO,EAAC,OAAO;;AAAC,qBAAqB;EAAC,OAAO,EAAC,OAAO;;AAAC,iBAAiB;EAAC,OAAO,EAAC,OAAO;;AAAC,wBAAwB;EAAC,OAAO,EAAC,OAAO;;AAAC,mBAAmB;EAAC,OAAO,EAAC,OAAO;;AAAC,yBAAyB;EAAC,OAAO,EAAC,OAAO;;AAAC,0BAA0B;EAAC,OAAO,EAAC,OAAO;;AAAC,qBAAqB;EAAC,OAAO,EAAC,OAAO;;AAAC,sBAAsB;EAAC,OAAO,EAAC,OAAO;;AAAC,gBAAgB;EAAC,OAAO,EAAC,OAAO;;AAAC,oBAAoB;EAAC,OAAO,EAAC,OAAO;;AAAC,8DAA6D;EAAC,OAAO,EAAC,OAAO;;AAAC,sCAAqC;EAAC,OAAO,EAAC,OAAO;;AAAC,QAAQ;EAAC,KAAK,EAAC,GAAG;EAAC,MAAM,EAAC,GAAG;EAAC,MAAM,EAAC,IAAI;EAAC,IAAI,EAAC,gBAAgB;EAAC,IAAI,EAAC,wBAAwB;EAAC,QAAQ,EAAC,QAAQ;EAAC,KAAK,EAAC,IAAI;EAAC,MAAM,EAAC,IAAI;EAAC,MAAM,EAAC,CAAC;EAAC,OAAO,EAAC,CAAC;EAAC,QAAQ,EAAC,MAAM;EAAC,MAAM,EAAC,CAAC;;AAAC,mDAAkD;EAAC,QAAQ,EAAC,MAAM;EAAC,KAAK,EAAC,IAAI;EAAC,MAAM,EAAC,IAAI;EAAC,MAAM,EAAC,CAAC;EAAC,QAAQ,EAAC,OAAO;EAAC,IAAI,EAAC,IAAI;;AAAC,mDAAkD;EAAC,QAAQ,EAAC,MAAM;EAAC,KAAK,EAAC,IAAI;EAAC,MAAM,EAAC,IAAI;EAAC,MAAM,EAAC,CAAC;EAAC,QAAQ,EAAC,OAAO;EAAC,IAAI,EAAC,IAAI;;AAAC,UAAU;EAAC,OAAO,EAAC,eAAe;;AAAC,gBAAgB;EAAC,OAAO,EAAC,eAAe;;AAAC,OAAO;EAAC,OAAO,EAAC,eAAe;;AAAC,0BAAyB;EAAC,OAAO,EAAC,eAAe;;AAAC,wCAA+B;EAAC,OAAO,EAAC,eAAe;;AAAC,uDAAsD;EAAC,MAAM,EAAC,IAAI;EAAC,KAAK,EAAC,IAAI;EAAC,YAAY,EAAC,GAAG;EAAC,QAAQ,EAAC,QAAQ;EAAC,UAAU,EAAC,iBAAiB;;AAAC,8BAA8B;EAAC,gBAAgB,EAAC,wBAAqB;;AAAC,qCAAqC;EAAC,gBAAgB,EAAC,KAAK;;AAAC,2BAA2B;EAAC,OAAO,EAAC,KAAK;EAAC,QAAQ,EAAC,QAAQ;EAAC,OAAO,EAAC,qBAAqB;EAAC,gBAAgB,EAAC,WAAW;EAAC,UAAU,EAAC,qBAAqB;;AAAC,iCAAiC;EAAC,OAAO,EAAC,iBAAiB;EAAC,gBAAgB,EAAC,kBAAe;;AAAC,wBAAwB;EAAC,OAAO,EAAC,iBAAiB;EAAC,gBAAgB,EAAC,iEAA8D;EAAC,UAAU,EAAC,iBAAiB;;AAAC,sBAAsB;EAAC,QAAQ,EAAC,QAAQ;EAAC,GAAG,EAAC,KAAK;EAAC,IAAI,EAAC,MAAM;EAAC,OAAO,EAAC,KAAK;;AAAC,4BAA4B;EAAC,GAAG,EAAC,IAAI;EAAC,IAAI,EAAC,IAAI;;AAAC,uEAAsE;EAAC,gBAAgB,EAAC,kBAAe;EAAC,OAAO,EAAC,EAAE;;AAAC,cAAc;EAAC,KAAK,EAAC,OAAO;;AAAC,WAAW;EAAC,KAAK,EAAC,OAAO;;AAAC,cAAc;EAAC,KAAK,EAAC,OAAO;EAAC,gBAAgB,EAAC,OAAO;;AAAC,aAAa;EAAC,KAAK,EAAC,OAAO;;AAAC,mBAAmB;EAAC,KAAK,EAAC,OAAO;;AAAC,4OAAqO;EAAC,KAAK,EAAC,IAAI;;AAAC,QAAQ;EAAC,IAAI,EAAC,wBAAwB;EAAC,QAAQ,EAAC,QAAQ;EAAC,KAAK,EAAC,IAAI;EAAC,MAAM,EAAC,IAAI;EAAC,MAAM,EAAC,CAAC;EAAC,OAAO,EAAC,CAAC;EAAC,QAAQ,EAAC,MAAM;EAAC,MAAM,EAAC,CAAC;;AAAC,cAAc;EAAC,gBAAgB,EAAC,IAAI;EAAC,aAAa,EAAC,GAAG;EAAC,IAAI,EAAC,IAAI;EAAC,KAAK,EAAC,OAAO;EAAC,OAAO,EAAC,KAAK;EAAC,SAAS,EAAC,IAAI;EAAC,MAAM,EAAC,IAAI;EAAC,WAAW,EAAC,IAAI;EAAC,OAAO,EAAC,UAAU;EAAC,QAAQ,EAAC,QAAQ;EAAC,IAAI,EAAC,GAAG;EAAC,GAAG,EAAC,GAAG;EAAC,eAAe,EAAC,IAAI;EAAC,cAAc,EAAC,IAAI;EAAC,KAAK,EAAC,IAAI;EAAC,OAAO,EAAC,MAAM;;AAAC,aAAa;EAAC,SAAS,EAAC,IAAI;;AAAC,MAAM;EAAC,QAAQ,EAAC,QAAQ;EAAC,UAAU,EAAC,IAAI;EAAC,aAAa,EAAC,GAAG;EAAC,MAAM,EAAC,iBAAiB;EAAC,UAAU,EAAC,CAAC;EAAC,aAAa,EAAC,eAAe;;AAAC,uBAAuB;EAAC,OAAO,EAAC,YAAY;EAAC,UAAU,EAAC,GAAG;;AAAC,mBAAmB;EAAC,QAAQ,EAAC,QAAQ;EAAC,GAAG,EAAC,IAAI;EAAC,KAAK,EAAC,IAAI;EAAC,OAAO,EAAC,EAAE;EAAC,OAAO,EAAC,CAAC;;AAAC,cAAc;EAAC,aAAa,EAAC,GAAG;;AAAC,qBAAqB;EAAC,OAAO,EAAC,EAAE;;AAAC,yBAAwB;EAAC,MAAM;IAAC,OAAO,EAAC,mBAAmB;;EAAC,oBAAoB;IAAC,YAAY,EAAC,CAAC;AAAE,yBAAwB;EAAC,sBAAsB;IAAC,KAAK,EAAC,IAAI;IAAC,KAAK,EAAC,GAAG;;EAAC,mBAAmB;IAAC,GAAG,EAAC,IAAI;IAAC,KAAK,EAAC,GAAG;IAAC,OAAO,EAAC,EAAE;AAAE,yBAAwB;EAAC,oBAAoB;IAAC,SAAS,EAAC,IAAI;AAAE,iBAAiB;EAAC,OAAO,EAAC,KAAK;EAAC,OAAO,EAAC,GAAG;;AAAC,+CAA+C;EAAC,aAAa,EAAC,CAAC;;AAAC,qEAAqE;EAAC,OAAO,EAAC,IAAI;;AAAC,yDAAyD;EAAC,WAAW,EAAC,IAAI;;AAAC,YAAY;EAAC,eAAe,EAAC,QAAQ;;AAAC,iBAAiB;EAAC,KAAK,EAAC,GAAG;;AAAC,oBAAoB;EAAC,KAAK,EAAC,GAAG;EAAC,UAAU,EAAC,MAAM;;AAAC,+BAA+B;EAAC,UAAU,EAAC,kBAAkB;EAAC,aAAa,EAAC,kBAAkB;;AAAC,2CAA2C;EAAC,WAAW,EAAC,kBAAkB;;AAAC,0CAA0C;EAAC,YAAY,EAAC,kBAAkB;;AAAC,qBAAqB;EAAC,QAAQ,EAAC,QAAQ;EAAC,MAAM,EAAC,CAAC;EAAC,UAAU,EAAC,GAAG;EAAC,OAAO,EAAC,CAAC;EAAC,MAAM,EAAC,mBAAmB;EAAC,gBAAgB,EAAC,IAAI;EAAC,aAAa,EAAC,GAAG;EAAC,QAAQ,EAAC,MAAM;EAAC,OAAO,EAAC,EAAE;;AAAC,2BAA2B;EAAC,OAAO,EAAC,KAAK;EAAC,MAAM,EAAC,CAAC;EAAC,OAAO,EAAC,cAAc;EAAC,aAAa,EAAC,mBAAmB;EAAC,MAAM,EAAC,OAAO;;AAAC,sCAAsC;EAAC,MAAM,EAAC,CAAC;;AAAC,iCAAiC;EAAC,gBAAgB,EAAC,IAAI;;AAAC,mCAAmC;EAAC,gBAAgB,EAAC,IAAI;;AAAC,oCAAoC;EAAC,gBAAgB,EAAC,OAAO;EAAC,KAAK,EAAC,IAAI;;AAAC,iCAAiC;EAAC,OAAO,EAAC,KAAK;EAAC,KAAK,EAAC,QAAQ;;AAAC,eAAe;EAAC,OAAO,EAAC,OAAO;;AAAC,iBAAiB;EAAC,OAAO,EAAC,OAAO;;AAAC,YAAY;EAAC,OAAO,EAAC,OAAO;;AAAC,gBAAgB;EAAC,OAAO,EAAC,OAAO;;AAAC,eAAe;EAAC,OAAO,EAAC,OAAO;;AAAC,kBAAkB;EAAC,OAAO,EAAC,OAAO;;AAAC,gBAAgB;EAAC,OAAO,EAAC,OAAO;;AAAC,gBAAgB;EAAC,OAAO,EAAC,OAAO;;AAAC,wBAAwB;EAAC,OAAO,EAAC,OAAO;;AAAC,kBAAkB;EAAC,OAAO,EAAC,OAAO;;AAAC,wBAAwB;EAAC,OAAO,EAAC,OAAO;;AAAC,kBAAkB;EAAC,OAAO,EAAC,OAAO;;AAAC,gBAAgB;EAAC,OAAO,EAAC,OAAO;;AAAC,qBAAqB;EAAC,OAAO,EAAC,OAAO;;AAAC,oBAAoB;EAAC,OAAO,EAAC,OAAO;;AAAC,oBAAoB;EAAC,OAAO,EAAC,OAAO;;AAAC,qBAAqB;EAAC,OAAO,EAAC,OAAO;;AAAC,gBAAgB;EAAC,OAAO,EAAC,OAAO;;AAAC,kBAAkB;EAAC,OAAO,EAAC,OAAO;;AAAC,cAAc;EAAC,OAAO,EAAC,OAAO;;AAAC,qBAAqB;EAAC,OAAO,EAAC,OAAO;;AAAC,cAAc;EAAC,OAAO,EAAC,OAAO;;AAAC,oBAAoB;EAAC,OAAO,EAAC,OAAO;;AAAC,wBAAwB;EAAC,OAAO,EAAC,OAAO;;AAAC,uBAAuB;EAAC,OAAO,EAAC,OAAO;;AAAC,yBAAyB;EAAC,OAAO,EAAC,OAAO;;AAAC,oBAAoB;EAAC,OAAO,EAAC,OAAO;;AAAC,iBAAiB;EAAC,OAAO,EAAC,OAAO;;AAAC,mBAAmB;EAAC,OAAO,EAAC,OAAO;;AAAC,gBAAgB;EAAC,OAAO,EAAC,OAAO;;AAAC,wBAAwB;EAAC,OAAO,EAAC,OAAO;;AAAC,yBAAyB;EAAC,OAAO,EAAC,OAAO;;AAAC,gBAAgB;EAAC,OAAO,EAAC,OAAO;;AAAC,sBAAsB;EAAC,OAAO,EAAC,OAAO;;AAAC,iBAAiB;EAAC,OAAO,EAAC,OAAO;;AAAC,gBAAgB;EAAC,OAAO,EAAC,OAAO;;AAAC,mBAAmB;EAAC,OAAO,EAAC,OAAO;;AAAC,wBAAwB;EAAC,OAAO,EAAC,OAAO;;AAAC,oBAAoB;EAAC,OAAO,EAAC,OAAO;;AAAC,yBAAyB;EAAC,OAAO,EAAC,OAAO;;AAAC,wBAAwB;EAAC,OAAO,EAAC,OAAO;;AAAC,uBAAuB;EAAC,OAAO,EAAC,OAAO;;AAAC,mBAAmB;EAAC,OAAO,EAAC,OAAO;;AAAC,uBAAuB;EAAC,OAAO,EAAC,OAAO;;AAAC,kBAAkB;EAAC,OAAO,EAAC,OAAO;;AAAC,gBAAgB;EAAC,OAAO,EAAC,OAAO;;AAAC,yBAAyB;EAAC,OAAO,EAAC,OAAO;;AAAC,wBAAwB;EAAC,OAAO,EAAC,OAAO;;AAAC,oBAAoB;EAAC,OAAO,EAAC,OAAO;;AAAC,oBAAoB;EAAC,OAAO,EAAC,OAAO;;AAAC,kCAAkC;EAAC,KAAK,EAAC,IAAI;EAAC,KAAK,EAAC,IAAI;EAAC,OAAO,EAAC,CAAC;EAAC,WAAW,EAAC,KAAK;EAAC,WAAW,EAAC,aAAa;EAAC,UAAU,EAAC,MAAM;EAAC,WAAW,EAAC,MAAM;EAAC,MAAM,EAAC,OAAO;EAAC,eAAe,EAAC,OAAO;EAAC,KAAK,EAAC,IAAI;;AAAC,iDAA+C;EAAC,OAAO,EAAC,OAAO;;AAAC,mDAAiD;EAAC,OAAO,EAAC,OAAO;;AAAC,iDAA+C;EAAC,OAAO,EAAC,GAAG;;AAAC,4BAA4B;EAAC,KAAK,EAAC,IAAI;EAAC,KAAK,EAAC,IAAI;;AAAC,8BAA8B;EAAC,OAAO,EAAC,OAAO;EAAC,WAAW,EAAC,MAAM;;AAAC,sEAAqE;EAAC,YAAY,EAAC,IAAI;;AAAC,oCAAoC;EAAC,OAAO,EAAC,IAAI;;AAAC,+BAA+B;EAAC,KAAK,EAAC,IAAI;EAAC,gBAAgB,EAAC,OAAO;;AAAC,4CAA4C;EAAC,KAAK,EAAC,IAAI;;AAAC,gCAAgC;EAAC,UAAU,EAAC,MAAM;EAAC,KAAK,EAAC,OAAO;EAAC,WAAW,EAAC,IAAI;;AAAC,oBAAoB;EAAC,UAAU,EAAC,MAAM;EAAC,YAAY,EAAC,cAAc;;AAAC,qEAAkE;EAAC,WAAW,EAAC,IAAI;EAAC,KAAK,EAAC,IAAI;;AAAC,yBAAyB;EAAC,KAAK,EAAC,IAAI;EAAC,KAAK,EAAC,IAAI;EAAC,OAAO,EAAC,CAAC;EAAC,WAAW,EAAC,KAAK;EAAC,WAAW,EAAC,aAAa;EAAC,WAAW,EAAC,MAAM;EAAC,UAAU,EAAC,MAAM;EAAC,eAAe,EAAC,OAAO;EAAC,MAAM,EAAC,OAAO;EAAC,KAAK,EAAC,IAAI;;AAAC,wDAAsD;EAAC,OAAO,EAAC,OAAO;;AAAC,0DAAwD;EAAC,OAAO,EAAC,OAAO;;AAAC,wDAAsD;EAAC,OAAO,EAAC,GAAG;;AAAC,0BAA0B;EAAC,OAAO,EAAC,KAAK;EAAC,YAAY,EAAC,GAAG;EAAC,QAAQ,EAAC,MAAM;EAAC,aAAa,EAAC,QAAQ;EAAC,WAAW,EAAC,MAAM;;AAAC,kCAAkC;EAAC,OAAO,EAAC,CAAC;;AAAC,8GAAyG;EAAC,gBAAgB,EAAC,OAAO;EAAC,KAAK,EAAC,IAAI;;AAAC,+BAA8B;EAAC,UAAU,EAAC,IAAI;;AAAC,sBAAsB;EAAC,MAAM,EAAC,IAAI;;AAAC,kBAAkB;EAAC,YAAY,EAAC,IAAI;;AAAC,cAAc;EAAC,OAAO,EAAC,IAAI;;AAAC,qBAAqB;EAAC,OAAO,EAAC,OAAO;;AAAC,MAAM;EAAC,gBAAgB,EAAC,kBAAe;;AAAC,8BAA4B;EAAC,QAAQ,EAAC,QAAQ;EAAC,KAAK,EAAC,KAAK;EAAC,GAAG,EAAC,CAAC;EAAC,OAAO,EAAC,CAAC;EAAC,SAAS,EAAC,IAAI;EAAC,KAAK,EAAC,IAAI;EAAC,OAAO,EAAC,EAAE;;AAAC,oCAAkC;EAAC,OAAO,EAAC,CAAC;;AAAC,4CAA2C;EAAC,UAAU,EAAC,KAAK;EAAC,aAAa,EAAC,MAAM;;AAAC,0BAA0B;EAAC,aAAa,EAAC,GAAG;;AAAC,+BAA+B;EAAC,cAAc,EAAC,GAAG;;AAAC,0BAAwB;EAAC,UAAU,EAAC,CAAC;;AAAC,gBAAgB;EAAC,UAAU,EAAC,IAAI;;AAAC,qCAAoC;EAAC,OAAO,EAAC,IAAI;;AAAC,oCAAmC;EAAC,cAAc;IAAC,UAAU,EAAC,MAAM;;EAAC,uBAAuB;IAAC,QAAQ,EAAC,KAAK;IAAC,MAAM,EAAC,IAAI;IAAC,GAAG,EAAC,CAAC;IAAC,KAAK,EAAC,GAAG;IAAC,YAAY,EAAC,CAAC;IAAC,aAAa,EAAC,CAAC;IAAC,UAAU,EAAC,IAAI;;EAAC,0BAA0B;IAAC,YAAY,EAAC,GAAG;;EAAC,iCAAiC;IAAC,WAAW,EAAC,IAAI;;EAAC,6EAA4E;IAAC,WAAW,EAAC,CAAC;IAAC,YAAY,EAAC,CAAC;IAAC,aAAa,EAAC,YAAY;;EAAC,qBAAqB;IAAC,UAAU,EAAC,MAAM;;EAAC,6BAA6B;IAAC,YAAY,EAAC,IAAI;;EAAC,mCAAmC;IAAC,UAAU,EAAC,IAAI;;EAAC,oCAAoC;IAAC,WAAW,EAAC,GAAG;IAAC,YAAY,EAAC,IAAI;;EAAC,6CAA6C;IAAC,IAAI,EAAC,CAAC;;EAAC,uDAAuD;IAAC,KAAK,EAAC,IAAI;;EAAC,sDAAsD;IAAC,IAAI,EAAC,GAAG;;EAAC,sCAAsC;IAAC,IAAI,EAAC,IAAI;;EAAC,gDAAgD;IAAC,KAAK,EAAC,KAAK;;EAAC,+CAA+C;IAAC,aAAa,EAAC,WAAW;IAAC,IAAI,EAAC,CAAC;;EAAC,8BAA8B;IAAC,aAAa,EAAC,IAAI;;EAAC,iDAA+C;IAAC,UAAU,EAAC,IAAI;;EAAC,qCAAqC;IAAC,WAAW,EAAC,IAAI;IAAC,YAAY,EAAC,GAAG;;EAAC,8CAA8C;IAAC,KAAK,EAAC,CAAC;;EAAC,wDAAwD;IAAC,IAAI,EAAC,IAAI;;EAAC,uDAAuD;IAAC,KAAK,EAAC,GAAG;;EAAC,uCAAuC;IAAC,KAAK,EAAC,IAAI;;EAAC,iDAAiD;IAAC,IAAI,EAAC,KAAK;;EAAC,gDAAgD;IAAC,aAAa,EAAC,WAAW;IAAC,KAAK,EAAC,CAAC;;EAAC,iCAAiC;IAAC,OAAO,EAAC,KAAK;IAAC,QAAQ,EAAC,KAAK;IAAC,GAAG,EAAC,CAAC;IAAC,KAAK,EAAC,IAAI;IAAC,MAAM,EAAC,IAAI;IAAC,gBAAgB,EAAC,kBAAe;IAAC,OAAO,EAAC,CAAC;;EAAC,gCAAgC;IAAC,OAAO,EAAC,KAAK;IAAC,QAAQ,EAAC,KAAK;IAAC,GAAG,EAAC,GAAG;IAAC,KAAK,EAAC,UAAU;IAAC,OAAO,EAAC,MAAM;IAAC,UAAU,EAAC,OAAO;IAAC,KAAK,EAAC,IAAI;IAAC,UAAU,EAAC,MAAM;IAAC,OAAO,EAAC,CAAC;;EAAC,uGAAqG;IAAC,MAAM,EAAC,OAAO;;EAAC,4GAAyG;IAAC,kBAAkB,EAAC,iBAAiB;IAAC,aAAa,EAAC,iBAAiB;IAAC,UAAU,EAAC,iBAAiB;AAAE,8CAA6C;EAAC,MAAM,EAAC,CAAC;EAAC,OAAO,EAAC,CAAC;;AAAC,YAAY;EAAC,UAAU,EAAC,KAAK;;AAAC,cAAc;EAAC,YAAY,EAAC,IAAI;;AAAC,YAAY;EAAC,UAAU,EAAC,KAAK;;AAAC,aAAa;EAAC,OAAO,EAAC,YAAY;EAAC,MAAM,EAAC,SAAS;EAAC,aAAa,EAAC,GAAG;EAAC,OAAO,EAAC,OAAO;EAAC,SAAS,EAAC,IAAI;EAAC,WAAW,EAAC,UAAU;EAAC,aAAa,EAAC,GAAG;;AAAC,sBAAsB;EAAC,gBAAgB,EAAC,OAAO;;AAAC,wBAAwB;EAAC,KAAK,EAAC,IAAI;;AAAC,6BAA6B;EAAC,KAAK,EAAC,IAAI;EAAC,gBAAgB,EAAC,IAAI;;AAAC,mCAAmC;EAAC,KAAK,EAAC,OAAO;;AAAC,wBAAwB;EAAC,KAAK,EAAC,IAAI;;AAAC,eAAe;EAAC,MAAM,EAAC,CAAC;;AAAC,kBAAkB;EAAC,OAAO,EAAC,MAAM;;AAAC,kDAAkD;EAAC,gBAAgB,EAAC,OAAO;;AAAC,mBAAmB;EAAC,KAAK,EAAC,IAAI;;AAAC,qBAAqB;EAAC,KAAK,EAAC,OAAO;;AAAC,yBAAuB;EAAC,KAAK,EAAC,IAAI;EAAC,eAAe,EAAC,IAAI;;AAAC,sBAAsB;EAAC,OAAO,EAAC,IAAI;;AAAC,iEAAgE;EAAC,OAAO,EAAC,EAAE;;AAAC,6DAA0D;EAAC,WAAW,EAAC,QAAQ;;AAAC,wBAAwB;EAAC,OAAO,EAAC,YAAY;;AAAC,8BAA8B;EAAC,UAAU,EAAC,GAAG;;AAAC,yBAAwB;EAAC,KAAK;IAAC,UAAU,EAAC,KAAK;AAAE,OAAO;EAAC,WAAW,EAAC,MAAM;;AAAC,iBAAiB;EAAC,KAAK,EAAC,IAAI;EAAC,aAAa,EAAC,KAAK;;AAAC,mCAAkC;EAAC,QAAQ,EAAC,OAAO;;AAAC,wBAAwB;EAAC,WAAW,EAAC,CAAC;EAAC,YAAY,EAAC,CAAC;;AAAC,cAAc;EAAC,UAAU,EAAC,CAAC;EAAC,aAAa,EAAC,IAAI;;AAAC,2CAA0C;EAAC,OAAO,EAAC,GAAG;EAAC,OAAO,EAAC,KAAK;;AAAC,oBAAoB;EAAC,KAAK,EAAC,IAAI;;AAAC,2CAA0C;EAAC,OAAO,EAAC,GAAG;EAAC,OAAO,EAAC,KAAK;;AAAC,oBAAoB;EAAC,KAAK,EAAC,IAAI;;AAAC,yCAAwC;EAAC,OAAO,EAAC,CAAC;EAAC,UAAU,EAAC,MAAM;;AAAC,6CAA4C;EAAC,OAAO,EAAC,YAAY;EAAC,UAAU,EAAC,MAAM;;AAAC,iDAAgD;EAAC,SAAS,EAAC,IAAI;EAAC,UAAU,EAAC,KAAK;;AAAC,4HAAqH;EAAC,KAAK,EAAC,IAAI;;AAAC,4IAAqI;EAAC,SAAS,EAAC,IAAI;;AAAC,gIAAyH;EAAC,KAAK,EAAC,KAAK;;AAAC,gJAAyI;EAAC,SAAS,EAAC,KAAK;;AAAC,4HAAqH;EAAC,KAAK,EAAC,KAAK;;AAAC,4IAAqI;EAAC,SAAS,EAAC,KAAK;;AAAC,mBAAmB;EAAC,YAAY,EAAC,IAAI;;AAAC,oBAAoB;EAAC,aAAa,EAAC,IAAI;;AAAC,cAAc;EAAC,WAAW,EAAC,IAAI;;AAAC,yCAAyC;EAAC,OAAO,EAAC,IAAI;;AAAC,0CAA0C;EAAC,KAAK,EAAC,IAAI;;AAAC,yBAAwB;EAAC,SAAS;IAAC,eAAe,EAAC,SAAS;AAAE,yBAAwB;EAAC,iBAAiB;IAAC,OAAO,EAAC,CAAC;;EAAC,mBAAmB;IAAC,aAAa,EAAC,CAAC;AAAE,yBAAyB;EAAC,OAAO,EAAC,KAAK;EAAC,WAAW,EAAC,MAAM;EAAC,aAAa,EAAC,IAAI;EAAC,WAAW,EAAC,qBAAqB;;AAAC,kCAAkC;EAAC,UAAU,EAAC,KAAK;EAAC,WAAW,EAAC,MAAM;EAAC,YAAY,EAAC,MAAM;EAAC,sBAAsB,EAAC,GAAG;EAAC,uBAAuB,EAAC,GAAG;;AAAC,0CAA0C;EAAC,OAAO,EAAC,OAAO;EAAC,QAAQ,EAAC,QAAQ;EAAC,KAAK,EAAC,IAAI;EAAC,KAAK,EAAC,IAAI;;AAAC,yBAAyB;EAAC,WAAW,EAAC,MAAM;EAAC,OAAO,EAAC,IAAI;EAAC,UAAU,EAAC,IAAI;;AAAC,2BAA2B;EAAC,WAAW,EAAC,MAAM;EAAC,OAAO,EAAC,KAAK;EAAC,MAAM,EAAC,cAAc;EAAC,gBAAgB,EAAC,IAAI;EAAC,yBAAyB,EAAC,GAAG;EAAC,0BAA0B,EAAC,GAAG;;AAAC,wCAAwC;EAAC,OAAO,EAAC,CAAC;;AAAC,2BAA2B;EAAC,aAAa,EAAC,CAAC;;AAAC,iCAAiC;EAAC,MAAM,EAAC,OAAO;;AAAC,kCAAkC;EAAC,OAAO,EAAC,IAAI;;AAAC,uBAAuB;EAAC,aAAa,EAAC,CAAC;;AAAC,cAAc;EAAC,OAAO,EAAC,MAAM;;AAAC,iBAAiB;EAAC,SAAS,EAAC,OAAO;EAAC,UAAU,EAAC,MAAM;;AAAC,cAAc;EAAC,YAAY,EAAC,IAAI;;AAAC,cAAc;EAAC,UAAU,EAAC,MAAM;;AAAC,oCAAoC;EAAC,WAAW,EAAC,IAAI;;AAAC,iDAAiD;EAAC,MAAM,EAAC,OAAO;;AAAC,2DAA2D;EAAC,aAAa,EAAC,GAAG;;AAAC,iEAAiE;EAAC,OAAO,EAAC,OAAO;;AAAC,uDAAuD;EAAC,OAAO,EAAC,OAAO;EAAC,KAAK,EAAC,KAAK;;AAAC,0EAAyE;EAAC,sBAAsB,EAAC,CAAC;EAAC,uBAAuB,EAAC,CAAC;;AAAC,8FAA6F;EAAC,yBAAyB,EAAC,GAAG;EAAC,0BAA0B,EAAC,GAAG;;AAAC,oDAAoD;EAAC,yBAAyB,EAAC,GAAG;EAAC,0BAA0B,EAAC,GAAG;;AAAC,qBAAqB;EAAC,UAAU,EAAC,CAAC;EAAC,YAAY,EAAC,IAAI;EAAC,WAAW,EAAC,MAAM;EAAC,aAAa,EAAC,CAAC;;AAAC,sBAAsB;EAAC,WAAW,EAAC,IAAI;;AAAC,iBAAiB;EAAC,eAAe,EAAC,IAAI;;AAAC,iBAAiB;EAAC,WAAW,EAAC,IAAI;;AAAC,uBAAuB;EAAC,KAAK,EAAC,OAAO;;AAAC,2CAA2C;EAAC,OAAO,EAAC,OAAO;;AAAC,gBAAgB;EAAC,UAAU,EAAC,IAAI;;AAAC,4CAA4C;EAAC,MAAM,EAAC,CAAC;;AAAC,+CAA+C;EAAC,KAAK,EAAC,GAAG;EAAC,MAAM,EAAC,GAAG;EAAC,MAAM,EAAC,GAAG;EAAC,gBAAgB,EAAC,wBAAqB;EAAC,YAAY,EAAC,IAAI;;AAAC,sCAAsC;EAAC,QAAQ,EAAC,QAAQ;EAAC,OAAO,EAAC,KAAK;EAAC,SAAS,EAAC,KAAK;EAAC,UAAU,EAAC,KAAK;EAAC,MAAM,EAAC,IAAI;EAAC,UAAU,EAAC,MAAM;;AAAC,0CAA0C;EAAC,SAAS,EAAC,IAAI;EAAC,MAAM,EAAC,MAAM;;AAAC,+CAA+C;EAAC,QAAQ,EAAC,QAAQ;EAAC,GAAG,EAAC,CAAC;EAAC,IAAI,EAAC,CAAC;EAAC,OAAO,EAAC,IAAI;EAAC,KAAK,EAAC,IAAI;EAAC,MAAM,EAAC,IAAI;EAAC,OAAO,EAAC,WAAW;EAAC,KAAK,EAAC,IAAI;EAAC,gBAAgB,EAAC,kBAAe;;AAAC,qDAAqD;EAAC,OAAO,EAAC,KAAK;;AAAC,6BAA6B;EAAC,OAAO,EAAC,KAAK;;AAAC,iBAAiB;EAAC,OAAO,EAAC,QAAQ;EAAC,UAAU,EAAC,MAAM;;AAAC,2CAA2C;EAAC,KAAK,EAAC,IAAI;;AAAC,+BAA+B;EAAC,UAAU,EAAC,IAAI;EAAC,UAAU,EAAC,gCAA6B;;AAAC,gCAAgC;EAAC,UAAU,EAAC,OAAO;EAAC,gBAAgB,EAAC,IAAI;EAAC,MAAM,EAAC,iBAAiB;EAAC,UAAU,EAAC,IAAI;EAAC,OAAO,EAAC,EAAE;;AAAC,uHAAqH;EAAC,OAAO,EAAC,CAAC;EAAC,UAAU,EAAC,IAAI;EAAC,YAAY,EAAC,IAAI;;AAAC,+EAA8E;EAAC,YAAY,EAAC,OAAO;;AAAC,mCAAmC;EAAC,UAAU,EAAC,IAAI;EAAC,UAAU,EAAC,iCAA8B;;AAAC,uBAAuB;EAAC,OAAO,EAAC,IAAI;;AAAC,mBAAmB;EAAC,eAAe,EAAC,SAAS;;AAAC,8DAA4D;EAAC,KAAK,EAAC,IAAI;;AAAC,qBAAqB;EAAC,OAAO,EAAC,IAAI;;AAAC,cAAc;EAAC,MAAM,EAAC,CAAC;;AAAC,gBAAe;EAAC,UAAU,EAAC,IAAI;EAAC,OAAO,EAAC,SAAS;;AAAC,SAAS;EAAC,gBAAgB,EAAC,IAAI;;AAAC,GAAG;EAAC,SAAS,EAAC,IAAI;;AAAC,QAAQ;EAAC,KAAK,EAAC,KAAK;;AAAC,eAAe;EAAC,OAAO,EAAC,QAAQ;EAAC,WAAW,EAAC,MAAM;;AAAC,kBAAkB;EAAC,OAAO,EAAC,YAAY;;AAAC,8BAA4B;EAAC,YAAY,EAAC,GAAG;EAAC,aAAa,EAAC,GAAG;EAAC,KAAK,EAAC,IAAI;EAAC,OAAO,EAAC,QAAQ;;AAAC,YAAY;EAAC,OAAO,EAAC,GAAG;;AAAC,yBAAwB;EAAC,wBAAwB;IAAC,aAAa,EAAC,CAAC;;EAAC,WAAW;IAAC,UAAU,EAAC,CAAC;AAAE,yBAAwB;EAAC,EAAE;IAAC,SAAS,EAAC,IAAI;IAAC,WAAW,EAAC,MAAM;;EAAC,EAAE;IAAC,SAAS,EAAC,IAAI;IAAC,WAAW,EAAC,MAAM;;EAAC,aAAa;IAAC,SAAS,EAAC,KAAK;AAAE,yBAAwB;EAAC,EAAE;IAAC,SAAS,EAAC,IAAI;;EAAC,EAAE;IAAC,SAAS,EAAC,IAAI;;EAAC,WAAW;IAAC,WAAW,EAAC,CAAC;AAAE,UAAU;EAAC,aAAa,EAAC,CAAC;;AAAC,UAAU;EAAC,aAAa,EAAC,CAAC;;AAAC,qQAA4P;EAAC,KAAK,EAAC,OAAO;;AAAC,wBAAwB;EAAC,YAAY,EAAC,OAAO;EAAC,kBAAkB,EAAC,oCAAiC;EAAC,UAAU,EAAC,oCAAiC;;AAAC,8BAA8B;EAAC,YAAY,EAAC,OAAO;EAAC,kBAAkB,EAAC,qDAAiD;EAAC,UAAU,EAAC,qDAAiD;;AAAC,6BAA6B;EAAC,KAAK,EAAC,OAAO;EAAC,YAAY,EAAC,OAAO;EAAC,gBAAgB,EAAC,OAAO;;AAAC,iCAAiC;EAAC,KAAK,EAAC,OAAO;;AAAC,uBAAuB;EAAC,MAAM,EAAC,CAAC;EAAC,WAAW,EAAC,GAAG;EAAC,cAAc,EAAC,GAAG;;AAAC,6BAA6B;EAAC,OAAO,EAAC,CAAC;;AAAC,QAAQ;EAAC,KAAK,EAAC,IAAI;;AAAC,mCAAmC;EAAC,SAAS,EAAC,UAAU;;AAAC,+CAA+C;EAAC,OAAO,EAAC,OAAO;EAAC,SAAS,EAAC,GAAG;EAAC,UAAU,EAAC,KAAK;EAAC,UAAU,EAAC,CAAC;;AAAC,wBAAwB;EAAC,WAAW,EAAC,KAAK;EAAC,aAAa,EAAC,GAAG;;AAAC,qBAAqB;EAAC,gBAAgB,EAAC,IAAI;EAAC,IAAI,EAAC,OAAO;EAAC,aAAa,EAAC,OAAO;EAAC,MAAM,EAAC,OAAO;;AAAC,MAAM;EAAC,YAAY,EAAC,KAAK;EAAC,SAAS,EAAC,UAAU;;AAAC,KAAK;EAAC,QAAQ,EAAC,QAAQ;EAAC,UAAU,EAAC,WAAW;EAAC,MAAM,EAAC,IAAI;EAAC,QAAQ,EAAC,MAAM;EAAC,IAAI,EAAC,eAAe;EAAC,WAAW,EAAC,IAAI;EAAC,MAAM,EAAC,eAAe;;AAAC,SAAS;EAAC,UAAU,EAAC,CAAC;;AAAC,SAAS;EAAC,MAAM,EAAC,eAAe;;AAAC,YAAY;EAAC,QAAQ,EAAC,QAAQ;EAAC,MAAM,EAAC,CAAC;EAAC,IAAI,EAAC,CAAC;EAAC,UAAU,EAAC,GAAG;EAAC,OAAO,EAAC,OAAO;EAAC,SAAS,EAAC,GAAG;EAAC,gBAAgB,EAAC,kBAAe;EAAC,aAAa,EAAC,CAAC;EAAC,WAAW,EAAC,IAAI;;AAAC,UAAU;EAAC,KAAK,EAAC,IAAI;;AAAC,iBAAiB;EAAC,WAAW,EAAC,KAAK;;AAAC,gEAA+D;EAAC,OAAO,EAAC,IAAI;;AAAC,8EAA6E;EAAC,OAAO,EAAC,EAAE;;AAAC,8mDAAogD;EAAC,mBAAmB,EAAC,aAAa;EAAC,iBAAiB,EAAC,SAAS;EAAC,KAAK,EAAC,WAAW;EAAC,OAAO,EAAC,EAAE;EAAC,OAAO,EAAC,YAAY;EAAC,MAAM,EAAC,IAAI;EAAC,MAAM,EAAC,CAAC;EAAC,OAAO,EAAC,CAAC;EAAC,WAAW,EAAC,IAAI;EAAC,cAAc,EAAC,WAAW;EAAC,KAAK,EAAC,IAAI;;AAAC,KAAK;EAAC,gBAAgB,EAAC,qCAAqC;;AAAC,YAAY;EAAC,gBAAgB,EAAC,kCAAkC;;AAAC,aAAa;EAAC,gBAAgB,EAAC,+BAA+B;;AAAC,UAAU;EAAC,gBAAgB,EAAC,8BAA8B;;AAAC,SAAS;EAAC,gBAAgB,EAAC,+BAA+B;;AAAC,SAAS;EAAC,gBAAgB,EAAC,+BAA+B;;AAAC,gBAAgB;EAAC,gBAAgB,EAAC,sCAAsC;;AAAC,mBAAmB;EAAC,gBAAgB,EAAC,yCAAyC;;AAAC,kBAAkB;EAAC,gBAAgB,EAAC,wCAAwC;;AAAC,aAAa;EAAC,gBAAgB,EAAC,uCAAuC;;AAAC,YAAY;EAAC,gBAAgB,EAAC,mCAAmC;;AAAC,qBAAqB;EAAC,gBAAgB,EAAC,wCAAwC;;AAAC,0CAA0C;EAAC,gBAAgB,EAAC,wCAAwC;;AAAC,iBAAiB;EAAC,gBAAgB,EAAC,qCAAqC;;AAAC,sCAAsC;EAAC,gBAAgB,EAAC,qCAAqC;;AAAC,UAAU;EAAC,gBAAgB,EAAC,6BAA6B;;AAAC,UAAU;EAAC,gBAAgB,EAAC,oCAAoC;;AAAC,kBAAkB;EAAC,gBAAgB,EAAC,iCAAiC;;AAAC,YAAY;EAAC,gBAAgB,EAAC,mCAAmC;;AAAC,UAAU;EAAC,gBAAgB,EAAC,gCAAgC;;AAAC,kBAAkB;EAAC,gBAAgB,EAAC,6BAA6B;;AAAC,YAAY;EAAC,gBAAgB,EAAC,gCAAgC;;AAAC,UAAU;EAAC,gBAAgB,EAAC,yCAAyC;;AAAC,SAAS;EAAC,gBAAgB,EAAC,+BAA+B;;AAAC,eAAe;EAAC,gBAAgB,EAAC,gCAAgC;;AAAC,0CAAwC;EAAC,gBAAgB,EAAC,gCAAgC;;AAAC,aAAa;EAAC,gBAAgB,EAAC,wCAAwC;;AAAC,kBAAkB;EAAC,gBAAgB,EAAC,kCAAkC;;AAAC,cAAc;EAAC,gBAAgB,EAAC,+BAA+B;;AAAC,SAAS;EAAC,gBAAgB,EAAC,mCAAmC;;AAAC,cAAc;EAAC,gBAAgB,EAAC,2CAA2C;;AAAC,eAAe;EAAC,gBAAgB,EAAC,+BAA+B;;AAAC,UAAU;EAAC,gBAAgB,EAAC,gCAAgC;;AAAC,SAAS;EAAC,gBAAgB,EAAC,oCAAoC;;AAAC,UAAU;EAAC,gBAAgB,EAAC,gCAAgC;;AAAC,SAAS;EAAC,gBAAgB,EAAC,gCAAgC;;AAAC,UAAU;EAAC,gBAAgB,EAAC,8BAA8B;;AAAC,YAAY;EAAC,gBAAgB,EAAC,+BAA+B;;AAAC,QAAQ;EAAC,gBAAgB,EAAC,oCAAoC;;AAAC,sBAAqB;EAAC,gBAAgB,EAAC,sCAAsC;;AAAC,SAAS;EAAC,gBAAgB,EAAC,oCAAoC;;AAAC,0BAAyB;EAAC,gBAAgB,EAAC,0CAA0C;;AAAC,SAAS;EAAC,gBAAgB,EAAC,+BAA+B;;AAAC,eAAe;EAAC,gBAAgB,EAAC,iCAAiC;;AAAC,QAAQ;EAAC,gBAAgB,EAAC,8BAA8B;;AAAC,cAAc;EAAC,gBAAgB,EAAC,+BAA+B;;AAAC,kCAAiC;EAAC,gBAAgB,EAAC,iCAAiC;;AAAC,WAAW;EAAC,gBAAgB,EAAC,gCAAgC;;AAAC,kBAAkB;EAAC,gBAAgB,EAAC,qCAAqC;;AAAC,iBAAiB;EAAC,gBAAgB,EAAC,gCAAgC;;AAAC,mBAAmB;EAAC,gBAAgB,EAAC,gCAAgC;;AAAC,cAAc;EAAC,gBAAgB,EAAC,oCAAoC;;AAAC,OAAO;EAAC,gBAAgB,EAAC,+BAA+B;;AAAC,WAAW;EAAC,gBAAgB,EAAC,mCAAmC;;AAAC,aAAa;EAAC,gBAAgB,EAAC,qCAAqC;;AAAC,UAAU;EAAC,gBAAgB,EAAC,gCAAgC;;AAAC,kBAAkB;EAAC,gBAAgB,EAAC,+BAA+B;;AAAC,mBAAmB;EAAC,gBAAgB,EAAC,8BAA8B;;AAAC,SAAS;EAAC,gBAAgB,EAAC,8BAA8B;;AAAC,gBAAgB;EAAC,gBAAgB,EAAC,8BAA8B;;AAAC,UAAU;EAAC,gBAAgB,EAAC,kCAAkC;;AAAC,WAAW;EAAC,gBAAgB,EAAC,iCAAiC;;AAAC,WAAW;EAAC,gBAAgB,EAAC,iCAAiC;;AAAC,UAAU;EAAC,gBAAgB,EAAC,gCAAgC;;AAAC,cAAc;EAAC,gBAAgB,EAAC,mCAAmC;;AAAC,WAAW;EAAC,gBAAgB,EAAC,iCAAiC;;AAAC,QAAQ;EAAC,gBAAgB,EAAC,+BAA+B;;AAAC,SAAS;EAAC,gBAAgB,EAAC,+BAA+B;;AAAC,WAAW;EAAC,gBAAgB,EAAC,oCAAoC;;AAAC,gBAAgB;EAAC,gBAAgB,EAAC,gCAAgC;;AAAC,WAAW;EAAC,gBAAgB,EAAC,2CAA2C;;AAAC,kBAAkB;EAAC,gBAAgB,EAAC,+BAA+B;;AAAC,YAAY;EAAC,gBAAgB,EAAC,kCAAkC;;AAAC,aAAa;EAAC,gBAAgB,EAAC,mCAAmC;;AAAC,UAAU;EAAC,gBAAgB,EAAC,+BAA+B;;AAAC,aAAa;EAAC,gBAAgB,EAAC,mCAAmC;;AAAC,kBAAkB;EAAC,gBAAgB,EAAC,gCAAgC;;AAAC,cAAc;EAAC,gBAAgB,EAAC,6BAA6B;;AAAC,mBAAmB;EAAC,gBAAgB,EAAC,gCAAgC;;AAAC,YAAY;EAAC,gBAAgB,EAAC,uCAAuC;;AAAC,SAAS;EAAC,gBAAgB,EAAC,+BAA+B;;AAAC,mBAAmB;EAAC,gBAAgB,EAAC,wCAAwC;;AAAC,aAAa;EAAC,gBAAgB,EAAC,kCAAkC;;AAAC,kBAAkB;EAAC,gBAAgB,EAAC,gCAAgC;;AAAC,iBAAiB;EAAC,gBAAgB,EAAC,gCAAgC;;AAAC,aAAa;EAAC,gBAAgB,EAAC,+BAA+B;;AAAC,iBAAiB;EAAC,gBAAgB,EAAC,+BAA+B;;AAAC,wBAAuB;EAAC,gBAAgB,EAAC,8BAA8B;;AAAC,SAAS;EAAC,gBAAgB,EAAC,sCAAsC;;AAAC,eAAe;EAAC,gBAAgB,EAAC,oCAAoC;;AAAC,YAAY;EAAC,gBAAgB,EAAC,qCAAqC;;AAAC,QAAQ;EAAC,gBAAgB,EAAC,uCAAuC;;AAAC,SAAS;EAAC,gBAAgB,EAAC,+BAA+B;;AAAC,UAAU;EAAC,gBAAgB,EAAC,qCAAqC;;AAAC,mBAAmB;EAAC,gBAAgB,EAAC,qCAAqC;;AAAC,kBAAkB;EAAC,gBAAgB,EAAC,qCAAqC;;AAAC,cAAc;EAAC,gBAAgB,EAAC,6BAA6B;;AAAC,cAAc;EAAC,gBAAgB,EAAC,+BAA+B;;AAAC,WAAW;EAAC,gBAAgB,EAAC,sCAAsC;;AAAC,2BAA2B;EAAC,gBAAgB,EAAC,sCAAsC;;AAAC,kBAAkB;EAAC,gBAAgB,EAAC,uCAAuC;;AAAC,4BAA4B;EAAC,gBAAgB,EAAC,oCAAoC;;AAAC,kBAAkB;EAAC,KAAK,EAAC,IAAI;EAAC,MAAM,EAAC,QAAQ;EAAC,KAAK,EAAC,GAAG;;AAAC,sBAAsB;EAAC,MAAM,EAAC,CAAC;;AAAC,aAAa;EAAC,UAAU,EAAC,IAAI;;AAAC,qCAAoC;EAAC,YAAY,EAAC,OAAO;;AAAC,iBAAiB;EAAC,MAAM,EAAC,WAAW;EAAC,OAAO,EAAC,CAAC;;AAAC,6FAA2F;EAAC,SAAS,EAAC,IAAI;EAAC,MAAM,EAAC,IAAI;EAAC,OAAO,EAAC,OAAO;;AAAC,iHAA+G;EAAC,MAAM,EAAC,IAAI;;AAAC,yBAAwB;EAAC,+BAA+B;IAAC,KAAK,EAAC,KAAK;AAAE,2BAA2B;EAAC,aAAa,EAAC,CAAC;EAAC,OAAO,EAAC,KAAK;;AAAC,gCAAgC;EAAC,aAAa,EAAC,IAAI;EAAC,aAAa,EAAC,CAAC;EAAC,cAAc,EAAC,GAAG;;AAAC,sCAAsC;EAAC,UAAU,EAAC,GAAG;EAAC,YAAY,EAAC,WAAW;EAAC,eAAe,EAAC,SAAS;;AAAC,sFAAqF;EAAC,UAAU,EAAC,IAAI;EAAC,YAAY,EAAC,OAAO;EAAC,aAAa,EAAC,CAAC;EAAC,eAAe,EAAC,IAAI;EAAC,OAAO,EAAC,CAAC;;AAAC,IAAI;EAAC,UAAU,EAAC,OAAO;EAAC,SAAS,EAAC,IAAI;;AAAC,UAAU;EAAC,UAAU,EAAC,IAAI;EAAC,OAAO,EAAC,CAAC;;AAAC,gBAAgB;EAAC,OAAO,EAAC,UAAU;;AAAC,YAAW;EAAC,KAAK,EAAC,IAAI;;AAAC,wBAAuB;EAAC,KAAK,EAAC,IAAI;;AAAC,MAAM;EAAC,OAAO,EAAC,GAAG;;AAAC,IAAI;EAAC,OAAO,EAAC,WAAW;;AAAC,gBAAgB;EAAC,UAAU,EAAC,IAAI;EAAC,gBAAgB,EAAC,2BAA2B;EAAC,MAAM,EAAC,cAAc;EAAC,KAAK,EAAC,IAAI;EAAC,WAAW,EAAC,YAAY;;AAAC,sBAAsB;EAAC,WAAW,EAAC,IAAI;;AAAC,8BAA6B;EAAC,YAAY,EAAC,OAAO;;AAAC,0BAAyB;EAAC,YAAY,EAAC,OAAO;;AAAC,gCAA+B;EAAC,YAAY,EAAC,OAAO;;AAAC,gCAA+B;EAAC,YAAY,EAAC,OAAO;;AAAC,gCAA+B;EAAC,YAAY,EAAC,OAAO;;AAAC,qCAAqC;EAAC,UAAU,EAAC,IAAI;;AAAC,qBAAqB;EAAC,MAAM,EAAC,OAAO;;AAAC,gBAAgB;EAAC,UAAU,EAAC,IAAI;;AAAC,EAAE;EAAC,MAAM,EAAC,SAAS;;AAAC,uCAAsC;EAAC,MAAM,EAAC,UAAU;EAAC,OAAO,EAAC,KAAK;;AAAC,aAAS;EAAC,OAAO,EAAC,QAAQ;;AAAC,UAAU;EAAC,OAAO,EAAC,KAAK;EAAC,MAAM,EAAC,MAAM;;AAAC,OAAO;EAAC,UAAU,EAAC,GAAG;;AAAC,YAAY;EAAC,UAAU,EAAC,GAAG;EAAC,aAAa,EAAC,GAAG;;AAAC,WAAW;EAAC,OAAO,EAAC,KAAK;EAAC,MAAM,EAAC,SAAS;;AAAC,6CAAoC;EAAC,OAAO,EAAC,YAAY;;AAAC,iMAAoK;EAAC,UAAU,EAAC,OAAO;EAAC,YAAY,EAAC,OAAO;;AAAC,cAAc;EAAC,OAAO,EAAC,CAAC;;AAAC,gBAAgB;EAAC,MAAM,EAAC,OAAO;EAAC,OAAO,EAAC,YAAY;EAAC,OAAO,EAAC,GAAG;EAAC,KAAK,EAAC,IAAI;;AAAC,kBAAkB;EAAC,OAAO,EAAC,OAAO;EAAC,MAAM,EAAC,MAAM;;AAAC,QAAM;EAAC,OAAO,EAAC,KAAK;;AAAC,eAAe;EAAC,OAAO,EAAC,KAAK;;AAAC,YAAY;EAAC,OAAO,EAAC,OAAO;EAAC,MAAM,EAAC,cAAc;EAAC,UAAU,EAAC,CAAC;;AAAC,yBAAwB;EAAC,IAAI;IAAC,OAAO,EAAC,GAAG;;EAAC,MAAM;IAAC,UAAU,EAAC,CAAC;;EAAC,yCAAuC;IAAC,SAAS,EAAC,GAAG;AAAE,yBAAwB;EAAC,MAAM;IAAC,SAAS,EAAC,GAAG;IAAC,UAAU,EAAC,GAAG;;EAAC,yCAAuC;IAAC,WAAW,EAAC,IAAI;;EAAC,aAAa;IAAC,KAAK,EAAC,KAAK;AAAE,MAAM;EAAC,UAAU,EAAC,IAAI;;AAAC,kBAAkB;EAAC,SAAS,EAAC,IAAI;;AAAC,cAAc;EAAC,aAAa,EAAC,WAAW;EAAC,OAAO,EAAC,MAAM;;AAAC,0BAA0B;EAAC,OAAO,EAAC,eAAe;;AAAC,4BAA4B;EAAC,MAAM,EAAC,IAAI;EAAC,KAAK,EAAC,KAAK;EAAC,UAAU,EAAC,GAAG;EAAC,KAAK,EAAC,WAAW;EAAC,gBAAgB,EAAC,gCAAgC;EAAC,mBAAmB,EAAC,aAAa;EAAC,iBAAiB,EAAC,SAAS;EAAC,eAAe,EAAC,OAAO;;AAAC,2GAAyG;EAAC,KAAK,EAAC,WAAW;;AAAC,oCAAoC;EAAC,gBAAgB,EAAC,mCAAmC;;AAAC,mCAA+B;EAAC,OAAO,EAAC,QAAQ;;AAAC,4BAA4B;EAAC,UAAU,EAAC,IAAI;;AAAC,yBAAwB;EAAC,mCAA+B;IAAC,OAAO,EAAC,QAAQ;;EAAC,4BAA4B;IAAC,MAAM,EAAC,CAAC;AAAE,iBAAiB;EAAC,UAAU,EAAC,sCAAsC;EAAC,OAAO,EAAC,gBAAgB;;AAAC,8BAA8B;EAAC,MAAM,EAAC,CAAC;;AAAC,2CAA2C;EAAC,MAAM,EAAC,cAAc;;AAAC,yBAAwB;EAAC,wCAAwC;IAAC,UAAU,EAAC,KAAK;;EAAC,uBAAuB;IAAC,UAAU,EAAC,GAAG;IAAC,aAAa,EAAC,GAAG;AAAE,kBAAkB;EAAC,MAAM,EAAC,cAAc;EAAC,aAAa,EAAC,CAAC;EAAC,YAAY,EAAC,KAAK;EAAC,SAAS,EAAC,IAAI;EAAC,aAAa,EAAC,GAAG;EAAC,OAAO,EAAC,YAAY;;AAAC,MAAM;EAAC,aAAa,EAAC,IAAI;;AAAC,iBAAiB;EAAC,aAAa,EAAC,WAAW;EAAC,UAAU,EAAC,cAAc;EAAC,WAAW,EAAC,IAAI;;AAAC,SAAS;EAAC,OAAO,EAAC,IAAI;;AAAC,QAAQ;EAAC,MAAM,EAAC,CAAC;;AAAC,SAAS;EAAC,YAAY,EAAC,IAAI;;AAAC,+BAA+B;EAAC,MAAM,EAAC,CAAC;;AAAC,gCAAgC;EAAC,cAAc,EAAC,IAAI;EAAC,SAAS,EAAC,IAAI;EAAC,UAAU,EAAC,IAAI;EAAC,UAAU,EAAC,YAAY;EAAC,KAAK,EAAC,OAAO;;AAAC,mCAAmC;EAAC,KAAK,EAAC,IAAI;;AAAC,8BAA8B;EAAC,KAAK,EAAC,IAAI;;AAAC,uCAAuC;EAAC,UAAU,EAAC,IAAI;;AAAC,SAAS;EAAC,UAAU,EAAC,IAAI;EAAC,OAAO,EAAC,CAAC;EAAC,MAAM,EAAC,CAAC;EAAC,UAAU,EAAC,OAAO;;AAAC,YAAY;EAAC,cAAc,EAAC,IAAI;;AAAC,gBAAgB;EAAC,MAAM,EAAC,UAAU;;AAAC,gCAA+B;EAAC,UAAU,EAAC,MAAM;;AAAC,sBAAsB;EAAC,MAAM,EAAC,MAAM;;AAAC,wBAAwB;EAAC,OAAO,EAAC,KAAK;;AAAC,8BAA8B;EAAC,OAAO,EAAC,IAAI;;AAAC,yCAAyC;EAAC,MAAM,EAAC,cAAc;EAAC,OAAO,EAAC,GAAG;EAAC,MAAM,EAAC,KAAK;;AAAC,kDAAkD;EAAC,MAAM,EAAC,MAAM;;AAAC,kCAA8B;EAAC,OAAO,EAAC,YAAY;EAAC,KAAK,EAAC,IAAI;EAAC,MAAM,EAAC,cAAc;;AAAC,eAAe;EAAC,UAAU,EAAC,CAAC;EAAC,WAAW,EAAC,CAAC;EAAC,cAAc,EAAC,GAAG;EAAC,SAAS,EAAC,GAAG;EAAC,cAAc,EAAC,OAAO;;AAAC,yBAAyB;EAAC,UAAU,EAAC,GAAG;;AAAC,kBAAkB;EAAC,aAAa,EAAC,GAAG;;AAAC,OAAO;EAAC,OAAO,EAAC,IAAI;EAAC,WAAW,EAAC,OAAO;;AAAC,uBAAuB;EAAC,gBAAgB,EAAC,IAAI;;AAAC,kCAAkC;EAAC,UAAU,EAAC,IAAI;EAAC,WAAW,EAAC,KAAK;EAAC,cAAc,EAAC,KAAK;;AAAC,WAAS;EAAC,OAAO,EAAC,KAAK;;AAAC,cAAc;EAAC,OAAO,EAAC,YAAY;EAAC,aAAa,EAAC,GAAG;;AAAC,+BAA+B;EAAC,UAAU,EAAC,IAAI;;AAAC,cAAc;EAAC,MAAM,EAAC,CAAC;;AAAC,YAAY;EAAC,OAAO,EAAC,CAAC;;AAAC,mBAAmB;EAAC,MAAM,EAAC,OAAO;EAAC,OAAO,EAAC,aAAa;;AAAC,sBAAsB;EAAC,YAAY,EAAC,IAAI;;AAAC,sBAAsB;EAAC,UAAU,EAAC,IAAI;;AAAC,yBAAwB;EAAC,qCAAqC;IAAC,UAAU,EAAC,IAAI;;EAAC,gBAAgB;IAAC,MAAM,EAAC,QAAQ;IAAC,OAAO,EAAC,KAAK;AAAE,oBAAoB;EAAC,aAAa,EAAC,GAAG;;AAAC,0CAA0C;EAAC,YAAY,EAAC,IAAI;;AAAC,+DAA+D;EAAC,UAAU,EAAC,GAAG;;AAAC,yBAAyB;EAAC,OAAO,EAAC,YAAY;;AAAC,gCAAgC;EAAC,KAAK,EAAC,IAAI;;AAAC,uCAAuC;EAAC,KAAK,EAAC,IAAI;;AAAC,wEAAuE;EAAC,UAAU,EAAC,IAAI;EAAC,YAAY,EAAC,IAAI;;AAAC,kCAAkC;EAAC,KAAK,EAAC,IAAI;;AAAC,0BAA0B;EAAC,MAAM,EAAC,aAAa;EAAC,KAAK,EAAC,GAAG;;AAAC,yCAAyC;EAAC,UAAU,EAAC,OAAO;EAAC,OAAO,EAAC,CAAC;;AAAC,iBAAiB;EAAC,KAAK,EAAC,IAAI;;AAAC,uBAAuB;EAAC,KAAK,EAAC,OAAO",
+"sources": ["../css/compiled.css"],
+"names": [],
+"file": "compiled.scss"
+}
diff --git a/themes/bootprint3/scss/icons.scss b/themes/bootprint3/scss/icons.scss
new file mode 100644
index 0000000000000000000000000000000000000000..10dbd116a5aa27fc2c4f221bf91365b73c575960
--- /dev/null
+++ b/themes/bootprint3/scss/icons.scss
@@ -0,0 +1,123 @@
+$bp3-icon-path: '../../bootprint3/images/icons' !default;
+.bp-icon {
+  background-position: center center;
+  background-repeat: no-repeat;
+  color: transparent;
+  content: '';
+  display: inline-block;
+  height: 16px;
+  margin: 0;
+  padding: 0;
+  text-shadow: none;
+  vertical-align: text-bottom;
+  width: 16px;
+}
+.fa-x { background-image: unquote("url('#{$bp3-icon-path}/page_white.png')"); @extend .bp-icon; }
+i.fa-archive { background-image: unquote("url('#{$bp3-icon-path}/package.png')"); @extend .bp-icon; }
+i.fa-asterisk { background-image: unquote("url('#{$bp3-icon-path}/list.png')"); @extend .bp-icon; }
+i.fa-atlas { background-image: unquote("url('#{$bp3-icon-path}/map.png')"); @extend .bp-icon; }
+i.fa-bell { background-image: unquote("url('#{$bp3-icon-path}/bell.png')"); @extend .bp-icon; }
+i.fa-book { background-image: unquote("url('#{$bp3-icon-path}/book.png')"); @extend .bp-icon; }
+i.fa-bookbag-add { background-image: unquote("url('#{$bp3-icon-path}/bookbag_add.png')"); @extend .bp-icon; }
+i.fa-bookbag-delete { background-image: unquote("url('#{$bp3-icon-path}/bookbag_delete.png')"); @extend .bp-icon; }
+i.fa-bookbag-empty { background-image: unquote("url('#{$bp3-icon-path}/bookbag_empty.png')"); @extend .bp-icon; }
+i.fa-bookmark { background-image: unquote("url('#{$bp3-icon-path}/bookmark_add.png')"); @extend .bp-icon; }
+i.fa-braille { background-image: unquote("url('#{$bp3-icon-path}/page_red.png')"); @extend .bp-icon; }
+i.fa-cancel-all-holds { background-image: unquote("url('#{$bp3-icon-path}/holdCancelAll.png')"); @extend .bp-icon; }
+i.fa-cancel-all-storage-retrieval-requests { background-image: unquote("url('#{$bp3-icon-path}/holdCancelAll.png')"); @extend .bp-icon; }
+i.fa-cancel-holds { background-image: unquote("url('#{$bp3-icon-path}/holdCancel.png')"); @extend .bp-icon; }
+i.fa-cancel-storage-retrieval-requests { background-image: unquote("url('#{$bp3-icon-path}/holdCancel.png')"); @extend .bp-icon; }
+i.fa-cdrom { background-image: unquote("url('#{$bp3-icon-path}/cd.png')"); @extend .bp-icon; }
+i.fa-chart { background-image: unquote("url('#{$bp3-icon-path}/chart_bar.png')"); @extend .bp-icon; }
+i.fa-chipcartridge { background-image: unquote("url('#{$bp3-icon-path}/server.png')"); @extend .bp-icon; }
+i.fa-collage { background-image: unquote("url('#{$bp3-icon-path}/pictures.png')"); @extend .bp-icon; }
+i.fa-close { background-image: unquote("url('#{$bp3-icon-path}/cross.png')"); @extend .bp-icon; }
+i.fa-disccartridge { background-image: unquote("url('#{$bp3-icon-path}/cd.png')"); @extend .bp-icon; }
+i.fa-drawing { background-image: unquote("url('#{$bp3-icon-path}/photo.png')"); @extend .bp-icon; }
+i.fa-ebook { background-image: unquote("url('#{$bp3-icon-path}/book_addresses.png')"); @extend .bp-icon; }
+i.fa-edit { background-image: unquote("url('#{$bp3-icon-path}/edit.png')"); @extend .bp-icon; }
+i.fa-electronic { background-image: unquote("url('#{$bp3-icon-path}/mouse.png')"); @extend .bp-icon; }
+i.fa-email,
+i.fa-envelope,
+i.fa-envelope-o { background-image: unquote("url('#{$bp3-icon-path}/email.png')"); @extend .bp-icon; }
+i.fa-exchange { background-image: unquote("url('#{$bp3-icon-path}/arrow_refresh.png')"); @extend .bp-icon; }
+i.fa-external-link { background-image: unquote("url('#{$bp3-icon-path}/link_go.png')"); @extend .bp-icon; }
+i.fa-filmstrip { background-image: unquote("url('#{$bp3-icon-path}/film.png')"); @extend .bp-icon; }
+i.fa-flag { background-image: unquote("url('#{$bp3-icon-path}/flag_red.png')"); @extend .bp-icon; }
+i.fa-flashcard { background-image: unquote("url('#{$bp3-icon-path}/table_lightening.png')"); @extend .bp-icon; }
+i.fa-floppydisk { background-image: unquote("url('#{$bp3-icon-path}/disk.png')"); @extend .bp-icon; }
+i.fa-globe { background-image: unquote("url('#{$bp3-icon-path}/world.png')"); @extend .bp-icon; }
+i.fa-grid { background-image: unquote("url('#{$bp3-icon-path}/view_grid.png')"); @extend .bp-icon; }
+i.fa-heart { background-image: unquote("url('#{$bp3-icon-path}/heart.png')"); @extend .bp-icon; }
+i.fa-home { background-image: unquote("url('#{$bp3-icon-path}/house.png')"); @extend .bp-icon; }
+i.fa-inbox { background-image: unquote("url('#{$bp3-icon-path}/box.png')"); @extend .bp-icon; }
+i.fa-journal { background-image: unquote("url('#{$bp3-icon-path}/book.png')"); @extend .bp-icon; }
+i.fa-kit { background-image: unquote("url('#{$bp3-icon-path}/briefcase.png')"); @extend .bp-icon; }
+i.fa-leaf,.fa-sitemap { background-image: unquote("url('#{$bp3-icon-path}/treeCurrent.png')"); @extend .bp-icon; }
+i.fa-list { background-image: unquote("url('#{$bp3-icon-path}/view_list.png')"); @extend .bp-icon; }
+i.fa-list-alt,i.fa-export { background-image: unquote("url('#{$bp3-icon-path}/application_add.png')"); @extend .bp-icon; }
+i.fa-lock { background-image: unquote("url('#{$bp3-icon-path}/lock.png')"); @extend .bp-icon; }
+i.fa-manuscript { background-image: unquote("url('#{$bp3-icon-path}/script.png')"); @extend .bp-icon; }
+i.fa-map { background-image: unquote("url('#{$bp3-icon-path}/map.png')"); @extend .bp-icon; }
+i.fa-microfilm { background-image: unquote("url('#{$bp3-icon-path}/film.png')"); @extend .bp-icon; }
+i.fa-minus-circle,i.fa-minus-sign { background-image: unquote("url('#{$bp3-icon-path}/delete.png')"); @extend .bp-icon; }
+i.fa-mobile { background-image: unquote("url('#{$bp3-icon-path}/phone.png')"); @extend .bp-icon; }
+i.fa-motionpicture { background-image: unquote("url('#{$bp3-icon-path}/television.png')"); @extend .bp-icon; }
+i.fa-musicalscore { background-image: unquote("url('#{$bp3-icon-path}/music.png')"); @extend .bp-icon; }
+i.fa-musicrecording { background-image: unquote("url('#{$bp3-icon-path}/music.png')"); @extend .bp-icon; }
+i.fa-newspaper { background-image: unquote("url('#{$bp3-icon-path}/newspaper.png')"); @extend .bp-icon; }
+i.fa-ok { background-image: unquote("url('#{$bp3-icon-path}/tick.png')"); @extend .bp-icon; }
+i.fa-online { background-image: unquote("url('#{$bp3-icon-path}/computer.png')"); @extend .bp-icon; }
+i.fa-painting { background-image: unquote("url('#{$bp3-icon-path}/paintbrush.png')"); @extend .bp-icon; }
+i.fa-photo { background-image: unquote("url('#{$bp3-icon-path}/photo.png')"); @extend .bp-icon; }
+i.fa-photonegative { background-image: unquote("url('#{$bp3-icon-path}/film.png')"); @extend .bp-icon; }
+i.fa-physicalobject { background-image: unquote("url('#{$bp3-icon-path}/box.png')"); @extend .bp-icon; }
+i.fa-plus { background-image: unquote("url('#{$bp3-icon-path}/add.png')"); @extend .bp-icon; }
+i.fa-plus-circle { background-image: unquote("url('#{$bp3-icon-path}/add.png')"); @extend .bp-icon; }
+i.fa-print { background-image: unquote("url('#{$bp3-icon-path}/printer.png')"); @extend .bp-icon; }
+i.fa-qrcode { background-image: unquote("url('#{$bp3-icon-path}/qrcode.png')"); @extend .bp-icon; }
+i.fa-remove { background-image: unquote("url('#{$bp3-icon-path}/delete.png')"); @extend .bp-icon; }
+i.fa-renew { background-image: unquote("url('#{$bp3-icon-path}/renew.png')"); @extend .bp-icon; }
+i.fa-renew-all { background-image: unquote("url('#{$bp3-icon-path}/renewAll.png')"); @extend .bp-icon; }
+i.fa-report { background-image: unquote("url('#{$bp3-icon-path}/report.png')"); @extend .bp-icon; }
+i.fa-rss { background-image: unquote("url('#{$bp3-icon-path}/feed.png')"); @extend .bp-icon; }
+i.fa-save { background-image: unquote("url('#{$bp3-icon-path}/disk.png')"); @extend .bp-icon; }
+i.fa-search { background-image: unquote("url('#{$bp3-icon-path}/magnifier.png')"); @extend .bp-icon; }
+i.fa-sensorimage { background-image: unquote("url('#{$bp3-icon-path}/photo.png')"); @extend .bp-icon; }
+i.fa-serial { background-image: unquote("url('#{$bp3-icon-path}/page_white_stack.png')"); @extend .bp-icon; }
+i.fa-shopping-cart { background-image: unquote("url('#{$bp3-icon-path}/cart.png')"); @extend .bp-icon; }
+i.fa-sign-in { background-image: unquote("url('#{$bp3-icon-path}/door_in.png')"); @extend .bp-icon; }
+i.fa-sign-out { background-image: unquote("url('#{$bp3-icon-path}/door_out.png')"); @extend .bp-icon; }
+i.fa-slide { background-image: unquote("url('#{$bp3-icon-path}/film.png')"); @extend .bp-icon; }
+i.fa-software { background-image: unquote("url('#{$bp3-icon-path}/drive_cd.png')"); @extend .bp-icon; }
+i.fa-soundcassette { background-image: unquote("url('#{$bp3-icon-path}/sound.png')"); @extend .bp-icon; }
+i.fa-sounddisc { background-image: unquote("url('#{$bp3-icon-path}/cd.png')"); @extend .bp-icon; }
+i.fa-soundrecording { background-image: unquote("url('#{$bp3-icon-path}/sound.png')"); @extend .bp-icon; }
+i.fa-spinner { background-image: unquote("url('#{$bp3-icon-path}/ajax_loading.gif')"); @extend .bp-icon; }
+i.fa-star { background-image: unquote("url('#{$bp3-icon-path}/star.png')"); @extend .bp-icon; }
+i.fa-status-unknown { background-image: unquote("url('#{$bp3-icon-path}/bullet_orange.png')"); @extend .bp-icon; }
+i.fa-suitcase { background-image: unquote("url('#{$bp3-icon-path}/bookbag.png')"); @extend .bp-icon; }
+i.fa-tapecartridge { background-image: unquote("url('#{$bp3-icon-path}/drive.png')"); @extend .bp-icon; }
+i.fa-tapecassette { background-image: unquote("url('#{$bp3-icon-path}/drive.png')"); @extend .bp-icon; }
+i.fa-tapereel { background-image: unquote("url('#{$bp3-icon-path}/film.png')"); @extend .bp-icon; }
+i.fa-transparency { background-image: unquote("url('#{$bp3-icon-path}/film.png')"); @extend .bp-icon; }
+i.fa-trash,
+i.fa-trash-o { background-image: unquote("url('#{$bp3-icon-path}/bin.png')"); @extend .bp-icon; }
+i.fa-tree { background-image: unquote("url('#{$bp3-icon-path}/treeCurrent.png')"); @extend .bp-icon; }
+i.fa-tree-muted { background-image: unquote("url('#{$bp3-icon-path}/treeMuted.png')"); @extend .bp-icon; }
+i.fa-unknown { background-image: unquote("url('#{$bp3-icon-path}/page_white.png')"); @extend .bp-icon; }
+i.fa-usd { background-image: unquote("url('#{$bp3-icon-path}/money_dollar.png')"); @extend .bp-icon; }
+i.fa-user { background-image: unquote("url('#{$bp3-icon-path}/user.png')"); @extend .bp-icon; }
+i.fa-video{background-image: unquote("url('#{$bp3-icon-path}/television.png')"); @extend .bp-icon; }
+i.fa-videocartridge { background-image: unquote("url('#{$bp3-icon-path}/television.png')"); @extend .bp-icon; }
+i.fa-videocassette{background-image: unquote("url('#{$bp3-icon-path}/television.png')"); @extend .bp-icon; }
+i.fa-videodisc{background-image: unquote("url('#{$bp3-icon-path}/cd.png')"); @extend .bp-icon; }
+i.fa-videoreel{background-image: unquote("url('#{$bp3-icon-path}/film.png')"); @extend .bp-icon; }
+i.fa-visual { background-image: unquote("url('#{$bp3-icon-path}/view_visual.png')"); @extend .bp-icon; }
+
+body.rtl {
+  i.fa-external-link { background-image: unquote("url('#{$bp3-icon-path}/link_go_rtl.png')"); }
+  i.fa-flag { background-image: unquote("url('#{$bp3-icon-path}/flag_red_rtl.png')"); }
+}
+
+#cart-empty-label i.fa-close { background-image: unquote("url('#{$bp3-icon-path}/briefcase.png')"); @extend .bp-icon; }
diff --git a/themes/bootprint3/scss/search.scss b/themes/bootprint3/scss/search.scss
new file mode 100644
index 0000000000000000000000000000000000000000..77f9d4d663da63e1df4fc89369e2f94545fdac70
--- /dev/null
+++ b/themes/bootprint3/scss/search.scss
@@ -0,0 +1,51 @@
+.searchHomeContent {
+  float: none;
+  margin: 1em auto;
+  width: 90%;
+}
+#advSearchForm .search { margin: 0; }
+.group .match { margin-top: .5em; }
+
+/* --- Search form --- */
+.searchForm_lookfor,
+.searchForm_type { border-color: $brand-primary; }
+[name=searchForm] {
+  margin: 6px 8px 8px;
+  padding: 0;
+
+  .clear-btn,
+  .btn-primary,
+  .form-control {
+    font-size: 14px;
+    height: 32px;
+    padding: 5px 8px;
+  }
+  .clear-btn,
+  .btn-primary[multiple],
+  .form-control[multiple] { height: auto; }
+  @media (min-width: 768px) {
+    .search-query { width: 400px; }
+  }
+  .nav-tabs {
+    border-bottom: 0;
+    padding: 0 6px;
+  }
+  .nav-tabs li a {
+    margin-bottom: -1px;
+    border-bottom: 0;
+    padding-bottom: 6px;
+    &:hover {
+      background: 0 0;
+      border-color: transparent;
+      text-decoration: underline;
+    }
+  }
+  .nav-tabs li.active a,
+  .nav-tabs li.active a:hover {
+    background: #FFF;
+    border-color: $brand-primary;
+    border-bottom: 0;
+    text-decoration: none;
+    z-index: 5;
+  }
+}
diff --git a/themes/bootprint3/scss/variables.scss b/themes/bootprint3/scss/variables.scss
new file mode 100644
index 0000000000000000000000000000000000000000..3201c5b518f63f9ea894bf3db7d11f04589651fa
--- /dev/null
+++ b/themes/bootprint3/scss/variables.scss
@@ -0,0 +1,39 @@
+$brand-primary: #619144;
+
+$font-size-base: 13px !default;
+
+$padding-base-vertical   : 3px !default;
+$padding-base-horizontal : 5px !default;
+$padding-large-vertical  : 8px !default;
+$padding-large-horizontal: 5px !default;
+$padding-small-vertical  : 1px !default;
+$padding-small-horizontal: 2px !default;
+$padding-xs-horizontal   : 1px !default;
+
+$border-radius-base : 3px !default;
+$border-radius-large: 5px !default;
+$border-radius-small: 2px !default;
+
+$input-border-focus: $brand-primary !default;
+
+$legend-border-color: #777 !default; // $gray-light
+
+$grid-gutter-width: 14px !default;
+
+$container-desktop      : 952px !default;
+$container-large-desktop: 952px !default;
+
+$navbar-height       : 65px !default;
+$navbar-margin-bottom: 0px !default;
+$nav-link-padding    : 5px !default;
+
+$pagination-active-bg    : #5bc0de !default; // $brand-info
+$pagination-active-border: #5bc0de !default; // $brand-info
+
+$panel-body-padding: 5px !default;
+
+$breadcrumb-padding-vertical  : 6px !default;
+$breadcrumb-padding-horizontal: 20px !default;
+$breadcrumb-bg                : #FFF !default;
+$breadcrumb-color             : #777 !default; // $gray-light
+$breadcrumb-active-color      : #333 !default; // $gray-dark
\ No newline at end of file
diff --git a/themes/bootstrap3/css/bootstrap-custom.css b/themes/bootstrap3/css/bootstrap-custom.css
index 4e5375228ef9a6882e8e348989966f53a416acad..43c59d4b86fdc728f7825810b80604ced5aa968a 100644
--- a/themes/bootstrap3/css/bootstrap-custom.css
+++ b/themes/bootstrap3/css/bootstrap-custom.css
@@ -1,102 +1,57 @@
-/* --- Global --- */
-.alert.alert-info a { text-decoration: underline }
-.btn.disabled:active,
-.btn.disabled:focus,
-.btn.disabled:hover { color: #000 }
-[data-toggle~="dropdown"] { cursor: pointer }
-.fa { cursor: default }
-.list-unstyled { margin: 0 }
-.highlight,
-mark {
-  background: #ff6;
-  padding: .1em .2em;
-}
-.icon-bar { background-color: #888 }
-img { max-width: 100% }
-label.list-group-item {
-  margin-top: 0;
-  padding-left: 35px;
-  font-weight: normal;
-  border-radius: 0;
-}
-.list-group-item.title { font-weight: bold }
-#modal { background-color: rgba(0,0,0,0.2) }
-#modal .modal-body > h2:first-child { display: none }
-.popover { width: 250px }
-.sub-breadcrumb {
-  padding: 5px 10px;
-  white-space: nowrap;
-}
-.sub-breadcrumb li { display: inline-block }
-.sub-breadcrumb li + li:before {
-  padding-left: 5px;
-  padding-right: 5px;
-  color: #ccc;
-  content: "/\00a0";
-}
-.tab-content { padding: 4px }
+/* mixins */
+input::-webkit-input-placeholder,
+textarea::-webkit-input-placeholder,
+input:-ms-input-placeholder,
+textarea:-ms-input-placeholder,
+input::-ms-input-placeholder,
+textarea::-ms-input-placeholder,
+input::placeholder,
+textarea::placeholder {
+  color: #888;
+}
+.sr-only {
+  clip: rect(1px, 1px, 1px, 1px);
+  position: absolute;
+  /* reset */
 
-@media (max-width: 991px) {
-  header .container.navbar {margin-bottom: 0;}
-  .searchForm {margin-top: 0;}
-}
-@media (min-width: 768px) {
-  h2 {
-    font-size: 23px;
-    font-weight: normal;
-  }
-  h3 {
-    font-size: 20px;
-    font-weight: normal;
-  }
-  .form-control { max-width: 400px; }
-}
-@media (max-width: 767px) {
-  h2 { font-size: 20px; }
-  h3 { font-size: 16px; }
-  .searchForm {padding-top: 0;}
+  width: auto;
+  height: auto;
+  margin: 0;
+  padding: 0;
+  overflow: hidden;
+  border: 0;
 }
+.sr-only:focus {
+  background-color: #ffffff;
+  border-radius: 4px;
+  clip: auto;
+  color: #132531;
+  display: block;
+  font-size: 14px;
+  height: 50px;
+  line-height: 20px;
+  padding: 15px 15px;
+  position: absolute;
+  left: 5px;
+  top: 5px;
+  text-decoration: none;
+  text-transform: none;
+  width: auto;
+  z-index: 100000;
+  /* Above WP toolbar */
 
-/* --- Form Errors --- */
-.has-error { margin-bottom: 0 }
-.sms-error { margin-bottom: 0 }
-.sms-error .help-block,
-.sms-error .control-label,
-.sms-error .radio,
-.sms-error .checkbox,
-.sms-error .radio-inline,
-.sms-error .checkbox-inline,
-.sms-error.radio label,
-.sms-error.checkbox label,
-.sms-error.radio-inline label,
-.sms-error.checkbox-inline label { color: #a94442 }
-.sms-error .form-control {
-  border-color: #a94442;
-  -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,0.075);
-  box-shadow: inset 0 1px 1px rgba(0,0,0,0.075);
-}
-.sms-error .form-control:focus {
-  border-color: #843534;
-  -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,0.075), 0 0 6px #ce8483;
-  box-shadow: inset 0 1px 1px rgba(0,0,0,0.075), 0 0 6px #ce8483;
-}
-.sms-error .input-group-addon {
-  color: #a94442;
-  border-color: #a94442;
-  background-color: #f2dede;
 }
-.sms-error .form-control-feedback { color: #a94442 }
-.help-block.with-errors {
-  margin: 0;
-  padding-top: 6px;
-  padding-bottom: 6px;
+/* this mixins give output generate duplicate CSS code
+ * alternatively you will to have to rewrite the original mixin in buttons.less
+ */
+.navbar-brand {
+  font-size: 20px;
 }
-.help-block.with-errors:empty { padding: 0 }
-
-/* --- Advanced Search --- */
+/* buttons */
+/* link on white background */
 .group {
   position: relative;
-  background: #eee;
+  background: #eeeeee;
   border-radius: 4px;
   border: 1px solid #c8c8c8;
   margin-top: 0;
@@ -113,11 +68,19 @@ label.list-group-item {
   opacity: .4;
   z-index: 2;
 }
-.group .search { margin-bottom: 2px }
-.group .search .close { opacity: .8 }
+.group .search {
+  margin-bottom: 2px;
+}
+.group .search .close {
+  opacity: .8;
+}
 @media (min-width: 768px) {
-  .group { padding: 10px 10px 10px 25px }
-  .group [class^=col-] { padding-left: 0 }
+  .group {
+    padding: 10px 10px 10px 25px;
+  }
+  .group [class^=col-] {
+    padding-left: 0;
+  }
 }
 @media (max-width: 767px) {
   .group .search .middle {
@@ -131,370 +94,235 @@ label.list-group-item {
   }
 }
 @media (max-width: 991px) {
-  .group .form-control { max-width: none }
+  .group .form-control {
+    max-width: none;
+  }
 }
 #groupPlaceHolder {
   display: block;
   padding: 6px;
 }
-.template-dir-eds.template-name-advanced legend { margin-bottom: 0 }
-.template-dir-eds.template-name-advanced .no-js .group:nth-child(n+3) { display: none }
-.template-dir-eds.template-name-advanced .search .close a { margin-left: -2em }
+.template-dir-eds.template-name-advanced legend {
+  margin-bottom: 0;
+}
+.template-dir-eds.template-name-advanced .no-js .group:nth-child(n+3) {
+  display: none;
+}
+.template-dir-eds.template-name-advanced .search .close a {
+  margin-left: -2em;
+}
+.alphabrowse {
+  border-collapse: separate;
+  /* highlighting the row makes ff bugs; operate on its children */
 
-/* --- Alphabrowse --- */
-.alphabrowse { border-collapse: separate }
-.alphabrowse .lcc { width: 20% }
+}
+.alphabrowse .lcc {
+  width: 20%;
+}
 .alphabrowse .titles {
   width: 10%;
   text-align: center;
 }
 .alphabrowse tr.browse-match td {
-  border-top: .2em solid #265680;
-  border-bottom: .2em solid #265680;
+  border-top: 0.2em solid #265680;
+  border-bottom: 0.2em solid #265680;
 }
-.alphabrowse tr.browse-match td:first-child { border-left: .2em solid #265680 }
-.alphabrowse tr.browse-match td:last-child { border-right: .2em solid #265680 }
-
-/* --- Autocomplete --- */
+.alphabrowse tr.browse-match td:first-child {
+  border-left: 0.2em solid #265680;
+}
+.alphabrowse tr.browse-match td:last-child {
+  border-right: 0.2em solid #265680;
+}
+/* crhallberg/autocomplete.js 0.14 */
 .autocomplete-results {
+  position: absolute;
+  margin: 0;
   margin-top: 2px;
-  border: 1px solid #ddd;
-  background-color: #fff;
+  padding: 0;
+  border: 1px solid #d3d3d3;
+  background-color: #ffffff;
   border-radius: 4px;
   overflow: hidden;
+  z-index: 50;
 }
 .autocomplete-results .item {
   display: block;
-  padding-top: .75rem;
-  padding-right: 1rem;
-  padding-bottom: .75rem;
-  padding-left: 1.25rem;
-  border-bottom: 1px solid #ddd;
+  margin: 0;
+  padding: .75rem 1.25rem;
+  border-bottom: 1px solid #d3d3d3;
   cursor: pointer;
 }
-.autocomplete-results .item:last-child { border: 0 }
+.autocomplete-results .item:last-child {
+  border: 0;
+}
+.autocomplete-results .item:hover {
+  background-color: #e2edf6;
+}
+.autocomplete-results .item.loading {
+  background-color: #ffffff;
+}
 .autocomplete-results .item.selected {
   background-color: #265680;
-  color: #fff;
+  color: #ffffff;
 }
-
-/* --- Badges - blend the links in --- */
-.badge a { color: #fff }
-
-/* --- Browse --- */
-.browse.list-group .list-group-item { word-wrap: break-word }
-.browse.list-group .list-group-item.view-record {
-  padding: 2px 4px;
-  font-size: 85%;
-  text-align: right;
-  border-top: 0;
+.autocomplete-results .item small {
+  display: block;
+  color: #a9a9a9;
 }
-
-/* --- Cart --- */
-.cart-controls .checkbox {
-  line-height: 2.5em;
-  padding-right: 1em;
+.fa-grid:before {
+  content: "\f00a";
 }
-#modal .cart-controls .btn { margin-bottom: 4px }
-#modal .cart-controls .checkbox { padding-bottom: 1em }
-#modal .cart-controls ~ hr { margin-top: 0 }
-
-/* --- Search Icons --- */
-.fa-grid:before { content: "\f00a" }
-.fa-visual:before { content: "\f008" }
-/* --- Format Type Icons --- */
-.fa-x:before { content: "\f0f6" }
-.fa-atlas:before { content: "\f14e" }
-.fa-book:before { content: "\f02d" }
-.fa-braille:before { content: "\f0a6" }
-.fa-cdrom:before { content: "\f109" }
-.fa-chart:before { content: "\f012" }
-.fa-chipcartridge:before { content: "\f109" }
-.fa-collage:before { content: "\f03e" }
-.fa-disccartridge:before { content: "\f109" }
-.fa-drawing:before { content: "\f03e" }
-.fa-ebook:before { content: "\f0f6" }
-.fa-electronic:before { content: "\f1c6" }
-.fa-filmstrip:before { content: "\f008" }
-.fa-flashcard:before { content: "\f0e7" }
-.fa-floppydisk:before { content: "\f0c7" }
-.fa-globe:before { content: "\f0ac" }
-.fa-journal:before { content: "\f0f6" }
-.fa-kit:before { content: "\f0b1" }
-.fa-manuscript:before { content: "\f0f6" }
-.fa-map:before { content: "\f14e" }
-.fa-microfilm:before { content: "\f008" }
-.fa-motionpicture:before { content: "\f03d" }
-.fa-musicalscore:before { content: "\f001" }
-.fa-musicrecording:before { content: "\f001" }
-.fa-newspaper:before { content: "\f0f6" }
-.fa-online:before { content: "\f109" }
-.fa-painting:before { content: "\f03e" }
-.fa-photo:before { content: "\f03e" }
-.fa-photonegative:before { content: "\f03e" }
-.fa-physicalobject:before { content: "\f187" }
-.fa-print:before { content: "\f03e" }
-.fa-sensorimage:before { content: "\f03e" }
-.fa-serial:before { content: "\f0f6" }
-.fa-slide:before { content: "\f008" }
-.fa-software:before { content: "\f109" }
-.fa-soundcassette:before { content: "\f025" }
-.fa-sounddisc:before { content: "\f109" }
-.fa-soundrecording:before { content: "\f025" }
-.fa-tapecartridge:before { content: "\f109" }
-.fa-tapecassette:before { content: "\f025" }
-.fa-tapereel:before { content: "\f008" }
-.fa-transparency:before { content: "\f008" }
-.fa-unknown:before { content: "\f128" }
-.fa-video:before { content: "\f03d" }
-.fa-videocartridge:before { content: "\f03d" }
-.fa-videocassette:before { content: "\f03d" }
-.fa-videodisc:before { content: "\f109" }
-.fa-videoreel:before { content: "\f03d" }
-
-/* --- PubDateVis --- */
-#dateVisColorSettings {
-  background-color: #fff;
-  fill: #eaeaea;
-  outline-color: #e8cfac;
-  stroke: #265680;
+.fa-visual:before {
+  content: "\f008";
 }
-
-/* --- Record --- */
-.citation .pace-car th,
-.citation .pace-car td {
-  border: 0;
-  padding: 0;
+.fa-x:before {
+  content: "\f0f6";
 }
-.citation th { text-align: right }
-#hierarchyTreeHolder {
-  overflow-x: hidden;
-  border-right: 1px solid #eee;
+.fa-atlas:before {
+  content: "\f14e";
 }
-#hierarchyTree .currentHierarchy > a,
-#hierarchyTree .currentRecord a {
-  font-weight: bold;
-  color: #000;
+.fa-book:before {
+  content: "\f02d";
 }
-.item-notes ul { padding-left: 2rem }
-.recordcover { max-height: 300px }
-.tagList .tag {
-  display: inline-block;
-  margin: 0 1px 1px;
-  padding: 6px 6px;
-  font-size: 14px;
-  line-height: 1.42857143;
-  border-radius: 4px;
+.fa-braille:before {
+  content: "\f0a6";
 }
-.tagList .tag.selected { background-color: #265680 }
-.tagList .tag.selected a { color: #fff }
-.tagList .tag.selected .badge {
-  color: #222;
-  background-color: #fff;
+.fa-cdrom:before {
+  content: "\f109";
 }
-.tagList .tag.selected .badge:hover { color: #a94442 }
-.tagList .tag .badge .fa { width: 12px }
-.tagList button { border: 0 }
-.tagList .tag-form { display: inline }
-.tagList.loggedin .tag:not(.selected) .badge:hover { background-color: #028302 }
-
-/* --- Search --- */
-.bulkActionButtons label { display: inline-block }
-.bulkActionButtons label input { margin-top: 2px }
-@media (max-width: 767px) {
-  .grid { min-height: 250px }
+.fa-chart:before {
+  content: "\f012";
 }
-.result a.title { font-weight: bold }
-.result .left { text-align: center }
-.result .left img { max-width: 100% }
-@media (max-width: 767px) {
-  .result a { text-decoration: underline }
-  .result .middle,
-  .result .right { padding: 0 }
+.fa-chipcartridge:before {
+  content: "\f109";
 }
-@media (max-width: 530px) {
-  .result .checkbox { display: none !important }
-  .result .left { width: 40% }
-  .result .middle { width: 60% }
-  .result .right { display: none }
+.fa-collage:before {
+  content: "\f03e";
 }
-.search-controls .alert { margin-bottom: 0 }
-.searchtools a { padding: 0 .5em }
-.title-in-heading {
-  font-size: inherit;
-  font-style: italic;
+.fa-disccartridge:before {
+  content: "\f109";
 }
-
-/* --- Sidebar --- */
-/* Sidebar rounded corners */
-.narrow-toggle { text-align: center }
-.sidebar label:not(.list-group-item) { margin-left: 20px }
-.sidebar .list-group:not(.filters) .title { cursor: pointer }
-.sidebar .list-group:not(.filters) .title.collapsed { border-radius: 4px }
-.sidebar .list-group:not(.filters) .title.collapsed:after { content: '\25BC' }
-.sidebar .list-group:not(.filters) .title:after {
-  content: '\25B2';
-  float: right;
+.fa-drawing:before {
+  content: "\f03e";
 }
-.sidebar .collapse .list-group-item,
-.sidebar .collapsing .list-group-item {
-  border-top-left-radius: 0px;
-  border-top-right-radius: 0px;
+.fa-ebook:before {
+  content: "\f0f6";
 }
-.sidebar .collapse .list-group-item[id^=more],
-.sidebar .collapsing .list-group-item[id^=more] {
-  border-bottom-left-radius: 4px;
-  border-bottom-right-radius: 4px;
+.fa-electronic:before {
+  content: "\f1c6";
 }
-.sidebar #side-collapse-publishDate .list-group-item {
-  border-bottom-left-radius: 4px;
-  border-bottom-right-radius: 4px;
+.fa-filmstrip:before {
+  content: "\f008";
 }
-.list-group-item i.fa,
-.badge i.fa { cursor: inherit }
-.sidebar .facet a { text-decoration: none }
-.top-row .applied { font-weight: bold }
-.top-row .applied:hover { color: #a94442 }
-.top-row .applied:hover .fa.fa-check:before { content: "\f00d" }
-
-/* --- Slider accessibility --- */
-.slider-container {
-  padding: 4px 10px;
-  text-align: center;
+.fa-flashcard:before {
+  content: "\f0e7";
 }
-.slider-container .slider.slider-horizontal { width: 100% }
-.slider-container .slider-track {
-  background: #777;
-  box-shadow: inset 0 1px 0 rgba(0,0,0,0.4);
+.fa-floppydisk:before {
+  content: "\f0c7";
 }
-.slider-container .slider-handle {
-  background: #265680;
-  background-image: none;
-  border: 1px solid #265680;
-  box-shadow: none;
-  opacity: .9;
+.fa-globe:before {
+  content: "\f0ac";
 }
-.slider-container .slider-handle:hover,
-.slider-container .slider-handle:active,
-.slider-container .slider-handle:focus {
-  opacity: 1;
-  background: #FFF;
-  border-color: #777;
+.fa-journal:before {
+  content: "\f0f6";
 }
-.slider-container .slider-handle:active,
-.slider-container .slider-handle:focus { border-color: #265680 }
-.slider-container .slider-selection {
-  background: #CCC;
-  box-shadow: inset 0 -1px 0 rgba(0,0,0,0.3);
+.fa-kit:before {
+  content: "\f0b1";
 }
-.slider-container input { display: none }
-
-/* --- Table wrapping to prevent horizontal overflow --- */
-.table {
-  table-layout: fixed;
-  word-wrap: break-word;
+.fa-manuscript:before {
+  content: "\f0f6";
 }
-
-/* --- Visualization View --- */
-.node {
-  position: absolute;
-  box-sizing: content-box;
-  margin: -1px;
-  overflow: hidden;
-  font: 10px sans-serif;
-  line-height: 12px;
-  border: 1px solid #fff;
+.fa-map:before {
+  content: "\f14e";
 }
-.node div { margin-top: 0px }
-.toplevel { border: 2px solid #000 }
-.node .label {
-  position: absolute;
-  bottom: 0;
-  left: 0;
-  min-height: 1px;
-  padding: 2px 4px;
-  font-size: 85%;
-  background-color: rgba(0,0,0,0.5);
-  border-radius: 0;
-  text-shadow: none;
+.fa-microfilm:before {
+  content: "\f008";
 }
-.notalabel { color: #000 }
-#viz-instructions { padding-top: 600px }
-
-/* ----- HOVER OVERLAY ------ */
-/* - similar items carousel - */
-#similar-items-carousel .carousel-indicators { bottom: 0px }
-#similar-items-carousel .carousel-indicators li {
-  width: 8px;
-  height: 8px;
-  margin: 2px;
-  background-color: rgba(255,255,255,0.3);
-  border-color: #222;
+.fa-motionpicture:before {
+  content: "\f03d";
 }
-#similar-items-carousel .hover-overlay {
-  position: relative;
-  display: block;
-  min-width: 150px;
-  min-height: 200px;
-  margin: auto;
-  text-align: center;
+.fa-musicalscore:before {
+  content: "\f001";
 }
-#similar-items-carousel .hover-overlay img {
-  max-width: 100%;
-  margin: 10px 0;
+.fa-musicrecording:before {
+  content: "\f001";
 }
-#similar-items-carousel .hover-overlay .content {
-  position: absolute;
-  top: 0;
-  left: 0;
-  display: none;
-  width: 100%;
-  height: 100%;
-  padding: .5em .5em 0;
-  color: #fff;
-  background-color: rgba(0,0,0,0.5);
+.fa-newspaper:before {
+  content: "\f0f6";
 }
-#similar-items-carousel .hover-overlay:hover .content { display: block }
-#similar-items-carousel .item { padding: 0 4em }
-
-/* --- Hierarchical facets --- */
-/* jsTree arrows */
-.facet .jstree-ocl:before {
-  float: left;
-  width: 10px;
-  padding: 0;
-  margin-left: -10px;
-  font-family: 'FontAwesome';
-  font-weight: normal;
-  font-style: normal;
-  text-decoration: inherit;
-  cursor: pointer;
-  speak: none;
+.fa-online:before {
+  content: "\f109";
 }
-.facet .jstree-default .jstree-open > .jstree-ocl:before { content: "\f0d7" }
-.facet .jstree-default .jstree-closed > .jstree-ocl:before { content: "\f0da" }
-.facet .jstree-default .jstree-leaf > .jstree-ocl:before { content: " " }
-/* facet list styling */
-.jstree-facet li span.main {
-  display: block;
-  padding-left: 1px;
-  overflow: hidden;
-  text-overflow: ellipsis;
-  white-space: nowrap;
+.fa-painting:before {
+  content: "\f03e";
 }
-.jstree-facet .jstree-container-ul { padding: 0 }
-.jstree-facet .jstree-container-ul > li.active,
-.jstree-facet .jstree-container-ul > li.active a.jstree-anchor {
-  background-color: #265680;
-  color: #fff;
+.fa-photo:before {
+  content: "\f03e";
 }
-li.jstree-facet,
-li.jstree-node { list-style: none }
-li.jstree-facet .badge { cursor: text }
-li.jstree-facet ul { padding-left: 20px }
+.fa-photonegative:before {
+  content: "\f03e";
+}
+.fa-physicalobject:before {
+  content: "\f187";
+}
+.fa-print:before {
+  content: "\f03e";
+}
+.fa-sensorimage:before {
+  content: "\f03e";
+}
+.fa-serial:before {
+  content: "\f0f6";
+}
+.fa-slide:before {
+  content: "\f008";
+}
+.fa-software:before {
+  content: "\f109";
+}
+.fa-soundcassette:before {
+  content: "\f025";
+}
+.fa-sounddisc:before {
+  content: "\f109";
+}
+.fa-soundrecording:before {
+  content: "\f025";
+}
+.fa-tapecartridge:before {
+  content: "\f109";
+}
+.fa-tapecassette:before {
+  content: "\f025";
+}
+.fa-tapereel:before {
+  content: "\f008";
+}
+.fa-transparency:before {
+  content: "\f008";
+}
+.fa-unknown:before {
+  content: "\f128";
+}
+.fa-video:before {
+  content: "\f03d";
+}
+.fa-videocartridge:before {
+  content: "\f03d";
+}
+.fa-videocassette:before {
+  content: "\f03d";
+}
+.fa-videodisc:before {
+  content: "\f109";
+}
+.fa-videoreel:before {
+  content: "\f03d";
+}
+.hierarchy-tree {
+  /* jsTree arrows */
 
-/* --- Hierarchy tree --- */
-/* jsTree arrows */
+}
 .hierarchy-tree .jstree-ocl:before {
   float: left;
   width: 10px;
@@ -507,9 +335,15 @@ li.jstree-facet ul { padding-left: 20px }
   text-decoration: inherit;
   speak: none;
 }
-.hierarchy-tree .jstree-open > .jstree-ocl:before { content: "\f0d7" }
-.hierarchy-tree .jstree-closed > .jstree-ocl:before { content: "\f0da" }
-.hierarchy-tree .jstree-leaf > .jstree-ocl:before { content: " " }
+.hierarchy-tree .jstree-open > .jstree-ocl:before {
+  content: "\f0d7";
+}
+.hierarchy-tree .jstree-closed > .jstree-ocl:before {
+  content: "\f0da";
+}
+.hierarchy-tree .jstree-leaf > .jstree-ocl:before {
+  content: " ";
+}
 .hierarchy-tree .jstree-icon {
   width: 16px;
   color: #000;
@@ -519,19 +353,123 @@ li.jstree-facet ul { padding-left: 20px }
   white-space: nowrap;
 }
 .hierarchy-tree .jstree-container-ul,
-.hierarchy-tree .jstree-children { padding-left: 16px }
-.hierarchy-tree .jstree-initial-node { display: none }
+.hierarchy-tree .jstree-children {
+  padding-left: 16px;
+}
+.hierarchy-tree .jstree-initial-node {
+  display: none;
+}
 .hierarchy-tree .jstree-clicked {
-  color: #fff;
+  color: #ffffff;
   background-color: #265680;
 }
-.hierarchy-tree .jstree-clicked .jstree-icon { color: #fff }
+.hierarchy-tree .jstree-clicked .jstree-icon {
+  color: #fff;
+}
+.hierarchy-tree .jstree-search a {
+  font-style: italic;
+  color: #8b0000 ;
+  font-weight: bold;
+}
+/* --- Record --- */
+#hierarchyTreeHolder {
+  overflow-x: hidden;
+  border-right: 1px solid #eeeeee;
+}
+#hierarchyTree .currentHierarchy > a,
+#hierarchyTree .currentRecord a {
+  font-weight: bold;
+  color: #000;
+}
+/* --- Facets --- */
+.facet .jstree-ocl:before {
+  float: left;
+  width: 10px;
+  padding: 0;
+  margin-left: -10px;
+  font-family: 'FontAwesome';
+  font-weight: normal;
+  font-style: normal;
+  text-decoration: inherit;
+  cursor: pointer;
+  speak: none;
+}
+.facet .jstree-default .jstree-open > .jstree-ocl:before {
+  content: "\f0d7";
+}
+.facet .jstree-default .jstree-closed > .jstree-ocl:before {
+  content: "\f0da";
+}
+.facet .jstree-default .jstree-leaf > .jstree-ocl:before {
+  content: " ";
+}
+/* facet list styling */
+.jstree-facet li span.main {
+  display: block;
+  padding-left: 1px;
+  /* Fix Firefox cutting the checkboxes */
 
-/* --- Offcanvas --- */
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+.jstree-facet .jstree-container-ul {
+  padding: 0;
+}
+.jstree-facet .jstree-container-ul > li.active,
+.jstree-facet .jstree-container-ul > li.active a.jstree-anchor {
+  background-color: #265680;
+  color: #fff;
+}
+li.jstree-facet,
+li.jstree-node {
+  list-style: none;
+}
+li.jstree-facet .badge {
+  cursor: text;
+}
+li.jstree-facet ul {
+  padding-left: 20px;
+}
+#modal {
+  background-color: rgba(0, 0, 0, 0.2);
+}
+#modal .modal-content > .close {
+  position: absolute;
+  right: -50px;
+  top: 0;
+  z-index: 2;
+  font-size: 32pt;
+  color: #fff;
+  opacity: .7;
+}
+#modal .modal-content > .close:hover {
+  opacity: 1;
+}
+#modal .modal-body h1,
+#modal .modal-body h2 {
+  margin-top: 0.3rem;
+  margin-bottom: 1.3rem;
+}
+#modal .cart-controls .btn {
+  margin-bottom: 4px;
+}
+#modal .cart-controls .checkbox {
+  padding-bottom: 1em;
+}
+#modal .cart-controls ~ hr {
+  margin-top: 0;
+}
 .offcanvas-overlay,
-.offcanvas-toggle { display: none }
+.offcanvas-toggle {
+  display: none;
+}
 @media screen and (max-width: 767px) {
-  body.offcanvas { overflow-x: hidden }
+  body.offcanvas {
+    overflow-x: hidden;
+    /* Prevent scroll on narrow devices */
+  
+  }
   body.offcanvas .sidebar {
     position: fixed;
     height: 100%;
@@ -541,41 +479,75 @@ li.jstree-facet ul { padding-left: 20px }
     padding-right: 0;
     overflow-y: auto;
   }
-  body.offcanvas .sidebar h4 { padding-left: 12px }
-  body.offcanvas .sidebar .checkbox { margin-left: 32px }
+  body.offcanvas .sidebar h4 {
+    padding-left: 12px;
+  }
+  body.offcanvas .sidebar .checkbox {
+    margin-left: 32px;
+  }
   body.offcanvas .sidebar .list-group,
   body.offcanvas .sidebar .list-group-item {
     border-left: 0;
     border-right: 0;
     border-radius: 0 !important;
   }
-  body.offcanvas.active { overflow-y: hidden }
-  body.offcanvas.offcanvas-left { padding-left: 15px }
-  body.offcanvas.offcanvas-left .main { background: #FFF }
+  body.offcanvas.active {
+    overflow-y: hidden;
+  }
+  body.offcanvas.offcanvas-left {
+    padding-left: 15px;
+  }
+  body.offcanvas.offcanvas-left .main {
+    background: #FFF;
+  }
   body.offcanvas.offcanvas-left.active {
     margin-left: 75%;
     margin-right: -75%;
   }
-  body.offcanvas.offcanvas-left.active .sidebar { left: 0 }
-  body.offcanvas.offcanvas-left.active .offcanvas-overlay { right: -75% }
-  body.offcanvas.offcanvas-left.active .offcanvas-toggle { left: 75% }
-  body.offcanvas.offcanvas-left .sidebar { left: -75% }
-  body.offcanvas.offcanvas-left .offcanvas-overlay { right: -100% }
+  body.offcanvas.offcanvas-left.active .sidebar {
+    left: 0;
+  }
+  body.offcanvas.offcanvas-left.active .offcanvas-overlay {
+    right: -75%;
+  }
+  body.offcanvas.offcanvas-left.active .offcanvas-toggle {
+    left: 75%;
+  }
+  body.offcanvas.offcanvas-left .sidebar {
+    left: -75%;
+  }
+  body.offcanvas.offcanvas-left .offcanvas-overlay {
+    right: -100%;
+  }
   body.offcanvas.offcanvas-left .offcanvas-toggle {
     border-radius: 0 3px 3px 0;
     left: 0;
   }
-  body.offcanvas.offcanvas-right { padding-right: 15px }
-  body.offcanvas.offcanvas-right .main > .container { background: #FFF }
+  body.offcanvas.offcanvas-right {
+    padding-right: 15px;
+  }
+  body.offcanvas.offcanvas-right .main > .container {
+    background: #FFF;
+  }
   body.offcanvas.offcanvas-right.active {
     margin-left: -75%;
     margin-right: 75%;
   }
-  body.offcanvas.offcanvas-right.active .sidebar { right: 0 }
-  body.offcanvas.offcanvas-right.active .offcanvas-overlay { left: -75% }
-  body.offcanvas.offcanvas-right.active .offcanvas-toggle { right: 75% }
-  body.offcanvas.offcanvas-right .sidebar { right: -75% }
-  body.offcanvas.offcanvas-right .offcanvas-overlay { left: -100% }
+  body.offcanvas.offcanvas-right.active .sidebar {
+    right: 0;
+  }
+  body.offcanvas.offcanvas-right.active .offcanvas-overlay {
+    left: -75%;
+  }
+  body.offcanvas.offcanvas-right.active .offcanvas-toggle {
+    right: 75%;
+  }
+  body.offcanvas.offcanvas-right .sidebar {
+    right: -75%;
+  }
+  body.offcanvas.offcanvas-right .offcanvas-overlay {
+    left: -100%;
+  }
   body.offcanvas.offcanvas-right .offcanvas-toggle {
     border-radius: 3px 0 0 3px;
     right: 0;
@@ -586,7 +558,7 @@ li.jstree-facet ul { padding-left: 20px }
     top: 0;
     width: 100%;
     height: 100%;
-    background-color: rgba(0,0,0,0.3);
+    background-color: rgba(0, 0, 0, 0.3);
     z-index: 3;
   }
   body.offcanvas .offcanvas-toggle {
@@ -602,13 +574,428 @@ li.jstree-facet ul { padding-left: 20px }
   }
   body.offcanvas .offcanvas-overlay,
   body.offcanvas .offcanvas-toggle,
-  body.offcanvas .offcanvas-toggle * { cursor: pointer }
+  body.offcanvas .offcanvas-toggle * {
+    cursor: pointer;
+  }
   body.offcanvas,
   body.offcanvas .sidebar,
   body.offcanvas .offcanvas-overlay,
   body.offcanvas .offcanvas-toggle {
-    -webkit-transition: all .25s ease-out;
-    -o-transition: all .25s ease-out;
-    transition: all .25s ease-out;
+    -webkit-transition: all 0.25s ease-out;
+    -o-transition: all 0.25s ease-out;
+    transition: all 0.25s ease-out;
+  }
+}
+.citation .pace-car th,
+.citation .pace-car td {
+  border: 0;
+  padding: 0;
+}
+.citation th {
+  text-align: right;
+}
+.item-notes ul {
+  padding-left: 2rem;
+}
+.recordcover {
+  max-height: 300px;
+}
+.tagList .tag {
+  display: inline-block;
+  margin: 0 1px 1px;
+  padding: 6px 6px;
+  font-size: 14px;
+  line-height: 1.428571429;
+  border-radius: 4px;
+}
+.tagList .tag.selected {
+  background-color: #265680;
+}
+.tagList .tag.selected a {
+  color: #fff;
+}
+.tagList .tag.selected .badge {
+  color: #222222;
+  background-color: #fff;
+}
+.tagList .tag.selected .badge:hover {
+  color: #a94442;
+}
+.tagList .tag .badge .fa {
+  width: 12px;
+}
+.tagList button {
+  border: 0;
+}
+.tagList .tag-form {
+  display: inline;
+}
+.tagList.loggedin .tag:not(.selected) .badge:hover {
+  background-color: #028302;
+}
+.subject-line:hover {
+  color: #999;
+}
+.subject-line:hover a {
+  color: #092b47;
+}
+.subject-line a:hover ~ a {
+  color: #999;
+  text-decoration: none;
+}
+.bulkActionButtons label {
+  display: inline-block;
+}
+.bulkActionButtons label input {
+  margin-top: 2px;
+}
+@media (max-width: 767px) {
+  .grid {
+    min-height: 250px;
+  }
+}
+.result a.title {
+  font-weight: bold;
+}
+.result .left {
+  text-align: center;
+}
+.result .left img {
+  max-width: 100%;
+}
+@media (max-width: 767px) {
+  .result a {
+    text-decoration: underline;
+  }
+  .result .middle,
+  .result .right {
+    padding: 0;
+  }
+}
+@media (max-width: 530px) {
+  .result .checkbox {
+    display: none !important;
+  }
+  .result .left {
+    width: 40%;
+  }
+  .result .middle {
+    width: 60%;
+  }
+  .result .right {
+    display: none;
+  }
+}
+.search-controls .alert {
+  margin-bottom: 0;
+}
+.searchtools a {
+  padding: 0 .5em;
+}
+.title-in-heading {
+  font-size: inherit;
+  font-style: italic;
+}
+.wikipedia img {
+  margin-right: 1rem;
+}
+/* Sidebar rounded corners */
+.narrow-toggle {
+  text-align: center;
+}
+.sidebar label:not(.list-group-item) {
+  margin-left: 20px;
+}
+.sidebar .list-group.facet .list-group-item.title {
+  cursor: pointer;
+}
+.sidebar .list-group.facet .list-group-item.title.collapsed {
+  border-radius: 4px;
+}
+.sidebar .list-group.facet .list-group-item.title.collapsed:after {
+  content: '\25BC';
+}
+.sidebar .list-group.facet .list-group-item.title:after {
+  content: '\25B2';
+  float: right;
+}
+.sidebar .collapse .list-group-item,
+.sidebar .collapsing .list-group-item {
+  border-top-left-radius: 0px;
+  border-top-right-radius: 0px;
+}
+.sidebar .collapse .list-group-item[id^=more],
+.sidebar .collapsing .list-group-item[id^=more] {
+  border-bottom-left-radius: 4px;
+  border-bottom-right-radius: 4px;
+}
+.sidebar #side-collapse-publishDate .list-group-item {
+  border-bottom-left-radius: 4px;
+  border-bottom-right-radius: 4px;
+}
+label.list-group-item {
+  margin-top: 0;
+  padding-left: 35px;
+  font-weight: normal;
+  border-radius: 0;
+}
+.list-group-item.title {
+  font-weight: bold;
+}
+.list-group-item i.fa,
+.badge i.fa {
+  cursor: inherit;
+}
+.sidebar .facet a {
+  text-decoration: none;
+}
+.top-row .applied {
+  font-weight: bold;
+}
+.top-row .applied:hover {
+  color: #a94442;
+}
+.top-row .applied:hover .fa.fa-check:before {
+  content: "\f00d";
+}
+#similar-items-carousel .carousel-indicators {
+  bottom: 0px;
+}
+#similar-items-carousel .carousel-indicators li {
+  width: 8px;
+  height: 8px;
+  margin: 2px;
+  background-color: rgba(255, 255, 255, 0.3);
+  border-color: #222222;
+}
+#similar-items-carousel .hover-overlay {
+  position: relative;
+  display: block;
+  min-width: 150px;
+  min-height: 200px;
+  margin: auto;
+  text-align: center;
+}
+#similar-items-carousel .hover-overlay img {
+  max-width: 100%;
+  margin: 10px 0;
+}
+#similar-items-carousel .hover-overlay .content {
+  position: absolute;
+  top: 0;
+  left: 0;
+  display: none;
+  width: 100%;
+  height: 100%;
+  padding: .5em .5em 0;
+  color: #fff;
+  background-color: rgba(0, 0, 0, 0.5);
+}
+#similar-items-carousel .hover-overlay:hover .content {
+  display: block;
+}
+#similar-items-carousel .item {
+  padding: 0 4em;
+}
+.slider-container {
+  padding: 4px 10px;
+  text-align: center;
+}
+.slider-container .slider.slider-horizontal {
+  width: 100%;
+}
+.slider-container .slider-track {
+  background: #777777;
+  box-shadow: inset 0 1px 0 rgba(0, 0, 0, 0.4);
+}
+.slider-container .slider-handle {
+  background: #265680;
+  background-image: none;
+  border: 1px solid #265680;
+  box-shadow: none;
+  opacity: .9;
+}
+.slider-container .slider-handle:hover,
+.slider-container .slider-handle:active,
+.slider-container .slider-handle:focus {
+  opacity: 1;
+  background: #FFF;
+  border-color: #777777;
+}
+.slider-container .slider-handle:active,
+.slider-container .slider-handle:focus {
+  border-color: #265680;
+}
+.slider-container .slider-selection {
+  background: #CCC;
+  box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.3);
+}
+.slider-container input {
+  display: none;
+}
+.alert.alert-info a {
+  text-decoration: underline;
+}
+.btn.disabled:active,
+.btn.disabled:focus,
+.btn.disabled:hover {
+  color: #000;
+}
+[data-toggle~="dropdown"] {
+  cursor: pointer;
+}
+.fa {
+  cursor: default;
+}
+.list-unstyled {
+  margin: 0;
+}
+.highlight,
+mark {
+  background: #ffff66;
+  padding: .1em .2em;
+}
+.icon-bar {
+  background-color: #888;
+}
+img {
+  max-width: 100%;
+}
+.popover {
+  width: 250px;
+}
+.sub-breadcrumb {
+  padding: 5px 10px;
+  white-space: nowrap;
+}
+.sub-breadcrumb li {
+  display: inline-block;
+}
+.sub-breadcrumb li + li:before {
+  padding-left: 5px;
+  padding-right: 5px;
+  color: #cccccc;
+  content: "/\00a0";
+}
+.tab-content {
+  padding: 4px;
+}
+@media (max-width: 991px) {
+  header .container.navbar {
+    margin-bottom: 0;
+  }
+  .searchForm {
+    margin-top: 0;
+  }
+}
+@media (min-width: 768px) {
+  h2 {
+    font-size: 23px;
+    font-weight: normal;
+  }
+  h3 {
+    font-size: 20px;
+    font-weight: normal;
+  }
+  .form-control {
+    max-width: 400px;
   }
 }
+@media (max-width: 767px) {
+  h2 {
+    font-size: 20px;
+  }
+  h3 {
+    font-size: 16px;
+  }
+  .searchForm {
+    padding-top: 0;
+  }
+}
+/* --- Form Errors --- */
+.has-error {
+  margin-bottom: 0;
+}
+.sms-error {
+  margin-bottom: 0;
+}
+.help-block.with-errors {
+  margin: 0;
+  padding-top: 6px;
+  padding-bottom: 6px;
+}
+.help-block.with-errors:empty {
+  padding: 0;
+}
+/* --- Badges - blend the links in --- */
+.badge a {
+  color: #fff;
+}
+/* --- Browse --- */
+.browse.list-group .list-group-item {
+  word-wrap: break-word;
+}
+.browse.list-group .list-group-item.view-record {
+  padding: 2px 4px;
+  font-size: 85%;
+  text-align: right;
+  border-top: 0;
+}
+/* --- Cart --- */
+.cart-controls .checkbox {
+  line-height: 2.5em;
+  padding-right: 1em;
+}
+/* --- PubDateVis --- */
+#dateVisColorSettings {
+  background-color: #fff;
+  fill: #eaeaea;
+  outline-color: #e8cfac;
+  stroke: #265680;
+}
+/* --- Table wrapping to prevent horizontal overflow --- */
+.table {
+  table-layout: fixed;
+  word-wrap: break-word;
+}
+/* --- Visualization View --- */
+.node {
+  position: absolute;
+  box-sizing: content-box;
+  margin: -1px;
+  overflow: hidden;
+  font: 10px sans-serif;
+  line-height: 12px;
+  border: 1px solid white;
+}
+.node div {
+  margin-top: 0px;
+}
+.toplevel {
+  border: 2px solid black;
+}
+.node .label {
+  position: absolute;
+  bottom: 0;
+  left: 0;
+  min-height: 1px;
+  padding: 2px 4px;
+  font-size: 85%;
+  background-color: rgba(0, 0, 0, 0.5);
+  border-radius: 0;
+  text-shadow: none;
+}
+.notalabel {
+  color: #000;
+}
+#viz-instructions {
+  padding-top: 600px;
+}
+span[class^="services-"],
+span[class*=" services-"] span::before {
+  content: ", ";
+}
+span[class^="services-"],
+span[class*=" services-"] span:first-of-type::before {
+  content: "";
+}
diff --git a/themes/bootstrap3/css/fonts/FontAwesome.otf b/themes/bootstrap3/css/fonts/FontAwesome.otf
index 81c9ad949b47f64afeca5642ee2494b6e3147f44..d4de13e832d567ff29c5b4e9561b8c370348cc9c 100644
Binary files a/themes/bootstrap3/css/fonts/FontAwesome.otf and b/themes/bootstrap3/css/fonts/FontAwesome.otf differ
diff --git a/themes/bootstrap3/css/fonts/fontawesome-webfont.eot b/themes/bootstrap3/css/fonts/fontawesome-webfont.eot
index 84677bc0c5f37f1fac9d87548c4554b5c91717cf..c7b00d2ba8896fd29de846b19f89fcf0d56ad152 100644
Binary files a/themes/bootstrap3/css/fonts/fontawesome-webfont.eot and b/themes/bootstrap3/css/fonts/fontawesome-webfont.eot differ
diff --git a/themes/bootstrap3/css/fonts/fontawesome-webfont.svg b/themes/bootstrap3/css/fonts/fontawesome-webfont.svg
index d907b25ae60ec7e3d32e4027aa6e6b7595de97af..8b66187fe067c3aa389ce8c98108f349ceae159c 100644
--- a/themes/bootstrap3/css/fonts/fontawesome-webfont.svg
+++ b/themes/bootstrap3/css/fonts/fontawesome-webfont.svg
@@ -147,14 +147,14 @@
 <glyph unicode="&#xf077;" horiz-adv-x="1792" d="M1683 205l-166 -165q-19 -19 -45 -19t-45 19l-531 531l-531 -531q-19 -19 -45 -19t-45 19l-166 165q-19 19 -19 45.5t19 45.5l742 741q19 19 45 19t45 -19l742 -741q19 -19 19 -45.5t-19 -45.5z" />
 <glyph unicode="&#xf078;" horiz-adv-x="1792" d="M1683 728l-742 -741q-19 -19 -45 -19t-45 19l-742 741q-19 19 -19 45.5t19 45.5l166 165q19 19 45 19t45 -19l531 -531l531 531q19 19 45 19t45 -19l166 -165q19 -19 19 -45.5t-19 -45.5z" />
 <glyph unicode="&#xf079;" horiz-adv-x="1920" d="M1280 32q0 -13 -9.5 -22.5t-22.5 -9.5h-960q-8 0 -13.5 2t-9 7t-5.5 8t-3 11.5t-1 11.5v13v11v160v416h-192q-26 0 -45 19t-19 45q0 24 15 41l320 384q19 22 49 22t49 -22l320 -384q15 -17 15 -41q0 -26 -19 -45t-45 -19h-192v-384h576q16 0 25 -11l160 -192q7 -11 7 -21 zM1920 448q0 -24 -15 -41l-320 -384q-20 -23 -49 -23t-49 23l-320 384q-15 17 -15 41q0 26 19 45t45 19h192v384h-576q-16 0 -25 12l-160 192q-7 9 -7 20q0 13 9.5 22.5t22.5 9.5h960q8 0 13.5 -2t9 -7t5.5 -8t3 -11.5t1 -11.5v-13v-11v-160v-416h192q26 0 45 -19t19 -45z " />
-<glyph unicode="&#xf07a;" horiz-adv-x="1664" d="M640 0q0 -53 -37.5 -90.5t-90.5 -37.5t-90.5 37.5t-37.5 90.5t37.5 90.5t90.5 37.5t90.5 -37.5t37.5 -90.5zM1536 0q0 -53 -37.5 -90.5t-90.5 -37.5t-90.5 37.5t-37.5 90.5t37.5 90.5t90.5 37.5t90.5 -37.5t37.5 -90.5zM1664 1088v-512q0 -24 -16 -42.5t-41 -21.5 l-1044 -122q1 -7 4.5 -21.5t6 -26.5t2.5 -22q0 -16 -24 -64h920q26 0 45 -19t19 -45t-19 -45t-45 -19h-1024q-26 0 -45 19t-19 45q0 14 11 39.5t29.5 59.5t20.5 38l-177 823h-204q-26 0 -45 19t-19 45t19 45t45 19h256q16 0 28.5 -6.5t20 -15.5t13 -24.5t7.5 -26.5 t5.5 -29.5t4.5 -25.5h1201q26 0 45 -19t19 -45z" />
+<glyph unicode="&#xf07a;" horiz-adv-x="1664" d="M640 0q0 -52 -38 -90t-90 -38t-90 38t-38 90t38 90t90 38t90 -38t38 -90zM1536 0q0 -52 -38 -90t-90 -38t-90 38t-38 90t38 90t90 38t90 -38t38 -90zM1664 1088v-512q0 -24 -16.5 -42.5t-40.5 -21.5l-1044 -122q13 -60 13 -70q0 -16 -24 -64h920q26 0 45 -19t19 -45 t-19 -45t-45 -19h-1024q-26 0 -45 19t-19 45q0 11 8 31.5t16 36t21.5 40t15.5 29.5l-177 823h-204q-26 0 -45 19t-19 45t19 45t45 19h256q16 0 28.5 -6.5t19.5 -15.5t13 -24.5t8 -26t5.5 -29.5t4.5 -26h1201q26 0 45 -19t19 -45z" />
 <glyph unicode="&#xf07b;" horiz-adv-x="1664" d="M1664 928v-704q0 -92 -66 -158t-158 -66h-1216q-92 0 -158 66t-66 158v960q0 92 66 158t158 66h320q92 0 158 -66t66 -158v-32h672q92 0 158 -66t66 -158z" />
 <glyph unicode="&#xf07c;" horiz-adv-x="1920" d="M1879 584q0 -31 -31 -66l-336 -396q-43 -51 -120.5 -86.5t-143.5 -35.5h-1088q-34 0 -60.5 13t-26.5 43q0 31 31 66l336 396q43 51 120.5 86.5t143.5 35.5h1088q34 0 60.5 -13t26.5 -43zM1536 928v-160h-832q-94 0 -197 -47.5t-164 -119.5l-337 -396l-5 -6q0 4 -0.5 12.5 t-0.5 12.5v960q0 92 66 158t158 66h320q92 0 158 -66t66 -158v-32h544q92 0 158 -66t66 -158z" />
 <glyph unicode="&#xf07d;" horiz-adv-x="768" d="M704 1216q0 -26 -19 -45t-45 -19h-128v-1024h128q26 0 45 -19t19 -45t-19 -45l-256 -256q-19 -19 -45 -19t-45 19l-256 256q-19 19 -19 45t19 45t45 19h128v1024h-128q-26 0 -45 19t-19 45t19 45l256 256q19 19 45 19t45 -19l256 -256q19 -19 19 -45z" />
 <glyph unicode="&#xf07e;" horiz-adv-x="1792" d="M1792 640q0 -26 -19 -45l-256 -256q-19 -19 -45 -19t-45 19t-19 45v128h-1024v-128q0 -26 -19 -45t-45 -19t-45 19l-256 256q-19 19 -19 45t19 45l256 256q19 19 45 19t45 -19t19 -45v-128h1024v128q0 26 19 45t45 19t45 -19l256 -256q19 -19 19 -45z" />
 <glyph unicode="&#xf080;" horiz-adv-x="2048" d="M640 640v-512h-256v512h256zM1024 1152v-1024h-256v1024h256zM2048 0v-128h-2048v1536h128v-1408h1920zM1408 896v-768h-256v768h256zM1792 1280v-1152h-256v1152h256z" />
 <glyph unicode="&#xf081;" d="M1280 926q-56 -25 -121 -34q68 40 93 117q-65 -38 -134 -51q-61 66 -153 66q-87 0 -148.5 -61.5t-61.5 -148.5q0 -29 5 -48q-129 7 -242 65t-192 155q-29 -50 -29 -106q0 -114 91 -175q-47 1 -100 26v-2q0 -75 50 -133.5t123 -72.5q-29 -8 -51 -8q-13 0 -39 4 q21 -63 74.5 -104t121.5 -42q-116 -90 -261 -90q-26 0 -50 3q148 -94 322 -94q112 0 210 35.5t168 95t120.5 137t75 162t24.5 168.5q0 18 -1 27q63 45 105 109zM1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5 t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" />
-<glyph unicode="&#xf082;" d="M1536 160q0 -119 -84.5 -203.5t-203.5 -84.5h-192v608h203l30 224h-233v143q0 54 28 83t96 29l132 1v207q-96 9 -180 9q-136 0 -218 -80.5t-82 -225.5v-166h-224v-224h224v-608h-544q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960 q119 0 203.5 -84.5t84.5 -203.5v-960z" />
+<glyph unicode="&#xf082;" d="M1248 1408q119 0 203.5 -84.5t84.5 -203.5v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-188v595h199l30 232h-229v148q0 56 23.5 84t91.5 28l122 1v207q-63 9 -178 9q-136 0 -217.5 -80t-81.5 -226v-171h-200v-232h200v-595h-532q-119 0 -203.5 84.5t-84.5 203.5v960 q0 119 84.5 203.5t203.5 84.5h960z" />
 <glyph unicode="&#xf083;" horiz-adv-x="1792" d="M928 704q0 14 -9 23t-23 9q-66 0 -113 -47t-47 -113q0 -14 9 -23t23 -9t23 9t9 23q0 40 28 68t68 28q14 0 23 9t9 23zM1152 574q0 -106 -75 -181t-181 -75t-181 75t-75 181t75 181t181 75t181 -75t75 -181zM128 0h1536v128h-1536v-128zM1280 574q0 159 -112.5 271.5 t-271.5 112.5t-271.5 -112.5t-112.5 -271.5t112.5 -271.5t271.5 -112.5t271.5 112.5t112.5 271.5zM256 1216h384v128h-384v-128zM128 1024h1536v118v138h-828l-64 -128h-644v-128zM1792 1280v-1280q0 -53 -37.5 -90.5t-90.5 -37.5h-1536q-53 0 -90.5 37.5t-37.5 90.5v1280 q0 53 37.5 90.5t90.5 37.5h1536q53 0 90.5 -37.5t37.5 -90.5z" />
 <glyph unicode="&#xf084;" horiz-adv-x="1792" d="M832 1024q0 80 -56 136t-136 56t-136 -56t-56 -136q0 -42 19 -83q-41 19 -83 19q-80 0 -136 -56t-56 -136t56 -136t136 -56t136 56t56 136q0 42 -19 83q41 -19 83 -19q80 0 136 56t56 136zM1683 320q0 -17 -49 -66t-66 -49q-9 0 -28.5 16t-36.5 33t-38.5 40t-24.5 26 l-96 -96l220 -220q28 -28 28 -68q0 -42 -39 -81t-81 -39q-40 0 -68 28l-671 671q-176 -131 -365 -131q-163 0 -265.5 102.5t-102.5 265.5q0 160 95 313t248 248t313 95q163 0 265.5 -102.5t102.5 -265.5q0 -189 -131 -365l355 -355l96 96q-3 3 -26 24.5t-40 38.5t-33 36.5 t-16 28.5q0 17 49 66t66 49q13 0 23 -10q6 -6 46 -44.5t82 -79.5t86.5 -86t73 -78t28.5 -41z" />
 <glyph unicode="&#xf085;" horiz-adv-x="1920" d="M896 640q0 106 -75 181t-181 75t-181 -75t-75 -181t75 -181t181 -75t181 75t75 181zM1664 128q0 52 -38 90t-90 38t-90 -38t-38 -90q0 -53 37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM1664 1152q0 52 -38 90t-90 38t-90 -38t-38 -90q0 -53 37.5 -90.5t90.5 -37.5 t90.5 37.5t37.5 90.5zM1280 731v-185q0 -10 -7 -19.5t-16 -10.5l-155 -24q-11 -35 -32 -76q34 -48 90 -115q7 -10 7 -20q0 -12 -7 -19q-23 -30 -82.5 -89.5t-78.5 -59.5q-11 0 -21 7l-115 90q-37 -19 -77 -31q-11 -108 -23 -155q-7 -24 -30 -24h-186q-11 0 -20 7.5t-10 17.5 l-23 153q-34 10 -75 31l-118 -89q-7 -7 -20 -7q-11 0 -21 8q-144 133 -144 160q0 9 7 19q10 14 41 53t47 61q-23 44 -35 82l-152 24q-10 1 -17 9.5t-7 19.5v185q0 10 7 19.5t16 10.5l155 24q11 35 32 76q-34 48 -90 115q-7 11 -7 20q0 12 7 20q22 30 82 89t79 59q11 0 21 -7 l115 -90q34 18 77 32q11 108 23 154q7 24 30 24h186q11 0 20 -7.5t10 -17.5l23 -153q34 -10 75 -31l118 89q8 7 20 7q11 0 21 -8q144 -133 144 -160q0 -9 -7 -19q-12 -16 -42 -54t-45 -60q23 -48 34 -82l152 -23q10 -2 17 -10.5t7 -19.5zM1920 198v-140q0 -16 -149 -31 q-12 -27 -30 -52q51 -113 51 -138q0 -4 -4 -7q-122 -71 -124 -71q-8 0 -46 47t-52 68q-20 -2 -30 -2t-30 2q-14 -21 -52 -68t-46 -47q-2 0 -124 71q-4 3 -4 7q0 25 51 138q-18 25 -30 52q-149 15 -149 31v140q0 16 149 31q13 29 30 52q-51 113 -51 138q0 4 4 7q4 2 35 20 t59 34t30 16q8 0 46 -46.5t52 -67.5q20 2 30 2t30 -2q51 71 92 112l6 2q4 0 124 -70q4 -3 4 -7q0 -25 -51 -138q17 -23 30 -52q149 -15 149 -31zM1920 1222v-140q0 -16 -149 -31q-12 -27 -30 -52q51 -113 51 -138q0 -4 -4 -7q-122 -71 -124 -71q-8 0 -46 47t-52 68 q-20 -2 -30 -2t-30 2q-14 -21 -52 -68t-46 -47q-2 0 -124 71q-4 3 -4 7q0 25 51 138q-18 25 -30 52q-149 15 -149 31v140q0 16 149 31q13 29 30 52q-51 113 -51 138q0 4 4 7q4 2 35 20t59 34t30 16q8 0 46 -46.5t52 -67.5q20 2 30 2t30 -2q51 71 92 112l6 2q4 0 124 -70 q4 -3 4 -7q0 -25 -51 -138q17 -23 30 -52q149 -15 149 -31z" />
@@ -169,7 +169,7 @@
 <glyph unicode="&#xf08e;" horiz-adv-x="1792" d="M1408 608v-320q0 -119 -84.5 -203.5t-203.5 -84.5h-832q-119 0 -203.5 84.5t-84.5 203.5v832q0 119 84.5 203.5t203.5 84.5h704q14 0 23 -9t9 -23v-64q0 -14 -9 -23t-23 -9h-704q-66 0 -113 -47t-47 -113v-832q0 -66 47 -113t113 -47h832q66 0 113 47t47 113v320 q0 14 9 23t23 9h64q14 0 23 -9t9 -23zM1792 1472v-512q0 -26 -19 -45t-45 -19t-45 19l-176 176l-652 -652q-10 -10 -23 -10t-23 10l-114 114q-10 10 -10 23t10 23l652 652l-176 176q-19 19 -19 45t19 45t45 19h512q26 0 45 -19t19 -45z" />
 <glyph unicode="&#xf090;" d="M1184 640q0 -26 -19 -45l-544 -544q-19 -19 -45 -19t-45 19t-19 45v288h-448q-26 0 -45 19t-19 45v384q0 26 19 45t45 19h448v288q0 26 19 45t45 19t45 -19l544 -544q19 -19 19 -45zM1536 992v-704q0 -119 -84.5 -203.5t-203.5 -84.5h-320q-13 0 -22.5 9.5t-9.5 22.5 q0 4 -1 20t-0.5 26.5t3 23.5t10 19.5t20.5 6.5h320q66 0 113 47t47 113v704q0 66 -47 113t-113 47h-288h-11h-13t-11.5 1t-11.5 3t-8 5.5t-7 9t-2 13.5q0 4 -1 20t-0.5 26.5t3 23.5t10 19.5t20.5 6.5h320q119 0 203.5 -84.5t84.5 -203.5z" />
 <glyph unicode="&#xf091;" horiz-adv-x="1664" d="M458 653q-74 162 -74 371h-256v-96q0 -78 94.5 -162t235.5 -113zM1536 928v96h-256q0 -209 -74 -371q141 29 235.5 113t94.5 162zM1664 1056v-128q0 -71 -41.5 -143t-112 -130t-173 -97.5t-215.5 -44.5q-42 -54 -95 -95q-38 -34 -52.5 -72.5t-14.5 -89.5q0 -54 30.5 -91 t97.5 -37q75 0 133.5 -45.5t58.5 -114.5v-64q0 -14 -9 -23t-23 -9h-832q-14 0 -23 9t-9 23v64q0 69 58.5 114.5t133.5 45.5q67 0 97.5 37t30.5 91q0 51 -14.5 89.5t-52.5 72.5q-53 41 -95 95q-113 5 -215.5 44.5t-173 97.5t-112 130t-41.5 143v128q0 40 28 68t68 28h288v96 q0 66 47 113t113 47h576q66 0 113 -47t47 -113v-96h288q40 0 68 -28t28 -68z" />
-<glyph unicode="&#xf092;" d="M394 184q-8 -9 -20 3q-13 11 -4 19q8 9 20 -3q12 -11 4 -19zM352 245q9 -12 0 -19q-8 -6 -17 7t0 18q9 7 17 -6zM291 305q-5 -7 -13 -2q-10 5 -7 12q3 5 13 2q10 -5 7 -12zM322 271q-6 -7 -16 3q-9 11 -2 16q6 6 16 -3q9 -11 2 -16zM451 159q-4 -12 -19 -6q-17 4 -13 15 t19 7q16 -5 13 -16zM514 154q0 -11 -16 -11q-17 -2 -17 11q0 11 16 11q17 2 17 -11zM572 164q2 -10 -14 -14t-18 8t14 15q16 2 18 -9zM1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-224q-16 0 -24.5 1t-19.5 5t-16 14.5t-5 27.5v239q0 97 -52 142q57 6 102.5 18t94 39 t81 66.5t53 105t20.5 150.5q0 121 -79 206q37 91 -8 204q-28 9 -81 -11t-92 -44l-38 -24q-93 26 -192 26t-192 -26q-16 11 -42.5 27t-83.5 38.5t-86 13.5q-44 -113 -7 -204q-79 -85 -79 -206q0 -85 20.5 -150t52.5 -105t80.5 -67t94 -39t102.5 -18q-40 -36 -49 -103 q-21 -10 -45 -15t-57 -5t-65.5 21.5t-55.5 62.5q-19 32 -48.5 52t-49.5 24l-20 3q-21 0 -29 -4.5t-5 -11.5t9 -14t13 -12l7 -5q22 -10 43.5 -38t31.5 -51l10 -23q13 -38 44 -61.5t67 -30t69.5 -7t55.5 3.5l23 4q0 -38 0.5 -103t0.5 -68q0 -22 -11 -33.5t-22 -13t-33 -1.5 h-224q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" />
+<glyph unicode="&#xf092;" d="M519 336q4 6 -3 13q-9 7 -14 2q-4 -6 3 -13q9 -7 14 -2zM491 377q-5 7 -12 4q-6 -4 0 -12q7 -8 12 -5q6 4 0 13zM450 417q2 4 -5 8q-7 2 -8 -2q-3 -5 4 -8q8 -2 9 2zM471 394q2 1 1.5 4.5t-3.5 5.5q-6 7 -10 3t1 -11q6 -6 11 -2zM557 319q2 7 -9 11q-9 3 -13 -4 q-2 -7 9 -11q9 -3 13 4zM599 316q0 8 -12 8q-10 0 -10 -8t11 -8t11 8zM638 323q-2 7 -13 5t-9 -9q2 -8 12 -6t10 10zM1280 640q0 212 -150 362t-362 150t-362 -150t-150 -362q0 -167 98 -300.5t252 -185.5q18 -3 26.5 5t8.5 20q0 52 -1 95q-6 -1 -15.5 -2.5t-35.5 -2t-48 4 t-43.5 20t-29.5 41.5q-23 59 -57 74q-2 1 -4.5 3.5l-8 8t-7 9.5t4 7.5t19.5 3.5q6 0 15 -2t30 -15.5t33 -35.5q16 -28 37.5 -42t43.5 -14t38 3.5t30 9.5q7 47 33 69q-49 6 -86 18.5t-73 39t-55.5 76t-19.5 119.5q0 79 53 137q-24 62 5 136q19 6 54.5 -7.5t60.5 -29.5l26 -16 q58 17 128 17t128 -17q11 7 28.5 18t55.5 26t57 9q29 -74 5 -136q53 -58 53 -137q0 -57 -14 -100.5t-35.5 -70t-53.5 -44.5t-62.5 -26t-68.5 -12q35 -31 35 -95q0 -40 -0.5 -89t-0.5 -51q0 -12 8.5 -20t26.5 -5q154 52 252 185.5t98 300.5zM1536 1120v-960 q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" />
 <glyph unicode="&#xf093;" horiz-adv-x="1664" d="M1280 64q0 26 -19 45t-45 19t-45 -19t-19 -45t19 -45t45 -19t45 19t19 45zM1536 64q0 26 -19 45t-45 19t-45 -19t-19 -45t19 -45t45 -19t45 19t19 45zM1664 288v-320q0 -40 -28 -68t-68 -28h-1472q-40 0 -68 28t-28 68v320q0 40 28 68t68 28h427q21 -56 70.5 -92 t110.5 -36h256q61 0 110.5 36t70.5 92h427q40 0 68 -28t28 -68zM1339 936q-17 -40 -59 -40h-256v-448q0 -26 -19 -45t-45 -19h-256q-26 0 -45 19t-19 45v448h-256q-42 0 -59 40q-17 39 14 69l448 448q18 19 45 19t45 -19l448 -448q31 -30 14 -69z" />
 <glyph unicode="&#xf094;" d="M1407 710q0 44 -7 113.5t-18 96.5q-12 30 -17 44t-9 36.5t-4 48.5q0 23 5 68.5t5 67.5q0 37 -10 55q-4 1 -13 1q-19 0 -58 -4.5t-59 -4.5q-60 0 -176 24t-175 24q-43 0 -94.5 -11.5t-85 -23.5t-89.5 -34q-137 -54 -202 -103q-96 -73 -159.5 -189.5t-88 -236t-24.5 -248.5 q0 -40 12.5 -120t12.5 -121q0 -23 -11 -66.5t-11 -65.5t12 -36.5t34 -14.5q24 0 72.5 11t73.5 11q57 0 169.5 -15.5t169.5 -15.5q181 0 284 36q129 45 235.5 152.5t166 245.5t59.5 275zM1535 712q0 -165 -70 -327.5t-196 -288t-281 -180.5q-124 -44 -326 -44 q-57 0 -170 14.5t-169 14.5q-24 0 -72.5 -14.5t-73.5 -14.5q-73 0 -123.5 55.5t-50.5 128.5q0 24 11 68t11 67q0 40 -12.5 120.5t-12.5 121.5q0 111 18 217.5t54.5 209.5t100.5 194t150 156q78 59 232 120q194 78 316 78q60 0 175.5 -24t173.5 -24q19 0 57 5t58 5 q81 0 118 -50.5t37 -134.5q0 -23 -5 -68t-5 -68q0 -10 1 -18.5t3 -17t4 -13.5t6.5 -16t6.5 -17q16 -40 25 -118.5t9 -136.5z" />
 <glyph unicode="&#xf095;" horiz-adv-x="1408" d="M1408 296q0 -27 -10 -70.5t-21 -68.5q-21 -50 -122 -106q-94 -51 -186 -51q-27 0 -52.5 3.5t-57.5 12.5t-47.5 14.5t-55.5 20.5t-49 18q-98 35 -175 83q-128 79 -264.5 215.5t-215.5 264.5q-48 77 -83 175q-3 9 -18 49t-20.5 55.5t-14.5 47.5t-12.5 57.5t-3.5 52.5 q0 92 51 186q56 101 106 122q25 11 68.5 21t70.5 10q14 0 21 -3q18 -6 53 -76q11 -19 30 -54t35 -63.5t31 -53.5q3 -4 17.5 -25t21.5 -35.5t7 -28.5q0 -20 -28.5 -50t-62 -55t-62 -53t-28.5 -46q0 -9 5 -22.5t8.5 -20.5t14 -24t11.5 -19q76 -137 174 -235t235 -174 q2 -1 19 -11.5t24 -14t20.5 -8.5t22.5 -5q18 0 46 28.5t53 62t55 62t50 28.5q14 0 28.5 -7t35.5 -21.5t25 -17.5q25 -15 53.5 -31t63.5 -35t54 -30q70 -35 76 -53q3 -7 3 -21z" />
@@ -178,7 +178,7 @@
 <glyph unicode="&#xf098;" d="M1280 343q0 11 -2 16q-3 8 -38.5 29.5t-88.5 49.5l-53 29q-5 3 -19 13t-25 15t-21 5q-18 0 -47 -32.5t-57 -65.5t-44 -33q-7 0 -16.5 3.5t-15.5 6.5t-17 9.5t-14 8.5q-99 55 -170.5 126.5t-126.5 170.5q-2 3 -8.5 14t-9.5 17t-6.5 15.5t-3.5 16.5q0 13 20.5 33.5t45 38.5 t45 39.5t20.5 36.5q0 10 -5 21t-15 25t-13 19q-3 6 -15 28.5t-25 45.5t-26.5 47.5t-25 40.5t-16.5 18t-16 2q-48 0 -101 -22q-46 -21 -80 -94.5t-34 -130.5q0 -16 2.5 -34t5 -30.5t9 -33t10 -29.5t12.5 -33t11 -30q60 -164 216.5 -320.5t320.5 -216.5q6 -2 30 -11t33 -12.5 t29.5 -10t33 -9t30.5 -5t34 -2.5q57 0 130.5 34t94.5 80q22 53 22 101zM1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" />
 <glyph unicode="&#xf099;" horiz-adv-x="1664" d="M1620 1128q-67 -98 -162 -167q1 -14 1 -42q0 -130 -38 -259.5t-115.5 -248.5t-184.5 -210.5t-258 -146t-323 -54.5q-271 0 -496 145q35 -4 78 -4q225 0 401 138q-105 2 -188 64.5t-114 159.5q33 -5 61 -5q43 0 85 11q-112 23 -185.5 111.5t-73.5 205.5v4q68 -38 146 -41 q-66 44 -105 115t-39 154q0 88 44 163q121 -149 294.5 -238.5t371.5 -99.5q-8 38 -8 74q0 134 94.5 228.5t228.5 94.5q140 0 236 -102q109 21 205 78q-37 -115 -142 -178q93 10 186 50z" />
 <glyph unicode="&#xf09a;" horiz-adv-x="1024" d="M959 1524v-264h-157q-86 0 -116 -36t-30 -108v-189h293l-39 -296h-254v-759h-306v759h-255v296h255v218q0 186 104 288.5t277 102.5q147 0 228 -12z" />
-<glyph unicode="&#xf09b;" d="M1536 640q0 -251 -146.5 -451.5t-378.5 -277.5q-27 -5 -39.5 7t-12.5 30v211q0 97 -52 142q57 6 102.5 18t94 39t81 66.5t53 105t20.5 150.5q0 121 -79 206q37 91 -8 204q-28 9 -81 -11t-92 -44l-38 -24q-93 26 -192 26t-192 -26q-16 11 -42.5 27t-83.5 38.5t-86 13.5 q-44 -113 -7 -204q-79 -85 -79 -206q0 -85 20.5 -150t52.5 -105t80.5 -67t94 -39t102.5 -18q-40 -36 -49 -103q-21 -10 -45 -15t-57 -5t-65.5 21.5t-55.5 62.5q-19 32 -48.5 52t-49.5 24l-20 3q-21 0 -29 -4.5t-5 -11.5t9 -14t13 -12l7 -5q22 -10 43.5 -38t31.5 -51l10 -23 q13 -38 44 -61.5t67 -30t69.5 -7t55.5 3.5l23 4q0 -38 0.5 -89t0.5 -54q0 -18 -13 -30t-40 -7q-232 77 -378.5 277.5t-146.5 451.5q0 209 103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" />
+<glyph unicode="&#xf09b;" d="M768 1408q209 0 385.5 -103t279.5 -279.5t103 -385.5q0 -251 -146.5 -451.5t-378.5 -277.5q-27 -5 -40 7t-13 30q0 3 0.5 76.5t0.5 134.5q0 97 -52 142q57 6 102.5 18t94 39t81 66.5t53 105t20.5 150.5q0 119 -79 206q37 91 -8 204q-28 9 -81 -11t-92 -44l-38 -24 q-93 26 -192 26t-192 -26q-16 11 -42.5 27t-83.5 38.5t-85 13.5q-45 -113 -8 -204q-79 -87 -79 -206q0 -85 20.5 -150t52.5 -105t80.5 -67t94 -39t102.5 -18q-39 -36 -49 -103q-21 -10 -45 -15t-57 -5t-65.5 21.5t-55.5 62.5q-19 32 -48.5 52t-49.5 24l-20 3q-21 0 -29 -4.5 t-5 -11.5t9 -14t13 -12l7 -5q22 -10 43.5 -38t31.5 -51l10 -23q13 -38 44 -61.5t67 -30t69.5 -7t55.5 3.5l23 4q0 -38 0.5 -88.5t0.5 -54.5q0 -18 -13 -30t-40 -7q-232 77 -378.5 277.5t-146.5 451.5q0 209 103 385.5t279.5 279.5t385.5 103zM291 305q3 7 -7 12 q-10 3 -13 -2q-3 -7 7 -12q9 -6 13 2zM322 271q7 5 -2 16q-10 9 -16 3q-7 -5 2 -16q10 -10 16 -3zM352 226q9 7 0 19q-8 13 -17 6q-9 -5 0 -18t17 -7zM394 184q8 8 -4 19q-12 12 -20 3q-9 -8 4 -19q12 -12 20 -3zM451 159q3 11 -13 16q-15 4 -19 -7t13 -15q15 -6 19 6z M514 154q0 13 -17 11q-16 0 -16 -11q0 -13 17 -11q16 0 16 11zM572 164q-2 11 -18 9q-16 -3 -14 -15t18 -8t14 14z" />
 <glyph unicode="&#xf09c;" horiz-adv-x="1664" d="M1664 960v-256q0 -26 -19 -45t-45 -19h-64q-26 0 -45 19t-19 45v256q0 106 -75 181t-181 75t-181 -75t-75 -181v-192h96q40 0 68 -28t28 -68v-576q0 -40 -28 -68t-68 -28h-960q-40 0 -68 28t-28 68v576q0 40 28 68t68 28h672v192q0 185 131.5 316.5t316.5 131.5 t316.5 -131.5t131.5 -316.5z" />
 <glyph unicode="&#xf09d;" horiz-adv-x="1920" d="M1760 1408q66 0 113 -47t47 -113v-1216q0 -66 -47 -113t-113 -47h-1600q-66 0 -113 47t-47 113v1216q0 66 47 113t113 47h1600zM160 1280q-13 0 -22.5 -9.5t-9.5 -22.5v-224h1664v224q0 13 -9.5 22.5t-22.5 9.5h-1600zM1760 0q13 0 22.5 9.5t9.5 22.5v608h-1664v-608 q0 -13 9.5 -22.5t22.5 -9.5h1600zM256 128v128h256v-128h-256zM640 128v128h384v-128h-384z" />
 <glyph unicode="&#xf09e;" horiz-adv-x="1408" d="M384 192q0 -80 -56 -136t-136 -56t-136 56t-56 136t56 136t136 56t136 -56t56 -136zM896 69q2 -28 -17 -48q-18 -21 -47 -21h-135q-25 0 -43 16.5t-20 41.5q-22 229 -184.5 391.5t-391.5 184.5q-25 2 -41.5 20t-16.5 43v135q0 29 21 47q17 17 43 17h5q160 -13 306 -80.5 t259 -181.5q114 -113 181.5 -259t80.5 -306zM1408 67q2 -27 -18 -47q-18 -20 -46 -20h-143q-26 0 -44.5 17.5t-19.5 42.5q-12 215 -101 408.5t-231.5 336t-336 231.5t-408.5 102q-25 1 -42.5 19.5t-17.5 43.5v143q0 28 20 46q18 18 44 18h3q262 -13 501.5 -120t425.5 -294 q187 -186 294 -425.5t120 -501.5z" />
@@ -219,8 +219,8 @@
 <glyph unicode="&#xf0d1;" horiz-adv-x="1792" d="M640 128q0 52 -38 90t-90 38t-90 -38t-38 -90t38 -90t90 -38t90 38t38 90zM256 640h384v256h-158q-13 0 -22 -9l-195 -195q-9 -9 -9 -22v-30zM1536 128q0 52 -38 90t-90 38t-90 -38t-38 -90t38 -90t90 -38t90 38t38 90zM1792 1216v-1024q0 -15 -4 -26.5t-13.5 -18.5 t-16.5 -11.5t-23.5 -6t-22.5 -2t-25.5 0t-22.5 0.5q0 -106 -75 -181t-181 -75t-181 75t-75 181h-384q0 -106 -75 -181t-181 -75t-181 75t-75 181h-64q-3 0 -22.5 -0.5t-25.5 0t-22.5 2t-23.5 6t-16.5 11.5t-13.5 18.5t-4 26.5q0 26 19 45t45 19v320q0 8 -0.5 35t0 38 t2.5 34.5t6.5 37t14 30.5t22.5 30l198 198q19 19 50.5 32t58.5 13h160v192q0 26 19 45t45 19h1024q26 0 45 -19t19 -45z" />
 <glyph unicode="&#xf0d2;" d="M1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103q-111 0 -218 32q59 93 78 164q9 34 54 211q20 -39 73 -67.5t114 -28.5q121 0 216 68.5t147 188.5t52 270q0 114 -59.5 214t-172.5 163t-255 63q-105 0 -196 -29t-154.5 -77t-109 -110.5t-67 -129.5t-21.5 -134 q0 -104 40 -183t117 -111q30 -12 38 20q2 7 8 31t8 30q6 23 -11 43q-51 61 -51 151q0 151 104.5 259.5t273.5 108.5q151 0 235.5 -82t84.5 -213q0 -170 -68.5 -289t-175.5 -119q-61 0 -98 43.5t-23 104.5q8 35 26.5 93.5t30 103t11.5 75.5q0 50 -27 83t-77 33 q-62 0 -105 -57t-43 -142q0 -73 25 -122l-99 -418q-17 -70 -13 -177q-206 91 -333 281t-127 423q0 209 103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" />
 <glyph unicode="&#xf0d3;" d="M1248 1408q119 0 203.5 -84.5t84.5 -203.5v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-725q85 122 108 210q9 34 53 209q21 -39 73.5 -67t112.5 -28q181 0 295.5 147.5t114.5 373.5q0 84 -35 162.5t-96.5 139t-152.5 97t-197 36.5q-104 0 -194.5 -28.5t-153 -76.5 t-107.5 -109.5t-66.5 -128t-21.5 -132.5q0 -102 39.5 -180t116.5 -110q13 -5 23.5 0t14.5 19q10 44 15 61q6 23 -11 42q-50 62 -50 150q0 150 103.5 256.5t270.5 106.5q149 0 232.5 -81t83.5 -210q0 -168 -67.5 -286t-173.5 -118q-60 0 -97 43.5t-23 103.5q8 34 26.5 92.5 t29.5 102t11 74.5q0 49 -26.5 81.5t-75.5 32.5q-61 0 -103.5 -56.5t-42.5 -139.5q0 -72 24 -121l-98 -414q-24 -100 -7 -254h-183q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960z" />
-<glyph unicode="&#xf0d4;" d="M829 318q0 -76 -58.5 -112.5t-139.5 -36.5q-41 0 -80.5 9.5t-75.5 28.5t-58 53t-22 78q0 46 25 80t65.5 51.5t82 25t84.5 7.5q20 0 31 -2q2 -1 23 -16.5t26 -19t23 -18t24.5 -22t19 -22.5t17 -26t9 -26.5t4.5 -31.5zM755 863q0 -60 -33 -99.5t-92 -39.5q-53 0 -93 42.5 t-57.5 96.5t-17.5 106q0 61 32 104t92 43q53 0 93.5 -45t58 -101t17.5 -107zM861 1120l88 64h-265q-85 0 -161 -32t-127.5 -98t-51.5 -153q0 -93 64.5 -154.5t158.5 -61.5q22 0 43 3q-13 -29 -13 -54q0 -44 40 -94q-175 -12 -257 -63q-47 -29 -75.5 -73t-28.5 -95 q0 -43 18.5 -77.5t48.5 -56.5t69 -37t77.5 -21t76.5 -6q60 0 120.5 15.5t113.5 46t86 82.5t33 117q0 49 -20 89.5t-49 66.5t-58 47.5t-49 44t-20 44.5t15.5 42.5t37.5 39.5t44 42t37.5 59.5t15.5 82.5q0 60 -22.5 99.5t-72.5 90.5h83zM1152 672h128v64h-128v128h-64v-128 h-128v-64h128v-160h64v160zM1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" />
-<glyph unicode="&#xf0d5;" horiz-adv-x="1664" d="M735 740q0 -36 32 -70.5t77.5 -68t90.5 -73.5t77 -104t32 -142q0 -90 -48 -173q-72 -122 -211 -179.5t-298 -57.5q-132 0 -246.5 41.5t-171.5 137.5q-37 60 -37 131q0 81 44.5 150t118.5 115q131 82 404 100q-32 42 -47.5 74t-15.5 73q0 36 21 85q-46 -4 -68 -4 q-148 0 -249.5 96.5t-101.5 244.5q0 82 36 159t99 131q77 66 182.5 98t217.5 32h418l-138 -88h-131q74 -63 112 -133t38 -160q0 -72 -24.5 -129.5t-59 -93t-69.5 -65t-59.5 -61.5t-24.5 -66zM589 836q38 0 78 16.5t66 43.5q53 57 53 159q0 58 -17 125t-48.5 129.5 t-84.5 103.5t-117 41q-42 0 -82.5 -19.5t-65.5 -52.5q-47 -59 -47 -160q0 -46 10 -97.5t31.5 -103t52 -92.5t75 -67t96.5 -26zM591 -37q58 0 111.5 13t99 39t73 73t27.5 109q0 25 -7 49t-14.5 42t-27 41.5t-29.5 35t-38.5 34.5t-36.5 29t-41.5 30t-36.5 26q-16 2 -48 2 q-53 0 -105 -7t-107.5 -25t-97 -46t-68.5 -74.5t-27 -105.5q0 -70 35 -123.5t91.5 -83t119 -44t127.5 -14.5zM1401 839h213v-108h-213v-219h-105v219h-212v108h212v217h105v-217z" />
+<glyph unicode="&#xf0d4;" d="M917 631q0 26 -6 64h-362v-132h217q-3 -24 -16.5 -50t-37.5 -53t-66.5 -44.5t-96.5 -17.5q-99 0 -169 71t-70 171t70 171t169 71q92 0 153 -59l104 101q-108 100 -257 100q-160 0 -272 -112.5t-112 -271.5t112 -271.5t272 -112.5q165 0 266.5 105t101.5 270zM1262 585 h109v110h-109v110h-110v-110h-110v-110h110v-110h110v110zM1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" />
+<glyph unicode="&#xf0d5;" horiz-adv-x="2304" d="M1437 623q0 -208 -87 -370.5t-248 -254t-369 -91.5q-149 0 -285 58t-234 156t-156 234t-58 285t58 285t156 234t234 156t285 58q286 0 491 -192l-199 -191q-117 113 -292 113q-123 0 -227.5 -62t-165.5 -168.5t-61 -232.5t61 -232.5t165.5 -168.5t227.5 -62 q83 0 152.5 23t114.5 57.5t78.5 78.5t49 83t21.5 74h-416v252h692q12 -63 12 -122zM2304 745v-210h-209v-209h-210v209h-209v210h209v209h210v-209h209z" />
 <glyph unicode="&#xf0d6;" horiz-adv-x="1920" d="M768 384h384v96h-128v448h-114l-148 -137l77 -80q42 37 55 57h2v-288h-128v-96zM1280 640q0 -70 -21 -142t-59.5 -134t-101.5 -101t-138 -39t-138 39t-101.5 101t-59.5 134t-21 142t21 142t59.5 134t101.5 101t138 39t138 -39t101.5 -101t59.5 -134t21 -142zM1792 384 v512q-106 0 -181 75t-75 181h-1152q0 -106 -75 -181t-181 -75v-512q106 0 181 -75t75 -181h1152q0 106 75 181t181 75zM1920 1216v-1152q0 -26 -19 -45t-45 -19h-1792q-26 0 -45 19t-19 45v1152q0 26 19 45t45 19h1792q26 0 45 -19t19 -45z" />
 <glyph unicode="&#xf0d7;" horiz-adv-x="1024" d="M1024 832q0 -26 -19 -45l-448 -448q-19 -19 -45 -19t-45 19l-448 448q-19 19 -19 45t19 45t45 19h896q26 0 45 -19t19 -45z" />
 <glyph unicode="&#xf0d8;" horiz-adv-x="1024" d="M1024 320q0 -26 -19 -45t-45 -19h-896q-26 0 -45 19t-19 45t19 45l448 448q19 19 45 19t45 -19l448 -448q19 -19 19 -45z" />
@@ -275,7 +275,7 @@
 <glyph unicode="&#xf10c;" d="M768 1184q-148 0 -273 -73t-198 -198t-73 -273t73 -273t198 -198t273 -73t273 73t198 198t73 273t-73 273t-198 198t-273 73zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103 t279.5 -279.5t103 -385.5z" />
 <glyph unicode="&#xf10d;" horiz-adv-x="1664" d="M768 576v-384q0 -80 -56 -136t-136 -56h-384q-80 0 -136 56t-56 136v704q0 104 40.5 198.5t109.5 163.5t163.5 109.5t198.5 40.5h64q26 0 45 -19t19 -45v-128q0 -26 -19 -45t-45 -19h-64q-106 0 -181 -75t-75 -181v-32q0 -40 28 -68t68 -28h224q80 0 136 -56t56 -136z M1664 576v-384q0 -80 -56 -136t-136 -56h-384q-80 0 -136 56t-56 136v704q0 104 40.5 198.5t109.5 163.5t163.5 109.5t198.5 40.5h64q26 0 45 -19t19 -45v-128q0 -26 -19 -45t-45 -19h-64q-106 0 -181 -75t-75 -181v-32q0 -40 28 -68t68 -28h224q80 0 136 -56t56 -136z" />
 <glyph unicode="&#xf10e;" horiz-adv-x="1664" d="M768 1216v-704q0 -104 -40.5 -198.5t-109.5 -163.5t-163.5 -109.5t-198.5 -40.5h-64q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h64q106 0 181 75t75 181v32q0 40 -28 68t-68 28h-224q-80 0 -136 56t-56 136v384q0 80 56 136t136 56h384q80 0 136 -56t56 -136zM1664 1216 v-704q0 -104 -40.5 -198.5t-109.5 -163.5t-163.5 -109.5t-198.5 -40.5h-64q-26 0 -45 19t-19 45v128q0 26 19 45t45 19h64q106 0 181 75t75 181v32q0 40 -28 68t-68 28h-224q-80 0 -136 56t-56 136v384q0 80 56 136t136 56h384q80 0 136 -56t56 -136z" />
-<glyph unicode="&#xf110;" horiz-adv-x="1568" d="M496 192q0 -60 -42.5 -102t-101.5 -42q-60 0 -102 42t-42 102t42 102t102 42q59 0 101.5 -42t42.5 -102zM928 0q0 -53 -37.5 -90.5t-90.5 -37.5t-90.5 37.5t-37.5 90.5t37.5 90.5t90.5 37.5t90.5 -37.5t37.5 -90.5zM320 640q0 -66 -47 -113t-113 -47t-113 47t-47 113 t47 113t113 47t113 -47t47 -113zM1360 192q0 -46 -33 -79t-79 -33t-79 33t-33 79t33 79t79 33t79 -33t33 -79zM528 1088q0 -73 -51.5 -124.5t-124.5 -51.5t-124.5 51.5t-51.5 124.5t51.5 124.5t124.5 51.5t124.5 -51.5t51.5 -124.5zM992 1280q0 -80 -56 -136t-136 -56 t-136 56t-56 136t56 136t136 56t136 -56t56 -136zM1536 640q0 -40 -28 -68t-68 -28t-68 28t-28 68t28 68t68 28t68 -28t28 -68zM1328 1088q0 -33 -23.5 -56.5t-56.5 -23.5t-56.5 23.5t-23.5 56.5t23.5 56.5t56.5 23.5t56.5 -23.5t23.5 -56.5z" />
+<glyph unicode="&#xf110;" horiz-adv-x="1792" d="M526 142q0 -53 -37.5 -90.5t-90.5 -37.5q-52 0 -90 38t-38 90q0 53 37.5 90.5t90.5 37.5t90.5 -37.5t37.5 -90.5zM1024 -64q0 -53 -37.5 -90.5t-90.5 -37.5t-90.5 37.5t-37.5 90.5t37.5 90.5t90.5 37.5t90.5 -37.5t37.5 -90.5zM320 640q0 -53 -37.5 -90.5t-90.5 -37.5 t-90.5 37.5t-37.5 90.5t37.5 90.5t90.5 37.5t90.5 -37.5t37.5 -90.5zM1522 142q0 -52 -38 -90t-90 -38q-53 0 -90.5 37.5t-37.5 90.5t37.5 90.5t90.5 37.5t90.5 -37.5t37.5 -90.5zM558 1138q0 -66 -47 -113t-113 -47t-113 47t-47 113t47 113t113 47t113 -47t47 -113z M1728 640q0 -53 -37.5 -90.5t-90.5 -37.5t-90.5 37.5t-37.5 90.5t37.5 90.5t90.5 37.5t90.5 -37.5t37.5 -90.5zM1088 1344q0 -80 -56 -136t-136 -56t-136 56t-56 136t56 136t136 56t136 -56t56 -136zM1618 1138q0 -93 -66 -158.5t-158 -65.5q-93 0 -158.5 65.5t-65.5 158.5 q0 92 65.5 158t158.5 66q92 0 158 -66t66 -158z" />
 <glyph unicode="&#xf111;" d="M1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" />
 <glyph unicode="&#xf112;" horiz-adv-x="1792" d="M1792 416q0 -166 -127 -451q-3 -7 -10.5 -24t-13.5 -30t-13 -22q-12 -17 -28 -17q-15 0 -23.5 10t-8.5 25q0 9 2.5 26.5t2.5 23.5q5 68 5 123q0 101 -17.5 181t-48.5 138.5t-80 101t-105.5 69.5t-133 42.5t-154 21.5t-175.5 6h-224v-256q0 -26 -19 -45t-45 -19t-45 19 l-512 512q-19 19 -19 45t19 45l512 512q19 19 45 19t45 -19t19 -45v-256h224q713 0 875 -403q53 -134 53 -333z" />
 <glyph unicode="&#xf113;" horiz-adv-x="1664" d="M640 320q0 -40 -12.5 -82t-43 -76t-72.5 -34t-72.5 34t-43 76t-12.5 82t12.5 82t43 76t72.5 34t72.5 -34t43 -76t12.5 -82zM1280 320q0 -40 -12.5 -82t-43 -76t-72.5 -34t-72.5 34t-43 76t-12.5 82t12.5 82t43 76t72.5 34t72.5 -34t43 -76t12.5 -82zM1440 320 q0 120 -69 204t-187 84q-41 0 -195 -21q-71 -11 -157 -11t-157 11q-152 21 -195 21q-118 0 -187 -84t-69 -204q0 -88 32 -153.5t81 -103t122 -60t140 -29.5t149 -7h168q82 0 149 7t140 29.5t122 60t81 103t32 153.5zM1664 496q0 -207 -61 -331q-38 -77 -105.5 -133t-141 -86 t-170 -47.5t-171.5 -22t-167 -4.5q-78 0 -142 3t-147.5 12.5t-152.5 30t-137 51.5t-121 81t-86 115q-62 123 -62 331q0 237 136 396q-27 82 -27 170q0 116 51 218q108 0 190 -39.5t189 -123.5q147 35 309 35q148 0 280 -32q105 82 187 121t189 39q51 -102 51 -218 q0 -87 -27 -168q136 -160 136 -398z" />
@@ -362,8 +362,8 @@
 <glyph unicode="&#xf169;" d="M685 771q0 1 -126 222q-21 34 -52 34h-184q-18 0 -26 -11q-7 -12 1 -29l125 -216v-1l-196 -346q-9 -14 0 -28q8 -13 24 -13h185q31 0 50 36zM1309 1268q-7 12 -24 12h-187q-30 0 -49 -35l-411 -729q1 -2 262 -481q20 -35 52 -35h184q18 0 25 12q8 13 -1 28l-260 476v1 l409 723q8 16 0 28zM1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" />
 <glyph unicode="&#xf16a;" horiz-adv-x="1792" d="M1280 640q0 37 -30 54l-512 320q-31 20 -65 2q-33 -18 -33 -56v-640q0 -38 33 -56q16 -8 31 -8q20 0 34 10l512 320q30 17 30 54zM1792 640q0 -96 -1 -150t-8.5 -136.5t-22.5 -147.5q-16 -73 -69 -123t-124 -58q-222 -25 -671 -25t-671 25q-71 8 -124.5 58t-69.5 123 q-14 65 -21.5 147.5t-8.5 136.5t-1 150t1 150t8.5 136.5t22.5 147.5q16 73 69 123t124 58q222 25 671 25t671 -25q71 -8 124.5 -58t69.5 -123q14 -65 21.5 -147.5t8.5 -136.5t1 -150z" />
 <glyph unicode="&#xf16b;" horiz-adv-x="1792" d="M402 829l494 -305l-342 -285l-490 319zM1388 274v-108l-490 -293v-1l-1 1l-1 -1v1l-489 293v108l147 -96l342 284v2l1 -1l1 1v-2l343 -284zM554 1418l342 -285l-494 -304l-338 270zM1390 829l338 -271l-489 -319l-343 285zM1239 1418l489 -319l-338 -270l-494 304z" />
-<glyph unicode="&#xf16c;" horiz-adv-x="1408" d="M928 135v-151l-707 -1v151zM1169 481v-701l-1 -35v-1h-1132l-35 1h-1v736h121v-618h928v618h120zM241 393l704 -65l-13 -150l-705 65zM309 709l683 -183l-39 -146l-683 183zM472 1058l609 -360l-77 -130l-609 360zM832 1389l398 -585l-124 -85l-399 584zM1285 1536 l121 -697l-149 -26l-121 697z" />
-<glyph unicode="&#xf16d;" d="M1362 110v648h-135q20 -63 20 -131q0 -126 -64 -232.5t-174 -168.5t-240 -62q-197 0 -337 135.5t-140 327.5q0 68 20 131h-141v-648q0 -26 17.5 -43.5t43.5 -17.5h1069q25 0 43 17.5t18 43.5zM1078 643q0 124 -90.5 211.5t-218.5 87.5q-127 0 -217.5 -87.5t-90.5 -211.5 t90.5 -211.5t217.5 -87.5q128 0 218.5 87.5t90.5 211.5zM1362 1003v165q0 28 -20 48.5t-49 20.5h-174q-29 0 -49 -20.5t-20 -48.5v-165q0 -29 20 -49t49 -20h174q29 0 49 20t20 49zM1536 1211v-1142q0 -81 -58 -139t-139 -58h-1142q-81 0 -139 58t-58 139v1142q0 81 58 139 t139 58h1142q81 0 139 -58t58 -139z" />
+<glyph unicode="&#xf16c;" d="M1289 -96h-1118v480h-160v-640h1438v640h-160v-480zM347 428l33 157l783 -165l-33 -156zM450 802l67 146l725 -339l-67 -145zM651 1158l102 123l614 -513l-102 -123zM1048 1536l477 -641l-128 -96l-477 641zM330 65v159h800v-159h-800z" />
+<glyph unicode="&#xf16d;" d="M1024 640q0 106 -75 181t-181 75t-181 -75t-75 -181t75 -181t181 -75t181 75t75 181zM1162 640q0 -164 -115 -279t-279 -115t-279 115t-115 279t115 279t279 115t279 -115t115 -279zM1270 1050q0 -38 -27 -65t-65 -27t-65 27t-27 65t27 65t65 27t65 -27t27 -65zM768 1270 q-7 0 -76.5 0.5t-105.5 0t-96.5 -3t-103 -10t-71.5 -18.5q-50 -20 -88 -58t-58 -88q-11 -29 -18.5 -71.5t-10 -103t-3 -96.5t0 -105.5t0.5 -76.5t-0.5 -76.5t0 -105.5t3 -96.5t10 -103t18.5 -71.5q20 -50 58 -88t88 -58q29 -11 71.5 -18.5t103 -10t96.5 -3t105.5 0t76.5 0.5 t76.5 -0.5t105.5 0t96.5 3t103 10t71.5 18.5q50 20 88 58t58 88q11 29 18.5 71.5t10 103t3 96.5t0 105.5t-0.5 76.5t0.5 76.5t0 105.5t-3 96.5t-10 103t-18.5 71.5q-20 50 -58 88t-88 58q-29 11 -71.5 18.5t-103 10t-96.5 3t-105.5 0t-76.5 -0.5zM1536 640q0 -229 -5 -317 q-10 -208 -124 -322t-322 -124q-88 -5 -317 -5t-317 5q-208 10 -322 124t-124 322q-5 88 -5 317t5 317q10 208 124 322t322 124q88 5 317 5t317 -5q208 -10 322 -124t124 -322q5 -88 5 -317z" />
 <glyph unicode="&#xf16e;" d="M1248 1408q119 0 203.5 -84.5t84.5 -203.5v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960zM698 640q0 88 -62 150t-150 62t-150 -62t-62 -150t62 -150t150 -62t150 62t62 150zM1262 640q0 88 -62 150 t-150 62t-150 -62t-62 -150t62 -150t150 -62t150 62t62 150z" />
 <glyph unicode="&#xf170;" d="M768 914l201 -306h-402zM1133 384h94l-459 691l-459 -691h94l104 160h522zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" />
 <glyph unicode="&#xf171;" horiz-adv-x="1408" d="M815 677q8 -63 -50.5 -101t-111.5 -6q-39 17 -53.5 58t-0.5 82t52 58q36 18 72.5 12t64 -35.5t27.5 -67.5zM926 698q-14 107 -113 164t-197 13q-63 -28 -100.5 -88.5t-34.5 -129.5q4 -91 77.5 -155t165.5 -56q91 8 152 84t50 168zM1165 1240q-20 27 -56 44.5t-58 22 t-71 12.5q-291 47 -566 -2q-43 -7 -66 -12t-55 -22t-50 -43q30 -28 76 -45.5t73.5 -22t87.5 -11.5q228 -29 448 -1q63 8 89.5 12t72.5 21.5t75 46.5zM1222 205q-8 -26 -15.5 -76.5t-14 -84t-28.5 -70t-58 -56.5q-86 -48 -189.5 -71.5t-202 -22t-201.5 18.5q-46 8 -81.5 18 t-76.5 27t-73 43.5t-52 61.5q-25 96 -57 292l6 16l18 9q223 -148 506.5 -148t507.5 148q21 -6 24 -23t-5 -45t-8 -37zM1403 1166q-26 -167 -111 -655q-5 -30 -27 -56t-43.5 -40t-54.5 -31q-252 -126 -610 -88q-248 27 -394 139q-15 12 -25.5 26.5t-17 35t-9 34t-6 39.5 t-5.5 35q-9 50 -26.5 150t-28 161.5t-23.5 147.5t-22 158q3 26 17.5 48.5t31.5 37.5t45 30t46 22.5t48 18.5q125 46 313 64q379 37 676 -50q155 -46 215 -122q16 -20 16.5 -51t-5.5 -54z" />
@@ -399,7 +399,7 @@
 <glyph unicode="&#xf191;" d="M1024 960v-640q0 -26 -19 -45t-45 -19q-20 0 -37 12l-448 320q-27 19 -27 52t27 52l448 320q17 12 37 12q26 0 45 -19t19 -45zM1280 160v960q0 13 -9.5 22.5t-22.5 9.5h-960q-13 0 -22.5 -9.5t-9.5 -22.5v-960q0 -13 9.5 -22.5t22.5 -9.5h960q13 0 22.5 9.5t9.5 22.5z M1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" />
 <glyph unicode="&#xf192;" d="M1024 640q0 -106 -75 -181t-181 -75t-181 75t-75 181t75 181t181 75t181 -75t75 -181zM768 1184q-148 0 -273 -73t-198 -198t-73 -273t73 -273t198 -198t273 -73t273 73t198 198t73 273t-73 273t-198 198t-273 73zM1536 640q0 -209 -103 -385.5t-279.5 -279.5 t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" />
 <glyph unicode="&#xf193;" horiz-adv-x="1664" d="M1023 349l102 -204q-58 -179 -210 -290t-339 -111q-156 0 -288.5 77.5t-210 210t-77.5 288.5q0 181 104.5 330t274.5 211l17 -131q-122 -54 -195 -165.5t-73 -244.5q0 -185 131.5 -316.5t316.5 -131.5q126 0 232.5 65t165 175.5t49.5 236.5zM1571 249l58 -114l-256 -128 q-13 -7 -29 -7q-40 0 -57 35l-239 477h-472q-24 0 -42.5 16.5t-21.5 40.5l-96 779q-2 16 6 42q14 51 57 82.5t97 31.5q66 0 113 -47t47 -113q0 -69 -52 -117.5t-120 -41.5l37 -289h423v-128h-407l16 -128h455q40 0 57 -35l228 -455z" />
-<glyph unicode="&#xf194;" d="M1254 899q16 85 -21 132q-52 65 -187 45q-17 -3 -41 -12.5t-57.5 -30.5t-64.5 -48.5t-59.5 -70t-44.5 -91.5q80 7 113.5 -16t26.5 -99q-5 -52 -52 -143q-43 -78 -71 -99q-44 -32 -87 14q-23 24 -37.5 64.5t-19 73t-10 84t-8.5 71.5q-23 129 -34 164q-12 37 -35.5 69 t-50.5 40q-57 16 -127 -25q-54 -32 -136.5 -106t-122.5 -102v-7q16 -8 25.5 -26t21.5 -20q21 -3 54.5 8.5t58 10.5t41.5 -30q11 -18 18.5 -38.5t15 -48t12.5 -40.5q17 -46 53 -187q36 -146 57 -197q42 -99 103 -125q43 -12 85 -1.5t76 31.5q131 77 250 237 q104 139 172.5 292.5t82.5 226.5zM1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" />
+<glyph unicode="&#xf194;" d="M1292 898q10 216 -161 222q-231 8 -312 -261q44 19 82 19q85 0 74 -96q-4 -57 -74 -167t-105 -110q-43 0 -82 169q-13 54 -45 255q-30 189 -160 177q-59 -7 -164 -100l-81 -72l-81 -72l52 -67q76 52 87 52q57 0 107 -179q15 -55 45 -164.5t45 -164.5q68 -179 164 -179 q157 0 383 294q220 283 226 444zM1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" />
 <glyph unicode="&#xf195;" horiz-adv-x="1152" d="M1152 704q0 -191 -94.5 -353t-256.5 -256.5t-353 -94.5h-160q-14 0 -23 9t-9 23v611l-215 -66q-3 -1 -9 -1q-10 0 -19 6q-13 10 -13 26v128q0 23 23 31l233 71v93l-215 -66q-3 -1 -9 -1q-10 0 -19 6q-13 10 -13 26v128q0 23 23 31l233 71v250q0 14 9 23t23 9h160 q14 0 23 -9t9 -23v-181l375 116q15 5 28 -5t13 -26v-128q0 -23 -23 -31l-393 -121v-93l375 116q15 5 28 -5t13 -26v-128q0 -23 -23 -31l-393 -121v-487q188 13 318 151t130 328q0 14 9 23t23 9h160q14 0 23 -9t9 -23z" />
 <glyph unicode="&#xf196;" horiz-adv-x="1408" d="M1152 736v-64q0 -14 -9 -23t-23 -9h-352v-352q0 -14 -9 -23t-23 -9h-64q-14 0 -23 9t-9 23v352h-352q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h352v352q0 14 9 23t23 9h64q14 0 23 -9t9 -23v-352h352q14 0 23 -9t9 -23zM1280 288v832q0 66 -47 113t-113 47h-832 q-66 0 -113 -47t-47 -113v-832q0 -66 47 -113t113 -47h832q66 0 113 47t47 113zM1408 1120v-832q0 -119 -84.5 -203.5t-203.5 -84.5h-832q-119 0 -203.5 84.5t-84.5 203.5v832q0 119 84.5 203.5t203.5 84.5h832q119 0 203.5 -84.5t84.5 -203.5z" />
 <glyph unicode="&#xf197;" horiz-adv-x="2176" d="M620 416q-110 -64 -268 -64h-128v64h-64q-13 0 -22.5 23.5t-9.5 56.5q0 24 7 49q-58 2 -96.5 10.5t-38.5 20.5t38.5 20.5t96.5 10.5q-7 25 -7 49q0 33 9.5 56.5t22.5 23.5h64v64h128q158 0 268 -64h1113q42 -7 106.5 -18t80.5 -14q89 -15 150 -40.5t83.5 -47.5t22.5 -40 t-22.5 -40t-83.5 -47.5t-150 -40.5q-16 -3 -80.5 -14t-106.5 -18h-1113zM1739 668q53 -36 53 -92t-53 -92l81 -30q68 48 68 122t-68 122zM625 400h1015q-217 -38 -456 -80q-57 0 -113 -24t-83 -48l-28 -24l-288 -288q-26 -26 -70.5 -45t-89.5 -19h-96l-93 464h29 q157 0 273 64zM352 816h-29l93 464h96q46 0 90 -19t70 -45l288 -288q4 -4 11 -10.5t30.5 -23t48.5 -29t61.5 -23t72.5 -10.5l456 -80h-1015q-116 64 -273 64z" />
@@ -410,9 +410,9 @@
 <glyph unicode="&#xf19c;" horiz-adv-x="2048" d="M960 1536l960 -384v-128h-128q0 -26 -20.5 -45t-48.5 -19h-1526q-28 0 -48.5 19t-20.5 45h-128v128zM256 896h256v-768h128v768h256v-768h128v768h256v-768h128v768h256v-768h59q28 0 48.5 -19t20.5 -45v-64h-1664v64q0 26 20.5 45t48.5 19h59v768zM1851 -64 q28 0 48.5 -19t20.5 -45v-128h-1920v128q0 26 20.5 45t48.5 19h1782z" />
 <glyph unicode="&#xf19d;" horiz-adv-x="2304" d="M1774 700l18 -316q4 -69 -82 -128t-235 -93.5t-323 -34.5t-323 34.5t-235 93.5t-82 128l18 316l574 -181q22 -7 48 -7t48 7zM2304 1024q0 -23 -22 -31l-1120 -352q-4 -1 -10 -1t-10 1l-652 206q-43 -34 -71 -111.5t-34 -178.5q63 -36 63 -109q0 -69 -58 -107l58 -433 q2 -14 -8 -25q-9 -11 -24 -11h-192q-15 0 -24 11q-10 11 -8 25l58 433q-58 38 -58 107q0 73 65 111q11 207 98 330l-333 104q-22 8 -22 31t22 31l1120 352q4 1 10 1t10 -1l1120 -352q22 -8 22 -31z" />
 <glyph unicode="&#xf19e;" d="M859 579l13 -707q-62 11 -105 11q-41 0 -105 -11l13 707q-40 69 -168.5 295.5t-216.5 374.5t-181 287q58 -15 108 -15q43 0 111 15q63 -111 133.5 -229.5t167 -276.5t138.5 -227q37 61 109.5 177.5t117.5 190t105 176t107 189.5q54 -14 107 -14q56 0 114 14v0 q-28 -39 -60 -88.5t-49.5 -78.5t-56.5 -96t-49 -84q-146 -248 -353 -610z" />
-<glyph unicode="&#xf1a0;" horiz-adv-x="1280" d="M981 197q0 25 -7 49t-14.5 42t-27 41.5t-29.5 35t-38.5 34.5t-36.5 29t-41.5 30t-36.5 26q-16 2 -49 2q-53 0 -104.5 -7t-107 -25t-97 -46t-68.5 -74.5t-27 -105.5q0 -56 23.5 -102t61 -75.5t87 -50t100 -29t101.5 -8.5q58 0 111.5 13t99 39t73 73t27.5 109zM864 1055 q0 59 -17 125.5t-48 129t-84 103.5t-117 41q-42 0 -82.5 -19.5t-66.5 -52.5q-46 -59 -46 -160q0 -46 10 -97.5t31.5 -103t52 -92.5t75 -67t96.5 -26q37 0 77.5 16.5t65.5 43.5q53 56 53 159zM752 1536h417l-137 -88h-132q75 -63 113 -133t38 -160q0 -72 -24.5 -129.5 t-59.5 -93t-69.5 -65t-59 -61.5t-24.5 -66q0 -36 32 -70.5t77 -68t90.5 -73.5t77.5 -104t32 -142q0 -91 -49 -173q-71 -122 -209.5 -179.5t-298.5 -57.5q-132 0 -246.5 41.5t-172.5 137.5q-36 59 -36 131q0 81 44.5 150t118.5 115q131 82 404 100q-32 41 -47.5 73.5 t-15.5 73.5q0 40 21 85q-46 -4 -68 -4q-148 0 -249.5 96.5t-101.5 244.5q0 82 36 159t99 131q76 66 182 98t218 32z" />
-<glyph unicode="&#xf1a1;" horiz-adv-x="1984" d="M831 572q0 -56 -40.5 -96t-96.5 -40q-57 0 -98 40t-41 96q0 57 41.5 98t97.5 41t96.5 -41t40.5 -98zM1292 711q56 0 96.5 -41t40.5 -98q0 -56 -40.5 -96t-96.5 -40q-57 0 -98 40t-41 96q0 57 41.5 98t97.5 41zM1984 722q0 -62 -31 -114t-83 -82q5 -33 5 -61 q0 -121 -68.5 -230.5t-197.5 -193.5q-125 -82 -285.5 -125.5t-335.5 -43.5q-176 0 -336.5 43.5t-284.5 125.5q-129 84 -197.5 193t-68.5 231q0 29 5 66q-48 31 -77 81.5t-29 109.5q0 94 66 160t160 66q83 0 148 -55q248 158 592 164l134 423q4 14 17.5 21.5t28.5 4.5 l347 -82q22 50 68.5 81t102.5 31q77 0 131.5 -54.5t54.5 -131.5t-54.5 -132t-131.5 -55q-76 0 -130.5 54t-55.5 131l-315 74l-116 -366q327 -14 560 -166q64 58 151 58q94 0 160 -66t66 -160zM1664 1459q-45 0 -77 -32t-32 -77t32 -77t77 -32t77 32t32 77t-32 77t-77 32z M77 722q0 -67 51 -111q49 131 180 235q-36 25 -82 25q-62 0 -105.5 -43.5t-43.5 -105.5zM1567 105q112 73 171.5 166t59.5 194t-59.5 193.5t-171.5 165.5q-116 75 -265.5 115.5t-313.5 40.5t-313.5 -40.5t-265.5 -115.5q-112 -73 -171.5 -165.5t-59.5 -193.5t59.5 -194 t171.5 -166q116 -75 265.5 -115.5t313.5 -40.5t313.5 40.5t265.5 115.5zM1850 605q57 46 57 117q0 62 -43.5 105.5t-105.5 43.5q-49 0 -86 -28q131 -105 178 -238zM1258 237q11 11 27 11t27 -11t11 -27.5t-11 -27.5q-99 -99 -319 -99h-2q-220 0 -319 99q-11 11 -11 27.5 t11 27.5t27 11t27 -11q77 -77 265 -77h2q188 0 265 77z" />
-<glyph unicode="&#xf1a2;" d="M950 393q7 7 17.5 7t17.5 -7t7 -18t-7 -18q-65 -64 -208 -64h-1h-1q-143 0 -207 64q-8 7 -8 18t8 18q7 7 17.5 7t17.5 -7q49 -51 172 -51h1h1q122 0 173 51zM671 613q0 -37 -26 -64t-63 -27t-63 27t-26 64t26 63t63 26t63 -26t26 -63zM1214 1049q-29 0 -50 21t-21 50 q0 30 21 51t50 21q30 0 51 -21t21 -51q0 -29 -21 -50t-51 -21zM1216 1408q132 0 226 -94t94 -227v-894q0 -133 -94 -227t-226 -94h-896q-132 0 -226 94t-94 227v894q0 133 94 227t226 94h896zM1321 596q35 14 57 45.5t22 70.5q0 51 -36 87.5t-87 36.5q-60 0 -98 -48 q-151 107 -375 115l83 265l206 -49q1 -50 36.5 -85t84.5 -35q50 0 86 35.5t36 85.5t-36 86t-86 36q-36 0 -66 -20.5t-45 -53.5l-227 54q-9 2 -17.5 -2.5t-11.5 -14.5l-95 -302q-224 -4 -381 -113q-36 43 -93 43q-51 0 -87 -36.5t-36 -87.5q0 -37 19.5 -67.5t52.5 -45.5 q-7 -25 -7 -54q0 -98 74 -181.5t201.5 -132t278.5 -48.5q150 0 277.5 48.5t201.5 132t74 181.5q0 27 -6 54zM971 702q37 0 63 -26t26 -63t-26 -64t-63 -27t-63 27t-26 64t26 63t63 26z" />
+<glyph unicode="&#xf1a0;" d="M768 750h725q12 -67 12 -128q0 -217 -91 -387.5t-259.5 -266.5t-386.5 -96q-157 0 -299 60.5t-245 163.5t-163.5 245t-60.5 299t60.5 299t163.5 245t245 163.5t299 60.5q300 0 515 -201l-209 -201q-123 119 -306 119q-129 0 -238.5 -65t-173.5 -176.5t-64 -243.5 t64 -243.5t173.5 -176.5t238.5 -65q87 0 160 24t120 60t82 82t51.5 87t22.5 78h-436v264z" />
+<glyph unicode="&#xf1a1;" horiz-adv-x="1792" d="M1095 369q16 -16 0 -31q-62 -62 -199 -62t-199 62q-16 15 0 31q6 6 15 6t15 -6q48 -49 169 -49q120 0 169 49q6 6 15 6t15 -6zM788 550q0 -37 -26 -63t-63 -26t-63.5 26t-26.5 63q0 38 26.5 64t63.5 26t63 -26.5t26 -63.5zM1183 550q0 -37 -26.5 -63t-63.5 -26t-63 26 t-26 63t26 63.5t63 26.5t63.5 -26t26.5 -64zM1434 670q0 49 -35 84t-85 35t-86 -36q-130 90 -311 96l63 283l200 -45q0 -37 26 -63t63 -26t63.5 26.5t26.5 63.5t-26.5 63.5t-63.5 26.5q-54 0 -80 -50l-221 49q-19 5 -25 -16l-69 -312q-180 -7 -309 -97q-35 37 -87 37 q-50 0 -85 -35t-35 -84q0 -35 18.5 -64t49.5 -44q-6 -27 -6 -56q0 -142 140 -243t337 -101q198 0 338 101t140 243q0 32 -7 57q30 15 48 43.5t18 63.5zM1792 640q0 -182 -71 -348t-191 -286t-286 -191t-348 -71t-348 71t-286 191t-191 286t-71 348t71 348t191 286t286 191 t348 71t348 -71t286 -191t191 -286t71 -348z" />
+<glyph unicode="&#xf1a2;" d="M939 407q13 -13 0 -26q-53 -53 -171 -53t-171 53q-13 13 0 26q5 6 13 6t13 -6q42 -42 145 -42t145 42q5 6 13 6t13 -6zM676 563q0 -31 -23 -54t-54 -23t-54 23t-23 54q0 32 22.5 54.5t54.5 22.5t54.5 -22.5t22.5 -54.5zM1014 563q0 -31 -23 -54t-54 -23t-54 23t-23 54 q0 32 22.5 54.5t54.5 22.5t54.5 -22.5t22.5 -54.5zM1229 666q0 42 -30 72t-73 30q-42 0 -73 -31q-113 78 -267 82l54 243l171 -39q1 -32 23.5 -54t53.5 -22q32 0 54.5 22.5t22.5 54.5t-22.5 54.5t-54.5 22.5q-48 0 -69 -43l-189 42q-17 5 -21 -13l-60 -268q-154 -6 -265 -83 q-30 32 -74 32q-43 0 -73 -30t-30 -72q0 -30 16 -55t42 -38q-5 -25 -5 -48q0 -122 120 -208.5t289 -86.5q170 0 290 86.5t120 208.5q0 25 -6 49q25 13 40.5 37.5t15.5 54.5zM1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960 q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" />
 <glyph unicode="&#xf1a3;" d="M866 697l90 27v62q0 79 -58 135t-138 56t-138 -55.5t-58 -134.5v-283q0 -20 -14 -33.5t-33 -13.5t-32.5 13.5t-13.5 33.5v120h-151v-122q0 -82 57.5 -139t139.5 -57q81 0 138.5 56.5t57.5 136.5v280q0 19 13.5 33t33.5 14q19 0 32.5 -14t13.5 -33v-54zM1199 502v122h-150 v-126q0 -20 -13.5 -33.5t-33.5 -13.5q-19 0 -32.5 14t-13.5 33v123l-90 -26l-60 28v-123q0 -80 58 -137t139 -57t138.5 57t57.5 139zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103 t385.5 -103t279.5 -279.5t103 -385.5z" />
 <glyph unicode="&#xf1a4;" horiz-adv-x="1920" d="M1062 824v118q0 42 -30 72t-72 30t-72 -30t-30 -72v-612q0 -175 -126 -299t-303 -124q-178 0 -303.5 125.5t-125.5 303.5v266h328v-262q0 -43 30 -72.5t72 -29.5t72 29.5t30 72.5v620q0 171 126.5 292t301.5 121q176 0 302 -122t126 -294v-136l-195 -58zM1592 602h328 v-266q0 -178 -125.5 -303.5t-303.5 -125.5q-177 0 -303 124.5t-126 300.5v268l131 -61l195 58v-270q0 -42 30 -71.5t72 -29.5t72 29.5t30 71.5v275z" />
 <glyph unicode="&#xf1a5;" d="M1472 160v480h-704v704h-480q-93 0 -158.5 -65.5t-65.5 -158.5v-480h704v-704h480q93 0 158.5 65.5t65.5 158.5zM1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5 t84.5 -203.5z" />
@@ -438,7 +438,7 @@
 <glyph unicode="&#xf1ba;" horiz-adv-x="2048" d="M1824 640q93 0 158.5 -65.5t65.5 -158.5v-384q0 -14 -9 -23t-23 -9h-96v-64q0 -80 -56 -136t-136 -56t-136 56t-56 136v64h-1024v-64q0 -80 -56 -136t-136 -56t-136 56t-56 136v64h-96q-14 0 -23 9t-9 23v384q0 93 65.5 158.5t158.5 65.5h28l105 419q23 94 104 157.5 t179 63.5h128v224q0 14 9 23t23 9h448q14 0 23 -9t9 -23v-224h128q98 0 179 -63.5t104 -157.5l105 -419h28zM320 160q66 0 113 47t47 113t-47 113t-113 47t-113 -47t-47 -113t47 -113t113 -47zM516 640h1016l-89 357q-2 8 -14 17.5t-21 9.5h-768q-9 0 -21 -9.5t-14 -17.5z M1728 160q66 0 113 47t47 113t-47 113t-113 47t-113 -47t-47 -113t47 -113t113 -47z" />
 <glyph unicode="&#xf1bb;" d="M1504 64q0 -26 -19 -45t-45 -19h-462q1 -17 6 -87.5t5 -108.5q0 -25 -18 -42.5t-43 -17.5h-320q-25 0 -43 17.5t-18 42.5q0 38 5 108.5t6 87.5h-462q-26 0 -45 19t-19 45t19 45l402 403h-229q-26 0 -45 19t-19 45t19 45l402 403h-197q-26 0 -45 19t-19 45t19 45l384 384 q19 19 45 19t45 -19l384 -384q19 -19 19 -45t-19 -45t-45 -19h-197l402 -403q19 -19 19 -45t-19 -45t-45 -19h-229l402 -403q19 -19 19 -45z" />
 <glyph unicode="&#xf1bc;" d="M1127 326q0 32 -30 51q-193 115 -447 115q-133 0 -287 -34q-42 -9 -42 -52q0 -20 13.5 -34.5t35.5 -14.5q5 0 37 8q132 27 243 27q226 0 397 -103q19 -11 33 -11q19 0 33 13.5t14 34.5zM1223 541q0 40 -35 61q-237 141 -548 141q-153 0 -303 -42q-48 -13 -48 -64 q0 -25 17.5 -42.5t42.5 -17.5q7 0 37 8q122 33 251 33q279 0 488 -124q24 -13 38 -13q25 0 42.5 17.5t17.5 42.5zM1331 789q0 47 -40 70q-126 73 -293 110.5t-343 37.5q-204 0 -364 -47q-23 -7 -38.5 -25.5t-15.5 -48.5q0 -31 20.5 -52t51.5 -21q11 0 40 8q133 37 307 37 q159 0 309.5 -34t253.5 -95q21 -12 40 -12q29 0 50.5 20.5t21.5 51.5zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" />
-<glyph unicode="&#xf1bd;" d="M1397 1408q58 0 98.5 -40.5t40.5 -98.5v-1258q0 -58 -40.5 -98.5t-98.5 -40.5h-1258q-58 0 -98.5 40.5t-40.5 98.5v1258q0 58 40.5 98.5t98.5 40.5h1258zM1465 11v1258q0 28 -20 48t-48 20h-1258q-28 0 -48 -20t-20 -48v-1258q0 -28 20 -48t48 -20h1258q28 0 48 20t20 48 zM694 749l188 -387l533 145v-496q0 -7 -5.5 -12.5t-12.5 -5.5h-1258q-7 0 -12.5 5.5t-5.5 12.5v141l711 195l-212 439q4 1 12 2.5t12 1.5q170 32 303.5 21.5t221 -46t143.5 -94.5q27 -28 -25 -42q-64 -16 -256 -62l-97 198q-111 7 -240 -16zM1397 1287q7 0 12.5 -5.5 t5.5 -12.5v-428q-85 30 -188 52q-294 64 -645 12l-18 -3l-65 134h-233l85 -190q-132 -51 -230 -137v560q0 7 5.5 12.5t12.5 5.5h1258zM286 387q-14 -3 -26 4.5t-14 21.5q-24 203 166 305l129 -270z" />
+<glyph unicode="&#xf1bd;" horiz-adv-x="1024" d="M1024 1233l-303 -582l24 -31h279v-415h-507l-44 -30l-142 -273l-30 -30h-301v303l303 583l-24 30h-279v415h507l44 30l142 273l30 30h301v-303z" />
 <glyph unicode="&#xf1be;" horiz-adv-x="2304" d="M784 164l16 241l-16 523q-1 10 -7.5 17t-16.5 7q-9 0 -16 -7t-7 -17l-14 -523l14 -241q1 -10 7.5 -16.5t15.5 -6.5q22 0 24 23zM1080 193l11 211l-12 586q0 16 -13 24q-8 5 -16 5t-16 -5q-13 -8 -13 -24l-1 -6l-10 -579q0 -1 11 -236v-1q0 -10 6 -17q9 -11 23 -11 q11 0 20 9q9 7 9 20zM35 533l20 -128l-20 -126q-2 -9 -9 -9t-9 9l-17 126l17 128q2 9 9 9t9 -9zM121 612l26 -207l-26 -203q-2 -9 -10 -9q-9 0 -9 10l-23 202l23 207q0 9 9 9q8 0 10 -9zM401 159zM213 650l25 -245l-25 -237q0 -11 -11 -11q-10 0 -12 11l-21 237l21 245 q2 12 12 12q11 0 11 -12zM307 657l23 -252l-23 -244q-2 -13 -14 -13q-13 0 -13 13l-21 244l21 252q0 13 13 13q12 0 14 -13zM401 639l21 -234l-21 -246q-2 -16 -16 -16q-6 0 -10.5 4.5t-4.5 11.5l-20 246l20 234q0 6 4.5 10.5t10.5 4.5q14 0 16 -15zM784 164zM495 785 l21 -380l-21 -246q0 -7 -5 -12.5t-12 -5.5q-16 0 -18 18l-18 246l18 380q2 18 18 18q7 0 12 -5.5t5 -12.5zM589 871l19 -468l-19 -244q0 -8 -5.5 -13.5t-13.5 -5.5q-18 0 -20 19l-16 244l16 468q2 19 20 19q8 0 13.5 -5.5t5.5 -13.5zM687 911l18 -506l-18 -242 q-2 -21 -22 -21q-19 0 -21 21l-16 242l16 506q0 9 6.5 15.5t14.5 6.5q9 0 15 -6.5t7 -15.5zM1079 169v0v0zM881 915l15 -510l-15 -239q0 -10 -7.5 -17.5t-17.5 -7.5t-17 7t-8 18l-14 239l14 510q0 11 7.5 18t17.5 7t17.5 -7t7.5 -18zM980 896l14 -492l-14 -236q0 -11 -8 -19 t-19 -8t-19 8t-9 19l-12 236l12 492q1 12 9 20t19 8t18.5 -8t8.5 -20zM1192 404l-14 -231v0q0 -13 -9 -22t-22 -9t-22 9t-10 22l-6 114l-6 117l12 636v3q2 15 12 24q9 7 20 7q8 0 15 -5q14 -8 16 -26zM2304 423q0 -117 -83 -199.5t-200 -82.5h-786q-13 2 -22 11t-9 22v899 q0 23 28 33q85 34 181 34q195 0 338 -131.5t160 -323.5q53 22 110 22q117 0 200 -83t83 -201z" />
 <glyph unicode="&#xf1c0;" d="M768 768q237 0 443 43t325 127v-170q0 -69 -103 -128t-280 -93.5t-385 -34.5t-385 34.5t-280 93.5t-103 128v170q119 -84 325 -127t443 -43zM768 0q237 0 443 43t325 127v-170q0 -69 -103 -128t-280 -93.5t-385 -34.5t-385 34.5t-280 93.5t-103 128v170q119 -84 325 -127 t443 -43zM768 384q237 0 443 43t325 127v-170q0 -69 -103 -128t-280 -93.5t-385 -34.5t-385 34.5t-280 93.5t-103 128v170q119 -84 325 -127t443 -43zM768 1536q208 0 385 -34.5t280 -93.5t103 -128v-128q0 -69 -103 -128t-280 -93.5t-385 -34.5t-385 34.5t-280 93.5 t-103 128v128q0 69 103 128t280 93.5t385 34.5z" />
 <glyph unicode="&#xf1c1;" d="M1468 1156q28 -28 48 -76t20 -88v-1152q0 -40 -28 -68t-68 -28h-1344q-40 0 -68 28t-28 68v1600q0 40 28 68t68 28h896q40 0 88 -20t76 -48zM1024 1400v-376h376q-10 29 -22 41l-313 313q-12 12 -41 22zM1408 -128v1024h-416q-40 0 -68 28t-28 68v416h-768v-1536h1280z M894 465q33 -26 84 -56q59 7 117 7q147 0 177 -49q16 -22 2 -52q0 -1 -1 -2l-2 -2v-1q-6 -38 -71 -38q-48 0 -115 20t-130 53q-221 -24 -392 -83q-153 -262 -242 -262q-15 0 -28 7l-24 12q-1 1 -6 5q-10 10 -6 36q9 40 56 91.5t132 96.5q14 9 23 -6q2 -2 2 -4q52 85 107 197 q68 136 104 262q-24 82 -30.5 159.5t6.5 127.5q11 40 42 40h21h1q23 0 35 -15q18 -21 9 -68q-2 -6 -4 -8q1 -3 1 -8v-30q-2 -123 -14 -192q55 -164 146 -238zM318 54q52 24 137 158q-51 -40 -87.5 -84t-49.5 -74zM716 974q-15 -42 -2 -132q1 7 7 44q0 3 7 43q1 4 4 8 q-1 1 -1 2t-0.5 1.5t-0.5 1.5q-1 22 -13 36q0 -1 -1 -2v-2zM592 313q135 54 284 81q-2 1 -13 9.5t-16 13.5q-76 67 -127 176q-27 -86 -83 -197q-30 -56 -45 -83zM1238 329q-24 24 -140 24q76 -28 124 -28q14 0 18 1q0 1 -2 3z" />
@@ -454,12 +454,12 @@
 <glyph unicode="&#xf1cb;" horiz-adv-x="1792" d="M216 367l603 -402v359l-334 223zM154 511l193 129l-193 129v-258zM973 -35l603 402l-269 180l-334 -223v-359zM896 458l272 182l-272 182l-272 -182zM485 733l334 223v359l-603 -402zM1445 640l193 -129v258zM1307 733l269 180l-603 402v-359zM1792 913v-546 q0 -41 -34 -64l-819 -546q-21 -13 -43 -13t-43 13l-819 546q-34 23 -34 64v546q0 41 34 64l819 546q21 13 43 13t43 -13l819 -546q34 -23 34 -64z" />
 <glyph unicode="&#xf1cc;" horiz-adv-x="2048" d="M1800 764q111 -46 179.5 -145.5t68.5 -221.5q0 -164 -118 -280.5t-285 -116.5q-4 0 -11.5 0.5t-10.5 0.5h-1209h-1h-2h-5q-170 10 -288 125.5t-118 280.5q0 110 55 203t147 147q-12 39 -12 82q0 115 82 196t199 81q95 0 172 -58q75 154 222.5 248t326.5 94 q166 0 306 -80.5t221.5 -218.5t81.5 -301q0 -6 -0.5 -18t-0.5 -18zM468 498q0 -122 84 -193t208 -71q137 0 240 99q-16 20 -47.5 56.5t-43.5 50.5q-67 -65 -144 -65q-55 0 -93.5 33.5t-38.5 87.5q0 53 38.5 87t91.5 34q44 0 84.5 -21t73 -55t65 -75t69 -82t77 -75t97 -55 t121.5 -21q121 0 204.5 71.5t83.5 190.5q0 121 -84 192t-207 71q-143 0 -241 -97q14 -16 29.5 -34t34.5 -40t29 -34q66 64 142 64q52 0 92 -33t40 -84q0 -57 -37 -91.5t-94 -34.5q-43 0 -82.5 21t-72 55t-65.5 75t-69.5 82t-77.5 75t-96.5 55t-118.5 21q-122 0 -207 -70.5 t-85 -189.5z" />
 <glyph unicode="&#xf1cd;" horiz-adv-x="1792" d="M896 1536q182 0 348 -71t286 -191t191 -286t71 -348t-71 -348t-191 -286t-286 -191t-348 -71t-348 71t-286 191t-191 286t-71 348t71 348t191 286t286 191t348 71zM896 1408q-190 0 -361 -90l194 -194q82 28 167 28t167 -28l194 194q-171 90 -361 90zM218 279l194 194 q-28 82 -28 167t28 167l-194 194q-90 -171 -90 -361t90 -361zM896 -128q190 0 361 90l-194 194q-82 -28 -167 -28t-167 28l-194 -194q171 -90 361 -90zM896 256q159 0 271.5 112.5t112.5 271.5t-112.5 271.5t-271.5 112.5t-271.5 -112.5t-112.5 -271.5t112.5 -271.5 t271.5 -112.5zM1380 473l194 -194q90 171 90 361t-90 361l-194 -194q28 -82 28 -167t-28 -167z" />
-<glyph unicode="&#xf1ce;" horiz-adv-x="1792" d="M1792 640q0 -182 -71 -348t-191 -286t-286 -191t-348 -71t-348 71t-286 191t-191 286t-71 348q0 222 101 414.5t276.5 317t390.5 155.5v-260q-221 -45 -366.5 -221t-145.5 -406q0 -130 51 -248.5t136.5 -204t204 -136.5t248.5 -51t248.5 51t204 136.5t136.5 204t51 248.5 q0 230 -145.5 406t-366.5 221v260q215 -31 390.5 -155.5t276.5 -317t101 -414.5z" />
+<glyph unicode="&#xf1ce;" horiz-adv-x="1792" d="M1760 640q0 -176 -68.5 -336t-184 -275.5t-275.5 -184t-336 -68.5t-336 68.5t-275.5 184t-184 275.5t-68.5 336q0 213 97 398.5t265 305.5t374 151v-228q-221 -45 -366.5 -221t-145.5 -406q0 -130 51 -248.5t136.5 -204t204 -136.5t248.5 -51t248.5 51t204 136.5 t136.5 204t51 248.5q0 230 -145.5 406t-366.5 221v228q206 -31 374 -151t265 -305.5t97 -398.5z" />
 <glyph unicode="&#xf1d0;" horiz-adv-x="1792" d="M19 662q8 217 116 406t305 318h5q0 -1 -1 -3q-8 -8 -28 -33.5t-52 -76.5t-60 -110.5t-44.5 -135.5t-14 -150.5t39 -157.5t108.5 -154q50 -50 102 -69.5t90.5 -11.5t69.5 23.5t47 32.5l16 16q39 51 53 116.5t6.5 122.5t-21 107t-26.5 80l-14 29q-10 25 -30.5 49.5t-43 41 t-43.5 29.5t-35 19l-13 6l104 115q39 -17 78 -52t59 -61l19 -27q1 48 -18.5 103.5t-40.5 87.5l-20 31l161 183l160 -181q-33 -46 -52.5 -102.5t-22.5 -90.5l-4 -33q22 37 61.5 72.5t67.5 52.5l28 17l103 -115q-44 -14 -85 -50t-60 -65l-19 -29q-31 -56 -48 -133.5t-7 -170 t57 -156.5q33 -45 77.5 -60.5t85 -5.5t76 26.5t57.5 33.5l21 16q60 53 96.5 115t48.5 121.5t10 121.5t-18 118t-37 107.5t-45.5 93t-45 72t-34.5 47.5l-13 17q-14 13 -7 13l10 -3q40 -29 62.5 -46t62 -50t64 -58t58.5 -65t55.5 -77t45.5 -88t38 -103t23.5 -117t10.5 -136 q3 -259 -108 -465t-312 -321t-456 -115q-185 0 -351 74t-283.5 198t-184 293t-60.5 353z" />
 <glyph unicode="&#xf1d1;" horiz-adv-x="1792" d="M874 -102v-66q-208 6 -385 109.5t-283 275.5l58 34q29 -49 73 -99l65 57q148 -168 368 -212l-17 -86q65 -12 121 -13zM276 428l-83 -28q22 -60 49 -112l-57 -33q-98 180 -98 385t98 385l57 -33q-30 -56 -49 -112l82 -28q-35 -100 -35 -212q0 -109 36 -212zM1528 251 l58 -34q-106 -172 -283 -275.5t-385 -109.5v66q56 1 121 13l-17 86q220 44 368 212l65 -57q44 50 73 99zM1377 805l-233 -80q14 -42 14 -85t-14 -85l232 -80q-31 -92 -98 -169l-185 162q-57 -67 -147 -85l48 -241q-52 -10 -98 -10t-98 10l48 241q-90 18 -147 85l-185 -162 q-67 77 -98 169l232 80q-14 42 -14 85t14 85l-233 80q33 93 99 169l185 -162q59 68 147 86l-48 240q44 10 98 10t98 -10l-48 -240q88 -18 147 -86l185 162q66 -76 99 -169zM874 1448v-66q-65 -2 -121 -13l17 -86q-220 -42 -368 -211l-65 56q-38 -42 -73 -98l-57 33 q106 172 282 275.5t385 109.5zM1705 640q0 -205 -98 -385l-57 33q27 52 49 112l-83 28q36 103 36 212q0 112 -35 212l82 28q-19 56 -49 112l57 33q98 -180 98 -385zM1585 1063l-57 -33q-35 56 -73 98l-65 -56q-148 169 -368 211l17 86q-56 11 -121 13v66q209 -6 385 -109.5 t282 -275.5zM1748 640q0 173 -67.5 331t-181.5 272t-272 181.5t-331 67.5t-331 -67.5t-272 -181.5t-181.5 -272t-67.5 -331t67.5 -331t181.5 -272t272 -181.5t331 -67.5t331 67.5t272 181.5t181.5 272t67.5 331zM1792 640q0 -182 -71 -348t-191 -286t-286 -191t-348 -71 t-348 71t-286 191t-191 286t-71 348t71 348t191 286t286 191t348 71t348 -71t286 -191t191 -286t71 -348z" />
 <glyph unicode="&#xf1d2;" d="M582 228q0 -66 -93 -66q-107 0 -107 63q0 64 98 64q102 0 102 -61zM546 694q0 -85 -74 -85q-77 0 -77 84q0 90 77 90q36 0 55 -25.5t19 -63.5zM712 769v125q-78 -29 -135 -29q-50 29 -110 29q-86 0 -145 -57t-59 -143q0 -50 29.5 -102t73.5 -67v-3q-38 -17 -38 -85 q0 -53 41 -77v-3q-113 -37 -113 -139q0 -45 20 -78.5t54 -51t72 -25.5t81 -8q224 0 224 188q0 67 -48 99t-126 46q-27 5 -51.5 20.5t-24.5 39.5q0 44 49 52q77 15 122 70t45 134q0 24 -10 52q37 9 49 13zM771 350h137q-2 27 -2 82v387q0 46 2 69h-137q3 -23 3 -71v-392 q0 -50 -3 -75zM1280 366v121q-30 -21 -68 -21q-53 0 -53 82v225h52q9 0 26.5 -1t26.5 -1v117h-105q0 82 3 102h-140q4 -24 4 -55v-47h-60v-117q36 3 37 3q3 0 11 -0.5t12 -0.5v-2h-2v-217q0 -37 2.5 -64t11.5 -56.5t24.5 -48.5t43.5 -31t66 -12q64 0 108 24zM924 1072 q0 36 -24 63.5t-60 27.5t-60.5 -27t-24.5 -64q0 -36 25 -62.5t60 -26.5t59.5 27t24.5 62zM1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" />
 <glyph unicode="&#xf1d3;" horiz-adv-x="1792" d="M595 22q0 100 -165 100q-158 0 -158 -104q0 -101 172 -101q151 0 151 105zM536 777q0 61 -30 102t-89 41q-124 0 -124 -145q0 -135 124 -135q119 0 119 137zM805 1101v-202q-36 -12 -79 -22q16 -43 16 -84q0 -127 -73 -216.5t-197 -112.5q-40 -8 -59.5 -27t-19.5 -58 q0 -31 22.5 -51.5t58 -32t78.5 -22t86 -25.5t78.5 -37.5t58 -64t22.5 -98.5q0 -304 -363 -304q-69 0 -130 12.5t-116 41t-87.5 82t-32.5 127.5q0 165 182 225v4q-67 41 -67 126q0 109 63 137v4q-72 24 -119.5 108.5t-47.5 165.5q0 139 95 231.5t235 92.5q96 0 178 -47 q98 0 218 47zM1123 220h-222q4 45 4 134v609q0 94 -4 128h222q-4 -33 -4 -124v-613q0 -89 4 -134zM1724 442v-196q-71 -39 -174 -39q-62 0 -107 20t-70 50t-39.5 78t-18.5 92t-4 103v351h2v4q-7 0 -19 1t-18 1q-21 0 -59 -6v190h96v76q0 54 -6 89h227q-6 -41 -6 -165h171 v-190q-15 0 -43.5 2t-42.5 2h-85v-365q0 -131 87 -131q61 0 109 33zM1148 1389q0 -58 -39 -101.5t-96 -43.5q-58 0 -98 43.5t-40 101.5q0 59 39.5 103t98.5 44q58 0 96.5 -44.5t38.5 -102.5z" />
-<glyph unicode="&#xf1d4;" d="M825 547l343 588h-150q-21 -39 -63.5 -118.5t-68 -128.5t-59.5 -118.5t-60 -128.5h-3q-21 48 -44.5 97t-52 105.5t-46.5 92t-54 104.5t-49 95h-150l323 -589v-435h134v436zM1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960 q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" />
+<glyph unicode="&#xf1d4;" d="M809 532l266 499h-112l-157 -312q-24 -48 -44 -92l-42 92l-155 312h-120l263 -493v-324h101v318zM1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" />
 <glyph unicode="&#xf1d5;" horiz-adv-x="1280" d="M842 964q0 -80 -57 -136.5t-136 -56.5q-60 0 -111 35q-62 -67 -115 -146q-247 -371 -202 -859q1 -22 -12.5 -38.5t-34.5 -18.5h-5q-20 0 -35 13.5t-17 33.5q-14 126 -3.5 247.5t29.5 217t54 186t69 155.5t74 125q61 90 132 165q-16 35 -16 77q0 80 56.5 136.5t136.5 56.5 t136.5 -56.5t56.5 -136.5zM1223 953q0 -158 -78 -292t-212.5 -212t-292.5 -78q-64 0 -131 14q-21 5 -32.5 23.5t-6.5 39.5q5 20 23 31.5t39 7.5q51 -13 108 -13q97 0 186 38t153 102t102 153t38 186t-38 186t-102 153t-153 102t-186 38t-186 -38t-153 -102t-102 -153 t-38 -186q0 -114 52 -218q10 -20 3.5 -40t-25.5 -30t-39.5 -3t-30.5 26q-64 123 -64 265q0 119 46.5 227t124.5 186t186 124t226 46q158 0 292.5 -78t212.5 -212.5t78 -292.5z" />
 <glyph unicode="&#xf1d6;" horiz-adv-x="1792" d="M270 730q-8 19 -8 52q0 20 11 49t24 45q-1 22 7.5 53t22.5 43q0 139 92.5 288.5t217.5 209.5q139 66 324 66q133 0 266 -55q49 -21 90 -48t71 -56t55 -68t42 -74t32.5 -84.5t25.5 -89.5t22 -98l1 -5q55 -83 55 -150q0 -14 -9 -40t-9 -38q0 -1 1.5 -3.5t3.5 -5t2 -3.5 q77 -114 120.5 -214.5t43.5 -208.5q0 -43 -19.5 -100t-55.5 -57q-9 0 -19.5 7.5t-19 17.5t-19 26t-16 26.5t-13.5 26t-9 17.5q-1 1 -3 1l-5 -4q-59 -154 -132 -223q20 -20 61.5 -38.5t69 -41.5t35.5 -65q-2 -4 -4 -16t-7 -18q-64 -97 -302 -97q-53 0 -110.5 9t-98 20 t-104.5 30q-15 5 -23 7q-14 4 -46 4.5t-40 1.5q-41 -45 -127.5 -65t-168.5 -20q-35 0 -69 1.5t-93 9t-101 20.5t-74.5 40t-32.5 64q0 40 10 59.5t41 48.5q11 2 40.5 13t49.5 12q4 0 14 2q2 2 2 4l-2 3q-48 11 -108 105.5t-73 156.5l-5 3q-4 0 -12 -20q-18 -41 -54.5 -74.5 t-77.5 -37.5h-1q-4 0 -6 4.5t-5 5.5q-23 54 -23 100q0 275 252 466z" />
 <glyph unicode="&#xf1d7;" horiz-adv-x="2048" d="M580 1075q0 41 -25 66t-66 25q-43 0 -76 -25.5t-33 -65.5q0 -39 33 -64.5t76 -25.5q41 0 66 24.5t25 65.5zM1323 568q0 28 -25.5 50t-65.5 22q-27 0 -49.5 -22.5t-22.5 -49.5q0 -28 22.5 -50.5t49.5 -22.5q40 0 65.5 22t25.5 51zM1087 1075q0 41 -24.5 66t-65.5 25 q-43 0 -76 -25.5t-33 -65.5q0 -39 33 -64.5t76 -25.5q41 0 65.5 24.5t24.5 65.5zM1722 568q0 28 -26 50t-65 22q-27 0 -49.5 -22.5t-22.5 -49.5q0 -28 22.5 -50.5t49.5 -22.5q39 0 65 22t26 51zM1456 965q-31 4 -70 4q-169 0 -311 -77t-223.5 -208.5t-81.5 -287.5 q0 -78 23 -152q-35 -3 -68 -3q-26 0 -50 1.5t-55 6.5t-44.5 7t-54.5 10.5t-50 10.5l-253 -127l72 218q-290 203 -290 490q0 169 97.5 311t264 223.5t363.5 81.5q176 0 332.5 -66t262 -182.5t136.5 -260.5zM2048 404q0 -117 -68.5 -223.5t-185.5 -193.5l55 -181l-199 109 q-150 -37 -218 -37q-169 0 -311 70.5t-223.5 191.5t-81.5 264t81.5 264t223.5 191.5t311 70.5q161 0 303 -70.5t227.5 -192t85.5 -263.5z" />
@@ -483,13 +483,13 @@
 <glyph unicode="&#xf1ea;" horiz-adv-x="2048" d="M1024 1024h-384v-384h384v384zM1152 384v-128h-640v128h640zM1152 1152v-640h-640v640h640zM1792 384v-128h-512v128h512zM1792 640v-128h-512v128h512zM1792 896v-128h-512v128h512zM1792 1152v-128h-512v128h512zM256 192v960h-128v-960q0 -26 19 -45t45 -19t45 19 t19 45zM1920 192v1088h-1536v-1088q0 -33 -11 -64h1483q26 0 45 19t19 45zM2048 1408v-1216q0 -80 -56 -136t-136 -56h-1664q-80 0 -136 56t-56 136v1088h256v128h1792z" />
 <glyph unicode="&#xf1eb;" horiz-adv-x="2048" d="M1024 13q-20 0 -93 73.5t-73 93.5q0 32 62.5 54t103.5 22t103.5 -22t62.5 -54q0 -20 -73 -93.5t-93 -73.5zM1294 284q-2 0 -40 25t-101.5 50t-128.5 25t-128.5 -25t-101 -50t-40.5 -25q-18 0 -93.5 75t-75.5 93q0 13 10 23q78 77 196 121t233 44t233 -44t196 -121 q10 -10 10 -23q0 -18 -75.5 -93t-93.5 -75zM1567 556q-11 0 -23 8q-136 105 -252 154.5t-268 49.5q-85 0 -170.5 -22t-149 -53t-113.5 -62t-79 -53t-31 -22q-17 0 -92 75t-75 93q0 12 10 22q132 132 320 205t380 73t380 -73t320 -205q10 -10 10 -22q0 -18 -75 -93t-92 -75z M1838 827q-11 0 -22 9q-179 157 -371.5 236.5t-420.5 79.5t-420.5 -79.5t-371.5 -236.5q-11 -9 -22 -9q-17 0 -92.5 75t-75.5 93q0 13 10 23q187 186 445 288t527 102t527 -102t445 -288q10 -10 10 -23q0 -18 -75.5 -93t-92.5 -75z" />
 <glyph unicode="&#xf1ec;" horiz-adv-x="1792" d="M384 0q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM768 0q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM384 384q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5 t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM1152 0q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM768 384q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5 t37.5 90.5zM384 768q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM1152 384q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM768 768q0 53 -37.5 90.5t-90.5 37.5 t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM1536 0v384q0 52 -38 90t-90 38t-90 -38t-38 -90v-384q0 -52 38 -90t90 -38t90 38t38 90zM1152 768q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5z M1536 1088v256q0 26 -19 45t-45 19h-1280q-26 0 -45 -19t-19 -45v-256q0 -26 19 -45t45 -19h1280q26 0 45 19t19 45zM1536 768q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM1664 1408v-1536q0 -52 -38 -90t-90 -38 h-1408q-52 0 -90 38t-38 90v1536q0 52 38 90t90 38h1408q52 0 90 -38t38 -90z" />
-<glyph unicode="&#xf1ed;" horiz-adv-x="1792" d="M1112 1090q0 159 -237 159h-70q-32 0 -59.5 -21.5t-34.5 -52.5l-63 -276q-2 -5 -2 -16q0 -24 17 -39.5t41 -15.5h53q69 0 128.5 13t112.5 41t83.5 81.5t30.5 126.5zM1716 938q0 -265 -220 -428q-219 -161 -612 -161h-61q-32 0 -59 -21.5t-34 -52.5l-73 -316 q-8 -36 -40.5 -61.5t-69.5 -25.5h-213q-31 0 -53 20t-22 51q0 10 13 65h151q34 0 64 23.5t38 56.5l73 316q8 33 37.5 57t63.5 24h61q390 0 607 160t217 421q0 129 -51 207q183 -92 183 -335zM1533 1123q0 -264 -221 -428q-218 -161 -612 -161h-60q-32 0 -59.5 -22t-34.5 -53 l-73 -315q-8 -36 -40 -61.5t-69 -25.5h-214q-31 0 -52.5 19.5t-21.5 51.5q0 8 2 20l300 1301q8 36 40.5 61.5t69.5 25.5h444q68 0 125 -4t120.5 -15t113.5 -30t96.5 -50.5t77.5 -74t49.5 -103.5t18.5 -136z" />
-<glyph unicode="&#xf1ee;" horiz-adv-x="1792" d="M602 949q19 -61 31 -123.5t17 -141.5t-14 -159t-62 -145q-21 81 -67 157t-95.5 127t-99 90.5t-78.5 57.5t-33 19q-62 34 -81.5 100t14.5 128t101 81.5t129 -14.5q138 -83 238 -177zM927 1236q11 -25 20.5 -46t36.5 -100.5t42.5 -150.5t25.5 -179.5t0 -205.5t-47.5 -209.5 t-105.5 -208.5q-51 -72 -138 -72q-54 0 -98 31q-57 40 -69 109t28 127q60 85 81 195t13 199.5t-32 180.5t-39 128t-22 52q-31 63 -8.5 129.5t85.5 97.5q34 17 75 17q47 0 88.5 -25t63.5 -69zM1248 567q-17 -160 -72 -311q-17 131 -63 246q25 174 -5 361q-27 178 -94 342 q114 -90 212 -211q9 -37 15 -80q26 -179 7 -347zM1520 1440q9 -17 23.5 -49.5t43.5 -117.5t50.5 -178t34 -227.5t5 -269t-47 -300t-112.5 -323.5q-22 -48 -66 -75.5t-95 -27.5q-39 0 -74 16q-67 31 -92.5 100t4.5 136q58 126 90 257.5t37.5 239.5t-3.5 213.5t-26.5 180.5 t-38.5 138.5t-32.5 90t-15.5 32.5q-34 65 -11.5 135.5t87.5 104.5q37 20 81 20q49 0 91.5 -25.5t66.5 -70.5z" />
+<glyph unicode="&#xf1ed;" d="M1519 890q18 -84 -4 -204q-87 -444 -565 -444h-44q-25 0 -44 -16.5t-24 -42.5l-4 -19l-55 -346l-2 -15q-5 -26 -24.5 -42.5t-44.5 -16.5h-251q-21 0 -33 15t-9 36q9 56 26.5 168t26.5 168t27 167.5t27 167.5q5 37 43 37h131q133 -2 236 21q175 39 287 144q102 95 155 246 q24 70 35 133q1 6 2.5 7.5t3.5 1t6 -3.5q79 -59 98 -162zM1347 1172q0 -107 -46 -236q-80 -233 -302 -315q-113 -40 -252 -42q0 -1 -90 -1l-90 1q-100 0 -118 -96q-2 -8 -85 -530q-1 -10 -12 -10h-295q-22 0 -36.5 16.5t-11.5 38.5l232 1471q5 29 27.5 48t51.5 19h598 q34 0 97.5 -13t111.5 -32q107 -41 163.5 -123t56.5 -196z" />
+<glyph unicode="&#xf1ee;" horiz-adv-x="1792" d="M441 864q32 0 52 -26q266 -364 362 -774h-446q-127 441 -367 749q-12 16 -3 33.5t29 17.5h373zM1000 507q-49 -199 -125 -393q-79 310 -256 594q40 221 44 449q211 -340 337 -650zM1099 1216q235 -324 384.5 -698.5t184.5 -773.5h-451q-41 665 -553 1472h435zM1792 640 q0 -424 -101 -812q-67 560 -359 1083q-25 301 -106 584q-4 16 5.5 28.5t25.5 12.5h359q21 0 38.5 -13t22.5 -33q115 -409 115 -850z" />
 <glyph unicode="&#xf1f0;" horiz-adv-x="2304" d="M1975 546h-138q14 37 66 179l3 9q4 10 10 26t9 26l12 -55zM531 611l-58 295q-11 54 -75 54h-268l-2 -13q311 -79 403 -336zM710 960l-162 -438l-17 89q-26 70 -85 129.5t-131 88.5l135 -510h175l261 641h-176zM849 318h166l104 642h-166zM1617 944q-69 27 -149 27 q-123 0 -201 -59t-79 -153q-1 -102 145 -174q48 -23 67 -41t19 -39q0 -30 -30 -46t-69 -16q-86 0 -156 33l-22 11l-23 -144q74 -34 185 -34q130 -1 208.5 59t80.5 160q0 106 -140 174q-49 25 -71 42t-22 38q0 22 24.5 38.5t70.5 16.5q70 1 124 -24l15 -8zM2042 960h-128 q-65 0 -87 -54l-246 -588h174l35 96h212q5 -22 20 -96h154zM2304 1280v-1280q0 -52 -38 -90t-90 -38h-2048q-52 0 -90 38t-38 90v1280q0 52 38 90t90 38h2048q52 0 90 -38t38 -90z" />
 <glyph unicode="&#xf1f1;" horiz-adv-x="2304" d="M671 603h-13q-47 0 -47 -32q0 -22 20 -22q17 0 28 15t12 39zM1066 639h62v3q1 4 0.5 6.5t-1 7t-2 8t-4.5 6.5t-7.5 5t-11.5 2q-28 0 -36 -38zM1606 603h-12q-48 0 -48 -32q0 -22 20 -22q17 0 28 15t12 39zM1925 629q0 41 -30 41q-19 0 -31 -20t-12 -51q0 -42 28 -42 q20 0 32.5 20t12.5 52zM480 770h87l-44 -262h-56l32 201l-71 -201h-39l-4 200l-34 -200h-53l44 262h81l2 -163zM733 663q0 -6 -4 -42q-16 -101 -17 -113h-47l1 22q-20 -26 -58 -26q-23 0 -37.5 16t-14.5 42q0 39 26 60.5t73 21.5q14 0 23 -1q0 3 0.5 5.5t1 4.5t0.5 3 q0 20 -36 20q-29 0 -59 -10q0 4 7 48q38 11 67 11q74 0 74 -62zM889 721l-8 -49q-22 3 -41 3q-27 0 -27 -17q0 -8 4.5 -12t21.5 -11q40 -19 40 -60q0 -72 -87 -71q-34 0 -58 6q0 2 7 49q29 -8 51 -8q32 0 32 19q0 7 -4.5 11.5t-21.5 12.5q-43 20 -43 59q0 72 84 72 q30 0 50 -4zM977 721h28l-7 -52h-29q-2 -17 -6.5 -40.5t-7 -38.5t-2.5 -18q0 -16 19 -16q8 0 16 2l-8 -47q-21 -7 -40 -7q-43 0 -45 47q0 12 8 56q3 20 25 146h55zM1180 648q0 -23 -7 -52h-111q-3 -22 10 -33t38 -11q30 0 58 14l-9 -54q-30 -8 -57 -8q-95 0 -95 95 q0 55 27.5 90.5t69.5 35.5q35 0 55.5 -21t20.5 -56zM1319 722q-13 -23 -22 -62q-22 2 -31 -24t-25 -128h-56l3 14q22 130 29 199h51l-3 -33q14 21 25.5 29.5t28.5 4.5zM1506 763l-9 -57q-28 14 -50 14q-31 0 -51 -27.5t-20 -70.5q0 -30 13.5 -47t38.5 -17q21 0 48 13 l-10 -59q-28 -8 -50 -8q-45 0 -71.5 30.5t-26.5 82.5q0 70 35.5 114.5t91.5 44.5q26 0 61 -13zM1668 663q0 -18 -4 -42q-13 -79 -17 -113h-46l1 22q-20 -26 -59 -26q-23 0 -37 16t-14 42q0 39 25.5 60.5t72.5 21.5q15 0 23 -1q2 7 2 13q0 20 -36 20q-29 0 -59 -10q0 4 8 48 q38 11 67 11q73 0 73 -62zM1809 722q-14 -24 -21 -62q-23 2 -31.5 -23t-25.5 -129h-56l3 14q19 104 29 199h52q0 -11 -4 -33q15 21 26.5 29.5t27.5 4.5zM1950 770h56l-43 -262h-53l3 19q-23 -23 -52 -23q-31 0 -49.5 24t-18.5 64q0 53 27.5 92t64.5 39q31 0 53 -29z M2061 640q0 148 -72.5 273t-198 198t-273.5 73q-181 0 -328 -110q127 -116 171 -284h-50q-44 150 -158 253q-114 -103 -158 -253h-50q44 168 171 284q-147 110 -328 110q-148 0 -273.5 -73t-198 -198t-72.5 -273t72.5 -273t198 -198t273.5 -73q181 0 328 110 q-120 111 -165 264h50q46 -138 152 -233q106 95 152 233h50q-45 -153 -165 -264q147 -110 328 -110q148 0 273.5 73t198 198t72.5 273zM2304 1280v-1280q0 -52 -38 -90t-90 -38h-2048q-52 0 -90 38t-38 90v1280q0 52 38 90t90 38h2048q52 0 90 -38t38 -90z" />
 <glyph unicode="&#xf1f2;" horiz-adv-x="2304" d="M313 759q0 -51 -36 -84q-29 -26 -89 -26h-17v220h17q61 0 89 -27q36 -31 36 -83zM2089 824q0 -52 -64 -52h-19v101h20q63 0 63 -49zM380 759q0 74 -50 120.5t-129 46.5h-95v-333h95q74 0 119 38q60 51 60 128zM410 593h65v333h-65v-333zM730 694q0 40 -20.5 62t-75.5 42 q-29 10 -39.5 19t-10.5 23q0 16 13.5 26.5t34.5 10.5q29 0 53 -27l34 44q-41 37 -98 37q-44 0 -74 -27.5t-30 -67.5q0 -35 18 -55.5t64 -36.5q37 -13 45 -19q19 -12 19 -34q0 -20 -14 -33.5t-36 -13.5q-48 0 -71 44l-42 -40q44 -64 115 -64q51 0 83 30.5t32 79.5zM1008 604 v77q-37 -37 -78 -37q-49 0 -80.5 32.5t-31.5 82.5q0 48 31.5 81.5t77.5 33.5q43 0 81 -38v77q-40 20 -80 20q-74 0 -125.5 -50.5t-51.5 -123.5t51 -123.5t125 -50.5q42 0 81 19zM2240 0v527q-65 -40 -144.5 -84t-237.5 -117t-329.5 -137.5t-417.5 -134.5t-504 -118h1569 q26 0 45 19t19 45zM1389 757q0 75 -53 128t-128 53t-128 -53t-53 -128t53 -128t128 -53t128 53t53 128zM1541 584l144 342h-71l-90 -224l-89 224h-71l142 -342h35zM1714 593h184v56h-119v90h115v56h-115v74h119v57h-184v-333zM2105 593h80l-105 140q76 16 76 94q0 47 -31 73 t-87 26h-97v-333h65v133h9zM2304 1274v-1268q0 -56 -38.5 -95t-93.5 -39h-2040q-55 0 -93.5 39t-38.5 95v1268q0 56 38.5 95t93.5 39h2040q55 0 93.5 -39t38.5 -95z" />
 <glyph unicode="&#xf1f3;" horiz-adv-x="2304" d="M119 854h89l-45 108zM740 328l74 79l-70 79h-163v-49h142v-55h-142v-54h159zM898 406l99 -110v217zM1186 453q0 33 -40 33h-84v-69h83q41 0 41 36zM1475 457q0 29 -42 29h-82v-61h81q43 0 43 32zM1197 923q0 29 -42 29h-82v-60h81q43 0 43 31zM1656 854h89l-44 108z M699 1009v-271h-66v212l-94 -212h-57l-94 212v-212h-132l-25 60h-135l-25 -60h-70l116 271h96l110 -257v257h106l85 -184l77 184h108zM1255 453q0 -20 -5.5 -35t-14 -25t-22.5 -16.5t-26 -10t-31.5 -4.5t-31.5 -1t-32.5 0.5t-29.5 0.5v-91h-126l-80 90l-83 -90h-256v271h260 l80 -89l82 89h207q109 0 109 -89zM964 794v-56h-217v271h217v-57h-152v-49h148v-55h-148v-54h152zM2304 235v-229q0 -55 -38.5 -94.5t-93.5 -39.5h-2040q-55 0 -93.5 39.5t-38.5 94.5v678h111l25 61h55l25 -61h218v46l19 -46h113l20 47v-47h541v99l10 1q10 0 10 -14v-86h279 v23q23 -12 55 -18t52.5 -6.5t63 0.5t51.5 1l25 61h56l25 -61h227v58l34 -58h182v378h-180v-44l-25 44h-185v-44l-23 44h-249q-69 0 -109 -22v22h-172v-22q-24 22 -73 22h-628l-43 -97l-43 97h-198v-44l-22 44h-169l-78 -179v391q0 55 38.5 94.5t93.5 39.5h2040 q55 0 93.5 -39.5t38.5 -94.5v-678h-120q-51 0 -81 -22v22h-177q-55 0 -78 -22v22h-316v-22q-31 22 -87 22h-209v-22q-23 22 -91 22h-234l-54 -58l-50 58h-349v-378h343l55 59l52 -59h211v89h21q59 0 90 13v-102h174v99h8q8 0 10 -2t2 -10v-87h529q57 0 88 24v-24h168 q60 0 95 17zM1546 469q0 -23 -12 -43t-34 -29q25 -9 34 -26t9 -46v-54h-65v45q0 33 -12 43.5t-46 10.5h-69v-99h-65v271h154q48 0 77 -15t29 -58zM1269 936q0 -24 -12.5 -44t-33.5 -29q26 -9 34.5 -25.5t8.5 -46.5v-53h-65q0 9 0.5 26.5t0 25t-3 18.5t-8.5 16t-17.5 8.5 t-29.5 3.5h-70v-98h-64v271l153 -1q49 0 78 -14.5t29 -57.5zM1798 327v-56h-216v271h216v-56h-151v-49h148v-55h-148v-54zM1372 1009v-271h-66v271h66zM2065 357q0 -86 -102 -86h-126v58h126q34 0 34 25q0 16 -17 21t-41.5 5t-49.5 3.5t-42 22.5t-17 55q0 39 26 60t66 21 h130v-57h-119q-36 0 -36 -25q0 -16 17.5 -20.5t42 -4t49 -2.5t42 -21.5t17.5 -54.5zM2304 407v-101q-24 -35 -88 -35h-125v58h125q33 0 33 25q0 13 -12.5 19t-31 5.5t-40 2t-40 8t-31 24t-12.5 48.5q0 39 26.5 60t66.5 21h129v-57h-118q-36 0 -36 -25q0 -20 29 -22t68.5 -5 t56.5 -26zM2139 1008v-270h-92l-122 203v-203h-132l-26 60h-134l-25 -60h-75q-129 0 -129 133q0 138 133 138h63v-59q-7 0 -28 1t-28.5 0.5t-23 -2t-21.5 -6.5t-14.5 -13.5t-11.5 -23t-3 -33.5q0 -38 13.5 -58t49.5 -20h29l92 213h97l109 -256v256h99l114 -188v188h66z" />
-<glyph unicode="&#xf1f4;" horiz-adv-x="2304" d="M322 689h-15q-19 0 -19 18q0 28 19 85q5 15 15 19.5t28 4.5q77 0 77 -49q0 -41 -30.5 -59.5t-74.5 -18.5zM664 528q-47 0 -47 29q0 62 123 62l3 -3q-5 -88 -79 -88zM1438 687h-15q-19 0 -19 19q0 28 19 85q5 15 14.5 19t28.5 4q77 0 77 -49q0 -41 -30.5 -59.5 t-74.5 -18.5zM1780 527q-47 0 -47 30q0 62 123 62l3 -3q-5 -89 -79 -89zM373 894h-128q-8 0 -14.5 -4t-8.5 -7.5t-7 -12.5q-3 -7 -45 -190t-42 -192q0 -7 5.5 -12.5t13.5 -5.5h62q25 0 32.5 34.5l15 69t32.5 34.5q47 0 87.5 7.5t80.5 24.5t63.5 52.5t23.5 84.5 q0 36 -14.5 61t-41 36.5t-53.5 15.5t-62 4zM719 798q-38 0 -74 -6q-2 0 -8.5 -1t-9 -1.5l-7.5 -1.5t-7.5 -2t-6.5 -3t-6.5 -4t-5 -5t-4.5 -7t-4 -9q-9 -29 -9 -39t9 -10q5 0 21.5 5t19.5 6q30 8 58 8q74 0 74 -36q0 -11 -10 -14q-8 -2 -18 -3t-21.5 -1.5t-17.5 -1.5 q-38 -4 -64.5 -10t-56.5 -19.5t-45.5 -39t-15.5 -62.5q0 -38 26 -59.5t64 -21.5q24 0 45.5 6.5t33 13t38.5 23.5q-3 -7 -3 -15t5.5 -13.5t12.5 -5.5h56q1 1 7 3.5t7.5 3.5t5 3.5t5 5.5t2.5 8l45 194q4 13 4 30q0 81 -145 81zM1247 793h-74q-22 0 -39 -23q-5 -7 -29.5 -51 t-46.5 -81.5t-26 -38.5l-5 4q0 77 -27 166q-1 5 -3.5 8.5t-6 6.5t-6.5 5t-8.5 3t-8.5 1.5t-9.5 1t-9 0.5h-10h-8.5q-38 0 -38 -21l1 -5q5 -53 25 -151t25 -143q2 -16 2 -24q0 -19 -30.5 -61.5t-30.5 -58.5q0 -13 40 -13q61 0 76 25l245 415q10 20 10 26q0 9 -8 9zM1489 892 h-129q-18 0 -29 -23q-6 -13 -46.5 -191.5t-40.5 -190.5q0 -20 43 -20h7.5h9h9t9.5 1t8.5 2t8.5 3t6.5 4.5t5.5 6t3 8.5l21 91q2 10 10.5 17t19.5 7q47 0 87.5 7t80.5 24.5t63.5 52.5t23.5 84q0 36 -14.5 61t-41 36.5t-53.5 15.5t-62 4zM1835 798q-26 0 -74 -6 q-38 -6 -48 -16q-7 -8 -11 -19q-8 -24 -8 -39q0 -10 8 -10q1 0 41 12q30 8 58 8q74 0 74 -36q0 -12 -10 -14q-4 -1 -57 -7q-38 -4 -64.5 -10t-56.5 -19.5t-45.5 -39t-15.5 -62.5t26 -58.5t64 -21.5q24 0 45 6t34 13t38 24q-3 -15 -3 -16q0 -5 2 -8.5t6.5 -5.5t8 -3.5 t10.5 -2t9.5 -0.5h9.5h8q42 0 48 25l45 194q3 15 3 31q0 81 -145 81zM2157 889h-55q-25 0 -33 -40q-10 -44 -36.5 -167t-42.5 -190v-5q0 -16 16 -18h1h57q10 0 18.5 6.5t10.5 16.5l83 374h-1l1 5q0 7 -5.5 12.5t-13.5 5.5zM2304 1280v-1280q0 -52 -38 -90t-90 -38h-2048 q-52 0 -90 38t-38 90v1280q0 52 38 90t90 38h2048q52 0 90 -38t38 -90z" />
+<glyph unicode="&#xf1f4;" horiz-adv-x="2304" d="M745 630q0 -37 -25.5 -61.5t-62.5 -24.5q-29 0 -46.5 16t-17.5 44q0 37 25 62.5t62 25.5q28 0 46.5 -16.5t18.5 -45.5zM1530 779q0 -42 -22 -57t-66 -15l-32 -1l17 107q2 11 13 11h18q22 0 35 -2t25 -12.5t12 -30.5zM1881 630q0 -36 -25.5 -61t-61.5 -25q-29 0 -47 16 t-18 44q0 37 25 62.5t62 25.5q28 0 46.5 -16.5t18.5 -45.5zM513 801q0 59 -38.5 85.5t-100.5 26.5h-160q-19 0 -21 -19l-65 -408q-1 -6 3 -11t10 -5h76q20 0 22 19l18 110q1 8 7 13t15 6.5t17 1.5t19 -1t14 -1q86 0 135 48.5t49 134.5zM822 489l41 261q1 6 -3 11t-10 5h-76 q-14 0 -17 -33q-27 40 -95 40q-72 0 -122.5 -54t-50.5 -127q0 -59 34.5 -94t92.5 -35q28 0 58 12t48 32q-4 -12 -4 -21q0 -16 13 -16h69q19 0 22 19zM1269 752q0 5 -4 9.5t-9 4.5h-77q-11 0 -18 -10l-106 -156l-44 150q-5 16 -22 16h-75q-5 0 -9 -4.5t-4 -9.5q0 -2 19.5 -59 t42 -123t23.5 -70q-82 -112 -82 -120q0 -13 13 -13h77q11 0 18 10l255 368q2 2 2 7zM1649 801q0 59 -38.5 85.5t-100.5 26.5h-159q-20 0 -22 -19l-65 -408q-1 -6 3 -11t10 -5h82q12 0 16 13l18 116q1 8 7 13t15 6.5t17 1.5t19 -1t14 -1q86 0 135 48.5t49 134.5zM1958 489 l41 261q1 6 -3 11t-10 5h-76q-14 0 -17 -33q-26 40 -95 40q-72 0 -122.5 -54t-50.5 -127q0 -59 34.5 -94t92.5 -35q29 0 59 12t47 32q0 -1 -2 -9t-2 -12q0 -16 13 -16h69q19 0 22 19zM2176 898v1q0 14 -13 14h-74q-11 0 -13 -11l-65 -416l-1 -2q0 -5 4 -9.5t10 -4.5h66 q19 0 21 19zM392 764q-5 -35 -26 -46t-60 -11l-33 -1l17 107q2 11 13 11h19q40 0 58 -11.5t12 -48.5zM2304 1280v-1280q0 -52 -38 -90t-90 -38h-2048q-52 0 -90 38t-38 90v1280q0 52 38 90t90 38h2048q52 0 90 -38t38 -90z" />
 <glyph unicode="&#xf1f5;" horiz-adv-x="2304" d="M1597 633q0 -69 -21 -106q-19 -35 -52 -35q-23 0 -41 9v224q29 30 57 30q57 0 57 -122zM2035 669h-110q6 98 56 98q51 0 54 -98zM476 534q0 59 -33 91.5t-101 57.5q-36 13 -52 24t-16 25q0 26 38 26q58 0 124 -33l18 112q-67 32 -149 32q-77 0 -123 -38q-48 -39 -48 -109 q0 -58 32.5 -90.5t99.5 -56.5q39 -14 54.5 -25.5t15.5 -27.5q0 -31 -48 -31q-29 0 -70 12.5t-72 30.5l-18 -113q72 -41 168 -41q81 0 129 37q51 41 51 117zM771 749l19 111h-96v135l-129 -21l-18 -114l-46 -8l-17 -103h62v-219q0 -84 44 -120q38 -30 111 -30q32 0 79 11v118 q-32 -7 -44 -7q-42 0 -42 50v197h77zM1087 724v139q-15 3 -28 3q-32 0 -55.5 -16t-33.5 -46l-10 56h-131v-471h150v306q26 31 82 31q16 0 26 -2zM1124 389h150v471h-150v-471zM1746 638q0 122 -45 179q-40 52 -111 52q-64 0 -117 -56l-8 47h-132v-645l150 25v151 q36 -11 68 -11q83 0 134 56q61 65 61 202zM1278 986q0 33 -23 56t-56 23t-56 -23t-23 -56t23 -56.5t56 -23.5t56 23.5t23 56.5zM2176 629q0 113 -48 176q-50 64 -144 64q-96 0 -151.5 -66t-55.5 -180q0 -128 63 -188q55 -55 161 -55q101 0 160 40l-16 103q-57 -31 -128 -31 q-43 0 -63 19q-23 19 -28 66h248q2 14 2 52zM2304 1280v-1280q0 -52 -38 -90t-90 -38h-2048q-52 0 -90 38t-38 90v1280q0 52 38 90t90 38h2048q52 0 90 -38t38 -90z" />
 <glyph unicode="&#xf1f6;" horiz-adv-x="2048" d="M1558 684q61 -356 298 -556q0 -52 -38 -90t-90 -38h-448q0 -106 -75 -181t-181 -75t-180.5 74.5t-75.5 180.5zM1024 -176q16 0 16 16t-16 16q-59 0 -101.5 42.5t-42.5 101.5q0 16 -16 16t-16 -16q0 -73 51.5 -124.5t124.5 -51.5zM2026 1424q8 -10 7.5 -23.5t-10.5 -22.5 l-1872 -1622q-10 -8 -23.5 -7t-21.5 11l-84 96q-8 10 -7.5 23.5t10.5 21.5l186 161q-19 32 -19 66q50 42 91 88t85 119.5t74.5 158.5t50 206t19.5 260q0 152 117 282.5t307 158.5q-8 19 -8 39q0 40 28 68t68 28t68 -28t28 -68q0 -20 -8 -39q124 -18 219 -82.5t148 -157.5 l418 363q10 8 23.5 7t21.5 -11z" />
 <glyph unicode="&#xf1f7;" horiz-adv-x="2048" d="M1040 -160q0 16 -16 16q-59 0 -101.5 42.5t-42.5 101.5q0 16 -16 16t-16 -16q0 -73 51.5 -124.5t124.5 -51.5q16 0 16 16zM503 315l877 760q-42 88 -132.5 146.5t-223.5 58.5q-93 0 -169.5 -31.5t-121.5 -80.5t-69 -103t-24 -105q0 -384 -137 -645zM1856 128 q0 -52 -38 -90t-90 -38h-448q0 -106 -75 -181t-181 -75t-180.5 74.5t-75.5 180.5l149 129h757q-166 187 -227 459l111 97q61 -356 298 -556zM1942 1520l84 -96q8 -10 7.5 -23.5t-10.5 -22.5l-1872 -1622q-10 -8 -23.5 -7t-21.5 11l-84 96q-8 10 -7.5 23.5t10.5 21.5l186 161 q-19 32 -19 66q50 42 91 88t85 119.5t74.5 158.5t50 206t19.5 260q0 152 117 282.5t307 158.5q-8 19 -8 39q0 40 28 68t68 28t68 -28t28 -68q0 -20 -8 -39q124 -18 219 -82.5t148 -157.5l418 363q10 8 23.5 7t21.5 -11z" />
@@ -513,8 +513,173 @@
 <glyph unicode="&#xf20a;" horiz-adv-x="2048" d="M785 528h207q-14 -158 -98.5 -248.5t-214.5 -90.5q-162 0 -254.5 116t-92.5 316q0 194 93 311.5t233 117.5q148 0 232 -87t97 -247h-203q-5 64 -35.5 99t-81.5 35q-57 0 -88.5 -60.5t-31.5 -177.5q0 -48 5 -84t18 -69.5t40 -51.5t66 -18q95 0 109 139zM1497 528h206 q-14 -158 -98 -248.5t-214 -90.5q-162 0 -254.5 116t-92.5 316q0 194 93 311.5t233 117.5q148 0 232 -87t97 -247h-204q-4 64 -35 99t-81 35q-57 0 -88.5 -60.5t-31.5 -177.5q0 -48 5 -84t18 -69.5t39.5 -51.5t65.5 -18q49 0 76.5 38t33.5 101zM1856 647q0 207 -15.5 307 t-60.5 161q-6 8 -13.5 14t-21.5 15t-16 11q-86 63 -697 63q-625 0 -710 -63q-5 -4 -17.5 -11.5t-21 -14t-14.5 -14.5q-45 -60 -60 -159.5t-15 -308.5q0 -208 15 -307.5t60 -160.5q6 -8 15 -15t20.5 -14t17.5 -12q44 -33 239.5 -49t470.5 -16q610 0 697 65q5 4 17 11t20.5 14 t13.5 16q46 60 61 159t15 309zM2048 1408v-1536h-2048v1536h2048z" />
 <glyph unicode="&#xf20b;" d="M992 912v-496q0 -14 -9 -23t-23 -9h-160q-14 0 -23 9t-9 23v496q0 112 -80 192t-192 80h-272v-1152q0 -14 -9 -23t-23 -9h-160q-14 0 -23 9t-9 23v1344q0 14 9 23t23 9h464q135 0 249 -66.5t180.5 -180.5t66.5 -249zM1376 1376v-880q0 -135 -66.5 -249t-180.5 -180.5 t-249 -66.5h-464q-14 0 -23 9t-9 23v960q0 14 9 23t23 9h160q14 0 23 -9t9 -23v-768h272q112 0 192 80t80 192v880q0 14 9 23t23 9h160q14 0 23 -9t9 -23z" />
 <glyph unicode="&#xf20c;" d="M1311 694v-114q0 -24 -13.5 -38t-37.5 -14h-202q-24 0 -38 14t-14 38v114q0 24 14 38t38 14h202q24 0 37.5 -14t13.5 -38zM821 464v250q0 53 -32.5 85.5t-85.5 32.5h-133q-68 0 -96 -52q-28 52 -96 52h-130q-53 0 -85.5 -32.5t-32.5 -85.5v-250q0 -22 21 -22h55 q22 0 22 22v230q0 24 13.5 38t38.5 14h94q24 0 38 -14t14 -38v-230q0 -22 21 -22h54q22 0 22 22v230q0 24 14 38t38 14h97q24 0 37.5 -14t13.5 -38v-230q0 -22 22 -22h55q21 0 21 22zM1410 560v154q0 53 -33 85.5t-86 32.5h-264q-53 0 -86 -32.5t-33 -85.5v-410 q0 -21 22 -21h55q21 0 21 21v180q31 -42 94 -42h191q53 0 86 32.5t33 85.5zM1536 1176v-1072q0 -96 -68 -164t-164 -68h-1072q-96 0 -164 68t-68 164v1072q0 96 68 164t164 68h1072q96 0 164 -68t68 -164z" />
-<glyph unicode="&#xf20d;" horiz-adv-x="1792" />
-<glyph unicode="&#xf20e;" horiz-adv-x="1792" />
+<glyph unicode="&#xf20d;" d="M915 450h-294l147 551zM1001 128h311l-324 1024h-440l-324 -1024h311l383 314zM1536 1120v-960q0 -118 -85 -203t-203 -85h-960q-118 0 -203 85t-85 203v960q0 118 85 203t203 85h960q118 0 203 -85t85 -203z" />
+<glyph unicode="&#xf20e;" horiz-adv-x="2048" d="M2048 641q0 -21 -13 -36.5t-33 -19.5l-205 -356q3 -9 3 -18q0 -20 -12.5 -35.5t-32.5 -19.5l-193 -337q3 -8 3 -16q0 -23 -16.5 -40t-40.5 -17q-25 0 -41 18h-400q-17 -20 -43 -20t-43 20h-399q-17 -20 -43 -20q-23 0 -40 16.5t-17 40.5q0 8 4 20l-193 335 q-20 4 -32.5 19.5t-12.5 35.5q0 9 3 18l-206 356q-20 5 -32.5 20.5t-12.5 35.5q0 21 13.5 36.5t33.5 19.5l199 344q0 1 -0.5 3t-0.5 3q0 36 34 51l209 363q-4 10 -4 18q0 24 17 40.5t40 16.5q26 0 44 -21h396q16 21 43 21t43 -21h398q18 21 44 21q23 0 40 -16.5t17 -40.5 q0 -6 -4 -18l207 -358q23 -1 39 -17.5t16 -38.5q0 -13 -7 -27l187 -324q19 -4 31.5 -19.5t12.5 -35.5zM1063 -158h389l-342 354h-143l-342 -354h360q18 16 39 16t39 -16zM112 654q1 -4 1 -13q0 -10 -2 -15l208 -360q2 0 4.5 -1t5.5 -2.5l5 -2.5l188 199v347l-187 194 q-13 -8 -29 -10zM986 1438h-388l190 -200l554 200h-280q-16 -16 -38 -16t-38 16zM1689 226q1 6 5 11l-64 68l-17 -79h76zM1583 226l22 105l-252 266l-296 -307l63 -64h463zM1495 -142l16 28l65 310h-427l333 -343q8 4 13 5zM578 -158h5l342 354h-373v-335l4 -6q14 -5 22 -13 zM552 226h402l64 66l-309 321l-157 -166v-221zM359 226h163v189l-168 -177q4 -8 5 -12zM358 1051q0 -1 0.5 -2t0.5 -2q0 -16 -8 -29l171 -177v269zM552 1121v-311l153 -157l297 314l-223 236zM556 1425l-4 -8v-264l205 74l-191 201q-6 -2 -10 -3zM1447 1438h-16l-621 -224 l213 -225zM1023 946l-297 -315l311 -319l296 307zM688 634l-136 141v-284zM1038 270l-42 -44h85zM1374 618l238 -251l132 624l-3 5l-1 1zM1718 1018q-8 13 -8 29v2l-216 376q-5 1 -13 5l-437 -463l310 -327zM522 1142v223l-163 -282zM522 196h-163l163 -283v283zM1607 196 l-48 -227l130 227h-82zM1729 266l207 361q-2 10 -2 14q0 1 3 16l-171 296l-129 -612l77 -82q5 3 15 7z" />
+<glyph unicode="&#xf210;" d="M0 856q0 131 91.5 226.5t222.5 95.5h742l352 358v-1470q0 -132 -91.5 -227t-222.5 -95h-780q-131 0 -222.5 95t-91.5 227v790zM1232 102l-176 180v425q0 46 -32 79t-78 33h-484q-46 0 -78 -33t-32 -79v-492q0 -46 32.5 -79.5t77.5 -33.5h770z" />
+<glyph unicode="&#xf211;" d="M934 1386q-317 -121 -556 -362.5t-358 -560.5q-20 89 -20 176q0 208 102.5 384.5t278.5 279t384 102.5q82 0 169 -19zM1203 1267q93 -65 164 -155q-389 -113 -674.5 -400.5t-396.5 -676.5q-93 72 -155 162q112 386 395 671t667 399zM470 -67q115 356 379.5 622t619.5 384 q40 -92 54 -195q-292 -120 -516 -345t-343 -518q-103 14 -194 52zM1536 -125q-193 50 -367 115q-135 -84 -290 -107q109 205 274 370.5t369 275.5q-21 -152 -101 -284q65 -175 115 -370z" />
+<glyph unicode="&#xf212;" horiz-adv-x="2048" d="M1893 1144l155 -1272q-131 0 -257 57q-200 91 -393 91q-226 0 -374 -148q-148 148 -374 148q-193 0 -393 -91q-128 -57 -252 -57h-5l155 1272q224 127 482 127q233 0 387 -106q154 106 387 106q258 0 482 -127zM1398 157q129 0 232 -28.5t260 -93.5l-124 1021 q-171 78 -368 78q-224 0 -374 -141q-150 141 -374 141q-197 0 -368 -78l-124 -1021q105 43 165.5 65t148.5 39.5t178 17.5q202 0 374 -108q172 108 374 108zM1438 191l-55 907q-211 -4 -359 -155q-152 155 -374 155q-176 0 -336 -66l-114 -941q124 51 228.5 76t221.5 25 q209 0 374 -102q172 107 374 102z" />
+<glyph unicode="&#xf213;" horiz-adv-x="2048" d="M1500 165v733q0 21 -15 36t-35 15h-93q-20 0 -35 -15t-15 -36v-733q0 -20 15 -35t35 -15h93q20 0 35 15t15 35zM1216 165v531q0 20 -15 35t-35 15h-101q-20 0 -35 -15t-15 -35v-531q0 -20 15 -35t35 -15h101q20 0 35 15t15 35zM924 165v429q0 20 -15 35t-35 15h-101 q-20 0 -35 -15t-15 -35v-429q0 -20 15 -35t35 -15h101q20 0 35 15t15 35zM632 165v362q0 20 -15 35t-35 15h-101q-20 0 -35 -15t-15 -35v-362q0 -20 15 -35t35 -15h101q20 0 35 15t15 35zM2048 311q0 -166 -118 -284t-284 -118h-1244q-166 0 -284 118t-118 284 q0 116 63 214.5t168 148.5q-10 34 -10 73q0 113 80.5 193.5t193.5 80.5q102 0 180 -67q45 183 194 300t338 117q149 0 275 -73.5t199.5 -199.5t73.5 -275q0 -66 -14 -122q135 -33 221 -142.5t86 -247.5z" />
+<glyph unicode="&#xf214;" d="M0 1536h1536v-1392l-776 -338l-760 338v1392zM1436 209v926h-1336v-926l661 -294zM1436 1235v201h-1336v-201h1336zM181 937v-115h-37v115h37zM181 789v-115h-37v115h37zM181 641v-115h-37v115h37zM181 493v-115h-37v115h37zM181 345v-115h-37v115h37zM207 202l15 34 l105 -47l-15 -33zM343 142l15 34l105 -46l-15 -34zM478 82l15 34l105 -46l-15 -34zM614 23l15 33l104 -46l-15 -34zM797 10l105 46l15 -33l-105 -47zM932 70l105 46l15 -34l-105 -46zM1068 130l105 46l15 -34l-105 -46zM1203 189l105 47l15 -34l-105 -46zM259 1389v-36h-114 v36h114zM421 1389v-36h-115v36h115zM583 1389v-36h-115v36h115zM744 1389v-36h-114v36h114zM906 1389v-36h-114v36h114zM1068 1389v-36h-115v36h115zM1230 1389v-36h-115v36h115zM1391 1389v-36h-114v36h114zM181 1049v-79h-37v115h115v-36h-78zM421 1085v-36h-115v36h115z M583 1085v-36h-115v36h115zM744 1085v-36h-114v36h114zM906 1085v-36h-114v36h114zM1068 1085v-36h-115v36h115zM1230 1085v-36h-115v36h115zM1355 970v79h-78v36h115v-115h-37zM1355 822v115h37v-115h-37zM1355 674v115h37v-115h-37zM1355 526v115h37v-115h-37zM1355 378 v115h37v-115h-37zM1355 230v115h37v-115h-37zM760 265q-129 0 -221 91.5t-92 221.5q0 129 92 221t221 92q130 0 221.5 -92t91.5 -221q0 -130 -91.5 -221.5t-221.5 -91.5zM595 646q0 -36 19.5 -56.5t49.5 -25t64 -7t64 -2t49.5 -9t19.5 -30.5q0 -49 -112 -49q-97 0 -123 51 h-3l-31 -63q67 -42 162 -42q29 0 56.5 5t55.5 16t45.5 33t17.5 53q0 46 -27.5 69.5t-67.5 27t-79.5 3t-67 5t-27.5 25.5q0 21 20.5 33t40.5 15t41 3q34 0 70.5 -11t51.5 -34h3l30 58q-3 1 -21 8.5t-22.5 9t-19.5 7t-22 7t-20 4.5t-24 4t-23 1q-29 0 -56.5 -5t-54 -16.5 t-43 -34t-16.5 -53.5z" />
+<glyph unicode="&#xf215;" horiz-adv-x="2048" d="M863 504q0 112 -79.5 191.5t-191.5 79.5t-191 -79.5t-79 -191.5t79 -191t191 -79t191.5 79t79.5 191zM1726 505q0 112 -79 191t-191 79t-191.5 -79t-79.5 -191q0 -113 79.5 -192t191.5 -79t191 79.5t79 191.5zM2048 1314v-1348q0 -44 -31.5 -75.5t-76.5 -31.5h-1832 q-45 0 -76.5 31.5t-31.5 75.5v1348q0 44 31.5 75.5t76.5 31.5h431q44 0 76 -31.5t32 -75.5v-161h754v161q0 44 32 75.5t76 31.5h431q45 0 76.5 -31.5t31.5 -75.5z" />
+<glyph unicode="&#xf216;" horiz-adv-x="2048" d="M1430 953zM1690 749q148 0 253 -98.5t105 -244.5q0 -157 -109 -261.5t-267 -104.5q-85 0 -162 27.5t-138 73.5t-118 106t-109 126.5t-103.5 132.5t-108.5 126t-117 106t-136 73.5t-159 27.5q-154 0 -251.5 -91.5t-97.5 -244.5q0 -157 104 -250t263 -93q100 0 208 37.5 t193 98.5q5 4 21 18.5t30 24t22 9.5q14 0 24.5 -10.5t10.5 -24.5q0 -24 -60 -77q-101 -88 -234.5 -142t-260.5 -54q-133 0 -245.5 58t-180 165t-67.5 241q0 205 141.5 341t347.5 136q120 0 226.5 -43.5t185.5 -113t151.5 -153t139 -167.5t133.5 -153.5t149.5 -113 t172.5 -43.5q102 0 168.5 61.5t66.5 162.5q0 95 -64.5 159t-159.5 64q-30 0 -81.5 -18.5t-68.5 -18.5q-20 0 -35.5 15t-15.5 35q0 18 8.5 57t8.5 59q0 159 -107.5 263t-266.5 104q-58 0 -111.5 -18.5t-84 -40.5t-55.5 -40.5t-33 -18.5q-15 0 -25.5 10.5t-10.5 25.5 q0 19 25 46q59 67 147 103.5t182 36.5q191 0 318 -125.5t127 -315.5q0 -37 -4 -66q57 15 115 15z" />
+<glyph unicode="&#xf217;" horiz-adv-x="1664" d="M1216 832q0 26 -19 45t-45 19h-128v128q0 26 -19 45t-45 19t-45 -19t-19 -45v-128h-128q-26 0 -45 -19t-19 -45t19 -45t45 -19h128v-128q0 -26 19 -45t45 -19t45 19t19 45v128h128q26 0 45 19t19 45zM640 0q0 -53 -37.5 -90.5t-90.5 -37.5t-90.5 37.5t-37.5 90.5 t37.5 90.5t90.5 37.5t90.5 -37.5t37.5 -90.5zM1536 0q0 -53 -37.5 -90.5t-90.5 -37.5t-90.5 37.5t-37.5 90.5t37.5 90.5t90.5 37.5t90.5 -37.5t37.5 -90.5zM1664 1088v-512q0 -24 -16 -42.5t-41 -21.5l-1044 -122q1 -7 4.5 -21.5t6 -26.5t2.5 -22q0 -16 -24 -64h920 q26 0 45 -19t19 -45t-19 -45t-45 -19h-1024q-26 0 -45 19t-19 45q0 14 11 39.5t29.5 59.5t20.5 38l-177 823h-204q-26 0 -45 19t-19 45t19 45t45 19h256q16 0 28.5 -6.5t20 -15.5t13 -24.5t7.5 -26.5t5.5 -29.5t4.5 -25.5h1201q26 0 45 -19t19 -45z" />
+<glyph unicode="&#xf218;" horiz-adv-x="1664" d="M1280 832q0 26 -19 45t-45 19t-45 -19l-147 -146v293q0 26 -19 45t-45 19t-45 -19t-19 -45v-293l-147 146q-19 19 -45 19t-45 -19t-19 -45t19 -45l256 -256q19 -19 45 -19t45 19l256 256q19 19 19 45zM640 0q0 -53 -37.5 -90.5t-90.5 -37.5t-90.5 37.5t-37.5 90.5 t37.5 90.5t90.5 37.5t90.5 -37.5t37.5 -90.5zM1536 0q0 -53 -37.5 -90.5t-90.5 -37.5t-90.5 37.5t-37.5 90.5t37.5 90.5t90.5 37.5t90.5 -37.5t37.5 -90.5zM1664 1088v-512q0 -24 -16 -42.5t-41 -21.5l-1044 -122q1 -7 4.5 -21.5t6 -26.5t2.5 -22q0 -16 -24 -64h920 q26 0 45 -19t19 -45t-19 -45t-45 -19h-1024q-26 0 -45 19t-19 45q0 14 11 39.5t29.5 59.5t20.5 38l-177 823h-204q-26 0 -45 19t-19 45t19 45t45 19h256q16 0 28.5 -6.5t20 -15.5t13 -24.5t7.5 -26.5t5.5 -29.5t4.5 -25.5h1201q26 0 45 -19t19 -45z" />
+<glyph unicode="&#xf219;" horiz-adv-x="2048" d="M212 768l623 -665l-300 665h-323zM1024 -4l349 772h-698zM538 896l204 384h-262l-288 -384h346zM1213 103l623 665h-323zM683 896h682l-204 384h-274zM1510 896h346l-288 384h-262zM1651 1382l384 -512q14 -18 13 -41.5t-17 -40.5l-960 -1024q-18 -20 -47 -20t-47 20 l-960 1024q-16 17 -17 40.5t13 41.5l384 512q18 26 51 26h1152q33 0 51 -26z" />
+<glyph unicode="&#xf21a;" horiz-adv-x="2048" d="M1811 -19q19 19 45 19t45 -19l128 -128l-90 -90l-83 83l-83 -83q-18 -19 -45 -19t-45 19l-83 83l-83 -83q-19 -19 -45 -19t-45 19l-83 83l-83 -83q-19 -19 -45 -19t-45 19l-83 83l-83 -83q-19 -19 -45 -19t-45 19l-83 83l-83 -83q-19 -19 -45 -19t-45 19l-83 83l-83 -83 q-19 -19 -45 -19t-45 19l-83 83l-83 -83q-19 -19 -45 -19t-45 19l-128 128l90 90l83 -83l83 83q19 19 45 19t45 -19l83 -83l83 83q19 19 45 19t45 -19l83 -83l83 83q19 19 45 19t45 -19l83 -83l83 83q19 19 45 19t45 -19l83 -83l83 83q19 19 45 19t45 -19l83 -83l83 83 q19 19 45 19t45 -19l83 -83zM237 19q-19 -19 -45 -19t-45 19l-128 128l90 90l83 -82l83 82q19 19 45 19t45 -19l83 -82l64 64v293l-210 314q-17 26 -7 56.5t40 40.5l177 58v299h128v128h256v128h256v-128h256v-128h128v-299l177 -58q30 -10 40 -40.5t-7 -56.5l-210 -314 v-293l19 18q19 19 45 19t45 -19l83 -82l83 82q19 19 45 19t45 -19l128 -128l-90 -90l-83 83l-83 -83q-18 -19 -45 -19t-45 19l-83 83l-83 -83q-19 -19 -45 -19t-45 19l-83 83l-83 -83q-19 -19 -45 -19t-45 19l-83 83l-83 -83q-19 -19 -45 -19t-45 19l-83 83l-83 -83 q-19 -19 -45 -19t-45 19l-83 83l-83 -83q-19 -19 -45 -19t-45 19l-83 83zM640 1152v-128l384 128l384 -128v128h-128v128h-512v-128h-128z" />
+<glyph unicode="&#xf21b;" d="M576 0l96 448l-96 128l-128 64zM832 0l128 640l-128 -64l-96 -128zM992 1010q-2 4 -4 6q-10 8 -96 8q-70 0 -167 -19q-7 -2 -21 -2t-21 2q-97 19 -167 19q-86 0 -96 -8q-2 -2 -4 -6q2 -18 4 -27q2 -3 7.5 -6.5t7.5 -10.5q2 -4 7.5 -20.5t7 -20.5t7.5 -17t8.5 -17t9 -14 t12 -13.5t14 -9.5t17.5 -8t20.5 -4t24.5 -2q36 0 59 12.5t32.5 30t14.5 34.5t11.5 29.5t17.5 12.5h12q11 0 17.5 -12.5t11.5 -29.5t14.5 -34.5t32.5 -30t59 -12.5q13 0 24.5 2t20.5 4t17.5 8t14 9.5t12 13.5t9 14t8.5 17t7.5 17t7 20.5t7.5 20.5q2 7 7.5 10.5t7.5 6.5 q2 9 4 27zM1408 131q0 -121 -73 -190t-194 -69h-874q-121 0 -194 69t-73 190q0 61 4.5 118t19 125.5t37.5 123.5t63.5 103.5t93.5 74.5l-90 220h214q-22 64 -22 128q0 12 2 32q-194 40 -194 96q0 57 210 99q17 62 51.5 134t70.5 114q32 37 76 37q30 0 84 -31t84 -31t84 31 t84 31q44 0 76 -37q36 -42 70.5 -114t51.5 -134q210 -42 210 -99q0 -56 -194 -96q7 -81 -20 -160h214l-82 -225q63 -33 107.5 -96.5t65.5 -143.5t29 -151.5t8 -148.5z" />
+<glyph unicode="&#xf21c;" horiz-adv-x="2304" d="M2301 500q12 -103 -22 -198.5t-99 -163.5t-158.5 -106t-196.5 -31q-161 11 -279.5 125t-134.5 274q-12 111 27.5 210.5t118.5 170.5l-71 107q-96 -80 -151 -194t-55 -244q0 -27 -18.5 -46.5t-45.5 -19.5h-256h-69q-23 -164 -149 -274t-294 -110q-185 0 -316.5 131.5 t-131.5 316.5t131.5 316.5t316.5 131.5q76 0 152 -27l24 45q-123 110 -304 110h-64q-26 0 -45 19t-19 45t19 45t45 19h128q78 0 145 -13.5t116.5 -38.5t71.5 -39.5t51 -36.5h512h115l-85 128h-222q-30 0 -49 22.5t-14 52.5q4 23 23 38t43 15h253q33 0 53 -28l70 -105 l114 114q19 19 46 19h101q26 0 45 -19t19 -45v-128q0 -26 -19 -45t-45 -19h-179l115 -172q131 63 275 36q143 -26 244 -134.5t118 -253.5zM448 128q115 0 203 72.5t111 183.5h-314q-35 0 -55 31q-18 32 -1 63l147 277q-47 13 -91 13q-132 0 -226 -94t-94 -226t94 -226 t226 -94zM1856 128q132 0 226 94t94 226t-94 226t-226 94q-60 0 -121 -24l174 -260q15 -23 10 -49t-27 -40q-15 -11 -36 -11q-35 0 -53 29l-174 260q-93 -95 -93 -225q0 -132 94 -226t226 -94z" />
+<glyph unicode="&#xf21d;" d="M1408 0q0 -63 -61.5 -113.5t-164 -81t-225 -46t-253.5 -15.5t-253.5 15.5t-225 46t-164 81t-61.5 113.5q0 49 33 88.5t91 66.5t118 44.5t131 29.5q26 5 48 -10.5t26 -41.5q5 -26 -10.5 -48t-41.5 -26q-58 -10 -106 -23.5t-76.5 -25.5t-48.5 -23.5t-27.5 -19.5t-8.5 -12 q3 -11 27 -26.5t73 -33t114 -32.5t160.5 -25t201.5 -10t201.5 10t160.5 25t114 33t73 33.5t27 27.5q-1 4 -8.5 11t-27.5 19t-48.5 23.5t-76.5 25t-106 23.5q-26 4 -41.5 26t-10.5 48q4 26 26 41.5t48 10.5q71 -12 131 -29.5t118 -44.5t91 -66.5t33 -88.5zM1024 896v-384 q0 -26 -19 -45t-45 -19h-64v-384q0 -26 -19 -45t-45 -19h-256q-26 0 -45 19t-19 45v384h-64q-26 0 -45 19t-19 45v384q0 53 37.5 90.5t90.5 37.5h384q53 0 90.5 -37.5t37.5 -90.5zM928 1280q0 -93 -65.5 -158.5t-158.5 -65.5t-158.5 65.5t-65.5 158.5t65.5 158.5t158.5 65.5 t158.5 -65.5t65.5 -158.5z" />
+<glyph unicode="&#xf21e;" horiz-adv-x="1792" d="M1280 512h305q-5 -6 -10 -10.5t-9 -7.5l-3 -4l-623 -600q-18 -18 -44 -18t-44 18l-624 602q-5 2 -21 20h369q22 0 39.5 13.5t22.5 34.5l70 281l190 -667q6 -20 23 -33t39 -13q21 0 38 13t23 33l146 485l56 -112q18 -35 57 -35zM1792 940q0 -145 -103 -300h-369l-111 221 q-8 17 -25.5 27t-36.5 8q-45 -5 -56 -46l-129 -430l-196 686q-6 20 -23.5 33t-39.5 13t-39 -13.5t-22 -34.5l-116 -464h-423q-103 155 -103 300q0 220 127 344t351 124q62 0 126.5 -21.5t120 -58t95.5 -68.5t76 -68q36 36 76 68t95.5 68.5t120 58t126.5 21.5q224 0 351 -124 t127 -344z" />
+<glyph unicode="&#xf221;" horiz-adv-x="1280" d="M1152 960q0 -221 -147.5 -384.5t-364.5 -187.5v-260h224q14 0 23 -9t9 -23v-64q0 -14 -9 -23t-23 -9h-224v-224q0 -14 -9 -23t-23 -9h-64q-14 0 -23 9t-9 23v224h-224q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h224v260q-150 16 -271.5 103t-186 224t-52.5 292 q11 134 80.5 249t182 188t245.5 88q170 19 319 -54t236 -212t87 -306zM128 960q0 -185 131.5 -316.5t316.5 -131.5t316.5 131.5t131.5 316.5t-131.5 316.5t-316.5 131.5t-316.5 -131.5t-131.5 -316.5z" />
+<glyph unicode="&#xf222;" d="M1472 1408q26 0 45 -19t19 -45v-416q0 -14 -9 -23t-23 -9h-64q-14 0 -23 9t-9 23v262l-382 -383q126 -156 126 -359q0 -117 -45.5 -223.5t-123 -184t-184 -123t-223.5 -45.5t-223.5 45.5t-184 123t-123 184t-45.5 223.5t45.5 223.5t123 184t184 123t223.5 45.5 q203 0 359 -126l382 382h-261q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h416zM576 0q185 0 316.5 131.5t131.5 316.5t-131.5 316.5t-316.5 131.5t-316.5 -131.5t-131.5 -316.5t131.5 -316.5t316.5 -131.5z" />
+<glyph unicode="&#xf223;" horiz-adv-x="1280" d="M830 1220q145 -72 233.5 -210.5t88.5 -305.5q0 -221 -147.5 -384.5t-364.5 -187.5v-132h96q14 0 23 -9t9 -23v-64q0 -14 -9 -23t-23 -9h-96v-96q0 -14 -9 -23t-23 -9h-64q-14 0 -23 9t-9 23v96h-96q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h96v132q-217 24 -364.5 187.5 t-147.5 384.5q0 167 88.5 305.5t233.5 210.5q-165 96 -228 273q-6 16 3.5 29.5t26.5 13.5h69q21 0 29 -20q44 -106 140 -171t214 -65t214 65t140 171q8 20 37 20h61q17 0 26.5 -13.5t3.5 -29.5q-63 -177 -228 -273zM576 256q185 0 316.5 131.5t131.5 316.5t-131.5 316.5 t-316.5 131.5t-316.5 -131.5t-131.5 -316.5t131.5 -316.5t316.5 -131.5z" />
+<glyph unicode="&#xf224;" d="M1024 1504q0 14 9 23t23 9h288q26 0 45 -19t19 -45v-288q0 -14 -9 -23t-23 -9h-64q-14 0 -23 9t-9 23v134l-254 -255q126 -158 126 -359q0 -221 -147.5 -384.5t-364.5 -187.5v-132h96q14 0 23 -9t9 -23v-64q0 -14 -9 -23t-23 -9h-96v-96q0 -14 -9 -23t-23 -9h-64 q-14 0 -23 9t-9 23v96h-96q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h96v132q-149 16 -270.5 103t-186.5 223.5t-53 291.5q16 204 160 353.5t347 172.5q118 14 228 -19t198 -103l255 254h-134q-14 0 -23 9t-9 23v64zM576 256q185 0 316.5 131.5t131.5 316.5t-131.5 316.5 t-316.5 131.5t-316.5 -131.5t-131.5 -316.5t131.5 -316.5t316.5 -131.5z" />
+<glyph unicode="&#xf225;" horiz-adv-x="1792" d="M1280 1504q0 14 9 23t23 9h288q26 0 45 -19t19 -45v-288q0 -14 -9 -23t-23 -9h-64q-14 0 -23 9t-9 23v134l-254 -255q126 -158 126 -359q0 -221 -147.5 -384.5t-364.5 -187.5v-132h96q14 0 23 -9t9 -23v-64q0 -14 -9 -23t-23 -9h-96v-96q0 -14 -9 -23t-23 -9h-64 q-14 0 -23 9t-9 23v96h-96q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h96v132q-217 24 -364.5 187.5t-147.5 384.5q0 201 126 359l-52 53l-101 -111q-9 -10 -22 -10.5t-23 7.5l-48 44q-10 8 -10.5 21.5t8.5 23.5l105 115l-111 112v-134q0 -14 -9 -23t-23 -9h-64q-14 0 -23 9 t-9 23v288q0 26 19 45t45 19h288q14 0 23 -9t9 -23v-64q0 -14 -9 -23t-23 -9h-133l106 -107l86 94q9 10 22 10.5t23 -7.5l48 -44q10 -8 10.5 -21.5t-8.5 -23.5l-90 -99l57 -56q158 126 359 126t359 -126l255 254h-134q-14 0 -23 9t-9 23v64zM832 256q185 0 316.5 131.5 t131.5 316.5t-131.5 316.5t-316.5 131.5t-316.5 -131.5t-131.5 -316.5t131.5 -316.5t316.5 -131.5z" />
+<glyph unicode="&#xf226;" horiz-adv-x="1792" d="M1790 1007q12 -155 -52.5 -292t-186 -224t-271.5 -103v-260h224q14 0 23 -9t9 -23v-64q0 -14 -9 -23t-23 -9h-224v-224q0 -14 -9 -23t-23 -9h-64q-14 0 -23 9t-9 23v224h-512v-224q0 -14 -9 -23t-23 -9h-64q-14 0 -23 9t-9 23v224h-224q-14 0 -23 9t-9 23v64q0 14 9 23 t23 9h224v260q-150 16 -271.5 103t-186 224t-52.5 292q17 206 164.5 356.5t352.5 169.5q206 21 377 -94q171 115 377 94q205 -19 352.5 -169.5t164.5 -356.5zM896 647q128 131 128 313t-128 313q-128 -131 -128 -313t128 -313zM576 512q115 0 218 57q-154 165 -154 391 q0 224 154 391q-103 57 -218 57q-185 0 -316.5 -131.5t-131.5 -316.5t131.5 -316.5t316.5 -131.5zM1152 128v260q-137 15 -256 94q-119 -79 -256 -94v-260h512zM1216 512q185 0 316.5 131.5t131.5 316.5t-131.5 316.5t-316.5 131.5q-115 0 -218 -57q154 -167 154 -391 q0 -226 -154 -391q103 -57 218 -57z" />
+<glyph unicode="&#xf227;" horiz-adv-x="1920" d="M1536 1120q0 14 9 23t23 9h288q26 0 45 -19t19 -45v-288q0 -14 -9 -23t-23 -9h-64q-14 0 -23 9t-9 23v134l-254 -255q76 -95 107.5 -214t9.5 -247q-31 -182 -166 -312t-318 -156q-210 -29 -384.5 80t-241.5 300q-117 6 -221 57.5t-177.5 133t-113.5 192.5t-32 230 q9 135 78 252t182 191.5t248 89.5q118 14 227.5 -19t198.5 -103l255 254h-134q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h288q26 0 45 -19t19 -45v-288q0 -14 -9 -23t-23 -9h-64q-14 0 -23 9t-9 23v134l-254 -255q59 -74 93 -169q182 -9 328 -124l255 254h-134q-14 0 -23 9 t-9 23v64zM1024 704q0 20 -4 58q-162 -25 -271 -150t-109 -292q0 -20 4 -58q162 25 271 150t109 292zM128 704q0 -168 111 -294t276 -149q-3 29 -3 59q0 210 135 369.5t338 196.5q-53 120 -163.5 193t-245.5 73q-185 0 -316.5 -131.5t-131.5 -316.5zM1088 -128 q185 0 316.5 131.5t131.5 316.5q0 168 -111 294t-276 149q3 -29 3 -59q0 -210 -135 -369.5t-338 -196.5q53 -120 163.5 -193t245.5 -73z" />
+<glyph unicode="&#xf228;" horiz-adv-x="2048" d="M1664 1504q0 14 9 23t23 9h288q26 0 45 -19t19 -45v-288q0 -14 -9 -23t-23 -9h-64q-14 0 -23 9t-9 23v134l-254 -255q76 -95 107.5 -214t9.5 -247q-32 -180 -164.5 -310t-313.5 -157q-223 -34 -409 90q-117 -78 -256 -93v-132h96q14 0 23 -9t9 -23v-64q0 -14 -9 -23 t-23 -9h-96v-96q0 -14 -9 -23t-23 -9h-64q-14 0 -23 9t-9 23v96h-96q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h96v132q-155 17 -279.5 109.5t-187 237.5t-39.5 307q25 187 159.5 322.5t320.5 164.5q224 34 410 -90q146 97 320 97q201 0 359 -126l255 254h-134q-14 0 -23 9 t-9 23v64zM896 391q128 131 128 313t-128 313q-128 -131 -128 -313t128 -313zM128 704q0 -185 131.5 -316.5t316.5 -131.5q117 0 218 57q-154 167 -154 391t154 391q-101 57 -218 57q-185 0 -316.5 -131.5t-131.5 -316.5zM1216 256q185 0 316.5 131.5t131.5 316.5 t-131.5 316.5t-316.5 131.5q-117 0 -218 -57q154 -167 154 -391t-154 -391q101 -57 218 -57z" />
+<glyph unicode="&#xf229;" d="M1472 1408q26 0 45 -19t19 -45v-416q0 -14 -9 -23t-23 -9h-64q-14 0 -23 9t-9 23v262l-213 -214l140 -140q9 -10 9 -23t-9 -22l-46 -46q-9 -9 -22 -9t-23 9l-140 141l-78 -79q126 -156 126 -359q0 -117 -45.5 -223.5t-123 -184t-184 -123t-223.5 -45.5t-223.5 45.5 t-184 123t-123 184t-45.5 223.5t45.5 223.5t123 184t184 123t223.5 45.5q203 0 359 -126l78 78l-172 172q-9 10 -9 23t9 22l46 46q9 9 22 9t23 -9l172 -172l213 213h-261q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h416zM576 0q185 0 316.5 131.5t131.5 316.5t-131.5 316.5 t-316.5 131.5t-316.5 -131.5t-131.5 -316.5t131.5 -316.5t316.5 -131.5z" />
+<glyph unicode="&#xf22a;" horiz-adv-x="1280" d="M640 892q217 -24 364.5 -187.5t147.5 -384.5q0 -167 -87 -306t-236 -212t-319 -54q-133 15 -245.5 88t-182 188t-80.5 249q-12 155 52.5 292t186 224t271.5 103v132h-160q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h160v165l-92 -92q-10 -9 -23 -9t-22 9l-46 46q-9 9 -9 22 t9 23l202 201q19 19 45 19t45 -19l202 -201q9 -10 9 -23t-9 -22l-46 -46q-9 -9 -22 -9t-23 9l-92 92v-165h160q14 0 23 -9t9 -23v-64q0 -14 -9 -23t-23 -9h-160v-132zM576 -128q185 0 316.5 131.5t131.5 316.5t-131.5 316.5t-316.5 131.5t-316.5 -131.5t-131.5 -316.5 t131.5 -316.5t316.5 -131.5z" />
+<glyph unicode="&#xf22b;" horiz-adv-x="2048" d="M1901 621q19 -19 19 -45t-19 -45l-294 -294q-9 -10 -22.5 -10t-22.5 10l-45 45q-10 9 -10 22.5t10 22.5l185 185h-294v-224q0 -14 -9 -23t-23 -9h-64q-14 0 -23 9t-9 23v224h-132q-24 -217 -187.5 -364.5t-384.5 -147.5q-167 0 -306 87t-212 236t-54 319q15 133 88 245.5 t188 182t249 80.5q155 12 292 -52.5t224 -186t103 -271.5h132v224q0 14 9 23t23 9h64q14 0 23 -9t9 -23v-224h294l-185 185q-10 9 -10 22.5t10 22.5l45 45q9 10 22.5 10t22.5 -10zM576 128q185 0 316.5 131.5t131.5 316.5t-131.5 316.5t-316.5 131.5t-316.5 -131.5 t-131.5 -316.5t131.5 -316.5t316.5 -131.5z" />
+<glyph unicode="&#xf22c;" horiz-adv-x="1280" d="M1152 960q0 -221 -147.5 -384.5t-364.5 -187.5v-612q0 -14 -9 -23t-23 -9h-64q-14 0 -23 9t-9 23v612q-217 24 -364.5 187.5t-147.5 384.5q0 117 45.5 223.5t123 184t184 123t223.5 45.5t223.5 -45.5t184 -123t123 -184t45.5 -223.5zM576 512q185 0 316.5 131.5 t131.5 316.5t-131.5 316.5t-316.5 131.5t-316.5 -131.5t-131.5 -316.5t131.5 -316.5t316.5 -131.5z" />
+<glyph unicode="&#xf22d;" horiz-adv-x="1280" d="M1024 576q0 185 -131.5 316.5t-316.5 131.5t-316.5 -131.5t-131.5 -316.5t131.5 -316.5t316.5 -131.5t316.5 131.5t131.5 316.5zM1152 576q0 -117 -45.5 -223.5t-123 -184t-184 -123t-223.5 -45.5t-223.5 45.5t-184 123t-123 184t-45.5 223.5t45.5 223.5t123 184t184 123 t223.5 45.5t223.5 -45.5t184 -123t123 -184t45.5 -223.5z" />
+<glyph unicode="&#xf22e;" horiz-adv-x="1792" />
+<glyph unicode="&#xf22f;" horiz-adv-x="1792" />
+<glyph unicode="&#xf230;" d="M1451 1408q35 0 60 -25t25 -60v-1366q0 -35 -25 -60t-60 -25h-391v595h199l30 232h-229v148q0 56 23.5 84t91.5 28l122 1v207q-63 9 -178 9q-136 0 -217.5 -80t-81.5 -226v-171h-200v-232h200v-595h-735q-35 0 -60 25t-25 60v1366q0 35 25 60t60 25h1366z" />
+<glyph unicode="&#xf231;" horiz-adv-x="1280" d="M0 939q0 108 37.5 203.5t103.5 166.5t152 123t185 78t202 26q158 0 294 -66.5t221 -193.5t85 -287q0 -96 -19 -188t-60 -177t-100 -149.5t-145 -103t-189 -38.5q-68 0 -135 32t-96 88q-10 -39 -28 -112.5t-23.5 -95t-20.5 -71t-26 -71t-32 -62.5t-46 -77.5t-62 -86.5 l-14 -5l-9 10q-15 157 -15 188q0 92 21.5 206.5t66.5 287.5t52 203q-32 65 -32 169q0 83 52 156t132 73q61 0 95 -40.5t34 -102.5q0 -66 -44 -191t-44 -187q0 -63 45 -104.5t109 -41.5q55 0 102 25t78.5 68t56 95t38 110.5t20 111t6.5 99.5q0 173 -109.5 269.5t-285.5 96.5 q-200 0 -334 -129.5t-134 -328.5q0 -44 12.5 -85t27 -65t27 -45.5t12.5 -30.5q0 -28 -15 -73t-37 -45q-2 0 -17 3q-51 15 -90.5 56t-61 94.5t-32.5 108t-11 106.5z" />
+<glyph unicode="&#xf232;" d="M985 562q13 0 97.5 -44t89.5 -53q2 -5 2 -15q0 -33 -17 -76q-16 -39 -71 -65.5t-102 -26.5q-57 0 -190 62q-98 45 -170 118t-148 185q-72 107 -71 194v8q3 91 74 158q24 22 52 22q6 0 18 -1.5t19 -1.5q19 0 26.5 -6.5t15.5 -27.5q8 -20 33 -88t25 -75q0 -21 -34.5 -57.5 t-34.5 -46.5q0 -7 5 -15q34 -73 102 -137q56 -53 151 -101q12 -7 22 -7q15 0 54 48.5t52 48.5zM782 32q127 0 243.5 50t200.5 134t134 200.5t50 243.5t-50 243.5t-134 200.5t-200.5 134t-243.5 50t-243.5 -50t-200.5 -134t-134 -200.5t-50 -243.5q0 -203 120 -368l-79 -233 l242 77q158 -104 345 -104zM782 1414q153 0 292.5 -60t240.5 -161t161 -240.5t60 -292.5t-60 -292.5t-161 -240.5t-240.5 -161t-292.5 -60q-195 0 -365 94l-417 -134l136 405q-108 178 -108 389q0 153 60 292.5t161 240.5t240.5 161t292.5 60z" />
+<glyph unicode="&#xf233;" horiz-adv-x="1792" d="M128 128h1024v128h-1024v-128zM128 640h1024v128h-1024v-128zM1696 192q0 40 -28 68t-68 28t-68 -28t-28 -68t28 -68t68 -28t68 28t28 68zM128 1152h1024v128h-1024v-128zM1696 704q0 40 -28 68t-68 28t-68 -28t-28 -68t28 -68t68 -28t68 28t28 68zM1696 1216 q0 40 -28 68t-68 28t-68 -28t-28 -68t28 -68t68 -28t68 28t28 68zM1792 384v-384h-1792v384h1792zM1792 896v-384h-1792v384h1792zM1792 1408v-384h-1792v384h1792z" />
+<glyph unicode="&#xf234;" horiz-adv-x="2048" d="M704 640q-159 0 -271.5 112.5t-112.5 271.5t112.5 271.5t271.5 112.5t271.5 -112.5t112.5 -271.5t-112.5 -271.5t-271.5 -112.5zM1664 512h352q13 0 22.5 -9.5t9.5 -22.5v-192q0 -13 -9.5 -22.5t-22.5 -9.5h-352v-352q0 -13 -9.5 -22.5t-22.5 -9.5h-192q-13 0 -22.5 9.5 t-9.5 22.5v352h-352q-13 0 -22.5 9.5t-9.5 22.5v192q0 13 9.5 22.5t22.5 9.5h352v352q0 13 9.5 22.5t22.5 9.5h192q13 0 22.5 -9.5t9.5 -22.5v-352zM928 288q0 -52 38 -90t90 -38h256v-238q-68 -50 -171 -50h-874q-121 0 -194 69t-73 190q0 53 3.5 103.5t14 109t26.5 108.5 t43 97.5t62 81t85.5 53.5t111.5 20q19 0 39 -17q79 -61 154.5 -91.5t164.5 -30.5t164.5 30.5t154.5 91.5q20 17 39 17q132 0 217 -96h-223q-52 0 -90 -38t-38 -90v-192z" />
+<glyph unicode="&#xf235;" horiz-adv-x="2048" d="M704 640q-159 0 -271.5 112.5t-112.5 271.5t112.5 271.5t271.5 112.5t271.5 -112.5t112.5 -271.5t-112.5 -271.5t-271.5 -112.5zM1781 320l249 -249q9 -9 9 -23q0 -13 -9 -22l-136 -136q-9 -9 -22 -9q-14 0 -23 9l-249 249l-249 -249q-9 -9 -23 -9q-13 0 -22 9l-136 136 q-9 9 -9 22q0 14 9 23l249 249l-249 249q-9 9 -9 23q0 13 9 22l136 136q9 9 22 9q14 0 23 -9l249 -249l249 249q9 9 23 9q13 0 22 -9l136 -136q9 -9 9 -22q0 -14 -9 -23zM1283 320l-181 -181q-37 -37 -37 -91q0 -53 37 -90l83 -83q-21 -3 -44 -3h-874q-121 0 -194 69 t-73 190q0 53 3.5 103.5t14 109t26.5 108.5t43 97.5t62 81t85.5 53.5t111.5 20q19 0 39 -17q154 -122 319 -122t319 122q20 17 39 17q28 0 57 -6q-28 -27 -41 -50t-13 -56q0 -54 37 -91z" />
+<glyph unicode="&#xf236;" horiz-adv-x="2048" d="M256 512h1728q26 0 45 -19t19 -45v-448h-256v256h-1536v-256h-256v1216q0 26 19 45t45 19h128q26 0 45 -19t19 -45v-704zM832 832q0 106 -75 181t-181 75t-181 -75t-75 -181t75 -181t181 -75t181 75t75 181zM2048 576v64q0 159 -112.5 271.5t-271.5 112.5h-704 q-26 0 -45 -19t-19 -45v-384h1152z" />
+<glyph unicode="&#xf237;" d="M1536 1536l-192 -448h192v-192h-274l-55 -128h329v-192h-411l-357 -832l-357 832h-411v192h329l-55 128h-274v192h192l-192 448h256l323 -768h378l323 768h256zM768 320l108 256h-216z" />
+<glyph unicode="&#xf238;" d="M1088 1536q185 0 316.5 -93.5t131.5 -226.5v-896q0 -130 -125.5 -222t-305.5 -97l213 -202q16 -15 8 -35t-30 -20h-1056q-22 0 -30 20t8 35l213 202q-180 5 -305.5 97t-125.5 222v896q0 133 131.5 226.5t316.5 93.5h640zM768 192q80 0 136 56t56 136t-56 136t-136 56 t-136 -56t-56 -136t56 -136t136 -56zM1344 768v512h-1152v-512h1152z" />
+<glyph unicode="&#xf239;" d="M1088 1536q185 0 316.5 -93.5t131.5 -226.5v-896q0 -130 -125.5 -222t-305.5 -97l213 -202q16 -15 8 -35t-30 -20h-1056q-22 0 -30 20t8 35l213 202q-180 5 -305.5 97t-125.5 222v896q0 133 131.5 226.5t316.5 93.5h640zM288 224q66 0 113 47t47 113t-47 113t-113 47 t-113 -47t-47 -113t47 -113t113 -47zM704 768v512h-544v-512h544zM1248 224q66 0 113 47t47 113t-47 113t-113 47t-113 -47t-47 -113t47 -113t113 -47zM1408 768v512h-576v-512h576z" />
+<glyph unicode="&#xf23a;" horiz-adv-x="1792" d="M597 1115v-1173q0 -25 -12.5 -42.5t-36.5 -17.5q-17 0 -33 8l-465 233q-21 10 -35.5 33.5t-14.5 46.5v1140q0 20 10 34t29 14q14 0 44 -15l511 -256q3 -3 3 -5zM661 1014l534 -866l-534 266v600zM1792 996v-1054q0 -25 -14 -40.5t-38 -15.5t-47 13l-441 220zM1789 1116 q0 -3 -256.5 -419.5t-300.5 -487.5l-390 634l324 527q17 28 52 28q14 0 26 -6l541 -270q4 -2 4 -6z" />
+<glyph unicode="&#xf23b;" d="M809 532l266 499h-112l-157 -312q-24 -48 -44 -92l-42 92l-155 312h-120l263 -493v-324h101v318zM1536 1408v-1536h-1536v1536h1536z" />
+<glyph unicode="&#xf23c;" horiz-adv-x="2296" d="M478 -139q-8 -16 -27 -34.5t-37 -25.5q-25 -9 -51.5 3.5t-28.5 31.5q-1 22 40 55t68 38q23 4 34 -21.5t2 -46.5zM1819 -139q7 -16 26 -34.5t38 -25.5q25 -9 51.5 3.5t27.5 31.5q2 22 -39.5 55t-68.5 38q-22 4 -33 -21.5t-2 -46.5zM1867 -30q13 -27 56.5 -59.5t77.5 -41.5 q45 -13 82 4.5t37 50.5q0 46 -67.5 100.5t-115.5 59.5q-40 5 -63.5 -37.5t-6.5 -76.5zM428 -30q-13 -27 -56 -59.5t-77 -41.5q-45 -13 -82 4.5t-37 50.5q0 46 67.5 100.5t115.5 59.5q40 5 63 -37.5t6 -76.5zM1158 1094h1q-41 0 -76 -15q27 -8 44 -30.5t17 -49.5 q0 -35 -27 -60t-65 -25q-52 0 -80 43q-5 -23 -5 -42q0 -74 56 -126.5t135 -52.5q80 0 136 52.5t56 126.5t-56 126.5t-136 52.5zM1462 1312q-99 109 -220.5 131.5t-245.5 -44.5q27 60 82.5 96.5t118 39.5t121.5 -17t99.5 -74.5t44.5 -131.5zM2212 73q8 -11 -11 -42 q7 -23 7 -40q1 -56 -44.5 -112.5t-109.5 -91.5t-118 -37q-48 -2 -92 21.5t-66 65.5q-687 -25 -1259 0q-23 -41 -66.5 -65t-92.5 -22q-86 3 -179.5 80.5t-92.5 160.5q2 22 7 40q-19 31 -11 42q6 10 31 1q14 22 41 51q-7 29 2 38q11 10 39 -4q29 20 59 34q0 29 13 37 q23 12 51 -16q35 5 61 -2q18 -4 38 -19v73q-11 0 -18 2q-53 10 -97 44.5t-55 87.5q-9 38 0 81q15 62 93 95q2 17 19 35.5t36 23.5t33 -7.5t19 -30.5h13q46 -5 60 -23q3 -3 5 -7q10 1 30.5 3.5t30.5 3.5q-15 11 -30 17q-23 40 -91 43q0 6 1 10q-62 2 -118.5 18.5t-84.5 47.5 q-32 36 -42.5 92t-2.5 112q16 126 90 179q23 16 52 4.5t32 -40.5q0 -1 1.5 -14t2.5 -21t3 -20t5.5 -19t8.5 -10q27 -14 76 -12q48 46 98 74q-40 4 -162 -14l47 46q61 58 163 111q145 73 282 86q-20 8 -41 15.5t-47 14t-42.5 10.5t-47.5 11t-43 10q595 126 904 -139 q98 -84 158 -222q85 -10 121 9h1q5 3 8.5 10t5.5 19t3 19.5t3 21.5l1 14q3 28 32 40t52 -5q73 -52 91 -178q7 -57 -3.5 -113t-42.5 -91q-28 -32 -83.5 -48.5t-115.5 -18.5v-10q-71 -2 -95 -43q-14 -5 -31 -17q11 -1 32 -3.5t30 -3.5q1 4 5 8q16 18 60 23h13q5 18 19 30t33 8 t36 -23t19 -36q79 -32 93 -95q9 -40 1 -81q-12 -53 -56 -88t-97 -44q-10 -2 -17 -2q0 -49 -1 -73q20 15 38 19q26 7 61 2q28 28 51 16q14 -9 14 -37q33 -16 59 -34q27 13 38 4q10 -10 2 -38q28 -30 41 -51q23 8 31 -1zM1937 1025q0 -29 -9 -54q82 -32 112 -132 q4 37 -9.5 98.5t-41.5 90.5q-20 19 -36 17t-16 -20zM1859 925q35 -42 47.5 -108.5t-0.5 -124.5q67 13 97 45q13 14 18 28q-3 64 -31 114.5t-79 66.5q-15 -15 -52 -21zM1822 921q-30 0 -44 1q42 -115 53 -239q21 0 43 3q16 68 1 135t-53 100zM258 839q30 100 112 132 q-9 25 -9 54q0 18 -16.5 20t-35.5 -17q-28 -29 -41.5 -90.5t-9.5 -98.5zM294 737q29 -31 97 -45q-13 58 -0.5 124.5t47.5 108.5v0q-37 6 -52 21q-51 -16 -78.5 -66t-31.5 -115q9 -17 18 -28zM471 683q14 124 73 235q-19 -4 -55 -18l-45 -19v1q-46 -89 -20 -196q25 -3 47 -3z M1434 644q8 -38 16.5 -108.5t11.5 -89.5q3 -18 9.5 -21.5t23.5 4.5q40 20 62 85.5t23 125.5q-24 2 -146 4zM1152 1285q-116 0 -199 -82.5t-83 -198.5q0 -117 83 -199.5t199 -82.5t199 82.5t83 199.5q0 116 -83 198.5t-199 82.5zM1380 646q-106 2 -211 0v1q-1 -27 2.5 -86 t13.5 -66q29 -14 93.5 -14.5t95.5 10.5q9 3 11 39t-0.5 69.5t-4.5 46.5zM1112 447q8 4 9.5 48t-0.5 88t-4 63v1q-212 -3 -214 -3q-4 -20 -7 -62t0 -83t14 -46q34 -15 101 -16t101 10zM718 636q-16 -59 4.5 -118.5t77.5 -84.5q15 -8 24 -5t12 21q3 16 8 90t10 103 q-69 -2 -136 -6zM591 510q3 -23 -34 -36q132 -141 271.5 -240t305.5 -154q172 49 310.5 146t293.5 250q-33 13 -30 34l3 9v1v-1q-17 2 -50 5.5t-48 4.5q-26 -90 -82 -132q-51 -38 -82 1q-5 6 -9 14q-7 13 -17 62q-2 -5 -5 -9t-7.5 -7t-8 -5.5t-9.5 -4l-10 -2.5t-12 -2 l-12 -1.5t-13.5 -1t-13.5 -0.5q-106 -9 -163 11q-4 -17 -10 -26.5t-21 -15t-23 -7t-36 -3.5q-2 0 -3 -0.5t-3 -0.5h-3q-179 -17 -203 40q-2 -63 -56 -54q-47 8 -91 54q-12 13 -20 26q-17 29 -26 65q-58 -6 -87 -10q1 -2 4 -10zM507 -118q3 14 3 30q-17 71 -51 130t-73 70 q-41 12 -101.5 -14.5t-104.5 -80t-39 -107.5q35 -53 100 -93t119 -42q51 -2 94 28t53 79zM510 53q23 -63 27 -119q195 113 392 174q-98 52 -180.5 120t-179.5 165q-6 -4 -29 -13q0 -2 -1 -5t-1 -4q31 -18 22 -37q-12 -23 -56 -34q-10 -13 -29 -24h-1q-2 -83 1 -150 q19 -34 35 -73zM579 -113q532 -21 1145 0q-254 147 -428 196q-76 -35 -156 -57q-8 -3 -16 0q-65 21 -129 49q-208 -60 -416 -188h-1v-1q1 0 1 1zM1763 -67q4 54 28 120q14 38 33 71l-1 -1q3 77 3 153q-15 8 -30 25q-42 9 -56 33q-9 20 22 38q-2 4 -2 9q-16 4 -28 12 q-204 -190 -383 -284q198 -59 414 -176zM2155 -90q5 54 -39 107.5t-104 80t-102 14.5q-38 -11 -72.5 -70.5t-51.5 -129.5q0 -16 3 -30q10 -49 53 -79t94 -28q54 2 119 42t100 93z" />
+<glyph unicode="&#xf23d;" horiz-adv-x="2304" d="M1524 -25q0 -68 -48 -116t-116 -48t-116.5 48t-48.5 116t48.5 116.5t116.5 48.5t116 -48.5t48 -116.5zM775 -25q0 -68 -48.5 -116t-116.5 -48t-116 48t-48 116t48 116.5t116 48.5t116.5 -48.5t48.5 -116.5zM0 1469q57 -60 110.5 -104.5t121 -82t136 -63t166 -45.5 t200 -31.5t250 -18.5t304 -9.5t372.5 -2.5q139 0 244.5 -5t181 -16.5t124 -27.5t71 -39.5t24 -51.5t-19.5 -64t-56.5 -76.5t-89.5 -91t-116 -104.5t-139 -119q-185 -157 -286 -247q29 51 76.5 109t94 105.5t94.5 98.5t83 91.5t54 80.5t13 70t-45.5 55.5t-116.5 41t-204 23.5 t-304 5q-168 -2 -314 6t-256 23t-204.5 41t-159.5 51.5t-122.5 62.5t-91.5 66.5t-68 71.5t-50.5 69.5t-40 68t-36.5 59.5z" />
+<glyph unicode="&#xf23e;" horiz-adv-x="1792" d="M896 1472q-169 0 -323 -66t-265.5 -177.5t-177.5 -265.5t-66 -323t66 -323t177.5 -265.5t265.5 -177.5t323 -66t323 66t265.5 177.5t177.5 265.5t66 323t-66 323t-177.5 265.5t-265.5 177.5t-323 66zM896 1536q182 0 348 -71t286 -191t191 -286t71 -348t-71 -348 t-191 -286t-286 -191t-348 -71t-348 71t-286 191t-191 286t-71 348t71 348t191 286t286 191t348 71zM496 704q16 0 16 -16v-480q0 -16 -16 -16h-32q-16 0 -16 16v480q0 16 16 16h32zM896 640q53 0 90.5 -37.5t37.5 -90.5q0 -35 -17.5 -64t-46.5 -46v-114q0 -14 -9 -23 t-23 -9h-64q-14 0 -23 9t-9 23v114q-29 17 -46.5 46t-17.5 64q0 53 37.5 90.5t90.5 37.5zM896 1408q209 0 385.5 -103t279.5 -279.5t103 -385.5t-103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103zM544 928v-96 q0 -14 9 -23t23 -9h64q14 0 23 9t9 23v96q0 93 65.5 158.5t158.5 65.5t158.5 -65.5t65.5 -158.5v-96q0 -14 9 -23t23 -9h64q14 0 23 9t9 23v96q0 146 -103 249t-249 103t-249 -103t-103 -249zM1408 192v512q0 26 -19 45t-45 19h-896q-26 0 -45 -19t-19 -45v-512 q0 -26 19 -45t45 -19h896q26 0 45 19t19 45z" />
+<glyph unicode="&#xf240;" horiz-adv-x="2304" d="M1920 1024v-768h-1664v768h1664zM2048 448h128v384h-128v288q0 14 -9 23t-23 9h-1856q-14 0 -23 -9t-9 -23v-960q0 -14 9 -23t23 -9h1856q14 0 23 9t9 23v288zM2304 832v-384q0 -53 -37.5 -90.5t-90.5 -37.5v-160q0 -66 -47 -113t-113 -47h-1856q-66 0 -113 47t-47 113 v960q0 66 47 113t113 47h1856q66 0 113 -47t47 -113v-160q53 0 90.5 -37.5t37.5 -90.5z" />
+<glyph unicode="&#xf241;" horiz-adv-x="2304" d="M256 256v768h1280v-768h-1280zM2176 960q53 0 90.5 -37.5t37.5 -90.5v-384q0 -53 -37.5 -90.5t-90.5 -37.5v-160q0 -66 -47 -113t-113 -47h-1856q-66 0 -113 47t-47 113v960q0 66 47 113t113 47h1856q66 0 113 -47t47 -113v-160zM2176 448v384h-128v288q0 14 -9 23t-23 9 h-1856q-14 0 -23 -9t-9 -23v-960q0 -14 9 -23t23 -9h1856q14 0 23 9t9 23v288h128z" />
+<glyph unicode="&#xf242;" horiz-adv-x="2304" d="M256 256v768h896v-768h-896zM2176 960q53 0 90.5 -37.5t37.5 -90.5v-384q0 -53 -37.5 -90.5t-90.5 -37.5v-160q0 -66 -47 -113t-113 -47h-1856q-66 0 -113 47t-47 113v960q0 66 47 113t113 47h1856q66 0 113 -47t47 -113v-160zM2176 448v384h-128v288q0 14 -9 23t-23 9 h-1856q-14 0 -23 -9t-9 -23v-960q0 -14 9 -23t23 -9h1856q14 0 23 9t9 23v288h128z" />
+<glyph unicode="&#xf243;" horiz-adv-x="2304" d="M256 256v768h512v-768h-512zM2176 960q53 0 90.5 -37.5t37.5 -90.5v-384q0 -53 -37.5 -90.5t-90.5 -37.5v-160q0 -66 -47 -113t-113 -47h-1856q-66 0 -113 47t-47 113v960q0 66 47 113t113 47h1856q66 0 113 -47t47 -113v-160zM2176 448v384h-128v288q0 14 -9 23t-23 9 h-1856q-14 0 -23 -9t-9 -23v-960q0 -14 9 -23t23 -9h1856q14 0 23 9t9 23v288h128z" />
+<glyph unicode="&#xf244;" horiz-adv-x="2304" d="M2176 960q53 0 90.5 -37.5t37.5 -90.5v-384q0 -53 -37.5 -90.5t-90.5 -37.5v-160q0 -66 -47 -113t-113 -47h-1856q-66 0 -113 47t-47 113v960q0 66 47 113t113 47h1856q66 0 113 -47t47 -113v-160zM2176 448v384h-128v288q0 14 -9 23t-23 9h-1856q-14 0 -23 -9t-9 -23 v-960q0 -14 9 -23t23 -9h1856q14 0 23 9t9 23v288h128z" />
+<glyph unicode="&#xf245;" horiz-adv-x="1280" d="M1133 493q31 -30 14 -69q-17 -40 -59 -40h-382l201 -476q10 -25 0 -49t-34 -35l-177 -75q-25 -10 -49 0t-35 34l-191 452l-312 -312q-19 -19 -45 -19q-12 0 -24 5q-40 17 -40 59v1504q0 42 40 59q12 5 24 5q27 0 45 -19z" />
+<glyph unicode="&#xf246;" horiz-adv-x="1024" d="M832 1408q-320 0 -320 -224v-416h128v-128h-128v-544q0 -224 320 -224h64v-128h-64q-272 0 -384 146q-112 -146 -384 -146h-64v128h64q320 0 320 224v544h-128v128h128v416q0 224 -320 224h-64v128h64q272 0 384 -146q112 146 384 146h64v-128h-64z" />
+<glyph unicode="&#xf247;" horiz-adv-x="2048" d="M2048 1152h-128v-1024h128v-384h-384v128h-1280v-128h-384v384h128v1024h-128v384h384v-128h1280v128h384v-384zM1792 1408v-128h128v128h-128zM128 1408v-128h128v128h-128zM256 -128v128h-128v-128h128zM1664 0v128h128v1024h-128v128h-1280v-128h-128v-1024h128v-128 h1280zM1920 -128v128h-128v-128h128zM1280 896h384v-768h-896v256h-384v768h896v-256zM512 512h640v512h-640v-512zM1536 256v512h-256v-384h-384v-128h640z" />
+<glyph unicode="&#xf248;" horiz-adv-x="2304" d="M2304 768h-128v-640h128v-384h-384v128h-896v-128h-384v384h128v128h-384v-128h-384v384h128v640h-128v384h384v-128h896v128h384v-384h-128v-128h384v128h384v-384zM2048 1024v-128h128v128h-128zM1408 1408v-128h128v128h-128zM128 1408v-128h128v128h-128zM256 256 v128h-128v-128h128zM1536 384h-128v-128h128v128zM384 384h896v128h128v640h-128v128h-896v-128h-128v-640h128v-128zM896 -128v128h-128v-128h128zM2176 -128v128h-128v-128h128zM2048 128v640h-128v128h-384v-384h128v-384h-384v128h-384v-128h128v-128h896v128h128z" />
+<glyph unicode="&#xf249;" d="M1024 288v-416h-928q-40 0 -68 28t-28 68v1344q0 40 28 68t68 28h1344q40 0 68 -28t28 -68v-928h-416q-40 0 -68 -28t-28 -68zM1152 256h381q-15 -82 -65 -132l-184 -184q-50 -50 -132 -65v381z" />
+<glyph unicode="&#xf24a;" d="M1400 256h-248v-248q29 10 41 22l185 185q12 12 22 41zM1120 384h288v896h-1280v-1280h896v288q0 40 28 68t68 28zM1536 1312v-1024q0 -40 -20 -88t-48 -76l-184 -184q-28 -28 -76 -48t-88 -20h-1024q-40 0 -68 28t-28 68v1344q0 40 28 68t68 28h1344q40 0 68 -28t28 -68 z" />
+<glyph unicode="&#xf24b;" horiz-adv-x="2304" d="M1951 538q0 -26 -15.5 -44.5t-38.5 -23.5q-8 -2 -18 -2h-153v140h153q10 0 18 -2q23 -5 38.5 -23.5t15.5 -44.5zM1933 751q0 -25 -15 -42t-38 -21q-3 -1 -15 -1h-139v129h139q3 0 8.5 -0.5t6.5 -0.5q23 -4 38 -21.5t15 -42.5zM728 587v308h-228v-308q0 -58 -38 -94.5 t-105 -36.5q-108 0 -229 59v-112q53 -15 121 -23t109 -9l42 -1q328 0 328 217zM1442 403v113q-99 -52 -200 -59q-108 -8 -169 41t-61 142t61 142t169 41q101 -7 200 -58v112q-48 12 -100 19.5t-80 9.5l-28 2q-127 6 -218.5 -14t-140.5 -60t-71 -88t-22 -106t22 -106t71 -88 t140.5 -60t218.5 -14q101 4 208 31zM2176 518q0 54 -43 88.5t-109 39.5v3q57 8 89 41.5t32 79.5q0 55 -41 88t-107 36q-3 0 -12 0.5t-14 0.5h-455v-510h491q74 0 121.5 36.5t47.5 96.5zM2304 1280v-1280q0 -52 -38 -90t-90 -38h-2048q-52 0 -90 38t-38 90v1280q0 52 38 90 t90 38h2048q52 0 90 -38t38 -90z" />
+<glyph unicode="&#xf24c;" horiz-adv-x="2304" d="M858 295v693q-106 -41 -172 -135.5t-66 -211.5t66 -211.5t172 -134.5zM1362 641q0 117 -66 211.5t-172 135.5v-694q106 41 172 135.5t66 211.5zM1577 641q0 -159 -78.5 -294t-213.5 -213.5t-294 -78.5q-119 0 -227.5 46.5t-187 125t-125 187t-46.5 227.5q0 159 78.5 294 t213.5 213.5t294 78.5t294 -78.5t213.5 -213.5t78.5 -294zM1960 634q0 139 -55.5 261.5t-147.5 205.5t-213.5 131t-252.5 48h-301q-176 0 -323.5 -81t-235 -230t-87.5 -335q0 -171 87 -317.5t236 -231.5t323 -85h301q129 0 251.5 50.5t214.5 135t147.5 202.5t55.5 246z M2304 1280v-1280q0 -52 -38 -90t-90 -38h-2048q-52 0 -90 38t-38 90v1280q0 52 38 90t90 38h2048q52 0 90 -38t38 -90z" />
+<glyph unicode="&#xf24d;" horiz-adv-x="1792" d="M1664 -96v1088q0 13 -9.5 22.5t-22.5 9.5h-1088q-13 0 -22.5 -9.5t-9.5 -22.5v-1088q0 -13 9.5 -22.5t22.5 -9.5h1088q13 0 22.5 9.5t9.5 22.5zM1792 992v-1088q0 -66 -47 -113t-113 -47h-1088q-66 0 -113 47t-47 113v1088q0 66 47 113t113 47h1088q66 0 113 -47t47 -113 zM1408 1376v-160h-128v160q0 13 -9.5 22.5t-22.5 9.5h-1088q-13 0 -22.5 -9.5t-9.5 -22.5v-1088q0 -13 9.5 -22.5t22.5 -9.5h160v-128h-160q-66 0 -113 47t-47 113v1088q0 66 47 113t113 47h1088q66 0 113 -47t47 -113z" />
+<glyph unicode="&#xf24e;" horiz-adv-x="2304" d="M1728 1088l-384 -704h768zM448 1088l-384 -704h768zM1269 1280q-14 -40 -45.5 -71.5t-71.5 -45.5v-1291h608q14 0 23 -9t9 -23v-64q0 -14 -9 -23t-23 -9h-1344q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h608v1291q-40 14 -71.5 45.5t-45.5 71.5h-491q-14 0 -23 9t-9 23v64 q0 14 9 23t23 9h491q21 57 70 92.5t111 35.5t111 -35.5t70 -92.5h491q14 0 23 -9t9 -23v-64q0 -14 -9 -23t-23 -9h-491zM1088 1264q33 0 56.5 23.5t23.5 56.5t-23.5 56.5t-56.5 23.5t-56.5 -23.5t-23.5 -56.5t23.5 -56.5t56.5 -23.5zM2176 384q0 -73 -46.5 -131t-117.5 -91 t-144.5 -49.5t-139.5 -16.5t-139.5 16.5t-144.5 49.5t-117.5 91t-46.5 131q0 11 35 81t92 174.5t107 195.5t102 184t56 100q18 33 56 33t56 -33q4 -7 56 -100t102 -184t107 -195.5t92 -174.5t35 -81zM896 384q0 -73 -46.5 -131t-117.5 -91t-144.5 -49.5t-139.5 -16.5 t-139.5 16.5t-144.5 49.5t-117.5 91t-46.5 131q0 11 35 81t92 174.5t107 195.5t102 184t56 100q18 33 56 33t56 -33q4 -7 56 -100t102 -184t107 -195.5t92 -174.5t35 -81z" />
+<glyph unicode="&#xf250;" d="M1408 1408q0 -261 -106.5 -461.5t-266.5 -306.5q160 -106 266.5 -306.5t106.5 -461.5h96q14 0 23 -9t9 -23v-64q0 -14 -9 -23t-23 -9h-1472q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h96q0 261 106.5 461.5t266.5 306.5q-160 106 -266.5 306.5t-106.5 461.5h-96q-14 0 -23 9 t-9 23v64q0 14 9 23t23 9h1472q14 0 23 -9t9 -23v-64q0 -14 -9 -23t-23 -9h-96zM874 700q77 29 149 92.5t129.5 152.5t92.5 210t35 253h-1024q0 -132 35 -253t92.5 -210t129.5 -152.5t149 -92.5q19 -7 30.5 -23.5t11.5 -36.5t-11.5 -36.5t-30.5 -23.5q-77 -29 -149 -92.5 t-129.5 -152.5t-92.5 -210t-35 -253h1024q0 132 -35 253t-92.5 210t-129.5 152.5t-149 92.5q-19 7 -30.5 23.5t-11.5 36.5t11.5 36.5t30.5 23.5z" />
+<glyph unicode="&#xf251;" d="M1408 1408q0 -261 -106.5 -461.5t-266.5 -306.5q160 -106 266.5 -306.5t106.5 -461.5h96q14 0 23 -9t9 -23v-64q0 -14 -9 -23t-23 -9h-1472q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h96q0 261 106.5 461.5t266.5 306.5q-160 106 -266.5 306.5t-106.5 461.5h-96q-14 0 -23 9 t-9 23v64q0 14 9 23t23 9h1472q14 0 23 -9t9 -23v-64q0 -14 -9 -23t-23 -9h-96zM1280 1408h-1024q0 -66 9 -128h1006q9 61 9 128zM1280 -128q0 130 -34 249.5t-90.5 208t-126.5 152t-146 94.5h-230q-76 -31 -146 -94.5t-126.5 -152t-90.5 -208t-34 -249.5h1024z" />
+<glyph unicode="&#xf252;" d="M1408 1408q0 -261 -106.5 -461.5t-266.5 -306.5q160 -106 266.5 -306.5t106.5 -461.5h96q14 0 23 -9t9 -23v-64q0 -14 -9 -23t-23 -9h-1472q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h96q0 261 106.5 461.5t266.5 306.5q-160 106 -266.5 306.5t-106.5 461.5h-96q-14 0 -23 9 t-9 23v64q0 14 9 23t23 9h1472q14 0 23 -9t9 -23v-64q0 -14 -9 -23t-23 -9h-96zM1280 1408h-1024q0 -206 85 -384h854q85 178 85 384zM1223 192q-54 141 -145.5 241.5t-194.5 142.5h-230q-103 -42 -194.5 -142.5t-145.5 -241.5h910z" />
+<glyph unicode="&#xf253;" d="M1408 1408q0 -261 -106.5 -461.5t-266.5 -306.5q160 -106 266.5 -306.5t106.5 -461.5h96q14 0 23 -9t9 -23v-64q0 -14 -9 -23t-23 -9h-1472q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h96q0 261 106.5 461.5t266.5 306.5q-160 106 -266.5 306.5t-106.5 461.5h-96q-14 0 -23 9 t-9 23v64q0 14 9 23t23 9h1472q14 0 23 -9t9 -23v-64q0 -14 -9 -23t-23 -9h-96zM874 700q77 29 149 92.5t129.5 152.5t92.5 210t35 253h-1024q0 -132 35 -253t92.5 -210t129.5 -152.5t149 -92.5q19 -7 30.5 -23.5t11.5 -36.5t-11.5 -36.5t-30.5 -23.5q-137 -51 -244 -196 h700q-107 145 -244 196q-19 7 -30.5 23.5t-11.5 36.5t11.5 36.5t30.5 23.5z" />
+<glyph unicode="&#xf254;" d="M1504 -64q14 0 23 -9t9 -23v-128q0 -14 -9 -23t-23 -9h-1472q-14 0 -23 9t-9 23v128q0 14 9 23t23 9h1472zM130 0q3 55 16 107t30 95t46 87t53.5 76t64.5 69.5t66 60t70.5 55t66.5 47.5t65 43q-43 28 -65 43t-66.5 47.5t-70.5 55t-66 60t-64.5 69.5t-53.5 76t-46 87 t-30 95t-16 107h1276q-3 -55 -16 -107t-30 -95t-46 -87t-53.5 -76t-64.5 -69.5t-66 -60t-70.5 -55t-66.5 -47.5t-65 -43q43 -28 65 -43t66.5 -47.5t70.5 -55t66 -60t64.5 -69.5t53.5 -76t46 -87t30 -95t16 -107h-1276zM1504 1536q14 0 23 -9t9 -23v-128q0 -14 -9 -23t-23 -9 h-1472q-14 0 -23 9t-9 23v128q0 14 9 23t23 9h1472z" />
+<glyph unicode="&#xf255;" d="M768 1152q-53 0 -90.5 -37.5t-37.5 -90.5v-128h-32v93q0 48 -32 81.5t-80 33.5q-46 0 -79 -33t-33 -79v-429l-32 30v172q0 48 -32 81.5t-80 33.5q-46 0 -79 -33t-33 -79v-224q0 -47 35 -82l310 -296q39 -39 39 -102q0 -26 19 -45t45 -19h640q26 0 45 19t19 45v25 q0 41 10 77l108 436q10 36 10 77v246q0 48 -32 81.5t-80 33.5q-46 0 -79 -33t-33 -79v-32h-32v125q0 40 -25 72.5t-64 40.5q-14 2 -23 2q-46 0 -79 -33t-33 -79v-128h-32v122q0 51 -32.5 89.5t-82.5 43.5q-5 1 -13 1zM768 1280q84 0 149 -50q57 34 123 34q59 0 111 -27 t86 -76q27 7 59 7q100 0 170 -71.5t70 -171.5v-246q0 -51 -13 -108l-109 -436q-6 -24 -6 -71q0 -80 -56 -136t-136 -56h-640q-84 0 -138 58.5t-54 142.5l-308 296q-76 73 -76 175v224q0 99 70.5 169.5t169.5 70.5q11 0 16 -1q6 95 75.5 160t164.5 65q52 0 98 -21 q72 69 174 69z" />
+<glyph unicode="&#xf256;" horiz-adv-x="1792" d="M880 1408q-46 0 -79 -33t-33 -79v-656h-32v528q0 46 -33 79t-79 33t-79 -33t-33 -79v-528v-256l-154 205q-38 51 -102 51q-53 0 -90.5 -37.5t-37.5 -90.5q0 -43 26 -77l384 -512q38 -51 102 -51h688q34 0 61 22t34 56l76 405q5 32 5 59v498q0 46 -33 79t-79 33t-79 -33 t-33 -79v-272h-32v528q0 46 -33 79t-79 33t-79 -33t-33 -79v-528h-32v656q0 46 -33 79t-79 33zM880 1536q68 0 125.5 -35.5t88.5 -96.5q19 4 42 4q99 0 169.5 -70.5t70.5 -169.5v-17q105 6 180.5 -64t75.5 -175v-498q0 -40 -8 -83l-76 -404q-14 -79 -76.5 -131t-143.5 -52 h-688q-60 0 -114.5 27.5t-90.5 74.5l-384 512q-51 68 -51 154q0 106 75 181t181 75q78 0 128 -34v434q0 99 70.5 169.5t169.5 70.5q23 0 42 -4q31 61 88.5 96.5t125.5 35.5z" />
+<glyph unicode="&#xf257;" horiz-adv-x="1792" d="M1073 -128h-177q-163 0 -226 141q-23 49 -23 102v5q-62 30 -98.5 88.5t-36.5 127.5q0 38 5 48h-261q-106 0 -181 75t-75 181t75 181t181 75h113l-44 17q-74 28 -119.5 93.5t-45.5 145.5q0 106 75 181t181 75q46 0 91 -17l628 -239h401q106 0 181 -75t75 -181v-668 q0 -88 -54 -157.5t-140 -90.5l-339 -85q-92 -23 -186 -23zM1024 583l-155 -71l-163 -74q-30 -14 -48 -41.5t-18 -60.5q0 -46 33 -79t79 -33q26 0 46 10l338 154q-49 10 -80.5 50t-31.5 90v55zM1344 272q0 46 -33 79t-79 33q-26 0 -46 -10l-290 -132q-28 -13 -37 -17 t-30.5 -17t-29.5 -23.5t-16 -29t-8 -40.5q0 -50 31.5 -82t81.5 -32q20 0 38 9l352 160q30 14 48 41.5t18 60.5zM1112 1024l-650 248q-24 8 -46 8q-53 0 -90.5 -37.5t-37.5 -90.5q0 -40 22.5 -73t59.5 -47l526 -200v-64h-640q-53 0 -90.5 -37.5t-37.5 -90.5t37.5 -90.5 t90.5 -37.5h535l233 106v198q0 63 46 106l111 102h-69zM1073 0q82 0 155 19l339 85q43 11 70 45.5t27 78.5v668q0 53 -37.5 90.5t-90.5 37.5h-308l-136 -126q-36 -33 -36 -82v-296q0 -46 33 -77t79 -31t79 35t33 81v208h32v-208q0 -70 -57 -114q52 -8 86.5 -48.5t34.5 -93.5 q0 -42 -23 -78t-61 -53l-310 -141h91z" />
+<glyph unicode="&#xf258;" horiz-adv-x="2048" d="M1151 1536q61 0 116 -28t91 -77l572 -781q118 -159 118 -359v-355q0 -80 -56 -136t-136 -56h-384q-80 0 -136 56t-56 136v177l-286 143h-546q-80 0 -136 56t-56 136v32q0 119 84.5 203.5t203.5 84.5h420l42 128h-686q-100 0 -173.5 67.5t-81.5 166.5q-65 79 -65 182v32 q0 80 56 136t136 56h959zM1920 -64v355q0 157 -93 284l-573 781q-39 52 -103 52h-959q-26 0 -45 -19t-19 -45q0 -32 1.5 -49.5t9.5 -40.5t25 -43q10 31 35.5 50t56.5 19h832v-32h-832q-26 0 -45 -19t-19 -45q0 -44 3 -58q8 -44 44 -73t81 -29h640h91q40 0 68 -28t28 -68 q0 -15 -5 -30l-64 -192q-10 -29 -35 -47.5t-56 -18.5h-443q-66 0 -113 -47t-47 -113v-32q0 -26 19 -45t45 -19h561q16 0 29 -7l317 -158q24 -13 38.5 -36t14.5 -50v-197q0 -26 19 -45t45 -19h384q26 0 45 19t19 45z" />
+<glyph unicode="&#xf259;" horiz-adv-x="2048" d="M816 1408q-48 0 -79.5 -34t-31.5 -82q0 -14 3 -28l150 -624h-26l-116 482q-9 38 -39.5 62t-69.5 24q-47 0 -79 -34t-32 -81q0 -11 4 -29q3 -13 39 -161t68 -282t32 -138v-227l-307 230q-34 26 -77 26q-52 0 -89.5 -36.5t-37.5 -88.5q0 -67 56 -110l507 -379 q34 -26 76 -26h694q33 0 59 20.5t34 52.5l100 401q8 30 10 88t9 86l116 478q3 12 3 26q0 46 -33 79t-80 33q-38 0 -69 -25.5t-40 -62.5l-99 -408h-26l132 547q3 14 3 28q0 47 -32 80t-80 33q-38 0 -68.5 -24t-39.5 -62l-145 -602h-127l-164 682q-9 38 -39.5 62t-68.5 24z M1461 -256h-694q-85 0 -153 51l-507 380q-50 38 -78.5 94t-28.5 118q0 105 75 179t180 74q25 0 49.5 -5.5t41.5 -11t41 -20.5t35 -23t38.5 -29.5t37.5 -28.5l-123 512q-7 35 -7 59q0 93 60 162t152 79q14 87 80.5 144.5t155.5 57.5q83 0 148 -51.5t85 -132.5l103 -428 l83 348q20 81 85 132.5t148 51.5q87 0 152.5 -54t82.5 -139q93 -10 155 -78t62 -161q0 -30 -7 -57l-116 -477q-5 -22 -5 -67q0 -51 -13 -108l-101 -401q-19 -75 -79.5 -122.5t-137.5 -47.5z" />
+<glyph unicode="&#xf25a;" horiz-adv-x="1792" d="M640 1408q-53 0 -90.5 -37.5t-37.5 -90.5v-512v-384l-151 202q-41 54 -107 54q-52 0 -89 -38t-37 -90q0 -43 26 -77l384 -512q38 -51 102 -51h718q22 0 39.5 13.5t22.5 34.5l92 368q24 96 24 194v217q0 41 -28 71t-68 30t-68 -28t-28 -68h-32v61q0 48 -32 81.5t-80 33.5 q-46 0 -79 -33t-33 -79v-64h-32v90q0 55 -37 94.5t-91 39.5q-53 0 -90.5 -37.5t-37.5 -90.5v-96h-32v570q0 55 -37 94.5t-91 39.5zM640 1536q107 0 181.5 -77.5t74.5 -184.5v-220q22 2 32 2q99 0 173 -69q47 21 99 21q113 0 184 -87q27 7 56 7q94 0 159 -67.5t65 -161.5 v-217q0 -116 -28 -225l-92 -368q-16 -64 -68 -104.5t-118 -40.5h-718q-60 0 -114.5 27.5t-90.5 74.5l-384 512q-51 68 -51 154q0 105 74.5 180.5t179.5 75.5q71 0 130 -35v547q0 106 75 181t181 75zM768 128v384h-32v-384h32zM1024 128v384h-32v-384h32zM1280 128v384h-32 v-384h32z" />
+<glyph unicode="&#xf25b;" d="M1288 889q60 0 107 -23q141 -63 141 -226v-177q0 -94 -23 -186l-85 -339q-21 -86 -90.5 -140t-157.5 -54h-668q-106 0 -181 75t-75 181v401l-239 628q-17 45 -17 91q0 106 75 181t181 75q80 0 145.5 -45.5t93.5 -119.5l17 -44v113q0 106 75 181t181 75t181 -75t75 -181 v-261q27 5 48 5q69 0 127.5 -36.5t88.5 -98.5zM1072 896q-33 0 -60.5 -18t-41.5 -48l-74 -163l-71 -155h55q50 0 90 -31.5t50 -80.5l154 338q10 20 10 46q0 46 -33 79t-79 33zM1293 761q-22 0 -40.5 -8t-29 -16t-23.5 -29.5t-17 -30.5t-17 -37l-132 -290q-10 -20 -10 -46 q0 -46 33 -79t79 -33q33 0 60.5 18t41.5 48l160 352q9 18 9 38q0 50 -32 81.5t-82 31.5zM128 1120q0 -22 8 -46l248 -650v-69l102 111q43 46 106 46h198l106 233v535q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5v-640h-64l-200 526q-14 37 -47 59.5t-73 22.5 q-53 0 -90.5 -37.5t-37.5 -90.5zM1180 -128q44 0 78.5 27t45.5 70l85 339q19 73 19 155v91l-141 -310q-17 -38 -53 -61t-78 -23q-53 0 -93.5 34.5t-48.5 86.5q-44 -57 -114 -57h-208v32h208q46 0 81 33t35 79t-31 79t-77 33h-296q-49 0 -82 -36l-126 -136v-308 q0 -53 37.5 -90.5t90.5 -37.5h668z" />
+<glyph unicode="&#xf25c;" horiz-adv-x="1973" d="M857 992v-117q0 -13 -9.5 -22t-22.5 -9h-298v-812q0 -13 -9 -22.5t-22 -9.5h-135q-13 0 -22.5 9t-9.5 23v812h-297q-13 0 -22.5 9t-9.5 22v117q0 14 9 23t23 9h793q13 0 22.5 -9.5t9.5 -22.5zM1895 995l77 -961q1 -13 -8 -24q-10 -10 -23 -10h-134q-12 0 -21 8.5 t-10 20.5l-46 588l-189 -425q-8 -19 -29 -19h-120q-20 0 -29 19l-188 427l-45 -590q-1 -12 -10 -20.5t-21 -8.5h-135q-13 0 -23 10q-9 10 -9 24l78 961q1 12 10 20.5t21 8.5h142q20 0 29 -19l220 -520q10 -24 20 -51q3 7 9.5 24.5t10.5 26.5l221 520q9 19 29 19h141 q13 0 22 -8.5t10 -20.5z" />
+<glyph unicode="&#xf25d;" horiz-adv-x="1792" d="M1042 833q0 88 -60 121q-33 18 -117 18h-123v-281h162q66 0 102 37t36 105zM1094 548l205 -373q8 -17 -1 -31q-8 -16 -27 -16h-152q-20 0 -28 17l-194 365h-155v-350q0 -14 -9 -23t-23 -9h-134q-14 0 -23 9t-9 23v960q0 14 9 23t23 9h294q128 0 190 -24q85 -31 134 -109 t49 -180q0 -92 -42.5 -165.5t-115.5 -109.5q6 -10 9 -16zM896 1376q-150 0 -286 -58.5t-234.5 -157t-157 -234.5t-58.5 -286t58.5 -286t157 -234.5t234.5 -157t286 -58.5t286 58.5t234.5 157t157 234.5t58.5 286t-58.5 286t-157 234.5t-234.5 157t-286 58.5zM1792 640 q0 -182 -71 -348t-191 -286t-286 -191t-348 -71t-348 71t-286 191t-191 286t-71 348t71 348t191 286t286 191t348 71t348 -71t286 -191t191 -286t71 -348z" />
+<glyph unicode="&#xf25e;" horiz-adv-x="1792" d="M605 303q153 0 257 104q14 18 3 36l-45 82q-6 13 -24 17q-16 2 -27 -11l-4 -3q-4 -4 -11.5 -10t-17.5 -13t-23.5 -14.5t-28.5 -13.5t-33.5 -9.5t-37.5 -3.5q-76 0 -125 50t-49 127q0 76 48 125.5t122 49.5q37 0 71.5 -14t50.5 -28l16 -14q11 -11 26 -10q16 2 24 14l53 78 q13 20 -2 39q-3 4 -11 12t-30 23.5t-48.5 28t-67.5 22.5t-86 10q-148 0 -246 -96.5t-98 -240.5q0 -146 97 -241.5t247 -95.5zM1235 303q153 0 257 104q14 18 4 36l-45 82q-8 14 -25 17q-16 2 -27 -11l-4 -3q-4 -4 -11.5 -10t-17.5 -13t-23.5 -14.5t-28.5 -13.5t-33.5 -9.5 t-37.5 -3.5q-76 0 -125 50t-49 127q0 76 48 125.5t122 49.5q37 0 71.5 -14t50.5 -28l16 -14q11 -11 26 -10q16 2 24 14l53 78q13 20 -2 39q-3 4 -11 12t-30 23.5t-48.5 28t-67.5 22.5t-86 10q-147 0 -245.5 -96.5t-98.5 -240.5q0 -146 97 -241.5t247 -95.5zM896 1376 q-150 0 -286 -58.5t-234.5 -157t-157 -234.5t-58.5 -286t58.5 -286t157 -234.5t234.5 -157t286 -58.5t286 58.5t234.5 157t157 234.5t58.5 286t-58.5 286t-157 234.5t-234.5 157t-286 58.5zM896 1536q182 0 348 -71t286 -191t191 -286t71 -348t-71 -348t-191 -286t-286 -191 t-348 -71t-348 71t-286 191t-191 286t-71 348t71 348t191 286t286 191t348 71z" />
+<glyph unicode="&#xf260;" horiz-adv-x="2048" d="M736 736l384 -384l-384 -384l-672 672l672 672l168 -168l-96 -96l-72 72l-480 -480l480 -480l193 193l-289 287zM1312 1312l672 -672l-672 -672l-168 168l96 96l72 -72l480 480l-480 480l-193 -193l289 -287l-96 -96l-384 384z" />
+<glyph unicode="&#xf261;" horiz-adv-x="1792" d="M717 182l271 271l-279 279l-88 -88l192 -191l-96 -96l-279 279l279 279l40 -40l87 87l-127 128l-454 -454zM1075 190l454 454l-454 454l-271 -271l279 -279l88 88l-192 191l96 96l279 -279l-279 -279l-40 40l-87 -88zM1792 640q0 -182 -71 -348t-191 -286t-286 -191 t-348 -71t-348 71t-286 191t-191 286t-71 348t71 348t191 286t286 191t348 71t348 -71t286 -191t191 -286t71 -348z" />
+<glyph unicode="&#xf262;" horiz-adv-x="2304" d="M651 539q0 -39 -27.5 -66.5t-65.5 -27.5q-39 0 -66.5 27.5t-27.5 66.5q0 38 27.5 65.5t66.5 27.5q38 0 65.5 -27.5t27.5 -65.5zM1805 540q0 -39 -27.5 -66.5t-66.5 -27.5t-66.5 27.5t-27.5 66.5t27.5 66t66.5 27t66.5 -27t27.5 -66zM765 539q0 79 -56.5 136t-136.5 57 t-136.5 -56.5t-56.5 -136.5t56.5 -136.5t136.5 -56.5t136.5 56.5t56.5 136.5zM1918 540q0 80 -56.5 136.5t-136.5 56.5q-79 0 -136 -56.5t-57 -136.5t56.5 -136.5t136.5 -56.5t136.5 56.5t56.5 136.5zM850 539q0 -116 -81.5 -197.5t-196.5 -81.5q-116 0 -197.5 82t-81.5 197 t82 196.5t197 81.5t196.5 -81.5t81.5 -196.5zM2004 540q0 -115 -81.5 -196.5t-197.5 -81.5q-115 0 -196.5 81.5t-81.5 196.5t81.5 196.5t196.5 81.5q116 0 197.5 -81.5t81.5 -196.5zM1040 537q0 191 -135.5 326.5t-326.5 135.5q-125 0 -231 -62t-168 -168.5t-62 -231.5 t62 -231.5t168 -168.5t231 -62q191 0 326.5 135.5t135.5 326.5zM1708 1110q-254 111 -556 111q-319 0 -573 -110q117 0 223 -45.5t182.5 -122.5t122 -183t45.5 -223q0 115 43.5 219.5t118 180.5t177.5 123t217 50zM2187 537q0 191 -135 326.5t-326 135.5t-326.5 -135.5 t-135.5 -326.5t135.5 -326.5t326.5 -135.5t326 135.5t135 326.5zM1921 1103h383q-44 -51 -75 -114.5t-40 -114.5q110 -151 110 -337q0 -156 -77 -288t-209 -208.5t-287 -76.5q-133 0 -249 56t-196 155q-47 -56 -129 -179q-11 22 -53.5 82.5t-74.5 97.5 q-80 -99 -196.5 -155.5t-249.5 -56.5q-155 0 -287 76.5t-209 208.5t-77 288q0 186 110 337q-9 51 -40 114.5t-75 114.5h365q149 100 355 156.5t432 56.5q224 0 421 -56t348 -157z" />
+<glyph unicode="&#xf263;" horiz-adv-x="1280" d="M640 629q-188 0 -321 133t-133 320q0 188 133 321t321 133t321 -133t133 -321q0 -187 -133 -320t-321 -133zM640 1306q-92 0 -157.5 -65.5t-65.5 -158.5q0 -92 65.5 -157.5t157.5 -65.5t157.5 65.5t65.5 157.5q0 93 -65.5 158.5t-157.5 65.5zM1163 574q13 -27 15 -49.5 t-4.5 -40.5t-26.5 -38.5t-42.5 -37t-61.5 -41.5q-115 -73 -315 -94l73 -72l267 -267q30 -31 30 -74t-30 -73l-12 -13q-31 -30 -74 -30t-74 30q-67 68 -267 268l-267 -268q-31 -30 -74 -30t-73 30l-12 13q-31 30 -31 73t31 74l267 267l72 72q-203 21 -317 94 q-39 25 -61.5 41.5t-42.5 37t-26.5 38.5t-4.5 40.5t15 49.5q10 20 28 35t42 22t56 -2t65 -35q5 -4 15 -11t43 -24.5t69 -30.5t92 -24t113 -11q91 0 174 25.5t120 50.5l38 25q33 26 65 35t56 2t42 -22t28 -35z" />
+<glyph unicode="&#xf264;" d="M927 956q0 -66 -46.5 -112.5t-112.5 -46.5t-112.5 46.5t-46.5 112.5t46.5 112.5t112.5 46.5t112.5 -46.5t46.5 -112.5zM1141 593q-10 20 -28 32t-47.5 9.5t-60.5 -27.5q-10 -8 -29 -20t-81 -32t-127 -20t-124 18t-86 36l-27 18q-31 25 -60.5 27.5t-47.5 -9.5t-28 -32 q-22 -45 -2 -74.5t87 -73.5q83 -53 226 -67l-51 -52q-142 -142 -191 -190q-22 -22 -22 -52.5t22 -52.5l9 -9q22 -22 52.5 -22t52.5 22l191 191q114 -115 191 -191q22 -22 52.5 -22t52.5 22l9 9q22 22 22 52.5t-22 52.5l-191 190l-52 52q141 14 225 67q67 44 87 73.5t-2 74.5 zM1092 956q0 134 -95 229t-229 95t-229 -95t-95 -229t95 -229t229 -95t229 95t95 229zM1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" />
+<glyph unicode="&#xf265;" horiz-adv-x="1720" d="M1565 1408q65 0 110 -45.5t45 -110.5v-519q0 -176 -68 -336t-182.5 -275t-274 -182.5t-334.5 -67.5q-176 0 -335.5 67.5t-274.5 182.5t-183 275t-68 336v519q0 64 46 110t110 46h1409zM861 344q47 0 82 33l404 388q37 35 37 85q0 49 -34.5 83.5t-83.5 34.5q-47 0 -82 -33 l-323 -310l-323 310q-35 33 -81 33q-49 0 -83.5 -34.5t-34.5 -83.5q0 -51 36 -85l405 -388q33 -33 81 -33z" />
+<glyph unicode="&#xf266;" horiz-adv-x="2304" d="M1494 -103l-295 695q-25 -49 -158.5 -305.5t-198.5 -389.5q-1 -1 -27.5 -0.5t-26.5 1.5q-82 193 -255.5 587t-259.5 596q-21 50 -66.5 107.5t-103.5 100.5t-102 43q0 5 -0.5 24t-0.5 27h583v-50q-39 -2 -79.5 -16t-66.5 -43t-10 -64q26 -59 216.5 -499t235.5 -540 q31 61 140 266.5t131 247.5q-19 39 -126 281t-136 295q-38 69 -201 71v50l513 -1v-47q-60 -2 -93.5 -25t-12.5 -69q33 -70 87 -189.5t86 -187.5q110 214 173 363q24 55 -10 79.5t-129 26.5q1 7 1 25v24q64 0 170.5 0.5t180 1t92.5 0.5v-49q-62 -2 -119 -33t-90 -81 l-213 -442q13 -33 127.5 -290t121.5 -274l441 1017q-14 38 -49.5 62.5t-65 31.5t-55.5 8v50l460 -4l1 -2l-1 -44q-139 -4 -201 -145q-526 -1216 -559 -1291h-49z" />
+<glyph unicode="&#xf267;" horiz-adv-x="1792" d="M949 643q0 -26 -16.5 -45t-41.5 -19q-26 0 -45 16.5t-19 41.5q0 26 17 45t42 19t44 -16.5t19 -41.5zM964 585l350 581q-9 -8 -67.5 -62.5t-125.5 -116.5t-136.5 -127t-117 -110.5t-50.5 -51.5l-349 -580q7 7 67 62t126 116.5t136 127t117 111t50 50.5zM1611 640 q0 -201 -104 -371q-3 2 -17 11t-26.5 16.5t-16.5 7.5q-13 0 -13 -13q0 -10 59 -44q-74 -112 -184.5 -190.5t-241.5 -110.5l-16 67q-1 10 -15 10q-5 0 -8 -5.5t-2 -9.5l16 -68q-72 -15 -146 -15q-199 0 -372 105q1 2 13 20.5t21.5 33.5t9.5 19q0 13 -13 13q-6 0 -17 -14.5 t-22.5 -34.5t-13.5 -23q-113 75 -192 187.5t-110 244.5l69 15q10 3 10 15q0 5 -5.5 8t-10.5 2l-68 -15q-14 72 -14 139q0 206 109 379q2 -1 18.5 -12t30 -19t17.5 -8q13 0 13 12q0 6 -12.5 15.5t-32.5 21.5l-20 12q77 112 189 189t244 107l15 -67q2 -10 15 -10q5 0 8 5.5 t2 10.5l-15 66q71 13 134 13q204 0 379 -109q-39 -56 -39 -65q0 -13 12 -13q11 0 48 64q111 -75 187.5 -186t107.5 -241l-56 -12q-10 -2 -10 -16q0 -5 5.5 -8t9.5 -2l57 13q14 -72 14 -140zM1696 640q0 163 -63.5 311t-170.5 255t-255 170.5t-311 63.5t-311 -63.5 t-255 -170.5t-170.5 -255t-63.5 -311t63.5 -311t170.5 -255t255 -170.5t311 -63.5t311 63.5t255 170.5t170.5 255t63.5 311zM1792 640q0 -182 -71 -348t-191 -286t-286 -191t-348 -71t-348 71t-286 191t-191 286t-71 348t71 348t191 286t286 191t348 71t348 -71t286 -191 t191 -286t71 -348z" />
+<glyph unicode="&#xf268;" horiz-adv-x="1792" d="M893 1536q240 2 451 -120q232 -134 352 -372l-742 39q-160 9 -294 -74.5t-185 -229.5l-276 424q128 159 311 245.5t383 87.5zM146 1131l337 -663q72 -143 211 -217t293 -45l-230 -451q-212 33 -385 157.5t-272.5 316t-99.5 411.5q0 267 146 491zM1732 962 q58 -150 59.5 -310.5t-48.5 -306t-153 -272t-246 -209.5q-230 -133 -498 -119l405 623q88 131 82.5 290.5t-106.5 277.5zM896 942q125 0 213.5 -88.5t88.5 -213.5t-88.5 -213.5t-213.5 -88.5t-213.5 88.5t-88.5 213.5t88.5 213.5t213.5 88.5z" />
+<glyph unicode="&#xf269;" horiz-adv-x="1792" d="M903 -256q-283 0 -504.5 150.5t-329.5 398.5q-58 131 -67 301t26 332.5t111 312t179 242.5l-11 -281q11 14 68 15.5t70 -15.5q42 81 160.5 138t234.5 59q-54 -45 -119.5 -148.5t-58.5 -163.5q25 -8 62.5 -13.5t63 -7.5t68 -4t50.5 -3q15 -5 9.5 -45.5t-30.5 -75.5 q-5 -7 -16.5 -18.5t-56.5 -35.5t-101 -34l15 -189l-139 67q-18 -43 -7.5 -81.5t36 -66.5t65.5 -41.5t81 -6.5q51 9 98 34.5t83.5 45t73.5 17.5q61 -4 89.5 -33t19.5 -65q-1 -2 -2.5 -5.5t-8.5 -12.5t-18 -15.5t-31.5 -10.5t-46.5 -1q-60 -95 -144.5 -135.5t-209.5 -29.5 q74 -61 162.5 -82.5t168.5 -6t154.5 52t128 87.5t80.5 104q43 91 39 192.5t-37.5 188.5t-78.5 125q87 -38 137 -79.5t77 -112.5q15 170 -57.5 343t-209.5 284q265 -77 412 -279.5t151 -517.5q2 -127 -40.5 -255t-123.5 -238t-189 -196t-247.5 -135.5t-288.5 -49.5z" />
+<glyph unicode="&#xf26a;" horiz-adv-x="1792" d="M1493 1308q-165 110 -359 110q-155 0 -293 -73t-240 -200q-75 -93 -119.5 -218t-48.5 -266v-42q4 -141 48.5 -266t119.5 -218q102 -127 240 -200t293 -73q194 0 359 110q-121 -108 -274.5 -168t-322.5 -60q-29 0 -43 1q-175 8 -333 82t-272 193t-181 281t-67 339 q0 182 71 348t191 286t286 191t348 71h3q168 -1 320.5 -60.5t273.5 -167.5zM1792 640q0 -192 -77 -362.5t-213 -296.5q-104 -63 -222 -63q-137 0 -255 84q154 56 253.5 233t99.5 405q0 227 -99 404t-253 234q119 83 254 83q119 0 226 -65q135 -125 210.5 -295t75.5 -361z " />
+<glyph unicode="&#xf26b;" horiz-adv-x="1792" d="M1792 599q0 -56 -7 -104h-1151q0 -146 109.5 -244.5t257.5 -98.5q99 0 185.5 46.5t136.5 130.5h423q-56 -159 -170.5 -281t-267.5 -188.5t-321 -66.5q-187 0 -356 83q-228 -116 -394 -116q-237 0 -237 263q0 115 45 275q17 60 109 229q199 360 475 606 q-184 -79 -427 -354q63 274 283.5 449.5t501.5 175.5q30 0 45 -1q255 117 433 117q64 0 116 -13t94.5 -40.5t66.5 -76.5t24 -115q0 -116 -75 -286q101 -182 101 -390zM1722 1239q0 83 -53 132t-137 49q-108 0 -254 -70q121 -47 222.5 -131.5t170.5 -195.5q51 135 51 216z M128 2q0 -86 48.5 -132.5t134.5 -46.5q115 0 266 83q-122 72 -213.5 183t-137.5 245q-98 -205 -98 -332zM632 715h728q-5 142 -113 237t-251 95q-144 0 -251.5 -95t-112.5 -237z" />
+<glyph unicode="&#xf26c;" horiz-adv-x="2048" d="M1792 288v960q0 13 -9.5 22.5t-22.5 9.5h-1600q-13 0 -22.5 -9.5t-9.5 -22.5v-960q0 -13 9.5 -22.5t22.5 -9.5h1600q13 0 22.5 9.5t9.5 22.5zM1920 1248v-960q0 -66 -47 -113t-113 -47h-736v-128h352q14 0 23 -9t9 -23v-64q0 -14 -9 -23t-23 -9h-832q-14 0 -23 9t-9 23 v64q0 14 9 23t23 9h352v128h-736q-66 0 -113 47t-47 113v960q0 66 47 113t113 47h1600q66 0 113 -47t47 -113z" />
+<glyph unicode="&#xf26d;" horiz-adv-x="1792" d="M138 1408h197q-70 -64 -126 -149q-36 -56 -59 -115t-30 -125.5t-8.5 -120t10.5 -132t21 -126t28 -136.5q4 -19 6 -28q51 -238 81 -329q57 -171 152 -275h-272q-48 0 -82 34t-34 82v1304q0 48 34 82t82 34zM1346 1408h308q48 0 82 -34t34 -82v-1304q0 -48 -34 -82t-82 -34 h-178q212 210 196 565l-469 -101q-2 -45 -12 -82t-31 -72t-59.5 -59.5t-93.5 -36.5q-123 -26 -199 40q-32 27 -53 61t-51.5 129t-64.5 258q-35 163 -45.5 263t-5.5 139t23 77q20 41 62.5 73t102.5 45q45 12 83.5 6.5t67 -17t54 -35t43 -48t34.5 -56.5l468 100 q-68 175 -180 287z" />
+<glyph unicode="&#xf26e;" d="M1401 -11l-6 -6q-113 -114 -259 -175q-154 -64 -317 -64q-165 0 -317 64q-148 63 -259 175q-113 112 -175 258q-42 103 -54 189q-4 28 48 36q51 8 56 -20q1 -1 1 -4q18 -90 46 -159q50 -124 152 -226q98 -98 226 -152q132 -56 276 -56q143 0 276 56q128 55 225 152l6 6 q10 10 25 6q12 -3 33 -22q36 -37 17 -58zM929 604l-66 -66l63 -63q21 -21 -7 -49q-17 -17 -32 -17q-10 0 -19 10l-62 61l-66 -66q-5 -5 -15 -5q-15 0 -31 16l-2 2q-18 15 -18 29q0 7 8 17l66 65l-66 66q-16 16 14 45q18 18 31 18q6 0 13 -5l65 -66l65 65q18 17 48 -13 q27 -27 11 -44zM1400 547q0 -118 -46 -228q-45 -105 -126 -186q-80 -80 -187 -126t-228 -46t-228 46t-187 126q-82 82 -125 186q-15 32 -15 40h-1q-9 27 43 44q50 16 60 -12q37 -99 97 -167h1v339v2q3 136 102 232q105 103 253 103q147 0 251 -103t104 -249 q0 -147 -104.5 -251t-250.5 -104q-58 0 -112 16q-28 11 -13 61q16 51 44 43l14 -3q14 -3 32.5 -6t30.5 -3q104 0 176 71.5t72 174.5q0 101 -72 171q-71 71 -175 71q-107 0 -178 -80q-64 -72 -64 -160v-413q110 -67 242 -67q96 0 185 36.5t156 103.5t103.5 155t36.5 183 q0 198 -141 339q-140 140 -339 140q-200 0 -340 -140q-53 -53 -77 -87l-2 -2q-8 -11 -13 -15.5t-21.5 -9.5t-38.5 3q-21 5 -36.5 16.5t-15.5 26.5v680q0 15 10.5 26.5t27.5 11.5h877q30 0 30 -55t-30 -55h-811v-483h1q40 42 102 84t108 61q109 46 231 46q121 0 228 -46 t187 -126q81 -81 126 -186q46 -112 46 -229zM1369 1128q9 -8 9 -18t-5.5 -18t-16.5 -21q-26 -26 -39 -26q-9 0 -16 7q-106 91 -207 133q-128 56 -276 56q-133 0 -262 -49q-27 -10 -45 37q-9 25 -8 38q3 16 16 20q130 57 299 57q164 0 316 -64q137 -58 235 -152z" />
+<glyph unicode="&#xf270;" horiz-adv-x="1792" d="M1551 60q15 6 26 3t11 -17.5t-15 -33.5q-13 -16 -44 -43.5t-95.5 -68t-141 -74t-188 -58t-229.5 -24.5q-119 0 -238 31t-209 76.5t-172.5 104t-132.5 105t-84 87.5q-8 9 -10 16.5t1 12t8 7t11.5 2t11.5 -4.5q192 -117 300 -166q389 -176 799 -90q190 40 391 135z M1758 175q11 -16 2.5 -69.5t-28.5 -102.5q-34 -83 -85 -124q-17 -14 -26 -9t0 24q21 45 44.5 121.5t6.5 98.5q-5 7 -15.5 11.5t-27 6t-29.5 2.5t-35 0t-31.5 -2t-31 -3t-22.5 -2q-6 -1 -13 -1.5t-11 -1t-8.5 -1t-7 -0.5h-5.5h-4.5t-3 0.5t-2 1.5l-1.5 3q-6 16 47 40t103 30 q46 7 108 1t76 -24zM1364 618q0 -31 13.5 -64t32 -58t37.5 -46t33 -32l13 -11l-227 -224q-40 37 -79 75.5t-58 58.5l-19 20q-11 11 -25 33q-38 -59 -97.5 -102.5t-127.5 -63.5t-140 -23t-137.5 21t-117.5 65.5t-83 113t-31 162.5q0 84 28 154t72 116.5t106.5 83t122.5 57 t130 34.5t119.5 18.5t99.5 6.5v127q0 65 -21 97q-34 53 -121 53q-6 0 -16.5 -1t-40.5 -12t-56 -29.5t-56 -59.5t-48 -96l-294 27q0 60 22 119t67 113t108 95t151.5 65.5t190.5 24.5q100 0 181 -25t129.5 -61.5t81 -83t45 -86t12.5 -73.5v-589zM692 597q0 -86 70 -133 q66 -44 139 -22q84 25 114 123q14 45 14 101v162q-59 -2 -111 -12t-106.5 -33.5t-87 -71t-32.5 -114.5z" />
+<glyph unicode="&#xf271;" horiz-adv-x="1792" d="M1536 1280q52 0 90 -38t38 -90v-1280q0 -52 -38 -90t-90 -38h-1408q-52 0 -90 38t-38 90v1280q0 52 38 90t90 38h128v96q0 66 47 113t113 47h64q66 0 113 -47t47 -113v-96h384v96q0 66 47 113t113 47h64q66 0 113 -47t47 -113v-96h128zM1152 1376v-288q0 -14 9 -23t23 -9 h64q14 0 23 9t9 23v288q0 14 -9 23t-23 9h-64q-14 0 -23 -9t-9 -23zM384 1376v-288q0 -14 9 -23t23 -9h64q14 0 23 9t9 23v288q0 14 -9 23t-23 9h-64q-14 0 -23 -9t-9 -23zM1536 -128v1024h-1408v-1024h1408zM896 448h224q14 0 23 -9t9 -23v-64q0 -14 -9 -23t-23 -9h-224 v-224q0 -14 -9 -23t-23 -9h-64q-14 0 -23 9t-9 23v224h-224q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h224v224q0 14 9 23t23 9h64q14 0 23 -9t9 -23v-224z" />
+<glyph unicode="&#xf272;" horiz-adv-x="1792" d="M1152 416v-64q0 -14 -9 -23t-23 -9h-576q-14 0 -23 9t-9 23v64q0 14 9 23t23 9h576q14 0 23 -9t9 -23zM128 -128h1408v1024h-1408v-1024zM512 1088v288q0 14 -9 23t-23 9h-64q-14 0 -23 -9t-9 -23v-288q0 -14 9 -23t23 -9h64q14 0 23 9t9 23zM1280 1088v288q0 14 -9 23 t-23 9h-64q-14 0 -23 -9t-9 -23v-288q0 -14 9 -23t23 -9h64q14 0 23 9t9 23zM1664 1152v-1280q0 -52 -38 -90t-90 -38h-1408q-52 0 -90 38t-38 90v1280q0 52 38 90t90 38h128v96q0 66 47 113t113 47h64q66 0 113 -47t47 -113v-96h384v96q0 66 47 113t113 47h64q66 0 113 -47 t47 -113v-96h128q52 0 90 -38t38 -90z" />
+<glyph unicode="&#xf273;" horiz-adv-x="1792" d="M1111 151l-46 -46q-9 -9 -22 -9t-23 9l-188 189l-188 -189q-10 -9 -23 -9t-22 9l-46 46q-9 9 -9 22t9 23l189 188l-189 188q-9 10 -9 23t9 22l46 46q9 9 22 9t23 -9l188 -188l188 188q10 9 23 9t22 -9l46 -46q9 -9 9 -22t-9 -23l-188 -188l188 -188q9 -10 9 -23t-9 -22z M128 -128h1408v1024h-1408v-1024zM512 1088v288q0 14 -9 23t-23 9h-64q-14 0 -23 -9t-9 -23v-288q0 -14 9 -23t23 -9h64q14 0 23 9t9 23zM1280 1088v288q0 14 -9 23t-23 9h-64q-14 0 -23 -9t-9 -23v-288q0 -14 9 -23t23 -9h64q14 0 23 9t9 23zM1664 1152v-1280 q0 -52 -38 -90t-90 -38h-1408q-52 0 -90 38t-38 90v1280q0 52 38 90t90 38h128v96q0 66 47 113t113 47h64q66 0 113 -47t47 -113v-96h384v96q0 66 47 113t113 47h64q66 0 113 -47t47 -113v-96h128q52 0 90 -38t38 -90z" />
+<glyph unicode="&#xf274;" horiz-adv-x="1792" d="M1303 572l-512 -512q-10 -9 -23 -9t-23 9l-288 288q-9 10 -9 23t9 22l46 46q9 9 22 9t23 -9l220 -220l444 444q10 9 23 9t22 -9l46 -46q9 -9 9 -22t-9 -23zM128 -128h1408v1024h-1408v-1024zM512 1088v288q0 14 -9 23t-23 9h-64q-14 0 -23 -9t-9 -23v-288q0 -14 9 -23 t23 -9h64q14 0 23 9t9 23zM1280 1088v288q0 14 -9 23t-23 9h-64q-14 0 -23 -9t-9 -23v-288q0 -14 9 -23t23 -9h64q14 0 23 9t9 23zM1664 1152v-1280q0 -52 -38 -90t-90 -38h-1408q-52 0 -90 38t-38 90v1280q0 52 38 90t90 38h128v96q0 66 47 113t113 47h64q66 0 113 -47 t47 -113v-96h384v96q0 66 47 113t113 47h64q66 0 113 -47t47 -113v-96h128q52 0 90 -38t38 -90z" />
+<glyph unicode="&#xf275;" horiz-adv-x="1792" d="M448 1536q26 0 45 -19t19 -45v-891l536 429q17 14 40 14q26 0 45 -19t19 -45v-379l536 429q17 14 40 14q26 0 45 -19t19 -45v-1152q0 -26 -19 -45t-45 -19h-1664q-26 0 -45 19t-19 45v1664q0 26 19 45t45 19h384z" />
+<glyph unicode="&#xf276;" horiz-adv-x="1024" d="M512 448q66 0 128 15v-655q0 -26 -19 -45t-45 -19h-128q-26 0 -45 19t-19 45v655q61 -15 128 -15zM512 1536q212 0 362 -150t150 -362t-150 -362t-362 -150t-362 150t-150 362t150 362t362 150zM512 1312q14 0 23 9t9 23t-9 23t-23 9q-146 0 -249 -103t-103 -249 q0 -14 9 -23t23 -9t23 9t9 23q0 119 84.5 203.5t203.5 84.5z" />
+<glyph unicode="&#xf277;" horiz-adv-x="1792" d="M1745 1239q10 -10 10 -23t-10 -23l-141 -141q-28 -28 -68 -28h-1344q-26 0 -45 19t-19 45v256q0 26 19 45t45 19h576v64q0 26 19 45t45 19h128q26 0 45 -19t19 -45v-64h512q40 0 68 -28zM768 320h256v-512q0 -26 -19 -45t-45 -19h-128q-26 0 -45 19t-19 45v512zM1600 768 q26 0 45 -19t19 -45v-256q0 -26 -19 -45t-45 -19h-1344q-40 0 -68 28l-141 141q-10 10 -10 23t10 23l141 141q28 28 68 28h512v192h256v-192h576z" />
+<glyph unicode="&#xf278;" horiz-adv-x="2048" d="M2020 1525q28 -20 28 -53v-1408q0 -20 -11 -36t-29 -23l-640 -256q-24 -11 -48 0l-616 246l-616 -246q-10 -5 -24 -5q-19 0 -36 11q-28 20 -28 53v1408q0 20 11 36t29 23l640 256q24 11 48 0l616 -246l616 246q32 13 60 -6zM736 1390v-1270l576 -230v1270zM128 1173 v-1270l544 217v1270zM1920 107v1270l-544 -217v-1270z" />
+<glyph unicode="&#xf279;" horiz-adv-x="1792" d="M512 1536q13 0 22.5 -9.5t9.5 -22.5v-1472q0 -20 -17 -28l-480 -256q-7 -4 -15 -4q-13 0 -22.5 9.5t-9.5 22.5v1472q0 20 17 28l480 256q7 4 15 4zM1760 1536q13 0 22.5 -9.5t9.5 -22.5v-1472q0 -20 -17 -28l-480 -256q-7 -4 -15 -4q-13 0 -22.5 9.5t-9.5 22.5v1472 q0 20 17 28l480 256q7 4 15 4zM640 1536q8 0 14 -3l512 -256q18 -10 18 -29v-1472q0 -13 -9.5 -22.5t-22.5 -9.5q-8 0 -14 3l-512 256q-18 10 -18 29v1472q0 13 9.5 22.5t22.5 9.5z" />
+<glyph unicode="&#xf27a;" horiz-adv-x="1792" d="M640 640q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM1024 640q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM1408 640q0 53 -37.5 90.5t-90.5 37.5 t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5zM1792 640q0 -174 -120 -321.5t-326 -233t-450 -85.5q-110 0 -211 18q-173 -173 -435 -229q-52 -10 -86 -13q-12 -1 -22 6t-13 18q-4 15 20 37q5 5 23.5 21.5t25.5 23.5t23.5 25.5t24 31.5t20.5 37 t20 48t14.5 57.5t12.5 72.5q-146 90 -229.5 216.5t-83.5 269.5q0 174 120 321.5t326 233t450 85.5t450 -85.5t326 -233t120 -321.5z" />
+<glyph unicode="&#xf27b;" horiz-adv-x="1792" d="M640 640q0 -53 -37.5 -90.5t-90.5 -37.5t-90.5 37.5t-37.5 90.5t37.5 90.5t90.5 37.5t90.5 -37.5t37.5 -90.5zM1024 640q0 -53 -37.5 -90.5t-90.5 -37.5t-90.5 37.5t-37.5 90.5t37.5 90.5t90.5 37.5t90.5 -37.5t37.5 -90.5zM1408 640q0 -53 -37.5 -90.5t-90.5 -37.5 t-90.5 37.5t-37.5 90.5t37.5 90.5t90.5 37.5t90.5 -37.5t37.5 -90.5zM896 1152q-204 0 -381.5 -69.5t-282 -187.5t-104.5 -255q0 -112 71.5 -213.5t201.5 -175.5l87 -50l-27 -96q-24 -91 -70 -172q152 63 275 171l43 38l57 -6q69 -8 130 -8q204 0 381.5 69.5t282 187.5 t104.5 255t-104.5 255t-282 187.5t-381.5 69.5zM1792 640q0 -174 -120 -321.5t-326 -233t-450 -85.5q-70 0 -145 8q-198 -175 -460 -242q-49 -14 -114 -22h-5q-15 0 -27 10.5t-16 27.5v1q-3 4 -0.5 12t2 10t4.5 9.5l6 9t7 8.5t8 9q7 8 31 34.5t34.5 38t31 39.5t32.5 51 t27 59t26 76q-157 89 -247.5 220t-90.5 281q0 130 71 248.5t191 204.5t286 136.5t348 50.5t348 -50.5t286 -136.5t191 -204.5t71 -248.5z" />
+<glyph unicode="&#xf27c;" horiz-adv-x="1024" d="M512 345l512 295v-591l-512 -296v592zM0 640v-591l512 296zM512 1527v-591l-512 -296v591zM512 936l512 295v-591z" />
+<glyph unicode="&#xf27d;" horiz-adv-x="1792" d="M1709 1018q-10 -236 -332 -651q-333 -431 -562 -431q-142 0 -240 263q-44 160 -132 482q-72 262 -157 262q-18 0 -127 -76l-77 98q24 21 108 96.5t130 115.5q156 138 241 146q95 9 153 -55.5t81 -203.5q44 -287 66 -373q55 -249 120 -249q51 0 154 161q101 161 109 246 q13 139 -109 139q-57 0 -121 -26q120 393 459 382q251 -8 236 -326z" />
+<glyph unicode="&#xf27e;" d="M0 1408h1536v-1536h-1536v1536zM1085 293l-221 631l221 297h-634l221 -297l-221 -631l317 -304z" />
+<glyph unicode="&#xf280;" d="M0 1408h1536v-1536h-1536v1536zM908 1088l-12 -33l75 -83l-31 -114l25 -25l107 57l107 -57l25 25l-31 114l75 83l-12 33h-95l-53 96h-32l-53 -96h-95zM641 925q32 0 44.5 -16t11.5 -63l174 21q0 55 -17.5 92.5t-50.5 56t-69 25.5t-85 7q-133 0 -199 -57.5t-66 -182.5v-72 h-96v-128h76q20 0 20 -8v-382q0 -14 -5 -20t-18 -7l-73 -7v-88h448v86l-149 14q-6 1 -8.5 1.5t-3.5 2.5t-0.5 4t1 7t0.5 10v387h191l38 128h-231q-6 0 -2 6t4 9v80q0 27 1.5 40.5t7.5 28t19.5 20t36.5 5.5zM1248 96v86l-54 9q-7 1 -9.5 2.5t-2.5 3t1 7.5t1 12v520h-275 l-23 -101l83 -22q23 -7 23 -27v-370q0 -14 -6 -18.5t-20 -6.5l-70 -9v-86h352z" />
+<glyph unicode="&#xf281;" horiz-adv-x="1792" d="M1792 690q0 -58 -29.5 -105.5t-79.5 -72.5q12 -46 12 -96q0 -155 -106.5 -287t-290.5 -208.5t-400 -76.5t-399.5 76.5t-290 208.5t-106.5 287q0 47 11 94q-51 25 -82 73.5t-31 106.5q0 82 58 140.5t141 58.5q85 0 145 -63q218 152 515 162l116 521q3 13 15 21t26 5 l369 -81q18 37 54 59.5t79 22.5q62 0 106 -43.5t44 -105.5t-44 -106t-106 -44t-105.5 43.5t-43.5 105.5l-334 74l-104 -472q300 -9 519 -160q58 61 143 61q83 0 141 -58.5t58 -140.5zM418 491q0 -62 43.5 -106t105.5 -44t106 44t44 106t-44 105.5t-106 43.5q-61 0 -105 -44 t-44 -105zM1228 136q11 11 11 26t-11 26q-10 10 -25 10t-26 -10q-41 -42 -121 -62t-160 -20t-160 20t-121 62q-11 10 -26 10t-25 -10q-11 -10 -11 -25.5t11 -26.5q43 -43 118.5 -68t122.5 -29.5t91 -4.5t91 4.5t122.5 29.5t118.5 68zM1225 341q62 0 105.5 44t43.5 106 q0 61 -44 105t-105 44q-62 0 -106 -43.5t-44 -105.5t44 -106t106 -44z" />
+<glyph unicode="&#xf282;" horiz-adv-x="1792" d="M69 741h1q16 126 58.5 241.5t115 217t167.5 176t223.5 117.5t276.5 43q231 0 414 -105.5t294 -303.5q104 -187 104 -442v-188h-1125q1 -111 53.5 -192.5t136.5 -122.5t189.5 -57t213 -3t208 46.5t173.5 84.5v-377q-92 -55 -229.5 -92t-312.5 -38t-316 53 q-189 73 -311.5 249t-124.5 372q-3 242 111 412t325 268q-48 -60 -78 -125.5t-46 -159.5h635q8 77 -8 140t-47 101.5t-70.5 66.5t-80.5 41t-75 20.5t-56 8.5l-22 1q-135 -5 -259.5 -44.5t-223.5 -104.5t-176 -140.5t-138 -163.5z" />
+<glyph unicode="&#xf283;" horiz-adv-x="2304" d="M0 32v608h2304v-608q0 -66 -47 -113t-113 -47h-1984q-66 0 -113 47t-47 113zM640 256v-128h384v128h-384zM256 256v-128h256v128h-256zM2144 1408q66 0 113 -47t47 -113v-224h-2304v224q0 66 47 113t113 47h1984z" />
+<glyph unicode="&#xf284;" horiz-adv-x="1792" d="M1549 857q55 0 85.5 -28.5t30.5 -83.5t-34 -82t-91 -27h-136v-177h-25v398h170zM1710 267l-4 -11l-5 -10q-113 -230 -330.5 -366t-474.5 -136q-182 0 -348 71t-286 191t-191 286t-71 348t71 348t191 286t286 191t348 71q244 0 454.5 -124t329.5 -338l2 -4l8 -16 q-30 -15 -136.5 -68.5t-163.5 -84.5q-6 -3 -479 -268q384 -183 799 -366zM896 -234q250 0 462.5 132.5t322.5 357.5l-287 129q-72 -140 -206 -222t-292 -82q-151 0 -280 75t-204 204t-75 280t75 280t204 204t280 75t280 -73.5t204 -204.5l280 143q-116 208 -321 329 t-443 121q-119 0 -232.5 -31.5t-209 -87.5t-176.5 -137t-137 -176.5t-87.5 -209t-31.5 -232.5t31.5 -232.5t87.5 -209t137 -176.5t176.5 -137t209 -87.5t232.5 -31.5z" />
+<glyph unicode="&#xf285;" horiz-adv-x="1792" d="M1427 827l-614 386l92 151h855zM405 562l-184 116v858l1183 -743zM1424 697l147 -95v-858l-532 335zM1387 718l-500 -802h-855l356 571z" />
+<glyph unicode="&#xf286;" horiz-adv-x="1792" d="M640 528v224q0 16 -16 16h-96q-16 0 -16 -16v-224q0 -16 16 -16h96q16 0 16 16zM1152 528v224q0 16 -16 16h-96q-16 0 -16 -16v-224q0 -16 16 -16h96q16 0 16 16zM1664 496v-752h-640v320q0 80 -56 136t-136 56t-136 -56t-56 -136v-320h-640v752q0 16 16 16h96 q16 0 16 -16v-112h128v624q0 16 16 16h96q16 0 16 -16v-112h128v112q0 16 16 16h96q16 0 16 -16v-112h128v112q0 6 2.5 9.5t8.5 5t9.5 2t11.5 0t9 -0.5v391q-32 15 -32 50q0 23 16.5 39t38.5 16t38.5 -16t16.5 -39q0 -35 -32 -50v-17q45 10 83 10q21 0 59.5 -7.5t54.5 -7.5 q17 0 47 7.5t37 7.5q16 0 16 -16v-210q0 -15 -35 -21.5t-62 -6.5q-18 0 -54.5 7.5t-55.5 7.5q-40 0 -90 -12v-133q1 0 9 0.5t11.5 0t9.5 -2t8.5 -5t2.5 -9.5v-112h128v112q0 16 16 16h96q16 0 16 -16v-112h128v112q0 16 16 16h96q16 0 16 -16v-624h128v112q0 16 16 16h96 q16 0 16 -16z" />
+<glyph unicode="&#xf287;" horiz-adv-x="2304" d="M2288 731q16 -8 16 -27t-16 -27l-320 -192q-8 -5 -16 -5q-9 0 -16 4q-16 10 -16 28v128h-858q37 -58 83 -165q16 -37 24.5 -55t24 -49t27 -47t27 -34t31.5 -26t33 -8h96v96q0 14 9 23t23 9h320q14 0 23 -9t9 -23v-320q0 -14 -9 -23t-23 -9h-320q-14 0 -23 9t-9 23v96h-96 q-32 0 -61 10t-51 23.5t-45 40.5t-37 46t-33.5 57t-28.5 57.5t-28 60.5q-23 53 -37 81.5t-36 65t-44.5 53.5t-46.5 17h-360q-22 -84 -91 -138t-157 -54q-106 0 -181 75t-75 181t75 181t181 75q88 0 157 -54t91 -138h104q24 0 46.5 17t44.5 53.5t36 65t37 81.5q19 41 28 60.5 t28.5 57.5t33.5 57t37 46t45 40.5t51 23.5t61 10h107q21 57 70 92.5t111 35.5q80 0 136 -56t56 -136t-56 -136t-136 -56q-62 0 -111 35.5t-70 92.5h-107q-17 0 -33 -8t-31.5 -26t-27 -34t-27 -47t-24 -49t-24.5 -55q-46 -107 -83 -165h1114v128q0 18 16 28t32 -1z" />
+<glyph unicode="&#xf288;" horiz-adv-x="1792" d="M1150 774q0 -56 -39.5 -95t-95.5 -39h-253v269h253q56 0 95.5 -39.5t39.5 -95.5zM1329 774q0 130 -91.5 222t-222.5 92h-433v-896h180v269h253q130 0 222 91.5t92 221.5zM1792 640q0 -182 -71 -348t-191 -286t-286 -191t-348 -71t-348 71t-286 191t-191 286t-71 348 t71 348t191 286t286 191t348 71t348 -71t286 -191t191 -286t71 -348z" />
+<glyph unicode="&#xf289;" horiz-adv-x="2304" d="M1645 438q0 59 -34 106.5t-87 68.5q-7 -45 -23 -92q-7 -24 -27.5 -38t-44.5 -14q-12 0 -24 3q-31 10 -45 38.5t-4 58.5q23 71 23 143q0 123 -61 227.5t-166 165.5t-228 61q-134 0 -247 -73t-167 -194q108 -28 188 -106q22 -23 22 -55t-22 -54t-54 -22t-55 22 q-75 75 -180 75q-106 0 -181 -74.5t-75 -180.5t75 -180.5t181 -74.5h1046q79 0 134.5 55.5t55.5 133.5zM1798 438q0 -142 -100.5 -242t-242.5 -100h-1046q-169 0 -289 119.5t-120 288.5q0 153 100 267t249 136q62 184 221 298t354 114q235 0 408.5 -158.5t196.5 -389.5 q116 -25 192.5 -118.5t76.5 -214.5zM2048 438q0 -175 -97 -319q-23 -33 -64 -33q-24 0 -43 13q-26 17 -32 48.5t12 57.5q71 104 71 233t-71 233q-18 26 -12 57t32 49t57.5 11.5t49.5 -32.5q97 -142 97 -318zM2304 438q0 -244 -134 -443q-23 -34 -64 -34q-23 0 -42 13 q-26 18 -32.5 49t11.5 57q108 164 108 358q0 195 -108 357q-18 26 -11.5 57.5t32.5 48.5q26 18 57 12t49 -33q134 -198 134 -442z" />
+<glyph unicode="&#xf28a;" d="M1500 -13q0 -89 -63 -152.5t-153 -63.5t-153.5 63.5t-63.5 152.5q0 90 63.5 153.5t153.5 63.5t153 -63.5t63 -153.5zM1267 268q-115 -15 -192.5 -102.5t-77.5 -205.5q0 -74 33 -138q-146 -78 -379 -78q-109 0 -201 21t-153.5 54.5t-110.5 76.5t-76 85t-44.5 83 t-23.5 66.5t-6 39.5q0 19 4.5 42.5t18.5 56t36.5 58t64 43.5t94.5 18t94 -17.5t63 -41t35.5 -53t17.5 -49t4 -33.5q0 -34 -23 -81q28 -27 82 -42t93 -17l40 -1q115 0 190 51t75 133q0 26 -9 48.5t-31.5 44.5t-49.5 41t-74 44t-93.5 47.5t-119.5 56.5q-28 13 -43 20 q-116 55 -187 100t-122.5 102t-72 125.5t-20.5 162.5q0 78 20.5 150t66 137.5t112.5 114t166.5 77t221.5 28.5q120 0 220 -26t164.5 -67t109.5 -94t64 -105.5t19 -103.5q0 -46 -15 -82.5t-36.5 -58t-48.5 -36t-49 -19.5t-39 -5h-8h-32t-39 5t-44 14t-41 28t-37 46t-24 70.5 t-10 97.5q-15 16 -59 25.5t-81 10.5l-37 1q-68 0 -117.5 -31t-70.5 -70t-21 -76q0 -24 5 -43t24 -46t53 -51t97 -53.5t150 -58.5q76 -25 138.5 -53.5t109 -55.5t83 -59t60.5 -59.5t41 -62.5t26.5 -62t14.5 -63.5t6 -62t1 -62.5z" />
+<glyph unicode="&#xf28b;" d="M704 352v576q0 14 -9 23t-23 9h-256q-14 0 -23 -9t-9 -23v-576q0 -14 9 -23t23 -9h256q14 0 23 9t9 23zM1152 352v576q0 14 -9 23t-23 9h-256q-14 0 -23 -9t-9 -23v-576q0 -14 9 -23t23 -9h256q14 0 23 9t9 23zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103 t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" />
+<glyph unicode="&#xf28c;" d="M768 1408q209 0 385.5 -103t279.5 -279.5t103 -385.5t-103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103zM768 96q148 0 273 73t198 198t73 273t-73 273t-198 198t-273 73t-273 -73t-198 -198t-73 -273 t73 -273t198 -198t273 -73zM864 320q-14 0 -23 9t-9 23v576q0 14 9 23t23 9h192q14 0 23 -9t9 -23v-576q0 -14 -9 -23t-23 -9h-192zM480 320q-14 0 -23 9t-9 23v576q0 14 9 23t23 9h192q14 0 23 -9t9 -23v-576q0 -14 -9 -23t-23 -9h-192z" />
+<glyph unicode="&#xf28d;" d="M1088 352v576q0 14 -9 23t-23 9h-576q-14 0 -23 -9t-9 -23v-576q0 -14 9 -23t23 -9h576q14 0 23 9t9 23zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5 t103 -385.5z" />
+<glyph unicode="&#xf28e;" d="M768 1408q209 0 385.5 -103t279.5 -279.5t103 -385.5t-103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103zM768 96q148 0 273 73t198 198t73 273t-73 273t-198 198t-273 73t-273 -73t-198 -198t-73 -273 t73 -273t198 -198t273 -73zM480 320q-14 0 -23 9t-9 23v576q0 14 9 23t23 9h576q14 0 23 -9t9 -23v-576q0 -14 -9 -23t-23 -9h-576z" />
+<glyph unicode="&#xf290;" horiz-adv-x="1792" d="M1757 128l35 -313q3 -28 -16 -50q-19 -21 -48 -21h-1664q-29 0 -48 21q-19 22 -16 50l35 313h1722zM1664 967l86 -775h-1708l86 775q3 24 21 40.5t43 16.5h256v-128q0 -53 37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5v128h384v-128q0 -53 37.5 -90.5t90.5 -37.5 t90.5 37.5t37.5 90.5v128h256q25 0 43 -16.5t21 -40.5zM1280 1152v-256q0 -26 -19 -45t-45 -19t-45 19t-19 45v256q0 106 -75 181t-181 75t-181 -75t-75 -181v-256q0 -26 -19 -45t-45 -19t-45 19t-19 45v256q0 159 112.5 271.5t271.5 112.5t271.5 -112.5t112.5 -271.5z" />
+<glyph unicode="&#xf291;" horiz-adv-x="2048" d="M1920 768q53 0 90.5 -37.5t37.5 -90.5t-37.5 -90.5t-90.5 -37.5h-15l-115 -662q-8 -46 -44 -76t-82 -30h-1280q-46 0 -82 30t-44 76l-115 662h-15q-53 0 -90.5 37.5t-37.5 90.5t37.5 90.5t90.5 37.5h1792zM485 -32q26 2 43.5 22.5t15.5 46.5l-32 416q-2 26 -22.5 43.5 t-46.5 15.5t-43.5 -22.5t-15.5 -46.5l32 -416q2 -25 20.5 -42t43.5 -17h5zM896 32v416q0 26 -19 45t-45 19t-45 -19t-19 -45v-416q0 -26 19 -45t45 -19t45 19t19 45zM1280 32v416q0 26 -19 45t-45 19t-45 -19t-19 -45v-416q0 -26 19 -45t45 -19t45 19t19 45zM1632 27l32 416 q2 26 -15.5 46.5t-43.5 22.5t-46.5 -15.5t-22.5 -43.5l-32 -416q-2 -26 15.5 -46.5t43.5 -22.5h5q25 0 43.5 17t20.5 42zM476 1244l-93 -412h-132l101 441q19 88 89 143.5t160 55.5h167q0 26 19 45t45 19h384q26 0 45 -19t19 -45h167q90 0 160 -55.5t89 -143.5l101 -441 h-132l-93 412q-11 44 -45.5 72t-79.5 28h-167q0 -26 -19 -45t-45 -19h-384q-26 0 -45 19t-19 45h-167q-45 0 -79.5 -28t-45.5 -72z" />
+<glyph unicode="&#xf292;" horiz-adv-x="1792" d="M991 512l64 256h-254l-64 -256h254zM1759 1016l-56 -224q-7 -24 -31 -24h-327l-64 -256h311q15 0 25 -12q10 -14 6 -28l-56 -224q-5 -24 -31 -24h-327l-81 -328q-7 -24 -31 -24h-224q-16 0 -26 12q-9 12 -6 28l78 312h-254l-81 -328q-7 -24 -31 -24h-225q-15 0 -25 12 q-9 12 -6 28l78 312h-311q-15 0 -25 12q-9 12 -6 28l56 224q7 24 31 24h327l64 256h-311q-15 0 -25 12q-10 14 -6 28l56 224q5 24 31 24h327l81 328q7 24 32 24h224q15 0 25 -12q9 -12 6 -28l-78 -312h254l81 328q7 24 32 24h224q15 0 25 -12q9 -12 6 -28l-78 -312h311 q15 0 25 -12q9 -12 6 -28z" />
+<glyph unicode="&#xf293;" d="M841 483l148 -148l-149 -149zM840 1094l149 -149l-148 -148zM710 -130l464 464l-306 306l306 306l-464 464v-611l-255 255l-93 -93l320 -321l-320 -321l93 -93l255 255v-611zM1429 640q0 -209 -32 -365.5t-87.5 -257t-140.5 -162.5t-181.5 -86.5t-219.5 -24.5 t-219.5 24.5t-181.5 86.5t-140.5 162.5t-87.5 257t-32 365.5t32 365.5t87.5 257t140.5 162.5t181.5 86.5t219.5 24.5t219.5 -24.5t181.5 -86.5t140.5 -162.5t87.5 -257t32 -365.5z" />
+<glyph unicode="&#xf294;" horiz-adv-x="1024" d="M596 113l173 172l-173 172v-344zM596 823l173 172l-173 172v-344zM628 640l356 -356l-539 -540v711l-297 -296l-108 108l372 373l-372 373l108 108l297 -296v711l539 -540z" />
+<glyph unicode="&#xf295;" d="M1280 256q0 52 -38 90t-90 38t-90 -38t-38 -90t38 -90t90 -38t90 38t38 90zM512 1024q0 52 -38 90t-90 38t-90 -38t-38 -90t38 -90t90 -38t90 38t38 90zM1536 256q0 -159 -112.5 -271.5t-271.5 -112.5t-271.5 112.5t-112.5 271.5t112.5 271.5t271.5 112.5t271.5 -112.5 t112.5 -271.5zM1440 1344q0 -20 -13 -38l-1056 -1408q-19 -26 -51 -26h-160q-26 0 -45 19t-19 45q0 20 13 38l1056 1408q19 26 51 26h160q26 0 45 -19t19 -45zM768 1024q0 -159 -112.5 -271.5t-271.5 -112.5t-271.5 112.5t-112.5 271.5t112.5 271.5t271.5 112.5 t271.5 -112.5t112.5 -271.5z" />
+<glyph unicode="&#xf296;" horiz-adv-x="1792" d="M104 830l792 -1015l-868 630q-18 13 -25 34.5t0 42.5l101 308v0zM566 830h660l-330 -1015v0zM368 1442l198 -612h-462l198 612q8 23 33 23t33 -23zM1688 830l101 -308q7 -21 0 -42.5t-25 -34.5l-868 -630l792 1015v0zM1688 830h-462l198 612q8 23 33 23t33 -23z" />
+<glyph unicode="&#xf297;" horiz-adv-x="1792" d="M384 704h160v224h-160v-224zM1221 372v92q-104 -36 -243 -38q-135 -1 -259.5 46.5t-220.5 122.5l1 -96q88 -80 212 -128.5t272 -47.5q129 0 238 49zM640 704h640v224h-640v-224zM1792 736q0 -187 -99 -352q89 -102 89 -229q0 -157 -129.5 -268t-313.5 -111 q-122 0 -225 52.5t-161 140.5q-19 -1 -57 -1t-57 1q-58 -88 -161 -140.5t-225 -52.5q-184 0 -313.5 111t-129.5 268q0 127 89 229q-99 165 -99 352q0 209 120 385.5t326.5 279.5t449.5 103t449.5 -103t326.5 -279.5t120 -385.5z" />
+<glyph unicode="&#xf298;" d="M515 625v-128h-252v128h252zM515 880v-127h-252v127h252zM1273 369v-128h-341v128h341zM1273 625v-128h-672v128h672zM1273 880v-127h-672v127h672zM1408 20v1240q0 8 -6 14t-14 6h-32l-378 -256l-210 171l-210 -171l-378 256h-32q-8 0 -14 -6t-6 -14v-1240q0 -8 6 -14 t14 -6h1240q8 0 14 6t6 14zM553 1130l185 150h-406zM983 1130l221 150h-406zM1536 1260v-1240q0 -62 -43 -105t-105 -43h-1240q-62 0 -105 43t-43 105v1240q0 62 43 105t105 43h1240q62 0 105 -43t43 -105z" />
+<glyph unicode="&#xf299;" horiz-adv-x="1792" d="M896 720q-104 196 -160 278q-139 202 -347 318q-34 19 -70 36q-89 40 -94 32t34 -38l39 -31q62 -43 112.5 -93.5t94.5 -116.5t70.5 -113t70.5 -131q9 -17 13 -25q44 -84 84 -153t98 -154t115.5 -150t131 -123.5t148.5 -90.5q153 -66 154 -60q1 3 -49 37q-53 36 -81 57 q-77 58 -179 211t-185 310zM549 177q-76 60 -132.5 125t-98 143.5t-71 154.5t-58.5 186t-52 209t-60.5 252t-76.5 289q273 0 497.5 -36t379 -92t271 -144.5t185.5 -172.5t110 -198.5t56 -199.5t12.5 -198.5t-9.5 -173t-20 -143.5t-13 -107l323 -327h-104l-281 285 q-22 -2 -91.5 -14t-121.5 -19t-138 -6t-160.5 17t-167.5 59t-179 111z" />
+<glyph unicode="&#xf29a;" horiz-adv-x="1792" d="M1374 879q-6 26 -28.5 39.5t-48.5 7.5q-261 -62 -401 -62t-401 62q-26 6 -48.5 -7.5t-28.5 -39.5t7.5 -48.5t39.5 -28.5q194 -46 303 -58q-2 -158 -15.5 -269t-26.5 -155.5t-41 -115.5l-9 -21q-10 -25 1 -49t36 -34q9 -4 23 -4q44 0 60 41l8 20q54 139 71 259h42 q17 -120 71 -259l8 -20q16 -41 60 -41q14 0 23 4q25 10 36 34t1 49l-9 21q-28 71 -41 115.5t-26.5 155.5t-15.5 269q109 12 303 58q26 6 39.5 28.5t7.5 48.5zM1024 1024q0 53 -37.5 90.5t-90.5 37.5t-90.5 -37.5t-37.5 -90.5t37.5 -90.5t90.5 -37.5t90.5 37.5t37.5 90.5z M1600 640q0 -143 -55.5 -273.5t-150 -225t-225 -150t-273.5 -55.5t-273.5 55.5t-225 150t-150 225t-55.5 273.5t55.5 273.5t150 225t225 150t273.5 55.5t273.5 -55.5t225 -150t150 -225t55.5 -273.5zM896 1408q-156 0 -298 -61t-245 -164t-164 -245t-61 -298t61 -298 t164 -245t245 -164t298 -61t298 61t245 164t164 245t61 298t-61 298t-164 245t-245 164t-298 61zM1792 640q0 -182 -71 -348t-191 -286t-286 -191t-348 -71t-348 71t-286 191t-191 286t-71 348t71 348t191 286t286 191t348 71t348 -71t286 -191t191 -286t71 -348z" />
+<glyph unicode="&#xf29b;" d="M1438 723q34 -35 29 -82l-44 -551q-4 -42 -34.5 -70t-71.5 -28q-6 0 -9 1q-44 3 -72.5 36.5t-25.5 77.5l35 429l-143 -8q55 -113 55 -240q0 -216 -148 -372l-137 137q91 101 91 235q0 145 -102.5 248t-247.5 103q-134 0 -236 -92l-137 138q120 114 284 141l264 300 l-149 87l-181 -161q-33 -30 -77 -27.5t-73 35.5t-26.5 77t34.5 73l239 213q26 23 60 26.5t64 -14.5l488 -283q36 -21 48 -68q17 -67 -26 -117l-205 -232l371 20q49 3 83 -32zM1240 1180q-74 0 -126 52t-52 126t52 126t126 52t126.5 -52t52.5 -126t-52.5 -126t-126.5 -52z M613 -62q106 0 196 61l139 -139q-146 -116 -335 -116q-148 0 -273.5 73t-198.5 198t-73 273q0 188 116 336l139 -139q-60 -88 -60 -197q0 -145 102.5 -247.5t247.5 -102.5z" />
+<glyph unicode="&#xf29c;" d="M880 336v-160q0 -14 -9 -23t-23 -9h-160q-14 0 -23 9t-9 23v160q0 14 9 23t23 9h160q14 0 23 -9t9 -23zM1136 832q0 -50 -15 -90t-45.5 -69t-52 -44t-59.5 -36q-32 -18 -46.5 -28t-26 -24t-11.5 -29v-32q0 -14 -9 -23t-23 -9h-160q-14 0 -23 9t-9 23v68q0 35 10.5 64.5 t24 47.5t39 35.5t41 25.5t44.5 21q53 25 75 43t22 49q0 42 -43.5 71.5t-95.5 29.5q-56 0 -95 -27q-29 -20 -80 -83q-9 -12 -25 -12q-11 0 -19 6l-108 82q-10 7 -12 20t5 23q122 192 349 192q129 0 238.5 -89.5t109.5 -214.5zM768 1280q-130 0 -248.5 -51t-204 -136.5 t-136.5 -204t-51 -248.5t51 -248.5t136.5 -204t204 -136.5t248.5 -51t248.5 51t204 136.5t136.5 204t51 248.5t-51 248.5t-136.5 204t-204 136.5t-248.5 51zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5 t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" />
+<glyph unicode="&#xf29d;" horiz-adv-x="1408" d="M366 1225q-64 0 -110 45.5t-46 110.5q0 64 46 109.5t110 45.5t109.5 -45.5t45.5 -109.5q0 -65 -45.5 -110.5t-109.5 -45.5zM917 583q0 -50 -30 -67.5t-63.5 -6.5t-47.5 34l-367 438q-7 12 -14 15.5t-11 1.5l-3 -3q-7 -8 4 -21l122 -139l1 -354l-161 -457 q-67 -192 -92 -234q-16 -26 -28 -32q-50 -26 -103 -1q-29 13 -41.5 43t-9.5 57q2 17 197 618l5 416l-85 -164l35 -222q4 -24 -1 -42t-14 -27.5t-19 -16t-17 -7.5l-7 -2q-19 -3 -34.5 3t-24 16t-14 22t-7.5 19.5t-2 9.5l-46 299l211 381q23 34 113 34q75 0 107 -40l424 -521 q7 -5 14 -17l3 -3l-1 -1q7 -13 7 -29zM514 433q43 -113 88.5 -225t69.5 -168l24 -55q36 -93 42 -125q11 -70 -36 -97q-35 -22 -66 -16t-51 22t-29 35h-1q-6 16 -8 25l-124 351zM1338 -159q31 -49 31 -57q0 -5 -3 -7q-9 -5 -14.5 0.5t-15.5 26t-16 30.5q-114 172 -423 661 q3 -1 7 1t7 4l3 2q11 9 11 17z" />
+<glyph unicode="&#xf29e;" horiz-adv-x="2304" d="M504 542h171l-1 265zM1530 641q0 87 -50.5 140t-146.5 53h-54v-388h52q91 0 145 57t54 138zM956 1018l1 -756q0 -14 -9.5 -24t-23.5 -10h-216q-14 0 -23.5 10t-9.5 24v62h-291l-55 -81q-10 -15 -28 -15h-267q-21 0 -30.5 18t3.5 35l556 757q9 14 27 14h332q14 0 24 -10 t10 -24zM1783 641q0 -193 -125.5 -303t-324.5 -110h-270q-14 0 -24 10t-10 24v756q0 14 10 24t24 10h268q200 0 326 -109t126 -302zM1939 640q0 -11 -0.5 -29t-8 -71.5t-21.5 -102t-44.5 -108t-73.5 -102.5h-51q38 45 66.5 104.5t41.5 112t21 98t9 72.5l1 27q0 8 -0.5 22.5 t-7.5 60t-20 91.5t-41 111.5t-66 124.5h43q41 -47 72 -107t45.5 -111.5t23 -96t10.5 -70.5zM2123 640q0 -11 -0.5 -29t-8 -71.5t-21.5 -102t-45 -108t-74 -102.5h-51q38 45 66.5 104.5t41.5 112t21 98t9 72.5l1 27q0 8 -0.5 22.5t-7.5 60t-19.5 91.5t-40.5 111.5t-66 124.5 h43q41 -47 72 -107t45.5 -111.5t23 -96t10.5 -70.5zM2304 640q0 -11 -0.5 -29t-8 -71.5t-21.5 -102t-44.5 -108t-73.5 -102.5h-51q38 45 66 104.5t41 112t21 98t9 72.5l1 27q0 8 -0.5 22.5t-7.5 60t-19.5 91.5t-40.5 111.5t-66 124.5h43q41 -47 72 -107t45.5 -111.5t23 -96 t9.5 -70.5z" />
+<glyph unicode="&#xf2a0;" horiz-adv-x="1408" d="M617 -153q0 11 -13 58t-31 107t-20 69q-1 4 -5 26.5t-8.5 36t-13.5 21.5q-15 14 -51 14q-23 0 -70 -5.5t-71 -5.5q-34 0 -47 11q-6 5 -11 15.5t-7.5 20t-6.5 24t-5 18.5q-37 128 -37 255t37 255q1 4 5 18.5t6.5 24t7.5 20t11 15.5q13 11 47 11q24 0 71 -5.5t70 -5.5 q36 0 51 14q9 8 13.5 21.5t8.5 36t5 26.5q2 9 20 69t31 107t13 58q0 22 -43.5 52.5t-75.5 42.5q-20 8 -45 8q-34 0 -98 -18q-57 -17 -96.5 -40.5t-71 -66t-46 -70t-45.5 -94.5q-6 -12 -9 -19q-49 -107 -68 -216t-19 -244t19 -244t68 -216q56 -122 83 -161q63 -91 179 -127 l6 -2q64 -18 98 -18q25 0 45 8q32 12 75.5 42.5t43.5 52.5zM776 760q-26 0 -45 19t-19 45.5t19 45.5q37 37 37 90q0 52 -37 91q-19 19 -19 45t19 45t45 19t45 -19q75 -75 75 -181t-75 -181q-21 -19 -45 -19zM957 579q-27 0 -45 19q-19 19 -19 45t19 45q112 114 112 272 t-112 272q-19 19 -19 45t19 45t45 19t45 -19q150 -150 150 -362t-150 -362q-18 -19 -45 -19zM1138 398q-27 0 -45 19q-19 19 -19 45t19 45q90 91 138.5 208t48.5 245t-48.5 245t-138.5 208q-19 19 -19 45t19 45t45 19t45 -19q109 -109 167 -249t58 -294t-58 -294t-167 -249 q-18 -19 -45 -19z" />
+<glyph unicode="&#xf2a1;" horiz-adv-x="2176" d="M192 352q-66 0 -113 -47t-47 -113t47 -113t113 -47t113 47t47 113t-47 113t-113 47zM704 352q-66 0 -113 -47t-47 -113t47 -113t113 -47t113 47t47 113t-47 113t-113 47zM704 864q-66 0 -113 -47t-47 -113t47 -113t113 -47t113 47t47 113t-47 113t-113 47zM1472 352 q-66 0 -113 -47t-47 -113t47 -113t113 -47t113 47t47 113t-47 113t-113 47zM1984 352q-66 0 -113 -47t-47 -113t47 -113t113 -47t113 47t47 113t-47 113t-113 47zM1472 864q-66 0 -113 -47t-47 -113t47 -113t113 -47t113 47t47 113t-47 113t-113 47zM1984 864 q-66 0 -113 -47t-47 -113t47 -113t113 -47t113 47t47 113t-47 113t-113 47zM1984 1376q-66 0 -113 -47t-47 -113t47 -113t113 -47t113 47t47 113t-47 113t-113 47zM384 192q0 -80 -56 -136t-136 -56t-136 56t-56 136t56 136t136 56t136 -56t56 -136zM896 192q0 -80 -56 -136 t-136 -56t-136 56t-56 136t56 136t136 56t136 -56t56 -136zM384 704q0 -80 -56 -136t-136 -56t-136 56t-56 136t56 136t136 56t136 -56t56 -136zM896 704q0 -80 -56 -136t-136 -56t-136 56t-56 136t56 136t136 56t136 -56t56 -136zM384 1216q0 -80 -56 -136t-136 -56 t-136 56t-56 136t56 136t136 56t136 -56t56 -136zM1664 192q0 -80 -56 -136t-136 -56t-136 56t-56 136t56 136t136 56t136 -56t56 -136zM896 1216q0 -80 -56 -136t-136 -56t-136 56t-56 136t56 136t136 56t136 -56t56 -136zM2176 192q0 -80 -56 -136t-136 -56t-136 56 t-56 136t56 136t136 56t136 -56t56 -136zM1664 704q0 -80 -56 -136t-136 -56t-136 56t-56 136t56 136t136 56t136 -56t56 -136zM2176 704q0 -80 -56 -136t-136 -56t-136 56t-56 136t56 136t136 56t136 -56t56 -136zM1664 1216q0 -80 -56 -136t-136 -56t-136 56t-56 136 t56 136t136 56t136 -56t56 -136zM2176 1216q0 -80 -56 -136t-136 -56t-136 56t-56 136t56 136t136 56t136 -56t56 -136z" />
+<glyph unicode="&#xf2a2;" horiz-adv-x="1792" d="M128 -192q0 -26 -19 -45t-45 -19t-45 19t-19 45t19 45t45 19t45 -19t19 -45zM320 0q0 -26 -19 -45t-45 -19t-45 19t-19 45t19 45t45 19t45 -19t19 -45zM365 365l256 -256l-90 -90l-256 256zM704 384q0 -26 -19 -45t-45 -19t-45 19t-19 45t19 45t45 19t45 -19t19 -45z M1411 704q0 -59 -11.5 -108.5t-37.5 -93.5t-44 -67.5t-53 -64.5q-31 -35 -45.5 -54t-33.5 -50t-26.5 -64t-7.5 -74q0 -159 -112.5 -271.5t-271.5 -112.5q-26 0 -45 19t-19 45t19 45t45 19q106 0 181 75t75 181q0 57 11.5 105.5t37 91t43.5 66.5t52 63q40 46 59.5 72 t37.5 74.5t18 103.5q0 185 -131.5 316.5t-316.5 131.5t-316.5 -131.5t-131.5 -316.5q0 -26 -19 -45t-45 -19t-45 19t-19 45q0 117 45.5 223.5t123 184t184 123t223.5 45.5t223.5 -45.5t184 -123t123 -184t45.5 -223.5zM896 576q0 -26 -19 -45t-45 -19t-45 19t-19 45t19 45 t45 19t45 -19t19 -45zM1184 704q0 -26 -19 -45t-45 -19t-45 19t-19 45q0 93 -65.5 158.5t-158.5 65.5q-92 0 -158 -65.5t-66 -158.5q0 -26 -19 -45t-45 -19t-45 19t-19 45q0 146 103 249t249 103t249 -103t103 -249zM1578 993q10 -25 -1 -49t-36 -34q-9 -4 -23 -4 q-19 0 -35.5 11t-23.5 30q-68 178 -224 295q-21 16 -25 42t12 47q17 21 43 25t47 -12q183 -137 266 -351zM1788 1074q9 -25 -1.5 -49t-35.5 -34q-11 -4 -23 -4q-44 0 -60 41q-92 238 -297 393q-22 16 -25.5 42t12.5 47q16 22 42 25.5t47 -12.5q235 -175 341 -449z" />
+<glyph unicode="&#xf2a3;" horiz-adv-x="2304" d="M1032 576q-59 2 -84 55q-17 34 -48 53.5t-68 19.5q-53 0 -90.5 -37.5t-37.5 -90.5q0 -56 36 -89l10 -8q34 -31 82 -31q37 0 68 19.5t48 53.5q25 53 84 55zM1600 704q0 56 -36 89l-10 8q-34 31 -82 31q-37 0 -68 -19.5t-48 -53.5q-25 -53 -84 -55q59 -2 84 -55 q17 -34 48 -53.5t68 -19.5q53 0 90.5 37.5t37.5 90.5zM1174 925q-17 -35 -55 -48t-73 4q-62 31 -134 31q-51 0 -99 -17q3 0 9.5 0.5t9.5 0.5q92 0 170.5 -50t118.5 -133q17 -36 3.5 -73.5t-49.5 -54.5q-18 -9 -39 -9q21 0 39 -9q36 -17 49.5 -54.5t-3.5 -73.5 q-40 -83 -118.5 -133t-170.5 -50h-6q-16 2 -44 4l-290 27l-239 -120q-14 -7 -29 -7q-40 0 -57 35l-160 320q-11 23 -4 47.5t29 37.5l209 119l148 267q17 155 91.5 291.5t195.5 236.5q31 25 70.5 21.5t64.5 -34.5t21.5 -70t-34.5 -65q-70 -59 -117 -128q123 84 267 101 q40 5 71.5 -19t35.5 -64q5 -40 -19 -71.5t-64 -35.5q-84 -10 -159 -55q46 10 99 10q115 0 218 -50q36 -18 49 -55.5t-5 -73.5zM2137 1085l160 -320q11 -23 4 -47.5t-29 -37.5l-209 -119l-148 -267q-17 -155 -91.5 -291.5t-195.5 -236.5q-26 -22 -61 -22q-45 0 -74 35 q-25 31 -21.5 70t34.5 65q70 59 117 128q-123 -84 -267 -101q-4 -1 -12 -1q-36 0 -63.5 24t-31.5 60q-5 40 19 71.5t64 35.5q84 10 159 55q-46 -10 -99 -10q-115 0 -218 50q-36 18 -49 55.5t5 73.5q17 35 55 48t73 -4q62 -31 134 -31q51 0 99 17q-3 0 -9.5 -0.5t-9.5 -0.5 q-92 0 -170.5 50t-118.5 133q-17 36 -3.5 73.5t49.5 54.5q18 9 39 9q-21 0 -39 9q-36 17 -49.5 54.5t3.5 73.5q40 83 118.5 133t170.5 50h6h1q14 -2 42 -4l291 -27l239 120q14 7 29 7q40 0 57 -35z" />
+<glyph unicode="&#xf2a4;" horiz-adv-x="1792" d="M1056 704q0 -26 19 -45t45 -19t45 19t19 45q0 146 -103 249t-249 103t-249 -103t-103 -249q0 -26 19 -45t45 -19t45 19t19 45q0 93 66 158.5t158 65.5t158 -65.5t66 -158.5zM835 1280q-117 0 -223.5 -45.5t-184 -123t-123 -184t-45.5 -223.5q0 -26 19 -45t45 -19t45 19 t19 45q0 185 131.5 316.5t316.5 131.5t316.5 -131.5t131.5 -316.5q0 -55 -18 -103.5t-37.5 -74.5t-59.5 -72q-34 -39 -52 -63t-43.5 -66.5t-37 -91t-11.5 -105.5q0 -106 -75 -181t-181 -75q-26 0 -45 -19t-19 -45t19 -45t45 -19q159 0 271.5 112.5t112.5 271.5q0 41 7.5 74 t26.5 64t33.5 50t45.5 54q35 41 53 64.5t44 67.5t37.5 93.5t11.5 108.5q0 117 -45.5 223.5t-123 184t-184 123t-223.5 45.5zM591 561l226 -226l-579 -579q-12 -12 -29 -12t-29 12l-168 168q-12 12 -12 29t12 29zM1612 1524l168 -168q12 -12 12 -29t-12 -30l-233 -233 l-26 -25l-71 -71q-66 153 -195 258l91 91l207 207q13 12 30 12t29 -12z" />
+<glyph unicode="&#xf2a5;" d="M866 1021q0 -27 -13 -94q-11 -50 -31.5 -150t-30.5 -150q-2 -11 -4.5 -12.5t-13.5 -2.5q-20 -2 -31 -2q-58 0 -84 49.5t-26 113.5q0 88 35 174t103 124q28 14 51 14q28 0 36.5 -16.5t8.5 -47.5zM1352 597q0 14 -39 75.5t-52 66.5q-21 8 -34 8q-91 0 -226 -77l-2 2 q3 22 27.5 135t24.5 178q0 233 -242 233q-24 0 -68 -6q-94 -17 -168.5 -89.5t-111.5 -166.5t-37 -189q0 -146 80.5 -225t227.5 -79q25 0 25 -3t-1 -5q-4 -34 -26 -117q-14 -52 -51.5 -101t-82.5 -49q-42 0 -42 47q0 24 10.5 47.5t25 39.5t29.5 28.5t26 20t11 8.5q0 3 -7 10 q-24 22 -58.5 36.5t-65.5 14.5q-35 0 -63.5 -34t-41 -75t-12.5 -75q0 -88 51.5 -142t138.5 -54q82 0 155 53t117.5 126t65.5 153q6 22 15.5 66.5t14.5 66.5q3 12 14 18q118 60 227 60q48 0 127 -18q1 -1 4 -1q5 0 9.5 4.5t4.5 8.5zM1536 1120v-960q0 -119 -84.5 -203.5 t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z" />
+<glyph unicode="&#xf2a6;" horiz-adv-x="1535" d="M744 1231q0 24 -2 38.5t-8.5 30t-21 23t-37.5 7.5q-39 0 -78 -23q-105 -58 -159 -190.5t-54 -269.5q0 -44 8.5 -85.5t26.5 -80.5t52.5 -62.5t81.5 -23.5q4 0 18 -0.5t20 0t16 3t15 8.5t7 16q16 77 48 231.5t48 231.5q19 91 19 146zM1498 575q0 -7 -7.5 -13.5t-15.5 -6.5 l-6 1q-22 3 -62 11t-72 12.5t-63 4.5q-167 0 -351 -93q-15 -8 -21 -27q-10 -36 -24.5 -105.5t-22.5 -100.5q-23 -91 -70 -179.5t-112.5 -164.5t-154.5 -123t-185 -47q-135 0 -214.5 83.5t-79.5 219.5q0 53 19.5 117t63 116.5t97.5 52.5q38 0 120 -33.5t83 -61.5 q0 -1 -16.5 -12.5t-39.5 -31t-46 -44.5t-39 -61t-16 -74q0 -33 16.5 -53t48.5 -20q45 0 85 31.5t66.5 78t48 105.5t32.5 107t16 90v9q0 2 -3.5 3.5t-8.5 1.5h-10t-10 -0.5t-6 -0.5q-227 0 -352 122.5t-125 348.5q0 108 34.5 221t96 210t156 167.5t204.5 89.5q52 9 106 9 q374 0 374 -360q0 -98 -38 -273t-43 -211l3 -3q101 57 182.5 88t167.5 31q22 0 53 -13q19 -7 80 -102.5t61 -116.5z" />
+<glyph unicode="&#xf2a7;" horiz-adv-x="1664" d="M831 863q32 0 59 -18l222 -148q61 -40 110 -97l146 -170q40 -46 29 -106l-72 -413q-6 -32 -29.5 -53.5t-55.5 -25.5l-527 -56l-352 -32h-9q-39 0 -67.5 28t-28.5 68q0 37 27 64t65 32l260 32h-448q-41 0 -69.5 30t-26.5 71q2 39 32 65t69 26l442 1l-521 64q-41 5 -66 37 t-19 73q6 35 34.5 57.5t65.5 22.5h10l481 -60l-351 94q-38 10 -62 41.5t-18 68.5q6 36 33 58.5t62 22.5q6 0 20 -2l448 -96l217 -37q1 0 3 -0.5t3 -0.5q23 0 30.5 23t-12.5 36l-186 125q-35 23 -42 63.5t18 73.5q27 38 76 38zM761 661l186 -125l-218 37l-5 2l-36 38 l-238 262q-1 1 -2.5 3.5t-2.5 3.5q-24 31 -18.5 70t37.5 64q31 23 68 17.5t64 -33.5l142 -147l-4 -4t-5 -4q-32 -45 -23 -99t55 -85zM1648 1115l15 -266q4 -73 -11 -147l-48 -219q-12 -59 -67 -87l-106 -54q2 62 -39 109l-146 170q-53 61 -117 103l-222 148q-34 23 -76 23 q-51 0 -88 -37l-235 312q-25 33 -18 73.5t41 63.5q33 22 71.5 14t62.5 -40l266 -352l-262 455q-21 35 -10.5 75t47.5 59q35 18 72.5 6t57.5 -46l241 -420l-136 337q-15 35 -4.5 74t44.5 56q37 19 76 6t56 -51l193 -415l101 -196q8 -15 23 -17.5t27 7.5t11 26l-12 224 q-2 41 26 71t69 31q39 0 67 -28.5t30 -67.5z" />
+<glyph unicode="&#xf2a8;" horiz-adv-x="1792" d="M335 180q-2 0 -6 2q-86 57 -168.5 145t-139.5 180q-21 30 -21 69q0 9 2 19t4 18t7 18t8.5 16t10.5 17t10 15t12 15.5t11 14.5q184 251 452 365q-110 198 -110 211q0 19 17 29q116 64 128 64q18 0 28 -16l124 -229q92 19 192 19q266 0 497.5 -137.5t378.5 -369.5 q20 -31 20 -69t-20 -69q-91 -142 -218.5 -253.5t-278.5 -175.5q110 -198 110 -211q0 -20 -17 -29q-116 -64 -127 -64q-19 0 -29 16l-124 229l-64 119l-444 820l7 7q-58 -24 -99 -47q3 -5 127 -234t243 -449t119 -223q0 -7 -9 -9q-13 -3 -72 -3q-57 0 -60 7l-456 841 q-39 -28 -82 -68q24 -43 214 -393.5t190 -354.5q0 -10 -11 -10q-14 0 -82.5 22t-72.5 28l-106 197l-224 413q-44 -53 -78 -106q2 -3 18 -25t23 -34l176 -327q0 -10 -10 -10zM1165 282l49 -91q273 111 450 385q-180 277 -459 389q67 -64 103 -148.5t36 -176.5 q0 -106 -47 -200.5t-132 -157.5zM848 896q0 -20 14 -34t34 -14q86 0 147 -61t61 -147q0 -20 14 -34t34 -14t34 14t14 34q0 126 -89 215t-215 89q-20 0 -34 -14t-14 -34zM1214 961l-9 4l7 -7z" />
+<glyph unicode="&#xf2a9;" horiz-adv-x="1280" d="M1050 430q0 -215 -147 -374q-148 -161 -378 -161q-232 0 -378 161q-147 159 -147 374q0 147 68 270.5t189 196.5t268 73q96 0 182 -31q-32 -62 -39 -126q-66 28 -143 28q-167 0 -280.5 -123t-113.5 -291q0 -170 112.5 -288.5t281.5 -118.5t281 118.5t112 288.5 q0 89 -32 166q66 13 123 49q41 -98 41 -212zM846 619q0 -192 -79.5 -345t-238.5 -253l-14 -1q-29 0 -62 5q83 32 146.5 102.5t99.5 154.5t58.5 189t30 192.5t7.5 178.5q0 69 -3 103q55 -160 55 -326zM791 947v-2q-73 214 -206 440q88 -59 142.5 -186.5t63.5 -251.5z M1035 744q-83 0 -160 75q218 120 290 247q19 37 21 56q-42 -94 -139.5 -166.5t-204.5 -97.5q-35 54 -35 113q0 37 17 79t43 68q46 44 157 74q59 16 106 58.5t74 100.5q74 -105 74 -253q0 -109 -24 -170q-32 -77 -88.5 -130.5t-130.5 -53.5z" />
+<glyph unicode="&#xf2aa;" d="M1050 495q0 78 -28 147q-41 -25 -85 -34q22 -50 22 -114q0 -117 -77 -198.5t-193 -81.5t-193.5 81.5t-77.5 198.5q0 115 78 199.5t193 84.5q53 0 98 -19q4 43 27 87q-60 21 -125 21q-154 0 -257.5 -108.5t-103.5 -263.5t103.5 -261t257.5 -106t257.5 106.5t103.5 260.5z M872 850q2 -24 2 -71q0 -63 -5 -123t-20.5 -132.5t-40.5 -130t-68.5 -106t-100.5 -70.5q21 -3 42 -3h10q219 139 219 411q0 116 -38 225zM872 850q-4 80 -44 171.5t-98 130.5q92 -156 142 -302zM1207 955q0 102 -51 174q-41 -86 -124 -109q-69 -19 -109 -53.5t-40 -99.5 q0 -40 24 -77q74 17 140.5 67t95.5 115q-4 -52 -74.5 -111.5t-138.5 -97.5q52 -52 110 -52q51 0 90 37t60 90q17 43 17 117zM1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5 t84.5 -203.5z" />
+<glyph unicode="&#xf2ab;" d="M1279 388q0 22 -22 27q-67 15 -118 59t-80 108q-7 19 -7 25q0 15 19.5 26t43 17t43 20.5t19.5 36.5q0 19 -18.5 31.5t-38.5 12.5q-12 0 -32 -8t-31 -8q-4 0 -12 2q5 95 5 114q0 79 -17 114q-36 78 -103 121.5t-152 43.5q-199 0 -275 -165q-17 -35 -17 -114q0 -19 5 -114 q-4 -2 -14 -2q-12 0 -32 7.5t-30 7.5q-21 0 -38.5 -12t-17.5 -32q0 -21 19.5 -35.5t43 -20.5t43 -17t19.5 -26q0 -6 -7 -25q-64 -138 -198 -167q-22 -5 -22 -27q0 -46 137 -68q2 -5 6 -26t11.5 -30.5t23.5 -9.5q12 0 37.5 4.5t39.5 4.5q35 0 67 -15t54 -32.5t57.5 -32.5 t76.5 -15q43 0 79 15t57.5 32.5t53.5 32.5t67 15q14 0 39.5 -4t38.5 -4q16 0 23 10t11 30t6 25q137 22 137 68zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5 t103 -385.5z" />
+<glyph unicode="&#xf2ac;" horiz-adv-x="1664" d="M848 1408q134 1 240.5 -68.5t163.5 -192.5q27 -58 27 -179q0 -47 -9 -191q14 -7 28 -7q18 0 51 13.5t51 13.5q29 0 56 -18t27 -46q0 -32 -31.5 -54t-69 -31.5t-69 -29t-31.5 -47.5q0 -15 12 -43q37 -82 102.5 -150t144.5 -101q28 -12 80 -23q28 -6 28 -35 q0 -70 -219 -103q-7 -11 -11 -39t-14 -46.5t-33 -18.5q-20 0 -62 6.5t-64 6.5q-37 0 -62 -5q-32 -5 -63 -22.5t-58 -38t-58 -40.5t-76 -33.5t-99 -13.5q-52 0 -96.5 13.5t-75 33.5t-57.5 40.5t-58 38t-62 22.5q-26 5 -63 5q-24 0 -65.5 -7.5t-58.5 -7.5q-25 0 -35 18.5 t-14 47.5t-11 40q-219 33 -219 103q0 29 28 35q52 11 80 23q78 32 144.5 101t102.5 150q12 28 12 43q0 28 -31.5 47.5t-69.5 29.5t-69.5 31.5t-31.5 52.5q0 27 26 45.5t55 18.5q15 0 48 -13t53 -13q18 0 32 7q-9 142 -9 190q0 122 27 180q64 137 172 198t264 63z" />
+<glyph unicode="&#xf2ad;" d="M1280 388q0 22 -22 27q-67 14 -118 58t-80 109q-7 14 -7 25q0 15 19.5 26t42.5 17t42.5 20.5t19.5 36.5q0 19 -18.5 31.5t-38.5 12.5q-11 0 -31 -8t-32 -8q-4 0 -12 2q5 63 5 115q0 78 -17 114q-36 78 -102.5 121.5t-152.5 43.5q-198 0 -275 -165q-18 -38 -18 -115 q0 -38 6 -114q-10 -2 -15 -2q-11 0 -31.5 8t-30.5 8q-20 0 -37.5 -12.5t-17.5 -32.5q0 -21 19.5 -35.5t42.5 -20.5t42.5 -17t19.5 -26q0 -11 -7 -25q-64 -138 -198 -167q-22 -5 -22 -27q0 -47 138 -69q2 -5 6 -26t11 -30.5t23 -9.5q13 0 38.5 5t38.5 5q35 0 67.5 -15 t54.5 -32.5t57.5 -32.5t76.5 -15q43 0 79 15t57.5 32.5t54 32.5t67.5 15q13 0 39 -4.5t39 -4.5q15 0 22.5 9.5t11.5 31t5 24.5q138 22 138 69zM1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960 q119 0 203.5 -84.5t84.5 -203.5z" />
+<glyph unicode="&#xf2ae;" horiz-adv-x="2304" d="M2304 1536q-69 -46 -125 -92t-89 -81t-59.5 -71.5t-37.5 -57.5t-22 -44.5t-14 -29.5q-10 -18 -35.5 -136.5t-48.5 -164.5q-15 -29 -50 -60.5t-67.5 -50.5t-72.5 -41t-48 -28q-47 -31 -151 -231q-341 14 -630 -158q-92 -53 -303 -179q47 16 86 31t55 22l15 7 q71 27 163 64.5t133.5 53.5t108 34.5t142.5 31.5q186 31 465 -7q1 0 10 -3q11 -6 14 -17t-3 -22l-194 -345q-15 -29 -47 -22q-128 24 -354 24q-146 0 -402 -44.5t-392 -46.5q-82 -1 -149 13t-107 37t-61 40t-33 34l-1 1v2q0 6 6 6q138 0 371 55q192 366 374.5 524t383.5 158 q5 0 14.5 -0.5t38 -5t55 -12t61.5 -24.5t63 -39.5t54 -59t40 -82.5l102 177q2 4 21 42.5t44.5 86.5t61 109.5t84 133.5t100.5 137q66 82 128 141.5t121.5 96.5t92.5 53.5t88 39.5z" />
+<glyph unicode="&#xf2b0;" d="M1322 640q0 -45 -5 -76l-236 14l224 -78q-19 -73 -58 -141l-214 103l177 -158q-44 -61 -107 -108l-157 178l103 -215q-61 -37 -140 -59l-79 228l14 -240q-38 -6 -76 -6t-76 6l14 238l-78 -226q-74 19 -140 59l103 215l-157 -178q-59 43 -108 108l178 158l-214 -104 q-39 69 -58 141l224 79l-237 -14q-5 42 -5 76q0 35 5 77l238 -14l-225 79q19 73 58 140l214 -104l-177 159q46 61 107 108l158 -178l-103 215q67 39 140 58l77 -224l-13 236q36 6 75 6q38 0 76 -6l-14 -237l78 225q74 -19 140 -59l-103 -214l158 178q61 -47 107 -108 l-177 -159l213 104q37 -62 58 -141l-224 -78l237 14q5 -31 5 -77zM1352 640q0 160 -78.5 295.5t-213 214t-292.5 78.5q-119 0 -227 -46.5t-186.5 -125t-124.5 -187.5t-46 -229q0 -119 46 -228t124.5 -187.5t186.5 -125t227 -46.5q158 0 292.5 78.5t213 214t78.5 294.5z M1425 1023v-766l-657 -383l-657 383v766l657 383zM768 -183l708 412v823l-708 411l-708 -411v-823zM1536 1088v-896l-768 -448l-768 448v896l768 448z" />
+<glyph unicode="&#xf2b1;" horiz-adv-x="1664" d="M339 1318h691l-26 -72h-665q-110 0 -188.5 -79t-78.5 -189v-771q0 -95 60.5 -169.5t153.5 -93.5q23 -5 98 -5v-72h-45q-140 0 -239.5 100t-99.5 240v771q0 140 99.5 240t239.5 100zM1190 1536h247l-482 -1294q-23 -61 -40.5 -103.5t-45 -98t-54 -93.5t-64.5 -78.5 t-79.5 -65t-95.5 -41t-116 -18.5v195q163 26 220 182q20 52 20 105q0 54 -20 106l-285 733h228l187 -585zM1664 978v-1111h-795q37 55 45 73h678v1038q0 85 -49.5 155t-129.5 99l25 67q101 -34 163.5 -123.5t62.5 -197.5z" />
+<glyph unicode="&#xf2b2;" horiz-adv-x="1792" d="M852 1227q0 -29 -17 -52.5t-45 -23.5t-45 23.5t-17 52.5t17 52.5t45 23.5t45 -23.5t17 -52.5zM688 -149v114q0 30 -20.5 51.5t-50.5 21.5t-50 -21.5t-20 -51.5v-114q0 -30 20.5 -52t49.5 -22q30 0 50.5 22t20.5 52zM860 -149v114q0 30 -20 51.5t-50 21.5t-50.5 -21.5 t-20.5 -51.5v-114q0 -30 20.5 -52t50.5 -22q29 0 49.5 22t20.5 52zM1034 -149v114q0 30 -20.5 51.5t-50.5 21.5t-50.5 -21.5t-20.5 -51.5v-114q0 -30 20.5 -52t50.5 -22t50.5 22t20.5 52zM1208 -149v114q0 30 -20.5 51.5t-50.5 21.5t-50.5 -21.5t-20.5 -51.5v-114 q0 -30 20.5 -52t50.5 -22t50.5 22t20.5 52zM1476 535q-84 -160 -232 -259.5t-323 -99.5q-123 0 -229.5 51.5t-178.5 137t-113 197.5t-41 232q0 88 21 174q-104 -175 -104 -390q0 -162 65 -312t185 -251q30 57 91 57q56 0 86 -50q32 50 87 50q56 0 86 -50q32 50 87 50t87 -50 q30 50 86 50q28 0 52.5 -15.5t37.5 -40.5q112 94 177 231.5t73 287.5zM1326 564q0 75 -72 75q-17 0 -47 -6q-95 -19 -149 -19q-226 0 -226 243q0 86 30 204q-83 -127 -83 -275q0 -150 89 -260.5t235 -110.5q111 0 210 70q13 48 13 79zM884 1223q0 50 -32 89.5t-81 39.5 t-81 -39.5t-32 -89.5q0 -51 31.5 -90.5t81.5 -39.5t81.5 39.5t31.5 90.5zM1513 884q0 96 -37.5 179t-113 137t-173.5 54q-77 0 -149 -35t-127 -94q-48 -159 -48 -268q0 -104 45.5 -157t147.5 -53q53 0 142 19q36 6 53 6q51 0 77.5 -28t26.5 -80q0 -26 -4 -46 q75 68 117.5 165.5t42.5 200.5zM1792 667q0 -111 -33.5 -249.5t-93.5 -204.5q-58 -64 -195 -142.5t-228 -104.5l-4 -1v-114q0 -43 -29.5 -75t-72.5 -32q-56 0 -86 50q-32 -50 -87 -50t-87 50q-30 -50 -86 -50q-55 0 -87 50q-30 -50 -86 -50q-47 0 -75 33.5t-28 81.5 q-90 -68 -198 -68q-118 0 -211 80q54 1 106 20q-113 31 -182 127q32 -7 71 -7q89 0 164 46q-192 192 -240 306q-24 56 -24 160q0 57 9 125.5t31.5 146.5t55 141t86.5 105t120 42q59 0 81 -52q19 29 42 54q2 3 12 13t13 16q10 15 23 38t25 42t28 39q87 111 211.5 177 t260.5 66q35 0 62 -4q59 64 146 64q83 0 140 -57q5 -5 5 -12q0 -5 -6 -13.5t-12.5 -16t-16 -17l-10.5 -10.5q17 -6 36 -18t19 -24q0 -6 -16 -25q157 -138 197 -378q25 30 60 30q45 0 100 -49q90 -80 90 -279z" />
+<glyph unicode="&#xf2b3;" d="M917 631q0 33 -6 64h-362v-132h217q-12 -76 -74.5 -120.5t-142.5 -44.5q-99 0 -169 71.5t-70 170.5t70 170.5t169 71.5q93 0 153 -59l104 101q-108 100 -257 100q-160 0 -272 -112.5t-112 -271.5t112 -271.5t272 -112.5q165 0 266.5 105t101.5 270zM1262 585h109v110 h-109v110h-110v-110h-110v-110h110v-110h110v110zM1536 640q0 -209 -103 -385.5t-279.5 -279.5t-385.5 -103t-385.5 103t-279.5 279.5t-103 385.5t103 385.5t279.5 279.5t385.5 103t385.5 -103t279.5 -279.5t103 -385.5z" />
+<glyph unicode="&#xf2b4;" d="M1536 1024v-839q0 -48 -49 -62q-174 -52 -338 -52q-73 0 -215.5 29.5t-227.5 29.5q-164 0 -370 -48v-338h-160v1368q-63 25 -101 81t-38 124q0 91 64 155t155 64t155 -64t64 -155q0 -68 -38 -124t-101 -81v-68q190 44 343 44q99 0 198 -15q14 -2 111.5 -22.5t149.5 -20.5 q77 0 165 18q11 2 80 21t89 19q26 0 45 -19t19 -45z" />
+<glyph unicode="&#xf2b5;" horiz-adv-x="1792" />
+<glyph unicode="&#xf2b6;" horiz-adv-x="1792" />
+<glyph unicode="&#xf2b7;" horiz-adv-x="1792" />
+<glyph unicode="&#xf2b8;" horiz-adv-x="1792" />
+<glyph unicode="&#xf2b9;" horiz-adv-x="1792" />
+<glyph unicode="&#xf2ba;" horiz-adv-x="1792" />
+<glyph unicode="&#xf2bb;" horiz-adv-x="1792" />
+<glyph unicode="&#xf2bc;" horiz-adv-x="1792" />
+<glyph unicode="&#xf2bd;" horiz-adv-x="1792" />
+<glyph unicode="&#xf2be;" horiz-adv-x="1792" />
 <glyph unicode="&#xf500;" horiz-adv-x="1792" />
 </font>
 </defs></svg> 
\ No newline at end of file
diff --git a/themes/bootstrap3/css/fonts/fontawesome-webfont.ttf b/themes/bootstrap3/css/fonts/fontawesome-webfont.ttf
index 96a3639cdde5e8ab459c6380e3b9524ee81641dc..f221e50a2ef60738ba30932d834530cdfe55cb3e 100644
Binary files a/themes/bootstrap3/css/fonts/fontawesome-webfont.ttf and b/themes/bootstrap3/css/fonts/fontawesome-webfont.ttf differ
diff --git a/themes/bootstrap3/css/fonts/fontawesome-webfont.woff b/themes/bootstrap3/css/fonts/fontawesome-webfont.woff
index 628b6a52a87e62c6f22426e17c01f6a303aa194e..6e7483cf61b490c08ed644d6ef802c69472eb247 100644
Binary files a/themes/bootstrap3/css/fonts/fontawesome-webfont.woff and b/themes/bootstrap3/css/fonts/fontawesome-webfont.woff differ
diff --git a/themes/bootstrap3/css/fonts/fontawesome-webfont.woff2 b/themes/bootstrap3/css/fonts/fontawesome-webfont.woff2
new file mode 100644
index 0000000000000000000000000000000000000000..7eb74fd127ee5eddf3b95fee6a20dc1684b0963b
Binary files /dev/null and b/themes/bootstrap3/css/fonts/fontawesome-webfont.woff2 differ
diff --git a/themes/bootstrap3/css/print.css b/themes/bootstrap3/css/print.css
index e12eed5ceacd58f0fd198b64f704f23224c5d59b..ed5f07cbc820edcade024cd14a681b8f16e9f535 100644
--- a/themes/bootstrap3/css/print.css
+++ b/themes/bootstrap3/css/print.css
@@ -1,6 +1,9 @@
-.hidden-print,.hidden-print *,.hidden-print[class*="span"] {display:none}
-.row-fluid .span9 {margin:auto;width:90%}
-a[href]:after{content:''}
-* {background-color:#FFF}
-.container {margin:0}
-#datevispublishDatexWrapper {width:90%}
\ No newline at end of file
+* {
+  background-color: #FFF;
+  color: #000;
+}
+a[href]:after{ content: ''; } /* hide following urls */
+.container { margin:0; } /* print margins */
+/* print utilities */
+.hidden-print,
+.hidden-print * { display:none !important; }
\ No newline at end of file
diff --git a/themes/bootstrap3/css/vendor/bootstrap-rtl.min.css b/themes/bootstrap3/css/vendor/bootstrap-rtl.min.css
index 94a629ea171054b497e26ee494fa91daa19fe8e6..6653eba1e1409fc6aadbaed5e14f484f9b5ed3fb 100644
--- a/themes/bootstrap3/css/vendor/bootstrap-rtl.min.css
+++ b/themes/bootstrap3/css/vendor/bootstrap-rtl.min.css
@@ -1,12 +1,12 @@
 /*******************************************************************************
- *              bootstrap-rtl (version 3.3.1)
+ *              bootstrap-rtl (version 3.3.4)
  *      Author: Morteza Ansarinia (http://github.com/morteza)
- *  Created on: January 21,2015
+ *  Created on: August 13,2015
  *     Project: bootstrap-rtl
  *   Copyright: Unlicensed Public Domain
  *******************************************************************************/
 
-html{direction:rtl}body{direction:rtl}.list-unstyled{padding-right:0;padding-left:initial}.list-inline{padding-right:0;padding-left:initial;margin-right:-5px;margin-left:0}dd{margin-right:0;margin-left:initial}@media (min-width:768px){.dl-horizontal dt{float:right;clear:right;text-align:left}.dl-horizontal dd{margin-right:180px;margin-left:0}}blockquote{border-right:5px solid #eee;border-left:0}.blockquote-reverse,blockquote.pull-left{padding-left:15px;padding-right:0;border-left:5px solid #eee;border-right:0;text-align:left}.col-xs-1,.col-sm-1,.col-md-1,.col-lg-1,.col-xs-2,.col-sm-2,.col-md-2,.col-lg-2,.col-xs-3,.col-sm-3,.col-md-3,.col-lg-3,.col-xs-4,.col-sm-4,.col-md-4,.col-lg-4,.col-xs-5,.col-sm-5,.col-md-5,.col-lg-5,.col-xs-6,.col-sm-6,.col-md-6,.col-lg-6,.col-xs-7,.col-sm-7,.col-md-7,.col-lg-7,.col-xs-8,.col-sm-8,.col-md-8,.col-lg-8,.col-xs-9,.col-sm-9,.col-md-9,.col-lg-9,.col-xs-10,.col-sm-10,.col-md-10,.col-lg-10,.col-xs-11,.col-sm-11,.col-md-11,.col-lg-11,.col-xs-12,.col-sm-12,.col-md-12,.col-lg-12{position:relative;min-height:1px;padding-left:15px;padding-right:15px}.col-xs-1,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9,.col-xs-10,.col-xs-11,.col-xs-12{float:right}.col-xs-12{width:100%}.col-xs-11{width:91.66666667%}.col-xs-10{width:83.33333333%}.col-xs-9{width:75%}.col-xs-8{width:66.66666667%}.col-xs-7{width:58.33333333%}.col-xs-6{width:50%}.col-xs-5{width:41.66666667%}.col-xs-4{width:33.33333333%}.col-xs-3{width:25%}.col-xs-2{width:16.66666667%}.col-xs-1{width:8.33333333%}.col-xs-pull-12{left:100%;right:auto}.col-xs-pull-11{left:91.66666667%;right:auto}.col-xs-pull-10{left:83.33333333%;right:auto}.col-xs-pull-9{left:75%;right:auto}.col-xs-pull-8{left:66.66666667%;right:auto}.col-xs-pull-7{left:58.33333333%;right:auto}.col-xs-pull-6{left:50%;right:auto}.col-xs-pull-5{left:41.66666667%;right:auto}.col-xs-pull-4{left:33.33333333%;right:auto}.col-xs-pull-3{left:25%;right:auto}.col-xs-pull-2{left:16.66666667%;right:auto}.col-xs-pull-1{left:8.33333333%;right:auto}.col-xs-pull-0{left:auto;right:auto}.col-xs-push-12{right:100%;left:0}.col-xs-push-11{right:91.66666667%;left:0}.col-xs-push-10{right:83.33333333%;left:0}.col-xs-push-9{right:75%;left:0}.col-xs-push-8{right:66.66666667%;left:0}.col-xs-push-7{right:58.33333333%;left:0}.col-xs-push-6{right:50%;left:0}.col-xs-push-5{right:41.66666667%;left:0}.col-xs-push-4{right:33.33333333%;left:0}.col-xs-push-3{right:25%;left:0}.col-xs-push-2{right:16.66666667%;left:0}.col-xs-push-1{right:8.33333333%;left:0}.col-xs-push-0{right:auto;left:0}.col-xs-offset-12{margin-right:100%;margin-left:0}.col-xs-offset-11{margin-right:91.66666667%;margin-left:0}.col-xs-offset-10{margin-right:83.33333333%;margin-left:0}.col-xs-offset-9{margin-right:75%;margin-left:0}.col-xs-offset-8{margin-right:66.66666667%;margin-left:0}.col-xs-offset-7{margin-right:58.33333333%;margin-left:0}.col-xs-offset-6{margin-right:50%;margin-left:0}.col-xs-offset-5{margin-right:41.66666667%;margin-left:0}.col-xs-offset-4{margin-right:33.33333333%;margin-left:0}.col-xs-offset-3{margin-right:25%;margin-left:0}.col-xs-offset-2{margin-right:16.66666667%;margin-left:0}.col-xs-offset-1{margin-right:8.33333333%;margin-left:0}.col-xs-offset-0{margin-right:0;margin-left:0}@media (min-width:768px){.col-sm-1,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-sm-10,.col-sm-11,.col-sm-12{float:right}.col-sm-12{width:100%}.col-sm-11{width:91.66666667%}.col-sm-10{width:83.33333333%}.col-sm-9{width:75%}.col-sm-8{width:66.66666667%}.col-sm-7{width:58.33333333%}.col-sm-6{width:50%}.col-sm-5{width:41.66666667%}.col-sm-4{width:33.33333333%}.col-sm-3{width:25%}.col-sm-2{width:16.66666667%}.col-sm-1{width:8.33333333%}.col-sm-pull-12{left:100%;right:auto}.col-sm-pull-11{left:91.66666667%;right:auto}.col-sm-pull-10{left:83.33333333%;right:auto}.col-sm-pull-9{left:75%;right:auto}.col-sm-pull-8{left:66.66666667%;right:auto}.col-sm-pull-7{left:58.33333333%;right:auto}.col-sm-pull-6{left:50%;right:auto}.col-sm-pull-5{left:41.66666667%;right:auto}.col-sm-pull-4{left:33.33333333%;right:auto}.col-sm-pull-3{left:25%;right:auto}.col-sm-pull-2{left:16.66666667%;right:auto}.col-sm-pull-1{left:8.33333333%;right:auto}.col-sm-pull-0{left:auto;right:auto}.col-sm-push-12{right:100%;left:0}.col-sm-push-11{right:91.66666667%;left:0}.col-sm-push-10{right:83.33333333%;left:0}.col-sm-push-9{right:75%;left:0}.col-sm-push-8{right:66.66666667%;left:0}.col-sm-push-7{right:58.33333333%;left:0}.col-sm-push-6{right:50%;left:0}.col-sm-push-5{right:41.66666667%;left:0}.col-sm-push-4{right:33.33333333%;left:0}.col-sm-push-3{right:25%;left:0}.col-sm-push-2{right:16.66666667%;left:0}.col-sm-push-1{right:8.33333333%;left:0}.col-sm-push-0{right:auto;left:0}.col-sm-offset-12{margin-right:100%;margin-left:0}.col-sm-offset-11{margin-right:91.66666667%;margin-left:0}.col-sm-offset-10{margin-right:83.33333333%;margin-left:0}.col-sm-offset-9{margin-right:75%;margin-left:0}.col-sm-offset-8{margin-right:66.66666667%;margin-left:0}.col-sm-offset-7{margin-right:58.33333333%;margin-left:0}.col-sm-offset-6{margin-right:50%;margin-left:0}.col-sm-offset-5{margin-right:41.66666667%;margin-left:0}.col-sm-offset-4{margin-right:33.33333333%;margin-left:0}.col-sm-offset-3{margin-right:25%;margin-left:0}.col-sm-offset-2{margin-right:16.66666667%;margin-left:0}.col-sm-offset-1{margin-right:8.33333333%;margin-left:0}.col-sm-offset-0{margin-right:0;margin-left:0}}@media (min-width:992px){.col-md-1,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-md-10,.col-md-11,.col-md-12{float:right}.col-md-12{width:100%}.col-md-11{width:91.66666667%}.col-md-10{width:83.33333333%}.col-md-9{width:75%}.col-md-8{width:66.66666667%}.col-md-7{width:58.33333333%}.col-md-6{width:50%}.col-md-5{width:41.66666667%}.col-md-4{width:33.33333333%}.col-md-3{width:25%}.col-md-2{width:16.66666667%}.col-md-1{width:8.33333333%}.col-md-pull-12{left:100%;right:auto}.col-md-pull-11{left:91.66666667%;right:auto}.col-md-pull-10{left:83.33333333%;right:auto}.col-md-pull-9{left:75%;right:auto}.col-md-pull-8{left:66.66666667%;right:auto}.col-md-pull-7{left:58.33333333%;right:auto}.col-md-pull-6{left:50%;right:auto}.col-md-pull-5{left:41.66666667%;right:auto}.col-md-pull-4{left:33.33333333%;right:auto}.col-md-pull-3{left:25%;right:auto}.col-md-pull-2{left:16.66666667%;right:auto}.col-md-pull-1{left:8.33333333%;right:auto}.col-md-pull-0{left:auto;right:auto}.col-md-push-12{right:100%;left:0}.col-md-push-11{right:91.66666667%;left:0}.col-md-push-10{right:83.33333333%;left:0}.col-md-push-9{right:75%;left:0}.col-md-push-8{right:66.66666667%;left:0}.col-md-push-7{right:58.33333333%;left:0}.col-md-push-6{right:50%;left:0}.col-md-push-5{right:41.66666667%;left:0}.col-md-push-4{right:33.33333333%;left:0}.col-md-push-3{right:25%;left:0}.col-md-push-2{right:16.66666667%;left:0}.col-md-push-1{right:8.33333333%;left:0}.col-md-push-0{right:auto;left:0}.col-md-offset-12{margin-right:100%;margin-left:0}.col-md-offset-11{margin-right:91.66666667%;margin-left:0}.col-md-offset-10{margin-right:83.33333333%;margin-left:0}.col-md-offset-9{margin-right:75%;margin-left:0}.col-md-offset-8{margin-right:66.66666667%;margin-left:0}.col-md-offset-7{margin-right:58.33333333%;margin-left:0}.col-md-offset-6{margin-right:50%;margin-left:0}.col-md-offset-5{margin-right:41.66666667%;margin-left:0}.col-md-offset-4{margin-right:33.33333333%;margin-left:0}.col-md-offset-3{margin-right:25%;margin-left:0}.col-md-offset-2{margin-right:16.66666667%;margin-left:0}.col-md-offset-1{margin-right:8.33333333%;margin-left:0}.col-md-offset-0{margin-right:0;margin-left:0}}@media (min-width:1200px){.col-lg-1,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-lg-10,.col-lg-11,.col-lg-12{float:right}.col-lg-12{width:100%}.col-lg-11{width:91.66666667%}.col-lg-10{width:83.33333333%}.col-lg-9{width:75%}.col-lg-8{width:66.66666667%}.col-lg-7{width:58.33333333%}.col-lg-6{width:50%}.col-lg-5{width:41.66666667%}.col-lg-4{width:33.33333333%}.col-lg-3{width:25%}.col-lg-2{width:16.66666667%}.col-lg-1{width:8.33333333%}.col-lg-pull-12{left:100%;right:auto}.col-lg-pull-11{left:91.66666667%;right:auto}.col-lg-pull-10{left:83.33333333%;right:auto}.col-lg-pull-9{left:75%;right:auto}.col-lg-pull-8{left:66.66666667%;right:auto}.col-lg-pull-7{left:58.33333333%;right:auto}.col-lg-pull-6{left:50%;right:auto}.col-lg-pull-5{left:41.66666667%;right:auto}.col-lg-pull-4{left:33.33333333%;right:auto}.col-lg-pull-3{left:25%;right:auto}.col-lg-pull-2{left:16.66666667%;right:auto}.col-lg-pull-1{left:8.33333333%;right:auto}.col-lg-pull-0{left:auto;right:auto}.col-lg-push-12{right:100%;left:0}.col-lg-push-11{right:91.66666667%;left:0}.col-lg-push-10{right:83.33333333%;left:0}.col-lg-push-9{right:75%;left:0}.col-lg-push-8{right:66.66666667%;left:0}.col-lg-push-7{right:58.33333333%;left:0}.col-lg-push-6{right:50%;left:0}.col-lg-push-5{right:41.66666667%;left:0}.col-lg-push-4{right:33.33333333%;left:0}.col-lg-push-3{right:25%;left:0}.col-lg-push-2{right:16.66666667%;left:0}.col-lg-push-1{right:8.33333333%;left:0}.col-lg-push-0{right:auto;left:0}.col-lg-offset-12{margin-right:100%;margin-left:0}.col-lg-offset-11{margin-right:91.66666667%;margin-left:0}.col-lg-offset-10{margin-right:83.33333333%;margin-left:0}.col-lg-offset-9{margin-right:75%;margin-left:0}.col-lg-offset-8{margin-right:66.66666667%;margin-left:0}.col-lg-offset-7{margin-right:58.33333333%;margin-left:0}.col-lg-offset-6{margin-right:50%;margin-left:0}.col-lg-offset-5{margin-right:41.66666667%;margin-left:0}.col-lg-offset-4{margin-right:33.33333333%;margin-left:0}.col-lg-offset-3{margin-right:25%;margin-left:0}.col-lg-offset-2{margin-right:16.66666667%;margin-left:0}.col-lg-offset-1{margin-right:8.33333333%;margin-left:0}.col-lg-offset-0{margin-right:0;margin-left:0}}caption{text-align:right}th{text-align:right}@media screen and (max-width:767px){.table-responsive>.table-bordered{border:0}.table-responsive>.table-bordered>thead>tr>th:first-child,.table-responsive>.table-bordered>tbody>tr>th:first-child,.table-responsive>.table-bordered>tfoot>tr>th:first-child,.table-responsive>.table-bordered>thead>tr>td:first-child,.table-responsive>.table-bordered>tbody>tr>td:first-child,.table-responsive>.table-bordered>tfoot>tr>td:first-child{border-right:0;border-left:initial}.table-responsive>.table-bordered>thead>tr>th:last-child,.table-responsive>.table-bordered>tbody>tr>th:last-child,.table-responsive>.table-bordered>tfoot>tr>th:last-child,.table-responsive>.table-bordered>thead>tr>td:last-child,.table-responsive>.table-bordered>tbody>tr>td:last-child,.table-responsive>.table-bordered>tfoot>tr>td:last-child{border-left:0;border-right:initial}}.radio label,.checkbox label{padding-right:20px;padding-left:initial}.radio input[type=radio],.radio-inline input[type=radio],.checkbox input[type=checkbox],.checkbox-inline input[type=checkbox]{margin-right:-20px;margin-left:auto}.radio-inline,.checkbox-inline{padding-right:20px;padding-left:0}.radio-inline+.radio-inline,.checkbox-inline+.checkbox-inline{margin-right:10px;margin-left:0}.has-feedback .form-control{padding-left:42.5px;padding-right:12px}.form-control-feedback{left:0;right:auto}@media (min-width:768px){.form-inline label{padding-right:0;padding-left:initial}.form-inline .radio input[type=radio],.form-inline .checkbox input[type=checkbox]{margin-right:0;margin-left:auto}}@media (min-width:768px){.form-horizontal .control-label{text-align:left}}.form-horizontal .has-feedback .form-control-feedback{left:15px;right:auto}.caret{margin-right:2px;margin-left:0}.dropdown-menu{right:0;left:auto;float:left;text-align:right}.dropdown-menu.pull-right{left:0;right:auto;float:right}.dropdown-menu-right{left:auto;right:0}.dropdown-menu-left{left:0;right:auto}@media (min-width:768px){.navbar-right .dropdown-menu{left:auto;right:0}.navbar-right .dropdown-menu-left{left:0;right:auto}}.btn-group>.btn,.btn-group-vertical>.btn{float:right}.btn-group .btn+.btn,.btn-group .btn+.btn-group,.btn-group .btn-group+.btn,.btn-group .btn-group+.btn-group{margin-right:-1px;margin-left:0}.btn-toolbar{margin-right:-5px;margin-left:0}.btn-toolbar .btn-group,.btn-toolbar .input-group{float:right}.btn-toolbar>.btn,.btn-toolbar>.btn-group,.btn-toolbar>.input-group{margin-right:5px;margin-left:0}.btn-group>.btn:first-child{margin-right:0}.btn-group>.btn:first-child:not(:last-child):not(.dropdown-toggle){border-top-right-radius:4px;border-bottom-right-radius:4px;border-bottom-left-radius:0;border-top-left-radius:0}.btn-group>.btn:last-child:not(:first-child),.btn-group>.dropdown-toggle:not(:first-child){border-top-left-radius:4px;border-bottom-left-radius:4px;border-bottom-right-radius:0;border-top-right-radius:0}.btn-group>.btn-group{float:right}.btn-group.btn-group-justified>.btn,.btn-group.btn-group-justified>.btn-group{float:none}.btn-group>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group>.btn-group:first-child>.btn:last-child,.btn-group>.btn-group:first-child>.dropdown-toggle{border-top-right-radius:4px;border-bottom-right-radius:4px;border-bottom-left-radius:0;border-top-left-radius:0}.btn-group>.btn-group:last-child>.btn:first-child{border-top-left-radius:4px;border-bottom-left-radius:4px;border-bottom-right-radius:0;border-top-right-radius:0}.btn .caret{margin-right:0}.btn-group-vertical>.btn+.btn,.btn-group-vertical>.btn+.btn-group,.btn-group-vertical>.btn-group+.btn,.btn-group-vertical>.btn-group+.btn-group{margin-top:-1px;margin-right:0}.input-group .form-control{float:right}.input-group .form-control:first-child,.input-group-addon:first-child,.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group>.btn,.input-group-btn:first-child>.dropdown-toggle,.input-group-btn:last-child>.btn:not(:last-child):not(.dropdown-toggle),.input-group-btn:last-child>.btn-group:not(:last-child)>.btn{border-bottom-right-radius:4px;border-top-right-radius:4px;border-bottom-left-radius:0;border-top-left-radius:0}.input-group-addon:first-child{border-right-width:1px;border-right-style:solid;border-left:0}.input-group .form-control:last-child,.input-group-addon:last-child,.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group>.btn,.input-group-btn:last-child>.dropdown-toggle,.input-group-btn:first-child>.btn:not(:first-child),.input-group-btn:first-child>.btn-group:not(:first-child)>.btn{border-bottom-left-radius:4px;border-top-left-radius:4px;border-bottom-right-radius:0;border-top-right-radius:0}.input-group-addon:last-child{border-left-width:1px;border-left-style:solid;border-right:0}.input-group-btn>.btn+.btn{margin-right:-1px;margin-left:auto}.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group{margin-left:-1px;margin-right:auto}.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group{margin-right:-1px;margin-left:auto}.nav{padding-right:0;padding-left:initial}.nav-tabs>li{float:right}.nav-tabs>li>a{margin-left:auto;margin-right:-2px;border-radius:4px 4px 0 0}.nav-pills>li{float:right}.nav-pills>li>a{border-radius:4px}.nav-pills>li+li{margin-right:2px;margin-left:auto}.nav-stacked>li{float:none}.nav-stacked>li+li{margin-right:0;margin-left:auto}.nav-justified>.dropdown .dropdown-menu{right:auto}.nav-tabs-justified>li>a{margin-left:0;margin-right:auto}@media (min-width:768px){.nav-tabs-justified>li>a{border-radius:4px 4px 0 0}}@media (min-width:768px){.navbar-header{float:right}}.navbar-collapse{padding-right:15px;padding-left:15px}.navbar-brand{float:right}@media (min-width:768px){.navbar>.container .navbar-brand,.navbar>.container-fluid .navbar-brand{margin-right:-15px;margin-left:auto}}.navbar-toggle{float:left;margin-left:15px;margin-right:auto}@media (max-width:767px){.navbar-nav .open .dropdown-menu>li>a,.navbar-nav .open .dropdown-menu .dropdown-header{padding:5px 25px 5px 15px}}@media (min-width:768px){.navbar-nav{float:right}.navbar-nav>li{float:right}}@media (min-width:768px){.navbar-left.flip{float:right!important}.navbar-right:last-child{margin-left:-15px;margin-right:auto}.navbar-right.flip{float:left!important;margin-left:-15px;margin-right:auto}.navbar-right .dropdown-menu{left:0;right:auto}}@media (min-width:768px){.navbar-text{float:right}.navbar-text.navbar-right:last-child{margin-left:0;margin-right:auto}}.pagination{padding-right:0}.pagination>li>a,.pagination>li>span{float:right;margin-right:-1px;margin-left:0}.pagination>li:first-child>a,.pagination>li:first-child>span{margin-left:0;border-bottom-right-radius:4px;border-top-right-radius:4px;border-bottom-left-radius:0;border-top-left-radius:0}.pagination>li:last-child>a,.pagination>li:last-child>span{margin-right:-1px;border-bottom-left-radius:4px;border-top-left-radius:4px;border-bottom-right-radius:0;border-top-right-radius:0}.pager{padding-right:0;padding-left:initial}.pager .next>a,.pager .next>span{float:left}.pager .previous>a,.pager .previous>span{float:right}.nav-pills>li>a>.badge{margin-left:0;margin-right:3px}.list-group-item>.badge{float:left}.list-group-item>.badge+.badge{margin-left:5px;margin-right:auto}.alert-dismissable,.alert-dismissible{padding-left:35px;padding-right:15px}.alert-dismissable .close,.alert-dismissible .close{right:auto;left:-21px}.progress-bar{float:right}.media>.pull-left{margin-right:10px}.media>.pull-left.flip{margin-right:0;margin-left:10px}.media>.pull-right{margin-left:10px}.media>.pull-right.flip{margin-left:0;margin-right:10px}.media-right,.media>.pull-right{padding-right:10px;padding-left:initial}.media-left,.media>.pull-left{padding-left:10px;padding-right:initial}.media-list{padding-right:0;padding-left:initial;list-style:none}.list-group{padding-right:0;padding-left:initial}.panel>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:first-child{border-top-right-radius:3px;border-top-left-radius:0}.panel>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:last-child{border-top-left-radius:3px;border-top-right-radius:0}.panel>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:first-child{border-bottom-left-radius:3px;border-top-right-radius:0}.panel>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:last-child{border-bottom-right-radius:3px;border-top-left-radius:0}.panel>.table-bordered>thead>tr>th:first-child,.panel>.table-responsive>.table-bordered>thead>tr>th:first-child,.panel>.table-bordered>tbody>tr>th:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:first-child,.panel>.table-bordered>tfoot>tr>th:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:first-child,.panel>.table-bordered>thead>tr>td:first-child,.panel>.table-responsive>.table-bordered>thead>tr>td:first-child,.panel>.table-bordered>tbody>tr>td:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:first-child,.panel>.table-bordered>tfoot>tr>td:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:first-child{border-right:0;border-left:none}.panel>.table-bordered>thead>tr>th:last-child,.panel>.table-responsive>.table-bordered>thead>tr>th:last-child,.panel>.table-bordered>tbody>tr>th:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:last-child,.panel>.table-bordered>tfoot>tr>th:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:last-child,.panel>.table-bordered>thead>tr>td:last-child,.panel>.table-responsive>.table-bordered>thead>tr>td:last-child,.panel>.table-bordered>tbody>tr>td:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:last-child,.panel>.table-bordered>tfoot>tr>td:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:last-child{border-right:none;border-left:0}.embed-responsive .embed-responsive-item,.embed-responsive iframe,.embed-responsive embed,.embed-responsive object{right:0;left:auto}.close{float:left}.modal-footer{text-align:left}.modal-footer .btn+.btn{margin-left:auto;margin-right:5px}.modal-footer .btn-group .btn+.btn{margin-right:-1px;margin-left:auto}.modal-footer .btn-block+.btn-block{margin-right:0;margin-left:auto}.popover{left:auto;text-align:right}.popover.top>.arrow{right:50%;left:auto;margin-right:-11px;margin-left:auto}.popover.top>.arrow:after{margin-right:-10px;margin-left:auto}.popover.bottom>.arrow{right:50%;left:auto;margin-right:-11px;margin-left:auto}.popover.bottom>.arrow:after{margin-right:-10px;margin-left:auto}.carousel-control{right:0;bottom:0}.carousel-control.left{right:auto;left:0;background-image:-webkit-linear-gradient(left,color-stop(rgba(0,0,0,.5)0),color-stop(rgba(0,0,0,.0001)100%));background-image:-o-linear-gradient(left,rgba(0,0,0,.5)0,rgba(0,0,0,.0001)100%);background-image:linear-gradient(to right,rgba(0,0,0,.5)0,rgba(0,0,0,.0001)100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1)}.carousel-control.right{left:auto;right:0;background-image:-webkit-linear-gradient(left,color-stop(rgba(0,0,0,.0001)0),color-stop(rgba(0,0,0,.5)100%));background-image:-o-linear-gradient(left,rgba(0,0,0,.0001)0,rgba(0,0,0,.5)100%);background-image:linear-gradient(to right,rgba(0,0,0,.0001)0,rgba(0,0,0,.5)100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1)}.carousel-control .icon-prev,.carousel-control .glyphicon-chevron-left{left:50%;right:auto;margin-right:-10px}.carousel-control .icon-next,.carousel-control .glyphicon-chevron-right{right:50%;left:auto;margin-left:-10px}.carousel-indicators{right:50%;left:0;margin-right:-30%;margin-left:0;padding-left:0}@media screen and (min-width:768px){.carousel-control .glyphicon-chevron-left,.carousel-control .icon-prev{margin-left:0;margin-right:-15px}.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next{margin-left:0;margin-right:-15px}.carousel-caption{left:20%;right:20%;padding-bottom:30px}}.pull-right.flip{float:left!important}.pull-left.flip{float:right!important}
+html{direction:rtl}body{direction:rtl}.flip.text-left{text-align:right}.flip.text-right{text-align:left}.list-unstyled{padding-right:0;padding-left:initial}.list-inline{padding-right:0;padding-left:initial;margin-right:-5px;margin-left:0}dd{margin-right:0;margin-left:initial}@media (min-width:768px){.dl-horizontal dt{float:right;clear:right;text-align:left}.dl-horizontal dd{margin-right:180px;margin-left:0}}blockquote{border-right:5px solid #eee;border-left:0}.blockquote-reverse,blockquote.pull-left{padding-left:15px;padding-right:0;border-left:5px solid #eee;border-right:0;text-align:left}.col-xs-1,.col-sm-1,.col-md-1,.col-lg-1,.col-xs-2,.col-sm-2,.col-md-2,.col-lg-2,.col-xs-3,.col-sm-3,.col-md-3,.col-lg-3,.col-xs-4,.col-sm-4,.col-md-4,.col-lg-4,.col-xs-5,.col-sm-5,.col-md-5,.col-lg-5,.col-xs-6,.col-sm-6,.col-md-6,.col-lg-6,.col-xs-7,.col-sm-7,.col-md-7,.col-lg-7,.col-xs-8,.col-sm-8,.col-md-8,.col-lg-8,.col-xs-9,.col-sm-9,.col-md-9,.col-lg-9,.col-xs-10,.col-sm-10,.col-md-10,.col-lg-10,.col-xs-11,.col-sm-11,.col-md-11,.col-lg-11,.col-xs-12,.col-sm-12,.col-md-12,.col-lg-12{position:relative;min-height:1px;padding-left:15px;padding-right:15px}.col-xs-1,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9,.col-xs-10,.col-xs-11,.col-xs-12{float:right}.col-xs-12{width:100%}.col-xs-11{width:91.66666667%}.col-xs-10{width:83.33333333%}.col-xs-9{width:75%}.col-xs-8{width:66.66666667%}.col-xs-7{width:58.33333333%}.col-xs-6{width:50%}.col-xs-5{width:41.66666667%}.col-xs-4{width:33.33333333%}.col-xs-3{width:25%}.col-xs-2{width:16.66666667%}.col-xs-1{width:8.33333333%}.col-xs-pull-12{left:100%;right:auto}.col-xs-pull-11{left:91.66666667%;right:auto}.col-xs-pull-10{left:83.33333333%;right:auto}.col-xs-pull-9{left:75%;right:auto}.col-xs-pull-8{left:66.66666667%;right:auto}.col-xs-pull-7{left:58.33333333%;right:auto}.col-xs-pull-6{left:50%;right:auto}.col-xs-pull-5{left:41.66666667%;right:auto}.col-xs-pull-4{left:33.33333333%;right:auto}.col-xs-pull-3{left:25%;right:auto}.col-xs-pull-2{left:16.66666667%;right:auto}.col-xs-pull-1{left:8.33333333%;right:auto}.col-xs-pull-0{left:auto;right:auto}.col-xs-push-12{right:100%;left:0}.col-xs-push-11{right:91.66666667%;left:0}.col-xs-push-10{right:83.33333333%;left:0}.col-xs-push-9{right:75%;left:0}.col-xs-push-8{right:66.66666667%;left:0}.col-xs-push-7{right:58.33333333%;left:0}.col-xs-push-6{right:50%;left:0}.col-xs-push-5{right:41.66666667%;left:0}.col-xs-push-4{right:33.33333333%;left:0}.col-xs-push-3{right:25%;left:0}.col-xs-push-2{right:16.66666667%;left:0}.col-xs-push-1{right:8.33333333%;left:0}.col-xs-push-0{right:auto;left:0}.col-xs-offset-12{margin-right:100%;margin-left:0}.col-xs-offset-11{margin-right:91.66666667%;margin-left:0}.col-xs-offset-10{margin-right:83.33333333%;margin-left:0}.col-xs-offset-9{margin-right:75%;margin-left:0}.col-xs-offset-8{margin-right:66.66666667%;margin-left:0}.col-xs-offset-7{margin-right:58.33333333%;margin-left:0}.col-xs-offset-6{margin-right:50%;margin-left:0}.col-xs-offset-5{margin-right:41.66666667%;margin-left:0}.col-xs-offset-4{margin-right:33.33333333%;margin-left:0}.col-xs-offset-3{margin-right:25%;margin-left:0}.col-xs-offset-2{margin-right:16.66666667%;margin-left:0}.col-xs-offset-1{margin-right:8.33333333%;margin-left:0}.col-xs-offset-0{margin-right:0;margin-left:0}@media (min-width:768px){.col-sm-1,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-sm-10,.col-sm-11,.col-sm-12{float:right}.col-sm-12{width:100%}.col-sm-11{width:91.66666667%}.col-sm-10{width:83.33333333%}.col-sm-9{width:75%}.col-sm-8{width:66.66666667%}.col-sm-7{width:58.33333333%}.col-sm-6{width:50%}.col-sm-5{width:41.66666667%}.col-sm-4{width:33.33333333%}.col-sm-3{width:25%}.col-sm-2{width:16.66666667%}.col-sm-1{width:8.33333333%}.col-sm-pull-12{left:100%;right:auto}.col-sm-pull-11{left:91.66666667%;right:auto}.col-sm-pull-10{left:83.33333333%;right:auto}.col-sm-pull-9{left:75%;right:auto}.col-sm-pull-8{left:66.66666667%;right:auto}.col-sm-pull-7{left:58.33333333%;right:auto}.col-sm-pull-6{left:50%;right:auto}.col-sm-pull-5{left:41.66666667%;right:auto}.col-sm-pull-4{left:33.33333333%;right:auto}.col-sm-pull-3{left:25%;right:auto}.col-sm-pull-2{left:16.66666667%;right:auto}.col-sm-pull-1{left:8.33333333%;right:auto}.col-sm-pull-0{left:auto;right:auto}.col-sm-push-12{right:100%;left:0}.col-sm-push-11{right:91.66666667%;left:0}.col-sm-push-10{right:83.33333333%;left:0}.col-sm-push-9{right:75%;left:0}.col-sm-push-8{right:66.66666667%;left:0}.col-sm-push-7{right:58.33333333%;left:0}.col-sm-push-6{right:50%;left:0}.col-sm-push-5{right:41.66666667%;left:0}.col-sm-push-4{right:33.33333333%;left:0}.col-sm-push-3{right:25%;left:0}.col-sm-push-2{right:16.66666667%;left:0}.col-sm-push-1{right:8.33333333%;left:0}.col-sm-push-0{right:auto;left:0}.col-sm-offset-12{margin-right:100%;margin-left:0}.col-sm-offset-11{margin-right:91.66666667%;margin-left:0}.col-sm-offset-10{margin-right:83.33333333%;margin-left:0}.col-sm-offset-9{margin-right:75%;margin-left:0}.col-sm-offset-8{margin-right:66.66666667%;margin-left:0}.col-sm-offset-7{margin-right:58.33333333%;margin-left:0}.col-sm-offset-6{margin-right:50%;margin-left:0}.col-sm-offset-5{margin-right:41.66666667%;margin-left:0}.col-sm-offset-4{margin-right:33.33333333%;margin-left:0}.col-sm-offset-3{margin-right:25%;margin-left:0}.col-sm-offset-2{margin-right:16.66666667%;margin-left:0}.col-sm-offset-1{margin-right:8.33333333%;margin-left:0}.col-sm-offset-0{margin-right:0;margin-left:0}}@media (min-width:992px){.col-md-1,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-md-10,.col-md-11,.col-md-12{float:right}.col-md-12{width:100%}.col-md-11{width:91.66666667%}.col-md-10{width:83.33333333%}.col-md-9{width:75%}.col-md-8{width:66.66666667%}.col-md-7{width:58.33333333%}.col-md-6{width:50%}.col-md-5{width:41.66666667%}.col-md-4{width:33.33333333%}.col-md-3{width:25%}.col-md-2{width:16.66666667%}.col-md-1{width:8.33333333%}.col-md-pull-12{left:100%;right:auto}.col-md-pull-11{left:91.66666667%;right:auto}.col-md-pull-10{left:83.33333333%;right:auto}.col-md-pull-9{left:75%;right:auto}.col-md-pull-8{left:66.66666667%;right:auto}.col-md-pull-7{left:58.33333333%;right:auto}.col-md-pull-6{left:50%;right:auto}.col-md-pull-5{left:41.66666667%;right:auto}.col-md-pull-4{left:33.33333333%;right:auto}.col-md-pull-3{left:25%;right:auto}.col-md-pull-2{left:16.66666667%;right:auto}.col-md-pull-1{left:8.33333333%;right:auto}.col-md-pull-0{left:auto;right:auto}.col-md-push-12{right:100%;left:0}.col-md-push-11{right:91.66666667%;left:0}.col-md-push-10{right:83.33333333%;left:0}.col-md-push-9{right:75%;left:0}.col-md-push-8{right:66.66666667%;left:0}.col-md-push-7{right:58.33333333%;left:0}.col-md-push-6{right:50%;left:0}.col-md-push-5{right:41.66666667%;left:0}.col-md-push-4{right:33.33333333%;left:0}.col-md-push-3{right:25%;left:0}.col-md-push-2{right:16.66666667%;left:0}.col-md-push-1{right:8.33333333%;left:0}.col-md-push-0{right:auto;left:0}.col-md-offset-12{margin-right:100%;margin-left:0}.col-md-offset-11{margin-right:91.66666667%;margin-left:0}.col-md-offset-10{margin-right:83.33333333%;margin-left:0}.col-md-offset-9{margin-right:75%;margin-left:0}.col-md-offset-8{margin-right:66.66666667%;margin-left:0}.col-md-offset-7{margin-right:58.33333333%;margin-left:0}.col-md-offset-6{margin-right:50%;margin-left:0}.col-md-offset-5{margin-right:41.66666667%;margin-left:0}.col-md-offset-4{margin-right:33.33333333%;margin-left:0}.col-md-offset-3{margin-right:25%;margin-left:0}.col-md-offset-2{margin-right:16.66666667%;margin-left:0}.col-md-offset-1{margin-right:8.33333333%;margin-left:0}.col-md-offset-0{margin-right:0;margin-left:0}}@media (min-width:1200px){.col-lg-1,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-lg-10,.col-lg-11,.col-lg-12{float:right}.col-lg-12{width:100%}.col-lg-11{width:91.66666667%}.col-lg-10{width:83.33333333%}.col-lg-9{width:75%}.col-lg-8{width:66.66666667%}.col-lg-7{width:58.33333333%}.col-lg-6{width:50%}.col-lg-5{width:41.66666667%}.col-lg-4{width:33.33333333%}.col-lg-3{width:25%}.col-lg-2{width:16.66666667%}.col-lg-1{width:8.33333333%}.col-lg-pull-12{left:100%;right:auto}.col-lg-pull-11{left:91.66666667%;right:auto}.col-lg-pull-10{left:83.33333333%;right:auto}.col-lg-pull-9{left:75%;right:auto}.col-lg-pull-8{left:66.66666667%;right:auto}.col-lg-pull-7{left:58.33333333%;right:auto}.col-lg-pull-6{left:50%;right:auto}.col-lg-pull-5{left:41.66666667%;right:auto}.col-lg-pull-4{left:33.33333333%;right:auto}.col-lg-pull-3{left:25%;right:auto}.col-lg-pull-2{left:16.66666667%;right:auto}.col-lg-pull-1{left:8.33333333%;right:auto}.col-lg-pull-0{left:auto;right:auto}.col-lg-push-12{right:100%;left:0}.col-lg-push-11{right:91.66666667%;left:0}.col-lg-push-10{right:83.33333333%;left:0}.col-lg-push-9{right:75%;left:0}.col-lg-push-8{right:66.66666667%;left:0}.col-lg-push-7{right:58.33333333%;left:0}.col-lg-push-6{right:50%;left:0}.col-lg-push-5{right:41.66666667%;left:0}.col-lg-push-4{right:33.33333333%;left:0}.col-lg-push-3{right:25%;left:0}.col-lg-push-2{right:16.66666667%;left:0}.col-lg-push-1{right:8.33333333%;left:0}.col-lg-push-0{right:auto;left:0}.col-lg-offset-12{margin-right:100%;margin-left:0}.col-lg-offset-11{margin-right:91.66666667%;margin-left:0}.col-lg-offset-10{margin-right:83.33333333%;margin-left:0}.col-lg-offset-9{margin-right:75%;margin-left:0}.col-lg-offset-8{margin-right:66.66666667%;margin-left:0}.col-lg-offset-7{margin-right:58.33333333%;margin-left:0}.col-lg-offset-6{margin-right:50%;margin-left:0}.col-lg-offset-5{margin-right:41.66666667%;margin-left:0}.col-lg-offset-4{margin-right:33.33333333%;margin-left:0}.col-lg-offset-3{margin-right:25%;margin-left:0}.col-lg-offset-2{margin-right:16.66666667%;margin-left:0}.col-lg-offset-1{margin-right:8.33333333%;margin-left:0}.col-lg-offset-0{margin-right:0;margin-left:0}}caption{text-align:right}th{text-align:right}@media screen and (max-width:767px){.table-responsive>.table-bordered{border:0}.table-responsive>.table-bordered>thead>tr>th:first-child,.table-responsive>.table-bordered>tbody>tr>th:first-child,.table-responsive>.table-bordered>tfoot>tr>th:first-child,.table-responsive>.table-bordered>thead>tr>td:first-child,.table-responsive>.table-bordered>tbody>tr>td:first-child,.table-responsive>.table-bordered>tfoot>tr>td:first-child{border-right:0;border-left:initial}.table-responsive>.table-bordered>thead>tr>th:last-child,.table-responsive>.table-bordered>tbody>tr>th:last-child,.table-responsive>.table-bordered>tfoot>tr>th:last-child,.table-responsive>.table-bordered>thead>tr>td:last-child,.table-responsive>.table-bordered>tbody>tr>td:last-child,.table-responsive>.table-bordered>tfoot>tr>td:last-child{border-left:0;border-right:initial}}.radio label,.checkbox label{padding-right:20px;padding-left:initial}.radio input[type=radio],.radio-inline input[type=radio],.checkbox input[type=checkbox],.checkbox-inline input[type=checkbox]{margin-right:-20px;margin-left:auto}.radio-inline,.checkbox-inline{padding-right:20px;padding-left:0}.radio-inline+.radio-inline,.checkbox-inline+.checkbox-inline{margin-right:10px;margin-left:0}.has-feedback .form-control{padding-left:42.5px;padding-right:12px}.form-control-feedback{left:0;right:auto}@media (min-width:768px){.form-inline label{padding-right:0;padding-left:initial}.form-inline .radio input[type=radio],.form-inline .checkbox input[type=checkbox]{margin-right:0;margin-left:auto}}@media (min-width:768px){.form-horizontal .control-label{text-align:left}}.form-horizontal .has-feedback .form-control-feedback{left:15px;right:auto}.caret{margin-right:2px;margin-left:0}.dropdown-menu{right:0;left:auto;float:left;text-align:right}.dropdown-menu.pull-right{left:0;right:auto;float:right}.dropdown-menu-right{left:auto;right:0}.dropdown-menu-left{left:0;right:auto}@media (min-width:768px){.navbar-right .dropdown-menu{left:auto;right:0}.navbar-right .dropdown-menu-left{left:0;right:auto}}.btn-group>.btn,.btn-group-vertical>.btn{float:right}.btn-group .btn+.btn,.btn-group .btn+.btn-group,.btn-group .btn-group+.btn,.btn-group .btn-group+.btn-group{margin-right:-1px;margin-left:0}.btn-toolbar{margin-right:-5px;margin-left:0}.btn-toolbar .btn-group,.btn-toolbar .input-group{float:right}.btn-toolbar>.btn,.btn-toolbar>.btn-group,.btn-toolbar>.input-group{margin-right:5px;margin-left:0}.btn-group>.btn:first-child{margin-right:0}.btn-group>.btn:first-child:not(:last-child):not(.dropdown-toggle){border-top-right-radius:4px;border-bottom-right-radius:4px;border-bottom-left-radius:0;border-top-left-radius:0}.btn-group>.btn:last-child:not(:first-child),.btn-group>.dropdown-toggle:not(:first-child){border-top-left-radius:4px;border-bottom-left-radius:4px;border-bottom-right-radius:0;border-top-right-radius:0}.btn-group>.btn-group{float:right}.btn-group.btn-group-justified>.btn,.btn-group.btn-group-justified>.btn-group{float:none}.btn-group>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group>.btn-group:first-child>.btn:last-child,.btn-group>.btn-group:first-child>.dropdown-toggle{border-top-right-radius:4px;border-bottom-right-radius:4px;border-bottom-left-radius:0;border-top-left-radius:0}.btn-group>.btn-group:last-child>.btn:first-child{border-top-left-radius:4px;border-bottom-left-radius:4px;border-bottom-right-radius:0;border-top-right-radius:0}.btn .caret{margin-right:0}.btn-group-vertical>.btn+.btn,.btn-group-vertical>.btn+.btn-group,.btn-group-vertical>.btn-group+.btn,.btn-group-vertical>.btn-group+.btn-group{margin-top:-1px;margin-right:0}.input-group .form-control{float:right}.input-group .form-control:first-child,.input-group-addon:first-child,.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group>.btn,.input-group-btn:first-child>.dropdown-toggle,.input-group-btn:last-child>.btn:not(:last-child):not(.dropdown-toggle),.input-group-btn:last-child>.btn-group:not(:last-child)>.btn{border-bottom-right-radius:4px;border-top-right-radius:4px;border-bottom-left-radius:0;border-top-left-radius:0}.input-group-addon:first-child{border-left:0;border-right:1px solid}.input-group .form-control:last-child,.input-group-addon:last-child,.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group>.btn,.input-group-btn:last-child>.dropdown-toggle,.input-group-btn:first-child>.btn:not(:first-child),.input-group-btn:first-child>.btn-group:not(:first-child)>.btn{border-bottom-left-radius:4px;border-top-left-radius:4px;border-bottom-right-radius:0;border-top-right-radius:0}.input-group-addon:last-child{border-left-width:1px;border-left-style:solid;border-right:0}.input-group-btn>.btn+.btn{margin-right:-1px;margin-left:auto}.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group{margin-left:-1px;margin-right:auto}.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group{margin-right:-1px;margin-left:auto}.nav{padding-right:0;padding-left:initial}.nav-tabs>li{float:right}.nav-tabs>li>a{margin-left:auto;margin-right:-2px;border-radius:4px 4px 0 0}.nav-pills>li{float:right}.nav-pills>li>a{border-radius:4px}.nav-pills>li+li{margin-right:2px;margin-left:auto}.nav-stacked>li{float:none}.nav-stacked>li+li{margin-right:0;margin-left:auto}.nav-justified>.dropdown .dropdown-menu{right:auto}.nav-tabs-justified>li>a{margin-left:0;margin-right:auto}@media (min-width:768px){.nav-tabs-justified>li>a{border-radius:4px 4px 0 0}}@media (min-width:768px){.navbar-header{float:right}}.navbar-collapse{padding-right:15px;padding-left:15px}.navbar-brand{float:right}@media (min-width:768px){.navbar>.container .navbar-brand,.navbar>.container-fluid .navbar-brand{margin-right:-15px;margin-left:auto}}.navbar-toggle{float:left;margin-left:15px;margin-right:auto}@media (max-width:767px){.navbar-nav .open .dropdown-menu>li>a,.navbar-nav .open .dropdown-menu .dropdown-header{padding:5px 25px 5px 15px}}@media (min-width:768px){.navbar-nav{float:right}.navbar-nav>li{float:right}}@media (min-width:768px){.navbar-left.flip{float:right!important}.navbar-right:last-child{margin-left:-15px;margin-right:auto}.navbar-right.flip{float:left!important;margin-left:-15px;margin-right:auto}.navbar-right .dropdown-menu{left:0;right:auto}}@media (min-width:768px){.navbar-text{float:right}.navbar-text.navbar-right:last-child{margin-left:0;margin-right:auto}}.pagination{padding-right:0}.pagination>li>a,.pagination>li>span{float:right;margin-right:-1px;margin-left:0}.pagination>li:first-child>a,.pagination>li:first-child>span{margin-left:0;border-bottom-right-radius:4px;border-top-right-radius:4px;border-bottom-left-radius:0;border-top-left-radius:0}.pagination>li:last-child>a,.pagination>li:last-child>span{margin-right:-1px;border-bottom-left-radius:4px;border-top-left-radius:4px;border-bottom-right-radius:0;border-top-right-radius:0}.pager{padding-right:0;padding-left:initial}.pager .next>a,.pager .next>span{float:left}.pager .previous>a,.pager .previous>span{float:right}.nav-pills>li>a>.badge{margin-left:0;margin-right:3px}.list-group-item>.badge{float:left}.list-group-item>.badge+.badge{margin-left:5px;margin-right:auto}.alert-dismissable,.alert-dismissible{padding-left:35px;padding-right:15px}.alert-dismissable .close,.alert-dismissible .close{right:auto;left:-21px}.progress-bar{float:right}.media>.pull-left{margin-right:10px}.media>.pull-left.flip{margin-right:0;margin-left:10px}.media>.pull-right{margin-left:10px}.media>.pull-right.flip{margin-left:0;margin-right:10px}.media-right,.media>.pull-right{padding-right:10px;padding-left:initial}.media-left,.media>.pull-left{padding-left:10px;padding-right:initial}.media-list{padding-right:0;padding-left:initial;list-style:none}.list-group{padding-right:0;padding-left:initial}.panel>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:first-child{border-top-right-radius:3px;border-top-left-radius:0}.panel>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:last-child{border-top-left-radius:3px;border-top-right-radius:0}.panel>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:first-child{border-bottom-left-radius:3px;border-top-right-radius:0}.panel>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:last-child{border-bottom-right-radius:3px;border-top-left-radius:0}.panel>.table-bordered>thead>tr>th:first-child,.panel>.table-responsive>.table-bordered>thead>tr>th:first-child,.panel>.table-bordered>tbody>tr>th:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:first-child,.panel>.table-bordered>tfoot>tr>th:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:first-child,.panel>.table-bordered>thead>tr>td:first-child,.panel>.table-responsive>.table-bordered>thead>tr>td:first-child,.panel>.table-bordered>tbody>tr>td:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:first-child,.panel>.table-bordered>tfoot>tr>td:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:first-child{border-right:0;border-left:none}.panel>.table-bordered>thead>tr>th:last-child,.panel>.table-responsive>.table-bordered>thead>tr>th:last-child,.panel>.table-bordered>tbody>tr>th:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:last-child,.panel>.table-bordered>tfoot>tr>th:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:last-child,.panel>.table-bordered>thead>tr>td:last-child,.panel>.table-responsive>.table-bordered>thead>tr>td:last-child,.panel>.table-bordered>tbody>tr>td:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:last-child,.panel>.table-bordered>tfoot>tr>td:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:last-child{border-right:none;border-left:0}.embed-responsive .embed-responsive-item,.embed-responsive iframe,.embed-responsive embed,.embed-responsive object{right:0;left:auto}.close{float:left}.modal-footer{text-align:left}.modal-footer.flip{text-align:right}.modal-footer .btn+.btn{margin-left:auto;margin-right:5px}.modal-footer .btn-group .btn+.btn{margin-right:-1px;margin-left:auto}.modal-footer .btn-block+.btn-block{margin-right:0;margin-left:auto}.popover{left:auto;text-align:right}.popover.top>.arrow{right:50%;left:auto;margin-right:-11px;margin-left:auto}.popover.top>.arrow:after{margin-right:-10px;margin-left:auto}.popover.bottom>.arrow{right:50%;left:auto;margin-right:-11px;margin-left:auto}.popover.bottom>.arrow:after{margin-right:-10px;margin-left:auto}.carousel-control{right:0;bottom:0}.carousel-control.left{right:auto;left:0;background-image:-webkit-linear-gradient(left,color-stop(rgba(0,0,0,.5) 0),color-stop(rgba(0,0,0,.0001) 100%));background-image:-o-linear-gradient(left,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);background-image:linear-gradient(to right,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1)}.carousel-control.right{left:auto;right:0;background-image:-webkit-linear-gradient(left,color-stop(rgba(0,0,0,.0001) 0),color-stop(rgba(0,0,0,.5) 100%));background-image:-o-linear-gradient(left,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);background-image:linear-gradient(to right,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1)}.carousel-control .icon-prev,.carousel-control .glyphicon-chevron-left{left:50%;right:auto;margin-right:-10px}.carousel-control .icon-next,.carousel-control .glyphicon-chevron-right{right:50%;left:auto;margin-left:-10px}.carousel-indicators{right:50%;left:0;margin-right:-30%;margin-left:0;padding-left:0}@media screen and (min-width:768px){.carousel-control .glyphicon-chevron-left,.carousel-control .icon-prev{margin-left:0;margin-right:-15px}.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next{margin-left:0;margin-right:-15px}.carousel-caption{left:20%;right:20%;padding-bottom:30px}}.pull-right.flip{float:left!important}.pull-left.flip{float:right!important}
 
 /* Customization for advanced search */
 .group {
@@ -85,6 +85,9 @@ body.rtl i.fa.fa-chevron-right {content: "\f053";}
   body.offcanvas.offcanvas-right.flip.active .offcanvas-toggle {left: 75%; right: auto;}
 }
 
+/* Customization for search */
+.result .checkbox { float: right; }
+
 /* Customization for sidebar */
 .sidebar .list-group:not(.filters) .title:after {float: left;}
 .sidebar label:not(.list-group-item) {
@@ -94,4 +97,4 @@ body.rtl i.fa.fa-chevron-right {content: "\f053";}
 
 /* Customization for typeahead */
 span.twitter-typeahead,
-.search-query {direction: RTL !important;}
\ No newline at end of file
+.search-query {direction: RTL !important;}
diff --git a/themes/bootstrap3/css/vendor/bootstrap-slider.min.css b/themes/bootstrap3/css/vendor/bootstrap-slider.min.css
index 10c5c9fc56f54c915c5cba4d2a27d87b8601e951..4ddddf82f707d92707f855a8e08efa1f81fe923d 100644
--- a/themes/bootstrap3/css/vendor/bootstrap-slider.min.css
+++ b/themes/bootstrap3/css/vendor/bootstrap-slider.min.css
@@ -1,5 +1,5 @@
 /*! =======================================================
-                      VERSION  6.1.2
+                      VERSION  7.1.1
 ========================================================= */
 /*! =========================================================
  * bootstrap-slider.js
@@ -25,4 +25,4 @@
  * 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.
- * ========================================================= */.slider{display:inline-block;vertical-align:middle;position:relative}.slider.slider-horizontal{width:210px;height:20px}.slider.slider-horizontal .slider-track{height:10px;width:100%;margin-top:-5px;top:50%;left:0}.slider.slider-horizontal .slider-selection,.slider.slider-horizontal .slider-track-low,.slider.slider-horizontal .slider-track-high{height:100%;top:0;bottom:0}.slider.slider-horizontal .slider-tick,.slider.slider-horizontal .slider-handle{margin-left:-10px;margin-top:-5px}.slider.slider-horizontal .slider-tick.triangle,.slider.slider-horizontal .slider-handle.triangle{border-width:0 10px 10px 10px;width:0;height:0;border-bottom-color:#0480be;margin-top:0}.slider.slider-horizontal .slider-tick-label-container{white-space:nowrap;margin-top:20px}.slider.slider-horizontal .slider-tick-label-container .slider-tick-label{padding-top:4px;display:inline-block;text-align:center}.slider.slider-vertical{height:210px;width:20px}.slider.slider-vertical .slider-track{width:10px;height:100%;margin-left:-5px;left:50%;top:0}.slider.slider-vertical .slider-selection{width:100%;left:0;top:0;bottom:0}.slider.slider-vertical .slider-track-low,.slider.slider-vertical .slider-track-high{width:100%;left:0;right:0}.slider.slider-vertical .slider-tick,.slider.slider-vertical .slider-handle{margin-left:-5px;margin-top:-10px}.slider.slider-vertical .slider-tick.triangle,.slider.slider-vertical .slider-handle.triangle{border-width:10px 0 10px 10px;width:1px;height:1px;border-left-color:#0480be;margin-left:0}.slider.slider-vertical .slider-tick-label-container{white-space:nowrap}.slider.slider-vertical .slider-tick-label-container .slider-tick-label{padding-left:4px}.slider.slider-disabled .slider-handle{background-image:-webkit-linear-gradient(top,#dfdfdf 0,#bebebe 100%);background-image:-o-linear-gradient(top,#dfdfdf 0,#bebebe 100%);background-image:linear-gradient(to bottom,#dfdfdf 0,#bebebe 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdfdfdf',endColorstr='#ffbebebe',GradientType=0)}.slider.slider-disabled .slider-track{background-image:-webkit-linear-gradient(top,#e5e5e5 0,#e9e9e9 100%);background-image:-o-linear-gradient(top,#e5e5e5 0,#e9e9e9 100%);background-image:linear-gradient(to bottom,#e5e5e5 0,#e9e9e9 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe5e5e5',endColorstr='#ffe9e9e9',GradientType=0);cursor:not-allowed}.slider input{display:none}.slider .tooltip.top{margin-top:-36px}.slider .tooltip-inner{white-space:nowrap}.slider .hide{display:none}.slider-track{position:absolute;cursor:pointer;background-image:-webkit-linear-gradient(top,#f5f5f5 0,#f9f9f9 100%);background-image:-o-linear-gradient(top,#f5f5f5 0,#f9f9f9 100%);background-image:linear-gradient(to bottom,#f5f5f5 0,#f9f9f9 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5',endColorstr='#fff9f9f9',GradientType=0);-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,0.1);box-shadow:inset 0 1px 2px rgba(0,0,0,0.1);border-radius:4px}.slider-selection{position:absolute;background-image:-webkit-linear-gradient(top,#f9f9f9 0,#f5f5f5 100%);background-image:-o-linear-gradient(top,#f9f9f9 0,#f5f5f5 100%);background-image:linear-gradient(to bottom,#f9f9f9 0,#f5f5f5 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff9f9f9',endColorstr='#fff5f5f5',GradientType=0);-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;border-radius:4px}.slider-selection.tick-slider-selection{background-image:-webkit-linear-gradient(top,#89cdef 0,#81bfde 100%);background-image:-o-linear-gradient(top,#89cdef 0,#81bfde 100%);background-image:linear-gradient(to bottom,#89cdef 0,#81bfde 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff89cdef',endColorstr='#ff81bfde',GradientType=0)}.slider-track-low,.slider-track-high{position:absolute;background:transparent;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;border-radius:4px}.slider-handle{position:absolute;width:20px;height:20px;background-color:#337ab7;background-image:-webkit-linear-gradient(top,#149bdf 0,#0480be 100%);background-image:-o-linear-gradient(top,#149bdf 0,#0480be 100%);background-image:linear-gradient(to bottom,#149bdf 0,#0480be 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff149bdf',endColorstr='#ff0480be',GradientType=0);filter:none;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.2),0 1px 2px rgba(0,0,0,.05);box-shadow:inset 0 1px 0 rgba(255,255,255,.2),0 1px 2px rgba(0,0,0,.05);border:0 solid transparent}.slider-handle.round{border-radius:50%}.slider-handle.triangle{background:transparent none}.slider-handle.custom{background:transparent none}.slider-handle.custom::before{line-height:20px;font-size:20px;content:'\2605';color:#726204}.slider-tick{position:absolute;width:20px;height:20px;background-image:-webkit-linear-gradient(top,#f9f9f9 0,#f5f5f5 100%);background-image:-o-linear-gradient(top,#f9f9f9 0,#f5f5f5 100%);background-image:linear-gradient(to bottom,#f9f9f9 0,#f5f5f5 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff9f9f9',endColorstr='#fff5f5f5',GradientType=0);-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;filter:none;opacity:.8;border:0 solid transparent}.slider-tick.round{border-radius:50%}.slider-tick.triangle{background:transparent none}.slider-tick.custom{background:transparent none}.slider-tick.custom::before{line-height:20px;font-size:20px;content:'\2605';color:#726204}.slider-tick.in-selection{background-image:-webkit-linear-gradient(top,#89cdef 0,#81bfde 100%);background-image:-o-linear-gradient(top,#89cdef 0,#81bfde 100%);background-image:linear-gradient(to bottom,#89cdef 0,#81bfde 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff89cdef',endColorstr='#ff81bfde',GradientType=0);opacity:1}
\ No newline at end of file
+ * ========================================================= */.slider{display:inline-block;vertical-align:middle;position:relative}.slider.slider-horizontal{width:210px;height:20px}.slider.slider-horizontal .slider-track{height:10px;width:100%;margin-top:-5px;top:50%;left:0}.slider.slider-horizontal .slider-selection,.slider.slider-horizontal .slider-track-low,.slider.slider-horizontal .slider-track-high{height:100%;top:0;bottom:0}.slider.slider-horizontal .slider-tick,.slider.slider-horizontal .slider-handle{margin-left:-10px;margin-top:-5px}.slider.slider-horizontal .slider-tick.triangle,.slider.slider-horizontal .slider-handle.triangle{border-width:0 10px 10px 10px;width:0;height:0;border-bottom-color:#0480be;margin-top:0}.slider.slider-horizontal .slider-tick-label-container{white-space:nowrap;margin-top:20px}.slider.slider-horizontal .slider-tick-label-container .slider-tick-label{padding-top:4px;display:inline-block;text-align:center}.slider.slider-vertical{height:210px;width:20px}.slider.slider-vertical .slider-track{width:10px;height:100%;margin-left:-5px;left:50%;top:0}.slider.slider-vertical .slider-selection{width:100%;left:0;top:0;bottom:0}.slider.slider-vertical .slider-track-low,.slider.slider-vertical .slider-track-high{width:100%;left:0;right:0}.slider.slider-vertical .slider-tick,.slider.slider-vertical .slider-handle{margin-left:-5px;margin-top:-10px}.slider.slider-vertical .slider-tick.triangle,.slider.slider-vertical .slider-handle.triangle{border-width:10px 0 10px 10px;width:1px;height:1px;border-left-color:#0480be;margin-left:0}.slider.slider-vertical .slider-tick-label-container{white-space:nowrap}.slider.slider-vertical .slider-tick-label-container .slider-tick-label{padding-left:4px}.slider.slider-disabled .slider-handle{background-image:-webkit-linear-gradient(top,#dfdfdf 0,#bebebe 100%);background-image:-o-linear-gradient(top,#dfdfdf 0,#bebebe 100%);background-image:linear-gradient(to bottom,#dfdfdf 0,#bebebe 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdfdfdf',endColorstr='#ffbebebe',GradientType=0)}.slider.slider-disabled .slider-track{background-image:-webkit-linear-gradient(top,#e5e5e5 0,#e9e9e9 100%);background-image:-o-linear-gradient(top,#e5e5e5 0,#e9e9e9 100%);background-image:linear-gradient(to bottom,#e5e5e5 0,#e9e9e9 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe5e5e5',endColorstr='#ffe9e9e9',GradientType=0);cursor:not-allowed}.slider input{display:none}.slider .tooltip.top{margin-top:-36px}.slider .tooltip-inner{white-space:nowrap;max-width:none}.slider .hide{display:none}.slider-track{position:absolute;cursor:pointer;background-image:-webkit-linear-gradient(top,#f5f5f5 0,#f9f9f9 100%);background-image:-o-linear-gradient(top,#f5f5f5 0,#f9f9f9 100%);background-image:linear-gradient(to bottom,#f5f5f5 0,#f9f9f9 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5',endColorstr='#fff9f9f9',GradientType=0);-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,0.1);box-shadow:inset 0 1px 2px rgba(0,0,0,0.1);border-radius:4px}.slider-selection{position:absolute;background-image:-webkit-linear-gradient(top,#f9f9f9 0,#f5f5f5 100%);background-image:-o-linear-gradient(top,#f9f9f9 0,#f5f5f5 100%);background-image:linear-gradient(to bottom,#f9f9f9 0,#f5f5f5 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff9f9f9',endColorstr='#fff5f5f5',GradientType=0);-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;border-radius:4px}.slider-selection.tick-slider-selection{background-image:-webkit-linear-gradient(top,#89cdef 0,#81bfde 100%);background-image:-o-linear-gradient(top,#89cdef 0,#81bfde 100%);background-image:linear-gradient(to bottom,#89cdef 0,#81bfde 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff89cdef',endColorstr='#ff81bfde',GradientType=0)}.slider-track-low,.slider-track-high{position:absolute;background:transparent;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;border-radius:4px}.slider-handle{position:absolute;width:20px;height:20px;background-color:#337ab7;background-image:-webkit-linear-gradient(top,#149bdf 0,#0480be 100%);background-image:-o-linear-gradient(top,#149bdf 0,#0480be 100%);background-image:linear-gradient(to bottom,#149bdf 0,#0480be 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff149bdf',endColorstr='#ff0480be',GradientType=0);filter:none;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.2),0 1px 2px rgba(0,0,0,.05);box-shadow:inset 0 1px 0 rgba(255,255,255,.2),0 1px 2px rgba(0,0,0,.05);border:0 solid transparent}.slider-handle.round{border-radius:50%}.slider-handle.triangle{background:transparent none}.slider-handle.custom{background:transparent none}.slider-handle.custom::before{line-height:20px;font-size:20px;content:'\2605';color:#726204}.slider-tick{position:absolute;width:20px;height:20px;background-image:-webkit-linear-gradient(top,#f9f9f9 0,#f5f5f5 100%);background-image:-o-linear-gradient(top,#f9f9f9 0,#f5f5f5 100%);background-image:linear-gradient(to bottom,#f9f9f9 0,#f5f5f5 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff9f9f9',endColorstr='#fff5f5f5',GradientType=0);-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;filter:none;opacity:.8;border:0 solid transparent}.slider-tick.round{border-radius:50%}.slider-tick.triangle{background:transparent none}.slider-tick.custom{background:transparent none}.slider-tick.custom::before{line-height:20px;font-size:20px;content:'\2605';color:#726204}.slider-tick.in-selection{background-image:-webkit-linear-gradient(top,#89cdef 0,#81bfde 100%);background-image:-o-linear-gradient(top,#89cdef 0,#81bfde 100%);background-image:linear-gradient(to bottom,#89cdef 0,#81bfde 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff89cdef',endColorstr='#ff81bfde',GradientType=0);opacity:1}
\ No newline at end of file
diff --git a/themes/bootstrap3/css/vendor/bootstrap.min.css b/themes/bootstrap3/css/vendor/bootstrap.min.css
index b6fe4e0fbceb8ec00001531beb7fb16fc8681c8f..ed3905e0e0c91d4ed7d8aa14412dffeb038745ff 100644
--- a/themes/bootstrap3/css/vendor/bootstrap.min.css
+++ b/themes/bootstrap3/css/vendor/bootstrap.min.css
@@ -1,5 +1,6 @@
 /*!
- * Bootstrap v3.3.1 (http://getbootstrap.com)
- * Copyright 2011-2014 Twitter, Inc.
+ * Bootstrap v3.3.7 (http://getbootstrap.com)
+ * Copyright 2011-2016 Twitter, Inc.
  * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
- *//*! normalize.css v3.0.2 | MIT License | git.io/normalize */html{font-family:sans-serif;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:700}dfn{font-style:italic}h1{margin:.67em 0;font-size:2em}mark{color:#000;background:#ff0}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{height:0;-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}button,input,optgroup,select,textarea{margin:0;font:inherit;color:inherit}button{overflow:visible}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{padding:0;border:0}input{line-height:normal}input[type=checkbox],input[type=radio]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;padding:0}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{height:auto}input[type=search]{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;-webkit-appearance:textfield}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}fieldset{padding:.35em .625em .75em;margin:0 2px;border:1px solid silver}legend{padding:0;border:0}textarea{overflow:auto}optgroup{font-weight:700}table{border-spacing:0;border-collapse:collapse}td,th{padding:0}/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */@media print{*,:before,:after{color:#000!important;text-shadow:none!important;background:transparent!important;-webkit-box-shadow:none!important;box-shadow:none!important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href) ")"}abbr[title]:after{content:" (" attr(title) ")"}a[href^="#"]:after,a[href^="javascript:"]:after{content:""}pre,blockquote{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}tr,img{page-break-inside:avoid}img{max-width:100%!important}p,h2,h3{orphans:3;widows:3}h2,h3{page-break-after:avoid}select{background:#fff!important}.navbar{display:none}.btn>.caret,.dropup>.btn>.caret{border-top-color:#000!important}.label{border:1px solid #000}.table{border-collapse:collapse!important}.table td,.table th{background-color:#fff!important}.table-bordered th,.table-bordered td{border:1px solid #ddd!important}}@font-face{font-family:'Glyphicons Halflings';src:url(../fonts/glyphicons-halflings-regular.eot);src:url(../fonts/glyphicons-halflings-regular.eot?#iefix) format('embedded-opentype'),url(../fonts/glyphicons-halflings-regular.woff) format('woff'),url(../fonts/glyphicons-halflings-regular.ttf) format('truetype'),url(../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular) format('svg')}.glyphicon{position:relative;top:1px;display:inline-block;font-family:'Glyphicons Halflings';font-style:normal;font-weight:400;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.glyphicon-asterisk:before{content:"\2a"}.glyphicon-plus:before{content:"\2b"}.glyphicon-euro:before,.glyphicon-eur:before{content:"\20ac"}.glyphicon-minus:before{content:"\2212"}.glyphicon-cloud:before{content:"\2601"}.glyphicon-envelope:before{content:"\2709"}.glyphicon-pencil:before{content:"\270f"}.glyphicon-glass:before{content:"\e001"}.glyphicon-music:before{content:"\e002"}.glyphicon-search:before{content:"\e003"}.glyphicon-heart:before{content:"\e005"}.glyphicon-star:before{content:"\e006"}.glyphicon-star-empty:before{content:"\e007"}.glyphicon-user:before{content:"\e008"}.glyphicon-film:before{content:"\e009"}.glyphicon-th-large:before{content:"\e010"}.glyphicon-th:before{content:"\e011"}.glyphicon-th-list:before{content:"\e012"}.glyphicon-ok:before{content:"\e013"}.glyphicon-remove:before{content:"\e014"}.glyphicon-zoom-in:before{content:"\e015"}.glyphicon-zoom-out:before{content:"\e016"}.glyphicon-off:before{content:"\e017"}.glyphicon-signal:before{content:"\e018"}.glyphicon-cog:before{content:"\e019"}.glyphicon-trash:before{content:"\e020"}.glyphicon-home:before{content:"\e021"}.glyphicon-file:before{content:"\e022"}.glyphicon-time:before{content:"\e023"}.glyphicon-road:before{content:"\e024"}.glyphicon-download-alt:before{content:"\e025"}.glyphicon-download:before{content:"\e026"}.glyphicon-upload:before{content:"\e027"}.glyphicon-inbox:before{content:"\e028"}.glyphicon-play-circle:before{content:"\e029"}.glyphicon-repeat:before{content:"\e030"}.glyphicon-refresh:before{content:"\e031"}.glyphicon-list-alt:before{content:"\e032"}.glyphicon-lock:before{content:"\e033"}.glyphicon-flag:before{content:"\e034"}.glyphicon-headphones:before{content:"\e035"}.glyphicon-volume-off:before{content:"\e036"}.glyphicon-volume-down:before{content:"\e037"}.glyphicon-volume-up:before{content:"\e038"}.glyphicon-qrcode:before{content:"\e039"}.glyphicon-barcode:before{content:"\e040"}.glyphicon-tag:before{content:"\e041"}.glyphicon-tags:before{content:"\e042"}.glyphicon-book:before{content:"\e043"}.glyphicon-bookmark:before{content:"\e044"}.glyphicon-print:before{content:"\e045"}.glyphicon-camera:before{content:"\e046"}.glyphicon-font:before{content:"\e047"}.glyphicon-bold:before{content:"\e048"}.glyphicon-italic:before{content:"\e049"}.glyphicon-text-height:before{content:"\e050"}.glyphicon-text-width:before{content:"\e051"}.glyphicon-align-left:before{content:"\e052"}.glyphicon-align-center:before{content:"\e053"}.glyphicon-align-right:before{content:"\e054"}.glyphicon-align-justify:before{content:"\e055"}.glyphicon-list:before{content:"\e056"}.glyphicon-indent-left:before{content:"\e057"}.glyphicon-indent-right:before{content:"\e058"}.glyphicon-facetime-video:before{content:"\e059"}.glyphicon-picture:before{content:"\e060"}.glyphicon-map-marker:before{content:"\e062"}.glyphicon-adjust:before{content:"\e063"}.glyphicon-tint:before{content:"\e064"}.glyphicon-edit:before{content:"\e065"}.glyphicon-share:before{content:"\e066"}.glyphicon-check:before{content:"\e067"}.glyphicon-move:before{content:"\e068"}.glyphicon-step-backward:before{content:"\e069"}.glyphicon-fast-backward:before{content:"\e070"}.glyphicon-backward:before{content:"\e071"}.glyphicon-play:before{content:"\e072"}.glyphicon-pause:before{content:"\e073"}.glyphicon-stop:before{content:"\e074"}.glyphicon-forward:before{content:"\e075"}.glyphicon-fast-forward:before{content:"\e076"}.glyphicon-step-forward:before{content:"\e077"}.glyphicon-eject:before{content:"\e078"}.glyphicon-chevron-left:before{content:"\e079"}.glyphicon-chevron-right:before{content:"\e080"}.glyphicon-plus-sign:before{content:"\e081"}.glyphicon-minus-sign:before{content:"\e082"}.glyphicon-remove-sign:before{content:"\e083"}.glyphicon-ok-sign:before{content:"\e084"}.glyphicon-question-sign:before{content:"\e085"}.glyphicon-info-sign:before{content:"\e086"}.glyphicon-screenshot:before{content:"\e087"}.glyphicon-remove-circle:before{content:"\e088"}.glyphicon-ok-circle:before{content:"\e089"}.glyphicon-ban-circle:before{content:"\e090"}.glyphicon-arrow-left:before{content:"\e091"}.glyphicon-arrow-right:before{content:"\e092"}.glyphicon-arrow-up:before{content:"\e093"}.glyphicon-arrow-down:before{content:"\e094"}.glyphicon-share-alt:before{content:"\e095"}.glyphicon-resize-full:before{content:"\e096"}.glyphicon-resize-small:before{content:"\e097"}.glyphicon-exclamation-sign:before{content:"\e101"}.glyphicon-gift:before{content:"\e102"}.glyphicon-leaf:before{content:"\e103"}.glyphicon-fire:before{content:"\e104"}.glyphicon-eye-open:before{content:"\e105"}.glyphicon-eye-close:before{content:"\e106"}.glyphicon-warning-sign:before{content:"\e107"}.glyphicon-plane:before{content:"\e108"}.glyphicon-calendar:before{content:"\e109"}.glyphicon-random:before{content:"\e110"}.glyphicon-comment:before{content:"\e111"}.glyphicon-magnet:before{content:"\e112"}.glyphicon-chevron-up:before{content:"\e113"}.glyphicon-chevron-down:before{content:"\e114"}.glyphicon-retweet:before{content:"\e115"}.glyphicon-shopping-cart:before{content:"\e116"}.glyphicon-folder-close:before{content:"\e117"}.glyphicon-folder-open:before{content:"\e118"}.glyphicon-resize-vertical:before{content:"\e119"}.glyphicon-resize-horizontal:before{content:"\e120"}.glyphicon-hdd:before{content:"\e121"}.glyphicon-bullhorn:before{content:"\e122"}.glyphicon-bell:before{content:"\e123"}.glyphicon-certificate:before{content:"\e124"}.glyphicon-thumbs-up:before{content:"\e125"}.glyphicon-thumbs-down:before{content:"\e126"}.glyphicon-hand-right:before{content:"\e127"}.glyphicon-hand-left:before{content:"\e128"}.glyphicon-hand-up:before{content:"\e129"}.glyphicon-hand-down:before{content:"\e130"}.glyphicon-circle-arrow-right:before{content:"\e131"}.glyphicon-circle-arrow-left:before{content:"\e132"}.glyphicon-circle-arrow-up:before{content:"\e133"}.glyphicon-circle-arrow-down:before{content:"\e134"}.glyphicon-globe:before{content:"\e135"}.glyphicon-wrench:before{content:"\e136"}.glyphicon-tasks:before{content:"\e137"}.glyphicon-filter:before{content:"\e138"}.glyphicon-briefcase:before{content:"\e139"}.glyphicon-fullscreen:before{content:"\e140"}.glyphicon-dashboard:before{content:"\e141"}.glyphicon-paperclip:before{content:"\e142"}.glyphicon-heart-empty:before{content:"\e143"}.glyphicon-link:before{content:"\e144"}.glyphicon-phone:before{content:"\e145"}.glyphicon-pushpin:before{content:"\e146"}.glyphicon-usd:before{content:"\e148"}.glyphicon-gbp:before{content:"\e149"}.glyphicon-sort:before{content:"\e150"}.glyphicon-sort-by-alphabet:before{content:"\e151"}.glyphicon-sort-by-alphabet-alt:before{content:"\e152"}.glyphicon-sort-by-order:before{content:"\e153"}.glyphicon-sort-by-order-alt:before{content:"\e154"}.glyphicon-sort-by-attributes:before{content:"\e155"}.glyphicon-sort-by-attributes-alt:before{content:"\e156"}.glyphicon-unchecked:before{content:"\e157"}.glyphicon-expand:before{content:"\e158"}.glyphicon-collapse-down:before{content:"\e159"}.glyphicon-collapse-up:before{content:"\e160"}.glyphicon-log-in:before{content:"\e161"}.glyphicon-flash:before{content:"\e162"}.glyphicon-log-out:before{content:"\e163"}.glyphicon-new-window:before{content:"\e164"}.glyphicon-record:before{content:"\e165"}.glyphicon-save:before{content:"\e166"}.glyphicon-open:before{content:"\e167"}.glyphicon-saved:before{content:"\e168"}.glyphicon-import:before{content:"\e169"}.glyphicon-export:before{content:"\e170"}.glyphicon-send:before{content:"\e171"}.glyphicon-floppy-disk:before{content:"\e172"}.glyphicon-floppy-saved:before{content:"\e173"}.glyphicon-floppy-remove:before{content:"\e174"}.glyphicon-floppy-save:before{content:"\e175"}.glyphicon-floppy-open:before{content:"\e176"}.glyphicon-credit-card:before{content:"\e177"}.glyphicon-transfer:before{content:"\e178"}.glyphicon-cutlery:before{content:"\e179"}.glyphicon-header:before{content:"\e180"}.glyphicon-compressed:before{content:"\e181"}.glyphicon-earphone:before{content:"\e182"}.glyphicon-phone-alt:before{content:"\e183"}.glyphicon-tower:before{content:"\e184"}.glyphicon-stats:before{content:"\e185"}.glyphicon-sd-video:before{content:"\e186"}.glyphicon-hd-video:before{content:"\e187"}.glyphicon-subtitles:before{content:"\e188"}.glyphicon-sound-stereo:before{content:"\e189"}.glyphicon-sound-dolby:before{content:"\e190"}.glyphicon-sound-5-1:before{content:"\e191"}.glyphicon-sound-6-1:before{content:"\e192"}.glyphicon-sound-7-1:before{content:"\e193"}.glyphicon-copyright-mark:before{content:"\e194"}.glyphicon-registration-mark:before{content:"\e195"}.glyphicon-cloud-download:before{content:"\e197"}.glyphicon-cloud-upload:before{content:"\e198"}.glyphicon-tree-conifer:before{content:"\e199"}.glyphicon-tree-deciduous:before{content:"\e200"}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}:before,:after{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:10px;-webkit-tap-highlight-color:rgba(0,0,0,0)}body{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;line-height:1.42857143;color:#333;background-color:#fff}input,button,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit}a{color:#337ab7;text-decoration:none}a:hover,a:focus{color:#23527c;text-decoration:underline}a:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}figure{margin:0}img{vertical-align:middle}.img-responsive,.thumbnail>img,.thumbnail a>img,.carousel-inner>.item>img,.carousel-inner>.item>a>img{display:block;max-width:100%;height:auto}.img-rounded{border-radius:6px}.img-thumbnail{display:inline-block;max-width:100%;height:auto;padding:4px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:all .2s ease-in-out;-o-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.img-circle{border-radius:50%}hr{margin-top:20px;margin-bottom:20px;border:0;border-top:1px solid #eee}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}h1,h2,h3,h4,h5,h6,.h1,.h2,.h3,.h4,.h5,.h6{font-family:inherit;font-weight:500;line-height:1.1;color:inherit}h1 small,h2 small,h3 small,h4 small,h5 small,h6 small,.h1 small,.h2 small,.h3 small,.h4 small,.h5 small,.h6 small,h1 .small,h2 .small,h3 .small,h4 .small,h5 .small,h6 .small,.h1 .small,.h2 .small,.h3 .small,.h4 .small,.h5 .small,.h6 .small{font-weight:400;line-height:1;color:#777}h1,.h1,h2,.h2,h3,.h3{margin-top:20px;margin-bottom:10px}h1 small,.h1 small,h2 small,.h2 small,h3 small,.h3 small,h1 .small,.h1 .small,h2 .small,.h2 .small,h3 .small,.h3 .small{font-size:65%}h4,.h4,h5,.h5,h6,.h6{margin-top:10px;margin-bottom:10px}h4 small,.h4 small,h5 small,.h5 small,h6 small,.h6 small,h4 .small,.h4 .small,h5 .small,.h5 .small,h6 .small,.h6 .small{font-size:75%}h1,.h1{font-size:36px}h2,.h2{font-size:30px}h3,.h3{font-size:24px}h4,.h4{font-size:18px}h5,.h5{font-size:14px}h6,.h6{font-size:12px}p{margin:0 0 10px}.lead{margin-bottom:20px;font-size:16px;font-weight:300;line-height:1.4}@media (min-width:768px){.lead{font-size:21px}}small,.small{font-size:85%}mark,.mark{padding:.2em;background-color:#fcf8e3}.text-left{text-align:left}.text-right{text-align:right}.text-center{text-align:center}.text-justify{text-align:justify}.text-nowrap{white-space:nowrap}.text-lowercase{text-transform:lowercase}.text-uppercase{text-transform:uppercase}.text-capitalize{text-transform:capitalize}.text-muted{color:#777}.text-primary{color:#337ab7}a.text-primary:hover{color:#286090}.text-success{color:#3c763d}a.text-success:hover{color:#2b542c}.text-info{color:#31708f}a.text-info:hover{color:#245269}.text-warning{color:#8a6d3b}a.text-warning:hover{color:#66512c}.text-danger{color:#a94442}a.text-danger:hover{color:#843534}.bg-primary{color:#fff;background-color:#337ab7}a.bg-primary:hover{background-color:#286090}.bg-success{background-color:#dff0d8}a.bg-success:hover{background-color:#c1e2b3}.bg-info{background-color:#d9edf7}a.bg-info:hover{background-color:#afd9ee}.bg-warning{background-color:#fcf8e3}a.bg-warning:hover{background-color:#f7ecb5}.bg-danger{background-color:#f2dede}a.bg-danger:hover{background-color:#e4b9b9}.page-header{padding-bottom:9px;margin:40px 0 20px;border-bottom:1px solid #eee}ul,ol{margin-top:0;margin-bottom:10px}ul ul,ol ul,ul ol,ol ol{margin-bottom:0}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;margin-left:-5px;list-style:none}.list-inline>li{display:inline-block;padding-right:5px;padding-left:5px}dl{margin-top:0;margin-bottom:20px}dt,dd{line-height:1.42857143}dt{font-weight:700}dd{margin-left:0}@media (min-width:768px){.dl-horizontal dt{float:left;width:160px;overflow:hidden;clear:left;text-align:right;text-overflow:ellipsis;white-space:nowrap}.dl-horizontal dd{margin-left:180px}}abbr[title],abbr[data-original-title]{cursor:help;border-bottom:1px dotted #777}.initialism{font-size:90%;text-transform:uppercase}blockquote{padding:10px 20px;margin:0 0 20px;font-size:17.5px;border-left:5px solid #eee}blockquote p:last-child,blockquote ul:last-child,blockquote ol:last-child{margin-bottom:0}blockquote footer,blockquote small,blockquote .small{display:block;font-size:80%;line-height:1.42857143;color:#777}blockquote footer:before,blockquote small:before,blockquote .small:before{content:'\2014 \00A0'}.blockquote-reverse,blockquote.pull-right{padding-right:15px;padding-left:0;text-align:right;border-right:5px solid #eee;border-left:0}.blockquote-reverse footer:before,blockquote.pull-right footer:before,.blockquote-reverse small:before,blockquote.pull-right small:before,.blockquote-reverse .small:before,blockquote.pull-right .small:before{content:''}.blockquote-reverse footer:after,blockquote.pull-right footer:after,.blockquote-reverse small:after,blockquote.pull-right small:after,.blockquote-reverse .small:after,blockquote.pull-right .small:after{content:'\00A0 \2014'}address{margin-bottom:20px;font-style:normal;line-height:1.42857143}code,kbd,pre,samp{font-family:Menlo,Monaco,Consolas,"Courier New",monospace}code{padding:2px 4px;font-size:90%;color:#c7254e;background-color:#f9f2f4;border-radius:4px}kbd{padding:2px 4px;font-size:90%;color:#fff;background-color:#333;border-radius:3px;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,.25);box-shadow:inset 0 -1px 0 rgba(0,0,0,.25)}kbd kbd{padding:0;font-size:100%;font-weight:700;-webkit-box-shadow:none;box-shadow:none}pre{display:block;padding:9.5px;margin:0 0 10px;font-size:13px;line-height:1.42857143;color:#333;word-break:break-all;word-wrap:break-word;background-color:#f5f5f5;border:1px solid #ccc;border-radius:4px}pre code{padding:0;font-size:inherit;color:inherit;white-space:pre-wrap;background-color:transparent;border-radius:0}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width:768px){.container{width:750px}}@media (min-width:992px){.container{width:970px}}@media (min-width:1200px){.container{width:1170px}}.container-fluid{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}.row{margin-right:-15px;margin-left:-15px}.col-xs-1,.col-sm-1,.col-md-1,.col-lg-1,.col-xs-2,.col-sm-2,.col-md-2,.col-lg-2,.col-xs-3,.col-sm-3,.col-md-3,.col-lg-3,.col-xs-4,.col-sm-4,.col-md-4,.col-lg-4,.col-xs-5,.col-sm-5,.col-md-5,.col-lg-5,.col-xs-6,.col-sm-6,.col-md-6,.col-lg-6,.col-xs-7,.col-sm-7,.col-md-7,.col-lg-7,.col-xs-8,.col-sm-8,.col-md-8,.col-lg-8,.col-xs-9,.col-sm-9,.col-md-9,.col-lg-9,.col-xs-10,.col-sm-10,.col-md-10,.col-lg-10,.col-xs-11,.col-sm-11,.col-md-11,.col-lg-11,.col-xs-12,.col-sm-12,.col-md-12,.col-lg-12{position:relative;min-height:1px;padding-right:15px;padding-left:15px}.col-xs-1,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9,.col-xs-10,.col-xs-11,.col-xs-12{float:left}.col-xs-12{width:100%}.col-xs-11{width:91.66666667%}.col-xs-10{width:83.33333333%}.col-xs-9{width:75%}.col-xs-8{width:66.66666667%}.col-xs-7{width:58.33333333%}.col-xs-6{width:50%}.col-xs-5{width:41.66666667%}.col-xs-4{width:33.33333333%}.col-xs-3{width:25%}.col-xs-2{width:16.66666667%}.col-xs-1{width:8.33333333%}.col-xs-pull-12{right:100%}.col-xs-pull-11{right:91.66666667%}.col-xs-pull-10{right:83.33333333%}.col-xs-pull-9{right:75%}.col-xs-pull-8{right:66.66666667%}.col-xs-pull-7{right:58.33333333%}.col-xs-pull-6{right:50%}.col-xs-pull-5{right:41.66666667%}.col-xs-pull-4{right:33.33333333%}.col-xs-pull-3{right:25%}.col-xs-pull-2{right:16.66666667%}.col-xs-pull-1{right:8.33333333%}.col-xs-pull-0{right:auto}.col-xs-push-12{left:100%}.col-xs-push-11{left:91.66666667%}.col-xs-push-10{left:83.33333333%}.col-xs-push-9{left:75%}.col-xs-push-8{left:66.66666667%}.col-xs-push-7{left:58.33333333%}.col-xs-push-6{left:50%}.col-xs-push-5{left:41.66666667%}.col-xs-push-4{left:33.33333333%}.col-xs-push-3{left:25%}.col-xs-push-2{left:16.66666667%}.col-xs-push-1{left:8.33333333%}.col-xs-push-0{left:auto}.col-xs-offset-12{margin-left:100%}.col-xs-offset-11{margin-left:91.66666667%}.col-xs-offset-10{margin-left:83.33333333%}.col-xs-offset-9{margin-left:75%}.col-xs-offset-8{margin-left:66.66666667%}.col-xs-offset-7{margin-left:58.33333333%}.col-xs-offset-6{margin-left:50%}.col-xs-offset-5{margin-left:41.66666667%}.col-xs-offset-4{margin-left:33.33333333%}.col-xs-offset-3{margin-left:25%}.col-xs-offset-2{margin-left:16.66666667%}.col-xs-offset-1{margin-left:8.33333333%}.col-xs-offset-0{margin-left:0}@media (min-width:768px){.col-sm-1,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-sm-10,.col-sm-11,.col-sm-12{float:left}.col-sm-12{width:100%}.col-sm-11{width:91.66666667%}.col-sm-10{width:83.33333333%}.col-sm-9{width:75%}.col-sm-8{width:66.66666667%}.col-sm-7{width:58.33333333%}.col-sm-6{width:50%}.col-sm-5{width:41.66666667%}.col-sm-4{width:33.33333333%}.col-sm-3{width:25%}.col-sm-2{width:16.66666667%}.col-sm-1{width:8.33333333%}.col-sm-pull-12{right:100%}.col-sm-pull-11{right:91.66666667%}.col-sm-pull-10{right:83.33333333%}.col-sm-pull-9{right:75%}.col-sm-pull-8{right:66.66666667%}.col-sm-pull-7{right:58.33333333%}.col-sm-pull-6{right:50%}.col-sm-pull-5{right:41.66666667%}.col-sm-pull-4{right:33.33333333%}.col-sm-pull-3{right:25%}.col-sm-pull-2{right:16.66666667%}.col-sm-pull-1{right:8.33333333%}.col-sm-pull-0{right:auto}.col-sm-push-12{left:100%}.col-sm-push-11{left:91.66666667%}.col-sm-push-10{left:83.33333333%}.col-sm-push-9{left:75%}.col-sm-push-8{left:66.66666667%}.col-sm-push-7{left:58.33333333%}.col-sm-push-6{left:50%}.col-sm-push-5{left:41.66666667%}.col-sm-push-4{left:33.33333333%}.col-sm-push-3{left:25%}.col-sm-push-2{left:16.66666667%}.col-sm-push-1{left:8.33333333%}.col-sm-push-0{left:auto}.col-sm-offset-12{margin-left:100%}.col-sm-offset-11{margin-left:91.66666667%}.col-sm-offset-10{margin-left:83.33333333%}.col-sm-offset-9{margin-left:75%}.col-sm-offset-8{margin-left:66.66666667%}.col-sm-offset-7{margin-left:58.33333333%}.col-sm-offset-6{margin-left:50%}.col-sm-offset-5{margin-left:41.66666667%}.col-sm-offset-4{margin-left:33.33333333%}.col-sm-offset-3{margin-left:25%}.col-sm-offset-2{margin-left:16.66666667%}.col-sm-offset-1{margin-left:8.33333333%}.col-sm-offset-0{margin-left:0}}@media (min-width:992px){.col-md-1,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-md-10,.col-md-11,.col-md-12{float:left}.col-md-12{width:100%}.col-md-11{width:91.66666667%}.col-md-10{width:83.33333333%}.col-md-9{width:75%}.col-md-8{width:66.66666667%}.col-md-7{width:58.33333333%}.col-md-6{width:50%}.col-md-5{width:41.66666667%}.col-md-4{width:33.33333333%}.col-md-3{width:25%}.col-md-2{width:16.66666667%}.col-md-1{width:8.33333333%}.col-md-pull-12{right:100%}.col-md-pull-11{right:91.66666667%}.col-md-pull-10{right:83.33333333%}.col-md-pull-9{right:75%}.col-md-pull-8{right:66.66666667%}.col-md-pull-7{right:58.33333333%}.col-md-pull-6{right:50%}.col-md-pull-5{right:41.66666667%}.col-md-pull-4{right:33.33333333%}.col-md-pull-3{right:25%}.col-md-pull-2{right:16.66666667%}.col-md-pull-1{right:8.33333333%}.col-md-pull-0{right:auto}.col-md-push-12{left:100%}.col-md-push-11{left:91.66666667%}.col-md-push-10{left:83.33333333%}.col-md-push-9{left:75%}.col-md-push-8{left:66.66666667%}.col-md-push-7{left:58.33333333%}.col-md-push-6{left:50%}.col-md-push-5{left:41.66666667%}.col-md-push-4{left:33.33333333%}.col-md-push-3{left:25%}.col-md-push-2{left:16.66666667%}.col-md-push-1{left:8.33333333%}.col-md-push-0{left:auto}.col-md-offset-12{margin-left:100%}.col-md-offset-11{margin-left:91.66666667%}.col-md-offset-10{margin-left:83.33333333%}.col-md-offset-9{margin-left:75%}.col-md-offset-8{margin-left:66.66666667%}.col-md-offset-7{margin-left:58.33333333%}.col-md-offset-6{margin-left:50%}.col-md-offset-5{margin-left:41.66666667%}.col-md-offset-4{margin-left:33.33333333%}.col-md-offset-3{margin-left:25%}.col-md-offset-2{margin-left:16.66666667%}.col-md-offset-1{margin-left:8.33333333%}.col-md-offset-0{margin-left:0}}@media (min-width:1200px){.col-lg-1,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-lg-10,.col-lg-11,.col-lg-12{float:left}.col-lg-12{width:100%}.col-lg-11{width:91.66666667%}.col-lg-10{width:83.33333333%}.col-lg-9{width:75%}.col-lg-8{width:66.66666667%}.col-lg-7{width:58.33333333%}.col-lg-6{width:50%}.col-lg-5{width:41.66666667%}.col-lg-4{width:33.33333333%}.col-lg-3{width:25%}.col-lg-2{width:16.66666667%}.col-lg-1{width:8.33333333%}.col-lg-pull-12{right:100%}.col-lg-pull-11{right:91.66666667%}.col-lg-pull-10{right:83.33333333%}.col-lg-pull-9{right:75%}.col-lg-pull-8{right:66.66666667%}.col-lg-pull-7{right:58.33333333%}.col-lg-pull-6{right:50%}.col-lg-pull-5{right:41.66666667%}.col-lg-pull-4{right:33.33333333%}.col-lg-pull-3{right:25%}.col-lg-pull-2{right:16.66666667%}.col-lg-pull-1{right:8.33333333%}.col-lg-pull-0{right:auto}.col-lg-push-12{left:100%}.col-lg-push-11{left:91.66666667%}.col-lg-push-10{left:83.33333333%}.col-lg-push-9{left:75%}.col-lg-push-8{left:66.66666667%}.col-lg-push-7{left:58.33333333%}.col-lg-push-6{left:50%}.col-lg-push-5{left:41.66666667%}.col-lg-push-4{left:33.33333333%}.col-lg-push-3{left:25%}.col-lg-push-2{left:16.66666667%}.col-lg-push-1{left:8.33333333%}.col-lg-push-0{left:auto}.col-lg-offset-12{margin-left:100%}.col-lg-offset-11{margin-left:91.66666667%}.col-lg-offset-10{margin-left:83.33333333%}.col-lg-offset-9{margin-left:75%}.col-lg-offset-8{margin-left:66.66666667%}.col-lg-offset-7{margin-left:58.33333333%}.col-lg-offset-6{margin-left:50%}.col-lg-offset-5{margin-left:41.66666667%}.col-lg-offset-4{margin-left:33.33333333%}.col-lg-offset-3{margin-left:25%}.col-lg-offset-2{margin-left:16.66666667%}.col-lg-offset-1{margin-left:8.33333333%}.col-lg-offset-0{margin-left:0}}table{background-color:transparent}caption{padding-top:8px;padding-bottom:8px;color:#777;text-align:left}th{text-align:left}.table{width:100%;max-width:100%;margin-bottom:20px}.table>thead>tr>th,.table>tbody>tr>th,.table>tfoot>tr>th,.table>thead>tr>td,.table>tbody>tr>td,.table>tfoot>tr>td{padding:8px;line-height:1.42857143;vertical-align:top;border-top:1px solid #ddd}.table>thead>tr>th{vertical-align:bottom;border-bottom:2px solid #ddd}.table>caption+thead>tr:first-child>th,.table>colgroup+thead>tr:first-child>th,.table>thead:first-child>tr:first-child>th,.table>caption+thead>tr:first-child>td,.table>colgroup+thead>tr:first-child>td,.table>thead:first-child>tr:first-child>td{border-top:0}.table>tbody+tbody{border-top:2px solid #ddd}.table .table{background-color:#fff}.table-condensed>thead>tr>th,.table-condensed>tbody>tr>th,.table-condensed>tfoot>tr>th,.table-condensed>thead>tr>td,.table-condensed>tbody>tr>td,.table-condensed>tfoot>tr>td{padding:5px}.table-bordered{border:1px solid #ddd}.table-bordered>thead>tr>th,.table-bordered>tbody>tr>th,.table-bordered>tfoot>tr>th,.table-bordered>thead>tr>td,.table-bordered>tbody>tr>td,.table-bordered>tfoot>tr>td{border:1px solid #ddd}.table-bordered>thead>tr>th,.table-bordered>thead>tr>td{border-bottom-width:2px}.table-striped>tbody>tr:nth-child(odd){background-color:#f9f9f9}.table-hover>tbody>tr:hover{background-color:#f5f5f5}table col[class*=col-]{position:static;display:table-column;float:none}table td[class*=col-],table th[class*=col-]{position:static;display:table-cell;float:none}.table>thead>tr>td.active,.table>tbody>tr>td.active,.table>tfoot>tr>td.active,.table>thead>tr>th.active,.table>tbody>tr>th.active,.table>tfoot>tr>th.active,.table>thead>tr.active>td,.table>tbody>tr.active>td,.table>tfoot>tr.active>td,.table>thead>tr.active>th,.table>tbody>tr.active>th,.table>tfoot>tr.active>th{background-color:#f5f5f5}.table-hover>tbody>tr>td.active:hover,.table-hover>tbody>tr>th.active:hover,.table-hover>tbody>tr.active:hover>td,.table-hover>tbody>tr:hover>.active,.table-hover>tbody>tr.active:hover>th{background-color:#e8e8e8}.table>thead>tr>td.success,.table>tbody>tr>td.success,.table>tfoot>tr>td.success,.table>thead>tr>th.success,.table>tbody>tr>th.success,.table>tfoot>tr>th.success,.table>thead>tr.success>td,.table>tbody>tr.success>td,.table>tfoot>tr.success>td,.table>thead>tr.success>th,.table>tbody>tr.success>th,.table>tfoot>tr.success>th{background-color:#dff0d8}.table-hover>tbody>tr>td.success:hover,.table-hover>tbody>tr>th.success:hover,.table-hover>tbody>tr.success:hover>td,.table-hover>tbody>tr:hover>.success,.table-hover>tbody>tr.success:hover>th{background-color:#d0e9c6}.table>thead>tr>td.info,.table>tbody>tr>td.info,.table>tfoot>tr>td.info,.table>thead>tr>th.info,.table>tbody>tr>th.info,.table>tfoot>tr>th.info,.table>thead>tr.info>td,.table>tbody>tr.info>td,.table>tfoot>tr.info>td,.table>thead>tr.info>th,.table>tbody>tr.info>th,.table>tfoot>tr.info>th{background-color:#d9edf7}.table-hover>tbody>tr>td.info:hover,.table-hover>tbody>tr>th.info:hover,.table-hover>tbody>tr.info:hover>td,.table-hover>tbody>tr:hover>.info,.table-hover>tbody>tr.info:hover>th{background-color:#c4e3f3}.table>thead>tr>td.warning,.table>tbody>tr>td.warning,.table>tfoot>tr>td.warning,.table>thead>tr>th.warning,.table>tbody>tr>th.warning,.table>tfoot>tr>th.warning,.table>thead>tr.warning>td,.table>tbody>tr.warning>td,.table>tfoot>tr.warning>td,.table>thead>tr.warning>th,.table>tbody>tr.warning>th,.table>tfoot>tr.warning>th{background-color:#fcf8e3}.table-hover>tbody>tr>td.warning:hover,.table-hover>tbody>tr>th.warning:hover,.table-hover>tbody>tr.warning:hover>td,.table-hover>tbody>tr:hover>.warning,.table-hover>tbody>tr.warning:hover>th{background-color:#faf2cc}.table>thead>tr>td.danger,.table>tbody>tr>td.danger,.table>tfoot>tr>td.danger,.table>thead>tr>th.danger,.table>tbody>tr>th.danger,.table>tfoot>tr>th.danger,.table>thead>tr.danger>td,.table>tbody>tr.danger>td,.table>tfoot>tr.danger>td,.table>thead>tr.danger>th,.table>tbody>tr.danger>th,.table>tfoot>tr.danger>th{background-color:#f2dede}.table-hover>tbody>tr>td.danger:hover,.table-hover>tbody>tr>th.danger:hover,.table-hover>tbody>tr.danger:hover>td,.table-hover>tbody>tr:hover>.danger,.table-hover>tbody>tr.danger:hover>th{background-color:#ebcccc}.table-responsive{min-height:.01%;overflow-x:auto}@media screen and (max-width:767px){.table-responsive{width:100%;margin-bottom:15px;overflow-y:hidden;-ms-overflow-style:-ms-autohiding-scrollbar;border:1px solid #ddd}.table-responsive>.table{margin-bottom:0}.table-responsive>.table>thead>tr>th,.table-responsive>.table>tbody>tr>th,.table-responsive>.table>tfoot>tr>th,.table-responsive>.table>thead>tr>td,.table-responsive>.table>tbody>tr>td,.table-responsive>.table>tfoot>tr>td{white-space:nowrap}.table-responsive>.table-bordered{border:0}.table-responsive>.table-bordered>thead>tr>th:first-child,.table-responsive>.table-bordered>tbody>tr>th:first-child,.table-responsive>.table-bordered>tfoot>tr>th:first-child,.table-responsive>.table-bordered>thead>tr>td:first-child,.table-responsive>.table-bordered>tbody>tr>td:first-child,.table-responsive>.table-bordered>tfoot>tr>td:first-child{border-left:0}.table-responsive>.table-bordered>thead>tr>th:last-child,.table-responsive>.table-bordered>tbody>tr>th:last-child,.table-responsive>.table-bordered>tfoot>tr>th:last-child,.table-responsive>.table-bordered>thead>tr>td:last-child,.table-responsive>.table-bordered>tbody>tr>td:last-child,.table-responsive>.table-bordered>tfoot>tr>td:last-child{border-right:0}.table-responsive>.table-bordered>tbody>tr:last-child>th,.table-responsive>.table-bordered>tfoot>tr:last-child>th,.table-responsive>.table-bordered>tbody>tr:last-child>td,.table-responsive>.table-bordered>tfoot>tr:last-child>td{border-bottom:0}}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;padding:0;margin-bottom:20px;font-size:21px;line-height:inherit;color:#333;border:0;border-bottom:1px solid #e5e5e5}label{display:inline-block;max-width:100%;margin-bottom:5px;font-weight:700}input[type=search]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}input[type=radio],input[type=checkbox]{margin:4px 0 0;margin-top:1px \9;line-height:normal}input[type=file]{display:block}input[type=range]{display:block;width:100%}select[multiple],select[size]{height:auto}input[type=file]:focus,input[type=radio]:focus,input[type=checkbox]:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}output{display:block;padding-top:7px;font-size:14px;line-height:1.42857143;color:#555}.form-control{display:block;width:100%;height:34px;padding:6px 12px;font-size:14px;line-height:1.42857143;color:#555;background-color:#fff;background-image:none;border:1px solid #ccc;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075);-webkit-transition:border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s;-o-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s}.form-control:focus{border-color:#66afe9;outline:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6);box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6)}.form-control::-moz-placeholder{color:#999;opacity:1}.form-control:-ms-input-placeholder{color:#999}.form-control::-webkit-input-placeholder{color:#999}.form-control[disabled],.form-control[readonly],fieldset[disabled] .form-control{cursor:not-allowed;background-color:#eee;opacity:1}textarea.form-control{height:auto}input[type=search]{-webkit-appearance:none}@media screen and (-webkit-min-device-pixel-ratio:0){input[type=date],input[type=time],input[type=datetime-local],input[type=month]{line-height:34px}input[type=date].input-sm,input[type=time].input-sm,input[type=datetime-local].input-sm,input[type=month].input-sm{line-height:30px}input[type=date].input-lg,input[type=time].input-lg,input[type=datetime-local].input-lg,input[type=month].input-lg{line-height:46px}}.form-group{margin-bottom:15px}.radio,.checkbox{position:relative;display:block;margin-top:10px;margin-bottom:10px}.radio label,.checkbox label{min-height:20px;padding-left:20px;margin-bottom:0;font-weight:400;cursor:pointer}.radio input[type=radio],.radio-inline input[type=radio],.checkbox input[type=checkbox],.checkbox-inline input[type=checkbox]{position:absolute;margin-top:4px \9;margin-left:-20px}.radio+.radio,.checkbox+.checkbox{margin-top:-5px}.radio-inline,.checkbox-inline{display:inline-block;padding-left:20px;margin-bottom:0;font-weight:400;vertical-align:middle;cursor:pointer}.radio-inline+.radio-inline,.checkbox-inline+.checkbox-inline{margin-top:0;margin-left:10px}input[type=radio][disabled],input[type=checkbox][disabled],input[type=radio].disabled,input[type=checkbox].disabled,fieldset[disabled] input[type=radio],fieldset[disabled] input[type=checkbox]{cursor:not-allowed}.radio-inline.disabled,.checkbox-inline.disabled,fieldset[disabled] .radio-inline,fieldset[disabled] .checkbox-inline{cursor:not-allowed}.radio.disabled label,.checkbox.disabled label,fieldset[disabled] .radio label,fieldset[disabled] .checkbox label{cursor:not-allowed}.form-control-static{padding-top:7px;padding-bottom:7px;margin-bottom:0}.form-control-static.input-lg,.form-control-static.input-sm{padding-right:0;padding-left:0}.input-sm,.form-group-sm .form-control{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-sm,select.form-group-sm .form-control{height:30px;line-height:30px}textarea.input-sm,textarea.form-group-sm .form-control,select[multiple].input-sm,select[multiple].form-group-sm .form-control{height:auto}.input-lg,.form-group-lg .form-control{height:46px;padding:10px 16px;font-size:18px;line-height:1.33;border-radius:6px}select.input-lg,select.form-group-lg .form-control{height:46px;line-height:46px}textarea.input-lg,textarea.form-group-lg .form-control,select[multiple].input-lg,select[multiple].form-group-lg .form-control{height:auto}.has-feedback{position:relative}.has-feedback .form-control{padding-right:42.5px}.form-control-feedback{position:absolute;top:0;right:0;z-index:2;display:block;width:34px;height:34px;line-height:34px;text-align:center;pointer-events:none}.input-lg+.form-control-feedback{width:46px;height:46px;line-height:46px}.input-sm+.form-control-feedback{width:30px;height:30px;line-height:30px}.has-success .help-block,.has-success .control-label,.has-success .radio,.has-success .checkbox,.has-success .radio-inline,.has-success .checkbox-inline,.has-success.radio label,.has-success.checkbox label,.has-success.radio-inline label,.has-success.checkbox-inline label{color:#3c763d}.has-success .form-control{border-color:#3c763d;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-success .form-control:focus{border-color:#2b542c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168}.has-success .input-group-addon{color:#3c763d;background-color:#dff0d8;border-color:#3c763d}.has-success .form-control-feedback{color:#3c763d}.has-warning .help-block,.has-warning .control-label,.has-warning .radio,.has-warning .checkbox,.has-warning .radio-inline,.has-warning .checkbox-inline,.has-warning.radio label,.has-warning.checkbox label,.has-warning.radio-inline label,.has-warning.checkbox-inline label{color:#8a6d3b}.has-warning .form-control{border-color:#8a6d3b;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-warning .form-control:focus{border-color:#66512c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b}.has-warning .input-group-addon{color:#8a6d3b;background-color:#fcf8e3;border-color:#8a6d3b}.has-warning .form-control-feedback{color:#8a6d3b}.has-error .help-block,.has-error .control-label,.has-error .radio,.has-error .checkbox,.has-error .radio-inline,.has-error .checkbox-inline,.has-error.radio label,.has-error.checkbox label,.has-error.radio-inline label,.has-error.checkbox-inline label{color:#a94442}.has-error .form-control{border-color:#a94442;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-error .form-control:focus{border-color:#843534;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483}.has-error .input-group-addon{color:#a94442;background-color:#f2dede;border-color:#a94442}.has-error .form-control-feedback{color:#a94442}.has-feedback label~.form-control-feedback{top:25px}.has-feedback label.sr-only~.form-control-feedback{top:0}.help-block{display:block;margin-top:5px;margin-bottom:10px;color:#737373}@media (min-width:768px){.form-inline .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .form-control-static{display:inline-block}.form-inline .input-group{display:inline-table;vertical-align:middle}.form-inline .input-group .input-group-addon,.form-inline .input-group .input-group-btn,.form-inline .input-group .form-control{width:auto}.form-inline .input-group>.form-control{width:100%}.form-inline .control-label{margin-bottom:0;vertical-align:middle}.form-inline .radio,.form-inline .checkbox{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.form-inline .radio label,.form-inline .checkbox label{padding-left:0}.form-inline .radio input[type=radio],.form-inline .checkbox input[type=checkbox]{position:relative;margin-left:0}.form-inline .has-feedback .form-control-feedback{top:0}}.form-horizontal .radio,.form-horizontal .checkbox,.form-horizontal .radio-inline,.form-horizontal .checkbox-inline{padding-top:7px;margin-top:0;margin-bottom:0}.form-horizontal .radio,.form-horizontal .checkbox{min-height:27px}.form-horizontal .form-group{margin-right:-15px;margin-left:-15px}@media (min-width:768px){.form-horizontal .control-label{padding-top:7px;margin-bottom:0;text-align:right}}.form-horizontal .has-feedback .form-control-feedback{right:15px}@media (min-width:768px){.form-horizontal .form-group-lg .control-label{padding-top:14.3px}}@media (min-width:768px){.form-horizontal .form-group-sm .control-label{padding-top:6px}}.btn{display:inline-block;padding:6px 12px;margin-bottom:0;font-size:14px;font-weight:400;line-height:1.42857143;text-align:center;white-space:nowrap;vertical-align:middle;-ms-touch-action:manipulation;touch-action:manipulation;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background-image:none;border:1px solid transparent;border-radius:4px}.btn:focus,.btn:active:focus,.btn.active:focus,.btn.focus,.btn:active.focus,.btn.active.focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.btn:hover,.btn:focus,.btn.focus{color:#333;text-decoration:none}.btn:active,.btn.active{background-image:none;outline:0;-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn.disabled,.btn[disabled],fieldset[disabled] .btn{pointer-events:none;cursor:not-allowed;filter:alpha(opacity=65);-webkit-box-shadow:none;box-shadow:none;opacity:.65}.btn-default{color:#333;background-color:#fff;border-color:#ccc}.btn-default:hover,.btn-default:focus,.btn-default.focus,.btn-default:active,.btn-default.active,.open>.dropdown-toggle.btn-default{color:#333;background-color:#e6e6e6;border-color:#adadad}.btn-default:active,.btn-default.active,.open>.dropdown-toggle.btn-default{background-image:none}.btn-default.disabled,.btn-default[disabled],fieldset[disabled] .btn-default,.btn-default.disabled:hover,.btn-default[disabled]:hover,fieldset[disabled] .btn-default:hover,.btn-default.disabled:focus,.btn-default[disabled]:focus,fieldset[disabled] .btn-default:focus,.btn-default.disabled.focus,.btn-default[disabled].focus,fieldset[disabled] .btn-default.focus,.btn-default.disabled:active,.btn-default[disabled]:active,fieldset[disabled] .btn-default:active,.btn-default.disabled.active,.btn-default[disabled].active,fieldset[disabled] .btn-default.active{background-color:#fff;border-color:#ccc}.btn-default .badge{color:#fff;background-color:#333}.btn-primary{color:#fff;background-color:#337ab7;border-color:#2e6da4}.btn-primary:hover,.btn-primary:focus,.btn-primary.focus,.btn-primary:active,.btn-primary.active,.open>.dropdown-toggle.btn-primary{color:#fff;background-color:#286090;border-color:#204d74}.btn-primary:active,.btn-primary.active,.open>.dropdown-toggle.btn-primary{background-image:none}.btn-primary.disabled,.btn-primary[disabled],fieldset[disabled] .btn-primary,.btn-primary.disabled:hover,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary:hover,.btn-primary.disabled:focus,.btn-primary[disabled]:focus,fieldset[disabled] .btn-primary:focus,.btn-primary.disabled.focus,.btn-primary[disabled].focus,fieldset[disabled] .btn-primary.focus,.btn-primary.disabled:active,.btn-primary[disabled]:active,fieldset[disabled] .btn-primary:active,.btn-primary.disabled.active,.btn-primary[disabled].active,fieldset[disabled] .btn-primary.active{background-color:#337ab7;border-color:#2e6da4}.btn-primary .badge{color:#337ab7;background-color:#fff}.btn-success{color:#fff;background-color:#5cb85c;border-color:#4cae4c}.btn-success:hover,.btn-success:focus,.btn-success.focus,.btn-success:active,.btn-success.active,.open>.dropdown-toggle.btn-success{color:#fff;background-color:#449d44;border-color:#398439}.btn-success:active,.btn-success.active,.open>.dropdown-toggle.btn-success{background-image:none}.btn-success.disabled,.btn-success[disabled],fieldset[disabled] .btn-success,.btn-success.disabled:hover,.btn-success[disabled]:hover,fieldset[disabled] .btn-success:hover,.btn-success.disabled:focus,.btn-success[disabled]:focus,fieldset[disabled] .btn-success:focus,.btn-success.disabled.focus,.btn-success[disabled].focus,fieldset[disabled] .btn-success.focus,.btn-success.disabled:active,.btn-success[disabled]:active,fieldset[disabled] .btn-success:active,.btn-success.disabled.active,.btn-success[disabled].active,fieldset[disabled] .btn-success.active{background-color:#5cb85c;border-color:#4cae4c}.btn-success .badge{color:#5cb85c;background-color:#fff}.btn-info{color:#fff;background-color:#5bc0de;border-color:#46b8da}.btn-info:hover,.btn-info:focus,.btn-info.focus,.btn-info:active,.btn-info.active,.open>.dropdown-toggle.btn-info{color:#fff;background-color:#31b0d5;border-color:#269abc}.btn-info:active,.btn-info.active,.open>.dropdown-toggle.btn-info{background-image:none}.btn-info.disabled,.btn-info[disabled],fieldset[disabled] .btn-info,.btn-info.disabled:hover,.btn-info[disabled]:hover,fieldset[disabled] .btn-info:hover,.btn-info.disabled:focus,.btn-info[disabled]:focus,fieldset[disabled] .btn-info:focus,.btn-info.disabled.focus,.btn-info[disabled].focus,fieldset[disabled] .btn-info.focus,.btn-info.disabled:active,.btn-info[disabled]:active,fieldset[disabled] .btn-info:active,.btn-info.disabled.active,.btn-info[disabled].active,fieldset[disabled] .btn-info.active{background-color:#5bc0de;border-color:#46b8da}.btn-info .badge{color:#5bc0de;background-color:#fff}.btn-warning{color:#fff;background-color:#f0ad4e;border-color:#eea236}.btn-warning:hover,.btn-warning:focus,.btn-warning.focus,.btn-warning:active,.btn-warning.active,.open>.dropdown-toggle.btn-warning{color:#fff;background-color:#ec971f;border-color:#d58512}.btn-warning:active,.btn-warning.active,.open>.dropdown-toggle.btn-warning{background-image:none}.btn-warning.disabled,.btn-warning[disabled],fieldset[disabled] .btn-warning,.btn-warning.disabled:hover,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning:hover,.btn-warning.disabled:focus,.btn-warning[disabled]:focus,fieldset[disabled] .btn-warning:focus,.btn-warning.disabled.focus,.btn-warning[disabled].focus,fieldset[disabled] .btn-warning.focus,.btn-warning.disabled:active,.btn-warning[disabled]:active,fieldset[disabled] .btn-warning:active,.btn-warning.disabled.active,.btn-warning[disabled].active,fieldset[disabled] .btn-warning.active{background-color:#f0ad4e;border-color:#eea236}.btn-warning .badge{color:#f0ad4e;background-color:#fff}.btn-danger{color:#fff;background-color:#d9534f;border-color:#d43f3a}.btn-danger:hover,.btn-danger:focus,.btn-danger.focus,.btn-danger:active,.btn-danger.active,.open>.dropdown-toggle.btn-danger{color:#fff;background-color:#c9302c;border-color:#ac2925}.btn-danger:active,.btn-danger.active,.open>.dropdown-toggle.btn-danger{background-image:none}.btn-danger.disabled,.btn-danger[disabled],fieldset[disabled] .btn-danger,.btn-danger.disabled:hover,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger:hover,.btn-danger.disabled:focus,.btn-danger[disabled]:focus,fieldset[disabled] .btn-danger:focus,.btn-danger.disabled.focus,.btn-danger[disabled].focus,fieldset[disabled] .btn-danger.focus,.btn-danger.disabled:active,.btn-danger[disabled]:active,fieldset[disabled] .btn-danger:active,.btn-danger.disabled.active,.btn-danger[disabled].active,fieldset[disabled] .btn-danger.active{background-color:#d9534f;border-color:#d43f3a}.btn-danger .badge{color:#d9534f;background-color:#fff}.btn-link{font-weight:400;color:#337ab7;border-radius:0}.btn-link,.btn-link:active,.btn-link.active,.btn-link[disabled],fieldset[disabled] .btn-link{background-color:transparent;-webkit-box-shadow:none;box-shadow:none}.btn-link,.btn-link:hover,.btn-link:focus,.btn-link:active{border-color:transparent}.btn-link:hover,.btn-link:focus{color:#23527c;text-decoration:underline;background-color:transparent}.btn-link[disabled]:hover,fieldset[disabled] .btn-link:hover,.btn-link[disabled]:focus,fieldset[disabled] .btn-link:focus{color:#777;text-decoration:none}.btn-lg,.btn-group-lg>.btn{padding:10px 16px;font-size:18px;line-height:1.33;border-radius:6px}.btn-sm,.btn-group-sm>.btn{padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.btn-xs,.btn-group-xs>.btn{padding:1px 5px;font-size:12px;line-height:1.5;border-radius:3px}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:5px}input[type=submit].btn-block,input[type=reset].btn-block,input[type=button].btn-block{width:100%}.fade{opacity:0;-webkit-transition:opacity .15s linear;-o-transition:opacity .15s linear;transition:opacity .15s linear}.fade.in{opacity:1}.collapse{display:none;visibility:hidden}.collapse.in{display:block;visibility:visible}tr.collapse.in{display:table-row}tbody.collapse.in{display:table-row-group}.collapsing{position:relative;height:0;overflow:hidden;-webkit-transition-timing-function:ease;-o-transition-timing-function:ease;transition-timing-function:ease;-webkit-transition-duration:.35s;-o-transition-duration:.35s;transition-duration:.35s;-webkit-transition-property:height,visibility;-o-transition-property:height,visibility;transition-property:height,visibility}.caret{display:inline-block;width:0;height:0;margin-left:2px;vertical-align:middle;border-top:4px solid;border-right:4px solid transparent;border-left:4px solid transparent}.dropdown{position:relative}.dropdown-toggle:focus{outline:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:160px;padding:5px 0;margin:2px 0 0;font-size:14px;text-align:left;list-style:none;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid #ccc;border:1px solid rgba(0,0,0,.15);border-radius:4px;-webkit-box-shadow:0 6px 12px rgba(0,0,0,.175);box-shadow:0 6px 12px rgba(0,0,0,.175)}.dropdown-menu.pull-right{right:0;left:auto}.dropdown-menu .divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.dropdown-menu>li>a{display:block;padding:3px 20px;clear:both;font-weight:400;line-height:1.42857143;color:#333;white-space:nowrap}.dropdown-menu>li>a:hover,.dropdown-menu>li>a:focus{color:#262626;text-decoration:none;background-color:#f5f5f5}.dropdown-menu>.active>a,.dropdown-menu>.active>a:hover,.dropdown-menu>.active>a:focus{color:#fff;text-decoration:none;background-color:#337ab7;outline:0}.dropdown-menu>.disabled>a,.dropdown-menu>.disabled>a:hover,.dropdown-menu>.disabled>a:focus{color:#777}.dropdown-menu>.disabled>a:hover,.dropdown-menu>.disabled>a:focus{text-decoration:none;cursor:not-allowed;background-color:transparent;background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.open>.dropdown-menu{display:block}.open>a{outline:0}.dropdown-menu-right{right:0;left:auto}.dropdown-menu-left{right:auto;left:0}.dropdown-header{display:block;padding:3px 20px;font-size:12px;line-height:1.42857143;color:#777;white-space:nowrap}.dropdown-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:990}.pull-right>.dropdown-menu{right:0;left:auto}.dropup .caret,.navbar-fixed-bottom .dropdown .caret{content:"";border-top:0;border-bottom:4px solid}.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{top:auto;bottom:100%;margin-bottom:1px}@media (min-width:768px){.navbar-right .dropdown-menu{right:0;left:auto}.navbar-right .dropdown-menu-left{right:auto;left:0}}.btn-group,.btn-group-vertical{position:relative;display:inline-block;vertical-align:middle}.btn-group>.btn,.btn-group-vertical>.btn{position:relative;float:left}.btn-group>.btn:hover,.btn-group-vertical>.btn:hover,.btn-group>.btn:focus,.btn-group-vertical>.btn:focus,.btn-group>.btn:active,.btn-group-vertical>.btn:active,.btn-group>.btn.active,.btn-group-vertical>.btn.active{z-index:2}.btn-group .btn+.btn,.btn-group .btn+.btn-group,.btn-group .btn-group+.btn,.btn-group .btn-group+.btn-group{margin-left:-1px}.btn-toolbar{margin-left:-5px}.btn-toolbar .btn-group,.btn-toolbar .input-group{float:left}.btn-toolbar>.btn,.btn-toolbar>.btn-group,.btn-toolbar>.input-group{margin-left:5px}.btn-group>.btn:not(:first-child):not(:last-child):not(.dropdown-toggle){border-radius:0}.btn-group>.btn:first-child{margin-left:0}.btn-group>.btn:first-child:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn:last-child:not(:first-child),.btn-group>.dropdown-toggle:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.btn-group>.btn-group{float:left}.btn-group>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group>.btn-group:first-child>.btn:last-child,.btn-group>.btn-group:first-child>.dropdown-toggle{border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:last-child>.btn:first-child{border-top-left-radius:0;border-bottom-left-radius:0}.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0}.btn-group>.btn+.dropdown-toggle{padding-right:8px;padding-left:8px}.btn-group>.btn-lg+.dropdown-toggle{padding-right:12px;padding-left:12px}.btn-group.open .dropdown-toggle{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn-group.open .dropdown-toggle.btn-link{-webkit-box-shadow:none;box-shadow:none}.btn .caret{margin-left:0}.btn-lg .caret{border-width:5px 5px 0;border-bottom-width:0}.dropup .btn-lg .caret{border-width:0 5px 5px}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group,.btn-group-vertical>.btn-group>.btn{display:block;float:none;width:100%;max-width:100%}.btn-group-vertical>.btn-group>.btn{float:none}.btn-group-vertical>.btn+.btn,.btn-group-vertical>.btn+.btn-group,.btn-group-vertical>.btn-group+.btn,.btn-group-vertical>.btn-group+.btn-group{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn:not(:first-child):not(:last-child){border-radius:0}.btn-group-vertical>.btn:first-child:not(:last-child){border-top-right-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn:last-child:not(:first-child){border-top-left-radius:0;border-top-right-radius:0;border-bottom-left-radius:4px}.btn-group-vertical>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group-vertical>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group-vertical>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-left-radius:0;border-top-right-radius:0}.btn-group-justified{display:table;width:100%;table-layout:fixed;border-collapse:separate}.btn-group-justified>.btn,.btn-group-justified>.btn-group{display:table-cell;float:none;width:1%}.btn-group-justified>.btn-group .btn{width:100%}.btn-group-justified>.btn-group .dropdown-menu{left:auto}[data-toggle=buttons]>.btn input[type=radio],[data-toggle=buttons]>.btn-group>.btn input[type=radio],[data-toggle=buttons]>.btn input[type=checkbox],[data-toggle=buttons]>.btn-group>.btn input[type=checkbox]{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.input-group{position:relative;display:table;border-collapse:separate}.input-group[class*=col-]{float:none;padding-right:0;padding-left:0}.input-group .form-control{position:relative;z-index:2;float:left;width:100%;margin-bottom:0}.input-group-lg>.form-control,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.btn{height:46px;padding:10px 16px;font-size:18px;line-height:1.33;border-radius:6px}select.input-group-lg>.form-control,select.input-group-lg>.input-group-addon,select.input-group-lg>.input-group-btn>.btn{height:46px;line-height:46px}textarea.input-group-lg>.form-control,textarea.input-group-lg>.input-group-addon,textarea.input-group-lg>.input-group-btn>.btn,select[multiple].input-group-lg>.form-control,select[multiple].input-group-lg>.input-group-addon,select[multiple].input-group-lg>.input-group-btn>.btn{height:auto}.input-group-sm>.form-control,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.btn{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-group-sm>.form-control,select.input-group-sm>.input-group-addon,select.input-group-sm>.input-group-btn>.btn{height:30px;line-height:30px}textarea.input-group-sm>.form-control,textarea.input-group-sm>.input-group-addon,textarea.input-group-sm>.input-group-btn>.btn,select[multiple].input-group-sm>.form-control,select[multiple].input-group-sm>.input-group-addon,select[multiple].input-group-sm>.input-group-btn>.btn{height:auto}.input-group-addon,.input-group-btn,.input-group .form-control{display:table-cell}.input-group-addon:not(:first-child):not(:last-child),.input-group-btn:not(:first-child):not(:last-child),.input-group .form-control:not(:first-child):not(:last-child){border-radius:0}.input-group-addon,.input-group-btn{width:1%;white-space:nowrap;vertical-align:middle}.input-group-addon{padding:6px 12px;font-size:14px;font-weight:400;line-height:1;color:#555;text-align:center;background-color:#eee;border:1px solid #ccc;border-radius:4px}.input-group-addon.input-sm{padding:5px 10px;font-size:12px;border-radius:3px}.input-group-addon.input-lg{padding:10px 16px;font-size:18px;border-radius:6px}.input-group-addon input[type=radio],.input-group-addon input[type=checkbox]{margin-top:0}.input-group .form-control:first-child,.input-group-addon:first-child,.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group>.btn,.input-group-btn:first-child>.dropdown-toggle,.input-group-btn:last-child>.btn:not(:last-child):not(.dropdown-toggle),.input-group-btn:last-child>.btn-group:not(:last-child)>.btn{border-top-right-radius:0;border-bottom-right-radius:0}.input-group-addon:first-child{border-right:0}.input-group .form-control:last-child,.input-group-addon:last-child,.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group>.btn,.input-group-btn:last-child>.dropdown-toggle,.input-group-btn:first-child>.btn:not(:first-child),.input-group-btn:first-child>.btn-group:not(:first-child)>.btn{border-top-left-radius:0;border-bottom-left-radius:0}.input-group-addon:last-child{border-left:0}.input-group-btn{position:relative;font-size:0;white-space:nowrap}.input-group-btn>.btn{position:relative}.input-group-btn>.btn+.btn{margin-left:-1px}.input-group-btn>.btn:hover,.input-group-btn>.btn:focus,.input-group-btn>.btn:active{z-index:2}.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group{margin-right:-1px}.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group{margin-left:-1px}.nav{padding-left:0;margin-bottom:0;list-style:none}.nav>li{position:relative;display:block}.nav>li>a{position:relative;display:block;padding:10px 15px}.nav>li>a:hover,.nav>li>a:focus{text-decoration:none;background-color:#eee}.nav>li.disabled>a{color:#777}.nav>li.disabled>a:hover,.nav>li.disabled>a:focus{color:#777;text-decoration:none;cursor:not-allowed;background-color:transparent}.nav .open>a,.nav .open>a:hover,.nav .open>a:focus{background-color:#eee;border-color:#337ab7}.nav .nav-divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.nav>li>a>img{max-width:none}.nav-tabs{border-bottom:1px solid #ddd}.nav-tabs>li{float:left;margin-bottom:-1px}.nav-tabs>li>a{margin-right:2px;line-height:1.42857143;border:1px solid transparent;border-radius:4px 4px 0 0}.nav-tabs>li>a:hover{border-color:#eee #eee #ddd}.nav-tabs>li.active>a,.nav-tabs>li.active>a:hover,.nav-tabs>li.active>a:focus{color:#555;cursor:default;background-color:#fff;border:1px solid #ddd;border-bottom-color:transparent}.nav-tabs.nav-justified{width:100%;border-bottom:0}.nav-tabs.nav-justified>li{float:none}.nav-tabs.nav-justified>li>a{margin-bottom:5px;text-align:center}.nav-tabs.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-tabs.nav-justified>li{display:table-cell;width:1%}.nav-tabs.nav-justified>li>a{margin-bottom:0}}.nav-tabs.nav-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:hover,.nav-tabs.nav-justified>.active>a:focus{border:1px solid #ddd}@media (min-width:768px){.nav-tabs.nav-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:hover,.nav-tabs.nav-justified>.active>a:focus{border-bottom-color:#fff}}.nav-pills>li{float:left}.nav-pills>li>a{border-radius:4px}.nav-pills>li+li{margin-left:2px}.nav-pills>li.active>a,.nav-pills>li.active>a:hover,.nav-pills>li.active>a:focus{color:#fff;background-color:#337ab7}.nav-stacked>li{float:none}.nav-stacked>li+li{margin-top:2px;margin-left:0}.nav-justified{width:100%}.nav-justified>li{float:none}.nav-justified>li>a{margin-bottom:5px;text-align:center}.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-justified>li{display:table-cell;width:1%}.nav-justified>li>a{margin-bottom:0}}.nav-tabs-justified{border-bottom:0}.nav-tabs-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:hover,.nav-tabs-justified>.active>a:focus{border:1px solid #ddd}@media (min-width:768px){.nav-tabs-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:hover,.nav-tabs-justified>.active>a:focus{border-bottom-color:#fff}}.tab-content>.tab-pane{display:none;visibility:hidden}.tab-content>.active{display:block;visibility:visible}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.navbar{position:relative;min-height:50px;margin-bottom:20px;border:1px solid transparent}@media (min-width:768px){.navbar{border-radius:4px}}@media (min-width:768px){.navbar-header{float:left}}.navbar-collapse{padding-right:15px;padding-left:15px;overflow-x:visible;-webkit-overflow-scrolling:touch;border-top:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 0 rgba(255,255,255,.1)}.navbar-collapse.in{overflow-y:auto}@media (min-width:768px){.navbar-collapse{width:auto;border-top:0;-webkit-box-shadow:none;box-shadow:none}.navbar-collapse.collapse{display:block!important;height:auto!important;padding-bottom:0;overflow:visible!important;visibility:visible!important}.navbar-collapse.in{overflow-y:visible}.navbar-fixed-top .navbar-collapse,.navbar-static-top .navbar-collapse,.navbar-fixed-bottom .navbar-collapse{padding-right:0;padding-left:0}}.navbar-fixed-top .navbar-collapse,.navbar-fixed-bottom .navbar-collapse{max-height:340px}@media (max-device-width:480px) and (orientation:landscape){.navbar-fixed-top .navbar-collapse,.navbar-fixed-bottom .navbar-collapse{max-height:200px}}.container>.navbar-header,.container-fluid>.navbar-header,.container>.navbar-collapse,.container-fluid>.navbar-collapse{margin-right:-15px;margin-left:-15px}@media (min-width:768px){.container>.navbar-header,.container-fluid>.navbar-header,.container>.navbar-collapse,.container-fluid>.navbar-collapse{margin-right:0;margin-left:0}}.navbar-static-top{z-index:1000;border-width:0 0 1px}@media (min-width:768px){.navbar-static-top{border-radius:0}}.navbar-fixed-top,.navbar-fixed-bottom{position:fixed;right:0;left:0;z-index:1030}@media (min-width:768px){.navbar-fixed-top,.navbar-fixed-bottom{border-radius:0}}.navbar-fixed-top{top:0;border-width:0 0 1px}.navbar-fixed-bottom{bottom:0;margin-bottom:0;border-width:1px 0 0}.navbar-brand{float:left;height:50px;padding:15px 15px;font-size:18px;line-height:20px}.navbar-brand:hover,.navbar-brand:focus{text-decoration:none}.navbar-brand>img{display:block}@media (min-width:768px){.navbar>.container .navbar-brand,.navbar>.container-fluid .navbar-brand{margin-left:-15px}}.navbar-toggle{position:relative;float:right;padding:9px 10px;margin-top:8px;margin-right:15px;margin-bottom:8px;background-color:transparent;background-image:none;border:1px solid transparent;border-radius:4px}.navbar-toggle:focus{outline:0}.navbar-toggle .icon-bar{display:block;width:22px;height:2px;border-radius:1px}.navbar-toggle .icon-bar+.icon-bar{margin-top:4px}@media (min-width:768px){.navbar-toggle{display:none}}.navbar-nav{margin:7.5px -15px}.navbar-nav>li>a{padding-top:10px;padding-bottom:10px;line-height:20px}@media (max-width:767px){.navbar-nav .open .dropdown-menu{position:static;float:none;width:auto;margin-top:0;background-color:transparent;border:0;-webkit-box-shadow:none;box-shadow:none}.navbar-nav .open .dropdown-menu>li>a,.navbar-nav .open .dropdown-menu .dropdown-header{padding:5px 15px 5px 25px}.navbar-nav .open .dropdown-menu>li>a{line-height:20px}.navbar-nav .open .dropdown-menu>li>a:hover,.navbar-nav .open .dropdown-menu>li>a:focus{background-image:none}}@media (min-width:768px){.navbar-nav{float:left;margin:0}.navbar-nav>li{float:left}.navbar-nav>li>a{padding-top:15px;padding-bottom:15px}}.navbar-form{padding:10px 15px;margin-top:8px;margin-right:-15px;margin-bottom:8px;margin-left:-15px;border-top:1px solid transparent;border-bottom:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1)}@media (min-width:768px){.navbar-form .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.navbar-form .form-control{display:inline-block;width:auto;vertical-align:middle}.navbar-form .form-control-static{display:inline-block}.navbar-form .input-group{display:inline-table;vertical-align:middle}.navbar-form .input-group .input-group-addon,.navbar-form .input-group .input-group-btn,.navbar-form .input-group .form-control{width:auto}.navbar-form .input-group>.form-control{width:100%}.navbar-form .control-label{margin-bottom:0;vertical-align:middle}.navbar-form .radio,.navbar-form .checkbox{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.navbar-form .radio label,.navbar-form .checkbox label{padding-left:0}.navbar-form .radio input[type=radio],.navbar-form .checkbox input[type=checkbox]{position:relative;margin-left:0}.navbar-form .has-feedback .form-control-feedback{top:0}}@media (max-width:767px){.navbar-form .form-group{margin-bottom:5px}.navbar-form .form-group:last-child{margin-bottom:0}}@media (min-width:768px){.navbar-form{width:auto;padding-top:0;padding-bottom:0;margin-right:0;margin-left:0;border:0;-webkit-box-shadow:none;box-shadow:none}}.navbar-nav>li>.dropdown-menu{margin-top:0;border-top-left-radius:0;border-top-right-radius:0}.navbar-fixed-bottom .navbar-nav>li>.dropdown-menu{border-top-left-radius:4px;border-top-right-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0}.navbar-btn{margin-top:8px;margin-bottom:8px}.navbar-btn.btn-sm{margin-top:10px;margin-bottom:10px}.navbar-btn.btn-xs{margin-top:14px;margin-bottom:14px}.navbar-text{margin-top:15px;margin-bottom:15px}@media (min-width:768px){.navbar-text{float:left;margin-right:15px;margin-left:15px}}@media (min-width:768px){.navbar-left{float:left!important}.navbar-right{float:right!important;margin-right:-15px}.navbar-right~.navbar-right{margin-right:0}}.navbar-default{background-color:#f8f8f8;border-color:#e7e7e7}.navbar-default .navbar-brand{color:#777}.navbar-default .navbar-brand:hover,.navbar-default .navbar-brand:focus{color:#5e5e5e;background-color:transparent}.navbar-default .navbar-text{color:#777}.navbar-default .navbar-nav>li>a{color:#777}.navbar-default .navbar-nav>li>a:hover,.navbar-default .navbar-nav>li>a:focus{color:#333;background-color:transparent}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.active>a:hover,.navbar-default .navbar-nav>.active>a:focus{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav>.disabled>a,.navbar-default .navbar-nav>.disabled>a:hover,.navbar-default .navbar-nav>.disabled>a:focus{color:#ccc;background-color:transparent}.navbar-default .navbar-toggle{border-color:#ddd}.navbar-default .navbar-toggle:hover,.navbar-default .navbar-toggle:focus{background-color:#ddd}.navbar-default .navbar-toggle .icon-bar{background-color:#888}.navbar-default .navbar-collapse,.navbar-default .navbar-form{border-color:#e7e7e7}.navbar-default .navbar-nav>.open>a,.navbar-default .navbar-nav>.open>a:hover,.navbar-default .navbar-nav>.open>a:focus{color:#555;background-color:#e7e7e7}@media (max-width:767px){.navbar-default .navbar-nav .open .dropdown-menu>li>a{color:#777}.navbar-default .navbar-nav .open .dropdown-menu>li>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>li>a:focus{color:#333;background-color:transparent}.navbar-default .navbar-nav .open .dropdown-menu>.active>a,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:focus{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:focus{color:#ccc;background-color:transparent}}.navbar-default .navbar-link{color:#777}.navbar-default .navbar-link:hover{color:#333}.navbar-default .btn-link{color:#777}.navbar-default .btn-link:hover,.navbar-default .btn-link:focus{color:#333}.navbar-default .btn-link[disabled]:hover,fieldset[disabled] .navbar-default .btn-link:hover,.navbar-default .btn-link[disabled]:focus,fieldset[disabled] .navbar-default .btn-link:focus{color:#ccc}.navbar-inverse{background-color:#222;border-color:#080808}.navbar-inverse .navbar-brand{color:#9d9d9d}.navbar-inverse .navbar-brand:hover,.navbar-inverse .navbar-brand:focus{color:#fff;background-color:transparent}.navbar-inverse .navbar-text{color:#9d9d9d}.navbar-inverse .navbar-nav>li>a{color:#9d9d9d}.navbar-inverse .navbar-nav>li>a:hover,.navbar-inverse .navbar-nav>li>a:focus{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.active>a:hover,.navbar-inverse .navbar-nav>.active>a:focus{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav>.disabled>a,.navbar-inverse .navbar-nav>.disabled>a:hover,.navbar-inverse .navbar-nav>.disabled>a:focus{color:#444;background-color:transparent}.navbar-inverse .navbar-toggle{border-color:#333}.navbar-inverse .navbar-toggle:hover,.navbar-inverse .navbar-toggle:focus{background-color:#333}.navbar-inverse .navbar-toggle .icon-bar{background-color:#fff}.navbar-inverse .navbar-collapse,.navbar-inverse .navbar-form{border-color:#101010}.navbar-inverse .navbar-nav>.open>a,.navbar-inverse .navbar-nav>.open>a:hover,.navbar-inverse .navbar-nav>.open>a:focus{color:#fff;background-color:#080808}@media (max-width:767px){.navbar-inverse .navbar-nav .open .dropdown-menu>.dropdown-header{border-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu .divider{background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a{color:#9d9d9d}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:focus{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:focus{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:focus{color:#444;background-color:transparent}}.navbar-inverse .navbar-link{color:#9d9d9d}.navbar-inverse .navbar-link:hover{color:#fff}.navbar-inverse .btn-link{color:#9d9d9d}.navbar-inverse .btn-link:hover,.navbar-inverse .btn-link:focus{color:#fff}.navbar-inverse .btn-link[disabled]:hover,fieldset[disabled] .navbar-inverse .btn-link:hover,.navbar-inverse .btn-link[disabled]:focus,fieldset[disabled] .navbar-inverse .btn-link:focus{color:#444}.breadcrumb{padding:8px 15px;margin-bottom:20px;list-style:none;background-color:#f5f5f5;border-radius:4px}.breadcrumb>li{display:inline-block}.breadcrumb>li+li:before{padding:0 5px;color:#ccc;content:"/\00a0"}.breadcrumb>.active{color:#777}.pagination{display:inline-block;padding-left:0;margin:20px 0;border-radius:4px}.pagination>li{display:inline}.pagination>li>a,.pagination>li>span{position:relative;float:left;padding:6px 12px;margin-left:-1px;line-height:1.42857143;color:#337ab7;text-decoration:none;background-color:#fff;border:1px solid #ddd}.pagination>li:first-child>a,.pagination>li:first-child>span{margin-left:0;border-top-left-radius:4px;border-bottom-left-radius:4px}.pagination>li:last-child>a,.pagination>li:last-child>span{border-top-right-radius:4px;border-bottom-right-radius:4px}.pagination>li>a:hover,.pagination>li>span:hover,.pagination>li>a:focus,.pagination>li>span:focus{color:#23527c;background-color:#eee;border-color:#ddd}.pagination>.active>a,.pagination>.active>span,.pagination>.active>a:hover,.pagination>.active>span:hover,.pagination>.active>a:focus,.pagination>.active>span:focus{z-index:2;color:#fff;cursor:default;background-color:#337ab7;border-color:#337ab7}.pagination>.disabled>span,.pagination>.disabled>span:hover,.pagination>.disabled>span:focus,.pagination>.disabled>a,.pagination>.disabled>a:hover,.pagination>.disabled>a:focus{color:#777;cursor:not-allowed;background-color:#fff;border-color:#ddd}.pagination-lg>li>a,.pagination-lg>li>span{padding:10px 16px;font-size:18px}.pagination-lg>li:first-child>a,.pagination-lg>li:first-child>span{border-top-left-radius:6px;border-bottom-left-radius:6px}.pagination-lg>li:last-child>a,.pagination-lg>li:last-child>span{border-top-right-radius:6px;border-bottom-right-radius:6px}.pagination-sm>li>a,.pagination-sm>li>span{padding:5px 10px;font-size:12px}.pagination-sm>li:first-child>a,.pagination-sm>li:first-child>span{border-top-left-radius:3px;border-bottom-left-radius:3px}.pagination-sm>li:last-child>a,.pagination-sm>li:last-child>span{border-top-right-radius:3px;border-bottom-right-radius:3px}.pager{padding-left:0;margin:20px 0;text-align:center;list-style:none}.pager li{display:inline}.pager li>a,.pager li>span{display:inline-block;padding:5px 14px;background-color:#fff;border:1px solid #ddd;border-radius:15px}.pager li>a:hover,.pager li>a:focus{text-decoration:none;background-color:#eee}.pager .next>a,.pager .next>span{float:right}.pager .previous>a,.pager .previous>span{float:left}.pager .disabled>a,.pager .disabled>a:hover,.pager .disabled>a:focus,.pager .disabled>span{color:#777;cursor:not-allowed;background-color:#fff}.label{display:inline;padding:.2em .6em .3em;font-size:75%;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25em}a.label:hover,a.label:focus{color:#fff;text-decoration:none;cursor:pointer}.label:empty{display:none}.btn .label{position:relative;top:-1px}.label-default{background-color:#777}.label-default[href]:hover,.label-default[href]:focus{background-color:#5e5e5e}.label-primary{background-color:#337ab7}.label-primary[href]:hover,.label-primary[href]:focus{background-color:#286090}.label-success{background-color:#5cb85c}.label-success[href]:hover,.label-success[href]:focus{background-color:#449d44}.label-info{background-color:#5bc0de}.label-info[href]:hover,.label-info[href]:focus{background-color:#31b0d5}.label-warning{background-color:#f0ad4e}.label-warning[href]:hover,.label-warning[href]:focus{background-color:#ec971f}.label-danger{background-color:#d9534f}.label-danger[href]:hover,.label-danger[href]:focus{background-color:#c9302c}.badge{display:inline-block;min-width:10px;padding:3px 7px;font-size:12px;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;background-color:#777;border-radius:10px}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.btn-xs .badge{top:0;padding:1px 5px}a.badge:hover,a.badge:focus{color:#fff;text-decoration:none;cursor:pointer}.list-group-item.active>.badge,.nav-pills>.active>a>.badge{color:#337ab7;background-color:#fff}.list-group-item>.badge{float:right}.list-group-item>.badge+.badge{margin-right:5px}.nav-pills>li>a>.badge{margin-left:3px}.jumbotron{padding:30px 15px;margin-bottom:30px;color:inherit;background-color:#eee}.jumbotron h1,.jumbotron .h1{color:inherit}.jumbotron p{margin-bottom:15px;font-size:21px;font-weight:200}.jumbotron>hr{border-top-color:#d5d5d5}.container .jumbotron,.container-fluid .jumbotron{border-radius:6px}.jumbotron .container{max-width:100%}@media screen and (min-width:768px){.jumbotron{padding:48px 0}.container .jumbotron,.container-fluid .jumbotron{padding-right:60px;padding-left:60px}.jumbotron h1,.jumbotron .h1{font-size:63px}}.thumbnail{display:block;padding:4px;margin-bottom:20px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:border .2s ease-in-out;-o-transition:border .2s ease-in-out;transition:border .2s ease-in-out}.thumbnail>img,.thumbnail a>img{margin-right:auto;margin-left:auto}a.thumbnail:hover,a.thumbnail:focus,a.thumbnail.active{border-color:#337ab7}.thumbnail .caption{padding:9px;color:#333}.alert{padding:15px;margin-bottom:20px;border:1px solid transparent;border-radius:4px}.alert h4{margin-top:0;color:inherit}.alert .alert-link{font-weight:700}.alert>p,.alert>ul{margin-bottom:0}.alert>p+p{margin-top:5px}.alert-dismissable,.alert-dismissible{padding-right:35px}.alert-dismissable .close,.alert-dismissible .close{position:relative;top:-2px;right:-21px;color:inherit}.alert-success{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.alert-success hr{border-top-color:#c9e2b3}.alert-success .alert-link{color:#2b542c}.alert-info{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.alert-info hr{border-top-color:#a6e1ec}.alert-info .alert-link{color:#245269}.alert-warning{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.alert-warning hr{border-top-color:#f7e1b5}.alert-warning .alert-link{color:#66512c}.alert-danger{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.alert-danger hr{border-top-color:#e4b9c0}.alert-danger .alert-link{color:#843534}@-webkit-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@-o-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}.progress{height:20px;margin-bottom:20px;overflow:hidden;background-color:#f5f5f5;border-radius:4px;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,.1);box-shadow:inset 0 1px 2px rgba(0,0,0,.1)}.progress-bar{float:left;width:0;height:100%;font-size:12px;line-height:20px;color:#fff;text-align:center;background-color:#337ab7;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);-webkit-transition:width .6s ease;-o-transition:width .6s ease;transition:width .6s ease}.progress-striped .progress-bar,.progress-bar-striped{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);-webkit-background-size:40px 40px;background-size:40px 40px}.progress.active .progress-bar,.progress-bar.active{-webkit-animation:progress-bar-stripes 2s linear infinite;-o-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite}.progress-bar-success{background-color:#5cb85c}.progress-striped .progress-bar-success{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-info{background-color:#5bc0de}.progress-striped .progress-bar-info{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-warning{background-color:#f0ad4e}.progress-striped .progress-bar-warning{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-danger{background-color:#d9534f}.progress-striped .progress-bar-danger{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.media{margin-top:15px}.media:first-child{margin-top:0}.media-right,.media>.pull-right{padding-left:10px}.media-left,.media>.pull-left{padding-right:10px}.media-left,.media-right,.media-body{display:table-cell;vertical-align:top}.media-middle{vertical-align:middle}.media-bottom{vertical-align:bottom}.media-heading{margin-top:0;margin-bottom:5px}.media-list{padding-left:0;list-style:none}.list-group{padding-left:0;margin-bottom:20px}.list-group-item{position:relative;display:block;padding:10px 15px;margin-bottom:-1px;background-color:#fff;border:1px solid #ddd}.list-group-item:first-child{border-top-left-radius:4px;border-top-right-radius:4px}.list-group-item:last-child{margin-bottom:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px}a.list-group-item{color:#555}a.list-group-item .list-group-item-heading{color:#333}a.list-group-item:hover,a.list-group-item:focus{color:#555;text-decoration:none;background-color:#f5f5f5}.list-group-item.disabled,.list-group-item.disabled:hover,.list-group-item.disabled:focus{color:#777;cursor:not-allowed;background-color:#eee}.list-group-item.disabled .list-group-item-heading,.list-group-item.disabled:hover .list-group-item-heading,.list-group-item.disabled:focus .list-group-item-heading{color:inherit}.list-group-item.disabled .list-group-item-text,.list-group-item.disabled:hover .list-group-item-text,.list-group-item.disabled:focus .list-group-item-text{color:#777}.list-group-item.active,.list-group-item.active:hover,.list-group-item.active:focus{z-index:2;color:#fff;background-color:#337ab7;border-color:#337ab7}.list-group-item.active .list-group-item-heading,.list-group-item.active:hover .list-group-item-heading,.list-group-item.active:focus .list-group-item-heading,.list-group-item.active .list-group-item-heading>small,.list-group-item.active:hover .list-group-item-heading>small,.list-group-item.active:focus .list-group-item-heading>small,.list-group-item.active .list-group-item-heading>.small,.list-group-item.active:hover .list-group-item-heading>.small,.list-group-item.active:focus .list-group-item-heading>.small{color:inherit}.list-group-item.active .list-group-item-text,.list-group-item.active:hover .list-group-item-text,.list-group-item.active:focus .list-group-item-text{color:#c7ddef}.list-group-item-success{color:#3c763d;background-color:#dff0d8}a.list-group-item-success{color:#3c763d}a.list-group-item-success .list-group-item-heading{color:inherit}a.list-group-item-success:hover,a.list-group-item-success:focus{color:#3c763d;background-color:#d0e9c6}a.list-group-item-success.active,a.list-group-item-success.active:hover,a.list-group-item-success.active:focus{color:#fff;background-color:#3c763d;border-color:#3c763d}.list-group-item-info{color:#31708f;background-color:#d9edf7}a.list-group-item-info{color:#31708f}a.list-group-item-info .list-group-item-heading{color:inherit}a.list-group-item-info:hover,a.list-group-item-info:focus{color:#31708f;background-color:#c4e3f3}a.list-group-item-info.active,a.list-group-item-info.active:hover,a.list-group-item-info.active:focus{color:#fff;background-color:#31708f;border-color:#31708f}.list-group-item-warning{color:#8a6d3b;background-color:#fcf8e3}a.list-group-item-warning{color:#8a6d3b}a.list-group-item-warning .list-group-item-heading{color:inherit}a.list-group-item-warning:hover,a.list-group-item-warning:focus{color:#8a6d3b;background-color:#faf2cc}a.list-group-item-warning.active,a.list-group-item-warning.active:hover,a.list-group-item-warning.active:focus{color:#fff;background-color:#8a6d3b;border-color:#8a6d3b}.list-group-item-danger{color:#a94442;background-color:#f2dede}a.list-group-item-danger{color:#a94442}a.list-group-item-danger .list-group-item-heading{color:inherit}a.list-group-item-danger:hover,a.list-group-item-danger:focus{color:#a94442;background-color:#ebcccc}a.list-group-item-danger.active,a.list-group-item-danger.active:hover,a.list-group-item-danger.active:focus{color:#fff;background-color:#a94442;border-color:#a94442}.list-group-item-heading{margin-top:0;margin-bottom:5px}.list-group-item-text{margin-bottom:0;line-height:1.3}.panel{margin-bottom:20px;background-color:#fff;border:1px solid transparent;border-radius:4px;-webkit-box-shadow:0 1px 1px rgba(0,0,0,.05);box-shadow:0 1px 1px rgba(0,0,0,.05)}.panel-body{padding:15px}.panel-heading{padding:10px 15px;border-bottom:1px solid transparent;border-top-left-radius:3px;border-top-right-radius:3px}.panel-heading>.dropdown .dropdown-toggle{color:inherit}.panel-title{margin-top:0;margin-bottom:0;font-size:16px;color:inherit}.panel-title>a{color:inherit}.panel-footer{padding:10px 15px;background-color:#f5f5f5;border-top:1px solid #ddd;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.list-group,.panel>.panel-collapse>.list-group{margin-bottom:0}.panel>.list-group .list-group-item,.panel>.panel-collapse>.list-group .list-group-item{border-width:1px 0;border-radius:0}.panel>.list-group:first-child .list-group-item:first-child,.panel>.panel-collapse>.list-group:first-child .list-group-item:first-child{border-top:0;border-top-left-radius:3px;border-top-right-radius:3px}.panel>.list-group:last-child .list-group-item:last-child,.panel>.panel-collapse>.list-group:last-child .list-group-item:last-child{border-bottom:0;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel-heading+.list-group .list-group-item:first-child{border-top-width:0}.list-group+.panel-footer{border-top-width:0}.panel>.table,.panel>.table-responsive>.table,.panel>.panel-collapse>.table{margin-bottom:0}.panel>.table caption,.panel>.table-responsive>.table caption,.panel>.panel-collapse>.table caption{padding-right:15px;padding-left:15px}.panel>.table:first-child,.panel>.table-responsive:first-child>.table:first-child{border-top-left-radius:3px;border-top-right-radius:3px}.panel>.table:first-child>thead:first-child>tr:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child{border-top-left-radius:3px;border-top-right-radius:3px}.panel>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:first-child{border-top-left-radius:3px}.panel>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:last-child{border-top-right-radius:3px}.panel>.table:last-child,.panel>.table-responsive:last-child>.table:last-child{border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.table:last-child>tbody:last-child>tr:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child{border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:first-child{border-bottom-left-radius:3px}.panel>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:last-child{border-bottom-right-radius:3px}.panel>.panel-body+.table,.panel>.panel-body+.table-responsive,.panel>.table+.panel-body,.panel>.table-responsive+.panel-body{border-top:1px solid #ddd}.panel>.table>tbody:first-child>tr:first-child th,.panel>.table>tbody:first-child>tr:first-child td{border-top:0}.panel>.table-bordered,.panel>.table-responsive>.table-bordered{border:0}.panel>.table-bordered>thead>tr>th:first-child,.panel>.table-responsive>.table-bordered>thead>tr>th:first-child,.panel>.table-bordered>tbody>tr>th:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:first-child,.panel>.table-bordered>tfoot>tr>th:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:first-child,.panel>.table-bordered>thead>tr>td:first-child,.panel>.table-responsive>.table-bordered>thead>tr>td:first-child,.panel>.table-bordered>tbody>tr>td:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:first-child,.panel>.table-bordered>tfoot>tr>td:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:first-child{border-left:0}.panel>.table-bordered>thead>tr>th:last-child,.panel>.table-responsive>.table-bordered>thead>tr>th:last-child,.panel>.table-bordered>tbody>tr>th:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:last-child,.panel>.table-bordered>tfoot>tr>th:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:last-child,.panel>.table-bordered>thead>tr>td:last-child,.panel>.table-responsive>.table-bordered>thead>tr>td:last-child,.panel>.table-bordered>tbody>tr>td:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:last-child,.panel>.table-bordered>tfoot>tr>td:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:last-child{border-right:0}.panel>.table-bordered>thead>tr:first-child>td,.panel>.table-responsive>.table-bordered>thead>tr:first-child>td,.panel>.table-bordered>tbody>tr:first-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>td,.panel>.table-bordered>thead>tr:first-child>th,.panel>.table-responsive>.table-bordered>thead>tr:first-child>th,.panel>.table-bordered>tbody>tr:first-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>th{border-bottom:0}.panel>.table-bordered>tbody>tr:last-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>td,.panel>.table-bordered>tfoot>tr:last-child>td,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>td,.panel>.table-bordered>tbody>tr:last-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>th,.panel>.table-bordered>tfoot>tr:last-child>th,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>th{border-bottom:0}.panel>.table-responsive{margin-bottom:0;border:0}.panel-group{margin-bottom:20px}.panel-group .panel{margin-bottom:0;border-radius:4px}.panel-group .panel+.panel{margin-top:5px}.panel-group .panel-heading{border-bottom:0}.panel-group .panel-heading+.panel-collapse>.panel-body,.panel-group .panel-heading+.panel-collapse>.list-group{border-top:1px solid #ddd}.panel-group .panel-footer{border-top:0}.panel-group .panel-footer+.panel-collapse .panel-body{border-bottom:1px solid #ddd}.panel-default{border-color:#ddd}.panel-default>.panel-heading{color:#333;background-color:#f5f5f5;border-color:#ddd}.panel-default>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ddd}.panel-default>.panel-heading .badge{color:#f5f5f5;background-color:#333}.panel-default>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ddd}.panel-primary{border-color:#337ab7}.panel-primary>.panel-heading{color:#fff;background-color:#337ab7;border-color:#337ab7}.panel-primary>.panel-heading+.panel-collapse>.panel-body{border-top-color:#337ab7}.panel-primary>.panel-heading .badge{color:#337ab7;background-color:#fff}.panel-primary>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#337ab7}.panel-success{border-color:#d6e9c6}.panel-success>.panel-heading{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.panel-success>.panel-heading+.panel-collapse>.panel-body{border-top-color:#d6e9c6}.panel-success>.panel-heading .badge{color:#dff0d8;background-color:#3c763d}.panel-success>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#d6e9c6}.panel-info{border-color:#bce8f1}.panel-info>.panel-heading{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.panel-info>.panel-heading+.panel-collapse>.panel-body{border-top-color:#bce8f1}.panel-info>.panel-heading .badge{color:#d9edf7;background-color:#31708f}.panel-info>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#bce8f1}.panel-warning{border-color:#faebcc}.panel-warning>.panel-heading{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.panel-warning>.panel-heading+.panel-collapse>.panel-body{border-top-color:#faebcc}.panel-warning>.panel-heading .badge{color:#fcf8e3;background-color:#8a6d3b}.panel-warning>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#faebcc}.panel-danger{border-color:#ebccd1}.panel-danger>.panel-heading{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.panel-danger>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ebccd1}.panel-danger>.panel-heading .badge{color:#f2dede;background-color:#a94442}.panel-danger>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ebccd1}.embed-responsive{position:relative;display:block;height:0;padding:0;overflow:hidden}.embed-responsive .embed-responsive-item,.embed-responsive iframe,.embed-responsive embed,.embed-responsive object,.embed-responsive video{position:absolute;top:0;bottom:0;left:0;width:100%;height:100%;border:0}.embed-responsive.embed-responsive-16by9{padding-bottom:56.25%}.embed-responsive.embed-responsive-4by3{padding-bottom:75%}.well{min-height:20px;padding:19px;margin-bottom:20px;background-color:#f5f5f5;border:1px solid #e3e3e3;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.05);box-shadow:inset 0 1px 1px rgba(0,0,0,.05)}.well blockquote{border-color:#ddd;border-color:rgba(0,0,0,.15)}.well-lg{padding:24px;border-radius:6px}.well-sm{padding:9px;border-radius:3px}.close{float:right;font-size:21px;font-weight:700;line-height:1;color:#000;text-shadow:0 1px 0 #fff;filter:alpha(opacity=20);opacity:.2}.close:hover,.close:focus{color:#000;text-decoration:none;cursor:pointer;filter:alpha(opacity=50);opacity:.5}button.close{-webkit-appearance:none;padding:0;cursor:pointer;background:0 0;border:0}.modal-open{overflow:hidden}.modal{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;display:none;overflow:hidden;-webkit-overflow-scrolling:touch;outline:0}.modal.fade .modal-dialog{-webkit-transition:-webkit-transform .3s ease-out;-o-transition:-o-transform .3s ease-out;transition:transform .3s ease-out;-webkit-transform:translate(0,-25%);-ms-transform:translate(0,-25%);-o-transform:translate(0,-25%);transform:translate(0,-25%)}.modal.in .modal-dialog{-webkit-transform:translate(0,0);-ms-transform:translate(0,0);-o-transform:translate(0,0);transform:translate(0,0)}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal-dialog{position:relative;width:auto;margin:10px}.modal-content{position:relative;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid #999;border:1px solid rgba(0,0,0,.2);border-radius:6px;outline:0;-webkit-box-shadow:0 3px 9px rgba(0,0,0,.5);box-shadow:0 3px 9px rgba(0,0,0,.5)}.modal-backdrop{position:absolute;top:0;right:0;left:0;background-color:#000}.modal-backdrop.fade{filter:alpha(opacity=0);opacity:0}.modal-backdrop.in{filter:alpha(opacity=50);opacity:.5}.modal-header{min-height:16.43px;padding:15px;border-bottom:1px solid #e5e5e5}.modal-header .close{margin-top:-2px}.modal-title{margin:0;line-height:1.42857143}.modal-body{position:relative;padding:15px}.modal-footer{padding:15px;text-align:right;border-top:1px solid #e5e5e5}.modal-footer .btn+.btn{margin-bottom:0;margin-left:5px}.modal-footer .btn-group .btn+.btn{margin-left:-1px}.modal-footer .btn-block+.btn-block{margin-left:0}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width:768px){.modal-dialog{width:600px;margin:30px auto}.modal-content{-webkit-box-shadow:0 5px 15px rgba(0,0,0,.5);box-shadow:0 5px 15px rgba(0,0,0,.5)}.modal-sm{width:300px}}@media (min-width:992px){.modal-lg{width:900px}}.tooltip{position:absolute;z-index:1070;display:block;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:12px;font-weight:400;line-height:1.4;visibility:visible;filter:alpha(opacity=0);opacity:0}.tooltip.in{filter:alpha(opacity=90);opacity:.9}.tooltip.top{padding:5px 0;margin-top:-3px}.tooltip.right{padding:0 5px;margin-left:3px}.tooltip.bottom{padding:5px 0;margin-top:3px}.tooltip.left{padding:0 5px;margin-left:-3px}.tooltip-inner{max-width:200px;padding:3px 8px;color:#fff;text-align:center;text-decoration:none;background-color:#000;border-radius:4px}.tooltip-arrow{position:absolute;width:0;height:0;border-color:transparent;border-style:solid}.tooltip.top .tooltip-arrow{bottom:0;left:50%;margin-left:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-left .tooltip-arrow{right:5px;bottom:0;margin-bottom:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-right .tooltip-arrow{bottom:0;left:5px;margin-bottom:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.right .tooltip-arrow{top:50%;left:0;margin-top:-5px;border-width:5px 5px 5px 0;border-right-color:#000}.tooltip.left .tooltip-arrow{top:50%;right:0;margin-top:-5px;border-width:5px 0 5px 5px;border-left-color:#000}.tooltip.bottom .tooltip-arrow{top:0;left:50%;margin-left:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-left .tooltip-arrow{top:0;right:5px;margin-top:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-right .tooltip-arrow{top:0;left:5px;margin-top:-5px;border-width:0 5px 5px;border-bottom-color:#000}.popover{position:absolute;top:0;left:0;z-index:1060;display:none;max-width:276px;padding:1px;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;font-weight:400;line-height:1.42857143;text-align:left;white-space:normal;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid #ccc;border:1px solid rgba(0,0,0,.2);border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,.2);box-shadow:0 5px 10px rgba(0,0,0,.2)}.popover.top{margin-top:-10px}.popover.right{margin-left:10px}.popover.bottom{margin-top:10px}.popover.left{margin-left:-10px}.popover-title{padding:8px 14px;margin:0;font-size:14px;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-radius:5px 5px 0 0}.popover-content{padding:9px 14px}.popover>.arrow,.popover>.arrow:after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.popover>.arrow{border-width:11px}.popover>.arrow:after{content:"";border-width:10px}.popover.top>.arrow{bottom:-11px;left:50%;margin-left:-11px;border-top-color:#999;border-top-color:rgba(0,0,0,.25);border-bottom-width:0}.popover.top>.arrow:after{bottom:1px;margin-left:-10px;content:" ";border-top-color:#fff;border-bottom-width:0}.popover.right>.arrow{top:50%;left:-11px;margin-top:-11px;border-right-color:#999;border-right-color:rgba(0,0,0,.25);border-left-width:0}.popover.right>.arrow:after{bottom:-10px;left:1px;content:" ";border-right-color:#fff;border-left-width:0}.popover.bottom>.arrow{top:-11px;left:50%;margin-left:-11px;border-top-width:0;border-bottom-color:#999;border-bottom-color:rgba(0,0,0,.25)}.popover.bottom>.arrow:after{top:1px;margin-left:-10px;content:" ";border-top-width:0;border-bottom-color:#fff}.popover.left>.arrow{top:50%;right:-11px;margin-top:-11px;border-right-width:0;border-left-color:#999;border-left-color:rgba(0,0,0,.25)}.popover.left>.arrow:after{right:1px;bottom:-10px;content:" ";border-right-width:0;border-left-color:#fff}.carousel{position:relative}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner>.item{position:relative;display:none;-webkit-transition:.6s ease-in-out left;-o-transition:.6s ease-in-out left;transition:.6s ease-in-out left}.carousel-inner>.item>img,.carousel-inner>.item>a>img{line-height:1}@media all and (transform-3d),(-webkit-transform-3d){.carousel-inner>.item{-webkit-transition:-webkit-transform .6s ease-in-out;-o-transition:-o-transform .6s ease-in-out;transition:transform .6s ease-in-out;-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-perspective:1000;perspective:1000}.carousel-inner>.item.next,.carousel-inner>.item.active.right{left:0;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}.carousel-inner>.item.prev,.carousel-inner>.item.active.left{left:0;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}.carousel-inner>.item.next.left,.carousel-inner>.item.prev.right,.carousel-inner>.item.active{left:0;-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}}.carousel-inner>.active,.carousel-inner>.next,.carousel-inner>.prev{display:block}.carousel-inner>.active{left:0}.carousel-inner>.next,.carousel-inner>.prev{position:absolute;top:0;width:100%}.carousel-inner>.next{left:100%}.carousel-inner>.prev{left:-100%}.carousel-inner>.next.left,.carousel-inner>.prev.right{left:0}.carousel-inner>.active.left{left:-100%}.carousel-inner>.active.right{left:100%}.carousel-control{position:absolute;top:0;bottom:0;left:0;width:15%;font-size:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,.6);filter:alpha(opacity=50);opacity:.5}.carousel-control.left{background-image:-webkit-linear-gradient(left,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);background-image:-o-linear-gradient(left,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);background-image:-webkit-gradient(linear,left top,right top,from(rgba(0,0,0,.5)),to(rgba(0,0,0,.0001)));background-image:linear-gradient(to right,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1);background-repeat:repeat-x}.carousel-control.right{right:0;left:auto;background-image:-webkit-linear-gradient(left,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);background-image:-o-linear-gradient(left,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);background-image:-webkit-gradient(linear,left top,right top,from(rgba(0,0,0,.0001)),to(rgba(0,0,0,.5)));background-image:linear-gradient(to right,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1);background-repeat:repeat-x}.carousel-control:hover,.carousel-control:focus{color:#fff;text-decoration:none;filter:alpha(opacity=90);outline:0;opacity:.9}.carousel-control .icon-prev,.carousel-control .icon-next,.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right{position:absolute;top:50%;z-index:5;display:inline-block}.carousel-control .icon-prev,.carousel-control .glyphicon-chevron-left{left:50%;margin-left:-10px}.carousel-control .icon-next,.carousel-control .glyphicon-chevron-right{right:50%;margin-right:-10px}.carousel-control .icon-prev,.carousel-control .icon-next{width:20px;height:20px;margin-top:-10px;font-family:serif}.carousel-control .icon-prev:before{content:'\2039'}.carousel-control .icon-next:before{content:'\203a'}.carousel-indicators{position:absolute;bottom:10px;left:50%;z-index:15;width:60%;padding-left:0;margin-left:-30%;text-align:center;list-style:none}.carousel-indicators li{display:inline-block;width:10px;height:10px;margin:1px;text-indent:-999px;cursor:pointer;background-color:#000 \9;background-color:rgba(0,0,0,0);border:1px solid #fff;border-radius:10px}.carousel-indicators .active{width:12px;height:12px;margin:0;background-color:#fff}.carousel-caption{position:absolute;right:15%;bottom:20px;left:15%;z-index:10;padding-top:20px;padding-bottom:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,.6)}.carousel-caption .btn{text-shadow:none}@media screen and (min-width:768px){.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right,.carousel-control .icon-prev,.carousel-control .icon-next{width:30px;height:30px;margin-top:-15px;font-size:30px}.carousel-control .glyphicon-chevron-left,.carousel-control .icon-prev{margin-left:-15px}.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next{margin-right:-15px}.carousel-caption{right:20%;left:20%;padding-bottom:30px}.carousel-indicators{bottom:20px}}.clearfix:before,.clearfix:after,.dl-horizontal dd:before,.dl-horizontal dd:after,.container:before,.container:after,.container-fluid:before,.container-fluid:after,.row:before,.row:after,.form-horizontal .form-group:before,.form-horizontal .form-group:after,.btn-toolbar:before,.btn-toolbar:after,.btn-group-vertical>.btn-group:before,.btn-group-vertical>.btn-group:after,.nav:before,.nav:after,.navbar:before,.navbar:after,.navbar-header:before,.navbar-header:after,.navbar-collapse:before,.navbar-collapse:after,.pager:before,.pager:after,.panel-body:before,.panel-body:after,.modal-footer:before,.modal-footer:after{display:table;content:" "}.clearfix:after,.dl-horizontal dd:after,.container:after,.container-fluid:after,.row:after,.form-horizontal .form-group:after,.btn-toolbar:after,.btn-group-vertical>.btn-group:after,.nav:after,.navbar:after,.navbar-header:after,.navbar-collapse:after,.pager:after,.panel-body:after,.modal-footer:after{clear:both}.center-block{display:block;margin-right:auto;margin-left:auto}.pull-right{float:right!important}.pull-left{float:left!important}.hide{display:none!important}.show{display:block!important}.invisible{visibility:hidden}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.hidden{display:none!important;visibility:hidden!important}.affix{position:fixed}@-ms-viewport{width:device-width}.visible-xs,.visible-sm,.visible-md,.visible-lg{display:none!important}.visible-xs-block,.visible-xs-inline,.visible-xs-inline-block,.visible-sm-block,.visible-sm-inline,.visible-sm-inline-block,.visible-md-block,.visible-md-inline,.visible-md-inline-block,.visible-lg-block,.visible-lg-inline,.visible-lg-inline-block{display:none!important}@media (max-width:767px){.visible-xs{display:block!important}table.visible-xs{display:table}tr.visible-xs{display:table-row!important}th.visible-xs,td.visible-xs{display:table-cell!important}}@media (max-width:767px){.visible-xs-block{display:block!important}}@media (max-width:767px){.visible-xs-inline{display:inline!important}}@media (max-width:767px){.visible-xs-inline-block{display:inline-block!important}}@media (min-width:768px) and (max-width:991px){.visible-sm{display:block!important}table.visible-sm{display:table}tr.visible-sm{display:table-row!important}th.visible-sm,td.visible-sm{display:table-cell!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-block{display:block!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-inline{display:inline!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-inline-block{display:inline-block!important}}@media (min-width:992px) and (max-width:1199px){.visible-md{display:block!important}table.visible-md{display:table}tr.visible-md{display:table-row!important}th.visible-md,td.visible-md{display:table-cell!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-block{display:block!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-inline{display:inline!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-inline-block{display:inline-block!important}}@media (min-width:1200px){.visible-lg{display:block!important}table.visible-lg{display:table}tr.visible-lg{display:table-row!important}th.visible-lg,td.visible-lg{display:table-cell!important}}@media (min-width:1200px){.visible-lg-block{display:block!important}}@media (min-width:1200px){.visible-lg-inline{display:inline!important}}@media (min-width:1200px){.visible-lg-inline-block{display:inline-block!important}}@media (max-width:767px){.hidden-xs{display:none!important}}@media (min-width:768px) and (max-width:991px){.hidden-sm{display:none!important}}@media (min-width:992px) and (max-width:1199px){.hidden-md{display:none!important}}@media (min-width:1200px){.hidden-lg{display:none!important}}.visible-print{display:none!important}@media print{.visible-print{display:block!important}table.visible-print{display:table}tr.visible-print{display:table-row!important}th.visible-print,td.visible-print{display:table-cell!important}}.visible-print-block{display:none!important}@media print{.visible-print-block{display:block!important}}.visible-print-inline{display:none!important}@media print{.visible-print-inline{display:inline!important}}.visible-print-inline-block{display:none!important}@media print{.visible-print-inline-block{display:inline-block!important}}@media print{.hidden-print{display:none!important}}
\ No newline at end of file
+ *//*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */html{font-family:sans-serif;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:700}dfn{font-style:italic}h1{margin:.67em 0;font-size:2em}mark{color:#000;background:#ff0}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{height:0;-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}button,input,optgroup,select,textarea{margin:0;font:inherit;color:inherit}button{overflow:visible}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{padding:0;border:0}input{line-height:normal}input[type=checkbox],input[type=radio]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;padding:0}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{height:auto}input[type=search]{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;-webkit-appearance:textfield}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}fieldset{padding:.35em .625em .75em;margin:0 2px;border:1px solid silver}legend{padding:0;border:0}textarea{overflow:auto}optgroup{font-weight:700}table{border-spacing:0;border-collapse:collapse}td,th{padding:0}/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */@media print{*,:after,:before{color:#000!important;text-shadow:none!important;background:0 0!important;-webkit-box-shadow:none!important;box-shadow:none!important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href) ")"}abbr[title]:after{content:" (" attr(title) ")"}a[href^="javascript:"]:after,a[href^="#"]:after{content:""}blockquote,pre{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}img,tr{page-break-inside:avoid}img{max-width:100%!important}h2,h3,p{orphans:3;widows:3}h2,h3{page-break-after:avoid}.navbar{display:none}.btn>.caret,.dropup>.btn>.caret{border-top-color:#000!important}.label{border:1px solid #000}.table{border-collapse:collapse!important}.table td,.table th{background-color:#fff!important}.table-bordered td,.table-bordered th{border:1px solid #ddd!important}}@font-face{font-family:'Glyphicons Halflings';src:url(../fonts/glyphicons-halflings-regular.eot);src:url(../fonts/glyphicons-halflings-regular.eot?#iefix) format('embedded-opentype'),url(../fonts/glyphicons-halflings-regular.woff2) format('woff2'),url(../fonts/glyphicons-halflings-regular.woff) format('woff'),url(../fonts/glyphicons-halflings-regular.ttf) format('truetype'),url(../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular) format('svg')}.glyphicon{position:relative;top:1px;display:inline-block;font-family:'Glyphicons Halflings';font-style:normal;font-weight:400;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.glyphicon-asterisk:before{content:"\002a"}.glyphicon-plus:before{content:"\002b"}.glyphicon-eur:before,.glyphicon-euro:before{content:"\20ac"}.glyphicon-minus:before{content:"\2212"}.glyphicon-cloud:before{content:"\2601"}.glyphicon-envelope:before{content:"\2709"}.glyphicon-pencil:before{content:"\270f"}.glyphicon-glass:before{content:"\e001"}.glyphicon-music:before{content:"\e002"}.glyphicon-search:before{content:"\e003"}.glyphicon-heart:before{content:"\e005"}.glyphicon-star:before{content:"\e006"}.glyphicon-star-empty:before{content:"\e007"}.glyphicon-user:before{content:"\e008"}.glyphicon-film:before{content:"\e009"}.glyphicon-th-large:before{content:"\e010"}.glyphicon-th:before{content:"\e011"}.glyphicon-th-list:before{content:"\e012"}.glyphicon-ok:before{content:"\e013"}.glyphicon-remove:before{content:"\e014"}.glyphicon-zoom-in:before{content:"\e015"}.glyphicon-zoom-out:before{content:"\e016"}.glyphicon-off:before{content:"\e017"}.glyphicon-signal:before{content:"\e018"}.glyphicon-cog:before{content:"\e019"}.glyphicon-trash:before{content:"\e020"}.glyphicon-home:before{content:"\e021"}.glyphicon-file:before{content:"\e022"}.glyphicon-time:before{content:"\e023"}.glyphicon-road:before{content:"\e024"}.glyphicon-download-alt:before{content:"\e025"}.glyphicon-download:before{content:"\e026"}.glyphicon-upload:before{content:"\e027"}.glyphicon-inbox:before{content:"\e028"}.glyphicon-play-circle:before{content:"\e029"}.glyphicon-repeat:before{content:"\e030"}.glyphicon-refresh:before{content:"\e031"}.glyphicon-list-alt:before{content:"\e032"}.glyphicon-lock:before{content:"\e033"}.glyphicon-flag:before{content:"\e034"}.glyphicon-headphones:before{content:"\e035"}.glyphicon-volume-off:before{content:"\e036"}.glyphicon-volume-down:before{content:"\e037"}.glyphicon-volume-up:before{content:"\e038"}.glyphicon-qrcode:before{content:"\e039"}.glyphicon-barcode:before{content:"\e040"}.glyphicon-tag:before{content:"\e041"}.glyphicon-tags:before{content:"\e042"}.glyphicon-book:before{content:"\e043"}.glyphicon-bookmark:before{content:"\e044"}.glyphicon-print:before{content:"\e045"}.glyphicon-camera:before{content:"\e046"}.glyphicon-font:before{content:"\e047"}.glyphicon-bold:before{content:"\e048"}.glyphicon-italic:before{content:"\e049"}.glyphicon-text-height:before{content:"\e050"}.glyphicon-text-width:before{content:"\e051"}.glyphicon-align-left:before{content:"\e052"}.glyphicon-align-center:before{content:"\e053"}.glyphicon-align-right:before{content:"\e054"}.glyphicon-align-justify:before{content:"\e055"}.glyphicon-list:before{content:"\e056"}.glyphicon-indent-left:before{content:"\e057"}.glyphicon-indent-right:before{content:"\e058"}.glyphicon-facetime-video:before{content:"\e059"}.glyphicon-picture:before{content:"\e060"}.glyphicon-map-marker:before{content:"\e062"}.glyphicon-adjust:before{content:"\e063"}.glyphicon-tint:before{content:"\e064"}.glyphicon-edit:before{content:"\e065"}.glyphicon-share:before{content:"\e066"}.glyphicon-check:before{content:"\e067"}.glyphicon-move:before{content:"\e068"}.glyphicon-step-backward:before{content:"\e069"}.glyphicon-fast-backward:before{content:"\e070"}.glyphicon-backward:before{content:"\e071"}.glyphicon-play:before{content:"\e072"}.glyphicon-pause:before{content:"\e073"}.glyphicon-stop:before{content:"\e074"}.glyphicon-forward:before{content:"\e075"}.glyphicon-fast-forward:before{content:"\e076"}.glyphicon-step-forward:before{content:"\e077"}.glyphicon-eject:before{content:"\e078"}.glyphicon-chevron-left:before{content:"\e079"}.glyphicon-chevron-right:before{content:"\e080"}.glyphicon-plus-sign:before{content:"\e081"}.glyphicon-minus-sign:before{content:"\e082"}.glyphicon-remove-sign:before{content:"\e083"}.glyphicon-ok-sign:before{content:"\e084"}.glyphicon-question-sign:before{content:"\e085"}.glyphicon-info-sign:before{content:"\e086"}.glyphicon-screenshot:before{content:"\e087"}.glyphicon-remove-circle:before{content:"\e088"}.glyphicon-ok-circle:before{content:"\e089"}.glyphicon-ban-circle:before{content:"\e090"}.glyphicon-arrow-left:before{content:"\e091"}.glyphicon-arrow-right:before{content:"\e092"}.glyphicon-arrow-up:before{content:"\e093"}.glyphicon-arrow-down:before{content:"\e094"}.glyphicon-share-alt:before{content:"\e095"}.glyphicon-resize-full:before{content:"\e096"}.glyphicon-resize-small:before{content:"\e097"}.glyphicon-exclamation-sign:before{content:"\e101"}.glyphicon-gift:before{content:"\e102"}.glyphicon-leaf:before{content:"\e103"}.glyphicon-fire:before{content:"\e104"}.glyphicon-eye-open:before{content:"\e105"}.glyphicon-eye-close:before{content:"\e106"}.glyphicon-warning-sign:before{content:"\e107"}.glyphicon-plane:before{content:"\e108"}.glyphicon-calendar:before{content:"\e109"}.glyphicon-random:before{content:"\e110"}.glyphicon-comment:before{content:"\e111"}.glyphicon-magnet:before{content:"\e112"}.glyphicon-chevron-up:before{content:"\e113"}.glyphicon-chevron-down:before{content:"\e114"}.glyphicon-retweet:before{content:"\e115"}.glyphicon-shopping-cart:before{content:"\e116"}.glyphicon-folder-close:before{content:"\e117"}.glyphicon-folder-open:before{content:"\e118"}.glyphicon-resize-vertical:before{content:"\e119"}.glyphicon-resize-horizontal:before{content:"\e120"}.glyphicon-hdd:before{content:"\e121"}.glyphicon-bullhorn:before{content:"\e122"}.glyphicon-bell:before{content:"\e123"}.glyphicon-certificate:before{content:"\e124"}.glyphicon-thumbs-up:before{content:"\e125"}.glyphicon-thumbs-down:before{content:"\e126"}.glyphicon-hand-right:before{content:"\e127"}.glyphicon-hand-left:before{content:"\e128"}.glyphicon-hand-up:before{content:"\e129"}.glyphicon-hand-down:before{content:"\e130"}.glyphicon-circle-arrow-right:before{content:"\e131"}.glyphicon-circle-arrow-left:before{content:"\e132"}.glyphicon-circle-arrow-up:before{content:"\e133"}.glyphicon-circle-arrow-down:before{content:"\e134"}.glyphicon-globe:before{content:"\e135"}.glyphicon-wrench:before{content:"\e136"}.glyphicon-tasks:before{content:"\e137"}.glyphicon-filter:before{content:"\e138"}.glyphicon-briefcase:before{content:"\e139"}.glyphicon-fullscreen:before{content:"\e140"}.glyphicon-dashboard:before{content:"\e141"}.glyphicon-paperclip:before{content:"\e142"}.glyphicon-heart-empty:before{content:"\e143"}.glyphicon-link:before{content:"\e144"}.glyphicon-phone:before{content:"\e145"}.glyphicon-pushpin:before{content:"\e146"}.glyphicon-usd:before{content:"\e148"}.glyphicon-gbp:before{content:"\e149"}.glyphicon-sort:before{content:"\e150"}.glyphicon-sort-by-alphabet:before{content:"\e151"}.glyphicon-sort-by-alphabet-alt:before{content:"\e152"}.glyphicon-sort-by-order:before{content:"\e153"}.glyphicon-sort-by-order-alt:before{content:"\e154"}.glyphicon-sort-by-attributes:before{content:"\e155"}.glyphicon-sort-by-attributes-alt:before{content:"\e156"}.glyphicon-unchecked:before{content:"\e157"}.glyphicon-expand:before{content:"\e158"}.glyphicon-collapse-down:before{content:"\e159"}.glyphicon-collapse-up:before{content:"\e160"}.glyphicon-log-in:before{content:"\e161"}.glyphicon-flash:before{content:"\e162"}.glyphicon-log-out:before{content:"\e163"}.glyphicon-new-window:before{content:"\e164"}.glyphicon-record:before{content:"\e165"}.glyphicon-save:before{content:"\e166"}.glyphicon-open:before{content:"\e167"}.glyphicon-saved:before{content:"\e168"}.glyphicon-import:before{content:"\e169"}.glyphicon-export:before{content:"\e170"}.glyphicon-send:before{content:"\e171"}.glyphicon-floppy-disk:before{content:"\e172"}.glyphicon-floppy-saved:before{content:"\e173"}.glyphicon-floppy-remove:before{content:"\e174"}.glyphicon-floppy-save:before{content:"\e175"}.glyphicon-floppy-open:before{content:"\e176"}.glyphicon-credit-card:before{content:"\e177"}.glyphicon-transfer:before{content:"\e178"}.glyphicon-cutlery:before{content:"\e179"}.glyphicon-header:before{content:"\e180"}.glyphicon-compressed:before{content:"\e181"}.glyphicon-earphone:before{content:"\e182"}.glyphicon-phone-alt:before{content:"\e183"}.glyphicon-tower:before{content:"\e184"}.glyphicon-stats:before{content:"\e185"}.glyphicon-sd-video:before{content:"\e186"}.glyphicon-hd-video:before{content:"\e187"}.glyphicon-subtitles:before{content:"\e188"}.glyphicon-sound-stereo:before{content:"\e189"}.glyphicon-sound-dolby:before{content:"\e190"}.glyphicon-sound-5-1:before{content:"\e191"}.glyphicon-sound-6-1:before{content:"\e192"}.glyphicon-sound-7-1:before{content:"\e193"}.glyphicon-copyright-mark:before{content:"\e194"}.glyphicon-registration-mark:before{content:"\e195"}.glyphicon-cloud-download:before{content:"\e197"}.glyphicon-cloud-upload:before{content:"\e198"}.glyphicon-tree-conifer:before{content:"\e199"}.glyphicon-tree-deciduous:before{content:"\e200"}.glyphicon-cd:before{content:"\e201"}.glyphicon-save-file:before{content:"\e202"}.glyphicon-open-file:before{content:"\e203"}.glyphicon-level-up:before{content:"\e204"}.glyphicon-copy:before{content:"\e205"}.glyphicon-paste:before{content:"\e206"}.glyphicon-alert:before{content:"\e209"}.glyphicon-equalizer:before{content:"\e210"}.glyphicon-king:before{content:"\e211"}.glyphicon-queen:before{content:"\e212"}.glyphicon-pawn:before{content:"\e213"}.glyphicon-bishop:before{content:"\e214"}.glyphicon-knight:before{content:"\e215"}.glyphicon-baby-formula:before{content:"\e216"}.glyphicon-tent:before{content:"\26fa"}.glyphicon-blackboard:before{content:"\e218"}.glyphicon-bed:before{content:"\e219"}.glyphicon-apple:before{content:"\f8ff"}.glyphicon-erase:before{content:"\e221"}.glyphicon-hourglass:before{content:"\231b"}.glyphicon-lamp:before{content:"\e223"}.glyphicon-duplicate:before{content:"\e224"}.glyphicon-piggy-bank:before{content:"\e225"}.glyphicon-scissors:before{content:"\e226"}.glyphicon-bitcoin:before{content:"\e227"}.glyphicon-btc:before{content:"\e227"}.glyphicon-xbt:before{content:"\e227"}.glyphicon-yen:before{content:"\00a5"}.glyphicon-jpy:before{content:"\00a5"}.glyphicon-ruble:before{content:"\20bd"}.glyphicon-rub:before{content:"\20bd"}.glyphicon-scale:before{content:"\e230"}.glyphicon-ice-lolly:before{content:"\e231"}.glyphicon-ice-lolly-tasted:before{content:"\e232"}.glyphicon-education:before{content:"\e233"}.glyphicon-option-horizontal:before{content:"\e234"}.glyphicon-option-vertical:before{content:"\e235"}.glyphicon-menu-hamburger:before{content:"\e236"}.glyphicon-modal-window:before{content:"\e237"}.glyphicon-oil:before{content:"\e238"}.glyphicon-grain:before{content:"\e239"}.glyphicon-sunglasses:before{content:"\e240"}.glyphicon-text-size:before{content:"\e241"}.glyphicon-text-color:before{content:"\e242"}.glyphicon-text-background:before{content:"\e243"}.glyphicon-object-align-top:before{content:"\e244"}.glyphicon-object-align-bottom:before{content:"\e245"}.glyphicon-object-align-horizontal:before{content:"\e246"}.glyphicon-object-align-left:before{content:"\e247"}.glyphicon-object-align-vertical:before{content:"\e248"}.glyphicon-object-align-right:before{content:"\e249"}.glyphicon-triangle-right:before{content:"\e250"}.glyphicon-triangle-left:before{content:"\e251"}.glyphicon-triangle-bottom:before{content:"\e252"}.glyphicon-triangle-top:before{content:"\e253"}.glyphicon-console:before{content:"\e254"}.glyphicon-superscript:before{content:"\e255"}.glyphicon-subscript:before{content:"\e256"}.glyphicon-menu-left:before{content:"\e257"}.glyphicon-menu-right:before{content:"\e258"}.glyphicon-menu-down:before{content:"\e259"}.glyphicon-menu-up:before{content:"\e260"}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}:after,:before{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:10px;-webkit-tap-highlight-color:rgba(0,0,0,0)}body{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;line-height:1.42857143;color:#333;background-color:#fff}button,input,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit}a{color:#337ab7;text-decoration:none}a:focus,a:hover{color:#23527c;text-decoration:underline}a:focus{outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}figure{margin:0}img{vertical-align:middle}.carousel-inner>.item>a>img,.carousel-inner>.item>img,.img-responsive,.thumbnail a>img,.thumbnail>img{display:block;max-width:100%;height:auto}.img-rounded{border-radius:6px}.img-thumbnail{display:inline-block;max-width:100%;height:auto;padding:4px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:all .2s ease-in-out;-o-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.img-circle{border-radius:50%}hr{margin-top:20px;margin-bottom:20px;border:0;border-top:1px solid #eee}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}[role=button]{cursor:pointer}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{font-family:inherit;font-weight:500;line-height:1.1;color:inherit}.h1 .small,.h1 small,.h2 .small,.h2 small,.h3 .small,.h3 small,.h4 .small,.h4 small,.h5 .small,.h5 small,.h6 .small,.h6 small,h1 .small,h1 small,h2 .small,h2 small,h3 .small,h3 small,h4 .small,h4 small,h5 .small,h5 small,h6 .small,h6 small{font-weight:400;line-height:1;color:#777}.h1,.h2,.h3,h1,h2,h3{margin-top:20px;margin-bottom:10px}.h1 .small,.h1 small,.h2 .small,.h2 small,.h3 .small,.h3 small,h1 .small,h1 small,h2 .small,h2 small,h3 .small,h3 small{font-size:65%}.h4,.h5,.h6,h4,h5,h6{margin-top:10px;margin-bottom:10px}.h4 .small,.h4 small,.h5 .small,.h5 small,.h6 .small,.h6 small,h4 .small,h4 small,h5 .small,h5 small,h6 .small,h6 small{font-size:75%}.h1,h1{font-size:36px}.h2,h2{font-size:30px}.h3,h3{font-size:24px}.h4,h4{font-size:18px}.h5,h5{font-size:14px}.h6,h6{font-size:12px}p{margin:0 0 10px}.lead{margin-bottom:20px;font-size:16px;font-weight:300;line-height:1.4}@media (min-width:768px){.lead{font-size:21px}}.small,small{font-size:85%}.mark,mark{padding:.2em;background-color:#fcf8e3}.text-left{text-align:left}.text-right{text-align:right}.text-center{text-align:center}.text-justify{text-align:justify}.text-nowrap{white-space:nowrap}.text-lowercase{text-transform:lowercase}.text-uppercase{text-transform:uppercase}.text-capitalize{text-transform:capitalize}.text-muted{color:#777}.text-primary{color:#337ab7}a.text-primary:focus,a.text-primary:hover{color:#286090}.text-success{color:#3c763d}a.text-success:focus,a.text-success:hover{color:#2b542c}.text-info{color:#31708f}a.text-info:focus,a.text-info:hover{color:#245269}.text-warning{color:#8a6d3b}a.text-warning:focus,a.text-warning:hover{color:#66512c}.text-danger{color:#a94442}a.text-danger:focus,a.text-danger:hover{color:#843534}.bg-primary{color:#fff;background-color:#337ab7}a.bg-primary:focus,a.bg-primary:hover{background-color:#286090}.bg-success{background-color:#dff0d8}a.bg-success:focus,a.bg-success:hover{background-color:#c1e2b3}.bg-info{background-color:#d9edf7}a.bg-info:focus,a.bg-info:hover{background-color:#afd9ee}.bg-warning{background-color:#fcf8e3}a.bg-warning:focus,a.bg-warning:hover{background-color:#f7ecb5}.bg-danger{background-color:#f2dede}a.bg-danger:focus,a.bg-danger:hover{background-color:#e4b9b9}.page-header{padding-bottom:9px;margin:40px 0 20px;border-bottom:1px solid #eee}ol,ul{margin-top:0;margin-bottom:10px}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;margin-left:-5px;list-style:none}.list-inline>li{display:inline-block;padding-right:5px;padding-left:5px}dl{margin-top:0;margin-bottom:20px}dd,dt{line-height:1.42857143}dt{font-weight:700}dd{margin-left:0}@media (min-width:768px){.dl-horizontal dt{float:left;width:160px;overflow:hidden;clear:left;text-align:right;text-overflow:ellipsis;white-space:nowrap}.dl-horizontal dd{margin-left:180px}}abbr[data-original-title],abbr[title]{cursor:help;border-bottom:1px dotted #777}.initialism{font-size:90%;text-transform:uppercase}blockquote{padding:10px 20px;margin:0 0 20px;font-size:17.5px;border-left:5px solid #eee}blockquote ol:last-child,blockquote p:last-child,blockquote ul:last-child{margin-bottom:0}blockquote .small,blockquote footer,blockquote small{display:block;font-size:80%;line-height:1.42857143;color:#777}blockquote .small:before,blockquote footer:before,blockquote small:before{content:'\2014 \00A0'}.blockquote-reverse,blockquote.pull-right{padding-right:15px;padding-left:0;text-align:right;border-right:5px solid #eee;border-left:0}.blockquote-reverse .small:before,.blockquote-reverse footer:before,.blockquote-reverse small:before,blockquote.pull-right .small:before,blockquote.pull-right footer:before,blockquote.pull-right small:before{content:''}.blockquote-reverse .small:after,.blockquote-reverse footer:after,.blockquote-reverse small:after,blockquote.pull-right .small:after,blockquote.pull-right footer:after,blockquote.pull-right small:after{content:'\00A0 \2014'}address{margin-bottom:20px;font-style:normal;line-height:1.42857143}code,kbd,pre,samp{font-family:Menlo,Monaco,Consolas,"Courier New",monospace}code{padding:2px 4px;font-size:90%;color:#c7254e;background-color:#f9f2f4;border-radius:4px}kbd{padding:2px 4px;font-size:90%;color:#fff;background-color:#333;border-radius:3px;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,.25);box-shadow:inset 0 -1px 0 rgba(0,0,0,.25)}kbd kbd{padding:0;font-size:100%;font-weight:700;-webkit-box-shadow:none;box-shadow:none}pre{display:block;padding:9.5px;margin:0 0 10px;font-size:13px;line-height:1.42857143;color:#333;word-break:break-all;word-wrap:break-word;background-color:#f5f5f5;border:1px solid #ccc;border-radius:4px}pre code{padding:0;font-size:inherit;color:inherit;white-space:pre-wrap;background-color:transparent;border-radius:0}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width:768px){.container{width:750px}}@media (min-width:992px){.container{width:970px}}@media (min-width:1200px){.container{width:1170px}}.container-fluid{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}.row{margin-right:-15px;margin-left:-15px}.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-xs-1,.col-xs-10,.col-xs-11,.col-xs-12,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9{position:relative;min-height:1px;padding-right:15px;padding-left:15px}.col-xs-1,.col-xs-10,.col-xs-11,.col-xs-12,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9{float:left}.col-xs-12{width:100%}.col-xs-11{width:91.66666667%}.col-xs-10{width:83.33333333%}.col-xs-9{width:75%}.col-xs-8{width:66.66666667%}.col-xs-7{width:58.33333333%}.col-xs-6{width:50%}.col-xs-5{width:41.66666667%}.col-xs-4{width:33.33333333%}.col-xs-3{width:25%}.col-xs-2{width:16.66666667%}.col-xs-1{width:8.33333333%}.col-xs-pull-12{right:100%}.col-xs-pull-11{right:91.66666667%}.col-xs-pull-10{right:83.33333333%}.col-xs-pull-9{right:75%}.col-xs-pull-8{right:66.66666667%}.col-xs-pull-7{right:58.33333333%}.col-xs-pull-6{right:50%}.col-xs-pull-5{right:41.66666667%}.col-xs-pull-4{right:33.33333333%}.col-xs-pull-3{right:25%}.col-xs-pull-2{right:16.66666667%}.col-xs-pull-1{right:8.33333333%}.col-xs-pull-0{right:auto}.col-xs-push-12{left:100%}.col-xs-push-11{left:91.66666667%}.col-xs-push-10{left:83.33333333%}.col-xs-push-9{left:75%}.col-xs-push-8{left:66.66666667%}.col-xs-push-7{left:58.33333333%}.col-xs-push-6{left:50%}.col-xs-push-5{left:41.66666667%}.col-xs-push-4{left:33.33333333%}.col-xs-push-3{left:25%}.col-xs-push-2{left:16.66666667%}.col-xs-push-1{left:8.33333333%}.col-xs-push-0{left:auto}.col-xs-offset-12{margin-left:100%}.col-xs-offset-11{margin-left:91.66666667%}.col-xs-offset-10{margin-left:83.33333333%}.col-xs-offset-9{margin-left:75%}.col-xs-offset-8{margin-left:66.66666667%}.col-xs-offset-7{margin-left:58.33333333%}.col-xs-offset-6{margin-left:50%}.col-xs-offset-5{margin-left:41.66666667%}.col-xs-offset-4{margin-left:33.33333333%}.col-xs-offset-3{margin-left:25%}.col-xs-offset-2{margin-left:16.66666667%}.col-xs-offset-1{margin-left:8.33333333%}.col-xs-offset-0{margin-left:0}@media (min-width:768px){.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9{float:left}.col-sm-12{width:100%}.col-sm-11{width:91.66666667%}.col-sm-10{width:83.33333333%}.col-sm-9{width:75%}.col-sm-8{width:66.66666667%}.col-sm-7{width:58.33333333%}.col-sm-6{width:50%}.col-sm-5{width:41.66666667%}.col-sm-4{width:33.33333333%}.col-sm-3{width:25%}.col-sm-2{width:16.66666667%}.col-sm-1{width:8.33333333%}.col-sm-pull-12{right:100%}.col-sm-pull-11{right:91.66666667%}.col-sm-pull-10{right:83.33333333%}.col-sm-pull-9{right:75%}.col-sm-pull-8{right:66.66666667%}.col-sm-pull-7{right:58.33333333%}.col-sm-pull-6{right:50%}.col-sm-pull-5{right:41.66666667%}.col-sm-pull-4{right:33.33333333%}.col-sm-pull-3{right:25%}.col-sm-pull-2{right:16.66666667%}.col-sm-pull-1{right:8.33333333%}.col-sm-pull-0{right:auto}.col-sm-push-12{left:100%}.col-sm-push-11{left:91.66666667%}.col-sm-push-10{left:83.33333333%}.col-sm-push-9{left:75%}.col-sm-push-8{left:66.66666667%}.col-sm-push-7{left:58.33333333%}.col-sm-push-6{left:50%}.col-sm-push-5{left:41.66666667%}.col-sm-push-4{left:33.33333333%}.col-sm-push-3{left:25%}.col-sm-push-2{left:16.66666667%}.col-sm-push-1{left:8.33333333%}.col-sm-push-0{left:auto}.col-sm-offset-12{margin-left:100%}.col-sm-offset-11{margin-left:91.66666667%}.col-sm-offset-10{margin-left:83.33333333%}.col-sm-offset-9{margin-left:75%}.col-sm-offset-8{margin-left:66.66666667%}.col-sm-offset-7{margin-left:58.33333333%}.col-sm-offset-6{margin-left:50%}.col-sm-offset-5{margin-left:41.66666667%}.col-sm-offset-4{margin-left:33.33333333%}.col-sm-offset-3{margin-left:25%}.col-sm-offset-2{margin-left:16.66666667%}.col-sm-offset-1{margin-left:8.33333333%}.col-sm-offset-0{margin-left:0}}@media (min-width:992px){.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9{float:left}.col-md-12{width:100%}.col-md-11{width:91.66666667%}.col-md-10{width:83.33333333%}.col-md-9{width:75%}.col-md-8{width:66.66666667%}.col-md-7{width:58.33333333%}.col-md-6{width:50%}.col-md-5{width:41.66666667%}.col-md-4{width:33.33333333%}.col-md-3{width:25%}.col-md-2{width:16.66666667%}.col-md-1{width:8.33333333%}.col-md-pull-12{right:100%}.col-md-pull-11{right:91.66666667%}.col-md-pull-10{right:83.33333333%}.col-md-pull-9{right:75%}.col-md-pull-8{right:66.66666667%}.col-md-pull-7{right:58.33333333%}.col-md-pull-6{right:50%}.col-md-pull-5{right:41.66666667%}.col-md-pull-4{right:33.33333333%}.col-md-pull-3{right:25%}.col-md-pull-2{right:16.66666667%}.col-md-pull-1{right:8.33333333%}.col-md-pull-0{right:auto}.col-md-push-12{left:100%}.col-md-push-11{left:91.66666667%}.col-md-push-10{left:83.33333333%}.col-md-push-9{left:75%}.col-md-push-8{left:66.66666667%}.col-md-push-7{left:58.33333333%}.col-md-push-6{left:50%}.col-md-push-5{left:41.66666667%}.col-md-push-4{left:33.33333333%}.col-md-push-3{left:25%}.col-md-push-2{left:16.66666667%}.col-md-push-1{left:8.33333333%}.col-md-push-0{left:auto}.col-md-offset-12{margin-left:100%}.col-md-offset-11{margin-left:91.66666667%}.col-md-offset-10{margin-left:83.33333333%}.col-md-offset-9{margin-left:75%}.col-md-offset-8{margin-left:66.66666667%}.col-md-offset-7{margin-left:58.33333333%}.col-md-offset-6{margin-left:50%}.col-md-offset-5{margin-left:41.66666667%}.col-md-offset-4{margin-left:33.33333333%}.col-md-offset-3{margin-left:25%}.col-md-offset-2{margin-left:16.66666667%}.col-md-offset-1{margin-left:8.33333333%}.col-md-offset-0{margin-left:0}}@media (min-width:1200px){.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9{float:left}.col-lg-12{width:100%}.col-lg-11{width:91.66666667%}.col-lg-10{width:83.33333333%}.col-lg-9{width:75%}.col-lg-8{width:66.66666667%}.col-lg-7{width:58.33333333%}.col-lg-6{width:50%}.col-lg-5{width:41.66666667%}.col-lg-4{width:33.33333333%}.col-lg-3{width:25%}.col-lg-2{width:16.66666667%}.col-lg-1{width:8.33333333%}.col-lg-pull-12{right:100%}.col-lg-pull-11{right:91.66666667%}.col-lg-pull-10{right:83.33333333%}.col-lg-pull-9{right:75%}.col-lg-pull-8{right:66.66666667%}.col-lg-pull-7{right:58.33333333%}.col-lg-pull-6{right:50%}.col-lg-pull-5{right:41.66666667%}.col-lg-pull-4{right:33.33333333%}.col-lg-pull-3{right:25%}.col-lg-pull-2{right:16.66666667%}.col-lg-pull-1{right:8.33333333%}.col-lg-pull-0{right:auto}.col-lg-push-12{left:100%}.col-lg-push-11{left:91.66666667%}.col-lg-push-10{left:83.33333333%}.col-lg-push-9{left:75%}.col-lg-push-8{left:66.66666667%}.col-lg-push-7{left:58.33333333%}.col-lg-push-6{left:50%}.col-lg-push-5{left:41.66666667%}.col-lg-push-4{left:33.33333333%}.col-lg-push-3{left:25%}.col-lg-push-2{left:16.66666667%}.col-lg-push-1{left:8.33333333%}.col-lg-push-0{left:auto}.col-lg-offset-12{margin-left:100%}.col-lg-offset-11{margin-left:91.66666667%}.col-lg-offset-10{margin-left:83.33333333%}.col-lg-offset-9{margin-left:75%}.col-lg-offset-8{margin-left:66.66666667%}.col-lg-offset-7{margin-left:58.33333333%}.col-lg-offset-6{margin-left:50%}.col-lg-offset-5{margin-left:41.66666667%}.col-lg-offset-4{margin-left:33.33333333%}.col-lg-offset-3{margin-left:25%}.col-lg-offset-2{margin-left:16.66666667%}.col-lg-offset-1{margin-left:8.33333333%}.col-lg-offset-0{margin-left:0}}table{background-color:transparent}caption{padding-top:8px;padding-bottom:8px;color:#777;text-align:left}th{text-align:left}.table{width:100%;max-width:100%;margin-bottom:20px}.table>tbody>tr>td,.table>tbody>tr>th,.table>tfoot>tr>td,.table>tfoot>tr>th,.table>thead>tr>td,.table>thead>tr>th{padding:8px;line-height:1.42857143;vertical-align:top;border-top:1px solid #ddd}.table>thead>tr>th{vertical-align:bottom;border-bottom:2px solid #ddd}.table>caption+thead>tr:first-child>td,.table>caption+thead>tr:first-child>th,.table>colgroup+thead>tr:first-child>td,.table>colgroup+thead>tr:first-child>th,.table>thead:first-child>tr:first-child>td,.table>thead:first-child>tr:first-child>th{border-top:0}.table>tbody+tbody{border-top:2px solid #ddd}.table .table{background-color:#fff}.table-condensed>tbody>tr>td,.table-condensed>tbody>tr>th,.table-condensed>tfoot>tr>td,.table-condensed>tfoot>tr>th,.table-condensed>thead>tr>td,.table-condensed>thead>tr>th{padding:5px}.table-bordered{border:1px solid #ddd}.table-bordered>tbody>tr>td,.table-bordered>tbody>tr>th,.table-bordered>tfoot>tr>td,.table-bordered>tfoot>tr>th,.table-bordered>thead>tr>td,.table-bordered>thead>tr>th{border:1px solid #ddd}.table-bordered>thead>tr>td,.table-bordered>thead>tr>th{border-bottom-width:2px}.table-striped>tbody>tr:nth-of-type(odd){background-color:#f9f9f9}.table-hover>tbody>tr:hover{background-color:#f5f5f5}table col[class*=col-]{position:static;display:table-column;float:none}table td[class*=col-],table th[class*=col-]{position:static;display:table-cell;float:none}.table>tbody>tr.active>td,.table>tbody>tr.active>th,.table>tbody>tr>td.active,.table>tbody>tr>th.active,.table>tfoot>tr.active>td,.table>tfoot>tr.active>th,.table>tfoot>tr>td.active,.table>tfoot>tr>th.active,.table>thead>tr.active>td,.table>thead>tr.active>th,.table>thead>tr>td.active,.table>thead>tr>th.active{background-color:#f5f5f5}.table-hover>tbody>tr.active:hover>td,.table-hover>tbody>tr.active:hover>th,.table-hover>tbody>tr:hover>.active,.table-hover>tbody>tr>td.active:hover,.table-hover>tbody>tr>th.active:hover{background-color:#e8e8e8}.table>tbody>tr.success>td,.table>tbody>tr.success>th,.table>tbody>tr>td.success,.table>tbody>tr>th.success,.table>tfoot>tr.success>td,.table>tfoot>tr.success>th,.table>tfoot>tr>td.success,.table>tfoot>tr>th.success,.table>thead>tr.success>td,.table>thead>tr.success>th,.table>thead>tr>td.success,.table>thead>tr>th.success{background-color:#dff0d8}.table-hover>tbody>tr.success:hover>td,.table-hover>tbody>tr.success:hover>th,.table-hover>tbody>tr:hover>.success,.table-hover>tbody>tr>td.success:hover,.table-hover>tbody>tr>th.success:hover{background-color:#d0e9c6}.table>tbody>tr.info>td,.table>tbody>tr.info>th,.table>tbody>tr>td.info,.table>tbody>tr>th.info,.table>tfoot>tr.info>td,.table>tfoot>tr.info>th,.table>tfoot>tr>td.info,.table>tfoot>tr>th.info,.table>thead>tr.info>td,.table>thead>tr.info>th,.table>thead>tr>td.info,.table>thead>tr>th.info{background-color:#d9edf7}.table-hover>tbody>tr.info:hover>td,.table-hover>tbody>tr.info:hover>th,.table-hover>tbody>tr:hover>.info,.table-hover>tbody>tr>td.info:hover,.table-hover>tbody>tr>th.info:hover{background-color:#c4e3f3}.table>tbody>tr.warning>td,.table>tbody>tr.warning>th,.table>tbody>tr>td.warning,.table>tbody>tr>th.warning,.table>tfoot>tr.warning>td,.table>tfoot>tr.warning>th,.table>tfoot>tr>td.warning,.table>tfoot>tr>th.warning,.table>thead>tr.warning>td,.table>thead>tr.warning>th,.table>thead>tr>td.warning,.table>thead>tr>th.warning{background-color:#fcf8e3}.table-hover>tbody>tr.warning:hover>td,.table-hover>tbody>tr.warning:hover>th,.table-hover>tbody>tr:hover>.warning,.table-hover>tbody>tr>td.warning:hover,.table-hover>tbody>tr>th.warning:hover{background-color:#faf2cc}.table>tbody>tr.danger>td,.table>tbody>tr.danger>th,.table>tbody>tr>td.danger,.table>tbody>tr>th.danger,.table>tfoot>tr.danger>td,.table>tfoot>tr.danger>th,.table>tfoot>tr>td.danger,.table>tfoot>tr>th.danger,.table>thead>tr.danger>td,.table>thead>tr.danger>th,.table>thead>tr>td.danger,.table>thead>tr>th.danger{background-color:#f2dede}.table-hover>tbody>tr.danger:hover>td,.table-hover>tbody>tr.danger:hover>th,.table-hover>tbody>tr:hover>.danger,.table-hover>tbody>tr>td.danger:hover,.table-hover>tbody>tr>th.danger:hover{background-color:#ebcccc}.table-responsive{min-height:.01%;overflow-x:auto}@media screen and (max-width:767px){.table-responsive{width:100%;margin-bottom:15px;overflow-y:hidden;-ms-overflow-style:-ms-autohiding-scrollbar;border:1px solid #ddd}.table-responsive>.table{margin-bottom:0}.table-responsive>.table>tbody>tr>td,.table-responsive>.table>tbody>tr>th,.table-responsive>.table>tfoot>tr>td,.table-responsive>.table>tfoot>tr>th,.table-responsive>.table>thead>tr>td,.table-responsive>.table>thead>tr>th{white-space:nowrap}.table-responsive>.table-bordered{border:0}.table-responsive>.table-bordered>tbody>tr>td:first-child,.table-responsive>.table-bordered>tbody>tr>th:first-child,.table-responsive>.table-bordered>tfoot>tr>td:first-child,.table-responsive>.table-bordered>tfoot>tr>th:first-child,.table-responsive>.table-bordered>thead>tr>td:first-child,.table-responsive>.table-bordered>thead>tr>th:first-child{border-left:0}.table-responsive>.table-bordered>tbody>tr>td:last-child,.table-responsive>.table-bordered>tbody>tr>th:last-child,.table-responsive>.table-bordered>tfoot>tr>td:last-child,.table-responsive>.table-bordered>tfoot>tr>th:last-child,.table-responsive>.table-bordered>thead>tr>td:last-child,.table-responsive>.table-bordered>thead>tr>th:last-child{border-right:0}.table-responsive>.table-bordered>tbody>tr:last-child>td,.table-responsive>.table-bordered>tbody>tr:last-child>th,.table-responsive>.table-bordered>tfoot>tr:last-child>td,.table-responsive>.table-bordered>tfoot>tr:last-child>th{border-bottom:0}}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;padding:0;margin-bottom:20px;font-size:21px;line-height:inherit;color:#333;border:0;border-bottom:1px solid #e5e5e5}label{display:inline-block;max-width:100%;margin-bottom:5px;font-weight:700}input[type=search]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}input[type=checkbox],input[type=radio]{margin:4px 0 0;margin-top:1px\9;line-height:normal}input[type=file]{display:block}input[type=range]{display:block;width:100%}select[multiple],select[size]{height:auto}input[type=file]:focus,input[type=checkbox]:focus,input[type=radio]:focus{outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}output{display:block;padding-top:7px;font-size:14px;line-height:1.42857143;color:#555}.form-control{display:block;width:100%;height:34px;padding:6px 12px;font-size:14px;line-height:1.42857143;color:#555;background-color:#fff;background-image:none;border:1px solid #ccc;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075);-webkit-transition:border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s;-o-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s}.form-control:focus{border-color:#66afe9;outline:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6);box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6)}.form-control::-moz-placeholder{color:#999;opacity:1}.form-control:-ms-input-placeholder{color:#999}.form-control::-webkit-input-placeholder{color:#999}.form-control::-ms-expand{background-color:transparent;border:0}.form-control[disabled],.form-control[readonly],fieldset[disabled] .form-control{background-color:#eee;opacity:1}.form-control[disabled],fieldset[disabled] .form-control{cursor:not-allowed}textarea.form-control{height:auto}input[type=search]{-webkit-appearance:none}@media screen and (-webkit-min-device-pixel-ratio:0){input[type=date].form-control,input[type=time].form-control,input[type=datetime-local].form-control,input[type=month].form-control{line-height:34px}.input-group-sm input[type=date],.input-group-sm input[type=time],.input-group-sm input[type=datetime-local],.input-group-sm input[type=month],input[type=date].input-sm,input[type=time].input-sm,input[type=datetime-local].input-sm,input[type=month].input-sm{line-height:30px}.input-group-lg input[type=date],.input-group-lg input[type=time],.input-group-lg input[type=datetime-local],.input-group-lg input[type=month],input[type=date].input-lg,input[type=time].input-lg,input[type=datetime-local].input-lg,input[type=month].input-lg{line-height:46px}}.form-group{margin-bottom:15px}.checkbox,.radio{position:relative;display:block;margin-top:10px;margin-bottom:10px}.checkbox label,.radio label{min-height:20px;padding-left:20px;margin-bottom:0;font-weight:400;cursor:pointer}.checkbox input[type=checkbox],.checkbox-inline input[type=checkbox],.radio input[type=radio],.radio-inline input[type=radio]{position:absolute;margin-top:4px\9;margin-left:-20px}.checkbox+.checkbox,.radio+.radio{margin-top:-5px}.checkbox-inline,.radio-inline{position:relative;display:inline-block;padding-left:20px;margin-bottom:0;font-weight:400;vertical-align:middle;cursor:pointer}.checkbox-inline+.checkbox-inline,.radio-inline+.radio-inline{margin-top:0;margin-left:10px}fieldset[disabled] input[type=checkbox],fieldset[disabled] input[type=radio],input[type=checkbox].disabled,input[type=checkbox][disabled],input[type=radio].disabled,input[type=radio][disabled]{cursor:not-allowed}.checkbox-inline.disabled,.radio-inline.disabled,fieldset[disabled] .checkbox-inline,fieldset[disabled] .radio-inline{cursor:not-allowed}.checkbox.disabled label,.radio.disabled label,fieldset[disabled] .checkbox label,fieldset[disabled] .radio label{cursor:not-allowed}.form-control-static{min-height:34px;padding-top:7px;padding-bottom:7px;margin-bottom:0}.form-control-static.input-lg,.form-control-static.input-sm{padding-right:0;padding-left:0}.input-sm{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-sm{height:30px;line-height:30px}select[multiple].input-sm,textarea.input-sm{height:auto}.form-group-sm .form-control{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.form-group-sm select.form-control{height:30px;line-height:30px}.form-group-sm select[multiple].form-control,.form-group-sm textarea.form-control{height:auto}.form-group-sm .form-control-static{height:30px;min-height:32px;padding:6px 10px;font-size:12px;line-height:1.5}.input-lg{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}select.input-lg{height:46px;line-height:46px}select[multiple].input-lg,textarea.input-lg{height:auto}.form-group-lg .form-control{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}.form-group-lg select.form-control{height:46px;line-height:46px}.form-group-lg select[multiple].form-control,.form-group-lg textarea.form-control{height:auto}.form-group-lg .form-control-static{height:46px;min-height:38px;padding:11px 16px;font-size:18px;line-height:1.3333333}.has-feedback{position:relative}.has-feedback .form-control{padding-right:42.5px}.form-control-feedback{position:absolute;top:0;right:0;z-index:2;display:block;width:34px;height:34px;line-height:34px;text-align:center;pointer-events:none}.form-group-lg .form-control+.form-control-feedback,.input-group-lg+.form-control-feedback,.input-lg+.form-control-feedback{width:46px;height:46px;line-height:46px}.form-group-sm .form-control+.form-control-feedback,.input-group-sm+.form-control-feedback,.input-sm+.form-control-feedback{width:30px;height:30px;line-height:30px}.has-success .checkbox,.has-success .checkbox-inline,.has-success .control-label,.has-success .help-block,.has-success .radio,.has-success .radio-inline,.has-success.checkbox label,.has-success.checkbox-inline label,.has-success.radio label,.has-success.radio-inline label{color:#3c763d}.has-success .form-control{border-color:#3c763d;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-success .form-control:focus{border-color:#2b542c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168}.has-success .input-group-addon{color:#3c763d;background-color:#dff0d8;border-color:#3c763d}.has-success .form-control-feedback{color:#3c763d}.has-warning .checkbox,.has-warning .checkbox-inline,.has-warning .control-label,.has-warning .help-block,.has-warning .radio,.has-warning .radio-inline,.has-warning.checkbox label,.has-warning.checkbox-inline label,.has-warning.radio label,.has-warning.radio-inline label{color:#8a6d3b}.has-warning .form-control{border-color:#8a6d3b;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-warning .form-control:focus{border-color:#66512c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b}.has-warning .input-group-addon{color:#8a6d3b;background-color:#fcf8e3;border-color:#8a6d3b}.has-warning .form-control-feedback{color:#8a6d3b}.has-error .checkbox,.has-error .checkbox-inline,.has-error .control-label,.has-error .help-block,.has-error .radio,.has-error .radio-inline,.has-error.checkbox label,.has-error.checkbox-inline label,.has-error.radio label,.has-error.radio-inline label{color:#a94442}.has-error .form-control{border-color:#a94442;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-error .form-control:focus{border-color:#843534;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483}.has-error .input-group-addon{color:#a94442;background-color:#f2dede;border-color:#a94442}.has-error .form-control-feedback{color:#a94442}.has-feedback label~.form-control-feedback{top:25px}.has-feedback label.sr-only~.form-control-feedback{top:0}.help-block{display:block;margin-top:5px;margin-bottom:10px;color:#737373}@media (min-width:768px){.form-inline .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .form-control-static{display:inline-block}.form-inline .input-group{display:inline-table;vertical-align:middle}.form-inline .input-group .form-control,.form-inline .input-group .input-group-addon,.form-inline .input-group .input-group-btn{width:auto}.form-inline .input-group>.form-control{width:100%}.form-inline .control-label{margin-bottom:0;vertical-align:middle}.form-inline .checkbox,.form-inline .radio{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.form-inline .checkbox label,.form-inline .radio label{padding-left:0}.form-inline .checkbox input[type=checkbox],.form-inline .radio input[type=radio]{position:relative;margin-left:0}.form-inline .has-feedback .form-control-feedback{top:0}}.form-horizontal .checkbox,.form-horizontal .checkbox-inline,.form-horizontal .radio,.form-horizontal .radio-inline{padding-top:7px;margin-top:0;margin-bottom:0}.form-horizontal .checkbox,.form-horizontal .radio{min-height:27px}.form-horizontal .form-group{margin-right:-15px;margin-left:-15px}@media (min-width:768px){.form-horizontal .control-label{padding-top:7px;margin-bottom:0;text-align:right}}.form-horizontal .has-feedback .form-control-feedback{right:15px}@media (min-width:768px){.form-horizontal .form-group-lg .control-label{padding-top:11px;font-size:18px}}@media (min-width:768px){.form-horizontal .form-group-sm .control-label{padding-top:6px;font-size:12px}}.btn{display:inline-block;padding:6px 12px;margin-bottom:0;font-size:14px;font-weight:400;line-height:1.42857143;text-align:center;white-space:nowrap;vertical-align:middle;-ms-touch-action:manipulation;touch-action:manipulation;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background-image:none;border:1px solid transparent;border-radius:4px}.btn.active.focus,.btn.active:focus,.btn.focus,.btn:active.focus,.btn:active:focus,.btn:focus{outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.btn.focus,.btn:focus,.btn:hover{color:#333;text-decoration:none}.btn.active,.btn:active{background-image:none;outline:0;-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn.disabled,.btn[disabled],fieldset[disabled] .btn{cursor:not-allowed;filter:alpha(opacity=65);-webkit-box-shadow:none;box-shadow:none;opacity:.65}a.btn.disabled,fieldset[disabled] a.btn{pointer-events:none}.btn-default{color:#333;background-color:#fff;border-color:#ccc}.btn-default.focus,.btn-default:focus{color:#333;background-color:#e6e6e6;border-color:#8c8c8c}.btn-default:hover{color:#333;background-color:#e6e6e6;border-color:#adadad}.btn-default.active,.btn-default:active,.open>.dropdown-toggle.btn-default{color:#333;background-color:#e6e6e6;border-color:#adadad}.btn-default.active.focus,.btn-default.active:focus,.btn-default.active:hover,.btn-default:active.focus,.btn-default:active:focus,.btn-default:active:hover,.open>.dropdown-toggle.btn-default.focus,.open>.dropdown-toggle.btn-default:focus,.open>.dropdown-toggle.btn-default:hover{color:#333;background-color:#d4d4d4;border-color:#8c8c8c}.btn-default.active,.btn-default:active,.open>.dropdown-toggle.btn-default{background-image:none}.btn-default.disabled.focus,.btn-default.disabled:focus,.btn-default.disabled:hover,.btn-default[disabled].focus,.btn-default[disabled]:focus,.btn-default[disabled]:hover,fieldset[disabled] .btn-default.focus,fieldset[disabled] .btn-default:focus,fieldset[disabled] .btn-default:hover{background-color:#fff;border-color:#ccc}.btn-default .badge{color:#fff;background-color:#333}.btn-primary{color:#fff;background-color:#337ab7;border-color:#2e6da4}.btn-primary.focus,.btn-primary:focus{color:#fff;background-color:#286090;border-color:#122b40}.btn-primary:hover{color:#fff;background-color:#286090;border-color:#204d74}.btn-primary.active,.btn-primary:active,.open>.dropdown-toggle.btn-primary{color:#fff;background-color:#286090;border-color:#204d74}.btn-primary.active.focus,.btn-primary.active:focus,.btn-primary.active:hover,.btn-primary:active.focus,.btn-primary:active:focus,.btn-primary:active:hover,.open>.dropdown-toggle.btn-primary.focus,.open>.dropdown-toggle.btn-primary:focus,.open>.dropdown-toggle.btn-primary:hover{color:#fff;background-color:#204d74;border-color:#122b40}.btn-primary.active,.btn-primary:active,.open>.dropdown-toggle.btn-primary{background-image:none}.btn-primary.disabled.focus,.btn-primary.disabled:focus,.btn-primary.disabled:hover,.btn-primary[disabled].focus,.btn-primary[disabled]:focus,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary.focus,fieldset[disabled] .btn-primary:focus,fieldset[disabled] .btn-primary:hover{background-color:#337ab7;border-color:#2e6da4}.btn-primary .badge{color:#337ab7;background-color:#fff}.btn-success{color:#fff;background-color:#5cb85c;border-color:#4cae4c}.btn-success.focus,.btn-success:focus{color:#fff;background-color:#449d44;border-color:#255625}.btn-success:hover{color:#fff;background-color:#449d44;border-color:#398439}.btn-success.active,.btn-success:active,.open>.dropdown-toggle.btn-success{color:#fff;background-color:#449d44;border-color:#398439}.btn-success.active.focus,.btn-success.active:focus,.btn-success.active:hover,.btn-success:active.focus,.btn-success:active:focus,.btn-success:active:hover,.open>.dropdown-toggle.btn-success.focus,.open>.dropdown-toggle.btn-success:focus,.open>.dropdown-toggle.btn-success:hover{color:#fff;background-color:#398439;border-color:#255625}.btn-success.active,.btn-success:active,.open>.dropdown-toggle.btn-success{background-image:none}.btn-success.disabled.focus,.btn-success.disabled:focus,.btn-success.disabled:hover,.btn-success[disabled].focus,.btn-success[disabled]:focus,.btn-success[disabled]:hover,fieldset[disabled] .btn-success.focus,fieldset[disabled] .btn-success:focus,fieldset[disabled] .btn-success:hover{background-color:#5cb85c;border-color:#4cae4c}.btn-success .badge{color:#5cb85c;background-color:#fff}.btn-info{color:#fff;background-color:#5bc0de;border-color:#46b8da}.btn-info.focus,.btn-info:focus{color:#fff;background-color:#31b0d5;border-color:#1b6d85}.btn-info:hover{color:#fff;background-color:#31b0d5;border-color:#269abc}.btn-info.active,.btn-info:active,.open>.dropdown-toggle.btn-info{color:#fff;background-color:#31b0d5;border-color:#269abc}.btn-info.active.focus,.btn-info.active:focus,.btn-info.active:hover,.btn-info:active.focus,.btn-info:active:focus,.btn-info:active:hover,.open>.dropdown-toggle.btn-info.focus,.open>.dropdown-toggle.btn-info:focus,.open>.dropdown-toggle.btn-info:hover{color:#fff;background-color:#269abc;border-color:#1b6d85}.btn-info.active,.btn-info:active,.open>.dropdown-toggle.btn-info{background-image:none}.btn-info.disabled.focus,.btn-info.disabled:focus,.btn-info.disabled:hover,.btn-info[disabled].focus,.btn-info[disabled]:focus,.btn-info[disabled]:hover,fieldset[disabled] .btn-info.focus,fieldset[disabled] .btn-info:focus,fieldset[disabled] .btn-info:hover{background-color:#5bc0de;border-color:#46b8da}.btn-info .badge{color:#5bc0de;background-color:#fff}.btn-warning{color:#fff;background-color:#f0ad4e;border-color:#eea236}.btn-warning.focus,.btn-warning:focus{color:#fff;background-color:#ec971f;border-color:#985f0d}.btn-warning:hover{color:#fff;background-color:#ec971f;border-color:#d58512}.btn-warning.active,.btn-warning:active,.open>.dropdown-toggle.btn-warning{color:#fff;background-color:#ec971f;border-color:#d58512}.btn-warning.active.focus,.btn-warning.active:focus,.btn-warning.active:hover,.btn-warning:active.focus,.btn-warning:active:focus,.btn-warning:active:hover,.open>.dropdown-toggle.btn-warning.focus,.open>.dropdown-toggle.btn-warning:focus,.open>.dropdown-toggle.btn-warning:hover{color:#fff;background-color:#d58512;border-color:#985f0d}.btn-warning.active,.btn-warning:active,.open>.dropdown-toggle.btn-warning{background-image:none}.btn-warning.disabled.focus,.btn-warning.disabled:focus,.btn-warning.disabled:hover,.btn-warning[disabled].focus,.btn-warning[disabled]:focus,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning.focus,fieldset[disabled] .btn-warning:focus,fieldset[disabled] .btn-warning:hover{background-color:#f0ad4e;border-color:#eea236}.btn-warning .badge{color:#f0ad4e;background-color:#fff}.btn-danger{color:#fff;background-color:#d9534f;border-color:#d43f3a}.btn-danger.focus,.btn-danger:focus{color:#fff;background-color:#c9302c;border-color:#761c19}.btn-danger:hover{color:#fff;background-color:#c9302c;border-color:#ac2925}.btn-danger.active,.btn-danger:active,.open>.dropdown-toggle.btn-danger{color:#fff;background-color:#c9302c;border-color:#ac2925}.btn-danger.active.focus,.btn-danger.active:focus,.btn-danger.active:hover,.btn-danger:active.focus,.btn-danger:active:focus,.btn-danger:active:hover,.open>.dropdown-toggle.btn-danger.focus,.open>.dropdown-toggle.btn-danger:focus,.open>.dropdown-toggle.btn-danger:hover{color:#fff;background-color:#ac2925;border-color:#761c19}.btn-danger.active,.btn-danger:active,.open>.dropdown-toggle.btn-danger{background-image:none}.btn-danger.disabled.focus,.btn-danger.disabled:focus,.btn-danger.disabled:hover,.btn-danger[disabled].focus,.btn-danger[disabled]:focus,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger.focus,fieldset[disabled] .btn-danger:focus,fieldset[disabled] .btn-danger:hover{background-color:#d9534f;border-color:#d43f3a}.btn-danger .badge{color:#d9534f;background-color:#fff}.btn-link{font-weight:400;color:#337ab7;border-radius:0}.btn-link,.btn-link.active,.btn-link:active,.btn-link[disabled],fieldset[disabled] .btn-link{background-color:transparent;-webkit-box-shadow:none;box-shadow:none}.btn-link,.btn-link:active,.btn-link:focus,.btn-link:hover{border-color:transparent}.btn-link:focus,.btn-link:hover{color:#23527c;text-decoration:underline;background-color:transparent}.btn-link[disabled]:focus,.btn-link[disabled]:hover,fieldset[disabled] .btn-link:focus,fieldset[disabled] .btn-link:hover{color:#777;text-decoration:none}.btn-group-lg>.btn,.btn-lg{padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}.btn-group-sm>.btn,.btn-sm{padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.btn-group-xs>.btn,.btn-xs{padding:1px 5px;font-size:12px;line-height:1.5;border-radius:3px}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:5px}input[type=button].btn-block,input[type=reset].btn-block,input[type=submit].btn-block{width:100%}.fade{opacity:0;-webkit-transition:opacity .15s linear;-o-transition:opacity .15s linear;transition:opacity .15s linear}.fade.in{opacity:1}.collapse{display:none}.collapse.in{display:block}tr.collapse.in{display:table-row}tbody.collapse.in{display:table-row-group}.collapsing{position:relative;height:0;overflow:hidden;-webkit-transition-timing-function:ease;-o-transition-timing-function:ease;transition-timing-function:ease;-webkit-transition-duration:.35s;-o-transition-duration:.35s;transition-duration:.35s;-webkit-transition-property:height,visibility;-o-transition-property:height,visibility;transition-property:height,visibility}.caret{display:inline-block;width:0;height:0;margin-left:2px;vertical-align:middle;border-top:4px dashed;border-top:4px solid\9;border-right:4px solid transparent;border-left:4px solid transparent}.dropdown,.dropup{position:relative}.dropdown-toggle:focus{outline:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:160px;padding:5px 0;margin:2px 0 0;font-size:14px;text-align:left;list-style:none;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid #ccc;border:1px solid rgba(0,0,0,.15);border-radius:4px;-webkit-box-shadow:0 6px 12px rgba(0,0,0,.175);box-shadow:0 6px 12px rgba(0,0,0,.175)}.dropdown-menu.pull-right{right:0;left:auto}.dropdown-menu .divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.dropdown-menu>li>a{display:block;padding:3px 20px;clear:both;font-weight:400;line-height:1.42857143;color:#333;white-space:nowrap}.dropdown-menu>li>a:focus,.dropdown-menu>li>a:hover{color:#262626;text-decoration:none;background-color:#f5f5f5}.dropdown-menu>.active>a,.dropdown-menu>.active>a:focus,.dropdown-menu>.active>a:hover{color:#fff;text-decoration:none;background-color:#337ab7;outline:0}.dropdown-menu>.disabled>a,.dropdown-menu>.disabled>a:focus,.dropdown-menu>.disabled>a:hover{color:#777}.dropdown-menu>.disabled>a:focus,.dropdown-menu>.disabled>a:hover{text-decoration:none;cursor:not-allowed;background-color:transparent;background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.open>.dropdown-menu{display:block}.open>a{outline:0}.dropdown-menu-right{right:0;left:auto}.dropdown-menu-left{right:auto;left:0}.dropdown-header{display:block;padding:3px 20px;font-size:12px;line-height:1.42857143;color:#777;white-space:nowrap}.dropdown-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:990}.pull-right>.dropdown-menu{right:0;left:auto}.dropup .caret,.navbar-fixed-bottom .dropdown .caret{content:"";border-top:0;border-bottom:4px dashed;border-bottom:4px solid\9}.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{top:auto;bottom:100%;margin-bottom:2px}@media (min-width:768px){.navbar-right .dropdown-menu{right:0;left:auto}.navbar-right .dropdown-menu-left{right:auto;left:0}}.btn-group,.btn-group-vertical{position:relative;display:inline-block;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;float:left}.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:hover,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus,.btn-group>.btn:hover{z-index:2}.btn-group .btn+.btn,.btn-group .btn+.btn-group,.btn-group .btn-group+.btn,.btn-group .btn-group+.btn-group{margin-left:-1px}.btn-toolbar{margin-left:-5px}.btn-toolbar .btn,.btn-toolbar .btn-group,.btn-toolbar .input-group{float:left}.btn-toolbar>.btn,.btn-toolbar>.btn-group,.btn-toolbar>.input-group{margin-left:5px}.btn-group>.btn:not(:first-child):not(:last-child):not(.dropdown-toggle){border-radius:0}.btn-group>.btn:first-child{margin-left:0}.btn-group>.btn:first-child:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn:last-child:not(:first-child),.btn-group>.dropdown-toggle:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.btn-group>.btn-group{float:left}.btn-group>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-left-radius:0;border-bottom-left-radius:0}.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0}.btn-group>.btn+.dropdown-toggle{padding-right:8px;padding-left:8px}.btn-group>.btn-lg+.dropdown-toggle{padding-right:12px;padding-left:12px}.btn-group.open .dropdown-toggle{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn-group.open .dropdown-toggle.btn-link{-webkit-box-shadow:none;box-shadow:none}.btn .caret{margin-left:0}.btn-lg .caret{border-width:5px 5px 0;border-bottom-width:0}.dropup .btn-lg .caret{border-width:0 5px 5px}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group,.btn-group-vertical>.btn-group>.btn{display:block;float:none;width:100%;max-width:100%}.btn-group-vertical>.btn-group>.btn{float:none}.btn-group-vertical>.btn+.btn,.btn-group-vertical>.btn+.btn-group,.btn-group-vertical>.btn-group+.btn,.btn-group-vertical>.btn-group+.btn-group{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn:not(:first-child):not(:last-child){border-radius:0}.btn-group-vertical>.btn:first-child:not(:last-child){border-top-left-radius:4px;border-top-right-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn:last-child:not(:first-child){border-top-left-radius:0;border-top-right-radius:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px}.btn-group-vertical>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group-vertical>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group-vertical>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-left-radius:0;border-top-right-radius:0}.btn-group-justified{display:table;width:100%;table-layout:fixed;border-collapse:separate}.btn-group-justified>.btn,.btn-group-justified>.btn-group{display:table-cell;float:none;width:1%}.btn-group-justified>.btn-group .btn{width:100%}.btn-group-justified>.btn-group .dropdown-menu{left:auto}[data-toggle=buttons]>.btn input[type=checkbox],[data-toggle=buttons]>.btn input[type=radio],[data-toggle=buttons]>.btn-group>.btn input[type=checkbox],[data-toggle=buttons]>.btn-group>.btn input[type=radio]{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.input-group{position:relative;display:table;border-collapse:separate}.input-group[class*=col-]{float:none;padding-right:0;padding-left:0}.input-group .form-control{position:relative;z-index:2;float:left;width:100%;margin-bottom:0}.input-group .form-control:focus{z-index:3}.input-group-lg>.form-control,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.btn{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}select.input-group-lg>.form-control,select.input-group-lg>.input-group-addon,select.input-group-lg>.input-group-btn>.btn{height:46px;line-height:46px}select[multiple].input-group-lg>.form-control,select[multiple].input-group-lg>.input-group-addon,select[multiple].input-group-lg>.input-group-btn>.btn,textarea.input-group-lg>.form-control,textarea.input-group-lg>.input-group-addon,textarea.input-group-lg>.input-group-btn>.btn{height:auto}.input-group-sm>.form-control,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.btn{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-group-sm>.form-control,select.input-group-sm>.input-group-addon,select.input-group-sm>.input-group-btn>.btn{height:30px;line-height:30px}select[multiple].input-group-sm>.form-control,select[multiple].input-group-sm>.input-group-addon,select[multiple].input-group-sm>.input-group-btn>.btn,textarea.input-group-sm>.form-control,textarea.input-group-sm>.input-group-addon,textarea.input-group-sm>.input-group-btn>.btn{height:auto}.input-group .form-control,.input-group-addon,.input-group-btn{display:table-cell}.input-group .form-control:not(:first-child):not(:last-child),.input-group-addon:not(:first-child):not(:last-child),.input-group-btn:not(:first-child):not(:last-child){border-radius:0}.input-group-addon,.input-group-btn{width:1%;white-space:nowrap;vertical-align:middle}.input-group-addon{padding:6px 12px;font-size:14px;font-weight:400;line-height:1;color:#555;text-align:center;background-color:#eee;border:1px solid #ccc;border-radius:4px}.input-group-addon.input-sm{padding:5px 10px;font-size:12px;border-radius:3px}.input-group-addon.input-lg{padding:10px 16px;font-size:18px;border-radius:6px}.input-group-addon input[type=checkbox],.input-group-addon input[type=radio]{margin-top:0}.input-group .form-control:first-child,.input-group-addon:first-child,.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group>.btn,.input-group-btn:first-child>.dropdown-toggle,.input-group-btn:last-child>.btn-group:not(:last-child)>.btn,.input-group-btn:last-child>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.input-group-addon:first-child{border-right:0}.input-group .form-control:last-child,.input-group-addon:last-child,.input-group-btn:first-child>.btn-group:not(:first-child)>.btn,.input-group-btn:first-child>.btn:not(:first-child),.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group>.btn,.input-group-btn:last-child>.dropdown-toggle{border-top-left-radius:0;border-bottom-left-radius:0}.input-group-addon:last-child{border-left:0}.input-group-btn{position:relative;font-size:0;white-space:nowrap}.input-group-btn>.btn{position:relative}.input-group-btn>.btn+.btn{margin-left:-1px}.input-group-btn>.btn:active,.input-group-btn>.btn:focus,.input-group-btn>.btn:hover{z-index:2}.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group{margin-right:-1px}.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group{z-index:2;margin-left:-1px}.nav{padding-left:0;margin-bottom:0;list-style:none}.nav>li{position:relative;display:block}.nav>li>a{position:relative;display:block;padding:10px 15px}.nav>li>a:focus,.nav>li>a:hover{text-decoration:none;background-color:#eee}.nav>li.disabled>a{color:#777}.nav>li.disabled>a:focus,.nav>li.disabled>a:hover{color:#777;text-decoration:none;cursor:not-allowed;background-color:transparent}.nav .open>a,.nav .open>a:focus,.nav .open>a:hover{background-color:#eee;border-color:#337ab7}.nav .nav-divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.nav>li>a>img{max-width:none}.nav-tabs{border-bottom:1px solid #ddd}.nav-tabs>li{float:left;margin-bottom:-1px}.nav-tabs>li>a{margin-right:2px;line-height:1.42857143;border:1px solid transparent;border-radius:4px 4px 0 0}.nav-tabs>li>a:hover{border-color:#eee #eee #ddd}.nav-tabs>li.active>a,.nav-tabs>li.active>a:focus,.nav-tabs>li.active>a:hover{color:#555;cursor:default;background-color:#fff;border:1px solid #ddd;border-bottom-color:transparent}.nav-tabs.nav-justified{width:100%;border-bottom:0}.nav-tabs.nav-justified>li{float:none}.nav-tabs.nav-justified>li>a{margin-bottom:5px;text-align:center}.nav-tabs.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-tabs.nav-justified>li{display:table-cell;width:1%}.nav-tabs.nav-justified>li>a{margin-bottom:0}}.nav-tabs.nav-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:focus,.nav-tabs.nav-justified>.active>a:hover{border:1px solid #ddd}@media (min-width:768px){.nav-tabs.nav-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:focus,.nav-tabs.nav-justified>.active>a:hover{border-bottom-color:#fff}}.nav-pills>li{float:left}.nav-pills>li>a{border-radius:4px}.nav-pills>li+li{margin-left:2px}.nav-pills>li.active>a,.nav-pills>li.active>a:focus,.nav-pills>li.active>a:hover{color:#fff;background-color:#337ab7}.nav-stacked>li{float:none}.nav-stacked>li+li{margin-top:2px;margin-left:0}.nav-justified{width:100%}.nav-justified>li{float:none}.nav-justified>li>a{margin-bottom:5px;text-align:center}.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-justified>li{display:table-cell;width:1%}.nav-justified>li>a{margin-bottom:0}}.nav-tabs-justified{border-bottom:0}.nav-tabs-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:focus,.nav-tabs-justified>.active>a:hover{border:1px solid #ddd}@media (min-width:768px){.nav-tabs-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:focus,.nav-tabs-justified>.active>a:hover{border-bottom-color:#fff}}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.navbar{position:relative;min-height:50px;margin-bottom:20px;border:1px solid transparent}@media (min-width:768px){.navbar{border-radius:4px}}@media (min-width:768px){.navbar-header{float:left}}.navbar-collapse{padding-right:15px;padding-left:15px;overflow-x:visible;-webkit-overflow-scrolling:touch;border-top:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 0 rgba(255,255,255,.1)}.navbar-collapse.in{overflow-y:auto}@media (min-width:768px){.navbar-collapse{width:auto;border-top:0;-webkit-box-shadow:none;box-shadow:none}.navbar-collapse.collapse{display:block!important;height:auto!important;padding-bottom:0;overflow:visible!important}.navbar-collapse.in{overflow-y:visible}.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse,.navbar-static-top .navbar-collapse{padding-right:0;padding-left:0}}.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse{max-height:340px}@media (max-device-width:480px) and (orientation:landscape){.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse{max-height:200px}}.container-fluid>.navbar-collapse,.container-fluid>.navbar-header,.container>.navbar-collapse,.container>.navbar-header{margin-right:-15px;margin-left:-15px}@media (min-width:768px){.container-fluid>.navbar-collapse,.container-fluid>.navbar-header,.container>.navbar-collapse,.container>.navbar-header{margin-right:0;margin-left:0}}.navbar-static-top{z-index:1000;border-width:0 0 1px}@media (min-width:768px){.navbar-static-top{border-radius:0}}.navbar-fixed-bottom,.navbar-fixed-top{position:fixed;right:0;left:0;z-index:1030}@media (min-width:768px){.navbar-fixed-bottom,.navbar-fixed-top{border-radius:0}}.navbar-fixed-top{top:0;border-width:0 0 1px}.navbar-fixed-bottom{bottom:0;margin-bottom:0;border-width:1px 0 0}.navbar-brand{float:left;height:50px;padding:15px 15px;font-size:18px;line-height:20px}.navbar-brand:focus,.navbar-brand:hover{text-decoration:none}.navbar-brand>img{display:block}@media (min-width:768px){.navbar>.container .navbar-brand,.navbar>.container-fluid .navbar-brand{margin-left:-15px}}.navbar-toggle{position:relative;float:right;padding:9px 10px;margin-top:8px;margin-right:15px;margin-bottom:8px;background-color:transparent;background-image:none;border:1px solid transparent;border-radius:4px}.navbar-toggle:focus{outline:0}.navbar-toggle .icon-bar{display:block;width:22px;height:2px;border-radius:1px}.navbar-toggle .icon-bar+.icon-bar{margin-top:4px}@media (min-width:768px){.navbar-toggle{display:none}}.navbar-nav{margin:7.5px -15px}.navbar-nav>li>a{padding-top:10px;padding-bottom:10px;line-height:20px}@media (max-width:767px){.navbar-nav .open .dropdown-menu{position:static;float:none;width:auto;margin-top:0;background-color:transparent;border:0;-webkit-box-shadow:none;box-shadow:none}.navbar-nav .open .dropdown-menu .dropdown-header,.navbar-nav .open .dropdown-menu>li>a{padding:5px 15px 5px 25px}.navbar-nav .open .dropdown-menu>li>a{line-height:20px}.navbar-nav .open .dropdown-menu>li>a:focus,.navbar-nav .open .dropdown-menu>li>a:hover{background-image:none}}@media (min-width:768px){.navbar-nav{float:left;margin:0}.navbar-nav>li{float:left}.navbar-nav>li>a{padding-top:15px;padding-bottom:15px}}.navbar-form{padding:10px 15px;margin-top:8px;margin-right:-15px;margin-bottom:8px;margin-left:-15px;border-top:1px solid transparent;border-bottom:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1)}@media (min-width:768px){.navbar-form .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.navbar-form .form-control{display:inline-block;width:auto;vertical-align:middle}.navbar-form .form-control-static{display:inline-block}.navbar-form .input-group{display:inline-table;vertical-align:middle}.navbar-form .input-group .form-control,.navbar-form .input-group .input-group-addon,.navbar-form .input-group .input-group-btn{width:auto}.navbar-form .input-group>.form-control{width:100%}.navbar-form .control-label{margin-bottom:0;vertical-align:middle}.navbar-form .checkbox,.navbar-form .radio{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.navbar-form .checkbox label,.navbar-form .radio label{padding-left:0}.navbar-form .checkbox input[type=checkbox],.navbar-form .radio input[type=radio]{position:relative;margin-left:0}.navbar-form .has-feedback .form-control-feedback{top:0}}@media (max-width:767px){.navbar-form .form-group{margin-bottom:5px}.navbar-form .form-group:last-child{margin-bottom:0}}@media (min-width:768px){.navbar-form{width:auto;padding-top:0;padding-bottom:0;margin-right:0;margin-left:0;border:0;-webkit-box-shadow:none;box-shadow:none}}.navbar-nav>li>.dropdown-menu{margin-top:0;border-top-left-radius:0;border-top-right-radius:0}.navbar-fixed-bottom .navbar-nav>li>.dropdown-menu{margin-bottom:0;border-top-left-radius:4px;border-top-right-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0}.navbar-btn{margin-top:8px;margin-bottom:8px}.navbar-btn.btn-sm{margin-top:10px;margin-bottom:10px}.navbar-btn.btn-xs{margin-top:14px;margin-bottom:14px}.navbar-text{margin-top:15px;margin-bottom:15px}@media (min-width:768px){.navbar-text{float:left;margin-right:15px;margin-left:15px}}@media (min-width:768px){.navbar-left{float:left!important}.navbar-right{float:right!important;margin-right:-15px}.navbar-right~.navbar-right{margin-right:0}}.navbar-default{background-color:#f8f8f8;border-color:#e7e7e7}.navbar-default .navbar-brand{color:#777}.navbar-default .navbar-brand:focus,.navbar-default .navbar-brand:hover{color:#5e5e5e;background-color:transparent}.navbar-default .navbar-text{color:#777}.navbar-default .navbar-nav>li>a{color:#777}.navbar-default .navbar-nav>li>a:focus,.navbar-default .navbar-nav>li>a:hover{color:#333;background-color:transparent}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.active>a:focus,.navbar-default .navbar-nav>.active>a:hover{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav>.disabled>a,.navbar-default .navbar-nav>.disabled>a:focus,.navbar-default .navbar-nav>.disabled>a:hover{color:#ccc;background-color:transparent}.navbar-default .navbar-toggle{border-color:#ddd}.navbar-default .navbar-toggle:focus,.navbar-default .navbar-toggle:hover{background-color:#ddd}.navbar-default .navbar-toggle .icon-bar{background-color:#888}.navbar-default .navbar-collapse,.navbar-default .navbar-form{border-color:#e7e7e7}.navbar-default .navbar-nav>.open>a,.navbar-default .navbar-nav>.open>a:focus,.navbar-default .navbar-nav>.open>a:hover{color:#555;background-color:#e7e7e7}@media (max-width:767px){.navbar-default .navbar-nav .open .dropdown-menu>li>a{color:#777}.navbar-default .navbar-nav .open .dropdown-menu>li>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>li>a:hover{color:#333;background-color:transparent}.navbar-default .navbar-nav .open .dropdown-menu>.active>a,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:hover{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:hover{color:#ccc;background-color:transparent}}.navbar-default .navbar-link{color:#777}.navbar-default .navbar-link:hover{color:#333}.navbar-default .btn-link{color:#777}.navbar-default .btn-link:focus,.navbar-default .btn-link:hover{color:#333}.navbar-default .btn-link[disabled]:focus,.navbar-default .btn-link[disabled]:hover,fieldset[disabled] .navbar-default .btn-link:focus,fieldset[disabled] .navbar-default .btn-link:hover{color:#ccc}.navbar-inverse{background-color:#222;border-color:#080808}.navbar-inverse .navbar-brand{color:#9d9d9d}.navbar-inverse .navbar-brand:focus,.navbar-inverse .navbar-brand:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-text{color:#9d9d9d}.navbar-inverse .navbar-nav>li>a{color:#9d9d9d}.navbar-inverse .navbar-nav>li>a:focus,.navbar-inverse .navbar-nav>li>a:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.active>a:focus,.navbar-inverse .navbar-nav>.active>a:hover{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav>.disabled>a,.navbar-inverse .navbar-nav>.disabled>a:focus,.navbar-inverse .navbar-nav>.disabled>a:hover{color:#444;background-color:transparent}.navbar-inverse .navbar-toggle{border-color:#333}.navbar-inverse .navbar-toggle:focus,.navbar-inverse .navbar-toggle:hover{background-color:#333}.navbar-inverse .navbar-toggle .icon-bar{background-color:#fff}.navbar-inverse .navbar-collapse,.navbar-inverse .navbar-form{border-color:#101010}.navbar-inverse .navbar-nav>.open>a,.navbar-inverse .navbar-nav>.open>a:focus,.navbar-inverse .navbar-nav>.open>a:hover{color:#fff;background-color:#080808}@media (max-width:767px){.navbar-inverse .navbar-nav .open .dropdown-menu>.dropdown-header{border-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu .divider{background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a{color:#9d9d9d}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:hover{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:hover{color:#444;background-color:transparent}}.navbar-inverse .navbar-link{color:#9d9d9d}.navbar-inverse .navbar-link:hover{color:#fff}.navbar-inverse .btn-link{color:#9d9d9d}.navbar-inverse .btn-link:focus,.navbar-inverse .btn-link:hover{color:#fff}.navbar-inverse .btn-link[disabled]:focus,.navbar-inverse .btn-link[disabled]:hover,fieldset[disabled] .navbar-inverse .btn-link:focus,fieldset[disabled] .navbar-inverse .btn-link:hover{color:#444}.breadcrumb{padding:8px 15px;margin-bottom:20px;list-style:none;background-color:#f5f5f5;border-radius:4px}.breadcrumb>li{display:inline-block}.breadcrumb>li+li:before{padding:0 5px;color:#ccc;content:"/\00a0"}.breadcrumb>.active{color:#777}.pagination{display:inline-block;padding-left:0;margin:20px 0;border-radius:4px}.pagination>li{display:inline}.pagination>li>a,.pagination>li>span{position:relative;float:left;padding:6px 12px;margin-left:-1px;line-height:1.42857143;color:#337ab7;text-decoration:none;background-color:#fff;border:1px solid #ddd}.pagination>li:first-child>a,.pagination>li:first-child>span{margin-left:0;border-top-left-radius:4px;border-bottom-left-radius:4px}.pagination>li:last-child>a,.pagination>li:last-child>span{border-top-right-radius:4px;border-bottom-right-radius:4px}.pagination>li>a:focus,.pagination>li>a:hover,.pagination>li>span:focus,.pagination>li>span:hover{z-index:2;color:#23527c;background-color:#eee;border-color:#ddd}.pagination>.active>a,.pagination>.active>a:focus,.pagination>.active>a:hover,.pagination>.active>span,.pagination>.active>span:focus,.pagination>.active>span:hover{z-index:3;color:#fff;cursor:default;background-color:#337ab7;border-color:#337ab7}.pagination>.disabled>a,.pagination>.disabled>a:focus,.pagination>.disabled>a:hover,.pagination>.disabled>span,.pagination>.disabled>span:focus,.pagination>.disabled>span:hover{color:#777;cursor:not-allowed;background-color:#fff;border-color:#ddd}.pagination-lg>li>a,.pagination-lg>li>span{padding:10px 16px;font-size:18px;line-height:1.3333333}.pagination-lg>li:first-child>a,.pagination-lg>li:first-child>span{border-top-left-radius:6px;border-bottom-left-radius:6px}.pagination-lg>li:last-child>a,.pagination-lg>li:last-child>span{border-top-right-radius:6px;border-bottom-right-radius:6px}.pagination-sm>li>a,.pagination-sm>li>span{padding:5px 10px;font-size:12px;line-height:1.5}.pagination-sm>li:first-child>a,.pagination-sm>li:first-child>span{border-top-left-radius:3px;border-bottom-left-radius:3px}.pagination-sm>li:last-child>a,.pagination-sm>li:last-child>span{border-top-right-radius:3px;border-bottom-right-radius:3px}.pager{padding-left:0;margin:20px 0;text-align:center;list-style:none}.pager li{display:inline}.pager li>a,.pager li>span{display:inline-block;padding:5px 14px;background-color:#fff;border:1px solid #ddd;border-radius:15px}.pager li>a:focus,.pager li>a:hover{text-decoration:none;background-color:#eee}.pager .next>a,.pager .next>span{float:right}.pager .previous>a,.pager .previous>span{float:left}.pager .disabled>a,.pager .disabled>a:focus,.pager .disabled>a:hover,.pager .disabled>span{color:#777;cursor:not-allowed;background-color:#fff}.label{display:inline;padding:.2em .6em .3em;font-size:75%;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25em}a.label:focus,a.label:hover{color:#fff;text-decoration:none;cursor:pointer}.label:empty{display:none}.btn .label{position:relative;top:-1px}.label-default{background-color:#777}.label-default[href]:focus,.label-default[href]:hover{background-color:#5e5e5e}.label-primary{background-color:#337ab7}.label-primary[href]:focus,.label-primary[href]:hover{background-color:#286090}.label-success{background-color:#5cb85c}.label-success[href]:focus,.label-success[href]:hover{background-color:#449d44}.label-info{background-color:#5bc0de}.label-info[href]:focus,.label-info[href]:hover{background-color:#31b0d5}.label-warning{background-color:#f0ad4e}.label-warning[href]:focus,.label-warning[href]:hover{background-color:#ec971f}.label-danger{background-color:#d9534f}.label-danger[href]:focus,.label-danger[href]:hover{background-color:#c9302c}.badge{display:inline-block;min-width:10px;padding:3px 7px;font-size:12px;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:middle;background-color:#777;border-radius:10px}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.btn-group-xs>.btn .badge,.btn-xs .badge{top:0;padding:1px 5px}a.badge:focus,a.badge:hover{color:#fff;text-decoration:none;cursor:pointer}.list-group-item.active>.badge,.nav-pills>.active>a>.badge{color:#337ab7;background-color:#fff}.list-group-item>.badge{float:right}.list-group-item>.badge+.badge{margin-right:5px}.nav-pills>li>a>.badge{margin-left:3px}.jumbotron{padding-top:30px;padding-bottom:30px;margin-bottom:30px;color:inherit;background-color:#eee}.jumbotron .h1,.jumbotron h1{color:inherit}.jumbotron p{margin-bottom:15px;font-size:21px;font-weight:200}.jumbotron>hr{border-top-color:#d5d5d5}.container .jumbotron,.container-fluid .jumbotron{padding-right:15px;padding-left:15px;border-radius:6px}.jumbotron .container{max-width:100%}@media screen and (min-width:768px){.jumbotron{padding-top:48px;padding-bottom:48px}.container .jumbotron,.container-fluid .jumbotron{padding-right:60px;padding-left:60px}.jumbotron .h1,.jumbotron h1{font-size:63px}}.thumbnail{display:block;padding:4px;margin-bottom:20px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:border .2s ease-in-out;-o-transition:border .2s ease-in-out;transition:border .2s ease-in-out}.thumbnail a>img,.thumbnail>img{margin-right:auto;margin-left:auto}a.thumbnail.active,a.thumbnail:focus,a.thumbnail:hover{border-color:#337ab7}.thumbnail .caption{padding:9px;color:#333}.alert{padding:15px;margin-bottom:20px;border:1px solid transparent;border-radius:4px}.alert h4{margin-top:0;color:inherit}.alert .alert-link{font-weight:700}.alert>p,.alert>ul{margin-bottom:0}.alert>p+p{margin-top:5px}.alert-dismissable,.alert-dismissible{padding-right:35px}.alert-dismissable .close,.alert-dismissible .close{position:relative;top:-2px;right:-21px;color:inherit}.alert-success{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.alert-success hr{border-top-color:#c9e2b3}.alert-success .alert-link{color:#2b542c}.alert-info{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.alert-info hr{border-top-color:#a6e1ec}.alert-info .alert-link{color:#245269}.alert-warning{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.alert-warning hr{border-top-color:#f7e1b5}.alert-warning .alert-link{color:#66512c}.alert-danger{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.alert-danger hr{border-top-color:#e4b9c0}.alert-danger .alert-link{color:#843534}@-webkit-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@-o-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}.progress{height:20px;margin-bottom:20px;overflow:hidden;background-color:#f5f5f5;border-radius:4px;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,.1);box-shadow:inset 0 1px 2px rgba(0,0,0,.1)}.progress-bar{float:left;width:0;height:100%;font-size:12px;line-height:20px;color:#fff;text-align:center;background-color:#337ab7;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);-webkit-transition:width .6s ease;-o-transition:width .6s ease;transition:width .6s ease}.progress-bar-striped,.progress-striped .progress-bar{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);-webkit-background-size:40px 40px;background-size:40px 40px}.progress-bar.active,.progress.active .progress-bar{-webkit-animation:progress-bar-stripes 2s linear infinite;-o-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite}.progress-bar-success{background-color:#5cb85c}.progress-striped .progress-bar-success{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-info{background-color:#5bc0de}.progress-striped .progress-bar-info{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-warning{background-color:#f0ad4e}.progress-striped .progress-bar-warning{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-danger{background-color:#d9534f}.progress-striped .progress-bar-danger{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.media{margin-top:15px}.media:first-child{margin-top:0}.media,.media-body{overflow:hidden;zoom:1}.media-body{width:10000px}.media-object{display:block}.media-object.img-thumbnail{max-width:none}.media-right,.media>.pull-right{padding-left:10px}.media-left,.media>.pull-left{padding-right:10px}.media-body,.media-left,.media-right{display:table-cell;vertical-align:top}.media-middle{vertical-align:middle}.media-bottom{vertical-align:bottom}.media-heading{margin-top:0;margin-bottom:5px}.media-list{padding-left:0;list-style:none}.list-group{padding-left:0;margin-bottom:20px}.list-group-item{position:relative;display:block;padding:10px 15px;margin-bottom:-1px;background-color:#fff;border:1px solid #ddd}.list-group-item:first-child{border-top-left-radius:4px;border-top-right-radius:4px}.list-group-item:last-child{margin-bottom:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px}a.list-group-item,button.list-group-item{color:#555}a.list-group-item .list-group-item-heading,button.list-group-item .list-group-item-heading{color:#333}a.list-group-item:focus,a.list-group-item:hover,button.list-group-item:focus,button.list-group-item:hover{color:#555;text-decoration:none;background-color:#f5f5f5}button.list-group-item{width:100%;text-align:left}.list-group-item.disabled,.list-group-item.disabled:focus,.list-group-item.disabled:hover{color:#777;cursor:not-allowed;background-color:#eee}.list-group-item.disabled .list-group-item-heading,.list-group-item.disabled:focus .list-group-item-heading,.list-group-item.disabled:hover .list-group-item-heading{color:inherit}.list-group-item.disabled .list-group-item-text,.list-group-item.disabled:focus .list-group-item-text,.list-group-item.disabled:hover .list-group-item-text{color:#777}.list-group-item.active,.list-group-item.active:focus,.list-group-item.active:hover{z-index:2;color:#fff;background-color:#337ab7;border-color:#337ab7}.list-group-item.active .list-group-item-heading,.list-group-item.active .list-group-item-heading>.small,.list-group-item.active .list-group-item-heading>small,.list-group-item.active:focus .list-group-item-heading,.list-group-item.active:focus .list-group-item-heading>.small,.list-group-item.active:focus .list-group-item-heading>small,.list-group-item.active:hover .list-group-item-heading,.list-group-item.active:hover .list-group-item-heading>.small,.list-group-item.active:hover .list-group-item-heading>small{color:inherit}.list-group-item.active .list-group-item-text,.list-group-item.active:focus .list-group-item-text,.list-group-item.active:hover .list-group-item-text{color:#c7ddef}.list-group-item-success{color:#3c763d;background-color:#dff0d8}a.list-group-item-success,button.list-group-item-success{color:#3c763d}a.list-group-item-success .list-group-item-heading,button.list-group-item-success .list-group-item-heading{color:inherit}a.list-group-item-success:focus,a.list-group-item-success:hover,button.list-group-item-success:focus,button.list-group-item-success:hover{color:#3c763d;background-color:#d0e9c6}a.list-group-item-success.active,a.list-group-item-success.active:focus,a.list-group-item-success.active:hover,button.list-group-item-success.active,button.list-group-item-success.active:focus,button.list-group-item-success.active:hover{color:#fff;background-color:#3c763d;border-color:#3c763d}.list-group-item-info{color:#31708f;background-color:#d9edf7}a.list-group-item-info,button.list-group-item-info{color:#31708f}a.list-group-item-info .list-group-item-heading,button.list-group-item-info .list-group-item-heading{color:inherit}a.list-group-item-info:focus,a.list-group-item-info:hover,button.list-group-item-info:focus,button.list-group-item-info:hover{color:#31708f;background-color:#c4e3f3}a.list-group-item-info.active,a.list-group-item-info.active:focus,a.list-group-item-info.active:hover,button.list-group-item-info.active,button.list-group-item-info.active:focus,button.list-group-item-info.active:hover{color:#fff;background-color:#31708f;border-color:#31708f}.list-group-item-warning{color:#8a6d3b;background-color:#fcf8e3}a.list-group-item-warning,button.list-group-item-warning{color:#8a6d3b}a.list-group-item-warning .list-group-item-heading,button.list-group-item-warning .list-group-item-heading{color:inherit}a.list-group-item-warning:focus,a.list-group-item-warning:hover,button.list-group-item-warning:focus,button.list-group-item-warning:hover{color:#8a6d3b;background-color:#faf2cc}a.list-group-item-warning.active,a.list-group-item-warning.active:focus,a.list-group-item-warning.active:hover,button.list-group-item-warning.active,button.list-group-item-warning.active:focus,button.list-group-item-warning.active:hover{color:#fff;background-color:#8a6d3b;border-color:#8a6d3b}.list-group-item-danger{color:#a94442;background-color:#f2dede}a.list-group-item-danger,button.list-group-item-danger{color:#a94442}a.list-group-item-danger .list-group-item-heading,button.list-group-item-danger .list-group-item-heading{color:inherit}a.list-group-item-danger:focus,a.list-group-item-danger:hover,button.list-group-item-danger:focus,button.list-group-item-danger:hover{color:#a94442;background-color:#ebcccc}a.list-group-item-danger.active,a.list-group-item-danger.active:focus,a.list-group-item-danger.active:hover,button.list-group-item-danger.active,button.list-group-item-danger.active:focus,button.list-group-item-danger.active:hover{color:#fff;background-color:#a94442;border-color:#a94442}.list-group-item-heading{margin-top:0;margin-bottom:5px}.list-group-item-text{margin-bottom:0;line-height:1.3}.panel{margin-bottom:20px;background-color:#fff;border:1px solid transparent;border-radius:4px;-webkit-box-shadow:0 1px 1px rgba(0,0,0,.05);box-shadow:0 1px 1px rgba(0,0,0,.05)}.panel-body{padding:15px}.panel-heading{padding:10px 15px;border-bottom:1px solid transparent;border-top-left-radius:3px;border-top-right-radius:3px}.panel-heading>.dropdown .dropdown-toggle{color:inherit}.panel-title{margin-top:0;margin-bottom:0;font-size:16px;color:inherit}.panel-title>.small,.panel-title>.small>a,.panel-title>a,.panel-title>small,.panel-title>small>a{color:inherit}.panel-footer{padding:10px 15px;background-color:#f5f5f5;border-top:1px solid #ddd;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.list-group,.panel>.panel-collapse>.list-group{margin-bottom:0}.panel>.list-group .list-group-item,.panel>.panel-collapse>.list-group .list-group-item{border-width:1px 0;border-radius:0}.panel>.list-group:first-child .list-group-item:first-child,.panel>.panel-collapse>.list-group:first-child .list-group-item:first-child{border-top:0;border-top-left-radius:3px;border-top-right-radius:3px}.panel>.list-group:last-child .list-group-item:last-child,.panel>.panel-collapse>.list-group:last-child .list-group-item:last-child{border-bottom:0;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.panel-heading+.panel-collapse>.list-group .list-group-item:first-child{border-top-left-radius:0;border-top-right-radius:0}.panel-heading+.list-group .list-group-item:first-child{border-top-width:0}.list-group+.panel-footer{border-top-width:0}.panel>.panel-collapse>.table,.panel>.table,.panel>.table-responsive>.table{margin-bottom:0}.panel>.panel-collapse>.table caption,.panel>.table caption,.panel>.table-responsive>.table caption{padding-right:15px;padding-left:15px}.panel>.table-responsive:first-child>.table:first-child,.panel>.table:first-child{border-top-left-radius:3px;border-top-right-radius:3px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child,.panel>.table:first-child>thead:first-child>tr:first-child{border-top-left-radius:3px;border-top-right-radius:3px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table:first-child>thead:first-child>tr:first-child th:first-child{border-top-left-radius:3px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table:first-child>thead:first-child>tr:first-child th:last-child{border-top-right-radius:3px}.panel>.table-responsive:last-child>.table:last-child,.panel>.table:last-child{border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child{border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:first-child{border-bottom-left-radius:3px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:last-child{border-bottom-right-radius:3px}.panel>.panel-body+.table,.panel>.panel-body+.table-responsive,.panel>.table+.panel-body,.panel>.table-responsive+.panel-body{border-top:1px solid #ddd}.panel>.table>tbody:first-child>tr:first-child td,.panel>.table>tbody:first-child>tr:first-child th{border-top:0}.panel>.table-bordered,.panel>.table-responsive>.table-bordered{border:0}.panel>.table-bordered>tbody>tr>td:first-child,.panel>.table-bordered>tbody>tr>th:first-child,.panel>.table-bordered>tfoot>tr>td:first-child,.panel>.table-bordered>tfoot>tr>th:first-child,.panel>.table-bordered>thead>tr>td:first-child,.panel>.table-bordered>thead>tr>th:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:first-child,.panel>.table-responsive>.table-bordered>thead>tr>td:first-child,.panel>.table-responsive>.table-bordered>thead>tr>th:first-child{border-left:0}.panel>.table-bordered>tbody>tr>td:last-child,.panel>.table-bordered>tbody>tr>th:last-child,.panel>.table-bordered>tfoot>tr>td:last-child,.panel>.table-bordered>tfoot>tr>th:last-child,.panel>.table-bordered>thead>tr>td:last-child,.panel>.table-bordered>thead>tr>th:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:last-child,.panel>.table-responsive>.table-bordered>thead>tr>td:last-child,.panel>.table-responsive>.table-bordered>thead>tr>th:last-child{border-right:0}.panel>.table-bordered>tbody>tr:first-child>td,.panel>.table-bordered>tbody>tr:first-child>th,.panel>.table-bordered>thead>tr:first-child>td,.panel>.table-bordered>thead>tr:first-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>th,.panel>.table-responsive>.table-bordered>thead>tr:first-child>td,.panel>.table-responsive>.table-bordered>thead>tr:first-child>th{border-bottom:0}.panel>.table-bordered>tbody>tr:last-child>td,.panel>.table-bordered>tbody>tr:last-child>th,.panel>.table-bordered>tfoot>tr:last-child>td,.panel>.table-bordered>tfoot>tr:last-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>th,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>td,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>th{border-bottom:0}.panel>.table-responsive{margin-bottom:0;border:0}.panel-group{margin-bottom:20px}.panel-group .panel{margin-bottom:0;border-radius:4px}.panel-group .panel+.panel{margin-top:5px}.panel-group .panel-heading{border-bottom:0}.panel-group .panel-heading+.panel-collapse>.list-group,.panel-group .panel-heading+.panel-collapse>.panel-body{border-top:1px solid #ddd}.panel-group .panel-footer{border-top:0}.panel-group .panel-footer+.panel-collapse .panel-body{border-bottom:1px solid #ddd}.panel-default{border-color:#ddd}.panel-default>.panel-heading{color:#333;background-color:#f5f5f5;border-color:#ddd}.panel-default>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ddd}.panel-default>.panel-heading .badge{color:#f5f5f5;background-color:#333}.panel-default>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ddd}.panel-primary{border-color:#337ab7}.panel-primary>.panel-heading{color:#fff;background-color:#337ab7;border-color:#337ab7}.panel-primary>.panel-heading+.panel-collapse>.panel-body{border-top-color:#337ab7}.panel-primary>.panel-heading .badge{color:#337ab7;background-color:#fff}.panel-primary>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#337ab7}.panel-success{border-color:#d6e9c6}.panel-success>.panel-heading{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.panel-success>.panel-heading+.panel-collapse>.panel-body{border-top-color:#d6e9c6}.panel-success>.panel-heading .badge{color:#dff0d8;background-color:#3c763d}.panel-success>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#d6e9c6}.panel-info{border-color:#bce8f1}.panel-info>.panel-heading{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.panel-info>.panel-heading+.panel-collapse>.panel-body{border-top-color:#bce8f1}.panel-info>.panel-heading .badge{color:#d9edf7;background-color:#31708f}.panel-info>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#bce8f1}.panel-warning{border-color:#faebcc}.panel-warning>.panel-heading{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.panel-warning>.panel-heading+.panel-collapse>.panel-body{border-top-color:#faebcc}.panel-warning>.panel-heading .badge{color:#fcf8e3;background-color:#8a6d3b}.panel-warning>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#faebcc}.panel-danger{border-color:#ebccd1}.panel-danger>.panel-heading{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.panel-danger>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ebccd1}.panel-danger>.panel-heading .badge{color:#f2dede;background-color:#a94442}.panel-danger>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ebccd1}.embed-responsive{position:relative;display:block;height:0;padding:0;overflow:hidden}.embed-responsive .embed-responsive-item,.embed-responsive embed,.embed-responsive iframe,.embed-responsive object,.embed-responsive video{position:absolute;top:0;bottom:0;left:0;width:100%;height:100%;border:0}.embed-responsive-16by9{padding-bottom:56.25%}.embed-responsive-4by3{padding-bottom:75%}.well{min-height:20px;padding:19px;margin-bottom:20px;background-color:#f5f5f5;border:1px solid #e3e3e3;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.05);box-shadow:inset 0 1px 1px rgba(0,0,0,.05)}.well blockquote{border-color:#ddd;border-color:rgba(0,0,0,.15)}.well-lg{padding:24px;border-radius:6px}.well-sm{padding:9px;border-radius:3px}.close{float:right;font-size:21px;font-weight:700;line-height:1;color:#000;text-shadow:0 1px 0 #fff;filter:alpha(opacity=20);opacity:.2}.close:focus,.close:hover{color:#000;text-decoration:none;cursor:pointer;filter:alpha(opacity=50);opacity:.5}button.close{-webkit-appearance:none;padding:0;cursor:pointer;background:0 0;border:0}.modal-open{overflow:hidden}.modal{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1050;display:none;overflow:hidden;-webkit-overflow-scrolling:touch;outline:0}.modal.fade .modal-dialog{-webkit-transition:-webkit-transform .3s ease-out;-o-transition:-o-transform .3s ease-out;transition:transform .3s ease-out;-webkit-transform:translate(0,-25%);-ms-transform:translate(0,-25%);-o-transform:translate(0,-25%);transform:translate(0,-25%)}.modal.in .modal-dialog{-webkit-transform:translate(0,0);-ms-transform:translate(0,0);-o-transform:translate(0,0);transform:translate(0,0)}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal-dialog{position:relative;width:auto;margin:10px}.modal-content{position:relative;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid #999;border:1px solid rgba(0,0,0,.2);border-radius:6px;outline:0;-webkit-box-shadow:0 3px 9px rgba(0,0,0,.5);box-shadow:0 3px 9px rgba(0,0,0,.5)}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000}.modal-backdrop.fade{filter:alpha(opacity=0);opacity:0}.modal-backdrop.in{filter:alpha(opacity=50);opacity:.5}.modal-header{padding:15px;border-bottom:1px solid #e5e5e5}.modal-header .close{margin-top:-2px}.modal-title{margin:0;line-height:1.42857143}.modal-body{position:relative;padding:15px}.modal-footer{padding:15px;text-align:right;border-top:1px solid #e5e5e5}.modal-footer .btn+.btn{margin-bottom:0;margin-left:5px}.modal-footer .btn-group .btn+.btn{margin-left:-1px}.modal-footer .btn-block+.btn-block{margin-left:0}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width:768px){.modal-dialog{width:600px;margin:30px auto}.modal-content{-webkit-box-shadow:0 5px 15px rgba(0,0,0,.5);box-shadow:0 5px 15px rgba(0,0,0,.5)}.modal-sm{width:300px}}@media (min-width:992px){.modal-lg{width:900px}}.tooltip{position:absolute;z-index:1070;display:block;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:12px;font-style:normal;font-weight:400;line-height:1.42857143;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;word-wrap:normal;white-space:normal;filter:alpha(opacity=0);opacity:0;line-break:auto}.tooltip.in{filter:alpha(opacity=90);opacity:.9}.tooltip.top{padding:5px 0;margin-top:-3px}.tooltip.right{padding:0 5px;margin-left:3px}.tooltip.bottom{padding:5px 0;margin-top:3px}.tooltip.left{padding:0 5px;margin-left:-3px}.tooltip-inner{max-width:200px;padding:3px 8px;color:#fff;text-align:center;background-color:#000;border-radius:4px}.tooltip-arrow{position:absolute;width:0;height:0;border-color:transparent;border-style:solid}.tooltip.top .tooltip-arrow{bottom:0;left:50%;margin-left:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-left .tooltip-arrow{right:5px;bottom:0;margin-bottom:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-right .tooltip-arrow{bottom:0;left:5px;margin-bottom:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.right .tooltip-arrow{top:50%;left:0;margin-top:-5px;border-width:5px 5px 5px 0;border-right-color:#000}.tooltip.left .tooltip-arrow{top:50%;right:0;margin-top:-5px;border-width:5px 0 5px 5px;border-left-color:#000}.tooltip.bottom .tooltip-arrow{top:0;left:50%;margin-left:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-left .tooltip-arrow{top:0;right:5px;margin-top:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-right .tooltip-arrow{top:0;left:5px;margin-top:-5px;border-width:0 5px 5px;border-bottom-color:#000}.popover{position:absolute;top:0;left:0;z-index:1060;display:none;max-width:276px;padding:1px;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;font-style:normal;font-weight:400;line-height:1.42857143;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;word-wrap:normal;white-space:normal;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid #ccc;border:1px solid rgba(0,0,0,.2);border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,.2);box-shadow:0 5px 10px rgba(0,0,0,.2);line-break:auto}.popover.top{margin-top:-10px}.popover.right{margin-left:10px}.popover.bottom{margin-top:10px}.popover.left{margin-left:-10px}.popover-title{padding:8px 14px;margin:0;font-size:14px;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-radius:5px 5px 0 0}.popover-content{padding:9px 14px}.popover>.arrow,.popover>.arrow:after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.popover>.arrow{border-width:11px}.popover>.arrow:after{content:"";border-width:10px}.popover.top>.arrow{bottom:-11px;left:50%;margin-left:-11px;border-top-color:#999;border-top-color:rgba(0,0,0,.25);border-bottom-width:0}.popover.top>.arrow:after{bottom:1px;margin-left:-10px;content:" ";border-top-color:#fff;border-bottom-width:0}.popover.right>.arrow{top:50%;left:-11px;margin-top:-11px;border-right-color:#999;border-right-color:rgba(0,0,0,.25);border-left-width:0}.popover.right>.arrow:after{bottom:-10px;left:1px;content:" ";border-right-color:#fff;border-left-width:0}.popover.bottom>.arrow{top:-11px;left:50%;margin-left:-11px;border-top-width:0;border-bottom-color:#999;border-bottom-color:rgba(0,0,0,.25)}.popover.bottom>.arrow:after{top:1px;margin-left:-10px;content:" ";border-top-width:0;border-bottom-color:#fff}.popover.left>.arrow{top:50%;right:-11px;margin-top:-11px;border-right-width:0;border-left-color:#999;border-left-color:rgba(0,0,0,.25)}.popover.left>.arrow:after{right:1px;bottom:-10px;content:" ";border-right-width:0;border-left-color:#fff}.carousel{position:relative}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner>.item{position:relative;display:none;-webkit-transition:.6s ease-in-out left;-o-transition:.6s ease-in-out left;transition:.6s ease-in-out left}.carousel-inner>.item>a>img,.carousel-inner>.item>img{line-height:1}@media all and (transform-3d),(-webkit-transform-3d){.carousel-inner>.item{-webkit-transition:-webkit-transform .6s ease-in-out;-o-transition:-o-transform .6s ease-in-out;transition:transform .6s ease-in-out;-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-perspective:1000px;perspective:1000px}.carousel-inner>.item.active.right,.carousel-inner>.item.next{left:0;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}.carousel-inner>.item.active.left,.carousel-inner>.item.prev{left:0;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}.carousel-inner>.item.active,.carousel-inner>.item.next.left,.carousel-inner>.item.prev.right{left:0;-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}}.carousel-inner>.active,.carousel-inner>.next,.carousel-inner>.prev{display:block}.carousel-inner>.active{left:0}.carousel-inner>.next,.carousel-inner>.prev{position:absolute;top:0;width:100%}.carousel-inner>.next{left:100%}.carousel-inner>.prev{left:-100%}.carousel-inner>.next.left,.carousel-inner>.prev.right{left:0}.carousel-inner>.active.left{left:-100%}.carousel-inner>.active.right{left:100%}.carousel-control{position:absolute;top:0;bottom:0;left:0;width:15%;font-size:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,.6);background-color:rgba(0,0,0,0);filter:alpha(opacity=50);opacity:.5}.carousel-control.left{background-image:-webkit-linear-gradient(left,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);background-image:-o-linear-gradient(left,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);background-image:-webkit-gradient(linear,left top,right top,from(rgba(0,0,0,.5)),to(rgba(0,0,0,.0001)));background-image:linear-gradient(to right,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1);background-repeat:repeat-x}.carousel-control.right{right:0;left:auto;background-image:-webkit-linear-gradient(left,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);background-image:-o-linear-gradient(left,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);background-image:-webkit-gradient(linear,left top,right top,from(rgba(0,0,0,.0001)),to(rgba(0,0,0,.5)));background-image:linear-gradient(to right,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1);background-repeat:repeat-x}.carousel-control:focus,.carousel-control:hover{color:#fff;text-decoration:none;filter:alpha(opacity=90);outline:0;opacity:.9}.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next,.carousel-control .icon-prev{position:absolute;top:50%;z-index:5;display:inline-block;margin-top:-10px}.carousel-control .glyphicon-chevron-left,.carousel-control .icon-prev{left:50%;margin-left:-10px}.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next{right:50%;margin-right:-10px}.carousel-control .icon-next,.carousel-control .icon-prev{width:20px;height:20px;font-family:serif;line-height:1}.carousel-control .icon-prev:before{content:'\2039'}.carousel-control .icon-next:before{content:'\203a'}.carousel-indicators{position:absolute;bottom:10px;left:50%;z-index:15;width:60%;padding-left:0;margin-left:-30%;text-align:center;list-style:none}.carousel-indicators li{display:inline-block;width:10px;height:10px;margin:1px;text-indent:-999px;cursor:pointer;background-color:#000\9;background-color:rgba(0,0,0,0);border:1px solid #fff;border-radius:10px}.carousel-indicators .active{width:12px;height:12px;margin:0;background-color:#fff}.carousel-caption{position:absolute;right:15%;bottom:20px;left:15%;z-index:10;padding-top:20px;padding-bottom:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,.6)}.carousel-caption .btn{text-shadow:none}@media screen and (min-width:768px){.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next,.carousel-control .icon-prev{width:30px;height:30px;margin-top:-10px;font-size:30px}.carousel-control .glyphicon-chevron-left,.carousel-control .icon-prev{margin-left:-10px}.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next{margin-right:-10px}.carousel-caption{right:20%;left:20%;padding-bottom:30px}.carousel-indicators{bottom:20px}}.btn-group-vertical>.btn-group:after,.btn-group-vertical>.btn-group:before,.btn-toolbar:after,.btn-toolbar:before,.clearfix:after,.clearfix:before,.container-fluid:after,.container-fluid:before,.container:after,.container:before,.dl-horizontal dd:after,.dl-horizontal dd:before,.form-horizontal .form-group:after,.form-horizontal .form-group:before,.modal-footer:after,.modal-footer:before,.modal-header:after,.modal-header:before,.nav:after,.nav:before,.navbar-collapse:after,.navbar-collapse:before,.navbar-header:after,.navbar-header:before,.navbar:after,.navbar:before,.pager:after,.pager:before,.panel-body:after,.panel-body:before,.row:after,.row:before{display:table;content:" "}.btn-group-vertical>.btn-group:after,.btn-toolbar:after,.clearfix:after,.container-fluid:after,.container:after,.dl-horizontal dd:after,.form-horizontal .form-group:after,.modal-footer:after,.modal-header:after,.nav:after,.navbar-collapse:after,.navbar-header:after,.navbar:after,.pager:after,.panel-body:after,.row:after{clear:both}.center-block{display:block;margin-right:auto;margin-left:auto}.pull-right{float:right!important}.pull-left{float:left!important}.hide{display:none!important}.show{display:block!important}.invisible{visibility:hidden}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.hidden{display:none!important}.affix{position:fixed}@-ms-viewport{width:device-width}.visible-lg,.visible-md,.visible-sm,.visible-xs{display:none!important}.visible-lg-block,.visible-lg-inline,.visible-lg-inline-block,.visible-md-block,.visible-md-inline,.visible-md-inline-block,.visible-sm-block,.visible-sm-inline,.visible-sm-inline-block,.visible-xs-block,.visible-xs-inline,.visible-xs-inline-block{display:none!important}@media (max-width:767px){.visible-xs{display:block!important}table.visible-xs{display:table!important}tr.visible-xs{display:table-row!important}td.visible-xs,th.visible-xs{display:table-cell!important}}@media (max-width:767px){.visible-xs-block{display:block!important}}@media (max-width:767px){.visible-xs-inline{display:inline!important}}@media (max-width:767px){.visible-xs-inline-block{display:inline-block!important}}@media (min-width:768px) and (max-width:991px){.visible-sm{display:block!important}table.visible-sm{display:table!important}tr.visible-sm{display:table-row!important}td.visible-sm,th.visible-sm{display:table-cell!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-block{display:block!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-inline{display:inline!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-inline-block{display:inline-block!important}}@media (min-width:992px) and (max-width:1199px){.visible-md{display:block!important}table.visible-md{display:table!important}tr.visible-md{display:table-row!important}td.visible-md,th.visible-md{display:table-cell!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-block{display:block!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-inline{display:inline!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-inline-block{display:inline-block!important}}@media (min-width:1200px){.visible-lg{display:block!important}table.visible-lg{display:table!important}tr.visible-lg{display:table-row!important}td.visible-lg,th.visible-lg{display:table-cell!important}}@media (min-width:1200px){.visible-lg-block{display:block!important}}@media (min-width:1200px){.visible-lg-inline{display:inline!important}}@media (min-width:1200px){.visible-lg-inline-block{display:inline-block!important}}@media (max-width:767px){.hidden-xs{display:none!important}}@media (min-width:768px) and (max-width:991px){.hidden-sm{display:none!important}}@media (min-width:992px) and (max-width:1199px){.hidden-md{display:none!important}}@media (min-width:1200px){.hidden-lg{display:none!important}}.visible-print{display:none!important}@media print{.visible-print{display:block!important}table.visible-print{display:table!important}tr.visible-print{display:table-row!important}td.visible-print,th.visible-print{display:table-cell!important}}.visible-print-block{display:none!important}@media print{.visible-print-block{display:block!important}}.visible-print-inline{display:none!important}@media print{.visible-print-inline{display:inline!important}}.visible-print-inline-block{display:none!important}@media print{.visible-print-inline-block{display:inline-block!important}}@media print{.hidden-print{display:none!important}}
+/*# sourceMappingURL=bootstrap.min.css.map */
\ No newline at end of file
diff --git a/themes/bootstrap3/css/vendor/font-awesome.min.css b/themes/bootstrap3/css/vendor/font-awesome.min.css
index ec53d4d6d5bf13db8ca757ea991a9c7839f23c89..9b27f8ea8f8da544b622a801c4a47f73e3865929 100644
--- a/themes/bootstrap3/css/vendor/font-awesome.min.css
+++ b/themes/bootstrap3/css/vendor/font-awesome.min.css
@@ -1,4 +1,4 @@
 /*!
- *  Font Awesome 4.2.0 by @davegandy - http://fontawesome.io - @fontawesome
+ *  Font Awesome 4.6.3 by @davegandy - http://fontawesome.io - @fontawesome
  *  License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License)
- */@font-face{font-family:'FontAwesome';src:url('../fonts/fontawesome-webfont.eot?v=4.2.0');src:url('../fonts/fontawesome-webfont.eot?#iefix&v=4.2.0') format('embedded-opentype'),url('../fonts/fontawesome-webfont.woff?v=4.2.0') format('woff'),url('../fonts/fontawesome-webfont.ttf?v=4.2.0') format('truetype'),url('../fonts/fontawesome-webfont.svg?v=4.2.0#fontawesomeregular') format('svg');font-weight:normal;font-style:normal}.fa{display:inline-block;font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.33333333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.28571429em;text-align:center}.fa-ul{padding-left:0;margin-left:2.14285714em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.14285714em;width:2.14285714em;top:.14285714em;text-align:center}.fa-li.fa-lg{left:-1.85714286em}.fa-border{padding:.2em .25em .15em;border:solid .08em #eee;border-radius:.1em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left{margin-right:.3em}.fa.pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s infinite linear;animation:fa-spin 2s infinite linear}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=1);-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=2);-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=3);-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1);-webkit-transform:scale(-1, 1);-ms-transform:scale(-1, 1);transform:scale(-1, 1)}.fa-flip-vertical{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1);-webkit-transform:scale(1, -1);-ms-transform:scale(1, -1);transform:scale(1, -1)}:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270,:root .fa-flip-horizontal,:root .fa-flip-vertical{filter:none}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:"\f000"}.fa-music:before{content:"\f001"}.fa-search:before{content:"\f002"}.fa-envelope-o:before{content:"\f003"}.fa-heart:before{content:"\f004"}.fa-star:before{content:"\f005"}.fa-star-o:before{content:"\f006"}.fa-user:before{content:"\f007"}.fa-film:before{content:"\f008"}.fa-th-large:before{content:"\f009"}.fa-th:before{content:"\f00a"}.fa-th-list:before{content:"\f00b"}.fa-check:before{content:"\f00c"}.fa-remove:before,.fa-close:before,.fa-times:before{content:"\f00d"}.fa-search-plus:before{content:"\f00e"}.fa-search-minus:before{content:"\f010"}.fa-power-off:before{content:"\f011"}.fa-signal:before{content:"\f012"}.fa-gear:before,.fa-cog:before{content:"\f013"}.fa-trash-o:before{content:"\f014"}.fa-home:before{content:"\f015"}.fa-file-o:before{content:"\f016"}.fa-clock-o:before{content:"\f017"}.fa-road:before{content:"\f018"}.fa-download:before{content:"\f019"}.fa-arrow-circle-o-down:before{content:"\f01a"}.fa-arrow-circle-o-up:before{content:"\f01b"}.fa-inbox:before{content:"\f01c"}.fa-play-circle-o:before{content:"\f01d"}.fa-rotate-right:before,.fa-repeat:before{content:"\f01e"}.fa-refresh:before{content:"\f021"}.fa-list-alt:before{content:"\f022"}.fa-lock:before{content:"\f023"}.fa-flag:before{content:"\f024"}.fa-headphones:before{content:"\f025"}.fa-volume-off:before{content:"\f026"}.fa-volume-down:before{content:"\f027"}.fa-volume-up:before{content:"\f028"}.fa-qrcode:before{content:"\f029"}.fa-barcode:before{content:"\f02a"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-book:before{content:"\f02d"}.fa-bookmark:before{content:"\f02e"}.fa-print:before{content:"\f02f"}.fa-camera:before{content:"\f030"}.fa-font:before{content:"\f031"}.fa-bold:before{content:"\f032"}.fa-italic:before{content:"\f033"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-align-left:before{content:"\f036"}.fa-align-center:before{content:"\f037"}.fa-align-right:before{content:"\f038"}.fa-align-justify:before{content:"\f039"}.fa-list:before{content:"\f03a"}.fa-dedent:before,.fa-outdent:before{content:"\f03b"}.fa-indent:before{content:"\f03c"}.fa-video-camera:before{content:"\f03d"}.fa-photo:before,.fa-image:before,.fa-picture-o:before{content:"\f03e"}.fa-pencil:before{content:"\f040"}.fa-map-marker:before{content:"\f041"}.fa-adjust:before{content:"\f042"}.fa-tint:before{content:"\f043"}.fa-edit:before,.fa-pencil-square-o:before{content:"\f044"}.fa-share-square-o:before{content:"\f045"}.fa-check-square-o:before{content:"\f046"}.fa-arrows:before{content:"\f047"}.fa-step-backward:before{content:"\f048"}.fa-fast-backward:before{content:"\f049"}.fa-backward:before{content:"\f04a"}.fa-play:before{content:"\f04b"}.fa-pause:before{content:"\f04c"}.fa-stop:before{content:"\f04d"}.fa-forward:before{content:"\f04e"}.fa-fast-forward:before{content:"\f050"}.fa-step-forward:before{content:"\f051"}.fa-eject:before{content:"\f052"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-plus-circle:before{content:"\f055"}.fa-minus-circle:before{content:"\f056"}.fa-times-circle:before{content:"\f057"}.fa-check-circle:before{content:"\f058"}.fa-question-circle:before{content:"\f059"}.fa-info-circle:before{content:"\f05a"}.fa-crosshairs:before{content:"\f05b"}.fa-times-circle-o:before{content:"\f05c"}.fa-check-circle-o:before{content:"\f05d"}.fa-ban:before{content:"\f05e"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrow-down:before{content:"\f063"}.fa-mail-forward:before,.fa-share:before{content:"\f064"}.fa-expand:before{content:"\f065"}.fa-compress:before{content:"\f066"}.fa-plus:before{content:"\f067"}.fa-minus:before{content:"\f068"}.fa-asterisk:before{content:"\f069"}.fa-exclamation-circle:before{content:"\f06a"}.fa-gift:before{content:"\f06b"}.fa-leaf:before{content:"\f06c"}.fa-fire:before{content:"\f06d"}.fa-eye:before{content:"\f06e"}.fa-eye-slash:before{content:"\f070"}.fa-warning:before,.fa-exclamation-triangle:before{content:"\f071"}.fa-plane:before{content:"\f072"}.fa-calendar:before{content:"\f073"}.fa-random:before{content:"\f074"}.fa-comment:before{content:"\f075"}.fa-magnet:before{content:"\f076"}.fa-chevron-up:before{content:"\f077"}.fa-chevron-down:before{content:"\f078"}.fa-retweet:before{content:"\f079"}.fa-shopping-cart:before{content:"\f07a"}.fa-folder:before{content:"\f07b"}.fa-folder-open:before{content:"\f07c"}.fa-arrows-v:before{content:"\f07d"}.fa-arrows-h:before{content:"\f07e"}.fa-bar-chart-o:before,.fa-bar-chart:before{content:"\f080"}.fa-twitter-square:before{content:"\f081"}.fa-facebook-square:before{content:"\f082"}.fa-camera-retro:before{content:"\f083"}.fa-key:before{content:"\f084"}.fa-gears:before,.fa-cogs:before{content:"\f085"}.fa-comments:before{content:"\f086"}.fa-thumbs-o-up:before{content:"\f087"}.fa-thumbs-o-down:before{content:"\f088"}.fa-star-half:before{content:"\f089"}.fa-heart-o:before{content:"\f08a"}.fa-sign-out:before{content:"\f08b"}.fa-linkedin-square:before{content:"\f08c"}.fa-thumb-tack:before{content:"\f08d"}.fa-external-link:before{content:"\f08e"}.fa-sign-in:before{content:"\f090"}.fa-trophy:before{content:"\f091"}.fa-github-square:before{content:"\f092"}.fa-upload:before{content:"\f093"}.fa-lemon-o:before{content:"\f094"}.fa-phone:before{content:"\f095"}.fa-square-o:before{content:"\f096"}.fa-bookmark-o:before{content:"\f097"}.fa-phone-square:before{content:"\f098"}.fa-twitter:before{content:"\f099"}.fa-facebook:before{content:"\f09a"}.fa-github:before{content:"\f09b"}.fa-unlock:before{content:"\f09c"}.fa-credit-card:before{content:"\f09d"}.fa-rss:before{content:"\f09e"}.fa-hdd-o:before{content:"\f0a0"}.fa-bullhorn:before{content:"\f0a1"}.fa-bell:before{content:"\f0f3"}.fa-certificate:before{content:"\f0a3"}.fa-hand-o-right:before{content:"\f0a4"}.fa-hand-o-left:before{content:"\f0a5"}.fa-hand-o-up:before{content:"\f0a6"}.fa-hand-o-down:before{content:"\f0a7"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-globe:before{content:"\f0ac"}.fa-wrench:before{content:"\f0ad"}.fa-tasks:before{content:"\f0ae"}.fa-filter:before{content:"\f0b0"}.fa-briefcase:before{content:"\f0b1"}.fa-arrows-alt:before{content:"\f0b2"}.fa-group:before,.fa-users:before{content:"\f0c0"}.fa-chain:before,.fa-link:before{content:"\f0c1"}.fa-cloud:before{content:"\f0c2"}.fa-flask:before{content:"\f0c3"}.fa-cut:before,.fa-scissors:before{content:"\f0c4"}.fa-copy:before,.fa-files-o:before{content:"\f0c5"}.fa-paperclip:before{content:"\f0c6"}.fa-save:before,.fa-floppy-o:before{content:"\f0c7"}.fa-square:before{content:"\f0c8"}.fa-navicon:before,.fa-reorder:before,.fa-bars:before{content:"\f0c9"}.fa-list-ul:before{content:"\f0ca"}.fa-list-ol:before{content:"\f0cb"}.fa-strikethrough:before{content:"\f0cc"}.fa-underline:before{content:"\f0cd"}.fa-table:before{content:"\f0ce"}.fa-magic:before{content:"\f0d0"}.fa-truck:before{content:"\f0d1"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-square:before{content:"\f0d3"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-plus:before{content:"\f0d5"}.fa-money:before{content:"\f0d6"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-up:before{content:"\f0d8"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-columns:before{content:"\f0db"}.fa-unsorted:before,.fa-sort:before{content:"\f0dc"}.fa-sort-down:before,.fa-sort-desc:before{content:"\f0dd"}.fa-sort-up:before,.fa-sort-asc:before{content:"\f0de"}.fa-envelope:before{content:"\f0e0"}.fa-linkedin:before{content:"\f0e1"}.fa-rotate-left:before,.fa-undo:before{content:"\f0e2"}.fa-legal:before,.fa-gavel:before{content:"\f0e3"}.fa-dashboard:before,.fa-tachometer:before{content:"\f0e4"}.fa-comment-o:before{content:"\f0e5"}.fa-comments-o:before{content:"\f0e6"}.fa-flash:before,.fa-bolt:before{content:"\f0e7"}.fa-sitemap:before{content:"\f0e8"}.fa-umbrella:before{content:"\f0e9"}.fa-paste:before,.fa-clipboard:before{content:"\f0ea"}.fa-lightbulb-o:before{content:"\f0eb"}.fa-exchange:before{content:"\f0ec"}.fa-cloud-download:before{content:"\f0ed"}.fa-cloud-upload:before{content:"\f0ee"}.fa-user-md:before{content:"\f0f0"}.fa-stethoscope:before{content:"\f0f1"}.fa-suitcase:before{content:"\f0f2"}.fa-bell-o:before{content:"\f0a2"}.fa-coffee:before{content:"\f0f4"}.fa-cutlery:before{content:"\f0f5"}.fa-file-text-o:before{content:"\f0f6"}.fa-building-o:before{content:"\f0f7"}.fa-hospital-o:before{content:"\f0f8"}.fa-ambulance:before{content:"\f0f9"}.fa-medkit:before{content:"\f0fa"}.fa-fighter-jet:before{content:"\f0fb"}.fa-beer:before{content:"\f0fc"}.fa-h-square:before{content:"\f0fd"}.fa-plus-square:before{content:"\f0fe"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angle-down:before{content:"\f107"}.fa-desktop:before{content:"\f108"}.fa-laptop:before{content:"\f109"}.fa-tablet:before{content:"\f10a"}.fa-mobile-phone:before,.fa-mobile:before{content:"\f10b"}.fa-circle-o:before{content:"\f10c"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-spinner:before{content:"\f110"}.fa-circle:before{content:"\f111"}.fa-mail-reply:before,.fa-reply:before{content:"\f112"}.fa-github-alt:before{content:"\f113"}.fa-folder-o:before{content:"\f114"}.fa-folder-open-o:before{content:"\f115"}.fa-smile-o:before{content:"\f118"}.fa-frown-o:before{content:"\f119"}.fa-meh-o:before{content:"\f11a"}.fa-gamepad:before{content:"\f11b"}.fa-keyboard-o:before{content:"\f11c"}.fa-flag-o:before{content:"\f11d"}.fa-flag-checkered:before{content:"\f11e"}.fa-terminal:before{content:"\f120"}.fa-code:before{content:"\f121"}.fa-mail-reply-all:before,.fa-reply-all:before{content:"\f122"}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:"\f123"}.fa-location-arrow:before{content:"\f124"}.fa-crop:before{content:"\f125"}.fa-code-fork:before{content:"\f126"}.fa-unlink:before,.fa-chain-broken:before{content:"\f127"}.fa-question:before{content:"\f128"}.fa-info:before{content:"\f129"}.fa-exclamation:before{content:"\f12a"}.fa-superscript:before{content:"\f12b"}.fa-subscript:before{content:"\f12c"}.fa-eraser:before{content:"\f12d"}.fa-puzzle-piece:before{content:"\f12e"}.fa-microphone:before{content:"\f130"}.fa-microphone-slash:before{content:"\f131"}.fa-shield:before{content:"\f132"}.fa-calendar-o:before{content:"\f133"}.fa-fire-extinguisher:before{content:"\f134"}.fa-rocket:before{content:"\f135"}.fa-maxcdn:before{content:"\f136"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-html5:before{content:"\f13b"}.fa-css3:before{content:"\f13c"}.fa-anchor:before{content:"\f13d"}.fa-unlock-alt:before{content:"\f13e"}.fa-bullseye:before{content:"\f140"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-rss-square:before{content:"\f143"}.fa-play-circle:before{content:"\f144"}.fa-ticket:before{content:"\f145"}.fa-minus-square:before{content:"\f146"}.fa-minus-square-o:before{content:"\f147"}.fa-level-up:before{content:"\f148"}.fa-level-down:before{content:"\f149"}.fa-check-square:before{content:"\f14a"}.fa-pencil-square:before{content:"\f14b"}.fa-external-link-square:before{content:"\f14c"}.fa-share-square:before{content:"\f14d"}.fa-compass:before{content:"\f14e"}.fa-toggle-down:before,.fa-caret-square-o-down:before{content:"\f150"}.fa-toggle-up:before,.fa-caret-square-o-up:before{content:"\f151"}.fa-toggle-right:before,.fa-caret-square-o-right:before{content:"\f152"}.fa-euro:before,.fa-eur:before{content:"\f153"}.fa-gbp:before{content:"\f154"}.fa-dollar:before,.fa-usd:before{content:"\f155"}.fa-rupee:before,.fa-inr:before{content:"\f156"}.fa-cny:before,.fa-rmb:before,.fa-yen:before,.fa-jpy:before{content:"\f157"}.fa-ruble:before,.fa-rouble:before,.fa-rub:before{content:"\f158"}.fa-won:before,.fa-krw:before{content:"\f159"}.fa-bitcoin:before,.fa-btc:before{content:"\f15a"}.fa-file:before{content:"\f15b"}.fa-file-text:before{content:"\f15c"}.fa-sort-alpha-asc:before{content:"\f15d"}.fa-sort-alpha-desc:before{content:"\f15e"}.fa-sort-amount-asc:before{content:"\f160"}.fa-sort-amount-desc:before{content:"\f161"}.fa-sort-numeric-asc:before{content:"\f162"}.fa-sort-numeric-desc:before{content:"\f163"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbs-down:before{content:"\f165"}.fa-youtube-square:before{content:"\f166"}.fa-youtube:before{content:"\f167"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-youtube-play:before{content:"\f16a"}.fa-dropbox:before{content:"\f16b"}.fa-stack-overflow:before{content:"\f16c"}.fa-instagram:before{content:"\f16d"}.fa-flickr:before{content:"\f16e"}.fa-adn:before{content:"\f170"}.fa-bitbucket:before{content:"\f171"}.fa-bitbucket-square:before{content:"\f172"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-long-arrow-down:before{content:"\f175"}.fa-long-arrow-up:before{content:"\f176"}.fa-long-arrow-left:before{content:"\f177"}.fa-long-arrow-right:before{content:"\f178"}.fa-apple:before{content:"\f179"}.fa-windows:before{content:"\f17a"}.fa-android:before{content:"\f17b"}.fa-linux:before{content:"\f17c"}.fa-dribbble:before{content:"\f17d"}.fa-skype:before{content:"\f17e"}.fa-foursquare:before{content:"\f180"}.fa-trello:before{content:"\f181"}.fa-female:before{content:"\f182"}.fa-male:before{content:"\f183"}.fa-gittip:before{content:"\f184"}.fa-sun-o:before{content:"\f185"}.fa-moon-o:before{content:"\f186"}.fa-archive:before{content:"\f187"}.fa-bug:before{content:"\f188"}.fa-vk:before{content:"\f189"}.fa-weibo:before{content:"\f18a"}.fa-renren:before{content:"\f18b"}.fa-pagelines:before{content:"\f18c"}.fa-stack-exchange:before{content:"\f18d"}.fa-arrow-circle-o-right:before{content:"\f18e"}.fa-arrow-circle-o-left:before{content:"\f190"}.fa-toggle-left:before,.fa-caret-square-o-left:before{content:"\f191"}.fa-dot-circle-o:before{content:"\f192"}.fa-wheelchair:before{content:"\f193"}.fa-vimeo-square:before{content:"\f194"}.fa-turkish-lira:before,.fa-try:before{content:"\f195"}.fa-plus-square-o:before{content:"\f196"}.fa-space-shuttle:before{content:"\f197"}.fa-slack:before{content:"\f198"}.fa-envelope-square:before{content:"\f199"}.fa-wordpress:before{content:"\f19a"}.fa-openid:before{content:"\f19b"}.fa-institution:before,.fa-bank:before,.fa-university:before{content:"\f19c"}.fa-mortar-board:before,.fa-graduation-cap:before{content:"\f19d"}.fa-yahoo:before{content:"\f19e"}.fa-google:before{content:"\f1a0"}.fa-reddit:before{content:"\f1a1"}.fa-reddit-square:before{content:"\f1a2"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-stumbleupon:before{content:"\f1a4"}.fa-delicious:before{content:"\f1a5"}.fa-digg:before{content:"\f1a6"}.fa-pied-piper:before{content:"\f1a7"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-drupal:before{content:"\f1a9"}.fa-joomla:before{content:"\f1aa"}.fa-language:before{content:"\f1ab"}.fa-fax:before{content:"\f1ac"}.fa-building:before{content:"\f1ad"}.fa-child:before{content:"\f1ae"}.fa-paw:before{content:"\f1b0"}.fa-spoon:before{content:"\f1b1"}.fa-cube:before{content:"\f1b2"}.fa-cubes:before{content:"\f1b3"}.fa-behance:before{content:"\f1b4"}.fa-behance-square:before{content:"\f1b5"}.fa-steam:before{content:"\f1b6"}.fa-steam-square:before{content:"\f1b7"}.fa-recycle:before{content:"\f1b8"}.fa-automobile:before,.fa-car:before{content:"\f1b9"}.fa-cab:before,.fa-taxi:before{content:"\f1ba"}.fa-tree:before{content:"\f1bb"}.fa-spotify:before{content:"\f1bc"}.fa-deviantart:before{content:"\f1bd"}.fa-soundcloud:before{content:"\f1be"}.fa-database:before{content:"\f1c0"}.fa-file-pdf-o:before{content:"\f1c1"}.fa-file-word-o:before{content:"\f1c2"}.fa-file-excel-o:before{content:"\f1c3"}.fa-file-powerpoint-o:before{content:"\f1c4"}.fa-file-photo-o:before,.fa-file-picture-o:before,.fa-file-image-o:before{content:"\f1c5"}.fa-file-zip-o:before,.fa-file-archive-o:before{content:"\f1c6"}.fa-file-sound-o:before,.fa-file-audio-o:before{content:"\f1c7"}.fa-file-movie-o:before,.fa-file-video-o:before{content:"\f1c8"}.fa-file-code-o:before{content:"\f1c9"}.fa-vine:before{content:"\f1ca"}.fa-codepen:before{content:"\f1cb"}.fa-jsfiddle:before{content:"\f1cc"}.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-saver:before,.fa-support:before,.fa-life-ring:before{content:"\f1cd"}.fa-circle-o-notch:before{content:"\f1ce"}.fa-ra:before,.fa-rebel:before{content:"\f1d0"}.fa-ge:before,.fa-empire:before{content:"\f1d1"}.fa-git-square:before{content:"\f1d2"}.fa-git:before{content:"\f1d3"}.fa-hacker-news:before{content:"\f1d4"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-qq:before{content:"\f1d6"}.fa-wechat:before,.fa-weixin:before{content:"\f1d7"}.fa-send:before,.fa-paper-plane:before{content:"\f1d8"}.fa-send-o:before,.fa-paper-plane-o:before{content:"\f1d9"}.fa-history:before{content:"\f1da"}.fa-circle-thin:before{content:"\f1db"}.fa-header:before{content:"\f1dc"}.fa-paragraph:before{content:"\f1dd"}.fa-sliders:before{content:"\f1de"}.fa-share-alt:before{content:"\f1e0"}.fa-share-alt-square:before{content:"\f1e1"}.fa-bomb:before{content:"\f1e2"}.fa-soccer-ball-o:before,.fa-futbol-o:before{content:"\f1e3"}.fa-tty:before{content:"\f1e4"}.fa-binoculars:before{content:"\f1e5"}.fa-plug:before{content:"\f1e6"}.fa-slideshare:before{content:"\f1e7"}.fa-twitch:before{content:"\f1e8"}.fa-yelp:before{content:"\f1e9"}.fa-newspaper-o:before{content:"\f1ea"}.fa-wifi:before{content:"\f1eb"}.fa-calculator:before{content:"\f1ec"}.fa-paypal:before{content:"\f1ed"}.fa-google-wallet:before{content:"\f1ee"}.fa-cc-visa:before{content:"\f1f0"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-cc-discover:before{content:"\f1f2"}.fa-cc-amex:before{content:"\f1f3"}.fa-cc-paypal:before{content:"\f1f4"}.fa-cc-stripe:before{content:"\f1f5"}.fa-bell-slash:before{content:"\f1f6"}.fa-bell-slash-o:before{content:"\f1f7"}.fa-trash:before{content:"\f1f8"}.fa-copyright:before{content:"\f1f9"}.fa-at:before{content:"\f1fa"}.fa-eyedropper:before{content:"\f1fb"}.fa-paint-brush:before{content:"\f1fc"}.fa-birthday-cake:before{content:"\f1fd"}.fa-area-chart:before{content:"\f1fe"}.fa-pie-chart:before{content:"\f200"}.fa-line-chart:before{content:"\f201"}.fa-lastfm:before{content:"\f202"}.fa-lastfm-square:before{content:"\f203"}.fa-toggle-off:before{content:"\f204"}.fa-toggle-on:before{content:"\f205"}.fa-bicycle:before{content:"\f206"}.fa-bus:before{content:"\f207"}.fa-ioxhost:before{content:"\f208"}.fa-angellist:before{content:"\f209"}.fa-cc:before{content:"\f20a"}.fa-shekel:before,.fa-sheqel:before,.fa-ils:before{content:"\f20b"}.fa-meanpath:before{content:"\f20c"}
\ No newline at end of file
+ */@font-face{font-family:'FontAwesome';src:url('../fonts/fontawesome-webfont.eot?v=4.6.3');src:url('../fonts/fontawesome-webfont.eot?#iefix&v=4.6.3') format('embedded-opentype'),url('../fonts/fontawesome-webfont.woff2?v=4.6.3') format('woff2'),url('../fonts/fontawesome-webfont.woff?v=4.6.3') format('woff'),url('../fonts/fontawesome-webfont.ttf?v=4.6.3') format('truetype'),url('../fonts/fontawesome-webfont.svg?v=4.6.3#fontawesomeregular') format('svg');font-weight:normal;font-style:normal}.fa{display:inline-block;font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.33333333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.28571429em;text-align:center}.fa-ul{padding-left:0;margin-left:2.14285714em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.14285714em;width:2.14285714em;top:.14285714em;text-align:center}.fa-li.fa-lg{left:-1.85714286em}.fa-border{padding:.2em .25em .15em;border:solid .08em #eee;border-radius:.1em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left{margin-right:.3em}.fa.fa-pull-right{margin-left:.3em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left{margin-right:.3em}.fa.pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s infinite linear;animation:fa-spin 2s infinite linear}.fa-pulse{-webkit-animation:fa-spin 1s infinite steps(8);animation:fa-spin 1s infinite steps(8)}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";-webkit-transform:scale(-1, 1);-ms-transform:scale(-1, 1);transform:scale(-1, 1)}.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)";-webkit-transform:scale(1, -1);-ms-transform:scale(1, -1);transform:scale(1, -1)}:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270,:root .fa-flip-horizontal,:root .fa-flip-vertical{filter:none}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:"\f000"}.fa-music:before{content:"\f001"}.fa-search:before{content:"\f002"}.fa-envelope-o:before{content:"\f003"}.fa-heart:before{content:"\f004"}.fa-star:before{content:"\f005"}.fa-star-o:before{content:"\f006"}.fa-user:before{content:"\f007"}.fa-film:before{content:"\f008"}.fa-th-large:before{content:"\f009"}.fa-th:before{content:"\f00a"}.fa-th-list:before{content:"\f00b"}.fa-check:before{content:"\f00c"}.fa-remove:before,.fa-close:before,.fa-times:before{content:"\f00d"}.fa-search-plus:before{content:"\f00e"}.fa-search-minus:before{content:"\f010"}.fa-power-off:before{content:"\f011"}.fa-signal:before{content:"\f012"}.fa-gear:before,.fa-cog:before{content:"\f013"}.fa-trash-o:before{content:"\f014"}.fa-home:before{content:"\f015"}.fa-file-o:before{content:"\f016"}.fa-clock-o:before{content:"\f017"}.fa-road:before{content:"\f018"}.fa-download:before{content:"\f019"}.fa-arrow-circle-o-down:before{content:"\f01a"}.fa-arrow-circle-o-up:before{content:"\f01b"}.fa-inbox:before{content:"\f01c"}.fa-play-circle-o:before{content:"\f01d"}.fa-rotate-right:before,.fa-repeat:before{content:"\f01e"}.fa-refresh:before{content:"\f021"}.fa-list-alt:before{content:"\f022"}.fa-lock:before{content:"\f023"}.fa-flag:before{content:"\f024"}.fa-headphones:before{content:"\f025"}.fa-volume-off:before{content:"\f026"}.fa-volume-down:before{content:"\f027"}.fa-volume-up:before{content:"\f028"}.fa-qrcode:before{content:"\f029"}.fa-barcode:before{content:"\f02a"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-book:before{content:"\f02d"}.fa-bookmark:before{content:"\f02e"}.fa-print:before{content:"\f02f"}.fa-camera:before{content:"\f030"}.fa-font:before{content:"\f031"}.fa-bold:before{content:"\f032"}.fa-italic:before{content:"\f033"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-align-left:before{content:"\f036"}.fa-align-center:before{content:"\f037"}.fa-align-right:before{content:"\f038"}.fa-align-justify:before{content:"\f039"}.fa-list:before{content:"\f03a"}.fa-dedent:before,.fa-outdent:before{content:"\f03b"}.fa-indent:before{content:"\f03c"}.fa-video-camera:before{content:"\f03d"}.fa-photo:before,.fa-image:before,.fa-picture-o:before{content:"\f03e"}.fa-pencil:before{content:"\f040"}.fa-map-marker:before{content:"\f041"}.fa-adjust:before{content:"\f042"}.fa-tint:before{content:"\f043"}.fa-edit:before,.fa-pencil-square-o:before{content:"\f044"}.fa-share-square-o:before{content:"\f045"}.fa-check-square-o:before{content:"\f046"}.fa-arrows:before{content:"\f047"}.fa-step-backward:before{content:"\f048"}.fa-fast-backward:before{content:"\f049"}.fa-backward:before{content:"\f04a"}.fa-play:before{content:"\f04b"}.fa-pause:before{content:"\f04c"}.fa-stop:before{content:"\f04d"}.fa-forward:before{content:"\f04e"}.fa-fast-forward:before{content:"\f050"}.fa-step-forward:before{content:"\f051"}.fa-eject:before{content:"\f052"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-plus-circle:before{content:"\f055"}.fa-minus-circle:before{content:"\f056"}.fa-times-circle:before{content:"\f057"}.fa-check-circle:before{content:"\f058"}.fa-question-circle:before{content:"\f059"}.fa-info-circle:before{content:"\f05a"}.fa-crosshairs:before{content:"\f05b"}.fa-times-circle-o:before{content:"\f05c"}.fa-check-circle-o:before{content:"\f05d"}.fa-ban:before{content:"\f05e"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrow-down:before{content:"\f063"}.fa-mail-forward:before,.fa-share:before{content:"\f064"}.fa-expand:before{content:"\f065"}.fa-compress:before{content:"\f066"}.fa-plus:before{content:"\f067"}.fa-minus:before{content:"\f068"}.fa-asterisk:before{content:"\f069"}.fa-exclamation-circle:before{content:"\f06a"}.fa-gift:before{content:"\f06b"}.fa-leaf:before{content:"\f06c"}.fa-fire:before{content:"\f06d"}.fa-eye:before{content:"\f06e"}.fa-eye-slash:before{content:"\f070"}.fa-warning:before,.fa-exclamation-triangle:before{content:"\f071"}.fa-plane:before{content:"\f072"}.fa-calendar:before{content:"\f073"}.fa-random:before{content:"\f074"}.fa-comment:before{content:"\f075"}.fa-magnet:before{content:"\f076"}.fa-chevron-up:before{content:"\f077"}.fa-chevron-down:before{content:"\f078"}.fa-retweet:before{content:"\f079"}.fa-shopping-cart:before{content:"\f07a"}.fa-folder:before{content:"\f07b"}.fa-folder-open:before{content:"\f07c"}.fa-arrows-v:before{content:"\f07d"}.fa-arrows-h:before{content:"\f07e"}.fa-bar-chart-o:before,.fa-bar-chart:before{content:"\f080"}.fa-twitter-square:before{content:"\f081"}.fa-facebook-square:before{content:"\f082"}.fa-camera-retro:before{content:"\f083"}.fa-key:before{content:"\f084"}.fa-gears:before,.fa-cogs:before{content:"\f085"}.fa-comments:before{content:"\f086"}.fa-thumbs-o-up:before{content:"\f087"}.fa-thumbs-o-down:before{content:"\f088"}.fa-star-half:before{content:"\f089"}.fa-heart-o:before{content:"\f08a"}.fa-sign-out:before{content:"\f08b"}.fa-linkedin-square:before{content:"\f08c"}.fa-thumb-tack:before{content:"\f08d"}.fa-external-link:before{content:"\f08e"}.fa-sign-in:before{content:"\f090"}.fa-trophy:before{content:"\f091"}.fa-github-square:before{content:"\f092"}.fa-upload:before{content:"\f093"}.fa-lemon-o:before{content:"\f094"}.fa-phone:before{content:"\f095"}.fa-square-o:before{content:"\f096"}.fa-bookmark-o:before{content:"\f097"}.fa-phone-square:before{content:"\f098"}.fa-twitter:before{content:"\f099"}.fa-facebook-f:before,.fa-facebook:before{content:"\f09a"}.fa-github:before{content:"\f09b"}.fa-unlock:before{content:"\f09c"}.fa-credit-card:before{content:"\f09d"}.fa-feed:before,.fa-rss:before{content:"\f09e"}.fa-hdd-o:before{content:"\f0a0"}.fa-bullhorn:before{content:"\f0a1"}.fa-bell:before{content:"\f0f3"}.fa-certificate:before{content:"\f0a3"}.fa-hand-o-right:before{content:"\f0a4"}.fa-hand-o-left:before{content:"\f0a5"}.fa-hand-o-up:before{content:"\f0a6"}.fa-hand-o-down:before{content:"\f0a7"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-globe:before{content:"\f0ac"}.fa-wrench:before{content:"\f0ad"}.fa-tasks:before{content:"\f0ae"}.fa-filter:before{content:"\f0b0"}.fa-briefcase:before{content:"\f0b1"}.fa-arrows-alt:before{content:"\f0b2"}.fa-group:before,.fa-users:before{content:"\f0c0"}.fa-chain:before,.fa-link:before{content:"\f0c1"}.fa-cloud:before{content:"\f0c2"}.fa-flask:before{content:"\f0c3"}.fa-cut:before,.fa-scissors:before{content:"\f0c4"}.fa-copy:before,.fa-files-o:before{content:"\f0c5"}.fa-paperclip:before{content:"\f0c6"}.fa-save:before,.fa-floppy-o:before{content:"\f0c7"}.fa-square:before{content:"\f0c8"}.fa-navicon:before,.fa-reorder:before,.fa-bars:before{content:"\f0c9"}.fa-list-ul:before{content:"\f0ca"}.fa-list-ol:before{content:"\f0cb"}.fa-strikethrough:before{content:"\f0cc"}.fa-underline:before{content:"\f0cd"}.fa-table:before{content:"\f0ce"}.fa-magic:before{content:"\f0d0"}.fa-truck:before{content:"\f0d1"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-square:before{content:"\f0d3"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-plus:before{content:"\f0d5"}.fa-money:before{content:"\f0d6"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-up:before{content:"\f0d8"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-columns:before{content:"\f0db"}.fa-unsorted:before,.fa-sort:before{content:"\f0dc"}.fa-sort-down:before,.fa-sort-desc:before{content:"\f0dd"}.fa-sort-up:before,.fa-sort-asc:before{content:"\f0de"}.fa-envelope:before{content:"\f0e0"}.fa-linkedin:before{content:"\f0e1"}.fa-rotate-left:before,.fa-undo:before{content:"\f0e2"}.fa-legal:before,.fa-gavel:before{content:"\f0e3"}.fa-dashboard:before,.fa-tachometer:before{content:"\f0e4"}.fa-comment-o:before{content:"\f0e5"}.fa-comments-o:before{content:"\f0e6"}.fa-flash:before,.fa-bolt:before{content:"\f0e7"}.fa-sitemap:before{content:"\f0e8"}.fa-umbrella:before{content:"\f0e9"}.fa-paste:before,.fa-clipboard:before{content:"\f0ea"}.fa-lightbulb-o:before{content:"\f0eb"}.fa-exchange:before{content:"\f0ec"}.fa-cloud-download:before{content:"\f0ed"}.fa-cloud-upload:before{content:"\f0ee"}.fa-user-md:before{content:"\f0f0"}.fa-stethoscope:before{content:"\f0f1"}.fa-suitcase:before{content:"\f0f2"}.fa-bell-o:before{content:"\f0a2"}.fa-coffee:before{content:"\f0f4"}.fa-cutlery:before{content:"\f0f5"}.fa-file-text-o:before{content:"\f0f6"}.fa-building-o:before{content:"\f0f7"}.fa-hospital-o:before{content:"\f0f8"}.fa-ambulance:before{content:"\f0f9"}.fa-medkit:before{content:"\f0fa"}.fa-fighter-jet:before{content:"\f0fb"}.fa-beer:before{content:"\f0fc"}.fa-h-square:before{content:"\f0fd"}.fa-plus-square:before{content:"\f0fe"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angle-down:before{content:"\f107"}.fa-desktop:before{content:"\f108"}.fa-laptop:before{content:"\f109"}.fa-tablet:before{content:"\f10a"}.fa-mobile-phone:before,.fa-mobile:before{content:"\f10b"}.fa-circle-o:before{content:"\f10c"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-spinner:before{content:"\f110"}.fa-circle:before{content:"\f111"}.fa-mail-reply:before,.fa-reply:before{content:"\f112"}.fa-github-alt:before{content:"\f113"}.fa-folder-o:before{content:"\f114"}.fa-folder-open-o:before{content:"\f115"}.fa-smile-o:before{content:"\f118"}.fa-frown-o:before{content:"\f119"}.fa-meh-o:before{content:"\f11a"}.fa-gamepad:before{content:"\f11b"}.fa-keyboard-o:before{content:"\f11c"}.fa-flag-o:before{content:"\f11d"}.fa-flag-checkered:before{content:"\f11e"}.fa-terminal:before{content:"\f120"}.fa-code:before{content:"\f121"}.fa-mail-reply-all:before,.fa-reply-all:before{content:"\f122"}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:"\f123"}.fa-location-arrow:before{content:"\f124"}.fa-crop:before{content:"\f125"}.fa-code-fork:before{content:"\f126"}.fa-unlink:before,.fa-chain-broken:before{content:"\f127"}.fa-question:before{content:"\f128"}.fa-info:before{content:"\f129"}.fa-exclamation:before{content:"\f12a"}.fa-superscript:before{content:"\f12b"}.fa-subscript:before{content:"\f12c"}.fa-eraser:before{content:"\f12d"}.fa-puzzle-piece:before{content:"\f12e"}.fa-microphone:before{content:"\f130"}.fa-microphone-slash:before{content:"\f131"}.fa-shield:before{content:"\f132"}.fa-calendar-o:before{content:"\f133"}.fa-fire-extinguisher:before{content:"\f134"}.fa-rocket:before{content:"\f135"}.fa-maxcdn:before{content:"\f136"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-html5:before{content:"\f13b"}.fa-css3:before{content:"\f13c"}.fa-anchor:before{content:"\f13d"}.fa-unlock-alt:before{content:"\f13e"}.fa-bullseye:before{content:"\f140"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-rss-square:before{content:"\f143"}.fa-play-circle:before{content:"\f144"}.fa-ticket:before{content:"\f145"}.fa-minus-square:before{content:"\f146"}.fa-minus-square-o:before{content:"\f147"}.fa-level-up:before{content:"\f148"}.fa-level-down:before{content:"\f149"}.fa-check-square:before{content:"\f14a"}.fa-pencil-square:before{content:"\f14b"}.fa-external-link-square:before{content:"\f14c"}.fa-share-square:before{content:"\f14d"}.fa-compass:before{content:"\f14e"}.fa-toggle-down:before,.fa-caret-square-o-down:before{content:"\f150"}.fa-toggle-up:before,.fa-caret-square-o-up:before{content:"\f151"}.fa-toggle-right:before,.fa-caret-square-o-right:before{content:"\f152"}.fa-euro:before,.fa-eur:before{content:"\f153"}.fa-gbp:before{content:"\f154"}.fa-dollar:before,.fa-usd:before{content:"\f155"}.fa-rupee:before,.fa-inr:before{content:"\f156"}.fa-cny:before,.fa-rmb:before,.fa-yen:before,.fa-jpy:before{content:"\f157"}.fa-ruble:before,.fa-rouble:before,.fa-rub:before{content:"\f158"}.fa-won:before,.fa-krw:before{content:"\f159"}.fa-bitcoin:before,.fa-btc:before{content:"\f15a"}.fa-file:before{content:"\f15b"}.fa-file-text:before{content:"\f15c"}.fa-sort-alpha-asc:before{content:"\f15d"}.fa-sort-alpha-desc:before{content:"\f15e"}.fa-sort-amount-asc:before{content:"\f160"}.fa-sort-amount-desc:before{content:"\f161"}.fa-sort-numeric-asc:before{content:"\f162"}.fa-sort-numeric-desc:before{content:"\f163"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbs-down:before{content:"\f165"}.fa-youtube-square:before{content:"\f166"}.fa-youtube:before{content:"\f167"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-youtube-play:before{content:"\f16a"}.fa-dropbox:before{content:"\f16b"}.fa-stack-overflow:before{content:"\f16c"}.fa-instagram:before{content:"\f16d"}.fa-flickr:before{content:"\f16e"}.fa-adn:before{content:"\f170"}.fa-bitbucket:before{content:"\f171"}.fa-bitbucket-square:before{content:"\f172"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-long-arrow-down:before{content:"\f175"}.fa-long-arrow-up:before{content:"\f176"}.fa-long-arrow-left:before{content:"\f177"}.fa-long-arrow-right:before{content:"\f178"}.fa-apple:before{content:"\f179"}.fa-windows:before{content:"\f17a"}.fa-android:before{content:"\f17b"}.fa-linux:before{content:"\f17c"}.fa-dribbble:before{content:"\f17d"}.fa-skype:before{content:"\f17e"}.fa-foursquare:before{content:"\f180"}.fa-trello:before{content:"\f181"}.fa-female:before{content:"\f182"}.fa-male:before{content:"\f183"}.fa-gittip:before,.fa-gratipay:before{content:"\f184"}.fa-sun-o:before{content:"\f185"}.fa-moon-o:before{content:"\f186"}.fa-archive:before{content:"\f187"}.fa-bug:before{content:"\f188"}.fa-vk:before{content:"\f189"}.fa-weibo:before{content:"\f18a"}.fa-renren:before{content:"\f18b"}.fa-pagelines:before{content:"\f18c"}.fa-stack-exchange:before{content:"\f18d"}.fa-arrow-circle-o-right:before{content:"\f18e"}.fa-arrow-circle-o-left:before{content:"\f190"}.fa-toggle-left:before,.fa-caret-square-o-left:before{content:"\f191"}.fa-dot-circle-o:before{content:"\f192"}.fa-wheelchair:before{content:"\f193"}.fa-vimeo-square:before{content:"\f194"}.fa-turkish-lira:before,.fa-try:before{content:"\f195"}.fa-plus-square-o:before{content:"\f196"}.fa-space-shuttle:before{content:"\f197"}.fa-slack:before{content:"\f198"}.fa-envelope-square:before{content:"\f199"}.fa-wordpress:before{content:"\f19a"}.fa-openid:before{content:"\f19b"}.fa-institution:before,.fa-bank:before,.fa-university:before{content:"\f19c"}.fa-mortar-board:before,.fa-graduation-cap:before{content:"\f19d"}.fa-yahoo:before{content:"\f19e"}.fa-google:before{content:"\f1a0"}.fa-reddit:before{content:"\f1a1"}.fa-reddit-square:before{content:"\f1a2"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-stumbleupon:before{content:"\f1a4"}.fa-delicious:before{content:"\f1a5"}.fa-digg:before{content:"\f1a6"}.fa-pied-piper-pp:before{content:"\f1a7"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-drupal:before{content:"\f1a9"}.fa-joomla:before{content:"\f1aa"}.fa-language:before{content:"\f1ab"}.fa-fax:before{content:"\f1ac"}.fa-building:before{content:"\f1ad"}.fa-child:before{content:"\f1ae"}.fa-paw:before{content:"\f1b0"}.fa-spoon:before{content:"\f1b1"}.fa-cube:before{content:"\f1b2"}.fa-cubes:before{content:"\f1b3"}.fa-behance:before{content:"\f1b4"}.fa-behance-square:before{content:"\f1b5"}.fa-steam:before{content:"\f1b6"}.fa-steam-square:before{content:"\f1b7"}.fa-recycle:before{content:"\f1b8"}.fa-automobile:before,.fa-car:before{content:"\f1b9"}.fa-cab:before,.fa-taxi:before{content:"\f1ba"}.fa-tree:before{content:"\f1bb"}.fa-spotify:before{content:"\f1bc"}.fa-deviantart:before{content:"\f1bd"}.fa-soundcloud:before{content:"\f1be"}.fa-database:before{content:"\f1c0"}.fa-file-pdf-o:before{content:"\f1c1"}.fa-file-word-o:before{content:"\f1c2"}.fa-file-excel-o:before{content:"\f1c3"}.fa-file-powerpoint-o:before{content:"\f1c4"}.fa-file-photo-o:before,.fa-file-picture-o:before,.fa-file-image-o:before{content:"\f1c5"}.fa-file-zip-o:before,.fa-file-archive-o:before{content:"\f1c6"}.fa-file-sound-o:before,.fa-file-audio-o:before{content:"\f1c7"}.fa-file-movie-o:before,.fa-file-video-o:before{content:"\f1c8"}.fa-file-code-o:before{content:"\f1c9"}.fa-vine:before{content:"\f1ca"}.fa-codepen:before{content:"\f1cb"}.fa-jsfiddle:before{content:"\f1cc"}.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-saver:before,.fa-support:before,.fa-life-ring:before{content:"\f1cd"}.fa-circle-o-notch:before{content:"\f1ce"}.fa-ra:before,.fa-resistance:before,.fa-rebel:before{content:"\f1d0"}.fa-ge:before,.fa-empire:before{content:"\f1d1"}.fa-git-square:before{content:"\f1d2"}.fa-git:before{content:"\f1d3"}.fa-y-combinator-square:before,.fa-yc-square:before,.fa-hacker-news:before{content:"\f1d4"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-qq:before{content:"\f1d6"}.fa-wechat:before,.fa-weixin:before{content:"\f1d7"}.fa-send:before,.fa-paper-plane:before{content:"\f1d8"}.fa-send-o:before,.fa-paper-plane-o:before{content:"\f1d9"}.fa-history:before{content:"\f1da"}.fa-circle-thin:before{content:"\f1db"}.fa-header:before{content:"\f1dc"}.fa-paragraph:before{content:"\f1dd"}.fa-sliders:before{content:"\f1de"}.fa-share-alt:before{content:"\f1e0"}.fa-share-alt-square:before{content:"\f1e1"}.fa-bomb:before{content:"\f1e2"}.fa-soccer-ball-o:before,.fa-futbol-o:before{content:"\f1e3"}.fa-tty:before{content:"\f1e4"}.fa-binoculars:before{content:"\f1e5"}.fa-plug:before{content:"\f1e6"}.fa-slideshare:before{content:"\f1e7"}.fa-twitch:before{content:"\f1e8"}.fa-yelp:before{content:"\f1e9"}.fa-newspaper-o:before{content:"\f1ea"}.fa-wifi:before{content:"\f1eb"}.fa-calculator:before{content:"\f1ec"}.fa-paypal:before{content:"\f1ed"}.fa-google-wallet:before{content:"\f1ee"}.fa-cc-visa:before{content:"\f1f0"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-cc-discover:before{content:"\f1f2"}.fa-cc-amex:before{content:"\f1f3"}.fa-cc-paypal:before{content:"\f1f4"}.fa-cc-stripe:before{content:"\f1f5"}.fa-bell-slash:before{content:"\f1f6"}.fa-bell-slash-o:before{content:"\f1f7"}.fa-trash:before{content:"\f1f8"}.fa-copyright:before{content:"\f1f9"}.fa-at:before{content:"\f1fa"}.fa-eyedropper:before{content:"\f1fb"}.fa-paint-brush:before{content:"\f1fc"}.fa-birthday-cake:before{content:"\f1fd"}.fa-area-chart:before{content:"\f1fe"}.fa-pie-chart:before{content:"\f200"}.fa-line-chart:before{content:"\f201"}.fa-lastfm:before{content:"\f202"}.fa-lastfm-square:before{content:"\f203"}.fa-toggle-off:before{content:"\f204"}.fa-toggle-on:before{content:"\f205"}.fa-bicycle:before{content:"\f206"}.fa-bus:before{content:"\f207"}.fa-ioxhost:before{content:"\f208"}.fa-angellist:before{content:"\f209"}.fa-cc:before{content:"\f20a"}.fa-shekel:before,.fa-sheqel:before,.fa-ils:before{content:"\f20b"}.fa-meanpath:before{content:"\f20c"}.fa-buysellads:before{content:"\f20d"}.fa-connectdevelop:before{content:"\f20e"}.fa-dashcube:before{content:"\f210"}.fa-forumbee:before{content:"\f211"}.fa-leanpub:before{content:"\f212"}.fa-sellsy:before{content:"\f213"}.fa-shirtsinbulk:before{content:"\f214"}.fa-simplybuilt:before{content:"\f215"}.fa-skyatlas:before{content:"\f216"}.fa-cart-plus:before{content:"\f217"}.fa-cart-arrow-down:before{content:"\f218"}.fa-diamond:before{content:"\f219"}.fa-ship:before{content:"\f21a"}.fa-user-secret:before{content:"\f21b"}.fa-motorcycle:before{content:"\f21c"}.fa-street-view:before{content:"\f21d"}.fa-heartbeat:before{content:"\f21e"}.fa-venus:before{content:"\f221"}.fa-mars:before{content:"\f222"}.fa-mercury:before{content:"\f223"}.fa-intersex:before,.fa-transgender:before{content:"\f224"}.fa-transgender-alt:before{content:"\f225"}.fa-venus-double:before{content:"\f226"}.fa-mars-double:before{content:"\f227"}.fa-venus-mars:before{content:"\f228"}.fa-mars-stroke:before{content:"\f229"}.fa-mars-stroke-v:before{content:"\f22a"}.fa-mars-stroke-h:before{content:"\f22b"}.fa-neuter:before{content:"\f22c"}.fa-genderless:before{content:"\f22d"}.fa-facebook-official:before{content:"\f230"}.fa-pinterest-p:before{content:"\f231"}.fa-whatsapp:before{content:"\f232"}.fa-server:before{content:"\f233"}.fa-user-plus:before{content:"\f234"}.fa-user-times:before{content:"\f235"}.fa-hotel:before,.fa-bed:before{content:"\f236"}.fa-viacoin:before{content:"\f237"}.fa-train:before{content:"\f238"}.fa-subway:before{content:"\f239"}.fa-medium:before{content:"\f23a"}.fa-yc:before,.fa-y-combinator:before{content:"\f23b"}.fa-optin-monster:before{content:"\f23c"}.fa-opencart:before{content:"\f23d"}.fa-expeditedssl:before{content:"\f23e"}.fa-battery-4:before,.fa-battery-full:before{content:"\f240"}.fa-battery-3:before,.fa-battery-three-quarters:before{content:"\f241"}.fa-battery-2:before,.fa-battery-half:before{content:"\f242"}.fa-battery-1:before,.fa-battery-quarter:before{content:"\f243"}.fa-battery-0:before,.fa-battery-empty:before{content:"\f244"}.fa-mouse-pointer:before{content:"\f245"}.fa-i-cursor:before{content:"\f246"}.fa-object-group:before{content:"\f247"}.fa-object-ungroup:before{content:"\f248"}.fa-sticky-note:before{content:"\f249"}.fa-sticky-note-o:before{content:"\f24a"}.fa-cc-jcb:before{content:"\f24b"}.fa-cc-diners-club:before{content:"\f24c"}.fa-clone:before{content:"\f24d"}.fa-balance-scale:before{content:"\f24e"}.fa-hourglass-o:before{content:"\f250"}.fa-hourglass-1:before,.fa-hourglass-start:before{content:"\f251"}.fa-hourglass-2:before,.fa-hourglass-half:before{content:"\f252"}.fa-hourglass-3:before,.fa-hourglass-end:before{content:"\f253"}.fa-hourglass:before{content:"\f254"}.fa-hand-grab-o:before,.fa-hand-rock-o:before{content:"\f255"}.fa-hand-stop-o:before,.fa-hand-paper-o:before{content:"\f256"}.fa-hand-scissors-o:before{content:"\f257"}.fa-hand-lizard-o:before{content:"\f258"}.fa-hand-spock-o:before{content:"\f259"}.fa-hand-pointer-o:before{content:"\f25a"}.fa-hand-peace-o:before{content:"\f25b"}.fa-trademark:before{content:"\f25c"}.fa-registered:before{content:"\f25d"}.fa-creative-commons:before{content:"\f25e"}.fa-gg:before{content:"\f260"}.fa-gg-circle:before{content:"\f261"}.fa-tripadvisor:before{content:"\f262"}.fa-odnoklassniki:before{content:"\f263"}.fa-odnoklassniki-square:before{content:"\f264"}.fa-get-pocket:before{content:"\f265"}.fa-wikipedia-w:before{content:"\f266"}.fa-safari:before{content:"\f267"}.fa-chrome:before{content:"\f268"}.fa-firefox:before{content:"\f269"}.fa-opera:before{content:"\f26a"}.fa-internet-explorer:before{content:"\f26b"}.fa-tv:before,.fa-television:before{content:"\f26c"}.fa-contao:before{content:"\f26d"}.fa-500px:before{content:"\f26e"}.fa-amazon:before{content:"\f270"}.fa-calendar-plus-o:before{content:"\f271"}.fa-calendar-minus-o:before{content:"\f272"}.fa-calendar-times-o:before{content:"\f273"}.fa-calendar-check-o:before{content:"\f274"}.fa-industry:before{content:"\f275"}.fa-map-pin:before{content:"\f276"}.fa-map-signs:before{content:"\f277"}.fa-map-o:before{content:"\f278"}.fa-map:before{content:"\f279"}.fa-commenting:before{content:"\f27a"}.fa-commenting-o:before{content:"\f27b"}.fa-houzz:before{content:"\f27c"}.fa-vimeo:before{content:"\f27d"}.fa-black-tie:before{content:"\f27e"}.fa-fonticons:before{content:"\f280"}.fa-reddit-alien:before{content:"\f281"}.fa-edge:before{content:"\f282"}.fa-credit-card-alt:before{content:"\f283"}.fa-codiepie:before{content:"\f284"}.fa-modx:before{content:"\f285"}.fa-fort-awesome:before{content:"\f286"}.fa-usb:before{content:"\f287"}.fa-product-hunt:before{content:"\f288"}.fa-mixcloud:before{content:"\f289"}.fa-scribd:before{content:"\f28a"}.fa-pause-circle:before{content:"\f28b"}.fa-pause-circle-o:before{content:"\f28c"}.fa-stop-circle:before{content:"\f28d"}.fa-stop-circle-o:before{content:"\f28e"}.fa-shopping-bag:before{content:"\f290"}.fa-shopping-basket:before{content:"\f291"}.fa-hashtag:before{content:"\f292"}.fa-bluetooth:before{content:"\f293"}.fa-bluetooth-b:before{content:"\f294"}.fa-percent:before{content:"\f295"}.fa-gitlab:before{content:"\f296"}.fa-wpbeginner:before{content:"\f297"}.fa-wpforms:before{content:"\f298"}.fa-envira:before{content:"\f299"}.fa-universal-access:before{content:"\f29a"}.fa-wheelchair-alt:before{content:"\f29b"}.fa-question-circle-o:before{content:"\f29c"}.fa-blind:before{content:"\f29d"}.fa-audio-description:before{content:"\f29e"}.fa-volume-control-phone:before{content:"\f2a0"}.fa-braille:before{content:"\f2a1"}.fa-assistive-listening-systems:before{content:"\f2a2"}.fa-asl-interpreting:before,.fa-american-sign-language-interpreting:before{content:"\f2a3"}.fa-deafness:before,.fa-hard-of-hearing:before,.fa-deaf:before{content:"\f2a4"}.fa-glide:before{content:"\f2a5"}.fa-glide-g:before{content:"\f2a6"}.fa-signing:before,.fa-sign-language:before{content:"\f2a7"}.fa-low-vision:before{content:"\f2a8"}.fa-viadeo:before{content:"\f2a9"}.fa-viadeo-square:before{content:"\f2aa"}.fa-snapchat:before{content:"\f2ab"}.fa-snapchat-ghost:before{content:"\f2ac"}.fa-snapchat-square:before{content:"\f2ad"}.fa-pied-piper:before{content:"\f2ae"}.fa-first-order:before{content:"\f2b0"}.fa-yoast:before{content:"\f2b1"}.fa-themeisle:before{content:"\f2b2"}.fa-google-plus-circle:before,.fa-google-plus-official:before{content:"\f2b3"}.fa-fa:before,.fa-font-awesome:before{content:"\f2b4"}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0, 0, 0, 0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}
diff --git a/themes/bootstrap3/css/vendor/ol/ol.css b/themes/bootstrap3/css/vendor/ol/ol.css
new file mode 100644
index 0000000000000000000000000000000000000000..251dcb463dca4cc989d9e3b50c43ff3c536645f6
--- /dev/null
+++ b/themes/bootstrap3/css/vendor/ol/ol.css
@@ -0,0 +1 @@
+.ol-control,.ol-scale-line{position:absolute;padding:2px}.ol-box{box-sizing:border-box;border-radius:2px;border:2px solid #00f}.ol-mouse-position{top:8px;right:8px;position:absolute}.ol-scale-line{background:rgba(0,60,136,.3);border-radius:4px;bottom:8px;left:8px}.ol-scale-line-inner{border:1px solid #eee;border-top:none;color:#eee;font-size:10px;text-align:center;margin:1px;will-change:contents,width}.ol-overlay-container{will-change:left,right,top,bottom}.ol-unsupported{display:none}.ol-viewport .ol-unselectable{-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-tap-highlight-color:transparent}.ol-control{background-color:rgba(255,255,255,.4);border-radius:4px}.ol-control:hover{background-color:rgba(255,255,255,.6)}.ol-zoom{top:.5em;left:.5em}.ol-rotate{top:.5em;right:.5em;transition:opacity .25s linear,visibility 0s linear}.ol-rotate.ol-hidden{opacity:0;visibility:hidden;transition:opacity .25s linear,visibility 0s linear .25s}.ol-zoom-extent{top:4.643em;left:.5em}.ol-full-screen{right:.5em;top:.5em}@media print{.ol-control{display:none}}.ol-control button{display:block;margin:1px;padding:0;color:#fff;font-size:1.14em;font-weight:700;text-decoration:none;text-align:center;height:1.375em;width:1.375em;line-height:.4em;background-color:rgba(0,60,136,.5);border:none;border-radius:2px}.ol-control button::-moz-focus-inner{border:none;padding:0}.ol-zoom-extent button{line-height:1.4em}.ol-compass{display:block;font-weight:400;font-size:1.2em;will-change:transform}.ol-touch .ol-control button{font-size:1.5em}.ol-touch .ol-zoom-extent{top:5.5em}.ol-control button:focus,.ol-control button:hover{text-decoration:none;background-color:rgba(0,60,136,.7)}.ol-zoom .ol-zoom-in{border-radius:2px 2px 0 0}.ol-zoom .ol-zoom-out{border-radius:0 0 2px 2px}.ol-attribution{text-align:right;bottom:.5em;right:.5em;max-width:calc(100% - 1.3em)}.ol-attribution ul{margin:0;padding:0 .5em;font-size:.7rem;line-height:1.375em;color:#000;text-shadow:0 0 2px #fff}.ol-attribution li{display:inline;list-style:none;line-height:inherit}.ol-attribution li:not(:last-child):after{content:" "}.ol-attribution img{max-height:2em;max-width:inherit;vertical-align:middle}.ol-attribution button,.ol-attribution ul{display:inline-block}.ol-attribution.ol-collapsed ul{display:none}.ol-attribution.ol-logo-only ul{display:block}.ol-attribution:not(.ol-collapsed){background:rgba(255,255,255,.8)}.ol-attribution.ol-uncollapsible{bottom:0;right:0;border-radius:4px 0 0;height:1.1em;line-height:1em}.ol-attribution.ol-logo-only{background:0 0;bottom:.4em;height:1.1em;line-height:1em}.ol-attribution.ol-uncollapsible img{margin-top:-.2em;max-height:1.6em}.ol-attribution.ol-logo-only button,.ol-attribution.ol-uncollapsible button{display:none}.ol-zoomslider{top:4.5em;left:.5em;height:200px}.ol-zoomslider button{position:relative;height:10px}.ol-touch .ol-zoomslider{top:5.5em}.ol-overviewmap{left:.5em;bottom:.5em}.ol-overviewmap.ol-uncollapsible{bottom:0;left:0;border-radius:0 4px 0 0}.ol-overviewmap .ol-overviewmap-map,.ol-overviewmap button{display:inline-block}.ol-overviewmap .ol-overviewmap-map{border:1px solid #7b98bc;height:150px;margin:2px;width:150px}.ol-overviewmap:not(.ol-collapsed) button{bottom:1px;left:2px;position:absolute}.ol-overviewmap.ol-collapsed .ol-overviewmap-map,.ol-overviewmap.ol-uncollapsible button{display:none}.ol-overviewmap:not(.ol-collapsed){background:rgba(255,255,255,.8)}.ol-overviewmap-box{border:2px dotted rgba(0,60,136,.7)}
diff --git a/themes/bootstrap3/css/vudl.css b/themes/bootstrap3/css/vudl.css
deleted file mode 100644
index 95fb012f944f817cd95565b58d3207f2438ded35..0000000000000000000000000000000000000000
--- a/themes/bootstrap3/css/vudl.css
+++ /dev/null
@@ -1,61 +0,0 @@
-.grid-btn {font-size:14px}
-.sibling-form .navbar-nav {display:table;float:none;margin:auto}
-.sibling-form .navbar-nav li {margin:0 4px}
-.sibling-form .navbar {min-height:1px}
-.sibling-form .navbar-nav > li > a {
-  padding-top:6px;
-  padding-bottom:6px;
-}
-
-/* Navbar buttons */
-.sibling-form .navbar-nav li a.btn.btn-default:active,
-.sibling-form .navbar-nav li a.btn.btn-default:focus,
-.sibling-form .navbar-nav li a.btn.btn-default:hover {
-  color:#FFF;
-  background-color:#333;
-  border-color:#ADADAD;
-}
-
-.panel {border:1px solid #DDD}
-.panel .panel-heading {background:#EEE}
-.panel-collapse {max-height:500px;overflow-y:auto}
-
-.details {text-align:center}
-.details .copyright {margin-bottom:1em}
-.details .description {margin-top:1em;text-align:left}
-.details .table {text-align:left}
-
-.page-link {
-  display:block;
-  margin:1em 0;
-  text-align:center;
-  width:100%;
-}
-.page-link.active {cursor:pointer}
-.page-link.loading {color:#059}
-.page-link.selected {background:#D9EDF7}
-.page-link .fa {font-size:7em}
-
-.page-grid {text-align:center}
-
-.panel .panel-heading a,
-.panel .panel-heading a i.fa,
-.nav-tabs .static.opener a {
-  cursor:pointer;
-}
-
-#view #preview {max-width:100%}
-.front {position:absolute;left:10%;top:150px;width:80%;z-index:5}
-
-#zoomy {background:#F0F0F0}
-
-#allFiles .btn {display:block;margin:auto;max-width:400px}
-#download-button .details {font-size:14px}
-div.xml { display:block;font:10pt Courier;text-align:left;padding-left:1em }
-div.xml.collapsed > div { display:none;margin-left:2px }
-
-.fa.fa-file:before {content: "\f0f6";}
-.fa.file-audio:before {content: "\f1c7";}
-.fa.file-msexcel:before {content: "\f1c3";}
-.fa.file-msword:before {content: "\f1c2";}
-.fa.file-pdf:before {content: "\f1c1";}
\ No newline at end of file
diff --git a/themes/bootstrap3/js/advanced_search.js b/themes/bootstrap3/js/advanced_search.js
index aa9e6a4aaaf344399f36729e666190e47ce39e86..af944a0c8820df20791938079d150fb5e387a1a0 100644
--- a/themes/bootstrap3/js/advanced_search.js
+++ b/themes/bootstrap3/js/advanced_search.js
@@ -1,40 +1,38 @@
+/*exported addGroup, addSearch, deleteGroup, deleteSearch */
 var nextGroup = 0;
 var groupLength = [];
 
-function addSearch(group, fieldValues)
-{
-  if(typeof fieldValues === "undefined") {
-    fieldValues = {};
-  }
+function addSearch(group, _fieldValues) {
+  var fieldValues = _fieldValues || {};
   // Build the new search
-  var inputID = group+'_'+groupLength[group];
+  var inputID = group + '_' + groupLength[group];
   var $newSearch = $($('#new_search_template').html());
 
-  $newSearch.attr('id', 'search'+inputID);
+  $newSearch.attr('id', 'search' + inputID);
   $newSearch.find('input.form-control')
-    .attr('id', 'search_lookfor'+inputID)
-    .attr('name', 'lookfor'+group+'[]')
+    .attr('id', 'search_lookfor' + inputID)
+    .attr('name', 'lookfor' + group + '[]')
     .attr('value', '');
   $newSearch.find('select.type option:first-child').attr('selected', 1);
   $newSearch.find('select.type')
-    .attr('id', 'search_type'+inputID)
-    .attr('name', 'type'+group+'[]');
+    .attr('id', 'search_type' + inputID)
+    .attr('name', 'type' + group + '[]');
   $newSearch.find('.close a')
-    .attr('onClick', 'deleteSearch('+group+','+groupLength[group]+')');
+    .attr('onClick', 'deleteSearch(' + group + ',' + groupLength[group] + ')');
   // Preset Values
-  if(typeof fieldValues.term !== "undefined") {
+  if (typeof fieldValues.term !== "undefined") {
     $newSearch.find('input.form-control').attr('value', fieldValues.term);
   }
-  if(typeof fieldValues.field !== "undefined") {
-    $newSearch.find('select.type option[value="'+fieldValues.field+'"]').attr('selected', 1);
+  if (typeof fieldValues.field !== "undefined") {
+    $newSearch.find('select.type option[value="' + fieldValues.field + '"]').attr('selected', 1);
   }
   if (typeof fieldValues.op !== "undefined") {
-    $newSearch.find('select.op option[value="'+fieldValues.op+'"]').attr('selected', 1);
+    $newSearch.find('select.op option[value="' + fieldValues.op + '"]').attr('selected', 1);
   }
   // Insert it
   $("#group" + group + "Holder").before($newSearch);
   // Individual search ops (for searches like EDS)
-  if (groupLength[group] == 0) {
+  if (groupLength[group] === 0) {
     $newSearch.find('.first-op')
       .attr('name', 'op' + group + '[]')
       .removeClass('hidden');
@@ -47,62 +45,60 @@ function addSearch(group, fieldValues)
     $newSearch.find('.first-op').remove();
     $newSearch.find('label').remove();
     // Show x if we have more than one search inputs
-    $('#group'+group+' .search .close').removeClass('hidden');
+    $('#group' + group + ' .search .close').removeClass('hidden');
   }
   groupLength[group]++;
 }
 
-function deleteSearch(group, sindex)
-{
-  for(var i=sindex;i<groupLength[group]-1;i++) {
-    var $search0 = $('#search'+group+'_'+i);
-    var $search1 = $('#search'+group+'_'+(i+1));
+function deleteSearch(group, sindex) {
+  for (var i = sindex; i < groupLength[group] - 1; i++) {
+    var $search0 = $('#search' + group + '_' + i);
+    var $search1 = $('#search' + group + '_' + (i + 1));
     $search0.find('input').val($search1.find('input').val());
     var select0 = $search0.find('select')[0];
     var select1 = $search1.find('select')[0];
     select0.selectedIndex = select1.selectedIndex;
   }
-  if(groupLength[group] > 1) {
+  if (groupLength[group] > 1) {
     groupLength[group]--;
-    $('#search'+group+'_'+groupLength[group]).remove();
-    if(groupLength[group] == 1) {
-      $('#group'+group+' .search .close').addClass('hidden'); // Hide x
+    $('#search' + group + '_' + groupLength[group]).remove();
+    if (groupLength[group] === 1) {
+      $('#group' + group + ' .search .close').addClass('hidden'); // Hide x
     }
   }
 }
 
-function addGroup(firstTerm, firstField, join)
-{
-  if (firstTerm  == undefined) {firstTerm  = '';}
-  if (firstField == undefined) {firstField = '';}
-  if (join       == undefined) {join       = '';}
+function addGroup(_firstTerm, _firstField, _join) {
+  var firstTerm = _firstTerm || '';
+  var firstField = _firstField || '';
+  var join = _join || '';
 
   var $newGroup = $($('#new_group_template').html());
-  $newGroup.attr('id', 'group'+nextGroup);
+  $newGroup.attr('id', 'group' + nextGroup);
   $newGroup.find('.search_place_holder')
-    .attr('id', 'group'+nextGroup+'Holder')
+    .attr('id', 'group' + nextGroup + 'Holder')
     .removeClass('hidden');
   $newGroup.find('.add_search_link')
-    .attr('id', 'add_search_link_'+nextGroup)
-    .attr('onClick', 'addSearch('+nextGroup+')')
+    .attr('id', 'add_search_link_' + nextGroup)
+    .attr('onClick', 'addSearch(' + nextGroup + ')')
     .removeClass('hidden');
   $newGroup.find('.group-close')
-    .attr('onClick', 'deleteGroup('+nextGroup+')');
+    .attr('onClick', 'deleteGroup(' + nextGroup + ')');
   $newGroup.find('select.form-control')
-    .attr('id', 'search_bool'+nextGroup)
-    .attr('name', 'bool'+nextGroup+'[]');
+    .attr('id', 'search_bool' + nextGroup)
+    .attr('name', 'bool' + nextGroup + '[]');
   $newGroup.find('.search_bool')
-    .attr('for', 'search_bool'+nextGroup);
-  if(join.length > 0) {
-    $newGroup.find('option[value="'+join+'"]').attr('selected', 1);
+    .attr('for', 'search_bool' + nextGroup);
+  if (join.length > 0) {
+    $newGroup.find('option[value="' + join + '"]').attr('selected', 1);
   }
   // Insert
   $('#groupPlaceHolder').before($newGroup);
   // Populate
   groupLength[nextGroup] = 0;
-  addSearch(nextGroup, {term:firstTerm, field:firstField});
+  addSearch(nextGroup, {term: firstTerm, field: firstField});
   // Show join menu
-  if(nextGroup > 0) {
+  if (nextGroup > 0) {
     $('#groupJoin').removeClass('hidden');
     // Show x
     $('.group .group-close').removeClass('hidden');
@@ -110,37 +106,20 @@ function addGroup(firstTerm, firstField, join)
   return nextGroup++;
 }
 
-function deleteGroup(group)
-{
+function deleteGroup(group) {
   // Find the group and remove it
   $("#group" + group).remove();
   // If the last group was removed, add an empty group
-  if($('.group').length == 0) {
+  if ($('.group').length === 0) {
     addGroup();
-  } else if($('#advSearchForm .group').length == 1) {
+  } else if ($('#advSearchForm .group').length === 1) {
     $('#groupJoin').addClass('hidden'); // Hide join menu
     $('.group .group-close').addClass('hidden'); // Hide x
   }
 }
 
-// Fired by onclick event
-function deleteGroupJS(group)
-{
-  var groupNum = group.id.replace("delete_link_", "");
-  deleteGroup(groupNum);
-  return false;
-}
-
-// Fired by onclick event
-function addSearchJS(group)
-{
-  var groupNum = group.id.replace("add_search_link_", "");
-  addSearch(groupNum);
-  return false;
-}
-
-$(document).ready(function() {
-  $('.clear-btn').click(function() {
+$(document).ready(function advSearchReady() {
+  $('.clear-btn').click(function clearBtnClick() {
     $('input[type="text"]').val('');
     $("option:selected").removeAttr("selected");
     $("#illustrated_-1").click();
diff --git a/themes/bootstrap3/js/autocomplete.js b/themes/bootstrap3/js/autocomplete.js
index 43f84437b9278f9951bb046e374dceac0ee3e74e..731b6b63c3609da6dec562f46e1484c1a2e72c2b 100644
--- a/themes/bootstrap3/js/autocomplete.js
+++ b/themes/bootstrap3/js/autocomplete.js
@@ -1,21 +1,19 @@
-/*global jQuery, window, document, console, setTimeout, clearTimeout */
 /**
- * crhallberg/autocomplete.js 0.15
+ * crhallberg/autocomplete.js 0.15.1
  * ~ @crhallberg
  */
-(function ( $ ) {
+(function autocomplete( $ ) {
   var cache = {},
-      element = false,
-      input = false,
-      options = {
-        ajaxDelay: 200,
-        cache: true,
-        hidingClass: 'hidden',
-        highlight: true,
-        loadingString: 'Loading...',
-        maxResults: 20,
-        minLength: 3
-      };
+    element = false,
+    options = {
+      ajaxDelay: 200,
+      cache: true,
+      hidingClass: 'hidden',
+      highlight: true,
+      loadingString: 'Loading...',
+      maxResults: 20,
+      minLength: 3
+    };
 
   var xhr = false;
 
@@ -45,16 +43,16 @@
     hide();
   }
 
-  function createList(data, input) {
+  function createList(fulldata, input) {
     // Limit results
-    data = data.slice(0, Math.min(options.maxResults, data.length));
+    var data = fulldata.slice(0, Math.min(options.maxResults, fulldata.length));
     input.data('length', data.length);
     // highlighting setup
     // escape term for regex - https://github.com/sindresorhus/escape-string-regexp/blob/master/index.js
     var escapedTerm = input.val().replace(/[|\\{}()\[\]\^$+*?.]/g, '\\$&');
-    var regex = new RegExp('('+escapedTerm+')', 'ig');
+    var regex = new RegExp('(' + escapedTerm + ')', 'ig');
     var shell = $('<div/>');
-    for (var i=0; i<data.length; i++) {
+    for (var i = 0; i < data.length; i++) {
       if (typeof data[i] === 'string') {
         data[i] = {value: data[i]};
       }
@@ -79,9 +77,9 @@
       shell.append(item);
     }
     element.html(shell);
-    element.find('.item').mousedown(function() {
+    element.find('.item').mousedown(function acItemClick() {
       populate($(this).data(), input, {mouse: true});
-      setTimeout(function() {
+      setTimeout(function acClickDelay() {
         input.focus();
         hide();
       }, 10);
@@ -92,7 +90,7 @@
   function search(input) {
     if (xhr) { xhr.abort(); }
     if (input.val().length >= options.minLength) {
-      element.html('<i class="item loading">'+options.loadingString+'</i>');
+      element.html('<i class="item loading">' + options.loadingString + '</i>');
       show();
       align(input);
       var term = input.val();
@@ -104,7 +102,7 @@
           createList(cache[cid][term], input);
         }
       } else {
-        options.handler(input, function(data) {
+        options.handler(input, function achandlerCallback(data) {
           cache[cid][term] = data;
           if (data.length === 0) {
             hide();
@@ -119,11 +117,11 @@
     }
   }
 
-  function setup(input, element) {
-    if (typeof element === 'undefined') {
+  function setup(input) {
+    if ($('.autocomplete-results').length === 0) {
       element = $('<div/>')
         .addClass('autocomplete-results hidden')
-        .html('<i class="item loading">'+options.loadingString+'</i>');
+        .html('<i class="item loading">' + options.loadingString + '</i>');
       align(input);
       $(document.body).append(element);
     }
@@ -132,25 +130,25 @@
     input.data('length', 0);
 
     if (options.cache) {
-      var cid = Math.floor(Math.random()*1000);
+      var cid = Math.floor(Math.random() * 1000);
       input.data('cache-id', cid);
       cache[cid] = {};
     }
 
-    input.blur(function(e) {
+    input.blur(function acinputBlur(e) {
       if (e.target.acitem) {
         setTimeout(hide, 10);
       } else {
         hide();
       }
     });
-    input.click(function() {
+    input.click(function acinputClick() {
       search(input, element);
     });
-    input.focus(function() {
+    input.focus(function acinputFocus() {
       search(input, element);
     });
-    input.keyup(function(event) {
+    input.keyup(function acinputKeyup(event) {
       // Ignore navigation keys
       // - Ignore control functions
       if (event.ctrlKey || event.which === 17) {
@@ -161,29 +159,29 @@
         return;
       }
       switch (event.which) {
-        case 9:    // tab
-        case 13:   // enter
-        case 16:   // shift
-        case 20:   // caps lock
-        case 27:   // esc
-        case 33:   // page up
-        case 34:   // page down
-        case 35:   // end
-        case 36:   // home
-        case 37:   // arrows
-        case 38:
-        case 39:
-        case 40:
-        case 45:   // insert
-        case 144:  // num lock
-        case 145:  // scroll lock
-        case 19:   // pause/break
-          return;
-        default:
-          search(input, element);
+      case 9:    // tab
+      case 13:   // enter
+      case 16:   // shift
+      case 20:   // caps lock
+      case 27:   // esc
+      case 33:   // page up
+      case 34:   // page down
+      case 35:   // end
+      case 36:   // home
+      case 37:   // arrows
+      case 38:
+      case 39:
+      case 40:
+      case 45:   // insert
+      case 144:  // num lock
+      case 145:  // scroll lock
+      case 19:   // pause/break
+        return;
+      default:
+        search(input, element);
       }
     });
-    input.keydown(function(event) {
+    input.keydown(function acinputKeydown(event) {
       // - Ignore control functions
       if (event.ctrlKey || event.which === 17) {
         return;
@@ -191,45 +189,45 @@
       var position = $(this).data('selected');
       switch (event.which) {
         // arrow keys through items
-        case 38: // up key
-          event.preventDefault();
+      case 38: // up key
+        event.preventDefault();
+        element.find('.item.selected').removeClass('selected');
+        if (position-- > 0) {
+          element.find('.item:eq(' + position + ')').addClass('selected');
+        }
+        $(this).data('selected', position);
+        break;
+      case 40: // down key
+        event.preventDefault();
+        if (element.hasClass(options.hidingClass)) {
+          search(input, element);
+        } else if (position < input.data('length') - 1) {
+          position++;
           element.find('.item.selected').removeClass('selected');
-          if (position-- > 0) {
-            element.find('.item:eq('+position+')').addClass('selected');
-          }
+          element.find('.item:eq(' + position + ')').addClass('selected');
           $(this).data('selected', position);
-          break;
-        case 40: // down key
+        }
+        break;
+        // enter to nav or populate
+      case 9:
+      case 13:
+        var selected = element.find('.item.selected');
+        if (selected.length > 0) {
           event.preventDefault();
-          if (element.hasClass(options.hidingClass)) {
-            search(input, element);
-          } else if (position < input.data('length')-1) {
-            position++;
+          if (event.which === 13 && selected.attr('href')) {
+            window.location.assign(selected.attr('href'));
+          } else {
+            populate(selected.data(), $(this), {key: true});
             element.find('.item.selected').removeClass('selected');
-            element.find('.item:eq('+position+')').addClass('selected');
-            $(this).data('selected', position);
+            $(this).data('selected', -1);
           }
-          break;
-        // enter to nav or populate
-        case 9:
-        case 13:
-          var selected = element.find('.item.selected');
-          if (selected.length > 0) {
-            event.preventDefault();
-            if (event.which === 13 && selected.attr('href')) {
-              window.location.assign(selected.attr('href'));
-            } else {
-              populate(selected.data(), $(this), {key: true});
-              element.find('.item.selected').removeClass('selected');
-              $(this).data('selected', -1);
-            }
-          }
-          break;
+        }
+        break;
         // hide on escape
-        case 27:
-          hide();
-          $(this).data('selected', -1);
-          break;
+      case 27:
+        hide();
+        $(this).data('selected', -1);
+        break;
       }
     });
 
@@ -238,12 +236,11 @@
     return element;
   }
 
-  $.fn.autocomplete = function(settings) {
-
+  $.fn.autocomplete = function acJQuery(settings) {
 
-    return this.each(function() {
+    return this.each(function acJQueryEach() {
 
-      input = $(this);
+      var input = $(this);
 
       if (typeof settings === "string") {
         if (settings === "show") {
@@ -261,12 +258,7 @@
         return this;
       } else {
         options = $.extend( {}, options, settings );
-        element = $('.autocomplete-results');
-        if (element.length == 0) {
-          element = setup(input);
-        } else {
-          setup(input, element);
-        }
+        setup(input);
       }
 
       return input;
@@ -275,11 +267,11 @@
   };
 
   var timer = false;
-  $.fn.autocomplete.ajax = function(ops) {
+  $.fn.autocomplete.ajax = function acAjax(ops) {
     if (timer) { clearTimeout(timer); }
     if (xhr) { xhr.abort(); }
     timer = setTimeout(
-      function() { xhr = $.ajax(ops); },
+      function acajaxDelay() { xhr = $.ajax(ops); },
       options.ajaxDelay
     );
   };
diff --git a/themes/bootstrap3/js/cart.js b/themes/bootstrap3/js/cart.js
index c79f58b1f30affe3907fca5cabd37ec660ce2992..ca69eea6e9d1770f5880f43364e731b69fe1b57b 100644
--- a/themes/bootstrap3/js/cart.js
+++ b/themes/bootstrap3/js/cart.js
@@ -1,186 +1,197 @@
 /*global Cookies, VuFind */
+/*exported cartFormHandler */
 
-VuFind.register('cart', function() {
+VuFind.register('cart', function Cart() {
   var _COOKIE = 'vufind_cart';
   var _COOKIE_SOURCES = 'vufind_cart_src';
   var _COOKIE_DELIM = "\t";
   var _COOKIE_DOMAIN = false;
+  var _COOKIE_PATH = '/';
 
-  var setDomain = function(domain) {
+  function setDomain(domain) {
     _COOKIE_DOMAIN = domain;
-  };
+  }
 
-  var _uniqueArray = function(op) {
+  function setCookiePath(path) {
+    _COOKIE_PATH = path;
+  }
+
+  function _uniqueArray(op) {
     var ret = [];
-    for(var i=0;i<op.length;i++) {
-      if(ret.indexOf(op[i]) < 0) {
+    for (var i = 0; i < op.length; i++) {
+      if (ret.indexOf(op[i]) < 0) {
         ret.push(op[i]);
       }
     }
     return ret;
-  };
+  }
 
-  var _getItems = function() {
+  function _getItems() {
     var items = Cookies.getItem(_COOKIE);
-    if(items) {
+    if (items) {
       return items.split(_COOKIE_DELIM);
     }
     return [];
-  };
-  var _getSources = function() {
+  }
+  function _getSources() {
     var items = Cookies.getItem(_COOKIE_SOURCES);
-    if(items) {
+    if (items) {
       return items.split(_COOKIE_DELIM);
     }
     return [];
-  };
-  var getFullItems = function() {
+  }
+  function getFullItems() {
     var items = _getItems();
     var sources = _getSources();
     var full = [];
-    if(items.length == 0) {
+    if (items.length === 0) {
       return [];
     }
-    for(var i=items.length;i--;) {
-      full[full.length] = sources[items[i].charCodeAt(0)-65]+'|'+items[i].substr(1);
+    for (var i = items.length; i--;) {
+      full[full.length] = sources[items[i].charCodeAt(0) - 65] + '|' + items[i].substr(1);
     }
     return full;
-  };
+  }
 
-  var updateCount = function() {
-    var items = _getItems();
+  function updateCount() {
+    var items = VuFind.cart.getFullItems();
     $('#cartItems strong').html(items.length);
-  };
-
-  var addItem = function(id,source) {
-    if(!source) {
-      source = VuFind.defaultSearchBackend;
+    if (items.length === parseInt(VuFind.translate('bookbagMax'), 10)) {
+      $('#cartItems .full').removeClass('hidden');
+    } else {
+      $('#cartItems .full').addClass('hidden');
     }
+  }
+
+  function addItem(id, _source) {
+    var source = _source || VuFind.defaultSearchBackend;
     var cartItems = _getItems();
     var cartSources = _getSources();
+    if (cartItems.length >= parseInt(VuFind.translate('bookbagMax'), 10)) {
+      return false;
+    }
     var sIndex = cartSources.indexOf(source);
-    if(sIndex < 0) {
+    if (sIndex < 0) {
       // Add source to source cookie
-      cartItems[cartItems.length] = String.fromCharCode(65+cartSources.length) + id;
+      cartItems[cartItems.length] = String.fromCharCode(65 + cartSources.length) + id;
       cartSources[cartSources.length] = source;
-      Cookies.setItem(_COOKIE_SOURCES, cartSources.join(_COOKIE_DELIM), false, '/', _COOKIE_DOMAIN);
+      Cookies.setItem(_COOKIE_SOURCES, cartSources.join(_COOKIE_DELIM), false, _COOKIE_PATH, _COOKIE_DOMAIN);
     } else {
-      cartItems[cartItems.length] = String.fromCharCode(65+sIndex) + id;
+      cartItems[cartItems.length] = String.fromCharCode(65 + sIndex) + id;
     }
-    Cookies.setItem(_COOKIE, $.unique(cartItems).join(_COOKIE_DELIM), false, '/', _COOKIE_DOMAIN);
+    Cookies.setItem(_COOKIE, _uniqueArray(cartItems).join(_COOKIE_DELIM), false, _COOKIE_PATH, _COOKIE_DOMAIN);
     updateCount();
     return true;
-  };
-  var removeItem = function(id,source) {
+  }
+  function removeItem(id, source) {
     var cartItems = _getItems();
     var cartSources = _getSources();
     // Find
-    var cartIndex = cartItems.indexOf(String.fromCharCode(65+cartSources.indexOf(source))+id);
-    if(cartIndex > -1) {
-      var sourceIndex = cartItems[cartIndex].charCodeAt(0)-65;
-      var cartItem = cartItems[cartIndex];
+    var cartIndex = cartItems.indexOf(String.fromCharCode(65 + cartSources.indexOf(source)) + id);
+    if (cartIndex > -1) {
+      var sourceIndex = cartItems[cartIndex].charCodeAt(0) - 65;
       var saveSource = false;
-      for(var i=cartItems.length;i--;) {
-        if(i==cartIndex) {
+      for (var i = cartItems.length; i--;) {
+        if (i === cartIndex) {
           continue;
         }
         // If this source is shared by another, keep it
-        if(cartItems[i].charCodeAt(0)-65 == sourceIndex) {
+        if (cartItems[i].charCodeAt(0) - 65 === sourceIndex) {
           saveSource = true;
           break;
         }
       }
-      cartItems.splice(cartIndex,1);
+      cartItems.splice(cartIndex, 1);
       // Remove unused sources
-      if(!saveSource) {
+      if (!saveSource) {
         var oldSources = cartSources.slice(0);
-        cartSources.splice(sourceIndex,1);
+        cartSources.splice(sourceIndex, 1);
         // Adjust source index characters
-        for(var j=cartItems.length;j--;) {
-          var si = cartItems[j].charCodeAt(0)-65;
+        for (var j = cartItems.length; j--;) {
+          var si = cartItems[j].charCodeAt(0) - 65;
           var ni = cartSources.indexOf(oldSources[si]);
-          cartItems[j] = String.fromCharCode(65+ni)+cartItems[j].substring(1);
+          cartItems[j] = String.fromCharCode(65 + ni) + cartItems[j].substring(1);
         }
       }
-      if(cartItems.length > 0) {
-        Cookies.setItem(_COOKIE, _uniqueArray(cartItems).join(_COOKIE_DELIM), false, '/', _COOKIE_DOMAIN);
-        Cookies.setItem(_COOKIE_SOURCES, _uniqueArray(cartSources).join(_COOKIE_DELIM), false, '/', _COOKIE_DOMAIN);
+      if (cartItems.length > 0) {
+        Cookies.setItem(_COOKIE, _uniqueArray(cartItems).join(_COOKIE_DELIM), false, _COOKIE_PATH, _COOKIE_DOMAIN);
+        Cookies.setItem(_COOKIE_SOURCES, _uniqueArray(cartSources).join(_COOKIE_DELIM), false, _COOKIE_PATH, _COOKIE_DOMAIN);
       } else {
-        Cookies.removeItem(_COOKIE, '/', _COOKIE_DOMAIN);
-        Cookies.removeItem(_COOKIE_SOURCES, '/', _COOKIE_DOMAIN);
+        Cookies.removeItem(_COOKIE, _COOKIE_PATH, _COOKIE_DOMAIN);
+        Cookies.removeItem(_COOKIE_SOURCES, _COOKIE_PATH, _COOKIE_DOMAIN);
       }
       updateCount();
       return true;
     }
     return false;
-  };
+  }
 
   var _cartNotificationTimeout = false;
-  var _registerUpdate = function($form) {
-    if($form) {
-      $("#updateCart, #bottom_updateCart").unbind('click').click(function(){
+  function _registerUpdate($form) {
+    if ($form) {
+      $("#updateCart, #bottom_updateCart").unbind('click').click(function cartUpdate() {
         var elId = this.id;
         var selectedBoxes = $("input[name='ids[]']:checked", $form);
         var selected = [];
-        $(selectedBoxes).each(function(i) {
+        $(selectedBoxes).each(function cartCheckboxValues(i) {
           selected[i] = this.value;
         });
         if (selected.length > 0) {
-          var inCart = 0;
           var msg = "";
           var orig = getFullItems();
-          $(selected).each(function(i) {
-            for (var x in orig) {
-              if (this == orig[x]) {
-                inCart++;
-                return;
-              }
-            }
+          $(selected).each(function cartCheckedItemsAdd() {
             var data = this.split('|');
             addItem(data[1], data[0]);
           });
           var updated = getFullItems();
           var added = updated.length - orig.length;
+          var inCart = selected.length - added;
           msg += added + " " + VuFind.translate('itemsAddBag');
+          if (updated.length >= parseInt(VuFind.translate('bookbagMax'), 10)) {
+            msg += "<br/>" + VuFind.translate('bookbagFull');
+          }
           if (inCart > 0 && orig.length > 0) {
             msg += "<br/>" + inCart + " " + VuFind.translate('itemsInBag');
           }
-          if (updated.length >= VuFind.translate('bookbagMax')) {
-            msg += "<br/>" + VuFind.translate('bookbagFull');
-          }
-          $('#'+elId).data('bs.popover').options.content = msg;
+          $('#' + elId).data('bs.popover').options.content = msg;
           $('#cartItems strong').html(updated.length);
         } else {
-          $('#'+elId).data('bs.popover').options.content = VuFind.translate('bulk_noitems_advice');
+          $('#' + elId).data('bs.popover').options.content = VuFind.translate('bulk_noitems_advice');
         }
-        $('#'+elId).popover('show');
+        $('#' + elId).popover('show');
         if (_cartNotificationTimeout !== false) {
           clearTimeout(_cartNotificationTimeout);
         }
-        _cartNotificationTimeout = setTimeout(function() {
-          $('#'+elId).popover('hide');
+        _cartNotificationTimeout = setTimeout(function notificationHide() {
+          $('#' + elId).popover('hide');
         }, 5000);
         return false;
       });
     }
-  };
+  }
 
-  var init = function() {
+  function init() {
     // Record buttons
     var $cartId = $('.cartId');
-    if($cartId.length > 0) {
-      $cartId.each(function() {
+    if ($cartId.length > 0) {
+      $cartId.each(function cartIdEach() {
         var cartId = this.value.split('|');
         var currentId = cartId[1];
         var currentSource = cartId[0];
         var $parent = $(this).parent();
         $parent.find('.cart-add.correct,.cart-remove.correct').removeClass('correct hidden');
-        $parent.find('.cart-add').click(function() {
-          addItem(currentId,currentSource);
-          $parent.find('.cart-add,.cart-remove').toggleClass('hidden');
+        $parent.find('.cart-add').click(function cartAddClick() {
+          if (addItem(currentId, currentSource)) {
+            $parent.find('.cart-add,.cart-remove').toggleClass('hidden');
+          } else {
+            $parent.popover({content: VuFind.translate('bookbagFull')});
+            setTimeout(function recordCartFullHide() {
+              $parent.popover('hide');
+            }, 5000);
+          }
         });
-        $parent.find('.cart-remove').click(function() {
-          removeItem(currentId,currentSource);
+        $parent.find('.cart-remove').click(function cartRemoveClick() {
+          removeItem(currentId, currentSource);
           $parent.find('.cart-add,.cart-remove').toggleClass('hidden');
         });
       });
@@ -189,8 +200,9 @@ VuFind.register('cart', function() {
       var $form = $('form[name="bulkActionForm"]');
       _registerUpdate($form);
     }
-    $("#updateCart, #bottom_updateCart").popover({content:'', html:true, trigger:'manual'});
-  };
+    $("#updateCart, #bottom_updateCart").popover({content: '', html: true, trigger: 'manual'});
+    updateCount();
+  }
 
   // Reveal
   return {
@@ -200,6 +212,7 @@ VuFind.register('cart', function() {
     getFullItems: getFullItems,
     updateCount: updateCount,
     setDomain: setDomain,
+    setCookiePath: setCookiePath,
     // Init
     init: init
   };
@@ -207,10 +220,12 @@ VuFind.register('cart', function() {
 
 // Building an array and checking indexes prevents a race situation
 // We want to prioritize empty over printing
-var cartFormHandler = function(event, data) {
+function cartFormHandler(event, data) {
   var keys = [];
   for (var i in data) {
-    keys.push(data[i].name);
+    if (data.hasOwnProperty(i)) {
+      keys.push(data[i].name);
+    }
   }
   if (keys.indexOf('ids[]') === -1) {
     return null;
@@ -218,6 +233,6 @@ var cartFormHandler = function(event, data) {
   if (keys.indexOf('print') > -1) {
     return true;
   }
-};
+}
 
 document.addEventListener('VuFind.lightbox.closed', VuFind.cart.updateCount, false);
diff --git a/themes/bootstrap3/js/check_item_statuses.js b/themes/bootstrap3/js/check_item_statuses.js
index 8581d13f563f27362c8d02a8849c31d3bd44d29e..48be5222529e5ff50ec73d20fd7c592a3abb9deb 100644
--- a/themes/bootstrap3/js/check_item_statuses.js
+++ b/themes/bootstrap3/js/check_item_statuses.js
@@ -1,18 +1,26 @@
 /*global VuFind */
-
-function checkItemStatuses(container) {
-  if (typeof(container) == 'undefined') {
-    container = $('body');
+function linkCallnumbers(callnumber, callnumber_handler) {
+  if (callnumber_handler) {
+    var cns = callnumber.split(',\t');
+    for (var i = 0; i < cns.length; i++) {
+      cns[i] = '<a href="' + VuFind.path + '/Alphabrowse/Home?source=' + encodeURI(callnumber_handler) + '&amp;from=' + encodeURI(cns[i]) + '">' + cns[i] + '</a>';
+    }
+    return cns.join(',\t');
   }
+  return callnumber;
+}
+
+function checkItemStatuses(_container) {
+  var container = _container || $('body');
 
   var elements = {};
-  var data = $.map(container.find('.ajaxItem'), function(record) {
-    if ($(record).find('.hiddenId').length == 0) {
+  var data = $.map(container.find('.ajaxItem'), function ajaxItemMap(record) {
+    if ($(record).find('.hiddenId').length === 0) {
       return null;
     }
     var datum = $(record).find('.hiddenId').val();
     if (typeof elements[datum] === 'undefined') {
-        elements[datum] = $();
+      elements[datum] = $();
     }
     elements[datum] = elements[datum].add($(record));
     return datum;
@@ -26,10 +34,10 @@ function checkItemStatuses(container) {
     dataType: 'json',
     method: 'POST',
     url: VuFind.path + '/AJAX/JSON?method=getItemStatuses',
-    data: {'id':data}
+    data: {'id': data}
   })
-  .done(function(response) {
-    $.each(response.data, function(i, result) {
+  .done(function checkItemStatusDone(response) {
+    $.each(response.data, function checkItemDoneEach(i, result) {
       var item = elements[result.id];
       if (!item) {
         return;
@@ -58,51 +66,51 @@ function checkItemStatuses(container) {
         item.find('.hideIfDetailed').addClass('hidden');
         item.find('.location').addClass('hidden');
         var locationListHTML = "";
-        for (var x=0; x<result.locationList.length; x++) {
+        for (var x = 0; x < result.locationList.length; x++) {
           locationListHTML += '<div class="groupLocation">';
           if (result.locationList[x].availability) {
-            locationListHTML += '<i class="fa fa-ok text-success"></i> <span class="text-success">'
+            locationListHTML += '<i class="fa fa-ok text-success" aria-hidden="true"></i> <span class="text-success">'
               + result.locationList[x].location + '</span> ';
           } else if (typeof(result.locationList[x].status_unknown) !== 'undefined'
               && result.locationList[x].status_unknown
           ) {
             if (result.locationList[x].location) {
-              locationListHTML += '<i class="fa fa-status-unknown text-warning"></i> <span class="text-warning">'
+              locationListHTML += '<i class="fa fa-status-unknown text-warning" aria-hidden="true"></i> <span class="text-warning">'
                 + result.locationList[x].location + '</span> ';
             }
           } else {
-            locationListHTML += '<i class="fa fa-remove text-danger"></i> <span class="text-danger"">'
+            locationListHTML += '<i class="fa fa-remove text-danger" aria-hidden="true"></i> <span class="text-danger"">'
               + result.locationList[x].location + '</span> ';
           }
           locationListHTML += '</div>';
           locationListHTML += '<div class="groupCallnumber">';
           locationListHTML += (result.locationList[x].callnumbers)
-               ?  result.locationList[x].callnumbers : '';
+               ? linkCallnumbers(result.locationList[x].callnumbers, result.locationList[x].callnumber_handler) : '';
           locationListHTML += '</div>';
         }
         item.find('.locationDetails').removeClass('hidden');
         item.find('.locationDetails').empty().append(locationListHTML);
       } else {
         // Default case -- load call number and location into appropriate containers:
-        item.find('.callnumber').empty().append(result.callnumber+'<br/>');
+        item.find('.callnumber').empty().append(linkCallnumbers(result.callnumber, result.callnumber_handler) + '<br/>');
         item.find('.location').empty().append(
-          result.reserve == 'true'
-          ? result.reserve_message
-          : result.location
+          result.reserve === 'true'
+            ? result.reserve_message
+            : result.location
         );
       }
     });
 
     $(".ajax-availability").removeClass('ajax-availability');
   })
-  .fail(function(response, textStatus) {
+  .fail(function checkItemStatusFail(response, textStatus) {
     $('.ajax-availability').empty();
-    if (textStatus == 'abort' || typeof response.responseJSON === 'undefined') { return; }
+    if (textStatus === 'abort' || typeof response.responseJSON === 'undefined') { return; }
     // display the error message on each of the ajax status place holder
     $('.ajax-availability').append(response.responseJSON.data).addClass('text-danger');
   });
 }
 
-$(document).ready(function() {
+$(document).ready(function checkItemStatusReady() {
   checkItemStatuses();
 });
diff --git a/themes/bootstrap3/js/check_save_statuses.js b/themes/bootstrap3/js/check_save_statuses.js
index bc6dbf878de220fd73b685b94b89829d66961854..411b88d8cec72b6ff4d56e2dad5b2fbd1498839f 100644
--- a/themes/bootstrap3/js/check_save_statuses.js
+++ b/themes/bootstrap3/js/check_save_statuses.js
@@ -1,20 +1,21 @@
-/*global htmlEncode, VuFind, userIsLoggedIn */
+/*global htmlEncode, userIsLoggedIn, VuFind */
 
-function checkSaveStatuses(container) {
+function checkSaveStatuses(_container) {
   if (!userIsLoggedIn) {
     return;
   }
-  if (typeof(container) == 'undefined') {
-    container = $('body');
-  }
+  var container = _container || $('body');
 
   var elements = {};
-  var data = $.map(container.find('.result,.record'), function(record) {
-    if ($(record).find('.hiddenId').length == 0 || $(record).find('.hiddenSource').length == 0) {
+  var data = $.map(container.find('.result,.record'), function checkSaveRecordMap(record) {
+    if ($(record).find('.hiddenId').length === 0 || $(record).find('.hiddenSource').length === 0) {
       return null;
     }
-    var datum = {'id':$(record).find('.hiddenId').val(), 'source':$(record).find('.hiddenSource')[0].value};
-    var key = datum.source+'|'+datum.id;
+    var datum = {
+      id: $(record).find('.hiddenId').val(),
+      source: $(record).find('.hiddenSource')[0].value
+    };
+    var key = datum.source + '|' + datum.id;
     if (typeof elements[key] === 'undefined') {
       elements[key] = $();
     }
@@ -24,34 +25,36 @@ function checkSaveStatuses(container) {
   if (data.length) {
     var ids = [];
     var srcs = [];
-    for (var i = 0; i < data.length; i++) {
-      ids.push(data[i].id);
-      srcs.push(data[i].source);
+    for (var d = 0; d < data.length; d++) {
+      ids.push(data[d].id);
+      srcs.push(data[d].source);
     }
     $.ajax({
       dataType: 'json',
       method: 'POST',
       url: VuFind.path + '/AJAX/JSON?method=getSaveStatuses',
-      data: {'id':ids, 'source':srcs}
+      data: {id: ids, source: srcs}
     })
-    .done(function(response) {
+    .done(function checkSaveStatusDone(response) {
       for (var sel in response.data) {
-        var list = elements[sel];
-        if (!list) {
-          list = $('.savedLists');
-        }
-        var html = list.find('strong')[0].outerHTML+'<ul>';
-        for (var i=0; i<response.data[sel].length; i++) {
-          html += '<li><a href="' + response.data[sel][i].list_url + '">'
-            + htmlEncode(response.data[sel][i].list_title) + '</a></li>';
+        if (response.data.hasOwnProperty(sel)) {
+          var list = elements[sel];
+          if (!list) {
+            list = $('.savedLists');
+          }
+          var html = list.find('strong')[0].outerHTML + '<ul>';
+          for (var i = 0; i < response.data[sel].length; i++) {
+            html += '<li><a href="' + response.data[sel][i].list_url + '">'
+              + htmlEncode(response.data[sel][i].list_title) + '</a></li>';
+          }
+          html += '</ul>';
+          list.html(html).removeClass('hidden');
         }
-        html += '</ul>';
-        list.html(html).removeClass('hidden');
       }
     });
   }
 }
 
-$(document).ready(function() {
+$(document).ready(function checkSaveStatusFail() {
   checkSaveStatuses();
 });
diff --git a/themes/bootstrap3/js/collection_record.js b/themes/bootstrap3/js/collection_record.js
index 9574b6f9c7be0bb9ed70952d4ffe6c444e3381ff..e7616b788eeff3c764b07195521b1d0cd2f18386 100644
--- a/themes/bootstrap3/js/collection_record.js
+++ b/themes/bootstrap3/js/collection_record.js
@@ -5,16 +5,16 @@ function toggleCollectionInfo() {
 function showMoreInfoToggle() {
   // no rows in table? don't bother!
   if ($("#collectionInfo").find('tr').length < 1) {
-      return;
+    return;
   }
   toggleCollectionInfo();
   $("#moreInfoToggle").removeClass('hidden');
-  $("#moreInfoToggle").click(function(e) {
+  $("#moreInfoToggle").click(function moreInfoToggleClick(e) {
     e.preventDefault();
     toggleCollectionInfo();
   });
 }
 
-$(document).ready(function() {
+$(document).ready(function collectionRecordReady() {
   showMoreInfoToggle();
 });
\ No newline at end of file
diff --git a/themes/bootstrap3/js/combined-search.js b/themes/bootstrap3/js/combined-search.js
index 3477225688bc1b73be98cf8c6d03e669d223b371..dd4c75d9117eb51142b04e646f50ccf42edc8fba 100644
--- a/themes/bootstrap3/js/combined-search.js
+++ b/themes/bootstrap3/js/combined-search.js
@@ -1,8 +1,8 @@
 /*global VuFind, checkItemStatuses, checkSaveStatuses */
-VuFind.combinedSearch = (function() {
-  var init = function(container, url) {
-    container.load(url, '', function(responseText) {
-      if (responseText.length == 0) {
+VuFind.combinedSearch = (function CombinedSearch() {
+  var init = function init(container, url) {
+    container.load(url, '', function containerLoad(responseText) {
+      if (responseText.length === 0) {
         container.hide();
       } else {
         VuFind.openurl.init(container);
@@ -11,7 +11,7 @@ VuFind.combinedSearch = (function() {
       }
     });
   };
-  
+
   var my = {
     init: init
   };
diff --git a/themes/bootstrap3/js/common.js b/themes/bootstrap3/js/common.js
index 9531bfc310aa9c22283d49d1320108f275a461a8..3ab48e620494ccc57962867d4841af81c7d1076c 100644
--- a/themes/bootstrap3/js/common.js
+++ b/themes/bootstrap3/js/common.js
@@ -1,41 +1,62 @@
-/*global btoa, console, hexEncode, isPhoneNumberValid, Lightbox, rc4Encrypt, unescape */
+/*global grecaptcha, isPhoneNumberValid */
+/*exported VuFind, htmlEncode, deparam, moreFacets, lessFacets, phoneNumberFormHandler, recaptchaOnLoad, bulkFormHandler */
 
 // IE 9< console polyfill
-window.console = window.console || {log: function () {}};
+window.console = window.console || {log: function polyfillLog() {}};
 
-var VuFind = (function() {
+var VuFind = (function VuFind() {
   var defaultSearchBackend = null;
   var path = null;
   var _initialized = false;
   var _submodules = [];
   var _translations = {};
 
-  var register = function(name, module) {
+  var register = function register(name, module) {
     if (_submodules.indexOf(name) === -1) {
       _submodules.push(name);
       this[name] = typeof module == 'function' ? module() : module;
     }
     // If the object has already initialized, we should auto-init on register:
-    if (_initialized) {
+    if (_initialized && this[name].init) {
       this[name].init();
     }
   };
-  var init = function() {
-    for (var i=0; i<_submodules.length; i++) {
-      this[_submodules[i]].init();
+  var init = function init() {
+    for (var i = 0; i < _submodules.length; i++) {
+      if (this[_submodules[i]].init) {
+        this[_submodules[i]].init();
+      }
     }
     _initialized = true;
   };
 
-  var addTranslations = function(s) {
+  var addTranslations = function addTranslations(s) {
     for (var i in s) {
-      _translations[i] = s[i];
+      if (s.hasOwnProperty(i)) {
+        _translations[i] = s[i];
+      }
     }
   };
-  var translate = function(op) {
+  var translate = function translate(op) {
     return _translations[op] || op;
   };
 
+  /**
+   * Reload the page without causing trouble with POST parameters while keeping hash
+   */
+  var refreshPage = function refreshPage() {
+    var parts = window.location.href.split('#');
+    if (typeof parts[1] === 'undefined') {
+      window.location.href = window.location.href;
+    } else {
+      var href = parts[0];
+      // Force reload with a timestamp
+      href += href.indexOf('?') === -1 ? '?_=' : '&_=';
+      href += new Date().getTime() + '#' + parts[1];
+      window.location.href = href;
+    }
+  };
+
   //Reveal
   return {
     defaultSearchBackend: defaultSearchBackend,
@@ -43,6 +64,7 @@ var VuFind = (function() {
 
     addTranslations: addTranslations,
     init: init,
+    refreshPage: refreshPage,
     register: register,
     translate: translate
   };
@@ -51,19 +73,19 @@ var VuFind = (function() {
 /* --- GLOBAL FUNCTIONS --- */
 function htmlEncode(value) {
   if (value) {
-    return jQuery('<div />').text(value).html();
+    return $('<div />').text(value).html();
   } else {
     return '';
   }
 }
-function extractClassParams(str) {
-  str = $(str).attr('class');
+function extractClassParams(selector) {
+  var str = $(selector).attr('class');
   if (typeof str === "undefined") {
     return [];
   }
   var params = {};
   var classes = str.split(/\s+/);
-  for(var i = 0; i < classes.length; i++) {
+  for (var i = 0; i < classes.length; i++) {
     if (classes[i].indexOf(':') > 0) {
       var pair = classes[i].split(':');
       params[pair[0]] = pair[1];
@@ -73,7 +95,7 @@ function extractClassParams(str) {
 }
 // Turn GET string into array
 function deparam(url) {
-  if(!url.match(/\?|&/)) {
+  if (!url.match(/\?|&/)) {
     return [];
   }
   var request = {};
@@ -81,12 +103,12 @@ function deparam(url) {
   for (var i = 0; i < pairs.length; i++) {
     var pair = pairs[i].split('=');
     var name = decodeURIComponent(pair[0].replace(/\+/g, ' '));
-    if(name.length == 0) {
+    if (name.length === 0) {
       continue;
     }
-    if(name.substring(name.length-2) == '[]') {
-      name = name.substring(0,name.length-2);
-      if(!request[name]) {
+    if (name.substring(name.length - 2) === '[]') {
+      name = name.substring(0, name.length - 2);
+      if (!request[name]) {
         request[name] = [];
       }
       request[name].push(decodeURIComponent(pair[1].replace(/\+/g, ' ')));
@@ -99,12 +121,24 @@ function deparam(url) {
 
 // Sidebar
 function moreFacets(id) {
-  $('.'+id).removeClass('hidden');
-  $('#more-'+id).addClass('hidden');
+  $('.' + id).removeClass('hidden');
+  $('#more-' + id).addClass('hidden');
+  return false;
 }
 function lessFacets(id) {
-  $('.'+id).addClass('hidden');
-  $('#more-'+id).removeClass('hidden');
+  $('.' + id).addClass('hidden');
+  $('#more-' + id).removeClass('hidden');
+  return false;
+}
+function facetSessionStorage(e) {
+  var source = $('#result0 .hiddenSource').val();
+  var id = e.target.id;
+  var key = 'sidefacet-' + source + id;
+  if (!sessionStorage.getItem(key)) {
+    sessionStorage.setItem(key, document.getElementById(id).className);
+  } else {
+    sessionStorage.removeItem(key);
+  }
 }
 
 // Phone number validation
@@ -112,8 +146,8 @@ function phoneNumberFormHandler(numID, regionCode) {
   var phoneInput = document.getElementById(numID);
   var number = phoneInput.value;
   var valid = isPhoneNumberValid(number, regionCode);
-  if(valid != true) {
-    if(typeof valid === 'string') {
+  if (valid !== true) {
+    if (typeof valid === 'string') {
       valid = VuFind.translate(valid);
     } else {
       valid = VuFind.translate('libphonenumber_invalid');
@@ -127,14 +161,23 @@ function phoneNumberFormHandler(numID, regionCode) {
   }
 }
 
+// Setup captchas after Google script loads
+function recaptchaOnLoad() {
+  if (typeof grecaptcha !== 'undefined') {
+    var captchas = $('.g-recaptcha:empty');
+    for (var i = 0; i < captchas.length; i++) {
+      $(captchas[i]).data('captchaId', grecaptcha.render(captchas[i], $(captchas[i]).data()));
+    }
+  }
+}
+
 function bulkFormHandler(event, data) {
-  if ($('.checkbox-select-item:checked,checkbox-select-all:checked').length == 0) {
+  if ($('.checkbox-select-item:checked,checkbox-select-all:checked').length === 0) {
     VuFind.lightbox.alert(VuFind.translate('bulk_noitems_advice'), 'danger');
     return false;
   }
-  var keys = [];
   for (var i in data) {
-    if ('print' == data[i].name) {
+    if ('print' === data[i].name) {
       return true;
     }
   }
@@ -142,16 +185,17 @@ function bulkFormHandler(event, data) {
 
 // Ready functions
 function setupOffcanvas() {
-  if($('.sidebar').length > 0) {
-    $('[data-toggle="offcanvas"]').click(function () {
+  if ($('.sidebar').length > 0) {
+    $('[data-toggle="offcanvas"]').click(function offcanvasClick() {
       $('body.offcanvas').toggleClass('active');
       var active = $('body.offcanvas').hasClass('active');
       var right = $('body.offcanvas').hasClass('offcanvas-right');
-      if((active && !right) || (!active && right)) {
+      if ((active && !right) || (!active && right)) {
         $('.offcanvas-toggle .fa').removeClass('fa-chevron-right').addClass('fa-chevron-left');
       } else {
         $('.offcanvas-toggle .fa').removeClass('fa-chevron-left').addClass('fa-chevron-right');
       }
+      $('.offcanvas-toggle .fa').attr('title', VuFind.translate(active ? 'sidebar_close' : 'sidebar_expand'));
     });
     $('[data-toggle="offcanvas"]').click().click();
   } else {
@@ -161,32 +205,32 @@ function setupOffcanvas() {
 
 function setupAutocomplete() {
   // Search autocomplete
-  $('.autocomplete').each(function(i, op) {
+  $('.autocomplete').each(function autocompleteSetup(i, op) {
     $(op).autocomplete({
       maxResults: 10,
-      loadingString: VuFind.translate('loading')+'...',
-      handler: function(input, cb) {
+      loadingString: VuFind.translate('loading') + '...',
+      handler: function vufindACHandler(input, cb) {
         var query = input.val();
         var searcher = extractClassParams(input);
         var hiddenFilters = [];
-        $(input).closest('.searchForm').find('input[name="hiddenFilters[]"]').each(function() {
+        $(input).closest('.searchForm').find('input[name="hiddenFilters[]"]').each(function hiddenFiltersEach() {
           hiddenFilters.push($(this).val());
         });
         $.fn.autocomplete.ajax({
           url: VuFind.path + '/AJAX/JSON',
           data: {
-            q:query,
-            method:'getACSuggestions',
-            searcher:searcher['searcher'],
-            type:searcher['type'] ? searcher['type'] : $(input).closest('.searchForm').find('.searchForm_type').val(),
-            hiddenFilters:hiddenFilters
+            q: query,
+            method: 'getACSuggestions',
+            searcher: searcher.searcher,
+            type: searcher.type ? searcher.type : $(input).closest('.searchForm').find('.searchForm_type').val(),
+            hiddenFilters: hiddenFilters
           },
-          dataType:'json',
-          success: function(json) {
+          dataType: 'json',
+          success: function autocompleteJSON(json) {
             if (json.data.length > 0) {
               var datums = [];
-              for (var i=0;i<json.data.length;i++) {
-                datums.push(json.data[i]);
+              for (var j = 0; j < json.data.length; j++) {
+                datums.push(json.data[j]);
               }
               cb(datums);
             } else {
@@ -198,56 +242,96 @@ function setupAutocomplete() {
     });
   });
   // Update autocomplete on type change
-  $('.searchForm_type').change(function() {
+  $('.searchForm_type').change(function searchTypeChange() {
     var $lookfor = $(this).closest('.searchForm').find('.searchForm_lookfor[name]');
     $lookfor.autocomplete('clear cache');
-    $lookfor.focus();
   });
 }
 
 /**
  * Handle arrow keys to jump to next record
- * @returns {undefined}
  */
 function keyboardShortcuts() {
-    var $searchform = $('.searchForm_lookfor');
-    if ($('.pager').length > 0) {
-        $(window).keydown(function(e) {
-          if (!$searchform.is(':focus')) {
-            var $target = null;
-            switch (e.keyCode) {
-              case 37: // left arrow key
-                $target = $('.pager').find('a.previous');
-                if ($target.length > 0) {
-                    $target[0].click();
-                    return;
-                }
-                break;
-              case 38: // up arrow key
-                if (e.ctrlKey) {
-                    $target = $('.pager').find('a.backtosearch');
-                    if ($target.length > 0) {
-                        $target[0].click();
-                        return;
-                    }
-                }
-                break;
-              case 39: //right arrow key
-                $target = $('.pager').find('a.next');
-                if ($target.length > 0) {
-                    $target[0].click();
-                    return;
-                }
-                break;
-              case 40: // down arrow key
-                break;
+  var $searchform = $('.searchForm_lookfor');
+  if ($('.pager').length > 0) {
+    $(window).keydown(function shortcutKeyDown(e) {
+      if (!$searchform.is(':focus')) {
+        var $target = null;
+        switch (e.keyCode) {
+        case 37: // left arrow key
+          $target = $('.pager').find('a.previous');
+          if ($target.length > 0) {
+            $target[0].click();
+            return;
+          }
+          break;
+        case 38: // up arrow key
+          if (e.ctrlKey) {
+            $target = $('.pager').find('a.backtosearch');
+            if ($target.length > 0) {
+              $target[0].click();
+              return;
             }
           }
-        });
+          break;
+        case 39: //right arrow key
+          $target = $('.pager').find('a.next');
+          if ($target.length > 0) {
+            $target[0].click();
+            return;
+          }
+          break;
+        case 40: // down arrow key
+          break;
+        }
+      }
+    });
+  }
+}
+
+/**
+ * Setup facets
+ */
+function setupFacets() {
+  // Advanced facets
+  $('.facetAND a,.facetOR a').click(function facetBlocking() {
+    $(this).closest('.collapse').html('<div class="list-group-item">' + VuFind.translate('loading') + '...</div>');
+    window.location.assign($(this).attr('href'));
+  });
+
+  // Side facet status saving
+  $('.facet.list-group .collapse').each(function openStoredFacets(index, item) {
+    var source = $('#result0 .hiddenSource').val();
+    var storedItem = sessionStorage.getItem('sidefacet-' + source + item.id);
+    if (storedItem) {
+      var saveTransition = $.support.transition;
+      try {
+        $.support.transition = false;
+        if ((' ' + storedItem + ' ').indexOf(' in ') > -1) {
+          $(item).collapse('show');
+        } else {
+          $(item).collapse('hide');
+        }
+      } finally {
+        $.support.transition = saveTransition;    
+      }
     }
+  });
+  $('.facet.list-group .collapse').on('shown.bs.collapse', facetSessionStorage);
+  $('.facet.list-group .collapse').on('hidden.bs.collapse', facetSessionStorage);
 }
 
-$(document).ready(function() {
+function setupIeSupport() {
+  // Disable Bootstrap modal focus enforce on IE since it breaks Recaptcha.
+  // Cannot use conditional comments since IE 11 doesn't support them but still has
+  // the issue
+  var ua = window.navigator.userAgent;
+  if (ua.indexOf('MSIE') || ua.indexOf('Trident/')) {
+    $.fn.modal.Constructor.prototype.enforceFocus = function emptyEnforceFocus() { };
+  }
+}
+
+$(document).ready(function commonDocReady() {
   // Start up all of our submodules
   VuFind.init();
   // Setup search autocomplete
@@ -258,18 +342,18 @@ $(document).ready(function() {
   keyboardShortcuts();
 
   // support "jump menu" dropdown boxes
-  $('select.jumpMenu').change(function(){ $(this).parent('form').submit(); });
+  $('select.jumpMenu').change(function jumpMenu(){ $(this).parent('form').submit(); });
 
   // Checkbox select all
-  $('.checkbox-select-all').change(function() {
+  $('.checkbox-select-all').change(function selectAllCheckboxes() {
     $(this).closest('form').find('.checkbox-select-item').prop('checked', this.checked);
   });
-  $('.checkbox-select-item').change(function() {
+  $('.checkbox-select-item').change(function selectAllDisable() {
     $(this).closest('form').find('.checkbox-select-all').prop('checked', false);
   });
 
   // handle QR code links
-  $('a.qrcodeLink').click(function() {
+  $('a.qrcodeLink').click(function qrcodeToggle() {
     if ($(this).hasClass("active")) {
       $(this).html(VuFind.translate('qrcode_show')).removeClass("active");
     } else {
@@ -277,7 +361,7 @@ $(document).ready(function() {
     }
 
     var holder = $(this).next('.qrcode');
-    if (holder.find('img').length == 0) {
+    if (holder.find('img').length === 0) {
       // We need to insert the QRCode image
       var template = holder.find('.qrCodeImgTag').html();
       holder.html(template);
@@ -288,18 +372,26 @@ $(document).ready(function() {
 
   // Print
   var url = window.location.href;
-  if(url.indexOf('?' + 'print' + '=') != -1  || url.indexOf('&' + 'print' + '=') != -1) {
+  if (url.indexOf('?' + 'print' + '=') !== -1 || url.indexOf('&' + 'print' + '=') !== -1) {
     $("link[media='print']").attr("media", "all");
-    $(document).ajaxStop(function() {
+    $(document).ajaxStop(function triggerPrint() {
       window.print();
     });
     // Make an ajax call to ensure that ajaxStop is triggered
     $.getJSON(VuFind.path + '/AJAX/JSON', {method: 'keepAlive'});
   }
 
-  // Advanced facets
-  $('.facetOR').click(function() {
-    $(this).closest('.collapse').html('<div class="list-group-item">'+VuFind.translate('loading')+'...</div>');
-    window.location.assign($(this).attr('href'));
+  setupFacets();
+
+  // retain filter sessionStorage
+  $('.searchFormKeepFilters').click(function retainFiltersInSessionStorage() {
+    sessionStorage.setItem('vufind_retain_filters', this.checked ? 'true' : 'false');
   });
+  if (sessionStorage.getItem('vufind_retain_filters')) {
+    var state = (sessionStorage.getItem('vufind_retain_filters') === 'true');
+    $('.searchFormKeepFilters').prop('checked', state);
+    $('.applied-filter').prop('checked', state);
+  }
+
+  setupIeSupport();
 });
diff --git a/themes/bootstrap3/js/embedGBS.js b/themes/bootstrap3/js/embedGBS.js
index ae81464ebeed323816e8dcb0e1ea3296bf22fbc7..515a6fca77b7be78268304c174d0c782592808f2 100644
--- a/themes/bootstrap3/js/embedGBS.js
+++ b/themes/bootstrap3/js/embedGBS.js
@@ -2,7 +2,7 @@
 
 // we don't need to wait for dom ready since lang is in the dom root
 var lang = document.documentElement.getAttribute('lang');
-google.load("books", "0", {"language":lang});
+google.load("books", "0", { language: lang });
 
 function initialize() {
   var bibkeys = getBibKeyString().split(/\s+/);
diff --git a/themes/bootstrap3/js/embedded_record.js b/themes/bootstrap3/js/embedded_record.js
new file mode 100644
index 0000000000000000000000000000000000000000..7140732402195b9bbefb9232e65bf4dfff2b0c62
--- /dev/null
+++ b/themes/bootstrap3/js/embedded_record.js
@@ -0,0 +1,238 @@
+/*global checkSaveStatuses, registerAjaxCommentRecord, registerTabEvents, syn_get_widget, VuFind */
+VuFind.register('embedded', function embedded() {
+  var _STORAGEKEY = 'vufind_search_open';
+  var _SEPERATOR = ':::';
+  var _DELIM = ',';
+  var _STATUS = {};
+
+  function saveStatusToStorage() {
+    var storage = [];
+    var str;
+    for (str in _STATUS) {
+      if ({}.hasOwnProperty.call(_STATUS, str)) {
+        if (_STATUS[str]) {
+          str += _SEPERATOR + _STATUS[str];
+        }
+        storage.push(str);
+      }
+    }
+    sessionStorage.setItem(_STORAGEKEY, $.unique(storage).join(_DELIM));
+  }
+  function addToStorage(id, tab) {
+    _STATUS[id] = tab;
+    saveStatusToStorage();
+  }
+  function removeFromStorage(id) {
+    if (delete _STATUS[id]) {
+      saveStatusToStorage();
+    }
+  }
+
+  function ajaxLoadTab(tabid, _click) {
+    var click = _click || false;
+    var $tab = $('#' + tabid);
+    var $result = $tab.closest('.result');
+    if ($result.length === 0) {
+      return true;
+    }
+    var id = $result.find('.hiddenId')[0].value;
+    var source = $result.find('.hiddenSource')[0].value;
+    if ($tab.parent().hasClass('noajax')) {
+      if ($tab.is('a')) {
+        // tab case:
+        window.location.href = $tab.attr('href');
+      } else {
+        // accordion case:
+        window.location.href = $tab.find('a').attr('data-href');
+      }
+      return false;
+    }
+    var urlroot;
+    if (source === VuFind.defaultSearchBackend) {
+      urlroot = 'Record';
+    } else {
+      urlroot = source.charAt(0).toUpperCase() + source.slice(1).toLowerCase() + 'Record';
+    }
+    if (!$tab.hasClass('loaded')) {
+      $('#' + tabid + '-content').html(
+        '<i class="fa fa-spinner fa-spin"></i> ' + VuFind.translate('loading') + '...'
+      );
+      var tab = tabid.split('_');
+      tab = tab[0];
+      $.ajax({
+        url: VuFind.path + '/' + urlroot + '/' + encodeURIComponent(id) + '/AjaxTab',
+        type: 'POST',
+        data: { tab: tab },
+        success: function ajaxTabSuccess(data) {
+          var html = data.trim();
+          if (html.length > 0) {
+            $('#' + tabid + '-content').html(html);
+            registerTabEvents();
+          } else {
+            $('#' + tabid + '-content').html(VuFind.translate('collection_empty'));
+          }
+          if (typeof syn_get_widget === 'function') {
+            syn_get_widget();
+          }
+          $('#' + tabid).addClass('loaded');
+        }
+      });
+    }
+    if (click && !$tab.parent().hasClass('default')) {
+      $tab.click();
+    }
+    return true;
+  }
+
+  function toggleDataView(_link, tabid) {
+    var $link = $(_link);
+    var viewType = $link.attr('data-view');
+    // If full, return true
+    if (viewType === 'full') {
+      return true;
+    }
+    var result = $link.closest('.result');
+    var mediaBody = result.find('.media-body');
+    var shortNode = mediaBody.find('.short-view');
+    var longNode = mediaBody.find('.long-view');
+    // Insert new elements
+    if (!$link.hasClass('js-setup')) {
+      $link.prependTo(mediaBody);
+      result.addClass('embedded');
+      mediaBody.find('.short-view').addClass('collapse');
+      longNode = $('<div class="long-view collapse"></div>');
+      // Add loading status
+      shortNode
+        .before('<div class="loading hidden"><i class="fa fa-spin fa-spinner"></i> '
+                + VuFind.translate('loading') + '...</div>')
+        .before(longNode);
+      $link.addClass('js-setup');
+      longNode.on('show.bs.collapse', function embeddedExpand() {
+        $link.addClass('expanded');
+      });
+      longNode.on('hidden.bs.collapse', function embeddedCollapsed() {
+        $link.removeClass('expanded');
+      });
+    }
+    // Gather information
+    var divID = result.find('.hiddenId')[0].value;
+    // Toggle visibility
+    if (!longNode.is(':visible')) {
+      // AJAX for information
+      if (longNode.is(':empty')) {
+        var loadingNode = mediaBody.find('.loading');
+        loadingNode.removeClass('hidden');
+        $link.addClass('expanded');
+        $.ajax({
+          dataType: 'json',
+          url: VuFind.path + '/AJAX/JSON?' + $.param({
+            method: 'getRecordDetails',
+            id: divID,
+            type: viewType,
+            source: result.find('.hiddenSource')[0].value
+          }),
+          success: function getRecordDetailsSuccess(response) {
+            if (response.status === 'OK') {
+              // Insert tabs html
+              longNode.html(response.data);
+              // Hide loading
+              loadingNode.addClass('hidden');
+              longNode.collapse('show');
+              // Load first tab
+              if (tabid) {
+                ajaxLoadTab(tabid, true);
+              } else {
+                var $firstTab = $(longNode).find('.list-tab-toggle.active');
+                if ($firstTab.length === 0) {
+                  $firstTab = $(longNode).find('.list-tab-toggle:eq(0)');
+                }
+                ajaxLoadTab($firstTab.attr('id'), true);
+              }
+              // Bind tab clicks
+              longNode.find('.list-tab-toggle').click(function embeddedTabLoad() {
+                if (!$(this).parent().hasClass('noajax')) {
+                  addToStorage(divID, this.id);
+                }
+                return ajaxLoadTab(this.id);
+              });
+              longNode.find('[id^=usercomment]').find('input[type=submit]').unbind('click').click(
+                function embeddedComments() {
+                  return registerAjaxCommentRecord(
+                    longNode.find('[id^=usercomment]').find('input[type=submit]').closest('form')
+                  );
+                }
+              );
+              longNode.find('[data-background]').each(function setupEmbeddedBackgroundTabs(index, el) {
+                ajaxLoadTab(el.id, false);
+              });
+              // Add events to record toolbar
+              VuFind.lightbox.bind(longNode);
+              if (typeof checkSaveStatuses == 'function') {
+                checkSaveStatuses(longNode);
+              }
+            }
+          }
+        });
+      } else {
+        longNode.collapse('show');
+      }
+      shortNode.collapse('hide');
+      if (!$link.hasClass('auto')) {
+        addToStorage(divID, $(longNode).find('.list-tab-toggle.active').attr('id'));
+      } else {
+        $link.removeClass('auto');
+      }
+    } else {
+      shortNode.collapse('show');
+      longNode.collapse('hide');
+      removeFromStorage(divID);
+    }
+    return false;
+  }
+
+  function loadStorage() {
+    var storage = sessionStorage.getItem(_STORAGEKEY);
+    if (!storage) {
+      return;
+    }
+    var items = storage.split(_DELIM);
+    var doomed = [];
+    var hiddenIds;
+    var parts;
+    var result;
+    var i;
+    var j;
+    if (!storage) {
+      return;
+    }
+    hiddenIds = $('.hiddenId');
+    for (i = 0; i < items.length; i++) {
+      parts = items[i].split(_SEPERATOR);
+      _STATUS[parts[0]] = parts[1] || null;
+      result = null;
+      for (j = 0; j < hiddenIds.length; j++) {
+        if (hiddenIds[j].value === parts[0]) {
+          result = $(hiddenIds[j]).closest('.result');
+          break;
+        }
+      }
+      if (result === null) {
+        doomed.push(parts[0]);
+        continue;
+      }
+      var $link = result.find('.getFull');
+      $link.addClass('auto expanded');
+      toggleDataView($link, parts[1]);
+    }
+    for (i = 0; i < doomed.length; i++) {
+      removeFromStorage(doomed[i]);
+    }
+  }
+
+  function init() {
+    $('.getFull').click(function linkToggle() { return toggleDataView(this); });
+    loadStorage();
+  }
+
+  return { init: init };
+});
diff --git a/themes/bootstrap3/js/facets.js b/themes/bootstrap3/js/facets.js
index f906209f4c0748579e40c41d05f456ea01003b71..29573565242720c566573ed3228552e7d05faea3 100644
--- a/themes/bootstrap3/js/facets.js
+++ b/themes/bootstrap3/js/facets.js
@@ -1,9 +1,10 @@
 /*global htmlEncode, VuFind */
+/*exported initFacetTree */
 function buildFacetNodes(data, currentPath, allowExclude, excludeTitle, counts)
 {
   var json = [];
 
-  $(data).each(function() {
+  $(data).each(function facetNodesEach() {
     var html = '';
     if (!this.isApplied && counts) {
       html = '<span class="badge" style="float: right">' + this.count.toString().replace(/\B(?=(\d{3})+\b)/g, VuFind.translate('number_thousands_separator'));
@@ -11,7 +12,7 @@ function buildFacetNodes(data, currentPath, allowExclude, excludeTitle, counts)
         var excludeURL = currentPath + this.exclude;
         excludeURL.replace("'", "\\'");
         // Just to be safe
-        html += ' <a href="' + excludeURL + '" onclick="document.location.href=\'' + excludeURL + '\'; return false;" title="' + htmlEncode(excludeTitle) + '"><i class="fa fa-times"></i></a>';
+        html += ' <a href="' + excludeURL + '" onclick="document.location.href=\'' + excludeURL + '\'; return false;" title="' + htmlEncode(excludeTitle) + '"><i class="fa fa-times" title="' + VuFind.translate('Selected') + '"></i></a>';
       }
       html += '</span>';
     }
@@ -21,14 +22,14 @@ function buildFacetNodes(data, currentPath, allowExclude, excludeTitle, counts)
     url.replace("'", "\\'");
     html += '<span class="main' + (this.isApplied ? ' applied' : '') + '" title="' + htmlEncode(this.displayText) + '"'
       + ' onclick="document.location.href=\'' + url + '\'; return false;">';
-    if (this.operator == 'OR') {
+    if (this.operator === 'OR') {
       if (this.isApplied) {
-        html += '<i class="fa fa-check-square-o"></i>';
+        html += '<i class="fa fa-check-square-o" title="' + VuFind.translate('Selected') + '"></i>';
       } else {
-        html += '<i class="fa fa-square-o"></i>';
+        html += '<i class="fa fa-square-o" aria-hidden="true"></i>';
       }
     } else if (this.isApplied) {
-      html += '<i class="fa fa-check pull-right"></i>';
+      html += '<i class="fa fa-check pull-right" title="' + VuFind.translate('Selected') + '"></i>';
     }
     html += ' ' + this.displayText;
     html += '</span>';
@@ -68,9 +69,9 @@ function initFacetTree(treeNode, inSidebar)
   var query = window.location.href.split('?')[1];
 
   if (inSidebar) {
-    treeNode.prepend('<li class="list-group-item"><i class="fa fa-spinner fa-spin"></i></li>');
+    treeNode.prepend('<li class="list-group-item"><i class="fa fa-spinner fa-spin" aria-hidden="true"></i></li>');
   } else {
-    treeNode.prepend('<div><i class="fa fa-spinner fa-spin"></i><div>');
+    treeNode.prepend('<div><i class="fa fa-spinner fa-spin" aria-hidden="true"></i><div>');
   }
   $.getJSON(VuFind.path + '/AJAX/JSON?' + query,
     {
@@ -79,12 +80,12 @@ function initFacetTree(treeNode, inSidebar)
       facetSort: sort,
       facetOperator: operator
     },
-    function(response, textStatus) {
-      if (response.status == "OK") {
+    function getFacetData(response/*, textStatus*/) {
+      if (response.status === "OK") {
         var results = buildFacetNodes(response.data, currentPath, allowExclude, excludeTitle, inSidebar);
         treeNode.find('.fa-spinner').parent().remove();
         if (inSidebar) {
-          treeNode.on('loaded.jstree open_node.jstree', function (e, data) {
+          treeNode.on('loaded.jstree open_node.jstree', function treeNodeOpen(/*e, data*/) {
             treeNode.find('ul.jstree-container-ul > li.jstree-node').addClass('list-group-item');
           });
         }
@@ -97,3 +98,67 @@ function initFacetTree(treeNode, inSidebar)
     }
   );
 }
+
+/* --- Lightbox Facets --- */
+VuFind.register('lightbox_facets', function LightboxFacets() {
+  function lightboxFacetSorting() {
+    var sortButtons = $('.js-facet-sort');
+    function sortAjax(button) {
+      var sort = $(button).data('sort');
+      var list = $('#facet-list-' + sort);
+      if (list.find('.js-facet-item').length === 0) {
+        list.find('.js-facet-next-page').text(VuFind.translate('loading') + '...');
+        $.ajax(button.href + '&layout=lightbox')
+          .done(function facetSortTitleDone(data) {
+            list.prepend($('<span>' + data + '</span>').find('.js-facet-item'));
+            list.find('.js-facet-next-page').text(VuFind.translate('more') + ' ...');
+          });
+      }
+      $('.full-facet-list').addClass('hidden');
+      list.removeClass('hidden');
+      sortButtons.removeClass('active');
+    }
+    sortButtons.click(function facetSortButton() {
+      sortAjax(this);
+      $(this).addClass('active');
+      return false;
+    });
+  }
+
+  function setup() {
+    lightboxFacetSorting();
+    $('.js-facet-next-page').click(function facetLightboxMore() {
+      var button = $(this);
+      var page = parseInt(button.attr('data-page'), 10);
+      if (button.attr('disabled')) {
+        return false;
+      }
+      button.attr('disabled', 1);
+      button.text(VuFind.translate('loading') + '...');
+      $.ajax(this.href + '&layout=lightbox')
+        .done(function facetLightboxMoreDone(data) {
+          var htmlDiv = $('<div>' + data + '</div>');
+          var list = htmlDiv.find('.js-facet-item');
+          button.before(list);
+          if (list.length && htmlDiv.find('.js-facet-next-page').length) {
+            button.attr('data-page', page + 1);
+            button.attr('href', button.attr('href').replace(/facetpage=\d+/, 'facetpage=' + (page + 1)));
+            button.text(VuFind.translate('more') + ' ...');
+            button.removeAttr('disabled');
+          } else {
+            button.remove();
+          }
+        });
+      return false;
+    });
+    var margin = 230;
+    $('#modal').on('show.bs.modal', function facetListHeight() {
+      $('#modal .lightbox-scroll').css('max-height', window.innerHeight - margin);
+    });
+    $(window).resize(function facetListResize() {
+      $('#modal .lightbox-scroll').css('max-height', window.innerHeight - margin);
+    });
+  }
+
+  return { setup: setup };
+});
diff --git a/themes/bootstrap3/js/hierarchyTree.js b/themes/bootstrap3/js/hierarchyTree.js
index 3c228b4f8961083b84cba2a8536453f23c03cf14..316d26ddd47ba152db06d548b32c7b2e21b6ee28 100644
--- a/themes/bootstrap3/js/hierarchyTree.js
+++ b/themes/bootstrap3/js/hierarchyTree.js
@@ -1,13 +1,12 @@
-/*global hierarchySettings, VuFind */
+/*global VuFind */
 
 var hierarchyID, recordID, htmlID, hierarchyContext;
-var baseTreeSearchFullURL;
 
 /* Utility functions */
 function htmlEncodeId(id) {
   return id.replace(/\W/g, "-"); // Also change Hierarchy/TreeRenderer/JSTree.php
 }
-function html_entity_decode(string, quote_style) {
+function html_entity_decode(string) {
   var hash_map = {
     '&': '&amp;',
     '>': '&gt;',
@@ -16,25 +15,27 @@ function html_entity_decode(string, quote_style) {
   var tmp_str = string.toString();
 
   for (var symbol in hash_map) {
-    var entity = hash_map[symbol];
-    tmp_str = tmp_str.split(entity).join(symbol);
+    if (hash_map.hasOwnProperty(symbol)) {
+      var entity = hash_map[symbol];
+      tmp_str = tmp_str.split(entity).join(symbol);
+    }
   }
   tmp_str = tmp_str.split('&#039;').join("'");
 
   return tmp_str;
 }
 
-function getRecord(recordID) {
+function getRecord(id) {
   $.ajax({
-    url: VuFind.path + '/Hierarchy/GetRecord?' + $.param({id: recordID}),
+    url: VuFind.path + '/Hierarchy/GetRecord?' + $.param({id: id}),
     dataType: 'html'
   })
-  .done(function(response) {
+  .done(function getRecordDone(response) {
     $('#hierarchyRecord').html(html_entity_decode(response));
     // Remove the old path highlighting
     $('#hierarchyTree a').removeClass("jstree-highlight");
     // Add Current path highlighting
-    var jsTreeNode = $(":input[value='"+recordID+"']").parent();
+    var jsTreeNode = $(":input[value='" + id + "']").parent();
     jsTreeNode.children("a").addClass("jstree-highlight");
     jsTreeNode.parents("li").children("a").addClass("jstree-highlight");
   });
@@ -60,36 +61,35 @@ var searchAjax = false;
 function doTreeSearch() {
   $('#treeSearchLoadingImg').removeClass('hidden');
   var keyword = $("#treeSearchText").val();
-  var type = $("#treeSearchType").val();
-  if(keyword.length == 0) {
+  if (keyword.length === 0) {
     $('#hierarchyTree').find('.jstree-search').removeClass('jstree-search');
     var tree = $('#hierarchyTree').jstree(true);
     tree.close_all();
     tree._open_to(htmlID);
     $('#treeSearchLoadingImg').addClass('hidden');
   } else {
-    if(searchAjax) {
+    if (searchAjax) {
       searchAjax.abort();
     }
     searchAjax = $.ajax({
-      "url" : VuFind.path + '/Hierarchy/SearchTree?' + $.param({
-        'lookfor': keyword,
-        'hierarchyID': hierarchyID,
-        'type': $("#treeSearchType").val()
+      url: VuFind.path + '/Hierarchy/SearchTree?' + $.param({
+        lookfor: keyword,
+        hierarchyID: hierarchyID,
+        type: $("#treeSearchType").val()
       }) + "&format=true"
     })
-    .done(function(data) {
-      if(data.results.length > 0) {
+    .done(function searchTreeAjaxDone(data) {
+      if (data.results.length > 0) {
         $('#hierarchyTree').find('.jstree-search').removeClass('jstree-search');
-        var tree = $('#hierarchyTree').jstree(true);
-        tree.close_all();
-        for(var i=data.results.length;i--;) {
+        var jstree = $('#hierarchyTree').jstree(true);
+        jstree.close_all();
+        for (var i = data.results.length; i--;) {
           var id = htmlEncodeId(data.results[i]);
-          tree._open_to(id);
+          jstree._open_to(id);
         }
-        for(i=data.results.length;i--;) {
-          var tid = htmlEncodeId(data.results[i]);
-          $('#hierarchyTree').find('#'+tid).addClass('jstree-search');
+        for (var j = data.results.length; j--;) {
+          var tid = htmlEncodeId(data.results[j]);
+          $('#hierarchyTree').find('#' + tid).addClass('jstree-search');
         }
         changeNoResultLabel(false);
         changeLimitReachedLabel(data.limitReached);
@@ -103,21 +103,19 @@ function doTreeSearch() {
 
 function buildJSONNodes(xml) {
   var jsonNode = [];
-  $(xml).children('item').each(function() {
+  $(xml).children('item').each(function xmlTreeChildren() {
     var content = $(this).children('content');
     var id = content.children("name[class='JSTreeID']");
     var name = content.children('name[href]');
     jsonNode.push({
-      'id': htmlEncodeId(id.text()),
-      'text': name.text(),
-      'li_attr': {
-        'recordid': id.text()
-      },
-      'a_attr': {
-        'href': name.attr('href'),
-        'title': name.text()
+      id: htmlEncodeId(id.text()),
+      text: name.text(),
+      li_attr: { recordid: id.text() },
+      a_attr: {
+        href: name.attr('href'),
+        title: name.text()
       },
-      'type': name.attr('href').match(/\/Collection\//) ? 'collection' : 'record',
+      type: name.attr('href').match(/\/Collection\//) ? 'collection' : 'record',
       children: buildJSONNodes(this)
     });
   });
@@ -126,21 +124,21 @@ function buildJSONNodes(xml) {
 
 function buildTreeWithXml(cb) {
   $.ajax({
-    'url': VuFind.path + '/Hierarchy/GetTree',
-    'data': {
-      'hierarchyID': hierarchyID,
-      'id': recordID,
-      'context': hierarchyContext,
-      'mode': 'Tree'
+    url: VuFind.path + '/Hierarchy/GetTree',
+    data: {
+      hierarchyID: hierarchyID,
+      id: recordID,
+      context: hierarchyContext,
+      mode: 'Tree'
     }
   })
-  .done(function(xml) {
+  .done(function getTreeDone(xml) {
     var nodes = buildJSONNodes($(xml).find('root'));
     cb.call(this, nodes);
   });
 }
 
-$(document).ready(function() {
+$(document).ready(function hierarchyTreeReady() {
   // Code for the search button
   hierarchyID = $("#hierarchyTree").find(".hiddenHierarchyId")[0].value;
   recordID = $("#hierarchyTree").find(".hiddenRecordId")[0].value;
@@ -150,21 +148,21 @@ $(document).ready(function() {
   $("#hierarchyLoading").removeClass('hide');
 
   $("#hierarchyTree")
-    .bind("ready.jstree", function (event, data) {
+    .bind("ready.jstree", function jsTreeReady(/*event, data*/) {
       $("#hierarchyLoading").addClass('hide');
       var tree = $("#hierarchyTree").jstree(true);
       tree.select_node(htmlID);
       tree._open_to(htmlID);
 
-      if (hierarchyContext == "Collection") {
+      if (hierarchyContext === "Collection") {
         getRecord(recordID);
       }
 
-      $("#hierarchyTree").bind('select_node.jstree', function(e, data) {
-        if (hierarchyContext == "Record") {
-          window.location.href = data.node.a_attr.href;
+      $("#hierarchyTree").bind('select_node.jstree', function jsTreeSelect(e, resp) {
+        if (hierarchyContext === "Record") {
+          window.location.href = resp.node.a_attr.href;
         } else {
-          getRecord(data.node.li_attr.recordid);
+          getRecord(resp.node.li_attr.recordid);
         }
       });
 
@@ -184,44 +182,44 @@ $(document).ready(function() {
       }
     })
     .jstree({
-      'plugins': ['search','types'],
-      'core' : {
-        'data' : function (obj, cb) {
+      plugins: ['search', 'types'],
+      core: {
+        data: function jsTreeCoreData(obj, cb) {
           $.ajax({
-            'url': VuFind.path + '/Hierarchy/GetTreeJSON',
-            'data': {
-              'hierarchyID': hierarchyID,
-              'id': recordID
+            url: VuFind.path + '/Hierarchy/GetTreeJSON',
+            data: {
+              hierarchyID: hierarchyID,
+              id: recordID
             },
-            'statusCode': {
-              200: function(json, status, request) {
+            statusCode: {
+              200: function jsTree200Status(json /*, status, request*/) {
                 cb.call(this, json);
               },
-              204: function(json, status, request) { // No Content
+              204: function jsTree204Status(/*json, status, request*/) { // No Content
                 buildTreeWithXml(cb);
               },
-              503: function(json, status, request) { // Service Unavailable
+              503: function jsTree503Status(/*json, status, request*/) { // Service Unavailable
                 buildTreeWithXml(cb);
               }
             }
           });
         }
       },
-      'types' : {
-        'record': {
-          'icon':'fa fa-file-o'
+      types: {
+        record: {
+          icon: 'fa fa-file-o'
         },
-        'collection': {
-          'icon':'fa fa-folder'
+        collection: {
+          icon: 'fa fa-folder'
         }
       }
     });
 
   $('#treeSearch').removeClass('hidden');
   $('#treeSearch [type=submit]').click(doTreeSearch);
-  $('#treeSearchText').keyup(function (e) {
+  $('#treeSearchText').keyup(function treeSearchKeyup(e) {
     var code = (e.keyCode ? e.keyCode : e.which);
-    if(code == 13 || $(this).val().length == 0) {
+    if (code === 13 || $(this).val().length === 0) {
       doTreeSearch();
     }
   });
diff --git a/themes/bootstrap3/js/hold.js b/themes/bootstrap3/js/hold.js
index 8e9b85948134a90370bd8489bfed678f6c68fabf..1359a489187f4c302e26a63cdbd9ce8a52404e26 100644
--- a/themes/bootstrap3/js/hold.js
+++ b/themes/bootstrap3/js/hold.js
@@ -1,11 +1,12 @@
 /*global VuFind */
+/*exported setUpHoldRequestForm */
 function setUpHoldRequestForm(recordId) {
-  $('#requestGroupId').change(function() {
+  $('#requestGroupId').change(function requestGroupChange() {
     var $emptyOption = $("#pickUpLocation option[value='']");
     $("#pickUpLocation option[value!='']").remove();
     if ($('#requestGroupId').val() === '') {
-        $('#pickUpLocation').attr('disabled', 'disabled');
-        return;
+      $('#pickUpLocation').attr('disabled', 'disabled');
+      return;
     }
     $('#pickUpLocationLabel i').addClass("fa fa-spinner icon-spin");
     var params = {
@@ -19,11 +20,12 @@ function setUpHoldRequestForm(recordId) {
       cache: false,
       url: VuFind.path + '/AJAX/JSON'
     })
-    .done(function(response) {
+    .done(function holdPickupLocationsDone(response) {
       var defaultValue = $('#pickUpLocation').data('default');
-      $.each(response.data.locations, function() {
+      $.each(response.data.locations, function holdPickupLocationEach() {
         var option = $('<option></option>').attr('value', this.locationID).text(this.locationDisplay);
-        if (this.locationID == defaultValue || (defaultValue == '' && this.isDefault && $emptyOption.length == 0)) {
+        // Make sure to compare locationID and defaultValue as Strings since locationID may be an integer
+        if (String(this.locationID) === String(defaultValue) || (defaultValue === '' && this.isDefault && $emptyOption.length === 0)) {
           option.attr('selected', 'selected');
         }
         $('#pickUpLocation').append(option);
@@ -32,7 +34,7 @@ function setUpHoldRequestForm(recordId) {
       $('#pickUpLocationLabel i').removeClass("fa fa-spinner icon-spin");
       $('#pickUpLocation').removeAttr('disabled');
     })
-    .fail(function(response) {
+    .fail(function holdPickupLocationsFail(/*response*/) {
       $('#pickUpLocationLabel i').removeClass("fa fa-spinner icon-spin");
       $('#pickUpLocation').removeAttr('disabled');
     });
diff --git a/themes/bootstrap3/js/ill.js b/themes/bootstrap3/js/ill.js
index 7b67942cac583570c8d8a242b2e700b701edb0f1..bafc8f371f88152f5d74fbc991b223e72015ec27 100644
--- a/themes/bootstrap3/js/ill.js
+++ b/themes/bootstrap3/js/ill.js
@@ -1,11 +1,12 @@
 /*global VuFind */
+/*exported setUpILLRequestForm */
 function setUpILLRequestForm(recordId) {
-  $("#ILLRequestForm #pickupLibrary").change(function() {
+  $("#ILLRequestForm #pickupLibrary").change(function illPickupChange() {
     $("#ILLRequestForm #pickupLibraryLocation option").remove();
     $("#ILLRequestForm #pickupLibraryLocationLabel i").addClass("fa fa-spinner icon-spin");
     var url = VuFind.path + '/AJAX/JSON?' + $.param({
       id: recordId,
-      method:'getLibraryPickupLocations',
+      method: 'getLibraryPickupLocations',
       pickupLib: $("#ILLRequestForm #pickupLibrary").val()
     });
     $.ajax({
@@ -13,8 +14,8 @@ function setUpILLRequestForm(recordId) {
       cache: false,
       url: url
     })
-    .done(function(response) {
-      $.each(response.data.locations, function() {
+    .done(function illPickupLocationsDone(response) {
+      $.each(response.data.locations, function illPickupLocationEach() {
         var option = $("<option></option>").attr("value", this.id).text(this.name);
         if (this.isDefault) {
           option.attr("selected", "selected");
@@ -23,7 +24,7 @@ function setUpILLRequestForm(recordId) {
       });
       $("#ILLRequestForm #pickupLibraryLocationLabel i").removeClass("fa fa-spinner icon-spin");
     })
-    .fail(function(response) {
+    .fail(function illPickupLocationsFail(/*response*/) {
       $("#ILLRequestForm #pickupLibraryLocationLabel i").removeClass("fa fa-spinner icon-spin");
     });
   });
diff --git a/themes/bootstrap3/js/keep_alive.js b/themes/bootstrap3/js/keep_alive.js
index 7df79b51986210405d627d7bab6b0a70de0f2fe1..65a7e252f16c52d36e67a6ab825fb0960bb7f33b 100644
--- a/themes/bootstrap3/js/keep_alive.js
+++ b/themes/bootstrap3/js/keep_alive.js
@@ -1,7 +1,7 @@
 /*global keepAliveInterval, VuFind */
 
-$(document).ready(function() {
-  window.setInterval(function() {
+$(document).ready(function keepAliveReady() {
+  window.setInterval(function keepAliveInterval() {
     $.getJSON(VuFind.path + '/AJAX/JSON', {method: 'keepAlive'});
   }, keepAliveInterval * 1000);
 });
diff --git a/themes/bootstrap3/js/lightbox.js b/themes/bootstrap3/js/lightbox.js
index 2c57669a763c6db6bb5c532aea5b210d85d90f71..9c82c9d3a86d16d5755d6f1b2a194bc136703bfb 100644
--- a/themes/bootstrap3/js/lightbox.js
+++ b/themes/bootstrap3/js/lightbox.js
@@ -1,5 +1,5 @@
-/*global $, document, CustomEvent, VuFind, window */
-VuFind.register('lightbox', function() {
+/*global grecaptcha, recaptchaOnLoad, VuFind */
+VuFind.register('lightbox', function Lightbox() {
   // State
   var _originalUrl = false;
   var _currentUrl = false;
@@ -8,30 +8,28 @@ VuFind.register('lightbox', function() {
   // Elements
   var _modal, _modalBody, _clickedButton = null;
   // Utilities
-  var _storeClickedStatus = function() {
+  function _storeClickedStatus() {
     _clickedButton = this;
-  };
-  var _html = function(html) {
-    _modalBody.html(html);
+  }
+  function _html(content) {
+    _modalBody.html(content);
     // Set or update title if we have one
-    if (_lightboxTitle != '') {
+    if (_lightboxTitle !== '') {
       var h2 = _modalBody.find('h2:first-child');
-      if (h2.length == 0) {
+      if (h2.length === 0) {
         h2 = $('<h2/>').prependTo(_modalBody);
       }
       h2.text(_lightboxTitle);
       _lightboxTitle = '';
     }
     _modal.modal('handleUpdate');
-  };
-  var _emit = function(msg, details) {
-    if ('undefined' == typeof details) {
-      details = {};
-    }
+  }
+  function _emit(msg, _details) {
+    var details = _details || {};
     // Fallback to document.createEvent() if creating a new CustomEvent fails (e.g. IE 11)
     var event;
     try {
-       event = new CustomEvent(msg, {
+      event = new CustomEvent(msg, {
         detail: details,
         bubbles: true,
         cancelable: true
@@ -41,37 +39,21 @@ VuFind.register('lightbox', function() {
       event.initCustomEvent(msg, true, true, details);
     }
     return document.dispatchEvent(event);
-  };
+  }
 
-  /**
-   * Reload the page without causing trouble with POST parameters while keeping hash
-   */
-  var _refreshPage = function() {
-    var parts = window.location.href.split('#');
-    if (typeof parts[1] === 'undefined') {
-      window.location.href = window.location.href;
-    } else {
-      var href = parts[0];
-      // Force reload with a timestamp
-      href += href.indexOf('?') == -1 ? '?_=' : '&_=';
-      href += new Date().getTime() + '#' + parts[1];
-      window.location.href = href;
-    }
-  };
   // Public: Present an alert
-  var showAlert = function(message, type) {
-    if ('undefined' == typeof type) {
-      type = 'info';
-    }
-    _html('<div class="flash-message alert alert-'+type+'">'+message+'</div>'
+  function showAlert(message, _type) {
+    var type = _type || 'info';
+    _html('<div class="flash-message alert alert-' + type + '">' + message + '</div>'
         + '<button class="btn btn-default" data-dismiss="modal">' + VuFind.translate('close') + '</button>');
     _modal.modal('show');
-  };
-  var flashMessage = function(message, type) {
+  }
+  function flashMessage(message, _type) {
+    var type = _type || 'info';
     _modalBody.find('.flash-message,.fa.fa-spinner').remove();
     _modalBody.find('h2:first-of-type')
-      .after('<div class="flash-message alert alert-'+type+'">'+message+'</div>');
-  };
+      .after('<div class="flash-message alert alert-' + type + '">' + message + '</div>');
+  }
 
   /**
    * Update content
@@ -80,23 +62,32 @@ VuFind.register('lightbox', function() {
    *
    * data-lightbox-ignore = do not submit this form in lightbox
    */
-  var _update = function(html) {
-    if (!html.match) {
+  var _constrainLink; // function declarations to avoid style warnings
+  var _formSubmit;    // about circular references
+  function _update(content) {
+    if (!content.match) {
       return;
     }
     // Isolate successes
-    var htmlDiv = $('<div/>').html(html);
+    var htmlDiv = $('<div/>').html(content);
     var alerts = htmlDiv.find('.flash-message.alert-success');
     if (alerts.length > 0) {
-      showAlert(alerts[0].innerHTML, 'success');
+      var href = alerts.find('.download').attr('href');
+      if (typeof href !== 'undefined') {
+        location.href = href;
+        _modal.modal('hide');
+      } else {
+        showAlert(alerts[0].innerHTML, 'success');
+      }
       return;
     }
     // Deframe HTML
-    if (html.match('<!DOCTYPE html>')) {
-      html = htmlDiv.find('.main > .container').html();
+    var finalHTML = content;
+    if (content.match('<!DOCTYPE html>')) {
+      finalHTML = htmlDiv.find('.main > .container').html();
     }
     // Fill HTML
-    _html(html);
+    _html(finalHTML);
     _modal.modal('show');
     // Attach capturing events
     _modalBody.find('a').click(_constrainLink);
@@ -106,21 +97,23 @@ VuFind.register('lightbox', function() {
     _modalBody.find('[type=submit]').click(_storeClickedStatus);
 
     var forms = _modalBody.find('form:not([data-lightbox-ignore])');
-    for (var i=0;i<forms.length;i++) {
+    for (var i = 0; i < forms.length; i++) {
       $(forms[i]).on('submit', _formSubmit);
     }
     // Select all checkboxes
-    $('#modal').find('.checkbox-select-all').change(function() {
+    $('#modal').find('.checkbox-select-all').change(function lbSelectAllCheckboxes() {
       $(this).closest('.modal-body').find('.checkbox-select-item').prop('checked', this.checked);
     });
-    $('#modal').find('.checkbox-select-item').change(function() {
+    $('#modal').find('.checkbox-select-item').change(function lbSelectAllDisable() {
       $(this).closest('.modal-body').find('.checkbox-select-all').prop('checked', false);
     });
-  };
+    // Recaptcha
+    recaptchaOnLoad();
+  }
 
   var _xhr = false;
   // Public: Handle AJAX in the Lightbox
-  var ajax = function(obj) {
+  function ajax(obj) {
     if (_xhr !== false) {
       return;
     }
@@ -131,21 +124,21 @@ VuFind.register('lightbox', function() {
     if (!obj.url.match(/layout=lightbox/)) {
       var parts = obj.url.split('#');
       obj.url = parts[0].indexOf('?') < 0
-        ? parts[0]+'?'
-        : parts[0]+'&';
-      obj.url += 'layout=lightbox&lbreferer='+encodeURIComponent(_currentUrl);
-      obj.url += parts.length < 2 ? '' : '#'+parts[1];
+        ? parts[0] + '?'
+        : parts[0] + '&';
+      obj.url += 'layout=lightbox&lbreferer=' + encodeURIComponent(_currentUrl);
+      obj.url += parts.length < 2 ? '' : '#' + parts[1];
     }
     _xhr = $.ajax(obj);
-    _xhr.always(function() { _xhr = false; })
-      .done(function(html, status, jq_xhr) {
-        if (jq_xhr.status == 205) {
-          _refreshPage();
+    _xhr.always(function lbAjaxAlways() { _xhr = false; })
+      .done(function lbAjaxDone(content, status, jq_xhr) {
+        if (jq_xhr.status === 205) {
+          VuFind.refreshPage();
           return;
         }
         // Place Hold error isolation
         if (obj.url.match(/\/Record/) && (obj.url.match(/Hold\?/) || obj.url.match(/Request\?/))) {
-          var testDiv = $('<div/>').html(html);
+          var testDiv = $('<div/>').html(content);
           var error = testDiv.find('.flash-message.alert-danger');
           if (error.length && testDiv.find('.record').length) {
             showAlert(error[0].innerHTML, 'danger');
@@ -153,10 +146,10 @@ VuFind.register('lightbox', function() {
           }
         }
         if ( // Close the lightbox after deliberate login
-          obj.method                                                                // is a form
-          && ((obj.url.match(/MyResearch/) && !obj.url.match(/Bulk/))               // that matches login/create account
-            || obj.url.match(/catalogLogin/))                                       // or catalog login for holds
-          && $('<div/>').html(html).find('.flash-message.alert-danger').length == 0 // skip failed logins
+          obj.method                                                                 // is a form
+          && ((obj.url.match(/MyResearch/) && !obj.url.match(/Bulk/) && !obj.url.match(/Delete/)) // that matches login/create account
+            || obj.url.match(/catalogLogin/))                                        // or catalog login for holds
+          && $('<div/>').html(content).find('.flash-message.alert-danger').length === 0 // skip failed logins
         ) {
           var eventResult = _emit('VuFind.lightbox.login', {
             originalUrl: _originalUrl,
@@ -164,7 +157,7 @@ VuFind.register('lightbox', function() {
           });
           if (_originalUrl.match(/UserLogin/) || obj.url.match(/catalogLogin/)) {
             if (eventResult) {
-              _refreshPage();
+              VuFind.refreshPage();
             }
             return false;
           } else {
@@ -172,27 +165,27 @@ VuFind.register('lightbox', function() {
           }
           _currentUrl = _originalUrl; // Now that we're logged in, where were we?
         }
-        _update(html);
+        _update(content);
       })
-      .fail(function() {
-        showAlert(VuFind.translate('error_occurred'), 'danger');
+      .fail(function lbAjaxFail(deferred, errorType, msg) {
+        showAlert(VuFind.translate('error_occurred') + '<br/>' + msg, 'danger');
       });
     return _xhr;
-  };
-  var reload = function() {
-    ajax({url:_currentUrl || _originalUrl});
-  };
+  }
+  function reload() {
+    ajax({ url: _currentUrl || _originalUrl });
+  }
 
   /**
    * Evaluate a callback
    */
-  var _evalCallback = function(callback, event, data) {
+  function _evalCallback(callback, event, data) {
     if ('function' === typeof window[callback]) {
       return window[callback](event, data);
     } else {
       return eval('(function(event, data) {' + callback + '}())'); // inline code
     }
-  };
+  }
 
   /**
    * Modal link data options
@@ -202,14 +195,16 @@ VuFind.register('lightbox', function() {
    * data-lightbox-post = post data
    * data-lightbox-title = Lightbox title (overrides any title the page provides)
    */
-  var _constrainLink = function(event) {
-    if (typeof $(this).data('lightboxIgnore') != 'undefined' || this.attributes.href.value.charAt(0) === '#') {
+  _constrainLink = function constrainLink(event) {
+    if (typeof $(this).data('lightboxIgnore') != 'undefined'
+      || typeof this.attributes.href === 'undefined' || this.attributes.href.value.charAt(0) === '#'
+    ) {
       return true;
     }
     if (this.href.length > 1) {
       event.preventDefault();
       var obj = {url: $(this).data('lightboxHref') || this.href};
-      if("string" === typeof $(this).data('lightboxPost')) {
+      if ("string" === typeof $(this).data('lightboxPost')) {
         obj.type = 'POST';
         obj.data = $(this).data('lightboxPost');
       }
@@ -234,15 +229,23 @@ VuFind.register('lightbox', function() {
    *
    * data-lightbox-ignore = do not handle clicking this button in lightbox
    */
-  var _formSubmit = function(event) {
+  _formSubmit = function formSubmit(event) {
     // Gather data
     var form = event.target;
     var data = $(form).serializeArray();
-    data.push({'name':'layout', 'value':'lightbox'}); // Return in lightbox, please
+    // Check for recaptcha
+    if (typeof grecaptcha !== 'undefined') {
+      var recaptcha = $(form).find('.g-recaptcha');
+      if (recaptcha.length > 0) {
+        data.push({ name: 'g-recaptcha-response', value: grecaptcha.getResponse(recaptcha.data('captchaId')) });
+      }
+    }
+    // Force layout
+    data.push({ name: 'layout', value: 'lightbox' }); // Return in lightbox, please
     // Add submit button information
     var submit = $(_clickedButton);
     _clickedButton = null;
-    var buttonData = {'name':name, 'value':1};
+    var buttonData = { name: 'submit', value: 1 };
     if (submit.length > 0) {
       if (typeof submit.data('lightbox-ignore') !== 'undefined') {
         return true;
@@ -263,12 +266,13 @@ VuFind.register('lightbox', function() {
     }
     // onclose behavior
     if ('string' === typeof $(form).data('lightboxOnclose')) {
-      document.addEventListener('VuFind.lightbox.closed', function(event) {
-        _evalCallback($(form).data('lightboxOnclose'), event);
+      document.addEventListener('VuFind.lightbox.closed', function lightboxClosed(e) {
+        this.removeEventListener('VuFind.lightbox.closed', arguments.callee);
+        _evalCallback($(form).data('lightboxOnclose'), e, form);
       }, false);
     }
     // Loading
-    _modalBody.prepend('<i class="fa fa-spinner fa-spin pull-right"></i>');
+    _modalBody.prepend('<i class="fa fa-spinner fa-spin pull-right" title="' + VuFind.translate('loading') + '"></i>');
     // Prevent multiple submission of submit button in lightbox
     if (submit.closest(_modal).length > 0) {
       submit.attr('disabled', 'disabled');
@@ -280,6 +284,10 @@ VuFind.register('lightbox', function() {
       url: $(form).attr('action') || _currentUrl,
       method: $(form).attr('method') || 'GET',
       data: data
+    }).done(function recaptchaReset() {
+      if (typeof grecaptcha !== 'undefined') {
+        grecaptcha.reset($(form).find('.g-recaptcha').data('captchaId'));
+      }
     });
 
     VuFind.modal('show');
@@ -287,10 +295,8 @@ VuFind.register('lightbox', function() {
   };
 
   // Public: Attach listeners to the page
-  var bind = function(target) {
-    if ('undefined' === typeof target) {
-      target = document;
-    }
+  function bind(el) {
+    var target = el || document;
     $(target).find('a[data-lightbox]')
       .unbind('click', _constrainLink)
       .on('click', _constrainLink);
@@ -302,31 +308,31 @@ VuFind.register('lightbox', function() {
     // information about which button was clicked here as checking focused button
     // doesn't work on all browsers and platforms.
     $('form[data-lightbox] [type=submit]').click(_storeClickedStatus);
-  };
+  }
 
-  var reset = function() {
+  function reset() {
     _html(VuFind.translate('loading') + '...');
     _originalUrl = false;
     _currentUrl = false;
     _lightboxTitle = '';
-  };
-  var init = function() {
+  }
+  function init() {
     _modal = $('#modal');
     _modalBody = _modal.find('.modal-body');
-    _modal.on('hide.bs.modal', function() {
+    _modal.on('hide.bs.modal', function lightboxHide() {
       if (VuFind.lightbox.refreshOnClose) {
-        _refreshPage();
+        VuFind.refreshPage();
       }
       _emit('VuFind.lightbox.closing');
     });
-    _modal.on('hidden.bs.modal', function() {
+    _modal.on('hidden.bs.modal', function lightboxHidden() {
       VuFind.lightbox.reset();
       _emit('VuFind.lightbox.closed');
     });
 
-    VuFind.modal = function(cmd) { _modal.modal(cmd); };
+    VuFind.modal = function modalShortcut(cmd) { _modal.modal(cmd); };
     bind();
-  };
+  }
 
   // Reveal
   return {
diff --git a/themes/bootstrap3/js/map_selection.js b/themes/bootstrap3/js/map_selection.js
new file mode 100644
index 0000000000000000000000000000000000000000..a3abce25c15408f702cd7e15ad4c6afe59a7775c
--- /dev/null
+++ b/themes/bootstrap3/js/map_selection.js
@@ -0,0 +1,272 @@
+/*global ol */
+/*exported loadMapSelection */
+//Coordinate order:  Storage and Query: WENS ; Display: WSEN
+
+function loadMapSelection(geoField, boundingBox, baseURL, homeURL, searchParams, showSelection, resultsCoords, popupTitle) {
+  var init = true;
+  var pTitle = popupTitle + '<button class="close">&times;</button>';
+  var srcProj = 'EPSG:4326';
+  var dstProj = 'EPSG:900913';
+  var osm = new ol.layer.Tile({source: new ol.source.OSM()});
+  var searchboxSource = new ol.source.Vector();
+  var searchboxStyle = new ol.style.Style({
+    fill: new ol.style.Fill({
+      color: [255, 0, 0, 0.1]
+    }),
+    stroke: new ol.style.Stroke({
+      color: [255, 0, 0, 1],
+      width: 2
+    })
+  });
+  var searchboxLayer = new ol.layer.Vector({
+    source: searchboxSource,
+    style: searchboxStyle
+  });
+  var draw, map;
+  var count = resultsCoords.length;
+  var searchResults = new Array(count);
+  var searchIds = new Array(count);
+  for (var i = 0; i < count; ++i) {
+    var coordinates = ol.proj.transform(
+        [resultsCoords[i][1], resultsCoords[i][2]], srcProj, dstProj
+    );
+    searchResults[i] = new ol.Feature({
+      geometry: new ol.geom.Point(coordinates),
+      id: resultsCoords[i][0],
+      name: resultsCoords[i][3]
+    });
+    searchIds[i] = resultsCoords[i][0];
+  }
+  var resultSource = new ol.source.Vector({
+    features: searchResults
+  });
+  var clusterSource = new ol.source.Cluster({
+    distance: 60,
+    source: resultSource
+  });
+  var styleCache = {};
+  var clusterLayer = new ol.layer.Vector({
+    id: 'clusterLayer',
+    source: clusterSource,
+    style: function addClusterStyle(feature) {
+      var size = feature.get('features').length;
+      var pointRadius = 8 + (size.toString().length * 2);
+      var style = styleCache[size];
+      if (!style) {
+        style = [
+          new ol.style.Style({
+            image: new ol.style.Circle({
+              radius: pointRadius,
+              stroke: new ol.style.Stroke({
+                color: '#ff0000'
+              }),
+              fill: new ol.style.Fill({
+                color: '#ffb3b3'
+              })
+            }),
+            text: new ol.style.Text({
+              text: size.toString(),
+              font: 'bold 12px arial,sans-serif',
+              fill: new ol.style.Fill({
+                color: 'black'
+              })
+            })
+          })
+        ];
+        styleCache[size] = style;
+      }
+      map.removeInteraction(draw);
+      return style;
+    }
+  });
+
+  $('#geo_search').show();
+  init = function drawMap() {
+    map = new ol.Map({
+      interactions: ol.interaction.defaults({
+        shiftDragZoom: false
+      }),
+      target: 'geo_search_map',
+      projection: dstProj,
+      layers: [osm, searchboxLayer, clusterLayer],
+      view: new ol.View({
+        center: [0, 0],
+        zoom: 1
+      })
+    });
+
+    if (showSelection === true) {
+      searchboxSource.clear();
+      // Adjust bounding box (WSEN) display for queries crossing the dateline
+      if (boundingBox[0] > boundingBox[2]) {
+        boundingBox[2] = boundingBox[2] + 360;
+      }
+      var newBbox = new ol.geom.Polygon([[
+        ol.proj.transform([boundingBox[0], boundingBox[3]], srcProj, dstProj),
+        ol.proj.transform([boundingBox[0], boundingBox[1]], srcProj, dstProj),
+        ol.proj.transform([boundingBox[2], boundingBox[1]], srcProj, dstProj),
+        ol.proj.transform([boundingBox[2], boundingBox[3]], srcProj, dstProj),
+        ol.proj.transform([boundingBox[0], boundingBox[3]], srcProj, dstProj)
+      ]]);
+      var featureBbox = new ol.Feature({
+        name: "bbox",
+        geometry: newBbox
+      });
+      searchboxSource.addFeature(featureBbox);
+      map.getView().fit(searchboxSource.getExtent(), map.getSize());
+    }
+
+    //Get popup elements from webpage
+    var element = document.getElementById('popup');
+
+    // Add popup element to map
+    var popup = new ol.Overlay({
+      element: element,
+      stopEvent: true
+    });
+    map.addOverlay(popup);
+
+    // Display popup on click
+    map.on('click', function displayPopup(evt) {
+      var popupfeature = map.forEachFeatureAtPixel(evt.pixel,
+        function showFeature(feature) {
+          return {'feature': feature, 'layer': clusterLayer};
+        });
+      if (popupfeature) {
+        var cFeatures = popupfeature.feature.get('features');
+        var fType = typeof cFeatures;
+        var fLayerId = popupfeature.layer.get('id');
+        if ((fLayerId === 'clusterLayer') && (fType === 'object') && (cFeatures.length < 5)) {
+          var coordinate = map.getCoordinateFromPixel(evt.pixel);
+          var pcontent = '';
+          for (var j = 0; j < cFeatures.length; j++) {
+            var cFeatureName = cFeatures[j].get('name');
+            var cFeatureId = cFeatures[j].get('id');
+            var cFeatureContent = '<article class="geoItem">' +
+              cFeatureName.link(homeURL + 'Record/' + cFeatureId) + '</article>';
+            pcontent += cFeatureContent;
+          }
+          popup.setPosition(coordinate);
+          $(element).popover({
+            'placement': 'auto',
+            'container': 'body',
+            'animation': false,
+            'html': true,
+            'title': pTitle
+          }).on('shown.bs.popover', function closePopup(e) {
+            // 'aria-describedby' is the id of the current popover
+            var current_popover = '#' + $(e.target).attr('aria-describedby');
+            var $cur_pop = $(current_popover);
+            $cur_pop.find('.close').click(function closeCurPop(){
+              $(element).popover('hide');
+            });
+          });
+          $(element).data('bs.popover').options.content = pcontent;
+          $(element).popover('show');
+        }
+      } else {
+        $(element).popover('destroy');
+      }
+    });
+
+    // change mouse cursor when over marker
+    map.on('pointermove', function changeMouseCursor(evt) {
+      if (evt.dragging) {
+        $(element).popover('destroy');
+        return;
+      }
+      var pixel = map.getEventPixel(evt.originalEvent);
+      var hit = map.hasFeatureAtPixel(pixel);
+      if (hit) {
+        var fl = map.forEachFeatureAtPixel(pixel, function getFeature(feature) {
+          return {'feature': feature, 'layer': clusterLayer};
+        });
+        var cFeatures = fl.feature.get('features');
+        var fType = typeof cFeatures;
+        var fLayerId = fl.layer.get('id');
+        if ((fLayerId === 'clusterLayer') && (fType === 'object') && (cFeatures.length < 5)) {
+          map.getTargetElement().style.cursor = 'pointer';
+        } else {
+          map.getTargetElement().style.cursor = 'default';
+        }
+      }
+    });
+  };
+  function addInteraction() {
+    draw = new ol.interaction.Draw ({
+      source: searchboxSource,
+      type: 'LineString',
+      maxPoints: 2,
+      geometryFunction: function rectangleFunction(coords, geometryParam) {
+        var geometry = geometryParam ? geometryParam : new ol.geom.Polygon(null);
+        var start = coords[0];
+        var end = coords[1];
+        geometry.setCoordinates([
+          [start, [start[0], end[1]], end, [end[0], start[1]], start]
+        ]);
+        return geometry;
+      }
+    });
+
+    draw.on('drawend', function drawSearchBox(evt) {
+      var geometry = evt.feature.getGeometry();
+      var geoCoordinates = geometry.getCoordinates();
+      var westnorth = ol.proj.transform(geoCoordinates[0][0], dstProj, srcProj);
+      var eastsouth = ol.proj.transform(geoCoordinates[0][2], dstProj, srcProj);
+      // Check to make sure the coordinates are in the correct order
+      var west = westnorth[0];
+      var east = eastsouth[0];
+      var north = westnorth[1];
+      var south = eastsouth[1];
+      if (west > east){
+        west = eastsouth[0];
+        east = westnorth[0];
+      }
+      if (south > north) {
+        north = eastsouth[1];
+        south = westnorth[1];
+      }
+      // Make corrections for queries that cross the dateline
+      if (west > 180) {
+        if (west > 360) {
+          west = west - (360 * Math.floor(west / -360));
+          if (west > 180) {
+            west = west - 360;
+          }
+        } else {
+          west = west - 360;
+        }
+      }
+      if (west < -180) {
+        if (west < -360) {
+          west = west + (360 * Math.floor(west / -360));
+          if (west < -180) {
+            west = west + 360;
+          }
+        } else {
+          west = west + 360;
+        }
+      }
+      if (east > 180) {
+        // Fix overlapping longitudinal query parameters
+        if (east > 360) {
+          east = east - (360 * Math.floor(east / 360));
+          if (east > 180) {
+            east = east - 360;
+          }
+        } else {
+          east = east - 360;
+        }
+      }
+      var rawFilter = geoField + ':Intersects(ENVELOPE(' + west + ', ' + east + ', ' + north + ', ' + south + '))';
+      location.href = baseURL + searchParams + "&filter[]=" + rawFilter;
+    }, this);
+    map.addInteraction(draw);
+  }
+  init();
+  document.getElementById("draw_box").onclick = function clearAndDrawMap() {
+    map.removeInteraction(draw);
+    addInteraction();
+  };
+  init = false;
+}
diff --git a/themes/bootstrap3/js/map_tab_ol.js b/themes/bootstrap3/js/map_tab_ol.js
new file mode 100644
index 0000000000000000000000000000000000000000..1e26b2f073a464dd8acb62d1d751f1993e600d88
--- /dev/null
+++ b/themes/bootstrap3/js/map_tab_ol.js
@@ -0,0 +1,178 @@
+/*global ol */
+/*exported loadMapTab */
+//Coordinate order:  Storage and Query: WENS ; Display: WSEN
+function loadMapTab(mapData, popupTitle) {
+  var init = true;
+  var pTitle = popupTitle + '<button class="close">&times;</button>';
+  var srcProj = 'EPSG:4326';
+  var dstProj = 'EPSG:900913';
+  var osm = new ol.layer.Tile({source: new ol.source.OSM()});
+  var vectorSource = new ol.source.Vector();
+  var map;
+  var iconStyle = new ol.style.Style({
+    image: new ol.style.Circle({
+      radius: 5,
+      fill: new ol.style.Fill({
+        color: 'red'
+      })
+    })
+  });
+  var polyStyle = new ol.style.Style({
+    fill: new ol.style.Fill({
+      color: [200, 0, 0, 0.1]
+    }),
+    stroke: new ol.style.Stroke({
+      color: 'red',
+      width: 2
+    })
+  });
+
+  // close map popups if not on the Map tab
+  $('.record-tabs .map').on('hide.bs.tab', function closePopups() {
+    $('#popup').popover('destroy');
+  });
+
+  $('#map-canvas').show();
+  init = function drawMap() {
+    var featureCount = mapData.length;
+    var label, label_on;
+    var label_name;
+    var label_coords, label_coord, label_coord1, label_coord2;
+    var i = 0;
+    for (i; i < featureCount; i++) {
+      // Construct the label names
+      label_name = mapData[i][5];
+      //Construct the coordinate labels
+      label_coords = mapData[i][6];
+      if (label_coords) {
+        label_coord1 = mapData[i][6].substring(0, 16);
+        label_coord2 = mapData[i][6].substring(16);
+        if (label_coord2) {
+          label_coord = label_coord1 + '<br/>' + label_coord2;
+        } else {
+          label_coord = label_coord1;
+        }
+      }
+      // Construct the entire label string
+      if (label_coord && label_name) {
+        label = label_coord + '<br/>' + label_name;
+        label_on = true;
+      } else if (label_name){
+        label = label_name;
+        label_on = true;
+      } else if (label_coord) {
+        label = label_coord;
+        label_on = true;
+      } else {
+        label = 'No information available';
+        label_on = false;
+      }
+      // Determine if entry is point or polygon - Does W=E & N=S? //
+      if (mapData[i][4] === 2) {
+      //It's a point feature //
+        var lonlat = ol.proj.transform([mapData[i][0], mapData[i][1]], srcProj, dstProj);
+        var iconFeature = new ol.Feature({
+          geometry: new ol.geom.Point(lonlat),
+          name: label
+        });
+        iconFeature.setStyle(iconStyle);
+        vectorSource.addFeature(iconFeature);
+      } else if (mapData[i][4] === 4) { // It's a polygon feature //
+        var point1 = ol.proj.transform([mapData[i][0], mapData[i][3]], srcProj, dstProj);
+        var point2 = ol.proj.transform([mapData[i][0], mapData[i][1]], srcProj, dstProj);
+        var point3 = ol.proj.transform([mapData[i][2], mapData[i][1]], srcProj, dstProj);
+        var point4 = ol.proj.transform([mapData[i][2], mapData[i][3]], srcProj, dstProj);
+        var polyFeature = new ol.Feature({
+          geometry: new ol.geom.Polygon([
+            [point1, point2, point3, point4, point1]
+          ]),
+          name: label
+        });
+        polyFeature.setStyle(polyStyle);
+        vectorSource.addFeature(polyFeature);
+      }
+    }
+    var vectorLayer = new ol.layer.Vector({
+      source: vectorSource,
+      renderBuffer: 500
+    });
+    map = new ol.Map({
+      renderer: 'canvas',
+      projection: dstProj,
+      layers: [osm, vectorLayer],
+      target: 'map-canvas',
+      view: new ol.View({
+        center: [0, 0],
+        zoom: 1
+      })
+    });
+
+  // Adjust zoom extent and center of map
+    if (featureCount === 1 && mapData[0][4] === 2) {
+      map.getView().setZoom(4);
+      var centerCoord = ol.proj.transform([mapData[0][0], mapData[0][1]], srcProj, dstProj);
+      map.getView().setCenter(centerCoord);
+    } else {
+      var extent = vectorLayer.getSource().getExtent();
+      map.getView().fit(extent, map.getSize());
+    }
+
+  // Turn on popup tool tips if labels or coordinates are enabled.
+    if (label_on === true) {
+      var element = document.getElementById('popup');
+      var popup = new ol.Overlay({
+        element: element,
+        stopEvent: true
+      });
+      map.addOverlay(popup);
+
+      // Display popup on click
+      map.on('click', function displayPopup(evt) {
+        var popupfeature = map.forEachFeatureAtPixel(evt.pixel,
+          function showFeature(feature) {
+            return feature;
+          });
+        if (popupfeature) {
+          var coordinate = evt.coordinate;
+          popup.setPosition(coordinate);
+          $(element).popover({
+            'placement': 'auto',
+            'container': 'body',
+            'animation': false,
+            'html': true,
+            'title': pTitle
+          }).on('shown.bs.popover', function closePopup(e) {
+            // 'aria-describedby' is the id of the current popover
+            var current_popover = '#' + $(e.target).attr('aria-describedby');
+            var $cur_pop = $(current_popover);
+            $cur_pop.find('.close').click(function closeCurPop(){
+              $(element).popover('destroy');
+            });
+          });
+          $(element).data('bs.popover').options.content = popupfeature.get('name');
+          $(element).popover('show');
+        } else {
+          $(element).popover('destroy');
+        }
+      });
+
+      // change mouse cursor when over marker
+      map.on('pointermove', function changeMouseCursor(e) {
+        if (e.dragging) {
+          $(element).popover('destroy');
+          return;
+        }
+        var pixel = map.getEventPixel(e.originalEvent);
+        var hit = map.hasFeatureAtPixel(pixel);
+        var target = map.getTarget();
+        if (hit === true) {
+          document.getElementById(target).style.cursor = "pointer";
+        } else {
+          document.getElementById(target).style.cursor = "default";
+        }
+      });
+    }
+  };
+  init();
+  init = false;
+}
diff --git a/themes/bootstrap3/js/openurl.js b/themes/bootstrap3/js/openurl.js
index 28cd7ae7a8fe0f7df9067ba00774b1b0dad26068..b0771c5aab1415c4844d511ea06a64def7890490 100644
--- a/themes/bootstrap3/js/openurl.js
+++ b/themes/bootstrap3/js/openurl.js
@@ -1,23 +1,27 @@
 /*global extractClassParams, VuFind */
-VuFind.register('openurl', function() {
-  var _loadResolverLinks = function($target, openUrl, searchClassId) {
+VuFind.register('openurl', function OpenUrl() {
+  function _loadResolverLinks($target, openUrl, searchClassId) {
     $target.addClass('ajax_availability');
-    var url = VuFind.path + '/AJAX/JSON?' + $.param({method:'getResolverLinks',openurl:openUrl,searchClassId:searchClassId});
+    var url = VuFind.path + '/AJAX/JSON?' + $.param({
+      method: 'getResolverLinks',
+      openurl: openUrl,
+      searchClassId: searchClassId
+    });
     $.ajax({
       dataType: 'json',
       url: url
     })
-    .done(function(response) {
+    .done(function getResolverLinksDone(response) {
       $target.removeClass('ajax_availability').empty().append(response.data);
     })
-    .fail(function(response, textStatus) {
+    .fail(function getResolverLinksFail(response, textStatus) {
       $target.removeClass('ajax_availability').addClass('text-danger').empty();
-      if (textStatus == 'abort' || typeof response.responseJSON === 'undefined') { return; }
+      if (textStatus === 'abort' || typeof response.responseJSON == 'undefined') { return; }
       $target.append(response.responseJSON.data);
     });
-  };
+  }
 
-  var _embedOpenUrlLinks = function(element) {
+  function embedOpenUrlLinks(element) {
     // Extract the OpenURL associated with the clicked element:
     var openUrl = element.children('span.openUrl:first').attr('title');
 
@@ -31,20 +35,16 @@ VuFind.register('openurl', function() {
     // If the target is already visible, a previous click has populated it;
     // don't waste time doing redundant work.
     if (target.hasClass('hidden')) {
-      _loadResolverLinks(target.removeClass('hidden'), openUrl, element.data('search-class-id'));
+      _loadResolverLinks(target.removeClass('hidden'), openUrl, element.data('searchClassId'));
     }
-  };
+  }
 
-  // Assign actions to the OpenURL links. This can be called with a container e.g. when 
+  // Assign actions to the OpenURL links. This can be called with a container e.g. when
   // combined results fetched with AJAX are loaded.
-  var init = function(container)
-  {
-    if (typeof(container) == 'undefined') {
-      container = $('body');
-    }
-
+  function init(_container) {
+    var container = _container || $('body');
      // assign action to the openUrlWindow link class
-    container.find('a.openUrlWindow').unbind('click').click(function() {
+    container.find('a.openUrlWindow').unbind('click').click(function openUrlWindowClick() {
       var params = extractClassParams(this);
       var settings = params.window_settings;
       window.open($(this).attr('href'), 'openurl', settings);
@@ -52,12 +52,15 @@ VuFind.register('openurl', function() {
     });
 
     // assign action to the openUrlEmbed link class
-    container.find('.openUrlEmbed a').unbind('click').click(function() {
-      _embedOpenUrlLinks($(this));
+    container.find('.openUrlEmbed a').unbind('click').click(function openUrlEmbedClick() {
+      embedOpenUrlLinks($(this));
       return false;
     });
 
     container.find('.openUrlEmbed.openUrlEmbedAutoLoad a').trigger('click');
+  }
+  return {
+    init: init,
+    embedOpenUrlLinks: embedOpenUrlLinks
   };
-  return {init: init};
 });
diff --git a/themes/bootstrap3/js/preview.js b/themes/bootstrap3/js/preview.js
index 513ec2667a818ab09028cffd696343fabd0cabd2..a94d594462efb87fc4c6bbdf565a76f6a91b579f 100644
--- a/themes/bootstrap3/js/preview.js
+++ b/themes/bootstrap3/js/preview.js
@@ -1,96 +1,100 @@
+/*exported processGBSBookInfo, processOLBookInfo, processHTBookInfo */
+
 // functions to get rights codes for previews
 function getHathiOptions() {
-    return $('[class*="hathiPreviewSpan"]').attr("class").split('__')[1].split(',');
+  return $('[class*="hathiPreviewSpan"]').attr("class").split('__')[1].split(',');
 }
 function getGoogleOptions() {
-    var opts_temp = $('[class*="googlePreviewSpan"]').attr("class").split('__')[1].split(';');
-    var options = {};
-    for(var key in opts_temp) {
-        var arr = opts_temp[key].split(':');
-        options[arr[0]] = arr[1].split(',');
+  var opts_temp = $('[class*="googlePreviewSpan"]').attr("class").split('__')[1].split(';');
+  var options = {};
+  for (var key in opts_temp) {
+    if (opts_temp.hasOwnProperty(key)) {
+      var arr = opts_temp[key].split(':');
+      options[arr[0]] = arr[1].split(',');
     }
-    return options;
+  }
+  return options;
 }
 function getOLOptions() {
-    return $('[class*="olPreviewSpan"]').attr("class").split('__')[1].split(',');
+  return $('[class*="olPreviewSpan"]').attr("class").split('__')[1].split(',');
 }
 
-function getHTPreviews(skeys) {
-    skeys = skeys.replace(/(ISBN|LCCN|OCLC)/gi, '$1:').toLowerCase();
-    var bibkeys = skeys.split(/\s+/);
+function getHTPreviews(keys) {
+  var skeys = keys.replace(/(ISBN|LCCN|OCLC)/gi, '$1:').toLowerCase();
+  var bibkeys = skeys.split(/\s+/);
     // fetch 20 books at time if there are more than 20
     // since hathitrust only allows 20 at a time
     // as per https://vufind.org/jira/browse/VUFIND-317
-    var batch = [];
-    for(var i = 0; i < bibkeys.length; i++) {
-        batch.push(bibkeys[i]);
-        if ((i > 0 && i % 20 == 0) || i == bibkeys.length-1) {
-            var script = 'https://catalog.hathitrust.org/api/volumes/brief/json/'
+  var batch = [];
+  for (var i = 0; i < bibkeys.length; i++) {
+    batch.push(bibkeys[i]);
+    if ((i > 0 && i % 20 === 0) || i === bibkeys.length - 1) {
+      var script = 'https://catalog.hathitrust.org/api/volumes/brief/json/'
                 + batch.join('|') + '&callback=processHTBookInfo';
-            $.getScript(script);
-            batch = [];
-        }
+      $.getScript(script);
+      batch = [];
     }
+  }
 }
 
 function applyPreviewUrl($link, url) {
     // Update the preview button:
-    $link.attr('href', url).removeClass('hidden');
+  $link.attr('href', url).removeClass('hidden');
 
     // Update associated record thumbnail, if any:
-    $link.parents('.result,.record')
-        .find('.recordcover').parents('a').attr('href', url);
+  $link.parents('.result,.record')
+        .find('.recordcover[data-linkpreview="true"]').parents('a').attr('href', url);
 }
 
 function processBookInfo(booksInfo, previewClass, viewOptions) {
-    for (var bibkey in booksInfo) {
-        var bookInfo = booksInfo[bibkey];
-        if (bookInfo) {
-          if (viewOptions.indexOf(bookInfo.preview)>= 0) {
-                applyPreviewUrl(
-                    $('.' + previewClass + '.' + bibkey), bookInfo.preview_url
-                );
-            }
-        }
+  for (var bibkey in booksInfo) {
+    if (booksInfo[bibkey]) {
+      if (viewOptions.indexOf(booksInfo[bibkey].preview) >= 0) {
+        applyPreviewUrl(
+          $('.' + previewClass + '.' + bibkey), booksInfo[bibkey].preview_url
+        );
+      }
     }
+  }
 }
 
 function processGBSBookInfo(booksInfo) {
-    var viewOptions = getGoogleOptions();
-    if (viewOptions['link'] && viewOptions['link'].length > 0) {
-        processBookInfo(booksInfo, 'previewGBS', viewOptions['link']);
-    }
-    if (viewOptions['tab'] && viewOptions['tab'].length > 0) {
+  var viewOptions = getGoogleOptions();
+  if (viewOptions.link && viewOptions.link.length > 0) {
+    processBookInfo(booksInfo, 'previewGBS', viewOptions.link);
+  }
+  if (viewOptions.tab && viewOptions.tab.length > 0) {
         // check for "embeddable: true" in bookinfo
-        for (var bibkey in booksInfo) {
-            var bookInfo = booksInfo[bibkey];
-            if (bookInfo) {
-                if (viewOptions['tab'].indexOf(bookInfo.preview)>= 0
-                && (bookInfo.embeddable)) {
-                    // make tab visible
-                    $('ul.nav-tabs li.hidden a.preview').parent().removeClass('hidden');
-                }
-            }
+    for (var bibkey in booksInfo) {
+      if (booksInfo[bibkey]) {
+        if (viewOptions.tab.indexOf(booksInfo[bibkey].preview) >= 0
+        && (booksInfo[bibkey].embeddable)) {
+          // make tab visible
+          $('ul.nav-tabs li.hidden a.preview').parent().removeClass('hidden');
         }
+      }
     }
+  }
 }
 
 function processOLBookInfo(booksInfo) {
-    processBookInfo(booksInfo, 'previewOL', getOLOptions());
+  processBookInfo(booksInfo, 'previewOL', getOLOptions());
 }
 
 function processHTBookInfo(booksInfo) {
-    for (var b in booksInfo) {
-        var bibkey = b.replace(/:/, '').toUpperCase();
-        var $link = $('.previewHT.' + bibkey);
-        var items = booksInfo[b].items;
-        for (var i = 0; i < items.length; i++) {
-            // check if items possess an eligible rights code
-            if (getHathiOptions().indexOf(items[i].rightsCode) >= 0) {
-                applyPreviewUrl($link, items[i].itemURL);
-            }
+  for (var b in booksInfo) {
+    if (booksInfo.hasOwnProperty(b)) {
+      var bibkey = b.replace(/:/, '').toUpperCase();
+      var $link = $('.previewHT.' + bibkey);
+      var items = booksInfo[b].items;
+      for (var i = 0; i < items.length; i++) {
+        // check if items possess an eligible rights code
+        if (getHathiOptions().indexOf(items[i].rightsCode) >= 0) {
+          applyPreviewUrl($link, items[i].itemURL);
         }
+      }
     }
+  }
 }
 
 /**
@@ -100,94 +104,94 @@ function processHTBookInfo(booksInfo) {
  * developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/indexOf
  */
 function setIndexOf() {
-    Array.prototype.indexOf = function (searchElement /*, fromIndex */ ) {
-        "use strict";
-        if (this == null) {
-            throw new TypeError();
-        }
-        var t = Object(this);
+  Array.prototype.indexOf = function indexOfPolyfill(searchElement /*, fromIndex */ ) {
+    "use strict";
+    if (this == null) {
+      throw new TypeError();
+    }
+    var t = Object(this);
         /*jslint bitwise: false*/
-        var len = t.length >>> 0;
+    var len = t.length >>> 0;
         /*jslint bitwise: true*/
-        if (len === 0) {
-            return -1;
-        }
-        var n = 0;
-        if (arguments.length > 1) {
-            n = Number(arguments[1]);
-            if (n != n) { // shortcut for verifying if it's NaN
-                n = 0;
-            } else if (n != 0 && n != Infinity && n != -Infinity) {
-                n = (n > 0 || -1) * Math.floor(Math.abs(n));
-            }
-        }
-        if (n >= len) {
-            return -1;
-        }
-        var k = n >= 0 ? n : Math.max(len - Math.abs(n), 0);
-        for (; k < len; k++) {
-            if (k in t && t[k] === searchElement) {
-                return k;
-            }
-        }
-        return -1;
-    };
+    if (len === 0) {
+      return -1;
+    }
+    var n = 0;
+    if (arguments.length > 1) {
+      n = Number(arguments[1]);
+      if (n !== n) { // shortcut for verifying if it's NaN
+        n = 0;
+      } else if (n !== 0 && n !== Infinity && n !== -Infinity) {
+        n = (n > 0 || -1) * Math.floor(Math.abs(n));
+      }
+    }
+    if (n >= len) {
+      return -1;
+    }
+    var k = n >= 0 ? n : Math.max(len - Math.abs(n), 0);
+    for (; k < len; k++) {
+      if (k in t && t[k] === searchElement) {
+        return k;
+      }
+    }
+    return -1;
+  };
 }
 
 function getBibKeyString() {
-    var skeys = '';
-    $('.previewBibkeys').each(function(){
-        skeys += $(this).attr('class');
-    });
-    return skeys.replace(/previewBibkeys/g, '').replace(/^\s+|\s+$/g, '');
+  var skeys = '';
+  $('.previewBibkeys').each(function previewBibkeysEach(){
+    skeys += $(this).attr('class');
+  });
+  return skeys.replace(/previewBibkeys/g, '').replace(/^\s+|\s+$/g, '');
 }
 
 function getBookPreviews() {
-    var skeys = getBibKeyString();
-    var bibkeys = skeys.split(/\s+/);
-    var script;
+  var skeys = getBibKeyString();
+  var bibkeys = skeys.split(/\s+/);
+  var script;
 
     // fetch Google preview if enabled
-    if ($('[class*="googlePreviewSpan"]').length > 0) {
+  if ($('[class*="googlePreviewSpan"]').length > 0) {
         // checks if query string might break URI limit - if not, run as normal
-        if (bibkeys.length <= 150){
-            script = 'https://encrypted.google.com/books?jscmd=viewapi&bibkeys='
+    if (bibkeys.length <= 150){
+      script = 'https://encrypted.google.com/books?jscmd=viewapi&bibkeys='
                 + bibkeys.join(',') + '&callback=processGBSBookInfo';
-            $.getScript(script);
-        } else {
+      $.getScript(script);
+    } else {
             // if so, break request into chunks of 100
-            var keyString = '';
+      var keyString = '';
             // loop through array
-            for (var i=0; i < bibkeys.length; i++){
-                keyString += bibkeys[i] + ',';
+      for (var i = 0; i < bibkeys.length; i++){
+        keyString += bibkeys[i] + ',';
                 // send request when there are 100 requests ready or when there are no
                 // more elements to be sent
-                if ((i > 0 && i % 100 == 0) || i == bibkeys.length-1) {
-                    script = 'https://encrypted.google.com/books?jscmd=viewapi&bibkeys='
+        if ((i > 0 && i % 100 === 0) || i === bibkeys.length - 1) {
+          script = 'https://encrypted.google.com/books?jscmd=viewapi&bibkeys='
                         + keyString + '&callback=processGBSBookInfo';
-                    $.getScript(script);
-                    keyString = '';
-                }
-            }
+          $.getScript(script);
+          keyString = '';
         }
+      }
     }
+  }
 
     // fetch OpenLibrary preview if enabled
-    if ($('[class*="olPreviewSpan"]').length > 0) {
-        script = '//openlibrary.org/api/books?bibkeys='
+  if ($('[class*="olPreviewSpan"]').length > 0) {
+    script = '//openlibrary.org/api/books?bibkeys='
             + bibkeys.join(',') + '&callback=processOLBookInfo';
-        $.getScript(script);
-    }
+    $.getScript(script);
+  }
 
     // fetch HathiTrust preview if enabled
-    if ($('[class*="hathiPreviewSpan"]').length > 0) {
-        getHTPreviews(skeys);
-    }
+  if ($('[class*="hathiPreviewSpan"]').length > 0) {
+    getHTPreviews(skeys);
+  }
 }
 
-$(document).ready(function() {
-    if (!Array.prototype.indexOf) {
-        setIndexOf();
-    }
-    getBookPreviews();
+$(document).ready(function previewDocReady() {
+  if (!Array.prototype.indexOf) {
+    setIndexOf();
+  }
+  getBookPreviews();
 });
diff --git a/themes/bootstrap3/js/pubdate_vis.js b/themes/bootstrap3/js/pubdate_vis.js
index ef7721426c11e3a4e7b6fe90e4ce0c2c0694e172..5001c9ebc97f4aefe2d93ef8a7ee2c86b6645e4b 100644
--- a/themes/bootstrap3/js/pubdate_vis.js
+++ b/themes/bootstrap3/js/pubdate_vis.js
@@ -1,14 +1,13 @@
-/*global htmlEncode*/
+/*global htmlEncode */
+/*exported loadVis */
 
-function PadDigits(n, totalDigits) {
-  if (n <= 0){
-    n= 1;
-  }
+function PadDigits(number, totalDigits) {
+  var n = number <= 0 ? 1 : number;
   n = n.toString();
   var pd = '';
   if (totalDigits > n.length)
   {
-    for (var i=0; i < (totalDigits-n.length); i++)
+    for (var i = 0; i < (totalDigits - n.length); i++)
     {
       pd += '0';
     }
@@ -22,13 +21,13 @@ function loadVis(facetFields, searchParams, baseURL, zooming) {
     'background-color': '#fff', // background of box
     'fill': '#eee',             // box fill color
     'stroke': '#265680',        // box outline color
-    'outline-color': '#e8cfac'  // selection color
+    'outline-color': '#c38835'  // selection color
   };
   var $dateVisColorSettings = $('#dateVisColorSettings');
-  for(var rule in cssColorSettings) {
-    if($dateVisColorSettings.css(rule)) {
+  for (var rule in cssColorSettings) {
+    if ($dateVisColorSettings.css(rule)) {
       var match = $dateVisColorSettings.css(rule).match(/rgb[a]?\([^\)]+\)|#[a-fA-F0-9]+/);
-      if(null != match) {
+      if (null != match) {
         cssColorSettings[rule] = match[0];
       }
     }
@@ -40,23 +39,23 @@ function loadVis(facetFields, searchParams, baseURL, zooming) {
         show: true,
         align: "center",
         fill: true,
-        fillColor: cssColorSettings['fill']
+        fillColor: cssColorSettings.fill
       }
     },
-    colors: [cssColorSettings['stroke']],
+    colors: [cssColorSettings.stroke],
     legend: { noColumns: 2 },
     xaxis: { tickDecimals: 0 },
     yaxis: { min: 0, ticks: [] },
-    selection: {mode: "x", color: cssColorSettings['outline-color']},
+    selection: {mode: "x", color: cssColorSettings['outline-color'], minSize: 0},
     grid: { backgroundColor: cssColorSettings['background-color'] }
   };
 
   // AJAX call
   var url = baseURL + '/AJAX/json?method=getVisData&facetFields=' + encodeURIComponent(facetFields) + '&' + searchParams;
-  $.getJSON(url, function (data) {
-    $.each(data['data'], function(key, val) {
+  $.getJSON(url, function getVisDataJSON(data) {
+    $.each(data.data, function getVisDataEach(key, val) {
       //check if there is data to display, if there isn't hide the box
-      if (val['data'] == undefined || val['data'].length == 0) {
+      if (val.data === undefined || val.data.length === 0) {
         return;
       }
       $("#datevis" + key + "xWrapper").removeClass('hidden');
@@ -68,32 +67,32 @@ function loadVis(facetFields, searchParams, baseURL, zooming) {
       var hasFilter = true;
 
       //set the has filter
-      if (val['min'] == 0 && val['max']== 0) {
+      if (val.min === 0 && val.max === 0) {
         hasFilter = false;
       }
 
       //check if the min and max value have been set otherwise set them to the ends of the graph
-      if (val['min'] == 0) {
-        val['min'] = val['data'][0][0] - 5;
+      if (val.min === 0) {
+        val.min = val.data[0][0] - 5;
       }
-      if (val['max']== 0) {
-        val['max'] =  parseInt(val['data'][val['data'].length - 1][0], 10) + 5;
+      if (val.max === 0) {
+        val.max = parseInt(val.data[val.data.length - 1][0], 10) + 5;
       }
 
       if (zooming) {
         //check the first and last elements of the data array against min and max value (+padding)
         //if the element exists leave it, otherwise create a new marker with a minus one value
-        if (val['data'][val['data'].length - 1][0] != parseInt(val['max'], 10) + 5) {
-          val['data'].push([parseInt(val['max'], 10) + 5, -1]);
+        if (val.data[val.data.length - 1][0] !== parseInt(val.max, 10) + 5) {
+          val.data.push([parseInt(val.max, 10) + 5, -1]);
         }
-        if (val['data'][0][0] != val['min'] - 5) {
-          val['data'].push([val['min'] - 5, -1]);
+        if (val.data[0][0] !== val.min - 5) {
+          val.data.push([val.min - 5, -1]);
         }
         //check for values outside the selected range and remove them by setting them to null
-        for (var i=0; i<val['data'].length; i++) {
-          if (val['data'][i][0] < val['min'] -5 || val['data'][i][0] > parseInt(val['max'], 10) + 5) {
+        for (var i = 0; i < val.data.length; i++) {
+          if (val.data[i][0] < val.min - 5 || val.data[i][0] > parseInt(val.max, 10) + 5) {
             //remove this
-            val['data'].splice(i,1);
+            val.data.splice(i, 1);
             i--;
           }
         }
@@ -101,29 +100,29 @@ function loadVis(facetFields, searchParams, baseURL, zooming) {
       } else {
         //no zooming means that we need to specifically set the margins
         //do the last one first to avoid getting the new last element
-        val['data'].push([parseInt(val['data'][val['data'].length - 1][0], 10) + 5, -1]);
+        val.data.push([parseInt(val.data[val.data.length - 1][0], 10) + 5, -1]);
         //now get the first element
-        val['data'].push([val['data'][0][0] - 5, -1]);
+        val.data.push([val.data[0][0] - 5, -1]);
       }
 
 
       var plot = $.plot(placeholder, [val], options);
       if (hasFilter) {
         // mark pre-selected area
-        plot.setSelection({ x1: val['min'] , x2: val['max']});
+        plot.setSelection({ x1: val.min, x2: val.max});
       }
       // selection handler
-      placeholder.bind("plotselected", function (event, ranges) {
+      placeholder.bind("plotselected", function plotselected(event, ranges) {
         var from = Math.floor(ranges.xaxis.from);
         var to = Math.ceil(ranges.xaxis.to);
-        location.href = val['removalURL'] + '&daterange[]=' + key + '&' + key + 'to=' + PadDigits(to,4) + '&' + key + 'from=' + PadDigits(from,4);
+        window.location.href = val.removalURL + '&daterange[]=' + key + '&' + key + 'to=' + PadDigits(to, 4) + '&' + key + 'from=' + PadDigits(from, 4);
       });
 
       if (hasFilter) {
         var newdiv = document.createElement('span');
         var text = document.getElementById("clearButtonText").innerHTML;
         newdiv.setAttribute('id', 'clearButton' + key);
-        newdiv.innerHTML = '<a href="' + htmlEncode(val['removalURL']) + '">' + text + '</a>';
+        newdiv.innerHTML = '<a href="' + htmlEncode(val.removalURL) + '">' + text + '</a>';
         newdiv.className += "dateVisClear";
         placeholder.before(newdiv);
       }
diff --git a/themes/bootstrap3/js/record.js b/themes/bootstrap3/js/record.js
index e3727b6c3e5f80c949a66dc16d9d928762317f83..34749a9f4dc47c29ef5348a7463f8d192102e36e 100644
--- a/themes/bootstrap3/js/record.js
+++ b/themes/bootstrap3/js/record.js
@@ -1,4 +1,5 @@
-/*global checkSaveStatuses, deparam, extractClassParams, getListUrlFromHTML, htmlEncode, Lightbox, syn_get_widget, userIsLoggedIn, VuFind */
+/*global deparam, grecaptcha, recaptchaOnLoad, syn_get_widget, userIsLoggedIn, VuFind */
+/*exported ajaxTagUpdate, recordDocReady */
 
 /**
  * Functions and event handlers specific to record pages.
@@ -6,103 +7,127 @@
 function checkRequestIsValid(element, requestType) {
   var recordId = element.href.match(/\/Record\/([^\/]+)\//)[1];
   var vars = deparam(element.href);
-  vars['id'] = recordId;
+  vars.id = recordId;
 
-  var url = VuFind.path + '/AJAX/JSON?' + $.param({method:'checkRequestIsValid', id: recordId, requestType: requestType, data: vars});
+  var url = VuFind.path + '/AJAX/JSON?' + $.param({
+    method: 'checkRequestIsValid',
+    id: recordId,
+    requestType: requestType,
+    data: vars
+  });
   $.ajax({
     dataType: 'json',
     cache: false,
     url: url
   })
-  .done(function(response) {
+  .done(function checkValidDone(response) {
     if (response.data.status) {
       $(element).removeClass('disabled')
         .attr('title', response.data.msg)
-        .html('<i class="fa fa-flag"></i>&nbsp;'+response.data.msg);
+        .html('<i class="fa fa-flag" aria-hidden="true"></i>&nbsp;' + response.data.msg);
     } else {
       $(element).remove();
     }
   })
-  .fail(function(response) {
+  .fail(function checkValidFail(/*response*/) {
     $(element).remove();
   });
 }
 
 function setUpCheckRequest() {
-  $('.checkRequest').each(function(i) {
+  $('.checkRequest').each(function checkRequest() {
     checkRequestIsValid(this, 'Hold');
   });
-  $('.checkStorageRetrievalRequest').each(function(i) {
+  $('.checkStorageRetrievalRequest').each(function checkStorageRetrievalRequest() {
     checkRequestIsValid(this, 'StorageRetrievalRequest');
   });
-  $('.checkILLRequest').each(function(i) {
+  $('.checkILLRequest').each(function checkILLRequest() {
     checkRequestIsValid(this, 'ILLRequest');
   });
 }
 
 function deleteRecordComment(element, recordId, recordSource, commentId) {
-  var url = VuFind.path + '/AJAX/JSON?' + $.param({method:'deleteRecordComment',id:commentId});
+  var url = VuFind.path + '/AJAX/JSON?' + $.param({ method: 'deleteRecordComment', id: commentId });
   $.ajax({
     dataType: 'json',
     url: url
   })
-  .done(function(response) {
+  .done(function deleteCommentDone(/*response*/) {
     $($(element).closest('.comment')[0]).remove();
   });
 }
 
 function refreshCommentList($target, recordId, recordSource) {
-  var url = VuFind.path + '/AJAX/JSON?' + $.param({method:'getRecordCommentsAsHTML',id:recordId,'source':recordSource});
+  var url = VuFind.path + '/AJAX/JSON?' + $.param({
+    method: 'getRecordCommentsAsHTML',
+    id: recordId,
+    source: recordSource
+  });
   $.ajax({
     dataType: 'json',
     url: url
   })
-  .done(function(response) {
+  .done(function refreshCommentListDone(response) {
     // Update HTML
     var $commentList = $target.find('.comment-list');
     $commentList.empty();
     $commentList.append(response.data);
-    $commentList.find('.delete').unbind('click').click(function() {
+    $commentList.find('.delete').unbind('click').click(function commentRefreshDeleteClick() {
       var commentId = $(this).attr('id').substr('recordComment'.length);
       deleteRecordComment(this, recordId, recordSource, commentId);
       return false;
     });
     $target.find('.comment-form input[type="submit"]').button('reset');
+    if (typeof grecaptcha !== 'undefined') {
+      grecaptcha.reset();
+    }
   });
 }
 
 function registerAjaxCommentRecord() {
   // Form submission
-  $('form.comment-form').unbind('submit').submit(function() {
+  $('form.comment-form').unbind('submit').submit(function commentFormSubmit() {
     var form = this;
     var id = form.id.value;
     var recordSource = form.source.value;
-    var url = VuFind.path + '/AJAX/JSON?' + $.param({method:'commentRecord'});
+    var url = VuFind.path + '/AJAX/JSON?' + $.param({ method: 'commentRecord' });
     var data = {
-      comment:form.comment.value,
-      id:id,
-      source:recordSource
+      comment: form.comment.value,
+      id: id,
+      source: recordSource
     };
+    if (typeof grecaptcha !== 'undefined') {
+      var recaptcha = $(form).find('.g-recaptcha');
+      if (recaptcha.length > 0) {
+        data['g-recaptcha-response'] = grecaptcha.getResponse(recaptcha.data('captchaId'));
+      }
+    }
     $.ajax({
       type: 'POST',
-      url:  url,
+      url: url,
       data: data,
       dataType: 'json'
     })
-    .done(function(response) {
-      var $tab = $(form).closest('.tab-pane');
+    .done(function addCommentDone(/*response, textStatus*/) {
+      var $tab = $(form).closest('.list-tab-content');
+      if (!$tab.length) {
+        $tab = $(form).closest('.tab-pane');
+      }
       refreshCommentList($tab, id, recordSource);
       $(form).find('textarea[name="comment"]').val('');
       $(form).find('input[type="submit"]').button('loading');
+      if (typeof grecaptcha !== 'undefined') {
+        grecaptcha.reset($(form).find('.g-recaptcha').data('captchaId'));
+      }
     })
-    .fail(function(response, textStatus) {
-      if (textStatus == 'abort' || typeof response.responseJSON === 'undefined') { return; }
-      VuFind.lightbox.update(response.responseJSON.data);
+    .fail(function addCommentFail(response, textStatus) {
+      if (textStatus === 'abort' || typeof response.responseJSON === 'undefined') { return; }
+      VuFind.lightbox.alert(response.responseJSON.data, 'danger');
     });
     return false;
   });
   // Delete links
-  $('.delete').click(function() {
+  $('.delete').click(function commentDeleteClick() {
     var commentId = this.id.substr('recordComment'.length);
     deleteRecordComment(this, $('.hiddenId').val(), $('.hiddenSource').val(), commentId);
     return false;
@@ -114,8 +139,13 @@ function registerAjaxCommentRecord() {
 function registerTabEvents() {
   // Logged in AJAX
   registerAjaxCommentRecord();
+  // Render recaptcha
+  recaptchaOnLoad();
   // Delete links
-  $('.delete').click(function(){deleteRecordComment(this, $('.hiddenId').val(), $('.hiddenSource').val(), this.id.substr(13));return false;});
+  $('.delete').click(function commentTabDeleteClick() {
+    deleteRecordComment(this, $('.hiddenId').val(), $('.hiddenSource').val(), this.id.substr(13));
+    return false;
+  });
 
   setUpCheckRequest();
 
@@ -134,7 +164,7 @@ function ajaxLoadTab($newTab, tabid, setHash) {
     urlroot = '/' + chunks[3] + '/' + chunks[4];
   } else {
     // standard case -- VuFind has its own path under site:
-    var pathInUrl = urlWithoutFragment.indexOf(path);
+    var pathInUrl = urlWithoutFragment.indexOf(path, urlWithoutFragment.indexOf('//') + 2);
     var parts = urlWithoutFragment.substring(pathInUrl + path.length + 1).split('/');
     urlroot = '/' + parts[0] + '/' + parts[1];
   }
@@ -145,37 +175,41 @@ function ajaxLoadTab($newTab, tabid, setHash) {
     type: 'POST',
     data: {tab: tabid}
   })
-  .done(function(data) {
+  .done(function ajaxLoadTabDone(data) {
     $newTab.html(data);
     registerTabEvents();
-    if(typeof syn_get_widget === "function") {
+    if (typeof syn_get_widget === "function") {
       syn_get_widget();
     }
     if (typeof setHash == 'undefined' || setHash) {
       window.location.hash = tabid;
+    } else {
+      removeHashFromLocation();
     }
   });
   return false;
 }
 
-function refreshTagList(target, loggedin) {
-  loggedin = !!loggedin || userIsLoggedIn;
-  if (typeof target === 'undefined') {
-    target = document;
-  }
+function refreshTagList(_target, _loggedin) {
+  var loggedin = !!_loggedin || userIsLoggedIn;
+  var target = _target || document;
   var recordId = $(target).find('.hiddenId').val();
   var recordSource = $(target).find('.hiddenSource').val();
   var $tagList = $(target).find('.tagList');
   if ($tagList.length > 0) {
-    var url = VuFind.path + '/AJAX/JSON?' + $.param({method:'getRecordTags',id:recordId,'source':recordSource});
+    var url = VuFind.path + '/AJAX/JSON?' + $.param({
+      method: 'getRecordTags',
+      id: recordId,
+      source: recordSource
+    });
     $.ajax({
       dataType: 'html',
       url: url
     })
-    .done(function(response) {
+    .done(function getRecordTagsDone(response) {
       $tagList.empty();
       $tagList.replaceWith(response);
-      if(loggedin) {
+      if (loggedin) {
         $tagList.addClass('loggedin');
       } else {
         $tagList.removeClass('loggedin');
@@ -184,49 +218,67 @@ function refreshTagList(target, loggedin) {
   }
 }
 
-function ajaxTagUpdate(link, tag, remove) {
-  if(typeof link === "undefined") {
-    link = document;
-  }
-  if(typeof remove === "undefined") {
-    remove = false;
-  }
+function ajaxTagUpdate(_link, tag, _remove) {
+  var link = _link || document;
+  var remove = _remove || false;
   var $target = $(link).closest('.record');
   var recordId = $target.find('.hiddenId').val();
   var recordSource = $target.find('.hiddenSource').val();
   $.ajax({
-    url:VuFind.path + '/AJAX/JSON?method=tagRecord',
-    method:'POST',
-    data:{
-      tag:'"'+tag.replace(/\+/g, ' ')+'"',
-      id:recordId,
-      source:recordSource,
-      remove:remove
+    url: VuFind.path + '/AJAX/JSON?method=tagRecord',
+    method: 'POST',
+    data: {
+      tag: '"' + tag.replace(/\+/g, ' ') + '"',
+      id: recordId,
+      source: recordSource,
+      remove: remove
     }
   })
-  .always(function() {
+  .always(function tagRecordAlways() {
     refreshTagList($target, false);
   });
 }
 
+function getNewRecordTab(tabid) {
+  return $('<div class="tab-pane ' + tabid + '-tab"><i class="fa fa-spinner fa-spin" aria-hidden="true"></i> ' + VuFind.translate('loading') + '...</div>');
+}
+
+function backgroundLoadTab(tabid) {
+  if ($('.' + tabid + '-tab').length > 0) {
+    return;
+  }
+  var newTab = getNewRecordTab(tabid);
+  $('.nav-tabs a.' + tabid).closest('.result,.record').find('.tab-content').append(newTab);
+  return ajaxLoadTab(newTab, tabid, false);
+}
+
 function applyRecordTabHash() {
   var activeTab = $('.record-tabs li.active a').attr('class');
   var $initiallyActiveTab = $('.record-tabs li.initiallyActive a');
   var newTab = typeof window.location.hash !== 'undefined'
     ? window.location.hash.toLowerCase() : '';
 
-  // Open tag in url hash
-  if (newTab.length == 0 || newTab == '#tabnav') {
+  // Open tab in url hash
+  if (newTab.length <= 1 || newTab === '#tabnav') {
     $initiallyActiveTab.click();
-  } else if (newTab.length > 0 && '#' + activeTab != newTab) {
-    $('.'+newTab.substr(1)).click();
+  } else if (newTab.length > 1 && '#' + activeTab !== newTab) {
+    $('.' + newTab.substr(1)).click();
+  }
+}
+
+function removeHashFromLocation() {
+  if (window.history.replaceState) {
+    var href = window.location.href.split('#');
+    window.history.replaceState({}, document.title, href[0]);  
+  } else {
+    window.location.hash = '#';  
   }
 }
 
 $(window).on('hashchange', applyRecordTabHash);
 
 function recordDocReady() {
-  $('.record-tabs .nav-tabs a').click(function (e) {
+  $('.record-tabs .nav-tabs a').click(function recordTabsClick() {
     var $li = $(this).parent();
     // If it's an active tab, click again to follow to a shareable link.
     if ($li.hasClass('active')) {
@@ -241,7 +293,7 @@ function recordDocReady() {
       if ($li.hasClass('initiallyActive')) {
         $(this).tab('show');
         $top.find('.tab-pane.active').removeClass('active');
-        $top.find('.'+tabid+'-tab').addClass('active');
+        $top.find('.' + tabid + '-tab').addClass('active');
         window.location.hash = 'tabnav';
         return false;
       }
@@ -250,17 +302,25 @@ function recordDocReady() {
     }
     $top.find('.tab-pane.active').removeClass('active');
     $(this).tab('show');
-    if ($top.find('.'+tabid+'-tab').length > 0) {
-      $top.find('.'+tabid+'-tab').addClass('active');
-      window.location.hash = tabid;
+    if ($top.find('.' + tabid + '-tab').length > 0) {
+      $top.find('.' + tabid + '-tab').addClass('active');
+      if ($(this).parent().hasClass('initiallyActive')) {
+        removeHashFromLocation();
+      } else {
+        window.location.hash = tabid;
+      }
       return false;
     } else {
-      var newTab = $('<div class="tab-pane active '+tabid+'-tab"><i class="fa fa-spinner fa-spin"></i> '+VuFind.translate('loading')+'...</div>');
+      var newTab = getNewRecordTab(tabid).addClass('active');
       $top.find('.tab-content').append(newTab);
       return ajaxLoadTab(newTab, tabid, !$(this).parent().hasClass('initiallyActive'));
     }
   });
 
+  $('[data-background]').each(function setupBackgroundTabs(index, el) {
+    backgroundLoadTab(el.className);
+  });
+
   registerTabEvents();
   applyRecordTabHash();
 }
diff --git a/themes/bootstrap3/js/vendor/bootlint.min.js b/themes/bootstrap3/js/vendor/bootlint.min.js
deleted file mode 100644
index a3e3aef31c2c4cd0ebe2fff3f391517576b695a2..0000000000000000000000000000000000000000
--- a/themes/bootstrap3/js/vendor/bootlint.min.js
+++ /dev/null
@@ -1,9902 +0,0 @@
-/*!
- * Bootlint v0.4.0 (https://github.com/twbs/bootlint)
- * HTML linter for Bootstrap projects
- * Copyright (c) 2014 Christopher Rebert
- * Licensed under the MIT License (https://github.com/twbs/bootlint/blob/master/LICENSE).
- */
-
-(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
-/*!
- * jQuery JavaScript Library v2.1.1
- * http://jquery.com/
- *
- * Includes Sizzle.js
- * http://sizzlejs.com/
- *
- * Copyright 2005, 2014 jQuery Foundation, Inc. and other contributors
- * Released under the MIT license
- * http://jquery.org/license
- *
- * Date: 2014-05-01T17:11Z
- */
-
-(function( global, factory ) {
-
-	if ( typeof module === "object" && typeof module.exports === "object" ) {
-		// For CommonJS and CommonJS-like environments where a proper window is present,
-		// execute the factory and get jQuery
-		// For environments that do not inherently posses a window with a document
-		// (such as Node.js), expose a jQuery-making factory as module.exports
-		// This accentuates the need for the creation of a real window
-		// e.g. var jQuery = require("jquery")(window);
-		// See ticket #14549 for more info
-		module.exports = global.document ?
-			factory( global, true ) :
-			function( w ) {
-				if ( !w.document ) {
-					throw new Error( "jQuery requires a window with a document" );
-				}
-				return factory( w );
-			};
-	} else {
-		factory( global );
-	}
-
-// Pass this if window is not defined yet
-}(typeof window !== "undefined" ? window : this, function( window, noGlobal ) {
-
-// Can't do this because several apps including ASP.NET trace
-// the stack via arguments.caller.callee and Firefox dies if
-// you try to trace through "use strict" call chains. (#13335)
-// Support: Firefox 18+
-//
-
-var arr = [];
-
-var slice = arr.slice;
-
-var concat = arr.concat;
-
-var push = arr.push;
-
-var indexOf = arr.indexOf;
-
-var class2type = {};
-
-var toString = class2type.toString;
-
-var hasOwn = class2type.hasOwnProperty;
-
-var support = {};
-
-
-
-var
-	// Use the correct document accordingly with window argument (sandbox)
-	document = window.document,
-
-	version = "2.1.1",
-
-	// Define a local copy of jQuery
-	jQuery = function( selector, context ) {
-		// The jQuery object is actually just the init constructor 'enhanced'
-		// Need init if jQuery is called (just allow error to be thrown if not included)
-		return new jQuery.fn.init( selector, context );
-	},
-
-	// Support: Android<4.1
-	// Make sure we trim BOM and NBSP
-	rtrim = /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,
-
-	// Matches dashed string for camelizing
-	rmsPrefix = /^-ms-/,
-	rdashAlpha = /-([\da-z])/gi,
-
-	// Used by jQuery.camelCase as callback to replace()
-	fcamelCase = function( all, letter ) {
-		return letter.toUpperCase();
-	};
-
-jQuery.fn = jQuery.prototype = {
-	// The current version of jQuery being used
-	jquery: version,
-
-	constructor: jQuery,
-
-	// Start with an empty selector
-	selector: "",
-
-	// The default length of a jQuery object is 0
-	length: 0,
-
-	toArray: function() {
-		return slice.call( this );
-	},
-
-	// Get the Nth element in the matched element set OR
-	// Get the whole matched element set as a clean array
-	get: function( num ) {
-		return num != null ?
-
-			// Return just the one element from the set
-			( num < 0 ? this[ num + this.length ] : this[ num ] ) :
-
-			// Return all the elements in a clean array
-			slice.call( this );
-	},
-
-	// Take an array of elements and push it onto the stack
-	// (returning the new matched element set)
-	pushStack: function( elems ) {
-
-		// Build a new jQuery matched element set
-		var ret = jQuery.merge( this.constructor(), elems );
-
-		// Add the old object onto the stack (as a reference)
-		ret.prevObject = this;
-		ret.context = this.context;
-
-		// Return the newly-formed element set
-		return ret;
-	},
-
-	// Execute a callback for every element in the matched set.
-	// (You can seed the arguments with an array of args, but this is
-	// only used internally.)
-	each: function( callback, args ) {
-		return jQuery.each( this, callback, args );
-	},
-
-	map: function( callback ) {
-		return this.pushStack( jQuery.map(this, function( elem, i ) {
-			return callback.call( elem, i, elem );
-		}));
-	},
-
-	slice: function() {
-		return this.pushStack( slice.apply( this, arguments ) );
-	},
-
-	first: function() {
-		return this.eq( 0 );
-	},
-
-	last: function() {
-		return this.eq( -1 );
-	},
-
-	eq: function( i ) {
-		var len = this.length,
-			j = +i + ( i < 0 ? len : 0 );
-		return this.pushStack( j >= 0 && j < len ? [ this[j] ] : [] );
-	},
-
-	end: function() {
-		return this.prevObject || this.constructor(null);
-	},
-
-	// For internal use only.
-	// Behaves like an Array's method, not like a jQuery method.
-	push: push,
-	sort: arr.sort,
-	splice: arr.splice
-};
-
-jQuery.extend = jQuery.fn.extend = function() {
-	var options, name, src, copy, copyIsArray, clone,
-		target = arguments[0] || {},
-		i = 1,
-		length = arguments.length,
-		deep = false;
-
-	// Handle a deep copy situation
-	if ( typeof target === "boolean" ) {
-		deep = target;
-
-		// skip the boolean and the target
-		target = arguments[ i ] || {};
-		i++;
-	}
-
-	// Handle case when target is a string or something (possible in deep copy)
-	if ( typeof target !== "object" && !jQuery.isFunction(target) ) {
-		target = {};
-	}
-
-	// extend jQuery itself if only one argument is passed
-	if ( i === length ) {
-		target = this;
-		i--;
-	}
-
-	for ( ; i < length; i++ ) {
-		// Only deal with non-null/undefined values
-		if ( (options = arguments[ i ]) != null ) {
-			// Extend the base object
-			for ( name in options ) {
-				src = target[ name ];
-				copy = options[ name ];
-
-				// Prevent never-ending loop
-				if ( target === copy ) {
-					continue;
-				}
-
-				// Recurse if we're merging plain objects or arrays
-				if ( deep && copy && ( jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)) ) ) {
-					if ( copyIsArray ) {
-						copyIsArray = false;
-						clone = src && jQuery.isArray(src) ? src : [];
-
-					} else {
-						clone = src && jQuery.isPlainObject(src) ? src : {};
-					}
-
-					// Never move original objects, clone them
-					target[ name ] = jQuery.extend( deep, clone, copy );
-
-				// Don't bring in undefined values
-				} else if ( copy !== undefined ) {
-					target[ name ] = copy;
-				}
-			}
-		}
-	}
-
-	// Return the modified object
-	return target;
-};
-
-jQuery.extend({
-	// Unique for each copy of jQuery on the page
-	expando: "jQuery" + ( version + Math.random() ).replace( /\D/g, "" ),
-
-	// Assume jQuery is ready without the ready module
-	isReady: true,
-
-	error: function( msg ) {
-		throw new Error( msg );
-	},
-
-	noop: function() {},
-
-	// See test/unit/core.js for details concerning isFunction.
-	// Since version 1.3, DOM methods and functions like alert
-	// aren't supported. They return false on IE (#2968).
-	isFunction: function( obj ) {
-		return jQuery.type(obj) === "function";
-	},
-
-	isArray: Array.isArray,
-
-	isWindow: function( obj ) {
-		return obj != null && obj === obj.window;
-	},
-
-	isNumeric: function( obj ) {
-		// parseFloat NaNs numeric-cast false positives (null|true|false|"")
-		// ...but misinterprets leading-number strings, particularly hex literals ("0x...")
-		// subtraction forces infinities to NaN
-		return !jQuery.isArray( obj ) && obj - parseFloat( obj ) >= 0;
-	},
-
-	isPlainObject: function( obj ) {
-		// Not plain objects:
-		// - Any object or value whose internal [[Class]] property is not "[object Object]"
-		// - DOM nodes
-		// - window
-		if ( jQuery.type( obj ) !== "object" || obj.nodeType || jQuery.isWindow( obj ) ) {
-			return false;
-		}
-
-		if ( obj.constructor &&
-				!hasOwn.call( obj.constructor.prototype, "isPrototypeOf" ) ) {
-			return false;
-		}
-
-		// If the function hasn't returned already, we're confident that
-		// |obj| is a plain object, created by {} or constructed with new Object
-		return true;
-	},
-
-	isEmptyObject: function( obj ) {
-		var name;
-		for ( name in obj ) {
-			return false;
-		}
-		return true;
-	},
-
-	type: function( obj ) {
-		if ( obj == null ) {
-			return obj + "";
-		}
-		// Support: Android < 4.0, iOS < 6 (functionish RegExp)
-		return typeof obj === "object" || typeof obj === "function" ?
-			class2type[ toString.call(obj) ] || "object" :
-			typeof obj;
-	},
-
-	// Evaluates a script in a global context
-	globalEval: function( code ) {
-		var script,
-			indirect = eval;
-
-		code = jQuery.trim( code );
-
-		if ( code ) {
-			// If the code includes a valid, prologue position
-			// strict mode pragma, execute code by injecting a
-			// script tag into the document.
-			if ( code.indexOf("use strict") === 1 ) {
-				script = document.createElement("script");
-				script.text = code;
-				document.head.appendChild( script ).parentNode.removeChild( script );
-			} else {
-			// Otherwise, avoid the DOM node creation, insertion
-			// and removal by using an indirect global eval
-				indirect( code );
-			}
-		}
-	},
-
-	// Convert dashed to camelCase; used by the css and data modules
-	// Microsoft forgot to hump their vendor prefix (#9572)
-	camelCase: function( string ) {
-		return string.replace( rmsPrefix, "ms-" ).replace( rdashAlpha, fcamelCase );
-	},
-
-	nodeName: function( elem, name ) {
-		return elem.nodeName && elem.nodeName.toLowerCase() === name.toLowerCase();
-	},
-
-	// args is for internal usage only
-	each: function( obj, callback, args ) {
-		var value,
-			i = 0,
-			length = obj.length,
-			isArray = isArraylike( obj );
-
-		if ( args ) {
-			if ( isArray ) {
-				for ( ; i < length; i++ ) {
-					value = callback.apply( obj[ i ], args );
-
-					if ( value === false ) {
-						break;
-					}
-				}
-			} else {
-				for ( i in obj ) {
-					value = callback.apply( obj[ i ], args );
-
-					if ( value === false ) {
-						break;
-					}
-				}
-			}
-
-		// A special, fast, case for the most common use of each
-		} else {
-			if ( isArray ) {
-				for ( ; i < length; i++ ) {
-					value = callback.call( obj[ i ], i, obj[ i ] );
-
-					if ( value === false ) {
-						break;
-					}
-				}
-			} else {
-				for ( i in obj ) {
-					value = callback.call( obj[ i ], i, obj[ i ] );
-
-					if ( value === false ) {
-						break;
-					}
-				}
-			}
-		}
-
-		return obj;
-	},
-
-	// Support: Android<4.1
-	trim: function( text ) {
-		return text == null ?
-			"" :
-			( text + "" ).replace( rtrim, "" );
-	},
-
-	// results is for internal usage only
-	makeArray: function( arr, results ) {
-		var ret = results || [];
-
-		if ( arr != null ) {
-			if ( isArraylike( Object(arr) ) ) {
-				jQuery.merge( ret,
-					typeof arr === "string" ?
-					[ arr ] : arr
-				);
-			} else {
-				push.call( ret, arr );
-			}
-		}
-
-		return ret;
-	},
-
-	inArray: function( elem, arr, i ) {
-		return arr == null ? -1 : indexOf.call( arr, elem, i );
-	},
-
-	merge: function( first, second ) {
-		var len = +second.length,
-			j = 0,
-			i = first.length;
-
-		for ( ; j < len; j++ ) {
-			first[ i++ ] = second[ j ];
-		}
-
-		first.length = i;
-
-		return first;
-	},
-
-	grep: function( elems, callback, invert ) {
-		var callbackInverse,
-			matches = [],
-			i = 0,
-			length = elems.length,
-			callbackExpect = !invert;
-
-		// Go through the array, only saving the items
-		// that pass the validator function
-		for ( ; i < length; i++ ) {
-			callbackInverse = !callback( elems[ i ], i );
-			if ( callbackInverse !== callbackExpect ) {
-				matches.push( elems[ i ] );
-			}
-		}
-
-		return matches;
-	},
-
-	// arg is for internal usage only
-	map: function( elems, callback, arg ) {
-		var value,
-			i = 0,
-			length = elems.length,
-			isArray = isArraylike( elems ),
-			ret = [];
-
-		// Go through the array, translating each of the items to their new values
-		if ( isArray ) {
-			for ( ; i < length; i++ ) {
-				value = callback( elems[ i ], i, arg );
-
-				if ( value != null ) {
-					ret.push( value );
-				}
-			}
-
-		// Go through every key on the object,
-		} else {
-			for ( i in elems ) {
-				value = callback( elems[ i ], i, arg );
-
-				if ( value != null ) {
-					ret.push( value );
-				}
-			}
-		}
-
-		// Flatten any nested arrays
-		return concat.apply( [], ret );
-	},
-
-	// A global GUID counter for objects
-	guid: 1,
-
-	// Bind a function to a context, optionally partially applying any
-	// arguments.
-	proxy: function( fn, context ) {
-		var tmp, args, proxy;
-
-		if ( typeof context === "string" ) {
-			tmp = fn[ context ];
-			context = fn;
-			fn = tmp;
-		}
-
-		// Quick check to determine if target is callable, in the spec
-		// this throws a TypeError, but we will just return undefined.
-		if ( !jQuery.isFunction( fn ) ) {
-			return undefined;
-		}
-
-		// Simulated bind
-		args = slice.call( arguments, 2 );
-		proxy = function() {
-			return fn.apply( context || this, args.concat( slice.call( arguments ) ) );
-		};
-
-		// Set the guid of unique handler to the same of original handler, so it can be removed
-		proxy.guid = fn.guid = fn.guid || jQuery.guid++;
-
-		return proxy;
-	},
-
-	now: Date.now,
-
-	// jQuery.support is not used in Core but other projects attach their
-	// properties to it so it needs to exist.
-	support: support
-});
-
-// Populate the class2type map
-jQuery.each("Boolean Number String Function Array Date RegExp Object Error".split(" "), function(i, name) {
-	class2type[ "[object " + name + "]" ] = name.toLowerCase();
-});
-
-function isArraylike( obj ) {
-	var length = obj.length,
-		type = jQuery.type( obj );
-
-	if ( type === "function" || jQuery.isWindow( obj ) ) {
-		return false;
-	}
-
-	if ( obj.nodeType === 1 && length ) {
-		return true;
-	}
-
-	return type === "array" || length === 0 ||
-		typeof length === "number" && length > 0 && ( length - 1 ) in obj;
-}
-var Sizzle =
-/*!
- * Sizzle CSS Selector Engine v1.10.19
- * http://sizzlejs.com/
- *
- * Copyright 2013 jQuery Foundation, Inc. and other contributors
- * Released under the MIT license
- * http://jquery.org/license
- *
- * Date: 2014-04-18
- */
-(function( window ) {
-
-var i,
-	support,
-	Expr,
-	getText,
-	isXML,
-	tokenize,
-	compile,
-	select,
-	outermostContext,
-	sortInput,
-	hasDuplicate,
-
-	// Local document vars
-	setDocument,
-	document,
-	docElem,
-	documentIsHTML,
-	rbuggyQSA,
-	rbuggyMatches,
-	matches,
-	contains,
-
-	// Instance-specific data
-	expando = "sizzle" + -(new Date()),
-	preferredDoc = window.document,
-	dirruns = 0,
-	done = 0,
-	classCache = createCache(),
-	tokenCache = createCache(),
-	compilerCache = createCache(),
-	sortOrder = function( a, b ) {
-		if ( a === b ) {
-			hasDuplicate = true;
-		}
-		return 0;
-	},
-
-	// General-purpose constants
-	strundefined = typeof undefined,
-	MAX_NEGATIVE = 1 << 31,
-
-	// Instance methods
-	hasOwn = ({}).hasOwnProperty,
-	arr = [],
-	pop = arr.pop,
-	push_native = arr.push,
-	push = arr.push,
-	slice = arr.slice,
-	// Use a stripped-down indexOf if we can't use a native one
-	indexOf = arr.indexOf || function( elem ) {
-		var i = 0,
-			len = this.length;
-		for ( ; i < len; i++ ) {
-			if ( this[i] === elem ) {
-				return i;
-			}
-		}
-		return -1;
-	},
-
-	booleans = "checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",
-
-	// Regular expressions
-
-	// Whitespace characters http://www.w3.org/TR/css3-selectors/#whitespace
-	whitespace = "[\\x20\\t\\r\\n\\f]",
-	// http://www.w3.org/TR/css3-syntax/#characters
-	characterEncoding = "(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",
-
-	// Loosely modeled on CSS identifier characters
-	// An unquoted value should be a CSS identifier http://www.w3.org/TR/css3-selectors/#attribute-selectors
-	// Proper syntax: http://www.w3.org/TR/CSS21/syndata.html#value-def-identifier
-	identifier = characterEncoding.replace( "w", "w#" ),
-
-	// Attribute selectors: http://www.w3.org/TR/selectors/#attribute-selectors
-	attributes = "\\[" + whitespace + "*(" + characterEncoding + ")(?:" + whitespace +
-		// Operator (capture 2)
-		"*([*^$|!~]?=)" + whitespace +
-		// "Attribute values must be CSS identifiers [capture 5] or strings [capture 3 or capture 4]"
-		"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|(" + identifier + "))|)" + whitespace +
-		"*\\]",
-
-	pseudos = ":(" + characterEncoding + ")(?:\\((" +
-		// To reduce the number of selectors needing tokenize in the preFilter, prefer arguments:
-		// 1. quoted (capture 3; capture 4 or capture 5)
-		"('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|" +
-		// 2. simple (capture 6)
-		"((?:\\\\.|[^\\\\()[\\]]|" + attributes + ")*)|" +
-		// 3. anything else (capture 2)
-		".*" +
-		")\\)|)",
-
-	// Leading and non-escaped trailing whitespace, capturing some non-whitespace characters preceding the latter
-	rtrim = new RegExp( "^" + whitespace + "+|((?:^|[^\\\\])(?:\\\\.)*)" + whitespace + "+$", "g" ),
-
-	rcomma = new RegExp( "^" + whitespace + "*," + whitespace + "*" ),
-	rcombinators = new RegExp( "^" + whitespace + "*([>+~]|" + whitespace + ")" + whitespace + "*" ),
-
-	rattributeQuotes = new RegExp( "=" + whitespace + "*([^\\]'\"]*?)" + whitespace + "*\\]", "g" ),
-
-	rpseudo = new RegExp( pseudos ),
-	ridentifier = new RegExp( "^" + identifier + "$" ),
-
-	matchExpr = {
-		"ID": new RegExp( "^#(" + characterEncoding + ")" ),
-		"CLASS": new RegExp( "^\\.(" + characterEncoding + ")" ),
-		"TAG": new RegExp( "^(" + characterEncoding.replace( "w", "w*" ) + ")" ),
-		"ATTR": new RegExp( "^" + attributes ),
-		"PSEUDO": new RegExp( "^" + pseudos ),
-		"CHILD": new RegExp( "^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\(" + whitespace +
-			"*(even|odd|(([+-]|)(\\d*)n|)" + whitespace + "*(?:([+-]|)" + whitespace +
-			"*(\\d+)|))" + whitespace + "*\\)|)", "i" ),
-		"bool": new RegExp( "^(?:" + booleans + ")$", "i" ),
-		// For use in libraries implementing .is()
-		// We use this for POS matching in `select`
-		"needsContext": new RegExp( "^" + whitespace + "*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\(" +
-			whitespace + "*((?:-\\d)?\\d*)" + whitespace + "*\\)|)(?=[^-]|$)", "i" )
-	},
-
-	rinputs = /^(?:input|select|textarea|button)$/i,
-	rheader = /^h\d$/i,
-
-	rnative = /^[^{]+\{\s*\[native \w/,
-
-	// Easily-parseable/retrievable ID or TAG or CLASS selectors
-	rquickExpr = /^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,
-
-	rsibling = /[+~]/,
-	rescape = /'|\\/g,
-
-	// CSS escapes http://www.w3.org/TR/CSS21/syndata.html#escaped-characters
-	runescape = new RegExp( "\\\\([\\da-f]{1,6}" + whitespace + "?|(" + whitespace + ")|.)", "ig" ),
-	funescape = function( _, escaped, escapedWhitespace ) {
-		var high = "0x" + escaped - 0x10000;
-		// NaN means non-codepoint
-		// Support: Firefox<24
-		// Workaround erroneous numeric interpretation of +"0x"
-		return high !== high || escapedWhitespace ?
-			escaped :
-			high < 0 ?
-				// BMP codepoint
-				String.fromCharCode( high + 0x10000 ) :
-				// Supplemental Plane codepoint (surrogate pair)
-				String.fromCharCode( high >> 10 | 0xD800, high & 0x3FF | 0xDC00 );
-	};
-
-// Optimize for push.apply( _, NodeList )
-try {
-	push.apply(
-		(arr = slice.call( preferredDoc.childNodes )),
-		preferredDoc.childNodes
-	);
-	// Support: Android<4.0
-	// Detect silently failing push.apply
-	arr[ preferredDoc.childNodes.length ].nodeType;
-} catch ( e ) {
-	push = { apply: arr.length ?
-
-		// Leverage slice if possible
-		function( target, els ) {
-			push_native.apply( target, slice.call(els) );
-		} :
-
-		// Support: IE<9
-		// Otherwise append directly
-		function( target, els ) {
-			var j = target.length,
-				i = 0;
-			// Can't trust NodeList.length
-			while ( (target[j++] = els[i++]) ) {}
-			target.length = j - 1;
-		}
-	};
-}
-
-function Sizzle( selector, context, results, seed ) {
-	var match, elem, m, nodeType,
-		// QSA vars
-		i, groups, old, nid, newContext, newSelector;
-
-	if ( ( context ? context.ownerDocument || context : preferredDoc ) !== document ) {
-		setDocument( context );
-	}
-
-	context = context || document;
-	results = results || [];
-
-	if ( !selector || typeof selector !== "string" ) {
-		return results;
-	}
-
-	if ( (nodeType = context.nodeType) !== 1 && nodeType !== 9 ) {
-		return [];
-	}
-
-	if ( documentIsHTML && !seed ) {
-
-		// Shortcuts
-		if ( (match = rquickExpr.exec( selector )) ) {
-			// Speed-up: Sizzle("#ID")
-			if ( (m = match[1]) ) {
-				if ( nodeType === 9 ) {
-					elem = context.getElementById( m );
-					// Check parentNode to catch when Blackberry 4.6 returns
-					// nodes that are no longer in the document (jQuery #6963)
-					if ( elem && elem.parentNode ) {
-						// Handle the case where IE, Opera, and Webkit return items
-						// by name instead of ID
-						if ( elem.id === m ) {
-							results.push( elem );
-							return results;
-						}
-					} else {
-						return results;
-					}
-				} else {
-					// Context is not a document
-					if ( context.ownerDocument && (elem = context.ownerDocument.getElementById( m )) &&
-						contains( context, elem ) && elem.id === m ) {
-						results.push( elem );
-						return results;
-					}
-				}
-
-			// Speed-up: Sizzle("TAG")
-			} else if ( match[2] ) {
-				push.apply( results, context.getElementsByTagName( selector ) );
-				return results;
-
-			// Speed-up: Sizzle(".CLASS")
-			} else if ( (m = match[3]) && support.getElementsByClassName && context.getElementsByClassName ) {
-				push.apply( results, context.getElementsByClassName( m ) );
-				return results;
-			}
-		}
-
-		// QSA path
-		if ( support.qsa && (!rbuggyQSA || !rbuggyQSA.test( selector )) ) {
-			nid = old = expando;
-			newContext = context;
-			newSelector = nodeType === 9 && selector;
-
-			// qSA works strangely on Element-rooted queries
-			// We can work around this by specifying an extra ID on the root
-			// and working up from there (Thanks to Andrew Dupont for the technique)
-			// IE 8 doesn't work on object elements
-			if ( nodeType === 1 && context.nodeName.toLowerCase() !== "object" ) {
-				groups = tokenize( selector );
-
-				if ( (old = context.getAttribute("id")) ) {
-					nid = old.replace( rescape, "\\$&" );
-				} else {
-					context.setAttribute( "id", nid );
-				}
-				nid = "[id='" + nid + "'] ";
-
-				i = groups.length;
-				while ( i-- ) {
-					groups[i] = nid + toSelector( groups[i] );
-				}
-				newContext = rsibling.test( selector ) && testContext( context.parentNode ) || context;
-				newSelector = groups.join(",");
-			}
-
-			if ( newSelector ) {
-				try {
-					push.apply( results,
-						newContext.querySelectorAll( newSelector )
-					);
-					return results;
-				} catch(qsaError) {
-				} finally {
-					if ( !old ) {
-						context.removeAttribute("id");
-					}
-				}
-			}
-		}
-	}
-
-	// All others
-	return select( selector.replace( rtrim, "$1" ), context, results, seed );
-}
-
-/**
- * Create key-value caches of limited size
- * @returns {Function(string, Object)} Returns the Object data after storing it on itself with
- *	property name the (space-suffixed) string and (if the cache is larger than Expr.cacheLength)
- *	deleting the oldest entry
- */
-function createCache() {
-	var keys = [];
-
-	function cache( key, value ) {
-		// Use (key + " ") to avoid collision with native prototype properties (see Issue #157)
-		if ( keys.push( key + " " ) > Expr.cacheLength ) {
-			// Only keep the most recent entries
-			delete cache[ keys.shift() ];
-		}
-		return (cache[ key + " " ] = value);
-	}
-	return cache;
-}
-
-/**
- * Mark a function for special use by Sizzle
- * @param {Function} fn The function to mark
- */
-function markFunction( fn ) {
-	fn[ expando ] = true;
-	return fn;
-}
-
-/**
- * Support testing using an element
- * @param {Function} fn Passed the created div and expects a boolean result
- */
-function assert( fn ) {
-	var div = document.createElement("div");
-
-	try {
-		return !!fn( div );
-	} catch (e) {
-		return false;
-	} finally {
-		// Remove from its parent by default
-		if ( div.parentNode ) {
-			div.parentNode.removeChild( div );
-		}
-		// release memory in IE
-		div = null;
-	}
-}
-
-/**
- * Adds the same handler for all of the specified attrs
- * @param {String} attrs Pipe-separated list of attributes
- * @param {Function} handler The method that will be applied
- */
-function addHandle( attrs, handler ) {
-	var arr = attrs.split("|"),
-		i = attrs.length;
-
-	while ( i-- ) {
-		Expr.attrHandle[ arr[i] ] = handler;
-	}
-}
-
-/**
- * Checks document order of two siblings
- * @param {Element} a
- * @param {Element} b
- * @returns {Number} Returns less than 0 if a precedes b, greater than 0 if a follows b
- */
-function siblingCheck( a, b ) {
-	var cur = b && a,
-		diff = cur && a.nodeType === 1 && b.nodeType === 1 &&
-			( ~b.sourceIndex || MAX_NEGATIVE ) -
-			( ~a.sourceIndex || MAX_NEGATIVE );
-
-	// Use IE sourceIndex if available on both nodes
-	if ( diff ) {
-		return diff;
-	}
-
-	// Check if b follows a
-	if ( cur ) {
-		while ( (cur = cur.nextSibling) ) {
-			if ( cur === b ) {
-				return -1;
-			}
-		}
-	}
-
-	return a ? 1 : -1;
-}
-
-/**
- * Returns a function to use in pseudos for input types
- * @param {String} type
- */
-function createInputPseudo( type ) {
-	return function( elem ) {
-		var name = elem.nodeName.toLowerCase();
-		return name === "input" && elem.type === type;
-	};
-}
-
-/**
- * Returns a function to use in pseudos for buttons
- * @param {String} type
- */
-function createButtonPseudo( type ) {
-	return function( elem ) {
-		var name = elem.nodeName.toLowerCase();
-		return (name === "input" || name === "button") && elem.type === type;
-	};
-}
-
-/**
- * Returns a function to use in pseudos for positionals
- * @param {Function} fn
- */
-function createPositionalPseudo( fn ) {
-	return markFunction(function( argument ) {
-		argument = +argument;
-		return markFunction(function( seed, matches ) {
-			var j,
-				matchIndexes = fn( [], seed.length, argument ),
-				i = matchIndexes.length;
-
-			// Match elements found at the specified indexes
-			while ( i-- ) {
-				if ( seed[ (j = matchIndexes[i]) ] ) {
-					seed[j] = !(matches[j] = seed[j]);
-				}
-			}
-		});
-	});
-}
-
-/**
- * Checks a node for validity as a Sizzle context
- * @param {Element|Object=} context
- * @returns {Element|Object|Boolean} The input node if acceptable, otherwise a falsy value
- */
-function testContext( context ) {
-	return context && typeof context.getElementsByTagName !== strundefined && context;
-}
-
-// Expose support vars for convenience
-support = Sizzle.support = {};
-
-/**
- * Detects XML nodes
- * @param {Element|Object} elem An element or a document
- * @returns {Boolean} True iff elem is a non-HTML XML node
- */
-isXML = Sizzle.isXML = function( elem ) {
-	// documentElement is verified for cases where it doesn't yet exist
-	// (such as loading iframes in IE - #4833)
-	var documentElement = elem && (elem.ownerDocument || elem).documentElement;
-	return documentElement ? documentElement.nodeName !== "HTML" : false;
-};
-
-/**
- * Sets document-related variables once based on the current document
- * @param {Element|Object} [doc] An element or document object to use to set the document
- * @returns {Object} Returns the current document
- */
-setDocument = Sizzle.setDocument = function( node ) {
-	var hasCompare,
-		doc = node ? node.ownerDocument || node : preferredDoc,
-		parent = doc.defaultView;
-
-	// If no document and documentElement is available, return
-	if ( doc === document || doc.nodeType !== 9 || !doc.documentElement ) {
-		return document;
-	}
-
-	// Set our document
-	document = doc;
-	docElem = doc.documentElement;
-
-	// Support tests
-	documentIsHTML = !isXML( doc );
-
-	// Support: IE>8
-	// If iframe document is assigned to "document" variable and if iframe has been reloaded,
-	// IE will throw "permission denied" error when accessing "document" variable, see jQuery #13936
-	// IE6-8 do not support the defaultView property so parent will be undefined
-	if ( parent && parent !== parent.top ) {
-		// IE11 does not have attachEvent, so all must suffer
-		if ( parent.addEventListener ) {
-			parent.addEventListener( "unload", function() {
-				setDocument();
-			}, false );
-		} else if ( parent.attachEvent ) {
-			parent.attachEvent( "onunload", function() {
-				setDocument();
-			});
-		}
-	}
-
-	/* Attributes
-	---------------------------------------------------------------------- */
-
-	// Support: IE<8
-	// Verify that getAttribute really returns attributes and not properties (excepting IE8 booleans)
-	support.attributes = assert(function( div ) {
-		div.className = "i";
-		return !div.getAttribute("className");
-	});
-
-	/* getElement(s)By*
-	---------------------------------------------------------------------- */
-
-	// Check if getElementsByTagName("*") returns only elements
-	support.getElementsByTagName = assert(function( div ) {
-		div.appendChild( doc.createComment("") );
-		return !div.getElementsByTagName("*").length;
-	});
-
-	// Check if getElementsByClassName can be trusted
-	support.getElementsByClassName = rnative.test( doc.getElementsByClassName ) && assert(function( div ) {
-		div.innerHTML = "<div class='a'></div><div class='a i'></div>";
-
-		// Support: Safari<4
-		// Catch class over-caching
-		div.firstChild.className = "i";
-		// Support: Opera<10
-		// Catch gEBCN failure to find non-leading classes
-		return div.getElementsByClassName("i").length === 2;
-	});
-
-	// Support: IE<10
-	// Check if getElementById returns elements by name
-	// The broken getElementById methods don't pick up programatically-set names,
-	// so use a roundabout getElementsByName test
-	support.getById = assert(function( div ) {
-		docElem.appendChild( div ).id = expando;
-		return !doc.getElementsByName || !doc.getElementsByName( expando ).length;
-	});
-
-	// ID find and filter
-	if ( support.getById ) {
-		Expr.find["ID"] = function( id, context ) {
-			if ( typeof context.getElementById !== strundefined && documentIsHTML ) {
-				var m = context.getElementById( id );
-				// Check parentNode to catch when Blackberry 4.6 returns
-				// nodes that are no longer in the document #6963
-				return m && m.parentNode ? [ m ] : [];
-			}
-		};
-		Expr.filter["ID"] = function( id ) {
-			var attrId = id.replace( runescape, funescape );
-			return function( elem ) {
-				return elem.getAttribute("id") === attrId;
-			};
-		};
-	} else {
-		// Support: IE6/7
-		// getElementById is not reliable as a find shortcut
-		delete Expr.find["ID"];
-
-		Expr.filter["ID"] =  function( id ) {
-			var attrId = id.replace( runescape, funescape );
-			return function( elem ) {
-				var node = typeof elem.getAttributeNode !== strundefined && elem.getAttributeNode("id");
-				return node && node.value === attrId;
-			};
-		};
-	}
-
-	// Tag
-	Expr.find["TAG"] = support.getElementsByTagName ?
-		function( tag, context ) {
-			if ( typeof context.getElementsByTagName !== strundefined ) {
-				return context.getElementsByTagName( tag );
-			}
-		} :
-		function( tag, context ) {
-			var elem,
-				tmp = [],
-				i = 0,
-				results = context.getElementsByTagName( tag );
-
-			// Filter out possible comments
-			if ( tag === "*" ) {
-				while ( (elem = results[i++]) ) {
-					if ( elem.nodeType === 1 ) {
-						tmp.push( elem );
-					}
-				}
-
-				return tmp;
-			}
-			return results;
-		};
-
-	// Class
-	Expr.find["CLASS"] = support.getElementsByClassName && function( className, context ) {
-		if ( typeof context.getElementsByClassName !== strundefined && documentIsHTML ) {
-			return context.getElementsByClassName( className );
-		}
-	};
-
-	/* QSA/matchesSelector
-	---------------------------------------------------------------------- */
-
-	// QSA and matchesSelector support
-
-	// matchesSelector(:active) reports false when true (IE9/Opera 11.5)
-	rbuggyMatches = [];
-
-	// qSa(:focus) reports false when true (Chrome 21)
-	// We allow this because of a bug in IE8/9 that throws an error
-	// whenever `document.activeElement` is accessed on an iframe
-	// So, we allow :focus to pass through QSA all the time to avoid the IE error
-	// See http://bugs.jquery.com/ticket/13378
-	rbuggyQSA = [];
-
-	if ( (support.qsa = rnative.test( doc.querySelectorAll )) ) {
-		// Build QSA regex
-		// Regex strategy adopted from Diego Perini
-		assert(function( div ) {
-			// Select is set to empty string on purpose
-			// This is to test IE's treatment of not explicitly
-			// setting a boolean content attribute,
-			// since its presence should be enough
-			// http://bugs.jquery.com/ticket/12359
-			div.innerHTML = "<select msallowclip=''><option selected=''></option></select>";
-
-			// Support: IE8, Opera 11-12.16
-			// Nothing should be selected when empty strings follow ^= or $= or *=
-			// The test attribute must be unknown in Opera but "safe" for WinRT
-			// http://msdn.microsoft.com/en-us/library/ie/hh465388.aspx#attribute_section
-			if ( div.querySelectorAll("[msallowclip^='']").length ) {
-				rbuggyQSA.push( "[*^$]=" + whitespace + "*(?:''|\"\")" );
-			}
-
-			// Support: IE8
-			// Boolean attributes and "value" are not treated correctly
-			if ( !div.querySelectorAll("[selected]").length ) {
-				rbuggyQSA.push( "\\[" + whitespace + "*(?:value|" + booleans + ")" );
-			}
-
-			// Webkit/Opera - :checked should return selected option elements
-			// http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked
-			// IE8 throws error here and will not see later tests
-			if ( !div.querySelectorAll(":checked").length ) {
-				rbuggyQSA.push(":checked");
-			}
-		});
-
-		assert(function( div ) {
-			// Support: Windows 8 Native Apps
-			// The type and name attributes are restricted during .innerHTML assignment
-			var input = doc.createElement("input");
-			input.setAttribute( "type", "hidden" );
-			div.appendChild( input ).setAttribute( "name", "D" );
-
-			// Support: IE8
-			// Enforce case-sensitivity of name attribute
-			if ( div.querySelectorAll("[name=d]").length ) {
-				rbuggyQSA.push( "name" + whitespace + "*[*^$|!~]?=" );
-			}
-
-			// FF 3.5 - :enabled/:disabled and hidden elements (hidden elements are still enabled)
-			// IE8 throws error here and will not see later tests
-			if ( !div.querySelectorAll(":enabled").length ) {
-				rbuggyQSA.push( ":enabled", ":disabled" );
-			}
-
-			// Opera 10-11 does not throw on post-comma invalid pseudos
-			div.querySelectorAll("*,:x");
-			rbuggyQSA.push(",.*:");
-		});
-	}
-
-	if ( (support.matchesSelector = rnative.test( (matches = docElem.matches ||
-		docElem.webkitMatchesSelector ||
-		docElem.mozMatchesSelector ||
-		docElem.oMatchesSelector ||
-		docElem.msMatchesSelector) )) ) {
-
-		assert(function( div ) {
-			// Check to see if it's possible to do matchesSelector
-			// on a disconnected node (IE 9)
-			support.disconnectedMatch = matches.call( div, "div" );
-
-			// This should fail with an exception
-			// Gecko does not error, returns false instead
-			matches.call( div, "[s!='']:x" );
-			rbuggyMatches.push( "!=", pseudos );
-		});
-	}
-
-	rbuggyQSA = rbuggyQSA.length && new RegExp( rbuggyQSA.join("|") );
-	rbuggyMatches = rbuggyMatches.length && new RegExp( rbuggyMatches.join("|") );
-
-	/* Contains
-	---------------------------------------------------------------------- */
-	hasCompare = rnative.test( docElem.compareDocumentPosition );
-
-	// Element contains another
-	// Purposefully does not implement inclusive descendent
-	// As in, an element does not contain itself
-	contains = hasCompare || rnative.test( docElem.contains ) ?
-		function( a, b ) {
-			var adown = a.nodeType === 9 ? a.documentElement : a,
-				bup = b && b.parentNode;
-			return a === bup || !!( bup && bup.nodeType === 1 && (
-				adown.contains ?
-					adown.contains( bup ) :
-					a.compareDocumentPosition && a.compareDocumentPosition( bup ) & 16
-			));
-		} :
-		function( a, b ) {
-			if ( b ) {
-				while ( (b = b.parentNode) ) {
-					if ( b === a ) {
-						return true;
-					}
-				}
-			}
-			return false;
-		};
-
-	/* Sorting
-	---------------------------------------------------------------------- */
-
-	// Document order sorting
-	sortOrder = hasCompare ?
-	function( a, b ) {
-
-		// Flag for duplicate removal
-		if ( a === b ) {
-			hasDuplicate = true;
-			return 0;
-		}
-
-		// Sort on method existence if only one input has compareDocumentPosition
-		var compare = !a.compareDocumentPosition - !b.compareDocumentPosition;
-		if ( compare ) {
-			return compare;
-		}
-
-		// Calculate position if both inputs belong to the same document
-		compare = ( a.ownerDocument || a ) === ( b.ownerDocument || b ) ?
-			a.compareDocumentPosition( b ) :
-
-			// Otherwise we know they are disconnected
-			1;
-
-		// Disconnected nodes
-		if ( compare & 1 ||
-			(!support.sortDetached && b.compareDocumentPosition( a ) === compare) ) {
-
-			// Choose the first element that is related to our preferred document
-			if ( a === doc || a.ownerDocument === preferredDoc && contains(preferredDoc, a) ) {
-				return -1;
-			}
-			if ( b === doc || b.ownerDocument === preferredDoc && contains(preferredDoc, b) ) {
-				return 1;
-			}
-
-			// Maintain original order
-			return sortInput ?
-				( indexOf.call( sortInput, a ) - indexOf.call( sortInput, b ) ) :
-				0;
-		}
-
-		return compare & 4 ? -1 : 1;
-	} :
-	function( a, b ) {
-		// Exit early if the nodes are identical
-		if ( a === b ) {
-			hasDuplicate = true;
-			return 0;
-		}
-
-		var cur,
-			i = 0,
-			aup = a.parentNode,
-			bup = b.parentNode,
-			ap = [ a ],
-			bp = [ b ];
-
-		// Parentless nodes are either documents or disconnected
-		if ( !aup || !bup ) {
-			return a === doc ? -1 :
-				b === doc ? 1 :
-				aup ? -1 :
-				bup ? 1 :
-				sortInput ?
-				( indexOf.call( sortInput, a ) - indexOf.call( sortInput, b ) ) :
-				0;
-
-		// If the nodes are siblings, we can do a quick check
-		} else if ( aup === bup ) {
-			return siblingCheck( a, b );
-		}
-
-		// Otherwise we need full lists of their ancestors for comparison
-		cur = a;
-		while ( (cur = cur.parentNode) ) {
-			ap.unshift( cur );
-		}
-		cur = b;
-		while ( (cur = cur.parentNode) ) {
-			bp.unshift( cur );
-		}
-
-		// Walk down the tree looking for a discrepancy
-		while ( ap[i] === bp[i] ) {
-			i++;
-		}
-
-		return i ?
-			// Do a sibling check if the nodes have a common ancestor
-			siblingCheck( ap[i], bp[i] ) :
-
-			// Otherwise nodes in our document sort first
-			ap[i] === preferredDoc ? -1 :
-			bp[i] === preferredDoc ? 1 :
-			0;
-	};
-
-	return doc;
-};
-
-Sizzle.matches = function( expr, elements ) {
-	return Sizzle( expr, null, null, elements );
-};
-
-Sizzle.matchesSelector = function( elem, expr ) {
-	// Set document vars if needed
-	if ( ( elem.ownerDocument || elem ) !== document ) {
-		setDocument( elem );
-	}
-
-	// Make sure that attribute selectors are quoted
-	expr = expr.replace( rattributeQuotes, "='$1']" );
-
-	if ( support.matchesSelector && documentIsHTML &&
-		( !rbuggyMatches || !rbuggyMatches.test( expr ) ) &&
-		( !rbuggyQSA     || !rbuggyQSA.test( expr ) ) ) {
-
-		try {
-			var ret = matches.call( elem, expr );
-
-			// IE 9's matchesSelector returns false on disconnected nodes
-			if ( ret || support.disconnectedMatch ||
-					// As well, disconnected nodes are said to be in a document
-					// fragment in IE 9
-					elem.document && elem.document.nodeType !== 11 ) {
-				return ret;
-			}
-		} catch(e) {}
-	}
-
-	return Sizzle( expr, document, null, [ elem ] ).length > 0;
-};
-
-Sizzle.contains = function( context, elem ) {
-	// Set document vars if needed
-	if ( ( context.ownerDocument || context ) !== document ) {
-		setDocument( context );
-	}
-	return contains( context, elem );
-};
-
-Sizzle.attr = function( elem, name ) {
-	// Set document vars if needed
-	if ( ( elem.ownerDocument || elem ) !== document ) {
-		setDocument( elem );
-	}
-
-	var fn = Expr.attrHandle[ name.toLowerCase() ],
-		// Don't get fooled by Object.prototype properties (jQuery #13807)
-		val = fn && hasOwn.call( Expr.attrHandle, name.toLowerCase() ) ?
-			fn( elem, name, !documentIsHTML ) :
-			undefined;
-
-	return val !== undefined ?
-		val :
-		support.attributes || !documentIsHTML ?
-			elem.getAttribute( name ) :
-			(val = elem.getAttributeNode(name)) && val.specified ?
-				val.value :
-				null;
-};
-
-Sizzle.error = function( msg ) {
-	throw new Error( "Syntax error, unrecognized expression: " + msg );
-};
-
-/**
- * Document sorting and removing duplicates
- * @param {ArrayLike} results
- */
-Sizzle.uniqueSort = function( results ) {
-	var elem,
-		duplicates = [],
-		j = 0,
-		i = 0;
-
-	// Unless we *know* we can detect duplicates, assume their presence
-	hasDuplicate = !support.detectDuplicates;
-	sortInput = !support.sortStable && results.slice( 0 );
-	results.sort( sortOrder );
-
-	if ( hasDuplicate ) {
-		while ( (elem = results[i++]) ) {
-			if ( elem === results[ i ] ) {
-				j = duplicates.push( i );
-			}
-		}
-		while ( j-- ) {
-			results.splice( duplicates[ j ], 1 );
-		}
-	}
-
-	// Clear input after sorting to release objects
-	// See https://github.com/jquery/sizzle/pull/225
-	sortInput = null;
-
-	return results;
-};
-
-/**
- * Utility function for retrieving the text value of an array of DOM nodes
- * @param {Array|Element} elem
- */
-getText = Sizzle.getText = function( elem ) {
-	var node,
-		ret = "",
-		i = 0,
-		nodeType = elem.nodeType;
-
-	if ( !nodeType ) {
-		// If no nodeType, this is expected to be an array
-		while ( (node = elem[i++]) ) {
-			// Do not traverse comment nodes
-			ret += getText( node );
-		}
-	} else if ( nodeType === 1 || nodeType === 9 || nodeType === 11 ) {
-		// Use textContent for elements
-		// innerText usage removed for consistency of new lines (jQuery #11153)
-		if ( typeof elem.textContent === "string" ) {
-			return elem.textContent;
-		} else {
-			// Traverse its children
-			for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) {
-				ret += getText( elem );
-			}
-		}
-	} else if ( nodeType === 3 || nodeType === 4 ) {
-		return elem.nodeValue;
-	}
-	// Do not include comment or processing instruction nodes
-
-	return ret;
-};
-
-Expr = Sizzle.selectors = {
-
-	// Can be adjusted by the user
-	cacheLength: 50,
-
-	createPseudo: markFunction,
-
-	match: matchExpr,
-
-	attrHandle: {},
-
-	find: {},
-
-	relative: {
-		">": { dir: "parentNode", first: true },
-		" ": { dir: "parentNode" },
-		"+": { dir: "previousSibling", first: true },
-		"~": { dir: "previousSibling" }
-	},
-
-	preFilter: {
-		"ATTR": function( match ) {
-			match[1] = match[1].replace( runescape, funescape );
-
-			// Move the given value to match[3] whether quoted or unquoted
-			match[3] = ( match[3] || match[4] || match[5] || "" ).replace( runescape, funescape );
-
-			if ( match[2] === "~=" ) {
-				match[3] = " " + match[3] + " ";
-			}
-
-			return match.slice( 0, 4 );
-		},
-
-		"CHILD": function( match ) {
-			/* matches from matchExpr["CHILD"]
-				1 type (only|nth|...)
-				2 what (child|of-type)
-				3 argument (even|odd|\d*|\d*n([+-]\d+)?|...)
-				4 xn-component of xn+y argument ([+-]?\d*n|)
-				5 sign of xn-component
-				6 x of xn-component
-				7 sign of y-component
-				8 y of y-component
-			*/
-			match[1] = match[1].toLowerCase();
-
-			if ( match[1].slice( 0, 3 ) === "nth" ) {
-				// nth-* requires argument
-				if ( !match[3] ) {
-					Sizzle.error( match[0] );
-				}
-
-				// numeric x and y parameters for Expr.filter.CHILD
-				// remember that false/true cast respectively to 0/1
-				match[4] = +( match[4] ? match[5] + (match[6] || 1) : 2 * ( match[3] === "even" || match[3] === "odd" ) );
-				match[5] = +( ( match[7] + match[8] ) || match[3] === "odd" );
-
-			// other types prohibit arguments
-			} else if ( match[3] ) {
-				Sizzle.error( match[0] );
-			}
-
-			return match;
-		},
-
-		"PSEUDO": function( match ) {
-			var excess,
-				unquoted = !match[6] && match[2];
-
-			if ( matchExpr["CHILD"].test( match[0] ) ) {
-				return null;
-			}
-
-			// Accept quoted arguments as-is
-			if ( match[3] ) {
-				match[2] = match[4] || match[5] || "";
-
-			// Strip excess characters from unquoted arguments
-			} else if ( unquoted && rpseudo.test( unquoted ) &&
-				// Get excess from tokenize (recursively)
-				(excess = tokenize( unquoted, true )) &&
-				// advance to the next closing parenthesis
-				(excess = unquoted.indexOf( ")", unquoted.length - excess ) - unquoted.length) ) {
-
-				// excess is a negative index
-				match[0] = match[0].slice( 0, excess );
-				match[2] = unquoted.slice( 0, excess );
-			}
-
-			// Return only captures needed by the pseudo filter method (type and argument)
-			return match.slice( 0, 3 );
-		}
-	},
-
-	filter: {
-
-		"TAG": function( nodeNameSelector ) {
-			var nodeName = nodeNameSelector.replace( runescape, funescape ).toLowerCase();
-			return nodeNameSelector === "*" ?
-				function() { return true; } :
-				function( elem ) {
-					return elem.nodeName && elem.nodeName.toLowerCase() === nodeName;
-				};
-		},
-
-		"CLASS": function( className ) {
-			var pattern = classCache[ className + " " ];
-
-			return pattern ||
-				(pattern = new RegExp( "(^|" + whitespace + ")" + className + "(" + whitespace + "|$)" )) &&
-				classCache( className, function( elem ) {
-					return pattern.test( typeof elem.className === "string" && elem.className || typeof elem.getAttribute !== strundefined && elem.getAttribute("class") || "" );
-				});
-		},
-
-		"ATTR": function( name, operator, check ) {
-			return function( elem ) {
-				var result = Sizzle.attr( elem, name );
-
-				if ( result == null ) {
-					return operator === "!=";
-				}
-				if ( !operator ) {
-					return true;
-				}
-
-				result += "";
-
-				return operator === "=" ? result === check :
-					operator === "!=" ? result !== check :
-					operator === "^=" ? check && result.indexOf( check ) === 0 :
-					operator === "*=" ? check && result.indexOf( check ) > -1 :
-					operator === "$=" ? check && result.slice( -check.length ) === check :
-					operator === "~=" ? ( " " + result + " " ).indexOf( check ) > -1 :
-					operator === "|=" ? result === check || result.slice( 0, check.length + 1 ) === check + "-" :
-					false;
-			};
-		},
-
-		"CHILD": function( type, what, argument, first, last ) {
-			var simple = type.slice( 0, 3 ) !== "nth",
-				forward = type.slice( -4 ) !== "last",
-				ofType = what === "of-type";
-
-			return first === 1 && last === 0 ?
-
-				// Shortcut for :nth-*(n)
-				function( elem ) {
-					return !!elem.parentNode;
-				} :
-
-				function( elem, context, xml ) {
-					var cache, outerCache, node, diff, nodeIndex, start,
-						dir = simple !== forward ? "nextSibling" : "previousSibling",
-						parent = elem.parentNode,
-						name = ofType && elem.nodeName.toLowerCase(),
-						useCache = !xml && !ofType;
-
-					if ( parent ) {
-
-						// :(first|last|only)-(child|of-type)
-						if ( simple ) {
-							while ( dir ) {
-								node = elem;
-								while ( (node = node[ dir ]) ) {
-									if ( ofType ? node.nodeName.toLowerCase() === name : node.nodeType === 1 ) {
-										return false;
-									}
-								}
-								// Reverse direction for :only-* (if we haven't yet done so)
-								start = dir = type === "only" && !start && "nextSibling";
-							}
-							return true;
-						}
-
-						start = [ forward ? parent.firstChild : parent.lastChild ];
-
-						// non-xml :nth-child(...) stores cache data on `parent`
-						if ( forward && useCache ) {
-							// Seek `elem` from a previously-cached index
-							outerCache = parent[ expando ] || (parent[ expando ] = {});
-							cache = outerCache[ type ] || [];
-							nodeIndex = cache[0] === dirruns && cache[1];
-							diff = cache[0] === dirruns && cache[2];
-							node = nodeIndex && parent.childNodes[ nodeIndex ];
-
-							while ( (node = ++nodeIndex && node && node[ dir ] ||
-
-								// Fallback to seeking `elem` from the start
-								(diff = nodeIndex = 0) || start.pop()) ) {
-
-								// When found, cache indexes on `parent` and break
-								if ( node.nodeType === 1 && ++diff && node === elem ) {
-									outerCache[ type ] = [ dirruns, nodeIndex, diff ];
-									break;
-								}
-							}
-
-						// Use previously-cached element index if available
-						} else if ( useCache && (cache = (elem[ expando ] || (elem[ expando ] = {}))[ type ]) && cache[0] === dirruns ) {
-							diff = cache[1];
-
-						// xml :nth-child(...) or :nth-last-child(...) or :nth(-last)?-of-type(...)
-						} else {
-							// Use the same loop as above to seek `elem` from the start
-							while ( (node = ++nodeIndex && node && node[ dir ] ||
-								(diff = nodeIndex = 0) || start.pop()) ) {
-
-								if ( ( ofType ? node.nodeName.toLowerCase() === name : node.nodeType === 1 ) && ++diff ) {
-									// Cache the index of each encountered element
-									if ( useCache ) {
-										(node[ expando ] || (node[ expando ] = {}))[ type ] = [ dirruns, diff ];
-									}
-
-									if ( node === elem ) {
-										break;
-									}
-								}
-							}
-						}
-
-						// Incorporate the offset, then check against cycle size
-						diff -= last;
-						return diff === first || ( diff % first === 0 && diff / first >= 0 );
-					}
-				};
-		},
-
-		"PSEUDO": function( pseudo, argument ) {
-			// pseudo-class names are case-insensitive
-			// http://www.w3.org/TR/selectors/#pseudo-classes
-			// Prioritize by case sensitivity in case custom pseudos are added with uppercase letters
-			// Remember that setFilters inherits from pseudos
-			var args,
-				fn = Expr.pseudos[ pseudo ] || Expr.setFilters[ pseudo.toLowerCase() ] ||
-					Sizzle.error( "unsupported pseudo: " + pseudo );
-
-			// The user may use createPseudo to indicate that
-			// arguments are needed to create the filter function
-			// just as Sizzle does
-			if ( fn[ expando ] ) {
-				return fn( argument );
-			}
-
-			// But maintain support for old signatures
-			if ( fn.length > 1 ) {
-				args = [ pseudo, pseudo, "", argument ];
-				return Expr.setFilters.hasOwnProperty( pseudo.toLowerCase() ) ?
-					markFunction(function( seed, matches ) {
-						var idx,
-							matched = fn( seed, argument ),
-							i = matched.length;
-						while ( i-- ) {
-							idx = indexOf.call( seed, matched[i] );
-							seed[ idx ] = !( matches[ idx ] = matched[i] );
-						}
-					}) :
-					function( elem ) {
-						return fn( elem, 0, args );
-					};
-			}
-
-			return fn;
-		}
-	},
-
-	pseudos: {
-		// Potentially complex pseudos
-		"not": markFunction(function( selector ) {
-			// Trim the selector passed to compile
-			// to avoid treating leading and trailing
-			// spaces as combinators
-			var input = [],
-				results = [],
-				matcher = compile( selector.replace( rtrim, "$1" ) );
-
-			return matcher[ expando ] ?
-				markFunction(function( seed, matches, context, xml ) {
-					var elem,
-						unmatched = matcher( seed, null, xml, [] ),
-						i = seed.length;
-
-					// Match elements unmatched by `matcher`
-					while ( i-- ) {
-						if ( (elem = unmatched[i]) ) {
-							seed[i] = !(matches[i] = elem);
-						}
-					}
-				}) :
-				function( elem, context, xml ) {
-					input[0] = elem;
-					matcher( input, null, xml, results );
-					return !results.pop();
-				};
-		}),
-
-		"has": markFunction(function( selector ) {
-			return function( elem ) {
-				return Sizzle( selector, elem ).length > 0;
-			};
-		}),
-
-		"contains": markFunction(function( text ) {
-			return function( elem ) {
-				return ( elem.textContent || elem.innerText || getText( elem ) ).indexOf( text ) > -1;
-			};
-		}),
-
-		// "Whether an element is represented by a :lang() selector
-		// is based solely on the element's language value
-		// being equal to the identifier C,
-		// or beginning with the identifier C immediately followed by "-".
-		// The matching of C against the element's language value is performed case-insensitively.
-		// The identifier C does not have to be a valid language name."
-		// http://www.w3.org/TR/selectors/#lang-pseudo
-		"lang": markFunction( function( lang ) {
-			// lang value must be a valid identifier
-			if ( !ridentifier.test(lang || "") ) {
-				Sizzle.error( "unsupported lang: " + lang );
-			}
-			lang = lang.replace( runescape, funescape ).toLowerCase();
-			return function( elem ) {
-				var elemLang;
-				do {
-					if ( (elemLang = documentIsHTML ?
-						elem.lang :
-						elem.getAttribute("xml:lang") || elem.getAttribute("lang")) ) {
-
-						elemLang = elemLang.toLowerCase();
-						return elemLang === lang || elemLang.indexOf( lang + "-" ) === 0;
-					}
-				} while ( (elem = elem.parentNode) && elem.nodeType === 1 );
-				return false;
-			};
-		}),
-
-		// Miscellaneous
-		"target": function( elem ) {
-			var hash = window.location && window.location.hash;
-			return hash && hash.slice( 1 ) === elem.id;
-		},
-
-		"root": function( elem ) {
-			return elem === docElem;
-		},
-
-		"focus": function( elem ) {
-			return elem === document.activeElement && (!document.hasFocus || document.hasFocus()) && !!(elem.type || elem.href || ~elem.tabIndex);
-		},
-
-		// Boolean properties
-		"enabled": function( elem ) {
-			return elem.disabled === false;
-		},
-
-		"disabled": function( elem ) {
-			return elem.disabled === true;
-		},
-
-		"checked": function( elem ) {
-			// In CSS3, :checked should return both checked and selected elements
-			// http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked
-			var nodeName = elem.nodeName.toLowerCase();
-			return (nodeName === "input" && !!elem.checked) || (nodeName === "option" && !!elem.selected);
-		},
-
-		"selected": function( elem ) {
-			// Accessing this property makes selected-by-default
-			// options in Safari work properly
-			if ( elem.parentNode ) {
-				elem.parentNode.selectedIndex;
-			}
-
-			return elem.selected === true;
-		},
-
-		// Contents
-		"empty": function( elem ) {
-			// http://www.w3.org/TR/selectors/#empty-pseudo
-			// :empty is negated by element (1) or content nodes (text: 3; cdata: 4; entity ref: 5),
-			//   but not by others (comment: 8; processing instruction: 7; etc.)
-			// nodeType < 6 works because attributes (2) do not appear as children
-			for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) {
-				if ( elem.nodeType < 6 ) {
-					return false;
-				}
-			}
-			return true;
-		},
-
-		"parent": function( elem ) {
-			return !Expr.pseudos["empty"]( elem );
-		},
-
-		// Element/input types
-		"header": function( elem ) {
-			return rheader.test( elem.nodeName );
-		},
-
-		"input": function( elem ) {
-			return rinputs.test( elem.nodeName );
-		},
-
-		"button": function( elem ) {
-			var name = elem.nodeName.toLowerCase();
-			return name === "input" && elem.type === "button" || name === "button";
-		},
-
-		"text": function( elem ) {
-			var attr;
-			return elem.nodeName.toLowerCase() === "input" &&
-				elem.type === "text" &&
-
-				// Support: IE<8
-				// New HTML5 attribute values (e.g., "search") appear with elem.type === "text"
-				( (attr = elem.getAttribute("type")) == null || attr.toLowerCase() === "text" );
-		},
-
-		// Position-in-collection
-		"first": createPositionalPseudo(function() {
-			return [ 0 ];
-		}),
-
-		"last": createPositionalPseudo(function( matchIndexes, length ) {
-			return [ length - 1 ];
-		}),
-
-		"eq": createPositionalPseudo(function( matchIndexes, length, argument ) {
-			return [ argument < 0 ? argument + length : argument ];
-		}),
-
-		"even": createPositionalPseudo(function( matchIndexes, length ) {
-			var i = 0;
-			for ( ; i < length; i += 2 ) {
-				matchIndexes.push( i );
-			}
-			return matchIndexes;
-		}),
-
-		"odd": createPositionalPseudo(function( matchIndexes, length ) {
-			var i = 1;
-			for ( ; i < length; i += 2 ) {
-				matchIndexes.push( i );
-			}
-			return matchIndexes;
-		}),
-
-		"lt": createPositionalPseudo(function( matchIndexes, length, argument ) {
-			var i = argument < 0 ? argument + length : argument;
-			for ( ; --i >= 0; ) {
-				matchIndexes.push( i );
-			}
-			return matchIndexes;
-		}),
-
-		"gt": createPositionalPseudo(function( matchIndexes, length, argument ) {
-			var i = argument < 0 ? argument + length : argument;
-			for ( ; ++i < length; ) {
-				matchIndexes.push( i );
-			}
-			return matchIndexes;
-		})
-	}
-};
-
-Expr.pseudos["nth"] = Expr.pseudos["eq"];
-
-// Add button/input type pseudos
-for ( i in { radio: true, checkbox: true, file: true, password: true, image: true } ) {
-	Expr.pseudos[ i ] = createInputPseudo( i );
-}
-for ( i in { submit: true, reset: true } ) {
-	Expr.pseudos[ i ] = createButtonPseudo( i );
-}
-
-// Easy API for creating new setFilters
-function setFilters() {}
-setFilters.prototype = Expr.filters = Expr.pseudos;
-Expr.setFilters = new setFilters();
-
-tokenize = Sizzle.tokenize = function( selector, parseOnly ) {
-	var matched, match, tokens, type,
-		soFar, groups, preFilters,
-		cached = tokenCache[ selector + " " ];
-
-	if ( cached ) {
-		return parseOnly ? 0 : cached.slice( 0 );
-	}
-
-	soFar = selector;
-	groups = [];
-	preFilters = Expr.preFilter;
-
-	while ( soFar ) {
-
-		// Comma and first run
-		if ( !matched || (match = rcomma.exec( soFar )) ) {
-			if ( match ) {
-				// Don't consume trailing commas as valid
-				soFar = soFar.slice( match[0].length ) || soFar;
-			}
-			groups.push( (tokens = []) );
-		}
-
-		matched = false;
-
-		// Combinators
-		if ( (match = rcombinators.exec( soFar )) ) {
-			matched = match.shift();
-			tokens.push({
-				value: matched,
-				// Cast descendant combinators to space
-				type: match[0].replace( rtrim, " " )
-			});
-			soFar = soFar.slice( matched.length );
-		}
-
-		// Filters
-		for ( type in Expr.filter ) {
-			if ( (match = matchExpr[ type ].exec( soFar )) && (!preFilters[ type ] ||
-				(match = preFilters[ type ]( match ))) ) {
-				matched = match.shift();
-				tokens.push({
-					value: matched,
-					type: type,
-					matches: match
-				});
-				soFar = soFar.slice( matched.length );
-			}
-		}
-
-		if ( !matched ) {
-			break;
-		}
-	}
-
-	// Return the length of the invalid excess
-	// if we're just parsing
-	// Otherwise, throw an error or return tokens
-	return parseOnly ?
-		soFar.length :
-		soFar ?
-			Sizzle.error( selector ) :
-			// Cache the tokens
-			tokenCache( selector, groups ).slice( 0 );
-};
-
-function toSelector( tokens ) {
-	var i = 0,
-		len = tokens.length,
-		selector = "";
-	for ( ; i < len; i++ ) {
-		selector += tokens[i].value;
-	}
-	return selector;
-}
-
-function addCombinator( matcher, combinator, base ) {
-	var dir = combinator.dir,
-		checkNonElements = base && dir === "parentNode",
-		doneName = done++;
-
-	return combinator.first ?
-		// Check against closest ancestor/preceding element
-		function( elem, context, xml ) {
-			while ( (elem = elem[ dir ]) ) {
-				if ( elem.nodeType === 1 || checkNonElements ) {
-					return matcher( elem, context, xml );
-				}
-			}
-		} :
-
-		// Check against all ancestor/preceding elements
-		function( elem, context, xml ) {
-			var oldCache, outerCache,
-				newCache = [ dirruns, doneName ];
-
-			// We can't set arbitrary data on XML nodes, so they don't benefit from dir caching
-			if ( xml ) {
-				while ( (elem = elem[ dir ]) ) {
-					if ( elem.nodeType === 1 || checkNonElements ) {
-						if ( matcher( elem, context, xml ) ) {
-							return true;
-						}
-					}
-				}
-			} else {
-				while ( (elem = elem[ dir ]) ) {
-					if ( elem.nodeType === 1 || checkNonElements ) {
-						outerCache = elem[ expando ] || (elem[ expando ] = {});
-						if ( (oldCache = outerCache[ dir ]) &&
-							oldCache[ 0 ] === dirruns && oldCache[ 1 ] === doneName ) {
-
-							// Assign to newCache so results back-propagate to previous elements
-							return (newCache[ 2 ] = oldCache[ 2 ]);
-						} else {
-							// Reuse newcache so results back-propagate to previous elements
-							outerCache[ dir ] = newCache;
-
-							// A match means we're done; a fail means we have to keep checking
-							if ( (newCache[ 2 ] = matcher( elem, context, xml )) ) {
-								return true;
-							}
-						}
-					}
-				}
-			}
-		};
-}
-
-function elementMatcher( matchers ) {
-	return matchers.length > 1 ?
-		function( elem, context, xml ) {
-			var i = matchers.length;
-			while ( i-- ) {
-				if ( !matchers[i]( elem, context, xml ) ) {
-					return false;
-				}
-			}
-			return true;
-		} :
-		matchers[0];
-}
-
-function multipleContexts( selector, contexts, results ) {
-	var i = 0,
-		len = contexts.length;
-	for ( ; i < len; i++ ) {
-		Sizzle( selector, contexts[i], results );
-	}
-	return results;
-}
-
-function condense( unmatched, map, filter, context, xml ) {
-	var elem,
-		newUnmatched = [],
-		i = 0,
-		len = unmatched.length,
-		mapped = map != null;
-
-	for ( ; i < len; i++ ) {
-		if ( (elem = unmatched[i]) ) {
-			if ( !filter || filter( elem, context, xml ) ) {
-				newUnmatched.push( elem );
-				if ( mapped ) {
-					map.push( i );
-				}
-			}
-		}
-	}
-
-	return newUnmatched;
-}
-
-function setMatcher( preFilter, selector, matcher, postFilter, postFinder, postSelector ) {
-	if ( postFilter && !postFilter[ expando ] ) {
-		postFilter = setMatcher( postFilter );
-	}
-	if ( postFinder && !postFinder[ expando ] ) {
-		postFinder = setMatcher( postFinder, postSelector );
-	}
-	return markFunction(function( seed, results, context, xml ) {
-		var temp, i, elem,
-			preMap = [],
-			postMap = [],
-			preexisting = results.length,
-
-			// Get initial elements from seed or context
-			elems = seed || multipleContexts( selector || "*", context.nodeType ? [ context ] : context, [] ),
-
-			// Prefilter to get matcher input, preserving a map for seed-results synchronization
-			matcherIn = preFilter && ( seed || !selector ) ?
-				condense( elems, preMap, preFilter, context, xml ) :
-				elems,
-
-			matcherOut = matcher ?
-				// If we have a postFinder, or filtered seed, or non-seed postFilter or preexisting results,
-				postFinder || ( seed ? preFilter : preexisting || postFilter ) ?
-
-					// ...intermediate processing is necessary
-					[] :
-
-					// ...otherwise use results directly
-					results :
-				matcherIn;
-
-		// Find primary matches
-		if ( matcher ) {
-			matcher( matcherIn, matcherOut, context, xml );
-		}
-
-		// Apply postFilter
-		if ( postFilter ) {
-			temp = condense( matcherOut, postMap );
-			postFilter( temp, [], context, xml );
-
-			// Un-match failing elements by moving them back to matcherIn
-			i = temp.length;
-			while ( i-- ) {
-				if ( (elem = temp[i]) ) {
-					matcherOut[ postMap[i] ] = !(matcherIn[ postMap[i] ] = elem);
-				}
-			}
-		}
-
-		if ( seed ) {
-			if ( postFinder || preFilter ) {
-				if ( postFinder ) {
-					// Get the final matcherOut by condensing this intermediate into postFinder contexts
-					temp = [];
-					i = matcherOut.length;
-					while ( i-- ) {
-						if ( (elem = matcherOut[i]) ) {
-							// Restore matcherIn since elem is not yet a final match
-							temp.push( (matcherIn[i] = elem) );
-						}
-					}
-					postFinder( null, (matcherOut = []), temp, xml );
-				}
-
-				// Move matched elements from seed to results to keep them synchronized
-				i = matcherOut.length;
-				while ( i-- ) {
-					if ( (elem = matcherOut[i]) &&
-						(temp = postFinder ? indexOf.call( seed, elem ) : preMap[i]) > -1 ) {
-
-						seed[temp] = !(results[temp] = elem);
-					}
-				}
-			}
-
-		// Add elements to results, through postFinder if defined
-		} else {
-			matcherOut = condense(
-				matcherOut === results ?
-					matcherOut.splice( preexisting, matcherOut.length ) :
-					matcherOut
-			);
-			if ( postFinder ) {
-				postFinder( null, results, matcherOut, xml );
-			} else {
-				push.apply( results, matcherOut );
-			}
-		}
-	});
-}
-
-function matcherFromTokens( tokens ) {
-	var checkContext, matcher, j,
-		len = tokens.length,
-		leadingRelative = Expr.relative[ tokens[0].type ],
-		implicitRelative = leadingRelative || Expr.relative[" "],
-		i = leadingRelative ? 1 : 0,
-
-		// The foundational matcher ensures that elements are reachable from top-level context(s)
-		matchContext = addCombinator( function( elem ) {
-			return elem === checkContext;
-		}, implicitRelative, true ),
-		matchAnyContext = addCombinator( function( elem ) {
-			return indexOf.call( checkContext, elem ) > -1;
-		}, implicitRelative, true ),
-		matchers = [ function( elem, context, xml ) {
-			return ( !leadingRelative && ( xml || context !== outermostContext ) ) || (
-				(checkContext = context).nodeType ?
-					matchContext( elem, context, xml ) :
-					matchAnyContext( elem, context, xml ) );
-		} ];
-
-	for ( ; i < len; i++ ) {
-		if ( (matcher = Expr.relative[ tokens[i].type ]) ) {
-			matchers = [ addCombinator(elementMatcher( matchers ), matcher) ];
-		} else {
-			matcher = Expr.filter[ tokens[i].type ].apply( null, tokens[i].matches );
-
-			// Return special upon seeing a positional matcher
-			if ( matcher[ expando ] ) {
-				// Find the next relative operator (if any) for proper handling
-				j = ++i;
-				for ( ; j < len; j++ ) {
-					if ( Expr.relative[ tokens[j].type ] ) {
-						break;
-					}
-				}
-				return setMatcher(
-					i > 1 && elementMatcher( matchers ),
-					i > 1 && toSelector(
-						// If the preceding token was a descendant combinator, insert an implicit any-element `*`
-						tokens.slice( 0, i - 1 ).concat({ value: tokens[ i - 2 ].type === " " ? "*" : "" })
-					).replace( rtrim, "$1" ),
-					matcher,
-					i < j && matcherFromTokens( tokens.slice( i, j ) ),
-					j < len && matcherFromTokens( (tokens = tokens.slice( j )) ),
-					j < len && toSelector( tokens )
-				);
-			}
-			matchers.push( matcher );
-		}
-	}
-
-	return elementMatcher( matchers );
-}
-
-function matcherFromGroupMatchers( elementMatchers, setMatchers ) {
-	var bySet = setMatchers.length > 0,
-		byElement = elementMatchers.length > 0,
-		superMatcher = function( seed, context, xml, results, outermost ) {
-			var elem, j, matcher,
-				matchedCount = 0,
-				i = "0",
-				unmatched = seed && [],
-				setMatched = [],
-				contextBackup = outermostContext,
-				// We must always have either seed elements or outermost context
-				elems = seed || byElement && Expr.find["TAG"]( "*", outermost ),
-				// Use integer dirruns iff this is the outermost matcher
-				dirrunsUnique = (dirruns += contextBackup == null ? 1 : Math.random() || 0.1),
-				len = elems.length;
-
-			if ( outermost ) {
-				outermostContext = context !== document && context;
-			}
-
-			// Add elements passing elementMatchers directly to results
-			// Keep `i` a string if there are no elements so `matchedCount` will be "00" below
-			// Support: IE<9, Safari
-			// Tolerate NodeList properties (IE: "length"; Safari: <number>) matching elements by id
-			for ( ; i !== len && (elem = elems[i]) != null; i++ ) {
-				if ( byElement && elem ) {
-					j = 0;
-					while ( (matcher = elementMatchers[j++]) ) {
-						if ( matcher( elem, context, xml ) ) {
-							results.push( elem );
-							break;
-						}
-					}
-					if ( outermost ) {
-						dirruns = dirrunsUnique;
-					}
-				}
-
-				// Track unmatched elements for set filters
-				if ( bySet ) {
-					// They will have gone through all possible matchers
-					if ( (elem = !matcher && elem) ) {
-						matchedCount--;
-					}
-
-					// Lengthen the array for every element, matched or not
-					if ( seed ) {
-						unmatched.push( elem );
-					}
-				}
-			}
-
-			// Apply set filters to unmatched elements
-			matchedCount += i;
-			if ( bySet && i !== matchedCount ) {
-				j = 0;
-				while ( (matcher = setMatchers[j++]) ) {
-					matcher( unmatched, setMatched, context, xml );
-				}
-
-				if ( seed ) {
-					// Reintegrate element matches to eliminate the need for sorting
-					if ( matchedCount > 0 ) {
-						while ( i-- ) {
-							if ( !(unmatched[i] || setMatched[i]) ) {
-								setMatched[i] = pop.call( results );
-							}
-						}
-					}
-
-					// Discard index placeholder values to get only actual matches
-					setMatched = condense( setMatched );
-				}
-
-				// Add matches to results
-				push.apply( results, setMatched );
-
-				// Seedless set matches succeeding multiple successful matchers stipulate sorting
-				if ( outermost && !seed && setMatched.length > 0 &&
-					( matchedCount + setMatchers.length ) > 1 ) {
-
-					Sizzle.uniqueSort( results );
-				}
-			}
-
-			// Override manipulation of globals by nested matchers
-			if ( outermost ) {
-				dirruns = dirrunsUnique;
-				outermostContext = contextBackup;
-			}
-
-			return unmatched;
-		};
-
-	return bySet ?
-		markFunction( superMatcher ) :
-		superMatcher;
-}
-
-compile = Sizzle.compile = function( selector, match /* Internal Use Only */ ) {
-	var i,
-		setMatchers = [],
-		elementMatchers = [],
-		cached = compilerCache[ selector + " " ];
-
-	if ( !cached ) {
-		// Generate a function of recursive functions that can be used to check each element
-		if ( !match ) {
-			match = tokenize( selector );
-		}
-		i = match.length;
-		while ( i-- ) {
-			cached = matcherFromTokens( match[i] );
-			if ( cached[ expando ] ) {
-				setMatchers.push( cached );
-			} else {
-				elementMatchers.push( cached );
-			}
-		}
-
-		// Cache the compiled function
-		cached = compilerCache( selector, matcherFromGroupMatchers( elementMatchers, setMatchers ) );
-
-		// Save selector and tokenization
-		cached.selector = selector;
-	}
-	return cached;
-};
-
-/**
- * A low-level selection function that works with Sizzle's compiled
- *  selector functions
- * @param {String|Function} selector A selector or a pre-compiled
- *  selector function built with Sizzle.compile
- * @param {Element} context
- * @param {Array} [results]
- * @param {Array} [seed] A set of elements to match against
- */
-select = Sizzle.select = function( selector, context, results, seed ) {
-	var i, tokens, token, type, find,
-		compiled = typeof selector === "function" && selector,
-		match = !seed && tokenize( (selector = compiled.selector || selector) );
-
-	results = results || [];
-
-	// Try to minimize operations if there is no seed and only one group
-	if ( match.length === 1 ) {
-
-		// Take a shortcut and set the context if the root selector is an ID
-		tokens = match[0] = match[0].slice( 0 );
-		if ( tokens.length > 2 && (token = tokens[0]).type === "ID" &&
-				support.getById && context.nodeType === 9 && documentIsHTML &&
-				Expr.relative[ tokens[1].type ] ) {
-
-			context = ( Expr.find["ID"]( token.matches[0].replace(runescape, funescape), context ) || [] )[0];
-			if ( !context ) {
-				return results;
-
-			// Precompiled matchers will still verify ancestry, so step up a level
-			} else if ( compiled ) {
-				context = context.parentNode;
-			}
-
-			selector = selector.slice( tokens.shift().value.length );
-		}
-
-		// Fetch a seed set for right-to-left matching
-		i = matchExpr["needsContext"].test( selector ) ? 0 : tokens.length;
-		while ( i-- ) {
-			token = tokens[i];
-
-			// Abort if we hit a combinator
-			if ( Expr.relative[ (type = token.type) ] ) {
-				break;
-			}
-			if ( (find = Expr.find[ type ]) ) {
-				// Search, expanding context for leading sibling combinators
-				if ( (seed = find(
-					token.matches[0].replace( runescape, funescape ),
-					rsibling.test( tokens[0].type ) && testContext( context.parentNode ) || context
-				)) ) {
-
-					// If seed is empty or no tokens remain, we can return early
-					tokens.splice( i, 1 );
-					selector = seed.length && toSelector( tokens );
-					if ( !selector ) {
-						push.apply( results, seed );
-						return results;
-					}
-
-					break;
-				}
-			}
-		}
-	}
-
-	// Compile and execute a filtering function if one is not provided
-	// Provide `match` to avoid retokenization if we modified the selector above
-	( compiled || compile( selector, match ) )(
-		seed,
-		context,
-		!documentIsHTML,
-		results,
-		rsibling.test( selector ) && testContext( context.parentNode ) || context
-	);
-	return results;
-};
-
-// One-time assignments
-
-// Sort stability
-support.sortStable = expando.split("").sort( sortOrder ).join("") === expando;
-
-// Support: Chrome<14
-// Always assume duplicates if they aren't passed to the comparison function
-support.detectDuplicates = !!hasDuplicate;
-
-// Initialize against the default document
-setDocument();
-
-// Support: Webkit<537.32 - Safari 6.0.3/Chrome 25 (fixed in Chrome 27)
-// Detached nodes confoundingly follow *each other*
-support.sortDetached = assert(function( div1 ) {
-	// Should return 1, but returns 4 (following)
-	return div1.compareDocumentPosition( document.createElement("div") ) & 1;
-});
-
-// Support: IE<8
-// Prevent attribute/property "interpolation"
-// http://msdn.microsoft.com/en-us/library/ms536429%28VS.85%29.aspx
-if ( !assert(function( div ) {
-	div.innerHTML = "<a href='#'></a>";
-	return div.firstChild.getAttribute("href") === "#" ;
-}) ) {
-	addHandle( "type|href|height|width", function( elem, name, isXML ) {
-		if ( !isXML ) {
-			return elem.getAttribute( name, name.toLowerCase() === "type" ? 1 : 2 );
-		}
-	});
-}
-
-// Support: IE<9
-// Use defaultValue in place of getAttribute("value")
-if ( !support.attributes || !assert(function( div ) {
-	div.innerHTML = "<input/>";
-	div.firstChild.setAttribute( "value", "" );
-	return div.firstChild.getAttribute( "value" ) === "";
-}) ) {
-	addHandle( "value", function( elem, name, isXML ) {
-		if ( !isXML && elem.nodeName.toLowerCase() === "input" ) {
-			return elem.defaultValue;
-		}
-	});
-}
-
-// Support: IE<9
-// Use getAttributeNode to fetch booleans when getAttribute lies
-if ( !assert(function( div ) {
-	return div.getAttribute("disabled") == null;
-}) ) {
-	addHandle( booleans, function( elem, name, isXML ) {
-		var val;
-		if ( !isXML ) {
-			return elem[ name ] === true ? name.toLowerCase() :
-					(val = elem.getAttributeNode( name )) && val.specified ?
-					val.value :
-				null;
-		}
-	});
-}
-
-return Sizzle;
-
-})( window );
-
-
-
-jQuery.find = Sizzle;
-jQuery.expr = Sizzle.selectors;
-jQuery.expr[":"] = jQuery.expr.pseudos;
-jQuery.unique = Sizzle.uniqueSort;
-jQuery.text = Sizzle.getText;
-jQuery.isXMLDoc = Sizzle.isXML;
-jQuery.contains = Sizzle.contains;
-
-
-
-var rneedsContext = jQuery.expr.match.needsContext;
-
-var rsingleTag = (/^<(\w+)\s*\/?>(?:<\/\1>|)$/);
-
-
-
-var risSimple = /^.[^:#\[\.,]*$/;
-
-// Implement the identical functionality for filter and not
-function winnow( elements, qualifier, not ) {
-	if ( jQuery.isFunction( qualifier ) ) {
-		return jQuery.grep( elements, function( elem, i ) {
-			/* jshint -W018 */
-			return !!qualifier.call( elem, i, elem ) !== not;
-		});
-
-	}
-
-	if ( qualifier.nodeType ) {
-		return jQuery.grep( elements, function( elem ) {
-			return ( elem === qualifier ) !== not;
-		});
-
-	}
-
-	if ( typeof qualifier === "string" ) {
-		if ( risSimple.test( qualifier ) ) {
-			return jQuery.filter( qualifier, elements, not );
-		}
-
-		qualifier = jQuery.filter( qualifier, elements );
-	}
-
-	return jQuery.grep( elements, function( elem ) {
-		return ( indexOf.call( qualifier, elem ) >= 0 ) !== not;
-	});
-}
-
-jQuery.filter = function( expr, elems, not ) {
-	var elem = elems[ 0 ];
-
-	if ( not ) {
-		expr = ":not(" + expr + ")";
-	}
-
-	return elems.length === 1 && elem.nodeType === 1 ?
-		jQuery.find.matchesSelector( elem, expr ) ? [ elem ] : [] :
-		jQuery.find.matches( expr, jQuery.grep( elems, function( elem ) {
-			return elem.nodeType === 1;
-		}));
-};
-
-jQuery.fn.extend({
-	find: function( selector ) {
-		var i,
-			len = this.length,
-			ret = [],
-			self = this;
-
-		if ( typeof selector !== "string" ) {
-			return this.pushStack( jQuery( selector ).filter(function() {
-				for ( i = 0; i < len; i++ ) {
-					if ( jQuery.contains( self[ i ], this ) ) {
-						return true;
-					}
-				}
-			}) );
-		}
-
-		for ( i = 0; i < len; i++ ) {
-			jQuery.find( selector, self[ i ], ret );
-		}
-
-		// Needed because $( selector, context ) becomes $( context ).find( selector )
-		ret = this.pushStack( len > 1 ? jQuery.unique( ret ) : ret );
-		ret.selector = this.selector ? this.selector + " " + selector : selector;
-		return ret;
-	},
-	filter: function( selector ) {
-		return this.pushStack( winnow(this, selector || [], false) );
-	},
-	not: function( selector ) {
-		return this.pushStack( winnow(this, selector || [], true) );
-	},
-	is: function( selector ) {
-		return !!winnow(
-			this,
-
-			// If this is a positional/relative selector, check membership in the returned set
-			// so $("p:first").is("p:last") won't return true for a doc with two "p".
-			typeof selector === "string" && rneedsContext.test( selector ) ?
-				jQuery( selector ) :
-				selector || [],
-			false
-		).length;
-	}
-});
-
-
-// Initialize a jQuery object
-
-
-// A central reference to the root jQuery(document)
-var rootjQuery,
-
-	// A simple way to check for HTML strings
-	// Prioritize #id over <tag> to avoid XSS via location.hash (#9521)
-	// Strict HTML recognition (#11290: must start with <)
-	rquickExpr = /^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,
-
-	init = jQuery.fn.init = function( selector, context ) {
-		var match, elem;
-
-		// HANDLE: $(""), $(null), $(undefined), $(false)
-		if ( !selector ) {
-			return this;
-		}
-
-		// Handle HTML strings
-		if ( typeof selector === "string" ) {
-			if ( selector[0] === "<" && selector[ selector.length - 1 ] === ">" && selector.length >= 3 ) {
-				// Assume that strings that start and end with <> are HTML and skip the regex check
-				match = [ null, selector, null ];
-
-			} else {
-				match = rquickExpr.exec( selector );
-			}
-
-			// Match html or make sure no context is specified for #id
-			if ( match && (match[1] || !context) ) {
-
-				// HANDLE: $(html) -> $(array)
-				if ( match[1] ) {
-					context = context instanceof jQuery ? context[0] : context;
-
-					// scripts is true for back-compat
-					// Intentionally let the error be thrown if parseHTML is not present
-					jQuery.merge( this, jQuery.parseHTML(
-						match[1],
-						context && context.nodeType ? context.ownerDocument || context : document,
-						true
-					) );
-
-					// HANDLE: $(html, props)
-					if ( rsingleTag.test( match[1] ) && jQuery.isPlainObject( context ) ) {
-						for ( match in context ) {
-							// Properties of context are called as methods if possible
-							if ( jQuery.isFunction( this[ match ] ) ) {
-								this[ match ]( context[ match ] );
-
-							// ...and otherwise set as attributes
-							} else {
-								this.attr( match, context[ match ] );
-							}
-						}
-					}
-
-					return this;
-
-				// HANDLE: $(#id)
-				} else {
-					elem = document.getElementById( match[2] );
-
-					// Check parentNode to catch when Blackberry 4.6 returns
-					// nodes that are no longer in the document #6963
-					if ( elem && elem.parentNode ) {
-						// Inject the element directly into the jQuery object
-						this.length = 1;
-						this[0] = elem;
-					}
-
-					this.context = document;
-					this.selector = selector;
-					return this;
-				}
-
-			// HANDLE: $(expr, $(...))
-			} else if ( !context || context.jquery ) {
-				return ( context || rootjQuery ).find( selector );
-
-			// HANDLE: $(expr, context)
-			// (which is just equivalent to: $(context).find(expr)
-			} else {
-				return this.constructor( context ).find( selector );
-			}
-
-		// HANDLE: $(DOMElement)
-		} else if ( selector.nodeType ) {
-			this.context = this[0] = selector;
-			this.length = 1;
-			return this;
-
-		// HANDLE: $(function)
-		// Shortcut for document ready
-		} else if ( jQuery.isFunction( selector ) ) {
-			return typeof rootjQuery.ready !== "undefined" ?
-				rootjQuery.ready( selector ) :
-				// Execute immediately if ready is not present
-				selector( jQuery );
-		}
-
-		if ( selector.selector !== undefined ) {
-			this.selector = selector.selector;
-			this.context = selector.context;
-		}
-
-		return jQuery.makeArray( selector, this );
-	};
-
-// Give the init function the jQuery prototype for later instantiation
-init.prototype = jQuery.fn;
-
-// Initialize central reference
-rootjQuery = jQuery( document );
-
-
-var rparentsprev = /^(?:parents|prev(?:Until|All))/,
-	// methods guaranteed to produce a unique set when starting from a unique set
-	guaranteedUnique = {
-		children: true,
-		contents: true,
-		next: true,
-		prev: true
-	};
-
-jQuery.extend({
-	dir: function( elem, dir, until ) {
-		var matched = [],
-			truncate = until !== undefined;
-
-		while ( (elem = elem[ dir ]) && elem.nodeType !== 9 ) {
-			if ( elem.nodeType === 1 ) {
-				if ( truncate && jQuery( elem ).is( until ) ) {
-					break;
-				}
-				matched.push( elem );
-			}
-		}
-		return matched;
-	},
-
-	sibling: function( n, elem ) {
-		var matched = [];
-
-		for ( ; n; n = n.nextSibling ) {
-			if ( n.nodeType === 1 && n !== elem ) {
-				matched.push( n );
-			}
-		}
-
-		return matched;
-	}
-});
-
-jQuery.fn.extend({
-	has: function( target ) {
-		var targets = jQuery( target, this ),
-			l = targets.length;
-
-		return this.filter(function() {
-			var i = 0;
-			for ( ; i < l; i++ ) {
-				if ( jQuery.contains( this, targets[i] ) ) {
-					return true;
-				}
-			}
-		});
-	},
-
-	closest: function( selectors, context ) {
-		var cur,
-			i = 0,
-			l = this.length,
-			matched = [],
-			pos = rneedsContext.test( selectors ) || typeof selectors !== "string" ?
-				jQuery( selectors, context || this.context ) :
-				0;
-
-		for ( ; i < l; i++ ) {
-			for ( cur = this[i]; cur && cur !== context; cur = cur.parentNode ) {
-				// Always skip document fragments
-				if ( cur.nodeType < 11 && (pos ?
-					pos.index(cur) > -1 :
-
-					// Don't pass non-elements to Sizzle
-					cur.nodeType === 1 &&
-						jQuery.find.matchesSelector(cur, selectors)) ) {
-
-					matched.push( cur );
-					break;
-				}
-			}
-		}
-
-		return this.pushStack( matched.length > 1 ? jQuery.unique( matched ) : matched );
-	},
-
-	// Determine the position of an element within
-	// the matched set of elements
-	index: function( elem ) {
-
-		// No argument, return index in parent
-		if ( !elem ) {
-			return ( this[ 0 ] && this[ 0 ].parentNode ) ? this.first().prevAll().length : -1;
-		}
-
-		// index in selector
-		if ( typeof elem === "string" ) {
-			return indexOf.call( jQuery( elem ), this[ 0 ] );
-		}
-
-		// Locate the position of the desired element
-		return indexOf.call( this,
-
-			// If it receives a jQuery object, the first element is used
-			elem.jquery ? elem[ 0 ] : elem
-		);
-	},
-
-	add: function( selector, context ) {
-		return this.pushStack(
-			jQuery.unique(
-				jQuery.merge( this.get(), jQuery( selector, context ) )
-			)
-		);
-	},
-
-	addBack: function( selector ) {
-		return this.add( selector == null ?
-			this.prevObject : this.prevObject.filter(selector)
-		);
-	}
-});
-
-function sibling( cur, dir ) {
-	while ( (cur = cur[dir]) && cur.nodeType !== 1 ) {}
-	return cur;
-}
-
-jQuery.each({
-	parent: function( elem ) {
-		var parent = elem.parentNode;
-		return parent && parent.nodeType !== 11 ? parent : null;
-	},
-	parents: function( elem ) {
-		return jQuery.dir( elem, "parentNode" );
-	},
-	parentsUntil: function( elem, i, until ) {
-		return jQuery.dir( elem, "parentNode", until );
-	},
-	next: function( elem ) {
-		return sibling( elem, "nextSibling" );
-	},
-	prev: function( elem ) {
-		return sibling( elem, "previousSibling" );
-	},
-	nextAll: function( elem ) {
-		return jQuery.dir( elem, "nextSibling" );
-	},
-	prevAll: function( elem ) {
-		return jQuery.dir( elem, "previousSibling" );
-	},
-	nextUntil: function( elem, i, until ) {
-		return jQuery.dir( elem, "nextSibling", until );
-	},
-	prevUntil: function( elem, i, until ) {
-		return jQuery.dir( elem, "previousSibling", until );
-	},
-	siblings: function( elem ) {
-		return jQuery.sibling( ( elem.parentNode || {} ).firstChild, elem );
-	},
-	children: function( elem ) {
-		return jQuery.sibling( elem.firstChild );
-	},
-	contents: function( elem ) {
-		return elem.contentDocument || jQuery.merge( [], elem.childNodes );
-	}
-}, function( name, fn ) {
-	jQuery.fn[ name ] = function( until, selector ) {
-		var matched = jQuery.map( this, fn, until );
-
-		if ( name.slice( -5 ) !== "Until" ) {
-			selector = until;
-		}
-
-		if ( selector && typeof selector === "string" ) {
-			matched = jQuery.filter( selector, matched );
-		}
-
-		if ( this.length > 1 ) {
-			// Remove duplicates
-			if ( !guaranteedUnique[ name ] ) {
-				jQuery.unique( matched );
-			}
-
-			// Reverse order for parents* and prev-derivatives
-			if ( rparentsprev.test( name ) ) {
-				matched.reverse();
-			}
-		}
-
-		return this.pushStack( matched );
-	};
-});
-var rnotwhite = (/\S+/g);
-
-
-
-// String to Object options format cache
-var optionsCache = {};
-
-// Convert String-formatted options into Object-formatted ones and store in cache
-function createOptions( options ) {
-	var object = optionsCache[ options ] = {};
-	jQuery.each( options.match( rnotwhite ) || [], function( _, flag ) {
-		object[ flag ] = true;
-	});
-	return object;
-}
-
-/*
- * Create a callback list using the following parameters:
- *
- *	options: an optional list of space-separated options that will change how
- *			the callback list behaves or a more traditional option object
- *
- * By default a callback list will act like an event callback list and can be
- * "fired" multiple times.
- *
- * Possible options:
- *
- *	once:			will ensure the callback list can only be fired once (like a Deferred)
- *
- *	memory:			will keep track of previous values and will call any callback added
- *					after the list has been fired right away with the latest "memorized"
- *					values (like a Deferred)
- *
- *	unique:			will ensure a callback can only be added once (no duplicate in the list)
- *
- *	stopOnFalse:	interrupt callings when a callback returns false
- *
- */
-jQuery.Callbacks = function( options ) {
-
-	// Convert options from String-formatted to Object-formatted if needed
-	// (we check in cache first)
-	options = typeof options === "string" ?
-		( optionsCache[ options ] || createOptions( options ) ) :
-		jQuery.extend( {}, options );
-
-	var // Last fire value (for non-forgettable lists)
-		memory,
-		// Flag to know if list was already fired
-		fired,
-		// Flag to know if list is currently firing
-		firing,
-		// First callback to fire (used internally by add and fireWith)
-		firingStart,
-		// End of the loop when firing
-		firingLength,
-		// Index of currently firing callback (modified by remove if needed)
-		firingIndex,
-		// Actual callback list
-		list = [],
-		// Stack of fire calls for repeatable lists
-		stack = !options.once && [],
-		// Fire callbacks
-		fire = function( data ) {
-			memory = options.memory && data;
-			fired = true;
-			firingIndex = firingStart || 0;
-			firingStart = 0;
-			firingLength = list.length;
-			firing = true;
-			for ( ; list && firingIndex < firingLength; firingIndex++ ) {
-				if ( list[ firingIndex ].apply( data[ 0 ], data[ 1 ] ) === false && options.stopOnFalse ) {
-					memory = false; // To prevent further calls using add
-					break;
-				}
-			}
-			firing = false;
-			if ( list ) {
-				if ( stack ) {
-					if ( stack.length ) {
-						fire( stack.shift() );
-					}
-				} else if ( memory ) {
-					list = [];
-				} else {
-					self.disable();
-				}
-			}
-		},
-		// Actual Callbacks object
-		self = {
-			// Add a callback or a collection of callbacks to the list
-			add: function() {
-				if ( list ) {
-					// First, we save the current length
-					var start = list.length;
-					(function add( args ) {
-						jQuery.each( args, function( _, arg ) {
-							var type = jQuery.type( arg );
-							if ( type === "function" ) {
-								if ( !options.unique || !self.has( arg ) ) {
-									list.push( arg );
-								}
-							} else if ( arg && arg.length && type !== "string" ) {
-								// Inspect recursively
-								add( arg );
-							}
-						});
-					})( arguments );
-					// Do we need to add the callbacks to the
-					// current firing batch?
-					if ( firing ) {
-						firingLength = list.length;
-					// With memory, if we're not firing then
-					// we should call right away
-					} else if ( memory ) {
-						firingStart = start;
-						fire( memory );
-					}
-				}
-				return this;
-			},
-			// Remove a callback from the list
-			remove: function() {
-				if ( list ) {
-					jQuery.each( arguments, function( _, arg ) {
-						var index;
-						while ( ( index = jQuery.inArray( arg, list, index ) ) > -1 ) {
-							list.splice( index, 1 );
-							// Handle firing indexes
-							if ( firing ) {
-								if ( index <= firingLength ) {
-									firingLength--;
-								}
-								if ( index <= firingIndex ) {
-									firingIndex--;
-								}
-							}
-						}
-					});
-				}
-				return this;
-			},
-			// Check if a given callback is in the list.
-			// If no argument is given, return whether or not list has callbacks attached.
-			has: function( fn ) {
-				return fn ? jQuery.inArray( fn, list ) > -1 : !!( list && list.length );
-			},
-			// Remove all callbacks from the list
-			empty: function() {
-				list = [];
-				firingLength = 0;
-				return this;
-			},
-			// Have the list do nothing anymore
-			disable: function() {
-				list = stack = memory = undefined;
-				return this;
-			},
-			// Is it disabled?
-			disabled: function() {
-				return !list;
-			},
-			// Lock the list in its current state
-			lock: function() {
-				stack = undefined;
-				if ( !memory ) {
-					self.disable();
-				}
-				return this;
-			},
-			// Is it locked?
-			locked: function() {
-				return !stack;
-			},
-			// Call all callbacks with the given context and arguments
-			fireWith: function( context, args ) {
-				if ( list && ( !fired || stack ) ) {
-					args = args || [];
-					args = [ context, args.slice ? args.slice() : args ];
-					if ( firing ) {
-						stack.push( args );
-					} else {
-						fire( args );
-					}
-				}
-				return this;
-			},
-			// Call all the callbacks with the given arguments
-			fire: function() {
-				self.fireWith( this, arguments );
-				return this;
-			},
-			// To know if the callbacks have already been called at least once
-			fired: function() {
-				return !!fired;
-			}
-		};
-
-	return self;
-};
-
-
-jQuery.extend({
-
-	Deferred: function( func ) {
-		var tuples = [
-				// action, add listener, listener list, final state
-				[ "resolve", "done", jQuery.Callbacks("once memory"), "resolved" ],
-				[ "reject", "fail", jQuery.Callbacks("once memory"), "rejected" ],
-				[ "notify", "progress", jQuery.Callbacks("memory") ]
-			],
-			state = "pending",
-			promise = {
-				state: function() {
-					return state;
-				},
-				always: function() {
-					deferred.done( arguments ).fail( arguments );
-					return this;
-				},
-				then: function( /* fnDone, fnFail, fnProgress */ ) {
-					var fns = arguments;
-					return jQuery.Deferred(function( newDefer ) {
-						jQuery.each( tuples, function( i, tuple ) {
-							var fn = jQuery.isFunction( fns[ i ] ) && fns[ i ];
-							// deferred[ done | fail | progress ] for forwarding actions to newDefer
-							deferred[ tuple[1] ](function() {
-								var returned = fn && fn.apply( this, arguments );
-								if ( returned && jQuery.isFunction( returned.promise ) ) {
-									returned.promise()
-										.done( newDefer.resolve )
-										.fail( newDefer.reject )
-										.progress( newDefer.notify );
-								} else {
-									newDefer[ tuple[ 0 ] + "With" ]( this === promise ? newDefer.promise() : this, fn ? [ returned ] : arguments );
-								}
-							});
-						});
-						fns = null;
-					}).promise();
-				},
-				// Get a promise for this deferred
-				// If obj is provided, the promise aspect is added to the object
-				promise: function( obj ) {
-					return obj != null ? jQuery.extend( obj, promise ) : promise;
-				}
-			},
-			deferred = {};
-
-		// Keep pipe for back-compat
-		promise.pipe = promise.then;
-
-		// Add list-specific methods
-		jQuery.each( tuples, function( i, tuple ) {
-			var list = tuple[ 2 ],
-				stateString = tuple[ 3 ];
-
-			// promise[ done | fail | progress ] = list.add
-			promise[ tuple[1] ] = list.add;
-
-			// Handle state
-			if ( stateString ) {
-				list.add(function() {
-					// state = [ resolved | rejected ]
-					state = stateString;
-
-				// [ reject_list | resolve_list ].disable; progress_list.lock
-				}, tuples[ i ^ 1 ][ 2 ].disable, tuples[ 2 ][ 2 ].lock );
-			}
-
-			// deferred[ resolve | reject | notify ]
-			deferred[ tuple[0] ] = function() {
-				deferred[ tuple[0] + "With" ]( this === deferred ? promise : this, arguments );
-				return this;
-			};
-			deferred[ tuple[0] + "With" ] = list.fireWith;
-		});
-
-		// Make the deferred a promise
-		promise.promise( deferred );
-
-		// Call given func if any
-		if ( func ) {
-			func.call( deferred, deferred );
-		}
-
-		// All done!
-		return deferred;
-	},
-
-	// Deferred helper
-	when: function( subordinate /* , ..., subordinateN */ ) {
-		var i = 0,
-			resolveValues = slice.call( arguments ),
-			length = resolveValues.length,
-
-			// the count of uncompleted subordinates
-			remaining = length !== 1 || ( subordinate && jQuery.isFunction( subordinate.promise ) ) ? length : 0,
-
-			// the master Deferred. If resolveValues consist of only a single Deferred, just use that.
-			deferred = remaining === 1 ? subordinate : jQuery.Deferred(),
-
-			// Update function for both resolve and progress values
-			updateFunc = function( i, contexts, values ) {
-				return function( value ) {
-					contexts[ i ] = this;
-					values[ i ] = arguments.length > 1 ? slice.call( arguments ) : value;
-					if ( values === progressValues ) {
-						deferred.notifyWith( contexts, values );
-					} else if ( !( --remaining ) ) {
-						deferred.resolveWith( contexts, values );
-					}
-				};
-			},
-
-			progressValues, progressContexts, resolveContexts;
-
-		// add listeners to Deferred subordinates; treat others as resolved
-		if ( length > 1 ) {
-			progressValues = new Array( length );
-			progressContexts = new Array( length );
-			resolveContexts = new Array( length );
-			for ( ; i < length; i++ ) {
-				if ( resolveValues[ i ] && jQuery.isFunction( resolveValues[ i ].promise ) ) {
-					resolveValues[ i ].promise()
-						.done( updateFunc( i, resolveContexts, resolveValues ) )
-						.fail( deferred.reject )
-						.progress( updateFunc( i, progressContexts, progressValues ) );
-				} else {
-					--remaining;
-				}
-			}
-		}
-
-		// if we're not waiting on anything, resolve the master
-		if ( !remaining ) {
-			deferred.resolveWith( resolveContexts, resolveValues );
-		}
-
-		return deferred.promise();
-	}
-});
-
-
-// The deferred used on DOM ready
-var readyList;
-
-jQuery.fn.ready = function( fn ) {
-	// Add the callback
-	jQuery.ready.promise().done( fn );
-
-	return this;
-};
-
-jQuery.extend({
-	// Is the DOM ready to be used? Set to true once it occurs.
-	isReady: false,
-
-	// A counter to track how many items to wait for before
-	// the ready event fires. See #6781
-	readyWait: 1,
-
-	// Hold (or release) the ready event
-	holdReady: function( hold ) {
-		if ( hold ) {
-			jQuery.readyWait++;
-		} else {
-			jQuery.ready( true );
-		}
-	},
-
-	// Handle when the DOM is ready
-	ready: function( wait ) {
-
-		// Abort if there are pending holds or we're already ready
-		if ( wait === true ? --jQuery.readyWait : jQuery.isReady ) {
-			return;
-		}
-
-		// Remember that the DOM is ready
-		jQuery.isReady = true;
-
-		// If a normal DOM Ready event fired, decrement, and wait if need be
-		if ( wait !== true && --jQuery.readyWait > 0 ) {
-			return;
-		}
-
-		// If there are functions bound, to execute
-		readyList.resolveWith( document, [ jQuery ] );
-
-		// Trigger any bound ready events
-		if ( jQuery.fn.triggerHandler ) {
-			jQuery( document ).triggerHandler( "ready" );
-			jQuery( document ).off( "ready" );
-		}
-	}
-});
-
-/**
- * The ready event handler and self cleanup method
- */
-function completed() {
-	document.removeEventListener( "DOMContentLoaded", completed, false );
-	window.removeEventListener( "load", completed, false );
-	jQuery.ready();
-}
-
-jQuery.ready.promise = function( obj ) {
-	if ( !readyList ) {
-
-		readyList = jQuery.Deferred();
-
-		// Catch cases where $(document).ready() is called after the browser event has already occurred.
-		// we once tried to use readyState "interactive" here, but it caused issues like the one
-		// discovered by ChrisS here: http://bugs.jquery.com/ticket/12282#comment:15
-		if ( document.readyState === "complete" ) {
-			// Handle it asynchronously to allow scripts the opportunity to delay ready
-			setTimeout( jQuery.ready );
-
-		} else {
-
-			// Use the handy event callback
-			document.addEventListener( "DOMContentLoaded", completed, false );
-
-			// A fallback to window.onload, that will always work
-			window.addEventListener( "load", completed, false );
-		}
-	}
-	return readyList.promise( obj );
-};
-
-// Kick off the DOM ready check even if the user does not
-jQuery.ready.promise();
-
-
-
-
-// Multifunctional method to get and set values of a collection
-// The value/s can optionally be executed if it's a function
-var access = jQuery.access = function( elems, fn, key, value, chainable, emptyGet, raw ) {
-	var i = 0,
-		len = elems.length,
-		bulk = key == null;
-
-	// Sets many values
-	if ( jQuery.type( key ) === "object" ) {
-		chainable = true;
-		for ( i in key ) {
-			jQuery.access( elems, fn, i, key[i], true, emptyGet, raw );
-		}
-
-	// Sets one value
-	} else if ( value !== undefined ) {
-		chainable = true;
-
-		if ( !jQuery.isFunction( value ) ) {
-			raw = true;
-		}
-
-		if ( bulk ) {
-			// Bulk operations run against the entire set
-			if ( raw ) {
-				fn.call( elems, value );
-				fn = null;
-
-			// ...except when executing function values
-			} else {
-				bulk = fn;
-				fn = function( elem, key, value ) {
-					return bulk.call( jQuery( elem ), value );
-				};
-			}
-		}
-
-		if ( fn ) {
-			for ( ; i < len; i++ ) {
-				fn( elems[i], key, raw ? value : value.call( elems[i], i, fn( elems[i], key ) ) );
-			}
-		}
-	}
-
-	return chainable ?
-		elems :
-
-		// Gets
-		bulk ?
-			fn.call( elems ) :
-			len ? fn( elems[0], key ) : emptyGet;
-};
-
-
-/**
- * Determines whether an object can have data
- */
-jQuery.acceptData = function( owner ) {
-	// Accepts only:
-	//  - Node
-	//    - Node.ELEMENT_NODE
-	//    - Node.DOCUMENT_NODE
-	//  - Object
-	//    - Any
-	/* jshint -W018 */
-	return owner.nodeType === 1 || owner.nodeType === 9 || !( +owner.nodeType );
-};
-
-
-function Data() {
-	// Support: Android < 4,
-	// Old WebKit does not have Object.preventExtensions/freeze method,
-	// return new empty object instead with no [[set]] accessor
-	Object.defineProperty( this.cache = {}, 0, {
-		get: function() {
-			return {};
-		}
-	});
-
-	this.expando = jQuery.expando + Math.random();
-}
-
-Data.uid = 1;
-Data.accepts = jQuery.acceptData;
-
-Data.prototype = {
-	key: function( owner ) {
-		// We can accept data for non-element nodes in modern browsers,
-		// but we should not, see #8335.
-		// Always return the key for a frozen object.
-		if ( !Data.accepts( owner ) ) {
-			return 0;
-		}
-
-		var descriptor = {},
-			// Check if the owner object already has a cache key
-			unlock = owner[ this.expando ];
-
-		// If not, create one
-		if ( !unlock ) {
-			unlock = Data.uid++;
-
-			// Secure it in a non-enumerable, non-writable property
-			try {
-				descriptor[ this.expando ] = { value: unlock };
-				Object.defineProperties( owner, descriptor );
-
-			// Support: Android < 4
-			// Fallback to a less secure definition
-			} catch ( e ) {
-				descriptor[ this.expando ] = unlock;
-				jQuery.extend( owner, descriptor );
-			}
-		}
-
-		// Ensure the cache object
-		if ( !this.cache[ unlock ] ) {
-			this.cache[ unlock ] = {};
-		}
-
-		return unlock;
-	},
-	set: function( owner, data, value ) {
-		var prop,
-			// There may be an unlock assigned to this node,
-			// if there is no entry for this "owner", create one inline
-			// and set the unlock as though an owner entry had always existed
-			unlock = this.key( owner ),
-			cache = this.cache[ unlock ];
-
-		// Handle: [ owner, key, value ] args
-		if ( typeof data === "string" ) {
-			cache[ data ] = value;
-
-		// Handle: [ owner, { properties } ] args
-		} else {
-			// Fresh assignments by object are shallow copied
-			if ( jQuery.isEmptyObject( cache ) ) {
-				jQuery.extend( this.cache[ unlock ], data );
-			// Otherwise, copy the properties one-by-one to the cache object
-			} else {
-				for ( prop in data ) {
-					cache[ prop ] = data[ prop ];
-				}
-			}
-		}
-		return cache;
-	},
-	get: function( owner, key ) {
-		// Either a valid cache is found, or will be created.
-		// New caches will be created and the unlock returned,
-		// allowing direct access to the newly created
-		// empty data object. A valid owner object must be provided.
-		var cache = this.cache[ this.key( owner ) ];
-
-		return key === undefined ?
-			cache : cache[ key ];
-	},
-	access: function( owner, key, value ) {
-		var stored;
-		// In cases where either:
-		//
-		//   1. No key was specified
-		//   2. A string key was specified, but no value provided
-		//
-		// Take the "read" path and allow the get method to determine
-		// which value to return, respectively either:
-		//
-		//   1. The entire cache object
-		//   2. The data stored at the key
-		//
-		if ( key === undefined ||
-				((key && typeof key === "string") && value === undefined) ) {
-
-			stored = this.get( owner, key );
-
-			return stored !== undefined ?
-				stored : this.get( owner, jQuery.camelCase(key) );
-		}
-
-		// [*]When the key is not a string, or both a key and value
-		// are specified, set or extend (existing objects) with either:
-		//
-		//   1. An object of properties
-		//   2. A key and value
-		//
-		this.set( owner, key, value );
-
-		// Since the "set" path can have two possible entry points
-		// return the expected data based on which path was taken[*]
-		return value !== undefined ? value : key;
-	},
-	remove: function( owner, key ) {
-		var i, name, camel,
-			unlock = this.key( owner ),
-			cache = this.cache[ unlock ];
-
-		if ( key === undefined ) {
-			this.cache[ unlock ] = {};
-
-		} else {
-			// Support array or space separated string of keys
-			if ( jQuery.isArray( key ) ) {
-				// If "name" is an array of keys...
-				// When data is initially created, via ("key", "val") signature,
-				// keys will be converted to camelCase.
-				// Since there is no way to tell _how_ a key was added, remove
-				// both plain key and camelCase key. #12786
-				// This will only penalize the array argument path.
-				name = key.concat( key.map( jQuery.camelCase ) );
-			} else {
-				camel = jQuery.camelCase( key );
-				// Try the string as a key before any manipulation
-				if ( key in cache ) {
-					name = [ key, camel ];
-				} else {
-					// If a key with the spaces exists, use it.
-					// Otherwise, create an array by matching non-whitespace
-					name = camel;
-					name = name in cache ?
-						[ name ] : ( name.match( rnotwhite ) || [] );
-				}
-			}
-
-			i = name.length;
-			while ( i-- ) {
-				delete cache[ name[ i ] ];
-			}
-		}
-	},
-	hasData: function( owner ) {
-		return !jQuery.isEmptyObject(
-			this.cache[ owner[ this.expando ] ] || {}
-		);
-	},
-	discard: function( owner ) {
-		if ( owner[ this.expando ] ) {
-			delete this.cache[ owner[ this.expando ] ];
-		}
-	}
-};
-var data_priv = new Data();
-
-var data_user = new Data();
-
-
-
-/*
-	Implementation Summary
-
-	1. Enforce API surface and semantic compatibility with 1.9.x branch
-	2. Improve the module's maintainability by reducing the storage
-		paths to a single mechanism.
-	3. Use the same single mechanism to support "private" and "user" data.
-	4. _Never_ expose "private" data to user code (TODO: Drop _data, _removeData)
-	5. Avoid exposing implementation details on user objects (eg. expando properties)
-	6. Provide a clear path for implementation upgrade to WeakMap in 2014
-*/
-var rbrace = /^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,
-	rmultiDash = /([A-Z])/g;
-
-function dataAttr( elem, key, data ) {
-	var name;
-
-	// If nothing was found internally, try to fetch any
-	// data from the HTML5 data-* attribute
-	if ( data === undefined && elem.nodeType === 1 ) {
-		name = "data-" + key.replace( rmultiDash, "-$1" ).toLowerCase();
-		data = elem.getAttribute( name );
-
-		if ( typeof data === "string" ) {
-			try {
-				data = data === "true" ? true :
-					data === "false" ? false :
-					data === "null" ? null :
-					// Only convert to a number if it doesn't change the string
-					+data + "" === data ? +data :
-					rbrace.test( data ) ? jQuery.parseJSON( data ) :
-					data;
-			} catch( e ) {}
-
-			// Make sure we set the data so it isn't changed later
-			data_user.set( elem, key, data );
-		} else {
-			data = undefined;
-		}
-	}
-	return data;
-}
-
-jQuery.extend({
-	hasData: function( elem ) {
-		return data_user.hasData( elem ) || data_priv.hasData( elem );
-	},
-
-	data: function( elem, name, data ) {
-		return data_user.access( elem, name, data );
-	},
-
-	removeData: function( elem, name ) {
-		data_user.remove( elem, name );
-	},
-
-	// TODO: Now that all calls to _data and _removeData have been replaced
-	// with direct calls to data_priv methods, these can be deprecated.
-	_data: function( elem, name, data ) {
-		return data_priv.access( elem, name, data );
-	},
-
-	_removeData: function( elem, name ) {
-		data_priv.remove( elem, name );
-	}
-});
-
-jQuery.fn.extend({
-	data: function( key, value ) {
-		var i, name, data,
-			elem = this[ 0 ],
-			attrs = elem && elem.attributes;
-
-		// Gets all values
-		if ( key === undefined ) {
-			if ( this.length ) {
-				data = data_user.get( elem );
-
-				if ( elem.nodeType === 1 && !data_priv.get( elem, "hasDataAttrs" ) ) {
-					i = attrs.length;
-					while ( i-- ) {
-
-						// Support: IE11+
-						// The attrs elements can be null (#14894)
-						if ( attrs[ i ] ) {
-							name = attrs[ i ].name;
-							if ( name.indexOf( "data-" ) === 0 ) {
-								name = jQuery.camelCase( name.slice(5) );
-								dataAttr( elem, name, data[ name ] );
-							}
-						}
-					}
-					data_priv.set( elem, "hasDataAttrs", true );
-				}
-			}
-
-			return data;
-		}
-
-		// Sets multiple values
-		if ( typeof key === "object" ) {
-			return this.each(function() {
-				data_user.set( this, key );
-			});
-		}
-
-		return access( this, function( value ) {
-			var data,
-				camelKey = jQuery.camelCase( key );
-
-			// The calling jQuery object (element matches) is not empty
-			// (and therefore has an element appears at this[ 0 ]) and the
-			// `value` parameter was not undefined. An empty jQuery object
-			// will result in `undefined` for elem = this[ 0 ] which will
-			// throw an exception if an attempt to read a data cache is made.
-			if ( elem && value === undefined ) {
-				// Attempt to get data from the cache
-				// with the key as-is
-				data = data_user.get( elem, key );
-				if ( data !== undefined ) {
-					return data;
-				}
-
-				// Attempt to get data from the cache
-				// with the key camelized
-				data = data_user.get( elem, camelKey );
-				if ( data !== undefined ) {
-					return data;
-				}
-
-				// Attempt to "discover" the data in
-				// HTML5 custom data-* attrs
-				data = dataAttr( elem, camelKey, undefined );
-				if ( data !== undefined ) {
-					return data;
-				}
-
-				// We tried really hard, but the data doesn't exist.
-				return;
-			}
-
-			// Set the data...
-			this.each(function() {
-				// First, attempt to store a copy or reference of any
-				// data that might've been store with a camelCased key.
-				var data = data_user.get( this, camelKey );
-
-				// For HTML5 data-* attribute interop, we have to
-				// store property names with dashes in a camelCase form.
-				// This might not apply to all properties...*
-				data_user.set( this, camelKey, value );
-
-				// *... In the case of properties that might _actually_
-				// have dashes, we need to also store a copy of that
-				// unchanged property.
-				if ( key.indexOf("-") !== -1 && data !== undefined ) {
-					data_user.set( this, key, value );
-				}
-			});
-		}, null, value, arguments.length > 1, null, true );
-	},
-
-	removeData: function( key ) {
-		return this.each(function() {
-			data_user.remove( this, key );
-		});
-	}
-});
-
-
-jQuery.extend({
-	queue: function( elem, type, data ) {
-		var queue;
-
-		if ( elem ) {
-			type = ( type || "fx" ) + "queue";
-			queue = data_priv.get( elem, type );
-
-			// Speed up dequeue by getting out quickly if this is just a lookup
-			if ( data ) {
-				if ( !queue || jQuery.isArray( data ) ) {
-					queue = data_priv.access( elem, type, jQuery.makeArray(data) );
-				} else {
-					queue.push( data );
-				}
-			}
-			return queue || [];
-		}
-	},
-
-	dequeue: function( elem, type ) {
-		type = type || "fx";
-
-		var queue = jQuery.queue( elem, type ),
-			startLength = queue.length,
-			fn = queue.shift(),
-			hooks = jQuery._queueHooks( elem, type ),
-			next = function() {
-				jQuery.dequeue( elem, type );
-			};
-
-		// If the fx queue is dequeued, always remove the progress sentinel
-		if ( fn === "inprogress" ) {
-			fn = queue.shift();
-			startLength--;
-		}
-
-		if ( fn ) {
-
-			// Add a progress sentinel to prevent the fx queue from being
-			// automatically dequeued
-			if ( type === "fx" ) {
-				queue.unshift( "inprogress" );
-			}
-
-			// clear up the last queue stop function
-			delete hooks.stop;
-			fn.call( elem, next, hooks );
-		}
-
-		if ( !startLength && hooks ) {
-			hooks.empty.fire();
-		}
-	},
-
-	// not intended for public consumption - generates a queueHooks object, or returns the current one
-	_queueHooks: function( elem, type ) {
-		var key = type + "queueHooks";
-		return data_priv.get( elem, key ) || data_priv.access( elem, key, {
-			empty: jQuery.Callbacks("once memory").add(function() {
-				data_priv.remove( elem, [ type + "queue", key ] );
-			})
-		});
-	}
-});
-
-jQuery.fn.extend({
-	queue: function( type, data ) {
-		var setter = 2;
-
-		if ( typeof type !== "string" ) {
-			data = type;
-			type = "fx";
-			setter--;
-		}
-
-		if ( arguments.length < setter ) {
-			return jQuery.queue( this[0], type );
-		}
-
-		return data === undefined ?
-			this :
-			this.each(function() {
-				var queue = jQuery.queue( this, type, data );
-
-				// ensure a hooks for this queue
-				jQuery._queueHooks( this, type );
-
-				if ( type === "fx" && queue[0] !== "inprogress" ) {
-					jQuery.dequeue( this, type );
-				}
-			});
-	},
-	dequeue: function( type ) {
-		return this.each(function() {
-			jQuery.dequeue( this, type );
-		});
-	},
-	clearQueue: function( type ) {
-		return this.queue( type || "fx", [] );
-	},
-	// Get a promise resolved when queues of a certain type
-	// are emptied (fx is the type by default)
-	promise: function( type, obj ) {
-		var tmp,
-			count = 1,
-			defer = jQuery.Deferred(),
-			elements = this,
-			i = this.length,
-			resolve = function() {
-				if ( !( --count ) ) {
-					defer.resolveWith( elements, [ elements ] );
-				}
-			};
-
-		if ( typeof type !== "string" ) {
-			obj = type;
-			type = undefined;
-		}
-		type = type || "fx";
-
-		while ( i-- ) {
-			tmp = data_priv.get( elements[ i ], type + "queueHooks" );
-			if ( tmp && tmp.empty ) {
-				count++;
-				tmp.empty.add( resolve );
-			}
-		}
-		resolve();
-		return defer.promise( obj );
-	}
-});
-var pnum = (/[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/).source;
-
-var cssExpand = [ "Top", "Right", "Bottom", "Left" ];
-
-var isHidden = function( elem, el ) {
-		// isHidden might be called from jQuery#filter function;
-		// in that case, element will be second argument
-		elem = el || elem;
-		return jQuery.css( elem, "display" ) === "none" || !jQuery.contains( elem.ownerDocument, elem );
-	};
-
-var rcheckableType = (/^(?:checkbox|radio)$/i);
-
-
-
-(function() {
-	var fragment = document.createDocumentFragment(),
-		div = fragment.appendChild( document.createElement( "div" ) ),
-		input = document.createElement( "input" );
-
-	// #11217 - WebKit loses check when the name is after the checked attribute
-	// Support: Windows Web Apps (WWA)
-	// `name` and `type` need .setAttribute for WWA
-	input.setAttribute( "type", "radio" );
-	input.setAttribute( "checked", "checked" );
-	input.setAttribute( "name", "t" );
-
-	div.appendChild( input );
-
-	// Support: Safari 5.1, iOS 5.1, Android 4.x, Android 2.3
-	// old WebKit doesn't clone checked state correctly in fragments
-	support.checkClone = div.cloneNode( true ).cloneNode( true ).lastChild.checked;
-
-	// Make sure textarea (and checkbox) defaultValue is properly cloned
-	// Support: IE9-IE11+
-	div.innerHTML = "<textarea>x</textarea>";
-	support.noCloneChecked = !!div.cloneNode( true ).lastChild.defaultValue;
-})();
-var strundefined = typeof undefined;
-
-
-
-support.focusinBubbles = "onfocusin" in window;
-
-
-var
-	rkeyEvent = /^key/,
-	rmouseEvent = /^(?:mouse|pointer|contextmenu)|click/,
-	rfocusMorph = /^(?:focusinfocus|focusoutblur)$/,
-	rtypenamespace = /^([^.]*)(?:\.(.+)|)$/;
-
-function returnTrue() {
-	return true;
-}
-
-function returnFalse() {
-	return false;
-}
-
-function safeActiveElement() {
-	try {
-		return document.activeElement;
-	} catch ( err ) { }
-}
-
-/*
- * Helper functions for managing events -- not part of the public interface.
- * Props to Dean Edwards' addEvent library for many of the ideas.
- */
-jQuery.event = {
-
-	global: {},
-
-	add: function( elem, types, handler, data, selector ) {
-
-		var handleObjIn, eventHandle, tmp,
-			events, t, handleObj,
-			special, handlers, type, namespaces, origType,
-			elemData = data_priv.get( elem );
-
-		// Don't attach events to noData or text/comment nodes (but allow plain objects)
-		if ( !elemData ) {
-			return;
-		}
-
-		// Caller can pass in an object of custom data in lieu of the handler
-		if ( handler.handler ) {
-			handleObjIn = handler;
-			handler = handleObjIn.handler;
-			selector = handleObjIn.selector;
-		}
-
-		// Make sure that the handler has a unique ID, used to find/remove it later
-		if ( !handler.guid ) {
-			handler.guid = jQuery.guid++;
-		}
-
-		// Init the element's event structure and main handler, if this is the first
-		if ( !(events = elemData.events) ) {
-			events = elemData.events = {};
-		}
-		if ( !(eventHandle = elemData.handle) ) {
-			eventHandle = elemData.handle = function( e ) {
-				// Discard the second event of a jQuery.event.trigger() and
-				// when an event is called after a page has unloaded
-				return typeof jQuery !== strundefined && jQuery.event.triggered !== e.type ?
-					jQuery.event.dispatch.apply( elem, arguments ) : undefined;
-			};
-		}
-
-		// Handle multiple events separated by a space
-		types = ( types || "" ).match( rnotwhite ) || [ "" ];
-		t = types.length;
-		while ( t-- ) {
-			tmp = rtypenamespace.exec( types[t] ) || [];
-			type = origType = tmp[1];
-			namespaces = ( tmp[2] || "" ).split( "." ).sort();
-
-			// There *must* be a type, no attaching namespace-only handlers
-			if ( !type ) {
-				continue;
-			}
-
-			// If event changes its type, use the special event handlers for the changed type
-			special = jQuery.event.special[ type ] || {};
-
-			// If selector defined, determine special event api type, otherwise given type
-			type = ( selector ? special.delegateType : special.bindType ) || type;
-
-			// Update special based on newly reset type
-			special = jQuery.event.special[ type ] || {};
-
-			// handleObj is passed to all event handlers
-			handleObj = jQuery.extend({
-				type: type,
-				origType: origType,
-				data: data,
-				handler: handler,
-				guid: handler.guid,
-				selector: selector,
-				needsContext: selector && jQuery.expr.match.needsContext.test( selector ),
-				namespace: namespaces.join(".")
-			}, handleObjIn );
-
-			// Init the event handler queue if we're the first
-			if ( !(handlers = events[ type ]) ) {
-				handlers = events[ type ] = [];
-				handlers.delegateCount = 0;
-
-				// Only use addEventListener if the special events handler returns false
-				if ( !special.setup || special.setup.call( elem, data, namespaces, eventHandle ) === false ) {
-					if ( elem.addEventListener ) {
-						elem.addEventListener( type, eventHandle, false );
-					}
-				}
-			}
-
-			if ( special.add ) {
-				special.add.call( elem, handleObj );
-
-				if ( !handleObj.handler.guid ) {
-					handleObj.handler.guid = handler.guid;
-				}
-			}
-
-			// Add to the element's handler list, delegates in front
-			if ( selector ) {
-				handlers.splice( handlers.delegateCount++, 0, handleObj );
-			} else {
-				handlers.push( handleObj );
-			}
-
-			// Keep track of which events have ever been used, for event optimization
-			jQuery.event.global[ type ] = true;
-		}
-
-	},
-
-	// Detach an event or set of events from an element
-	remove: function( elem, types, handler, selector, mappedTypes ) {
-
-		var j, origCount, tmp,
-			events, t, handleObj,
-			special, handlers, type, namespaces, origType,
-			elemData = data_priv.hasData( elem ) && data_priv.get( elem );
-
-		if ( !elemData || !(events = elemData.events) ) {
-			return;
-		}
-
-		// Once for each type.namespace in types; type may be omitted
-		types = ( types || "" ).match( rnotwhite ) || [ "" ];
-		t = types.length;
-		while ( t-- ) {
-			tmp = rtypenamespace.exec( types[t] ) || [];
-			type = origType = tmp[1];
-			namespaces = ( tmp[2] || "" ).split( "." ).sort();
-
-			// Unbind all events (on this namespace, if provided) for the element
-			if ( !type ) {
-				for ( type in events ) {
-					jQuery.event.remove( elem, type + types[ t ], handler, selector, true );
-				}
-				continue;
-			}
-
-			special = jQuery.event.special[ type ] || {};
-			type = ( selector ? special.delegateType : special.bindType ) || type;
-			handlers = events[ type ] || [];
-			tmp = tmp[2] && new RegExp( "(^|\\.)" + namespaces.join("\\.(?:.*\\.|)") + "(\\.|$)" );
-
-			// Remove matching events
-			origCount = j = handlers.length;
-			while ( j-- ) {
-				handleObj = handlers[ j ];
-
-				if ( ( mappedTypes || origType === handleObj.origType ) &&
-					( !handler || handler.guid === handleObj.guid ) &&
-					( !tmp || tmp.test( handleObj.namespace ) ) &&
-					( !selector || selector === handleObj.selector || selector === "**" && handleObj.selector ) ) {
-					handlers.splice( j, 1 );
-
-					if ( handleObj.selector ) {
-						handlers.delegateCount--;
-					}
-					if ( special.remove ) {
-						special.remove.call( elem, handleObj );
-					}
-				}
-			}
-
-			// Remove generic event handler if we removed something and no more handlers exist
-			// (avoids potential for endless recursion during removal of special event handlers)
-			if ( origCount && !handlers.length ) {
-				if ( !special.teardown || special.teardown.call( elem, namespaces, elemData.handle ) === false ) {
-					jQuery.removeEvent( elem, type, elemData.handle );
-				}
-
-				delete events[ type ];
-			}
-		}
-
-		// Remove the expando if it's no longer used
-		if ( jQuery.isEmptyObject( events ) ) {
-			delete elemData.handle;
-			data_priv.remove( elem, "events" );
-		}
-	},
-
-	trigger: function( event, data, elem, onlyHandlers ) {
-
-		var i, cur, tmp, bubbleType, ontype, handle, special,
-			eventPath = [ elem || document ],
-			type = hasOwn.call( event, "type" ) ? event.type : event,
-			namespaces = hasOwn.call( event, "namespace" ) ? event.namespace.split(".") : [];
-
-		cur = tmp = elem = elem || document;
-
-		// Don't do events on text and comment nodes
-		if ( elem.nodeType === 3 || elem.nodeType === 8 ) {
-			return;
-		}
-
-		// focus/blur morphs to focusin/out; ensure we're not firing them right now
-		if ( rfocusMorph.test( type + jQuery.event.triggered ) ) {
-			return;
-		}
-
-		if ( type.indexOf(".") >= 0 ) {
-			// Namespaced trigger; create a regexp to match event type in handle()
-			namespaces = type.split(".");
-			type = namespaces.shift();
-			namespaces.sort();
-		}
-		ontype = type.indexOf(":") < 0 && "on" + type;
-
-		// Caller can pass in a jQuery.Event object, Object, or just an event type string
-		event = event[ jQuery.expando ] ?
-			event :
-			new jQuery.Event( type, typeof event === "object" && event );
-
-		// Trigger bitmask: & 1 for native handlers; & 2 for jQuery (always true)
-		event.isTrigger = onlyHandlers ? 2 : 3;
-		event.namespace = namespaces.join(".");
-		event.namespace_re = event.namespace ?
-			new RegExp( "(^|\\.)" + namespaces.join("\\.(?:.*\\.|)") + "(\\.|$)" ) :
-			null;
-
-		// Clean up the event in case it is being reused
-		event.result = undefined;
-		if ( !event.target ) {
-			event.target = elem;
-		}
-
-		// Clone any incoming data and prepend the event, creating the handler arg list
-		data = data == null ?
-			[ event ] :
-			jQuery.makeArray( data, [ event ] );
-
-		// Allow special events to draw outside the lines
-		special = jQuery.event.special[ type ] || {};
-		if ( !onlyHandlers && special.trigger && special.trigger.apply( elem, data ) === false ) {
-			return;
-		}
-
-		// Determine event propagation path in advance, per W3C events spec (#9951)
-		// Bubble up to document, then to window; watch for a global ownerDocument var (#9724)
-		if ( !onlyHandlers && !special.noBubble && !jQuery.isWindow( elem ) ) {
-
-			bubbleType = special.delegateType || type;
-			if ( !rfocusMorph.test( bubbleType + type ) ) {
-				cur = cur.parentNode;
-			}
-			for ( ; cur; cur = cur.parentNode ) {
-				eventPath.push( cur );
-				tmp = cur;
-			}
-
-			// Only add window if we got to document (e.g., not plain obj or detached DOM)
-			if ( tmp === (elem.ownerDocument || document) ) {
-				eventPath.push( tmp.defaultView || tmp.parentWindow || window );
-			}
-		}
-
-		// Fire handlers on the event path
-		i = 0;
-		while ( (cur = eventPath[i++]) && !event.isPropagationStopped() ) {
-
-			event.type = i > 1 ?
-				bubbleType :
-				special.bindType || type;
-
-			// jQuery handler
-			handle = ( data_priv.get( cur, "events" ) || {} )[ event.type ] && data_priv.get( cur, "handle" );
-			if ( handle ) {
-				handle.apply( cur, data );
-			}
-
-			// Native handler
-			handle = ontype && cur[ ontype ];
-			if ( handle && handle.apply && jQuery.acceptData( cur ) ) {
-				event.result = handle.apply( cur, data );
-				if ( event.result === false ) {
-					event.preventDefault();
-				}
-			}
-		}
-		event.type = type;
-
-		// If nobody prevented the default action, do it now
-		if ( !onlyHandlers && !event.isDefaultPrevented() ) {
-
-			if ( (!special._default || special._default.apply( eventPath.pop(), data ) === false) &&
-				jQuery.acceptData( elem ) ) {
-
-				// Call a native DOM method on the target with the same name name as the event.
-				// Don't do default actions on window, that's where global variables be (#6170)
-				if ( ontype && jQuery.isFunction( elem[ type ] ) && !jQuery.isWindow( elem ) ) {
-
-					// Don't re-trigger an onFOO event when we call its FOO() method
-					tmp = elem[ ontype ];
-
-					if ( tmp ) {
-						elem[ ontype ] = null;
-					}
-
-					// Prevent re-triggering of the same event, since we already bubbled it above
-					jQuery.event.triggered = type;
-					elem[ type ]();
-					jQuery.event.triggered = undefined;
-
-					if ( tmp ) {
-						elem[ ontype ] = tmp;
-					}
-				}
-			}
-		}
-
-		return event.result;
-	},
-
-	dispatch: function( event ) {
-
-		// Make a writable jQuery.Event from the native event object
-		event = jQuery.event.fix( event );
-
-		var i, j, ret, matched, handleObj,
-			handlerQueue = [],
-			args = slice.call( arguments ),
-			handlers = ( data_priv.get( this, "events" ) || {} )[ event.type ] || [],
-			special = jQuery.event.special[ event.type ] || {};
-
-		// Use the fix-ed jQuery.Event rather than the (read-only) native event
-		args[0] = event;
-		event.delegateTarget = this;
-
-		// Call the preDispatch hook for the mapped type, and let it bail if desired
-		if ( special.preDispatch && special.preDispatch.call( this, event ) === false ) {
-			return;
-		}
-
-		// Determine handlers
-		handlerQueue = jQuery.event.handlers.call( this, event, handlers );
-
-		// Run delegates first; they may want to stop propagation beneath us
-		i = 0;
-		while ( (matched = handlerQueue[ i++ ]) && !event.isPropagationStopped() ) {
-			event.currentTarget = matched.elem;
-
-			j = 0;
-			while ( (handleObj = matched.handlers[ j++ ]) && !event.isImmediatePropagationStopped() ) {
-
-				// Triggered event must either 1) have no namespace, or
-				// 2) have namespace(s) a subset or equal to those in the bound event (both can have no namespace).
-				if ( !event.namespace_re || event.namespace_re.test( handleObj.namespace ) ) {
-
-					event.handleObj = handleObj;
-					event.data = handleObj.data;
-
-					ret = ( (jQuery.event.special[ handleObj.origType ] || {}).handle || handleObj.handler )
-							.apply( matched.elem, args );
-
-					if ( ret !== undefined ) {
-						if ( (event.result = ret) === false ) {
-							event.preventDefault();
-							event.stopPropagation();
-						}
-					}
-				}
-			}
-		}
-
-		// Call the postDispatch hook for the mapped type
-		if ( special.postDispatch ) {
-			special.postDispatch.call( this, event );
-		}
-
-		return event.result;
-	},
-
-	handlers: function( event, handlers ) {
-		var i, matches, sel, handleObj,
-			handlerQueue = [],
-			delegateCount = handlers.delegateCount,
-			cur = event.target;
-
-		// Find delegate handlers
-		// Black-hole SVG <use> instance trees (#13180)
-		// Avoid non-left-click bubbling in Firefox (#3861)
-		if ( delegateCount && cur.nodeType && (!event.button || event.type !== "click") ) {
-
-			for ( ; cur !== this; cur = cur.parentNode || this ) {
-
-				// Don't process clicks on disabled elements (#6911, #8165, #11382, #11764)
-				if ( cur.disabled !== true || event.type !== "click" ) {
-					matches = [];
-					for ( i = 0; i < delegateCount; i++ ) {
-						handleObj = handlers[ i ];
-
-						// Don't conflict with Object.prototype properties (#13203)
-						sel = handleObj.selector + " ";
-
-						if ( matches[ sel ] === undefined ) {
-							matches[ sel ] = handleObj.needsContext ?
-								jQuery( sel, this ).index( cur ) >= 0 :
-								jQuery.find( sel, this, null, [ cur ] ).length;
-						}
-						if ( matches[ sel ] ) {
-							matches.push( handleObj );
-						}
-					}
-					if ( matches.length ) {
-						handlerQueue.push({ elem: cur, handlers: matches });
-					}
-				}
-			}
-		}
-
-		// Add the remaining (directly-bound) handlers
-		if ( delegateCount < handlers.length ) {
-			handlerQueue.push({ elem: this, handlers: handlers.slice( delegateCount ) });
-		}
-
-		return handlerQueue;
-	},
-
-	// Includes some event props shared by KeyEvent and MouseEvent
-	props: "altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "),
-
-	fixHooks: {},
-
-	keyHooks: {
-		props: "char charCode key keyCode".split(" "),
-		filter: function( event, original ) {
-
-			// Add which for key events
-			if ( event.which == null ) {
-				event.which = original.charCode != null ? original.charCode : original.keyCode;
-			}
-
-			return event;
-		}
-	},
-
-	mouseHooks: {
-		props: "button buttons clientX clientY offsetX offsetY pageX pageY screenX screenY toElement".split(" "),
-		filter: function( event, original ) {
-			var eventDoc, doc, body,
-				button = original.button;
-
-			// Calculate pageX/Y if missing and clientX/Y available
-			if ( event.pageX == null && original.clientX != null ) {
-				eventDoc = event.target.ownerDocument || document;
-				doc = eventDoc.documentElement;
-				body = eventDoc.body;
-
-				event.pageX = original.clientX + ( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) - ( doc && doc.clientLeft || body && body.clientLeft || 0 );
-				event.pageY = original.clientY + ( doc && doc.scrollTop  || body && body.scrollTop  || 0 ) - ( doc && doc.clientTop  || body && body.clientTop  || 0 );
-			}
-
-			// Add which for click: 1 === left; 2 === middle; 3 === right
-			// Note: button is not normalized, so don't use it
-			if ( !event.which && button !== undefined ) {
-				event.which = ( button & 1 ? 1 : ( button & 2 ? 3 : ( button & 4 ? 2 : 0 ) ) );
-			}
-
-			return event;
-		}
-	},
-
-	fix: function( event ) {
-		if ( event[ jQuery.expando ] ) {
-			return event;
-		}
-
-		// Create a writable copy of the event object and normalize some properties
-		var i, prop, copy,
-			type = event.type,
-			originalEvent = event,
-			fixHook = this.fixHooks[ type ];
-
-		if ( !fixHook ) {
-			this.fixHooks[ type ] = fixHook =
-				rmouseEvent.test( type ) ? this.mouseHooks :
-				rkeyEvent.test( type ) ? this.keyHooks :
-				{};
-		}
-		copy = fixHook.props ? this.props.concat( fixHook.props ) : this.props;
-
-		event = new jQuery.Event( originalEvent );
-
-		i = copy.length;
-		while ( i-- ) {
-			prop = copy[ i ];
-			event[ prop ] = originalEvent[ prop ];
-		}
-
-		// Support: Cordova 2.5 (WebKit) (#13255)
-		// All events should have a target; Cordova deviceready doesn't
-		if ( !event.target ) {
-			event.target = document;
-		}
-
-		// Support: Safari 6.0+, Chrome < 28
-		// Target should not be a text node (#504, #13143)
-		if ( event.target.nodeType === 3 ) {
-			event.target = event.target.parentNode;
-		}
-
-		return fixHook.filter ? fixHook.filter( event, originalEvent ) : event;
-	},
-
-	special: {
-		load: {
-			// Prevent triggered image.load events from bubbling to window.load
-			noBubble: true
-		},
-		focus: {
-			// Fire native event if possible so blur/focus sequence is correct
-			trigger: function() {
-				if ( this !== safeActiveElement() && this.focus ) {
-					this.focus();
-					return false;
-				}
-			},
-			delegateType: "focusin"
-		},
-		blur: {
-			trigger: function() {
-				if ( this === safeActiveElement() && this.blur ) {
-					this.blur();
-					return false;
-				}
-			},
-			delegateType: "focusout"
-		},
-		click: {
-			// For checkbox, fire native event so checked state will be right
-			trigger: function() {
-				if ( this.type === "checkbox" && this.click && jQuery.nodeName( this, "input" ) ) {
-					this.click();
-					return false;
-				}
-			},
-
-			// For cross-browser consistency, don't fire native .click() on links
-			_default: function( event ) {
-				return jQuery.nodeName( event.target, "a" );
-			}
-		},
-
-		beforeunload: {
-			postDispatch: function( event ) {
-
-				// Support: Firefox 20+
-				// Firefox doesn't alert if the returnValue field is not set.
-				if ( event.result !== undefined && event.originalEvent ) {
-					event.originalEvent.returnValue = event.result;
-				}
-			}
-		}
-	},
-
-	simulate: function( type, elem, event, bubble ) {
-		// Piggyback on a donor event to simulate a different one.
-		// Fake originalEvent to avoid donor's stopPropagation, but if the
-		// simulated event prevents default then we do the same on the donor.
-		var e = jQuery.extend(
-			new jQuery.Event(),
-			event,
-			{
-				type: type,
-				isSimulated: true,
-				originalEvent: {}
-			}
-		);
-		if ( bubble ) {
-			jQuery.event.trigger( e, null, elem );
-		} else {
-			jQuery.event.dispatch.call( elem, e );
-		}
-		if ( e.isDefaultPrevented() ) {
-			event.preventDefault();
-		}
-	}
-};
-
-jQuery.removeEvent = function( elem, type, handle ) {
-	if ( elem.removeEventListener ) {
-		elem.removeEventListener( type, handle, false );
-	}
-};
-
-jQuery.Event = function( src, props ) {
-	// Allow instantiation without the 'new' keyword
-	if ( !(this instanceof jQuery.Event) ) {
-		return new jQuery.Event( src, props );
-	}
-
-	// Event object
-	if ( src && src.type ) {
-		this.originalEvent = src;
-		this.type = src.type;
-
-		// Events bubbling up the document may have been marked as prevented
-		// by a handler lower down the tree; reflect the correct value.
-		this.isDefaultPrevented = src.defaultPrevented ||
-				src.defaultPrevented === undefined &&
-				// Support: Android < 4.0
-				src.returnValue === false ?
-			returnTrue :
-			returnFalse;
-
-	// Event type
-	} else {
-		this.type = src;
-	}
-
-	// Put explicitly provided properties onto the event object
-	if ( props ) {
-		jQuery.extend( this, props );
-	}
-
-	// Create a timestamp if incoming event doesn't have one
-	this.timeStamp = src && src.timeStamp || jQuery.now();
-
-	// Mark it as fixed
-	this[ jQuery.expando ] = true;
-};
-
-// jQuery.Event is based on DOM3 Events as specified by the ECMAScript Language Binding
-// http://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html
-jQuery.Event.prototype = {
-	isDefaultPrevented: returnFalse,
-	isPropagationStopped: returnFalse,
-	isImmediatePropagationStopped: returnFalse,
-
-	preventDefault: function() {
-		var e = this.originalEvent;
-
-		this.isDefaultPrevented = returnTrue;
-
-		if ( e && e.preventDefault ) {
-			e.preventDefault();
-		}
-	},
-	stopPropagation: function() {
-		var e = this.originalEvent;
-
-		this.isPropagationStopped = returnTrue;
-
-		if ( e && e.stopPropagation ) {
-			e.stopPropagation();
-		}
-	},
-	stopImmediatePropagation: function() {
-		var e = this.originalEvent;
-
-		this.isImmediatePropagationStopped = returnTrue;
-
-		if ( e && e.stopImmediatePropagation ) {
-			e.stopImmediatePropagation();
-		}
-
-		this.stopPropagation();
-	}
-};
-
-// Create mouseenter/leave events using mouseover/out and event-time checks
-// Support: Chrome 15+
-jQuery.each({
-	mouseenter: "mouseover",
-	mouseleave: "mouseout",
-	pointerenter: "pointerover",
-	pointerleave: "pointerout"
-}, function( orig, fix ) {
-	jQuery.event.special[ orig ] = {
-		delegateType: fix,
-		bindType: fix,
-
-		handle: function( event ) {
-			var ret,
-				target = this,
-				related = event.relatedTarget,
-				handleObj = event.handleObj;
-
-			// For mousenter/leave call the handler if related is outside the target.
-			// NB: No relatedTarget if the mouse left/entered the browser window
-			if ( !related || (related !== target && !jQuery.contains( target, related )) ) {
-				event.type = handleObj.origType;
-				ret = handleObj.handler.apply( this, arguments );
-				event.type = fix;
-			}
-			return ret;
-		}
-	};
-});
-
-// Create "bubbling" focus and blur events
-// Support: Firefox, Chrome, Safari
-if ( !support.focusinBubbles ) {
-	jQuery.each({ focus: "focusin", blur: "focusout" }, function( orig, fix ) {
-
-		// Attach a single capturing handler on the document while someone wants focusin/focusout
-		var handler = function( event ) {
-				jQuery.event.simulate( fix, event.target, jQuery.event.fix( event ), true );
-			};
-
-		jQuery.event.special[ fix ] = {
-			setup: function() {
-				var doc = this.ownerDocument || this,
-					attaches = data_priv.access( doc, fix );
-
-				if ( !attaches ) {
-					doc.addEventListener( orig, handler, true );
-				}
-				data_priv.access( doc, fix, ( attaches || 0 ) + 1 );
-			},
-			teardown: function() {
-				var doc = this.ownerDocument || this,
-					attaches = data_priv.access( doc, fix ) - 1;
-
-				if ( !attaches ) {
-					doc.removeEventListener( orig, handler, true );
-					data_priv.remove( doc, fix );
-
-				} else {
-					data_priv.access( doc, fix, attaches );
-				}
-			}
-		};
-	});
-}
-
-jQuery.fn.extend({
-
-	on: function( types, selector, data, fn, /*INTERNAL*/ one ) {
-		var origFn, type;
-
-		// Types can be a map of types/handlers
-		if ( typeof types === "object" ) {
-			// ( types-Object, selector, data )
-			if ( typeof selector !== "string" ) {
-				// ( types-Object, data )
-				data = data || selector;
-				selector = undefined;
-			}
-			for ( type in types ) {
-				this.on( type, selector, data, types[ type ], one );
-			}
-			return this;
-		}
-
-		if ( data == null && fn == null ) {
-			// ( types, fn )
-			fn = selector;
-			data = selector = undefined;
-		} else if ( fn == null ) {
-			if ( typeof selector === "string" ) {
-				// ( types, selector, fn )
-				fn = data;
-				data = undefined;
-			} else {
-				// ( types, data, fn )
-				fn = data;
-				data = selector;
-				selector = undefined;
-			}
-		}
-		if ( fn === false ) {
-			fn = returnFalse;
-		} else if ( !fn ) {
-			return this;
-		}
-
-		if ( one === 1 ) {
-			origFn = fn;
-			fn = function( event ) {
-				// Can use an empty set, since event contains the info
-				jQuery().off( event );
-				return origFn.apply( this, arguments );
-			};
-			// Use same guid so caller can remove using origFn
-			fn.guid = origFn.guid || ( origFn.guid = jQuery.guid++ );
-		}
-		return this.each( function() {
-			jQuery.event.add( this, types, fn, data, selector );
-		});
-	},
-	one: function( types, selector, data, fn ) {
-		return this.on( types, selector, data, fn, 1 );
-	},
-	off: function( types, selector, fn ) {
-		var handleObj, type;
-		if ( types && types.preventDefault && types.handleObj ) {
-			// ( event )  dispatched jQuery.Event
-			handleObj = types.handleObj;
-			jQuery( types.delegateTarget ).off(
-				handleObj.namespace ? handleObj.origType + "." + handleObj.namespace : handleObj.origType,
-				handleObj.selector,
-				handleObj.handler
-			);
-			return this;
-		}
-		if ( typeof types === "object" ) {
-			// ( types-object [, selector] )
-			for ( type in types ) {
-				this.off( type, selector, types[ type ] );
-			}
-			return this;
-		}
-		if ( selector === false || typeof selector === "function" ) {
-			// ( types [, fn] )
-			fn = selector;
-			selector = undefined;
-		}
-		if ( fn === false ) {
-			fn = returnFalse;
-		}
-		return this.each(function() {
-			jQuery.event.remove( this, types, fn, selector );
-		});
-	},
-
-	trigger: function( type, data ) {
-		return this.each(function() {
-			jQuery.event.trigger( type, data, this );
-		});
-	},
-	triggerHandler: function( type, data ) {
-		var elem = this[0];
-		if ( elem ) {
-			return jQuery.event.trigger( type, data, elem, true );
-		}
-	}
-});
-
-
-var
-	rxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,
-	rtagName = /<([\w:]+)/,
-	rhtml = /<|&#?\w+;/,
-	rnoInnerhtml = /<(?:script|style|link)/i,
-	// checked="checked" or checked
-	rchecked = /checked\s*(?:[^=]|=\s*.checked.)/i,
-	rscriptType = /^$|\/(?:java|ecma)script/i,
-	rscriptTypeMasked = /^true\/(.*)/,
-	rcleanScript = /^\s*<!(?:\[CDATA\[|--)|(?:\]\]|--)>\s*$/g,
-
-	// We have to close these tags to support XHTML (#13200)
-	wrapMap = {
-
-		// Support: IE 9
-		option: [ 1, "<select multiple='multiple'>", "</select>" ],
-
-		thead: [ 1, "<table>", "</table>" ],
-		col: [ 2, "<table><colgroup>", "</colgroup></table>" ],
-		tr: [ 2, "<table><tbody>", "</tbody></table>" ],
-		td: [ 3, "<table><tbody><tr>", "</tr></tbody></table>" ],
-
-		_default: [ 0, "", "" ]
-	};
-
-// Support: IE 9
-wrapMap.optgroup = wrapMap.option;
-
-wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead;
-wrapMap.th = wrapMap.td;
-
-// Support: 1.x compatibility
-// Manipulating tables requires a tbody
-function manipulationTarget( elem, content ) {
-	return jQuery.nodeName( elem, "table" ) &&
-		jQuery.nodeName( content.nodeType !== 11 ? content : content.firstChild, "tr" ) ?
-
-		elem.getElementsByTagName("tbody")[0] ||
-			elem.appendChild( elem.ownerDocument.createElement("tbody") ) :
-		elem;
-}
-
-// Replace/restore the type attribute of script elements for safe DOM manipulation
-function disableScript( elem ) {
-	elem.type = (elem.getAttribute("type") !== null) + "/" + elem.type;
-	return elem;
-}
-function restoreScript( elem ) {
-	var match = rscriptTypeMasked.exec( elem.type );
-
-	if ( match ) {
-		elem.type = match[ 1 ];
-	} else {
-		elem.removeAttribute("type");
-	}
-
-	return elem;
-}
-
-// Mark scripts as having already been evaluated
-function setGlobalEval( elems, refElements ) {
-	var i = 0,
-		l = elems.length;
-
-	for ( ; i < l; i++ ) {
-		data_priv.set(
-			elems[ i ], "globalEval", !refElements || data_priv.get( refElements[ i ], "globalEval" )
-		);
-	}
-}
-
-function cloneCopyEvent( src, dest ) {
-	var i, l, type, pdataOld, pdataCur, udataOld, udataCur, events;
-
-	if ( dest.nodeType !== 1 ) {
-		return;
-	}
-
-	// 1. Copy private data: events, handlers, etc.
-	if ( data_priv.hasData( src ) ) {
-		pdataOld = data_priv.access( src );
-		pdataCur = data_priv.set( dest, pdataOld );
-		events = pdataOld.events;
-
-		if ( events ) {
-			delete pdataCur.handle;
-			pdataCur.events = {};
-
-			for ( type in events ) {
-				for ( i = 0, l = events[ type ].length; i < l; i++ ) {
-					jQuery.event.add( dest, type, events[ type ][ i ] );
-				}
-			}
-		}
-	}
-
-	// 2. Copy user data
-	if ( data_user.hasData( src ) ) {
-		udataOld = data_user.access( src );
-		udataCur = jQuery.extend( {}, udataOld );
-
-		data_user.set( dest, udataCur );
-	}
-}
-
-function getAll( context, tag ) {
-	var ret = context.getElementsByTagName ? context.getElementsByTagName( tag || "*" ) :
-			context.querySelectorAll ? context.querySelectorAll( tag || "*" ) :
-			[];
-
-	return tag === undefined || tag && jQuery.nodeName( context, tag ) ?
-		jQuery.merge( [ context ], ret ) :
-		ret;
-}
-
-// Support: IE >= 9
-function fixInput( src, dest ) {
-	var nodeName = dest.nodeName.toLowerCase();
-
-	// Fails to persist the checked state of a cloned checkbox or radio button.
-	if ( nodeName === "input" && rcheckableType.test( src.type ) ) {
-		dest.checked = src.checked;
-
-	// Fails to return the selected option to the default selected state when cloning options
-	} else if ( nodeName === "input" || nodeName === "textarea" ) {
-		dest.defaultValue = src.defaultValue;
-	}
-}
-
-jQuery.extend({
-	clone: function( elem, dataAndEvents, deepDataAndEvents ) {
-		var i, l, srcElements, destElements,
-			clone = elem.cloneNode( true ),
-			inPage = jQuery.contains( elem.ownerDocument, elem );
-
-		// Support: IE >= 9
-		// Fix Cloning issues
-		if ( !support.noCloneChecked && ( elem.nodeType === 1 || elem.nodeType === 11 ) &&
-				!jQuery.isXMLDoc( elem ) ) {
-
-			// We eschew Sizzle here for performance reasons: http://jsperf.com/getall-vs-sizzle/2
-			destElements = getAll( clone );
-			srcElements = getAll( elem );
-
-			for ( i = 0, l = srcElements.length; i < l; i++ ) {
-				fixInput( srcElements[ i ], destElements[ i ] );
-			}
-		}
-
-		// Copy the events from the original to the clone
-		if ( dataAndEvents ) {
-			if ( deepDataAndEvents ) {
-				srcElements = srcElements || getAll( elem );
-				destElements = destElements || getAll( clone );
-
-				for ( i = 0, l = srcElements.length; i < l; i++ ) {
-					cloneCopyEvent( srcElements[ i ], destElements[ i ] );
-				}
-			} else {
-				cloneCopyEvent( elem, clone );
-			}
-		}
-
-		// Preserve script evaluation history
-		destElements = getAll( clone, "script" );
-		if ( destElements.length > 0 ) {
-			setGlobalEval( destElements, !inPage && getAll( elem, "script" ) );
-		}
-
-		// Return the cloned set
-		return clone;
-	},
-
-	buildFragment: function( elems, context, scripts, selection ) {
-		var elem, tmp, tag, wrap, contains, j,
-			fragment = context.createDocumentFragment(),
-			nodes = [],
-			i = 0,
-			l = elems.length;
-
-		for ( ; i < l; i++ ) {
-			elem = elems[ i ];
-
-			if ( elem || elem === 0 ) {
-
-				// Add nodes directly
-				if ( jQuery.type( elem ) === "object" ) {
-					// Support: QtWebKit
-					// jQuery.merge because push.apply(_, arraylike) throws
-					jQuery.merge( nodes, elem.nodeType ? [ elem ] : elem );
-
-				// Convert non-html into a text node
-				} else if ( !rhtml.test( elem ) ) {
-					nodes.push( context.createTextNode( elem ) );
-
-				// Convert html into DOM nodes
-				} else {
-					tmp = tmp || fragment.appendChild( context.createElement("div") );
-
-					// Deserialize a standard representation
-					tag = ( rtagName.exec( elem ) || [ "", "" ] )[ 1 ].toLowerCase();
-					wrap = wrapMap[ tag ] || wrapMap._default;
-					tmp.innerHTML = wrap[ 1 ] + elem.replace( rxhtmlTag, "<$1></$2>" ) + wrap[ 2 ];
-
-					// Descend through wrappers to the right content
-					j = wrap[ 0 ];
-					while ( j-- ) {
-						tmp = tmp.lastChild;
-					}
-
-					// Support: QtWebKit
-					// jQuery.merge because push.apply(_, arraylike) throws
-					jQuery.merge( nodes, tmp.childNodes );
-
-					// Remember the top-level container
-					tmp = fragment.firstChild;
-
-					// Fixes #12346
-					// Support: Webkit, IE
-					tmp.textContent = "";
-				}
-			}
-		}
-
-		// Remove wrapper from fragment
-		fragment.textContent = "";
-
-		i = 0;
-		while ( (elem = nodes[ i++ ]) ) {
-
-			// #4087 - If origin and destination elements are the same, and this is
-			// that element, do not do anything
-			if ( selection && jQuery.inArray( elem, selection ) !== -1 ) {
-				continue;
-			}
-
-			contains = jQuery.contains( elem.ownerDocument, elem );
-
-			// Append to fragment
-			tmp = getAll( fragment.appendChild( elem ), "script" );
-
-			// Preserve script evaluation history
-			if ( contains ) {
-				setGlobalEval( tmp );
-			}
-
-			// Capture executables
-			if ( scripts ) {
-				j = 0;
-				while ( (elem = tmp[ j++ ]) ) {
-					if ( rscriptType.test( elem.type || "" ) ) {
-						scripts.push( elem );
-					}
-				}
-			}
-		}
-
-		return fragment;
-	},
-
-	cleanData: function( elems ) {
-		var data, elem, type, key,
-			special = jQuery.event.special,
-			i = 0;
-
-		for ( ; (elem = elems[ i ]) !== undefined; i++ ) {
-			if ( jQuery.acceptData( elem ) ) {
-				key = elem[ data_priv.expando ];
-
-				if ( key && (data = data_priv.cache[ key ]) ) {
-					if ( data.events ) {
-						for ( type in data.events ) {
-							if ( special[ type ] ) {
-								jQuery.event.remove( elem, type );
-
-							// This is a shortcut to avoid jQuery.event.remove's overhead
-							} else {
-								jQuery.removeEvent( elem, type, data.handle );
-							}
-						}
-					}
-					if ( data_priv.cache[ key ] ) {
-						// Discard any remaining `private` data
-						delete data_priv.cache[ key ];
-					}
-				}
-			}
-			// Discard any remaining `user` data
-			delete data_user.cache[ elem[ data_user.expando ] ];
-		}
-	}
-});
-
-jQuery.fn.extend({
-	text: function( value ) {
-		return access( this, function( value ) {
-			return value === undefined ?
-				jQuery.text( this ) :
-				this.empty().each(function() {
-					if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) {
-						this.textContent = value;
-					}
-				});
-		}, null, value, arguments.length );
-	},
-
-	append: function() {
-		return this.domManip( arguments, function( elem ) {
-			if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) {
-				var target = manipulationTarget( this, elem );
-				target.appendChild( elem );
-			}
-		});
-	},
-
-	prepend: function() {
-		return this.domManip( arguments, function( elem ) {
-			if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) {
-				var target = manipulationTarget( this, elem );
-				target.insertBefore( elem, target.firstChild );
-			}
-		});
-	},
-
-	before: function() {
-		return this.domManip( arguments, function( elem ) {
-			if ( this.parentNode ) {
-				this.parentNode.insertBefore( elem, this );
-			}
-		});
-	},
-
-	after: function() {
-		return this.domManip( arguments, function( elem ) {
-			if ( this.parentNode ) {
-				this.parentNode.insertBefore( elem, this.nextSibling );
-			}
-		});
-	},
-
-	remove: function( selector, keepData /* Internal Use Only */ ) {
-		var elem,
-			elems = selector ? jQuery.filter( selector, this ) : this,
-			i = 0;
-
-		for ( ; (elem = elems[i]) != null; i++ ) {
-			if ( !keepData && elem.nodeType === 1 ) {
-				jQuery.cleanData( getAll( elem ) );
-			}
-
-			if ( elem.parentNode ) {
-				if ( keepData && jQuery.contains( elem.ownerDocument, elem ) ) {
-					setGlobalEval( getAll( elem, "script" ) );
-				}
-				elem.parentNode.removeChild( elem );
-			}
-		}
-
-		return this;
-	},
-
-	empty: function() {
-		var elem,
-			i = 0;
-
-		for ( ; (elem = this[i]) != null; i++ ) {
-			if ( elem.nodeType === 1 ) {
-
-				// Prevent memory leaks
-				jQuery.cleanData( getAll( elem, false ) );
-
-				// Remove any remaining nodes
-				elem.textContent = "";
-			}
-		}
-
-		return this;
-	},
-
-	clone: function( dataAndEvents, deepDataAndEvents ) {
-		dataAndEvents = dataAndEvents == null ? false : dataAndEvents;
-		deepDataAndEvents = deepDataAndEvents == null ? dataAndEvents : deepDataAndEvents;
-
-		return this.map(function() {
-			return jQuery.clone( this, dataAndEvents, deepDataAndEvents );
-		});
-	},
-
-	html: function( value ) {
-		return access( this, function( value ) {
-			var elem = this[ 0 ] || {},
-				i = 0,
-				l = this.length;
-
-			if ( value === undefined && elem.nodeType === 1 ) {
-				return elem.innerHTML;
-			}
-
-			// See if we can take a shortcut and just use innerHTML
-			if ( typeof value === "string" && !rnoInnerhtml.test( value ) &&
-				!wrapMap[ ( rtagName.exec( value ) || [ "", "" ] )[ 1 ].toLowerCase() ] ) {
-
-				value = value.replace( rxhtmlTag, "<$1></$2>" );
-
-				try {
-					for ( ; i < l; i++ ) {
-						elem = this[ i ] || {};
-
-						// Remove element nodes and prevent memory leaks
-						if ( elem.nodeType === 1 ) {
-							jQuery.cleanData( getAll( elem, false ) );
-							elem.innerHTML = value;
-						}
-					}
-
-					elem = 0;
-
-				// If using innerHTML throws an exception, use the fallback method
-				} catch( e ) {}
-			}
-
-			if ( elem ) {
-				this.empty().append( value );
-			}
-		}, null, value, arguments.length );
-	},
-
-	replaceWith: function() {
-		var arg = arguments[ 0 ];
-
-		// Make the changes, replacing each context element with the new content
-		this.domManip( arguments, function( elem ) {
-			arg = this.parentNode;
-
-			jQuery.cleanData( getAll( this ) );
-
-			if ( arg ) {
-				arg.replaceChild( elem, this );
-			}
-		});
-
-		// Force removal if there was no new content (e.g., from empty arguments)
-		return arg && (arg.length || arg.nodeType) ? this : this.remove();
-	},
-
-	detach: function( selector ) {
-		return this.remove( selector, true );
-	},
-
-	domManip: function( args, callback ) {
-
-		// Flatten any nested arrays
-		args = concat.apply( [], args );
-
-		var fragment, first, scripts, hasScripts, node, doc,
-			i = 0,
-			l = this.length,
-			set = this,
-			iNoClone = l - 1,
-			value = args[ 0 ],
-			isFunction = jQuery.isFunction( value );
-
-		// We can't cloneNode fragments that contain checked, in WebKit
-		if ( isFunction ||
-				( l > 1 && typeof value === "string" &&
-					!support.checkClone && rchecked.test( value ) ) ) {
-			return this.each(function( index ) {
-				var self = set.eq( index );
-				if ( isFunction ) {
-					args[ 0 ] = value.call( this, index, self.html() );
-				}
-				self.domManip( args, callback );
-			});
-		}
-
-		if ( l ) {
-			fragment = jQuery.buildFragment( args, this[ 0 ].ownerDocument, false, this );
-			first = fragment.firstChild;
-
-			if ( fragment.childNodes.length === 1 ) {
-				fragment = first;
-			}
-
-			if ( first ) {
-				scripts = jQuery.map( getAll( fragment, "script" ), disableScript );
-				hasScripts = scripts.length;
-
-				// Use the original fragment for the last item instead of the first because it can end up
-				// being emptied incorrectly in certain situations (#8070).
-				for ( ; i < l; i++ ) {
-					node = fragment;
-
-					if ( i !== iNoClone ) {
-						node = jQuery.clone( node, true, true );
-
-						// Keep references to cloned scripts for later restoration
-						if ( hasScripts ) {
-							// Support: QtWebKit
-							// jQuery.merge because push.apply(_, arraylike) throws
-							jQuery.merge( scripts, getAll( node, "script" ) );
-						}
-					}
-
-					callback.call( this[ i ], node, i );
-				}
-
-				if ( hasScripts ) {
-					doc = scripts[ scripts.length - 1 ].ownerDocument;
-
-					// Reenable scripts
-					jQuery.map( scripts, restoreScript );
-
-					// Evaluate executable scripts on first document insertion
-					for ( i = 0; i < hasScripts; i++ ) {
-						node = scripts[ i ];
-						if ( rscriptType.test( node.type || "" ) &&
-							!data_priv.access( node, "globalEval" ) && jQuery.contains( doc, node ) ) {
-
-							if ( node.src ) {
-								// Optional AJAX dependency, but won't run scripts if not present
-								if ( jQuery._evalUrl ) {
-									jQuery._evalUrl( node.src );
-								}
-							} else {
-								jQuery.globalEval( node.textContent.replace( rcleanScript, "" ) );
-							}
-						}
-					}
-				}
-			}
-		}
-
-		return this;
-	}
-});
-
-jQuery.each({
-	appendTo: "append",
-	prependTo: "prepend",
-	insertBefore: "before",
-	insertAfter: "after",
-	replaceAll: "replaceWith"
-}, function( name, original ) {
-	jQuery.fn[ name ] = function( selector ) {
-		var elems,
-			ret = [],
-			insert = jQuery( selector ),
-			last = insert.length - 1,
-			i = 0;
-
-		for ( ; i <= last; i++ ) {
-			elems = i === last ? this : this.clone( true );
-			jQuery( insert[ i ] )[ original ]( elems );
-
-			// Support: QtWebKit
-			// .get() because push.apply(_, arraylike) throws
-			push.apply( ret, elems.get() );
-		}
-
-		return this.pushStack( ret );
-	};
-});
-
-
-var iframe,
-	elemdisplay = {};
-
-/**
- * Retrieve the actual display of a element
- * @param {String} name nodeName of the element
- * @param {Object} doc Document object
- */
-// Called only from within defaultDisplay
-function actualDisplay( name, doc ) {
-	var style,
-		elem = jQuery( doc.createElement( name ) ).appendTo( doc.body ),
-
-		// getDefaultComputedStyle might be reliably used only on attached element
-		display = window.getDefaultComputedStyle && ( style = window.getDefaultComputedStyle( elem[ 0 ] ) ) ?
-
-			// Use of this method is a temporary fix (more like optmization) until something better comes along,
-			// since it was removed from specification and supported only in FF
-			style.display : jQuery.css( elem[ 0 ], "display" );
-
-	// We don't have any data stored on the element,
-	// so use "detach" method as fast way to get rid of the element
-	elem.detach();
-
-	return display;
-}
-
-/**
- * Try to determine the default display value of an element
- * @param {String} nodeName
- */
-function defaultDisplay( nodeName ) {
-	var doc = document,
-		display = elemdisplay[ nodeName ];
-
-	if ( !display ) {
-		display = actualDisplay( nodeName, doc );
-
-		// If the simple way fails, read from inside an iframe
-		if ( display === "none" || !display ) {
-
-			// Use the already-created iframe if possible
-			iframe = (iframe || jQuery( "<iframe frameborder='0' width='0' height='0'/>" )).appendTo( doc.documentElement );
-
-			// Always write a new HTML skeleton so Webkit and Firefox don't choke on reuse
-			doc = iframe[ 0 ].contentDocument;
-
-			// Support: IE
-			doc.write();
-			doc.close();
-
-			display = actualDisplay( nodeName, doc );
-			iframe.detach();
-		}
-
-		// Store the correct default display
-		elemdisplay[ nodeName ] = display;
-	}
-
-	return display;
-}
-var rmargin = (/^margin/);
-
-var rnumnonpx = new RegExp( "^(" + pnum + ")(?!px)[a-z%]+$", "i" );
-
-var getStyles = function( elem ) {
-		return elem.ownerDocument.defaultView.getComputedStyle( elem, null );
-	};
-
-
-
-function curCSS( elem, name, computed ) {
-	var width, minWidth, maxWidth, ret,
-		style = elem.style;
-
-	computed = computed || getStyles( elem );
-
-	// Support: IE9
-	// getPropertyValue is only needed for .css('filter') in IE9, see #12537
-	if ( computed ) {
-		ret = computed.getPropertyValue( name ) || computed[ name ];
-	}
-
-	if ( computed ) {
-
-		if ( ret === "" && !jQuery.contains( elem.ownerDocument, elem ) ) {
-			ret = jQuery.style( elem, name );
-		}
-
-		// Support: iOS < 6
-		// A tribute to the "awesome hack by Dean Edwards"
-		// iOS < 6 (at least) returns percentage for a larger set of values, but width seems to be reliably pixels
-		// this is against the CSSOM draft spec: http://dev.w3.org/csswg/cssom/#resolved-values
-		if ( rnumnonpx.test( ret ) && rmargin.test( name ) ) {
-
-			// Remember the original values
-			width = style.width;
-			minWidth = style.minWidth;
-			maxWidth = style.maxWidth;
-
-			// Put in the new values to get a computed value out
-			style.minWidth = style.maxWidth = style.width = ret;
-			ret = computed.width;
-
-			// Revert the changed values
-			style.width = width;
-			style.minWidth = minWidth;
-			style.maxWidth = maxWidth;
-		}
-	}
-
-	return ret !== undefined ?
-		// Support: IE
-		// IE returns zIndex value as an integer.
-		ret + "" :
-		ret;
-}
-
-
-function addGetHookIf( conditionFn, hookFn ) {
-	// Define the hook, we'll check on the first run if it's really needed.
-	return {
-		get: function() {
-			if ( conditionFn() ) {
-				// Hook not needed (or it's not possible to use it due to missing dependency),
-				// remove it.
-				// Since there are no other hooks for marginRight, remove the whole object.
-				delete this.get;
-				return;
-			}
-
-			// Hook needed; redefine it so that the support test is not executed again.
-
-			return (this.get = hookFn).apply( this, arguments );
-		}
-	};
-}
-
-
-(function() {
-	var pixelPositionVal, boxSizingReliableVal,
-		docElem = document.documentElement,
-		container = document.createElement( "div" ),
-		div = document.createElement( "div" );
-
-	if ( !div.style ) {
-		return;
-	}
-
-	div.style.backgroundClip = "content-box";
-	div.cloneNode( true ).style.backgroundClip = "";
-	support.clearCloneStyle = div.style.backgroundClip === "content-box";
-
-	container.style.cssText = "border:0;width:0;height:0;top:0;left:-9999px;margin-top:1px;" +
-		"position:absolute";
-	container.appendChild( div );
-
-	// Executing both pixelPosition & boxSizingReliable tests require only one layout
-	// so they're executed at the same time to save the second computation.
-	function computePixelPositionAndBoxSizingReliable() {
-		div.style.cssText =
-			// Support: Firefox<29, Android 2.3
-			// Vendor-prefix box-sizing
-			"-webkit-box-sizing:border-box;-moz-box-sizing:border-box;" +
-			"box-sizing:border-box;display:block;margin-top:1%;top:1%;" +
-			"border:1px;padding:1px;width:4px;position:absolute";
-		div.innerHTML = "";
-		docElem.appendChild( container );
-
-		var divStyle = window.getComputedStyle( div, null );
-		pixelPositionVal = divStyle.top !== "1%";
-		boxSizingReliableVal = divStyle.width === "4px";
-
-		docElem.removeChild( container );
-	}
-
-	// Support: node.js jsdom
-	// Don't assume that getComputedStyle is a property of the global object
-	if ( window.getComputedStyle ) {
-		jQuery.extend( support, {
-			pixelPosition: function() {
-				// This test is executed only once but we still do memoizing
-				// since we can use the boxSizingReliable pre-computing.
-				// No need to check if the test was already performed, though.
-				computePixelPositionAndBoxSizingReliable();
-				return pixelPositionVal;
-			},
-			boxSizingReliable: function() {
-				if ( boxSizingReliableVal == null ) {
-					computePixelPositionAndBoxSizingReliable();
-				}
-				return boxSizingReliableVal;
-			},
-			reliableMarginRight: function() {
-				// Support: Android 2.3
-				// Check if div with explicit width and no margin-right incorrectly
-				// gets computed margin-right based on width of container. (#3333)
-				// WebKit Bug 13343 - getComputedStyle returns wrong value for margin-right
-				// This support function is only executed once so no memoizing is needed.
-				var ret,
-					marginDiv = div.appendChild( document.createElement( "div" ) );
-
-				// Reset CSS: box-sizing; display; margin; border; padding
-				marginDiv.style.cssText = div.style.cssText =
-					// Support: Firefox<29, Android 2.3
-					// Vendor-prefix box-sizing
-					"-webkit-box-sizing:content-box;-moz-box-sizing:content-box;" +
-					"box-sizing:content-box;display:block;margin:0;border:0;padding:0";
-				marginDiv.style.marginRight = marginDiv.style.width = "0";
-				div.style.width = "1px";
-				docElem.appendChild( container );
-
-				ret = !parseFloat( window.getComputedStyle( marginDiv, null ).marginRight );
-
-				docElem.removeChild( container );
-
-				return ret;
-			}
-		});
-	}
-})();
-
-
-// A method for quickly swapping in/out CSS properties to get correct calculations.
-jQuery.swap = function( elem, options, callback, args ) {
-	var ret, name,
-		old = {};
-
-	// Remember the old values, and insert the new ones
-	for ( name in options ) {
-		old[ name ] = elem.style[ name ];
-		elem.style[ name ] = options[ name ];
-	}
-
-	ret = callback.apply( elem, args || [] );
-
-	// Revert the old values
-	for ( name in options ) {
-		elem.style[ name ] = old[ name ];
-	}
-
-	return ret;
-};
-
-
-var
-	// swappable if display is none or starts with table except "table", "table-cell", or "table-caption"
-	// see here for display values: https://developer.mozilla.org/en-US/docs/CSS/display
-	rdisplayswap = /^(none|table(?!-c[ea]).+)/,
-	rnumsplit = new RegExp( "^(" + pnum + ")(.*)$", "i" ),
-	rrelNum = new RegExp( "^([+-])=(" + pnum + ")", "i" ),
-
-	cssShow = { position: "absolute", visibility: "hidden", display: "block" },
-	cssNormalTransform = {
-		letterSpacing: "0",
-		fontWeight: "400"
-	},
-
-	cssPrefixes = [ "Webkit", "O", "Moz", "ms" ];
-
-// return a css property mapped to a potentially vendor prefixed property
-function vendorPropName( style, name ) {
-
-	// shortcut for names that are not vendor prefixed
-	if ( name in style ) {
-		return name;
-	}
-
-	// check for vendor prefixed names
-	var capName = name[0].toUpperCase() + name.slice(1),
-		origName = name,
-		i = cssPrefixes.length;
-
-	while ( i-- ) {
-		name = cssPrefixes[ i ] + capName;
-		if ( name in style ) {
-			return name;
-		}
-	}
-
-	return origName;
-}
-
-function setPositiveNumber( elem, value, subtract ) {
-	var matches = rnumsplit.exec( value );
-	return matches ?
-		// Guard against undefined "subtract", e.g., when used as in cssHooks
-		Math.max( 0, matches[ 1 ] - ( subtract || 0 ) ) + ( matches[ 2 ] || "px" ) :
-		value;
-}
-
-function augmentWidthOrHeight( elem, name, extra, isBorderBox, styles ) {
-	var i = extra === ( isBorderBox ? "border" : "content" ) ?
-		// If we already have the right measurement, avoid augmentation
-		4 :
-		// Otherwise initialize for horizontal or vertical properties
-		name === "width" ? 1 : 0,
-
-		val = 0;
-
-	for ( ; i < 4; i += 2 ) {
-		// both box models exclude margin, so add it if we want it
-		if ( extra === "margin" ) {
-			val += jQuery.css( elem, extra + cssExpand[ i ], true, styles );
-		}
-
-		if ( isBorderBox ) {
-			// border-box includes padding, so remove it if we want content
-			if ( extra === "content" ) {
-				val -= jQuery.css( elem, "padding" + cssExpand[ i ], true, styles );
-			}
-
-			// at this point, extra isn't border nor margin, so remove border
-			if ( extra !== "margin" ) {
-				val -= jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles );
-			}
-		} else {
-			// at this point, extra isn't content, so add padding
-			val += jQuery.css( elem, "padding" + cssExpand[ i ], true, styles );
-
-			// at this point, extra isn't content nor padding, so add border
-			if ( extra !== "padding" ) {
-				val += jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles );
-			}
-		}
-	}
-
-	return val;
-}
-
-function getWidthOrHeight( elem, name, extra ) {
-
-	// Start with offset property, which is equivalent to the border-box value
-	var valueIsBorderBox = true,
-		val = name === "width" ? elem.offsetWidth : elem.offsetHeight,
-		styles = getStyles( elem ),
-		isBorderBox = jQuery.css( elem, "boxSizing", false, styles ) === "border-box";
-
-	// some non-html elements return undefined for offsetWidth, so check for null/undefined
-	// svg - https://bugzilla.mozilla.org/show_bug.cgi?id=649285
-	// MathML - https://bugzilla.mozilla.org/show_bug.cgi?id=491668
-	if ( val <= 0 || val == null ) {
-		// Fall back to computed then uncomputed css if necessary
-		val = curCSS( elem, name, styles );
-		if ( val < 0 || val == null ) {
-			val = elem.style[ name ];
-		}
-
-		// Computed unit is not pixels. Stop here and return.
-		if ( rnumnonpx.test(val) ) {
-			return val;
-		}
-
-		// we need the check for style in case a browser which returns unreliable values
-		// for getComputedStyle silently falls back to the reliable elem.style
-		valueIsBorderBox = isBorderBox &&
-			( support.boxSizingReliable() || val === elem.style[ name ] );
-
-		// Normalize "", auto, and prepare for extra
-		val = parseFloat( val ) || 0;
-	}
-
-	// use the active box-sizing model to add/subtract irrelevant styles
-	return ( val +
-		augmentWidthOrHeight(
-			elem,
-			name,
-			extra || ( isBorderBox ? "border" : "content" ),
-			valueIsBorderBox,
-			styles
-		)
-	) + "px";
-}
-
-function showHide( elements, show ) {
-	var display, elem, hidden,
-		values = [],
-		index = 0,
-		length = elements.length;
-
-	for ( ; index < length; index++ ) {
-		elem = elements[ index ];
-		if ( !elem.style ) {
-			continue;
-		}
-
-		values[ index ] = data_priv.get( elem, "olddisplay" );
-		display = elem.style.display;
-		if ( show ) {
-			// Reset the inline display of this element to learn if it is
-			// being hidden by cascaded rules or not
-			if ( !values[ index ] && display === "none" ) {
-				elem.style.display = "";
-			}
-
-			// Set elements which have been overridden with display: none
-			// in a stylesheet to whatever the default browser style is
-			// for such an element
-			if ( elem.style.display === "" && isHidden( elem ) ) {
-				values[ index ] = data_priv.access( elem, "olddisplay", defaultDisplay(elem.nodeName) );
-			}
-		} else {
-			hidden = isHidden( elem );
-
-			if ( display !== "none" || !hidden ) {
-				data_priv.set( elem, "olddisplay", hidden ? display : jQuery.css( elem, "display" ) );
-			}
-		}
-	}
-
-	// Set the display of most of the elements in a second loop
-	// to avoid the constant reflow
-	for ( index = 0; index < length; index++ ) {
-		elem = elements[ index ];
-		if ( !elem.style ) {
-			continue;
-		}
-		if ( !show || elem.style.display === "none" || elem.style.display === "" ) {
-			elem.style.display = show ? values[ index ] || "" : "none";
-		}
-	}
-
-	return elements;
-}
-
-jQuery.extend({
-	// Add in style property hooks for overriding the default
-	// behavior of getting and setting a style property
-	cssHooks: {
-		opacity: {
-			get: function( elem, computed ) {
-				if ( computed ) {
-					// We should always get a number back from opacity
-					var ret = curCSS( elem, "opacity" );
-					return ret === "" ? "1" : ret;
-				}
-			}
-		}
-	},
-
-	// Don't automatically add "px" to these possibly-unitless properties
-	cssNumber: {
-		"columnCount": true,
-		"fillOpacity": true,
-		"flexGrow": true,
-		"flexShrink": true,
-		"fontWeight": true,
-		"lineHeight": true,
-		"opacity": true,
-		"order": true,
-		"orphans": true,
-		"widows": true,
-		"zIndex": true,
-		"zoom": true
-	},
-
-	// Add in properties whose names you wish to fix before
-	// setting or getting the value
-	cssProps: {
-		// normalize float css property
-		"float": "cssFloat"
-	},
-
-	// Get and set the style property on a DOM Node
-	style: function( elem, name, value, extra ) {
-		// Don't set styles on text and comment nodes
-		if ( !elem || elem.nodeType === 3 || elem.nodeType === 8 || !elem.style ) {
-			return;
-		}
-
-		// Make sure that we're working with the right name
-		var ret, type, hooks,
-			origName = jQuery.camelCase( name ),
-			style = elem.style;
-
-		name = jQuery.cssProps[ origName ] || ( jQuery.cssProps[ origName ] = vendorPropName( style, origName ) );
-
-		// gets hook for the prefixed version
-		// followed by the unprefixed version
-		hooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ];
-
-		// Check if we're setting a value
-		if ( value !== undefined ) {
-			type = typeof value;
-
-			// convert relative number strings (+= or -=) to relative numbers. #7345
-			if ( type === "string" && (ret = rrelNum.exec( value )) ) {
-				value = ( ret[1] + 1 ) * ret[2] + parseFloat( jQuery.css( elem, name ) );
-				// Fixes bug #9237
-				type = "number";
-			}
-
-			// Make sure that null and NaN values aren't set. See: #7116
-			if ( value == null || value !== value ) {
-				return;
-			}
-
-			// If a number was passed in, add 'px' to the (except for certain CSS properties)
-			if ( type === "number" && !jQuery.cssNumber[ origName ] ) {
-				value += "px";
-			}
-
-			// Fixes #8908, it can be done more correctly by specifying setters in cssHooks,
-			// but it would mean to define eight (for every problematic property) identical functions
-			if ( !support.clearCloneStyle && value === "" && name.indexOf( "background" ) === 0 ) {
-				style[ name ] = "inherit";
-			}
-
-			// If a hook was provided, use that value, otherwise just set the specified value
-			if ( !hooks || !("set" in hooks) || (value = hooks.set( elem, value, extra )) !== undefined ) {
-				style[ name ] = value;
-			}
-
-		} else {
-			// If a hook was provided get the non-computed value from there
-			if ( hooks && "get" in hooks && (ret = hooks.get( elem, false, extra )) !== undefined ) {
-				return ret;
-			}
-
-			// Otherwise just get the value from the style object
-			return style[ name ];
-		}
-	},
-
-	css: function( elem, name, extra, styles ) {
-		var val, num, hooks,
-			origName = jQuery.camelCase( name );
-
-		// Make sure that we're working with the right name
-		name = jQuery.cssProps[ origName ] || ( jQuery.cssProps[ origName ] = vendorPropName( elem.style, origName ) );
-
-		// gets hook for the prefixed version
-		// followed by the unprefixed version
-		hooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ];
-
-		// If a hook was provided get the computed value from there
-		if ( hooks && "get" in hooks ) {
-			val = hooks.get( elem, true, extra );
-		}
-
-		// Otherwise, if a way to get the computed value exists, use that
-		if ( val === undefined ) {
-			val = curCSS( elem, name, styles );
-		}
-
-		//convert "normal" to computed value
-		if ( val === "normal" && name in cssNormalTransform ) {
-			val = cssNormalTransform[ name ];
-		}
-
-		// Return, converting to number if forced or a qualifier was provided and val looks numeric
-		if ( extra === "" || extra ) {
-			num = parseFloat( val );
-			return extra === true || jQuery.isNumeric( num ) ? num || 0 : val;
-		}
-		return val;
-	}
-});
-
-jQuery.each([ "height", "width" ], function( i, name ) {
-	jQuery.cssHooks[ name ] = {
-		get: function( elem, computed, extra ) {
-			if ( computed ) {
-				// certain elements can have dimension info if we invisibly show them
-				// however, it must have a current display style that would benefit from this
-				return rdisplayswap.test( jQuery.css( elem, "display" ) ) && elem.offsetWidth === 0 ?
-					jQuery.swap( elem, cssShow, function() {
-						return getWidthOrHeight( elem, name, extra );
-					}) :
-					getWidthOrHeight( elem, name, extra );
-			}
-		},
-
-		set: function( elem, value, extra ) {
-			var styles = extra && getStyles( elem );
-			return setPositiveNumber( elem, value, extra ?
-				augmentWidthOrHeight(
-					elem,
-					name,
-					extra,
-					jQuery.css( elem, "boxSizing", false, styles ) === "border-box",
-					styles
-				) : 0
-			);
-		}
-	};
-});
-
-// Support: Android 2.3
-jQuery.cssHooks.marginRight = addGetHookIf( support.reliableMarginRight,
-	function( elem, computed ) {
-		if ( computed ) {
-			// WebKit Bug 13343 - getComputedStyle returns wrong value for margin-right
-			// Work around by temporarily setting element display to inline-block
-			return jQuery.swap( elem, { "display": "inline-block" },
-				curCSS, [ elem, "marginRight" ] );
-		}
-	}
-);
-
-// These hooks are used by animate to expand properties
-jQuery.each({
-	margin: "",
-	padding: "",
-	border: "Width"
-}, function( prefix, suffix ) {
-	jQuery.cssHooks[ prefix + suffix ] = {
-		expand: function( value ) {
-			var i = 0,
-				expanded = {},
-
-				// assumes a single number if not a string
-				parts = typeof value === "string" ? value.split(" ") : [ value ];
-
-			for ( ; i < 4; i++ ) {
-				expanded[ prefix + cssExpand[ i ] + suffix ] =
-					parts[ i ] || parts[ i - 2 ] || parts[ 0 ];
-			}
-
-			return expanded;
-		}
-	};
-
-	if ( !rmargin.test( prefix ) ) {
-		jQuery.cssHooks[ prefix + suffix ].set = setPositiveNumber;
-	}
-});
-
-jQuery.fn.extend({
-	css: function( name, value ) {
-		return access( this, function( elem, name, value ) {
-			var styles, len,
-				map = {},
-				i = 0;
-
-			if ( jQuery.isArray( name ) ) {
-				styles = getStyles( elem );
-				len = name.length;
-
-				for ( ; i < len; i++ ) {
-					map[ name[ i ] ] = jQuery.css( elem, name[ i ], false, styles );
-				}
-
-				return map;
-			}
-
-			return value !== undefined ?
-				jQuery.style( elem, name, value ) :
-				jQuery.css( elem, name );
-		}, name, value, arguments.length > 1 );
-	},
-	show: function() {
-		return showHide( this, true );
-	},
-	hide: function() {
-		return showHide( this );
-	},
-	toggle: function( state ) {
-		if ( typeof state === "boolean" ) {
-			return state ? this.show() : this.hide();
-		}
-
-		return this.each(function() {
-			if ( isHidden( this ) ) {
-				jQuery( this ).show();
-			} else {
-				jQuery( this ).hide();
-			}
-		});
-	}
-});
-
-
-function Tween( elem, options, prop, end, easing ) {
-	return new Tween.prototype.init( elem, options, prop, end, easing );
-}
-jQuery.Tween = Tween;
-
-Tween.prototype = {
-	constructor: Tween,
-	init: function( elem, options, prop, end, easing, unit ) {
-		this.elem = elem;
-		this.prop = prop;
-		this.easing = easing || "swing";
-		this.options = options;
-		this.start = this.now = this.cur();
-		this.end = end;
-		this.unit = unit || ( jQuery.cssNumber[ prop ] ? "" : "px" );
-	},
-	cur: function() {
-		var hooks = Tween.propHooks[ this.prop ];
-
-		return hooks && hooks.get ?
-			hooks.get( this ) :
-			Tween.propHooks._default.get( this );
-	},
-	run: function( percent ) {
-		var eased,
-			hooks = Tween.propHooks[ this.prop ];
-
-		if ( this.options.duration ) {
-			this.pos = eased = jQuery.easing[ this.easing ](
-				percent, this.options.duration * percent, 0, 1, this.options.duration
-			);
-		} else {
-			this.pos = eased = percent;
-		}
-		this.now = ( this.end - this.start ) * eased + this.start;
-
-		if ( this.options.step ) {
-			this.options.step.call( this.elem, this.now, this );
-		}
-
-		if ( hooks && hooks.set ) {
-			hooks.set( this );
-		} else {
-			Tween.propHooks._default.set( this );
-		}
-		return this;
-	}
-};
-
-Tween.prototype.init.prototype = Tween.prototype;
-
-Tween.propHooks = {
-	_default: {
-		get: function( tween ) {
-			var result;
-
-			if ( tween.elem[ tween.prop ] != null &&
-				(!tween.elem.style || tween.elem.style[ tween.prop ] == null) ) {
-				return tween.elem[ tween.prop ];
-			}
-
-			// passing an empty string as a 3rd parameter to .css will automatically
-			// attempt a parseFloat and fallback to a string if the parse fails
-			// so, simple values such as "10px" are parsed to Float.
-			// complex values such as "rotate(1rad)" are returned as is.
-			result = jQuery.css( tween.elem, tween.prop, "" );
-			// Empty strings, null, undefined and "auto" are converted to 0.
-			return !result || result === "auto" ? 0 : result;
-		},
-		set: function( tween ) {
-			// use step hook for back compat - use cssHook if its there - use .style if its
-			// available and use plain properties where available
-			if ( jQuery.fx.step[ tween.prop ] ) {
-				jQuery.fx.step[ tween.prop ]( tween );
-			} else if ( tween.elem.style && ( tween.elem.style[ jQuery.cssProps[ tween.prop ] ] != null || jQuery.cssHooks[ tween.prop ] ) ) {
-				jQuery.style( tween.elem, tween.prop, tween.now + tween.unit );
-			} else {
-				tween.elem[ tween.prop ] = tween.now;
-			}
-		}
-	}
-};
-
-// Support: IE9
-// Panic based approach to setting things on disconnected nodes
-
-Tween.propHooks.scrollTop = Tween.propHooks.scrollLeft = {
-	set: function( tween ) {
-		if ( tween.elem.nodeType && tween.elem.parentNode ) {
-			tween.elem[ tween.prop ] = tween.now;
-		}
-	}
-};
-
-jQuery.easing = {
-	linear: function( p ) {
-		return p;
-	},
-	swing: function( p ) {
-		return 0.5 - Math.cos( p * Math.PI ) / 2;
-	}
-};
-
-jQuery.fx = Tween.prototype.init;
-
-// Back Compat <1.8 extension point
-jQuery.fx.step = {};
-
-
-
-
-var
-	fxNow, timerId,
-	rfxtypes = /^(?:toggle|show|hide)$/,
-	rfxnum = new RegExp( "^(?:([+-])=|)(" + pnum + ")([a-z%]*)$", "i" ),
-	rrun = /queueHooks$/,
-	animationPrefilters = [ defaultPrefilter ],
-	tweeners = {
-		"*": [ function( prop, value ) {
-			var tween = this.createTween( prop, value ),
-				target = tween.cur(),
-				parts = rfxnum.exec( value ),
-				unit = parts && parts[ 3 ] || ( jQuery.cssNumber[ prop ] ? "" : "px" ),
-
-				// Starting value computation is required for potential unit mismatches
-				start = ( jQuery.cssNumber[ prop ] || unit !== "px" && +target ) &&
-					rfxnum.exec( jQuery.css( tween.elem, prop ) ),
-				scale = 1,
-				maxIterations = 20;
-
-			if ( start && start[ 3 ] !== unit ) {
-				// Trust units reported by jQuery.css
-				unit = unit || start[ 3 ];
-
-				// Make sure we update the tween properties later on
-				parts = parts || [];
-
-				// Iteratively approximate from a nonzero starting point
-				start = +target || 1;
-
-				do {
-					// If previous iteration zeroed out, double until we get *something*
-					// Use a string for doubling factor so we don't accidentally see scale as unchanged below
-					scale = scale || ".5";
-
-					// Adjust and apply
-					start = start / scale;
-					jQuery.style( tween.elem, prop, start + unit );
-
-				// Update scale, tolerating zero or NaN from tween.cur()
-				// And breaking the loop if scale is unchanged or perfect, or if we've just had enough
-				} while ( scale !== (scale = tween.cur() / target) && scale !== 1 && --maxIterations );
-			}
-
-			// Update tween properties
-			if ( parts ) {
-				start = tween.start = +start || +target || 0;
-				tween.unit = unit;
-				// If a +=/-= token was provided, we're doing a relative animation
-				tween.end = parts[ 1 ] ?
-					start + ( parts[ 1 ] + 1 ) * parts[ 2 ] :
-					+parts[ 2 ];
-			}
-
-			return tween;
-		} ]
-	};
-
-// Animations created synchronously will run synchronously
-function createFxNow() {
-	setTimeout(function() {
-		fxNow = undefined;
-	});
-	return ( fxNow = jQuery.now() );
-}
-
-// Generate parameters to create a standard animation
-function genFx( type, includeWidth ) {
-	var which,
-		i = 0,
-		attrs = { height: type };
-
-	// if we include width, step value is 1 to do all cssExpand values,
-	// if we don't include width, step value is 2 to skip over Left and Right
-	includeWidth = includeWidth ? 1 : 0;
-	for ( ; i < 4 ; i += 2 - includeWidth ) {
-		which = cssExpand[ i ];
-		attrs[ "margin" + which ] = attrs[ "padding" + which ] = type;
-	}
-
-	if ( includeWidth ) {
-		attrs.opacity = attrs.width = type;
-	}
-
-	return attrs;
-}
-
-function createTween( value, prop, animation ) {
-	var tween,
-		collection = ( tweeners[ prop ] || [] ).concat( tweeners[ "*" ] ),
-		index = 0,
-		length = collection.length;
-	for ( ; index < length; index++ ) {
-		if ( (tween = collection[ index ].call( animation, prop, value )) ) {
-
-			// we're done with this property
-			return tween;
-		}
-	}
-}
-
-function defaultPrefilter( elem, props, opts ) {
-	/* jshint validthis: true */
-	var prop, value, toggle, tween, hooks, oldfire, display, checkDisplay,
-		anim = this,
-		orig = {},
-		style = elem.style,
-		hidden = elem.nodeType && isHidden( elem ),
-		dataShow = data_priv.get( elem, "fxshow" );
-
-	// handle queue: false promises
-	if ( !opts.queue ) {
-		hooks = jQuery._queueHooks( elem, "fx" );
-		if ( hooks.unqueued == null ) {
-			hooks.unqueued = 0;
-			oldfire = hooks.empty.fire;
-			hooks.empty.fire = function() {
-				if ( !hooks.unqueued ) {
-					oldfire();
-				}
-			};
-		}
-		hooks.unqueued++;
-
-		anim.always(function() {
-			// doing this makes sure that the complete handler will be called
-			// before this completes
-			anim.always(function() {
-				hooks.unqueued--;
-				if ( !jQuery.queue( elem, "fx" ).length ) {
-					hooks.empty.fire();
-				}
-			});
-		});
-	}
-
-	// height/width overflow pass
-	if ( elem.nodeType === 1 && ( "height" in props || "width" in props ) ) {
-		// Make sure that nothing sneaks out
-		// Record all 3 overflow attributes because IE9-10 do not
-		// change the overflow attribute when overflowX and
-		// overflowY are set to the same value
-		opts.overflow = [ style.overflow, style.overflowX, style.overflowY ];
-
-		// Set display property to inline-block for height/width
-		// animations on inline elements that are having width/height animated
-		display = jQuery.css( elem, "display" );
-
-		// Test default display if display is currently "none"
-		checkDisplay = display === "none" ?
-			data_priv.get( elem, "olddisplay" ) || defaultDisplay( elem.nodeName ) : display;
-
-		if ( checkDisplay === "inline" && jQuery.css( elem, "float" ) === "none" ) {
-			style.display = "inline-block";
-		}
-	}
-
-	if ( opts.overflow ) {
-		style.overflow = "hidden";
-		anim.always(function() {
-			style.overflow = opts.overflow[ 0 ];
-			style.overflowX = opts.overflow[ 1 ];
-			style.overflowY = opts.overflow[ 2 ];
-		});
-	}
-
-	// show/hide pass
-	for ( prop in props ) {
-		value = props[ prop ];
-		if ( rfxtypes.exec( value ) ) {
-			delete props[ prop ];
-			toggle = toggle || value === "toggle";
-			if ( value === ( hidden ? "hide" : "show" ) ) {
-
-				// If there is dataShow left over from a stopped hide or show and we are going to proceed with show, we should pretend to be hidden
-				if ( value === "show" && dataShow && dataShow[ prop ] !== undefined ) {
-					hidden = true;
-				} else {
-					continue;
-				}
-			}
-			orig[ prop ] = dataShow && dataShow[ prop ] || jQuery.style( elem, prop );
-
-		// Any non-fx value stops us from restoring the original display value
-		} else {
-			display = undefined;
-		}
-	}
-
-	if ( !jQuery.isEmptyObject( orig ) ) {
-		if ( dataShow ) {
-			if ( "hidden" in dataShow ) {
-				hidden = dataShow.hidden;
-			}
-		} else {
-			dataShow = data_priv.access( elem, "fxshow", {} );
-		}
-
-		// store state if its toggle - enables .stop().toggle() to "reverse"
-		if ( toggle ) {
-			dataShow.hidden = !hidden;
-		}
-		if ( hidden ) {
-			jQuery( elem ).show();
-		} else {
-			anim.done(function() {
-				jQuery( elem ).hide();
-			});
-		}
-		anim.done(function() {
-			var prop;
-
-			data_priv.remove( elem, "fxshow" );
-			for ( prop in orig ) {
-				jQuery.style( elem, prop, orig[ prop ] );
-			}
-		});
-		for ( prop in orig ) {
-			tween = createTween( hidden ? dataShow[ prop ] : 0, prop, anim );
-
-			if ( !( prop in dataShow ) ) {
-				dataShow[ prop ] = tween.start;
-				if ( hidden ) {
-					tween.end = tween.start;
-					tween.start = prop === "width" || prop === "height" ? 1 : 0;
-				}
-			}
-		}
-
-	// If this is a noop like .hide().hide(), restore an overwritten display value
-	} else if ( (display === "none" ? defaultDisplay( elem.nodeName ) : display) === "inline" ) {
-		style.display = display;
-	}
-}
-
-function propFilter( props, specialEasing ) {
-	var index, name, easing, value, hooks;
-
-	// camelCase, specialEasing and expand cssHook pass
-	for ( index in props ) {
-		name = jQuery.camelCase( index );
-		easing = specialEasing[ name ];
-		value = props[ index ];
-		if ( jQuery.isArray( value ) ) {
-			easing = value[ 1 ];
-			value = props[ index ] = value[ 0 ];
-		}
-
-		if ( index !== name ) {
-			props[ name ] = value;
-			delete props[ index ];
-		}
-
-		hooks = jQuery.cssHooks[ name ];
-		if ( hooks && "expand" in hooks ) {
-			value = hooks.expand( value );
-			delete props[ name ];
-
-			// not quite $.extend, this wont overwrite keys already present.
-			// also - reusing 'index' from above because we have the correct "name"
-			for ( index in value ) {
-				if ( !( index in props ) ) {
-					props[ index ] = value[ index ];
-					specialEasing[ index ] = easing;
-				}
-			}
-		} else {
-			specialEasing[ name ] = easing;
-		}
-	}
-}
-
-function Animation( elem, properties, options ) {
-	var result,
-		stopped,
-		index = 0,
-		length = animationPrefilters.length,
-		deferred = jQuery.Deferred().always( function() {
-			// don't match elem in the :animated selector
-			delete tick.elem;
-		}),
-		tick = function() {
-			if ( stopped ) {
-				return false;
-			}
-			var currentTime = fxNow || createFxNow(),
-				remaining = Math.max( 0, animation.startTime + animation.duration - currentTime ),
-				// archaic crash bug won't allow us to use 1 - ( 0.5 || 0 ) (#12497)
-				temp = remaining / animation.duration || 0,
-				percent = 1 - temp,
-				index = 0,
-				length = animation.tweens.length;
-
-			for ( ; index < length ; index++ ) {
-				animation.tweens[ index ].run( percent );
-			}
-
-			deferred.notifyWith( elem, [ animation, percent, remaining ]);
-
-			if ( percent < 1 && length ) {
-				return remaining;
-			} else {
-				deferred.resolveWith( elem, [ animation ] );
-				return false;
-			}
-		},
-		animation = deferred.promise({
-			elem: elem,
-			props: jQuery.extend( {}, properties ),
-			opts: jQuery.extend( true, { specialEasing: {} }, options ),
-			originalProperties: properties,
-			originalOptions: options,
-			startTime: fxNow || createFxNow(),
-			duration: options.duration,
-			tweens: [],
-			createTween: function( prop, end ) {
-				var tween = jQuery.Tween( elem, animation.opts, prop, end,
-						animation.opts.specialEasing[ prop ] || animation.opts.easing );
-				animation.tweens.push( tween );
-				return tween;
-			},
-			stop: function( gotoEnd ) {
-				var index = 0,
-					// if we are going to the end, we want to run all the tweens
-					// otherwise we skip this part
-					length = gotoEnd ? animation.tweens.length : 0;
-				if ( stopped ) {
-					return this;
-				}
-				stopped = true;
-				for ( ; index < length ; index++ ) {
-					animation.tweens[ index ].run( 1 );
-				}
-
-				// resolve when we played the last frame
-				// otherwise, reject
-				if ( gotoEnd ) {
-					deferred.resolveWith( elem, [ animation, gotoEnd ] );
-				} else {
-					deferred.rejectWith( elem, [ animation, gotoEnd ] );
-				}
-				return this;
-			}
-		}),
-		props = animation.props;
-
-	propFilter( props, animation.opts.specialEasing );
-
-	for ( ; index < length ; index++ ) {
-		result = animationPrefilters[ index ].call( animation, elem, props, animation.opts );
-		if ( result ) {
-			return result;
-		}
-	}
-
-	jQuery.map( props, createTween, animation );
-
-	if ( jQuery.isFunction( animation.opts.start ) ) {
-		animation.opts.start.call( elem, animation );
-	}
-
-	jQuery.fx.timer(
-		jQuery.extend( tick, {
-			elem: elem,
-			anim: animation,
-			queue: animation.opts.queue
-		})
-	);
-
-	// attach callbacks from options
-	return animation.progress( animation.opts.progress )
-		.done( animation.opts.done, animation.opts.complete )
-		.fail( animation.opts.fail )
-		.always( animation.opts.always );
-}
-
-jQuery.Animation = jQuery.extend( Animation, {
-
-	tweener: function( props, callback ) {
-		if ( jQuery.isFunction( props ) ) {
-			callback = props;
-			props = [ "*" ];
-		} else {
-			props = props.split(" ");
-		}
-
-		var prop,
-			index = 0,
-			length = props.length;
-
-		for ( ; index < length ; index++ ) {
-			prop = props[ index ];
-			tweeners[ prop ] = tweeners[ prop ] || [];
-			tweeners[ prop ].unshift( callback );
-		}
-	},
-
-	prefilter: function( callback, prepend ) {
-		if ( prepend ) {
-			animationPrefilters.unshift( callback );
-		} else {
-			animationPrefilters.push( callback );
-		}
-	}
-});
-
-jQuery.speed = function( speed, easing, fn ) {
-	var opt = speed && typeof speed === "object" ? jQuery.extend( {}, speed ) : {
-		complete: fn || !fn && easing ||
-			jQuery.isFunction( speed ) && speed,
-		duration: speed,
-		easing: fn && easing || easing && !jQuery.isFunction( easing ) && easing
-	};
-
-	opt.duration = jQuery.fx.off ? 0 : typeof opt.duration === "number" ? opt.duration :
-		opt.duration in jQuery.fx.speeds ? jQuery.fx.speeds[ opt.duration ] : jQuery.fx.speeds._default;
-
-	// normalize opt.queue - true/undefined/null -> "fx"
-	if ( opt.queue == null || opt.queue === true ) {
-		opt.queue = "fx";
-	}
-
-	// Queueing
-	opt.old = opt.complete;
-
-	opt.complete = function() {
-		if ( jQuery.isFunction( opt.old ) ) {
-			opt.old.call( this );
-		}
-
-		if ( opt.queue ) {
-			jQuery.dequeue( this, opt.queue );
-		}
-	};
-
-	return opt;
-};
-
-jQuery.fn.extend({
-	fadeTo: function( speed, to, easing, callback ) {
-
-		// show any hidden elements after setting opacity to 0
-		return this.filter( isHidden ).css( "opacity", 0 ).show()
-
-			// animate to the value specified
-			.end().animate({ opacity: to }, speed, easing, callback );
-	},
-	animate: function( prop, speed, easing, callback ) {
-		var empty = jQuery.isEmptyObject( prop ),
-			optall = jQuery.speed( speed, easing, callback ),
-			doAnimation = function() {
-				// Operate on a copy of prop so per-property easing won't be lost
-				var anim = Animation( this, jQuery.extend( {}, prop ), optall );
-
-				// Empty animations, or finishing resolves immediately
-				if ( empty || data_priv.get( this, "finish" ) ) {
-					anim.stop( true );
-				}
-			};
-			doAnimation.finish = doAnimation;
-
-		return empty || optall.queue === false ?
-			this.each( doAnimation ) :
-			this.queue( optall.queue, doAnimation );
-	},
-	stop: function( type, clearQueue, gotoEnd ) {
-		var stopQueue = function( hooks ) {
-			var stop = hooks.stop;
-			delete hooks.stop;
-			stop( gotoEnd );
-		};
-
-		if ( typeof type !== "string" ) {
-			gotoEnd = clearQueue;
-			clearQueue = type;
-			type = undefined;
-		}
-		if ( clearQueue && type !== false ) {
-			this.queue( type || "fx", [] );
-		}
-
-		return this.each(function() {
-			var dequeue = true,
-				index = type != null && type + "queueHooks",
-				timers = jQuery.timers,
-				data = data_priv.get( this );
-
-			if ( index ) {
-				if ( data[ index ] && data[ index ].stop ) {
-					stopQueue( data[ index ] );
-				}
-			} else {
-				for ( index in data ) {
-					if ( data[ index ] && data[ index ].stop && rrun.test( index ) ) {
-						stopQueue( data[ index ] );
-					}
-				}
-			}
-
-			for ( index = timers.length; index--; ) {
-				if ( timers[ index ].elem === this && (type == null || timers[ index ].queue === type) ) {
-					timers[ index ].anim.stop( gotoEnd );
-					dequeue = false;
-					timers.splice( index, 1 );
-				}
-			}
-
-			// start the next in the queue if the last step wasn't forced
-			// timers currently will call their complete callbacks, which will dequeue
-			// but only if they were gotoEnd
-			if ( dequeue || !gotoEnd ) {
-				jQuery.dequeue( this, type );
-			}
-		});
-	},
-	finish: function( type ) {
-		if ( type !== false ) {
-			type = type || "fx";
-		}
-		return this.each(function() {
-			var index,
-				data = data_priv.get( this ),
-				queue = data[ type + "queue" ],
-				hooks = data[ type + "queueHooks" ],
-				timers = jQuery.timers,
-				length = queue ? queue.length : 0;
-
-			// enable finishing flag on private data
-			data.finish = true;
-
-			// empty the queue first
-			jQuery.queue( this, type, [] );
-
-			if ( hooks && hooks.stop ) {
-				hooks.stop.call( this, true );
-			}
-
-			// look for any active animations, and finish them
-			for ( index = timers.length; index--; ) {
-				if ( timers[ index ].elem === this && timers[ index ].queue === type ) {
-					timers[ index ].anim.stop( true );
-					timers.splice( index, 1 );
-				}
-			}
-
-			// look for any animations in the old queue and finish them
-			for ( index = 0; index < length; index++ ) {
-				if ( queue[ index ] && queue[ index ].finish ) {
-					queue[ index ].finish.call( this );
-				}
-			}
-
-			// turn off finishing flag
-			delete data.finish;
-		});
-	}
-});
-
-jQuery.each([ "toggle", "show", "hide" ], function( i, name ) {
-	var cssFn = jQuery.fn[ name ];
-	jQuery.fn[ name ] = function( speed, easing, callback ) {
-		return speed == null || typeof speed === "boolean" ?
-			cssFn.apply( this, arguments ) :
-			this.animate( genFx( name, true ), speed, easing, callback );
-	};
-});
-
-// Generate shortcuts for custom animations
-jQuery.each({
-	slideDown: genFx("show"),
-	slideUp: genFx("hide"),
-	slideToggle: genFx("toggle"),
-	fadeIn: { opacity: "show" },
-	fadeOut: { opacity: "hide" },
-	fadeToggle: { opacity: "toggle" }
-}, function( name, props ) {
-	jQuery.fn[ name ] = function( speed, easing, callback ) {
-		return this.animate( props, speed, easing, callback );
-	};
-});
-
-jQuery.timers = [];
-jQuery.fx.tick = function() {
-	var timer,
-		i = 0,
-		timers = jQuery.timers;
-
-	fxNow = jQuery.now();
-
-	for ( ; i < timers.length; i++ ) {
-		timer = timers[ i ];
-		// Checks the timer has not already been removed
-		if ( !timer() && timers[ i ] === timer ) {
-			timers.splice( i--, 1 );
-		}
-	}
-
-	if ( !timers.length ) {
-		jQuery.fx.stop();
-	}
-	fxNow = undefined;
-};
-
-jQuery.fx.timer = function( timer ) {
-	jQuery.timers.push( timer );
-	if ( timer() ) {
-		jQuery.fx.start();
-	} else {
-		jQuery.timers.pop();
-	}
-};
-
-jQuery.fx.interval = 13;
-
-jQuery.fx.start = function() {
-	if ( !timerId ) {
-		timerId = setInterval( jQuery.fx.tick, jQuery.fx.interval );
-	}
-};
-
-jQuery.fx.stop = function() {
-	clearInterval( timerId );
-	timerId = null;
-};
-
-jQuery.fx.speeds = {
-	slow: 600,
-	fast: 200,
-	// Default speed
-	_default: 400
-};
-
-
-// Based off of the plugin by Clint Helfers, with permission.
-// http://blindsignals.com/index.php/2009/07/jquery-delay/
-jQuery.fn.delay = function( time, type ) {
-	time = jQuery.fx ? jQuery.fx.speeds[ time ] || time : time;
-	type = type || "fx";
-
-	return this.queue( type, function( next, hooks ) {
-		var timeout = setTimeout( next, time );
-		hooks.stop = function() {
-			clearTimeout( timeout );
-		};
-	});
-};
-
-
-(function() {
-	var input = document.createElement( "input" ),
-		select = document.createElement( "select" ),
-		opt = select.appendChild( document.createElement( "option" ) );
-
-	input.type = "checkbox";
-
-	// Support: iOS 5.1, Android 4.x, Android 2.3
-	// Check the default checkbox/radio value ("" on old WebKit; "on" elsewhere)
-	support.checkOn = input.value !== "";
-
-	// Must access the parent to make an option select properly
-	// Support: IE9, IE10
-	support.optSelected = opt.selected;
-
-	// Make sure that the options inside disabled selects aren't marked as disabled
-	// (WebKit marks them as disabled)
-	select.disabled = true;
-	support.optDisabled = !opt.disabled;
-
-	// Check if an input maintains its value after becoming a radio
-	// Support: IE9, IE10
-	input = document.createElement( "input" );
-	input.value = "t";
-	input.type = "radio";
-	support.radioValue = input.value === "t";
-})();
-
-
-var nodeHook, boolHook,
-	attrHandle = jQuery.expr.attrHandle;
-
-jQuery.fn.extend({
-	attr: function( name, value ) {
-		return access( this, jQuery.attr, name, value, arguments.length > 1 );
-	},
-
-	removeAttr: function( name ) {
-		return this.each(function() {
-			jQuery.removeAttr( this, name );
-		});
-	}
-});
-
-jQuery.extend({
-	attr: function( elem, name, value ) {
-		var hooks, ret,
-			nType = elem.nodeType;
-
-		// don't get/set attributes on text, comment and attribute nodes
-		if ( !elem || nType === 3 || nType === 8 || nType === 2 ) {
-			return;
-		}
-
-		// Fallback to prop when attributes are not supported
-		if ( typeof elem.getAttribute === strundefined ) {
-			return jQuery.prop( elem, name, value );
-		}
-
-		// All attributes are lowercase
-		// Grab necessary hook if one is defined
-		if ( nType !== 1 || !jQuery.isXMLDoc( elem ) ) {
-			name = name.toLowerCase();
-			hooks = jQuery.attrHooks[ name ] ||
-				( jQuery.expr.match.bool.test( name ) ? boolHook : nodeHook );
-		}
-
-		if ( value !== undefined ) {
-
-			if ( value === null ) {
-				jQuery.removeAttr( elem, name );
-
-			} else if ( hooks && "set" in hooks && (ret = hooks.set( elem, value, name )) !== undefined ) {
-				return ret;
-
-			} else {
-				elem.setAttribute( name, value + "" );
-				return value;
-			}
-
-		} else if ( hooks && "get" in hooks && (ret = hooks.get( elem, name )) !== null ) {
-			return ret;
-
-		} else {
-			ret = jQuery.find.attr( elem, name );
-
-			// Non-existent attributes return null, we normalize to undefined
-			return ret == null ?
-				undefined :
-				ret;
-		}
-	},
-
-	removeAttr: function( elem, value ) {
-		var name, propName,
-			i = 0,
-			attrNames = value && value.match( rnotwhite );
-
-		if ( attrNames && elem.nodeType === 1 ) {
-			while ( (name = attrNames[i++]) ) {
-				propName = jQuery.propFix[ name ] || name;
-
-				// Boolean attributes get special treatment (#10870)
-				if ( jQuery.expr.match.bool.test( name ) ) {
-					// Set corresponding property to false
-					elem[ propName ] = false;
-				}
-
-				elem.removeAttribute( name );
-			}
-		}
-	},
-
-	attrHooks: {
-		type: {
-			set: function( elem, value ) {
-				if ( !support.radioValue && value === "radio" &&
-					jQuery.nodeName( elem, "input" ) ) {
-					// Setting the type on a radio button after the value resets the value in IE6-9
-					// Reset value to default in case type is set after value during creation
-					var val = elem.value;
-					elem.setAttribute( "type", value );
-					if ( val ) {
-						elem.value = val;
-					}
-					return value;
-				}
-			}
-		}
-	}
-});
-
-// Hooks for boolean attributes
-boolHook = {
-	set: function( elem, value, name ) {
-		if ( value === false ) {
-			// Remove boolean attributes when set to false
-			jQuery.removeAttr( elem, name );
-		} else {
-			elem.setAttribute( name, name );
-		}
-		return name;
-	}
-};
-jQuery.each( jQuery.expr.match.bool.source.match( /\w+/g ), function( i, name ) {
-	var getter = attrHandle[ name ] || jQuery.find.attr;
-
-	attrHandle[ name ] = function( elem, name, isXML ) {
-		var ret, handle;
-		if ( !isXML ) {
-			// Avoid an infinite loop by temporarily removing this function from the getter
-			handle = attrHandle[ name ];
-			attrHandle[ name ] = ret;
-			ret = getter( elem, name, isXML ) != null ?
-				name.toLowerCase() :
-				null;
-			attrHandle[ name ] = handle;
-		}
-		return ret;
-	};
-});
-
-
-
-
-var rfocusable = /^(?:input|select|textarea|button)$/i;
-
-jQuery.fn.extend({
-	prop: function( name, value ) {
-		return access( this, jQuery.prop, name, value, arguments.length > 1 );
-	},
-
-	removeProp: function( name ) {
-		return this.each(function() {
-			delete this[ jQuery.propFix[ name ] || name ];
-		});
-	}
-});
-
-jQuery.extend({
-	propFix: {
-		"for": "htmlFor",
-		"class": "className"
-	},
-
-	prop: function( elem, name, value ) {
-		var ret, hooks, notxml,
-			nType = elem.nodeType;
-
-		// don't get/set properties on text, comment and attribute nodes
-		if ( !elem || nType === 3 || nType === 8 || nType === 2 ) {
-			return;
-		}
-
-		notxml = nType !== 1 || !jQuery.isXMLDoc( elem );
-
-		if ( notxml ) {
-			// Fix name and attach hooks
-			name = jQuery.propFix[ name ] || name;
-			hooks = jQuery.propHooks[ name ];
-		}
-
-		if ( value !== undefined ) {
-			return hooks && "set" in hooks && (ret = hooks.set( elem, value, name )) !== undefined ?
-				ret :
-				( elem[ name ] = value );
-
-		} else {
-			return hooks && "get" in hooks && (ret = hooks.get( elem, name )) !== null ?
-				ret :
-				elem[ name ];
-		}
-	},
-
-	propHooks: {
-		tabIndex: {
-			get: function( elem ) {
-				return elem.hasAttribute( "tabindex" ) || rfocusable.test( elem.nodeName ) || elem.href ?
-					elem.tabIndex :
-					-1;
-			}
-		}
-	}
-});
-
-// Support: IE9+
-// Selectedness for an option in an optgroup can be inaccurate
-if ( !support.optSelected ) {
-	jQuery.propHooks.selected = {
-		get: function( elem ) {
-			var parent = elem.parentNode;
-			if ( parent && parent.parentNode ) {
-				parent.parentNode.selectedIndex;
-			}
-			return null;
-		}
-	};
-}
-
-jQuery.each([
-	"tabIndex",
-	"readOnly",
-	"maxLength",
-	"cellSpacing",
-	"cellPadding",
-	"rowSpan",
-	"colSpan",
-	"useMap",
-	"frameBorder",
-	"contentEditable"
-], function() {
-	jQuery.propFix[ this.toLowerCase() ] = this;
-});
-
-
-
-
-var rclass = /[\t\r\n\f]/g;
-
-jQuery.fn.extend({
-	addClass: function( value ) {
-		var classes, elem, cur, clazz, j, finalValue,
-			proceed = typeof value === "string" && value,
-			i = 0,
-			len = this.length;
-
-		if ( jQuery.isFunction( value ) ) {
-			return this.each(function( j ) {
-				jQuery( this ).addClass( value.call( this, j, this.className ) );
-			});
-		}
-
-		if ( proceed ) {
-			// The disjunction here is for better compressibility (see removeClass)
-			classes = ( value || "" ).match( rnotwhite ) || [];
-
-			for ( ; i < len; i++ ) {
-				elem = this[ i ];
-				cur = elem.nodeType === 1 && ( elem.className ?
-					( " " + elem.className + " " ).replace( rclass, " " ) :
-					" "
-				);
-
-				if ( cur ) {
-					j = 0;
-					while ( (clazz = classes[j++]) ) {
-						if ( cur.indexOf( " " + clazz + " " ) < 0 ) {
-							cur += clazz + " ";
-						}
-					}
-
-					// only assign if different to avoid unneeded rendering.
-					finalValue = jQuery.trim( cur );
-					if ( elem.className !== finalValue ) {
-						elem.className = finalValue;
-					}
-				}
-			}
-		}
-
-		return this;
-	},
-
-	removeClass: function( value ) {
-		var classes, elem, cur, clazz, j, finalValue,
-			proceed = arguments.length === 0 || typeof value === "string" && value,
-			i = 0,
-			len = this.length;
-
-		if ( jQuery.isFunction( value ) ) {
-			return this.each(function( j ) {
-				jQuery( this ).removeClass( value.call( this, j, this.className ) );
-			});
-		}
-		if ( proceed ) {
-			classes = ( value || "" ).match( rnotwhite ) || [];
-
-			for ( ; i < len; i++ ) {
-				elem = this[ i ];
-				// This expression is here for better compressibility (see addClass)
-				cur = elem.nodeType === 1 && ( elem.className ?
-					( " " + elem.className + " " ).replace( rclass, " " ) :
-					""
-				);
-
-				if ( cur ) {
-					j = 0;
-					while ( (clazz = classes[j++]) ) {
-						// Remove *all* instances
-						while ( cur.indexOf( " " + clazz + " " ) >= 0 ) {
-							cur = cur.replace( " " + clazz + " ", " " );
-						}
-					}
-
-					// only assign if different to avoid unneeded rendering.
-					finalValue = value ? jQuery.trim( cur ) : "";
-					if ( elem.className !== finalValue ) {
-						elem.className = finalValue;
-					}
-				}
-			}
-		}
-
-		return this;
-	},
-
-	toggleClass: function( value, stateVal ) {
-		var type = typeof value;
-
-		if ( typeof stateVal === "boolean" && type === "string" ) {
-			return stateVal ? this.addClass( value ) : this.removeClass( value );
-		}
-
-		if ( jQuery.isFunction( value ) ) {
-			return this.each(function( i ) {
-				jQuery( this ).toggleClass( value.call(this, i, this.className, stateVal), stateVal );
-			});
-		}
-
-		return this.each(function() {
-			if ( type === "string" ) {
-				// toggle individual class names
-				var className,
-					i = 0,
-					self = jQuery( this ),
-					classNames = value.match( rnotwhite ) || [];
-
-				while ( (className = classNames[ i++ ]) ) {
-					// check each className given, space separated list
-					if ( self.hasClass( className ) ) {
-						self.removeClass( className );
-					} else {
-						self.addClass( className );
-					}
-				}
-
-			// Toggle whole class name
-			} else if ( type === strundefined || type === "boolean" ) {
-				if ( this.className ) {
-					// store className if set
-					data_priv.set( this, "__className__", this.className );
-				}
-
-				// If the element has a class name or if we're passed "false",
-				// then remove the whole classname (if there was one, the above saved it).
-				// Otherwise bring back whatever was previously saved (if anything),
-				// falling back to the empty string if nothing was stored.
-				this.className = this.className || value === false ? "" : data_priv.get( this, "__className__" ) || "";
-			}
-		});
-	},
-
-	hasClass: function( selector ) {
-		var className = " " + selector + " ",
-			i = 0,
-			l = this.length;
-		for ( ; i < l; i++ ) {
-			if ( this[i].nodeType === 1 && (" " + this[i].className + " ").replace(rclass, " ").indexOf( className ) >= 0 ) {
-				return true;
-			}
-		}
-
-		return false;
-	}
-});
-
-
-
-
-var rreturn = /\r/g;
-
-jQuery.fn.extend({
-	val: function( value ) {
-		var hooks, ret, isFunction,
-			elem = this[0];
-
-		if ( !arguments.length ) {
-			if ( elem ) {
-				hooks = jQuery.valHooks[ elem.type ] || jQuery.valHooks[ elem.nodeName.toLowerCase() ];
-
-				if ( hooks && "get" in hooks && (ret = hooks.get( elem, "value" )) !== undefined ) {
-					return ret;
-				}
-
-				ret = elem.value;
-
-				return typeof ret === "string" ?
-					// handle most common string cases
-					ret.replace(rreturn, "") :
-					// handle cases where value is null/undef or number
-					ret == null ? "" : ret;
-			}
-
-			return;
-		}
-
-		isFunction = jQuery.isFunction( value );
-
-		return this.each(function( i ) {
-			var val;
-
-			if ( this.nodeType !== 1 ) {
-				return;
-			}
-
-			if ( isFunction ) {
-				val = value.call( this, i, jQuery( this ).val() );
-			} else {
-				val = value;
-			}
-
-			// Treat null/undefined as ""; convert numbers to string
-			if ( val == null ) {
-				val = "";
-
-			} else if ( typeof val === "number" ) {
-				val += "";
-
-			} else if ( jQuery.isArray( val ) ) {
-				val = jQuery.map( val, function( value ) {
-					return value == null ? "" : value + "";
-				});
-			}
-
-			hooks = jQuery.valHooks[ this.type ] || jQuery.valHooks[ this.nodeName.toLowerCase() ];
-
-			// If set returns undefined, fall back to normal setting
-			if ( !hooks || !("set" in hooks) || hooks.set( this, val, "value" ) === undefined ) {
-				this.value = val;
-			}
-		});
-	}
-});
-
-jQuery.extend({
-	valHooks: {
-		option: {
-			get: function( elem ) {
-				var val = jQuery.find.attr( elem, "value" );
-				return val != null ?
-					val :
-					// Support: IE10-11+
-					// option.text throws exceptions (#14686, #14858)
-					jQuery.trim( jQuery.text( elem ) );
-			}
-		},
-		select: {
-			get: function( elem ) {
-				var value, option,
-					options = elem.options,
-					index = elem.selectedIndex,
-					one = elem.type === "select-one" || index < 0,
-					values = one ? null : [],
-					max = one ? index + 1 : options.length,
-					i = index < 0 ?
-						max :
-						one ? index : 0;
-
-				// Loop through all the selected options
-				for ( ; i < max; i++ ) {
-					option = options[ i ];
-
-					// IE6-9 doesn't update selected after form reset (#2551)
-					if ( ( option.selected || i === index ) &&
-							// Don't return options that are disabled or in a disabled optgroup
-							( support.optDisabled ? !option.disabled : option.getAttribute( "disabled" ) === null ) &&
-							( !option.parentNode.disabled || !jQuery.nodeName( option.parentNode, "optgroup" ) ) ) {
-
-						// Get the specific value for the option
-						value = jQuery( option ).val();
-
-						// We don't need an array for one selects
-						if ( one ) {
-							return value;
-						}
-
-						// Multi-Selects return an array
-						values.push( value );
-					}
-				}
-
-				return values;
-			},
-
-			set: function( elem, value ) {
-				var optionSet, option,
-					options = elem.options,
-					values = jQuery.makeArray( value ),
-					i = options.length;
-
-				while ( i-- ) {
-					option = options[ i ];
-					if ( (option.selected = jQuery.inArray( option.value, values ) >= 0) ) {
-						optionSet = true;
-					}
-				}
-
-				// force browsers to behave consistently when non-matching value is set
-				if ( !optionSet ) {
-					elem.selectedIndex = -1;
-				}
-				return values;
-			}
-		}
-	}
-});
-
-// Radios and checkboxes getter/setter
-jQuery.each([ "radio", "checkbox" ], function() {
-	jQuery.valHooks[ this ] = {
-		set: function( elem, value ) {
-			if ( jQuery.isArray( value ) ) {
-				return ( elem.checked = jQuery.inArray( jQuery(elem).val(), value ) >= 0 );
-			}
-		}
-	};
-	if ( !support.checkOn ) {
-		jQuery.valHooks[ this ].get = function( elem ) {
-			// Support: Webkit
-			// "" is returned instead of "on" if a value isn't specified
-			return elem.getAttribute("value") === null ? "on" : elem.value;
-		};
-	}
-});
-
-
-
-
-// Return jQuery for attributes-only inclusion
-
-
-jQuery.each( ("blur focus focusin focusout load resize scroll unload click dblclick " +
-	"mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave " +
-	"change select submit keydown keypress keyup error contextmenu").split(" "), function( i, name ) {
-
-	// Handle event binding
-	jQuery.fn[ name ] = function( data, fn ) {
-		return arguments.length > 0 ?
-			this.on( name, null, data, fn ) :
-			this.trigger( name );
-	};
-});
-
-jQuery.fn.extend({
-	hover: function( fnOver, fnOut ) {
-		return this.mouseenter( fnOver ).mouseleave( fnOut || fnOver );
-	},
-
-	bind: function( types, data, fn ) {
-		return this.on( types, null, data, fn );
-	},
-	unbind: function( types, fn ) {
-		return this.off( types, null, fn );
-	},
-
-	delegate: function( selector, types, data, fn ) {
-		return this.on( types, selector, data, fn );
-	},
-	undelegate: function( selector, types, fn ) {
-		// ( namespace ) or ( selector, types [, fn] )
-		return arguments.length === 1 ? this.off( selector, "**" ) : this.off( types, selector || "**", fn );
-	}
-});
-
-
-var nonce = jQuery.now();
-
-var rquery = (/\?/);
-
-
-
-// Support: Android 2.3
-// Workaround failure to string-cast null input
-jQuery.parseJSON = function( data ) {
-	return JSON.parse( data + "" );
-};
-
-
-// Cross-browser xml parsing
-jQuery.parseXML = function( data ) {
-	var xml, tmp;
-	if ( !data || typeof data !== "string" ) {
-		return null;
-	}
-
-	// Support: IE9
-	try {
-		tmp = new DOMParser();
-		xml = tmp.parseFromString( data, "text/xml" );
-	} catch ( e ) {
-		xml = undefined;
-	}
-
-	if ( !xml || xml.getElementsByTagName( "parsererror" ).length ) {
-		jQuery.error( "Invalid XML: " + data );
-	}
-	return xml;
-};
-
-
-var
-	// Document location
-	ajaxLocParts,
-	ajaxLocation,
-
-	rhash = /#.*$/,
-	rts = /([?&])_=[^&]*/,
-	rheaders = /^(.*?):[ \t]*([^\r\n]*)$/mg,
-	// #7653, #8125, #8152: local protocol detection
-	rlocalProtocol = /^(?:about|app|app-storage|.+-extension|file|res|widget):$/,
-	rnoContent = /^(?:GET|HEAD)$/,
-	rprotocol = /^\/\//,
-	rurl = /^([\w.+-]+:)(?:\/\/(?:[^\/?#]*@|)([^\/?#:]*)(?::(\d+)|)|)/,
-
-	/* Prefilters
-	 * 1) They are useful to introduce custom dataTypes (see ajax/jsonp.js for an example)
-	 * 2) These are called:
-	 *    - BEFORE asking for a transport
-	 *    - AFTER param serialization (s.data is a string if s.processData is true)
-	 * 3) key is the dataType
-	 * 4) the catchall symbol "*" can be used
-	 * 5) execution will start with transport dataType and THEN continue down to "*" if needed
-	 */
-	prefilters = {},
-
-	/* Transports bindings
-	 * 1) key is the dataType
-	 * 2) the catchall symbol "*" can be used
-	 * 3) selection will start with transport dataType and THEN go to "*" if needed
-	 */
-	transports = {},
-
-	// Avoid comment-prolog char sequence (#10098); must appease lint and evade compression
-	allTypes = "*/".concat("*");
-
-// #8138, IE may throw an exception when accessing
-// a field from window.location if document.domain has been set
-try {
-	ajaxLocation = location.href;
-} catch( e ) {
-	// Use the href attribute of an A element
-	// since IE will modify it given document.location
-	ajaxLocation = document.createElement( "a" );
-	ajaxLocation.href = "";
-	ajaxLocation = ajaxLocation.href;
-}
-
-// Segment location into parts
-ajaxLocParts = rurl.exec( ajaxLocation.toLowerCase() ) || [];
-
-// Base "constructor" for jQuery.ajaxPrefilter and jQuery.ajaxTransport
-function addToPrefiltersOrTransports( structure ) {
-
-	// dataTypeExpression is optional and defaults to "*"
-	return function( dataTypeExpression, func ) {
-
-		if ( typeof dataTypeExpression !== "string" ) {
-			func = dataTypeExpression;
-			dataTypeExpression = "*";
-		}
-
-		var dataType,
-			i = 0,
-			dataTypes = dataTypeExpression.toLowerCase().match( rnotwhite ) || [];
-
-		if ( jQuery.isFunction( func ) ) {
-			// For each dataType in the dataTypeExpression
-			while ( (dataType = dataTypes[i++]) ) {
-				// Prepend if requested
-				if ( dataType[0] === "+" ) {
-					dataType = dataType.slice( 1 ) || "*";
-					(structure[ dataType ] = structure[ dataType ] || []).unshift( func );
-
-				// Otherwise append
-				} else {
-					(structure[ dataType ] = structure[ dataType ] || []).push( func );
-				}
-			}
-		}
-	};
-}
-
-// Base inspection function for prefilters and transports
-function inspectPrefiltersOrTransports( structure, options, originalOptions, jqXHR ) {
-
-	var inspected = {},
-		seekingTransport = ( structure === transports );
-
-	function inspect( dataType ) {
-		var selected;
-		inspected[ dataType ] = true;
-		jQuery.each( structure[ dataType ] || [], function( _, prefilterOrFactory ) {
-			var dataTypeOrTransport = prefilterOrFactory( options, originalOptions, jqXHR );
-			if ( typeof dataTypeOrTransport === "string" && !seekingTransport && !inspected[ dataTypeOrTransport ] ) {
-				options.dataTypes.unshift( dataTypeOrTransport );
-				inspect( dataTypeOrTransport );
-				return false;
-			} else if ( seekingTransport ) {
-				return !( selected = dataTypeOrTransport );
-			}
-		});
-		return selected;
-	}
-
-	return inspect( options.dataTypes[ 0 ] ) || !inspected[ "*" ] && inspect( "*" );
-}
-
-// A special extend for ajax options
-// that takes "flat" options (not to be deep extended)
-// Fixes #9887
-function ajaxExtend( target, src ) {
-	var key, deep,
-		flatOptions = jQuery.ajaxSettings.flatOptions || {};
-
-	for ( key in src ) {
-		if ( src[ key ] !== undefined ) {
-			( flatOptions[ key ] ? target : ( deep || (deep = {}) ) )[ key ] = src[ key ];
-		}
-	}
-	if ( deep ) {
-		jQuery.extend( true, target, deep );
-	}
-
-	return target;
-}
-
-/* Handles responses to an ajax request:
- * - finds the right dataType (mediates between content-type and expected dataType)
- * - returns the corresponding response
- */
-function ajaxHandleResponses( s, jqXHR, responses ) {
-
-	var ct, type, finalDataType, firstDataType,
-		contents = s.contents,
-		dataTypes = s.dataTypes;
-
-	// Remove auto dataType and get content-type in the process
-	while ( dataTypes[ 0 ] === "*" ) {
-		dataTypes.shift();
-		if ( ct === undefined ) {
-			ct = s.mimeType || jqXHR.getResponseHeader("Content-Type");
-		}
-	}
-
-	// Check if we're dealing with a known content-type
-	if ( ct ) {
-		for ( type in contents ) {
-			if ( contents[ type ] && contents[ type ].test( ct ) ) {
-				dataTypes.unshift( type );
-				break;
-			}
-		}
-	}
-
-	// Check to see if we have a response for the expected dataType
-	if ( dataTypes[ 0 ] in responses ) {
-		finalDataType = dataTypes[ 0 ];
-	} else {
-		// Try convertible dataTypes
-		for ( type in responses ) {
-			if ( !dataTypes[ 0 ] || s.converters[ type + " " + dataTypes[0] ] ) {
-				finalDataType = type;
-				break;
-			}
-			if ( !firstDataType ) {
-				firstDataType = type;
-			}
-		}
-		// Or just use first one
-		finalDataType = finalDataType || firstDataType;
-	}
-
-	// If we found a dataType
-	// We add the dataType to the list if needed
-	// and return the corresponding response
-	if ( finalDataType ) {
-		if ( finalDataType !== dataTypes[ 0 ] ) {
-			dataTypes.unshift( finalDataType );
-		}
-		return responses[ finalDataType ];
-	}
-}
-
-/* Chain conversions given the request and the original response
- * Also sets the responseXXX fields on the jqXHR instance
- */
-function ajaxConvert( s, response, jqXHR, isSuccess ) {
-	var conv2, current, conv, tmp, prev,
-		converters = {},
-		// Work with a copy of dataTypes in case we need to modify it for conversion
-		dataTypes = s.dataTypes.slice();
-
-	// Create converters map with lowercased keys
-	if ( dataTypes[ 1 ] ) {
-		for ( conv in s.converters ) {
-			converters[ conv.toLowerCase() ] = s.converters[ conv ];
-		}
-	}
-
-	current = dataTypes.shift();
-
-	// Convert to each sequential dataType
-	while ( current ) {
-
-		if ( s.responseFields[ current ] ) {
-			jqXHR[ s.responseFields[ current ] ] = response;
-		}
-
-		// Apply the dataFilter if provided
-		if ( !prev && isSuccess && s.dataFilter ) {
-			response = s.dataFilter( response, s.dataType );
-		}
-
-		prev = current;
-		current = dataTypes.shift();
-
-		if ( current ) {
-
-		// There's only work to do if current dataType is non-auto
-			if ( current === "*" ) {
-
-				current = prev;
-
-			// Convert response if prev dataType is non-auto and differs from current
-			} else if ( prev !== "*" && prev !== current ) {
-
-				// Seek a direct converter
-				conv = converters[ prev + " " + current ] || converters[ "* " + current ];
-
-				// If none found, seek a pair
-				if ( !conv ) {
-					for ( conv2 in converters ) {
-
-						// If conv2 outputs current
-						tmp = conv2.split( " " );
-						if ( tmp[ 1 ] === current ) {
-
-							// If prev can be converted to accepted input
-							conv = converters[ prev + " " + tmp[ 0 ] ] ||
-								converters[ "* " + tmp[ 0 ] ];
-							if ( conv ) {
-								// Condense equivalence converters
-								if ( conv === true ) {
-									conv = converters[ conv2 ];
-
-								// Otherwise, insert the intermediate dataType
-								} else if ( converters[ conv2 ] !== true ) {
-									current = tmp[ 0 ];
-									dataTypes.unshift( tmp[ 1 ] );
-								}
-								break;
-							}
-						}
-					}
-				}
-
-				// Apply converter (if not an equivalence)
-				if ( conv !== true ) {
-
-					// Unless errors are allowed to bubble, catch and return them
-					if ( conv && s[ "throws" ] ) {
-						response = conv( response );
-					} else {
-						try {
-							response = conv( response );
-						} catch ( e ) {
-							return { state: "parsererror", error: conv ? e : "No conversion from " + prev + " to " + current };
-						}
-					}
-				}
-			}
-		}
-	}
-
-	return { state: "success", data: response };
-}
-
-jQuery.extend({
-
-	// Counter for holding the number of active queries
-	active: 0,
-
-	// Last-Modified header cache for next request
-	lastModified: {},
-	etag: {},
-
-	ajaxSettings: {
-		url: ajaxLocation,
-		type: "GET",
-		isLocal: rlocalProtocol.test( ajaxLocParts[ 1 ] ),
-		global: true,
-		processData: true,
-		async: true,
-		contentType: "application/x-www-form-urlencoded; charset=UTF-8",
-		/*
-		timeout: 0,
-		data: null,
-		dataType: null,
-		username: null,
-		password: null,
-		cache: null,
-		throws: false,
-		traditional: false,
-		headers: {},
-		*/
-
-		accepts: {
-			"*": allTypes,
-			text: "text/plain",
-			html: "text/html",
-			xml: "application/xml, text/xml",
-			json: "application/json, text/javascript"
-		},
-
-		contents: {
-			xml: /xml/,
-			html: /html/,
-			json: /json/
-		},
-
-		responseFields: {
-			xml: "responseXML",
-			text: "responseText",
-			json: "responseJSON"
-		},
-
-		// Data converters
-		// Keys separate source (or catchall "*") and destination types with a single space
-		converters: {
-
-			// Convert anything to text
-			"* text": String,
-
-			// Text to html (true = no transformation)
-			"text html": true,
-
-			// Evaluate text as a json expression
-			"text json": jQuery.parseJSON,
-
-			// Parse text as xml
-			"text xml": jQuery.parseXML
-		},
-
-		// For options that shouldn't be deep extended:
-		// you can add your own custom options here if
-		// and when you create one that shouldn't be
-		// deep extended (see ajaxExtend)
-		flatOptions: {
-			url: true,
-			context: true
-		}
-	},
-
-	// Creates a full fledged settings object into target
-	// with both ajaxSettings and settings fields.
-	// If target is omitted, writes into ajaxSettings.
-	ajaxSetup: function( target, settings ) {
-		return settings ?
-
-			// Building a settings object
-			ajaxExtend( ajaxExtend( target, jQuery.ajaxSettings ), settings ) :
-
-			// Extending ajaxSettings
-			ajaxExtend( jQuery.ajaxSettings, target );
-	},
-
-	ajaxPrefilter: addToPrefiltersOrTransports( prefilters ),
-	ajaxTransport: addToPrefiltersOrTransports( transports ),
-
-	// Main method
-	ajax: function( url, options ) {
-
-		// If url is an object, simulate pre-1.5 signature
-		if ( typeof url === "object" ) {
-			options = url;
-			url = undefined;
-		}
-
-		// Force options to be an object
-		options = options || {};
-
-		var transport,
-			// URL without anti-cache param
-			cacheURL,
-			// Response headers
-			responseHeadersString,
-			responseHeaders,
-			// timeout handle
-			timeoutTimer,
-			// Cross-domain detection vars
-			parts,
-			// To know if global events are to be dispatched
-			fireGlobals,
-			// Loop variable
-			i,
-			// Create the final options object
-			s = jQuery.ajaxSetup( {}, options ),
-			// Callbacks context
-			callbackContext = s.context || s,
-			// Context for global events is callbackContext if it is a DOM node or jQuery collection
-			globalEventContext = s.context && ( callbackContext.nodeType || callbackContext.jquery ) ?
-				jQuery( callbackContext ) :
-				jQuery.event,
-			// Deferreds
-			deferred = jQuery.Deferred(),
-			completeDeferred = jQuery.Callbacks("once memory"),
-			// Status-dependent callbacks
-			statusCode = s.statusCode || {},
-			// Headers (they are sent all at once)
-			requestHeaders = {},
-			requestHeadersNames = {},
-			// The jqXHR state
-			state = 0,
-			// Default abort message
-			strAbort = "canceled",
-			// Fake xhr
-			jqXHR = {
-				readyState: 0,
-
-				// Builds headers hashtable if needed
-				getResponseHeader: function( key ) {
-					var match;
-					if ( state === 2 ) {
-						if ( !responseHeaders ) {
-							responseHeaders = {};
-							while ( (match = rheaders.exec( responseHeadersString )) ) {
-								responseHeaders[ match[1].toLowerCase() ] = match[ 2 ];
-							}
-						}
-						match = responseHeaders[ key.toLowerCase() ];
-					}
-					return match == null ? null : match;
-				},
-
-				// Raw string
-				getAllResponseHeaders: function() {
-					return state === 2 ? responseHeadersString : null;
-				},
-
-				// Caches the header
-				setRequestHeader: function( name, value ) {
-					var lname = name.toLowerCase();
-					if ( !state ) {
-						name = requestHeadersNames[ lname ] = requestHeadersNames[ lname ] || name;
-						requestHeaders[ name ] = value;
-					}
-					return this;
-				},
-
-				// Overrides response content-type header
-				overrideMimeType: function( type ) {
-					if ( !state ) {
-						s.mimeType = type;
-					}
-					return this;
-				},
-
-				// Status-dependent callbacks
-				statusCode: function( map ) {
-					var code;
-					if ( map ) {
-						if ( state < 2 ) {
-							for ( code in map ) {
-								// Lazy-add the new callback in a way that preserves old ones
-								statusCode[ code ] = [ statusCode[ code ], map[ code ] ];
-							}
-						} else {
-							// Execute the appropriate callbacks
-							jqXHR.always( map[ jqXHR.status ] );
-						}
-					}
-					return this;
-				},
-
-				// Cancel the request
-				abort: function( statusText ) {
-					var finalText = statusText || strAbort;
-					if ( transport ) {
-						transport.abort( finalText );
-					}
-					done( 0, finalText );
-					return this;
-				}
-			};
-
-		// Attach deferreds
-		deferred.promise( jqXHR ).complete = completeDeferred.add;
-		jqXHR.success = jqXHR.done;
-		jqXHR.error = jqXHR.fail;
-
-		// Remove hash character (#7531: and string promotion)
-		// Add protocol if not provided (prefilters might expect it)
-		// Handle falsy url in the settings object (#10093: consistency with old signature)
-		// We also use the url parameter if available
-		s.url = ( ( url || s.url || ajaxLocation ) + "" ).replace( rhash, "" )
-			.replace( rprotocol, ajaxLocParts[ 1 ] + "//" );
-
-		// Alias method option to type as per ticket #12004
-		s.type = options.method || options.type || s.method || s.type;
-
-		// Extract dataTypes list
-		s.dataTypes = jQuery.trim( s.dataType || "*" ).toLowerCase().match( rnotwhite ) || [ "" ];
-
-		// A cross-domain request is in order when we have a protocol:host:port mismatch
-		if ( s.crossDomain == null ) {
-			parts = rurl.exec( s.url.toLowerCase() );
-			s.crossDomain = !!( parts &&
-				( parts[ 1 ] !== ajaxLocParts[ 1 ] || parts[ 2 ] !== ajaxLocParts[ 2 ] ||
-					( parts[ 3 ] || ( parts[ 1 ] === "http:" ? "80" : "443" ) ) !==
-						( ajaxLocParts[ 3 ] || ( ajaxLocParts[ 1 ] === "http:" ? "80" : "443" ) ) )
-			);
-		}
-
-		// Convert data if not already a string
-		if ( s.data && s.processData && typeof s.data !== "string" ) {
-			s.data = jQuery.param( s.data, s.traditional );
-		}
-
-		// Apply prefilters
-		inspectPrefiltersOrTransports( prefilters, s, options, jqXHR );
-
-		// If request was aborted inside a prefilter, stop there
-		if ( state === 2 ) {
-			return jqXHR;
-		}
-
-		// We can fire global events as of now if asked to
-		fireGlobals = s.global;
-
-		// Watch for a new set of requests
-		if ( fireGlobals && jQuery.active++ === 0 ) {
-			jQuery.event.trigger("ajaxStart");
-		}
-
-		// Uppercase the type
-		s.type = s.type.toUpperCase();
-
-		// Determine if request has content
-		s.hasContent = !rnoContent.test( s.type );
-
-		// Save the URL in case we're toying with the If-Modified-Since
-		// and/or If-None-Match header later on
-		cacheURL = s.url;
-
-		// More options handling for requests with no content
-		if ( !s.hasContent ) {
-
-			// If data is available, append data to url
-			if ( s.data ) {
-				cacheURL = ( s.url += ( rquery.test( cacheURL ) ? "&" : "?" ) + s.data );
-				// #9682: remove data so that it's not used in an eventual retry
-				delete s.data;
-			}
-
-			// Add anti-cache in url if needed
-			if ( s.cache === false ) {
-				s.url = rts.test( cacheURL ) ?
-
-					// If there is already a '_' parameter, set its value
-					cacheURL.replace( rts, "$1_=" + nonce++ ) :
-
-					// Otherwise add one to the end
-					cacheURL + ( rquery.test( cacheURL ) ? "&" : "?" ) + "_=" + nonce++;
-			}
-		}
-
-		// Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode.
-		if ( s.ifModified ) {
-			if ( jQuery.lastModified[ cacheURL ] ) {
-				jqXHR.setRequestHeader( "If-Modified-Since", jQuery.lastModified[ cacheURL ] );
-			}
-			if ( jQuery.etag[ cacheURL ] ) {
-				jqXHR.setRequestHeader( "If-None-Match", jQuery.etag[ cacheURL ] );
-			}
-		}
-
-		// Set the correct header, if data is being sent
-		if ( s.data && s.hasContent && s.contentType !== false || options.contentType ) {
-			jqXHR.setRequestHeader( "Content-Type", s.contentType );
-		}
-
-		// Set the Accepts header for the server, depending on the dataType
-		jqXHR.setRequestHeader(
-			"Accept",
-			s.dataTypes[ 0 ] && s.accepts[ s.dataTypes[0] ] ?
-				s.accepts[ s.dataTypes[0] ] + ( s.dataTypes[ 0 ] !== "*" ? ", " + allTypes + "; q=0.01" : "" ) :
-				s.accepts[ "*" ]
-		);
-
-		// Check for headers option
-		for ( i in s.headers ) {
-			jqXHR.setRequestHeader( i, s.headers[ i ] );
-		}
-
-		// Allow custom headers/mimetypes and early abort
-		if ( s.beforeSend && ( s.beforeSend.call( callbackContext, jqXHR, s ) === false || state === 2 ) ) {
-			// Abort if not done already and return
-			return jqXHR.abort();
-		}
-
-		// aborting is no longer a cancellation
-		strAbort = "abort";
-
-		// Install callbacks on deferreds
-		for ( i in { success: 1, error: 1, complete: 1 } ) {
-			jqXHR[ i ]( s[ i ] );
-		}
-
-		// Get transport
-		transport = inspectPrefiltersOrTransports( transports, s, options, jqXHR );
-
-		// If no transport, we auto-abort
-		if ( !transport ) {
-			done( -1, "No Transport" );
-		} else {
-			jqXHR.readyState = 1;
-
-			// Send global event
-			if ( fireGlobals ) {
-				globalEventContext.trigger( "ajaxSend", [ jqXHR, s ] );
-			}
-			// Timeout
-			if ( s.async && s.timeout > 0 ) {
-				timeoutTimer = setTimeout(function() {
-					jqXHR.abort("timeout");
-				}, s.timeout );
-			}
-
-			try {
-				state = 1;
-				transport.send( requestHeaders, done );
-			} catch ( e ) {
-				// Propagate exception as error if not done
-				if ( state < 2 ) {
-					done( -1, e );
-				// Simply rethrow otherwise
-				} else {
-					throw e;
-				}
-			}
-		}
-
-		// Callback for when everything is done
-		function done( status, nativeStatusText, responses, headers ) {
-			var isSuccess, success, error, response, modified,
-				statusText = nativeStatusText;
-
-			// Called once
-			if ( state === 2 ) {
-				return;
-			}
-
-			// State is "done" now
-			state = 2;
-
-			// Clear timeout if it exists
-			if ( timeoutTimer ) {
-				clearTimeout( timeoutTimer );
-			}
-
-			// Dereference transport for early garbage collection
-			// (no matter how long the jqXHR object will be used)
-			transport = undefined;
-
-			// Cache response headers
-			responseHeadersString = headers || "";
-
-			// Set readyState
-			jqXHR.readyState = status > 0 ? 4 : 0;
-
-			// Determine if successful
-			isSuccess = status >= 200 && status < 300 || status === 304;
-
-			// Get response data
-			if ( responses ) {
-				response = ajaxHandleResponses( s, jqXHR, responses );
-			}
-
-			// Convert no matter what (that way responseXXX fields are always set)
-			response = ajaxConvert( s, response, jqXHR, isSuccess );
-
-			// If successful, handle type chaining
-			if ( isSuccess ) {
-
-				// Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode.
-				if ( s.ifModified ) {
-					modified = jqXHR.getResponseHeader("Last-Modified");
-					if ( modified ) {
-						jQuery.lastModified[ cacheURL ] = modified;
-					}
-					modified = jqXHR.getResponseHeader("etag");
-					if ( modified ) {
-						jQuery.etag[ cacheURL ] = modified;
-					}
-				}
-
-				// if no content
-				if ( status === 204 || s.type === "HEAD" ) {
-					statusText = "nocontent";
-
-				// if not modified
-				} else if ( status === 304 ) {
-					statusText = "notmodified";
-
-				// If we have data, let's convert it
-				} else {
-					statusText = response.state;
-					success = response.data;
-					error = response.error;
-					isSuccess = !error;
-				}
-			} else {
-				// We extract error from statusText
-				// then normalize statusText and status for non-aborts
-				error = statusText;
-				if ( status || !statusText ) {
-					statusText = "error";
-					if ( status < 0 ) {
-						status = 0;
-					}
-				}
-			}
-
-			// Set data for the fake xhr object
-			jqXHR.status = status;
-			jqXHR.statusText = ( nativeStatusText || statusText ) + "";
-
-			// Success/Error
-			if ( isSuccess ) {
-				deferred.resolveWith( callbackContext, [ success, statusText, jqXHR ] );
-			} else {
-				deferred.rejectWith( callbackContext, [ jqXHR, statusText, error ] );
-			}
-
-			// Status-dependent callbacks
-			jqXHR.statusCode( statusCode );
-			statusCode = undefined;
-
-			if ( fireGlobals ) {
-				globalEventContext.trigger( isSuccess ? "ajaxSuccess" : "ajaxError",
-					[ jqXHR, s, isSuccess ? success : error ] );
-			}
-
-			// Complete
-			completeDeferred.fireWith( callbackContext, [ jqXHR, statusText ] );
-
-			if ( fireGlobals ) {
-				globalEventContext.trigger( "ajaxComplete", [ jqXHR, s ] );
-				// Handle the global AJAX counter
-				if ( !( --jQuery.active ) ) {
-					jQuery.event.trigger("ajaxStop");
-				}
-			}
-		}
-
-		return jqXHR;
-	},
-
-	getJSON: function( url, data, callback ) {
-		return jQuery.get( url, data, callback, "json" );
-	},
-
-	getScript: function( url, callback ) {
-		return jQuery.get( url, undefined, callback, "script" );
-	}
-});
-
-jQuery.each( [ "get", "post" ], function( i, method ) {
-	jQuery[ method ] = function( url, data, callback, type ) {
-		// shift arguments if data argument was omitted
-		if ( jQuery.isFunction( data ) ) {
-			type = type || callback;
-			callback = data;
-			data = undefined;
-		}
-
-		return jQuery.ajax({
-			url: url,
-			type: method,
-			dataType: type,
-			data: data,
-			success: callback
-		});
-	};
-});
-
-// Attach a bunch of functions for handling common AJAX events
-jQuery.each( [ "ajaxStart", "ajaxStop", "ajaxComplete", "ajaxError", "ajaxSuccess", "ajaxSend" ], function( i, type ) {
-	jQuery.fn[ type ] = function( fn ) {
-		return this.on( type, fn );
-	};
-});
-
-
-jQuery._evalUrl = function( url ) {
-	return jQuery.ajax({
-		url: url,
-		type: "GET",
-		dataType: "script",
-		async: false,
-		global: false,
-		"throws": true
-	});
-};
-
-
-jQuery.fn.extend({
-	wrapAll: function( html ) {
-		var wrap;
-
-		if ( jQuery.isFunction( html ) ) {
-			return this.each(function( i ) {
-				jQuery( this ).wrapAll( html.call(this, i) );
-			});
-		}
-
-		if ( this[ 0 ] ) {
-
-			// The elements to wrap the target around
-			wrap = jQuery( html, this[ 0 ].ownerDocument ).eq( 0 ).clone( true );
-
-			if ( this[ 0 ].parentNode ) {
-				wrap.insertBefore( this[ 0 ] );
-			}
-
-			wrap.map(function() {
-				var elem = this;
-
-				while ( elem.firstElementChild ) {
-					elem = elem.firstElementChild;
-				}
-
-				return elem;
-			}).append( this );
-		}
-
-		return this;
-	},
-
-	wrapInner: function( html ) {
-		if ( jQuery.isFunction( html ) ) {
-			return this.each(function( i ) {
-				jQuery( this ).wrapInner( html.call(this, i) );
-			});
-		}
-
-		return this.each(function() {
-			var self = jQuery( this ),
-				contents = self.contents();
-
-			if ( contents.length ) {
-				contents.wrapAll( html );
-
-			} else {
-				self.append( html );
-			}
-		});
-	},
-
-	wrap: function( html ) {
-		var isFunction = jQuery.isFunction( html );
-
-		return this.each(function( i ) {
-			jQuery( this ).wrapAll( isFunction ? html.call(this, i) : html );
-		});
-	},
-
-	unwrap: function() {
-		return this.parent().each(function() {
-			if ( !jQuery.nodeName( this, "body" ) ) {
-				jQuery( this ).replaceWith( this.childNodes );
-			}
-		}).end();
-	}
-});
-
-
-jQuery.expr.filters.hidden = function( elem ) {
-	// Support: Opera <= 12.12
-	// Opera reports offsetWidths and offsetHeights less than zero on some elements
-	return elem.offsetWidth <= 0 && elem.offsetHeight <= 0;
-};
-jQuery.expr.filters.visible = function( elem ) {
-	return !jQuery.expr.filters.hidden( elem );
-};
-
-
-
-
-var r20 = /%20/g,
-	rbracket = /\[\]$/,
-	rCRLF = /\r?\n/g,
-	rsubmitterTypes = /^(?:submit|button|image|reset|file)$/i,
-	rsubmittable = /^(?:input|select|textarea|keygen)/i;
-
-function buildParams( prefix, obj, traditional, add ) {
-	var name;
-
-	if ( jQuery.isArray( obj ) ) {
-		// Serialize array item.
-		jQuery.each( obj, function( i, v ) {
-			if ( traditional || rbracket.test( prefix ) ) {
-				// Treat each array item as a scalar.
-				add( prefix, v );
-
-			} else {
-				// Item is non-scalar (array or object), encode its numeric index.
-				buildParams( prefix + "[" + ( typeof v === "object" ? i : "" ) + "]", v, traditional, add );
-			}
-		});
-
-	} else if ( !traditional && jQuery.type( obj ) === "object" ) {
-		// Serialize object item.
-		for ( name in obj ) {
-			buildParams( prefix + "[" + name + "]", obj[ name ], traditional, add );
-		}
-
-	} else {
-		// Serialize scalar item.
-		add( prefix, obj );
-	}
-}
-
-// Serialize an array of form elements or a set of
-// key/values into a query string
-jQuery.param = function( a, traditional ) {
-	var prefix,
-		s = [],
-		add = function( key, value ) {
-			// If value is a function, invoke it and return its value
-			value = jQuery.isFunction( value ) ? value() : ( value == null ? "" : value );
-			s[ s.length ] = encodeURIComponent( key ) + "=" + encodeURIComponent( value );
-		};
-
-	// Set traditional to true for jQuery <= 1.3.2 behavior.
-	if ( traditional === undefined ) {
-		traditional = jQuery.ajaxSettings && jQuery.ajaxSettings.traditional;
-	}
-
-	// If an array was passed in, assume that it is an array of form elements.
-	if ( jQuery.isArray( a ) || ( a.jquery && !jQuery.isPlainObject( a ) ) ) {
-		// Serialize the form elements
-		jQuery.each( a, function() {
-			add( this.name, this.value );
-		});
-
-	} else {
-		// If traditional, encode the "old" way (the way 1.3.2 or older
-		// did it), otherwise encode params recursively.
-		for ( prefix in a ) {
-			buildParams( prefix, a[ prefix ], traditional, add );
-		}
-	}
-
-	// Return the resulting serialization
-	return s.join( "&" ).replace( r20, "+" );
-};
-
-jQuery.fn.extend({
-	serialize: function() {
-		return jQuery.param( this.serializeArray() );
-	},
-	serializeArray: function() {
-		return this.map(function() {
-			// Can add propHook for "elements" to filter or add form elements
-			var elements = jQuery.prop( this, "elements" );
-			return elements ? jQuery.makeArray( elements ) : this;
-		})
-		.filter(function() {
-			var type = this.type;
-
-			// Use .is( ":disabled" ) so that fieldset[disabled] works
-			return this.name && !jQuery( this ).is( ":disabled" ) &&
-				rsubmittable.test( this.nodeName ) && !rsubmitterTypes.test( type ) &&
-				( this.checked || !rcheckableType.test( type ) );
-		})
-		.map(function( i, elem ) {
-			var val = jQuery( this ).val();
-
-			return val == null ?
-				null :
-				jQuery.isArray( val ) ?
-					jQuery.map( val, function( val ) {
-						return { name: elem.name, value: val.replace( rCRLF, "\r\n" ) };
-					}) :
-					{ name: elem.name, value: val.replace( rCRLF, "\r\n" ) };
-		}).get();
-	}
-});
-
-
-jQuery.ajaxSettings.xhr = function() {
-	try {
-		return new XMLHttpRequest();
-	} catch( e ) {}
-};
-
-var xhrId = 0,
-	xhrCallbacks = {},
-	xhrSuccessStatus = {
-		// file protocol always yields status code 0, assume 200
-		0: 200,
-		// Support: IE9
-		// #1450: sometimes IE returns 1223 when it should be 204
-		1223: 204
-	},
-	xhrSupported = jQuery.ajaxSettings.xhr();
-
-// Support: IE9
-// Open requests must be manually aborted on unload (#5280)
-if ( window.ActiveXObject ) {
-	jQuery( window ).on( "unload", function() {
-		for ( var key in xhrCallbacks ) {
-			xhrCallbacks[ key ]();
-		}
-	});
-}
-
-support.cors = !!xhrSupported && ( "withCredentials" in xhrSupported );
-support.ajax = xhrSupported = !!xhrSupported;
-
-jQuery.ajaxTransport(function( options ) {
-	var callback;
-
-	// Cross domain only allowed if supported through XMLHttpRequest
-	if ( support.cors || xhrSupported && !options.crossDomain ) {
-		return {
-			send: function( headers, complete ) {
-				var i,
-					xhr = options.xhr(),
-					id = ++xhrId;
-
-				xhr.open( options.type, options.url, options.async, options.username, options.password );
-
-				// Apply custom fields if provided
-				if ( options.xhrFields ) {
-					for ( i in options.xhrFields ) {
-						xhr[ i ] = options.xhrFields[ i ];
-					}
-				}
-
-				// Override mime type if needed
-				if ( options.mimeType && xhr.overrideMimeType ) {
-					xhr.overrideMimeType( options.mimeType );
-				}
-
-				// X-Requested-With header
-				// For cross-domain requests, seeing as conditions for a preflight are
-				// akin to a jigsaw puzzle, we simply never set it to be sure.
-				// (it can always be set on a per-request basis or even using ajaxSetup)
-				// For same-domain requests, won't change header if already provided.
-				if ( !options.crossDomain && !headers["X-Requested-With"] ) {
-					headers["X-Requested-With"] = "XMLHttpRequest";
-				}
-
-				// Set headers
-				for ( i in headers ) {
-					xhr.setRequestHeader( i, headers[ i ] );
-				}
-
-				// Callback
-				callback = function( type ) {
-					return function() {
-						if ( callback ) {
-							delete xhrCallbacks[ id ];
-							callback = xhr.onload = xhr.onerror = null;
-
-							if ( type === "abort" ) {
-								xhr.abort();
-							} else if ( type === "error" ) {
-								complete(
-									// file: protocol always yields status 0; see #8605, #14207
-									xhr.status,
-									xhr.statusText
-								);
-							} else {
-								complete(
-									xhrSuccessStatus[ xhr.status ] || xhr.status,
-									xhr.statusText,
-									// Support: IE9
-									// Accessing binary-data responseText throws an exception
-									// (#11426)
-									typeof xhr.responseText === "string" ? {
-										text: xhr.responseText
-									} : undefined,
-									xhr.getAllResponseHeaders()
-								);
-							}
-						}
-					};
-				};
-
-				// Listen to events
-				xhr.onload = callback();
-				xhr.onerror = callback("error");
-
-				// Create the abort callback
-				callback = xhrCallbacks[ id ] = callback("abort");
-
-				try {
-					// Do send the request (this may raise an exception)
-					xhr.send( options.hasContent && options.data || null );
-				} catch ( e ) {
-					// #14683: Only rethrow if this hasn't been notified as an error yet
-					if ( callback ) {
-						throw e;
-					}
-				}
-			},
-
-			abort: function() {
-				if ( callback ) {
-					callback();
-				}
-			}
-		};
-	}
-});
-
-
-
-
-// Install script dataType
-jQuery.ajaxSetup({
-	accepts: {
-		script: "text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"
-	},
-	contents: {
-		script: /(?:java|ecma)script/
-	},
-	converters: {
-		"text script": function( text ) {
-			jQuery.globalEval( text );
-			return text;
-		}
-	}
-});
-
-// Handle cache's special case and crossDomain
-jQuery.ajaxPrefilter( "script", function( s ) {
-	if ( s.cache === undefined ) {
-		s.cache = false;
-	}
-	if ( s.crossDomain ) {
-		s.type = "GET";
-	}
-});
-
-// Bind script tag hack transport
-jQuery.ajaxTransport( "script", function( s ) {
-	// This transport only deals with cross domain requests
-	if ( s.crossDomain ) {
-		var script, callback;
-		return {
-			send: function( _, complete ) {
-				script = jQuery("<script>").prop({
-					async: true,
-					charset: s.scriptCharset,
-					src: s.url
-				}).on(
-					"load error",
-					callback = function( evt ) {
-						script.remove();
-						callback = null;
-						if ( evt ) {
-							complete( evt.type === "error" ? 404 : 200, evt.type );
-						}
-					}
-				);
-				document.head.appendChild( script[ 0 ] );
-			},
-			abort: function() {
-				if ( callback ) {
-					callback();
-				}
-			}
-		};
-	}
-});
-
-
-
-
-var oldCallbacks = [],
-	rjsonp = /(=)\?(?=&|$)|\?\?/;
-
-// Default jsonp settings
-jQuery.ajaxSetup({
-	jsonp: "callback",
-	jsonpCallback: function() {
-		var callback = oldCallbacks.pop() || ( jQuery.expando + "_" + ( nonce++ ) );
-		this[ callback ] = true;
-		return callback;
-	}
-});
-
-// Detect, normalize options and install callbacks for jsonp requests
-jQuery.ajaxPrefilter( "json jsonp", function( s, originalSettings, jqXHR ) {
-
-	var callbackName, overwritten, responseContainer,
-		jsonProp = s.jsonp !== false && ( rjsonp.test( s.url ) ?
-			"url" :
-			typeof s.data === "string" && !( s.contentType || "" ).indexOf("application/x-www-form-urlencoded") && rjsonp.test( s.data ) && "data"
-		);
-
-	// Handle iff the expected data type is "jsonp" or we have a parameter to set
-	if ( jsonProp || s.dataTypes[ 0 ] === "jsonp" ) {
-
-		// Get callback name, remembering preexisting value associated with it
-		callbackName = s.jsonpCallback = jQuery.isFunction( s.jsonpCallback ) ?
-			s.jsonpCallback() :
-			s.jsonpCallback;
-
-		// Insert callback into url or form data
-		if ( jsonProp ) {
-			s[ jsonProp ] = s[ jsonProp ].replace( rjsonp, "$1" + callbackName );
-		} else if ( s.jsonp !== false ) {
-			s.url += ( rquery.test( s.url ) ? "&" : "?" ) + s.jsonp + "=" + callbackName;
-		}
-
-		// Use data converter to retrieve json after script execution
-		s.converters["script json"] = function() {
-			if ( !responseContainer ) {
-				jQuery.error( callbackName + " was not called" );
-			}
-			return responseContainer[ 0 ];
-		};
-
-		// force json dataType
-		s.dataTypes[ 0 ] = "json";
-
-		// Install callback
-		overwritten = window[ callbackName ];
-		window[ callbackName ] = function() {
-			responseContainer = arguments;
-		};
-
-		// Clean-up function (fires after converters)
-		jqXHR.always(function() {
-			// Restore preexisting value
-			window[ callbackName ] = overwritten;
-
-			// Save back as free
-			if ( s[ callbackName ] ) {
-				// make sure that re-using the options doesn't screw things around
-				s.jsonpCallback = originalSettings.jsonpCallback;
-
-				// save the callback name for future use
-				oldCallbacks.push( callbackName );
-			}
-
-			// Call if it was a function and we have a response
-			if ( responseContainer && jQuery.isFunction( overwritten ) ) {
-				overwritten( responseContainer[ 0 ] );
-			}
-
-			responseContainer = overwritten = undefined;
-		});
-
-		// Delegate to script
-		return "script";
-	}
-});
-
-
-
-
-// data: string of html
-// context (optional): If specified, the fragment will be created in this context, defaults to document
-// keepScripts (optional): If true, will include scripts passed in the html string
-jQuery.parseHTML = function( data, context, keepScripts ) {
-	if ( !data || typeof data !== "string" ) {
-		return null;
-	}
-	if ( typeof context === "boolean" ) {
-		keepScripts = context;
-		context = false;
-	}
-	context = context || document;
-
-	var parsed = rsingleTag.exec( data ),
-		scripts = !keepScripts && [];
-
-	// Single tag
-	if ( parsed ) {
-		return [ context.createElement( parsed[1] ) ];
-	}
-
-	parsed = jQuery.buildFragment( [ data ], context, scripts );
-
-	if ( scripts && scripts.length ) {
-		jQuery( scripts ).remove();
-	}
-
-	return jQuery.merge( [], parsed.childNodes );
-};
-
-
-// Keep a copy of the old load method
-var _load = jQuery.fn.load;
-
-/**
- * Load a url into a page
- */
-jQuery.fn.load = function( url, params, callback ) {
-	if ( typeof url !== "string" && _load ) {
-		return _load.apply( this, arguments );
-	}
-
-	var selector, type, response,
-		self = this,
-		off = url.indexOf(" ");
-
-	if ( off >= 0 ) {
-		selector = jQuery.trim( url.slice( off ) );
-		url = url.slice( 0, off );
-	}
-
-	// If it's a function
-	if ( jQuery.isFunction( params ) ) {
-
-		// We assume that it's the callback
-		callback = params;
-		params = undefined;
-
-	// Otherwise, build a param string
-	} else if ( params && typeof params === "object" ) {
-		type = "POST";
-	}
-
-	// If we have elements to modify, make the request
-	if ( self.length > 0 ) {
-		jQuery.ajax({
-			url: url,
-
-			// if "type" variable is undefined, then "GET" method will be used
-			type: type,
-			dataType: "html",
-			data: params
-		}).done(function( responseText ) {
-
-			// Save response for use in complete callback
-			response = arguments;
-
-			self.html( selector ?
-
-				// If a selector was specified, locate the right elements in a dummy div
-				// Exclude scripts to avoid IE 'Permission Denied' errors
-				jQuery("<div>").append( jQuery.parseHTML( responseText ) ).find( selector ) :
-
-				// Otherwise use the full result
-				responseText );
-
-		}).complete( callback && function( jqXHR, status ) {
-			self.each( callback, response || [ jqXHR.responseText, status, jqXHR ] );
-		});
-	}
-
-	return this;
-};
-
-
-
-
-jQuery.expr.filters.animated = function( elem ) {
-	return jQuery.grep(jQuery.timers, function( fn ) {
-		return elem === fn.elem;
-	}).length;
-};
-
-
-
-
-var docElem = window.document.documentElement;
-
-/**
- * Gets a window from an element
- */
-function getWindow( elem ) {
-	return jQuery.isWindow( elem ) ? elem : elem.nodeType === 9 && elem.defaultView;
-}
-
-jQuery.offset = {
-	setOffset: function( elem, options, i ) {
-		var curPosition, curLeft, curCSSTop, curTop, curOffset, curCSSLeft, calculatePosition,
-			position = jQuery.css( elem, "position" ),
-			curElem = jQuery( elem ),
-			props = {};
-
-		// Set position first, in-case top/left are set even on static elem
-		if ( position === "static" ) {
-			elem.style.position = "relative";
-		}
-
-		curOffset = curElem.offset();
-		curCSSTop = jQuery.css( elem, "top" );
-		curCSSLeft = jQuery.css( elem, "left" );
-		calculatePosition = ( position === "absolute" || position === "fixed" ) &&
-			( curCSSTop + curCSSLeft ).indexOf("auto") > -1;
-
-		// Need to be able to calculate position if either top or left is auto and position is either absolute or fixed
-		if ( calculatePosition ) {
-			curPosition = curElem.position();
-			curTop = curPosition.top;
-			curLeft = curPosition.left;
-
-		} else {
-			curTop = parseFloat( curCSSTop ) || 0;
-			curLeft = parseFloat( curCSSLeft ) || 0;
-		}
-
-		if ( jQuery.isFunction( options ) ) {
-			options = options.call( elem, i, curOffset );
-		}
-
-		if ( options.top != null ) {
-			props.top = ( options.top - curOffset.top ) + curTop;
-		}
-		if ( options.left != null ) {
-			props.left = ( options.left - curOffset.left ) + curLeft;
-		}
-
-		if ( "using" in options ) {
-			options.using.call( elem, props );
-
-		} else {
-			curElem.css( props );
-		}
-	}
-};
-
-jQuery.fn.extend({
-	offset: function( options ) {
-		if ( arguments.length ) {
-			return options === undefined ?
-				this :
-				this.each(function( i ) {
-					jQuery.offset.setOffset( this, options, i );
-				});
-		}
-
-		var docElem, win,
-			elem = this[ 0 ],
-			box = { top: 0, left: 0 },
-			doc = elem && elem.ownerDocument;
-
-		if ( !doc ) {
-			return;
-		}
-
-		docElem = doc.documentElement;
-
-		// Make sure it's not a disconnected DOM node
-		if ( !jQuery.contains( docElem, elem ) ) {
-			return box;
-		}
-
-		// If we don't have gBCR, just use 0,0 rather than error
-		// BlackBerry 5, iOS 3 (original iPhone)
-		if ( typeof elem.getBoundingClientRect !== strundefined ) {
-			box = elem.getBoundingClientRect();
-		}
-		win = getWindow( doc );
-		return {
-			top: box.top + win.pageYOffset - docElem.clientTop,
-			left: box.left + win.pageXOffset - docElem.clientLeft
-		};
-	},
-
-	position: function() {
-		if ( !this[ 0 ] ) {
-			return;
-		}
-
-		var offsetParent, offset,
-			elem = this[ 0 ],
-			parentOffset = { top: 0, left: 0 };
-
-		// Fixed elements are offset from window (parentOffset = {top:0, left: 0}, because it is its only offset parent
-		if ( jQuery.css( elem, "position" ) === "fixed" ) {
-			// We assume that getBoundingClientRect is available when computed position is fixed
-			offset = elem.getBoundingClientRect();
-
-		} else {
-			// Get *real* offsetParent
-			offsetParent = this.offsetParent();
-
-			// Get correct offsets
-			offset = this.offset();
-			if ( !jQuery.nodeName( offsetParent[ 0 ], "html" ) ) {
-				parentOffset = offsetParent.offset();
-			}
-
-			// Add offsetParent borders
-			parentOffset.top += jQuery.css( offsetParent[ 0 ], "borderTopWidth", true );
-			parentOffset.left += jQuery.css( offsetParent[ 0 ], "borderLeftWidth", true );
-		}
-
-		// Subtract parent offsets and element margins
-		return {
-			top: offset.top - parentOffset.top - jQuery.css( elem, "marginTop", true ),
-			left: offset.left - parentOffset.left - jQuery.css( elem, "marginLeft", true )
-		};
-	},
-
-	offsetParent: function() {
-		return this.map(function() {
-			var offsetParent = this.offsetParent || docElem;
-
-			while ( offsetParent && ( !jQuery.nodeName( offsetParent, "html" ) && jQuery.css( offsetParent, "position" ) === "static" ) ) {
-				offsetParent = offsetParent.offsetParent;
-			}
-
-			return offsetParent || docElem;
-		});
-	}
-});
-
-// Create scrollLeft and scrollTop methods
-jQuery.each( { scrollLeft: "pageXOffset", scrollTop: "pageYOffset" }, function( method, prop ) {
-	var top = "pageYOffset" === prop;
-
-	jQuery.fn[ method ] = function( val ) {
-		return access( this, function( elem, method, val ) {
-			var win = getWindow( elem );
-
-			if ( val === undefined ) {
-				return win ? win[ prop ] : elem[ method ];
-			}
-
-			if ( win ) {
-				win.scrollTo(
-					!top ? val : window.pageXOffset,
-					top ? val : window.pageYOffset
-				);
-
-			} else {
-				elem[ method ] = val;
-			}
-		}, method, val, arguments.length, null );
-	};
-});
-
-// Add the top/left cssHooks using jQuery.fn.position
-// Webkit bug: https://bugs.webkit.org/show_bug.cgi?id=29084
-// getComputedStyle returns percent when specified for top/left/bottom/right
-// rather than make the css module depend on the offset module, we just check for it here
-jQuery.each( [ "top", "left" ], function( i, prop ) {
-	jQuery.cssHooks[ prop ] = addGetHookIf( support.pixelPosition,
-		function( elem, computed ) {
-			if ( computed ) {
-				computed = curCSS( elem, prop );
-				// if curCSS returns percentage, fallback to offset
-				return rnumnonpx.test( computed ) ?
-					jQuery( elem ).position()[ prop ] + "px" :
-					computed;
-			}
-		}
-	);
-});
-
-
-// Create innerHeight, innerWidth, height, width, outerHeight and outerWidth methods
-jQuery.each( { Height: "height", Width: "width" }, function( name, type ) {
-	jQuery.each( { padding: "inner" + name, content: type, "": "outer" + name }, function( defaultExtra, funcName ) {
-		// margin is only for outerHeight, outerWidth
-		jQuery.fn[ funcName ] = function( margin, value ) {
-			var chainable = arguments.length && ( defaultExtra || typeof margin !== "boolean" ),
-				extra = defaultExtra || ( margin === true || value === true ? "margin" : "border" );
-
-			return access( this, function( elem, type, value ) {
-				var doc;
-
-				if ( jQuery.isWindow( elem ) ) {
-					// As of 5/8/2012 this will yield incorrect results for Mobile Safari, but there
-					// isn't a whole lot we can do. See pull request at this URL for discussion:
-					// https://github.com/jquery/jquery/pull/764
-					return elem.document.documentElement[ "client" + name ];
-				}
-
-				// Get document width or height
-				if ( elem.nodeType === 9 ) {
-					doc = elem.documentElement;
-
-					// Either scroll[Width/Height] or offset[Width/Height] or client[Width/Height],
-					// whichever is greatest
-					return Math.max(
-						elem.body[ "scroll" + name ], doc[ "scroll" + name ],
-						elem.body[ "offset" + name ], doc[ "offset" + name ],
-						doc[ "client" + name ]
-					);
-				}
-
-				return value === undefined ?
-					// Get width or height on the element, requesting but not forcing parseFloat
-					jQuery.css( elem, type, extra ) :
-
-					// Set width or height on the element
-					jQuery.style( elem, type, value, extra );
-			}, type, chainable ? margin : undefined, chainable, null );
-		};
-	});
-});
-
-
-// The number of elements contained in the matched element set
-jQuery.fn.size = function() {
-	return this.length;
-};
-
-jQuery.fn.andSelf = jQuery.fn.addBack;
-
-
-
-
-// Register as a named AMD module, since jQuery can be concatenated with other
-// files that may use define, but not via a proper concatenation script that
-// understands anonymous AMD modules. A named AMD is safest and most robust
-// way to register. Lowercase jquery is used because AMD module names are
-// derived from file names, and jQuery is normally delivered in a lowercase
-// file name. Do this after creating the global so that if an AMD module wants
-// to call noConflict to hide this version of jQuery, it will work.
-
-// Note that for maximum portability, libraries that are not jQuery should
-// declare themselves as anonymous modules, and avoid setting a global if an
-// AMD loader is present. jQuery is a special case. For more information, see
-// https://github.com/jrburke/requirejs/wiki/Updating-existing-libraries#wiki-anon
-
-if ( typeof define === "function" && define.amd ) {
-	define( "jquery", [], function() {
-		return jQuery;
-	});
-}
-
-
-
-
-var
-	// Map over jQuery in case of overwrite
-	_jQuery = window.jQuery,
-
-	// Map over the $ in case of overwrite
-	_$ = window.$;
-
-jQuery.noConflict = function( deep ) {
-	if ( window.$ === jQuery ) {
-		window.$ = _$;
-	}
-
-	if ( deep && window.jQuery === jQuery ) {
-		window.jQuery = _jQuery;
-	}
-
-	return jQuery;
-};
-
-// Expose jQuery and $ identifiers, even in
-// AMD (#7102#comment:10, https://github.com/jquery/jquery/pull/557)
-// and CommonJS for browser emulators (#13566)
-if ( typeof noGlobal === strundefined ) {
-	window.jQuery = window.$ = jQuery;
-}
-
-
-
-
-return jQuery;
-
-}));
-
-},{}],2:[function(require,module,exports){
-/*!
- * Bootlint - an HTML linter for Bootstrap projects
- * https://github.com/twbs/bootlint
- * Copyright (c) 2014 Christopher Rebert
- * Licensed under the MIT License.
- */
-
-/*eslint-env node */
-
-var cheerio = require('cheerio');
-
-(function (exports) {
-    'use strict';
-    var NUM_COLS = 12;
-    var COL_REGEX = /\bcol-(xs|sm|md|lg)-(\d{0,2})\b/;
-    var COL_REGEX_G = /\bcol-(xs|sm|md|lg)-(\d{0,2})\b/g;
-    var COL_CLASSES = [];
-    var SCREENS = ['xs', 'sm', 'md', 'lg'];
-    SCREENS.forEach(function (screen) {
-        for (var n = 1; n <= NUM_COLS; n++) {
-            COL_CLASSES.push('.col-' + screen + '-' + n);
-        }
-    });
-    var SCREEN2NUM = {
-        'xs': 0,
-        'sm': 1,
-        'md': 2,
-        'lg': 3
-    };
-    var NUM2SCREEN = ['xs', 'sm', 'md', 'lg'];
-    var IN_NODE_JS = !!(cheerio.load);
-
-    function compareNums(a, b) {
-        return a - b;
-    }
-
-    function isDoctype(node) {
-        return node.type === 'directive' && node.name === '!doctype';
-    }
-
-    function filenameFromUrl(url) {
-        var filename = url.replace(/[#?].*$/, ''); // strip querystring & fragment ID
-        var lastSlash = filename.lastIndexOf('/');
-        if (lastSlash !== -1) {
-            filename = filename.slice(lastSlash + 1);
-        }
-        return filename;
-    }
-
-    function withoutClass(classes, klass) {
-        return classes.replace(new RegExp('\\b' + klass + '\\b', 'g'), '');
-    }
-
-    function columnClassKey(colClass) {
-        return SCREEN2NUM[COL_REGEX.exec(colClass)[1]];
-    }
-
-    function compareColumnClasses(a, b) {
-        return columnClassKey(a) - columnClassKey(b);
-    }
-
-    /**
-     * Moves any grid column classes to the end of the class string and sorts the grid classes by ascending screen size.
-     * @param {string} classes The "class" attribute of a DOM node
-     * @returns {string}
-     */
-    function sortedColumnClasses(classes) {
-        // extract column classes
-        var colClasses = [];
-        while (true) {
-            var match = COL_REGEX.exec(classes);
-            if (!match) {
-                break;
-            }
-            var colClass = match[0];
-            colClasses.push(colClass);
-            classes = withoutClass(classes, colClass);
-        }
-
-        colClasses.sort(compareColumnClasses);
-        return classes + ' ' + colClasses.join(' ');
-    }
-
-    /**
-     * @param {string} classes The "class" attribute of a DOM node
-     * @returns {Object.<string, integer[]>} Object mapping grid column widths (1 thru 12) to sorted arrays of screen size numbers (see SCREEN2NUM)
-     *      Widths not used in the classes will not have an entry in the object.
-     */
-    function width2screensFor(classes) {
-        var width = null;
-        var width2screens = {};
-        while (true) {
-            var match = COL_REGEX_G.exec(classes);
-            if (!match) {
-                break;
-            }
-            var screen = match[1];
-            width = match[2];
-            var screens = width2screens[width];
-            if (!screens) {
-                screens = width2screens[width] = [];
-            }
-            screens.push(SCREEN2NUM[screen]);
-        }
-
-        for (width in width2screens) {
-            if (width2screens.hasOwnProperty(width)) {
-                width2screens[width].sort(compareNums);
-            }
-        }
-
-        return width2screens;
-    }
-
-    /**
-     * Given a sorted array of integers, this finds all contiguous runs where each item is incremented by 1 from the next.
-     * For example:
-     *      [0, 2, 3, 5] has one such run: [2, 3]
-     *      [0, 2, 3, 4, 6, 8, 9, 11] has two such runs: [2, 3, 4], [8, 9]
-     *      [0, 2, 4] has no runs.
-     * @param {integer[]} list Sorted array of integers
-     * @returns {integer[][]} Array of pairs of start and end values of runs
-     */
-    function incrementingRunsFrom(list) {
-        list = list.concat([Infinity]);// use Infinity to ensure any nontrivial (length >= 2) run ends before the end of the loop
-        var runs = [];
-        var start = null;
-        var prev = null;
-        for (var i = 0; i < list.length; i++) {
-            var current = list[i];
-            if (start === null) {
-                // first element starts a trivial run
-                start = current;
-            }
-            else if (prev + 1 !== current) {
-                // run ended
-                if (start !== prev) {
-                    // run is nontrivial
-                    runs.push([start, prev]);
-                }
-                // start new run
-                start = current;
-            }
-            // else: the run continues
-
-            prev = current;
-        }
-        return runs;
-    }
-
-    function LintError(id, message) {
-        this.id = id;
-        this.message = message;
-    }
-    exports.LintError = LintError;
-
-    function LintWarning(id, message) {
-        this.id = id;
-        this.message = message;
-    }
-    exports.LintWarning = LintWarning;
-
-    var allLinters = {};
-    function addLinter(id, linter) {
-        if (allLinters[id]) {
-            throw new Error("Linter already registered with ID: " + id);
-        }
-
-        var Problem = null;
-        if (id[0] === 'E') {
-            Problem = LintError;
-        }
-        else if (id[0] === 'W') {
-            Problem = LintWarning;
-        }
-        else {
-            throw new Error("Invalid linter ID: " + id);
-        }
-
-        function linterWrapper($, reporter) {
-            function specializedReporter(message) {
-                reporter(new Problem(id, message));
-            }
-
-            linter($, specializedReporter);
-        }
-
-        linterWrapper.id = id;
-        allLinters[id] = linterWrapper;
-    }
-
-
-    addLinter("E001", (function () {
-        var MISSING_DOCTYPE = "Document is missing a DOCTYPE declaration";
-        var NON_HTML5_DOCTYPE = "Document declares a non-HTML5 DOCTYPE";
-        if (IN_NODE_JS) {
-            return function lintDoctype($, reporter) {
-                var doctype = $(':root')[0];
-                while (doctype && !isDoctype(doctype)) {
-                    doctype = doctype.prev;
-                }
-                if (!doctype) {
-                    reporter(MISSING_DOCTYPE);
-                    return;
-                }
-                var doctypeId = doctype.data.toLowerCase();
-                if (doctypeId !== '!doctype html' && doctypeId !== '!doctype html system "about:legacy-compat"') {
-                    reporter(NON_HTML5_DOCTYPE);
-                }
-            };
-        }
-        else {
-            return function lintDoctype($, reporter) {
-                /*eslint-disable no-undef, block-scoped-var */
-                var doc = window.document;
-                /*eslint-enable un-undef, block-scoped-var */
-                if (doc.doctype === null) {
-                    reporter(MISSING_DOCTYPE);
-                }
-                else if (doc.doctype.publicId) {
-                    reporter(NON_HTML5_DOCTYPE);
-                }
-                else if (doc.doctype.systemId && doc.doctype.systemId !== "about:legacy-compat") {
-                    reporter(NON_HTML5_DOCTYPE);
-                }
-            };
-        }
-    })());
-    addLinter("W001", function lintMetaCharsetUtf8($, reporter) {
-        var meta = $('head>meta[charset]');
-        var charset = meta.attr('charset');
-        if (!charset) {
-            reporter('<head> is missing UTF-8 charset <meta> tag');
-        }
-        else if (charset.toLowerCase() !== "utf-8") {
-            reporter('charset <meta> tag is specifying a legacy, non-UTF-8 charset');
-        }
-    });
-    addLinter("W002", function lintXUaCompatible($, reporter) {
-        var meta = $('head>meta[http-equiv="X-UA-Compatible"][content="IE=edge"]');
-        if (!meta.length) {
-            reporter("<head> is missing X-UA-Compatible <meta> tag that disables old IE compatibility modes");
-        }
-    });
-    addLinter("W003", function lintViewport($, reporter) {
-        var meta = $('head>meta[name="viewport"][content]');
-        if (!meta.length) {
-            reporter("<head> is missing viewport <meta> tag that enables responsiveness");
-        }
-    });
-    addLinter("E002", function lintBootstrapv2($, reporter) {
-        var columnClasses = [];
-        for (var n = 1; n <= 12; n++) {
-            columnClasses.push('.span' + n);
-        }
-        var selector = columnClasses.join(',');
-        var spanNs = $(selector);
-        if (spanNs.length) {
-            reporter("Found one or more uses of outdated Bootstrap v2 `.spanN` grid classes");
-        }
-    });
-    addLinter("E003", function lintContainers($, reporter) {
-        var notAnyColClass = COL_CLASSES.map(function (colClass) {
-            return ':not(' + colClass + ')';
-        }).join('');
-        var selector = '*' + notAnyColClass + '>.row';
-        var rowsOutsideColumns = $(selector);
-        var rowsOutsideColumnsAndContainers = rowsOutsideColumns.filter(function () {
-            var parent = $(this).parent();
-            while (parent.length) {
-                if (parent.is('.container, .container-fluid')) {
-                    return false;
-                }
-                parent = $(parent).parent();
-            }
-            return true;
-        });
-        if (rowsOutsideColumnsAndContainers.length) {
-            reporter("Found one or more `.row`s that were not children of a grid column or descendants of a `.container` or `.container-fluid`");
-        }
-    });
-    addLinter("E004", function lintNestedContainers($, reporter) {
-        var nestedContainers = $('.container, .container-fluid').children('.container, .container-fluid');
-        if (nestedContainers.length) {
-            reporter("Containers (`.container` and `.container-fluid`) are not nestable");
-        }
-    });
-    addLinter("E005", function lintRowAndColOnSameElem($, reporter) {
-        var selector = COL_CLASSES.map(function (col) {
-            return ".row" + col;
-        }).join(',');
-
-        var rowCols = $(selector);
-        if (rowCols.length) {
-            reporter("Found both `.row` and `.col-*-*` used on the same element");
-        }
-    });
-    addLinter("W004", function lintRemoteModals($, reporter) {
-        var remoteModalTriggers = $('[data-toggle="modal"][data-remote]');
-        if (remoteModalTriggers.length) {
-            reporter("Found one or more modals using the deprecated `remote` option");
-        }
-    });
-    addLinter("W005", function lintJquery($, reporter) {
-        var theWindow = null;
-        try {
-            /*eslint-disable no-undef, block-scoped-var */
-            theWindow = window;
-            /*eslint-enable no-undef, block-scoped-var */
-        }
-        catch (e) {
-            // deliberately do nothing
-        }
-        if (theWindow && (theWindow.$ || theWindow.jQuery)) {
-            return;
-        }
-        var jqueries = $('script[src*="jquery"],script[src*="jQuery"]');
-        if (!jqueries.length) {
-            reporter("Unable to locate jQuery, which is required for Bootstrap's JavaScript plugins to work");
-        }
-    });
-    addLinter("E006", function lintInputGroupFormControlTypes($, reporter) {
-        var selectInputGroups = $('.input-group select');
-        if (selectInputGroups.length) {
-            reporter("`.input-group` contains a <select>; this should be avoided as <select>s cannot be fully styled in WebKit browsers");
-        }
-        var textareaInputGroups = $('.input-group textarea');
-        if (textareaInputGroups.length) {
-            reporter("`.input-group` contains a <textarea>; only text-based <input>s are permitted in an `.input-group`");
-        }
-    });
-    addLinter("E007", function lintBootstrapJs($, reporter) {
-        var longhands = $('script[src*="bootstrap.js"]').filter(function (i, script) {
-            var url = $(script).attr('src');
-            var filename = filenameFromUrl(url);
-            return filename === "bootstrap.js";
-        });
-        if (!longhands.length) {
-            return;
-        }
-        var minifieds = $('script[src*="bootstrap.min.js"]').filter(function (i, script) {
-            var url = $(script).attr('src');
-            var filename = filenameFromUrl(url);
-            return filename === "bootstrap.min.js";
-        });
-        if (!minifieds.length) {
-            return;
-        }
-
-        reporter("Only one copy of Bootstrap's JS should be included; currently the webpage includes both bootstrap.js and bootstrap.min.js");
-    });
-    addLinter("W006", function lintTooltipsOnDisabledElems($, reporter) {
-        var selector = [
-            '[disabled][data-toggle="tooltip"]',
-            '.disabled[data-toggle="tooltip"]',
-            '[disabled][data-toggle="popover"]',
-            '.disabled[data-toggle="popover"]'
-        ].join(',');
-        var disabledWithTooltips = $(selector);
-        if (disabledWithTooltips.length) {
-            reporter(
-                "Tooltips and popovers on disabled elements cannot be triggered by user interaction unless the element becomes enabled." +
-                " To have tooltips and popovers be triggerable by the user even when their associated element is disabled," +
-                " put the disabled element inside a wrapper <div> and apply the tooltip or popover to the wrapper <div> instead."
-            );
-        }
-    });
-    addLinter("E008", function lintTooltipsInBtnGroups($, reporter) {
-        var nonBodyContainers = $('.btn-group [data-toggle="tooltip"]:not([data-container="body"]), .btn-group [data-toggle="popover"]:not([data-container="body"])');
-        if (nonBodyContainers.length) {
-            reporter("Tooltips and popovers within button groups should have their `container` set to 'body'. Found tooltips/popovers that might lack this setting.");
-        }
-    });
-    addLinter("E009", function lintMissingInputGroupSizes($, reporter) {
-        var selector = [
-            '.input-group:not(.input-group-lg) .btn-lg',
-            '.input-group:not(.input-group-lg) .input-lg',
-            '.input-group:not(.input-group-sm) .btn-sm',
-            '.input-group:not(.input-group-sm) .input-sm'
-        ].join(',');
-        var badInputGroupSizing = $(selector);
-        if (badInputGroupSizing.length) {
-            reporter("Button and input sizing within `.input-group`s can cause issues. Instead, use input group sizing classes `.input-group-lg` or `.input-group-sm`");
-        }
-    });
-    addLinter("E010", function lintMultipleFormControlsInInputGroup($, reporter) {
-        var badInputGroups = $('.input-group').filter(function (i, inputGroup) {
-            return $(inputGroup).find('.form-control').length > 1;
-        });
-        if (badInputGroups.length) {
-            reporter("Input groups cannot contain multiple `.form-control`s");
-        }
-    });
-    addLinter("E011", function lintFormGroupMixedWithInputGroup($, reporter) {
-        var badMixes = $('.input-group.form-group');
-        if (badMixes.length) {
-            reporter(".input-group and .form-group cannot be used directly on the same element. Instead, nest the .input-group within the .form-group");
-        }
-    });
-    addLinter("E012", function lintGridClassMixedWithInputGroup($, reporter) {
-        var selector = COL_CLASSES.map(function (colClass) {
-            return '.input-group' + colClass;
-        }).join(',');
-
-        var badMixes = $(selector);
-        if (badMixes.length) {
-            reporter(".input-group and .col-*-* cannot be used directly on the same element. Instead, nest the .input-group within the .col-*-*");
-        }
-    });
-    addLinter("E013", function lintRowChildrenAreCols($, reporter) {
-        var ALLOWED_CHILD_CLASSES = COL_CLASSES.concat(['.clearfix', '.bs-customizer-input']);
-        var selector = '.row>*' + ALLOWED_CHILD_CLASSES.map(function (colClass) {
-            return ':not(' + colClass + ')';
-        }).join('');
-
-        var nonColRowChildren = $(selector);
-        if (nonColRowChildren.length) {
-            reporter("Only columns (.col-*-*) may be children of `.row`s");
-        }
-    });
-    addLinter("E014", function lintColParentsAreRowsOrFormGroups($, reporter) {
-        var selector = COL_CLASSES.map(function (colClass) {
-            return '*:not(.row):not(.form-group)>' + colClass + ':not(col):not(th):not(td)';
-        }).join(',');
-
-        var colsOutsideRowsAndFormGroups = $(selector);
-        if (colsOutsideRowsAndFormGroups.length) {
-            reporter("Columns (.col-*-*) can only be children of `.row`s or `.form-group`s");
-        }
-    });
-    addLinter("E015", function lintInputGroupsWithMultipleAddOnsPerSide($, reporter) {
-        var addOnClasses = ['.input-group-addon', '.input-group-btn'];
-        var combos = [];
-        addOnClasses.forEach(function (first) {
-            addOnClasses.forEach(function (second) {
-                combos.push('.input-group>' + first + '+' + second);
-            });
-        });
-        var selector = combos.join(',');
-        var multipleAddOns = $(selector);
-        if (multipleAddOns.length) {
-            reporter("Having multiple add-ons on a single side of an input group is not supported");
-        }
-    });
-    addLinter("E016", function lintBtnToggle($, reporter) {
-        var badBtnToggle = $('.btn.dropdown-toggle ~ .btn');
-        if (badBtnToggle.length) {
-            reporter("`.btn.dropdown-toggle` must be the last button in a button group.");
-        }
-    });
-    addLinter("W007", function lintBtnType($, reporter) {
-        var badBtnType = $('button:not([type="submit"], [type="reset"], [type="button"])');
-        if (badBtnType.length) {
-            reporter("Always set a `type` on `<button>`s.");
-        }
-    });
-    addLinter("E017", function lintBlockCheckboxes($, reporter) {
-        var badCheckboxes = $('.checkbox').filter(function (i, div) {
-            return $(div).filter(':has(>label>input[type="checkbox"])').length <= 0;
-        });
-        if (badCheckboxes.length) {
-            reporter('Incorrect markup used with the `.checkbox` class. The correct markup structure is .checkbox>label>input[type="checkbox"]');
-        }
-    });
-    addLinter("E018", function lintBlockRadios($, reporter) {
-        var badRadios = $('.radio').filter(function (i, div) {
-            return $(div).filter(':has(>label>input[type="radio"])').length <= 0;
-        });
-        if (badRadios.length) {
-            reporter('Incorrect markup used with the `.radio` class. The correct markup structure is .radio>label>input[type="radio"]');
-        }
-    });
-    addLinter("E019", function lintInlineCheckboxes($, reporter) {
-        var wrongElems = $('.checkbox-inline:not(label)');
-        if (wrongElems.length) {
-            reporter(".checkbox-inline should only be used on <label> elements");
-        }
-        var badStructures = $('.checkbox-inline').filter(function (i, label) {
-            return $(label).children('input[type="checkbox"]').length <= 0;
-        });
-        if (badStructures.length) {
-            reporter('Incorrect markup used with the `.checkbox-inline` class. The correct markup structure is label.checkbox-inline>input[type="checkbox"]');
-        }
-    });
-    addLinter("E020", function lintInlineRadios($, reporter) {
-        var wrongElems = $('.radio-inline:not(label)');
-        if (wrongElems.length) {
-            reporter(".radio-inline should only be used on <label> elements");
-        }
-        var badStructures = $('.radio-inline').filter(function (i, label) {
-            return $(label).children('input[type="radio"]').length <= 0;
-        });
-        if (badStructures.length) {
-            reporter('Incorrect markup used with the `.radio-inline` class. The correct markup structure is label.radio-inline>input[type="radio"]');
-        }
-    });
-    addLinter("E021", function lintButtonsCheckedActive($, reporter) {
-        var selector = [
-            '[data-toggle="buttons"]>label:not(.active)>input[type="checkbox"][checked]',
-            '[data-toggle="buttons"]>label.active>input[type="checkbox"]:not([checked])',
-            '[data-toggle="buttons"]>label:not(.active)>input[type="radio"][checked]',
-            '[data-toggle="buttons"]>label.active>input[type="radio"]:not([checked])'
-        ].join(',');
-        var mismatchedButtonInputs = $(selector);
-        if (mismatchedButtonInputs.length) {
-            reporter(".active class used without the `checked` attribute (or vice-versa) in a button group using the button.js plugin");
-        }
-    });
-    addLinter("E022", function lintModalsWithinOtherComponents($, reporter) {
-        var badNestings = $('.table .modal');
-        if (badNestings.length) {
-            reporter("Modal markup should not be placed within other components, so as to avoid the component's styles interfering with the modal's appearance or functionality");
-        }
-    });
-    addLinter("E023", function lintPanelBodyWithoutPanel($, reporter) {
-        var badPanelBody = $('.panel-body').parent(':not(.panel, .panel-collapse)');
-        if (badPanelBody.length) {
-            reporter("`.panel-body` must have a `.panel` or `.panel-collapse` parent");
-        }
-    });
-    addLinter("E024", function lintPanelHeadingWithoutPanel($, reporter) {
-        var badPanelHeading = $('.panel-heading').parent(':not(.panel)');
-        if (badPanelHeading.length) {
-            reporter("`.panel-heading` must have a `.panel` parent");
-        }
-    });
-    addLinter("E025", function lintPanelFooterWithoutPanel($, reporter) {
-        var badPanelFooter = $('.panel-footer').parent(':not(.panel, .panel-collapse)');
-        if (badPanelFooter.length) {
-            reporter("`.panel-footer` must have a `.panel` or `.panel-collapse` parent");
-        }
-    });
-    addLinter("E026", function lintPanelTitleWithoutPanelHeading($, reporter) {
-        var badPanelTitle = $('.panel-title').parent(':not(.panel-heading)');
-        if (badPanelTitle.length) {
-            reporter("`.panel-title` must have a `.panel-heading` parent");
-        }
-    });
-    addLinter("E027", function lintTableResponsive($, reporter) {
-        var badStructure = $('.table.table-responsive,table.table-responsive');
-        if (badStructure.length) {
-            reporter("`.table-responsive` is supposed to be used on the table's parent wrapper <div>, not on the table itself");
-        }
-    });
-    addLinter("E028", function lintFormControlFeedbackWithoutHasFeedback($, reporter) {
-        var ancestorsMissingClasses = $('.form-control-feedback').filter(function () {
-            return $(this).closest('.form-group.has-feedback').length !== 1;
-        });
-        if (ancestorsMissingClasses.length) {
-            reporter("`.form-control-feedback` must have a `.form-group.has-feedback` ancestor");
-        }
-    });
-    addLinter("E029", function lintRedundantColumnClasses($, reporter) {
-        var columns = $(COL_CLASSES.join(','));
-        columns.each(function (_index, column) {
-            var classes = $(column).attr('class');
-            var simplifiedClasses = classes;
-            var width2screens = width2screensFor(classes);
-            var isRedundant = false;
-            for (var width = 1; width <= NUM_COLS; width++) {
-                var screens = width2screens[width];
-                if (!screens) {
-                    continue;
-                }
-                var runs = incrementingRunsFrom(screens);
-                if (!runs.length) {
-                    continue;
-                }
-
-                isRedundant = true;
-
-                for (var i = 0; i < runs.length; i++) {
-                    var run = runs[i];
-                    var min = run[0];
-                    var max = run[1];
-
-                    // remove redundant classes
-                    for (var screenNum = min + 1; screenNum <= max; screenNum++) {
-                        var colClass = 'col-' + NUM2SCREEN[screenNum] + '-' + width;
-                        simplifiedClasses = withoutClass(simplifiedClasses, colClass);
-                    }
-                }
-            }
-            if (!isRedundant) {
-                return;
-            }
-
-            simplifiedClasses = sortedColumnClasses(simplifiedClasses);
-            simplifiedClasses = simplifiedClasses.replace(/ {2,}/g, ' ').trim();
-            var oldClass = 'class="' + classes + '"';
-            var newClass = 'class="' + simplifiedClasses + '"';
-            reporter(
-                "Since grid classes apply to devices with screen widths greater than or equal to the breakpoint sizes (unless overridden by grid classes targeting larger screens), " +
-                oldClass + " is redundant and can be simplified to " + newClass
-            );
-        });
-    });
-    addLinter("E030", function lintSoloGlyphiconClasses($, reporter) {
-        var missingGlyphiconClass = $('[class*="glyphicon-"]:not(.glyphicon):not(.glyphicon-class)').filter(function () {
-            return /\bglyphicon-([a-zA-Z]+)\b/.test($(this).attr('class'));
-        });
-        if (missingGlyphiconClass.length) {
-            reporter("Found elements with a .glyphicon-* class that were missing the additional required .glyphicon class.");
-        }
-    });
-    addLinter("E031", function lintGlyphiconOnNonEmptyElement($, reporter) {
-        if ($('.glyphicon:not(:empty)').length) {
-            reporter("Glyphicon classes must only be used on elements that contain no text content and have no child elements.");
-        }
-    });
-    addLinter("E032", function lintModalStructure($, reporter) {
-        if ($('.modal-dialog').parent(':not(.modal)').length) {
-            reporter(".modal-dialog must be a child of .modal");
-        }
-        if ($('.modal-content').parent(':not(.modal-dialog)').length) {
-            reporter(".modal-content must be a child of .modal-dialog");
-        }
-
-        if ($('.modal-header').parent(':not(.modal-content)').length) {
-            reporter(".modal-header must be a child of .modal-content");
-        }
-        if ($('.modal-body').parent(':not(.modal-content)').length) {
-            reporter(".modal-body must be a child of .modal-content");
-        }
-        if ($('.modal-footer').parent(':not(.modal-content)').length) {
-            reporter(".modal-footer must be a child of .modal-content");
-        }
-
-        if ($('.modal-title').parent(':not(.modal-header)').length) {
-            reporter(".modal-title must be a child of .modal-header");
-        }
-    });
-
-    exports._lint = function ($, reporter, disabledIdList) {
-        var disabledIdSet = {};
-        disabledIdList.forEach(function (disabledId) {
-            disabledIdSet[disabledId] = true;
-        });
-        Object.keys(allLinters).sort().forEach(function (linterId) {
-            if (!disabledIdSet[linterId]) {
-                allLinters[linterId]($, reporter);
-            }
-        });
-    };
-    if (IN_NODE_JS) {
-        // cheerio; Node.js
-        /**
-         * Lints the given HTML.
-         * @param {string} html The HTML to lint
-         * @param reporter Function to call with each lint problem
-         * @param {string[]} disabledIds Array of string IDs of linters to disable
-         * @returns {undefined} Nothing
-         */
-        exports.lintHtml = function (html, reporter, disabledIds) {
-            var $ = cheerio.load(html);
-            this._lint($, reporter, disabledIds);
-        };
-    }
-    else {
-        // jQuery; in-browser
-        (function () {
-            var $ = cheerio;
-            /**
-             * Lints the HTML of the current document.
-             * @param reporter Function to call with each lint problem
-             * @param {string[]} disabledIds Array of string IDs of linters to disable
-             * @returns {undefined} Nothing
-             */
-            exports.lintCurrentDocument = function (reporter, disabledIds) {
-                this._lint($, reporter, disabledIds);
-            };
-            /**
-             * Lints the HTML of the current document.
-             * If there are any lint warnings, one general notification message will be window.alert()-ed to the user.
-             * Each warning will be output individually using console.warn().
-             * @param {string[]} disabledIds Array of string IDs of linters to disable
-             * @returns {undefined} Nothing
-             */
-            exports.showLintReportForCurrentDocument = function (disabledIds) {
-                var seenLint = false;
-                var reporter = function (lint) {
-                    if (!seenLint) {
-                        /*eslint-disable no-alert, no-undef, block-scoped-var */
-                        window.alert("bootlint found errors in this document! See the JavaScript console for details.");
-                        /*eslint-enable no-alert, no-undef, block-scoped-var */
-                        seenLint = true;
-                    }
-                    console.warn("bootlint:", lint.id, lint.message);
-                };
-                this.lintCurrentDocument(reporter, disabledIds);
-            };
-            /*eslint-disable no-undef, block-scoped-var */
-            window.bootlint = exports;
-            /*eslint-enable no-undef, block-scoped-var */
-            $(function () {
-                exports.showLintReportForCurrentDocument([]);
-            });
-        })();
-    }
-})(typeof exports === 'object' && exports || this);
-
-},{"cheerio":1}]},{},[2]);
\ No newline at end of file
diff --git a/themes/bootstrap3/js/vendor/bootstrap-slider.js b/themes/bootstrap3/js/vendor/bootstrap-slider.js
index 26920d63b3588ac244a2a47eb803971a0dd56f2c..8694ee7f38bbee3416a94673497827b49138bb86 100644
--- a/themes/bootstrap3/js/vendor/bootstrap-slider.js
+++ b/themes/bootstrap3/js/vendor/bootstrap-slider.js
@@ -1,5 +1,5 @@
 /*! =======================================================
-                      VERSION  6.0.14              
+                      VERSION  7.1.1
 ========================================================= */
 "use strict";function _typeof(a){return a&&"undefined"!=typeof Symbol&&a.constructor===Symbol?"symbol":typeof a}/*! =========================================================
  * bootstrap-slider.js
@@ -26,4 +26,4 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  * ========================================================= */
-!function(a){if("function"==typeof define&&define.amd)define(["jquery"],a);else if("object"===("undefined"==typeof module?"undefined":_typeof(module))&&module.exports){var b;try{b=require("jquery")}catch(c){b=null}module.exports=a(b)}else window&&(window.Slider=a(window.jQuery))}(function(a){var b;return function(a){function b(){}function c(a){function c(b){b.prototype.option||(b.prototype.option=function(b){a.isPlainObject(b)&&(this.options=a.extend(!0,this.options,b))})}function e(b,c){a.fn[b]=function(e){if("string"==typeof e){for(var g=d.call(arguments,1),h=0,i=this.length;i>h;h++){var j=this[h],k=a.data(j,b);if(k)if(a.isFunction(k[e])&&"_"!==e.charAt(0)){var l=k[e].apply(k,g);if(void 0!==l&&l!==k)return l}else f("no such method '"+e+"' for "+b+" instance");else f("cannot call methods on "+b+" prior to initialization; attempted to call '"+e+"'")}return this}var m=this.map(function(){var d=a.data(this,b);return d?(d.option(e),d._init()):(d=new c(this,e),a.data(this,b,d)),a(this)});return!m||m.length>1?m:m[0]}}if(a){var f="undefined"==typeof console?b:function(a){console.error(a)};return a.bridget=function(a,b){c(b),e(a,b)},a.bridget}}var d=Array.prototype.slice;c(a)}(a),function(a){function c(b,c){function d(a,b){var c="data-slider-"+b.replace(/_/g,"-"),d=a.getAttribute(c);try{return JSON.parse(d)}catch(e){return d}}this._state={value:null,enabled:null,offset:null,size:null,percentage:null,inDrag:!1,over:!1},"string"==typeof b?this.element=document.querySelector(b):b instanceof HTMLElement&&(this.element=b),c=c?c:{};for(var f=Object.keys(this.defaultOptions),g=0;g<f.length;g++){var h=f[g],i=c[h];i="undefined"!=typeof i?i:d(this.element,h),i=null!==i?i:this.defaultOptions[h],this.options||(this.options={}),this.options[h]=i}"vertical"!==this.options.orientation||"top"!==this.options.tooltip_position&&"bottom"!==this.options.tooltip_position?"horizontal"!==this.options.orientation||"left"!==this.options.tooltip_position&&"right"!==this.options.tooltip_position||(this.options.tooltip_position="top"):this.options.tooltip_position="right";var j,k,l,m,n,o=this.element.style.width,p=!1,q=this.element.parentNode;if(this.sliderElem)p=!0;else{this.sliderElem=document.createElement("div"),this.sliderElem.className="slider";var r=document.createElement("div");r.className="slider-track",k=document.createElement("div"),k.className="slider-track-low",j=document.createElement("div"),j.className="slider-selection",l=document.createElement("div"),l.className="slider-track-high",m=document.createElement("div"),m.className="slider-handle min-slider-handle",m.setAttribute("role","slider"),m.setAttribute("aria-valuemin",this.options.min),m.setAttribute("aria-valuemax",this.options.max),n=document.createElement("div"),n.className="slider-handle max-slider-handle",n.setAttribute("role","slider"),n.setAttribute("aria-valuemin",this.options.min),n.setAttribute("aria-valuemax",this.options.max),r.appendChild(k),r.appendChild(j),r.appendChild(l);var s=Array.isArray(this.options.labelledby);if(s&&this.options.labelledby[0]&&m.setAttribute("aria-labelledby",this.options.labelledby[0]),s&&this.options.labelledby[1]&&n.setAttribute("aria-labelledby",this.options.labelledby[1]),!s&&this.options.labelledby&&(m.setAttribute("aria-labelledby",this.options.labelledby),n.setAttribute("aria-labelledby",this.options.labelledby)),this.ticks=[],Array.isArray(this.options.ticks)&&this.options.ticks.length>0){for(g=0;g<this.options.ticks.length;g++){var t=document.createElement("div");t.className="slider-tick",this.ticks.push(t),r.appendChild(t)}j.className+=" tick-slider-selection"}if(r.appendChild(m),r.appendChild(n),this.tickLabels=[],Array.isArray(this.options.ticks_labels)&&this.options.ticks_labels.length>0)for(this.tickLabelContainer=document.createElement("div"),this.tickLabelContainer.className="slider-tick-label-container",g=0;g<this.options.ticks_labels.length;g++){var u=document.createElement("div"),v=0===this.options.ticks_positions.length,w=this.options.reversed&&v?this.options.ticks_labels.length-(g+1):g;u.className="slider-tick-label",u.innerHTML=this.options.ticks_labels[w],this.tickLabels.push(u),this.tickLabelContainer.appendChild(u)}var x=function(a){var b=document.createElement("div");b.className="tooltip-arrow";var c=document.createElement("div");c.className="tooltip-inner",a.appendChild(b),a.appendChild(c)},y=document.createElement("div");y.className="tooltip tooltip-main",y.setAttribute("role","presentation"),x(y);var z=document.createElement("div");z.className="tooltip tooltip-min",z.setAttribute("role","presentation"),x(z);var A=document.createElement("div");A.className="tooltip tooltip-max",A.setAttribute("role","presentation"),x(A),this.sliderElem.appendChild(r),this.sliderElem.appendChild(y),this.sliderElem.appendChild(z),this.sliderElem.appendChild(A),this.tickLabelContainer&&this.sliderElem.appendChild(this.tickLabelContainer),q.insertBefore(this.sliderElem,this.element),this.element.style.display="none"}if(a&&(this.$element=a(this.element),this.$sliderElem=a(this.sliderElem)),this.eventToCallbackMap={},this.sliderElem.id=this.options.id,this.touchCapable="ontouchstart"in window||window.DocumentTouch&&document instanceof window.DocumentTouch,this.tooltip=this.sliderElem.querySelector(".tooltip-main"),this.tooltipInner=this.tooltip.querySelector(".tooltip-inner"),this.tooltip_min=this.sliderElem.querySelector(".tooltip-min"),this.tooltipInner_min=this.tooltip_min.querySelector(".tooltip-inner"),this.tooltip_max=this.sliderElem.querySelector(".tooltip-max"),this.tooltipInner_max=this.tooltip_max.querySelector(".tooltip-inner"),e[this.options.scale]&&(this.options.scale=e[this.options.scale]),p===!0&&(this._removeClass(this.sliderElem,"slider-horizontal"),this._removeClass(this.sliderElem,"slider-vertical"),this._removeClass(this.tooltip,"hide"),this._removeClass(this.tooltip_min,"hide"),this._removeClass(this.tooltip_max,"hide"),["left","top","width","height"].forEach(function(a){this._removeProperty(this.trackLow,a),this._removeProperty(this.trackSelection,a),this._removeProperty(this.trackHigh,a)},this),[this.handle1,this.handle2].forEach(function(a){this._removeProperty(a,"left"),this._removeProperty(a,"top")},this),[this.tooltip,this.tooltip_min,this.tooltip_max].forEach(function(a){this._removeProperty(a,"left"),this._removeProperty(a,"top"),this._removeProperty(a,"margin-left"),this._removeProperty(a,"margin-top"),this._removeClass(a,"right"),this._removeClass(a,"top")},this)),"vertical"===this.options.orientation?(this._addClass(this.sliderElem,"slider-vertical"),this.stylePos="top",this.mousePos="pageY",this.sizePos="offsetHeight"):(this._addClass(this.sliderElem,"slider-horizontal"),this.sliderElem.style.width=o,this.options.orientation="horizontal",this.stylePos="left",this.mousePos="pageX",this.sizePos="offsetWidth"),this._setTooltipPosition(),Array.isArray(this.options.ticks)&&this.options.ticks.length>0&&(this.options.max=Math.max.apply(Math,this.options.ticks),this.options.min=Math.min.apply(Math,this.options.ticks)),Array.isArray(this.options.value)?(this.options.range=!0,this._state.value=this.options.value):this.options.range?this._state.value=[this.options.value,this.options.max]:this._state.value=this.options.value,this.trackLow=k||this.trackLow,this.trackSelection=j||this.trackSelection,this.trackHigh=l||this.trackHigh,"none"===this.options.selection&&(this._addClass(this.trackLow,"hide"),this._addClass(this.trackSelection,"hide"),this._addClass(this.trackHigh,"hide")),this.handle1=m||this.handle1,this.handle2=n||this.handle2,p===!0)for(this._removeClass(this.handle1,"round triangle"),this._removeClass(this.handle2,"round triangle hide"),g=0;g<this.ticks.length;g++)this._removeClass(this.ticks[g],"round triangle hide");var B=["round","triangle","custom"],C=-1!==B.indexOf(this.options.handle);if(C)for(this._addClass(this.handle1,this.options.handle),this._addClass(this.handle2,this.options.handle),g=0;g<this.ticks.length;g++)this._addClass(this.ticks[g],this.options.handle);this._state.offset=this._offset(this.sliderElem),this._state.size=this.sliderElem[this.sizePos],this.setValue(this._state.value),this.handle1Keydown=this._keydown.bind(this,0),this.handle1.addEventListener("keydown",this.handle1Keydown,!1),this.handle2Keydown=this._keydown.bind(this,1),this.handle2.addEventListener("keydown",this.handle2Keydown,!1),this.mousedown=this._mousedown.bind(this),this.touchCapable&&this.sliderElem.addEventListener("touchstart",this.mousedown,!1),this.sliderElem.addEventListener("mousedown",this.mousedown,!1),this.resize=this._resize.bind(this),window.addEventListener("resize",this.resize,!1),"hide"===this.options.tooltip?(this._addClass(this.tooltip,"hide"),this._addClass(this.tooltip_min,"hide"),this._addClass(this.tooltip_max,"hide")):"always"===this.options.tooltip?(this._showTooltip(),this._alwaysShowTooltip=!0):(this.showTooltip=this._showTooltip.bind(this),this.hideTooltip=this._hideTooltip.bind(this),this.sliderElem.addEventListener("mouseenter",this.showTooltip,!1),this.sliderElem.addEventListener("mouseleave",this.hideTooltip,!1),this.handle1.addEventListener("focus",this.showTooltip,!1),this.handle1.addEventListener("blur",this.hideTooltip,!1),this.handle2.addEventListener("focus",this.showTooltip,!1),this.handle2.addEventListener("blur",this.hideTooltip,!1)),this.options.enabled?this.enable():this.disable()}var d={formatInvalidInputErrorMsg:function(a){return"Invalid input value '"+a+"' passed in"},callingContextNotSliderInstance:"Calling context element does not have instance of Slider bound to it. Check your code to make sure the JQuery object returned from the call to the slider() initializer is calling the method"},e={linear:{toValue:function(a){var b=a/100*(this.options.max-this.options.min),c=!0;if(this.options.ticks_positions.length>0){for(var d,e,f,g=0,h=1;h<this.options.ticks_positions.length;h++)if(a<=this.options.ticks_positions[h]){d=this.options.ticks[h-1],f=this.options.ticks_positions[h-1],e=this.options.ticks[h],g=this.options.ticks_positions[h];break}var i=(a-f)/(g-f);b=d+i*(e-d),c=!1}var j=c?this.options.min:0,k=j+Math.round(b/this.options.step)*this.options.step;return k<this.options.min?this.options.min:k>this.options.max?this.options.max:k},toPercentage:function(a){if(this.options.max===this.options.min)return 0;if(this.options.ticks_positions.length>0){for(var b,c,d,e=0,f=0;f<this.options.ticks.length;f++)if(a<=this.options.ticks[f]){b=f>0?this.options.ticks[f-1]:0,d=f>0?this.options.ticks_positions[f-1]:0,c=this.options.ticks[f],e=this.options.ticks_positions[f];break}if(f>0){var g=(a-b)/(c-b);return d+g*(e-d)}}return 100*(a-this.options.min)/(this.options.max-this.options.min)}},logarithmic:{toValue:function(a){var b=0===this.options.min?0:Math.log(this.options.min),c=Math.log(this.options.max),d=Math.exp(b+(c-b)*a/100);return d=this.options.min+Math.round((d-this.options.min)/this.options.step)*this.options.step,d<this.options.min?this.options.min:d>this.options.max?this.options.max:d},toPercentage:function(a){if(this.options.max===this.options.min)return 0;var b=Math.log(this.options.max),c=0===this.options.min?0:Math.log(this.options.min),d=0===a?0:Math.log(a);return 100*(d-c)/(b-c)}}};if(b=function(a,b){return c.call(this,a,b),this},b.prototype={_init:function(){},constructor:b,defaultOptions:{id:"",min:0,max:10,step:1,precision:0,orientation:"horizontal",value:5,range:!1,selection:"before",tooltip:"show",tooltip_split:!1,handle:"round",reversed:!1,enabled:!0,formatter:function(a){return Array.isArray(a)?a[0]+" : "+a[1]:a},natural_arrow_keys:!1,ticks:[],ticks_positions:[],ticks_labels:[],ticks_snap_bounds:0,scale:"linear",focus:!1,tooltip_position:null,labelledby:null},getElement:function(){return this.sliderElem},getValue:function(){return this.options.range?this._state.value:this._state.value[0]},setValue:function(a,b,c){a||(a=0);var d=this.getValue();this._state.value=this._validateInputValue(a);var e=this._applyPrecision.bind(this);this.options.range?(this._state.value[0]=e(this._state.value[0]),this._state.value[1]=e(this._state.value[1]),this._state.value[0]=Math.max(this.options.min,Math.min(this.options.max,this._state.value[0])),this._state.value[1]=Math.max(this.options.min,Math.min(this.options.max,this._state.value[1]))):(this._state.value=e(this._state.value),this._state.value=[Math.max(this.options.min,Math.min(this.options.max,this._state.value))],this._addClass(this.handle2,"hide"),"after"===this.options.selection?this._state.value[1]=this.options.max:this._state.value[1]=this.options.min),this.options.max>this.options.min?this._state.percentage=[this._toPercentage(this._state.value[0]),this._toPercentage(this._state.value[1]),100*this.options.step/(this.options.max-this.options.min)]:this._state.percentage=[0,0,100],this._layout();var f=this.options.range?this._state.value:this._state.value[0];return this._setDataVal(f),b===!0&&this._trigger("slide",f),d!==f&&c===!0&&this._trigger("change",{oldValue:d,newValue:f}),this},destroy:function(){this._removeSliderEventHandlers(),this.sliderElem.parentNode.removeChild(this.sliderElem),this.element.style.display="",this._cleanUpEventCallbacksMap(),this.element.removeAttribute("data"),a&&(this._unbindJQueryEventHandlers(),this.$element.removeData("slider"))},disable:function(){return this._state.enabled=!1,this.handle1.removeAttribute("tabindex"),this.handle2.removeAttribute("tabindex"),this._addClass(this.sliderElem,"slider-disabled"),this._trigger("slideDisabled"),this},enable:function(){return this._state.enabled=!0,this.handle1.setAttribute("tabindex",0),this.handle2.setAttribute("tabindex",0),this._removeClass(this.sliderElem,"slider-disabled"),this._trigger("slideEnabled"),this},toggle:function(){return this._state.enabled?this.disable():this.enable(),this},isEnabled:function(){return this._state.enabled},on:function(a,b){return this._bindNonQueryEventHandler(a,b),this},off:function(b,c){a?(this.$element.off(b,c),this.$sliderElem.off(b,c)):this._unbindNonQueryEventHandler(b,c)},getAttribute:function(a){return a?this.options[a]:this.options},setAttribute:function(a,b){return this.options[a]=b,this},refresh:function(){return this._removeSliderEventHandlers(),c.call(this,this.element,this.options),a&&a.data(this.element,"slider",this),this},relayout:function(){return this._layout(),this},_removeSliderEventHandlers:function(){this.handle1.removeEventListener("keydown",this.handle1Keydown,!1),this.handle2.removeEventListener("keydown",this.handle2Keydown,!1),this.showTooltip&&(this.handle1.removeEventListener("focus",this.showTooltip,!1),this.handle2.removeEventListener("focus",this.showTooltip,!1)),this.hideTooltip&&(this.handle1.removeEventListener("blur",this.hideTooltip,!1),this.handle2.removeEventListener("blur",this.hideTooltip,!1)),this.showTooltip&&this.sliderElem.removeEventListener("mouseenter",this.showTooltip,!1),this.hideTooltip&&this.sliderElem.removeEventListener("mouseleave",this.hideTooltip,!1),this.sliderElem.removeEventListener("touchstart",this.mousedown,!1),this.sliderElem.removeEventListener("mousedown",this.mousedown,!1),window.removeEventListener("resize",this.resize,!1)},_bindNonQueryEventHandler:function(a,b){void 0===this.eventToCallbackMap[a]&&(this.eventToCallbackMap[a]=[]),this.eventToCallbackMap[a].push(b)},_unbindNonQueryEventHandler:function(a,b){var c=this.eventToCallbackMap[a];if(void 0!==c)for(var d=0;d<c.length;d++)if(c[d]===b){c.splice(d,1);break}},_cleanUpEventCallbacksMap:function(){for(var a=Object.keys(this.eventToCallbackMap),b=0;b<a.length;b++){var c=a[b];this.eventToCallbackMap[c]=null}},_showTooltip:function(){this.options.tooltip_split===!1?(this._addClass(this.tooltip,"in"),this.tooltip_min.style.display="none",this.tooltip_max.style.display="none"):(this._addClass(this.tooltip_min,"in"),this._addClass(this.tooltip_max,"in"),this.tooltip.style.display="none"),this._state.over=!0},_hideTooltip:function(){this._state.inDrag===!1&&this.alwaysShowTooltip!==!0&&(this._removeClass(this.tooltip,"in"),this._removeClass(this.tooltip_min,"in"),this._removeClass(this.tooltip_max,"in")),this._state.over=!1},_layout:function(){var a;if(a=this.options.reversed?[100-this._state.percentage[0],this.options.range?100-this._state.percentage[1]:this._state.percentage[1]]:[this._state.percentage[0],this._state.percentage[1]],this.handle1.style[this.stylePos]=a[0]+"%",this.handle1.setAttribute("aria-valuenow",this._state.value[0]),this.handle2.style[this.stylePos]=a[1]+"%",this.handle2.setAttribute("aria-valuenow",this._state.value[1]),Array.isArray(this.options.ticks)&&this.options.ticks.length>0){var b="vertical"===this.options.orientation?"height":"width",c="vertical"===this.options.orientation?"marginTop":"marginLeft",d=this._state.size/(this.options.ticks.length-1);if(this.tickLabelContainer){var e=0;if(0===this.options.ticks_positions.length)"vertical"!==this.options.orientation&&(this.tickLabelContainer.style[c]=-d/2+"px"),e=this.tickLabelContainer.offsetHeight;else for(f=0;f<this.tickLabelContainer.childNodes.length;f++)this.tickLabelContainer.childNodes[f].offsetHeight>e&&(e=this.tickLabelContainer.childNodes[f].offsetHeight);"horizontal"===this.options.orientation&&(this.sliderElem.style.marginBottom=e+"px")}for(var f=0;f<this.options.ticks.length;f++){var g=this.options.ticks_positions[f]||this._toPercentage(this.options.ticks[f]);this.options.reversed&&(g=100-g),this.ticks[f].style[this.stylePos]=g+"%",this._removeClass(this.ticks[f],"in-selection"),this.options.range?g>=a[0]&&g<=a[1]&&this._addClass(this.ticks[f],"in-selection"):"after"===this.options.selection&&g>=a[0]?this._addClass(this.ticks[f],"in-selection"):"before"===this.options.selection&&g<=a[0]&&this._addClass(this.ticks[f],"in-selection"),this.tickLabels[f]&&(this.tickLabels[f].style[b]=d+"px","vertical"!==this.options.orientation&&void 0!==this.options.ticks_positions[f]?(this.tickLabels[f].style.position="absolute",this.tickLabels[f].style[this.stylePos]=g+"%",this.tickLabels[f].style[c]=-d/2+"px"):"vertical"===this.options.orientation&&(this.tickLabels[f].style.marginLeft=this.sliderElem.offsetWidth+"px",this.tickLabelContainer.style.marginTop=this.sliderElem.offsetWidth/2*-1+"px"))}}var h;if(this.options.range){h=this.options.formatter(this._state.value),this._setText(this.tooltipInner,h),this.tooltip.style[this.stylePos]=(a[1]+a[0])/2+"%","vertical"===this.options.orientation?this._css(this.tooltip,"margin-top",-this.tooltip.offsetHeight/2+"px"):this._css(this.tooltip,"margin-left",-this.tooltip.offsetWidth/2+"px"),"vertical"===this.options.orientation?this._css(this.tooltip,"margin-top",-this.tooltip.offsetHeight/2+"px"):this._css(this.tooltip,"margin-left",-this.tooltip.offsetWidth/2+"px");var i=this.options.formatter(this._state.value[0]);this._setText(this.tooltipInner_min,i);var j=this.options.formatter(this._state.value[1]);this._setText(this.tooltipInner_max,j),this.tooltip_min.style[this.stylePos]=a[0]+"%","vertical"===this.options.orientation?this._css(this.tooltip_min,"margin-top",-this.tooltip_min.offsetHeight/2+"px"):this._css(this.tooltip_min,"margin-left",-this.tooltip_min.offsetWidth/2+"px"),this.tooltip_max.style[this.stylePos]=a[1]+"%","vertical"===this.options.orientation?this._css(this.tooltip_max,"margin-top",-this.tooltip_max.offsetHeight/2+"px"):this._css(this.tooltip_max,"margin-left",-this.tooltip_max.offsetWidth/2+"px")}else h=this.options.formatter(this._state.value[0]),this._setText(this.tooltipInner,h),this.tooltip.style[this.stylePos]=a[0]+"%","vertical"===this.options.orientation?this._css(this.tooltip,"margin-top",-this.tooltip.offsetHeight/2+"px"):this._css(this.tooltip,"margin-left",-this.tooltip.offsetWidth/2+"px");if("vertical"===this.options.orientation)this.trackLow.style.top="0",this.trackLow.style.height=Math.min(a[0],a[1])+"%",this.trackSelection.style.top=Math.min(a[0],a[1])+"%",this.trackSelection.style.height=Math.abs(a[0]-a[1])+"%",this.trackHigh.style.bottom="0",this.trackHigh.style.height=100-Math.min(a[0],a[1])-Math.abs(a[0]-a[1])+"%";else{this.trackLow.style.left="0",this.trackLow.style.width=Math.min(a[0],a[1])+"%",this.trackSelection.style.left=Math.min(a[0],a[1])+"%",this.trackSelection.style.width=Math.abs(a[0]-a[1])+"%",this.trackHigh.style.right="0",this.trackHigh.style.width=100-Math.min(a[0],a[1])-Math.abs(a[0]-a[1])+"%";var k=this.tooltip_min.getBoundingClientRect(),l=this.tooltip_max.getBoundingClientRect();"bottom"===this.options.tooltip_position?k.right>l.left?(this._removeClass(this.tooltip_max,"bottom"),this._addClass(this.tooltip_max,"top"),this.tooltip_max.style.top="",this.tooltip_max.style.bottom="22px"):(this._removeClass(this.tooltip_max,"top"),this._addClass(this.tooltip_max,"bottom"),this.tooltip_max.style.top=this.tooltip_min.style.top,this.tooltip_max.style.bottom=""):k.right>l.left?(this._removeClass(this.tooltip_max,"top"),this._addClass(this.tooltip_max,"bottom"),this.tooltip_max.style.top="18px"):(this._removeClass(this.tooltip_max,"bottom"),this._addClass(this.tooltip_max,"top"),this.tooltip_max.style.top=this.tooltip_min.style.top)}},_resize:function(a){this._state.offset=this._offset(this.sliderElem),this._state.size=this.sliderElem[this.sizePos],this._layout()},_removeProperty:function(a,b){a.style.removeProperty?a.style.removeProperty(b):a.style.removeAttribute(b)},_mousedown:function(a){if(!this._state.enabled)return!1;this._state.offset=this._offset(this.sliderElem),this._state.size=this.sliderElem[this.sizePos];var b=this._getPercentage(a);if(this.options.range){var c=Math.abs(this._state.percentage[0]-b),d=Math.abs(this._state.percentage[1]-b);this._state.dragged=d>c?0:1}else this._state.dragged=0;this._state.percentage[this._state.dragged]=b,this._layout(),this.touchCapable&&(document.removeEventListener("touchmove",this.mousemove,!1),document.removeEventListener("touchend",this.mouseup,!1)),this.mousemove&&document.removeEventListener("mousemove",this.mousemove,!1),this.mouseup&&document.removeEventListener("mouseup",this.mouseup,!1),this.mousemove=this._mousemove.bind(this),this.mouseup=this._mouseup.bind(this),this.touchCapable&&(document.addEventListener("touchmove",this.mousemove,!1),document.addEventListener("touchend",this.mouseup,!1)),document.addEventListener("mousemove",this.mousemove,!1),document.addEventListener("mouseup",this.mouseup,!1),this._state.inDrag=!0;var e=this._calculateValue();return this._trigger("slideStart",e),this._setDataVal(e),this.setValue(e,!1,!0),this._pauseEvent(a),this.options.focus&&this._triggerFocusOnHandle(this._state.dragged),!0},_triggerFocusOnHandle:function(a){0===a&&this.handle1.focus(),1===a&&this.handle2.focus()},_keydown:function(a,b){if(!this._state.enabled)return!1;var c;switch(b.keyCode){case 37:case 40:c=-1;break;case 39:case 38:c=1}if(c){if(this.options.natural_arrow_keys){var d="vertical"===this.options.orientation&&!this.options.reversed,e="horizontal"===this.options.orientation&&this.options.reversed;(d||e)&&(c=-c)}var f=this._state.value[a]+c*this.options.step;return this.options.range&&(f=[a?this._state.value[0]:f,a?f:this._state.value[1]]),this._trigger("slideStart",f),this._setDataVal(f),this.setValue(f,!0,!0),this._setDataVal(f),this._trigger("slideStop",f),this._layout(),this._pauseEvent(b),!1}},_pauseEvent:function(a){a.stopPropagation&&a.stopPropagation(),a.preventDefault&&a.preventDefault(),a.cancelBubble=!0,a.returnValue=!1},_mousemove:function(a){if(!this._state.enabled)return!1;var b=this._getPercentage(a);this._adjustPercentageForRangeSliders(b),this._state.percentage[this._state.dragged]=b,this._layout();var c=this._calculateValue(!0);return this.setValue(c,!0,!0),!1},_adjustPercentageForRangeSliders:function(a){if(this.options.range){var b=this._getNumDigitsAfterDecimalPlace(a);b=b?b-1:0;var c=this._applyToFixedAndParseFloat(a,b);0===this._state.dragged&&this._applyToFixedAndParseFloat(this._state.percentage[1],b)<c?(this._state.percentage[0]=this._state.percentage[1],this._state.dragged=1):1===this._state.dragged&&this._applyToFixedAndParseFloat(this._state.percentage[0],b)>c&&(this._state.percentage[1]=this._state.percentage[0],this._state.dragged=0)}},_mouseup:function(){if(!this._state.enabled)return!1;this.touchCapable&&(document.removeEventListener("touchmove",this.mousemove,!1),document.removeEventListener("touchend",this.mouseup,!1)),document.removeEventListener("mousemove",this.mousemove,!1),document.removeEventListener("mouseup",this.mouseup,!1),this._state.inDrag=!1,this._state.over===!1&&this._hideTooltip();var a=this._calculateValue(!0);return this._layout(),this._setDataVal(a),this._trigger("slideStop",a),!1},_calculateValue:function(a){var b;if(this.options.range?(b=[this.options.min,this.options.max],0!==this._state.percentage[0]&&(b[0]=this._toValue(this._state.percentage[0]),b[0]=this._applyPrecision(b[0])),100!==this._state.percentage[1]&&(b[1]=this._toValue(this._state.percentage[1]),b[1]=this._applyPrecision(b[1]))):(b=this._toValue(this._state.percentage[0]),b=parseFloat(b),b=this._applyPrecision(b)),a){for(var c=[b,1/0],d=0;d<this.options.ticks.length;d++){var e=Math.abs(this.options.ticks[d]-b);e<=c[1]&&(c=[this.options.ticks[d],e])}if(c[1]<=this.options.ticks_snap_bounds)return c[0]}return b},_applyPrecision:function(a){var b=this.options.precision||this._getNumDigitsAfterDecimalPlace(this.options.step);return this._applyToFixedAndParseFloat(a,b)},_getNumDigitsAfterDecimalPlace:function(a){var b=(""+a).match(/(?:\.(\d+))?(?:[eE]([+-]?\d+))?$/);return b?Math.max(0,(b[1]?b[1].length:0)-(b[2]?+b[2]:0)):0},_applyToFixedAndParseFloat:function(a,b){var c=a.toFixed(b);return parseFloat(c)},_getPercentage:function(a){!this.touchCapable||"touchstart"!==a.type&&"touchmove"!==a.type||(a=a.touches[0]);var b=a[this.mousePos],c=this._state.offset[this.stylePos],d=b-c,e=d/this._state.size*100;return e=Math.round(e/this._state.percentage[2])*this._state.percentage[2],this.options.reversed&&(e=100-e),Math.max(0,Math.min(100,e))},_validateInputValue:function(a){if("number"==typeof a)return a;if(Array.isArray(a))return this._validateArray(a),a;throw new Error(d.formatInvalidInputErrorMsg(a))},_validateArray:function(a){for(var b=0;b<a.length;b++){var c=a[b];if("number"!=typeof c)throw new Error(d.formatInvalidInputErrorMsg(c))}},_setDataVal:function(a){this.element.setAttribute("data-value",a),this.element.setAttribute("value",a),this.element.value=a},_trigger:function(b,c){c=c||0===c?c:void 0;var d=this.eventToCallbackMap[b];if(d&&d.length)for(var e=0;e<d.length;e++){var f=d[e];f(c)}a&&this._triggerJQueryEvent(b,c)},_triggerJQueryEvent:function(a,b){var c={type:a,value:b};this.$element.trigger(c),this.$sliderElem.trigger(c)},_unbindJQueryEventHandlers:function(){this.$element.off(),this.$sliderElem.off()},_setText:function(a,b){"undefined"!=typeof a.innerText?a.innerText=b:"undefined"!=typeof a.textContent&&(a.textContent=b)},_removeClass:function(a,b){for(var c=b.split(" "),d=a.className,e=0;e<c.length;e++){var f=c[e],g=new RegExp("(?:\\s|^)"+f+"(?:\\s|$)");d=d.replace(g," ")}a.className=d.trim()},_addClass:function(a,b){for(var c=b.split(" "),d=a.className,e=0;e<c.length;e++){var f=c[e],g=new RegExp("(?:\\s|^)"+f+"(?:\\s|$)"),h=g.test(d);h||(d+=" "+f)}a.className=d.trim()},_offsetLeft:function(a){return a.getBoundingClientRect().left},_offsetTop:function(a){for(var b=a.offsetTop;(a=a.offsetParent)&&!isNaN(a.offsetTop);)b+=a.offsetTop;return b},_offset:function(a){return{left:this._offsetLeft(a),top:this._offsetTop(a)}},_css:function(b,c,d){if(a)a.style(b,c,d);else{var e=c.replace(/^-ms-/,"ms-").replace(/-([\da-z])/gi,function(a,b){return b.toUpperCase()});b.style[e]=d}},_toValue:function(a){return this.options.scale.toValue.apply(this,[a])},_toPercentage:function(a){return this.options.scale.toPercentage.apply(this,[a])},_setTooltipPosition:function(){var a=[this.tooltip,this.tooltip_min,this.tooltip_max];if("vertical"===this.options.orientation){var b=this.options.tooltip_position||"right",c="left"===b?"right":"left";a.forEach(function(a){this._addClass(a,b),a.style[c]="100%"}.bind(this))}else"bottom"===this.options.tooltip_position?a.forEach(function(a){this._addClass(a,"bottom"),a.style.top="22px"}.bind(this)):a.forEach(function(a){this._addClass(a,"top"),a.style.top=-this.tooltip.outerHeight-14+"px"}.bind(this))}},a){var f=a.fn.slider?"bootstrapSlider":"slider";a.bridget(f,b)}}(a),b});
\ No newline at end of file
+!function(a){if("function"==typeof define&&define.amd)try{define(["jquery"],a)}catch(b){define([],a)}else if("object"===("undefined"==typeof module?"undefined":_typeof(module))&&module.exports){var c;try{c=require("jquery")}catch(b){c=null}module.exports=a(c)}else window&&(window.Slider=a(window.jQuery))}(function(a){var b;return function(a){function b(){}function c(a){function c(b){b.prototype.option||(b.prototype.option=function(b){a.isPlainObject(b)&&(this.options=a.extend(!0,this.options,b))})}function e(b,c){a.fn[b]=function(e){if("string"==typeof e){for(var g=d.call(arguments,1),h=0,i=this.length;i>h;h++){var j=this[h],k=a.data(j,b);if(k)if(a.isFunction(k[e])&&"_"!==e.charAt(0)){var l=k[e].apply(k,g);if(void 0!==l&&l!==k)return l}else f("no such method '"+e+"' for "+b+" instance");else f("cannot call methods on "+b+" prior to initialization; attempted to call '"+e+"'")}return this}var m=this.map(function(){var d=a.data(this,b);return d?(d.option(e),d._init()):(d=new c(this,e),a.data(this,b,d)),a(this)});return!m||m.length>1?m:m[0]}}if(a){var f="undefined"==typeof console?b:function(a){console.error(a)};return a.bridget=function(a,b){c(b),e(a,b)},a.bridget}}var d=Array.prototype.slice;c(a)}(a),function(a){function c(b,c){function d(a,b){var c="data-slider-"+b.replace(/_/g,"-"),d=a.getAttribute(c);try{return JSON.parse(d)}catch(e){return d}}this._state={value:null,enabled:null,offset:null,size:null,percentage:null,inDrag:!1,over:!1},"string"==typeof b?this.element=document.querySelector(b):b instanceof HTMLElement&&(this.element=b),c=c?c:{};for(var f=Object.keys(this.defaultOptions),g=0;g<f.length;g++){var h=f[g],i=c[h];i="undefined"!=typeof i?i:d(this.element,h),i=null!==i?i:this.defaultOptions[h],this.options||(this.options={}),this.options[h]=i}"vertical"!==this.options.orientation||"top"!==this.options.tooltip_position&&"bottom"!==this.options.tooltip_position?"horizontal"!==this.options.orientation||"left"!==this.options.tooltip_position&&"right"!==this.options.tooltip_position||(this.options.tooltip_position="top"):this.options.tooltip_position="right";var j,k,l,m,n,o=this.element.style.width,p=!1,q=this.element.parentNode;if(this.sliderElem)p=!0;else{this.sliderElem=document.createElement("div"),this.sliderElem.className="slider";var r=document.createElement("div");r.className="slider-track",k=document.createElement("div"),k.className="slider-track-low",j=document.createElement("div"),j.className="slider-selection",l=document.createElement("div"),l.className="slider-track-high",m=document.createElement("div"),m.className="slider-handle min-slider-handle",m.setAttribute("role","slider"),m.setAttribute("aria-valuemin",this.options.min),m.setAttribute("aria-valuemax",this.options.max),n=document.createElement("div"),n.className="slider-handle max-slider-handle",n.setAttribute("role","slider"),n.setAttribute("aria-valuemin",this.options.min),n.setAttribute("aria-valuemax",this.options.max),r.appendChild(k),r.appendChild(j),r.appendChild(l);var s=Array.isArray(this.options.labelledby);if(s&&this.options.labelledby[0]&&m.setAttribute("aria-labelledby",this.options.labelledby[0]),s&&this.options.labelledby[1]&&n.setAttribute("aria-labelledby",this.options.labelledby[1]),!s&&this.options.labelledby&&(m.setAttribute("aria-labelledby",this.options.labelledby),n.setAttribute("aria-labelledby",this.options.labelledby)),this.ticks=[],Array.isArray(this.options.ticks)&&this.options.ticks.length>0){for(g=0;g<this.options.ticks.length;g++){var t=document.createElement("div");t.className="slider-tick",this.ticks.push(t),r.appendChild(t)}j.className+=" tick-slider-selection"}if(r.appendChild(m),r.appendChild(n),this.tickLabels=[],Array.isArray(this.options.ticks_labels)&&this.options.ticks_labels.length>0)for(this.tickLabelContainer=document.createElement("div"),this.tickLabelContainer.className="slider-tick-label-container",g=0;g<this.options.ticks_labels.length;g++){var u=document.createElement("div"),v=0===this.options.ticks_positions.length,w=this.options.reversed&&v?this.options.ticks_labels.length-(g+1):g;u.className="slider-tick-label",u.innerHTML=this.options.ticks_labels[w],this.tickLabels.push(u),this.tickLabelContainer.appendChild(u)}var x=function(a){var b=document.createElement("div");b.className="tooltip-arrow";var c=document.createElement("div");c.className="tooltip-inner",a.appendChild(b),a.appendChild(c)},y=document.createElement("div");y.className="tooltip tooltip-main",y.setAttribute("role","presentation"),x(y);var z=document.createElement("div");z.className="tooltip tooltip-min",z.setAttribute("role","presentation"),x(z);var A=document.createElement("div");A.className="tooltip tooltip-max",A.setAttribute("role","presentation"),x(A),this.sliderElem.appendChild(r),this.sliderElem.appendChild(y),this.sliderElem.appendChild(z),this.sliderElem.appendChild(A),this.tickLabelContainer&&this.sliderElem.appendChild(this.tickLabelContainer),q.insertBefore(this.sliderElem,this.element),this.element.style.display="none"}if(a&&(this.$element=a(this.element),this.$sliderElem=a(this.sliderElem)),this.eventToCallbackMap={},this.sliderElem.id=this.options.id,this.touchCapable="ontouchstart"in window||window.DocumentTouch&&document instanceof window.DocumentTouch,this.touchX=0,this.touchY=0,this.tooltip=this.sliderElem.querySelector(".tooltip-main"),this.tooltipInner=this.tooltip.querySelector(".tooltip-inner"),this.tooltip_min=this.sliderElem.querySelector(".tooltip-min"),this.tooltipInner_min=this.tooltip_min.querySelector(".tooltip-inner"),this.tooltip_max=this.sliderElem.querySelector(".tooltip-max"),this.tooltipInner_max=this.tooltip_max.querySelector(".tooltip-inner"),e[this.options.scale]&&(this.options.scale=e[this.options.scale]),p===!0&&(this._removeClass(this.sliderElem,"slider-horizontal"),this._removeClass(this.sliderElem,"slider-vertical"),this._removeClass(this.tooltip,"hide"),this._removeClass(this.tooltip_min,"hide"),this._removeClass(this.tooltip_max,"hide"),["left","top","width","height"].forEach(function(a){this._removeProperty(this.trackLow,a),this._removeProperty(this.trackSelection,a),this._removeProperty(this.trackHigh,a)},this),[this.handle1,this.handle2].forEach(function(a){this._removeProperty(a,"left"),this._removeProperty(a,"top")},this),[this.tooltip,this.tooltip_min,this.tooltip_max].forEach(function(a){this._removeProperty(a,"left"),this._removeProperty(a,"top"),this._removeProperty(a,"margin-left"),this._removeProperty(a,"margin-top"),this._removeClass(a,"right"),this._removeClass(a,"top")},this)),"vertical"===this.options.orientation?(this._addClass(this.sliderElem,"slider-vertical"),this.stylePos="top",this.mousePos="pageY",this.sizePos="offsetHeight"):(this._addClass(this.sliderElem,"slider-horizontal"),this.sliderElem.style.width=o,this.options.orientation="horizontal",this.stylePos="left",this.mousePos="pageX",this.sizePos="offsetWidth"),this._setTooltipPosition(),Array.isArray(this.options.ticks)&&this.options.ticks.length>0&&(this.options.max=Math.max.apply(Math,this.options.ticks),this.options.min=Math.min.apply(Math,this.options.ticks)),Array.isArray(this.options.value)?(this.options.range=!0,this._state.value=this.options.value):this.options.range?this._state.value=[this.options.value,this.options.max]:this._state.value=this.options.value,this.trackLow=k||this.trackLow,this.trackSelection=j||this.trackSelection,this.trackHigh=l||this.trackHigh,"none"===this.options.selection&&(this._addClass(this.trackLow,"hide"),this._addClass(this.trackSelection,"hide"),this._addClass(this.trackHigh,"hide")),this.handle1=m||this.handle1,this.handle2=n||this.handle2,p===!0)for(this._removeClass(this.handle1,"round triangle"),this._removeClass(this.handle2,"round triangle hide"),g=0;g<this.ticks.length;g++)this._removeClass(this.ticks[g],"round triangle hide");var B=["round","triangle","custom"],C=-1!==B.indexOf(this.options.handle);if(C)for(this._addClass(this.handle1,this.options.handle),this._addClass(this.handle2,this.options.handle),g=0;g<this.ticks.length;g++)this._addClass(this.ticks[g],this.options.handle);this._state.offset=this._offset(this.sliderElem),this._state.size=this.sliderElem[this.sizePos],this.setValue(this._state.value),this.handle1Keydown=this._keydown.bind(this,0),this.handle1.addEventListener("keydown",this.handle1Keydown,!1),this.handle2Keydown=this._keydown.bind(this,1),this.handle2.addEventListener("keydown",this.handle2Keydown,!1),this.mousedown=this._mousedown.bind(this),this.touchstart=this._touchstart.bind(this),this.touchmove=this._touchmove.bind(this),this.touchCapable&&(this.sliderElem.addEventListener("touchstart",this.touchstart,!1),this.sliderElem.addEventListener("touchmove",this.touchmove,!1)),this.sliderElem.addEventListener("mousedown",this.mousedown,!1),this.resize=this._resize.bind(this),window.addEventListener("resize",this.resize,!1),"hide"===this.options.tooltip?(this._addClass(this.tooltip,"hide"),this._addClass(this.tooltip_min,"hide"),this._addClass(this.tooltip_max,"hide")):"always"===this.options.tooltip?(this._showTooltip(),this._alwaysShowTooltip=!0):(this.showTooltip=this._showTooltip.bind(this),this.hideTooltip=this._hideTooltip.bind(this),this.sliderElem.addEventListener("mouseenter",this.showTooltip,!1),this.sliderElem.addEventListener("mouseleave",this.hideTooltip,!1),this.handle1.addEventListener("focus",this.showTooltip,!1),this.handle1.addEventListener("blur",this.hideTooltip,!1),this.handle2.addEventListener("focus",this.showTooltip,!1),this.handle2.addEventListener("blur",this.hideTooltip,!1)),this.options.enabled?this.enable():this.disable()}var d={formatInvalidInputErrorMsg:function(a){return"Invalid input value '"+a+"' passed in"},callingContextNotSliderInstance:"Calling context element does not have instance of Slider bound to it. Check your code to make sure the JQuery object returned from the call to the slider() initializer is calling the method"},e={linear:{toValue:function(a){var b=a/100*(this.options.max-this.options.min),c=!0;if(this.options.ticks_positions.length>0){for(var d,e,f,g=0,h=1;h<this.options.ticks_positions.length;h++)if(a<=this.options.ticks_positions[h]){d=this.options.ticks[h-1],f=this.options.ticks_positions[h-1],e=this.options.ticks[h],g=this.options.ticks_positions[h];break}var i=(a-f)/(g-f);b=d+i*(e-d),c=!1}var j=c?this.options.min:0,k=j+Math.round(b/this.options.step)*this.options.step;return k<this.options.min?this.options.min:k>this.options.max?this.options.max:k},toPercentage:function(a){if(this.options.max===this.options.min)return 0;if(this.options.ticks_positions.length>0){for(var b,c,d,e=0,f=0;f<this.options.ticks.length;f++)if(a<=this.options.ticks[f]){b=f>0?this.options.ticks[f-1]:0,d=f>0?this.options.ticks_positions[f-1]:0,c=this.options.ticks[f],e=this.options.ticks_positions[f];break}if(f>0){var g=(a-b)/(c-b);return d+g*(e-d)}}return 100*(a-this.options.min)/(this.options.max-this.options.min)}},logarithmic:{toValue:function(a){var b=0===this.options.min?0:Math.log(this.options.min),c=Math.log(this.options.max),d=Math.exp(b+(c-b)*a/100);return d=this.options.min+Math.round((d-this.options.min)/this.options.step)*this.options.step,d<this.options.min?this.options.min:d>this.options.max?this.options.max:d},toPercentage:function(a){if(this.options.max===this.options.min)return 0;var b=Math.log(this.options.max),c=0===this.options.min?0:Math.log(this.options.min),d=0===a?0:Math.log(a);return 100*(d-c)/(b-c)}}};if(b=function(a,b){return c.call(this,a,b),this},b.prototype={_init:function(){},constructor:b,defaultOptions:{id:"",min:0,max:10,step:1,precision:0,orientation:"horizontal",value:5,range:!1,selection:"before",tooltip:"show",tooltip_split:!1,handle:"round",reversed:!1,enabled:!0,formatter:function(a){return Array.isArray(a)?a[0]+" : "+a[1]:a},natural_arrow_keys:!1,ticks:[],ticks_positions:[],ticks_labels:[],ticks_snap_bounds:0,scale:"linear",focus:!1,tooltip_position:null,labelledby:null},getElement:function(){return this.sliderElem},getValue:function(){return this.options.range?this._state.value:this._state.value[0]},setValue:function(a,b,c){a||(a=0);var d=this.getValue();this._state.value=this._validateInputValue(a);var e=this._applyPrecision.bind(this);this.options.range?(this._state.value[0]=e(this._state.value[0]),this._state.value[1]=e(this._state.value[1]),this._state.value[0]=Math.max(this.options.min,Math.min(this.options.max,this._state.value[0])),this._state.value[1]=Math.max(this.options.min,Math.min(this.options.max,this._state.value[1]))):(this._state.value=e(this._state.value),this._state.value=[Math.max(this.options.min,Math.min(this.options.max,this._state.value))],this._addClass(this.handle2,"hide"),"after"===this.options.selection?this._state.value[1]=this.options.max:this._state.value[1]=this.options.min),this.options.max>this.options.min?this._state.percentage=[this._toPercentage(this._state.value[0]),this._toPercentage(this._state.value[1]),100*this.options.step/(this.options.max-this.options.min)]:this._state.percentage=[0,0,100],this._layout();var f=this.options.range?this._state.value:this._state.value[0];return this._setDataVal(f),b===!0&&this._trigger("slide",f),d!==f&&c===!0&&this._trigger("change",{oldValue:d,newValue:f}),this},destroy:function(){this._removeSliderEventHandlers(),this.sliderElem.parentNode.removeChild(this.sliderElem),this.element.style.display="",this._cleanUpEventCallbacksMap(),this.element.removeAttribute("data"),a&&(this._unbindJQueryEventHandlers(),this.$element.removeData("slider"))},disable:function(){return this._state.enabled=!1,this.handle1.removeAttribute("tabindex"),this.handle2.removeAttribute("tabindex"),this._addClass(this.sliderElem,"slider-disabled"),this._trigger("slideDisabled"),this},enable:function(){return this._state.enabled=!0,this.handle1.setAttribute("tabindex",0),this.handle2.setAttribute("tabindex",0),this._removeClass(this.sliderElem,"slider-disabled"),this._trigger("slideEnabled"),this},toggle:function(){return this._state.enabled?this.disable():this.enable(),this},isEnabled:function(){return this._state.enabled},on:function(a,b){return this._bindNonQueryEventHandler(a,b),this},off:function(b,c){a?(this.$element.off(b,c),this.$sliderElem.off(b,c)):this._unbindNonQueryEventHandler(b,c)},getAttribute:function(a){return a?this.options[a]:this.options},setAttribute:function(a,b){return this.options[a]=b,this},refresh:function(){return this._removeSliderEventHandlers(),c.call(this,this.element,this.options),a&&a.data(this.element,"slider",this),this},relayout:function(){return this._resize(),this._layout(),this},_removeSliderEventHandlers:function(){this.handle1.removeEventListener("keydown",this.handle1Keydown,!1),this.handle2.removeEventListener("keydown",this.handle2Keydown,!1),this.showTooltip&&(this.handle1.removeEventListener("focus",this.showTooltip,!1),this.handle2.removeEventListener("focus",this.showTooltip,!1)),this.hideTooltip&&(this.handle1.removeEventListener("blur",this.hideTooltip,!1),this.handle2.removeEventListener("blur",this.hideTooltip,!1)),this.showTooltip&&this.sliderElem.removeEventListener("mouseenter",this.showTooltip,!1),this.hideTooltip&&this.sliderElem.removeEventListener("mouseleave",this.hideTooltip,!1),this.sliderElem.removeEventListener("touchstart",this.touchstart,!1),this.sliderElem.removeEventListener("touchmove",this.touchmove,!1),this.sliderElem.removeEventListener("mousedown",this.mousedown,!1),window.removeEventListener("resize",this.resize,!1)},_bindNonQueryEventHandler:function(a,b){void 0===this.eventToCallbackMap[a]&&(this.eventToCallbackMap[a]=[]),this.eventToCallbackMap[a].push(b)},_unbindNonQueryEventHandler:function(a,b){var c=this.eventToCallbackMap[a];if(void 0!==c)for(var d=0;d<c.length;d++)if(c[d]===b){c.splice(d,1);break}},_cleanUpEventCallbacksMap:function(){for(var a=Object.keys(this.eventToCallbackMap),b=0;b<a.length;b++){var c=a[b];this.eventToCallbackMap[c]=null}},_showTooltip:function(){this.options.tooltip_split===!1?(this._addClass(this.tooltip,"in"),this.tooltip_min.style.display="none",this.tooltip_max.style.display="none"):(this._addClass(this.tooltip_min,"in"),this._addClass(this.tooltip_max,"in"),this.tooltip.style.display="none"),this._state.over=!0},_hideTooltip:function(){this._state.inDrag===!1&&this.alwaysShowTooltip!==!0&&(this._removeClass(this.tooltip,"in"),this._removeClass(this.tooltip_min,"in"),this._removeClass(this.tooltip_max,"in")),this._state.over=!1},_layout:function(){var a;if(a=this.options.reversed?[100-this._state.percentage[0],this.options.range?100-this._state.percentage[1]:this._state.percentage[1]]:[this._state.percentage[0],this._state.percentage[1]],this.handle1.style[this.stylePos]=a[0]+"%",this.handle1.setAttribute("aria-valuenow",this._state.value[0]),this.handle2.style[this.stylePos]=a[1]+"%",this.handle2.setAttribute("aria-valuenow",this._state.value[1]),Array.isArray(this.options.ticks)&&this.options.ticks.length>0){var b="vertical"===this.options.orientation?"height":"width",c="vertical"===this.options.orientation?"marginTop":"marginLeft",d=this._state.size/(this.options.ticks.length-1);if(this.tickLabelContainer){var e=0;if(0===this.options.ticks_positions.length)"vertical"!==this.options.orientation&&(this.tickLabelContainer.style[c]=-d/2+"px"),e=this.tickLabelContainer.offsetHeight;else for(f=0;f<this.tickLabelContainer.childNodes.length;f++)this.tickLabelContainer.childNodes[f].offsetHeight>e&&(e=this.tickLabelContainer.childNodes[f].offsetHeight);"horizontal"===this.options.orientation&&(this.sliderElem.style.marginBottom=e+"px")}for(var f=0;f<this.options.ticks.length;f++){var g=this.options.ticks_positions[f]||this._toPercentage(this.options.ticks[f]);this.options.reversed&&(g=100-g),this.ticks[f].style[this.stylePos]=g+"%",this._removeClass(this.ticks[f],"in-selection"),this.options.range?g>=a[0]&&g<=a[1]&&this._addClass(this.ticks[f],"in-selection"):"after"===this.options.selection&&g>=a[0]?this._addClass(this.ticks[f],"in-selection"):"before"===this.options.selection&&g<=a[0]&&this._addClass(this.ticks[f],"in-selection"),this.tickLabels[f]&&(this.tickLabels[f].style[b]=d+"px","vertical"!==this.options.orientation&&void 0!==this.options.ticks_positions[f]?(this.tickLabels[f].style.position="absolute",this.tickLabels[f].style[this.stylePos]=g+"%",this.tickLabels[f].style[c]=-d/2+"px"):"vertical"===this.options.orientation&&(this.tickLabels[f].style.marginLeft=this.sliderElem.offsetWidth+"px",this.tickLabelContainer.style.marginTop=this.sliderElem.offsetWidth/2*-1+"px"))}}var h;if(this.options.range){h=this.options.formatter(this._state.value),this._setText(this.tooltipInner,h),this.tooltip.style[this.stylePos]=(a[1]+a[0])/2+"%","vertical"===this.options.orientation?this._css(this.tooltip,"margin-top",-this.tooltip.offsetHeight/2+"px"):this._css(this.tooltip,"margin-left",-this.tooltip.offsetWidth/2+"px"),"vertical"===this.options.orientation?this._css(this.tooltip,"margin-top",-this.tooltip.offsetHeight/2+"px"):this._css(this.tooltip,"margin-left",-this.tooltip.offsetWidth/2+"px");var i=this.options.formatter(this._state.value[0]);this._setText(this.tooltipInner_min,i);var j=this.options.formatter(this._state.value[1]);this._setText(this.tooltipInner_max,j),this.tooltip_min.style[this.stylePos]=a[0]+"%","vertical"===this.options.orientation?this._css(this.tooltip_min,"margin-top",-this.tooltip_min.offsetHeight/2+"px"):this._css(this.tooltip_min,"margin-left",-this.tooltip_min.offsetWidth/2+"px"),this.tooltip_max.style[this.stylePos]=a[1]+"%","vertical"===this.options.orientation?this._css(this.tooltip_max,"margin-top",-this.tooltip_max.offsetHeight/2+"px"):this._css(this.tooltip_max,"margin-left",-this.tooltip_max.offsetWidth/2+"px")}else h=this.options.formatter(this._state.value[0]),this._setText(this.tooltipInner,h),this.tooltip.style[this.stylePos]=a[0]+"%","vertical"===this.options.orientation?this._css(this.tooltip,"margin-top",-this.tooltip.offsetHeight/2+"px"):this._css(this.tooltip,"margin-left",-this.tooltip.offsetWidth/2+"px");if("vertical"===this.options.orientation)this.trackLow.style.top="0",this.trackLow.style.height=Math.min(a[0],a[1])+"%",this.trackSelection.style.top=Math.min(a[0],a[1])+"%",this.trackSelection.style.height=Math.abs(a[0]-a[1])+"%",this.trackHigh.style.bottom="0",this.trackHigh.style.height=100-Math.min(a[0],a[1])-Math.abs(a[0]-a[1])+"%";else{this.trackLow.style.left="0",this.trackLow.style.width=Math.min(a[0],a[1])+"%",this.trackSelection.style.left=Math.min(a[0],a[1])+"%",this.trackSelection.style.width=Math.abs(a[0]-a[1])+"%",this.trackHigh.style.right="0",this.trackHigh.style.width=100-Math.min(a[0],a[1])-Math.abs(a[0]-a[1])+"%";var k=this.tooltip_min.getBoundingClientRect(),l=this.tooltip_max.getBoundingClientRect();"bottom"===this.options.tooltip_position?k.right>l.left?(this._removeClass(this.tooltip_max,"bottom"),this._addClass(this.tooltip_max,"top"),this.tooltip_max.style.top="",this.tooltip_max.style.bottom="22px"):(this._removeClass(this.tooltip_max,"top"),this._addClass(this.tooltip_max,"bottom"),this.tooltip_max.style.top=this.tooltip_min.style.top,this.tooltip_max.style.bottom=""):k.right>l.left?(this._removeClass(this.tooltip_max,"top"),this._addClass(this.tooltip_max,"bottom"),this.tooltip_max.style.top="18px"):(this._removeClass(this.tooltip_max,"bottom"),this._addClass(this.tooltip_max,"top"),this.tooltip_max.style.top=this.tooltip_min.style.top)}},_resize:function(a){this._state.offset=this._offset(this.sliderElem),this._state.size=this.sliderElem[this.sizePos],this._layout()},_removeProperty:function(a,b){a.style.removeProperty?a.style.removeProperty(b):a.style.removeAttribute(b)},_mousedown:function(a){if(!this._state.enabled)return!1;this._state.offset=this._offset(this.sliderElem),this._state.size=this.sliderElem[this.sizePos];var b=this._getPercentage(a);if(this.options.range){var c=Math.abs(this._state.percentage[0]-b),d=Math.abs(this._state.percentage[1]-b);this._state.dragged=d>c?0:1,this._adjustPercentageForRangeSliders(b)}else this._state.dragged=0;this._state.percentage[this._state.dragged]=b,this._layout(),this.touchCapable&&(document.removeEventListener("touchmove",this.mousemove,!1),document.removeEventListener("touchend",this.mouseup,!1)),this.mousemove&&document.removeEventListener("mousemove",this.mousemove,!1),this.mouseup&&document.removeEventListener("mouseup",this.mouseup,!1),this.mousemove=this._mousemove.bind(this),this.mouseup=this._mouseup.bind(this),this.touchCapable&&(document.addEventListener("touchmove",this.mousemove,!1),document.addEventListener("touchend",this.mouseup,!1)),document.addEventListener("mousemove",this.mousemove,!1),document.addEventListener("mouseup",this.mouseup,!1),this._state.inDrag=!0;var e=this._calculateValue();return this._trigger("slideStart",e),this._setDataVal(e),this.setValue(e,!1,!0),this._pauseEvent(a),this.options.focus&&this._triggerFocusOnHandle(this._state.dragged),!0},_touchstart:function(a){if(void 0===a.changedTouches)return void this._mousedown(a);var b=a.changedTouches[0];this.touchX=b.pageX,this.touchY=b.pageY},_triggerFocusOnHandle:function(a){0===a&&this.handle1.focus(),1===a&&this.handle2.focus()},_keydown:function(a,b){if(!this._state.enabled)return!1;var c;switch(b.keyCode){case 37:case 40:c=-1;break;case 39:case 38:c=1}if(c){if(this.options.natural_arrow_keys){var d="vertical"===this.options.orientation&&!this.options.reversed,e="horizontal"===this.options.orientation&&this.options.reversed;(d||e)&&(c=-c)}var f=this._state.value[a]+c*this.options.step;return this.options.range&&(f=[a?this._state.value[0]:f,a?f:this._state.value[1]]),this._trigger("slideStart",f),this._setDataVal(f),this.setValue(f,!0,!0),this._setDataVal(f),this._trigger("slideStop",f),this._layout(),this._pauseEvent(b),!1}},_pauseEvent:function(a){a.stopPropagation&&a.stopPropagation(),a.preventDefault&&a.preventDefault(),a.cancelBubble=!0,a.returnValue=!1},_mousemove:function(a){if(!this._state.enabled)return!1;var b=this._getPercentage(a);this._adjustPercentageForRangeSliders(b),this._state.percentage[this._state.dragged]=b,this._layout();var c=this._calculateValue(!0);return this.setValue(c,!0,!0),!1},_touchmove:function(a){if(void 0!==a.changedTouches){var b=a.changedTouches[0],c=b.pageX-this.touchX,d=b.pageY-this.touchY;this._state.inDrag||("vertical"===this.options.orientation&&5>=c&&c>=-5&&(d>=15||-15>=d)?this._mousedown(a):5>=d&&d>=-5&&(c>=15||-15>=c)&&this._mousedown(a))}},_adjustPercentageForRangeSliders:function(a){if(this.options.range){var b=this._getNumDigitsAfterDecimalPlace(a);b=b?b-1:0;var c=this._applyToFixedAndParseFloat(a,b);0===this._state.dragged&&this._applyToFixedAndParseFloat(this._state.percentage[1],b)<c?(this._state.percentage[0]=this._state.percentage[1],this._state.dragged=1):1===this._state.dragged&&this._applyToFixedAndParseFloat(this._state.percentage[0],b)>c&&(this._state.percentage[1]=this._state.percentage[0],this._state.dragged=0)}},_mouseup:function(){if(!this._state.enabled)return!1;this.touchCapable&&(document.removeEventListener("touchmove",this.mousemove,!1),document.removeEventListener("touchend",this.mouseup,!1)),document.removeEventListener("mousemove",this.mousemove,!1),document.removeEventListener("mouseup",this.mouseup,!1),this._state.inDrag=!1,this._state.over===!1&&this._hideTooltip();var a=this._calculateValue(!0);return this._layout(),this._setDataVal(a),this._trigger("slideStop",a),!1},_calculateValue:function(a){var b;if(this.options.range?(b=[this.options.min,this.options.max],0!==this._state.percentage[0]&&(b[0]=this._toValue(this._state.percentage[0]),b[0]=this._applyPrecision(b[0])),100!==this._state.percentage[1]&&(b[1]=this._toValue(this._state.percentage[1]),b[1]=this._applyPrecision(b[1]))):(b=this._toValue(this._state.percentage[0]),b=parseFloat(b),b=this._applyPrecision(b)),a){for(var c=[b,1/0],d=0;d<this.options.ticks.length;d++){var e=Math.abs(this.options.ticks[d]-b);e<=c[1]&&(c=[this.options.ticks[d],e])}if(c[1]<=this.options.ticks_snap_bounds)return c[0]}return b},_applyPrecision:function(a){var b=this.options.precision||this._getNumDigitsAfterDecimalPlace(this.options.step);return this._applyToFixedAndParseFloat(a,b)},_getNumDigitsAfterDecimalPlace:function(a){var b=(""+a).match(/(?:\.(\d+))?(?:[eE]([+-]?\d+))?$/);return b?Math.max(0,(b[1]?b[1].length:0)-(b[2]?+b[2]:0)):0},_applyToFixedAndParseFloat:function(a,b){var c=a.toFixed(b);return parseFloat(c)},_getPercentage:function(a){!this.touchCapable||"touchstart"!==a.type&&"touchmove"!==a.type||(a=a.touches[0]);var b=a[this.mousePos],c=this._state.offset[this.stylePos],d=b-c,e=d/this._state.size*100;return e=Math.round(e/this._state.percentage[2])*this._state.percentage[2],this.options.reversed&&(e=100-e),Math.max(0,Math.min(100,e))},_validateInputValue:function(a){if("number"==typeof a)return a;if(Array.isArray(a))return this._validateArray(a),a;throw new Error(d.formatInvalidInputErrorMsg(a))},_validateArray:function(a){for(var b=0;b<a.length;b++){var c=a[b];if("number"!=typeof c)throw new Error(d.formatInvalidInputErrorMsg(c))}},_setDataVal:function(a){this.element.setAttribute("data-value",a),this.element.setAttribute("value",a),this.element.value=a},_trigger:function(b,c){c=c||0===c?c:void 0;var d=this.eventToCallbackMap[b];if(d&&d.length)for(var e=0;e<d.length;e++){var f=d[e];f(c)}a&&this._triggerJQueryEvent(b,c)},_triggerJQueryEvent:function(a,b){var c={type:a,value:b};this.$element.trigger(c),this.$sliderElem.trigger(c)},_unbindJQueryEventHandlers:function(){this.$element.off(),this.$sliderElem.off()},_setText:function(a,b){"undefined"!=typeof a.textContent?a.textContent=b:"undefined"!=typeof a.innerText&&(a.innerText=b)},_removeClass:function(a,b){for(var c=b.split(" "),d=a.className,e=0;e<c.length;e++){var f=c[e],g=new RegExp("(?:\\s|^)"+f+"(?:\\s|$)");d=d.replace(g," ")}a.className=d.trim()},_addClass:function(a,b){for(var c=b.split(" "),d=a.className,e=0;e<c.length;e++){var f=c[e],g=new RegExp("(?:\\s|^)"+f+"(?:\\s|$)"),h=g.test(d);h||(d+=" "+f)}a.className=d.trim()},_offsetLeft:function(a){return a.getBoundingClientRect().left},_offsetTop:function(a){for(var b=a.offsetTop;(a=a.offsetParent)&&!isNaN(a.offsetTop);)b+=a.offsetTop,"BODY"!==a.tagName&&(b-=a.scrollTop);return b},_offset:function(a){return{left:this._offsetLeft(a),top:this._offsetTop(a)}},_css:function(b,c,d){if(a)a.style(b,c,d);else{var e=c.replace(/^-ms-/,"ms-").replace(/-([\da-z])/gi,function(a,b){return b.toUpperCase()});b.style[e]=d}},_toValue:function(a){return this.options.scale.toValue.apply(this,[a])},_toPercentage:function(a){return this.options.scale.toPercentage.apply(this,[a])},_setTooltipPosition:function(){var a=[this.tooltip,this.tooltip_min,this.tooltip_max];if("vertical"===this.options.orientation){var b=this.options.tooltip_position||"right",c="left"===b?"right":"left";a.forEach(function(a){this._addClass(a,b),a.style[c]="100%"}.bind(this))}else"bottom"===this.options.tooltip_position?a.forEach(function(a){this._addClass(a,"bottom"),a.style.top="22px"}.bind(this)):a.forEach(function(a){this._addClass(a,"top"),a.style.top=-this.tooltip.outerHeight-14+"px"}.bind(this))}},a){var f=a.fn.slider?"bootstrapSlider":"slider";a.bridget(f,b),a(function(){a("input[data-provide=slider]")[f]()})}}(a),b});
\ No newline at end of file
diff --git a/themes/bootstrap3/js/vendor/bootstrap.min.js b/themes/bootstrap3/js/vendor/bootstrap.min.js
index e79c065134f2cfcf3e44a59cffcb5f090232f98f..9bcd2fccaed9442f1460191d6670ca5e8e08520c 100644
--- a/themes/bootstrap3/js/vendor/bootstrap.min.js
+++ b/themes/bootstrap3/js/vendor/bootstrap.min.js
@@ -1,7 +1,7 @@
 /*!
- * Bootstrap v3.3.6 (http://getbootstrap.com)
- * Copyright 2011-2015 Twitter, Inc.
+ * Bootstrap v3.3.7 (http://getbootstrap.com)
+ * Copyright 2011-2016 Twitter, Inc.
  * Licensed under the MIT license
  */
-if("undefined"==typeof jQuery)throw new Error("Bootstrap's JavaScript requires jQuery");+function(a){"use strict";var b=a.fn.jquery.split(" ")[0].split(".");if(b[0]<2&&b[1]<9||1==b[0]&&9==b[1]&&b[2]<1||b[0]>2)throw new Error("Bootstrap's JavaScript requires jQuery version 1.9.1 or higher, but lower than version 3")}(jQuery),+function(a){"use strict";function b(){var a=document.createElement("bootstrap"),b={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd otransitionend",transition:"transitionend"};for(var c in b)if(void 0!==a.style[c])return{end:b[c]};return!1}a.fn.emulateTransitionEnd=function(b){var c=!1,d=this;a(this).one("bsTransitionEnd",function(){c=!0});var e=function(){c||a(d).trigger(a.support.transition.end)};return setTimeout(e,b),this},a(function(){a.support.transition=b(),a.support.transition&&(a.event.special.bsTransitionEnd={bindType:a.support.transition.end,delegateType:a.support.transition.end,handle:function(b){return a(b.target).is(this)?b.handleObj.handler.apply(this,arguments):void 0}})})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var c=a(this),e=c.data("bs.alert");e||c.data("bs.alert",e=new d(this)),"string"==typeof b&&e[b].call(c)})}var c='[data-dismiss="alert"]',d=function(b){a(b).on("click",c,this.close)};d.VERSION="3.3.6",d.TRANSITION_DURATION=150,d.prototype.close=function(b){function c(){g.detach().trigger("closed.bs.alert").remove()}var e=a(this),f=e.attr("data-target");f||(f=e.attr("href"),f=f&&f.replace(/.*(?=#[^\s]*$)/,""));var g=a(f);b&&b.preventDefault(),g.length||(g=e.closest(".alert")),g.trigger(b=a.Event("close.bs.alert")),b.isDefaultPrevented()||(g.removeClass("in"),a.support.transition&&g.hasClass("fade")?g.one("bsTransitionEnd",c).emulateTransitionEnd(d.TRANSITION_DURATION):c())};var e=a.fn.alert;a.fn.alert=b,a.fn.alert.Constructor=d,a.fn.alert.noConflict=function(){return a.fn.alert=e,this},a(document).on("click.bs.alert.data-api",c,d.prototype.close)}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.button"),f="object"==typeof b&&b;e||d.data("bs.button",e=new c(this,f)),"toggle"==b?e.toggle():b&&e.setState(b)})}var c=function(b,d){this.$element=a(b),this.options=a.extend({},c.DEFAULTS,d),this.isLoading=!1};c.VERSION="3.3.6",c.DEFAULTS={loadingText:"loading..."},c.prototype.setState=function(b){var c="disabled",d=this.$element,e=d.is("input")?"val":"html",f=d.data();b+="Text",null==f.resetText&&d.data("resetText",d[e]()),setTimeout(a.proxy(function(){d[e](null==f[b]?this.options[b]:f[b]),"loadingText"==b?(this.isLoading=!0,d.addClass(c).attr(c,c)):this.isLoading&&(this.isLoading=!1,d.removeClass(c).removeAttr(c))},this),0)},c.prototype.toggle=function(){var a=!0,b=this.$element.closest('[data-toggle="buttons"]');if(b.length){var c=this.$element.find("input");"radio"==c.prop("type")?(c.prop("checked")&&(a=!1),b.find(".active").removeClass("active"),this.$element.addClass("active")):"checkbox"==c.prop("type")&&(c.prop("checked")!==this.$element.hasClass("active")&&(a=!1),this.$element.toggleClass("active")),c.prop("checked",this.$element.hasClass("active")),a&&c.trigger("change")}else this.$element.attr("aria-pressed",!this.$element.hasClass("active")),this.$element.toggleClass("active")};var d=a.fn.button;a.fn.button=b,a.fn.button.Constructor=c,a.fn.button.noConflict=function(){return a.fn.button=d,this},a(document).on("click.bs.button.data-api",'[data-toggle^="button"]',function(c){var d=a(c.target);d.hasClass("btn")||(d=d.closest(".btn")),b.call(d,"toggle"),a(c.target).is('input[type="radio"]')||a(c.target).is('input[type="checkbox"]')||c.preventDefault()}).on("focus.bs.button.data-api blur.bs.button.data-api",'[data-toggle^="button"]',function(b){a(b.target).closest(".btn").toggleClass("focus",/^focus(in)?$/.test(b.type))})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.carousel"),f=a.extend({},c.DEFAULTS,d.data(),"object"==typeof b&&b),g="string"==typeof b?b:f.slide;e||d.data("bs.carousel",e=new c(this,f)),"number"==typeof b?e.to(b):g?e[g]():f.interval&&e.pause().cycle()})}var c=function(b,c){this.$element=a(b),this.$indicators=this.$element.find(".carousel-indicators"),this.options=c,this.paused=null,this.sliding=null,this.interval=null,this.$active=null,this.$items=null,this.options.keyboard&&this.$element.on("keydown.bs.carousel",a.proxy(this.keydown,this)),"hover"==this.options.pause&&!("ontouchstart"in document.documentElement)&&this.$element.on("mouseenter.bs.carousel",a.proxy(this.pause,this)).on("mouseleave.bs.carousel",a.proxy(this.cycle,this))};c.VERSION="3.3.6",c.TRANSITION_DURATION=600,c.DEFAULTS={interval:5e3,pause:"hover",wrap:!0,keyboard:!0},c.prototype.keydown=function(a){if(!/input|textarea/i.test(a.target.tagName)){switch(a.which){case 37:this.prev();break;case 39:this.next();break;default:return}a.preventDefault()}},c.prototype.cycle=function(b){return b||(this.paused=!1),this.interval&&clearInterval(this.interval),this.options.interval&&!this.paused&&(this.interval=setInterval(a.proxy(this.next,this),this.options.interval)),this},c.prototype.getItemIndex=function(a){return this.$items=a.parent().children(".item"),this.$items.index(a||this.$active)},c.prototype.getItemForDirection=function(a,b){var c=this.getItemIndex(b),d="prev"==a&&0===c||"next"==a&&c==this.$items.length-1;if(d&&!this.options.wrap)return b;var e="prev"==a?-1:1,f=(c+e)%this.$items.length;return this.$items.eq(f)},c.prototype.to=function(a){var b=this,c=this.getItemIndex(this.$active=this.$element.find(".item.active"));return a>this.$items.length-1||0>a?void 0:this.sliding?this.$element.one("slid.bs.carousel",function(){b.to(a)}):c==a?this.pause().cycle():this.slide(a>c?"next":"prev",this.$items.eq(a))},c.prototype.pause=function(b){return b||(this.paused=!0),this.$element.find(".next, .prev").length&&a.support.transition&&(this.$element.trigger(a.support.transition.end),this.cycle(!0)),this.interval=clearInterval(this.interval),this},c.prototype.next=function(){return this.sliding?void 0:this.slide("next")},c.prototype.prev=function(){return this.sliding?void 0:this.slide("prev")},c.prototype.slide=function(b,d){var e=this.$element.find(".item.active"),f=d||this.getItemForDirection(b,e),g=this.interval,h="next"==b?"left":"right",i=this;if(f.hasClass("active"))return this.sliding=!1;var j=f[0],k=a.Event("slide.bs.carousel",{relatedTarget:j,direction:h});if(this.$element.trigger(k),!k.isDefaultPrevented()){if(this.sliding=!0,g&&this.pause(),this.$indicators.length){this.$indicators.find(".active").removeClass("active");var l=a(this.$indicators.children()[this.getItemIndex(f)]);l&&l.addClass("active")}var m=a.Event("slid.bs.carousel",{relatedTarget:j,direction:h});return a.support.transition&&this.$element.hasClass("slide")?(f.addClass(b),f[0].offsetWidth,e.addClass(h),f.addClass(h),e.one("bsTransitionEnd",function(){f.removeClass([b,h].join(" ")).addClass("active"),e.removeClass(["active",h].join(" ")),i.sliding=!1,setTimeout(function(){i.$element.trigger(m)},0)}).emulateTransitionEnd(c.TRANSITION_DURATION)):(e.removeClass("active"),f.addClass("active"),this.sliding=!1,this.$element.trigger(m)),g&&this.cycle(),this}};var d=a.fn.carousel;a.fn.carousel=b,a.fn.carousel.Constructor=c,a.fn.carousel.noConflict=function(){return a.fn.carousel=d,this};var e=function(c){var d,e=a(this),f=a(e.attr("data-target")||(d=e.attr("href"))&&d.replace(/.*(?=#[^\s]+$)/,""));if(f.hasClass("carousel")){var g=a.extend({},f.data(),e.data()),h=e.attr("data-slide-to");h&&(g.interval=!1),b.call(f,g),h&&f.data("bs.carousel").to(h),c.preventDefault()}};a(document).on("click.bs.carousel.data-api","[data-slide]",e).on("click.bs.carousel.data-api","[data-slide-to]",e),a(window).on("load",function(){a('[data-ride="carousel"]').each(function(){var c=a(this);b.call(c,c.data())})})}(jQuery),+function(a){"use strict";function b(b){var c,d=b.attr("data-target")||(c=b.attr("href"))&&c.replace(/.*(?=#[^\s]+$)/,"");return a(d)}function c(b){return this.each(function(){var c=a(this),e=c.data("bs.collapse"),f=a.extend({},d.DEFAULTS,c.data(),"object"==typeof b&&b);!e&&f.toggle&&/show|hide/.test(b)&&(f.toggle=!1),e||c.data("bs.collapse",e=new d(this,f)),"string"==typeof b&&e[b]()})}var d=function(b,c){this.$element=a(b),this.options=a.extend({},d.DEFAULTS,c),this.$trigger=a('[data-toggle="collapse"][href="#'+b.id+'"],[data-toggle="collapse"][data-target="#'+b.id+'"]'),this.transitioning=null,this.options.parent?this.$parent=this.getParent():this.addAriaAndCollapsedClass(this.$element,this.$trigger),this.options.toggle&&this.toggle()};d.VERSION="3.3.6",d.TRANSITION_DURATION=350,d.DEFAULTS={toggle:!0},d.prototype.dimension=function(){var a=this.$element.hasClass("width");return a?"width":"height"},d.prototype.show=function(){if(!this.transitioning&&!this.$element.hasClass("in")){var b,e=this.$parent&&this.$parent.children(".panel").children(".in, .collapsing");if(!(e&&e.length&&(b=e.data("bs.collapse"),b&&b.transitioning))){var f=a.Event("show.bs.collapse");if(this.$element.trigger(f),!f.isDefaultPrevented()){e&&e.length&&(c.call(e,"hide"),b||e.data("bs.collapse",null));var g=this.dimension();this.$element.removeClass("collapse").addClass("collapsing")[g](0).attr("aria-expanded",!0),this.$trigger.removeClass("collapsed").attr("aria-expanded",!0),this.transitioning=1;var h=function(){this.$element.removeClass("collapsing").addClass("collapse in")[g](""),this.transitioning=0,this.$element.trigger("shown.bs.collapse")};if(!a.support.transition)return h.call(this);var i=a.camelCase(["scroll",g].join("-"));this.$element.one("bsTransitionEnd",a.proxy(h,this)).emulateTransitionEnd(d.TRANSITION_DURATION)[g](this.$element[0][i])}}}},d.prototype.hide=function(){if(!this.transitioning&&this.$element.hasClass("in")){var b=a.Event("hide.bs.collapse");if(this.$element.trigger(b),!b.isDefaultPrevented()){var c=this.dimension();this.$element[c](this.$element[c]())[0].offsetHeight,this.$element.addClass("collapsing").removeClass("collapse in").attr("aria-expanded",!1),this.$trigger.addClass("collapsed").attr("aria-expanded",!1),this.transitioning=1;var e=function(){this.transitioning=0,this.$element.removeClass("collapsing").addClass("collapse").trigger("hidden.bs.collapse")};return a.support.transition?void this.$element[c](0).one("bsTransitionEnd",a.proxy(e,this)).emulateTransitionEnd(d.TRANSITION_DURATION):e.call(this)}}},d.prototype.toggle=function(){this[this.$element.hasClass("in")?"hide":"show"]()},d.prototype.getParent=function(){return a(this.options.parent).find('[data-toggle="collapse"][data-parent="'+this.options.parent+'"]').each(a.proxy(function(c,d){var e=a(d);this.addAriaAndCollapsedClass(b(e),e)},this)).end()},d.prototype.addAriaAndCollapsedClass=function(a,b){var c=a.hasClass("in");a.attr("aria-expanded",c),b.toggleClass("collapsed",!c).attr("aria-expanded",c)};var e=a.fn.collapse;a.fn.collapse=c,a.fn.collapse.Constructor=d,a.fn.collapse.noConflict=function(){return a.fn.collapse=e,this},a(document).on("click.bs.collapse.data-api",'[data-toggle="collapse"]',function(d){var e=a(this);e.attr("data-target")||d.preventDefault();var f=b(e),g=f.data("bs.collapse"),h=g?"toggle":e.data();c.call(f,h)})}(jQuery),+function(a){"use strict";function b(b){var c=b.attr("data-target");c||(c=b.attr("href"),c=c&&/#[A-Za-z]/.test(c)&&c.replace(/.*(?=#[^\s]*$)/,""));var d=c&&a(c);return d&&d.length?d:b.parent()}function c(c){c&&3===c.which||(a(e).remove(),a(f).each(function(){var d=a(this),e=b(d),f={relatedTarget:this};e.hasClass("open")&&(c&&"click"==c.type&&/input|textarea/i.test(c.target.tagName)&&a.contains(e[0],c.target)||(e.trigger(c=a.Event("hide.bs.dropdown",f)),c.isDefaultPrevented()||(d.attr("aria-expanded","false"),e.removeClass("open").trigger(a.Event("hidden.bs.dropdown",f)))))}))}function d(b){return this.each(function(){var c=a(this),d=c.data("bs.dropdown");d||c.data("bs.dropdown",d=new g(this)),"string"==typeof b&&d[b].call(c)})}var e=".dropdown-backdrop",f='[data-toggle="dropdown"]',g=function(b){a(b).on("click.bs.dropdown",this.toggle)};g.VERSION="3.3.6",g.prototype.toggle=function(d){var e=a(this);if(!e.is(".disabled, :disabled")){var f=b(e),g=f.hasClass("open");if(c(),!g){"ontouchstart"in document.documentElement&&!f.closest(".navbar-nav").length&&a(document.createElement("div")).addClass("dropdown-backdrop").insertAfter(a(this)).on("click",c);var h={relatedTarget:this};if(f.trigger(d=a.Event("show.bs.dropdown",h)),d.isDefaultPrevented())return;e.trigger("focus").attr("aria-expanded","true"),f.toggleClass("open").trigger(a.Event("shown.bs.dropdown",h))}return!1}},g.prototype.keydown=function(c){if(/(38|40|27|32)/.test(c.which)&&!/input|textarea/i.test(c.target.tagName)){var d=a(this);if(c.preventDefault(),c.stopPropagation(),!d.is(".disabled, :disabled")){var e=b(d),g=e.hasClass("open");if(!g&&27!=c.which||g&&27==c.which)return 27==c.which&&e.find(f).trigger("focus"),d.trigger("click");var h=" li:not(.disabled):visible a",i=e.find(".dropdown-menu"+h);if(i.length){var j=i.index(c.target);38==c.which&&j>0&&j--,40==c.which&&j<i.length-1&&j++,~j||(j=0),i.eq(j).trigger("focus")}}}};var h=a.fn.dropdown;a.fn.dropdown=d,a.fn.dropdown.Constructor=g,a.fn.dropdown.noConflict=function(){return a.fn.dropdown=h,this},a(document).on("click.bs.dropdown.data-api",c).on("click.bs.dropdown.data-api",".dropdown form",function(a){a.stopPropagation()}).on("click.bs.dropdown.data-api",f,g.prototype.toggle).on("keydown.bs.dropdown.data-api",f,g.prototype.keydown).on("keydown.bs.dropdown.data-api",".dropdown-menu",g.prototype.keydown)}(jQuery),+function(a){"use strict";function b(b,d){return this.each(function(){var e=a(this),f=e.data("bs.modal"),g=a.extend({},c.DEFAULTS,e.data(),"object"==typeof b&&b);f||e.data("bs.modal",f=new c(this,g)),"string"==typeof b?f[b](d):g.show&&f.show(d)})}var c=function(b,c){this.options=c,this.$body=a(document.body),this.$element=a(b),this.$dialog=this.$element.find(".modal-dialog"),this.$backdrop=null,this.isShown=null,this.originalBodyPad=null,this.scrollbarWidth=0,this.ignoreBackdropClick=!1,this.options.remote&&this.$element.find(".modal-content").load(this.options.remote,a.proxy(function(){this.$element.trigger("loaded.bs.modal")},this))};c.VERSION="3.3.6",c.TRANSITION_DURATION=300,c.BACKDROP_TRANSITION_DURATION=150,c.DEFAULTS={backdrop:!0,keyboard:!0,show:!0},c.prototype.toggle=function(a){return this.isShown?this.hide():this.show(a)},c.prototype.show=function(b){var d=this,e=a.Event("show.bs.modal",{relatedTarget:b});this.$element.trigger(e),this.isShown||e.isDefaultPrevented()||(this.isShown=!0,this.checkScrollbar(),this.setScrollbar(),this.$body.addClass("modal-open"),this.escape(),this.resize(),this.$element.on("click.dismiss.bs.modal",'[data-dismiss="modal"]',a.proxy(this.hide,this)),this.$dialog.on("mousedown.dismiss.bs.modal",function(){d.$element.one("mouseup.dismiss.bs.modal",function(b){a(b.target).is(d.$element)&&(d.ignoreBackdropClick=!0)})}),this.backdrop(function(){var e=a.support.transition&&d.$element.hasClass("fade");d.$element.parent().length||d.$element.appendTo(d.$body),d.$element.show().scrollTop(0),d.adjustDialog(),e&&d.$element[0].offsetWidth,d.$element.addClass("in"),d.enforceFocus();var f=a.Event("shown.bs.modal",{relatedTarget:b});e?d.$dialog.one("bsTransitionEnd",function(){d.$element.trigger("focus").trigger(f)}).emulateTransitionEnd(c.TRANSITION_DURATION):d.$element.trigger("focus").trigger(f)}))},c.prototype.hide=function(b){b&&b.preventDefault(),b=a.Event("hide.bs.modal"),this.$element.trigger(b),this.isShown&&!b.isDefaultPrevented()&&(this.isShown=!1,this.escape(),this.resize(),a(document).off("focusin.bs.modal"),this.$element.removeClass("in").off("click.dismiss.bs.modal").off("mouseup.dismiss.bs.modal"),this.$dialog.off("mousedown.dismiss.bs.modal"),a.support.transition&&this.$element.hasClass("fade")?this.$element.one("bsTransitionEnd",a.proxy(this.hideModal,this)).emulateTransitionEnd(c.TRANSITION_DURATION):this.hideModal())},c.prototype.enforceFocus=function(){a(document).off("focusin.bs.modal").on("focusin.bs.modal",a.proxy(function(a){this.$element[0]===a.target||this.$element.has(a.target).length||this.$element.trigger("focus")},this))},c.prototype.escape=function(){this.isShown&&this.options.keyboard?this.$element.on("keydown.dismiss.bs.modal",a.proxy(function(a){27==a.which&&this.hide()},this)):this.isShown||this.$element.off("keydown.dismiss.bs.modal")},c.prototype.resize=function(){this.isShown?a(window).on("resize.bs.modal",a.proxy(this.handleUpdate,this)):a(window).off("resize.bs.modal")},c.prototype.hideModal=function(){var a=this;this.$element.hide(),this.backdrop(function(){a.$body.removeClass("modal-open"),a.resetAdjustments(),a.resetScrollbar(),a.$element.trigger("hidden.bs.modal")})},c.prototype.removeBackdrop=function(){this.$backdrop&&this.$backdrop.remove(),this.$backdrop=null},c.prototype.backdrop=function(b){var d=this,e=this.$element.hasClass("fade")?"fade":"";if(this.isShown&&this.options.backdrop){var f=a.support.transition&&e;if(this.$backdrop=a(document.createElement("div")).addClass("modal-backdrop "+e).appendTo(this.$body),this.$element.on("click.dismiss.bs.modal",a.proxy(function(a){return this.ignoreBackdropClick?void(this.ignoreBackdropClick=!1):void(a.target===a.currentTarget&&("static"==this.options.backdrop?this.$element[0].focus():this.hide()))},this)),f&&this.$backdrop[0].offsetWidth,this.$backdrop.addClass("in"),!b)return;f?this.$backdrop.one("bsTransitionEnd",b).emulateTransitionEnd(c.BACKDROP_TRANSITION_DURATION):b()}else if(!this.isShown&&this.$backdrop){this.$backdrop.removeClass("in");var g=function(){d.removeBackdrop(),b&&b()};a.support.transition&&this.$element.hasClass("fade")?this.$backdrop.one("bsTransitionEnd",g).emulateTransitionEnd(c.BACKDROP_TRANSITION_DURATION):g()}else b&&b()},c.prototype.handleUpdate=function(){this.adjustDialog()},c.prototype.adjustDialog=function(){var a=this.$element[0].scrollHeight>document.documentElement.clientHeight;this.$element.css({paddingLeft:!this.bodyIsOverflowing&&a?this.scrollbarWidth:"",paddingRight:this.bodyIsOverflowing&&!a?this.scrollbarWidth:""})},c.prototype.resetAdjustments=function(){this.$element.css({paddingLeft:"",paddingRight:""})},c.prototype.checkScrollbar=function(){var a=window.innerWidth;if(!a){var b=document.documentElement.getBoundingClientRect();a=b.right-Math.abs(b.left)}this.bodyIsOverflowing=document.body.clientWidth<a,this.scrollbarWidth=this.measureScrollbar()},c.prototype.setScrollbar=function(){var a=parseInt(this.$body.css("padding-right")||0,10);this.originalBodyPad=document.body.style.paddingRight||"",this.bodyIsOverflowing&&this.$body.css("padding-right",a+this.scrollbarWidth)},c.prototype.resetScrollbar=function(){this.$body.css("padding-right",this.originalBodyPad)},c.prototype.measureScrollbar=function(){var a=document.createElement("div");a.className="modal-scrollbar-measure",this.$body.append(a);var b=a.offsetWidth-a.clientWidth;return this.$body[0].removeChild(a),b};var d=a.fn.modal;a.fn.modal=b,a.fn.modal.Constructor=c,a.fn.modal.noConflict=function(){return a.fn.modal=d,this},a(document).on("click.bs.modal.data-api",'[data-toggle="modal"]',function(c){var d=a(this),e=d.attr("href"),f=a(d.attr("data-target")||e&&e.replace(/.*(?=#[^\s]+$)/,"")),g=f.data("bs.modal")?"toggle":a.extend({remote:!/#/.test(e)&&e},f.data(),d.data());d.is("a")&&c.preventDefault(),f.one("show.bs.modal",function(a){a.isDefaultPrevented()||f.one("hidden.bs.modal",function(){d.is(":visible")&&d.trigger("focus")})}),b.call(f,g,this)})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.tooltip"),f="object"==typeof b&&b;(e||!/destroy|hide/.test(b))&&(e||d.data("bs.tooltip",e=new c(this,f)),"string"==typeof b&&e[b]())})}var c=function(a,b){this.type=null,this.options=null,this.enabled=null,this.timeout=null,this.hoverState=null,this.$element=null,this.inState=null,this.init("tooltip",a,b)};c.VERSION="3.3.6",c.TRANSITION_DURATION=150,c.DEFAULTS={animation:!0,placement:"top",selector:!1,template:'<div class="tooltip" role="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>',trigger:"hover focus",title:"",delay:0,html:!1,container:!1,viewport:{selector:"body",padding:0}},c.prototype.init=function(b,c,d){if(this.enabled=!0,this.type=b,this.$element=a(c),this.options=this.getOptions(d),this.$viewport=this.options.viewport&&a(a.isFunction(this.options.viewport)?this.options.viewport.call(this,this.$element):this.options.viewport.selector||this.options.viewport),this.inState={click:!1,hover:!1,focus:!1},this.$element[0]instanceof document.constructor&&!this.options.selector)throw new Error("`selector` option must be specified when initializing "+this.type+" on the window.document object!");for(var e=this.options.trigger.split(" "),f=e.length;f--;){var g=e[f];if("click"==g)this.$element.on("click."+this.type,this.options.selector,a.proxy(this.toggle,this));else if("manual"!=g){var h="hover"==g?"mouseenter":"focusin",i="hover"==g?"mouseleave":"focusout";this.$element.on(h+"."+this.type,this.options.selector,a.proxy(this.enter,this)),this.$element.on(i+"."+this.type,this.options.selector,a.proxy(this.leave,this))}}this.options.selector?this._options=a.extend({},this.options,{trigger:"manual",selector:""}):this.fixTitle()},c.prototype.getDefaults=function(){return c.DEFAULTS},c.prototype.getOptions=function(b){return b=a.extend({},this.getDefaults(),this.$element.data(),b),b.delay&&"number"==typeof b.delay&&(b.delay={show:b.delay,hide:b.delay}),b},c.prototype.getDelegateOptions=function(){var b={},c=this.getDefaults();return this._options&&a.each(this._options,function(a,d){c[a]!=d&&(b[a]=d)}),b},c.prototype.enter=function(b){var c=b instanceof this.constructor?b:a(b.currentTarget).data("bs."+this.type);return c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data("bs."+this.type,c)),b instanceof a.Event&&(c.inState["focusin"==b.type?"focus":"hover"]=!0),c.tip().hasClass("in")||"in"==c.hoverState?void(c.hoverState="in"):(clearTimeout(c.timeout),c.hoverState="in",c.options.delay&&c.options.delay.show?void(c.timeout=setTimeout(function(){"in"==c.hoverState&&c.show()},c.options.delay.show)):c.show())},c.prototype.isInStateTrue=function(){for(var a in this.inState)if(this.inState[a])return!0;return!1},c.prototype.leave=function(b){var c=b instanceof this.constructor?b:a(b.currentTarget).data("bs."+this.type);return c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data("bs."+this.type,c)),b instanceof a.Event&&(c.inState["focusout"==b.type?"focus":"hover"]=!1),c.isInStateTrue()?void 0:(clearTimeout(c.timeout),c.hoverState="out",c.options.delay&&c.options.delay.hide?void(c.timeout=setTimeout(function(){"out"==c.hoverState&&c.hide()},c.options.delay.hide)):c.hide())},c.prototype.show=function(){var b=a.Event("show.bs."+this.type);if(this.hasContent()&&this.enabled){this.$element.trigger(b);var d=a.contains(this.$element[0].ownerDocument.documentElement,this.$element[0]);if(b.isDefaultPrevented()||!d)return;var e=this,f=this.tip(),g=this.getUID(this.type);this.setContent(),f.attr("id",g),this.$element.attr("aria-describedby",g),this.options.animation&&f.addClass("fade");var h="function"==typeof this.options.placement?this.options.placement.call(this,f[0],this.$element[0]):this.options.placement,i=/\s?auto?\s?/i,j=i.test(h);j&&(h=h.replace(i,"")||"top"),f.detach().css({top:0,left:0,display:"block"}).addClass(h).data("bs."+this.type,this),this.options.container?f.appendTo(this.options.container):f.insertAfter(this.$element),this.$element.trigger("inserted.bs."+this.type);var k=this.getPosition(),l=f[0].offsetWidth,m=f[0].offsetHeight;if(j){var n=h,o=this.getPosition(this.$viewport);h="bottom"==h&&k.bottom+m>o.bottom?"top":"top"==h&&k.top-m<o.top?"bottom":"right"==h&&k.right+l>o.width?"left":"left"==h&&k.left-l<o.left?"right":h,f.removeClass(n).addClass(h)}var p=this.getCalculatedOffset(h,k,l,m);this.applyPlacement(p,h);var q=function(){var a=e.hoverState;e.$element.trigger("shown.bs."+e.type),e.hoverState=null,"out"==a&&e.leave(e)};a.support.transition&&this.$tip.hasClass("fade")?f.one("bsTransitionEnd",q).emulateTransitionEnd(c.TRANSITION_DURATION):q()}},c.prototype.applyPlacement=function(b,c){var d=this.tip(),e=d[0].offsetWidth,f=d[0].offsetHeight,g=parseInt(d.css("margin-top"),10),h=parseInt(d.css("margin-left"),10);isNaN(g)&&(g=0),isNaN(h)&&(h=0),b.top+=g,b.left+=h,a.offset.setOffset(d[0],a.extend({using:function(a){d.css({top:Math.round(a.top),left:Math.round(a.left)})}},b),0),d.addClass("in");var i=d[0].offsetWidth,j=d[0].offsetHeight;"top"==c&&j!=f&&(b.top=b.top+f-j);var k=this.getViewportAdjustedDelta(c,b,i,j);k.left?b.left+=k.left:b.top+=k.top;var l=/top|bottom/.test(c),m=l?2*k.left-e+i:2*k.top-f+j,n=l?"offsetWidth":"offsetHeight";d.offset(b),this.replaceArrow(m,d[0][n],l)},c.prototype.replaceArrow=function(a,b,c){this.arrow().css(c?"left":"top",50*(1-a/b)+"%").css(c?"top":"left","")},c.prototype.setContent=function(){var a=this.tip(),b=this.getTitle();a.find(".tooltip-inner")[this.options.html?"html":"text"](b),a.removeClass("fade in top bottom left right")},c.prototype.hide=function(b){function d(){"in"!=e.hoverState&&f.detach(),e.$element.removeAttr("aria-describedby").trigger("hidden.bs."+e.type),b&&b()}var e=this,f=a(this.$tip),g=a.Event("hide.bs."+this.type);return this.$element.trigger(g),g.isDefaultPrevented()?void 0:(f.removeClass("in"),a.support.transition&&f.hasClass("fade")?f.one("bsTransitionEnd",d).emulateTransitionEnd(c.TRANSITION_DURATION):d(),this.hoverState=null,this)},c.prototype.fixTitle=function(){var a=this.$element;(a.attr("title")||"string"!=typeof a.attr("data-original-title"))&&a.attr("data-original-title",a.attr("title")||"").attr("title","")},c.prototype.hasContent=function(){return this.getTitle()},c.prototype.getPosition=function(b){b=b||this.$element;var c=b[0],d="BODY"==c.tagName,e=c.getBoundingClientRect();null==e.width&&(e=a.extend({},e,{width:e.right-e.left,height:e.bottom-e.top}));var f=d?{top:0,left:0}:b.offset(),g={scroll:d?document.documentElement.scrollTop||document.body.scrollTop:b.scrollTop()},h=d?{width:a(window).width(),height:a(window).height()}:null;return a.extend({},e,g,h,f)},c.prototype.getCalculatedOffset=function(a,b,c,d){return"bottom"==a?{top:b.top+b.height,left:b.left+b.width/2-c/2}:"top"==a?{top:b.top-d,left:b.left+b.width/2-c/2}:"left"==a?{top:b.top+b.height/2-d/2,left:b.left-c}:{top:b.top+b.height/2-d/2,left:b.left+b.width}},c.prototype.getViewportAdjustedDelta=function(a,b,c,d){var e={top:0,left:0};if(!this.$viewport)return e;var f=this.options.viewport&&this.options.viewport.padding||0,g=this.getPosition(this.$viewport);if(/right|left/.test(a)){var h=b.top-f-g.scroll,i=b.top+f-g.scroll+d;h<g.top?e.top=g.top-h:i>g.top+g.height&&(e.top=g.top+g.height-i)}else{var j=b.left-f,k=b.left+f+c;j<g.left?e.left=g.left-j:k>g.right&&(e.left=g.left+g.width-k)}return e},c.prototype.getTitle=function(){var a,b=this.$element,c=this.options;return a=b.attr("data-original-title")||("function"==typeof c.title?c.title.call(b[0]):c.title)},c.prototype.getUID=function(a){do a+=~~(1e6*Math.random());while(document.getElementById(a));return a},c.prototype.tip=function(){if(!this.$tip&&(this.$tip=a(this.options.template),1!=this.$tip.length))throw new Error(this.type+" `template` option must consist of exactly 1 top-level element!");return this.$tip},c.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".tooltip-arrow")},c.prototype.enable=function(){this.enabled=!0},c.prototype.disable=function(){this.enabled=!1},c.prototype.toggleEnabled=function(){this.enabled=!this.enabled},c.prototype.toggle=function(b){var c=this;b&&(c=a(b.currentTarget).data("bs."+this.type),c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data("bs."+this.type,c))),b?(c.inState.click=!c.inState.click,c.isInStateTrue()?c.enter(c):c.leave(c)):c.tip().hasClass("in")?c.leave(c):c.enter(c)},c.prototype.destroy=function(){var a=this;clearTimeout(this.timeout),this.hide(function(){a.$element.off("."+a.type).removeData("bs."+a.type),a.$tip&&a.$tip.detach(),a.$tip=null,a.$arrow=null,a.$viewport=null})};var d=a.fn.tooltip;a.fn.tooltip=b,a.fn.tooltip.Constructor=c,a.fn.tooltip.noConflict=function(){return a.fn.tooltip=d,this}}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.popover"),f="object"==typeof b&&b;(e||!/destroy|hide/.test(b))&&(e||d.data("bs.popover",e=new c(this,f)),"string"==typeof b&&e[b]())})}var c=function(a,b){this.init("popover",a,b)};if(!a.fn.tooltip)throw new Error("Popover requires tooltip.js");c.VERSION="3.3.6",c.DEFAULTS=a.extend({},a.fn.tooltip.Constructor.DEFAULTS,{placement:"right",trigger:"click",content:"",template:'<div class="popover" role="tooltip"><div class="arrow"></div><h3 class="popover-title"></h3><div class="popover-content"></div></div>'}),c.prototype=a.extend({},a.fn.tooltip.Constructor.prototype),c.prototype.constructor=c,c.prototype.getDefaults=function(){return c.DEFAULTS},c.prototype.setContent=function(){var a=this.tip(),b=this.getTitle(),c=this.getContent();a.find(".popover-title")[this.options.html?"html":"text"](b),a.find(".popover-content").children().detach().end()[this.options.html?"string"==typeof c?"html":"append":"text"](c),a.removeClass("fade top bottom left right in"),a.find(".popover-title").html()||a.find(".popover-title").hide()},c.prototype.hasContent=function(){return this.getTitle()||this.getContent()},c.prototype.getContent=function(){var a=this.$element,b=this.options;return a.attr("data-content")||("function"==typeof b.content?b.content.call(a[0]):b.content)},c.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".arrow")};var d=a.fn.popover;a.fn.popover=b,a.fn.popover.Constructor=c,a.fn.popover.noConflict=function(){return a.fn.popover=d,this}}(jQuery),+function(a){"use strict";function b(c,d){this.$body=a(document.body),this.$scrollElement=a(a(c).is(document.body)?window:c),this.options=a.extend({},b.DEFAULTS,d),this.selector=(this.options.target||"")+" .nav li > a",this.offsets=[],this.targets=[],this.activeTarget=null,this.scrollHeight=0,this.$scrollElement.on("scroll.bs.scrollspy",a.proxy(this.process,this)),this.refresh(),this.process()}function c(c){return this.each(function(){var d=a(this),e=d.data("bs.scrollspy"),f="object"==typeof c&&c;e||d.data("bs.scrollspy",e=new b(this,f)),"string"==typeof c&&e[c]()})}b.VERSION="3.3.6",b.DEFAULTS={offset:10},b.prototype.getScrollHeight=function(){return this.$scrollElement[0].scrollHeight||Math.max(this.$body[0].scrollHeight,document.documentElement.scrollHeight)},b.prototype.refresh=function(){var b=this,c="offset",d=0;this.offsets=[],this.targets=[],this.scrollHeight=this.getScrollHeight(),a.isWindow(this.$scrollElement[0])||(c="position",d=this.$scrollElement.scrollTop()),this.$body.find(this.selector).map(function(){var b=a(this),e=b.data("target")||b.attr("href"),f=/^#./.test(e)&&a(e);return f&&f.length&&f.is(":visible")&&[[f[c]().top+d,e]]||null}).sort(function(a,b){return a[0]-b[0]}).each(function(){b.offsets.push(this[0]),b.targets.push(this[1])})},b.prototype.process=function(){var a,b=this.$scrollElement.scrollTop()+this.options.offset,c=this.getScrollHeight(),d=this.options.offset+c-this.$scrollElement.height(),e=this.offsets,f=this.targets,g=this.activeTarget;if(this.scrollHeight!=c&&this.refresh(),b>=d)return g!=(a=f[f.length-1])&&this.activate(a);if(g&&b<e[0])return this.activeTarget=null,this.clear();for(a=e.length;a--;)g!=f[a]&&b>=e[a]&&(void 0===e[a+1]||b<e[a+1])&&this.activate(f[a])},b.prototype.activate=function(b){this.activeTarget=b,this.clear();var c=this.selector+'[data-target="'+b+'"],'+this.selector+'[href="'+b+'"]',d=a(c).parents("li").addClass("active");
-d.parent(".dropdown-menu").length&&(d=d.closest("li.dropdown").addClass("active")),d.trigger("activate.bs.scrollspy")},b.prototype.clear=function(){a(this.selector).parentsUntil(this.options.target,".active").removeClass("active")};var d=a.fn.scrollspy;a.fn.scrollspy=c,a.fn.scrollspy.Constructor=b,a.fn.scrollspy.noConflict=function(){return a.fn.scrollspy=d,this},a(window).on("load.bs.scrollspy.data-api",function(){a('[data-spy="scroll"]').each(function(){var b=a(this);c.call(b,b.data())})})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.tab");e||d.data("bs.tab",e=new c(this)),"string"==typeof b&&e[b]()})}var c=function(b){this.element=a(b)};c.VERSION="3.3.6",c.TRANSITION_DURATION=150,c.prototype.show=function(){var b=this.element,c=b.closest("ul:not(.dropdown-menu)"),d=b.data("target");if(d||(d=b.attr("href"),d=d&&d.replace(/.*(?=#[^\s]*$)/,"")),!b.parent("li").hasClass("active")){var e=c.find(".active:last a"),f=a.Event("hide.bs.tab",{relatedTarget:b[0]}),g=a.Event("show.bs.tab",{relatedTarget:e[0]});if(e.trigger(f),b.trigger(g),!g.isDefaultPrevented()&&!f.isDefaultPrevented()){var h=a(d);this.activate(b.closest("li"),c),this.activate(h,h.parent(),function(){e.trigger({type:"hidden.bs.tab",relatedTarget:b[0]}),b.trigger({type:"shown.bs.tab",relatedTarget:e[0]})})}}},c.prototype.activate=function(b,d,e){function f(){g.removeClass("active").find("> .dropdown-menu > .active").removeClass("active").end().find('[data-toggle="tab"]').attr("aria-expanded",!1),b.addClass("active").find('[data-toggle="tab"]').attr("aria-expanded",!0),h?(b[0].offsetWidth,b.addClass("in")):b.removeClass("fade"),b.parent(".dropdown-menu").length&&b.closest("li.dropdown").addClass("active").end().find('[data-toggle="tab"]').attr("aria-expanded",!0),e&&e()}var g=d.find("> .active"),h=e&&a.support.transition&&(g.length&&g.hasClass("fade")||!!d.find("> .fade").length);g.length&&h?g.one("bsTransitionEnd",f).emulateTransitionEnd(c.TRANSITION_DURATION):f(),g.removeClass("in")};var d=a.fn.tab;a.fn.tab=b,a.fn.tab.Constructor=c,a.fn.tab.noConflict=function(){return a.fn.tab=d,this};var e=function(c){c.preventDefault(),b.call(a(this),"show")};a(document).on("click.bs.tab.data-api",'[data-toggle="tab"]',e).on("click.bs.tab.data-api",'[data-toggle="pill"]',e)}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.affix"),f="object"==typeof b&&b;e||d.data("bs.affix",e=new c(this,f)),"string"==typeof b&&e[b]()})}var c=function(b,d){this.options=a.extend({},c.DEFAULTS,d),this.$target=a(this.options.target).on("scroll.bs.affix.data-api",a.proxy(this.checkPosition,this)).on("click.bs.affix.data-api",a.proxy(this.checkPositionWithEventLoop,this)),this.$element=a(b),this.affixed=null,this.unpin=null,this.pinnedOffset=null,this.checkPosition()};c.VERSION="3.3.6",c.RESET="affix affix-top affix-bottom",c.DEFAULTS={offset:0,target:window},c.prototype.getState=function(a,b,c,d){var e=this.$target.scrollTop(),f=this.$element.offset(),g=this.$target.height();if(null!=c&&"top"==this.affixed)return c>e?"top":!1;if("bottom"==this.affixed)return null!=c?e+this.unpin<=f.top?!1:"bottom":a-d>=e+g?!1:"bottom";var h=null==this.affixed,i=h?e:f.top,j=h?g:b;return null!=c&&c>=e?"top":null!=d&&i+j>=a-d?"bottom":!1},c.prototype.getPinnedOffset=function(){if(this.pinnedOffset)return this.pinnedOffset;this.$element.removeClass(c.RESET).addClass("affix");var a=this.$target.scrollTop(),b=this.$element.offset();return this.pinnedOffset=b.top-a},c.prototype.checkPositionWithEventLoop=function(){setTimeout(a.proxy(this.checkPosition,this),1)},c.prototype.checkPosition=function(){if(this.$element.is(":visible")){var b=this.$element.height(),d=this.options.offset,e=d.top,f=d.bottom,g=Math.max(a(document).height(),a(document.body).height());"object"!=typeof d&&(f=e=d),"function"==typeof e&&(e=d.top(this.$element)),"function"==typeof f&&(f=d.bottom(this.$element));var h=this.getState(g,b,e,f);if(this.affixed!=h){null!=this.unpin&&this.$element.css("top","");var i="affix"+(h?"-"+h:""),j=a.Event(i+".bs.affix");if(this.$element.trigger(j),j.isDefaultPrevented())return;this.affixed=h,this.unpin="bottom"==h?this.getPinnedOffset():null,this.$element.removeClass(c.RESET).addClass(i).trigger(i.replace("affix","affixed")+".bs.affix")}"bottom"==h&&this.$element.offset({top:g-b-f})}};var d=a.fn.affix;a.fn.affix=b,a.fn.affix.Constructor=c,a.fn.affix.noConflict=function(){return a.fn.affix=d,this},a(window).on("load",function(){a('[data-spy="affix"]').each(function(){var c=a(this),d=c.data();d.offset=d.offset||{},null!=d.offsetBottom&&(d.offset.bottom=d.offsetBottom),null!=d.offsetTop&&(d.offset.top=d.offsetTop),b.call(c,d)})})}(jQuery);
\ No newline at end of file
+if("undefined"==typeof jQuery)throw new Error("Bootstrap's JavaScript requires jQuery");+function(a){"use strict";var b=a.fn.jquery.split(" ")[0].split(".");if(b[0]<2&&b[1]<9||1==b[0]&&9==b[1]&&b[2]<1||b[0]>3)throw new Error("Bootstrap's JavaScript requires jQuery version 1.9.1 or higher, but lower than version 4")}(jQuery),+function(a){"use strict";function b(){var a=document.createElement("bootstrap"),b={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd otransitionend",transition:"transitionend"};for(var c in b)if(void 0!==a.style[c])return{end:b[c]};return!1}a.fn.emulateTransitionEnd=function(b){var c=!1,d=this;a(this).one("bsTransitionEnd",function(){c=!0});var e=function(){c||a(d).trigger(a.support.transition.end)};return setTimeout(e,b),this},a(function(){a.support.transition=b(),a.support.transition&&(a.event.special.bsTransitionEnd={bindType:a.support.transition.end,delegateType:a.support.transition.end,handle:function(b){if(a(b.target).is(this))return b.handleObj.handler.apply(this,arguments)}})})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var c=a(this),e=c.data("bs.alert");e||c.data("bs.alert",e=new d(this)),"string"==typeof b&&e[b].call(c)})}var c='[data-dismiss="alert"]',d=function(b){a(b).on("click",c,this.close)};d.VERSION="3.3.7",d.TRANSITION_DURATION=150,d.prototype.close=function(b){function c(){g.detach().trigger("closed.bs.alert").remove()}var e=a(this),f=e.attr("data-target");f||(f=e.attr("href"),f=f&&f.replace(/.*(?=#[^\s]*$)/,""));var g=a("#"===f?[]:f);b&&b.preventDefault(),g.length||(g=e.closest(".alert")),g.trigger(b=a.Event("close.bs.alert")),b.isDefaultPrevented()||(g.removeClass("in"),a.support.transition&&g.hasClass("fade")?g.one("bsTransitionEnd",c).emulateTransitionEnd(d.TRANSITION_DURATION):c())};var e=a.fn.alert;a.fn.alert=b,a.fn.alert.Constructor=d,a.fn.alert.noConflict=function(){return a.fn.alert=e,this},a(document).on("click.bs.alert.data-api",c,d.prototype.close)}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.button"),f="object"==typeof b&&b;e||d.data("bs.button",e=new c(this,f)),"toggle"==b?e.toggle():b&&e.setState(b)})}var c=function(b,d){this.$element=a(b),this.options=a.extend({},c.DEFAULTS,d),this.isLoading=!1};c.VERSION="3.3.7",c.DEFAULTS={loadingText:"loading..."},c.prototype.setState=function(b){var c="disabled",d=this.$element,e=d.is("input")?"val":"html",f=d.data();b+="Text",null==f.resetText&&d.data("resetText",d[e]()),setTimeout(a.proxy(function(){d[e](null==f[b]?this.options[b]:f[b]),"loadingText"==b?(this.isLoading=!0,d.addClass(c).attr(c,c).prop(c,!0)):this.isLoading&&(this.isLoading=!1,d.removeClass(c).removeAttr(c).prop(c,!1))},this),0)},c.prototype.toggle=function(){var a=!0,b=this.$element.closest('[data-toggle="buttons"]');if(b.length){var c=this.$element.find("input");"radio"==c.prop("type")?(c.prop("checked")&&(a=!1),b.find(".active").removeClass("active"),this.$element.addClass("active")):"checkbox"==c.prop("type")&&(c.prop("checked")!==this.$element.hasClass("active")&&(a=!1),this.$element.toggleClass("active")),c.prop("checked",this.$element.hasClass("active")),a&&c.trigger("change")}else this.$element.attr("aria-pressed",!this.$element.hasClass("active")),this.$element.toggleClass("active")};var d=a.fn.button;a.fn.button=b,a.fn.button.Constructor=c,a.fn.button.noConflict=function(){return a.fn.button=d,this},a(document).on("click.bs.button.data-api",'[data-toggle^="button"]',function(c){var d=a(c.target).closest(".btn");b.call(d,"toggle"),a(c.target).is('input[type="radio"], input[type="checkbox"]')||(c.preventDefault(),d.is("input,button")?d.trigger("focus"):d.find("input:visible,button:visible").first().trigger("focus"))}).on("focus.bs.button.data-api blur.bs.button.data-api",'[data-toggle^="button"]',function(b){a(b.target).closest(".btn").toggleClass("focus",/^focus(in)?$/.test(b.type))})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.carousel"),f=a.extend({},c.DEFAULTS,d.data(),"object"==typeof b&&b),g="string"==typeof b?b:f.slide;e||d.data("bs.carousel",e=new c(this,f)),"number"==typeof b?e.to(b):g?e[g]():f.interval&&e.pause().cycle()})}var c=function(b,c){this.$element=a(b),this.$indicators=this.$element.find(".carousel-indicators"),this.options=c,this.paused=null,this.sliding=null,this.interval=null,this.$active=null,this.$items=null,this.options.keyboard&&this.$element.on("keydown.bs.carousel",a.proxy(this.keydown,this)),"hover"==this.options.pause&&!("ontouchstart"in document.documentElement)&&this.$element.on("mouseenter.bs.carousel",a.proxy(this.pause,this)).on("mouseleave.bs.carousel",a.proxy(this.cycle,this))};c.VERSION="3.3.7",c.TRANSITION_DURATION=600,c.DEFAULTS={interval:5e3,pause:"hover",wrap:!0,keyboard:!0},c.prototype.keydown=function(a){if(!/input|textarea/i.test(a.target.tagName)){switch(a.which){case 37:this.prev();break;case 39:this.next();break;default:return}a.preventDefault()}},c.prototype.cycle=function(b){return b||(this.paused=!1),this.interval&&clearInterval(this.interval),this.options.interval&&!this.paused&&(this.interval=setInterval(a.proxy(this.next,this),this.options.interval)),this},c.prototype.getItemIndex=function(a){return this.$items=a.parent().children(".item"),this.$items.index(a||this.$active)},c.prototype.getItemForDirection=function(a,b){var c=this.getItemIndex(b),d="prev"==a&&0===c||"next"==a&&c==this.$items.length-1;if(d&&!this.options.wrap)return b;var e="prev"==a?-1:1,f=(c+e)%this.$items.length;return this.$items.eq(f)},c.prototype.to=function(a){var b=this,c=this.getItemIndex(this.$active=this.$element.find(".item.active"));if(!(a>this.$items.length-1||a<0))return this.sliding?this.$element.one("slid.bs.carousel",function(){b.to(a)}):c==a?this.pause().cycle():this.slide(a>c?"next":"prev",this.$items.eq(a))},c.prototype.pause=function(b){return b||(this.paused=!0),this.$element.find(".next, .prev").length&&a.support.transition&&(this.$element.trigger(a.support.transition.end),this.cycle(!0)),this.interval=clearInterval(this.interval),this},c.prototype.next=function(){if(!this.sliding)return this.slide("next")},c.prototype.prev=function(){if(!this.sliding)return this.slide("prev")},c.prototype.slide=function(b,d){var e=this.$element.find(".item.active"),f=d||this.getItemForDirection(b,e),g=this.interval,h="next"==b?"left":"right",i=this;if(f.hasClass("active"))return this.sliding=!1;var j=f[0],k=a.Event("slide.bs.carousel",{relatedTarget:j,direction:h});if(this.$element.trigger(k),!k.isDefaultPrevented()){if(this.sliding=!0,g&&this.pause(),this.$indicators.length){this.$indicators.find(".active").removeClass("active");var l=a(this.$indicators.children()[this.getItemIndex(f)]);l&&l.addClass("active")}var m=a.Event("slid.bs.carousel",{relatedTarget:j,direction:h});return a.support.transition&&this.$element.hasClass("slide")?(f.addClass(b),f[0].offsetWidth,e.addClass(h),f.addClass(h),e.one("bsTransitionEnd",function(){f.removeClass([b,h].join(" ")).addClass("active"),e.removeClass(["active",h].join(" ")),i.sliding=!1,setTimeout(function(){i.$element.trigger(m)},0)}).emulateTransitionEnd(c.TRANSITION_DURATION)):(e.removeClass("active"),f.addClass("active"),this.sliding=!1,this.$element.trigger(m)),g&&this.cycle(),this}};var d=a.fn.carousel;a.fn.carousel=b,a.fn.carousel.Constructor=c,a.fn.carousel.noConflict=function(){return a.fn.carousel=d,this};var e=function(c){var d,e=a(this),f=a(e.attr("data-target")||(d=e.attr("href"))&&d.replace(/.*(?=#[^\s]+$)/,""));if(f.hasClass("carousel")){var g=a.extend({},f.data(),e.data()),h=e.attr("data-slide-to");h&&(g.interval=!1),b.call(f,g),h&&f.data("bs.carousel").to(h),c.preventDefault()}};a(document).on("click.bs.carousel.data-api","[data-slide]",e).on("click.bs.carousel.data-api","[data-slide-to]",e),a(window).on("load",function(){a('[data-ride="carousel"]').each(function(){var c=a(this);b.call(c,c.data())})})}(jQuery),+function(a){"use strict";function b(b){var c,d=b.attr("data-target")||(c=b.attr("href"))&&c.replace(/.*(?=#[^\s]+$)/,"");return a(d)}function c(b){return this.each(function(){var c=a(this),e=c.data("bs.collapse"),f=a.extend({},d.DEFAULTS,c.data(),"object"==typeof b&&b);!e&&f.toggle&&/show|hide/.test(b)&&(f.toggle=!1),e||c.data("bs.collapse",e=new d(this,f)),"string"==typeof b&&e[b]()})}var d=function(b,c){this.$element=a(b),this.options=a.extend({},d.DEFAULTS,c),this.$trigger=a('[data-toggle="collapse"][href="#'+b.id+'"],[data-toggle="collapse"][data-target="#'+b.id+'"]'),this.transitioning=null,this.options.parent?this.$parent=this.getParent():this.addAriaAndCollapsedClass(this.$element,this.$trigger),this.options.toggle&&this.toggle()};d.VERSION="3.3.7",d.TRANSITION_DURATION=350,d.DEFAULTS={toggle:!0},d.prototype.dimension=function(){var a=this.$element.hasClass("width");return a?"width":"height"},d.prototype.show=function(){if(!this.transitioning&&!this.$element.hasClass("in")){var b,e=this.$parent&&this.$parent.children(".panel").children(".in, .collapsing");if(!(e&&e.length&&(b=e.data("bs.collapse"),b&&b.transitioning))){var f=a.Event("show.bs.collapse");if(this.$element.trigger(f),!f.isDefaultPrevented()){e&&e.length&&(c.call(e,"hide"),b||e.data("bs.collapse",null));var g=this.dimension();this.$element.removeClass("collapse").addClass("collapsing")[g](0).attr("aria-expanded",!0),this.$trigger.removeClass("collapsed").attr("aria-expanded",!0),this.transitioning=1;var h=function(){this.$element.removeClass("collapsing").addClass("collapse in")[g](""),this.transitioning=0,this.$element.trigger("shown.bs.collapse")};if(!a.support.transition)return h.call(this);var i=a.camelCase(["scroll",g].join("-"));this.$element.one("bsTransitionEnd",a.proxy(h,this)).emulateTransitionEnd(d.TRANSITION_DURATION)[g](this.$element[0][i])}}}},d.prototype.hide=function(){if(!this.transitioning&&this.$element.hasClass("in")){var b=a.Event("hide.bs.collapse");if(this.$element.trigger(b),!b.isDefaultPrevented()){var c=this.dimension();this.$element[c](this.$element[c]())[0].offsetHeight,this.$element.addClass("collapsing").removeClass("collapse in").attr("aria-expanded",!1),this.$trigger.addClass("collapsed").attr("aria-expanded",!1),this.transitioning=1;var e=function(){this.transitioning=0,this.$element.removeClass("collapsing").addClass("collapse").trigger("hidden.bs.collapse")};return a.support.transition?void this.$element[c](0).one("bsTransitionEnd",a.proxy(e,this)).emulateTransitionEnd(d.TRANSITION_DURATION):e.call(this)}}},d.prototype.toggle=function(){this[this.$element.hasClass("in")?"hide":"show"]()},d.prototype.getParent=function(){return a(this.options.parent).find('[data-toggle="collapse"][data-parent="'+this.options.parent+'"]').each(a.proxy(function(c,d){var e=a(d);this.addAriaAndCollapsedClass(b(e),e)},this)).end()},d.prototype.addAriaAndCollapsedClass=function(a,b){var c=a.hasClass("in");a.attr("aria-expanded",c),b.toggleClass("collapsed",!c).attr("aria-expanded",c)};var e=a.fn.collapse;a.fn.collapse=c,a.fn.collapse.Constructor=d,a.fn.collapse.noConflict=function(){return a.fn.collapse=e,this},a(document).on("click.bs.collapse.data-api",'[data-toggle="collapse"]',function(d){var e=a(this);e.attr("data-target")||d.preventDefault();var f=b(e),g=f.data("bs.collapse"),h=g?"toggle":e.data();c.call(f,h)})}(jQuery),+function(a){"use strict";function b(b){var c=b.attr("data-target");c||(c=b.attr("href"),c=c&&/#[A-Za-z]/.test(c)&&c.replace(/.*(?=#[^\s]*$)/,""));var d=c&&a(c);return d&&d.length?d:b.parent()}function c(c){c&&3===c.which||(a(e).remove(),a(f).each(function(){var d=a(this),e=b(d),f={relatedTarget:this};e.hasClass("open")&&(c&&"click"==c.type&&/input|textarea/i.test(c.target.tagName)&&a.contains(e[0],c.target)||(e.trigger(c=a.Event("hide.bs.dropdown",f)),c.isDefaultPrevented()||(d.attr("aria-expanded","false"),e.removeClass("open").trigger(a.Event("hidden.bs.dropdown",f)))))}))}function d(b){return this.each(function(){var c=a(this),d=c.data("bs.dropdown");d||c.data("bs.dropdown",d=new g(this)),"string"==typeof b&&d[b].call(c)})}var e=".dropdown-backdrop",f='[data-toggle="dropdown"]',g=function(b){a(b).on("click.bs.dropdown",this.toggle)};g.VERSION="3.3.7",g.prototype.toggle=function(d){var e=a(this);if(!e.is(".disabled, :disabled")){var f=b(e),g=f.hasClass("open");if(c(),!g){"ontouchstart"in document.documentElement&&!f.closest(".navbar-nav").length&&a(document.createElement("div")).addClass("dropdown-backdrop").insertAfter(a(this)).on("click",c);var h={relatedTarget:this};if(f.trigger(d=a.Event("show.bs.dropdown",h)),d.isDefaultPrevented())return;e.trigger("focus").attr("aria-expanded","true"),f.toggleClass("open").trigger(a.Event("shown.bs.dropdown",h))}return!1}},g.prototype.keydown=function(c){if(/(38|40|27|32)/.test(c.which)&&!/input|textarea/i.test(c.target.tagName)){var d=a(this);if(c.preventDefault(),c.stopPropagation(),!d.is(".disabled, :disabled")){var e=b(d),g=e.hasClass("open");if(!g&&27!=c.which||g&&27==c.which)return 27==c.which&&e.find(f).trigger("focus"),d.trigger("click");var h=" li:not(.disabled):visible a",i=e.find(".dropdown-menu"+h);if(i.length){var j=i.index(c.target);38==c.which&&j>0&&j--,40==c.which&&j<i.length-1&&j++,~j||(j=0),i.eq(j).trigger("focus")}}}};var h=a.fn.dropdown;a.fn.dropdown=d,a.fn.dropdown.Constructor=g,a.fn.dropdown.noConflict=function(){return a.fn.dropdown=h,this},a(document).on("click.bs.dropdown.data-api",c).on("click.bs.dropdown.data-api",".dropdown form",function(a){a.stopPropagation()}).on("click.bs.dropdown.data-api",f,g.prototype.toggle).on("keydown.bs.dropdown.data-api",f,g.prototype.keydown).on("keydown.bs.dropdown.data-api",".dropdown-menu",g.prototype.keydown)}(jQuery),+function(a){"use strict";function b(b,d){return this.each(function(){var e=a(this),f=e.data("bs.modal"),g=a.extend({},c.DEFAULTS,e.data(),"object"==typeof b&&b);f||e.data("bs.modal",f=new c(this,g)),"string"==typeof b?f[b](d):g.show&&f.show(d)})}var c=function(b,c){this.options=c,this.$body=a(document.body),this.$element=a(b),this.$dialog=this.$element.find(".modal-dialog"),this.$backdrop=null,this.isShown=null,this.originalBodyPad=null,this.scrollbarWidth=0,this.ignoreBackdropClick=!1,this.options.remote&&this.$element.find(".modal-content").load(this.options.remote,a.proxy(function(){this.$element.trigger("loaded.bs.modal")},this))};c.VERSION="3.3.7",c.TRANSITION_DURATION=300,c.BACKDROP_TRANSITION_DURATION=150,c.DEFAULTS={backdrop:!0,keyboard:!0,show:!0},c.prototype.toggle=function(a){return this.isShown?this.hide():this.show(a)},c.prototype.show=function(b){var d=this,e=a.Event("show.bs.modal",{relatedTarget:b});this.$element.trigger(e),this.isShown||e.isDefaultPrevented()||(this.isShown=!0,this.checkScrollbar(),this.setScrollbar(),this.$body.addClass("modal-open"),this.escape(),this.resize(),this.$element.on("click.dismiss.bs.modal",'[data-dismiss="modal"]',a.proxy(this.hide,this)),this.$dialog.on("mousedown.dismiss.bs.modal",function(){d.$element.one("mouseup.dismiss.bs.modal",function(b){a(b.target).is(d.$element)&&(d.ignoreBackdropClick=!0)})}),this.backdrop(function(){var e=a.support.transition&&d.$element.hasClass("fade");d.$element.parent().length||d.$element.appendTo(d.$body),d.$element.show().scrollTop(0),d.adjustDialog(),e&&d.$element[0].offsetWidth,d.$element.addClass("in"),d.enforceFocus();var f=a.Event("shown.bs.modal",{relatedTarget:b});e?d.$dialog.one("bsTransitionEnd",function(){d.$element.trigger("focus").trigger(f)}).emulateTransitionEnd(c.TRANSITION_DURATION):d.$element.trigger("focus").trigger(f)}))},c.prototype.hide=function(b){b&&b.preventDefault(),b=a.Event("hide.bs.modal"),this.$element.trigger(b),this.isShown&&!b.isDefaultPrevented()&&(this.isShown=!1,this.escape(),this.resize(),a(document).off("focusin.bs.modal"),this.$element.removeClass("in").off("click.dismiss.bs.modal").off("mouseup.dismiss.bs.modal"),this.$dialog.off("mousedown.dismiss.bs.modal"),a.support.transition&&this.$element.hasClass("fade")?this.$element.one("bsTransitionEnd",a.proxy(this.hideModal,this)).emulateTransitionEnd(c.TRANSITION_DURATION):this.hideModal())},c.prototype.enforceFocus=function(){a(document).off("focusin.bs.modal").on("focusin.bs.modal",a.proxy(function(a){document===a.target||this.$element[0]===a.target||this.$element.has(a.target).length||this.$element.trigger("focus")},this))},c.prototype.escape=function(){this.isShown&&this.options.keyboard?this.$element.on("keydown.dismiss.bs.modal",a.proxy(function(a){27==a.which&&this.hide()},this)):this.isShown||this.$element.off("keydown.dismiss.bs.modal")},c.prototype.resize=function(){this.isShown?a(window).on("resize.bs.modal",a.proxy(this.handleUpdate,this)):a(window).off("resize.bs.modal")},c.prototype.hideModal=function(){var a=this;this.$element.hide(),this.backdrop(function(){a.$body.removeClass("modal-open"),a.resetAdjustments(),a.resetScrollbar(),a.$element.trigger("hidden.bs.modal")})},c.prototype.removeBackdrop=function(){this.$backdrop&&this.$backdrop.remove(),this.$backdrop=null},c.prototype.backdrop=function(b){var d=this,e=this.$element.hasClass("fade")?"fade":"";if(this.isShown&&this.options.backdrop){var f=a.support.transition&&e;if(this.$backdrop=a(document.createElement("div")).addClass("modal-backdrop "+e).appendTo(this.$body),this.$element.on("click.dismiss.bs.modal",a.proxy(function(a){return this.ignoreBackdropClick?void(this.ignoreBackdropClick=!1):void(a.target===a.currentTarget&&("static"==this.options.backdrop?this.$element[0].focus():this.hide()))},this)),f&&this.$backdrop[0].offsetWidth,this.$backdrop.addClass("in"),!b)return;f?this.$backdrop.one("bsTransitionEnd",b).emulateTransitionEnd(c.BACKDROP_TRANSITION_DURATION):b()}else if(!this.isShown&&this.$backdrop){this.$backdrop.removeClass("in");var g=function(){d.removeBackdrop(),b&&b()};a.support.transition&&this.$element.hasClass("fade")?this.$backdrop.one("bsTransitionEnd",g).emulateTransitionEnd(c.BACKDROP_TRANSITION_DURATION):g()}else b&&b()},c.prototype.handleUpdate=function(){this.adjustDialog()},c.prototype.adjustDialog=function(){var a=this.$element[0].scrollHeight>document.documentElement.clientHeight;this.$element.css({paddingLeft:!this.bodyIsOverflowing&&a?this.scrollbarWidth:"",paddingRight:this.bodyIsOverflowing&&!a?this.scrollbarWidth:""})},c.prototype.resetAdjustments=function(){this.$element.css({paddingLeft:"",paddingRight:""})},c.prototype.checkScrollbar=function(){var a=window.innerWidth;if(!a){var b=document.documentElement.getBoundingClientRect();a=b.right-Math.abs(b.left)}this.bodyIsOverflowing=document.body.clientWidth<a,this.scrollbarWidth=this.measureScrollbar()},c.prototype.setScrollbar=function(){var a=parseInt(this.$body.css("padding-right")||0,10);this.originalBodyPad=document.body.style.paddingRight||"",this.bodyIsOverflowing&&this.$body.css("padding-right",a+this.scrollbarWidth)},c.prototype.resetScrollbar=function(){this.$body.css("padding-right",this.originalBodyPad)},c.prototype.measureScrollbar=function(){var a=document.createElement("div");a.className="modal-scrollbar-measure",this.$body.append(a);var b=a.offsetWidth-a.clientWidth;return this.$body[0].removeChild(a),b};var d=a.fn.modal;a.fn.modal=b,a.fn.modal.Constructor=c,a.fn.modal.noConflict=function(){return a.fn.modal=d,this},a(document).on("click.bs.modal.data-api",'[data-toggle="modal"]',function(c){var d=a(this),e=d.attr("href"),f=a(d.attr("data-target")||e&&e.replace(/.*(?=#[^\s]+$)/,"")),g=f.data("bs.modal")?"toggle":a.extend({remote:!/#/.test(e)&&e},f.data(),d.data());d.is("a")&&c.preventDefault(),f.one("show.bs.modal",function(a){a.isDefaultPrevented()||f.one("hidden.bs.modal",function(){d.is(":visible")&&d.trigger("focus")})}),b.call(f,g,this)})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.tooltip"),f="object"==typeof b&&b;!e&&/destroy|hide/.test(b)||(e||d.data("bs.tooltip",e=new c(this,f)),"string"==typeof b&&e[b]())})}var c=function(a,b){this.type=null,this.options=null,this.enabled=null,this.timeout=null,this.hoverState=null,this.$element=null,this.inState=null,this.init("tooltip",a,b)};c.VERSION="3.3.7",c.TRANSITION_DURATION=150,c.DEFAULTS={animation:!0,placement:"top",selector:!1,template:'<div class="tooltip" role="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>',trigger:"hover focus",title:"",delay:0,html:!1,container:!1,viewport:{selector:"body",padding:0}},c.prototype.init=function(b,c,d){if(this.enabled=!0,this.type=b,this.$element=a(c),this.options=this.getOptions(d),this.$viewport=this.options.viewport&&a(a.isFunction(this.options.viewport)?this.options.viewport.call(this,this.$element):this.options.viewport.selector||this.options.viewport),this.inState={click:!1,hover:!1,focus:!1},this.$element[0]instanceof document.constructor&&!this.options.selector)throw new Error("`selector` option must be specified when initializing "+this.type+" on the window.document object!");for(var e=this.options.trigger.split(" "),f=e.length;f--;){var g=e[f];if("click"==g)this.$element.on("click."+this.type,this.options.selector,a.proxy(this.toggle,this));else if("manual"!=g){var h="hover"==g?"mouseenter":"focusin",i="hover"==g?"mouseleave":"focusout";this.$element.on(h+"."+this.type,this.options.selector,a.proxy(this.enter,this)),this.$element.on(i+"."+this.type,this.options.selector,a.proxy(this.leave,this))}}this.options.selector?this._options=a.extend({},this.options,{trigger:"manual",selector:""}):this.fixTitle()},c.prototype.getDefaults=function(){return c.DEFAULTS},c.prototype.getOptions=function(b){return b=a.extend({},this.getDefaults(),this.$element.data(),b),b.delay&&"number"==typeof b.delay&&(b.delay={show:b.delay,hide:b.delay}),b},c.prototype.getDelegateOptions=function(){var b={},c=this.getDefaults();return this._options&&a.each(this._options,function(a,d){c[a]!=d&&(b[a]=d)}),b},c.prototype.enter=function(b){var c=b instanceof this.constructor?b:a(b.currentTarget).data("bs."+this.type);return c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data("bs."+this.type,c)),b instanceof a.Event&&(c.inState["focusin"==b.type?"focus":"hover"]=!0),c.tip().hasClass("in")||"in"==c.hoverState?void(c.hoverState="in"):(clearTimeout(c.timeout),c.hoverState="in",c.options.delay&&c.options.delay.show?void(c.timeout=setTimeout(function(){"in"==c.hoverState&&c.show()},c.options.delay.show)):c.show())},c.prototype.isInStateTrue=function(){for(var a in this.inState)if(this.inState[a])return!0;return!1},c.prototype.leave=function(b){var c=b instanceof this.constructor?b:a(b.currentTarget).data("bs."+this.type);if(c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data("bs."+this.type,c)),b instanceof a.Event&&(c.inState["focusout"==b.type?"focus":"hover"]=!1),!c.isInStateTrue())return clearTimeout(c.timeout),c.hoverState="out",c.options.delay&&c.options.delay.hide?void(c.timeout=setTimeout(function(){"out"==c.hoverState&&c.hide()},c.options.delay.hide)):c.hide()},c.prototype.show=function(){var b=a.Event("show.bs."+this.type);if(this.hasContent()&&this.enabled){this.$element.trigger(b);var d=a.contains(this.$element[0].ownerDocument.documentElement,this.$element[0]);if(b.isDefaultPrevented()||!d)return;var e=this,f=this.tip(),g=this.getUID(this.type);this.setContent(),f.attr("id",g),this.$element.attr("aria-describedby",g),this.options.animation&&f.addClass("fade");var h="function"==typeof this.options.placement?this.options.placement.call(this,f[0],this.$element[0]):this.options.placement,i=/\s?auto?\s?/i,j=i.test(h);j&&(h=h.replace(i,"")||"top"),f.detach().css({top:0,left:0,display:"block"}).addClass(h).data("bs."+this.type,this),this.options.container?f.appendTo(this.options.container):f.insertAfter(this.$element),this.$element.trigger("inserted.bs."+this.type);var k=this.getPosition(),l=f[0].offsetWidth,m=f[0].offsetHeight;if(j){var n=h,o=this.getPosition(this.$viewport);h="bottom"==h&&k.bottom+m>o.bottom?"top":"top"==h&&k.top-m<o.top?"bottom":"right"==h&&k.right+l>o.width?"left":"left"==h&&k.left-l<o.left?"right":h,f.removeClass(n).addClass(h)}var p=this.getCalculatedOffset(h,k,l,m);this.applyPlacement(p,h);var q=function(){var a=e.hoverState;e.$element.trigger("shown.bs."+e.type),e.hoverState=null,"out"==a&&e.leave(e)};a.support.transition&&this.$tip.hasClass("fade")?f.one("bsTransitionEnd",q).emulateTransitionEnd(c.TRANSITION_DURATION):q()}},c.prototype.applyPlacement=function(b,c){var d=this.tip(),e=d[0].offsetWidth,f=d[0].offsetHeight,g=parseInt(d.css("margin-top"),10),h=parseInt(d.css("margin-left"),10);isNaN(g)&&(g=0),isNaN(h)&&(h=0),b.top+=g,b.left+=h,a.offset.setOffset(d[0],a.extend({using:function(a){d.css({top:Math.round(a.top),left:Math.round(a.left)})}},b),0),d.addClass("in");var i=d[0].offsetWidth,j=d[0].offsetHeight;"top"==c&&j!=f&&(b.top=b.top+f-j);var k=this.getViewportAdjustedDelta(c,b,i,j);k.left?b.left+=k.left:b.top+=k.top;var l=/top|bottom/.test(c),m=l?2*k.left-e+i:2*k.top-f+j,n=l?"offsetWidth":"offsetHeight";d.offset(b),this.replaceArrow(m,d[0][n],l)},c.prototype.replaceArrow=function(a,b,c){this.arrow().css(c?"left":"top",50*(1-a/b)+"%").css(c?"top":"left","")},c.prototype.setContent=function(){var a=this.tip(),b=this.getTitle();a.find(".tooltip-inner")[this.options.html?"html":"text"](b),a.removeClass("fade in top bottom left right")},c.prototype.hide=function(b){function d(){"in"!=e.hoverState&&f.detach(),e.$element&&e.$element.removeAttr("aria-describedby").trigger("hidden.bs."+e.type),b&&b()}var e=this,f=a(this.$tip),g=a.Event("hide.bs."+this.type);if(this.$element.trigger(g),!g.isDefaultPrevented())return f.removeClass("in"),a.support.transition&&f.hasClass("fade")?f.one("bsTransitionEnd",d).emulateTransitionEnd(c.TRANSITION_DURATION):d(),this.hoverState=null,this},c.prototype.fixTitle=function(){var a=this.$element;(a.attr("title")||"string"!=typeof a.attr("data-original-title"))&&a.attr("data-original-title",a.attr("title")||"").attr("title","")},c.prototype.hasContent=function(){return this.getTitle()},c.prototype.getPosition=function(b){b=b||this.$element;var c=b[0],d="BODY"==c.tagName,e=c.getBoundingClientRect();null==e.width&&(e=a.extend({},e,{width:e.right-e.left,height:e.bottom-e.top}));var f=window.SVGElement&&c instanceof window.SVGElement,g=d?{top:0,left:0}:f?null:b.offset(),h={scroll:d?document.documentElement.scrollTop||document.body.scrollTop:b.scrollTop()},i=d?{width:a(window).width(),height:a(window).height()}:null;return a.extend({},e,h,i,g)},c.prototype.getCalculatedOffset=function(a,b,c,d){return"bottom"==a?{top:b.top+b.height,left:b.left+b.width/2-c/2}:"top"==a?{top:b.top-d,left:b.left+b.width/2-c/2}:"left"==a?{top:b.top+b.height/2-d/2,left:b.left-c}:{top:b.top+b.height/2-d/2,left:b.left+b.width}},c.prototype.getViewportAdjustedDelta=function(a,b,c,d){var e={top:0,left:0};if(!this.$viewport)return e;var f=this.options.viewport&&this.options.viewport.padding||0,g=this.getPosition(this.$viewport);if(/right|left/.test(a)){var h=b.top-f-g.scroll,i=b.top+f-g.scroll+d;h<g.top?e.top=g.top-h:i>g.top+g.height&&(e.top=g.top+g.height-i)}else{var j=b.left-f,k=b.left+f+c;j<g.left?e.left=g.left-j:k>g.right&&(e.left=g.left+g.width-k)}return e},c.prototype.getTitle=function(){var a,b=this.$element,c=this.options;return a=b.attr("data-original-title")||("function"==typeof c.title?c.title.call(b[0]):c.title)},c.prototype.getUID=function(a){do a+=~~(1e6*Math.random());while(document.getElementById(a));return a},c.prototype.tip=function(){if(!this.$tip&&(this.$tip=a(this.options.template),1!=this.$tip.length))throw new Error(this.type+" `template` option must consist of exactly 1 top-level element!");return this.$tip},c.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".tooltip-arrow")},c.prototype.enable=function(){this.enabled=!0},c.prototype.disable=function(){this.enabled=!1},c.prototype.toggleEnabled=function(){this.enabled=!this.enabled},c.prototype.toggle=function(b){var c=this;b&&(c=a(b.currentTarget).data("bs."+this.type),c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data("bs."+this.type,c))),b?(c.inState.click=!c.inState.click,c.isInStateTrue()?c.enter(c):c.leave(c)):c.tip().hasClass("in")?c.leave(c):c.enter(c)},c.prototype.destroy=function(){var a=this;clearTimeout(this.timeout),this.hide(function(){a.$element.off("."+a.type).removeData("bs."+a.type),a.$tip&&a.$tip.detach(),a.$tip=null,a.$arrow=null,a.$viewport=null,a.$element=null})};var d=a.fn.tooltip;a.fn.tooltip=b,a.fn.tooltip.Constructor=c,a.fn.tooltip.noConflict=function(){return a.fn.tooltip=d,this}}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.popover"),f="object"==typeof b&&b;!e&&/destroy|hide/.test(b)||(e||d.data("bs.popover",e=new c(this,f)),"string"==typeof b&&e[b]())})}var c=function(a,b){this.init("popover",a,b)};if(!a.fn.tooltip)throw new Error("Popover requires tooltip.js");c.VERSION="3.3.7",c.DEFAULTS=a.extend({},a.fn.tooltip.Constructor.DEFAULTS,{placement:"right",trigger:"click",content:"",template:'<div class="popover" role="tooltip"><div class="arrow"></div><h3 class="popover-title"></h3><div class="popover-content"></div></div>'}),c.prototype=a.extend({},a.fn.tooltip.Constructor.prototype),c.prototype.constructor=c,c.prototype.getDefaults=function(){return c.DEFAULTS},c.prototype.setContent=function(){var a=this.tip(),b=this.getTitle(),c=this.getContent();a.find(".popover-title")[this.options.html?"html":"text"](b),a.find(".popover-content").children().detach().end()[this.options.html?"string"==typeof c?"html":"append":"text"](c),a.removeClass("fade top bottom left right in"),a.find(".popover-title").html()||a.find(".popover-title").hide()},c.prototype.hasContent=function(){return this.getTitle()||this.getContent()},c.prototype.getContent=function(){var a=this.$element,b=this.options;return a.attr("data-content")||("function"==typeof b.content?b.content.call(a[0]):b.content)},c.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".arrow")};var d=a.fn.popover;a.fn.popover=b,a.fn.popover.Constructor=c,a.fn.popover.noConflict=function(){return a.fn.popover=d,this}}(jQuery),+function(a){"use strict";function b(c,d){this.$body=a(document.body),this.$scrollElement=a(a(c).is(document.body)?window:c),this.options=a.extend({},b.DEFAULTS,d),this.selector=(this.options.target||"")+" .nav li > a",this.offsets=[],this.targets=[],this.activeTarget=null,this.scrollHeight=0,this.$scrollElement.on("scroll.bs.scrollspy",a.proxy(this.process,this)),this.refresh(),this.process()}function c(c){return this.each(function(){var d=a(this),e=d.data("bs.scrollspy"),f="object"==typeof c&&c;e||d.data("bs.scrollspy",e=new b(this,f)),"string"==typeof c&&e[c]()})}b.VERSION="3.3.7",b.DEFAULTS={offset:10},b.prototype.getScrollHeight=function(){return this.$scrollElement[0].scrollHeight||Math.max(this.$body[0].scrollHeight,document.documentElement.scrollHeight)},b.prototype.refresh=function(){var b=this,c="offset",d=0;this.offsets=[],this.targets=[],this.scrollHeight=this.getScrollHeight(),a.isWindow(this.$scrollElement[0])||(c="position",d=this.$scrollElement.scrollTop()),this.$body.find(this.selector).map(function(){var b=a(this),e=b.data("target")||b.attr("href"),f=/^#./.test(e)&&a(e);return f&&f.length&&f.is(":visible")&&[[f[c]().top+d,e]]||null}).sort(function(a,b){return a[0]-b[0]}).each(function(){b.offsets.push(this[0]),b.targets.push(this[1])})},b.prototype.process=function(){var a,b=this.$scrollElement.scrollTop()+this.options.offset,c=this.getScrollHeight(),d=this.options.offset+c-this.$scrollElement.height(),e=this.offsets,f=this.targets,g=this.activeTarget;if(this.scrollHeight!=c&&this.refresh(),b>=d)return g!=(a=f[f.length-1])&&this.activate(a);if(g&&b<e[0])return this.activeTarget=null,this.clear();for(a=e.length;a--;)g!=f[a]&&b>=e[a]&&(void 0===e[a+1]||b<e[a+1])&&this.activate(f[a])},b.prototype.activate=function(b){
+this.activeTarget=b,this.clear();var c=this.selector+'[data-target="'+b+'"],'+this.selector+'[href="'+b+'"]',d=a(c).parents("li").addClass("active");d.parent(".dropdown-menu").length&&(d=d.closest("li.dropdown").addClass("active")),d.trigger("activate.bs.scrollspy")},b.prototype.clear=function(){a(this.selector).parentsUntil(this.options.target,".active").removeClass("active")};var d=a.fn.scrollspy;a.fn.scrollspy=c,a.fn.scrollspy.Constructor=b,a.fn.scrollspy.noConflict=function(){return a.fn.scrollspy=d,this},a(window).on("load.bs.scrollspy.data-api",function(){a('[data-spy="scroll"]').each(function(){var b=a(this);c.call(b,b.data())})})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.tab");e||d.data("bs.tab",e=new c(this)),"string"==typeof b&&e[b]()})}var c=function(b){this.element=a(b)};c.VERSION="3.3.7",c.TRANSITION_DURATION=150,c.prototype.show=function(){var b=this.element,c=b.closest("ul:not(.dropdown-menu)"),d=b.data("target");if(d||(d=b.attr("href"),d=d&&d.replace(/.*(?=#[^\s]*$)/,"")),!b.parent("li").hasClass("active")){var e=c.find(".active:last a"),f=a.Event("hide.bs.tab",{relatedTarget:b[0]}),g=a.Event("show.bs.tab",{relatedTarget:e[0]});if(e.trigger(f),b.trigger(g),!g.isDefaultPrevented()&&!f.isDefaultPrevented()){var h=a(d);this.activate(b.closest("li"),c),this.activate(h,h.parent(),function(){e.trigger({type:"hidden.bs.tab",relatedTarget:b[0]}),b.trigger({type:"shown.bs.tab",relatedTarget:e[0]})})}}},c.prototype.activate=function(b,d,e){function f(){g.removeClass("active").find("> .dropdown-menu > .active").removeClass("active").end().find('[data-toggle="tab"]').attr("aria-expanded",!1),b.addClass("active").find('[data-toggle="tab"]').attr("aria-expanded",!0),h?(b[0].offsetWidth,b.addClass("in")):b.removeClass("fade"),b.parent(".dropdown-menu").length&&b.closest("li.dropdown").addClass("active").end().find('[data-toggle="tab"]').attr("aria-expanded",!0),e&&e()}var g=d.find("> .active"),h=e&&a.support.transition&&(g.length&&g.hasClass("fade")||!!d.find("> .fade").length);g.length&&h?g.one("bsTransitionEnd",f).emulateTransitionEnd(c.TRANSITION_DURATION):f(),g.removeClass("in")};var d=a.fn.tab;a.fn.tab=b,a.fn.tab.Constructor=c,a.fn.tab.noConflict=function(){return a.fn.tab=d,this};var e=function(c){c.preventDefault(),b.call(a(this),"show")};a(document).on("click.bs.tab.data-api",'[data-toggle="tab"]',e).on("click.bs.tab.data-api",'[data-toggle="pill"]',e)}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.affix"),f="object"==typeof b&&b;e||d.data("bs.affix",e=new c(this,f)),"string"==typeof b&&e[b]()})}var c=function(b,d){this.options=a.extend({},c.DEFAULTS,d),this.$target=a(this.options.target).on("scroll.bs.affix.data-api",a.proxy(this.checkPosition,this)).on("click.bs.affix.data-api",a.proxy(this.checkPositionWithEventLoop,this)),this.$element=a(b),this.affixed=null,this.unpin=null,this.pinnedOffset=null,this.checkPosition()};c.VERSION="3.3.7",c.RESET="affix affix-top affix-bottom",c.DEFAULTS={offset:0,target:window},c.prototype.getState=function(a,b,c,d){var e=this.$target.scrollTop(),f=this.$element.offset(),g=this.$target.height();if(null!=c&&"top"==this.affixed)return e<c&&"top";if("bottom"==this.affixed)return null!=c?!(e+this.unpin<=f.top)&&"bottom":!(e+g<=a-d)&&"bottom";var h=null==this.affixed,i=h?e:f.top,j=h?g:b;return null!=c&&e<=c?"top":null!=d&&i+j>=a-d&&"bottom"},c.prototype.getPinnedOffset=function(){if(this.pinnedOffset)return this.pinnedOffset;this.$element.removeClass(c.RESET).addClass("affix");var a=this.$target.scrollTop(),b=this.$element.offset();return this.pinnedOffset=b.top-a},c.prototype.checkPositionWithEventLoop=function(){setTimeout(a.proxy(this.checkPosition,this),1)},c.prototype.checkPosition=function(){if(this.$element.is(":visible")){var b=this.$element.height(),d=this.options.offset,e=d.top,f=d.bottom,g=Math.max(a(document).height(),a(document.body).height());"object"!=typeof d&&(f=e=d),"function"==typeof e&&(e=d.top(this.$element)),"function"==typeof f&&(f=d.bottom(this.$element));var h=this.getState(g,b,e,f);if(this.affixed!=h){null!=this.unpin&&this.$element.css("top","");var i="affix"+(h?"-"+h:""),j=a.Event(i+".bs.affix");if(this.$element.trigger(j),j.isDefaultPrevented())return;this.affixed=h,this.unpin="bottom"==h?this.getPinnedOffset():null,this.$element.removeClass(c.RESET).addClass(i).trigger(i.replace("affix","affixed")+".bs.affix")}"bottom"==h&&this.$element.offset({top:g-b-f})}};var d=a.fn.affix;a.fn.affix=b,a.fn.affix.Constructor=c,a.fn.affix.noConflict=function(){return a.fn.affix=d,this},a(window).on("load",function(){a('[data-spy="affix"]').each(function(){var c=a(this),d=c.data();d.offset=d.offset||{},null!=d.offsetBottom&&(d.offset.bottom=d.offsetBottom),null!=d.offsetTop&&(d.offset.top=d.offsetTop),b.call(c,d)})})}(jQuery);
\ No newline at end of file
diff --git a/themes/bootstrap3/js/vendor/cssrefresh.js b/themes/bootstrap3/js/vendor/cssrefresh.js
deleted file mode 100644
index 5d85acb03f555ebfe1bdf020874b45d765aaf27f..0000000000000000000000000000000000000000
--- a/themes/bootstrap3/js/vendor/cssrefresh.js
+++ /dev/null
@@ -1,16 +0,0 @@
-/*
- *	CSSrefresh v1.0.1
- *
- *	Copyright (c) 2012 Fred Heusschen
- *	www.frebsite.nl
- *
- *	Dual licensed under the MIT and GPL licenses.
- *	http://en.wikipedia.org/wiki/MIT_License
- *	http://en.wikipedia.org/wiki/GNU_General_Public_License
- */
-
-/*
- * MINIFIED
- */
-
-(function(){var e={array_filter:function(e,t){var n={};for(var r in e){if(t(e[r])){n[r]=e[r]}}return n},filemtime:function(e){var t=this.get_headers(e,1);return t&&t["Last-Modified"]&&Date.parse(t["Last-Modified"])/1e3||false},get_headers:function(e,t){var n=window.ActiveXObject?new ActiveXObject("Microsoft.XMLHTTP"):new XMLHttpRequest;if(!n){throw new Error("XMLHttpRequest not supported.")}var r,i,s,o,u=0;try{n.open("HEAD",e,false);n.send(null);if(n.readyState<3){return false}r=n.getAllResponseHeaders();r=r.split("\n");r=this.array_filter(r,function(e){return e.toString().substring(1)!==""});i=t?{}:[];for(o in r){if(t){s=r[o].toString().split(":");i[s.splice(0,1)]=s.join(":").substring(1)}else{i[u++]=r[o]}}return i}catch(a){return false}}};var t=function(){this.reloadFile=function(t){for(var n=0,r=t.length;n<r;n++){var i=t[n],s=e.filemtime(this.getRandom(i.href));if(i.last){if(i.last!=s){i.elem.setAttribute("href",this.getRandom(i.href))}}i.last=s}setTimeout(function(){this.reloadFile(t)},1e3)};this.getHref=function(e){return e.getAttribute("href").split("?")[0]};this.getRandom=function(e){return e+"?x="+Math.random()};var t=document.getElementsByTagName("link"),n=[];for(var r=0,i=t.length;r<i;r++){var s=t[r],o=s.rel;if(typeof o!="string"||o.length==0||o=="stylesheet"){n.push({elem:s,href:this.getHref(s),last:false})}}this.reloadFile(n)};t()})()
\ No newline at end of file
diff --git a/themes/bootstrap3/js/vendor/d3.js b/themes/bootstrap3/js/vendor/d3.js
index 8cfc9ef3f4917b76800f6783d33197cd8d618f33..71b0b37d57e9b549390a34a302b614c89ceab3c3 100644
--- a/themes/bootstrap3/js/vendor/d3.js
+++ b/themes/bootstrap3/js/vendor/d3.js
@@ -1,5 +1,8 @@
-!function(){function n(n){return null!=n&&!isNaN(n)}function t(n){return n.length}function e(n){for(var t=1;n*t%1;)t*=10;return t}function r(n,t){try{for(var e in t)Object.defineProperty(n.prototype,e,{value:t[e],enumerable:!1})}catch(r){n.prototype=t}}function u(){}function i(n){return aa+n in this}function o(n){return n=aa+n,n in this&&delete this[n]}function a(){var n=[];return this.forEach(function(t){n.push(t)}),n}function c(){var n=0;for(var t in this)t.charCodeAt(0)===ca&&++n;return n}function s(){for(var n in this)if(n.charCodeAt(0)===ca)return!1;return!0}function l(){}function f(n,t,e){return function(){var r=e.apply(t,arguments);return r===t?n:r}}function h(n,t){if(t in n)return t;t=t.charAt(0).toUpperCase()+t.substring(1);for(var e=0,r=sa.length;r>e;++e){var u=sa[e]+t;if(u in n)return u}}function g(){}function p(){}function v(n){function t(){for(var t,r=e,u=-1,i=r.length;++u<i;)(t=r[u].on)&&t.apply(this,arguments);return n}var e=[],r=new u;return t.on=function(t,u){var i,o=r.get(t);return arguments.length<2?o&&o.on:(o&&(o.on=null,e=e.slice(0,i=e.indexOf(o)).concat(e.slice(i+1)),r.remove(t)),u&&e.push(r.set(t,{on:u})),n)},t}function d(){Xo.event.preventDefault()}function m(){for(var n,t=Xo.event;n=t.sourceEvent;)t=n;return t}function y(n){for(var t=new p,e=0,r=arguments.length;++e<r;)t[arguments[e]]=v(t);return t.of=function(e,r){return function(u){try{var i=u.sourceEvent=Xo.event;u.target=n,Xo.event=u,t[u.type].apply(e,r)}finally{Xo.event=i}}},t}function x(n){return fa(n,da),n}function M(n){return"function"==typeof n?n:function(){return ha(n,this)}}function _(n){return"function"==typeof n?n:function(){return ga(n,this)}}function b(n,t){function e(){this.removeAttribute(n)}function r(){this.removeAttributeNS(n.space,n.local)}function u(){this.setAttribute(n,t)}function i(){this.setAttributeNS(n.space,n.local,t)}function o(){var e=t.apply(this,arguments);null==e?this.removeAttribute(n):this.setAttribute(n,e)}function a(){var e=t.apply(this,arguments);null==e?this.removeAttributeNS(n.space,n.local):this.setAttributeNS(n.space,n.local,e)}return n=Xo.ns.qualify(n),null==t?n.local?r:e:"function"==typeof t?n.local?a:o:n.local?i:u}function w(n){return n.trim().replace(/\s+/g," ")}function S(n){return new RegExp("(?:^|\\s+)"+Xo.requote(n)+"(?:\\s+|$)","g")}function k(n){return n.trim().split(/^|\s+/)}function E(n,t){function e(){for(var e=-1;++e<u;)n[e](this,t)}function r(){for(var e=-1,r=t.apply(this,arguments);++e<u;)n[e](this,r)}n=k(n).map(A);var u=n.length;return"function"==typeof t?r:e}function A(n){var t=S(n);return function(e,r){if(u=e.classList)return r?u.add(n):u.remove(n);var u=e.getAttribute("class")||"";r?(t.lastIndex=0,t.test(u)||e.setAttribute("class",w(u+" "+n))):e.setAttribute("class",w(u.replace(t," ")))}}function C(n,t,e){function r(){this.style.removeProperty(n)}function u(){this.style.setProperty(n,t,e)}function i(){var r=t.apply(this,arguments);null==r?this.style.removeProperty(n):this.style.setProperty(n,r,e)}return null==t?r:"function"==typeof t?i:u}function N(n,t){function e(){delete this[n]}function r(){this[n]=t}function u(){var e=t.apply(this,arguments);null==e?delete this[n]:this[n]=e}return null==t?e:"function"==typeof t?u:r}function L(n){return"function"==typeof n?n:(n=Xo.ns.qualify(n)).local?function(){return this.ownerDocument.createElementNS(n.space,n.local)}:function(){return this.ownerDocument.createElementNS(this.namespaceURI,n)}}function z(n){return{__data__:n}}function q(n){return function(){return va(this,n)}}function T(n){return arguments.length||(n=Xo.ascending),function(t,e){return t&&e?n(t.__data__,e.__data__):!t-!e}}function R(n,t){for(var e=0,r=n.length;r>e;e++)for(var u,i=n[e],o=0,a=i.length;a>o;o++)(u=i[o])&&t(u,o,e);return n}function D(n){return fa(n,ya),n}function P(n){var t,e;return function(r,u,i){var o,a=n[i].update,c=a.length;for(i!=e&&(e=i,t=0),u>=t&&(t=u+1);!(o=a[t])&&++t<c;);return o}}function U(){var n=this.__transition__;n&&++n.active}function j(n,t,e){function r(){var t=this[o];t&&(this.removeEventListener(n,t,t.$),delete this[o])}function u(){var u=c(t,Bo(arguments));r.call(this),this.addEventListener(n,this[o]=u,u.$=e),u._=t}function i(){var t,e=new RegExp("^__on([^.]+)"+Xo.requote(n)+"$");for(var r in this)if(t=r.match(e)){var u=this[r];this.removeEventListener(t[1],u,u.$),delete this[r]}}var o="__on"+n,a=n.indexOf("."),c=H;a>0&&(n=n.substring(0,a));var s=Ma.get(n);return s&&(n=s,c=F),a?t?u:r:t?g:i}function H(n,t){return function(e){var r=Xo.event;Xo.event=e,t[0]=this.__data__;try{n.apply(this,t)}finally{Xo.event=r}}}function F(n,t){var e=H(n,t);return function(n){var t=this,r=n.relatedTarget;r&&(r===t||8&r.compareDocumentPosition(t))||e.call(t,n)}}function O(){var n=".dragsuppress-"+ ++ba,t="click"+n,e=Xo.select(Go).on("touchmove"+n,d).on("dragstart"+n,d).on("selectstart"+n,d);if(_a){var r=Jo.style,u=r[_a];r[_a]="none"}return function(i){function o(){e.on(t,null)}e.on(n,null),_a&&(r[_a]=u),i&&(e.on(t,function(){d(),o()},!0),setTimeout(o,0))}}function Y(n,t){t.changedTouches&&(t=t.changedTouches[0]);var e=n.ownerSVGElement||n;if(e.createSVGPoint){var r=e.createSVGPoint();if(0>wa&&(Go.scrollX||Go.scrollY)){e=Xo.select("body").append("svg").style({position:"absolute",top:0,left:0,margin:0,padding:0,border:"none"},"important");var u=e[0][0].getScreenCTM();wa=!(u.f||u.e),e.remove()}return wa?(r.x=t.pageX,r.y=t.pageY):(r.x=t.clientX,r.y=t.clientY),r=r.matrixTransform(n.getScreenCTM().inverse()),[r.x,r.y]}var i=n.getBoundingClientRect();return[t.clientX-i.left-n.clientLeft,t.clientY-i.top-n.clientTop]}function I(n){return n>0?1:0>n?-1:0}function Z(n,t,e){return(t[0]-n[0])*(e[1]-n[1])-(t[1]-n[1])*(e[0]-n[0])}function V(n){return n>1?0:-1>n?Sa:Math.acos(n)}function X(n){return n>1?Ea:-1>n?-Ea:Math.asin(n)}function $(n){return((n=Math.exp(n))-1/n)/2}function B(n){return((n=Math.exp(n))+1/n)/2}function W(n){return((n=Math.exp(2*n))-1)/(n+1)}function J(n){return(n=Math.sin(n/2))*n}function G(){}function K(n,t,e){return new Q(n,t,e)}function Q(n,t,e){this.h=n,this.s=t,this.l=e}function nt(n,t,e){function r(n){return n>360?n-=360:0>n&&(n+=360),60>n?i+(o-i)*n/60:180>n?o:240>n?i+(o-i)*(240-n)/60:i}function u(n){return Math.round(255*r(n))}var i,o;return n=isNaN(n)?0:(n%=360)<0?n+360:n,t=isNaN(t)?0:0>t?0:t>1?1:t,e=0>e?0:e>1?1:e,o=.5>=e?e*(1+t):e+t-e*t,i=2*e-o,gt(u(n+120),u(n),u(n-120))}function tt(n,t,e){return new et(n,t,e)}function et(n,t,e){this.h=n,this.c=t,this.l=e}function rt(n,t,e){return isNaN(n)&&(n=0),isNaN(t)&&(t=0),ut(e,Math.cos(n*=Na)*t,Math.sin(n)*t)}function ut(n,t,e){return new it(n,t,e)}function it(n,t,e){this.l=n,this.a=t,this.b=e}function ot(n,t,e){var r=(n+16)/116,u=r+t/500,i=r-e/200;return u=ct(u)*Fa,r=ct(r)*Oa,i=ct(i)*Ya,gt(lt(3.2404542*u-1.5371385*r-.4985314*i),lt(-.969266*u+1.8760108*r+.041556*i),lt(.0556434*u-.2040259*r+1.0572252*i))}function at(n,t,e){return n>0?tt(Math.atan2(e,t)*La,Math.sqrt(t*t+e*e),n):tt(0/0,0/0,n)}function ct(n){return n>.206893034?n*n*n:(n-4/29)/7.787037}function st(n){return n>.008856?Math.pow(n,1/3):7.787037*n+4/29}function lt(n){return Math.round(255*(.00304>=n?12.92*n:1.055*Math.pow(n,1/2.4)-.055))}function ft(n){return gt(n>>16,255&n>>8,255&n)}function ht(n){return ft(n)+""}function gt(n,t,e){return new pt(n,t,e)}function pt(n,t,e){this.r=n,this.g=t,this.b=e}function vt(n){return 16>n?"0"+Math.max(0,n).toString(16):Math.min(255,n).toString(16)}function dt(n,t,e){var r,u,i,o=0,a=0,c=0;if(r=/([a-z]+)\((.*)\)/i.exec(n))switch(u=r[2].split(","),r[1]){case"hsl":return e(parseFloat(u[0]),parseFloat(u[1])/100,parseFloat(u[2])/100);case"rgb":return t(Mt(u[0]),Mt(u[1]),Mt(u[2]))}return(i=Va.get(n))?t(i.r,i.g,i.b):(null!=n&&"#"===n.charAt(0)&&(4===n.length?(o=n.charAt(1),o+=o,a=n.charAt(2),a+=a,c=n.charAt(3),c+=c):7===n.length&&(o=n.substring(1,3),a=n.substring(3,5),c=n.substring(5,7)),o=parseInt(o,16),a=parseInt(a,16),c=parseInt(c,16)),t(o,a,c))}function mt(n,t,e){var r,u,i=Math.min(n/=255,t/=255,e/=255),o=Math.max(n,t,e),a=o-i,c=(o+i)/2;return a?(u=.5>c?a/(o+i):a/(2-o-i),r=n==o?(t-e)/a+(e>t?6:0):t==o?(e-n)/a+2:(n-t)/a+4,r*=60):(r=0/0,u=c>0&&1>c?0:r),K(r,u,c)}function yt(n,t,e){n=xt(n),t=xt(t),e=xt(e);var r=st((.4124564*n+.3575761*t+.1804375*e)/Fa),u=st((.2126729*n+.7151522*t+.072175*e)/Oa),i=st((.0193339*n+.119192*t+.9503041*e)/Ya);return ut(116*u-16,500*(r-u),200*(u-i))}function xt(n){return(n/=255)<=.04045?n/12.92:Math.pow((n+.055)/1.055,2.4)}function Mt(n){var t=parseFloat(n);return"%"===n.charAt(n.length-1)?Math.round(2.55*t):t}function _t(n){return"function"==typeof n?n:function(){return n}}function bt(n){return n}function wt(n){return function(t,e,r){return 2===arguments.length&&"function"==typeof e&&(r=e,e=null),St(t,e,n,r)}}function St(n,t,e,r){function u(){var n,t=c.status;if(!t&&c.responseText||t>=200&&300>t||304===t){try{n=e.call(i,c)}catch(r){return o.error.call(i,r),void 0}o.load.call(i,n)}else o.error.call(i,c)}var i={},o=Xo.dispatch("beforesend","progress","load","error"),a={},c=new XMLHttpRequest,s=null;return!Go.XDomainRequest||"withCredentials"in c||!/^(http(s)?:)?\/\//.test(n)||(c=new XDomainRequest),"onload"in c?c.onload=c.onerror=u:c.onreadystatechange=function(){c.readyState>3&&u()},c.onprogress=function(n){var t=Xo.event;Xo.event=n;try{o.progress.call(i,c)}finally{Xo.event=t}},i.header=function(n,t){return n=(n+"").toLowerCase(),arguments.length<2?a[n]:(null==t?delete a[n]:a[n]=t+"",i)},i.mimeType=function(n){return arguments.length?(t=null==n?null:n+"",i):t},i.responseType=function(n){return arguments.length?(s=n,i):s},i.response=function(n){return e=n,i},["get","post"].forEach(function(n){i[n]=function(){return i.send.apply(i,[n].concat(Bo(arguments)))}}),i.send=function(e,r,u){if(2===arguments.length&&"function"==typeof r&&(u=r,r=null),c.open(e,n,!0),null==t||"accept"in a||(a.accept=t+",*/*"),c.setRequestHeader)for(var l in a)c.setRequestHeader(l,a[l]);return null!=t&&c.overrideMimeType&&c.overrideMimeType(t),null!=s&&(c.responseType=s),null!=u&&i.on("error",u).on("load",function(n){u(null,n)}),o.beforesend.call(i,c),c.send(null==r?null:r),i},i.abort=function(){return c.abort(),i},Xo.rebind(i,o,"on"),null==r?i:i.get(kt(r))}function kt(n){return 1===n.length?function(t,e){n(null==t?e:null)}:n}function Et(){var n=At(),t=Ct()-n;t>24?(isFinite(t)&&(clearTimeout(Wa),Wa=setTimeout(Et,t)),Ba=0):(Ba=1,Ga(Et))}function At(){var n=Date.now();for(Ja=Xa;Ja;)n>=Ja.t&&(Ja.f=Ja.c(n-Ja.t)),Ja=Ja.n;return n}function Ct(){for(var n,t=Xa,e=1/0;t;)t.f?t=n?n.n=t.n:Xa=t.n:(t.t<e&&(e=t.t),t=(n=t).n);return $a=n,e}function Nt(n,t){return t-(n?Math.ceil(Math.log(n)/Math.LN10):1)}function Lt(n,t){var e=Math.pow(10,3*oa(8-t));return{scale:t>8?function(n){return n/e}:function(n){return n*e},symbol:n}}function zt(n){var t=n.decimal,e=n.thousands,r=n.grouping,u=n.currency,i=r?function(n){for(var t=n.length,u=[],i=0,o=r[0];t>0&&o>0;)u.push(n.substring(t-=o,t+o)),o=r[i=(i+1)%r.length];return u.reverse().join(e)}:bt;return function(n){var e=Qa.exec(n),r=e[1]||" ",o=e[2]||">",a=e[3]||"",c=e[4]||"",s=e[5],l=+e[6],f=e[7],h=e[8],g=e[9],p=1,v="",d="",m=!1;switch(h&&(h=+h.substring(1)),(s||"0"===r&&"="===o)&&(s=r="0",o="=",f&&(l-=Math.floor((l-1)/4))),g){case"n":f=!0,g="g";break;case"%":p=100,d="%",g="f";break;case"p":p=100,d="%",g="r";break;case"b":case"o":case"x":case"X":"#"===c&&(v="0"+g.toLowerCase());case"c":case"d":m=!0,h=0;break;case"s":p=-1,g="r"}"$"===c&&(v=u[0],d=u[1]),"r"!=g||h||(g="g"),null!=h&&("g"==g?h=Math.max(1,Math.min(21,h)):("e"==g||"f"==g)&&(h=Math.max(0,Math.min(20,h)))),g=nc.get(g)||qt;var y=s&&f;return function(n){if(m&&n%1)return"";var e=0>n||0===n&&0>1/n?(n=-n,"-"):a;if(0>p){var u=Xo.formatPrefix(n,h);n=u.scale(n),d=u.symbol}else n*=p;n=g(n,h);var c=n.lastIndexOf("."),x=0>c?n:n.substring(0,c),M=0>c?"":t+n.substring(c+1);!s&&f&&(x=i(x));var _=v.length+x.length+M.length+(y?0:e.length),b=l>_?new Array(_=l-_+1).join(r):"";return y&&(x=i(b+x)),e+=v,n=x+M,("<"===o?e+n+b:">"===o?b+e+n:"^"===o?b.substring(0,_>>=1)+e+n+b.substring(_):e+(y?n:b+n))+d}}}function qt(n){return n+""}function Tt(){this._=new Date(arguments.length>1?Date.UTC.apply(this,arguments):arguments[0])}function Rt(n,t,e){function r(t){var e=n(t),r=i(e,1);return r-t>t-e?e:r}function u(e){return t(e=n(new ec(e-1)),1),e}function i(n,e){return t(n=new ec(+n),e),n}function o(n,r,i){var o=u(n),a=[];if(i>1)for(;r>o;)e(o)%i||a.push(new Date(+o)),t(o,1);else for(;r>o;)a.push(new Date(+o)),t(o,1);return a}function a(n,t,e){try{ec=Tt;var r=new Tt;return r._=n,o(r,t,e)}finally{ec=Date}}n.floor=n,n.round=r,n.ceil=u,n.offset=i,n.range=o;var c=n.utc=Dt(n);return c.floor=c,c.round=Dt(r),c.ceil=Dt(u),c.offset=Dt(i),c.range=a,n}function Dt(n){return function(t,e){try{ec=Tt;var r=new Tt;return r._=t,n(r,e)._}finally{ec=Date}}}function Pt(n){function t(n){function t(t){for(var e,u,i,o=[],a=-1,c=0;++a<r;)37===n.charCodeAt(a)&&(o.push(n.substring(c,a)),null!=(u=uc[e=n.charAt(++a)])&&(e=n.charAt(++a)),(i=C[e])&&(e=i(t,null==u?"e"===e?" ":"0":u)),o.push(e),c=a+1);return o.push(n.substring(c,a)),o.join("")}var r=n.length;return t.parse=function(t){var r={y:1900,m:0,d:1,H:0,M:0,S:0,L:0,Z:null},u=e(r,n,t,0);if(u!=t.length)return null;"p"in r&&(r.H=r.H%12+12*r.p);var i=null!=r.Z&&ec!==Tt,o=new(i?Tt:ec);return"j"in r?o.setFullYear(r.y,0,r.j):"w"in r&&("W"in r||"U"in r)?(o.setFullYear(r.y,0,1),o.setFullYear(r.y,0,"W"in r?(r.w+6)%7+7*r.W-(o.getDay()+5)%7:r.w+7*r.U-(o.getDay()+6)%7)):o.setFullYear(r.y,r.m,r.d),o.setHours(r.H+Math.floor(r.Z/100),r.M+r.Z%100,r.S,r.L),i?o._:o},t.toString=function(){return n},t}function e(n,t,e,r){for(var u,i,o,a=0,c=t.length,s=e.length;c>a;){if(r>=s)return-1;if(u=t.charCodeAt(a++),37===u){if(o=t.charAt(a++),i=N[o in uc?t.charAt(a++):o],!i||(r=i(n,e,r))<0)return-1}else if(u!=e.charCodeAt(r++))return-1}return r}function r(n,t,e){b.lastIndex=0;var r=b.exec(t.substring(e));return r?(n.w=w.get(r[0].toLowerCase()),e+r[0].length):-1}function u(n,t,e){M.lastIndex=0;var r=M.exec(t.substring(e));return r?(n.w=_.get(r[0].toLowerCase()),e+r[0].length):-1}function i(n,t,e){E.lastIndex=0;var r=E.exec(t.substring(e));return r?(n.m=A.get(r[0].toLowerCase()),e+r[0].length):-1}function o(n,t,e){S.lastIndex=0;var r=S.exec(t.substring(e));return r?(n.m=k.get(r[0].toLowerCase()),e+r[0].length):-1}function a(n,t,r){return e(n,C.c.toString(),t,r)}function c(n,t,r){return e(n,C.x.toString(),t,r)}function s(n,t,r){return e(n,C.X.toString(),t,r)}function l(n,t,e){var r=x.get(t.substring(e,e+=2).toLowerCase());return null==r?-1:(n.p=r,e)}var f=n.dateTime,h=n.date,g=n.time,p=n.periods,v=n.days,d=n.shortDays,m=n.months,y=n.shortMonths;t.utc=function(n){function e(n){try{ec=Tt;var t=new ec;return t._=n,r(t)}finally{ec=Date}}var r=t(n);return e.parse=function(n){try{ec=Tt;var t=r.parse(n);return t&&t._}finally{ec=Date}},e.toString=r.toString,e},t.multi=t.utc.multi=ee;var x=Xo.map(),M=jt(v),_=Ht(v),b=jt(d),w=Ht(d),S=jt(m),k=Ht(m),E=jt(y),A=Ht(y);p.forEach(function(n,t){x.set(n.toLowerCase(),t)});var C={a:function(n){return d[n.getDay()]},A:function(n){return v[n.getDay()]},b:function(n){return y[n.getMonth()]},B:function(n){return m[n.getMonth()]},c:t(f),d:function(n,t){return Ut(n.getDate(),t,2)},e:function(n,t){return Ut(n.getDate(),t,2)},H:function(n,t){return Ut(n.getHours(),t,2)},I:function(n,t){return Ut(n.getHours()%12||12,t,2)},j:function(n,t){return Ut(1+tc.dayOfYear(n),t,3)},L:function(n,t){return Ut(n.getMilliseconds(),t,3)},m:function(n,t){return Ut(n.getMonth()+1,t,2)},M:function(n,t){return Ut(n.getMinutes(),t,2)},p:function(n){return p[+(n.getHours()>=12)]},S:function(n,t){return Ut(n.getSeconds(),t,2)},U:function(n,t){return Ut(tc.sundayOfYear(n),t,2)},w:function(n){return n.getDay()},W:function(n,t){return Ut(tc.mondayOfYear(n),t,2)},x:t(h),X:t(g),y:function(n,t){return Ut(n.getFullYear()%100,t,2)},Y:function(n,t){return Ut(n.getFullYear()%1e4,t,4)},Z:ne,"%":function(){return"%"}},N={a:r,A:u,b:i,B:o,c:a,d:Bt,e:Bt,H:Jt,I:Jt,j:Wt,L:Qt,m:$t,M:Gt,p:l,S:Kt,U:Ot,w:Ft,W:Yt,x:c,X:s,y:Zt,Y:It,Z:Vt,"%":te};return t}function Ut(n,t,e){var r=0>n?"-":"",u=(r?-n:n)+"",i=u.length;return r+(e>i?new Array(e-i+1).join(t)+u:u)}function jt(n){return new RegExp("^(?:"+n.map(Xo.requote).join("|")+")","i")}function Ht(n){for(var t=new u,e=-1,r=n.length;++e<r;)t.set(n[e].toLowerCase(),e);return t}function Ft(n,t,e){ic.lastIndex=0;var r=ic.exec(t.substring(e,e+1));return r?(n.w=+r[0],e+r[0].length):-1}function Ot(n,t,e){ic.lastIndex=0;var r=ic.exec(t.substring(e));return r?(n.U=+r[0],e+r[0].length):-1}function Yt(n,t,e){ic.lastIndex=0;var r=ic.exec(t.substring(e));return r?(n.W=+r[0],e+r[0].length):-1}function It(n,t,e){ic.lastIndex=0;var r=ic.exec(t.substring(e,e+4));return r?(n.y=+r[0],e+r[0].length):-1}function Zt(n,t,e){ic.lastIndex=0;var r=ic.exec(t.substring(e,e+2));return r?(n.y=Xt(+r[0]),e+r[0].length):-1}function Vt(n,t,e){return/^[+-]\d{4}$/.test(t=t.substring(e,e+5))?(n.Z=+t,e+5):-1}function Xt(n){return n+(n>68?1900:2e3)}function $t(n,t,e){ic.lastIndex=0;var r=ic.exec(t.substring(e,e+2));return r?(n.m=r[0]-1,e+r[0].length):-1}function Bt(n,t,e){ic.lastIndex=0;var r=ic.exec(t.substring(e,e+2));return r?(n.d=+r[0],e+r[0].length):-1}function Wt(n,t,e){ic.lastIndex=0;var r=ic.exec(t.substring(e,e+3));return r?(n.j=+r[0],e+r[0].length):-1}function Jt(n,t,e){ic.lastIndex=0;var r=ic.exec(t.substring(e,e+2));return r?(n.H=+r[0],e+r[0].length):-1}function Gt(n,t,e){ic.lastIndex=0;var r=ic.exec(t.substring(e,e+2));return r?(n.M=+r[0],e+r[0].length):-1}function Kt(n,t,e){ic.lastIndex=0;var r=ic.exec(t.substring(e,e+2));return r?(n.S=+r[0],e+r[0].length):-1}function Qt(n,t,e){ic.lastIndex=0;var r=ic.exec(t.substring(e,e+3));return r?(n.L=+r[0],e+r[0].length):-1}function ne(n){var t=n.getTimezoneOffset(),e=t>0?"-":"+",r=~~(oa(t)/60),u=oa(t)%60;return e+Ut(r,"0",2)+Ut(u,"0",2)}function te(n,t,e){oc.lastIndex=0;var r=oc.exec(t.substring(e,e+1));return r?e+r[0].length:-1}function ee(n){for(var t=n.length,e=-1;++e<t;)n[e][0]=this(n[e][0]);return function(t){for(var e=0,r=n[e];!r[1](t);)r=n[++e];return r[0](t)}}function re(){}function ue(n,t,e){var r=e.s=n+t,u=r-n,i=r-u;e.t=n-i+(t-u)}function ie(n,t){n&&lc.hasOwnProperty(n.type)&&lc[n.type](n,t)}function oe(n,t,e){var r,u=-1,i=n.length-e;for(t.lineStart();++u<i;)r=n[u],t.point(r[0],r[1],r[2]);t.lineEnd()}function ae(n,t){var e=-1,r=n.length;for(t.polygonStart();++e<r;)oe(n[e],t,1);t.polygonEnd()}function ce(){function n(n,t){n*=Na,t=t*Na/2+Sa/4;var e=n-r,o=Math.cos(t),a=Math.sin(t),c=i*a,s=u*o+c*Math.cos(e),l=c*Math.sin(e);hc.add(Math.atan2(l,s)),r=n,u=o,i=a}var t,e,r,u,i;gc.point=function(o,a){gc.point=n,r=(t=o)*Na,u=Math.cos(a=(e=a)*Na/2+Sa/4),i=Math.sin(a)},gc.lineEnd=function(){n(t,e)}}function se(n){var t=n[0],e=n[1],r=Math.cos(e);return[r*Math.cos(t),r*Math.sin(t),Math.sin(e)]}function le(n,t){return n[0]*t[0]+n[1]*t[1]+n[2]*t[2]}function fe(n,t){return[n[1]*t[2]-n[2]*t[1],n[2]*t[0]-n[0]*t[2],n[0]*t[1]-n[1]*t[0]]}function he(n,t){n[0]+=t[0],n[1]+=t[1],n[2]+=t[2]}function ge(n,t){return[n[0]*t,n[1]*t,n[2]*t]}function pe(n){var t=Math.sqrt(n[0]*n[0]+n[1]*n[1]+n[2]*n[2]);n[0]/=t,n[1]/=t,n[2]/=t}function ve(n){return[Math.atan2(n[1],n[0]),X(n[2])]}function de(n,t){return oa(n[0]-t[0])<Aa&&oa(n[1]-t[1])<Aa}function me(n,t){n*=Na;var e=Math.cos(t*=Na);ye(e*Math.cos(n),e*Math.sin(n),Math.sin(t))}function ye(n,t,e){++pc,dc+=(n-dc)/pc,mc+=(t-mc)/pc,yc+=(e-yc)/pc}function xe(){function n(n,u){n*=Na;var i=Math.cos(u*=Na),o=i*Math.cos(n),a=i*Math.sin(n),c=Math.sin(u),s=Math.atan2(Math.sqrt((s=e*c-r*a)*s+(s=r*o-t*c)*s+(s=t*a-e*o)*s),t*o+e*a+r*c);vc+=s,xc+=s*(t+(t=o)),Mc+=s*(e+(e=a)),_c+=s*(r+(r=c)),ye(t,e,r)}var t,e,r;kc.point=function(u,i){u*=Na;var o=Math.cos(i*=Na);t=o*Math.cos(u),e=o*Math.sin(u),r=Math.sin(i),kc.point=n,ye(t,e,r)}}function Me(){kc.point=me}function _e(){function n(n,t){n*=Na;var e=Math.cos(t*=Na),o=e*Math.cos(n),a=e*Math.sin(n),c=Math.sin(t),s=u*c-i*a,l=i*o-r*c,f=r*a-u*o,h=Math.sqrt(s*s+l*l+f*f),g=r*o+u*a+i*c,p=h&&-V(g)/h,v=Math.atan2(h,g);bc+=p*s,wc+=p*l,Sc+=p*f,vc+=v,xc+=v*(r+(r=o)),Mc+=v*(u+(u=a)),_c+=v*(i+(i=c)),ye(r,u,i)}var t,e,r,u,i;kc.point=function(o,a){t=o,e=a,kc.point=n,o*=Na;var c=Math.cos(a*=Na);r=c*Math.cos(o),u=c*Math.sin(o),i=Math.sin(a),ye(r,u,i)},kc.lineEnd=function(){n(t,e),kc.lineEnd=Me,kc.point=me}}function be(){return!0}function we(n,t,e,r,u){var i=[],o=[];if(n.forEach(function(n){if(!((t=n.length-1)<=0)){var t,e=n[0],r=n[t];if(de(e,r)){u.lineStart();for(var a=0;t>a;++a)u.point((e=n[a])[0],e[1]);return u.lineEnd(),void 0}var c=new ke(e,n,null,!0),s=new ke(e,null,c,!1);c.o=s,i.push(c),o.push(s),c=new ke(r,n,null,!1),s=new ke(r,null,c,!0),c.o=s,i.push(c),o.push(s)}}),o.sort(t),Se(i),Se(o),i.length){for(var a=0,c=e,s=o.length;s>a;++a)o[a].e=c=!c;for(var l,f,h=i[0];;){for(var g=h,p=!0;g.v;)if((g=g.n)===h)return;l=g.z,u.lineStart();do{if(g.v=g.o.v=!0,g.e){if(p)for(var a=0,s=l.length;s>a;++a)u.point((f=l[a])[0],f[1]);else r(g.x,g.n.x,1,u);g=g.n}else{if(p){l=g.p.z;for(var a=l.length-1;a>=0;--a)u.point((f=l[a])[0],f[1])}else r(g.x,g.p.x,-1,u);g=g.p}g=g.o,l=g.z,p=!p}while(!g.v);u.lineEnd()}}}function Se(n){if(t=n.length){for(var t,e,r=0,u=n[0];++r<t;)u.n=e=n[r],e.p=u,u=e;u.n=e=n[0],e.p=u}}function ke(n,t,e,r){this.x=n,this.z=t,this.o=e,this.e=r,this.v=!1,this.n=this.p=null}function Ee(n,t,e,r){return function(u,i){function o(t,e){var r=u(t,e);n(t=r[0],e=r[1])&&i.point(t,e)}function a(n,t){var e=u(n,t);d.point(e[0],e[1])}function c(){y.point=a,d.lineStart()}function s(){y.point=o,d.lineEnd()}function l(n,t){v.push([n,t]);var e=u(n,t);M.point(e[0],e[1])}function f(){M.lineStart(),v=[]}function h(){l(v[0][0],v[0][1]),M.lineEnd();var n,t=M.clean(),e=x.buffer(),r=e.length;if(v.pop(),p.push(v),v=null,r){if(1&t){n=e[0];var u,r=n.length-1,o=-1;for(i.lineStart();++o<r;)i.point((u=n[o])[0],u[1]);return i.lineEnd(),void 0}r>1&&2&t&&e.push(e.pop().concat(e.shift())),g.push(e.filter(Ae))}}var g,p,v,d=t(i),m=u.invert(r[0],r[1]),y={point:o,lineStart:c,lineEnd:s,polygonStart:function(){y.point=l,y.lineStart=f,y.lineEnd=h,g=[],p=[],i.polygonStart()},polygonEnd:function(){y.point=o,y.lineStart=c,y.lineEnd=s,g=Xo.merge(g);var n=Le(m,p);g.length?we(g,Ne,n,e,i):n&&(i.lineStart(),e(null,null,1,i),i.lineEnd()),i.polygonEnd(),g=p=null},sphere:function(){i.polygonStart(),i.lineStart(),e(null,null,1,i),i.lineEnd(),i.polygonEnd()}},x=Ce(),M=t(x);return y}}function Ae(n){return n.length>1}function Ce(){var n,t=[];return{lineStart:function(){t.push(n=[])},point:function(t,e){n.push([t,e])},lineEnd:g,buffer:function(){var e=t;return t=[],n=null,e},rejoin:function(){t.length>1&&t.push(t.pop().concat(t.shift()))}}}function Ne(n,t){return((n=n.x)[0]<0?n[1]-Ea-Aa:Ea-n[1])-((t=t.x)[0]<0?t[1]-Ea-Aa:Ea-t[1])}function Le(n,t){var e=n[0],r=n[1],u=[Math.sin(e),-Math.cos(e),0],i=0,o=0;hc.reset();for(var a=0,c=t.length;c>a;++a){var s=t[a],l=s.length;if(l)for(var f=s[0],h=f[0],g=f[1]/2+Sa/4,p=Math.sin(g),v=Math.cos(g),d=1;;){d===l&&(d=0),n=s[d];var m=n[0],y=n[1]/2+Sa/4,x=Math.sin(y),M=Math.cos(y),_=m-h,b=oa(_)>Sa,w=p*x;if(hc.add(Math.atan2(w*Math.sin(_),v*M+w*Math.cos(_))),i+=b?_+(_>=0?ka:-ka):_,b^h>=e^m>=e){var S=fe(se(f),se(n));pe(S);var k=fe(u,S);pe(k);var E=(b^_>=0?-1:1)*X(k[2]);(r>E||r===E&&(S[0]||S[1]))&&(o+=b^_>=0?1:-1)}if(!d++)break;h=m,p=x,v=M,f=n}}return(-Aa>i||Aa>i&&0>hc)^1&o}function ze(n){var t,e=0/0,r=0/0,u=0/0;return{lineStart:function(){n.lineStart(),t=1},point:function(i,o){var a=i>0?Sa:-Sa,c=oa(i-e);oa(c-Sa)<Aa?(n.point(e,r=(r+o)/2>0?Ea:-Ea),n.point(u,r),n.lineEnd(),n.lineStart(),n.point(a,r),n.point(i,r),t=0):u!==a&&c>=Sa&&(oa(e-u)<Aa&&(e-=u*Aa),oa(i-a)<Aa&&(i-=a*Aa),r=qe(e,r,i,o),n.point(u,r),n.lineEnd(),n.lineStart(),n.point(a,r),t=0),n.point(e=i,r=o),u=a},lineEnd:function(){n.lineEnd(),e=r=0/0},clean:function(){return 2-t}}}function qe(n,t,e,r){var u,i,o=Math.sin(n-e);return oa(o)>Aa?Math.atan((Math.sin(t)*(i=Math.cos(r))*Math.sin(e)-Math.sin(r)*(u=Math.cos(t))*Math.sin(n))/(u*i*o)):(t+r)/2}function Te(n,t,e,r){var u;if(null==n)u=e*Ea,r.point(-Sa,u),r.point(0,u),r.point(Sa,u),r.point(Sa,0),r.point(Sa,-u),r.point(0,-u),r.point(-Sa,-u),r.point(-Sa,0),r.point(-Sa,u);else if(oa(n[0]-t[0])>Aa){var i=n[0]<t[0]?Sa:-Sa;u=e*i/2,r.point(-i,u),r.point(0,u),r.point(i,u)}else r.point(t[0],t[1])}function Re(n){function t(n,t){return Math.cos(n)*Math.cos(t)>i}function e(n){var e,i,c,s,l;return{lineStart:function(){s=c=!1,l=1},point:function(f,h){var g,p=[f,h],v=t(f,h),d=o?v?0:u(f,h):v?u(f+(0>f?Sa:-Sa),h):0;if(!e&&(s=c=v)&&n.lineStart(),v!==c&&(g=r(e,p),(de(e,g)||de(p,g))&&(p[0]+=Aa,p[1]+=Aa,v=t(p[0],p[1]))),v!==c)l=0,v?(n.lineStart(),g=r(p,e),n.point(g[0],g[1])):(g=r(e,p),n.point(g[0],g[1]),n.lineEnd()),e=g;else if(a&&e&&o^v){var m;d&i||!(m=r(p,e,!0))||(l=0,o?(n.lineStart(),n.point(m[0][0],m[0][1]),n.point(m[1][0],m[1][1]),n.lineEnd()):(n.point(m[1][0],m[1][1]),n.lineEnd(),n.lineStart(),n.point(m[0][0],m[0][1])))}!v||e&&de(e,p)||n.point(p[0],p[1]),e=p,c=v,i=d},lineEnd:function(){c&&n.lineEnd(),e=null},clean:function(){return l|(s&&c)<<1}}}function r(n,t,e){var r=se(n),u=se(t),o=[1,0,0],a=fe(r,u),c=le(a,a),s=a[0],l=c-s*s;if(!l)return!e&&n;var f=i*c/l,h=-i*s/l,g=fe(o,a),p=ge(o,f),v=ge(a,h);he(p,v);var d=g,m=le(p,d),y=le(d,d),x=m*m-y*(le(p,p)-1);if(!(0>x)){var M=Math.sqrt(x),_=ge(d,(-m-M)/y);if(he(_,p),_=ve(_),!e)return _;var b,w=n[0],S=t[0],k=n[1],E=t[1];w>S&&(b=w,w=S,S=b);var A=S-w,C=oa(A-Sa)<Aa,N=C||Aa>A;if(!C&&k>E&&(b=k,k=E,E=b),N?C?k+E>0^_[1]<(oa(_[0]-w)<Aa?k:E):k<=_[1]&&_[1]<=E:A>Sa^(w<=_[0]&&_[0]<=S)){var L=ge(d,(-m+M)/y);return he(L,p),[_,ve(L)]}}}function u(t,e){var r=o?n:Sa-n,u=0;return-r>t?u|=1:t>r&&(u|=2),-r>e?u|=4:e>r&&(u|=8),u}var i=Math.cos(n),o=i>0,a=oa(i)>Aa,c=cr(n,6*Na);return Ee(t,e,c,o?[0,-n]:[-Sa,n-Sa])}function De(n,t,e,r){return function(u){var i,o=u.a,a=u.b,c=o.x,s=o.y,l=a.x,f=a.y,h=0,g=1,p=l-c,v=f-s;if(i=n-c,p||!(i>0)){if(i/=p,0>p){if(h>i)return;g>i&&(g=i)}else if(p>0){if(i>g)return;i>h&&(h=i)}if(i=e-c,p||!(0>i)){if(i/=p,0>p){if(i>g)return;i>h&&(h=i)}else if(p>0){if(h>i)return;g>i&&(g=i)}if(i=t-s,v||!(i>0)){if(i/=v,0>v){if(h>i)return;g>i&&(g=i)}else if(v>0){if(i>g)return;i>h&&(h=i)}if(i=r-s,v||!(0>i)){if(i/=v,0>v){if(i>g)return;i>h&&(h=i)}else if(v>0){if(h>i)return;g>i&&(g=i)}return h>0&&(u.a={x:c+h*p,y:s+h*v}),1>g&&(u.b={x:c+g*p,y:s+g*v}),u}}}}}}function Pe(n,t,e,r){function u(r,u){return oa(r[0]-n)<Aa?u>0?0:3:oa(r[0]-e)<Aa?u>0?2:1:oa(r[1]-t)<Aa?u>0?1:0:u>0?3:2}function i(n,t){return o(n.x,t.x)}function o(n,t){var e=u(n,1),r=u(t,1);return e!==r?e-r:0===e?t[1]-n[1]:1===e?n[0]-t[0]:2===e?n[1]-t[1]:t[0]-n[0]}return function(a){function c(n){for(var t=0,e=d.length,r=n[1],u=0;e>u;++u)for(var i,o=1,a=d[u],c=a.length,s=a[0];c>o;++o)i=a[o],s[1]<=r?i[1]>r&&Z(s,i,n)>0&&++t:i[1]<=r&&Z(s,i,n)<0&&--t,s=i;return 0!==t}function s(i,a,c,s){var l=0,f=0;if(null==i||(l=u(i,c))!==(f=u(a,c))||o(i,a)<0^c>0){do s.point(0===l||3===l?n:e,l>1?r:t);while((l=(l+c+4)%4)!==f)}else s.point(a[0],a[1])}function l(u,i){return u>=n&&e>=u&&i>=t&&r>=i}function f(n,t){l(n,t)&&a.point(n,t)}function h(){N.point=p,d&&d.push(m=[]),S=!0,w=!1,_=b=0/0}function g(){v&&(p(y,x),M&&w&&A.rejoin(),v.push(A.buffer())),N.point=f,w&&a.lineEnd()}function p(n,t){n=Math.max(-Ac,Math.min(Ac,n)),t=Math.max(-Ac,Math.min(Ac,t));var e=l(n,t);if(d&&m.push([n,t]),S)y=n,x=t,M=e,S=!1,e&&(a.lineStart(),a.point(n,t));else if(e&&w)a.point(n,t);else{var r={a:{x:_,y:b},b:{x:n,y:t}};C(r)?(w||(a.lineStart(),a.point(r.a.x,r.a.y)),a.point(r.b.x,r.b.y),e||a.lineEnd(),k=!1):e&&(a.lineStart(),a.point(n,t),k=!1)}_=n,b=t,w=e}var v,d,m,y,x,M,_,b,w,S,k,E=a,A=Ce(),C=De(n,t,e,r),N={point:f,lineStart:h,lineEnd:g,polygonStart:function(){a=A,v=[],d=[],k=!0},polygonEnd:function(){a=E,v=Xo.merge(v);var t=c([n,r]),e=k&&t,u=v.length;(e||u)&&(a.polygonStart(),e&&(a.lineStart(),s(null,null,1,a),a.lineEnd()),u&&we(v,i,t,s,a),a.polygonEnd()),v=d=m=null}};return N}}function Ue(n,t){function e(e,r){return e=n(e,r),t(e[0],e[1])}return n.invert&&t.invert&&(e.invert=function(e,r){return e=t.invert(e,r),e&&n.invert(e[0],e[1])}),e}function je(n){var t=0,e=Sa/3,r=nr(n),u=r(t,e);return u.parallels=function(n){return arguments.length?r(t=n[0]*Sa/180,e=n[1]*Sa/180):[180*(t/Sa),180*(e/Sa)]},u}function He(n,t){function e(n,t){var e=Math.sqrt(i-2*u*Math.sin(t))/u;return[e*Math.sin(n*=u),o-e*Math.cos(n)]}var r=Math.sin(n),u=(r+Math.sin(t))/2,i=1+r*(2*u-r),o=Math.sqrt(i)/u;return e.invert=function(n,t){var e=o-t;return[Math.atan2(n,e)/u,X((i-(n*n+e*e)*u*u)/(2*u))]},e}function Fe(){function n(n,t){Nc+=u*n-r*t,r=n,u=t}var t,e,r,u;Rc.point=function(i,o){Rc.point=n,t=r=i,e=u=o},Rc.lineEnd=function(){n(t,e)}}function Oe(n,t){Lc>n&&(Lc=n),n>qc&&(qc=n),zc>t&&(zc=t),t>Tc&&(Tc=t)}function Ye(){function n(n,t){o.push("M",n,",",t,i)}function t(n,t){o.push("M",n,",",t),a.point=e}function e(n,t){o.push("L",n,",",t)}function r(){a.point=n}function u(){o.push("Z")}var i=Ie(4.5),o=[],a={point:n,lineStart:function(){a.point=t},lineEnd:r,polygonStart:function(){a.lineEnd=u},polygonEnd:function(){a.lineEnd=r,a.point=n},pointRadius:function(n){return i=Ie(n),a},result:function(){if(o.length){var n=o.join("");return o=[],n}}};return a}function Ie(n){return"m0,"+n+"a"+n+","+n+" 0 1,1 0,"+-2*n+"a"+n+","+n+" 0 1,1 0,"+2*n+"z"}function Ze(n,t){dc+=n,mc+=t,++yc}function Ve(){function n(n,r){var u=n-t,i=r-e,o=Math.sqrt(u*u+i*i);xc+=o*(t+n)/2,Mc+=o*(e+r)/2,_c+=o,Ze(t=n,e=r)}var t,e;Pc.point=function(r,u){Pc.point=n,Ze(t=r,e=u)}}function Xe(){Pc.point=Ze}function $e(){function n(n,t){var e=n-r,i=t-u,o=Math.sqrt(e*e+i*i);xc+=o*(r+n)/2,Mc+=o*(u+t)/2,_c+=o,o=u*n-r*t,bc+=o*(r+n),wc+=o*(u+t),Sc+=3*o,Ze(r=n,u=t)}var t,e,r,u;Pc.point=function(i,o){Pc.point=n,Ze(t=r=i,e=u=o)},Pc.lineEnd=function(){n(t,e)}}function Be(n){function t(t,e){n.moveTo(t,e),n.arc(t,e,o,0,ka)}function e(t,e){n.moveTo(t,e),a.point=r}function r(t,e){n.lineTo(t,e)}function u(){a.point=t}function i(){n.closePath()}var o=4.5,a={point:t,lineStart:function(){a.point=e},lineEnd:u,polygonStart:function(){a.lineEnd=i},polygonEnd:function(){a.lineEnd=u,a.point=t},pointRadius:function(n){return o=n,a},result:g};return a}function We(n){function t(n){return(a?r:e)(n)}function e(t){return Ke(t,function(e,r){e=n(e,r),t.point(e[0],e[1])})}function r(t){function e(e,r){e=n(e,r),t.point(e[0],e[1])}function r(){x=0/0,S.point=i,t.lineStart()}function i(e,r){var i=se([e,r]),o=n(e,r);u(x,M,y,_,b,w,x=o[0],M=o[1],y=e,_=i[0],b=i[1],w=i[2],a,t),t.point(x,M)}function o(){S.point=e,t.lineEnd()}function c(){r(),S.point=s,S.lineEnd=l}function s(n,t){i(f=n,h=t),g=x,p=M,v=_,d=b,m=w,S.point=i}function l(){u(x,M,y,_,b,w,g,p,f,v,d,m,a,t),S.lineEnd=o,o()}var f,h,g,p,v,d,m,y,x,M,_,b,w,S={point:e,lineStart:r,lineEnd:o,polygonStart:function(){t.polygonStart(),S.lineStart=c},polygonEnd:function(){t.polygonEnd(),S.lineStart=r}};return S}function u(t,e,r,a,c,s,l,f,h,g,p,v,d,m){var y=l-t,x=f-e,M=y*y+x*x;if(M>4*i&&d--){var _=a+g,b=c+p,w=s+v,S=Math.sqrt(_*_+b*b+w*w),k=Math.asin(w/=S),E=oa(oa(w)-1)<Aa||oa(r-h)<Aa?(r+h)/2:Math.atan2(b,_),A=n(E,k),C=A[0],N=A[1],L=C-t,z=N-e,q=x*L-y*z;(q*q/M>i||oa((y*L+x*z)/M-.5)>.3||o>a*g+c*p+s*v)&&(u(t,e,r,a,c,s,C,N,E,_/=S,b/=S,w,d,m),m.point(C,N),u(C,N,E,_,b,w,l,f,h,g,p,v,d,m))}}var i=.5,o=Math.cos(30*Na),a=16;return t.precision=function(n){return arguments.length?(a=(i=n*n)>0&&16,t):Math.sqrt(i)},t}function Je(n){var t=We(function(t,e){return n([t*La,e*La])});return function(n){return tr(t(n))}}function Ge(n){this.stream=n}function Ke(n,t){return{point:t,sphere:function(){n.sphere()},lineStart:function(){n.lineStart()},lineEnd:function(){n.lineEnd()},polygonStart:function(){n.polygonStart()},polygonEnd:function(){n.polygonEnd()}}}function Qe(n){return nr(function(){return n})()}function nr(n){function t(n){return n=a(n[0]*Na,n[1]*Na),[n[0]*h+c,s-n[1]*h]}function e(n){return n=a.invert((n[0]-c)/h,(s-n[1])/h),n&&[n[0]*La,n[1]*La]}function r(){a=Ue(o=ur(m,y,x),i);var n=i(v,d);return c=g-n[0]*h,s=p+n[1]*h,u()}function u(){return l&&(l.valid=!1,l=null),t}var i,o,a,c,s,l,f=We(function(n,t){return n=i(n,t),[n[0]*h+c,s-n[1]*h]}),h=150,g=480,p=250,v=0,d=0,m=0,y=0,x=0,M=Ec,_=bt,b=null,w=null;return t.stream=function(n){return l&&(l.valid=!1),l=tr(M(o,f(_(n)))),l.valid=!0,l},t.clipAngle=function(n){return arguments.length?(M=null==n?(b=n,Ec):Re((b=+n)*Na),u()):b
-},t.clipExtent=function(n){return arguments.length?(w=n,_=n?Pe(n[0][0],n[0][1],n[1][0],n[1][1]):bt,u()):w},t.scale=function(n){return arguments.length?(h=+n,r()):h},t.translate=function(n){return arguments.length?(g=+n[0],p=+n[1],r()):[g,p]},t.center=function(n){return arguments.length?(v=n[0]%360*Na,d=n[1]%360*Na,r()):[v*La,d*La]},t.rotate=function(n){return arguments.length?(m=n[0]%360*Na,y=n[1]%360*Na,x=n.length>2?n[2]%360*Na:0,r()):[m*La,y*La,x*La]},Xo.rebind(t,f,"precision"),function(){return i=n.apply(this,arguments),t.invert=i.invert&&e,r()}}function tr(n){return Ke(n,function(t,e){n.point(t*Na,e*Na)})}function er(n,t){return[n,t]}function rr(n,t){return[n>Sa?n-ka:-Sa>n?n+ka:n,t]}function ur(n,t,e){return n?t||e?Ue(or(n),ar(t,e)):or(n):t||e?ar(t,e):rr}function ir(n){return function(t,e){return t+=n,[t>Sa?t-ka:-Sa>t?t+ka:t,e]}}function or(n){var t=ir(n);return t.invert=ir(-n),t}function ar(n,t){function e(n,t){var e=Math.cos(t),a=Math.cos(n)*e,c=Math.sin(n)*e,s=Math.sin(t),l=s*r+a*u;return[Math.atan2(c*i-l*o,a*r-s*u),X(l*i+c*o)]}var r=Math.cos(n),u=Math.sin(n),i=Math.cos(t),o=Math.sin(t);return e.invert=function(n,t){var e=Math.cos(t),a=Math.cos(n)*e,c=Math.sin(n)*e,s=Math.sin(t),l=s*i-c*o;return[Math.atan2(c*i+s*o,a*r+l*u),X(l*r-a*u)]},e}function cr(n,t){var e=Math.cos(n),r=Math.sin(n);return function(u,i,o,a){var c=o*t;null!=u?(u=sr(e,u),i=sr(e,i),(o>0?i>u:u>i)&&(u+=o*ka)):(u=n+o*ka,i=n-.5*c);for(var s,l=u;o>0?l>i:i>l;l-=c)a.point((s=ve([e,-r*Math.cos(l),-r*Math.sin(l)]))[0],s[1])}}function sr(n,t){var e=se(t);e[0]-=n,pe(e);var r=V(-e[1]);return((-e[2]<0?-r:r)+2*Math.PI-Aa)%(2*Math.PI)}function lr(n,t,e){var r=Xo.range(n,t-Aa,e).concat(t);return function(n){return r.map(function(t){return[n,t]})}}function fr(n,t,e){var r=Xo.range(n,t-Aa,e).concat(t);return function(n){return r.map(function(t){return[t,n]})}}function hr(n){return n.source}function gr(n){return n.target}function pr(n,t,e,r){var u=Math.cos(t),i=Math.sin(t),o=Math.cos(r),a=Math.sin(r),c=u*Math.cos(n),s=u*Math.sin(n),l=o*Math.cos(e),f=o*Math.sin(e),h=2*Math.asin(Math.sqrt(J(r-t)+u*o*J(e-n))),g=1/Math.sin(h),p=h?function(n){var t=Math.sin(n*=h)*g,e=Math.sin(h-n)*g,r=e*c+t*l,u=e*s+t*f,o=e*i+t*a;return[Math.atan2(u,r)*La,Math.atan2(o,Math.sqrt(r*r+u*u))*La]}:function(){return[n*La,t*La]};return p.distance=h,p}function vr(){function n(n,u){var i=Math.sin(u*=Na),o=Math.cos(u),a=oa((n*=Na)-t),c=Math.cos(a);Uc+=Math.atan2(Math.sqrt((a=o*Math.sin(a))*a+(a=r*i-e*o*c)*a),e*i+r*o*c),t=n,e=i,r=o}var t,e,r;jc.point=function(u,i){t=u*Na,e=Math.sin(i*=Na),r=Math.cos(i),jc.point=n},jc.lineEnd=function(){jc.point=jc.lineEnd=g}}function dr(n,t){function e(t,e){var r=Math.cos(t),u=Math.cos(e),i=n(r*u);return[i*u*Math.sin(t),i*Math.sin(e)]}return e.invert=function(n,e){var r=Math.sqrt(n*n+e*e),u=t(r),i=Math.sin(u),o=Math.cos(u);return[Math.atan2(n*i,r*o),Math.asin(r&&e*i/r)]},e}function mr(n,t){function e(n,t){var e=oa(oa(t)-Ea)<Aa?0:o/Math.pow(u(t),i);return[e*Math.sin(i*n),o-e*Math.cos(i*n)]}var r=Math.cos(n),u=function(n){return Math.tan(Sa/4+n/2)},i=n===t?Math.sin(n):Math.log(r/Math.cos(t))/Math.log(u(t)/u(n)),o=r*Math.pow(u(n),i)/i;return i?(e.invert=function(n,t){var e=o-t,r=I(i)*Math.sqrt(n*n+e*e);return[Math.atan2(n,e)/i,2*Math.atan(Math.pow(o/r,1/i))-Ea]},e):xr}function yr(n,t){function e(n,t){var e=i-t;return[e*Math.sin(u*n),i-e*Math.cos(u*n)]}var r=Math.cos(n),u=n===t?Math.sin(n):(r-Math.cos(t))/(t-n),i=r/u+n;return oa(u)<Aa?er:(e.invert=function(n,t){var e=i-t;return[Math.atan2(n,e)/u,i-I(u)*Math.sqrt(n*n+e*e)]},e)}function xr(n,t){return[n,Math.log(Math.tan(Sa/4+t/2))]}function Mr(n){var t,e=Qe(n),r=e.scale,u=e.translate,i=e.clipExtent;return e.scale=function(){var n=r.apply(e,arguments);return n===e?t?e.clipExtent(null):e:n},e.translate=function(){var n=u.apply(e,arguments);return n===e?t?e.clipExtent(null):e:n},e.clipExtent=function(n){var o=i.apply(e,arguments);if(o===e){if(t=null==n){var a=Sa*r(),c=u();i([[c[0]-a,c[1]-a],[c[0]+a,c[1]+a]])}}else t&&(o=null);return o},e.clipExtent(null)}function _r(n,t){return[Math.log(Math.tan(Sa/4+t/2)),-n]}function br(n){return n[0]}function wr(n){return n[1]}function Sr(n){for(var t=n.length,e=[0,1],r=2,u=2;t>u;u++){for(;r>1&&Z(n[e[r-2]],n[e[r-1]],n[u])<=0;)--r;e[r++]=u}return e.slice(0,r)}function kr(n,t){return n[0]-t[0]||n[1]-t[1]}function Er(n,t,e){return(e[0]-t[0])*(n[1]-t[1])<(e[1]-t[1])*(n[0]-t[0])}function Ar(n,t,e,r){var u=n[0],i=e[0],o=t[0]-u,a=r[0]-i,c=n[1],s=e[1],l=t[1]-c,f=r[1]-s,h=(a*(c-s)-f*(u-i))/(f*o-a*l);return[u+h*o,c+h*l]}function Cr(n){var t=n[0],e=n[n.length-1];return!(t[0]-e[0]||t[1]-e[1])}function Nr(){Jr(this),this.edge=this.site=this.circle=null}function Lr(n){var t=Jc.pop()||new Nr;return t.site=n,t}function zr(n){Or(n),$c.remove(n),Jc.push(n),Jr(n)}function qr(n){var t=n.circle,e=t.x,r=t.cy,u={x:e,y:r},i=n.P,o=n.N,a=[n];zr(n);for(var c=i;c.circle&&oa(e-c.circle.x)<Aa&&oa(r-c.circle.cy)<Aa;)i=c.P,a.unshift(c),zr(c),c=i;a.unshift(c),Or(c);for(var s=o;s.circle&&oa(e-s.circle.x)<Aa&&oa(r-s.circle.cy)<Aa;)o=s.N,a.push(s),zr(s),s=o;a.push(s),Or(s);var l,f=a.length;for(l=1;f>l;++l)s=a[l],c=a[l-1],$r(s.edge,c.site,s.site,u);c=a[0],s=a[f-1],s.edge=Vr(c.site,s.site,null,u),Fr(c),Fr(s)}function Tr(n){for(var t,e,r,u,i=n.x,o=n.y,a=$c._;a;)if(r=Rr(a,o)-i,r>Aa)a=a.L;else{if(u=i-Dr(a,o),!(u>Aa)){r>-Aa?(t=a.P,e=a):u>-Aa?(t=a,e=a.N):t=e=a;break}if(!a.R){t=a;break}a=a.R}var c=Lr(n);if($c.insert(t,c),t||e){if(t===e)return Or(t),e=Lr(t.site),$c.insert(c,e),c.edge=e.edge=Vr(t.site,c.site),Fr(t),Fr(e),void 0;if(!e)return c.edge=Vr(t.site,c.site),void 0;Or(t),Or(e);var s=t.site,l=s.x,f=s.y,h=n.x-l,g=n.y-f,p=e.site,v=p.x-l,d=p.y-f,m=2*(h*d-g*v),y=h*h+g*g,x=v*v+d*d,M={x:(d*y-g*x)/m+l,y:(h*x-v*y)/m+f};$r(e.edge,s,p,M),c.edge=Vr(s,n,null,M),e.edge=Vr(n,p,null,M),Fr(t),Fr(e)}}function Rr(n,t){var e=n.site,r=e.x,u=e.y,i=u-t;if(!i)return r;var o=n.P;if(!o)return-1/0;e=o.site;var a=e.x,c=e.y,s=c-t;if(!s)return a;var l=a-r,f=1/i-1/s,h=l/s;return f?(-h+Math.sqrt(h*h-2*f*(l*l/(-2*s)-c+s/2+u-i/2)))/f+r:(r+a)/2}function Dr(n,t){var e=n.N;if(e)return Rr(e,t);var r=n.site;return r.y===t?r.x:1/0}function Pr(n){this.site=n,this.edges=[]}function Ur(n){for(var t,e,r,u,i,o,a,c,s,l,f=n[0][0],h=n[1][0],g=n[0][1],p=n[1][1],v=Xc,d=v.length;d--;)if(i=v[d],i&&i.prepare())for(a=i.edges,c=a.length,o=0;c>o;)l=a[o].end(),r=l.x,u=l.y,s=a[++o%c].start(),t=s.x,e=s.y,(oa(r-t)>Aa||oa(u-e)>Aa)&&(a.splice(o,0,new Br(Xr(i.site,l,oa(r-f)<Aa&&p-u>Aa?{x:f,y:oa(t-f)<Aa?e:p}:oa(u-p)<Aa&&h-r>Aa?{x:oa(e-p)<Aa?t:h,y:p}:oa(r-h)<Aa&&u-g>Aa?{x:h,y:oa(t-h)<Aa?e:g}:oa(u-g)<Aa&&r-f>Aa?{x:oa(e-g)<Aa?t:f,y:g}:null),i.site,null)),++c)}function jr(n,t){return t.angle-n.angle}function Hr(){Jr(this),this.x=this.y=this.arc=this.site=this.cy=null}function Fr(n){var t=n.P,e=n.N;if(t&&e){var r=t.site,u=n.site,i=e.site;if(r!==i){var o=u.x,a=u.y,c=r.x-o,s=r.y-a,l=i.x-o,f=i.y-a,h=2*(c*f-s*l);if(!(h>=-Ca)){var g=c*c+s*s,p=l*l+f*f,v=(f*g-s*p)/h,d=(c*p-l*g)/h,f=d+a,m=Gc.pop()||new Hr;m.arc=n,m.site=u,m.x=v+o,m.y=f+Math.sqrt(v*v+d*d),m.cy=f,n.circle=m;for(var y=null,x=Wc._;x;)if(m.y<x.y||m.y===x.y&&m.x<=x.x){if(!x.L){y=x.P;break}x=x.L}else{if(!x.R){y=x;break}x=x.R}Wc.insert(y,m),y||(Bc=m)}}}}function Or(n){var t=n.circle;t&&(t.P||(Bc=t.N),Wc.remove(t),Gc.push(t),Jr(t),n.circle=null)}function Yr(n){for(var t,e=Vc,r=De(n[0][0],n[0][1],n[1][0],n[1][1]),u=e.length;u--;)t=e[u],(!Ir(t,n)||!r(t)||oa(t.a.x-t.b.x)<Aa&&oa(t.a.y-t.b.y)<Aa)&&(t.a=t.b=null,e.splice(u,1))}function Ir(n,t){var e=n.b;if(e)return!0;var r,u,i=n.a,o=t[0][0],a=t[1][0],c=t[0][1],s=t[1][1],l=n.l,f=n.r,h=l.x,g=l.y,p=f.x,v=f.y,d=(h+p)/2,m=(g+v)/2;if(v===g){if(o>d||d>=a)return;if(h>p){if(i){if(i.y>=s)return}else i={x:d,y:c};e={x:d,y:s}}else{if(i){if(i.y<c)return}else i={x:d,y:s};e={x:d,y:c}}}else if(r=(h-p)/(v-g),u=m-r*d,-1>r||r>1)if(h>p){if(i){if(i.y>=s)return}else i={x:(c-u)/r,y:c};e={x:(s-u)/r,y:s}}else{if(i){if(i.y<c)return}else i={x:(s-u)/r,y:s};e={x:(c-u)/r,y:c}}else if(v>g){if(i){if(i.x>=a)return}else i={x:o,y:r*o+u};e={x:a,y:r*a+u}}else{if(i){if(i.x<o)return}else i={x:a,y:r*a+u};e={x:o,y:r*o+u}}return n.a=i,n.b=e,!0}function Zr(n,t){this.l=n,this.r=t,this.a=this.b=null}function Vr(n,t,e,r){var u=new Zr(n,t);return Vc.push(u),e&&$r(u,n,t,e),r&&$r(u,t,n,r),Xc[n.i].edges.push(new Br(u,n,t)),Xc[t.i].edges.push(new Br(u,t,n)),u}function Xr(n,t,e){var r=new Zr(n,null);return r.a=t,r.b=e,Vc.push(r),r}function $r(n,t,e,r){n.a||n.b?n.l===e?n.b=r:n.a=r:(n.a=r,n.l=t,n.r=e)}function Br(n,t,e){var r=n.a,u=n.b;this.edge=n,this.site=t,this.angle=e?Math.atan2(e.y-t.y,e.x-t.x):n.l===t?Math.atan2(u.x-r.x,r.y-u.y):Math.atan2(r.x-u.x,u.y-r.y)}function Wr(){this._=null}function Jr(n){n.U=n.C=n.L=n.R=n.P=n.N=null}function Gr(n,t){var e=t,r=t.R,u=e.U;u?u.L===e?u.L=r:u.R=r:n._=r,r.U=u,e.U=r,e.R=r.L,e.R&&(e.R.U=e),r.L=e}function Kr(n,t){var e=t,r=t.L,u=e.U;u?u.L===e?u.L=r:u.R=r:n._=r,r.U=u,e.U=r,e.L=r.R,e.L&&(e.L.U=e),r.R=e}function Qr(n){for(;n.L;)n=n.L;return n}function nu(n,t){var e,r,u,i=n.sort(tu).pop();for(Vc=[],Xc=new Array(n.length),$c=new Wr,Wc=new Wr;;)if(u=Bc,i&&(!u||i.y<u.y||i.y===u.y&&i.x<u.x))(i.x!==e||i.y!==r)&&(Xc[i.i]=new Pr(i),Tr(i),e=i.x,r=i.y),i=n.pop();else{if(!u)break;qr(u.arc)}t&&(Yr(t),Ur(t));var o={cells:Xc,edges:Vc};return $c=Wc=Vc=Xc=null,o}function tu(n,t){return t.y-n.y||t.x-n.x}function eu(n,t,e){return(n.x-e.x)*(t.y-n.y)-(n.x-t.x)*(e.y-n.y)}function ru(n){return n.x}function uu(n){return n.y}function iu(){return{leaf:!0,nodes:[],point:null,x:null,y:null}}function ou(n,t,e,r,u,i){if(!n(t,e,r,u,i)){var o=.5*(e+u),a=.5*(r+i),c=t.nodes;c[0]&&ou(n,c[0],e,r,o,a),c[1]&&ou(n,c[1],o,r,u,a),c[2]&&ou(n,c[2],e,a,o,i),c[3]&&ou(n,c[3],o,a,u,i)}}function au(n,t){n=Xo.rgb(n),t=Xo.rgb(t);var e=n.r,r=n.g,u=n.b,i=t.r-e,o=t.g-r,a=t.b-u;return function(n){return"#"+vt(Math.round(e+i*n))+vt(Math.round(r+o*n))+vt(Math.round(u+a*n))}}function cu(n,t){var e,r={},u={};for(e in n)e in t?r[e]=fu(n[e],t[e]):u[e]=n[e];for(e in t)e in n||(u[e]=t[e]);return function(n){for(e in r)u[e]=r[e](n);return u}}function su(n,t){return t-=n=+n,function(e){return n+t*e}}function lu(n,t){var e,r,u,i,o,a=0,c=0,s=[],l=[];for(n+="",t+="",Qc.lastIndex=0,r=0;e=Qc.exec(t);++r)e.index&&s.push(t.substring(a,c=e.index)),l.push({i:s.length,x:e[0]}),s.push(null),a=Qc.lastIndex;for(a<t.length&&s.push(t.substring(a)),r=0,i=l.length;(e=Qc.exec(n))&&i>r;++r)if(o=l[r],o.x==e[0]){if(o.i)if(null==s[o.i+1])for(s[o.i-1]+=o.x,s.splice(o.i,1),u=r+1;i>u;++u)l[u].i--;else for(s[o.i-1]+=o.x+s[o.i+1],s.splice(o.i,2),u=r+1;i>u;++u)l[u].i-=2;else if(null==s[o.i+1])s[o.i]=o.x;else for(s[o.i]=o.x+s[o.i+1],s.splice(o.i+1,1),u=r+1;i>u;++u)l[u].i--;l.splice(r,1),i--,r--}else o.x=su(parseFloat(e[0]),parseFloat(o.x));for(;i>r;)o=l.pop(),null==s[o.i+1]?s[o.i]=o.x:(s[o.i]=o.x+s[o.i+1],s.splice(o.i+1,1)),i--;return 1===s.length?null==s[0]?(o=l[0].x,function(n){return o(n)+""}):function(){return t}:function(n){for(r=0;i>r;++r)s[(o=l[r]).i]=o.x(n);return s.join("")}}function fu(n,t){for(var e,r=Xo.interpolators.length;--r>=0&&!(e=Xo.interpolators[r](n,t)););return e}function hu(n,t){var e,r=[],u=[],i=n.length,o=t.length,a=Math.min(n.length,t.length);for(e=0;a>e;++e)r.push(fu(n[e],t[e]));for(;i>e;++e)u[e]=n[e];for(;o>e;++e)u[e]=t[e];return function(n){for(e=0;a>e;++e)u[e]=r[e](n);return u}}function gu(n){return function(t){return 0>=t?0:t>=1?1:n(t)}}function pu(n){return function(t){return 1-n(1-t)}}function vu(n){return function(t){return.5*(.5>t?n(2*t):2-n(2-2*t))}}function du(n){return n*n}function mu(n){return n*n*n}function yu(n){if(0>=n)return 0;if(n>=1)return 1;var t=n*n,e=t*n;return 4*(.5>n?e:3*(n-t)+e-.75)}function xu(n){return function(t){return Math.pow(t,n)}}function Mu(n){return 1-Math.cos(n*Ea)}function _u(n){return Math.pow(2,10*(n-1))}function bu(n){return 1-Math.sqrt(1-n*n)}function wu(n,t){var e;return arguments.length<2&&(t=.45),arguments.length?e=t/ka*Math.asin(1/n):(n=1,e=t/4),function(r){return 1+n*Math.pow(2,-10*r)*Math.sin((r-e)*ka/t)}}function Su(n){return n||(n=1.70158),function(t){return t*t*((n+1)*t-n)}}function ku(n){return 1/2.75>n?7.5625*n*n:2/2.75>n?7.5625*(n-=1.5/2.75)*n+.75:2.5/2.75>n?7.5625*(n-=2.25/2.75)*n+.9375:7.5625*(n-=2.625/2.75)*n+.984375}function Eu(n,t){n=Xo.hcl(n),t=Xo.hcl(t);var e=n.h,r=n.c,u=n.l,i=t.h-e,o=t.c-r,a=t.l-u;return isNaN(o)&&(o=0,r=isNaN(r)?t.c:r),isNaN(i)?(i=0,e=isNaN(e)?t.h:e):i>180?i-=360:-180>i&&(i+=360),function(n){return rt(e+i*n,r+o*n,u+a*n)+""}}function Au(n,t){n=Xo.hsl(n),t=Xo.hsl(t);var e=n.h,r=n.s,u=n.l,i=t.h-e,o=t.s-r,a=t.l-u;return isNaN(o)&&(o=0,r=isNaN(r)?t.s:r),isNaN(i)?(i=0,e=isNaN(e)?t.h:e):i>180?i-=360:-180>i&&(i+=360),function(n){return nt(e+i*n,r+o*n,u+a*n)+""}}function Cu(n,t){n=Xo.lab(n),t=Xo.lab(t);var e=n.l,r=n.a,u=n.b,i=t.l-e,o=t.a-r,a=t.b-u;return function(n){return ot(e+i*n,r+o*n,u+a*n)+""}}function Nu(n,t){return t-=n,function(e){return Math.round(n+t*e)}}function Lu(n){var t=[n.a,n.b],e=[n.c,n.d],r=qu(t),u=zu(t,e),i=qu(Tu(e,t,-u))||0;t[0]*e[1]<e[0]*t[1]&&(t[0]*=-1,t[1]*=-1,r*=-1,u*=-1),this.rotate=(r?Math.atan2(t[1],t[0]):Math.atan2(-e[0],e[1]))*La,this.translate=[n.e,n.f],this.scale=[r,i],this.skew=i?Math.atan2(u,i)*La:0}function zu(n,t){return n[0]*t[0]+n[1]*t[1]}function qu(n){var t=Math.sqrt(zu(n,n));return t&&(n[0]/=t,n[1]/=t),t}function Tu(n,t,e){return n[0]+=e*t[0],n[1]+=e*t[1],n}function Ru(n,t){var e,r=[],u=[],i=Xo.transform(n),o=Xo.transform(t),a=i.translate,c=o.translate,s=i.rotate,l=o.rotate,f=i.skew,h=o.skew,g=i.scale,p=o.scale;return a[0]!=c[0]||a[1]!=c[1]?(r.push("translate(",null,",",null,")"),u.push({i:1,x:su(a[0],c[0])},{i:3,x:su(a[1],c[1])})):c[0]||c[1]?r.push("translate("+c+")"):r.push(""),s!=l?(s-l>180?l+=360:l-s>180&&(s+=360),u.push({i:r.push(r.pop()+"rotate(",null,")")-2,x:su(s,l)})):l&&r.push(r.pop()+"rotate("+l+")"),f!=h?u.push({i:r.push(r.pop()+"skewX(",null,")")-2,x:su(f,h)}):h&&r.push(r.pop()+"skewX("+h+")"),g[0]!=p[0]||g[1]!=p[1]?(e=r.push(r.pop()+"scale(",null,",",null,")"),u.push({i:e-4,x:su(g[0],p[0])},{i:e-2,x:su(g[1],p[1])})):(1!=p[0]||1!=p[1])&&r.push(r.pop()+"scale("+p+")"),e=u.length,function(n){for(var t,i=-1;++i<e;)r[(t=u[i]).i]=t.x(n);return r.join("")}}function Du(n,t){return t=t-(n=+n)?1/(t-n):0,function(e){return(e-n)*t}}function Pu(n,t){return t=t-(n=+n)?1/(t-n):0,function(e){return Math.max(0,Math.min(1,(e-n)*t))}}function Uu(n){for(var t=n.source,e=n.target,r=Hu(t,e),u=[t];t!==r;)t=t.parent,u.push(t);for(var i=u.length;e!==r;)u.splice(i,0,e),e=e.parent;return u}function ju(n){for(var t=[],e=n.parent;null!=e;)t.push(n),n=e,e=e.parent;return t.push(n),t}function Hu(n,t){if(n===t)return n;for(var e=ju(n),r=ju(t),u=e.pop(),i=r.pop(),o=null;u===i;)o=u,u=e.pop(),i=r.pop();return o}function Fu(n){n.fixed|=2}function Ou(n){n.fixed&=-7}function Yu(n){n.fixed|=4,n.px=n.x,n.py=n.y}function Iu(n){n.fixed&=-5}function Zu(n,t,e){var r=0,u=0;if(n.charge=0,!n.leaf)for(var i,o=n.nodes,a=o.length,c=-1;++c<a;)i=o[c],null!=i&&(Zu(i,t,e),n.charge+=i.charge,r+=i.charge*i.cx,u+=i.charge*i.cy);if(n.point){n.leaf||(n.point.x+=Math.random()-.5,n.point.y+=Math.random()-.5);var s=t*e[n.point.index];n.charge+=n.pointCharge=s,r+=s*n.point.x,u+=s*n.point.y}n.cx=r/n.charge,n.cy=u/n.charge}function Vu(n,t){return Xo.rebind(n,t,"sort","children","value"),n.nodes=n,n.links=Wu,n}function Xu(n){return n.children}function $u(n){return n.value}function Bu(n,t){return t.value-n.value}function Wu(n){return Xo.merge(n.map(function(n){return(n.children||[]).map(function(t){return{source:n,target:t}})}))}function Ju(n){return n.x}function Gu(n){return n.y}function Ku(n,t,e){n.y0=t,n.y=e}function Qu(n){return Xo.range(n.length)}function ni(n){for(var t=-1,e=n[0].length,r=[];++t<e;)r[t]=0;return r}function ti(n){for(var t,e=1,r=0,u=n[0][1],i=n.length;i>e;++e)(t=n[e][1])>u&&(r=e,u=t);return r}function ei(n){return n.reduce(ri,0)}function ri(n,t){return n+t[1]}function ui(n,t){return ii(n,Math.ceil(Math.log(t.length)/Math.LN2+1))}function ii(n,t){for(var e=-1,r=+n[0],u=(n[1]-r)/t,i=[];++e<=t;)i[e]=u*e+r;return i}function oi(n){return[Xo.min(n),Xo.max(n)]}function ai(n,t){return n.parent==t.parent?1:2}function ci(n){var t=n.children;return t&&t.length?t[0]:n._tree.thread}function si(n){var t,e=n.children;return e&&(t=e.length)?e[t-1]:n._tree.thread}function li(n,t){var e=n.children;if(e&&(u=e.length))for(var r,u,i=-1;++i<u;)t(r=li(e[i],t),n)>0&&(n=r);return n}function fi(n,t){return n.x-t.x}function hi(n,t){return t.x-n.x}function gi(n,t){return n.depth-t.depth}function pi(n,t){function e(n,r){var u=n.children;if(u&&(o=u.length))for(var i,o,a=null,c=-1;++c<o;)i=u[c],e(i,a),a=i;t(n,r)}e(n,null)}function vi(n){for(var t,e=0,r=0,u=n.children,i=u.length;--i>=0;)t=u[i]._tree,t.prelim+=e,t.mod+=e,e+=t.shift+(r+=t.change)}function di(n,t,e){n=n._tree,t=t._tree;var r=e/(t.number-n.number);n.change+=r,t.change-=r,t.shift+=e,t.prelim+=e,t.mod+=e}function mi(n,t,e){return n._tree.ancestor.parent==t.parent?n._tree.ancestor:e}function yi(n,t){return n.value-t.value}function xi(n,t){var e=n._pack_next;n._pack_next=t,t._pack_prev=n,t._pack_next=e,e._pack_prev=t}function Mi(n,t){n._pack_next=t,t._pack_prev=n}function _i(n,t){var e=t.x-n.x,r=t.y-n.y,u=n.r+t.r;return.999*u*u>e*e+r*r}function bi(n){function t(n){l=Math.min(n.x-n.r,l),f=Math.max(n.x+n.r,f),h=Math.min(n.y-n.r,h),g=Math.max(n.y+n.r,g)}if((e=n.children)&&(s=e.length)){var e,r,u,i,o,a,c,s,l=1/0,f=-1/0,h=1/0,g=-1/0;if(e.forEach(wi),r=e[0],r.x=-r.r,r.y=0,t(r),s>1&&(u=e[1],u.x=u.r,u.y=0,t(u),s>2))for(i=e[2],Ei(r,u,i),t(i),xi(r,i),r._pack_prev=i,xi(i,u),u=r._pack_next,o=3;s>o;o++){Ei(r,u,i=e[o]);var p=0,v=1,d=1;for(a=u._pack_next;a!==u;a=a._pack_next,v++)if(_i(a,i)){p=1;break}if(1==p)for(c=r._pack_prev;c!==a._pack_prev&&!_i(c,i);c=c._pack_prev,d++);p?(d>v||v==d&&u.r<r.r?Mi(r,u=a):Mi(r=c,u),o--):(xi(r,i),u=i,t(i))}var m=(l+f)/2,y=(h+g)/2,x=0;for(o=0;s>o;o++)i=e[o],i.x-=m,i.y-=y,x=Math.max(x,i.r+Math.sqrt(i.x*i.x+i.y*i.y));n.r=x,e.forEach(Si)}}function wi(n){n._pack_next=n._pack_prev=n}function Si(n){delete n._pack_next,delete n._pack_prev}function ki(n,t,e,r){var u=n.children;if(n.x=t+=r*n.x,n.y=e+=r*n.y,n.r*=r,u)for(var i=-1,o=u.length;++i<o;)ki(u[i],t,e,r)}function Ei(n,t,e){var r=n.r+e.r,u=t.x-n.x,i=t.y-n.y;if(r&&(u||i)){var o=t.r+e.r,a=u*u+i*i;o*=o,r*=r;var c=.5+(r-o)/(2*a),s=Math.sqrt(Math.max(0,2*o*(r+a)-(r-=a)*r-o*o))/(2*a);e.x=n.x+c*u+s*i,e.y=n.y+c*i-s*u}else e.x=n.x+r,e.y=n.y}function Ai(n){return 1+Xo.max(n,function(n){return n.y})}function Ci(n){return n.reduce(function(n,t){return n+t.x},0)/n.length}function Ni(n){var t=n.children;return t&&t.length?Ni(t[0]):n}function Li(n){var t,e=n.children;return e&&(t=e.length)?Li(e[t-1]):n}function zi(n){return{x:n.x,y:n.y,dx:n.dx,dy:n.dy}}function qi(n,t){var e=n.x+t[3],r=n.y+t[0],u=n.dx-t[1]-t[3],i=n.dy-t[0]-t[2];return 0>u&&(e+=u/2,u=0),0>i&&(r+=i/2,i=0),{x:e,y:r,dx:u,dy:i}}function Ti(n){var t=n[0],e=n[n.length-1];return e>t?[t,e]:[e,t]}function Ri(n){return n.rangeExtent?n.rangeExtent():Ti(n.range())}function Di(n,t,e,r){var u=e(n[0],n[1]),i=r(t[0],t[1]);return function(n){return i(u(n))}}function Pi(n,t){var e,r=0,u=n.length-1,i=n[r],o=n[u];return i>o&&(e=r,r=u,u=e,e=i,i=o,o=e),n[r]=t.floor(i),n[u]=t.ceil(o),n}function Ui(n){return n?{floor:function(t){return Math.floor(t/n)*n},ceil:function(t){return Math.ceil(t/n)*n}}:ls}function ji(n,t,e,r){var u=[],i=[],o=0,a=Math.min(n.length,t.length)-1;for(n[a]<n[0]&&(n=n.slice().reverse(),t=t.slice().reverse());++o<=a;)u.push(e(n[o-1],n[o])),i.push(r(t[o-1],t[o]));return function(t){var e=Xo.bisect(n,t,1,a)-1;return i[e](u[e](t))}}function Hi(n,t,e,r){function u(){var u=Math.min(n.length,t.length)>2?ji:Di,c=r?Pu:Du;return o=u(n,t,c,e),a=u(t,n,c,fu),i}function i(n){return o(n)}var o,a;return i.invert=function(n){return a(n)},i.domain=function(t){return arguments.length?(n=t.map(Number),u()):n},i.range=function(n){return arguments.length?(t=n,u()):t},i.rangeRound=function(n){return i.range(n).interpolate(Nu)},i.clamp=function(n){return arguments.length?(r=n,u()):r},i.interpolate=function(n){return arguments.length?(e=n,u()):e},i.ticks=function(t){return Ii(n,t)},i.tickFormat=function(t,e){return Zi(n,t,e)},i.nice=function(t){return Oi(n,t),u()},i.copy=function(){return Hi(n,t,e,r)},u()}function Fi(n,t){return Xo.rebind(n,t,"range","rangeRound","interpolate","clamp")}function Oi(n,t){return Pi(n,Ui(Yi(n,t)[2]))}function Yi(n,t){null==t&&(t=10);var e=Ti(n),r=e[1]-e[0],u=Math.pow(10,Math.floor(Math.log(r/t)/Math.LN10)),i=t/r*u;return.15>=i?u*=10:.35>=i?u*=5:.75>=i&&(u*=2),e[0]=Math.ceil(e[0]/u)*u,e[1]=Math.floor(e[1]/u)*u+.5*u,e[2]=u,e}function Ii(n,t){return Xo.range.apply(Xo,Yi(n,t))}function Zi(n,t,e){var r=Yi(n,t);return Xo.format(e?e.replace(Qa,function(n,t,e,u,i,o,a,c,s,l){return[t,e,u,i,o,a,c,s||"."+Xi(l,r),l].join("")}):",."+Vi(r[2])+"f")}function Vi(n){return-Math.floor(Math.log(n)/Math.LN10+.01)}function Xi(n,t){var e=Vi(t[2]);return n in fs?Math.abs(e-Vi(Math.max(Math.abs(t[0]),Math.abs(t[1]))))+ +("e"!==n):e-2*("%"===n)}function $i(n,t,e,r){function u(n){return(e?Math.log(0>n?0:n):-Math.log(n>0?0:-n))/Math.log(t)}function i(n){return e?Math.pow(t,n):-Math.pow(t,-n)}function o(t){return n(u(t))}return o.invert=function(t){return i(n.invert(t))},o.domain=function(t){return arguments.length?(e=t[0]>=0,n.domain((r=t.map(Number)).map(u)),o):r},o.base=function(e){return arguments.length?(t=+e,n.domain(r.map(u)),o):t},o.nice=function(){var t=Pi(r.map(u),e?Math:gs);return n.domain(t),r=t.map(i),o},o.ticks=function(){var n=Ti(r),o=[],a=n[0],c=n[1],s=Math.floor(u(a)),l=Math.ceil(u(c)),f=t%1?2:t;if(isFinite(l-s)){if(e){for(;l>s;s++)for(var h=1;f>h;h++)o.push(i(s)*h);o.push(i(s))}else for(o.push(i(s));s++<l;)for(var h=f-1;h>0;h--)o.push(i(s)*h);for(s=0;o[s]<a;s++);for(l=o.length;o[l-1]>c;l--);o=o.slice(s,l)}return o},o.tickFormat=function(n,t){if(!arguments.length)return hs;arguments.length<2?t=hs:"function"!=typeof t&&(t=Xo.format(t));var r,a=Math.max(.1,n/o.ticks().length),c=e?(r=1e-12,Math.ceil):(r=-1e-12,Math.floor);return function(n){return n/i(c(u(n)+r))<=a?t(n):""}},o.copy=function(){return $i(n.copy(),t,e,r)},Fi(o,n)}function Bi(n,t,e){function r(t){return n(u(t))}var u=Wi(t),i=Wi(1/t);return r.invert=function(t){return i(n.invert(t))},r.domain=function(t){return arguments.length?(n.domain((e=t.map(Number)).map(u)),r):e},r.ticks=function(n){return Ii(e,n)},r.tickFormat=function(n,t){return Zi(e,n,t)},r.nice=function(n){return r.domain(Oi(e,n))},r.exponent=function(o){return arguments.length?(u=Wi(t=o),i=Wi(1/t),n.domain(e.map(u)),r):t},r.copy=function(){return Bi(n.copy(),t,e)},Fi(r,n)}function Wi(n){return function(t){return 0>t?-Math.pow(-t,n):Math.pow(t,n)}}function Ji(n,t){function e(e){return o[((i.get(e)||"range"===t.t&&i.set(e,n.push(e)))-1)%o.length]}function r(t,e){return Xo.range(n.length).map(function(n){return t+e*n})}var i,o,a;return e.domain=function(r){if(!arguments.length)return n;n=[],i=new u;for(var o,a=-1,c=r.length;++a<c;)i.has(o=r[a])||i.set(o,n.push(o));return e[t.t].apply(e,t.a)},e.range=function(n){return arguments.length?(o=n,a=0,t={t:"range",a:arguments},e):o},e.rangePoints=function(u,i){arguments.length<2&&(i=0);var c=u[0],s=u[1],l=(s-c)/(Math.max(1,n.length-1)+i);return o=r(n.length<2?(c+s)/2:c+l*i/2,l),a=0,t={t:"rangePoints",a:arguments},e},e.rangeBands=function(u,i,c){arguments.length<2&&(i=0),arguments.length<3&&(c=i);var s=u[1]<u[0],l=u[s-0],f=u[1-s],h=(f-l)/(n.length-i+2*c);return o=r(l+h*c,h),s&&o.reverse(),a=h*(1-i),t={t:"rangeBands",a:arguments},e},e.rangeRoundBands=function(u,i,c){arguments.length<2&&(i=0),arguments.length<3&&(c=i);var s=u[1]<u[0],l=u[s-0],f=u[1-s],h=Math.floor((f-l)/(n.length-i+2*c)),g=f-l-(n.length-i)*h;return o=r(l+Math.round(g/2),h),s&&o.reverse(),a=Math.round(h*(1-i)),t={t:"rangeRoundBands",a:arguments},e},e.rangeBand=function(){return a},e.rangeExtent=function(){return Ti(t.a[0])},e.copy=function(){return Ji(n,t)},e.domain(n)}function Gi(n,t){function e(){var e=0,i=t.length;for(u=[];++e<i;)u[e-1]=Xo.quantile(n,e/i);return r}function r(n){return isNaN(n=+n)?void 0:t[Xo.bisect(u,n)]}var u;return r.domain=function(t){return arguments.length?(n=t.filter(function(n){return!isNaN(n)}).sort(Xo.ascending),e()):n},r.range=function(n){return arguments.length?(t=n,e()):t},r.quantiles=function(){return u},r.invertExtent=function(e){return e=t.indexOf(e),0>e?[0/0,0/0]:[e>0?u[e-1]:n[0],e<u.length?u[e]:n[n.length-1]]},r.copy=function(){return Gi(n,t)},e()}function Ki(n,t,e){function r(t){return e[Math.max(0,Math.min(o,Math.floor(i*(t-n))))]}function u(){return i=e.length/(t-n),o=e.length-1,r}var i,o;return r.domain=function(e){return arguments.length?(n=+e[0],t=+e[e.length-1],u()):[n,t]},r.range=function(n){return arguments.length?(e=n,u()):e},r.invertExtent=function(t){return t=e.indexOf(t),t=0>t?0/0:t/i+n,[t,t+1/i]},r.copy=function(){return Ki(n,t,e)},u()}function Qi(n,t){function e(e){return e>=e?t[Xo.bisect(n,e)]:void 0}return e.domain=function(t){return arguments.length?(n=t,e):n},e.range=function(n){return arguments.length?(t=n,e):t},e.invertExtent=function(e){return e=t.indexOf(e),[n[e-1],n[e]]},e.copy=function(){return Qi(n,t)},e}function no(n){function t(n){return+n}return t.invert=t,t.domain=t.range=function(e){return arguments.length?(n=e.map(t),t):n},t.ticks=function(t){return Ii(n,t)},t.tickFormat=function(t,e){return Zi(n,t,e)},t.copy=function(){return no(n)},t}function to(n){return n.innerRadius}function eo(n){return n.outerRadius}function ro(n){return n.startAngle}function uo(n){return n.endAngle}function io(n){function t(t){function o(){s.push("M",i(n(l),a))}for(var c,s=[],l=[],f=-1,h=t.length,g=_t(e),p=_t(r);++f<h;)u.call(this,c=t[f],f)?l.push([+g.call(this,c,f),+p.call(this,c,f)]):l.length&&(o(),l=[]);return l.length&&o(),s.length?s.join(""):null}var e=br,r=wr,u=be,i=oo,o=i.key,a=.7;return t.x=function(n){return arguments.length?(e=n,t):e},t.y=function(n){return arguments.length?(r=n,t):r},t.defined=function(n){return arguments.length?(u=n,t):u},t.interpolate=function(n){return arguments.length?(o="function"==typeof n?i=n:(i=Ms.get(n)||oo).key,t):o},t.tension=function(n){return arguments.length?(a=n,t):a},t}function oo(n){return n.join("L")}function ao(n){return oo(n)+"Z"}function co(n){for(var t=0,e=n.length,r=n[0],u=[r[0],",",r[1]];++t<e;)u.push("H",(r[0]+(r=n[t])[0])/2,"V",r[1]);return e>1&&u.push("H",r[0]),u.join("")}function so(n){for(var t=0,e=n.length,r=n[0],u=[r[0],",",r[1]];++t<e;)u.push("V",(r=n[t])[1],"H",r[0]);return u.join("")}function lo(n){for(var t=0,e=n.length,r=n[0],u=[r[0],",",r[1]];++t<e;)u.push("H",(r=n[t])[0],"V",r[1]);return u.join("")}function fo(n,t){return n.length<4?oo(n):n[1]+po(n.slice(1,n.length-1),vo(n,t))}function ho(n,t){return n.length<3?oo(n):n[0]+po((n.push(n[0]),n),vo([n[n.length-2]].concat(n,[n[1]]),t))}function go(n,t){return n.length<3?oo(n):n[0]+po(n,vo(n,t))}function po(n,t){if(t.length<1||n.length!=t.length&&n.length!=t.length+2)return oo(n);var e=n.length!=t.length,r="",u=n[0],i=n[1],o=t[0],a=o,c=1;if(e&&(r+="Q"+(i[0]-2*o[0]/3)+","+(i[1]-2*o[1]/3)+","+i[0]+","+i[1],u=n[1],c=2),t.length>1){a=t[1],i=n[c],c++,r+="C"+(u[0]+o[0])+","+(u[1]+o[1])+","+(i[0]-a[0])+","+(i[1]-a[1])+","+i[0]+","+i[1];for(var s=2;s<t.length;s++,c++)i=n[c],a=t[s],r+="S"+(i[0]-a[0])+","+(i[1]-a[1])+","+i[0]+","+i[1]}if(e){var l=n[c];r+="Q"+(i[0]+2*a[0]/3)+","+(i[1]+2*a[1]/3)+","+l[0]+","+l[1]}return r}function vo(n,t){for(var e,r=[],u=(1-t)/2,i=n[0],o=n[1],a=1,c=n.length;++a<c;)e=i,i=o,o=n[a],r.push([u*(o[0]-e[0]),u*(o[1]-e[1])]);return r}function mo(n){if(n.length<3)return oo(n);var t=1,e=n.length,r=n[0],u=r[0],i=r[1],o=[u,u,u,(r=n[1])[0]],a=[i,i,i,r[1]],c=[u,",",i,"L",_o(ws,o),",",_o(ws,a)];for(n.push(n[e-1]);++t<=e;)r=n[t],o.shift(),o.push(r[0]),a.shift(),a.push(r[1]),bo(c,o,a);return n.pop(),c.push("L",r),c.join("")}function yo(n){if(n.length<4)return oo(n);for(var t,e=[],r=-1,u=n.length,i=[0],o=[0];++r<3;)t=n[r],i.push(t[0]),o.push(t[1]);for(e.push(_o(ws,i)+","+_o(ws,o)),--r;++r<u;)t=n[r],i.shift(),i.push(t[0]),o.shift(),o.push(t[1]),bo(e,i,o);return e.join("")}function xo(n){for(var t,e,r=-1,u=n.length,i=u+4,o=[],a=[];++r<4;)e=n[r%u],o.push(e[0]),a.push(e[1]);for(t=[_o(ws,o),",",_o(ws,a)],--r;++r<i;)e=n[r%u],o.shift(),o.push(e[0]),a.shift(),a.push(e[1]),bo(t,o,a);return t.join("")}function Mo(n,t){var e=n.length-1;if(e)for(var r,u,i=n[0][0],o=n[0][1],a=n[e][0]-i,c=n[e][1]-o,s=-1;++s<=e;)r=n[s],u=s/e,r[0]=t*r[0]+(1-t)*(i+u*a),r[1]=t*r[1]+(1-t)*(o+u*c);return mo(n)}function _o(n,t){return n[0]*t[0]+n[1]*t[1]+n[2]*t[2]+n[3]*t[3]}function bo(n,t,e){n.push("C",_o(_s,t),",",_o(_s,e),",",_o(bs,t),",",_o(bs,e),",",_o(ws,t),",",_o(ws,e))}function wo(n,t){return(t[1]-n[1])/(t[0]-n[0])}function So(n){for(var t=0,e=n.length-1,r=[],u=n[0],i=n[1],o=r[0]=wo(u,i);++t<e;)r[t]=(o+(o=wo(u=i,i=n[t+1])))/2;return r[t]=o,r}function ko(n){for(var t,e,r,u,i=[],o=So(n),a=-1,c=n.length-1;++a<c;)t=wo(n[a],n[a+1]),oa(t)<Aa?o[a]=o[a+1]=0:(e=o[a]/t,r=o[a+1]/t,u=e*e+r*r,u>9&&(u=3*t/Math.sqrt(u),o[a]=u*e,o[a+1]=u*r));for(a=-1;++a<=c;)u=(n[Math.min(c,a+1)][0]-n[Math.max(0,a-1)][0])/(6*(1+o[a]*o[a])),i.push([u||0,o[a]*u||0]);return i}function Eo(n){return n.length<3?oo(n):n[0]+po(n,ko(n))}function Ao(n){for(var t,e,r,u=-1,i=n.length;++u<i;)t=n[u],e=t[0],r=t[1]+ys,t[0]=e*Math.cos(r),t[1]=e*Math.sin(r);return n}function Co(n){function t(t){function c(){v.push("M",a(n(m),f),l,s(n(d.reverse()),f),"Z")}for(var h,g,p,v=[],d=[],m=[],y=-1,x=t.length,M=_t(e),_=_t(u),b=e===r?function(){return g}:_t(r),w=u===i?function(){return p}:_t(i);++y<x;)o.call(this,h=t[y],y)?(d.push([g=+M.call(this,h,y),p=+_.call(this,h,y)]),m.push([+b.call(this,h,y),+w.call(this,h,y)])):d.length&&(c(),d=[],m=[]);return d.length&&c(),v.length?v.join(""):null}var e=br,r=br,u=0,i=wr,o=be,a=oo,c=a.key,s=a,l="L",f=.7;return t.x=function(n){return arguments.length?(e=r=n,t):r},t.x0=function(n){return arguments.length?(e=n,t):e},t.x1=function(n){return arguments.length?(r=n,t):r},t.y=function(n){return arguments.length?(u=i=n,t):i},t.y0=function(n){return arguments.length?(u=n,t):u},t.y1=function(n){return arguments.length?(i=n,t):i},t.defined=function(n){return arguments.length?(o=n,t):o},t.interpolate=function(n){return arguments.length?(c="function"==typeof n?a=n:(a=Ms.get(n)||oo).key,s=a.reverse||a,l=a.closed?"M":"L",t):c},t.tension=function(n){return arguments.length?(f=n,t):f},t}function No(n){return n.radius}function Lo(n){return[n.x,n.y]}function zo(n){return function(){var t=n.apply(this,arguments),e=t[0],r=t[1]+ys;return[e*Math.cos(r),e*Math.sin(r)]}}function qo(){return 64}function To(){return"circle"}function Ro(n){var t=Math.sqrt(n/Sa);return"M0,"+t+"A"+t+","+t+" 0 1,1 0,"+-t+"A"+t+","+t+" 0 1,1 0,"+t+"Z"}function Do(n,t){return fa(n,Ns),n.id=t,n}function Po(n,t,e,r){var u=n.id;return R(n,"function"==typeof e?function(n,i,o){n.__transition__[u].tween.set(t,r(e.call(n,n.__data__,i,o)))}:(e=r(e),function(n){n.__transition__[u].tween.set(t,e)}))}function Uo(n){return null==n&&(n=""),function(){this.textContent=n}}function jo(n,t,e,r){var i=n.__transition__||(n.__transition__={active:0,count:0}),o=i[e];if(!o){var a=r.time;o=i[e]={tween:new u,time:a,ease:r.ease,delay:r.delay,duration:r.duration},++i.count,Xo.timer(function(r){function u(r){return i.active>e?s():(i.active=e,o.event&&o.event.start.call(n,l,t),o.tween.forEach(function(e,r){(r=r.call(n,l,t))&&v.push(r)}),Xo.timer(function(){return p.c=c(r||1)?be:c,1},0,a),void 0)}function c(r){if(i.active!==e)return s();for(var u=r/g,a=f(u),c=v.length;c>0;)v[--c].call(n,a);return u>=1?(o.event&&o.event.end.call(n,l,t),s()):void 0}function s(){return--i.count?delete i[e]:delete n.__transition__,1}var l=n.__data__,f=o.ease,h=o.delay,g=o.duration,p=Ja,v=[];return p.t=h+a,r>=h?u(r-h):(p.c=u,void 0)},0,a)}}function Ho(n,t){n.attr("transform",function(n){return"translate("+t(n)+",0)"})}function Fo(n,t){n.attr("transform",function(n){return"translate(0,"+t(n)+")"})}function Oo(n){return n.toISOString()}function Yo(n,t,e){function r(t){return n(t)}function u(n,e){var r=n[1]-n[0],u=r/e,i=Xo.bisect(js,u);return i==js.length?[t.year,Yi(n.map(function(n){return n/31536e6}),e)[2]]:i?t[u/js[i-1]<js[i]/u?i-1:i]:[Os,Yi(n,e)[2]]
-}return r.invert=function(t){return Io(n.invert(t))},r.domain=function(t){return arguments.length?(n.domain(t),r):n.domain().map(Io)},r.nice=function(n,t){function e(e){return!isNaN(e)&&!n.range(e,Io(+e+1),t).length}var i=r.domain(),o=Ti(i),a=null==n?u(o,10):"number"==typeof n&&u(o,n);return a&&(n=a[0],t=a[1]),r.domain(Pi(i,t>1?{floor:function(t){for(;e(t=n.floor(t));)t=Io(t-1);return t},ceil:function(t){for(;e(t=n.ceil(t));)t=Io(+t+1);return t}}:n))},r.ticks=function(n,t){var e=Ti(r.domain()),i=null==n?u(e,10):"number"==typeof n?u(e,n):!n.range&&[{range:n},t];return i&&(n=i[0],t=i[1]),n.range(e[0],Io(+e[1]+1),1>t?1:t)},r.tickFormat=function(){return e},r.copy=function(){return Yo(n.copy(),t,e)},Fi(r,n)}function Io(n){return new Date(n)}function Zo(n){return JSON.parse(n.responseText)}function Vo(n){var t=Wo.createRange();return t.selectNode(Wo.body),t.createContextualFragment(n.responseText)}var Xo={version:"3.4.1"};Date.now||(Date.now=function(){return+new Date});var $o=[].slice,Bo=function(n){return $o.call(n)},Wo=document,Jo=Wo.documentElement,Go=window;try{Bo(Jo.childNodes)[0].nodeType}catch(Ko){Bo=function(n){for(var t=n.length,e=new Array(t);t--;)e[t]=n[t];return e}}try{Wo.createElement("div").style.setProperty("opacity",0,"")}catch(Qo){var na=Go.Element.prototype,ta=na.setAttribute,ea=na.setAttributeNS,ra=Go.CSSStyleDeclaration.prototype,ua=ra.setProperty;na.setAttribute=function(n,t){ta.call(this,n,t+"")},na.setAttributeNS=function(n,t,e){ea.call(this,n,t,e+"")},ra.setProperty=function(n,t,e){ua.call(this,n,t+"",e)}}Xo.ascending=function(n,t){return t>n?-1:n>t?1:n>=t?0:0/0},Xo.descending=function(n,t){return n>t?-1:t>n?1:t>=n?0:0/0},Xo.min=function(n,t){var e,r,u=-1,i=n.length;if(1===arguments.length){for(;++u<i&&!(null!=(e=n[u])&&e>=e);)e=void 0;for(;++u<i;)null!=(r=n[u])&&e>r&&(e=r)}else{for(;++u<i&&!(null!=(e=t.call(n,n[u],u))&&e>=e);)e=void 0;for(;++u<i;)null!=(r=t.call(n,n[u],u))&&e>r&&(e=r)}return e},Xo.max=function(n,t){var e,r,u=-1,i=n.length;if(1===arguments.length){for(;++u<i&&!(null!=(e=n[u])&&e>=e);)e=void 0;for(;++u<i;)null!=(r=n[u])&&r>e&&(e=r)}else{for(;++u<i&&!(null!=(e=t.call(n,n[u],u))&&e>=e);)e=void 0;for(;++u<i;)null!=(r=t.call(n,n[u],u))&&r>e&&(e=r)}return e},Xo.extent=function(n,t){var e,r,u,i=-1,o=n.length;if(1===arguments.length){for(;++i<o&&!(null!=(e=u=n[i])&&e>=e);)e=u=void 0;for(;++i<o;)null!=(r=n[i])&&(e>r&&(e=r),r>u&&(u=r))}else{for(;++i<o&&!(null!=(e=u=t.call(n,n[i],i))&&e>=e);)e=void 0;for(;++i<o;)null!=(r=t.call(n,n[i],i))&&(e>r&&(e=r),r>u&&(u=r))}return[e,u]},Xo.sum=function(n,t){var e,r=0,u=n.length,i=-1;if(1===arguments.length)for(;++i<u;)isNaN(e=+n[i])||(r+=e);else for(;++i<u;)isNaN(e=+t.call(n,n[i],i))||(r+=e);return r},Xo.mean=function(t,e){var r,u=t.length,i=0,o=-1,a=0;if(1===arguments.length)for(;++o<u;)n(r=t[o])&&(i+=(r-i)/++a);else for(;++o<u;)n(r=e.call(t,t[o],o))&&(i+=(r-i)/++a);return a?i:void 0},Xo.quantile=function(n,t){var e=(n.length-1)*t+1,r=Math.floor(e),u=+n[r-1],i=e-r;return i?u+i*(n[r]-u):u},Xo.median=function(t,e){return arguments.length>1&&(t=t.map(e)),t=t.filter(n),t.length?Xo.quantile(t.sort(Xo.ascending),.5):void 0},Xo.bisector=function(n){return{left:function(t,e,r,u){for(arguments.length<3&&(r=0),arguments.length<4&&(u=t.length);u>r;){var i=r+u>>>1;n.call(t,t[i],i)<e?r=i+1:u=i}return r},right:function(t,e,r,u){for(arguments.length<3&&(r=0),arguments.length<4&&(u=t.length);u>r;){var i=r+u>>>1;e<n.call(t,t[i],i)?u=i:r=i+1}return r}}};var ia=Xo.bisector(function(n){return n});Xo.bisectLeft=ia.left,Xo.bisect=Xo.bisectRight=ia.right,Xo.shuffle=function(n){for(var t,e,r=n.length;r;)e=0|Math.random()*r--,t=n[r],n[r]=n[e],n[e]=t;return n},Xo.permute=function(n,t){for(var e=t.length,r=new Array(e);e--;)r[e]=n[t[e]];return r},Xo.pairs=function(n){for(var t,e=0,r=n.length-1,u=n[0],i=new Array(0>r?0:r);r>e;)i[e]=[t=u,u=n[++e]];return i},Xo.zip=function(){if(!(u=arguments.length))return[];for(var n=-1,e=Xo.min(arguments,t),r=new Array(e);++n<e;)for(var u,i=-1,o=r[n]=new Array(u);++i<u;)o[i]=arguments[i][n];return r},Xo.transpose=function(n){return Xo.zip.apply(Xo,n)},Xo.keys=function(n){var t=[];for(var e in n)t.push(e);return t},Xo.values=function(n){var t=[];for(var e in n)t.push(n[e]);return t},Xo.entries=function(n){var t=[];for(var e in n)t.push({key:e,value:n[e]});return t},Xo.merge=function(n){for(var t,e,r,u=n.length,i=-1,o=0;++i<u;)o+=n[i].length;for(e=new Array(o);--u>=0;)for(r=n[u],t=r.length;--t>=0;)e[--o]=r[t];return e};var oa=Math.abs;Xo.range=function(n,t,r){if(arguments.length<3&&(r=1,arguments.length<2&&(t=n,n=0)),1/0===(t-n)/r)throw new Error("infinite range");var u,i=[],o=e(oa(r)),a=-1;if(n*=o,t*=o,r*=o,0>r)for(;(u=n+r*++a)>t;)i.push(u/o);else for(;(u=n+r*++a)<t;)i.push(u/o);return i},Xo.map=function(n){var t=new u;if(n instanceof u)n.forEach(function(n,e){t.set(n,e)});else for(var e in n)t.set(e,n[e]);return t},r(u,{has:i,get:function(n){return this[aa+n]},set:function(n,t){return this[aa+n]=t},remove:o,keys:a,values:function(){var n=[];return this.forEach(function(t,e){n.push(e)}),n},entries:function(){var n=[];return this.forEach(function(t,e){n.push({key:t,value:e})}),n},size:c,empty:s,forEach:function(n){for(var t in this)t.charCodeAt(0)===ca&&n.call(this,t.substring(1),this[t])}});var aa="\x00",ca=aa.charCodeAt(0);Xo.nest=function(){function n(t,a,c){if(c>=o.length)return r?r.call(i,a):e?a.sort(e):a;for(var s,l,f,h,g=-1,p=a.length,v=o[c++],d=new u;++g<p;)(h=d.get(s=v(l=a[g])))?h.push(l):d.set(s,[l]);return t?(l=t(),f=function(e,r){l.set(e,n(t,r,c))}):(l={},f=function(e,r){l[e]=n(t,r,c)}),d.forEach(f),l}function t(n,e){if(e>=o.length)return n;var r=[],u=a[e++];return n.forEach(function(n,u){r.push({key:n,values:t(u,e)})}),u?r.sort(function(n,t){return u(n.key,t.key)}):r}var e,r,i={},o=[],a=[];return i.map=function(t,e){return n(e,t,0)},i.entries=function(e){return t(n(Xo.map,e,0),0)},i.key=function(n){return o.push(n),i},i.sortKeys=function(n){return a[o.length-1]=n,i},i.sortValues=function(n){return e=n,i},i.rollup=function(n){return r=n,i},i},Xo.set=function(n){var t=new l;if(n)for(var e=0,r=n.length;r>e;++e)t.add(n[e]);return t},r(l,{has:i,add:function(n){return this[aa+n]=!0,n},remove:function(n){return n=aa+n,n in this&&delete this[n]},values:a,size:c,empty:s,forEach:function(n){for(var t in this)t.charCodeAt(0)===ca&&n.call(this,t.substring(1))}}),Xo.behavior={},Xo.rebind=function(n,t){for(var e,r=1,u=arguments.length;++r<u;)n[e=arguments[r]]=f(n,t,t[e]);return n};var sa=["webkit","ms","moz","Moz","o","O"];Xo.dispatch=function(){for(var n=new p,t=-1,e=arguments.length;++t<e;)n[arguments[t]]=v(n);return n},p.prototype.on=function(n,t){var e=n.indexOf("."),r="";if(e>=0&&(r=n.substring(e+1),n=n.substring(0,e)),n)return arguments.length<2?this[n].on(r):this[n].on(r,t);if(2===arguments.length){if(null==t)for(n in this)this.hasOwnProperty(n)&&this[n].on(r,null);return this}},Xo.event=null,Xo.requote=function(n){return n.replace(la,"\\$&")};var la=/[\\\^\$\*\+\?\|\[\]\(\)\.\{\}]/g,fa={}.__proto__?function(n,t){n.__proto__=t}:function(n,t){for(var e in t)n[e]=t[e]},ha=function(n,t){return t.querySelector(n)},ga=function(n,t){return t.querySelectorAll(n)},pa=Jo[h(Jo,"matchesSelector")],va=function(n,t){return pa.call(n,t)};"function"==typeof Sizzle&&(ha=function(n,t){return Sizzle(n,t)[0]||null},ga=function(n,t){return Sizzle.uniqueSort(Sizzle(n,t))},va=Sizzle.matchesSelector),Xo.selection=function(){return xa};var da=Xo.selection.prototype=[];da.select=function(n){var t,e,r,u,i=[];n=M(n);for(var o=-1,a=this.length;++o<a;){i.push(t=[]),t.parentNode=(r=this[o]).parentNode;for(var c=-1,s=r.length;++c<s;)(u=r[c])?(t.push(e=n.call(u,u.__data__,c,o)),e&&"__data__"in u&&(e.__data__=u.__data__)):t.push(null)}return x(i)},da.selectAll=function(n){var t,e,r=[];n=_(n);for(var u=-1,i=this.length;++u<i;)for(var o=this[u],a=-1,c=o.length;++a<c;)(e=o[a])&&(r.push(t=Bo(n.call(e,e.__data__,a,u))),t.parentNode=e);return x(r)};var ma={svg:"http://www.w3.org/2000/svg",xhtml:"http://www.w3.org/1999/xhtml",xlink:"http://www.w3.org/1999/xlink",xml:"http://www.w3.org/XML/1998/namespace",xmlns:"http://www.w3.org/2000/xmlns/"};Xo.ns={prefix:ma,qualify:function(n){var t=n.indexOf(":"),e=n;return t>=0&&(e=n.substring(0,t),n=n.substring(t+1)),ma.hasOwnProperty(e)?{space:ma[e],local:n}:n}},da.attr=function(n,t){if(arguments.length<2){if("string"==typeof n){var e=this.node();return n=Xo.ns.qualify(n),n.local?e.getAttributeNS(n.space,n.local):e.getAttribute(n)}for(t in n)this.each(b(t,n[t]));return this}return this.each(b(n,t))},da.classed=function(n,t){if(arguments.length<2){if("string"==typeof n){var e=this.node(),r=(n=k(n)).length,u=-1;if(t=e.classList){for(;++u<r;)if(!t.contains(n[u]))return!1}else for(t=e.getAttribute("class");++u<r;)if(!S(n[u]).test(t))return!1;return!0}for(t in n)this.each(E(t,n[t]));return this}return this.each(E(n,t))},da.style=function(n,t,e){var r=arguments.length;if(3>r){if("string"!=typeof n){2>r&&(t="");for(e in n)this.each(C(e,n[e],t));return this}if(2>r)return Go.getComputedStyle(this.node(),null).getPropertyValue(n);e=""}return this.each(C(n,t,e))},da.property=function(n,t){if(arguments.length<2){if("string"==typeof n)return this.node()[n];for(t in n)this.each(N(t,n[t]));return this}return this.each(N(n,t))},da.text=function(n){return arguments.length?this.each("function"==typeof n?function(){var t=n.apply(this,arguments);this.textContent=null==t?"":t}:null==n?function(){this.textContent=""}:function(){this.textContent=n}):this.node().textContent},da.html=function(n){return arguments.length?this.each("function"==typeof n?function(){var t=n.apply(this,arguments);this.innerHTML=null==t?"":t}:null==n?function(){this.innerHTML=""}:function(){this.innerHTML=n}):this.node().innerHTML},da.append=function(n){return n=L(n),this.select(function(){return this.appendChild(n.apply(this,arguments))})},da.insert=function(n,t){return n=L(n),t=M(t),this.select(function(){return this.insertBefore(n.apply(this,arguments),t.apply(this,arguments)||null)})},da.remove=function(){return this.each(function(){var n=this.parentNode;n&&n.removeChild(this)})},da.data=function(n,t){function e(n,e){var r,i,o,a=n.length,f=e.length,h=Math.min(a,f),g=new Array(f),p=new Array(f),v=new Array(a);if(t){var d,m=new u,y=new u,x=[];for(r=-1;++r<a;)d=t.call(i=n[r],i.__data__,r),m.has(d)?v[r]=i:m.set(d,i),x.push(d);for(r=-1;++r<f;)d=t.call(e,o=e[r],r),(i=m.get(d))?(g[r]=i,i.__data__=o):y.has(d)||(p[r]=z(o)),y.set(d,o),m.remove(d);for(r=-1;++r<a;)m.has(x[r])&&(v[r]=n[r])}else{for(r=-1;++r<h;)i=n[r],o=e[r],i?(i.__data__=o,g[r]=i):p[r]=z(o);for(;f>r;++r)p[r]=z(e[r]);for(;a>r;++r)v[r]=n[r]}p.update=g,p.parentNode=g.parentNode=v.parentNode=n.parentNode,c.push(p),s.push(g),l.push(v)}var r,i,o=-1,a=this.length;if(!arguments.length){for(n=new Array(a=(r=this[0]).length);++o<a;)(i=r[o])&&(n[o]=i.__data__);return n}var c=D([]),s=x([]),l=x([]);if("function"==typeof n)for(;++o<a;)e(r=this[o],n.call(r,r.parentNode.__data__,o));else for(;++o<a;)e(r=this[o],n);return s.enter=function(){return c},s.exit=function(){return l},s},da.datum=function(n){return arguments.length?this.property("__data__",n):this.property("__data__")},da.filter=function(n){var t,e,r,u=[];"function"!=typeof n&&(n=q(n));for(var i=0,o=this.length;o>i;i++){u.push(t=[]),t.parentNode=(e=this[i]).parentNode;for(var a=0,c=e.length;c>a;a++)(r=e[a])&&n.call(r,r.__data__,a,i)&&t.push(r)}return x(u)},da.order=function(){for(var n=-1,t=this.length;++n<t;)for(var e,r=this[n],u=r.length-1,i=r[u];--u>=0;)(e=r[u])&&(i&&i!==e.nextSibling&&i.parentNode.insertBefore(e,i),i=e);return this},da.sort=function(n){n=T.apply(this,arguments);for(var t=-1,e=this.length;++t<e;)this[t].sort(n);return this.order()},da.each=function(n){return R(this,function(t,e,r){n.call(t,t.__data__,e,r)})},da.call=function(n){var t=Bo(arguments);return n.apply(t[0]=this,t),this},da.empty=function(){return!this.node()},da.node=function(){for(var n=0,t=this.length;t>n;n++)for(var e=this[n],r=0,u=e.length;u>r;r++){var i=e[r];if(i)return i}return null},da.size=function(){var n=0;return this.each(function(){++n}),n};var ya=[];Xo.selection.enter=D,Xo.selection.enter.prototype=ya,ya.append=da.append,ya.empty=da.empty,ya.node=da.node,ya.call=da.call,ya.size=da.size,ya.select=function(n){for(var t,e,r,u,i,o=[],a=-1,c=this.length;++a<c;){r=(u=this[a]).update,o.push(t=[]),t.parentNode=u.parentNode;for(var s=-1,l=u.length;++s<l;)(i=u[s])?(t.push(r[s]=e=n.call(u.parentNode,i.__data__,s,a)),e.__data__=i.__data__):t.push(null)}return x(o)},ya.insert=function(n,t){return arguments.length<2&&(t=P(this)),da.insert.call(this,n,t)},da.transition=function(){for(var n,t,e=ks||++Ls,r=[],u=Es||{time:Date.now(),ease:yu,delay:0,duration:250},i=-1,o=this.length;++i<o;){r.push(n=[]);for(var a=this[i],c=-1,s=a.length;++c<s;)(t=a[c])&&jo(t,c,e,u),n.push(t)}return Do(r,e)},da.interrupt=function(){return this.each(U)},Xo.select=function(n){var t=["string"==typeof n?ha(n,Wo):n];return t.parentNode=Jo,x([t])},Xo.selectAll=function(n){var t=Bo("string"==typeof n?ga(n,Wo):n);return t.parentNode=Jo,x([t])};var xa=Xo.select(Jo);da.on=function(n,t,e){var r=arguments.length;if(3>r){if("string"!=typeof n){2>r&&(t=!1);for(e in n)this.each(j(e,n[e],t));return this}if(2>r)return(r=this.node()["__on"+n])&&r._;e=!1}return this.each(j(n,t,e))};var Ma=Xo.map({mouseenter:"mouseover",mouseleave:"mouseout"});Ma.forEach(function(n){"on"+n in Wo&&Ma.remove(n)});var _a="onselectstart"in Wo?null:h(Jo.style,"userSelect"),ba=0;Xo.mouse=function(n){return Y(n,m())};var wa=/WebKit/.test(Go.navigator.userAgent)?-1:0;Xo.touches=function(n,t){return arguments.length<2&&(t=m().touches),t?Bo(t).map(function(t){var e=Y(n,t);return e.identifier=t.identifier,e}):[]},Xo.behavior.drag=function(){function n(){this.on("mousedown.drag",o).on("touchstart.drag",a)}function t(){return Xo.event.changedTouches[0].identifier}function e(n,t){return Xo.touches(n).filter(function(n){return n.identifier===t})[0]}function r(n,t,e,r){return function(){function o(){var n=t(l,g),e=n[0]-v[0],r=n[1]-v[1];d|=e|r,v=n,f({type:"drag",x:n[0]+c[0],y:n[1]+c[1],dx:e,dy:r})}function a(){m.on(e+"."+p,null).on(r+"."+p,null),y(d&&Xo.event.target===h),f({type:"dragend"})}var c,s=this,l=s.parentNode,f=u.of(s,arguments),h=Xo.event.target,g=n(),p=null==g?"drag":"drag-"+g,v=t(l,g),d=0,m=Xo.select(Go).on(e+"."+p,o).on(r+"."+p,a),y=O();i?(c=i.apply(s,arguments),c=[c.x-v[0],c.y-v[1]]):c=[0,0],f({type:"dragstart"})}}var u=y(n,"drag","dragstart","dragend"),i=null,o=r(g,Xo.mouse,"mousemove","mouseup"),a=r(t,e,"touchmove","touchend");return n.origin=function(t){return arguments.length?(i=t,n):i},Xo.rebind(n,u,"on")};var Sa=Math.PI,ka=2*Sa,Ea=Sa/2,Aa=1e-6,Ca=Aa*Aa,Na=Sa/180,La=180/Sa,za=Math.SQRT2,qa=2,Ta=4;Xo.interpolateZoom=function(n,t){function e(n){var t=n*y;if(m){var e=B(v),o=i/(qa*h)*(e*W(za*t+v)-$(v));return[r+o*s,u+o*l,i*e/B(za*t+v)]}return[r+n*s,u+n*l,i*Math.exp(za*t)]}var r=n[0],u=n[1],i=n[2],o=t[0],a=t[1],c=t[2],s=o-r,l=a-u,f=s*s+l*l,h=Math.sqrt(f),g=(c*c-i*i+Ta*f)/(2*i*qa*h),p=(c*c-i*i-Ta*f)/(2*c*qa*h),v=Math.log(Math.sqrt(g*g+1)-g),d=Math.log(Math.sqrt(p*p+1)-p),m=d-v,y=(m||Math.log(c/i))/za;return e.duration=1e3*y,e},Xo.behavior.zoom=function(){function n(n){n.on(A,s).on(Pa+".zoom",f).on(C,h).on("dblclick.zoom",g).on(L,l)}function t(n){return[(n[0]-S.x)/S.k,(n[1]-S.y)/S.k]}function e(n){return[n[0]*S.k+S.x,n[1]*S.k+S.y]}function r(n){S.k=Math.max(E[0],Math.min(E[1],n))}function u(n,t){t=e(t),S.x+=n[0]-t[0],S.y+=n[1]-t[1]}function i(){_&&_.domain(M.range().map(function(n){return(n-S.x)/S.k}).map(M.invert)),w&&w.domain(b.range().map(function(n){return(n-S.y)/S.k}).map(b.invert))}function o(n){n({type:"zoomstart"})}function a(n){i(),n({type:"zoom",scale:S.k,translate:[S.x,S.y]})}function c(n){n({type:"zoomend"})}function s(){function n(){l=1,u(Xo.mouse(r),g),a(i)}function e(){f.on(C,Go===r?h:null).on(N,null),p(l&&Xo.event.target===s),c(i)}var r=this,i=z.of(r,arguments),s=Xo.event.target,l=0,f=Xo.select(Go).on(C,n).on(N,e),g=t(Xo.mouse(r)),p=O();U.call(r),o(i)}function l(){function n(){var n=Xo.touches(g);return h=S.k,n.forEach(function(n){n.identifier in v&&(v[n.identifier]=t(n))}),n}function e(){for(var t=Xo.event.changedTouches,e=0,i=t.length;i>e;++e)v[t[e].identifier]=null;var o=n(),c=Date.now();if(1===o.length){if(500>c-x){var s=o[0],l=v[s.identifier];r(2*S.k),u(s,l),d(),a(p)}x=c}else if(o.length>1){var s=o[0],f=o[1],h=s[0]-f[0],g=s[1]-f[1];m=h*h+g*g}}function i(){for(var n,t,e,i,o=Xo.touches(g),c=0,s=o.length;s>c;++c,i=null)if(e=o[c],i=v[e.identifier]){if(t)break;n=e,t=i}if(i){var l=(l=e[0]-n[0])*l+(l=e[1]-n[1])*l,f=m&&Math.sqrt(l/m);n=[(n[0]+e[0])/2,(n[1]+e[1])/2],t=[(t[0]+i[0])/2,(t[1]+i[1])/2],r(f*h)}x=null,u(n,t),a(p)}function f(){if(Xo.event.touches.length){for(var t=Xo.event.changedTouches,e=0,r=t.length;r>e;++e)delete v[t[e].identifier];for(var u in v)return void n()}b.on(M,null).on(_,null),w.on(A,s).on(L,l),k(),c(p)}var h,g=this,p=z.of(g,arguments),v={},m=0,y=Xo.event.changedTouches[0].identifier,M="touchmove.zoom-"+y,_="touchend.zoom-"+y,b=Xo.select(Go).on(M,i).on(_,f),w=Xo.select(g).on(A,null).on(L,e),k=O();U.call(g),e(),o(p)}function f(){var n=z.of(this,arguments);m?clearTimeout(m):(U.call(this),o(n)),m=setTimeout(function(){m=null,c(n)},50),d();var e=v||Xo.mouse(this);p||(p=t(e)),r(Math.pow(2,.002*Ra())*S.k),u(e,p),a(n)}function h(){p=null}function g(){var n=z.of(this,arguments),e=Xo.mouse(this),i=t(e),s=Math.log(S.k)/Math.LN2;o(n),r(Math.pow(2,Xo.event.shiftKey?Math.ceil(s)-1:Math.floor(s)+1)),u(e,i),a(n),c(n)}var p,v,m,x,M,_,b,w,S={x:0,y:0,k:1},k=[960,500],E=Da,A="mousedown.zoom",C="mousemove.zoom",N="mouseup.zoom",L="touchstart.zoom",z=y(n,"zoomstart","zoom","zoomend");return n.event=function(n){n.each(function(){var n=z.of(this,arguments),t=S;ks?Xo.select(this).transition().each("start.zoom",function(){S=this.__chart__||{x:0,y:0,k:1},o(n)}).tween("zoom:zoom",function(){var e=k[0],r=k[1],u=e/2,i=r/2,o=Xo.interpolateZoom([(u-S.x)/S.k,(i-S.y)/S.k,e/S.k],[(u-t.x)/t.k,(i-t.y)/t.k,e/t.k]);return function(t){var r=o(t),c=e/r[2];this.__chart__=S={x:u-r[0]*c,y:i-r[1]*c,k:c},a(n)}}).each("end.zoom",function(){c(n)}):(this.__chart__=S,o(n),a(n),c(n))})},n.translate=function(t){return arguments.length?(S={x:+t[0],y:+t[1],k:S.k},i(),n):[S.x,S.y]},n.scale=function(t){return arguments.length?(S={x:S.x,y:S.y,k:+t},i(),n):S.k},n.scaleExtent=function(t){return arguments.length?(E=null==t?Da:[+t[0],+t[1]],n):E},n.center=function(t){return arguments.length?(v=t&&[+t[0],+t[1]],n):v},n.size=function(t){return arguments.length?(k=t&&[+t[0],+t[1]],n):k},n.x=function(t){return arguments.length?(_=t,M=t.copy(),S={x:0,y:0,k:1},n):_},n.y=function(t){return arguments.length?(w=t,b=t.copy(),S={x:0,y:0,k:1},n):w},Xo.rebind(n,z,"on")};var Ra,Da=[0,1/0],Pa="onwheel"in Wo?(Ra=function(){return-Xo.event.deltaY*(Xo.event.deltaMode?120:1)},"wheel"):"onmousewheel"in Wo?(Ra=function(){return Xo.event.wheelDelta},"mousewheel"):(Ra=function(){return-Xo.event.detail},"MozMousePixelScroll");G.prototype.toString=function(){return this.rgb()+""},Xo.hsl=function(n,t,e){return 1===arguments.length?n instanceof Q?K(n.h,n.s,n.l):dt(""+n,mt,K):K(+n,+t,+e)};var Ua=Q.prototype=new G;Ua.brighter=function(n){return n=Math.pow(.7,arguments.length?n:1),K(this.h,this.s,this.l/n)},Ua.darker=function(n){return n=Math.pow(.7,arguments.length?n:1),K(this.h,this.s,n*this.l)},Ua.rgb=function(){return nt(this.h,this.s,this.l)},Xo.hcl=function(n,t,e){return 1===arguments.length?n instanceof et?tt(n.h,n.c,n.l):n instanceof it?at(n.l,n.a,n.b):at((n=yt((n=Xo.rgb(n)).r,n.g,n.b)).l,n.a,n.b):tt(+n,+t,+e)};var ja=et.prototype=new G;ja.brighter=function(n){return tt(this.h,this.c,Math.min(100,this.l+Ha*(arguments.length?n:1)))},ja.darker=function(n){return tt(this.h,this.c,Math.max(0,this.l-Ha*(arguments.length?n:1)))},ja.rgb=function(){return rt(this.h,this.c,this.l).rgb()},Xo.lab=function(n,t,e){return 1===arguments.length?n instanceof it?ut(n.l,n.a,n.b):n instanceof et?rt(n.l,n.c,n.h):yt((n=Xo.rgb(n)).r,n.g,n.b):ut(+n,+t,+e)};var Ha=18,Fa=.95047,Oa=1,Ya=1.08883,Ia=it.prototype=new G;Ia.brighter=function(n){return ut(Math.min(100,this.l+Ha*(arguments.length?n:1)),this.a,this.b)},Ia.darker=function(n){return ut(Math.max(0,this.l-Ha*(arguments.length?n:1)),this.a,this.b)},Ia.rgb=function(){return ot(this.l,this.a,this.b)},Xo.rgb=function(n,t,e){return 1===arguments.length?n instanceof pt?gt(n.r,n.g,n.b):dt(""+n,gt,nt):gt(~~n,~~t,~~e)};var Za=pt.prototype=new G;Za.brighter=function(n){n=Math.pow(.7,arguments.length?n:1);var t=this.r,e=this.g,r=this.b,u=30;return t||e||r?(t&&u>t&&(t=u),e&&u>e&&(e=u),r&&u>r&&(r=u),gt(Math.min(255,~~(t/n)),Math.min(255,~~(e/n)),Math.min(255,~~(r/n)))):gt(u,u,u)},Za.darker=function(n){return n=Math.pow(.7,arguments.length?n:1),gt(~~(n*this.r),~~(n*this.g),~~(n*this.b))},Za.hsl=function(){return mt(this.r,this.g,this.b)},Za.toString=function(){return"#"+vt(this.r)+vt(this.g)+vt(this.b)};var Va=Xo.map({aliceblue:15792383,antiquewhite:16444375,aqua:65535,aquamarine:8388564,azure:15794175,beige:16119260,bisque:16770244,black:0,blanchedalmond:16772045,blue:255,blueviolet:9055202,brown:10824234,burlywood:14596231,cadetblue:6266528,chartreuse:8388352,chocolate:13789470,coral:16744272,cornflowerblue:6591981,cornsilk:16775388,crimson:14423100,cyan:65535,darkblue:139,darkcyan:35723,darkgoldenrod:12092939,darkgray:11119017,darkgreen:25600,darkgrey:11119017,darkkhaki:12433259,darkmagenta:9109643,darkolivegreen:5597999,darkorange:16747520,darkorchid:10040012,darkred:9109504,darksalmon:15308410,darkseagreen:9419919,darkslateblue:4734347,darkslategray:3100495,darkslategrey:3100495,darkturquoise:52945,darkviolet:9699539,deeppink:16716947,deepskyblue:49151,dimgray:6908265,dimgrey:6908265,dodgerblue:2003199,firebrick:11674146,floralwhite:16775920,forestgreen:2263842,fuchsia:16711935,gainsboro:14474460,ghostwhite:16316671,gold:16766720,goldenrod:14329120,gray:8421504,green:32768,greenyellow:11403055,grey:8421504,honeydew:15794160,hotpink:16738740,indianred:13458524,indigo:4915330,ivory:16777200,khaki:15787660,lavender:15132410,lavenderblush:16773365,lawngreen:8190976,lemonchiffon:16775885,lightblue:11393254,lightcoral:15761536,lightcyan:14745599,lightgoldenrodyellow:16448210,lightgray:13882323,lightgreen:9498256,lightgrey:13882323,lightpink:16758465,lightsalmon:16752762,lightseagreen:2142890,lightskyblue:8900346,lightslategray:7833753,lightslategrey:7833753,lightsteelblue:11584734,lightyellow:16777184,lime:65280,limegreen:3329330,linen:16445670,magenta:16711935,maroon:8388608,mediumaquamarine:6737322,mediumblue:205,mediumorchid:12211667,mediumpurple:9662683,mediumseagreen:3978097,mediumslateblue:8087790,mediumspringgreen:64154,mediumturquoise:4772300,mediumvioletred:13047173,midnightblue:1644912,mintcream:16121850,mistyrose:16770273,moccasin:16770229,navajowhite:16768685,navy:128,oldlace:16643558,olive:8421376,olivedrab:7048739,orange:16753920,orangered:16729344,orchid:14315734,palegoldenrod:15657130,palegreen:10025880,paleturquoise:11529966,palevioletred:14381203,papayawhip:16773077,peachpuff:16767673,peru:13468991,pink:16761035,plum:14524637,powderblue:11591910,purple:8388736,red:16711680,rosybrown:12357519,royalblue:4286945,saddlebrown:9127187,salmon:16416882,sandybrown:16032864,seagreen:3050327,seashell:16774638,sienna:10506797,silver:12632256,skyblue:8900331,slateblue:6970061,slategray:7372944,slategrey:7372944,snow:16775930,springgreen:65407,steelblue:4620980,tan:13808780,teal:32896,thistle:14204888,tomato:16737095,turquoise:4251856,violet:15631086,wheat:16113331,white:16777215,whitesmoke:16119285,yellow:16776960,yellowgreen:10145074});Va.forEach(function(n,t){Va.set(n,ft(t))}),Xo.functor=_t,Xo.xhr=wt(bt),Xo.dsv=function(n,t){function e(n,e,i){arguments.length<3&&(i=e,e=null);var o=St(n,t,null==e?r:u(e),i);return o.row=function(n){return arguments.length?o.response(null==(e=n)?r:u(n)):e},o}function r(n){return e.parse(n.responseText)}function u(n){return function(t){return e.parse(t.responseText,n)}}function i(t){return t.map(o).join(n)}function o(n){return a.test(n)?'"'+n.replace(/\"/g,'""')+'"':n}var a=new RegExp('["'+n+"\n]"),c=n.charCodeAt(0);return e.parse=function(n,t){var r;return e.parseRows(n,function(n,e){if(r)return r(n,e-1);var u=new Function("d","return {"+n.map(function(n,t){return JSON.stringify(n)+": d["+t+"]"}).join(",")+"}");r=t?function(n,e){return t(u(n),e)}:u})},e.parseRows=function(n,t){function e(){if(l>=s)return o;if(u)return u=!1,i;var t=l;if(34===n.charCodeAt(t)){for(var e=t;e++<s;)if(34===n.charCodeAt(e)){if(34!==n.charCodeAt(e+1))break;++e}l=e+2;var r=n.charCodeAt(e+1);return 13===r?(u=!0,10===n.charCodeAt(e+2)&&++l):10===r&&(u=!0),n.substring(t+1,e).replace(/""/g,'"')}for(;s>l;){var r=n.charCodeAt(l++),a=1;if(10===r)u=!0;else if(13===r)u=!0,10===n.charCodeAt(l)&&(++l,++a);else if(r!==c)continue;return n.substring(t,l-a)}return n.substring(t)}for(var r,u,i={},o={},a=[],s=n.length,l=0,f=0;(r=e())!==o;){for(var h=[];r!==i&&r!==o;)h.push(r),r=e();(!t||(h=t(h,f++)))&&a.push(h)}return a},e.format=function(t){if(Array.isArray(t[0]))return e.formatRows(t);var r=new l,u=[];return t.forEach(function(n){for(var t in n)r.has(t)||u.push(r.add(t))}),[u.map(o).join(n)].concat(t.map(function(t){return u.map(function(n){return o(t[n])}).join(n)})).join("\n")},e.formatRows=function(n){return n.map(i).join("\n")},e},Xo.csv=Xo.dsv(",","text/csv"),Xo.tsv=Xo.dsv("	","text/tab-separated-values");var Xa,$a,Ba,Wa,Ja,Ga=Go[h(Go,"requestAnimationFrame")]||function(n){setTimeout(n,17)};Xo.timer=function(n,t,e){var r=arguments.length;2>r&&(t=0),3>r&&(e=Date.now());var u=e+t,i={c:n,t:u,f:!1,n:null};$a?$a.n=i:Xa=i,$a=i,Ba||(Wa=clearTimeout(Wa),Ba=1,Ga(Et))},Xo.timer.flush=function(){At(),Ct()},Xo.round=function(n,t){return t?Math.round(n*(t=Math.pow(10,t)))/t:Math.round(n)};var Ka=["y","z","a","f","p","n","\xb5","m","","k","M","G","T","P","E","Z","Y"].map(Lt);Xo.formatPrefix=function(n,t){var e=0;return n&&(0>n&&(n*=-1),t&&(n=Xo.round(n,Nt(n,t))),e=1+Math.floor(1e-12+Math.log(n)/Math.LN10),e=Math.max(-24,Math.min(24,3*Math.floor((0>=e?e+1:e-1)/3)))),Ka[8+e/3]};var Qa=/(?:([^{])?([<>=^]))?([+\- ])?([$#])?(0)?(\d+)?(,)?(\.-?\d+)?([a-z%])?/i,nc=Xo.map({b:function(n){return n.toString(2)},c:function(n){return String.fromCharCode(n)},o:function(n){return n.toString(8)},x:function(n){return n.toString(16)},X:function(n){return n.toString(16).toUpperCase()},g:function(n,t){return n.toPrecision(t)},e:function(n,t){return n.toExponential(t)},f:function(n,t){return n.toFixed(t)},r:function(n,t){return(n=Xo.round(n,Nt(n,t))).toFixed(Math.max(0,Math.min(20,Nt(n*(1+1e-15),t))))}}),tc=Xo.time={},ec=Date;Tt.prototype={getDate:function(){return this._.getUTCDate()},getDay:function(){return this._.getUTCDay()},getFullYear:function(){return this._.getUTCFullYear()},getHours:function(){return this._.getUTCHours()},getMilliseconds:function(){return this._.getUTCMilliseconds()},getMinutes:function(){return this._.getUTCMinutes()},getMonth:function(){return this._.getUTCMonth()},getSeconds:function(){return this._.getUTCSeconds()},getTime:function(){return this._.getTime()},getTimezoneOffset:function(){return 0},valueOf:function(){return this._.valueOf()},setDate:function(){rc.setUTCDate.apply(this._,arguments)},setDay:function(){rc.setUTCDay.apply(this._,arguments)},setFullYear:function(){rc.setUTCFullYear.apply(this._,arguments)},setHours:function(){rc.setUTCHours.apply(this._,arguments)},setMilliseconds:function(){rc.setUTCMilliseconds.apply(this._,arguments)},setMinutes:function(){rc.setUTCMinutes.apply(this._,arguments)},setMonth:function(){rc.setUTCMonth.apply(this._,arguments)},setSeconds:function(){rc.setUTCSeconds.apply(this._,arguments)},setTime:function(){rc.setTime.apply(this._,arguments)}};var rc=Date.prototype;tc.year=Rt(function(n){return n=tc.day(n),n.setMonth(0,1),n},function(n,t){n.setFullYear(n.getFullYear()+t)},function(n){return n.getFullYear()}),tc.years=tc.year.range,tc.years.utc=tc.year.utc.range,tc.day=Rt(function(n){var t=new ec(2e3,0);return t.setFullYear(n.getFullYear(),n.getMonth(),n.getDate()),t},function(n,t){n.setDate(n.getDate()+t)},function(n){return n.getDate()-1}),tc.days=tc.day.range,tc.days.utc=tc.day.utc.range,tc.dayOfYear=function(n){var t=tc.year(n);return Math.floor((n-t-6e4*(n.getTimezoneOffset()-t.getTimezoneOffset()))/864e5)},["sunday","monday","tuesday","wednesday","thursday","friday","saturday"].forEach(function(n,t){t=7-t;var e=tc[n]=Rt(function(n){return(n=tc.day(n)).setDate(n.getDate()-(n.getDay()+t)%7),n},function(n,t){n.setDate(n.getDate()+7*Math.floor(t))},function(n){var e=tc.year(n).getDay();return Math.floor((tc.dayOfYear(n)+(e+t)%7)/7)-(e!==t)});tc[n+"s"]=e.range,tc[n+"s"].utc=e.utc.range,tc[n+"OfYear"]=function(n){var e=tc.year(n).getDay();return Math.floor((tc.dayOfYear(n)+(e+t)%7)/7)}}),tc.week=tc.sunday,tc.weeks=tc.sunday.range,tc.weeks.utc=tc.sunday.utc.range,tc.weekOfYear=tc.sundayOfYear;var uc={"-":"",_:" ",0:"0"},ic=/^\s*\d+/,oc=/^%/;Xo.locale=function(n){return{numberFormat:zt(n),timeFormat:Pt(n)}};var ac=Xo.locale({decimal:".",thousands:",",grouping:[3],currency:["$",""],dateTime:"%a %b %e %X %Y",date:"%m/%d/%Y",time:"%H:%M:%S",periods:["AM","PM"],days:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],shortDays:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],months:["January","February","March","April","May","June","July","August","September","October","November","December"],shortMonths:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]});Xo.format=ac.numberFormat,Xo.geo={},re.prototype={s:0,t:0,add:function(n){ue(n,this.t,cc),ue(cc.s,this.s,this),this.s?this.t+=cc.t:this.s=cc.t},reset:function(){this.s=this.t=0},valueOf:function(){return this.s}};var cc=new re;Xo.geo.stream=function(n,t){n&&sc.hasOwnProperty(n.type)?sc[n.type](n,t):ie(n,t)};var sc={Feature:function(n,t){ie(n.geometry,t)},FeatureCollection:function(n,t){for(var e=n.features,r=-1,u=e.length;++r<u;)ie(e[r].geometry,t)}},lc={Sphere:function(n,t){t.sphere()},Point:function(n,t){n=n.coordinates,t.point(n[0],n[1],n[2])},MultiPoint:function(n,t){for(var e=n.coordinates,r=-1,u=e.length;++r<u;)n=e[r],t.point(n[0],n[1],n[2])},LineString:function(n,t){oe(n.coordinates,t,0)},MultiLineString:function(n,t){for(var e=n.coordinates,r=-1,u=e.length;++r<u;)oe(e[r],t,0)},Polygon:function(n,t){ae(n.coordinates,t)},MultiPolygon:function(n,t){for(var e=n.coordinates,r=-1,u=e.length;++r<u;)ae(e[r],t)},GeometryCollection:function(n,t){for(var e=n.geometries,r=-1,u=e.length;++r<u;)ie(e[r],t)}};Xo.geo.area=function(n){return fc=0,Xo.geo.stream(n,gc),fc};var fc,hc=new re,gc={sphere:function(){fc+=4*Sa},point:g,lineStart:g,lineEnd:g,polygonStart:function(){hc.reset(),gc.lineStart=ce},polygonEnd:function(){var n=2*hc;fc+=0>n?4*Sa+n:n,gc.lineStart=gc.lineEnd=gc.point=g}};Xo.geo.bounds=function(){function n(n,t){x.push(M=[l=n,h=n]),f>t&&(f=t),t>g&&(g=t)}function t(t,e){var r=se([t*Na,e*Na]);if(m){var u=fe(m,r),i=[u[1],-u[0],0],o=fe(i,u);pe(o),o=ve(o);var c=t-p,s=c>0?1:-1,v=o[0]*La*s,d=oa(c)>180;if(d^(v>s*p&&s*t>v)){var y=o[1]*La;y>g&&(g=y)}else if(v=(v+360)%360-180,d^(v>s*p&&s*t>v)){var y=-o[1]*La;f>y&&(f=y)}else f>e&&(f=e),e>g&&(g=e);d?p>t?a(l,t)>a(l,h)&&(h=t):a(t,h)>a(l,h)&&(l=t):h>=l?(l>t&&(l=t),t>h&&(h=t)):t>p?a(l,t)>a(l,h)&&(h=t):a(t,h)>a(l,h)&&(l=t)}else n(t,e);m=r,p=t}function e(){_.point=t}function r(){M[0]=l,M[1]=h,_.point=n,m=null}function u(n,e){if(m){var r=n-p;y+=oa(r)>180?r+(r>0?360:-360):r}else v=n,d=e;gc.point(n,e),t(n,e)}function i(){gc.lineStart()}function o(){u(v,d),gc.lineEnd(),oa(y)>Aa&&(l=-(h=180)),M[0]=l,M[1]=h,m=null}function a(n,t){return(t-=n)<0?t+360:t}function c(n,t){return n[0]-t[0]}function s(n,t){return t[0]<=t[1]?t[0]<=n&&n<=t[1]:n<t[0]||t[1]<n}var l,f,h,g,p,v,d,m,y,x,M,_={point:n,lineStart:e,lineEnd:r,polygonStart:function(){_.point=u,_.lineStart=i,_.lineEnd=o,y=0,gc.polygonStart()},polygonEnd:function(){gc.polygonEnd(),_.point=n,_.lineStart=e,_.lineEnd=r,0>hc?(l=-(h=180),f=-(g=90)):y>Aa?g=90:-Aa>y&&(f=-90),M[0]=l,M[1]=h
-}};return function(n){g=h=-(l=f=1/0),x=[],Xo.geo.stream(n,_);var t=x.length;if(t){x.sort(c);for(var e,r=1,u=x[0],i=[u];t>r;++r)e=x[r],s(e[0],u)||s(e[1],u)?(a(u[0],e[1])>a(u[0],u[1])&&(u[1]=e[1]),a(e[0],u[1])>a(u[0],u[1])&&(u[0]=e[0])):i.push(u=e);for(var o,e,p=-1/0,t=i.length-1,r=0,u=i[t];t>=r;u=e,++r)e=i[r],(o=a(u[1],e[0]))>p&&(p=o,l=e[0],h=u[1])}return x=M=null,1/0===l||1/0===f?[[0/0,0/0],[0/0,0/0]]:[[l,f],[h,g]]}}(),Xo.geo.centroid=function(n){pc=vc=dc=mc=yc=xc=Mc=_c=bc=wc=Sc=0,Xo.geo.stream(n,kc);var t=bc,e=wc,r=Sc,u=t*t+e*e+r*r;return Ca>u&&(t=xc,e=Mc,r=_c,Aa>vc&&(t=dc,e=mc,r=yc),u=t*t+e*e+r*r,Ca>u)?[0/0,0/0]:[Math.atan2(e,t)*La,X(r/Math.sqrt(u))*La]};var pc,vc,dc,mc,yc,xc,Mc,_c,bc,wc,Sc,kc={sphere:g,point:me,lineStart:xe,lineEnd:Me,polygonStart:function(){kc.lineStart=_e},polygonEnd:function(){kc.lineStart=xe}},Ec=Ee(be,ze,Te,[-Sa,-Sa/2]),Ac=1e9;Xo.geo.clipExtent=function(){var n,t,e,r,u,i,o={stream:function(n){return u&&(u.valid=!1),u=i(n),u.valid=!0,u},extent:function(a){return arguments.length?(i=Pe(n=+a[0][0],t=+a[0][1],e=+a[1][0],r=+a[1][1]),u&&(u.valid=!1,u=null),o):[[n,t],[e,r]]}};return o.extent([[0,0],[960,500]])},(Xo.geo.conicEqualArea=function(){return je(He)}).raw=He,Xo.geo.albers=function(){return Xo.geo.conicEqualArea().rotate([96,0]).center([-.6,38.7]).parallels([29.5,45.5]).scale(1070)},Xo.geo.albersUsa=function(){function n(n){var i=n[0],o=n[1];return t=null,e(i,o),t||(r(i,o),t)||u(i,o),t}var t,e,r,u,i=Xo.geo.albers(),o=Xo.geo.conicEqualArea().rotate([154,0]).center([-2,58.5]).parallels([55,65]),a=Xo.geo.conicEqualArea().rotate([157,0]).center([-3,19.9]).parallels([8,18]),c={point:function(n,e){t=[n,e]}};return n.invert=function(n){var t=i.scale(),e=i.translate(),r=(n[0]-e[0])/t,u=(n[1]-e[1])/t;return(u>=.12&&.234>u&&r>=-.425&&-.214>r?o:u>=.166&&.234>u&&r>=-.214&&-.115>r?a:i).invert(n)},n.stream=function(n){var t=i.stream(n),e=o.stream(n),r=a.stream(n);return{point:function(n,u){t.point(n,u),e.point(n,u),r.point(n,u)},sphere:function(){t.sphere(),e.sphere(),r.sphere()},lineStart:function(){t.lineStart(),e.lineStart(),r.lineStart()},lineEnd:function(){t.lineEnd(),e.lineEnd(),r.lineEnd()},polygonStart:function(){t.polygonStart(),e.polygonStart(),r.polygonStart()},polygonEnd:function(){t.polygonEnd(),e.polygonEnd(),r.polygonEnd()}}},n.precision=function(t){return arguments.length?(i.precision(t),o.precision(t),a.precision(t),n):i.precision()},n.scale=function(t){return arguments.length?(i.scale(t),o.scale(.35*t),a.scale(t),n.translate(i.translate())):i.scale()},n.translate=function(t){if(!arguments.length)return i.translate();var s=i.scale(),l=+t[0],f=+t[1];return e=i.translate(t).clipExtent([[l-.455*s,f-.238*s],[l+.455*s,f+.238*s]]).stream(c).point,r=o.translate([l-.307*s,f+.201*s]).clipExtent([[l-.425*s+Aa,f+.12*s+Aa],[l-.214*s-Aa,f+.234*s-Aa]]).stream(c).point,u=a.translate([l-.205*s,f+.212*s]).clipExtent([[l-.214*s+Aa,f+.166*s+Aa],[l-.115*s-Aa,f+.234*s-Aa]]).stream(c).point,n},n.scale(1070)};var Cc,Nc,Lc,zc,qc,Tc,Rc={point:g,lineStart:g,lineEnd:g,polygonStart:function(){Nc=0,Rc.lineStart=Fe},polygonEnd:function(){Rc.lineStart=Rc.lineEnd=Rc.point=g,Cc+=oa(Nc/2)}},Dc={point:Oe,lineStart:g,lineEnd:g,polygonStart:g,polygonEnd:g},Pc={point:Ze,lineStart:Ve,lineEnd:Xe,polygonStart:function(){Pc.lineStart=$e},polygonEnd:function(){Pc.point=Ze,Pc.lineStart=Ve,Pc.lineEnd=Xe}};Xo.geo.path=function(){function n(n){return n&&("function"==typeof a&&i.pointRadius(+a.apply(this,arguments)),o&&o.valid||(o=u(i)),Xo.geo.stream(n,o)),i.result()}function t(){return o=null,n}var e,r,u,i,o,a=4.5;return n.area=function(n){return Cc=0,Xo.geo.stream(n,u(Rc)),Cc},n.centroid=function(n){return dc=mc=yc=xc=Mc=_c=bc=wc=Sc=0,Xo.geo.stream(n,u(Pc)),Sc?[bc/Sc,wc/Sc]:_c?[xc/_c,Mc/_c]:yc?[dc/yc,mc/yc]:[0/0,0/0]},n.bounds=function(n){return qc=Tc=-(Lc=zc=1/0),Xo.geo.stream(n,u(Dc)),[[Lc,zc],[qc,Tc]]},n.projection=function(n){return arguments.length?(u=(e=n)?n.stream||Je(n):bt,t()):e},n.context=function(n){return arguments.length?(i=null==(r=n)?new Ye:new Be(n),"function"!=typeof a&&i.pointRadius(a),t()):r},n.pointRadius=function(t){return arguments.length?(a="function"==typeof t?t:(i.pointRadius(+t),+t),n):a},n.projection(Xo.geo.albersUsa()).context(null)},Xo.geo.transform=function(n){return{stream:function(t){var e=new Ge(t);for(var r in n)e[r]=n[r];return e}}},Ge.prototype={point:function(n,t){this.stream.point(n,t)},sphere:function(){this.stream.sphere()},lineStart:function(){this.stream.lineStart()},lineEnd:function(){this.stream.lineEnd()},polygonStart:function(){this.stream.polygonStart()},polygonEnd:function(){this.stream.polygonEnd()}},Xo.geo.projection=Qe,Xo.geo.projectionMutator=nr,(Xo.geo.equirectangular=function(){return Qe(er)}).raw=er.invert=er,Xo.geo.rotation=function(n){function t(t){return t=n(t[0]*Na,t[1]*Na),t[0]*=La,t[1]*=La,t}return n=ur(n[0]%360*Na,n[1]*Na,n.length>2?n[2]*Na:0),t.invert=function(t){return t=n.invert(t[0]*Na,t[1]*Na),t[0]*=La,t[1]*=La,t},t},rr.invert=er,Xo.geo.circle=function(){function n(){var n="function"==typeof r?r.apply(this,arguments):r,t=ur(-n[0]*Na,-n[1]*Na,0).invert,u=[];return e(null,null,1,{point:function(n,e){u.push(n=t(n,e)),n[0]*=La,n[1]*=La}}),{type:"Polygon",coordinates:[u]}}var t,e,r=[0,0],u=6;return n.origin=function(t){return arguments.length?(r=t,n):r},n.angle=function(r){return arguments.length?(e=cr((t=+r)*Na,u*Na),n):t},n.precision=function(r){return arguments.length?(e=cr(t*Na,(u=+r)*Na),n):u},n.angle(90)},Xo.geo.distance=function(n,t){var e,r=(t[0]-n[0])*Na,u=n[1]*Na,i=t[1]*Na,o=Math.sin(r),a=Math.cos(r),c=Math.sin(u),s=Math.cos(u),l=Math.sin(i),f=Math.cos(i);return Math.atan2(Math.sqrt((e=f*o)*e+(e=s*l-c*f*a)*e),c*l+s*f*a)},Xo.geo.graticule=function(){function n(){return{type:"MultiLineString",coordinates:t()}}function t(){return Xo.range(Math.ceil(i/d)*d,u,d).map(h).concat(Xo.range(Math.ceil(s/m)*m,c,m).map(g)).concat(Xo.range(Math.ceil(r/p)*p,e,p).filter(function(n){return oa(n%d)>Aa}).map(l)).concat(Xo.range(Math.ceil(a/v)*v,o,v).filter(function(n){return oa(n%m)>Aa}).map(f))}var e,r,u,i,o,a,c,s,l,f,h,g,p=10,v=p,d=90,m=360,y=2.5;return n.lines=function(){return t().map(function(n){return{type:"LineString",coordinates:n}})},n.outline=function(){return{type:"Polygon",coordinates:[h(i).concat(g(c).slice(1),h(u).reverse().slice(1),g(s).reverse().slice(1))]}},n.extent=function(t){return arguments.length?n.majorExtent(t).minorExtent(t):n.minorExtent()},n.majorExtent=function(t){return arguments.length?(i=+t[0][0],u=+t[1][0],s=+t[0][1],c=+t[1][1],i>u&&(t=i,i=u,u=t),s>c&&(t=s,s=c,c=t),n.precision(y)):[[i,s],[u,c]]},n.minorExtent=function(t){return arguments.length?(r=+t[0][0],e=+t[1][0],a=+t[0][1],o=+t[1][1],r>e&&(t=r,r=e,e=t),a>o&&(t=a,a=o,o=t),n.precision(y)):[[r,a],[e,o]]},n.step=function(t){return arguments.length?n.majorStep(t).minorStep(t):n.minorStep()},n.majorStep=function(t){return arguments.length?(d=+t[0],m=+t[1],n):[d,m]},n.minorStep=function(t){return arguments.length?(p=+t[0],v=+t[1],n):[p,v]},n.precision=function(t){return arguments.length?(y=+t,l=lr(a,o,90),f=fr(r,e,y),h=lr(s,c,90),g=fr(i,u,y),n):y},n.majorExtent([[-180,-90+Aa],[180,90-Aa]]).minorExtent([[-180,-80-Aa],[180,80+Aa]])},Xo.geo.greatArc=function(){function n(){return{type:"LineString",coordinates:[t||r.apply(this,arguments),e||u.apply(this,arguments)]}}var t,e,r=hr,u=gr;return n.distance=function(){return Xo.geo.distance(t||r.apply(this,arguments),e||u.apply(this,arguments))},n.source=function(e){return arguments.length?(r=e,t="function"==typeof e?null:e,n):r},n.target=function(t){return arguments.length?(u=t,e="function"==typeof t?null:t,n):u},n.precision=function(){return arguments.length?n:0},n},Xo.geo.interpolate=function(n,t){return pr(n[0]*Na,n[1]*Na,t[0]*Na,t[1]*Na)},Xo.geo.length=function(n){return Uc=0,Xo.geo.stream(n,jc),Uc};var Uc,jc={sphere:g,point:g,lineStart:vr,lineEnd:g,polygonStart:g,polygonEnd:g},Hc=dr(function(n){return Math.sqrt(2/(1+n))},function(n){return 2*Math.asin(n/2)});(Xo.geo.azimuthalEqualArea=function(){return Qe(Hc)}).raw=Hc;var Fc=dr(function(n){var t=Math.acos(n);return t&&t/Math.sin(t)},bt);(Xo.geo.azimuthalEquidistant=function(){return Qe(Fc)}).raw=Fc,(Xo.geo.conicConformal=function(){return je(mr)}).raw=mr,(Xo.geo.conicEquidistant=function(){return je(yr)}).raw=yr;var Oc=dr(function(n){return 1/n},Math.atan);(Xo.geo.gnomonic=function(){return Qe(Oc)}).raw=Oc,xr.invert=function(n,t){return[n,2*Math.atan(Math.exp(t))-Ea]},(Xo.geo.mercator=function(){return Mr(xr)}).raw=xr;var Yc=dr(function(){return 1},Math.asin);(Xo.geo.orthographic=function(){return Qe(Yc)}).raw=Yc;var Ic=dr(function(n){return 1/(1+n)},function(n){return 2*Math.atan(n)});(Xo.geo.stereographic=function(){return Qe(Ic)}).raw=Ic,_r.invert=function(n,t){return[-t,2*Math.atan(Math.exp(n))-Ea]},(Xo.geo.transverseMercator=function(){var n=Mr(_r),t=n.center,e=n.rotate;return n.center=function(n){return n?t([-n[1],n[0]]):(n=t(),[-n[1],n[0]])},n.rotate=function(n){return n?e([n[0],n[1],n.length>2?n[2]+90:90]):(n=e(),[n[0],n[1],n[2]-90])},n.rotate([0,0])}).raw=_r,Xo.geom={},Xo.geom.hull=function(n){function t(n){if(n.length<3)return[];var t,u=_t(e),i=_t(r),o=n.length,a=[],c=[];for(t=0;o>t;t++)a.push([+u.call(this,n[t],t),+i.call(this,n[t],t),t]);for(a.sort(kr),t=0;o>t;t++)c.push([a[t][0],-a[t][1]]);var s=Sr(a),l=Sr(c),f=l[0]===s[0],h=l[l.length-1]===s[s.length-1],g=[];for(t=s.length-1;t>=0;--t)g.push(n[a[s[t]][2]]);for(t=+f;t<l.length-h;++t)g.push(n[a[l[t]][2]]);return g}var e=br,r=wr;return arguments.length?t(n):(t.x=function(n){return arguments.length?(e=n,t):e},t.y=function(n){return arguments.length?(r=n,t):r},t)},Xo.geom.polygon=function(n){return fa(n,Zc),n};var Zc=Xo.geom.polygon.prototype=[];Zc.area=function(){for(var n,t=-1,e=this.length,r=this[e-1],u=0;++t<e;)n=r,r=this[t],u+=n[1]*r[0]-n[0]*r[1];return.5*u},Zc.centroid=function(n){var t,e,r=-1,u=this.length,i=0,o=0,a=this[u-1];for(arguments.length||(n=-1/(6*this.area()));++r<u;)t=a,a=this[r],e=t[0]*a[1]-a[0]*t[1],i+=(t[0]+a[0])*e,o+=(t[1]+a[1])*e;return[i*n,o*n]},Zc.clip=function(n){for(var t,e,r,u,i,o,a=Cr(n),c=-1,s=this.length-Cr(this),l=this[s-1];++c<s;){for(t=n.slice(),n.length=0,u=this[c],i=t[(r=t.length-a)-1],e=-1;++e<r;)o=t[e],Er(o,l,u)?(Er(i,l,u)||n.push(Ar(i,o,l,u)),n.push(o)):Er(i,l,u)&&n.push(Ar(i,o,l,u)),i=o;a&&n.push(n[0]),l=u}return n};var Vc,Xc,$c,Bc,Wc,Jc=[],Gc=[];Pr.prototype.prepare=function(){for(var n,t=this.edges,e=t.length;e--;)n=t[e].edge,n.b&&n.a||t.splice(e,1);return t.sort(jr),t.length},Br.prototype={start:function(){return this.edge.l===this.site?this.edge.a:this.edge.b},end:function(){return this.edge.l===this.site?this.edge.b:this.edge.a}},Wr.prototype={insert:function(n,t){var e,r,u;if(n){if(t.P=n,t.N=n.N,n.N&&(n.N.P=t),n.N=t,n.R){for(n=n.R;n.L;)n=n.L;n.L=t}else n.R=t;e=n}else this._?(n=Qr(this._),t.P=null,t.N=n,n.P=n.L=t,e=n):(t.P=t.N=null,this._=t,e=null);for(t.L=t.R=null,t.U=e,t.C=!0,n=t;e&&e.C;)r=e.U,e===r.L?(u=r.R,u&&u.C?(e.C=u.C=!1,r.C=!0,n=r):(n===e.R&&(Gr(this,e),n=e,e=n.U),e.C=!1,r.C=!0,Kr(this,r))):(u=r.L,u&&u.C?(e.C=u.C=!1,r.C=!0,n=r):(n===e.L&&(Kr(this,e),n=e,e=n.U),e.C=!1,r.C=!0,Gr(this,r))),e=n.U;this._.C=!1},remove:function(n){n.N&&(n.N.P=n.P),n.P&&(n.P.N=n.N),n.N=n.P=null;var t,e,r,u=n.U,i=n.L,o=n.R;if(e=i?o?Qr(o):i:o,u?u.L===n?u.L=e:u.R=e:this._=e,i&&o?(r=e.C,e.C=n.C,e.L=i,i.U=e,e!==o?(u=e.U,e.U=n.U,n=e.R,u.L=n,e.R=o,o.U=e):(e.U=u,u=e,n=e.R)):(r=n.C,n=e),n&&(n.U=u),!r){if(n&&n.C)return n.C=!1,void 0;do{if(n===this._)break;if(n===u.L){if(t=u.R,t.C&&(t.C=!1,u.C=!0,Gr(this,u),t=u.R),t.L&&t.L.C||t.R&&t.R.C){t.R&&t.R.C||(t.L.C=!1,t.C=!0,Kr(this,t),t=u.R),t.C=u.C,u.C=t.R.C=!1,Gr(this,u),n=this._;break}}else if(t=u.L,t.C&&(t.C=!1,u.C=!0,Kr(this,u),t=u.L),t.L&&t.L.C||t.R&&t.R.C){t.L&&t.L.C||(t.R.C=!1,t.C=!0,Gr(this,t),t=u.L),t.C=u.C,u.C=t.L.C=!1,Kr(this,u),n=this._;break}t.C=!0,n=u,u=u.U}while(!n.C);n&&(n.C=!1)}}},Xo.geom.voronoi=function(n){function t(n){var t=new Array(n.length),r=a[0][0],u=a[0][1],i=a[1][0],o=a[1][1];return nu(e(n),a).cells.forEach(function(e,a){var c=e.edges,s=e.site,l=t[a]=c.length?c.map(function(n){var t=n.start();return[t.x,t.y]}):s.x>=r&&s.x<=i&&s.y>=u&&s.y<=o?[[r,o],[i,o],[i,u],[r,u]]:[];l.point=n[a]}),t}function e(n){return n.map(function(n,t){return{x:Math.round(i(n,t)/Aa)*Aa,y:Math.round(o(n,t)/Aa)*Aa,i:t}})}var r=br,u=wr,i=r,o=u,a=Kc;return n?t(n):(t.links=function(n){return nu(e(n)).edges.filter(function(n){return n.l&&n.r}).map(function(t){return{source:n[t.l.i],target:n[t.r.i]}})},t.triangles=function(n){var t=[];return nu(e(n)).cells.forEach(function(e,r){for(var u,i,o=e.site,a=e.edges.sort(jr),c=-1,s=a.length,l=a[s-1].edge,f=l.l===o?l.r:l.l;++c<s;)u=l,i=f,l=a[c].edge,f=l.l===o?l.r:l.l,r<i.i&&r<f.i&&eu(o,i,f)<0&&t.push([n[r],n[i.i],n[f.i]])}),t},t.x=function(n){return arguments.length?(i=_t(r=n),t):r},t.y=function(n){return arguments.length?(o=_t(u=n),t):u},t.clipExtent=function(n){return arguments.length?(a=null==n?Kc:n,t):a===Kc?null:a},t.size=function(n){return arguments.length?t.clipExtent(n&&[[0,0],n]):a===Kc?null:a&&a[1]},t)};var Kc=[[-1e6,-1e6],[1e6,1e6]];Xo.geom.delaunay=function(n){return Xo.geom.voronoi().triangles(n)},Xo.geom.quadtree=function(n,t,e,r,u){function i(n){function i(n,t,e,r,u,i,o,a){if(!isNaN(e)&&!isNaN(r))if(n.leaf){var c=n.x,l=n.y;if(null!=c)if(oa(c-e)+oa(l-r)<.01)s(n,t,e,r,u,i,o,a);else{var f=n.point;n.x=n.y=n.point=null,s(n,f,c,l,u,i,o,a),s(n,t,e,r,u,i,o,a)}else n.x=e,n.y=r,n.point=t}else s(n,t,e,r,u,i,o,a)}function s(n,t,e,r,u,o,a,c){var s=.5*(u+a),l=.5*(o+c),f=e>=s,h=r>=l,g=(h<<1)+f;n.leaf=!1,n=n.nodes[g]||(n.nodes[g]=iu()),f?u=s:a=s,h?o=l:c=l,i(n,t,e,r,u,o,a,c)}var l,f,h,g,p,v,d,m,y,x=_t(a),M=_t(c);if(null!=t)v=t,d=e,m=r,y=u;else if(m=y=-(v=d=1/0),f=[],h=[],p=n.length,o)for(g=0;p>g;++g)l=n[g],l.x<v&&(v=l.x),l.y<d&&(d=l.y),l.x>m&&(m=l.x),l.y>y&&(y=l.y),f.push(l.x),h.push(l.y);else for(g=0;p>g;++g){var _=+x(l=n[g],g),b=+M(l,g);v>_&&(v=_),d>b&&(d=b),_>m&&(m=_),b>y&&(y=b),f.push(_),h.push(b)}var w=m-v,S=y-d;w>S?y=d+w:m=v+S;var k=iu();if(k.add=function(n){i(k,n,+x(n,++g),+M(n,g),v,d,m,y)},k.visit=function(n){ou(n,k,v,d,m,y)},g=-1,null==t){for(;++g<p;)i(k,n[g],f[g],h[g],v,d,m,y);--g}else n.forEach(k.add);return f=h=n=l=null,k}var o,a=br,c=wr;return(o=arguments.length)?(a=ru,c=uu,3===o&&(u=e,r=t,e=t=0),i(n)):(i.x=function(n){return arguments.length?(a=n,i):a},i.y=function(n){return arguments.length?(c=n,i):c},i.extent=function(n){return arguments.length?(null==n?t=e=r=u=null:(t=+n[0][0],e=+n[0][1],r=+n[1][0],u=+n[1][1]),i):null==t?null:[[t,e],[r,u]]},i.size=function(n){return arguments.length?(null==n?t=e=r=u=null:(t=e=0,r=+n[0],u=+n[1]),i):null==t?null:[r-t,u-e]},i)},Xo.interpolateRgb=au,Xo.interpolateObject=cu,Xo.interpolateNumber=su,Xo.interpolateString=lu;var Qc=/[-+]?(?:\d+\.?\d*|\.?\d+)(?:[eE][-+]?\d+)?/g;Xo.interpolate=fu,Xo.interpolators=[function(n,t){var e=typeof t;return("string"===e?Va.has(t)||/^(#|rgb\(|hsl\()/.test(t)?au:lu:t instanceof G?au:"object"===e?Array.isArray(t)?hu:cu:su)(n,t)}],Xo.interpolateArray=hu;var ns=function(){return bt},ts=Xo.map({linear:ns,poly:xu,quad:function(){return du},cubic:function(){return mu},sin:function(){return Mu},exp:function(){return _u},circle:function(){return bu},elastic:wu,back:Su,bounce:function(){return ku}}),es=Xo.map({"in":bt,out:pu,"in-out":vu,"out-in":function(n){return vu(pu(n))}});Xo.ease=function(n){var t=n.indexOf("-"),e=t>=0?n.substring(0,t):n,r=t>=0?n.substring(t+1):"in";return e=ts.get(e)||ns,r=es.get(r)||bt,gu(r(e.apply(null,$o.call(arguments,1))))},Xo.interpolateHcl=Eu,Xo.interpolateHsl=Au,Xo.interpolateLab=Cu,Xo.interpolateRound=Nu,Xo.transform=function(n){var t=Wo.createElementNS(Xo.ns.prefix.svg,"g");return(Xo.transform=function(n){if(null!=n){t.setAttribute("transform",n);var e=t.transform.baseVal.consolidate()}return new Lu(e?e.matrix:rs)})(n)},Lu.prototype.toString=function(){return"translate("+this.translate+")rotate("+this.rotate+")skewX("+this.skew+")scale("+this.scale+")"};var rs={a:1,b:0,c:0,d:1,e:0,f:0};Xo.interpolateTransform=Ru,Xo.layout={},Xo.layout.bundle=function(){return function(n){for(var t=[],e=-1,r=n.length;++e<r;)t.push(Uu(n[e]));return t}},Xo.layout.chord=function(){function n(){var n,s,f,h,g,p={},v=[],d=Xo.range(i),m=[];for(e=[],r=[],n=0,h=-1;++h<i;){for(s=0,g=-1;++g<i;)s+=u[h][g];v.push(s),m.push(Xo.range(i)),n+=s}for(o&&d.sort(function(n,t){return o(v[n],v[t])}),a&&m.forEach(function(n,t){n.sort(function(n,e){return a(u[t][n],u[t][e])})}),n=(ka-l*i)/n,s=0,h=-1;++h<i;){for(f=s,g=-1;++g<i;){var y=d[h],x=m[y][g],M=u[y][x],_=s,b=s+=M*n;p[y+"-"+x]={index:y,subindex:x,startAngle:_,endAngle:b,value:M}}r[y]={index:y,startAngle:f,endAngle:s,value:(s-f)/n},s+=l}for(h=-1;++h<i;)for(g=h-1;++g<i;){var w=p[h+"-"+g],S=p[g+"-"+h];(w.value||S.value)&&e.push(w.value<S.value?{source:S,target:w}:{source:w,target:S})}c&&t()}function t(){e.sort(function(n,t){return c((n.source.value+n.target.value)/2,(t.source.value+t.target.value)/2)})}var e,r,u,i,o,a,c,s={},l=0;return s.matrix=function(n){return arguments.length?(i=(u=n)&&u.length,e=r=null,s):u},s.padding=function(n){return arguments.length?(l=n,e=r=null,s):l},s.sortGroups=function(n){return arguments.length?(o=n,e=r=null,s):o},s.sortSubgroups=function(n){return arguments.length?(a=n,e=null,s):a},s.sortChords=function(n){return arguments.length?(c=n,e&&t(),s):c},s.chords=function(){return e||n(),e},s.groups=function(){return r||n(),r},s},Xo.layout.force=function(){function n(n){return function(t,e,r,u){if(t.point!==n){var i=t.cx-n.x,o=t.cy-n.y,a=u-e,c=i*i+o*o;if(c>a*a/d){if(p>c){var s=t.charge/c;n.px-=i*s,n.py-=o*s}return!0}if(t.point&&c&&p>c){var s=t.pointCharge/c;n.px-=i*s,n.py-=o*s}}return!t.charge}}function t(n){n.px=Xo.event.x,n.py=Xo.event.y,a.resume()}var e,r,u,i,o,a={},c=Xo.dispatch("start","tick","end"),s=[1,1],l=.9,f=us,h=is,g=-30,p=os,v=.1,d=.64,m=[],y=[];return a.tick=function(){if((r*=.99)<.005)return c.end({type:"end",alpha:r=0}),!0;var t,e,a,f,h,p,d,x,M,_=m.length,b=y.length;for(e=0;b>e;++e)a=y[e],f=a.source,h=a.target,x=h.x-f.x,M=h.y-f.y,(p=x*x+M*M)&&(p=r*i[e]*((p=Math.sqrt(p))-u[e])/p,x*=p,M*=p,h.x-=x*(d=f.weight/(h.weight+f.weight)),h.y-=M*d,f.x+=x*(d=1-d),f.y+=M*d);if((d=r*v)&&(x=s[0]/2,M=s[1]/2,e=-1,d))for(;++e<_;)a=m[e],a.x+=(x-a.x)*d,a.y+=(M-a.y)*d;if(g)for(Zu(t=Xo.geom.quadtree(m),r,o),e=-1;++e<_;)(a=m[e]).fixed||t.visit(n(a));for(e=-1;++e<_;)a=m[e],a.fixed?(a.x=a.px,a.y=a.py):(a.x-=(a.px-(a.px=a.x))*l,a.y-=(a.py-(a.py=a.y))*l);c.tick({type:"tick",alpha:r})},a.nodes=function(n){return arguments.length?(m=n,a):m},a.links=function(n){return arguments.length?(y=n,a):y},a.size=function(n){return arguments.length?(s=n,a):s},a.linkDistance=function(n){return arguments.length?(f="function"==typeof n?n:+n,a):f},a.distance=a.linkDistance,a.linkStrength=function(n){return arguments.length?(h="function"==typeof n?n:+n,a):h},a.friction=function(n){return arguments.length?(l=+n,a):l},a.charge=function(n){return arguments.length?(g="function"==typeof n?n:+n,a):g},a.chargeDistance=function(n){return arguments.length?(p=n*n,a):Math.sqrt(p)},a.gravity=function(n){return arguments.length?(v=+n,a):v},a.theta=function(n){return arguments.length?(d=n*n,a):Math.sqrt(d)},a.alpha=function(n){return arguments.length?(n=+n,r?r=n>0?n:0:n>0&&(c.start({type:"start",alpha:r=n}),Xo.timer(a.tick)),a):r},a.start=function(){function n(n,r){if(!e){for(e=new Array(c),a=0;c>a;++a)e[a]=[];for(a=0;s>a;++a){var u=y[a];e[u.source.index].push(u.target),e[u.target.index].push(u.source)}}for(var i,o=e[t],a=-1,s=o.length;++a<s;)if(!isNaN(i=o[a][n]))return i;return Math.random()*r}var t,e,r,c=m.length,l=y.length,p=s[0],v=s[1];for(t=0;c>t;++t)(r=m[t]).index=t,r.weight=0;for(t=0;l>t;++t)r=y[t],"number"==typeof r.source&&(r.source=m[r.source]),"number"==typeof r.target&&(r.target=m[r.target]),++r.source.weight,++r.target.weight;for(t=0;c>t;++t)r=m[t],isNaN(r.x)&&(r.x=n("x",p)),isNaN(r.y)&&(r.y=n("y",v)),isNaN(r.px)&&(r.px=r.x),isNaN(r.py)&&(r.py=r.y);if(u=[],"function"==typeof f)for(t=0;l>t;++t)u[t]=+f.call(this,y[t],t);else for(t=0;l>t;++t)u[t]=f;if(i=[],"function"==typeof h)for(t=0;l>t;++t)i[t]=+h.call(this,y[t],t);else for(t=0;l>t;++t)i[t]=h;if(o=[],"function"==typeof g)for(t=0;c>t;++t)o[t]=+g.call(this,m[t],t);else for(t=0;c>t;++t)o[t]=g;return a.resume()},a.resume=function(){return a.alpha(.1)},a.stop=function(){return a.alpha(0)},a.drag=function(){return e||(e=Xo.behavior.drag().origin(bt).on("dragstart.force",Fu).on("drag.force",t).on("dragend.force",Ou)),arguments.length?(this.on("mouseover.force",Yu).on("mouseout.force",Iu).call(e),void 0):e},Xo.rebind(a,c,"on")};var us=20,is=1,os=1/0;Xo.layout.hierarchy=function(){function n(t,o,a){var c=u.call(e,t,o);if(t.depth=o,a.push(t),c&&(s=c.length)){for(var s,l,f=-1,h=t.children=new Array(s),g=0,p=o+1;++f<s;)l=h[f]=n(c[f],p,a),l.parent=t,g+=l.value;r&&h.sort(r),i&&(t.value=g)}else delete t.children,i&&(t.value=+i.call(e,t,o)||0);return t}function t(n,r){var u=n.children,o=0;if(u&&(a=u.length))for(var a,c=-1,s=r+1;++c<a;)o+=t(u[c],s);else i&&(o=+i.call(e,n,r)||0);return i&&(n.value=o),o}function e(t){var e=[];return n(t,0,e),e}var r=Bu,u=Xu,i=$u;return e.sort=function(n){return arguments.length?(r=n,e):r},e.children=function(n){return arguments.length?(u=n,e):u},e.value=function(n){return arguments.length?(i=n,e):i},e.revalue=function(n){return t(n,0),n},e},Xo.layout.partition=function(){function n(t,e,r,u){var i=t.children;if(t.x=e,t.y=t.depth*u,t.dx=r,t.dy=u,i&&(o=i.length)){var o,a,c,s=-1;for(r=t.value?r/t.value:0;++s<o;)n(a=i[s],e,c=a.value*r,u),e+=c}}function t(n){var e=n.children,r=0;if(e&&(u=e.length))for(var u,i=-1;++i<u;)r=Math.max(r,t(e[i]));return 1+r}function e(e,i){var o=r.call(this,e,i);return n(o[0],0,u[0],u[1]/t(o[0])),o}var r=Xo.layout.hierarchy(),u=[1,1];return e.size=function(n){return arguments.length?(u=n,e):u},Vu(e,r)},Xo.layout.pie=function(){function n(i){var o=i.map(function(e,r){return+t.call(n,e,r)}),a=+("function"==typeof r?r.apply(this,arguments):r),c=(("function"==typeof u?u.apply(this,arguments):u)-a)/Xo.sum(o),s=Xo.range(i.length);null!=e&&s.sort(e===as?function(n,t){return o[t]-o[n]}:function(n,t){return e(i[n],i[t])});var l=[];return s.forEach(function(n){var t;l[n]={data:i[n],value:t=o[n],startAngle:a,endAngle:a+=t*c}}),l}var t=Number,e=as,r=0,u=ka;return n.value=function(e){return arguments.length?(t=e,n):t},n.sort=function(t){return arguments.length?(e=t,n):e},n.startAngle=function(t){return arguments.length?(r=t,n):r},n.endAngle=function(t){return arguments.length?(u=t,n):u},n};var as={};Xo.layout.stack=function(){function n(a,c){var s=a.map(function(e,r){return t.call(n,e,r)}),l=s.map(function(t){return t.map(function(t,e){return[i.call(n,t,e),o.call(n,t,e)]})}),f=e.call(n,l,c);s=Xo.permute(s,f),l=Xo.permute(l,f);var h,g,p,v=r.call(n,l,c),d=s.length,m=s[0].length;for(g=0;m>g;++g)for(u.call(n,s[0][g],p=v[g],l[0][g][1]),h=1;d>h;++h)u.call(n,s[h][g],p+=l[h-1][g][1],l[h][g][1]);return a}var t=bt,e=Qu,r=ni,u=Ku,i=Ju,o=Gu;return n.values=function(e){return arguments.length?(t=e,n):t},n.order=function(t){return arguments.length?(e="function"==typeof t?t:cs.get(t)||Qu,n):e},n.offset=function(t){return arguments.length?(r="function"==typeof t?t:ss.get(t)||ni,n):r},n.x=function(t){return arguments.length?(i=t,n):i},n.y=function(t){return arguments.length?(o=t,n):o},n.out=function(t){return arguments.length?(u=t,n):u},n};var cs=Xo.map({"inside-out":function(n){var t,e,r=n.length,u=n.map(ti),i=n.map(ei),o=Xo.range(r).sort(function(n,t){return u[n]-u[t]}),a=0,c=0,s=[],l=[];for(t=0;r>t;++t)e=o[t],c>a?(a+=i[e],s.push(e)):(c+=i[e],l.push(e));return l.reverse().concat(s)},reverse:function(n){return Xo.range(n.length).reverse()},"default":Qu}),ss=Xo.map({silhouette:function(n){var t,e,r,u=n.length,i=n[0].length,o=[],a=0,c=[];for(e=0;i>e;++e){for(t=0,r=0;u>t;t++)r+=n[t][e][1];r>a&&(a=r),o.push(r)}for(e=0;i>e;++e)c[e]=(a-o[e])/2;return c},wiggle:function(n){var t,e,r,u,i,o,a,c,s,l=n.length,f=n[0],h=f.length,g=[];for(g[0]=c=s=0,e=1;h>e;++e){for(t=0,u=0;l>t;++t)u+=n[t][e][1];for(t=0,i=0,a=f[e][0]-f[e-1][0];l>t;++t){for(r=0,o=(n[t][e][1]-n[t][e-1][1])/(2*a);t>r;++r)o+=(n[r][e][1]-n[r][e-1][1])/a;i+=o*n[t][e][1]}g[e]=c-=u?i/u*a:0,s>c&&(s=c)}for(e=0;h>e;++e)g[e]-=s;return g},expand:function(n){var t,e,r,u=n.length,i=n[0].length,o=1/u,a=[];for(e=0;i>e;++e){for(t=0,r=0;u>t;t++)r+=n[t][e][1];if(r)for(t=0;u>t;t++)n[t][e][1]/=r;else for(t=0;u>t;t++)n[t][e][1]=o}for(e=0;i>e;++e)a[e]=0;return a},zero:ni});Xo.layout.histogram=function(){function n(n,i){for(var o,a,c=[],s=n.map(e,this),l=r.call(this,s,i),f=u.call(this,l,s,i),i=-1,h=s.length,g=f.length-1,p=t?1:1/h;++i<g;)o=c[i]=[],o.dx=f[i+1]-(o.x=f[i]),o.y=0;if(g>0)for(i=-1;++i<h;)a=s[i],a>=l[0]&&a<=l[1]&&(o=c[Xo.bisect(f,a,1,g)-1],o.y+=p,o.push(n[i]));return c}var t=!0,e=Number,r=oi,u=ui;return n.value=function(t){return arguments.length?(e=t,n):e},n.range=function(t){return arguments.length?(r=_t(t),n):r},n.bins=function(t){return arguments.length?(u="number"==typeof t?function(n){return ii(n,t)}:_t(t),n):u},n.frequency=function(e){return arguments.length?(t=!!e,n):t},n},Xo.layout.tree=function(){function n(n,i){function o(n,t){var r=n.children,u=n._tree;if(r&&(i=r.length)){for(var i,a,s,l=r[0],f=l,h=-1;++h<i;)s=r[h],o(s,a),f=c(s,a,f),a=s;vi(n);var g=.5*(l._tree.prelim+s._tree.prelim);t?(u.prelim=t._tree.prelim+e(n,t),u.mod=u.prelim-g):u.prelim=g}else t&&(u.prelim=t._tree.prelim+e(n,t))}function a(n,t){n.x=n._tree.prelim+t;var e=n.children;if(e&&(r=e.length)){var r,u=-1;for(t+=n._tree.mod;++u<r;)a(e[u],t)}}function c(n,t,r){if(t){for(var u,i=n,o=n,a=t,c=n.parent.children[0],s=i._tree.mod,l=o._tree.mod,f=a._tree.mod,h=c._tree.mod;a=si(a),i=ci(i),a&&i;)c=ci(c),o=si(o),o._tree.ancestor=n,u=a._tree.prelim+f-i._tree.prelim-s+e(a,i),u>0&&(di(mi(a,n,r),n,u),s+=u,l+=u),f+=a._tree.mod,s+=i._tree.mod,h+=c._tree.mod,l+=o._tree.mod;a&&!si(o)&&(o._tree.thread=a,o._tree.mod+=f-l),i&&!ci(c)&&(c._tree.thread=i,c._tree.mod+=s-h,r=n)}return r}var s=t.call(this,n,i),l=s[0];pi(l,function(n,t){n._tree={ancestor:n,prelim:0,mod:0,change:0,shift:0,number:t?t._tree.number+1:0}}),o(l),a(l,-l._tree.prelim);var f=li(l,hi),h=li(l,fi),g=li(l,gi),p=f.x-e(f,h)/2,v=h.x+e(h,f)/2,d=g.depth||1;return pi(l,u?function(n){n.x*=r[0],n.y=n.depth*r[1],delete n._tree}:function(n){n.x=(n.x-p)/(v-p)*r[0],n.y=n.depth/d*r[1],delete n._tree}),s}var t=Xo.layout.hierarchy().sort(null).value(null),e=ai,r=[1,1],u=!1;return n.separation=function(t){return arguments.length?(e=t,n):e},n.size=function(t){return arguments.length?(u=null==(r=t),n):u?null:r},n.nodeSize=function(t){return arguments.length?(u=null!=(r=t),n):u?r:null},Vu(n,t)},Xo.layout.pack=function(){function n(n,i){var o=e.call(this,n,i),a=o[0],c=u[0],s=u[1],l=null==t?Math.sqrt:"function"==typeof t?t:function(){return t};if(a.x=a.y=0,pi(a,function(n){n.r=+l(n.value)}),pi(a,bi),r){var f=r*(t?1:Math.max(2*a.r/c,2*a.r/s))/2;pi(a,function(n){n.r+=f}),pi(a,bi),pi(a,function(n){n.r-=f})}return ki(a,c/2,s/2,t?1:1/Math.max(2*a.r/c,2*a.r/s)),o}var t,e=Xo.layout.hierarchy().sort(yi),r=0,u=[1,1];return n.size=function(t){return arguments.length?(u=t,n):u},n.radius=function(e){return arguments.length?(t=null==e||"function"==typeof e?e:+e,n):t},n.padding=function(t){return arguments.length?(r=+t,n):r},Vu(n,e)},Xo.layout.cluster=function(){function n(n,i){var o,a=t.call(this,n,i),c=a[0],s=0;pi(c,function(n){var t=n.children;t&&t.length?(n.x=Ci(t),n.y=Ai(t)):(n.x=o?s+=e(n,o):0,n.y=0,o=n)});var l=Ni(c),f=Li(c),h=l.x-e(l,f)/2,g=f.x+e(f,l)/2;return pi(c,u?function(n){n.x=(n.x-c.x)*r[0],n.y=(c.y-n.y)*r[1]}:function(n){n.x=(n.x-h)/(g-h)*r[0],n.y=(1-(c.y?n.y/c.y:1))*r[1]}),a}var t=Xo.layout.hierarchy().sort(null).value(null),e=ai,r=[1,1],u=!1;return n.separation=function(t){return arguments.length?(e=t,n):e},n.size=function(t){return arguments.length?(u=null==(r=t),n):u?null:r},n.nodeSize=function(t){return arguments.length?(u=null!=(r=t),n):u?r:null},Vu(n,t)},Xo.layout.treemap=function(){function n(n,t){for(var e,r,u=-1,i=n.length;++u<i;)r=(e=n[u]).value*(0>t?0:t),e.area=isNaN(r)||0>=r?0:r}function t(e){var i=e.children;if(i&&i.length){var o,a,c,s=f(e),l=[],h=i.slice(),p=1/0,v="slice"===g?s.dx:"dice"===g?s.dy:"slice-dice"===g?1&e.depth?s.dy:s.dx:Math.min(s.dx,s.dy);for(n(h,s.dx*s.dy/e.value),l.area=0;(c=h.length)>0;)l.push(o=h[c-1]),l.area+=o.area,"squarify"!==g||(a=r(l,v))<=p?(h.pop(),p=a):(l.area-=l.pop().area,u(l,v,s,!1),v=Math.min(s.dx,s.dy),l.length=l.area=0,p=1/0);l.length&&(u(l,v,s,!0),l.length=l.area=0),i.forEach(t)}}function e(t){var r=t.children;if(r&&r.length){var i,o=f(t),a=r.slice(),c=[];for(n(a,o.dx*o.dy/t.value),c.area=0;i=a.pop();)c.push(i),c.area+=i.area,null!=i.z&&(u(c,i.z?o.dx:o.dy,o,!a.length),c.length=c.area=0);r.forEach(e)}}function r(n,t){for(var e,r=n.area,u=0,i=1/0,o=-1,a=n.length;++o<a;)(e=n[o].area)&&(i>e&&(i=e),e>u&&(u=e));return r*=r,t*=t,r?Math.max(t*u*p/r,r/(t*i*p)):1/0}function u(n,t,e,r){var u,i=-1,o=n.length,a=e.x,s=e.y,l=t?c(n.area/t):0;if(t==e.dx){for((r||l>e.dy)&&(l=e.dy);++i<o;)u=n[i],u.x=a,u.y=s,u.dy=l,a+=u.dx=Math.min(e.x+e.dx-a,l?c(u.area/l):0);u.z=!0,u.dx+=e.x+e.dx-a,e.y+=l,e.dy-=l}else{for((r||l>e.dx)&&(l=e.dx);++i<o;)u=n[i],u.x=a,u.y=s,u.dx=l,s+=u.dy=Math.min(e.y+e.dy-s,l?c(u.area/l):0);u.z=!1,u.dy+=e.y+e.dy-s,e.x+=l,e.dx-=l}}function i(r){var u=o||a(r),i=u[0];return i.x=0,i.y=0,i.dx=s[0],i.dy=s[1],o&&a.revalue(i),n([i],i.dx*i.dy/i.value),(o?e:t)(i),h&&(o=u),u}var o,a=Xo.layout.hierarchy(),c=Math.round,s=[1,1],l=null,f=zi,h=!1,g="squarify",p=.5*(1+Math.sqrt(5));return i.size=function(n){return arguments.length?(s=n,i):s},i.padding=function(n){function t(t){var e=n.call(i,t,t.depth);return null==e?zi(t):qi(t,"number"==typeof e?[e,e,e,e]:e)}function e(t){return qi(t,n)}if(!arguments.length)return l;var r;return f=null==(l=n)?zi:"function"==(r=typeof n)?t:"number"===r?(n=[n,n,n,n],e):e,i},i.round=function(n){return arguments.length?(c=n?Math.round:Number,i):c!=Number},i.sticky=function(n){return arguments.length?(h=n,o=null,i):h},i.ratio=function(n){return arguments.length?(p=n,i):p},i.mode=function(n){return arguments.length?(g=n+"",i):g},Vu(i,a)},Xo.random={normal:function(n,t){var e=arguments.length;return 2>e&&(t=1),1>e&&(n=0),function(){var e,r,u;do e=2*Math.random()-1,r=2*Math.random()-1,u=e*e+r*r;while(!u||u>1);return n+t*e*Math.sqrt(-2*Math.log(u)/u)}},logNormal:function(){var n=Xo.random.normal.apply(Xo,arguments);return function(){return Math.exp(n())}},bates:function(n){var t=Xo.random.irwinHall(n);return function(){return t()/n}},irwinHall:function(n){return function(){for(var t=0,e=0;n>e;e++)t+=Math.random();return t}}},Xo.scale={};var ls={floor:bt,ceil:bt};Xo.scale.linear=function(){return Hi([0,1],[0,1],fu,!1)};var fs={s:1,g:1,p:1,r:1,e:1};Xo.scale.log=function(){return $i(Xo.scale.linear().domain([0,1]),10,!0,[1,10])};var hs=Xo.format(".0e"),gs={floor:function(n){return-Math.ceil(-n)},ceil:function(n){return-Math.floor(-n)}};Xo.scale.pow=function(){return Bi(Xo.scale.linear(),1,[0,1])},Xo.scale.sqrt=function(){return Xo.scale.pow().exponent(.5)},Xo.scale.ordinal=function(){return Ji([],{t:"range",a:[[]]})},Xo.scale.category10=function(){return Xo.scale.ordinal().range(ps)},Xo.scale.category20=function(){return Xo.scale.ordinal().range(vs)},Xo.scale.category20b=function(){return Xo.scale.ordinal().range(ds)},Xo.scale.category20c=function(){return Xo.scale.ordinal().range(ms)};var ps=[2062260,16744206,2924588,14034728,9725885,9197131,14907330,8355711,12369186,1556175].map(ht),vs=[2062260,11454440,16744206,16759672,2924588,10018698,14034728,16750742,9725885,12955861,9197131,12885140,14907330,16234194,8355711,13092807,12369186,14408589,1556175,10410725].map(ht),ds=[3750777,5395619,7040719,10264286,6519097,9216594,11915115,13556636,9202993,12426809,15186514,15190932,8666169,11356490,14049643,15177372,8077683,10834324,13528509,14589654].map(ht),ms=[3244733,7057110,10406625,13032431,15095053,16616764,16625259,16634018,3253076,7652470,10607003,13101504,7695281,10394312,12369372,14342891,6513507,9868950,12434877,14277081].map(ht);Xo.scale.quantile=function(){return Gi([],[])
-},Xo.scale.quantize=function(){return Ki(0,1,[0,1])},Xo.scale.threshold=function(){return Qi([.5],[0,1])},Xo.scale.identity=function(){return no([0,1])},Xo.svg={},Xo.svg.arc=function(){function n(){var n=t.apply(this,arguments),i=e.apply(this,arguments),o=r.apply(this,arguments)+ys,a=u.apply(this,arguments)+ys,c=(o>a&&(c=o,o=a,a=c),a-o),s=Sa>c?"0":"1",l=Math.cos(o),f=Math.sin(o),h=Math.cos(a),g=Math.sin(a);return c>=xs?n?"M0,"+i+"A"+i+","+i+" 0 1,1 0,"+-i+"A"+i+","+i+" 0 1,1 0,"+i+"M0,"+n+"A"+n+","+n+" 0 1,0 0,"+-n+"A"+n+","+n+" 0 1,0 0,"+n+"Z":"M0,"+i+"A"+i+","+i+" 0 1,1 0,"+-i+"A"+i+","+i+" 0 1,1 0,"+i+"Z":n?"M"+i*l+","+i*f+"A"+i+","+i+" 0 "+s+",1 "+i*h+","+i*g+"L"+n*h+","+n*g+"A"+n+","+n+" 0 "+s+",0 "+n*l+","+n*f+"Z":"M"+i*l+","+i*f+"A"+i+","+i+" 0 "+s+",1 "+i*h+","+i*g+"L0,0"+"Z"}var t=to,e=eo,r=ro,u=uo;return n.innerRadius=function(e){return arguments.length?(t=_t(e),n):t},n.outerRadius=function(t){return arguments.length?(e=_t(t),n):e},n.startAngle=function(t){return arguments.length?(r=_t(t),n):r},n.endAngle=function(t){return arguments.length?(u=_t(t),n):u},n.centroid=function(){var n=(t.apply(this,arguments)+e.apply(this,arguments))/2,i=(r.apply(this,arguments)+u.apply(this,arguments))/2+ys;return[Math.cos(i)*n,Math.sin(i)*n]},n};var ys=-Ea,xs=ka-Aa;Xo.svg.line=function(){return io(bt)};var Ms=Xo.map({linear:oo,"linear-closed":ao,step:co,"step-before":so,"step-after":lo,basis:mo,"basis-open":yo,"basis-closed":xo,bundle:Mo,cardinal:go,"cardinal-open":fo,"cardinal-closed":ho,monotone:Eo});Ms.forEach(function(n,t){t.key=n,t.closed=/-closed$/.test(n)});var _s=[0,2/3,1/3,0],bs=[0,1/3,2/3,0],ws=[0,1/6,2/3,1/6];Xo.svg.line.radial=function(){var n=io(Ao);return n.radius=n.x,delete n.x,n.angle=n.y,delete n.y,n},so.reverse=lo,lo.reverse=so,Xo.svg.area=function(){return Co(bt)},Xo.svg.area.radial=function(){var n=Co(Ao);return n.radius=n.x,delete n.x,n.innerRadius=n.x0,delete n.x0,n.outerRadius=n.x1,delete n.x1,n.angle=n.y,delete n.y,n.startAngle=n.y0,delete n.y0,n.endAngle=n.y1,delete n.y1,n},Xo.svg.chord=function(){function n(n,a){var c=t(this,i,n,a),s=t(this,o,n,a);return"M"+c.p0+r(c.r,c.p1,c.a1-c.a0)+(e(c,s)?u(c.r,c.p1,c.r,c.p0):u(c.r,c.p1,s.r,s.p0)+r(s.r,s.p1,s.a1-s.a0)+u(s.r,s.p1,c.r,c.p0))+"Z"}function t(n,t,e,r){var u=t.call(n,e,r),i=a.call(n,u,r),o=c.call(n,u,r)+ys,l=s.call(n,u,r)+ys;return{r:i,a0:o,a1:l,p0:[i*Math.cos(o),i*Math.sin(o)],p1:[i*Math.cos(l),i*Math.sin(l)]}}function e(n,t){return n.a0==t.a0&&n.a1==t.a1}function r(n,t,e){return"A"+n+","+n+" 0 "+ +(e>Sa)+",1 "+t}function u(n,t,e,r){return"Q 0,0 "+r}var i=hr,o=gr,a=No,c=ro,s=uo;return n.radius=function(t){return arguments.length?(a=_t(t),n):a},n.source=function(t){return arguments.length?(i=_t(t),n):i},n.target=function(t){return arguments.length?(o=_t(t),n):o},n.startAngle=function(t){return arguments.length?(c=_t(t),n):c},n.endAngle=function(t){return arguments.length?(s=_t(t),n):s},n},Xo.svg.diagonal=function(){function n(n,u){var i=t.call(this,n,u),o=e.call(this,n,u),a=(i.y+o.y)/2,c=[i,{x:i.x,y:a},{x:o.x,y:a},o];return c=c.map(r),"M"+c[0]+"C"+c[1]+" "+c[2]+" "+c[3]}var t=hr,e=gr,r=Lo;return n.source=function(e){return arguments.length?(t=_t(e),n):t},n.target=function(t){return arguments.length?(e=_t(t),n):e},n.projection=function(t){return arguments.length?(r=t,n):r},n},Xo.svg.diagonal.radial=function(){var n=Xo.svg.diagonal(),t=Lo,e=n.projection;return n.projection=function(n){return arguments.length?e(zo(t=n)):t},n},Xo.svg.symbol=function(){function n(n,r){return(Ss.get(t.call(this,n,r))||Ro)(e.call(this,n,r))}var t=To,e=qo;return n.type=function(e){return arguments.length?(t=_t(e),n):t},n.size=function(t){return arguments.length?(e=_t(t),n):e},n};var Ss=Xo.map({circle:Ro,cross:function(n){var t=Math.sqrt(n/5)/2;return"M"+-3*t+","+-t+"H"+-t+"V"+-3*t+"H"+t+"V"+-t+"H"+3*t+"V"+t+"H"+t+"V"+3*t+"H"+-t+"V"+t+"H"+-3*t+"Z"},diamond:function(n){var t=Math.sqrt(n/(2*Cs)),e=t*Cs;return"M0,"+-t+"L"+e+",0"+" 0,"+t+" "+-e+",0"+"Z"},square:function(n){var t=Math.sqrt(n)/2;return"M"+-t+","+-t+"L"+t+","+-t+" "+t+","+t+" "+-t+","+t+"Z"},"triangle-down":function(n){var t=Math.sqrt(n/As),e=t*As/2;return"M0,"+e+"L"+t+","+-e+" "+-t+","+-e+"Z"},"triangle-up":function(n){var t=Math.sqrt(n/As),e=t*As/2;return"M0,"+-e+"L"+t+","+e+" "+-t+","+e+"Z"}});Xo.svg.symbolTypes=Ss.keys();var ks,Es,As=Math.sqrt(3),Cs=Math.tan(30*Na),Ns=[],Ls=0;Ns.call=da.call,Ns.empty=da.empty,Ns.node=da.node,Ns.size=da.size,Xo.transition=function(n){return arguments.length?ks?n.transition():n:xa.transition()},Xo.transition.prototype=Ns,Ns.select=function(n){var t,e,r,u=this.id,i=[];n=M(n);for(var o=-1,a=this.length;++o<a;){i.push(t=[]);for(var c=this[o],s=-1,l=c.length;++s<l;)(r=c[s])&&(e=n.call(r,r.__data__,s,o))?("__data__"in r&&(e.__data__=r.__data__),jo(e,s,u,r.__transition__[u]),t.push(e)):t.push(null)}return Do(i,u)},Ns.selectAll=function(n){var t,e,r,u,i,o=this.id,a=[];n=_(n);for(var c=-1,s=this.length;++c<s;)for(var l=this[c],f=-1,h=l.length;++f<h;)if(r=l[f]){i=r.__transition__[o],e=n.call(r,r.__data__,f,c),a.push(t=[]);for(var g=-1,p=e.length;++g<p;)(u=e[g])&&jo(u,g,o,i),t.push(u)}return Do(a,o)},Ns.filter=function(n){var t,e,r,u=[];"function"!=typeof n&&(n=q(n));for(var i=0,o=this.length;o>i;i++){u.push(t=[]);for(var e=this[i],a=0,c=e.length;c>a;a++)(r=e[a])&&n.call(r,r.__data__,a,i)&&t.push(r)}return Do(u,this.id)},Ns.tween=function(n,t){var e=this.id;return arguments.length<2?this.node().__transition__[e].tween.get(n):R(this,null==t?function(t){t.__transition__[e].tween.remove(n)}:function(r){r.__transition__[e].tween.set(n,t)})},Ns.attr=function(n,t){function e(){this.removeAttribute(a)}function r(){this.removeAttributeNS(a.space,a.local)}function u(n){return null==n?e:(n+="",function(){var t,e=this.getAttribute(a);return e!==n&&(t=o(e,n),function(n){this.setAttribute(a,t(n))})})}function i(n){return null==n?r:(n+="",function(){var t,e=this.getAttributeNS(a.space,a.local);return e!==n&&(t=o(e,n),function(n){this.setAttributeNS(a.space,a.local,t(n))})})}if(arguments.length<2){for(t in n)this.attr(t,n[t]);return this}var o="transform"==n?Ru:fu,a=Xo.ns.qualify(n);return Po(this,"attr."+n,t,a.local?i:u)},Ns.attrTween=function(n,t){function e(n,e){var r=t.call(this,n,e,this.getAttribute(u));return r&&function(n){this.setAttribute(u,r(n))}}function r(n,e){var r=t.call(this,n,e,this.getAttributeNS(u.space,u.local));return r&&function(n){this.setAttributeNS(u.space,u.local,r(n))}}var u=Xo.ns.qualify(n);return this.tween("attr."+n,u.local?r:e)},Ns.style=function(n,t,e){function r(){this.style.removeProperty(n)}function u(t){return null==t?r:(t+="",function(){var r,u=Go.getComputedStyle(this,null).getPropertyValue(n);return u!==t&&(r=fu(u,t),function(t){this.style.setProperty(n,r(t),e)})})}var i=arguments.length;if(3>i){if("string"!=typeof n){2>i&&(t="");for(e in n)this.style(e,n[e],t);return this}e=""}return Po(this,"style."+n,t,u)},Ns.styleTween=function(n,t,e){function r(r,u){var i=t.call(this,r,u,Go.getComputedStyle(this,null).getPropertyValue(n));return i&&function(t){this.style.setProperty(n,i(t),e)}}return arguments.length<3&&(e=""),this.tween("style."+n,r)},Ns.text=function(n){return Po(this,"text",n,Uo)},Ns.remove=function(){return this.each("end.transition",function(){var n;this.__transition__.count<2&&(n=this.parentNode)&&n.removeChild(this)})},Ns.ease=function(n){var t=this.id;return arguments.length<1?this.node().__transition__[t].ease:("function"!=typeof n&&(n=Xo.ease.apply(Xo,arguments)),R(this,function(e){e.__transition__[t].ease=n}))},Ns.delay=function(n){var t=this.id;return R(this,"function"==typeof n?function(e,r,u){e.__transition__[t].delay=+n.call(e,e.__data__,r,u)}:(n=+n,function(e){e.__transition__[t].delay=n}))},Ns.duration=function(n){var t=this.id;return R(this,"function"==typeof n?function(e,r,u){e.__transition__[t].duration=Math.max(1,n.call(e,e.__data__,r,u))}:(n=Math.max(1,n),function(e){e.__transition__[t].duration=n}))},Ns.each=function(n,t){var e=this.id;if(arguments.length<2){var r=Es,u=ks;ks=e,R(this,function(t,r,u){Es=t.__transition__[e],n.call(t,t.__data__,r,u)}),Es=r,ks=u}else R(this,function(r){var u=r.__transition__[e];(u.event||(u.event=Xo.dispatch("start","end"))).on(n,t)});return this},Ns.transition=function(){for(var n,t,e,r,u=this.id,i=++Ls,o=[],a=0,c=this.length;c>a;a++){o.push(n=[]);for(var t=this[a],s=0,l=t.length;l>s;s++)(e=t[s])&&(r=Object.create(e.__transition__[u]),r.delay+=r.duration,jo(e,s,i,r)),n.push(e)}return Do(o,i)},Xo.svg.axis=function(){function n(n){n.each(function(){var n,s=Xo.select(this),l=this.__chart__||e,f=this.__chart__=e.copy(),h=null==c?f.ticks?f.ticks.apply(f,a):f.domain():c,g=null==t?f.tickFormat?f.tickFormat.apply(f,a):bt:t,p=s.selectAll(".tick").data(h,f),v=p.enter().insert("g",".domain").attr("class","tick").style("opacity",Aa),d=Xo.transition(p.exit()).style("opacity",Aa).remove(),m=Xo.transition(p).style("opacity",1),y=Ri(f),x=s.selectAll(".domain").data([0]),M=(x.enter().append("path").attr("class","domain"),Xo.transition(x));v.append("line"),v.append("text");var _=v.select("line"),b=m.select("line"),w=p.select("text").text(g),S=v.select("text"),k=m.select("text");switch(r){case"bottom":n=Ho,_.attr("y2",u),S.attr("y",Math.max(u,0)+o),b.attr("x2",0).attr("y2",u),k.attr("x",0).attr("y",Math.max(u,0)+o),w.attr("dy",".71em").style("text-anchor","middle"),M.attr("d","M"+y[0]+","+i+"V0H"+y[1]+"V"+i);break;case"top":n=Ho,_.attr("y2",-u),S.attr("y",-(Math.max(u,0)+o)),b.attr("x2",0).attr("y2",-u),k.attr("x",0).attr("y",-(Math.max(u,0)+o)),w.attr("dy","0em").style("text-anchor","middle"),M.attr("d","M"+y[0]+","+-i+"V0H"+y[1]+"V"+-i);break;case"left":n=Fo,_.attr("x2",-u),S.attr("x",-(Math.max(u,0)+o)),b.attr("x2",-u).attr("y2",0),k.attr("x",-(Math.max(u,0)+o)).attr("y",0),w.attr("dy",".32em").style("text-anchor","end"),M.attr("d","M"+-i+","+y[0]+"H0V"+y[1]+"H"+-i);break;case"right":n=Fo,_.attr("x2",u),S.attr("x",Math.max(u,0)+o),b.attr("x2",u).attr("y2",0),k.attr("x",Math.max(u,0)+o).attr("y",0),w.attr("dy",".32em").style("text-anchor","start"),M.attr("d","M"+i+","+y[0]+"H0V"+y[1]+"H"+i)}if(f.rangeBand){var E=f,A=E.rangeBand()/2;l=f=function(n){return E(n)+A}}else l.rangeBand?l=f:d.call(n,f);v.call(n,l),m.call(n,f)})}var t,e=Xo.scale.linear(),r=zs,u=6,i=6,o=3,a=[10],c=null;return n.scale=function(t){return arguments.length?(e=t,n):e},n.orient=function(t){return arguments.length?(r=t in qs?t+"":zs,n):r},n.ticks=function(){return arguments.length?(a=arguments,n):a},n.tickValues=function(t){return arguments.length?(c=t,n):c},n.tickFormat=function(e){return arguments.length?(t=e,n):t},n.tickSize=function(t){var e=arguments.length;return e?(u=+t,i=+arguments[e-1],n):u},n.innerTickSize=function(t){return arguments.length?(u=+t,n):u},n.outerTickSize=function(t){return arguments.length?(i=+t,n):i},n.tickPadding=function(t){return arguments.length?(o=+t,n):o},n.tickSubdivide=function(){return arguments.length&&n},n};var zs="bottom",qs={top:1,right:1,bottom:1,left:1};Xo.svg.brush=function(){function n(i){i.each(function(){var i=Xo.select(this).style("pointer-events","all").style("-webkit-tap-highlight-color","rgba(0,0,0,0)").on("mousedown.brush",u).on("touchstart.brush",u),o=i.selectAll(".background").data([0]);o.enter().append("rect").attr("class","background").style("visibility","hidden").style("cursor","crosshair"),i.selectAll(".extent").data([0]).enter().append("rect").attr("class","extent").style("cursor","move");var a=i.selectAll(".resize").data(p,bt);a.exit().remove(),a.enter().append("g").attr("class",function(n){return"resize "+n}).style("cursor",function(n){return Ts[n]}).append("rect").attr("x",function(n){return/[ew]$/.test(n)?-3:null}).attr("y",function(n){return/^[ns]/.test(n)?-3:null}).attr("width",6).attr("height",6).style("visibility","hidden"),a.style("display",n.empty()?"none":null);var l,f=Xo.transition(i),h=Xo.transition(o);c&&(l=Ri(c),h.attr("x",l[0]).attr("width",l[1]-l[0]),e(f)),s&&(l=Ri(s),h.attr("y",l[0]).attr("height",l[1]-l[0]),r(f)),t(f)})}function t(n){n.selectAll(".resize").attr("transform",function(n){return"translate("+l[+/e$/.test(n)]+","+f[+/^s/.test(n)]+")"})}function e(n){n.select(".extent").attr("x",l[0]),n.selectAll(".extent,.n>rect,.s>rect").attr("width",l[1]-l[0])}function r(n){n.select(".extent").attr("y",f[0]),n.selectAll(".extent,.e>rect,.w>rect").attr("height",f[1]-f[0])}function u(){function u(){32==Xo.event.keyCode&&(C||(x=null,L[0]-=l[1],L[1]-=f[1],C=2),d())}function p(){32==Xo.event.keyCode&&2==C&&(L[0]+=l[1],L[1]+=f[1],C=0,d())}function v(){var n=Xo.mouse(_),u=!1;M&&(n[0]+=M[0],n[1]+=M[1]),C||(Xo.event.altKey?(x||(x=[(l[0]+l[1])/2,(f[0]+f[1])/2]),L[0]=l[+(n[0]<x[0])],L[1]=f[+(n[1]<x[1])]):x=null),E&&m(n,c,0)&&(e(S),u=!0),A&&m(n,s,1)&&(r(S),u=!0),u&&(t(S),w({type:"brush",mode:C?"move":"resize"}))}function m(n,t,e){var r,u,a=Ri(t),c=a[0],s=a[1],p=L[e],v=e?f:l,d=v[1]-v[0];return C&&(c-=p,s-=d+p),r=(e?g:h)?Math.max(c,Math.min(s,n[e])):n[e],C?u=(r+=p)+d:(x&&(p=Math.max(c,Math.min(s,2*x[e]-r))),r>p?(u=r,r=p):u=p),v[0]!=r||v[1]!=u?(e?o=null:i=null,v[0]=r,v[1]=u,!0):void 0}function y(){v(),S.style("pointer-events","all").selectAll(".resize").style("display",n.empty()?"none":null),Xo.select("body").style("cursor",null),z.on("mousemove.brush",null).on("mouseup.brush",null).on("touchmove.brush",null).on("touchend.brush",null).on("keydown.brush",null).on("keyup.brush",null),N(),w({type:"brushend"})}var x,M,_=this,b=Xo.select(Xo.event.target),w=a.of(_,arguments),S=Xo.select(_),k=b.datum(),E=!/^(n|s)$/.test(k)&&c,A=!/^(e|w)$/.test(k)&&s,C=b.classed("extent"),N=O(),L=Xo.mouse(_),z=Xo.select(Go).on("keydown.brush",u).on("keyup.brush",p);if(Xo.event.changedTouches?z.on("touchmove.brush",v).on("touchend.brush",y):z.on("mousemove.brush",v).on("mouseup.brush",y),S.interrupt().selectAll("*").interrupt(),C)L[0]=l[0]-L[0],L[1]=f[0]-L[1];else if(k){var q=+/w$/.test(k),T=+/^n/.test(k);M=[l[1-q]-L[0],f[1-T]-L[1]],L[0]=l[q],L[1]=f[T]}else Xo.event.altKey&&(x=L.slice());S.style("pointer-events","none").selectAll(".resize").style("display",null),Xo.select("body").style("cursor",b.style("cursor")),w({type:"brushstart"}),v()}var i,o,a=y(n,"brushstart","brush","brushend"),c=null,s=null,l=[0,0],f=[0,0],h=!0,g=!0,p=Rs[0];return n.event=function(n){n.each(function(){var n=a.of(this,arguments),t={x:l,y:f,i:i,j:o},e=this.__chart__||t;this.__chart__=t,ks?Xo.select(this).transition().each("start.brush",function(){i=e.i,o=e.j,l=e.x,f=e.y,n({type:"brushstart"})}).tween("brush:brush",function(){var e=hu(l,t.x),r=hu(f,t.y);return i=o=null,function(u){l=t.x=e(u),f=t.y=r(u),n({type:"brush",mode:"resize"})}}).each("end.brush",function(){i=t.i,o=t.j,n({type:"brush",mode:"resize"}),n({type:"brushend"})}):(n({type:"brushstart"}),n({type:"brush",mode:"resize"}),n({type:"brushend"}))})},n.x=function(t){return arguments.length?(c=t,p=Rs[!c<<1|!s],n):c},n.y=function(t){return arguments.length?(s=t,p=Rs[!c<<1|!s],n):s},n.clamp=function(t){return arguments.length?(c&&s?(h=!!t[0],g=!!t[1]):c?h=!!t:s&&(g=!!t),n):c&&s?[h,g]:c?h:s?g:null},n.extent=function(t){var e,r,u,a,h;return arguments.length?(c&&(e=t[0],r=t[1],s&&(e=e[0],r=r[0]),i=[e,r],c.invert&&(e=c(e),r=c(r)),e>r&&(h=e,e=r,r=h),(e!=l[0]||r!=l[1])&&(l=[e,r])),s&&(u=t[0],a=t[1],c&&(u=u[1],a=a[1]),o=[u,a],s.invert&&(u=s(u),a=s(a)),u>a&&(h=u,u=a,a=h),(u!=f[0]||a!=f[1])&&(f=[u,a])),n):(c&&(i?(e=i[0],r=i[1]):(e=l[0],r=l[1],c.invert&&(e=c.invert(e),r=c.invert(r)),e>r&&(h=e,e=r,r=h))),s&&(o?(u=o[0],a=o[1]):(u=f[0],a=f[1],s.invert&&(u=s.invert(u),a=s.invert(a)),u>a&&(h=u,u=a,a=h))),c&&s?[[e,u],[r,a]]:c?[e,r]:s&&[u,a])},n.clear=function(){return n.empty()||(l=[0,0],f=[0,0],i=o=null),n},n.empty=function(){return!!c&&l[0]==l[1]||!!s&&f[0]==f[1]},Xo.rebind(n,a,"on")};var Ts={n:"ns-resize",e:"ew-resize",s:"ns-resize",w:"ew-resize",nw:"nwse-resize",ne:"nesw-resize",se:"nwse-resize",sw:"nesw-resize"},Rs=[["n","e","s","w","nw","ne","se","sw"],["e","w"],["n","s"],[]],Ds=tc.format=ac.timeFormat,Ps=Ds.utc,Us=Ps("%Y-%m-%dT%H:%M:%S.%LZ");Ds.iso=Date.prototype.toISOString&&+new Date("2000-01-01T00:00:00.000Z")?Oo:Us,Oo.parse=function(n){var t=new Date(n);return isNaN(t)?null:t},Oo.toString=Us.toString,tc.second=Rt(function(n){return new ec(1e3*Math.floor(n/1e3))},function(n,t){n.setTime(n.getTime()+1e3*Math.floor(t))},function(n){return n.getSeconds()}),tc.seconds=tc.second.range,tc.seconds.utc=tc.second.utc.range,tc.minute=Rt(function(n){return new ec(6e4*Math.floor(n/6e4))},function(n,t){n.setTime(n.getTime()+6e4*Math.floor(t))},function(n){return n.getMinutes()}),tc.minutes=tc.minute.range,tc.minutes.utc=tc.minute.utc.range,tc.hour=Rt(function(n){var t=n.getTimezoneOffset()/60;return new ec(36e5*(Math.floor(n/36e5-t)+t))},function(n,t){n.setTime(n.getTime()+36e5*Math.floor(t))},function(n){return n.getHours()}),tc.hours=tc.hour.range,tc.hours.utc=tc.hour.utc.range,tc.month=Rt(function(n){return n=tc.day(n),n.setDate(1),n},function(n,t){n.setMonth(n.getMonth()+t)},function(n){return n.getMonth()}),tc.months=tc.month.range,tc.months.utc=tc.month.utc.range;var js=[1e3,5e3,15e3,3e4,6e4,3e5,9e5,18e5,36e5,108e5,216e5,432e5,864e5,1728e5,6048e5,2592e6,7776e6,31536e6],Hs=[[tc.second,1],[tc.second,5],[tc.second,15],[tc.second,30],[tc.minute,1],[tc.minute,5],[tc.minute,15],[tc.minute,30],[tc.hour,1],[tc.hour,3],[tc.hour,6],[tc.hour,12],[tc.day,1],[tc.day,2],[tc.week,1],[tc.month,1],[tc.month,3],[tc.year,1]],Fs=Ds.multi([[".%L",function(n){return n.getMilliseconds()}],[":%S",function(n){return n.getSeconds()}],["%I:%M",function(n){return n.getMinutes()}],["%I %p",function(n){return n.getHours()}],["%a %d",function(n){return n.getDay()&&1!=n.getDate()}],["%b %d",function(n){return 1!=n.getDate()}],["%B",function(n){return n.getMonth()}],["%Y",be]]),Os={range:function(n,t,e){return Xo.range(+n,+t,e).map(Io)},floor:bt,ceil:bt};Hs.year=tc.year,tc.scale=function(){return Yo(Xo.scale.linear(),Hs,Fs)};var Ys=Hs.map(function(n){return[n[0].utc,n[1]]}),Is=Ps.multi([[".%L",function(n){return n.getUTCMilliseconds()}],[":%S",function(n){return n.getUTCSeconds()}],["%I:%M",function(n){return n.getUTCMinutes()}],["%I %p",function(n){return n.getUTCHours()}],["%a %d",function(n){return n.getUTCDay()&&1!=n.getUTCDate()}],["%b %d",function(n){return 1!=n.getUTCDate()}],["%B",function(n){return n.getUTCMonth()}],["%Y",be]]);Ys.year=tc.year.utc,tc.scale.utc=function(){return Yo(Xo.scale.linear(),Ys,Is)},Xo.text=wt(function(n){return n.responseText}),Xo.json=function(n,t){return St(n,"application/json",Zo,t)},Xo.html=function(n,t){return St(n,"text/html",Vo,t)},Xo.xml=wt(function(n){return n.responseXML}),"function"==typeof define&&define.amd?define(Xo):"object"==typeof module&&module.exports?module.exports=Xo:this.d3=Xo}();
\ No newline at end of file
+// https://d3js.org Version 4.1.0. Copyright 2016 Mike Bostock.
+!function(t,n){"object"==typeof exports&&"undefined"!=typeof module?n(exports):"function"==typeof define&&define.amd?define(["exports"],n):n(t.d3=t.d3||{})}(this,function(t){"use strict";function n(t,n){return t<n?-1:t>n?1:t>=n?0:NaN}function e(t){return 1===t.length&&(t=r(t)),{left:function(n,e,r,i){for(null==r&&(r=0),null==i&&(i=n.length);r<i;){var o=r+i>>>1;t(n[o],e)<0?r=o+1:i=o}return r},right:function(n,e,r,i){for(null==r&&(r=0),null==i&&(i=n.length);r<i;){var o=r+i>>>1;t(n[o],e)>0?i=o:r=o+1}return r}}}function r(t){return function(e,r){return n(t(e),r)}}function i(t,n){return n<t?-1:n>t?1:n>=t?0:NaN}function o(t){return null===t?NaN:+t}function u(t,n){var e,r,i=t.length,u=0,a=0,c=-1,s=0;if(null==n)for(;++c<i;)isNaN(e=o(t[c]))||(r=e-u,u+=r/++s,a+=r*(e-u));else for(;++c<i;)isNaN(e=o(n(t[c],c,t)))||(r=e-u,u+=r/++s,a+=r*(e-u));if(s>1)return a/(s-1)}function a(t,n){var e=u(t,n);return e?Math.sqrt(e):e}function c(t,n){var e,r,i,o=-1,u=t.length;if(null==n){for(;++o<u;)if(null!=(r=t[o])&&r>=r){e=i=r;break}for(;++o<u;)null!=(r=t[o])&&(e>r&&(e=r),i<r&&(i=r))}else{for(;++o<u;)if(null!=(r=n(t[o],o,t))&&r>=r){e=i=r;break}for(;++o<u;)null!=(r=n(t[o],o,t))&&(e>r&&(e=r),i<r&&(i=r))}return[e,i]}function s(t){return function(){return t}}function f(t){return t}function l(t,n,e){t=+t,n=+n,e=(i=arguments.length)<2?(n=t,t=0,1):i<3?1:+e;for(var r=-1,i=0|Math.max(0,Math.ceil((n-t)/e)),o=new Array(i);++r<i;)o[r]=t+r*e;return o}function h(t,n,e){var r=p(t,n,e);return l(Math.ceil(t/r)*r,Math.floor(n/r)*r+r/2,r)}function p(t,n,e){var r=Math.abs(n-t)/Math.max(0,e),i=Math.pow(10,Math.floor(Math.log(r)/Math.LN10)),o=r/i;return o>=Pd?i*=10:o>=qd?i*=5:o>=Ld&&(i*=2),n<t?-i:i}function d(t){return Math.ceil(Math.log(t.length)/Math.LN2)+1}function v(){function t(t){var i,o,u=t.length,a=new Array(u);for(i=0;i<u;++i)a[i]=n(t[i],i,t);var c=e(a),s=c[0],f=c[1],l=r(a,s,f);Array.isArray(l)||(l=h(s,f,l));for(var p=l.length;l[0]<=s;)l.shift(),--p;for(;l[p-1]>=f;)l.pop(),--p;var d,v=new Array(p+1);for(i=0;i<=p;++i)d=v[i]=[],d.x0=i>0?l[i-1]:s,d.x1=i<p?l[i]:f;for(i=0;i<u;++i)o=a[i],s<=o&&o<=f&&v[Sd(l,o,0,p)].push(t[i]);return v}var n=f,e=c,r=d;return t.value=function(e){return arguments.length?(n="function"==typeof e?e:s(e),t):n},t.domain=function(n){return arguments.length?(e="function"==typeof n?n:s([n[0],n[1]]),t):e},t.thresholds=function(n){return arguments.length?(r="function"==typeof n?n:s(Array.isArray(n)?Cd.call(n):n),t):r},t}function _(t,n,e){if(null==e&&(e=o),r=t.length){if((n=+n)<=0||r<2)return+e(t[0],0,t);if(n>=1)return+e(t[r-1],r-1,t);var r,i=(r-1)*n,u=Math.floor(i),a=+e(t[u],u,t),c=+e(t[u+1],u+1,t);return a+(c-a)*(i-u)}}function y(t,e,r){return t=zd.call(t,o).sort(n),Math.ceil((r-e)/(2*(_(t,.75)-_(t,.25))*Math.pow(t.length,-1/3)))}function g(t,n,e){return Math.ceil((e-n)/(3.5*a(t)*Math.pow(t.length,-1/3)))}function m(t,n){var e,r,i=-1,o=t.length;if(null==n){for(;++i<o;)if(null!=(r=t[i])&&r>=r){e=r;break}for(;++i<o;)null!=(r=t[i])&&r>e&&(e=r)}else{for(;++i<o;)if(null!=(r=n(t[i],i,t))&&r>=r){e=r;break}for(;++i<o;)null!=(r=n(t[i],i,t))&&r>e&&(e=r)}return e}function x(t,n){var e,r=0,i=t.length,u=-1,a=i;if(null==n)for(;++u<i;)isNaN(e=o(t[u]))?--a:r+=e;else for(;++u<i;)isNaN(e=o(n(t[u],u,t)))?--a:r+=e;if(a)return r/a}function b(t,e){var r,i=[],u=t.length,a=-1;if(null==e)for(;++a<u;)isNaN(r=o(t[a]))||i.push(r);else for(;++a<u;)isNaN(r=o(e(t[a],a,t)))||i.push(r);return _(i.sort(n),.5)}function w(t){for(var n,e,r,i=t.length,o=-1,u=0;++o<i;)u+=t[o].length;for(e=new Array(u);--i>=0;)for(r=t[i],n=r.length;--n>=0;)e[--u]=r[n];return e}function M(t,n){var e,r,i=-1,o=t.length;if(null==n){for(;++i<o;)if(null!=(r=t[i])&&r>=r){e=r;break}for(;++i<o;)null!=(r=t[i])&&e>r&&(e=r)}else{for(;++i<o;)if(null!=(r=n(t[i],i,t))&&r>=r){e=r;break}for(;++i<o;)null!=(r=n(t[i],i,t))&&e>r&&(e=r)}return e}function T(t){for(var n=0,e=t.length-1,r=t[0],i=new Array(e<0?0:e);n<e;)i[n]=[r,r=t[++n]];return i}function k(t,n){for(var e=n.length,r=new Array(e);e--;)r[e]=t[n[e]];return r}function N(t,e){if(r=t.length){var r,i,o=0,u=0,a=t[u];for(e||(e=n);++o<r;)(e(i=t[o],a)<0||0!==e(a,a))&&(a=i,u=o);return 0===e(a,a)?u:void 0}}function S(t,n,e){for(var r,i,o=(null==e?t.length:e)-(n=null==n?0:+n);o;)i=Math.random()*o--|0,r=t[o+n],t[o+n]=t[i+n],t[i+n]=r;return t}function A(t,n){var e,r=0,i=t.length,o=-1;if(null==n)for(;++o<i;)(e=+t[o])&&(r+=e);else for(;++o<i;)(e=+n(t[o],o,t))&&(r+=e);return r}function E(t){if(!(i=t.length))return[];for(var n=-1,e=M(t,C),r=new Array(e);++n<e;)for(var i,o=-1,u=r[n]=new Array(i);++o<i;)u[o]=t[o][n];return r}function C(t){return t.length}function z(){return E(arguments)}function P(){}function q(t,n){var e=new P;if(t instanceof P)t.each(function(t,n){e.set(n,t)});else if(Array.isArray(t)){var r,i=-1,o=t.length;if(null==n)for(;++i<o;)e.set(i,t[i]);else for(;++i<o;)e.set(n(r=t[i],i,t),r)}else if(t)for(var u in t)e.set(u,t[u]);return e}function L(){function t(n,i,u,a){if(i>=o.length)return null!=r?r(n):null!=e?n.sort(e):n;for(var c,s,f,l=-1,h=n.length,p=o[i++],d=q(),v=u();++l<h;)(f=d.get(c=p(s=n[l])+""))?f.push(s):d.set(c,[s]);return d.each(function(n,e){a(v,e,t(n,i,u,a))}),v}function n(t,e){if(++e>o.length)return t;var i,a=u[e-1];return null!=r&&e>=o.length?i=t.entries():(i=[],t.each(function(t,r){i.push({key:r,values:n(t,e)})})),null!=a?i.sort(function(t,n){return a(t.key,n.key)}):i}var e,r,i,o=[],u=[];return i={object:function(n){return t(n,0,R,U)},map:function(n){return t(n,0,D,O)},entries:function(e){return n(t(e,0,D,O),0)},key:function(t){return o.push(t),i},sortKeys:function(t){return u[o.length-1]=t,i},sortValues:function(t){return e=t,i},rollup:function(t){return r=t,i}}}function R(){return{}}function U(t,n,e){t[n]=e}function D(){return q()}function O(t,n,e){t.set(n,e)}function F(){}function I(t,n){var e=new F;if(t instanceof F)t.each(function(t){e.add(t)});else if(t){var r=-1,i=t.length;if(null==n)for(;++r<i;)e.add(t[r]);else for(;++r<i;)e.add(n(t[r],r,t))}return e}function Y(t){var n=[];for(var e in t)n.push(e);return n}function B(t){var n=[];for(var e in t)n.push(t[e]);return n}function j(t){var n=[];for(var e in t)n.push({key:e,value:t[e]});return n}function H(t,n){return t=null==t?0:+t,n=null==n?1:+n,1===arguments.length?(n=t,t=0):n-=t,function(){return Math.random()*n+t}}function X(t,n){var e,r;return t=null==t?0:+t,n=null==n?1:+n,function(){var i;if(null!=e)i=e,e=null;else do e=2*Math.random()-1,i=2*Math.random()-1,r=e*e+i*i;while(!r||r>1);return t+n*i*Math.sqrt(-2*Math.log(r)/r)}}function V(){var t=X.apply(this,arguments);return function(){return Math.exp(t())}}function W(t){return function(){for(var n=0,e=0;e<t;++e)n+=Math.random();return n}}function $(t){var n=W(t);return function(){return n()/t}}function Z(t){return function(){return-Math.log(1-Math.random())/t}}function G(t){return+t}function J(t){return t*t}function Q(t){return t*(2-t)}function K(t){return((t*=2)<=1?t*t:--t*(2-t)+1)/2}function tt(t){return t*t*t}function nt(t){return--t*t*t+1}function et(t){return((t*=2)<=1?t*t*t:(t-=2)*t*t+2)/2}function rt(t){return 1-Math.cos(t*Bd)}function it(t){return Math.sin(t*Bd)}function ot(t){return(1-Math.cos(Yd*t))/2}function ut(t){return Math.pow(2,10*t-10)}function at(t){return 1-Math.pow(2,-10*t)}function ct(t){return((t*=2)<=1?Math.pow(2,10*t-10):2-Math.pow(2,10-10*t))/2}function st(t){return 1-Math.sqrt(1-t*t)}function ft(t){return Math.sqrt(1- --t*t)}function lt(t){return((t*=2)<=1?1-Math.sqrt(1-t*t):Math.sqrt(1-(t-=2)*t)+1)/2}function ht(t){return 1-pt(1-t)}function pt(t){return(t=+t)<jd?Qd*t*t:t<Xd?Qd*(t-=Hd)*t+Vd:t<$d?Qd*(t-=Wd)*t+Zd:Qd*(t-=Gd)*t+Jd}function dt(t){return((t*=2)<=1?1-pt(1-t):pt(t-1)+1)/2}function vt(t){for(var n,e=-1,r=t.length,i=t[r-1],o=0;++e<r;)n=i,i=t[e],o+=n[1]*i[0]-n[0]*i[1];return o/2}function _t(t){for(var n,e,r=-1,i=t.length,o=0,u=0,a=t[i-1],c=0;++r<i;)n=a,a=t[r],c+=e=n[0]*a[1]-a[0]*n[1],o+=(n[0]+a[0])*e,u+=(n[1]+a[1])*e;return c*=3,[o/c,u/c]}function yt(t,n,e){return(n[0]-t[0])*(e[1]-t[1])-(n[1]-t[1])*(e[0]-t[0])}function gt(t,n){return t[0]-n[0]||t[1]-n[1]}function mt(t){for(var n=t.length,e=[0,1],r=2,i=2;i<n;++i){for(;r>1&&yt(t[e[r-2]],t[e[r-1]],t[i])<=0;)--r;e[r++]=i}return e.slice(0,r)}function xt(t){if((e=t.length)<3)return null;var n,e,r=new Array(e),i=new Array(e);for(n=0;n<e;++n)r[n]=[+t[n][0],+t[n][1],n];for(r.sort(gt),n=0;n<e;++n)i[n]=[r[n][0],-r[n][1]];var o=mt(r),u=mt(i),a=u[0]===o[0],c=u[u.length-1]===o[o.length-1],s=[];for(n=o.length-1;n>=0;--n)s.push(t[r[o[n]][2]]);for(n=+a;n<u.length-c;++n)s.push(t[r[u[n]][2]]);return s}function bt(t,n){for(var e,r,i=t.length,o=t[i-1],u=n[0],a=n[1],c=o[0],s=o[1],f=!1,l=0;l<i;++l)o=t[l],e=o[0],r=o[1],r>a!=s>a&&u<(c-e)*(a-r)/(s-r)+e&&(f=!f),c=e,s=r;return f}function wt(t){for(var n,e,r=-1,i=t.length,o=t[i-1],u=o[0],a=o[1],c=0;++r<i;)n=u,e=a,o=t[r],u=o[0],a=o[1],n-=u,e-=a,c+=Math.sqrt(n*n+e*e);return c}function Mt(){this._x0=this._y0=this._x1=this._y1=null,this._=[]}function Tt(){return new Mt}function kt(t){var n=+this._x.call(null,t),e=+this._y.call(null,t);return Nt(this.cover(n,e),n,e,t)}function Nt(t,n,e,r){if(isNaN(n)||isNaN(e))return t;var i,o,u,a,c,s,f,l,h,p=t._root,d={data:r},v=t._x0,_=t._y0,y=t._x1,g=t._y1;if(!p)return t._root=d,t;for(;p.length;)if((s=n>=(o=(v+y)/2))?v=o:y=o,(f=e>=(u=(_+g)/2))?_=u:g=u,i=p,!(p=p[l=f<<1|s]))return i[l]=d,t;if(a=+t._x.call(null,p.data),c=+t._y.call(null,p.data),n===a&&e===c)return d.next=p,i?i[l]=d:t._root=d,t;do i=i?i[l]=new Array(4):t._root=new Array(4),(s=n>=(o=(v+y)/2))?v=o:y=o,(f=e>=(u=(_+g)/2))?_=u:g=u;while((l=f<<1|s)===(h=(c>=u)<<1|a>=o));return i[h]=p,i[l]=d,t}function St(t){var n,e,r,i,o=t.length,u=new Array(o),a=new Array(o),c=1/0,s=1/0,f=-(1/0),l=-(1/0);for(e=0;e<o;++e)isNaN(r=+this._x.call(null,n=t[e]))||isNaN(i=+this._y.call(null,n))||(u[e]=r,a[e]=i,r<c&&(c=r),r>f&&(f=r),i<s&&(s=i),i>l&&(l=i));for(f<c&&(c=this._x0,f=this._x1),l<s&&(s=this._y0,l=this._y1),this.cover(c,s).cover(f,l),e=0;e<o;++e)Nt(this,u[e],a[e],t[e]);return this}function At(t,n){if(isNaN(t=+t)||isNaN(n=+n))return this;var e=this._x0,r=this._y0,i=this._x1,o=this._y1;if(isNaN(e))i=(e=Math.floor(t))+1,o=(r=Math.floor(n))+1;else{if(!(e>t||t>i||r>n||n>o))return this;var u,a,c=i-e,s=this._root;switch(a=(n<(r+o)/2)<<1|t<(e+i)/2){case 0:do u=new Array(4),u[a]=s,s=u;while(c*=2,i=e+c,o=r+c,t>i||n>o);break;case 1:do u=new Array(4),u[a]=s,s=u;while(c*=2,e=i-c,o=r+c,e>t||n>o);break;case 2:do u=new Array(4),u[a]=s,s=u;while(c*=2,i=e+c,r=o-c,t>i||r>n);break;case 3:do u=new Array(4),u[a]=s,s=u;while(c*=2,e=i-c,r=o-c,e>t||r>n)}this._root&&this._root.length&&(this._root=s)}return this._x0=e,this._y0=r,this._x1=i,this._y1=o,this}function Et(){var t=[];return this.visit(function(n){if(!n.length)do t.push(n.data);while(n=n.next)}),t}function Ct(t){return arguments.length?this.cover(+t[0][0],+t[0][1]).cover(+t[1][0],+t[1][1]):isNaN(this._x0)?void 0:[[this._x0,this._y0],[this._x1,this._y1]]}function zt(t,n,e,r,i){this.node=t,this.x0=n,this.y0=e,this.x1=r,this.y1=i}function Pt(t,n,e){var r,i,o,u,a,c,s,f=this._x0,l=this._y0,h=this._x1,p=this._y1,d=[],v=this._root;for(v&&d.push(new zt(v,f,l,h,p)),null==e?e=1/0:(f=t-e,l=n-e,h=t+e,p=n+e,e*=e);c=d.pop();)if(!(!(v=c.node)||(i=c.x0)>h||(o=c.y0)>p||(u=c.x1)<f||(a=c.y1)<l))if(v.length){var _=(i+u)/2,y=(o+a)/2;d.push(new zt(v[3],_,y,u,a),new zt(v[2],i,y,_,a),new zt(v[1],_,o,u,y),new zt(v[0],i,o,_,y)),(s=(n>=y)<<1|t>=_)&&(c=d[d.length-1],d[d.length-1]=d[d.length-1-s],d[d.length-1-s]=c)}else{var g=t-+this._x.call(null,v.data),m=n-+this._y.call(null,v.data),x=g*g+m*m;if(x<e){var b=Math.sqrt(e=x);f=t-b,l=n-b,h=t+b,p=n+b,r=v.data}}return r}function qt(t){if(isNaN(o=+this._x.call(null,t))||isNaN(u=+this._y.call(null,t)))return this;var n,e,r,i,o,u,a,c,s,f,l,h,p=this._root,d=this._x0,v=this._y0,_=this._x1,y=this._y1;if(!p)return this;if(p.length)for(;;){if((s=o>=(a=(d+_)/2))?d=a:_=a,(f=u>=(c=(v+y)/2))?v=c:y=c,n=p,!(p=p[l=f<<1|s]))return this;if(!p.length)break;(n[l+1&3]||n[l+2&3]||n[l+3&3])&&(e=n,h=l)}for(;p.data!==t;)if(r=p,!(p=p.next))return this;return(i=p.next)&&delete p.next,r?(i?r.next=i:delete r.next,this):n?(i?n[l]=i:delete n[l],(p=n[0]||n[1]||n[2]||n[3])&&p===(n[3]||n[2]||n[1]||n[0])&&!p.length&&(e?e[h]=p:this._root=p),this):(this._root=i,this)}function Lt(t){for(var n=0,e=t.length;n<e;++n)this.remove(t[n]);return this}function Rt(){return this._root}function Ut(){var t=0;return this.visit(function(n){if(!n.length)do++t;while(n=n.next)}),t}function Dt(t){var n,e,r,i,o,u,a=[],c=this._root;for(c&&a.push(new zt(c,this._x0,this._y0,this._x1,this._y1));n=a.pop();)if(!t(c=n.node,r=n.x0,i=n.y0,o=n.x1,u=n.y1)&&c.length){var s=(r+o)/2,f=(i+u)/2;(e=c[3])&&a.push(new zt(e,s,f,o,u)),(e=c[2])&&a.push(new zt(e,r,f,s,u)),(e=c[1])&&a.push(new zt(e,s,i,o,f)),(e=c[0])&&a.push(new zt(e,r,i,s,f))}return this}function Ot(t){var n,e=[],r=[];for(this._root&&e.push(new zt(this._root,this._x0,this._y0,this._x1,this._y1));n=e.pop();){var i=n.node;if(i.length){var o,u=n.x0,a=n.y0,c=n.x1,s=n.y1,f=(u+c)/2,l=(a+s)/2;(o=i[0])&&e.push(new zt(o,u,a,f,l)),(o=i[1])&&e.push(new zt(o,f,a,c,l)),(o=i[2])&&e.push(new zt(o,u,l,f,s)),(o=i[3])&&e.push(new zt(o,f,l,c,s))}r.push(n)}for(;n=r.pop();)t(n.node,n.x0,n.y0,n.x1,n.y1);return this}function Ft(t){return t[0]}function It(t){return arguments.length?(this._x=t,this):this._x}function Yt(t){return t[1]}function Bt(t){return arguments.length?(this._y=t,this):this._y}function jt(t,n,e){var r=new Ht(null==n?Ft:n,null==e?Yt:e,NaN,NaN,NaN,NaN);return null==t?r:r.addAll(t)}function Ht(t,n,e,r,i,o){this._x=t,this._y=n,this._x0=e,this._y0=r,this._x1=i,this._y1=o,this._root=void 0}function Xt(t){for(var n={data:t.data},e=n;t=t.next;)e=e.next={data:t.data};return n}function Vt(t){if(!(t>=1))throw new Error;this._size=t,this._call=this._error=null,this._tasks=[],this._data=[],this._waiting=this._active=this._ended=this._start=0}function Wt(t){if(!t._start)try{$t(t)}catch(n){t._tasks[t._ended+t._active-1]&&Gt(t,n)}}function $t(t){for(;t._start=t._waiting&&t._active<t._size;){var n=t._ended+t._active,e=t._tasks[n],r=e.length-1,i=e[r];e[r]=Zt(t,n),--t._waiting,++t._active,e=i.apply(null,e),t._tasks[n]&&(t._tasks[n]=e||vv)}}function Zt(t,n){return function(e,r){t._tasks[n]&&(--t._active,++t._ended,t._tasks[n]=null,null==t._error&&(null!=e?Gt(t,e):(t._data[n]=r,t._waiting?Wt(t):Jt(t))))}}function Gt(t,n){var e,r=t._tasks.length;for(t._error=n,t._data=void 0,t._waiting=NaN;--r>=0;)if((e=t._tasks[r])&&(t._tasks[r]=null,e.abort))try{e.abort()}catch(n){}t._active=NaN,Jt(t)}function Jt(t){!t._active&&t._call&&t._call(t._error,t._data)}function Qt(t){return new Vt(arguments.length?+t:1/0)}function Kt(t){return function(){return t}}function tn(t){return t.innerRadius}function nn(t){return t.outerRadius}function en(t){return t.startAngle}function rn(t){return t.endAngle}function on(t){return t&&t.padAngle}function un(t){return t>=1?gv:t<=-1?-gv:Math.asin(t)}function an(t,n,e,r,i,o,u,a){var c=e-t,s=r-n,f=u-i,l=a-o,h=(f*(n-o)-l*(t-i))/(l*c-f*s);return[t+h*c,n+h*s]}function cn(t,n,e,r,i,o,u){var a=t-e,c=n-r,s=(u?o:-o)/Math.sqrt(a*a+c*c),f=s*c,l=-s*a,h=t+f,p=n+l,d=e+f,v=r+l,_=(h+d)/2,y=(p+v)/2,g=d-h,m=v-p,x=g*g+m*m,b=i-o,w=h*v-d*p,M=(m<0?-1:1)*Math.sqrt(Math.max(0,b*b*x-w*w)),T=(w*m-g*M)/x,k=(-w*g-m*M)/x,N=(w*m+g*M)/x,S=(-w*g+m*M)/x,A=T-_,E=k-y,C=N-_,z=S-y;return A*A+E*E>C*C+z*z&&(T=N,k=S),{cx:T,cy:k,x01:-f,y01:-l,x11:T*(i/b-1),y11:k*(i/b-1)}}function sn(){function t(){var t,s,f=+n.apply(this,arguments),l=+e.apply(this,arguments),h=o.apply(this,arguments)-gv,p=u.apply(this,arguments)-gv,d=Math.abs(p-h),v=p>h;if(c||(c=t=Tt()),l<f&&(s=l,l=f,f=s),l>_v)if(d>mv-_v)c.moveTo(l*Math.cos(h),l*Math.sin(h)),c.arc(0,0,l,h,p,!v),f>_v&&(c.moveTo(f*Math.cos(p),f*Math.sin(p)),c.arc(0,0,f,p,h,v));else{var _,y,g=h,m=p,x=h,b=p,w=d,M=d,T=a.apply(this,arguments)/2,k=T>_v&&(i?+i.apply(this,arguments):Math.sqrt(f*f+l*l)),N=Math.min(Math.abs(l-f)/2,+r.apply(this,arguments)),S=N,A=N;if(k>_v){var E=un(k/f*Math.sin(T)),C=un(k/l*Math.sin(T));(w-=2*E)>_v?(E*=v?1:-1,x+=E,b-=E):(w=0,x=b=(h+p)/2),(M-=2*C)>_v?(C*=v?1:-1,g+=C,m-=C):(M=0,g=m=(h+p)/2)}var z=l*Math.cos(g),P=l*Math.sin(g),q=f*Math.cos(b),L=f*Math.sin(b);if(N>_v){var R=l*Math.cos(m),U=l*Math.sin(m),D=f*Math.cos(x),O=f*Math.sin(x);if(d<yv){var F=w>_v?an(z,P,D,O,R,U,q,L):[q,L],I=z-F[0],Y=P-F[1],B=R-F[0],j=U-F[1],H=1/Math.sin(Math.acos((I*B+Y*j)/(Math.sqrt(I*I+Y*Y)*Math.sqrt(B*B+j*j)))/2),X=Math.sqrt(F[0]*F[0]+F[1]*F[1]);S=Math.min(N,(f-X)/(H-1)),A=Math.min(N,(l-X)/(H+1))}}M>_v?A>_v?(_=cn(D,O,z,P,l,A,v),y=cn(R,U,q,L,l,A,v),c.moveTo(_.cx+_.x01,_.cy+_.y01),A<N?c.arc(_.cx,_.cy,A,Math.atan2(_.y01,_.x01),Math.atan2(y.y01,y.x01),!v):(c.arc(_.cx,_.cy,A,Math.atan2(_.y01,_.x01),Math.atan2(_.y11,_.x11),!v),c.arc(0,0,l,Math.atan2(_.cy+_.y11,_.cx+_.x11),Math.atan2(y.cy+y.y11,y.cx+y.x11),!v),c.arc(y.cx,y.cy,A,Math.atan2(y.y11,y.x11),Math.atan2(y.y01,y.x01),!v))):(c.moveTo(z,P),c.arc(0,0,l,g,m,!v)):c.moveTo(z,P),f>_v&&w>_v?S>_v?(_=cn(q,L,R,U,f,-S,v),y=cn(z,P,D,O,f,-S,v),c.lineTo(_.cx+_.x01,_.cy+_.y01),S<N?c.arc(_.cx,_.cy,S,Math.atan2(_.y01,_.x01),Math.atan2(y.y01,y.x01),!v):(c.arc(_.cx,_.cy,S,Math.atan2(_.y01,_.x01),Math.atan2(_.y11,_.x11),!v),c.arc(0,0,f,Math.atan2(_.cy+_.y11,_.cx+_.x11),Math.atan2(y.cy+y.y11,y.cx+y.x11),v),c.arc(y.cx,y.cy,S,Math.atan2(y.y11,y.x11),Math.atan2(y.y01,y.x01),!v))):c.arc(0,0,f,b,x,v):c.lineTo(q,L)}else c.moveTo(0,0);if(c.closePath(),t)return c=null,t+""||null}var n=tn,e=nn,r=Kt(0),i=null,o=en,u=rn,a=on,c=null;return t.centroid=function(){var t=(+n.apply(this,arguments)+ +e.apply(this,arguments))/2,r=(+o.apply(this,arguments)+ +u.apply(this,arguments))/2-yv/2;return[Math.cos(r)*t,Math.sin(r)*t]},t.innerRadius=function(e){return arguments.length?(n="function"==typeof e?e:Kt(+e),t):n},t.outerRadius=function(n){return arguments.length?(e="function"==typeof n?n:Kt(+n),t):e},t.cornerRadius=function(n){return arguments.length?(r="function"==typeof n?n:Kt(+n),t):r},t.padRadius=function(n){return arguments.length?(i=null==n?null:"function"==typeof n?n:Kt(+n),t):i},t.startAngle=function(n){return arguments.length?(o="function"==typeof n?n:Kt(+n),t):o},t.endAngle=function(n){return arguments.length?(u="function"==typeof n?n:Kt(+n),t):u},t.padAngle=function(n){return arguments.length?(a="function"==typeof n?n:Kt(+n),t):a},t.context=function(n){return arguments.length?(c=null==n?null:n,t):c},t}function fn(t){this._context=t}function ln(t){return new fn(t)}function hn(t){return t[0]}function pn(t){return t[1]}function dn(){function t(t){var a,c,s,f=t.length,l=!1;for(null==i&&(u=o(s=Tt())),a=0;a<=f;++a)!(a<f&&r(c=t[a],a,t))===l&&((l=!l)?u.lineStart():u.lineEnd()),l&&u.point(+n(c,a,t),+e(c,a,t));if(s)return u=null,s+""||null}var n=hn,e=pn,r=Kt(!0),i=null,o=ln,u=null;return t.x=function(e){return arguments.length?(n="function"==typeof e?e:Kt(+e),t):n},t.y=function(n){return arguments.length?(e="function"==typeof n?n:Kt(+n),t):e},t.defined=function(n){return arguments.length?(r="function"==typeof n?n:Kt(!!n),t):r},t.curve=function(n){return arguments.length?(o=n,null!=i&&(u=o(i)),t):o},t.context=function(n){return arguments.length?(null==n?i=u=null:u=o(i=n),t):i},t}function vn(){function t(t){var n,f,l,h,p,d=t.length,v=!1,_=new Array(d),y=new Array(d);for(null==a&&(s=c(p=Tt())),n=0;n<=d;++n){if(!(n<d&&u(h=t[n],n,t))===v)if(v=!v)f=n,s.areaStart(),s.lineStart();else{for(s.lineEnd(),s.lineStart(),l=n-1;l>=f;--l)s.point(_[l],y[l]);s.lineEnd(),s.areaEnd()}v&&(_[n]=+e(h,n,t),y[n]=+i(h,n,t),s.point(r?+r(h,n,t):_[n],o?+o(h,n,t):y[n]))}if(p)return s=null,p+""||null}function n(){return dn().defined(u).curve(c).context(a)}var e=hn,r=null,i=Kt(0),o=pn,u=Kt(!0),a=null,c=ln,s=null;return t.x=function(n){return arguments.length?(e="function"==typeof n?n:Kt(+n),r=null,t):e},t.x0=function(n){return arguments.length?(e="function"==typeof n?n:Kt(+n),t):e},t.x1=function(n){return arguments.length?(r=null==n?null:"function"==typeof n?n:Kt(+n),t):r},t.y=function(n){return arguments.length?(i="function"==typeof n?n:Kt(+n),o=null,t):i},t.y0=function(n){return arguments.length?(i="function"==typeof n?n:Kt(+n),t):i},t.y1=function(n){return arguments.length?(o=null==n?null:"function"==typeof n?n:Kt(+n),t):o},t.lineX0=t.lineY0=function(){return n().x(e).y(i)},t.lineY1=function(){return n().x(e).y(o)},t.lineX1=function(){return n().x(r).y(i)},t.defined=function(n){return arguments.length?(u="function"==typeof n?n:Kt(!!n),t):u},t.curve=function(n){return arguments.length?(c=n,null!=a&&(s=c(a)),t):c},t.context=function(n){return arguments.length?(null==n?a=s=null:s=c(a=n),t):a},t}function _n(t,n){return n<t?-1:n>t?1:n>=t?0:NaN}function yn(t){return t}function gn(){function t(t){var a,c,s,f,l,h=t.length,p=0,d=new Array(h),v=new Array(h),_=+i.apply(this,arguments),y=Math.min(mv,Math.max(-mv,o.apply(this,arguments)-_)),g=Math.min(Math.abs(y)/h,u.apply(this,arguments)),m=g*(y<0?-1:1);for(a=0;a<h;++a)(l=v[d[a]=a]=+n(t[a],a,t))>0&&(p+=l);for(null!=e?d.sort(function(t,n){return e(v[t],v[n])}):null!=r&&d.sort(function(n,e){return r(t[n],t[e])}),a=0,s=p?(y-h*m)/p:0;a<h;++a,_=f)c=d[a],l=v[c],f=_+(l>0?l*s:0)+m,v[c]={data:t[c],index:a,value:l,startAngle:_,endAngle:f,padAngle:g};return v}var n=yn,e=_n,r=null,i=Kt(0),o=Kt(mv),u=Kt(0);return t.value=function(e){return arguments.length?(n="function"==typeof e?e:Kt(+e),t):n},t.sortValues=function(n){return arguments.length?(e=n,r=null,t):e},t.sort=function(n){return arguments.length?(r=n,e=null,t):r},t.startAngle=function(n){return arguments.length?(i="function"==typeof n?n:Kt(+n),t):i},t.endAngle=function(n){return arguments.length?(o="function"==typeof n?n:Kt(+n),t):o},t.padAngle=function(n){return arguments.length?(u="function"==typeof n?n:Kt(+n),t):u},t}function mn(t){this._curve=t}function xn(t){function n(n){return new mn(t(n))}return n._curve=t,n}function bn(t){var n=t.curve;return t.angle=t.x,delete t.x,t.radius=t.y,delete t.y,t.curve=function(t){return arguments.length?n(xn(t)):n()._curve},t}function wn(){return bn(dn().curve(xv))}function Mn(){var t=vn().curve(xv),n=t.curve,e=t.lineX0,r=t.lineX1,i=t.lineY0,o=t.lineY1;return t.angle=t.x,delete t.x,t.startAngle=t.x0,delete t.x0,t.endAngle=t.x1,delete t.x1,t.radius=t.y,delete t.y,t.innerRadius=t.y0,delete t.y0,t.outerRadius=t.y1,delete t.y1,t.lineStartAngle=function(){return bn(e())},delete t.lineX0,t.lineEndAngle=function(){return bn(r())},delete t.lineX1,t.lineInnerRadius=function(){return bn(i())},delete t.lineY0,t.lineOuterRadius=function(){return bn(o())},delete t.lineY1,t.curve=function(t){return arguments.length?n(xn(t)):n()._curve},t}function Tn(){function t(){var t;if(r||(r=t=Tt()),n.apply(this,arguments).draw(r,+e.apply(this,arguments)),t)return r=null,t+""||null}var n=Kt(bv),e=Kt(64),r=null;return t.type=function(e){return arguments.length?(n="function"==typeof e?e:Kt(e),t):n},t.size=function(n){return arguments.length?(e="function"==typeof n?n:Kt(+n),t):e},t.context=function(n){return arguments.length?(r=null==n?null:n,t):r},t}function kn(){}function Nn(t,n,e){t._context.bezierCurveTo((2*t._x0+t._x1)/3,(2*t._y0+t._y1)/3,(t._x0+2*t._x1)/3,(t._y0+2*t._y1)/3,(t._x0+4*t._x1+n)/6,(t._y0+4*t._y1+e)/6)}function Sn(t){this._context=t}function An(t){return new Sn(t)}function En(t){this._context=t}function Cn(t){return new En(t)}function zn(t){this._context=t}function Pn(t){return new zn(t)}function qn(t,n){this._basis=new Sn(t),this._beta=n}function Ln(t,n,e){t._context.bezierCurveTo(t._x1+t._k*(t._x2-t._x0),t._y1+t._k*(t._y2-t._y0),t._x2+t._k*(t._x1-n),t._y2+t._k*(t._y1-e),t._x2,t._y2)}function Rn(t,n){this._context=t,this._k=(1-n)/6}function Un(t,n){this._context=t,this._k=(1-n)/6}function Dn(t,n){this._context=t,this._k=(1-n)/6}function On(t,n,e){var r=t._x1,i=t._y1,o=t._x2,u=t._y2;if(t._l01_a>_v){var a=2*t._l01_2a+3*t._l01_a*t._l12_a+t._l12_2a,c=3*t._l01_a*(t._l01_a+t._l12_a);r=(r*a-t._x0*t._l12_2a+t._x2*t._l01_2a)/c,i=(i*a-t._y0*t._l12_2a+t._y2*t._l01_2a)/c}if(t._l23_a>_v){var s=2*t._l23_2a+3*t._l23_a*t._l12_a+t._l12_2a,f=3*t._l23_a*(t._l23_a+t._l12_a);o=(o*s+t._x1*t._l23_2a-n*t._l12_2a)/f,u=(u*s+t._y1*t._l23_2a-e*t._l12_2a)/f}t._context.bezierCurveTo(r,i,o,u,t._x2,t._y2)}function Fn(t,n){this._context=t,this._alpha=n}function In(t,n){this._context=t,this._alpha=n}function Yn(t,n){this._context=t,this._alpha=n}function Bn(t){this._context=t}function jn(t){return new Bn(t)}function Hn(t){return t<0?-1:1}function Xn(t,n,e){var r=t._x1-t._x0,i=n-t._x1,o=(t._y1-t._y0)/(r||i<0&&-0),u=(e-t._y1)/(i||r<0&&-0),a=(o*i+u*r)/(r+i);return(Hn(o)+Hn(u))*Math.min(Math.abs(o),Math.abs(u),.5*Math.abs(a))||0}function Vn(t,n){var e=t._x1-t._x0;return e?(3*(t._y1-t._y0)/e-n)/2:n}function Wn(t,n,e){var r=t._x0,i=t._y0,o=t._x1,u=t._y1,a=(o-r)/3;t._context.bezierCurveTo(r+a,i+a*n,o-a,u-a*e,o,u)}function $n(t){this._context=t}function Zn(t){this._context=new Gn(t)}function Gn(t){this._context=t}function Jn(t){return new $n(t)}function Qn(t){return new Zn(t)}function Kn(t){this._context=t}function te(t){var n,e,r=t.length-1,i=new Array(r),o=new Array(r),u=new Array(r);for(i[0]=0,o[0]=2,u[0]=t[0]+2*t[1],n=1;n<r-1;++n)i[n]=1,o[n]=4,u[n]=4*t[n]+2*t[n+1];for(i[r-1]=2,o[r-1]=7,u[r-1]=8*t[r-1]+t[r],n=1;n<r;++n)e=i[n]/o[n-1],o[n]-=e,u[n]-=e*u[n-1];for(i[r-1]=u[r-1]/o[r-1],n=r-2;n>=0;--n)i[n]=(u[n]-i[n+1])/o[n];for(o[r-1]=(t[r]+i[r-1])/2,n=0;n<r-1;++n)o[n]=2*t[n+1]-i[n+1];return[i,o]}function ne(t){return new Kn(t)}function ee(t,n){this._context=t,this._t=n}function re(t){return new ee(t,.5)}function ie(t){return new ee(t,0)}function oe(t){return new ee(t,1)}function ue(t,n){if((r=t.length)>1)for(var e,r,i=1,o=t[n[0]],u=o.length;i<r;++i){e=o,o=t[n[i]];for(var a=0;a<u;++a)o[a][1]+=o[a][0]=isNaN(e[a][1])?e[a][0]:e[a][1]}}function ae(t){for(var n=t.length,e=new Array(n);--n>=0;)e[n]=n;return e}function ce(t,n){return t[n]}function se(){function t(t){var o,u,a=n.apply(this,arguments),c=t.length,s=a.length,f=new Array(s);for(o=0;o<s;++o){for(var l,h=a[o],p=f[o]=new Array(c),d=0;d<c;++d)p[d]=l=[0,+i(t[d],h,d,t)],l.data=t[d];p.key=h}for(o=0,u=e(f);o<s;++o)f[u[o]].index=o;return r(f,u),f}var n=Kt([]),e=ae,r=ue,i=ce;return t.keys=function(e){return arguments.length?(n="function"==typeof e?e:Kt(Wv.call(e)),t):n},t.value=function(n){return arguments.length?(i="function"==typeof n?n:Kt(+n),t):i},t.order=function(n){return arguments.length?(e=null==n?ae:"function"==typeof n?n:Kt(Wv.call(n)),t):e},t.offset=function(n){return arguments.length?(r=null==n?ue:n,t):r},t}function fe(t,n){if((r=t.length)>0){for(var e,r,i,o=0,u=t[0].length;o<u;++o){for(i=e=0;e<r;++e)i+=t[e][o][1]||0;if(i)for(e=0;e<r;++e)t[e][o][1]/=i}ue(t,n)}}function le(t,n){if((e=t.length)>0){for(var e,r=0,i=t[n[0]],o=i.length;r<o;++r){for(var u=0,a=0;u<e;++u)a+=t[u][r][1]||0;i[r][1]+=i[r][0]=-a/2}ue(t,n)}}function he(t,n){if((i=t.length)>0&&(r=(e=t[n[0]]).length)>0){for(var e,r,i,o=0,u=1;u<r;++u){for(var a=0,c=0,s=0;a<i;++a){for(var f=t[n[a]],l=f[u][1]||0,h=f[u-1][1]||0,p=(l-h)/2,d=0;d<a;++d){var v=t[n[d]],_=v[u][1]||0,y=v[u-1][1]||0;p+=_-y}c+=l,s+=p*l}e[u-1][1]+=e[u-1][0]=o,c&&(o-=s/c)}e[u-1][1]+=e[u-1][0]=o,ue(t,n)}}function pe(t){var n=t.map(de);return ae(t).sort(function(t,e){return n[t]-n[e]})}function de(t){for(var n,e=0,r=-1,i=t.length;++r<i;)(n=+t[r][1])&&(e+=n);return e}function ve(t){return pe(t).reverse()}function _e(t){var n,e,r=t.length,i=t.map(de),o=ae(t).sort(function(t,n){return i[n]-i[t]}),u=0,a=0,c=[],s=[];for(n=0;n<r;++n)e=o[n],u<a?(u+=i[e],c.push(e)):(a+=i[e],s.push(e));return s.reverse().concat(c)}function ye(t){return ae(t).reverse()}function ge(t,n,e){t.prototype=n.prototype=e,e.constructor=t}function me(t,n){var e=Object.create(t.prototype);for(var r in n)e[r]=n[r];return e}function xe(){}function be(t){var n;return t=(t+"").trim().toLowerCase(),(n=Gv.exec(t))?(n=parseInt(n[1],16),new Ne(n>>8&15|n>>4&240,n>>4&15|240&n,(15&n)<<4|15&n,1)):(n=Jv.exec(t))?we(parseInt(n[1],16)):(n=Qv.exec(t))?new Ne(n[1],n[2],n[3],1):(n=Kv.exec(t))?new Ne(255*n[1]/100,255*n[2]/100,255*n[3]/100,1):(n=t_.exec(t))?Me(n[1],n[2],n[3],n[4]):(n=n_.exec(t))?Me(255*n[1]/100,255*n[2]/100,255*n[3]/100,n[4]):(n=e_.exec(t))?Se(n[1],n[2]/100,n[3]/100,1):(n=r_.exec(t))?Se(n[1],n[2]/100,n[3]/100,n[4]):i_.hasOwnProperty(t)?we(i_[t]):"transparent"===t?new Ne(NaN,NaN,NaN,0):null}function we(t){return new Ne(t>>16&255,t>>8&255,255&t,1)}function Me(t,n,e,r){return r<=0&&(t=n=e=NaN),new Ne(t,n,e,r)}function Te(t){return t instanceof xe||(t=be(t)),t?(t=t.rgb(),new Ne(t.r,t.g,t.b,t.opacity)):new Ne}function ke(t,n,e,r){return 1===arguments.length?Te(t):new Ne(t,n,e,null==r?1:r)}function Ne(t,n,e,r){this.r=+t,this.g=+n,this.b=+e,this.opacity=+r}function Se(t,n,e,r){return r<=0?t=n=e=NaN:e<=0||e>=1?t=n=NaN:n<=0&&(t=NaN),new Ce(t,n,e,r)}function Ae(t){if(t instanceof Ce)return new Ce(t.h,t.s,t.l,t.opacity);if(t instanceof xe||(t=be(t)),!t)return new Ce;if(t instanceof Ce)return t;t=t.rgb();var n=t.r/255,e=t.g/255,r=t.b/255,i=Math.min(n,e,r),o=Math.max(n,e,r),u=NaN,a=o-i,c=(o+i)/2;return a?(u=n===o?(e-r)/a+6*(e<r):e===o?(r-n)/a+2:(n-e)/a+4,a/=c<.5?o+i:2-o-i,u*=60):a=c>0&&c<1?0:u,new Ce(u,a,c,t.opacity)}function Ee(t,n,e,r){return 1===arguments.length?Ae(t):new Ce(t,n,e,null==r?1:r)}function Ce(t,n,e,r){this.h=+t,this.s=+n,this.l=+e,this.opacity=+r}function ze(t,n,e){return 255*(t<60?n+(e-n)*t/60:t<180?e:t<240?n+(e-n)*(240-t)/60:n)}function Pe(t){if(t instanceof Le)return new Le(t.l,t.a,t.b,t.opacity);if(t instanceof Ye){var n=t.h*o_;return new Le(t.l,Math.cos(n)*t.c,Math.sin(n)*t.c,t.opacity)}t instanceof Ne||(t=Te(t));var e=Oe(t.r),r=Oe(t.g),i=Oe(t.b),o=Re((.4124564*e+.3575761*r+.1804375*i)/c_),u=Re((.2126729*e+.7151522*r+.072175*i)/s_),a=Re((.0193339*e+.119192*r+.9503041*i)/f_);return new Le(116*u-16,500*(o-u),200*(u-a),t.opacity)}function qe(t,n,e,r){return 1===arguments.length?Pe(t):new Le(t,n,e,null==r?1:r)}function Le(t,n,e,r){this.l=+t,this.a=+n,this.b=+e,this.opacity=+r}function Re(t){return t>d_?Math.pow(t,1/3):t/p_+l_}function Ue(t){return t>h_?t*t*t:p_*(t-l_)}function De(t){return 255*(t<=.0031308?12.92*t:1.055*Math.pow(t,1/2.4)-.055)}function Oe(t){return(t/=255)<=.04045?t/12.92:Math.pow((t+.055)/1.055,2.4)}function Fe(t){if(t instanceof Ye)return new Ye(t.h,t.c,t.l,t.opacity);t instanceof Le||(t=Pe(t));var n=Math.atan2(t.b,t.a)*u_;return new Ye(n<0?n+360:n,Math.sqrt(t.a*t.a+t.b*t.b),t.l,t.opacity)}function Ie(t,n,e,r){return 1===arguments.length?Fe(t):new Ye(t,n,e,null==r?1:r)}function Ye(t,n,e,r){this.h=+t,this.c=+n,this.l=+e,this.opacity=+r}function Be(t){if(t instanceof He)return new He(t.h,t.s,t.l,t.opacity);t instanceof Ne||(t=Te(t));var n=t.r/255,e=t.g/255,r=t.b/255,i=(w_*r+x_*n-b_*e)/(w_+x_-b_),o=r-i,u=(m_*(e-i)-y_*o)/g_,a=Math.sqrt(u*u+o*o)/(m_*i*(1-i)),c=a?Math.atan2(u,o)*u_-120:NaN;return new He(c<0?c+360:c,a,i,t.opacity)}function je(t,n,e,r){return 1===arguments.length?Be(t):new He(t,n,e,null==r?1:r)}function He(t,n,e,r){this.h=+t,this.s=+n,this.l=+e,this.opacity=+r}function Xe(t,n,e,r,i){var o=t*t,u=o*t;return((1-3*t+3*o-u)*n+(4-6*o+3*u)*e+(1+3*t+3*o-3*u)*r+u*i)/6}function Ve(t){var n=t.length-1;return function(e){var r=e<=0?e=0:e>=1?(e=1,n-1):Math.floor(e*n),i=t[r],o=t[r+1],u=r>0?t[r-1]:2*i-o,a=r<n-1?t[r+2]:2*o-i;return Xe((e-r/n)*n,u,i,o,a)}}function We(t){var n=t.length;return function(e){var r=Math.floor(((e%=1)<0?++e:e)*n),i=t[(r+n-1)%n],o=t[r%n],u=t[(r+1)%n],a=t[(r+2)%n];return Xe((e-r/n)*n,i,o,u,a)}}function $e(t){return function(){return t}}function Ze(t,n){return function(e){return t+e*n}}function Ge(t,n,e){return t=Math.pow(t,e),n=Math.pow(n,e)-t,e=1/e,function(r){return Math.pow(t+r*n,e)}}function Je(t,n){var e=n-t;return e?Ze(t,e>180||e<-180?e-360*Math.round(e/360):e):$e(isNaN(t)?n:t)}function Qe(t){return 1===(t=+t)?Ke:function(n,e){return e-n?Ge(n,e,t):$e(isNaN(n)?e:n)}}function Ke(t,n){var e=n-t;return e?Ze(t,e):$e(isNaN(t)?n:t)}function tr(t){return function(n){var e,r,i=n.length,o=new Array(i),u=new Array(i),a=new Array(i);for(e=0;e<i;++e)r=ke(n[e]),o[e]=r.r||0,u[e]=r.g||0,a[e]=r.b||0;return o=t(o),u=t(u),a=t(a),r.opacity=1,function(t){return r.r=o(t),r.g=u(t),r.b=a(t),r+""}}}function nr(t,n){var e,r=n?n.length:0,i=t?Math.min(r,t.length):0,o=new Array(r),u=new Array(r);for(e=0;e<i;++e)o[e]=cr(t[e],n[e]);
+for(;e<r;++e)u[e]=n[e];return function(t){for(e=0;e<i;++e)u[e]=o[e](t);return u}}function er(t,n){var e=new Date;return t=+t,n-=t,function(r){return e.setTime(t+n*r),e}}function rr(t,n){return t=+t,n-=t,function(e){return t+n*e}}function ir(t,n){var e,r={},i={};null!==t&&"object"==typeof t||(t={}),null!==n&&"object"==typeof n||(n={});for(e in n)e in t?r[e]=cr(t[e],n[e]):i[e]=n[e];return function(t){for(e in r)i[e]=r[e](t);return i}}function or(t){return function(){return t}}function ur(t){return function(n){return t(n)+""}}function ar(t,n){var e,r,i,o=C_.lastIndex=z_.lastIndex=0,u=-1,a=[],c=[];for(t+="",n+="";(e=C_.exec(t))&&(r=z_.exec(n));)(i=r.index)>o&&(i=n.slice(o,i),a[u]?a[u]+=i:a[++u]=i),(e=e[0])===(r=r[0])?a[u]?a[u]+=r:a[++u]=r:(a[++u]=null,c.push({i:u,x:rr(e,r)})),o=z_.lastIndex;return o<n.length&&(i=n.slice(o),a[u]?a[u]+=i:a[++u]=i),a.length<2?c[0]?ur(c[0].x):or(n):(n=c.length,function(t){for(var e,r=0;r<n;++r)a[(e=c[r]).i]=e.x(t);return a.join("")})}function cr(t,n){var e,r=typeof n;return null==n||"boolean"===r?$e(n):("number"===r?rr:"string"===r?(e=be(n))?(n=e,S_):ar:n instanceof be?S_:n instanceof Date?er:Array.isArray(n)?nr:isNaN(n)?ir:rr)(t,n)}function sr(t,n){return t=+t,n-=t,function(e){return Math.round(t+n*e)}}function fr(t,n,e,r,i,o){var u,a,c;return(u=Math.sqrt(t*t+n*n))&&(t/=u,n/=u),(c=t*e+n*r)&&(e-=t*c,r-=n*c),(a=Math.sqrt(e*e+r*r))&&(e/=a,r/=a,c/=a),t*r<n*e&&(t=-t,n=-n,c=-c,u=-u),{translateX:i,translateY:o,rotate:Math.atan2(n,t)*P_,skewX:Math.atan(c)*P_,scaleX:u,scaleY:a}}function lr(t){return"none"===t?q_:(M_||(M_=document.createElement("DIV"),T_=document.documentElement,k_=document.defaultView),M_.style.transform=t,t=k_.getComputedStyle(T_.appendChild(M_),null).getPropertyValue("transform"),T_.removeChild(M_),t=t.slice(7,-1).split(","),fr(+t[0],+t[1],+t[2],+t[3],+t[4],+t[5]))}function hr(t){return null==t?q_:(N_||(N_=document.createElementNS("http://www.w3.org/2000/svg","g")),N_.setAttribute("transform",t),(t=N_.transform.baseVal.consolidate())?(t=t.matrix,fr(t.a,t.b,t.c,t.d,t.e,t.f)):q_)}function pr(t,n,e,r){function i(t){return t.length?t.pop()+" ":""}function o(t,r,i,o,u,a){if(t!==i||r!==o){var c=u.push("translate(",null,n,null,e);a.push({i:c-4,x:rr(t,i)},{i:c-2,x:rr(r,o)})}else(i||o)&&u.push("translate("+i+n+o+e)}function u(t,n,e,o){t!==n?(t-n>180?n+=360:n-t>180&&(t+=360),o.push({i:e.push(i(e)+"rotate(",null,r)-2,x:rr(t,n)})):n&&e.push(i(e)+"rotate("+n+r)}function a(t,n,e,o){t!==n?o.push({i:e.push(i(e)+"skewX(",null,r)-2,x:rr(t,n)}):n&&e.push(i(e)+"skewX("+n+r)}function c(t,n,e,r,o,u){if(t!==e||n!==r){var a=o.push(i(o)+"scale(",null,",",null,")");u.push({i:a-4,x:rr(t,e)},{i:a-2,x:rr(n,r)})}else 1===e&&1===r||o.push(i(o)+"scale("+e+","+r+")")}return function(n,e){var r=[],i=[];return n=t(n),e=t(e),o(n.translateX,n.translateY,e.translateX,e.translateY,r,i),u(n.rotate,e.rotate,r,i),a(n.skewX,e.skewX,r,i),c(n.scaleX,n.scaleY,e.scaleX,e.scaleY,r,i),n=e=null,function(t){for(var n,e=-1,o=i.length;++e<o;)r[(n=i[e]).i]=n.x(t);return r.join("")}}}function dr(t){return((t=Math.exp(t))+1/t)/2}function vr(t){return((t=Math.exp(t))-1/t)/2}function _r(t){return((t=Math.exp(2*t))-1)/(t+1)}function yr(t,n){var e,r,i=t[0],o=t[1],u=t[2],a=n[0],c=n[1],s=n[2],f=a-i,l=c-o,h=f*f+l*l;if(h<F_)r=Math.log(s/u)/U_,e=function(t){return[i+t*f,o+t*l,u*Math.exp(U_*t*r)]};else{var p=Math.sqrt(h),d=(s*s-u*u+O_*h)/(2*u*D_*p),v=(s*s-u*u-O_*h)/(2*s*D_*p),_=Math.log(Math.sqrt(d*d+1)-d),y=Math.log(Math.sqrt(v*v+1)-v);r=(y-_)/U_,e=function(t){var n=t*r,e=dr(_),a=u/(D_*p)*(e*_r(U_*n+_)-vr(_));return[i+a*f,o+a*l,u*e/dr(U_*n+_)]}}return e.duration=1e3*r,e}function gr(t){return function(n,e){var r=t((n=Ee(n)).h,(e=Ee(e)).h),i=Ke(n.s,e.s),o=Ke(n.l,e.l),u=Ke(n.opacity,e.opacity);return function(t){return n.h=r(t),n.s=i(t),n.l=o(t),n.opacity=u(t),n+""}}}function mr(t,n){var e=Ke((t=qe(t)).l,(n=qe(n)).l),r=Ke(t.a,n.a),i=Ke(t.b,n.b),o=Ke(t.opacity,n.opacity);return function(n){return t.l=e(n),t.a=r(n),t.b=i(n),t.opacity=o(n),t+""}}function xr(t){return function(n,e){var r=t((n=Ie(n)).h,(e=Ie(e)).h),i=Ke(n.c,e.c),o=Ke(n.l,e.l),u=Ke(n.opacity,e.opacity);return function(t){return n.h=r(t),n.c=i(t),n.l=o(t),n.opacity=u(t),n+""}}}function br(t){return function n(e){function r(n,r){var i=t((n=je(n)).h,(r=je(r)).h),o=Ke(n.s,r.s),u=Ke(n.l,r.l),a=Ke(n.opacity,r.opacity);return function(t){return n.h=i(t),n.s=o(t),n.l=u(Math.pow(t,e)),n.opacity=a(t),n+""}}return e=+e,r.gamma=n,r}(1)}function wr(t,n){for(var e=new Array(n),r=0;r<n;++r)e[r]=t(r/(n-1));return e}function Mr(){for(var t,n=0,e=arguments.length,r={};n<e;++n){if(!(t=arguments[n]+"")||t in r)throw new Error("illegal type: "+t);r[t]=[]}return new Tr(r)}function Tr(t){this._=t}function kr(t,n){return t.trim().split(/^|\s+/).map(function(t){var e="",r=t.indexOf(".");if(r>=0&&(e=t.slice(r+1),t=t.slice(0,r)),t&&!n.hasOwnProperty(t))throw new Error("unknown type: "+t);return{type:t,name:e}})}function Nr(t,n){for(var e,r=0,i=t.length;r<i;++r)if((e=t[r]).name===n)return e.value}function Sr(t,n,e){for(var r=0,i=t.length;r<i;++r)if(t[r].name===n){t[r]=V_,t=t.slice(0,r).concat(t.slice(r+1));break}return null!=e&&t.push({name:n,value:e}),t}function Ar(t){return new Function("d","return {"+t.map(function(t,n){return JSON.stringify(t)+": d["+n+"]"}).join(",")+"}")}function Er(t,n){var e=Ar(t);return function(r,i){return n(e(r),i,t)}}function Cr(t){var n=Object.create(null),e=[];return t.forEach(function(t){for(var r in t)r in n||e.push(n[r]=r)}),e}function zr(t){function n(t,n){var r,i,o=e(t,function(t,e){return r?r(t,e-1):(i=t,void(r=n?Er(t,n):Ar(t)))});return o.columns=i,o}function e(t,n){function e(){if(f>=s)return u;if(i)return i=!1,o;var n,e=f;if(34===t.charCodeAt(e)){for(var r=e;r++<s;)if(34===t.charCodeAt(r)){if(34!==t.charCodeAt(r+1))break;++r}return f=r+2,n=t.charCodeAt(r+1),13===n?(i=!0,10===t.charCodeAt(r+2)&&++f):10===n&&(i=!0),t.slice(e+1,r).replace(/""/g,'"')}for(;f<s;){var a=1;if(n=t.charCodeAt(f++),10===n)i=!0;else if(13===n)i=!0,10===t.charCodeAt(f)&&(++f,++a);else if(n!==c)continue;return t.slice(e,f-a)}return t.slice(e)}for(var r,i,o={},u={},a=[],s=t.length,f=0,l=0;(r=e())!==u;){for(var h=[];r!==o&&r!==u;)h.push(r),r=e();n&&null==(h=n(h,l++))||a.push(h)}return a}function r(n,e){return null==e&&(e=Cr(n)),[e.map(u).join(t)].concat(n.map(function(n){return e.map(function(t){return u(n[t])}).join(t)})).join("\n")}function i(t){return t.map(o).join("\n")}function o(n){return n.map(u).join(t)}function u(t){return null==t?"":a.test(t+="")?'"'+t.replace(/\"/g,'""')+'"':t}var a=new RegExp('["'+t+"\n]"),c=t.charCodeAt(0);return{parse:n,parseRows:e,format:r,formatRows:i}}function Pr(t,n){function e(t){var n,e=s.status;if(!e&&Lr(s)||e>=200&&e<300||304===e){if(o)try{n=o.call(r,s)}catch(i){return void a.call("error",r,i)}else n=s;a.call("load",r,n)}else a.call("error",r,t)}var r,i,o,u,a=Mr("beforesend","progress","load","error"),c=q(),s=new XMLHttpRequest,f=null,l=null,h=0;return"undefined"==typeof XDomainRequest||"withCredentials"in s||!/^(http(s)?:)?\/\//.test(t)||(s=new XDomainRequest),"onload"in s?s.onload=s.onerror=s.ontimeout=e:s.onreadystatechange=function(t){s.readyState>3&&e(t)},s.onprogress=function(t){a.call("progress",r,t)},r={header:function(t,n){return t=(t+"").toLowerCase(),arguments.length<2?c.get(t):(null==n?c.remove(t):c.set(t,n+""),r)},mimeType:function(t){return arguments.length?(i=null==t?null:t+"",r):i},responseType:function(t){return arguments.length?(u=t,r):u},timeout:function(t){return arguments.length?(h=+t,r):h},user:function(t){return arguments.length<1?f:(f=null==t?null:t+"",r)},password:function(t){return arguments.length<1?l:(l=null==t?null:t+"",r)},response:function(t){return o=t,r},get:function(t,n){return r.send("GET",t,n)},post:function(t,n){return r.send("POST",t,n)},send:function(n,e,o){return o||"function"!=typeof e||(o=e,e=null),o&&1===o.length&&(o=qr(o)),s.open(n,t,!0,f,l),null==i||c.has("accept")||c.set("accept",i+",*/*"),s.setRequestHeader&&c.each(function(t,n){s.setRequestHeader(n,t)}),null!=i&&s.overrideMimeType&&s.overrideMimeType(i),null!=u&&(s.responseType=u),h>0&&(s.timeout=h),o&&r.on("error",o).on("load",function(t){o(null,t)}),a.call("beforesend",r,s),s.send(null==e?null:e),r},abort:function(){return s.abort(),r},on:function(){var t=a.on.apply(a,arguments);return t===a?r:t}},n?r.get(n):r}function qr(t){return function(n,e){t(null==n?e:null)}}function Lr(t){var n=t.responseType;return n&&"text"!==n?t.response:t.responseText}function Rr(t,n){return function(e,r){var i=Pr(e).mimeType(t).response(n);return r?i.get(r):i}}function Ur(t,n){return function(e,r,i){arguments.length<3&&(i=r,r=null);var o=Pr(e).mimeType(t);return o.row=function(t){return arguments.length?o.response(Dr(n,r=t)):r},o.row(r),i?o.get(i):o}}function Dr(t,n){return function(e){return t(e.responseText,n)}}function Or(){return _y||(my(Fr),_y=gy.now()+yy)}function Fr(){_y=0}function Ir(){this._call=this._time=this._next=null}function Yr(t,n,e){var r=new Ir;return r.restart(t,n,e),r}function Br(){Or(),++ly;for(var t,n=W_;n;)(t=_y-n._time)>=0&&n._call.call(null,t),n=n._next;--ly}function jr(t){_y=(vy=t||gy.now())+yy,ly=hy=0;try{Br()}finally{ly=0,Xr(),_y=0}}function Hr(){var t=gy.now(),n=t-vy;n>dy&&(yy-=n,vy=t)}function Xr(){for(var t,n,e=W_,r=1/0;e;)e._call?(r>e._time&&(r=e._time),t=e,e=e._next):(n=e._next,e._next=null,e=t?t._next=n:W_=n);$_=t,Vr(r)}function Vr(t){if(!ly){hy&&(hy=clearTimeout(hy));var n=t-_y;n>24?(t<1/0&&(hy=setTimeout(jr,n)),py&&(py=clearInterval(py))):(py||(py=setInterval(Hr,dy)),ly=1,my(jr))}}function Wr(t,n,e){var r=new Ir;return n=null==n?0:+n,r.restart(function(e){r.stop(),t(e+n)},n,e),r}function $r(t,n,e){var r=new Ir,i=n;return null==n?(r.restart(t,n,e),r):(n=+n,e=null==e?Or():+e,r.restart(function o(u){u+=i,r.restart(o,i+=n,e),t(u)},n,e),r)}function Zr(t,n,e,r){function i(n){return t(n=new Date((+n))),n}return i.floor=i,i.ceil=function(e){return t(e=new Date(e-1)),n(e,1),t(e),e},i.round=function(t){var n=i(t),e=i.ceil(t);return t-n<e-t?n:e},i.offset=function(t,e){return n(t=new Date((+t)),null==e?1:Math.floor(e)),t},i.range=function(e,r,o){var u=[];if(e=i.ceil(e),o=null==o?1:Math.floor(o),!(e<r&&o>0))return u;do u.push(new Date((+e)));while(n(e,o),t(e),e<r);return u},i.filter=function(e){return Zr(function(n){for(;t(n),!e(n);)n.setTime(n-1)},function(t,r){for(;--r>=0;)for(;n(t,1),!e(t););})},e&&(i.count=function(n,r){return xy.setTime(+n),by.setTime(+r),t(xy),t(by),Math.floor(e(xy,by))},i.every=function(t){return t=Math.floor(t),isFinite(t)&&t>0?t>1?i.filter(r?function(n){return r(n)%t===0}:function(n){return i.count(0,n)%t===0}):i:null}),i}function Gr(t){return Zr(function(n){n.setDate(n.getDate()-(n.getDay()+7-t)%7),n.setHours(0,0,0,0)},function(t,n){t.setDate(t.getDate()+7*n)},function(t,n){return(n-t-(n.getTimezoneOffset()-t.getTimezoneOffset())*ky)/Ay})}function Jr(t){return Zr(function(n){n.setUTCDate(n.getUTCDate()-(n.getUTCDay()+7-t)%7),n.setUTCHours(0,0,0,0)},function(t,n){t.setUTCDate(t.getUTCDate()+7*n)},function(t,n){return(n-t)/Ay})}function Qr(t,n){if((e=(t=n?t.toExponential(n-1):t.toExponential()).indexOf("e"))<0)return null;var e,r=t.slice(0,e);return[r.length>1?r[0]+r.slice(2):r,+t.slice(e+1)]}function Kr(t){return t=Qr(Math.abs(t)),t?t[1]:NaN}function ti(t,n){return function(e,r){for(var i=e.length,o=[],u=0,a=t[0],c=0;i>0&&a>0&&(c+a+1>r&&(a=Math.max(1,r-c)),o.push(e.substring(i-=a,i+a)),!((c+=a+1)>r));)a=t[u=(u+1)%t.length];return o.reverse().join(n)}}function ni(t,n){t=t.toPrecision(n);t:for(var e,r=t.length,i=1,o=-1;i<r;++i)switch(t[i]){case".":o=e=i;break;case"0":0===o&&(o=i),e=i;break;case"e":break t;default:o>0&&(o=0)}return o>0?t.slice(0,o)+t.slice(e+1):t}function ei(t,n){var e=Qr(t,n);if(!e)return t+"";var r=e[0],i=e[1],o=i-(Tg=3*Math.max(-8,Math.min(8,Math.floor(i/3))))+1,u=r.length;return o===u?r:o>u?r+new Array(o-u+1).join("0"):o>0?r.slice(0,o)+"."+r.slice(o):"0."+new Array(1-o).join("0")+Qr(t,Math.max(0,n+o-1))[0]}function ri(t,n){var e=Qr(t,n);if(!e)return t+"";var r=e[0],i=e[1];return i<0?"0."+new Array((-i)).join("0")+r:r.length>i+1?r.slice(0,i+1)+"."+r.slice(i+1):r+new Array(i-r.length+2).join("0")}function ii(t){return new oi(t)}function oi(t){if(!(n=Sg.exec(t)))throw new Error("invalid format: "+t);var n,e=n[1]||" ",r=n[2]||">",i=n[3]||"-",o=n[4]||"",u=!!n[5],a=n[6]&&+n[6],c=!!n[7],s=n[8]&&+n[8].slice(1),f=n[9]||"";"n"===f?(c=!0,f="g"):Ng[f]||(f=""),(u||"0"===e&&"="===r)&&(u=!0,e="0",r="="),this.fill=e,this.align=r,this.sign=i,this.symbol=o,this.zero=u,this.width=a,this.comma=c,this.precision=s,this.type=f}function ui(t){return t}function ai(t){function n(t){function n(t){var n,i,c,g=d,m=v;if("c"===p)m=_(t)+m,t="";else{t=+t;var x=(t<0||1/t<0)&&(t*=-1,!0);if(t=_(t,h),x)for(n=-1,i=t.length,x=!1;++n<i;)if(c=t.charCodeAt(n),48<c&&c<58||"x"===p&&96<c&&c<103||"X"===p&&64<c&&c<71){x=!0;break}if(g=(x?"("===a?a:"-":"-"===a||"("===a?"":a)+g,m=m+("s"===p?Eg[8+Tg/3]:"")+(x&&"("===a?")":""),y)for(n=-1,i=t.length;++n<i;)if(c=t.charCodeAt(n),48>c||c>57){m=(46===c?o+t.slice(n+1):t.slice(n))+m,t=t.slice(0,n);break}}l&&!s&&(t=r(t,1/0));var b=g.length+t.length+m.length,w=b<f?new Array(f-b+1).join(e):"";switch(l&&s&&(t=r(w+t,w.length?f-m.length:1/0),w=""),u){case"<":return g+t+m+w;case"=":return g+w+t+m;case"^":return w.slice(0,b=w.length>>1)+g+t+m+w.slice(b)}return w+g+t+m}t=ii(t);var e=t.fill,u=t.align,a=t.sign,c=t.symbol,s=t.zero,f=t.width,l=t.comma,h=t.precision,p=t.type,d="$"===c?i[0]:"#"===c&&/[boxX]/.test(p)?"0"+p.toLowerCase():"",v="$"===c?i[1]:/[%p]/.test(p)?"%":"",_=Ng[p],y=!p||/[defgprs%]/.test(p);return h=null==h?p?6:12:/[gprs]/.test(p)?Math.max(1,Math.min(21,h)):Math.max(0,Math.min(20,h)),n.toString=function(){return t+""},n}function e(t,e){var r=n((t=ii(t),t.type="f",t)),i=3*Math.max(-8,Math.min(8,Math.floor(Kr(e)/3))),o=Math.pow(10,-i),u=Eg[8+i/3];return function(t){return r(o*t)+u}}var r=t.grouping&&t.thousands?ti(t.grouping,t.thousands):ui,i=t.currency,o=t.decimal;return{format:n,formatPrefix:e}}function ci(n){return Ag=ai(n),t.format=Ag.format,t.formatPrefix=Ag.formatPrefix,Ag}function si(t){return Math.max(0,-Kr(Math.abs(t)))}function fi(t,n){return Math.max(0,3*Math.max(-8,Math.min(8,Math.floor(Kr(n)/3)))-Kr(Math.abs(t)))}function li(t,n){return t=Math.abs(t),n=Math.abs(n)-t,Math.max(0,Kr(n)-Kr(t))+1}function hi(t){if(0<=t.y&&t.y<100){var n=new Date((-1),t.m,t.d,t.H,t.M,t.S,t.L);return n.setFullYear(t.y),n}return new Date(t.y,t.m,t.d,t.H,t.M,t.S,t.L)}function pi(t){if(0<=t.y&&t.y<100){var n=new Date(Date.UTC(-1,t.m,t.d,t.H,t.M,t.S,t.L));return n.setUTCFullYear(t.y),n}return new Date(Date.UTC(t.y,t.m,t.d,t.H,t.M,t.S,t.L))}function di(t){return{y:t,m:0,d:1,H:0,M:0,S:0,L:0}}function vi(t){function n(t,n){return function(e){var r,i,o,u=[],a=-1,c=0,s=t.length;for(e instanceof Date||(e=new Date((+e)));++a<s;)37===t.charCodeAt(a)&&(u.push(t.slice(c,a)),null!=(i=zg[r=t.charAt(++a)])?r=t.charAt(++a):i="e"===r?" ":"0",(o=n[r])&&(r=o(e,i)),u.push(r),c=a+1);return u.push(t.slice(c,a)),u.join("")}}function e(t,n){return function(e){var i=di(1900),o=r(i,t,e+="",0);if(o!=e.length)return null;if("p"in i&&(i.H=i.H%12+12*i.p),"W"in i||"U"in i){"w"in i||(i.w="W"in i?1:0);var u="Z"in i?pi(di(i.y)).getUTCDay():n(di(i.y)).getDay();i.m=0,i.d="W"in i?(i.w+6)%7+7*i.W-(u+5)%7:i.w+7*i.U-(u+6)%7}return"Z"in i?(i.H+=i.Z/100|0,i.M+=i.Z%100,pi(i)):n(i)}}function r(t,n,e,r){for(var i,o,u=0,a=n.length,c=e.length;u<a;){if(r>=c)return-1;if(i=n.charCodeAt(u++),37===i){if(i=n.charAt(u++),o=B[i in zg?n.charAt(u++):i],!o||(r=o(t,e,r))<0)return-1}else if(i!=e.charCodeAt(r++))return-1}return r}function i(t,n,e){var r=C.exec(n.slice(e));return r?(t.p=z[r[0].toLowerCase()],e+r[0].length):-1}function o(t,n,e){var r=L.exec(n.slice(e));return r?(t.w=R[r[0].toLowerCase()],e+r[0].length):-1}function u(t,n,e){var r=P.exec(n.slice(e));return r?(t.w=q[r[0].toLowerCase()],e+r[0].length):-1}function a(t,n,e){var r=O.exec(n.slice(e));return r?(t.m=F[r[0].toLowerCase()],e+r[0].length):-1}function c(t,n,e){var r=U.exec(n.slice(e));return r?(t.m=D[r[0].toLowerCase()],e+r[0].length):-1}function s(t,n,e){return r(t,w,n,e)}function f(t,n,e){return r(t,M,n,e)}function l(t,n,e){return r(t,T,n,e)}function h(t){return S[t.getDay()]}function p(t){return N[t.getDay()]}function d(t){return E[t.getMonth()]}function v(t){return A[t.getMonth()]}function _(t){return k[+(t.getHours()>=12)]}function y(t){return S[t.getUTCDay()]}function g(t){return N[t.getUTCDay()]}function m(t){return E[t.getUTCMonth()]}function x(t){return A[t.getUTCMonth()]}function b(t){return k[+(t.getUTCHours()>=12)]}var w=t.dateTime,M=t.date,T=t.time,k=t.periods,N=t.days,S=t.shortDays,A=t.months,E=t.shortMonths,C=gi(k),z=mi(k),P=gi(N),q=mi(N),L=gi(S),R=mi(S),U=gi(A),D=mi(A),O=gi(E),F=mi(E),I={a:h,A:p,b:d,B:v,c:null,d:Li,e:Li,H:Ri,I:Ui,j:Di,L:Oi,m:Fi,M:Ii,p:_,S:Yi,U:Bi,w:ji,W:Hi,x:null,X:null,y:Xi,Y:Vi,Z:Wi,"%":co},Y={a:y,A:g,b:m,B:x,c:null,d:$i,e:$i,H:Zi,I:Gi,j:Ji,L:Qi,m:Ki,M:to,p:b,S:no,U:eo,w:ro,W:io,x:null,X:null,y:oo,Y:uo,Z:ao,"%":co},B={a:o,A:u,b:a,B:c,c:s,d:Si,e:Si,H:Ei,I:Ei,j:Ai,L:Pi,m:Ni,M:Ci,p:i,S:zi,U:bi,w:xi,W:wi,x:f,X:l,y:Ti,Y:Mi,Z:ki,"%":qi};return I.x=n(M,I),I.X=n(T,I),I.c=n(w,I),Y.x=n(M,Y),Y.X=n(T,Y),Y.c=n(w,Y),{format:function(t){var e=n(t+="",I);return e.toString=function(){return t},e},parse:function(t){var n=e(t+="",hi);return n.toString=function(){return t},n},utcFormat:function(t){var e=n(t+="",Y);return e.toString=function(){return t},e},utcParse:function(t){var n=e(t,pi);return n.toString=function(){return t},n}}}function _i(t,n,e){var r=t<0?"-":"",i=(r?-t:t)+"",o=i.length;return r+(o<e?new Array(e-o+1).join(n)+i:i)}function yi(t){return t.replace(Lg,"\\$&")}function gi(t){return new RegExp("^(?:"+t.map(yi).join("|")+")","i")}function mi(t){for(var n={},e=-1,r=t.length;++e<r;)n[t[e].toLowerCase()]=e;return n}function xi(t,n,e){var r=Pg.exec(n.slice(e,e+1));return r?(t.w=+r[0],e+r[0].length):-1}function bi(t,n,e){var r=Pg.exec(n.slice(e));return r?(t.U=+r[0],e+r[0].length):-1}function wi(t,n,e){var r=Pg.exec(n.slice(e));return r?(t.W=+r[0],e+r[0].length):-1}function Mi(t,n,e){var r=Pg.exec(n.slice(e,e+4));return r?(t.y=+r[0],e+r[0].length):-1}function Ti(t,n,e){var r=Pg.exec(n.slice(e,e+2));return r?(t.y=+r[0]+(+r[0]>68?1900:2e3),e+r[0].length):-1}function ki(t,n,e){var r=/^(Z)|([+-]\d\d)(?:\:?(\d\d))?/.exec(n.slice(e,e+6));return r?(t.Z=r[1]?0:-(r[2]+(r[3]||"00")),e+r[0].length):-1}function Ni(t,n,e){var r=Pg.exec(n.slice(e,e+2));return r?(t.m=r[0]-1,e+r[0].length):-1}function Si(t,n,e){var r=Pg.exec(n.slice(e,e+2));return r?(t.d=+r[0],e+r[0].length):-1}function Ai(t,n,e){var r=Pg.exec(n.slice(e,e+3));return r?(t.m=0,t.d=+r[0],e+r[0].length):-1}function Ei(t,n,e){var r=Pg.exec(n.slice(e,e+2));return r?(t.H=+r[0],e+r[0].length):-1}function Ci(t,n,e){var r=Pg.exec(n.slice(e,e+2));return r?(t.M=+r[0],e+r[0].length):-1}function zi(t,n,e){var r=Pg.exec(n.slice(e,e+2));return r?(t.S=+r[0],e+r[0].length):-1}function Pi(t,n,e){var r=Pg.exec(n.slice(e,e+3));return r?(t.L=+r[0],e+r[0].length):-1}function qi(t,n,e){var r=qg.exec(n.slice(e,e+1));return r?e+r[0].length:-1}function Li(t,n){return _i(t.getDate(),n,2)}function Ri(t,n){return _i(t.getHours(),n,2)}function Ui(t,n){return _i(t.getHours()%12||12,n,2)}function Di(t,n){return _i(1+Ry.count(Ky(t),t),n,3)}function Oi(t,n){return _i(t.getMilliseconds(),n,3)}function Fi(t,n){return _i(t.getMonth()+1,n,2)}function Ii(t,n){return _i(t.getMinutes(),n,2)}function Yi(t,n){return _i(t.getSeconds(),n,2)}function Bi(t,n){return _i(Dy.count(Ky(t),t),n,2)}function ji(t){return t.getDay()}function Hi(t,n){return _i(Oy.count(Ky(t),t),n,2)}function Xi(t,n){return _i(t.getFullYear()%100,n,2)}function Vi(t,n){return _i(t.getFullYear()%1e4,n,4)}function Wi(t){var n=t.getTimezoneOffset();return(n>0?"-":(n*=-1,"+"))+_i(n/60|0,"0",2)+_i(n%60,"0",2)}function $i(t,n){return _i(t.getUTCDate(),n,2)}function Zi(t,n){return _i(t.getUTCHours(),n,2)}function Gi(t,n){return _i(t.getUTCHours()%12||12,n,2)}function Ji(t,n){return _i(1+og.count(Mg(t),t),n,3)}function Qi(t,n){return _i(t.getUTCMilliseconds(),n,3)}function Ki(t,n){return _i(t.getUTCMonth()+1,n,2)}function to(t,n){return _i(t.getUTCMinutes(),n,2)}function no(t,n){return _i(t.getUTCSeconds(),n,2)}function eo(t,n){return _i(ag.count(Mg(t),t),n,2)}function ro(t){return t.getUTCDay()}function io(t,n){return _i(cg.count(Mg(t),t),n,2)}function oo(t,n){return _i(t.getUTCFullYear()%100,n,2)}function uo(t,n){return _i(t.getUTCFullYear()%1e4,n,4)}function ao(){return"+0000"}function co(){return"%"}function so(n){return Cg=vi(n),t.timeFormat=Cg.format,t.timeParse=Cg.parse,t.utcFormat=Cg.utcFormat,t.utcParse=Cg.utcParse,Cg}function fo(t){return t.toISOString()}function lo(t){var n=new Date(t);return isNaN(n)?null:n}function ho(t){function n(n){var o=n+"",u=e.get(o);if(!u){if(i!==Yg)return i;e.set(o,u=r.push(n))}return t[(u-1)%t.length]}var e=q(),r=[],i=Yg;return t=null==t?[]:Ig.call(t),n.domain=function(t){if(!arguments.length)return r.slice();r=[],e=q();for(var i,o,u=-1,a=t.length;++u<a;)e.has(o=(i=t[u])+"")||e.set(o,r.push(i));return n},n.range=function(e){return arguments.length?(t=Ig.call(e),n):t.slice()},n.unknown=function(t){return arguments.length?(i=t,n):i},n.copy=function(){return ho().domain(r).range(t).unknown(i)},n}function po(){function t(){var t=i().length,r=u[1]<u[0],h=u[r-0],p=u[1-r];n=(p-h)/Math.max(1,t-c+2*s),a&&(n=Math.floor(n)),h+=(p-h-n*(t-c))*f,e=n*(1-c),a&&(h=Math.round(h),e=Math.round(e));var d=l(t).map(function(t){return h+n*t});return o(r?d.reverse():d)}var n,e,r=ho().unknown(void 0),i=r.domain,o=r.range,u=[0,1],a=!1,c=0,s=0,f=.5;return delete r.unknown,r.domain=function(n){return arguments.length?(i(n),t()):i()},r.range=function(n){return arguments.length?(u=[+n[0],+n[1]],t()):u.slice()},r.rangeRound=function(n){return u=[+n[0],+n[1]],a=!0,t()},r.bandwidth=function(){return e},r.step=function(){return n},r.round=function(n){return arguments.length?(a=!!n,t()):a},r.padding=function(n){return arguments.length?(c=s=Math.max(0,Math.min(1,n)),t()):c},r.paddingInner=function(n){return arguments.length?(c=Math.max(0,Math.min(1,n)),t()):c},r.paddingOuter=function(n){return arguments.length?(s=Math.max(0,Math.min(1,n)),t()):s},r.align=function(n){return arguments.length?(f=Math.max(0,Math.min(1,n)),t()):f},r.copy=function(){return po().domain(i()).range(u).round(a).paddingInner(c).paddingOuter(s).align(f)},t()}function vo(t){var n=t.copy;return t.padding=t.paddingOuter,delete t.paddingInner,delete t.paddingOuter,t.copy=function(){return vo(n())},t}function _o(){return vo(po().paddingInner(1))}function yo(t){return function(){return t}}function go(t){return+t}function mo(t,n){return(n-=t=+t)?function(e){return(e-t)/n}:yo(n)}function xo(t){return function(n,e){var r=t(n=+n,e=+e);return function(t){return t<=n?0:t>=e?1:r(t)}}}function bo(t){return function(n,e){var r=t(n=+n,e=+e);return function(t){return t<=0?n:t>=1?e:r(t)}}}function wo(t,n,e,r){var i=t[0],o=t[1],u=n[0],a=n[1];return o<i?(i=e(o,i),u=r(a,u)):(i=e(i,o),u=r(u,a)),function(t){return u(i(t))}}function Mo(t,n,e,r){var i=Math.min(t.length,n.length)-1,o=new Array(i),u=new Array(i),a=-1;for(t[i]<t[0]&&(t=t.slice().reverse(),n=n.slice().reverse());++a<i;)o[a]=e(t[a],t[a+1]),u[a]=r(n[a],n[a+1]);return function(n){var e=Sd(t,n,1,i)-1;return u[e](o[e](n))}}function To(t,n){return n.domain(t.domain()).range(t.range()).interpolate(t.interpolate()).clamp(t.clamp())}function ko(t,n){function e(){return i=Math.min(a.length,c.length)>2?Mo:wo,o=u=null,r}function r(n){return(o||(o=i(a,c,f?xo(t):t,s)))(+n)}var i,o,u,a=Bg,c=Bg,s=cr,f=!1;return r.invert=function(t){return(u||(u=i(c,a,mo,f?bo(n):n)))(+t)},r.domain=function(t){return arguments.length?(a=Fg.call(t,go),e()):a.slice()},r.range=function(t){return arguments.length?(c=Ig.call(t),e()):c.slice()},r.rangeRound=function(t){return c=Ig.call(t),s=sr,e()},r.clamp=function(t){return arguments.length?(f=!!t,e()):f},r.interpolate=function(t){return arguments.length?(s=t,e()):s},e()}function No(n,e,r){var i,o=n[0],u=n[n.length-1],a=p(o,u,null==e?10:e);switch(r=ii(null==r?",f":r),r.type){case"s":var c=Math.max(Math.abs(o),Math.abs(u));return null!=r.precision||isNaN(i=fi(a,c))||(r.precision=i),t.formatPrefix(r,c);case"":case"e":case"g":case"p":case"r":null!=r.precision||isNaN(i=li(a,Math.max(Math.abs(o),Math.abs(u))))||(r.precision=i-("e"===r.type));break;case"f":case"%":null!=r.precision||isNaN(i=si(a))||(r.precision=i-2*("%"===r.type))}return t.format(r)}function So(t){var n=t.domain;return t.ticks=function(t){var e=n();return h(e[0],e[e.length-1],null==t?10:t)},t.tickFormat=function(t,e){return No(n(),t,e)},t.nice=function(e){var r=n(),i=r.length-1,o=null==e?10:e,u=r[0],a=r[i],c=p(u,a,o);return c&&(c=p(Math.floor(u/c)*c,Math.ceil(a/c)*c,o),r[0]=Math.floor(u/c)*c,r[i]=Math.ceil(a/c)*c,n(r)),t},t}function Ao(){var t=ko(mo,rr);return t.copy=function(){return To(t,Ao())},So(t)}function Eo(){function t(t){return+t}var n=[0,1];return t.invert=t,t.domain=t.range=function(e){return arguments.length?(n=Fg.call(e,go),t):n.slice()},t.copy=function(){return Eo().domain(n)},So(t)}function Co(t,n){t=t.slice();var e,r=0,i=t.length-1,o=t[r],u=t[i];return u<o&&(e=r,r=i,i=e,e=o,o=u,u=e),t[r]=n.floor(o),t[i]=n.ceil(u),t}function zo(t,n){return(n=Math.log(n/t))?function(e){return Math.log(e/t)/n}:yo(n)}function Po(t,n){return t<0?function(e){return-Math.pow(-n,e)*Math.pow(-t,1-e)}:function(e){return Math.pow(n,e)*Math.pow(t,1-e)}}function qo(t){return isFinite(t)?+("1e"+t):t<0?0:t}function Lo(t){return 10===t?qo:t===Math.E?Math.exp:function(n){return Math.pow(t,n)}}function Ro(t){return t===Math.E?Math.log:10===t&&Math.log10||2===t&&Math.log2||(t=Math.log(t),function(n){return Math.log(n)/t})}function Uo(t){return function(n){return-t(-n)}}function Do(){function n(){return o=Ro(i),u=Lo(i),r()[0]<0&&(o=Uo(o),u=Uo(u)),e}var e=ko(zo,Po).domain([1,10]),r=e.domain,i=10,o=Ro(10),u=Lo(10);return e.base=function(t){return arguments.length?(i=+t,n()):i},e.domain=function(t){return arguments.length?(r(t),n()):r()},e.ticks=function(t){var n,e=r(),a=e[0],c=e[e.length-1];(n=c<a)&&(p=a,a=c,c=p);var s,f,l,p=o(a),d=o(c),v=null==t?10:+t,_=[];if(!(i%1)&&d-p<v){if(p=Math.round(p)-1,d=Math.round(d)+1,a>0){for(;p<d;++p)for(f=1,s=u(p);f<i;++f)if(l=s*f,!(l<a)){if(l>c)break;_.push(l)}}else for(;p<d;++p)for(f=i-1,s=u(p);f>=1;--f)if(l=s*f,!(l<a)){if(l>c)break;_.push(l)}}else _=h(p,d,Math.min(d-p,v)).map(u);return n?_.reverse():_},e.tickFormat=function(n,r){if(null==r&&(r=10===i?".0e":","),"function"!=typeof r&&(r=t.format(r)),n===1/0)return r;null==n&&(n=10);var a=Math.max(1,i*n/e.ticks().length);return function(t){var n=t/u(Math.round(o(t)));return n*i<i-.5&&(n*=i),n<=a?r(t):""}},e.nice=function(){return r(Co(r(),{floor:function(t){return u(Math.floor(o(t)))},ceil:function(t){return u(Math.ceil(o(t)))}}))},e.copy=function(){return To(e,Do().base(i))},e}function Oo(t,n){return t<0?-Math.pow(-t,n):Math.pow(t,n)}function Fo(){function t(t,n){return(n=Oo(n,e)-(t=Oo(t,e)))?function(r){return(Oo(r,e)-t)/n}:yo(n)}function n(t,n){return n=Oo(n,e)-(t=Oo(t,e)),function(r){return Oo(t+n*r,1/e)}}var e=1,r=ko(t,n),i=r.domain;return r.exponent=function(t){return arguments.length?(e=+t,i(i())):e},r.copy=function(){return To(r,Fo().exponent(e))},So(r)}function Io(){return Fo().exponent(.5)}function Yo(){function t(){var t=0,n=Math.max(1,i.length);for(o=new Array(n-1);++t<n;)o[t-1]=_(r,t/n);return e}function e(t){if(!isNaN(t=+t))return i[Sd(o,t)]}var r=[],i=[],o=[];return e.invertExtent=function(t){var n=i.indexOf(t);return n<0?[NaN,NaN]:[n>0?o[n-1]:r[0],n<o.length?o[n]:r[r.length-1]]},e.domain=function(e){if(!arguments.length)return r.slice();r=[];for(var i,o=0,u=e.length;o<u;++o)i=e[o],null==i||isNaN(i=+i)||r.push(i);return r.sort(n),t()},e.range=function(n){return arguments.length?(i=Ig.call(n),t()):i.slice()},e.quantiles=function(){return o.slice()},e.copy=function(){return Yo().domain(r).range(i)},e}function Bo(){function t(t){if(t<=t)return u[Sd(o,t,0,i)]}function n(){var n=-1;for(o=new Array(i);++n<i;)o[n]=((n+1)*r-(n-i)*e)/(i+1);return t}var e=0,r=1,i=1,o=[.5],u=[0,1];return t.domain=function(t){return arguments.length?(e=+t[0],r=+t[1],n()):[e,r]},t.range=function(t){return arguments.length?(i=(u=Ig.call(t)).length-1,n()):u.slice()},t.invertExtent=function(t){var n=u.indexOf(t);return n<0?[NaN,NaN]:n<1?[e,o[0]]:n>=i?[o[i-1],r]:[o[n-1],o[n]]},t.copy=function(){return Bo().domain([e,r]).range(u)},So(t)}function jo(){function t(t){if(t<=t)return e[Sd(n,t,0,r)]}var n=[.5],e=[0,1],r=1;return t.domain=function(i){return arguments.length?(n=Ig.call(i),r=Math.min(n.length,e.length-1),t):n.slice()},t.range=function(i){return arguments.length?(e=Ig.call(i),r=Math.min(n.length,e.length-1),t):e.slice()},t.invertExtent=function(t){var r=e.indexOf(t);return[n[r-1],n[r]]},t.copy=function(){return jo().domain(n).range(e)},t}function Ho(t){return new Date(t)}function Xo(t){return t instanceof Date?+t:+new Date((+t))}function Vo(t,n,r,i,o,u,a,c,s){function f(e){return(a(e)<e?_:u(e)<e?y:o(e)<e?g:i(e)<e?m:n(e)<e?r(e)<e?x:b:t(e)<e?w:M)(e)}function l(n,r,i,o){if(null==n&&(n=10),"number"==typeof n){var u=Math.abs(i-r)/n,a=e(function(t){return t[2]}).right(T,u);a===T.length?(o=p(r/Zg,i/Zg,n),n=t):a?(a=T[u/T[a-1][2]<T[a][2]/u?a-1:a],o=a[1],n=a[0]):(o=p(r,i,n),n=c)}return null==o?n:n.every(o)}var h=ko(mo,rr),d=h.invert,v=h.domain,_=s(".%L"),y=s(":%S"),g=s("%I:%M"),m=s("%I %p"),x=s("%a %d"),b=s("%b %d"),w=s("%B"),M=s("%Y"),T=[[a,1,jg],[a,5,5*jg],[a,15,15*jg],[a,30,30*jg],[u,1,Hg],[u,5,5*Hg],[u,15,15*Hg],[u,30,30*Hg],[o,1,Xg],[o,3,3*Xg],[o,6,6*Xg],[o,12,12*Xg],[i,1,Vg],[i,2,2*Vg],[r,1,Wg],[n,1,$g],[n,3,3*$g],[t,1,Zg]];return h.invert=function(t){return new Date(d(t))},h.domain=function(t){return arguments.length?v(Fg.call(t,Xo)):v().map(Ho)},h.ticks=function(t,n){var e,r=v(),i=r[0],o=r[r.length-1],u=o<i;return u&&(e=i,i=o,o=e),e=l(t,i,o,n),e=e?e.range(i,o+1):[],u?e.reverse():e},h.tickFormat=function(t,n){return null==n?f:s(n)},h.nice=function(t,n){var e=v();return(t=l(t,e[0],e[e.length-1],n))?v(Co(e,t)):h},h.copy=function(){return To(h,Vo(t,n,r,i,o,u,a,c,s))},h}function Wo(){return Vo(Ky,Jy,Dy,Ry,qy,zy,Ey,wy,t.timeFormat).domain([new Date(2e3,0,1),new Date(2e3,0,2)])}function $o(){return Vo(Mg,bg,ag,og,rg,ng,Ey,wy,t.utcFormat).domain([Date.UTC(2e3,0,1),Date.UTC(2e3,0,2)])}function Zo(t){return t.match(/.{6}/g).map(function(t){return"#"+t})}function Go(t){(t<0||t>1)&&(t-=Math.floor(t));var n=Math.abs(t-.5);return rm.h=360*t-100,rm.s=1.5-1.5*n,rm.l=.8-.9*n,rm+""}function Jo(t){var n=t.length;return function(e){return t[Math.max(0,Math.min(n-1,Math.floor(e*n)))]}}function Qo(t){function n(n){var o=(n-e)/(r-e);return t(i?Math.max(0,Math.min(1,o)):o)}var e=0,r=1,i=!1;return n.domain=function(t){return arguments.length?(e=+t[0],r=+t[1],n):[e,r]},n.clamp=function(t){return arguments.length?(i=!!t,n):i},n.interpolator=function(e){return arguments.length?(t=e,n):t},n.copy=function(){return Qo(t).domain([e,r]).clamp(i)},So(n)}function Ko(t){var n=t+="",e=n.indexOf(":");return e>=0&&"xmlns"!==(n=t.slice(0,e))&&(t=t.slice(e+1)),sm.hasOwnProperty(n)?{space:sm[n],local:t}:t}function tu(t){return function(){var n=this.ownerDocument,e=this.namespaceURI;return e===cm&&n.documentElement.namespaceURI===cm?n.createElement(t):n.createElementNS(e,t)}}function nu(t){return function(){return this.ownerDocument.createElementNS(t.space,t.local)}}function eu(t){var n=Ko(t);return(n.local?nu:tu)(n)}function ru(){return new iu}function iu(){this._="@"+(++fm).toString(36)}function ou(t,n,e){return t=uu(t,n,e),function(n){var e=n.relatedTarget;e&&(e===this||8&e.compareDocumentPosition(this))||t.call(this,n)}}function uu(n,e,r){return function(i){var o=t.event;t.event=i;try{n.call(this,this.__data__,e,r)}finally{t.event=o}}}function au(t){return t.trim().split(/^|\s+/).map(function(t){var n="",e=t.indexOf(".");return e>=0&&(n=t.slice(e+1),t=t.slice(0,e)),{type:t,name:n}})}function cu(t){return function(){var n=this.__on;if(n){for(var e,r=0,i=-1,o=n.length;r<o;++r)e=n[r],t.type&&e.type!==t.type||e.name!==t.name?n[++i]=e:this.removeEventListener(e.type,e.listener,e.capture);
+++i?n.length=i:delete this.__on}}}function su(t,n,e){var r=vm.hasOwnProperty(t.type)?ou:uu;return function(i,o,u){var a,c=this.__on,s=r(n,o,u);if(c)for(var f=0,l=c.length;f<l;++f)if((a=c[f]).type===t.type&&a.name===t.name)return this.removeEventListener(a.type,a.listener,a.capture),this.addEventListener(a.type,a.listener=s,a.capture=e),void(a.value=n);this.addEventListener(t.type,s,e),a={type:t.type,name:t.name,value:n,listener:s,capture:e},c?c.push(a):this.__on=[a]}}function fu(t,n,e){var r,i,o=au(t+""),u=o.length;{if(!(arguments.length<2)){for(a=n?su:cu,null==e&&(e=!1),r=0;r<u;++r)this.each(a(o[r],n,e));return this}var a=this.node().__on;if(a)for(var c,s=0,f=a.length;s<f;++s)for(r=0,c=a[s];r<u;++r)if((i=o[r]).type===c.type&&i.name===c.name)return c.value}}function lu(n,e,r,i){var o=t.event;n.sourceEvent=t.event,t.event=n;try{return e.apply(r,i)}finally{t.event=o}}function hu(){for(var n,e=t.event;n=e.sourceEvent;)e=n;return e}function pu(t,n){var e=t.ownerSVGElement||t;if(e.createSVGPoint){var r=e.createSVGPoint();return r.x=n.clientX,r.y=n.clientY,r=r.matrixTransform(t.getScreenCTM().inverse()),[r.x,r.y]}var i=t.getBoundingClientRect();return[n.clientX-i.left-t.clientLeft,n.clientY-i.top-t.clientTop]}function du(t){var n=hu();return n.changedTouches&&(n=n.changedTouches[0]),pu(t,n)}function vu(){}function _u(t){return null==t?vu:function(){return this.querySelector(t)}}function yu(t){"function"!=typeof t&&(t=_u(t));for(var n=this._groups,e=n.length,r=new Array(e),i=0;i<e;++i)for(var o,u,a=n[i],c=a.length,s=r[i]=new Array(c),f=0;f<c;++f)(o=a[f])&&(u=t.call(o,o.__data__,f,a))&&("__data__"in o&&(u.__data__=o.__data__),s[f]=u);return new qa(r,this._parents)}function gu(){return[]}function mu(t){return null==t?gu:function(){return this.querySelectorAll(t)}}function xu(t){"function"!=typeof t&&(t=mu(t));for(var n=this._groups,e=n.length,r=[],i=[],o=0;o<e;++o)for(var u,a=n[o],c=a.length,s=0;s<c;++s)(u=a[s])&&(r.push(t.call(u,u.__data__,s,a)),i.push(u));return new qa(r,i)}function bu(t){"function"!=typeof t&&(t=dm(t));for(var n=this._groups,e=n.length,r=new Array(e),i=0;i<e;++i)for(var o,u=n[i],a=u.length,c=r[i]=[],s=0;s<a;++s)(o=u[s])&&t.call(o,o.__data__,s,u)&&c.push(o);return new qa(r,this._parents)}function wu(t){return new Array(t.length)}function Mu(){return new qa(this._enter||this._groups.map(wu),this._parents)}function Tu(t,n){this.ownerDocument=t.ownerDocument,this.namespaceURI=t.namespaceURI,this._next=null,this._parent=t,this.__data__=n}function ku(t){return function(){return t}}function Nu(t,n,e,r,i,o){for(var u,a=0,c=n.length,s=o.length;a<s;++a)(u=n[a])?(u.__data__=o[a],r[a]=u):e[a]=new Tu(t,o[a]);for(;a<c;++a)(u=n[a])&&(i[a]=u)}function Su(t,n,e,r,i,o,u){var a,c,s,f={},l=n.length,h=o.length,p=new Array(l);for(a=0;a<l;++a)(c=n[a])&&(p[a]=s=ym+u.call(c,c.__data__,a,n),s in f?i[a]=c:f[s]=c);for(a=0;a<h;++a)s=ym+u.call(t,o[a],a,o),(c=f[s])?(r[a]=c,c.__data__=o[a],f[s]=null):e[a]=new Tu(t,o[a]);for(a=0;a<l;++a)(c=n[a])&&f[p[a]]===c&&(i[a]=c)}function Au(t,n){if(!t)return p=new Array(this.size()),s=-1,this.each(function(t){p[++s]=t}),p;var e=n?Su:Nu,r=this._parents,i=this._groups;"function"!=typeof t&&(t=ku(t));for(var o=i.length,u=new Array(o),a=new Array(o),c=new Array(o),s=0;s<o;++s){var f=r[s],l=i[s],h=l.length,p=t.call(f,f&&f.__data__,s,r),d=p.length,v=a[s]=new Array(d),_=u[s]=new Array(d),y=c[s]=new Array(h);e(f,l,v,_,y,p,n);for(var g,m,x=0,b=0;x<d;++x)if(g=v[x]){for(x>=b&&(b=x+1);!(m=_[b])&&++b<d;);g._next=m||null}}return u=new qa(u,r),u._enter=a,u._exit=c,u}function Eu(){return new qa(this._exit||this._groups.map(wu),this._parents)}function Cu(t){for(var n=this._groups,e=t._groups,r=n.length,i=e.length,o=Math.min(r,i),u=new Array(r),a=0;a<o;++a)for(var c,s=n[a],f=e[a],l=s.length,h=u[a]=new Array(l),p=0;p<l;++p)(c=s[p]||f[p])&&(h[p]=c);for(;a<r;++a)u[a]=n[a];return new qa(u,this._parents)}function zu(){for(var t=this._groups,n=-1,e=t.length;++n<e;)for(var r,i=t[n],o=i.length-1,u=i[o];--o>=0;)(r=i[o])&&(u&&u!==r.nextSibling&&u.parentNode.insertBefore(r,u),u=r);return this}function Pu(t){function n(n,e){return n&&e?t(n.__data__,e.__data__):!n-!e}t||(t=qu);for(var e=this._groups,r=e.length,i=new Array(r),o=0;o<r;++o){for(var u,a=e[o],c=a.length,s=i[o]=new Array(c),f=0;f<c;++f)(u=a[f])&&(s[f]=u);s.sort(n)}return new qa(i,this._parents).order()}function qu(t,n){return t<n?-1:t>n?1:t>=n?0:NaN}function Lu(){var t=arguments[0];return arguments[0]=this,t.apply(null,arguments),this}function Ru(){var t=new Array(this.size()),n=-1;return this.each(function(){t[++n]=this}),t}function Uu(){for(var t=this._groups,n=0,e=t.length;n<e;++n)for(var r=t[n],i=0,o=r.length;i<o;++i){var u=r[i];if(u)return u}return null}function Du(){var t=0;return this.each(function(){++t}),t}function Ou(){return!this.node()}function Fu(t){for(var n=this._groups,e=0,r=n.length;e<r;++e)for(var i,o=n[e],u=0,a=o.length;u<a;++u)(i=o[u])&&t.call(i,i.__data__,u,o);return this}function Iu(t){return function(){this.removeAttribute(t)}}function Yu(t){return function(){this.removeAttributeNS(t.space,t.local)}}function Bu(t,n){return function(){this.setAttribute(t,n)}}function ju(t,n){return function(){this.setAttributeNS(t.space,t.local,n)}}function Hu(t,n){return function(){var e=n.apply(this,arguments);null==e?this.removeAttribute(t):this.setAttribute(t,e)}}function Xu(t,n){return function(){var e=n.apply(this,arguments);null==e?this.removeAttributeNS(t.space,t.local):this.setAttributeNS(t.space,t.local,e)}}function Vu(t,n){var e=Ko(t);if(arguments.length<2){var r=this.node();return e.local?r.getAttributeNS(e.space,e.local):r.getAttribute(e)}return this.each((null==n?e.local?Yu:Iu:"function"==typeof n?e.local?Xu:Hu:e.local?ju:Bu)(e,n))}function Wu(t){return t.ownerDocument&&t.ownerDocument.defaultView||t.document&&t||t.defaultView}function $u(t){return function(){this.style.removeProperty(t)}}function Zu(t,n,e){return function(){this.style.setProperty(t,n,e)}}function Gu(t,n,e){return function(){var r=n.apply(this,arguments);null==r?this.style.removeProperty(t):this.style.setProperty(t,r,e)}}function Ju(t,n,e){var r;return arguments.length>1?this.each((null==n?$u:"function"==typeof n?Gu:Zu)(t,n,null==e?"":e)):Wu(r=this.node()).getComputedStyle(r,null).getPropertyValue(t)}function Qu(t){return function(){delete this[t]}}function Ku(t,n){return function(){this[t]=n}}function ta(t,n){return function(){var e=n.apply(this,arguments);null==e?delete this[t]:this[t]=e}}function na(t,n){return arguments.length>1?this.each((null==n?Qu:"function"==typeof n?ta:Ku)(t,n)):this.node()[t]}function ea(t){return t.trim().split(/^|\s+/)}function ra(t){return t.classList||new ia(t)}function ia(t){this._node=t,this._names=ea(t.getAttribute("class")||"")}function oa(t,n){for(var e=ra(t),r=-1,i=n.length;++r<i;)e.add(n[r])}function ua(t,n){for(var e=ra(t),r=-1,i=n.length;++r<i;)e.remove(n[r])}function aa(t){return function(){oa(this,t)}}function ca(t){return function(){ua(this,t)}}function sa(t,n){return function(){(n.apply(this,arguments)?oa:ua)(this,t)}}function fa(t,n){var e=ea(t+"");if(arguments.length<2){for(var r=ra(this.node()),i=-1,o=e.length;++i<o;)if(!r.contains(e[i]))return!1;return!0}return this.each(("function"==typeof n?sa:n?aa:ca)(e,n))}function la(){this.textContent=""}function ha(t){return function(){this.textContent=t}}function pa(t){return function(){var n=t.apply(this,arguments);this.textContent=null==n?"":n}}function da(t){return arguments.length?this.each(null==t?la:("function"==typeof t?pa:ha)(t)):this.node().textContent}function va(){this.innerHTML=""}function _a(t){return function(){this.innerHTML=t}}function ya(t){return function(){var n=t.apply(this,arguments);this.innerHTML=null==n?"":n}}function ga(t){return arguments.length?this.each(null==t?va:("function"==typeof t?ya:_a)(t)):this.node().innerHTML}function ma(){this.nextSibling&&this.parentNode.appendChild(this)}function xa(){return this.each(ma)}function ba(){this.previousSibling&&this.parentNode.insertBefore(this,this.parentNode.firstChild)}function wa(){return this.each(ba)}function Ma(t){var n="function"==typeof t?t:eu(t);return this.select(function(){return this.appendChild(n.apply(this,arguments))})}function Ta(){return null}function ka(t,n){var e="function"==typeof t?t:eu(t),r=null==n?Ta:"function"==typeof n?n:_u(n);return this.select(function(){return this.insertBefore(e.apply(this,arguments),r.apply(this,arguments)||null)})}function Na(){var t=this.parentNode;t&&t.removeChild(this)}function Sa(){return this.each(Na)}function Aa(t){return arguments.length?this.property("__data__",t):this.node().__data__}function Ea(t,n,e){var r=Wu(t),i=r.CustomEvent;i?i=new i(n,e):(i=r.document.createEvent("Event"),e?(i.initEvent(n,e.bubbles,e.cancelable),i.detail=e.detail):i.initEvent(n,!1,!1)),t.dispatchEvent(i)}function Ca(t,n){return function(){return Ea(this,t,n)}}function za(t,n){return function(){return Ea(this,t,n.apply(this,arguments))}}function Pa(t,n){return this.each(("function"==typeof n?za:Ca)(t,n))}function qa(t,n){this._groups=t,this._parents=n}function La(){return new qa([[document.documentElement]],gm)}function Ra(t){return"string"==typeof t?new qa([[document.querySelector(t)]],[document.documentElement]):new qa([[t]],gm)}function Ua(t){return"string"==typeof t?new qa([document.querySelectorAll(t)],[document.documentElement]):new qa([null==t?[]:t],gm)}function Da(t,n,e){arguments.length<3&&(e=n,n=hu().changedTouches);for(var r,i=0,o=n?n.length:0;i<o;++i)if((r=n[i]).identifier===e)return pu(t,r);return null}function Oa(t,n){null==n&&(n=hu().touches);for(var e=0,r=n?n.length:0,i=new Array(r);e<r;++e)i[e]=pu(t,n[e]);return i}function Fa(t,n,e,r,i,o){var u=t.__transition;if(u){if(e in u)return}else t.__transition={};ja(t,e,{name:n,index:r,group:i,on:mm,tween:xm,time:o.time,delay:o.delay,duration:o.duration,ease:o.ease,timer:null,state:bm})}function Ia(t,n){var e=t.__transition;if(!e||!(e=e[n])||e.state>bm)throw new Error("too late");return e}function Ya(t,n){var e=t.__transition;if(!e||!(e=e[n])||e.state>Mm)throw new Error("too late");return e}function Ba(t,n){var e=t.__transition;if(!e||!(e=e[n]))throw new Error("too late");return e}function ja(t,n,e){function r(t){e.state=wm,e.delay<=t?i(t-e.delay):e.timer.restart(i,e.delay,e.time)}function i(r){var i,c,s,f;for(i in a)f=a[i],f.name===e.name&&(f.state===Tm?(f.state=Nm,f.timer.stop(),f.on.call("interrupt",t,t.__data__,f.index,f.group),delete a[i]):+i<n&&(f.state=Nm,f.timer.stop(),delete a[i]));if(Wr(function(){e.state===Tm&&(e.timer.restart(o,e.delay,e.time),o(r))}),e.state=Mm,e.on.call("start",t,t.__data__,e.index,e.group),e.state===Mm){for(e.state=Tm,u=new Array(s=e.tween.length),i=0,c=-1;i<s;++i)(f=e.tween[i].value.call(t,t.__data__,e.index,e.group))&&(u[++c]=f);u.length=c+1}}function o(r){for(var i=r<e.duration?e.ease.call(null,r/e.duration):(e.state=km,1),o=-1,c=u.length;++o<c;)u[o].call(null,i);if(e.state===km){e.state=Nm,e.timer.stop(),e.on.call("end",t,t.__data__,e.index,e.group);for(o in a)if(+o!==n)return void delete a[n];delete t.__transition}}var u,a=t.__transition;a[n]=e,e.timer=Yr(r,0,e.time)}function Ha(t,n){var e,r,i,o=t.__transition,u=!0;if(o){n=null==n?null:n+"";for(i in o)(e=o[i]).name===n?(r=e.state===Tm,e.state=Nm,e.timer.stop(),r&&e.on.call("interrupt",t,t.__data__,e.index,e.group),delete o[i]):u=!1;u&&delete t.__transition}}function Xa(t){return this.each(function(){Ha(this,t)})}function Va(t,n){var e,r;return function(){var i=Ya(this,t),o=i.tween;if(o!==e){r=e=o;for(var u=0,a=r.length;u<a;++u)if(r[u].name===n){r=r.slice(),r.splice(u,1);break}}i.tween=r}}function Wa(t,n,e){var r,i;if("function"!=typeof e)throw new Error;return function(){var o=Ya(this,t),u=o.tween;if(u!==r){i=(r=u).slice();for(var a={name:n,value:e},c=0,s=i.length;c<s;++c)if(i[c].name===n){i[c]=a;break}c===s&&i.push(a)}o.tween=i}}function $a(t,n){var e=this._id;if(t+="",arguments.length<2){for(var r,i=Ba(this.node(),e).tween,o=0,u=i.length;o<u;++o)if((r=i[o]).name===t)return r.value;return null}return this.each((null==n?Va:Wa)(e,t,n))}function Za(t,n,e){var r=t._id;return t.each(function(){var t=Ya(this,r);(t.value||(t.value={}))[n]=e.apply(this,arguments)}),function(t){return Ba(t,r).value[n]}}function Ga(t,n){var e;return("number"==typeof n?rr:n instanceof be?S_:(e=be(n))?(n=e,S_):ar)(t,n)}function Ja(t){return function(){this.removeAttribute(t)}}function Qa(t){return function(){this.removeAttributeNS(t.space,t.local)}}function Ka(t,n,e){var r,i;return function(){var o=this.getAttribute(t);return o===e?null:o===r?i:i=n(r=o,e)}}function tc(t,n,e){var r,i;return function(){var o=this.getAttributeNS(t.space,t.local);return o===e?null:o===r?i:i=n(r=o,e)}}function nc(t,n,e){var r,i,o;return function(){var u,a=e(this);return null==a?void this.removeAttribute(t):(u=this.getAttribute(t),u===a?null:u===r&&a===i?o:o=n(r=u,i=a))}}function ec(t,n,e){var r,i,o;return function(){var u,a=e(this);return null==a?void this.removeAttributeNS(t.space,t.local):(u=this.getAttributeNS(t.space,t.local),u===a?null:u===r&&a===i?o:o=n(r=u,i=a))}}function rc(t,n){var e=Ko(t),r="transform"===e?R_:Ga;return this.attrTween(t,"function"==typeof n?(e.local?ec:nc)(e,r,Za(this,"attr."+t,n)):null==n?(e.local?Qa:Ja)(e):(e.local?tc:Ka)(e,r,n))}function ic(t,n){function e(){var e=this,r=n.apply(e,arguments);return r&&function(n){e.setAttributeNS(t.space,t.local,r(n))}}return e._value=n,e}function oc(t,n){function e(){var e=this,r=n.apply(e,arguments);return r&&function(n){e.setAttribute(t,r(n))}}return e._value=n,e}function uc(t,n){var e="attr."+t;if(arguments.length<2)return(e=this.tween(e))&&e._value;if(null==n)return this.tween(e,null);if("function"!=typeof n)throw new Error;var r=Ko(t);return this.tween(e,(r.local?ic:oc)(r,n))}function ac(t,n){return function(){Ia(this,t).delay=+n.apply(this,arguments)}}function cc(t,n){return n=+n,function(){Ia(this,t).delay=n}}function sc(t){var n=this._id;return arguments.length?this.each(("function"==typeof t?ac:cc)(n,t)):Ba(this.node(),n).delay}function fc(t,n){return function(){Ya(this,t).duration=+n.apply(this,arguments)}}function lc(t,n){return n=+n,function(){Ya(this,t).duration=n}}function hc(t){var n=this._id;return arguments.length?this.each(("function"==typeof t?fc:lc)(n,t)):Ba(this.node(),n).duration}function pc(t,n){if("function"!=typeof n)throw new Error;return function(){Ya(this,t).ease=n}}function dc(t){var n=this._id;return arguments.length?this.each(pc(n,t)):Ba(this.node(),n).ease}function vc(t){"function"!=typeof t&&(t=dm(t));for(var n=this._groups,e=n.length,r=new Array(e),i=0;i<e;++i)for(var o,u=n[i],a=u.length,c=r[i]=[],s=0;s<a;++s)(o=u[s])&&t.call(o,o.__data__,s,u)&&c.push(o);return new Uc(r,this._parents,this._name,this._id)}function _c(t){if(t._id!==this._id)throw new Error;for(var n=this._groups,e=t._groups,r=n.length,i=e.length,o=Math.min(r,i),u=new Array(r),a=0;a<o;++a)for(var c,s=n[a],f=e[a],l=s.length,h=u[a]=new Array(l),p=0;p<l;++p)(c=s[p]||f[p])&&(h[p]=c);for(;a<r;++a)u[a]=n[a];return new Uc(u,this._parents,this._name,this._id)}function yc(t){return(t+"").trim().split(/^|\s+/).every(function(t){var n=t.indexOf(".");return n>=0&&(t=t.slice(0,n)),!t||"start"===t})}function gc(t,n,e){var r,i,o=yc(n)?Ia:Ya;return function(){var u=o(this,t),a=u.on;a!==r&&(i=(r=a).copy()).on(n,e),u.on=i}}function mc(t,n){var e=this._id;return arguments.length<2?Ba(this.node(),e).on.on(t):this.each(gc(e,t,n))}function xc(t){return function(){var n=this.parentNode;for(var e in this.__transition)if(+e!==t)return;n&&n.removeChild(this)}}function bc(){return this.on("end.remove",xc(this._id))}function wc(t){var n=this._name,e=this._id;"function"!=typeof t&&(t=_u(t));for(var r=this._groups,i=r.length,o=new Array(i),u=0;u<i;++u)for(var a,c,s=r[u],f=s.length,l=o[u]=new Array(f),h=0;h<f;++h)(a=s[h])&&(c=t.call(a,a.__data__,h,s))&&("__data__"in a&&(c.__data__=a.__data__),l[h]=c,Fa(l[h],n,e,h,l,Ba(a,e)));return new Uc(o,this._parents,n,e)}function Mc(t){var n=this._name,e=this._id;"function"!=typeof t&&(t=mu(t));for(var r=this._groups,i=r.length,o=[],u=[],a=0;a<i;++a)for(var c,s=r[a],f=s.length,l=0;l<f;++l)if(c=s[l]){for(var h,p=t.call(c,c.__data__,l,s),d=Ba(c,e),v=0,_=p.length;v<_;++v)(h=p[v])&&Fa(h,n,e,v,p,d);o.push(p),u.push(c)}return new Uc(o,u,n,e)}function Tc(){return new Sm(this._groups,this._parents)}function kc(t,n){var e,r,i;return function(){var o=Wu(this).getComputedStyle(this,null),u=o.getPropertyValue(t),a=(this.style.removeProperty(t),o.getPropertyValue(t));return u===a?null:u===e&&a===r?i:i=n(e=u,r=a)}}function Nc(t){return function(){this.style.removeProperty(t)}}function Sc(t,n,e){var r,i;return function(){var o=Wu(this).getComputedStyle(this,null).getPropertyValue(t);return o===e?null:o===r?i:i=n(r=o,e)}}function Ac(t,n,e){var r,i,o;return function(){var u=Wu(this).getComputedStyle(this,null),a=u.getPropertyValue(t),c=e(this);return null==c&&(this.style.removeProperty(t),c=u.getPropertyValue(t)),a===c?null:a===r&&c===i?o:o=n(r=a,i=c)}}function Ec(t,n,e){var r="transform"==(t+="")?L_:Ga;return null==n?this.styleTween(t,kc(t,r)).on("end.style."+t,Nc(t)):this.styleTween(t,"function"==typeof n?Ac(t,r,Za(this,"style."+t,n)):Sc(t,r,n),e)}function Cc(t,n,e){function r(){var r=this,i=n.apply(r,arguments);return i&&function(n){r.style.setProperty(t,i(n),e)}}return r._value=n,r}function zc(t,n,e){var r="style."+(t+="");if(arguments.length<2)return(r=this.tween(r))&&r._value;if(null==n)return this.tween(r,null);if("function"!=typeof n)throw new Error;return this.tween(r,Cc(t,n,null==e?"":e))}function Pc(t){return function(){this.textContent=t}}function qc(t){return function(){var n=t(this);this.textContent=null==n?"":n}}function Lc(t){return this.tween("text","function"==typeof t?qc(Za(this,"text",t)):Pc(null==t?"":t+""))}function Rc(){for(var t=this._name,n=this._id,e=Oc(),r=this._groups,i=r.length,o=0;o<i;++o)for(var u,a=r[o],c=a.length,s=0;s<c;++s)if(u=a[s]){var f=Ba(u,n);Fa(u,t,e,s,a,{time:f.time+f.delay+f.duration,delay:0,duration:f.duration,ease:f.ease})}return new Uc(r,this._parents,t,e)}function Uc(t,n,e,r){this._groups=t,this._parents=n,this._name=e,this._id=r}function Dc(t){return La().transition(t)}function Oc(){return++Am}function Fc(t,n){for(var e;!(e=t.__transition)||!(e=e[n]);)if(!(t=t.parentNode))return Cm.time=Or(),Cm;return e}function Ic(t){var n,e;t instanceof Uc?(n=t._id,t=t._name):(n=Oc(),(e=Cm).time=Or(),t=null==t?null:t+"");for(var r=this._groups,i=r.length,o=0;o<i;++o)for(var u,a=r[o],c=a.length,s=0;s<c;++s)(u=a[s])&&Fa(u,t,n,s,a,e||Fc(u,n));return new Uc(r,this._parents,t,n)}function Yc(t,n){var e,r,i=t.__transition;if(i){n=null==n?null:n+"";for(r in i)if((e=i[r]).state>wm&&e.name===n)return new Uc([[t]],zm,n,(+r))}return null}function Bc(t){return t}function jc(t,n,e){var r=t(e);return"translate("+(isFinite(r)?r:n(e))+",0)"}function Hc(t,n,e){var r=t(e);return"translate(0,"+(isFinite(r)?r:n(e))+")"}function Xc(t){var n=t.bandwidth()/2;return function(e){return t(e)+n}}function Vc(){return!this.__axis}function Wc(t,n){function e(e){var s,f=null==i?n.ticks?n.ticks.apply(n,r):n.domain():i,l=null==o?n.tickFormat?n.tickFormat.apply(n,r):Bc:o,h=Math.max(u,0)+c,p=t===qm||t===Rm?jc:Hc,d=n.range(),v=d[0]+.5,_=d[d.length-1]+.5,y=(n.bandwidth?Xc:Bc)(n.copy()),g=e.selection?e.selection():e,m=g.selectAll(".domain").data([null]),x=g.selectAll(".tick").data(f,n).order(),b=x.exit(),w=x.enter().append("g").attr("class","tick"),M=x.select("line"),T=x.select("text"),k=t===qm||t===Um?-1:1,N=t===Um||t===Lm?(s="x","y"):(s="y","x");m=m.merge(m.enter().insert("path",".tick").attr("class","domain").attr("stroke","#000")),x=x.merge(w),M=M.merge(w.append("line").attr("stroke","#000").attr(s+"2",k*u).attr(N+"1",.5).attr(N+"2",.5)),T=T.merge(w.append("text").attr("fill","#000").attr(s,k*h).attr(N,.5).attr("dy",t===qm?"0em":t===Rm?".71em":".32em")),e!==g&&(m=m.transition(e),x=x.transition(e),M=M.transition(e),T=T.transition(e),b=b.transition(e).attr("opacity",Dm).attr("transform",function(t){return p(y,this.parentNode.__axis||y,t)}),w.attr("opacity",Dm).attr("transform",function(t){return p(this.parentNode.__axis||y,y,t)})),b.remove(),m.attr("d",t===Um||t==Lm?"M"+k*a+","+v+"H0.5V"+_+"H"+k*a:"M"+v+","+k*a+"V0.5H"+_+"V"+k*a),x.attr("opacity",1).attr("transform",function(t){return p(y,y,t)}),M.attr(s+"2",k*u),T.attr(s,k*h).text(l),g.filter(Vc).attr("fill","none").attr("font-size",10).attr("font-family","sans-serif").attr("text-anchor",t===Lm?"start":t===Um?"end":"middle"),g.each(function(){this.__axis=y})}var r=[],i=null,o=null,u=6,a=6,c=3;return e.scale=function(t){return arguments.length?(n=t,e):n},e.ticks=function(){return r=Pm.call(arguments),e},e.tickArguments=function(t){return arguments.length?(r=null==t?[]:Pm.call(t),e):r.slice()},e.tickValues=function(t){return arguments.length?(i=null==t?null:Pm.call(t),e):i&&i.slice()},e.tickFormat=function(t){return arguments.length?(o=t,e):o},e.tickSize=function(t){return arguments.length?(u=a=+t,e):u},e.tickSizeInner=function(t){return arguments.length?(u=+t,e):u},e.tickSizeOuter=function(t){return arguments.length?(a=+t,e):a},e.tickPadding=function(t){return arguments.length?(c=+t,e):c},e}function $c(t){return Wc(qm,t)}function Zc(t){return Wc(Lm,t)}function Gc(t){return Wc(Rm,t)}function Jc(t){return Wc(Um,t)}function Qc(t,n){return t.parent===n.parent?1:2}function Kc(t){return t.reduce(ts,0)/t.length}function ts(t,n){return t+n.x}function ns(t){return 1+t.reduce(es,0)}function es(t,n){return Math.max(t,n.y)}function rs(t){for(var n;n=t.children;)t=n[0];return t}function is(t){for(var n;n=t.children;)t=n[n.length-1];return t}function os(){function t(t){var o,u=0;t.eachAfter(function(t){var e=t.children;e?(t.x=Kc(e),t.y=ns(e)):(t.x=o?u+=n(t,o):0,t.y=0,o=t)});var a=rs(t),c=is(t),s=a.x-n(a,c)/2,f=c.x+n(c,a)/2;return t.eachAfter(i?function(n){n.x=(n.x-t.x)*e,n.y=(t.y-n.y)*r}:function(n){n.x=(n.x-s)/(f-s)*e,n.y=(1-(t.y?n.y/t.y:1))*r})}var n=Qc,e=1,r=1,i=!1;return t.separation=function(e){return arguments.length?(n=e,t):n},t.size=function(n){return arguments.length?(i=!1,e=+n[0],r=+n[1],t):i?null:[e,r]},t.nodeSize=function(n){return arguments.length?(i=!0,e=+n[0],r=+n[1],t):i?[e,r]:null},t}function us(t){var n,e,r,i,o=this,u=[o];do for(n=u.reverse(),u=[];o=n.pop();)if(t(o),e=o.children)for(r=0,i=e.length;r<i;++r)u.push(e[r]);while(u.length);return this}function as(t){for(var n,e,r=this,i=[r];r=i.pop();)if(t(r),n=r.children)for(e=n.length-1;e>=0;--e)i.push(n[e]);return this}function cs(t){for(var n,e,r,i=this,o=[i],u=[];i=o.pop();)if(u.push(i),n=i.children)for(e=0,r=n.length;e<r;++e)o.push(n[e]);for(;i=u.pop();)t(i);return this}function ss(t){return this.eachAfter(function(n){for(var e=+t(n.data)||0,r=n.children,i=r&&r.length;--i>=0;)e+=r[i].value;n.value=e})}function fs(t){return this.eachBefore(function(n){n.children&&n.children.sort(t)})}function ls(t){for(var n=this,e=hs(n,t),r=[n];n!==e;)n=n.parent,r.push(n);for(var i=r.length;t!==e;)r.splice(i,0,t),t=t.parent;return r}function hs(t,n){if(t===n)return t;var e=t.ancestors(),r=n.ancestors(),i=null;for(t=e.pop(),n=r.pop();t===n;)i=t,t=e.pop(),n=r.pop();return i}function ps(){for(var t=this,n=[t];t=t.parent;)n.push(t);return n}function ds(){var t=[];return this.each(function(n){t.push(n)}),t}function vs(){var t=[];return this.eachBefore(function(n){n.children||t.push(n)}),t}function _s(){var t=this,n=[];return t.each(function(e){e!==t&&n.push({source:e.parent,target:e})}),n}function ys(t,n){var e,r,i,o,u,a=new ws(t),c=+t.value&&(a.value=t.value),s=[a];for(null==n&&(n=ms);e=s.pop();)if(c&&(e.value=+e.data.value),(i=n(e.data))&&(u=i.length))for(e.children=new Array(u),o=u-1;o>=0;--o)s.push(r=e.children[o]=new ws(i[o])),r.parent=e,r.depth=e.depth+1;return a.eachBefore(bs)}function gs(){return ys(this).eachBefore(xs)}function ms(t){return t.children}function xs(t){t.data=t.data.data}function bs(t){var n=0;do t.height=n;while((t=t.parent)&&t.height<++n)}function ws(t){this.data=t,this.depth=this.height=0,this.parent=null}function Ms(t){this._=t,this.next=null}function Ts(t){for(var n,e=(t=t.slice()).length,r=null,i=r;e;){var o=new Ms(t[e-1]);i=i?i.next=o:r=o,t[n]=t[--e]}return{head:r,tail:i}}function ks(t){return Ss(Ts(t),[])}function Ns(t,n){var e=n.x-t.x,r=n.y-t.y,i=t.r-n.r;return i*i+1e-6>e*e+r*r}function Ss(t,n){var e,r,i,o=null,u=t.head;switch(n.length){case 1:e=As(n[0]);break;case 2:e=Es(n[0],n[1]);break;case 3:e=Cs(n[0],n[1],n[2])}for(;u;)i=u._,r=u.next,e&&Ns(e,i)?o=u:(o?(t.tail=o,o.next=null):t.head=t.tail=null,n.push(i),e=Ss(t,n),n.pop(),t.head?(u.next=t.head,t.head=u):(u.next=null,t.head=t.tail=u),o=t.tail,o.next=r),u=r;return t.tail=o,e}function As(t){return{x:t.x,y:t.y,r:t.r}}function Es(t,n){var e=t.x,r=t.y,i=t.r,o=n.x,u=n.y,a=n.r,c=o-e,s=u-r,f=a-i,l=Math.sqrt(c*c+s*s);return{x:(e+o+c/l*f)/2,y:(r+u+s/l*f)/2,r:(l+i+a)/2}}function Cs(t,n,e){var r=t.x,i=t.y,o=t.r,u=n.x,a=n.y,c=n.r,s=e.x,f=e.y,l=e.r,h=2*(r-u),p=2*(i-a),d=2*(c-o),v=r*r+i*i-o*o-u*u-a*a+c*c,_=2*(r-s),y=2*(i-f),g=2*(l-o),m=r*r+i*i-o*o-s*s-f*f+l*l,x=_*p-h*y,b=(p*m-y*v)/x-r,w=(y*d-p*g)/x,M=(_*v-h*m)/x-i,T=(h*g-_*d)/x,k=w*w+T*T-1,N=2*(b*w+M*T+o),S=b*b+M*M-o*o,A=(-N-Math.sqrt(N*N-4*k*S))/(2*k);return{x:b+w*A+r,y:M+T*A+i,r:A}}function zs(t,n,e){var r=t.x,i=t.y,o=n.r+e.r,u=t.r+e.r,a=n.x-r,c=n.y-i,s=a*a+c*c;if(s){var f=.5+((u*=u)-(o*=o))/(2*s),l=Math.sqrt(Math.max(0,2*o*(u+s)-(u-=s)*u-o*o))/(2*s);e.x=r+f*a+l*c,e.y=i+f*c-l*a}else e.x=r+u,e.y=i}function Ps(t,n){var e=n.x-t.x,r=n.y-t.y,i=t.r+n.r;return i*i>e*e+r*r}function qs(t,n,e){var r=t.x-n,i=t.y-e;return r*r+i*i}function Ls(t){this._=t,this.next=null,this.previous=null}function Rs(t){if(!(i=t.length))return 0;var n,e,r,i;if(n=t[0],n.x=0,n.y=0,!(i>1))return n.r;if(e=t[1],n.x=-e.r,e.x=n.r,e.y=0,!(i>2))return n.r+e.r;zs(e,n,r=t[2]);var o,u,a,c,s,f,l,h=n.r*n.r,p=e.r*e.r,d=r.r*r.r,v=h+p+d,_=h*n.x+p*e.x+d*r.x,y=h*n.y+p*e.y+d*r.y;n=new Ls(n),e=new Ls(e),r=new Ls(r),n.next=r.previous=e,e.next=n.previous=r,r.next=e.previous=n;t:for(a=3;a<i;++a){if(zs(n._,e._,r=t[a]),r=new Ls(r),(s=n.previous)===(c=e.next)){if(Ps(c._,r._)){n=e,e=c,--a;continue t}}else{f=c._.r,l=s._.r;do if(f<=l){if(Ps(c._,r._)){e=c,n.next=e,e.previous=n,--a;continue t}c=c.next,f+=c._.r}else{if(Ps(s._,r._)){n=s,n.next=e,e.previous=n,--a;continue t}s=s.previous,l+=s._.r}while(c!==s.next)}for(r.previous=n,r.next=e,n.next=e.previous=e=r,v+=d=r._.r*r._.r,_+=d*r._.x,y+=d*r._.y,h=qs(n._,o=_/v,u=y/v);(r=r.next)!==e;)(d=qs(r._,o,u))<h&&(n=r,h=d);e=n.next}for(n=[e._],r=e;(r=r.next)!==e;)n.push(r._);for(r=ks(n),a=0;a<i;++a)n=t[a],n.x-=r.x,n.y-=r.y;return r.r}function Us(t){return Rs(t),t}function Ds(t){return null==t?null:Os(t)}function Os(t){if("function"!=typeof t)throw new Error;return t}function Fs(){return 0}function Is(t){return function(){return t}}function Ys(t){return Math.sqrt(t.value)}function Bs(){function t(t){return t.x=e/2,t.y=r/2,n?t.eachBefore(js(n)).eachAfter(Hs(i,.5)).eachBefore(Xs(1)):t.eachBefore(js(Ys)).eachAfter(Hs(Fs,1)).eachAfter(Hs(i,t.r/Math.min(e,r))).eachBefore(Xs(Math.min(e,r)/(2*t.r))),t}var n=null,e=1,r=1,i=Fs;return t.radius=function(e){return arguments.length?(n=Ds(e),t):n},t.size=function(n){return arguments.length?(e=+n[0],r=+n[1],t):[e,r]},t.padding=function(n){return arguments.length?(i="function"==typeof n?n:Is(+n),t):i},t}function js(t){return function(n){n.children||(n.r=Math.max(0,+t(n)||0))}}function Hs(t,n){return function(e){if(r=e.children){var r,i,o,u=r.length,a=t(e)*n||0;if(a)for(i=0;i<u;++i)r[i].r+=a;if(o=Rs(r),a)for(i=0;i<u;++i)r[i].r-=a;e.r=o+a}}}function Xs(t){return function(n){var e=n.parent;n.r*=t,e&&(n.x=e.x+t*n.x,n.y=e.y+t*n.y)}}function Vs(t){t.x0=Math.round(t.x0),t.y0=Math.round(t.y0),t.x1=Math.round(t.x1),t.y1=Math.round(t.y1)}function Ws(t,n,e,r,i){for(var o,u=t.children,a=-1,c=u.length,s=t.value&&(r-n)/t.value;++a<c;)o=u[a],o.y0=e,o.y1=i,o.x0=n,o.x1=n+=o.value*s}function $s(){function t(t){var u=t.height+1;return t.x0=t.y0=i,t.x1=e,t.y1=r/u,t.eachBefore(n(r,u)),o&&t.eachBefore(Vs),t}function n(t,n){return function(e){e.children&&Ws(e,e.x0,t*(e.depth+1)/n,e.x1,t*(e.depth+2)/n);var r=e.x0,o=e.y0,u=e.x1-i,a=e.y1-i;u<r&&(r=u=(r+u)/2),a<o&&(o=a=(o+a)/2),e.x0=r,e.y0=o,e.x1=u,e.y1=a}}var e=1,r=1,i=0,o=!1;return t.round=function(n){return arguments.length?(o=!!n,t):o},t.size=function(n){return arguments.length?(e=+n[0],r=+n[1],t):[e,r]},t.padding=function(n){return arguments.length?(i=+n,t):i},t}function Zs(t){return t.id}function Gs(t){return t.parentId}function Js(){function t(t){var r,i,o,u,a,c,s,f=t.length,l=new Array(f),h={};for(i=0;i<f;++i)r=t[i],a=l[i]=new ws(r),null!=(c=n(r,i,t))&&(c+="")&&(s=Om+(a.id=c),h[s]=s in h?Im:a);for(i=0;i<f;++i)if(a=l[i],c=e(t[i],i,t),null!=c&&(c+="")){if(u=h[Om+c],!u)throw new Error("missing: "+c);if(u===Im)throw new Error("ambiguous: "+c);u.children?u.children.push(a):u.children=[a],a.parent=u}else{if(o)throw new Error("multiple roots");o=a}if(!o)throw new Error("no root");if(o.parent=Fm,o.eachBefore(function(t){t.depth=t.parent.depth+1,--f}).eachBefore(bs),o.parent=null,f>0)throw new Error("cycle");return o}var n=Zs,e=Gs;return t.id=function(e){return arguments.length?(n=Os(e),t):n},t.parentId=function(n){return arguments.length?(e=Os(n),t):e},t}function Qs(t,n){return t.parent===n.parent?1:2}function Ks(t){var n=t.children;return n?n[0]:t.t}function tf(t){var n=t.children;return n?n[n.length-1]:t.t}function nf(t,n,e){var r=e/(n.i-t.i);n.c-=r,n.s+=e,t.c+=r,n.z+=e,n.m+=e}function ef(t){for(var n,e=0,r=0,i=t.children,o=i.length;--o>=0;)n=i[o],n.z+=e,n.m+=e,e+=n.s+(r+=n.c)}function rf(t,n,e){return t.a.parent===n.parent?t.a:e}function of(t,n){this._=t,this.parent=null,this.children=null,this.A=null,this.a=this,this.z=0,this.m=0,this.c=0,this.s=0,this.t=null,this.i=n}function uf(t){for(var n,e,r,i,o,u=new of(t,0),a=[u];n=a.pop();)if(r=n._.children)for(n.children=new Array(o=r.length),i=o-1;i>=0;--i)a.push(e=n.children[i]=new of(r[i],i)),e.parent=n;return(u.parent=new of(null,0)).children=[u],u}function af(){function t(t){var r=uf(t);if(r.eachAfter(n),r.parent.m=-r.z,r.eachBefore(e),c)t.eachBefore(i);else{var s=t,f=t,l=t;t.eachBefore(function(t){t.x<s.x&&(s=t),t.x>f.x&&(f=t),t.depth>l.depth&&(l=t)});var h=s===f?1:o(s,f)/2,p=h-s.x,d=u/(f.x+h+p),v=a/(l.depth||1);t.eachBefore(function(t){t.x=(t.x+p)*d,t.y=t.depth*v})}return t}function n(t){var n=t.children,e=t.parent.children,i=t.i?e[t.i-1]:null;if(n){ef(t);var u=(n[0].z+n[n.length-1].z)/2;i?(t.z=i.z+o(t._,i._),t.m=t.z-u):t.z=u}else i&&(t.z=i.z+o(t._,i._));t.parent.A=r(t,i,t.parent.A||e[0])}function e(t){t._.x=t.z+t.parent.m,t.m+=t.parent.m}function r(t,n,e){if(n){for(var r,i=t,u=t,a=n,c=i.parent.children[0],s=i.m,f=u.m,l=a.m,h=c.m;a=tf(a),i=Ks(i),a&&i;)c=Ks(c),u=tf(u),u.a=t,r=a.z+l-i.z-s+o(a._,i._),r>0&&(nf(rf(a,t,e),t,r),s+=r,f+=r),l+=a.m,s+=i.m,h+=c.m,f+=u.m;a&&!tf(u)&&(u.t=a,u.m+=l-f),i&&!Ks(c)&&(c.t=i,c.m+=s-h,e=t)}return e}function i(t){t.x*=u,t.y=t.depth*a}var o=Qs,u=1,a=1,c=null;return t.separation=function(n){return arguments.length?(o=n,t):o},t.size=function(n){return arguments.length?(c=!1,u=+n[0],a=+n[1],t):c?null:[u,a]},t.nodeSize=function(n){return arguments.length?(c=!0,u=+n[0],a=+n[1],t):c?[u,a]:null},t}function cf(t,n,e,r,i){for(var o,u=t.children,a=-1,c=u.length,s=t.value&&(i-e)/t.value;++a<c;)o=u[a],o.x0=n,o.x1=r,o.y0=e,o.y1=e+=o.value*s}function sf(t,n,e,r,i,o){for(var u,a,c,s,f,l,h,p,d,v,_,y,g=[],m=n.children,x=0,b=m.length,w=n.value;x<b;){for(s=i-e,f=o-r,h=p=l=m[x].value,_=Math.max(f/s,s/f)/(w*t),y=l*l*_,v=Math.max(p/y,y/h),c=x+1;c<b;++c){if(l+=a=m[c].value,a<h&&(h=a),a>p&&(p=a),y=l*l*_,d=Math.max(p/y,y/h),d>v){l-=a;break}v=d}g.push(u={value:l,dice:s<f,children:m.slice(x,c)}),u.dice?Ws(u,e,r,i,w?r+=f*l/w:o):cf(u,e,r,w?e+=s*l/w:i,o),w-=l,x=c}return g}function ff(){function t(t){return t.x0=t.y0=0,t.x1=i,t.y1=o,t.eachBefore(n),u=[0],r&&t.eachBefore(Vs),t}function n(t){var n=u[t.depth],r=t.x0+n,i=t.y0+n,o=t.x1-n,h=t.y1-n;o<r&&(r=o=(r+o)/2),h<i&&(i=h=(i+h)/2),t.x0=r,t.y0=i,t.x1=o,t.y1=h,t.children&&(n=u[t.depth+1]=a(t)/2,r+=l(t)-n,i+=c(t)-n,o-=s(t)-n,h-=f(t)-n,o<r&&(r=o=(r+o)/2),h<i&&(i=h=(i+h)/2),e(t,r,i,o,h))}
+var e=Bm,r=!1,i=1,o=1,u=[0],a=Fs,c=Fs,s=Fs,f=Fs,l=Fs;return t.round=function(n){return arguments.length?(r=!!n,t):r},t.size=function(n){return arguments.length?(i=+n[0],o=+n[1],t):[i,o]},t.tile=function(n){return arguments.length?(e=Os(n),t):e},t.padding=function(n){return arguments.length?t.paddingInner(n).paddingOuter(n):t.paddingInner()},t.paddingInner=function(n){return arguments.length?(a="function"==typeof n?n:Is(+n),t):a},t.paddingOuter=function(n){return arguments.length?t.paddingTop(n).paddingRight(n).paddingBottom(n).paddingLeft(n):t.paddingTop()},t.paddingTop=function(n){return arguments.length?(c="function"==typeof n?n:Is(+n),t):c},t.paddingRight=function(n){return arguments.length?(s="function"==typeof n?n:Is(+n),t):s},t.paddingBottom=function(n){return arguments.length?(f="function"==typeof n?n:Is(+n),t):f},t.paddingLeft=function(n){return arguments.length?(l="function"==typeof n?n:Is(+n),t):l},t}function lf(t,n,e,r,i){function o(t,n,e,r,i,u,a){if(t>=n-1){var s=c[t];return s.x0=r,s.y0=i,s.x1=u,s.y1=a,void 0}for(var l=f[t],h=e/2+l,p=t+1,d=n-1;p<d;){var v=p+d>>>1;f[v]<h?p=v+1:d=v}var _=f[p]-l,y=e-_;if(a-i>u-r){var g=(i*y+a*_)/e;o(t,p,_,r,i,u,g),o(p,n,y,r,g,u,a)}else{var m=(r*y+u*_)/e;o(t,p,_,r,i,m,a),o(p,n,y,m,i,u,a)}}var u,a,c=t.children,s=c.length,f=new Array(s+1);for(f[0]=a=u=0;u<s;++u)f[u+1]=a+=c[u].value;o(0,s,t.value,n,e,r,i)}function hf(t,n,e,r,i){(1&t.depth?cf:Ws)(t,n,e,r,i)}function pf(t,n){function e(){var e,i,o=r.length,u=0,a=0;for(e=0;e<o;++e)i=r[e],u+=i.x,a+=i.y;for(u=u/o-t,a=a/o-n,e=0;e<o;++e)i=r[e],i.x-=u,i.y-=a}var r;return null==t&&(t=0),null==n&&(n=0),e.initialize=function(t){r=t},e.x=function(n){return arguments.length?(t=+n,e):t},e.y=function(t){return arguments.length?(n=+t,e):n},e}function df(t){return function(){return t}}function vf(){return 1e-6*(Math.random()-.5)}function _f(t){return t.x+t.vx}function yf(t){return t.y+t.vy}function gf(t){function n(){function t(t,e,r,i,u){var a=t.data,p=t.r,d=l+p;{if(!a)return e>s+d||i<s-d||r>f+d||u<f-d;if(a.index>n){var v=s-a.x-a.vx,_=f-a.y-a.vy,y=v*v+_*_;y<d*d&&(0===v&&(v=vf(),y+=v*v),0===_&&(_=vf(),y+=_*_),y=(d-(y=Math.sqrt(y)))/y*o,c.vx+=(v*=y)*(d=(p*=p)/(h+p)),c.vy+=(_*=y)*d,a.vx-=v*(d=1-d),a.vy-=_*d)}}}for(var n,a,c,s,f,l,h,p=r.length,d=0;d<u;++d)for(a=jt(r,_f,yf).visitAfter(e),n=0;n<p;++n)c=r[n],l=i[n],h=l*l,s=c.x+c.vx,f=c.y+c.vy,a.visit(t)}function e(t){if(t.data)return t.r=i[t.data.index];for(var n=t.r=0;n<4;++n)t[n]&&t[n].r>t.r&&(t.r=t[n].r)}var r,i,o=1,u=1;return"function"!=typeof t&&(t=df(null==t?1:+t)),n.initialize=function(n){var e,o=(r=n).length;for(i=new Array(o),e=0;e<o;++e)i[e]=+t(r[e],e,r)},n.iterations=function(t){return arguments.length?(u=+t,n):u},n.strength=function(t){return arguments.length?(o=+t,n):o},n.radius=function(e){return arguments.length?(t="function"==typeof e?e:df(+e),n):t},n}function mf(t,n){return n}function xf(t){function n(t){return 1/Math.min(s[t.source.index],s[t.target.index])}function e(n){for(var e=0,r=t.length;e<d;++e)for(var i,o,c,s,l,h,p,v=0;v<r;++v)i=t[v],o=i.source,c=i.target,s=c.x+c.vx-o.x-o.vx||vf(),l=c.y+c.vy-o.y-o.vy||vf(),h=Math.sqrt(s*s+l*l),h=(h-a[v])/h*n*u[v],s*=h,l*=h,c.vx-=s*(p=f[v]),c.vy-=l*p,o.vx+=s*(p=1-p),o.vy+=l*p}function r(){if(c){var n,e,r=c.length,h=t.length,p=q(c,l);for(n=0,s=new Array(r);n<r;++n)s[n]=0;for(n=0;n<h;++n)e=t[n],e.index=n,"object"!=typeof e.source&&(e.source=p.get(e.source)),"object"!=typeof e.target&&(e.target=p.get(e.target)),++s[e.source.index],++s[e.target.index];for(n=0,f=new Array(h);n<h;++n)e=t[n],f[n]=s[e.source.index]/(s[e.source.index]+s[e.target.index]);u=new Array(h),i(),a=new Array(h),o()}}function i(){if(c)for(var n=0,e=t.length;n<e;++n)u[n]=+h(t[n],n,t)}function o(){if(c)for(var n=0,e=t.length;n<e;++n)a[n]=+p(t[n],n,t)}var u,a,c,s,f,l=mf,h=n,p=df(30),d=1;return null==t&&(t=[]),e.initialize=function(t){c=t,r()},e.links=function(n){return arguments.length?(t=n,r(),e):t},e.id=function(t){return arguments.length?(l=t,e):l},e.iterations=function(t){return arguments.length?(d=+t,e):d},e.strength=function(t){return arguments.length?(h="function"==typeof t?t:df(+t),i(),e):h},e.distance=function(t){return arguments.length?(p="function"==typeof t?t:df(+t),o(),e):p},e}function bf(t){return t.x}function wf(t){return t.y}function Mf(t){function n(){e(),p.call("tick",o),u<a&&(h.stop(),p.call("end",o))}function e(){var n,e,r=t.length;for(u+=(s-u)*c,l.each(function(t){t(u)}),n=0;n<r;++n)e=t[n],null==e.fx?e.x+=e.vx*=f:(e.x=e.fx,e.vx=0),null==e.fy?e.y+=e.vy*=f:(e.y=e.fy,e.vy=0)}function r(){for(var n,e=0,r=t.length;e<r;++e){if(n=t[e],n.index=e,isNaN(n.x)||isNaN(n.y)){var i=Hm*Math.sqrt(e),o=e*Xm;n.x=i*Math.cos(o),n.y=i*Math.sin(o)}(isNaN(n.vx)||isNaN(n.vy))&&(n.vx=n.vy=0)}}function i(n){return n.initialize&&n.initialize(t),n}var o,u=1,a=.001,c=1-Math.pow(a,1/300),s=0,f=.6,l=q(),h=Yr(n),p=Mr("tick","end");return null==t&&(t=[]),r(),o={tick:e,restart:function(){return h.restart(n),o},stop:function(){return h.stop(),o},nodes:function(n){return arguments.length?(t=n,r(),l.each(i),o):t},alpha:function(t){return arguments.length?(u=+t,o):u},alphaMin:function(t){return arguments.length?(a=+t,o):a},alphaDecay:function(t){return arguments.length?(c=+t,o):+c},alphaTarget:function(t){return arguments.length?(s=+t,o):s},velocityDecay:function(t){return arguments.length?(f=1-t,o):1-f},force:function(t,n){return arguments.length>1?(null==n?l.remove(t):l.set(t,i(n)),o):l.get(t)},find:function(n,e,r){var i,o,u,a,c,s=0,f=t.length;for(null==r?r=1/0:r*=r,s=0;s<f;++s)a=t[s],i=n-a.x,o=e-a.y,u=i*i+o*o,u<r&&(c=a,r=u);return c},on:function(t,n){return arguments.length>1?(p.on(t,n),o):p.on(t)}}}function Tf(){function t(t){var n,a=i.length,c=jt(i,bf,wf).visitAfter(e);for(u=t,n=0;n<a;++n)o=i[n],c.visit(r)}function n(){if(i){var t,n=i.length;for(a=new Array(n),t=0;t<n;++t)a[t]=+c(i[t],t,i)}}function e(t){var n,e,r,i,o,u=0;if(t.length){for(r=i=o=0;o<4;++o)(n=t[o])&&(e=n.value)&&(u+=e,r+=e*n.x,i+=e*n.y);t.x=r/u,t.y=i/u}else{n=t,n.x=n.data.x,n.y=n.data.y;do u+=a[n.data.index];while(n=n.next)}t.value=u}function r(t,n,e,r){if(!t.value)return!0;var i=t.x-o.x,c=t.y-o.y,h=r-n,p=i*i+c*c;if(h*h/l<p)return p<f&&(0===i&&(i=vf(),p+=i*i),0===c&&(c=vf(),p+=c*c),p<s&&(p=Math.sqrt(s*p)),o.vx+=i*t.value*u/p,o.vy+=c*t.value*u/p),!0;if(!(t.length||p>=f)){(t.data!==o||t.next)&&(0===i&&(i=vf(),p+=i*i),0===c&&(c=vf(),p+=c*c),p<s&&(p=Math.sqrt(s*p)));do t.data!==o&&(h=a[t.data.index]*u/p,o.vx+=i*h,o.vy+=c*h);while(t=t.next)}}var i,o,u,a,c=df(-30),s=1,f=1/0,l=.81;return t.initialize=function(t){i=t,n()},t.strength=function(e){return arguments.length?(c="function"==typeof e?e:df(+e),n(),t):c},t.distanceMin=function(n){return arguments.length?(s=n*n,t):Math.sqrt(s)},t.distanceMax=function(n){return arguments.length?(f=n*n,t):Math.sqrt(f)},t.theta=function(n){return arguments.length?(l=n*n,t):Math.sqrt(l)},t}function kf(t){function n(t){for(var n,e=0,u=r.length;e<u;++e)n=r[e],n.vx+=(o[e]-n.x)*i[e]*t}function e(){if(r){var n,e=r.length;for(i=new Array(e),o=new Array(e),n=0;n<e;++n)i[n]=isNaN(o[n]=+t(r[n],n,r))?0:+u(r[n],n,r)}}var r,i,o,u=df(.1);return"function"!=typeof t&&(t=df(null==t?0:+t)),n.initialize=function(t){r=t,e()},n.strength=function(t){return arguments.length?(u="function"==typeof t?t:df(+t),e(),n):u},n.x=function(r){return arguments.length?(t="function"==typeof r?r:df(+r),e(),n):t},n}function Nf(t){function n(t){for(var n,e=0,u=r.length;e<u;++e)n=r[e],n.vy+=(o[e]-n.y)*i[e]*t}function e(){if(r){var n,e=r.length;for(i=new Array(e),o=new Array(e),n=0;n<e;++n)i[n]=isNaN(o[n]=+t(r[n],n,r))?0:+u(r[n],n,r)}}var r,i,o,u=df(.1);return"function"!=typeof t&&(t=df(null==t?0:+t)),n.initialize=function(t){r=t,e()},n.strength=function(t){return arguments.length?(u="function"==typeof t?t:df(+t),e(),n):u},n.y=function(r){return arguments.length?(t="function"==typeof r?r:df(+r),e(),n):t},n}function Sf(){t.event.stopImmediatePropagation()}function Af(){t.event.preventDefault(),t.event.stopImmediatePropagation()}function Ef(t){var n=t.document.documentElement,e=Ra(t).on("dragstart.drag",Af,!0);"onselectstart"in n?e.on("selectstart.drag",Af,!0):(n.__noselect=n.style.MozUserSelect,n.style.MozUserSelect="none")}function Cf(t,n){var e=t.document.documentElement,r=Ra(t).on("dragstart.drag",null);n&&(r.on("click.drag",Af,!0),setTimeout(function(){r.on("click.drag",null)},0)),"onselectstart"in e?r.on("selectstart.drag",null):(e.style.MozUserSelect=e.__noselect,delete e.__noselect)}function zf(t){return function(){return t}}function Pf(t,n,e,r,i,o,u,a,c,s){this.target=t,this.type=n,this.subject=e,this.identifier=r,this.active=i,this.x=o,this.y=u,this.dx=a,this.dy=c,this._=s}function qf(){return!t.event.button}function Lf(){return this.parentNode}function Rf(n){return null==n?{x:t.event.x,y:t.event.y}:n}function Uf(){function n(t){t.on("mousedown.drag",e).on("touchstart.drag",o).on("touchmove.drag",u).on("touchend.drag touchcancel.drag",a).style("-webkit-tap-highlight-color","rgba(0,0,0,0)")}function e(){if(!f&&l.apply(this,arguments)){var n=c("mouse",h.apply(this,arguments),du,this,arguments);n&&(Ra(t.event.view).on("mousemove.drag",r,!0).on("mouseup.drag",i,!0),Ef(t.event.view),Sf(),s=!1,n("start"))}}function r(){Af(),s=!0,d.mouse("drag")}function i(){Ra(t.event.view).on("mousemove.drag mouseup.drag",null),Cf(t.event.view,s),Af(),d.mouse("end")}function o(){if(l.apply(this,arguments)){var n,e,r=t.event.changedTouches,i=h.apply(this,arguments),o=r.length;for(n=0;n<o;++n)(e=c(r[n].identifier,i,Da,this,arguments))&&(Sf(),e("start"))}}function u(){var n,e,r=t.event.changedTouches,i=r.length;for(n=0;n<i;++n)(e=d[r[n].identifier])&&(Af(),e("drag"))}function a(){var n,e,r=t.event.changedTouches,i=r.length;for(f&&clearTimeout(f),f=setTimeout(function(){f=null},500),n=0;n<i;++n)(e=d[r[n].identifier])&&(Sf(),e("end"))}function c(e,r,i,o,u){var a,c,s,f=i(r,e),l=v.copy();if(lu(new Pf(n,"beforestart",a,e,_,f[0],f[1],0,0,l),function(){return null!=(t.event.subject=a=p.apply(o,u))&&(c=a.x-f[0]||0,s=a.y-f[1]||0,!0)}))return function h(t){var p,v=f;switch(t){case"start":d[e]=h,p=_++;break;case"end":delete d[e],--_;case"drag":f=i(r,e),p=_}lu(new Pf(n,t,a,e,p,f[0]+c,f[1]+s,f[0]-v[0],f[1]-v[1],l),l.apply,l,[t,o,u])}}var s,f,l=qf,h=Lf,p=Rf,d={},v=Mr("start","drag","end"),_=0;return n.filter=function(t){return arguments.length?(l="function"==typeof t?t:zf(!!t),n):l},n.container=function(t){return arguments.length?(h="function"==typeof t?t:zf(t),n):h},n.subject=function(t){return arguments.length?(p="function"==typeof t?t:zf(t),n):p},n.on=function(){var t=v.on.apply(v,arguments);return t===v?n:t},n}function Df(t){return function(){return t}}function Of(t){return t[0]}function Ff(t){return t[1]}function If(){this._=null}function Yf(t){t.U=t.C=t.L=t.R=t.P=t.N=null}function Bf(t,n){var e=n,r=n.R,i=e.U;i?i.L===e?i.L=r:i.R=r:t._=r,r.U=i,e.U=r,e.R=r.L,e.R&&(e.R.U=e),r.L=e}function jf(t,n){var e=n,r=n.L,i=e.U;i?i.L===e?i.L=r:i.R=r:t._=r,r.U=i,e.U=r,e.L=r.R,e.L&&(e.L.U=e),r.R=e}function Hf(t){for(;t.L;)t=t.L;return t}function Xf(t,n,e,r){var i=[null,null],o=Gm.push(i)-1;return i.left=t,i.right=n,e&&Wf(i,t,n,e),r&&Wf(i,n,t,r),$m[t.index].halfedges.push(o),$m[n.index].halfedges.push(o),i}function Vf(t,n,e){var r=[n,e];return r.left=t,r}function Wf(t,n,e,r){t[0]||t[1]?t.left===e?t[1]=r:t[0]=r:(t[0]=r,t.left=n,t.right=e)}function $f(t,n,e,r,i){var o,u=t[0],a=t[1],c=u[0],s=u[1],f=a[0],l=a[1],h=0,p=1,d=f-c,v=l-s;if(o=n-c,d||!(o>0)){if(o/=d,d<0){if(o<h)return;o<p&&(p=o)}else if(d>0){if(o>p)return;o>h&&(h=o)}if(o=r-c,d||!(o<0)){if(o/=d,d<0){if(o>p)return;o>h&&(h=o)}else if(d>0){if(o<h)return;o<p&&(p=o)}if(o=e-s,v||!(o>0)){if(o/=v,v<0){if(o<h)return;o<p&&(p=o)}else if(v>0){if(o>p)return;o>h&&(h=o)}if(o=i-s,v||!(o<0)){if(o/=v,v<0){if(o>p)return;o>h&&(h=o)}else if(v>0){if(o<h)return;o<p&&(p=o)}return!(h>0||p<1)||(h>0&&(t[0]=[c+h*d,s+h*v]),p<1&&(t[1]=[c+p*d,s+p*v]),!0)}}}}}function Zf(t,n,e,r,i){var o=t[1];if(o)return!0;var u,a,c=t[0],s=t.left,f=t.right,l=s[0],h=s[1],p=f[0],d=f[1],v=(l+p)/2,_=(h+d)/2;if(d===h){if(v<n||v>=r)return;if(l>p){if(c){if(c[1]>=i)return}else c=[v,e];o=[v,i]}else{if(c){if(c[1]<e)return}else c=[v,i];o=[v,e]}}else if(u=(l-p)/(d-h),a=_-u*v,u<-1||u>1)if(l>p){if(c){if(c[1]>=i)return}else c=[(e-a)/u,e];o=[(i-a)/u,i]}else{if(c){if(c[1]<e)return}else c=[(i-a)/u,i];o=[(e-a)/u,e]}else if(h<d){if(c){if(c[0]>=r)return}else c=[n,u*n+a];o=[r,u*r+a]}else{if(c){if(c[0]<n)return}else c=[r,u*r+a];o=[n,u*n+a]}return t[0]=c,t[1]=o,!0}function Gf(t,n,e,r){for(var i,o=Gm.length;o--;)Zf(i=Gm[o],t,n,e,r)&&$f(i,t,n,e,r)&&(Math.abs(i[0][0]-i[1][0])>Km||Math.abs(i[0][1]-i[1][1])>Km)||delete Gm[o]}function Jf(t){return $m[t.index]={site:t,halfedges:[]}}function Qf(t,n){var e=t.site,r=n.left,i=n.right;return e===i&&(i=r,r=e),i?Math.atan2(i[1]-r[1],i[0]-r[0]):(e===r?(r=n[1],i=n[0]):(r=n[0],i=n[1]),Math.atan2(r[0]-i[0],i[1]-r[1]))}function Kf(t,n){return n[+(n.left!==t.site)]}function tl(t,n){return n[+(n.left===t.site)]}function nl(){for(var t,n,e,r,i=0,o=$m.length;i<o;++i)if((t=$m[i])&&(r=(n=t.halfedges).length)){var u=new Array(r),a=new Array(r);for(e=0;e<r;++e)u[e]=e,a[e]=Qf(t,Gm[n[e]]);for(u.sort(function(t,n){return a[n]-a[t]}),e=0;e<r;++e)a[e]=n[u[e]];for(e=0;e<r;++e)n[e]=a[e]}}function el(t,n,e,r){var i,o,u,a,c,s,f,l,h,p,d,v,_=$m.length,y=!0;for(i=0;i<_;++i)if(o=$m[i]){for(u=o.site,c=o.halfedges,a=c.length;a--;)Gm[c[a]]||c.splice(a,1);for(a=0,s=c.length;a<s;)p=tl(o,Gm[c[a]]),d=p[0],v=p[1],f=Kf(o,Gm[c[++a%s]]),l=f[0],h=f[1],(Math.abs(d-l)>Km||Math.abs(v-h)>Km)&&(c.splice(a,0,Gm.push(Vf(u,p,Math.abs(d-t)<Km&&r-v>Km?[t,Math.abs(l-t)<Km?h:r]:Math.abs(v-r)<Km&&e-d>Km?[Math.abs(h-r)<Km?l:e,r]:Math.abs(d-e)<Km&&v-n>Km?[e,Math.abs(l-e)<Km?h:n]:Math.abs(v-n)<Km&&d-t>Km?[Math.abs(h-n)<Km?l:t,n]:null))-1),++s);s&&(y=!1)}if(y){var g,m,x,b=1/0;for(i=0,y=null;i<_;++i)(o=$m[i])&&(u=o.site,g=u[0]-t,m=u[1]-n,x=g*g+m*m,x<b&&(b=x,y=o));if(y){var w=[t,n],M=[t,r],T=[e,r],k=[e,n];y.halfedges.push(Gm.push(Vf(u=y.site,w,M))-1,Gm.push(Vf(u,M,T))-1,Gm.push(Vf(u,T,k))-1,Gm.push(Vf(u,k,w))-1)}}for(i=0;i<_;++i)(o=$m[i])&&(o.halfedges.length||delete $m[i])}function rl(){Yf(this),this.x=this.y=this.arc=this.site=this.cy=null}function il(t){var n=t.P,e=t.N;if(n&&e){var r=n.site,i=t.site,o=e.site;if(r!==o){var u=i[0],a=i[1],c=r[0]-u,s=r[1]-a,f=o[0]-u,l=o[1]-a,h=2*(c*l-s*f);if(!(h>=-tx)){var p=c*c+s*s,d=f*f+l*l,v=(l*p-s*d)/h,_=(c*d-f*p)/h,y=Jm.pop()||new rl;y.arc=t,y.site=i,y.x=v+u,y.y=(y.cy=_+a)+Math.sqrt(v*v+_*_),t.circle=y;for(var g=null,m=Zm._;m;)if(y.y<m.y||y.y===m.y&&y.x<=m.x){if(!m.L){g=m.P;break}m=m.L}else{if(!m.R){g=m;break}m=m.R}Zm.insert(g,y),g||(Vm=y)}}}}function ol(t){var n=t.circle;n&&(n.P||(Vm=n.N),Zm.remove(n),Jm.push(n),Yf(n),t.circle=null)}function ul(){Yf(this),this.edge=this.site=this.circle=null}function al(t){var n=Qm.pop()||new ul;return n.site=t,n}function cl(t){ol(t),Wm.remove(t),Qm.push(t),Yf(t)}function sl(t){var n=t.circle,e=n.x,r=n.cy,i=[e,r],o=t.P,u=t.N,a=[t];cl(t);for(var c=o;c.circle&&Math.abs(e-c.circle.x)<Km&&Math.abs(r-c.circle.cy)<Km;)o=c.P,a.unshift(c),cl(c),c=o;a.unshift(c),ol(c);for(var s=u;s.circle&&Math.abs(e-s.circle.x)<Km&&Math.abs(r-s.circle.cy)<Km;)u=s.N,a.push(s),cl(s),s=u;a.push(s),ol(s);var f,l=a.length;for(f=1;f<l;++f)s=a[f],c=a[f-1],Wf(s.edge,c.site,s.site,i);c=a[0],s=a[l-1],s.edge=Xf(c.site,s.site,null,i),il(c),il(s)}function fl(t){for(var n,e,r,i,o=t[0],u=t[1],a=Wm._;a;)if(r=ll(a,u)-o,r>Km)a=a.L;else{if(i=o-hl(a,u),!(i>Km)){r>-Km?(n=a.P,e=a):i>-Km?(n=a,e=a.N):n=e=a;break}if(!a.R){n=a;break}a=a.R}Jf(t);var c=al(t);if(Wm.insert(n,c),n||e){if(n===e)return ol(n),e=al(n.site),Wm.insert(c,e),c.edge=e.edge=Xf(n.site,c.site),il(n),void il(e);if(!e)return void(c.edge=Xf(n.site,c.site));ol(n),ol(e);var s=n.site,f=s[0],l=s[1],h=t[0]-f,p=t[1]-l,d=e.site,v=d[0]-f,_=d[1]-l,y=2*(h*_-p*v),g=h*h+p*p,m=v*v+_*_,x=[(_*g-p*m)/y+f,(h*m-v*g)/y+l];Wf(e.edge,s,d,x),c.edge=Xf(s,t,null,x),e.edge=Xf(t,d,null,x),il(n),il(e)}}function ll(t,n){var e=t.site,r=e[0],i=e[1],o=i-n;if(!o)return r;var u=t.P;if(!u)return-(1/0);e=u.site;var a=e[0],c=e[1],s=c-n;if(!s)return a;var f=a-r,l=1/o-1/s,h=f/s;return l?(-h+Math.sqrt(h*h-2*l*(f*f/(-2*s)-c+s/2+i-o/2)))/l+r:(r+a)/2}function hl(t,n){var e=t.N;if(e)return ll(e,n);var r=t.site;return r[1]===n?r[0]:1/0}function pl(t,n,e){return(t[0]-e[0])*(n[1]-t[1])-(t[0]-n[0])*(e[1]-t[1])}function dl(t,n){return n[1]-t[1]||n[0]-t[0]}function vl(t,n){var e,r,i,o=t.sort(dl).pop();for(Gm=[],$m=new Array(t.length),Wm=new If,Zm=new If;;)if(i=Vm,o&&(!i||o[1]<i.y||o[1]===i.y&&o[0]<i.x))o[0]===e&&o[1]===r||(fl(o),e=o[0],r=o[1]),o=t.pop();else{if(!i)break;sl(i.arc)}if(nl(),n){var u=+n[0][0],a=+n[0][1],c=+n[1][0],s=+n[1][1];Gf(u,a,c,s),el(u,a,c,s)}this.edges=Gm,this.cells=$m,Wm=Zm=Gm=$m=null}function _l(){function t(t){return new vl(t.map(function(r,i){var o=[Math.round(n(r,i,t)/Km)*Km,Math.round(e(r,i,t)/Km)*Km];return o.index=i,o.data=r,o}),r)}var n=Of,e=Ff,r=null;return t.polygons=function(n){return t(n).polygons()},t.links=function(n){return t(n).links()},t.triangles=function(n){return t(n).triangles()},t.x=function(e){return arguments.length?(n="function"==typeof e?e:Df(+e),t):n},t.y=function(n){return arguments.length?(e="function"==typeof n?n:Df(+n),t):e},t.extent=function(n){return arguments.length?(r=null==n?null:[[+n[0][0],+n[0][1]],[+n[1][0],+n[1][1]]],t):r&&[[r[0][0],r[0][1]],[r[1][0],r[1][1]]]},t.size=function(n){return arguments.length?(r=null==n?null:[[0,0],[+n[0],+n[1]]],t):r&&[r[1][0],r[1][1]]},t}function yl(t){return function(){return t}}function gl(t,n,e){this.target=t,this.type=n,this.transform=e}function ml(t,n,e){this.k=t,this.x=n,this.y=e}function xl(t){return t.__zoom||nx}function bl(){t.event.stopImmediatePropagation()}function wl(){t.event.preventDefault(),t.event.stopImmediatePropagation()}function Ml(){return!t.event.button}function Tl(){var t,n,e=this;return e instanceof SVGElement?(e=e.ownerSVGElement||e,t=e.width.baseVal.value,n=e.height.baseVal.value):(t=e.clientWidth,n=e.clientHeight),[[0,0],[t,n]]}function kl(){return this.__zoom||nx}function Nl(){function n(t){t.on("wheel.zoom",s).on("mousedown.zoom",f).on("dblclick.zoom",l).on("touchstart.zoom",h).on("touchmove.zoom",p).on("touchend.zoom touchcancel.zoom",d).style("-webkit-tap-highlight-color","rgba(0,0,0,0)").property("__zoom",kl)}function e(t,n){return n=Math.max(M,Math.min(T,n)),n===t.k?t:new ml(n,t.x,t.y)}function r(t,n,e){var r=n[0]-e[0]*t.k,i=n[1]-e[1]*t.k;return r===t.x&&i===t.y?t:new ml(t.k,r,i)}function i(t,n){var e=Math.min(0,t.invertX(n[0][0])-k)||Math.max(0,t.invertX(n[1][0])-N),r=Math.min(0,t.invertY(n[0][1])-S)||Math.max(0,t.invertY(n[1][1])-A);return e||r?t.translate(e,r):t}function o(t){return[(+t[0][0]+ +t[1][0])/2,(+t[0][1]+ +t[1][1])/2]}function u(t,n,e){t.on("start.zoom",function(){a(this,arguments).start()}).on("interrupt.zoom end.zoom",function(){a(this,arguments).end()}).tween("zoom",function(){var t=this,r=arguments,i=a(t,r),u=w.apply(t,r),c=e||o(u),s=Math.max(u[1][0]-u[0][0],u[1][1]-u[0][1]),f=t.__zoom,l="function"==typeof n?n.apply(t,r):n,h=yr(f.invert(c).concat(s/f.k),l.invert(c).concat(s/l.k));return function(t){if(1===t)t=l;else{var n=h(t),e=s/n[2];t=new ml(e,c[0]-n[0]*e,c[1]-n[1]*e)}i.zoom(null,t)}})}function a(t,n){for(var e,r=0,i=C.length;r<i;++r)if((e=C[r]).that===t)return e;return new c(t,n)}function c(t,n){this.that=t,this.args=n,this.index=-1,this.active=0}function s(){function n(){x=null,o.end()}if(b.apply(this,arguments)){var o=a(this,arguments),u=this.__zoom,c=Math.max(M,Math.min(T,u.k*Math.pow(2,-t.event.deltaY*(t.event.deltaMode?120:1)/500)));if(x){var s=du(this);_[0]===s[0]&&_[1]===s[1]||(y=u.invert(_=s)),clearTimeout(x)}else{if(u.k===c)return;o.extent=w.apply(this,arguments),y=u.invert(_=du(this)),Ha(this),o.start()}wl(),x=setTimeout(n,q),o.zoom("mouse",i(r(e(u,c),_,y),o.extent))}}function f(){function n(){wl(),v=!0,o.zoom("mouse",i(r(o.that.__zoom,_=du(o.that),y),o.extent))}function e(){u.on("mousemove.zoom mouseup.zoom",null),Cf(t.event.view,v),wl(),o.end()}if(!m&&b.apply(this,arguments)){var o=a(this,arguments),u=Ra(t.event.view).on("mousemove.zoom",n,!0).on("mouseup.zoom",e,!0);Ef(t.event.view),bl(),v=!1,o.extent=w.apply(this,arguments),y=this.__zoom.invert(_=du(this)),Ha(this),o.start()}}function l(){if(b.apply(this,arguments)){var o=this.__zoom,a=du(this),c=o.invert(a),s=o.k*(t.event.shiftKey?.5:2),f=i(r(e(o,s),a,c),w.apply(this,arguments));wl(),E>0?Ra(this).transition().duration(E).call(u,f,a):Ra(this).call(n.transform,f)}}function h(){if(b.apply(this,arguments)){var n,e,r,i=a(this,arguments),o=t.event.changedTouches,u=o.length;for(bl(),n=0;n<u;++n)e=o[n],r=Da(this,o,e.identifier),r=[r,this.__zoom.invert(r),e.identifier],i.touch0?i.touch1||(i.touch1=r):i.touch0=r;return g&&(g=clearTimeout(g),!i.touch1)?(i.end(),l.apply(this,arguments)):void(t.event.touches.length===u&&(g=setTimeout(function(){g=null},P),Ha(this),i.extent=w.apply(this,arguments),i.start()))}}function p(){var n,o,u,c,s=a(this,arguments),f=t.event.changedTouches,l=f.length;for(wl(),g&&(g=clearTimeout(g)),n=0;n<l;++n)o=f[n],u=Da(this,f,o.identifier),s.touch0&&s.touch0[2]===o.identifier?s.touch0[0]=u:s.touch1&&s.touch1[2]===o.identifier&&(s.touch1[0]=u);if(o=s.that.__zoom,s.touch1){var h=s.touch0[0],p=s.touch0[1],d=s.touch1[0],v=s.touch1[1],_=(_=d[0]-h[0])*_+(_=d[1]-h[1])*_,y=(y=v[0]-p[0])*y+(y=v[1]-p[1])*y;o=e(o,Math.sqrt(_/y)),u=[(h[0]+d[0])/2,(h[1]+d[1])/2],c=[(p[0]+v[0])/2,(p[1]+v[1])/2]}else{if(!s.touch0)return;u=s.touch0[0],c=s.touch0[1]}s.zoom("touch",i(r(o,u,c),s.extent))}function d(){var n,e,r=a(this,arguments),i=t.event.changedTouches,o=i.length;for(bl(),m&&clearTimeout(m),m=setTimeout(function(){m=null},P),n=0;n<o;++n)e=i[n],r.touch0&&r.touch0[2]===e.identifier?delete r.touch0:r.touch1&&r.touch1[2]===e.identifier&&delete r.touch1;r.touch1&&!r.touch0&&(r.touch0=r.touch1,delete r.touch1),r.touch0||r.end()}var v,_,y,g,m,x,b=Ml,w=Tl,M=0,T=1/0,k=-T,N=T,S=k,A=N,E=250,C=[],z=Mr("start","zoom","end"),P=500,q=150;return n.transform=function(t,n){var e=t.selection?t.selection():t;e.property("__zoom",kl),t!==e?u(t,n):e.interrupt().each(function(){a(this,arguments).start().zoom(null,"function"==typeof n?n.apply(this,arguments):n).end()})},n.scaleBy=function(t,e){n.scaleTo(t,function(){var t=this.__zoom.k,n="function"==typeof e?e.apply(this,arguments):e;return t*n})},n.scaleTo=function(t,u){n.transform(t,function(){var t=w.apply(this,arguments),n=this.__zoom,a=o(t),c=n.invert(a),s="function"==typeof u?u.apply(this,arguments):u;return i(r(e(n,s),a,c),t)})},n.translateBy=function(t,e,r){n.transform(t,function(){return i(this.__zoom.translate("function"==typeof e?e.apply(this,arguments):e,"function"==typeof r?r.apply(this,arguments):r),w.apply(this,arguments))})},c.prototype={start:function(){return 1===++this.active&&(this.index=C.push(this)-1,this.emit("start")),this},zoom:function(t,n){return _&&"mouse"!==t&&(y=n.invert(_)),this.touch0&&"touch"!==t&&(this.touch0[1]=n.invert(this.touch0[0])),this.touch1&&"touch"!==t&&(this.touch1[1]=n.invert(this.touch1[0])),this.that.__zoom=n,this.emit("zoom"),this},end:function(){return 0===--this.active&&(C.splice(this.index,1),_=y=null,this.index=-1,this.emit("end")),this},emit:function(t){lu(new gl(n,t,this.that.__zoom),z.apply,z,[t,this.that,this.args])}},n.filter=function(t){return arguments.length?(b="function"==typeof t?t:yl(!!t),n):b},n.extent=function(t){return arguments.length?(w="function"==typeof t?t:yl([[+t[0][0],+t[0][1]],[+t[1][0],+t[1][1]]]),n):w},n.scaleExtent=function(t){return arguments.length?(M=+t[0],T=+t[1],n):[M,T]},n.translateExtent=function(t){return arguments.length?(k=+t[0][0],N=+t[1][0],S=+t[0][1],A=+t[1][1],n):[[k,S],[N,A]]},n.duration=function(t){return arguments.length?(E=+t,n):E},n.on=function(){var t=z.on.apply(z,arguments);return t===z?n:t},n}function Sl(t){return function(){return t}}function Al(t,n,e){this.target=t,this.type=n,this.selection=e}function El(){t.event.stopImmediatePropagation()}function Cl(){t.event.preventDefault(),t.event.stopImmediatePropagation()}function zl(t){return{type:t}}function Pl(){return!t.event.button}function ql(){var t=this.ownerSVGElement||this;return[[0,0],[t.width.baseVal.value,t.height.baseVal.value]]}function Ll(t){for(;!t.__brush;)if(!(t=t.parentNode))return;return t.__brush}function Rl(t){return t[0][0]===t[1][0]||t[0][1]===t[1][1]}function Ul(t){var n=t.__brush;return n?n.dim.output(n.selection):null}function Dl(){return Il(ux)}function Ol(){return Il(ax)}function Fl(){return Il(cx)}function Il(n){function e(t){var e=t.property("__brush",a).selectAll(".overlay").data([zl("overlay")]);e.enter().append("rect").attr("class","overlay").attr("pointer-events","all").attr("cursor",sx.overlay).merge(e).each(function(){var t=Ll(this).extent;Ra(this).attr("x",t[0][0]).attr("y",t[0][1]).attr("width",t[1][0]-t[0][0]).attr("height",t[1][1]-t[0][1])}),t.selectAll(".selection").data([zl("selection")]).enter().append("rect").attr("class","selection").attr("cursor",sx.selection).attr("fill","#777").attr("fill-opacity",.3).attr("stroke","#fff").attr("shape-rendering","crispEdges");var i=t.selectAll(".handle").data(n.handles,function(t){return t.type});i.exit().remove(),i.enter().append("rect").attr("class",function(t){return"handle handle--"+t.type}).attr("cursor",function(t){return sx[t.type]}),t.each(r).attr("fill","none").attr("pointer-events","all").style("-webkit-tap-highlight-color","rgba(0,0,0,0)").on("mousedown.brush touchstart.brush",u)}function r(){var t=Ra(this),n=Ll(this).selection;n?(t.selectAll(".selection").style("display",null).attr("x",n[0][0]).attr("y",n[0][1]).attr("width",n[1][0]-n[0][0]).attr("height",n[1][1]-n[0][1]),t.selectAll(".handle").style("display",null).attr("x",function(t){return"e"===t.type[t.type.length-1]?n[1][0]-h/2:n[0][0]-h/2}).attr("y",function(t){return"s"===t.type[0]?n[1][1]-h/2:n[0][1]-h/2}).attr("width",function(t){return"n"===t.type||"s"===t.type?n[1][0]-n[0][0]+h:h}).attr("height",function(t){return"e"===t.type||"w"===t.type?n[1][1]-n[0][1]+h:h})):t.selectAll(".selection,.handle").style("display","none").attr("x",null).attr("y",null).attr("width",null).attr("height",null)}function i(t,n){return t.__brush.emitter||new o(t,n)}function o(t,n){this.that=t,this.args=n,this.state=t.__brush,this.active=0}function u(){function e(){var t=du(T);!U||w||M||(Math.abs(t[0]-O[0])>Math.abs(t[1]-O[1])?M=!0:w=!0),O=t,b=!0,Cl(),o()}function o(){var t;switch(m=O[0]-D[0],x=O[1]-D[1],N){case rx:case ex:S&&(m=Math.max(P-l,Math.min(L-v,m)),h=l+m,_=v+m),A&&(x=Math.max(q-p,Math.min(R-y,x)),d=p+x,g=y+x);break;case ix:S<0?(m=Math.max(P-l,Math.min(L-l,m)),h=l+m,_=v):S>0&&(m=Math.max(P-v,Math.min(L-v,m)),h=l,_=v+m),A<0?(x=Math.max(q-p,Math.min(R-p,x)),d=p+x,g=y):A>0&&(x=Math.max(q-y,Math.min(R-y,x)),d=p,g=y+x);break;case ox:S&&(h=Math.max(P,Math.min(L,l-m*S)),_=Math.max(P,Math.min(L,v+m*S))),A&&(d=Math.max(q,Math.min(R,p-x*A)),g=Math.max(q,Math.min(R,y+x*A)))}_<h&&(S*=-1,t=l,l=v,v=t,t=h,h=_,_=t,k in fx&&Y.attr("cursor",sx[k=fx[k]])),g<d&&(A*=-1,t=p,p=y,y=t,t=d,d=g,g=t,k in lx&&Y.attr("cursor",sx[k=lx[k]])),z=E.selection,w&&(h=z[0][0],_=z[1][0]),M&&(d=z[0][1],g=z[1][1]),z[0][0]===h&&z[0][1]===d&&z[1][0]===_&&z[1][1]===g||(E.selection=[[h,d],[_,g]],r.call(T),F.brush())}function u(){if(El(),t.event.touches){if(t.event.touches.length)return;c&&clearTimeout(c),c=setTimeout(function(){c=null},500),I.on("touchmove.brush touchend.brush touchcancel.brush",null)}else Cf(t.event.view,b),B.on("keydown.brush keyup.brush mousemove.brush mouseup.brush",null);I.attr("pointer-events","all"),Y.attr("cursor",sx.overlay),Rl(z)&&(E.selection=null,r.call(T)),F.end()}function a(){switch(t.event.keyCode){case 16:U=S&&A;break;case 18:N===ix&&(S&&(v=_-m*S,l=h+m*S),A&&(y=g-x*A,p=d+x*A),N=ox,o());break;case 32:N!==ix&&N!==ox||(S<0?v=_-m:S>0&&(l=h-m),A<0?y=g-x:A>0&&(p=d-x),N=rx,Y.attr("cursor",sx.selection),o());break;default:return}Cl()}function s(){switch(t.event.keyCode){case 16:U&&(w=M=U=!1,o());break;case 18:N===ox&&(S<0?v=_:S>0&&(l=h),A<0?y=g:A>0&&(p=d),N=ix,o());break;case 32:N===rx&&(t.event.altKey?(S&&(v=_-m*S,l=h+m*S),A&&(y=g-x*A,p=d+x*A),N=ox):(S<0?v=_:S>0&&(l=h),A<0?y=g:A>0&&(p=d),N=ix),Y.attr("cursor",sx[k]),o());break;default:return}Cl()}if(t.event.touches){if(t.event.changedTouches.length<t.event.touches.length)return Cl()}else if(c)return;if(f.apply(this,arguments)){var l,h,p,d,v,_,y,g,m,x,b,w,M,T=this,k=t.event.target.__data__.type,N="selection"===(t.event.metaKey?k="overlay":k)?ex:t.event.altKey?ox:ix,S=n===ax?null:hx[k],A=n===ux?null:px[k],E=Ll(T),C=E.extent,z=E.selection,P=C[0][0],q=C[0][1],L=C[1][0],R=C[1][1],U=S&&A&&t.event.shiftKey,D=du(T),O=D,F=i(T,arguments).beforestart();"overlay"===k?E.selection=z=[[l=n===ax?P:D[0],p=n===ux?q:D[1]],[v=n===ax?L:l,y=n===ux?R:p]]:(l=z[0][0],p=z[0][1],v=z[1][0],y=z[1][1]),h=l,d=p,_=v,g=y;var I=Ra(T).attr("pointer-events","none"),Y=I.selectAll(".overlay").attr("cursor",sx[k]);if(t.event.touches)I.on("touchmove.brush",e,!0).on("touchend.brush touchcancel.brush",u,!0);else{var B=Ra(t.event.view).on("keydown.brush",a,!0).on("keyup.brush",s,!0).on("mousemove.brush",e,!0).on("mouseup.brush",u,!0);Ef(t.event.view)}El(),Ha(T),r.call(T),F.start()}}function a(){var t=this.__brush||{selection:null};return t.extent=s.apply(this,arguments),t.dim=n,t}var c,s=ql,f=Pl,l=Mr(e,"start","brush","end"),h=6;return e.move=function(t,e){t.selection?t.on("start.brush",function(){i(this,arguments).beforestart().start()}).on("interrupt.brush end.brush",function(){i(this,arguments).end()}).tween("brush",function(){function t(t){u.selection=1===t&&Rl(s)?null:f(t),r.call(o),a.brush()}var o=this,u=o.__brush,a=i(o,arguments),c=u.selection,s=n.input("function"==typeof e?e.apply(this,arguments):e,u.extent),f=cr(c,s);return c&&s?t:t(1)}):t.each(function(){var t=this,o=arguments,u=t.__brush,a=n.input("function"==typeof e?e.apply(t,o):e,u.extent),c=i(t,o).beforestart();Ha(t),u.selection=null==a||Rl(a)?null:a,r.call(t),c.start().brush().end()})},o.prototype={beforestart:function(){return 1===++this.active&&(this.state.emitter=this,this.starting=!0),this},start:function(){return this.starting&&(this.starting=!1,this.emit("start")),this},brush:function(){return this.emit("brush"),this},end:function(){return 0===--this.active&&(delete this.state.emitter,this.emit("end")),this},emit:function(t){lu(new Al(e,t,n.output(this.state.selection)),l.apply,l,[t,this.that,this.args])}},e.extent=function(t){return arguments.length?(s="function"==typeof t?t:Sl([[+t[0][0],+t[0][1]],[+t[1][0],+t[1][1]]]),e):s},e.filter=function(t){return arguments.length?(f="function"==typeof t?t:Sl(!!t),e):f},e.handleSize=function(t){return arguments.length?(h=+t,e):h},e.on=function(){var t=l.on.apply(l,arguments);return t===l?e:t},e}function Yl(t){return function(n,e){return t(n.source.value+n.target.value,e.source.value+e.target.value)}}function Bl(){function t(t){var o,u,a,c,s,f,h=t.length,p=[],d=l(h),v=[],_=[],y=_.groups=new Array(h),g=new Array(h*h);for(o=0,s=-1;++s<h;){for(u=0,f=-1;++f<h;)u+=t[s][f];p.push(u),v.push(l(h)),o+=u}for(e&&d.sort(function(t,n){return e(p[t],p[n])}),r&&v.forEach(function(n,e){n.sort(function(n,i){return r(t[e][n],t[e][i])})}),o=mx(0,gx-n*h)/o,c=o?n:gx/h,u=0,s=-1;++s<h;){for(a=u,f=-1;++f<h;){var m=d[s],x=v[m][f],b=t[m][x],w=u,M=u+=b*o;g[x*h+m]={index:m,subindex:x,startAngle:w,endAngle:M,value:b}}y[m]={index:m,startAngle:a,endAngle:u,value:p[m]},u+=c}for(s=-1;++s<h;)for(f=s-1;++f<h;){var T=g[f*h+s],k=g[s*h+f];(T.value||k.value)&&_.push(T.value<k.value?{source:k,target:T}:{source:T,target:k})}return i?_.sort(i):_}var n=0,e=null,r=null,i=null;return t.padAngle=function(e){return arguments.length?(n=mx(0,e),t):n},t.sortGroups=function(n){return arguments.length?(e=n,t):e},t.sortSubgroups=function(n){return arguments.length?(r=n,t):r},t.sortChords=function(n){return arguments.length?(null==n?i=null:(i=Yl(n))._=n,t):i&&i._},t}function jl(t){return function(){return t}}function Hl(t){return t.source}function Xl(t){return t.target}function Vl(t){return t.radius}function Wl(t){return t.startAngle}function $l(t){return t.endAngle}function Zl(){function t(){var t,a=xx.call(arguments),c=n.apply(this,a),s=e.apply(this,a),f=+r.apply(this,(a[0]=c,
+a)),l=i.apply(this,a)-yx,h=o.apply(this,a)-yx,p=f*dx(l),d=f*vx(l),v=+r.apply(this,(a[0]=s,a)),_=i.apply(this,a)-yx,y=o.apply(this,a)-yx;if(u||(u=t=Tt()),u.moveTo(p,d),u.arc(0,0,f,l,h),l===_&&h===y||(u.quadraticCurveTo(0,0,v*dx(_),v*vx(_)),u.arc(0,0,v,_,y)),u.quadraticCurveTo(0,0,p,d),u.closePath(),t)return u=null,t+""||null}var n=Hl,e=Xl,r=Vl,i=Wl,o=$l,u=null;return t.radius=function(n){return arguments.length?(r="function"==typeof n?n:jl(+n),t):r},t.startAngle=function(n){return arguments.length?(i="function"==typeof n?n:jl(+n),t):i},t.endAngle=function(n){return arguments.length?(o="function"==typeof n?n:jl(+n),t):o},t.source=function(e){return arguments.length?(n=e,t):n},t.target=function(n){return arguments.length?(e=n,t):e},t.context=function(n){return arguments.length?(u=null==n?null:n,t):u},t}function Gl(){return new Jl}function Jl(){this.reset()}function Ql(t,n,e){var r=t.s=n+e,i=r-n,o=r-i;t.t=n-o+(e-i)}function Kl(t){return t>1?0:t<-1?ib:Math.acos(t)}function th(t){return t>1?ob:t<-1?-ob:Math.asin(t)}function nh(t){return(t=gb(t/2))*t}function eh(){}function rh(t,n){t&&Mb.hasOwnProperty(t.type)&&Mb[t.type](t,n)}function ih(t,n,e){var r,i=-1,o=t.length-e;for(n.lineStart();++i<o;)r=t[i],n.point(r[0],r[1],r[2]);n.lineEnd()}function oh(t,n){var e=-1,r=t.length;for(n.polygonStart();++e<r;)ih(t[e],n,1);n.polygonEnd()}function uh(t,n){t&&wb.hasOwnProperty(t.type)?wb[t.type](t,n):rh(t,n)}function ah(){Tb.point=sh}function ch(){fh(Mx,Tx)}function sh(t,n){Tb.point=fh,Mx=t,Tx=n,t*=sb,n*=sb,kx=t,Nx=pb(n=n/2+ub),Sx=gb(n)}function fh(t,n){t*=sb,n*=sb,n=n/2+ub;var e=t-kx,r=e>=0?1:-1,i=r*e,o=pb(n),u=gb(n),a=Sx*u,c=Nx*o+a*pb(i),s=a*r*gb(i);bx.add(hb(s,c)),kx=t,Nx=o,Sx=u}function lh(t){return wx?wx.reset():(wx=Gl(),bx=Gl()),uh(t,Tb),2*wx}function hh(t){return[hb(t[1],t[0]),th(t[2])]}function ph(t){var n=t[0],e=t[1],r=pb(e);return[r*pb(n),r*gb(n),gb(e)]}function dh(t,n){return t[0]*n[0]+t[1]*n[1]+t[2]*n[2]}function vh(t,n){return[t[1]*n[2]-t[2]*n[1],t[2]*n[0]-t[0]*n[2],t[0]*n[1]-t[1]*n[0]]}function _h(t,n){t[0]+=n[0],t[1]+=n[1],t[2]+=n[2]}function yh(t,n){return[t[0]*n,t[1]*n,t[2]*n]}function gh(t){var n=xb(t[0]*t[0]+t[1]*t[1]+t[2]*t[2]);t[0]/=n,t[1]/=n,t[2]/=n}function mh(t,n){Dx.push(Ox=[Ax=t,Cx=t]),n<Ex&&(Ex=n),n>zx&&(zx=n)}function xh(t,n){var e=ph([t*sb,n*sb]);if(Rx){var r=vh(Rx,e),i=[r[1],-r[0],0],o=vh(i,r);gh(o),o=hh(o);var u,a=t-Px,c=a>0?1:-1,s=o[0]*cb*c,f=fb(a)>180;f^(c*Px<s&&s<c*t)?(u=o[1]*cb,u>zx&&(zx=u)):(s=(s+360)%360-180,f^(c*Px<s&&s<c*t)?(u=-o[1]*cb,u<Ex&&(Ex=u)):(n<Ex&&(Ex=n),n>zx&&(zx=n))),f?t<Px?Nh(Ax,t)>Nh(Ax,Cx)&&(Cx=t):Nh(t,Cx)>Nh(Ax,Cx)&&(Ax=t):Cx>=Ax?(t<Ax&&(Ax=t),t>Cx&&(Cx=t)):t>Px?Nh(Ax,t)>Nh(Ax,Cx)&&(Cx=t):Nh(t,Cx)>Nh(Ax,Cx)&&(Ax=t)}else mh(t,n);Rx=e,Px=t}function bh(){kb.point=xh}function wh(){Ox[0]=Ax,Ox[1]=Cx,kb.point=mh,Rx=null}function Mh(t,n){if(Rx){var e=t-Px;Ux.add(fb(e)>180?e+(e>0?360:-360):e)}else qx=t,Lx=n;Tb.point(t,n),xh(t,n)}function Th(){Tb.lineStart()}function kh(){Mh(qx,Lx),Tb.lineEnd(),fb(Ux)>eb&&(Ax=-(Cx=180)),Ox[0]=Ax,Ox[1]=Cx,Rx=null}function Nh(t,n){return(n-=t)<0?n+360:n}function Sh(t,n){return t[0]-n[0]}function Ah(t,n){return t[0]<=t[1]?t[0]<=n&&n<=t[1]:n<t[0]||t[1]<n}function Eh(t){var n,e,r,i,o,u,a;if(Ux?Ux.reset():Ux=Gl(),zx=Cx=-(Ax=Ex=1/0),Dx=[],uh(t,kb),e=Dx.length){for(Dx.sort(Sh),n=1,r=Dx[0],o=[r];n<e;++n)i=Dx[n],Ah(r,i[0])||Ah(r,i[1])?(Nh(r[0],i[1])>Nh(r[0],r[1])&&(r[1]=i[1]),Nh(i[0],r[1])>Nh(r[0],r[1])&&(r[0]=i[0])):o.push(r=i);for(u=-(1/0),e=o.length-1,n=0,r=o[e];n<=e;r=i,++n)i=o[n],(a=Nh(r[1],i[0]))>u&&(u=a,Ax=i[0],Cx=r[1])}return Dx=Ox=null,Ax===1/0||Ex===1/0?[[NaN,NaN],[NaN,NaN]]:[[Ax,Ex],[Cx,zx]]}function Ch(t,n){t*=sb,n*=sb;var e=pb(n);zh(e*pb(t),e*gb(t),gb(n))}function zh(t,n,e){++Fx,Yx+=(t-Yx)/Fx,Bx+=(n-Bx)/Fx,jx+=(e-jx)/Fx}function Ph(){Nb.point=qh}function qh(t,n){t*=sb,n*=sb;var e=pb(n);Qx=e*pb(t),Kx=e*gb(t),tb=gb(n),Nb.point=Lh,zh(Qx,Kx,tb)}function Lh(t,n){t*=sb,n*=sb;var e=pb(n),r=e*pb(t),i=e*gb(t),o=gb(n),u=hb(xb((u=Kx*o-tb*i)*u+(u=tb*r-Qx*o)*u+(u=Qx*i-Kx*r)*u),Qx*r+Kx*i+tb*o);Ix+=u,Hx+=u*(Qx+(Qx=r)),Xx+=u*(Kx+(Kx=i)),Vx+=u*(tb+(tb=o)),zh(Qx,Kx,tb)}function Rh(){Nb.point=Ch}function Uh(){Nb.point=Oh}function Dh(){Fh(Gx,Jx),Nb.point=Ch}function Oh(t,n){Gx=t,Jx=n,t*=sb,n*=sb,Nb.point=Fh;var e=pb(n);Qx=e*pb(t),Kx=e*gb(t),tb=gb(n),zh(Qx,Kx,tb)}function Fh(t,n){t*=sb,n*=sb;var e=pb(n),r=e*pb(t),i=e*gb(t),o=gb(n),u=Kx*o-tb*i,a=tb*r-Qx*o,c=Qx*i-Kx*r,s=xb(u*u+a*a+c*c),f=Qx*r+Kx*i+tb*o,l=s&&-Kl(f)/s,h=hb(s,f);Wx+=l*u,$x+=l*a,Zx+=l*c,Ix+=h,Hx+=h*(Qx+(Qx=r)),Xx+=h*(Kx+(Kx=i)),Vx+=h*(tb+(tb=o)),zh(Qx,Kx,tb)}function Ih(t){Fx=Ix=Yx=Bx=jx=Hx=Xx=Vx=Wx=$x=Zx=0,uh(t,Nb);var n=Wx,e=$x,r=Zx,i=n*n+e*e+r*r;return i<rb&&(n=Hx,e=Xx,r=Vx,Ix<eb&&(n=Yx,e=Bx,r=jx),i=n*n+e*e+r*r,i<rb)?[NaN,NaN]:[hb(e,n)*cb,th(r/xb(i))*cb]}function Yh(t){return function(){return t}}function Bh(t,n){function e(e,r){return e=t(e,r),n(e[0],e[1])}return t.invert&&n.invert&&(e.invert=function(e,r){return e=n.invert(e,r),e&&t.invert(e[0],e[1])}),e}function jh(t,n){return[t>ib?t-ab:t<-ib?t+ab:t,n]}function Hh(t,n,e){return(t%=ab)?n||e?Bh(Vh(t),Wh(n,e)):Vh(t):n||e?Wh(n,e):jh}function Xh(t){return function(n,e){return n+=t,[n>ib?n-ab:n<-ib?n+ab:n,e]}}function Vh(t){var n=Xh(t);return n.invert=Xh(-t),n}function Wh(t,n){function e(t,n){var e=pb(n),a=pb(t)*e,c=gb(t)*e,s=gb(n),f=s*r+a*i;return[hb(c*o-f*u,a*r-s*i),th(f*o+c*u)]}var r=pb(t),i=gb(t),o=pb(n),u=gb(n);return e.invert=function(t,n){var e=pb(n),a=pb(t)*e,c=gb(t)*e,s=gb(n),f=s*o-c*u;return[hb(c*o+s*u,a*r+f*i),th(f*r-a*i)]},e}function $h(t){function n(n){return n=t(n[0]*sb,n[1]*sb),n[0]*=cb,n[1]*=cb,n}return t=Hh(t[0]*sb,t[1]*sb,t.length>2?t[2]*sb:0),n.invert=function(n){return n=t.invert(n[0]*sb,n[1]*sb),n[0]*=cb,n[1]*=cb,n},n}function Zh(t,n,e,r,i,o){if(e){var u=pb(n),a=gb(n),c=r*e;null==i?(i=n+r*ab,o=n-c/2):(i=Gh(u,i),o=Gh(u,o),(r>0?i<o:i>o)&&(i+=r*ab));for(var s,f=i;r>0?f>o:f<o;f-=c)s=hh([u,-a*pb(f),-a*gb(f)]),t.point(s[0],s[1])}}function Gh(t,n){n=ph(n),n[0]-=t,gh(n);var e=Kl(-n[1]);return((-n[2]<0?-e:e)+ab-eb)%ab}function Jh(){function t(t,n){e.push(t=r(t,n)),t[0]*=cb,t[1]*=cb}function n(){var t=i.apply(this,arguments),n=o.apply(this,arguments)*sb,c=u.apply(this,arguments)*sb;return e=[],r=Hh(-t[0]*sb,-t[1]*sb,0).invert,Zh(a,n,c,1),t={type:"Polygon",coordinates:[e]},e=r=null,t}var e,r,i=Yh([0,0]),o=Yh(90),u=Yh(6),a={point:t};return n.center=function(t){return arguments.length?(i="function"==typeof t?t:Yh([+t[0],+t[1]]),n):i},n.radius=function(t){return arguments.length?(o="function"==typeof t?t:Yh(+t),n):o},n.precision=function(t){return arguments.length?(u="function"==typeof t?t:Yh(+t),n):u},n}function Qh(){var t,n=[];return{point:function(n,e){t.push([n,e])},lineStart:function(){n.push(t=[])},lineEnd:eh,rejoin:function(){n.length>1&&n.push(n.pop().concat(n.shift()))},result:function(){var e=n;return n=[],t=null,e}}}function Kh(t,n,e,r,i,o){var u,a=t[0],c=t[1],s=n[0],f=n[1],l=0,h=1,p=s-a,d=f-c;if(u=e-a,p||!(u>0)){if(u/=p,p<0){if(u<l)return;u<h&&(h=u)}else if(p>0){if(u>h)return;u>l&&(l=u)}if(u=i-a,p||!(u<0)){if(u/=p,p<0){if(u>h)return;u>l&&(l=u)}else if(p>0){if(u<l)return;u<h&&(h=u)}if(u=r-c,d||!(u>0)){if(u/=d,d<0){if(u<l)return;u<h&&(h=u)}else if(d>0){if(u>h)return;u>l&&(l=u)}if(u=o-c,d||!(u<0)){if(u/=d,d<0){if(u>h)return;u>l&&(l=u)}else if(d>0){if(u<l)return;u<h&&(h=u)}return l>0&&(t[0]=a+l*p,t[1]=c+l*d),h<1&&(n[0]=a+h*p,n[1]=c+h*d),!0}}}}}function tp(t,n){return fb(t[0]-n[0])<eb&&fb(t[1]-n[1])<eb}function np(t,n,e,r){this.x=t,this.z=n,this.o=e,this.e=r,this.v=!1,this.n=this.p=null}function ep(t,n,e,r,i){var o,u,a=[],c=[];if(t.forEach(function(t){if(!((n=t.length-1)<=0)){var n,e,r=t[0],u=t[n];if(tp(r,u)){for(i.lineStart(),o=0;o<n;++o)i.point((r=t[o])[0],r[1]);return void i.lineEnd()}a.push(e=new np(r,t,null,(!0))),c.push(e.o=new np(r,null,e,(!1))),a.push(e=new np(u,t,null,(!1))),c.push(e.o=new np(u,null,e,(!0)))}}),a.length){for(c.sort(n),rp(a),rp(c),o=0,u=c.length;o<u;++o)c[o].e=e=!e;for(var s,f,l=a[0];;){for(var h=l,p=!0;h.v;)if((h=h.n)===l)return;s=h.z,i.lineStart();do{if(h.v=h.o.v=!0,h.e){if(p)for(o=0,u=s.length;o<u;++o)i.point((f=s[o])[0],f[1]);else r(h.x,h.n.x,1,i);h=h.n}else{if(p)for(s=h.p.z,o=s.length-1;o>=0;--o)i.point((f=s[o])[0],f[1]);else r(h.x,h.p.x,-1,i);h=h.p}h=h.o,s=h.z,p=!p}while(!h.v);i.lineEnd()}}}function rp(t){if(n=t.length){for(var n,e,r=0,i=t[0];++r<n;)i.n=e=t[r],e.p=i,i=e;i.n=e=t[0],e.p=i}}function ip(t,n,e,r){function i(i,o){return t<=i&&i<=e&&n<=o&&o<=r}function o(i,o,a,s){var f=0,l=0;if(null==i||(f=u(i,a))!==(l=u(o,a))||c(i,o)<0^a>0){do s.point(0===f||3===f?t:e,f>1?r:n);while((f=(f+a+4)%4)!==l)}else s.point(o[0],o[1])}function u(r,i){return fb(r[0]-t)<eb?i>0?0:3:fb(r[0]-e)<eb?i>0?2:1:fb(r[1]-n)<eb?i>0?1:0:i>0?3:2}function a(t,n){return c(t.x,n.x)}function c(t,n){var e=u(t,1),r=u(n,1);return e!==r?e-r:0===e?n[1]-t[1]:1===e?t[0]-n[0]:2===e?t[1]-n[1]:n[0]-t[0]}return function(u){function c(t,n){i(t,n)&&S.point(t,n)}function s(){for(var n=0,e=0,i=_.length;e<i;++e)for(var o,u,a=_[e],c=1,s=a.length,f=a[0],l=f[0],h=f[1];c<s;++c)o=l,u=h,f=a[c],l=f[0],h=f[1],u<=r?h>r&&(l-o)*(r-u)>(h-u)*(t-o)&&++n:h<=r&&(l-o)*(r-u)<(h-u)*(t-o)&&--n;return n}function f(){S=A,v=[],_=[],N=!0}function l(){var t=s(),n=N&&t,e=(v=w(v)).length;(n||e)&&(u.polygonStart(),n&&(u.lineStart(),o(null,null,1,u),u.lineEnd()),e&&ep(v,a,t,o,u),u.polygonEnd()),S=u,v=_=y=null}function h(){E.point=d,_&&_.push(y=[]),k=!0,T=!1,b=M=NaN}function p(){v&&(d(g,m),x&&T&&A.rejoin(),v.push(A.result())),E.point=c,T&&S.lineEnd()}function d(o,u){var a=i(o,u);if(_&&y.push([o,u]),k)g=o,m=u,x=a,k=!1,a&&(S.lineStart(),S.point(o,u));else if(a&&T)S.point(o,u);else{var c=[b=Math.max(Ib,Math.min(Fb,b)),M=Math.max(Ib,Math.min(Fb,M))],s=[o=Math.max(Ib,Math.min(Fb,o)),u=Math.max(Ib,Math.min(Fb,u))];Kh(c,s,t,n,e,r)?(T||(S.lineStart(),S.point(c[0],c[1])),S.point(s[0],s[1]),a||S.lineEnd(),N=!1):a&&(S.lineStart(),S.point(o,u),N=!1)}b=o,M=u,T=a}var v,_,y,g,m,x,b,M,T,k,N,S=u,A=Qh(),E={point:c,lineStart:h,lineEnd:p,polygonStart:f,polygonEnd:l};return E}}function op(){var t,n,e,r=0,i=0,o=960,u=500;return e={stream:function(e){return t&&n===e?t:t=ip(r,i,o,u)(n=e)},extent:function(a){return arguments.length?(r=+a[0][0],i=+a[0][1],o=+a[1][0],u=+a[1][1],t=n=null,e):[[r,i],[o,u]]}}}function up(){Yb.point=cp,Yb.lineEnd=ap}function ap(){Yb.point=Yb.lineEnd=eh}function cp(t,n){t*=sb,n*=sb,Ab=t,Eb=gb(n),Cb=pb(n),Yb.point=sp}function sp(t,n){t*=sb,n*=sb;var e=gb(n),r=pb(n),i=fb(t-Ab),o=pb(i),u=gb(i),a=r*u,c=Cb*e-Eb*r*o,s=Eb*e+Cb*r*o;Sb.add(hb(xb(a*a+c*c),s)),Ab=t,Eb=e,Cb=r}function fp(t){return Sb?Sb.reset():Sb=Gl(),uh(t,Yb),+Sb}function lp(t,n){return Bb[0]=t,Bb[1]=n,fp(jb)}function hp(t,n,e){var r=l(t,n-eb,e).concat(n);return function(t){return r.map(function(n){return[t,n]})}}function pp(t,n,e){var r=l(t,n-eb,e).concat(n);return function(t){return r.map(function(n){return[n,t]})}}function dp(){function t(){return{type:"MultiLineString",coordinates:n()}}function n(){return l(db(o/y)*y,i,y).map(p).concat(l(db(s/g)*g,c,g).map(d)).concat(l(db(r/v)*v,e,v).filter(function(t){return fb(t%y)>eb}).map(f)).concat(l(db(a/_)*_,u,_).filter(function(t){return fb(t%g)>eb}).map(h))}var e,r,i,o,u,a,c,s,f,h,p,d,v=10,_=v,y=90,g=360,m=2.5;return t.lines=function(){return n().map(function(t){return{type:"LineString",coordinates:t}})},t.outline=function(){return{type:"Polygon",coordinates:[p(o).concat(d(c).slice(1),p(i).reverse().slice(1),d(s).reverse().slice(1))]}},t.extent=function(n){return arguments.length?t.extentMajor(n).extentMinor(n):t.extentMinor()},t.extentMajor=function(n){return arguments.length?(o=+n[0][0],i=+n[1][0],s=+n[0][1],c=+n[1][1],o>i&&(n=o,o=i,i=n),s>c&&(n=s,s=c,c=n),t.precision(m)):[[o,s],[i,c]]},t.extentMinor=function(n){return arguments.length?(r=+n[0][0],e=+n[1][0],a=+n[0][1],u=+n[1][1],r>e&&(n=r,r=e,e=n),a>u&&(n=a,a=u,u=n),t.precision(m)):[[r,a],[e,u]]},t.step=function(n){return arguments.length?t.stepMajor(n).stepMinor(n):t.stepMinor()},t.stepMajor=function(n){return arguments.length?(y=+n[0],g=+n[1],t):[y,g]},t.stepMinor=function(n){return arguments.length?(v=+n[0],_=+n[1],t):[v,_]},t.precision=function(n){return arguments.length?(m=+n,f=hp(a,u,90),h=pp(r,e,m),p=hp(s,c,90),d=pp(o,i,m),t):m},t.extentMajor([[-180,-90+eb],[180,90-eb]]).extentMinor([[-180,-80-eb],[180,80+eb]])}function vp(t,n){var e=t[0]*sb,r=t[1]*sb,i=n[0]*sb,o=n[1]*sb,u=pb(r),a=gb(r),c=pb(o),s=gb(o),f=u*pb(e),l=u*gb(e),h=c*pb(i),p=c*gb(i),d=2*th(xb(nh(o-r)+u*c*nh(i-e))),v=gb(d),_=d?function(t){var n=gb(t*=d)/v,e=gb(d-t)/v,r=e*f+n*h,i=e*l+n*p,o=e*a+n*s;return[hb(i,r)*cb,hb(o,xb(r*r+i*i))*cb]}:function(){return[e*cb,r*cb]};return _.distance=d,_}function _p(t){return t}function yp(){Vb.point=gp}function gp(t,n){Vb.point=mp,zb=qb=t,Pb=Lb=n}function mp(t,n){Xb.add(Lb*t-qb*n),qb=t,Lb=n}function xp(){mp(zb,Pb)}function bp(t,n){t<Wb&&(Wb=t),t>Zb&&(Zb=t),n<$b&&($b=n),n>Gb&&(Gb=n)}function wp(t,n){Qb+=t,Kb+=n,++tw}function Mp(){aw.point=Tp}function Tp(t,n){aw.point=kp,wp(Db=t,Ob=n)}function kp(t,n){var e=t-Db,r=n-Ob,i=xb(e*e+r*r);nw+=i*(Db+t)/2,ew+=i*(Ob+n)/2,rw+=i,wp(Db=t,Ob=n)}function Np(){aw.point=wp}function Sp(){aw.point=Ep}function Ap(){Cp(Rb,Ub)}function Ep(t,n){aw.point=Cp,wp(Rb=Db=t,Ub=Ob=n)}function Cp(t,n){var e=t-Db,r=n-Ob,i=xb(e*e+r*r);nw+=i*(Db+t)/2,ew+=i*(Ob+n)/2,rw+=i,i=Ob*t-Db*n,iw+=i*(Db+t),ow+=i*(Ob+n),uw+=3*i,wp(Db=t,Ob=n)}function zp(t){function n(n,e){t.moveTo(n+u,e),t.arc(n,e,u,0,ab)}function e(n,e){t.moveTo(n,e),a.point=r}function r(n,e){t.lineTo(n,e)}function i(){a.point=n}function o(){t.closePath()}var u=4.5,a={point:n,lineStart:function(){a.point=e},lineEnd:i,polygonStart:function(){a.lineEnd=o},polygonEnd:function(){a.lineEnd=i,a.point=n},pointRadius:function(t){return u=t,a},result:eh};return a}function Pp(){function t(t,n){a.push("M",t,",",n,u)}function n(t,n){a.push("M",t,",",n),c.point=e}function e(t,n){a.push("L",t,",",n)}function r(){c.point=n}function i(){c.point=t}function o(){a.push("Z")}var u=qp(4.5),a=[],c={point:t,lineStart:r,lineEnd:i,polygonStart:function(){c.lineEnd=o},polygonEnd:function(){c.lineEnd=i,c.point=t},pointRadius:function(t){return u=qp(t),c},result:function(){if(a.length){var t=a.join("");return a=[],t}}};return c}function qp(t){return"m0,"+t+"a"+t+","+t+" 0 1,1 0,"+-2*t+"a"+t+","+t+" 0 1,1 0,"+2*t+"z"}function Lp(){function t(t){return t&&("function"==typeof o&&i.pointRadius(+o.apply(this,arguments)),uh(t,e(i))),i.result()}var n,e,r,i,o=4.5;return t.area=function(t){return uh(t,e(Vb)),Vb.result()},t.bounds=function(t){return uh(t,e(Jb)),Jb.result()},t.centroid=function(t){return uh(t,e(aw)),aw.result()},t.projection=function(r){return arguments.length?(e=null==(n=r)?_p:r.stream,t):n},t.context=function(n){return arguments.length?(i=null==(r=n)?new Pp:new zp(n),"function"!=typeof o&&i.pointRadius(o),t):r},t.pointRadius=function(n){return arguments.length?(o="function"==typeof n?n:(i.pointRadius(+n),+n),t):o},t.projection(null).context(null)}function Rp(t,n){for(var e=n[0],r=n[1],i=[gb(e),-pb(e),0],o=0,u=0,a=0,c=t.length;a<c;++a)if(f=(s=t[a]).length)for(var s,f,l=s[f-1],h=l[0],p=l[1]/2+ub,d=gb(p),v=pb(p),_=0;_<f;++_,h=g,d=x,v=b,l=y){var y=s[_],g=y[0],m=y[1]/2+ub,x=gb(m),b=pb(m),w=g-h,M=w>=0?1:-1,T=M*w,k=T>ib,N=d*x;if(cw.add(hb(N*M*gb(T),v*b+N*pb(T))),o+=k?w+M*ab:w,k^h>=e^g>=e){var S=vh(ph(l),ph(y));gh(S);var A=vh(i,S);gh(A);var E=(k^w>=0?-1:1)*th(A[2]);(r>E||r===E&&(S[0]||S[1]))&&(u+=k^w>=0?1:-1)}}var C=(o<-eb||o<eb&&cw<-eb)^1&u;return cw.reset(),C}function Up(t,n,e,r){return function(i,o){function u(n,e){var r=i(n,e);t(n=r[0],e=r[1])&&o.point(n,e)}function a(t,n){var e=i(t,n);_.point(e[0],e[1])}function c(){b.point=a,_.lineStart()}function s(){b.point=u,_.lineEnd()}function f(t,n){v.push([t,n]);var e=i(t,n);m.point(e[0],e[1])}function l(){m.lineStart(),v=[]}function h(){f(v[0][0],v[0][1]),m.lineEnd();var t,n,e,r,i=m.clean(),u=g.result(),a=u.length;if(v.pop(),p.push(v),v=null,a)if(1&i){if(e=u[0],(n=e.length-1)>0){for(x||(o.polygonStart(),x=!0),o.lineStart(),t=0;t<n;++t)o.point((r=e[t])[0],r[1]);o.lineEnd()}}else a>1&&2&i&&u.push(u.pop().concat(u.shift())),d.push(u.filter(Dp))}var p,d,v,_=n(o),y=i.invert(r[0],r[1]),g=Qh(),m=n(g),x=!1,b={point:u,lineStart:c,lineEnd:s,polygonStart:function(){b.point=f,b.lineStart=l,b.lineEnd=h,d=[],p=[]},polygonEnd:function(){b.point=u,b.lineStart=c,b.lineEnd=s,d=w(d);var t=Rp(p,y);d.length?(x||(o.polygonStart(),x=!0),ep(d,Op,t,e,o)):t&&(x||(o.polygonStart(),x=!0),o.lineStart(),e(null,null,1,o),o.lineEnd()),x&&(o.polygonEnd(),x=!1),d=p=null},sphere:function(){o.polygonStart(),o.lineStart(),e(null,null,1,o),o.lineEnd(),o.polygonEnd()}};return b}}function Dp(t){return t.length>1}function Op(t,n){return((t=t.x)[0]<0?t[1]-ob-eb:ob-t[1])-((n=n.x)[0]<0?n[1]-ob-eb:ob-n[1])}function Fp(t){var n,e=NaN,r=NaN,i=NaN;return{lineStart:function(){t.lineStart(),n=1},point:function(o,u){var a=o>0?ib:-ib,c=fb(o-e);fb(c-ib)<eb?(t.point(e,r=(r+u)/2>0?ob:-ob),t.point(i,r),t.lineEnd(),t.lineStart(),t.point(a,r),t.point(o,r),n=0):i!==a&&c>=ib&&(fb(e-i)<eb&&(e-=i*eb),fb(o-a)<eb&&(o-=a*eb),r=Ip(e,r,o,u),t.point(i,r),t.lineEnd(),t.lineStart(),t.point(a,r),n=0),t.point(e=o,r=u),i=a},lineEnd:function(){t.lineEnd(),e=r=NaN},clean:function(){return 2-n}}}function Ip(t,n,e,r){var i,o,u=gb(t-e);return fb(u)>eb?lb((gb(n)*(o=pb(r))*gb(e)-gb(r)*(i=pb(n))*gb(t))/(i*o*u)):(n+r)/2}function Yp(t,n,e,r){var i;if(null==t)i=e*ob,r.point(-ib,i),r.point(0,i),r.point(ib,i),r.point(ib,0),r.point(ib,-i),r.point(0,-i),r.point(-ib,-i),r.point(-ib,0),r.point(-ib,i);else if(fb(t[0]-n[0])>eb){var o=t[0]<n[0]?ib:-ib;i=e*o/2,r.point(-o,i),r.point(0,i),r.point(o,i)}else r.point(n[0],n[1])}function Bp(t,n){function e(e,r,i,o){Zh(o,t,n,i,e,r)}function r(t,n){return pb(t)*pb(n)>a}function i(t){var n,e,i,a,f;return{lineStart:function(){a=i=!1,f=1},point:function(l,h){var p,d=[l,h],v=r(l,h),_=c?v?0:u(l,h):v?u(l+(l<0?ib:-ib),h):0;if(!n&&(a=i=v)&&t.lineStart(),v!==i&&(p=o(n,d),(tp(n,p)||tp(d,p))&&(d[0]+=eb,d[1]+=eb,v=r(d[0],d[1]))),v!==i)f=0,v?(t.lineStart(),p=o(d,n),t.point(p[0],p[1])):(p=o(n,d),t.point(p[0],p[1]),t.lineEnd()),n=p;else if(s&&n&&c^v){var y;_&e||!(y=o(d,n,!0))||(f=0,c?(t.lineStart(),t.point(y[0][0],y[0][1]),t.point(y[1][0],y[1][1]),t.lineEnd()):(t.point(y[1][0],y[1][1]),t.lineEnd(),t.lineStart(),t.point(y[0][0],y[0][1])))}!v||n&&tp(n,d)||t.point(d[0],d[1]),n=d,i=v,e=_},lineEnd:function(){i&&t.lineEnd(),n=null},clean:function(){return f|(a&&i)<<1}}}function o(t,n,e){var r=ph(t),i=ph(n),o=[1,0,0],u=vh(r,i),c=dh(u,u),s=u[0],f=c-s*s;if(!f)return!e&&t;var l=a*c/f,h=-a*s/f,p=vh(o,u),d=yh(o,l),v=yh(u,h);_h(d,v);var _=p,y=dh(d,_),g=dh(_,_),m=y*y-g*(dh(d,d)-1);if(!(m<0)){var x=xb(m),b=yh(_,(-y-x)/g);if(_h(b,d),b=hh(b),!e)return b;var w,M=t[0],T=n[0],k=t[1],N=n[1];T<M&&(w=M,M=T,T=w);var S=T-M,A=fb(S-ib)<eb,E=A||S<eb;if(!A&&N<k&&(w=k,k=N,N=w),E?A?k+N>0^b[1]<(fb(b[0]-M)<eb?k:N):k<=b[1]&&b[1]<=N:S>ib^(M<=b[0]&&b[0]<=T)){var C=yh(_,(-y+x)/g);return _h(C,d),[b,hh(C)]}}}function u(n,e){var r=c?t:ib-t,i=0;return n<-r?i|=1:n>r&&(i|=2),e<-r?i|=4:e>r&&(i|=8),i}var a=pb(t),c=a>0,s=fb(a)>eb;return Up(r,i,e,c?[0,-t]:[-ib,t-ib])}function jp(t){return{stream:Hp(t)}}function Hp(t){function n(){}var e=n.prototype=Object.create(Xp.prototype);for(var r in t)e[r]=t[r];return function(t){var e=new n;return e.stream=t,e}}function Xp(){}function Vp(t,n){return+n?$p(t,n):Wp(t)}function Wp(t){return Hp({point:function(n,e){n=t(n,e),this.stream.point(n[0],n[1])}})}function $p(t,n){function e(r,i,o,u,a,c,s,f,l,h,p,d,v,_){var y=s-r,g=f-i,m=y*y+g*g;if(m>4*n&&v--){var x=u+h,b=a+p,w=c+d,M=xb(x*x+b*b+w*w),T=th(w/=M),k=fb(fb(w)-1)<eb||fb(o-l)<eb?(o+l)/2:hb(b,x),N=t(k,T),S=N[0],A=N[1],E=S-r,C=A-i,z=g*E-y*C;(z*z/m>n||fb((y*E+g*C)/m-.5)>.3||u*h+a*p+c*d<lw)&&(e(r,i,o,u,a,c,S,A,k,x/=M,b/=M,w,v,_),_.point(S,A),e(S,A,k,x,b,w,s,f,l,h,p,d,v,_))}}return function(n){function r(e,r){e=t(e,r),n.point(e[0],e[1])}function i(){y=NaN,w.point=o,n.lineStart()}function o(r,i){var o=ph([r,i]),u=t(r,i);e(y,g,_,m,x,b,y=u[0],g=u[1],_=r,m=o[0],x=o[1],b=o[2],fw,n),n.point(y,g)}function u(){w.point=r,n.lineEnd()}function a(){i(),w.point=c,w.lineEnd=s}function c(t,n){o(f=t,n),l=y,h=g,p=m,d=x,v=b,w.point=o}function s(){e(y,g,_,m,x,b,l,h,f,p,d,v,fw,n),w.lineEnd=u,u()}var f,l,h,p,d,v,_,y,g,m,x,b,w={point:r,lineStart:i,lineEnd:u,polygonStart:function(){n.polygonStart(),w.lineStart=a},polygonEnd:function(){n.polygonEnd(),w.lineStart=i}};return w}}function Zp(t){return Gp(function(){return t})()}function Gp(t){function n(t){return t=f(t[0]*sb,t[1]*sb),[t[0]*_+a,c-t[1]*_]}function e(t){return t=f.invert((t[0]-a)/_,(c-t[1])/_),t&&[t[0]*cb,t[1]*cb]}function r(t,n){return t=u(t,n),[t[0]*_+a,c-t[1]*_]}function i(){f=Bh(s=Hh(b,w,M),u);var t=u(m,x);return a=y-t[0]*_,c=g+t[1]*_,o()}function o(){return d=v=null,n}var u,a,c,s,f,l,h,p,d,v,_=150,y=480,g=250,m=0,x=0,b=0,w=0,M=0,T=null,k=sw,N=null,S=_p,A=.5,E=Vp(r,A);return n.stream=function(t){return d&&v===t?d:d=hw(k(s,E(S(v=t))))},n.clipAngle=function(t){return arguments.length?(k=+t?Bp(T=t*sb,6*sb):(T=null,sw),o()):T*cb},n.clipExtent=function(t){return arguments.length?(S=null==t?(N=l=h=p=null,_p):ip(N=+t[0][0],l=+t[0][1],h=+t[1][0],p=+t[1][1]),o()):null==N?null:[[N,l],[h,p]]},n.scale=function(t){return arguments.length?(_=+t,i()):_},n.translate=function(t){return arguments.length?(y=+t[0],g=+t[1],i()):[y,g]},n.center=function(t){return arguments.length?(m=t[0]%360*sb,x=t[1]%360*sb,i()):[m*cb,x*cb]},n.rotate=function(t){return arguments.length?(b=t[0]%360*sb,w=t[1]%360*sb,M=t.length>2?t[2]%360*sb:0,i()):[b*cb,w*cb,M*cb]},n.precision=function(t){return arguments.length?(E=Vp(r,A=t*t),o()):xb(A)},function(){return u=t.apply(this,arguments),n.invert=u.invert&&e,i()}}function Jp(t){var n=0,e=ib/3,r=Gp(t),i=r(n,e);return i.parallels=function(t){return arguments.length?r(n=t[0]*sb,e=t[1]*sb):[n*cb,e*cb]},i}function Qp(t,n){function e(t,n){var e=xb(o-2*i*gb(n))/i;return[e*gb(t*=i),u-e*pb(t)]}var r=gb(t),i=(r+gb(n))/2,o=1+r*(2*i-r),u=xb(o)/i;return e.invert=function(t,n){var e=u-n;return[hb(t,e)/i,th((o-(t*t+e*e)*i*i)/(2*i))]},e}function Kp(){return Jp(Qp).scale(151).translate([480,347])}function td(){return Kp().parallels([29.5,45.5]).scale(1070).translate([480,250]).rotate([96,0]).center([-.6,38.7])}function nd(t){var n=t.length;return{point:function(e,r){for(var i=-1;++i<n;)t[i].point(e,r)},sphere:function(){for(var e=-1;++e<n;)t[e].sphere()},lineStart:function(){for(var e=-1;++e<n;)t[e].lineStart()},lineEnd:function(){for(var e=-1;++e<n;)t[e].lineEnd()},polygonStart:function(){for(var e=-1;++e<n;)t[e].polygonStart()},polygonEnd:function(){for(var e=-1;++e<n;)t[e].polygonEnd()}}}function ed(){function t(t){var n=t[0],e=t[1];return u=null,r.point(n,e),u||(i.point(n,e),u)||(o.point(n,e),u)}var n,e,r,i,o,u,a=td(),c=Kp().rotate([154,0]).center([-2,58.5]).parallels([55,65]),s=Kp().rotate([157,0]).center([-3,19.9]).parallels([8,18]),f={point:function(t,n){u=[t,n]}};return t.invert=function(t){var n=a.scale(),e=a.translate(),r=(t[0]-e[0])/n,i=(t[1]-e[1])/n;return(i>=.12&&i<.234&&r>=-.425&&r<-.214?c:i>=.166&&i<.234&&r>=-.214&&r<-.115?s:a).invert(t)},t.stream=function(t){return n&&e===t?n:n=nd([a.stream(e=t),c.stream(t),s.stream(t)])},t.precision=function(n){return arguments.length?(a.precision(n),c.precision(n),s.precision(n),t):a.precision()},t.scale=function(n){return arguments.length?(a.scale(n),c.scale(.35*n),s.scale(n),t.translate(a.translate())):a.scale()},t.translate=function(n){if(!arguments.length)return a.translate();var e=a.scale(),u=+n[0],l=+n[1];return r=a.translate(n).clipExtent([[u-.455*e,l-.238*e],[u+.455*e,l+.238*e]]).stream(f),i=c.translate([u-.307*e,l+.201*e]).clipExtent([[u-.425*e+eb,l+.12*e+eb],[u-.214*e-eb,l+.234*e-eb]]).stream(f),o=s.translate([u-.205*e,l+.212*e]).clipExtent([[u-.214*e+eb,l+.166*e+eb],[u-.115*e-eb,l+.234*e-eb]]).stream(f),t},t.scale(1070)}function rd(t){return function(n,e){var r=pb(n),i=pb(e),o=t(r*i);return[o*i*gb(n),o*gb(e)]}}function id(t){return function(n,e){var r=xb(n*n+e*e),i=t(r),o=gb(i),u=pb(i);return[hb(n*o,r*u),th(r&&e*o/r)]}}function od(){return Zp(pw).scale(120).clipAngle(179.999)}function ud(){return Zp(dw).scale(480/ab).clipAngle(179.999)}function ad(t,n){return[t,_b(bb((ob+n)/2))]}function cd(){return sd(ad)}function sd(t){var n,e=Zp(t),r=e.scale,i=e.translate,o=e.clipExtent;return e.scale=function(t){return arguments.length?(r(t),n&&e.clipExtent(null),e):r()},e.translate=function(t){return arguments.length?(i(t),n&&e.clipExtent(null),e):i()},e.clipExtent=function(t){if(!arguments.length)return n?null:o();if(n=null==t){var u=ib*r(),a=i();t=[[a[0]-u,a[1]-u],[a[0]+u,a[1]+u]]}return o(t),e},e.clipExtent(null).scale(961/ab)}function fd(t){return bb((ob+t)/2)}function ld(t,n){function e(t,n){o>0?n<-ob+eb&&(n=-ob+eb):n>ob-eb&&(n=ob-eb);var e=o/yb(fd(n),i);return[e*gb(i*t),o-e*pb(i*t)]}var r=pb(t),i=t===n?gb(t):_b(r/pb(n))/_b(fd(n)/fd(t)),o=r*yb(fd(t),i)/i;return i?(e.invert=function(t,n){var e=o-n,r=mb(i)*xb(t*t+e*e);return[hb(t,e)/i,2*lb(yb(o/r,1/i))-ob]},e):ad}function hd(){return Jp(ld)}function pd(t,n){return[t,n]}function dd(){return Zp(pd).scale(480/ib)}function vd(t,n){function e(t,n){var e=o-n,r=i*t;return[e*gb(r),o-e*pb(r)]}var r=pb(t),i=t===n?gb(t):(r-pb(n))/(n-t),o=r/i+t;return fb(i)<eb?pd:(e.invert=function(t,n){var e=o-n;return[hb(t,e)/i,o-mb(i)*xb(t*t+e*e)]},e)}function _d(){return Jp(vd).scale(128).translate([480,280])}function yd(t,n){var e=pb(n),r=pb(t)*e;return[e*gb(t)/r,gb(n)/r]}function gd(){return Zp(yd).scale(139).clipAngle(60)}function md(t,n){return[pb(n)*gb(t),gb(n)]}function xd(){return Zp(md).scale(240).clipAngle(90+eb)}function bd(t,n){var e=pb(n),r=1+pb(t)*e;return[e*gb(t)/r,gb(n)/r]}function wd(){return Zp(bd).scale(240).clipAngle(142)}function Md(t,n){return[_b(bb((ob+n)/2)),-t]}function Td(){var t=sd(Md),n=t.center,e=t.rotate;return t.center=function(t){return arguments.length?n([-t[1],t[0]]):(t=n(),[t[1],-t[0]])},t.rotate=function(t){return arguments.length?e([t[0],t[1],t.length>2?t[2]+90:90]):(t=e(),[t[0],t[1],t[2]-90])},e([0,0,90])}var kd="4.1.0",Nd=e(n),Sd=Nd.right,Ad=Nd.left,Ed=Array.prototype,Cd=Ed.slice,zd=Ed.map,Pd=Math.sqrt(50),qd=Math.sqrt(10),Ld=Math.sqrt(2),Rd="$";P.prototype=q.prototype={constructor:P,has:function(t){return Rd+t in this},get:function(t){return this[Rd+t]},set:function(t,n){return this[Rd+t]=n,this},remove:function(t){var n=Rd+t;return n in this&&delete this[n]},clear:function(){for(var t in this)t[0]===Rd&&delete this[t]},keys:function(){var t=[];for(var n in this)n[0]===Rd&&t.push(n.slice(1));return t},values:function(){var t=[];for(var n in this)n[0]===Rd&&t.push(this[n]);return t},entries:function(){var t=[];for(var n in this)n[0]===Rd&&t.push({key:n.slice(1),value:this[n]});return t},size:function(){var t=0;for(var n in this)n[0]===Rd&&++t;return t},empty:function(){for(var t in this)if(t[0]===Rd)return!1;return!0},each:function(t){for(var n in this)n[0]===Rd&&t(this[n],n.slice(1),this)}};var Ud=q.prototype;F.prototype=I.prototype={constructor:F,has:Ud.has,add:function(t){return t+="",this[Rd+t]=t,this},remove:Ud.remove,clear:Ud.clear,values:Ud.keys,size:Ud.size,empty:Ud.empty,each:Ud.each};var Dd=3,Od=function vw(t){function n(n){return Math.pow(n,t)}return t=+t,n.exponent=vw,n}(Dd),Fd=function _w(t){function n(n){return 1-Math.pow(1-n,t)}return t=+t,n.exponent=_w,n}(Dd),Id=function yw(t){function n(n){return((n*=2)<=1?Math.pow(n,t):2-Math.pow(2-n,t))/2}return t=+t,n.exponent=yw,n}(Dd),Yd=Math.PI,Bd=Yd/2,jd=4/11,Hd=6/11,Xd=8/11,Vd=.75,Wd=9/11,$d=10/11,Zd=.9375,Gd=21/22,Jd=63/64,Qd=1/jd/jd,Kd=1.70158,tv=function gw(t){function n(n){return n*n*((t+1)*n-t)}return t=+t,n.overshoot=gw,n}(Kd),nv=function mw(t){function n(n){return--n*n*((t+1)*n+t)+1}return t=+t,n.overshoot=mw,n}(Kd),ev=function xw(t){function n(n){return((n*=2)<1?n*n*((t+1)*n-t):(n-=2)*n*((t+1)*n+t)+2)/2}return t=+t,n.overshoot=xw,n}(Kd),rv=2*Math.PI,iv=1,ov=.3,uv=function bw(t,n){function e(e){return t*Math.pow(2,10*--e)*Math.sin((r-e)/n)}var r=Math.asin(1/(t=Math.max(1,t)))*(n/=rv);return e.amplitude=function(t){return bw(t,n*rv)},e.period=function(n){return bw(t,n)},e}(iv,ov),av=function ww(t,n){function e(e){return 1-t*Math.pow(2,-10*(e=+e))*Math.sin((e+r)/n)}var r=Math.asin(1/(t=Math.max(1,t)))*(n/=rv);return e.amplitude=function(t){return ww(t,n*rv)},e.period=function(n){return ww(t,n)},e}(iv,ov),cv=function Mw(t,n){function e(e){return((e=2*e-1)<0?t*Math.pow(2,10*e)*Math.sin((r-e)/n):2-t*Math.pow(2,-10*e)*Math.sin((r+e)/n))/2}var r=Math.asin(1/(t=Math.max(1,t)))*(n/=rv);return e.amplitude=function(t){return Mw(t,n*rv)},e.period=function(n){return Mw(t,n)},e}(iv,ov),sv=Math.PI,fv=2*sv,lv=1e-6,hv=fv-lv;Mt.prototype=Tt.prototype={constructor:Mt,moveTo:function(t,n){this._.push("M",this._x0=this._x1=+t,",",this._y0=this._y1=+n)},closePath:function(){null!==this._x1&&(this._x1=this._x0,this._y1=this._y0,this._.push("Z"))},lineTo:function(t,n){this._.push("L",this._x1=+t,",",this._y1=+n)},quadraticCurveTo:function(t,n,e,r){this._.push("Q",+t,",",+n,",",this._x1=+e,",",this._y1=+r)},bezierCurveTo:function(t,n,e,r,i,o){this._.push("C",+t,",",+n,",",+e,",",+r,",",this._x1=+i,",",this._y1=+o)},arcTo:function(t,n,e,r,i){t=+t,n=+n,e=+e,r=+r,i=+i;var o=this._x1,u=this._y1,a=e-t,c=r-n,s=o-t,f=u-n,l=s*s+f*f;if(i<0)throw new Error("negative radius: "+i);if(null===this._x1)this._.push("M",this._x1=t,",",this._y1=n);else if(l>lv)if(Math.abs(f*a-c*s)>lv&&i){var h=e-o,p=r-u,d=a*a+c*c,v=h*h+p*p,_=Math.sqrt(d),y=Math.sqrt(l),g=i*Math.tan((sv-Math.acos((d+l-v)/(2*_*y)))/2),m=g/y,x=g/_;Math.abs(m-1)>lv&&this._.push("L",t+m*s,",",n+m*f),this._.push("A",i,",",i,",0,0,",+(f*h>s*p),",",this._x1=t+x*a,",",this._y1=n+x*c)}else this._.push("L",this._x1=t,",",this._y1=n);else;},arc:function(t,n,e,r,i,o){t=+t,n=+n,e=+e;var u=e*Math.cos(r),a=e*Math.sin(r),c=t+u,s=n+a,f=1^o,l=o?r-i:i-r;if(e<0)throw new Error("negative radius: "+e);null===this._x1?this._.push("M",c,",",s):(Math.abs(this._x1-c)>lv||Math.abs(this._y1-s)>lv)&&this._.push("L",c,",",s),e&&(l>hv?this._.push("A",e,",",e,",0,1,",f,",",t-u,",",n-a,"A",e,",",e,",0,1,",f,",",this._x1=c,",",this._y1=s):(l<0&&(l=l%fv+fv),this._.push("A",e,",",e,",0,",+(l>=sv),",",f,",",this._x1=t+e*Math.cos(i),",",this._y1=n+e*Math.sin(i))))},rect:function(t,n,e,r){this._.push("M",this._x0=this._x1=+t,",",this._y0=this._y1=+n,"h",+e,"v",+r,"h",-e,"Z")},toString:function(){return this._.join("")}};var pv=jt.prototype=Ht.prototype;pv.copy=function(){var t,n,e=new Ht(this._x,this._y,this._x0,this._y0,this._x1,this._y1),r=this._root;if(!r)return e;if(!r.length)return e._root=Xt(r),e;for(t=[{source:r,target:e._root=new Array(4)}];r=t.pop();)for(var i=0;i<4;++i)(n=r.source[i])&&(n.length?t.push({source:n,target:r.target[i]=new Array(4)}):r.target[i]=Xt(n));return e},pv.add=kt,pv.addAll=St,pv.cover=At,pv.data=Et,pv.extent=Ct,pv.find=Pt,pv.remove=qt,pv.removeAll=Lt,pv.root=Rt,pv.size=Ut,pv.visit=Dt,pv.visitAfter=Ot,pv.x=It,pv.y=Bt;var dv=[].slice,vv={};Vt.prototype=Qt.prototype={constructor:Vt,defer:function(t){if("function"!=typeof t||this._call)throw new Error;if(null!=this._error)return this;var n=dv.call(arguments,1);return n.push(t),++this._waiting,this._tasks.push(n),Wt(this),this},abort:function(){return null==this._error&&Gt(this,new Error("abort")),this},await:function(t){if("function"!=typeof t||this._call)throw new Error;return this._call=function(n,e){t.apply(null,[n].concat(e))},Jt(this),this},awaitAll:function(t){if("function"!=typeof t||this._call)throw new Error;return this._call=t,Jt(this),this}};var _v=1e-12,yv=Math.PI,gv=yv/2,mv=2*yv;fn.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._point=0},lineEnd:function(){(this._line||0!==this._line&&1===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1,this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break;case 1:this._point=2;default:this._context.lineTo(t,n)}}};var xv=xn(ln);mn.prototype={areaStart:function(){this._curve.areaStart()},areaEnd:function(){this._curve.areaEnd()},lineStart:function(){this._curve.lineStart()},
+lineEnd:function(){this._curve.lineEnd()},point:function(t,n){this._curve.point(n*Math.sin(t),n*-Math.cos(t))}};var bv={draw:function(t,n){var e=Math.sqrt(n/yv);t.moveTo(e,0),t.arc(0,0,e,0,mv)}},wv={draw:function(t,n){var e=Math.sqrt(n/5)/2;t.moveTo(-3*e,-e),t.lineTo(-e,-e),t.lineTo(-e,-3*e),t.lineTo(e,-3*e),t.lineTo(e,-e),t.lineTo(3*e,-e),t.lineTo(3*e,e),t.lineTo(e,e),t.lineTo(e,3*e),t.lineTo(-e,3*e),t.lineTo(-e,e),t.lineTo(-3*e,e),t.closePath()}},Mv=Math.sqrt(1/3),Tv=2*Mv,kv={draw:function(t,n){var e=Math.sqrt(n/Tv),r=e*Mv;t.moveTo(0,-e),t.lineTo(r,0),t.lineTo(0,e),t.lineTo(-r,0),t.closePath()}},Nv=.8908130915292852,Sv=Math.sin(yv/10)/Math.sin(7*yv/10),Av=Math.sin(mv/10)*Sv,Ev=-Math.cos(mv/10)*Sv,Cv={draw:function(t,n){var e=Math.sqrt(n*Nv),r=Av*e,i=Ev*e;t.moveTo(0,-e),t.lineTo(r,i);for(var o=1;o<5;++o){var u=mv*o/5,a=Math.cos(u),c=Math.sin(u);t.lineTo(c*e,-a*e),t.lineTo(a*r-c*i,c*r+a*i)}t.closePath()}},zv={draw:function(t,n){var e=Math.sqrt(n),r=-e/2;t.rect(r,r,e,e)}},Pv=Math.sqrt(3),qv={draw:function(t,n){var e=-Math.sqrt(n/(3*Pv));t.moveTo(0,2*e),t.lineTo(-Pv*e,-e),t.lineTo(Pv*e,-e),t.closePath()}},Lv=-.5,Rv=Math.sqrt(3)/2,Uv=1/Math.sqrt(12),Dv=3*(Uv/2+1),Ov={draw:function(t,n){var e=Math.sqrt(n/Dv),r=e/2,i=e*Uv,o=r,u=e*Uv+e,a=-o,c=u;t.moveTo(r,i),t.lineTo(o,u),t.lineTo(a,c),t.lineTo(Lv*r-Rv*i,Rv*r+Lv*i),t.lineTo(Lv*o-Rv*u,Rv*o+Lv*u),t.lineTo(Lv*a-Rv*c,Rv*a+Lv*c),t.lineTo(Lv*r+Rv*i,Lv*i-Rv*r),t.lineTo(Lv*o+Rv*u,Lv*u-Rv*o),t.lineTo(Lv*a+Rv*c,Lv*c-Rv*a),t.closePath()}},Fv=[bv,wv,kv,zv,Cv,qv,Ov];Sn.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._y0=this._y1=NaN,this._point=0},lineEnd:function(){switch(this._point){case 3:Nn(this,this._x1,this._y1);case 2:this._context.lineTo(this._x1,this._y1)}(this._line||0!==this._line&&1===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1,this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break;case 1:this._point=2;break;case 2:this._point=3,this._context.lineTo((5*this._x0+this._x1)/6,(5*this._y0+this._y1)/6);default:Nn(this,t,n)}this._x0=this._x1,this._x1=t,this._y0=this._y1,this._y1=n}},En.prototype={areaStart:kn,areaEnd:kn,lineStart:function(){this._x0=this._x1=this._x2=this._x3=this._x4=this._y0=this._y1=this._y2=this._y3=this._y4=NaN,this._point=0},lineEnd:function(){switch(this._point){case 1:this._context.moveTo(this._x2,this._y2),this._context.closePath();break;case 2:this._context.moveTo((this._x2+2*this._x3)/3,(this._y2+2*this._y3)/3),this._context.lineTo((this._x3+2*this._x2)/3,(this._y3+2*this._y2)/3),this._context.closePath();break;case 3:this.point(this._x2,this._y2),this.point(this._x3,this._y3),this.point(this._x4,this._y4)}},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1,this._x2=t,this._y2=n;break;case 1:this._point=2,this._x3=t,this._y3=n;break;case 2:this._point=3,this._x4=t,this._y4=n,this._context.moveTo((this._x0+4*this._x1+t)/6,(this._y0+4*this._y1+n)/6);break;default:Nn(this,t,n)}this._x0=this._x1,this._x1=t,this._y0=this._y1,this._y1=n}},zn.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._y0=this._y1=NaN,this._point=0},lineEnd:function(){(this._line||0!==this._line&&3===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1;break;case 1:this._point=2;break;case 2:this._point=3;var e=(this._x0+4*this._x1+t)/6,r=(this._y0+4*this._y1+n)/6;this._line?this._context.lineTo(e,r):this._context.moveTo(e,r);break;case 3:this._point=4;default:Nn(this,t,n)}this._x0=this._x1,this._x1=t,this._y0=this._y1,this._y1=n}},qn.prototype={lineStart:function(){this._x=[],this._y=[],this._basis.lineStart()},lineEnd:function(){var t=this._x,n=this._y,e=t.length-1;if(e>0)for(var r,i=t[0],o=n[0],u=t[e]-i,a=n[e]-o,c=-1;++c<=e;)r=c/e,this._basis.point(this._beta*t[c]+(1-this._beta)*(i+r*u),this._beta*n[c]+(1-this._beta)*(o+r*a));this._x=this._y=null,this._basis.lineEnd()},point:function(t,n){this._x.push(+t),this._y.push(+n)}};var Iv=function Tw(t){function n(n){return 1===t?new Sn(n):new qn(n,t)}return n.beta=function(t){return Tw(+t)},n}(.85);Rn.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._x2=this._y0=this._y1=this._y2=NaN,this._point=0},lineEnd:function(){switch(this._point){case 2:this._context.lineTo(this._x2,this._y2);break;case 3:Ln(this,this._x1,this._y1)}(this._line||0!==this._line&&1===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1,this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break;case 1:this._point=2,this._x1=t,this._y1=n;break;case 2:this._point=3;default:Ln(this,t,n)}this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}};var Yv=function kw(t){function n(n){return new Rn(n,t)}return n.tension=function(t){return kw(+t)},n}(0);Un.prototype={areaStart:kn,areaEnd:kn,lineStart:function(){this._x0=this._x1=this._x2=this._x3=this._x4=this._x5=this._y0=this._y1=this._y2=this._y3=this._y4=this._y5=NaN,this._point=0},lineEnd:function(){switch(this._point){case 1:this._context.moveTo(this._x3,this._y3),this._context.closePath();break;case 2:this._context.lineTo(this._x3,this._y3),this._context.closePath();break;case 3:this.point(this._x3,this._y3),this.point(this._x4,this._y4),this.point(this._x5,this._y5)}},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1,this._x3=t,this._y3=n;break;case 1:this._point=2,this._context.moveTo(this._x4=t,this._y4=n);break;case 2:this._point=3,this._x5=t,this._y5=n;break;default:Ln(this,t,n)}this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}};var Bv=function Nw(t){function n(n){return new Un(n,t)}return n.tension=function(t){return Nw(+t)},n}(0);Dn.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._x2=this._y0=this._y1=this._y2=NaN,this._point=0},lineEnd:function(){(this._line||0!==this._line&&3===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1;break;case 1:this._point=2;break;case 2:this._point=3,this._line?this._context.lineTo(this._x2,this._y2):this._context.moveTo(this._x2,this._y2);break;case 3:this._point=4;default:Ln(this,t,n)}this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}};var jv=function Sw(t){function n(n){return new Dn(n,t)}return n.tension=function(t){return Sw(+t)},n}(0);Fn.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._x2=this._y0=this._y1=this._y2=NaN,this._l01_a=this._l12_a=this._l23_a=this._l01_2a=this._l12_2a=this._l23_2a=this._point=0},lineEnd:function(){switch(this._point){case 2:this._context.lineTo(this._x2,this._y2);break;case 3:this.point(this,this._x2,this._y2)}(this._line||0!==this._line&&1===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){if(t=+t,n=+n,this._point){var e=this._x2-t,r=this._y2-n;this._l23_a=Math.sqrt(this._l23_2a=Math.pow(e*e+r*r,this._alpha))}switch(this._point){case 0:this._point=1,this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break;case 1:this._point=2;break;case 2:this._point=3;default:On(this,t,n)}this._l01_a=this._l12_a,this._l12_a=this._l23_a,this._l01_2a=this._l12_2a,this._l12_2a=this._l23_2a,this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}};var Hv=function Aw(t){function n(n){return t?new Fn(n,t):new Rn(n,0)}return n.alpha=function(t){return Aw(+t)},n}(.5);In.prototype={areaStart:kn,areaEnd:kn,lineStart:function(){this._x0=this._x1=this._x2=this._x3=this._x4=this._x5=this._y0=this._y1=this._y2=this._y3=this._y4=this._y5=NaN,this._l01_a=this._l12_a=this._l23_a=this._l01_2a=this._l12_2a=this._l23_2a=this._point=0},lineEnd:function(){switch(this._point){case 1:this._context.moveTo(this._x3,this._y3),this._context.closePath();break;case 2:this._context.lineTo(this._x3,this._y3),this._context.closePath();break;case 3:this.point(this._x3,this._y3),this.point(this._x4,this._y4),this.point(this._x5,this._y5)}},point:function(t,n){if(t=+t,n=+n,this._point){var e=this._x2-t,r=this._y2-n;this._l23_a=Math.sqrt(this._l23_2a=Math.pow(e*e+r*r,this._alpha))}switch(this._point){case 0:this._point=1,this._x3=t,this._y3=n;break;case 1:this._point=2,this._context.moveTo(this._x4=t,this._y4=n);break;case 2:this._point=3,this._x5=t,this._y5=n;break;default:On(this,t,n)}this._l01_a=this._l12_a,this._l12_a=this._l23_a,this._l01_2a=this._l12_2a,this._l12_2a=this._l23_2a,this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}};var Xv=function Ew(t){function n(n){return t?new In(n,t):new Un(n,0)}return n.alpha=function(t){return Ew(+t)},n}(.5);Yn.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._x2=this._y0=this._y1=this._y2=NaN,this._l01_a=this._l12_a=this._l23_a=this._l01_2a=this._l12_2a=this._l23_2a=this._point=0},lineEnd:function(){(this._line||0!==this._line&&3===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){if(t=+t,n=+n,this._point){var e=this._x2-t,r=this._y2-n;this._l23_a=Math.sqrt(this._l23_2a=Math.pow(e*e+r*r,this._alpha))}switch(this._point){case 0:this._point=1;break;case 1:this._point=2;break;case 2:this._point=3,this._line?this._context.lineTo(this._x2,this._y2):this._context.moveTo(this._x2,this._y2);break;case 3:this._point=4;default:On(this,t,n)}this._l01_a=this._l12_a,this._l12_a=this._l23_a,this._l01_2a=this._l12_2a,this._l12_2a=this._l23_2a,this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}};var Vv=function Cw(t){function n(n){return t?new Yn(n,t):new Dn(n,0)}return n.alpha=function(t){return Cw(+t)},n}(.5);Bn.prototype={areaStart:kn,areaEnd:kn,lineStart:function(){this._point=0},lineEnd:function(){this._point&&this._context.closePath()},point:function(t,n){t=+t,n=+n,this._point?this._context.lineTo(t,n):(this._point=1,this._context.moveTo(t,n))}},$n.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._y0=this._y1=this._t0=NaN,this._point=0},lineEnd:function(){switch(this._point){case 2:this._context.lineTo(this._x1,this._y1);break;case 3:Wn(this,this._t0,Vn(this,this._t0))}(this._line||0!==this._line&&1===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){var e=NaN;if(t=+t,n=+n,t!==this._x1||n!==this._y1){switch(this._point){case 0:this._point=1,this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break;case 1:this._point=2;break;case 2:this._point=3,Wn(this,Vn(this,e=Xn(this,t,n)),e);break;default:Wn(this,this._t0,e=Xn(this,t,n))}this._x0=this._x1,this._x1=t,this._y0=this._y1,this._y1=n,this._t0=e}}},(Zn.prototype=Object.create($n.prototype)).point=function(t,n){$n.prototype.point.call(this,n,t)},Gn.prototype={moveTo:function(t,n){this._context.moveTo(n,t)},closePath:function(){this._context.closePath()},lineTo:function(t,n){this._context.lineTo(n,t)},bezierCurveTo:function(t,n,e,r,i,o){this._context.bezierCurveTo(n,t,r,e,o,i)}},Kn.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x=[],this._y=[]},lineEnd:function(){var t=this._x,n=this._y,e=t.length;if(e)if(this._line?this._context.lineTo(t[0],n[0]):this._context.moveTo(t[0],n[0]),2===e)this._context.lineTo(t[1],n[1]);else for(var r=te(t),i=te(n),o=0,u=1;u<e;++o,++u)this._context.bezierCurveTo(r[0][o],i[0][o],r[1][o],i[1][o],t[u],n[u]);(this._line||0!==this._line&&1===e)&&this._context.closePath(),this._line=1-this._line,this._x=this._y=null},point:function(t,n){this._x.push(+t),this._y.push(+n)}},ee.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x=this._y=NaN,this._point=0},lineEnd:function(){0<this._t&&this._t<1&&2===this._point&&this._context.lineTo(this._x,this._y),(this._line||0!==this._line&&1===this._point)&&this._context.closePath(),this._line>=0&&(this._t=1-this._t,this._line=1-this._line)},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1,this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break;case 1:this._point=2;default:if(this._t<=0)this._context.lineTo(this._x,n),this._context.lineTo(t,n);else{var e=this._x*(1-this._t)+t*this._t;this._context.lineTo(e,this._y),this._context.lineTo(e,n)}}this._x=t,this._y=n}};var Wv=Array.prototype.slice,$v=.7,Zv=1/$v,Gv=/^#([0-9a-f]{3})$/,Jv=/^#([0-9a-f]{6})$/,Qv=/^rgb\(\s*([-+]?\d+)\s*,\s*([-+]?\d+)\s*,\s*([-+]?\d+)\s*\)$/,Kv=/^rgb\(\s*([-+]?\d+(?:\.\d+)?)%\s*,\s*([-+]?\d+(?:\.\d+)?)%\s*,\s*([-+]?\d+(?:\.\d+)?)%\s*\)$/,t_=/^rgba\(\s*([-+]?\d+)\s*,\s*([-+]?\d+)\s*,\s*([-+]?\d+)\s*,\s*([-+]?\d+(?:\.\d+)?)\s*\)$/,n_=/^rgba\(\s*([-+]?\d+(?:\.\d+)?)%\s*,\s*([-+]?\d+(?:\.\d+)?)%\s*,\s*([-+]?\d+(?:\.\d+)?)%\s*,\s*([-+]?\d+(?:\.\d+)?)\s*\)$/,e_=/^hsl\(\s*([-+]?\d+(?:\.\d+)?)\s*,\s*([-+]?\d+(?:\.\d+)?)%\s*,\s*([-+]?\d+(?:\.\d+)?)%\s*\)$/,r_=/^hsla\(\s*([-+]?\d+(?:\.\d+)?)\s*,\s*([-+]?\d+(?:\.\d+)?)%\s*,\s*([-+]?\d+(?:\.\d+)?)%\s*,\s*([-+]?\d+(?:\.\d+)?)\s*\)$/,i_={aliceblue:15792383,antiquewhite:16444375,aqua:65535,aquamarine:8388564,azure:15794175,beige:16119260,bisque:16770244,black:0,blanchedalmond:16772045,blue:255,blueviolet:9055202,brown:10824234,burlywood:14596231,cadetblue:6266528,chartreuse:8388352,chocolate:13789470,coral:16744272,cornflowerblue:6591981,cornsilk:16775388,crimson:14423100,cyan:65535,darkblue:139,darkcyan:35723,darkgoldenrod:12092939,darkgray:11119017,darkgreen:25600,darkgrey:11119017,darkkhaki:12433259,darkmagenta:9109643,darkolivegreen:5597999,darkorange:16747520,darkorchid:10040012,darkred:9109504,darksalmon:15308410,darkseagreen:9419919,darkslateblue:4734347,darkslategray:3100495,darkslategrey:3100495,darkturquoise:52945,darkviolet:9699539,deeppink:16716947,deepskyblue:49151,dimgray:6908265,dimgrey:6908265,dodgerblue:2003199,firebrick:11674146,floralwhite:16775920,forestgreen:2263842,fuchsia:16711935,gainsboro:14474460,ghostwhite:16316671,gold:16766720,goldenrod:14329120,gray:8421504,green:32768,greenyellow:11403055,grey:8421504,honeydew:15794160,hotpink:16738740,indianred:13458524,indigo:4915330,ivory:16777200,khaki:15787660,lavender:15132410,lavenderblush:16773365,lawngreen:8190976,lemonchiffon:16775885,lightblue:11393254,lightcoral:15761536,lightcyan:14745599,lightgoldenrodyellow:16448210,lightgray:13882323,lightgreen:9498256,lightgrey:13882323,lightpink:16758465,lightsalmon:16752762,lightseagreen:2142890,lightskyblue:8900346,lightslategray:7833753,lightslategrey:7833753,lightsteelblue:11584734,lightyellow:16777184,lime:65280,limegreen:3329330,linen:16445670,magenta:16711935,maroon:8388608,mediumaquamarine:6737322,mediumblue:205,mediumorchid:12211667,mediumpurple:9662683,mediumseagreen:3978097,mediumslateblue:8087790,mediumspringgreen:64154,mediumturquoise:4772300,mediumvioletred:13047173,midnightblue:1644912,mintcream:16121850,mistyrose:16770273,moccasin:16770229,navajowhite:16768685,navy:128,oldlace:16643558,olive:8421376,olivedrab:7048739,orange:16753920,orangered:16729344,orchid:14315734,palegoldenrod:15657130,palegreen:10025880,paleturquoise:11529966,palevioletred:14381203,papayawhip:16773077,peachpuff:16767673,peru:13468991,pink:16761035,plum:14524637,powderblue:11591910,purple:8388736,rebeccapurple:6697881,red:16711680,rosybrown:12357519,royalblue:4286945,saddlebrown:9127187,salmon:16416882,sandybrown:16032864,seagreen:3050327,seashell:16774638,sienna:10506797,silver:12632256,skyblue:8900331,slateblue:6970061,slategray:7372944,slategrey:7372944,snow:16775930,springgreen:65407,steelblue:4620980,tan:13808780,teal:32896,thistle:14204888,tomato:16737095,turquoise:4251856,violet:15631086,wheat:16113331,white:16777215,whitesmoke:16119285,yellow:16776960,yellowgreen:10145074};ge(xe,be,{displayable:function(){return this.rgb().displayable()},toString:function(){return this.rgb()+""}}),ge(Ne,ke,me(xe,{brighter:function(t){return t=null==t?Zv:Math.pow(Zv,t),new Ne(this.r*t,this.g*t,this.b*t,this.opacity)},darker:function(t){return t=null==t?$v:Math.pow($v,t),new Ne(this.r*t,this.g*t,this.b*t,this.opacity)},rgb:function(){return this},displayable:function(){return 0<=this.r&&this.r<=255&&0<=this.g&&this.g<=255&&0<=this.b&&this.b<=255&&0<=this.opacity&&this.opacity<=1},toString:function(){var t=this.opacity;return t=isNaN(t)?1:Math.max(0,Math.min(1,t)),(1===t?"rgb(":"rgba(")+Math.max(0,Math.min(255,Math.round(this.r)||0))+", "+Math.max(0,Math.min(255,Math.round(this.g)||0))+", "+Math.max(0,Math.min(255,Math.round(this.b)||0))+(1===t?")":", "+t+")")}})),ge(Ce,Ee,me(xe,{brighter:function(t){return t=null==t?Zv:Math.pow(Zv,t),new Ce(this.h,this.s,this.l*t,this.opacity)},darker:function(t){return t=null==t?$v:Math.pow($v,t),new Ce(this.h,this.s,this.l*t,this.opacity)},rgb:function(){var t=this.h%360+360*(this.h<0),n=isNaN(t)||isNaN(this.s)?0:this.s,e=this.l,r=e+(e<.5?e:1-e)*n,i=2*e-r;return new Ne(ze(t>=240?t-240:t+120,i,r),ze(t,i,r),ze(t<120?t+240:t-120,i,r),this.opacity)},displayable:function(){return(0<=this.s&&this.s<=1||isNaN(this.s))&&0<=this.l&&this.l<=1&&0<=this.opacity&&this.opacity<=1}}));var o_=Math.PI/180,u_=180/Math.PI,a_=18,c_=.95047,s_=1,f_=1.08883,l_=4/29,h_=6/29,p_=3*h_*h_,d_=h_*h_*h_;ge(Le,qe,me(xe,{brighter:function(t){return new Le(this.l+a_*(null==t?1:t),this.a,this.b,this.opacity)},darker:function(t){return new Le(this.l-a_*(null==t?1:t),this.a,this.b,this.opacity)},rgb:function(){var t=(this.l+16)/116,n=isNaN(this.a)?t:t+this.a/500,e=isNaN(this.b)?t:t-this.b/200;return t=s_*Ue(t),n=c_*Ue(n),e=f_*Ue(e),new Ne(De(3.2404542*n-1.5371385*t-.4985314*e),De(-.969266*n+1.8760108*t+.041556*e),De(.0556434*n-.2040259*t+1.0572252*e),this.opacity)}})),ge(Ye,Ie,me(xe,{brighter:function(t){return new Ye(this.h,this.c,this.l+a_*(null==t?1:t),this.opacity)},darker:function(t){return new Ye(this.h,this.c,this.l-a_*(null==t?1:t),this.opacity)},rgb:function(){return Pe(this).rgb()}}));var v_=-.14861,__=1.78277,y_=-.29227,g_=-.90649,m_=1.97294,x_=m_*g_,b_=m_*__,w_=__*y_-g_*v_;ge(He,je,me(xe,{brighter:function(t){return t=null==t?Zv:Math.pow(Zv,t),new He(this.h,this.s,this.l*t,this.opacity)},darker:function(t){return t=null==t?$v:Math.pow($v,t),new He(this.h,this.s,this.l*t,this.opacity)},rgb:function(){var t=isNaN(this.h)?0:(this.h+120)*o_,n=+this.l,e=isNaN(this.s)?0:this.s*n*(1-n),r=Math.cos(t),i=Math.sin(t);return new Ne(255*(n+e*(v_*r+__*i)),255*(n+e*(y_*r+g_*i)),255*(n+e*(m_*r)),this.opacity)}}));var M_,T_,k_,N_,S_=function zw(t){function n(t,n){var r=e((t=ke(t)).r,(n=ke(n)).r),i=e(t.g,n.g),o=e(t.b,n.b),u=e(t.opacity,n.opacity);return function(n){return t.r=r(n),t.g=i(n),t.b=o(n),t.opacity=u(n),t+""}}var e=Qe(t);return n.gamma=zw,n}(1),A_=tr(Ve),E_=tr(We),C_=/[-+]?(?:\d+\.?\d*|\.?\d+)(?:[eE][-+]?\d+)?/g,z_=new RegExp(C_.source,"g"),P_=180/Math.PI,q_={translateX:0,translateY:0,rotate:0,skewX:0,scaleX:1,scaleY:1},L_=pr(lr,"px, ","px)","deg)"),R_=pr(hr,", ",")",")"),U_=Math.SQRT2,D_=2,O_=4,F_=1e-12,I_=gr(Je),Y_=gr(Ke),B_=xr(Je),j_=xr(Ke),H_=br(Je),X_=br(Ke),V_={value:function(){}};Tr.prototype=Mr.prototype={constructor:Tr,on:function(t,n){var e,r=this._,i=kr(t+"",r),o=-1,u=i.length;{if(!(arguments.length<2)){if(null!=n&&"function"!=typeof n)throw new Error("invalid callback: "+n);for(;++o<u;)if(e=(t=i[o]).type)r[e]=Sr(r[e],t.name,n);else if(null==n)for(e in r)r[e]=Sr(r[e],t.name,null);return this}for(;++o<u;)if((e=(t=i[o]).type)&&(e=Nr(r[e],t.name)))return e}},copy:function(){var t={},n=this._;for(var e in n)t[e]=n[e].slice();return new Tr(t)},call:function(t,n){if((e=arguments.length-2)>0)for(var e,r,i=new Array(e),o=0;o<e;++o)i[o]=arguments[o+2];if(!this._.hasOwnProperty(t))throw new Error("unknown type: "+t);for(r=this._[t],o=0,e=r.length;o<e;++o)r[o].value.apply(n,i)},apply:function(t,n,e){if(!this._.hasOwnProperty(t))throw new Error("unknown type: "+t);for(var r=this._[t],i=0,o=r.length;i<o;++i)r[i].value.apply(n,e)}};var W_,$_,Z_=zr(","),G_=Z_.parse,J_=Z_.parseRows,Q_=Z_.format,K_=Z_.formatRows,ty=zr("\t"),ny=ty.parse,ey=ty.parseRows,ry=ty.format,iy=ty.formatRows,oy=Rr("text/html",function(t){return document.createRange().createContextualFragment(t.responseText)}),uy=Rr("application/json",function(t){return JSON.parse(t.responseText)}),ay=Rr("text/plain",function(t){return t.responseText}),cy=Rr("application/xml",function(t){var n=t.responseXML;if(!n)throw new Error("parse error");return n}),sy=Ur("text/csv",G_),fy=Ur("text/tab-separated-values",ny),ly=0,hy=0,py=0,dy=1e3,vy=0,_y=0,yy=0,gy="object"==typeof performance&&performance.now?performance:Date,my="function"==typeof requestAnimationFrame?gy===Date?function(t){requestAnimationFrame(function(){t(gy.now())})}:requestAnimationFrame:function(t){setTimeout(t,17)};Ir.prototype=Yr.prototype={constructor:Ir,restart:function(t,n,e){if("function"!=typeof t)throw new TypeError("callback is not a function");e=(null==e?Or():+e)+(null==n?0:+n),this._next||$_===this||($_?$_._next=this:W_=this,$_=this),this._call=t,this._time=e,Vr()},stop:function(){this._call&&(this._call=null,this._time=1/0,Vr())}};var xy=new Date,by=new Date,wy=Zr(function(){},function(t,n){t.setTime(+t+n)},function(t,n){return n-t});wy.every=function(t){return t=Math.floor(t),isFinite(t)&&t>0?t>1?Zr(function(n){n.setTime(Math.floor(n/t)*t)},function(n,e){n.setTime(+n+e*t)},function(n,e){return(e-n)/t}):wy:null};var My=wy.range,Ty=1e3,ky=6e4,Ny=36e5,Sy=864e5,Ay=6048e5,Ey=Zr(function(t){t.setTime(Math.floor(t/Ty)*Ty)},function(t,n){t.setTime(+t+n*Ty)},function(t,n){return(n-t)/Ty},function(t){return t.getUTCSeconds()}),Cy=Ey.range,zy=Zr(function(t){t.setTime(Math.floor(t/ky)*ky)},function(t,n){t.setTime(+t+n*ky)},function(t,n){return(n-t)/ky},function(t){return t.getMinutes()}),Py=zy.range,qy=Zr(function(t){var n=t.getTimezoneOffset()*ky%Ny;n<0&&(n+=Ny),t.setTime(Math.floor((+t-n)/Ny)*Ny+n)},function(t,n){t.setTime(+t+n*Ny)},function(t,n){return(n-t)/Ny},function(t){return t.getHours()}),Ly=qy.range,Ry=Zr(function(t){t.setHours(0,0,0,0)},function(t,n){t.setDate(t.getDate()+n)},function(t,n){return(n-t-(n.getTimezoneOffset()-t.getTimezoneOffset())*ky)/Sy},function(t){return t.getDate()-1}),Uy=Ry.range,Dy=Gr(0),Oy=Gr(1),Fy=Gr(2),Iy=Gr(3),Yy=Gr(4),By=Gr(5),jy=Gr(6),Hy=Dy.range,Xy=Oy.range,Vy=Fy.range,Wy=Iy.range,$y=Yy.range,Zy=By.range,Gy=jy.range,Jy=Zr(function(t){t.setDate(1),t.setHours(0,0,0,0)},function(t,n){t.setMonth(t.getMonth()+n)},function(t,n){return n.getMonth()-t.getMonth()+12*(n.getFullYear()-t.getFullYear())},function(t){return t.getMonth()}),Qy=Jy.range,Ky=Zr(function(t){t.setMonth(0,1),t.setHours(0,0,0,0)},function(t,n){t.setFullYear(t.getFullYear()+n)},function(t,n){return n.getFullYear()-t.getFullYear()},function(t){return t.getFullYear()});Ky.every=function(t){return isFinite(t=Math.floor(t))&&t>0?Zr(function(n){n.setFullYear(Math.floor(n.getFullYear()/t)*t),n.setMonth(0,1),n.setHours(0,0,0,0)},function(n,e){n.setFullYear(n.getFullYear()+e*t)}):null};var tg=Ky.range,ng=Zr(function(t){t.setUTCSeconds(0,0)},function(t,n){t.setTime(+t+n*ky)},function(t,n){return(n-t)/ky},function(t){return t.getUTCMinutes()}),eg=ng.range,rg=Zr(function(t){t.setUTCMinutes(0,0,0)},function(t,n){t.setTime(+t+n*Ny)},function(t,n){return(n-t)/Ny},function(t){return t.getUTCHours()}),ig=rg.range,og=Zr(function(t){t.setUTCHours(0,0,0,0)},function(t,n){t.setUTCDate(t.getUTCDate()+n)},function(t,n){return(n-t)/Sy},function(t){return t.getUTCDate()-1}),ug=og.range,ag=Jr(0),cg=Jr(1),sg=Jr(2),fg=Jr(3),lg=Jr(4),hg=Jr(5),pg=Jr(6),dg=ag.range,vg=cg.range,_g=sg.range,yg=fg.range,gg=lg.range,mg=hg.range,xg=pg.range,bg=Zr(function(t){t.setUTCDate(1),t.setUTCHours(0,0,0,0)},function(t,n){t.setUTCMonth(t.getUTCMonth()+n)},function(t,n){return n.getUTCMonth()-t.getUTCMonth()+12*(n.getUTCFullYear()-t.getUTCFullYear())},function(t){return t.getUTCMonth()}),wg=bg.range,Mg=Zr(function(t){t.setUTCMonth(0,1),t.setUTCHours(0,0,0,0)},function(t,n){t.setUTCFullYear(t.getUTCFullYear()+n)},function(t,n){return n.getUTCFullYear()-t.getUTCFullYear()},function(t){return t.getUTCFullYear()});Mg.every=function(t){return isFinite(t=Math.floor(t))&&t>0?Zr(function(n){n.setUTCFullYear(Math.floor(n.getUTCFullYear()/t)*t),n.setUTCMonth(0,1),n.setUTCHours(0,0,0,0)},function(n,e){n.setUTCFullYear(n.getUTCFullYear()+e*t)}):null};var Tg,kg=Mg.range,Ng={"":ni,"%":function(t,n){return(100*t).toFixed(n)},b:function(t){return Math.round(t).toString(2)},c:function(t){return t+""},d:function(t){return Math.round(t).toString(10)},e:function(t,n){return t.toExponential(n)},f:function(t,n){return t.toFixed(n)},g:function(t,n){return t.toPrecision(n)},o:function(t){return Math.round(t).toString(8)},p:function(t,n){return ri(100*t,n)},r:ri,s:ei,X:function(t){return Math.round(t).toString(16).toUpperCase()},x:function(t){return Math.round(t).toString(16)}},Sg=/^(?:(.)?([<>=^]))?([+\-\( ])?([$#])?(0)?(\d+)?(,)?(\.\d+)?([a-z%])?$/i;oi.prototype.toString=function(){return this.fill+this.align+this.sign+this.symbol+(this.zero?"0":"")+(null==this.width?"":Math.max(1,0|this.width))+(this.comma?",":"")+(null==this.precision?"":"."+Math.max(0,0|this.precision))+this.type};var Ag,Eg=["y","z","a","f","p","n","µ","m","","k","M","G","T","P","E","Z","Y"];t.format,t.formatPrefix,ci({decimal:".",thousands:",",grouping:[3],currency:["$",""]});var Cg,zg={"-":"",_:" ",0:"0"},Pg=/^\s*\d+/,qg=/^%/,Lg=/[\\\^\$\*\+\?\|\[\]\(\)\.\{\}]/g;t.timeFormat,t.timeParse,t.utcFormat,t.utcParse,so({dateTime:"%x, %X",date:"%-m/%-d/%Y",time:"%-I:%M:%S %p",periods:["AM","PM"],days:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],shortDays:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],months:["January","February","March","April","May","June","July","August","September","October","November","December"],shortMonths:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]});var Rg="%Y-%m-%dT%H:%M:%S.%LZ",Ug=Date.prototype.toISOString?fo:t.utcFormat(Rg),Dg=+new Date("2000-01-01T00:00:00.000Z")?lo:t.utcParse(Rg),Og=Array.prototype,Fg=Og.map,Ig=Og.slice,Yg={name:"implicit"},Bg=[0,1],jg=1e3,Hg=60*jg,Xg=60*Hg,Vg=24*Xg,Wg=7*Vg,$g=30*Vg,Zg=365*Vg,Gg=Zo("1f77b4ff7f0e2ca02cd627289467bd8c564be377c27f7f7fbcbd2217becf"),Jg=Zo("393b795254a36b6ecf9c9ede6379398ca252b5cf6bcedb9c8c6d31bd9e39e7ba52e7cb94843c39ad494ad6616be7969c7b4173a55194ce6dbdde9ed6"),Qg=Zo("3182bd6baed69ecae1c6dbefe6550dfd8d3cfdae6bfdd0a231a35474c476a1d99bc7e9c0756bb19e9ac8bcbddcdadaeb636363969696bdbdbdd9d9d9"),Kg=Zo("1f77b4aec7e8ff7f0effbb782ca02c98df8ad62728ff98969467bdc5b0d58c564bc49c94e377c2f7b6d27f7f7fc7c7c7bcbd22dbdb8d17becf9edae5"),tm=X_(je(300,.5,0),je(-240,.5,1)),nm=X_(je(-100,.75,.35),je(80,1.5,.8)),em=X_(je(260,.75,.35),je(80,1.5,.8)),rm=je(),im=Jo(Zo("44015444025645045745055946075a46085c460a5d460b5e470d60470e6147106347116447136548146748166848176948186a481a6c481b6d481c6e481d6f481f70482071482173482374482475482576482677482878482979472a7a472c7a472d7b472e7c472f7d46307e46327e46337f463480453581453781453882443983443a83443b84433d84433e85423f854240864241864142874144874045884046883f47883f48893e49893e4a893e4c8a3d4d8a3d4e8a3c4f8a3c508b3b518b3b528b3a538b3a548c39558c39568c38588c38598c375a8c375b8d365c8d365d8d355e8d355f8d34608d34618d33628d33638d32648e32658e31668e31678e31688e30698e306a8e2f6b8e2f6c8e2e6d8e2e6e8e2e6f8e2d708e2d718e2c718e2c728e2c738e2b748e2b758e2a768e2a778e2a788e29798e297a8e297b8e287c8e287d8e277e8e277f8e27808e26818e26828e26828e25838e25848e25858e24868e24878e23888e23898e238a8d228b8d228c8d228d8d218e8d218f8d21908d21918c20928c20928c20938c1f948c1f958b1f968b1f978b1f988b1f998a1f9a8a1e9b8a1e9c891e9d891f9e891f9f881fa0881fa1881fa1871fa28720a38620a48621a58521a68522a78522a88423a98324aa8325ab8225ac8226ad8127ad8128ae8029af7f2ab07f2cb17e2db27d2eb37c2fb47c31b57b32b67a34b67935b77937b87838b9773aba763bbb753dbc743fbc7340bd7242be7144bf7046c06f48c16e4ac16d4cc26c4ec36b50c46a52c56954c56856c66758c7655ac8645cc8635ec96260ca6063cb5f65cb5e67cc5c69cd5b6ccd5a6ece5870cf5773d05675d05477d1537ad1517cd2507fd34e81d34d84d44b86d54989d5488bd6468ed64590d74393d74195d84098d83e9bd93c9dd93ba0da39a2da37a5db36a8db34aadc32addc30b0dd2fb2dd2db5de2bb8de29bade28bddf26c0df25c2df23c5e021c8e020cae11fcde11dd0e11cd2e21bd5e21ad8e219dae319dde318dfe318e2e418e5e419e7e419eae51aece51befe51cf1e51df4e61ef6e620f8e621fbe723fde725")),om=Jo(Zo("00000401000501010601010802010902020b02020d03030f03031204041405041606051806051a07061c08071e0907200a08220b09240c09260d0a290e0b2b100b2d110c2f120d31130d34140e36150e38160f3b180f3d19103f1a10421c10441d11471e114920114b21114e22115024125325125527125829115a2a115c2c115f2d11612f116331116533106734106936106b38106c390f6e3b0f703d0f713f0f72400f74420f75440f764510774710784910784a10794c117a4e117b4f127b51127c52137c54137d56147d57157e59157e5a167e5c167f5d177f5f187f601880621980641a80651a80671b80681c816a1c816b1d816d1d816e1e81701f81721f817320817521817621817822817922827b23827c23827e24828025828125818326818426818627818827818928818b29818c29818e2a81902a81912b81932b80942c80962c80982d80992d809b2e7f9c2e7f9e2f7fa02f7fa1307ea3307ea5317ea6317da8327daa337dab337cad347cae347bb0357bb2357bb3367ab5367ab73779b83779ba3878bc3978bd3977bf3a77c03a76c23b75c43c75c53c74c73d73c83e73ca3e72cc3f71cd4071cf4070d0416fd2426fd3436ed5446dd6456cd8456cd9466bdb476adc4869de4968df4a68e04c67e24d66e34e65e44f64e55064e75263e85362e95462ea5661eb5760ec5860ed5a5fee5b5eef5d5ef05f5ef1605df2625df2645cf3655cf4675cf4695cf56b5cf66c5cf66e5cf7705cf7725cf8745cf8765cf9785df9795df97b5dfa7d5efa7f5efa815ffb835ffb8560fb8761fc8961fc8a62fc8c63fc8e64fc9065fd9266fd9467fd9668fd9869fd9a6afd9b6bfe9d6cfe9f6dfea16efea36ffea571fea772fea973feaa74feac76feae77feb078feb27afeb47bfeb67cfeb77efeb97ffebb81febd82febf84fec185fec287fec488fec68afec88cfeca8dfecc8ffecd90fecf92fed194fed395fed597fed799fed89afdda9cfddc9efddea0fde0a1fde2a3fde3a5fde5a7fde7a9fde9aafdebacfcecaefceeb0fcf0b2fcf2b4fcf4b6fcf6b8fcf7b9fcf9bbfcfbbdfcfdbf")),um=Jo(Zo("00000401000501010601010802010a02020c02020e03021004031204031405041706041907051b08051d09061f0a07220b07240c08260d08290e092b10092d110a30120a32140b34150b37160b39180c3c190c3e1b0c411c0c431e0c451f0c48210c4a230c4c240c4f260c51280b53290b552b0b572d0b592f0a5b310a5c320a5e340a5f3609613809623909633b09643d09653e0966400a67420a68440a68450a69470b6a490b6a4a0c6b4c0c6b4d0d6c4f0d6c510e6c520e6d540f6d550f6d57106e59106e5a116e5c126e5d126e5f136e61136e62146e64156e65156e67166e69166e6a176e6c186e6d186e6f196e71196e721a6e741a6e751b6e771c6d781c6d7a1d6d7c1d6d7d1e6d7f1e6c801f6c82206c84206b85216b87216b88226a8a226a8c23698d23698f24699025689225689326679526679727669827669a28659b29649d29649f2a63a02a63a22b62a32c61a52c60a62d60a82e5fa92e5eab2f5ead305dae305cb0315bb1325ab3325ab43359b63458b73557b93556ba3655bc3754bd3853bf3952c03a51c13a50c33b4fc43c4ec63d4dc73e4cc83f4bca404acb4149cc4248ce4347cf4446d04545d24644d34743d44842d54a41d74b3fd84c3ed94d3dda4e3cdb503bdd513ade5238df5337e05536e15635e25734e35933e45a31e55c30e65d2fe75e2ee8602de9612bea632aeb6429eb6628ec6726ed6925ee6a24ef6c23ef6e21f06f20f1711ff1731df2741cf3761bf37819f47918f57b17f57d15f67e14f68013f78212f78410f8850ff8870ef8890cf98b0bf98c0af98e09fa9008fa9207fa9407fb9606fb9706fb9906fb9b06fb9d07fc9f07fca108fca309fca50afca60cfca80dfcaa0ffcac11fcae12fcb014fcb216fcb418fbb61afbb81dfbba1ffbbc21fbbe23fac026fac228fac42afac62df9c72ff9c932f9cb35f8cd37f8cf3af7d13df7d340f6d543f6d746f5d949f5db4cf4dd4ff4df53f4e156f3e35af3e55df2e661f2e865f2ea69f1ec6df1ed71f1ef75f1f179f2f27df2f482f3f586f3f68af4f88ef5f992f6fa96f8fb9af9fc9dfafda1fcffa4")),am=Jo(Zo("0d088710078813078916078a19068c1b068d1d068e20068f2206902406912605912805922a05932c05942e05952f059631059733059735049837049938049a3a049a3c049b3e049c3f049c41049d43039e44039e46039f48039f4903a04b03a14c02a14e02a25002a25102a35302a35502a45601a45801a45901a55b01a55c01a65e01a66001a66100a76300a76400a76600a76700a86900a86a00a86c00a86e00a86f00a87100a87201a87401a87501a87701a87801a87a02a87b02a87d03a87e03a88004a88104a78305a78405a78606a68707a68808a68a09a58b0aa58d0ba58e0ca48f0da4910ea3920fa39410a29511a19613a19814a099159f9a169f9c179e9d189d9e199da01a9ca11b9ba21d9aa31e9aa51f99a62098a72197a82296aa2395ab2494ac2694ad2793ae2892b02991b12a90b22b8fb32c8eb42e8db52f8cb6308bb7318ab83289ba3388bb3488bc3587bd3786be3885bf3984c03a83c13b82c23c81c33d80c43e7fc5407ec6417dc7427cc8437bc9447aca457acb4679cc4778cc4977cd4a76ce4b75cf4c74d04d73d14e72d24f71d35171d45270d5536fd5546ed6556dd7566cd8576bd9586ada5a6ada5b69db5c68dc5d67dd5e66de5f65de6164df6263e06363e16462e26561e26660e3685fe4695ee56a5de56b5de66c5ce76e5be76f5ae87059e97158e97257ea7457eb7556eb7655ec7754ed7953ed7a52ee7b51ef7c51ef7e50f07f4ff0804ef1814df1834cf2844bf3854bf3874af48849f48948f58b47f58c46f68d45f68f44f79044f79143f79342f89441f89540f9973ff9983ef99a3efa9b3dfa9c3cfa9e3bfb9f3afba139fba238fca338fca537fca636fca835fca934fdab33fdac33fdae32fdaf31fdb130fdb22ffdb42ffdb52efeb72dfeb82cfeba2cfebb2bfebd2afebe2afec029fdc229fdc328fdc527fdc627fdc827fdca26fdcb26fccd25fcce25fcd025fcd225fbd324fbd524fbd724fad824fada24f9dc24f9dd25f8df25f8e125f7e225f7e425f6e626f6e826f5e926f5eb27f4ed27f3ee27f3f027f2f227f1f426f1f525f0f724f0f921")),cm="http://www.w3.org/1999/xhtml",sm={
+svg:"http://www.w3.org/2000/svg",xhtml:cm,xlink:"http://www.w3.org/1999/xlink",xml:"http://www.w3.org/XML/1998/namespace",xmlns:"http://www.w3.org/2000/xmlns/"},fm=0;iu.prototype=ru.prototype={constructor:iu,get:function(t){for(var n=this._;!(n in t);)if(!(t=t.parentNode))return;return t[n]},set:function(t,n){return t[this._]=n},remove:function(t){return this._ in t&&delete t[this._]},toString:function(){return this._}};var lm=function(t){return function(){return this.matches(t)}};if("undefined"!=typeof document){var hm=document.documentElement;if(!hm.matches){var pm=hm.webkitMatchesSelector||hm.msMatchesSelector||hm.mozMatchesSelector||hm.oMatchesSelector;lm=function(t){return function(){return pm.call(this,t)}}}}var dm=lm,vm={};if(t.event=null,"undefined"!=typeof document){var _m=document.documentElement;"onmouseenter"in _m||(vm={mouseenter:"mouseover",mouseleave:"mouseout"})}Tu.prototype={constructor:Tu,appendChild:function(t){return this._parent.insertBefore(t,this._next)},insertBefore:function(t,n){return this._parent.insertBefore(t,n)},querySelector:function(t){return this._parent.querySelector(t)},querySelectorAll:function(t){return this._parent.querySelectorAll(t)}};var ym="$";ia.prototype={add:function(t){var n=this._names.indexOf(t);n<0&&(this._names.push(t),this._node.setAttribute("class",this._names.join(" ")))},remove:function(t){var n=this._names.indexOf(t);n>=0&&(this._names.splice(n,1),this._node.setAttribute("class",this._names.join(" ")))},contains:function(t){return this._names.indexOf(t)>=0}};var gm=[null];qa.prototype=La.prototype={constructor:qa,select:yu,selectAll:xu,filter:bu,data:Au,enter:Mu,exit:Eu,merge:Cu,order:zu,sort:Pu,call:Lu,nodes:Ru,node:Uu,size:Du,empty:Ou,each:Fu,attr:Vu,style:Ju,property:na,classed:fa,text:da,html:ga,raise:xa,lower:wa,append:Ma,insert:ka,remove:Sa,datum:Aa,on:fu,dispatch:Pa};var mm=Mr("start","end","interrupt"),xm=[],bm=0,wm=1,Mm=2,Tm=3,km=4,Nm=5,Sm=La.prototype.constructor,Am=0,Em=La.prototype;Uc.prototype=Dc.prototype={constructor:Uc,select:wc,selectAll:Mc,filter:vc,merge:_c,selection:Tc,transition:Rc,call:Em.call,nodes:Em.nodes,node:Em.node,size:Em.size,empty:Em.empty,each:Em.each,on:mc,attr:rc,attrTween:uc,style:Ec,styleTween:zc,text:Lc,remove:bc,tween:$a,delay:sc,duration:hc,ease:dc};var Cm={time:null,delay:0,duration:250,ease:et};La.prototype.interrupt=Xa,La.prototype.transition=Ic;var zm=[null],Pm=Array.prototype.slice,qm=1,Lm=2,Rm=3,Um=4,Dm=1e-6;ws.prototype=ys.prototype={constructor:ws,each:us,eachAfter:cs,eachBefore:as,sum:ss,sort:fs,path:ls,ancestors:ps,descendants:ds,leaves:vs,links:_s,copy:gs};var Om="$",Fm={depth:-1},Im={};of.prototype=Object.create(ws.prototype);var Ym=(1+Math.sqrt(5))/2,Bm=function Pw(t){function n(n,e,r,i,o){sf(t,n,e,r,i,o)}return n.ratio=function(t){return Pw((t=+t)>1?t:1)},n}(Ym),jm=function qw(t){function n(n,e,r,i,o){if((u=n._squarify)&&u.ratio===t)for(var u,a,c,s,f,l=-1,h=u.length,p=n.value;++l<h;){for(a=u[l],c=a.children,s=a.value=0,f=c.length;s<f;++s)a.value+=c[s].value;a.dice?Ws(a,e,r,i,r+=(o-r)*a.value/p):cf(a,e,r,e+=(i-e)*a.value/p,o),p-=a.value}else n._squarify=u=sf(t,n,e,r,i,o),u.ratio=t}return n.ratio=function(t){return qw((t=+t)>1?t:1)},n}(Ym),Hm=10,Xm=Math.PI*(3-Math.sqrt(5));Pf.prototype.on=function(){var t=this._.on.apply(this._,arguments);return t===this._?this:t},If.prototype={constructor:If,insert:function(t,n){var e,r,i;if(t){if(n.P=t,n.N=t.N,t.N&&(t.N.P=n),t.N=n,t.R){for(t=t.R;t.L;)t=t.L;t.L=n}else t.R=n;e=t}else this._?(t=Hf(this._),n.P=null,n.N=t,t.P=t.L=n,e=t):(n.P=n.N=null,this._=n,e=null);for(n.L=n.R=null,n.U=e,n.C=!0,t=n;e&&e.C;)r=e.U,e===r.L?(i=r.R,i&&i.C?(e.C=i.C=!1,r.C=!0,t=r):(t===e.R&&(Bf(this,e),t=e,e=t.U),e.C=!1,r.C=!0,jf(this,r))):(i=r.L,i&&i.C?(e.C=i.C=!1,r.C=!0,t=r):(t===e.L&&(jf(this,e),t=e,e=t.U),e.C=!1,r.C=!0,Bf(this,r))),e=t.U;this._.C=!1},remove:function(t){t.N&&(t.N.P=t.P),t.P&&(t.P.N=t.N),t.N=t.P=null;var n,e,r,i=t.U,o=t.L,u=t.R;if(e=o?u?Hf(u):o:u,i?i.L===t?i.L=e:i.R=e:this._=e,o&&u?(r=e.C,e.C=t.C,e.L=o,o.U=e,e!==u?(i=e.U,e.U=t.U,t=e.R,i.L=t,e.R=u,u.U=e):(e.U=i,i=e,t=e.R)):(r=t.C,t=e),t&&(t.U=i),!r){if(t&&t.C)return void(t.C=!1);do{if(t===this._)break;if(t===i.L){if(n=i.R,n.C&&(n.C=!1,i.C=!0,Bf(this,i),n=i.R),n.L&&n.L.C||n.R&&n.R.C){n.R&&n.R.C||(n.L.C=!1,n.C=!0,jf(this,n),n=i.R),n.C=i.C,i.C=n.R.C=!1,Bf(this,i),t=this._;break}}else if(n=i.L,n.C&&(n.C=!1,i.C=!0,jf(this,i),n=i.L),n.L&&n.L.C||n.R&&n.R.C){n.L&&n.L.C||(n.R.C=!1,n.C=!0,Bf(this,n),n=i.L),n.C=i.C,i.C=n.L.C=!1,jf(this,i),t=this._;break}n.C=!0,t=i,i=i.U}while(!t.C);t&&(t.C=!1)}}};var Vm,Wm,$m,Zm,Gm,Jm=[],Qm=[],Km=1e-6,tx=1e-12;vl.prototype={constructor:vl,polygons:function(){var t=this.edges;return this.cells.map(function(n){var e=n.halfedges.map(function(e){return Kf(n,t[e])});return e.data=n.site.data,e})},triangles:function(){var t=[],n=this.edges;return this.cells.forEach(function(e,r){for(var i,o=e.site,u=e.halfedges,a=-1,c=u.length,s=n[u[c-1]],f=s.left===o?s.right:s.left;++a<c;)i=f,s=n[u[a]],f=s.left===o?s.right:s.left,r<i.index&&r<f.index&&pl(o,i,f)<0&&t.push([o.data,i.data,f.data])}),t},links:function(){return this.edges.filter(function(t){return t.right}).map(function(t){return{source:t.left.data,target:t.right.data}})}},ml.prototype={constructor:ml,scale:function(t){return 1===t?this:new ml(this.k*t,this.x,this.y)},translate:function(t,n){return 0===t&0===n?this:new ml(this.k,this.x+this.k*t,this.y+this.k*n)},apply:function(t){return[t[0]*this.k+this.x,t[1]*this.k+this.y]},applyX:function(t){return t*this.k+this.x},applyY:function(t){return t*this.k+this.y},invert:function(t){return[(t[0]-this.x)/this.k,(t[1]-this.y)/this.k]},invertX:function(t){return(t-this.x)/this.k},invertY:function(t){return(t-this.y)/this.k},rescaleX:function(t){return t.copy().domain(t.range().map(this.invertX,this).map(t.invert,t))},rescaleY:function(t){return t.copy().domain(t.range().map(this.invertY,this).map(t.invert,t))},toString:function(){return"translate("+this.x+","+this.y+") scale("+this.k+")"}};var nx=new ml(1,0,0);xl.prototype=ml.prototype;var ex={name:"drag"},rx={name:"space"},ix={name:"handle"},ox={name:"center"},ux={name:"x",handles:["e","w"].map(zl),input:function(t,n){return t&&[[t[0],n[0][1]],[t[1],n[1][1]]]},output:function(t){return t&&[t[0][0],t[1][0]]}},ax={name:"y",handles:["n","s"].map(zl),input:function(t,n){return t&&[[n[0][0],t[0]],[n[1][0],t[1]]]},output:function(t){return t&&[t[0][1],t[1][1]]}},cx={name:"xy",handles:["n","e","s","w","nw","ne","se","sw"].map(zl),input:function(t){return t},output:function(t){return t}},sx={overlay:"crosshair",selection:"move",n:"ns-resize",e:"ew-resize",s:"ns-resize",w:"ew-resize",nw:"nwse-resize",ne:"nesw-resize",se:"nwse-resize",sw:"nesw-resize"},fx={e:"w",w:"e",nw:"ne",ne:"nw",se:"sw",sw:"se"},lx={n:"s",s:"n",nw:"sw",ne:"se",se:"ne",sw:"nw"},hx={overlay:1,selection:1,n:null,e:1,s:null,w:-1,nw:-1,ne:1,se:1,sw:-1},px={overlay:1,selection:1,n:-1,e:null,s:1,w:null,nw:-1,ne:-1,se:1,sw:1},dx=Math.cos,vx=Math.sin,_x=Math.PI,yx=_x/2,gx=2*_x,mx=Math.max,xx=Array.prototype.slice;Jl.prototype={constructor:Jl,reset:function(){this.s=this.t=0},add:function(t){Ql(nb,t,this.t),Ql(this,nb.s,this.s),this.s?this.t+=nb.t:this.s=nb.t},valueOf:function(){return this.s}};var bx,wx,Mx,Tx,kx,Nx,Sx,Ax,Ex,Cx,zx,Px,qx,Lx,Rx,Ux,Dx,Ox,Fx,Ix,Yx,Bx,jx,Hx,Xx,Vx,Wx,$x,Zx,Gx,Jx,Qx,Kx,tb,nb=new Jl,eb=1e-6,rb=1e-12,ib=Math.PI,ob=ib/2,ub=ib/4,ab=2*ib,cb=180/ib,sb=ib/180,fb=Math.abs,lb=Math.atan,hb=Math.atan2,pb=Math.cos,db=Math.ceil,vb=Math.exp,_b=Math.log,yb=Math.pow,gb=Math.sin,mb=Math.sign||function(t){return t>0?1:t<0?-1:0},xb=Math.sqrt,bb=Math.tan,wb={Feature:function(t,n){rh(t.geometry,n)},FeatureCollection:function(t,n){for(var e=t.features,r=-1,i=e.length;++r<i;)rh(e[r].geometry,n)}},Mb={Sphere:function(t,n){n.sphere()},Point:function(t,n){t=t.coordinates,n.point(t[0],t[1],t[2])},MultiPoint:function(t,n){for(var e=t.coordinates,r=-1,i=e.length;++r<i;)t=e[r],n.point(t[0],t[1],t[2])},LineString:function(t,n){ih(t.coordinates,n,0)},MultiLineString:function(t,n){for(var e=t.coordinates,r=-1,i=e.length;++r<i;)ih(e[r],n,0)},Polygon:function(t,n){oh(t.coordinates,n)},MultiPolygon:function(t,n){for(var e=t.coordinates,r=-1,i=e.length;++r<i;)oh(e[r],n)},GeometryCollection:function(t,n){for(var e=t.geometries,r=-1,i=e.length;++r<i;)rh(e[r],n)}},Tb={point:eh,lineStart:eh,lineEnd:eh,polygonStart:function(){bx.reset(),Tb.lineStart=ah,Tb.lineEnd=ch},polygonEnd:function(){var t=+bx;wx.add(t<0?ab+t:t),this.lineStart=this.lineEnd=this.point=eh},sphere:function(){wx.add(ab)}},kb={point:mh,lineStart:bh,lineEnd:wh,polygonStart:function(){kb.point=Mh,kb.lineStart=Th,kb.lineEnd=kh,Ux.reset(),Tb.polygonStart()},polygonEnd:function(){Tb.polygonEnd(),kb.point=mh,kb.lineStart=bh,kb.lineEnd=wh,bx<0?(Ax=-(Cx=180),Ex=-(zx=90)):Ux>eb?zx=90:Ux<-eb&&(Ex=-90),Ox[0]=Ax,Ox[1]=Cx}},Nb={sphere:eh,point:Ch,lineStart:Ph,lineEnd:Rh,polygonStart:function(){Nb.lineStart=Uh,Nb.lineEnd=Dh},polygonEnd:function(){Nb.lineStart=Ph,Nb.lineEnd=Rh}};jh.invert=jh;var Sb,Ab,Eb,Cb,zb,Pb,qb,Lb,Rb,Ub,Db,Ob,Fb=1e9,Ib=-Fb,Yb={sphere:eh,point:eh,lineStart:up,lineEnd:eh,polygonStart:eh,polygonEnd:eh},Bb=[null,null],jb={type:"LineString",coordinates:Bb},Hb=Gl(),Xb=Gl(),Vb={point:eh,lineStart:eh,lineEnd:eh,polygonStart:function(){Vb.lineStart=yp,Vb.lineEnd=xp},polygonEnd:function(){Vb.lineStart=Vb.lineEnd=Vb.point=eh,Hb.add(fb(Xb)),Xb.reset()},result:function(){var t=Hb/2;return Hb.reset(),t}},Wb=1/0,$b=Wb,Zb=-Wb,Gb=Zb,Jb={point:bp,lineStart:eh,lineEnd:eh,polygonStart:eh,polygonEnd:eh,result:function(){var t=[[Wb,$b],[Zb,Gb]];return Zb=Gb=-($b=Wb=1/0),t}},Qb=0,Kb=0,tw=0,nw=0,ew=0,rw=0,iw=0,ow=0,uw=0,aw={point:wp,lineStart:Mp,lineEnd:Np,polygonStart:function(){aw.lineStart=Sp,aw.lineEnd=Ap},polygonEnd:function(){aw.point=wp,aw.lineStart=Mp,aw.lineEnd=Np},result:function(){var t=uw?[iw/uw,ow/uw]:rw?[nw/rw,ew/rw]:tw?[Qb/tw,Kb/tw]:[NaN,NaN];return Qb=Kb=tw=nw=ew=rw=iw=ow=uw=0,t}},cw=Gl(),sw=Up(function(){return!0},Fp,Yp,[-ib,-ob]);Xp.prototype={point:function(t,n){this.stream.point(t,n)},sphere:function(){this.stream.sphere()},lineStart:function(){this.stream.lineStart()},lineEnd:function(){this.stream.lineEnd()},polygonStart:function(){this.stream.polygonStart()},polygonEnd:function(){this.stream.polygonEnd()}};var fw=16,lw=pb(30*sb),hw=Hp({point:function(t,n){this.stream.point(t*sb,n*sb)}}),pw=rd(function(t){return xb(2/(1+t))});pw.invert=id(function(t){return 2*th(t/2)});var dw=rd(function(t){return(t=Kl(t))&&t/gb(t)});dw.invert=id(function(t){return t}),ad.invert=function(t,n){return[t,2*lb(vb(n))-ob]},pd.invert=pd,yd.invert=id(lb),md.invert=id(th),bd.invert=id(function(t){return 2+lb(t)}),Md.invert=function(t,n){return[-n,2*lb(vb(t))-ob]},t.version=kd,t.bisect=Sd,t.bisectRight=Sd,t.bisectLeft=Ad,t.ascending=n,t.bisector=e,t.descending=i,t.deviation=a,t.extent=c,t.histogram=v,t.thresholdFreedmanDiaconis=y,t.thresholdScott=g,t.thresholdSturges=d,t.max=m,t.mean=x,t.median=b,t.merge=w,t.min=M,t.pairs=T,t.permute=k,t.quantile=_,t.range=l,t.scan=N,t.shuffle=S,t.sum=A,t.ticks=h,t.tickStep=p,t.transpose=E,t.variance=u,t.zip=z,t.entries=j,t.keys=Y,t.values=B,t.map=q,t.set=I,t.nest=L,t.randomUniform=H,t.randomNormal=X,t.randomLogNormal=V,t.randomBates=$,t.randomIrwinHall=W,t.randomExponential=Z,t.easeLinear=G,t.easeQuad=K,t.easeQuadIn=J,t.easeQuadOut=Q,t.easeQuadInOut=K,t.easeCubic=et,t.easeCubicIn=tt,t.easeCubicOut=nt,t.easeCubicInOut=et,t.easePoly=Id,t.easePolyIn=Od,t.easePolyOut=Fd,t.easePolyInOut=Id,t.easeSin=ot,t.easeSinIn=rt,t.easeSinOut=it,t.easeSinInOut=ot,t.easeExp=ct,t.easeExpIn=ut,t.easeExpOut=at,t.easeExpInOut=ct,t.easeCircle=lt,t.easeCircleIn=st,t.easeCircleOut=ft,t.easeCircleInOut=lt,t.easeBounce=pt,t.easeBounceIn=ht,t.easeBounceOut=pt,t.easeBounceInOut=dt,t.easeBack=ev,t.easeBackIn=tv,t.easeBackOut=nv,t.easeBackInOut=ev,t.easeElastic=av,t.easeElasticIn=uv,t.easeElasticOut=av,t.easeElasticInOut=cv,t.polygonArea=vt,t.polygonCentroid=_t,t.polygonHull=xt,t.polygonContains=bt,t.polygonLength=wt,t.path=Tt,t.quadtree=jt,t.queue=Qt,t.arc=sn,t.area=vn,t.line=dn,t.pie=gn,t.radialArea=Mn,t.radialLine=wn,t.symbol=Tn,t.symbols=Fv,t.symbolCircle=bv,t.symbolCross=wv,t.symbolDiamond=kv,t.symbolSquare=zv,t.symbolStar=Cv,t.symbolTriangle=qv,t.symbolWye=Ov,t.curveBasisClosed=Cn,t.curveBasisOpen=Pn,t.curveBasis=An,t.curveBundle=Iv,t.curveCardinalClosed=Bv,t.curveCardinalOpen=jv,t.curveCardinal=Yv,t.curveCatmullRomClosed=Xv,t.curveCatmullRomOpen=Vv,t.curveCatmullRom=Hv,t.curveLinearClosed=jn,t.curveLinear=ln,t.curveMonotoneX=Jn,t.curveMonotoneY=Qn,t.curveNatural=ne,t.curveStep=re,t.curveStepAfter=oe,t.curveStepBefore=ie,t.stack=se,t.stackOffsetExpand=fe,t.stackOffsetNone=ue,t.stackOffsetSilhouette=le,t.stackOffsetWiggle=he,t.stackOrderAscending=pe,t.stackOrderDescending=ve,t.stackOrderInsideOut=_e,t.stackOrderNone=ae,t.stackOrderReverse=ye,t.color=be,t.rgb=ke,t.hsl=Ee,t.lab=qe,t.hcl=Ie,t.cubehelix=je,t.interpolate=cr,t.interpolateArray=nr,t.interpolateDate=er,t.interpolateNumber=rr,t.interpolateObject=ir,t.interpolateRound=sr,t.interpolateString=ar,t.interpolateTransformCss=L_,t.interpolateTransformSvg=R_,t.interpolateZoom=yr,t.interpolateRgb=S_,t.interpolateRgbBasis=A_,t.interpolateRgbBasisClosed=E_,t.interpolateHsl=I_,t.interpolateHslLong=Y_,t.interpolateLab=mr,t.interpolateHcl=B_,t.interpolateHclLong=j_,t.interpolateCubehelix=H_,t.interpolateCubehelixLong=X_,t.interpolateBasis=Ve,t.interpolateBasisClosed=We,t.quantize=wr,t.dispatch=Mr,t.dsvFormat=zr,t.csvParse=G_,t.csvParseRows=J_,t.csvFormat=Q_,t.csvFormatRows=K_,t.tsvParse=ny,t.tsvParseRows=ey,t.tsvFormat=ry,t.tsvFormatRows=iy,t.request=Pr,t.html=oy,t.json=uy,t.text=ay,t.xml=cy,t.csv=sy,t.tsv=fy,t.now=Or,t.timer=Yr,t.timerFlush=Br,t.timeout=Wr,t.interval=$r,t.timeInterval=Zr,t.timeMillisecond=wy,t.timeMilliseconds=My,t.timeSecond=Ey,t.timeSeconds=Cy,t.timeMinute=zy,t.timeMinutes=Py,t.timeHour=qy,t.timeHours=Ly,t.timeDay=Ry,t.timeDays=Uy,t.timeWeek=Dy,t.timeWeeks=Hy,t.timeSunday=Dy,t.timeSundays=Hy,t.timeMonday=Oy,t.timeMondays=Xy,t.timeTuesday=Fy,t.timeTuesdays=Vy,t.timeWednesday=Iy,t.timeWednesdays=Wy,t.timeThursday=Yy,t.timeThursdays=$y,t.timeFriday=By,t.timeFridays=Zy,t.timeSaturday=jy,t.timeSaturdays=Gy,t.timeMonth=Jy,t.timeMonths=Qy,t.timeYear=Ky,t.timeYears=tg,t.utcMillisecond=wy,t.utcMilliseconds=My,t.utcSecond=Ey,t.utcSeconds=Cy,t.utcMinute=ng,t.utcMinutes=eg,t.utcHour=rg,t.utcHours=ig,t.utcDay=og,t.utcDays=ug,t.utcWeek=ag,t.utcWeeks=dg,t.utcSunday=ag,t.utcSundays=dg,t.utcMonday=cg,t.utcMondays=vg,t.utcTuesday=sg,t.utcTuesdays=_g,t.utcWednesday=fg,t.utcWednesdays=yg,t.utcThursday=lg,t.utcThursdays=gg,t.utcFriday=hg,t.utcFridays=mg,t.utcSaturday=pg,t.utcSaturdays=xg,t.utcMonth=bg,t.utcMonths=wg,t.utcYear=Mg,t.utcYears=kg,t.formatLocale=ai,t.formatDefaultLocale=ci,t.formatSpecifier=ii,t.precisionFixed=si,t.precisionPrefix=fi,t.precisionRound=li,t.isoFormat=Ug,t.isoParse=Dg,t.timeFormatLocale=vi,t.timeFormatDefaultLocale=so,t.scaleBand=po,t.scalePoint=_o,t.scaleIdentity=Eo,t.scaleLinear=Ao,t.scaleLog=Do,t.scaleOrdinal=ho,t.scaleImplicit=Yg,t.scalePow=Fo,t.scaleSqrt=Io,t.scaleQuantile=Yo,t.scaleQuantize=Bo,t.scaleThreshold=jo,t.scaleTime=Wo,t.scaleUtc=$o,t.schemeCategory10=Gg,t.schemeCategory20b=Jg,t.schemeCategory20c=Qg,t.schemeCategory20=Kg,t.scaleSequential=Qo,t.interpolateCubehelixDefault=tm,t.interpolateRainbow=Go,t.interpolateWarm=nm,t.interpolateCool=em,t.interpolateViridis=im,t.interpolateMagma=om,t.interpolateInferno=um,t.interpolatePlasma=am,t.creator=eu,t.customEvent=lu,t.local=ru,t.matcher=dm,t.mouse=du,t.namespace=Ko,t.namespaces=sm,t.select=Ra,t.selectAll=Ua,t.selection=La,t.selector=_u,t.selectorAll=mu,t.touch=Da,t.touches=Oa,t.window=Wu,t.active=Yc,t.interrupt=Ha,t.transition=Dc,t.axisTop=$c,t.axisRight=Zc,t.axisBottom=Gc,t.axisLeft=Jc,t.cluster=os,t.hierarchy=ys,t.pack=Bs,t.packSiblings=Us,t.packEnclose=ks,t.partition=$s,t.stratify=Js,t.tree=af,t.treemap=ff,t.treemapBinary=lf,t.treemapDice=Ws,t.treemapSlice=cf,t.treemapSliceDice=hf,t.treemapSquarify=Bm,t.treemapResquarify=jm,t.forceCenter=pf,t.forceCollide=gf,t.forceLink=xf,t.forceManyBody=Tf,t.forceSimulation=Mf,t.forceX=kf,t.forceY=Nf,t.drag=Uf,t.dragDisable=Ef,t.dragEnable=Cf,t.voronoi=_l,t.zoom=Nl,t.zoomIdentity=nx,t.zoomTransform=xl,t.brush=Fl,t.brushX=Dl,t.brushY=Ol,t.brushSelection=Ul,t.chord=Bl,t.ribbon=Zl,t.geoAlbers=td,t.geoAlbersUsa=ed,t.geoArea=lh,t.geoAzimuthalEqualArea=od,t.geoAzimuthalEqualAreaRaw=pw,t.geoAzimuthalEquidistant=ud,t.geoAzimuthalEquidistantRaw=dw,t.geoBounds=Eh,t.geoCentroid=Ih,t.geoCircle=Jh,t.geoClipExtent=op,t.geoConicConformal=hd,t.geoConicConformalRaw=ld,t.geoConicEqualArea=Kp,t.geoConicEqualAreaRaw=Qp,t.geoConicEquidistant=_d,t.geoConicEquidistantRaw=vd,t.geoDistance=lp,t.geoEquirectangular=dd,t.geoEquirectangularRaw=pd,t.geoGnomonic=gd,t.geoGnomonicRaw=yd,t.geoGraticule=dp,t.geoInterpolate=vp,t.geoLength=fp,t.geoMercator=cd,t.geoMercatorRaw=ad,t.geoOrthographic=xd,t.geoOrthographicRaw=md,t.geoPath=Lp,t.geoProjection=Zp,t.geoProjectionMutator=Gp,t.geoRotation=$h,t.geoStereographic=wd,t.geoStereographicRaw=bd,t.geoStream=uh,t.geoTransform=jp,t.geoTransverseMercator=Td,t.geoTransverseMercatorRaw=Md,Object.defineProperty(t,"__esModule",{value:!0})});
diff --git a/themes/bootstrap3/js/vendor/d3_LICENSE b/themes/bootstrap3/js/vendor/d3_LICENSE
new file mode 100644
index 0000000000000000000000000000000000000000..721bd22ece6587a9408eda1b6a3949c425b5624a
--- /dev/null
+++ b/themes/bootstrap3/js/vendor/d3_LICENSE
@@ -0,0 +1,27 @@
+Copyright 2010-2016 Mike Bostock
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice, this
+  list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright notice,
+  this list of conditions and the following disclaimer in the documentation
+  and/or other materials provided with the distribution.
+
+* Neither the name of the author nor the names of 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.
diff --git a/themes/bootstrap3/js/vendor/d3_license.txt b/themes/bootstrap3/js/vendor/d3_license.txt
deleted file mode 100644
index ef77418668b6b7b5d31f22a79dee139c50fdabfb..0000000000000000000000000000000000000000
--- a/themes/bootstrap3/js/vendor/d3_license.txt
+++ /dev/null
@@ -1,27 +0,0 @@
-Copyright (c) 2010-2014, Michael Bostock
-All rights reserved.
-
-Redistribution and use in source and binary forms, with or without
-modification, are permitted provided that the following conditions are met:
-
-* Redistributions of source code must retain the above copyright notice, this
-  list of conditions and the following disclaimer.
-
-* Redistributions in binary form must reproduce the above copyright notice,
-  this list of conditions and the following disclaimer in the documentation
-  and/or other materials provided with the distribution.
-
-* The name Michael Bostock may not 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 MICHAEL BOSTOCK 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.
-
diff --git a/themes/bootstrap3/js/vendor/flot/excanvas.min.js b/themes/bootstrap3/js/vendor/flot/excanvas.min.js
index 12c74f7bea844f60953021cadc8468297712f0df..fcf876c749ed323a9b08e5e46d8cdf7f021e14aa 100644
--- a/themes/bootstrap3/js/vendor/flot/excanvas.min.js
+++ b/themes/bootstrap3/js/vendor/flot/excanvas.min.js
@@ -1 +1 @@
-if(!document.createElement("canvas").getContext){(function(){var z=Math;var K=z.round;var J=z.sin;var U=z.cos;var b=z.abs;var k=z.sqrt;var D=10;var F=D/2;function T(){return this.context_||(this.context_=new W(this))}var O=Array.prototype.slice;function G(i,j,m){var Z=O.call(arguments,2);return function(){return i.apply(j,Z.concat(O.call(arguments)))}}function AD(Z){return String(Z).replace(/&/g,"&amp;").replace(/"/g,"&quot;")}function r(i){if(!i.namespaces.g_vml_){i.namespaces.add("g_vml_","urn:schemas-microsoft-com:vml","#default#VML")}if(!i.namespaces.g_o_){i.namespaces.add("g_o_","urn:schemas-microsoft-com:office:office","#default#VML")}if(!i.styleSheets.ex_canvas_){var Z=i.createStyleSheet();Z.owningElement.id="ex_canvas_";Z.cssText="canvas{display:inline-block;overflow:hidden;text-align:left;width:300px;height:150px}"}}r(document);var E={init:function(Z){if(/MSIE/.test(navigator.userAgent)&&!window.opera){var i=Z||document;i.createElement("canvas");i.attachEvent("onreadystatechange",G(this.init_,this,i))}},init_:function(m){var j=m.getElementsByTagName("canvas");for(var Z=0;Z<j.length;Z++){this.initElement(j[Z])}},initElement:function(i){if(!i.getContext){i.getContext=T;r(i.ownerDocument);i.innerHTML="";i.attachEvent("onpropertychange",S);i.attachEvent("onresize",w);var Z=i.attributes;if(Z.width&&Z.width.specified){i.style.width=Z.width.nodeValue+"px"}else{i.width=i.clientWidth}if(Z.height&&Z.height.specified){i.style.height=Z.height.nodeValue+"px"}else{i.height=i.clientHeight}}return i}};function S(i){var Z=i.srcElement;switch(i.propertyName){case"width":Z.getContext().clearRect();Z.style.width=Z.attributes.width.nodeValue+"px";Z.firstChild.style.width=Z.clientWidth+"px";break;case"height":Z.getContext().clearRect();Z.style.height=Z.attributes.height.nodeValue+"px";Z.firstChild.style.height=Z.clientHeight+"px";break}}function w(i){var Z=i.srcElement;if(Z.firstChild){Z.firstChild.style.width=Z.clientWidth+"px";Z.firstChild.style.height=Z.clientHeight+"px"}}E.init();var I=[];for(var AC=0;AC<16;AC++){for(var AB=0;AB<16;AB++){I[AC*16+AB]=AC.toString(16)+AB.toString(16)}}function V(){return[[1,0,0],[0,1,0],[0,0,1]]}function d(m,j){var i=V();for(var Z=0;Z<3;Z++){for(var AF=0;AF<3;AF++){var p=0;for(var AE=0;AE<3;AE++){p+=m[Z][AE]*j[AE][AF]}i[Z][AF]=p}}return i}function Q(i,Z){Z.fillStyle=i.fillStyle;Z.lineCap=i.lineCap;Z.lineJoin=i.lineJoin;Z.lineWidth=i.lineWidth;Z.miterLimit=i.miterLimit;Z.shadowBlur=i.shadowBlur;Z.shadowColor=i.shadowColor;Z.shadowOffsetX=i.shadowOffsetX;Z.shadowOffsetY=i.shadowOffsetY;Z.strokeStyle=i.strokeStyle;Z.globalAlpha=i.globalAlpha;Z.font=i.font;Z.textAlign=i.textAlign;Z.textBaseline=i.textBaseline;Z.arcScaleX_=i.arcScaleX_;Z.arcScaleY_=i.arcScaleY_;Z.lineScale_=i.lineScale_}var B={aliceblue:"#F0F8FF",antiquewhite:"#FAEBD7",aquamarine:"#7FFFD4",azure:"#F0FFFF",beige:"#F5F5DC",bisque:"#FFE4C4",black:"#000000",blanchedalmond:"#FFEBCD",blueviolet:"#8A2BE2",brown:"#A52A2A",burlywood:"#DEB887",cadetblue:"#5F9EA0",chartreuse:"#7FFF00",chocolate:"#D2691E",coral:"#FF7F50",cornflowerblue:"#6495ED",cornsilk:"#FFF8DC",crimson:"#DC143C",cyan:"#00FFFF",darkblue:"#00008B",darkcyan:"#008B8B",darkgoldenrod:"#B8860B",darkgray:"#A9A9A9",darkgreen:"#006400",darkgrey:"#A9A9A9",darkkhaki:"#BDB76B",darkmagenta:"#8B008B",darkolivegreen:"#556B2F",darkorange:"#FF8C00",darkorchid:"#9932CC",darkred:"#8B0000",darksalmon:"#E9967A",darkseagreen:"#8FBC8F",darkslateblue:"#483D8B",darkslategray:"#2F4F4F",darkslategrey:"#2F4F4F",darkturquoise:"#00CED1",darkviolet:"#9400D3",deeppink:"#FF1493",deepskyblue:"#00BFFF",dimgray:"#696969",dimgrey:"#696969",dodgerblue:"#1E90FF",firebrick:"#B22222",floralwhite:"#FFFAF0",forestgreen:"#228B22",gainsboro:"#DCDCDC",ghostwhite:"#F8F8FF",gold:"#FFD700",goldenrod:"#DAA520",grey:"#808080",greenyellow:"#ADFF2F",honeydew:"#F0FFF0",hotpink:"#FF69B4",indianred:"#CD5C5C",indigo:"#4B0082",ivory:"#FFFFF0",khaki:"#F0E68C",lavender:"#E6E6FA",lavenderblush:"#FFF0F5",lawngreen:"#7CFC00",lemonchiffon:"#FFFACD",lightblue:"#ADD8E6",lightcoral:"#F08080",lightcyan:"#E0FFFF",lightgoldenrodyellow:"#FAFAD2",lightgreen:"#90EE90",lightgrey:"#D3D3D3",lightpink:"#FFB6C1",lightsalmon:"#FFA07A",lightseagreen:"#20B2AA",lightskyblue:"#87CEFA",lightslategray:"#778899",lightslategrey:"#778899",lightsteelblue:"#B0C4DE",lightyellow:"#FFFFE0",limegreen:"#32CD32",linen:"#FAF0E6",magenta:"#FF00FF",mediumaquamarine:"#66CDAA",mediumblue:"#0000CD",mediumorchid:"#BA55D3",mediumpurple:"#9370DB",mediumseagreen:"#3CB371",mediumslateblue:"#7B68EE",mediumspringgreen:"#00FA9A",mediumturquoise:"#48D1CC",mediumvioletred:"#C71585",midnightblue:"#191970",mintcream:"#F5FFFA",mistyrose:"#FFE4E1",moccasin:"#FFE4B5",navajowhite:"#FFDEAD",oldlace:"#FDF5E6",olivedrab:"#6B8E23",orange:"#FFA500",orangered:"#FF4500",orchid:"#DA70D6",palegoldenrod:"#EEE8AA",palegreen:"#98FB98",paleturquoise:"#AFEEEE",palevioletred:"#DB7093",papayawhip:"#FFEFD5",peachpuff:"#FFDAB9",peru:"#CD853F",pink:"#FFC0CB",plum:"#DDA0DD",powderblue:"#B0E0E6",rosybrown:"#BC8F8F",royalblue:"#4169E1",saddlebrown:"#8B4513",salmon:"#FA8072",sandybrown:"#F4A460",seagreen:"#2E8B57",seashell:"#FFF5EE",sienna:"#A0522D",skyblue:"#87CEEB",slateblue:"#6A5ACD",slategray:"#708090",slategrey:"#708090",snow:"#FFFAFA",springgreen:"#00FF7F",steelblue:"#4682B4",tan:"#D2B48C",thistle:"#D8BFD8",tomato:"#FF6347",turquoise:"#40E0D0",violet:"#EE82EE",wheat:"#F5DEB3",whitesmoke:"#F5F5F5",yellowgreen:"#9ACD32"};function g(i){var m=i.indexOf("(",3);var Z=i.indexOf(")",m+1);var j=i.substring(m+1,Z).split(",");if(j.length==4&&i.substr(3,1)=="a"){alpha=Number(j[3])}else{j[3]=1}return j}function C(Z){return parseFloat(Z)/100}function N(i,j,Z){return Math.min(Z,Math.max(j,i))}function c(AF){var j,i,Z;h=parseFloat(AF[0])/360%360;if(h<0){h++}s=N(C(AF[1]),0,1);l=N(C(AF[2]),0,1);if(s==0){j=i=Z=l}else{var m=l<0.5?l*(1+s):l+s-l*s;var AE=2*l-m;j=A(AE,m,h+1/3);i=A(AE,m,h);Z=A(AE,m,h-1/3)}return"#"+I[Math.floor(j*255)]+I[Math.floor(i*255)]+I[Math.floor(Z*255)]}function A(i,Z,j){if(j<0){j++}if(j>1){j--}if(6*j<1){return i+(Z-i)*6*j}else{if(2*j<1){return Z}else{if(3*j<2){return i+(Z-i)*(2/3-j)*6}else{return i}}}}function Y(Z){var AE,p=1;Z=String(Z);if(Z.charAt(0)=="#"){AE=Z}else{if(/^rgb/.test(Z)){var m=g(Z);var AE="#",AF;for(var j=0;j<3;j++){if(m[j].indexOf("%")!=-1){AF=Math.floor(C(m[j])*255)}else{AF=Number(m[j])}AE+=I[N(AF,0,255)]}p=m[3]}else{if(/^hsl/.test(Z)){var m=g(Z);AE=c(m);p=m[3]}else{AE=B[Z]||Z}}}return{color:AE,alpha:p}}var L={style:"normal",variant:"normal",weight:"normal",size:10,family:"sans-serif"};var f={};function X(Z){if(f[Z]){return f[Z]}var m=document.createElement("div");var j=m.style;try{j.font=Z}catch(i){}return f[Z]={style:j.fontStyle||L.style,variant:j.fontVariant||L.variant,weight:j.fontWeight||L.weight,size:j.fontSize||L.size,family:j.fontFamily||L.family}}function P(j,i){var Z={};for(var AF in j){Z[AF]=j[AF]}var AE=parseFloat(i.currentStyle.fontSize),m=parseFloat(j.size);if(typeof j.size=="number"){Z.size=j.size}else{if(j.size.indexOf("px")!=-1){Z.size=m}else{if(j.size.indexOf("em")!=-1){Z.size=AE*m}else{if(j.size.indexOf("%")!=-1){Z.size=(AE/100)*m}else{if(j.size.indexOf("pt")!=-1){Z.size=m/0.75}else{Z.size=AE}}}}}Z.size*=0.981;return Z}function AA(Z){return Z.style+" "+Z.variant+" "+Z.weight+" "+Z.size+"px "+Z.family}function t(Z){switch(Z){case"butt":return"flat";case"round":return"round";case"square":default:return"square"}}function W(i){this.m_=V();this.mStack_=[];this.aStack_=[];this.currentPath_=[];this.strokeStyle="#000";this.fillStyle="#000";this.lineWidth=1;this.lineJoin="miter";this.lineCap="butt";this.miterLimit=D*1;this.globalAlpha=1;this.font="10px sans-serif";this.textAlign="left";this.textBaseline="alphabetic";this.canvas=i;var Z=i.ownerDocument.createElement("div");Z.style.width=i.clientWidth+"px";Z.style.height=i.clientHeight+"px";Z.style.overflow="hidden";Z.style.position="absolute";i.appendChild(Z);this.element_=Z;this.arcScaleX_=1;this.arcScaleY_=1;this.lineScale_=1}var M=W.prototype;M.clearRect=function(){if(this.textMeasureEl_){this.textMeasureEl_.removeNode(true);this.textMeasureEl_=null}this.element_.innerHTML=""};M.beginPath=function(){this.currentPath_=[]};M.moveTo=function(i,Z){var j=this.getCoords_(i,Z);this.currentPath_.push({type:"moveTo",x:j.x,y:j.y});this.currentX_=j.x;this.currentY_=j.y};M.lineTo=function(i,Z){var j=this.getCoords_(i,Z);this.currentPath_.push({type:"lineTo",x:j.x,y:j.y});this.currentX_=j.x;this.currentY_=j.y};M.bezierCurveTo=function(j,i,AI,AH,AG,AE){var Z=this.getCoords_(AG,AE);var AF=this.getCoords_(j,i);var m=this.getCoords_(AI,AH);e(this,AF,m,Z)};function e(Z,m,j,i){Z.currentPath_.push({type:"bezierCurveTo",cp1x:m.x,cp1y:m.y,cp2x:j.x,cp2y:j.y,x:i.x,y:i.y});Z.currentX_=i.x;Z.currentY_=i.y}M.quadraticCurveTo=function(AG,j,i,Z){var AF=this.getCoords_(AG,j);var AE=this.getCoords_(i,Z);var AH={x:this.currentX_+2/3*(AF.x-this.currentX_),y:this.currentY_+2/3*(AF.y-this.currentY_)};var m={x:AH.x+(AE.x-this.currentX_)/3,y:AH.y+(AE.y-this.currentY_)/3};e(this,AH,m,AE)};M.arc=function(AJ,AH,AI,AE,i,j){AI*=D;var AN=j?"at":"wa";var AK=AJ+U(AE)*AI-F;var AM=AH+J(AE)*AI-F;var Z=AJ+U(i)*AI-F;var AL=AH+J(i)*AI-F;if(AK==Z&&!j){AK+=0.125}var m=this.getCoords_(AJ,AH);var AG=this.getCoords_(AK,AM);var AF=this.getCoords_(Z,AL);this.currentPath_.push({type:AN,x:m.x,y:m.y,radius:AI,xStart:AG.x,yStart:AG.y,xEnd:AF.x,yEnd:AF.y})};M.rect=function(j,i,Z,m){this.moveTo(j,i);this.lineTo(j+Z,i);this.lineTo(j+Z,i+m);this.lineTo(j,i+m);this.closePath()};M.strokeRect=function(j,i,Z,m){var p=this.currentPath_;this.beginPath();this.moveTo(j,i);this.lineTo(j+Z,i);this.lineTo(j+Z,i+m);this.lineTo(j,i+m);this.closePath();this.stroke();this.currentPath_=p};M.fillRect=function(j,i,Z,m){var p=this.currentPath_;this.beginPath();this.moveTo(j,i);this.lineTo(j+Z,i);this.lineTo(j+Z,i+m);this.lineTo(j,i+m);this.closePath();this.fill();this.currentPath_=p};M.createLinearGradient=function(i,m,Z,j){var p=new v("gradient");p.x0_=i;p.y0_=m;p.x1_=Z;p.y1_=j;return p};M.createRadialGradient=function(m,AE,j,i,p,Z){var AF=new v("gradientradial");AF.x0_=m;AF.y0_=AE;AF.r0_=j;AF.x1_=i;AF.y1_=p;AF.r1_=Z;return AF};M.drawImage=function(AO,j){var AH,AF,AJ,AV,AM,AK,AQ,AX;var AI=AO.runtimeStyle.width;var AN=AO.runtimeStyle.height;AO.runtimeStyle.width="auto";AO.runtimeStyle.height="auto";var AG=AO.width;var AT=AO.height;AO.runtimeStyle.width=AI;AO.runtimeStyle.height=AN;if(arguments.length==3){AH=arguments[1];AF=arguments[2];AM=AK=0;AQ=AJ=AG;AX=AV=AT}else{if(arguments.length==5){AH=arguments[1];AF=arguments[2];AJ=arguments[3];AV=arguments[4];AM=AK=0;AQ=AG;AX=AT}else{if(arguments.length==9){AM=arguments[1];AK=arguments[2];AQ=arguments[3];AX=arguments[4];AH=arguments[5];AF=arguments[6];AJ=arguments[7];AV=arguments[8]}else{throw Error("Invalid number of arguments")}}}var AW=this.getCoords_(AH,AF);var m=AQ/2;var i=AX/2;var AU=[];var Z=10;var AE=10;AU.push(" <g_vml_:group",' coordsize="',D*Z,",",D*AE,'"',' coordorigin="0,0"',' style="width:',Z,"px;height:",AE,"px;position:absolute;");if(this.m_[0][0]!=1||this.m_[0][1]||this.m_[1][1]!=1||this.m_[1][0]){var p=[];p.push("M11=",this.m_[0][0],",","M12=",this.m_[1][0],",","M21=",this.m_[0][1],",","M22=",this.m_[1][1],",","Dx=",K(AW.x/D),",","Dy=",K(AW.y/D),"");var AS=AW;var AR=this.getCoords_(AH+AJ,AF);var AP=this.getCoords_(AH,AF+AV);var AL=this.getCoords_(AH+AJ,AF+AV);AS.x=z.max(AS.x,AR.x,AP.x,AL.x);AS.y=z.max(AS.y,AR.y,AP.y,AL.y);AU.push("padding:0 ",K(AS.x/D),"px ",K(AS.y/D),"px 0;filter:progid:DXImageTransform.Microsoft.Matrix(",p.join(""),", sizingmethod='clip');")}else{AU.push("top:",K(AW.y/D),"px;left:",K(AW.x/D),"px;")}AU.push(' ">','<g_vml_:image src="',AO.src,'"',' style="width:',D*AJ,"px;"," height:",D*AV,'px"',' cropleft="',AM/AG,'"',' croptop="',AK/AT,'"',' cropright="',(AG-AM-AQ)/AG,'"',' cropbottom="',(AT-AK-AX)/AT,'"'," />","</g_vml_:group>");this.element_.insertAdjacentHTML("BeforeEnd",AU.join(""))};M.stroke=function(AM){var m=10;var AN=10;var AE=5000;var AG={x:null,y:null};var AL={x:null,y:null};for(var AH=0;AH<this.currentPath_.length;AH+=AE){var AK=[];var AF=false;AK.push("<g_vml_:shape",' filled="',!!AM,'"',' style="position:absolute;width:',m,"px;height:",AN,'px;"',' coordorigin="0,0"',' coordsize="',D*m,",",D*AN,'"',' stroked="',!AM,'"',' path="');var AO=false;for(var AI=AH;AI<Math.min(AH+AE,this.currentPath_.length);AI++){if(AI%AE==0&&AI>0){AK.push(" m ",K(this.currentPath_[AI-1].x),",",K(this.currentPath_[AI-1].y))}var Z=this.currentPath_[AI];var AJ;switch(Z.type){case"moveTo":AJ=Z;AK.push(" m ",K(Z.x),",",K(Z.y));break;case"lineTo":AK.push(" l ",K(Z.x),",",K(Z.y));break;case"close":AK.push(" x ");Z=null;break;case"bezierCurveTo":AK.push(" c ",K(Z.cp1x),",",K(Z.cp1y),",",K(Z.cp2x),",",K(Z.cp2y),",",K(Z.x),",",K(Z.y));break;case"at":case"wa":AK.push(" ",Z.type," ",K(Z.x-this.arcScaleX_*Z.radius),",",K(Z.y-this.arcScaleY_*Z.radius)," ",K(Z.x+this.arcScaleX_*Z.radius),",",K(Z.y+this.arcScaleY_*Z.radius)," ",K(Z.xStart),",",K(Z.yStart)," ",K(Z.xEnd),",",K(Z.yEnd));break}if(Z){if(AG.x==null||Z.x<AG.x){AG.x=Z.x}if(AL.x==null||Z.x>AL.x){AL.x=Z.x}if(AG.y==null||Z.y<AG.y){AG.y=Z.y}if(AL.y==null||Z.y>AL.y){AL.y=Z.y}}}AK.push(' ">');if(!AM){R(this,AK)}else{a(this,AK,AG,AL)}AK.push("</g_vml_:shape>");this.element_.insertAdjacentHTML("beforeEnd",AK.join(""))}};function R(j,AE){var i=Y(j.strokeStyle);var m=i.color;var p=i.alpha*j.globalAlpha;var Z=j.lineScale_*j.lineWidth;if(Z<1){p*=Z}AE.push("<g_vml_:stroke",' opacity="',p,'"',' joinstyle="',j.lineJoin,'"',' miterlimit="',j.miterLimit,'"',' endcap="',t(j.lineCap),'"',' weight="',Z,'px"',' color="',m,'" />')}function a(AO,AG,Ah,AP){var AH=AO.fillStyle;var AY=AO.arcScaleX_;var AX=AO.arcScaleY_;var Z=AP.x-Ah.x;var m=AP.y-Ah.y;if(AH instanceof v){var AL=0;var Ac={x:0,y:0};var AU=0;var AK=1;if(AH.type_=="gradient"){var AJ=AH.x0_/AY;var j=AH.y0_/AX;var AI=AH.x1_/AY;var Aj=AH.y1_/AX;var Ag=AO.getCoords_(AJ,j);var Af=AO.getCoords_(AI,Aj);var AE=Af.x-Ag.x;var p=Af.y-Ag.y;AL=Math.atan2(AE,p)*180/Math.PI;if(AL<0){AL+=360}if(AL<0.000001){AL=0}}else{var Ag=AO.getCoords_(AH.x0_,AH.y0_);Ac={x:(Ag.x-Ah.x)/Z,y:(Ag.y-Ah.y)/m};Z/=AY*D;m/=AX*D;var Aa=z.max(Z,m);AU=2*AH.r0_/Aa;AK=2*AH.r1_/Aa-AU}var AS=AH.colors_;AS.sort(function(Ak,i){return Ak.offset-i.offset});var AN=AS.length;var AR=AS[0].color;var AQ=AS[AN-1].color;var AW=AS[0].alpha*AO.globalAlpha;var AV=AS[AN-1].alpha*AO.globalAlpha;var Ab=[];for(var Ae=0;Ae<AN;Ae++){var AM=AS[Ae];Ab.push(AM.offset*AK+AU+" "+AM.color)}AG.push('<g_vml_:fill type="',AH.type_,'"',' method="none" focus="100%"',' color="',AR,'"',' color2="',AQ,'"',' colors="',Ab.join(","),'"',' opacity="',AV,'"',' g_o_:opacity2="',AW,'"',' angle="',AL,'"',' focusposition="',Ac.x,",",Ac.y,'" />')}else{if(AH instanceof u){if(Z&&m){var AF=-Ah.x;var AZ=-Ah.y;AG.push("<g_vml_:fill",' position="',AF/Z*AY*AY,",",AZ/m*AX*AX,'"',' type="tile"',' src="',AH.src_,'" />')}}else{var Ai=Y(AO.fillStyle);var AT=Ai.color;var Ad=Ai.alpha*AO.globalAlpha;AG.push('<g_vml_:fill color="',AT,'" opacity="',Ad,'" />')}}}M.fill=function(){this.stroke(true)};M.closePath=function(){this.currentPath_.push({type:"close"})};M.getCoords_=function(j,i){var Z=this.m_;return{x:D*(j*Z[0][0]+i*Z[1][0]+Z[2][0])-F,y:D*(j*Z[0][1]+i*Z[1][1]+Z[2][1])-F}};M.save=function(){var Z={};Q(this,Z);this.aStack_.push(Z);this.mStack_.push(this.m_);this.m_=d(V(),this.m_)};M.restore=function(){if(this.aStack_.length){Q(this.aStack_.pop(),this);this.m_=this.mStack_.pop()}};function H(Z){return isFinite(Z[0][0])&&isFinite(Z[0][1])&&isFinite(Z[1][0])&&isFinite(Z[1][1])&&isFinite(Z[2][0])&&isFinite(Z[2][1])}function y(i,Z,j){if(!H(Z)){return }i.m_=Z;if(j){var p=Z[0][0]*Z[1][1]-Z[0][1]*Z[1][0];i.lineScale_=k(b(p))}}M.translate=function(j,i){var Z=[[1,0,0],[0,1,0],[j,i,1]];y(this,d(Z,this.m_),false)};M.rotate=function(i){var m=U(i);var j=J(i);var Z=[[m,j,0],[-j,m,0],[0,0,1]];y(this,d(Z,this.m_),false)};M.scale=function(j,i){this.arcScaleX_*=j;this.arcScaleY_*=i;var Z=[[j,0,0],[0,i,0],[0,0,1]];y(this,d(Z,this.m_),true)};M.transform=function(p,m,AF,AE,i,Z){var j=[[p,m,0],[AF,AE,0],[i,Z,1]];y(this,d(j,this.m_),true)};M.setTransform=function(AE,p,AG,AF,j,i){var Z=[[AE,p,0],[AG,AF,0],[j,i,1]];y(this,Z,true)};M.drawText_=function(AK,AI,AH,AN,AG){var AM=this.m_,AQ=1000,i=0,AP=AQ,AF={x:0,y:0},AE=[];var Z=P(X(this.font),this.element_);var j=AA(Z);var AR=this.element_.currentStyle;var p=this.textAlign.toLowerCase();switch(p){case"left":case"center":case"right":break;case"end":p=AR.direction=="ltr"?"right":"left";break;case"start":p=AR.direction=="rtl"?"right":"left";break;default:p="left"}switch(this.textBaseline){case"hanging":case"top":AF.y=Z.size/1.75;break;case"middle":break;default:case null:case"alphabetic":case"ideographic":case"bottom":AF.y=-Z.size/2.25;break}switch(p){case"right":i=AQ;AP=0.05;break;case"center":i=AP=AQ/2;break}var AO=this.getCoords_(AI+AF.x,AH+AF.y);AE.push('<g_vml_:line from="',-i,' 0" to="',AP,' 0.05" ',' coordsize="100 100" coordorigin="0 0"',' filled="',!AG,'" stroked="',!!AG,'" style="position:absolute;width:1px;height:1px;">');if(AG){R(this,AE)}else{a(this,AE,{x:-i,y:0},{x:AP,y:Z.size})}var AL=AM[0][0].toFixed(3)+","+AM[1][0].toFixed(3)+","+AM[0][1].toFixed(3)+","+AM[1][1].toFixed(3)+",0,0";var AJ=K(AO.x/D)+","+K(AO.y/D);AE.push('<g_vml_:skew on="t" matrix="',AL,'" ',' offset="',AJ,'" origin="',i,' 0" />','<g_vml_:path textpathok="true" />','<g_vml_:textpath on="true" string="',AD(AK),'" style="v-text-align:',p,";font:",AD(j),'" /></g_vml_:line>');this.element_.insertAdjacentHTML("beforeEnd",AE.join(""))};M.fillText=function(j,Z,m,i){this.drawText_(j,Z,m,i,false)};M.strokeText=function(j,Z,m,i){this.drawText_(j,Z,m,i,true)};M.measureText=function(j){if(!this.textMeasureEl_){var Z='<span style="position:absolute;top:-20000px;left:0;padding:0;margin:0;border:none;white-space:pre;"></span>';this.element_.insertAdjacentHTML("beforeEnd",Z);this.textMeasureEl_=this.element_.lastChild}var i=this.element_.ownerDocument;this.textMeasureEl_.innerHTML="";this.textMeasureEl_.style.font=this.font;this.textMeasureEl_.appendChild(i.createTextNode(j));return{width:this.textMeasureEl_.offsetWidth}};M.clip=function(){};M.arcTo=function(){};M.createPattern=function(i,Z){return new u(i,Z)};function v(Z){this.type_=Z;this.x0_=0;this.y0_=0;this.r0_=0;this.x1_=0;this.y1_=0;this.r1_=0;this.colors_=[]}v.prototype.addColorStop=function(i,Z){Z=Y(Z);this.colors_.push({offset:i,color:Z.color,alpha:Z.alpha})};function u(i,Z){q(i);switch(Z){case"repeat":case null:case"":this.repetition_="repeat";break;case"repeat-x":case"repeat-y":case"no-repeat":this.repetition_=Z;break;default:n("SYNTAX_ERR")}this.src_=i.src;this.width_=i.width;this.height_=i.height}function n(Z){throw new o(Z)}function q(Z){if(!Z||Z.nodeType!=1||Z.tagName!="IMG"){n("TYPE_MISMATCH_ERR")}if(Z.readyState!="complete"){n("INVALID_STATE_ERR")}}function o(Z){this.code=this[Z];this.message=Z+": DOM Exception "+this.code}var x=o.prototype=new Error;x.INDEX_SIZE_ERR=1;x.DOMSTRING_SIZE_ERR=2;x.HIERARCHY_REQUEST_ERR=3;x.WRONG_DOCUMENT_ERR=4;x.INVALID_CHARACTER_ERR=5;x.NO_DATA_ALLOWED_ERR=6;x.NO_MODIFICATION_ALLOWED_ERR=7;x.NOT_FOUND_ERR=8;x.NOT_SUPPORTED_ERR=9;x.INUSE_ATTRIBUTE_ERR=10;x.INVALID_STATE_ERR=11;x.SYNTAX_ERR=12;x.INVALID_MODIFICATION_ERR=13;x.NAMESPACE_ERR=14;x.INVALID_ACCESS_ERR=15;x.VALIDATION_ERR=16;x.TYPE_MISMATCH_ERR=17;G_vmlCanvasManager=E;CanvasRenderingContext2D=W;CanvasGradient=v;CanvasPattern=u;DOMException=o})()};
\ No newline at end of file
+if(!document.createElement("canvas").getContext){(function(){var ab=Math;var n=ab.round;var l=ab.sin;var A=ab.cos;var H=ab.abs;var N=ab.sqrt;var d=10;var f=d/2;var z=+navigator.userAgent.match(/MSIE ([\d.]+)?/)[1];function y(){return this.context_||(this.context_=new D(this))}var t=Array.prototype.slice;function g(j,m,p){var i=t.call(arguments,2);return function(){return j.apply(m,i.concat(t.call(arguments)))}}function af(i){return String(i).replace(/&/g,"&amp;").replace(/"/g,"&quot;")}function Y(m,j,i){if(!m.namespaces[j]){m.namespaces.add(j,i,"#default#VML")}}function R(j){Y(j,"g_vml_","urn:schemas-microsoft-com:vml");Y(j,"g_o_","urn:schemas-microsoft-com:office:office");if(!j.styleSheets.ex_canvas_){var i=j.createStyleSheet();i.owningElement.id="ex_canvas_";i.cssText="canvas{display:inline-block;overflow:hidden;text-align:left;width:300px;height:150px}"}}R(document);var e={init:function(i){var j=i||document;j.createElement("canvas");j.attachEvent("onreadystatechange",g(this.init_,this,j))},init_:function(p){var m=p.getElementsByTagName("canvas");for(var j=0;j<m.length;j++){this.initElement(m[j])}},initElement:function(j){if(!j.getContext){j.getContext=y;R(j.ownerDocument);j.innerHTML="";j.attachEvent("onpropertychange",x);j.attachEvent("onresize",W);var i=j.attributes;if(i.width&&i.width.specified){j.style.width=i.width.nodeValue+"px"}else{j.width=j.clientWidth}if(i.height&&i.height.specified){j.style.height=i.height.nodeValue+"px"}else{j.height=j.clientHeight}}return j}};function x(j){var i=j.srcElement;switch(j.propertyName){case"width":i.getContext().clearRect();i.style.width=i.attributes.width.nodeValue+"px";i.firstChild.style.width=i.clientWidth+"px";break;case"height":i.getContext().clearRect();i.style.height=i.attributes.height.nodeValue+"px";i.firstChild.style.height=i.clientHeight+"px";break}}function W(j){var i=j.srcElement;if(i.firstChild){i.firstChild.style.width=i.clientWidth+"px";i.firstChild.style.height=i.clientHeight+"px"}}e.init();var k=[];for(var ae=0;ae<16;ae++){for(var ad=0;ad<16;ad++){k[ae*16+ad]=ae.toString(16)+ad.toString(16)}}function B(){return[[1,0,0],[0,1,0],[0,0,1]]}function J(p,m){var j=B();for(var i=0;i<3;i++){for(var ah=0;ah<3;ah++){var Z=0;for(var ag=0;ag<3;ag++){Z+=p[i][ag]*m[ag][ah]}j[i][ah]=Z}}return j}function v(j,i){i.fillStyle=j.fillStyle;i.lineCap=j.lineCap;i.lineJoin=j.lineJoin;i.lineWidth=j.lineWidth;i.miterLimit=j.miterLimit;i.shadowBlur=j.shadowBlur;i.shadowColor=j.shadowColor;i.shadowOffsetX=j.shadowOffsetX;i.shadowOffsetY=j.shadowOffsetY;i.strokeStyle=j.strokeStyle;i.globalAlpha=j.globalAlpha;i.font=j.font;i.textAlign=j.textAlign;i.textBaseline=j.textBaseline;i.arcScaleX_=j.arcScaleX_;i.arcScaleY_=j.arcScaleY_;i.lineScale_=j.lineScale_}var b={aliceblue:"#F0F8FF",antiquewhite:"#FAEBD7",aquamarine:"#7FFFD4",azure:"#F0FFFF",beige:"#F5F5DC",bisque:"#FFE4C4",black:"#000000",blanchedalmond:"#FFEBCD",blueviolet:"#8A2BE2",brown:"#A52A2A",burlywood:"#DEB887",cadetblue:"#5F9EA0",chartreuse:"#7FFF00",chocolate:"#D2691E",coral:"#FF7F50",cornflowerblue:"#6495ED",cornsilk:"#FFF8DC",crimson:"#DC143C",cyan:"#00FFFF",darkblue:"#00008B",darkcyan:"#008B8B",darkgoldenrod:"#B8860B",darkgray:"#A9A9A9",darkgreen:"#006400",darkgrey:"#A9A9A9",darkkhaki:"#BDB76B",darkmagenta:"#8B008B",darkolivegreen:"#556B2F",darkorange:"#FF8C00",darkorchid:"#9932CC",darkred:"#8B0000",darksalmon:"#E9967A",darkseagreen:"#8FBC8F",darkslateblue:"#483D8B",darkslategray:"#2F4F4F",darkslategrey:"#2F4F4F",darkturquoise:"#00CED1",darkviolet:"#9400D3",deeppink:"#FF1493",deepskyblue:"#00BFFF",dimgray:"#696969",dimgrey:"#696969",dodgerblue:"#1E90FF",firebrick:"#B22222",floralwhite:"#FFFAF0",forestgreen:"#228B22",gainsboro:"#DCDCDC",ghostwhite:"#F8F8FF",gold:"#FFD700",goldenrod:"#DAA520",grey:"#808080",greenyellow:"#ADFF2F",honeydew:"#F0FFF0",hotpink:"#FF69B4",indianred:"#CD5C5C",indigo:"#4B0082",ivory:"#FFFFF0",khaki:"#F0E68C",lavender:"#E6E6FA",lavenderblush:"#FFF0F5",lawngreen:"#7CFC00",lemonchiffon:"#FFFACD",lightblue:"#ADD8E6",lightcoral:"#F08080",lightcyan:"#E0FFFF",lightgoldenrodyellow:"#FAFAD2",lightgreen:"#90EE90",lightgrey:"#D3D3D3",lightpink:"#FFB6C1",lightsalmon:"#FFA07A",lightseagreen:"#20B2AA",lightskyblue:"#87CEFA",lightslategray:"#778899",lightslategrey:"#778899",lightsteelblue:"#B0C4DE",lightyellow:"#FFFFE0",limegreen:"#32CD32",linen:"#FAF0E6",magenta:"#FF00FF",mediumaquamarine:"#66CDAA",mediumblue:"#0000CD",mediumorchid:"#BA55D3",mediumpurple:"#9370DB",mediumseagreen:"#3CB371",mediumslateblue:"#7B68EE",mediumspringgreen:"#00FA9A",mediumturquoise:"#48D1CC",mediumvioletred:"#C71585",midnightblue:"#191970",mintcream:"#F5FFFA",mistyrose:"#FFE4E1",moccasin:"#FFE4B5",navajowhite:"#FFDEAD",oldlace:"#FDF5E6",olivedrab:"#6B8E23",orange:"#FFA500",orangered:"#FF4500",orchid:"#DA70D6",palegoldenrod:"#EEE8AA",palegreen:"#98FB98",paleturquoise:"#AFEEEE",palevioletred:"#DB7093",papayawhip:"#FFEFD5",peachpuff:"#FFDAB9",peru:"#CD853F",pink:"#FFC0CB",plum:"#DDA0DD",powderblue:"#B0E0E6",rosybrown:"#BC8F8F",royalblue:"#4169E1",saddlebrown:"#8B4513",salmon:"#FA8072",sandybrown:"#F4A460",seagreen:"#2E8B57",seashell:"#FFF5EE",sienna:"#A0522D",skyblue:"#87CEEB",slateblue:"#6A5ACD",slategray:"#708090",slategrey:"#708090",snow:"#FFFAFA",springgreen:"#00FF7F",steelblue:"#4682B4",tan:"#D2B48C",thistle:"#D8BFD8",tomato:"#FF6347",turquoise:"#40E0D0",violet:"#EE82EE",wheat:"#F5DEB3",whitesmoke:"#F5F5F5",yellowgreen:"#9ACD32"};function M(j){var p=j.indexOf("(",3);var i=j.indexOf(")",p+1);var m=j.substring(p+1,i).split(",");if(m.length!=4||j.charAt(3)!="a"){m[3]=1}return m}function c(i){return parseFloat(i)/100}function r(j,m,i){return Math.min(i,Math.max(m,j))}function I(ag){var i,ai,aj,ah,ak,Z;ah=parseFloat(ag[0])/360%360;if(ah<0){ah++}ak=r(c(ag[1]),0,1);Z=r(c(ag[2]),0,1);if(ak==0){i=ai=aj=Z}else{var j=Z<0.5?Z*(1+ak):Z+ak-Z*ak;var m=2*Z-j;i=a(m,j,ah+1/3);ai=a(m,j,ah);aj=a(m,j,ah-1/3)}return"#"+k[Math.floor(i*255)]+k[Math.floor(ai*255)]+k[Math.floor(aj*255)]}function a(j,i,m){if(m<0){m++}if(m>1){m--}if(6*m<1){return j+(i-j)*6*m}else{if(2*m<1){return i}else{if(3*m<2){return j+(i-j)*(2/3-m)*6}else{return j}}}}var C={};function F(j){if(j in C){return C[j]}var ag,Z=1;j=String(j);if(j.charAt(0)=="#"){ag=j}else{if(/^rgb/.test(j)){var p=M(j);var ag="#",ah;for(var m=0;m<3;m++){if(p[m].indexOf("%")!=-1){ah=Math.floor(c(p[m])*255)}else{ah=+p[m]}ag+=k[r(ah,0,255)]}Z=+p[3]}else{if(/^hsl/.test(j)){var p=M(j);ag=I(p);Z=p[3]}else{ag=b[j]||j}}}return C[j]={color:ag,alpha:Z}}var o={style:"normal",variant:"normal",weight:"normal",size:10,family:"sans-serif"};var L={};function E(i){if(L[i]){return L[i]}var p=document.createElement("div");var m=p.style;try{m.font=i}catch(j){}return L[i]={style:m.fontStyle||o.style,variant:m.fontVariant||o.variant,weight:m.fontWeight||o.weight,size:m.fontSize||o.size,family:m.fontFamily||o.family}}function u(m,j){var i={};for(var ah in m){i[ah]=m[ah]}var ag=parseFloat(j.currentStyle.fontSize),Z=parseFloat(m.size);if(typeof m.size=="number"){i.size=m.size}else{if(m.size.indexOf("px")!=-1){i.size=Z}else{if(m.size.indexOf("em")!=-1){i.size=ag*Z}else{if(m.size.indexOf("%")!=-1){i.size=(ag/100)*Z}else{if(m.size.indexOf("pt")!=-1){i.size=Z/0.75}else{i.size=ag}}}}}i.size*=0.981;return i}function ac(i){return i.style+" "+i.variant+" "+i.weight+" "+i.size+"px "+i.family}var s={butt:"flat",round:"round"};function S(i){return s[i]||"square"}function D(i){this.m_=B();this.mStack_=[];this.aStack_=[];this.currentPath_=[];this.strokeStyle="#000";this.fillStyle="#000";this.lineWidth=1;this.lineJoin="miter";this.lineCap="butt";this.miterLimit=d*1;this.globalAlpha=1;this.font="10px sans-serif";this.textAlign="left";this.textBaseline="alphabetic";this.canvas=i;var m="width:"+i.clientWidth+"px;height:"+i.clientHeight+"px;overflow:hidden;position:absolute";var j=i.ownerDocument.createElement("div");j.style.cssText=m;i.appendChild(j);var p=j.cloneNode(false);p.style.backgroundColor="red";p.style.filter="alpha(opacity=0)";i.appendChild(p);this.element_=j;this.arcScaleX_=1;this.arcScaleY_=1;this.lineScale_=1}var q=D.prototype;q.clearRect=function(){if(this.textMeasureEl_){this.textMeasureEl_.removeNode(true);this.textMeasureEl_=null}this.element_.innerHTML=""};q.beginPath=function(){this.currentPath_=[]};q.moveTo=function(j,i){var m=V(this,j,i);this.currentPath_.push({type:"moveTo",x:m.x,y:m.y});this.currentX_=m.x;this.currentY_=m.y};q.lineTo=function(j,i){var m=V(this,j,i);this.currentPath_.push({type:"lineTo",x:m.x,y:m.y});this.currentX_=m.x;this.currentY_=m.y};q.bezierCurveTo=function(m,j,ak,aj,ai,ag){var i=V(this,ai,ag);var ah=V(this,m,j);var Z=V(this,ak,aj);K(this,ah,Z,i)};function K(i,Z,m,j){i.currentPath_.push({type:"bezierCurveTo",cp1x:Z.x,cp1y:Z.y,cp2x:m.x,cp2y:m.y,x:j.x,y:j.y});i.currentX_=j.x;i.currentY_=j.y}q.quadraticCurveTo=function(ai,m,j,i){var ah=V(this,ai,m);var ag=V(this,j,i);var aj={x:this.currentX_+2/3*(ah.x-this.currentX_),y:this.currentY_+2/3*(ah.y-this.currentY_)};var Z={x:aj.x+(ag.x-this.currentX_)/3,y:aj.y+(ag.y-this.currentY_)/3};K(this,aj,Z,ag)};q.arc=function(al,aj,ak,ag,j,m){ak*=d;var ap=m?"at":"wa";var am=al+A(ag)*ak-f;var ao=aj+l(ag)*ak-f;var i=al+A(j)*ak-f;var an=aj+l(j)*ak-f;if(am==i&&!m){am+=0.125}var Z=V(this,al,aj);var ai=V(this,am,ao);var ah=V(this,i,an);this.currentPath_.push({type:ap,x:Z.x,y:Z.y,radius:ak,xStart:ai.x,yStart:ai.y,xEnd:ah.x,yEnd:ah.y})};q.rect=function(m,j,i,p){this.moveTo(m,j);this.lineTo(m+i,j);this.lineTo(m+i,j+p);this.lineTo(m,j+p);this.closePath()};q.strokeRect=function(m,j,i,p){var Z=this.currentPath_;this.beginPath();this.moveTo(m,j);this.lineTo(m+i,j);this.lineTo(m+i,j+p);this.lineTo(m,j+p);this.closePath();this.stroke();this.currentPath_=Z};q.fillRect=function(m,j,i,p){var Z=this.currentPath_;this.beginPath();this.moveTo(m,j);this.lineTo(m+i,j);this.lineTo(m+i,j+p);this.lineTo(m,j+p);this.closePath();this.fill();this.currentPath_=Z};q.createLinearGradient=function(j,p,i,m){var Z=new U("gradient");Z.x0_=j;Z.y0_=p;Z.x1_=i;Z.y1_=m;return Z};q.createRadialGradient=function(p,ag,m,j,Z,i){var ah=new U("gradientradial");ah.x0_=p;ah.y0_=ag;ah.r0_=m;ah.x1_=j;ah.y1_=Z;ah.r1_=i;return ah};q.drawImage=function(aq,m){var aj,ah,al,ay,ao,am,at,aA;var ak=aq.runtimeStyle.width;var ap=aq.runtimeStyle.height;aq.runtimeStyle.width="auto";aq.runtimeStyle.height="auto";var ai=aq.width;var aw=aq.height;aq.runtimeStyle.width=ak;aq.runtimeStyle.height=ap;if(arguments.length==3){aj=arguments[1];ah=arguments[2];ao=am=0;at=al=ai;aA=ay=aw}else{if(arguments.length==5){aj=arguments[1];ah=arguments[2];al=arguments[3];ay=arguments[4];ao=am=0;at=ai;aA=aw}else{if(arguments.length==9){ao=arguments[1];am=arguments[2];at=arguments[3];aA=arguments[4];aj=arguments[5];ah=arguments[6];al=arguments[7];ay=arguments[8]}else{throw Error("Invalid number of arguments")}}}var az=V(this,aj,ah);var p=at/2;var j=aA/2;var ax=[];var i=10;var ag=10;ax.push(" <g_vml_:group",' coordsize="',d*i,",",d*ag,'"',' coordorigin="0,0"',' style="width:',i,"px;height:",ag,"px;position:absolute;");if(this.m_[0][0]!=1||this.m_[0][1]||this.m_[1][1]!=1||this.m_[1][0]){var Z=[];Z.push("M11=",this.m_[0][0],",","M12=",this.m_[1][0],",","M21=",this.m_[0][1],",","M22=",this.m_[1][1],",","Dx=",n(az.x/d),",","Dy=",n(az.y/d),"");var av=az;var au=V(this,aj+al,ah);var ar=V(this,aj,ah+ay);var an=V(this,aj+al,ah+ay);av.x=ab.max(av.x,au.x,ar.x,an.x);av.y=ab.max(av.y,au.y,ar.y,an.y);ax.push("padding:0 ",n(av.x/d),"px ",n(av.y/d),"px 0;filter:progid:DXImageTransform.Microsoft.Matrix(",Z.join(""),", sizingmethod='clip');")}else{ax.push("top:",n(az.y/d),"px;left:",n(az.x/d),"px;")}ax.push(' ">','<g_vml_:image src="',aq.src,'"',' style="width:',d*al,"px;"," height:",d*ay,'px"',' cropleft="',ao/ai,'"',' croptop="',am/aw,'"',' cropright="',(ai-ao-at)/ai,'"',' cropbottom="',(aw-am-aA)/aw,'"'," />","</g_vml_:group>");this.element_.insertAdjacentHTML("BeforeEnd",ax.join(""))};q.stroke=function(ao){var Z=10;var ap=10;var ag=5000;var ai={x:null,y:null};var an={x:null,y:null};for(var aj=0;aj<this.currentPath_.length;aj+=ag){var am=[];var ah=false;am.push("<g_vml_:shape",' filled="',!!ao,'"',' style="position:absolute;width:',Z,"px;height:",ap,'px;"',' coordorigin="0,0"',' coordsize="',d*Z,",",d*ap,'"',' stroked="',!ao,'"',' path="');var aq=false;for(var ak=aj;ak<Math.min(aj+ag,this.currentPath_.length);ak++){if(ak%ag==0&&ak>0){am.push(" m ",n(this.currentPath_[ak-1].x),",",n(this.currentPath_[ak-1].y))}var m=this.currentPath_[ak];var al;switch(m.type){case"moveTo":al=m;am.push(" m ",n(m.x),",",n(m.y));break;case"lineTo":am.push(" l ",n(m.x),",",n(m.y));break;case"close":am.push(" x ");m=null;break;case"bezierCurveTo":am.push(" c ",n(m.cp1x),",",n(m.cp1y),",",n(m.cp2x),",",n(m.cp2y),",",n(m.x),",",n(m.y));break;case"at":case"wa":am.push(" ",m.type," ",n(m.x-this.arcScaleX_*m.radius),",",n(m.y-this.arcScaleY_*m.radius)," ",n(m.x+this.arcScaleX_*m.radius),",",n(m.y+this.arcScaleY_*m.radius)," ",n(m.xStart),",",n(m.yStart)," ",n(m.xEnd),",",n(m.yEnd));break}if(m){if(ai.x==null||m.x<ai.x){ai.x=m.x}if(an.x==null||m.x>an.x){an.x=m.x}if(ai.y==null||m.y<ai.y){ai.y=m.y}if(an.y==null||m.y>an.y){an.y=m.y}}}am.push(' ">');if(!ao){w(this,am)}else{G(this,am,ai,an)}am.push("</g_vml_:shape>");this.element_.insertAdjacentHTML("beforeEnd",am.join(""))}};function w(m,ag){var j=F(m.strokeStyle);var p=j.color;var Z=j.alpha*m.globalAlpha;var i=m.lineScale_*m.lineWidth;if(i<1){Z*=i}ag.push("<g_vml_:stroke",' opacity="',Z,'"',' joinstyle="',m.lineJoin,'"',' miterlimit="',m.miterLimit,'"',' endcap="',S(m.lineCap),'"',' weight="',i,'px"',' color="',p,'" />')}function G(aq,ai,aK,ar){var aj=aq.fillStyle;var aB=aq.arcScaleX_;var aA=aq.arcScaleY_;var j=ar.x-aK.x;var p=ar.y-aK.y;if(aj instanceof U){var an=0;var aF={x:0,y:0};var ax=0;var am=1;if(aj.type_=="gradient"){var al=aj.x0_/aB;var m=aj.y0_/aA;var ak=aj.x1_/aB;var aM=aj.y1_/aA;var aJ=V(aq,al,m);var aI=V(aq,ak,aM);var ag=aI.x-aJ.x;var Z=aI.y-aJ.y;an=Math.atan2(ag,Z)*180/Math.PI;if(an<0){an+=360}if(an<0.000001){an=0}}else{var aJ=V(aq,aj.x0_,aj.y0_);aF={x:(aJ.x-aK.x)/j,y:(aJ.y-aK.y)/p};j/=aB*d;p/=aA*d;var aD=ab.max(j,p);ax=2*aj.r0_/aD;am=2*aj.r1_/aD-ax}var av=aj.colors_;av.sort(function(aN,i){return aN.offset-i.offset});var ap=av.length;var au=av[0].color;var at=av[ap-1].color;var az=av[0].alpha*aq.globalAlpha;var ay=av[ap-1].alpha*aq.globalAlpha;var aE=[];for(var aH=0;aH<ap;aH++){var ao=av[aH];aE.push(ao.offset*am+ax+" "+ao.color)}ai.push('<g_vml_:fill type="',aj.type_,'"',' method="none" focus="100%"',' color="',au,'"',' color2="',at,'"',' colors="',aE.join(","),'"',' opacity="',ay,'"',' g_o_:opacity2="',az,'"',' angle="',an,'"',' focusposition="',aF.x,",",aF.y,'" />')}else{if(aj instanceof T){if(j&&p){var ah=-aK.x;var aC=-aK.y;ai.push("<g_vml_:fill",' position="',ah/j*aB*aB,",",aC/p*aA*aA,'"',' type="tile"',' src="',aj.src_,'" />')}}else{var aL=F(aq.fillStyle);var aw=aL.color;var aG=aL.alpha*aq.globalAlpha;ai.push('<g_vml_:fill color="',aw,'" opacity="',aG,'" />')}}}q.fill=function(){this.stroke(true)};q.closePath=function(){this.currentPath_.push({type:"close"})};function V(j,Z,p){var i=j.m_;return{x:d*(Z*i[0][0]+p*i[1][0]+i[2][0])-f,y:d*(Z*i[0][1]+p*i[1][1]+i[2][1])-f}}q.save=function(){var i={};v(this,i);this.aStack_.push(i);this.mStack_.push(this.m_);this.m_=J(B(),this.m_)};q.restore=function(){if(this.aStack_.length){v(this.aStack_.pop(),this);this.m_=this.mStack_.pop()}};function h(i){return isFinite(i[0][0])&&isFinite(i[0][1])&&isFinite(i[1][0])&&isFinite(i[1][1])&&isFinite(i[2][0])&&isFinite(i[2][1])}function aa(j,i,p){if(!h(i)){return}j.m_=i;if(p){var Z=i[0][0]*i[1][1]-i[0][1]*i[1][0];j.lineScale_=N(H(Z))}}q.translate=function(m,j){var i=[[1,0,0],[0,1,0],[m,j,1]];aa(this,J(i,this.m_),false)};q.rotate=function(j){var p=A(j);var m=l(j);var i=[[p,m,0],[-m,p,0],[0,0,1]];aa(this,J(i,this.m_),false)};q.scale=function(m,j){this.arcScaleX_*=m;this.arcScaleY_*=j;var i=[[m,0,0],[0,j,0],[0,0,1]];aa(this,J(i,this.m_),true)};q.transform=function(Z,p,ah,ag,j,i){var m=[[Z,p,0],[ah,ag,0],[j,i,1]];aa(this,J(m,this.m_),true)};q.setTransform=function(ag,Z,ai,ah,p,j){var i=[[ag,Z,0],[ai,ah,0],[p,j,1]];aa(this,i,true)};q.drawText_=function(am,ak,aj,ap,ai){var ao=this.m_,at=1000,j=0,ar=at,ah={x:0,y:0},ag=[];var i=u(E(this.font),this.element_);var p=ac(i);var au=this.element_.currentStyle;var Z=this.textAlign.toLowerCase();switch(Z){case"left":case"center":case"right":break;case"end":Z=au.direction=="ltr"?"right":"left";break;case"start":Z=au.direction=="rtl"?"right":"left";break;default:Z="left"}switch(this.textBaseline){case"hanging":case"top":ah.y=i.size/1.75;break;case"middle":break;default:case null:case"alphabetic":case"ideographic":case"bottom":ah.y=-i.size/2.25;break}switch(Z){case"right":j=at;ar=0.05;break;case"center":j=ar=at/2;break}var aq=V(this,ak+ah.x,aj+ah.y);ag.push('<g_vml_:line from="',-j,' 0" to="',ar,' 0.05" ',' coordsize="100 100" coordorigin="0 0"',' filled="',!ai,'" stroked="',!!ai,'" style="position:absolute;width:1px;height:1px;">');if(ai){w(this,ag)}else{G(this,ag,{x:-j,y:0},{x:ar,y:i.size})}var an=ao[0][0].toFixed(3)+","+ao[1][0].toFixed(3)+","+ao[0][1].toFixed(3)+","+ao[1][1].toFixed(3)+",0,0";var al=n(aq.x/d)+","+n(aq.y/d);ag.push('<g_vml_:skew on="t" matrix="',an,'" ',' offset="',al,'" origin="',j,' 0" />','<g_vml_:path textpathok="true" />','<g_vml_:textpath on="true" string="',af(am),'" style="v-text-align:',Z,";font:",af(p),'" /></g_vml_:line>');this.element_.insertAdjacentHTML("beforeEnd",ag.join(""))};q.fillText=function(m,i,p,j){this.drawText_(m,i,p,j,false)};q.strokeText=function(m,i,p,j){this.drawText_(m,i,p,j,true)};q.measureText=function(m){if(!this.textMeasureEl_){var i='<span style="position:absolute;top:-20000px;left:0;padding:0;margin:0;border:none;white-space:pre;"></span>';this.element_.insertAdjacentHTML("beforeEnd",i);this.textMeasureEl_=this.element_.lastChild}var j=this.element_.ownerDocument;this.textMeasureEl_.innerHTML="";this.textMeasureEl_.style.font=this.font;this.textMeasureEl_.appendChild(j.createTextNode(m));return{width:this.textMeasureEl_.offsetWidth}};q.clip=function(){};q.arcTo=function(){};q.createPattern=function(j,i){return new T(j,i)};function U(i){this.type_=i;this.x0_=0;this.y0_=0;this.r0_=0;this.x1_=0;this.y1_=0;this.r1_=0;this.colors_=[]}U.prototype.addColorStop=function(j,i){i=F(i);this.colors_.push({offset:j,color:i.color,alpha:i.alpha})};function T(j,i){Q(j);switch(i){case"repeat":case null:case"":this.repetition_="repeat";break;case"repeat-x":case"repeat-y":case"no-repeat":this.repetition_=i;break;default:O("SYNTAX_ERR")}this.src_=j.src;this.width_=j.width;this.height_=j.height}function O(i){throw new P(i)}function Q(i){if(!i||i.nodeType!=1||i.tagName!="IMG"){O("TYPE_MISMATCH_ERR")}if(i.readyState!="complete"){O("INVALID_STATE_ERR")}}function P(i){this.code=this[i];this.message=i+": DOM Exception "+this.code}var X=P.prototype=new Error;X.INDEX_SIZE_ERR=1;X.DOMSTRING_SIZE_ERR=2;X.HIERARCHY_REQUEST_ERR=3;X.WRONG_DOCUMENT_ERR=4;X.INVALID_CHARACTER_ERR=5;X.NO_DATA_ALLOWED_ERR=6;X.NO_MODIFICATION_ALLOWED_ERR=7;X.NOT_FOUND_ERR=8;X.NOT_SUPPORTED_ERR=9;X.INUSE_ATTRIBUTE_ERR=10;X.INVALID_STATE_ERR=11;X.SYNTAX_ERR=12;X.INVALID_MODIFICATION_ERR=13;X.NAMESPACE_ERR=14;X.INVALID_ACCESS_ERR=15;X.VALIDATION_ERR=16;X.TYPE_MISMATCH_ERR=17;G_vmlCanvasManager=e;CanvasRenderingContext2D=D;CanvasGradient=U;CanvasPattern=T;DOMException=P})()};
\ No newline at end of file
diff --git a/themes/bootstrap3/js/vendor/flot/jquery.flot.min.js b/themes/bootstrap3/js/vendor/flot/jquery.flot.min.js
index 4467fc5d8cd31731709691d912355a1a32578b3e..968d3ebd990df8201274cf924cd74ee52023f7ee 100644
--- a/themes/bootstrap3/js/vendor/flot/jquery.flot.min.js
+++ b/themes/bootstrap3/js/vendor/flot/jquery.flot.min.js
@@ -1,6 +1,8 @@
-/* Javascript plotting library for jQuery, v. 0.7.
- *
- * Released under the MIT license by IOLA, December 2007.
- *
- */
-(function(b){b.color={};b.color.make=function(d,e,g,f){var c={};c.r=d||0;c.g=e||0;c.b=g||0;c.a=f!=null?f:1;c.add=function(h,j){for(var k=0;k<h.length;++k){c[h.charAt(k)]+=j}return c.normalize()};c.scale=function(h,j){for(var k=0;k<h.length;++k){c[h.charAt(k)]*=j}return c.normalize()};c.toString=function(){if(c.a>=1){return"rgb("+[c.r,c.g,c.b].join(",")+")"}else{return"rgba("+[c.r,c.g,c.b,c.a].join(",")+")"}};c.normalize=function(){function h(k,j,l){return j<k?k:(j>l?l:j)}c.r=h(0,parseInt(c.r),255);c.g=h(0,parseInt(c.g),255);c.b=h(0,parseInt(c.b),255);c.a=h(0,c.a,1);return c};c.clone=function(){return b.color.make(c.r,c.b,c.g,c.a)};return c.normalize()};b.color.extract=function(d,e){var c;do{c=d.css(e).toLowerCase();if(c!=""&&c!="transparent"){break}d=d.parent()}while(!b.nodeName(d.get(0),"body"));if(c=="rgba(0, 0, 0, 0)"){c="transparent"}return b.color.parse(c)};b.color.parse=function(c){var d,f=b.color.make;if(d=/rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(c)){return f(parseInt(d[1],10),parseInt(d[2],10),parseInt(d[3],10))}if(d=/rgba\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(c)){return f(parseInt(d[1],10),parseInt(d[2],10),parseInt(d[3],10),parseFloat(d[4]))}if(d=/rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(c)){return f(parseFloat(d[1])*2.55,parseFloat(d[2])*2.55,parseFloat(d[3])*2.55)}if(d=/rgba\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(c)){return f(parseFloat(d[1])*2.55,parseFloat(d[2])*2.55,parseFloat(d[3])*2.55,parseFloat(d[4]))}if(d=/#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(c)){return f(parseInt(d[1],16),parseInt(d[2],16),parseInt(d[3],16))}if(d=/#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(c)){return f(parseInt(d[1]+d[1],16),parseInt(d[2]+d[2],16),parseInt(d[3]+d[3],16))}var e=b.trim(c).toLowerCase();if(e=="transparent"){return f(255,255,255,0)}else{d=a[e]||[0,0,0];return f(d[0],d[1],d[2])}};var a={aqua:[0,255,255],azure:[240,255,255],beige:[245,245,220],black:[0,0,0],blue:[0,0,255],brown:[165,42,42],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgrey:[169,169,169],darkgreen:[0,100,0],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkviolet:[148,0,211],fuchsia:[255,0,255],gold:[255,215,0],green:[0,128,0],indigo:[75,0,130],khaki:[240,230,140],lightblue:[173,216,230],lightcyan:[224,255,255],lightgreen:[144,238,144],lightgrey:[211,211,211],lightpink:[255,182,193],lightyellow:[255,255,224],lime:[0,255,0],magenta:[255,0,255],maroon:[128,0,0],navy:[0,0,128],olive:[128,128,0],orange:[255,165,0],pink:[255,192,203],purple:[128,0,128],violet:[128,0,128],red:[255,0,0],silver:[192,192,192],white:[255,255,255],yellow:[255,255,0]}})(jQuery);(function(c){function b(av,ai,J,af){var Q=[],O={colors:["#edc240","#afd8f8","#cb4b4b","#4da74d","#9440ed"],legend:{show:true,noColumns:1,labelFormatter:null,labelBoxBorderColor:"#ccc",container:null,position:"ne",margin:5,backgroundColor:null,backgroundOpacity:0.85},xaxis:{show:null,position:"bottom",mode:null,color:null,tickColor:null,transform:null,inverseTransform:null,min:null,max:null,autoscaleMargin:null,ticks:null,tickFormatter:null,labelWidth:null,labelHeight:null,reserveSpace:null,tickLength:null,alignTicksWithAxis:null,tickDecimals:null,tickSize:null,minTickSize:null,monthNames:null,timeformat:null,twelveHourClock:false},yaxis:{autoscaleMargin:0.02,position:"left"},xaxes:[],yaxes:[],series:{points:{show:false,radius:3,lineWidth:2,fill:true,fillColor:"#ffffff",symbol:"circle"},lines:{lineWidth:2,fill:false,fillColor:null,steps:false},bars:{show:false,lineWidth:2,barWidth:1,fill:true,fillColor:null,align:"left",horizontal:false},shadowSize:3},grid:{show:true,aboveData:false,color:"#545454",backgroundColor:null,borderColor:null,tickColor:null,labelMargin:5,axisMargin:8,borderWidth:2,minBorderMargin:null,markings:null,markingsColor:"#f4f4f4",markingsLineWidth:2,clickable:false,hoverable:false,autoHighlight:true,mouseActiveRadius:10},hooks:{}},az=null,ad=null,y=null,H=null,A=null,p=[],aw=[],q={left:0,right:0,top:0,bottom:0},G=0,I=0,h=0,w=0,ak={processOptions:[],processRawData:[],processDatapoints:[],drawSeries:[],draw:[],bindEvents:[],drawOverlay:[],shutdown:[]},aq=this;aq.setData=aj;aq.setupGrid=t;aq.draw=W;aq.getPlaceholder=function(){return av};aq.getCanvas=function(){return az};aq.getPlotOffset=function(){return q};aq.width=function(){return h};aq.height=function(){return w};aq.offset=function(){var aB=y.offset();aB.left+=q.left;aB.top+=q.top;return aB};aq.getData=function(){return Q};aq.getAxes=function(){var aC={},aB;c.each(p.concat(aw),function(aD,aE){if(aE){aC[aE.direction+(aE.n!=1?aE.n:"")+"axis"]=aE}});return aC};aq.getXAxes=function(){return p};aq.getYAxes=function(){return aw};aq.c2p=C;aq.p2c=ar;aq.getOptions=function(){return O};aq.highlight=x;aq.unhighlight=T;aq.triggerRedrawOverlay=f;aq.pointOffset=function(aB){return{left:parseInt(p[aA(aB,"x")-1].p2c(+aB.x)+q.left),top:parseInt(aw[aA(aB,"y")-1].p2c(+aB.y)+q.top)}};aq.shutdown=ag;aq.resize=function(){B();g(az);g(ad)};aq.hooks=ak;F(aq);Z(J);X();aj(ai);t();W();ah();function an(aD,aB){aB=[aq].concat(aB);for(var aC=0;aC<aD.length;++aC){aD[aC].apply(this,aB)}}function F(){for(var aB=0;aB<af.length;++aB){var aC=af[aB];aC.init(aq);if(aC.options){c.extend(true,O,aC.options)}}}function Z(aC){var aB;c.extend(true,O,aC);if(O.xaxis.color==null){O.xaxis.color=O.grid.color}if(O.yaxis.color==null){O.yaxis.color=O.grid.color}if(O.xaxis.tickColor==null){O.xaxis.tickColor=O.grid.tickColor}if(O.yaxis.tickColor==null){O.yaxis.tickColor=O.grid.tickColor}if(O.grid.borderColor==null){O.grid.borderColor=O.grid.color}if(O.grid.tickColor==null){O.grid.tickColor=c.color.parse(O.grid.color).scale("a",0.22).toString()}for(aB=0;aB<Math.max(1,O.xaxes.length);++aB){O.xaxes[aB]=c.extend(true,{},O.xaxis,O.xaxes[aB])}for(aB=0;aB<Math.max(1,O.yaxes.length);++aB){O.yaxes[aB]=c.extend(true,{},O.yaxis,O.yaxes[aB])}if(O.xaxis.noTicks&&O.xaxis.ticks==null){O.xaxis.ticks=O.xaxis.noTicks}if(O.yaxis.noTicks&&O.yaxis.ticks==null){O.yaxis.ticks=O.yaxis.noTicks}if(O.x2axis){O.xaxes[1]=c.extend(true,{},O.xaxis,O.x2axis);O.xaxes[1].position="top"}if(O.y2axis){O.yaxes[1]=c.extend(true,{},O.yaxis,O.y2axis);O.yaxes[1].position="right"}if(O.grid.coloredAreas){O.grid.markings=O.grid.coloredAreas}if(O.grid.coloredAreasColor){O.grid.markingsColor=O.grid.coloredAreasColor}if(O.lines){c.extend(true,O.series.lines,O.lines)}if(O.points){c.extend(true,O.series.points,O.points)}if(O.bars){c.extend(true,O.series.bars,O.bars)}if(O.shadowSize!=null){O.series.shadowSize=O.shadowSize}for(aB=0;aB<O.xaxes.length;++aB){V(p,aB+1).options=O.xaxes[aB]}for(aB=0;aB<O.yaxes.length;++aB){V(aw,aB+1).options=O.yaxes[aB]}for(var aD in ak){if(O.hooks[aD]&&O.hooks[aD].length){ak[aD]=ak[aD].concat(O.hooks[aD])}}an(ak.processOptions,[O])}function aj(aB){Q=Y(aB);ax();z()}function Y(aE){var aC=[];for(var aB=0;aB<aE.length;++aB){var aD=c.extend(true,{},O.series);if(aE[aB].data!=null){aD.data=aE[aB].data;delete aE[aB].data;c.extend(true,aD,aE[aB]);aE[aB].data=aD.data}else{aD.data=aE[aB]}aC.push(aD)}return aC}function aA(aC,aD){var aB=aC[aD+"axis"];if(typeof aB=="object"){aB=aB.n}if(typeof aB!="number"){aB=1}return aB}function m(){return c.grep(p.concat(aw),function(aB){return aB})}function C(aE){var aC={},aB,aD;for(aB=0;aB<p.length;++aB){aD=p[aB];if(aD&&aD.used){aC["x"+aD.n]=aD.c2p(aE.left)}}for(aB=0;aB<aw.length;++aB){aD=aw[aB];if(aD&&aD.used){aC["y"+aD.n]=aD.c2p(aE.top)}}if(aC.x1!==undefined){aC.x=aC.x1}if(aC.y1!==undefined){aC.y=aC.y1}return aC}function ar(aF){var aD={},aC,aE,aB;for(aC=0;aC<p.length;++aC){aE=p[aC];if(aE&&aE.used){aB="x"+aE.n;if(aF[aB]==null&&aE.n==1){aB="x"}if(aF[aB]!=null){aD.left=aE.p2c(aF[aB]);break}}}for(aC=0;aC<aw.length;++aC){aE=aw[aC];if(aE&&aE.used){aB="y"+aE.n;if(aF[aB]==null&&aE.n==1){aB="y"}if(aF[aB]!=null){aD.top=aE.p2c(aF[aB]);break}}}return aD}function V(aC,aB){if(!aC[aB-1]){aC[aB-1]={n:aB,direction:aC==p?"x":"y",options:c.extend(true,{},aC==p?O.xaxis:O.yaxis)}}return aC[aB-1]}function ax(){var aG;var aM=Q.length,aB=[],aE=[];for(aG=0;aG<Q.length;++aG){var aJ=Q[aG].color;if(aJ!=null){--aM;if(typeof aJ=="number"){aE.push(aJ)}else{aB.push(c.color.parse(Q[aG].color))}}}for(aG=0;aG<aE.length;++aG){aM=Math.max(aM,aE[aG]+1)}var aC=[],aF=0;aG=0;while(aC.length<aM){var aI;if(O.colors.length==aG){aI=c.color.make(100,100,100)}else{aI=c.color.parse(O.colors[aG])}var aD=aF%2==1?-1:1;aI.scale("rgb",1+aD*Math.ceil(aF/2)*0.2);aC.push(aI);++aG;if(aG>=O.colors.length){aG=0;++aF}}var aH=0,aN;for(aG=0;aG<Q.length;++aG){aN=Q[aG];if(aN.color==null){aN.color=aC[aH].toString();++aH}else{if(typeof aN.color=="number"){aN.color=aC[aN.color].toString()}}if(aN.lines.show==null){var aL,aK=true;for(aL in aN){if(aN[aL]&&aN[aL].show){aK=false;break}}if(aK){aN.lines.show=true}}aN.xaxis=V(p,aA(aN,"x"));aN.yaxis=V(aw,aA(aN,"y"))}}function z(){var aO=Number.POSITIVE_INFINITY,aI=Number.NEGATIVE_INFINITY,aB=Number.MAX_VALUE,aU,aS,aR,aN,aD,aJ,aT,aP,aH,aG,aC,a0,aX,aL;function aF(a3,a2,a1){if(a2<a3.datamin&&a2!=-aB){a3.datamin=a2}if(a1>a3.datamax&&a1!=aB){a3.datamax=a1}}c.each(m(),function(a1,a2){a2.datamin=aO;a2.datamax=aI;a2.used=false});for(aU=0;aU<Q.length;++aU){aJ=Q[aU];aJ.datapoints={points:[]};an(ak.processRawData,[aJ,aJ.data,aJ.datapoints])}for(aU=0;aU<Q.length;++aU){aJ=Q[aU];var aZ=aJ.data,aW=aJ.datapoints.format;if(!aW){aW=[];aW.push({x:true,number:true,required:true});aW.push({y:true,number:true,required:true});if(aJ.bars.show||(aJ.lines.show&&aJ.lines.fill)){aW.push({y:true,number:true,required:false,defaultValue:0});if(aJ.bars.horizontal){delete aW[aW.length-1].y;aW[aW.length-1].x=true}}aJ.datapoints.format=aW}if(aJ.datapoints.pointsize!=null){continue}aJ.datapoints.pointsize=aW.length;aP=aJ.datapoints.pointsize;aT=aJ.datapoints.points;insertSteps=aJ.lines.show&&aJ.lines.steps;aJ.xaxis.used=aJ.yaxis.used=true;for(aS=aR=0;aS<aZ.length;++aS,aR+=aP){aL=aZ[aS];var aE=aL==null;if(!aE){for(aN=0;aN<aP;++aN){a0=aL[aN];aX=aW[aN];if(aX){if(aX.number&&a0!=null){a0=+a0;if(isNaN(a0)){a0=null}else{if(a0==Infinity){a0=aB}else{if(a0==-Infinity){a0=-aB}}}}if(a0==null){if(aX.required){aE=true}if(aX.defaultValue!=null){a0=aX.defaultValue}}}aT[aR+aN]=a0}}if(aE){for(aN=0;aN<aP;++aN){a0=aT[aR+aN];if(a0!=null){aX=aW[aN];if(aX.x){aF(aJ.xaxis,a0,a0)}if(aX.y){aF(aJ.yaxis,a0,a0)}}aT[aR+aN]=null}}else{if(insertSteps&&aR>0&&aT[aR-aP]!=null&&aT[aR-aP]!=aT[aR]&&aT[aR-aP+1]!=aT[aR+1]){for(aN=0;aN<aP;++aN){aT[aR+aP+aN]=aT[aR+aN]}aT[aR+1]=aT[aR-aP+1];aR+=aP}}}}for(aU=0;aU<Q.length;++aU){aJ=Q[aU];an(ak.processDatapoints,[aJ,aJ.datapoints])}for(aU=0;aU<Q.length;++aU){aJ=Q[aU];aT=aJ.datapoints.points,aP=aJ.datapoints.pointsize;var aK=aO,aQ=aO,aM=aI,aV=aI;for(aS=0;aS<aT.length;aS+=aP){if(aT[aS]==null){continue}for(aN=0;aN<aP;++aN){a0=aT[aS+aN];aX=aW[aN];if(!aX||a0==aB||a0==-aB){continue}if(aX.x){if(a0<aK){aK=a0}if(a0>aM){aM=a0}}if(aX.y){if(a0<aQ){aQ=a0}if(a0>aV){aV=a0}}}}if(aJ.bars.show){var aY=aJ.bars.align=="left"?0:-aJ.bars.barWidth/2;if(aJ.bars.horizontal){aQ+=aY;aV+=aY+aJ.bars.barWidth}else{aK+=aY;aM+=aY+aJ.bars.barWidth}}aF(aJ.xaxis,aK,aM);aF(aJ.yaxis,aQ,aV)}c.each(m(),function(a1,a2){if(a2.datamin==aO){a2.datamin=null}if(a2.datamax==aI){a2.datamax=null}})}function j(aB,aC){var aD=document.createElement("canvas");aD.className=aC;aD.width=G;aD.height=I;if(!aB){c(aD).css({position:"absolute",left:0,top:0})}c(aD).appendTo(av);if(!aD.getContext){aD=window.G_vmlCanvasManager.initElement(aD)}aD.getContext("2d").save();return aD}function B(){G=av.width();I=av.height();if(G<=0||I<=0){throw"Invalid dimensions for plot, width = "+G+", height = "+I}}function g(aC){if(aC.width!=G){aC.width=G}if(aC.height!=I){aC.height=I}var aB=aC.getContext("2d");aB.restore();aB.save()}function X(){var aC,aB=av.children("canvas.base"),aD=av.children("canvas.overlay");if(aB.length==0||aD==0){av.html("");av.css({padding:0});if(av.css("position")=="static"){av.css("position","relative")}B();az=j(true,"base");ad=j(false,"overlay");aC=false}else{az=aB.get(0);ad=aD.get(0);aC=true}H=az.getContext("2d");A=ad.getContext("2d");y=c([ad,az]);if(aC){av.data("plot").shutdown();aq.resize();A.clearRect(0,0,G,I);y.unbind();av.children().not([az,ad]).remove()}av.data("plot",aq)}function ah(){if(O.grid.hoverable){y.mousemove(aa);y.mouseleave(l)}if(O.grid.clickable){y.click(R)}an(ak.bindEvents,[y])}function ag(){if(M){clearTimeout(M)}y.unbind("mousemove",aa);y.unbind("mouseleave",l);y.unbind("click",R);an(ak.shutdown,[y])}function r(aG){function aC(aH){return aH}var aF,aB,aD=aG.options.transform||aC,aE=aG.options.inverseTransform;if(aG.direction=="x"){aF=aG.scale=h/Math.abs(aD(aG.max)-aD(aG.min));aB=Math.min(aD(aG.max),aD(aG.min))}else{aF=aG.scale=w/Math.abs(aD(aG.max)-aD(aG.min));aF=-aF;aB=Math.max(aD(aG.max),aD(aG.min))}if(aD==aC){aG.p2c=function(aH){return(aH-aB)*aF}}else{aG.p2c=function(aH){return(aD(aH)-aB)*aF}}if(!aE){aG.c2p=function(aH){return aB+aH/aF}}else{aG.c2p=function(aH){return aE(aB+aH/aF)}}}function L(aD){var aB=aD.options,aF,aJ=aD.ticks||[],aI=[],aE,aK=aB.labelWidth,aG=aB.labelHeight,aC;function aH(aM,aL){return c('<div style="position:absolute;top:-10000px;'+aL+'font-size:smaller"><div class="'+aD.direction+"Axis "+aD.direction+aD.n+'Axis">'+aM.join("")+"</div></div>").appendTo(av)}if(aD.direction=="x"){if(aK==null){aK=Math.floor(G/(aJ.length>0?aJ.length:1))}if(aG==null){aI=[];for(aF=0;aF<aJ.length;++aF){aE=aJ[aF].label;if(aE){aI.push('<div class="tickLabel" style="float:left;width:'+aK+'px">'+aE+"</div>")}}if(aI.length>0){aI.push('<div style="clear:left"></div>');aC=aH(aI,"width:10000px;");aG=aC.height();aC.remove()}}}else{if(aK==null||aG==null){for(aF=0;aF<aJ.length;++aF){aE=aJ[aF].label;if(aE){aI.push('<div class="tickLabel">'+aE+"</div>")}}if(aI.length>0){aC=aH(aI,"");if(aK==null){aK=aC.children().width()}if(aG==null){aG=aC.find("div.tickLabel").height()}aC.remove()}}}if(aK==null){aK=0}if(aG==null){aG=0}aD.labelWidth=aK;aD.labelHeight=aG}function au(aD){var aC=aD.labelWidth,aL=aD.labelHeight,aH=aD.options.position,aF=aD.options.tickLength,aG=O.grid.axisMargin,aJ=O.grid.labelMargin,aK=aD.direction=="x"?p:aw,aE;var aB=c.grep(aK,function(aN){return aN&&aN.options.position==aH&&aN.reserveSpace});if(c.inArray(aD,aB)==aB.length-1){aG=0}if(aF==null){aF="full"}var aI=c.grep(aK,function(aN){return aN&&aN.reserveSpace});var aM=c.inArray(aD,aI)==0;if(!aM&&aF=="full"){aF=5}if(!isNaN(+aF)){aJ+=+aF}if(aD.direction=="x"){aL+=aJ;if(aH=="bottom"){q.bottom+=aL+aG;aD.box={top:I-q.bottom,height:aL}}else{aD.box={top:q.top+aG,height:aL};q.top+=aL+aG}}else{aC+=aJ;if(aH=="left"){aD.box={left:q.left+aG,width:aC};q.left+=aC+aG}else{q.right+=aC+aG;aD.box={left:G-q.right,width:aC}}}aD.position=aH;aD.tickLength=aF;aD.box.padding=aJ;aD.innermost=aM}function U(aB){if(aB.direction=="x"){aB.box.left=q.left;aB.box.width=h}else{aB.box.top=q.top;aB.box.height=w}}function t(){var aC,aE=m();c.each(aE,function(aF,aG){aG.show=aG.options.show;if(aG.show==null){aG.show=aG.used}aG.reserveSpace=aG.show||aG.options.reserveSpace;n(aG)});allocatedAxes=c.grep(aE,function(aF){return aF.reserveSpace});q.left=q.right=q.top=q.bottom=0;if(O.grid.show){c.each(allocatedAxes,function(aF,aG){S(aG);P(aG);ap(aG,aG.ticks);L(aG)});for(aC=allocatedAxes.length-1;aC>=0;--aC){au(allocatedAxes[aC])}var aD=O.grid.minBorderMargin;if(aD==null){aD=0;for(aC=0;aC<Q.length;++aC){aD=Math.max(aD,Q[aC].points.radius+Q[aC].points.lineWidth/2)}}for(var aB in q){q[aB]+=O.grid.borderWidth;q[aB]=Math.max(aD,q[aB])}}h=G-q.left-q.right;w=I-q.bottom-q.top;c.each(aE,function(aF,aG){r(aG)});if(O.grid.show){c.each(allocatedAxes,function(aF,aG){U(aG)});k()}o()}function n(aE){var aF=aE.options,aD=+(aF.min!=null?aF.min:aE.datamin),aB=+(aF.max!=null?aF.max:aE.datamax),aH=aB-aD;if(aH==0){var aC=aB==0?1:0.01;if(aF.min==null){aD-=aC}if(aF.max==null||aF.min!=null){aB+=aC}}else{var aG=aF.autoscaleMargin;if(aG!=null){if(aF.min==null){aD-=aH*aG;if(aD<0&&aE.datamin!=null&&aE.datamin>=0){aD=0}}if(aF.max==null){aB+=aH*aG;if(aB>0&&aE.datamax!=null&&aE.datamax<=0){aB=0}}}}aE.min=aD;aE.max=aB}function S(aG){var aM=aG.options;var aH;if(typeof aM.ticks=="number"&&aM.ticks>0){aH=aM.ticks}else{aH=0.3*Math.sqrt(aG.direction=="x"?G:I)}var aT=(aG.max-aG.min)/aH,aO,aB,aN,aR,aS,aQ,aI;if(aM.mode=="time"){var aJ={second:1000,minute:60*1000,hour:60*60*1000,day:24*60*60*1000,month:30*24*60*60*1000,year:365.2425*24*60*60*1000};var aK=[[1,"second"],[2,"second"],[5,"second"],[10,"second"],[30,"second"],[1,"minute"],[2,"minute"],[5,"minute"],[10,"minute"],[30,"minute"],[1,"hour"],[2,"hour"],[4,"hour"],[8,"hour"],[12,"hour"],[1,"day"],[2,"day"],[3,"day"],[0.25,"month"],[0.5,"month"],[1,"month"],[2,"month"],[3,"month"],[6,"month"],[1,"year"]];var aC=0;if(aM.minTickSize!=null){if(typeof aM.tickSize=="number"){aC=aM.tickSize}else{aC=aM.minTickSize[0]*aJ[aM.minTickSize[1]]}}for(var aS=0;aS<aK.length-1;++aS){if(aT<(aK[aS][0]*aJ[aK[aS][1]]+aK[aS+1][0]*aJ[aK[aS+1][1]])/2&&aK[aS][0]*aJ[aK[aS][1]]>=aC){break}}aO=aK[aS][0];aN=aK[aS][1];if(aN=="year"){aQ=Math.pow(10,Math.floor(Math.log(aT/aJ.year)/Math.LN10));aI=(aT/aJ.year)/aQ;if(aI<1.5){aO=1}else{if(aI<3){aO=2}else{if(aI<7.5){aO=5}else{aO=10}}}aO*=aQ}aG.tickSize=aM.tickSize||[aO,aN];aB=function(aX){var a2=[],a0=aX.tickSize[0],a3=aX.tickSize[1],a1=new Date(aX.min);var aW=a0*aJ[a3];if(a3=="second"){a1.setUTCSeconds(a(a1.getUTCSeconds(),a0))}if(a3=="minute"){a1.setUTCMinutes(a(a1.getUTCMinutes(),a0))}if(a3=="hour"){a1.setUTCHours(a(a1.getUTCHours(),a0))}if(a3=="month"){a1.setUTCMonth(a(a1.getUTCMonth(),a0))}if(a3=="year"){a1.setUTCFullYear(a(a1.getUTCFullYear(),a0))}a1.setUTCMilliseconds(0);if(aW>=aJ.minute){a1.setUTCSeconds(0)}if(aW>=aJ.hour){a1.setUTCMinutes(0)}if(aW>=aJ.day){a1.setUTCHours(0)}if(aW>=aJ.day*4){a1.setUTCDate(1)}if(aW>=aJ.year){a1.setUTCMonth(0)}var a5=0,a4=Number.NaN,aY;do{aY=a4;a4=a1.getTime();a2.push(a4);if(a3=="month"){if(a0<1){a1.setUTCDate(1);var aV=a1.getTime();a1.setUTCMonth(a1.getUTCMonth()+1);var aZ=a1.getTime();a1.setTime(a4+a5*aJ.hour+(aZ-aV)*a0);a5=a1.getUTCHours();a1.setUTCHours(0)}else{a1.setUTCMonth(a1.getUTCMonth()+a0)}}else{if(a3=="year"){a1.setUTCFullYear(a1.getUTCFullYear()+a0)}else{a1.setTime(a4+aW)}}}while(a4<aX.max&&a4!=aY);return a2};aR=function(aV,aY){var a0=new Date(aV);if(aM.timeformat!=null){return c.plot.formatDate(a0,aM.timeformat,aM.monthNames)}var aW=aY.tickSize[0]*aJ[aY.tickSize[1]];var aX=aY.max-aY.min;var aZ=(aM.twelveHourClock)?" %p":"";if(aW<aJ.minute){fmt="%h:%M:%S"+aZ}else{if(aW<aJ.day){if(aX<2*aJ.day){fmt="%h:%M"+aZ}else{fmt="%b %d %h:%M"+aZ}}else{if(aW<aJ.month){fmt="%b %d"}else{if(aW<aJ.year){if(aX<aJ.year){fmt="%b"}else{fmt="%b %y"}}else{fmt="%y"}}}}return c.plot.formatDate(a0,fmt,aM.monthNames)}}else{var aU=aM.tickDecimals;var aP=-Math.floor(Math.log(aT)/Math.LN10);if(aU!=null&&aP>aU){aP=aU}aQ=Math.pow(10,-aP);aI=aT/aQ;if(aI<1.5){aO=1}else{if(aI<3){aO=2;if(aI>2.25&&(aU==null||aP+1<=aU)){aO=2.5;++aP}}else{if(aI<7.5){aO=5}else{aO=10}}}aO*=aQ;if(aM.minTickSize!=null&&aO<aM.minTickSize){aO=aM.minTickSize}aG.tickDecimals=Math.max(0,aU!=null?aU:aP);aG.tickSize=aM.tickSize||aO;aB=function(aX){var aZ=[];var a0=a(aX.min,aX.tickSize),aW=0,aV=Number.NaN,aY;do{aY=aV;aV=a0+aW*aX.tickSize;aZ.push(aV);++aW}while(aV<aX.max&&aV!=aY);return aZ};aR=function(aV,aW){return aV.toFixed(aW.tickDecimals)}}if(aM.alignTicksWithAxis!=null){var aF=(aG.direction=="x"?p:aw)[aM.alignTicksWithAxis-1];if(aF&&aF.used&&aF!=aG){var aL=aB(aG);if(aL.length>0){if(aM.min==null){aG.min=Math.min(aG.min,aL[0])}if(aM.max==null&&aL.length>1){aG.max=Math.max(aG.max,aL[aL.length-1])}}aB=function(aX){var aY=[],aV,aW;for(aW=0;aW<aF.ticks.length;++aW){aV=(aF.ticks[aW].v-aF.min)/(aF.max-aF.min);aV=aX.min+aV*(aX.max-aX.min);aY.push(aV)}return aY};if(aG.mode!="time"&&aM.tickDecimals==null){var aE=Math.max(0,-Math.floor(Math.log(aT)/Math.LN10)+1),aD=aB(aG);if(!(aD.length>1&&/\..*0$/.test((aD[1]-aD[0]).toFixed(aE)))){aG.tickDecimals=aE}}}}aG.tickGenerator=aB;if(c.isFunction(aM.tickFormatter)){aG.tickFormatter=function(aV,aW){return""+aM.tickFormatter(aV,aW)}}else{aG.tickFormatter=aR}}function P(aF){var aH=aF.options.ticks,aG=[];if(aH==null||(typeof aH=="number"&&aH>0)){aG=aF.tickGenerator(aF)}else{if(aH){if(c.isFunction(aH)){aG=aH({min:aF.min,max:aF.max})}else{aG=aH}}}var aE,aB;aF.ticks=[];for(aE=0;aE<aG.length;++aE){var aC=null;var aD=aG[aE];if(typeof aD=="object"){aB=+aD[0];if(aD.length>1){aC=aD[1]}}else{aB=+aD}if(aC==null){aC=aF.tickFormatter(aB,aF)}if(!isNaN(aB)){aF.ticks.push({v:aB,label:aC})}}}function ap(aB,aC){if(aB.options.autoscaleMargin&&aC.length>0){if(aB.options.min==null){aB.min=Math.min(aB.min,aC[0].v)}if(aB.options.max==null&&aC.length>1){aB.max=Math.max(aB.max,aC[aC.length-1].v)}}}function W(){H.clearRect(0,0,G,I);var aC=O.grid;if(aC.show&&aC.backgroundColor){N()}if(aC.show&&!aC.aboveData){ac()}for(var aB=0;aB<Q.length;++aB){an(ak.drawSeries,[H,Q[aB]]);d(Q[aB])}an(ak.draw,[H]);if(aC.show&&aC.aboveData){ac()}}function D(aB,aI){var aE,aH,aG,aD,aF=m();for(i=0;i<aF.length;++i){aE=aF[i];if(aE.direction==aI){aD=aI+aE.n+"axis";if(!aB[aD]&&aE.n==1){aD=aI+"axis"}if(aB[aD]){aH=aB[aD].from;aG=aB[aD].to;break}}}if(!aB[aD]){aE=aI=="x"?p[0]:aw[0];aH=aB[aI+"1"];aG=aB[aI+"2"]}if(aH!=null&&aG!=null&&aH>aG){var aC=aH;aH=aG;aG=aC}return{from:aH,to:aG,axis:aE}}function N(){H.save();H.translate(q.left,q.top);H.fillStyle=am(O.grid.backgroundColor,w,0,"rgba(255, 255, 255, 0)");H.fillRect(0,0,h,w);H.restore()}function ac(){var aF;H.save();H.translate(q.left,q.top);var aH=O.grid.markings;if(aH){if(c.isFunction(aH)){var aK=aq.getAxes();aK.xmin=aK.xaxis.min;aK.xmax=aK.xaxis.max;aK.ymin=aK.yaxis.min;aK.ymax=aK.yaxis.max;aH=aH(aK)}for(aF=0;aF<aH.length;++aF){var aD=aH[aF],aC=D(aD,"x"),aI=D(aD,"y");if(aC.from==null){aC.from=aC.axis.min}if(aC.to==null){aC.to=aC.axis.max}if(aI.from==null){aI.from=aI.axis.min}if(aI.to==null){aI.to=aI.axis.max}if(aC.to<aC.axis.min||aC.from>aC.axis.max||aI.to<aI.axis.min||aI.from>aI.axis.max){continue}aC.from=Math.max(aC.from,aC.axis.min);aC.to=Math.min(aC.to,aC.axis.max);aI.from=Math.max(aI.from,aI.axis.min);aI.to=Math.min(aI.to,aI.axis.max);if(aC.from==aC.to&&aI.from==aI.to){continue}aC.from=aC.axis.p2c(aC.from);aC.to=aC.axis.p2c(aC.to);aI.from=aI.axis.p2c(aI.from);aI.to=aI.axis.p2c(aI.to);if(aC.from==aC.to||aI.from==aI.to){H.beginPath();H.strokeStyle=aD.color||O.grid.markingsColor;H.lineWidth=aD.lineWidth||O.grid.markingsLineWidth;H.moveTo(aC.from,aI.from);H.lineTo(aC.to,aI.to);H.stroke()}else{H.fillStyle=aD.color||O.grid.markingsColor;H.fillRect(aC.from,aI.to,aC.to-aC.from,aI.from-aI.to)}}}var aK=m(),aM=O.grid.borderWidth;for(var aE=0;aE<aK.length;++aE){var aB=aK[aE],aG=aB.box,aQ=aB.tickLength,aN,aL,aP,aJ;if(!aB.show||aB.ticks.length==0){continue}H.strokeStyle=aB.options.tickColor||c.color.parse(aB.options.color).scale("a",0.22).toString();H.lineWidth=1;if(aB.direction=="x"){aN=0;if(aQ=="full"){aL=(aB.position=="top"?0:w)}else{aL=aG.top-q.top+(aB.position=="top"?aG.height:0)}}else{aL=0;if(aQ=="full"){aN=(aB.position=="left"?0:h)}else{aN=aG.left-q.left+(aB.position=="left"?aG.width:0)}}if(!aB.innermost){H.beginPath();aP=aJ=0;if(aB.direction=="x"){aP=h}else{aJ=w}if(H.lineWidth==1){aN=Math.floor(aN)+0.5;aL=Math.floor(aL)+0.5}H.moveTo(aN,aL);H.lineTo(aN+aP,aL+aJ);H.stroke()}H.beginPath();for(aF=0;aF<aB.ticks.length;++aF){var aO=aB.ticks[aF].v;aP=aJ=0;if(aO<aB.min||aO>aB.max||(aQ=="full"&&aM>0&&(aO==aB.min||aO==aB.max))){continue}if(aB.direction=="x"){aN=aB.p2c(aO);aJ=aQ=="full"?-w:aQ;if(aB.position=="top"){aJ=-aJ}}else{aL=aB.p2c(aO);aP=aQ=="full"?-h:aQ;if(aB.position=="left"){aP=-aP}}if(H.lineWidth==1){if(aB.direction=="x"){aN=Math.floor(aN)+0.5}else{aL=Math.floor(aL)+0.5}}H.moveTo(aN,aL);H.lineTo(aN+aP,aL+aJ)}H.stroke()}if(aM){H.lineWidth=aM;H.strokeStyle=O.grid.borderColor;H.strokeRect(-aM/2,-aM/2,h+aM,w+aM)}H.restore()}function k(){av.find(".tickLabels").remove();var aG=['<div class="tickLabels" style="font-size:smaller">'];var aJ=m();for(var aD=0;aD<aJ.length;++aD){var aC=aJ[aD],aF=aC.box;if(!aC.show){continue}aG.push('<div class="'+aC.direction+"Axis "+aC.direction+aC.n+'Axis" style="color:'+aC.options.color+'">');for(var aE=0;aE<aC.ticks.length;++aE){var aH=aC.ticks[aE];if(!aH.label||aH.v<aC.min||aH.v>aC.max){continue}var aK={},aI;if(aC.direction=="x"){aI="center";aK.left=Math.round(q.left+aC.p2c(aH.v)-aC.labelWidth/2);if(aC.position=="bottom"){aK.top=aF.top+aF.padding}else{aK.bottom=I-(aF.top+aF.height-aF.padding)}}else{aK.top=Math.round(q.top+aC.p2c(aH.v)-aC.labelHeight/2);if(aC.position=="left"){aK.right=G-(aF.left+aF.width-aF.padding);aI="right"}else{aK.left=aF.left+aF.padding;aI="left"}}aK.width=aC.labelWidth;var aB=["position:absolute","text-align:"+aI];for(var aL in aK){aB.push(aL+":"+aK[aL]+"px")}aG.push('<div class="tickLabel" style="'+aB.join(";")+'">'+aH.label+"</div>")}aG.push("</div>")}aG.push("</div>");av.append(aG.join(""))}function d(aB){if(aB.lines.show){at(aB)}if(aB.bars.show){e(aB)}if(aB.points.show){ao(aB)}}function at(aE){function aD(aP,aQ,aI,aU,aT){var aV=aP.points,aJ=aP.pointsize,aN=null,aM=null;H.beginPath();for(var aO=aJ;aO<aV.length;aO+=aJ){var aL=aV[aO-aJ],aS=aV[aO-aJ+1],aK=aV[aO],aR=aV[aO+1];if(aL==null||aK==null){continue}if(aS<=aR&&aS<aT.min){if(aR<aT.min){continue}aL=(aT.min-aS)/(aR-aS)*(aK-aL)+aL;aS=aT.min}else{if(aR<=aS&&aR<aT.min){if(aS<aT.min){continue}aK=(aT.min-aS)/(aR-aS)*(aK-aL)+aL;aR=aT.min}}if(aS>=aR&&aS>aT.max){if(aR>aT.max){continue}aL=(aT.max-aS)/(aR-aS)*(aK-aL)+aL;aS=aT.max}else{if(aR>=aS&&aR>aT.max){if(aS>aT.max){continue}aK=(aT.max-aS)/(aR-aS)*(aK-aL)+aL;aR=aT.max}}if(aL<=aK&&aL<aU.min){if(aK<aU.min){continue}aS=(aU.min-aL)/(aK-aL)*(aR-aS)+aS;aL=aU.min}else{if(aK<=aL&&aK<aU.min){if(aL<aU.min){continue}aR=(aU.min-aL)/(aK-aL)*(aR-aS)+aS;aK=aU.min}}if(aL>=aK&&aL>aU.max){if(aK>aU.max){continue}aS=(aU.max-aL)/(aK-aL)*(aR-aS)+aS;aL=aU.max}else{if(aK>=aL&&aK>aU.max){if(aL>aU.max){continue}aR=(aU.max-aL)/(aK-aL)*(aR-aS)+aS;aK=aU.max}}if(aL!=aN||aS!=aM){H.moveTo(aU.p2c(aL)+aQ,aT.p2c(aS)+aI)}aN=aK;aM=aR;H.lineTo(aU.p2c(aK)+aQ,aT.p2c(aR)+aI)}H.stroke()}function aF(aI,aQ,aP){var aW=aI.points,aV=aI.pointsize,aN=Math.min(Math.max(0,aP.min),aP.max),aX=0,aU,aT=false,aM=1,aL=0,aR=0;while(true){if(aV>0&&aX>aW.length+aV){break}aX+=aV;var aZ=aW[aX-aV],aK=aW[aX-aV+aM],aY=aW[aX],aJ=aW[aX+aM];if(aT){if(aV>0&&aZ!=null&&aY==null){aR=aX;aV=-aV;aM=2;continue}if(aV<0&&aX==aL+aV){H.fill();aT=false;aV=-aV;aM=1;aX=aL=aR+aV;continue}}if(aZ==null||aY==null){continue}if(aZ<=aY&&aZ<aQ.min){if(aY<aQ.min){continue}aK=(aQ.min-aZ)/(aY-aZ)*(aJ-aK)+aK;aZ=aQ.min}else{if(aY<=aZ&&aY<aQ.min){if(aZ<aQ.min){continue}aJ=(aQ.min-aZ)/(aY-aZ)*(aJ-aK)+aK;aY=aQ.min}}if(aZ>=aY&&aZ>aQ.max){if(aY>aQ.max){continue}aK=(aQ.max-aZ)/(aY-aZ)*(aJ-aK)+aK;aZ=aQ.max}else{if(aY>=aZ&&aY>aQ.max){if(aZ>aQ.max){continue}aJ=(aQ.max-aZ)/(aY-aZ)*(aJ-aK)+aK;aY=aQ.max}}if(!aT){H.beginPath();H.moveTo(aQ.p2c(aZ),aP.p2c(aN));aT=true}if(aK>=aP.max&&aJ>=aP.max){H.lineTo(aQ.p2c(aZ),aP.p2c(aP.max));H.lineTo(aQ.p2c(aY),aP.p2c(aP.max));continue}else{if(aK<=aP.min&&aJ<=aP.min){H.lineTo(aQ.p2c(aZ),aP.p2c(aP.min));H.lineTo(aQ.p2c(aY),aP.p2c(aP.min));continue}}var aO=aZ,aS=aY;if(aK<=aJ&&aK<aP.min&&aJ>=aP.min){aZ=(aP.min-aK)/(aJ-aK)*(aY-aZ)+aZ;aK=aP.min}else{if(aJ<=aK&&aJ<aP.min&&aK>=aP.min){aY=(aP.min-aK)/(aJ-aK)*(aY-aZ)+aZ;aJ=aP.min}}if(aK>=aJ&&aK>aP.max&&aJ<=aP.max){aZ=(aP.max-aK)/(aJ-aK)*(aY-aZ)+aZ;aK=aP.max}else{if(aJ>=aK&&aJ>aP.max&&aK<=aP.max){aY=(aP.max-aK)/(aJ-aK)*(aY-aZ)+aZ;aJ=aP.max}}if(aZ!=aO){H.lineTo(aQ.p2c(aO),aP.p2c(aK))}H.lineTo(aQ.p2c(aZ),aP.p2c(aK));H.lineTo(aQ.p2c(aY),aP.p2c(aJ));if(aY!=aS){H.lineTo(aQ.p2c(aY),aP.p2c(aJ));H.lineTo(aQ.p2c(aS),aP.p2c(aJ))}}}H.save();H.translate(q.left,q.top);H.lineJoin="round";var aG=aE.lines.lineWidth,aB=aE.shadowSize;if(aG>0&&aB>0){H.lineWidth=aB;H.strokeStyle="rgba(0,0,0,0.1)";var aH=Math.PI/18;aD(aE.datapoints,Math.sin(aH)*(aG/2+aB/2),Math.cos(aH)*(aG/2+aB/2),aE.xaxis,aE.yaxis);H.lineWidth=aB/2;aD(aE.datapoints,Math.sin(aH)*(aG/2+aB/4),Math.cos(aH)*(aG/2+aB/4),aE.xaxis,aE.yaxis)}H.lineWidth=aG;H.strokeStyle=aE.color;var aC=ae(aE.lines,aE.color,0,w);if(aC){H.fillStyle=aC;aF(aE.datapoints,aE.xaxis,aE.yaxis)}if(aG>0){aD(aE.datapoints,0,0,aE.xaxis,aE.yaxis)}H.restore()}function ao(aE){function aH(aN,aM,aU,aK,aS,aT,aQ,aJ){var aR=aN.points,aI=aN.pointsize;for(var aL=0;aL<aR.length;aL+=aI){var aP=aR[aL],aO=aR[aL+1];if(aP==null||aP<aT.min||aP>aT.max||aO<aQ.min||aO>aQ.max){continue}H.beginPath();aP=aT.p2c(aP);aO=aQ.p2c(aO)+aK;if(aJ=="circle"){H.arc(aP,aO,aM,0,aS?Math.PI:Math.PI*2,false)}else{aJ(H,aP,aO,aM,aS)}H.closePath();if(aU){H.fillStyle=aU;H.fill()}H.stroke()}}H.save();H.translate(q.left,q.top);var aG=aE.points.lineWidth,aC=aE.shadowSize,aB=aE.points.radius,aF=aE.points.symbol;if(aG>0&&aC>0){var aD=aC/2;H.lineWidth=aD;H.strokeStyle="rgba(0,0,0,0.1)";aH(aE.datapoints,aB,null,aD+aD/2,true,aE.xaxis,aE.yaxis,aF);H.strokeStyle="rgba(0,0,0,0.2)";aH(aE.datapoints,aB,null,aD/2,true,aE.xaxis,aE.yaxis,aF)}H.lineWidth=aG;H.strokeStyle=aE.color;aH(aE.datapoints,aB,ae(aE.points,aE.color),0,false,aE.xaxis,aE.yaxis,aF);H.restore()}function E(aN,aM,aV,aI,aQ,aF,aD,aL,aK,aU,aR,aC){var aE,aT,aJ,aP,aG,aB,aO,aH,aS;if(aR){aH=aB=aO=true;aG=false;aE=aV;aT=aN;aP=aM+aI;aJ=aM+aQ;if(aT<aE){aS=aT;aT=aE;aE=aS;aG=true;aB=false}}else{aG=aB=aO=true;aH=false;aE=aN+aI;aT=aN+aQ;aJ=aV;aP=aM;if(aP<aJ){aS=aP;aP=aJ;aJ=aS;aH=true;aO=false}}if(aT<aL.min||aE>aL.max||aP<aK.min||aJ>aK.max){return}if(aE<aL.min){aE=aL.min;aG=false}if(aT>aL.max){aT=aL.max;aB=false}if(aJ<aK.min){aJ=aK.min;aH=false}if(aP>aK.max){aP=aK.max;aO=false}aE=aL.p2c(aE);aJ=aK.p2c(aJ);aT=aL.p2c(aT);aP=aK.p2c(aP);if(aD){aU.beginPath();aU.moveTo(aE,aJ);aU.lineTo(aE,aP);aU.lineTo(aT,aP);aU.lineTo(aT,aJ);aU.fillStyle=aD(aJ,aP);aU.fill()}if(aC>0&&(aG||aB||aO||aH)){aU.beginPath();aU.moveTo(aE,aJ+aF);if(aG){aU.lineTo(aE,aP+aF)}else{aU.moveTo(aE,aP+aF)}if(aO){aU.lineTo(aT,aP+aF)}else{aU.moveTo(aT,aP+aF)}if(aB){aU.lineTo(aT,aJ+aF)}else{aU.moveTo(aT,aJ+aF)}if(aH){aU.lineTo(aE,aJ+aF)}else{aU.moveTo(aE,aJ+aF)}aU.stroke()}}function e(aD){function aC(aJ,aI,aL,aG,aK,aN,aM){var aO=aJ.points,aF=aJ.pointsize;for(var aH=0;aH<aO.length;aH+=aF){if(aO[aH]==null){continue}E(aO[aH],aO[aH+1],aO[aH+2],aI,aL,aG,aK,aN,aM,H,aD.bars.horizontal,aD.bars.lineWidth)}}H.save();H.translate(q.left,q.top);H.lineWidth=aD.bars.lineWidth;H.strokeStyle=aD.color;var aB=aD.bars.align=="left"?0:-aD.bars.barWidth/2;var aE=aD.bars.fill?function(aF,aG){return ae(aD.bars,aD.color,aF,aG)}:null;aC(aD.datapoints,aB,aB+aD.bars.barWidth,0,aE,aD.xaxis,aD.yaxis);H.restore()}function ae(aD,aB,aC,aF){var aE=aD.fill;if(!aE){return null}if(aD.fillColor){return am(aD.fillColor,aC,aF,aB)}var aG=c.color.parse(aB);aG.a=typeof aE=="number"?aE:0.4;aG.normalize();return aG.toString()}function o(){av.find(".legend").remove();if(!O.legend.show){return}var aH=[],aF=false,aN=O.legend.labelFormatter,aM,aJ;for(var aE=0;aE<Q.length;++aE){aM=Q[aE];aJ=aM.label;if(!aJ){continue}if(aE%O.legend.noColumns==0){if(aF){aH.push("</tr>")}aH.push("<tr>");aF=true}if(aN){aJ=aN(aJ,aM)}aH.push('<td class="legendColorBox"><div style="border:1px solid '+O.legend.labelBoxBorderColor+';padding:1px"><div style="width:4px;height:0;border:5px solid '+aM.color+';overflow:hidden"></div></div></td><td class="legendLabel">'+aJ+"</td>")}if(aF){aH.push("</tr>")}if(aH.length==0){return}var aL='<table style="font-size:smaller;color:'+O.grid.color+'">'+aH.join("")+"</table>";if(O.legend.container!=null){c(O.legend.container).html(aL)}else{var aI="",aC=O.legend.position,aD=O.legend.margin;if(aD[0]==null){aD=[aD,aD]}if(aC.charAt(0)=="n"){aI+="top:"+(aD[1]+q.top)+"px;"}else{if(aC.charAt(0)=="s"){aI+="bottom:"+(aD[1]+q.bottom)+"px;"}}if(aC.charAt(1)=="e"){aI+="right:"+(aD[0]+q.right)+"px;"}else{if(aC.charAt(1)=="w"){aI+="left:"+(aD[0]+q.left)+"px;"}}var aK=c('<div class="legend">'+aL.replace('style="','style="position:absolute;'+aI+";")+"</div>").appendTo(av);if(O.legend.backgroundOpacity!=0){var aG=O.legend.backgroundColor;if(aG==null){aG=O.grid.backgroundColor;if(aG&&typeof aG=="string"){aG=c.color.parse(aG)}else{aG=c.color.extract(aK,"background-color")}aG.a=1;aG=aG.toString()}var aB=aK.children();c('<div style="position:absolute;width:'+aB.width()+"px;height:"+aB.height()+"px;"+aI+"background-color:"+aG+';"> </div>').prependTo(aK).css("opacity",O.legend.backgroundOpacity)}}}var ab=[],M=null;function K(aI,aG,aD){var aO=O.grid.mouseActiveRadius,a0=aO*aO+1,aY=null,aR=false,aW,aU;for(aW=Q.length-1;aW>=0;--aW){if(!aD(Q[aW])){continue}var aP=Q[aW],aH=aP.xaxis,aF=aP.yaxis,aV=aP.datapoints.points,aT=aP.datapoints.pointsize,aQ=aH.c2p(aI),aN=aF.c2p(aG),aC=aO/aH.scale,aB=aO/aF.scale;if(aH.options.inverseTransform){aC=Number.MAX_VALUE}if(aF.options.inverseTransform){aB=Number.MAX_VALUE}if(aP.lines.show||aP.points.show){for(aU=0;aU<aV.length;aU+=aT){var aK=aV[aU],aJ=aV[aU+1];if(aK==null){continue}if(aK-aQ>aC||aK-aQ<-aC||aJ-aN>aB||aJ-aN<-aB){continue}var aM=Math.abs(aH.p2c(aK)-aI),aL=Math.abs(aF.p2c(aJ)-aG),aS=aM*aM+aL*aL;if(aS<a0){a0=aS;aY=[aW,aU/aT]}}}if(aP.bars.show&&!aY){var aE=aP.bars.align=="left"?0:-aP.bars.barWidth/2,aX=aE+aP.bars.barWidth;for(aU=0;aU<aV.length;aU+=aT){var aK=aV[aU],aJ=aV[aU+1],aZ=aV[aU+2];if(aK==null){continue}if(Q[aW].bars.horizontal?(aQ<=Math.max(aZ,aK)&&aQ>=Math.min(aZ,aK)&&aN>=aJ+aE&&aN<=aJ+aX):(aQ>=aK+aE&&aQ<=aK+aX&&aN>=Math.min(aZ,aJ)&&aN<=Math.max(aZ,aJ))){aY=[aW,aU/aT]}}}}if(aY){aW=aY[0];aU=aY[1];aT=Q[aW].datapoints.pointsize;return{datapoint:Q[aW].datapoints.points.slice(aU*aT,(aU+1)*aT),dataIndex:aU,series:Q[aW],seriesIndex:aW}}return null}function aa(aB){if(O.grid.hoverable){u("plothover",aB,function(aC){return aC.hoverable!=false})}}function l(aB){if(O.grid.hoverable){u("plothover",aB,function(aC){return false})}}function R(aB){u("plotclick",aB,function(aC){return aC.clickable!=false})}function u(aC,aB,aD){var aE=y.offset(),aH=aB.pageX-aE.left-q.left,aF=aB.pageY-aE.top-q.top,aJ=C({left:aH,top:aF});aJ.pageX=aB.pageX;aJ.pageY=aB.pageY;var aK=K(aH,aF,aD);if(aK){aK.pageX=parseInt(aK.series.xaxis.p2c(aK.datapoint[0])+aE.left+q.left);aK.pageY=parseInt(aK.series.yaxis.p2c(aK.datapoint[1])+aE.top+q.top)}if(O.grid.autoHighlight){for(var aG=0;aG<ab.length;++aG){var aI=ab[aG];if(aI.auto==aC&&!(aK&&aI.series==aK.series&&aI.point[0]==aK.datapoint[0]&&aI.point[1]==aK.datapoint[1])){T(aI.series,aI.point)}}if(aK){x(aK.series,aK.datapoint,aC)}}av.trigger(aC,[aJ,aK])}function f(){if(!M){M=setTimeout(s,30)}}function s(){M=null;A.save();A.clearRect(0,0,G,I);A.translate(q.left,q.top);var aC,aB;for(aC=0;aC<ab.length;++aC){aB=ab[aC];if(aB.series.bars.show){v(aB.series,aB.point)}else{ay(aB.series,aB.point)}}A.restore();an(ak.drawOverlay,[A])}function x(aD,aB,aF){if(typeof aD=="number"){aD=Q[aD]}if(typeof aB=="number"){var aE=aD.datapoints.pointsize;aB=aD.datapoints.points.slice(aE*aB,aE*(aB+1))}var aC=al(aD,aB);if(aC==-1){ab.push({series:aD,point:aB,auto:aF});f()}else{if(!aF){ab[aC].auto=false}}}function T(aD,aB){if(aD==null&&aB==null){ab=[];f()}if(typeof aD=="number"){aD=Q[aD]}if(typeof aB=="number"){aB=aD.data[aB]}var aC=al(aD,aB);if(aC!=-1){ab.splice(aC,1);f()}}function al(aD,aE){for(var aB=0;aB<ab.length;++aB){var aC=ab[aB];if(aC.series==aD&&aC.point[0]==aE[0]&&aC.point[1]==aE[1]){return aB}}return -1}function ay(aE,aD){var aC=aD[0],aI=aD[1],aH=aE.xaxis,aG=aE.yaxis;if(aC<aH.min||aC>aH.max||aI<aG.min||aI>aG.max){return}var aF=aE.points.radius+aE.points.lineWidth/2;A.lineWidth=aF;A.strokeStyle=c.color.parse(aE.color).scale("a",0.5).toString();var aB=1.5*aF,aC=aH.p2c(aC),aI=aG.p2c(aI);A.beginPath();if(aE.points.symbol=="circle"){A.arc(aC,aI,aB,0,2*Math.PI,false)}else{aE.points.symbol(A,aC,aI,aB,false)}A.closePath();A.stroke()}function v(aE,aB){A.lineWidth=aE.bars.lineWidth;A.strokeStyle=c.color.parse(aE.color).scale("a",0.5).toString();var aD=c.color.parse(aE.color).scale("a",0.5).toString();var aC=aE.bars.align=="left"?0:-aE.bars.barWidth/2;E(aB[0],aB[1],aB[2]||0,aC,aC+aE.bars.barWidth,0,function(){return aD},aE.xaxis,aE.yaxis,A,aE.bars.horizontal,aE.bars.lineWidth)}function am(aJ,aB,aH,aC){if(typeof aJ=="string"){return aJ}else{var aI=H.createLinearGradient(0,aH,0,aB);for(var aE=0,aD=aJ.colors.length;aE<aD;++aE){var aF=aJ.colors[aE];if(typeof aF!="string"){var aG=c.color.parse(aC);if(aF.brightness!=null){aG=aG.scale("rgb",aF.brightness)}if(aF.opacity!=null){aG.a*=aF.opacity}aF=aG.toString()}aI.addColorStop(aE/(aD-1),aF)}return aI}}}c.plot=function(g,e,d){var f=new b(c(g),e,d,c.plot.plugins);return f};c.plot.version="0.7";c.plot.plugins=[];c.plot.formatDate=function(l,f,h){var o=function(d){d=""+d;return d.length==1?"0"+d:d};var e=[];var p=false,j=false;var n=l.getUTCHours();var k=n<12;if(h==null){h=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]}if(f.search(/%p|%P/)!=-1){if(n>12){n=n-12}else{if(n==0){n=12}}}for(var g=0;g<f.length;++g){var m=f.charAt(g);if(p){switch(m){case"h":m=""+n;break;case"H":m=o(n);break;case"M":m=o(l.getUTCMinutes());break;case"S":m=o(l.getUTCSeconds());break;case"d":m=""+l.getUTCDate();break;case"m":m=""+(l.getUTCMonth()+1);break;case"y":m=""+l.getUTCFullYear();break;case"b":m=""+h[l.getUTCMonth()];break;case"p":m=(k)?("am"):("pm");break;case"P":m=(k)?("AM"):("PM");break;case"0":m="";j=true;break}if(m&&j){m=o(m);j=false}e.push(m);if(!j){p=false}}else{if(m=="%"){p=true}else{e.push(m)}}}return e.join("")};function a(e,d){return d*Math.floor(e/d)}})(jQuery);
\ No newline at end of file
+/* Javascript plotting library for jQuery, version 0.8.3.
+
+Copyright (c) 2007-2014 IOLA and Ole Laursen.
+Licensed under the MIT license.
+
+*/
+(function($){$.color={};$.color.make=function(r,g,b,a){var o={};o.r=r||0;o.g=g||0;o.b=b||0;o.a=a!=null?a:1;o.add=function(c,d){for(var i=0;i<c.length;++i)o[c.charAt(i)]+=d;return o.normalize()};o.scale=function(c,f){for(var i=0;i<c.length;++i)o[c.charAt(i)]*=f;return o.normalize()};o.toString=function(){if(o.a>=1){return"rgb("+[o.r,o.g,o.b].join(",")+")"}else{return"rgba("+[o.r,o.g,o.b,o.a].join(",")+")"}};o.normalize=function(){function clamp(min,value,max){return value<min?min:value>max?max:value}o.r=clamp(0,parseInt(o.r),255);o.g=clamp(0,parseInt(o.g),255);o.b=clamp(0,parseInt(o.b),255);o.a=clamp(0,o.a,1);return o};o.clone=function(){return $.color.make(o.r,o.b,o.g,o.a)};return o.normalize()};$.color.extract=function(elem,css){var c;do{c=elem.css(css).toLowerCase();if(c!=""&&c!="transparent")break;elem=elem.parent()}while(elem.length&&!$.nodeName(elem.get(0),"body"));if(c=="rgba(0, 0, 0, 0)")c="transparent";return $.color.parse(c)};$.color.parse=function(str){var res,m=$.color.make;if(res=/rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(str))return m(parseInt(res[1],10),parseInt(res[2],10),parseInt(res[3],10));if(res=/rgba\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(str))return m(parseInt(res[1],10),parseInt(res[2],10),parseInt(res[3],10),parseFloat(res[4]));if(res=/rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(str))return m(parseFloat(res[1])*2.55,parseFloat(res[2])*2.55,parseFloat(res[3])*2.55);if(res=/rgba\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(str))return m(parseFloat(res[1])*2.55,parseFloat(res[2])*2.55,parseFloat(res[3])*2.55,parseFloat(res[4]));if(res=/#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(str))return m(parseInt(res[1],16),parseInt(res[2],16),parseInt(res[3],16));if(res=/#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(str))return m(parseInt(res[1]+res[1],16),parseInt(res[2]+res[2],16),parseInt(res[3]+res[3],16));var name=$.trim(str).toLowerCase();if(name=="transparent")return m(255,255,255,0);else{res=lookupColors[name]||[0,0,0];return m(res[0],res[1],res[2])}};var lookupColors={aqua:[0,255,255],azure:[240,255,255],beige:[245,245,220],black:[0,0,0],blue:[0,0,255],brown:[165,42,42],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgrey:[169,169,169],darkgreen:[0,100,0],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkviolet:[148,0,211],fuchsia:[255,0,255],gold:[255,215,0],green:[0,128,0],indigo:[75,0,130],khaki:[240,230,140],lightblue:[173,216,230],lightcyan:[224,255,255],lightgreen:[144,238,144],lightgrey:[211,211,211],lightpink:[255,182,193],lightyellow:[255,255,224],lime:[0,255,0],magenta:[255,0,255],maroon:[128,0,0],navy:[0,0,128],olive:[128,128,0],orange:[255,165,0],pink:[255,192,203],purple:[128,0,128],violet:[128,0,128],red:[255,0,0],silver:[192,192,192],white:[255,255,255],yellow:[255,255,0]}})(jQuery);(function($){var hasOwnProperty=Object.prototype.hasOwnProperty;if(!$.fn.detach){$.fn.detach=function(){return this.each(function(){if(this.parentNode){this.parentNode.removeChild(this)}})}}function Canvas(cls,container){var element=container.children("."+cls)[0];if(element==null){element=document.createElement("canvas");element.className=cls;$(element).css({direction:"ltr",position:"absolute",left:0,top:0}).appendTo(container);if(!element.getContext){if(window.G_vmlCanvasManager){element=window.G_vmlCanvasManager.initElement(element)}else{throw new Error("Canvas is not available. If you're using IE with a fall-back such as Excanvas, then there's either a mistake in your conditional include, or the page has no DOCTYPE and is rendering in Quirks Mode.")}}}this.element=element;var context=this.context=element.getContext("2d");var devicePixelRatio=window.devicePixelRatio||1,backingStoreRatio=context.webkitBackingStorePixelRatio||context.mozBackingStorePixelRatio||context.msBackingStorePixelRatio||context.oBackingStorePixelRatio||context.backingStorePixelRatio||1;this.pixelRatio=devicePixelRatio/backingStoreRatio;this.resize(container.width(),container.height());this.textContainer=null;this.text={};this._textCache={}}Canvas.prototype.resize=function(width,height){if(width<=0||height<=0){throw new Error("Invalid dimensions for plot, width = "+width+", height = "+height)}var element=this.element,context=this.context,pixelRatio=this.pixelRatio;if(this.width!=width){element.width=width*pixelRatio;element.style.width=width+"px";this.width=width}if(this.height!=height){element.height=height*pixelRatio;element.style.height=height+"px";this.height=height}context.restore();context.save();context.scale(pixelRatio,pixelRatio)};Canvas.prototype.clear=function(){this.context.clearRect(0,0,this.width,this.height)};Canvas.prototype.render=function(){var cache=this._textCache;for(var layerKey in cache){if(hasOwnProperty.call(cache,layerKey)){var layer=this.getTextLayer(layerKey),layerCache=cache[layerKey];layer.hide();for(var styleKey in layerCache){if(hasOwnProperty.call(layerCache,styleKey)){var styleCache=layerCache[styleKey];for(var key in styleCache){if(hasOwnProperty.call(styleCache,key)){var positions=styleCache[key].positions;for(var i=0,position;position=positions[i];i++){if(position.active){if(!position.rendered){layer.append(position.element);position.rendered=true}}else{positions.splice(i--,1);if(position.rendered){position.element.detach()}}}if(positions.length==0){delete styleCache[key]}}}}}layer.show()}}};Canvas.prototype.getTextLayer=function(classes){var layer=this.text[classes];if(layer==null){if(this.textContainer==null){this.textContainer=$("<div class='flot-text'></div>").css({position:"absolute",top:0,left:0,bottom:0,right:0,"font-size":"smaller",color:"#545454"}).insertAfter(this.element)}layer=this.text[classes]=$("<div></div>").addClass(classes).css({position:"absolute",top:0,left:0,bottom:0,right:0}).appendTo(this.textContainer)}return layer};Canvas.prototype.getTextInfo=function(layer,text,font,angle,width){var textStyle,layerCache,styleCache,info;text=""+text;if(typeof font==="object"){textStyle=font.style+" "+font.variant+" "+font.weight+" "+font.size+"px/"+font.lineHeight+"px "+font.family}else{textStyle=font}layerCache=this._textCache[layer];if(layerCache==null){layerCache=this._textCache[layer]={}}styleCache=layerCache[textStyle];if(styleCache==null){styleCache=layerCache[textStyle]={}}info=styleCache[text];if(info==null){var element=$("<div></div>").html(text).css({position:"absolute","max-width":width,top:-9999}).appendTo(this.getTextLayer(layer));if(typeof font==="object"){element.css({font:textStyle,color:font.color})}else if(typeof font==="string"){element.addClass(font)}info=styleCache[text]={width:element.outerWidth(true),height:element.outerHeight(true),element:element,positions:[]};element.detach()}return info};Canvas.prototype.addText=function(layer,x,y,text,font,angle,width,halign,valign){var info=this.getTextInfo(layer,text,font,angle,width),positions=info.positions;if(halign=="center"){x-=info.width/2}else if(halign=="right"){x-=info.width}if(valign=="middle"){y-=info.height/2}else if(valign=="bottom"){y-=info.height}for(var i=0,position;position=positions[i];i++){if(position.x==x&&position.y==y){position.active=true;return}}position={active:true,rendered:false,element:positions.length?info.element.clone():info.element,x:x,y:y};positions.push(position);position.element.css({top:Math.round(y),left:Math.round(x),"text-align":halign})};Canvas.prototype.removeText=function(layer,x,y,text,font,angle){if(text==null){var layerCache=this._textCache[layer];if(layerCache!=null){for(var styleKey in layerCache){if(hasOwnProperty.call(layerCache,styleKey)){var styleCache=layerCache[styleKey];for(var key in styleCache){if(hasOwnProperty.call(styleCache,key)){var positions=styleCache[key].positions;for(var i=0,position;position=positions[i];i++){position.active=false}}}}}}}else{var positions=this.getTextInfo(layer,text,font,angle).positions;for(var i=0,position;position=positions[i];i++){if(position.x==x&&position.y==y){position.active=false}}}};function Plot(placeholder,data_,options_,plugins){var series=[],options={colors:["#edc240","#afd8f8","#cb4b4b","#4da74d","#9440ed"],legend:{show:true,noColumns:1,labelFormatter:null,labelBoxBorderColor:"#ccc",container:null,position:"ne",margin:5,backgroundColor:null,backgroundOpacity:.85,sorted:null},xaxis:{show:null,position:"bottom",mode:null,font:null,color:null,tickColor:null,transform:null,inverseTransform:null,min:null,max:null,autoscaleMargin:null,ticks:null,tickFormatter:null,labelWidth:null,labelHeight:null,reserveSpace:null,tickLength:null,alignTicksWithAxis:null,tickDecimals:null,tickSize:null,minTickSize:null},yaxis:{autoscaleMargin:.02,position:"left"},xaxes:[],yaxes:[],series:{points:{show:false,radius:3,lineWidth:2,fill:true,fillColor:"#ffffff",symbol:"circle"},lines:{lineWidth:2,fill:false,fillColor:null,steps:false},bars:{show:false,lineWidth:2,barWidth:1,fill:true,fillColor:null,align:"left",horizontal:false,zero:true},shadowSize:3,highlightColor:null},grid:{show:true,aboveData:false,color:"#545454",backgroundColor:null,borderColor:null,tickColor:null,margin:0,labelMargin:5,axisMargin:8,borderWidth:2,minBorderMargin:null,markings:null,markingsColor:"#f4f4f4",markingsLineWidth:2,clickable:false,hoverable:false,autoHighlight:true,mouseActiveRadius:10},interaction:{redrawOverlayInterval:1e3/60},hooks:{}},surface=null,overlay=null,eventHolder=null,ctx=null,octx=null,xaxes=[],yaxes=[],plotOffset={left:0,right:0,top:0,bottom:0},plotWidth=0,plotHeight=0,hooks={processOptions:[],processRawData:[],processDatapoints:[],processOffset:[],drawBackground:[],drawSeries:[],draw:[],bindEvents:[],drawOverlay:[],shutdown:[]},plot=this;plot.setData=setData;plot.setupGrid=setupGrid;plot.draw=draw;plot.getPlaceholder=function(){return placeholder};plot.getCanvas=function(){return surface.element};plot.getPlotOffset=function(){return plotOffset};plot.width=function(){return plotWidth};plot.height=function(){return plotHeight};plot.offset=function(){var o=eventHolder.offset();o.left+=plotOffset.left;o.top+=plotOffset.top;return o};plot.getData=function(){return series};plot.getAxes=function(){var res={},i;$.each(xaxes.concat(yaxes),function(_,axis){if(axis)res[axis.direction+(axis.n!=1?axis.n:"")+"axis"]=axis});return res};plot.getXAxes=function(){return xaxes};plot.getYAxes=function(){return yaxes};plot.c2p=canvasToAxisCoords;plot.p2c=axisToCanvasCoords;plot.getOptions=function(){return options};plot.highlight=highlight;plot.unhighlight=unhighlight;plot.triggerRedrawOverlay=triggerRedrawOverlay;plot.pointOffset=function(point){return{left:parseInt(xaxes[axisNumber(point,"x")-1].p2c(+point.x)+plotOffset.left,10),top:parseInt(yaxes[axisNumber(point,"y")-1].p2c(+point.y)+plotOffset.top,10)}};plot.shutdown=shutdown;plot.destroy=function(){shutdown();placeholder.removeData("plot").empty();series=[];options=null;surface=null;overlay=null;eventHolder=null;ctx=null;octx=null;xaxes=[];yaxes=[];hooks=null;highlights=[];plot=null};plot.resize=function(){var width=placeholder.width(),height=placeholder.height();surface.resize(width,height);overlay.resize(width,height)};plot.hooks=hooks;initPlugins(plot);parseOptions(options_);setupCanvases();setData(data_);setupGrid();draw();bindEvents();function executeHooks(hook,args){args=[plot].concat(args);for(var i=0;i<hook.length;++i)hook[i].apply(this,args)}function initPlugins(){var classes={Canvas:Canvas};for(var i=0;i<plugins.length;++i){var p=plugins[i];p.init(plot,classes);if(p.options)$.extend(true,options,p.options)}}function parseOptions(opts){$.extend(true,options,opts);if(opts&&opts.colors){options.colors=opts.colors}if(options.xaxis.color==null)options.xaxis.color=$.color.parse(options.grid.color).scale("a",.22).toString();if(options.yaxis.color==null)options.yaxis.color=$.color.parse(options.grid.color).scale("a",.22).toString();if(options.xaxis.tickColor==null)options.xaxis.tickColor=options.grid.tickColor||options.xaxis.color;if(options.yaxis.tickColor==null)options.yaxis.tickColor=options.grid.tickColor||options.yaxis.color;if(options.grid.borderColor==null)options.grid.borderColor=options.grid.color;if(options.grid.tickColor==null)options.grid.tickColor=$.color.parse(options.grid.color).scale("a",.22).toString();var i,axisOptions,axisCount,fontSize=placeholder.css("font-size"),fontSizeDefault=fontSize?+fontSize.replace("px",""):13,fontDefaults={style:placeholder.css("font-style"),size:Math.round(.8*fontSizeDefault),variant:placeholder.css("font-variant"),weight:placeholder.css("font-weight"),family:placeholder.css("font-family")};axisCount=options.xaxes.length||1;for(i=0;i<axisCount;++i){axisOptions=options.xaxes[i];if(axisOptions&&!axisOptions.tickColor){axisOptions.tickColor=axisOptions.color}axisOptions=$.extend(true,{},options.xaxis,axisOptions);options.xaxes[i]=axisOptions;if(axisOptions.font){axisOptions.font=$.extend({},fontDefaults,axisOptions.font);if(!axisOptions.font.color){axisOptions.font.color=axisOptions.color}if(!axisOptions.font.lineHeight){axisOptions.font.lineHeight=Math.round(axisOptions.font.size*1.15)}}}axisCount=options.yaxes.length||1;for(i=0;i<axisCount;++i){axisOptions=options.yaxes[i];if(axisOptions&&!axisOptions.tickColor){axisOptions.tickColor=axisOptions.color}axisOptions=$.extend(true,{},options.yaxis,axisOptions);options.yaxes[i]=axisOptions;if(axisOptions.font){axisOptions.font=$.extend({},fontDefaults,axisOptions.font);if(!axisOptions.font.color){axisOptions.font.color=axisOptions.color}if(!axisOptions.font.lineHeight){axisOptions.font.lineHeight=Math.round(axisOptions.font.size*1.15)}}}if(options.xaxis.noTicks&&options.xaxis.ticks==null)options.xaxis.ticks=options.xaxis.noTicks;if(options.yaxis.noTicks&&options.yaxis.ticks==null)options.yaxis.ticks=options.yaxis.noTicks;if(options.x2axis){options.xaxes[1]=$.extend(true,{},options.xaxis,options.x2axis);options.xaxes[1].position="top";if(options.x2axis.min==null){options.xaxes[1].min=null}if(options.x2axis.max==null){options.xaxes[1].max=null}}if(options.y2axis){options.yaxes[1]=$.extend(true,{},options.yaxis,options.y2axis);options.yaxes[1].position="right";if(options.y2axis.min==null){options.yaxes[1].min=null}if(options.y2axis.max==null){options.yaxes[1].max=null}}if(options.grid.coloredAreas)options.grid.markings=options.grid.coloredAreas;if(options.grid.coloredAreasColor)options.grid.markingsColor=options.grid.coloredAreasColor;if(options.lines)$.extend(true,options.series.lines,options.lines);if(options.points)$.extend(true,options.series.points,options.points);if(options.bars)$.extend(true,options.series.bars,options.bars);if(options.shadowSize!=null)options.series.shadowSize=options.shadowSize;if(options.highlightColor!=null)options.series.highlightColor=options.highlightColor;for(i=0;i<options.xaxes.length;++i)getOrCreateAxis(xaxes,i+1).options=options.xaxes[i];for(i=0;i<options.yaxes.length;++i)getOrCreateAxis(yaxes,i+1).options=options.yaxes[i];for(var n in hooks)if(options.hooks[n]&&options.hooks[n].length)hooks[n]=hooks[n].concat(options.hooks[n]);executeHooks(hooks.processOptions,[options])}function setData(d){series=parseData(d);fillInSeriesOptions();processData()}function parseData(d){var res=[];for(var i=0;i<d.length;++i){var s=$.extend(true,{},options.series);if(d[i].data!=null){s.data=d[i].data;delete d[i].data;$.extend(true,s,d[i]);d[i].data=s.data}else s.data=d[i];res.push(s)}return res}function axisNumber(obj,coord){var a=obj[coord+"axis"];if(typeof a=="object")a=a.n;if(typeof a!="number")a=1;return a}function allAxes(){return $.grep(xaxes.concat(yaxes),function(a){return a})}function canvasToAxisCoords(pos){var res={},i,axis;for(i=0;i<xaxes.length;++i){axis=xaxes[i];if(axis&&axis.used)res["x"+axis.n]=axis.c2p(pos.left)}for(i=0;i<yaxes.length;++i){axis=yaxes[i];if(axis&&axis.used)res["y"+axis.n]=axis.c2p(pos.top)}if(res.x1!==undefined)res.x=res.x1;if(res.y1!==undefined)res.y=res.y1;return res}function axisToCanvasCoords(pos){var res={},i,axis,key;for(i=0;i<xaxes.length;++i){axis=xaxes[i];if(axis&&axis.used){key="x"+axis.n;if(pos[key]==null&&axis.n==1)key="x";if(pos[key]!=null){res.left=axis.p2c(pos[key]);break}}}for(i=0;i<yaxes.length;++i){axis=yaxes[i];if(axis&&axis.used){key="y"+axis.n;if(pos[key]==null&&axis.n==1)key="y";if(pos[key]!=null){res.top=axis.p2c(pos[key]);break}}}return res}function getOrCreateAxis(axes,number){if(!axes[number-1])axes[number-1]={n:number,direction:axes==xaxes?"x":"y",options:$.extend(true,{},axes==xaxes?options.xaxis:options.yaxis)};return axes[number-1]}function fillInSeriesOptions(){var neededColors=series.length,maxIndex=-1,i;for(i=0;i<series.length;++i){var sc=series[i].color;if(sc!=null){neededColors--;if(typeof sc=="number"&&sc>maxIndex){maxIndex=sc}}}if(neededColors<=maxIndex){neededColors=maxIndex+1}var c,colors=[],colorPool=options.colors,colorPoolSize=colorPool.length,variation=0;for(i=0;i<neededColors;i++){c=$.color.parse(colorPool[i%colorPoolSize]||"#666");if(i%colorPoolSize==0&&i){if(variation>=0){if(variation<.5){variation=-variation-.2}else variation=0}else variation=-variation}colors[i]=c.scale("rgb",1+variation)}var colori=0,s;for(i=0;i<series.length;++i){s=series[i];if(s.color==null){s.color=colors[colori].toString();++colori}else if(typeof s.color=="number")s.color=colors[s.color].toString();if(s.lines.show==null){var v,show=true;for(v in s)if(s[v]&&s[v].show){show=false;break}if(show)s.lines.show=true}if(s.lines.zero==null){s.lines.zero=!!s.lines.fill}s.xaxis=getOrCreateAxis(xaxes,axisNumber(s,"x"));s.yaxis=getOrCreateAxis(yaxes,axisNumber(s,"y"))}}function processData(){var topSentry=Number.POSITIVE_INFINITY,bottomSentry=Number.NEGATIVE_INFINITY,fakeInfinity=Number.MAX_VALUE,i,j,k,m,length,s,points,ps,x,y,axis,val,f,p,data,format;function updateAxis(axis,min,max){if(min<axis.datamin&&min!=-fakeInfinity)axis.datamin=min;if(max>axis.datamax&&max!=fakeInfinity)axis.datamax=max}$.each(allAxes(),function(_,axis){axis.datamin=topSentry;axis.datamax=bottomSentry;axis.used=false});for(i=0;i<series.length;++i){s=series[i];s.datapoints={points:[]};executeHooks(hooks.processRawData,[s,s.data,s.datapoints])}for(i=0;i<series.length;++i){s=series[i];data=s.data;format=s.datapoints.format;if(!format){format=[];format.push({x:true,number:true,required:true});format.push({y:true,number:true,required:true});if(s.bars.show||s.lines.show&&s.lines.fill){var autoscale=!!(s.bars.show&&s.bars.zero||s.lines.show&&s.lines.zero);format.push({y:true,number:true,required:false,defaultValue:0,autoscale:autoscale});if(s.bars.horizontal){delete format[format.length-1].y;format[format.length-1].x=true}}s.datapoints.format=format}if(s.datapoints.pointsize!=null)continue;s.datapoints.pointsize=format.length;ps=s.datapoints.pointsize;points=s.datapoints.points;var insertSteps=s.lines.show&&s.lines.steps;s.xaxis.used=s.yaxis.used=true;for(j=k=0;j<data.length;++j,k+=ps){p=data[j];var nullify=p==null;if(!nullify){for(m=0;m<ps;++m){val=p[m];f=format[m];if(f){if(f.number&&val!=null){val=+val;if(isNaN(val))val=null;else if(val==Infinity)val=fakeInfinity;else if(val==-Infinity)val=-fakeInfinity}if(val==null){if(f.required)nullify=true;if(f.defaultValue!=null)val=f.defaultValue}}points[k+m]=val}}if(nullify){for(m=0;m<ps;++m){val=points[k+m];if(val!=null){f=format[m];if(f.autoscale!==false){if(f.x){updateAxis(s.xaxis,val,val)}if(f.y){updateAxis(s.yaxis,val,val)}}}points[k+m]=null}}else{if(insertSteps&&k>0&&points[k-ps]!=null&&points[k-ps]!=points[k]&&points[k-ps+1]!=points[k+1]){for(m=0;m<ps;++m)points[k+ps+m]=points[k+m];points[k+1]=points[k-ps+1];k+=ps}}}}for(i=0;i<series.length;++i){s=series[i];executeHooks(hooks.processDatapoints,[s,s.datapoints])}for(i=0;i<series.length;++i){s=series[i];points=s.datapoints.points;ps=s.datapoints.pointsize;format=s.datapoints.format;var xmin=topSentry,ymin=topSentry,xmax=bottomSentry,ymax=bottomSentry;for(j=0;j<points.length;j+=ps){if(points[j]==null)continue;for(m=0;m<ps;++m){val=points[j+m];f=format[m];if(!f||f.autoscale===false||val==fakeInfinity||val==-fakeInfinity)continue;if(f.x){if(val<xmin)xmin=val;if(val>xmax)xmax=val}if(f.y){if(val<ymin)ymin=val;if(val>ymax)ymax=val}}}if(s.bars.show){var delta;switch(s.bars.align){case"left":delta=0;break;case"right":delta=-s.bars.barWidth;break;default:delta=-s.bars.barWidth/2}if(s.bars.horizontal){ymin+=delta;ymax+=delta+s.bars.barWidth}else{xmin+=delta;xmax+=delta+s.bars.barWidth}}updateAxis(s.xaxis,xmin,xmax);updateAxis(s.yaxis,ymin,ymax)}$.each(allAxes(),function(_,axis){if(axis.datamin==topSentry)axis.datamin=null;if(axis.datamax==bottomSentry)axis.datamax=null})}function setupCanvases(){placeholder.css("padding",0).children().filter(function(){return!$(this).hasClass("flot-overlay")&&!$(this).hasClass("flot-base")}).remove();if(placeholder.css("position")=="static")placeholder.css("position","relative");surface=new Canvas("flot-base",placeholder);overlay=new Canvas("flot-overlay",placeholder);ctx=surface.context;octx=overlay.context;eventHolder=$(overlay.element).unbind();var existing=placeholder.data("plot");if(existing){existing.shutdown();overlay.clear()}placeholder.data("plot",plot)}function bindEvents(){if(options.grid.hoverable){eventHolder.mousemove(onMouseMove);eventHolder.bind("mouseleave",onMouseLeave)}if(options.grid.clickable)eventHolder.click(onClick);executeHooks(hooks.bindEvents,[eventHolder])}function shutdown(){if(redrawTimeout)clearTimeout(redrawTimeout);eventHolder.unbind("mousemove",onMouseMove);eventHolder.unbind("mouseleave",onMouseLeave);eventHolder.unbind("click",onClick);executeHooks(hooks.shutdown,[eventHolder])}function setTransformationHelpers(axis){function identity(x){return x}var s,m,t=axis.options.transform||identity,it=axis.options.inverseTransform;if(axis.direction=="x"){s=axis.scale=plotWidth/Math.abs(t(axis.max)-t(axis.min));m=Math.min(t(axis.max),t(axis.min))}else{s=axis.scale=plotHeight/Math.abs(t(axis.max)-t(axis.min));s=-s;m=Math.max(t(axis.max),t(axis.min))}if(t==identity)axis.p2c=function(p){return(p-m)*s};else axis.p2c=function(p){return(t(p)-m)*s};if(!it)axis.c2p=function(c){return m+c/s};else axis.c2p=function(c){return it(m+c/s)}}function measureTickLabels(axis){var opts=axis.options,ticks=axis.ticks||[],labelWidth=opts.labelWidth||0,labelHeight=opts.labelHeight||0,maxWidth=labelWidth||(axis.direction=="x"?Math.floor(surface.width/(ticks.length||1)):null),legacyStyles=axis.direction+"Axis "+axis.direction+axis.n+"Axis",layer="flot-"+axis.direction+"-axis flot-"+axis.direction+axis.n+"-axis "+legacyStyles,font=opts.font||"flot-tick-label tickLabel";for(var i=0;i<ticks.length;++i){var t=ticks[i];if(!t.label)continue;var info=surface.getTextInfo(layer,t.label,font,null,maxWidth);labelWidth=Math.max(labelWidth,info.width);labelHeight=Math.max(labelHeight,info.height)}axis.labelWidth=opts.labelWidth||labelWidth;axis.labelHeight=opts.labelHeight||labelHeight}function allocateAxisBoxFirstPhase(axis){var lw=axis.labelWidth,lh=axis.labelHeight,pos=axis.options.position,isXAxis=axis.direction==="x",tickLength=axis.options.tickLength,axisMargin=options.grid.axisMargin,padding=options.grid.labelMargin,innermost=true,outermost=true,first=true,found=false;$.each(isXAxis?xaxes:yaxes,function(i,a){if(a&&(a.show||a.reserveSpace)){if(a===axis){found=true}else if(a.options.position===pos){if(found){outermost=false}else{innermost=false}}if(!found){first=false}}});if(outermost){axisMargin=0}if(tickLength==null){tickLength=first?"full":5}if(!isNaN(+tickLength))padding+=+tickLength;if(isXAxis){lh+=padding;if(pos=="bottom"){plotOffset.bottom+=lh+axisMargin;axis.box={top:surface.height-plotOffset.bottom,height:lh}}else{axis.box={top:plotOffset.top+axisMargin,height:lh};plotOffset.top+=lh+axisMargin}}else{lw+=padding;if(pos=="left"){axis.box={left:plotOffset.left+axisMargin,width:lw};plotOffset.left+=lw+axisMargin}else{plotOffset.right+=lw+axisMargin;axis.box={left:surface.width-plotOffset.right,width:lw}}}axis.position=pos;axis.tickLength=tickLength;axis.box.padding=padding;axis.innermost=innermost}function allocateAxisBoxSecondPhase(axis){if(axis.direction=="x"){axis.box.left=plotOffset.left-axis.labelWidth/2;axis.box.width=surface.width-plotOffset.left-plotOffset.right+axis.labelWidth}else{axis.box.top=plotOffset.top-axis.labelHeight/2;axis.box.height=surface.height-plotOffset.bottom-plotOffset.top+axis.labelHeight}}function adjustLayoutForThingsStickingOut(){var minMargin=options.grid.minBorderMargin,axis,i;if(minMargin==null){minMargin=0;for(i=0;i<series.length;++i)minMargin=Math.max(minMargin,2*(series[i].points.radius+series[i].points.lineWidth/2))}var margins={left:minMargin,right:minMargin,top:minMargin,bottom:minMargin};$.each(allAxes(),function(_,axis){if(axis.reserveSpace&&axis.ticks&&axis.ticks.length){if(axis.direction==="x"){margins.left=Math.max(margins.left,axis.labelWidth/2);margins.right=Math.max(margins.right,axis.labelWidth/2)}else{margins.bottom=Math.max(margins.bottom,axis.labelHeight/2);margins.top=Math.max(margins.top,axis.labelHeight/2)}}});plotOffset.left=Math.ceil(Math.max(margins.left,plotOffset.left));plotOffset.right=Math.ceil(Math.max(margins.right,plotOffset.right));plotOffset.top=Math.ceil(Math.max(margins.top,plotOffset.top));plotOffset.bottom=Math.ceil(Math.max(margins.bottom,plotOffset.bottom))}function setupGrid(){var i,axes=allAxes(),showGrid=options.grid.show;for(var a in plotOffset){var margin=options.grid.margin||0;plotOffset[a]=typeof margin=="number"?margin:margin[a]||0}executeHooks(hooks.processOffset,[plotOffset]);for(var a in plotOffset){if(typeof options.grid.borderWidth=="object"){plotOffset[a]+=showGrid?options.grid.borderWidth[a]:0}else{plotOffset[a]+=showGrid?options.grid.borderWidth:0}}$.each(axes,function(_,axis){var axisOpts=axis.options;axis.show=axisOpts.show==null?axis.used:axisOpts.show;axis.reserveSpace=axisOpts.reserveSpace==null?axis.show:axisOpts.reserveSpace;setRange(axis)});if(showGrid){var allocatedAxes=$.grep(axes,function(axis){return axis.show||axis.reserveSpace});$.each(allocatedAxes,function(_,axis){setupTickGeneration(axis);setTicks(axis);snapRangeToTicks(axis,axis.ticks);measureTickLabels(axis)});for(i=allocatedAxes.length-1;i>=0;--i)allocateAxisBoxFirstPhase(allocatedAxes[i]);adjustLayoutForThingsStickingOut();$.each(allocatedAxes,function(_,axis){allocateAxisBoxSecondPhase(axis)})}plotWidth=surface.width-plotOffset.left-plotOffset.right;plotHeight=surface.height-plotOffset.bottom-plotOffset.top;$.each(axes,function(_,axis){setTransformationHelpers(axis)});if(showGrid){drawAxisLabels()}insertLegend()}function setRange(axis){var opts=axis.options,min=+(opts.min!=null?opts.min:axis.datamin),max=+(opts.max!=null?opts.max:axis.datamax),delta=max-min;if(delta==0){var widen=max==0?1:.01;if(opts.min==null)min-=widen;if(opts.max==null||opts.min!=null)max+=widen}else{var margin=opts.autoscaleMargin;if(margin!=null){if(opts.min==null){min-=delta*margin;if(min<0&&axis.datamin!=null&&axis.datamin>=0)min=0}if(opts.max==null){max+=delta*margin;if(max>0&&axis.datamax!=null&&axis.datamax<=0)max=0}}}axis.min=min;axis.max=max}function setupTickGeneration(axis){var opts=axis.options;var noTicks;if(typeof opts.ticks=="number"&&opts.ticks>0)noTicks=opts.ticks;else noTicks=.3*Math.sqrt(axis.direction=="x"?surface.width:surface.height);var delta=(axis.max-axis.min)/noTicks,dec=-Math.floor(Math.log(delta)/Math.LN10),maxDec=opts.tickDecimals;if(maxDec!=null&&dec>maxDec){dec=maxDec}var magn=Math.pow(10,-dec),norm=delta/magn,size;if(norm<1.5){size=1}else if(norm<3){size=2;if(norm>2.25&&(maxDec==null||dec+1<=maxDec)){size=2.5;++dec}}else if(norm<7.5){size=5}else{size=10}size*=magn;if(opts.minTickSize!=null&&size<opts.minTickSize){size=opts.minTickSize}axis.delta=delta;axis.tickDecimals=Math.max(0,maxDec!=null?maxDec:dec);axis.tickSize=opts.tickSize||size;if(opts.mode=="time"&&!axis.tickGenerator){throw new Error("Time mode requires the flot.time plugin.")}if(!axis.tickGenerator){axis.tickGenerator=function(axis){var ticks=[],start=floorInBase(axis.min,axis.tickSize),i=0,v=Number.NaN,prev;do{prev=v;v=start+i*axis.tickSize;ticks.push(v);++i}while(v<axis.max&&v!=prev);return ticks};axis.tickFormatter=function(value,axis){var factor=axis.tickDecimals?Math.pow(10,axis.tickDecimals):1;var formatted=""+Math.round(value*factor)/factor;if(axis.tickDecimals!=null){var decimal=formatted.indexOf(".");var precision=decimal==-1?0:formatted.length-decimal-1;if(precision<axis.tickDecimals){return(precision?formatted:formatted+".")+(""+factor).substr(1,axis.tickDecimals-precision)}}return formatted}}if($.isFunction(opts.tickFormatter))axis.tickFormatter=function(v,axis){return""+opts.tickFormatter(v,axis)};if(opts.alignTicksWithAxis!=null){var otherAxis=(axis.direction=="x"?xaxes:yaxes)[opts.alignTicksWithAxis-1];if(otherAxis&&otherAxis.used&&otherAxis!=axis){var niceTicks=axis.tickGenerator(axis);if(niceTicks.length>0){if(opts.min==null)axis.min=Math.min(axis.min,niceTicks[0]);if(opts.max==null&&niceTicks.length>1)axis.max=Math.max(axis.max,niceTicks[niceTicks.length-1])}axis.tickGenerator=function(axis){var ticks=[],v,i;for(i=0;i<otherAxis.ticks.length;++i){v=(otherAxis.ticks[i].v-otherAxis.min)/(otherAxis.max-otherAxis.min);v=axis.min+v*(axis.max-axis.min);ticks.push(v)}return ticks};if(!axis.mode&&opts.tickDecimals==null){var extraDec=Math.max(0,-Math.floor(Math.log(axis.delta)/Math.LN10)+1),ts=axis.tickGenerator(axis);if(!(ts.length>1&&/\..*0$/.test((ts[1]-ts[0]).toFixed(extraDec))))axis.tickDecimals=extraDec}}}}function setTicks(axis){var oticks=axis.options.ticks,ticks=[];if(oticks==null||typeof oticks=="number"&&oticks>0)ticks=axis.tickGenerator(axis);else if(oticks){if($.isFunction(oticks))ticks=oticks(axis);else ticks=oticks}var i,v;axis.ticks=[];for(i=0;i<ticks.length;++i){var label=null;var t=ticks[i];if(typeof t=="object"){v=+t[0];if(t.length>1)label=t[1]}else v=+t;if(label==null)label=axis.tickFormatter(v,axis);if(!isNaN(v))axis.ticks.push({v:v,label:label})}}function snapRangeToTicks(axis,ticks){if(axis.options.autoscaleMargin&&ticks.length>0){if(axis.options.min==null)axis.min=Math.min(axis.min,ticks[0].v);if(axis.options.max==null&&ticks.length>1)axis.max=Math.max(axis.max,ticks[ticks.length-1].v)}}function draw(){surface.clear();executeHooks(hooks.drawBackground,[ctx]);var grid=options.grid;if(grid.show&&grid.backgroundColor)drawBackground();if(grid.show&&!grid.aboveData){drawGrid()}for(var i=0;i<series.length;++i){executeHooks(hooks.drawSeries,[ctx,series[i]]);drawSeries(series[i])}executeHooks(hooks.draw,[ctx]);if(grid.show&&grid.aboveData){drawGrid()}surface.render();triggerRedrawOverlay()}function extractRange(ranges,coord){var axis,from,to,key,axes=allAxes();for(var i=0;i<axes.length;++i){axis=axes[i];if(axis.direction==coord){key=coord+axis.n+"axis";if(!ranges[key]&&axis.n==1)key=coord+"axis";if(ranges[key]){from=ranges[key].from;to=ranges[key].to;break}}}if(!ranges[key]){axis=coord=="x"?xaxes[0]:yaxes[0];from=ranges[coord+"1"];to=ranges[coord+"2"]}if(from!=null&&to!=null&&from>to){var tmp=from;from=to;to=tmp}return{from:from,to:to,axis:axis}}function drawBackground(){ctx.save();ctx.translate(plotOffset.left,plotOffset.top);ctx.fillStyle=getColorOrGradient(options.grid.backgroundColor,plotHeight,0,"rgba(255, 255, 255, 0)");ctx.fillRect(0,0,plotWidth,plotHeight);ctx.restore()}function drawGrid(){var i,axes,bw,bc;ctx.save();ctx.translate(plotOffset.left,plotOffset.top);var markings=options.grid.markings;if(markings){if($.isFunction(markings)){axes=plot.getAxes();axes.xmin=axes.xaxis.min;axes.xmax=axes.xaxis.max;axes.ymin=axes.yaxis.min;axes.ymax=axes.yaxis.max;markings=markings(axes)}for(i=0;i<markings.length;++i){var m=markings[i],xrange=extractRange(m,"x"),yrange=extractRange(m,"y");if(xrange.from==null)xrange.from=xrange.axis.min;if(xrange.to==null)xrange.to=xrange.axis.max;
+if(yrange.from==null)yrange.from=yrange.axis.min;if(yrange.to==null)yrange.to=yrange.axis.max;if(xrange.to<xrange.axis.min||xrange.from>xrange.axis.max||yrange.to<yrange.axis.min||yrange.from>yrange.axis.max)continue;xrange.from=Math.max(xrange.from,xrange.axis.min);xrange.to=Math.min(xrange.to,xrange.axis.max);yrange.from=Math.max(yrange.from,yrange.axis.min);yrange.to=Math.min(yrange.to,yrange.axis.max);var xequal=xrange.from===xrange.to,yequal=yrange.from===yrange.to;if(xequal&&yequal){continue}xrange.from=Math.floor(xrange.axis.p2c(xrange.from));xrange.to=Math.floor(xrange.axis.p2c(xrange.to));yrange.from=Math.floor(yrange.axis.p2c(yrange.from));yrange.to=Math.floor(yrange.axis.p2c(yrange.to));if(xequal||yequal){var lineWidth=m.lineWidth||options.grid.markingsLineWidth,subPixel=lineWidth%2?.5:0;ctx.beginPath();ctx.strokeStyle=m.color||options.grid.markingsColor;ctx.lineWidth=lineWidth;if(xequal){ctx.moveTo(xrange.to+subPixel,yrange.from);ctx.lineTo(xrange.to+subPixel,yrange.to)}else{ctx.moveTo(xrange.from,yrange.to+subPixel);ctx.lineTo(xrange.to,yrange.to+subPixel)}ctx.stroke()}else{ctx.fillStyle=m.color||options.grid.markingsColor;ctx.fillRect(xrange.from,yrange.to,xrange.to-xrange.from,yrange.from-yrange.to)}}}axes=allAxes();bw=options.grid.borderWidth;for(var j=0;j<axes.length;++j){var axis=axes[j],box=axis.box,t=axis.tickLength,x,y,xoff,yoff;if(!axis.show||axis.ticks.length==0)continue;ctx.lineWidth=1;if(axis.direction=="x"){x=0;if(t=="full")y=axis.position=="top"?0:plotHeight;else y=box.top-plotOffset.top+(axis.position=="top"?box.height:0)}else{y=0;if(t=="full")x=axis.position=="left"?0:plotWidth;else x=box.left-plotOffset.left+(axis.position=="left"?box.width:0)}if(!axis.innermost){ctx.strokeStyle=axis.options.color;ctx.beginPath();xoff=yoff=0;if(axis.direction=="x")xoff=plotWidth+1;else yoff=plotHeight+1;if(ctx.lineWidth==1){if(axis.direction=="x"){y=Math.floor(y)+.5}else{x=Math.floor(x)+.5}}ctx.moveTo(x,y);ctx.lineTo(x+xoff,y+yoff);ctx.stroke()}ctx.strokeStyle=axis.options.tickColor;ctx.beginPath();for(i=0;i<axis.ticks.length;++i){var v=axis.ticks[i].v;xoff=yoff=0;if(isNaN(v)||v<axis.min||v>axis.max||t=="full"&&(typeof bw=="object"&&bw[axis.position]>0||bw>0)&&(v==axis.min||v==axis.max))continue;if(axis.direction=="x"){x=axis.p2c(v);yoff=t=="full"?-plotHeight:t;if(axis.position=="top")yoff=-yoff}else{y=axis.p2c(v);xoff=t=="full"?-plotWidth:t;if(axis.position=="left")xoff=-xoff}if(ctx.lineWidth==1){if(axis.direction=="x")x=Math.floor(x)+.5;else y=Math.floor(y)+.5}ctx.moveTo(x,y);ctx.lineTo(x+xoff,y+yoff)}ctx.stroke()}if(bw){bc=options.grid.borderColor;if(typeof bw=="object"||typeof bc=="object"){if(typeof bw!=="object"){bw={top:bw,right:bw,bottom:bw,left:bw}}if(typeof bc!=="object"){bc={top:bc,right:bc,bottom:bc,left:bc}}if(bw.top>0){ctx.strokeStyle=bc.top;ctx.lineWidth=bw.top;ctx.beginPath();ctx.moveTo(0-bw.left,0-bw.top/2);ctx.lineTo(plotWidth,0-bw.top/2);ctx.stroke()}if(bw.right>0){ctx.strokeStyle=bc.right;ctx.lineWidth=bw.right;ctx.beginPath();ctx.moveTo(plotWidth+bw.right/2,0-bw.top);ctx.lineTo(plotWidth+bw.right/2,plotHeight);ctx.stroke()}if(bw.bottom>0){ctx.strokeStyle=bc.bottom;ctx.lineWidth=bw.bottom;ctx.beginPath();ctx.moveTo(plotWidth+bw.right,plotHeight+bw.bottom/2);ctx.lineTo(0,plotHeight+bw.bottom/2);ctx.stroke()}if(bw.left>0){ctx.strokeStyle=bc.left;ctx.lineWidth=bw.left;ctx.beginPath();ctx.moveTo(0-bw.left/2,plotHeight+bw.bottom);ctx.lineTo(0-bw.left/2,0);ctx.stroke()}}else{ctx.lineWidth=bw;ctx.strokeStyle=options.grid.borderColor;ctx.strokeRect(-bw/2,-bw/2,plotWidth+bw,plotHeight+bw)}}ctx.restore()}function drawAxisLabels(){$.each(allAxes(),function(_,axis){var box=axis.box,legacyStyles=axis.direction+"Axis "+axis.direction+axis.n+"Axis",layer="flot-"+axis.direction+"-axis flot-"+axis.direction+axis.n+"-axis "+legacyStyles,font=axis.options.font||"flot-tick-label tickLabel",tick,x,y,halign,valign;surface.removeText(layer);if(!axis.show||axis.ticks.length==0)return;for(var i=0;i<axis.ticks.length;++i){tick=axis.ticks[i];if(!tick.label||tick.v<axis.min||tick.v>axis.max)continue;if(axis.direction=="x"){halign="center";x=plotOffset.left+axis.p2c(tick.v);if(axis.position=="bottom"){y=box.top+box.padding}else{y=box.top+box.height-box.padding;valign="bottom"}}else{valign="middle";y=plotOffset.top+axis.p2c(tick.v);if(axis.position=="left"){x=box.left+box.width-box.padding;halign="right"}else{x=box.left+box.padding}}surface.addText(layer,x,y,tick.label,font,null,null,halign,valign)}})}function drawSeries(series){if(series.lines.show)drawSeriesLines(series);if(series.bars.show)drawSeriesBars(series);if(series.points.show)drawSeriesPoints(series)}function drawSeriesLines(series){function plotLine(datapoints,xoffset,yoffset,axisx,axisy){var points=datapoints.points,ps=datapoints.pointsize,prevx=null,prevy=null;ctx.beginPath();for(var i=ps;i<points.length;i+=ps){var x1=points[i-ps],y1=points[i-ps+1],x2=points[i],y2=points[i+1];if(x1==null||x2==null)continue;if(y1<=y2&&y1<axisy.min){if(y2<axisy.min)continue;x1=(axisy.min-y1)/(y2-y1)*(x2-x1)+x1;y1=axisy.min}else if(y2<=y1&&y2<axisy.min){if(y1<axisy.min)continue;x2=(axisy.min-y1)/(y2-y1)*(x2-x1)+x1;y2=axisy.min}if(y1>=y2&&y1>axisy.max){if(y2>axisy.max)continue;x1=(axisy.max-y1)/(y2-y1)*(x2-x1)+x1;y1=axisy.max}else if(y2>=y1&&y2>axisy.max){if(y1>axisy.max)continue;x2=(axisy.max-y1)/(y2-y1)*(x2-x1)+x1;y2=axisy.max}if(x1<=x2&&x1<axisx.min){if(x2<axisx.min)continue;y1=(axisx.min-x1)/(x2-x1)*(y2-y1)+y1;x1=axisx.min}else if(x2<=x1&&x2<axisx.min){if(x1<axisx.min)continue;y2=(axisx.min-x1)/(x2-x1)*(y2-y1)+y1;x2=axisx.min}if(x1>=x2&&x1>axisx.max){if(x2>axisx.max)continue;y1=(axisx.max-x1)/(x2-x1)*(y2-y1)+y1;x1=axisx.max}else if(x2>=x1&&x2>axisx.max){if(x1>axisx.max)continue;y2=(axisx.max-x1)/(x2-x1)*(y2-y1)+y1;x2=axisx.max}if(x1!=prevx||y1!=prevy)ctx.moveTo(axisx.p2c(x1)+xoffset,axisy.p2c(y1)+yoffset);prevx=x2;prevy=y2;ctx.lineTo(axisx.p2c(x2)+xoffset,axisy.p2c(y2)+yoffset)}ctx.stroke()}function plotLineArea(datapoints,axisx,axisy){var points=datapoints.points,ps=datapoints.pointsize,bottom=Math.min(Math.max(0,axisy.min),axisy.max),i=0,top,areaOpen=false,ypos=1,segmentStart=0,segmentEnd=0;while(true){if(ps>0&&i>points.length+ps)break;i+=ps;var x1=points[i-ps],y1=points[i-ps+ypos],x2=points[i],y2=points[i+ypos];if(areaOpen){if(ps>0&&x1!=null&&x2==null){segmentEnd=i;ps=-ps;ypos=2;continue}if(ps<0&&i==segmentStart+ps){ctx.fill();areaOpen=false;ps=-ps;ypos=1;i=segmentStart=segmentEnd+ps;continue}}if(x1==null||x2==null)continue;if(x1<=x2&&x1<axisx.min){if(x2<axisx.min)continue;y1=(axisx.min-x1)/(x2-x1)*(y2-y1)+y1;x1=axisx.min}else if(x2<=x1&&x2<axisx.min){if(x1<axisx.min)continue;y2=(axisx.min-x1)/(x2-x1)*(y2-y1)+y1;x2=axisx.min}if(x1>=x2&&x1>axisx.max){if(x2>axisx.max)continue;y1=(axisx.max-x1)/(x2-x1)*(y2-y1)+y1;x1=axisx.max}else if(x2>=x1&&x2>axisx.max){if(x1>axisx.max)continue;y2=(axisx.max-x1)/(x2-x1)*(y2-y1)+y1;x2=axisx.max}if(!areaOpen){ctx.beginPath();ctx.moveTo(axisx.p2c(x1),axisy.p2c(bottom));areaOpen=true}if(y1>=axisy.max&&y2>=axisy.max){ctx.lineTo(axisx.p2c(x1),axisy.p2c(axisy.max));ctx.lineTo(axisx.p2c(x2),axisy.p2c(axisy.max));continue}else if(y1<=axisy.min&&y2<=axisy.min){ctx.lineTo(axisx.p2c(x1),axisy.p2c(axisy.min));ctx.lineTo(axisx.p2c(x2),axisy.p2c(axisy.min));continue}var x1old=x1,x2old=x2;if(y1<=y2&&y1<axisy.min&&y2>=axisy.min){x1=(axisy.min-y1)/(y2-y1)*(x2-x1)+x1;y1=axisy.min}else if(y2<=y1&&y2<axisy.min&&y1>=axisy.min){x2=(axisy.min-y1)/(y2-y1)*(x2-x1)+x1;y2=axisy.min}if(y1>=y2&&y1>axisy.max&&y2<=axisy.max){x1=(axisy.max-y1)/(y2-y1)*(x2-x1)+x1;y1=axisy.max}else if(y2>=y1&&y2>axisy.max&&y1<=axisy.max){x2=(axisy.max-y1)/(y2-y1)*(x2-x1)+x1;y2=axisy.max}if(x1!=x1old){ctx.lineTo(axisx.p2c(x1old),axisy.p2c(y1))}ctx.lineTo(axisx.p2c(x1),axisy.p2c(y1));ctx.lineTo(axisx.p2c(x2),axisy.p2c(y2));if(x2!=x2old){ctx.lineTo(axisx.p2c(x2),axisy.p2c(y2));ctx.lineTo(axisx.p2c(x2old),axisy.p2c(y2))}}}ctx.save();ctx.translate(plotOffset.left,plotOffset.top);ctx.lineJoin="round";var lw=series.lines.lineWidth,sw=series.shadowSize;if(lw>0&&sw>0){ctx.lineWidth=sw;ctx.strokeStyle="rgba(0,0,0,0.1)";var angle=Math.PI/18;plotLine(series.datapoints,Math.sin(angle)*(lw/2+sw/2),Math.cos(angle)*(lw/2+sw/2),series.xaxis,series.yaxis);ctx.lineWidth=sw/2;plotLine(series.datapoints,Math.sin(angle)*(lw/2+sw/4),Math.cos(angle)*(lw/2+sw/4),series.xaxis,series.yaxis)}ctx.lineWidth=lw;ctx.strokeStyle=series.color;var fillStyle=getFillStyle(series.lines,series.color,0,plotHeight);if(fillStyle){ctx.fillStyle=fillStyle;plotLineArea(series.datapoints,series.xaxis,series.yaxis)}if(lw>0)plotLine(series.datapoints,0,0,series.xaxis,series.yaxis);ctx.restore()}function drawSeriesPoints(series){function plotPoints(datapoints,radius,fillStyle,offset,shadow,axisx,axisy,symbol){var points=datapoints.points,ps=datapoints.pointsize;for(var i=0;i<points.length;i+=ps){var x=points[i],y=points[i+1];if(x==null||x<axisx.min||x>axisx.max||y<axisy.min||y>axisy.max)continue;ctx.beginPath();x=axisx.p2c(x);y=axisy.p2c(y)+offset;if(symbol=="circle")ctx.arc(x,y,radius,0,shadow?Math.PI:Math.PI*2,false);else symbol(ctx,x,y,radius,shadow);ctx.closePath();if(fillStyle){ctx.fillStyle=fillStyle;ctx.fill()}ctx.stroke()}}ctx.save();ctx.translate(plotOffset.left,plotOffset.top);var lw=series.points.lineWidth,sw=series.shadowSize,radius=series.points.radius,symbol=series.points.symbol;if(lw==0)lw=1e-4;if(lw>0&&sw>0){var w=sw/2;ctx.lineWidth=w;ctx.strokeStyle="rgba(0,0,0,0.1)";plotPoints(series.datapoints,radius,null,w+w/2,true,series.xaxis,series.yaxis,symbol);ctx.strokeStyle="rgba(0,0,0,0.2)";plotPoints(series.datapoints,radius,null,w/2,true,series.xaxis,series.yaxis,symbol)}ctx.lineWidth=lw;ctx.strokeStyle=series.color;plotPoints(series.datapoints,radius,getFillStyle(series.points,series.color),0,false,series.xaxis,series.yaxis,symbol);ctx.restore()}function drawBar(x,y,b,barLeft,barRight,fillStyleCallback,axisx,axisy,c,horizontal,lineWidth){var left,right,bottom,top,drawLeft,drawRight,drawTop,drawBottom,tmp;if(horizontal){drawBottom=drawRight=drawTop=true;drawLeft=false;left=b;right=x;top=y+barLeft;bottom=y+barRight;if(right<left){tmp=right;right=left;left=tmp;drawLeft=true;drawRight=false}}else{drawLeft=drawRight=drawTop=true;drawBottom=false;left=x+barLeft;right=x+barRight;bottom=b;top=y;if(top<bottom){tmp=top;top=bottom;bottom=tmp;drawBottom=true;drawTop=false}}if(right<axisx.min||left>axisx.max||top<axisy.min||bottom>axisy.max)return;if(left<axisx.min){left=axisx.min;drawLeft=false}if(right>axisx.max){right=axisx.max;drawRight=false}if(bottom<axisy.min){bottom=axisy.min;drawBottom=false}if(top>axisy.max){top=axisy.max;drawTop=false}left=axisx.p2c(left);bottom=axisy.p2c(bottom);right=axisx.p2c(right);top=axisy.p2c(top);if(fillStyleCallback){c.fillStyle=fillStyleCallback(bottom,top);c.fillRect(left,top,right-left,bottom-top)}if(lineWidth>0&&(drawLeft||drawRight||drawTop||drawBottom)){c.beginPath();c.moveTo(left,bottom);if(drawLeft)c.lineTo(left,top);else c.moveTo(left,top);if(drawTop)c.lineTo(right,top);else c.moveTo(right,top);if(drawRight)c.lineTo(right,bottom);else c.moveTo(right,bottom);if(drawBottom)c.lineTo(left,bottom);else c.moveTo(left,bottom);c.stroke()}}function drawSeriesBars(series){function plotBars(datapoints,barLeft,barRight,fillStyleCallback,axisx,axisy){var points=datapoints.points,ps=datapoints.pointsize;for(var i=0;i<points.length;i+=ps){if(points[i]==null)continue;drawBar(points[i],points[i+1],points[i+2],barLeft,barRight,fillStyleCallback,axisx,axisy,ctx,series.bars.horizontal,series.bars.lineWidth)}}ctx.save();ctx.translate(plotOffset.left,plotOffset.top);ctx.lineWidth=series.bars.lineWidth;ctx.strokeStyle=series.color;var barLeft;switch(series.bars.align){case"left":barLeft=0;break;case"right":barLeft=-series.bars.barWidth;break;default:barLeft=-series.bars.barWidth/2}var fillStyleCallback=series.bars.fill?function(bottom,top){return getFillStyle(series.bars,series.color,bottom,top)}:null;plotBars(series.datapoints,barLeft,barLeft+series.bars.barWidth,fillStyleCallback,series.xaxis,series.yaxis);ctx.restore()}function getFillStyle(filloptions,seriesColor,bottom,top){var fill=filloptions.fill;if(!fill)return null;if(filloptions.fillColor)return getColorOrGradient(filloptions.fillColor,bottom,top,seriesColor);var c=$.color.parse(seriesColor);c.a=typeof fill=="number"?fill:.4;c.normalize();return c.toString()}function insertLegend(){if(options.legend.container!=null){$(options.legend.container).html("")}else{placeholder.find(".legend").remove()}if(!options.legend.show){return}var fragments=[],entries=[],rowStarted=false,lf=options.legend.labelFormatter,s,label;for(var i=0;i<series.length;++i){s=series[i];if(s.label){label=lf?lf(s.label,s):s.label;if(label){entries.push({label:label,color:s.color})}}}if(options.legend.sorted){if($.isFunction(options.legend.sorted)){entries.sort(options.legend.sorted)}else if(options.legend.sorted=="reverse"){entries.reverse()}else{var ascending=options.legend.sorted!="descending";entries.sort(function(a,b){return a.label==b.label?0:a.label<b.label!=ascending?1:-1})}}for(var i=0;i<entries.length;++i){var entry=entries[i];if(i%options.legend.noColumns==0){if(rowStarted)fragments.push("</tr>");fragments.push("<tr>");rowStarted=true}fragments.push('<td class="legendColorBox"><div style="border:1px solid '+options.legend.labelBoxBorderColor+';padding:1px"><div style="width:4px;height:0;border:5px solid '+entry.color+';overflow:hidden"></div></div></td>'+'<td class="legendLabel">'+entry.label+"</td>")}if(rowStarted)fragments.push("</tr>");if(fragments.length==0)return;var table='<table style="font-size:smaller;color:'+options.grid.color+'">'+fragments.join("")+"</table>";if(options.legend.container!=null)$(options.legend.container).html(table);else{var pos="",p=options.legend.position,m=options.legend.margin;if(m[0]==null)m=[m,m];if(p.charAt(0)=="n")pos+="top:"+(m[1]+plotOffset.top)+"px;";else if(p.charAt(0)=="s")pos+="bottom:"+(m[1]+plotOffset.bottom)+"px;";if(p.charAt(1)=="e")pos+="right:"+(m[0]+plotOffset.right)+"px;";else if(p.charAt(1)=="w")pos+="left:"+(m[0]+plotOffset.left)+"px;";var legend=$('<div class="legend">'+table.replace('style="','style="position:absolute;'+pos+";")+"</div>").appendTo(placeholder);if(options.legend.backgroundOpacity!=0){var c=options.legend.backgroundColor;if(c==null){c=options.grid.backgroundColor;if(c&&typeof c=="string")c=$.color.parse(c);else c=$.color.extract(legend,"background-color");c.a=1;c=c.toString()}var div=legend.children();$('<div style="position:absolute;width:'+div.width()+"px;height:"+div.height()+"px;"+pos+"background-color:"+c+';"> </div>').prependTo(legend).css("opacity",options.legend.backgroundOpacity)}}}var highlights=[],redrawTimeout=null;function findNearbyItem(mouseX,mouseY,seriesFilter){var maxDistance=options.grid.mouseActiveRadius,smallestDistance=maxDistance*maxDistance+1,item=null,foundPoint=false,i,j,ps;for(i=series.length-1;i>=0;--i){if(!seriesFilter(series[i]))continue;var s=series[i],axisx=s.xaxis,axisy=s.yaxis,points=s.datapoints.points,mx=axisx.c2p(mouseX),my=axisy.c2p(mouseY),maxx=maxDistance/axisx.scale,maxy=maxDistance/axisy.scale;ps=s.datapoints.pointsize;if(axisx.options.inverseTransform)maxx=Number.MAX_VALUE;if(axisy.options.inverseTransform)maxy=Number.MAX_VALUE;if(s.lines.show||s.points.show){for(j=0;j<points.length;j+=ps){var x=points[j],y=points[j+1];if(x==null)continue;if(x-mx>maxx||x-mx<-maxx||y-my>maxy||y-my<-maxy)continue;var dx=Math.abs(axisx.p2c(x)-mouseX),dy=Math.abs(axisy.p2c(y)-mouseY),dist=dx*dx+dy*dy;if(dist<smallestDistance){smallestDistance=dist;item=[i,j/ps]}}}if(s.bars.show&&!item){var barLeft,barRight;switch(s.bars.align){case"left":barLeft=0;break;case"right":barLeft=-s.bars.barWidth;break;default:barLeft=-s.bars.barWidth/2}barRight=barLeft+s.bars.barWidth;for(j=0;j<points.length;j+=ps){var x=points[j],y=points[j+1],b=points[j+2];if(x==null)continue;if(series[i].bars.horizontal?mx<=Math.max(b,x)&&mx>=Math.min(b,x)&&my>=y+barLeft&&my<=y+barRight:mx>=x+barLeft&&mx<=x+barRight&&my>=Math.min(b,y)&&my<=Math.max(b,y))item=[i,j/ps]}}}if(item){i=item[0];j=item[1];ps=series[i].datapoints.pointsize;return{datapoint:series[i].datapoints.points.slice(j*ps,(j+1)*ps),dataIndex:j,series:series[i],seriesIndex:i}}return null}function onMouseMove(e){if(options.grid.hoverable)triggerClickHoverEvent("plothover",e,function(s){return s["hoverable"]!=false})}function onMouseLeave(e){if(options.grid.hoverable)triggerClickHoverEvent("plothover",e,function(s){return false})}function onClick(e){triggerClickHoverEvent("plotclick",e,function(s){return s["clickable"]!=false})}function triggerClickHoverEvent(eventname,event,seriesFilter){var offset=eventHolder.offset(),canvasX=event.pageX-offset.left-plotOffset.left,canvasY=event.pageY-offset.top-plotOffset.top,pos=canvasToAxisCoords({left:canvasX,top:canvasY});pos.pageX=event.pageX;pos.pageY=event.pageY;var item=findNearbyItem(canvasX,canvasY,seriesFilter);if(item){item.pageX=parseInt(item.series.xaxis.p2c(item.datapoint[0])+offset.left+plotOffset.left,10);item.pageY=parseInt(item.series.yaxis.p2c(item.datapoint[1])+offset.top+plotOffset.top,10)}if(options.grid.autoHighlight){for(var i=0;i<highlights.length;++i){var h=highlights[i];if(h.auto==eventname&&!(item&&h.series==item.series&&h.point[0]==item.datapoint[0]&&h.point[1]==item.datapoint[1]))unhighlight(h.series,h.point)}if(item)highlight(item.series,item.datapoint,eventname)}placeholder.trigger(eventname,[pos,item])}function triggerRedrawOverlay(){var t=options.interaction.redrawOverlayInterval;if(t==-1){drawOverlay();return}if(!redrawTimeout)redrawTimeout=setTimeout(drawOverlay,t)}function drawOverlay(){redrawTimeout=null;octx.save();overlay.clear();octx.translate(plotOffset.left,plotOffset.top);var i,hi;for(i=0;i<highlights.length;++i){hi=highlights[i];if(hi.series.bars.show)drawBarHighlight(hi.series,hi.point);else drawPointHighlight(hi.series,hi.point)}octx.restore();executeHooks(hooks.drawOverlay,[octx])}function highlight(s,point,auto){if(typeof s=="number")s=series[s];if(typeof point=="number"){var ps=s.datapoints.pointsize;point=s.datapoints.points.slice(ps*point,ps*(point+1))}var i=indexOfHighlight(s,point);if(i==-1){highlights.push({series:s,point:point,auto:auto});triggerRedrawOverlay()}else if(!auto)highlights[i].auto=false}function unhighlight(s,point){if(s==null&&point==null){highlights=[];triggerRedrawOverlay();return}if(typeof s=="number")s=series[s];if(typeof point=="number"){var ps=s.datapoints.pointsize;point=s.datapoints.points.slice(ps*point,ps*(point+1))}var i=indexOfHighlight(s,point);if(i!=-1){highlights.splice(i,1);triggerRedrawOverlay()}}function indexOfHighlight(s,p){for(var i=0;i<highlights.length;++i){var h=highlights[i];if(h.series==s&&h.point[0]==p[0]&&h.point[1]==p[1])return i}return-1}function drawPointHighlight(series,point){var x=point[0],y=point[1],axisx=series.xaxis,axisy=series.yaxis,highlightColor=typeof series.highlightColor==="string"?series.highlightColor:$.color.parse(series.color).scale("a",.5).toString();if(x<axisx.min||x>axisx.max||y<axisy.min||y>axisy.max)return;var pointRadius=series.points.radius+series.points.lineWidth/2;octx.lineWidth=pointRadius;octx.strokeStyle=highlightColor;var radius=1.5*pointRadius;x=axisx.p2c(x);y=axisy.p2c(y);octx.beginPath();if(series.points.symbol=="circle")octx.arc(x,y,radius,0,2*Math.PI,false);else series.points.symbol(octx,x,y,radius,false);octx.closePath();octx.stroke()}function drawBarHighlight(series,point){var highlightColor=typeof series.highlightColor==="string"?series.highlightColor:$.color.parse(series.color).scale("a",.5).toString(),fillStyle=highlightColor,barLeft;switch(series.bars.align){case"left":barLeft=0;break;case"right":barLeft=-series.bars.barWidth;break;default:barLeft=-series.bars.barWidth/2}octx.lineWidth=series.bars.lineWidth;octx.strokeStyle=highlightColor;drawBar(point[0],point[1],point[2]||0,barLeft,barLeft+series.bars.barWidth,function(){return fillStyle},series.xaxis,series.yaxis,octx,series.bars.horizontal,series.bars.lineWidth)}function getColorOrGradient(spec,bottom,top,defaultColor){if(typeof spec=="string")return spec;else{var gradient=ctx.createLinearGradient(0,top,0,bottom);for(var i=0,l=spec.colors.length;i<l;++i){var c=spec.colors[i];if(typeof c!="string"){var co=$.color.parse(defaultColor);if(c.brightness!=null)co=co.scale("rgb",c.brightness);if(c.opacity!=null)co.a*=c.opacity;c=co.toString()}gradient.addColorStop(i/(l-1),c)}return gradient}}}$.plot=function(placeholder,data,options){var plot=new Plot($(placeholder),data,options,$.plot.plugins);return plot};$.plot.version="0.8.3";$.plot.plugins=[];$.fn.plot=function(data,options){return this.each(function(){$.plot(this,data,options)})};function floorInBase(n,base){return base*Math.floor(n/base)}})(jQuery);
\ No newline at end of file
diff --git a/themes/bootstrap3/js/vendor/flot/jquery.flot.resize.min.js b/themes/bootstrap3/js/vendor/flot/jquery.flot.resize.min.js
index 1fa0771f570ea1a6cfd9259fb02d707d279ca859..7e92aa681c9fbd1b1e8eaa9e37e9b51e2f065fe8 100644
--- a/themes/bootstrap3/js/vendor/flot/jquery.flot.resize.min.js
+++ b/themes/bootstrap3/js/vendor/flot/jquery.flot.resize.min.js
@@ -1 +1,7 @@
-(function(n,p,u){var w=n([]),s=n.resize=n.extend(n.resize,{}),o,l="setTimeout",m="resize",t=m+"-special-event",v="delay",r="throttleWindow";s[v]=250;s[r]=true;n.event.special[m]={setup:function(){if(!s[r]&&this[l]){return false}var a=n(this);w=w.add(a);n.data(this,t,{w:a.width(),h:a.height()});if(w.length===1){q()}},teardown:function(){if(!s[r]&&this[l]){return false}var a=n(this);w=w.not(a);a.removeData(t);if(!w.length){clearTimeout(o)}},add:function(b){if(!s[r]&&this[l]){return false}var c;function a(d,h,g){var f=n(this),e=n.data(this,t);e.w=h!==u?h:f.width();e.h=g!==u?g:f.height();c.apply(this,arguments)}if(n.isFunction(b)){c=b;return a}else{c=b.handler;b.handler=a}}};function q(){o=p[l](function(){w.each(function(){var d=n(this),a=d.width(),b=d.height(),c=n.data(this,t);if(a!==c.w||b!==c.h){d.trigger(m,[c.w=a,c.h=b])}});q()},s[v])}})(jQuery,this);(function(b){var a={};function c(f){function e(){var h=f.getPlaceholder();if(h.width()==0||h.height()==0){return}f.resize();f.setupGrid();f.draw()}function g(i,h){i.getPlaceholder().resize(e)}function d(i,h){i.getPlaceholder().unbind("resize",e)}f.hooks.bindEvents.push(g);f.hooks.shutdown.push(d)}b.plot.plugins.push({init:c,options:a,name:"resize",version:"1.0"})})(jQuery);
\ No newline at end of file
+/* Javascript plotting library for jQuery, version 0.8.3.
+
+Copyright (c) 2007-2014 IOLA and Ole Laursen.
+Licensed under the MIT license.
+
+*/
+(function($,e,t){"$:nomunge";var i=[],n=$.resize=$.extend($.resize,{}),a,r=false,s="setTimeout",u="resize",m=u+"-special-event",o="pendingDelay",l="activeDelay",f="throttleWindow";n[o]=200;n[l]=20;n[f]=true;$.event.special[u]={setup:function(){if(!n[f]&&this[s]){return false}var e=$(this);i.push(this);e.data(m,{w:e.width(),h:e.height()});if(i.length===1){a=t;h()}},teardown:function(){if(!n[f]&&this[s]){return false}var e=$(this);for(var t=i.length-1;t>=0;t--){if(i[t]==this){i.splice(t,1);break}}e.removeData(m);if(!i.length){if(r){cancelAnimationFrame(a)}else{clearTimeout(a)}a=null}},add:function(e){if(!n[f]&&this[s]){return false}var i;function a(e,n,a){var r=$(this),s=r.data(m)||{};s.w=n!==t?n:r.width();s.h=a!==t?a:r.height();i.apply(this,arguments)}if($.isFunction(e)){i=e;return a}else{i=e.handler;e.handler=a}}};function h(t){if(r===true){r=t||1}for(var s=i.length-1;s>=0;s--){var l=$(i[s]);if(l[0]==e||l.is(":visible")){var f=l.width(),c=l.height(),d=l.data(m);if(d&&(f!==d.w||c!==d.h)){l.trigger(u,[d.w=f,d.h=c]);r=t||true}}else{d=l.data(m);d.w=0;d.h=0}}if(a!==null){if(r&&(t==null||t-r<1e3)){a=e.requestAnimationFrame(h)}else{a=setTimeout(h,n[o]);r=false}}}if(!e.requestAnimationFrame){e.requestAnimationFrame=function(){return e.webkitRequestAnimationFrame||e.mozRequestAnimationFrame||e.oRequestAnimationFrame||e.msRequestAnimationFrame||function(t,i){return e.setTimeout(function(){t((new Date).getTime())},n[l])}}()}if(!e.cancelAnimationFrame){e.cancelAnimationFrame=function(){return e.webkitCancelRequestAnimationFrame||e.mozCancelRequestAnimationFrame||e.oCancelRequestAnimationFrame||e.msCancelRequestAnimationFrame||clearTimeout}()}})(jQuery,this);(function($){var options={};function init(plot){function onResize(){var placeholder=plot.getPlaceholder();if(placeholder.width()==0||placeholder.height()==0)return;plot.resize();plot.setupGrid();plot.draw()}function bindEvents(plot,eventHolder){plot.getPlaceholder().resize(onResize)}function shutdown(plot,eventHolder){plot.getPlaceholder().unbind("resize",onResize)}plot.hooks.bindEvents.push(bindEvents);plot.hooks.shutdown.push(shutdown)}$.plot.plugins.push({init:init,options:options,name:"resize",version:"1.0"})})(jQuery);
\ No newline at end of file
diff --git a/themes/bootstrap3/js/vendor/flot/jquery.flot.selection.min.js b/themes/bootstrap3/js/vendor/flot/jquery.flot.selection.min.js
index badc0052dbe059b8ec850783d982639a9e4266c5..a0154fbc5bb0e93d4351eddc9bf87b10686c4489 100644
--- a/themes/bootstrap3/js/vendor/flot/jquery.flot.selection.min.js
+++ b/themes/bootstrap3/js/vendor/flot/jquery.flot.selection.min.js
@@ -1 +1,7 @@
-(function(a){function b(k){var p={first:{x:-1,y:-1},second:{x:-1,y:-1},show:false,active:false};var m={};var r=null;function e(s){if(p.active){l(s);k.getPlaceholder().trigger("plotselecting",[g()])}}function n(s){if(s.which!=1){return}document.body.focus();if(document.onselectstart!==undefined&&m.onselectstart==null){m.onselectstart=document.onselectstart;document.onselectstart=function(){return false}}if(document.ondrag!==undefined&&m.ondrag==null){m.ondrag=document.ondrag;document.ondrag=function(){return false}}d(p.first,s);p.active=true;r=function(t){j(t)};a(document).one("mouseup",r)}function j(s){r=null;if(document.onselectstart!==undefined){document.onselectstart=m.onselectstart}if(document.ondrag!==undefined){document.ondrag=m.ondrag}p.active=false;l(s);if(f()){i()}else{k.getPlaceholder().trigger("plotunselected",[]);k.getPlaceholder().trigger("plotselecting",[null])}return false}function g(){if(!f()){return null}var u={},t=p.first,s=p.second;a.each(k.getAxes(),function(v,w){if(w.used){var y=w.c2p(t[w.direction]),x=w.c2p(s[w.direction]);u[v]={from:Math.min(y,x),to:Math.max(y,x)}}});return u}function i(){var s=g();k.getPlaceholder().trigger("plotselected",[s]);if(s.xaxis&&s.yaxis){k.getPlaceholder().trigger("selected",[{x1:s.xaxis.from,y1:s.yaxis.from,x2:s.xaxis.to,y2:s.yaxis.to}])}}function h(t,u,s){return u<t?t:(u>s?s:u)}function d(w,t){var v=k.getOptions();var u=k.getPlaceholder().offset();var s=k.getPlotOffset();w.x=h(0,t.pageX-u.left-s.left,k.width());w.y=h(0,t.pageY-u.top-s.top,k.height());if(v.selection.mode=="y"){w.x=w==p.first?0:k.width()}if(v.selection.mode=="x"){w.y=w==p.first?0:k.height()}}function l(s){if(s.pageX==null){return}d(p.second,s);if(f()){p.show=true;k.triggerRedrawOverlay()}else{q(true)}}function q(s){if(p.show){p.show=false;k.triggerRedrawOverlay();if(!s){k.getPlaceholder().trigger("plotunselected",[])}}}function c(s,w){var t,y,z,A,x=k.getAxes();for(var u in x){t=x[u];if(t.direction==w){A=w+t.n+"axis";if(!s[A]&&t.n==1){A=w+"axis"}if(s[A]){y=s[A].from;z=s[A].to;break}}}if(!s[A]){t=w=="x"?k.getXAxes()[0]:k.getYAxes()[0];y=s[w+"1"];z=s[w+"2"]}if(y!=null&&z!=null&&y>z){var v=y;y=z;z=v}return{from:y,to:z,axis:t}}function o(t,s){var v,u,w=k.getOptions();if(w.selection.mode=="y"){p.first.x=0;p.second.x=k.width()}else{u=c(t,"x");p.first.x=u.axis.p2c(u.from);p.second.x=u.axis.p2c(u.to)}if(w.selection.mode=="x"){p.first.y=0;p.second.y=k.height()}else{u=c(t,"y");p.first.y=u.axis.p2c(u.from);p.second.y=u.axis.p2c(u.to)}p.show=true;k.triggerRedrawOverlay();if(!s&&f()){i()}}function f(){var s=5;return Math.abs(p.second.x-p.first.x)>=s&&Math.abs(p.second.y-p.first.y)>=s}k.clearSelection=q;k.setSelection=o;k.getSelection=g;k.hooks.bindEvents.push(function(t,s){var u=t.getOptions();if(u.selection.mode!=null){s.mousemove(e);s.mousedown(n)}});k.hooks.drawOverlay.push(function(v,D){if(p.show&&f()){var t=v.getPlotOffset();var s=v.getOptions();D.save();D.translate(t.left,t.top);var z=a.color.parse(s.selection.color);D.strokeStyle=z.scale("a",0.8).toString();D.lineWidth=1;D.lineJoin="round";D.fillStyle=z.scale("a",0.4).toString();var B=Math.min(p.first.x,p.second.x),A=Math.min(p.first.y,p.second.y),C=Math.abs(p.second.x-p.first.x),u=Math.abs(p.second.y-p.first.y);D.fillRect(B,A,C,u);D.strokeRect(B,A,C,u);D.restore()}});k.hooks.shutdown.push(function(t,s){s.unbind("mousemove",e);s.unbind("mousedown",n);if(r){a(document).unbind("mouseup",r)}})}a.plot.plugins.push({init:b,options:{selection:{mode:null,color:"#e8cfac"}},name:"selection",version:"1.1"})})(jQuery);
\ No newline at end of file
+/* Javascript plotting library for jQuery, version 0.8.3.
+
+Copyright (c) 2007-2014 IOLA and Ole Laursen.
+Licensed under the MIT license.
+
+*/
+(function($){function init(plot){var selection={first:{x:-1,y:-1},second:{x:-1,y:-1},show:false,active:false};var savedhandlers={};var mouseUpHandler=null;function onMouseMove(e){if(selection.active){updateSelection(e);plot.getPlaceholder().trigger("plotselecting",[getSelection()])}}function onMouseDown(e){if(e.which!=1)return;document.body.focus();if(document.onselectstart!==undefined&&savedhandlers.onselectstart==null){savedhandlers.onselectstart=document.onselectstart;document.onselectstart=function(){return false}}if(document.ondrag!==undefined&&savedhandlers.ondrag==null){savedhandlers.ondrag=document.ondrag;document.ondrag=function(){return false}}setSelectionPos(selection.first,e);selection.active=true;mouseUpHandler=function(e){onMouseUp(e)};$(document).one("mouseup",mouseUpHandler)}function onMouseUp(e){mouseUpHandler=null;if(document.onselectstart!==undefined)document.onselectstart=savedhandlers.onselectstart;if(document.ondrag!==undefined)document.ondrag=savedhandlers.ondrag;selection.active=false;updateSelection(e);if(selectionIsSane())triggerSelectedEvent();else{plot.getPlaceholder().trigger("plotunselected",[]);plot.getPlaceholder().trigger("plotselecting",[null])}return false}function getSelection(){if(!selectionIsSane())return null;if(!selection.show)return null;var r={},c1=selection.first,c2=selection.second;$.each(plot.getAxes(),function(name,axis){if(axis.used){var p1=axis.c2p(c1[axis.direction]),p2=axis.c2p(c2[axis.direction]);r[name]={from:Math.min(p1,p2),to:Math.max(p1,p2)}}});return r}function triggerSelectedEvent(){var r=getSelection();plot.getPlaceholder().trigger("plotselected",[r]);if(r.xaxis&&r.yaxis)plot.getPlaceholder().trigger("selected",[{x1:r.xaxis.from,y1:r.yaxis.from,x2:r.xaxis.to,y2:r.yaxis.to}])}function clamp(min,value,max){return value<min?min:value>max?max:value}function setSelectionPos(pos,e){var o=plot.getOptions();var offset=plot.getPlaceholder().offset();var plotOffset=plot.getPlotOffset();pos.x=clamp(0,e.pageX-offset.left-plotOffset.left,plot.width());pos.y=clamp(0,e.pageY-offset.top-plotOffset.top,plot.height());if(o.selection.mode=="y")pos.x=pos==selection.first?0:plot.width();if(o.selection.mode=="x")pos.y=pos==selection.first?0:plot.height()}function updateSelection(pos){if(pos.pageX==null)return;setSelectionPos(selection.second,pos);if(selectionIsSane()){selection.show=true;plot.triggerRedrawOverlay()}else clearSelection(true)}function clearSelection(preventEvent){if(selection.show){selection.show=false;plot.triggerRedrawOverlay();if(!preventEvent)plot.getPlaceholder().trigger("plotunselected",[])}}function extractRange(ranges,coord){var axis,from,to,key,axes=plot.getAxes();for(var k in axes){axis=axes[k];if(axis.direction==coord){key=coord+axis.n+"axis";if(!ranges[key]&&axis.n==1)key=coord+"axis";if(ranges[key]){from=ranges[key].from;to=ranges[key].to;break}}}if(!ranges[key]){axis=coord=="x"?plot.getXAxes()[0]:plot.getYAxes()[0];from=ranges[coord+"1"];to=ranges[coord+"2"]}if(from!=null&&to!=null&&from>to){var tmp=from;from=to;to=tmp}return{from:from,to:to,axis:axis}}function setSelection(ranges,preventEvent){var axis,range,o=plot.getOptions();if(o.selection.mode=="y"){selection.first.x=0;selection.second.x=plot.width()}else{range=extractRange(ranges,"x");selection.first.x=range.axis.p2c(range.from);selection.second.x=range.axis.p2c(range.to)}if(o.selection.mode=="x"){selection.first.y=0;selection.second.y=plot.height()}else{range=extractRange(ranges,"y");selection.first.y=range.axis.p2c(range.from);selection.second.y=range.axis.p2c(range.to)}selection.show=true;plot.triggerRedrawOverlay();if(!preventEvent&&selectionIsSane())triggerSelectedEvent()}function selectionIsSane(){var minSize=plot.getOptions().selection.minSize;return Math.abs(selection.second.x-selection.first.x)>=minSize&&Math.abs(selection.second.y-selection.first.y)>=minSize}plot.clearSelection=clearSelection;plot.setSelection=setSelection;plot.getSelection=getSelection;plot.hooks.bindEvents.push(function(plot,eventHolder){var o=plot.getOptions();if(o.selection.mode!=null){eventHolder.mousemove(onMouseMove);eventHolder.mousedown(onMouseDown)}});plot.hooks.drawOverlay.push(function(plot,ctx){if(selection.show&&selectionIsSane()){var plotOffset=plot.getPlotOffset();var o=plot.getOptions();ctx.save();ctx.translate(plotOffset.left,plotOffset.top);var c=$.color.parse(o.selection.color);ctx.strokeStyle=c.scale("a",.8).toString();ctx.lineWidth=1;ctx.lineJoin=o.selection.shape;ctx.fillStyle=c.scale("a",.4).toString();var x=Math.min(selection.first.x,selection.second.x)+.5,y=Math.min(selection.first.y,selection.second.y)+.5,w=Math.abs(selection.second.x-selection.first.x)-1,h=Math.abs(selection.second.y-selection.first.y)-1;ctx.fillRect(x,y,w,h);ctx.strokeRect(x,y,w,h);ctx.restore()}});plot.hooks.shutdown.push(function(plot,eventHolder){eventHolder.unbind("mousemove",onMouseMove);eventHolder.unbind("mousedown",onMouseDown);if(mouseUpHandler)$(document).unbind("mouseup",mouseUpHandler)})}$.plot.plugins.push({init:init,options:{selection:{mode:null,color:"#e8cfac",shape:"round",minSize:5}},name:"selection",version:"1.1"})})(jQuery);
\ No newline at end of file
diff --git a/themes/bootstrap3/js/vendor/markerclusterer.min.js b/themes/bootstrap3/js/vendor/markerclusterer.min.js
new file mode 100644
index 0000000000000000000000000000000000000000..e7e4fc96f58a99beef132ffee988ffb2cd9245eb
--- /dev/null
+++ b/themes/bootstrap3/js/vendor/markerclusterer.min.js
@@ -0,0 +1 @@
+function ClusterIcon(cluster,styles){cluster.getMarkerClusterer().extend(ClusterIcon,google.maps.OverlayView),this.cluster_=cluster,this.className_=cluster.getMarkerClusterer().getClusterClass(),this.styles_=styles,this.center_=null,this.div_=null,this.sums_=null,this.visible_=!1,this.setMap(cluster.getMap())}function Cluster(mc){this.markerClusterer_=mc,this.map_=mc.getMap(),this.gridSize_=mc.getGridSize(),this.minClusterSize_=mc.getMinimumClusterSize(),this.averageCenter_=mc.getAverageCenter(),this.hideLabel_=mc.getHideLabel(),this.markers_=[],this.center_=null,this.bounds_=null,this.clusterIcon_=new ClusterIcon(this,mc.getStyles())}function MarkerClusterer(map,opt_markers,opt_options){this.extend(MarkerClusterer,google.maps.OverlayView),opt_markers=opt_markers||[],opt_options=opt_options||{},this.markers_=[],this.clusters_=[],this.listeners_=[],this.activeMap_=null,this.ready_=!1,this.gridSize_=opt_options.gridSize||60,this.minClusterSize_=opt_options.minimumClusterSize||2,this.maxZoom_=opt_options.maxZoom||null,this.styles_=opt_options.styles||[],this.title_=opt_options.title||"",this.zoomOnClick_=!0,void 0!==opt_options.zoomOnClick&&(this.zoomOnClick_=opt_options.zoomOnClick),this.averageCenter_=!1,void 0!==opt_options.averageCenter&&(this.averageCenter_=opt_options.averageCenter),this.ignoreHidden_=!1,void 0!==opt_options.ignoreHidden&&(this.ignoreHidden_=opt_options.ignoreHidden),this.enableRetinaIcons_=!1,void 0!==opt_options.enableRetinaIcons&&(this.enableRetinaIcons_=opt_options.enableRetinaIcons),this.hideLabel_=!1,void 0!==opt_options.hideLabel&&(this.hideLabel_=opt_options.hideLabel),this.imagePath_=opt_options.imagePath||MarkerClusterer.IMAGE_PATH,this.imageExtension_=opt_options.imageExtension||MarkerClusterer.IMAGE_EXTENSION,this.imageSizes_=opt_options.imageSizes||MarkerClusterer.IMAGE_SIZES,this.calculator_=opt_options.calculator||MarkerClusterer.CALCULATOR,this.batchSize_=opt_options.batchSize||MarkerClusterer.BATCH_SIZE,this.batchSizeIE_=opt_options.batchSizeIE||MarkerClusterer.BATCH_SIZE_IE,this.clusterClass_=opt_options.clusterClass||"cluster",-1!==navigator.userAgent.toLowerCase().indexOf("msie")&&(this.batchSize_=this.batchSizeIE_),this.setupStyles_(),this.addMarkers(opt_markers,!0),this.setMap(map)}ClusterIcon.prototype.onAdd=function(){var cMouseDownInCluster,cDraggingMapByCluster,cClusterIcon=this;this.div_=document.createElement("div"),this.div_.className=this.className_,this.visible_&&this.show(),this.getPanes().overlayMouseTarget.appendChild(this.div_),this.boundsChangedListener_=google.maps.event.addListener(this.getMap(),"bounds_changed",function(){cDraggingMapByCluster=cMouseDownInCluster}),google.maps.event.addDomListener(this.div_,"mousedown",function(){cMouseDownInCluster=!0,cDraggingMapByCluster=!1}),google.maps.event.addDomListener(this.div_,"click",function(e){if(cMouseDownInCluster=!1,!cDraggingMapByCluster){var theBounds,mz,mc=cClusterIcon.cluster_.getMarkerClusterer();google.maps.event.trigger(mc,"click",cClusterIcon.cluster_),google.maps.event.trigger(mc,"clusterclick",cClusterIcon.cluster_),mc.getZoomOnClick()&&(mz=mc.getMaxZoom(),theBounds=cClusterIcon.cluster_.getBounds(),mc.getMap().fitBounds(theBounds),setTimeout(function(){mc.getMap().fitBounds(theBounds),null!==mz&&mc.getMap().getZoom()>mz&&mc.getMap().setZoom(mz+1)},100)),e.cancelBubble=!0,e.stopPropagation&&e.stopPropagation()}}),google.maps.event.addDomListener(this.div_,"mouseover",function(){var mc=cClusterIcon.cluster_.getMarkerClusterer();google.maps.event.trigger(mc,"mouseover",cClusterIcon.cluster_)}),google.maps.event.addDomListener(this.div_,"mouseout",function(){var mc=cClusterIcon.cluster_.getMarkerClusterer();google.maps.event.trigger(mc,"mouseout",cClusterIcon.cluster_)})},ClusterIcon.prototype.onRemove=function(){this.div_&&this.div_.parentNode&&(this.hide(),google.maps.event.removeListener(this.boundsChangedListener_),google.maps.event.clearInstanceListeners(this.div_),this.div_.parentNode.removeChild(this.div_),this.div_=null)},ClusterIcon.prototype.draw=function(){if(this.visible_){var pos=this.getPosFromLatLng_(this.center_);this.div_.style.top=pos.y+"px",this.div_.style.left=pos.x+"px"}},ClusterIcon.prototype.hide=function(){this.div_&&(this.div_.style.display="none"),this.visible_=!1},ClusterIcon.prototype.show=function(){if(this.div_){var img="",bp=this.backgroundPosition_.split(" "),spriteH=parseInt(bp[0].trim(),10),spriteV=parseInt(bp[1].trim(),10),pos=this.getPosFromLatLng_(this.center_);this.div_.style.cssText=this.createCss(pos),img="<img src='"+this.url_+"' style='position: absolute; top: "+spriteV+"px; left: "+spriteH+"px; ",img+=this.cluster_.getMarkerClusterer().enableRetinaIcons_?"width: "+this.width_+"px;height: "+this.height_+"px;":"clip: rect("+-1*spriteV+"px, "+(-1*spriteH+this.width_)+"px, "+(-1*spriteV+this.height_)+"px, "+-1*spriteH+"px);",img+="'>",this.div_.innerHTML=img+"<div style='position: absolute;top: "+this.anchorText_[0]+"px;left: "+this.anchorText_[1]+"px;color: "+this.textColor_+";font-size: "+this.textSize_+"px;font-family: "+this.fontFamily_+";font-weight: "+this.fontWeight_+";font-style: "+this.fontStyle_+";text-decoration: "+this.textDecoration_+";text-align: center;width: "+this.width_+"px;line-height:"+this.height_+"px;'>"+(this.cluster_.hideLabel_?" ":this.sums_.text)+"</div>",this.div_.title="undefined"==typeof this.sums_.title||""===this.sums_.title?this.cluster_.getMarkerClusterer().getTitle():this.sums_.title,this.div_.style.display=""}this.visible_=!0},ClusterIcon.prototype.useStyle=function(sums){this.sums_=sums;var index=Math.max(0,sums.index-1);index=Math.min(this.styles_.length-1,index);var style=this.styles_[index];this.url_=style.url,this.height_=style.height,this.width_=style.width,this.anchorText_=style.anchorText||[0,0],this.anchorIcon_=style.anchorIcon||[parseInt(this.height_/2,10),parseInt(this.width_/2,10)],this.textColor_=style.textColor||"black",this.textSize_=style.textSize||11,this.textDecoration_=style.textDecoration||"none",this.fontWeight_=style.fontWeight||"bold",this.fontStyle_=style.fontStyle||"normal",this.fontFamily_=style.fontFamily||"Arial,sans-serif",this.backgroundPosition_=style.backgroundPosition||"0 0"},ClusterIcon.prototype.setCenter=function(center){this.center_=center},ClusterIcon.prototype.createCss=function(pos){var style=[];return style.push("cursor: pointer;"),style.push("position: absolute; top: "+pos.y+"px; left: "+pos.x+"px;"),style.push("width: "+this.width_+"px; height: "+this.height_+"px;"),style.join("")},ClusterIcon.prototype.getPosFromLatLng_=function(latlng){var pos=this.getProjection().fromLatLngToDivPixel(latlng);return pos.x-=this.anchorIcon_[1],pos.y-=this.anchorIcon_[0],pos.x=parseInt(pos.x,10),pos.y=parseInt(pos.y,10),pos},Cluster.prototype.getSize=function(){return this.markers_.length},Cluster.prototype.getMarkers=function(){return this.markers_},Cluster.prototype.getCenter=function(){return this.center_},Cluster.prototype.getMap=function(){return this.map_},Cluster.prototype.getMarkerClusterer=function(){return this.markerClusterer_},Cluster.prototype.getBounds=function(){var i,bounds=new google.maps.LatLngBounds(this.center_,this.center_),markers=this.getMarkers();for(i=0;i<markers.length;i++)bounds.extend(markers[i].getPosition());return bounds},Cluster.prototype.remove=function(){this.clusterIcon_.setMap(null),this.markers_=[],delete this.markers_},Cluster.prototype.addMarker=function(marker){var i,mCount,mz;if(this.isMarkerAlreadyAdded_(marker))return!1;if(this.center_){if(this.averageCenter_){var l=this.markers_.length+1,lat=(this.center_.lat()*(l-1)+marker.getPosition().lat())/l,lng=(this.center_.lng()*(l-1)+marker.getPosition().lng())/l;this.center_=new google.maps.LatLng(lat,lng),this.calculateBounds_()}}else this.center_=marker.getPosition(),this.calculateBounds_();if(marker.isAdded=!0,this.markers_.push(marker),mCount=this.markers_.length,mz=this.markerClusterer_.getMaxZoom(),null!==mz&&this.map_.getZoom()>mz)marker.getMap()!==this.map_&&marker.setMap(this.map_);else if(mCount<this.minClusterSize_)marker.getMap()!==this.map_&&marker.setMap(this.map_);else if(mCount===this.minClusterSize_)for(i=0;mCount>i;i++)this.markers_[i].setMap(null);else marker.setMap(null);return!0},Cluster.prototype.isMarkerInClusterBounds=function(marker){return this.bounds_.contains(marker.getPosition())},Cluster.prototype.calculateBounds_=function(){var bounds=new google.maps.LatLngBounds(this.center_,this.center_);this.bounds_=this.markerClusterer_.getExtendedBounds(bounds)},Cluster.prototype.updateIcon_=function(){var mCount=this.markers_.length,mz=this.markerClusterer_.getMaxZoom();if(null!==mz&&this.map_.getZoom()>mz)return void this.clusterIcon_.hide();if(mCount<this.minClusterSize_)return void this.clusterIcon_.hide();var numStyles=this.markerClusterer_.getStyles().length,sums=this.markerClusterer_.getCalculator()(this.markers_,numStyles);this.clusterIcon_.setCenter(this.center_),this.clusterIcon_.useStyle(sums),this.clusterIcon_.show()},Cluster.prototype.isMarkerAlreadyAdded_=function(marker){for(var i=0,n=this.markers_.length;n>i;i++)if(marker===this.markers_[i])return!0;return!1},MarkerClusterer.prototype.onAdd=function(){var cMarkerClusterer=this;this.activeMap_=this.getMap(),this.ready_=!0,this.repaint(),this.listeners_=[google.maps.event.addListener(this.getMap(),"zoom_changed",function(){cMarkerClusterer.resetViewport_(!1),(this.getZoom()===(this.get("minZoom")||0)||this.getZoom()===this.get("maxZoom"))&&google.maps.event.trigger(this,"idle")}),google.maps.event.addListener(this.getMap(),"idle",function(){cMarkerClusterer.redraw_()})]},MarkerClusterer.prototype.onRemove=function(){var i;for(i=0;i<this.markers_.length;i++)this.markers_[i].getMap()!==this.activeMap_&&this.markers_[i].setMap(this.activeMap_);for(i=0;i<this.clusters_.length;i++)this.clusters_[i].remove();for(this.clusters_=[],i=0;i<this.listeners_.length;i++)google.maps.event.removeListener(this.listeners_[i]);this.listeners_=[],this.activeMap_=null,this.ready_=!1},MarkerClusterer.prototype.draw=function(){},MarkerClusterer.prototype.setupStyles_=function(){var i,size;if(!(this.styles_.length>0))for(i=0;i<this.imageSizes_.length;i++)size=this.imageSizes_[i],this.styles_.push({url:this.imagePath_+(i+1)+"."+this.imageExtension_,height:size,width:size})},MarkerClusterer.prototype.fitMapToMarkers=function(){var i,markers=this.getMarkers(),bounds=new google.maps.LatLngBounds;for(i=0;i<markers.length;i++)bounds.extend(markers[i].getPosition());this.getMap().fitBounds(bounds)},MarkerClusterer.prototype.getGridSize=function(){return this.gridSize_},MarkerClusterer.prototype.setGridSize=function(gridSize){this.gridSize_=gridSize},MarkerClusterer.prototype.getMinimumClusterSize=function(){return this.minClusterSize_},MarkerClusterer.prototype.setMinimumClusterSize=function(minimumClusterSize){this.minClusterSize_=minimumClusterSize},MarkerClusterer.prototype.getMaxZoom=function(){return this.maxZoom_},MarkerClusterer.prototype.setMaxZoom=function(maxZoom){this.maxZoom_=maxZoom},MarkerClusterer.prototype.getStyles=function(){return this.styles_},MarkerClusterer.prototype.setStyles=function(styles){this.styles_=styles},MarkerClusterer.prototype.getTitle=function(){return this.title_},MarkerClusterer.prototype.setTitle=function(title){this.title_=title},MarkerClusterer.prototype.getZoomOnClick=function(){return this.zoomOnClick_},MarkerClusterer.prototype.setZoomOnClick=function(zoomOnClick){this.zoomOnClick_=zoomOnClick},MarkerClusterer.prototype.getAverageCenter=function(){return this.averageCenter_},MarkerClusterer.prototype.setAverageCenter=function(averageCenter){this.averageCenter_=averageCenter},MarkerClusterer.prototype.getIgnoreHidden=function(){return this.ignoreHidden_},MarkerClusterer.prototype.setIgnoreHidden=function(ignoreHidden){this.ignoreHidden_=ignoreHidden},MarkerClusterer.prototype.getEnableRetinaIcons=function(){return this.enableRetinaIcons_},MarkerClusterer.prototype.setEnableRetinaIcons=function(enableRetinaIcons){this.enableRetinaIcons_=enableRetinaIcons},MarkerClusterer.prototype.getImageExtension=function(){return this.imageExtension_},MarkerClusterer.prototype.setImageExtension=function(imageExtension){this.imageExtension_=imageExtension},MarkerClusterer.prototype.getImagePath=function(){return this.imagePath_},MarkerClusterer.prototype.setImagePath=function(imagePath){this.imagePath_=imagePath},MarkerClusterer.prototype.getImageSizes=function(){return this.imageSizes_},MarkerClusterer.prototype.setImageSizes=function(imageSizes){this.imageSizes_=imageSizes},MarkerClusterer.prototype.getCalculator=function(){return this.calculator_},MarkerClusterer.prototype.setCalculator=function(calculator){this.calculator_=calculator},MarkerClusterer.prototype.setHideLabel=function(hideLabel){this.hideLabel_=hideLabel},MarkerClusterer.prototype.getHideLabel=function(){return this.hideLabel_},MarkerClusterer.prototype.getBatchSizeIE=function(){return this.batchSizeIE_},MarkerClusterer.prototype.setBatchSizeIE=function(batchSizeIE){this.batchSizeIE_=batchSizeIE},MarkerClusterer.prototype.getClusterClass=function(){return this.clusterClass_},MarkerClusterer.prototype.setClusterClass=function(clusterClass){this.clusterClass_=clusterClass},MarkerClusterer.prototype.getMarkers=function(){return this.markers_},MarkerClusterer.prototype.getTotalMarkers=function(){return this.markers_.length},MarkerClusterer.prototype.getClusters=function(){return this.clusters_},MarkerClusterer.prototype.getTotalClusters=function(){return this.clusters_.length},MarkerClusterer.prototype.addMarker=function(marker,opt_nodraw){this.pushMarkerTo_(marker),opt_nodraw||this.redraw_()},MarkerClusterer.prototype.addMarkers=function(markers,opt_nodraw){var key;for(key in markers)markers.hasOwnProperty(key)&&this.pushMarkerTo_(markers[key]);opt_nodraw||this.redraw_()},MarkerClusterer.prototype.pushMarkerTo_=function(marker){if(marker.getDraggable()){var cMarkerClusterer=this;google.maps.event.addListener(marker,"dragend",function(){cMarkerClusterer.ready_&&(this.isAdded=!1,cMarkerClusterer.repaint())})}marker.isAdded=!1,this.markers_.push(marker)},MarkerClusterer.prototype.removeMarker=function(marker,opt_nodraw,opt_noMapRemove){var removeFromMap=!0&&!opt_noMapRemove,removed=this.removeMarker_(marker,removeFromMap);return!opt_nodraw&&removed&&this.repaint(),removed},MarkerClusterer.prototype.removeMarkers=function(markers,opt_nodraw,opt_noMapRemove){var i,r,removed=!1,removeFromMap=!0&&!opt_noMapRemove;for(i=0;i<markers.length;i++)r=this.removeMarker_(markers[i],removeFromMap),removed=removed||r;return!opt_nodraw&&removed&&this.repaint(),removed},MarkerClusterer.prototype.removeMarker_=function(marker,removeFromMap){var i,index=-1;if(this.markers_.indexOf)index=this.markers_.indexOf(marker);else for(i=0;i<this.markers_.length;i++)if(marker===this.markers_[i]){index=i;break}return-1===index?!1:(removeFromMap&&marker.setMap(null),this.markers_.splice(index,1),!0)},MarkerClusterer.prototype.clearMarkers=function(){this.resetViewport_(!0),this.markers_=[]},MarkerClusterer.prototype.repaint=function(){var oldClusters=this.clusters_.slice();this.clusters_=[],this.resetViewport_(!1),this.redraw_(),setTimeout(function(){var i;for(i=0;i<oldClusters.length;i++)oldClusters[i].remove()},0)},MarkerClusterer.prototype.getExtendedBounds=function(bounds){var projection=this.getProjection(),tr=new google.maps.LatLng(bounds.getNorthEast().lat(),bounds.getNorthEast().lng()),bl=new google.maps.LatLng(bounds.getSouthWest().lat(),bounds.getSouthWest().lng()),trPix=projection.fromLatLngToDivPixel(tr);trPix.x+=this.gridSize_,trPix.y-=this.gridSize_;var blPix=projection.fromLatLngToDivPixel(bl);blPix.x-=this.gridSize_,blPix.y+=this.gridSize_;var ne=projection.fromDivPixelToLatLng(trPix),sw=projection.fromDivPixelToLatLng(blPix);return bounds.extend(ne),bounds.extend(sw),bounds},MarkerClusterer.prototype.redraw_=function(){this.createClusters_(0)},MarkerClusterer.prototype.resetViewport_=function(opt_hide){var i,marker;for(i=0;i<this.clusters_.length;i++)this.clusters_[i].remove();for(this.clusters_=[],i=0;i<this.markers_.length;i++)marker=this.markers_[i],marker.isAdded=!1,opt_hide&&marker.setMap(null)},MarkerClusterer.prototype.distanceBetweenPoints_=function(p1,p2){var R=6371,dLat=(p2.lat()-p1.lat())*Math.PI/180,dLon=(p2.lng()-p1.lng())*Math.PI/180,a=Math.sin(dLat/2)*Math.sin(dLat/2)+Math.cos(p1.lat()*Math.PI/180)*Math.cos(p2.lat()*Math.PI/180)*Math.sin(dLon/2)*Math.sin(dLon/2),c=2*Math.atan2(Math.sqrt(a),Math.sqrt(1-a)),d=R*c;return d},MarkerClusterer.prototype.isMarkerInBounds_=function(marker,bounds){return bounds.contains(marker.getPosition())},MarkerClusterer.prototype.addToClosestCluster_=function(marker){var i,d,cluster,center,distance=4e4,clusterToAddTo=null;for(i=0;i<this.clusters_.length;i++)cluster=this.clusters_[i],center=cluster.getCenter(),center&&(d=this.distanceBetweenPoints_(center,marker.getPosition()),distance>d&&(distance=d,clusterToAddTo=cluster));clusterToAddTo&&clusterToAddTo.isMarkerInClusterBounds(marker)?clusterToAddTo.addMarker(marker):(cluster=new Cluster(this),cluster.addMarker(marker),this.clusters_.push(cluster))},MarkerClusterer.prototype.createClusters_=function(iFirst){var i,marker,mapBounds,cMarkerClusterer=this;if(this.ready_){0===iFirst&&(google.maps.event.trigger(this,"clusteringbegin",this),"undefined"!=typeof this.timerRefStatic&&(clearTimeout(this.timerRefStatic),delete this.timerRefStatic)),mapBounds=this.getMap().getZoom()>3?new google.maps.LatLngBounds(this.getMap().getBounds().getSouthWest(),this.getMap().getBounds().getNorthEast()):new google.maps.LatLngBounds(new google.maps.LatLng(85.02070771743472,-178.48388434375),new google.maps.LatLng(-85.08136444384544,178.00048865625));var bounds=this.getExtendedBounds(mapBounds),iLast=Math.min(iFirst+this.batchSize_,this.markers_.length);for(i=iFirst;iLast>i;i++)marker=this.markers_[i],!marker.isAdded&&this.isMarkerInBounds_(marker,bounds)&&(!this.ignoreHidden_||this.ignoreHidden_&&marker.getVisible())&&this.addToClosestCluster_(marker);if(iLast<this.markers_.length)this.timerRefStatic=setTimeout(function(){cMarkerClusterer.createClusters_(iLast)},0);else for(delete this.timerRefStatic,google.maps.event.trigger(this,"clusteringend",this),i=0;i<this.clusters_.length;i++)this.clusters_[i].updateIcon_()}},MarkerClusterer.prototype.extend=function(obj1,obj2){return function(object){var property;for(property in object.prototype)this.prototype[property]=object.prototype[property];return this}.apply(obj1,[obj2])},MarkerClusterer.CALCULATOR=function(markers,numStyles){for(var index=0,title="",count=markers.length.toString(),dv=count;0!==dv;)dv=parseInt(dv/10,10),index++;return index=Math.min(index,numStyles),{text:count,index:index,title:title}},MarkerClusterer.BATCH_SIZE=2e3,MarkerClusterer.BATCH_SIZE_IE=500,MarkerClusterer.IMAGE_PATH="//cdn.rawgit.com/mahnunchik/markerclustererplus/master/images/m",MarkerClusterer.IMAGE_EXTENSION="png",MarkerClusterer.IMAGE_SIZES=[53,56,66,78,90],"function"!=typeof String.prototype.trim&&(String.prototype.trim=function(){return this.replace(/^\s+|\s+$/g,"")});
\ No newline at end of file
diff --git a/themes/bootstrap3/js/vendor/masonry.min.js b/themes/bootstrap3/js/vendor/masonry.min.js
deleted file mode 100644
index 7398e283215cc0998c50b2bcc5d340299f2ef72f..0000000000000000000000000000000000000000
--- a/themes/bootstrap3/js/vendor/masonry.min.js
+++ /dev/null
@@ -1,9 +0,0 @@
-/**
- * Vanilla Masonry v1.0.7
- * Dynamic layouts for the flip-side of CSS Floats
- * http://vanilla-masonry.desandro.com
- *
- * Licensed under the MIT license.
- * Copyright 2012 David DeSandro
- */
-(function(a,b){function e(a){return new RegExp("(^|\\s+)"+a+"(\\s+|$)")}function o(a,b,c){if(c.indexOf("%")===-1)return c;var d=a.style,e=d.width,f;return d.width=c,f=b.width,d.width=e,f}function p(a,b,c){var d=b!=="height",e=d?a.offsetWidth:a.offsetHeight,f=d?"Left":"Top",g=d?"Right":"Bottom",h=j(a),i=parseFloat(h["padding"+f])||0,k=parseFloat(h["padding"+g])||0,l=parseFloat(h["border"+f+"Width"])||0,m=parseFloat(h["border"+g+"Width"])||0,p=h["margin"+f],q=h["margin"+g],r,s;n||(p=o(a,h,p),q=o(a,h,q)),r=parseFloat(p)||0,s=parseFloat(q)||0;if(e>0)c?e+=r+s:e-=i+k+l+m;else{e=h[b];if(e<0||e===null)e=a.style[b]||0;e=parseFloat(e)||0,c&&(e+=i+k+r+s+l+m)}return e}function q(b,c,d){b.addEventListener?b.addEventListener(c,d,!1):b.attachEvent&&(b["e"+c+d]=d,b[c+d]=function(){b["e"+c+d](a.event)},b.attachEvent("on"+c,b[c+d]))}function r(a,b,c){a.removeEventListener?a.removeEventListener(b,c,!1):a.detachEvent&&(a.detachEvent("on"+b,a[b+c]),a[b+c]=null,a["e"+b+c]=null)}function s(a,b){if(!a)return;this.element=a,this.options={};for(var c in s.defaults)this.options[c]=s.defaults[c];for(c in b)this.options[c]=b[c];this._create(),this.build()}"use strict";var c=a.document,d="classList"in c.createElement("div"),f=d?function(a,b){return a.classList.contains(b)}:function(a,b){return e(b).test(a.className)},g=d?function(a,b){a.classList.add(b)}:function(a,b){f(a,b)||(a.className=a.className+" "+b)},h=d?function(a,b){a.classList.remove(b)}:function(a,b){a.className=a.className.replace(e(b)," ")},i=c.defaultView,j=i&&i.getComputedStyle?function(a){return i.getComputedStyle(a,null)}:function(a){return a.currentStyle},k=c.getElementsByTagName("body")[0],l=c.createElement("div"),m=k||c.createElement("body");l.style.marginTop="1%",m.appendChild(l);var n=j(l).marginTop!=="1%";m.removeChild(l);var t=["position","height"];s.defaults={isResizable:!0,gutterWidth:0,isRTL:!1,isFitWidth:!1},s.prototype={_getBricks:function(a){var b;for(var c=0,d=a.length;c<d;c++)b=a[c],b.style.position="absolute",g(b,"masonry-brick"),this.bricks.push(b)},_create:function(){this.reloadItems();var b=this.element.style;this._originalStyle={};for(var c=0,d=t.length;c<d;c++){var e=t[c];this._originalStyle[e]=b[e]||""}this.element.style.position="relative",this.horizontalDirection=this.options.isRTL?"right":"left",this.offset={};var f=j(this.element),h=this.options.isRTL?"paddingRight":"paddingLeft";this.offset.y=parseFloat(f.paddingTop)||0,this.offset.x=parseFloat(f[h])||0,this.isFluid=this.options.columnWidth&&typeof this.options.columnWidth=="function";var i=this;setTimeout(function(){g(i.element,"masonry")}),this.options.isResizable&&q(a,"resize",function(){i._handleResize()})},build:function(a){this._getColumns(),this._reLayout(a)},_getColumns:function(){var a=this.options.isFitWidth?this.element.parentNode:this.element,b=p(a,"width");this.columnWidth=this.isFluid?this.options.columnWidth(b):this.options.columnWidth||p(this.bricks[0],"width",!0)||b,this.columnWidth+=this.options.gutterWidth,this.cols=Math.floor((b+this.options.gutterWidth)/this.columnWidth),this.cols=Math.max(this.cols,1)},reloadItems:function(){this.bricks=[],this._getBricks(this.element.children)},_reLayout:function(a){var b=this.cols;this.colYs=[];while(b--)this.colYs.push(0);this.layout(this.bricks,a)},layout:function(a,b){if(!a||!a.length)return;var c,d,e,f,g,h,i;for(var j=0,k=a.length;j<k;j++){c=a[j];if(c.nodeType!==1)continue;d=Math.ceil(p(c,"width",!0)/this.columnWidth),d=Math.min(d,this.cols);if(d===1)i=this.colYs;else{e=this.cols+1-d,i=[];for(h=0;h<e;h++)g=this.colYs.slice(h,h+d),i[h]=Math.max.apply(Math,g)}var l=Math.min.apply(Math,i);for(var m=0,n=i.length;m<n;m++)if(i[m]===l)break;c.style.top=l+this.offset.y+"px",c.style[this.horizontalDirection]=this.columnWidth*m+this.offset.x+"px";var o=l+p(c,"height",!0),q=this.cols+1-n;for(h=0;h<q;h++)this.colYs[m+h]=o}var r={};this.element.style.height=Math.max.apply(Math,this.colYs)+"px";if(this.options.isFitWidth){var s=0;j=this.cols;while(--j){if(this.colYs[j]!==0)break;s++}this.element.style.width=(this.cols-s)*this.columnWidth-this.options.gutterWidth+"px"}b&&b.call(a)},_handleResize:function(){function b(){a.resize(),a._resizeTimeout=null}var a=this;this._resizeTimeout&&clearTimeout(this._resizeTimeout),this._resizeTimeout=setTimeout(b,100)},resize:function(){var a=this.cols;this._getColumns(),(this.isFluid||this.cols!==a)&&this._reLayout()},reload:function(a){this.reloadItems(),this.build(a)},appended:function(a,b,c){var d=this,e=function(){d._appended(a,c)};if(b){var f=p(this.element,"height")+"px";for(var g=0,h=a.length;g<h;g++)a[g].style.top=f;setTimeout(e,1)}else e()},_appended:function(a,b){this._getBricks(a),this.layout(a,b)},destroy:function(){var b;for(var c=0,d=this.bricks.length;c<d;c++)b=this.bricks[c],b.style.position="",b.style.top="",b.style.left="",h(b,"masonry-brick");var e=this.element.style;d=t.length;for(c=0;c<d;c++){var f=t[c];e[f]=this._originalStyle[f]}h(this.element,"masonry"),this.resizeHandler&&r(a,"resize",this.resizeHandler)}},s.getWH=p,a.Masonry=s})(window);
\ No newline at end of file
diff --git a/themes/bootstrap3/js/vendor/ol/ol-debug.js b/themes/bootstrap3/js/vendor/ol/ol-debug.js
new file mode 100644
index 0000000000000000000000000000000000000000..56fd2d897aaa3dee3d10d350a4a684e2987963e6
--- /dev/null
+++ b/themes/bootstrap3/js/vendor/ol/ol-debug.js
@@ -0,0 +1,107541 @@
+// OpenLayers 3. See http://openlayers.org/
+// License: https://raw.githubusercontent.com/openlayers/ol3/master/LICENSE.md
+// Version: v3.17.1
+
+(function (root, factory) {
+  if (typeof exports === "object") {
+    module.exports = factory();
+  } else if (typeof define === "function" && define.amd) {
+    define([], factory);
+  } else {
+    root.ol = factory();
+  }
+}(this, function () {
+  var OPENLAYERS = {};
+  var goog = this.goog = {};
+this.CLOSURE_NO_DEPS = true;
+// Copyright 2006 The Closure Library Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS-IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+/**
+ * @fileoverview Bootstrap for the Google JS Library (Closure).
+ *
+ * In uncompiled mode base.js will write out Closure's deps file, unless the
+ * global <code>CLOSURE_NO_DEPS</code> is set to true.  This allows projects to
+ * include their own deps file(s) from different locations.
+ *
+ * @author arv@google.com (Erik Arvidsson)
+ *
+ * @provideGoog
+ */
+
+
+/**
+ * @define {boolean} Overridden to true by the compiler when
+ *     --process_closure_primitives is specified.
+ */
+var COMPILED = false;
+
+
+/**
+ * Base namespace for the Closure library.  Checks to see goog is already
+ * defined in the current scope before assigning to prevent clobbering if
+ * base.js is loaded more than once.
+ *
+ * @const
+ */
+var goog = goog || {};
+
+
+/**
+ * Reference to the global context.  In most cases this will be 'window'.
+ */
+goog.global = this;
+
+
+/**
+ * A hook for overriding the define values in uncompiled mode.
+ *
+ * In uncompiled mode, {@code CLOSURE_UNCOMPILED_DEFINES} may be defined before
+ * loading base.js.  If a key is defined in {@code CLOSURE_UNCOMPILED_DEFINES},
+ * {@code goog.define} will use the value instead of the default value.  This
+ * allows flags to be overwritten without compilation (this is normally
+ * accomplished with the compiler's "define" flag).
+ *
+ * Example:
+ * <pre>
+ *   var CLOSURE_UNCOMPILED_DEFINES = {'goog.DEBUG': false};
+ * </pre>
+ *
+ * @type {Object<string, (string|number|boolean)>|undefined}
+ */
+goog.global.CLOSURE_UNCOMPILED_DEFINES;
+
+
+/**
+ * A hook for overriding the define values in uncompiled or compiled mode,
+ * like CLOSURE_UNCOMPILED_DEFINES but effective in compiled code.  In
+ * uncompiled code CLOSURE_UNCOMPILED_DEFINES takes precedence.
+ *
+ * Also unlike CLOSURE_UNCOMPILED_DEFINES the values must be number, boolean or
+ * string literals or the compiler will emit an error.
+ *
+ * While any @define value may be set, only those set with goog.define will be
+ * effective for uncompiled code.
+ *
+ * Example:
+ * <pre>
+ *   var CLOSURE_DEFINES = {'goog.DEBUG': false} ;
+ * </pre>
+ *
+ * @type {Object<string, (string|number|boolean)>|undefined}
+ */
+goog.global.CLOSURE_DEFINES;
+
+
+/**
+ * Returns true if the specified value is not undefined.
+ * WARNING: Do not use this to test if an object has a property. Use the in
+ * operator instead.
+ *
+ * @param {?} val Variable to test.
+ * @return {boolean} Whether variable is defined.
+ */
+goog.isDef = function(val) {
+  // void 0 always evaluates to undefined and hence we do not need to depend on
+  // the definition of the global variable named 'undefined'.
+  return val !== void 0;
+};
+
+
+/**
+ * Builds an object structure for the provided namespace path, ensuring that
+ * names that already exist are not overwritten. For example:
+ * "a.b.c" -> a = {};a.b={};a.b.c={};
+ * Used by goog.provide and goog.exportSymbol.
+ * @param {string} name name of the object that this file defines.
+ * @param {*=} opt_object the object to expose at the end of the path.
+ * @param {Object=} opt_objectToExportTo The object to add the path to; default
+ *     is |goog.global|.
+ * @private
+ */
+goog.exportPath_ = function(name, opt_object, opt_objectToExportTo) {
+  var parts = name.split('.');
+  var cur = opt_objectToExportTo || goog.global;
+
+  // Internet Explorer exhibits strange behavior when throwing errors from
+  // methods externed in this manner.  See the testExportSymbolExceptions in
+  // base_test.html for an example.
+  if (!(parts[0] in cur) && cur.execScript) {
+    cur.execScript('var ' + parts[0]);
+  }
+
+  // Certain browsers cannot parse code in the form for((a in b); c;);
+  // This pattern is produced by the JSCompiler when it collapses the
+  // statement above into the conditional loop below. To prevent this from
+  // happening, use a for-loop and reserve the init logic as below.
+
+  // Parentheses added to eliminate strict JS warning in Firefox.
+  for (var part; parts.length && (part = parts.shift());) {
+    if (!parts.length && goog.isDef(opt_object)) {
+      // last part and we have an object; use it
+      cur[part] = opt_object;
+    } else if (cur[part]) {
+      cur = cur[part];
+    } else {
+      cur = cur[part] = {};
+    }
+  }
+};
+
+
+/**
+ * Defines a named value. In uncompiled mode, the value is retrieved from
+ * CLOSURE_DEFINES or CLOSURE_UNCOMPILED_DEFINES if the object is defined and
+ * has the property specified, and otherwise used the defined defaultValue.
+ * When compiled the default can be overridden using the compiler
+ * options or the value set in the CLOSURE_DEFINES object.
+ *
+ * @param {string} name The distinguished name to provide.
+ * @param {string|number|boolean} defaultValue
+ */
+goog.define = function(name, defaultValue) {
+  var value = defaultValue;
+  if (!COMPILED) {
+    if (goog.global.CLOSURE_UNCOMPILED_DEFINES &&
+        Object.prototype.hasOwnProperty.call(
+            goog.global.CLOSURE_UNCOMPILED_DEFINES, name)) {
+      value = goog.global.CLOSURE_UNCOMPILED_DEFINES[name];
+    } else if (
+        goog.global.CLOSURE_DEFINES &&
+        Object.prototype.hasOwnProperty.call(
+            goog.global.CLOSURE_DEFINES, name)) {
+      value = goog.global.CLOSURE_DEFINES[name];
+    }
+  }
+  goog.exportPath_(name, value);
+};
+
+
+/**
+ * @define {boolean} DEBUG is provided as a convenience so that debugging code
+ * that should not be included in a production js_binary can be easily stripped
+ * by specifying --define goog.DEBUG=false to the JSCompiler. For example, most
+ * toString() methods should be declared inside an "if (goog.DEBUG)" conditional
+ * because they are generally used for debugging purposes and it is difficult
+ * for the JSCompiler to statically determine whether they are used.
+ */
+goog.define('goog.DEBUG', true);
+
+
+/**
+ * @define {string} LOCALE defines the locale being used for compilation. It is
+ * used to select locale specific data to be compiled in js binary. BUILD rule
+ * can specify this value by "--define goog.LOCALE=<locale_name>" as JSCompiler
+ * option.
+ *
+ * Take into account that the locale code format is important. You should use
+ * the canonical Unicode format with hyphen as a delimiter. Language must be
+ * lowercase, Language Script - Capitalized, Region - UPPERCASE.
+ * There are few examples: pt-BR, en, en-US, sr-Latin-BO, zh-Hans-CN.
+ *
+ * See more info about locale codes here:
+ * http://www.unicode.org/reports/tr35/#Unicode_Language_and_Locale_Identifiers
+ *
+ * For language codes you should use values defined by ISO 693-1. See it here
+ * http://www.w3.org/WAI/ER/IG/ert/iso639.htm. There is only one exception from
+ * this rule: the Hebrew language. For legacy reasons the old code (iw) should
+ * be used instead of the new code (he), see http://wiki/Main/IIISynonyms.
+ */
+goog.define('goog.LOCALE', 'en');  // default to en
+
+
+/**
+ * @define {boolean} Whether this code is running on trusted sites.
+ *
+ * On untrusted sites, several native functions can be defined or overridden by
+ * external libraries like Prototype, Datejs, and JQuery and setting this flag
+ * to false forces closure to use its own implementations when possible.
+ *
+ * If your JavaScript can be loaded by a third party site and you are wary about
+ * relying on non-standard implementations, specify
+ * "--define goog.TRUSTED_SITE=false" to the JSCompiler.
+ */
+goog.define('goog.TRUSTED_SITE', true);
+
+
+/**
+ * @define {boolean} Whether a project is expected to be running in strict mode.
+ *
+ * This define can be used to trigger alternate implementations compatible with
+ * running in EcmaScript Strict mode or warn about unavailable functionality.
+ * @see https://goo.gl/PudQ4y
+ *
+ */
+goog.define('goog.STRICT_MODE_COMPATIBLE', false);
+
+
+/**
+ * @define {boolean} Whether code that calls {@link goog.setTestOnly} should
+ *     be disallowed in the compilation unit.
+ */
+goog.define('goog.DISALLOW_TEST_ONLY_CODE', COMPILED && !goog.DEBUG);
+
+
+/**
+ * @define {boolean} Whether to use a Chrome app CSP-compliant method for
+ *     loading scripts via goog.require. @see appendScriptSrcNode_.
+ */
+goog.define('goog.ENABLE_CHROME_APP_SAFE_SCRIPT_LOADING', false);
+
+
+/**
+ * Defines a namespace in Closure.
+ *
+ * A namespace may only be defined once in a codebase. It may be defined using
+ * goog.provide() or goog.module().
+ *
+ * The presence of one or more goog.provide() calls in a file indicates
+ * that the file defines the given objects/namespaces.
+ * Provided symbols must not be null or undefined.
+ *
+ * In addition, goog.provide() creates the object stubs for a namespace
+ * (for example, goog.provide("goog.foo.bar") will create the object
+ * goog.foo.bar if it does not already exist).
+ *
+ * Build tools also scan for provide/require/module statements
+ * to discern dependencies, build dependency files (see deps.js), etc.
+ *
+ * @see goog.require
+ * @see goog.module
+ * @param {string} name Namespace provided by this file in the form
+ *     "goog.package.part".
+ */
+goog.provide = function(name) {
+  if (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 symbols by
+ * itself. If declared symbols are desired, use
+ * goog.module.declareLegacyNamespace().
+ *
+ *
+ * See the public goog.module proposal: http://goo.gl/Va1hin
+ *
+ * @param {string} name Namespace provided by this file in the form
+ *     "goog.package.part", is expected but not required.
+ */
+goog.module = function(name) {
+  if (!goog.isString(name) || !name ||
+      name.search(goog.VALID_MODULE_RE_) == -1) {
+    throw Error('Invalid module identifier');
+  }
+  if (!goog.isInModuleLoader_()) {
+    throw Error('Module ' + name + ' has been loaded incorrectly.');
+  }
+  if (goog.moduleLoaderState_.moduleName) {
+    throw Error('goog.module may only be called once per module.');
+  }
+
+  // Store the module name for the loader.
+  goog.moduleLoaderState_.moduleName = name;
+  if (!COMPILED) {
+    // Ensure that the same namespace isn't provided twice.
+    // A goog.module/goog.provide maps a goog.require to a specific file
+    if (goog.isProvided_(name)) {
+      throw Error('Namespace "' + name + '" already declared.');
+    }
+    delete goog.implicitNamespaces_[name];
+  }
+};
+
+
+/**
+ * @param {string} name The module identifier.
+ * @return {?} The module exports for an already loaded module or null.
+ *
+ * Note: This is not an alternative to goog.require, it does not
+ * indicate a hard dependency, instead it is used to indicate
+ * an optional dependency or to access the exports of a module
+ * that has already been loaded.
+ * @suppress {missingProvide}
+ */
+goog.module.get = function(name) {
+  return goog.module.getInternal_(name);
+};
+
+
+/**
+ * @param {string} name The module identifier.
+ * @return {?} The module exports for an already loaded module or null.
+ * @private
+ */
+goog.module.getInternal_ = function(name) {
+  if (!COMPILED) {
+    if (goog.isProvided_(name)) {
+      // goog.require only return a value with-in goog.module files.
+      return name in goog.loadedModules_ ? goog.loadedModules_[name] :
+                                           goog.getObjectByName(name);
+    } else {
+      return null;
+    }
+  }
+};
+
+
+/**
+ * @private {?{moduleName: (string|undefined), declareLegacyNamespace:boolean}}
+ */
+goog.moduleLoaderState_ = null;
+
+
+/**
+ * @private
+ * @return {boolean} Whether a goog.module is currently being initialized.
+ */
+goog.isInModuleLoader_ = function() {
+  return goog.moduleLoaderState_ != null;
+};
+
+
+/**
+ * Provide the module's exports as a globally accessible object under the
+ * module's declared name.  This is intended to ease migration to goog.module
+ * for files that have existing usages.
+ * @suppress {missingProvide}
+ */
+goog.module.declareLegacyNamespace = function() {
+  if (!COMPILED && !goog.isInModuleLoader_()) {
+    throw new Error(
+        'goog.module.declareLegacyNamespace must be called from ' +
+        'within a goog.module');
+  }
+  if (!COMPILED && !goog.moduleLoaderState_.moduleName) {
+    throw Error(
+        'goog.module must be called prior to ' +
+        'goog.module.declareLegacyNamespace.');
+  }
+  goog.moduleLoaderState_.declareLegacyNamespace = true;
+};
+
+
+/**
+ * Marks that the current file should only be used for testing, and never for
+ * live code in production.
+ *
+ * In the case of unit tests, the message may optionally be an exact namespace
+ * for the test (e.g. 'goog.stringTest'). The linter will then ignore the extra
+ * provide (if not explicitly defined in the code).
+ *
+ * @param {string=} opt_message Optional message to add to the error that's
+ *     raised when used in production code.
+ */
+goog.setTestOnly = function(opt_message) {
+  if (goog.DISALLOW_TEST_ONLY_CODE) {
+    opt_message = opt_message || '';
+    throw Error(
+        'Importing test-only code into non-debug environment' +
+        (opt_message ? ': ' + opt_message : '.'));
+  }
+};
+
+
+/**
+ * Forward declares a symbol. This is an indication to the compiler that the
+ * symbol may be used in the source yet is not required and may not be provided
+ * in compilation.
+ *
+ * The most common usage of forward declaration is code that takes a type as a
+ * function parameter but does not need to require it. By forward declaring
+ * instead of requiring, no hard dependency is made, and (if not required
+ * elsewhere) the namespace may never be required and thus, not be pulled
+ * into the JavaScript binary. If it is required elsewhere, it will be type
+ * checked as normal.
+ *
+ *
+ * @param {string} name The namespace to forward declare in the form of
+ *     "goog.package.part".
+ */
+goog.forwardDeclare = function(name) {};
+
+
+/**
+ * Forward declare type information. Used to assign types to goog.global
+ * referenced object that would otherwise result in unknown type references
+ * and thus block property disambiguation.
+ */
+goog.forwardDeclare('Document');
+goog.forwardDeclare('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 {boolean} Whether the name has been provided.
+   * @private
+   */
+  goog.isProvided_ = function(name) {
+    return (name in goog.loadedModules_) ||
+        (!goog.implicitNamespaces_[name] &&
+         goog.isDefAndNotNull(goog.getObjectByName(name)));
+  };
+
+  /**
+   * Namespaces implicitly defined by goog.provide. For example,
+   * goog.provide('goog.events.Event') implicitly declares that 'goog' and
+   * 'goog.events' must be namespaces.
+   *
+   * @type {!Object<string, (boolean|undefined)>}
+   * @private
+   */
+  goog.implicitNamespaces_ = {'goog.module': true};
+
+  // NOTE: We add goog.module as an implicit namespace as goog.module is defined
+  // here and because the existing module package has not been moved yet out of
+  // the goog.module namespace. This satisifies both the debug loader and
+  // ahead-of-time dependency management.
+}
+
+
+/**
+ * Returns an object based on its fully qualified external name.  The object
+ * is not found if null or undefined.  If you are using a compilation pass that
+ * renames property names beware that using this function will not find renamed
+ * properties.
+ *
+ * @param {string} name The fully qualified name.
+ * @param {Object=} opt_obj The object within which to look; default is
+ *     |goog.global|.
+ * @return {?} The value (object or primitive) or, if not found, null.
+ */
+goog.getObjectByName = function(name, opt_obj) {
+  var parts = name.split('.');
+  var cur = opt_obj || goog.global;
+  for (var part; part = parts.shift();) {
+    if (goog.isDefAndNotNull(cur[part])) {
+      cur = cur[part];
+    } else {
+      return null;
+    }
+  }
+  return cur;
+};
+
+
+/**
+ * Globalizes a whole namespace, such as goog or goog.lang.
+ *
+ * @param {!Object} obj The namespace to globalize.
+ * @param {Object=} opt_global The object to add the properties to.
+ * @deprecated Properties may be explicitly exported to the global scope, but
+ *     this should no longer be done in bulk.
+ */
+goog.globalize = function(obj, opt_global) {
+  var global = opt_global || goog.global;
+  for (var x in obj) {
+    global[x] = obj[x];
+  }
+};
+
+
+/**
+ * Adds a dependency from a file to the files it requires.
+ * @param {string} relPath The path to the js file.
+ * @param {!Array<string>} provides An array of strings with
+ *     the names of the objects this file provides.
+ * @param {!Array<string>} requires An array of strings with
+ *     the names of the objects this file requires.
+ * @param {boolean|!Object<string>=} opt_loadFlags Parameters indicating
+ *     how the file must be loaded.  The boolean '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 === 'boolean') {
+      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 {boolean} Whether to enable the debug loader.
+ *
+ * If enabled, a call to goog.require() will attempt to load the namespace by
+ * appending a script tag to the DOM (if the namespace has been registered).
+ *
+ * If disabled, goog.require() will simply assert that the namespace has been
+ * provided (and depend on the fact that some outside tool correctly ordered
+ * the script).
+ */
+goog.define('goog.ENABLE_DEBUG_LOADER', true);
+
+
+/**
+ * @param {string} msg
+ * @private
+ */
+goog.logToConsole_ = function(msg) {
+  if (goog.global.console) {
+    goog.global.console['error'](msg);
+  }
+};
+
+
+/**
+ * Implements a system for the dynamic resolution of dependencies that works in
+ * parallel with the BUILD system. Note that all calls to goog.require will be
+ * stripped by the JSCompiler when the --process_closure_primitives option is
+ * used.
+ * @see goog.provide
+ * @param {string} name Namespace to include (as was given in goog.provide()) in
+ *     the form "goog.package.part".
+ * @return {?} If called within a goog.module file, the associated namespace or
+ *     module otherwise null.
+ */
+goog.require = function(name) {
+  // If the object already exists we do not need do do anything.
+  if (!COMPILED) {
+    if (goog.ENABLE_DEBUG_LOADER && goog.IS_OLD_IE_) {
+      goog.maybeProcessDeferredDep_(name);
+    }
+
+    if (goog.isProvided_(name)) {
+      if (goog.isInModuleLoader_()) {
+        return goog.module.getInternal_(name);
+      } else {
+        return null;
+      }
+    }
+
+    if (goog.ENABLE_DEBUG_LOADER) {
+      var path = goog.getPathFromDeps_(name);
+      if (path) {
+        goog.writeScripts_(path);
+        return null;
+      }
+    }
+
+    var errorMessage = 'goog.require could not find: ' + name;
+    goog.logToConsole_(errorMessage);
+
+    throw Error(errorMessage);
+  }
+};
+
+
+/**
+ * Path for included scripts.
+ * @type {string}
+ */
+goog.basePath = '';
+
+
+/**
+ * A hook for overriding the base path.
+ * @type {string|undefined}
+ */
+goog.global.CLOSURE_BASE_PATH;
+
+
+/**
+ * Whether to write out Closure's deps file. By default, the deps are written.
+ * @type {boolean|undefined}
+ */
+goog.global.CLOSURE_NO_DEPS;
+
+
+/**
+ * A function to import a single script. This is meant to be overridden when
+ * Closure is being run in non-HTML contexts, such as web workers. It's defined
+ * in the global scope so that it can be set before base.js is loaded, which
+ * allows deps.js to be imported properly.
+ *
+ * The function is passed the script source, which is a relative URI. It should
+ * return true if the script was imported, false otherwise.
+ * @type {(function(string): boolean)|undefined}
+ */
+goog.global.CLOSURE_IMPORT_SCRIPT;
+
+
+/**
+ * Null function used for default values of callbacks, etc.
+ * @return {void} Nothing.
+ */
+goog.nullFunction = function() {};
+
+
+/**
+ * When defining a class Foo with an abstract method bar(), you can do:
+ * Foo.prototype.bar = goog.abstractMethod
+ *
+ * Now if a subclass of Foo fails to override bar(), an error will be thrown
+ * when bar() is invoked.
+ *
+ * Note: This does not take the name of the function to override as an argument
+ * because that would make it more difficult to obfuscate our JavaScript code.
+ *
+ * @type {!Function}
+ * @throws {Error} when invoked to indicate the method should be overridden.
+ */
+goog.abstractMethod = function() {
+  throw Error('unimplemented abstract method');
+};
+
+
+/**
+ * Adds a {@code getInstance} static method that always returns the same
+ * instance object.
+ * @param {!Function} ctor The constructor for the class to add the static
+ *     method to.
+ */
+goog.addSingletonGetter = function(ctor) {
+  ctor.getInstance = function() {
+    if (ctor.instance_) {
+      return ctor.instance_;
+    }
+    if (goog.DEBUG) {
+      // NOTE: JSCompiler can't optimize away Array#push.
+      goog.instantiatedSingletons_[goog.instantiatedSingletons_.length] = ctor;
+    }
+    return ctor.instance_ = new ctor;
+  };
+};
+
+
+/**
+ * All singleton classes that have been instantiated, for testing. Don't read
+ * it directly, use the {@code goog.testing.singleton} module. The compiler
+ * removes this variable if unused.
+ * @type {!Array<!Function>}
+ * @private
+ */
+goog.instantiatedSingletons_ = [];
+
+
+/**
+ * @define {boolean} Whether to load goog.modules using {@code eval} when using
+ * the debug loader.  This provides a better debugging experience as the
+ * source is unmodified and can be edited using Chrome Workspaces or similar.
+ * However in some environments the use of {@code eval} is banned
+ * so we provide an alternative.
+ */
+goog.define('goog.LOAD_MODULE_USING_EVAL', true);
+
+
+/**
+ * @define {boolean} Whether the exports of goog.modules should be sealed when
+ * possible.
+ */
+goog.define('goog.SEAL_MODULE_EXPORTS', goog.DEBUG);
+
+
+/**
+ * The registry of initialized modules:
+ * the module identifier to module exports map.
+ * @private @const {!Object<string, ?>}
+ */
+goog.loadedModules_ = {};
+
+
+/**
+ * True if goog.dependencies_ is available.
+ * @const {boolean}
+ */
+goog.DEPENDENCIES_ENABLED = !COMPILED && goog.ENABLE_DEBUG_LOADER;
+
+
+/**
+ * @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, boolean>>,
+   *   visited: !Object<string, boolean>,
+   *   written: !Object<string, boolean>,
+   *   deferred: !Object<string, string>
+   * }}
+   */
+  goog.dependencies_ = {
+    loadFlags: {},  // 1 to 1
+
+    nameToPath: {},  // 1 to 1
+
+    requires: {},  // 1 to many
+
+    // Used when resolving dependencies to prevent us from visiting file twice.
+    visited: {},
+
+    written: {},  // Used to keep track of script files we have written.
+
+    deferred: {}  // Used to track deferred module evaluations in old IEs
+  };
+
+
+  /**
+   * Tries to detect whether is in the context of an HTML document.
+   * @return {boolean} True if it looks like HTML document.
+   * @private
+   */
+  goog.inHtmlDocument_ = function() {
+    /** @type {Document} */
+    var doc = goog.global.document;
+    return 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)) {
+      goog.basePath = goog.global.CLOSURE_BASE_PATH;
+      return;
+    } else if (!goog.inHtmlDocument_()) {
+      return;
+    }
+    /** @type {Document} */
+    var doc = goog.global.document;
+    var scripts = doc.getElementsByTagName('SCRIPT');
+    // Search backwards since the current script is in almost all cases the one
+    // that has base.js.
+    for (var i = scripts.length - 1; i >= 0; --i) {
+      var script = /** @type {!HTMLScriptElement} */ (scripts[i]);
+      var src = script.src;
+      var qmark = src.lastIndexOf('?');
+      var l = qmark == -1 ? src.length : qmark;
+      if (src.substr(l - 7, 7) == 'base.js') {
+        goog.basePath = src.substr(0, l - 7);
+        return;
+      }
+    }
+  };
+
+
+  /**
+   * Imports a script if, and only if, that script hasn't already been imported.
+   * (Must be called at execution time)
+   * @param {string} src Script source.
+   * @param {string=} opt_sourceText The optionally source text to evaluate
+   * @private
+   */
+  goog.importScript_ = function(src, opt_sourceText) {
+    var importScript =
+        goog.global.CLOSURE_IMPORT_SCRIPT || goog.writeScriptTag_;
+    if (importScript(src, opt_sourceText)) {
+      goog.dependencies_.written[src] = true;
+    }
+  };
+
+
+  /**
+   * Whether the browser is IE9 or earlier, which needs special handling
+   * for deferred modules.
+   * @const @private {boolean}
+   */
+  goog.IS_OLD_IE_ =
+      !!(!goog.global.atob && goog.global.document && goog.global.document.all);
+
+
+  /**
+   * Given a URL initiate retrieval and execution of a script that needs
+   * pre-processing.
+   * @param {string} src Script source URL.
+   * @param {boolean} isModule Whether this is a goog.module.
+   * @param {boolean} 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);
+      }
+    }
+  };
+
+
+  /**
+   * Eval the named module if its dependencies are
+   * available.
+   * @param {string} name The module to load.
+   * @private
+   */
+  goog.maybeProcessDeferredDep_ = function(name) {
+    if (goog.isDeferredModule_(name) && goog.allDepsAreAvailable_(name)) {
+      var path = goog.getPathFromDeps_(name);
+      goog.maybeProcessDeferredPath_(goog.basePath + path);
+    }
+  };
+
+  /**
+   * @param {string} name The module to check.
+   * @return {boolean} Whether the name represents a
+   *     module whose evaluation has been deferred.
+   * @private
+   */
+  goog.isDeferredModule_ = function(name) {
+    var path = goog.getPathFromDeps_(name);
+    var loadFlags = path && goog.dependencies_.loadFlags[path] || {};
+    if (path && (loadFlags['module'] == 'goog' ||
+                 goog.needsTranspile_(loadFlags['lang']))) {
+      var abspath = goog.basePath + path;
+      return (abspath) in goog.dependencies_.deferred;
+    }
+    return false;
+  };
+
+  /**
+   * @param {string} name The module to check.
+   * @return {boolean} Whether the name represents a
+   *     module whose declared dependencies have all been loaded
+   *     (eval'd or a deferred module load)
+   * @private
+   */
+  goog.allDepsAreAvailable_ = function(name) {
+    var path = goog.getPathFromDeps_(name);
+    if (path && (path in goog.dependencies_.requires)) {
+      for (var requireName in goog.dependencies_.requires[path]) {
+        if (!goog.isProvided_(requireName) &&
+            !goog.isDeferredModule_(requireName)) {
+          return false;
+        }
+      }
+    }
+    return true;
+  };
+
+
+  /**
+   * @param {string} abspath
+   * @private
+   */
+  goog.maybeProcessDeferredPath_ = function(abspath) {
+    if (abspath in goog.dependencies_.deferred) {
+      var src = goog.dependencies_.deferred[abspath];
+      delete goog.dependencies_.deferred[abspath];
+      goog.globalEval(src);
+    }
+  };
+
+
+  /**
+   * 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 {boolean} 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 symbol after document load,
+      // something has gone terribly wrong. Doing a document.write would
+      // wipe out the page. This does not apply to the CSP-compliant method
+      // of writing script tags.
+      if (!goog.ENABLE_CHROME_APP_SAFE_SCRIPT_LOADING &&
+          doc.readyState == 'complete') {
+        // Certain test frameworks load base.js multiple times, which tries
+        // to write deps.js each time. If that happens, just fail silently.
+        // These frameworks wipe the page between each load of base.js, so this
+        // is OK.
+        var isDeps = /\bdeps.js$/.test(src);
+        if (isDeps) {
+          return false;
+        } else {
+          throw Error('Cannot write "' + src + '" after document load');
+        }
+      }
+
+      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 {
+          var state = " onreadystatechange='goog.onScriptLoad_(this, " +
+              ++goog.lastNonModuleScriptIndex_ + ")' ";
+          doc.write(
+              '<script type="text/javascript" src="' + src + '"' + state +
+              '></' +
+              'script>');
+        }
+      } else {
+        doc.write(
+            '<script type="text/javascript">' + opt_sourceText + '</' +
+            'script>');
+      }
+      return true;
+    } else {
+      return false;
+    }
+  };
+
+
+  /**
+   * Determines whether the given language needs to be transpiled.
+   * @param {string} lang
+   * @return {boolean}
+   * @private
+   */
+  goog.needsTranspile_ = function(lang) {
+    if (goog.TRANSPILE == 'always') {
+      return true;
+    } else if (goog.TRANSPILE == 'never') {
+      return false;
+    } else if (!goog.transpiledLanguages_) {
+      goog.transpiledLanguages_ = {'es5': true, 'es6': true, 'es6-impl': true};
+      /** @preserveTry */
+      try {
+        // Perform some quick conformance checks, to distinguish
+        // between browsers that support es5, es6-impl, or es6.
+
+        // Identify ES3-only browsers by their incorrect treatment of commas.
+        goog.transpiledLanguages_['es5'] = eval('[1,].length!=1');
+
+        // As browsers mature, features will be moved from the full test
+        // into the impl test.  This must happen before the corresponding
+        // features are changed in the Closure Compiler's FeatureSet object.
+
+        // Test 1: es6-impl [FF49, Edge 13, Chrome 49]
+        //   (a) let/const keyword, (b) class expressions, (c) Map object,
+        //   (d) iterable arguments, (e) spread operator
+        var es6implTest =
+            'let a={};const X=class{constructor(){}x(z){return new Map([' +
+            '...arguments]).get(z[0])==3}};return new X().x([a,3])';
+
+        // Test 2: 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}';
+
+        if (eval('(()=>{"use strict";' + es6implTest + '})()')) {
+          goog.transpiledLanguages_['es6-impl'] = false;
+        }
+        if (eval('(()=>{"use strict";' + es6fullTest + '})()')) {
+          goog.transpiledLanguages_['es6'] = false;
+        }
+      } catch (err) {
+      }
+    }
+    return !!goog.transpiledLanguages_[lang];
+  };
+
+
+  /** @private {?Object<string, boolean>} */
+  goog.transpiledLanguages_ = null;
+
+
+  /** @private {number} */
+  goog.lastNonModuleScriptIndex_ = 0;
+
+
+  /**
+   * A readystatechange handler for legacy IE
+   * @param {!HTMLScriptElement} script
+   * @param {number} scriptIndex
+   * @return {boolean}
+   * @private
+   */
+  goog.onScriptLoad_ = function(script, scriptIndex) {
+    // for now load the modules when we reach the last script,
+    // later allow more inter-mingling.
+    if (script.readyState == 'complete' &&
+        goog.lastNonModuleScriptIndex_ == scriptIndex) {
+      goog.loadQueuedModules_();
+    }
+    return true;
+  };
+
+  /**
+   * Resolves dependencies based on the dependencies added using addDependency
+   * and calls importScript_ in the correct order.
+   * @param {string} pathToLoad The path from which to start discovering
+   *     dependencies.
+   * @private
+   */
+  goog.writeScripts_ = function(pathToLoad) {
+    /** @type {!Array<string>} The scripts we need to write this time. */
+    var scripts = [];
+    var seenScript = {};
+    var deps = goog.dependencies_;
+
+    /** @param {string} path */
+    function visitNode(path) {
+      if (path in deps.written) {
+        return;
+      }
+
+      // We have already visited this one. We can get here if we have cyclic
+      // dependencies.
+      if (path in deps.visited) {
+        return;
+      }
+
+      deps.visited[path] = true;
+
+      if (path in deps.requires) {
+        for (var requireName in deps.requires[path]) {
+          // If the required name is defined, we assume that it was already
+          // bootstrapped by other means.
+          if (!goog.isProvided_(requireName)) {
+            if (requireName in deps.nameToPath) {
+              visitNode(deps.nameToPath[requireName]);
+            } else {
+              throw Error('Undefined nameToPath for ' + requireName);
+            }
+          }
+        }
+      }
+
+      if (!(path in seenScript)) {
+        seenScript[path] = true;
+        scripts.push(path);
+      }
+    }
+
+    visitNode(pathToLoad);
+
+    // record that we are going to load all these scripts.
+    for (var i = 0; i < scripts.length; i++) {
+      var path = scripts[i];
+      goog.dependencies_.written[path] = true;
+    }
+
+    // If a module is loaded synchronously then we need to
+    // clear the current inModuleLoader value, and restore it when we are
+    // done loading the current "requires".
+    var moduleState = goog.moduleLoaderState_;
+    goog.moduleLoaderState_ = null;
+
+    for (var i = 0; i < scripts.length; i++) {
+      var path = scripts[i];
+      if (path) {
+        var loadFlags = deps.loadFlags[path] || {};
+        var needsTranspile = goog.needsTranspile_(loadFlags['lang']);
+        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');
+  }
+}
+
+
+/**
+ * @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)) {
+      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) {
+      Object.seal(exports);
+    }
+
+    goog.loadedModules_[moduleName] = exports;
+  } finally {
+    goog.moduleLoaderState_ = previousState;
+  }
+};
+
+
+/**
+ * @private @const {function(string):?}
+ *
+ * The new type inference warns because this function has no formal
+ * parameters, but its jsdoc says that it takes one argument.
+ * (The argument is used via arguments[0], but NTI does not detect this.)
+ * @suppress {newCheckTypes}
+ */
+goog.loadModuleFromSource_ = function() {
+  // NOTE: we avoid declaring parameters or local variables here to avoid
+  // masking globals or leaking values into the module definition.
+  'use strict';
+  var exports = {};
+  eval(arguments[0]);
+  return exports;
+};
+
+
+/**
+ * Normalize a file path by removing redundant ".." and extraneous "." file
+ * path components.
+ * @param {string} path
+ * @return {string}
+ * @private
+ */
+goog.normalizePath_ = function(path) {
+  var components = path.split('/');
+  var i = 0;
+  while (i < components.length) {
+    if (components[i] == '.') {
+      components.splice(i, 1);
+    } else if (
+        i && components[i] == '..' && components[i - 1] &&
+        components[i - 1] != '..') {
+      components.splice(--i, 2);
+    } else {
+      i++;
+    }
+  }
+  return components.join('/');
+};
+
+
+/**
+ * Loads file by synchronous XHR. Should not be used in production environments.
+ * @param {string} src Source URL.
+ * @return {?string} File contents, 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 {boolean} isModule Whether to load as a module.
+ * @param {boolean} 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
+    // console doesn't auto-canonicalize XHR loads as it does <script> srcs.
+    src = goog.normalizePath_(src);
+
+    var importScript =
+        goog.global.CLOSURE_IMPORT_SCRIPT || goog.writeScriptTag_;
+
+    var scriptText = goog.loadFileSync_(src);
+    if (scriptText == null) {
+      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.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);
+      // Note: transpile.js reassigns goog.global['$jscomp'] so pull it again.
+      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.logToConsole_(path + suffix);
+      return code;
+    };
+  }
+  // Note: any transpilation errors/warnings will be logged to the console.
+  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 following steps are taken:
+      //      1. Get the [[Class]] property of this object.
+      //      2. Compute a string value by concatenating the three strings
+      //         "[object ", Result(1), and "]".
+      //      3. Return Result(2).
+      // and this behavior survives the destruction of the execution context.
+      if ((className == '[object Array]' ||
+           // In IE all non value types are wrapped as objects across window
+           // boundaries (not iframe though) so we have to do object detection
+           // for this edge case.
+           typeof value.length == 'number' &&
+               typeof value.splice != 'undefined' &&
+               typeof value.propertyIsEnumerable != 'undefined' &&
+               !value.propertyIsEnumerable('splice')
+
+               )) {
+        return 'array';
+      }
+      // HACK: There is still an array case that fails.
+      //     function ArrayImpostor() {}
+      //     ArrayImpostor.prototype = [];
+      //     var impostor = new ArrayImpostor;
+      // this can be fixed by getting rid of the fast path
+      // (value instanceof Array) and solely relying on
+      // (value && Object.prototype.toString.vall(value) === '[object Array]')
+      // but that would require many more function calls and is not warranted
+      // unless closure code is receiving objects from untrusted sources.
+
+      // IE in cross-window calls does not correctly marshal the function type
+      // (it appears just as an object) so we cannot use just typeof val ==
+      // 'function'. However, if the object has a call property, it is a
+      // function.
+      if ((className == '[object Function]' ||
+           typeof value.call != 'undefined' &&
+               typeof value.propertyIsEnumerable != 'undefined' &&
+               !value.propertyIsEnumerable('call'))) {
+        return 'function';
+      }
+
+    } else {
+      return 'null';
+    }
+
+  } else if (s == 'function' && typeof value.call == 'undefined') {
+    // In Safari typeof nodeList returns 'function', and on Firefox typeof
+    // behaves similarly for HTML{Applet,Embed,Object}, Elements and RegExps. We
+    // would like to return object for those and we can detect an invalid
+    // function by making sure that the function object has a call method.
+    return 'object';
+  }
+  return s;
+};
+
+
+/**
+ * Returns true if the specified value is null.
+ * @param {?} val Variable to test.
+ * @return {boolean} Whether variable is null.
+ */
+goog.isNull = function(val) {
+  return val === null;
+};
+
+
+/**
+ * Returns true if the specified value is defined and not null.
+ * @param {?} val Variable to test.
+ * @return {boolean} Whether variable is defined and not null.
+ */
+goog.isDefAndNotNull = function(val) {
+  // Note that undefined == null.
+  return val != null;
+};
+
+
+/**
+ * Returns true if the specified value is an array.
+ * @param {?} val Variable to test.
+ * @return {boolean} Whether variable is an array.
+ */
+goog.isArray = function(val) {
+  return goog.typeOf(val) == 'array';
+};
+
+
+/**
+ * Returns true if the object looks like an array. To qualify as array like
+ * the value needs to be either a NodeList or an object with a Number length
+ * property. As a special case, a function value is not array like, because its
+ * length property is fixed to correspond to the number of expected arguments.
+ * @param {?} val Variable to test.
+ * @return {boolean} Whether variable is an array.
+ */
+goog.isArrayLike = function(val) {
+  var type = goog.typeOf(val);
+  // We do not use goog.isObject here in order to exclude function values.
+  return type == 'array' || type == 'object' && typeof val.length == 'number';
+};
+
+
+/**
+ * Returns true if the object looks like a Date. To qualify as Date-like the
+ * value needs to be an object and have a getFullYear() function.
+ * @param {?} val Variable to test.
+ * @return {boolean} Whether variable is a like a Date.
+ */
+goog.isDateLike = function(val) {
+  return goog.isObject(val) && typeof val.getFullYear == 'function';
+};
+
+
+/**
+ * Returns true if the specified value is a string.
+ * @param {?} val Variable to test.
+ * @return {boolean} Whether variable is a string.
+ */
+goog.isString = function(val) {
+  return typeof val == 'string';
+};
+
+
+/**
+ * Returns true if the specified value is a boolean.
+ * @param {?} val Variable to test.
+ * @return {boolean} Whether variable is boolean.
+ */
+goog.isBoolean = function(val) {
+  return typeof val == 'boolean';
+};
+
+
+/**
+ * Returns true if the specified value is a number.
+ * @param {?} val Variable to test.
+ * @return {boolean} Whether variable is a number.
+ */
+goog.isNumber = function(val) {
+  return typeof val == 'number';
+};
+
+
+/**
+ * Returns true if the specified value is a function.
+ * @param {?} val Variable to test.
+ * @return {boolean} Whether variable is a function.
+ */
+goog.isFunction = function(val) {
+  return goog.typeOf(val) == 'function';
+};
+
+
+/**
+ * Returns true if the specified value is an object.  This includes arrays and
+ * functions.
+ * @param {?} val Variable to test.
+ * @return {boolean} Whether variable is an object.
+ */
+goog.isObject = function(val) {
+  var type = typeof val;
+  return type == 'object' && val != null || type == 'function';
+  // return Object(val) === val also works, but is slower, especially if val is
+  // not an object.
+};
+
+
+/**
+ * Gets a unique ID for an object. This mutates the object so that further calls
+ * with the same object as a parameter returns the same value. The unique ID is
+ * guaranteed to be unique across the current session amongst objects that are
+ * passed into {@code getUid}. There is no guarantee that the ID is unique or
+ * consistent across sessions. It is unsafe to generate unique ID for function
+ * prototypes.
+ *
+ * @param {Object} obj The object to get the unique ID for.
+ * @return {number} The unique ID for the object.
+ */
+goog.getUid = function(obj) {
+  // TODO(arv): Make the type stricter, do not accept null.
+
+  // In Opera window.hasOwnProperty exists but always returns false so we avoid
+  // using it. As a consequence the unique ID generated for BaseClass.prototype
+  // and SubClass.prototype will be the same.
+  return obj[goog.UID_PROPERTY_] ||
+      (obj[goog.UID_PROPERTY_] = ++goog.uidCounter_);
+};
+
+
+/**
+ * Whether the given object is already assigned a unique ID.
+ *
+ * This does not modify the object.
+ *
+ * @param {!Object} obj The object to check.
+ * @return {boolean} Whether there is an assigned unique id for the object.
+ */
+goog.hasUid = function(obj) {
+  return !!obj[goog.UID_PROPERTY_];
+};
+
+
+/**
+ * Removes the unique ID from an object. This is useful if the object was
+ * previously mutated using {@code goog.getUid} in which case the mutation is
+ * undone.
+ * @param {Object} obj The object to remove the unique ID field from.
+ */
+goog.removeUid = function(obj) {
+  // TODO(arv): Make the type stricter, do not accept null.
+
+  // In IE, DOM nodes are not instances of Object and throw an exception if we
+  // try to delete.  Instead we try to use removeAttribute.
+  if (obj !== null && 'removeAttribute' in obj) {
+    obj.removeAttribute(goog.UID_PROPERTY_);
+  }
+  /** @preserveTry */
+  try {
+    delete obj[goog.UID_PROPERTY_];
+  } catch (ex) {
+  }
+};
+
+
+/**
+ * Name for unique ID property. Initialized in a way to help avoid collisions
+ * with other closure JavaScript on the same page.
+ * @type {string}
+ * @private
+ */
+goog.UID_PROPERTY_ = 'closure_uid_' + ((Math.random() * 1e9) >>> 0);
+
+
+/**
+ * Counter for UID.
+ * @type {number}
+ * @private
+ */
+goog.uidCounter_ = 0;
+
+
+/**
+ * Adds a hash code field to an object. The hash code is unique for the
+ * given object.
+ * @param {Object} obj The object to get the hash code for.
+ * @return {number} The hash code for the object.
+ * @deprecated Use goog.getUid instead.
+ */
+goog.getHashCode = goog.getUid;
+
+
+/**
+ * Removes the hash code field from an object.
+ * @param {Object} obj The object to remove the field from.
+ * @deprecated Use goog.removeUid instead.
+ */
+goog.removeHashCode = goog.removeUid;
+
+
+/**
+ * Clones a value. The input may be an Object, Array, or basic type. Objects and
+ * arrays will be cloned recursively.
+ *
+ * WARNINGS:
+ * <code>goog.cloneObject</code> does not detect reference loops. Objects that
+ * refer to themselves will cause infinite recursion.
+ *
+ * <code>goog.cloneObject</code> is unaware of unique identifiers, and copies
+ * UIDs created by <code>getUid</code> into cloned results.
+ *
+ * @param {*} obj The value to clone.
+ * @return {*} A clone of the input value.
+ * @deprecated goog.cloneObject is unsafe. Prefer the goog.object methods.
+ */
+goog.cloneObject = function(obj) {
+  var type = goog.typeOf(obj);
+  if (type == 'object' || type == 'array') {
+    if (obj.clone) {
+      return obj.clone();
+    }
+    var clone = type == 'array' ? [] : {};
+    for (var key in obj) {
+      clone[key] = goog.cloneObject(obj[key]);
+    }
+    return clone;
+  }
+
+  return obj;
+};
+
+
+/**
+ * A native implementation of goog.bind.
+ * @param {Function} fn A function to partially apply.
+ * @param {Object|undefined} selfObj Specifies the object which this should
+ *     point to when the function is run.
+ * @param {...*} var_args Additional arguments that are partially applied to the
+ *     function.
+ * @return {!Function} A partially-applied form of the function bind() was
+ *     invoked as a method of.
+ * @private
+ * @suppress {deprecated} The compiler thinks that Function.prototype.bind is
+ *     deprecated because some people have declared a pure-JS version.
+ *     Only the pure-JS version is truly deprecated.
+ */
+goog.bindNative_ = function(fn, selfObj, var_args) {
+  return /** @type {!Function} */ (fn.call.apply(fn.bind, arguments));
+};
+
+
+/**
+ * A pure-JS implementation of goog.bind.
+ * @param {Function} fn A function to partially apply.
+ * @param {Object|undefined} selfObj Specifies the object which this should
+ *     point to when the function is run.
+ * @param {...*} var_args Additional arguments that are partially applied to the
+ *     function.
+ * @return {!Function} A partially-applied form of the function bind() was
+ *     invoked as a method of.
+ * @private
+ */
+goog.bindJs_ = function(fn, selfObj, var_args) {
+  if (!fn) {
+    throw new Error();
+  }
+
+  if (arguments.length > 2) {
+    var boundArgs = Array.prototype.slice.call(arguments, 2);
+    return function() {
+      // Prepend the bound arguments to the current arguments.
+      var newArgs = Array.prototype.slice.call(arguments);
+      Array.prototype.unshift.apply(newArgs, boundArgs);
+      return fn.apply(selfObj, newArgs);
+    };
+
+  } else {
+    return function() { return fn.apply(selfObj, arguments); };
+  }
+};
+
+
+/**
+ * Partially applies this function to a particular 'this object' and zero or
+ * more arguments. The result is a new function with some arguments of the first
+ * function pre-filled and the value of this 'pre-specified'.
+ *
+ * Remaining arguments specified at call-time are appended to the pre-specified
+ * ones.
+ *
+ * Also see: {@link #partial}.
+ *
+ * Usage:
+ * <pre>var barMethBound = goog.bind(myFunction, myObj, 'arg1', 'arg2');
+ * barMethBound('arg3', 'arg4');</pre>
+ *
+ * @param {?function(this:T, ...)} fn A function to partially apply.
+ * @param {T} selfObj Specifies the object which this should point to when the
+ *     function is run.
+ * @param {...*} var_args Additional arguments that are partially applied to the
+ *     function.
+ * @return {!Function} A partially-applied form of the function goog.bind() was
+ *     invoked as a method of.
+ * @template T
+ * @suppress {deprecated} See above.
+ */
+goog.bind = function(fn, selfObj, var_args) {
+  // TODO(nicksantos): narrow the type signature.
+  if (Function.prototype.bind &&
+      // NOTE(nicksantos): Somebody pulled base.js into the default Chrome
+      // extension environment. This means that for Chrome extensions, they get
+      // the implementation of Function.prototype.bind that calls goog.bind
+      // instead of the native one. Even worse, we don't want to introduce a
+      // circular dependency between goog.bind and Function.prototype.bind, so
+      // we have to hack this to make sure it works correctly.
+      Function.prototype.bind.toString().indexOf('native code') != -1) {
+    goog.bind = goog.bindNative_;
+  } else {
+    goog.bind = goog.bindJs_;
+  }
+  return goog.bind.apply(null, arguments);
+};
+
+
+/**
+ * Like goog.bind(), except that a 'this object' is not required. Useful when
+ * the target function is already bound.
+ *
+ * Usage:
+ * var g = goog.partial(f, arg1, arg2);
+ * g(arg3, arg4);
+ *
+ * @param {Function} fn A function to partially apply.
+ * @param {...*} var_args Additional arguments that are partially applied to fn.
+ * @return {!Function} A partially-applied form of the function goog.partial()
+ *     was invoked as a method of.
+ */
+goog.partial = function(fn, var_args) {
+  var args = Array.prototype.slice.call(arguments, 1);
+  return function() {
+    // Clone the array (with slice()) and append additional arguments
+    // to the existing arguments.
+    var newArgs = args.slice();
+    newArgs.push.apply(newArgs, arguments);
+    return fn.apply(this, newArgs);
+  };
+};
+
+
+/**
+ * Copies all the members of a source object to a target object. This method
+ * does not work on all browsers for all objects that contain keys such as
+ * toString or hasOwnProperty. Use goog.object.extend for this purpose.
+ * @param {Object} target Target.
+ * @param {Object} source Source.
+ */
+goog.mixin = function(target, source) {
+  for (var x in source) {
+    target[x] = source[x];
+  }
+
+  // For IE7 or lower, the for-in-loop does not contain any properties that are
+  // not enumerable on the prototype object (for example, isPrototypeOf from
+  // Object.prototype) but also it will not include 'replace' on objects that
+  // extend String and change 'replace' (not that it is common for anyone to
+  // extend anything except Object).
+};
+
+
+/**
+ * @return {number} An integer value representing the number of milliseconds
+ *     between midnight, January 1, 1970 and the current time.
+ */
+goog.now = (goog.TRUSTED_SITE && Date.now) || (function() {
+             // Unary plus operator converts its operand to a number which in
+             // the case of
+             // a date is done by calling getTime().
+             return +new Date();
+           });
+
+
+/**
+ * Evals JavaScript in the global scope.  In IE this uses execScript, other
+ * browsers use goog.global.eval. If goog.global.eval does not evaluate in the
+ * global scope (for example, in Safari), appends a script tag instead.
+ * Throws an exception if neither execScript or eval is defined.
+ * @param {string} script JavaScript string.
+ */
+goog.globalEval = function(script) {
+  if (goog.global.execScript) {
+    goog.global.execScript(script, 'JavaScript');
+  } else if (goog.global.eval) {
+    // Test to see if eval works
+    if (goog.evalWorksForGlobals_ == null) {
+      goog.global.eval('var _evalTest_ = 1;');
+      if (typeof goog.global['_evalTest_'] != 'undefined') {
+        try {
+          delete goog.global['_evalTest_'];
+        } catch (ignore) {
+          // Microsoft edge fails the deletion above in strict mode.
+        }
+        goog.evalWorksForGlobals_ = true;
+      } else {
+        goog.evalWorksForGlobals_ = false;
+      }
+    }
+
+    if (goog.evalWorksForGlobals_) {
+      goog.global.eval(script);
+    } else {
+      /** @type {Document} */
+      var doc = goog.global.document;
+      var scriptElt =
+          /** @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 Boolean by the first call to goog.globalEval (which
+ * empirically tests whether eval works for globals). @see goog.globalEval
+ * @type {?boolean}
+ * @private
+ */
+goog.evalWorksForGlobals_ = null;
+
+
+/**
+ * Optional map of CSS class names to obfuscated names used with
+ * goog.getCssName().
+ * @private {!Object<string, string>|undefined}
+ * @see goog.setCssNameMapping
+ */
+goog.cssNameMapping_;
+
+
+/**
+ * Optional obfuscation style for CSS class names. Should be set to either
+ * 'BY_WHOLE' or 'BY_PART' if defined.
+ * @type {string|undefined}
+ * @private
+ * @see goog.setCssNameMapping
+ */
+goog.cssNameMappingStyle_;
+
+
+/**
+ * Handles strings that are intended to be used as CSS class names.
+ *
+ * This function works in tandem with @see goog.setCssNameMapping.
+ *
+ * Without any mapping set, the arguments are simple joined with a hyphen and
+ * passed through unaltered.
+ *
+ * When there is a mapping, there are two possible styles in which these
+ * mappings are used. In the BY_PART style, each part (i.e. in between hyphens)
+ * of the passed in css name is rewritten according to the map. In the BY_WHOLE
+ * style, the full css name is looked up in the map directly. If a rewrite is
+ * not specified by the map, the compiler will output a warning.
+ *
+ * When the mapping is passed to the compiler, it will replace calls to
+ * goog.getCssName with the strings from the mapping, e.g.
+ *     var x = goog.getCssName('foo');
+ *     var y = goog.getCssName(this.baseClass, 'active');
+ *  becomes:
+ *     var x = 'foo';
+ *     var y = this.baseClass + '-active';
+ *
+ * If one argument is passed it will be processed, if two are passed only the
+ * modifier will be processed, as it is assumed the first argument was generated
+ * as a result of calling goog.getCssName.
+ *
+ * @param {string} className The class name.
+ * @param {string=} opt_modifier A modifier to be appended to the class name.
+ * @return {string} The class name or the concatenation of the class name and
+ *     the modifier.
+ */
+goog.getCssName = function(className, opt_modifier) {
+  var getMapping = function(cssName) {
+    return goog.cssNameMapping_[cssName] || cssName;
+  };
+
+  var renameByParts = function(cssName) {
+    // Remap all the parts individually.
+    var parts = cssName.split('-');
+    var mapped = [];
+    for (var i = 0; i < parts.length; i++) {
+      mapped.push(getMapping(parts[i]));
+    }
+    return mapped.join('-');
+  };
+
+  var rename;
+  if (goog.cssNameMapping_) {
+    rename =
+        goog.cssNameMappingStyle_ == 'BY_WHOLE' ? getMapping : renameByParts;
+  } else {
+    rename = function(a) { return a; };
+  }
+
+  if (opt_modifier) {
+    return className + '-' + rename(opt_modifier);
+  } else {
+    return rename(className);
+  }
+};
+
+
+/**
+ * Sets the map to check when returning a value from goog.getCssName(). Example:
+ * <pre>
+ * goog.setCssNameMapping({
+ *   "goog": "a",
+ *   "disabled": "b",
+ * });
+ *
+ * var x = goog.getCssName('goog');
+ * // The following evaluates to: "a a-b".
+ * goog.getCssName('goog') + ' ' + goog.getCssName(x, 'disabled')
+ * </pre>
+ * When declared as a map of string literals to string literals, the JSCompiler
+ * will replace all calls to goog.getCssName() using the supplied map if the
+ * --process_closure_primitives flag is set.
+ *
+ * @param {!Object} mapping A map of strings to strings where keys are possible
+ *     arguments to goog.getCssName() and values are the corresponding values
+ *     that should be returned.
+ * @param {string=} opt_style The style of css name mapping. There are two valid
+ *     options: 'BY_PART', and 'BY_WHOLE'.
+ * @see goog.getCssName for a description.
+ */
+goog.setCssNameMapping = function(mapping, opt_style) {
+  goog.cssNameMapping_ = mapping;
+  goog.cssNameMappingStyle_ = opt_style;
+};
+
+
+/**
+ * To use CSS renaming in compiled mode, one of the input files should have a
+ * call to goog.setCssNameMapping() with an object literal that the JSCompiler
+ * can extract and use to replace all calls to goog.getCssName(). In uncompiled
+ * mode, JavaScript code should be loaded before this base.js file that declares
+ * a global variable, CLOSURE_CSS_NAME_MAPPING, which is used below. This is
+ * to ensure that the mapping is loaded before any calls to goog.getCssName()
+ * are made in uncompiled mode.
+ *
+ * A hook for overriding the CSS name mapping.
+ * @type {!Object<string, string>|undefined}
+ */
+goog.global.CLOSURE_CSS_NAME_MAPPING;
+
+
+if (!COMPILED && goog.global.CLOSURE_CSS_NAME_MAPPING) {
+  // This does not call goog.setCssNameMapping() because the JSCompiler
+  // requires that goog.setCssNameMapping() be called with an object literal.
+  goog.cssNameMapping_ = goog.global.CLOSURE_CSS_NAME_MAPPING;
+}
+
+
+/**
+ * Gets a localized message.
+ *
+ * This function is a compiler primitive. If you give the compiler a localized
+ * message bundle, it will replace the string at compile-time with a localized
+ * version, and expand goog.getMsg call to a concatenated string.
+ *
+ * Messages must be initialized in the form:
+ * <code>
+ * var MSG_NAME = goog.getMsg('Hello {$placeholder}', {'placeholder': 'world'});
+ * </code>
+ *
+ * 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 holders in the form {$foo}.
+ * @param {Object<string, string>=} opt_values Maps place holder name to value.
+ * @return {string} message with placeholders filled.
+ */
+goog.getMsg = function(str, opt_values) {
+  if (opt_values) {
+    str = str.replace(/\{\$([^}]+)}/g, function(match, key) {
+      return (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.exportSymbol('public.path.Foo', Foo);
+ *
+ * ex. goog.exportSymbol('public.path.Foo.staticFunction', Foo.staticFunction);
+ *     public.path.Foo.staticFunction();
+ *
+ * ex. goog.exportSymbol('public.path.Foo.prototype.myMethod',
+ *                       Foo.prototype.myMethod);
+ *     new public.path.Foo().myMethod();
+ *
+ * @param {string} publicPath Unobfuscated name to export.
+ * @param {*} object Object the name should point to.
+ * @param {Object=} opt_objectToExportTo The object to add the path to; default
+ *     is goog.global.
+ */
+goog.exportSymbol = function(publicPath, object, opt_objectToExportTo) {
+  goog.exportPath_(publicPath, object, opt_objectToExportTo);
+};
+
+
+/**
+ * Exports a property unobfuscated into the object's namespace.
+ * ex. goog.exportProperty(Foo, 'staticFunction', Foo.staticFunction);
+ * ex. goog.exportProperty(Foo.prototype, 'myMethod', Foo.prototype.myMethod);
+ * @param {Object} object Object whose static property is being exported.
+ * @param {string} publicName Unobfuscated name to export.
+ * @param {*} symbol Object the name should point to.
+ */
+goog.exportProperty = function(object, publicName, symbol) {
+  object[publicName] = symbol;
+};
+
+
+/**
+ * Inherit the prototype methods from one constructor into another.
+ *
+ * Usage:
+ * <pre>
+ * function ParentClass(a, b) { }
+ * ParentClass.prototype.foo = function(a) { };
+ *
+ * function ChildClass(a, b, c) {
+ *   ChildClass.base(this, 'constructor', a, b);
+ * }
+ * goog.inherits(ChildClass, ParentClass);
+ *
+ * var child = new ChildClass('a', 'b', 'see');
+ * child.foo(); // This works.
+ * </pre>
+ *
+ * @param {!Function} childCtor Child class.
+ * @param {!Function} parentCtor Parent class.
+ */
+goog.inherits = function(childCtor, parentCtor) {
+  /** @constructor */
+  function tempCtor() {}
+  tempCtor.prototype = parentCtor.prototype;
+  childCtor.superClass_ = parentCtor.prototype;
+  childCtor.prototype = new tempCtor();
+  /** @override */
+  childCtor.prototype.constructor = childCtor;
+
+  /**
+   * Calls superclass constructor/method.
+   *
+   * This function is only available if you use goog.inherits to
+   * express inheritance relationships between classes.
+   *
+   * NOTE: This is a replacement for goog.base and for superClass_
+   * property defined in childCtor.
+   *
+   * @param {!Object} me Should always be "this".
+   * @param {string} methodName The method name to call. Calling
+   *     superclass constructor can be done with the special string
+   *     'constructor'.
+   * @param {...*} var_args The arguments to pass to superclass
+   *     method/constructor.
+   * @return {*} The return value of the superclass method/constructor.
+   */
+  childCtor.base = function(me, methodName, var_args) {
+    // Copying using loop to avoid deop due to passing arguments object to
+    // function. This is faster in many JS engines as of late 2014.
+    var args = new Array(arguments.length - 2);
+    for (var i = 2; i < arguments.length; i++) {
+      args[i - 2] = arguments[i];
+    }
+    return parentCtor.prototype[methodName].apply(me, args);
+  };
+};
+
+
+/**
+ * Call up to the superclass.
+ *
+ * If this is called from a constructor, then this calls the superclass
+ * constructor with arguments 1-N.
+ *
+ * If this is called from a prototype method, then you must pass the name of the
+ * method as the second argument to this function. If you do not, you will get a
+ * runtime error. This calls the superclass' method with arguments 2-N.
+ *
+ * This function only works if you use goog.inherits to express inheritance
+ * relationships between your classes.
+ *
+ * This function is a compiler primitive. At compile-time, the compiler will do
+ * macro expansion to remove a lot of the extra overhead that this function
+ * introduces. The compiler will also enforce a lot of the assumptions that this
+ * function makes, and treat it as a compiler error if you break them.
+ *
+ * @param {!Object} me Should always be "this".
+ * @param {*=} opt_methodName The method name if calling a super method.
+ * @param {...*} var_args The rest of the arguments.
+ * @return {*} The return value of the superclass method.
+ * @suppress {es5Strict} This method can not be used in strict mode, but
+ *     all Closure Library consumers must depend on this file.
+ */
+goog.base = function(me, opt_methodName, var_args) {
+  var caller = arguments.callee.caller;
+
+  if (goog.STRICT_MODE_COMPATIBLE || (goog.DEBUG && !caller)) {
+    throw Error(
+        'arguments.caller not defined.  goog.base() cannot be used ' +
+        'with strict mode code. See ' +
+        'http://www.ecma-international.org/ecma-262/5.1/#sec-C');
+  }
+
+  if (caller.superClass_) {
+    // Copying using loop to avoid deop due to passing arguments object to
+    // function. This is faster in many JS engines as of late 2014.
+    var ctorArgs = new Array(arguments.length - 1);
+    for (var i = 1; i < arguments.length; i++) {
+      ctorArgs[i - 1] = arguments[i];
+    }
+    // This is a constructor. Call the superclass constructor.
+    return caller.superClass_.constructor.apply(me, ctorArgs);
+  }
+
+  // Copying using loop to avoid deop due to passing arguments object to
+  // function. This is faster in many JS engines as of late 2014.
+  var args = new Array(arguments.length - 2);
+  for (var i = 2; i < arguments.length; i++) {
+    args[i - 2] = arguments[i];
+  }
+  var foundCaller = false;
+  for (var ctor = me.constructor; ctor;
+       ctor = ctor.superClass_ && ctor.superClass_.constructor) {
+    if (ctor.prototype[opt_methodName] === caller) {
+      foundCaller = true;
+    } else if (foundCaller) {
+      return ctor.prototype[opt_methodName].apply(me, args);
+    }
+  }
+
+  // If we did not find the caller in the prototype chain, then one of two
+  // things happened:
+  // 1) The caller is an instance method.
+  // 2) This method was not called by the right caller.
+  if (me[opt_methodName] === caller) {
+    return me.constructor.prototype[opt_methodName].apply(me, args);
+  } else {
+    throw Error(
+        'goog.base called from a method of one name ' +
+        'to a method of a different name');
+  }
+};
+
+
+/**
+ * Allow for aliasing within scope functions.  This function exists for
+ * uncompiled code - in compiled code the calls will be inlined and the aliases
+ * applied.  In uncompiled code the function is simply run since the aliases as
+ * written are valid JavaScript.
+ *
+ *
+ * @param {function()} fn Function to call.  This function can contain aliases
+ *     to namespaces (e.g. "var dom = goog.dom") or classes
+ *     (e.g. "var Timer = goog.Timer").
+ */
+goog.scope = function(fn) {
+  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 symbols "goog" and "COMPILED".
+ * Exporting "goog" breaks the compiler optimizations, so we required that
+ * be defined externally.
+ * NOTE: We don't use goog.exportSymbol here because we don't want to trigger
+ * extern generation when that compiler option is enabled.
+ */
+if (!COMPILED) {
+  goog.global['COMPILED'] = COMPILED;
+}
+
+
+//==============================================================================
+// goog.defineClass implementation
+//==============================================================================
+
+
+/**
+ * Creates a restricted form of a Closure "class":
+ *   - from the compiler's perspective, the instance returned from the
+ *     constructor is sealed (no new properties may be added).  This enables
+ *     better checks.
+ *   - the compiler will rewrite this definition to a form that is optimal
+ *     for type checking and optimization (initially this will be a more
+ *     traditional form).
+ *
+ * @param {Function} superClass The superclass, Object or null.
+ * @param {goog.defineClass.ClassDescriptor} def
+ *     An object literal describing
+ *     the class.  It may have the following properties:
+ *     "constructor": the constructor function
+ *     "statics": an object literal containing methods to add to the constructor
+ *        as "static" methods or a function that will receive the constructor
+ *        function as its only parameter to which static properties can
+ *        be added.
+ *     all other properties are added to the prototype.
+ * @return {!Function} The class constructor.
+ */
+goog.defineClass = function(superClass, def) {
+  // TODO(johnlenz): consider making the superClass an optional parameter.
+  var constructor = def.constructor;
+  var statics = def.statics;
+  // Wrap the constructor prior to setting up the prototype and static methods.
+  if (!constructor || constructor == Object.prototype.constructor) {
+    constructor = function() {
+      throw Error('cannot instantiate an interface (no constructor defined).');
+    };
+  }
+
+  var cls = goog.defineClass.createSealingConstructor_(constructor, superClass);
+  if (superClass) {
+    goog.inherits(cls, superClass);
+  }
+
+  // Remove all the properties that should not be copied to the prototype.
+  delete def.constructor;
+  delete def.statics;
+
+  goog.defineClass.applyProperties_(cls.prototype, def);
+  if (statics != null) {
+    if (statics instanceof Function) {
+      statics(cls);
+    } else {
+      goog.defineClass.applyProperties_(cls, statics);
+    }
+  }
+
+  return cls;
+};
+
+
+/**
+ * @typedef {{
+ *   constructor: (!Function|undefined),
+ *   statics: (Object|undefined|function(Function):void)
+ * }}
+ * @suppress {missingProvide}
+ */
+goog.defineClass.ClassDescriptor;
+
+
+/**
+ * @define {boolean} 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.
+ * @returns {boolean} 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 older idiom of assigning properties on the
+ * prototype rather than in the constructor. As such, goog.defineClass
+ * must not seal subclasses of these old-style classes until they are fixed.
+ * Until then, this marks a class as "broken", instructing defineClass
+ * not to seal subclasses.
+ * @param {!Function} ctr The legacy constructor to tag as unsealable.
+ */
+goog.tagUnsealableClass = function(ctr) {
+  if (!COMPILED && goog.defineClass.SEAL_CLASS_INSTANCES) {
+    ctr.prototype[goog.UNSEALABLE_CONSTRUCTOR_PROPERTY_] = true;
+  }
+};
+
+
+/**
+ * Name for unsealable tag property.
+ * @const @private {string}
+ */
+goog.UNSEALABLE_CONSTRUCTOR_PROPERTY_ = 'goog_defineClass_legacy_unsealable';
+
+goog.provide('ol');
+
+
+/**
+ * Constants defined with the define tag cannot be changed in application
+ * code, but can be set at compile time.
+ * Some reduce the size of the build in advanced compile mode.
+ */
+
+
+/**
+ * @define {boolean} Assume touch.  Default is `false`.
+ */
+ol.ASSUME_TOUCH = false;
+
+
+/**
+ * TODO: rename this to something having to do with tile grids
+ * see https://github.com/openlayers/ol3/issues/2076
+ * @define {number} Default maximum zoom for default tile grids.
+ */
+ol.DEFAULT_MAX_ZOOM = 42;
+
+
+/**
+ * @define {number} Default min zoom level for the map view.  Default is `0`.
+ */
+ol.DEFAULT_MIN_ZOOM = 0;
+
+
+/**
+ * @define {number} Default maximum allowed threshold  (in pixels) for
+ *     reprojection triangulation. Default is `0.5`.
+ */
+ol.DEFAULT_RASTER_REPROJECTION_ERROR_THRESHOLD = 0.5;
+
+
+/**
+ * @define {number} Default tile size.
+ */
+ol.DEFAULT_TILE_SIZE = 256;
+
+
+/**
+ * @define {string} Default WMS version.
+ */
+ol.DEFAULT_WMS_VERSION = '1.3.0';
+
+
+/**
+ * @define {number} Hysteresis pixels.
+ */
+ol.DRAG_BOX_HYSTERESIS_PIXELS = 8;
+
+
+/**
+ * @define {boolean} Enable the Canvas renderer.  Default is `true`. Setting
+ *     this to false at compile time in advanced mode removes all code
+ *     supporting the Canvas renderer from the build.
+ */
+ol.ENABLE_CANVAS = true;
+
+
+/**
+ * @define {boolean} Enable the DOM renderer (used as a fallback where Canvas is
+ *     not available).  Default is `true`. Setting this to false at compile time
+ *     in advanced mode removes all code supporting the DOM renderer from the
+ *     build.
+ */
+ol.ENABLE_DOM = true;
+
+
+/**
+ * @define {boolean} Enable rendering of ol.layer.Image based layers.  Default
+ *     is `true`. Setting this to false at compile time in advanced mode removes
+ *     all code supporting Image layers from the build.
+ */
+ol.ENABLE_IMAGE = true;
+
+
+/**
+ * @define {boolean} Enable Closure named colors (`goog.color.names`).
+ *     Enabling these colors adds about 3KB uncompressed / 1.5KB compressed to
+ *     the final build size.  Default is `false`. This setting has no effect
+ *     with Canvas renderer, which uses its own names, whether this is true or
+ *     false.
+ */
+ol.ENABLE_NAMED_COLORS = false;
+
+
+/**
+ * @define {boolean} Enable integration with the Proj4js library.  Default is
+ *     `true`.
+ */
+ol.ENABLE_PROJ4JS = true;
+
+
+/**
+ * @define {boolean} Enable automatic reprojection of raster sources. Default is
+ *     `true`.
+ */
+ol.ENABLE_RASTER_REPROJECTION = true;
+
+
+/**
+ * @define {boolean} Enable rendering of ol.layer.Tile based layers.  Default is
+ *     `true`. Setting this to false at compile time in advanced mode removes
+ *     all code supporting Tile layers from the build.
+ */
+ol.ENABLE_TILE = true;
+
+
+/**
+ * @define {boolean} Enable rendering of ol.layer.Vector based layers.  Default
+ *     is `true`. Setting this to false at compile time in advanced mode removes
+ *     all code supporting Vector layers from the build.
+ */
+ol.ENABLE_VECTOR = true;
+
+
+/**
+ * @define {boolean} Enable rendering of ol.layer.VectorTile based layers.
+ *     Default is `true`. Setting this to false at compile time in advanced mode
+ *     removes all code supporting VectorTile layers from the build.
+ */
+ol.ENABLE_VECTOR_TILE = true;
+
+
+/**
+ * @define {boolean} Enable the WebGL renderer.  Default is `true`. Setting
+ *     this to false at compile time in advanced mode removes all code
+ *     supporting the WebGL renderer from the build.
+ */
+ol.ENABLE_WEBGL = true;
+
+
+/**
+ * @define {number} The size in pixels of the first atlas image. Default is
+ * `256`.
+ */
+ol.INITIAL_ATLAS_SIZE = 256;
+
+
+/**
+ * @define {number} The maximum size in pixels of atlas images. Default is
+ * `-1`, meaning it is not used (and `ol.WEBGL_MAX_TEXTURE_SIZE` is
+ * used instead).
+ */
+ol.MAX_ATLAS_SIZE = -1;
+
+
+/**
+ * @define {number} Maximum mouse wheel delta.
+ */
+ol.MOUSEWHEELZOOM_MAXDELTA = 1;
+
+
+/**
+ * @define {number} Mouse wheel timeout duration.
+ */
+ol.MOUSEWHEELZOOM_TIMEOUT_DURATION = 80;
+
+
+/**
+ * @define {number} Maximum width and/or height extent ratio that determines
+ * when the overview map should be zoomed out.
+ */
+ol.OVERVIEWMAP_MAX_RATIO = 0.75;
+
+
+/**
+ * @define {number} Minimum width and/or height extent ratio that determines
+ * when the overview map should be zoomed in.
+ */
+ol.OVERVIEWMAP_MIN_RATIO = 0.1;
+
+
+/**
+ * @define {number} Maximum number of source tiles for raster reprojection of
+ *     a single tile.
+ *     If too many source tiles are determined to be loaded to create a single
+ *     reprojected tile the browser can become unresponsive or even crash.
+ *     This can happen if the developer defines projections improperly and/or
+ *     with unlimited extents.
+ *     If too many tiles are required, no tiles are loaded and
+ *     `ol.TileState.ERROR` state is set. Default is `100`.
+ */
+ol.RASTER_REPROJECTION_MAX_SOURCE_TILES = 100;
+
+
+/**
+ * @define {number} Maximum number of subdivision steps during raster
+ *     reprojection triangulation. Prevents high memory usage and large
+ *     number of proj4 calls (for certain transformations and areas).
+ *     At most `2*(2^this)` triangles are created for each triangulated
+ *     extent (tile/image). Default is `10`.
+ */
+ol.RASTER_REPROJECTION_MAX_SUBDIVISION = 10;
+
+
+/**
+ * @define {number} Maximum allowed size of triangle relative to world width.
+ *     When transforming corners of world extent between certain projections,
+ *     the resulting triangulation seems to have zero error and no subdivision
+ *     is performed.
+ *     If the triangle width is more than this (relative to world width; 0-1),
+ *     subdivison is forced (up to `ol.RASTER_REPROJECTION_MAX_SUBDIVISION`).
+ *     Default is `0.25`.
+ */
+ol.RASTER_REPROJECTION_MAX_TRIANGLE_WIDTH = 0.25;
+
+
+/**
+ * @define {number} Tolerance for geometry simplification in device pixels.
+ */
+ol.SIMPLIFY_TOLERANCE = 0.5;
+
+
+/**
+ * @define {number} Texture cache high water mark.
+ */
+ol.WEBGL_TEXTURE_CACHE_HIGH_WATER_MARK = 1024;
+
+
+/**
+ * The maximum supported WebGL texture size in pixels. If WebGL is not
+ * supported, the value is set to `undefined`.
+ * @const
+ * @type {number|undefined}
+ */
+ol.WEBGL_MAX_TEXTURE_SIZE; // value is set in `ol.has`
+
+
+/**
+ * List of supported WebGL extensions.
+ * @const
+ * @type {Array.<string>}
+ */
+ol.WEBGL_EXTENSIONS; // value is set in `ol.has`
+
+
+/**
+ * Inherit the prototype methods from one constructor into another.
+ *
+ * Usage:
+ *
+ *     function ParentClass(a, b) { }
+ *     ParentClass.prototype.foo = function(a) { }
+ *
+ *     function ChildClass(a, b, c) {
+ *       // Call parent constructor
+ *       ParentClass.call(this, a, b);
+ *     }
+ *     ol.inherits(ChildClass, ParentClass);
+ *
+ *     var child = new ChildClass('a', 'b', 'see');
+ *     child.foo(); // This works.
+ *
+ * @param {!Function} childCtor Child constructor.
+ * @param {!Function} parentCtor Parent constructor.
+ * @function
+ * @api
+ */
+ol.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.
+ */
+ol.nullFunction = function() {};
+
+
+ol.global = Function('return this')();
+
+// Copyright 2009 The Closure Library Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS-IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+/**
+ * @fileoverview Provides a base class for custom Error objects such that the
+ * stack is correctly maintained.
+ *
+ * You should never need to throw goog.debug.Error(msg) directly, Error(msg) is
+ * sufficient.
+ *
+ */
+
+goog.provide('goog.debug.Error');
+
+
+
+/**
+ * Base class for custom error objects.
+ * @param {*=} opt_msg The message associated with the error.
+ * @constructor
+ * @extends {Error}
+ */
+goog.debug.Error = function(opt_msg) {
+
+  // Attempt to ensure there is a stack trace.
+  if (Error.captureStackTrace) {
+    Error.captureStackTrace(this, goog.debug.Error);
+  } else {
+    var stack = new Error().stack;
+    if (stack) {
+      this.stack = stack;
+    }
+  }
+
+  if (opt_msg) {
+    this.message = String(opt_msg);
+  }
+
+  /**
+   * Whether to report this error to the server. Setting this to false will
+   * cause the error reporter to not report the error back to the server,
+   * which can be useful if the client knows that the error has already been
+   * logged on the server.
+   * @type {boolean}
+   */
+  this.reportErrorToServer = true;
+};
+goog.inherits(goog.debug.Error, Error);
+
+
+/** @override */
+goog.debug.Error.prototype.name = 'CustomError';
+
+// Copyright 2006 The Closure Library Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS-IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+/**
+ * @fileoverview Definition of goog.dom.NodeType.
+ */
+
+goog.provide('goog.dom.NodeType');
+
+
+/**
+ * Constants for the nodeType attribute in the Node interface.
+ *
+ * These constants match those specified in the Node interface. These are
+ * usually present on the Node object in recent browsers, but not in older
+ * browsers (specifically, early IEs) and thus are given here.
+ *
+ * In some browsers (early IEs), these are not defined on the Node object,
+ * so they are provided here.
+ *
+ * See http://www.w3.org/TR/DOM-Level-2-Core/core.html#ID-1950641247
+ * @enum {number}
+ */
+goog.dom.NodeType = {
+  ELEMENT: 1,
+  ATTRIBUTE: 2,
+  TEXT: 3,
+  CDATA_SECTION: 4,
+  ENTITY_REFERENCE: 5,
+  ENTITY: 6,
+  PROCESSING_INSTRUCTION: 7,
+  COMMENT: 8,
+  DOCUMENT: 9,
+  DOCUMENT_TYPE: 10,
+  DOCUMENT_FRAGMENT: 11,
+  NOTATION: 12
+};
+
+// Copyright 2006 The Closure Library Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS-IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+/**
+ * @fileoverview Utilities for string manipulation.
+ * @author arv@google.com (Erik Arvidsson)
+ */
+
+
+/**
+ * Namespace for string utilities
+ */
+goog.provide('goog.string');
+goog.provide('goog.string.Unicode');
+
+
+/**
+ * @define {boolean} Enables HTML escaping of lowercase letter "e" which helps
+ * with detection of double-escaping as this letter is frequently used.
+ */
+goog.define('goog.string.DETECT_DOUBLE_ESCAPING', false);
+
+
+/**
+ * @define {boolean} Whether to force non-dom html unescaping.
+ */
+goog.define('goog.string.FORCE_NON_DOM_HTML_UNESCAPING', false);
+
+
+/**
+ * Common Unicode string characters.
+ * @enum {string}
+ */
+goog.string.Unicode = {
+  NBSP: '\xa0'
+};
+
+
+/**
+ * Fast prefix-checker.
+ * @param {string} str The string to check.
+ * @param {string} prefix A string to look for at the start of {@code str}.
+ * @return {boolean} True if {@code str} begins with {@code prefix}.
+ */
+goog.string.startsWith = function(str, prefix) {
+  return str.lastIndexOf(prefix, 0) == 0;
+};
+
+
+/**
+ * Fast suffix-checker.
+ * @param {string} str The string to check.
+ * @param {string} suffix A string to look for at the end of {@code str}.
+ * @return {boolean} True if {@code str} ends with {@code suffix}.
+ */
+goog.string.endsWith = function(str, suffix) {
+  var l = str.length - suffix.length;
+  return l >= 0 && str.indexOf(suffix, l) == l;
+};
+
+
+/**
+ * Case-insensitive prefix-checker.
+ * @param {string} str The string to check.
+ * @param {string} prefix  A string to look for at the end of {@code str}.
+ * @return {boolean} True if {@code str} begins with {@code prefix} (ignoring
+ *     case).
+ */
+goog.string.caseInsensitiveStartsWith = function(str, prefix) {
+  return goog.string.caseInsensitiveCompare(
+             prefix, str.substr(0, prefix.length)) == 0;
+};
+
+
+/**
+ * Case-insensitive suffix-checker.
+ * @param {string} str The string to check.
+ * @param {string} suffix A string to look for at the end of {@code str}.
+ * @return {boolean} True if {@code str} ends with {@code suffix} (ignoring
+ *     case).
+ */
+goog.string.caseInsensitiveEndsWith = function(str, suffix) {
+  return goog.string.caseInsensitiveCompare(
+             suffix, str.substr(str.length - suffix.length, suffix.length)) ==
+      0;
+};
+
+
+/**
+ * Case-insensitive equality checker.
+ * @param {string} str1 First string to check.
+ * @param {string} str2 Second string to check.
+ * @return {boolean} True if {@code str1} and {@code str2} are the same string,
+ *     ignoring case.
+ */
+goog.string.caseInsensitiveEquals = function(str1, str2) {
+  return str1.toLowerCase() == str2.toLowerCase();
+};
+
+
+/**
+ * Does simple python-style string substitution.
+ * subs("foo%s hot%s", "bar", "dog") becomes "foobar hotdog".
+ * @param {string} str The string containing the pattern.
+ * @param {...*} var_args The items to substitute into the pattern.
+ * @return {string} A copy of {@code str} in which each occurrence of
+ *     {@code %s} has been replaced an argument from {@code var_args}.
+ */
+goog.string.subs = function(str, var_args) {
+  var splitParts = str.split('%s');
+  var returnString = '';
+
+  var subsArguments = Array.prototype.slice.call(arguments, 1);
+  while (subsArguments.length &&
+         // Replace up to the last split part. We are inserting in the
+         // positions between split parts.
+         splitParts.length > 1) {
+    returnString += splitParts.shift() + subsArguments.shift();
+  }
+
+  return returnString + splitParts.join('%s');  // Join unused '%s'
+};
+
+
+/**
+ * Converts multiple whitespace chars (spaces, non-breaking-spaces, new lines
+ * and tabs) to a single space, and strips leading and trailing whitespace.
+ * @param {string} str Input string.
+ * @return {string} A copy of {@code str} with collapsed whitespace.
+ */
+goog.string.collapseWhitespace = function(str) {
+  // Since IE doesn't include non-breaking-space (0xa0) in their \s character
+  // class (as required by section 7.2 of the ECMAScript spec), we explicitly
+  // include it in the regexp to enforce consistent cross-browser behavior.
+  return str.replace(/[\s\xa0]+/g, ' ').replace(/^\s+|\s+$/g, '');
+};
+
+
+/**
+ * Checks if a string is empty or contains only whitespaces.
+ * @param {string} str The string to check.
+ * @return {boolean} Whether {@code str} is empty or whitespace only.
+ */
+goog.string.isEmptyOrWhitespace = function(str) {
+  // testing length == 0 first is actually slower in all browsers (about the
+  // same in Opera).
+  // Since IE doesn't include non-breaking-space (0xa0) in their \s character
+  // class (as required by section 7.2 of the ECMAScript spec), we explicitly
+  // include it in the regexp to enforce consistent cross-browser behavior.
+  return /^[\s\xa0]*$/.test(str);
+};
+
+
+/**
+ * Checks if a string is empty.
+ * @param {string} str The string to check.
+ * @return {boolean} Whether {@code str} is empty.
+ */
+goog.string.isEmptyString = function(str) {
+  return str.length == 0;
+};
+
+
+/**
+ * Checks if a string is empty or contains only whitespaces.
+ *
+ * TODO(user): Deprecate this when clients have been switched over to
+ * goog.string.isEmptyOrWhitespace.
+ *
+ * @param {string} str The string to check.
+ * @return {boolean} Whether {@code str} is empty or whitespace only.
+ */
+goog.string.isEmpty = goog.string.isEmptyOrWhitespace;
+
+
+/**
+ * Checks if a string is null, undefined, empty or contains only whitespaces.
+ * @param {*} str The string to check.
+ * @return {boolean} Whether {@code str} is null, undefined, empty, or
+ *     whitespace only.
+ * @deprecated Use goog.string.isEmptyOrWhitespace(goog.string.makeSafe(str))
+ *     instead.
+ */
+goog.string.isEmptyOrWhitespaceSafe = function(str) {
+  return goog.string.isEmptyOrWhitespace(goog.string.makeSafe(str));
+};
+
+
+/**
+ * Checks if a string is null, undefined, empty or contains only whitespaces.
+ *
+ * TODO(user): Deprecate this when clients have been switched over to
+ * goog.string.isEmptyOrWhitespaceSafe.
+ *
+ * @param {*} str The string to check.
+ * @return {boolean} Whether {@code str} is null, undefined, empty, or
+ *     whitespace only.
+ */
+goog.string.isEmptySafe = goog.string.isEmptyOrWhitespaceSafe;
+
+
+/**
+ * Checks if a string is all breaking whitespace.
+ * @param {string} str The string to check.
+ * @return {boolean} Whether the string is all breaking whitespace.
+ */
+goog.string.isBreakingWhitespace = function(str) {
+  return !/[^\t\n\r ]/.test(str);
+};
+
+
+/**
+ * Checks if a string contains all letters.
+ * @param {string} str string to check.
+ * @return {boolean} True if {@code str} consists entirely of letters.
+ */
+goog.string.isAlpha = function(str) {
+  return !/[^a-zA-Z]/.test(str);
+};
+
+
+/**
+ * Checks if a string contains only numbers.
+ * @param {*} str string to check. If not a string, it will be
+ *     casted to one.
+ * @return {boolean} True if {@code str} is numeric.
+ */
+goog.string.isNumeric = function(str) {
+  return !/[^0-9]/.test(str);
+};
+
+
+/**
+ * Checks if a string contains only numbers or letters.
+ * @param {string} str string to check.
+ * @return {boolean} True if {@code str} is alphanumeric.
+ */
+goog.string.isAlphaNumeric = function(str) {
+  return !/[^a-zA-Z0-9]/.test(str);
+};
+
+
+/**
+ * Checks if a character is a space character.
+ * @param {string} ch Character to check.
+ * @return {boolean} True if {@code ch} is a space.
+ */
+goog.string.isSpace = function(ch) {
+  return ch == ' ';
+};
+
+
+/**
+ * Checks if a character is a valid unicode character.
+ * @param {string} ch Character to check.
+ * @return {boolean} True if {@code ch} is a valid unicode character.
+ */
+goog.string.isUnicodeChar = function(ch) {
+  return ch.length == 1 && ch >= ' ' && ch <= '~' ||
+      ch >= '\u0080' && ch <= '\uFFFD';
+};
+
+
+/**
+ * Takes a string and replaces newlines with a space. Multiple lines are
+ * replaced with a single space.
+ * @param {string} str The string from which to strip newlines.
+ * @return {string} A copy of {@code str} stripped of newlines.
+ */
+goog.string.stripNewlines = function(str) {
+  return str.replace(/(\r\n|\r|\n)+/g, ' ');
+};
+
+
+/**
+ * Replaces Windows and Mac new lines with unix style: \r or \r\n with \n.
+ * @param {string} str The string to in which to canonicalize newlines.
+ * @return {string} {@code str} A copy of {@code} with canonicalized newlines.
+ */
+goog.string.canonicalizeNewlines = function(str) {
+  return str.replace(/(\r\n|\r|\n)/g, '\n');
+};
+
+
+/**
+ * Normalizes whitespace in a string, replacing all whitespace chars with
+ * a space.
+ * @param {string} str The string in which to normalize whitespace.
+ * @return {string} A copy of {@code str} with all whitespace normalized.
+ */
+goog.string.normalizeWhitespace = function(str) {
+  return str.replace(/\xa0|\s/g, ' ');
+};
+
+
+/**
+ * Normalizes spaces in a string, replacing all consecutive spaces and tabs
+ * with a single space. Replaces non-breaking space with a space.
+ * @param {string} str The string in which to normalize spaces.
+ * @return {string} A copy of {@code str} with all consecutive spaces and tabs
+ *    replaced with a single space.
+ */
+goog.string.normalizeSpaces = function(str) {
+  return str.replace(/\xa0|[ \t]+/g, ' ');
+};
+
+
+/**
+ * Removes the breaking spaces from the left and right of the string and
+ * collapses the sequences of breaking spaces in the middle into single spaces.
+ * The original and the result strings render the same way in HTML.
+ * @param {string} str A string in which to collapse spaces.
+ * @return {string} Copy of the string with normalized breaking spaces.
+ */
+goog.string.collapseBreakingSpaces = function(str) {
+  return str.replace(/[\t\r\n ]+/g, ' ')
+      .replace(/^[\t\r\n ]+|[\t\r\n ]+$/g, '');
+};
+
+
+/**
+ * Trims white spaces to the left and right of a string.
+ * @param {string} str The string to trim.
+ * @return {string} A trimmed copy of {@code str}.
+ */
+goog.string.trim =
+    (goog.TRUSTED_SITE && String.prototype.trim) ? function(str) {
+      return str.trim();
+    } : function(str) {
+      // Since IE doesn't include non-breaking-space (0xa0) in their \s
+      // character class (as required by section 7.2 of the ECMAScript spec),
+      // we explicitly include it in the regexp to enforce consistent
+      // cross-browser behavior.
+      return str.replace(/^[\s\xa0]+|[\s\xa0]+$/g, '');
+    };
+
+
+/**
+ * Trims whitespaces at the left end of a string.
+ * @param {string} str The string to left trim.
+ * @return {string} A trimmed copy of {@code str}.
+ */
+goog.string.trimLeft = function(str) {
+  // Since IE doesn't include non-breaking-space (0xa0) in their \s character
+  // class (as required by section 7.2 of the ECMAScript spec), we explicitly
+  // include it in the regexp to enforce consistent cross-browser behavior.
+  return str.replace(/^[\s\xa0]+/, '');
+};
+
+
+/**
+ * Trims whitespaces at the right end of a string.
+ * @param {string} str The string to right trim.
+ * @return {string} A trimmed copy of {@code str}.
+ */
+goog.string.trimRight = function(str) {
+  // Since IE doesn't include non-breaking-space (0xa0) in their \s character
+  // class (as required by section 7.2 of the ECMAScript spec), we explicitly
+  // include it in the regexp to enforce consistent cross-browser behavior.
+  return str.replace(/[\s\xa0]+$/, '');
+};
+
+
+/**
+ * A string comparator that ignores case.
+ * -1 = str1 less than str2
+ *  0 = str1 equals str2
+ *  1 = str1 greater than str2
+ *
+ * @param {string} str1 The string to compare.
+ * @param {string} str2 The string to compare {@code str1} to.
+ * @return {number} The comparator result, as described above.
+ */
+goog.string.caseInsensitiveCompare = function(str1, str2) {
+  var test1 = String(str1).toLowerCase();
+  var test2 = String(str2).toLowerCase();
+
+  if (test1 < test2) {
+    return -1;
+  } else if (test1 == test2) {
+    return 0;
+  } else {
+    return 1;
+  }
+};
+
+
+/**
+ * Compares two strings interpreting their numeric substrings as numbers.
+ *
+ * @param {string} str1 First string.
+ * @param {string} str2 Second string.
+ * @param {!RegExp} tokenizerRegExp Splits a string into substrings of
+ *     non-negative integers, non-numeric characters and optionally fractional
+ *     numbers starting with a decimal point.
+ * @return {number} Negative if str1 < str2, 0 is str1 == str2, positive if
+ *     str1 > str2.
+ * @private
+ */
+goog.string.numberAwareCompare_ = function(str1, str2, tokenizerRegExp) {
+  if (str1 == str2) {
+    return 0;
+  }
+  if (!str1) {
+    return -1;
+  }
+  if (!str2) {
+    return 1;
+  }
+
+  // Using match to split the entire string ahead of time turns out to be faster
+  // for most inputs than using RegExp.exec or iterating over each character.
+  var tokens1 = str1.toLowerCase().match(tokenizerRegExp);
+  var tokens2 = str2.toLowerCase().match(tokenizerRegExp);
+
+  var count = Math.min(tokens1.length, tokens2.length);
+
+  for (var i = 0; i < count; i++) {
+    var a = tokens1[i];
+    var b = tokens2[i];
+
+    // Compare pairs of tokens, returning if one token sorts before the other.
+    if (a != b) {
+      // Only if both tokens are integers is a special comparison required.
+      // Decimal numbers are sorted as strings (e.g., '.09' < '.1').
+      var num1 = parseInt(a, 10);
+      if (!isNaN(num1)) {
+        var num2 = parseInt(b, 10);
+        if (!isNaN(num2) && num1 - num2) {
+          return num1 - num2;
+        }
+      }
+      return a < b ? -1 : 1;
+    }
+  }
+
+  // If one string is a substring of the other, the shorter string sorts first.
+  if (tokens1.length != tokens2.length) {
+    return tokens1.length - tokens2.length;
+  }
+
+  // The two strings must be equivalent except for case (perfect equality is
+  // tested at the head of the function.) Revert to default ASCII string
+  // comparison to stabilize the sort.
+  return str1 < str2 ? -1 : 1;
+};
+
+
+/**
+ * String comparison function that handles non-negative integer numbers in a
+ * way humans might expect. Using this function, the string 'File 2.jpg' sorts
+ * before 'File 10.jpg', and 'Version 1.9' before 'Version 1.10'. The comparison
+ * is mostly case-insensitive, though strings that are identical except for case
+ * are sorted with the upper-case strings before lower-case.
+ *
+ * This comparison function is up to 50x slower than either the default or the
+ * case-insensitive compare. It should not be used in time-critical code, but
+ * should be fast enough to sort several hundred short strings (like filenames)
+ * with a reasonable delay.
+ *
+ * @param {string} str1 The string to compare in a numerically sensitive way.
+ * @param {string} str2 The string to compare {@code str1} to.
+ * @return {number} less than 0 if str1 < str2, 0 if str1 == str2, greater than
+ *     0 if str1 > str2.
+ */
+goog.string.intAwareCompare = function(str1, str2) {
+  return goog.string.numberAwareCompare_(str1, str2, /\d+|\D+/g);
+};
+
+
+/**
+ * String comparison function that handles non-negative integer and fractional
+ * numbers in a way humans might expect. Using this function, the string
+ * 'File 2.jpg' sorts before 'File 10.jpg', and '3.14' before '3.2'. Equivalent
+ * to {@link goog.string.intAwareCompare} apart from the way how it interprets
+ * dots.
+ *
+ * @param {string} str1 The string to compare in a numerically sensitive way.
+ * @param {string} str2 The string to compare {@code str1} to.
+ * @return {number} less than 0 if str1 < str2, 0 if str1 == str2, greater than
+ *     0 if str1 > str2.
+ */
+goog.string.floatAwareCompare = function(str1, str2) {
+  return goog.string.numberAwareCompare_(str1, str2, /\d+|\.\d+|\D+/g);
+};
+
+
+/**
+ * Alias for {@link goog.string.floatAwareCompare}.
+ *
+ * @param {string} str1
+ * @param {string} str2
+ * @return {number}
+ */
+goog.string.numerateCompare = goog.string.floatAwareCompare;
+
+
+/**
+ * URL-encodes a string
+ * @param {*} str The string to url-encode.
+ * @return {string} An encoded copy of {@code str} that is safe for urls.
+ *     Note that '#', ':', and other characters used to delimit portions
+ *     of URLs *will* be encoded.
+ */
+goog.string.urlEncode = function(str) {
+  return encodeURIComponent(String(str));
+};
+
+
+/**
+ * URL-decodes the string. We need to specially handle '+'s because
+ * the javascript library doesn't convert them to spaces.
+ * @param {string} str The string to url decode.
+ * @return {string} The decoded {@code str}.
+ */
+goog.string.urlDecode = function(str) {
+  return decodeURIComponent(str.replace(/\+/g, ' '));
+};
+
+
+/**
+ * Converts \n to <br>s or <br />s.
+ * @param {string} str The string in which to convert newlines.
+ * @param {boolean=} opt_xml Whether to use XML compatible tags.
+ * @return {string} A copy of {@code str} with converted newlines.
+ */
+goog.string.newLineToBr = function(str, opt_xml) {
+  return str.replace(/(\r\n|\r|\n)/g, opt_xml ? '<br />' : '<br>');
+};
+
+
+/**
+ * Escapes double quote '"' and single quote '\'' characters in addition to
+ * '&', '<', and '>' so that a string can be included in an HTML tag attribute
+ * value within double or single quotes.
+ *
+ * It should be noted that > doesn't need to be escaped for the HTML or XML to
+ * be valid, but it has been decided to escape it for consistency with other
+ * implementations.
+ *
+ * With goog.string.DETECT_DOUBLE_ESCAPING, this function escapes also the
+ * lowercase letter "e".
+ *
+ * NOTE(user):
+ * HtmlEscape is often called during the generation of large blocks of HTML.
+ * Using statics for the regular expressions and strings is an optimization
+ * that can more than half the amount of time IE spends in this function for
+ * large apps, since strings and regexes both contribute to GC allocations.
+ *
+ * Testing for the presence of a character before escaping increases the number
+ * of function calls, but actually provides a speed increase for the average
+ * case -- since the average case often doesn't require the escaping of all 4
+ * characters and indexOf() is much cheaper than replace().
+ * The worst case does suffer slightly from the additional calls, therefore the
+ * opt_isLikelyToContainHtmlChars option has been included for situations
+ * where all 4 HTML entities are very likely to be present and need escaping.
+ *
+ * Some benchmarks (times tended to fluctuate +-0.05ms):
+ *                                     FireFox                     IE6
+ * (no chars / average (mix of cases) / all 4 chars)
+ * no checks                     0.13 / 0.22 / 0.22         0.23 / 0.53 / 0.80
+ * indexOf                       0.08 / 0.17 / 0.26         0.22 / 0.54 / 0.84
+ * indexOf + re test             0.07 / 0.17 / 0.28         0.19 / 0.50 / 0.85
+ *
+ * An additional advantage of checking if replace actually needs to be called
+ * is a reduction in the number of object allocations, so as the size of the
+ * application grows the difference between the various methods would increase.
+ *
+ * @param {string} str string to be escaped.
+ * @param {boolean=} opt_isLikelyToContainHtmlChars Don't perform a check to see
+ *     if the character needs replacing - use this option if you expect each of
+ *     the characters to appear often. Leave false if you expect few html
+ *     characters to occur in your strings, such as if you are escaping HTML.
+ * @return {string} An escaped copy of {@code str}.
+ */
+goog.string.htmlEscape = function(str, opt_isLikelyToContainHtmlChars) {
+
+  if (opt_isLikelyToContainHtmlChars) {
+    str = str.replace(goog.string.AMP_RE_, '&amp;')
+              .replace(goog.string.LT_RE_, '&lt;')
+              .replace(goog.string.GT_RE_, '&gt;')
+              .replace(goog.string.QUOT_RE_, '&quot;')
+              .replace(goog.string.SINGLE_QUOTE_RE_, '&#39;')
+              .replace(goog.string.NULL_RE_, '&#0;');
+    if (goog.string.DETECT_DOUBLE_ESCAPING) {
+      str = str.replace(goog.string.E_RE_, '&#101;');
+    }
+    return str;
+
+  } else {
+    // quick test helps in the case when there are no chars to replace, in
+    // worst case this makes barely a difference to the time taken
+    if (!goog.string.ALL_RE_.test(str)) return str;
+
+    // str.indexOf is faster than regex.test in this case
+    if (str.indexOf('&') != -1) {
+      str = str.replace(goog.string.AMP_RE_, '&amp;');
+    }
+    if (str.indexOf('<') != -1) {
+      str = str.replace(goog.string.LT_RE_, '&lt;');
+    }
+    if (str.indexOf('>') != -1) {
+      str = str.replace(goog.string.GT_RE_, '&gt;');
+    }
+    if (str.indexOf('"') != -1) {
+      str = str.replace(goog.string.QUOT_RE_, '&quot;');
+    }
+    if (str.indexOf('\'') != -1) {
+      str = str.replace(goog.string.SINGLE_QUOTE_RE_, '&#39;');
+    }
+    if (str.indexOf('\x00') != -1) {
+      str = str.replace(goog.string.NULL_RE_, '&#0;');
+    }
+    if (goog.string.DETECT_DOUBLE_ESCAPING && str.indexOf('e') != -1) {
+      str = str.replace(goog.string.E_RE_, '&#101;');
+    }
+    return str;
+  }
+};
+
+
+/**
+ * Regular expression that matches an ampersand, for use in escaping.
+ * @const {!RegExp}
+ * @private
+ */
+goog.string.AMP_RE_ = /&/g;
+
+
+/**
+ * Regular expression that matches a less than sign, for use in escaping.
+ * @const {!RegExp}
+ * @private
+ */
+goog.string.LT_RE_ = /</g;
+
+
+/**
+ * Regular expression that matches a greater than sign, for use in escaping.
+ * @const {!RegExp}
+ * @private
+ */
+goog.string.GT_RE_ = />/g;
+
+
+/**
+ * Regular expression that matches a double quote, for use in escaping.
+ * @const {!RegExp}
+ * @private
+ */
+goog.string.QUOT_RE_ = /"/g;
+
+
+/**
+ * Regular expression that matches a single quote, for use in escaping.
+ * @const {!RegExp}
+ * @private
+ */
+goog.string.SINGLE_QUOTE_RE_ = /'/g;
+
+
+/**
+ * Regular expression that matches null character, for use in escaping.
+ * @const {!RegExp}
+ * @private
+ */
+goog.string.NULL_RE_ = /\x00/g;
+
+
+/**
+ * Regular expression that matches a lowercase letter "e", for use in escaping.
+ * @const {!RegExp}
+ * @private
+ */
+goog.string.E_RE_ = /e/g;
+
+
+/**
+ * Regular expression that matches any character that needs to be escaped.
+ * @const {!RegExp}
+ * @private
+ */
+goog.string.ALL_RE_ =
+    (goog.string.DETECT_DOUBLE_ESCAPING ? /[\x00&<>"'e]/ : /[\x00&<>"']/);
+
+
+/**
+ * Unescapes an HTML string.
+ *
+ * @param {string} str The string to unescape.
+ * @return {string} An unescaped copy of {@code str}.
+ */
+goog.string.unescapeEntities = function(str) {
+  if (goog.string.contains(str, '&')) {
+    // We are careful not to use a DOM if we do not have one or we explicitly
+    // requested non-DOM html unescaping.
+    if (!goog.string.FORCE_NON_DOM_HTML_UNESCAPING &&
+        'document' in goog.global) {
+      return goog.string.unescapeEntitiesUsingDom_(str);
+    } else {
+      // Fall back on pure XML entities
+      return goog.string.unescapePureXmlEntities_(str);
+    }
+  }
+  return str;
+};
+
+
+/**
+ * Unescapes a HTML string using the provided document.
+ *
+ * @param {string} str The string to unescape.
+ * @param {!Document} document A document to use in escaping the string.
+ * @return {string} An unescaped copy of {@code str}.
+ */
+goog.string.unescapeEntitiesWithDocument = function(str, document) {
+  if (goog.string.contains(str, '&')) {
+    return goog.string.unescapeEntitiesUsingDom_(str, document);
+  }
+  return str;
+};
+
+
+/**
+ * Unescapes an HTML string using a DOM to resolve non-XML, non-numeric
+ * entities. This function is XSS-safe and whitespace-preserving.
+ * @private
+ * @param {string} str The string to unescape.
+ * @param {Document=} opt_document An optional document to use for creating
+ *     elements. If this is not specified then the default window.document
+ *     will be used.
+ * @return {string} The unescaped {@code str} string.
+ */
+goog.string.unescapeEntitiesUsingDom_ = function(str, opt_document) {
+  /** @type {!Object<string, string>} */
+  var seen = {'&amp;': '&', '&lt;': '<', '&gt;': '>', '&quot;': '"'};
+  var div;
+  if (opt_document) {
+    div = opt_document.createElement('div');
+  } else {
+    div = goog.global.document.createElement('div');
+  }
+  // Match as many valid entity characters as possible. If the actual entity
+  // happens to be shorter, it will still work as innerHTML will return the
+  // trailing characters unchanged. Since the entity characters do not include
+  // open angle bracket, there is no chance of XSS from the innerHTML use.
+  // Since no whitespace is passed to innerHTML, whitespace is preserved.
+  return str.replace(goog.string.HTML_ENTITY_PATTERN_, function(s, entity) {
+    // Check for cached entity.
+    var value = seen[s];
+    if (value) {
+      return value;
+    }
+    // Check for numeric entity.
+    if (entity.charAt(0) == '#') {
+      // Prefix with 0 so that hex entities (e.g. &#x10) parse as hex numbers.
+      var n = Number('0' + entity.substr(1));
+      if (!isNaN(n)) {
+        value = String.fromCharCode(n);
+      }
+    }
+    // Fall back to innerHTML otherwise.
+    if (!value) {
+      // Append a non-entity character to avoid a bug in Webkit that parses
+      // an invalid entity at the end of innerHTML text as the empty string.
+      div.innerHTML = s + ' ';
+      // Then remove the trailing character from the result.
+      value = div.firstChild.nodeValue.slice(0, -1);
+    }
+    // Cache and return.
+    return seen[s] = value;
+  });
+};
+
+
+/**
+ * Unescapes XML entities.
+ * @private
+ * @param {string} str The string to unescape.
+ * @return {string} An unescaped copy of {@code str}.
+ */
+goog.string.unescapePureXmlEntities_ = function(str) {
+  return str.replace(/&([^;]+);/g, function(s, entity) {
+    switch (entity) {
+      case 'amp':
+        return '&';
+      case 'lt':
+        return '<';
+      case 'gt':
+        return '>';
+      case 'quot':
+        return '"';
+      default:
+        if (entity.charAt(0) == '#') {
+          // Prefix with 0 so that hex entities (e.g. &#x10) parse as hex.
+          var n = Number('0' + entity.substr(1));
+          if (!isNaN(n)) {
+            return String.fromCharCode(n);
+          }
+        }
+        // For invalid entities we just return the entity
+        return s;
+    }
+  });
+};
+
+
+/**
+ * Regular expression that matches an HTML entity.
+ * See also HTML5: Tokenization / Tokenizing character references.
+ * @private
+ * @type {!RegExp}
+ */
+goog.string.HTML_ENTITY_PATTERN_ = /&([^;\s<&]+);?/g;
+
+
+/**
+ * Do escaping of whitespace to preserve spatial formatting. We use character
+ * entity #160 to make it safer for xml.
+ * @param {string} str The string in which to escape whitespace.
+ * @param {boolean=} opt_xml Whether to use XML compatible tags.
+ * @return {string} An escaped copy of {@code str}.
+ */
+goog.string.whitespaceEscape = function(str, opt_xml) {
+  // This doesn't use goog.string.preserveSpaces for backwards compatibility.
+  return goog.string.newLineToBr(str.replace(/  /g, ' &#160;'), opt_xml);
+};
+
+
+/**
+ * Preserve spaces that would be otherwise collapsed in HTML by replacing them
+ * with non-breaking space Unicode characters.
+ * @param {string} str The string in which to preserve whitespace.
+ * @return {string} A copy of {@code str} with preserved whitespace.
+ */
+goog.string.preserveSpaces = function(str) {
+  return str.replace(/(^|[\n ]) /g, '$1' + goog.string.Unicode.NBSP);
+};
+
+
+/**
+ * Strip quote characters around a string.  The second argument is a string of
+ * characters to treat as quotes.  This can be a single character or a string of
+ * multiple character and in that case each of those are treated as possible
+ * quote characters. For example:
+ *
+ * <pre>
+ * goog.string.stripQuotes('"abc"', '"`') --> 'abc'
+ * goog.string.stripQuotes('`abc`', '"`') --> 'abc'
+ * </pre>
+ *
+ * @param {string} str The string to strip.
+ * @param {string} quoteChars The quote characters to strip.
+ * @return {string} A copy of {@code str} without the quotes.
+ */
+goog.string.stripQuotes = function(str, quoteChars) {
+  var length = quoteChars.length;
+  for (var i = 0; i < length; i++) {
+    var quoteChar = length == 1 ? quoteChars : quoteChars.charAt(i);
+    if (str.charAt(0) == quoteChar && str.charAt(str.length - 1) == quoteChar) {
+      return str.substring(1, str.length - 1);
+    }
+  }
+  return str;
+};
+
+
+/**
+ * Truncates a string to a certain length and adds '...' if necessary.  The
+ * length also accounts for the ellipsis, so a maximum length of 10 and a string
+ * 'Hello World!' produces 'Hello W...'.
+ * @param {string} str The string to truncate.
+ * @param {number} chars Max number of characters.
+ * @param {boolean=} opt_protectEscapedCharacters Whether to protect escaped
+ *     characters from being cut off in the middle.
+ * @return {string} The truncated {@code str} string.
+ */
+goog.string.truncate = function(str, chars, opt_protectEscapedCharacters) {
+  if (opt_protectEscapedCharacters) {
+    str = goog.string.unescapeEntities(str);
+  }
+
+  if (str.length > chars) {
+    str = str.substring(0, chars - 3) + '...';
+  }
+
+  if (opt_protectEscapedCharacters) {
+    str = goog.string.htmlEscape(str);
+  }
+
+  return str;
+};
+
+
+/**
+ * Truncate a string in the middle, adding "..." if necessary,
+ * and favoring the beginning of the string.
+ * @param {string} str The string to truncate the middle of.
+ * @param {number} chars Max number of characters.
+ * @param {boolean=} opt_protectEscapedCharacters Whether to protect escaped
+ *     characters from being cutoff in the middle.
+ * @param {number=} opt_trailingChars Optional number of trailing characters to
+ *     leave at the end of the string, instead of truncating as close to the
+ *     middle as possible.
+ * @return {string} A truncated copy of {@code str}.
+ */
+goog.string.truncateMiddle = function(
+    str, chars, opt_protectEscapedCharacters, opt_trailingChars) {
+  if (opt_protectEscapedCharacters) {
+    str = goog.string.unescapeEntities(str);
+  }
+
+  if (opt_trailingChars && str.length > chars) {
+    if (opt_trailingChars > chars) {
+      opt_trailingChars = chars;
+    }
+    var endPoint = str.length - opt_trailingChars;
+    var startPoint = chars - opt_trailingChars;
+    str = str.substring(0, startPoint) + '...' + str.substring(endPoint);
+  } else if (str.length > chars) {
+    // Favor the beginning of the string:
+    var half = Math.floor(chars / 2);
+    var endPos = str.length - half;
+    half += chars % 2;
+    str = str.substring(0, half) + '...' + str.substring(endPos);
+  }
+
+  if (opt_protectEscapedCharacters) {
+    str = goog.string.htmlEscape(str);
+  }
+
+  return str;
+};
+
+
+/**
+ * Special chars that need to be escaped for goog.string.quote.
+ * @private {!Object<string, string>}
+ */
+goog.string.specialEscapeChars_ = {
+  '\0': '\\0',
+  '\b': '\\b',
+  '\f': '\\f',
+  '\n': '\\n',
+  '\r': '\\r',
+  '\t': '\\t',
+  '\x0B': '\\x0B',  // '\v' is not supported in JScript
+  '"': '\\"',
+  '\\': '\\\\',
+  // To support the use case of embedding quoted strings inside of script
+  // tags, we have to make sure HTML comments and opening/closing script tags do
+  // not appear in the resulting string. The specific strings that must be
+  // escaped are documented at:
+  // http://www.w3.org/TR/html51/semantics.html#restrictions-for-contents-of-script-elements
+  '<': '\x3c'
+};
+
+
+/**
+ * Character mappings used internally for goog.string.escapeChar.
+ * @private {!Object<string, string>}
+ */
+goog.string.jsEscapeCache_ = {
+  '\'': '\\\''
+};
+
+
+/**
+ * Encloses a string in double quotes and escapes characters so that the
+ * string is a valid JS string. The resulting string is safe to embed in
+ * `<script>` tags as "<" is escaped.
+ * @param {string} s The string to quote.
+ * @return {string} A copy of {@code s} surrounded by double quotes.
+ */
+goog.string.quote = function(s) {
+  s = String(s);
+  var sb = ['"'];
+  for (var i = 0; i < s.length; i++) {
+    var ch = s.charAt(i);
+    var cc = ch.charCodeAt(0);
+    sb[i + 1] = goog.string.specialEscapeChars_[ch] ||
+        ((cc > 31 && cc < 127) ? ch : goog.string.escapeChar(ch));
+  }
+  sb.push('"');
+  return sb.join('');
+};
+
+
+/**
+ * Takes a string and returns the escaped string for that character.
+ * @param {string} str The string to escape.
+ * @return {string} An escaped string representing {@code str}.
+ */
+goog.string.escapeString = function(str) {
+  var sb = [];
+  for (var i = 0; i < str.length; i++) {
+    sb[i] = goog.string.escapeChar(str.charAt(i));
+  }
+  return sb.join('');
+};
+
+
+/**
+ * Takes a character and returns the escaped string for that character. For
+ * example escapeChar(String.fromCharCode(15)) -> "\\x0E".
+ * @param {string} c The character to escape.
+ * @return {string} An escaped string representing {@code c}.
+ */
+goog.string.escapeChar = function(c) {
+  if (c in goog.string.jsEscapeCache_) {
+    return goog.string.jsEscapeCache_[c];
+  }
+
+  if (c in goog.string.specialEscapeChars_) {
+    return goog.string.jsEscapeCache_[c] = goog.string.specialEscapeChars_[c];
+  }
+
+  var rv = c;
+  var cc = c.charCodeAt(0);
+  if (cc > 31 && cc < 127) {
+    rv = c;
+  } else {
+    // tab is 9 but handled above
+    if (cc < 256) {
+      rv = '\\x';
+      if (cc < 16 || cc > 256) {
+        rv += '0';
+      }
+    } else {
+      rv = '\\u';
+      if (cc < 4096) {  // \u1000
+        rv += '0';
+      }
+    }
+    rv += cc.toString(16).toUpperCase();
+  }
+
+  return goog.string.jsEscapeCache_[c] = rv;
+};
+
+
+/**
+ * Determines whether a string contains a substring.
+ * @param {string} str The string to search.
+ * @param {string} subString The substring to search for.
+ * @return {boolean} Whether {@code str} contains {@code subString}.
+ */
+goog.string.contains = function(str, subString) {
+  return str.indexOf(subString) != -1;
+};
+
+
+/**
+ * Determines whether a string contains a substring, ignoring case.
+ * @param {string} str The string to search.
+ * @param {string} subString The substring to search for.
+ * @return {boolean} Whether {@code str} contains {@code subString}.
+ */
+goog.string.caseInsensitiveContains = function(str, subString) {
+  return goog.string.contains(str.toLowerCase(), subString.toLowerCase());
+};
+
+
+/**
+ * Returns the non-overlapping occurrences of ss in s.
+ * If either s or ss evalutes to false, then returns zero.
+ * @param {string} s The string to look in.
+ * @param {string} ss The string to look for.
+ * @return {number} Number of occurrences of ss in s.
+ */
+goog.string.countOf = function(s, ss) {
+  return s && ss ? s.split(ss).length - 1 : 0;
+};
+
+
+/**
+ * Removes a substring of a specified length at a specific
+ * index in a string.
+ * @param {string} s The base string from which to remove.
+ * @param {number} index The index at which to remove the substring.
+ * @param {number} stringLength The length of the substring to remove.
+ * @return {string} A copy of {@code s} with the substring removed or the full
+ *     string if nothing is removed or the input is invalid.
+ */
+goog.string.removeAt = function(s, index, stringLength) {
+  var resultStr = s;
+  // If the index is greater or equal to 0 then remove substring
+  if (index >= 0 && index < s.length && stringLength > 0) {
+    resultStr = s.substr(0, index) +
+        s.substr(index + stringLength, s.length - index - stringLength);
+  }
+  return resultStr;
+};
+
+
+/**
+ *  Removes the first occurrence of a substring from a string.
+ *  @param {string} s The base string from which to remove.
+ *  @param {string} ss The string to remove.
+ *  @return {string} A copy of {@code s} with {@code ss} removed or the full
+ *      string if nothing is removed.
+ */
+goog.string.remove = function(s, ss) {
+  var re = new RegExp(goog.string.regExpEscape(ss), '');
+  return s.replace(re, '');
+};
+
+
+/**
+ *  Removes all occurrences of a substring from a string.
+ *  @param {string} s The base string from which to remove.
+ *  @param {string} ss The string to remove.
+ *  @return {string} A copy of {@code s} with {@code ss} removed or the full
+ *      string if nothing is removed.
+ */
+goog.string.removeAll = function(s, ss) {
+  var re = new RegExp(goog.string.regExpEscape(ss), 'g');
+  return s.replace(re, '');
+};
+
+
+/**
+ * Escapes characters in the string that are not safe to use in a RegExp.
+ * @param {*} s The string to escape. If not a string, it will be casted
+ *     to one.
+ * @return {string} A RegExp safe, escaped copy of {@code s}.
+ */
+goog.string.regExpEscape = function(s) {
+  return String(s)
+      .replace(/([-()\[\]{}+?*.$\^|,:#<!\\])/g, '\\$1')
+      .replace(/\x08/g, '\\x08');
+};
+
+
+/**
+ * Repeats a string n times.
+ * @param {string} string The string to repeat.
+ * @param {number} length The number of times to repeat.
+ * @return {string} A string containing {@code length} repetitions of
+ *     {@code string}.
+ */
+goog.string.repeat = (String.prototype.repeat) ? function(string, length) {
+  // The native method is over 100 times faster than the alternative.
+  return string.repeat(length);
+} : function(string, length) {
+  return new Array(length + 1).join(string);
+};
+
+
+/**
+ * Pads number to given length and optionally rounds it to a given precision.
+ * For example:
+ * <pre>padNumber(1.25, 2, 3) -> '01.250'
+ * padNumber(1.25, 2) -> '01.25'
+ * padNumber(1.25, 2, 1) -> '01.3'
+ * padNumber(1.25, 0) -> '1.25'</pre>
+ *
+ * @param {number} num The number to pad.
+ * @param {number} length The desired length.
+ * @param {number=} opt_precision The desired precision.
+ * @return {string} {@code num} as a string with the given options.
+ */
+goog.string.padNumber = function(num, length, opt_precision) {
+  var s = goog.isDef(opt_precision) ? num.toFixed(opt_precision) : String(num);
+  var index = s.indexOf('.');
+  if (index == -1) {
+    index = s.length;
+  }
+  return goog.string.repeat('0', Math.max(0, length - index)) + s;
+};
+
+
+/**
+ * Returns a string representation of the given object, with
+ * null and undefined being returned as the empty string.
+ *
+ * @param {*} obj The object to convert.
+ * @return {string} A string representation of the {@code obj}.
+ */
+goog.string.makeSafe = function(obj) {
+  return obj == null ? '' : String(obj);
+};
+
+
+/**
+ * Concatenates string expressions. This is useful
+ * since some browsers are very inefficient when it comes to using plus to
+ * concat strings. Be careful when using null and undefined here since
+ * these will not be included in the result. If you need to represent these
+ * be sure to cast the argument to a String first.
+ * For example:
+ * <pre>buildString('a', 'b', 'c', 'd') -> 'abcd'
+ * buildString(null, undefined) -> ''
+ * </pre>
+ * @param {...*} var_args A list of strings to concatenate. If not a string,
+ *     it will be casted to one.
+ * @return {string} The concatenation of {@code var_args}.
+ */
+goog.string.buildString = function(var_args) {
+  return Array.prototype.join.call(arguments, '');
+};
+
+
+/**
+ * Returns a string with at least 64-bits of randomness.
+ *
+ * Doesn't trust Javascript's random function entirely. Uses a combination of
+ * random and current timestamp, and then encodes the string in base-36 to
+ * make it shorter.
+ *
+ * @return {string} A random string, e.g. sn1s7vb4gcic.
+ */
+goog.string.getRandomString = function() {
+  var x = 2147483648;
+  return Math.floor(Math.random() * x).toString(36) +
+      Math.abs(Math.floor(Math.random() * x) ^ goog.now()).toString(36);
+};
+
+
+/**
+ * Compares two version numbers.
+ *
+ * @param {string|number} version1 Version of first item.
+ * @param {string|number} version2 Version of second item.
+ *
+ * @return {number}  1 if {@code version1} is higher.
+ *                   0 if arguments are equal.
+ *                  -1 if {@code version2} is higher.
+ */
+goog.string.compareVersions = function(version1, version2) {
+  var order = 0;
+  // Trim leading and trailing whitespace and split the versions into
+  // subversions.
+  var v1Subs = goog.string.trim(String(version1)).split('.');
+  var v2Subs = goog.string.trim(String(version2)).split('.');
+  var subCount = Math.max(v1Subs.length, v2Subs.length);
+
+  // Iterate over the subversions, as long as they appear to be equivalent.
+  for (var subIdx = 0; order == 0 && subIdx < subCount; subIdx++) {
+    var v1Sub = v1Subs[subIdx] || '';
+    var v2Sub = v2Subs[subIdx] || '';
+
+    // Split the subversions into pairs of numbers and qualifiers (like 'b').
+    // Two different RegExp objects are needed because they are both using
+    // the 'g' flag.
+    var v1CompParser = new RegExp('(\\d*)(\\D*)', 'g');
+    var v2CompParser = new RegExp('(\\d*)(\\D*)', 'g');
+    do {
+      var v1Comp = v1CompParser.exec(v1Sub) || ['', '', ''];
+      var v2Comp = v2CompParser.exec(v2Sub) || ['', '', ''];
+      // Break if there are no more matches.
+      if (v1Comp[0].length == 0 && v2Comp[0].length == 0) {
+        break;
+      }
+
+      // Parse the numeric part of the subversion. A missing number is
+      // equivalent to 0.
+      var v1CompNum = v1Comp[1].length == 0 ? 0 : parseInt(v1Comp[1], 10);
+      var v2CompNum = v2Comp[1].length == 0 ? 0 : parseInt(v2Comp[1], 10);
+
+      // Compare the subversion components. The number has the highest
+      // precedence. Next, if the numbers are equal, a subversion without any
+      // qualifier is always higher than a subversion with any qualifier. Next,
+      // the qualifiers are compared as strings.
+      order = goog.string.compareElements_(v1CompNum, v2CompNum) ||
+          goog.string.compareElements_(
+              v1Comp[2].length == 0, v2Comp[2].length == 0) ||
+          goog.string.compareElements_(v1Comp[2], v2Comp[2]);
+      // Stop as soon as an inequality is discovered.
+    } while (order == 0);
+  }
+
+  return order;
+};
+
+
+/**
+ * Compares elements of a version number.
+ *
+ * @param {string|number|boolean} left An element from a version number.
+ * @param {string|number|boolean} right An element from a version number.
+ *
+ * @return {number}  1 if {@code left} is higher.
+ *                   0 if arguments are equal.
+ *                  -1 if {@code right} is higher.
+ * @private
+ */
+goog.string.compareElements_ = function(left, right) {
+  if (left < right) {
+    return -1;
+  } else if (left > right) {
+    return 1;
+  }
+  return 0;
+};
+
+
+/**
+ * String hash function similar to java.lang.String.hashCode().
+ * The hash code for a string is computed as
+ * s[0] * 31 ^ (n - 1) + s[1] * 31 ^ (n - 2) + ... + s[n - 1],
+ * where s[i] is the ith character of the string and n is the length of
+ * the string. We mod the result to make it between 0 (inclusive) and 2^32
+ * (exclusive).
+ * @param {string} str A string.
+ * @return {number} Hash value for {@code str}, between 0 (inclusive) and 2^32
+ *  (exclusive). The empty string returns 0.
+ */
+goog.string.hashCode = function(str) {
+  var result = 0;
+  for (var i = 0; i < str.length; ++i) {
+    // Normalize to 4 byte range, 0 ... 2^32.
+    result = (31 * result + str.charCodeAt(i)) >>> 0;
+  }
+  return result;
+};
+
+
+/**
+ * The most recent unique ID. |0 is equivalent to Math.floor in this case.
+ * @type {number}
+ * @private
+ */
+goog.string.uniqueStringCounter_ = Math.random() * 0x80000000 | 0;
+
+
+/**
+ * Generates and returns a string which is unique in the current document.
+ * This is useful, for example, to create unique IDs for DOM elements.
+ * @return {string} A unique id.
+ */
+goog.string.createUniqueString = function() {
+  return 'goog_' + goog.string.uniqueStringCounter_++;
+};
+
+
+/**
+ * Converts the supplied string to a number, which may be Infinity or NaN.
+ * This function strips whitespace: (toNumber(' 123') === 123)
+ * This function accepts scientific notation: (toNumber('1e1') === 10)
+ *
+ * This is better than Javascript's built-in conversions because, sadly:
+ *     (Number(' ') === 0) and (parseFloat('123a') === 123)
+ *
+ * @param {string} str The string to convert.
+ * @return {number} The number the supplied string represents, or NaN.
+ */
+goog.string.toNumber = function(str) {
+  var num = Number(str);
+  if (num == 0 && goog.string.isEmptyOrWhitespace(str)) {
+    return NaN;
+  }
+  return num;
+};
+
+
+/**
+ * Returns whether the given string is lower camel case (e.g. "isFooBar").
+ *
+ * Note that this assumes the string is entirely letters.
+ * @see http://en.wikipedia.org/wiki/CamelCase#Variations_and_synonyms
+ *
+ * @param {string} str String to test.
+ * @return {boolean} Whether the string is lower camel case.
+ */
+goog.string.isLowerCamelCase = function(str) {
+  return /^[a-z]+([A-Z][a-z]*)*$/.test(str);
+};
+
+
+/**
+ * Returns whether the given string is upper camel case (e.g. "FooBarBaz").
+ *
+ * Note that this assumes the string is entirely letters.
+ * @see http://en.wikipedia.org/wiki/CamelCase#Variations_and_synonyms
+ *
+ * @param {string} str String to test.
+ * @return {boolean} Whether the string is upper camel case.
+ */
+goog.string.isUpperCamelCase = function(str) {
+  return /^([A-Z][a-z]*)+$/.test(str);
+};
+
+
+/**
+ * Converts a string from selector-case to camelCase (e.g. from
+ * "multi-part-string" to "multiPartString"), useful for converting
+ * CSS selectors and HTML dataset keys to their equivalent JS properties.
+ * @param {string} str The string in selector-case form.
+ * @return {string} The string in camelCase form.
+ */
+goog.string.toCamelCase = function(str) {
+  return String(str).replace(
+      /\-([a-z])/g, function(all, match) { return match.toUpperCase(); });
+};
+
+
+/**
+ * Converts a string from camelCase to selector-case (e.g. from
+ * "multiPartString" to "multi-part-string"), useful for converting JS
+ * style and dataset properties to equivalent CSS selectors and HTML keys.
+ * @param {string} str The string in camelCase form.
+ * @return {string} The string in selector-case form.
+ */
+goog.string.toSelectorCase = function(str) {
+  return String(str).replace(/([A-Z])/g, '-$1').toLowerCase();
+};
+
+
+/**
+ * Converts a string into TitleCase. First character of the string is always
+ * capitalized in addition to the first letter of every subsequent word.
+ * Words are delimited by one or more whitespaces by default. Custom delimiters
+ * can optionally be specified to replace the default, which doesn't preserve
+ * whitespace delimiters and instead must be explicitly included if needed.
+ *
+ * Default delimiter => " ":
+ *    goog.string.toTitleCase('oneTwoThree')    => 'OneTwoThree'
+ *    goog.string.toTitleCase('one two three')  => 'One Two Three'
+ *    goog.string.toTitleCase('  one   two   ') => '  One   Two   '
+ *    goog.string.toTitleCase('one_two_three')  => 'One_two_three'
+ *    goog.string.toTitleCase('one-two-three')  => 'One-two-three'
+ *
+ * Custom delimiter => "_-.":
+ *    goog.string.toTitleCase('oneTwoThree', '_-.')       => 'OneTwoThree'
+ *    goog.string.toTitleCase('one two three', '_-.')     => 'One two three'
+ *    goog.string.toTitleCase('  one   two   ', '_-.')    => '  one   two   '
+ *    goog.string.toTitleCase('one_two_three', '_-.')     => 'One_Two_Three'
+ *    goog.string.toTitleCase('one-two-three', '_-.')     => 'One-Two-Three'
+ *    goog.string.toTitleCase('one...two...three', '_-.') => 'One...Two...Three'
+ *    goog.string.toTitleCase('one. two. three', '_-.')   => 'One. two. three'
+ *    goog.string.toTitleCase('one-two.three', '_-.')     => 'One-Two.Three'
+ *
+ * @param {string} str String value in camelCase form.
+ * @param {string=} opt_delimiters Custom delimiter character set used to
+ *      distinguish words in the string value. Each character represents a
+ *      single delimiter. When provided, default whitespace delimiter is
+ *      overridden and must be explicitly included if needed.
+ * @return {string} String value in TitleCase form.
+ */
+goog.string.toTitleCase = function(str, opt_delimiters) {
+  var delimiters = goog.isString(opt_delimiters) ?
+      goog.string.regExpEscape(opt_delimiters) :
+      '\\s';
+
+  // For IE8, we need to prevent using an empty character set. Otherwise,
+  // incorrect matching will occur.
+  delimiters = delimiters ? '|[' + delimiters + ']+' : '';
+
+  var regexp = new RegExp('(^' + delimiters + ')([a-z])', 'g');
+  return str.replace(
+      regexp, function(all, p1, p2) { return p1 + p2.toUpperCase(); });
+};
+
+
+/**
+ * Capitalizes a string, i.e. converts the first letter to uppercase
+ * and all other letters to lowercase, e.g.:
+ *
+ * goog.string.capitalize('one')     => 'One'
+ * goog.string.capitalize('ONE')     => 'One'
+ * goog.string.capitalize('one two') => 'One two'
+ *
+ * Note that this function does not trim initial whitespace.
+ *
+ * @param {string} str String value to capitalize.
+ * @return {string} String value with first letter in uppercase.
+ */
+goog.string.capitalize = function(str) {
+  return String(str.charAt(0)).toUpperCase() +
+      String(str.substr(1)).toLowerCase();
+};
+
+
+/**
+ * Parse a string in decimal or hexidecimal ('0xFFFF') form.
+ *
+ * To parse a particular radix, please use parseInt(string, radix) directly. See
+ * https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/parseInt
+ *
+ * This is a wrapper for the built-in parseInt function that will only parse
+ * numbers as base 10 or base 16.  Some JS implementations assume strings
+ * starting with "0" are intended to be octal. ES3 allowed but discouraged
+ * this behavior. ES5 forbids it.  This function emulates the ES5 behavior.
+ *
+ * For more information, see Mozilla JS Reference: http://goo.gl/8RiFj
+ *
+ * @param {string|number|null|undefined} value The value to be parsed.
+ * @return {number} The number, parsed. If the string failed to parse, this
+ *     will be NaN.
+ */
+goog.string.parseInt = function(value) {
+  // Force finite numbers to strings.
+  if (isFinite(value)) {
+    value = String(value);
+  }
+
+  if (goog.isString(value)) {
+    // If the string starts with '0x' or '-0x', parse as hex.
+    return /^\s*-?0x/i.test(value) ? parseInt(value, 16) : parseInt(value, 10);
+  }
+
+  return NaN;
+};
+
+
+/**
+ * Splits a string on a separator a limited number of times.
+ *
+ * This implementation is more similar to Python or Java, where the limit
+ * parameter specifies the maximum number of splits rather than truncating
+ * the number of results.
+ *
+ * See http://docs.python.org/2/library/stdtypes.html#str.split
+ * See JavaDoc: http://goo.gl/F2AsY
+ * See Mozilla reference: http://goo.gl/dZdZs
+ *
+ * @param {string} str String to split.
+ * @param {string} separator The separator.
+ * @param {number} limit The limit to the number of splits. The resulting array
+ *     will have a maximum length of limit+1.  Negative numbers are the same
+ *     as zero.
+ * @return {!Array<string>} The string, split.
+ */
+goog.string.splitLimit = function(str, separator, limit) {
+  var parts = str.split(separator);
+  var returnVal = [];
+
+  // Only continue doing this while we haven't hit the limit and we have
+  // parts left.
+  while (limit > 0 && parts.length) {
+    returnVal.push(parts.shift());
+    limit--;
+  }
+
+  // If there are remaining parts, append them to the end.
+  if (parts.length) {
+    returnVal.push(parts.join(separator));
+  }
+
+  return returnVal;
+};
+
+
+/**
+ * Finds the characters to the right of the last instance of any separator
+ *
+ * This function is similar to goog.string.path.baseName, except it can take a
+ * list of characters to split the string on. It will return the rightmost
+ * grouping of characters to the right of any separator as a left-to-right
+ * oriented string.
+ *
+ * @see goog.string.path.baseName
+ * @param {string} str The string
+ * @param {string|!Array<string>} separators A list of separator characters
+ * @return {string} The last part of the string with respect to the separators
+ */
+goog.string.lastComponent = function(str, separators) {
+  if (!separators) {
+    return str;
+  } else if (typeof separators == 'string') {
+    separators = [separators];
+  }
+
+  var lastSeparatorIndex = -1;
+  for (var i = 0; i < separators.length; i++) {
+    if (separators[i] == '') {
+      continue;
+    }
+    var currentSeparatorIndex = str.lastIndexOf(separators[i]);
+    if (currentSeparatorIndex > lastSeparatorIndex) {
+      lastSeparatorIndex = currentSeparatorIndex;
+    }
+  }
+  if (lastSeparatorIndex == -1) {
+    return str;
+  }
+  return str.slice(lastSeparatorIndex + 1);
+};
+
+
+/**
+ * Computes the Levenshtein edit distance between two strings.
+ * @param {string} a
+ * @param {string} b
+ * @return {number} The edit distance between the two strings.
+ */
+goog.string.editDistance = function(a, b) {
+  var v0 = [];
+  var v1 = [];
+
+  if (a == b) {
+    return 0;
+  }
+
+  if (!a.length || !b.length) {
+    return Math.max(a.length, b.length);
+  }
+
+  for (var i = 0; i < b.length + 1; i++) {
+    v0[i] = i;
+  }
+
+  for (var i = 0; i < a.length; i++) {
+    v1[0] = i + 1;
+
+    for (var j = 0; j < b.length; j++) {
+      var cost = Number(a[i] != b[j]);
+      // Cost for the substring is the minimum of adding one character, removing
+      // one character, or a swap.
+      v1[j + 1] = Math.min(v1[j] + 1, v0[j + 1] + 1, v0[j] + cost);
+    }
+
+    for (var j = 0; j < v0.length; j++) {
+      v0[j] = v1[j];
+    }
+  }
+
+  return v1[b.length];
+};
+
+// Copyright 2008 The Closure Library Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS-IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+/**
+ * @fileoverview Utilities to check the preconditions, postconditions and
+ * invariants runtime.
+ *
+ * Methods in this package should be given special treatment by the compiler
+ * for type-inference. For example, <code>goog.asserts.assert(foo)</code>
+ * will restrict <code>foo</code> to a truthy value.
+ *
+ * The compiler has an option to disable asserts. So code like:
+ * <code>
+ * var x = goog.asserts.assert(foo()); goog.asserts.assert(bar());
+ * </code>
+ * will be transformed into:
+ * <code>
+ * var x = foo();
+ * </code>
+ * The compiler will leave in foo() (because its return value is used),
+ * but it will remove bar() because it assumes it does not have side-effects.
+ *
+ * @author agrieve@google.com (Andrew Grieve)
+ */
+
+goog.provide('goog.asserts');
+goog.provide('goog.asserts.AssertionError');
+
+goog.require('goog.debug.Error');
+goog.require('goog.dom.NodeType');
+goog.require('goog.string');
+
+
+/**
+ * @define {boolean} Whether to strip out asserts or to leave them in.
+ */
+goog.define('goog.asserts.ENABLE_ASSERTS', goog.DEBUG);
+
+
+
+/**
+ * Error object for failed assertions.
+ * @param {string} messagePattern The pattern that was used to form message.
+ * @param {!Array<*>} messageArgs The items to substitute into the pattern.
+ * @constructor
+ * @extends {goog.debug.Error}
+ * @final
+ */
+goog.asserts.AssertionError = function(messagePattern, messageArgs) {
+  messageArgs.unshift(messagePattern);
+  goog.debug.Error.call(this, goog.string.subs.apply(null, messageArgs));
+  // Remove the messagePattern afterwards to avoid permanently modifying the
+  // passed in array.
+  messageArgs.shift();
+
+  /**
+   * The message pattern used to format the error message. Error handlers can
+   * use this to uniquely identify the assertion.
+   * @type {string}
+   */
+  this.messagePattern = messagePattern;
+};
+goog.inherits(goog.asserts.AssertionError, goog.debug.Error);
+
+
+/** @override */
+goog.asserts.AssertionError.prototype.name = 'AssertionError';
+
+
+/**
+ * The default error handler.
+ * @param {!goog.asserts.AssertionError} e The exception to be handled.
+ */
+goog.asserts.DEFAULT_ERROR_HANDLER = function(e) {
+  throw e;
+};
+
+
+/**
+ * The handler responsible for throwing or logging assertion errors.
+ * @private {function(!goog.asserts.AssertionError)}
+ */
+goog.asserts.errorHandler_ = goog.asserts.DEFAULT_ERROR_HANDLER;
+
+
+/**
+ * Throws an exception with the given message and "Assertion failed" prefixed
+ * onto it.
+ * @param {string} defaultMessage The message to use if givenMessage is empty.
+ * @param {Array<*>} defaultArgs The substitution arguments for defaultMessage.
+ * @param {string|undefined} givenMessage Message supplied by the caller.
+ * @param {Array<*>} givenArgs The substitution arguments for givenMessage.
+ * @throws {goog.asserts.AssertionError} When the value is not a number.
+ * @private
+ */
+goog.asserts.doAssertFailure_ = function(
+    defaultMessage, defaultArgs, givenMessage, givenArgs) {
+  var message = 'Assertion failed';
+  if (givenMessage) {
+    message += ': ' + givenMessage;
+    var args = givenArgs;
+  } else if (defaultMessage) {
+    message += ': ' + defaultMessage;
+    args = defaultArgs;
+  }
+  // The '' + works around an Opera 10 bug in the unit tests. Without it,
+  // a stack trace is added to var message above. With this, a stack trace is
+  // not added until this line (it causes the extra garbage to be added after
+  // the assertion message instead of in the middle of it).
+  var e = new goog.asserts.AssertionError('' + message, args || []);
+  goog.asserts.errorHandler_(e);
+};
+
+
+/**
+ * Sets a custom error handler that can be used to customize the behavior of
+ * assertion failures, for example by turning all assertion failures into log
+ * messages.
+ * @param {function(!goog.asserts.AssertionError)} errorHandler
+ */
+goog.asserts.setErrorHandler = function(errorHandler) {
+  if (goog.asserts.ENABLE_ASSERTS) {
+    goog.asserts.errorHandler_ = errorHandler;
+  }
+};
+
+
+/**
+ * Checks if the condition evaluates to true if goog.asserts.ENABLE_ASSERTS is
+ * true.
+ * @template T
+ * @param {T} condition The condition to check.
+ * @param {string=} opt_message Error message in case of failure.
+ * @param {...*} var_args The items to substitute into the failure message.
+ * @return {T} The value of the condition.
+ * @throws {goog.asserts.AssertionError} When the condition evaluates to false.
+ */
+goog.asserts.assert = function(condition, opt_message, var_args) {
+  if (goog.asserts.ENABLE_ASSERTS && !condition) {
+    goog.asserts.doAssertFailure_(
+        '', null, opt_message, Array.prototype.slice.call(arguments, 2));
+  }
+  return condition;
+};
+
+
+/**
+ * Fails if goog.asserts.ENABLE_ASSERTS is true. This function is useful in case
+ * when we want to add a check in the unreachable area like switch-case
+ * statement:
+ *
+ * <pre>
+ *  switch(type) {
+ *    case FOO: doSomething(); break;
+ *    case BAR: doSomethingElse(); break;
+ *    default: goog.asserts.fail('Unrecognized type: ' + type);
+ *      // We have only 2 types - "default:" section is unreachable code.
+ *  }
+ * </pre>
+ *
+ * @param {string=} opt_message Error message in case of failure.
+ * @param {...*} var_args The items to substitute into the failure message.
+ * @throws {goog.asserts.AssertionError} Failure.
+ */
+goog.asserts.fail = function(opt_message, var_args) {
+  if (goog.asserts.ENABLE_ASSERTS) {
+    goog.asserts.errorHandler_(
+        new goog.asserts.AssertionError(
+            'Failure' + (opt_message ? ': ' + opt_message : ''),
+            Array.prototype.slice.call(arguments, 1)));
+  }
+};
+
+
+/**
+ * Checks if the value is a number if goog.asserts.ENABLE_ASSERTS is true.
+ * @param {*} value The value to check.
+ * @param {string=} opt_message Error message in case of failure.
+ * @param {...*} var_args The items to substitute into the failure message.
+ * @return {number} The value, guaranteed to be a number when asserts enabled.
+ * @throws {goog.asserts.AssertionError} When the value is not a number.
+ */
+goog.asserts.assertNumber = function(value, opt_message, var_args) {
+  if (goog.asserts.ENABLE_ASSERTS && !goog.isNumber(value)) {
+    goog.asserts.doAssertFailure_(
+        'Expected number but got %s: %s.', [goog.typeOf(value), value],
+        opt_message, Array.prototype.slice.call(arguments, 2));
+  }
+  return /** @type {number} */ (value);
+};
+
+
+/**
+ * Checks if the value is a string if goog.asserts.ENABLE_ASSERTS is true.
+ * @param {*} value The value to check.
+ * @param {string=} opt_message Error message in case of failure.
+ * @param {...*} var_args The items to substitute into the failure message.
+ * @return {string} The value, guaranteed to be a string when asserts enabled.
+ * @throws {goog.asserts.AssertionError} When the value is not a string.
+ */
+goog.asserts.assertString = function(value, opt_message, var_args) {
+  if (goog.asserts.ENABLE_ASSERTS && !goog.isString(value)) {
+    goog.asserts.doAssertFailure_(
+        'Expected string but got %s: %s.', [goog.typeOf(value), value],
+        opt_message, Array.prototype.slice.call(arguments, 2));
+  }
+  return /** @type {string} */ (value);
+};
+
+
+/**
+ * Checks if the value is a function if goog.asserts.ENABLE_ASSERTS is true.
+ * @param {*} value The value to check.
+ * @param {string=} opt_message Error message in case of failure.
+ * @param {...*} var_args The items to substitute into the failure message.
+ * @return {!Function} The value, guaranteed to be a function when asserts
+ *     enabled.
+ * @throws {goog.asserts.AssertionError} When the value is not a function.
+ */
+goog.asserts.assertFunction = function(value, opt_message, var_args) {
+  if (goog.asserts.ENABLE_ASSERTS && !goog.isFunction(value)) {
+    goog.asserts.doAssertFailure_(
+        'Expected function but got %s: %s.', [goog.typeOf(value), value],
+        opt_message, Array.prototype.slice.call(arguments, 2));
+  }
+  return /** @type {!Function} */ (value);
+};
+
+
+/**
+ * Checks if the value is an Object if goog.asserts.ENABLE_ASSERTS is true.
+ * @param {*} value The value to check.
+ * @param {string=} opt_message Error message in case of failure.
+ * @param {...*} var_args The items to substitute into the failure message.
+ * @return {!Object} The value, guaranteed to be a non-null object.
+ * @throws {goog.asserts.AssertionError} When the value is not an object.
+ */
+goog.asserts.assertObject = function(value, opt_message, var_args) {
+  if (goog.asserts.ENABLE_ASSERTS && !goog.isObject(value)) {
+    goog.asserts.doAssertFailure_(
+        'Expected object but got %s: %s.', [goog.typeOf(value), value],
+        opt_message, Array.prototype.slice.call(arguments, 2));
+  }
+  return /** @type {!Object} */ (value);
+};
+
+
+/**
+ * Checks if the value is an Array if goog.asserts.ENABLE_ASSERTS is true.
+ * @param {*} value The value to check.
+ * @param {string=} opt_message Error message in case of failure.
+ * @param {...*} var_args The items to substitute into the failure message.
+ * @return {!Array<?>} The value, guaranteed to be a non-null array.
+ * @throws {goog.asserts.AssertionError} When the value is not an array.
+ */
+goog.asserts.assertArray = function(value, opt_message, var_args) {
+  if (goog.asserts.ENABLE_ASSERTS && !goog.isArray(value)) {
+    goog.asserts.doAssertFailure_(
+        'Expected array but got %s: %s.', [goog.typeOf(value), value],
+        opt_message, Array.prototype.slice.call(arguments, 2));
+  }
+  return /** @type {!Array<?>} */ (value);
+};
+
+
+/**
+ * Checks if the value is a boolean if goog.asserts.ENABLE_ASSERTS is true.
+ * @param {*} value The value to check.
+ * @param {string=} opt_message Error message in case of failure.
+ * @param {...*} var_args The items to substitute into the failure message.
+ * @return {boolean} The value, guaranteed to be a boolean when asserts are
+ *     enabled.
+ * @throws {goog.asserts.AssertionError} When the value is not a boolean.
+ */
+goog.asserts.assertBoolean = function(value, opt_message, var_args) {
+  if (goog.asserts.ENABLE_ASSERTS && !goog.isBoolean(value)) {
+    goog.asserts.doAssertFailure_(
+        'Expected boolean but got %s: %s.', [goog.typeOf(value), value],
+        opt_message, Array.prototype.slice.call(arguments, 2));
+  }
+  return /** @type {boolean} */ (value);
+};
+
+
+/**
+ * Checks if the value is a DOM Element if goog.asserts.ENABLE_ASSERTS is true.
+ * @param {*} value The value to check.
+ * @param {string=} opt_message Error message in case of failure.
+ * @param {...*} var_args The items to substitute into the failure message.
+ * @return {!Element} The value, likely to be a DOM Element when asserts are
+ *     enabled.
+ * @throws {goog.asserts.AssertionError} When the value is not an Element.
+ */
+goog.asserts.assertElement = function(value, opt_message, var_args) {
+  if (goog.asserts.ENABLE_ASSERTS &&
+      (!goog.isObject(value) || value.nodeType != goog.dom.NodeType.ELEMENT)) {
+    goog.asserts.doAssertFailure_(
+        'Expected Element but got %s: %s.', [goog.typeOf(value), value],
+        opt_message, Array.prototype.slice.call(arguments, 2));
+  }
+  return /** @type {!Element} */ (value);
+};
+
+
+/**
+ * Checks if the value is an instance of the user-defined type if
+ * goog.asserts.ENABLE_ASSERTS is true.
+ *
+ * The compiler may tighten the type returned by this function.
+ *
+ * @param {?} value The value to check.
+ * @param {function(new: T, ...)} type A user-defined constructor.
+ * @param {string=} opt_message Error message in case of failure.
+ * @param {...*} var_args The items to substitute into the failure message.
+ * @throws {goog.asserts.AssertionError} When the value is not an instance of
+ *     type.
+ * @return {T}
+ * @template T
+ */
+goog.asserts.assertInstanceof = function(value, type, opt_message, var_args) {
+  if (goog.asserts.ENABLE_ASSERTS && !(value instanceof type)) {
+    goog.asserts.doAssertFailure_(
+        'Expected instanceof %s but got %s.',
+        [goog.asserts.getType_(type), goog.asserts.getType_(value)],
+        opt_message, Array.prototype.slice.call(arguments, 3));
+  }
+  return value;
+};
+
+
+/**
+ * Checks that no enumerable keys are present in Object.prototype. Such keys
+ * would break most code that use {@code for (var ... in ...)} loops.
+ */
+goog.asserts.assertObjectPrototypeIsIntact = function() {
+  for (var key in Object.prototype) {
+    goog.asserts.fail(key + ' should not be enumerable in Object.prototype.');
+  }
+};
+
+
+/**
+ * Returns the type of a value. If a constructor is passed, and a suitable
+ * string cannot be found, 'unknown type name' will be returned.
+ * @param {*} value A constructor, object, or primitive.
+ * @return {string} The best display name for the value, or 'unknown type name'.
+ * @private
+ */
+goog.asserts.getType_ = function(value) {
+  if (value instanceof Function) {
+    return value.displayName || value.name || 'unknown type name';
+  } else if (value instanceof Object) {
+    return value.constructor.displayName || value.constructor.name ||
+        Object.prototype.toString.call(value);
+  } else {
+    return value === null ? 'null' : typeof value;
+  }
+};
+
+goog.provide('ol.math');
+
+goog.require('goog.asserts');
+
+
+/**
+ * Takes a number and clamps it to within the provided bounds.
+ * @param {number} value The input number.
+ * @param {number} min The minimum value to return.
+ * @param {number} max The maximum value to return.
+ * @return {number} The input number if it is within bounds, or the nearest
+ *     number within the bounds.
+ */
+ol.math.clamp = function(value, min, max) {
+  return Math.min(Math.max(value, min), max);
+};
+
+
+/**
+ * Return the hyperbolic cosine of a given number. The method will use the
+ * native `Math.cosh` function if it is available, otherwise the hyperbolic
+ * cosine will be calculated via the reference implementation of the Mozilla
+ * developer network.
+ *
+ * @param {number} x X.
+ * @return {number} Hyperbolic cosine of x.
+ */
+ol.math.cosh = (function() {
+  // Wrapped in a iife, to save the overhead of checking for the native
+  // implementation on every invocation.
+  var cosh;
+  if ('cosh' in Math) {
+    // The environment supports the native Math.cosh function, use it…
+    cosh = Math.cosh;
+  } else {
+    // … else, use the reference implementation of MDN:
+    cosh = function(x) {
+      var y = Math.exp(x);
+      return (y + 1 / y) / 2;
+    };
+  }
+  return cosh;
+}());
+
+
+/**
+ * @param {number} x X.
+ * @return {number} The smallest power of two greater than or equal to x.
+ */
+ol.math.roundUpToPowerOfTwo = function(x) {
+  goog.asserts.assert(0 < x, 'x should be larger than 0');
+  return Math.pow(2, Math.ceil(Math.log(x) / Math.LN2));
+};
+
+
+/**
+ * Returns the square of the closest distance between the point (x, y) and the
+ * line segment (x1, y1) to (x2, y2).
+ * @param {number} x X.
+ * @param {number} y Y.
+ * @param {number} x1 X1.
+ * @param {number} y1 Y1.
+ * @param {number} x2 X2.
+ * @param {number} y2 Y2.
+ * @return {number} Squared distance.
+ */
+ol.math.squaredSegmentDistance = function(x, y, x1, y1, x2, y2) {
+  var dx = x2 - x1;
+  var dy = y2 - y1;
+  if (dx !== 0 || dy !== 0) {
+    var t = ((x - x1) * dx + (y - y1) * dy) / (dx * dx + dy * dy);
+    if (t > 1) {
+      x1 = x2;
+      y1 = y2;
+    } else if (t > 0) {
+      x1 += dx * t;
+      y1 += dy * t;
+    }
+  }
+  return ol.math.squaredDistance(x, y, x1, y1);
+};
+
+
+/**
+ * Returns the square of the distance between the points (x1, y1) and (x2, y2).
+ * @param {number} x1 X1.
+ * @param {number} y1 Y1.
+ * @param {number} x2 X2.
+ * @param {number} y2 Y2.
+ * @return {number} Squared distance.
+ */
+ol.math.squaredDistance = function(x1, y1, x2, y2) {
+  var dx = x2 - x1;
+  var dy = y2 - y1;
+  return dx * dx + dy * dy;
+};
+
+
+/**
+ * Solves system of linear equations using Gaussian elimination method.
+ *
+ * @param {Array.<Array.<number>>} mat Augmented matrix (n x n + 1 column)
+ *                                     in row-major order.
+ * @return {Array.<number>} The resulting vector.
+ */
+ol.math.solveLinearSystem = function(mat) {
+  var n = mat.length;
+
+  if (goog.asserts.ENABLE_ASSERTS) {
+    for (var row = 0; row < n; row++) {
+      goog.asserts.assert(mat[row].length == n + 1,
+                          'every row should have correct number of columns');
+    }
+  }
+
+  for (var i = 0; i < n; i++) {
+    // Find max in the i-th column (ignoring i - 1 first rows)
+    var maxRow = i;
+    var maxEl = Math.abs(mat[i][i]);
+    for (var r = i + 1; r < n; r++) {
+      var absValue = Math.abs(mat[r][i]);
+      if (absValue > maxEl) {
+        maxEl = absValue;
+        maxRow = r;
+      }
+    }
+
+    if (maxEl === 0) {
+      return null; // matrix is singular
+    }
+
+    // Swap max row with i-th (current) row
+    var tmp = mat[maxRow];
+    mat[maxRow] = mat[i];
+    mat[i] = tmp;
+
+    // Subtract the i-th row to make all the remaining rows 0 in the i-th column
+    for (var j = i + 1; j < n; j++) {
+      var coef = -mat[j][i] / mat[i][i];
+      for (var k = i; k < n + 1; k++) {
+        if (i == k) {
+          mat[j][k] = 0;
+        } else {
+          mat[j][k] += coef * mat[i][k];
+        }
+      }
+    }
+  }
+
+  // Solve Ax=b for upper triangular matrix A (mat)
+  var x = new Array(n);
+  for (var l = n - 1; l >= 0; l--) {
+    x[l] = mat[l][n] / mat[l][l];
+    for (var m = l - 1; m >= 0; m--) {
+      mat[m][n] -= mat[m][l] * x[l];
+    }
+  }
+  return x;
+};
+
+
+/**
+ * Converts radians to to degrees.
+ *
+ * @param {number} angleInRadians Angle in radians.
+ * @return {number} Angle in degrees.
+ */
+ol.math.toDegrees = function(angleInRadians) {
+  return angleInRadians * 180 / Math.PI;
+};
+
+
+/**
+ * Converts degrees to radians.
+ *
+ * @param {number} angleInDegrees Angle in degrees.
+ * @return {number} Angle in radians.
+ */
+ol.math.toRadians = function(angleInDegrees) {
+  return angleInDegrees * Math.PI / 180;
+};
+
+/**
+ * Returns the modulo of a / b, depending on the sign of b.
+ *
+ * @param {number} a Dividend.
+ * @param {number} b Divisor.
+ * @return {number} Modulo.
+ */
+ol.math.modulo = function(a, b) {
+  var r = a % b;
+  return r * b < 0 ? r + b : r;
+};
+
+/**
+ * Calculates the linearly interpolated value of x between a and b.
+ *
+ * @param {number} a Number
+ * @param {number} b Number
+ * @param {number} x Value to be interpolated.
+ * @return {number} Interpolated value.
+ */
+ol.math.lerp = function(a, b, x) {
+  return a + x * (b - a);
+};
+
+goog.provide('ol.CenterConstraint');
+
+goog.require('ol.math');
+
+
+/**
+ * @param {ol.Extent} extent Extent.
+ * @return {ol.CenterConstraintType} The constraint.
+ */
+ol.CenterConstraint.createExtent = function(extent) {
+  return (
+      /**
+       * @param {ol.Coordinate|undefined} center Center.
+       * @return {ol.Coordinate|undefined} Center.
+       */
+      function(center) {
+        if (center) {
+          return [
+            ol.math.clamp(center[0], extent[0], extent[2]),
+            ol.math.clamp(center[1], extent[1], extent[3])
+          ];
+        } else {
+          return undefined;
+        }
+      });
+};
+
+
+/**
+ * @param {ol.Coordinate|undefined} center Center.
+ * @return {ol.Coordinate|undefined} Center.
+ */
+ol.CenterConstraint.none = function(center) {
+  return center;
+};
+
+goog.provide('ol.Constraints');
+
+
+/**
+ * @constructor
+ * @param {ol.CenterConstraintType} centerConstraint Center constraint.
+ * @param {ol.ResolutionConstraintType} resolutionConstraint
+ *     Resolution constraint.
+ * @param {ol.RotationConstraintType} rotationConstraint
+ *     Rotation constraint.
+ */
+ol.Constraints = function(centerConstraint, resolutionConstraint, rotationConstraint) {
+
+  /**
+   * @type {ol.CenterConstraintType}
+   */
+  this.center = centerConstraint;
+
+  /**
+   * @type {ol.ResolutionConstraintType}
+   */
+  this.resolution = resolutionConstraint;
+
+  /**
+   * @type {ol.RotationConstraintType}
+   */
+  this.rotation = rotationConstraint;
+
+};
+
+goog.provide('ol.object');
+
+
+/**
+ * Polyfill 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.
+ */
+ol.object.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.
+ */
+ol.object.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
+ */
+ol.object.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 {boolean} The object is empty.
+ */
+ol.object.isEmpty = function(object) {
+  var property;
+  for (property in object) {
+    return false;
+  }
+  return !property;
+};
+
+goog.provide('ol.events');
+goog.provide('ol.events.EventType');
+goog.provide('ol.events.KeyCode');
+
+goog.require('ol.object');
+
+
+/**
+ * @enum {string}
+ * @const
+ */
+ol.events.EventType = {
+  /**
+   * Generic change event.
+   * @event ol.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'
+};
+
+
+/**
+ * @enum {number}
+ * @const
+ */
+ol.events.KeyCode = {
+  LEFT: 37,
+  UP: 38,
+  RIGHT: 39,
+  DOWN: 40
+};
+
+
+/**
+ * Property name on an event target for the listener map associated with the
+ * event target.
+ * @const {string}
+ * @private
+ */
+ol.events.LISTENER_MAP_PROP_ = 'olm_' + ((Math.random() * 1e4) | 0);
+
+
+/**
+ * @param {ol.EventsKey} listenerObj Listener object.
+ * @return {ol.EventsListenerFunctionType} Bound listener.
+ */
+ol.events.bindListener_ = function(listenerObj) {
+  var boundListener = function(evt) {
+    var listener = listenerObj.listener;
+    var bindTo = listenerObj.bindTo || listenerObj.target;
+    if (listenerObj.callOnce) {
+      ol.events.unlistenByKey(listenerObj);
+    }
+    return listener.call(bindTo, evt);
+  };
+  listenerObj.boundListener = boundListener;
+  return boundListener;
+};
+
+
+/**
+ * Finds the matching {@link ol.EventsKey} in the given listener
+ * array.
+ *
+ * @param {!Array<!ol.EventsKey>} listeners Array of listeners.
+ * @param {!Function} listener The listener function.
+ * @param {Object=} opt_this The `this` value inside the listener.
+ * @param {boolean=} opt_setDeleteIndex Set the deleteIndex on the matching
+ *     listener, for {@link ol.events.unlistenByKey}.
+ * @return {ol.EventsKey|undefined} The matching listener object.
+ * @private
+ */
+ol.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 {ol.EventTargetLike} target Target.
+ * @param {string} type Type.
+ * @return {Array.<ol.EventsKey>|undefined} Listeners.
+ */
+ol.events.getListeners = function(target, type) {
+  var listenerMap = target[ol.events.LISTENER_MAP_PROP_];
+  return listenerMap ? listenerMap[type] : undefined;
+};
+
+
+/**
+ * Get the lookup of listeners.  If one does not exist on the target, it is
+ * created.
+ * @param {ol.EventTargetLike} target Target.
+ * @return {!Object.<string, Array.<ol.EventsKey>>} Map of
+ *     listeners by event type.
+ * @private
+ */
+ol.events.getListenerMap_ = function(target) {
+  var listenerMap = target[ol.events.LISTENER_MAP_PROP_];
+  if (!listenerMap) {
+    listenerMap = target[ol.events.LISTENER_MAP_PROP_] = {};
+  }
+  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 {ol.EventTargetLike} target Target.
+ * @param {string} type Type.
+ * @private
+ */
+ol.events.removeListeners_ = function(target, type) {
+  var listeners = ol.events.getListeners(target, type);
+  if (listeners) {
+    for (var i = 0, ii = listeners.length; i < ii; ++i) {
+      target.removeEventListener(type, listeners[i].boundListener);
+      ol.object.clear(listeners[i]);
+    }
+    listeners.length = 0;
+    var listenerMap = target[ol.events.LISTENER_MAP_PROP_];
+    if (listenerMap) {
+      delete listenerMap[type];
+      if (Object.keys(listenerMap).length === 0) {
+        delete target[ol.events.LISTENER_MAP_PROP_];
+      }
+    }
+  }
+};
+
+
+/**
+ * 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 ol.events.unlistenByKey}.
+ *
+ * @param {ol.EventTargetLike} target Event target.
+ * @param {string} type Event type.
+ * @param {ol.EventsListenerFunctionType} listener Listener.
+ * @param {Object=} opt_this Object referenced by the `this` keyword in the
+ *     listener. Default is the `target`.
+ * @param {boolean=} opt_once If true, add the listener as one-off listener.
+ * @return {ol.EventsKey} Unique key for the listener.
+ */
+ol.events.listen = function(target, type, listener, opt_this, opt_once) {
+  var listenerMap = ol.events.getListenerMap_(target);
+  var listeners = listenerMap[type];
+  if (!listeners) {
+    listeners = listenerMap[type] = [];
+  }
+  var listenerObj = ol.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 {ol.EventsKey} */ ({
+      bindTo: opt_this,
+      callOnce: !!opt_once,
+      listener: listener,
+      target: target,
+      type: type
+    });
+    target.addEventListener(type, ol.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 ol.events.unlistenByKey} in case the listener needs to be unregistered
+ * before it is called.
+ *
+ * When {@link ol.events.listen} is called with the same arguments after this
+ * function, the self-unregistering listener will be turned into a permanent
+ * listener.
+ *
+ * @param {ol.EventTargetLike} target Event target.
+ * @param {string} type Event type.
+ * @param {ol.EventsListenerFunctionType} listener Listener.
+ * @param {Object=} opt_this Object referenced by the `this` keyword in the
+ *     listener. Default is the `target`.
+ * @return {ol.EventsKey} Key for unlistenByKey.
+ */
+ol.events.listenOnce = function(target, type, listener, opt_this) {
+  return ol.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 ol.events.listen} call.
+ *
+ * @param {ol.EventTargetLike} target Event target.
+ * @param {string} type Event type.
+ * @param {ol.EventsListenerFunctionType} listener Listener.
+ * @param {Object=} opt_this Object referenced by the `this` keyword in the
+ *     listener. Default is the `target`.
+ */
+ol.events.unlisten = function(target, type, listener, opt_this) {
+  var listeners = ol.events.getListeners(target, type);
+  if (listeners) {
+    var listenerObj = ol.events.findListener_(listeners, listener, opt_this,
+        true);
+    if (listenerObj) {
+      ol.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 ol.events.listen} or {@link ol.events.listenOnce}.
+ *
+ * @param {ol.EventsKey} key The key.
+ */
+ol.events.unlistenByKey = function(key) {
+  if (key && key.target) {
+    key.target.removeEventListener(key.type, key.boundListener);
+    var listeners = ol.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) {
+        ol.events.removeListeners_(key.target, key.type);
+      }
+    }
+    ol.object.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 {ol.EventTargetLike} target Target.
+ */
+ol.events.unlistenAll = function(target) {
+  var listenerMap = ol.events.getListenerMap_(target);
+  for (var type in listenerMap) {
+    ol.events.removeListeners_(target, type);
+  }
+};
+
+goog.provide('ol.Disposable');
+
+goog.require('ol');
+
+/**
+ * Objects that need to clean up after themselves.
+ * @constructor
+ */
+ol.Disposable = function() {};
+
+/**
+ * The object has already been disposed.
+ * @type {boolean}
+ * @private
+ */
+ol.Disposable.prototype.disposed_ = false;
+
+/**
+ * Clean up.
+ */
+ol.Disposable.prototype.dispose = function() {
+  if (!this.disposed_) {
+    this.disposed_ = true;
+    this.disposeInternal();
+  }
+};
+
+/**
+ * Extension point for disposable objects.
+ * @protected
+ */
+ol.Disposable.prototype.disposeInternal = ol.nullFunction;
+
+goog.provide('ol.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 ol.events.EventTarget}.
+ *
+ * @constructor
+ * @implements {oli.events.Event}
+ * @param {string} type Type.
+ * @param {Object=} opt_target Target.
+ */
+ol.events.Event = function(type, opt_target) {
+
+  /**
+   * @type {boolean}
+   */
+  this.propagationStopped;
+
+  /**
+   * The event type.
+   * @type {string}
+   * @api stable
+   */
+  this.type = type;
+
+  /**
+   * The event target.
+   * @type {Object}
+   * @api stable
+   */
+  this.target = opt_target || null;
+
+};
+
+
+/**
+ * Stop event propagation.
+ * @function
+ * @api stable
+ */
+ol.events.Event.prototype.preventDefault =
+
+/**
+ * Stop event propagation.
+ * @function
+ * @api stable
+ */
+ol.events.Event.prototype.stopPropagation = function() {
+  this.propagationStopped = true;
+};
+
+
+/**
+ * @param {Event|ol.events.Event} evt Event
+ */
+ol.events.Event.stopPropagation = function(evt) {
+  evt.stopPropagation();
+};
+
+
+/**
+ * @param {Event|ol.events.Event} evt Event
+ */
+ol.events.Event.preventDefault = function(evt) {
+  evt.preventDefault();
+};
+
+goog.provide('ol.events.EventTarget');
+
+goog.require('goog.asserts');
+goog.require('ol.Disposable');
+goog.require('ol.events');
+goog.require('ol.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 {ol.Disposable}
+ */
+ol.events.EventTarget = function() {
+
+  ol.Disposable.call(this);
+
+  /**
+   * @private
+   * @type {!Object.<string, number>}
+   */
+  this.pendingRemovals_ = {};
+
+  /**
+   * @private
+   * @type {!Object.<string, number>}
+   */
+  this.dispatching_ = {};
+
+  /**
+   * @private
+   * @type {!Object.<string, Array.<ol.EventsListenerFunctionType>>}
+   */
+  this.listeners_ = {};
+
+};
+ol.inherits(ol.events.EventTarget, ol.Disposable);
+
+
+/**
+ * @param {string} type Type.
+ * @param {ol.EventsListenerFunctionType} listener Listener.
+ */
+ol.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|ol.events.EventTarget|undefined)}|ol.events.Event|
+ *     string} event Event or event type.
+ * @return {boolean|undefined} `false` if anyone called preventDefault on the
+ *     event object or if any of the listeners returned false.
+ */
+ol.events.EventTarget.prototype.dispatchEvent = function(event) {
+  var evt = typeof event === 'string' ? new ol.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, ol.nullFunction);
+      }
+      delete this.dispatching_[type];
+    }
+    return propagate;
+  }
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.events.EventTarget.prototype.disposeInternal = function() {
+  ol.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.<ol.EventsListenerFunctionType>} Listeners.
+ */
+ol.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 {boolean} Has listeners.
+ */
+ol.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 {ol.EventsListenerFunctionType} listener Listener.
+ */
+ol.events.EventTarget.prototype.removeEventListener = function(type, listener) {
+  var listeners = this.listeners_[type];
+  if (listeners) {
+    var index = listeners.indexOf(listener);
+    goog.asserts.assert(index != -1, 'listener not found');
+    if (type in this.pendingRemovals_) {
+      // make listener a no-op, and remove later in #dispatchEvent()
+      listeners[index] = ol.nullFunction;
+      ++this.pendingRemovals_[type];
+    } else {
+      listeners.splice(index, 1);
+      if (listeners.length === 0) {
+        delete this.listeners_[type];
+      }
+    }
+  }
+};
+
+goog.provide('ol.Observable');
+
+goog.require('ol.events');
+goog.require('ol.events.EventTarget');
+goog.require('ol.events.EventType');
+
+
+/**
+ * @classdesc
+ * Abstract base class; normally only used for creating subclasses and not
+ * instantiated in apps.
+ * An event target providing convenient methods for listener registration
+ * and unregistration. A generic `change` event is always available through
+ * {@link ol.Observable#changed}.
+ *
+ * @constructor
+ * @extends {ol.events.EventTarget}
+ * @fires change
+ * @struct
+ * @api stable
+ */
+ol.Observable = function() {
+
+  ol.events.EventTarget.call(this);
+
+  /**
+   * @private
+   * @type {number}
+   */
+  this.revision_ = 0;
+
+};
+ol.inherits(ol.Observable, ol.events.EventTarget);
+
+
+/**
+ * Removes an event listener using the key returned by `on()` or `once()`.
+ * @param {ol.EventsKey|Array.<ol.EventsKey>} key The key returned by `on()`
+ *     or `once()` (or an array of keys).
+ * @api stable
+ */
+ol.Observable.unByKey = function(key) {
+  if (Array.isArray(key)) {
+    for (var i = 0, ii = key.length; i < ii; ++i) {
+      ol.events.unlistenByKey(key[i]);
+    }
+  } else {
+    ol.events.unlistenByKey(/** @type {ol.EventsKey} */ (key));
+  }
+};
+
+
+/**
+ * Increases the revision counter and dispatches a 'change' event.
+ * @api
+ */
+ol.Observable.prototype.changed = function() {
+  ++this.revision_;
+  this.dispatchEvent(ol.events.EventType.CHANGE);
+};
+
+
+/**
+ * Triggered when the revision counter is increased.
+ * @event change
+ * @api
+ */
+
+
+/**
+ * Dispatches an event and calls all listeners listening for events
+ * of this type. The event parameter can either be a string or an
+ * Object with a `type` property.
+ *
+ * @param {{type: string,
+ *     target: (EventTarget|ol.events.EventTarget|undefined)}|ol.events.Event|
+ *     string} event Event object.
+ * @function
+ * @api
+ */
+ol.Observable.prototype.dispatchEvent;
+
+
+/**
+ * Get the version number for this object.  Each time the object is modified,
+ * its version number will be incremented.
+ * @return {number} Revision.
+ * @api
+ */
+ol.Observable.prototype.getRevision = function() {
+  return this.revision_;
+};
+
+
+/**
+ * Listen for a certain type of event.
+ * @param {string|Array.<string>} type The event type or array of event types.
+ * @param {function(?): ?} listener The listener function.
+ * @param {Object=} opt_this The object to use as `this` in `listener`.
+ * @return {ol.EventsKey|Array.<ol.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 stable
+ */
+ol.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] = ol.events.listen(this, type[i], listener, opt_this);
+    }
+    return keys;
+  } else {
+    return ol.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 {ol.EventsKey|Array.<ol.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 stable
+ */
+ol.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] = ol.events.listenOnce(this, type[i], listener, opt_this);
+    }
+    return keys;
+  } else {
+    return ol.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 stable
+ */
+ol.Observable.prototype.un = function(type, listener, opt_this) {
+  if (Array.isArray(type)) {
+    for (var i = 0, ii = type.length; i < ii; ++i) {
+      ol.events.unlisten(this, type[i], listener, opt_this);
+    }
+    return;
+  } else {
+    ol.events.unlisten(this, /** @type {string} */ (type), listener, opt_this);
+  }
+};
+
+
+/**
+ * Removes an event listener using the key returned by `on()` or `once()`.
+ * Note that using the {@link ol.Observable.unByKey} static function is to
+ * be preferred.
+ * @param {ol.EventsKey|Array.<ol.EventsKey>} key The key returned by `on()`
+ *     or `once()` (or an array of keys).
+ * @function
+ * @api stable
+ */
+ol.Observable.prototype.unByKey = ol.Observable.unByKey;
+
+goog.provide('ol.Object');
+goog.provide('ol.ObjectEvent');
+goog.provide('ol.ObjectEventType');
+
+goog.require('ol.Observable');
+goog.require('ol.events');
+goog.require('ol.events.Event');
+goog.require('ol.object');
+
+
+/**
+ * @enum {string}
+ */
+ol.ObjectEventType = {
+  /**
+   * Triggered when a property is changed.
+   * @event ol.ObjectEvent#propertychange
+   * @api stable
+   */
+  PROPERTYCHANGE: 'propertychange'
+};
+
+
+/**
+ * @classdesc
+ * Events emitted by {@link ol.Object} instances are instances of this type.
+ *
+ * @param {string} type The event type.
+ * @param {string} key The property name.
+ * @param {*} oldValue The old value for `key`.
+ * @extends {ol.events.Event}
+ * @implements {oli.ObjectEvent}
+ * @constructor
+ */
+ol.ObjectEvent = function(type, key, oldValue) {
+  ol.events.Event.call(this, type);
+
+  /**
+   * The name of the property whose value is changing.
+   * @type {string}
+   * @api stable
+   */
+  this.key = key;
+
+  /**
+   * The old value. To get the new value use `e.target.get(e.key)` where
+   * `e` is the event object.
+   * @type {*}
+   * @api stable
+   */
+  this.oldValue = oldValue;
+
+};
+ol.inherits(ol.ObjectEvent, ol.events.Event);
+
+
+/**
+ * @classdesc
+ * Abstract base class; normally only used for creating subclasses and not
+ * instantiated in apps.
+ * Most non-trivial classes inherit from this.
+ *
+ * This extends {@link ol.Observable} with observable properties, where each
+ * property is observable as well as the object as a whole.
+ *
+ * Classes that inherit from this have pre-defined properties, to which you can
+ * add your owns. The pre-defined properties are listed in this documentation as
+ * 'Observable Properties', and have their own accessors; for example,
+ * {@link ol.Map} has a `target` property, accessed with `getTarget()`  and
+ * changed with `setTarget()`. Not all properties are however settable. There
+ * are also general-purpose accessors `get()` and `set()`. For example,
+ * `get('target')` is equivalent to `getTarget()`.
+ *
+ * The `set` accessors trigger a change event, and you can monitor this by
+ * registering a listener. For example, {@link ol.View} has a `center`
+ * property, so `view.on('change:center', function(evt) {...});` would call the
+ * function whenever the value of the center property changes. Within the
+ * function, `evt.target` would be the view, so `evt.target.getCenter()` would
+ * return the new center.
+ *
+ * You can add your own observable properties with
+ * `object.set('prop', 'value')`, and retrieve that with `object.get('prop')`.
+ * You can listen for changes on that property value with
+ * `object.on('change:prop', listener)`. You can get a list of all
+ * properties with {@link ol.Object#getProperties object.getProperties()}.
+ *
+ * Note that the observable properties are separate from standard JS properties.
+ * You can, for example, give your map object a title with
+ * `map.title='New title'` and with `map.set('title', 'Another title')`. The
+ * first will be a `hasOwnProperty`; the second will appear in
+ * `getProperties()`. Only the second is observable.
+ *
+ * Properties can be deleted by using the unset method. E.g.
+ * object.unset('foo').
+ *
+ * @constructor
+ * @extends {ol.Observable}
+ * @param {Object.<string, *>=} opt_values An object with key-value pairs.
+ * @fires ol.ObjectEvent
+ * @api
+ */
+ol.Object = function(opt_values) {
+  ol.Observable.call(this);
+
+  // Call goog.getUid to ensure that the order of objects' ids is the same as
+  // the order in which they were created.  This also helps to ensure that
+  // object properties are always added in the same order, which helps many
+  // JavaScript engines generate faster code.
+  goog.getUid(this);
+
+  /**
+   * @private
+   * @type {!Object.<string, *>}
+   */
+  this.values_ = {};
+
+  if (opt_values !== undefined) {
+    this.setProperties(opt_values);
+  }
+};
+ol.inherits(ol.Object, ol.Observable);
+
+
+/**
+ * @private
+ * @type {Object.<string, string>}
+ */
+ol.Object.changeEventTypeCache_ = {};
+
+
+/**
+ * @param {string} key Key name.
+ * @return {string} Change name.
+ */
+ol.Object.getChangeEventType = function(key) {
+  return ol.Object.changeEventTypeCache_.hasOwnProperty(key) ?
+      ol.Object.changeEventTypeCache_[key] :
+      (ol.Object.changeEventTypeCache_[key] = 'change:' + key);
+};
+
+
+/**
+ * Gets a value.
+ * @param {string} key Key name.
+ * @return {*} Value.
+ * @api stable
+ */
+ol.Object.prototype.get = function(key) {
+  var value;
+  if (this.values_.hasOwnProperty(key)) {
+    value = this.values_[key];
+  }
+  return value;
+};
+
+
+/**
+ * Get a list of object property names.
+ * @return {Array.<string>} List of property names.
+ * @api stable
+ */
+ol.Object.prototype.getKeys = function() {
+  return Object.keys(this.values_);
+};
+
+
+/**
+ * Get an object of all property names and values.
+ * @return {Object.<string, *>} Object.
+ * @api stable
+ */
+ol.Object.prototype.getProperties = function() {
+  return ol.object.assign({}, this.values_);
+};
+
+
+/**
+ * @param {string} key Key name.
+ * @param {*} oldValue Old value.
+ */
+ol.Object.prototype.notify = function(key, oldValue) {
+  var eventType;
+  eventType = ol.Object.getChangeEventType(key);
+  this.dispatchEvent(new ol.ObjectEvent(eventType, key, oldValue));
+  eventType = ol.ObjectEventType.PROPERTYCHANGE;
+  this.dispatchEvent(new ol.ObjectEvent(eventType, key, oldValue));
+};
+
+
+/**
+ * Sets a value.
+ * @param {string} key Key name.
+ * @param {*} value Value.
+ * @param {boolean=} opt_silent Update without triggering an event.
+ * @api stable
+ */
+ol.Object.prototype.set = function(key, value, opt_silent) {
+  if (opt_silent) {
+    this.values_[key] = value;
+  } else {
+    var oldValue = this.values_[key];
+    this.values_[key] = value;
+    if (oldValue !== value) {
+      this.notify(key, oldValue);
+    }
+  }
+};
+
+
+/**
+ * Sets a collection of key-value pairs.  Note that this changes any existing
+ * properties and adds new ones (it does not remove any existing properties).
+ * @param {Object.<string, *>} values Values.
+ * @param {boolean=} opt_silent Update without triggering an event.
+ * @api stable
+ */
+ol.Object.prototype.setProperties = function(values, opt_silent) {
+  var key;
+  for (key in values) {
+    this.set(key, values[key], opt_silent);
+  }
+};
+
+
+/**
+ * Unsets a property.
+ * @param {string} key Key name.
+ * @param {boolean=} opt_silent Unset without triggering an event.
+ * @api stable
+ */
+ol.Object.prototype.unset = function(key, opt_silent) {
+  if (key in this.values_) {
+    var oldValue = this.values_[key];
+    delete this.values_[key];
+    if (!opt_silent) {
+      this.notify(key, oldValue);
+    }
+  }
+};
+
+goog.provide('ol.array');
+
+goog.require('goog.asserts');
+
+
+/**
+ * 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.
+ */
+ol.array.binarySearch = function(haystack, needle, opt_comparator) {
+  var mid, cmp;
+  var comparator = opt_comparator || ol.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;
+};
+
+/**
+ * @param {Array.<number>} arr Array.
+ * @param {number} target Target.
+ * @return {number} Index.
+ */
+ol.array.binaryFindNearest = function(arr, target) {
+  var index = ol.array.binarySearch(arr, target,
+      /**
+       * @param {number} a A.
+       * @param {number} b B.
+       * @return {number} b minus a.
+       */
+      function(a, b) {
+        return b - a;
+      });
+  if (index >= 0) {
+    return index;
+  } else if (index == -1) {
+    return 0;
+  } else if (index == -arr.length - 1) {
+    return arr.length - 1;
+  } else {
+    var left = -index - 2;
+    var right = -index - 1;
+    if (arr[left] - target < target - arr[right]) {
+      return left;
+    } else {
+      return right;
+    }
+  }
+};
+
+
+/**
+ * Compare function for array sort that is safe for numbers.
+ * @param {*} a The first object to be compared.
+ * @param {*} b The second object to be compared.
+ * @return {number} A negative number, zero, or a positive number as the first
+ *     argument is less than, equal to, or greater than the second.
+ */
+ol.array.numberSafeCompareFunction = function(a, b) {
+  return a > b ? 1 : a < b ? -1 : 0;
+};
+
+
+/**
+ * Whether the array contains the given object.
+ * @param {Array.<*>} arr The array to test for the presence of the element.
+ * @param {*} obj The object for which to test.
+ * @return {boolean} The object is in the array.
+ */
+ol.array.includes = function(arr, obj) {
+  return arr.indexOf(obj) >= 0;
+};
+
+
+/**
+ * @param {Array.<number>} arr Array.
+ * @param {number} target Target.
+ * @param {number} direction 0 means return the nearest, > 0
+ *    means return the largest nearest, < 0 means return the
+ *    smallest nearest.
+ * @return {number} Index.
+ */
+ol.array.linearFindNearest = function(arr, target, direction) {
+  var n = arr.length;
+  if (arr[0] <= target) {
+    return 0;
+  } else if (target <= arr[n - 1]) {
+    return n - 1;
+  } else {
+    var i;
+    if (direction > 0) {
+      for (i = 1; i < n; ++i) {
+        if (arr[i] < target) {
+          return i - 1;
+        }
+      }
+    } else if (direction < 0) {
+      for (i = 1; i < n; ++i) {
+        if (arr[i] <= target) {
+          return i;
+        }
+      }
+    } else {
+      for (i = 1; i < n; ++i) {
+        if (arr[i] == target) {
+          return i;
+        } else if (arr[i] < target) {
+          if (arr[i - 1] - target < target - arr[i]) {
+            return i - 1;
+          } else {
+            return i;
+          }
+        }
+      }
+    }
+    // We should never get here, but the compiler complains
+    // if it finds a path for which no number is returned.
+    goog.asserts.fail();
+    return n - 1;
+  }
+};
+
+
+/**
+ * @param {Array.<*>} arr Array.
+ * @param {number} begin Begin index.
+ * @param {number} end End index.
+ */
+ol.array.reverseSubArray = function(arr, begin, end) {
+  goog.asserts.assert(begin >= 0,
+      'Array begin index should be equal to or greater than 0');
+  goog.asserts.assert(end < arr.length,
+      'Array end index should be less than the array length');
+  while (begin < end) {
+    var tmp = arr[begin];
+    arr[begin] = arr[end];
+    arr[end] = tmp;
+    ++begin;
+    --end;
+  }
+};
+
+
+/**
+ * @param {Array.<*>} arr Array.
+ * @return {!Array.<?>} Flattened Array.
+ */
+ol.array.flatten = function(arr) {
+  var data = arr.reduce(function(flattened, value) {
+    if (Array.isArray(value)) {
+      return flattened.concat(ol.array.flatten(value));
+    } else {
+      return flattened.concat(value);
+    }
+  }, []);
+  return data;
+};
+
+
+/**
+ * @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
+ */
+ol.array.extend = function(arr, data) {
+  var i;
+  var extension = goog.isArrayLike(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 {boolean} If the element was removed.
+ */
+ol.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, ?) : boolean} func The function to compare.
+ * @template VALUE
+ * @return {VALUE} The element found.
+ */
+ol.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 {boolean} Whether the two arrays are equal.
+ */
+ol.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.
+ */
+ol.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.
+ */
+ol.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 {boolean=} opt_strict Strictly sorted (default false).
+ * @return {boolean} Return index.
+ */
+ol.array.isSorted = function(arr, opt_func, opt_strict) {
+  var compare = opt_func || ol.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('ol.ResolutionConstraint');
+
+goog.require('ol.array');
+goog.require('ol.math');
+
+
+/**
+ * @param {Array.<number>} resolutions Resolutions.
+ * @return {ol.ResolutionConstraintType} Zoom function.
+ */
+ol.ResolutionConstraint.createSnapToResolutions = function(resolutions) {
+  return (
+      /**
+       * @param {number|undefined} resolution Resolution.
+       * @param {number} delta Delta.
+       * @param {number} direction Direction.
+       * @return {number|undefined} Resolution.
+       */
+      function(resolution, delta, direction) {
+        if (resolution !== undefined) {
+          var z =
+              ol.array.linearFindNearest(resolutions, resolution, direction);
+          z = ol.math.clamp(z + delta, 0, resolutions.length - 1);
+          return resolutions[z];
+        } else {
+          return undefined;
+        }
+      });
+};
+
+
+/**
+ * @param {number} power Power.
+ * @param {number} maxResolution Maximum resolution.
+ * @param {number=} opt_maxLevel Maximum level.
+ * @return {ol.ResolutionConstraintType} Zoom function.
+ */
+ol.ResolutionConstraint.createSnapToPower = function(power, maxResolution, opt_maxLevel) {
+  return (
+      /**
+       * @param {number|undefined} resolution Resolution.
+       * @param {number} delta Delta.
+       * @param {number} direction Direction.
+       * @return {number|undefined} Resolution.
+       */
+      function(resolution, delta, direction) {
+        if (resolution !== undefined) {
+          var offset;
+          if (direction > 0) {
+            offset = 0;
+          } else if (direction < 0) {
+            offset = 1;
+          } else {
+            offset = 0.5;
+          }
+          var oldLevel = Math.floor(
+              Math.log(maxResolution / resolution) / Math.log(power) + offset);
+          var newLevel = Math.max(oldLevel + delta, 0);
+          if (opt_maxLevel !== undefined) {
+            newLevel = Math.min(newLevel, opt_maxLevel);
+          }
+          return maxResolution / Math.pow(power, newLevel);
+        } else {
+          return undefined;
+        }
+      });
+};
+
+goog.provide('ol.RotationConstraint');
+
+goog.require('ol.math');
+
+
+/**
+ * @param {number|undefined} rotation Rotation.
+ * @param {number} delta Delta.
+ * @return {number|undefined} Rotation.
+ */
+ol.RotationConstraint.disable = function(rotation, delta) {
+  if (rotation !== undefined) {
+    return 0;
+  } else {
+    return undefined;
+  }
+};
+
+
+/**
+ * @param {number|undefined} rotation Rotation.
+ * @param {number} delta Delta.
+ * @return {number|undefined} Rotation.
+ */
+ol.RotationConstraint.none = function(rotation, delta) {
+  if (rotation !== undefined) {
+    return rotation + delta;
+  } else {
+    return undefined;
+  }
+};
+
+
+/**
+ * @param {number} n N.
+ * @return {ol.RotationConstraintType} Rotation constraint.
+ */
+ol.RotationConstraint.createSnapToN = function(n) {
+  var theta = 2 * Math.PI / n;
+  return (
+      /**
+       * @param {number|undefined} rotation Rotation.
+       * @param {number} delta Delta.
+       * @return {number|undefined} Rotation.
+       */
+      function(rotation, delta) {
+        if (rotation !== undefined) {
+          rotation = Math.floor((rotation + delta) / theta + 0.5) * theta;
+          return rotation;
+        } else {
+          return undefined;
+        }
+      });
+};
+
+
+/**
+ * @param {number=} opt_tolerance Tolerance.
+ * @return {ol.RotationConstraintType} Rotation constraint.
+ */
+ol.RotationConstraint.createSnapToZero = function(opt_tolerance) {
+  var tolerance = opt_tolerance || ol.math.toRadians(5);
+  return (
+      /**
+       * @param {number|undefined} rotation Rotation.
+       * @param {number} delta Delta.
+       * @return {number|undefined} Rotation.
+       */
+      function(rotation, delta) {
+        if (rotation !== undefined) {
+          if (Math.abs(rotation + delta) <= tolerance) {
+            return 0;
+          } else {
+            return rotation + delta;
+          }
+        } else {
+          return undefined;
+        }
+      });
+};
+
+goog.provide('ol.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
+*/
+ol.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
+ */
+ol.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('ol.coordinate');
+
+goog.require('ol.math');
+goog.require('ol.string');
+
+
+/**
+ * Add `delta` to `coordinate`. `coordinate` is modified in place and returned
+ * by the function.
+ *
+ * Example:
+ *
+ *     var coord = [7.85, 47.983333];
+ *     ol.coordinate.add(coord, [-2, 4]);
+ *     // coord is now [5.85, 51.983333]
+ *
+ * @param {ol.Coordinate} coordinate Coordinate.
+ * @param {ol.Coordinate} delta Delta.
+ * @return {ol.Coordinate} The input coordinate adjusted by the given delta.
+ * @api stable
+ */
+ol.coordinate.add = function(coordinate, delta) {
+  coordinate[0] += delta[0];
+  coordinate[1] += delta[1];
+  return coordinate;
+};
+
+
+/**
+ * Calculates the point closest to the passed coordinate on the passed segment.
+ * This is the foot of the perpendicular of the coordinate to the segment when
+ * the foot is on the segment, or the closest segment coordinate when the foot
+ * is outside the segment.
+ *
+ * @param {ol.Coordinate} coordinate The coordinate.
+ * @param {Array.<ol.Coordinate>} segment The two coordinates of the segment.
+ * @return {ol.Coordinate} The foot of the perpendicular of the coordinate to
+ *     the segment.
+ */
+ol.coordinate.closestOnSegment = function(coordinate, segment) {
+  var x0 = coordinate[0];
+  var y0 = coordinate[1];
+  var start = segment[0];
+  var end = segment[1];
+  var x1 = start[0];
+  var y1 = start[1];
+  var x2 = end[0];
+  var y2 = end[1];
+  var dx = x2 - x1;
+  var dy = y2 - y1;
+  var along = (dx === 0 && dy === 0) ? 0 :
+      ((dx * (x0 - x1)) + (dy * (y0 - y1))) / ((dx * dx + dy * dy) || 0);
+  var x, y;
+  if (along <= 0) {
+    x = x1;
+    y = y1;
+  } else if (along >= 1) {
+    x = x2;
+    y = y2;
+  } else {
+    x = x1 + along * dx;
+    y = y1 + along * dy;
+  }
+  return [x, y];
+};
+
+
+/**
+ * Returns a {@link ol.CoordinateFormatType} function that can be used to format
+ * a {ol.Coordinate} to a string.
+ *
+ * Example without specifying the fractional digits:
+ *
+ *     var coord = [7.85, 47.983333];
+ *     var stringifyFunc = ol.coordinate.createStringXY();
+ *     var out = stringifyFunc(coord);
+ *     // out is now '8, 48'
+ *
+ * Example with explicitly specifying 2 fractional digits:
+ *
+ *     var coord = [7.85, 47.983333];
+ *     var stringifyFunc = ol.coordinate.createStringXY(2);
+ *     var out = stringifyFunc(coord);
+ *     // out is now '7.85, 47.98'
+ *
+ * @param {number=} opt_fractionDigits The number of digits to include
+ *    after the decimal point. Default is `0`.
+ * @return {ol.CoordinateFormatType} Coordinate format.
+ * @api stable
+ */
+ol.coordinate.createStringXY = function(opt_fractionDigits) {
+  return (
+      /**
+       * @param {ol.Coordinate|undefined} coordinate Coordinate.
+       * @return {string} String XY.
+       */
+      function(coordinate) {
+        return ol.coordinate.toStringXY(coordinate, opt_fractionDigits);
+      });
+};
+
+
+/**
+ * @private
+ * @param {number} degrees Degrees.
+ * @param {string} hemispheres Hemispheres.
+ * @param {number=} opt_fractionDigits The number of digits to include
+ *    after the decimal point. Default is `0`.
+ * @return {string} String.
+ */
+ol.coordinate.degreesToStringHDMS_ = function(degrees, hemispheres, opt_fractionDigits) {
+  var normalizedDegrees = ol.math.modulo(degrees + 180, 360) - 180;
+  var x = Math.abs(3600 * normalizedDegrees);
+  var dflPrecision = opt_fractionDigits || 0;
+  return Math.floor(x / 3600) + '\u00b0 ' +
+      ol.string.padNumber(Math.floor((x / 60) % 60), 2) + '\u2032 ' +
+      ol.string.padNumber((x % 60), 2, dflPrecision) + '\u2033 ' +
+      hemispheres.charAt(normalizedDegrees < 0 ? 1 : 0);
+};
+
+
+/**
+ * Transforms the given {@link ol.Coordinate} to a string using the given string
+ * template. The strings `{x}` and `{y}` in the template will be replaced with
+ * the first and second coordinate values respectively.
+ *
+ * Example without specifying the fractional digits:
+ *
+ *     var coord = [7.85, 47.983333];
+ *     var template = 'Coordinate is ({x}|{y}).';
+ *     var out = ol.coordinate.format(coord, template);
+ *     // out is now 'Coordinate is (8|48).'
+ *
+ * Example explicitly specifying the fractional digits:
+ *
+ *     var coord = [7.85, 47.983333];
+ *     var template = 'Coordinate is ({x}|{y}).';
+ *     var out = ol.coordinate.format(coord, template, 2);
+ *     // out is now 'Coordinate is (7.85|47.98).'
+ *
+ * @param {ol.Coordinate|undefined} coordinate Coordinate.
+ * @param {string} template A template string with `{x}` and `{y}` placeholders
+ *     that will be replaced by first and second coordinate values.
+ * @param {number=} opt_fractionDigits The number of digits to include
+ *    after the decimal point. Default is `0`.
+ * @return {string} Formatted coordinate.
+ * @api stable
+ */
+ol.coordinate.format = function(coordinate, template, opt_fractionDigits) {
+  if (coordinate) {
+    return template
+      .replace('{x}', coordinate[0].toFixed(opt_fractionDigits))
+      .replace('{y}', coordinate[1].toFixed(opt_fractionDigits));
+  } else {
+    return '';
+  }
+};
+
+
+/**
+ * @param {ol.Coordinate} coordinate1 First coordinate.
+ * @param {ol.Coordinate} coordinate2 Second coordinate.
+ * @return {boolean} Whether the passed coordinates are equal.
+ */
+ol.coordinate.equals = function(coordinate1, coordinate2) {
+  var equals = true;
+  for (var i = coordinate1.length - 1; i >= 0; --i) {
+    if (coordinate1[i] != coordinate2[i]) {
+      equals = false;
+      break;
+    }
+  }
+  return equals;
+};
+
+
+/**
+ * Rotate `coordinate` by `angle`. `coordinate` is modified in place and
+ * returned by the function.
+ *
+ * Example:
+ *
+ *     var coord = [7.85, 47.983333];
+ *     var rotateRadians = Math.PI / 2; // 90 degrees
+ *     ol.coordinate.rotate(coord, rotateRadians);
+ *     // coord is now [-47.983333, 7.85]
+ *
+ * @param {ol.Coordinate} coordinate Coordinate.
+ * @param {number} angle Angle in radian.
+ * @return {ol.Coordinate} Coordinate.
+ * @api stable
+ */
+ol.coordinate.rotate = function(coordinate, angle) {
+  var cosAngle = Math.cos(angle);
+  var sinAngle = Math.sin(angle);
+  var x = coordinate[0] * cosAngle - coordinate[1] * sinAngle;
+  var y = coordinate[1] * cosAngle + coordinate[0] * sinAngle;
+  coordinate[0] = x;
+  coordinate[1] = y;
+  return coordinate;
+};
+
+
+/**
+ * Scale `coordinate` by `scale`. `coordinate` is modified in place and returned
+ * by the function.
+ *
+ * Example:
+ *
+ *     var coord = [7.85, 47.983333];
+ *     var scale = 1.2;
+ *     ol.coordinate.scale(coord, scale);
+ *     // coord is now [9.42, 57.5799996]
+ *
+ * @param {ol.Coordinate} coordinate Coordinate.
+ * @param {number} scale Scale factor.
+ * @return {ol.Coordinate} Coordinate.
+ */
+ol.coordinate.scale = function(coordinate, scale) {
+  coordinate[0] *= scale;
+  coordinate[1] *= scale;
+  return coordinate;
+};
+
+
+/**
+ * Subtract `delta` to `coordinate`. `coordinate` is modified in place and
+ * returned by the function.
+ *
+ * @param {ol.Coordinate} coordinate Coordinate.
+ * @param {ol.Coordinate} delta Delta.
+ * @return {ol.Coordinate} Coordinate.
+ */
+ol.coordinate.sub = function(coordinate, delta) {
+  coordinate[0] -= delta[0];
+  coordinate[1] -= delta[1];
+  return coordinate;
+};
+
+
+/**
+ * @param {ol.Coordinate} coord1 First coordinate.
+ * @param {ol.Coordinate} coord2 Second coordinate.
+ * @return {number} Squared distance between coord1 and coord2.
+ */
+ol.coordinate.squaredDistance = function(coord1, coord2) {
+  var dx = coord1[0] - coord2[0];
+  var dy = coord1[1] - coord2[1];
+  return dx * dx + dy * dy;
+};
+
+
+/**
+ * Calculate the squared distance from a coordinate to a line segment.
+ *
+ * @param {ol.Coordinate} coordinate Coordinate of the point.
+ * @param {Array.<ol.Coordinate>} segment Line segment (2 coordinates).
+ * @return {number} Squared distance from the point to the line segment.
+ */
+ol.coordinate.squaredDistanceToSegment = function(coordinate, segment) {
+  return ol.coordinate.squaredDistance(coordinate,
+      ol.coordinate.closestOnSegment(coordinate, segment));
+};
+
+
+/**
+ * Format a geographic coordinate with the hemisphere, degrees, minutes, and
+ * seconds.
+ *
+ * Example without specifying fractional digits:
+ *
+ *     var coord = [7.85, 47.983333];
+ *     var out = ol.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 = ol.coordinate.toStringHDMS(coord, 1);
+ *     // out is now '47° 58′ 60.0″ N 7° 50′ 60.0″ E'
+ *
+ * @param {ol.Coordinate|undefined} coordinate Coordinate.
+ * @param {number=} opt_fractionDigits The number of digits to include
+ *    after the decimal point. Default is `0`.
+ * @return {string} Hemisphere, degrees, minutes and seconds.
+ * @api stable
+ */
+ol.coordinate.toStringHDMS = function(coordinate, opt_fractionDigits) {
+  if (coordinate) {
+    return ol.coordinate.degreesToStringHDMS_(coordinate[1], 'NS', opt_fractionDigits) + ' ' +
+        ol.coordinate.degreesToStringHDMS_(coordinate[0], 'EW', 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 = ol.coordinate.toStringXY(coord);
+ *     // out is now '8, 48'
+ *
+ * Example explicitly specifying 1 fractional digit:
+ *
+ *     var coord = [7.85, 47.983333];
+ *     var out = ol.coordinate.toStringXY(coord, 1);
+ *     // out is now '7.8, 48.0'
+ *
+ * @param {ol.Coordinate|undefined} coordinate Coordinate.
+ * @param {number=} opt_fractionDigits The number of digits to include
+ *    after the decimal point. Default is `0`.
+ * @return {string} XY.
+ * @api stable
+ */
+ol.coordinate.toStringXY = function(coordinate, opt_fractionDigits) {
+  return ol.coordinate.format(coordinate, '{x}, {y}', opt_fractionDigits);
+};
+
+
+/**
+ * Create an ol.Coordinate from an Array and take into account axis order.
+ *
+ * Examples:
+ *
+ *     var northCoord = ol.coordinate.fromProjectedArray([1, 2], 'n');
+ *     // northCoord is now [2, 1]
+ *
+ *     var eastCoord = ol.coordinate.fromProjectedArray([1, 2], 'e');
+ *     // eastCoord is now [1, 2]
+ *
+ * @param {Array} array The array with coordinates.
+ * @param {string} axis the axis info.
+ * @return {ol.Coordinate} The coordinate created.
+ */
+ol.coordinate.fromProjectedArray = function(array, axis) {
+  var firstAxis = axis.charAt(0);
+  if (firstAxis === 'n' || firstAxis === 's') {
+    return [array[1], array[0]];
+  } else {
+    return array;
+  }
+};
+
+goog.provide('ol.extent');
+goog.provide('ol.extent.Corner');
+goog.provide('ol.extent.Relationship');
+
+goog.require('goog.asserts');
+
+
+/**
+ * Extent corner.
+ * @enum {string}
+ */
+ol.extent.Corner = {
+  BOTTOM_LEFT: 'bottom-left',
+  BOTTOM_RIGHT: 'bottom-right',
+  TOP_LEFT: 'top-left',
+  TOP_RIGHT: 'top-right'
+};
+
+
+/**
+ * Relationship to an extent.
+ * @enum {number}
+ */
+ol.extent.Relationship = {
+  UNKNOWN: 0,
+  INTERSECTING: 1,
+  ABOVE: 2,
+  RIGHT: 4,
+  BELOW: 8,
+  LEFT: 16
+};
+
+
+/**
+ * Build an extent that includes all given coordinates.
+ *
+ * @param {Array.<ol.Coordinate>} coordinates Coordinates.
+ * @return {ol.Extent} Bounding extent.
+ * @api stable
+ */
+ol.extent.boundingExtent = function(coordinates) {
+  var extent = ol.extent.createEmpty();
+  for (var i = 0, ii = coordinates.length; i < ii; ++i) {
+    ol.extent.extendCoordinate(extent, coordinates[i]);
+  }
+  return extent;
+};
+
+
+/**
+ * @param {Array.<number>} xs Xs.
+ * @param {Array.<number>} ys Ys.
+ * @param {ol.Extent=} opt_extent Destination extent.
+ * @private
+ * @return {ol.Extent} Extent.
+ */
+ol.extent.boundingExtentXYs_ = function(xs, ys, opt_extent) {
+  goog.asserts.assert(xs.length > 0, 'xs length should be larger than 0');
+  goog.asserts.assert(ys.length > 0, 'ys length should be larger than 0');
+  var minX = Math.min.apply(null, xs);
+  var minY = Math.min.apply(null, ys);
+  var maxX = Math.max.apply(null, xs);
+  var maxY = Math.max.apply(null, ys);
+  return ol.extent.createOrUpdate(minX, minY, maxX, maxY, opt_extent);
+};
+
+
+/**
+ * Return extent increased by the provided value.
+ * @param {ol.Extent} extent Extent.
+ * @param {number} value The amount by which the extent should be buffered.
+ * @param {ol.Extent=} opt_extent Extent.
+ * @return {ol.Extent} Extent.
+ * @api stable
+ */
+ol.extent.buffer = function(extent, value, opt_extent) {
+  if (opt_extent) {
+    opt_extent[0] = extent[0] - value;
+    opt_extent[1] = extent[1] - value;
+    opt_extent[2] = extent[2] + value;
+    opt_extent[3] = extent[3] + value;
+    return opt_extent;
+  } else {
+    return [
+      extent[0] - value,
+      extent[1] - value,
+      extent[2] + value,
+      extent[3] + value
+    ];
+  }
+};
+
+
+/**
+ * Creates a clone of an extent.
+ *
+ * @param {ol.Extent} extent Extent to clone.
+ * @param {ol.Extent=} opt_extent Extent.
+ * @return {ol.Extent} The clone.
+ */
+ol.extent.clone = function(extent, opt_extent) {
+  if (opt_extent) {
+    opt_extent[0] = extent[0];
+    opt_extent[1] = extent[1];
+    opt_extent[2] = extent[2];
+    opt_extent[3] = extent[3];
+    return opt_extent;
+  } else {
+    return extent.slice();
+  }
+};
+
+
+/**
+ * @param {ol.Extent} extent Extent.
+ * @param {number} x X.
+ * @param {number} y Y.
+ * @return {number} Closest squared distance.
+ */
+ol.extent.closestSquaredDistanceXY = function(extent, x, y) {
+  var dx, dy;
+  if (x < extent[0]) {
+    dx = extent[0] - x;
+  } else if (extent[2] < x) {
+    dx = x - extent[2];
+  } else {
+    dx = 0;
+  }
+  if (y < extent[1]) {
+    dy = extent[1] - y;
+  } else if (extent[3] < y) {
+    dy = y - extent[3];
+  } else {
+    dy = 0;
+  }
+  return dx * dx + dy * dy;
+};
+
+
+/**
+ * Check if the passed coordinate is contained or on the edge of the extent.
+ *
+ * @param {ol.Extent} extent Extent.
+ * @param {ol.Coordinate} coordinate Coordinate.
+ * @return {boolean} The coordinate is contained in the extent.
+ * @api stable
+ */
+ol.extent.containsCoordinate = function(extent, coordinate) {
+  return ol.extent.containsXY(extent, coordinate[0], coordinate[1]);
+};
+
+
+/**
+ * Check if one extent contains another.
+ *
+ * An extent is deemed contained if it lies completely within the other extent,
+ * including if they share one or more edges.
+ *
+ * @param {ol.Extent} extent1 Extent 1.
+ * @param {ol.Extent} extent2 Extent 2.
+ * @return {boolean} The second extent is contained by or on the edge of the
+ *     first.
+ * @api stable
+ */
+ol.extent.containsExtent = function(extent1, extent2) {
+  return extent1[0] <= extent2[0] && extent2[2] <= extent1[2] &&
+      extent1[1] <= extent2[1] && extent2[3] <= extent1[3];
+};
+
+
+/**
+ * Check if the passed coordinate is contained or on the edge of the extent.
+ *
+ * @param {ol.Extent} extent Extent.
+ * @param {number} x X coordinate.
+ * @param {number} y Y coordinate.
+ * @return {boolean} The x, y values are contained in the extent.
+ * @api stable
+ */
+ol.extent.containsXY = function(extent, x, y) {
+  return extent[0] <= x && x <= extent[2] && extent[1] <= y && y <= extent[3];
+};
+
+
+/**
+ * Get the relationship between a coordinate and extent.
+ * @param {ol.Extent} extent The extent.
+ * @param {ol.Coordinate} coordinate The coordinate.
+ * @return {number} The relationship (bitwise compare with
+ *     ol.extent.Relationship).
+ */
+ol.extent.coordinateRelationship = function(extent, coordinate) {
+  var minX = extent[0];
+  var minY = extent[1];
+  var maxX = extent[2];
+  var maxY = extent[3];
+  var x = coordinate[0];
+  var y = coordinate[1];
+  var relationship = ol.extent.Relationship.UNKNOWN;
+  if (x < minX) {
+    relationship = relationship | ol.extent.Relationship.LEFT;
+  } else if (x > maxX) {
+    relationship = relationship | ol.extent.Relationship.RIGHT;
+  }
+  if (y < minY) {
+    relationship = relationship | ol.extent.Relationship.BELOW;
+  } else if (y > maxY) {
+    relationship = relationship | ol.extent.Relationship.ABOVE;
+  }
+  if (relationship === ol.extent.Relationship.UNKNOWN) {
+    relationship = ol.extent.Relationship.INTERSECTING;
+  }
+  return relationship;
+};
+
+
+/**
+ * Create an empty extent.
+ * @return {ol.Extent} Empty extent.
+ * @api stable
+ */
+ol.extent.createEmpty = function() {
+  return [Infinity, Infinity, -Infinity, -Infinity];
+};
+
+
+/**
+ * Create a new extent or update the provided extent.
+ * @param {number} minX Minimum X.
+ * @param {number} minY Minimum Y.
+ * @param {number} maxX Maximum X.
+ * @param {number} maxY Maximum Y.
+ * @param {ol.Extent=} opt_extent Destination extent.
+ * @return {ol.Extent} Extent.
+ */
+ol.extent.createOrUpdate = function(minX, minY, maxX, maxY, opt_extent) {
+  if (opt_extent) {
+    opt_extent[0] = minX;
+    opt_extent[1] = minY;
+    opt_extent[2] = maxX;
+    opt_extent[3] = maxY;
+    return opt_extent;
+  } else {
+    return [minX, minY, maxX, maxY];
+  }
+};
+
+
+/**
+ * Create a new empty extent or make the provided one empty.
+ * @param {ol.Extent=} opt_extent Extent.
+ * @return {ol.Extent} Extent.
+ */
+ol.extent.createOrUpdateEmpty = function(opt_extent) {
+  return ol.extent.createOrUpdate(
+      Infinity, Infinity, -Infinity, -Infinity, opt_extent);
+};
+
+
+/**
+ * @param {ol.Coordinate} coordinate Coordinate.
+ * @param {ol.Extent=} opt_extent Extent.
+ * @return {ol.Extent} Extent.
+ */
+ol.extent.createOrUpdateFromCoordinate = function(coordinate, opt_extent) {
+  var x = coordinate[0];
+  var y = coordinate[1];
+  return ol.extent.createOrUpdate(x, y, x, y, opt_extent);
+};
+
+
+/**
+ * @param {Array.<ol.Coordinate>} coordinates Coordinates.
+ * @param {ol.Extent=} opt_extent Extent.
+ * @return {ol.Extent} Extent.
+ */
+ol.extent.createOrUpdateFromCoordinates = function(coordinates, opt_extent) {
+  var extent = ol.extent.createOrUpdateEmpty(opt_extent);
+  return ol.extent.extendCoordinates(extent, coordinates);
+};
+
+
+/**
+ * @param {Array.<number>} flatCoordinates Flat coordinates.
+ * @param {number} offset Offset.
+ * @param {number} end End.
+ * @param {number} stride Stride.
+ * @param {ol.Extent=} opt_extent Extent.
+ * @return {ol.Extent} Extent.
+ */
+ol.extent.createOrUpdateFromFlatCoordinates = function(flatCoordinates, offset, end, stride, opt_extent) {
+  var extent = ol.extent.createOrUpdateEmpty(opt_extent);
+  return ol.extent.extendFlatCoordinates(
+      extent, flatCoordinates, offset, end, stride);
+};
+
+
+/**
+ * @param {Array.<Array.<ol.Coordinate>>} rings Rings.
+ * @param {ol.Extent=} opt_extent Extent.
+ * @return {ol.Extent} Extent.
+ */
+ol.extent.createOrUpdateFromRings = function(rings, opt_extent) {
+  var extent = ol.extent.createOrUpdateEmpty(opt_extent);
+  return ol.extent.extendRings(extent, rings);
+};
+
+
+/**
+ * Empty an extent in place.
+ * @param {ol.Extent} extent Extent.
+ * @return {ol.Extent} Extent.
+ */
+ol.extent.empty = function(extent) {
+  extent[0] = extent[1] = Infinity;
+  extent[2] = extent[3] = -Infinity;
+  return extent;
+};
+
+
+/**
+ * Determine if two extents are equivalent.
+ * @param {ol.Extent} extent1 Extent 1.
+ * @param {ol.Extent} extent2 Extent 2.
+ * @return {boolean} The two extents are equivalent.
+ * @api stable
+ */
+ol.extent.equals = function(extent1, extent2) {
+  return extent1[0] == extent2[0] && extent1[2] == extent2[2] &&
+      extent1[1] == extent2[1] && extent1[3] == extent2[3];
+};
+
+
+/**
+ * Modify an extent to include another extent.
+ * @param {ol.Extent} extent1 The extent to be modified.
+ * @param {ol.Extent} extent2 The extent that will be included in the first.
+ * @return {ol.Extent} A reference to the first (extended) extent.
+ * @api stable
+ */
+ol.extent.extend = function(extent1, extent2) {
+  if (extent2[0] < extent1[0]) {
+    extent1[0] = extent2[0];
+  }
+  if (extent2[2] > extent1[2]) {
+    extent1[2] = extent2[2];
+  }
+  if (extent2[1] < extent1[1]) {
+    extent1[1] = extent2[1];
+  }
+  if (extent2[3] > extent1[3]) {
+    extent1[3] = extent2[3];
+  }
+  return extent1;
+};
+
+
+/**
+ * @param {ol.Extent} extent Extent.
+ * @param {ol.Coordinate} coordinate Coordinate.
+ */
+ol.extent.extendCoordinate = function(extent, coordinate) {
+  if (coordinate[0] < extent[0]) {
+    extent[0] = coordinate[0];
+  }
+  if (coordinate[0] > extent[2]) {
+    extent[2] = coordinate[0];
+  }
+  if (coordinate[1] < extent[1]) {
+    extent[1] = coordinate[1];
+  }
+  if (coordinate[1] > extent[3]) {
+    extent[3] = coordinate[1];
+  }
+};
+
+
+/**
+ * @param {ol.Extent} extent Extent.
+ * @param {Array.<ol.Coordinate>} coordinates Coordinates.
+ * @return {ol.Extent} Extent.
+ */
+ol.extent.extendCoordinates = function(extent, coordinates) {
+  var i, ii;
+  for (i = 0, ii = coordinates.length; i < ii; ++i) {
+    ol.extent.extendCoordinate(extent, coordinates[i]);
+  }
+  return extent;
+};
+
+
+/**
+ * @param {ol.Extent} extent Extent.
+ * @param {Array.<number>} flatCoordinates Flat coordinates.
+ * @param {number} offset Offset.
+ * @param {number} end End.
+ * @param {number} stride Stride.
+ * @return {ol.Extent} Extent.
+ */
+ol.extent.extendFlatCoordinates = function(extent, flatCoordinates, offset, end, stride) {
+  for (; offset < end; offset += stride) {
+    ol.extent.extendXY(
+        extent, flatCoordinates[offset], flatCoordinates[offset + 1]);
+  }
+  return extent;
+};
+
+
+/**
+ * @param {ol.Extent} extent Extent.
+ * @param {Array.<Array.<ol.Coordinate>>} rings Rings.
+ * @return {ol.Extent} Extent.
+ */
+ol.extent.extendRings = function(extent, rings) {
+  var i, ii;
+  for (i = 0, ii = rings.length; i < ii; ++i) {
+    ol.extent.extendCoordinates(extent, rings[i]);
+  }
+  return extent;
+};
+
+
+/**
+ * @param {ol.Extent} extent Extent.
+ * @param {number} x X.
+ * @param {number} y Y.
+ */
+ol.extent.extendXY = function(extent, x, y) {
+  extent[0] = Math.min(extent[0], x);
+  extent[1] = Math.min(extent[1], y);
+  extent[2] = Math.max(extent[2], x);
+  extent[3] = Math.max(extent[3], y);
+};
+
+
+/**
+ * This function calls `callback` for each corner of the extent. If the
+ * callback returns a truthy value the function returns that value
+ * immediately. Otherwise the function returns `false`.
+ * @param {ol.Extent} extent Extent.
+ * @param {function(this:T, ol.Coordinate): S} callback Callback.
+ * @param {T=} opt_this Value to use as `this` when executing `callback`.
+ * @return {S|boolean} Value.
+ * @template S, T
+ */
+ol.extent.forEachCorner = function(extent, callback, opt_this) {
+  var val;
+  val = callback.call(opt_this, ol.extent.getBottomLeft(extent));
+  if (val) {
+    return val;
+  }
+  val = callback.call(opt_this, ol.extent.getBottomRight(extent));
+  if (val) {
+    return val;
+  }
+  val = callback.call(opt_this, ol.extent.getTopRight(extent));
+  if (val) {
+    return val;
+  }
+  val = callback.call(opt_this, ol.extent.getTopLeft(extent));
+  if (val) {
+    return val;
+  }
+  return false;
+};
+
+
+/**
+ * @param {ol.Extent} extent Extent.
+ * @return {number} Area.
+ */
+ol.extent.getArea = function(extent) {
+  var area = 0;
+  if (!ol.extent.isEmpty(extent)) {
+    area = ol.extent.getWidth(extent) * ol.extent.getHeight(extent);
+  }
+  return area;
+};
+
+
+/**
+ * Get the bottom left coordinate of an extent.
+ * @param {ol.Extent} extent Extent.
+ * @return {ol.Coordinate} Bottom left coordinate.
+ * @api stable
+ */
+ol.extent.getBottomLeft = function(extent) {
+  return [extent[0], extent[1]];
+};
+
+
+/**
+ * Get the bottom right coordinate of an extent.
+ * @param {ol.Extent} extent Extent.
+ * @return {ol.Coordinate} Bottom right coordinate.
+ * @api stable
+ */
+ol.extent.getBottomRight = function(extent) {
+  return [extent[2], extent[1]];
+};
+
+
+/**
+ * Get the center coordinate of an extent.
+ * @param {ol.Extent} extent Extent.
+ * @return {ol.Coordinate} Center.
+ * @api stable
+ */
+ol.extent.getCenter = function(extent) {
+  return [(extent[0] + extent[2]) / 2, (extent[1] + extent[3]) / 2];
+};
+
+
+/**
+ * Get a corner coordinate of an extent.
+ * @param {ol.Extent} extent Extent.
+ * @param {ol.extent.Corner} corner Corner.
+ * @return {ol.Coordinate} Corner coordinate.
+ */
+ol.extent.getCorner = function(extent, corner) {
+  var coordinate;
+  if (corner === ol.extent.Corner.BOTTOM_LEFT) {
+    coordinate = ol.extent.getBottomLeft(extent);
+  } else if (corner === ol.extent.Corner.BOTTOM_RIGHT) {
+    coordinate = ol.extent.getBottomRight(extent);
+  } else if (corner === ol.extent.Corner.TOP_LEFT) {
+    coordinate = ol.extent.getTopLeft(extent);
+  } else if (corner === ol.extent.Corner.TOP_RIGHT) {
+    coordinate = ol.extent.getTopRight(extent);
+  } else {
+    goog.asserts.fail('Invalid corner: %s', corner);
+  }
+  goog.asserts.assert(coordinate, 'coordinate should be defined');
+  return coordinate;
+};
+
+
+/**
+ * @param {ol.Extent} extent1 Extent 1.
+ * @param {ol.Extent} extent2 Extent 2.
+ * @return {number} Enlarged area.
+ */
+ol.extent.getEnlargedArea = function(extent1, extent2) {
+  var minX = Math.min(extent1[0], extent2[0]);
+  var minY = Math.min(extent1[1], extent2[1]);
+  var maxX = Math.max(extent1[2], extent2[2]);
+  var maxY = Math.max(extent1[3], extent2[3]);
+  return (maxX - minX) * (maxY - minY);
+};
+
+
+/**
+ * @param {ol.Coordinate} center Center.
+ * @param {number} resolution Resolution.
+ * @param {number} rotation Rotation.
+ * @param {ol.Size} size Size.
+ * @param {ol.Extent=} opt_extent Destination extent.
+ * @return {ol.Extent} Extent.
+ */
+ol.extent.getForViewAndSize = function(center, resolution, rotation, size, opt_extent) {
+  var dx = resolution * size[0] / 2;
+  var dy = resolution * size[1] / 2;
+  var cosRotation = Math.cos(rotation);
+  var sinRotation = Math.sin(rotation);
+  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 ol.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 {ol.Extent} extent Extent.
+ * @return {number} Height.
+ * @api stable
+ */
+ol.extent.getHeight = function(extent) {
+  return extent[3] - extent[1];
+};
+
+
+/**
+ * @param {ol.Extent} extent1 Extent 1.
+ * @param {ol.Extent} extent2 Extent 2.
+ * @return {number} Intersection area.
+ */
+ol.extent.getIntersectionArea = function(extent1, extent2) {
+  var intersection = ol.extent.getIntersection(extent1, extent2);
+  return ol.extent.getArea(intersection);
+};
+
+
+/**
+ * Get the intersection of two extents.
+ * @param {ol.Extent} extent1 Extent 1.
+ * @param {ol.Extent} extent2 Extent 2.
+ * @param {ol.Extent=} opt_extent Optional extent to populate with intersection.
+ * @return {ol.Extent} Intersecting extent.
+ * @api stable
+ */
+ol.extent.getIntersection = function(extent1, extent2, opt_extent) {
+  var intersection = opt_extent ? opt_extent : ol.extent.createEmpty();
+  if (ol.extent.intersects(extent1, extent2)) {
+    if (extent1[0] > extent2[0]) {
+      intersection[0] = extent1[0];
+    } else {
+      intersection[0] = extent2[0];
+    }
+    if (extent1[1] > extent2[1]) {
+      intersection[1] = extent1[1];
+    } else {
+      intersection[1] = extent2[1];
+    }
+    if (extent1[2] < extent2[2]) {
+      intersection[2] = extent1[2];
+    } else {
+      intersection[2] = extent2[2];
+    }
+    if (extent1[3] < extent2[3]) {
+      intersection[3] = extent1[3];
+    } else {
+      intersection[3] = extent2[3];
+    }
+  }
+  return intersection;
+};
+
+
+/**
+ * @param {ol.Extent} extent Extent.
+ * @return {number} Margin.
+ */
+ol.extent.getMargin = function(extent) {
+  return ol.extent.getWidth(extent) + ol.extent.getHeight(extent);
+};
+
+
+/**
+ * Get the size (width, height) of an extent.
+ * @param {ol.Extent} extent The extent.
+ * @return {ol.Size} The extent size.
+ * @api stable
+ */
+ol.extent.getSize = function(extent) {
+  return [extent[2] - extent[0], extent[3] - extent[1]];
+};
+
+
+/**
+ * Get the top left coordinate of an extent.
+ * @param {ol.Extent} extent Extent.
+ * @return {ol.Coordinate} Top left coordinate.
+ * @api stable
+ */
+ol.extent.getTopLeft = function(extent) {
+  return [extent[0], extent[3]];
+};
+
+
+/**
+ * Get the top right coordinate of an extent.
+ * @param {ol.Extent} extent Extent.
+ * @return {ol.Coordinate} Top right coordinate.
+ * @api stable
+ */
+ol.extent.getTopRight = function(extent) {
+  return [extent[2], extent[3]];
+};
+
+
+/**
+ * Get the width of an extent.
+ * @param {ol.Extent} extent Extent.
+ * @return {number} Width.
+ * @api stable
+ */
+ol.extent.getWidth = function(extent) {
+  return extent[2] - extent[0];
+};
+
+
+/**
+ * Determine if one extent intersects another.
+ * @param {ol.Extent} extent1 Extent 1.
+ * @param {ol.Extent} extent2 Extent.
+ * @return {boolean} The two extents intersect.
+ * @api stable
+ */
+ol.extent.intersects = function(extent1, extent2) {
+  return extent1[0] <= extent2[2] &&
+      extent1[2] >= extent2[0] &&
+      extent1[1] <= extent2[3] &&
+      extent1[3] >= extent2[1];
+};
+
+
+/**
+ * Determine if an extent is empty.
+ * @param {ol.Extent} extent Extent.
+ * @return {boolean} Is empty.
+ * @api stable
+ */
+ol.extent.isEmpty = function(extent) {
+  return extent[2] < extent[0] || extent[3] < extent[1];
+};
+
+
+/**
+ * @param {ol.Extent} extent Extent.
+ * @return {boolean} Is infinite.
+ */
+ol.extent.isInfinite = function(extent) {
+  return extent[0] == -Infinity || extent[1] == -Infinity ||
+      extent[2] == Infinity || extent[3] == Infinity;
+};
+
+
+/**
+ * @param {ol.Extent} extent Extent.
+ * @param {ol.Coordinate} coordinate Coordinate.
+ * @return {ol.Coordinate} Coordinate.
+ */
+ol.extent.normalize = function(extent, coordinate) {
+  return [
+    (coordinate[0] - extent[0]) / (extent[2] - extent[0]),
+    (coordinate[1] - extent[1]) / (extent[3] - extent[1])
+  ];
+};
+
+
+/**
+ * @param {ol.Extent} extent Extent.
+ * @param {ol.Extent=} opt_extent Extent.
+ * @return {ol.Extent} Extent.
+ */
+ol.extent.returnOrUpdate = function(extent, opt_extent) {
+  if (opt_extent) {
+    opt_extent[0] = extent[0];
+    opt_extent[1] = extent[1];
+    opt_extent[2] = extent[2];
+    opt_extent[3] = extent[3];
+    return opt_extent;
+  } else {
+    return extent;
+  }
+};
+
+
+/**
+ * @param {ol.Extent} extent Extent.
+ * @param {number} value Value.
+ */
+ol.extent.scaleFromCenter = function(extent, value) {
+  var deltaX = ((extent[2] - extent[0]) / 2) * (value - 1);
+  var deltaY = ((extent[3] - extent[1]) / 2) * (value - 1);
+  extent[0] -= deltaX;
+  extent[2] += deltaX;
+  extent[1] -= deltaY;
+  extent[3] += deltaY;
+};
+
+
+/**
+ * Determine if the segment between two coordinates intersects (crosses,
+ * touches, or is contained by) the provided extent.
+ * @param {ol.Extent} extent The extent.
+ * @param {ol.Coordinate} start Segment start coordinate.
+ * @param {ol.Coordinate} end Segment end coordinate.
+ * @return {boolean} The segment intersects the extent.
+ */
+ol.extent.intersectsSegment = function(extent, start, end) {
+  var intersects = false;
+  var startRel = ol.extent.coordinateRelationship(extent, start);
+  var endRel = ol.extent.coordinateRelationship(extent, end);
+  if (startRel === ol.extent.Relationship.INTERSECTING ||
+      endRel === ol.extent.Relationship.INTERSECTING) {
+    intersects = true;
+  } else {
+    var minX = extent[0];
+    var minY = extent[1];
+    var maxX = extent[2];
+    var maxY = extent[3];
+    var startX = start[0];
+    var startY = start[1];
+    var endX = end[0];
+    var endY = end[1];
+    var slope = (endY - startY) / (endX - startX);
+    var x, y;
+    if (!!(endRel & ol.extent.Relationship.ABOVE) &&
+        !(startRel & ol.extent.Relationship.ABOVE)) {
+      // potentially intersects top
+      x = endX - ((endY - maxY) / slope);
+      intersects = x >= minX && x <= maxX;
+    }
+    if (!intersects && !!(endRel & ol.extent.Relationship.RIGHT) &&
+        !(startRel & ol.extent.Relationship.RIGHT)) {
+      // potentially intersects right
+      y = endY - ((endX - maxX) * slope);
+      intersects = y >= minY && y <= maxY;
+    }
+    if (!intersects && !!(endRel & ol.extent.Relationship.BELOW) &&
+        !(startRel & ol.extent.Relationship.BELOW)) {
+      // potentially intersects bottom
+      x = endX - ((endY - minY) / slope);
+      intersects = x >= minX && x <= maxX;
+    }
+    if (!intersects && !!(endRel & ol.extent.Relationship.LEFT) &&
+        !(startRel & ol.extent.Relationship.LEFT)) {
+      // potentially intersects left
+      y = endY - ((endX - minX) * slope);
+      intersects = y >= minY && y <= maxY;
+    }
+
+  }
+  return intersects;
+};
+
+
+/**
+ * @param {ol.Extent} extent1 Extent 1.
+ * @param {ol.Extent} extent2 Extent 2.
+ * @return {boolean} Touches.
+ */
+ol.extent.touches = function(extent1, extent2) {
+  var intersects = ol.extent.intersects(extent1, extent2);
+  return intersects &&
+      (extent1[0] == extent2[2] || extent1[2] == extent2[0] ||
+       extent1[1] == extent2[3] || extent1[3] == extent2[1]);
+};
+
+
+/**
+ * Apply a transform function to the extent.
+ * @param {ol.Extent} extent Extent.
+ * @param {ol.TransformFunction} transformFn Transform function.  Called with
+ * [minX, minY, maxX, maxY] extent coordinates.
+ * @param {ol.Extent=} opt_extent Destination extent.
+ * @return {ol.Extent} Extent.
+ * @api stable
+ */
+ol.extent.applyTransform = function(extent, transformFn, opt_extent) {
+  var coordinates = [
+    extent[0], extent[1],
+    extent[0], extent[3],
+    extent[2], extent[1],
+    extent[2], extent[3]
+  ];
+  transformFn(coordinates, coordinates, 2);
+  var xs = [coordinates[0], coordinates[2], coordinates[4], coordinates[6]];
+  var ys = [coordinates[1], coordinates[3], coordinates[5], coordinates[7]];
+  return ol.extent.boundingExtentXYs_(xs, ys, opt_extent);
+};
+
+goog.provide('ol.functions');
+
+/**
+ * Always returns true.
+ * @returns {boolean} true.
+ */
+ol.functions.TRUE = function() {
+  return true;
+};
+
+/**
+ * Always returns false.
+ * @returns {boolean} false.
+ */
+ol.functions.FALSE = function() {
+  return false;
+};
+
+/**
+ * @license
+ * Latitude/longitude spherical geodesy formulae taken from
+ * http://www.movable-type.co.uk/scripts/latlong.html
+ * Licensed under CC-BY-3.0.
+ */
+
+goog.provide('ol.Sphere');
+
+goog.require('ol.math');
+
+
+/**
+ * @classdesc
+ * Class to create objects that can be used with {@link
+ * ol.geom.Polygon.circular}.
+ *
+ * For example to create a sphere whose radius is equal to the semi-major
+ * axis of the WGS84 ellipsoid:
+ *
+ * ```js
+ * var wgs84Sphere= new ol.Sphere(6378137);
+ * ```
+ *
+ * @constructor
+ * @param {number} radius Radius.
+ * @api
+ */
+ol.Sphere = function(radius) {
+
+  /**
+   * @type {number}
+   */
+  this.radius = radius;
+
+};
+
+
+/**
+ * Returns the geodesic area for a list of coordinates.
+ *
+ * [Reference](http://trs-new.jpl.nasa.gov/dspace/handle/2014/40409)
+ * Robert. G. Chamberlain and William H. Duquette, "Some Algorithms for
+ * Polygons on a Sphere", JPL Publication 07-03, Jet Propulsion
+ * Laboratory, Pasadena, CA, June 2007
+ *
+ * @param {Array.<ol.Coordinate>} coordinates List of coordinates of a linear
+ * ring. If the ring is oriented clockwise, the area will be positive,
+ * otherwise it will be negative.
+ * @return {number} Area.
+ * @api
+ */
+ol.Sphere.prototype.geodesicArea = function(coordinates) {
+  var area = 0, len = coordinates.length;
+  var x1 = coordinates[len - 1][0];
+  var y1 = coordinates[len - 1][1];
+  for (var i = 0; i < len; i++) {
+    var x2 = coordinates[i][0], y2 = coordinates[i][1];
+    area += ol.math.toRadians(x2 - x1) *
+        (2 + Math.sin(ol.math.toRadians(y1)) +
+        Math.sin(ol.math.toRadians(y2)));
+    x1 = x2;
+    y1 = y2;
+  }
+  return area * this.radius * this.radius / 2.0;
+};
+
+
+/**
+ * Returns the distance from c1 to c2 using the haversine formula.
+ *
+ * @param {ol.Coordinate} c1 Coordinate 1.
+ * @param {ol.Coordinate} c2 Coordinate 2.
+ * @return {number} Haversine distance.
+ * @api
+ */
+ol.Sphere.prototype.haversineDistance = function(c1, c2) {
+  var lat1 = ol.math.toRadians(c1[1]);
+  var lat2 = ol.math.toRadians(c2[1]);
+  var deltaLatBy2 = (lat2 - lat1) / 2;
+  var deltaLonBy2 = ol.math.toRadians(c2[0] - c1[0]) / 2;
+  var a = Math.sin(deltaLatBy2) * Math.sin(deltaLatBy2) +
+      Math.sin(deltaLonBy2) * Math.sin(deltaLonBy2) *
+      Math.cos(lat1) * Math.cos(lat2);
+  return 2 * this.radius * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
+};
+
+
+/**
+ * Returns the coordinate at the given distance and bearing from `c1`.
+ *
+ * @param {ol.Coordinate} c1 The origin point (`[lon, lat]` in degrees).
+ * @param {number} distance The great-circle distance between the origin
+ *     point and the target point.
+ * @param {number} bearing The bearing (in radians).
+ * @return {ol.Coordinate} The target point.
+ */
+ol.Sphere.prototype.offset = function(c1, distance, bearing) {
+  var lat1 = ol.math.toRadians(c1[1]);
+  var lon1 = ol.math.toRadians(c1[0]);
+  var dByR = distance / this.radius;
+  var lat = Math.asin(
+      Math.sin(lat1) * Math.cos(dByR) +
+      Math.cos(lat1) * Math.sin(dByR) * Math.cos(bearing));
+  var lon = lon1 + Math.atan2(
+      Math.sin(bearing) * Math.sin(dByR) * Math.cos(lat1),
+      Math.cos(dByR) - Math.sin(lat1) * Math.sin(lat));
+  return [ol.math.toDegrees(lon), ol.math.toDegrees(lat)];
+};
+
+goog.provide('ol.sphere.NORMAL');
+
+goog.require('ol.Sphere');
+
+
+/**
+ * The normal sphere.
+ * @const
+ * @type {ol.Sphere}
+ */
+ol.sphere.NORMAL = new ol.Sphere(6370997);
+
+goog.provide('ol.proj');
+goog.provide('ol.proj.METERS_PER_UNIT');
+goog.provide('ol.proj.Projection');
+goog.provide('ol.proj.Units');
+
+goog.require('goog.asserts');
+goog.require('ol');
+goog.require('ol.extent');
+goog.require('ol.object');
+goog.require('ol.sphere.NORMAL');
+
+
+/**
+ * Projection units: `'degrees'`, `'ft'`, `'m'`, `'pixels'`, `'tile-pixels'` or
+ * `'us-ft'`.
+ * @enum {string}
+ */
+ol.proj.Units = {
+  DEGREES: 'degrees',
+  FEET: 'ft',
+  METERS: 'm',
+  PIXELS: 'pixels',
+  TILE_PIXELS: 'tile-pixels',
+  USFEET: 'us-ft'
+};
+
+
+/**
+ * Meters per unit lookup table.
+ * @const
+ * @type {Object.<ol.proj.Units, number>}
+ * @api stable
+ */
+ol.proj.METERS_PER_UNIT = {};
+ol.proj.METERS_PER_UNIT[ol.proj.Units.DEGREES] =
+    2 * Math.PI * ol.sphere.NORMAL.radius / 360;
+ol.proj.METERS_PER_UNIT[ol.proj.Units.FEET] = 0.3048;
+ol.proj.METERS_PER_UNIT[ol.proj.Units.METERS] = 1;
+ol.proj.METERS_PER_UNIT[ol.proj.Units.USFEET] = 1200 / 3937;
+
+
+/**
+ * @classdesc
+ * Projection definition class. One of these is created for each projection
+ * supported in the application and stored in the {@link ol.proj} namespace.
+ * You can use these in applications, but this is not required, as API params
+ * and options use {@link ol.ProjectionLike} which means the simple string
+ * code will suffice.
+ *
+ * You can use {@link ol.proj.get} to retrieve the object for a particular
+ * projection.
+ *
+ * The library includes definitions for `EPSG:4326` and `EPSG:3857`, together
+ * with the following aliases:
+ * * `EPSG:4326`: CRS:84, urn:ogc:def:crs:EPSG:6.6:4326,
+ *     urn:ogc:def:crs:OGC:1.3:CRS84, urn:ogc:def:crs:OGC:2:84,
+ *     http://www.opengis.net/gml/srs/epsg.xml#4326,
+ *     urn:x-ogc:def:crs:EPSG:4326
+ * * `EPSG:3857`: EPSG:102100, EPSG:102113, EPSG:900913,
+ *     urn:ogc:def:crs:EPSG:6.18:3:3857,
+ *     http://www.opengis.net/gml/srs/epsg.xml#3857
+ *
+ * If you use proj4js, aliases can be added using `proj4.defs()`; see
+ * [documentation](https://github.com/proj4js/proj4js). To set an alternative
+ * namespace for proj4, use {@link ol.proj.setProj4}.
+ *
+ * @constructor
+ * @param {olx.ProjectionOptions} options Projection options.
+ * @struct
+ * @api stable
+ */
+ol.proj.Projection = function(options) {
+
+  /**
+   * @private
+   * @type {string}
+   */
+  this.code_ = options.code;
+
+  /**
+   * @private
+   * @type {ol.proj.Units}
+   */
+  this.units_ = /** @type {ol.proj.Units} */ (options.units);
+
+  /**
+   * @private
+   * @type {ol.Extent}
+   */
+  this.extent_ = options.extent !== undefined ? options.extent : null;
+
+  /**
+   * @private
+   * @type {ol.Extent}
+   */
+  this.worldExtent_ = options.worldExtent !== undefined ?
+      options.worldExtent : null;
+
+  /**
+   * @private
+   * @type {string}
+   */
+  this.axisOrientation_ = options.axisOrientation !== undefined ?
+      options.axisOrientation : 'enu';
+
+  /**
+   * @private
+   * @type {boolean}
+   */
+  this.global_ = options.global !== undefined ? options.global : false;
+
+
+  /**
+   * @private
+   * @type {boolean}
+   */
+  this.canWrapX_ = !!(this.global_ && this.extent_);
+
+  /**
+  * @private
+  * @type {function(number, ol.Coordinate):number}
+  */
+  this.getPointResolutionFunc_ = options.getPointResolution !== undefined ?
+      options.getPointResolution : this.getPointResolution_;
+
+  /**
+   * @private
+   * @type {ol.tilegrid.TileGrid}
+   */
+  this.defaultTileGrid_ = null;
+
+  /**
+   * @private
+   * @type {number|undefined}
+   */
+  this.metersPerUnit_ = options.metersPerUnit;
+
+  var projections = ol.proj.projections_;
+  var code = options.code;
+  goog.asserts.assert(code !== undefined,
+      'Option "code" is required for constructing instance');
+  if (ol.ENABLE_PROJ4JS) {
+    var proj4js = ol.proj.proj4_ || ol.global['proj4'];
+    if (typeof proj4js == 'function' && projections[code] === undefined) {
+      var def = proj4js.defs(code);
+      if (def !== undefined) {
+        if (def.axis !== undefined && options.axisOrientation === undefined) {
+          this.axisOrientation_ = def.axis;
+        }
+        if (options.metersPerUnit === undefined) {
+          this.metersPerUnit_ = def.to_meter;
+        }
+        if (options.units === undefined) {
+          this.units_ = def.units;
+        }
+        var currentCode, currentDef, currentProj, proj4Transform;
+        for (currentCode in projections) {
+          currentDef = proj4js.defs(currentCode);
+          if (currentDef !== undefined) {
+            currentProj = ol.proj.get(currentCode);
+            if (currentDef === def) {
+              ol.proj.addEquivalentProjections([currentProj, this]);
+            } else {
+              proj4Transform = proj4js(currentCode, code);
+              ol.proj.addCoordinateTransforms(currentProj, this,
+                  proj4Transform.forward, proj4Transform.inverse);
+            }
+          }
+        }
+      }
+    }
+  }
+
+};
+
+
+/**
+ * @return {boolean} The projection is suitable for wrapping the x-axis
+ */
+ol.proj.Projection.prototype.canWrapX = function() {
+  return this.canWrapX_;
+};
+
+
+/**
+ * Get the code for this projection, e.g. 'EPSG:4326'.
+ * @return {string} Code.
+ * @api stable
+ */
+ol.proj.Projection.prototype.getCode = function() {
+  return this.code_;
+};
+
+
+/**
+ * Get the validity extent for this projection.
+ * @return {ol.Extent} Extent.
+ * @api stable
+ */
+ol.proj.Projection.prototype.getExtent = function() {
+  return this.extent_;
+};
+
+
+/**
+ * Get the units of this projection.
+ * @return {ol.proj.Units} Units.
+ * @api stable
+ */
+ol.proj.Projection.prototype.getUnits = function() {
+  return this.units_;
+};
+
+
+/**
+ * Get the amount of meters per unit of this projection.  If the projection is
+ * not configured with `metersPerUnit` or a units identifier, the return is
+ * `undefined`.
+ * @return {number|undefined} Meters.
+ * @api stable
+ */
+ol.proj.Projection.prototype.getMetersPerUnit = function() {
+  return this.metersPerUnit_ || ol.proj.METERS_PER_UNIT[this.units_];
+};
+
+
+/**
+ * Get the world extent for this projection.
+ * @return {ol.Extent} Extent.
+ * @api
+ */
+ol.proj.Projection.prototype.getWorldExtent = function() {
+  return this.worldExtent_;
+};
+
+
+/**
+ * Get the axis orientation of this projection.
+ * Example values are:
+ * enu - the default easting, northing, elevation.
+ * neu - northing, easting, up - useful for "lat/long" geographic coordinates,
+ *     or south orientated transverse mercator.
+ * wnu - westing, northing, up - some planetary coordinate systems have
+ *     "west positive" coordinate systems
+ * @return {string} Axis orientation.
+ */
+ol.proj.Projection.prototype.getAxisOrientation = function() {
+  return this.axisOrientation_;
+};
+
+
+/**
+ * Is this projection a global projection which spans the whole world?
+ * @return {boolean} Whether the projection is global.
+ * @api stable
+ */
+ol.proj.Projection.prototype.isGlobal = function() {
+  return this.global_;
+};
+
+
+/**
+* Set if the projection is a global projection which spans the whole world
+* @param {boolean} global Whether the projection is global.
+* @api stable
+*/
+ol.proj.Projection.prototype.setGlobal = function(global) {
+  this.global_ = global;
+  this.canWrapX_ = !!(global && this.extent_);
+};
+
+
+/**
+ * @return {ol.tilegrid.TileGrid} The default tile grid.
+ */
+ol.proj.Projection.prototype.getDefaultTileGrid = function() {
+  return this.defaultTileGrid_;
+};
+
+
+/**
+ * @param {ol.tilegrid.TileGrid} tileGrid The default tile grid.
+ */
+ol.proj.Projection.prototype.setDefaultTileGrid = function(tileGrid) {
+  this.defaultTileGrid_ = tileGrid;
+};
+
+
+/**
+ * Set the validity extent for this projection.
+ * @param {ol.Extent} extent Extent.
+ * @api stable
+ */
+ol.proj.Projection.prototype.setExtent = function(extent) {
+  this.extent_ = extent;
+  this.canWrapX_ = !!(this.global_ && extent);
+};
+
+
+/**
+ * Set the world extent for this projection.
+ * @param {ol.Extent} worldExtent World extent
+ *     [minlon, minlat, maxlon, maxlat].
+ * @api
+ */
+ol.proj.Projection.prototype.setWorldExtent = function(worldExtent) {
+  this.worldExtent_ = worldExtent;
+};
+
+
+/**
+* Set the getPointResolution function for this projection.
+* @param {function(number, ol.Coordinate):number} func Function
+* @api
+*/
+ol.proj.Projection.prototype.setGetPointResolution = function(func) {
+  this.getPointResolutionFunc_ = func;
+};
+
+
+/**
+* Default version.
+* Get the resolution of the point in degrees or distance units.
+* For projections with degrees as the unit this will simply return the
+* provided resolution. For other projections the point resolution is
+* estimated by transforming the 'point' pixel to EPSG:4326,
+* measuring its width and height on the normal sphere,
+* and taking the average of the width and height.
+* @param {number} resolution Nominal resolution in projection units.
+* @param {ol.Coordinate} point Point to find adjusted resolution at.
+* @return {number} Point resolution at point in projection units.
+* @private
+*/
+ol.proj.Projection.prototype.getPointResolution_ = function(resolution, point) {
+  var units = this.getUnits();
+  if (units == ol.proj.Units.DEGREES) {
+    return resolution;
+  } else {
+    // Estimate point resolution by transforming the center pixel to EPSG:4326,
+    // measuring its width and height on the normal sphere, and taking the
+    // average of the width and height.
+    var toEPSG4326 = ol.proj.getTransformFromProjections(
+        this, ol.proj.get('EPSG:4326'));
+    var vertices = [
+      point[0] - resolution / 2, point[1],
+      point[0] + resolution / 2, point[1],
+      point[0], point[1] - resolution / 2,
+      point[0], point[1] + resolution / 2
+    ];
+    vertices = toEPSG4326(vertices, vertices, 2);
+    var width = ol.sphere.NORMAL.haversineDistance(
+        vertices.slice(0, 2), vertices.slice(2, 4));
+    var height = ol.sphere.NORMAL.haversineDistance(
+        vertices.slice(4, 6), vertices.slice(6, 8));
+    var pointResolution = (width + height) / 2;
+    var metersPerUnit = this.getMetersPerUnit();
+    if (metersPerUnit !== undefined) {
+      pointResolution /= metersPerUnit;
+    }
+    return pointResolution;
+  }
+};
+
+
+/**
+ * Get the resolution of the point in degrees or distance units.
+ * For projections with degrees as the unit this will simply return the
+ * provided resolution. The default for other projections is to estimate
+ * the point resolution by transforming the 'point' pixel to EPSG:4326,
+ * measuring its width and height on the normal sphere,
+ * and taking the average of the width and height.
+ * An alternative implementation may be given when constructing a
+ * projection. For many local projections,
+ * such a custom function will return the resolution unchanged.
+ * @param {number} resolution Resolution in projection units.
+ * @param {ol.Coordinate} point Point.
+ * @return {number} Point resolution in projection units.
+ * @api
+ */
+ol.proj.Projection.prototype.getPointResolution = function(resolution, point) {
+  return this.getPointResolutionFunc_(resolution, point);
+};
+
+
+/**
+ * @private
+ * @type {Object.<string, ol.proj.Projection>}
+ */
+ol.proj.projections_ = {};
+
+
+/**
+ * @private
+ * @type {Object.<string, Object.<string, ol.TransformFunction>>}
+ */
+ol.proj.transforms_ = {};
+
+
+/**
+ * @private
+ * @type {proj4}
+ */
+ol.proj.proj4_ = null;
+
+
+if (ol.ENABLE_PROJ4JS) {
+  /**
+   * Register proj4. If not explicitly registered, it will be assumed that
+   * proj4js will be loaded in the global namespace. For example in a
+   * browserify ES6 environment you could use:
+   *
+   *     import ol from 'openlayers';
+   *     import proj4 from 'proj4';
+   *     ol.proj.setProj4(proj4);
+   *
+   * @param {proj4} proj4 Proj4.
+   * @api
+   */
+  ol.proj.setProj4 = function(proj4) {
+    goog.asserts.assert(typeof proj4 == 'function',
+        'proj4 argument should be a function');
+    ol.proj.proj4_ = proj4;
+  };
+}
+
+
+/**
+ * Registers transformation functions that don't alter coordinates. Those allow
+ * to transform between projections with equal meaning.
+ *
+ * @param {Array.<ol.proj.Projection>} projections Projections.
+ * @api
+ */
+ol.proj.addEquivalentProjections = function(projections) {
+  ol.proj.addProjections(projections);
+  projections.forEach(function(source) {
+    projections.forEach(function(destination) {
+      if (source !== destination) {
+        ol.proj.addTransform(source, destination, ol.proj.cloneTransform);
+      }
+    });
+  });
+};
+
+
+/**
+ * Registers transformation functions to convert coordinates in any projection
+ * in projection1 to any projection in projection2.
+ *
+ * @param {Array.<ol.proj.Projection>} projections1 Projections with equal
+ *     meaning.
+ * @param {Array.<ol.proj.Projection>} projections2 Projections with equal
+ *     meaning.
+ * @param {ol.TransformFunction} forwardTransform Transformation from any
+ *   projection in projection1 to any projection in projection2.
+ * @param {ol.TransformFunction} inverseTransform Transform from any projection
+ *   in projection2 to any projection in projection1..
+ */
+ol.proj.addEquivalentTransforms = function(projections1, projections2, forwardTransform, inverseTransform) {
+  projections1.forEach(function(projection1) {
+    projections2.forEach(function(projection2) {
+      ol.proj.addTransform(projection1, projection2, forwardTransform);
+      ol.proj.addTransform(projection2, projection1, inverseTransform);
+    });
+  });
+};
+
+
+/**
+ * Add a Projection object to the list of supported projections that can be
+ * looked up by their code.
+ *
+ * @param {ol.proj.Projection} projection Projection instance.
+ * @api stable
+ */
+ol.proj.addProjection = function(projection) {
+  ol.proj.projections_[projection.getCode()] = projection;
+  ol.proj.addTransform(projection, projection, ol.proj.cloneTransform);
+};
+
+
+/**
+ * @param {Array.<ol.proj.Projection>} projections Projections.
+ */
+ol.proj.addProjections = function(projections) {
+  var addedProjections = [];
+  projections.forEach(function(projection) {
+    addedProjections.push(ol.proj.addProjection(projection));
+  });
+};
+
+
+/**
+ * FIXME empty description for jsdoc
+ */
+ol.proj.clearAllProjections = function() {
+  ol.proj.projections_ = {};
+  ol.proj.transforms_ = {};
+};
+
+
+/**
+ * @param {ol.proj.Projection|string|undefined} projection Projection.
+ * @param {string} defaultCode Default code.
+ * @return {ol.proj.Projection} Projection.
+ */
+ol.proj.createProjection = function(projection, defaultCode) {
+  if (!projection) {
+    return ol.proj.get(defaultCode);
+  } else if (typeof projection === 'string') {
+    return ol.proj.get(projection);
+  } else {
+    goog.asserts.assertInstanceof(projection, ol.proj.Projection,
+        'projection should be an ol.proj.Projection');
+    return projection;
+  }
+};
+
+
+/**
+ * Registers a conversion function to convert coordinates from the source
+ * projection to the destination projection.
+ *
+ * @param {ol.proj.Projection} source Source.
+ * @param {ol.proj.Projection} destination Destination.
+ * @param {ol.TransformFunction} transformFn Transform.
+ */
+ol.proj.addTransform = function(source, destination, transformFn) {
+  var sourceCode = source.getCode();
+  var destinationCode = destination.getCode();
+  var transforms = ol.proj.transforms_;
+  if (!(sourceCode in transforms)) {
+    transforms[sourceCode] = {};
+  }
+  transforms[sourceCode][destinationCode] = transformFn;
+};
+
+
+/**
+ * Registers coordinate transform functions to convert coordinates between the
+ * source projection and the destination projection.
+ * The forward and inverse functions convert coordinate pairs; this function
+ * converts these into the functions used internally which also handle
+ * extents and coordinate arrays.
+ *
+ * @param {ol.ProjectionLike} source Source projection.
+ * @param {ol.ProjectionLike} destination Destination projection.
+ * @param {function(ol.Coordinate): ol.Coordinate} forward The forward transform
+ *     function (that is, from the source projection to the destination
+ *     projection) that takes a {@link ol.Coordinate} as argument and returns
+ *     the transformed {@link ol.Coordinate}.
+ * @param {function(ol.Coordinate): ol.Coordinate} inverse The inverse transform
+ *     function (that is, from the destination projection to the source
+ *     projection) that takes a {@link ol.Coordinate} as argument and returns
+ *     the transformed {@link ol.Coordinate}.
+ * @api stable
+ */
+ol.proj.addCoordinateTransforms = function(source, destination, forward, inverse) {
+  var sourceProj = ol.proj.get(source);
+  var destProj = ol.proj.get(destination);
+  ol.proj.addTransform(sourceProj, destProj,
+      ol.proj.createTransformFromCoordinateTransform(forward));
+  ol.proj.addTransform(destProj, sourceProj,
+      ol.proj.createTransformFromCoordinateTransform(inverse));
+};
+
+
+/**
+ * Creates a {@link ol.TransformFunction} from a simple 2D coordinate transform
+ * function.
+ * @param {function(ol.Coordinate): ol.Coordinate} transform Coordinate
+ *     transform.
+ * @return {ol.TransformFunction} Transform function.
+ */
+ol.proj.createTransformFromCoordinateTransform = function(transform) {
+  return (
+      /**
+       * @param {Array.<number>} input Input.
+       * @param {Array.<number>=} opt_output Output.
+       * @param {number=} opt_dimension Dimension.
+       * @return {Array.<number>} Output.
+       */
+      function(input, opt_output, opt_dimension) {
+        var length = input.length;
+        var dimension = opt_dimension !== undefined ? opt_dimension : 2;
+        var output = opt_output !== undefined ? opt_output : new Array(length);
+        var point, i, j;
+        for (i = 0; i < length; i += dimension) {
+          point = transform([input[i], input[i + 1]]);
+          output[i] = point[0];
+          output[i + 1] = point[1];
+          for (j = dimension - 1; j >= 2; --j) {
+            output[i + j] = input[i + j];
+          }
+        }
+        return output;
+      });
+};
+
+
+/**
+ * Unregisters the conversion function to convert coordinates from the source
+ * projection to the destination projection.  This method is used to clean up
+ * cached transforms during testing.
+ *
+ * @param {ol.proj.Projection} source Source projection.
+ * @param {ol.proj.Projection} destination Destination projection.
+ * @return {ol.TransformFunction} transformFn The unregistered transform.
+ */
+ol.proj.removeTransform = function(source, destination) {
+  var sourceCode = source.getCode();
+  var destinationCode = destination.getCode();
+  var transforms = ol.proj.transforms_;
+  goog.asserts.assert(sourceCode in transforms,
+      'sourceCode should be in transforms');
+  goog.asserts.assert(destinationCode in transforms[sourceCode],
+      'destinationCode should be in transforms of sourceCode');
+  var transform = transforms[sourceCode][destinationCode];
+  delete transforms[sourceCode][destinationCode];
+  if (ol.object.isEmpty(transforms[sourceCode])) {
+    delete transforms[sourceCode];
+  }
+  return transform;
+};
+
+
+/**
+ * Transforms a coordinate from longitude/latitude to a different projection.
+ * @param {ol.Coordinate} coordinate Coordinate as longitude and latitude, i.e.
+ *     an array with longitude as 1st and latitude as 2nd element.
+ * @param {ol.ProjectionLike=} opt_projection Target projection. The
+ *     default is Web Mercator, i.e. 'EPSG:3857'.
+ * @return {ol.Coordinate} Coordinate projected to the target projection.
+ * @api stable
+ */
+ol.proj.fromLonLat = function(coordinate, opt_projection) {
+  return ol.proj.transform(coordinate, 'EPSG:4326',
+      opt_projection !== undefined ? opt_projection : 'EPSG:3857');
+};
+
+
+/**
+ * Transforms a coordinate to longitude/latitude.
+ * @param {ol.Coordinate} coordinate Projected coordinate.
+ * @param {ol.ProjectionLike=} opt_projection Projection of the coordinate.
+ *     The default is Web Mercator, i.e. 'EPSG:3857'.
+ * @return {ol.Coordinate} Coordinate as longitude and latitude, i.e. an array
+ *     with longitude as 1st and latitude as 2nd element.
+ * @api stable
+ */
+ol.proj.toLonLat = function(coordinate, opt_projection) {
+  return ol.proj.transform(coordinate,
+      opt_projection !== undefined ? opt_projection : 'EPSG:3857', 'EPSG:4326');
+};
+
+
+/**
+ * Fetches a Projection object for the code specified.
+ *
+ * @param {ol.ProjectionLike} projectionLike Either a code string which is
+ *     a combination of authority and identifier such as "EPSG:4326", or an
+ *     existing projection object, or undefined.
+ * @return {ol.proj.Projection} Projection object, or null if not in list.
+ * @api stable
+ */
+ol.proj.get = function(projectionLike) {
+  var projection;
+  if (projectionLike instanceof ol.proj.Projection) {
+    projection = projectionLike;
+  } else if (typeof projectionLike === 'string') {
+    var code = projectionLike;
+    projection = ol.proj.projections_[code];
+    if (ol.ENABLE_PROJ4JS) {
+      var proj4js = ol.proj.proj4_ || ol.global['proj4'];
+      if (projection === undefined && typeof proj4js == 'function' &&
+          proj4js.defs(code) !== undefined) {
+        projection = new ol.proj.Projection({code: code});
+        ol.proj.addProjection(projection);
+      }
+    }
+  } else {
+    projection = null;
+  }
+  return projection;
+};
+
+
+/**
+ * Checks if two projections are the same, that is every coordinate in one
+ * projection does represent the same geographic point as the same coordinate in
+ * the other projection.
+ *
+ * @param {ol.proj.Projection} projection1 Projection 1.
+ * @param {ol.proj.Projection} projection2 Projection 2.
+ * @return {boolean} Equivalent.
+ * @api
+ */
+ol.proj.equivalent = function(projection1, projection2) {
+  if (projection1 === projection2) {
+    return true;
+  }
+  var equalUnits = projection1.getUnits() === projection2.getUnits();
+  if (projection1.getCode() === projection2.getCode()) {
+    return equalUnits;
+  } else {
+    var transformFn = ol.proj.getTransformFromProjections(
+        projection1, projection2);
+    return transformFn === ol.proj.cloneTransform && equalUnits;
+  }
+};
+
+
+/**
+ * Given the projection-like objects, searches for a transformation
+ * function to convert a coordinates array from the source projection to the
+ * destination projection.
+ *
+ * @param {ol.ProjectionLike} source Source.
+ * @param {ol.ProjectionLike} destination Destination.
+ * @return {ol.TransformFunction} Transform function.
+ * @api stable
+ */
+ol.proj.getTransform = function(source, destination) {
+  var sourceProjection = ol.proj.get(source);
+  var destinationProjection = ol.proj.get(destination);
+  return ol.proj.getTransformFromProjections(
+      sourceProjection, destinationProjection);
+};
+
+
+/**
+ * Searches in the list of transform functions for the function for converting
+ * coordinates from the source projection to the destination projection.
+ *
+ * @param {ol.proj.Projection} sourceProjection Source Projection object.
+ * @param {ol.proj.Projection} destinationProjection Destination Projection
+ *     object.
+ * @return {ol.TransformFunction} Transform function.
+ */
+ol.proj.getTransformFromProjections = function(sourceProjection, destinationProjection) {
+  var transforms = ol.proj.transforms_;
+  var sourceCode = sourceProjection.getCode();
+  var destinationCode = destinationProjection.getCode();
+  var transform;
+  if (sourceCode in transforms && destinationCode in transforms[sourceCode]) {
+    transform = transforms[sourceCode][destinationCode];
+  }
+  if (transform === undefined) {
+    goog.asserts.assert(transform !== undefined, 'transform should be defined');
+    transform = ol.proj.identityTransform;
+  }
+  return transform;
+};
+
+
+/**
+ * @param {Array.<number>} input Input coordinate array.
+ * @param {Array.<number>=} opt_output Output array of coordinate values.
+ * @param {number=} opt_dimension Dimension.
+ * @return {Array.<number>} Input coordinate array (same array as input).
+ */
+ol.proj.identityTransform = function(input, opt_output, opt_dimension) {
+  if (opt_output !== undefined && input !== opt_output) {
+    // TODO: consider making this a warning instead
+    goog.asserts.fail('This should not be used internally.');
+    for (var i = 0, ii = input.length; i < ii; ++i) {
+      opt_output[i] = input[i];
+    }
+    input = opt_output;
+  }
+  return input;
+};
+
+
+/**
+ * @param {Array.<number>} input Input coordinate array.
+ * @param {Array.<number>=} opt_output Output array of coordinate values.
+ * @param {number=} opt_dimension Dimension.
+ * @return {Array.<number>} Output coordinate array (new array, same coordinate
+ *     values).
+ */
+ol.proj.cloneTransform = function(input, opt_output, opt_dimension) {
+  var output;
+  if (opt_output !== undefined) {
+    for (var i = 0, ii = input.length; i < ii; ++i) {
+      opt_output[i] = input[i];
+    }
+    output = opt_output;
+  } else {
+    output = input.slice();
+  }
+  return output;
+};
+
+
+/**
+ * Transforms a coordinate from source projection to destination projection.
+ * This returns a new coordinate (and does not modify the original).
+ *
+ * See {@link ol.proj.transformExtent} for extent transformation.
+ * See the transform method of {@link ol.geom.Geometry} and its subclasses for
+ * geometry transforms.
+ *
+ * @param {ol.Coordinate} coordinate Coordinate.
+ * @param {ol.ProjectionLike} source Source projection-like.
+ * @param {ol.ProjectionLike} destination Destination projection-like.
+ * @return {ol.Coordinate} Coordinate.
+ * @api stable
+ */
+ol.proj.transform = function(coordinate, source, destination) {
+  var transformFn = ol.proj.getTransform(source, destination);
+  return transformFn(coordinate, undefined, coordinate.length);
+};
+
+
+/**
+ * Transforms an extent from source projection to destination projection.  This
+ * returns a new extent (and does not modify the original).
+ *
+ * @param {ol.Extent} extent The extent to transform.
+ * @param {ol.ProjectionLike} source Source projection-like.
+ * @param {ol.ProjectionLike} destination Destination projection-like.
+ * @return {ol.Extent} The transformed extent.
+ * @api stable
+ */
+ol.proj.transformExtent = function(extent, source, destination) {
+  var transformFn = ol.proj.getTransform(source, destination);
+  return ol.extent.applyTransform(extent, transformFn);
+};
+
+
+/**
+ * Transforms the given point to the destination projection.
+ *
+ * @param {ol.Coordinate} point Point.
+ * @param {ol.proj.Projection} sourceProjection Source projection.
+ * @param {ol.proj.Projection} destinationProjection Destination projection.
+ * @return {ol.Coordinate} Point.
+ */
+ol.proj.transformWithProjections = function(point, sourceProjection, destinationProjection) {
+  var transformFn = ol.proj.getTransformFromProjections(
+      sourceProjection, destinationProjection);
+  return transformFn(point);
+};
+
+goog.provide('ol.geom.Geometry');
+goog.provide('ol.geom.GeometryLayout');
+goog.provide('ol.geom.GeometryType');
+
+goog.require('goog.asserts');
+goog.require('ol.functions');
+goog.require('ol.Object');
+goog.require('ol.extent');
+goog.require('ol.proj');
+goog.require('ol.proj.Units');
+
+
+/**
+ * The geometry type. One of `'Point'`, `'LineString'`, `'LinearRing'`,
+ * `'Polygon'`, `'MultiPoint'`, `'MultiLineString'`, `'MultiPolygon'`,
+ * `'GeometryCollection'`, `'Circle'`.
+ * @enum {string}
+ */
+ol.geom.GeometryType = {
+  POINT: 'Point',
+  LINE_STRING: 'LineString',
+  LINEAR_RING: 'LinearRing',
+  POLYGON: 'Polygon',
+  MULTI_POINT: 'MultiPoint',
+  MULTI_LINE_STRING: 'MultiLineString',
+  MULTI_POLYGON: 'MultiPolygon',
+  GEOMETRY_COLLECTION: 'GeometryCollection',
+  CIRCLE: 'Circle'
+};
+
+
+/**
+ * The coordinate layout for geometries, indicating whether a 3rd or 4th z ('Z')
+ * or measure ('M') coordinate is available. Supported values are `'XY'`,
+ * `'XYZ'`, `'XYM'`, `'XYZM'`.
+ * @enum {string}
+ */
+ol.geom.GeometryLayout = {
+  XY: 'XY',
+  XYZ: 'XYZ',
+  XYM: 'XYM',
+  XYZM: 'XYZM'
+};
+
+
+/**
+ * @classdesc
+ * Abstract base class; normally only used for creating subclasses and not
+ * instantiated in apps.
+ * Base class for vector geometries.
+ *
+ * To get notified of changes to the geometry, register a listener for the
+ * generic `change` event on your geometry instance.
+ *
+ * @constructor
+ * @extends {ol.Object}
+ * @api stable
+ */
+ol.geom.Geometry = function() {
+
+  ol.Object.call(this);
+
+  /**
+   * @private
+   * @type {ol.Extent}
+   */
+  this.extent_ = ol.extent.createEmpty();
+
+  /**
+   * @private
+   * @type {number}
+   */
+  this.extentRevision_ = -1;
+
+  /**
+   * @protected
+   * @type {Object.<string, ol.geom.Geometry>}
+   */
+  this.simplifiedGeometryCache = {};
+
+  /**
+   * @protected
+   * @type {number}
+   */
+  this.simplifiedGeometryMaxMinSquaredTolerance = 0;
+
+  /**
+   * @protected
+   * @type {number}
+   */
+  this.simplifiedGeometryRevision = 0;
+
+};
+ol.inherits(ol.geom.Geometry, ol.Object);
+
+
+/**
+ * Make a complete copy of the geometry.
+ * @function
+ * @return {!ol.geom.Geometry} Clone.
+ */
+ol.geom.Geometry.prototype.clone = goog.abstractMethod;
+
+
+/**
+ * @param {number} x X.
+ * @param {number} y Y.
+ * @param {ol.Coordinate} closestPoint Closest point.
+ * @param {number} minSquaredDistance Minimum squared distance.
+ * @return {number} Minimum squared distance.
+ */
+ol.geom.Geometry.prototype.closestPointXY = goog.abstractMethod;
+
+
+/**
+ * Return the closest point of the geometry to the passed point as
+ * {@link ol.Coordinate coordinate}.
+ * @param {ol.Coordinate} point Point.
+ * @param {ol.Coordinate=} opt_closestPoint Closest point.
+ * @return {ol.Coordinate} Closest point.
+ * @api stable
+ */
+ol.geom.Geometry.prototype.getClosestPoint = function(point, opt_closestPoint) {
+  var closestPoint = opt_closestPoint ? opt_closestPoint : [NaN, NaN];
+  this.closestPointXY(point[0], point[1], closestPoint, Infinity);
+  return closestPoint;
+};
+
+
+/**
+ * @param {ol.Coordinate} coordinate Coordinate.
+ * @return {boolean} Contains coordinate.
+ */
+ol.geom.Geometry.prototype.containsCoordinate = function(coordinate) {
+  return this.containsXY(coordinate[0], coordinate[1]);
+};
+
+
+/**
+ * @param {ol.Extent} extent Extent.
+ * @protected
+ * @return {ol.Extent} extent Extent.
+ */
+ol.geom.Geometry.prototype.computeExtent = goog.abstractMethod;
+
+
+/**
+ * @param {number} x X.
+ * @param {number} y Y.
+ * @return {boolean} Contains (x, y).
+ */
+ol.geom.Geometry.prototype.containsXY = ol.functions.FALSE;
+
+
+/**
+ * Get the extent of the geometry.
+ * @param {ol.Extent=} opt_extent Extent.
+ * @return {ol.Extent} extent Extent.
+ * @api stable
+ */
+ol.geom.Geometry.prototype.getExtent = function(opt_extent) {
+  if (this.extentRevision_ != this.getRevision()) {
+    this.extent_ = this.computeExtent(this.extent_);
+    this.extentRevision_ = this.getRevision();
+  }
+  return ol.extent.returnOrUpdate(this.extent_, opt_extent);
+};
+
+
+/**
+ * Rotate the geometry around a given coordinate. This modifies the geometry
+ * coordinates in place.
+ * @param {number} angle Rotation angle in radians.
+ * @param {ol.Coordinate} anchor The rotation center.
+ * @api
+ * @function
+ */
+ol.geom.Geometry.prototype.rotate = goog.abstractMethod;
+
+
+/**
+ * Create a simplified version of this geometry.  For linestrings, this uses
+ * the the {@link
+ * https://en.wikipedia.org/wiki/Ramer-Douglas-Peucker_algorithm
+ * Douglas Peucker} algorithm.  For polygons, a quantization-based
+ * simplification is used to preserve topology.
+ * @function
+ * @param {number} tolerance The tolerance distance for simplification.
+ * @return {ol.geom.Geometry} A new, simplified version of the original
+ *     geometry.
+ * @api
+ */
+ol.geom.Geometry.prototype.simplify = function(tolerance) {
+  return this.getSimplifiedGeometry(tolerance * tolerance);
+};
+
+
+/**
+ * Create a simplified version of this geometry using the Douglas Peucker
+ * algorithm.
+ * @see https://en.wikipedia.org/wiki/Ramer-Douglas-Peucker_algorithm
+ * @function
+ * @param {number} squaredTolerance Squared tolerance.
+ * @return {ol.geom.Geometry} Simplified geometry.
+ */
+ol.geom.Geometry.prototype.getSimplifiedGeometry = goog.abstractMethod;
+
+
+/**
+ * Get the type of this geometry.
+ * @function
+ * @return {ol.geom.GeometryType} Geometry type.
+ */
+ol.geom.Geometry.prototype.getType = goog.abstractMethod;
+
+
+/**
+ * Apply a transform function to each coordinate of the geometry.
+ * The geometry is modified in place.
+ * If you do not want the geometry modified in place, first `clone()` it and
+ * then use this function on the clone.
+ * @function
+ * @param {ol.TransformFunction} transformFn Transform.
+ */
+ol.geom.Geometry.prototype.applyTransform = goog.abstractMethod;
+
+
+/**
+ * Test if the geometry and the passed extent intersect.
+ * @param {ol.Extent} extent Extent.
+ * @return {boolean} `true` if the geometry and the extent intersect.
+ * @function
+ */
+ol.geom.Geometry.prototype.intersectsExtent = goog.abstractMethod;
+
+
+/**
+ * Translate the geometry.  This modifies the geometry coordinates in place.  If
+ * instead you want a new geometry, first `clone()` this geometry.
+ * @param {number} deltaX Delta X.
+ * @param {number} deltaY Delta Y.
+ * @function
+ */
+ol.geom.Geometry.prototype.translate = goog.abstractMethod;
+
+
+/**
+ * Transform each coordinate of the geometry from one coordinate reference
+ * system to another. The geometry is modified in place.
+ * For example, a line will be transformed to a line and a circle to a circle.
+ * If you do not want the geometry modified in place, first `clone()` it and
+ * then use this function on the clone.
+ *
+ * @param {ol.ProjectionLike} source The current projection.  Can be a
+ *     string identifier or a {@link ol.proj.Projection} object.
+ * @param {ol.ProjectionLike} destination The desired projection.  Can be a
+ *     string identifier or a {@link ol.proj.Projection} object.
+ * @return {ol.geom.Geometry} This geometry.  Note that original geometry is
+ *     modified in place.
+ * @api stable
+ */
+ol.geom.Geometry.prototype.transform = function(source, destination) {
+  goog.asserts.assert(
+      ol.proj.get(source).getUnits() !== ol.proj.Units.TILE_PIXELS &&
+      ol.proj.get(destination).getUnits() !== ol.proj.Units.TILE_PIXELS,
+      'cannot transform geometries with TILE_PIXELS units');
+  this.applyTransform(ol.proj.getTransform(source, destination));
+  return this;
+};
+
+// Copyright 2011 The Closure Library Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS-IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+
+/**
+ * @fileoverview Supplies a Float32Array implementation that implements
+ *     most of the Float32Array spec and that can be used when a built-in
+ *     implementation is not available.
+ *
+ *     Note that if no existing Float32Array implementation is found then
+ *     this class and all its public properties are exported as Float32Array.
+ *
+ *     Adding support for the other TypedArray classes here does not make sense
+ *     since this vector math library only needs Float32Array.
+ *
+ */
+goog.provide('goog.vec.Float32Array');
+
+
+
+/**
+ * Constructs a new Float32Array. The new array is initialized to all zeros.
+ *
+ * @param {goog.vec.Float32Array|Array|ArrayBuffer|number} p0
+ *     The length of the array, or an array to initialize the contents of the
+ *     new Float32Array.
+ * @constructor
+ * @implements {IArrayLike<number>}
+ * @final
+ */
+goog.vec.Float32Array = function(p0) {
+  this.length = /** @type {number} */ (p0.length || p0);
+  for (var i = 0; i < this.length; i++) {
+    this[i] = p0[i] || 0;
+  }
+};
+
+
+/**
+ * The number of bytes in an element (as defined by the Typed Array
+ * specification).
+ *
+ * @type {number}
+ */
+goog.vec.Float32Array.BYTES_PER_ELEMENT = 4;
+
+
+/**
+ * The number of bytes in an element (as defined by the Typed Array
+ * specification).
+ *
+ * @type {number}
+ */
+goog.vec.Float32Array.prototype.BYTES_PER_ELEMENT = 4;
+
+
+/**
+ * Sets elements of the array.
+ * @param {Array<number>|Float32Array} values The array of values.
+ * @param {number=} opt_offset The offset in this array to start.
+ */
+goog.vec.Float32Array.prototype.set = function(values, opt_offset) {
+  opt_offset = opt_offset || 0;
+  for (var i = 0; i < values.length && opt_offset + i < this.length; i++) {
+    this[opt_offset + i] = values[i];
+  }
+};
+
+
+/**
+ * Creates a string representation of this array.
+ * @return {string} The string version of this array.
+ * @override
+ */
+goog.vec.Float32Array.prototype.toString = Array.prototype.join;
+
+
+/**
+ * Note that we cannot implement the subarray() or (deprecated) slice()
+ * methods properly since doing so would require being able to overload
+ * the [] operator which is not possible in javascript.  So we leave
+ * them unimplemented.  Any attempt to call these methods will just result
+ * in a javascript error since we leave them undefined.
+ */
+
+
+/**
+ * If no existing Float32Array implementation is found then we export
+ * goog.vec.Float32Array as Float32Array.
+ */
+if (typeof Float32Array == 'undefined') {
+  goog.exportProperty(
+      goog.vec.Float32Array, 'BYTES_PER_ELEMENT',
+      goog.vec.Float32Array.BYTES_PER_ELEMENT);
+  goog.exportProperty(
+      goog.vec.Float32Array.prototype, 'BYTES_PER_ELEMENT',
+      goog.vec.Float32Array.prototype.BYTES_PER_ELEMENT);
+  goog.exportProperty(
+      goog.vec.Float32Array.prototype, 'set',
+      goog.vec.Float32Array.prototype.set);
+  goog.exportProperty(
+      goog.vec.Float32Array.prototype, 'toString',
+      goog.vec.Float32Array.prototype.toString);
+  goog.exportSymbol('Float32Array', goog.vec.Float32Array);
+}
+
+// Copyright 2011 The Closure Library Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS-IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+
+/**
+ * @fileoverview Supplies a Float64Array implementation that implements
+ * most of the Float64Array spec and that can be used when a built-in
+ * implementation is not available.
+ *
+ * Note that if no existing Float64Array implementation is found then this
+ * class and all its public properties are exported as Float64Array.
+ *
+ * Adding support for the other TypedArray classes here does not make sense
+ * since this vector math library only needs Float32Array and Float64Array.
+ *
+ */
+goog.provide('goog.vec.Float64Array');
+
+
+
+/**
+ * Constructs a new Float64Array. The new array is initialized to all zeros.
+ *
+ * @param {goog.vec.Float64Array|Array|ArrayBuffer|number} p0
+ *     The length of the array, or an array to initialize the contents of the
+ *     new Float64Array.
+ * @constructor
+ * @implements {IArrayLike<number>}
+ * @final
+ */
+goog.vec.Float64Array = function(p0) {
+  this.length = /** @type {number} */ (p0.length || p0);
+  for (var i = 0; i < this.length; i++) {
+    this[i] = p0[i] || 0;
+  }
+};
+
+
+/**
+ * The number of bytes in an element (as defined by the Typed Array
+ * specification).
+ *
+ * @type {number}
+ */
+goog.vec.Float64Array.BYTES_PER_ELEMENT = 8;
+
+
+/**
+ * The number of bytes in an element (as defined by the Typed Array
+ * specification).
+ *
+ * @type {number}
+ */
+goog.vec.Float64Array.prototype.BYTES_PER_ELEMENT = 8;
+
+
+/**
+ * Sets elements of the array.
+ * @param {Array<number>|Float64Array} values The array of values.
+ * @param {number=} opt_offset The offset in this array to start.
+ */
+goog.vec.Float64Array.prototype.set = function(values, opt_offset) {
+  opt_offset = opt_offset || 0;
+  for (var i = 0; i < values.length && opt_offset + i < this.length; i++) {
+    this[opt_offset + i] = values[i];
+  }
+};
+
+
+/**
+ * Creates a string representation of this array.
+ * @return {string} The string version of this array.
+ * @override
+ */
+goog.vec.Float64Array.prototype.toString = Array.prototype.join;
+
+
+/**
+ * Note that we cannot implement the subarray() or (deprecated) slice()
+ * methods properly since doing so would require being able to overload
+ * the [] operator which is not possible in javascript.  So we leave
+ * them unimplemented.  Any attempt to call these methods will just result
+ * in a javascript error since we leave them undefined.
+ */
+
+
+/**
+ * If no existing Float64Array implementation is found then we export
+ * goog.vec.Float64Array as Float64Array.
+ */
+if (typeof Float64Array == 'undefined') {
+  try {
+    goog.exportProperty(
+        goog.vec.Float64Array, 'BYTES_PER_ELEMENT',
+        goog.vec.Float64Array.BYTES_PER_ELEMENT);
+  } catch (float64ArrayError) {
+    // Do nothing.  This code is in place to fix b/7225850, in which an error
+    // is incorrectly thrown for Google TV on an old Chrome.
+    // TODO(user): remove after that version is retired.
+  }
+
+  goog.exportProperty(
+      goog.vec.Float64Array.prototype, 'BYTES_PER_ELEMENT',
+      goog.vec.Float64Array.prototype.BYTES_PER_ELEMENT);
+  goog.exportProperty(
+      goog.vec.Float64Array.prototype, 'set',
+      goog.vec.Float64Array.prototype.set);
+  goog.exportProperty(
+      goog.vec.Float64Array.prototype, 'toString',
+      goog.vec.Float64Array.prototype.toString);
+  goog.exportSymbol('Float64Array', goog.vec.Float64Array);
+}
+
+// Copyright 2011 The Closure Library Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS-IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+
+/**
+ * @fileoverview Supplies global data types and constants for the vector math
+ *     library.
+ */
+goog.provide('goog.vec');
+goog.provide('goog.vec.AnyType');
+goog.provide('goog.vec.ArrayType');
+goog.provide('goog.vec.Float32');
+goog.provide('goog.vec.Float64');
+goog.provide('goog.vec.Number');
+
+
+/**
+ * On platforms that don't have native Float32Array or Float64Array support we
+ * use a javascript implementation so that this math library can be used on all
+ * platforms.
+ * @suppress {extraRequire}
+ */
+goog.require('goog.vec.Float32Array');
+/** @suppress {extraRequire} */
+goog.require('goog.vec.Float64Array');
+
+// All vector and matrix operations are based upon arrays of numbers using
+// either Float32Array, Float64Array, or a standard Javascript Array of
+// Numbers.
+
+
+/** @typedef {!Float32Array} */
+goog.vec.Float32;
+
+
+/** @typedef {!Float64Array} */
+goog.vec.Float64;
+
+
+/** @typedef {!Array<number>} */
+goog.vec.Number;
+
+
+/** @typedef {!goog.vec.Float32|!goog.vec.Float64|!goog.vec.Number} */
+goog.vec.AnyType;
+
+
+/**
+ * @deprecated Use AnyType.
+ * @typedef {!Float32Array|!Array<number>}
+ */
+goog.vec.ArrayType;
+
+
+/**
+ * For graphics work, 6 decimal places of accuracy are typically all that is
+ * required.
+ *
+ * @type {number}
+ * @const
+ */
+goog.vec.EPSILON = 1e-6;
+
+// Copyright 2011 The Closure Library Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS-IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+
+/**
+ * @fileoverview Supplies 3 element vectors that are compatible with WebGL.
+ * Each element is a float32 since that is typically the desired size of a
+ * 3-vector in the GPU.  The API is structured to avoid unnecessary memory
+ * allocations.  The last parameter will typically be the output vector and
+ * an object can be both an input and output parameter to all methods except
+ * where noted.
+ *
+ */
+goog.provide('goog.vec.Vec3');
+
+/** @suppress {extraRequire} */
+goog.require('goog.vec');
+
+/** @typedef {goog.vec.Float32} */ goog.vec.Vec3.Float32;
+/** @typedef {goog.vec.Float64} */ goog.vec.Vec3.Float64;
+/** @typedef {goog.vec.Number} */ goog.vec.Vec3.Number;
+/** @typedef {goog.vec.AnyType} */ goog.vec.Vec3.AnyType;
+
+// The following two types are deprecated - use the above types instead.
+/** @typedef {Float32Array} */ goog.vec.Vec3.Type;
+/** @typedef {goog.vec.ArrayType} */ goog.vec.Vec3.Vec3Like;
+
+
+/**
+ * Creates a 3 element vector of Float32. The array is initialized to zero.
+ *
+ * @return {!goog.vec.Vec3.Float32} The new 3 element array.
+ */
+goog.vec.Vec3.createFloat32 = function() {
+  return new Float32Array(3);
+};
+
+
+/**
+ * Creates a 3 element vector of Float64. The array is initialized to zero.
+ *
+ * @return {!goog.vec.Vec3.Float64} The new 3 element array.
+ */
+goog.vec.Vec3.createFloat64 = function() {
+  return new Float64Array(3);
+};
+
+
+/**
+ * Creates a 3 element vector of Number. The array is initialized to zero.
+ *
+ * @return {!goog.vec.Vec3.Number} The new 3 element array.
+ */
+goog.vec.Vec3.createNumber = function() {
+  var a = new Array(3);
+  goog.vec.Vec3.setFromValues(a, 0, 0, 0);
+  return a;
+};
+
+
+/**
+ * Creates a 3 element vector of Float32Array. The array is initialized to zero.
+ *
+ * @deprecated Use createFloat32.
+ * @return {!goog.vec.Vec3.Type} The new 3 element array.
+ */
+goog.vec.Vec3.create = function() {
+  return new Float32Array(3);
+};
+
+
+/**
+ * Creates a new 3 element Float32 vector initialized with the value from the
+ * given array.
+ *
+ * @param {goog.vec.Vec3.AnyType} vec The source 3 element array.
+ * @return {!goog.vec.Vec3.Float32} The new 3 element array.
+ */
+goog.vec.Vec3.createFloat32FromArray = function(vec) {
+  var newVec = goog.vec.Vec3.createFloat32();
+  goog.vec.Vec3.setFromArray(newVec, vec);
+  return newVec;
+};
+
+
+/**
+ * Creates a new 3 element Float32 vector initialized with the supplied values.
+ *
+ * @param {number} v0 The value for element at index 0.
+ * @param {number} v1 The value for element at index 1.
+ * @param {number} v2 The value for element at index 2.
+ * @return {!goog.vec.Vec3.Float32} The new vector.
+ */
+goog.vec.Vec3.createFloat32FromValues = function(v0, v1, v2) {
+  var a = goog.vec.Vec3.createFloat32();
+  goog.vec.Vec3.setFromValues(a, v0, v1, v2);
+  return a;
+};
+
+
+/**
+ * Creates a clone of the given 3 element Float32 vector.
+ *
+ * @param {goog.vec.Vec3.Float32} vec The source 3 element vector.
+ * @return {!goog.vec.Vec3.Float32} The new cloned vector.
+ */
+goog.vec.Vec3.cloneFloat32 = goog.vec.Vec3.createFloat32FromArray;
+
+
+/**
+ * Creates a new 3 element Float64 vector initialized with the value from the
+ * given array.
+ *
+ * @param {goog.vec.Vec3.AnyType} vec The source 3 element array.
+ * @return {!goog.vec.Vec3.Float64} The new 3 element array.
+ */
+goog.vec.Vec3.createFloat64FromArray = function(vec) {
+  var newVec = goog.vec.Vec3.createFloat64();
+  goog.vec.Vec3.setFromArray(newVec, vec);
+  return newVec;
+};
+
+
+/**
+* Creates a new 3 element Float64 vector initialized with the supplied values.
+*
+* @param {number} v0 The value for element at index 0.
+* @param {number} v1 The value for element at index 1.
+* @param {number} v2 The value for element at index 2.
+* @return {!goog.vec.Vec3.Float64} The new vector.
+*/
+goog.vec.Vec3.createFloat64FromValues = function(v0, v1, v2) {
+  var vec = goog.vec.Vec3.createFloat64();
+  goog.vec.Vec3.setFromValues(vec, v0, v1, v2);
+  return vec;
+};
+
+
+/**
+ * Creates a clone of the given 3 element vector.
+ *
+ * @param {goog.vec.Vec3.Float64} vec The source 3 element vector.
+ * @return {!goog.vec.Vec3.Float64} The new cloned vector.
+ */
+goog.vec.Vec3.cloneFloat64 = goog.vec.Vec3.createFloat64FromArray;
+
+
+/**
+ * Creates a new 3 element vector initialized with the value from the given
+ * array.
+ *
+ * @deprecated Use createFloat32FromArray.
+ * @param {goog.vec.Vec3.Vec3Like} vec The source 3 element array.
+ * @return {!goog.vec.Vec3.Type} The new 3 element array.
+ */
+goog.vec.Vec3.createFromArray = function(vec) {
+  var newVec = goog.vec.Vec3.create();
+  goog.vec.Vec3.setFromArray(newVec, vec);
+  return newVec;
+};
+
+
+/**
+ * Creates a new 3 element vector initialized with the supplied values.
+ *
+ * @deprecated Use createFloat32FromValues.
+ * @param {number} v0 The value for element at index 0.
+ * @param {number} v1 The value for element at index 1.
+ * @param {number} v2 The value for element at index 2.
+ * @return {!goog.vec.Vec3.Type} The new vector.
+ */
+goog.vec.Vec3.createFromValues = function(v0, v1, v2) {
+  var vec = goog.vec.Vec3.create();
+  goog.vec.Vec3.setFromValues(vec, v0, v1, v2);
+  return vec;
+};
+
+
+/**
+ * Creates a clone of the given 3 element vector.
+ *
+ * @deprecated Use cloneFloat32.
+ * @param {goog.vec.Vec3.Vec3Like} vec The source 3 element vector.
+ * @return {!goog.vec.Vec3.Type} The new cloned vector.
+ */
+goog.vec.Vec3.clone = function(vec) {
+  var newVec = goog.vec.Vec3.create();
+  goog.vec.Vec3.setFromArray(newVec, vec);
+  return newVec;
+};
+
+
+/**
+ * Initializes the vector with the given values.
+ *
+ * @param {goog.vec.Vec3.AnyType} vec The vector to receive the values.
+ * @param {number} v0 The value for element at index 0.
+ * @param {number} v1 The value for element at index 1.
+ * @param {number} v2 The value for element at index 2.
+ * @return {!goog.vec.Vec3.AnyType} Return vec so that operations can be
+ *     chained together.
+ */
+goog.vec.Vec3.setFromValues = function(vec, v0, v1, v2) {
+  vec[0] = v0;
+  vec[1] = v1;
+  vec[2] = v2;
+  return vec;
+};
+
+
+/**
+ * Initializes the vector with the given array of values.
+ *
+ * @param {goog.vec.Vec3.AnyType} vec The vector to receive the
+ *     values.
+ * @param {goog.vec.Vec3.AnyType} values The array of values.
+ * @return {!goog.vec.Vec3.AnyType} Return vec so that operations can be
+ *     chained together.
+ */
+goog.vec.Vec3.setFromArray = function(vec, values) {
+  vec[0] = values[0];
+  vec[1] = values[1];
+  vec[2] = values[2];
+  return vec;
+};
+
+
+/**
+ * Performs a component-wise addition of vec0 and vec1 together storing the
+ * result into resultVec.
+ *
+ * @param {goog.vec.Vec3.AnyType} vec0 The first addend.
+ * @param {goog.vec.Vec3.AnyType} vec1 The second addend.
+ * @param {goog.vec.Vec3.AnyType} resultVec The vector to
+ *     receive the result. May be vec0 or vec1.
+ * @return {!goog.vec.Vec3.AnyType} Return resultVec so that operations can be
+ *     chained together.
+ */
+goog.vec.Vec3.add = function(vec0, vec1, resultVec) {
+  resultVec[0] = vec0[0] + vec1[0];
+  resultVec[1] = vec0[1] + vec1[1];
+  resultVec[2] = vec0[2] + vec1[2];
+  return resultVec;
+};
+
+
+/**
+ * Performs a component-wise subtraction of vec1 from vec0 storing the
+ * result into resultVec.
+ *
+ * @param {goog.vec.Vec3.AnyType} vec0 The minuend.
+ * @param {goog.vec.Vec3.AnyType} vec1 The subtrahend.
+ * @param {goog.vec.Vec3.AnyType} resultVec The vector to
+ *     receive the result. May be vec0 or vec1.
+ * @return {!goog.vec.Vec3.AnyType} Return resultVec so that operations can be
+ *     chained together.
+ */
+goog.vec.Vec3.subtract = function(vec0, vec1, resultVec) {
+  resultVec[0] = vec0[0] - vec1[0];
+  resultVec[1] = vec0[1] - vec1[1];
+  resultVec[2] = vec0[2] - vec1[2];
+  return resultVec;
+};
+
+
+/**
+ * Negates vec0, storing the result into resultVec.
+ *
+ * @param {goog.vec.Vec3.AnyType} vec0 The vector to negate.
+ * @param {goog.vec.Vec3.AnyType} resultVec The vector to
+ *     receive the result. May be vec0.
+ * @return {!goog.vec.Vec3.AnyType} Return resultVec so that operations can be
+ *     chained together.
+ */
+goog.vec.Vec3.negate = function(vec0, resultVec) {
+  resultVec[0] = -vec0[0];
+  resultVec[1] = -vec0[1];
+  resultVec[2] = -vec0[2];
+  return resultVec;
+};
+
+
+/**
+ * Takes the absolute value of each component of vec0 storing the result in
+ * resultVec.
+ *
+ * @param {goog.vec.Vec3.AnyType} vec0 The source vector.
+ * @param {goog.vec.Vec3.AnyType} resultVec The vector to receive the result.
+ *     May be vec0.
+ * @return {!goog.vec.Vec3.AnyType} Return resultVec so that operations can be
+ *     chained together.
+ */
+goog.vec.Vec3.abs = function(vec0, resultVec) {
+  resultVec[0] = Math.abs(vec0[0]);
+  resultVec[1] = Math.abs(vec0[1]);
+  resultVec[2] = Math.abs(vec0[2]);
+  return resultVec;
+};
+
+
+/**
+ * Multiplies each component of vec0 with scalar storing the product into
+ * resultVec.
+ *
+ * @param {goog.vec.Vec3.AnyType} vec0 The source vector.
+ * @param {number} scalar The value to multiply with each component of vec0.
+ * @param {goog.vec.Vec3.AnyType} resultVec The vector to
+ *     receive the result. May be vec0.
+ * @return {!goog.vec.Vec3.AnyType} Return resultVec so that operations can be
+ *     chained together.
+ */
+goog.vec.Vec3.scale = function(vec0, scalar, resultVec) {
+  resultVec[0] = vec0[0] * scalar;
+  resultVec[1] = vec0[1] * scalar;
+  resultVec[2] = vec0[2] * scalar;
+  return resultVec;
+};
+
+
+/**
+ * Returns the magnitudeSquared of the given vector.
+ *
+ * @param {goog.vec.Vec3.AnyType} vec0 The vector.
+ * @return {number} The magnitude of the vector.
+ */
+goog.vec.Vec3.magnitudeSquared = function(vec0) {
+  var x = vec0[0], y = vec0[1], z = vec0[2];
+  return x * x + y * y + z * z;
+};
+
+
+/**
+ * Returns the magnitude of the given vector.
+ *
+ * @param {goog.vec.Vec3.AnyType} vec0 The vector.
+ * @return {number} The magnitude of the vector.
+ */
+goog.vec.Vec3.magnitude = function(vec0) {
+  var x = vec0[0], y = vec0[1], z = vec0[2];
+  return Math.sqrt(x * x + y * y + z * z);
+};
+
+
+/**
+ * Normalizes the given vector storing the result into resultVec.
+ *
+ * @param {goog.vec.Vec3.AnyType} vec0 The vector to normalize.
+ * @param {goog.vec.Vec3.AnyType} resultVec The vector to
+ *     receive the result. May be vec0.
+ * @return {!goog.vec.Vec3.AnyType} Return resultVec so that operations can be
+ *     chained together.
+ */
+goog.vec.Vec3.normalize = function(vec0, resultVec) {
+  var ilen = 1 / goog.vec.Vec3.magnitude(vec0);
+  resultVec[0] = vec0[0] * ilen;
+  resultVec[1] = vec0[1] * ilen;
+  resultVec[2] = vec0[2] * ilen;
+  return resultVec;
+};
+
+
+/**
+ * Returns the scalar product of vectors v0 and v1.
+ *
+ * @param {goog.vec.Vec3.AnyType} v0 The first vector.
+ * @param {goog.vec.Vec3.AnyType} v1 The second vector.
+ * @return {number} The scalar product.
+ */
+goog.vec.Vec3.dot = function(v0, v1) {
+  return v0[0] * v1[0] + v0[1] * v1[1] + v0[2] * v1[2];
+};
+
+
+/**
+ * Computes the vector (cross) product of v0 and v1 storing the result into
+ * resultVec.
+ *
+ * @param {goog.vec.Vec3.AnyType} v0 The first vector.
+ * @param {goog.vec.Vec3.AnyType} v1 The second vector.
+ * @param {goog.vec.Vec3.AnyType} resultVec The vector to receive the
+ *     results. May be either v0 or v1.
+ * @return {!goog.vec.Vec3.AnyType} Return resultVec so that operations can be
+ *     chained together.
+ */
+goog.vec.Vec3.cross = function(v0, v1, resultVec) {
+  var x0 = v0[0], y0 = v0[1], z0 = v0[2];
+  var x1 = v1[0], y1 = v1[1], z1 = v1[2];
+  resultVec[0] = y0 * z1 - z0 * y1;
+  resultVec[1] = z0 * x1 - x0 * z1;
+  resultVec[2] = x0 * y1 - y0 * x1;
+  return resultVec;
+};
+
+
+/**
+ * Returns the squared distance between two points.
+ *
+ * @param {goog.vec.Vec3.AnyType} vec0 First point.
+ * @param {goog.vec.Vec3.AnyType} vec1 Second point.
+ * @return {number} The squared distance between the points.
+ */
+goog.vec.Vec3.distanceSquared = function(vec0, vec1) {
+  var x = vec0[0] - vec1[0];
+  var y = vec0[1] - vec1[1];
+  var z = vec0[2] - vec1[2];
+  return x * x + y * y + z * z;
+};
+
+
+/**
+ * Returns the distance between two points.
+ *
+ * @param {goog.vec.Vec3.AnyType} vec0 First point.
+ * @param {goog.vec.Vec3.AnyType} vec1 Second point.
+ * @return {number} The distance between the points.
+ */
+goog.vec.Vec3.distance = function(vec0, vec1) {
+  return Math.sqrt(goog.vec.Vec3.distanceSquared(vec0, vec1));
+};
+
+
+/**
+ * Returns a unit vector pointing from one point to another.
+ * If the input points are equal then the result will be all zeros.
+ *
+ * @param {goog.vec.Vec3.AnyType} vec0 Origin point.
+ * @param {goog.vec.Vec3.AnyType} vec1 Target point.
+ * @param {goog.vec.Vec3.AnyType} resultVec The vector to receive the
+ *     results (may be vec0 or vec1).
+ * @return {!goog.vec.Vec3.AnyType} Return resultVec so that operations can be
+ *     chained together.
+ */
+goog.vec.Vec3.direction = function(vec0, vec1, resultVec) {
+  var x = vec1[0] - vec0[0];
+  var y = vec1[1] - vec0[1];
+  var z = vec1[2] - vec0[2];
+  var d = Math.sqrt(x * x + y * y + z * z);
+  if (d) {
+    d = 1 / d;
+    resultVec[0] = x * d;
+    resultVec[1] = y * d;
+    resultVec[2] = z * d;
+  } else {
+    resultVec[0] = resultVec[1] = resultVec[2] = 0;
+  }
+  return resultVec;
+};
+
+
+/**
+ * Linearly interpolate from vec0 to v1 according to f. The value of f should be
+ * in the range [0..1] otherwise the results are undefined.
+ *
+ * @param {goog.vec.Vec3.AnyType} v0 The first vector.
+ * @param {goog.vec.Vec3.AnyType} v1 The second vector.
+ * @param {number} f The interpolation factor.
+ * @param {goog.vec.Vec3.AnyType} resultVec The vector to receive the
+ *     results (may be v0 or v1).
+ * @return {!goog.vec.Vec3.AnyType} Return resultVec so that operations can be
+ *     chained together.
+ */
+goog.vec.Vec3.lerp = function(v0, v1, f, resultVec) {
+  var x = v0[0], y = v0[1], z = v0[2];
+  resultVec[0] = (v1[0] - x) * f + x;
+  resultVec[1] = (v1[1] - y) * f + y;
+  resultVec[2] = (v1[2] - z) * f + z;
+  return resultVec;
+};
+
+
+/**
+ * Compares the components of vec0 with the components of another vector or
+ * scalar, storing the larger values in resultVec.
+ *
+ * @param {goog.vec.Vec3.AnyType} vec0 The source vector.
+ * @param {goog.vec.Vec3.AnyType|number} limit The limit vector or scalar.
+ * @param {goog.vec.Vec3.AnyType} resultVec The vector to receive the
+ *     results (may be vec0 or limit).
+ * @return {!goog.vec.Vec3.AnyType} Return resultVec so that operations can be
+ *     chained together.
+ */
+goog.vec.Vec3.max = function(vec0, limit, resultVec) {
+  if (goog.isNumber(limit)) {
+    resultVec[0] = Math.max(vec0[0], limit);
+    resultVec[1] = Math.max(vec0[1], limit);
+    resultVec[2] = Math.max(vec0[2], limit);
+  } else {
+    resultVec[0] = Math.max(vec0[0], limit[0]);
+    resultVec[1] = Math.max(vec0[1], limit[1]);
+    resultVec[2] = Math.max(vec0[2], limit[2]);
+  }
+  return resultVec;
+};
+
+
+/**
+ * Compares the components of vec0 with the components of another vector or
+ * scalar, storing the smaller values in resultVec.
+ *
+ * @param {goog.vec.Vec3.AnyType} vec0 The source vector.
+ * @param {goog.vec.Vec3.AnyType|number} limit The limit vector or scalar.
+ * @param {goog.vec.Vec3.AnyType} resultVec The vector to receive the
+ *     results (may be vec0 or limit).
+ * @return {!goog.vec.Vec3.AnyType} Return resultVec so that operations can be
+ *     chained together.
+ */
+goog.vec.Vec3.min = function(vec0, limit, resultVec) {
+  if (goog.isNumber(limit)) {
+    resultVec[0] = Math.min(vec0[0], limit);
+    resultVec[1] = Math.min(vec0[1], limit);
+    resultVec[2] = Math.min(vec0[2], limit);
+  } else {
+    resultVec[0] = Math.min(vec0[0], limit[0]);
+    resultVec[1] = Math.min(vec0[1], limit[1]);
+    resultVec[2] = Math.min(vec0[2], limit[2]);
+  }
+  return resultVec;
+};
+
+
+/**
+ * Returns true if the components of v0 are equal to the components of v1.
+ *
+ * @param {goog.vec.Vec3.AnyType} v0 The first vector.
+ * @param {goog.vec.Vec3.AnyType} v1 The second vector.
+ * @return {boolean} True if the vectors are equal, false otherwise.
+ */
+goog.vec.Vec3.equals = function(v0, v1) {
+  return v0.length == v1.length && v0[0] == v1[0] && v0[1] == v1[1] &&
+      v0[2] == v1[2];
+};
+
+// Copyright 2011 The Closure Library Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS-IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+
+/**
+ * @fileoverview Supplies 4 element vectors that are compatible with WebGL.
+ * Each element is a float32 since that is typically the desired size of a
+ * 4-vector in the GPU.  The API is structured to avoid unnecessary memory
+ * allocations.  The last parameter will typically be the output vector and
+ * an object can be both an input and output parameter to all methods except
+ * where noted.
+ *
+ */
+goog.provide('goog.vec.Vec4');
+
+/** @suppress {extraRequire} */
+goog.require('goog.vec');
+
+/** @typedef {goog.vec.Float32} */ goog.vec.Vec4.Float32;
+/** @typedef {goog.vec.Float64} */ goog.vec.Vec4.Float64;
+/** @typedef {goog.vec.Number} */ goog.vec.Vec4.Number;
+/** @typedef {goog.vec.AnyType} */ goog.vec.Vec4.AnyType;
+
+// The following two types are deprecated - use the above types instead.
+/** @typedef {Float32Array} */ goog.vec.Vec4.Type;
+/** @typedef {goog.vec.ArrayType} */ goog.vec.Vec4.Vec4Like;
+
+
+/**
+ * Creates a 4 element vector of Float32. The array is initialized to zero.
+ *
+ * @return {!goog.vec.Vec4.Float32} The new 3 element array.
+ */
+goog.vec.Vec4.createFloat32 = function() {
+  return new Float32Array(4);
+};
+
+
+/**
+ * Creates a 4 element vector of Float64. The array is initialized to zero.
+ *
+ * @return {!goog.vec.Vec4.Float64} The new 4 element array.
+ */
+goog.vec.Vec4.createFloat64 = function() {
+  return new Float64Array(4);
+};
+
+
+/**
+ * Creates a 4 element vector of Number. The array is initialized to zero.
+ *
+ * @return {!goog.vec.Vec4.Number} The new 4 element array.
+ */
+goog.vec.Vec4.createNumber = function() {
+  var v = new Array(4);
+  goog.vec.Vec4.setFromValues(v, 0, 0, 0, 0);
+  return v;
+};
+
+
+/**
+ * Creates a 4 element vector of Float32Array. The array is initialized to zero.
+ *
+ * @deprecated Use createFloat32.
+ * @return {!goog.vec.Vec4.Type} The new 4 element array.
+ */
+goog.vec.Vec4.create = function() {
+  return new Float32Array(4);
+};
+
+
+/**
+ * Creates a new 4 element vector initialized with the value from the given
+ * array.
+ *
+ * @deprecated Use createFloat32FromArray.
+ * @param {goog.vec.Vec4.Vec4Like} vec The source 4 element array.
+ * @return {!goog.vec.Vec4.Type} The new 4 element array.
+ */
+goog.vec.Vec4.createFromArray = function(vec) {
+  var newVec = goog.vec.Vec4.create();
+  goog.vec.Vec4.setFromArray(newVec, vec);
+  return newVec;
+};
+
+
+/**
+ * Creates a new 4 element FLoat32 vector initialized with the value from the
+ * given array.
+ *
+ * @param {goog.vec.Vec4.AnyType} vec The source 3 element array.
+ * @return {!goog.vec.Vec4.Float32} The new 3 element array.
+ */
+goog.vec.Vec4.createFloat32FromArray = function(vec) {
+  var newVec = goog.vec.Vec4.createFloat32();
+  goog.vec.Vec4.setFromArray(newVec, vec);
+  return newVec;
+};
+
+
+/**
+ * Creates a new 4 element Float32 vector initialized with the supplied values.
+ *
+ * @param {number} v0 The value for element at index 0.
+ * @param {number} v1 The value for element at index 1.
+ * @param {number} v2 The value for element at index 2.
+ * @param {number} v3 The value for element at index 3.
+ * @return {!goog.vec.Vec4.Float32} The new vector.
+ */
+goog.vec.Vec4.createFloat32FromValues = function(v0, v1, v2, v3) {
+  var vec = goog.vec.Vec4.createFloat32();
+  goog.vec.Vec4.setFromValues(vec, v0, v1, v2, v3);
+  return vec;
+};
+
+
+/**
+ * Creates a clone of the given 4 element Float32 vector.
+ *
+ * @param {goog.vec.Vec4.Float32} vec The source 3 element vector.
+ * @return {!goog.vec.Vec4.Float32} The new cloned vector.
+ */
+goog.vec.Vec4.cloneFloat32 = goog.vec.Vec4.createFloat32FromArray;
+
+
+/**
+ * Creates a new 4 element Float64 vector initialized with the value from the
+ * given array.
+ *
+ * @param {goog.vec.Vec4.AnyType} vec The source 4 element array.
+ * @return {!goog.vec.Vec4.Float64} The new 4 element array.
+ */
+goog.vec.Vec4.createFloat64FromArray = function(vec) {
+  var newVec = goog.vec.Vec4.createFloat64();
+  goog.vec.Vec4.setFromArray(newVec, vec);
+  return newVec;
+};
+
+
+/**
+* Creates a new 4 element Float64 vector initialized with the supplied values.
+*
+* @param {number} v0 The value for element at index 0.
+* @param {number} v1 The value for element at index 1.
+* @param {number} v2 The value for element at index 2.
+* @param {number} v3 The value for element at index 3.
+* @return {!goog.vec.Vec4.Float64} The new vector.
+*/
+goog.vec.Vec4.createFloat64FromValues = function(v0, v1, v2, v3) {
+  var vec = goog.vec.Vec4.createFloat64();
+  goog.vec.Vec4.setFromValues(vec, v0, v1, v2, v3);
+  return vec;
+};
+
+
+/**
+ * Creates a clone of the given 4 element vector.
+ *
+ * @param {goog.vec.Vec4.Float64} vec The source 4 element vector.
+ * @return {!goog.vec.Vec4.Float64} The new cloned vector.
+ */
+goog.vec.Vec4.cloneFloat64 = goog.vec.Vec4.createFloat64FromArray;
+
+
+/**
+ * Creates a new 4 element vector initialized with the supplied values.
+ *
+ * @deprecated Use createFloat32FromValues.
+ * @param {number} v0 The value for element at index 0.
+ * @param {number} v1 The value for element at index 1.
+ * @param {number} v2 The value for element at index 2.
+ * @param {number} v3 The value for element at index 3.
+ * @return {!goog.vec.Vec4.Type} The new vector.
+ */
+goog.vec.Vec4.createFromValues = function(v0, v1, v2, v3) {
+  var vec = goog.vec.Vec4.create();
+  goog.vec.Vec4.setFromValues(vec, v0, v1, v2, v3);
+  return vec;
+};
+
+
+/**
+ * Creates a clone of the given 4 element vector.
+ *
+ * @deprecated Use cloneFloat32.
+ * @param {goog.vec.Vec4.Vec4Like} vec The source 4 element vector.
+ * @return {!goog.vec.Vec4.Type} The new cloned vector.
+ */
+goog.vec.Vec4.clone = goog.vec.Vec4.createFromArray;
+
+
+/**
+ * Initializes the vector with the given values.
+ *
+ * @param {goog.vec.Vec4.AnyType} vec The vector to receive the values.
+ * @param {number} v0 The value for element at index 0.
+ * @param {number} v1 The value for element at index 1.
+ * @param {number} v2 The value for element at index 2.
+ * @param {number} v3 The value for element at index 3.
+ * @return {!goog.vec.Vec4.AnyType} Return vec so that operations can be
+ *     chained together.
+ */
+goog.vec.Vec4.setFromValues = function(vec, v0, v1, v2, v3) {
+  vec[0] = v0;
+  vec[1] = v1;
+  vec[2] = v2;
+  vec[3] = v3;
+  return vec;
+};
+
+
+/**
+ * Initializes the vector with the given array of values.
+ *
+ * @param {goog.vec.Vec4.AnyType} vec The vector to receive the
+ *     values.
+ * @param {goog.vec.Vec4.AnyType} values The array of values.
+ * @return {!goog.vec.Vec4.AnyType} Return vec so that operations can be
+ *     chained together.
+ */
+goog.vec.Vec4.setFromArray = function(vec, values) {
+  vec[0] = values[0];
+  vec[1] = values[1];
+  vec[2] = values[2];
+  vec[3] = values[3];
+  return vec;
+};
+
+
+/**
+ * Performs a component-wise addition of vec0 and vec1 together storing the
+ * result into resultVec.
+ *
+ * @param {goog.vec.Vec4.AnyType} vec0 The first addend.
+ * @param {goog.vec.Vec4.AnyType} vec1 The second addend.
+ * @param {goog.vec.Vec4.AnyType} resultVec The vector to
+ *     receive the result. May be vec0 or vec1.
+ * @return {!goog.vec.Vec4.AnyType} Return resultVec so that operations can be
+ *     chained together.
+ */
+goog.vec.Vec4.add = function(vec0, vec1, resultVec) {
+  resultVec[0] = vec0[0] + vec1[0];
+  resultVec[1] = vec0[1] + vec1[1];
+  resultVec[2] = vec0[2] + vec1[2];
+  resultVec[3] = vec0[3] + vec1[3];
+  return resultVec;
+};
+
+
+/**
+ * Performs a component-wise subtraction of vec1 from vec0 storing the
+ * result into resultVec.
+ *
+ * @param {goog.vec.Vec4.AnyType} vec0 The minuend.
+ * @param {goog.vec.Vec4.AnyType} vec1 The subtrahend.
+ * @param {goog.vec.Vec4.AnyType} resultVec The vector to
+ *     receive the result. May be vec0 or vec1.
+ * @return {!goog.vec.Vec4.AnyType} Return resultVec so that operations can be
+ *     chained together.
+ */
+goog.vec.Vec4.subtract = function(vec0, vec1, resultVec) {
+  resultVec[0] = vec0[0] - vec1[0];
+  resultVec[1] = vec0[1] - vec1[1];
+  resultVec[2] = vec0[2] - vec1[2];
+  resultVec[3] = vec0[3] - vec1[3];
+  return resultVec;
+};
+
+
+/**
+ * Negates vec0, storing the result into resultVec.
+ *
+ * @param {goog.vec.Vec4.AnyType} vec0 The vector to negate.
+ * @param {goog.vec.Vec4.AnyType} resultVec The vector to
+ *     receive the result. May be vec0.
+ * @return {!goog.vec.Vec4.AnyType} Return resultVec so that operations can be
+ *     chained together.
+ */
+goog.vec.Vec4.negate = function(vec0, resultVec) {
+  resultVec[0] = -vec0[0];
+  resultVec[1] = -vec0[1];
+  resultVec[2] = -vec0[2];
+  resultVec[3] = -vec0[3];
+  return resultVec;
+};
+
+
+/**
+ * Takes the absolute value of each component of vec0 storing the result in
+ * resultVec.
+ *
+ * @param {goog.vec.Vec4.AnyType} vec0 The source vector.
+ * @param {goog.vec.Vec4.AnyType} resultVec The vector to receive the result.
+ *     May be vec0.
+ * @return {!goog.vec.Vec4.AnyType} Return resultVec so that operations can be
+ *     chained together.
+ */
+goog.vec.Vec4.abs = function(vec0, resultVec) {
+  resultVec[0] = Math.abs(vec0[0]);
+  resultVec[1] = Math.abs(vec0[1]);
+  resultVec[2] = Math.abs(vec0[2]);
+  resultVec[3] = Math.abs(vec0[3]);
+  return resultVec;
+};
+
+
+/**
+ * Multiplies each component of vec0 with scalar storing the product into
+ * resultVec.
+ *
+ * @param {goog.vec.Vec4.AnyType} vec0 The source vector.
+ * @param {number} scalar The value to multiply with each component of vec0.
+ * @param {goog.vec.Vec4.AnyType} resultVec The vector to
+ *     receive the result. May be vec0.
+ * @return {!goog.vec.Vec4.AnyType} Return resultVec so that operations can be
+ *     chained together.
+ */
+goog.vec.Vec4.scale = function(vec0, scalar, resultVec) {
+  resultVec[0] = vec0[0] * scalar;
+  resultVec[1] = vec0[1] * scalar;
+  resultVec[2] = vec0[2] * scalar;
+  resultVec[3] = vec0[3] * scalar;
+  return resultVec;
+};
+
+
+/**
+ * Returns the magnitudeSquared of the given vector.
+ *
+ * @param {goog.vec.Vec4.AnyType} vec0 The vector.
+ * @return {number} The magnitude of the vector.
+ */
+goog.vec.Vec4.magnitudeSquared = function(vec0) {
+  var x = vec0[0], y = vec0[1], z = vec0[2], w = vec0[3];
+  return x * x + y * y + z * z + w * w;
+};
+
+
+/**
+ * Returns the magnitude of the given vector.
+ *
+ * @param {goog.vec.Vec4.AnyType} vec0 The vector.
+ * @return {number} The magnitude of the vector.
+ */
+goog.vec.Vec4.magnitude = function(vec0) {
+  var x = vec0[0], y = vec0[1], z = vec0[2], w = vec0[3];
+  return Math.sqrt(x * x + y * y + z * z + w * w);
+};
+
+
+/**
+ * Normalizes the given vector storing the result into resultVec.
+ *
+ * @param {goog.vec.Vec4.AnyType} vec0 The vector to normalize.
+ * @param {goog.vec.Vec4.AnyType} resultVec The vector to
+ *     receive the result. May be vec0.
+ * @return {!goog.vec.Vec4.AnyType} Return resultVec so that operations can be
+ *     chained together.
+ */
+goog.vec.Vec4.normalize = function(vec0, resultVec) {
+  var ilen = 1 / goog.vec.Vec4.magnitude(vec0);
+  resultVec[0] = vec0[0] * ilen;
+  resultVec[1] = vec0[1] * ilen;
+  resultVec[2] = vec0[2] * ilen;
+  resultVec[3] = vec0[3] * ilen;
+  return resultVec;
+};
+
+
+/**
+ * Returns the scalar product of vectors v0 and v1.
+ *
+ * @param {goog.vec.Vec4.AnyType} v0 The first vector.
+ * @param {goog.vec.Vec4.AnyType} v1 The second vector.
+ * @return {number} The scalar product.
+ */
+goog.vec.Vec4.dot = function(v0, v1) {
+  return v0[0] * v1[0] + v0[1] * v1[1] + v0[2] * v1[2] + v0[3] * v1[3];
+};
+
+
+/**
+ * Linearly interpolate from v0 to v1 according to f. The value of f should be
+ * in the range [0..1] otherwise the results are undefined.
+ *
+ * @param {goog.vec.Vec4.AnyType} v0 The first vector.
+ * @param {goog.vec.Vec4.AnyType} v1 The second vector.
+ * @param {number} f The interpolation factor.
+ * @param {goog.vec.Vec4.AnyType} resultVec The vector to receive the
+ *     results (may be v0 or v1).
+ * @return {!goog.vec.Vec4.AnyType} Return resultVec so that operations can be
+ *     chained together.
+ */
+goog.vec.Vec4.lerp = function(v0, v1, f, resultVec) {
+  var x = v0[0], y = v0[1], z = v0[2], w = v0[3];
+  resultVec[0] = (v1[0] - x) * f + x;
+  resultVec[1] = (v1[1] - y) * f + y;
+  resultVec[2] = (v1[2] - z) * f + z;
+  resultVec[3] = (v1[3] - w) * f + w;
+  return resultVec;
+};
+
+
+/**
+ * Compares the components of vec0 with the components of another vector or
+ * scalar, storing the larger values in resultVec.
+ *
+ * @param {goog.vec.Vec4.AnyType} vec0 The source vector.
+ * @param {goog.vec.Vec4.AnyType|number} limit The limit vector or scalar.
+ * @param {goog.vec.Vec4.AnyType} resultVec The vector to receive the
+ *     results (may be vec0 or limit).
+ * @return {!goog.vec.Vec4.AnyType} Return resultVec so that operations can be
+ *     chained together.
+ */
+goog.vec.Vec4.max = function(vec0, limit, resultVec) {
+  if (goog.isNumber(limit)) {
+    resultVec[0] = Math.max(vec0[0], limit);
+    resultVec[1] = Math.max(vec0[1], limit);
+    resultVec[2] = Math.max(vec0[2], limit);
+    resultVec[3] = Math.max(vec0[3], limit);
+  } else {
+    resultVec[0] = Math.max(vec0[0], limit[0]);
+    resultVec[1] = Math.max(vec0[1], limit[1]);
+    resultVec[2] = Math.max(vec0[2], limit[2]);
+    resultVec[3] = Math.max(vec0[3], limit[3]);
+  }
+  return resultVec;
+};
+
+
+/**
+ * Compares the components of vec0 with the components of another vector or
+ * scalar, storing the smaller values in resultVec.
+ *
+ * @param {goog.vec.Vec4.AnyType} vec0 The source vector.
+ * @param {goog.vec.Vec4.AnyType|number} limit The limit vector or scalar.
+ * @param {goog.vec.Vec4.AnyType} resultVec The vector to receive the
+ *     results (may be vec0 or limit).
+ * @return {!goog.vec.Vec4.AnyType} Return resultVec so that operations can be
+ *     chained together.
+ */
+goog.vec.Vec4.min = function(vec0, limit, resultVec) {
+  if (goog.isNumber(limit)) {
+    resultVec[0] = Math.min(vec0[0], limit);
+    resultVec[1] = Math.min(vec0[1], limit);
+    resultVec[2] = Math.min(vec0[2], limit);
+    resultVec[3] = Math.min(vec0[3], limit);
+  } else {
+    resultVec[0] = Math.min(vec0[0], limit[0]);
+    resultVec[1] = Math.min(vec0[1], limit[1]);
+    resultVec[2] = Math.min(vec0[2], limit[2]);
+    resultVec[3] = Math.min(vec0[3], limit[3]);
+  }
+  return resultVec;
+};
+
+
+/**
+ * Returns true if the components of v0 are equal to the components of v1.
+ *
+ * @param {goog.vec.Vec4.AnyType} v0 The first vector.
+ * @param {goog.vec.Vec4.AnyType} v1 The second vector.
+ * @return {boolean} True if the vectors are equal, false otherwise.
+ */
+goog.vec.Vec4.equals = function(v0, v1) {
+  return v0.length == v1.length && v0[0] == v1[0] && v0[1] == v1[1] &&
+      v0[2] == v1[2] && v0[3] == v1[3];
+};
+
+// Copyright 2011 The Closure Library Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS-IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+/**
+ * @fileoverview Implements 4x4 matrices and their related functions which are
+ * compatible with WebGL. The API is structured to avoid unnecessary memory
+ * allocations.  The last parameter will typically be the output vector and
+ * an object can be both an input and output parameter to all methods except
+ * where noted. Matrix operations follow the mathematical form when multiplying
+ * vectors as follows: resultVec = matrix * vec.
+ *
+ * The matrices are stored in column-major order.
+ *
+ */
+goog.provide('goog.vec.Mat4');
+
+goog.require('goog.vec');
+goog.require('goog.vec.Vec3');
+goog.require('goog.vec.Vec4');
+
+
+/** @typedef {goog.vec.Float32} */ goog.vec.Mat4.Float32;
+/** @typedef {goog.vec.Float64} */ goog.vec.Mat4.Float64;
+/** @typedef {goog.vec.Number} */ goog.vec.Mat4.Number;
+/** @typedef {goog.vec.AnyType} */ goog.vec.Mat4.AnyType;
+
+// The following two types are deprecated - use the above types instead.
+/** @typedef {!Float32Array} */ goog.vec.Mat4.Type;
+/** @typedef {goog.vec.ArrayType} */ goog.vec.Mat4.Mat4Like;
+
+
+/**
+ * Creates the array representation of a 4x4 matrix of Float32.
+ * The use of the array directly instead of a class reduces overhead.
+ * The returned matrix is cleared to all zeros.
+ *
+ * @return {!goog.vec.Mat4.Float32} The new matrix.
+ */
+goog.vec.Mat4.createFloat32 = function() {
+  return new Float32Array(16);
+};
+
+
+/**
+ * Creates the array representation of a 4x4 matrix of Float64.
+ * The returned matrix is cleared to all zeros.
+ *
+ * @return {!goog.vec.Mat4.Float64} The new matrix.
+ */
+goog.vec.Mat4.createFloat64 = function() {
+  return new Float64Array(16);
+};
+
+
+/**
+ * Creates the array representation of a 4x4 matrix of Number.
+ * The returned matrix is cleared to all zeros.
+ *
+ * @return {!goog.vec.Mat4.Number} The new matrix.
+ */
+goog.vec.Mat4.createNumber = function() {
+  var a = new Array(16);
+  goog.vec.Mat4.setFromValues(
+      a, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0);
+  return a;
+};
+
+
+/**
+ * Creates the array representation of a 4x4 matrix of Float32.
+ * The returned matrix is cleared to all zeros.
+ *
+ * @deprecated Use createFloat32.
+ * @return {!goog.vec.Mat4.Type} The new matrix.
+ */
+goog.vec.Mat4.create = function() {
+  return goog.vec.Mat4.createFloat32();
+};
+
+
+/**
+ * Creates a 4x4 identity matrix of Float32.
+ *
+ * @return {!goog.vec.Mat4.Float32} The new 16 element array.
+ */
+goog.vec.Mat4.createFloat32Identity = function() {
+  var mat = goog.vec.Mat4.createFloat32();
+  mat[0] = mat[5] = mat[10] = mat[15] = 1;
+  return mat;
+};
+
+
+/**
+ * Creates a 4x4 identity matrix of Float64.
+ *
+ * @return {!goog.vec.Mat4.Float64} The new 16 element array.
+ */
+goog.vec.Mat4.createFloat64Identity = function() {
+  var mat = goog.vec.Mat4.createFloat64();
+  mat[0] = mat[5] = mat[10] = mat[15] = 1;
+  return mat;
+};
+
+
+/**
+ * Creates a 4x4 identity matrix of Number.
+ * The returned matrix is cleared to all zeros.
+ *
+ * @return {!goog.vec.Mat4.Number} The new 16 element array.
+ */
+goog.vec.Mat4.createNumberIdentity = function() {
+  var a = new Array(16);
+  goog.vec.Mat4.setFromValues(
+      a, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+  return a;
+};
+
+
+/**
+ * Creates the array representation of a 4x4 matrix of Float32.
+ * The returned matrix is cleared to all zeros.
+ *
+ * @deprecated Use createFloat32Identity.
+ * @return {!goog.vec.Mat4.Type} The new 16 element array.
+ */
+goog.vec.Mat4.createIdentity = function() {
+  return goog.vec.Mat4.createFloat32Identity();
+};
+
+
+/**
+ * Creates a 4x4 matrix of Float32 initialized from the given array.
+ *
+ * @param {goog.vec.Mat4.AnyType} matrix The array containing the
+ *     matrix values in column major order.
+ * @return {!goog.vec.Mat4.Float32} The new, 16 element array.
+ */
+goog.vec.Mat4.createFloat32FromArray = function(matrix) {
+  var newMatrix = goog.vec.Mat4.createFloat32();
+  goog.vec.Mat4.setFromArray(newMatrix, matrix);
+  return newMatrix;
+};
+
+
+/**
+ * Creates a 4x4 matrix of Float32 initialized from the given values.
+ *
+ * @param {number} v00 The values at (0, 0).
+ * @param {number} v10 The values at (1, 0).
+ * @param {number} v20 The values at (2, 0).
+ * @param {number} v30 The values at (3, 0).
+ * @param {number} v01 The values at (0, 1).
+ * @param {number} v11 The values at (1, 1).
+ * @param {number} v21 The values at (2, 1).
+ * @param {number} v31 The values at (3, 1).
+ * @param {number} v02 The values at (0, 2).
+ * @param {number} v12 The values at (1, 2).
+ * @param {number} v22 The values at (2, 2).
+ * @param {number} v32 The values at (3, 2).
+ * @param {number} v03 The values at (0, 3).
+ * @param {number} v13 The values at (1, 3).
+ * @param {number} v23 The values at (2, 3).
+ * @param {number} v33 The values at (3, 3).
+ * @return {!goog.vec.Mat4.Float32} The new, 16 element array.
+ */
+goog.vec.Mat4.createFloat32FromValues = function(
+    v00, v10, v20, v30, v01, v11, v21, v31, v02, v12, v22, v32, v03, v13, v23,
+    v33) {
+  var newMatrix = goog.vec.Mat4.createFloat32();
+  goog.vec.Mat4.setFromValues(
+      newMatrix, v00, v10, v20, v30, v01, v11, v21, v31, v02, v12, v22, v32,
+      v03, v13, v23, v33);
+  return newMatrix;
+};
+
+
+/**
+ * Creates a clone of a 4x4 matrix of Float32.
+ *
+ * @param {goog.vec.Mat4.Float32} matrix The source 4x4 matrix.
+ * @return {!goog.vec.Mat4.Float32} The new 4x4 element matrix.
+ */
+goog.vec.Mat4.cloneFloat32 = goog.vec.Mat4.createFloat32FromArray;
+
+
+/**
+ * Creates a 4x4 matrix of Float64 initialized from the given array.
+ *
+ * @param {goog.vec.Mat4.AnyType} matrix The array containing the
+ *     matrix values in column major order.
+ * @return {!goog.vec.Mat4.Float64} The new, nine element array.
+ */
+goog.vec.Mat4.createFloat64FromArray = function(matrix) {
+  var newMatrix = goog.vec.Mat4.createFloat64();
+  goog.vec.Mat4.setFromArray(newMatrix, matrix);
+  return newMatrix;
+};
+
+
+/**
+ * Creates a 4x4 matrix of Float64 initialized from the given values.
+ *
+ * @param {number} v00 The values at (0, 0).
+ * @param {number} v10 The values at (1, 0).
+ * @param {number} v20 The values at (2, 0).
+ * @param {number} v30 The values at (3, 0).
+ * @param {number} v01 The values at (0, 1).
+ * @param {number} v11 The values at (1, 1).
+ * @param {number} v21 The values at (2, 1).
+ * @param {number} v31 The values at (3, 1).
+ * @param {number} v02 The values at (0, 2).
+ * @param {number} v12 The values at (1, 2).
+ * @param {number} v22 The values at (2, 2).
+ * @param {number} v32 The values at (3, 2).
+ * @param {number} v03 The values at (0, 3).
+ * @param {number} v13 The values at (1, 3).
+ * @param {number} v23 The values at (2, 3).
+ * @param {number} v33 The values at (3, 3).
+ * @return {!goog.vec.Mat4.Float64} The new, 16 element array.
+ */
+goog.vec.Mat4.createFloat64FromValues = function(
+    v00, v10, v20, v30, v01, v11, v21, v31, v02, v12, v22, v32, v03, v13, v23,
+    v33) {
+  var newMatrix = goog.vec.Mat4.createFloat64();
+  goog.vec.Mat4.setFromValues(
+      newMatrix, v00, v10, v20, v30, v01, v11, v21, v31, v02, v12, v22, v32,
+      v03, v13, v23, v33);
+  return newMatrix;
+};
+
+
+/**
+ * Creates a clone of a 4x4 matrix of Float64.
+ *
+ * @param {goog.vec.Mat4.Float64} matrix The source 4x4 matrix.
+ * @return {!goog.vec.Mat4.Float64} The new 4x4 element matrix.
+ */
+goog.vec.Mat4.cloneFloat64 = goog.vec.Mat4.createFloat64FromArray;
+
+
+/**
+ * Creates a 4x4 matrix of Float32 initialized from the given array.
+ *
+ * @deprecated Use createFloat32FromArray.
+ * @param {goog.vec.Mat4.Mat4Like} matrix The array containing the
+ *     matrix values in column major order.
+ * @return {!goog.vec.Mat4.Type} The new, nine element array.
+ */
+goog.vec.Mat4.createFromArray = function(matrix) {
+  var newMatrix = goog.vec.Mat4.createFloat32();
+  goog.vec.Mat4.setFromArray(newMatrix, matrix);
+  return newMatrix;
+};
+
+
+/**
+ * Creates a 4x4 matrix of Float32 initialized from the given values.
+ *
+ * @deprecated Use createFloat32FromValues.
+ * @param {number} v00 The values at (0, 0).
+ * @param {number} v10 The values at (1, 0).
+ * @param {number} v20 The values at (2, 0).
+ * @param {number} v30 The values at (3, 0).
+ * @param {number} v01 The values at (0, 1).
+ * @param {number} v11 The values at (1, 1).
+ * @param {number} v21 The values at (2, 1).
+ * @param {number} v31 The values at (3, 1).
+ * @param {number} v02 The values at (0, 2).
+ * @param {number} v12 The values at (1, 2).
+ * @param {number} v22 The values at (2, 2).
+ * @param {number} v32 The values at (3, 2).
+ * @param {number} v03 The values at (0, 3).
+ * @param {number} v13 The values at (1, 3).
+ * @param {number} v23 The values at (2, 3).
+ * @param {number} v33 The values at (3, 3).
+ * @return {!goog.vec.Mat4.Type} The new, 16 element array.
+ */
+goog.vec.Mat4.createFromValues = function(
+    v00, v10, v20, v30, v01, v11, v21, v31, v02, v12, v22, v32, v03, v13, v23,
+    v33) {
+  return goog.vec.Mat4.createFloat32FromValues(
+      v00, v10, v20, v30, v01, v11, v21, v31, v02, v12, v22, v32, v03, v13, v23,
+      v33);
+};
+
+
+/**
+ * Creates a clone of a 4x4 matrix of Float32.
+ *
+ * @deprecated Use cloneFloat32.
+ * @param {goog.vec.Mat4.Mat4Like} matrix The source 4x4 matrix.
+ * @return {!goog.vec.Mat4.Type} The new 4x4 element matrix.
+ */
+goog.vec.Mat4.clone = goog.vec.Mat4.createFromArray;
+
+
+/**
+ * Retrieves the element at the requested row and column.
+ *
+ * @param {goog.vec.Mat4.AnyType} mat The matrix containing the
+ *     value to retrieve.
+ * @param {number} row The row index.
+ * @param {number} column The column index.
+ * @return {number} The element value at the requested row, column indices.
+ */
+goog.vec.Mat4.getElement = function(mat, row, column) {
+  return mat[row + column * 4];
+};
+
+
+/**
+ * Sets the element at the requested row and column.
+ *
+ * @param {goog.vec.Mat4.AnyType} mat The matrix to set the value on.
+ * @param {number} row The row index.
+ * @param {number} column The column index.
+ * @param {number} value The value to set at the requested row, column.
+ * @return {goog.vec.Mat4.AnyType} return mat so that operations can be
+ *     chained together.
+ */
+goog.vec.Mat4.setElement = function(mat, row, column, value) {
+  mat[row + column * 4] = value;
+  return mat;
+};
+
+
+/**
+ * Initializes the matrix from the set of values. Note the values supplied are
+ * in column major order.
+ *
+ * @param {goog.vec.Mat4.AnyType} mat The matrix to receive the
+ *     values.
+ * @param {number} v00 The values at (0, 0).
+ * @param {number} v10 The values at (1, 0).
+ * @param {number} v20 The values at (2, 0).
+ * @param {number} v30 The values at (3, 0).
+ * @param {number} v01 The values at (0, 1).
+ * @param {number} v11 The values at (1, 1).
+ * @param {number} v21 The values at (2, 1).
+ * @param {number} v31 The values at (3, 1).
+ * @param {number} v02 The values at (0, 2).
+ * @param {number} v12 The values at (1, 2).
+ * @param {number} v22 The values at (2, 2).
+ * @param {number} v32 The values at (3, 2).
+ * @param {number} v03 The values at (0, 3).
+ * @param {number} v13 The values at (1, 3).
+ * @param {number} v23 The values at (2, 3).
+ * @param {number} v33 The values at (3, 3).
+ * @return {goog.vec.Mat4.AnyType} return mat so that operations can be
+ *     chained together.
+ */
+goog.vec.Mat4.setFromValues = function(
+    mat, v00, v10, v20, v30, v01, v11, v21, v31, v02, v12, v22, v32, v03, v13,
+    v23, v33) {
+  mat[0] = v00;
+  mat[1] = v10;
+  mat[2] = v20;
+  mat[3] = v30;
+  mat[4] = v01;
+  mat[5] = v11;
+  mat[6] = v21;
+  mat[7] = v31;
+  mat[8] = v02;
+  mat[9] = v12;
+  mat[10] = v22;
+  mat[11] = v32;
+  mat[12] = v03;
+  mat[13] = v13;
+  mat[14] = v23;
+  mat[15] = v33;
+  return mat;
+};
+
+
+/**
+ * Sets the matrix from the array of values stored in column major order.
+ *
+ * @param {goog.vec.Mat4.AnyType} mat The matrix to receive the values.
+ * @param {goog.vec.Mat4.AnyType} values The column major ordered
+ *     array of values to store in the matrix.
+ * @return {goog.vec.Mat4.AnyType} return mat so that operations can be
+ *     chained together.
+ */
+goog.vec.Mat4.setFromArray = function(mat, values) {
+  mat[0] = values[0];
+  mat[1] = values[1];
+  mat[2] = values[2];
+  mat[3] = values[3];
+  mat[4] = values[4];
+  mat[5] = values[5];
+  mat[6] = values[6];
+  mat[7] = values[7];
+  mat[8] = values[8];
+  mat[9] = values[9];
+  mat[10] = values[10];
+  mat[11] = values[11];
+  mat[12] = values[12];
+  mat[13] = values[13];
+  mat[14] = values[14];
+  mat[15] = values[15];
+  return mat;
+};
+
+
+/**
+ * Sets the matrix from the array of values stored in row major order.
+ *
+ * @param {goog.vec.Mat4.AnyType} mat The matrix to receive the values.
+ * @param {goog.vec.Mat4.AnyType} values The row major ordered array of
+ *     values to store in the matrix.
+ * @return {goog.vec.Mat4.AnyType} return mat so that operations can be
+ *     chained together.
+ */
+goog.vec.Mat4.setFromRowMajorArray = function(mat, values) {
+  mat[0] = values[0];
+  mat[1] = values[4];
+  mat[2] = values[8];
+  mat[3] = values[12];
+
+  mat[4] = values[1];
+  mat[5] = values[5];
+  mat[6] = values[9];
+  mat[7] = values[13];
+
+  mat[8] = values[2];
+  mat[9] = values[6];
+  mat[10] = values[10];
+  mat[11] = values[14];
+
+  mat[12] = values[3];
+  mat[13] = values[7];
+  mat[14] = values[11];
+  mat[15] = values[15];
+
+  return mat;
+};
+
+
+/**
+ * Sets the diagonal values of the matrix from the given values.
+ *
+ * @param {goog.vec.Mat4.AnyType} mat The matrix to receive the values.
+ * @param {number} v00 The values for (0, 0).
+ * @param {number} v11 The values for (1, 1).
+ * @param {number} v22 The values for (2, 2).
+ * @param {number} v33 The values for (3, 3).
+ * @return {goog.vec.Mat4.AnyType} return mat so that operations can be
+ *     chained together.
+ */
+goog.vec.Mat4.setDiagonalValues = function(mat, v00, v11, v22, v33) {
+  mat[0] = v00;
+  mat[5] = v11;
+  mat[10] = v22;
+  mat[15] = v33;
+  return mat;
+};
+
+
+/**
+ * Sets the diagonal values of the matrix from the given vector.
+ *
+ * @param {goog.vec.Mat4.AnyType} mat The matrix to receive the values.
+ * @param {goog.vec.Vec4.AnyType} vec The vector containing the values.
+ * @return {goog.vec.Mat4.AnyType} return mat so that operations can be
+ *     chained together.
+ */
+goog.vec.Mat4.setDiagonal = function(mat, vec) {
+  mat[0] = vec[0];
+  mat[5] = vec[1];
+  mat[10] = vec[2];
+  mat[15] = vec[3];
+  return mat;
+};
+
+
+/**
+ * Gets the diagonal values of the matrix into the given vector.
+ *
+ * @param {goog.vec.Mat4.AnyType} mat The matrix containing the values.
+ * @param {goog.vec.Vec4.AnyType} vec The vector to receive the values.
+ * @param {number=} opt_diagonal Which diagonal to get. A value of 0 selects the
+ *     main diagonal, a positive number selects a super diagonal and a negative
+ *     number selects a sub diagonal.
+ * @return {goog.vec.Vec4.AnyType} return vec so that operations can be
+ *     chained together.
+ */
+goog.vec.Mat4.getDiagonal = function(mat, vec, opt_diagonal) {
+  if (!opt_diagonal) {
+    // This is the most common case, so we avoid the for loop.
+    vec[0] = mat[0];
+    vec[1] = mat[5];
+    vec[2] = mat[10];
+    vec[3] = mat[15];
+  } else {
+    var offset = opt_diagonal > 0 ? 4 * opt_diagonal : -opt_diagonal;
+    for (var i = 0; i < 4 - Math.abs(opt_diagonal); i++) {
+      vec[i] = mat[offset + 5 * i];
+    }
+  }
+  return vec;
+};
+
+
+/**
+ * Sets the specified column with the supplied values.
+ *
+ * @param {goog.vec.Mat4.AnyType} mat The matrix to receive the values.
+ * @param {number} column The column index to set the values on.
+ * @param {number} v0 The value for row 0.
+ * @param {number} v1 The value for row 1.
+ * @param {number} v2 The value for row 2.
+ * @param {number} v3 The value for row 3.
+ * @return {goog.vec.Mat4.AnyType} return mat so that operations can be
+ *     chained together.
+ */
+goog.vec.Mat4.setColumnValues = function(mat, column, v0, v1, v2, v3) {
+  var i = column * 4;
+  mat[i] = v0;
+  mat[i + 1] = v1;
+  mat[i + 2] = v2;
+  mat[i + 3] = v3;
+  return mat;
+};
+
+
+/**
+ * Sets the specified column with the value from the supplied vector.
+ *
+ * @param {goog.vec.Mat4.AnyType} mat The matrix to receive the values.
+ * @param {number} column The column index to set the values on.
+ * @param {goog.vec.Vec4.AnyType} vec The vector of elements for the column.
+ * @return {goog.vec.Mat4.AnyType} return mat so that operations can be
+ *     chained together.
+ */
+goog.vec.Mat4.setColumn = function(mat, column, vec) {
+  var i = column * 4;
+  mat[i] = vec[0];
+  mat[i + 1] = vec[1];
+  mat[i + 2] = vec[2];
+  mat[i + 3] = vec[3];
+  return mat;
+};
+
+
+/**
+ * Retrieves the specified column from the matrix into the given vector.
+ *
+ * @param {goog.vec.Mat4.AnyType} mat The matrix supplying the values.
+ * @param {number} column The column to get the values from.
+ * @param {goog.vec.Vec4.AnyType} vec The vector of elements to
+ *     receive the column.
+ * @return {goog.vec.Vec4.AnyType} return vec so that operations can be
+ *     chained together.
+ */
+goog.vec.Mat4.getColumn = function(mat, column, vec) {
+  var i = column * 4;
+  vec[0] = mat[i];
+  vec[1] = mat[i + 1];
+  vec[2] = mat[i + 2];
+  vec[3] = mat[i + 3];
+  return vec;
+};
+
+
+/**
+ * Sets the columns of the matrix from the given vectors.
+ *
+ * @param {goog.vec.Mat4.AnyType} mat The matrix to receive the values.
+ * @param {goog.vec.Vec4.AnyType} vec0 The values for column 0.
+ * @param {goog.vec.Vec4.AnyType} vec1 The values for column 1.
+ * @param {goog.vec.Vec4.AnyType} vec2 The values for column 2.
+ * @param {goog.vec.Vec4.AnyType} vec3 The values for column 3.
+ * @return {goog.vec.Mat4.AnyType} return mat so that operations can be
+ *     chained together.
+ */
+goog.vec.Mat4.setColumns = function(mat, vec0, vec1, vec2, vec3) {
+  goog.vec.Mat4.setColumn(mat, 0, vec0);
+  goog.vec.Mat4.setColumn(mat, 1, vec1);
+  goog.vec.Mat4.setColumn(mat, 2, vec2);
+  goog.vec.Mat4.setColumn(mat, 3, vec3);
+  return mat;
+};
+
+
+/**
+ * Retrieves the column values from the given matrix into the given vectors.
+ *
+ * @param {goog.vec.Mat4.AnyType} mat The matrix supplying the columns.
+ * @param {goog.vec.Vec4.AnyType} vec0 The vector to receive column 0.
+ * @param {goog.vec.Vec4.AnyType} vec1 The vector to receive column 1.
+ * @param {goog.vec.Vec4.AnyType} vec2 The vector to receive column 2.
+ * @param {goog.vec.Vec4.AnyType} vec3 The vector to receive column 3.
+ */
+goog.vec.Mat4.getColumns = function(mat, vec0, vec1, vec2, vec3) {
+  goog.vec.Mat4.getColumn(mat, 0, vec0);
+  goog.vec.Mat4.getColumn(mat, 1, vec1);
+  goog.vec.Mat4.getColumn(mat, 2, vec2);
+  goog.vec.Mat4.getColumn(mat, 3, vec3);
+};
+
+
+/**
+ * Sets the row values from the supplied values.
+ *
+ * @param {goog.vec.Mat4.AnyType} mat The matrix to receive the values.
+ * @param {number} row The index of the row to receive the values.
+ * @param {number} v0 The value for column 0.
+ * @param {number} v1 The value for column 1.
+ * @param {number} v2 The value for column 2.
+ * @param {number} v3 The value for column 3.
+ * @return {goog.vec.Mat4.AnyType} return mat so that operations can be
+ *     chained together.
+ */
+goog.vec.Mat4.setRowValues = function(mat, row, v0, v1, v2, v3) {
+  mat[row] = v0;
+  mat[row + 4] = v1;
+  mat[row + 8] = v2;
+  mat[row + 12] = v3;
+  return mat;
+};
+
+
+/**
+ * Sets the row values from the supplied vector.
+ *
+ * @param {goog.vec.Mat4.AnyType} mat The matrix to receive the row values.
+ * @param {number} row The index of the row.
+ * @param {goog.vec.Vec4.AnyType} vec The vector containing the values.
+ * @return {goog.vec.Mat4.AnyType} return mat so that operations can be
+ *     chained together.
+ */
+goog.vec.Mat4.setRow = function(mat, row, vec) {
+  mat[row] = vec[0];
+  mat[row + 4] = vec[1];
+  mat[row + 8] = vec[2];
+  mat[row + 12] = vec[3];
+  return mat;
+};
+
+
+/**
+ * Retrieves the row values into the given vector.
+ *
+ * @param {goog.vec.Mat4.AnyType} mat The matrix supplying the values.
+ * @param {number} row The index of the row supplying the values.
+ * @param {goog.vec.Vec4.AnyType} vec The vector to receive the row.
+ * @return {goog.vec.Vec4.AnyType} return vec so that operations can be
+ *     chained together.
+ */
+goog.vec.Mat4.getRow = function(mat, row, vec) {
+  vec[0] = mat[row];
+  vec[1] = mat[row + 4];
+  vec[2] = mat[row + 8];
+  vec[3] = mat[row + 12];
+  return vec;
+};
+
+
+/**
+ * Sets the rows of the matrix from the supplied vectors.
+ *
+ * @param {goog.vec.Mat4.AnyType} mat The matrix to receive the values.
+ * @param {goog.vec.Vec4.AnyType} vec0 The values for row 0.
+ * @param {goog.vec.Vec4.AnyType} vec1 The values for row 1.
+ * @param {goog.vec.Vec4.AnyType} vec2 The values for row 2.
+ * @param {goog.vec.Vec4.AnyType} vec3 The values for row 3.
+ * @return {goog.vec.Mat4.AnyType} return mat so that operations can be
+ *     chained together.
+ */
+goog.vec.Mat4.setRows = function(mat, vec0, vec1, vec2, vec3) {
+  goog.vec.Mat4.setRow(mat, 0, vec0);
+  goog.vec.Mat4.setRow(mat, 1, vec1);
+  goog.vec.Mat4.setRow(mat, 2, vec2);
+  goog.vec.Mat4.setRow(mat, 3, vec3);
+  return mat;
+};
+
+
+/**
+ * Retrieves the rows of the matrix into the supplied vectors.
+ *
+ * @param {goog.vec.Mat4.AnyType} mat The matrix to supply the values.
+ * @param {goog.vec.Vec4.AnyType} vec0 The vector to receive row 0.
+ * @param {goog.vec.Vec4.AnyType} vec1 The vector to receive row 1.
+ * @param {goog.vec.Vec4.AnyType} vec2 The vector to receive row 2.
+ * @param {goog.vec.Vec4.AnyType} vec3 The vector to receive row 3.
+ */
+goog.vec.Mat4.getRows = function(mat, vec0, vec1, vec2, vec3) {
+  goog.vec.Mat4.getRow(mat, 0, vec0);
+  goog.vec.Mat4.getRow(mat, 1, vec1);
+  goog.vec.Mat4.getRow(mat, 2, vec2);
+  goog.vec.Mat4.getRow(mat, 3, vec3);
+};
+
+
+/**
+ * Makes the given 4x4 matrix the zero matrix.
+ *
+ * @param {goog.vec.Mat4.AnyType} mat The matrix.
+ * @return {!goog.vec.Mat4.AnyType} return mat so operations can be chained.
+ */
+goog.vec.Mat4.makeZero = function(mat) {
+  mat[0] = 0;
+  mat[1] = 0;
+  mat[2] = 0;
+  mat[3] = 0;
+  mat[4] = 0;
+  mat[5] = 0;
+  mat[6] = 0;
+  mat[7] = 0;
+  mat[8] = 0;
+  mat[9] = 0;
+  mat[10] = 0;
+  mat[11] = 0;
+  mat[12] = 0;
+  mat[13] = 0;
+  mat[14] = 0;
+  mat[15] = 0;
+  return mat;
+};
+
+
+/**
+ * Makes the given 4x4 matrix the identity matrix.
+ *
+ * @param {goog.vec.Mat4.AnyType} mat The matrix.
+ * @return {goog.vec.Mat4.AnyType} return mat so operations can be chained.
+ */
+goog.vec.Mat4.makeIdentity = function(mat) {
+  mat[0] = 1;
+  mat[1] = 0;
+  mat[2] = 0;
+  mat[3] = 0;
+  mat[4] = 0;
+  mat[5] = 1;
+  mat[6] = 0;
+  mat[7] = 0;
+  mat[8] = 0;
+  mat[9] = 0;
+  mat[10] = 1;
+  mat[11] = 0;
+  mat[12] = 0;
+  mat[13] = 0;
+  mat[14] = 0;
+  mat[15] = 1;
+  return mat;
+};
+
+
+/**
+ * Performs a per-component addition of the matrix mat0 and mat1, storing
+ * the result into resultMat.
+ *
+ * @param {goog.vec.Mat4.AnyType} mat0 The first addend.
+ * @param {goog.vec.Mat4.AnyType} mat1 The second addend.
+ * @param {goog.vec.Mat4.AnyType} resultMat The matrix to
+ *     receive the results (may be either mat0 or mat1).
+ * @return {goog.vec.Mat4.AnyType} return resultMat so that operations can be
+ *     chained together.
+ */
+goog.vec.Mat4.addMat = function(mat0, mat1, resultMat) {
+  resultMat[0] = mat0[0] + mat1[0];
+  resultMat[1] = mat0[1] + mat1[1];
+  resultMat[2] = mat0[2] + mat1[2];
+  resultMat[3] = mat0[3] + mat1[3];
+  resultMat[4] = mat0[4] + mat1[4];
+  resultMat[5] = mat0[5] + mat1[5];
+  resultMat[6] = mat0[6] + mat1[6];
+  resultMat[7] = mat0[7] + mat1[7];
+  resultMat[8] = mat0[8] + mat1[8];
+  resultMat[9] = mat0[9] + mat1[9];
+  resultMat[10] = mat0[10] + mat1[10];
+  resultMat[11] = mat0[11] + mat1[11];
+  resultMat[12] = mat0[12] + mat1[12];
+  resultMat[13] = mat0[13] + mat1[13];
+  resultMat[14] = mat0[14] + mat1[14];
+  resultMat[15] = mat0[15] + mat1[15];
+  return resultMat;
+};
+
+
+/**
+ * Performs a per-component subtraction of the matrix mat0 and mat1,
+ * storing the result into resultMat.
+ *
+ * @param {goog.vec.Mat4.AnyType} mat0 The minuend.
+ * @param {goog.vec.Mat4.AnyType} mat1 The subtrahend.
+ * @param {goog.vec.Mat4.AnyType} resultMat The matrix to receive
+ *     the results (may be either mat0 or mat1).
+ * @return {goog.vec.Mat4.AnyType} return resultMat so that operations can be
+ *     chained together.
+ */
+goog.vec.Mat4.subMat = function(mat0, mat1, resultMat) {
+  resultMat[0] = mat0[0] - mat1[0];
+  resultMat[1] = mat0[1] - mat1[1];
+  resultMat[2] = mat0[2] - mat1[2];
+  resultMat[3] = mat0[3] - mat1[3];
+  resultMat[4] = mat0[4] - mat1[4];
+  resultMat[5] = mat0[5] - mat1[5];
+  resultMat[6] = mat0[6] - mat1[6];
+  resultMat[7] = mat0[7] - mat1[7];
+  resultMat[8] = mat0[8] - mat1[8];
+  resultMat[9] = mat0[9] - mat1[9];
+  resultMat[10] = mat0[10] - mat1[10];
+  resultMat[11] = mat0[11] - mat1[11];
+  resultMat[12] = mat0[12] - mat1[12];
+  resultMat[13] = mat0[13] - mat1[13];
+  resultMat[14] = mat0[14] - mat1[14];
+  resultMat[15] = mat0[15] - mat1[15];
+  return resultMat;
+};
+
+
+/**
+ * Multiplies matrix mat with the given scalar, storing the result
+ * into resultMat.
+ *
+ * @param {goog.vec.Mat4.AnyType} mat The matrix.
+ * @param {number} scalar The scalar value to multiply to each element of mat.
+ * @param {goog.vec.Mat4.AnyType} resultMat The matrix to receive
+ *     the results (may be mat).
+ * @return {goog.vec.Mat4.AnyType} return resultMat so that operations can be
+ *     chained together.
+ */
+goog.vec.Mat4.multScalar = function(mat, scalar, resultMat) {
+  resultMat[0] = mat[0] * scalar;
+  resultMat[1] = mat[1] * scalar;
+  resultMat[2] = mat[2] * scalar;
+  resultMat[3] = mat[3] * scalar;
+  resultMat[4] = mat[4] * scalar;
+  resultMat[5] = mat[5] * scalar;
+  resultMat[6] = mat[6] * scalar;
+  resultMat[7] = mat[7] * scalar;
+  resultMat[8] = mat[8] * scalar;
+  resultMat[9] = mat[9] * scalar;
+  resultMat[10] = mat[10] * scalar;
+  resultMat[11] = mat[11] * scalar;
+  resultMat[12] = mat[12] * scalar;
+  resultMat[13] = mat[13] * scalar;
+  resultMat[14] = mat[14] * scalar;
+  resultMat[15] = mat[15] * scalar;
+  return resultMat;
+};
+
+
+/**
+ * Multiplies the two matrices mat0 and mat1 using matrix multiplication,
+ * storing the result into resultMat.
+ *
+ * @param {goog.vec.Mat4.AnyType} mat0 The first (left hand) matrix.
+ * @param {goog.vec.Mat4.AnyType} mat1 The second (right hand) matrix.
+ * @param {goog.vec.Mat4.AnyType} resultMat The matrix to receive
+ *     the results (may be either mat0 or mat1).
+ * @return {goog.vec.Mat4.AnyType} return resultMat so that operations can be
+ *     chained together.
+ */
+goog.vec.Mat4.multMat = function(mat0, mat1, resultMat) {
+  var a00 = mat0[0], a10 = mat0[1], a20 = mat0[2], a30 = mat0[3];
+  var a01 = mat0[4], a11 = mat0[5], a21 = mat0[6], a31 = mat0[7];
+  var a02 = mat0[8], a12 = mat0[9], a22 = mat0[10], a32 = mat0[11];
+  var a03 = mat0[12], a13 = mat0[13], a23 = mat0[14], a33 = mat0[15];
+
+  var b00 = mat1[0], b10 = mat1[1], b20 = mat1[2], b30 = mat1[3];
+  var b01 = mat1[4], b11 = mat1[5], b21 = mat1[6], b31 = mat1[7];
+  var b02 = mat1[8], b12 = mat1[9], b22 = mat1[10], b32 = mat1[11];
+  var b03 = mat1[12], b13 = mat1[13], b23 = mat1[14], b33 = mat1[15];
+
+  resultMat[0] = a00 * b00 + a01 * b10 + a02 * b20 + a03 * b30;
+  resultMat[1] = a10 * b00 + a11 * b10 + a12 * b20 + a13 * b30;
+  resultMat[2] = a20 * b00 + a21 * b10 + a22 * b20 + a23 * b30;
+  resultMat[3] = a30 * b00 + a31 * b10 + a32 * b20 + a33 * b30;
+
+  resultMat[4] = a00 * b01 + a01 * b11 + a02 * b21 + a03 * b31;
+  resultMat[5] = a10 * b01 + a11 * b11 + a12 * b21 + a13 * b31;
+  resultMat[6] = a20 * b01 + a21 * b11 + a22 * b21 + a23 * b31;
+  resultMat[7] = a30 * b01 + a31 * b11 + a32 * b21 + a33 * b31;
+
+  resultMat[8] = a00 * b02 + a01 * b12 + a02 * b22 + a03 * b32;
+  resultMat[9] = a10 * b02 + a11 * b12 + a12 * b22 + a13 * b32;
+  resultMat[10] = a20 * b02 + a21 * b12 + a22 * b22 + a23 * b32;
+  resultMat[11] = a30 * b02 + a31 * b12 + a32 * b22 + a33 * b32;
+
+  resultMat[12] = a00 * b03 + a01 * b13 + a02 * b23 + a03 * b33;
+  resultMat[13] = a10 * b03 + a11 * b13 + a12 * b23 + a13 * b33;
+  resultMat[14] = a20 * b03 + a21 * b13 + a22 * b23 + a23 * b33;
+  resultMat[15] = a30 * b03 + a31 * b13 + a32 * b23 + a33 * b33;
+  return resultMat;
+};
+
+
+/**
+ * Transposes the given matrix mat storing the result into resultMat.
+ *
+ * @param {goog.vec.Mat4.AnyType} mat The matrix to transpose.
+ * @param {goog.vec.Mat4.AnyType} resultMat The matrix to receive
+ *     the results (may be mat).
+ * @return {goog.vec.Mat4.AnyType} return resultMat so that operations can be
+ *     chained together.
+ */
+goog.vec.Mat4.transpose = function(mat, resultMat) {
+  if (resultMat == mat) {
+    var a10 = mat[1], a20 = mat[2], a30 = mat[3];
+    var a21 = mat[6], a31 = mat[7];
+    var a32 = mat[11];
+    resultMat[1] = mat[4];
+    resultMat[2] = mat[8];
+    resultMat[3] = mat[12];
+    resultMat[4] = a10;
+    resultMat[6] = mat[9];
+    resultMat[7] = mat[13];
+    resultMat[8] = a20;
+    resultMat[9] = a21;
+    resultMat[11] = mat[14];
+    resultMat[12] = a30;
+    resultMat[13] = a31;
+    resultMat[14] = a32;
+  } else {
+    resultMat[0] = mat[0];
+    resultMat[1] = mat[4];
+    resultMat[2] = mat[8];
+    resultMat[3] = mat[12];
+
+    resultMat[4] = mat[1];
+    resultMat[5] = mat[5];
+    resultMat[6] = mat[9];
+    resultMat[7] = mat[13];
+
+    resultMat[8] = mat[2];
+    resultMat[9] = mat[6];
+    resultMat[10] = mat[10];
+    resultMat[11] = mat[14];
+
+    resultMat[12] = mat[3];
+    resultMat[13] = mat[7];
+    resultMat[14] = mat[11];
+    resultMat[15] = mat[15];
+  }
+  return resultMat;
+};
+
+
+/**
+ * Computes the determinant of the matrix.
+ *
+ * @param {goog.vec.Mat4.AnyType} mat The matrix to compute the matrix for.
+ * @return {number} The determinant of the matrix.
+ */
+goog.vec.Mat4.determinant = function(mat) {
+  var m00 = mat[0], m10 = mat[1], m20 = mat[2], m30 = mat[3];
+  var m01 = mat[4], m11 = mat[5], m21 = mat[6], m31 = mat[7];
+  var m02 = mat[8], m12 = mat[9], m22 = mat[10], m32 = mat[11];
+  var m03 = mat[12], m13 = mat[13], m23 = mat[14], m33 = mat[15];
+
+  var a0 = m00 * m11 - m10 * m01;
+  var a1 = m00 * m21 - m20 * m01;
+  var a2 = m00 * m31 - m30 * m01;
+  var a3 = m10 * m21 - m20 * m11;
+  var a4 = m10 * m31 - m30 * m11;
+  var a5 = m20 * m31 - m30 * m21;
+  var b0 = m02 * m13 - m12 * m03;
+  var b1 = m02 * m23 - m22 * m03;
+  var b2 = m02 * m33 - m32 * m03;
+  var b3 = m12 * m23 - m22 * m13;
+  var b4 = m12 * m33 - m32 * m13;
+  var b5 = m22 * m33 - m32 * m23;
+
+  return a0 * b5 - a1 * b4 + a2 * b3 + a3 * b2 - a4 * b1 + a5 * b0;
+};
+
+
+/**
+ * Computes the inverse of mat storing the result into resultMat. If the
+ * inverse is defined, this function returns true, false otherwise.
+ *
+ * @param {goog.vec.Mat4.AnyType} mat The matrix to invert.
+ * @param {goog.vec.Mat4.AnyType} resultMat The matrix to receive
+ *     the result (may be mat).
+ * @return {boolean} True if the inverse is defined. If false is returned,
+ *     resultMat is not modified.
+ */
+goog.vec.Mat4.invert = function(mat, resultMat) {
+  var m00 = mat[0], m10 = mat[1], m20 = mat[2], m30 = mat[3];
+  var m01 = mat[4], m11 = mat[5], m21 = mat[6], m31 = mat[7];
+  var m02 = mat[8], m12 = mat[9], m22 = mat[10], m32 = mat[11];
+  var m03 = mat[12], m13 = mat[13], m23 = mat[14], m33 = mat[15];
+
+  var a0 = m00 * m11 - m10 * m01;
+  var a1 = m00 * m21 - m20 * m01;
+  var a2 = m00 * m31 - m30 * m01;
+  var a3 = m10 * m21 - m20 * m11;
+  var a4 = m10 * m31 - m30 * m11;
+  var a5 = m20 * m31 - m30 * m21;
+  var b0 = m02 * m13 - m12 * m03;
+  var b1 = m02 * m23 - m22 * m03;
+  var b2 = m02 * m33 - m32 * m03;
+  var b3 = m12 * m23 - m22 * m13;
+  var b4 = m12 * m33 - m32 * m13;
+  var b5 = m22 * m33 - m32 * m23;
+
+  var det = a0 * b5 - a1 * b4 + a2 * b3 + a3 * b2 - a4 * b1 + a5 * b0;
+  if (det == 0) {
+    return false;
+  }
+
+  var idet = 1.0 / det;
+  resultMat[0] = (m11 * b5 - m21 * b4 + m31 * b3) * idet;
+  resultMat[1] = (-m10 * b5 + m20 * b4 - m30 * b3) * idet;
+  resultMat[2] = (m13 * a5 - m23 * a4 + m33 * a3) * idet;
+  resultMat[3] = (-m12 * a5 + m22 * a4 - m32 * a3) * idet;
+  resultMat[4] = (-m01 * b5 + m21 * b2 - m31 * b1) * idet;
+  resultMat[5] = (m00 * b5 - m20 * b2 + m30 * b1) * idet;
+  resultMat[6] = (-m03 * a5 + m23 * a2 - m33 * a1) * idet;
+  resultMat[7] = (m02 * a5 - m22 * a2 + m32 * a1) * idet;
+  resultMat[8] = (m01 * b4 - m11 * b2 + m31 * b0) * idet;
+  resultMat[9] = (-m00 * b4 + m10 * b2 - m30 * b0) * idet;
+  resultMat[10] = (m03 * a4 - m13 * a2 + m33 * a0) * idet;
+  resultMat[11] = (-m02 * a4 + m12 * a2 - m32 * a0) * idet;
+  resultMat[12] = (-m01 * b3 + m11 * b1 - m21 * b0) * idet;
+  resultMat[13] = (m00 * b3 - m10 * b1 + m20 * b0) * idet;
+  resultMat[14] = (-m03 * a3 + m13 * a1 - m23 * a0) * idet;
+  resultMat[15] = (m02 * a3 - m12 * a1 + m22 * a0) * idet;
+  return true;
+};
+
+
+/**
+ * Returns true if the components of mat0 are equal to the components of mat1.
+ *
+ * @param {goog.vec.Mat4.AnyType} mat0 The first matrix.
+ * @param {goog.vec.Mat4.AnyType} mat1 The second matrix.
+ * @return {boolean} True if the the two matrices are equivalent.
+ */
+goog.vec.Mat4.equals = function(mat0, mat1) {
+  return mat0.length == mat1.length && mat0[0] == mat1[0] &&
+      mat0[1] == mat1[1] && mat0[2] == mat1[2] && mat0[3] == mat1[3] &&
+      mat0[4] == mat1[4] && mat0[5] == mat1[5] && mat0[6] == mat1[6] &&
+      mat0[7] == mat1[7] && mat0[8] == mat1[8] && mat0[9] == mat1[9] &&
+      mat0[10] == mat1[10] && mat0[11] == mat1[11] && mat0[12] == mat1[12] &&
+      mat0[13] == mat1[13] && mat0[14] == mat1[14] && mat0[15] == mat1[15];
+};
+
+
+/**
+ * Transforms the given vector with the given matrix storing the resulting,
+ * transformed vector into resultVec. The input vector is multiplied against the
+ * upper 3x4 matrix omitting the projective component.
+ *
+ * @param {goog.vec.Mat4.AnyType} mat The matrix supplying the transformation.
+ * @param {goog.vec.Vec3.AnyType} vec The 3 element vector to transform.
+ * @param {goog.vec.Vec3.AnyType} resultVec The 3 element vector to
+ *     receive the results (may be vec).
+ * @return {goog.vec.Vec3.AnyType} return resultVec so that operations can be
+ *     chained together.
+ */
+goog.vec.Mat4.multVec3 = function(mat, vec, resultVec) {
+  var x = vec[0], y = vec[1], z = vec[2];
+  resultVec[0] = x * mat[0] + y * mat[4] + z * mat[8] + mat[12];
+  resultVec[1] = x * mat[1] + y * mat[5] + z * mat[9] + mat[13];
+  resultVec[2] = x * mat[2] + y * mat[6] + z * mat[10] + mat[14];
+  return resultVec;
+};
+
+
+/**
+ * Transforms the given vector with the given matrix storing the resulting,
+ * transformed vector into resultVec. The input vector is multiplied against the
+ * upper 3x3 matrix omitting the projective component and translation
+ * components.
+ *
+ * @param {goog.vec.Mat4.AnyType} mat The matrix supplying the transformation.
+ * @param {goog.vec.Vec3.AnyType} vec The 3 element vector to transform.
+ * @param {goog.vec.Vec3.AnyType} resultVec The 3 element vector to
+ *     receive the results (may be vec).
+ * @return {goog.vec.Vec3.AnyType} return resultVec so that operations can be
+ *     chained together.
+ */
+goog.vec.Mat4.multVec3NoTranslate = function(mat, vec, resultVec) {
+  var x = vec[0], y = vec[1], z = vec[2];
+  resultVec[0] = x * mat[0] + y * mat[4] + z * mat[8];
+  resultVec[1] = x * mat[1] + y * mat[5] + z * mat[9];
+  resultVec[2] = x * mat[2] + y * mat[6] + z * mat[10];
+  return resultVec;
+};
+
+
+/**
+ * Transforms the given vector with the given matrix storing the resulting,
+ * transformed vector into resultVec. The input vector is multiplied against the
+ * full 4x4 matrix with the homogeneous divide applied to reduce the 4 element
+ * vector to a 3 element vector.
+ *
+ * @param {goog.vec.Mat4.AnyType} mat The matrix supplying the transformation.
+ * @param {goog.vec.Vec3.AnyType} vec The 3 element vector to transform.
+ * @param {goog.vec.Vec3.AnyType} resultVec The 3 element vector
+ *     to receive the results (may be vec).
+ * @return {goog.vec.Vec3.AnyType} return resultVec so that operations can be
+ *     chained together.
+ */
+goog.vec.Mat4.multVec3Projective = function(mat, vec, resultVec) {
+  var x = vec[0], y = vec[1], z = vec[2];
+  var invw = 1 / (x * mat[3] + y * mat[7] + z * mat[11] + mat[15]);
+  resultVec[0] = (x * mat[0] + y * mat[4] + z * mat[8] + mat[12]) * invw;
+  resultVec[1] = (x * mat[1] + y * mat[5] + z * mat[9] + mat[13]) * invw;
+  resultVec[2] = (x * mat[2] + y * mat[6] + z * mat[10] + mat[14]) * invw;
+  return resultVec;
+};
+
+
+/**
+ * Transforms the given vector with the given matrix storing the resulting,
+ * transformed vector into resultVec.
+ *
+ * @param {goog.vec.Mat4.AnyType} mat The matrix supplying the transformation.
+ * @param {goog.vec.Vec4.AnyType} vec The vector to transform.
+ * @param {goog.vec.Vec4.AnyType} resultVec The vector to
+ *     receive the results (may be vec).
+ * @return {goog.vec.Vec4.AnyType} return resultVec so that operations can be
+ *     chained together.
+ */
+goog.vec.Mat4.multVec4 = function(mat, vec, resultVec) {
+  var x = vec[0], y = vec[1], z = vec[2], w = vec[3];
+  resultVec[0] = x * mat[0] + y * mat[4] + z * mat[8] + w * mat[12];
+  resultVec[1] = x * mat[1] + y * mat[5] + z * mat[9] + w * mat[13];
+  resultVec[2] = x * mat[2] + y * mat[6] + z * mat[10] + w * mat[14];
+  resultVec[3] = x * mat[3] + y * mat[7] + z * mat[11] + w * mat[15];
+  return resultVec;
+};
+
+
+/**
+ * Makes the given 4x4 matrix a translation matrix with x, y and z
+ * translation factors.
+ *
+ * @param {goog.vec.Mat4.AnyType} mat The matrix.
+ * @param {number} x The translation along the x axis.
+ * @param {number} y The translation along the y axis.
+ * @param {number} z The translation along the z axis.
+ * @return {goog.vec.Mat4.AnyType} return mat so that operations can be
+ *     chained.
+ */
+goog.vec.Mat4.makeTranslate = function(mat, x, y, z) {
+  goog.vec.Mat4.makeIdentity(mat);
+  return goog.vec.Mat4.setColumnValues(mat, 3, x, y, z, 1);
+};
+
+
+/**
+ * Makes the given 4x4 matrix as a scale matrix with x, y and z scale factors.
+ *
+ * @param {goog.vec.Mat4.AnyType} mat The matrix.
+ * @param {number} x The scale along the x axis.
+ * @param {number} y The scale along the y axis.
+ * @param {number} z The scale along the z axis.
+ * @return {goog.vec.Mat4.AnyType} return mat so that operations can be
+ *     chained.
+ */
+goog.vec.Mat4.makeScale = function(mat, x, y, z) {
+  goog.vec.Mat4.makeIdentity(mat);
+  return goog.vec.Mat4.setDiagonalValues(mat, x, y, z, 1);
+};
+
+
+/**
+ * Makes the given 4x4 matrix a rotation matrix with the given rotation
+ * angle about the axis defined by the vector (ax, ay, az).
+ *
+ * @param {goog.vec.Mat4.AnyType} mat The matrix.
+ * @param {number} angle The rotation angle in radians.
+ * @param {number} ax The x component of the rotation axis.
+ * @param {number} ay The y component of the rotation axis.
+ * @param {number} az The z component of the rotation axis.
+ * @return {goog.vec.Mat4.AnyType} return mat so that operations can be
+ *     chained.
+ */
+goog.vec.Mat4.makeRotate = function(mat, angle, ax, ay, az) {
+  var c = Math.cos(angle);
+  var d = 1 - c;
+  var s = Math.sin(angle);
+
+  return goog.vec.Mat4.setFromValues(
+      mat, ax * ax * d + c, ax * ay * d + az * s, ax * az * d - ay * s, 0,
+
+      ax * ay * d - az * s, ay * ay * d + c, ay * az * d + ax * s, 0,
+
+      ax * az * d + ay * s, ay * az * d - ax * s, az * az * d + c, 0,
+
+      0, 0, 0, 1);
+};
+
+
+/**
+ * Makes the given 4x4 matrix a rotation matrix with the given rotation
+ * angle about the X axis.
+ *
+ * @param {goog.vec.Mat4.AnyType} mat The matrix.
+ * @param {number} angle The rotation angle in radians.
+ * @return {goog.vec.Mat4.AnyType} return mat so that operations can be
+ *     chained.
+ */
+goog.vec.Mat4.makeRotateX = function(mat, angle) {
+  var c = Math.cos(angle);
+  var s = Math.sin(angle);
+  return goog.vec.Mat4.setFromValues(
+      mat, 1, 0, 0, 0, 0, c, s, 0, 0, -s, c, 0, 0, 0, 0, 1);
+};
+
+
+/**
+ * Makes the given 4x4 matrix a rotation matrix with the given rotation
+ * angle about the Y axis.
+ *
+ * @param {goog.vec.Mat4.AnyType} mat The matrix.
+ * @param {number} angle The rotation angle in radians.
+ * @return {goog.vec.Mat4.AnyType} return mat so that operations can be
+ *     chained.
+ */
+goog.vec.Mat4.makeRotateY = function(mat, angle) {
+  var c = Math.cos(angle);
+  var s = Math.sin(angle);
+  return goog.vec.Mat4.setFromValues(
+      mat, c, 0, -s, 0, 0, 1, 0, 0, s, 0, c, 0, 0, 0, 0, 1);
+};
+
+
+/**
+ * Makes the given 4x4 matrix a rotation matrix with the given rotation
+ * angle about the Z axis.
+ *
+ * @param {goog.vec.Mat4.AnyType} mat The matrix.
+ * @param {number} angle The rotation angle in radians.
+ * @return {goog.vec.Mat4.AnyType} return mat so that operations can be
+ *     chained.
+ */
+goog.vec.Mat4.makeRotateZ = function(mat, angle) {
+  var c = Math.cos(angle);
+  var s = Math.sin(angle);
+  return goog.vec.Mat4.setFromValues(
+      mat, c, s, 0, 0, -s, c, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
+};
+
+
+/**
+ * Makes the given 4x4 matrix a perspective projection matrix.
+ *
+ * @param {goog.vec.Mat4.AnyType} mat The matrix.
+ * @param {number} left The coordinate of the left clipping plane.
+ * @param {number} right The coordinate of the right clipping plane.
+ * @param {number} bottom The coordinate of the bottom clipping plane.
+ * @param {number} top The coordinate of the top clipping plane.
+ * @param {number} near The distance to the near clipping plane.
+ * @param {number} far The distance to the far clipping plane.
+ * @return {goog.vec.Mat4.AnyType} return mat so that operations can be
+ *     chained.
+ */
+goog.vec.Mat4.makeFrustum = function(mat, left, right, bottom, top, near, far) {
+  var x = (2 * near) / (right - left);
+  var y = (2 * near) / (top - bottom);
+  var a = (right + left) / (right - left);
+  var b = (top + bottom) / (top - bottom);
+  var c = -(far + near) / (far - near);
+  var d = -(2 * far * near) / (far - near);
+
+  return goog.vec.Mat4.setFromValues(
+      mat, x, 0, 0, 0, 0, y, 0, 0, a, b, c, -1, 0, 0, d, 0);
+};
+
+
+/**
+ * Makes the given 4x4 matrix  perspective projection matrix given a
+ * field of view and aspect ratio.
+ *
+ * @param {goog.vec.Mat4.AnyType} mat The matrix.
+ * @param {number} fovy The field of view along the y (vertical) axis in
+ *     radians.
+ * @param {number} aspect The x (width) to y (height) aspect ratio.
+ * @param {number} near The distance to the near clipping plane.
+ * @param {number} far The distance to the far clipping plane.
+ * @return {goog.vec.Mat4.AnyType} return mat so that operations can be
+ *     chained.
+ */
+goog.vec.Mat4.makePerspective = function(mat, fovy, aspect, near, far) {
+  var angle = fovy / 2;
+  var dz = far - near;
+  var sinAngle = Math.sin(angle);
+  if (dz == 0 || sinAngle == 0 || aspect == 0) {
+    return mat;
+  }
+
+  var cot = Math.cos(angle) / sinAngle;
+  return goog.vec.Mat4.setFromValues(
+      mat, cot / aspect, 0, 0, 0, 0, cot, 0, 0, 0, 0, -(far + near) / dz, -1, 0,
+      0, -(2 * near * far) / dz, 0);
+};
+
+
+/**
+ * Makes the given 4x4 matrix an orthographic projection matrix.
+ *
+ * @param {goog.vec.Mat4.AnyType} mat The matrix.
+ * @param {number} left The coordinate of the left clipping plane.
+ * @param {number} right The coordinate of the right clipping plane.
+ * @param {number} bottom The coordinate of the bottom clipping plane.
+ * @param {number} top The coordinate of the top clipping plane.
+ * @param {number} near The distance to the near clipping plane.
+ * @param {number} far The distance to the far clipping plane.
+ * @return {goog.vec.Mat4.AnyType} return mat so that operations can be
+ *     chained.
+ */
+goog.vec.Mat4.makeOrtho = function(mat, left, right, bottom, top, near, far) {
+  var x = 2 / (right - left);
+  var y = 2 / (top - bottom);
+  var z = -2 / (far - near);
+  var a = -(right + left) / (right - left);
+  var b = -(top + bottom) / (top - bottom);
+  var c = -(far + near) / (far - near);
+
+  return goog.vec.Mat4.setFromValues(
+      mat, x, 0, 0, 0, 0, y, 0, 0, 0, 0, z, 0, a, b, c, 1);
+};
+
+
+/**
+ * Makes the given 4x4 matrix a modelview matrix of a camera so that
+ * the camera is 'looking at' the given center point.
+ *
+ * @param {goog.vec.Mat4.AnyType} mat The matrix.
+ * @param {goog.vec.Vec3.AnyType} eyePt The position of the eye point
+ *     (camera origin).
+ * @param {goog.vec.Vec3.AnyType} centerPt The point to aim the camera at.
+ * @param {goog.vec.Vec3.AnyType} worldUpVec The vector that identifies
+ *     the up direction for the camera.
+ * @return {goog.vec.Mat4.AnyType} return mat so that operations can be
+ *     chained.
+ */
+goog.vec.Mat4.makeLookAt = function(mat, eyePt, centerPt, worldUpVec) {
+  // Compute the direction vector from the eye point to the center point and
+  // normalize.
+  var fwdVec = goog.vec.Mat4.tmpVec4_[0];
+  goog.vec.Vec3.subtract(centerPt, eyePt, fwdVec);
+  goog.vec.Vec3.normalize(fwdVec, fwdVec);
+  fwdVec[3] = 0;
+
+  // Compute the side vector from the forward vector and the input up vector.
+  var sideVec = goog.vec.Mat4.tmpVec4_[1];
+  goog.vec.Vec3.cross(fwdVec, worldUpVec, sideVec);
+  goog.vec.Vec3.normalize(sideVec, sideVec);
+  sideVec[3] = 0;
+
+  // Now the up vector to form the orthonormal basis.
+  var upVec = goog.vec.Mat4.tmpVec4_[2];
+  goog.vec.Vec3.cross(sideVec, fwdVec, upVec);
+  goog.vec.Vec3.normalize(upVec, upVec);
+  upVec[3] = 0;
+
+  // Update the view matrix with the new orthonormal basis and position the
+  // camera at the given eye point.
+  goog.vec.Vec3.negate(fwdVec, fwdVec);
+  goog.vec.Mat4.setRow(mat, 0, sideVec);
+  goog.vec.Mat4.setRow(mat, 1, upVec);
+  goog.vec.Mat4.setRow(mat, 2, fwdVec);
+  goog.vec.Mat4.setRowValues(mat, 3, 0, 0, 0, 1);
+  goog.vec.Mat4.translate(mat, -eyePt[0], -eyePt[1], -eyePt[2]);
+
+  return mat;
+};
+
+
+/**
+ * Decomposes a matrix into the lookAt vectors eyePt, fwdVec and worldUpVec.
+ * The matrix represents the modelview matrix of a camera. It is the inverse
+ * of lookAt except for the output of the fwdVec instead of centerPt.
+ * The centerPt itself cannot be recovered from a modelview matrix.
+ *
+ * @param {goog.vec.Mat4.AnyType} mat The matrix.
+ * @param {goog.vec.Vec3.AnyType} eyePt The position of the eye point
+ *     (camera origin).
+ * @param {goog.vec.Vec3.AnyType} fwdVec The vector describing where
+ *     the camera points to.
+ * @param {goog.vec.Vec3.AnyType} worldUpVec The vector that
+ *     identifies the up direction for the camera.
+ * @return {boolean} True if the method succeeds, false otherwise.
+ *     The method can only fail if the inverse of viewMatrix is not defined.
+ */
+goog.vec.Mat4.toLookAt = function(mat, eyePt, fwdVec, worldUpVec) {
+  // Get eye of the camera.
+  var matInverse = goog.vec.Mat4.tmpMat4_[0];
+  if (!goog.vec.Mat4.invert(mat, matInverse)) {
+    // The input matrix does not have a valid inverse.
+    return false;
+  }
+
+  if (eyePt) {
+    eyePt[0] = matInverse[12];
+    eyePt[1] = matInverse[13];
+    eyePt[2] = matInverse[14];
+  }
+
+  // Get forward vector from the definition of lookAt.
+  if (fwdVec || worldUpVec) {
+    if (!fwdVec) {
+      fwdVec = goog.vec.Mat4.tmpVec3_[0];
+    }
+    fwdVec[0] = -mat[2];
+    fwdVec[1] = -mat[6];
+    fwdVec[2] = -mat[10];
+    // Normalize forward vector.
+    goog.vec.Vec3.normalize(fwdVec, fwdVec);
+  }
+
+  if (worldUpVec) {
+    // Get side vector from the definition of gluLookAt.
+    var side = goog.vec.Mat4.tmpVec3_[1];
+    side[0] = mat[0];
+    side[1] = mat[4];
+    side[2] = mat[8];
+    // Compute up vector as a up = side x forward.
+    goog.vec.Vec3.cross(side, fwdVec, worldUpVec);
+    // Normalize up vector.
+    goog.vec.Vec3.normalize(worldUpVec, worldUpVec);
+  }
+  return true;
+};
+
+
+/**
+ * Makes the given 4x4 matrix a rotation matrix given Euler angles using
+ * the ZXZ convention.
+ * Given the euler angles [theta1, theta2, theta3], the rotation is defined as
+ * rotation = rotation_z(theta1) * rotation_x(theta2) * rotation_z(theta3),
+ * with theta1 in [0, 2 * pi], theta2 in [0, pi] and theta3 in [0, 2 * pi].
+ * rotation_x(theta) means rotation around the X axis of theta radians,
+ *
+ * @param {goog.vec.Mat4.AnyType} mat The matrix.
+ * @param {number} theta1 The angle of rotation around the Z axis in radians.
+ * @param {number} theta2 The angle of rotation around the X axis in radians.
+ * @param {number} theta3 The angle of rotation around the Z axis in radians.
+ * @return {goog.vec.Mat4.AnyType} return mat so that operations can be
+ *     chained.
+ */
+goog.vec.Mat4.makeEulerZXZ = function(mat, theta1, theta2, theta3) {
+  var c1 = Math.cos(theta1);
+  var s1 = Math.sin(theta1);
+
+  var c2 = Math.cos(theta2);
+  var s2 = Math.sin(theta2);
+
+  var c3 = Math.cos(theta3);
+  var s3 = Math.sin(theta3);
+
+  mat[0] = c1 * c3 - c2 * s1 * s3;
+  mat[1] = c2 * c1 * s3 + c3 * s1;
+  mat[2] = s3 * s2;
+  mat[3] = 0;
+
+  mat[4] = -c1 * s3 - c3 * c2 * s1;
+  mat[5] = c1 * c2 * c3 - s1 * s3;
+  mat[6] = c3 * s2;
+  mat[7] = 0;
+
+  mat[8] = s2 * s1;
+  mat[9] = -c1 * s2;
+  mat[10] = c2;
+  mat[11] = 0;
+
+  mat[12] = 0;
+  mat[13] = 0;
+  mat[14] = 0;
+  mat[15] = 1;
+
+  return mat;
+};
+
+
+/**
+ * Decomposes a rotation matrix into Euler angles using the ZXZ convention so
+ * that rotation = rotation_z(theta1) * rotation_x(theta2) * rotation_z(theta3),
+ * with theta1 in [0, 2 * pi], theta2 in [0, pi] and theta3 in [0, 2 * pi].
+ * rotation_x(theta) means rotation around the X axis of theta radians.
+ *
+ * @param {goog.vec.Mat4.AnyType} mat The matrix.
+ * @param {goog.vec.Vec3.AnyType} euler The ZXZ Euler angles in
+ *     radians as [theta1, theta2, theta3].
+ * @param {boolean=} opt_theta2IsNegative Whether theta2 is in [-pi, 0] instead
+ *     of the default [0, pi].
+ * @return {goog.vec.Vec4.AnyType} return euler so that operations can be
+ *     chained together.
+ */
+goog.vec.Mat4.toEulerZXZ = function(mat, euler, opt_theta2IsNegative) {
+  // There is an ambiguity in the sign of sinTheta2 because of the sqrt.
+  var sinTheta2 = Math.sqrt(mat[2] * mat[2] + mat[6] * mat[6]);
+
+  // By default we explicitely constrain theta2 to be in [0, pi],
+  // so sinTheta2 is always positive. We can change the behavior and specify
+  // theta2 to be negative in [-pi, 0] with opt_Theta2IsNegative.
+  var signTheta2 = opt_theta2IsNegative ? -1 : 1;
+
+  if (sinTheta2 > goog.vec.EPSILON) {
+    euler[2] = Math.atan2(mat[2] * signTheta2, mat[6] * signTheta2);
+    euler[1] = Math.atan2(sinTheta2 * signTheta2, mat[10]);
+    euler[0] = Math.atan2(mat[8] * signTheta2, -mat[9] * signTheta2);
+  } else {
+    // There is also an arbitrary choice for theta1 = 0 or theta2 = 0 here.
+    // We assume theta1 = 0 as some applications do not allow the camera to roll
+    // (i.e. have theta1 != 0).
+    euler[0] = 0;
+    euler[1] = Math.atan2(sinTheta2 * signTheta2, mat[10]);
+    euler[2] = Math.atan2(mat[1], mat[0]);
+  }
+
+  // Atan2 outputs angles in [-pi, pi] so we bring them back to [0, 2 * pi].
+  euler[0] = (euler[0] + Math.PI * 2) % (Math.PI * 2);
+  euler[2] = (euler[2] + Math.PI * 2) % (Math.PI * 2);
+  // For theta2 we want the angle to be in [0, pi] or [-pi, 0] depending on
+  // signTheta2.
+  euler[1] =
+      ((euler[1] * signTheta2 + Math.PI * 2) % (Math.PI * 2)) * signTheta2;
+
+  return euler;
+};
+
+
+/**
+ * Translates the given matrix by x,y,z.  Equvialent to:
+ * goog.vec.Mat4.multMat(
+ *     mat,
+ *     goog.vec.Mat4.makeTranslate(goog.vec.Mat4.create(), x, y, z),
+ *     mat);
+ *
+ * @param {goog.vec.Mat4.AnyType} mat The matrix.
+ * @param {number} x The translation along the x axis.
+ * @param {number} y The translation along the y axis.
+ * @param {number} z The translation along the z axis.
+ * @return {goog.vec.Mat4.AnyType} return mat so that operations can be
+ *     chained.
+ */
+goog.vec.Mat4.translate = function(mat, x, y, z) {
+  return goog.vec.Mat4.setColumnValues(
+      mat, 3, mat[0] * x + mat[4] * y + mat[8] * z + mat[12],
+      mat[1] * x + mat[5] * y + mat[9] * z + mat[13],
+      mat[2] * x + mat[6] * y + mat[10] * z + mat[14],
+      mat[3] * x + mat[7] * y + mat[11] * z + mat[15]);
+};
+
+
+/**
+ * Scales the given matrix by x,y,z.  Equivalent to:
+ * goog.vec.Mat4.multMat(
+ *     mat,
+ *     goog.vec.Mat4.makeScale(goog.vec.Mat4.create(), x, y, z),
+ *     mat);
+ *
+ * @param {goog.vec.Mat4.AnyType} mat The matrix.
+ * @param {number} x The x scale factor.
+ * @param {number} y The y scale factor.
+ * @param {number} z The z scale factor.
+ * @return {goog.vec.Mat4.AnyType} return mat so that operations can be
+ *     chained.
+ */
+goog.vec.Mat4.scale = function(mat, x, y, z) {
+  return goog.vec.Mat4.setFromValues(
+      mat, mat[0] * x, mat[1] * x, mat[2] * x, mat[3] * x, mat[4] * y,
+      mat[5] * y, mat[6] * y, mat[7] * y, mat[8] * z, mat[9] * z, mat[10] * z,
+      mat[11] * z, mat[12], mat[13], mat[14], mat[15]);
+};
+
+
+/**
+ * Rotate the given matrix by angle about the x,y,z axis.  Equivalent to:
+ * goog.vec.Mat4.multMat(
+ *     mat,
+ *     goog.vec.Mat4.makeRotate(goog.vec.Mat4.create(), angle, x, y, z),
+ *     mat);
+ *
+ * @param {goog.vec.Mat4.AnyType} mat The matrix.
+ * @param {number} angle The angle in radians.
+ * @param {number} x The x component of the rotation axis.
+ * @param {number} y The y component of the rotation axis.
+ * @param {number} z The z component of the rotation axis.
+ * @return {goog.vec.Mat4.AnyType} return mat so that operations can be
+ *     chained.
+ */
+goog.vec.Mat4.rotate = function(mat, angle, x, y, z) {
+  var m00 = mat[0], m10 = mat[1], m20 = mat[2], m30 = mat[3];
+  var m01 = mat[4], m11 = mat[5], m21 = mat[6], m31 = mat[7];
+  var m02 = mat[8], m12 = mat[9], m22 = mat[10], m32 = mat[11];
+  var m03 = mat[12], m13 = mat[13], m23 = mat[14], m33 = mat[15];
+
+  var cosAngle = Math.cos(angle);
+  var sinAngle = Math.sin(angle);
+  var diffCosAngle = 1 - cosAngle;
+  var r00 = x * x * diffCosAngle + cosAngle;
+  var r10 = x * y * diffCosAngle + z * sinAngle;
+  var r20 = x * z * diffCosAngle - y * sinAngle;
+
+  var r01 = x * y * diffCosAngle - z * sinAngle;
+  var r11 = y * y * diffCosAngle + cosAngle;
+  var r21 = y * z * diffCosAngle + x * sinAngle;
+
+  var r02 = x * z * diffCosAngle + y * sinAngle;
+  var r12 = y * z * diffCosAngle - x * sinAngle;
+  var r22 = z * z * diffCosAngle + cosAngle;
+
+  return goog.vec.Mat4.setFromValues(
+      mat, m00 * r00 + m01 * r10 + m02 * r20, m10 * r00 + m11 * r10 + m12 * r20,
+      m20 * r00 + m21 * r10 + m22 * r20, m30 * r00 + m31 * r10 + m32 * r20,
+
+      m00 * r01 + m01 * r11 + m02 * r21, m10 * r01 + m11 * r11 + m12 * r21,
+      m20 * r01 + m21 * r11 + m22 * r21, m30 * r01 + m31 * r11 + m32 * r21,
+
+      m00 * r02 + m01 * r12 + m02 * r22, m10 * r02 + m11 * r12 + m12 * r22,
+      m20 * r02 + m21 * r12 + m22 * r22, m30 * r02 + m31 * r12 + m32 * r22,
+
+      m03, m13, m23, m33);
+};
+
+
+/**
+ * Rotate the given matrix by angle about the x axis.  Equivalent to:
+ * goog.vec.Mat4.multMat(
+ *     mat,
+ *     goog.vec.Mat4.makeRotateX(goog.vec.Mat4.create(), angle),
+ *     mat);
+ *
+ * @param {goog.vec.Mat4.AnyType} mat The matrix.
+ * @param {number} angle The angle in radians.
+ * @return {goog.vec.Mat4.AnyType} return mat so that operations can be
+ *     chained.
+ */
+goog.vec.Mat4.rotateX = function(mat, angle) {
+  var m01 = mat[4], m11 = mat[5], m21 = mat[6], m31 = mat[7];
+  var m02 = mat[8], m12 = mat[9], m22 = mat[10], m32 = mat[11];
+
+  var c = Math.cos(angle);
+  var s = Math.sin(angle);
+
+  mat[4] = m01 * c + m02 * s;
+  mat[5] = m11 * c + m12 * s;
+  mat[6] = m21 * c + m22 * s;
+  mat[7] = m31 * c + m32 * s;
+  mat[8] = m01 * -s + m02 * c;
+  mat[9] = m11 * -s + m12 * c;
+  mat[10] = m21 * -s + m22 * c;
+  mat[11] = m31 * -s + m32 * c;
+
+  return mat;
+};
+
+
+/**
+ * Rotate the given matrix by angle about the y axis.  Equivalent to:
+ * goog.vec.Mat4.multMat(
+ *     mat,
+ *     goog.vec.Mat4.makeRotateY(goog.vec.Mat4.create(), angle),
+ *     mat);
+ *
+ * @param {goog.vec.Mat4.AnyType} mat The matrix.
+ * @param {number} angle The angle in radians.
+ * @return {goog.vec.Mat4.AnyType} return mat so that operations can be
+ *     chained.
+ */
+goog.vec.Mat4.rotateY = function(mat, angle) {
+  var m00 = mat[0], m10 = mat[1], m20 = mat[2], m30 = mat[3];
+  var m02 = mat[8], m12 = mat[9], m22 = mat[10], m32 = mat[11];
+
+  var c = Math.cos(angle);
+  var s = Math.sin(angle);
+
+  mat[0] = m00 * c + m02 * -s;
+  mat[1] = m10 * c + m12 * -s;
+  mat[2] = m20 * c + m22 * -s;
+  mat[3] = m30 * c + m32 * -s;
+  mat[8] = m00 * s + m02 * c;
+  mat[9] = m10 * s + m12 * c;
+  mat[10] = m20 * s + m22 * c;
+  mat[11] = m30 * s + m32 * c;
+
+  return mat;
+};
+
+
+/**
+ * Rotate the given matrix by angle about the z axis.  Equivalent to:
+ * goog.vec.Mat4.multMat(
+ *     mat,
+ *     goog.vec.Mat4.makeRotateZ(goog.vec.Mat4.create(), angle),
+ *     mat);
+ *
+ * @param {goog.vec.Mat4.AnyType} mat The matrix.
+ * @param {number} angle The angle in radians.
+ * @return {goog.vec.Mat4.AnyType} return mat so that operations can be
+ *     chained.
+ */
+goog.vec.Mat4.rotateZ = function(mat, angle) {
+  var m00 = mat[0], m10 = mat[1], m20 = mat[2], m30 = mat[3];
+  var m01 = mat[4], m11 = mat[5], m21 = mat[6], m31 = mat[7];
+
+  var c = Math.cos(angle);
+  var s = Math.sin(angle);
+
+  mat[0] = m00 * c + m01 * s;
+  mat[1] = m10 * c + m11 * s;
+  mat[2] = m20 * c + m21 * s;
+  mat[3] = m30 * c + m31 * s;
+  mat[4] = m00 * -s + m01 * c;
+  mat[5] = m10 * -s + m11 * c;
+  mat[6] = m20 * -s + m21 * c;
+  mat[7] = m30 * -s + m31 * c;
+
+  return mat;
+};
+
+
+/**
+ * Retrieves the translation component of the transformation matrix.
+ *
+ * @param {goog.vec.Mat4.AnyType} mat The transformation matrix.
+ * @param {goog.vec.Vec3.AnyType} translation The vector for storing the
+ *     result.
+ * @return {goog.vec.Mat4.AnyType} return mat so that operations can be
+ *     chained.
+ */
+goog.vec.Mat4.getTranslation = function(mat, translation) {
+  translation[0] = mat[12];
+  translation[1] = mat[13];
+  translation[2] = mat[14];
+  return translation;
+};
+
+
+/**
+ * @type {!Array<!goog.vec.Vec3.Type>}
+ * @private
+ */
+goog.vec.Mat4.tmpVec3_ =
+    [goog.vec.Vec3.createFloat64(), goog.vec.Vec3.createFloat64()];
+
+
+/**
+ * @type {!Array<!goog.vec.Vec4.Type>}
+ * @private
+ */
+goog.vec.Mat4.tmpVec4_ = [
+  goog.vec.Vec4.createFloat64(), goog.vec.Vec4.createFloat64(),
+  goog.vec.Vec4.createFloat64()
+];
+
+
+/**
+ * @type {!Array<!goog.vec.Mat4.Type>}
+ * @private
+ */
+goog.vec.Mat4.tmpMat4_ = [goog.vec.Mat4.createFloat64()];
+
+goog.provide('ol.geom.flat.transform');
+
+goog.require('goog.vec.Mat4');
+
+
+/**
+ * @param {Array.<number>} flatCoordinates Flat coordinates.
+ * @param {number} offset Offset.
+ * @param {number} end End.
+ * @param {number} stride Stride.
+ * @param {goog.vec.Mat4.Number} transform Transform.
+ * @param {Array.<number>=} opt_dest Destination.
+ * @return {Array.<number>} Transformed coordinates.
+ */
+ol.geom.flat.transform.transform2D = function(flatCoordinates, offset, end, stride, transform, opt_dest) {
+  var m00 = goog.vec.Mat4.getElement(transform, 0, 0);
+  var m10 = goog.vec.Mat4.getElement(transform, 1, 0);
+  var m01 = goog.vec.Mat4.getElement(transform, 0, 1);
+  var m11 = goog.vec.Mat4.getElement(transform, 1, 1);
+  var m03 = goog.vec.Mat4.getElement(transform, 0, 3);
+  var m13 = goog.vec.Mat4.getElement(transform, 1, 3);
+  var dest = opt_dest ? opt_dest : [];
+  var i = 0;
+  var j;
+  for (j = offset; j < end; j += stride) {
+    var x = flatCoordinates[j];
+    var y = flatCoordinates[j + 1];
+    dest[i++] = m00 * x + m01 * y + m03;
+    dest[i++] = m10 * x + m11 * y + m13;
+  }
+  if (opt_dest && dest.length != i) {
+    dest.length = i;
+  }
+  return dest;
+};
+
+
+/**
+ * @param {Array.<number>} flatCoordinates Flat coordinates.
+ * @param {number} offset Offset.
+ * @param {number} end End.
+ * @param {number} stride Stride.
+ * @param {number} angle Angle.
+ * @param {Array.<number>} anchor Rotation anchor point.
+ * @param {Array.<number>=} opt_dest Destination.
+ * @return {Array.<number>} Transformed coordinates.
+ */
+ol.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;
+};
+
+
+/**
+ * @param {Array.<number>} flatCoordinates Flat coordinates.
+ * @param {number} offset Offset.
+ * @param {number} end End.
+ * @param {number} stride Stride.
+ * @param {number} deltaX Delta X.
+ * @param {number} deltaY Delta Y.
+ * @param {Array.<number>=} opt_dest Destination.
+ * @return {Array.<number>} Transformed coordinates.
+ */
+ol.geom.flat.transform.translate = function(flatCoordinates, offset, end, stride, deltaX, deltaY, opt_dest) {
+  var dest = opt_dest ? opt_dest : [];
+  var i = 0;
+  var j, k;
+  for (j = offset; j < end; j += stride) {
+    dest[i++] = flatCoordinates[j] + deltaX;
+    dest[i++] = flatCoordinates[j + 1] + deltaY;
+    for (k = j + 2; k < j + stride; ++k) {
+      dest[i++] = flatCoordinates[k];
+    }
+  }
+  if (opt_dest && dest.length != i) {
+    dest.length = i;
+  }
+  return dest;
+};
+
+goog.provide('ol.geom.SimpleGeometry');
+
+goog.require('goog.asserts');
+goog.require('ol.functions');
+goog.require('ol.extent');
+goog.require('ol.geom.Geometry');
+goog.require('ol.geom.GeometryLayout');
+goog.require('ol.geom.flat.transform');
+goog.require('ol.object');
+
+
+/**
+ * @classdesc
+ * Abstract base class; only used for creating subclasses; do not instantiate
+ * in apps, as cannot be rendered.
+ *
+ * @constructor
+ * @extends {ol.geom.Geometry}
+ * @api stable
+ */
+ol.geom.SimpleGeometry = function() {
+
+  ol.geom.Geometry.call(this);
+
+  /**
+   * @protected
+   * @type {ol.geom.GeometryLayout}
+   */
+  this.layout = ol.geom.GeometryLayout.XY;
+
+  /**
+   * @protected
+   * @type {number}
+   */
+  this.stride = 2;
+
+  /**
+   * @protected
+   * @type {Array.<number>}
+   */
+  this.flatCoordinates = null;
+
+};
+ol.inherits(ol.geom.SimpleGeometry, ol.geom.Geometry);
+
+
+/**
+ * @param {number} stride Stride.
+ * @private
+ * @return {ol.geom.GeometryLayout} layout Layout.
+ */
+ol.geom.SimpleGeometry.getLayoutForStride_ = function(stride) {
+  if (stride == 2) {
+    return ol.geom.GeometryLayout.XY;
+  } else if (stride == 3) {
+    return ol.geom.GeometryLayout.XYZ;
+  } else if (stride == 4) {
+    return ol.geom.GeometryLayout.XYZM;
+  } else {
+    goog.asserts.fail('unsupported stride: ' + stride);
+  }
+};
+
+
+/**
+ * @param {ol.geom.GeometryLayout} layout Layout.
+ * @return {number} Stride.
+ */
+ol.geom.SimpleGeometry.getStrideForLayout = function(layout) {
+  if (layout == ol.geom.GeometryLayout.XY) {
+    return 2;
+  } else if (layout == ol.geom.GeometryLayout.XYZ) {
+    return 3;
+  } else if (layout == ol.geom.GeometryLayout.XYM) {
+    return 3;
+  } else if (layout == ol.geom.GeometryLayout.XYZM) {
+    return 4;
+  } else {
+    goog.asserts.fail('unsupported layout: ' + layout);
+  }
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.geom.SimpleGeometry.prototype.containsXY = ol.functions.FALSE;
+
+
+/**
+ * @inheritDoc
+ */
+ol.geom.SimpleGeometry.prototype.computeExtent = function(extent) {
+  return ol.extent.createOrUpdateFromFlatCoordinates(
+      this.flatCoordinates, 0, this.flatCoordinates.length, this.stride,
+      extent);
+};
+
+
+/**
+ * @return {Array} Coordinates.
+ */
+ol.geom.SimpleGeometry.prototype.getCoordinates = goog.abstractMethod;
+
+
+/**
+ * Return the first coordinate of the geometry.
+ * @return {ol.Coordinate} First coordinate.
+ * @api stable
+ */
+ol.geom.SimpleGeometry.prototype.getFirstCoordinate = function() {
+  return this.flatCoordinates.slice(0, this.stride);
+};
+
+
+/**
+ * @return {Array.<number>} Flat coordinates.
+ */
+ol.geom.SimpleGeometry.prototype.getFlatCoordinates = function() {
+  return this.flatCoordinates;
+};
+
+
+/**
+ * Return the last coordinate of the geometry.
+ * @return {ol.Coordinate} Last point.
+ * @api stable
+ */
+ol.geom.SimpleGeometry.prototype.getLastCoordinate = function() {
+  return this.flatCoordinates.slice(this.flatCoordinates.length - this.stride);
+};
+
+
+/**
+ * Return the {@link ol.geom.GeometryLayout layout} of the geometry.
+ * @return {ol.geom.GeometryLayout} Layout.
+ * @api stable
+ */
+ol.geom.SimpleGeometry.prototype.getLayout = function() {
+  return this.layout;
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.geom.SimpleGeometry.prototype.getSimplifiedGeometry = function(squaredTolerance) {
+  if (this.simplifiedGeometryRevision != this.getRevision()) {
+    ol.object.clear(this.simplifiedGeometryCache);
+    this.simplifiedGeometryMaxMinSquaredTolerance = 0;
+    this.simplifiedGeometryRevision = this.getRevision();
+  }
+  // If squaredTolerance is negative or if we know that simplification will not
+  // have any effect then just return this.
+  if (squaredTolerance < 0 ||
+      (this.simplifiedGeometryMaxMinSquaredTolerance !== 0 &&
+       squaredTolerance <= this.simplifiedGeometryMaxMinSquaredTolerance)) {
+    return this;
+  }
+  var key = squaredTolerance.toString();
+  if (this.simplifiedGeometryCache.hasOwnProperty(key)) {
+    return this.simplifiedGeometryCache[key];
+  } else {
+    var simplifiedGeometry =
+        this.getSimplifiedGeometryInternal(squaredTolerance);
+    var simplifiedFlatCoordinates = simplifiedGeometry.getFlatCoordinates();
+    if (simplifiedFlatCoordinates.length < this.flatCoordinates.length) {
+      this.simplifiedGeometryCache[key] = simplifiedGeometry;
+      return simplifiedGeometry;
+    } else {
+      // Simplification did not actually remove any coordinates.  We now know
+      // that any calls to getSimplifiedGeometry with a squaredTolerance less
+      // than or equal to the current squaredTolerance will also not have any
+      // effect.  This allows us to short circuit simplification (saving CPU
+      // cycles) and prevents the cache of simplified geometries from filling
+      // up with useless identical copies of this geometry (saving memory).
+      this.simplifiedGeometryMaxMinSquaredTolerance = squaredTolerance;
+      return this;
+    }
+  }
+};
+
+
+/**
+ * @param {number} squaredTolerance Squared tolerance.
+ * @return {ol.geom.SimpleGeometry} Simplified geometry.
+ * @protected
+ */
+ol.geom.SimpleGeometry.prototype.getSimplifiedGeometryInternal = function(squaredTolerance) {
+  return this;
+};
+
+
+/**
+ * @return {number} Stride.
+ */
+ol.geom.SimpleGeometry.prototype.getStride = function() {
+  return this.stride;
+};
+
+
+/**
+ * @param {ol.geom.GeometryLayout} layout Layout.
+ * @param {Array.<number>} flatCoordinates Flat coordinates.
+ * @protected
+ */
+ol.geom.SimpleGeometry.prototype.setFlatCoordinatesInternal = function(layout, flatCoordinates) {
+  this.stride = ol.geom.SimpleGeometry.getStrideForLayout(layout);
+  this.layout = layout;
+  this.flatCoordinates = flatCoordinates;
+};
+
+
+/**
+ * @param {Array} coordinates Coordinates.
+ * @param {ol.geom.GeometryLayout=} opt_layout Layout.
+ */
+ol.geom.SimpleGeometry.prototype.setCoordinates = goog.abstractMethod;
+
+
+/**
+ * @param {ol.geom.GeometryLayout|undefined} layout Layout.
+ * @param {Array} coordinates Coordinates.
+ * @param {number} nesting Nesting.
+ * @protected
+ */
+ol.geom.SimpleGeometry.prototype.setLayout = function(layout, coordinates, nesting) {
+  /** @type {number} */
+  var stride;
+  if (layout) {
+    stride = ol.geom.SimpleGeometry.getStrideForLayout(layout);
+  } else {
+    var i;
+    for (i = 0; i < nesting; ++i) {
+      if (coordinates.length === 0) {
+        this.layout = ol.geom.GeometryLayout.XY;
+        this.stride = 2;
+        return;
+      } else {
+        coordinates = /** @type {Array} */ (coordinates[0]);
+      }
+    }
+    stride = coordinates.length;
+    layout = ol.geom.SimpleGeometry.getLayoutForStride_(stride);
+  }
+  this.layout = layout;
+  this.stride = stride;
+};
+
+
+/**
+ * @inheritDoc
+ * @api stable
+ */
+ol.geom.SimpleGeometry.prototype.applyTransform = function(transformFn) {
+  if (this.flatCoordinates) {
+    transformFn(this.flatCoordinates, this.flatCoordinates, this.stride);
+    this.changed();
+  }
+};
+
+
+/**
+ * @inheritDoc
+ * @api
+ */
+ol.geom.SimpleGeometry.prototype.rotate = function(angle, anchor) {
+  var flatCoordinates = this.getFlatCoordinates();
+  if (flatCoordinates) {
+    var stride = this.getStride();
+    ol.geom.flat.transform.rotate(
+        flatCoordinates, 0, flatCoordinates.length,
+        stride, angle, anchor, flatCoordinates);
+    this.changed();
+  }
+};
+
+
+/**
+ * @inheritDoc
+ * @api stable
+ */
+ol.geom.SimpleGeometry.prototype.translate = function(deltaX, deltaY) {
+  var flatCoordinates = this.getFlatCoordinates();
+  if (flatCoordinates) {
+    var stride = this.getStride();
+    ol.geom.flat.transform.translate(
+        flatCoordinates, 0, flatCoordinates.length, stride,
+        deltaX, deltaY, flatCoordinates);
+    this.changed();
+  }
+};
+
+
+/**
+ * @param {ol.geom.SimpleGeometry} simpleGeometry Simple geometry.
+ * @param {goog.vec.Mat4.Number} transform Transform.
+ * @param {Array.<number>=} opt_dest Destination.
+ * @return {Array.<number>} Transformed flat coordinates.
+ */
+ol.geom.transformSimpleGeometry2D = function(simpleGeometry, transform, opt_dest) {
+  var flatCoordinates = simpleGeometry.getFlatCoordinates();
+  if (!flatCoordinates) {
+    return null;
+  } else {
+    var stride = simpleGeometry.getStride();
+    return ol.geom.flat.transform.transform2D(
+        flatCoordinates, 0, flatCoordinates.length, stride,
+        transform, opt_dest);
+  }
+};
+
+goog.provide('ol.geom.flat.area');
+
+
+/**
+ * @param {Array.<number>} flatCoordinates Flat coordinates.
+ * @param {number} offset Offset.
+ * @param {number} end End.
+ * @param {number} stride Stride.
+ * @return {number} Area.
+ */
+ol.geom.flat.area.linearRing = function(flatCoordinates, offset, end, stride) {
+  var twiceArea = 0;
+  var x1 = flatCoordinates[end - stride];
+  var y1 = flatCoordinates[end - stride + 1];
+  for (; offset < end; offset += stride) {
+    var x2 = flatCoordinates[offset];
+    var y2 = flatCoordinates[offset + 1];
+    twiceArea += y1 * x2 - x1 * y2;
+    x1 = x2;
+    y1 = y2;
+  }
+  return twiceArea / 2;
+};
+
+
+/**
+ * @param {Array.<number>} flatCoordinates Flat coordinates.
+ * @param {number} offset Offset.
+ * @param {Array.<number>} ends Ends.
+ * @param {number} stride Stride.
+ * @return {number} Area.
+ */
+ol.geom.flat.area.linearRings = function(flatCoordinates, offset, ends, stride) {
+  var area = 0;
+  var i, ii;
+  for (i = 0, ii = ends.length; i < ii; ++i) {
+    var end = ends[i];
+    area += ol.geom.flat.area.linearRing(flatCoordinates, offset, end, stride);
+    offset = end;
+  }
+  return area;
+};
+
+
+/**
+ * @param {Array.<number>} flatCoordinates Flat coordinates.
+ * @param {number} offset Offset.
+ * @param {Array.<Array.<number>>} endss Endss.
+ * @param {number} stride Stride.
+ * @return {number} Area.
+ */
+ol.geom.flat.area.linearRingss = function(flatCoordinates, offset, endss, stride) {
+  var area = 0;
+  var i, ii;
+  for (i = 0, ii = endss.length; i < ii; ++i) {
+    var ends = endss[i];
+    area +=
+        ol.geom.flat.area.linearRings(flatCoordinates, offset, ends, stride);
+    offset = ends[ends.length - 1];
+  }
+  return area;
+};
+
+goog.provide('ol.geom.flat.closest');
+
+goog.require('goog.asserts');
+goog.require('ol.math');
+
+
+/**
+ * Returns the point on the 2D line segment flatCoordinates[offset1] to
+ * flatCoordinates[offset2] that is closest to the point (x, y).  Extra
+ * dimensions are linearly interpolated.
+ * @param {Array.<number>} flatCoordinates Flat coordinates.
+ * @param {number} offset1 Offset 1.
+ * @param {number} offset2 Offset 2.
+ * @param {number} stride Stride.
+ * @param {number} x X.
+ * @param {number} y Y.
+ * @param {Array.<number>} closestPoint Closest point.
+ */
+ol.geom.flat.closest.point = function(flatCoordinates, offset1, offset2, stride, x, y, closestPoint) {
+  var x1 = flatCoordinates[offset1];
+  var y1 = flatCoordinates[offset1 + 1];
+  var dx = flatCoordinates[offset2] - x1;
+  var dy = flatCoordinates[offset2 + 1] - y1;
+  var i, offset;
+  if (dx === 0 && dy === 0) {
+    offset = offset1;
+  } else {
+    var t = ((x - x1) * dx + (y - y1) * dy) / (dx * dx + dy * dy);
+    if (t > 1) {
+      offset = offset2;
+    } else if (t > 0) {
+      for (i = 0; i < stride; ++i) {
+        closestPoint[i] = ol.math.lerp(flatCoordinates[offset1 + i],
+            flatCoordinates[offset2 + i], t);
+      }
+      closestPoint.length = stride;
+      return;
+    } else {
+      offset = offset1;
+    }
+  }
+  for (i = 0; i < stride; ++i) {
+    closestPoint[i] = flatCoordinates[offset + i];
+  }
+  closestPoint.length = stride;
+};
+
+
+/**
+ * Return the squared of the largest distance between any pair of consecutive
+ * coordinates.
+ * @param {Array.<number>} flatCoordinates Flat coordinates.
+ * @param {number} offset Offset.
+ * @param {number} end End.
+ * @param {number} stride Stride.
+ * @param {number} maxSquaredDelta Max squared delta.
+ * @return {number} Max squared delta.
+ */
+ol.geom.flat.closest.getMaxSquaredDelta = function(flatCoordinates, offset, end, stride, maxSquaredDelta) {
+  var x1 = flatCoordinates[offset];
+  var y1 = flatCoordinates[offset + 1];
+  for (offset += stride; offset < end; offset += stride) {
+    var x2 = flatCoordinates[offset];
+    var y2 = flatCoordinates[offset + 1];
+    var squaredDelta = ol.math.squaredDistance(x1, y1, x2, y2);
+    if (squaredDelta > maxSquaredDelta) {
+      maxSquaredDelta = squaredDelta;
+    }
+    x1 = x2;
+    y1 = y2;
+  }
+  return maxSquaredDelta;
+};
+
+
+/**
+ * @param {Array.<number>} flatCoordinates Flat coordinates.
+ * @param {number} offset Offset.
+ * @param {Array.<number>} ends Ends.
+ * @param {number} stride Stride.
+ * @param {number} maxSquaredDelta Max squared delta.
+ * @return {number} Max squared delta.
+ */
+ol.geom.flat.closest.getsMaxSquaredDelta = function(flatCoordinates, offset, ends, stride, maxSquaredDelta) {
+  var i, ii;
+  for (i = 0, ii = ends.length; i < ii; ++i) {
+    var end = ends[i];
+    maxSquaredDelta = ol.geom.flat.closest.getMaxSquaredDelta(
+        flatCoordinates, offset, end, stride, maxSquaredDelta);
+    offset = end;
+  }
+  return maxSquaredDelta;
+};
+
+
+/**
+ * @param {Array.<number>} flatCoordinates Flat coordinates.
+ * @param {number} offset Offset.
+ * @param {Array.<Array.<number>>} endss Endss.
+ * @param {number} stride Stride.
+ * @param {number} maxSquaredDelta Max squared delta.
+ * @return {number} Max squared delta.
+ */
+ol.geom.flat.closest.getssMaxSquaredDelta = function(flatCoordinates, offset, endss, stride, maxSquaredDelta) {
+  var i, ii;
+  for (i = 0, ii = endss.length; i < ii; ++i) {
+    var ends = endss[i];
+    maxSquaredDelta = ol.geom.flat.closest.getsMaxSquaredDelta(
+        flatCoordinates, offset, ends, stride, maxSquaredDelta);
+    offset = ends[ends.length - 1];
+  }
+  return maxSquaredDelta;
+};
+
+
+/**
+ * @param {Array.<number>} flatCoordinates Flat coordinates.
+ * @param {number} offset Offset.
+ * @param {number} end End.
+ * @param {number} stride Stride.
+ * @param {number} maxDelta Max delta.
+ * @param {boolean} isRing Is ring.
+ * @param {number} x X.
+ * @param {number} y Y.
+ * @param {Array.<number>} closestPoint Closest point.
+ * @param {number} minSquaredDistance Minimum squared distance.
+ * @param {Array.<number>=} opt_tmpPoint Temporary point object.
+ * @return {number} Minimum squared distance.
+ */
+ol.geom.flat.closest.getClosestPoint = function(flatCoordinates, offset, end,
+    stride, maxDelta, isRing, x, y, closestPoint, minSquaredDistance,
+    opt_tmpPoint) {
+  if (offset == end) {
+    return minSquaredDistance;
+  }
+  var i, squaredDistance;
+  if (maxDelta === 0) {
+    // All points are identical, so just test the first point.
+    squaredDistance = ol.math.squaredDistance(
+        x, y, flatCoordinates[offset], flatCoordinates[offset + 1]);
+    if (squaredDistance < minSquaredDistance) {
+      for (i = 0; i < stride; ++i) {
+        closestPoint[i] = flatCoordinates[offset + i];
+      }
+      closestPoint.length = stride;
+      return squaredDistance;
+    } else {
+      return minSquaredDistance;
+    }
+  }
+  goog.asserts.assert(maxDelta > 0, 'maxDelta should be larger than 0');
+  var tmpPoint = opt_tmpPoint ? opt_tmpPoint : [NaN, NaN];
+  var index = offset + stride;
+  while (index < end) {
+    ol.geom.flat.closest.point(
+        flatCoordinates, index - stride, index, stride, x, y, tmpPoint);
+    squaredDistance = ol.math.squaredDistance(x, y, tmpPoint[0], tmpPoint[1]);
+    if (squaredDistance < minSquaredDistance) {
+      minSquaredDistance = squaredDistance;
+      for (i = 0; i < stride; ++i) {
+        closestPoint[i] = tmpPoint[i];
+      }
+      closestPoint.length = stride;
+      index += stride;
+    } else {
+      // Skip ahead multiple points, because we know that all the skipped
+      // points cannot be any closer than the closest point we have found so
+      // far.  We know this because we know how close the current point is, how
+      // close the closest point we have found so far is, and the maximum
+      // distance between consecutive points.  For example, if we're currently
+      // at distance 10, the best we've found so far is 3, and that the maximum
+      // distance between consecutive points is 2, then we'll need to skip at
+      // least (10 - 3) / 2 == 3 (rounded down) points to have any chance of
+      // finding a closer point.  We use Math.max(..., 1) to ensure that we
+      // always advance at least one point, to avoid an infinite loop.
+      index += stride * Math.max(
+          ((Math.sqrt(squaredDistance) -
+            Math.sqrt(minSquaredDistance)) / maxDelta) | 0, 1);
+    }
+  }
+  if (isRing) {
+    // Check the closing segment.
+    ol.geom.flat.closest.point(
+        flatCoordinates, end - stride, offset, stride, x, y, tmpPoint);
+    squaredDistance = ol.math.squaredDistance(x, y, tmpPoint[0], tmpPoint[1]);
+    if (squaredDistance < minSquaredDistance) {
+      minSquaredDistance = squaredDistance;
+      for (i = 0; i < stride; ++i) {
+        closestPoint[i] = tmpPoint[i];
+      }
+      closestPoint.length = stride;
+    }
+  }
+  return minSquaredDistance;
+};
+
+
+/**
+ * @param {Array.<number>} flatCoordinates Flat coordinates.
+ * @param {number} offset Offset.
+ * @param {Array.<number>} ends Ends.
+ * @param {number} stride Stride.
+ * @param {number} maxDelta Max delta.
+ * @param {boolean} isRing Is ring.
+ * @param {number} x X.
+ * @param {number} y Y.
+ * @param {Array.<number>} closestPoint Closest point.
+ * @param {number} minSquaredDistance Minimum squared distance.
+ * @param {Array.<number>=} opt_tmpPoint Temporary point object.
+ * @return {number} Minimum squared distance.
+ */
+ol.geom.flat.closest.getsClosestPoint = function(flatCoordinates, offset, ends,
+    stride, maxDelta, isRing, x, y, closestPoint, minSquaredDistance,
+    opt_tmpPoint) {
+  var tmpPoint = opt_tmpPoint ? opt_tmpPoint : [NaN, NaN];
+  var i, ii;
+  for (i = 0, ii = ends.length; i < ii; ++i) {
+    var end = ends[i];
+    minSquaredDistance = ol.geom.flat.closest.getClosestPoint(
+        flatCoordinates, offset, end, stride,
+        maxDelta, isRing, x, y, closestPoint, minSquaredDistance, tmpPoint);
+    offset = end;
+  }
+  return minSquaredDistance;
+};
+
+
+/**
+ * @param {Array.<number>} flatCoordinates Flat coordinates.
+ * @param {number} offset Offset.
+ * @param {Array.<Array.<number>>} endss Endss.
+ * @param {number} stride Stride.
+ * @param {number} maxDelta Max delta.
+ * @param {boolean} isRing Is ring.
+ * @param {number} x X.
+ * @param {number} y Y.
+ * @param {Array.<number>} closestPoint Closest point.
+ * @param {number} minSquaredDistance Minimum squared distance.
+ * @param {Array.<number>=} opt_tmpPoint Temporary point object.
+ * @return {number} Minimum squared distance.
+ */
+ol.geom.flat.closest.getssClosestPoint = function(flatCoordinates, offset,
+    endss, stride, maxDelta, isRing, x, y, closestPoint, minSquaredDistance,
+    opt_tmpPoint) {
+  var tmpPoint = opt_tmpPoint ? opt_tmpPoint : [NaN, NaN];
+  var i, ii;
+  for (i = 0, ii = endss.length; i < ii; ++i) {
+    var ends = endss[i];
+    minSquaredDistance = ol.geom.flat.closest.getsClosestPoint(
+        flatCoordinates, offset, ends, stride,
+        maxDelta, isRing, x, y, closestPoint, minSquaredDistance, tmpPoint);
+    offset = ends[ends.length - 1];
+  }
+  return minSquaredDistance;
+};
+
+goog.provide('ol.geom.flat.deflate');
+
+goog.require('goog.asserts');
+
+
+/**
+ * @param {Array.<number>} flatCoordinates Flat coordinates.
+ * @param {number} offset Offset.
+ * @param {ol.Coordinate} coordinate Coordinate.
+ * @param {number} stride Stride.
+ * @return {number} offset Offset.
+ */
+ol.geom.flat.deflate.coordinate = function(flatCoordinates, offset, coordinate, stride) {
+  goog.asserts.assert(coordinate.length == stride,
+      'length of the coordinate array should match stride');
+  var i, ii;
+  for (i = 0, ii = coordinate.length; i < ii; ++i) {
+    flatCoordinates[offset++] = coordinate[i];
+  }
+  return offset;
+};
+
+
+/**
+ * @param {Array.<number>} flatCoordinates Flat coordinates.
+ * @param {number} offset Offset.
+ * @param {Array.<ol.Coordinate>} coordinates Coordinates.
+ * @param {number} stride Stride.
+ * @return {number} offset Offset.
+ */
+ol.geom.flat.deflate.coordinates = function(flatCoordinates, offset, coordinates, stride) {
+  var i, ii;
+  for (i = 0, ii = coordinates.length; i < ii; ++i) {
+    var coordinate = coordinates[i];
+    goog.asserts.assert(coordinate.length == stride,
+        'length of coordinate array should match stride');
+    var j;
+    for (j = 0; j < stride; ++j) {
+      flatCoordinates[offset++] = coordinate[j];
+    }
+  }
+  return offset;
+};
+
+
+/**
+ * @param {Array.<number>} flatCoordinates Flat coordinates.
+ * @param {number} offset Offset.
+ * @param {Array.<Array.<ol.Coordinate>>} coordinatess Coordinatess.
+ * @param {number} stride Stride.
+ * @param {Array.<number>=} opt_ends Ends.
+ * @return {Array.<number>} Ends.
+ */
+ol.geom.flat.deflate.coordinatess = function(flatCoordinates, offset, coordinatess, stride, opt_ends) {
+  var ends = opt_ends ? opt_ends : [];
+  var i = 0;
+  var j, jj;
+  for (j = 0, jj = coordinatess.length; j < jj; ++j) {
+    var end = ol.geom.flat.deflate.coordinates(
+        flatCoordinates, offset, coordinatess[j], stride);
+    ends[i++] = end;
+    offset = end;
+  }
+  ends.length = i;
+  return ends;
+};
+
+
+/**
+ * @param {Array.<number>} flatCoordinates Flat coordinates.
+ * @param {number} offset Offset.
+ * @param {Array.<Array.<Array.<ol.Coordinate>>>} coordinatesss Coordinatesss.
+ * @param {number} stride Stride.
+ * @param {Array.<Array.<number>>=} opt_endss Endss.
+ * @return {Array.<Array.<number>>} Endss.
+ */
+ol.geom.flat.deflate.coordinatesss = function(flatCoordinates, offset, coordinatesss, stride, opt_endss) {
+  var endss = opt_endss ? opt_endss : [];
+  var i = 0;
+  var j, jj;
+  for (j = 0, jj = coordinatesss.length; j < jj; ++j) {
+    var ends = ol.geom.flat.deflate.coordinatess(
+        flatCoordinates, offset, coordinatesss[j], stride, endss[i]);
+    endss[i++] = ends;
+    offset = ends[ends.length - 1];
+  }
+  endss.length = i;
+  return endss;
+};
+
+goog.provide('ol.geom.flat.inflate');
+
+
+/**
+ * @param {Array.<number>} flatCoordinates Flat coordinates.
+ * @param {number} offset Offset.
+ * @param {number} end End.
+ * @param {number} stride Stride.
+ * @param {Array.<ol.Coordinate>=} opt_coordinates Coordinates.
+ * @return {Array.<ol.Coordinate>} Coordinates.
+ */
+ol.geom.flat.inflate.coordinates = function(flatCoordinates, offset, end, stride, opt_coordinates) {
+  var coordinates = opt_coordinates !== undefined ? opt_coordinates : [];
+  var i = 0;
+  var j;
+  for (j = offset; j < end; j += stride) {
+    coordinates[i++] = flatCoordinates.slice(j, j + stride);
+  }
+  coordinates.length = i;
+  return coordinates;
+};
+
+
+/**
+ * @param {Array.<number>} flatCoordinates Flat coordinates.
+ * @param {number} offset Offset.
+ * @param {Array.<number>} ends Ends.
+ * @param {number} stride Stride.
+ * @param {Array.<Array.<ol.Coordinate>>=} opt_coordinatess Coordinatess.
+ * @return {Array.<Array.<ol.Coordinate>>} Coordinatess.
+ */
+ol.geom.flat.inflate.coordinatess = function(flatCoordinates, offset, ends, stride, opt_coordinatess) {
+  var coordinatess = opt_coordinatess !== undefined ? opt_coordinatess : [];
+  var i = 0;
+  var j, jj;
+  for (j = 0, jj = ends.length; j < jj; ++j) {
+    var end = ends[j];
+    coordinatess[i++] = ol.geom.flat.inflate.coordinates(
+        flatCoordinates, offset, end, stride, coordinatess[i]);
+    offset = end;
+  }
+  coordinatess.length = i;
+  return coordinatess;
+};
+
+
+/**
+ * @param {Array.<number>} flatCoordinates Flat coordinates.
+ * @param {number} offset Offset.
+ * @param {Array.<Array.<number>>} endss Endss.
+ * @param {number} stride Stride.
+ * @param {Array.<Array.<Array.<ol.Coordinate>>>=} opt_coordinatesss
+ *     Coordinatesss.
+ * @return {Array.<Array.<Array.<ol.Coordinate>>>} Coordinatesss.
+ */
+ol.geom.flat.inflate.coordinatesss = function(flatCoordinates, offset, endss, stride, opt_coordinatesss) {
+  var coordinatesss = opt_coordinatesss !== undefined ? opt_coordinatesss : [];
+  var i = 0;
+  var j, jj;
+  for (j = 0, jj = endss.length; j < jj; ++j) {
+    var ends = endss[j];
+    coordinatesss[i++] = ol.geom.flat.inflate.coordinatess(
+        flatCoordinates, offset, ends, stride, coordinatesss[i]);
+    offset = ends[ends.length - 1];
+  }
+  coordinatesss.length = i;
+  return coordinatesss;
+};
+
+// Based on simplify-js https://github.com/mourner/simplify-js
+// Copyright (c) 2012, Vladimir Agafonkin
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+//    1. Redistributions of source code must retain the above copyright notice,
+//       this list of conditions and the following disclaimer.
+//
+//    2. Redistributions in binary form must reproduce the above copyright
+//       notice, this list of conditions and the following disclaimer in the
+//       documentation and/or other materials provided with the distribution.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+// POSSIBILITY OF SUCH DAMAGE.
+
+goog.provide('ol.geom.flat.simplify');
+
+goog.require('ol.math');
+
+
+/**
+ * @param {Array.<number>} flatCoordinates Flat coordinates.
+ * @param {number} offset Offset.
+ * @param {number} end End.
+ * @param {number} stride Stride.
+ * @param {number} squaredTolerance Squared tolerance.
+ * @param {boolean} highQuality Highest quality.
+ * @param {Array.<number>=} opt_simplifiedFlatCoordinates Simplified flat
+ *     coordinates.
+ * @return {Array.<number>} Simplified line string.
+ */
+ol.geom.flat.simplify.lineString = function(flatCoordinates, offset, end,
+    stride, squaredTolerance, highQuality, opt_simplifiedFlatCoordinates) {
+  var simplifiedFlatCoordinates = opt_simplifiedFlatCoordinates !== undefined ?
+      opt_simplifiedFlatCoordinates : [];
+  if (!highQuality) {
+    end = ol.geom.flat.simplify.radialDistance(flatCoordinates, offset, end,
+        stride, squaredTolerance,
+        simplifiedFlatCoordinates, 0);
+    flatCoordinates = simplifiedFlatCoordinates;
+    offset = 0;
+    stride = 2;
+  }
+  simplifiedFlatCoordinates.length = ol.geom.flat.simplify.douglasPeucker(
+      flatCoordinates, offset, end, stride, squaredTolerance,
+      simplifiedFlatCoordinates, 0);
+  return simplifiedFlatCoordinates;
+};
+
+
+/**
+ * @param {Array.<number>} flatCoordinates Flat coordinates.
+ * @param {number} offset Offset.
+ * @param {number} end End.
+ * @param {number} stride Stride.
+ * @param {number} squaredTolerance Squared tolerance.
+ * @param {Array.<number>} simplifiedFlatCoordinates Simplified flat
+ *     coordinates.
+ * @param {number} simplifiedOffset Simplified offset.
+ * @return {number} Simplified offset.
+ */
+ol.geom.flat.simplify.douglasPeucker = function(flatCoordinates, offset, end,
+    stride, squaredTolerance, simplifiedFlatCoordinates, simplifiedOffset) {
+  var n = (end - offset) / stride;
+  if (n < 3) {
+    for (; offset < end; offset += stride) {
+      simplifiedFlatCoordinates[simplifiedOffset++] =
+          flatCoordinates[offset];
+      simplifiedFlatCoordinates[simplifiedOffset++] =
+          flatCoordinates[offset + 1];
+    }
+    return simplifiedOffset;
+  }
+  /** @type {Array.<number>} */
+  var markers = new Array(n);
+  markers[0] = 1;
+  markers[n - 1] = 1;
+  /** @type {Array.<number>} */
+  var stack = [offset, end - stride];
+  var index = 0;
+  var i;
+  while (stack.length > 0) {
+    var last = stack.pop();
+    var first = stack.pop();
+    var maxSquaredDistance = 0;
+    var x1 = flatCoordinates[first];
+    var y1 = flatCoordinates[first + 1];
+    var x2 = flatCoordinates[last];
+    var y2 = flatCoordinates[last + 1];
+    for (i = first + stride; i < last; i += stride) {
+      var x = flatCoordinates[i];
+      var y = flatCoordinates[i + 1];
+      var squaredDistance = ol.math.squaredSegmentDistance(
+          x, y, x1, y1, x2, y2);
+      if (squaredDistance > maxSquaredDistance) {
+        index = i;
+        maxSquaredDistance = squaredDistance;
+      }
+    }
+    if (maxSquaredDistance > squaredTolerance) {
+      markers[(index - offset) / stride] = 1;
+      if (first + stride < index) {
+        stack.push(first, index);
+      }
+      if (index + stride < last) {
+        stack.push(index, last);
+      }
+    }
+  }
+  for (i = 0; i < n; ++i) {
+    if (markers[i]) {
+      simplifiedFlatCoordinates[simplifiedOffset++] =
+          flatCoordinates[offset + i * stride];
+      simplifiedFlatCoordinates[simplifiedOffset++] =
+          flatCoordinates[offset + i * stride + 1];
+    }
+  }
+  return simplifiedOffset;
+};
+
+
+/**
+ * @param {Array.<number>} flatCoordinates Flat coordinates.
+ * @param {number} offset Offset.
+ * @param {Array.<number>} ends Ends.
+ * @param {number} stride Stride.
+ * @param {number} squaredTolerance Squared tolerance.
+ * @param {Array.<number>} simplifiedFlatCoordinates Simplified flat
+ *     coordinates.
+ * @param {number} simplifiedOffset Simplified offset.
+ * @param {Array.<number>} simplifiedEnds Simplified ends.
+ * @return {number} Simplified offset.
+ */
+ol.geom.flat.simplify.douglasPeuckers = function(flatCoordinates, offset,
+    ends, stride, squaredTolerance, simplifiedFlatCoordinates,
+    simplifiedOffset, simplifiedEnds) {
+  var i, ii;
+  for (i = 0, ii = ends.length; i < ii; ++i) {
+    var end = ends[i];
+    simplifiedOffset = ol.geom.flat.simplify.douglasPeucker(
+        flatCoordinates, offset, end, stride, squaredTolerance,
+        simplifiedFlatCoordinates, simplifiedOffset);
+    simplifiedEnds.push(simplifiedOffset);
+    offset = end;
+  }
+  return simplifiedOffset;
+};
+
+
+/**
+ * @param {Array.<number>} flatCoordinates Flat coordinates.
+ * @param {number} offset Offset.
+ * @param {Array.<Array.<number>>} endss Endss.
+ * @param {number} stride Stride.
+ * @param {number} squaredTolerance Squared tolerance.
+ * @param {Array.<number>} simplifiedFlatCoordinates Simplified flat
+ *     coordinates.
+ * @param {number} simplifiedOffset Simplified offset.
+ * @param {Array.<Array.<number>>} simplifiedEndss Simplified endss.
+ * @return {number} Simplified offset.
+ */
+ol.geom.flat.simplify.douglasPeuckerss = function(
+    flatCoordinates, offset, endss, stride, squaredTolerance,
+    simplifiedFlatCoordinates, simplifiedOffset, simplifiedEndss) {
+  var i, ii;
+  for (i = 0, ii = endss.length; i < ii; ++i) {
+    var ends = endss[i];
+    var simplifiedEnds = [];
+    simplifiedOffset = ol.geom.flat.simplify.douglasPeuckers(
+        flatCoordinates, offset, ends, stride, squaredTolerance,
+        simplifiedFlatCoordinates, simplifiedOffset, simplifiedEnds);
+    simplifiedEndss.push(simplifiedEnds);
+    offset = ends[ends.length - 1];
+  }
+  return simplifiedOffset;
+};
+
+
+/**
+ * @param {Array.<number>} flatCoordinates Flat coordinates.
+ * @param {number} offset Offset.
+ * @param {number} end End.
+ * @param {number} stride Stride.
+ * @param {number} squaredTolerance Squared tolerance.
+ * @param {Array.<number>} simplifiedFlatCoordinates Simplified flat
+ *     coordinates.
+ * @param {number} simplifiedOffset Simplified offset.
+ * @return {number} Simplified offset.
+ */
+ol.geom.flat.simplify.radialDistance = function(flatCoordinates, offset, end,
+    stride, squaredTolerance, simplifiedFlatCoordinates, simplifiedOffset) {
+  if (end <= offset + stride) {
+    // zero or one point, no simplification possible, so copy and return
+    for (; offset < end; offset += stride) {
+      simplifiedFlatCoordinates[simplifiedOffset++] = flatCoordinates[offset];
+      simplifiedFlatCoordinates[simplifiedOffset++] =
+          flatCoordinates[offset + 1];
+    }
+    return simplifiedOffset;
+  }
+  var x1 = flatCoordinates[offset];
+  var y1 = flatCoordinates[offset + 1];
+  // copy first point
+  simplifiedFlatCoordinates[simplifiedOffset++] = x1;
+  simplifiedFlatCoordinates[simplifiedOffset++] = y1;
+  var x2 = x1;
+  var y2 = y1;
+  for (offset += stride; offset < end; offset += stride) {
+    x2 = flatCoordinates[offset];
+    y2 = flatCoordinates[offset + 1];
+    if (ol.math.squaredDistance(x1, y1, x2, y2) > squaredTolerance) {
+      // copy point at offset
+      simplifiedFlatCoordinates[simplifiedOffset++] = x2;
+      simplifiedFlatCoordinates[simplifiedOffset++] = y2;
+      x1 = x2;
+      y1 = y2;
+    }
+  }
+  if (x2 != x1 || y2 != y1) {
+    // copy last point
+    simplifiedFlatCoordinates[simplifiedOffset++] = x2;
+    simplifiedFlatCoordinates[simplifiedOffset++] = y2;
+  }
+  return simplifiedOffset;
+};
+
+
+/**
+ * @param {number} value Value.
+ * @param {number} tolerance Tolerance.
+ * @return {number} Rounded value.
+ */
+ol.geom.flat.simplify.snap = function(value, tolerance) {
+  return tolerance * Math.round(value / tolerance);
+};
+
+
+/**
+ * Simplifies a line string using an algorithm designed by Tim Schaub.
+ * Coordinates are snapped to the nearest value in a virtual grid and
+ * consecutive duplicate coordinates are discarded.  This effectively preserves
+ * topology as the simplification of any subsection of a line string is
+ * independent of the rest of the line string.  This means that, for examples,
+ * the common edge between two polygons will be simplified to the same line
+ * string independently in both polygons.  This implementation uses a single
+ * pass over the coordinates and eliminates intermediate collinear points.
+ * @param {Array.<number>} flatCoordinates Flat coordinates.
+ * @param {number} offset Offset.
+ * @param {number} end End.
+ * @param {number} stride Stride.
+ * @param {number} tolerance Tolerance.
+ * @param {Array.<number>} simplifiedFlatCoordinates Simplified flat
+ *     coordinates.
+ * @param {number} simplifiedOffset Simplified offset.
+ * @return {number} Simplified offset.
+ */
+ol.geom.flat.simplify.quantize = function(flatCoordinates, offset, end, stride,
+    tolerance, simplifiedFlatCoordinates, simplifiedOffset) {
+  // do nothing if the line is empty
+  if (offset == end) {
+    return simplifiedOffset;
+  }
+  // snap the first coordinate (P1)
+  var x1 = ol.geom.flat.simplify.snap(flatCoordinates[offset], tolerance);
+  var y1 = ol.geom.flat.simplify.snap(flatCoordinates[offset + 1], tolerance);
+  offset += stride;
+  // add the first coordinate to the output
+  simplifiedFlatCoordinates[simplifiedOffset++] = x1;
+  simplifiedFlatCoordinates[simplifiedOffset++] = y1;
+  // find the next coordinate that does not snap to the same value as the first
+  // coordinate (P2)
+  var x2, y2;
+  do {
+    x2 = ol.geom.flat.simplify.snap(flatCoordinates[offset], tolerance);
+    y2 = ol.geom.flat.simplify.snap(flatCoordinates[offset + 1], tolerance);
+    offset += stride;
+    if (offset == end) {
+      // all coordinates snap to the same value, the line collapses to a point
+      // push the last snapped value anyway to ensure that the output contains
+      // at least two points
+      // FIXME should we really return at least two points anyway?
+      simplifiedFlatCoordinates[simplifiedOffset++] = x2;
+      simplifiedFlatCoordinates[simplifiedOffset++] = y2;
+      return simplifiedOffset;
+    }
+  } while (x2 == x1 && y2 == y1);
+  while (offset < end) {
+    var x3, y3;
+    // snap the next coordinate (P3)
+    x3 = ol.geom.flat.simplify.snap(flatCoordinates[offset], tolerance);
+    y3 = ol.geom.flat.simplify.snap(flatCoordinates[offset + 1], tolerance);
+    offset += stride;
+    // skip P3 if it is equal to P2
+    if (x3 == x2 && y3 == y2) {
+      continue;
+    }
+    // calculate the delta between P1 and P2
+    var dx1 = x2 - x1;
+    var dy1 = y2 - y1;
+    // calculate the delta between P3 and P1
+    var dx2 = x3 - x1;
+    var dy2 = y3 - y1;
+    // if P1, P2, and P3 are colinear and P3 is further from P1 than P2 is from
+    // P1 in the same direction then P2 is on the straight line between P1 and
+    // P3
+    if ((dx1 * dy2 == dy1 * dx2) &&
+        ((dx1 < 0 && dx2 < dx1) || dx1 == dx2 || (dx1 > 0 && dx2 > dx1)) &&
+        ((dy1 < 0 && dy2 < dy1) || dy1 == dy2 || (dy1 > 0 && dy2 > dy1))) {
+      // discard P2 and set P2 = P3
+      x2 = x3;
+      y2 = y3;
+      continue;
+    }
+    // either P1, P2, and P3 are not colinear, or they are colinear but P3 is
+    // between P3 and P1 or on the opposite half of the line to P2.  add P2,
+    // and continue with P1 = P2 and P2 = P3
+    simplifiedFlatCoordinates[simplifiedOffset++] = x2;
+    simplifiedFlatCoordinates[simplifiedOffset++] = y2;
+    x1 = x2;
+    y1 = y2;
+    x2 = x3;
+    y2 = y3;
+  }
+  // add the last point (P2)
+  simplifiedFlatCoordinates[simplifiedOffset++] = x2;
+  simplifiedFlatCoordinates[simplifiedOffset++] = y2;
+  return simplifiedOffset;
+};
+
+
+/**
+ * @param {Array.<number>} flatCoordinates Flat coordinates.
+ * @param {number} offset Offset.
+ * @param {Array.<number>} ends Ends.
+ * @param {number} stride Stride.
+ * @param {number} tolerance Tolerance.
+ * @param {Array.<number>} simplifiedFlatCoordinates Simplified flat
+ *     coordinates.
+ * @param {number} simplifiedOffset Simplified offset.
+ * @param {Array.<number>} simplifiedEnds Simplified ends.
+ * @return {number} Simplified offset.
+ */
+ol.geom.flat.simplify.quantizes = function(
+    flatCoordinates, offset, ends, stride,
+    tolerance,
+    simplifiedFlatCoordinates, simplifiedOffset, simplifiedEnds) {
+  var i, ii;
+  for (i = 0, ii = ends.length; i < ii; ++i) {
+    var end = ends[i];
+    simplifiedOffset = ol.geom.flat.simplify.quantize(
+        flatCoordinates, offset, end, stride,
+        tolerance,
+        simplifiedFlatCoordinates, simplifiedOffset);
+    simplifiedEnds.push(simplifiedOffset);
+    offset = end;
+  }
+  return simplifiedOffset;
+};
+
+
+/**
+ * @param {Array.<number>} flatCoordinates Flat coordinates.
+ * @param {number} offset Offset.
+ * @param {Array.<Array.<number>>} endss Endss.
+ * @param {number} stride Stride.
+ * @param {number} tolerance Tolerance.
+ * @param {Array.<number>} simplifiedFlatCoordinates Simplified flat
+ *     coordinates.
+ * @param {number} simplifiedOffset Simplified offset.
+ * @param {Array.<Array.<number>>} simplifiedEndss Simplified endss.
+ * @return {number} Simplified offset.
+ */
+ol.geom.flat.simplify.quantizess = function(
+    flatCoordinates, offset, endss, stride,
+    tolerance,
+    simplifiedFlatCoordinates, simplifiedOffset, simplifiedEndss) {
+  var i, ii;
+  for (i = 0, ii = endss.length; i < ii; ++i) {
+    var ends = endss[i];
+    var simplifiedEnds = [];
+    simplifiedOffset = ol.geom.flat.simplify.quantizes(
+        flatCoordinates, offset, ends, stride,
+        tolerance,
+        simplifiedFlatCoordinates, simplifiedOffset, simplifiedEnds);
+    simplifiedEndss.push(simplifiedEnds);
+    offset = ends[ends.length - 1];
+  }
+  return simplifiedOffset;
+};
+
+goog.provide('ol.geom.LinearRing');
+
+goog.require('ol.extent');
+goog.require('ol.geom.GeometryLayout');
+goog.require('ol.geom.GeometryType');
+goog.require('ol.geom.SimpleGeometry');
+goog.require('ol.geom.flat.area');
+goog.require('ol.geom.flat.closest');
+goog.require('ol.geom.flat.deflate');
+goog.require('ol.geom.flat.inflate');
+goog.require('ol.geom.flat.simplify');
+
+
+/**
+ * @classdesc
+ * Linear ring geometry. Only used as part of polygon; cannot be rendered
+ * on its own.
+ *
+ * @constructor
+ * @extends {ol.geom.SimpleGeometry}
+ * @param {Array.<ol.Coordinate>} coordinates Coordinates.
+ * @param {ol.geom.GeometryLayout=} opt_layout Layout.
+ * @api stable
+ */
+ol.geom.LinearRing = function(coordinates, opt_layout) {
+
+  ol.geom.SimpleGeometry.call(this);
+
+  /**
+   * @private
+   * @type {number}
+   */
+  this.maxDelta_ = -1;
+
+  /**
+   * @private
+   * @type {number}
+   */
+  this.maxDeltaRevision_ = -1;
+
+  this.setCoordinates(coordinates, opt_layout);
+
+};
+ol.inherits(ol.geom.LinearRing, ol.geom.SimpleGeometry);
+
+
+/**
+ * Make a complete copy of the geometry.
+ * @return {!ol.geom.LinearRing} Clone.
+ * @api stable
+ */
+ol.geom.LinearRing.prototype.clone = function() {
+  var linearRing = new ol.geom.LinearRing(null);
+  linearRing.setFlatCoordinates(this.layout, this.flatCoordinates.slice());
+  return linearRing;
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.geom.LinearRing.prototype.closestPointXY = function(x, y, closestPoint, minSquaredDistance) {
+  if (minSquaredDistance <
+      ol.extent.closestSquaredDistanceXY(this.getExtent(), x, y)) {
+    return minSquaredDistance;
+  }
+  if (this.maxDeltaRevision_ != this.getRevision()) {
+    this.maxDelta_ = Math.sqrt(ol.geom.flat.closest.getMaxSquaredDelta(
+        this.flatCoordinates, 0, this.flatCoordinates.length, this.stride, 0));
+    this.maxDeltaRevision_ = this.getRevision();
+  }
+  return ol.geom.flat.closest.getClosestPoint(
+      this.flatCoordinates, 0, this.flatCoordinates.length, this.stride,
+      this.maxDelta_, true, x, y, closestPoint, minSquaredDistance);
+};
+
+
+/**
+ * Return the area of the linear ring on projected plane.
+ * @return {number} Area (on projected plane).
+ * @api stable
+ */
+ol.geom.LinearRing.prototype.getArea = function() {
+  return ol.geom.flat.area.linearRing(
+      this.flatCoordinates, 0, this.flatCoordinates.length, this.stride);
+};
+
+
+/**
+ * Return the coordinates of the linear ring.
+ * @return {Array.<ol.Coordinate>} Coordinates.
+ * @api stable
+ */
+ol.geom.LinearRing.prototype.getCoordinates = function() {
+  return ol.geom.flat.inflate.coordinates(
+      this.flatCoordinates, 0, this.flatCoordinates.length, this.stride);
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.geom.LinearRing.prototype.getSimplifiedGeometryInternal = function(squaredTolerance) {
+  var simplifiedFlatCoordinates = [];
+  simplifiedFlatCoordinates.length = ol.geom.flat.simplify.douglasPeucker(
+      this.flatCoordinates, 0, this.flatCoordinates.length, this.stride,
+      squaredTolerance, simplifiedFlatCoordinates, 0);
+  var simplifiedLinearRing = new ol.geom.LinearRing(null);
+  simplifiedLinearRing.setFlatCoordinates(
+      ol.geom.GeometryLayout.XY, simplifiedFlatCoordinates);
+  return simplifiedLinearRing;
+};
+
+
+/**
+ * @inheritDoc
+ * @api stable
+ */
+ol.geom.LinearRing.prototype.getType = function() {
+  return ol.geom.GeometryType.LINEAR_RING;
+};
+
+
+/**
+ * Set the coordinates of the linear ring.
+ * @param {Array.<ol.Coordinate>} coordinates Coordinates.
+ * @param {ol.geom.GeometryLayout=} opt_layout Layout.
+ * @api stable
+ */
+ol.geom.LinearRing.prototype.setCoordinates = function(coordinates, opt_layout) {
+  if (!coordinates) {
+    this.setFlatCoordinates(ol.geom.GeometryLayout.XY, null);
+  } else {
+    this.setLayout(opt_layout, coordinates, 1);
+    if (!this.flatCoordinates) {
+      this.flatCoordinates = [];
+    }
+    this.flatCoordinates.length = ol.geom.flat.deflate.coordinates(
+        this.flatCoordinates, 0, coordinates, this.stride);
+    this.changed();
+  }
+};
+
+
+/**
+ * @param {ol.geom.GeometryLayout} layout Layout.
+ * @param {Array.<number>} flatCoordinates Flat coordinates.
+ */
+ol.geom.LinearRing.prototype.setFlatCoordinates = function(layout, flatCoordinates) {
+  this.setFlatCoordinatesInternal(layout, flatCoordinates);
+  this.changed();
+};
+
+goog.provide('ol.geom.Point');
+
+goog.require('ol.extent');
+goog.require('ol.geom.GeometryLayout');
+goog.require('ol.geom.GeometryType');
+goog.require('ol.geom.SimpleGeometry');
+goog.require('ol.geom.flat.deflate');
+goog.require('ol.math');
+
+
+/**
+ * @classdesc
+ * Point geometry.
+ *
+ * @constructor
+ * @extends {ol.geom.SimpleGeometry}
+ * @param {ol.Coordinate} coordinates Coordinates.
+ * @param {ol.geom.GeometryLayout=} opt_layout Layout.
+ * @api stable
+ */
+ol.geom.Point = function(coordinates, opt_layout) {
+  ol.geom.SimpleGeometry.call(this);
+  this.setCoordinates(coordinates, opt_layout);
+};
+ol.inherits(ol.geom.Point, ol.geom.SimpleGeometry);
+
+
+/**
+ * Make a complete copy of the geometry.
+ * @return {!ol.geom.Point} Clone.
+ * @api stable
+ */
+ol.geom.Point.prototype.clone = function() {
+  var point = new ol.geom.Point(null);
+  point.setFlatCoordinates(this.layout, this.flatCoordinates.slice());
+  return point;
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.geom.Point.prototype.closestPointXY = function(x, y, closestPoint, minSquaredDistance) {
+  var flatCoordinates = this.flatCoordinates;
+  var squaredDistance = ol.math.squaredDistance(
+      x, y, flatCoordinates[0], flatCoordinates[1]);
+  if (squaredDistance < minSquaredDistance) {
+    var stride = this.stride;
+    var i;
+    for (i = 0; i < stride; ++i) {
+      closestPoint[i] = flatCoordinates[i];
+    }
+    closestPoint.length = stride;
+    return squaredDistance;
+  } else {
+    return minSquaredDistance;
+  }
+};
+
+
+/**
+ * Return the coordinate of the point.
+ * @return {ol.Coordinate} Coordinates.
+ * @api stable
+ */
+ol.geom.Point.prototype.getCoordinates = function() {
+  return !this.flatCoordinates ? [] : this.flatCoordinates.slice();
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.geom.Point.prototype.computeExtent = function(extent) {
+  return ol.extent.createOrUpdateFromCoordinate(this.flatCoordinates, extent);
+};
+
+
+/**
+ * @inheritDoc
+ * @api stable
+ */
+ol.geom.Point.prototype.getType = function() {
+  return ol.geom.GeometryType.POINT;
+};
+
+
+/**
+ * @inheritDoc
+ * @api stable
+ */
+ol.geom.Point.prototype.intersectsExtent = function(extent) {
+  return ol.extent.containsXY(extent,
+      this.flatCoordinates[0], this.flatCoordinates[1]);
+};
+
+
+/**
+ * Set the coordinate of the point.
+ * @param {ol.Coordinate} coordinates Coordinates.
+ * @param {ol.geom.GeometryLayout=} opt_layout Layout.
+ * @api stable
+ */
+ol.geom.Point.prototype.setCoordinates = function(coordinates, opt_layout) {
+  if (!coordinates) {
+    this.setFlatCoordinates(ol.geom.GeometryLayout.XY, null);
+  } else {
+    this.setLayout(opt_layout, coordinates, 0);
+    if (!this.flatCoordinates) {
+      this.flatCoordinates = [];
+    }
+    this.flatCoordinates.length = ol.geom.flat.deflate.coordinate(
+        this.flatCoordinates, 0, coordinates, this.stride);
+    this.changed();
+  }
+};
+
+
+/**
+ * @param {ol.geom.GeometryLayout} layout Layout.
+ * @param {Array.<number>} flatCoordinates Flat coordinates.
+ */
+ol.geom.Point.prototype.setFlatCoordinates = function(layout, flatCoordinates) {
+  this.setFlatCoordinatesInternal(layout, flatCoordinates);
+  this.changed();
+};
+
+goog.provide('ol.geom.flat.contains');
+
+goog.require('goog.asserts');
+goog.require('ol.extent');
+
+
+/**
+ * @param {Array.<number>} flatCoordinates Flat coordinates.
+ * @param {number} offset Offset.
+ * @param {number} end End.
+ * @param {number} stride Stride.
+ * @param {ol.Extent} extent Extent.
+ * @return {boolean} Contains extent.
+ */
+ol.geom.flat.contains.linearRingContainsExtent = function(flatCoordinates, offset, end, stride, extent) {
+  var outside = ol.extent.forEachCorner(extent,
+      /**
+       * @param {ol.Coordinate} coordinate Coordinate.
+       * @return {boolean} Contains (x, y).
+       */
+      function(coordinate) {
+        return !ol.geom.flat.contains.linearRingContainsXY(flatCoordinates,
+            offset, end, stride, coordinate[0], coordinate[1]);
+      });
+  return !outside;
+};
+
+
+/**
+ * @param {Array.<number>} flatCoordinates Flat coordinates.
+ * @param {number} offset Offset.
+ * @param {number} end End.
+ * @param {number} stride Stride.
+ * @param {number} x X.
+ * @param {number} y Y.
+ * @return {boolean} Contains (x, y).
+ */
+ol.geom.flat.contains.linearRingContainsXY = function(flatCoordinates, offset, end, stride, x, y) {
+  // http://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html
+  var contains = false;
+  var x1 = flatCoordinates[end - stride];
+  var y1 = flatCoordinates[end - stride + 1];
+  for (; offset < end; offset += stride) {
+    var x2 = flatCoordinates[offset];
+    var y2 = flatCoordinates[offset + 1];
+    var intersect = ((y1 > y) != (y2 > y)) &&
+        (x < (x2 - x1) * (y - y1) / (y2 - y1) + x1);
+    if (intersect) {
+      contains = !contains;
+    }
+    x1 = x2;
+    y1 = y2;
+  }
+  return contains;
+};
+
+
+/**
+ * @param {Array.<number>} flatCoordinates Flat coordinates.
+ * @param {number} offset Offset.
+ * @param {Array.<number>} ends Ends.
+ * @param {number} stride Stride.
+ * @param {number} x X.
+ * @param {number} y Y.
+ * @return {boolean} Contains (x, y).
+ */
+ol.geom.flat.contains.linearRingsContainsXY = function(flatCoordinates, offset, ends, stride, x, y) {
+  goog.asserts.assert(ends.length > 0, 'ends should not be an empty array');
+  if (ends.length === 0) {
+    return false;
+  }
+  if (!ol.geom.flat.contains.linearRingContainsXY(
+      flatCoordinates, offset, ends[0], stride, x, y)) {
+    return false;
+  }
+  var i, ii;
+  for (i = 1, ii = ends.length; i < ii; ++i) {
+    if (ol.geom.flat.contains.linearRingContainsXY(
+        flatCoordinates, ends[i - 1], ends[i], stride, x, y)) {
+      return false;
+    }
+  }
+  return true;
+};
+
+
+/**
+ * @param {Array.<number>} flatCoordinates Flat coordinates.
+ * @param {number} offset Offset.
+ * @param {Array.<Array.<number>>} endss Endss.
+ * @param {number} stride Stride.
+ * @param {number} x X.
+ * @param {number} y Y.
+ * @return {boolean} Contains (x, y).
+ */
+ol.geom.flat.contains.linearRingssContainsXY = function(flatCoordinates, offset, endss, stride, x, y) {
+  goog.asserts.assert(endss.length > 0, 'endss should not be an empty array');
+  if (endss.length === 0) {
+    return false;
+  }
+  var i, ii;
+  for (i = 0, ii = endss.length; i < ii; ++i) {
+    var ends = endss[i];
+    if (ol.geom.flat.contains.linearRingsContainsXY(
+        flatCoordinates, offset, ends, stride, x, y)) {
+      return true;
+    }
+    offset = ends[ends.length - 1];
+  }
+  return false;
+};
+
+goog.provide('ol.geom.flat.interiorpoint');
+
+goog.require('goog.asserts');
+goog.require('ol.array');
+goog.require('ol.geom.flat.contains');
+
+
+/**
+ * Calculates a point that is likely to lie in the interior of the linear rings.
+ * Inspired by JTS's com.vividsolutions.jts.geom.Geometry#getInteriorPoint.
+ * @param {Array.<number>} flatCoordinates Flat coordinates.
+ * @param {number} offset Offset.
+ * @param {Array.<number>} ends Ends.
+ * @param {number} stride Stride.
+ * @param {Array.<number>} flatCenters Flat centers.
+ * @param {number} flatCentersOffset Flat center offset.
+ * @param {Array.<number>=} opt_dest Destination.
+ * @return {Array.<number>} Destination.
+ */
+ol.geom.flat.interiorpoint.linearRings = function(flatCoordinates, offset,
+    ends, stride, flatCenters, flatCentersOffset, opt_dest) {
+  var i, ii, x, x1, x2, y1, y2;
+  var y = flatCenters[flatCentersOffset + 1];
+  /** @type {Array.<number>} */
+  var intersections = [];
+  // Calculate intersections with the horizontal line
+  var end = ends[0];
+  x1 = flatCoordinates[end - stride];
+  y1 = flatCoordinates[end - stride + 1];
+  for (i = offset; i < end; i += stride) {
+    x2 = flatCoordinates[i];
+    y2 = flatCoordinates[i + 1];
+    if ((y <= y1 && y2 <= y) || (y1 <= y && y <= y2)) {
+      x = (y - y1) / (y2 - y1) * (x2 - x1) + x1;
+      intersections.push(x);
+    }
+    x1 = x2;
+    y1 = y2;
+  }
+  // Find the longest segment of the horizontal line that has its center point
+  // inside the linear ring.
+  var pointX = NaN;
+  var maxSegmentLength = -Infinity;
+  intersections.sort(ol.array.numberSafeCompareFunction);
+  x1 = intersections[0];
+  for (i = 1, ii = intersections.length; i < ii; ++i) {
+    x2 = intersections[i];
+    var segmentLength = Math.abs(x2 - x1);
+    if (segmentLength > maxSegmentLength) {
+      x = (x1 + x2) / 2;
+      if (ol.geom.flat.contains.linearRingsContainsXY(
+          flatCoordinates, offset, ends, stride, x, y)) {
+        pointX = x;
+        maxSegmentLength = segmentLength;
+      }
+    }
+    x1 = x2;
+  }
+  if (isNaN(pointX)) {
+    // There is no horizontal line that has its center point inside the linear
+    // ring.  Use the center of the the linear ring's extent.
+    pointX = flatCenters[flatCentersOffset];
+  }
+  if (opt_dest) {
+    opt_dest.push(pointX, y);
+    return opt_dest;
+  } else {
+    return [pointX, y];
+  }
+};
+
+
+/**
+ * @param {Array.<number>} flatCoordinates Flat coordinates.
+ * @param {number} offset Offset.
+ * @param {Array.<Array.<number>>} endss Endss.
+ * @param {number} stride Stride.
+ * @param {Array.<number>} flatCenters Flat centers.
+ * @return {Array.<number>} Interior points.
+ */
+ol.geom.flat.interiorpoint.linearRingss = function(flatCoordinates, offset, endss, stride, flatCenters) {
+  goog.asserts.assert(2 * endss.length == flatCenters.length,
+      'endss.length times 2 should be flatCenters.length');
+  var interiorPoints = [];
+  var i, ii;
+  for (i = 0, ii = endss.length; i < ii; ++i) {
+    var ends = endss[i];
+    interiorPoints = ol.geom.flat.interiorpoint.linearRings(flatCoordinates,
+        offset, ends, stride, flatCenters, 2 * i, interiorPoints);
+    offset = ends[ends.length - 1];
+  }
+  return interiorPoints;
+};
+
+goog.provide('ol.geom.flat.segments');
+
+
+/**
+ * This function calls `callback` for each segment of the flat coordinates
+ * array. If the callback returns a truthy value the function returns that
+ * value immediately. Otherwise the function returns `false`.
+ * @param {Array.<number>} flatCoordinates Flat coordinates.
+ * @param {number} offset Offset.
+ * @param {number} end End.
+ * @param {number} stride Stride.
+ * @param {function(this: S, ol.Coordinate, ol.Coordinate): T} callback Function
+ *     called for each segment.
+ * @param {S=} opt_this The object to be used as the value of 'this'
+ *     within callback.
+ * @return {T|boolean} Value.
+ * @template T,S
+ */
+ol.geom.flat.segments.forEach = function(flatCoordinates, offset, end, stride, callback, opt_this) {
+  var point1 = [flatCoordinates[offset], flatCoordinates[offset + 1]];
+  var point2 = [];
+  var ret;
+  for (; (offset + stride) < end; offset += stride) {
+    point2[0] = flatCoordinates[offset + stride];
+    point2[1] = flatCoordinates[offset + stride + 1];
+    ret = callback.call(opt_this, point1, point2);
+    if (ret) {
+      return ret;
+    }
+    point1[0] = point2[0];
+    point1[1] = point2[1];
+  }
+  return false;
+};
+
+goog.provide('ol.geom.flat.intersectsextent');
+
+goog.require('goog.asserts');
+goog.require('ol.extent');
+goog.require('ol.geom.flat.contains');
+goog.require('ol.geom.flat.segments');
+
+
+/**
+ * @param {Array.<number>} flatCoordinates Flat coordinates.
+ * @param {number} offset Offset.
+ * @param {number} end End.
+ * @param {number} stride Stride.
+ * @param {ol.Extent} extent Extent.
+ * @return {boolean} True if the geometry and the extent intersect.
+ */
+ol.geom.flat.intersectsextent.lineString = function(flatCoordinates, offset, end, stride, extent) {
+  var coordinatesExtent = ol.extent.extendFlatCoordinates(
+      ol.extent.createEmpty(), flatCoordinates, offset, end, stride);
+  if (!ol.extent.intersects(extent, coordinatesExtent)) {
+    return false;
+  }
+  if (ol.extent.containsExtent(extent, coordinatesExtent)) {
+    return true;
+  }
+  if (coordinatesExtent[0] >= extent[0] &&
+      coordinatesExtent[2] <= extent[2]) {
+    return true;
+  }
+  if (coordinatesExtent[1] >= extent[1] &&
+      coordinatesExtent[3] <= extent[3]) {
+    return true;
+  }
+  return ol.geom.flat.segments.forEach(flatCoordinates, offset, end, stride,
+      /**
+       * @param {ol.Coordinate} point1 Start point.
+       * @param {ol.Coordinate} point2 End point.
+       * @return {boolean} `true` if the segment and the extent intersect,
+       *     `false` otherwise.
+       */
+      function(point1, point2) {
+        return ol.extent.intersectsSegment(extent, point1, point2);
+      });
+};
+
+
+/**
+ * @param {Array.<number>} flatCoordinates Flat coordinates.
+ * @param {number} offset Offset.
+ * @param {Array.<number>} ends Ends.
+ * @param {number} stride Stride.
+ * @param {ol.Extent} extent Extent.
+ * @return {boolean} True if the geometry and the extent intersect.
+ */
+ol.geom.flat.intersectsextent.lineStrings = function(flatCoordinates, offset, ends, stride, extent) {
+  var i, ii;
+  for (i = 0, ii = ends.length; i < ii; ++i) {
+    if (ol.geom.flat.intersectsextent.lineString(
+        flatCoordinates, offset, ends[i], stride, extent)) {
+      return true;
+    }
+    offset = ends[i];
+  }
+  return false;
+};
+
+
+/**
+ * @param {Array.<number>} flatCoordinates Flat coordinates.
+ * @param {number} offset Offset.
+ * @param {number} end End.
+ * @param {number} stride Stride.
+ * @param {ol.Extent} extent Extent.
+ * @return {boolean} True if the geometry and the extent intersect.
+ */
+ol.geom.flat.intersectsextent.linearRing = function(flatCoordinates, offset, end, stride, extent) {
+  if (ol.geom.flat.intersectsextent.lineString(
+      flatCoordinates, offset, end, stride, extent)) {
+    return true;
+  }
+  if (ol.geom.flat.contains.linearRingContainsXY(
+      flatCoordinates, offset, end, stride, extent[0], extent[1])) {
+    return true;
+  }
+  if (ol.geom.flat.contains.linearRingContainsXY(
+      flatCoordinates, offset, end, stride, extent[0], extent[3])) {
+    return true;
+  }
+  if (ol.geom.flat.contains.linearRingContainsXY(
+      flatCoordinates, offset, end, stride, extent[2], extent[1])) {
+    return true;
+  }
+  if (ol.geom.flat.contains.linearRingContainsXY(
+      flatCoordinates, offset, end, stride, extent[2], extent[3])) {
+    return true;
+  }
+  return false;
+};
+
+
+/**
+ * @param {Array.<number>} flatCoordinates Flat coordinates.
+ * @param {number} offset Offset.
+ * @param {Array.<number>} ends Ends.
+ * @param {number} stride Stride.
+ * @param {ol.Extent} extent Extent.
+ * @return {boolean} True if the geometry and the extent intersect.
+ */
+ol.geom.flat.intersectsextent.linearRings = function(flatCoordinates, offset, ends, stride, extent) {
+  goog.asserts.assert(ends.length > 0, 'ends should not be an empty array');
+  if (!ol.geom.flat.intersectsextent.linearRing(
+      flatCoordinates, offset, ends[0], stride, extent)) {
+    return false;
+  }
+  if (ends.length === 1) {
+    return true;
+  }
+  var i, ii;
+  for (i = 1, ii = ends.length; i < ii; ++i) {
+    if (ol.geom.flat.contains.linearRingContainsExtent(
+        flatCoordinates, ends[i - 1], ends[i], stride, extent)) {
+      return false;
+    }
+  }
+  return true;
+};
+
+
+/**
+ * @param {Array.<number>} flatCoordinates Flat coordinates.
+ * @param {number} offset Offset.
+ * @param {Array.<Array.<number>>} endss Endss.
+ * @param {number} stride Stride.
+ * @param {ol.Extent} extent Extent.
+ * @return {boolean} True if the geometry and the extent intersect.
+ */
+ol.geom.flat.intersectsextent.linearRingss = function(flatCoordinates, offset, endss, stride, extent) {
+  goog.asserts.assert(endss.length > 0, 'endss should not be an empty array');
+  var i, ii;
+  for (i = 0, ii = endss.length; i < ii; ++i) {
+    var ends = endss[i];
+    if (ol.geom.flat.intersectsextent.linearRings(
+        flatCoordinates, offset, ends, stride, extent)) {
+      return true;
+    }
+    offset = ends[ends.length - 1];
+  }
+  return false;
+};
+
+goog.provide('ol.geom.flat.reverse');
+
+
+/**
+ * @param {Array.<number>} flatCoordinates Flat coordinates.
+ * @param {number} offset Offset.
+ * @param {number} end End.
+ * @param {number} stride Stride.
+ */
+ol.geom.flat.reverse.coordinates = function(flatCoordinates, offset, end, stride) {
+  while (offset < end - stride) {
+    var i;
+    for (i = 0; i < stride; ++i) {
+      var tmp = flatCoordinates[offset + i];
+      flatCoordinates[offset + i] = flatCoordinates[end - stride + i];
+      flatCoordinates[end - stride + i] = tmp;
+    }
+    offset += stride;
+    end -= stride;
+  }
+};
+
+goog.provide('ol.geom.flat.orient');
+
+goog.require('ol');
+goog.require('ol.geom.flat.reverse');
+
+
+/**
+ * @param {Array.<number>} flatCoordinates Flat coordinates.
+ * @param {number} offset Offset.
+ * @param {number} end End.
+ * @param {number} stride Stride.
+ * @return {boolean} Is clockwise.
+ */
+ol.geom.flat.orient.linearRingIsClockwise = function(flatCoordinates, offset, end, stride) {
+  // http://tinyurl.com/clockwise-method
+  // https://github.com/OSGeo/gdal/blob/trunk/gdal/ogr/ogrlinearring.cpp
+  var edge = 0;
+  var x1 = flatCoordinates[end - stride];
+  var y1 = flatCoordinates[end - stride + 1];
+  for (; offset < end; offset += stride) {
+    var x2 = flatCoordinates[offset];
+    var y2 = flatCoordinates[offset + 1];
+    edge += (x2 - x1) * (y2 + y1);
+    x1 = x2;
+    y1 = y2;
+  }
+  return edge > 0;
+};
+
+
+/**
+ * Determines if linear rings are oriented.  By default, left-hand orientation
+ * is tested (first ring must be clockwise, remaining rings counter-clockwise).
+ * To test for right-hand orientation, use the `opt_right` argument.
+ *
+ * @param {Array.<number>} flatCoordinates Flat coordinates.
+ * @param {number} offset Offset.
+ * @param {Array.<number>} ends Array of end indexes.
+ * @param {number} stride Stride.
+ * @param {boolean=} opt_right Test for right-hand orientation
+ *     (counter-clockwise exterior ring and clockwise interior rings).
+ * @return {boolean} Rings are correctly oriented.
+ */
+ol.geom.flat.orient.linearRingsAreOriented = function(flatCoordinates, offset, ends, stride, opt_right) {
+  var right = opt_right !== undefined ? opt_right : false;
+  var i, ii;
+  for (i = 0, ii = ends.length; i < ii; ++i) {
+    var end = ends[i];
+    var isClockwise = ol.geom.flat.orient.linearRingIsClockwise(
+        flatCoordinates, offset, end, stride);
+    if (i === 0) {
+      if ((right && isClockwise) || (!right && !isClockwise)) {
+        return false;
+      }
+    } else {
+      if ((right && !isClockwise) || (!right && isClockwise)) {
+        return false;
+      }
+    }
+    offset = end;
+  }
+  return true;
+};
+
+
+/**
+ * Determines if linear rings are oriented.  By default, left-hand orientation
+ * is tested (first ring must be clockwise, remaining rings counter-clockwise).
+ * To test for right-hand orientation, use the `opt_right` argument.
+ *
+ * @param {Array.<number>} flatCoordinates Flat coordinates.
+ * @param {number} offset Offset.
+ * @param {Array.<Array.<number>>} endss Array of array of end indexes.
+ * @param {number} stride Stride.
+ * @param {boolean=} opt_right Test for right-hand orientation
+ *     (counter-clockwise exterior ring and clockwise interior rings).
+ * @return {boolean} Rings are correctly oriented.
+ */
+ol.geom.flat.orient.linearRingssAreOriented = function(flatCoordinates, offset, endss, stride, opt_right) {
+  var i, ii;
+  for (i = 0, ii = endss.length; i < ii; ++i) {
+    if (!ol.geom.flat.orient.linearRingsAreOriented(
+        flatCoordinates, offset, endss[i], stride, opt_right)) {
+      return false;
+    }
+  }
+  return true;
+};
+
+
+/**
+ * Orient coordinates in a flat array of linear rings.  By default, rings
+ * are oriented following the left-hand rule (clockwise for exterior and
+ * counter-clockwise for interior rings).  To orient according to the
+ * right-hand rule, use the `opt_right` argument.
+ *
+ * @param {Array.<number>} flatCoordinates Flat coordinates.
+ * @param {number} offset Offset.
+ * @param {Array.<number>} ends Ends.
+ * @param {number} stride Stride.
+ * @param {boolean=} opt_right Follow the right-hand rule for orientation.
+ * @return {number} End.
+ */
+ol.geom.flat.orient.orientLinearRings = function(flatCoordinates, offset, ends, stride, opt_right) {
+  var right = opt_right !== undefined ? opt_right : false;
+  var i, ii;
+  for (i = 0, ii = ends.length; i < ii; ++i) {
+    var end = ends[i];
+    var isClockwise = ol.geom.flat.orient.linearRingIsClockwise(
+        flatCoordinates, offset, end, stride);
+    var reverse = i === 0 ?
+        (right && isClockwise) || (!right && !isClockwise) :
+        (right && !isClockwise) || (!right && isClockwise);
+    if (reverse) {
+      ol.geom.flat.reverse.coordinates(flatCoordinates, offset, end, stride);
+    }
+    offset = end;
+  }
+  return offset;
+};
+
+
+/**
+ * Orient coordinates in a flat array of linear rings.  By default, rings
+ * are oriented following the left-hand rule (clockwise for exterior and
+ * counter-clockwise for interior rings).  To orient according to the
+ * right-hand rule, use the `opt_right` argument.
+ *
+ * @param {Array.<number>} flatCoordinates Flat coordinates.
+ * @param {number} offset Offset.
+ * @param {Array.<Array.<number>>} endss Array of array of end indexes.
+ * @param {number} stride Stride.
+ * @param {boolean=} opt_right Follow the right-hand rule for orientation.
+ * @return {number} End.
+ */
+ol.geom.flat.orient.orientLinearRingss = function(flatCoordinates, offset, endss, stride, opt_right) {
+  var i, ii;
+  for (i = 0, ii = endss.length; i < ii; ++i) {
+    offset = ol.geom.flat.orient.orientLinearRings(
+        flatCoordinates, offset, endss[i], stride, opt_right);
+  }
+  return offset;
+};
+
+goog.provide('ol.geom.Polygon');
+
+goog.require('goog.asserts');
+goog.require('ol');
+goog.require('ol.array');
+goog.require('ol.extent');
+goog.require('ol.geom.GeometryLayout');
+goog.require('ol.geom.GeometryType');
+goog.require('ol.geom.LinearRing');
+goog.require('ol.geom.Point');
+goog.require('ol.geom.SimpleGeometry');
+goog.require('ol.geom.flat.area');
+goog.require('ol.geom.flat.closest');
+goog.require('ol.geom.flat.contains');
+goog.require('ol.geom.flat.deflate');
+goog.require('ol.geom.flat.inflate');
+goog.require('ol.geom.flat.interiorpoint');
+goog.require('ol.geom.flat.intersectsextent');
+goog.require('ol.geom.flat.orient');
+goog.require('ol.geom.flat.simplify');
+goog.require('ol.math');
+
+
+/**
+ * @classdesc
+ * Polygon geometry.
+ *
+ * @constructor
+ * @extends {ol.geom.SimpleGeometry}
+ * @param {Array.<Array.<ol.Coordinate>>} coordinates Coordinates.
+ * @param {ol.geom.GeometryLayout=} opt_layout Layout.
+ * @api stable
+ */
+ol.geom.Polygon = function(coordinates, opt_layout) {
+
+  ol.geom.SimpleGeometry.call(this);
+
+  /**
+   * @type {Array.<number>}
+   * @private
+   */
+  this.ends_ = [];
+
+  /**
+   * @private
+   * @type {number}
+   */
+  this.flatInteriorPointRevision_ = -1;
+
+  /**
+   * @private
+   * @type {ol.Coordinate}
+   */
+  this.flatInteriorPoint_ = null;
+
+  /**
+   * @private
+   * @type {number}
+   */
+  this.maxDelta_ = -1;
+
+  /**
+   * @private
+   * @type {number}
+   */
+  this.maxDeltaRevision_ = -1;
+
+  /**
+   * @private
+   * @type {number}
+   */
+  this.orientedRevision_ = -1;
+
+  /**
+   * @private
+   * @type {Array.<number>}
+   */
+  this.orientedFlatCoordinates_ = null;
+
+  this.setCoordinates(coordinates, opt_layout);
+
+};
+ol.inherits(ol.geom.Polygon, ol.geom.SimpleGeometry);
+
+
+/**
+ * Append the passed linear ring to this polygon.
+ * @param {ol.geom.LinearRing} linearRing Linear ring.
+ * @api stable
+ */
+ol.geom.Polygon.prototype.appendLinearRing = function(linearRing) {
+  goog.asserts.assert(linearRing.getLayout() == this.layout,
+      'layout of linearRing should match layout');
+  if (!this.flatCoordinates) {
+    this.flatCoordinates = linearRing.getFlatCoordinates().slice();
+  } else {
+    ol.array.extend(this.flatCoordinates, linearRing.getFlatCoordinates());
+  }
+  this.ends_.push(this.flatCoordinates.length);
+  this.changed();
+};
+
+
+/**
+ * Make a complete copy of the geometry.
+ * @return {!ol.geom.Polygon} Clone.
+ * @api stable
+ */
+ol.geom.Polygon.prototype.clone = function() {
+  var polygon = new ol.geom.Polygon(null);
+  polygon.setFlatCoordinates(
+      this.layout, this.flatCoordinates.slice(), this.ends_.slice());
+  return polygon;
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.geom.Polygon.prototype.closestPointXY = function(x, y, closestPoint, minSquaredDistance) {
+  if (minSquaredDistance <
+      ol.extent.closestSquaredDistanceXY(this.getExtent(), x, y)) {
+    return minSquaredDistance;
+  }
+  if (this.maxDeltaRevision_ != this.getRevision()) {
+    this.maxDelta_ = Math.sqrt(ol.geom.flat.closest.getsMaxSquaredDelta(
+        this.flatCoordinates, 0, this.ends_, this.stride, 0));
+    this.maxDeltaRevision_ = this.getRevision();
+  }
+  return ol.geom.flat.closest.getsClosestPoint(
+      this.flatCoordinates, 0, this.ends_, this.stride,
+      this.maxDelta_, true, x, y, closestPoint, minSquaredDistance);
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.geom.Polygon.prototype.containsXY = function(x, y) {
+  return ol.geom.flat.contains.linearRingsContainsXY(
+      this.getOrientedFlatCoordinates(), 0, this.ends_, this.stride, x, y);
+};
+
+
+/**
+ * Return the area of the polygon on projected plane.
+ * @return {number} Area (on projected plane).
+ * @api stable
+ */
+ol.geom.Polygon.prototype.getArea = function() {
+  return ol.geom.flat.area.linearRings(
+      this.getOrientedFlatCoordinates(), 0, this.ends_, this.stride);
+};
+
+
+/**
+ * Get the coordinate array for this geometry.  This array has the structure
+ * of a GeoJSON coordinate array for polygons.
+ *
+ * @param {boolean=} opt_right Orient coordinates according to the right-hand
+ *     rule (counter-clockwise for exterior and clockwise for interior rings).
+ *     If `false`, coordinates will be oriented according to the left-hand rule
+ *     (clockwise for exterior and counter-clockwise for interior rings).
+ *     By default, coordinate orientation will depend on how the geometry was
+ *     constructed.
+ * @return {Array.<Array.<ol.Coordinate>>} Coordinates.
+ * @api stable
+ */
+ol.geom.Polygon.prototype.getCoordinates = function(opt_right) {
+  var flatCoordinates;
+  if (opt_right !== undefined) {
+    flatCoordinates = this.getOrientedFlatCoordinates().slice();
+    ol.geom.flat.orient.orientLinearRings(
+        flatCoordinates, 0, this.ends_, this.stride, opt_right);
+  } else {
+    flatCoordinates = this.flatCoordinates;
+  }
+
+  return ol.geom.flat.inflate.coordinatess(
+      flatCoordinates, 0, this.ends_, this.stride);
+};
+
+
+/**
+ * @return {Array.<number>} Ends.
+ */
+ol.geom.Polygon.prototype.getEnds = function() {
+  return this.ends_;
+};
+
+
+/**
+ * @return {Array.<number>} Interior point.
+ */
+ol.geom.Polygon.prototype.getFlatInteriorPoint = function() {
+  if (this.flatInteriorPointRevision_ != this.getRevision()) {
+    var flatCenter = ol.extent.getCenter(this.getExtent());
+    this.flatInteriorPoint_ = ol.geom.flat.interiorpoint.linearRings(
+        this.getOrientedFlatCoordinates(), 0, this.ends_, this.stride,
+        flatCenter, 0);
+    this.flatInteriorPointRevision_ = this.getRevision();
+  }
+  return this.flatInteriorPoint_;
+};
+
+
+/**
+ * Return an interior point of the polygon.
+ * @return {ol.geom.Point} Interior point.
+ * @api stable
+ */
+ol.geom.Polygon.prototype.getInteriorPoint = function() {
+  return new ol.geom.Point(this.getFlatInteriorPoint());
+};
+
+
+/**
+ * Return the number of rings of the polygon,  this includes the exterior
+ * ring and any interior rings.
+ *
+ * @return {number} Number of rings.
+ * @api
+ */
+ol.geom.Polygon.prototype.getLinearRingCount = function() {
+  return this.ends_.length;
+};
+
+
+/**
+ * Return the Nth linear ring of the polygon geometry. Return `null` if the
+ * given index is out of range.
+ * The exterior linear ring is available at index `0` and the interior rings
+ * at index `1` and beyond.
+ *
+ * @param {number} index Index.
+ * @return {ol.geom.LinearRing} Linear ring.
+ * @api stable
+ */
+ol.geom.Polygon.prototype.getLinearRing = function(index) {
+  goog.asserts.assert(0 <= index && index < this.ends_.length,
+      'index should be in between 0 and and length of this.ends_');
+  if (index < 0 || this.ends_.length <= index) {
+    return null;
+  }
+  var linearRing = new ol.geom.LinearRing(null);
+  linearRing.setFlatCoordinates(this.layout, this.flatCoordinates.slice(
+      index === 0 ? 0 : this.ends_[index - 1], this.ends_[index]));
+  return linearRing;
+};
+
+
+/**
+ * Return the linear rings of the polygon.
+ * @return {Array.<ol.geom.LinearRing>} Linear rings.
+ * @api stable
+ */
+ol.geom.Polygon.prototype.getLinearRings = function() {
+  var layout = this.layout;
+  var flatCoordinates = this.flatCoordinates;
+  var ends = this.ends_;
+  var linearRings = [];
+  var offset = 0;
+  var i, ii;
+  for (i = 0, ii = ends.length; i < ii; ++i) {
+    var end = ends[i];
+    var linearRing = new ol.geom.LinearRing(null);
+    linearRing.setFlatCoordinates(layout, flatCoordinates.slice(offset, end));
+    linearRings.push(linearRing);
+    offset = end;
+  }
+  return linearRings;
+};
+
+
+/**
+ * @return {Array.<number>} Oriented flat coordinates.
+ */
+ol.geom.Polygon.prototype.getOrientedFlatCoordinates = function() {
+  if (this.orientedRevision_ != this.getRevision()) {
+    var flatCoordinates = this.flatCoordinates;
+    if (ol.geom.flat.orient.linearRingsAreOriented(
+        flatCoordinates, 0, this.ends_, this.stride)) {
+      this.orientedFlatCoordinates_ = flatCoordinates;
+    } else {
+      this.orientedFlatCoordinates_ = flatCoordinates.slice();
+      this.orientedFlatCoordinates_.length =
+          ol.geom.flat.orient.orientLinearRings(
+              this.orientedFlatCoordinates_, 0, this.ends_, this.stride);
+    }
+    this.orientedRevision_ = this.getRevision();
+  }
+  return this.orientedFlatCoordinates_;
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.geom.Polygon.prototype.getSimplifiedGeometryInternal = function(squaredTolerance) {
+  var simplifiedFlatCoordinates = [];
+  var simplifiedEnds = [];
+  simplifiedFlatCoordinates.length = ol.geom.flat.simplify.quantizes(
+      this.flatCoordinates, 0, this.ends_, this.stride,
+      Math.sqrt(squaredTolerance),
+      simplifiedFlatCoordinates, 0, simplifiedEnds);
+  var simplifiedPolygon = new ol.geom.Polygon(null);
+  simplifiedPolygon.setFlatCoordinates(
+      ol.geom.GeometryLayout.XY, simplifiedFlatCoordinates, simplifiedEnds);
+  return simplifiedPolygon;
+};
+
+
+/**
+ * @inheritDoc
+ * @api stable
+ */
+ol.geom.Polygon.prototype.getType = function() {
+  return ol.geom.GeometryType.POLYGON;
+};
+
+
+/**
+ * @inheritDoc
+ * @api stable
+ */
+ol.geom.Polygon.prototype.intersectsExtent = function(extent) {
+  return ol.geom.flat.intersectsextent.linearRings(
+      this.getOrientedFlatCoordinates(), 0, this.ends_, this.stride, extent);
+};
+
+
+/**
+ * Set the coordinates of the polygon.
+ * @param {Array.<Array.<ol.Coordinate>>} coordinates Coordinates.
+ * @param {ol.geom.GeometryLayout=} opt_layout Layout.
+ * @api stable
+ */
+ol.geom.Polygon.prototype.setCoordinates = function(coordinates, opt_layout) {
+  if (!coordinates) {
+    this.setFlatCoordinates(ol.geom.GeometryLayout.XY, null, this.ends_);
+  } else {
+    this.setLayout(opt_layout, coordinates, 2);
+    if (!this.flatCoordinates) {
+      this.flatCoordinates = [];
+    }
+    var ends = ol.geom.flat.deflate.coordinatess(
+        this.flatCoordinates, 0, coordinates, this.stride, this.ends_);
+    this.flatCoordinates.length = ends.length === 0 ? 0 : ends[ends.length - 1];
+    this.changed();
+  }
+};
+
+
+/**
+ * @param {ol.geom.GeometryLayout} layout Layout.
+ * @param {Array.<number>} flatCoordinates Flat coordinates.
+ * @param {Array.<number>} ends Ends.
+ */
+ol.geom.Polygon.prototype.setFlatCoordinates = function(layout, flatCoordinates, ends) {
+  if (!flatCoordinates) {
+    goog.asserts.assert(ends && ends.length === 0,
+        'ends must be an empty array');
+  } else if (ends.length === 0) {
+    goog.asserts.assert(flatCoordinates.length === 0,
+        'flatCoordinates should be an empty array');
+  } else {
+    goog.asserts.assert(flatCoordinates.length == ends[ends.length - 1],
+        'the length of flatCoordinates should be the last entry of ends');
+  }
+  this.setFlatCoordinatesInternal(layout, flatCoordinates);
+  this.ends_ = ends;
+  this.changed();
+};
+
+
+/**
+ * Create an approximation of a circle on the surface of a sphere.
+ * @param {ol.Sphere} sphere The sphere.
+ * @param {ol.Coordinate} center Center (`[lon, lat]` in degrees).
+ * @param {number} radius The great-circle distance from the center to
+ *     the polygon vertices.
+ * @param {number=} opt_n Optional number of vertices for the resulting
+ *     polygon. Default is `32`.
+ * @return {ol.geom.Polygon} The "circular" polygon.
+ * @api stable
+ */
+ol.geom.Polygon.circular = function(sphere, center, radius, opt_n) {
+  var n = opt_n ? opt_n : 32;
+  /** @type {Array.<number>} */
+  var flatCoordinates = [];
+  var i;
+  for (i = 0; i < n; ++i) {
+    ol.array.extend(
+        flatCoordinates, sphere.offset(center, radius, 2 * Math.PI * i / n));
+  }
+  flatCoordinates.push(flatCoordinates[0], flatCoordinates[1]);
+  var polygon = new ol.geom.Polygon(null);
+  polygon.setFlatCoordinates(
+      ol.geom.GeometryLayout.XY, flatCoordinates, [flatCoordinates.length]);
+  return polygon;
+};
+
+
+/**
+ * Create a polygon from an extent. The layout used is `XY`.
+ * @param {ol.Extent} extent The extent.
+ * @return {ol.geom.Polygon} The polygon.
+ * @api
+ */
+ol.geom.Polygon.fromExtent = function(extent) {
+  var minX = extent[0];
+  var minY = extent[1];
+  var maxX = extent[2];
+  var maxY = extent[3];
+  var flatCoordinates =
+      [minX, minY, minX, maxY, maxX, maxY, maxX, minY, minX, minY];
+  var polygon = new ol.geom.Polygon(null);
+  polygon.setFlatCoordinates(
+      ol.geom.GeometryLayout.XY, flatCoordinates, [flatCoordinates.length]);
+  return polygon;
+};
+
+
+/**
+ * Create a regular polygon from a circle.
+ * @param {ol.geom.Circle} circle Circle geometry.
+ * @param {number=} opt_sides Number of sides of the polygon. Default is 32.
+ * @param {number=} opt_angle Start angle for the first vertex of the polygon in
+ *     radians. Default is 0.
+ * @return {ol.geom.Polygon} Polygon geometry.
+ * @api
+ */
+ol.geom.Polygon.fromCircle = function(circle, opt_sides, opt_angle) {
+  var sides = opt_sides ? opt_sides : 32;
+  var stride = circle.getStride();
+  var layout = circle.getLayout();
+  var polygon = new ol.geom.Polygon(null, layout);
+  var arrayLength = stride * (sides + 1);
+  var flatCoordinates = new Array(arrayLength);
+  for (var i = 0; i < arrayLength; i++) {
+    flatCoordinates[i] = 0;
+  }
+  var ends = [flatCoordinates.length];
+  polygon.setFlatCoordinates(layout, flatCoordinates, ends);
+  ol.geom.Polygon.makeRegular(
+      polygon, circle.getCenter(), circle.getRadius(), opt_angle);
+  return polygon;
+};
+
+
+/**
+ * Modify the coordinates of a polygon to make it a regular polygon.
+ * @param {ol.geom.Polygon} polygon Polygon geometry.
+ * @param {ol.Coordinate} center Center of the regular polygon.
+ * @param {number} radius Radius of the regular polygon.
+ * @param {number=} opt_angle Start angle for the first vertex of the polygon in
+ *     radians. Default is 0.
+ */
+ol.geom.Polygon.makeRegular = function(polygon, center, radius, opt_angle) {
+  var flatCoordinates = polygon.getFlatCoordinates();
+  var layout = polygon.getLayout();
+  var stride = polygon.getStride();
+  var ends = polygon.getEnds();
+  goog.asserts.assert(ends.length === 1, 'only 1 ring is supported');
+  var sides = flatCoordinates.length / stride - 1;
+  var startAngle = opt_angle ? opt_angle : 0;
+  var angle, offset;
+  for (var i = 0; i <= sides; ++i) {
+    offset = i * stride;
+    angle = startAngle + (ol.math.modulo(i, sides) * 2 * Math.PI / sides);
+    flatCoordinates[offset] = center[0] + (radius * Math.cos(angle));
+    flatCoordinates[offset + 1] = center[1] + (radius * Math.sin(angle));
+  }
+  polygon.setFlatCoordinates(layout, flatCoordinates, ends);
+};
+
+goog.provide('ol.View');
+goog.provide('ol.ViewHint');
+goog.provide('ol.ViewProperty');
+
+goog.require('goog.asserts');
+goog.require('ol');
+goog.require('ol.CenterConstraint');
+goog.require('ol.Constraints');
+goog.require('ol.Object');
+goog.require('ol.ResolutionConstraint');
+goog.require('ol.RotationConstraint');
+goog.require('ol.coordinate');
+goog.require('ol.extent');
+goog.require('ol.geom.Polygon');
+goog.require('ol.geom.SimpleGeometry');
+goog.require('ol.proj');
+goog.require('ol.proj.METERS_PER_UNIT');
+goog.require('ol.proj.Projection');
+goog.require('ol.proj.Units');
+
+
+/**
+ * @enum {string}
+ */
+ol.ViewProperty = {
+  CENTER: 'center',
+  RESOLUTION: 'resolution',
+  ROTATION: 'rotation'
+};
+
+
+/**
+ * @enum {number}
+ */
+ol.ViewHint = {
+  ANIMATING: 0,
+  INTERACTING: 1
+};
+
+
+/**
+ * @classdesc
+ * An ol.View object represents a simple 2D view of the map.
+ *
+ * This is the object to act upon to change the center, resolution,
+ * and rotation of the map.
+ *
+ * ### The view states
+ *
+ * An `ol.View` is determined by three states: `center`, `resolution`,
+ * and `rotation`. Each state has a corresponding getter and setter, e.g.
+ * `getCenter` and `setCenter` for the `center` state.
+ *
+ * An `ol.View` has a `projection`. The projection determines the
+ * coordinate system of the center, and its units determine the units of the
+ * resolution (projection units per pixel). The default projection is
+ * Spherical Mercator (EPSG:3857).
+ *
+ * ### The constraints
+ *
+ * `setCenter`, `setResolution` and `setRotation` can be used to change the
+ * states of the view. Any value can be passed to the setters. And the value
+ * that is passed to a setter will effectively be the value set in the view,
+ * and returned by the corresponding getter.
+ *
+ * But an `ol.View` object also has a *resolution constraint*, a
+ * *rotation constraint* and a *center constraint*.
+ *
+ * As said above, no constraints are applied when the setters are used to set
+ * new states for the view. Applying constraints is done explicitly through
+ * the use of the `constrain*` functions (`constrainResolution` and
+ * `constrainRotation` and `constrainCenter`).
+ *
+ * The main users of the constraints are the interactions and the
+ * controls. For example, double-clicking on the map changes the view to
+ * the "next" resolution. And releasing the fingers after pinch-zooming
+ * snaps to the closest resolution (with an animation).
+ *
+ * The *resolution constraint* snaps to specific resolutions. It is
+ * determined by the following options: `resolutions`, `maxResolution`,
+ * `maxZoom`, and `zoomFactor`. If `resolutions` is set, the other three
+ * options are ignored. See documentation for each option for more
+ * information.
+ *
+ * The *rotation constraint* snaps to specific angles. It is determined
+ * by the following options: `enableRotation` and `constrainRotation`.
+ * By default the rotation value is snapped to zero when approaching the
+ * horizontal.
+ *
+ * The *center constraint* is determined by the `extent` option. By
+ * default the center is not constrained at all.
+ *
+ * @constructor
+ * @extends {ol.Object}
+ * @param {olx.ViewOptions=} opt_options View options.
+ * @api stable
+ */
+ol.View = function(opt_options) {
+  ol.Object.call(this);
+  var options = opt_options || {};
+
+  /**
+   * @private
+   * @type {Array.<number>}
+   */
+  this.hints_ = [0, 0];
+
+  /**
+   * @type {Object.<string, *>}
+   */
+  var properties = {};
+  properties[ol.ViewProperty.CENTER] = options.center !== undefined ?
+      options.center : null;
+
+  /**
+   * @private
+   * @const
+   * @type {ol.proj.Projection}
+   */
+  this.projection_ = ol.proj.createProjection(options.projection, 'EPSG:3857');
+
+  var resolutionConstraintInfo = ol.View.createResolutionConstraint_(
+      options);
+
+  /**
+   * @private
+   * @type {number}
+   */
+  this.maxResolution_ = resolutionConstraintInfo.maxResolution;
+
+  /**
+   * @private
+   * @type {number}
+   */
+  this.minResolution_ = resolutionConstraintInfo.minResolution;
+
+  /**
+   * @private
+   * @type {Array.<number>|undefined}
+   */
+  this.resolutions_ = options.resolutions;
+
+  /**
+   * @private
+   * @type {number}
+   */
+  this.minZoom_ = resolutionConstraintInfo.minZoom;
+
+  var centerConstraint = ol.View.createCenterConstraint_(options);
+  var resolutionConstraint = resolutionConstraintInfo.constraint;
+  var rotationConstraint = ol.View.createRotationConstraint_(options);
+
+  /**
+   * @private
+   * @type {ol.Constraints}
+   */
+  this.constraints_ = new ol.Constraints(
+      centerConstraint, resolutionConstraint, rotationConstraint);
+
+  if (options.resolution !== undefined) {
+    properties[ol.ViewProperty.RESOLUTION] = options.resolution;
+  } else if (options.zoom !== undefined) {
+    properties[ol.ViewProperty.RESOLUTION] = this.constrainResolution(
+        this.maxResolution_, options.zoom - this.minZoom_);
+  }
+  properties[ol.ViewProperty.ROTATION] =
+      options.rotation !== undefined ? options.rotation : 0;
+  this.setProperties(properties);
+};
+ol.inherits(ol.View, ol.Object);
+
+
+/**
+ * @param {number} rotation Target rotation.
+ * @param {ol.Coordinate} anchor Rotation anchor.
+ * @return {ol.Coordinate|undefined} Center for rotation and anchor.
+ */
+ol.View.prototype.calculateCenterRotate = function(rotation, anchor) {
+  var center;
+  var currentCenter = this.getCenter();
+  if (currentCenter !== undefined) {
+    center = [currentCenter[0] - anchor[0], currentCenter[1] - anchor[1]];
+    ol.coordinate.rotate(center, rotation - this.getRotation());
+    ol.coordinate.add(center, anchor);
+  }
+  return center;
+};
+
+
+/**
+ * @param {number} resolution Target resolution.
+ * @param {ol.Coordinate} anchor Zoom anchor.
+ * @return {ol.Coordinate|undefined} Center for resolution and anchor.
+ */
+ol.View.prototype.calculateCenterZoom = function(resolution, anchor) {
+  var center;
+  var currentCenter = this.getCenter();
+  var currentResolution = this.getResolution();
+  if (currentCenter !== undefined && currentResolution !== undefined) {
+    var x = anchor[0] -
+        resolution * (anchor[0] - currentCenter[0]) / currentResolution;
+    var y = anchor[1] -
+        resolution * (anchor[1] - currentCenter[1]) / currentResolution;
+    center = [x, y];
+  }
+  return center;
+};
+
+
+/**
+ * Get the constrained center of this view.
+ * @param {ol.Coordinate|undefined} center Center.
+ * @return {ol.Coordinate|undefined} Constrained center.
+ * @api
+ */
+ol.View.prototype.constrainCenter = function(center) {
+  return this.constraints_.center(center);
+};
+
+
+/**
+ * Get the constrained resolution of this view.
+ * @param {number|undefined} resolution Resolution.
+ * @param {number=} opt_delta Delta. Default is `0`.
+ * @param {number=} opt_direction Direction. Default is `0`.
+ * @return {number|undefined} Constrained resolution.
+ * @api
+ */
+ol.View.prototype.constrainResolution = function(
+    resolution, opt_delta, opt_direction) {
+  var delta = opt_delta || 0;
+  var direction = opt_direction || 0;
+  return this.constraints_.resolution(resolution, delta, direction);
+};
+
+
+/**
+ * Get the constrained rotation of this view.
+ * @param {number|undefined} rotation Rotation.
+ * @param {number=} opt_delta Delta. Default is `0`.
+ * @return {number|undefined} Constrained rotation.
+ * @api
+ */
+ol.View.prototype.constrainRotation = function(rotation, opt_delta) {
+  var delta = opt_delta || 0;
+  return this.constraints_.rotation(rotation, delta);
+};
+
+
+/**
+ * Get the view center.
+ * @return {ol.Coordinate|undefined} The center of the view.
+ * @observable
+ * @api stable
+ */
+ol.View.prototype.getCenter = function() {
+  return /** @type {ol.Coordinate|undefined} */ (
+      this.get(ol.ViewProperty.CENTER));
+};
+
+
+/**
+ * @param {Array.<number>=} opt_hints Destination array.
+ * @return {Array.<number>} Hint.
+ */
+ol.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 {ol.Size} size Box pixel size.
+ * @return {ol.Extent} Extent.
+ * @api stable
+ */
+ol.View.prototype.calculateExtent = function(size) {
+  var center = this.getCenter();
+  goog.asserts.assert(center, 'The view center is not defined');
+  var resolution = this.getResolution();
+  goog.asserts.assert(resolution !== undefined,
+      'The view resolution is not defined');
+  var rotation = this.getRotation();
+  goog.asserts.assert(rotation !== undefined,
+      'The view rotation is not defined');
+
+  return ol.extent.getForViewAndSize(center, resolution, rotation, size);
+};
+
+
+/**
+ * Get the maximum resolution of the view.
+ * @return {number} The maximum resolution of the view.
+ * @api
+ */
+ol.View.prototype.getMaxResolution = function() {
+  return this.maxResolution_;
+};
+
+
+/**
+ * Get the minimum resolution of the view.
+ * @return {number} The minimum resolution of the view.
+ * @api
+ */
+ol.View.prototype.getMinResolution = function() {
+  return this.minResolution_;
+};
+
+
+/**
+ * Get the view projection.
+ * @return {ol.proj.Projection} The projection of the view.
+ * @api stable
+ */
+ol.View.prototype.getProjection = function() {
+  return this.projection_;
+};
+
+
+/**
+ * Get the view resolution.
+ * @return {number|undefined} The resolution of the view.
+ * @observable
+ * @api stable
+ */
+ol.View.prototype.getResolution = function() {
+  return /** @type {number|undefined} */ (
+      this.get(ol.ViewProperty.RESOLUTION));
+};
+
+
+/**
+ * Get the resolutions for the view. This returns the array of resolutions
+ * passed to the constructor of the {ol.View}, or undefined if none were given.
+ * @return {Array.<number>|undefined} The resolutions of the view.
+ * @api stable
+ */
+ol.View.prototype.getResolutions = function() {
+  return this.resolutions_;
+};
+
+
+/**
+ * Get the resolution for a provided extent (in map units) and size (in pixels).
+ * @param {ol.Extent} extent Extent.
+ * @param {ol.Size} size Box pixel size.
+ * @return {number} The resolution at which the provided extent will render at
+ *     the given size.
+ */
+ol.View.prototype.getResolutionForExtent = function(extent, size) {
+  var xResolution = ol.extent.getWidth(extent) / size[0];
+  var yResolution = ol.extent.getHeight(extent) / size[1];
+  return Math.max(xResolution, yResolution);
+};
+
+
+/**
+ * Return a function that returns a value between 0 and 1 for a
+ * resolution. Exponential scaling is assumed.
+ * @param {number=} opt_power Power.
+ * @return {function(number): number} Resolution for value function.
+ */
+ol.View.prototype.getResolutionForValueFunction = function(opt_power) {
+  var power = opt_power || 2;
+  var maxResolution = this.maxResolution_;
+  var minResolution = this.minResolution_;
+  var max = Math.log(maxResolution / minResolution) / Math.log(power);
+  return (
+      /**
+       * @param {number} value Value.
+       * @return {number} Resolution.
+       */
+      function(value) {
+        var resolution = maxResolution / Math.pow(power, value * max);
+        goog.asserts.assert(resolution >= minResolution &&
+            resolution <= maxResolution,
+            'calculated resolution outside allowed bounds (%s <= %s <= %s)',
+            minResolution, resolution, maxResolution);
+        return resolution;
+      });
+};
+
+
+/**
+ * Get the view rotation.
+ * @return {number} The rotation of the view in radians.
+ * @observable
+ * @api stable
+ */
+ol.View.prototype.getRotation = function() {
+  return /** @type {number} */ (this.get(ol.ViewProperty.ROTATION));
+};
+
+
+/**
+ * Return a function that returns a resolution for a value between
+ * 0 and 1. Exponential scaling is assumed.
+ * @param {number=} opt_power Power.
+ * @return {function(number): number} Value for resolution function.
+ */
+ol.View.prototype.getValueForResolutionFunction = function(opt_power) {
+  var power = opt_power || 2;
+  var maxResolution = this.maxResolution_;
+  var minResolution = this.minResolution_;
+  var max = Math.log(maxResolution / minResolution) / Math.log(power);
+  return (
+      /**
+       * @param {number} resolution Resolution.
+       * @return {number} Value.
+       */
+      function(resolution) {
+        var value =
+            (Math.log(maxResolution / resolution) / Math.log(power)) / max;
+        goog.asserts.assert(value >= 0 && value <= 1,
+            'calculated value (%s) ouside allowed range (0-1)', value);
+        return value;
+      });
+};
+
+
+/**
+ * @return {olx.ViewState} View state.
+ */
+ol.View.prototype.getState = function() {
+  goog.asserts.assert(this.isDef(),
+      'the view was not defined (had no center and/or resolution)');
+  var center = /** @type {ol.Coordinate} */ (this.getCenter());
+  var projection = this.getProjection();
+  var resolution = /** @type {number} */ (this.getResolution());
+  var rotation = this.getRotation();
+  return /** @type {olx.ViewState} */ ({
+    // Snap center to closest pixel
+    center: [
+      Math.round(center[0] / resolution) * resolution,
+      Math.round(center[1] / resolution) * resolution
+    ],
+    projection: projection !== undefined ? projection : null,
+    resolution: resolution,
+    rotation: rotation
+  });
+};
+
+
+/**
+ * Get the current zoom level. Return undefined if the current
+ * resolution is undefined or not a "constrained resolution".
+ * @return {number|undefined} Zoom.
+ * @api stable
+ */
+ol.View.prototype.getZoom = function() {
+  var offset;
+  var resolution = this.getResolution();
+
+  if (resolution !== undefined) {
+    var res, z = 0;
+    do {
+      res = this.constrainResolution(this.maxResolution_, z);
+      if (res == resolution) {
+        offset = z;
+        break;
+      }
+      ++z;
+    } while (res > this.minResolution_);
+  }
+
+  return offset !== undefined ? this.minZoom_ + offset : offset;
+};
+
+
+/**
+ * Fit the given geometry or extent based on the given map size and border.
+ * The size is pixel dimensions of the box to fit the extent into.
+ * In most cases you will want to use the map size, that is `map.getSize()`.
+ * Takes care of the map angle.
+ * @param {ol.geom.SimpleGeometry|ol.Extent} geometry Geometry.
+ * @param {ol.Size} size Box pixel size.
+ * @param {olx.view.FitOptions=} opt_options Options.
+ * @api
+ */
+ol.View.prototype.fit = function(geometry, size, opt_options) {
+  if (!(geometry instanceof ol.geom.SimpleGeometry)) {
+    goog.asserts.assert(Array.isArray(geometry),
+        'invalid extent or geometry');
+    goog.asserts.assert(!ol.extent.isEmpty(geometry),
+        'cannot fit empty extent');
+    geometry = ol.geom.Polygon.fromExtent(geometry);
+  }
+
+  var options = opt_options || {};
+
+  var padding = options.padding !== undefined ? options.padding : [0, 0, 0, 0];
+  var constrainResolution = options.constrainResolution !== undefined ?
+      options.constrainResolution : true;
+  var nearest = options.nearest !== undefined ? options.nearest : false;
+  var minResolution;
+  if (options.minResolution !== undefined) {
+    minResolution = options.minResolution;
+  } else if (options.maxZoom !== undefined) {
+    minResolution = this.constrainResolution(
+        this.maxResolution_, options.maxZoom - this.minZoom_, 0);
+  } else {
+    minResolution = 0;
+  }
+  var coords = geometry.getFlatCoordinates();
+
+  // calculate rotated extent
+  var rotation = this.getRotation();
+  goog.asserts.assert(rotation !== undefined, 'rotation was not defined');
+  var cosAngle = Math.cos(-rotation);
+  var sinAngle = Math.sin(-rotation);
+  var minRotX = +Infinity;
+  var minRotY = +Infinity;
+  var maxRotX = -Infinity;
+  var maxRotY = -Infinity;
+  var stride = geometry.getStride();
+  for (var i = 0, ii = coords.length; i < ii; i += stride) {
+    var rotX = coords[i] * cosAngle - coords[i + 1] * sinAngle;
+    var rotY = coords[i] * sinAngle + coords[i + 1] * cosAngle;
+    minRotX = Math.min(minRotX, rotX);
+    minRotY = Math.min(minRotY, rotY);
+    maxRotX = Math.max(maxRotX, rotX);
+    maxRotY = Math.max(maxRotY, rotY);
+  }
+
+  // calculate resolution
+  var resolution = this.getResolutionForExtent(
+      [minRotX, minRotY, maxRotX, maxRotY],
+      [size[0] - padding[1] - padding[3], size[1] - padding[0] - padding[2]]);
+  resolution = isNaN(resolution) ? minResolution :
+      Math.max(resolution, minResolution);
+  if (constrainResolution) {
+    var constrainedResolution = this.constrainResolution(resolution, 0, 0);
+    if (!nearest && constrainedResolution < resolution) {
+      constrainedResolution = this.constrainResolution(
+          constrainedResolution, -1, 0);
+    }
+    resolution = constrainedResolution;
+  }
+  this.setResolution(resolution);
+
+  // calculate center
+  sinAngle = -sinAngle; // go back to original rotation
+  var centerRotX = (minRotX + maxRotX) / 2;
+  var centerRotY = (minRotY + maxRotY) / 2;
+  centerRotX += (padding[1] - padding[3]) / 2 * resolution;
+  centerRotY += (padding[0] - padding[2]) / 2 * resolution;
+  var centerX = centerRotX * cosAngle - centerRotY * sinAngle;
+  var centerY = centerRotY * cosAngle + centerRotX * sinAngle;
+
+  this.setCenter([centerX, centerY]);
+};
+
+
+/**
+ * Center on coordinate and view position.
+ * @param {ol.Coordinate} coordinate Coordinate.
+ * @param {ol.Size} size Box pixel size.
+ * @param {ol.Pixel} position Position on the view to center on.
+ * @api
+ */
+ol.View.prototype.centerOn = function(coordinate, size, position) {
+  // calculate rotated position
+  var rotation = this.getRotation();
+  var cosAngle = Math.cos(-rotation);
+  var sinAngle = Math.sin(-rotation);
+  var rotX = coordinate[0] * cosAngle - coordinate[1] * sinAngle;
+  var rotY = coordinate[1] * cosAngle + coordinate[0] * sinAngle;
+  var resolution = this.getResolution();
+  rotX += (size[0] / 2 - position[0]) * resolution;
+  rotY += (position[1] - size[1] / 2) * resolution;
+
+  // go back to original angle
+  sinAngle = -sinAngle; // go back to original rotation
+  var centerX = rotX * cosAngle - rotY * sinAngle;
+  var centerY = rotY * cosAngle + rotX * sinAngle;
+
+  this.setCenter([centerX, centerY]);
+};
+
+
+/**
+ * @return {boolean} Is defined.
+ */
+ol.View.prototype.isDef = function() {
+  return !!this.getCenter() && this.getResolution() !== undefined;
+};
+
+
+/**
+ * Rotate the view around a given coordinate.
+ * @param {number} rotation New rotation value for the view.
+ * @param {ol.Coordinate=} opt_anchor The rotation center.
+ * @api stable
+ */
+ol.View.prototype.rotate = function(rotation, opt_anchor) {
+  if (opt_anchor !== undefined) {
+    var center = this.calculateCenterRotate(rotation, opt_anchor);
+    this.setCenter(center);
+  }
+  this.setRotation(rotation);
+};
+
+
+/**
+ * Set the center of the current view.
+ * @param {ol.Coordinate|undefined} center The center of the view.
+ * @observable
+ * @api stable
+ */
+ol.View.prototype.setCenter = function(center) {
+  this.set(ol.ViewProperty.CENTER, center);
+};
+
+
+/**
+ * @param {ol.ViewHint} hint Hint.
+ * @param {number} delta Delta.
+ * @return {number} New value.
+ */
+ol.View.prototype.setHint = function(hint, delta) {
+  goog.asserts.assert(0 <= hint && hint < this.hints_.length,
+      'illegal hint (%s), must be between 0 and %s', hint, this.hints_.length);
+  this.hints_[hint] += delta;
+  goog.asserts.assert(this.hints_[hint] >= 0,
+      'Hint at %s must be positive, was %s', hint, this.hints_[hint]);
+  return this.hints_[hint];
+};
+
+
+/**
+ * Set the resolution for this view.
+ * @param {number|undefined} resolution The resolution of the view.
+ * @observable
+ * @api stable
+ */
+ol.View.prototype.setResolution = function(resolution) {
+  this.set(ol.ViewProperty.RESOLUTION, resolution);
+};
+
+
+/**
+ * Set the rotation for this view.
+ * @param {number} rotation The rotation of the view in radians.
+ * @observable
+ * @api stable
+ */
+ol.View.prototype.setRotation = function(rotation) {
+  this.set(ol.ViewProperty.ROTATION, rotation);
+};
+
+
+/**
+ * Zoom to a specific zoom level.
+ * @param {number} zoom Zoom level.
+ * @api stable
+ */
+ol.View.prototype.setZoom = function(zoom) {
+  var resolution = this.constrainResolution(
+      this.maxResolution_, zoom - this.minZoom_, 0);
+  this.setResolution(resolution);
+};
+
+
+/**
+ * @param {olx.ViewOptions} options View options.
+ * @private
+ * @return {ol.CenterConstraintType} The constraint.
+ */
+ol.View.createCenterConstraint_ = function(options) {
+  if (options.extent !== undefined) {
+    return ol.CenterConstraint.createExtent(options.extent);
+  } else {
+    return ol.CenterConstraint.none;
+  }
+};
+
+
+/**
+ * @private
+ * @param {olx.ViewOptions} options View options.
+ * @return {{constraint: ol.ResolutionConstraintType, maxResolution: number,
+ *     minResolution: number}} The constraint.
+ */
+ol.View.createResolutionConstraint_ = function(options) {
+  var resolutionConstraint;
+  var maxResolution;
+  var minResolution;
+
+  // TODO: move these to be ol constants
+  // see https://github.com/openlayers/ol3/issues/2076
+  var defaultMaxZoom = 28;
+  var defaultZoomFactor = 2;
+
+  var minZoom = options.minZoom !== undefined ?
+      options.minZoom : ol.DEFAULT_MIN_ZOOM;
+
+  var maxZoom = options.maxZoom !== undefined ?
+      options.maxZoom : defaultMaxZoom;
+
+  var zoomFactor = options.zoomFactor !== undefined ?
+      options.zoomFactor : defaultZoomFactor;
+
+  if (options.resolutions !== undefined) {
+    var resolutions = options.resolutions;
+    maxResolution = resolutions[0];
+    minResolution = resolutions[resolutions.length - 1];
+    resolutionConstraint = ol.ResolutionConstraint.createSnapToResolutions(
+        resolutions);
+  } else {
+    // calculate the default min and max resolution
+    var projection = ol.proj.createProjection(options.projection, 'EPSG:3857');
+    var extent = projection.getExtent();
+    var size = !extent ?
+        // use an extent that can fit the whole world if need be
+        360 * ol.proj.METERS_PER_UNIT[ol.proj.Units.DEGREES] /
+            projection.getMetersPerUnit() :
+        Math.max(ol.extent.getWidth(extent), ol.extent.getHeight(extent));
+
+    var defaultMaxResolution = size / ol.DEFAULT_TILE_SIZE / Math.pow(
+        defaultZoomFactor, ol.DEFAULT_MIN_ZOOM);
+
+    var defaultMinResolution = defaultMaxResolution / Math.pow(
+        defaultZoomFactor, defaultMaxZoom - ol.DEFAULT_MIN_ZOOM);
+
+    // user provided maxResolution takes precedence
+    maxResolution = options.maxResolution;
+    if (maxResolution !== undefined) {
+      minZoom = 0;
+    } else {
+      maxResolution = defaultMaxResolution / Math.pow(zoomFactor, minZoom);
+    }
+
+    // user provided minResolution takes precedence
+    minResolution = options.minResolution;
+    if (minResolution === undefined) {
+      if (options.maxZoom !== undefined) {
+        if (options.maxResolution !== undefined) {
+          minResolution = maxResolution / Math.pow(zoomFactor, maxZoom);
+        } else {
+          minResolution = defaultMaxResolution / Math.pow(zoomFactor, maxZoom);
+        }
+      } else {
+        minResolution = defaultMinResolution;
+      }
+    }
+
+    // given discrete zoom levels, minResolution may be different than provided
+    maxZoom = minZoom + Math.floor(
+        Math.log(maxResolution / minResolution) / Math.log(zoomFactor));
+    minResolution = maxResolution / Math.pow(zoomFactor, maxZoom - minZoom);
+
+    resolutionConstraint = ol.ResolutionConstraint.createSnapToPower(
+        zoomFactor, maxResolution, maxZoom - minZoom);
+  }
+  return {constraint: resolutionConstraint, maxResolution: maxResolution,
+    minResolution: minResolution, minZoom: minZoom};
+};
+
+
+/**
+ * @private
+ * @param {olx.ViewOptions} options View options.
+ * @return {ol.RotationConstraintType} Rotation constraint.
+ */
+ol.View.createRotationConstraint_ = function(options) {
+  var enableRotation = options.enableRotation !== undefined ?
+      options.enableRotation : true;
+  if (enableRotation) {
+    var constrainRotation = options.constrainRotation;
+    if (constrainRotation === undefined || constrainRotation === true) {
+      return ol.RotationConstraint.createSnapToZero();
+    } else if (constrainRotation === false) {
+      return ol.RotationConstraint.none;
+    } else if (goog.isNumber(constrainRotation)) {
+      return ol.RotationConstraint.createSnapToN(constrainRotation);
+    } else {
+      goog.asserts.fail(
+          'illegal option for constrainRotation (%s)', constrainRotation);
+      return ol.RotationConstraint.none;
+    }
+  } else {
+    return ol.RotationConstraint.disable;
+  }
+};
+
+goog.provide('ol.easing');
+
+
+/**
+ * Start slow and speed up.
+ * @param {number} t Input between 0 and 1.
+ * @return {number} Output between 0 and 1.
+ * @api
+ */
+ol.easing.easeIn = function(t) {
+  return Math.pow(t, 3);
+};
+
+
+/**
+ * Start fast and slow down.
+ * @param {number} t Input between 0 and 1.
+ * @return {number} Output between 0 and 1.
+ * @api
+ */
+ol.easing.easeOut = function(t) {
+  return 1 - ol.easing.easeIn(1 - t);
+};
+
+
+/**
+ * Start slow, speed up, and then slow down again.
+ * @param {number} t Input between 0 and 1.
+ * @return {number} Output between 0 and 1.
+ * @api
+ */
+ol.easing.inAndOut = function(t) {
+  return 3 * t * t - 2 * t * t * t;
+};
+
+
+/**
+ * Maintain a constant speed over time.
+ * @param {number} t Input between 0 and 1.
+ * @return {number} Output between 0 and 1.
+ * @api
+ */
+ol.easing.linear = function(t) {
+  return t;
+};
+
+
+/**
+ * Start slow, speed up, and at the very end slow down again.  This has the
+ * same general behavior as {@link ol.easing.inAndOut}, but the final slowdown
+ * is delayed.
+ * @param {number} t Input between 0 and 1.
+ * @return {number} Output between 0 and 1.
+ * @api
+ */
+ol.easing.upAndDown = function(t) {
+  if (t < 0.5) {
+    return ol.easing.inAndOut(2 * t);
+  } else {
+    return 1 - ol.easing.inAndOut(2 * (t - 0.5));
+  }
+};
+
+goog.provide('ol.animation');
+
+goog.require('ol');
+goog.require('ol.ViewHint');
+goog.require('ol.coordinate');
+goog.require('ol.easing');
+
+
+/**
+ * Generate an animated transition that will "bounce" the resolution as it
+ * approaches the final value.
+ * @param {olx.animation.BounceOptions} options Bounce options.
+ * @return {ol.PreRenderFunction} Pre-render function.
+ * @api
+ */
+ol.animation.bounce = function(options) {
+  var resolution = options.resolution;
+  var start = options.start ? options.start : Date.now();
+  var duration = options.duration !== undefined ? options.duration : 1000;
+  var easing = options.easing ?
+      options.easing : ol.easing.upAndDown;
+  return (
+      /**
+       * @param {ol.Map} map Map.
+       * @param {?olx.FrameState} frameState Frame state.
+       * @return {boolean} Run this function in the next frame.
+       */
+      function(map, frameState) {
+        if (frameState.time < start) {
+          frameState.animate = true;
+          frameState.viewHints[ol.ViewHint.ANIMATING] += 1;
+          return true;
+        } else if (frameState.time < start + duration) {
+          var delta = easing((frameState.time - start) / duration);
+          var deltaResolution = resolution - frameState.viewState.resolution;
+          frameState.animate = true;
+          frameState.viewState.resolution += delta * deltaResolution;
+          frameState.viewHints[ol.ViewHint.ANIMATING] += 1;
+          return true;
+        } else {
+          return false;
+        }
+      });
+};
+
+
+/**
+ * Generate an animated transition while updating the view center.
+ * @param {olx.animation.PanOptions} options Pan options.
+ * @return {ol.PreRenderFunction} Pre-render function.
+ * @api
+ */
+ol.animation.pan = function(options) {
+  var source = options.source;
+  var start = options.start ? options.start : Date.now();
+  var sourceX = source[0];
+  var sourceY = source[1];
+  var duration = options.duration !== undefined ? options.duration : 1000;
+  var easing = options.easing ?
+      options.easing : ol.easing.inAndOut;
+  return (
+      /**
+       * @param {ol.Map} map Map.
+       * @param {?olx.FrameState} frameState Frame state.
+       * @return {boolean} Run this function in the next frame.
+       */
+      function(map, frameState) {
+        if (frameState.time < start) {
+          frameState.animate = true;
+          frameState.viewHints[ol.ViewHint.ANIMATING] += 1;
+          return true;
+        } else if (frameState.time < start + duration) {
+          var delta = 1 - easing((frameState.time - start) / duration);
+          var deltaX = sourceX - frameState.viewState.center[0];
+          var deltaY = sourceY - frameState.viewState.center[1];
+          frameState.animate = true;
+          frameState.viewState.center[0] += delta * deltaX;
+          frameState.viewState.center[1] += delta * deltaY;
+          frameState.viewHints[ol.ViewHint.ANIMATING] += 1;
+          return true;
+        } else {
+          return false;
+        }
+      });
+};
+
+
+/**
+ * Generate an animated transition while updating the view rotation.
+ * @param {olx.animation.RotateOptions} options Rotate options.
+ * @return {ol.PreRenderFunction} Pre-render function.
+ * @api
+ */
+ol.animation.rotate = function(options) {
+  var sourceRotation = options.rotation ? options.rotation : 0;
+  var start = options.start ? options.start : Date.now();
+  var duration = options.duration !== undefined ? options.duration : 1000;
+  var easing = options.easing ?
+      options.easing : ol.easing.inAndOut;
+  var anchor = options.anchor ?
+      options.anchor : null;
+
+  return (
+      /**
+       * @param {ol.Map} map Map.
+       * @param {?olx.FrameState} frameState Frame state.
+       * @return {boolean} Run this function in the next frame.
+       */
+      function(map, frameState) {
+        if (frameState.time < start) {
+          frameState.animate = true;
+          frameState.viewHints[ol.ViewHint.ANIMATING] += 1;
+          return true;
+        } else if (frameState.time < start + duration) {
+          var delta = 1 - easing((frameState.time - start) / duration);
+          var deltaRotation =
+              (sourceRotation - frameState.viewState.rotation) * delta;
+          frameState.animate = true;
+          frameState.viewState.rotation += deltaRotation;
+          if (anchor) {
+            var center = frameState.viewState.center;
+            ol.coordinate.sub(center, anchor);
+            ol.coordinate.rotate(center, deltaRotation);
+            ol.coordinate.add(center, anchor);
+          }
+          frameState.viewHints[ol.ViewHint.ANIMATING] += 1;
+          return true;
+        } else {
+          return false;
+        }
+      });
+};
+
+
+/**
+ * Generate an animated transition while updating the view resolution.
+ * @param {olx.animation.ZoomOptions} options Zoom options.
+ * @return {ol.PreRenderFunction} Pre-render function.
+ * @api
+ */
+ol.animation.zoom = function(options) {
+  var sourceResolution = options.resolution;
+  var start = options.start ? options.start : Date.now();
+  var duration = options.duration !== undefined ? options.duration : 1000;
+  var easing = options.easing ?
+      options.easing : ol.easing.inAndOut;
+  return (
+      /**
+       * @param {ol.Map} map Map.
+       * @param {?olx.FrameState} frameState Frame state.
+       * @return {boolean} Run this function in the next frame.
+       */
+      function(map, frameState) {
+        if (frameState.time < start) {
+          frameState.animate = true;
+          frameState.viewHints[ol.ViewHint.ANIMATING] += 1;
+          return true;
+        } else if (frameState.time < start + duration) {
+          var delta = 1 - easing((frameState.time - start) / duration);
+          var deltaResolution =
+              sourceResolution - frameState.viewState.resolution;
+          frameState.animate = true;
+          frameState.viewState.resolution += delta * deltaResolution;
+          frameState.viewHints[ol.ViewHint.ANIMATING] += 1;
+          return true;
+        } else {
+          return false;
+        }
+      });
+};
+
+goog.provide('ol.TileRange');
+
+goog.require('goog.asserts');
+
+
+/**
+ * A representation of a contiguous block of tiles.  A tile range is specified
+ * by its min/max tile coordinates and is inclusive of coordinates.
+ *
+ * @constructor
+ * @param {number} minX Minimum X.
+ * @param {number} maxX Maximum X.
+ * @param {number} minY Minimum Y.
+ * @param {number} maxY Maximum Y.
+ * @struct
+ */
+ol.TileRange = function(minX, maxX, minY, maxY) {
+
+  /**
+   * @type {number}
+   */
+  this.minX = minX;
+
+  /**
+   * @type {number}
+   */
+  this.maxX = maxX;
+
+  /**
+   * @type {number}
+   */
+  this.minY = minY;
+
+  /**
+   * @type {number}
+   */
+  this.maxY = maxY;
+
+};
+
+
+/**
+ * @param {...ol.TileCoord} var_args Tile coordinates.
+ * @return {!ol.TileRange} Bounding tile box.
+ */
+ol.TileRange.boundingTileRange = function(var_args) {
+  var tileCoord0 = /** @type {ol.TileCoord} */ (arguments[0]);
+  var tileCoord0Z = tileCoord0[0];
+  var tileCoord0X = tileCoord0[1];
+  var tileCoord0Y = tileCoord0[2];
+  var tileRange = new ol.TileRange(tileCoord0X, tileCoord0X,
+                                   tileCoord0Y, tileCoord0Y);
+  var i, ii, tileCoord, tileCoordX, tileCoordY, tileCoordZ;
+  for (i = 1, ii = arguments.length; i < ii; ++i) {
+    tileCoord = /** @type {ol.TileCoord} */ (arguments[i]);
+    tileCoordZ = tileCoord[0];
+    tileCoordX = tileCoord[1];
+    tileCoordY = tileCoord[2];
+    goog.asserts.assert(tileCoordZ == tileCoord0Z,
+        'passed tilecoords all have the same Z-value');
+    tileRange.minX = Math.min(tileRange.minX, tileCoordX);
+    tileRange.maxX = Math.max(tileRange.maxX, tileCoordX);
+    tileRange.minY = Math.min(tileRange.minY, tileCoordY);
+    tileRange.maxY = Math.max(tileRange.maxY, tileCoordY);
+  }
+  return tileRange;
+};
+
+
+/**
+ * @param {number} minX Minimum X.
+ * @param {number} maxX Maximum X.
+ * @param {number} minY Minimum Y.
+ * @param {number} maxY Maximum Y.
+ * @param {ol.TileRange|undefined} tileRange TileRange.
+ * @return {ol.TileRange} Tile range.
+ */
+ol.TileRange.createOrUpdate = function(minX, maxX, minY, maxY, tileRange) {
+  if (tileRange !== undefined) {
+    tileRange.minX = minX;
+    tileRange.maxX = maxX;
+    tileRange.minY = minY;
+    tileRange.maxY = maxY;
+    return tileRange;
+  } else {
+    return new ol.TileRange(minX, maxX, minY, maxY);
+  }
+};
+
+
+/**
+ * @param {ol.TileCoord} tileCoord Tile coordinate.
+ * @return {boolean} Contains tile coordinate.
+ */
+ol.TileRange.prototype.contains = function(tileCoord) {
+  return this.containsXY(tileCoord[1], tileCoord[2]);
+};
+
+
+/**
+ * @param {ol.TileRange} tileRange Tile range.
+ * @return {boolean} Contains.
+ */
+ol.TileRange.prototype.containsTileRange = function(tileRange) {
+  return this.minX <= tileRange.minX && tileRange.maxX <= this.maxX &&
+      this.minY <= tileRange.minY && tileRange.maxY <= this.maxY;
+};
+
+
+/**
+ * @param {number} x Tile coordinate x.
+ * @param {number} y Tile coordinate y.
+ * @return {boolean} Contains coordinate.
+ */
+ol.TileRange.prototype.containsXY = function(x, y) {
+  return this.minX <= x && x <= this.maxX && this.minY <= y && y <= this.maxY;
+};
+
+
+/**
+ * @param {ol.TileRange} tileRange Tile range.
+ * @return {boolean} Equals.
+ */
+ol.TileRange.prototype.equals = function(tileRange) {
+  return this.minX == tileRange.minX && this.minY == tileRange.minY &&
+      this.maxX == tileRange.maxX && this.maxY == tileRange.maxY;
+};
+
+
+/**
+ * @param {ol.TileRange} tileRange Tile range.
+ */
+ol.TileRange.prototype.extend = function(tileRange) {
+  if (tileRange.minX < this.minX) {
+    this.minX = tileRange.minX;
+  }
+  if (tileRange.maxX > this.maxX) {
+    this.maxX = tileRange.maxX;
+  }
+  if (tileRange.minY < this.minY) {
+    this.minY = tileRange.minY;
+  }
+  if (tileRange.maxY > this.maxY) {
+    this.maxY = tileRange.maxY;
+  }
+};
+
+
+/**
+ * @return {number} Height.
+ */
+ol.TileRange.prototype.getHeight = function() {
+  return this.maxY - this.minY + 1;
+};
+
+
+/**
+ * @return {ol.Size} Size.
+ */
+ol.TileRange.prototype.getSize = function() {
+  return [this.getWidth(), this.getHeight()];
+};
+
+
+/**
+ * @return {number} Width.
+ */
+ol.TileRange.prototype.getWidth = function() {
+  return this.maxX - this.minX + 1;
+};
+
+
+/**
+ * @param {ol.TileRange} tileRange Tile range.
+ * @return {boolean} Intersects.
+ */
+ol.TileRange.prototype.intersects = function(tileRange) {
+  return this.minX <= tileRange.maxX &&
+      this.maxX >= tileRange.minX &&
+      this.minY <= tileRange.maxY &&
+      this.maxY >= tileRange.minY;
+};
+
+goog.provide('ol.Attribution');
+
+goog.require('ol.TileRange');
+goog.require('ol.math');
+
+
+/**
+ * @classdesc
+ * An attribution for a layer source.
+ *
+ * Example:
+ *
+ *     source: new ol.source.OSM({
+ *       attributions: [
+ *         new ol.Attribution({
+ *           html: 'All maps &copy; ' +
+ *               '<a href="http://www.opencyclemap.org/">OpenCycleMap</a>'
+ *         }),
+ *         ol.source.OSM.ATTRIBUTION
+ *       ],
+ *     ..
+ *
+ * @constructor
+ * @param {olx.AttributionOptions} options Attribution options.
+ * @struct
+ * @api stable
+ */
+ol.Attribution = function(options) {
+
+  /**
+   * @private
+   * @type {string}
+   */
+  this.html_ = options.html;
+
+  /**
+   * @private
+   * @type {Object.<string, Array.<ol.TileRange>>}
+   */
+  this.tileRanges_ = options.tileRanges ? options.tileRanges : null;
+
+};
+
+
+/**
+ * Get the attribution markup.
+ * @return {string} The attribution HTML.
+ * @api stable
+ */
+ol.Attribution.prototype.getHTML = function() {
+  return this.html_;
+};
+
+
+/**
+ * @param {Object.<string, ol.TileRange>} tileRanges Tile ranges.
+ * @param {!ol.tilegrid.TileGrid} tileGrid Tile grid.
+ * @param {!ol.proj.Projection} projection Projection.
+ * @return {boolean} Intersects any tile range.
+ */
+ol.Attribution.prototype.intersectsAnyTileRange = function(tileRanges, tileGrid, projection) {
+  if (!this.tileRanges_) {
+    return true;
+  }
+  var i, ii, tileRange, zKey;
+  for (zKey in tileRanges) {
+    if (!(zKey in this.tileRanges_)) {
+      continue;
+    }
+    tileRange = tileRanges[zKey];
+    var testTileRange;
+    for (i = 0, ii = this.tileRanges_[zKey].length; i < ii; ++i) {
+      testTileRange = this.tileRanges_[zKey][i];
+      if (testTileRange.intersects(tileRange)) {
+        return true;
+      }
+      var extentTileRange = tileGrid.getTileRangeForExtentAndZ(
+          ol.tilegrid.extentFromProjection(projection), parseInt(zKey, 10));
+      var width = extentTileRange.getWidth();
+      if (tileRange.minX < extentTileRange.minX ||
+          tileRange.maxX > extentTileRange.maxX) {
+        if (testTileRange.intersects(new ol.TileRange(
+            ol.math.modulo(tileRange.minX, width),
+            ol.math.modulo(tileRange.maxX, width),
+            tileRange.minY, tileRange.maxY))) {
+          return true;
+        }
+        if (tileRange.getWidth() > width &&
+            testTileRange.intersects(extentTileRange)) {
+          return true;
+        }
+      }
+    }
+  }
+  return false;
+};
+
+/**
+ * An implementation of Google Maps' MVCArray.
+ * @see https://developers.google.com/maps/documentation/javascript/reference
+ */
+
+goog.provide('ol.Collection');
+goog.provide('ol.CollectionEvent');
+goog.provide('ol.CollectionEventType');
+
+goog.require('ol.events.Event');
+goog.require('ol.Object');
+
+
+/**
+ * @enum {string}
+ */
+ol.CollectionEventType = {
+  /**
+   * Triggered when an item is added to the collection.
+   * @event ol.CollectionEvent#add
+   * @api stable
+   */
+  ADD: 'add',
+  /**
+   * Triggered when an item is removed from the collection.
+   * @event ol.CollectionEvent#remove
+   * @api stable
+   */
+  REMOVE: 'remove'
+};
+
+
+/**
+ * @classdesc
+ * Events emitted by {@link ol.Collection} instances are instances of this
+ * type.
+ *
+ * @constructor
+ * @extends {ol.events.Event}
+ * @implements {oli.CollectionEvent}
+ * @param {ol.CollectionEventType} type Type.
+ * @param {*=} opt_element Element.
+ * @param {Object=} opt_target Target.
+ */
+ol.CollectionEvent = function(type, opt_element, opt_target) {
+
+  ol.events.Event.call(this, type, opt_target);
+
+  /**
+   * The element that is added to or removed from the collection.
+   * @type {*}
+   * @api stable
+   */
+  this.element = opt_element;
+
+};
+ol.inherits(ol.CollectionEvent, ol.events.Event);
+
+
+/**
+ * @enum {string}
+ */
+ol.CollectionProperty = {
+  LENGTH: 'length'
+};
+
+
+/**
+ * @classdesc
+ * An expanded version of standard JS Array, adding convenience methods for
+ * manipulation. Add and remove changes to the Collection trigger a Collection
+ * event. Note that this does not cover changes to the objects _within_ the
+ * Collection; they trigger events on the appropriate object, not on the
+ * Collection as a whole.
+ *
+ * @constructor
+ * @extends {ol.Object}
+ * @fires ol.CollectionEvent
+ * @param {!Array.<T>=} opt_array Array.
+ * @template T
+ * @api stable
+ */
+ol.Collection = function(opt_array) {
+
+  ol.Object.call(this);
+
+  /**
+   * @private
+   * @type {!Array.<T>}
+   */
+  this.array_ = opt_array ? opt_array : [];
+
+  this.updateLength_();
+
+};
+ol.inherits(ol.Collection, ol.Object);
+
+
+/**
+ * Remove all elements from the collection.
+ * @api stable
+ */
+ol.Collection.prototype.clear = function() {
+  while (this.getLength() > 0) {
+    this.pop();
+  }
+};
+
+
+/**
+ * Add elements to the collection.  This pushes each item in the provided array
+ * to the end of the collection.
+ * @param {!Array.<T>} arr Array.
+ * @return {ol.Collection.<T>} This collection.
+ * @api stable
+ */
+ol.Collection.prototype.extend = function(arr) {
+  var i, ii;
+  for (i = 0, ii = arr.length; i < ii; ++i) {
+    this.push(arr[i]);
+  }
+  return this;
+};
+
+
+/**
+ * Iterate over each element, calling the provided callback.
+ * @param {function(this: S, T, number, Array.<T>): *} f The function to call
+ *     for every element. This function takes 3 arguments (the element, the
+ *     index and the array). The return value is ignored.
+ * @param {S=} opt_this The object to use as `this` in `f`.
+ * @template S
+ * @api stable
+ */
+ol.Collection.prototype.forEach = function(f, opt_this) {
+  this.array_.forEach(f, opt_this);
+};
+
+
+/**
+ * Get a reference to the underlying Array object. Warning: if the array
+ * is mutated, no events will be dispatched by the collection, and the
+ * collection's "length" property won't be in sync with the actual length
+ * of the array.
+ * @return {!Array.<T>} Array.
+ * @api stable
+ */
+ol.Collection.prototype.getArray = function() {
+  return this.array_;
+};
+
+
+/**
+ * Get the element at the provided index.
+ * @param {number} index Index.
+ * @return {T} Element.
+ * @api stable
+ */
+ol.Collection.prototype.item = function(index) {
+  return this.array_[index];
+};
+
+
+/**
+ * Get the length of this collection.
+ * @return {number} The length of the array.
+ * @observable
+ * @api stable
+ */
+ol.Collection.prototype.getLength = function() {
+  return /** @type {number} */ (this.get(ol.CollectionProperty.LENGTH));
+};
+
+
+/**
+ * Insert an element at the provided index.
+ * @param {number} index Index.
+ * @param {T} elem Element.
+ * @api stable
+ */
+ol.Collection.prototype.insertAt = function(index, elem) {
+  this.array_.splice(index, 0, elem);
+  this.updateLength_();
+  this.dispatchEvent(
+      new ol.CollectionEvent(ol.CollectionEventType.ADD, elem, this));
+};
+
+
+/**
+ * Remove the last element of the collection and return it.
+ * Return `undefined` if the collection is empty.
+ * @return {T|undefined} Element.
+ * @api stable
+ */
+ol.Collection.prototype.pop = function() {
+  return this.removeAt(this.getLength() - 1);
+};
+
+
+/**
+ * Insert the provided element at the end of the collection.
+ * @param {T} elem Element.
+ * @return {number} Length.
+ * @api stable
+ */
+ol.Collection.prototype.push = function(elem) {
+  var n = this.array_.length;
+  this.insertAt(n, elem);
+  return n;
+};
+
+
+/**
+ * Remove the first occurrence of an element from the collection.
+ * @param {T} elem Element.
+ * @return {T|undefined} The removed element or undefined if none found.
+ * @api stable
+ */
+ol.Collection.prototype.remove = function(elem) {
+  var arr = this.array_;
+  var i, ii;
+  for (i = 0, ii = arr.length; i < ii; ++i) {
+    if (arr[i] === elem) {
+      return this.removeAt(i);
+    }
+  }
+  return undefined;
+};
+
+
+/**
+ * Remove the element at the provided index and return it.
+ * Return `undefined` if the collection does not contain this index.
+ * @param {number} index Index.
+ * @return {T|undefined} Value.
+ * @api stable
+ */
+ol.Collection.prototype.removeAt = function(index) {
+  var prev = this.array_[index];
+  this.array_.splice(index, 1);
+  this.updateLength_();
+  this.dispatchEvent(
+      new ol.CollectionEvent(ol.CollectionEventType.REMOVE, prev, this));
+  return prev;
+};
+
+
+/**
+ * Set the element at the provided index.
+ * @param {number} index Index.
+ * @param {T} elem Element.
+ * @api stable
+ */
+ol.Collection.prototype.setAt = function(index, elem) {
+  var n = this.getLength();
+  if (index < n) {
+    var prev = this.array_[index];
+    this.array_[index] = elem;
+    this.dispatchEvent(
+        new ol.CollectionEvent(ol.CollectionEventType.REMOVE, prev, this));
+    this.dispatchEvent(
+        new ol.CollectionEvent(ol.CollectionEventType.ADD, elem, this));
+  } else {
+    var j;
+    for (j = n; j < index; ++j) {
+      this.insertAt(j, undefined);
+    }
+    this.insertAt(index, elem);
+  }
+};
+
+
+/**
+ * @private
+ */
+ol.Collection.prototype.updateLength_ = function() {
+  this.set(ol.CollectionProperty.LENGTH, this.array_.length);
+};
+
+// Copyright 2006 The Closure Library Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS-IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+/**
+ * @fileoverview Names of standard colors with their associated hex values.
+ */
+
+goog.provide('goog.color.names');
+
+
+/**
+ * A map that contains a lot of colors that are recognised by various browsers.
+ * This list is way larger than the minimal one dictated by W3C.
+ * The keys of this map are the lowercase "readable" names of the colors, while
+ * the values are the "hex" values.
+ *
+ * @type {!Object<string, string>}
+ */
+goog.color.names = {
+  'aliceblue': '#f0f8ff',
+  'antiquewhite': '#faebd7',
+  'aqua': '#00ffff',
+  'aquamarine': '#7fffd4',
+  'azure': '#f0ffff',
+  'beige': '#f5f5dc',
+  'bisque': '#ffe4c4',
+  'black': '#000000',
+  'blanchedalmond': '#ffebcd',
+  'blue': '#0000ff',
+  'blueviolet': '#8a2be2',
+  'brown': '#a52a2a',
+  'burlywood': '#deb887',
+  'cadetblue': '#5f9ea0',
+  'chartreuse': '#7fff00',
+  'chocolate': '#d2691e',
+  'coral': '#ff7f50',
+  'cornflowerblue': '#6495ed',
+  'cornsilk': '#fff8dc',
+  'crimson': '#dc143c',
+  'cyan': '#00ffff',
+  'darkblue': '#00008b',
+  'darkcyan': '#008b8b',
+  'darkgoldenrod': '#b8860b',
+  'darkgray': '#a9a9a9',
+  'darkgreen': '#006400',
+  'darkgrey': '#a9a9a9',
+  'darkkhaki': '#bdb76b',
+  'darkmagenta': '#8b008b',
+  'darkolivegreen': '#556b2f',
+  'darkorange': '#ff8c00',
+  'darkorchid': '#9932cc',
+  'darkred': '#8b0000',
+  'darksalmon': '#e9967a',
+  'darkseagreen': '#8fbc8f',
+  'darkslateblue': '#483d8b',
+  'darkslategray': '#2f4f4f',
+  'darkslategrey': '#2f4f4f',
+  'darkturquoise': '#00ced1',
+  'darkviolet': '#9400d3',
+  'deeppink': '#ff1493',
+  'deepskyblue': '#00bfff',
+  'dimgray': '#696969',
+  'dimgrey': '#696969',
+  'dodgerblue': '#1e90ff',
+  'firebrick': '#b22222',
+  'floralwhite': '#fffaf0',
+  'forestgreen': '#228b22',
+  'fuchsia': '#ff00ff',
+  'gainsboro': '#dcdcdc',
+  'ghostwhite': '#f8f8ff',
+  'gold': '#ffd700',
+  'goldenrod': '#daa520',
+  'gray': '#808080',
+  'green': '#008000',
+  'greenyellow': '#adff2f',
+  'grey': '#808080',
+  'honeydew': '#f0fff0',
+  'hotpink': '#ff69b4',
+  'indianred': '#cd5c5c',
+  'indigo': '#4b0082',
+  'ivory': '#fffff0',
+  'khaki': '#f0e68c',
+  'lavender': '#e6e6fa',
+  'lavenderblush': '#fff0f5',
+  'lawngreen': '#7cfc00',
+  'lemonchiffon': '#fffacd',
+  'lightblue': '#add8e6',
+  'lightcoral': '#f08080',
+  'lightcyan': '#e0ffff',
+  'lightgoldenrodyellow': '#fafad2',
+  'lightgray': '#d3d3d3',
+  'lightgreen': '#90ee90',
+  'lightgrey': '#d3d3d3',
+  'lightpink': '#ffb6c1',
+  'lightsalmon': '#ffa07a',
+  'lightseagreen': '#20b2aa',
+  'lightskyblue': '#87cefa',
+  'lightslategray': '#778899',
+  'lightslategrey': '#778899',
+  'lightsteelblue': '#b0c4de',
+  'lightyellow': '#ffffe0',
+  'lime': '#00ff00',
+  'limegreen': '#32cd32',
+  'linen': '#faf0e6',
+  'magenta': '#ff00ff',
+  'maroon': '#800000',
+  'mediumaquamarine': '#66cdaa',
+  'mediumblue': '#0000cd',
+  'mediumorchid': '#ba55d3',
+  'mediumpurple': '#9370db',
+  'mediumseagreen': '#3cb371',
+  'mediumslateblue': '#7b68ee',
+  'mediumspringgreen': '#00fa9a',
+  'mediumturquoise': '#48d1cc',
+  'mediumvioletred': '#c71585',
+  'midnightblue': '#191970',
+  'mintcream': '#f5fffa',
+  'mistyrose': '#ffe4e1',
+  'moccasin': '#ffe4b5',
+  'navajowhite': '#ffdead',
+  'navy': '#000080',
+  'oldlace': '#fdf5e6',
+  'olive': '#808000',
+  'olivedrab': '#6b8e23',
+  'orange': '#ffa500',
+  'orangered': '#ff4500',
+  'orchid': '#da70d6',
+  'palegoldenrod': '#eee8aa',
+  'palegreen': '#98fb98',
+  'paleturquoise': '#afeeee',
+  'palevioletred': '#db7093',
+  'papayawhip': '#ffefd5',
+  'peachpuff': '#ffdab9',
+  'peru': '#cd853f',
+  'pink': '#ffc0cb',
+  'plum': '#dda0dd',
+  'powderblue': '#b0e0e6',
+  'purple': '#800080',
+  'red': '#ff0000',
+  'rosybrown': '#bc8f8f',
+  'royalblue': '#4169e1',
+  'saddlebrown': '#8b4513',
+  'salmon': '#fa8072',
+  'sandybrown': '#f4a460',
+  'seagreen': '#2e8b57',
+  'seashell': '#fff5ee',
+  'sienna': '#a0522d',
+  'silver': '#c0c0c0',
+  'skyblue': '#87ceeb',
+  'slateblue': '#6a5acd',
+  'slategray': '#708090',
+  'slategrey': '#708090',
+  'snow': '#fffafa',
+  'springgreen': '#00ff7f',
+  'steelblue': '#4682b4',
+  'tan': '#d2b48c',
+  'teal': '#008080',
+  'thistle': '#d8bfd8',
+  'tomato': '#ff6347',
+  'turquoise': '#40e0d0',
+  'violet': '#ee82ee',
+  'wheat': '#f5deb3',
+  'white': '#ffffff',
+  'whitesmoke': '#f5f5f5',
+  'yellow': '#ffff00',
+  'yellowgreen': '#9acd32'
+};
+
+// Copyright 2006 The Closure Library Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS-IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+/**
+ * @fileoverview Utilities for manipulating arrays.
+ *
+ * @author arv@google.com (Erik Arvidsson)
+ */
+
+
+goog.provide('goog.array');
+
+goog.require('goog.asserts');
+
+
+/**
+ * @define {boolean} NATIVE_ARRAY_PROTOTYPES indicates whether the code should
+ * rely on Array.prototype functions, if available.
+ *
+ * The Array.prototype functions can be defined by external libraries like
+ * Prototype and setting this flag to false forces closure to use its own
+ * goog.array implementation.
+ *
+ * If your javascript can be loaded by a third party site and you are wary about
+ * relying on the prototype functions, specify
+ * "--define goog.NATIVE_ARRAY_PROTOTYPES=false" to the JSCompiler.
+ *
+ * Setting goog.TRUSTED_SITE to false will automatically set
+ * NATIVE_ARRAY_PROTOTYPES to false.
+ */
+goog.define('goog.NATIVE_ARRAY_PROTOTYPES', goog.TRUSTED_SITE);
+
+
+/**
+ * @define {boolean} If true, JSCompiler will use the native implementation of
+ * array functions where appropriate (e.g., {@code Array#filter}) and remove the
+ * unused pure JS implementation.
+ */
+goog.define('goog.array.ASSUME_NATIVE_FUNCTIONS', false);
+
+
+/**
+ * Returns the last element in an array without removing it.
+ * Same as goog.array.last.
+ * @param {IArrayLike<T>|string} array The array.
+ * @return {T} Last item in array.
+ * @template T
+ */
+goog.array.peek = function(array) {
+  return array[array.length - 1];
+};
+
+
+/**
+ * Returns the last element in an array without removing it.
+ * Same as goog.array.peek.
+ * @param {IArrayLike<T>|string} array The array.
+ * @return {T} Last item in array.
+ * @template T
+ */
+goog.array.last = goog.array.peek;
+
+// NOTE(arv): Since most of the array functions are generic it allows you to
+// pass an array-like object. Strings have a length and are considered array-
+// like. However, the 'in' operator does not work on strings so we cannot just
+// use the array path even if the browser supports indexing into strings. We
+// therefore end up splitting the string.
+
+
+/**
+ * Returns the index of the first element of an array with a specified value, or
+ * -1 if the element is not present in the array.
+ *
+ * See {@link http://tinyurl.com/developer-mozilla-org-array-indexof}
+ *
+ * @param {IArrayLike<T>|string} arr The array to be searched.
+ * @param {T} obj The object for which we are searching.
+ * @param {number=} opt_fromIndex The index at which to start the search. If
+ *     omitted the search starts at index 0.
+ * @return {number} The index of the first matching array element.
+ * @template T
+ */
+goog.array.indexOf = goog.NATIVE_ARRAY_PROTOTYPES &&
+        (goog.array.ASSUME_NATIVE_FUNCTIONS || Array.prototype.indexOf) ?
+    function(arr, obj, opt_fromIndex) {
+      goog.asserts.assert(arr.length != null);
+
+      return Array.prototype.indexOf.call(arr, obj, opt_fromIndex);
+    } :
+    function(arr, obj, opt_fromIndex) {
+      var fromIndex = opt_fromIndex == null ?
+          0 :
+          (opt_fromIndex < 0 ? Math.max(0, arr.length + opt_fromIndex) :
+                               opt_fromIndex);
+
+      if (goog.isString(arr)) {
+        // Array.prototype.indexOf uses === so only strings should be found.
+        if (!goog.isString(obj) || obj.length != 1) {
+          return -1;
+        }
+        return arr.indexOf(obj, fromIndex);
+      }
+
+      for (var i = fromIndex; i < arr.length; i++) {
+        if (i in arr && arr[i] === obj) return i;
+      }
+      return -1;
+    };
+
+
+/**
+ * Returns the index of the last element of an array with a specified value, or
+ * -1 if the element is not present in the array.
+ *
+ * See {@link http://tinyurl.com/developer-mozilla-org-array-lastindexof}
+ *
+ * @param {!IArrayLike<T>|string} arr The array to be searched.
+ * @param {T} obj The object for which we are searching.
+ * @param {?number=} opt_fromIndex The index at which to start the search. If
+ *     omitted the search starts at the end of the array.
+ * @return {number} The index of the last matching array element.
+ * @template T
+ */
+goog.array.lastIndexOf = goog.NATIVE_ARRAY_PROTOTYPES &&
+        (goog.array.ASSUME_NATIVE_FUNCTIONS || Array.prototype.lastIndexOf) ?
+    function(arr, obj, opt_fromIndex) {
+      goog.asserts.assert(arr.length != null);
+
+      // Firefox treats undefined and null as 0 in the fromIndex argument which
+      // leads it to always return -1
+      var fromIndex = opt_fromIndex == null ? arr.length - 1 : opt_fromIndex;
+      return Array.prototype.lastIndexOf.call(arr, obj, fromIndex);
+    } :
+    function(arr, obj, opt_fromIndex) {
+      var fromIndex = opt_fromIndex == null ? arr.length - 1 : opt_fromIndex;
+
+      if (fromIndex < 0) {
+        fromIndex = Math.max(0, arr.length + fromIndex);
+      }
+
+      if (goog.isString(arr)) {
+        // Array.prototype.lastIndexOf uses === so only strings should be found.
+        if (!goog.isString(obj) || obj.length != 1) {
+          return -1;
+        }
+        return arr.lastIndexOf(obj, fromIndex);
+      }
+
+      for (var i = fromIndex; i >= 0; i--) {
+        if (i in arr && arr[i] === obj) return i;
+      }
+      return -1;
+    };
+
+
+/**
+ * Calls a function for each element in an array. Skips holes in the array.
+ * See {@link http://tinyurl.com/developer-mozilla-org-array-foreach}
+ *
+ * @param {IArrayLike<T>|string} arr Array or array like object over
+ *     which to iterate.
+ * @param {?function(this: S, T, number, ?): ?} f The function to call for every
+ *     element. This function takes 3 arguments (the element, the index and the
+ *     array). The return value is ignored.
+ * @param {S=} opt_obj The object to be used as the value of 'this' within f.
+ * @template T,S
+ */
+goog.array.forEach = goog.NATIVE_ARRAY_PROTOTYPES &&
+        (goog.array.ASSUME_NATIVE_FUNCTIONS || Array.prototype.forEach) ?
+    function(arr, f, opt_obj) {
+      goog.asserts.assert(arr.length != null);
+
+      Array.prototype.forEach.call(arr, f, opt_obj);
+    } :
+    function(arr, f, opt_obj) {
+      var l = arr.length;  // must be fixed during loop... see docs
+      var arr2 = goog.isString(arr) ? arr.split('') : arr;
+      for (var i = 0; i < l; i++) {
+        if (i in arr2) {
+          f.call(/** @type {?} */ (opt_obj), arr2[i], i, arr);
+        }
+      }
+    };
+
+
+/**
+ * Calls a function for each element in an array, starting from the last
+ * element rather than the first.
+ *
+ * @param {IArrayLike<T>|string} arr Array or array
+ *     like object over which to iterate.
+ * @param {?function(this: S, T, number, ?): ?} f The function to call for every
+ *     element. This function
+ *     takes 3 arguments (the element, the index and the array). The return
+ *     value is ignored.
+ * @param {S=} opt_obj The object to be used as the value of 'this'
+ *     within f.
+ * @template T,S
+ */
+goog.array.forEachRight = function(arr, f, opt_obj) {
+  var l = arr.length;  // must be fixed during loop... see docs
+  var arr2 = goog.isString(arr) ? arr.split('') : arr;
+  for (var i = l - 1; i >= 0; --i) {
+    if (i in arr2) {
+      f.call(/** @type {?} */ (opt_obj), arr2[i], i, arr);
+    }
+  }
+};
+
+
+/**
+ * Calls a function for each element in an array, and if the function returns
+ * true adds the element to a new array.
+ *
+ * See {@link http://tinyurl.com/developer-mozilla-org-array-filter}
+ *
+ * @param {IArrayLike<T>|string} arr Array or array
+ *     like object over which to iterate.
+ * @param {?function(this:S, T, number, ?):boolean} f The function to call for
+ *     every element. This function
+ *     takes 3 arguments (the element, the index and the array) and must
+ *     return a Boolean. If the return value is true the element is added to the
+ *     result array. If it is false the element is not included.
+ * @param {S=} opt_obj The object to be used as the value of 'this'
+ *     within f.
+ * @return {!Array<T>} a new array in which only elements that passed the test
+ *     are present.
+ * @template T,S
+ */
+goog.array.filter = goog.NATIVE_ARRAY_PROTOTYPES &&
+        (goog.array.ASSUME_NATIVE_FUNCTIONS || Array.prototype.filter) ?
+    function(arr, f, opt_obj) {
+      goog.asserts.assert(arr.length != null);
+
+      return Array.prototype.filter.call(arr, f, opt_obj);
+    } :
+    function(arr, f, opt_obj) {
+      var l = arr.length;  // must be fixed during loop... see docs
+      var res = [];
+      var resLength = 0;
+      var arr2 = goog.isString(arr) ? arr.split('') : arr;
+      for (var i = 0; i < l; i++) {
+        if (i in arr2) {
+          var val = arr2[i];  // in case f mutates arr2
+          if (f.call(/** @type {?} */ (opt_obj), val, i, arr)) {
+            res[resLength++] = val;
+          }
+        }
+      }
+      return res;
+    };
+
+
+/**
+ * Calls a function for each element in an array and inserts the result into a
+ * new array.
+ *
+ * See {@link http://tinyurl.com/developer-mozilla-org-array-map}
+ *
+ * @param {IArrayLike<VALUE>|string} arr Array or array like object
+ *     over which to iterate.
+ * @param {function(this:THIS, VALUE, number, ?): RESULT} f The function to call
+ *     for every element. This function takes 3 arguments (the element,
+ *     the index and the array) and should return something. The result will be
+ *     inserted into a new array.
+ * @param {THIS=} opt_obj The object to be used as the value of 'this' within f.
+ * @return {!Array<RESULT>} a new array with the results from f.
+ * @template THIS, VALUE, RESULT
+ */
+goog.array.map = goog.NATIVE_ARRAY_PROTOTYPES &&
+        (goog.array.ASSUME_NATIVE_FUNCTIONS || Array.prototype.map) ?
+    function(arr, f, opt_obj) {
+      goog.asserts.assert(arr.length != null);
+
+      return Array.prototype.map.call(arr, f, opt_obj);
+    } :
+    function(arr, f, opt_obj) {
+      var l = arr.length;  // must be fixed during loop... see docs
+      var res = new Array(l);
+      var arr2 = goog.isString(arr) ? arr.split('') : arr;
+      for (var i = 0; i < l; i++) {
+        if (i in arr2) {
+          res[i] = f.call(/** @type {?} */ (opt_obj), arr2[i], i, arr);
+        }
+      }
+      return res;
+    };
+
+
+/**
+ * Passes every element of an array into a function and accumulates the result.
+ *
+ * See {@link http://tinyurl.com/developer-mozilla-org-array-reduce}
+ *
+ * For example:
+ * var a = [1, 2, 3, 4];
+ * goog.array.reduce(a, function(r, v, i, arr) {return r + v;}, 0);
+ * returns 10
+ *
+ * @param {IArrayLike<T>|string} arr Array or array
+ *     like object over which to iterate.
+ * @param {function(this:S, R, T, number, ?) : R} f The function to call for
+ *     every element. This function
+ *     takes 4 arguments (the function's previous result or the initial value,
+ *     the value of the current array element, the current array index, and the
+ *     array itself)
+ *     function(previousValue, currentValue, index, array).
+ * @param {?} val The initial value to pass into the function on the first call.
+ * @param {S=} opt_obj  The object to be used as the value of 'this'
+ *     within f.
+ * @return {R} Result of evaluating f repeatedly across the values of the array.
+ * @template T,S,R
+ */
+goog.array.reduce = goog.NATIVE_ARRAY_PROTOTYPES &&
+        (goog.array.ASSUME_NATIVE_FUNCTIONS || Array.prototype.reduce) ?
+    function(arr, f, val, opt_obj) {
+      goog.asserts.assert(arr.length != null);
+      if (opt_obj) {
+        f = goog.bind(f, opt_obj);
+      }
+      return Array.prototype.reduce.call(arr, f, val);
+    } :
+    function(arr, f, val, opt_obj) {
+      var rval = val;
+      goog.array.forEach(arr, function(val, index) {
+        rval = f.call(/** @type {?} */ (opt_obj), rval, val, index, arr);
+      });
+      return rval;
+    };
+
+
+/**
+ * Passes every element of an array into a function and accumulates the result,
+ * starting from the last element and working towards the first.
+ *
+ * See {@link http://tinyurl.com/developer-mozilla-org-array-reduceright}
+ *
+ * For example:
+ * var a = ['a', 'b', 'c'];
+ * goog.array.reduceRight(a, function(r, v, i, arr) {return r + v;}, '');
+ * returns 'cba'
+ *
+ * @param {IArrayLike<T>|string} arr Array or array
+ *     like object over which to iterate.
+ * @param {?function(this:S, R, T, number, ?) : R} f The function to call for
+ *     every element. This function
+ *     takes 4 arguments (the function's previous result or the initial value,
+ *     the value of the current array element, the current array index, and the
+ *     array itself)
+ *     function(previousValue, currentValue, index, array).
+ * @param {?} val The initial value to pass into the function on the first call.
+ * @param {S=} opt_obj The object to be used as the value of 'this'
+ *     within f.
+ * @return {R} Object returned as a result of evaluating f repeatedly across the
+ *     values of the array.
+ * @template T,S,R
+ */
+goog.array.reduceRight = goog.NATIVE_ARRAY_PROTOTYPES &&
+        (goog.array.ASSUME_NATIVE_FUNCTIONS || Array.prototype.reduceRight) ?
+    function(arr, f, val, opt_obj) {
+      goog.asserts.assert(arr.length != null);
+      goog.asserts.assert(f != null);
+      if (opt_obj) {
+        f = goog.bind(f, opt_obj);
+      }
+      return Array.prototype.reduceRight.call(arr, f, val);
+    } :
+    function(arr, f, val, opt_obj) {
+      var rval = val;
+      goog.array.forEachRight(arr, function(val, index) {
+        rval = f.call(/** @type {?} */ (opt_obj), rval, val, index, arr);
+      });
+      return rval;
+    };
+
+
+/**
+ * Calls f for each element of an array. If any call returns true, some()
+ * returns true (without checking the remaining elements). If all calls
+ * return false, some() returns false.
+ *
+ * See {@link http://tinyurl.com/developer-mozilla-org-array-some}
+ *
+ * @param {IArrayLike<T>|string} arr Array or array
+ *     like object over which to iterate.
+ * @param {?function(this:S, T, number, ?) : boolean} f The function to call for
+ *     for every element. This function takes 3 arguments (the element, the
+ *     index and the array) and should return a boolean.
+ * @param {S=} opt_obj  The object to be used as the value of 'this'
+ *     within f.
+ * @return {boolean} true if any element passes the test.
+ * @template T,S
+ */
+goog.array.some = goog.NATIVE_ARRAY_PROTOTYPES &&
+        (goog.array.ASSUME_NATIVE_FUNCTIONS || Array.prototype.some) ?
+    function(arr, f, opt_obj) {
+      goog.asserts.assert(arr.length != null);
+
+      return Array.prototype.some.call(arr, f, opt_obj);
+    } :
+    function(arr, f, opt_obj) {
+      var l = arr.length;  // must be fixed during loop... see docs
+      var arr2 = goog.isString(arr) ? arr.split('') : arr;
+      for (var i = 0; i < l; i++) {
+        if (i in arr2 && f.call(/** @type {?} */ (opt_obj), arr2[i], i, arr)) {
+          return true;
+        }
+      }
+      return false;
+    };
+
+
+/**
+ * Call f for each element of an array. If all calls return true, every()
+ * returns true. If any call returns false, every() returns false and
+ * does not continue to check the remaining elements.
+ *
+ * See {@link http://tinyurl.com/developer-mozilla-org-array-every}
+ *
+ * @param {IArrayLike<T>|string} arr Array or array
+ *     like object over which to iterate.
+ * @param {?function(this:S, T, number, ?) : boolean} f The function to call for
+ *     for every element. This function takes 3 arguments (the element, the
+ *     index and the array) and should return a boolean.
+ * @param {S=} opt_obj The object to be used as the value of 'this'
+ *     within f.
+ * @return {boolean} false if any element fails the test.
+ * @template T,S
+ */
+goog.array.every = goog.NATIVE_ARRAY_PROTOTYPES &&
+        (goog.array.ASSUME_NATIVE_FUNCTIONS || Array.prototype.every) ?
+    function(arr, f, opt_obj) {
+      goog.asserts.assert(arr.length != null);
+
+      return Array.prototype.every.call(arr, f, opt_obj);
+    } :
+    function(arr, f, opt_obj) {
+      var l = arr.length;  // must be fixed during loop... see docs
+      var arr2 = goog.isString(arr) ? arr.split('') : arr;
+      for (var i = 0; i < l; i++) {
+        if (i in arr2 && !f.call(/** @type {?} */ (opt_obj), arr2[i], i, arr)) {
+          return false;
+        }
+      }
+      return true;
+    };
+
+
+/**
+ * Counts the array elements that fulfill the predicate, i.e. for which the
+ * callback function returns true. Skips holes in the array.
+ *
+ * @param {!IArrayLike<T>|string} arr Array or array like object
+ *     over which to iterate.
+ * @param {function(this: S, T, number, ?): boolean} f The function to call for
+ *     every element. Takes 3 arguments (the element, the index and the array).
+ * @param {S=} opt_obj The object to be used as the value of 'this' within f.
+ * @return {number} The number of the matching elements.
+ * @template T,S
+ */
+goog.array.count = function(arr, f, opt_obj) {
+  var count = 0;
+  goog.array.forEach(arr, function(element, index, arr) {
+    if (f.call(/** @type {?} */ (opt_obj), element, index, arr)) {
+      ++count;
+    }
+  }, opt_obj);
+  return count;
+};
+
+
+/**
+ * Search an array for the first element that satisfies a given condition and
+ * return that element.
+ * @param {IArrayLike<T>|string} arr Array or array
+ *     like object over which to iterate.
+ * @param {?function(this:S, T, number, ?) : boolean} f The function to call
+ *     for every element. This function takes 3 arguments (the element, the
+ *     index and the array) and should return a boolean.
+ * @param {S=} opt_obj An optional "this" context for the function.
+ * @return {T|null} The first array element that passes the test, or null if no
+ *     element is found.
+ * @template T,S
+ */
+goog.array.find = function(arr, f, opt_obj) {
+  var i = goog.array.findIndex(arr, f, opt_obj);
+  return i < 0 ? null : goog.isString(arr) ? arr.charAt(i) : arr[i];
+};
+
+
+/**
+ * Search an array for the first element that satisfies a given condition and
+ * return its index.
+ * @param {IArrayLike<T>|string} arr Array or array
+ *     like object over which to iterate.
+ * @param {?function(this:S, T, number, ?) : boolean} f The function to call for
+ *     every element. This function
+ *     takes 3 arguments (the element, the index and the array) and should
+ *     return a boolean.
+ * @param {S=} opt_obj An optional "this" context for the function.
+ * @return {number} The index of the first array element that passes the test,
+ *     or -1 if no element is found.
+ * @template T,S
+ */
+goog.array.findIndex = function(arr, f, opt_obj) {
+  var l = arr.length;  // must be fixed during loop... see docs
+  var arr2 = goog.isString(arr) ? arr.split('') : arr;
+  for (var i = 0; i < l; i++) {
+    if (i in arr2 && f.call(/** @type {?} */ (opt_obj), arr2[i], i, arr)) {
+      return i;
+    }
+  }
+  return -1;
+};
+
+
+/**
+ * Search an array (in reverse order) for the last element that satisfies a
+ * given condition and return that element.
+ * @param {IArrayLike<T>|string} arr Array or array
+ *     like object over which to iterate.
+ * @param {?function(this:S, T, number, ?) : boolean} f The function to call
+ *     for every element. This function
+ *     takes 3 arguments (the element, the index and the array) and should
+ *     return a boolean.
+ * @param {S=} opt_obj An optional "this" context for the function.
+ * @return {T|null} The last array element that passes the test, or null if no
+ *     element is found.
+ * @template T,S
+ */
+goog.array.findRight = function(arr, f, opt_obj) {
+  var i = goog.array.findIndexRight(arr, f, opt_obj);
+  return i < 0 ? null : goog.isString(arr) ? arr.charAt(i) : arr[i];
+};
+
+
+/**
+ * Search an array (in reverse order) for the last element that satisfies a
+ * given condition and return its index.
+ * @param {IArrayLike<T>|string} arr Array or array
+ *     like object over which to iterate.
+ * @param {?function(this:S, T, number, ?) : boolean} f The function to call
+ *     for every element. This function
+ *     takes 3 arguments (the element, the index and the array) and should
+ *     return a boolean.
+ * @param {S=} opt_obj An optional "this" context for the function.
+ * @return {number} The index of the last array element that passes the test,
+ *     or -1 if no element is found.
+ * @template T,S
+ */
+goog.array.findIndexRight = function(arr, f, opt_obj) {
+  var l = arr.length;  // must be fixed during loop... see docs
+  var arr2 = goog.isString(arr) ? arr.split('') : arr;
+  for (var i = l - 1; i >= 0; i--) {
+    if (i in arr2 && f.call(/** @type {?} */ (opt_obj), arr2[i], i, arr)) {
+      return i;
+    }
+  }
+  return -1;
+};
+
+
+/**
+ * Whether the array contains the given object.
+ * @param {IArrayLike<?>|string} arr The array to test for the presence of the
+ *     element.
+ * @param {*} obj The object for which to test.
+ * @return {boolean} true if obj is present.
+ */
+goog.array.contains = function(arr, obj) {
+  return goog.array.indexOf(arr, obj) >= 0;
+};
+
+
+/**
+ * Whether the array is empty.
+ * @param {IArrayLike<?>|string} arr The array to test.
+ * @return {boolean} true if empty.
+ */
+goog.array.isEmpty = function(arr) {
+  return arr.length == 0;
+};
+
+
+/**
+ * Clears the array.
+ * @param {IArrayLike<?>} arr Array or array like object to clear.
+ */
+goog.array.clear = function(arr) {
+  // For non real arrays we don't have the magic length so we delete the
+  // indices.
+  if (!goog.isArray(arr)) {
+    for (var i = arr.length - 1; i >= 0; i--) {
+      delete arr[i];
+    }
+  }
+  arr.length = 0;
+};
+
+
+/**
+ * Pushes an item into an array, if it's not already in the array.
+ * @param {Array<T>} arr Array into which to insert the item.
+ * @param {T} obj Value to add.
+ * @template T
+ */
+goog.array.insert = function(arr, obj) {
+  if (!goog.array.contains(arr, obj)) {
+    arr.push(obj);
+  }
+};
+
+
+/**
+ * Inserts an object at the given index of the array.
+ * @param {IArrayLike<?>} arr The array to modify.
+ * @param {*} obj The object to insert.
+ * @param {number=} opt_i The index at which to insert the object. If omitted,
+ *      treated as 0. A negative index is counted from the end of the array.
+ */
+goog.array.insertAt = function(arr, obj, opt_i) {
+  goog.array.splice(arr, opt_i, 0, obj);
+};
+
+
+/**
+ * Inserts at the given index of the array, all elements of another array.
+ * @param {IArrayLike<?>} arr The array to modify.
+ * @param {IArrayLike<?>} elementsToAdd The array of elements to add.
+ * @param {number=} opt_i The index at which to insert the object. If omitted,
+ *      treated as 0. A negative index is counted from the end of the array.
+ */
+goog.array.insertArrayAt = function(arr, elementsToAdd, opt_i) {
+  goog.partial(goog.array.splice, arr, opt_i, 0).apply(null, elementsToAdd);
+};
+
+
+/**
+ * Inserts an object into an array before a specified object.
+ * @param {Array<T>} arr The array to modify.
+ * @param {T} obj The object to insert.
+ * @param {T=} opt_obj2 The object before which obj should be inserted. If obj2
+ *     is omitted or not found, obj is inserted at the end of the array.
+ * @template T
+ */
+goog.array.insertBefore = function(arr, obj, opt_obj2) {
+  var i;
+  if (arguments.length == 2 || (i = goog.array.indexOf(arr, opt_obj2)) < 0) {
+    arr.push(obj);
+  } else {
+    goog.array.insertAt(arr, obj, i);
+  }
+};
+
+
+/**
+ * Removes the first occurrence of a particular value from an array.
+ * @param {IArrayLike<T>} arr Array from which to remove
+ *     value.
+ * @param {T} obj Object to remove.
+ * @return {boolean} True if an element was removed.
+ * @template T
+ */
+goog.array.remove = function(arr, obj) {
+  var i = goog.array.indexOf(arr, obj);
+  var rv;
+  if ((rv = i >= 0)) {
+    goog.array.removeAt(arr, i);
+  }
+  return rv;
+};
+
+
+/**
+ * Removes the last occurrence of a particular value from an array.
+ * @param {!IArrayLike<T>} arr Array from which to remove value.
+ * @param {T} obj Object to remove.
+ * @return {boolean} True if an element was removed.
+ * @template T
+ */
+goog.array.removeLast = function(arr, obj) {
+  var i = goog.array.lastIndexOf(arr, obj);
+  if (i >= 0) {
+    goog.array.removeAt(arr, i);
+    return true;
+  }
+  return false;
+};
+
+
+/**
+ * Removes from an array the element at index i
+ * @param {IArrayLike<?>} arr Array or array like object from which to
+ *     remove value.
+ * @param {number} i The index to remove.
+ * @return {boolean} True if an element was removed.
+ */
+goog.array.removeAt = function(arr, i) {
+  goog.asserts.assert(arr.length != null);
+
+  // use generic form of splice
+  // splice returns the removed items and if successful the length of that
+  // will be 1
+  return Array.prototype.splice.call(arr, i, 1).length == 1;
+};
+
+
+/**
+ * Removes the first value that satisfies the given condition.
+ * @param {IArrayLike<T>} arr Array or array
+ *     like object over which to iterate.
+ * @param {?function(this:S, T, number, ?) : boolean} f The function to call
+ *     for every element. This function
+ *     takes 3 arguments (the element, the index and the array) and should
+ *     return a boolean.
+ * @param {S=} opt_obj An optional "this" context for the function.
+ * @return {boolean} True if an element was removed.
+ * @template T,S
+ */
+goog.array.removeIf = function(arr, f, opt_obj) {
+  var i = goog.array.findIndex(arr, f, opt_obj);
+  if (i >= 0) {
+    goog.array.removeAt(arr, i);
+    return true;
+  }
+  return false;
+};
+
+
+/**
+ * Removes all values that satisfy the given condition.
+ * @param {IArrayLike<T>} arr Array or array
+ *     like object over which to iterate.
+ * @param {?function(this:S, T, number, ?) : boolean} f The function to call
+ *     for every element. This function
+ *     takes 3 arguments (the element, the index and the array) and should
+ *     return a boolean.
+ * @param {S=} opt_obj An optional "this" context for the function.
+ * @return {number} The number of items removed
+ * @template T,S
+ */
+goog.array.removeAllIf = function(arr, f, opt_obj) {
+  var removedCount = 0;
+  goog.array.forEachRight(arr, function(val, index) {
+    if (f.call(/** @type {?} */ (opt_obj), val, index, arr)) {
+      if (goog.array.removeAt(arr, index)) {
+        removedCount++;
+      }
+    }
+  });
+  return removedCount;
+};
+
+
+/**
+ * Returns a new array that is the result of joining the arguments.  If arrays
+ * are passed then their items are added, however, if non-arrays are passed they
+ * will be added to the return array as is.
+ *
+ * Note that ArrayLike objects will be added as is, rather than having their
+ * items added.
+ *
+ * goog.array.concat([1, 2], [3, 4]) -> [1, 2, 3, 4]
+ * goog.array.concat(0, [1, 2]) -> [0, 1, 2]
+ * goog.array.concat([1, 2], null) -> [1, 2, null]
+ *
+ * There is bug in all current versions of IE (6, 7 and 8) where arrays created
+ * in an iframe become corrupted soon (not immediately) after the iframe is
+ * destroyed. This is common if loading data via goog.net.IframeIo, for example.
+ * This corruption only affects the concat method which will start throwing
+ * Catastrophic Errors (#-2147418113).
+ *
+ * See http://endoflow.com/scratch/corrupted-arrays.html for a test case.
+ *
+ * Internally goog.array should use this, so that all methods will continue to
+ * work on these broken array objects.
+ *
+ * @param {...*} var_args Items to concatenate.  Arrays will have each item
+ *     added, while primitives and objects will be added as is.
+ * @return {!Array<?>} The new resultant array.
+ */
+goog.array.concat = function(var_args) {
+  return Array.prototype.concat.apply(Array.prototype, arguments);
+};
+
+
+/**
+ * Returns a new array that contains the contents of all the arrays passed.
+ * @param {...!Array<T>} var_args
+ * @return {!Array<T>}
+ * @template T
+ */
+goog.array.join = function(var_args) {
+  return Array.prototype.concat.apply(Array.prototype, arguments);
+};
+
+
+/**
+ * Converts an object to an array.
+ * @param {IArrayLike<T>|string} object  The object to convert to an
+ *     array.
+ * @return {!Array<T>} The object converted into an array. If object has a
+ *     length property, every property indexed with a non-negative number
+ *     less than length will be included in the result. If object does not
+ *     have a length property, an empty array will be returned.
+ * @template T
+ */
+goog.array.toArray = function(object) {
+  var length = object.length;
+
+  // If length is not a number the following it false. This case is kept for
+  // backwards compatibility since there are callers that pass objects that are
+  // not array like.
+  if (length > 0) {
+    var rv = new Array(length);
+    for (var i = 0; i < length; i++) {
+      rv[i] = object[i];
+    }
+    return rv;
+  }
+  return [];
+};
+
+
+/**
+ * Does a shallow copy of an array.
+ * @param {IArrayLike<T>|string} arr  Array or array-like object to
+ *     clone.
+ * @return {!Array<T>} Clone of the input array.
+ * @template T
+ */
+goog.array.clone = goog.array.toArray;
+
+
+/**
+ * Extends an array with another array, element, or "array like" object.
+ * This function operates 'in-place', it does not create a new Array.
+ *
+ * Example:
+ * var a = [];
+ * goog.array.extend(a, [0, 1]);
+ * a; // [0, 1]
+ * goog.array.extend(a, 2);
+ * a; // [0, 1, 2]
+ *
+ * @param {Array<VALUE>} arr1  The array to modify.
+ * @param {...(Array<VALUE>|VALUE)} var_args The elements or arrays of elements
+ *     to add to arr1.
+ * @template VALUE
+ */
+goog.array.extend = function(arr1, var_args) {
+  for (var i = 1; i < arguments.length; i++) {
+    var arr2 = arguments[i];
+    if (goog.isArrayLike(arr2)) {
+      var len1 = arr1.length || 0;
+      var len2 = arr2.length || 0;
+      arr1.length = len1 + len2;
+      for (var j = 0; j < len2; j++) {
+        arr1[len1 + j] = arr2[j];
+      }
+    } else {
+      arr1.push(arr2);
+    }
+  }
+};
+
+
+/**
+ * Adds or removes elements from an array. This is a generic version of Array
+ * splice. This means that it might work on other objects similar to arrays,
+ * such as the arguments object.
+ *
+ * @param {IArrayLike<T>} arr The array to modify.
+ * @param {number|undefined} index The index at which to start changing the
+ *     array. If not defined, treated as 0.
+ * @param {number} howMany How many elements to remove (0 means no removal. A
+ *     value below 0 is treated as zero and so is any other non number. Numbers
+ *     are floored).
+ * @param {...T} var_args Optional, additional elements to insert into the
+ *     array.
+ * @return {!Array<T>} the removed elements.
+ * @template T
+ */
+goog.array.splice = function(arr, index, howMany, var_args) {
+  goog.asserts.assert(arr.length != null);
+
+  return Array.prototype.splice.apply(arr, goog.array.slice(arguments, 1));
+};
+
+
+/**
+ * Returns a new array from a segment of an array. This is a generic version of
+ * Array slice. This means that it might work on other objects similar to
+ * arrays, such as the arguments object.
+ *
+ * @param {IArrayLike<T>|string} arr The array from
+ * which to copy a segment.
+ * @param {number} start The index of the first element to copy.
+ * @param {number=} opt_end The index after the last element to copy.
+ * @return {!Array<T>} A new array containing the specified segment of the
+ *     original array.
+ * @template T
+ */
+goog.array.slice = function(arr, start, opt_end) {
+  goog.asserts.assert(arr.length != null);
+
+  // passing 1 arg to slice is not the same as passing 2 where the second is
+  // null or undefined (in that case the second argument is treated as 0).
+  // we could use slice on the arguments object and then use apply instead of
+  // testing the length
+  if (arguments.length <= 2) {
+    return Array.prototype.slice.call(arr, start);
+  } else {
+    return Array.prototype.slice.call(arr, start, opt_end);
+  }
+};
+
+
+/**
+ * Removes all duplicates from an array (retaining only the first
+ * occurrence of each array element).  This function modifies the
+ * array in place and doesn't change the order of the non-duplicate items.
+ *
+ * For objects, duplicates are identified as having the same unique ID as
+ * defined by {@link goog.getUid}.
+ *
+ * Alternatively you can specify a custom hash function that returns a unique
+ * value for each item in the array it should consider unique.
+ *
+ * Runtime: N,
+ * Worstcase space: 2N (no dupes)
+ *
+ * @param {IArrayLike<T>} arr The array from which to remove
+ *     duplicates.
+ * @param {Array=} opt_rv An optional array in which to return the results,
+ *     instead of performing the removal inplace.  If specified, the original
+ *     array will remain unchanged.
+ * @param {function(T):string=} opt_hashFn An optional function to use to
+ *     apply to every item in the array. This function should return a unique
+ *     value for each item in the array it should consider unique.
+ * @template T
+ */
+goog.array.removeDuplicates = function(arr, opt_rv, opt_hashFn) {
+  var returnArray = opt_rv || arr;
+  var defaultHashFn = function(item) {
+    // Prefix each type with a single character representing the type to
+    // prevent conflicting keys (e.g. true and 'true').
+    return goog.isObject(item) ? 'o' + goog.getUid(item) :
+                                 (typeof item).charAt(0) + item;
+  };
+  var hashFn = opt_hashFn || defaultHashFn;
+
+  var seen = {}, cursorInsert = 0, cursorRead = 0;
+  while (cursorRead < arr.length) {
+    var current = arr[cursorRead++];
+    var key = hashFn(current);
+    if (!Object.prototype.hasOwnProperty.call(seen, key)) {
+      seen[key] = true;
+      returnArray[cursorInsert++] = current;
+    }
+  }
+  returnArray.length = cursorInsert;
+};
+
+
+/**
+ * Searches the specified array for the specified target using the binary
+ * search algorithm.  If no opt_compareFn is specified, elements are compared
+ * using <code>goog.array.defaultCompare</code>, which compares the elements
+ * using the built in < and > operators.  This will produce the expected
+ * behavior for homogeneous arrays of String(s) and Number(s). The array
+ * specified <b>must</b> be sorted in ascending order (as defined by the
+ * comparison function).  If the array is not sorted, results are undefined.
+ * If the array contains multiple instances of the specified target value, any
+ * of these instances may be found.
+ *
+ * Runtime: O(log n)
+ *
+ * @param {IArrayLike<VALUE>} arr The array to be searched.
+ * @param {TARGET} target The sought value.
+ * @param {function(TARGET, VALUE): number=} opt_compareFn Optional comparison
+ *     function by which the array is ordered. Should take 2 arguments to
+ *     compare, and return a negative number, zero, or a positive number
+ *     depending on whether the first argument is less than, equal to, or
+ *     greater than the second.
+ * @return {number} Lowest index of the target value if found, otherwise
+ *     (-(insertion point) - 1). The insertion point is where the value should
+ *     be inserted into arr to preserve the sorted property.  Return value >= 0
+ *     iff target is found.
+ * @template TARGET, VALUE
+ */
+goog.array.binarySearch = function(arr, target, opt_compareFn) {
+  return goog.array.binarySearch_(
+      arr, opt_compareFn || goog.array.defaultCompare, false /* isEvaluator */,
+      target);
+};
+
+
+/**
+ * Selects an index in the specified array using the binary search algorithm.
+ * The evaluator receives an element and determines whether the desired index
+ * is before, at, or after it.  The evaluator must be consistent (formally,
+ * goog.array.map(goog.array.map(arr, evaluator, opt_obj), goog.math.sign)
+ * must be monotonically non-increasing).
+ *
+ * Runtime: O(log n)
+ *
+ * @param {IArrayLike<VALUE>} arr The array to be searched.
+ * @param {function(this:THIS, VALUE, number, ?): number} evaluator
+ *     Evaluator function that receives 3 arguments (the element, the index and
+ *     the array). Should return a negative number, zero, or a positive number
+ *     depending on whether the desired index is before, at, or after the
+ *     element passed to it.
+ * @param {THIS=} opt_obj The object to be used as the value of 'this'
+ *     within evaluator.
+ * @return {number} Index of the leftmost element matched by the evaluator, if
+ *     such exists; otherwise (-(insertion point) - 1). The insertion point is
+ *     the index of the first element for which the evaluator returns negative,
+ *     or arr.length if no such element exists. The return value is non-negative
+ *     iff a match is found.
+ * @template THIS, VALUE
+ */
+goog.array.binarySelect = function(arr, evaluator, opt_obj) {
+  return goog.array.binarySearch_(
+      arr, evaluator, true /* isEvaluator */, undefined /* opt_target */,
+      opt_obj);
+};
+
+
+/**
+ * Implementation of a binary search algorithm which knows how to use both
+ * comparison functions and evaluators. If an evaluator is provided, will call
+ * the evaluator with the given optional data object, conforming to the
+ * interface defined in binarySelect. Otherwise, if a comparison function is
+ * provided, will call the comparison function against the given data object.
+ *
+ * This implementation purposefully does not use goog.bind or goog.partial for
+ * performance reasons.
+ *
+ * Runtime: O(log n)
+ *
+ * @param {IArrayLike<?>} arr The array to be searched.
+ * @param {function(?, ?, ?): number | function(?, ?): number} compareFn
+ *     Either an evaluator or a comparison function, as defined by binarySearch
+ *     and binarySelect above.
+ * @param {boolean} isEvaluator Whether the function is an evaluator or a
+ *     comparison function.
+ * @param {?=} opt_target If the function is a comparison function, then
+ *     this is the target to binary search for.
+ * @param {Object=} opt_selfObj If the function is an evaluator, this is an
+ *     optional this object for the evaluator.
+ * @return {number} Lowest index of the target value if found, otherwise
+ *     (-(insertion point) - 1). The insertion point is where the value should
+ *     be inserted into arr to preserve the sorted property.  Return value >= 0
+ *     iff target is found.
+ * @private
+ */
+goog.array.binarySearch_ = function(
+    arr, compareFn, isEvaluator, opt_target, opt_selfObj) {
+  var left = 0;            // inclusive
+  var right = arr.length;  // exclusive
+  var found;
+  while (left < right) {
+    var middle = (left + right) >> 1;
+    var compareResult;
+    if (isEvaluator) {
+      compareResult = compareFn.call(opt_selfObj, arr[middle], middle, arr);
+    } else {
+      // NOTE(dimvar): To avoid this cast, we'd have to use function overloading
+      // for the type of binarySearch_, which the type system can't express yet.
+      compareResult = /** @type {function(?, ?): number} */ (compareFn)(
+          opt_target, arr[middle]);
+    }
+    if (compareResult > 0) {
+      left = middle + 1;
+    } else {
+      right = middle;
+      // We are looking for the lowest index so we can't return immediately.
+      found = !compareResult;
+    }
+  }
+  // left is the index if found, or the insertion point otherwise.
+  // ~left is a shorthand for -left - 1.
+  return found ? left : ~left;
+};
+
+
+/**
+ * Sorts the specified array into ascending order.  If no opt_compareFn is
+ * specified, elements are compared using
+ * <code>goog.array.defaultCompare</code>, which compares the elements using
+ * the built in < and > operators.  This will produce the expected behavior
+ * for homogeneous arrays of String(s) and Number(s), unlike the native sort,
+ * but will give unpredictable results for heterogeneous lists of strings and
+ * numbers with different numbers of digits.
+ *
+ * This sort is not guaranteed to be stable.
+ *
+ * Runtime: Same as <code>Array.prototype.sort</code>
+ *
+ * @param {Array<T>} arr The array to be sorted.
+ * @param {?function(T,T):number=} opt_compareFn Optional comparison
+ *     function by which the
+ *     array is to be ordered. Should take 2 arguments to compare, and return a
+ *     negative number, zero, or a positive number depending on whether the
+ *     first argument is less than, equal to, or greater than the second.
+ * @template T
+ */
+goog.array.sort = function(arr, opt_compareFn) {
+  // TODO(arv): Update type annotation since null is not accepted.
+  arr.sort(opt_compareFn || goog.array.defaultCompare);
+};
+
+
+/**
+ * Sorts the specified array into ascending order in a stable way.  If no
+ * opt_compareFn is specified, elements are compared using
+ * <code>goog.array.defaultCompare</code>, which compares the elements using
+ * the built in < and > operators.  This will produce the expected behavior
+ * for homogeneous arrays of String(s) and Number(s).
+ *
+ * Runtime: Same as <code>Array.prototype.sort</code>, plus an additional
+ * O(n) overhead of copying the array twice.
+ *
+ * @param {Array<T>} arr The array to be sorted.
+ * @param {?function(T, T): number=} opt_compareFn Optional comparison function
+ *     by which the array is to be ordered. Should take 2 arguments to compare,
+ *     and return a negative number, zero, or a positive number depending on
+ *     whether the first argument is less than, equal to, or greater than the
+ *     second.
+ * @template T
+ */
+goog.array.stableSort = function(arr, opt_compareFn) {
+  var compArr = new Array(arr.length);
+  for (var i = 0; i < arr.length; i++) {
+    compArr[i] = {index: i, value: arr[i]};
+  }
+  var valueCompareFn = opt_compareFn || goog.array.defaultCompare;
+  function stableCompareFn(obj1, obj2) {
+    return valueCompareFn(obj1.value, obj2.value) || obj1.index - obj2.index;
+  }
+  goog.array.sort(compArr, stableCompareFn);
+  for (var i = 0; i < arr.length; i++) {
+    arr[i] = compArr[i].value;
+  }
+};
+
+
+/**
+ * Sort the specified array into ascending order based on item keys
+ * returned by the specified key function.
+ * If no opt_compareFn is specified, the keys are compared in ascending order
+ * using <code>goog.array.defaultCompare</code>.
+ *
+ * Runtime: O(S(f(n)), where S is runtime of <code>goog.array.sort</code>
+ * and f(n) is runtime of the key function.
+ *
+ * @param {Array<T>} arr The array to be sorted.
+ * @param {function(T): K} keyFn Function taking array element and returning
+ *     a key used for sorting this element.
+ * @param {?function(K, K): number=} opt_compareFn Optional comparison function
+ *     by which the keys are to be ordered. Should take 2 arguments to compare,
+ *     and return a negative number, zero, or a positive number depending on
+ *     whether the first argument is less than, equal to, or greater than the
+ *     second.
+ * @template T,K
+ */
+goog.array.sortByKey = function(arr, keyFn, opt_compareFn) {
+  var keyCompareFn = opt_compareFn || goog.array.defaultCompare;
+  goog.array.sort(
+      arr, function(a, b) { return keyCompareFn(keyFn(a), keyFn(b)); });
+};
+
+
+/**
+ * Sorts an array of objects by the specified object key and compare
+ * function. If no compare function is provided, the key values are
+ * compared in ascending order using <code>goog.array.defaultCompare</code>.
+ * This won't work for keys that get renamed by the compiler. So use
+ * {'foo': 1, 'bar': 2} rather than {foo: 1, bar: 2}.
+ * @param {Array<Object>} arr An array of objects to sort.
+ * @param {string} key The object key to sort by.
+ * @param {Function=} opt_compareFn The function to use to compare key
+ *     values.
+ */
+goog.array.sortObjectsByKey = function(arr, key, opt_compareFn) {
+  goog.array.sortByKey(arr, function(obj) { return obj[key]; }, opt_compareFn);
+};
+
+
+/**
+ * Tells if the array is sorted.
+ * @param {!Array<T>} arr The array.
+ * @param {?function(T,T):number=} opt_compareFn Function to compare the
+ *     array elements.
+ *     Should take 2 arguments to compare, and return a negative number, zero,
+ *     or a positive number depending on whether the first argument is less
+ *     than, equal to, or greater than the second.
+ * @param {boolean=} opt_strict If true no equal elements are allowed.
+ * @return {boolean} Whether the array is sorted.
+ * @template T
+ */
+goog.array.isSorted = function(arr, opt_compareFn, opt_strict) {
+  var compare = opt_compareFn || goog.array.defaultCompare;
+  for (var i = 1; i < arr.length; i++) {
+    var compareResult = compare(arr[i - 1], arr[i]);
+    if (compareResult > 0 || compareResult == 0 && opt_strict) {
+      return false;
+    }
+  }
+  return true;
+};
+
+
+/**
+ * Compares two arrays for equality. Two arrays are considered equal if they
+ * have the same length and their corresponding elements are equal according to
+ * the comparison function.
+ *
+ * @param {IArrayLike<?>} arr1 The first array to compare.
+ * @param {IArrayLike<?>} arr2 The second array to compare.
+ * @param {Function=} opt_equalsFn Optional comparison function.
+ *     Should take 2 arguments to compare, and return true if the arguments
+ *     are equal. Defaults to {@link goog.array.defaultCompareEquality} which
+ *     compares the elements using the built-in '===' operator.
+ * @return {boolean} Whether the two arrays are equal.
+ */
+goog.array.equals = function(arr1, arr2, opt_equalsFn) {
+  if (!goog.isArrayLike(arr1) || !goog.isArrayLike(arr2) ||
+      arr1.length != arr2.length) {
+    return false;
+  }
+  var l = arr1.length;
+  var equalsFn = opt_equalsFn || goog.array.defaultCompareEquality;
+  for (var i = 0; i < l; i++) {
+    if (!equalsFn(arr1[i], arr2[i])) {
+      return false;
+    }
+  }
+  return true;
+};
+
+
+/**
+ * 3-way array compare function.
+ * @param {!IArrayLike<VALUE>} arr1 The first array to
+ *     compare.
+ * @param {!IArrayLike<VALUE>} arr2 The second array to
+ *     compare.
+ * @param {function(VALUE, VALUE): number=} opt_compareFn Optional comparison
+ *     function by which the array is to be ordered. Should take 2 arguments to
+ *     compare, and return a negative number, zero, or a positive number
+ *     depending on whether the first argument is less than, equal to, or
+ *     greater than the second.
+ * @return {number} Negative number, zero, or a positive number depending on
+ *     whether the first argument is less than, equal to, or greater than the
+ *     second.
+ * @template VALUE
+ */
+goog.array.compare3 = function(arr1, arr2, opt_compareFn) {
+  var compare = opt_compareFn || goog.array.defaultCompare;
+  var l = Math.min(arr1.length, arr2.length);
+  for (var i = 0; i < l; i++) {
+    var result = compare(arr1[i], arr2[i]);
+    if (result != 0) {
+      return result;
+    }
+  }
+  return goog.array.defaultCompare(arr1.length, arr2.length);
+};
+
+
+/**
+ * Compares its two arguments for order, using the built in < and >
+ * operators.
+ * @param {VALUE} a The first object to be compared.
+ * @param {VALUE} b The second object to be compared.
+ * @return {number} A negative number, zero, or a positive number as the first
+ *     argument is less than, equal to, or greater than the second,
+ *     respectively.
+ * @template VALUE
+ */
+goog.array.defaultCompare = function(a, b) {
+  return a > b ? 1 : a < b ? -1 : 0;
+};
+
+
+/**
+ * Compares its two arguments for inverse order, using the built in < and >
+ * operators.
+ * @param {VALUE} a The first object to be compared.
+ * @param {VALUE} b The second object to be compared.
+ * @return {number} A negative number, zero, or a positive number as the first
+ *     argument is greater than, equal to, or less than the second,
+ *     respectively.
+ * @template VALUE
+ */
+goog.array.inverseDefaultCompare = function(a, b) {
+  return -goog.array.defaultCompare(a, b);
+};
+
+
+/**
+ * Compares its two arguments for equality, using the built in === operator.
+ * @param {*} a The first object to compare.
+ * @param {*} b The second object to compare.
+ * @return {boolean} True if the two arguments are equal, false otherwise.
+ */
+goog.array.defaultCompareEquality = function(a, b) {
+  return a === b;
+};
+
+
+/**
+ * Inserts a value into a sorted array. The array is not modified if the
+ * value is already present.
+ * @param {IArrayLike<VALUE>} array The array to modify.
+ * @param {VALUE} value The object to insert.
+ * @param {function(VALUE, VALUE): number=} opt_compareFn Optional comparison
+ *     function by which the array is ordered. Should take 2 arguments to
+ *     compare, and return a negative number, zero, or a positive number
+ *     depending on whether the first argument is less than, equal to, or
+ *     greater than the second.
+ * @return {boolean} True if an element was inserted.
+ * @template VALUE
+ */
+goog.array.binaryInsert = function(array, value, opt_compareFn) {
+  var index = goog.array.binarySearch(array, value, opt_compareFn);
+  if (index < 0) {
+    goog.array.insertAt(array, value, -(index + 1));
+    return true;
+  }
+  return false;
+};
+
+
+/**
+ * Removes a value from a sorted array.
+ * @param {!IArrayLike<VALUE>} array The array to modify.
+ * @param {VALUE} value The object to remove.
+ * @param {function(VALUE, VALUE): number=} opt_compareFn Optional comparison
+ *     function by which the array is ordered. Should take 2 arguments to
+ *     compare, and return a negative number, zero, or a positive number
+ *     depending on whether the first argument is less than, equal to, or
+ *     greater than the second.
+ * @return {boolean} True if an element was removed.
+ * @template VALUE
+ */
+goog.array.binaryRemove = function(array, value, opt_compareFn) {
+  var index = goog.array.binarySearch(array, value, opt_compareFn);
+  return (index >= 0) ? goog.array.removeAt(array, index) : false;
+};
+
+
+/**
+ * Splits an array into disjoint buckets according to a splitting function.
+ * @param {Array<T>} array The array.
+ * @param {function(this:S, T,number,Array<T>):?} sorter Function to call for
+ *     every element.  This takes 3 arguments (the element, the index and the
+ *     array) and must return a valid object key (a string, number, etc), or
+ *     undefined, if that object should not be placed in a bucket.
+ * @param {S=} opt_obj The object to be used as the value of 'this' within
+ *     sorter.
+ * @return {!Object} An object, with keys being all of the unique return values
+ *     of sorter, and values being arrays containing the items for
+ *     which the splitter returned that key.
+ * @template T,S
+ */
+goog.array.bucket = function(array, sorter, opt_obj) {
+  var buckets = {};
+
+  for (var i = 0; i < array.length; i++) {
+    var value = array[i];
+    var key = sorter.call(/** @type {?} */ (opt_obj), value, i, array);
+    if (goog.isDef(key)) {
+      // Push the value to the right bucket, creating it if necessary.
+      var bucket = buckets[key] || (buckets[key] = []);
+      bucket.push(value);
+    }
+  }
+
+  return buckets;
+};
+
+
+/**
+ * Creates a new object built from the provided array and the key-generation
+ * function.
+ * @param {IArrayLike<T>} arr Array or array like object over
+ *     which to iterate whose elements will be the values in the new object.
+ * @param {?function(this:S, T, number, ?) : string} keyFunc The function to
+ *     call for every element. This function takes 3 arguments (the element, the
+ *     index and the array) and should return a string that will be used as the
+ *     key for the element in the new object. If the function returns the same
+ *     key for more than one element, the value for that key is
+ *     implementation-defined.
+ * @param {S=} opt_obj The object to be used as the value of 'this'
+ *     within keyFunc.
+ * @return {!Object<T>} The new object.
+ * @template T,S
+ */
+goog.array.toObject = function(arr, keyFunc, opt_obj) {
+  var ret = {};
+  goog.array.forEach(arr, function(element, index) {
+    ret[keyFunc.call(/** @type {?} */ (opt_obj), element, index, arr)] =
+        element;
+  });
+  return ret;
+};
+
+
+/**
+ * Creates a range of numbers in an arithmetic progression.
+ *
+ * Range takes 1, 2, or 3 arguments:
+ * <pre>
+ * range(5) is the same as range(0, 5, 1) and produces [0, 1, 2, 3, 4]
+ * range(2, 5) is the same as range(2, 5, 1) and produces [2, 3, 4]
+ * range(-2, -5, -1) produces [-2, -3, -4]
+ * range(-2, -5, 1) produces [], since stepping by 1 wouldn't ever reach -5.
+ * </pre>
+ *
+ * @param {number} startOrEnd The starting value of the range if an end argument
+ *     is provided. Otherwise, the start value is 0, and this is the end value.
+ * @param {number=} opt_end The optional end value of the range.
+ * @param {number=} opt_step The step size between range values. Defaults to 1
+ *     if opt_step is undefined or 0.
+ * @return {!Array<number>} An array of numbers for the requested range. May be
+ *     an empty array if adding the step would not converge toward the end
+ *     value.
+ */
+goog.array.range = function(startOrEnd, opt_end, opt_step) {
+  var array = [];
+  var start = 0;
+  var end = startOrEnd;
+  var step = opt_step || 1;
+  if (opt_end !== undefined) {
+    start = startOrEnd;
+    end = opt_end;
+  }
+
+  if (step * (end - start) < 0) {
+    // Sign mismatch: start + step will never reach the end value.
+    return [];
+  }
+
+  if (step > 0) {
+    for (var i = start; i < end; i += step) {
+      array.push(i);
+    }
+  } else {
+    for (var i = start; i > end; i += step) {
+      array.push(i);
+    }
+  }
+  return array;
+};
+
+
+/**
+ * Returns an array consisting of the given value repeated N times.
+ *
+ * @param {VALUE} value The value to repeat.
+ * @param {number} n The repeat count.
+ * @return {!Array<VALUE>} An array with the repeated value.
+ * @template VALUE
+ */
+goog.array.repeat = function(value, n) {
+  var array = [];
+  for (var i = 0; i < n; i++) {
+    array[i] = value;
+  }
+  return array;
+};
+
+
+/**
+ * Returns an array consisting of every argument with all arrays
+ * expanded in-place recursively.
+ *
+ * @param {...*} var_args The values to flatten.
+ * @return {!Array<?>} An array containing the flattened values.
+ */
+goog.array.flatten = function(var_args) {
+  var CHUNK_SIZE = 8192;
+
+  var result = [];
+  for (var i = 0; i < arguments.length; i++) {
+    var element = arguments[i];
+    if (goog.isArray(element)) {
+      for (var c = 0; c < element.length; c += CHUNK_SIZE) {
+        var chunk = goog.array.slice(element, c, c + CHUNK_SIZE);
+        var recurseResult = goog.array.flatten.apply(null, chunk);
+        for (var r = 0; r < recurseResult.length; r++) {
+          result.push(recurseResult[r]);
+        }
+      }
+    } else {
+      result.push(element);
+    }
+  }
+  return result;
+};
+
+
+/**
+ * Rotates an array in-place. After calling this method, the element at
+ * index i will be the element previously at index (i - n) %
+ * array.length, for all values of i between 0 and array.length - 1,
+ * inclusive.
+ *
+ * For example, suppose list comprises [t, a, n, k, s]. After invoking
+ * rotate(array, 1) (or rotate(array, -4)), array will comprise [s, t, a, n, k].
+ *
+ * @param {!Array<T>} array The array to rotate.
+ * @param {number} n The amount to rotate.
+ * @return {!Array<T>} The array.
+ * @template T
+ */
+goog.array.rotate = function(array, n) {
+  goog.asserts.assert(array.length != null);
+
+  if (array.length) {
+    n %= array.length;
+    if (n > 0) {
+      Array.prototype.unshift.apply(array, array.splice(-n, n));
+    } else if (n < 0) {
+      Array.prototype.push.apply(array, array.splice(0, -n));
+    }
+  }
+  return array;
+};
+
+
+/**
+ * Moves one item of an array to a new position keeping the order of the rest
+ * of the items. Example use case: keeping a list of JavaScript objects
+ * synchronized with the corresponding list of DOM elements after one of the
+ * elements has been dragged to a new position.
+ * @param {!IArrayLike<?>} arr The array to modify.
+ * @param {number} fromIndex Index of the item to move between 0 and
+ *     {@code arr.length - 1}.
+ * @param {number} toIndex Target index between 0 and {@code arr.length - 1}.
+ */
+goog.array.moveItem = function(arr, fromIndex, toIndex) {
+  goog.asserts.assert(fromIndex >= 0 && fromIndex < arr.length);
+  goog.asserts.assert(toIndex >= 0 && toIndex < arr.length);
+  // Remove 1 item at fromIndex.
+  var removedItems = Array.prototype.splice.call(arr, fromIndex, 1);
+  // Insert the removed item at toIndex.
+  Array.prototype.splice.call(arr, toIndex, 0, removedItems[0]);
+  // We don't use goog.array.insertAt and goog.array.removeAt, because they're
+  // significantly slower than splice.
+};
+
+
+/**
+ * Creates a new array for which the element at position i is an array of the
+ * ith element of the provided arrays.  The returned array will only be as long
+ * as the shortest array provided; additional values are ignored.  For example,
+ * the result of zipping [1, 2] and [3, 4, 5] is [[1,3], [2, 4]].
+ *
+ * This is similar to the zip() function in Python.  See {@link
+ * http://docs.python.org/library/functions.html#zip}
+ *
+ * @param {...!IArrayLike<?>} var_args Arrays to be combined.
+ * @return {!Array<!Array<?>>} A new array of arrays created from
+ *     provided arrays.
+ */
+goog.array.zip = function(var_args) {
+  if (!arguments.length) {
+    return [];
+  }
+  var result = [];
+  var minLen = arguments[0].length;
+  for (var i = 1; i < arguments.length; i++) {
+    if (arguments[i].length < minLen) {
+      minLen = arguments[i].length;
+    }
+  }
+  for (var i = 0; i < minLen; i++) {
+    var value = [];
+    for (var j = 0; j < arguments.length; j++) {
+      value.push(arguments[j][i]);
+    }
+    result.push(value);
+  }
+  return result;
+};
+
+
+/**
+ * Shuffles the values in the specified array using the Fisher-Yates in-place
+ * shuffle (also known as the Knuth Shuffle). By default, calls Math.random()
+ * and so resets the state of that random number generator. Similarly, may reset
+ * the state of the any other specified random number generator.
+ *
+ * Runtime: O(n)
+ *
+ * @param {!Array<?>} arr The array to be shuffled.
+ * @param {function():number=} opt_randFn Optional random function to use for
+ *     shuffling.
+ *     Takes no arguments, and returns a random number on the interval [0, 1).
+ *     Defaults to Math.random() using JavaScript's built-in Math library.
+ */
+goog.array.shuffle = function(arr, opt_randFn) {
+  var randFn = opt_randFn || Math.random;
+
+  for (var i = arr.length - 1; i > 0; i--) {
+    // Choose a random array index in [0, i] (inclusive with i).
+    var j = Math.floor(randFn() * (i + 1));
+
+    var tmp = arr[i];
+    arr[i] = arr[j];
+    arr[j] = tmp;
+  }
+};
+
+
+/**
+ * Returns a new array of elements from arr, based on the indexes of elements
+ * provided by index_arr. For example, the result of index copying
+ * ['a', 'b', 'c'] with index_arr [1,0,0,2] is ['b', 'a', 'a', 'c'].
+ *
+ * @param {!Array<T>} arr The array to get a indexed copy from.
+ * @param {!Array<number>} index_arr An array of indexes to get from arr.
+ * @return {!Array<T>} A new array of elements from arr in index_arr order.
+ * @template T
+ */
+goog.array.copyByIndex = function(arr, index_arr) {
+  var result = [];
+  goog.array.forEach(index_arr, function(index) { result.push(arr[index]); });
+  return result;
+};
+
+
+/**
+ * Maps each element of the input array into zero or more elements of the output
+ * array.
+ *
+ * @param {!IArrayLike<VALUE>|string} arr Array or array like object
+ *     over which to iterate.
+ * @param {function(this:THIS, VALUE, number, ?): !Array<RESULT>} f The function
+ *     to call for every element. This function takes 3 arguments (the element,
+ *     the index and the array) and should return an array. The result will be
+ *     used to extend a new array.
+ * @param {THIS=} opt_obj The object to be used as the value of 'this' within f.
+ * @return {!Array<RESULT>} a new array with the concatenation of all arrays
+ *     returned from f.
+ * @template THIS, VALUE, RESULT
+ */
+goog.array.concatMap = function(arr, f, opt_obj) {
+  return goog.array.concat.apply([], goog.array.map(arr, f, opt_obj));
+};
+
+// Copyright 2006 The Closure Library Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS-IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+/**
+ * @fileoverview Additional mathematical functions.
+ */
+
+goog.provide('goog.math');
+
+goog.require('goog.array');
+goog.require('goog.asserts');
+
+
+/**
+ * Returns a random integer greater than or equal to 0 and less than {@code a}.
+ * @param {number} a  The upper bound for the random integer (exclusive).
+ * @return {number} A random integer N such that 0 <= N < a.
+ */
+goog.math.randomInt = function(a) {
+  return Math.floor(Math.random() * a);
+};
+
+
+/**
+ * Returns a random number greater than or equal to {@code a} and less than
+ * {@code b}.
+ * @param {number} a  The lower bound for the random number (inclusive).
+ * @param {number} b  The upper bound for the random number (exclusive).
+ * @return {number} A random number N such that a <= N < b.
+ */
+goog.math.uniformRandom = function(a, b) {
+  return a + Math.random() * (b - a);
+};
+
+
+/**
+ * Takes a number and clamps it to within the provided bounds.
+ * @param {number} value The input number.
+ * @param {number} min The minimum value to return.
+ * @param {number} max The maximum value to return.
+ * @return {number} The input number if it is within bounds, or the nearest
+ *     number within the bounds.
+ */
+goog.math.clamp = function(value, min, max) {
+  return Math.min(Math.max(value, min), max);
+};
+
+
+/**
+ * The % operator in JavaScript returns the remainder of a / b, but differs from
+ * some other languages in that the result will have the same sign as the
+ * dividend. For example, -1 % 8 == -1, whereas in some other languages
+ * (such as Python) the result would be 7. This function emulates the more
+ * correct modulo behavior, which is useful for certain applications such as
+ * calculating an offset index in a circular list.
+ *
+ * @param {number} a The dividend.
+ * @param {number} b The divisor.
+ * @return {number} a % b where the result is between 0 and b (either 0 <= x < b
+ *     or b < x <= 0, depending on the sign of b).
+ */
+goog.math.modulo = function(a, b) {
+  var r = a % b;
+  // If r and b differ in sign, add b to wrap the result to the correct sign.
+  return (r * b < 0) ? r + b : r;
+};
+
+
+/**
+ * Performs linear interpolation between values a and b. Returns the value
+ * between a and b proportional to x (when x is between 0 and 1. When x is
+ * outside this range, the return value is a linear extrapolation).
+ * @param {number} a A number.
+ * @param {number} b A number.
+ * @param {number} x The proportion between a and b.
+ * @return {number} The interpolated value between a and b.
+ */
+goog.math.lerp = function(a, b, x) {
+  return a + x * (b - a);
+};
+
+
+/**
+ * Tests whether the two values are equal to each other, within a certain
+ * tolerance to adjust for floating point errors.
+ * @param {number} a A number.
+ * @param {number} b A number.
+ * @param {number=} opt_tolerance Optional tolerance range. Defaults
+ *     to 0.000001. If specified, should be greater than 0.
+ * @return {boolean} Whether {@code a} and {@code b} are nearly equal.
+ */
+goog.math.nearlyEquals = function(a, b, opt_tolerance) {
+  return Math.abs(a - b) <= (opt_tolerance || 0.000001);
+};
+
+
+// TODO(user): Rename to normalizeAngle, retaining old name as deprecated
+// alias.
+/**
+ * Normalizes an angle to be in range [0-360). Angles outside this range will
+ * be normalized to be the equivalent angle with that range.
+ * @param {number} angle Angle in degrees.
+ * @return {number} Standardized angle.
+ */
+goog.math.standardAngle = function(angle) {
+  return goog.math.modulo(angle, 360);
+};
+
+
+/**
+ * Normalizes an angle to be in range [0-2*PI). Angles outside this range will
+ * be normalized to be the equivalent angle with that range.
+ * @param {number} angle Angle in radians.
+ * @return {number} Standardized angle.
+ */
+goog.math.standardAngleInRadians = function(angle) {
+  return goog.math.modulo(angle, 2 * Math.PI);
+};
+
+
+/**
+ * Converts degrees to radians.
+ * @param {number} angleDegrees Angle in degrees.
+ * @return {number} Angle in radians.
+ */
+goog.math.toRadians = function(angleDegrees) {
+  return angleDegrees * Math.PI / 180;
+};
+
+
+/**
+ * Converts radians to degrees.
+ * @param {number} angleRadians Angle in radians.
+ * @return {number} Angle in degrees.
+ */
+goog.math.toDegrees = function(angleRadians) {
+  return angleRadians * 180 / Math.PI;
+};
+
+
+/**
+ * For a given angle and radius, finds the X portion of the offset.
+ * @param {number} degrees Angle in degrees (zero points in +X direction).
+ * @param {number} radius Radius.
+ * @return {number} The x-distance for the angle and radius.
+ */
+goog.math.angleDx = function(degrees, radius) {
+  return radius * Math.cos(goog.math.toRadians(degrees));
+};
+
+
+/**
+ * For a given angle and radius, finds the Y portion of the offset.
+ * @param {number} degrees Angle in degrees (zero points in +X direction).
+ * @param {number} radius Radius.
+ * @return {number} The y-distance for the angle and radius.
+ */
+goog.math.angleDy = function(degrees, radius) {
+  return radius * Math.sin(goog.math.toRadians(degrees));
+};
+
+
+/**
+ * Computes the angle between two points (x1,y1) and (x2,y2).
+ * Angle zero points in the +X direction, 90 degrees points in the +Y
+ * direction (down) and from there we grow clockwise towards 360 degrees.
+ * @param {number} x1 x of first point.
+ * @param {number} y1 y of first point.
+ * @param {number} x2 x of second point.
+ * @param {number} y2 y of second point.
+ * @return {number} Standardized angle in degrees of the vector from
+ *     x1,y1 to x2,y2.
+ */
+goog.math.angle = function(x1, y1, x2, y2) {
+  return goog.math.standardAngle(
+      goog.math.toDegrees(Math.atan2(y2 - y1, x2 - x1)));
+};
+
+
+/**
+ * Computes the difference between startAngle and endAngle (angles in degrees).
+ * @param {number} startAngle  Start angle in degrees.
+ * @param {number} endAngle  End angle in degrees.
+ * @return {number} The number of degrees that when added to
+ *     startAngle will result in endAngle. Positive numbers mean that the
+ *     direction is clockwise. Negative numbers indicate a counter-clockwise
+ *     direction.
+ *     The shortest route (clockwise vs counter-clockwise) between the angles
+ *     is used.
+ *     When the difference is 180 degrees, the function returns 180 (not -180)
+ *     angleDifference(30, 40) is 10, and angleDifference(40, 30) is -10.
+ *     angleDifference(350, 10) is 20, and angleDifference(10, 350) is -20.
+ */
+goog.math.angleDifference = function(startAngle, endAngle) {
+  var d =
+      goog.math.standardAngle(endAngle) - goog.math.standardAngle(startAngle);
+  if (d > 180) {
+    d = d - 360;
+  } else if (d <= -180) {
+    d = 360 + d;
+  }
+  return d;
+};
+
+
+/**
+ * Returns the sign of a number as per the "sign" or "signum" function.
+ * @param {number} x The number to take the sign of.
+ * @return {number} -1 when negative, 1 when positive, 0 when 0. Preserves
+ *     signed zeros and NaN.
+ */
+goog.math.sign = Math.sign || function(x) {
+  if (x > 0) {
+    return 1;
+  }
+  if (x < 0) {
+    return -1;
+  }
+  return x;  // Preserves signed zeros and NaN.
+};
+
+
+/**
+ * JavaScript implementation of Longest Common Subsequence problem.
+ * http://en.wikipedia.org/wiki/Longest_common_subsequence
+ *
+ * Returns the longest possible array that is subarray of both of given arrays.
+ *
+ * @param {IArrayLike<S>} array1 First array of objects.
+ * @param {IArrayLike<T>} array2 Second array of objects.
+ * @param {Function=} opt_compareFn Function that acts as a custom comparator
+ *     for the array ojects. Function should return true if objects are equal,
+ *     otherwise false.
+ * @param {Function=} opt_collectorFn Function used to decide what to return
+ *     as a result subsequence. It accepts 2 arguments: index of common element
+ *     in the first array and index in the second. The default function returns
+ *     element from the first array.
+ * @return {!Array<S|T>} A list of objects that are common to both arrays
+ *     such that there is no common subsequence with size greater than the
+ *     length of the list.
+ * @template S,T
+ */
+goog.math.longestCommonSubsequence = function(
+    array1, array2, opt_compareFn, opt_collectorFn) {
+
+  var compare = opt_compareFn || function(a, b) { return a == b; };
+
+  var collect = opt_collectorFn || function(i1, i2) { return array1[i1]; };
+
+  var length1 = array1.length;
+  var length2 = array2.length;
+
+  var arr = [];
+  for (var i = 0; i < length1 + 1; i++) {
+    arr[i] = [];
+    arr[i][0] = 0;
+  }
+
+  for (var j = 0; j < length2 + 1; j++) {
+    arr[0][j] = 0;
+  }
+
+  for (i = 1; i <= length1; i++) {
+    for (j = 1; j <= length2; j++) {
+      if (compare(array1[i - 1], array2[j - 1])) {
+        arr[i][j] = arr[i - 1][j - 1] + 1;
+      } else {
+        arr[i][j] = Math.max(arr[i - 1][j], arr[i][j - 1]);
+      }
+    }
+  }
+
+  // Backtracking
+  var result = [];
+  var i = length1, j = length2;
+  while (i > 0 && j > 0) {
+    if (compare(array1[i - 1], array2[j - 1])) {
+      result.unshift(collect(i - 1, j - 1));
+      i--;
+      j--;
+    } else {
+      if (arr[i - 1][j] > arr[i][j - 1]) {
+        i--;
+      } else {
+        j--;
+      }
+    }
+  }
+
+  return result;
+};
+
+
+/**
+ * Returns the sum of the arguments.
+ * @param {...number} var_args Numbers to add.
+ * @return {number} The sum of the arguments (0 if no arguments were provided,
+ *     {@code NaN} if any of the arguments is not a valid number).
+ */
+goog.math.sum = function(var_args) {
+  return /** @type {number} */ (
+      goog.array.reduce(
+          arguments, function(sum, value) { return sum + value; }, 0));
+};
+
+
+/**
+ * Returns the arithmetic mean of the arguments.
+ * @param {...number} var_args Numbers to average.
+ * @return {number} The average of the arguments ({@code NaN} if no arguments
+ *     were provided or any of the arguments is not a valid number).
+ */
+goog.math.average = function(var_args) {
+  return goog.math.sum.apply(null, arguments) / arguments.length;
+};
+
+
+/**
+ * Returns the unbiased sample variance of the arguments. For a definition,
+ * see e.g. http://en.wikipedia.org/wiki/Variance
+ * @param {...number} var_args Number samples to analyze.
+ * @return {number} The unbiased sample variance of the arguments (0 if fewer
+ *     than two samples were provided, or {@code NaN} if any of the samples is
+ *     not a valid number).
+ */
+goog.math.sampleVariance = function(var_args) {
+  var sampleSize = arguments.length;
+  if (sampleSize < 2) {
+    return 0;
+  }
+
+  var mean = goog.math.average.apply(null, arguments);
+  var variance =
+      goog.math.sum.apply(null, goog.array.map(arguments, function(val) {
+        return Math.pow(val - mean, 2);
+      })) / (sampleSize - 1);
+
+  return variance;
+};
+
+
+/**
+ * Returns the sample standard deviation of the arguments.  For a definition of
+ * sample standard deviation, see e.g.
+ * http://en.wikipedia.org/wiki/Standard_deviation
+ * @param {...number} var_args Number samples to analyze.
+ * @return {number} The sample standard deviation of the arguments (0 if fewer
+ *     than two samples were provided, or {@code NaN} if any of the samples is
+ *     not a valid number).
+ */
+goog.math.standardDeviation = function(var_args) {
+  return Math.sqrt(goog.math.sampleVariance.apply(null, arguments));
+};
+
+
+/**
+ * Returns whether the supplied number represents an integer, i.e. that is has
+ * no fractional component.  No range-checking is performed on the number.
+ * @param {number} num The number to test.
+ * @return {boolean} Whether {@code num} is an integer.
+ */
+goog.math.isInt = function(num) {
+  return isFinite(num) && num % 1 == 0;
+};
+
+
+/**
+ * Returns whether the supplied number is finite and not NaN.
+ * @param {number} num The number to test.
+ * @return {boolean} Whether {@code num} is a finite number.
+ */
+goog.math.isFiniteNumber = function(num) {
+  return isFinite(num) && !isNaN(num);
+};
+
+
+/**
+ * @param {number} num The number to test.
+ * @return {boolean} Whether it is negative zero.
+ */
+goog.math.isNegativeZero = function(num) {
+  return num == 0 && 1 / num < 0;
+};
+
+
+/**
+ * Returns the precise value of floor(log10(num)).
+ * Simpler implementations didn't work because of floating point rounding
+ * errors. For example
+ * <ul>
+ * <li>Math.floor(Math.log(num) / Math.LN10) is off by one for num == 1e+3.
+ * <li>Math.floor(Math.log(num) * Math.LOG10E) is off by one for num == 1e+15.
+ * <li>Math.floor(Math.log10(num)) is off by one for num == 1e+15 - 1.
+ * </ul>
+ * @param {number} num A floating point number.
+ * @return {number} Its logarithm to base 10 rounded down to the nearest
+ *     integer if num > 0. -Infinity if num == 0. NaN if num < 0.
+ */
+goog.math.log10Floor = function(num) {
+  if (num > 0) {
+    var x = Math.round(Math.log(num) * Math.LOG10E);
+    return x - (parseFloat('1e' + x) > num ? 1 : 0);
+  }
+  return num == 0 ? -Infinity : NaN;
+};
+
+
+/**
+ * A tweaked variant of {@code Math.floor} which tolerates if the passed number
+ * is infinitesimally smaller than the closest integer. It often happens with
+ * the results of floating point calculations because of the finite precision
+ * of the intermediate results. For example {@code Math.floor(Math.log(1000) /
+ * Math.LN10) == 2}, not 3 as one would expect.
+ * @param {number} num A number.
+ * @param {number=} opt_epsilon An infinitesimally small positive number, the
+ *     rounding error to tolerate.
+ * @return {number} The largest integer less than or equal to {@code num}.
+ */
+goog.math.safeFloor = function(num, opt_epsilon) {
+  goog.asserts.assert(!goog.isDef(opt_epsilon) || opt_epsilon > 0);
+  return Math.floor(num + (opt_epsilon || 2e-15));
+};
+
+
+/**
+ * A tweaked variant of {@code Math.ceil}. See {@code goog.math.safeFloor} for
+ * details.
+ * @param {number} num A number.
+ * @param {number=} opt_epsilon An infinitesimally small positive number, the
+ *     rounding error to tolerate.
+ * @return {number} The smallest integer greater than or equal to {@code num}.
+ */
+goog.math.safeCeil = function(num, opt_epsilon) {
+  goog.asserts.assert(!goog.isDef(opt_epsilon) || opt_epsilon > 0);
+  return Math.ceil(num - (opt_epsilon || 2e-15));
+};
+
+// Copyright 2006 The Closure Library Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS-IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+/**
+ * @fileoverview Utilities related to color and color conversion.
+ */
+
+goog.provide('goog.color');
+goog.provide('goog.color.Hsl');
+goog.provide('goog.color.Hsv');
+goog.provide('goog.color.Rgb');
+
+goog.require('goog.color.names');
+goog.require('goog.math');
+
+
+/**
+ * RGB color representation. An array containing three elements [r, g, b],
+ * each an integer in [0, 255], representing the red, green, and blue components
+ * of the color respectively.
+ * @typedef {Array<number>}
+ */
+goog.color.Rgb;
+
+
+/**
+ * HSV color representation. An array containing three elements [h, s, v]:
+ * h (hue) must be an integer in [0, 360], cyclic.
+ * s (saturation) must be a number in [0, 1].
+ * v (value/brightness) must be an integer in [0, 255].
+ * @typedef {Array<number>}
+ */
+goog.color.Hsv;
+
+
+/**
+ * HSL color representation. An array containing three elements [h, s, l]:
+ * h (hue) must be an integer in [0, 360], cyclic.
+ * s (saturation) must be a number in [0, 1].
+ * l (lightness) must be a number in [0, 1].
+ * @typedef {Array<number>}
+ */
+goog.color.Hsl;
+
+
+/**
+ * Parses a color out of a string.
+ * @param {string} str Color in some format.
+ * @return {{hex: string, type: string}} 'hex' is a string containing a hex
+ *     representation of the color, 'type' is a string containing the type
+ *     of color format passed in ('hex', 'rgb', 'named').
+ */
+goog.color.parse = function(str) {
+  var result = {};
+  str = String(str);
+
+  var maybeHex = goog.color.prependHashIfNecessaryHelper(str);
+  if (goog.color.isValidHexColor_(maybeHex)) {
+    result.hex = goog.color.normalizeHex(maybeHex);
+    result.type = 'hex';
+    return result;
+  } else {
+    var rgb = goog.color.isValidRgbColor_(str);
+    if (rgb.length) {
+      result.hex = goog.color.rgbArrayToHex(rgb);
+      result.type = 'rgb';
+      return result;
+    } else if (goog.color.names) {
+      var hex = goog.color.names[str.toLowerCase()];
+      if (hex) {
+        result.hex = hex;
+        result.type = 'named';
+        return result;
+      }
+    }
+  }
+  throw Error(str + ' is not a valid color string');
+};
+
+
+/**
+ * Determines if the given string can be parsed as a color.
+ *     {@see goog.color.parse}.
+ * @param {string} str Potential color string.
+ * @return {boolean} True if str is in a format that can be parsed to a color.
+ */
+goog.color.isValidColor = function(str) {
+  var maybeHex = goog.color.prependHashIfNecessaryHelper(str);
+  return !!(
+      goog.color.isValidHexColor_(maybeHex) ||
+      goog.color.isValidRgbColor_(str).length ||
+      goog.color.names && goog.color.names[str.toLowerCase()]);
+};
+
+
+/**
+ * Parses red, green, blue components out of a valid rgb color string.
+ * Throws Error if the color string is invalid.
+ * @param {string} str RGB representation of a color.
+ *    {@see goog.color.isValidRgbColor_}.
+ * @return {!goog.color.Rgb} rgb representation of the color.
+ */
+goog.color.parseRgb = function(str) {
+  var rgb = goog.color.isValidRgbColor_(str);
+  if (!rgb.length) {
+    throw Error(str + ' is not a valid RGB color');
+  }
+  return rgb;
+};
+
+
+/**
+ * Converts a hex representation of a color to RGB.
+ * @param {string} hexColor Color to convert.
+ * @return {string} string of the form 'rgb(R,G,B)' which can be used in
+ *    styles.
+ */
+goog.color.hexToRgbStyle = function(hexColor) {
+  return goog.color.rgbStyle_(goog.color.hexToRgb(hexColor));
+};
+
+
+/**
+ * Regular expression for extracting the digits in a hex color triplet.
+ * @type {RegExp}
+ * @private
+ */
+goog.color.hexTripletRe_ = /#(.)(.)(.)/;
+
+
+/**
+ * Normalize an hex representation of a color
+ * @param {string} hexColor an hex color string.
+ * @return {string} hex color in the format '#rrggbb' with all lowercase
+ *     literals.
+ */
+goog.color.normalizeHex = function(hexColor) {
+  if (!goog.color.isValidHexColor_(hexColor)) {
+    throw Error("'" + hexColor + "' is not a valid hex color");
+  }
+  if (hexColor.length == 4) {  // of the form #RGB
+    hexColor = hexColor.replace(goog.color.hexTripletRe_, '#$1$1$2$2$3$3');
+  }
+  return hexColor.toLowerCase();
+};
+
+
+/**
+ * Converts a hex representation of a color to RGB.
+ * @param {string} hexColor Color to convert.
+ * @return {!goog.color.Rgb} rgb representation of the color.
+ */
+goog.color.hexToRgb = function(hexColor) {
+  hexColor = goog.color.normalizeHex(hexColor);
+  var r = parseInt(hexColor.substr(1, 2), 16);
+  var g = parseInt(hexColor.substr(3, 2), 16);
+  var b = parseInt(hexColor.substr(5, 2), 16);
+
+  return [r, g, b];
+};
+
+
+/**
+ * Converts a color from RGB to hex representation.
+ * @param {number} r Amount of red, int between 0 and 255.
+ * @param {number} g Amount of green, int between 0 and 255.
+ * @param {number} b Amount of blue, int between 0 and 255.
+ * @return {string} hex representation of the color.
+ */
+goog.color.rgbToHex = function(r, g, b) {
+  r = Number(r);
+  g = Number(g);
+  b = Number(b);
+  if (r != (r & 255) || g != (g & 255) || b != (b & 255)) {
+    throw Error('"(' + r + ',' + g + ',' + b + '") is not a valid RGB color');
+  }
+  var hexR = goog.color.prependZeroIfNecessaryHelper(r.toString(16));
+  var hexG = goog.color.prependZeroIfNecessaryHelper(g.toString(16));
+  var hexB = goog.color.prependZeroIfNecessaryHelper(b.toString(16));
+  return '#' + hexR + hexG + hexB;
+};
+
+
+/**
+ * Converts a color from RGB to hex representation.
+ * @param {goog.color.Rgb} rgb rgb representation of the color.
+ * @return {string} hex representation of the color.
+ */
+goog.color.rgbArrayToHex = function(rgb) {
+  return goog.color.rgbToHex(rgb[0], rgb[1], rgb[2]);
+};
+
+
+/**
+ * Converts a color from RGB color space to HSL color space.
+ * Modified from {@link http://en.wikipedia.org/wiki/HLS_color_space}.
+ * @param {number} r Value of red, in [0, 255].
+ * @param {number} g Value of green, in [0, 255].
+ * @param {number} b Value of blue, in [0, 255].
+ * @return {!goog.color.Hsl} hsl representation of the color.
+ */
+goog.color.rgbToHsl = function(r, g, b) {
+  // First must normalize r, g, b to be between 0 and 1.
+  var normR = r / 255;
+  var normG = g / 255;
+  var normB = b / 255;
+  var max = Math.max(normR, normG, normB);
+  var min = Math.min(normR, normG, normB);
+  var h = 0;
+  var s = 0;
+
+  // Luminosity is the average of the max and min rgb color intensities.
+  var l = 0.5 * (max + min);
+
+  // The hue and saturation are dependent on which color intensity is the max.
+  // If max and min are equal, the color is gray and h and s should be 0.
+  if (max != min) {
+    if (max == normR) {
+      h = 60 * (normG - normB) / (max - min);
+    } else if (max == normG) {
+      h = 60 * (normB - normR) / (max - min) + 120;
+    } else if (max == normB) {
+      h = 60 * (normR - normG) / (max - min) + 240;
+    }
+
+    if (0 < l && l <= 0.5) {
+      s = (max - min) / (2 * l);
+    } else {
+      s = (max - min) / (2 - 2 * l);
+    }
+  }
+
+  // Make sure the hue falls between 0 and 360.
+  return [Math.round(h + 360) % 360, s, l];
+};
+
+
+/**
+ * Converts a color from RGB color space to HSL color space.
+ * @param {goog.color.Rgb} rgb rgb representation of the color.
+ * @return {!goog.color.Hsl} hsl representation of the color.
+ */
+goog.color.rgbArrayToHsl = function(rgb) {
+  return goog.color.rgbToHsl(rgb[0], rgb[1], rgb[2]);
+};
+
+
+/**
+ * Helper for hslToRgb.
+ * @param {number} v1 Helper variable 1.
+ * @param {number} v2 Helper variable 2.
+ * @param {number} vH Helper variable 3.
+ * @return {number} Appropriate RGB value, given the above.
+ * @private
+ */
+goog.color.hueToRgb_ = function(v1, v2, vH) {
+  if (vH < 0) {
+    vH += 1;
+  } else if (vH > 1) {
+    vH -= 1;
+  }
+  if ((6 * vH) < 1) {
+    return (v1 + (v2 - v1) * 6 * vH);
+  } else if (2 * vH < 1) {
+    return v2;
+  } else if (3 * vH < 2) {
+    return (v1 + (v2 - v1) * ((2 / 3) - vH) * 6);
+  }
+  return v1;
+};
+
+
+/**
+ * Converts a color from HSL color space to RGB color space.
+ * Modified from {@link http://www.easyrgb.com/math.html}
+ * @param {number} h Hue, in [0, 360].
+ * @param {number} s Saturation, in [0, 1].
+ * @param {number} l Luminosity, in [0, 1].
+ * @return {!goog.color.Rgb} rgb representation of the color.
+ */
+goog.color.hslToRgb = function(h, s, l) {
+  var r = 0;
+  var g = 0;
+  var b = 0;
+  var normH = h / 360;  // normalize h to fall in [0, 1]
+
+  if (s == 0) {
+    r = g = b = l * 255;
+  } else {
+    var temp1 = 0;
+    var temp2 = 0;
+    if (l < 0.5) {
+      temp2 = l * (1 + s);
+    } else {
+      temp2 = l + s - (s * l);
+    }
+    temp1 = 2 * l - temp2;
+    r = 255 * goog.color.hueToRgb_(temp1, temp2, normH + (1 / 3));
+    g = 255 * goog.color.hueToRgb_(temp1, temp2, normH);
+    b = 255 * goog.color.hueToRgb_(temp1, temp2, normH - (1 / 3));
+  }
+
+  return [Math.round(r), Math.round(g), Math.round(b)];
+};
+
+
+/**
+ * Converts a color from HSL color space to RGB color space.
+ * @param {goog.color.Hsl} hsl hsl representation of the color.
+ * @return {!goog.color.Rgb} rgb representation of the color.
+ */
+goog.color.hslArrayToRgb = function(hsl) {
+  return goog.color.hslToRgb(hsl[0], hsl[1], hsl[2]);
+};
+
+
+/**
+ * Helper for isValidHexColor_.
+ * @type {RegExp}
+ * @private
+ */
+goog.color.validHexColorRe_ = /^#(?:[0-9a-f]{3}){1,2}$/i;
+
+
+/**
+ * Checks if a string is a valid hex color.  We expect strings of the format
+ * #RRGGBB (ex: #1b3d5f) or #RGB (ex: #3CA == #33CCAA).
+ * @param {string} str String to check.
+ * @return {boolean} Whether the string is a valid hex color.
+ * @private
+ */
+goog.color.isValidHexColor_ = function(str) {
+  return goog.color.validHexColorRe_.test(str);
+};
+
+
+/**
+ * Helper for isNormalizedHexColor_.
+ * @type {RegExp}
+ * @private
+ */
+goog.color.normalizedHexColorRe_ = /^#[0-9a-f]{6}$/;
+
+
+/**
+ * Checks if a string is a normalized hex color.
+ * We expect strings of the format #RRGGBB (ex: #1b3d5f)
+ * using only lowercase letters.
+ * @param {string} str String to check.
+ * @return {boolean} Whether the string is a normalized hex color.
+ * @private
+ */
+goog.color.isNormalizedHexColor_ = function(str) {
+  return goog.color.normalizedHexColorRe_.test(str);
+};
+
+
+/**
+ * Regular expression for matching and capturing RGB style strings. Helper for
+ * isValidRgbColor_.
+ * @type {RegExp}
+ * @private
+ */
+goog.color.rgbColorRe_ =
+    /^(?:rgb)?\((0|[1-9]\d{0,2}),\s?(0|[1-9]\d{0,2}),\s?(0|[1-9]\d{0,2})\)$/i;
+
+
+/**
+ * Checks if a string is a valid rgb color.  We expect strings of the format
+ * '(r, g, b)', or 'rgb(r, g, b)', where each color component is an int in
+ * [0, 255].
+ * @param {string} str String to check.
+ * @return {!goog.color.Rgb} the rgb representation of the color if it is
+ *     a valid color, or the empty array otherwise.
+ * @private
+ */
+goog.color.isValidRgbColor_ = function(str) {
+  // Each component is separate (rather than using a repeater) so we can
+  // capture the match. Also, we explicitly set each component to be either 0,
+  // or start with a non-zero, to prevent octal numbers from slipping through.
+  var regExpResultArray = str.match(goog.color.rgbColorRe_);
+  if (regExpResultArray) {
+    var r = Number(regExpResultArray[1]);
+    var g = Number(regExpResultArray[2]);
+    var b = Number(regExpResultArray[3]);
+    if (r >= 0 && r <= 255 && g >= 0 && g <= 255 && b >= 0 && b <= 255) {
+      return [r, g, b];
+    }
+  }
+  return [];
+};
+
+
+/**
+ * Takes a hex value and prepends a zero if it's a single digit.
+ * Small helper method for use by goog.color and friends.
+ * @param {string} hex Hex value to prepend if single digit.
+ * @return {string} hex value prepended with zero if it was single digit,
+ *     otherwise the same value that was passed in.
+ */
+goog.color.prependZeroIfNecessaryHelper = function(hex) {
+  return hex.length == 1 ? '0' + hex : hex;
+};
+
+
+/**
+ * Takes a string a prepends a '#' sign if one doesn't exist.
+ * Small helper method for use by goog.color and friends.
+ * @param {string} str String to check.
+ * @return {string} The value passed in, prepended with a '#' if it didn't
+ *     already have one.
+ */
+goog.color.prependHashIfNecessaryHelper = function(str) {
+  return str.charAt(0) == '#' ? str : '#' + str;
+};
+
+
+/**
+ * Takes an array of [r, g, b] and converts it into a string appropriate for
+ * CSS styles.
+ * @param {goog.color.Rgb} rgb rgb representation of the color.
+ * @return {string} string of the form 'rgb(r,g,b)'.
+ * @private
+ */
+goog.color.rgbStyle_ = function(rgb) {
+  return 'rgb(' + rgb.join(',') + ')';
+};
+
+
+/**
+ * Converts an HSV triplet to an RGB array.  V is brightness because b is
+ *   reserved for blue in RGB.
+ * @param {number} h Hue value in [0, 360].
+ * @param {number} s Saturation value in [0, 1].
+ * @param {number} brightness brightness in [0, 255].
+ * @return {!goog.color.Rgb} rgb representation of the color.
+ */
+goog.color.hsvToRgb = function(h, s, brightness) {
+  var red = 0;
+  var green = 0;
+  var blue = 0;
+  if (s == 0) {
+    red = brightness;
+    green = brightness;
+    blue = brightness;
+  } else {
+    var sextant = Math.floor(h / 60);
+    var remainder = (h / 60) - sextant;
+    var val1 = brightness * (1 - s);
+    var val2 = brightness * (1 - (s * remainder));
+    var val3 = brightness * (1 - (s * (1 - remainder)));
+    switch (sextant) {
+      case 1:
+        red = val2;
+        green = brightness;
+        blue = val1;
+        break;
+      case 2:
+        red = val1;
+        green = brightness;
+        blue = val3;
+        break;
+      case 3:
+        red = val1;
+        green = val2;
+        blue = brightness;
+        break;
+      case 4:
+        red = val3;
+        green = val1;
+        blue = brightness;
+        break;
+      case 5:
+        red = brightness;
+        green = val1;
+        blue = val2;
+        break;
+      case 6:
+      case 0:
+        red = brightness;
+        green = val3;
+        blue = val1;
+        break;
+    }
+  }
+
+  return [Math.floor(red), Math.floor(green), Math.floor(blue)];
+};
+
+
+/**
+ * Converts from RGB values to an array of HSV values.
+ * @param {number} red Red value in [0, 255].
+ * @param {number} green Green value in [0, 255].
+ * @param {number} blue Blue value in [0, 255].
+ * @return {!goog.color.Hsv} hsv representation of the color.
+ */
+goog.color.rgbToHsv = function(red, green, blue) {
+
+  var max = Math.max(Math.max(red, green), blue);
+  var min = Math.min(Math.min(red, green), blue);
+  var hue;
+  var saturation;
+  var value = max;
+  if (min == max) {
+    hue = 0;
+    saturation = 0;
+  } else {
+    var delta = (max - min);
+    saturation = delta / max;
+
+    if (red == max) {
+      hue = (green - blue) / delta;
+    } else if (green == max) {
+      hue = 2 + ((blue - red) / delta);
+    } else {
+      hue = 4 + ((red - green) / delta);
+    }
+    hue *= 60;
+    if (hue < 0) {
+      hue += 360;
+    }
+    if (hue > 360) {
+      hue -= 360;
+    }
+  }
+
+  return [hue, saturation, value];
+};
+
+
+/**
+ * Converts from an array of RGB values to an array of HSV values.
+ * @param {goog.color.Rgb} rgb rgb representation of the color.
+ * @return {!goog.color.Hsv} hsv representation of the color.
+ */
+goog.color.rgbArrayToHsv = function(rgb) {
+  return goog.color.rgbToHsv(rgb[0], rgb[1], rgb[2]);
+};
+
+
+/**
+ * Converts an HSV triplet to an RGB array.
+ * @param {goog.color.Hsv} hsv hsv representation of the color.
+ * @return {!goog.color.Rgb} rgb representation of the color.
+ */
+goog.color.hsvArrayToRgb = function(hsv) {
+  return goog.color.hsvToRgb(hsv[0], hsv[1], hsv[2]);
+};
+
+
+/**
+ * Converts a hex representation of a color to HSL.
+ * @param {string} hex Color to convert.
+ * @return {!goog.color.Hsv} hsv representation of the color.
+ */
+goog.color.hexToHsl = function(hex) {
+  var rgb = goog.color.hexToRgb(hex);
+  return goog.color.rgbToHsl(rgb[0], rgb[1], rgb[2]);
+};
+
+
+/**
+ * Converts from h,s,l values to a hex string
+ * @param {number} h Hue, in [0, 360].
+ * @param {number} s Saturation, in [0, 1].
+ * @param {number} l Luminosity, in [0, 1].
+ * @return {string} hex representation of the color.
+ */
+goog.color.hslToHex = function(h, s, l) {
+  return goog.color.rgbArrayToHex(goog.color.hslToRgb(h, s, l));
+};
+
+
+/**
+ * Converts from an hsl array to a hex string
+ * @param {goog.color.Hsl} hsl hsl representation of the color.
+ * @return {string} hex representation of the color.
+ */
+goog.color.hslArrayToHex = function(hsl) {
+  return goog.color.rgbArrayToHex(goog.color.hslToRgb(hsl[0], hsl[1], hsl[2]));
+};
+
+
+/**
+ * Converts a hex representation of a color to HSV
+ * @param {string} hex Color to convert.
+ * @return {!goog.color.Hsv} hsv representation of the color.
+ */
+goog.color.hexToHsv = function(hex) {
+  return goog.color.rgbArrayToHsv(goog.color.hexToRgb(hex));
+};
+
+
+/**
+ * Converts from h,s,v values to a hex string
+ * @param {number} h Hue, in [0, 360].
+ * @param {number} s Saturation, in [0, 1].
+ * @param {number} v Value, in [0, 255].
+ * @return {string} hex representation of the color.
+ */
+goog.color.hsvToHex = function(h, s, v) {
+  return goog.color.rgbArrayToHex(goog.color.hsvToRgb(h, s, v));
+};
+
+
+/**
+ * Converts from an HSV array to a hex string
+ * @param {goog.color.Hsv} hsv hsv representation of the color.
+ * @return {string} hex representation of the color.
+ */
+goog.color.hsvArrayToHex = function(hsv) {
+  return goog.color.hsvToHex(hsv[0], hsv[1], hsv[2]);
+};
+
+
+/**
+ * Calculates the Euclidean distance between two color vectors on an HSL sphere.
+ * A demo of the sphere can be found at:
+ * http://en.wikipedia.org/wiki/HSL_color_space
+ * In short, a vector for color (H, S, L) in this system can be expressed as
+ * (S*L'*cos(2*PI*H), S*L'*sin(2*PI*H), L), where L' = abs(L - 0.5), and we
+ * simply calculate the 1-2 distance using these coordinates
+ * @param {goog.color.Hsl} hsl1 First color in hsl representation.
+ * @param {goog.color.Hsl} hsl2 Second color in hsl representation.
+ * @return {number} Distance between the two colors, in the range [0, 1].
+ */
+goog.color.hslDistance = function(hsl1, hsl2) {
+  var sl1, sl2;
+  if (hsl1[2] <= 0.5) {
+    sl1 = hsl1[1] * hsl1[2];
+  } else {
+    sl1 = hsl1[1] * (1.0 - hsl1[2]);
+  }
+
+  if (hsl2[2] <= 0.5) {
+    sl2 = hsl2[1] * hsl2[2];
+  } else {
+    sl2 = hsl2[1] * (1.0 - hsl2[2]);
+  }
+
+  var h1 = hsl1[0] / 360.0;
+  var h2 = hsl2[0] / 360.0;
+  var dh = (h1 - h2) * 2.0 * Math.PI;
+  return (hsl1[2] - hsl2[2]) * (hsl1[2] - hsl2[2]) + sl1 * sl1 + sl2 * sl2 -
+      2 * sl1 * sl2 * Math.cos(dh);
+};
+
+
+/**
+ * Blend two colors together, using the specified factor to indicate the weight
+ * given to the first color
+ * @param {goog.color.Rgb} rgb1 First color represented in rgb.
+ * @param {goog.color.Rgb} rgb2 Second color represented in rgb.
+ * @param {number} factor The weight to be given to rgb1 over rgb2. Values
+ *     should be in the range [0, 1]. If less than 0, factor will be set to 0.
+ *     If greater than 1, factor will be set to 1.
+ * @return {!goog.color.Rgb} Combined color represented in rgb.
+ */
+goog.color.blend = function(rgb1, rgb2, factor) {
+  factor = goog.math.clamp(factor, 0, 1);
+
+  return [
+    Math.round(factor * rgb1[0] + (1.0 - factor) * rgb2[0]),
+    Math.round(factor * rgb1[1] + (1.0 - factor) * rgb2[1]),
+    Math.round(factor * rgb1[2] + (1.0 - factor) * rgb2[2])
+  ];
+};
+
+
+/**
+ * Adds black to the specified color, darkening it
+ * @param {goog.color.Rgb} rgb rgb representation of the color.
+ * @param {number} factor Number in the range [0, 1]. 0 will do nothing, while
+ *     1 will return black. If less than 0, factor will be set to 0. If greater
+ *     than 1, factor will be set to 1.
+ * @return {!goog.color.Rgb} Combined rgb color.
+ */
+goog.color.darken = function(rgb, factor) {
+  var black = [0, 0, 0];
+  return goog.color.blend(black, rgb, factor);
+};
+
+
+/**
+ * Adds white to the specified color, lightening it
+ * @param {goog.color.Rgb} rgb rgb representation of the color.
+ * @param {number} factor Number in the range [0, 1].  0 will do nothing, while
+ *     1 will return white. If less than 0, factor will be set to 0. If greater
+ *     than 1, factor will be set to 1.
+ * @return {!goog.color.Rgb} Combined rgb color.
+ */
+goog.color.lighten = function(rgb, factor) {
+  var white = [255, 255, 255];
+  return goog.color.blend(white, rgb, factor);
+};
+
+
+/**
+ * Find the "best" (highest-contrast) of the suggested colors for the prime
+ * color. Uses W3C formula for judging readability and visual accessibility:
+ * http://www.w3.org/TR/AERT#color-contrast
+ * @param {goog.color.Rgb} prime Color represented as a rgb array.
+ * @param {Array<goog.color.Rgb>} suggestions Array of colors,
+ *     each representing a rgb array.
+ * @return {!goog.color.Rgb} Highest-contrast color represented by an array..
+ */
+goog.color.highContrast = function(prime, suggestions) {
+  var suggestionsWithDiff = [];
+  for (var i = 0; i < suggestions.length; i++) {
+    suggestionsWithDiff.push({
+      color: suggestions[i],
+      diff: goog.color.yiqBrightnessDiff_(suggestions[i], prime) +
+          goog.color.colorDiff_(suggestions[i], prime)
+    });
+  }
+  suggestionsWithDiff.sort(function(a, b) { return b.diff - a.diff; });
+  return suggestionsWithDiff[0].color;
+};
+
+
+/**
+ * Calculate brightness of a color according to YIQ formula (brightness is Y).
+ * More info on YIQ here: http://en.wikipedia.org/wiki/YIQ. Helper method for
+ * goog.color.highContrast()
+ * @param {goog.color.Rgb} rgb Color represented by a rgb array.
+ * @return {number} brightness (Y).
+ * @private
+ */
+goog.color.yiqBrightness_ = function(rgb) {
+  return Math.round((rgb[0] * 299 + rgb[1] * 587 + rgb[2] * 114) / 1000);
+};
+
+
+/**
+ * Calculate difference in brightness of two colors. Helper method for
+ * goog.color.highContrast()
+ * @param {goog.color.Rgb} rgb1 Color represented by a rgb array.
+ * @param {goog.color.Rgb} rgb2 Color represented by a rgb array.
+ * @return {number} Brightness difference.
+ * @private
+ */
+goog.color.yiqBrightnessDiff_ = function(rgb1, rgb2) {
+  return Math.abs(
+      goog.color.yiqBrightness_(rgb1) - goog.color.yiqBrightness_(rgb2));
+};
+
+
+/**
+ * Calculate color difference between two colors. Helper method for
+ * goog.color.highContrast()
+ * @param {goog.color.Rgb} rgb1 Color represented by a rgb array.
+ * @param {goog.color.Rgb} rgb2 Color represented by a rgb array.
+ * @return {number} Color difference.
+ * @private
+ */
+goog.color.colorDiff_ = function(rgb1, rgb2) {
+  return Math.abs(rgb1[0] - rgb2[0]) + Math.abs(rgb1[1] - rgb2[1]) +
+      Math.abs(rgb1[2] - rgb2[2]);
+};
+
+// We can't use goog.color or goog.color.alpha because they interally use a hex
+// string representation that encodes each channel in a single byte.  This
+// causes occasional loss of precision and rounding errors, especially in the
+// alpha channel.
+
+goog.provide('ol.color');
+
+goog.require('goog.asserts');
+goog.require('goog.color');
+goog.require('goog.color.names');
+goog.require('ol');
+goog.require('ol.math');
+
+
+/**
+ * This RegExp matches # followed by 3 or 6 hex digits.
+ * @const
+ * @type {RegExp}
+ * @private
+ */
+ol.color.hexColorRe_ = /^#(?:[0-9a-f]{3}){1,2}$/i;
+
+
+/**
+ * @see goog.color.rgbColorRe_
+ * @const
+ * @type {RegExp}
+ * @private
+ */
+ol.color.rgbColorRe_ =
+    /^(?:rgb)?\((0|[1-9]\d{0,2}),\s?(0|[1-9]\d{0,2}),\s?(0|[1-9]\d{0,2})\)$/i;
+
+
+/**
+ * @see goog.color.alpha.rgbaColorRe_
+ * @const
+ * @type {RegExp}
+ * @private
+ */
+ol.color.rgbaColorRe_ =
+    /^(?:rgba)?\((0|[1-9]\d{0,2}),\s?(0|[1-9]\d{0,2}),\s?(0|[1-9]\d{0,2}),\s?(0|1|0\.\d{0,10})\)$/i;
+
+
+/**
+ * Return the color as an array. This function maintains a cache of calculated
+ * arrays which means the result should not be modified.
+ * @param {ol.Color|string} color Color.
+ * @return {ol.Color} Color.
+ * @api
+ */
+ol.color.asArray = function(color) {
+  if (Array.isArray(color)) {
+    return color;
+  } else {
+    goog.asserts.assert(typeof color === 'string', 'Color should be a string');
+    return ol.color.fromString(color);
+  }
+};
+
+
+/**
+ * Return the color as an rgba string.
+ * @param {ol.Color|string} color Color.
+ * @return {string} Rgba string.
+ * @api
+ */
+ol.color.asString = function(color) {
+  if (typeof color === 'string') {
+    return color;
+  } else {
+    goog.asserts.assert(Array.isArray(color), 'Color should be an array');
+    return ol.color.toString(color);
+  }
+};
+
+
+/**
+ * @param {string} s String.
+ * @return {ol.Color} Color.
+ */
+ol.color.fromString = (
+    function() {
+
+      // We maintain a small cache of parsed strings.  To provide cheap LRU-like
+      // semantics, whenever the cache grows too large we simply delete an
+      // arbitrary 25% of the entries.
+
+      /**
+       * @const
+       * @type {number}
+       */
+      var MAX_CACHE_SIZE = 1024;
+
+      /**
+       * @type {Object.<string, ol.Color>}
+       */
+      var cache = {};
+
+      /**
+       * @type {number}
+       */
+      var cacheSize = 0;
+
+      return (
+          /**
+           * @param {string} s String.
+           * @return {ol.Color} Color.
+           */
+          function(s) {
+            var color;
+            if (cache.hasOwnProperty(s)) {
+              color = cache[s];
+            } else {
+              if (cacheSize >= MAX_CACHE_SIZE) {
+                var i = 0;
+                var key;
+                for (key in cache) {
+                  if ((i++ & 3) === 0) {
+                    delete cache[key];
+                    --cacheSize;
+                  }
+                }
+              }
+              color = ol.color.fromStringInternal_(s);
+              cache[s] = color;
+              ++cacheSize;
+            }
+            return color;
+          });
+
+    })();
+
+
+/**
+ * @param {string} s String.
+ * @private
+ * @return {ol.Color} Color.
+ */
+ol.color.fromStringInternal_ = function(s) {
+
+  var isHex = false;
+  if (ol.ENABLE_NAMED_COLORS && goog.color.names.hasOwnProperty(s)) {
+    s = goog.color.names[s];
+    isHex = true;
+  }
+
+  var r, g, b, a, color, match;
+  if (isHex || (match = ol.color.hexColorRe_.exec(s))) { // hex
+    var n = s.length - 1; // number of hex digits
+    goog.asserts.assert(n == 3 || n == 6,
+        'Color string length should be 3 or 6');
+    var d = n == 3 ? 1 : 2; // number of digits per channel
+    r = parseInt(s.substr(1 + 0 * d, d), 16);
+    g = parseInt(s.substr(1 + 1 * d, d), 16);
+    b = parseInt(s.substr(1 + 2 * d, d), 16);
+    if (d == 1) {
+      r = (r << 4) + r;
+      g = (g << 4) + g;
+      b = (b << 4) + b;
+    }
+    a = 1;
+    color = [r, g, b, a];
+    goog.asserts.assert(ol.color.isValid(color),
+        'Color should be a valid color');
+    return color;
+  } else if ((match = ol.color.rgbaColorRe_.exec(s))) { // rgba()
+    r = Number(match[1]);
+    g = Number(match[2]);
+    b = Number(match[3]);
+    a = Number(match[4]);
+    color = [r, g, b, a];
+    return ol.color.normalize(color, color);
+  } else if ((match = ol.color.rgbColorRe_.exec(s))) { // rgb()
+    r = Number(match[1]);
+    g = Number(match[2]);
+    b = Number(match[3]);
+    color = [r, g, b, 1];
+    return ol.color.normalize(color, color);
+  } else {
+    goog.asserts.fail(s + ' is not a valid color');
+  }
+
+};
+
+
+/**
+ * @param {ol.Color} color Color.
+ * @return {boolean} Is valid.
+ */
+ol.color.isValid = function(color) {
+  return 0 <= color[0] && color[0] < 256 &&
+      0 <= color[1] && color[1] < 256 &&
+      0 <= color[2] && color[2] < 256 &&
+      0 <= color[3] && color[3] <= 1;
+};
+
+
+/**
+ * @param {ol.Color} color Color.
+ * @param {ol.Color=} opt_color Color.
+ * @return {ol.Color} Clamped color.
+ */
+ol.color.normalize = function(color, opt_color) {
+  var result = opt_color || [];
+  result[0] = ol.math.clamp((color[0] + 0.5) | 0, 0, 255);
+  result[1] = ol.math.clamp((color[1] + 0.5) | 0, 0, 255);
+  result[2] = ol.math.clamp((color[2] + 0.5) | 0, 0, 255);
+  result[3] = ol.math.clamp(color[3], 0, 1);
+  return result;
+};
+
+
+/**
+ * @param {ol.Color} color Color.
+ * @return {string} String.
+ */
+ol.color.toString = function(color) {
+  var r = color[0];
+  if (r != (r | 0)) {
+    r = (r + 0.5) | 0;
+  }
+  var g = color[1];
+  if (g != (g | 0)) {
+    g = (g + 0.5) | 0;
+  }
+  var b = color[2];
+  if (b != (b | 0)) {
+    b = (b + 0.5) | 0;
+  }
+  var a = color[3] === undefined ? 1 : color[3];
+  return 'rgba(' + r + ',' + g + ',' + b + ',' + a + ')';
+};
+
+goog.provide('ol.colorlike');
+
+goog.require('ol.color');
+
+
+/**
+ * @param {ol.Color|ol.ColorLike} color Color.
+ * @return {ol.ColorLike} The color as an ol.ColorLike
+ * @api
+ */
+ol.colorlike.asColorLike = function(color) {
+  if (ol.colorlike.isColorLike(color)) {
+    return /** @type {string|CanvasPattern|CanvasGradient} */ (color);
+  } else {
+    return ol.color.asString(/** @type {ol.Color} */ (color));
+  }
+};
+
+
+/**
+ * @param {?} color The value that is potentially an ol.ColorLike
+ * @return {boolean} Whether the color is an ol.ColorLike
+ */
+ol.colorlike.isColorLike = function(color) {
+  return (
+      typeof color === 'string' ||
+      color instanceof CanvasPattern ||
+      color instanceof CanvasGradient
+  );
+};
+
+// Copyright 2013 The Closure Library Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS-IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+/**
+ * @fileoverview Utilities used by goog.labs.userAgent tools. These functions
+ * should not be used outside of goog.labs.userAgent.*.
+ *
+ *
+ * @author nnaze@google.com (Nathan Naze)
+ */
+
+goog.provide('goog.labs.userAgent.util');
+
+goog.require('goog.string');
+
+
+/**
+ * Gets the native userAgent string from navigator if it exists.
+ * If navigator or navigator.userAgent string is missing, returns an empty
+ * string.
+ * @return {string}
+ * @private
+ */
+goog.labs.userAgent.util.getNativeUserAgentString_ = function() {
+  var navigator = goog.labs.userAgent.util.getNavigator_();
+  if (navigator) {
+    var userAgent = navigator.userAgent;
+    if (userAgent) {
+      return userAgent;
+    }
+  }
+  return '';
+};
+
+
+/**
+ * Getter for the native navigator.
+ * This is a separate function so it can be stubbed out in testing.
+ * @return {Navigator}
+ * @private
+ */
+goog.labs.userAgent.util.getNavigator_ = function() {
+  return goog.global.navigator;
+};
+
+
+/**
+ * A possible override for applications which wish to not check
+ * navigator.userAgent but use a specified value for detection instead.
+ * @private {string}
+ */
+goog.labs.userAgent.util.userAgent_ =
+    goog.labs.userAgent.util.getNativeUserAgentString_();
+
+
+/**
+ * Applications may override browser detection on the built in
+ * navigator.userAgent object by setting this string. Set to null to use the
+ * browser object instead.
+ * @param {?string=} opt_userAgent The User-Agent override.
+ */
+goog.labs.userAgent.util.setUserAgent = function(opt_userAgent) {
+  goog.labs.userAgent.util.userAgent_ =
+      opt_userAgent || goog.labs.userAgent.util.getNativeUserAgentString_();
+};
+
+
+/**
+ * @return {string} The user agent string.
+ */
+goog.labs.userAgent.util.getUserAgent = function() {
+  return goog.labs.userAgent.util.userAgent_;
+};
+
+
+/**
+ * @param {string} str
+ * @return {boolean} Whether the user agent contains the given string, ignoring
+ *     case.
+ */
+goog.labs.userAgent.util.matchUserAgent = function(str) {
+  var userAgent = goog.labs.userAgent.util.getUserAgent();
+  return goog.string.contains(userAgent, str);
+};
+
+
+/**
+ * @param {string} str
+ * @return {boolean} Whether the user agent contains the given string.
+ */
+goog.labs.userAgent.util.matchUserAgentIgnoreCase = function(str) {
+  var userAgent = goog.labs.userAgent.util.getUserAgent();
+  return goog.string.caseInsensitiveContains(userAgent, str);
+};
+
+
+/**
+ * Parses the user agent into tuples for each section.
+ * @param {string} userAgent
+ * @return {!Array<!Array<string>>} Tuples of key, version, and the contents
+ *     of the parenthetical.
+ */
+goog.labs.userAgent.util.extractVersionTuples = function(userAgent) {
+  // Matches each section of a user agent string.
+  // Example UA:
+  // Mozilla/5.0 (iPad; U; CPU OS 3_2_1 like Mac OS X; en-us)
+  // AppleWebKit/531.21.10 (KHTML, like Gecko) Mobile/7B405
+  // This has three version tuples: Mozilla, AppleWebKit, and Mobile.
+
+  var versionRegExp = new RegExp(
+      // Key. Note that a key may have a space.
+      // (i.e. 'Mobile Safari' in 'Mobile Safari/5.0')
+      '(\\w[\\w ]+)' +
+
+          '/' +                // slash
+          '([^\\s]+)' +        // version (i.e. '5.0b')
+          '\\s*' +             // whitespace
+          '(?:\\((.*?)\\))?',  // parenthetical info. parentheses not matched.
+      'g');
+
+  var data = [];
+  var match;
+
+  // Iterate and collect the version tuples.  Each iteration will be the
+  // next regex match.
+  while (match = versionRegExp.exec(userAgent)) {
+    data.push([
+      match[1],  // key
+      match[2],  // value
+      // || undefined as this is not undefined in IE7 and IE8
+      match[3] || undefined  // info
+    ]);
+  }
+
+  return data;
+};
+
+// Copyright 2006 The Closure Library Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS-IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+/**
+ * @fileoverview Utilities for manipulating objects/maps/hashes.
+ * @author arv@google.com (Erik Arvidsson)
+ */
+
+goog.provide('goog.object');
+
+
+/**
+ * Whether two values are not observably distinguishable. This
+ * correctly detects that 0 is not the same as -0 and two NaNs are
+ * practically equivalent.
+ *
+ * The implementation is as suggested by harmony:egal proposal.
+ *
+ * @param {*} v The first value to compare.
+ * @param {*} v2 The second value to compare.
+ * @return {boolean} Whether two values are not observably distinguishable.
+ * @see http://wiki.ecmascript.org/doku.php?id=harmony:egal
+ */
+goog.object.is = function(v, v2) {
+  if (v === v2) {
+    // 0 === -0, but they are not identical.
+    // We need the cast because the compiler requires that v2 is a
+    // number (although 1/v2 works with non-number). We cast to ? to
+    // stop the compiler from type-checking this statement.
+    return v !== 0 || 1 / v === 1 / /** @type {?} */ (v2);
+  }
+
+  // NaN is non-reflexive: NaN !== NaN, although they are identical.
+  return v !== v && v2 !== v2;
+};
+
+
+/**
+ * Calls a function for each element in an object/map/hash.
+ *
+ * @param {Object<K,V>} obj The object over which to iterate.
+ * @param {function(this:T,V,?,Object<K,V>):?} f The function to call
+ *     for every element. This function takes 3 arguments (the value, the
+ *     key and the object) and the return value is ignored.
+ * @param {T=} opt_obj This is used as the 'this' object within f.
+ * @template T,K,V
+ */
+goog.object.forEach = function(obj, f, opt_obj) {
+  for (var key in obj) {
+    f.call(/** @type {?} */ (opt_obj), obj[key], key, obj);
+  }
+};
+
+
+/**
+ * Calls a function for each element in an object/map/hash. If that call returns
+ * true, adds the element to a new object.
+ *
+ * @param {Object<K,V>} obj The object over which to iterate.
+ * @param {function(this:T,V,?,Object<K,V>):boolean} f The function to call
+ *     for every element. This
+ *     function takes 3 arguments (the value, the key and the object)
+ *     and should return a boolean. If the return value is true the
+ *     element is added to the result object. If it is false the
+ *     element is not included.
+ * @param {T=} opt_obj This is used as the 'this' object within f.
+ * @return {!Object<K,V>} a new object in which only elements that passed the
+ *     test are present.
+ * @template T,K,V
+ */
+goog.object.filter = function(obj, f, opt_obj) {
+  var res = {};
+  for (var key in obj) {
+    if (f.call(/** @type {?} */ (opt_obj), obj[key], key, obj)) {
+      res[key] = obj[key];
+    }
+  }
+  return res;
+};
+
+
+/**
+ * For every element in an object/map/hash calls a function and inserts the
+ * result into a new object.
+ *
+ * @param {Object<K,V>} obj The object over which to iterate.
+ * @param {function(this:T,V,?,Object<K,V>):R} f The function to call
+ *     for every element. This function
+ *     takes 3 arguments (the value, the key and the object)
+ *     and should return something. The result will be inserted
+ *     into a new object.
+ * @param {T=} opt_obj This is used as the 'this' object within f.
+ * @return {!Object<K,R>} a new object with the results from f.
+ * @template T,K,V,R
+ */
+goog.object.map = function(obj, f, opt_obj) {
+  var res = {};
+  for (var key in obj) {
+    res[key] = f.call(/** @type {?} */ (opt_obj), obj[key], key, obj);
+  }
+  return res;
+};
+
+
+/**
+ * Calls a function for each element in an object/map/hash. If any
+ * call returns true, returns true (without checking the rest). If
+ * all calls return false, returns false.
+ *
+ * @param {Object<K,V>} obj The object to check.
+ * @param {function(this:T,V,?,Object<K,V>):boolean} f The function to
+ *     call for every element. This function
+ *     takes 3 arguments (the value, the key and the object) and should
+ *     return a boolean.
+ * @param {T=} opt_obj This is used as the 'this' object within f.
+ * @return {boolean} true if any element passes the test.
+ * @template T,K,V
+ */
+goog.object.some = function(obj, f, opt_obj) {
+  for (var key in obj) {
+    if (f.call(/** @type {?} */ (opt_obj), obj[key], key, obj)) {
+      return true;
+    }
+  }
+  return false;
+};
+
+
+/**
+ * Calls a function for each element in an object/map/hash. If
+ * all calls return true, returns true. If any call returns false, returns
+ * false at this point and does not continue to check the remaining elements.
+ *
+ * @param {Object<K,V>} obj The object to check.
+ * @param {?function(this:T,V,?,Object<K,V>):boolean} f The function to
+ *     call for every element. This function
+ *     takes 3 arguments (the value, the key and the object) and should
+ *     return a boolean.
+ * @param {T=} opt_obj This is used as the 'this' object within f.
+ * @return {boolean} false if any element fails the test.
+ * @template T,K,V
+ */
+goog.object.every = function(obj, f, opt_obj) {
+  for (var key in obj) {
+    if (!f.call(/** @type {?} */ (opt_obj), obj[key], key, obj)) {
+      return false;
+    }
+  }
+  return true;
+};
+
+
+/**
+ * Returns the number of key-value pairs in the object map.
+ *
+ * @param {Object} obj The object for which to get the number of key-value
+ *     pairs.
+ * @return {number} The number of key-value pairs in the object map.
+ */
+goog.object.getCount = function(obj) {
+  var rv = 0;
+  for (var key in obj) {
+    rv++;
+  }
+  return rv;
+};
+
+
+/**
+ * Returns one key from the object map, if any exists.
+ * For map literals the returned key will be the first one in most of the
+ * browsers (a know exception is Konqueror).
+ *
+ * @param {Object} obj The object to pick a key from.
+ * @return {string|undefined} The key or undefined if the object is empty.
+ */
+goog.object.getAnyKey = function(obj) {
+  for (var key in obj) {
+    return key;
+  }
+};
+
+
+/**
+ * Returns one value from the object map, if any exists.
+ * For map literals the returned value will be the first one in most of the
+ * browsers (a know exception is Konqueror).
+ *
+ * @param {Object<K,V>} obj The object to pick a value from.
+ * @return {V|undefined} The value or undefined if the object is empty.
+ * @template K,V
+ */
+goog.object.getAnyValue = function(obj) {
+  for (var key in obj) {
+    return obj[key];
+  }
+};
+
+
+/**
+ * Whether the object/hash/map contains the given object as a value.
+ * An alias for goog.object.containsValue(obj, val).
+ *
+ * @param {Object<K,V>} obj The object in which to look for val.
+ * @param {V} val The object for which to check.
+ * @return {boolean} true if val is present.
+ * @template K,V
+ */
+goog.object.contains = function(obj, val) {
+  return goog.object.containsValue(obj, val);
+};
+
+
+/**
+ * Returns the values of the object/map/hash.
+ *
+ * @param {Object<K,V>} obj The object from which to get the values.
+ * @return {!Array<V>} The values in the object/map/hash.
+ * @template K,V
+ */
+goog.object.getValues = function(obj) {
+  var res = [];
+  var i = 0;
+  for (var key in obj) {
+    res[i++] = obj[key];
+  }
+  return res;
+};
+
+
+/**
+ * Returns the keys of the object/map/hash.
+ *
+ * @param {Object} obj The object from which to get the keys.
+ * @return {!Array<string>} Array of property keys.
+ */
+goog.object.getKeys = function(obj) {
+  var res = [];
+  var i = 0;
+  for (var key in obj) {
+    res[i++] = key;
+  }
+  return res;
+};
+
+
+/**
+ * Get a value from an object multiple levels deep.  This is useful for
+ * pulling values from deeply nested objects, such as JSON responses.
+ * Example usage: getValueByKeys(jsonObj, 'foo', 'entries', 3)
+ *
+ * @param {!Object} obj An object to get the value from.  Can be array-like.
+ * @param {...(string|number|!IArrayLike<number|string>)}
+ *     var_args A number of keys
+ *     (as strings, or numbers, for array-like objects).  Can also be
+ *     specified as a single array of keys.
+ * @return {*} The resulting value.  If, at any point, the value for a key
+ *     is undefined, returns undefined.
+ */
+goog.object.getValueByKeys = function(obj, var_args) {
+  var isArrayLike = goog.isArrayLike(var_args);
+  var keys = isArrayLike ? var_args : arguments;
+
+  // Start with the 2nd parameter for the variable parameters syntax.
+  for (var i = isArrayLike ? 0 : 1; i < keys.length; i++) {
+    obj = obj[keys[i]];
+    if (!goog.isDef(obj)) {
+      break;
+    }
+  }
+
+  return obj;
+};
+
+
+/**
+ * Whether the object/map/hash contains the given key.
+ *
+ * @param {Object} obj The object in which to look for key.
+ * @param {?} key The key for which to check.
+ * @return {boolean} true If the map contains the key.
+ */
+goog.object.containsKey = function(obj, key) {
+  return obj !== null && key in obj;
+};
+
+
+/**
+ * Whether the object/map/hash contains the given value. This is O(n).
+ *
+ * @param {Object<K,V>} obj The object in which to look for val.
+ * @param {V} val The value for which to check.
+ * @return {boolean} true If the map contains the value.
+ * @template K,V
+ */
+goog.object.containsValue = function(obj, val) {
+  for (var key in obj) {
+    if (obj[key] == val) {
+      return true;
+    }
+  }
+  return false;
+};
+
+
+/**
+ * Searches an object for an element that satisfies the given condition and
+ * returns its key.
+ * @param {Object<K,V>} obj The object to search in.
+ * @param {function(this:T,V,string,Object<K,V>):boolean} f The
+ *      function to call for every element. Takes 3 arguments (the value,
+ *     the key and the object) and should return a boolean.
+ * @param {T=} opt_this An optional "this" context for the function.
+ * @return {string|undefined} The key of an element for which the function
+ *     returns true or undefined if no such element is found.
+ * @template T,K,V
+ */
+goog.object.findKey = function(obj, f, opt_this) {
+  for (var key in obj) {
+    if (f.call(/** @type {?} */ (opt_this), obj[key], key, obj)) {
+      return key;
+    }
+  }
+  return undefined;
+};
+
+
+/**
+ * Searches an object for an element that satisfies the given condition and
+ * returns its value.
+ * @param {Object<K,V>} obj The object to search in.
+ * @param {function(this:T,V,string,Object<K,V>):boolean} f The function
+ *     to call for every element. Takes 3 arguments (the value, the key
+ *     and the object) and should return a boolean.
+ * @param {T=} opt_this An optional "this" context for the function.
+ * @return {V} The value of an element for which the function returns true or
+ *     undefined if no such element is found.
+ * @template T,K,V
+ */
+goog.object.findValue = function(obj, f, opt_this) {
+  var key = goog.object.findKey(obj, f, opt_this);
+  return key && obj[key];
+};
+
+
+/**
+ * Whether the object/map/hash is empty.
+ *
+ * @param {Object} obj The object to test.
+ * @return {boolean} true if obj is empty.
+ */
+goog.object.isEmpty = function(obj) {
+  for (var key in obj) {
+    return false;
+  }
+  return true;
+};
+
+
+/**
+ * Removes all key value pairs from the object/map/hash.
+ *
+ * @param {Object} obj The object to clear.
+ */
+goog.object.clear = function(obj) {
+  for (var i in obj) {
+    delete obj[i];
+  }
+};
+
+
+/**
+ * Removes a key-value pair based on the key.
+ *
+ * @param {Object} obj The object from which to remove the key.
+ * @param {?} key The key to remove.
+ * @return {boolean} Whether an element was removed.
+ */
+goog.object.remove = function(obj, key) {
+  var rv;
+  if (rv = key in /** @type {!Object} */ (obj)) {
+    delete obj[key];
+  }
+  return rv;
+};
+
+
+/**
+ * Adds a key-value pair to the object. Throws an exception if the key is
+ * already in use. Use set if you want to change an existing pair.
+ *
+ * @param {Object<K,V>} obj The object to which to add the key-value pair.
+ * @param {string} key The key to add.
+ * @param {V} val The value to add.
+ * @template K,V
+ */
+goog.object.add = function(obj, key, val) {
+  if (obj !== null && key in obj) {
+    throw Error('The object already contains the key "' + key + '"');
+  }
+  goog.object.set(obj, key, val);
+};
+
+
+/**
+ * Returns the value for the given key.
+ *
+ * @param {Object<K,V>} obj The object from which to get the value.
+ * @param {string} key The key for which to get the value.
+ * @param {R=} opt_val The value to return if no item is found for the given
+ *     key (default is undefined).
+ * @return {V|R|undefined} The value for the given key.
+ * @template K,V,R
+ */
+goog.object.get = function(obj, key, opt_val) {
+  if (obj !== null && key in obj) {
+    return obj[key];
+  }
+  return opt_val;
+};
+
+
+/**
+ * Adds a key-value pair to the object/map/hash.
+ *
+ * @param {Object<K,V>} obj The object to which to add the key-value pair.
+ * @param {string} key The key to add.
+ * @param {V} value The value to add.
+ * @template K,V
+ */
+goog.object.set = function(obj, key, value) {
+  obj[key] = value;
+};
+
+
+/**
+ * Adds a key-value pair to the object/map/hash if it doesn't exist yet.
+ *
+ * @param {Object<K,V>} obj The object to which to add the key-value pair.
+ * @param {string} key The key to add.
+ * @param {V} value The value to add if the key wasn't present.
+ * @return {V} The value of the entry at the end of the function.
+ * @template K,V
+ */
+goog.object.setIfUndefined = function(obj, key, value) {
+  return key in /** @type {!Object} */ (obj) ? obj[key] : (obj[key] = value);
+};
+
+
+/**
+ * Sets a key and value to an object if the key is not set. The value will be
+ * the return value of the given function. If the key already exists, the
+ * object will not be changed and the function will not be called (the function
+ * will be lazily evaluated -- only called if necessary).
+ *
+ * This function is particularly useful for use with a map used a as a cache.
+ *
+ * @param {!Object<K,V>} obj The object to which to add the key-value pair.
+ * @param {string} key The key to add.
+ * @param {function():V} f The value to add if the key wasn't present.
+ * @return {V} The value of the entry at the end of the function.
+ * @template K,V
+ */
+goog.object.setWithReturnValueIfNotSet = function(obj, key, f) {
+  if (key in obj) {
+    return obj[key];
+  }
+
+  var val = f();
+  obj[key] = val;
+  return val;
+};
+
+
+/**
+ * Compares two objects for equality using === on the values.
+ *
+ * @param {!Object<K,V>} a
+ * @param {!Object<K,V>} b
+ * @return {boolean}
+ * @template K,V
+ */
+goog.object.equals = function(a, b) {
+  for (var k in a) {
+    if (!(k in b) || a[k] !== b[k]) {
+      return false;
+    }
+  }
+  for (var k in b) {
+    if (!(k in a)) {
+      return false;
+    }
+  }
+  return true;
+};
+
+
+/**
+ * Returns a shallow clone of the object.
+ *
+ * @param {Object<K,V>} obj Object to clone.
+ * @return {!Object<K,V>} Clone of the input object.
+ * @template K,V
+ */
+goog.object.clone = function(obj) {
+  // We cannot use the prototype trick because a lot of methods depend on where
+  // the actual key is set.
+
+  var res = {};
+  for (var key in obj) {
+    res[key] = obj[key];
+  }
+  return res;
+  // We could also use goog.mixin but I wanted this to be independent from that.
+};
+
+
+/**
+ * Clones a value. The input may be an Object, Array, or basic type. Objects and
+ * arrays will be cloned recursively.
+ *
+ * WARNINGS:
+ * <code>goog.object.unsafeClone</code> does not detect reference loops. Objects
+ * that refer to themselves will cause infinite recursion.
+ *
+ * <code>goog.object.unsafeClone</code> is unaware of unique identifiers, and
+ * copies UIDs created by <code>getUid</code> into cloned results.
+ *
+ * @param {*} obj The value to clone.
+ * @return {*} A clone of the input value.
+ */
+goog.object.unsafeClone = function(obj) {
+  var type = goog.typeOf(obj);
+  if (type == 'object' || type == 'array') {
+    if (goog.isFunction(obj.clone)) {
+      return obj.clone();
+    }
+    var clone = type == 'array' ? [] : {};
+    for (var key in obj) {
+      clone[key] = goog.object.unsafeClone(obj[key]);
+    }
+    return clone;
+  }
+
+  return obj;
+};
+
+
+/**
+ * Returns a new object in which all the keys and values are interchanged
+ * (keys become values and values become keys). If multiple keys map to the
+ * same value, the chosen transposed value is implementation-dependent.
+ *
+ * @param {Object} obj The object to transpose.
+ * @return {!Object} The transposed object.
+ */
+goog.object.transpose = function(obj) {
+  var transposed = {};
+  for (var key in obj) {
+    transposed[obj[key]] = key;
+  }
+  return transposed;
+};
+
+
+/**
+ * The names of the fields that are defined on Object.prototype.
+ * @type {Array<string>}
+ * @private
+ */
+goog.object.PROTOTYPE_FIELDS_ = [
+  'constructor', 'hasOwnProperty', 'isPrototypeOf', 'propertyIsEnumerable',
+  'toLocaleString', 'toString', 'valueOf'
+];
+
+
+/**
+ * Extends an object with another object.
+ * This operates 'in-place'; it does not create a new Object.
+ *
+ * Example:
+ * var o = {};
+ * goog.object.extend(o, {a: 0, b: 1});
+ * o; // {a: 0, b: 1}
+ * goog.object.extend(o, {b: 2, c: 3});
+ * o; // {a: 0, b: 2, c: 3}
+ *
+ * @param {Object} target The object to modify. Existing properties will be
+ *     overwritten if they are also present in one of the objects in
+ *     {@code var_args}.
+ * @param {...Object} var_args The objects from which values will be copied.
+ */
+goog.object.extend = function(target, var_args) {
+  var key, source;
+  for (var i = 1; i < arguments.length; i++) {
+    source = arguments[i];
+    for (key in source) {
+      target[key] = source[key];
+    }
+
+    // For IE the for-in-loop does not contain any properties that are not
+    // enumerable on the prototype object (for example isPrototypeOf from
+    // Object.prototype) and it will also not include 'replace' on objects that
+    // extend String and change 'replace' (not that it is common for anyone to
+    // extend anything except Object).
+
+    for (var j = 0; j < goog.object.PROTOTYPE_FIELDS_.length; j++) {
+      key = goog.object.PROTOTYPE_FIELDS_[j];
+      if (Object.prototype.hasOwnProperty.call(source, key)) {
+        target[key] = source[key];
+      }
+    }
+  }
+};
+
+
+/**
+ * Creates a new object built from the key-value pairs provided as arguments.
+ * @param {...*} var_args If only one argument is provided and it is an array
+ *     then this is used as the arguments,  otherwise even arguments are used as
+ *     the property names and odd arguments are used as the property values.
+ * @return {!Object} The new object.
+ * @throws {Error} If there are uneven number of arguments or there is only one
+ *     non array argument.
+ */
+goog.object.create = function(var_args) {
+  var argLength = arguments.length;
+  if (argLength == 1 && goog.isArray(arguments[0])) {
+    return goog.object.create.apply(null, arguments[0]);
+  }
+
+  if (argLength % 2) {
+    throw Error('Uneven number of arguments');
+  }
+
+  var rv = {};
+  for (var i = 0; i < argLength; i += 2) {
+    rv[arguments[i]] = arguments[i + 1];
+  }
+  return rv;
+};
+
+
+/**
+ * Creates a new object where the property names come from the arguments but
+ * the value is always set to true
+ * @param {...*} var_args If only one argument is provided and it is an array
+ *     then this is used as the arguments,  otherwise the arguments are used
+ *     as the property names.
+ * @return {!Object} The new object.
+ */
+goog.object.createSet = function(var_args) {
+  var argLength = arguments.length;
+  if (argLength == 1 && goog.isArray(arguments[0])) {
+    return goog.object.createSet.apply(null, arguments[0]);
+  }
+
+  var rv = {};
+  for (var i = 0; i < argLength; i++) {
+    rv[arguments[i]] = true;
+  }
+  return rv;
+};
+
+
+/**
+ * Creates an immutable view of the underlying object, if the browser
+ * supports immutable objects.
+ *
+ * In default mode, writes to this view will fail silently. In strict mode,
+ * they will throw an error.
+ *
+ * @param {!Object<K,V>} obj An object.
+ * @return {!Object<K,V>} An immutable view of that object, or the
+ *     original object if this browser does not support immutables.
+ * @template K,V
+ */
+goog.object.createImmutableView = function(obj) {
+  var result = obj;
+  if (Object.isFrozen && !Object.isFrozen(obj)) {
+    result = Object.create(obj);
+    Object.freeze(result);
+  }
+  return result;
+};
+
+
+/**
+ * @param {!Object} obj An object.
+ * @return {boolean} Whether this is an immutable view of the object.
+ */
+goog.object.isImmutableView = function(obj) {
+  return !!Object.isFrozen && Object.isFrozen(obj);
+};
+
+// Copyright 2013 The Closure Library Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS-IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+/**
+ * @fileoverview Closure user agent detection (Browser).
+ * @see <a href="http://www.useragentstring.com/">User agent strings</a>
+ * For more information on rendering engine, platform, or device see the other
+ * sub-namespaces in goog.labs.userAgent, goog.labs.userAgent.platform,
+ * goog.labs.userAgent.device respectively.)
+ *
+ * @author martone@google.com (Andy Martone)
+ */
+
+goog.provide('goog.labs.userAgent.browser');
+
+goog.require('goog.array');
+goog.require('goog.labs.userAgent.util');
+goog.require('goog.object');
+goog.require('goog.string');
+
+
+// TODO(nnaze): Refactor to remove excessive exclusion logic in matching
+// functions.
+
+
+/**
+ * @return {boolean} Whether the user's browser is Opera.  Note: Chromium
+ *     based Opera (Opera 15+) is detected as Chrome to avoid unnecessary
+ *     special casing.
+ * @private
+ */
+goog.labs.userAgent.browser.matchOpera_ = function() {
+  return goog.labs.userAgent.util.matchUserAgent('Opera');
+};
+
+
+/**
+ * @return {boolean} Whether the user's browser is IE.
+ * @private
+ */
+goog.labs.userAgent.browser.matchIE_ = function() {
+  return goog.labs.userAgent.util.matchUserAgent('Trident') ||
+      goog.labs.userAgent.util.matchUserAgent('MSIE');
+};
+
+
+/**
+ * @return {boolean} Whether the user's browser is Edge.
+ * @private
+ */
+goog.labs.userAgent.browser.matchEdge_ = function() {
+  return goog.labs.userAgent.util.matchUserAgent('Edge');
+};
+
+
+/**
+ * @return {boolean} Whether the user's browser is Firefox.
+ * @private
+ */
+goog.labs.userAgent.browser.matchFirefox_ = function() {
+  return goog.labs.userAgent.util.matchUserAgent('Firefox');
+};
+
+
+/**
+ * @return {boolean} Whether the user's browser is Safari.
+ * @private
+ */
+goog.labs.userAgent.browser.matchSafari_ = function() {
+  return goog.labs.userAgent.util.matchUserAgent('Safari') &&
+      !(goog.labs.userAgent.browser.matchChrome_() ||
+        goog.labs.userAgent.browser.matchCoast_() ||
+        goog.labs.userAgent.browser.matchOpera_() ||
+        goog.labs.userAgent.browser.matchEdge_() ||
+        goog.labs.userAgent.browser.isSilk() ||
+        goog.labs.userAgent.util.matchUserAgent('Android'));
+};
+
+
+/**
+ * @return {boolean} Whether the user's browser is Coast (Opera's Webkit-based
+ *     iOS browser).
+ * @private
+ */
+goog.labs.userAgent.browser.matchCoast_ = function() {
+  return goog.labs.userAgent.util.matchUserAgent('Coast');
+};
+
+
+/**
+ * @return {boolean} Whether the user's browser is iOS Webview.
+ * @private
+ */
+goog.labs.userAgent.browser.matchIosWebview_ = function() {
+  // iOS Webview does not show up as Chrome or Safari. Also check for Opera's
+  // WebKit-based iOS browser, Coast.
+  return (goog.labs.userAgent.util.matchUserAgent('iPad') ||
+          goog.labs.userAgent.util.matchUserAgent('iPhone')) &&
+      !goog.labs.userAgent.browser.matchSafari_() &&
+      !goog.labs.userAgent.browser.matchChrome_() &&
+      !goog.labs.userAgent.browser.matchCoast_() &&
+      goog.labs.userAgent.util.matchUserAgent('AppleWebKit');
+};
+
+
+/**
+ * @return {boolean} Whether the user's browser is Chrome.
+ * @private
+ */
+goog.labs.userAgent.browser.matchChrome_ = function() {
+  return (goog.labs.userAgent.util.matchUserAgent('Chrome') ||
+          goog.labs.userAgent.util.matchUserAgent('CriOS')) &&
+      !goog.labs.userAgent.browser.matchEdge_();
+};
+
+
+/**
+ * @return {boolean} Whether the user's browser is the Android browser.
+ * @private
+ */
+goog.labs.userAgent.browser.matchAndroidBrowser_ = function() {
+  // Android can appear in the user agent string for Chrome on Android.
+  // This is not the Android standalone browser if it does.
+  return goog.labs.userAgent.util.matchUserAgent('Android') &&
+      !(goog.labs.userAgent.browser.isChrome() ||
+        goog.labs.userAgent.browser.isFirefox() ||
+        goog.labs.userAgent.browser.isOpera() ||
+        goog.labs.userAgent.browser.isSilk());
+};
+
+
+/**
+ * @return {boolean} Whether the user's browser is Opera.
+ */
+goog.labs.userAgent.browser.isOpera = goog.labs.userAgent.browser.matchOpera_;
+
+
+/**
+ * @return {boolean} Whether the user's browser is IE.
+ */
+goog.labs.userAgent.browser.isIE = goog.labs.userAgent.browser.matchIE_;
+
+
+/**
+ * @return {boolean} Whether the user's browser is Edge.
+ */
+goog.labs.userAgent.browser.isEdge = goog.labs.userAgent.browser.matchEdge_;
+
+
+/**
+ * @return {boolean} Whether the user's browser is Firefox.
+ */
+goog.labs.userAgent.browser.isFirefox =
+    goog.labs.userAgent.browser.matchFirefox_;
+
+
+/**
+ * @return {boolean} Whether the user's browser is Safari.
+ */
+goog.labs.userAgent.browser.isSafari = goog.labs.userAgent.browser.matchSafari_;
+
+
+/**
+ * @return {boolean} Whether the user's browser is Coast (Opera's Webkit-based
+ *     iOS browser).
+ */
+goog.labs.userAgent.browser.isCoast = goog.labs.userAgent.browser.matchCoast_;
+
+
+/**
+ * @return {boolean} Whether the user's browser is iOS Webview.
+ */
+goog.labs.userAgent.browser.isIosWebview =
+    goog.labs.userAgent.browser.matchIosWebview_;
+
+
+/**
+ * @return {boolean} Whether the user's browser is Chrome.
+ */
+goog.labs.userAgent.browser.isChrome = goog.labs.userAgent.browser.matchChrome_;
+
+
+/**
+ * @return {boolean} Whether the user's browser is the Android browser.
+ */
+goog.labs.userAgent.browser.isAndroidBrowser =
+    goog.labs.userAgent.browser.matchAndroidBrowser_;
+
+
+/**
+ * For more information, see:
+ * http://docs.aws.amazon.com/silk/latest/developerguide/user-agent.html
+ * @return {boolean} Whether the user's browser is Silk.
+ */
+goog.labs.userAgent.browser.isSilk = function() {
+  return goog.labs.userAgent.util.matchUserAgent('Silk');
+};
+
+
+/**
+ * @return {string} The browser version or empty string if version cannot be
+ *     determined. Note that for Internet Explorer, this returns the version of
+ *     the browser, not the version of the rendering engine. (IE 8 in
+ *     compatibility mode will return 8.0 rather than 7.0. To determine the
+ *     rendering engine version, look at document.documentMode instead. See
+ *     http://msdn.microsoft.com/en-us/library/cc196988(v=vs.85).aspx for more
+ *     details.)
+ */
+goog.labs.userAgent.browser.getVersion = function() {
+  var userAgentString = goog.labs.userAgent.util.getUserAgent();
+  // Special case IE since IE's version is inside the parenthesis and
+  // without the '/'.
+  if (goog.labs.userAgent.browser.isIE()) {
+    return goog.labs.userAgent.browser.getIEVersion_(userAgentString);
+  }
+
+  var versionTuples =
+      goog.labs.userAgent.util.extractVersionTuples(userAgentString);
+
+  // Construct a map for easy lookup.
+  var versionMap = {};
+  goog.array.forEach(versionTuples, function(tuple) {
+    // Note that the tuple is of length three, but we only care about the
+    // first two.
+    var key = tuple[0];
+    var value = tuple[1];
+    versionMap[key] = value;
+  });
+
+  var versionMapHasKey = goog.partial(goog.object.containsKey, versionMap);
+
+  // Gives the value with the first key it finds, otherwise empty string.
+  function lookUpValueWithKeys(keys) {
+    var key = goog.array.find(keys, versionMapHasKey);
+    return versionMap[key] || '';
+  }
+
+  // Check Opera before Chrome since Opera 15+ has "Chrome" in the string.
+  // See
+  // http://my.opera.com/ODIN/blog/2013/07/15/opera-user-agent-strings-opera-15-and-beyond
+  if (goog.labs.userAgent.browser.isOpera()) {
+    // Opera 10 has Version/10.0 but Opera/9.8, so look for "Version" first.
+    // Opera uses 'OPR' for more recent UAs.
+    return lookUpValueWithKeys(['Version', 'Opera']);
+  }
+
+  // Check Edge before Chrome since it has Chrome in the string.
+  if (goog.labs.userAgent.browser.isEdge()) {
+    return lookUpValueWithKeys(['Edge']);
+  }
+
+  if (goog.labs.userAgent.browser.isChrome()) {
+    return lookUpValueWithKeys(['Chrome', 'CriOS']);
+  }
+
+  // Usually products browser versions are in the third tuple after "Mozilla"
+  // and the engine.
+  var tuple = versionTuples[2];
+  return tuple && tuple[1] || '';
+};
+
+
+/**
+ * @param {string|number} version The version to check.
+ * @return {boolean} Whether the browser version is higher or the same as the
+ *     given version.
+ */
+goog.labs.userAgent.browser.isVersionOrHigher = function(version) {
+  return goog.string.compareVersions(
+             goog.labs.userAgent.browser.getVersion(), version) >= 0;
+};
+
+
+/**
+ * Determines IE version. More information:
+ * http://msdn.microsoft.com/en-us/library/ie/bg182625(v=vs.85).aspx#uaString
+ * http://msdn.microsoft.com/en-us/library/hh869301(v=vs.85).aspx
+ * http://blogs.msdn.com/b/ie/archive/2010/03/23/introducing-ie9-s-user-agent-string.aspx
+ * http://blogs.msdn.com/b/ie/archive/2009/01/09/the-internet-explorer-8-user-agent-string-updated-edition.aspx
+ *
+ * @param {string} userAgent the User-Agent.
+ * @return {string}
+ * @private
+ */
+goog.labs.userAgent.browser.getIEVersion_ = function(userAgent) {
+  // IE11 may identify itself as MSIE 9.0 or MSIE 10.0 due to an IE 11 upgrade
+  // bug. Example UA:
+  // Mozilla/5.0 (MSIE 9.0; Windows NT 6.1; WOW64; Trident/7.0; rv:11.0)
+  // like Gecko.
+  // See http://www.whatismybrowser.com/developers/unknown-user-agent-fragments.
+  var rv = /rv: *([\d\.]*)/.exec(userAgent);
+  if (rv && rv[1]) {
+    return rv[1];
+  }
+
+  var version = '';
+  var msie = /MSIE +([\d\.]+)/.exec(userAgent);
+  if (msie && msie[1]) {
+    // IE in compatibility mode usually identifies itself as MSIE 7.0; in this
+    // case, use the Trident version to determine the version of IE. For more
+    // details, see the links above.
+    var tridentVersion = /Trident\/(\d.\d)/.exec(userAgent);
+    if (msie[1] == '7.0') {
+      if (tridentVersion && tridentVersion[1]) {
+        switch (tridentVersion[1]) {
+          case '4.0':
+            version = '8.0';
+            break;
+          case '5.0':
+            version = '9.0';
+            break;
+          case '6.0':
+            version = '10.0';
+            break;
+          case '7.0':
+            version = '11.0';
+            break;
+        }
+      } else {
+        version = '7.0';
+      }
+    } else {
+      version = msie[1];
+    }
+  }
+  return version;
+};
+
+// Copyright 2013 The Closure Library Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS-IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+/**
+ * @fileoverview Closure user agent detection.
+ * @see http://en.wikipedia.org/wiki/User_agent
+ * For more information on browser brand, platform, or device see the other
+ * sub-namespaces in goog.labs.userAgent (browser, platform, and device).
+ *
+ */
+
+goog.provide('goog.labs.userAgent.engine');
+
+goog.require('goog.array');
+goog.require('goog.labs.userAgent.util');
+goog.require('goog.string');
+
+
+/**
+ * @return {boolean} Whether the rendering engine is Presto.
+ */
+goog.labs.userAgent.engine.isPresto = function() {
+  return goog.labs.userAgent.util.matchUserAgent('Presto');
+};
+
+
+/**
+ * @return {boolean} Whether the rendering engine is Trident.
+ */
+goog.labs.userAgent.engine.isTrident = function() {
+  // IE only started including the Trident token in IE8.
+  return goog.labs.userAgent.util.matchUserAgent('Trident') ||
+      goog.labs.userAgent.util.matchUserAgent('MSIE');
+};
+
+
+/**
+ * @return {boolean} Whether the rendering engine is Edge.
+ */
+goog.labs.userAgent.engine.isEdge = function() {
+  return goog.labs.userAgent.util.matchUserAgent('Edge');
+};
+
+
+/**
+ * @return {boolean} Whether the rendering engine is WebKit.
+ */
+goog.labs.userAgent.engine.isWebKit = function() {
+  return goog.labs.userAgent.util.matchUserAgentIgnoreCase('WebKit') &&
+      !goog.labs.userAgent.engine.isEdge();
+};
+
+
+/**
+ * @return {boolean} Whether the rendering engine is Gecko.
+ */
+goog.labs.userAgent.engine.isGecko = function() {
+  return goog.labs.userAgent.util.matchUserAgent('Gecko') &&
+      !goog.labs.userAgent.engine.isWebKit() &&
+      !goog.labs.userAgent.engine.isTrident() &&
+      !goog.labs.userAgent.engine.isEdge();
+};
+
+
+/**
+ * @return {string} The rendering engine's version or empty string if version
+ *     can't be determined.
+ */
+goog.labs.userAgent.engine.getVersion = function() {
+  var userAgentString = goog.labs.userAgent.util.getUserAgent();
+  if (userAgentString) {
+    var tuples = goog.labs.userAgent.util.extractVersionTuples(userAgentString);
+
+    var engineTuple = goog.labs.userAgent.engine.getEngineTuple_(tuples);
+    if (engineTuple) {
+      // In Gecko, the version string is either in the browser info or the
+      // Firefox version.  See Gecko user agent string reference:
+      // http://goo.gl/mULqa
+      if (engineTuple[0] == 'Gecko') {
+        return goog.labs.userAgent.engine.getVersionForKey_(tuples, 'Firefox');
+      }
+
+      return engineTuple[1];
+    }
+
+    // MSIE has only one version identifier, and the Trident version is
+    // specified in the parenthetical. IE Edge is covered in the engine tuple
+    // detection.
+    var browserTuple = tuples[0];
+    var info;
+    if (browserTuple && (info = browserTuple[2])) {
+      var match = /Trident\/([^\s;]+)/.exec(info);
+      if (match) {
+        return match[1];
+      }
+    }
+  }
+  return '';
+};
+
+
+/**
+ * @param {!Array<!Array<string>>} tuples Extracted version tuples.
+ * @return {!Array<string>|undefined} The engine tuple or undefined if not
+ *     found.
+ * @private
+ */
+goog.labs.userAgent.engine.getEngineTuple_ = function(tuples) {
+  if (!goog.labs.userAgent.engine.isEdge()) {
+    return tuples[1];
+  }
+  for (var i = 0; i < tuples.length; i++) {
+    var tuple = tuples[i];
+    if (tuple[0] == 'Edge') {
+      return tuple;
+    }
+  }
+};
+
+
+/**
+ * @param {string|number} version The version to check.
+ * @return {boolean} Whether the rendering engine version is higher or the same
+ *     as the given version.
+ */
+goog.labs.userAgent.engine.isVersionOrHigher = function(version) {
+  return goog.string.compareVersions(
+             goog.labs.userAgent.engine.getVersion(), version) >= 0;
+};
+
+
+/**
+ * @param {!Array<!Array<string>>} tuples Version tuples.
+ * @param {string} key The key to look for.
+ * @return {string} The version string of the given key, if present.
+ *     Otherwise, the empty string.
+ * @private
+ */
+goog.labs.userAgent.engine.getVersionForKey_ = function(tuples, key) {
+  // TODO(nnaze): Move to util if useful elsewhere.
+
+  var pair = goog.array.find(tuples, function(pair) { return key == pair[0]; });
+
+  return pair && pair[1] || '';
+};
+
+// Copyright 2013 The Closure Library Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS-IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+/**
+ * @fileoverview Closure user agent platform detection.
+ * @see <a href="http://www.useragentstring.com/">User agent strings</a>
+ * For more information on browser brand, rendering engine, or device see the
+ * other sub-namespaces in goog.labs.userAgent (browser, engine, and device
+ * respectively).
+ *
+ */
+
+goog.provide('goog.labs.userAgent.platform');
+
+goog.require('goog.labs.userAgent.util');
+goog.require('goog.string');
+
+
+/**
+ * @return {boolean} Whether the platform is Android.
+ */
+goog.labs.userAgent.platform.isAndroid = function() {
+  return goog.labs.userAgent.util.matchUserAgent('Android');
+};
+
+
+/**
+ * @return {boolean} Whether the platform is iPod.
+ */
+goog.labs.userAgent.platform.isIpod = function() {
+  return goog.labs.userAgent.util.matchUserAgent('iPod');
+};
+
+
+/**
+ * @return {boolean} Whether the platform is iPhone.
+ */
+goog.labs.userAgent.platform.isIphone = function() {
+  return goog.labs.userAgent.util.matchUserAgent('iPhone') &&
+      !goog.labs.userAgent.util.matchUserAgent('iPod') &&
+      !goog.labs.userAgent.util.matchUserAgent('iPad');
+};
+
+
+/**
+ * @return {boolean} Whether the platform is iPad.
+ */
+goog.labs.userAgent.platform.isIpad = function() {
+  return goog.labs.userAgent.util.matchUserAgent('iPad');
+};
+
+
+/**
+ * @return {boolean} Whether the platform is iOS.
+ */
+goog.labs.userAgent.platform.isIos = function() {
+  return goog.labs.userAgent.platform.isIphone() ||
+      goog.labs.userAgent.platform.isIpad() ||
+      goog.labs.userAgent.platform.isIpod();
+};
+
+
+/**
+ * @return {boolean} Whether the platform is Mac.
+ */
+goog.labs.userAgent.platform.isMacintosh = function() {
+  return goog.labs.userAgent.util.matchUserAgent('Macintosh');
+};
+
+
+/**
+ * Note: ChromeOS is not considered to be Linux as it does not report itself
+ * as Linux in the user agent string.
+ * @return {boolean} Whether the platform is Linux.
+ */
+goog.labs.userAgent.platform.isLinux = function() {
+  return goog.labs.userAgent.util.matchUserAgent('Linux');
+};
+
+
+/**
+ * @return {boolean} Whether the platform is Windows.
+ */
+goog.labs.userAgent.platform.isWindows = function() {
+  return goog.labs.userAgent.util.matchUserAgent('Windows');
+};
+
+
+/**
+ * @return {boolean} Whether the platform is ChromeOS.
+ */
+goog.labs.userAgent.platform.isChromeOS = function() {
+  return goog.labs.userAgent.util.matchUserAgent('CrOS');
+};
+
+
+/**
+ * The version of the platform. We only determine the version for Windows,
+ * Mac, and Chrome OS. It doesn't make much sense on Linux. For Windows, we only
+ * look at the NT version. Non-NT-based versions (e.g. 95, 98, etc.) are given
+ * version 0.0.
+ *
+ * @return {string} The platform version or empty string if version cannot be
+ *     determined.
+ */
+goog.labs.userAgent.platform.getVersion = function() {
+  var userAgentString = goog.labs.userAgent.util.getUserAgent();
+  var version = '', re;
+  if (goog.labs.userAgent.platform.isWindows()) {
+    re = /Windows (?:NT|Phone) ([0-9.]+)/;
+    var match = re.exec(userAgentString);
+    if (match) {
+      version = match[1];
+    } else {
+      version = '0.0';
+    }
+  } else if (goog.labs.userAgent.platform.isIos()) {
+    re = /(?:iPhone|iPod|iPad|CPU)\s+OS\s+(\S+)/;
+    var match = re.exec(userAgentString);
+    // Report the version as x.y.z and not x_y_z
+    version = match && match[1].replace(/_/g, '.');
+  } else if (goog.labs.userAgent.platform.isMacintosh()) {
+    re = /Mac OS X ([0-9_.]+)/;
+    var match = re.exec(userAgentString);
+    // Note: some old versions of Camino do not report an OSX version.
+    // Default to 10.
+    version = match ? match[1].replace(/_/g, '.') : '10';
+  } else if (goog.labs.userAgent.platform.isAndroid()) {
+    re = /Android\s+([^\);]+)(\)|;)/;
+    var match = re.exec(userAgentString);
+    version = match && match[1];
+  } else if (goog.labs.userAgent.platform.isChromeOS()) {
+    re = /(?:CrOS\s+(?:i686|x86_64)\s+([0-9.]+))/;
+    var match = re.exec(userAgentString);
+    version = match && match[1];
+  }
+  return version || '';
+};
+
+
+/**
+ * @param {string|number} version The version to check.
+ * @return {boolean} Whether the browser version is higher or the same as the
+ *     given version.
+ */
+goog.labs.userAgent.platform.isVersionOrHigher = function(version) {
+  return goog.string.compareVersions(
+             goog.labs.userAgent.platform.getVersion(), version) >= 0;
+};
+
+// Copyright 2006 The Closure Library Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS-IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+/**
+ * @fileoverview Rendering engine detection.
+ * @see <a href="http://www.useragentstring.com/">User agent strings</a>
+ * For information on the browser brand (such as Safari versus Chrome), see
+ * goog.userAgent.product.
+ * @author arv@google.com (Erik Arvidsson)
+ * @see ../demos/useragent.html
+ */
+
+goog.provide('goog.userAgent');
+
+goog.require('goog.labs.userAgent.browser');
+goog.require('goog.labs.userAgent.engine');
+goog.require('goog.labs.userAgent.platform');
+goog.require('goog.labs.userAgent.util');
+goog.require('goog.string');
+
+
+/**
+ * @define {boolean} Whether we know at compile-time that the browser is IE.
+ */
+goog.define('goog.userAgent.ASSUME_IE', false);
+
+
+/**
+ * @define {boolean} Whether we know at compile-time that the browser is EDGE.
+ */
+goog.define('goog.userAgent.ASSUME_EDGE', false);
+
+
+/**
+ * @define {boolean} Whether we know at compile-time that the browser is GECKO.
+ */
+goog.define('goog.userAgent.ASSUME_GECKO', false);
+
+
+/**
+ * @define {boolean} Whether we know at compile-time that the browser is WEBKIT.
+ */
+goog.define('goog.userAgent.ASSUME_WEBKIT', false);
+
+
+/**
+ * @define {boolean} Whether we know at compile-time that the browser is a
+ *     mobile device running WebKit e.g. iPhone or Android.
+ */
+goog.define('goog.userAgent.ASSUME_MOBILE_WEBKIT', false);
+
+
+/**
+ * @define {boolean} Whether we know at compile-time that the browser is OPERA.
+ */
+goog.define('goog.userAgent.ASSUME_OPERA', false);
+
+
+/**
+ * @define {boolean} Whether the
+ *     {@code goog.userAgent.isVersionOrHigher}
+ *     function will return true for any version.
+ */
+goog.define('goog.userAgent.ASSUME_ANY_VERSION', false);
+
+
+/**
+ * Whether we know the browser engine at compile-time.
+ * @type {boolean}
+ * @private
+ */
+goog.userAgent.BROWSER_KNOWN_ = goog.userAgent.ASSUME_IE ||
+    goog.userAgent.ASSUME_EDGE || goog.userAgent.ASSUME_GECKO ||
+    goog.userAgent.ASSUME_MOBILE_WEBKIT || goog.userAgent.ASSUME_WEBKIT ||
+    goog.userAgent.ASSUME_OPERA;
+
+
+/**
+ * Returns the userAgent string for the current browser.
+ *
+ * @return {string} The userAgent string.
+ */
+goog.userAgent.getUserAgentString = function() {
+  return goog.labs.userAgent.util.getUserAgent();
+};
+
+
+/**
+ * TODO(nnaze): Change type to "Navigator" and update compilation targets.
+ * @return {Object} The native navigator object.
+ */
+goog.userAgent.getNavigator = function() {
+  // Need a local navigator reference instead of using the global one,
+  // to avoid the rare case where they reference different objects.
+  // (in a WorkerPool, for example).
+  return goog.global['navigator'] || null;
+};
+
+
+/**
+ * Whether the user agent is Opera.
+ * @type {boolean}
+ */
+goog.userAgent.OPERA = goog.userAgent.BROWSER_KNOWN_ ?
+    goog.userAgent.ASSUME_OPERA :
+    goog.labs.userAgent.browser.isOpera();
+
+
+/**
+ * Whether the user agent is Internet Explorer.
+ * @type {boolean}
+ */
+goog.userAgent.IE = goog.userAgent.BROWSER_KNOWN_ ?
+    goog.userAgent.ASSUME_IE :
+    goog.labs.userAgent.browser.isIE();
+
+
+/**
+ * Whether the user agent is Microsoft Edge.
+ * @type {boolean}
+ */
+goog.userAgent.EDGE = goog.userAgent.BROWSER_KNOWN_ ?
+    goog.userAgent.ASSUME_EDGE :
+    goog.labs.userAgent.engine.isEdge();
+
+
+/**
+ * Whether the user agent is MS Internet Explorer or MS Edge.
+ * @type {boolean}
+ */
+goog.userAgent.EDGE_OR_IE = goog.userAgent.EDGE || goog.userAgent.IE;
+
+
+/**
+ * Whether the user agent is Gecko. Gecko is the rendering engine used by
+ * Mozilla, Firefox, and others.
+ * @type {boolean}
+ */
+goog.userAgent.GECKO = goog.userAgent.BROWSER_KNOWN_ ?
+    goog.userAgent.ASSUME_GECKO :
+    goog.labs.userAgent.engine.isGecko();
+
+
+/**
+ * Whether the user agent is WebKit. WebKit is the rendering engine that
+ * Safari, Android and others use.
+ * @type {boolean}
+ */
+goog.userAgent.WEBKIT = goog.userAgent.BROWSER_KNOWN_ ?
+    goog.userAgent.ASSUME_WEBKIT || goog.userAgent.ASSUME_MOBILE_WEBKIT :
+    goog.labs.userAgent.engine.isWebKit();
+
+
+/**
+ * Whether the user agent is running on a mobile device.
+ *
+ * This is a separate function so that the logic can be tested.
+ *
+ * TODO(nnaze): Investigate swapping in goog.labs.userAgent.device.isMobile().
+ *
+ * @return {boolean} Whether the user agent is running on a mobile device.
+ * @private
+ */
+goog.userAgent.isMobile_ = function() {
+  return goog.userAgent.WEBKIT &&
+      goog.labs.userAgent.util.matchUserAgent('Mobile');
+};
+
+
+/**
+ * Whether the user agent is running on a mobile device.
+ *
+ * TODO(nnaze): Consider deprecating MOBILE when labs.userAgent
+ *   is promoted as the gecko/webkit logic is likely inaccurate.
+ *
+ * @type {boolean}
+ */
+goog.userAgent.MOBILE =
+    goog.userAgent.ASSUME_MOBILE_WEBKIT || goog.userAgent.isMobile_();
+
+
+/**
+ * Used while transitioning code to use WEBKIT instead.
+ * @type {boolean}
+ * @deprecated Use {@link goog.userAgent.product.SAFARI} instead.
+ * TODO(nicksantos): Delete this from goog.userAgent.
+ */
+goog.userAgent.SAFARI = goog.userAgent.WEBKIT;
+
+
+/**
+ * @return {string} the platform (operating system) the user agent is running
+ *     on. Default to empty string because navigator.platform may not be defined
+ *     (on Rhino, for example).
+ * @private
+ */
+goog.userAgent.determinePlatform_ = function() {
+  var navigator = goog.userAgent.getNavigator();
+  return navigator && navigator.platform || '';
+};
+
+
+/**
+ * The platform (operating system) the user agent is running on. Default to
+ * empty string because navigator.platform may not be defined (on Rhino, for
+ * example).
+ * @type {string}
+ */
+goog.userAgent.PLATFORM = goog.userAgent.determinePlatform_();
+
+
+/**
+ * @define {boolean} Whether the user agent is running on a Macintosh operating
+ *     system.
+ */
+goog.define('goog.userAgent.ASSUME_MAC', false);
+
+
+/**
+ * @define {boolean} Whether the user agent is running on a Windows operating
+ *     system.
+ */
+goog.define('goog.userAgent.ASSUME_WINDOWS', false);
+
+
+/**
+ * @define {boolean} Whether the user agent is running on a Linux operating
+ *     system.
+ */
+goog.define('goog.userAgent.ASSUME_LINUX', false);
+
+
+/**
+ * @define {boolean} Whether the user agent is running on a X11 windowing
+ *     system.
+ */
+goog.define('goog.userAgent.ASSUME_X11', false);
+
+
+/**
+ * @define {boolean} Whether the user agent is running on Android.
+ */
+goog.define('goog.userAgent.ASSUME_ANDROID', false);
+
+
+/**
+ * @define {boolean} Whether the user agent is running on an iPhone.
+ */
+goog.define('goog.userAgent.ASSUME_IPHONE', false);
+
+
+/**
+ * @define {boolean} Whether the user agent is running on an iPad.
+ */
+goog.define('goog.userAgent.ASSUME_IPAD', false);
+
+
+/**
+ * @define {boolean} Whether the user agent is running on an iPod.
+ */
+goog.define('goog.userAgent.ASSUME_IPOD', false);
+
+
+/**
+ * @type {boolean}
+ * @private
+ */
+goog.userAgent.PLATFORM_KNOWN_ = goog.userAgent.ASSUME_MAC ||
+    goog.userAgent.ASSUME_WINDOWS || goog.userAgent.ASSUME_LINUX ||
+    goog.userAgent.ASSUME_X11 || goog.userAgent.ASSUME_ANDROID ||
+    goog.userAgent.ASSUME_IPHONE || goog.userAgent.ASSUME_IPAD ||
+    goog.userAgent.ASSUME_IPOD;
+
+
+/**
+ * Whether the user agent is running on a Macintosh operating system.
+ * @type {boolean}
+ */
+goog.userAgent.MAC = goog.userAgent.PLATFORM_KNOWN_ ?
+    goog.userAgent.ASSUME_MAC :
+    goog.labs.userAgent.platform.isMacintosh();
+
+
+/**
+ * Whether the user agent is running on a Windows operating system.
+ * @type {boolean}
+ */
+goog.userAgent.WINDOWS = goog.userAgent.PLATFORM_KNOWN_ ?
+    goog.userAgent.ASSUME_WINDOWS :
+    goog.labs.userAgent.platform.isWindows();
+
+
+/**
+ * Whether the user agent is Linux per the legacy behavior of
+ * goog.userAgent.LINUX, which considered ChromeOS to also be
+ * Linux.
+ * @return {boolean}
+ * @private
+ */
+goog.userAgent.isLegacyLinux_ = function() {
+  return goog.labs.userAgent.platform.isLinux() ||
+      goog.labs.userAgent.platform.isChromeOS();
+};
+
+
+/**
+ * Whether the user agent is running on a Linux operating system.
+ *
+ * Note that goog.userAgent.LINUX considers ChromeOS to be Linux,
+ * while goog.labs.userAgent.platform considers ChromeOS and
+ * Linux to be different OSes.
+ *
+ * @type {boolean}
+ */
+goog.userAgent.LINUX = goog.userAgent.PLATFORM_KNOWN_ ?
+    goog.userAgent.ASSUME_LINUX :
+    goog.userAgent.isLegacyLinux_();
+
+
+/**
+ * @return {boolean} Whether the user agent is an X11 windowing system.
+ * @private
+ */
+goog.userAgent.isX11_ = function() {
+  var navigator = goog.userAgent.getNavigator();
+  return !!navigator &&
+      goog.string.contains(navigator['appVersion'] || '', 'X11');
+};
+
+
+/**
+ * Whether the user agent is running on a X11 windowing system.
+ * @type {boolean}
+ */
+goog.userAgent.X11 = goog.userAgent.PLATFORM_KNOWN_ ?
+    goog.userAgent.ASSUME_X11 :
+    goog.userAgent.isX11_();
+
+
+/**
+ * Whether the user agent is running on Android.
+ * @type {boolean}
+ */
+goog.userAgent.ANDROID = goog.userAgent.PLATFORM_KNOWN_ ?
+    goog.userAgent.ASSUME_ANDROID :
+    goog.labs.userAgent.platform.isAndroid();
+
+
+/**
+ * Whether the user agent is running on an iPhone.
+ * @type {boolean}
+ */
+goog.userAgent.IPHONE = goog.userAgent.PLATFORM_KNOWN_ ?
+    goog.userAgent.ASSUME_IPHONE :
+    goog.labs.userAgent.platform.isIphone();
+
+
+/**
+ * Whether the user agent is running on an iPad.
+ * @type {boolean}
+ */
+goog.userAgent.IPAD = goog.userAgent.PLATFORM_KNOWN_ ?
+    goog.userAgent.ASSUME_IPAD :
+    goog.labs.userAgent.platform.isIpad();
+
+
+/**
+ * Whether the user agent is running on an iPod.
+ * @type {boolean}
+ */
+goog.userAgent.IPOD = goog.userAgent.PLATFORM_KNOWN_ ?
+    goog.userAgent.ASSUME_IPOD :
+    goog.labs.userAgent.platform.isIpod();
+
+
+/**
+ * @return {string} The string that describes the version number of the user
+ *     agent.
+ * @private
+ */
+goog.userAgent.determineVersion_ = function() {
+  // All browsers have different ways to detect the version and they all have
+  // different naming schemes.
+  // version is a string rather than a number because it may contain 'b', 'a',
+  // and so on.
+  var version = '';
+  var arr = goog.userAgent.getVersionRegexResult_();
+  if (arr) {
+    version = arr ? arr[1] : '';
+  }
+
+  if (goog.userAgent.IE) {
+    // IE9 can be in document mode 9 but be reporting an inconsistent user agent
+    // version.  If it is identifying as a version lower than 9 we take the
+    // documentMode as the version instead.  IE8 has similar behavior.
+    // It is recommended to set the X-UA-Compatible header to ensure that IE9
+    // uses documentMode 9.
+    var docMode = goog.userAgent.getDocumentMode_();
+    if (docMode != null && docMode > parseFloat(version)) {
+      return String(docMode);
+    }
+  }
+
+  return version;
+};
+
+
+/**
+ * @return {?Array|undefined} The version regex matches from parsing the user
+ *     agent string. These regex statements must be executed inline so they can
+ *     be compiled out by the closure compiler with the rest of the useragent
+ *     detection logic when ASSUME_* is specified.
+ * @private
+ */
+goog.userAgent.getVersionRegexResult_ = function() {
+  var userAgent = goog.userAgent.getUserAgentString();
+  if (goog.userAgent.GECKO) {
+    return /rv\:([^\);]+)(\)|;)/.exec(userAgent);
+  }
+  if (goog.userAgent.EDGE) {
+    return /Edge\/([\d\.]+)/.exec(userAgent);
+  }
+  if (goog.userAgent.IE) {
+    return /\b(?:MSIE|rv)[: ]([^\);]+)(\)|;)/.exec(userAgent);
+  }
+  if (goog.userAgent.WEBKIT) {
+    // WebKit/125.4
+    return /WebKit\/(\S+)/.exec(userAgent);
+  }
+  if (goog.userAgent.OPERA) {
+    // If none of the above browsers were detected but the browser is Opera, the
+    // only string that is of interest is 'Version/<number>'.
+    return /(?:Version)[ \/]?(\S+)/.exec(userAgent);
+  }
+  return undefined;
+};
+
+
+/**
+ * @return {number|undefined} Returns the document mode (for testing).
+ * @private
+ */
+goog.userAgent.getDocumentMode_ = function() {
+  // NOTE(user): goog.userAgent may be used in context where there is no DOM.
+  var doc = goog.global['document'];
+  return doc ? doc['documentMode'] : undefined;
+};
+
+
+/**
+ * The version of the user agent. This is a string because it might contain
+ * 'b' (as in beta) as well as multiple dots.
+ * @type {string}
+ */
+goog.userAgent.VERSION = goog.userAgent.determineVersion_();
+
+
+/**
+ * Compares two version numbers.
+ *
+ * @param {string} v1 Version of first item.
+ * @param {string} v2 Version of second item.
+ *
+ * @return {number}  1 if first argument is higher
+ *                   0 if arguments are equal
+ *                  -1 if second argument is higher.
+ * @deprecated Use goog.string.compareVersions.
+ */
+goog.userAgent.compare = function(v1, v2) {
+  return goog.string.compareVersions(v1, v2);
+};
+
+
+/**
+ * Cache for {@link goog.userAgent.isVersionOrHigher}.
+ * Calls to compareVersions are surprisingly expensive and, as a browser's
+ * version number is unlikely to change during a session, we cache the results.
+ * @const
+ * @private
+ */
+goog.userAgent.isVersionOrHigherCache_ = {};
+
+
+/**
+ * Whether the user agent version is higher or the same as the given version.
+ * NOTE: When checking the version numbers for Firefox and Safari, be sure to
+ * use the engine's version, not the browser's version number.  For example,
+ * Firefox 3.0 corresponds to Gecko 1.9 and Safari 3.0 to Webkit 522.11.
+ * Opera and Internet Explorer versions match the product release number.<br>
+ * @see <a href="http://en.wikipedia.org/wiki/Safari_version_history">
+ *     Webkit</a>
+ * @see <a href="http://en.wikipedia.org/wiki/Gecko_engine">Gecko</a>
+ *
+ * @param {string|number} version The version to check.
+ * @return {boolean} Whether the user agent version is higher or the same as
+ *     the given version.
+ */
+goog.userAgent.isVersionOrHigher = function(version) {
+  return goog.userAgent.ASSUME_ANY_VERSION ||
+      goog.userAgent.isVersionOrHigherCache_[version] ||
+      (goog.userAgent.isVersionOrHigherCache_[version] =
+           goog.string.compareVersions(goog.userAgent.VERSION, version) >= 0);
+};
+
+
+/**
+ * Deprecated alias to {@code goog.userAgent.isVersionOrHigher}.
+ * @param {string|number} version The version to check.
+ * @return {boolean} Whether the user agent version is higher or the same as
+ *     the given version.
+ * @deprecated Use goog.userAgent.isVersionOrHigher().
+ */
+goog.userAgent.isVersion = goog.userAgent.isVersionOrHigher;
+
+
+/**
+ * Whether the IE effective document mode is higher or the same as the given
+ * document mode version.
+ * NOTE: Only for IE, return false for another browser.
+ *
+ * @param {number} documentMode The document mode version to check.
+ * @return {boolean} Whether the IE effective document mode is higher or the
+ *     same as the given version.
+ */
+goog.userAgent.isDocumentModeOrHigher = function(documentMode) {
+  return Number(goog.userAgent.DOCUMENT_MODE) >= documentMode;
+};
+
+
+/**
+ * Deprecated alias to {@code goog.userAgent.isDocumentModeOrHigher}.
+ * @param {number} version The version to check.
+ * @return {boolean} Whether the IE effective document mode is higher or the
+ *      same as the given version.
+ * @deprecated Use goog.userAgent.isDocumentModeOrHigher().
+ */
+goog.userAgent.isDocumentMode = goog.userAgent.isDocumentModeOrHigher;
+
+
+/**
+ * For IE version < 7, documentMode is undefined, so attempt to use the
+ * CSS1Compat property to see if we are in standards mode. If we are in
+ * standards mode, treat the browser version as the document mode. Otherwise,
+ * IE is emulating version 5.
+ * @type {number|undefined}
+ * @const
+ */
+goog.userAgent.DOCUMENT_MODE = (function() {
+  var doc = goog.global['document'];
+  var mode = goog.userAgent.getDocumentMode_();
+  if (!doc || !goog.userAgent.IE) {
+    return undefined;
+  }
+  return mode || (doc['compatMode'] == 'CSS1Compat' ?
+                      parseInt(goog.userAgent.VERSION, 10) :
+                      5);
+})();
+
+goog.provide('ol.dom');
+
+goog.require('goog.asserts');
+goog.require('goog.userAgent');
+goog.require('goog.vec.Mat4');
+goog.require('ol');
+
+
+/**
+ * Create an html canvas element and returns its 2d context.
+ * @param {number=} opt_width Canvas width.
+ * @param {number=} opt_height Canvas height.
+ * @return {CanvasRenderingContext2D} The context.
+ */
+ol.dom.createCanvasContext2D = function(opt_width, opt_height) {
+  var canvas = document.createElement('CANVAS');
+  if (opt_width) {
+    canvas.width = opt_width;
+  }
+  if (opt_height) {
+    canvas.height = opt_height;
+  }
+  return canvas.getContext('2d');
+};
+
+
+/**
+ * Detect 2d transform.
+ * Adapted from http://stackoverflow.com/q/5661671/130442
+ * http://caniuse.com/#feat=transforms2d
+ * @return {boolean}
+ */
+ol.dom.canUseCssTransform = (function() {
+  var canUseCssTransform;
+  return function() {
+    if (canUseCssTransform === undefined) {
+      goog.asserts.assert(document.body,
+          'document.body should not be null');
+      goog.asserts.assert(ol.global.getComputedStyle,
+          'getComputedStyle is required (unsupported browser?)');
+
+      var el = document.createElement('P'),
+          has2d,
+          transforms = {
+            'webkitTransform': '-webkit-transform',
+            'OTransform': '-o-transform',
+            'msTransform': '-ms-transform',
+            'MozTransform': '-moz-transform',
+            'transform': 'transform'
+          };
+      document.body.appendChild(el);
+      for (var t in transforms) {
+        if (t in el.style) {
+          el.style[t] = 'translate(1px,1px)';
+          has2d = ol.global.getComputedStyle(el).getPropertyValue(
+              transforms[t]);
+        }
+      }
+      document.body.removeChild(el);
+
+      canUseCssTransform = (has2d && has2d !== 'none');
+    }
+    return canUseCssTransform;
+  };
+}());
+
+
+/**
+ * Detect 3d transform.
+ * Adapted from http://stackoverflow.com/q/5661671/130442
+ * http://caniuse.com/#feat=transforms3d
+ * @return {boolean}
+ */
+ol.dom.canUseCssTransform3D = (function() {
+  var canUseCssTransform3D;
+  return function() {
+    if (canUseCssTransform3D === undefined) {
+      goog.asserts.assert(document.body,
+          'document.body should not be null');
+      goog.asserts.assert(ol.global.getComputedStyle,
+          'getComputedStyle is required (unsupported browser?)');
+
+      var el = document.createElement('P'),
+          has3d,
+          transforms = {
+            'webkitTransform': '-webkit-transform',
+            'OTransform': '-o-transform',
+            'msTransform': '-ms-transform',
+            'MozTransform': '-moz-transform',
+            'transform': 'transform'
+          };
+      document.body.appendChild(el);
+      for (var t in transforms) {
+        if (t in el.style) {
+          el.style[t] = 'translate3d(1px,1px,1px)';
+          has3d = ol.global.getComputedStyle(el).getPropertyValue(
+              transforms[t]);
+        }
+      }
+      document.body.removeChild(el);
+
+      canUseCssTransform3D = (has3d && has3d !== 'none');
+    }
+    return canUseCssTransform3D;
+  };
+}());
+
+
+/**
+ * @param {Element} element Element.
+ * @param {string} value Value.
+ */
+ol.dom.setTransform = function(element, value) {
+  var style = element.style;
+  style.WebkitTransform = value;
+  style.MozTransform = value;
+  style.OTransform = value;
+  style.msTransform = value;
+  style.transform = value;
+
+  // IE 9+ seems to assume transform-origin: 100% 100%; for some unknown reason
+  if (goog.userAgent.IE && goog.userAgent.isVersionOrHigher('9.0')) {
+    element.style.transformOrigin = '0 0';
+  }
+};
+
+
+/**
+ * @param {!Element} element Element.
+ * @param {goog.vec.Mat4.Number} transform Matrix.
+ * @param {number=} opt_precision Precision.
+ */
+ol.dom.transformElement2D = function(element, transform, opt_precision) {
+  // using matrix() causes gaps in Chrome and Firefox on Mac OS X, so prefer
+  // matrix3d()
+  var i;
+  if (ol.dom.canUseCssTransform3D()) {
+    var value3D;
+
+    if (opt_precision !== undefined) {
+      /** @type {Array.<string>} */
+      var strings3D = new Array(16);
+      for (i = 0; i < 16; ++i) {
+        strings3D[i] = transform[i].toFixed(opt_precision);
+      }
+      value3D = strings3D.join(',');
+    } else {
+      value3D = transform.join(',');
+    }
+    ol.dom.setTransform(element, 'matrix3d(' + value3D + ')');
+  } else if (ol.dom.canUseCssTransform()) {
+    /** @type {Array.<number>} */
+    var transform2D = [
+      goog.vec.Mat4.getElement(transform, 0, 0),
+      goog.vec.Mat4.getElement(transform, 1, 0),
+      goog.vec.Mat4.getElement(transform, 0, 1),
+      goog.vec.Mat4.getElement(transform, 1, 1),
+      goog.vec.Mat4.getElement(transform, 0, 3),
+      goog.vec.Mat4.getElement(transform, 1, 3)
+    ];
+    var value2D;
+    if (opt_precision !== undefined) {
+      /** @type {Array.<string>} */
+      var strings2D = new Array(6);
+      for (i = 0; i < 6; ++i) {
+        strings2D[i] = transform2D[i].toFixed(opt_precision);
+      }
+      value2D = strings2D.join(',');
+    } else {
+      value2D = transform2D.join(',');
+    }
+    ol.dom.setTransform(element, 'matrix(' + value2D + ')');
+  } else {
+    element.style.left =
+        Math.round(goog.vec.Mat4.getElement(transform, 0, 3)) + 'px';
+    element.style.top =
+        Math.round(goog.vec.Mat4.getElement(transform, 1, 3)) + 'px';
+
+    // TODO: Add scaling here. This isn't quite as simple as multiplying
+    // width/height, because that only changes the container size, not the
+    // content size.
+  }
+};
+
+
+/**
+ * Get the current computed width for the given element including margin,
+ * padding and border.
+ * Equivalent to jQuery's `$(el).outerWidth(true)`.
+ * @param {!Element} element Element.
+ * @return {number} The width.
+ */
+ol.dom.outerWidth = function(element) {
+  var width = element.offsetWidth;
+  var style = element.currentStyle || ol.global.getComputedStyle(element);
+  width += parseInt(style.marginLeft, 10) + parseInt(style.marginRight, 10);
+
+  return width;
+};
+
+
+/**
+ * Get the current computed height for the given element including margin,
+ * padding and border.
+ * Equivalent to jQuery's `$(el).outerHeight(true)`.
+ * @param {!Element} element Element.
+ * @return {number} The height.
+ */
+ol.dom.outerHeight = function(element) {
+  var height = element.offsetHeight;
+  var style = element.currentStyle || ol.global.getComputedStyle(element);
+  height += parseInt(style.marginTop, 10) + parseInt(style.marginBottom, 10);
+
+  return height;
+};
+
+/**
+ * @param {Node} newNode Node to replace old node
+ * @param {Node} oldNode The node to be replaced
+ */
+ol.dom.replaceNode = function(newNode, oldNode) {
+  var parent = oldNode.parentNode;
+  if (parent) {
+    parent.replaceChild(newNode, oldNode);
+  }
+};
+
+/**
+ * @param {Node} node The node to remove.
+ * @returns {Node} The node that was removed or null.
+ */
+ol.dom.removeNode = function(node) {
+  return node && node.parentNode ? node.parentNode.removeChild(node) : null;
+};
+
+/**
+ * @param {Node} node The node to remove the children from.
+ */
+ol.dom.removeChildren = function(node) {
+  while (node.lastChild) {
+    node.removeChild(node.lastChild);
+  }
+};
+
+goog.provide('ol.MapEvent');
+goog.provide('ol.MapEventType');
+
+goog.require('ol.events.Event');
+
+
+/**
+ * @enum {string}
+ */
+ol.MapEventType = {
+
+  /**
+   * Triggered after a map frame is rendered.
+   * @event ol.MapEvent#postrender
+   * @api
+   */
+  POSTRENDER: 'postrender',
+
+  /**
+   * Triggered after the map is moved.
+   * @event ol.MapEvent#moveend
+   * @api stable
+   */
+  MOVEEND: 'moveend'
+
+};
+
+
+/**
+ * @classdesc
+ * Events emitted as map events are instances of this type.
+ * See {@link ol.Map} for which events trigger a map event.
+ *
+ * @constructor
+ * @extends {ol.events.Event}
+ * @implements {oli.MapEvent}
+ * @param {string} type Event type.
+ * @param {ol.Map} map Map.
+ * @param {?olx.FrameState=} opt_frameState Frame state.
+ */
+ol.MapEvent = function(type, map, opt_frameState) {
+
+  ol.events.Event.call(this, type);
+
+  /**
+   * The map where the event occurred.
+   * @type {ol.Map}
+   * @api stable
+   */
+  this.map = map;
+
+  /**
+   * The frame state at the time of the event.
+   * @type {?olx.FrameState}
+   * @api
+   */
+  this.frameState = opt_frameState !== undefined ? opt_frameState : null;
+
+};
+ol.inherits(ol.MapEvent, ol.events.Event);
+
+goog.provide('ol.control.Control');
+
+goog.require('ol.events');
+goog.require('ol');
+goog.require('ol.MapEventType');
+goog.require('ol.Object');
+goog.require('ol.dom');
+
+
+/**
+ * @classdesc
+ * A control is a visible widget with a DOM element in a fixed position on the
+ * screen. They can involve user input (buttons), or be informational only;
+ * the position is determined using CSS. By default these are placed in the
+ * container with CSS class name `ol-overlaycontainer-stopevent`, but can use
+ * any outside DOM element.
+ *
+ * This is the base class for controls. You can use it for simple custom
+ * controls by creating the element with listeners, creating an instance:
+ * ```js
+ * var myControl = new ol.control.Control({element: myElement});
+ * ```
+ * and then adding this to the map.
+ *
+ * The main advantage of having this as a control rather than a simple separate
+ * DOM element is that preventing propagation is handled for you. Controls
+ * will also be `ol.Object`s in a `ol.Collection`, so you can use their
+ * methods.
+ *
+ * You can also extend this base for your own control class. See
+ * examples/custom-controls for an example of how to do this.
+ *
+ * @constructor
+ * @extends {ol.Object}
+ * @implements {oli.control.Control}
+ * @param {olx.control.ControlOptions} options Control options.
+ * @api stable
+ */
+ol.control.Control = function(options) {
+
+  ol.Object.call(this);
+
+  /**
+   * @protected
+   * @type {Element}
+   */
+  this.element = options.element ? options.element : null;
+
+  /**
+   * @private
+   * @type {Element}
+   */
+  this.target_ = null;
+
+  /**
+   * @private
+   * @type {ol.Map}
+   */
+  this.map_ = null;
+
+  /**
+   * @protected
+   * @type {!Array.<ol.EventsKey>}
+   */
+  this.listenerKeys = [];
+
+  /**
+   * @type {function(ol.MapEvent)}
+   */
+  this.render = options.render ? options.render : ol.nullFunction;
+
+  if (options.target) {
+    this.setTarget(options.target);
+  }
+
+};
+ol.inherits(ol.control.Control, ol.Object);
+
+
+/**
+ * @inheritDoc
+ */
+ol.control.Control.prototype.disposeInternal = function() {
+  ol.dom.removeNode(this.element);
+  ol.Object.prototype.disposeInternal.call(this);
+};
+
+
+/**
+ * Get the map associated with this control.
+ * @return {ol.Map} Map.
+ * @api stable
+ */
+ol.control.Control.prototype.getMap = function() {
+  return this.map_;
+};
+
+
+/**
+ * Remove the control from its current map and attach it to the new map.
+ * Subclasses may set up event handlers to get notified about changes to
+ * the map here.
+ * @param {ol.Map} map Map.
+ * @api stable
+ */
+ol.control.Control.prototype.setMap = function(map) {
+  if (this.map_) {
+    ol.dom.removeNode(this.element);
+  }
+  for (var i = 0, ii = this.listenerKeys.length; i < ii; ++i) {
+    ol.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 !== ol.nullFunction) {
+      this.listenerKeys.push(ol.events.listen(map,
+          ol.MapEventType.POSTRENDER, this.render, this));
+    }
+    map.render();
+  }
+};
+
+
+/**
+ * This function is used to set a target element for the control. It has no
+ * effect if it is called after the control has been added to the map (i.e.
+ * after `setMap` is called on the control). If no `target` is set in the
+ * options passed to the control constructor and if `setTarget` is not called
+ * then the control is added to the map's overlay container.
+ * @param {Element|string} target Target.
+ * @api
+ */
+ol.control.Control.prototype.setTarget = function(target) {
+  this.target_ = typeof target === 'string' ?
+    document.getElementById(target) :
+    target;
+};
+
+goog.provide('ol.css');
+
+
+/**
+ * The CSS class for hidden feature.
+ *
+ * @const
+ * @type {string}
+ */
+ol.css.CLASS_HIDDEN = 'ol-hidden';
+
+
+/**
+ * The CSS class that we'll give the DOM elements to have them unselectable.
+ *
+ * @const
+ * @type {string}
+ */
+ol.css.CLASS_UNSELECTABLE = 'ol-unselectable';
+
+
+/**
+ * The CSS class for unsupported feature.
+ *
+ * @const
+ * @type {string}
+ */
+ol.css.CLASS_UNSUPPORTED = 'ol-unsupported';
+
+
+/**
+ * The CSS class for controls.
+ *
+ * @const
+ * @type {string}
+ */
+ol.css.CLASS_CONTROL = 'ol-control';
+
+goog.provide('ol.structs.LRUCache');
+
+goog.require('goog.asserts');
+goog.require('ol.object');
+
+
+/**
+ * Implements a Least-Recently-Used cache where the keys do not conflict with
+ * Object's properties (e.g. 'hasOwnProperty' is not allowed as a key). Expiring
+ * items from the cache is the responsibility of the user.
+ * @constructor
+ * @struct
+ * @template T
+ */
+ol.structs.LRUCache = function() {
+
+  /**
+   * @private
+   * @type {number}
+   */
+  this.count_ = 0;
+
+  /**
+   * @private
+   * @type {!Object.<string, ol.LRUCacheEntry>}
+   */
+  this.entries_ = {};
+
+  /**
+   * @private
+   * @type {?ol.LRUCacheEntry}
+   */
+  this.oldest_ = null;
+
+  /**
+   * @private
+   * @type {?ol.LRUCacheEntry}
+   */
+  this.newest_ = null;
+
+};
+
+
+/**
+ * FIXME empty description for jsdoc
+ */
+ol.structs.LRUCache.prototype.assertValid = function() {
+  if (this.count_ === 0) {
+    goog.asserts.assert(ol.object.isEmpty(this.entries_),
+        'entries must be an empty object (count = 0)');
+    goog.asserts.assert(!this.oldest_,
+        'oldest must be null (count = 0)');
+    goog.asserts.assert(!this.newest_,
+        'newest must be null (count = 0)');
+  } else {
+    goog.asserts.assert(Object.keys(this.entries_).length == this.count_,
+        'number of entries matches count');
+    goog.asserts.assert(this.oldest_,
+        'we have an oldest entry');
+    goog.asserts.assert(!this.oldest_.older,
+        'no entry is older than oldest');
+    goog.asserts.assert(this.newest_,
+        'we have a newest entry');
+    goog.asserts.assert(!this.newest_.newer,
+        'no entry is newer than newest');
+    var i, entry;
+    var older = null;
+    i = 0;
+    for (entry = this.oldest_; entry; entry = entry.newer) {
+      goog.asserts.assert(entry.older === older,
+          'entry.older links to correct older');
+      older = entry;
+      ++i;
+    }
+    goog.asserts.assert(i == this.count_, 'iterated correct amount of times');
+    var newer = null;
+    i = 0;
+    for (entry = this.newest_; entry; entry = entry.older) {
+      goog.asserts.assert(entry.newer === newer,
+          'entry.newer links to correct newer');
+      newer = entry;
+      ++i;
+    }
+    goog.asserts.assert(i == this.count_, 'iterated correct amount of times');
+  }
+};
+
+
+/**
+ * FIXME empty description for jsdoc
+ */
+ol.structs.LRUCache.prototype.clear = function() {
+  this.count_ = 0;
+  this.entries_ = {};
+  this.oldest_ = null;
+  this.newest_ = null;
+};
+
+
+/**
+ * @param {string} key Key.
+ * @return {boolean} Contains key.
+ */
+ol.structs.LRUCache.prototype.containsKey = function(key) {
+  return this.entries_.hasOwnProperty(key);
+};
+
+
+/**
+ * @param {function(this: S, T, string, ol.structs.LRUCache): ?} f The function
+ *     to call for every entry from the oldest to the newer. This function takes
+ *     3 arguments (the entry value, the entry key and the LRUCache object).
+ *     The return value is ignored.
+ * @param {S=} opt_this The object to use as `this` in `f`.
+ * @template S
+ */
+ol.structs.LRUCache.prototype.forEach = function(f, opt_this) {
+  var entry = this.oldest_;
+  while (entry) {
+    f.call(opt_this, entry.value_, entry.key_, this);
+    entry = entry.newer;
+  }
+};
+
+
+/**
+ * @param {string} key Key.
+ * @return {T} Value.
+ */
+ol.structs.LRUCache.prototype.get = function(key) {
+  var entry = this.entries_[key];
+  goog.asserts.assert(entry !== undefined, 'an entry exists for key %s', key);
+  if (entry === this.newest_) {
+    return entry.value_;
+  } else if (entry === this.oldest_) {
+    this.oldest_ = /** @type {ol.LRUCacheEntry} */ (this.oldest_.newer);
+    this.oldest_.older = null;
+  } else {
+    entry.newer.older = entry.older;
+    entry.older.newer = entry.newer;
+  }
+  entry.newer = null;
+  entry.older = this.newest_;
+  this.newest_.newer = entry;
+  this.newest_ = entry;
+  return entry.value_;
+};
+
+
+/**
+ * @return {number} Count.
+ */
+ol.structs.LRUCache.prototype.getCount = function() {
+  return this.count_;
+};
+
+
+/**
+ * @return {Array.<string>} Keys.
+ */
+ol.structs.LRUCache.prototype.getKeys = function() {
+  var keys = new Array(this.count_);
+  var i = 0;
+  var entry;
+  for (entry = this.newest_; entry; entry = entry.older) {
+    keys[i++] = entry.key_;
+  }
+  goog.asserts.assert(i == this.count_, 'iterated correct number of times');
+  return keys;
+};
+
+
+/**
+ * @return {Array.<T>} Values.
+ */
+ol.structs.LRUCache.prototype.getValues = function() {
+  var values = new Array(this.count_);
+  var i = 0;
+  var entry;
+  for (entry = this.newest_; entry; entry = entry.older) {
+    values[i++] = entry.value_;
+  }
+  goog.asserts.assert(i == this.count_, 'iterated correct number of times');
+  return values;
+};
+
+
+/**
+ * @return {T} Last value.
+ */
+ol.structs.LRUCache.prototype.peekLast = function() {
+  goog.asserts.assert(this.oldest_, 'oldest must not be null');
+  return this.oldest_.value_;
+};
+
+
+/**
+ * @return {string} Last key.
+ */
+ol.structs.LRUCache.prototype.peekLastKey = function() {
+  goog.asserts.assert(this.oldest_, 'oldest must not be null');
+  return this.oldest_.key_;
+};
+
+
+/**
+ * @return {T} value Value.
+ */
+ol.structs.LRUCache.prototype.pop = function() {
+  goog.asserts.assert(this.oldest_, 'oldest must not be null');
+  goog.asserts.assert(this.newest_, 'newest must not be null');
+  var entry = this.oldest_;
+  goog.asserts.assert(entry.key_ in this.entries_,
+      'oldest is indexed in entries');
+  delete this.entries_[entry.key_];
+  if (entry.newer) {
+    entry.newer.older = null;
+  }
+  this.oldest_ = /** @type {ol.LRUCacheEntry} */ (entry.newer);
+  if (!this.oldest_) {
+    this.newest_ = null;
+  }
+  --this.count_;
+  return entry.value_;
+};
+
+
+/**
+ * @param {string} key Key.
+ * @param {T} value Value.
+ */
+ol.structs.LRUCache.prototype.replace = function(key, value) {
+  this.get(key);  // update `newest_`
+  this.entries_[key].value_ = value;
+};
+
+
+/**
+ * @param {string} key Key.
+ * @param {T} value Value.
+ */
+ol.structs.LRUCache.prototype.set = function(key, value) {
+  goog.asserts.assert(!(key in {}),
+      'key is not a standard property of objects (e.g. "__proto__")');
+  goog.asserts.assert(!(key in this.entries_),
+      'key is not used already');
+  var entry = /** @type {ol.LRUCacheEntry} */ ({
+    key_: key,
+    newer: null,
+    older: this.newest_,
+    value_: value
+  });
+  if (!this.newest_) {
+    this.oldest_ = entry;
+  } else {
+    this.newest_.newer = entry;
+  }
+  this.newest_ = entry;
+  this.entries_[key] = entry;
+  ++this.count_;
+};
+
+goog.provide('ol.tilecoord');
+
+goog.require('goog.asserts');
+goog.require('ol.extent');
+
+
+/**
+ * @enum {number}
+ */
+ol.QuadKeyCharCode = {
+  ZERO: '0'.charCodeAt(0),
+  ONE: '1'.charCodeAt(0),
+  TWO: '2'.charCodeAt(0),
+  THREE: '3'.charCodeAt(0)
+};
+
+
+/**
+ * @param {string} str String that follows pattern “z/x/y” where x, y and z are
+ *   numbers.
+ * @return {ol.TileCoord} Tile coord.
+ */
+ol.tilecoord.createFromString = function(str) {
+  var v = str.split('/');
+  goog.asserts.assert(v.length === 3,
+      'must provide a string in "z/x/y" format, got "%s"', str);
+  return v.map(function(e) {
+    return parseInt(e, 10);
+  });
+};
+
+
+/**
+ * @param {number} z Z.
+ * @param {number} x X.
+ * @param {number} y Y.
+ * @param {ol.TileCoord=} opt_tileCoord Tile coordinate.
+ * @return {ol.TileCoord} Tile coordinate.
+ */
+ol.tilecoord.createOrUpdate = function(z, x, y, opt_tileCoord) {
+  if (opt_tileCoord !== undefined) {
+    opt_tileCoord[0] = z;
+    opt_tileCoord[1] = x;
+    opt_tileCoord[2] = y;
+    return opt_tileCoord;
+  } else {
+    return [z, x, y];
+  }
+};
+
+
+/**
+ * @param {number} z Z.
+ * @param {number} x X.
+ * @param {number} y Y.
+ * @return {string} Key.
+ */
+ol.tilecoord.getKeyZXY = function(z, x, y) {
+  return z + '/' + x + '/' + y;
+};
+
+
+/**
+ * @param {ol.TileCoord} tileCoord Tile coord.
+ * @return {number} Hash.
+ */
+ol.tilecoord.hash = function(tileCoord) {
+  return (tileCoord[1] << tileCoord[0]) + tileCoord[2];
+};
+
+
+/**
+ * @param {ol.TileCoord} tileCoord Tile coord.
+ * @return {string} Quad key.
+ */
+ol.tilecoord.quadKey = function(tileCoord) {
+  var z = tileCoord[0];
+  var digits = new Array(z);
+  var mask = 1 << (z - 1);
+  var i, charCode;
+  for (i = 0; i < z; ++i) {
+    charCode = ol.QuadKeyCharCode.ZERO;
+    if (tileCoord[1] & mask) {
+      charCode += 1;
+    }
+    if (tileCoord[2] & mask) {
+      charCode += 2;
+    }
+    digits[i] = String.fromCharCode(charCode);
+    mask >>= 1;
+  }
+  return digits.join('');
+};
+
+
+/**
+ * @param {ol.TileCoord} tileCoord Tile coordinate.
+ * @param {ol.tilegrid.TileGrid} tileGrid Tile grid.
+ * @param {ol.proj.Projection} projection Projection.
+ * @return {ol.TileCoord} Tile coordinate.
+ */
+ol.tilecoord.wrapX = function(tileCoord, tileGrid, projection) {
+  var z = tileCoord[0];
+  var center = tileGrid.getTileCoordCenter(tileCoord);
+  var projectionExtent = ol.tilegrid.extentFromProjection(projection);
+  if (!ol.extent.containsCoordinate(projectionExtent, center)) {
+    var worldWidth = ol.extent.getWidth(projectionExtent);
+    var worldsAway = Math.ceil((projectionExtent[0] - center[0]) / worldWidth);
+    center[0] += worldWidth * worldsAway;
+    return tileGrid.getTileCoordForCoordAndZ(center, z);
+  } else {
+    return tileCoord;
+  }
+};
+
+
+/**
+ * @param {ol.TileCoord} tileCoord Tile coordinate.
+ * @param {!ol.tilegrid.TileGrid} tileGrid Tile grid.
+ * @return {boolean} Tile coordinate is within extent and zoom level range.
+ */
+ol.tilecoord.withinExtentAndZ = function(tileCoord, tileGrid) {
+  var z = tileCoord[0];
+  var x = tileCoord[1];
+  var y = tileCoord[2];
+
+  if (tileGrid.getMinZoom() > z || z > tileGrid.getMaxZoom()) {
+    return false;
+  }
+  var extent = tileGrid.getExtent();
+  var tileRange;
+  if (!extent) {
+    tileRange = tileGrid.getFullTileRange(z);
+  } else {
+    tileRange = tileGrid.getTileRangeForExtentAndZ(extent, z);
+  }
+  if (!tileRange) {
+    return true;
+  } else {
+    return tileRange.containsXY(x, y);
+  }
+};
+
+goog.provide('ol.TileCache');
+
+goog.require('ol.TileRange');
+goog.require('ol.structs.LRUCache');
+goog.require('ol.tilecoord');
+
+
+/**
+ * @constructor
+ * @extends {ol.structs.LRUCache.<ol.Tile>}
+ * @param {number=} opt_highWaterMark High water mark.
+ * @struct
+ */
+ol.TileCache = function(opt_highWaterMark) {
+
+  ol.structs.LRUCache.call(this);
+
+  /**
+   * @private
+   * @type {number}
+   */
+  this.highWaterMark_ = opt_highWaterMark !== undefined ? opt_highWaterMark : 2048;
+
+};
+ol.inherits(ol.TileCache, ol.structs.LRUCache);
+
+
+/**
+ * @return {boolean} Can expire cache.
+ */
+ol.TileCache.prototype.canExpireCache = function() {
+  return this.getCount() > this.highWaterMark_;
+};
+
+
+/**
+ * @param {Object.<string, ol.TileRange>} usedTiles Used tiles.
+ */
+ol.TileCache.prototype.expireCache = function(usedTiles) {
+  var tile, zKey;
+  while (this.canExpireCache()) {
+    tile = this.peekLast();
+    zKey = tile.tileCoord[0].toString();
+    if (zKey in usedTiles && usedTiles[zKey].contains(tile.tileCoord)) {
+      break;
+    } else {
+      this.pop().dispose();
+    }
+  }
+};
+
+
+/**
+ * Remove a tile range from the cache, e.g. to invalidate tiles.
+ * @param {ol.TileRange} tileRange The tile range to prune.
+ */
+ol.TileCache.prototype.pruneTileRange = function(tileRange) {
+  var i = this.getCount(),
+      key;
+  while (i--) {
+    key = this.peekLastKey();
+    if (tileRange.contains(ol.tilecoord.createFromString(key))) {
+      this.pop().dispose();
+    } else {
+      this.get(key);
+    }
+  }
+};
+
+goog.provide('ol.Tile');
+goog.provide('ol.TileState');
+
+goog.require('ol.events');
+goog.require('ol.events.EventTarget');
+goog.require('ol.events.EventType');
+
+
+/**
+ * @enum {number}
+ */
+ol.TileState = {
+  IDLE: 0,
+  LOADING: 1,
+  LOADED: 2,
+  ERROR: 3,
+  EMPTY: 4,
+  ABORT: 5
+};
+
+
+/**
+ * @classdesc
+ * Base class for tiles.
+ *
+ * @constructor
+ * @extends {ol.events.EventTarget}
+ * @param {ol.TileCoord} tileCoord Tile coordinate.
+ * @param {ol.TileState} state State.
+ */
+ol.Tile = function(tileCoord, state) {
+
+  ol.events.EventTarget.call(this);
+
+  /**
+   * @type {ol.TileCoord}
+   */
+  this.tileCoord = tileCoord;
+
+  /**
+   * @protected
+   * @type {ol.TileState}
+   */
+  this.state = state;
+
+  /**
+   * An "interim" tile for this tile. The interim tile may be used while this
+   * one is loading, for "smooth" transitions when changing params/dimensions
+   * on the source.
+   * @type {ol.Tile}
+   */
+  this.interimTile = null;
+
+  /**
+   * A key assigned to the tile. This is used by the tile source to determine
+   * if this tile can effectively be used, or if a new tile should be created
+   * and this one be used as an interim tile for this new tile.
+   * @type {string}
+   */
+  this.key = '';
+
+};
+ol.inherits(ol.Tile, ol.events.EventTarget);
+
+
+/**
+ * @protected
+ */
+ol.Tile.prototype.changed = function() {
+  this.dispatchEvent(ol.events.EventType.CHANGE);
+};
+
+
+/**
+ * Get the HTML image element for this tile (may be a Canvas, Image, or Video).
+ * @function
+ * @param {Object=} opt_context Object.
+ * @return {HTMLCanvasElement|HTMLImageElement|HTMLVideoElement} Image.
+ */
+ol.Tile.prototype.getImage = goog.abstractMethod;
+
+
+/**
+ * @return {string} Key.
+ */
+ol.Tile.prototype.getKey = function() {
+  return goog.getUid(this).toString();
+};
+
+
+/**
+ * Get the tile coordinate for this tile.
+ * @return {ol.TileCoord} The tile coordinate.
+ * @api
+ */
+ol.Tile.prototype.getTileCoord = function() {
+  return this.tileCoord;
+};
+
+
+/**
+ * @return {ol.TileState} State.
+ */
+ol.Tile.prototype.getState = function() {
+  return this.state;
+};
+
+
+/**
+ * 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.
+ * @api
+ */
+ol.Tile.prototype.load = goog.abstractMethod;
+
+goog.provide('ol.size');
+
+
+goog.require('goog.asserts');
+
+
+/**
+ * Returns a buffered size.
+ * @param {ol.Size} size Size.
+ * @param {number} buffer Buffer.
+ * @param {ol.Size=} opt_size Optional reusable size array.
+ * @return {ol.Size} The buffered size.
+ */
+ol.size.buffer = function(size, buffer, opt_size) {
+  if (opt_size === undefined) {
+    opt_size = [0, 0];
+  }
+  opt_size[0] = size[0] + 2 * buffer;
+  opt_size[1] = size[1] + 2 * buffer;
+  return opt_size;
+};
+
+
+/**
+ * Compares sizes for equality.
+ * @param {ol.Size} a Size.
+ * @param {ol.Size} b Size.
+ * @return {boolean} Equals.
+ */
+ol.size.equals = function(a, b) {
+  return a[0] == b[0] && a[1] == b[1];
+};
+
+
+/**
+ * Determines if a size has a positive area.
+ * @param {ol.Size} size The size to test.
+ * @return {boolean} The size has a positive area.
+ */
+ol.size.hasArea = function(size) {
+  return size[0] > 0 && size[1] > 0;
+};
+
+
+/**
+ * Returns a size scaled by a ratio. The result will be an array of integers.
+ * @param {ol.Size} size Size.
+ * @param {number} ratio Ratio.
+ * @param {ol.Size=} opt_size Optional reusable size array.
+ * @return {ol.Size} The scaled size.
+ */
+ol.size.scale = function(size, ratio, opt_size) {
+  if (opt_size === undefined) {
+    opt_size = [0, 0];
+  }
+  opt_size[0] = (size[0] * ratio + 0.5) | 0;
+  opt_size[1] = (size[1] * ratio + 0.5) | 0;
+  return opt_size;
+};
+
+
+/**
+ * Returns an `ol.Size` array for the passed in number (meaning: square) or
+ * `ol.Size` array.
+ * (meaning: non-square),
+ * @param {number|ol.Size} size Width and height.
+ * @param {ol.Size=} opt_size Optional reusable size array.
+ * @return {ol.Size} Size.
+ * @api stable
+ */
+ol.size.toSize = function(size, opt_size) {
+  if (Array.isArray(size)) {
+    return size;
+  } else {
+    goog.asserts.assert(goog.isNumber(size));
+    if (opt_size === undefined) {
+      opt_size = [size, size];
+    } else {
+      opt_size[0] = size;
+      opt_size[1] = size;
+    }
+    return opt_size;
+  }
+};
+
+goog.provide('ol.source.Source');
+goog.provide('ol.source.State');
+
+goog.require('ol');
+goog.require('ol.Attribution');
+goog.require('ol.Object');
+goog.require('ol.proj');
+
+
+/**
+ * State of the source, one of 'undefined', 'loading', 'ready' or 'error'.
+ * @enum {string}
+ */
+ol.source.State = {
+  UNDEFINED: 'undefined',
+  LOADING: 'loading',
+  READY: 'ready',
+  ERROR: 'error'
+};
+
+
+/**
+ * @classdesc
+ * Abstract base class; normally only used for creating subclasses and not
+ * instantiated in apps.
+ * Base class for {@link ol.layer.Layer} sources.
+ *
+ * A generic `change` event is triggered when the state of the source changes.
+ *
+ * @constructor
+ * @extends {ol.Object}
+ * @param {ol.SourceSourceOptions} options Source options.
+ * @api stable
+ */
+ol.source.Source = function(options) {
+
+  ol.Object.call(this);
+
+  /**
+   * @private
+   * @type {ol.proj.Projection}
+   */
+  this.projection_ = ol.proj.get(options.projection);
+
+  /**
+   * @private
+   * @type {Array.<ol.Attribution>}
+   */
+  this.attributions_ = ol.source.Source.toAttributionsArray_(options.attributions);
+
+  /**
+   * @private
+   * @type {string|olx.LogoOptions|undefined}
+   */
+  this.logo_ = options.logo;
+
+  /**
+   * @private
+   * @type {ol.source.State}
+   */
+  this.state_ = options.state !== undefined ?
+      options.state : ol.source.State.READY;
+
+  /**
+   * @private
+   * @type {boolean}
+   */
+  this.wrapX_ = options.wrapX !== undefined ? options.wrapX : false;
+
+};
+ol.inherits(ol.source.Source, ol.Object);
+
+/**
+ * Turns various ways of defining an attribution to an array of `ol.Attributions`.
+ *
+ * @param {ol.AttributionLike|undefined}
+ *     attributionLike The attributions as string, array of strings,
+ *     `ol.Attribution`, array of `ol.Attribution` or undefined.
+ * @return {Array.<ol.Attribution>} The array of `ol.Attribution` or null if
+ *     `undefined` was given.
+ */
+ol.source.Source.toAttributionsArray_ = function(attributionLike) {
+  if (typeof attributionLike === 'string') {
+    return [new ol.Attribution({html: attributionLike})];
+  } else if (attributionLike instanceof ol.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 ol.Attribution({html: item});
+      } else {
+        attributions[i] = item;
+      }
+    }
+    return attributions;
+  } else {
+    return null;
+  }
+};
+
+
+/**
+ * @param {ol.Coordinate} coordinate Coordinate.
+ * @param {number} resolution Resolution.
+ * @param {number} rotation Rotation.
+ * @param {Object.<string, boolean>} skippedFeatureUids Skipped feature uids.
+ * @param {function((ol.Feature|ol.render.Feature)): T} callback Feature
+ *     callback.
+ * @return {T|undefined} Callback result.
+ * @template T
+ */
+ol.source.Source.prototype.forEachFeatureAtCoordinate = ol.nullFunction;
+
+
+/**
+ * Get the attributions of the source.
+ * @return {Array.<ol.Attribution>} Attributions.
+ * @api stable
+ */
+ol.source.Source.prototype.getAttributions = function() {
+  return this.attributions_;
+};
+
+
+/**
+ * Get the logo of the source.
+ * @return {string|olx.LogoOptions|undefined} Logo.
+ * @api stable
+ */
+ol.source.Source.prototype.getLogo = function() {
+  return this.logo_;
+};
+
+
+/**
+ * Get the projection of the source.
+ * @return {ol.proj.Projection} Projection.
+ * @api
+ */
+ol.source.Source.prototype.getProjection = function() {
+  return this.projection_;
+};
+
+
+/**
+ * @return {Array.<number>|undefined} Resolutions.
+ */
+ol.source.Source.prototype.getResolutions = goog.abstractMethod;
+
+
+/**
+ * Get the state of the source, see {@link ol.source.State} for possible states.
+ * @return {ol.source.State} State.
+ * @api
+ */
+ol.source.Source.prototype.getState = function() {
+  return this.state_;
+};
+
+
+/**
+ * @return {boolean|undefined} Wrap X.
+ */
+ol.source.Source.prototype.getWrapX = function() {
+  return this.wrapX_;
+};
+
+
+/**
+ * Refreshes the source and finally dispatches a 'change' event.
+ * @api
+ */
+ol.source.Source.prototype.refresh = function() {
+  this.changed();
+};
+
+
+/**
+ * Set the attributions of the source.
+ * @param {ol.AttributionLike|undefined} attributions Attributions.
+ *     Can be passed as `string`, `Array<string>`, `{@link ol.Attribution}`,
+ *     `Array<{@link ol.Attribution}>` or `undefined`.
+ * @api
+ */
+ol.source.Source.prototype.setAttributions = function(attributions) {
+  this.attributions_ = ol.source.Source.toAttributionsArray_(attributions);
+  this.changed();
+};
+
+
+/**
+ * Set the logo of the source.
+ * @param {string|olx.LogoOptions|undefined} logo Logo.
+ */
+ol.source.Source.prototype.setLogo = function(logo) {
+  this.logo_ = logo;
+};
+
+
+/**
+ * Set the state of the source.
+ * @param {ol.source.State} state State.
+ * @protected
+ */
+ol.source.Source.prototype.setState = function(state) {
+  this.state_ = state;
+  this.changed();
+};
+
+goog.provide('ol.tilegrid.TileGrid');
+
+goog.require('goog.asserts');
+goog.require('ol');
+goog.require('ol.TileRange');
+goog.require('ol.array');
+goog.require('ol.extent');
+goog.require('ol.extent.Corner');
+goog.require('ol.math');
+goog.require('ol.object');
+goog.require('ol.proj');
+goog.require('ol.proj.METERS_PER_UNIT');
+goog.require('ol.proj.Projection');
+goog.require('ol.proj.Units');
+goog.require('ol.size');
+goog.require('ol.tilecoord');
+
+
+/**
+ * @classdesc
+ * Base class for setting the grid pattern for sources accessing tiled-image
+ * servers.
+ *
+ * @constructor
+ * @param {olx.tilegrid.TileGridOptions} options Tile grid options.
+ * @struct
+ * @api stable
+ */
+ol.tilegrid.TileGrid = function(options) {
+
+  /**
+   * @protected
+   * @type {number}
+   */
+  this.minZoom = options.minZoom !== undefined ? options.minZoom : 0;
+
+  /**
+   * @private
+   * @type {!Array.<number>}
+   */
+  this.resolutions_ = options.resolutions;
+  goog.asserts.assert(ol.array.isSorted(this.resolutions_, function(a, b) {
+    return b - a;
+  }, true), 'resolutions must be sorted in descending order');
+
+  /**
+   * @protected
+   * @type {number}
+   */
+  this.maxZoom = this.resolutions_.length - 1;
+
+  /**
+   * @private
+   * @type {ol.Coordinate}
+   */
+  this.origin_ = options.origin !== undefined ? options.origin : null;
+
+  /**
+   * @private
+   * @type {Array.<ol.Coordinate>}
+   */
+  this.origins_ = null;
+  if (options.origins !== undefined) {
+    this.origins_ = options.origins;
+    goog.asserts.assert(this.origins_.length == this.resolutions_.length,
+        'number of origins and resolutions must be equal');
+  }
+
+  var extent = options.extent;
+
+  if (extent !== undefined &&
+      !this.origin_ && !this.origins_) {
+    this.origin_ = ol.extent.getTopLeft(extent);
+  }
+
+  goog.asserts.assert(
+      (!this.origin_ && this.origins_) ||
+      (this.origin_ && !this.origins_),
+      'either origin or origins must be configured, never both');
+
+  /**
+   * @private
+   * @type {Array.<number|ol.Size>}
+   */
+  this.tileSizes_ = null;
+  if (options.tileSizes !== undefined) {
+    this.tileSizes_ = options.tileSizes;
+    goog.asserts.assert(this.tileSizes_.length == this.resolutions_.length,
+        'number of tileSizes and resolutions must be equal');
+  }
+
+  /**
+   * @private
+   * @type {number|ol.Size}
+   */
+  this.tileSize_ = options.tileSize !== undefined ?
+      options.tileSize :
+      !this.tileSizes_ ? ol.DEFAULT_TILE_SIZE : null;
+  goog.asserts.assert(
+      (!this.tileSize_ && this.tileSizes_) ||
+      (this.tileSize_ && !this.tileSizes_),
+      'either tileSize or tileSizes must be configured, never both');
+
+  /**
+   * @private
+   * @type {ol.Extent}
+   */
+  this.extent_ = extent !== undefined ? extent : null;
+
+
+  /**
+   * @private
+   * @type {Array.<ol.TileRange>}
+   */
+  this.fullTileRanges_ = null;
+
+  /**
+   * @private
+   * @type {ol.Size}
+   */
+  this.tmpSize_ = [0, 0];
+
+  if (options.sizes !== undefined) {
+    goog.asserts.assert(options.sizes.length == this.resolutions_.length,
+        'number of sizes and resolutions must be equal');
+    this.fullTileRanges_ = options.sizes.map(function(size, z) {
+      goog.asserts.assert(size[0] !== 0, 'width must not be 0');
+      goog.asserts.assert(size[1] !== 0, 'height must not be 0');
+      var tileRange = new ol.TileRange(
+          Math.min(0, size[0]), Math.max(size[0] - 1, -1),
+          Math.min(0, size[1]), Math.max(size[1] - 1, -1));
+      if (this.minZoom <= z && z <= this.maxZoom && extent !== undefined) {
+        goog.asserts.assert(tileRange.containsTileRange(
+            this.getTileRangeForExtentAndZ(extent, z)),
+            'extent tile range must not exceed tilegrid width and height');
+      }
+      return tileRange;
+    }, this);
+  } else if (extent) {
+    this.calculateTileRanges_(extent);
+  }
+
+};
+
+
+/**
+ * @private
+ * @type {ol.TileCoord}
+ */
+ol.tilegrid.TileGrid.tmpTileCoord_ = [0, 0, 0];
+
+
+/**
+ * Call a function with each tile coordinate for a given extent and zoom level.
+ *
+ * @param {ol.Extent} extent Extent.
+ * @param {number} zoom Zoom level.
+ * @param {function(ol.TileCoord)} callback Function called with each tile coordinate.
+ * @api
+ */
+ol.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 {ol.TileCoord} tileCoord Tile coordinate.
+ * @param {function(this: T, number, ol.TileRange): boolean} callback Callback.
+ * @param {T=} opt_this The object to use as `this` in `callback`.
+ * @param {ol.TileRange=} opt_tileRange Temporary ol.TileRange object.
+ * @param {ol.Extent=} opt_extent Temporary ol.Extent object.
+ * @return {boolean} Callback succeeded.
+ * @template T
+ */
+ol.tilegrid.TileGrid.prototype.forEachTileCoordParentTileRange = function(tileCoord, callback, opt_this, opt_tileRange, opt_extent) {
+  var tileCoordExtent = this.getTileCoordExtent(tileCoord, opt_extent);
+  var z = tileCoord[0] - 1;
+  while (z >= this.minZoom) {
+    if (callback.call(opt_this, z,
+        this.getTileRangeForExtentAndZ(tileCoordExtent, z, opt_tileRange))) {
+      return true;
+    }
+    --z;
+  }
+  return false;
+};
+
+
+/**
+ * Get the extent for this tile grid, if it was configured.
+ * @return {ol.Extent} Extent.
+ */
+ol.tilegrid.TileGrid.prototype.getExtent = function() {
+  return this.extent_;
+};
+
+
+/**
+ * Get the maximum zoom level for the grid.
+ * @return {number} Max zoom.
+ * @api
+ */
+ol.tilegrid.TileGrid.prototype.getMaxZoom = function() {
+  return this.maxZoom;
+};
+
+
+/**
+ * Get the minimum zoom level for the grid.
+ * @return {number} Min zoom.
+ * @api
+ */
+ol.tilegrid.TileGrid.prototype.getMinZoom = function() {
+  return this.minZoom;
+};
+
+
+/**
+ * Get the origin for the grid at the given zoom level.
+ * @param {number} z Z.
+ * @return {ol.Coordinate} Origin.
+ * @api stable
+ */
+ol.tilegrid.TileGrid.prototype.getOrigin = function(z) {
+  if (this.origin_) {
+    return this.origin_;
+  } else {
+    goog.asserts.assert(this.origins_,
+        'origins cannot be null if origin is null');
+    goog.asserts.assert(this.minZoom <= z && z <= this.maxZoom,
+        'given z is not in allowed range (%s <= %s <= %s)',
+        this.minZoom, z, this.maxZoom);
+    return this.origins_[z];
+  }
+};
+
+
+/**
+ * Get the resolution for the given zoom level.
+ * @param {number} z Z.
+ * @return {number} Resolution.
+ * @api stable
+ */
+ol.tilegrid.TileGrid.prototype.getResolution = function(z) {
+  goog.asserts.assert(this.minZoom <= z && z <= this.maxZoom,
+      'given z is not in allowed range (%s <= %s <= %s)',
+      this.minZoom, z, this.maxZoom);
+  return this.resolutions_[z];
+};
+
+
+/**
+ * Get the list of resolutions for the tile grid.
+ * @return {Array.<number>} Resolutions.
+ * @api stable
+ */
+ol.tilegrid.TileGrid.prototype.getResolutions = function() {
+  return this.resolutions_;
+};
+
+
+/**
+ * @param {ol.TileCoord} tileCoord Tile coordinate.
+ * @param {ol.TileRange=} opt_tileRange Temporary ol.TileRange object.
+ * @param {ol.Extent=} opt_extent Temporary ol.Extent object.
+ * @return {ol.TileRange} Tile range.
+ */
+ol.tilegrid.TileGrid.prototype.getTileCoordChildTileRange = function(tileCoord, opt_tileRange, opt_extent) {
+  if (tileCoord[0] < this.maxZoom) {
+    var tileCoordExtent = this.getTileCoordExtent(tileCoord, opt_extent);
+    return this.getTileRangeForExtentAndZ(
+        tileCoordExtent, tileCoord[0] + 1, opt_tileRange);
+  } else {
+    return null;
+  }
+};
+
+
+/**
+ * @param {number} z Z.
+ * @param {ol.TileRange} tileRange Tile range.
+ * @param {ol.Extent=} opt_extent Temporary ol.Extent object.
+ * @return {ol.Extent} Extent.
+ */
+ol.tilegrid.TileGrid.prototype.getTileRangeExtent = function(z, tileRange, opt_extent) {
+  var origin = this.getOrigin(z);
+  var resolution = this.getResolution(z);
+  var tileSize = ol.size.toSize(this.getTileSize(z), this.tmpSize_);
+  var minX = origin[0] + tileRange.minX * tileSize[0] * resolution;
+  var maxX = origin[0] + (tileRange.maxX + 1) * tileSize[0] * resolution;
+  var minY = origin[1] + tileRange.minY * tileSize[1] * resolution;
+  var maxY = origin[1] + (tileRange.maxY + 1) * tileSize[1] * resolution;
+  return ol.extent.createOrUpdate(minX, minY, maxX, maxY, opt_extent);
+};
+
+
+/**
+ * @param {ol.Extent} extent Extent.
+ * @param {number} resolution Resolution.
+ * @param {ol.TileRange=} opt_tileRange Temporary tile range object.
+ * @return {ol.TileRange} Tile range.
+ */
+ol.tilegrid.TileGrid.prototype.getTileRangeForExtentAndResolution = function(extent, resolution, opt_tileRange) {
+  var tileCoord = ol.tilegrid.TileGrid.tmpTileCoord_;
+  this.getTileCoordForXYAndResolution_(
+      extent[0], extent[1], resolution, false, tileCoord);
+  var minX = tileCoord[1];
+  var minY = tileCoord[2];
+  this.getTileCoordForXYAndResolution_(
+      extent[2], extent[3], resolution, true, tileCoord);
+  return ol.TileRange.createOrUpdate(
+      minX, tileCoord[1], minY, tileCoord[2], opt_tileRange);
+};
+
+
+/**
+ * @param {ol.Extent} extent Extent.
+ * @param {number} z Z.
+ * @param {ol.TileRange=} opt_tileRange Temporary tile range object.
+ * @return {ol.TileRange} Tile range.
+ */
+ol.tilegrid.TileGrid.prototype.getTileRangeForExtentAndZ = function(extent, z, opt_tileRange) {
+  var resolution = this.getResolution(z);
+  return this.getTileRangeForExtentAndResolution(
+      extent, resolution, opt_tileRange);
+};
+
+
+/**
+ * @param {ol.TileCoord} tileCoord Tile coordinate.
+ * @return {ol.Coordinate} Tile center.
+ */
+ol.tilegrid.TileGrid.prototype.getTileCoordCenter = function(tileCoord) {
+  var origin = this.getOrigin(tileCoord[0]);
+  var resolution = this.getResolution(tileCoord[0]);
+  var tileSize = ol.size.toSize(this.getTileSize(tileCoord[0]), this.tmpSize_);
+  return [
+    origin[0] + (tileCoord[1] + 0.5) * tileSize[0] * resolution,
+    origin[1] + (tileCoord[2] + 0.5) * tileSize[1] * resolution
+  ];
+};
+
+
+/**
+ * Get the extent of a tile coordinate.
+ *
+ * @param {ol.TileCoord} tileCoord Tile coordinate.
+ * @param {ol.Extent=} opt_extent Temporary extent object.
+ * @return {ol.Extent} Extent.
+ * @api
+ */
+ol.tilegrid.TileGrid.prototype.getTileCoordExtent = function(tileCoord, opt_extent) {
+  var origin = this.getOrigin(tileCoord[0]);
+  var resolution = this.getResolution(tileCoord[0]);
+  var tileSize = ol.size.toSize(this.getTileSize(tileCoord[0]), this.tmpSize_);
+  var minX = origin[0] + tileCoord[1] * tileSize[0] * resolution;
+  var minY = origin[1] + tileCoord[2] * tileSize[1] * resolution;
+  var maxX = minX + tileSize[0] * resolution;
+  var maxY = minY + tileSize[1] * resolution;
+  return ol.extent.createOrUpdate(minX, minY, maxX, maxY, opt_extent);
+};
+
+
+/**
+ * Get the tile coordinate for the given map coordinate and resolution.  This
+ * method considers that coordinates that intersect tile boundaries should be
+ * assigned the higher tile coordinate.
+ *
+ * @param {ol.Coordinate} coordinate Coordinate.
+ * @param {number} resolution Resolution.
+ * @param {ol.TileCoord=} opt_tileCoord Destination ol.TileCoord object.
+ * @return {ol.TileCoord} Tile coordinate.
+ * @api
+ */
+ol.tilegrid.TileGrid.prototype.getTileCoordForCoordAndResolution = function(coordinate, resolution, opt_tileCoord) {
+  return this.getTileCoordForXYAndResolution_(
+      coordinate[0], coordinate[1], resolution, false, opt_tileCoord);
+};
+
+
+/**
+ * @param {number} x X.
+ * @param {number} y Y.
+ * @param {number} resolution Resolution.
+ * @param {boolean} reverseIntersectionPolicy Instead of letting edge
+ *     intersections go to the higher tile coordinate, let edge intersections
+ *     go to the lower tile coordinate.
+ * @param {ol.TileCoord=} opt_tileCoord Temporary ol.TileCoord object.
+ * @return {ol.TileCoord} Tile coordinate.
+ * @private
+ */
+ol.tilegrid.TileGrid.prototype.getTileCoordForXYAndResolution_ = function(
+    x, y, resolution, reverseIntersectionPolicy, opt_tileCoord) {
+  var z = this.getZForResolution(resolution);
+  var scale = resolution / this.getResolution(z);
+  var origin = this.getOrigin(z);
+  var tileSize = ol.size.toSize(this.getTileSize(z), this.tmpSize_);
+
+  var adjustX = reverseIntersectionPolicy ? 0.5 : 0;
+  var adjustY = reverseIntersectionPolicy ? 0 : 0.5;
+  var xFromOrigin = Math.floor((x - origin[0]) / resolution + adjustX);
+  var yFromOrigin = Math.floor((y - origin[1]) / resolution + adjustY);
+  var tileCoordX = scale * xFromOrigin / tileSize[0];
+  var tileCoordY = scale * yFromOrigin / tileSize[1];
+
+  if (reverseIntersectionPolicy) {
+    tileCoordX = Math.ceil(tileCoordX) - 1;
+    tileCoordY = Math.ceil(tileCoordY) - 1;
+  } else {
+    tileCoordX = Math.floor(tileCoordX);
+    tileCoordY = Math.floor(tileCoordY);
+  }
+
+  return ol.tilecoord.createOrUpdate(z, tileCoordX, tileCoordY, opt_tileCoord);
+};
+
+
+/**
+ * Get a tile coordinate given a map coordinate and zoom level.
+ * @param {ol.Coordinate} coordinate Coordinate.
+ * @param {number} z Zoom level.
+ * @param {ol.TileCoord=} opt_tileCoord Destination ol.TileCoord object.
+ * @return {ol.TileCoord} Tile coordinate.
+ * @api
+ */
+ol.tilegrid.TileGrid.prototype.getTileCoordForCoordAndZ = function(coordinate, z, opt_tileCoord) {
+  var resolution = this.getResolution(z);
+  return this.getTileCoordForXYAndResolution_(
+      coordinate[0], coordinate[1], resolution, false, opt_tileCoord);
+};
+
+
+/**
+ * @param {ol.TileCoord} tileCoord Tile coordinate.
+ * @return {number} Tile resolution.
+ */
+ol.tilegrid.TileGrid.prototype.getTileCoordResolution = function(tileCoord) {
+  goog.asserts.assert(
+      this.minZoom <= tileCoord[0] && tileCoord[0] <= this.maxZoom,
+      'z of given tilecoord is not in allowed range (%s <= %s <= %s',
+      this.minZoom, tileCoord[0], this.maxZoom);
+  return this.resolutions_[tileCoord[0]];
+};
+
+
+/**
+ * Get the tile size for a zoom level. The type of the return value matches the
+ * `tileSize` or `tileSizes` that the tile grid was configured with. To always
+ * get an `ol.Size`, run the result through `ol.size.toSize()`.
+ * @param {number} z Z.
+ * @return {number|ol.Size} Tile size.
+ * @api stable
+ */
+ol.tilegrid.TileGrid.prototype.getTileSize = function(z) {
+  if (this.tileSize_) {
+    return this.tileSize_;
+  } else {
+    goog.asserts.assert(this.tileSizes_,
+        'tileSizes cannot be null if tileSize is null');
+    goog.asserts.assert(this.minZoom <= z && z <= this.maxZoom,
+        'z is not in allowed range (%s <= %s <= %s',
+        this.minZoom, z, this.maxZoom);
+    return this.tileSizes_[z];
+  }
+};
+
+
+/**
+ * @param {number} z Zoom level.
+ * @return {ol.TileRange} Extent tile range for the specified zoom level.
+ */
+ol.tilegrid.TileGrid.prototype.getFullTileRange = function(z) {
+  if (!this.fullTileRanges_) {
+    return null;
+  } else {
+    goog.asserts.assert(this.minZoom <= z && z <= this.maxZoom,
+        'z is not in allowed range (%s <= %s <= %s',
+        this.minZoom, z, this.maxZoom);
+    return this.fullTileRanges_[z];
+  }
+};
+
+
+/**
+ * @param {number} resolution Resolution.
+ * @param {number=} opt_direction If 0, the nearest resolution will be used.
+ *     If 1, the nearest lower resolution will be used. If -1, the nearest
+ *     higher resolution will be used. Default is 0.
+ * @return {number} Z.
+ * @api
+ */
+ol.tilegrid.TileGrid.prototype.getZForResolution = function(
+    resolution, opt_direction) {
+  var z = ol.array.linearFindNearest(this.resolutions_, resolution,
+      opt_direction || 0);
+  return ol.math.clamp(z, this.minZoom, this.maxZoom);
+};
+
+
+/**
+ * @param {!ol.Extent} extent Extent for this tile grid.
+ * @private
+ */
+ol.tilegrid.TileGrid.prototype.calculateTileRanges_ = function(extent) {
+  var length = this.resolutions_.length;
+  var fullTileRanges = new Array(length);
+  for (var z = this.minZoom; z < length; ++z) {
+    fullTileRanges[z] = this.getTileRangeForExtentAndZ(extent, z);
+  }
+  this.fullTileRanges_ = fullTileRanges;
+};
+
+
+/**
+ * @param {ol.proj.Projection} projection Projection.
+ * @return {ol.tilegrid.TileGrid} Default tile grid for the passed projection.
+ */
+ol.tilegrid.getForProjection = function(projection) {
+  var tileGrid = projection.getDefaultTileGrid();
+  if (!tileGrid) {
+    tileGrid = ol.tilegrid.createForProjection(projection);
+    projection.setDefaultTileGrid(tileGrid);
+  }
+  return tileGrid;
+};
+
+
+/**
+ * @param {ol.Extent} extent Extent.
+ * @param {number=} opt_maxZoom Maximum zoom level (default is
+ *     ol.DEFAULT_MAX_ZOOM).
+ * @param {number|ol.Size=} opt_tileSize Tile size (default uses
+ *     ol.DEFAULT_TILE_SIZE).
+ * @param {ol.extent.Corner=} opt_corner Extent corner (default is
+ *     ol.extent.Corner.TOP_LEFT).
+ * @return {ol.tilegrid.TileGrid} TileGrid instance.
+ */
+ol.tilegrid.createForExtent = function(extent, opt_maxZoom, opt_tileSize, opt_corner) {
+  var corner = opt_corner !== undefined ?
+      opt_corner : ol.extent.Corner.TOP_LEFT;
+
+  var resolutions = ol.tilegrid.resolutionsFromExtent(
+      extent, opt_maxZoom, opt_tileSize);
+
+  return new ol.tilegrid.TileGrid({
+    extent: extent,
+    origin: ol.extent.getCorner(extent, corner),
+    resolutions: resolutions,
+    tileSize: opt_tileSize
+  });
+};
+
+
+/**
+ * Creates a tile grid with a standard XYZ tiling scheme.
+ * @param {olx.tilegrid.XYZOptions=} opt_options Tile grid options.
+ * @return {ol.tilegrid.TileGrid} Tile grid instance.
+ * @api
+ */
+ol.tilegrid.createXYZ = function(opt_options) {
+  var options = /** @type {olx.tilegrid.TileGridOptions} */ ({});
+  ol.object.assign(options, opt_options !== undefined ?
+      opt_options : /** @type {olx.tilegrid.XYZOptions} */ ({}));
+  if (options.extent === undefined) {
+    options.extent = ol.proj.get('EPSG:3857').getExtent();
+  }
+  options.resolutions = ol.tilegrid.resolutionsFromExtent(
+      options.extent, options.maxZoom, options.tileSize);
+  delete options.maxZoom;
+
+  return new ol.tilegrid.TileGrid(options);
+};
+
+
+/**
+ * Create a resolutions array from an extent.  A zoom factor of 2 is assumed.
+ * @param {ol.Extent} extent Extent.
+ * @param {number=} opt_maxZoom Maximum zoom level (default is
+ *     ol.DEFAULT_MAX_ZOOM).
+ * @param {number|ol.Size=} opt_tileSize Tile size (default uses
+ *     ol.DEFAULT_TILE_SIZE).
+ * @return {!Array.<number>} Resolutions array.
+ */
+ol.tilegrid.resolutionsFromExtent = function(extent, opt_maxZoom, opt_tileSize) {
+  var maxZoom = opt_maxZoom !== undefined ?
+      opt_maxZoom : ol.DEFAULT_MAX_ZOOM;
+
+  var height = ol.extent.getHeight(extent);
+  var width = ol.extent.getWidth(extent);
+
+  var tileSize = ol.size.toSize(opt_tileSize !== undefined ?
+      opt_tileSize : ol.DEFAULT_TILE_SIZE);
+  var maxResolution = Math.max(
+      width / tileSize[0], height / tileSize[1]);
+
+  var length = maxZoom + 1;
+  var resolutions = new Array(length);
+  for (var z = 0; z < length; ++z) {
+    resolutions[z] = maxResolution / Math.pow(2, z);
+  }
+  return resolutions;
+};
+
+
+/**
+ * @param {ol.ProjectionLike} projection Projection.
+ * @param {number=} opt_maxZoom Maximum zoom level (default is
+ *     ol.DEFAULT_MAX_ZOOM).
+ * @param {ol.Size=} opt_tileSize Tile size (default uses ol.DEFAULT_TILE_SIZE).
+ * @param {ol.extent.Corner=} opt_corner Extent corner (default is
+ *     ol.extent.Corner.BOTTOM_LEFT).
+ * @return {ol.tilegrid.TileGrid} TileGrid instance.
+ */
+ol.tilegrid.createForProjection = function(projection, opt_maxZoom, opt_tileSize, opt_corner) {
+  var extent = ol.tilegrid.extentFromProjection(projection);
+  return ol.tilegrid.createForExtent(
+      extent, opt_maxZoom, opt_tileSize, opt_corner);
+};
+
+
+/**
+ * Generate a tile grid extent from a projection.  If the projection has an
+ * extent, it is used.  If not, a global extent is assumed.
+ * @param {ol.ProjectionLike} projection Projection.
+ * @return {ol.Extent} Extent.
+ */
+ol.tilegrid.extentFromProjection = function(projection) {
+  projection = ol.proj.get(projection);
+  var extent = projection.getExtent();
+  if (!extent) {
+    var half = 180 * ol.proj.METERS_PER_UNIT[ol.proj.Units.DEGREES] /
+        projection.getMetersPerUnit();
+    extent = ol.extent.createOrUpdate(-half, -half, half, half);
+  }
+  return extent;
+};
+
+goog.provide('ol.source.Tile');
+goog.provide('ol.source.TileEvent');
+
+goog.require('goog.asserts');
+goog.require('ol.events.Event');
+goog.require('ol');
+goog.require('ol.TileCache');
+goog.require('ol.TileRange');
+goog.require('ol.TileState');
+goog.require('ol.proj');
+goog.require('ol.size');
+goog.require('ol.source.Source');
+goog.require('ol.tilecoord');
+goog.require('ol.tilegrid.TileGrid');
+
+
+/**
+ * @classdesc
+ * Abstract base class; normally only used for creating subclasses and not
+ * instantiated in apps.
+ * Base class for sources providing images divided into a tile grid.
+ *
+ * @constructor
+ * @extends {ol.source.Source}
+ * @param {ol.SourceTileOptions} options Tile source options.
+ * @api
+ */
+ol.source.Tile = function(options) {
+
+  ol.source.Source.call(this, {
+    attributions: options.attributions,
+    extent: options.extent,
+    logo: options.logo,
+    projection: options.projection,
+    state: options.state,
+    wrapX: options.wrapX
+  });
+
+  /**
+   * @private
+   * @type {boolean}
+   */
+  this.opaque_ = options.opaque !== undefined ? options.opaque : false;
+
+  /**
+   * @private
+   * @type {number}
+   */
+  this.tilePixelRatio_ = options.tilePixelRatio !== undefined ?
+      options.tilePixelRatio : 1;
+
+  /**
+   * @protected
+   * @type {ol.tilegrid.TileGrid}
+   */
+  this.tileGrid = options.tileGrid !== undefined ? options.tileGrid : null;
+
+  /**
+   * @protected
+   * @type {ol.TileCache}
+   */
+  this.tileCache = new ol.TileCache(options.cacheSize);
+
+  /**
+   * @protected
+   * @type {ol.Size}
+   */
+  this.tmpSize = [0, 0];
+
+  /**
+   * @private
+   * @type {string}
+   */
+  this.key_ = '';
+
+};
+ol.inherits(ol.source.Tile, ol.source.Source);
+
+
+/**
+ * @return {boolean} Can expire cache.
+ */
+ol.source.Tile.prototype.canExpireCache = function() {
+  return this.tileCache.canExpireCache();
+};
+
+
+/**
+ * @param {ol.proj.Projection} projection Projection.
+ * @param {Object.<string, ol.TileRange>} usedTiles Used tiles.
+ */
+ol.source.Tile.prototype.expireCache = function(projection, usedTiles) {
+  var tileCache = this.getTileCacheForProjection(projection);
+  if (tileCache) {
+    tileCache.expireCache(usedTiles);
+  }
+};
+
+
+/**
+ * @param {ol.proj.Projection} projection Projection.
+ * @param {number} z Zoom level.
+ * @param {ol.TileRange} tileRange Tile range.
+ * @param {function(ol.Tile):(boolean|undefined)} callback Called with each
+ *     loaded tile.  If the callback returns `false`, the tile will not be
+ *     considered loaded.
+ * @return {boolean} The tile range is fully covered with loaded tiles.
+ */
+ol.source.Tile.prototype.forEachLoadedTile = function(projection, z, tileRange, callback) {
+  var tileCache = this.getTileCacheForProjection(projection);
+  if (!tileCache) {
+    return false;
+  }
+
+  var covered = true;
+  var tile, tileCoordKey, loaded;
+  for (var x = tileRange.minX; x <= tileRange.maxX; ++x) {
+    for (var y = tileRange.minY; y <= tileRange.maxY; ++y) {
+      tileCoordKey = this.getKeyZXY(z, x, y);
+      loaded = false;
+      if (tileCache.containsKey(tileCoordKey)) {
+        tile = /** @type {!ol.Tile} */ (tileCache.get(tileCoordKey));
+        loaded = tile.getState() === ol.TileState.LOADED;
+        if (loaded) {
+          loaded = (callback(tile) !== false);
+        }
+      }
+      if (!loaded) {
+        covered = false;
+      }
+    }
+  }
+  return covered;
+};
+
+
+/**
+ * @param {ol.proj.Projection} projection Projection.
+ * @return {number} Gutter.
+ */
+ol.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
+ */
+ol.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
+ */
+ol.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
+ */
+ol.source.Tile.prototype.getKeyZXY = ol.tilecoord.getKeyZXY;
+
+
+/**
+ * @param {ol.proj.Projection} projection Projection.
+ * @return {boolean} Opaque.
+ */
+ol.source.Tile.prototype.getOpaque = function(projection) {
+  return this.opaque_;
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.source.Tile.prototype.getResolutions = function() {
+  return this.tileGrid.getResolutions();
+};
+
+
+/**
+ * @param {number} z Tile coordinate z.
+ * @param {number} x Tile coordinate x.
+ * @param {number} y Tile coordinate y.
+ * @param {number} pixelRatio Pixel ratio.
+ * @param {ol.proj.Projection} projection Projection.
+ * @return {!ol.Tile} Tile.
+ */
+ol.source.Tile.prototype.getTile = goog.abstractMethod;
+
+
+/**
+ * Return the tile grid of the tile source.
+ * @return {ol.tilegrid.TileGrid} Tile grid.
+ * @api stable
+ */
+ol.source.Tile.prototype.getTileGrid = function() {
+  return this.tileGrid;
+};
+
+
+/**
+ * @param {ol.proj.Projection} projection Projection.
+ * @return {ol.tilegrid.TileGrid} Tile grid.
+ */
+ol.source.Tile.prototype.getTileGridForProjection = function(projection) {
+  if (!this.tileGrid) {
+    return ol.tilegrid.getForProjection(projection);
+  } else {
+    return this.tileGrid;
+  }
+};
+
+
+/**
+ * @param {ol.proj.Projection} projection Projection.
+ * @return {ol.TileCache} Tile cache.
+ * @protected
+ */
+ol.source.Tile.prototype.getTileCacheForProjection = function(projection) {
+  var thisProj = this.getProjection();
+  if (thisProj && !ol.proj.equivalent(thisProj, projection)) {
+    return null;
+  } else {
+    return this.tileCache;
+  }
+};
+
+
+/**
+ * @param {number} pixelRatio Pixel ratio.
+ * @return {number} Tile pixel ratio.
+ */
+ol.source.Tile.prototype.getTilePixelRatio = function(pixelRatio) {
+  return this.tilePixelRatio_;
+};
+
+
+/**
+ * @param {number} z Z.
+ * @param {number} pixelRatio Pixel ratio.
+ * @param {ol.proj.Projection} projection Projection.
+ * @return {ol.Size} Tile size.
+ */
+ol.source.Tile.prototype.getTilePixelSize = function(z, pixelRatio, projection) {
+  var tileGrid = this.getTileGridForProjection(projection);
+  var tilePixelRatio = this.getTilePixelRatio(pixelRatio);
+  var tileSize = ol.size.toSize(tileGrid.getTileSize(z), this.tmpSize);
+  if (tilePixelRatio == 1) {
+    return tileSize;
+  } else {
+    return ol.size.scale(tileSize, tilePixelRatio, this.tmpSize);
+  }
+};
+
+
+/**
+ * Returns a tile coordinate wrapped around the x-axis. When the tile coordinate
+ * is outside the resolution and extent range of the tile grid, `null` will be
+ * returned.
+ * @param {ol.TileCoord} tileCoord Tile coordinate.
+ * @param {ol.proj.Projection=} opt_projection Projection.
+ * @return {ol.TileCoord} Tile coordinate to be passed to the tileUrlFunction or
+ *     null if no tile URL should be created for the passed `tileCoord`.
+ */
+ol.source.Tile.prototype.getTileCoordForTileUrlFunction = function(tileCoord, opt_projection) {
+  var projection = opt_projection !== undefined ?
+      opt_projection : this.getProjection();
+  var tileGrid = this.getTileGridForProjection(projection);
+  goog.asserts.assert(tileGrid, 'tile grid needed');
+  if (this.getWrapX() && projection.isGlobal()) {
+    tileCoord = ol.tilecoord.wrapX(tileCoord, tileGrid, projection);
+  }
+  return ol.tilecoord.withinExtentAndZ(tileCoord, tileGrid) ? tileCoord : null;
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.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 {ol.proj.Projection} projection Projection.
+ */
+ol.source.Tile.prototype.useTile = ol.nullFunction;
+
+
+/**
+ * @classdesc
+ * Events emitted by {@link ol.source.Tile} instances are instances of this
+ * type.
+ *
+ * @constructor
+ * @extends {ol.events.Event}
+ * @implements {oli.source.TileEvent}
+ * @param {string} type Type.
+ * @param {ol.Tile} tile The tile.
+ */
+ol.source.TileEvent = function(type, tile) {
+
+  ol.events.Event.call(this, type);
+
+  /**
+   * The tile related to the event.
+   * @type {ol.Tile}
+   * @api
+   */
+  this.tile = tile;
+
+};
+ol.inherits(ol.source.TileEvent, ol.events.Event);
+
+
+/**
+ * @enum {string}
+ */
+ol.source.TileEventType = {
+
+  /**
+   * Triggered when a tile starts loading.
+   * @event ol.source.TileEvent#tileloadstart
+   * @api stable
+   */
+  TILELOADSTART: 'tileloadstart',
+
+  /**
+   * Triggered when a tile finishes loading.
+   * @event ol.source.TileEvent#tileloadend
+   * @api stable
+   */
+  TILELOADEND: 'tileloadend',
+
+  /**
+   * Triggered if tile loading results in an error.
+   * @event ol.source.TileEvent#tileloaderror
+   * @api stable
+   */
+  TILELOADERROR: 'tileloaderror'
+
+};
+
+// FIXME handle date line wrap
+
+goog.provide('ol.control.Attribution');
+
+goog.require('goog.asserts');
+goog.require('ol');
+goog.require('ol.dom');
+goog.require('ol.Attribution');
+goog.require('ol.control.Control');
+goog.require('ol.css');
+goog.require('ol.events');
+goog.require('ol.events.EventType');
+goog.require('ol.object');
+goog.require('ol.source.Tile');
+
+
+/**
+ * @classdesc
+ * Control to show all the attributions associated with the layer sources
+ * in the map. This control is one of the default controls included in maps.
+ * By default it will show in the bottom right portion of the map, but this can
+ * be changed by using a css selector for `.ol-attribution`.
+ *
+ * @constructor
+ * @extends {ol.control.Control}
+ * @param {olx.control.AttributionOptions=} opt_options Attribution options.
+ * @api stable
+ */
+ol.control.Attribution = function(opt_options) {
+
+  var options = opt_options ? opt_options : {};
+
+  /**
+   * @private
+   * @type {Element}
+   */
+  this.ulElement_ = document.createElement('UL');
+
+  /**
+   * @private
+   * @type {Element}
+   */
+  this.logoLi_ = document.createElement('LI');
+
+  this.ulElement_.appendChild(this.logoLi_);
+  this.logoLi_.style.display = 'none';
+
+  /**
+   * @private
+   * @type {boolean}
+   */
+  this.collapsed_ = options.collapsed !== undefined ? options.collapsed : true;
+
+  /**
+   * @private
+   * @type {boolean}
+   */
+  this.collapsible_ = options.collapsible !== undefined ?
+      options.collapsible : true;
+
+  if (!this.collapsible_) {
+    this.collapsed_ = false;
+  }
+
+  var className = options.className !== undefined ? options.className : 'ol-attribution';
+
+  var tipLabel = options.tipLabel !== undefined ? options.tipLabel : 'Attributions';
+
+  var collapseLabel = options.collapseLabel !== undefined ? options.collapseLabel : '\u00BB';
+
+  if (typeof collapseLabel === 'string') {
+    /**
+     * @private
+     * @type {Node}
+     */
+    this.collapseLabel_ = document.createElement('span');
+    this.collapseLabel_.textContent = collapseLabel;
+  } else {
+    this.collapseLabel_ = collapseLabel;
+  }
+
+  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.collapsible_ && !this.collapsed_) ?
+      this.collapseLabel_ : this.label_;
+  var button = document.createElement('button');
+  button.setAttribute('type', 'button');
+  button.title = tipLabel;
+  button.appendChild(activeLabel);
+
+  ol.events.listen(button, ol.events.EventType.CLICK, this.handleClick_, this);
+
+  var cssClasses = className + ' ' + ol.css.CLASS_UNSELECTABLE + ' ' +
+      ol.css.CLASS_CONTROL +
+      (this.collapsed_ && this.collapsible_ ? ' ol-collapsed' : '') +
+      (this.collapsible_ ? '' : ' ol-uncollapsible');
+  var element = document.createElement('div');
+  element.className = cssClasses;
+  element.appendChild(this.ulElement_);
+  element.appendChild(button);
+
+  var render = options.render ? options.render : ol.control.Attribution.render;
+
+  ol.control.Control.call(this, {
+    element: element,
+    render: render,
+    target: options.target
+  });
+
+  /**
+   * @private
+   * @type {boolean}
+   */
+  this.renderedVisible_ = true;
+
+  /**
+   * @private
+   * @type {Object.<string, Element>}
+   */
+  this.attributionElements_ = {};
+
+  /**
+   * @private
+   * @type {Object.<string, boolean>}
+   */
+  this.attributionElementRenderedVisible_ = {};
+
+  /**
+   * @private
+   * @type {Object.<string, Element>}
+   */
+  this.logoElements_ = {};
+
+};
+ol.inherits(ol.control.Attribution, ol.control.Control);
+
+
+/**
+ * @param {?olx.FrameState} frameState Frame state.
+ * @return {Array.<Object.<string, ol.Attribution>>} Attributions.
+ */
+ol.control.Attribution.prototype.getSourceAttributions = function(frameState) {
+  var i, ii, j, jj, tileRanges, source, sourceAttribution,
+      sourceAttributionKey, sourceAttributions, sourceKey;
+  var intersectsTileRange;
+  var layerStatesArray = frameState.layerStatesArray;
+  /** @type {Object.<string, ol.Attribution>} */
+  var attributions = ol.object.assign({}, frameState.attributions);
+  /** @type {Object.<string, ol.Attribution>} */
+  var hiddenAttributions = {};
+  var projection = frameState.viewState.projection;
+  goog.asserts.assert(projection, 'projection of viewState required');
+  for (i = 0, ii = layerStatesArray.length; i < ii; i++) {
+    source = layerStatesArray[i].layer.getSource();
+    if (!source) {
+      continue;
+    }
+    sourceKey = goog.getUid(source).toString();
+    sourceAttributions = source.getAttributions();
+    if (!sourceAttributions) {
+      continue;
+    }
+    for (j = 0, jj = sourceAttributions.length; j < jj; j++) {
+      sourceAttribution = sourceAttributions[j];
+      sourceAttributionKey = goog.getUid(sourceAttribution).toString();
+      if (sourceAttributionKey in attributions) {
+        continue;
+      }
+      tileRanges = frameState.usedTiles[sourceKey];
+      if (tileRanges) {
+        goog.asserts.assertInstanceof(source, ol.source.Tile,
+            'source should be an ol.source.Tile');
+        var tileGrid = source.getTileGridForProjection(projection);
+        goog.asserts.assert(tileGrid, 'tileGrid required for projection');
+        intersectsTileRange = sourceAttribution.intersectsAnyTileRange(
+            tileRanges, tileGrid, projection);
+      } else {
+        intersectsTileRange = false;
+      }
+      if (intersectsTileRange) {
+        if (sourceAttributionKey in hiddenAttributions) {
+          delete hiddenAttributions[sourceAttributionKey];
+        }
+        attributions[sourceAttributionKey] = sourceAttribution;
+      } else {
+        hiddenAttributions[sourceAttributionKey] = sourceAttribution;
+      }
+    }
+  }
+  return [attributions, hiddenAttributions];
+};
+
+
+/**
+ * Update the attribution element.
+ * @param {ol.MapEvent} mapEvent Map event.
+ * @this {ol.control.Attribution}
+ * @api
+ */
+ol.control.Attribution.render = function(mapEvent) {
+  this.updateElement_(mapEvent.frameState);
+};
+
+
+/**
+ * @private
+ * @param {?olx.FrameState} frameState Frame state.
+ */
+ol.control.Attribution.prototype.updateElement_ = function(frameState) {
+
+  if (!frameState) {
+    if (this.renderedVisible_) {
+      this.element.style.display = 'none';
+      this.renderedVisible_ = false;
+    }
+    return;
+  }
+
+  var attributions = this.getSourceAttributions(frameState);
+  /** @type {Object.<string, ol.Attribution>} */
+  var visibleAttributions = attributions[0];
+  /** @type {Object.<string, ol.Attribution>} */
+  var hiddenAttributions = attributions[1];
+
+  var attributionElement, attributionKey;
+  for (attributionKey in this.attributionElements_) {
+    if (attributionKey in visibleAttributions) {
+      if (!this.attributionElementRenderedVisible_[attributionKey]) {
+        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 {
+      ol.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 =
+      !ol.object.isEmpty(this.attributionElementRenderedVisible_) ||
+      !ol.object.isEmpty(frameState.logos);
+  if (this.renderedVisible_ != renderVisible) {
+    this.element.style.display = renderVisible ? '' : 'none';
+    this.renderedVisible_ = renderVisible;
+  }
+  if (renderVisible &&
+      ol.object.isEmpty(this.attributionElementRenderedVisible_)) {
+    this.element.classList.add('ol-logo-only');
+  } else {
+    this.element.classList.remove('ol-logo-only');
+  }
+
+  this.insertLogos_(frameState);
+
+};
+
+
+/**
+ * @param {?olx.FrameState} frameState Frame state.
+ * @private
+ */
+ol.control.Attribution.prototype.insertLogos_ = function(frameState) {
+
+  var logo;
+  var logos = frameState.logos;
+  var logoElements = this.logoElements_;
+
+  for (logo in logoElements) {
+    if (!(logo in logos)) {
+      ol.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 = !ol.object.isEmpty(logos) ? '' : 'none';
+
+};
+
+
+/**
+ * @param {Event} event The event to handle
+ * @private
+ */
+ol.control.Attribution.prototype.handleClick_ = function(event) {
+  event.preventDefault();
+  this.handleToggle_();
+};
+
+
+/**
+ * @private
+ */
+ol.control.Attribution.prototype.handleToggle_ = function() {
+  this.element.classList.toggle('ol-collapsed');
+  if (this.collapsed_) {
+    ol.dom.replaceNode(this.collapseLabel_, this.label_);
+  } else {
+    ol.dom.replaceNode(this.label_, this.collapseLabel_);
+  }
+  this.collapsed_ = !this.collapsed_;
+};
+
+
+/**
+ * Return `true` if the attribution is collapsible, `false` otherwise.
+ * @return {boolean} True if the widget is collapsible.
+ * @api stable
+ */
+ol.control.Attribution.prototype.getCollapsible = function() {
+  return this.collapsible_;
+};
+
+
+/**
+ * Set whether the attribution should be collapsible.
+ * @param {boolean} collapsible True if the widget is collapsible.
+ * @api stable
+ */
+ol.control.Attribution.prototype.setCollapsible = function(collapsible) {
+  if (this.collapsible_ === collapsible) {
+    return;
+  }
+  this.collapsible_ = collapsible;
+  this.element.classList.toggle('ol-uncollapsible');
+  if (!collapsible && this.collapsed_) {
+    this.handleToggle_();
+  }
+};
+
+
+/**
+ * Collapse or expand the attribution according to the passed parameter. Will
+ * not do anything if the attribution isn't collapsible or if the current
+ * collapsed state is already the one requested.
+ * @param {boolean} collapsed True if the widget is collapsed.
+ * @api stable
+ */
+ol.control.Attribution.prototype.setCollapsed = function(collapsed) {
+  if (!this.collapsible_ || this.collapsed_ === collapsed) {
+    return;
+  }
+  this.handleToggle_();
+};
+
+
+/**
+ * Return `true` when the attribution is currently collapsed or `false`
+ * otherwise.
+ * @return {boolean} True if the widget is collapsed.
+ * @api stable
+ */
+ol.control.Attribution.prototype.getCollapsed = function() {
+  return this.collapsed_;
+};
+
+goog.provide('ol.control.Rotate');
+
+goog.require('ol.events');
+goog.require('ol.events.EventType');
+goog.require('ol');
+goog.require('ol.animation');
+goog.require('ol.control.Control');
+goog.require('ol.css');
+goog.require('ol.easing');
+
+
+/**
+ * @classdesc
+ * A button control to reset rotation to 0.
+ * To style this control use css selector `.ol-rotate`. A `.ol-hidden` css
+ * selector is added to the button when the rotation is 0.
+ *
+ * @constructor
+ * @extends {ol.control.Control}
+ * @param {olx.control.RotateOptions=} opt_options Rotate options.
+ * @api stable
+ */
+ol.control.Rotate = function(opt_options) {
+
+  var options = opt_options ? opt_options : {};
+
+  var className = options.className !== undefined ? options.className : 'ol-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 = 'ol-compass';
+    this.label_.textContent = label;
+  } else {
+    this.label_ = label;
+    this.label_.classList.add('ol-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_);
+
+  ol.events.listen(button, ol.events.EventType.CLICK,
+      ol.control.Rotate.prototype.handleClick_, this);
+
+  var cssClasses = className + ' ' + ol.css.CLASS_UNSELECTABLE + ' ' +
+      ol.css.CLASS_CONTROL;
+  var element = document.createElement('div');
+  element.className = cssClasses;
+  element.appendChild(button);
+
+  var render = options.render ? options.render : ol.control.Rotate.render;
+
+  this.callResetNorth_ = options.resetNorth ? options.resetNorth : undefined;
+
+  ol.control.Control.call(this, {
+    element: element,
+    render: render,
+    target: options.target
+  });
+
+  /**
+   * @type {number}
+   * @private
+   */
+  this.duration_ = options.duration !== undefined ? options.duration : 250;
+
+  /**
+   * @type {boolean}
+   * @private
+   */
+  this.autoHide_ = options.autoHide !== undefined ? options.autoHide : true;
+
+  /**
+   * @private
+   * @type {number|undefined}
+   */
+  this.rotation_ = undefined;
+
+  if (this.autoHide_) {
+    this.element.classList.add(ol.css.CLASS_HIDDEN);
+  }
+
+};
+ol.inherits(ol.control.Rotate, ol.control.Control);
+
+
+/**
+ * @param {Event} event The event to handle
+ * @private
+ */
+ol.control.Rotate.prototype.handleClick_ = function(event) {
+  event.preventDefault();
+  if (this.callResetNorth_ !== undefined) {
+    this.callResetNorth_();
+  } else {
+    this.resetNorth_();
+  }
+};
+
+
+/**
+ * @private
+ */
+ol.control.Rotate.prototype.resetNorth_ = function() {
+  var map = this.getMap();
+  var view = map.getView();
+  if (!view) {
+    // the map does not have a view, so we can't act
+    // upon it
+    return;
+  }
+  var currentRotation = view.getRotation();
+  if (currentRotation !== undefined) {
+    if (this.duration_ > 0) {
+      currentRotation = currentRotation % (2 * Math.PI);
+      if (currentRotation < -Math.PI) {
+        currentRotation += 2 * Math.PI;
+      }
+      if (currentRotation > Math.PI) {
+        currentRotation -= 2 * Math.PI;
+      }
+      map.beforeRender(ol.animation.rotate({
+        rotation: currentRotation,
+        duration: this.duration_,
+        easing: ol.easing.easeOut
+      }));
+    }
+    view.setRotation(0);
+  }
+};
+
+
+/**
+ * Update the rotate control element.
+ * @param {ol.MapEvent} mapEvent Map event.
+ * @this {ol.control.Rotate}
+ * @api
+ */
+ol.control.Rotate.render = function(mapEvent) {
+  var frameState = mapEvent.frameState;
+  if (!frameState) {
+    return;
+  }
+  var rotation = frameState.viewState.rotation;
+  if (rotation != this.rotation_) {
+    var transform = 'rotate(' + rotation + 'rad)';
+    if (this.autoHide_) {
+      var contains = this.element.classList.contains(ol.css.CLASS_HIDDEN);
+      if (!contains && rotation === 0) {
+        this.element.classList.add(ol.css.CLASS_HIDDEN);
+      } else if (contains && rotation !== 0) {
+        this.element.classList.remove(ol.css.CLASS_HIDDEN);
+      }
+    }
+    this.label_.style.msTransform = transform;
+    this.label_.style.webkitTransform = transform;
+    this.label_.style.transform = transform;
+  }
+  this.rotation_ = rotation;
+};
+
+goog.provide('ol.control.Zoom');
+
+goog.require('ol.events');
+goog.require('ol.events.EventType');
+goog.require('ol.animation');
+goog.require('ol.control.Control');
+goog.require('ol.css');
+goog.require('ol.easing');
+
+
+/**
+ * @classdesc
+ * A control with 2 buttons, one for zoom in and one for zoom out.
+ * This control is one of the default controls of a map. To style this control
+ * use css selectors `.ol-zoom-in` and `.ol-zoom-out`.
+ *
+ * @constructor
+ * @extends {ol.control.Control}
+ * @param {olx.control.ZoomOptions=} opt_options Zoom options.
+ * @api stable
+ */
+ol.control.Zoom = function(opt_options) {
+
+  var options = opt_options ? opt_options : {};
+
+  var className = options.className !== undefined ? options.className : 'ol-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
+  );
+
+  ol.events.listen(inElement, ol.events.EventType.CLICK,
+      ol.control.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
+  );
+
+  ol.events.listen(outElement, ol.events.EventType.CLICK,
+      ol.control.Zoom.prototype.handleClick_.bind(this, -delta));
+
+  var cssClasses = className + ' ' + ol.css.CLASS_UNSELECTABLE + ' ' +
+      ol.css.CLASS_CONTROL;
+  var element = document.createElement('div');
+  element.className = cssClasses;
+  element.appendChild(inElement);
+  element.appendChild(outElement);
+
+  ol.control.Control.call(this, {
+    element: element,
+    target: options.target
+  });
+
+  /**
+   * @type {number}
+   * @private
+   */
+  this.duration_ = options.duration !== undefined ? options.duration : 250;
+
+};
+ol.inherits(ol.control.Zoom, ol.control.Control);
+
+
+/**
+ * @param {number} delta Zoom delta.
+ * @param {Event} event The event to handle
+ * @private
+ */
+ol.control.Zoom.prototype.handleClick_ = function(delta, event) {
+  event.preventDefault();
+  this.zoomByDelta_(delta);
+};
+
+
+/**
+ * @param {number} delta Zoom delta.
+ * @private
+ */
+ol.control.Zoom.prototype.zoomByDelta_ = function(delta) {
+  var map = this.getMap();
+  var view = map.getView();
+  if (!view) {
+    // the map does not have a view, so we can't act
+    // upon it
+    return;
+  }
+  var currentResolution = view.getResolution();
+  if (currentResolution) {
+    if (this.duration_ > 0) {
+      map.beforeRender(ol.animation.zoom({
+        resolution: currentResolution,
+        duration: this.duration_,
+        easing: ol.easing.easeOut
+      }));
+    }
+    var newResolution = view.constrainResolution(currentResolution, delta);
+    view.setResolution(newResolution);
+  }
+};
+
+goog.provide('ol.control');
+
+goog.require('ol');
+goog.require('ol.Collection');
+goog.require('ol.control.Attribution');
+goog.require('ol.control.Rotate');
+goog.require('ol.control.Zoom');
+
+
+/**
+ * Set of controls included in maps by default. Unless configured otherwise,
+ * this returns a collection containing an instance of each of the following
+ * controls:
+ * * {@link ol.control.Zoom}
+ * * {@link ol.control.Rotate}
+ * * {@link ol.control.Attribution}
+ *
+ * @param {olx.control.DefaultsOptions=} opt_options Defaults options.
+ * @return {ol.Collection.<ol.control.Control>} Controls.
+ * @api stable
+ */
+ol.control.defaults = function(opt_options) {
+
+  var options = opt_options ? opt_options : {};
+
+  var controls = new ol.Collection();
+
+  var zoomControl = options.zoom !== undefined ? options.zoom : true;
+  if (zoomControl) {
+    controls.push(new ol.control.Zoom(options.zoomOptions));
+  }
+
+  var rotateControl = options.rotate !== undefined ? options.rotate : true;
+  if (rotateControl) {
+    controls.push(new ol.control.Rotate(options.rotateOptions));
+  }
+
+  var attributionControl = options.attribution !== undefined ?
+      options.attribution : true;
+  if (attributionControl) {
+    controls.push(new ol.control.Attribution(options.attributionOptions));
+  }
+
+  return controls;
+
+};
+
+goog.provide('ol.control.FullScreen');
+
+goog.require('goog.asserts');
+goog.require('ol.events');
+goog.require('ol.events.EventType');
+goog.require('ol');
+goog.require('ol.control.Control');
+goog.require('ol.dom');
+goog.require('ol.css');
+
+
+/**
+ * @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
+ * overriden 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 {ol.control.Control}
+ * @param {olx.control.FullScreenOptions=} opt_options Options.
+ * @api stable
+ */
+ol.control.FullScreen = function(opt_options) {
+
+  var options = opt_options ? opt_options : {};
+
+  /**
+   * @private
+   * @type {string}
+   */
+  this.cssClassName_ = options.className !== undefined ? options.className :
+      'ol-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_ + '-' + ol.control.FullScreen.isFullScreen();
+  button.setAttribute('type', 'button');
+  button.title = tipLabel;
+  button.appendChild(this.labelNode_);
+
+  ol.events.listen(button, ol.events.EventType.CLICK,
+      this.handleClick_, this);
+
+  var cssClasses = this.cssClassName_ + ' ' + ol.css.CLASS_UNSELECTABLE +
+      ' ' + ol.css.CLASS_CONTROL + ' ' +
+      (!ol.control.FullScreen.isFullScreenSupported() ? ol.css.CLASS_UNSUPPORTED : '');
+  var element = document.createElement('div');
+  element.className = cssClasses;
+  element.appendChild(button);
+
+  ol.control.Control.call(this, {
+    element: element,
+    target: options.target
+  });
+
+  /**
+   * @private
+   * @type {boolean}
+   */
+  this.keys_ = options.keys !== undefined ? options.keys : false;
+
+  /**
+   * @private
+   * @type {Element|string|undefined}
+   */
+  this.source_ = options.source;
+
+};
+ol.inherits(ol.control.FullScreen, ol.control.Control);
+
+
+/**
+ * @param {Event} event The event to handle
+ * @private
+ */
+ol.control.FullScreen.prototype.handleClick_ = function(event) {
+  event.preventDefault();
+  this.handleFullScreen_();
+};
+
+
+/**
+ * @private
+ */
+ol.control.FullScreen.prototype.handleFullScreen_ = function() {
+  if (!ol.control.FullScreen.isFullScreenSupported()) {
+    return;
+  }
+  var map = this.getMap();
+  if (!map) {
+    return;
+  }
+  if (ol.control.FullScreen.isFullScreen()) {
+    ol.control.FullScreen.exitFullScreen();
+  } else {
+    var element;
+    if (this.source_) {
+      element = typeof this.source_ === 'string' ?
+        document.getElementById(this.source_) :
+        this.source_;
+    } else {
+      element = map.getTargetElement();
+    }
+    goog.asserts.assert(element, 'element should be defined');
+    if (this.keys_) {
+      ol.control.FullScreen.requestFullScreenWithKeys(element);
+
+    } else {
+      ol.control.FullScreen.requestFullScreen(element);
+    }
+  }
+};
+
+
+/**
+ * @private
+ */
+ol.control.FullScreen.prototype.handleFullScreenChange_ = function() {
+  var button = this.element.firstElementChild;
+  var map = this.getMap();
+  if (ol.control.FullScreen.isFullScreen()) {
+    button.className = this.cssClassName_ + '-true';
+    ol.dom.replaceNode(this.labelActiveNode_, this.labelNode_);
+  } else {
+    button.className = this.cssClassName_ + '-false';
+    ol.dom.replaceNode(this.labelNode_, this.labelActiveNode_);
+  }
+  if (map) {
+    map.updateSize();
+  }
+};
+
+
+/**
+ * @inheritDoc
+ * @api stable
+ */
+ol.control.FullScreen.prototype.setMap = function(map) {
+  ol.control.Control.prototype.setMap.call(this, map);
+  if (map) {
+    this.listenerKeys.push(ol.events.listen(ol.global.document,
+        ol.control.FullScreen.getChangeType_(),
+        this.handleFullScreenChange_, this)
+    );
+  }
+};
+
+/**
+ * @return {boolean} Fullscreen is supported by the current platform.
+ */
+ol.control.FullScreen.isFullScreenSupported = function() {
+  var body = document.body;
+  return !!(
+    body.webkitRequestFullscreen ||
+    (body.mozRequestFullScreen && document.mozFullScreenEnabled) ||
+    (body.msRequestFullscreen && document.msFullscreenEnabled) ||
+    (body.requestFullscreen && document.fullscreenEnabled)
+  );
+};
+
+/**
+ * @return {boolean} Element is currently in fullscreen.
+ */
+ol.control.FullScreen.isFullScreen = function() {
+  return !!(
+    document.webkitIsFullScreen || document.mozFullScreen ||
+    document.msFullscreenElement || document.fullscreenElement
+  );
+};
+
+/**
+ * Request to fullscreen an element.
+ * @param {Node} element Element to request fullscreen
+ */
+ol.control.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
+ */
+ol.control.FullScreen.requestFullScreenWithKeys = function(element) {
+  if (element.mozRequestFullScreenWithKeys) {
+    element.mozRequestFullScreenWithKeys();
+  } else if (element.webkitRequestFullscreen) {
+    element.webkitRequestFullscreen(Element.ALLOW_KEYBOARD_INPUT);
+  } else {
+    ol.control.FullScreen.requestFullScreen(element);
+  }
+};
+
+/**
+ * Exit fullscreen.
+ */
+ol.control.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
+ */
+ol.control.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('ol.control.MousePosition');
+
+goog.require('ol.events');
+goog.require('ol.events.EventType');
+goog.require('ol.Object');
+goog.require('ol.control.Control');
+goog.require('ol.proj');
+goog.require('ol.proj.Projection');
+
+
+/**
+ * @enum {string}
+ */
+ol.control.MousePositionProperty = {
+  PROJECTION: 'projection',
+  COORDINATE_FORMAT: 'coordinateFormat'
+};
+
+
+/**
+ * @classdesc
+ * A control to show the 2D coordinates of the mouse cursor. By default, these
+ * are in the view projection, but can be in any supported projection.
+ * By default the control is shown in the top right corner of the map, but this
+ * can be changed by using the css selector `.ol-mouse-position`.
+ *
+ * @constructor
+ * @extends {ol.control.Control}
+ * @param {olx.control.MousePositionOptions=} opt_options Mouse position
+ *     options.
+ * @api stable
+ */
+ol.control.MousePosition = function(opt_options) {
+
+  var options = opt_options ? opt_options : {};
+
+  var element = document.createElement('DIV');
+  element.className = options.className !== undefined ? options.className : 'ol-mouse-position';
+
+  var render = options.render ?
+      options.render : ol.control.MousePosition.render;
+
+  ol.control.Control.call(this, {
+    element: element,
+    render: render,
+    target: options.target
+  });
+
+  ol.events.listen(this,
+      ol.Object.getChangeEventType(ol.control.MousePositionProperty.PROJECTION),
+      this.handleProjectionChanged_, this);
+
+  if (options.coordinateFormat) {
+    this.setCoordinateFormat(options.coordinateFormat);
+  }
+  if (options.projection) {
+    this.setProjection(ol.proj.get(options.projection));
+  }
+
+  /**
+   * @private
+   * @type {string}
+   */
+  this.undefinedHTML_ = options.undefinedHTML !== undefined ? options.undefinedHTML : '';
+
+  /**
+   * @private
+   * @type {string}
+   */
+  this.renderedHTML_ = element.innerHTML;
+
+  /**
+   * @private
+   * @type {ol.proj.Projection}
+   */
+  this.mapProjection_ = null;
+
+  /**
+   * @private
+   * @type {?ol.TransformFunction}
+   */
+  this.transform_ = null;
+
+  /**
+   * @private
+   * @type {ol.Pixel}
+   */
+  this.lastMouseMovePixel_ = null;
+
+};
+ol.inherits(ol.control.MousePosition, ol.control.Control);
+
+
+/**
+ * Update the mouseposition element.
+ * @param {ol.MapEvent} mapEvent Map event.
+ * @this {ol.control.MousePosition}
+ * @api
+ */
+ol.control.MousePosition.render = function(mapEvent) {
+  var frameState = mapEvent.frameState;
+  if (!frameState) {
+    this.mapProjection_ = null;
+  } else {
+    if (this.mapProjection_ != frameState.viewState.projection) {
+      this.mapProjection_ = frameState.viewState.projection;
+      this.transform_ = null;
+    }
+  }
+  this.updateHTML_(this.lastMouseMovePixel_);
+};
+
+
+/**
+ * @private
+ */
+ol.control.MousePosition.prototype.handleProjectionChanged_ = function() {
+  this.transform_ = null;
+};
+
+
+/**
+ * Return the coordinate format type used to render the current position or
+ * undefined.
+ * @return {ol.CoordinateFormatType|undefined} The format to render the current
+ *     position in.
+ * @observable
+ * @api stable
+ */
+ol.control.MousePosition.prototype.getCoordinateFormat = function() {
+  return /** @type {ol.CoordinateFormatType|undefined} */ (
+      this.get(ol.control.MousePositionProperty.COORDINATE_FORMAT));
+};
+
+
+/**
+ * Return the projection that is used to report the mouse position.
+ * @return {ol.proj.Projection|undefined} The projection to report mouse
+ *     position in.
+ * @observable
+ * @api stable
+ */
+ol.control.MousePosition.prototype.getProjection = function() {
+  return /** @type {ol.proj.Projection|undefined} */ (
+      this.get(ol.control.MousePositionProperty.PROJECTION));
+};
+
+
+/**
+ * @param {Event} event Browser event.
+ * @protected
+ */
+ol.control.MousePosition.prototype.handleMouseMove = function(event) {
+  var map = this.getMap();
+  this.lastMouseMovePixel_ = map.getEventPixel(event);
+  this.updateHTML_(this.lastMouseMovePixel_);
+};
+
+
+/**
+ * @param {Event} event Browser event.
+ * @protected
+ */
+ol.control.MousePosition.prototype.handleMouseOut = function(event) {
+  this.updateHTML_(null);
+  this.lastMouseMovePixel_ = null;
+};
+
+
+/**
+ * @inheritDoc
+ * @api stable
+ */
+ol.control.MousePosition.prototype.setMap = function(map) {
+  ol.control.Control.prototype.setMap.call(this, map);
+  if (map) {
+    var viewport = map.getViewport();
+    this.listenerKeys.push(
+        ol.events.listen(viewport, ol.events.EventType.MOUSEMOVE,
+            this.handleMouseMove, this),
+        ol.events.listen(viewport, ol.events.EventType.MOUSEOUT,
+            this.handleMouseOut, this)
+    );
+  }
+};
+
+
+/**
+ * Set the coordinate format type used to render the current position.
+ * @param {ol.CoordinateFormatType} format The format to render the current
+ *     position in.
+ * @observable
+ * @api stable
+ */
+ol.control.MousePosition.prototype.setCoordinateFormat = function(format) {
+  this.set(ol.control.MousePositionProperty.COORDINATE_FORMAT, format);
+};
+
+
+/**
+ * Set the projection that is used to report the mouse position.
+ * @param {ol.proj.Projection} projection The projection to report mouse
+ *     position in.
+ * @observable
+ * @api stable
+ */
+ol.control.MousePosition.prototype.setProjection = function(projection) {
+  this.set(ol.control.MousePositionProperty.PROJECTION, projection);
+};
+
+
+/**
+ * @param {?ol.Pixel} pixel Pixel.
+ * @private
+ */
+ol.control.MousePosition.prototype.updateHTML_ = function(pixel) {
+  var html = this.undefinedHTML_;
+  if (pixel && this.mapProjection_) {
+    if (!this.transform_) {
+      var projection = this.getProjection();
+      if (projection) {
+        this.transform_ = ol.proj.getTransformFromProjections(
+            this.mapProjection_, projection);
+      } else {
+        this.transform_ = ol.proj.identityTransform;
+      }
+    }
+    var map = this.getMap();
+    var coordinate = map.getCoordinateFromPixel(pixel);
+    if (coordinate) {
+      this.transform_(coordinate, coordinate);
+      var coordinateFormat = this.getCoordinateFormat();
+      if (coordinateFormat) {
+        html = coordinateFormat(coordinate);
+      } else {
+        html = coordinate.toString();
+      }
+    }
+  }
+  if (!this.renderedHTML_ || html != this.renderedHTML_) {
+    this.element.innerHTML = html;
+    this.renderedHTML_ = html;
+  }
+};
+
+// Copyright 2010 The Closure Library Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS-IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+/**
+ * @fileoverview A global registry for entry points into a program,
+ * so that they can be instrumented. Each module should register their
+ * entry points with this registry. Designed to be compiled out
+ * if no instrumentation is requested.
+ *
+ * Entry points may be registered before or after a call to
+ * goog.debug.entryPointRegistry.monitorAll. If an entry point is registered
+ * later, the existing monitor will instrument the new entry point.
+ *
+ * @author nicksantos@google.com (Nick Santos)
+ */
+
+goog.provide('goog.debug.EntryPointMonitor');
+goog.provide('goog.debug.entryPointRegistry');
+
+goog.require('goog.asserts');
+
+
+
+/**
+ * @interface
+ */
+goog.debug.EntryPointMonitor = function() {};
+
+
+/**
+ * Instruments a function.
+ *
+ * @param {!Function} fn A function to instrument.
+ * @return {!Function} The instrumented function.
+ */
+goog.debug.EntryPointMonitor.prototype.wrap;
+
+
+/**
+ * Try to remove an instrumentation wrapper created by this monitor.
+ * If the function passed to unwrap is not a wrapper created by this
+ * monitor, then we will do nothing.
+ *
+ * Notice that some wrappers may not be unwrappable. For example, if other
+ * monitors have applied their own wrappers, then it will be impossible to
+ * unwrap them because their wrappers will have captured our wrapper.
+ *
+ * So it is important that entry points are unwrapped in the reverse
+ * order that they were wrapped.
+ *
+ * @param {!Function} fn A function to unwrap.
+ * @return {!Function} The unwrapped function, or {@code fn} if it was not
+ *     a wrapped function created by this monitor.
+ */
+goog.debug.EntryPointMonitor.prototype.unwrap;
+
+
+/**
+ * An array of entry point callbacks.
+ * @type {!Array<function(!Function)>}
+ * @private
+ */
+goog.debug.entryPointRegistry.refList_ = [];
+
+
+/**
+ * Monitors that should wrap all the entry points.
+ * @type {!Array<!goog.debug.EntryPointMonitor>}
+ * @private
+ */
+goog.debug.entryPointRegistry.monitors_ = [];
+
+
+/**
+ * Whether goog.debug.entryPointRegistry.monitorAll has ever been called.
+ * Checking this allows the compiler to optimize out the registrations.
+ * @type {boolean}
+ * @private
+ */
+goog.debug.entryPointRegistry.monitorsMayExist_ = false;
+
+
+/**
+ * Register an entry point with this module.
+ *
+ * The entry point will be instrumented when a monitor is passed to
+ * goog.debug.entryPointRegistry.monitorAll. If this has already occurred, the
+ * entry point is instrumented immediately.
+ *
+ * @param {function(!Function)} callback A callback function which is called
+ *     with a transforming function to instrument the entry point. The callback
+ *     is responsible for wrapping the relevant entry point with the
+ *     transforming function.
+ */
+goog.debug.entryPointRegistry.register = function(callback) {
+  // Don't use push(), so that this can be compiled out.
+  goog.debug.entryPointRegistry
+      .refList_[goog.debug.entryPointRegistry.refList_.length] = callback;
+  // If no one calls monitorAll, this can be compiled out.
+  if (goog.debug.entryPointRegistry.monitorsMayExist_) {
+    var monitors = goog.debug.entryPointRegistry.monitors_;
+    for (var i = 0; i < monitors.length; i++) {
+      callback(goog.bind(monitors[i].wrap, monitors[i]));
+    }
+  }
+};
+
+
+/**
+ * Configures a monitor to wrap all entry points.
+ *
+ * Entry points that have already been registered are immediately wrapped by
+ * the monitor. When an entry point is registered in the future, it will also
+ * be wrapped by the monitor when it is registered.
+ *
+ * @param {!goog.debug.EntryPointMonitor} monitor An entry point monitor.
+ */
+goog.debug.entryPointRegistry.monitorAll = function(monitor) {
+  goog.debug.entryPointRegistry.monitorsMayExist_ = true;
+  var transformer = goog.bind(monitor.wrap, monitor);
+  for (var i = 0; i < goog.debug.entryPointRegistry.refList_.length; i++) {
+    goog.debug.entryPointRegistry.refList_[i](transformer);
+  }
+  goog.debug.entryPointRegistry.monitors_.push(monitor);
+};
+
+
+/**
+ * Try to unmonitor all the entry points that have already been registered. If
+ * an entry point is registered in the future, it will not be wrapped by the
+ * monitor when it is registered. Note that this may fail if the entry points
+ * have additional wrapping.
+ *
+ * @param {!goog.debug.EntryPointMonitor} monitor The last monitor to wrap
+ *     the entry points.
+ * @throws {Error} If the monitor is not the most recently configured monitor.
+ */
+goog.debug.entryPointRegistry.unmonitorAllIfPossible = function(monitor) {
+  var monitors = goog.debug.entryPointRegistry.monitors_;
+  goog.asserts.assert(
+      monitor == monitors[monitors.length - 1],
+      'Only the most recent monitor can be unwrapped.');
+  var transformer = goog.bind(monitor.unwrap, monitor);
+  for (var i = 0; i < goog.debug.entryPointRegistry.refList_.length; i++) {
+    goog.debug.entryPointRegistry.refList_[i](transformer);
+  }
+  monitors.length--;
+};
+
+// Copyright 2007 The Closure Library Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS-IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+/**
+ * @fileoverview Defines the goog.dom.TagName enum.  This enumerates
+ * all HTML tag names specified in either the the W3C HTML 4.01 index of
+ * elements or the HTML5 draft specification.
+ *
+ * References:
+ * http://www.w3.org/TR/html401/index/elements.html
+ * http://dev.w3.org/html5/spec/section-index.html
+ *
+ */
+goog.provide('goog.dom.TagName');
+
+
+/**
+ * Enum of all html tag names specified by the W3C HTML4.01 and HTML5
+ * specifications.
+ * @enum {string}
+ */
+goog.dom.TagName = {
+  A: 'A',
+  ABBR: 'ABBR',
+  ACRONYM: 'ACRONYM',
+  ADDRESS: 'ADDRESS',
+  APPLET: 'APPLET',
+  AREA: 'AREA',
+  ARTICLE: 'ARTICLE',
+  ASIDE: 'ASIDE',
+  AUDIO: 'AUDIO',
+  B: 'B',
+  BASE: 'BASE',
+  BASEFONT: 'BASEFONT',
+  BDI: 'BDI',
+  BDO: 'BDO',
+  BIG: 'BIG',
+  BLOCKQUOTE: 'BLOCKQUOTE',
+  BODY: 'BODY',
+  BR: 'BR',
+  BUTTON: 'BUTTON',
+  CANVAS: 'CANVAS',
+  CAPTION: 'CAPTION',
+  CENTER: 'CENTER',
+  CITE: 'CITE',
+  CODE: 'CODE',
+  COL: 'COL',
+  COLGROUP: 'COLGROUP',
+  COMMAND: 'COMMAND',
+  DATA: 'DATA',
+  DATALIST: 'DATALIST',
+  DD: 'DD',
+  DEL: 'DEL',
+  DETAILS: 'DETAILS',
+  DFN: 'DFN',
+  DIALOG: 'DIALOG',
+  DIR: 'DIR',
+  DIV: 'DIV',
+  DL: 'DL',
+  DT: 'DT',
+  EM: 'EM',
+  EMBED: 'EMBED',
+  FIELDSET: 'FIELDSET',
+  FIGCAPTION: 'FIGCAPTION',
+  FIGURE: 'FIGURE',
+  FONT: 'FONT',
+  FOOTER: 'FOOTER',
+  FORM: 'FORM',
+  FRAME: 'FRAME',
+  FRAMESET: 'FRAMESET',
+  H1: 'H1',
+  H2: 'H2',
+  H3: 'H3',
+  H4: 'H4',
+  H5: 'H5',
+  H6: 'H6',
+  HEAD: 'HEAD',
+  HEADER: 'HEADER',
+  HGROUP: 'HGROUP',
+  HR: 'HR',
+  HTML: 'HTML',
+  I: 'I',
+  IFRAME: 'IFRAME',
+  IMG: 'IMG',
+  INPUT: 'INPUT',
+  INS: 'INS',
+  ISINDEX: 'ISINDEX',
+  KBD: 'KBD',
+  KEYGEN: 'KEYGEN',
+  LABEL: 'LABEL',
+  LEGEND: 'LEGEND',
+  LI: 'LI',
+  LINK: 'LINK',
+  MAP: 'MAP',
+  MARK: 'MARK',
+  MATH: 'MATH',
+  MENU: 'MENU',
+  META: 'META',
+  METER: 'METER',
+  NAV: 'NAV',
+  NOFRAMES: 'NOFRAMES',
+  NOSCRIPT: 'NOSCRIPT',
+  OBJECT: 'OBJECT',
+  OL: 'OL',
+  OPTGROUP: 'OPTGROUP',
+  OPTION: 'OPTION',
+  OUTPUT: 'OUTPUT',
+  P: 'P',
+  PARAM: 'PARAM',
+  PRE: 'PRE',
+  PROGRESS: 'PROGRESS',
+  Q: 'Q',
+  RP: 'RP',
+  RT: 'RT',
+  RUBY: 'RUBY',
+  S: 'S',
+  SAMP: 'SAMP',
+  SCRIPT: 'SCRIPT',
+  SECTION: 'SECTION',
+  SELECT: 'SELECT',
+  SMALL: 'SMALL',
+  SOURCE: 'SOURCE',
+  SPAN: 'SPAN',
+  STRIKE: 'STRIKE',
+  STRONG: 'STRONG',
+  STYLE: 'STYLE',
+  SUB: 'SUB',
+  SUMMARY: 'SUMMARY',
+  SUP: 'SUP',
+  SVG: 'SVG',
+  TABLE: 'TABLE',
+  TBODY: 'TBODY',
+  TD: 'TD',
+  TEMPLATE: 'TEMPLATE',
+  TEXTAREA: 'TEXTAREA',
+  TFOOT: 'TFOOT',
+  TH: 'TH',
+  THEAD: 'THEAD',
+  TIME: 'TIME',
+  TITLE: 'TITLE',
+  TR: 'TR',
+  TRACK: 'TRACK',
+  TT: 'TT',
+  U: 'U',
+  UL: 'UL',
+  VAR: 'VAR',
+  VIDEO: 'VIDEO',
+  WBR: 'WBR'
+};
+
+// Copyright 2008 The Closure Library Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS-IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+/**
+ * @fileoverview Utilities for creating functions. Loosely inspired by the
+ * java classes: http://goo.gl/GM0Hmu and http://goo.gl/6k7nI8.
+ *
+ * @author nicksantos@google.com (Nick Santos)
+ */
+
+
+goog.provide('goog.functions');
+
+
+/**
+ * Creates a function that always returns the same value.
+ * @param {T} retValue The value to return.
+ * @return {function():T} The new function.
+ * @template T
+ */
+goog.functions.constant = function(retValue) {
+  return function() { return retValue; };
+};
+
+
+/**
+ * Always returns false.
+ * @type {function(...): boolean}
+ */
+goog.functions.FALSE = goog.functions.constant(false);
+
+
+/**
+ * Always returns true.
+ * @type {function(...): boolean}
+ */
+goog.functions.TRUE = goog.functions.constant(true);
+
+
+/**
+ * Always returns NULL.
+ * @type {function(...): null}
+ */
+goog.functions.NULL = goog.functions.constant(null);
+
+
+/**
+ * A simple function that returns the first argument of whatever is passed
+ * into it.
+ * @param {T=} opt_returnValue The single value that will be returned.
+ * @param {...*} var_args Optional trailing arguments. These are ignored.
+ * @return {T} The first argument passed in, or undefined if nothing was passed.
+ * @template T
+ */
+goog.functions.identity = function(opt_returnValue, var_args) {
+  return opt_returnValue;
+};
+
+
+/**
+ * Creates a function that always throws an error with the given message.
+ * @param {string} message The error message.
+ * @return {!Function} The error-throwing function.
+ */
+goog.functions.error = function(message) {
+  return function() { throw Error(message); };
+};
+
+
+/**
+ * Creates a function that throws the given object.
+ * @param {*} err An object to be thrown.
+ * @return {!Function} The error-throwing function.
+ */
+goog.functions.fail = function(err) {
+  return function() { throw err; };
+};
+
+
+/**
+ * Given a function, create a function that keeps opt_numArgs arguments and
+ * silently discards all additional arguments.
+ * @param {Function} f The original function.
+ * @param {number=} opt_numArgs The number of arguments to keep. Defaults to 0.
+ * @return {!Function} A version of f that only keeps the first opt_numArgs
+ *     arguments.
+ */
+goog.functions.lock = function(f, opt_numArgs) {
+  opt_numArgs = opt_numArgs || 0;
+  return function() {
+    return f.apply(this, Array.prototype.slice.call(arguments, 0, opt_numArgs));
+  };
+};
+
+
+/**
+ * Creates a function that returns its nth argument.
+ * @param {number} n The position of the return argument.
+ * @return {!Function} A new function.
+ */
+goog.functions.nth = function(n) {
+  return function() { return arguments[n]; };
+};
+
+
+/**
+ * Like goog.partial(), except that arguments are added after arguments to the
+ * returned function.
+ *
+ * Usage:
+ * function f(arg1, arg2, arg3, arg4) { ... }
+ * var g = goog.functions.partialRight(f, arg3, arg4);
+ * g(arg1, arg2);
+ *
+ * @param {!Function} fn A function to partially apply.
+ * @param {...*} var_args Additional arguments that are partially applied to fn
+ *     at the end.
+ * @return {!Function} A partially-applied form of the function goog.partial()
+ *     was invoked as a method of.
+ */
+goog.functions.partialRight = function(fn, var_args) {
+  var rightArgs = Array.prototype.slice.call(arguments, 1);
+  return function() {
+    var newArgs = Array.prototype.slice.call(arguments);
+    newArgs.push.apply(newArgs, rightArgs);
+    return fn.apply(this, newArgs);
+  };
+};
+
+
+/**
+ * Given a function, create a new function that swallows its return value
+ * and replaces it with a new one.
+ * @param {Function} f A function.
+ * @param {T} retValue A new return value.
+ * @return {function(...?):T} A new function.
+ * @template T
+ */
+goog.functions.withReturnValue = function(f, retValue) {
+  return goog.functions.sequence(f, goog.functions.constant(retValue));
+};
+
+
+/**
+ * Creates a function that returns whether its argument equals the given value.
+ *
+ * Example:
+ * var key = goog.object.findKey(obj, goog.functions.equalTo('needle'));
+ *
+ * @param {*} value The value to compare to.
+ * @param {boolean=} opt_useLooseComparison Whether to use a loose (==)
+ *     comparison rather than a strict (===) one. Defaults to false.
+ * @return {function(*):boolean} The new function.
+ */
+goog.functions.equalTo = function(value, opt_useLooseComparison) {
+  return function(other) {
+    return opt_useLooseComparison ? (value == other) : (value === other);
+  };
+};
+
+
+/**
+ * Creates the composition of the functions passed in.
+ * For example, (goog.functions.compose(f, g))(a) is equivalent to f(g(a)).
+ * @param {function(...?):T} fn The final function.
+ * @param {...Function} var_args A list of functions.
+ * @return {function(...?):T} The composition of all inputs.
+ * @template T
+ */
+goog.functions.compose = function(fn, var_args) {
+  var functions = arguments;
+  var length = functions.length;
+  return function() {
+    var result;
+    if (length) {
+      result = functions[length - 1].apply(this, arguments);
+    }
+
+    for (var i = length - 2; i >= 0; i--) {
+      result = functions[i].call(this, result);
+    }
+    return result;
+  };
+};
+
+
+/**
+ * Creates a function that calls the functions passed in in sequence, and
+ * returns the value of the last function. For example,
+ * (goog.functions.sequence(f, g))(x) is equivalent to f(x),g(x).
+ * @param {...Function} var_args A list of functions.
+ * @return {!Function} A function that calls all inputs in sequence.
+ */
+goog.functions.sequence = function(var_args) {
+  var functions = arguments;
+  var length = functions.length;
+  return function() {
+    var result;
+    for (var i = 0; i < length; i++) {
+      result = functions[i].apply(this, arguments);
+    }
+    return result;
+  };
+};
+
+
+/**
+ * Creates a function that returns true if each of its components evaluates
+ * to true. The components are evaluated in order, and the evaluation will be
+ * short-circuited as soon as a function returns false.
+ * For example, (goog.functions.and(f, g))(x) is equivalent to f(x) && g(x).
+ * @param {...Function} var_args A list of functions.
+ * @return {function(...?):boolean} A function that ANDs its component
+ *      functions.
+ */
+goog.functions.and = function(var_args) {
+  var functions = arguments;
+  var length = functions.length;
+  return function() {
+    for (var i = 0; i < length; i++) {
+      if (!functions[i].apply(this, arguments)) {
+        return false;
+      }
+    }
+    return true;
+  };
+};
+
+
+/**
+ * Creates a function that returns true if any of its components evaluates
+ * to true. The components are evaluated in order, and the evaluation will be
+ * short-circuited as soon as a function returns true.
+ * For example, (goog.functions.or(f, g))(x) is equivalent to f(x) || g(x).
+ * @param {...Function} var_args A list of functions.
+ * @return {function(...?):boolean} A function that ORs its component
+ *    functions.
+ */
+goog.functions.or = function(var_args) {
+  var functions = arguments;
+  var length = functions.length;
+  return function() {
+    for (var i = 0; i < length; i++) {
+      if (functions[i].apply(this, arguments)) {
+        return true;
+      }
+    }
+    return false;
+  };
+};
+
+
+/**
+ * Creates a function that returns the Boolean opposite of a provided function.
+ * For example, (goog.functions.not(f))(x) is equivalent to !f(x).
+ * @param {!Function} f The original function.
+ * @return {function(...?):boolean} A function that delegates to f and returns
+ * opposite.
+ */
+goog.functions.not = function(f) {
+  return function() { return !f.apply(this, arguments); };
+};
+
+
+/**
+ * Generic factory function to construct an object given the constructor
+ * and the arguments. Intended to be bound to create object factories.
+ *
+ * Example:
+ *
+ * var factory = goog.partial(goog.functions.create, Class);
+ *
+ * @param {function(new:T, ...)} constructor The constructor for the Object.
+ * @param {...*} var_args The arguments to be passed to the constructor.
+ * @return {T} A new instance of the class given in {@code constructor}.
+ * @template T
+ */
+goog.functions.create = function(constructor, var_args) {
+  /**
+   * @constructor
+   * @final
+   */
+  var temp = function() {};
+  temp.prototype = constructor.prototype;
+
+  // obj will have constructor's prototype in its chain and
+  // 'obj instanceof constructor' will be true.
+  var obj = new temp();
+
+  // obj is initialized by constructor.
+  // arguments is only array-like so lacks shift(), but can be used with
+  // the Array prototype function.
+  constructor.apply(obj, Array.prototype.slice.call(arguments, 1));
+  return obj;
+};
+
+
+/**
+ * @define {boolean} Whether the return value cache should be used.
+ *    This should only be used to disable caches when testing.
+ */
+goog.define('goog.functions.CACHE_RETURN_VALUE', true);
+
+
+/**
+ * Gives a wrapper function that caches the return value of a parameterless
+ * function when first called.
+ *
+ * When called for the first time, the given function is called and its
+ * return value is cached (thus this is only appropriate for idempotent
+ * functions).  Subsequent calls will return the cached return value. This
+ * allows the evaluation of expensive functions to be delayed until first used.
+ *
+ * To cache the return values of functions with parameters, see goog.memoize.
+ *
+ * @param {!function():T} fn A function to lazily evaluate.
+ * @return {!function():T} A wrapped version the function.
+ * @template T
+ */
+goog.functions.cacheReturnValue = function(fn) {
+  var called = false;
+  var value;
+
+  return function() {
+    if (!goog.functions.CACHE_RETURN_VALUE) {
+      return fn();
+    }
+
+    if (!called) {
+      value = fn();
+      called = true;
+    }
+
+    return value;
+  };
+};
+
+
+/**
+ * Wraps a function to allow it to be called, at most, once. All
+ * additional calls are no-ops.
+ *
+ * This is particularly useful for initialization functions
+ * that should be called, at most, once.
+ *
+ * @param {function():*} f Function to call.
+ * @return {function():undefined} Wrapped function.
+ */
+goog.functions.once = function(f) {
+  // Keep a reference to the function that we null out when we're done with
+  // it -- that way, the function can be GC'd when we're done with it.
+  var inner = f;
+  return function() {
+    if (inner) {
+      var tmp = inner;
+      inner = null;
+      tmp();
+    }
+  };
+};
+
+
+/**
+ * Wraps a function to allow it to be called, at most, once for each sequence of
+ * calls fired repeatedly so long as they are fired less than a specified
+ * interval apart (in milliseconds). Whether it receives one signal or multiple,
+ * it will always wait until a full interval has elapsed since the last signal
+ * before performing the action, passing the arguments from the last call of the
+ * debouncing decorator into the decorated function.
+ *
+ * This is particularly useful for bulking up repeated user actions (e.g. only
+ * refreshing a view once a user finishes typing rather than updating with every
+ * keystroke). For more stateful debouncing with support for pausing, resuming,
+ * and canceling debounced actions, use {@code goog.async.Debouncer}.
+ *
+ * @param {function(this:SCOPE, ...?)} f Function to call.
+ * @param {number} interval Interval over which to debounce. The function will
+ *     only be called after the full interval has elapsed since the last call.
+ * @param {SCOPE=} opt_scope Object in whose scope to call the function.
+ * @return {function(...?): undefined} Wrapped function.
+ * @template SCOPE
+ */
+goog.functions.debounce = function(f, interval, opt_scope) {
+  if (opt_scope) {
+    f = goog.bind(f, opt_scope);
+  }
+  var timeout = null;
+  return /** @type {function(...?)} */ (function(var_args) {
+    goog.global.clearTimeout(timeout);
+    var args = arguments;
+    timeout =
+        goog.global.setTimeout(function() { f.apply(null, args); }, interval);
+  });
+};
+
+
+/**
+ * Wraps a function to allow it to be called, at most, once per interval
+ * (specified in milliseconds). If it is called multiple times while it is
+ * waiting, it will only perform the action once at the end of the interval,
+ * passing the arguments from the last call of the throttling decorator into the
+ * decorated function.
+ *
+ * This is particularly useful for limiting repeated user requests (e.g.
+ * preventing a user from spamming a server with frequent view refreshes). For
+ * more stateful throttling with support for pausing, resuming, and canceling
+ * throttled actions, use {@code goog.async.Throttle}.
+ *
+ * @param {function(this:SCOPE, ...?)} f Function to call.
+ * @param {number} interval Interval over which to throttle. The function can
+ *     only be called once per interval.
+ * @param {SCOPE=} opt_scope Object in whose scope to call the function.
+ * @return {function(...?): undefined} Wrapped function.
+ * @template SCOPE
+ */
+goog.functions.throttle = function(f, interval, opt_scope) {
+  if (opt_scope) {
+    f = goog.bind(f, opt_scope);
+  }
+  var timeout = null;
+  var shouldFire = false;
+  var args = [];
+
+  var handleTimeout = function() {
+    timeout = null;
+    if (shouldFire) {
+      shouldFire = false;
+      fire();
+    }
+  };
+
+  var fire = function() {
+    timeout = goog.global.setTimeout(handleTimeout, interval);
+    f.apply(null, args);
+  };
+
+  return /** @type {function(...?)} */ (function(var_args) {
+    args = arguments;
+    if (!timeout) {
+      fire();
+    } else {
+      shouldFire = true;
+    }
+  });
+};
+
+// Copyright 2013 The Closure Library Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS-IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+/**
+ * @fileoverview Provides a function to schedule running a function as soon
+ * as possible after the current JS execution stops and yields to the event
+ * loop.
+ *
+ */
+
+goog.provide('goog.async.nextTick');
+goog.provide('goog.async.throwException');
+
+goog.require('goog.debug.entryPointRegistry');
+goog.require('goog.dom.TagName');
+goog.require('goog.functions');
+goog.require('goog.labs.userAgent.browser');
+goog.require('goog.labs.userAgent.engine');
+
+
+/**
+ * Throw an item without interrupting the current execution context.  For
+ * example, if processing a group of items in a loop, sometimes it is useful
+ * to report an error while still allowing the rest of the batch to be
+ * processed.
+ * @param {*} exception
+ */
+goog.async.throwException = function(exception) {
+  // Each throw needs to be in its own context.
+  goog.global.setTimeout(function() { throw exception; }, 0);
+};
+
+
+/**
+ * Fires the provided callbacks as soon as possible after the current JS
+ * execution context. setTimeout(…, 0) takes at least 4ms when called from
+ * within another setTimeout(…, 0) for legacy reasons.
+ *
+ * This will not schedule the callback as a microtask (i.e. a task that can
+ * preempt user input or networking callbacks). It is meant to emulate what
+ * setTimeout(_, 0) would do if it were not throttled. If you desire microtask
+ * behavior, use {@see goog.Promise} instead.
+ *
+ * @param {function(this:SCOPE)} callback Callback function to fire as soon as
+ *     possible.
+ * @param {SCOPE=} opt_context Object in whose scope to call the listener.
+ * @param {boolean=} opt_useSetImmediate Avoid the IE workaround that
+ *     ensures correctness at the cost of speed. See comments for details.
+ * @template SCOPE
+ */
+goog.async.nextTick = function(callback, opt_context, opt_useSetImmediate) {
+  var cb = callback;
+  if (opt_context) {
+    cb = goog.bind(callback, opt_context);
+  }
+  cb = goog.async.nextTick.wrapCallback_(cb);
+  // Note we do allow callers to also request setImmediate if they are willing
+  // to accept the possible tradeoffs of incorrectness in exchange for speed.
+  // The IE fallback of readystate change is much slower. See useSetImmediate_
+  // for details.
+  if (goog.isFunction(goog.global.setImmediate) &&
+      (opt_useSetImmediate || goog.async.nextTick.useSetImmediate_())) {
+    goog.global.setImmediate(cb);
+    return;
+  }
+
+  // Look for and cache the custom fallback version of setImmediate.
+  if (!goog.async.nextTick.setImmediate_) {
+    goog.async.nextTick.setImmediate_ =
+        goog.async.nextTick.getSetImmediateEmulator_();
+  }
+  goog.async.nextTick.setImmediate_(cb);
+};
+
+
+/**
+ * Returns whether should use setImmediate implementation currently on window.
+ *
+ * window.setImmediate was introduced and currently only supported by IE10+,
+ * but due to a bug in the implementation it is not guaranteed that
+ * setImmediate is faster than setTimeout nor that setImmediate N is before
+ * setImmediate N+1. That is why we do not use the native version if
+ * available. We do, however, call setImmediate if it is a non-native function
+ * because that indicates that it has been replaced by goog.testing.MockClock
+ * which we do want to support.
+ * See
+ * http://connect.microsoft.com/IE/feedback/details/801823/setimmediate-and-messagechannel-are-broken-in-ie10
+ *
+ * @return {boolean} Whether to use the implementation of setImmediate defined
+ *     on Window.
+ * @private
+ */
+goog.async.nextTick.useSetImmediate_ = function() {
+  // Not a browser environment.
+  if (!goog.global.Window || !goog.global.Window.prototype) {
+    return true;
+  }
+
+  // MS Edge has window.setImmediate natively, but it's not on Window.prototype.
+  // Also, there's no clean way to detect if the goog.global.setImmediate has
+  // been replaced by mockClock as its replacement also shows up as "[native
+  // code]" when using toString. Therefore, just always use
+  // goog.global.setImmediate for Edge. It's unclear if it suffers the same
+  // issues as IE10/11, but based on
+  // https://dev.modern.ie/testdrive/demos/setimmediatesorting/
+  // it seems they've been working to ensure it's WAI.
+  if (goog.labs.userAgent.browser.isEdge() ||
+      goog.global.Window.prototype.setImmediate != goog.global.setImmediate) {
+    // Something redefined setImmediate in which case we decide to use it (This
+    // is so that we use the mockClock setImmediate).
+    return true;
+  }
+
+  return false;
+};
+
+
+/**
+ * Cache for the setImmediate implementation.
+ * @type {function(function())}
+ * @private
+ */
+goog.async.nextTick.setImmediate_;
+
+
+/**
+ * Determines the best possible implementation to run a function as soon as
+ * the JS event loop is idle.
+ * @return {function(function())} The "setImmediate" implementation.
+ * @private
+ */
+goog.async.nextTick.getSetImmediateEmulator_ = function() {
+  // Create a private message channel and use it to postMessage empty messages
+  // to ourselves.
+  var Channel = goog.global['MessageChannel'];
+  // If MessageChannel is not available and we are in a browser, implement
+  // an iframe based polyfill in browsers that have postMessage and
+  // document.addEventListener. The latter excludes IE8 because it has a
+  // synchronous postMessage implementation.
+  if (typeof Channel === 'undefined' && typeof window !== 'undefined' &&
+      window.postMessage && window.addEventListener &&
+      // Presto (The old pre-blink Opera engine) has problems with iframes
+      // and contentWindow.
+      !goog.labs.userAgent.engine.isPresto()) {
+    /** @constructor */
+    Channel = function() {
+      // Make an empty, invisible iframe.
+      var iframe = /** @type {!HTMLIFrameElement} */ (
+          document.createElement(goog.dom.TagName.IFRAME));
+      iframe.style.display = 'none';
+      iframe.src = '';
+      document.documentElement.appendChild(iframe);
+      var win = iframe.contentWindow;
+      var doc = win.document;
+      doc.open();
+      doc.write('');
+      doc.close();
+      // Do not post anything sensitive over this channel, as the workaround for
+      // pages with file: origin could allow that information to be modified or
+      // intercepted.
+      var message = 'callImmediate' + Math.random();
+      // The same origin policy rejects attempts to postMessage from file: urls
+      // unless the origin is '*'.
+      // TODO(b/16335441): Use '*' origin for data: and other similar protocols.
+      var origin = win.location.protocol == 'file:' ?
+          '*' :
+          win.location.protocol + '//' + win.location.host;
+      var onmessage = goog.bind(function(e) {
+        // Validate origin and message to make sure that this message was
+        // intended for us. If the origin is set to '*' (see above) only the
+        // message needs to match since, for example, '*' != 'file://'. Allowing
+        // the wildcard is ok, as we are not concerned with security here.
+        if ((origin != '*' && e.origin != origin) || e.data != message) {
+          return;
+        }
+        this['port1'].onmessage();
+      }, this);
+      win.addEventListener('message', onmessage, false);
+      this['port1'] = {};
+      this['port2'] = {
+        postMessage: function() { win.postMessage(message, origin); }
+      };
+    };
+  }
+  if (typeof Channel !== 'undefined' && (!goog.labs.userAgent.browser.isIE())) {
+    // Exclude all of IE due to
+    // http://codeforhire.com/2013/09/21/setimmediate-and-messagechannel-broken-on-internet-explorer-10/
+    // which allows starving postMessage with a busy setTimeout loop.
+    // This currently affects IE10 and IE11 which would otherwise be able
+    // to use the postMessage based fallbacks.
+    var channel = new Channel();
+    // Use a fifo linked list to call callbacks in the right order.
+    var head = {};
+    var tail = head;
+    channel['port1'].onmessage = function() {
+      if (goog.isDef(head.next)) {
+        head = head.next;
+        var cb = head.cb;
+        head.cb = null;
+        cb();
+      }
+    };
+    return function(cb) {
+      tail.next = {cb: cb};
+      tail = tail.next;
+      channel['port2'].postMessage(0);
+    };
+  }
+  // Implementation for IE6 to IE10: Script elements fire an asynchronous
+  // onreadystatechange event when inserted into the DOM.
+  if (typeof document !== 'undefined' &&
+      'onreadystatechange' in document.createElement(goog.dom.TagName.SCRIPT)) {
+    return function(cb) {
+      var script = document.createElement(goog.dom.TagName.SCRIPT);
+      script.onreadystatechange = function() {
+        // Clean up and call the callback.
+        script.onreadystatechange = null;
+        script.parentNode.removeChild(script);
+        script = null;
+        cb();
+        cb = null;
+      };
+      document.documentElement.appendChild(script);
+    };
+  }
+  // Fall back to setTimeout with 0. In browsers this creates a delay of 5ms
+  // or more.
+  // NOTE(user): This fallback is used for IE11.
+  return function(cb) { goog.global.setTimeout(cb, 0); };
+};
+
+
+/**
+ * Helper function that is overrided to protect callbacks with entry point
+ * monitor if the application monitors entry points.
+ * @param {function()} callback Callback function to fire as soon as possible.
+ * @return {function()} The wrapped callback.
+ * @private
+ */
+goog.async.nextTick.wrapCallback_ = goog.functions.identity;
+
+
+// Register the callback function as an entry point, so that it can be
+// monitored for exception handling, etc. This has to be done in this file
+// since it requires special code to handle all browsers.
+goog.debug.entryPointRegistry.register(
+    /**
+     * @param {function(!Function): !Function} transformer The transforming
+     *     function.
+     */
+    function(transformer) { goog.async.nextTick.wrapCallback_ = transformer; });
+
+// Based on https://github.com/Polymer/PointerEvents
+
+// Copyright (c) 2013 The Polymer Authors. All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+goog.provide('ol.pointer.PointerEvent');
+
+
+goog.require('ol.events');
+goog.require('ol.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 {ol.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.
+ */
+ol.pointer.PointerEvent = function(type, originalEvent, opt_eventDict) {
+  ol.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 {boolean}
+   */
+  this.bubbles = 'bubbles' in eventDict ? eventDict['bubbles'] : false;
+
+  /**
+   * @type {boolean}
+   */
+  this.cancelable = 'cancelable' in eventDict ? eventDict['cancelable'] : false;
+
+  /**
+   * @type {Object}
+   */
+  this.view = 'view' in eventDict ? eventDict['view'] : null;
+
+  /**
+   * @type {number}
+   */
+  this.detail = 'detail' in eventDict ? eventDict['detail'] : null;
+
+  /**
+   * @type {number}
+   */
+  this.screenX = 'screenX' in eventDict ? eventDict['screenX'] : 0;
+
+  /**
+   * @type {number}
+   */
+  this.screenY = 'screenY' in eventDict ? eventDict['screenY'] : 0;
+
+  /**
+   * @type {number}
+   */
+  this.clientX = 'clientX' in eventDict ? eventDict['clientX'] : 0;
+
+  /**
+   * @type {number}
+   */
+  this.clientY = 'clientY' in eventDict ? eventDict['clientY'] : 0;
+
+  /**
+   * @type {boolean}
+   */
+  this.ctrlKey = 'ctrlKey' in eventDict ? eventDict['ctrlKey'] : false;
+
+  /**
+   * @type {boolean}
+   */
+  this.altKey = 'altKey' in eventDict ? eventDict['altKey'] : false;
+
+  /**
+   * @type {boolean}
+   */
+  this.shiftKey = 'shiftKey' in eventDict ? eventDict['shiftKey'] : false;
+
+  /**
+   * @type {boolean}
+   */
+  this.metaKey = 'metaKey' in eventDict ? eventDict['metaKey'] : false;
+
+  /**
+   * @type {number}
+   */
+  this.button = 'button' in eventDict ? eventDict['button'] : 0;
+
+  /**
+   * @type {Node}
+   */
+  this.relatedTarget = 'relatedTarget' in eventDict ?
+      eventDict['relatedTarget'] : null;
+
+  // PointerEvent related properties
+
+  /**
+   * @const
+   * @type {number}
+   */
+  this.pointerId = 'pointerId' in eventDict ? eventDict['pointerId'] : 0;
+
+  /**
+   * @type {number}
+   */
+  this.width = 'width' in eventDict ? eventDict['width'] : 0;
+
+  /**
+   * @type {number}
+   */
+  this.height = 'height' in eventDict ? eventDict['height'] : 0;
+
+  /**
+   * @type {number}
+   */
+  this.tiltX = 'tiltX' in eventDict ? eventDict['tiltX'] : 0;
+
+  /**
+   * @type {number}
+   */
+  this.tiltY = 'tiltY' in eventDict ? eventDict['tiltY'] : 0;
+
+  /**
+   * @type {string}
+   */
+  this.pointerType = 'pointerType' in eventDict ? eventDict['pointerType'] : '';
+
+  /**
+   * @type {number}
+   */
+  this.hwTimestamp = 'hwTimestamp' in eventDict ? eventDict['hwTimestamp'] : 0;
+
+  /**
+   * @type {boolean}
+   */
+  this.isPrimary = 'isPrimary' in eventDict ? eventDict['isPrimary'] : false;
+
+  // keep the semantics of preventDefault
+  if (originalEvent.preventDefault) {
+    this.preventDefault = function() {
+      originalEvent.preventDefault();
+    };
+  }
+};
+ol.inherits(ol.pointer.PointerEvent, ol.events.Event);
+
+
+/**
+ * @private
+ * @param {Object.<string, ?>} eventDict The event dictionary.
+ * @return {number} Button indicator.
+ */
+ol.pointer.PointerEvent.prototype.getButtons_ = function(eventDict) {
+  // According to the w3c spec,
+  // http://www.w3.org/TR/DOM-Level-3-Events/#events-MouseEvent-button
+  // MouseEvent.button == 0 can mean either no mouse button depressed, or the
+  // left mouse button depressed.
+  //
+  // As of now, the only way to distinguish between the two states of
+  // MouseEvent.button is by using the deprecated MouseEvent.which property, as
+  // this maps mouse buttons to positive integers > 0, and uses 0 to mean that
+  // no mouse button is held.
+  //
+  // MouseEvent.which is derived from MouseEvent.button at MouseEvent creation,
+  // but initMouseEvent does not expose an argument with which to set
+  // MouseEvent.which. Calling initMouseEvent with a buttonArg of 0 will set
+  // MouseEvent.button == 0 and MouseEvent.which == 1, breaking the expectations
+  // of app developers.
+  //
+  // The only way to propagate the correct state of MouseEvent.which and
+  // MouseEvent.button to a new MouseEvent.button == 0 and MouseEvent.which == 0
+  // is to call initMouseEvent with a buttonArg value of -1.
+  //
+  // This is fixed with DOM Level 4's use of buttons
+  var buttons;
+  if (eventDict.buttons || ol.pointer.PointerEvent.HAS_BUTTONS) {
+    buttons = eventDict.buttons;
+  } else {
+    switch (eventDict.which) {
+      case 1: buttons = 1; break;
+      case 2: buttons = 4; break;
+      case 3: buttons = 2; break;
+      default: buttons = 0;
+    }
+  }
+  return buttons;
+};
+
+
+/**
+ * @private
+ * @param {Object.<string, ?>} eventDict The event dictionary.
+ * @param {number} buttons Button indicator.
+ * @return {number} The pressure.
+ */
+ol.pointer.PointerEvent.prototype.getPressure_ = function(eventDict, buttons) {
+  // Spec requires that pointers without pressure specified use 0.5 for down
+  // state and 0 for up state.
+  var pressure = 0;
+  if (eventDict.pressure) {
+    pressure = eventDict.pressure;
+  } else {
+    pressure = buttons ? 0.5 : 0;
+  }
+  return pressure;
+};
+
+
+/**
+ * Is the `buttons` property supported?
+ * @type {boolean}
+ */
+ol.pointer.PointerEvent.HAS_BUTTONS = false;
+
+
+/**
+ * Checks if the `buttons` property is supported.
+ */
+(function() {
+  try {
+    var ev = new MouseEvent('click', {buttons: 1});
+    ol.pointer.PointerEvent.HAS_BUTTONS = ev.buttons === 1;
+  } catch (e) {
+    // pass
+  }
+})();
+
+goog.provide('ol.webgl');
+goog.provide('ol.webgl.WebGLContextEventType');
+
+
+/**
+ * @const
+ * @private
+ * @type {Array.<string>}
+ */
+ol.webgl.CONTEXT_IDS_ = [
+  'experimental-webgl',
+  'webgl',
+  'webkit-3d',
+  'moz-webgl'
+];
+
+
+/**
+ * @enum {string}
+ */
+ol.webgl.WebGLContextEventType = {
+  LOST: 'webglcontextlost',
+  RESTORED: 'webglcontextrestored'
+};
+
+
+/**
+ * @param {HTMLCanvasElement} canvas Canvas.
+ * @param {Object=} opt_attributes Attributes.
+ * @return {WebGLRenderingContext} WebGL rendering context.
+ */
+ol.webgl.getContext = function(canvas, opt_attributes) {
+  var context, i, ii = ol.webgl.CONTEXT_IDS_.length;
+  for (i = 0; i < ii; ++i) {
+    try {
+      context = canvas.getContext(ol.webgl.CONTEXT_IDS_[i], opt_attributes);
+      if (context) {
+        return /** @type {!WebGLRenderingContext} */ (context);
+      }
+    } catch (e) {
+      // pass
+    }
+  }
+  return null;
+};
+
+goog.provide('ol.has');
+
+goog.require('ol');
+goog.require('ol.dom');
+goog.require('ol.webgl');
+
+
+var ua = typeof navigator !== 'undefined' ?
+    navigator.userAgent.toLowerCase() : '';
+
+/**
+ * User agent string says we are dealing with Firefox as browser.
+ * @type {boolean}
+ */
+ol.has.FIREFOX = ua.indexOf('firefox') !== -1;
+
+/**
+ * User agent string says we are dealing with Safari as browser.
+ * @type {boolean}
+ */
+ol.has.SAFARI = ua.indexOf('safari') !== -1 && ua.indexOf('chrom') === -1;
+
+/**
+ * User agent string says we are dealing with a Mac as platform.
+ * @type {boolean}
+ */
+ol.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 stable
+ */
+ol.has.DEVICE_PIXEL_RATIO = ol.global.devicePixelRatio || 1;
+
+
+/**
+ * True if the browser's Canvas implementation implements {get,set}LineDash.
+ * @type {boolean}
+ */
+ol.has.CANVAS_LINE_DASH = false;
+
+
+/**
+ * True if both the library and browser support Canvas.  Always `false`
+ * if `ol.ENABLE_CANVAS` is set to `false` at compile time.
+ * @const
+ * @type {boolean}
+ * @api stable
+ */
+ol.has.CANVAS = ol.ENABLE_CANVAS && (
+    /**
+     * @return {boolean} Canvas supported.
+     */
+    function() {
+      if (!('HTMLCanvasElement' in ol.global)) {
+        return false;
+      }
+      try {
+        var context = ol.dom.createCanvasContext2D();
+        if (!context) {
+          return false;
+        } else {
+          if (context.setLineDash !== undefined) {
+            ol.has.CANVAS_LINE_DASH = true;
+          }
+          return true;
+        }
+      } catch (e) {
+        return false;
+      }
+    })();
+
+
+/**
+ * Indicates if DeviceOrientation is supported in the user's browser.
+ * @const
+ * @type {boolean}
+ * @api stable
+ */
+ol.has.DEVICE_ORIENTATION = 'DeviceOrientationEvent' in ol.global;
+
+
+/**
+ * True if `ol.ENABLE_DOM` is set to `true` at compile time.
+ * @const
+ * @type {boolean}
+ */
+ol.has.DOM = ol.ENABLE_DOM;
+
+
+/**
+ * Is HTML5 geolocation supported in the current browser?
+ * @const
+ * @type {boolean}
+ * @api stable
+ */
+ol.has.GEOLOCATION = 'geolocation' in ol.global.navigator;
+
+
+/**
+ * True if browser supports touch events.
+ * @const
+ * @type {boolean}
+ * @api stable
+ */
+ol.has.TOUCH = ol.ASSUME_TOUCH || 'ontouchstart' in ol.global;
+
+
+/**
+ * True if browser supports pointer events.
+ * @const
+ * @type {boolean}
+ */
+ol.has.POINTER = 'PointerEvent' in ol.global;
+
+
+/**
+ * True if browser supports ms pointer events (IE 10).
+ * @const
+ * @type {boolean}
+ */
+ol.has.MSPOINTER = !!(ol.global.navigator.msPointerEnabled);
+
+
+/**
+ * True if both OpenLayers and browser support WebGL.  Always `false`
+ * if `ol.ENABLE_WEBGL` is set to `false` at compile time.
+ * @const
+ * @type {boolean}
+ * @api stable
+ */
+ol.has.WEBGL;
+
+
+(function() {
+  if (ol.ENABLE_WEBGL) {
+    var hasWebGL = false;
+    var textureSize;
+    var /** @type {Array.<string>} */ extensions = [];
+
+    if ('WebGLRenderingContext' in ol.global) {
+      try {
+        var canvas = /** @type {HTMLCanvasElement} */
+            (document.createElement('CANVAS'));
+        var gl = ol.webgl.getContext(canvas, {
+          failIfMajorPerformanceCaveat: true
+        });
+        if (gl) {
+          hasWebGL = true;
+          textureSize = /** @type {number} */
+              (gl.getParameter(gl.MAX_TEXTURE_SIZE));
+          extensions = gl.getSupportedExtensions();
+        }
+      } catch (e) {
+        // pass
+      }
+    }
+    ol.has.WEBGL = hasWebGL;
+    ol.WEBGL_EXTENSIONS = extensions;
+    ol.WEBGL_MAX_TEXTURE_SIZE = textureSize;
+  }
+})();
+
+goog.provide('ol.pointer.EventSource');
+
+
+/**
+ * @param {ol.pointer.PointerEventHandler} dispatcher Event handler.
+ * @param {!Object.<string, function(Event)>} mapping Event
+ *     mapping.
+ * @constructor
+ */
+ol.pointer.EventSource = function(dispatcher, mapping) {
+  /**
+   * @type {ol.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
+ */
+ol.pointer.EventSource.prototype.getEvents = function() {
+  return Object.keys(this.mapping_);
+};
+
+
+/**
+ * Returns a mapping between the supported event types and
+ * the handlers that should handle an event.
+ * @return {Object.<string, function(Event)>}
+ *         Event/Handler mapping
+ */
+ol.pointer.EventSource.prototype.getMapping = function() {
+  return this.mapping_;
+};
+
+
+/**
+ * Returns the handler that should handle a given event type.
+ * @param {string} eventType The event type.
+ * @return {function(Event)} Handler
+ */
+ol.pointer.EventSource.prototype.getHandlerForEvent = function(eventType) {
+  return this.mapping_[eventType];
+};
+
+// Based on https://github.com/Polymer/PointerEvents
+
+// Copyright (c) 2013 The Polymer Authors. All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+goog.provide('ol.pointer.MouseSource');
+
+goog.require('ol.pointer.EventSource');
+
+
+/**
+ * @param {ol.pointer.PointerEventHandler} dispatcher Event handler.
+ * @constructor
+ * @extends {ol.pointer.EventSource}
+ */
+ol.pointer.MouseSource = function(dispatcher) {
+  var mapping = {
+    'mousedown': this.mousedown,
+    'mousemove': this.mousemove,
+    'mouseup': this.mouseup,
+    'mouseover': this.mouseover,
+    'mouseout': this.mouseout
+  };
+  ol.pointer.EventSource.call(this, dispatcher, mapping);
+
+  /**
+   * @const
+   * @type {!Object.<string, Event|Object>}
+   */
+  this.pointerMap = dispatcher.pointerMap;
+
+  /**
+   * @const
+   * @type {Array.<ol.Pixel>}
+   */
+  this.lastTouches = [];
+};
+ol.inherits(ol.pointer.MouseSource, ol.pointer.EventSource);
+
+
+/**
+ * @const
+ * @type {number}
+ */
+ol.pointer.MouseSource.POINTER_ID = 1;
+
+
+/**
+ * @const
+ * @type {string}
+ */
+ol.pointer.MouseSource.POINTER_TYPE = 'mouse';
+
+
+/**
+ * Radius around touchend that swallows mouse events.
+ *
+ * @const
+ * @type {number}
+ */
+ol.pointer.MouseSource.DEDUP_DIST = 25;
+
+
+/**
+ * Detect if a mouse event was simulated from a touch by
+ * checking if previously there was a touch event at the
+ * same position.
+ *
+ * FIXME - Known problem with the native Android browser on
+ * Samsung GT-I9100 (Android 4.1.2):
+ * In case the page is scrolled, this function does not work
+ * correctly when a canvas is used (WebGL or canvas renderer).
+ * Mouse listeners on canvas elements (for this browser), create
+ * two mouse events: One 'good' and one 'bad' one (on other browsers or
+ * when a div is used, there is only one event). For the 'bad' one,
+ * clientX/clientY and also pageX/pageY are wrong when the page
+ * is scrolled. Because of that, this function can not detect if
+ * the events were simulated from a touch event. As result, a
+ * pointer event at a wrong position is dispatched, which confuses
+ * the map interactions.
+ * It is unclear, how one can get the correct position for the event
+ * or detect that the positions are invalid.
+ *
+ * @private
+ * @param {Event} inEvent The in event.
+ * @return {boolean} True, if the event was generated by a touch.
+ */
+ol.pointer.MouseSource.prototype.isEventSimulatedFromTouch_ = function(inEvent) {
+  var lts = this.lastTouches;
+  var x = inEvent.clientX, y = inEvent.clientY;
+  for (var i = 0, l = lts.length, t; i < l && (t = lts[i]); i++) {
+    // simulated mouse events will be swallowed near a primary touchend
+    var dx = Math.abs(x - t[0]), dy = Math.abs(y - t[1]);
+    if (dx <= ol.pointer.MouseSource.DEDUP_DIST &&
+        dy <= ol.pointer.MouseSource.DEDUP_DIST) {
+      return true;
+    }
+  }
+  return false;
+};
+
+
+/**
+ * Creates a copy of the original event that will be used
+ * for the fake pointer event.
+ *
+ * @param {Event} inEvent The in event.
+ * @param {ol.pointer.PointerEventHandler} dispatcher Event handler.
+ * @return {Object} The copied event.
+ */
+ol.pointer.MouseSource.prepareEvent = function(inEvent, dispatcher) {
+  var e = dispatcher.cloneEvent(inEvent, inEvent);
+
+  // forward mouse preventDefault
+  var pd = e.preventDefault;
+  e.preventDefault = function() {
+    inEvent.preventDefault();
+    pd();
+  };
+
+  e.pointerId = ol.pointer.MouseSource.POINTER_ID;
+  e.isPrimary = true;
+  e.pointerType = ol.pointer.MouseSource.POINTER_TYPE;
+
+  return e;
+};
+
+
+/**
+ * Handler for `mousedown`.
+ *
+ * @param {Event} inEvent The in event.
+ */
+ol.pointer.MouseSource.prototype.mousedown = function(inEvent) {
+  if (!this.isEventSimulatedFromTouch_(inEvent)) {
+    // TODO(dfreedman) workaround for some elements not sending mouseup
+    // http://crbug/149091
+    if (ol.pointer.MouseSource.POINTER_ID.toString() in this.pointerMap) {
+      this.cancel(inEvent);
+    }
+    var e = ol.pointer.MouseSource.prepareEvent(inEvent, this.dispatcher);
+    this.pointerMap[ol.pointer.MouseSource.POINTER_ID.toString()] = inEvent;
+    this.dispatcher.down(e, inEvent);
+  }
+};
+
+
+/**
+ * Handler for `mousemove`.
+ *
+ * @param {Event} inEvent The in event.
+ */
+ol.pointer.MouseSource.prototype.mousemove = function(inEvent) {
+  if (!this.isEventSimulatedFromTouch_(inEvent)) {
+    var e = ol.pointer.MouseSource.prepareEvent(inEvent, this.dispatcher);
+    this.dispatcher.move(e, inEvent);
+  }
+};
+
+
+/**
+ * Handler for `mouseup`.
+ *
+ * @param {Event} inEvent The in event.
+ */
+ol.pointer.MouseSource.prototype.mouseup = function(inEvent) {
+  if (!this.isEventSimulatedFromTouch_(inEvent)) {
+    var p = this.pointerMap[ol.pointer.MouseSource.POINTER_ID.toString()];
+
+    if (p && p.button === inEvent.button) {
+      var e = ol.pointer.MouseSource.prepareEvent(inEvent, this.dispatcher);
+      this.dispatcher.up(e, inEvent);
+      this.cleanupMouse();
+    }
+  }
+};
+
+
+/**
+ * Handler for `mouseover`.
+ *
+ * @param {Event} inEvent The in event.
+ */
+ol.pointer.MouseSource.prototype.mouseover = function(inEvent) {
+  if (!this.isEventSimulatedFromTouch_(inEvent)) {
+    var e = ol.pointer.MouseSource.prepareEvent(inEvent, this.dispatcher);
+    this.dispatcher.enterOver(e, inEvent);
+  }
+};
+
+
+/**
+ * Handler for `mouseout`.
+ *
+ * @param {Event} inEvent The in event.
+ */
+ol.pointer.MouseSource.prototype.mouseout = function(inEvent) {
+  if (!this.isEventSimulatedFromTouch_(inEvent)) {
+    var e = ol.pointer.MouseSource.prepareEvent(inEvent, this.dispatcher);
+    this.dispatcher.leaveOut(e, inEvent);
+  }
+};
+
+
+/**
+ * Dispatches a `pointercancel` event.
+ *
+ * @param {Event} inEvent The in event.
+ */
+ol.pointer.MouseSource.prototype.cancel = function(inEvent) {
+  var e = ol.pointer.MouseSource.prepareEvent(inEvent, this.dispatcher);
+  this.dispatcher.cancel(e, inEvent);
+  this.cleanupMouse();
+};
+
+
+/**
+ * Remove the mouse from the list of active pointers.
+ */
+ol.pointer.MouseSource.prototype.cleanupMouse = function() {
+  delete this.pointerMap[ol.pointer.MouseSource.POINTER_ID.toString()];
+};
+
+// Based on https://github.com/Polymer/PointerEvents
+
+// Copyright (c) 2013 The Polymer Authors. All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+goog.provide('ol.pointer.MsSource');
+
+goog.require('ol.pointer.EventSource');
+
+
+/**
+ * @param {ol.pointer.PointerEventHandler} dispatcher Event handler.
+ * @constructor
+ * @extends {ol.pointer.EventSource}
+ */
+ol.pointer.MsSource = function(dispatcher) {
+  var mapping = {
+    'MSPointerDown': this.msPointerDown,
+    'MSPointerMove': this.msPointerMove,
+    'MSPointerUp': this.msPointerUp,
+    'MSPointerOut': this.msPointerOut,
+    'MSPointerOver': this.msPointerOver,
+    'MSPointerCancel': this.msPointerCancel,
+    'MSGotPointerCapture': this.msGotPointerCapture,
+    'MSLostPointerCapture': this.msLostPointerCapture
+  };
+  ol.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'
+  ];
+};
+ol.inherits(ol.pointer.MsSource, ol.pointer.EventSource);
+
+
+/**
+ * Creates a copy of the original event that will be used
+ * for the fake pointer event.
+ *
+ * @private
+ * @param {Event} inEvent The in event.
+ * @return {Object} The copied event.
+ */
+ol.pointer.MsSource.prototype.prepareEvent_ = function(inEvent) {
+  var e = inEvent;
+  if (goog.isNumber(inEvent.pointerType)) {
+    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.
+ */
+ol.pointer.MsSource.prototype.cleanup = function(pointerId) {
+  delete this.pointerMap[pointerId.toString()];
+};
+
+
+/**
+ * Handler for `msPointerDown`.
+ *
+ * @param {Event} inEvent The in event.
+ */
+ol.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.
+ */
+ol.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.
+ */
+ol.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.
+ */
+ol.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.
+ */
+ol.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.
+ */
+ol.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.
+ */
+ol.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.
+ */
+ol.pointer.MsSource.prototype.msGotPointerCapture = function(inEvent) {
+  var e = this.dispatcher.makeEvent('gotpointercapture',
+      inEvent, inEvent);
+  this.dispatcher.dispatchEvent(e);
+};
+
+// Based on https://github.com/Polymer/PointerEvents
+
+// Copyright (c) 2013 The Polymer Authors. All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+goog.provide('ol.pointer.NativeSource');
+
+goog.require('ol.pointer.EventSource');
+
+
+/**
+ * @param {ol.pointer.PointerEventHandler} dispatcher Event handler.
+ * @constructor
+ * @extends {ol.pointer.EventSource}
+ */
+ol.pointer.NativeSource = function(dispatcher) {
+  var mapping = {
+    'pointerdown': this.pointerDown,
+    'pointermove': this.pointerMove,
+    'pointerup': this.pointerUp,
+    'pointerout': this.pointerOut,
+    'pointerover': this.pointerOver,
+    'pointercancel': this.pointerCancel,
+    'gotpointercapture': this.gotPointerCapture,
+    'lostpointercapture': this.lostPointerCapture
+  };
+  ol.pointer.EventSource.call(this, dispatcher, mapping);
+};
+ol.inherits(ol.pointer.NativeSource, ol.pointer.EventSource);
+
+
+/**
+ * Handler for `pointerdown`.
+ *
+ * @param {Event} inEvent The in event.
+ */
+ol.pointer.NativeSource.prototype.pointerDown = function(inEvent) {
+  this.dispatcher.fireNativeEvent(inEvent);
+};
+
+
+/**
+ * Handler for `pointermove`.
+ *
+ * @param {Event} inEvent The in event.
+ */
+ol.pointer.NativeSource.prototype.pointerMove = function(inEvent) {
+  this.dispatcher.fireNativeEvent(inEvent);
+};
+
+
+/**
+ * Handler for `pointerup`.
+ *
+ * @param {Event} inEvent The in event.
+ */
+ol.pointer.NativeSource.prototype.pointerUp = function(inEvent) {
+  this.dispatcher.fireNativeEvent(inEvent);
+};
+
+
+/**
+ * Handler for `pointerout`.
+ *
+ * @param {Event} inEvent The in event.
+ */
+ol.pointer.NativeSource.prototype.pointerOut = function(inEvent) {
+  this.dispatcher.fireNativeEvent(inEvent);
+};
+
+
+/**
+ * Handler for `pointerover`.
+ *
+ * @param {Event} inEvent The in event.
+ */
+ol.pointer.NativeSource.prototype.pointerOver = function(inEvent) {
+  this.dispatcher.fireNativeEvent(inEvent);
+};
+
+
+/**
+ * Handler for `pointercancel`.
+ *
+ * @param {Event} inEvent The in event.
+ */
+ol.pointer.NativeSource.prototype.pointerCancel = function(inEvent) {
+  this.dispatcher.fireNativeEvent(inEvent);
+};
+
+
+/**
+ * Handler for `lostpointercapture`.
+ *
+ * @param {Event} inEvent The in event.
+ */
+ol.pointer.NativeSource.prototype.lostPointerCapture = function(inEvent) {
+  this.dispatcher.fireNativeEvent(inEvent);
+};
+
+
+/**
+ * Handler for `gotpointercapture`.
+ *
+ * @param {Event} inEvent The in event.
+ */
+ol.pointer.NativeSource.prototype.gotPointerCapture = function(inEvent) {
+  this.dispatcher.fireNativeEvent(inEvent);
+};
+
+// Based on https://github.com/Polymer/PointerEvents
+
+// Copyright (c) 2013 The Polymer Authors. All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+goog.provide('ol.pointer.TouchSource');
+
+goog.require('ol');
+goog.require('ol.array');
+goog.require('ol.pointer.EventSource');
+goog.require('ol.pointer.MouseSource');
+
+
+/**
+ * @constructor
+ * @param {ol.pointer.PointerEventHandler} dispatcher The event handler.
+ * @param {ol.pointer.MouseSource} mouseSource Mouse source.
+ * @extends {ol.pointer.EventSource}
+ */
+ol.pointer.TouchSource = function(dispatcher, mouseSource) {
+  var mapping = {
+    'touchstart': this.touchstart,
+    'touchmove': this.touchmove,
+    'touchend': this.touchend,
+    'touchcancel': this.touchcancel
+  };
+  ol.pointer.EventSource.call(this, dispatcher, mapping);
+
+  /**
+   * @const
+   * @type {!Object.<string, Event|Object>}
+   */
+  this.pointerMap = dispatcher.pointerMap;
+
+  /**
+   * @const
+   * @type {ol.pointer.MouseSource}
+   */
+  this.mouseSource = mouseSource;
+
+  /**
+   * @private
+   * @type {number|undefined}
+   */
+  this.firstTouchId_ = undefined;
+
+  /**
+   * @private
+   * @type {number}
+   */
+  this.clickCount_ = 0;
+
+  /**
+   * @private
+   * @type {number|undefined}
+   */
+  this.resetId_ = undefined;
+};
+ol.inherits(ol.pointer.TouchSource, ol.pointer.EventSource);
+
+
+/**
+ * Mouse event timeout: This should be long enough to
+ * ignore compat mouse events made by touch.
+ * @const
+ * @type {number}
+ */
+ol.pointer.TouchSource.DEDUP_TIMEOUT = 2500;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+ol.pointer.TouchSource.CLICK_COUNT_TIMEOUT = 200;
+
+
+/**
+ * @const
+ * @type {string}
+ */
+ol.pointer.TouchSource.POINTER_TYPE = 'touch';
+
+
+/**
+ * @private
+ * @param {Touch} inTouch The in touch.
+ * @return {boolean} True, if this is the primary touch.
+ */
+ol.pointer.TouchSource.prototype.isPrimaryTouch_ = function(inTouch) {
+  return this.firstTouchId_ === inTouch.identifier;
+};
+
+
+/**
+ * Set primary touch if there are no pointers, or the only pointer is the mouse.
+ * @param {Touch} inTouch The in touch.
+ * @private
+ */
+ol.pointer.TouchSource.prototype.setPrimaryTouch_ = function(inTouch) {
+  var count = Object.keys(this.pointerMap).length;
+  if (count === 0 || (count === 1 &&
+      ol.pointer.MouseSource.POINTER_ID.toString() in this.pointerMap)) {
+    this.firstTouchId_ = inTouch.identifier;
+    this.cancelResetClickCount_();
+  }
+};
+
+
+/**
+ * @private
+ * @param {Object} inPointer The in pointer object.
+ */
+ol.pointer.TouchSource.prototype.removePrimaryPointer_ = function(inPointer) {
+  if (inPointer.isPrimary) {
+    this.firstTouchId_ = undefined;
+    this.resetClickCount_();
+  }
+};
+
+
+/**
+ * @private
+ */
+ol.pointer.TouchSource.prototype.resetClickCount_ = function() {
+  this.resetId_ = ol.global.setTimeout(
+      this.resetClickCountHandler_.bind(this),
+      ol.pointer.TouchSource.CLICK_COUNT_TIMEOUT);
+};
+
+
+/**
+ * @private
+ */
+ol.pointer.TouchSource.prototype.resetClickCountHandler_ = function() {
+  this.clickCount_ = 0;
+  this.resetId_ = undefined;
+};
+
+
+/**
+ * @private
+ */
+ol.pointer.TouchSource.prototype.cancelResetClickCount_ = function() {
+  if (this.resetId_ !== undefined) {
+    ol.global.clearTimeout(this.resetId_);
+  }
+};
+
+
+/**
+ * @private
+ * @param {Event} browserEvent Browser event
+ * @param {Touch} inTouch Touch event
+ * @return {Object} A pointer object.
+ */
+ol.pointer.TouchSource.prototype.touchToPointer_ = function(browserEvent, inTouch) {
+  var e = this.dispatcher.cloneEvent(browserEvent, inTouch);
+  // Spec specifies that pointerId 1 is reserved for Mouse.
+  // Touch identifiers can start at 0.
+  // Add 2 to the touch identifier for compatibility.
+  e.pointerId = inTouch.identifier + 2;
+  // TODO: check if this is necessary?
+  //e.target = findTarget(e);
+  e.bubbles = true;
+  e.cancelable = true;
+  e.detail = this.clickCount_;
+  e.button = 0;
+  e.buttons = 1;
+  e.width = inTouch.webkitRadiusX || inTouch.radiusX || 0;
+  e.height = inTouch.webkitRadiusY || inTouch.radiusY || 0;
+  e.pressure = inTouch.webkitForce || inTouch.force || 0.5;
+  e.isPrimary = this.isPrimaryTouch_(inTouch);
+  e.pointerType = ol.pointer.TouchSource.POINTER_TYPE;
+
+  // make sure that the properties that are different for
+  // each `Touch` object are not copied from the BrowserEvent object
+  e.clientX = inTouch.clientX;
+  e.clientY = inTouch.clientY;
+  e.screenX = inTouch.screenX;
+  e.screenY = inTouch.screenY;
+
+  return e;
+};
+
+
+/**
+ * @private
+ * @param {Event} inEvent Touch event
+ * @param {function(Event, Object)} inFunction In function.
+ */
+ol.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 {boolean} True, if the `Touch` with the given id is in the list.
+ */
+ol.pointer.TouchSource.prototype.findTouch_ = function(touchList, searchId) {
+  var l = touchList.length;
+  var touch;
+  for (var i = 0; i < l; i++) {
+    touch = touchList[i];
+    if (touch.identifier === searchId) {
+      return true;
+    }
+  }
+  return false;
+};
+
+
+/**
+ * In some instances, a touchstart can happen without a touchend. This
+ * leaves the pointermap in a broken state.
+ * Therefore, on every touchstart, we remove the touches that did not fire a
+ * touchend event.
+ * To keep state globally consistent, we fire a pointercancel for
+ * this "abandoned" touch
+ *
+ * @private
+ * @param {Event} inEvent The in event.
+ */
+ol.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 != ol.pointer.MouseSource.POINTER_ID &&
+          !this.findTouch_(touchList, key - 2)) {
+        d.push(value.out);
+      }
+    }
+    for (i = 0; i < d.length; ++i) {
+      this.cancelOut_(inEvent, d[i]);
+    }
+  }
+};
+
+
+/**
+ * Handler for `touchstart`, triggers `pointerover`,
+ * `pointerenter` and `pointerdown` events.
+ *
+ * @param {Event} inEvent The in event.
+ */
+ol.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.
+ */
+ol.pointer.TouchSource.prototype.overDown_ = function(browserEvent, inPointer) {
+  this.pointerMap[inPointer.pointerId] = {
+    target: inPointer.target,
+    out: inPointer,
+    outTarget: inPointer.target
+  };
+  this.dispatcher.over(inPointer, browserEvent);
+  this.dispatcher.enter(inPointer, browserEvent);
+  this.dispatcher.down(inPointer, browserEvent);
+};
+
+
+/**
+ * Handler for `touchmove`.
+ *
+ * @param {Event} inEvent The in event.
+ */
+ol.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.
+ */
+ol.pointer.TouchSource.prototype.moveOverOut_ = function(browserEvent, inPointer) {
+  var event = inPointer;
+  var pointer = this.pointerMap[event.pointerId];
+  // a finger drifted off the screen, ignore it
+  if (!pointer) {
+    return;
+  }
+  var outEvent = pointer.out;
+  var outTarget = pointer.outTarget;
+  this.dispatcher.move(event, browserEvent);
+  if (outEvent && outTarget !== event.target) {
+    outEvent.relatedTarget = event.target;
+    event.relatedTarget = outTarget;
+    // recover from retargeting by shadow
+    outEvent.target = outTarget;
+    if (event.target) {
+      this.dispatcher.leaveOut(outEvent, browserEvent);
+      this.dispatcher.enterOver(event, browserEvent);
+    } else {
+      // clean up case when finger leaves the screen
+      event.target = outTarget;
+      event.relatedTarget = null;
+      this.cancelOut_(browserEvent, event);
+    }
+  }
+  pointer.out = event;
+  pointer.outTarget = event.target;
+};
+
+
+/**
+ * Handler for `touchend`, triggers `pointerup`,
+ * `pointerout` and `pointerleave` events.
+ *
+ * @param {Event} inEvent The event.
+ */
+ol.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.
+ */
+ol.pointer.TouchSource.prototype.upOut_ = function(browserEvent, inPointer) {
+  this.dispatcher.up(inPointer, browserEvent);
+  this.dispatcher.out(inPointer, browserEvent);
+  this.dispatcher.leave(inPointer, browserEvent);
+  this.cleanUpPointer_(inPointer);
+};
+
+
+/**
+ * Handler for `touchcancel`, triggers `pointercancel`,
+ * `pointerout` and `pointerleave` events.
+ *
+ * @param {Event} inEvent The in event.
+ */
+ol.pointer.TouchSource.prototype.touchcancel = function(inEvent) {
+  this.processTouches_(inEvent, this.cancelOut_);
+};
+
+
+/**
+ * @private
+ * @param {Event} browserEvent The event.
+ * @param {Object} inPointer The in pointer.
+ */
+ol.pointer.TouchSource.prototype.cancelOut_ = function(browserEvent, inPointer) {
+  this.dispatcher.cancel(inPointer, browserEvent);
+  this.dispatcher.out(inPointer, browserEvent);
+  this.dispatcher.leave(inPointer, browserEvent);
+  this.cleanUpPointer_(inPointer);
+};
+
+
+/**
+ * @private
+ * @param {Object} inPointer The inPointer object.
+ */
+ol.pointer.TouchSource.prototype.cleanUpPointer_ = function(inPointer) {
+  delete this.pointerMap[inPointer.pointerId];
+  this.removePrimaryPointer_(inPointer);
+};
+
+
+/**
+ * Prevent synth mouse events from creating pointer events.
+ *
+ * @private
+ * @param {Event} inEvent The in event.
+ */
+ol.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);
+
+    ol.global.setTimeout(function() {
+      // remove touch after timeout
+      ol.array.remove(lts, lt);
+    }, ol.pointer.TouchSource.DEDUP_TIMEOUT);
+  }
+};
+
+// Based on https://github.com/Polymer/PointerEvents
+
+// Copyright (c) 2013 The Polymer Authors. All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+goog.provide('ol.pointer.PointerEventHandler');
+
+goog.require('ol.events');
+goog.require('ol.events.EventTarget');
+
+goog.require('ol.has');
+goog.require('ol.pointer.MouseSource');
+goog.require('ol.pointer.MsSource');
+goog.require('ol.pointer.NativeSource');
+goog.require('ol.pointer.PointerEvent');
+goog.require('ol.pointer.TouchSource');
+
+
+/**
+ * @constructor
+ * @extends {ol.events.EventTarget}
+ * @param {Element|HTMLDocument} element Viewport element.
+ */
+ol.pointer.PointerEventHandler = function(element) {
+  ol.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.<ol.pointer.EventSource>}
+   * @private
+   */
+  this.eventSourceList_ = [];
+
+  this.registerSources();
+};
+ol.inherits(ol.pointer.PointerEventHandler, ol.events.EventTarget);
+
+
+/**
+ * Set up the event sources (mouse, touch and native pointers)
+ * that generate pointer events.
+ */
+ol.pointer.PointerEventHandler.prototype.registerSources = function() {
+  if (ol.has.POINTER) {
+    this.registerSource('native', new ol.pointer.NativeSource(this));
+  } else if (ol.has.MSPOINTER) {
+    this.registerSource('ms', new ol.pointer.MsSource(this));
+  } else {
+    var mouseSource = new ol.pointer.MouseSource(this);
+    this.registerSource('mouse', mouseSource);
+
+    if (ol.has.TOUCH) {
+      this.registerSource('touch',
+          new ol.pointer.TouchSource(this, mouseSource));
+    }
+  }
+
+  // register events on the viewport element
+  this.register_();
+};
+
+
+/**
+ * Add a new event source that will generate pointer events.
+ *
+ * @param {string} name A name for the event source
+ * @param {ol.pointer.EventSource} source The source event.
+ */
+ol.pointer.PointerEventHandler.prototype.registerSource = function(name, source) {
+  var s = source;
+  var newEvents = s.getEvents();
+
+  if (newEvents) {
+    newEvents.forEach(function(e) {
+      var handler = s.getHandlerForEvent(e);
+
+      if (handler) {
+        this.eventMap_[e] = handler.bind(s);
+      }
+    }, this);
+    this.eventSourceList_.push(s);
+  }
+};
+
+
+/**
+ * Set up the events for all registered event sources.
+ * @private
+ */
+ol.pointer.PointerEventHandler.prototype.register_ = function() {
+  var l = this.eventSourceList_.length;
+  var eventSource;
+  for (var i = 0; i < l; i++) {
+    eventSource = this.eventSourceList_[i];
+    this.addEvents_(eventSource.getEvents());
+  }
+};
+
+
+/**
+ * Remove all registered events.
+ * @private
+ */
+ol.pointer.PointerEventHandler.prototype.unregister_ = function() {
+  var l = this.eventSourceList_.length;
+  var eventSource;
+  for (var i = 0; i < l; i++) {
+    eventSource = this.eventSourceList_[i];
+    this.removeEvents_(eventSource.getEvents());
+  }
+};
+
+
+/**
+ * Calls the right handler for a new event.
+ * @private
+ * @param {Event} inEvent Browser event.
+ */
+ol.pointer.PointerEventHandler.prototype.eventHandler_ = function(inEvent) {
+  var type = inEvent.type;
+  var handler = this.eventMap_[type];
+  if (handler) {
+    handler(inEvent);
+  }
+};
+
+
+/**
+ * Setup listeners for the given events.
+ * @private
+ * @param {Array.<string>} events List of events.
+ */
+ol.pointer.PointerEventHandler.prototype.addEvents_ = function(events) {
+  events.forEach(function(eventName) {
+    ol.events.listen(this.element_, eventName, this.eventHandler_, this);
+  }, this);
+};
+
+
+/**
+ * Unregister listeners for the given events.
+ * @private
+ * @param {Array.<string>} events List of events.
+ */
+ol.pointer.PointerEventHandler.prototype.removeEvents_ = function(events) {
+  events.forEach(function(e) {
+    ol.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.
+ */
+ol.pointer.PointerEventHandler.prototype.cloneEvent = function(event, inEvent) {
+  var eventCopy = {}, p;
+  for (var i = 0, ii = ol.pointer.CLONE_PROPS.length; i < ii; i++) {
+    p = ol.pointer.CLONE_PROPS[i][0];
+    eventCopy[p] = event[p] || inEvent[p] || ol.pointer.CLONE_PROPS[i][1];
+  }
+
+  return eventCopy;
+};
+
+
+// EVENTS
+
+
+/**
+ * Triggers a 'pointerdown' event.
+ * @param {Object} data Pointer event data.
+ * @param {Event} event The event.
+ */
+ol.pointer.PointerEventHandler.prototype.down = function(data, event) {
+  this.fireEvent(ol.pointer.EventType.POINTERDOWN, data, event);
+};
+
+
+/**
+ * Triggers a 'pointermove' event.
+ * @param {Object} data Pointer event data.
+ * @param {Event} event The event.
+ */
+ol.pointer.PointerEventHandler.prototype.move = function(data, event) {
+  this.fireEvent(ol.pointer.EventType.POINTERMOVE, data, event);
+};
+
+
+/**
+ * Triggers a 'pointerup' event.
+ * @param {Object} data Pointer event data.
+ * @param {Event} event The event.
+ */
+ol.pointer.PointerEventHandler.prototype.up = function(data, event) {
+  this.fireEvent(ol.pointer.EventType.POINTERUP, data, event);
+};
+
+
+/**
+ * Triggers a 'pointerenter' event.
+ * @param {Object} data Pointer event data.
+ * @param {Event} event The event.
+ */
+ol.pointer.PointerEventHandler.prototype.enter = function(data, event) {
+  data.bubbles = false;
+  this.fireEvent(ol.pointer.EventType.POINTERENTER, data, event);
+};
+
+
+/**
+ * Triggers a 'pointerleave' event.
+ * @param {Object} data Pointer event data.
+ * @param {Event} event The event.
+ */
+ol.pointer.PointerEventHandler.prototype.leave = function(data, event) {
+  data.bubbles = false;
+  this.fireEvent(ol.pointer.EventType.POINTERLEAVE, data, event);
+};
+
+
+/**
+ * Triggers a 'pointerover' event.
+ * @param {Object} data Pointer event data.
+ * @param {Event} event The event.
+ */
+ol.pointer.PointerEventHandler.prototype.over = function(data, event) {
+  data.bubbles = true;
+  this.fireEvent(ol.pointer.EventType.POINTEROVER, data, event);
+};
+
+
+/**
+ * Triggers a 'pointerout' event.
+ * @param {Object} data Pointer event data.
+ * @param {Event} event The event.
+ */
+ol.pointer.PointerEventHandler.prototype.out = function(data, event) {
+  data.bubbles = true;
+  this.fireEvent(ol.pointer.EventType.POINTEROUT, data, event);
+};
+
+
+/**
+ * Triggers a 'pointercancel' event.
+ * @param {Object} data Pointer event data.
+ * @param {Event} event The event.
+ */
+ol.pointer.PointerEventHandler.prototype.cancel = function(data, event) {
+  this.fireEvent(ol.pointer.EventType.POINTERCANCEL, data, event);
+};
+
+
+/**
+ * Triggers a combination of 'pointerout' and 'pointerleave' events.
+ * @param {Object} data Pointer event data.
+ * @param {Event} event The event.
+ */
+ol.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.
+ */
+ol.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 {boolean} Returns true if the container element
+ *   contains the other element.
+ */
+ol.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 {ol.pointer.PointerEvent} A PointerEvent of type `inType`.
+ */
+ol.pointer.PointerEventHandler.prototype.makeEvent = function(inType, data, event) {
+  return new ol.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.
+ */
+ol.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.
+ */
+ol.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 {ol.pointer.PointerEvent} The wrapped event.
+ */
+ol.pointer.PointerEventHandler.prototype.wrapMouseEvent = function(eventType, event) {
+  var pointerEvent = this.makeEvent(
+      eventType, ol.pointer.MouseSource.prepareEvent(event, this), event);
+  return pointerEvent;
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.pointer.PointerEventHandler.prototype.disposeInternal = function() {
+  this.unregister_();
+  ol.events.EventTarget.prototype.disposeInternal.call(this);
+};
+
+
+/**
+ * Constants for event names.
+ * @enum {string}
+ */
+ol.pointer.EventType = {
+  POINTERMOVE: 'pointermove',
+  POINTERDOWN: 'pointerdown',
+  POINTERUP: 'pointerup',
+  POINTEROVER: 'pointerover',
+  POINTEROUT: 'pointerout',
+  POINTERENTER: 'pointerenter',
+  POINTERLEAVE: 'pointerleave',
+  POINTERCANCEL: 'pointercancel'
+};
+
+
+/**
+ * Properties to copy when cloning an event, with default values.
+ * @type {Array.<Array>}
+ */
+ol.pointer.CLONE_PROPS = [
+  // MouseEvent
+  ['bubbles', false],
+  ['cancelable', false],
+  ['view', null],
+  ['detail', null],
+  ['screenX', 0],
+  ['screenY', 0],
+  ['clientX', 0],
+  ['clientY', 0],
+  ['ctrlKey', false],
+  ['altKey', false],
+  ['shiftKey', false],
+  ['metaKey', false],
+  ['button', 0],
+  ['relatedTarget', null],
+  // DOM Level 3
+  ['buttons', 0],
+  // PointerEvent
+  ['pointerId', 0],
+  ['width', 0],
+  ['height', 0],
+  ['pressure', 0],
+  ['tiltX', 0],
+  ['tiltY', 0],
+  ['pointerType', ''],
+  ['hwTimestamp', 0],
+  ['isPrimary', false],
+  // event instance
+  ['type', ''],
+  ['target', null],
+  ['currentTarget', null],
+  ['which', 0]
+];
+
+goog.provide('ol.MapBrowserEvent');
+goog.provide('ol.MapBrowserEvent.EventType');
+goog.provide('ol.MapBrowserEventHandler');
+goog.provide('ol.MapBrowserPointerEvent');
+
+goog.require('goog.asserts');
+goog.require('ol');
+goog.require('ol.MapEvent');
+goog.require('ol.events');
+goog.require('ol.events.EventTarget');
+goog.require('ol.events.EventType');
+goog.require('ol.pointer.PointerEvent');
+goog.require('ol.pointer.PointerEventHandler');
+
+
+/**
+ * @classdesc
+ * Events emitted as map browser events are instances of this type.
+ * See {@link ol.Map} for which events trigger a map browser event.
+ *
+ * @constructor
+ * @extends {ol.MapEvent}
+ * @implements {oli.MapBrowserEvent}
+ * @param {string} type Event type.
+ * @param {ol.Map} map Map.
+ * @param {Event} browserEvent Browser event.
+ * @param {boolean=} opt_dragging Is the map currently being dragged?
+ * @param {?olx.FrameState=} opt_frameState Frame state.
+ */
+ol.MapBrowserEvent = function(type, map, browserEvent, opt_dragging,
+    opt_frameState) {
+
+  ol.MapEvent.call(this, type, map, opt_frameState);
+
+  /**
+   * The original browser event.
+   * @const
+   * @type {Event}
+   * @api stable
+   */
+  this.originalEvent = browserEvent;
+
+  /**
+   * The pixel of the original browser event.
+   * @type {ol.Pixel}
+   * @api stable
+   */
+  this.pixel = map.getEventPixel(browserEvent);
+
+  /**
+   * The coordinate of the original browser event.
+   * @type {ol.Coordinate}
+   * @api stable
+   */
+  this.coordinate = map.getCoordinateFromPixel(this.pixel);
+
+  /**
+   * Indicates if the map is currently being dragged. Only set for
+   * `POINTERDRAG` and `POINTERMOVE` events. Default is `false`.
+   *
+   * @type {boolean}
+   * @api stable
+   */
+  this.dragging = opt_dragging !== undefined ? opt_dragging : false;
+
+};
+ol.inherits(ol.MapBrowserEvent, ol.MapEvent);
+
+
+/**
+ * Prevents the default browser action.
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/event.preventDefault
+ * @override
+ * @api stable
+ */
+ol.MapBrowserEvent.prototype.preventDefault = function() {
+  ol.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 stable
+ */
+ol.MapBrowserEvent.prototype.stopPropagation = function() {
+  ol.MapEvent.prototype.stopPropagation.call(this);
+  this.originalEvent.stopPropagation();
+};
+
+
+/**
+ * @constructor
+ * @extends {ol.MapBrowserEvent}
+ * @param {string} type Event type.
+ * @param {ol.Map} map Map.
+ * @param {ol.pointer.PointerEvent} pointerEvent Pointer event.
+ * @param {boolean=} opt_dragging Is the map currently being dragged?
+ * @param {?olx.FrameState=} opt_frameState Frame state.
+ */
+ol.MapBrowserPointerEvent = function(type, map, pointerEvent, opt_dragging,
+    opt_frameState) {
+
+  ol.MapBrowserEvent.call(this, type, map, pointerEvent.originalEvent, opt_dragging,
+      opt_frameState);
+
+  /**
+   * @const
+   * @type {ol.pointer.PointerEvent}
+   */
+  this.pointerEvent = pointerEvent;
+
+};
+ol.inherits(ol.MapBrowserPointerEvent, ol.MapBrowserEvent);
+
+
+/**
+ * @param {ol.Map} map The map with the viewport to listen to events on.
+ * @constructor
+ * @extends {ol.events.EventTarget}
+ */
+ol.MapBrowserEventHandler = function(map) {
+
+  ol.events.EventTarget.call(this);
+
+  /**
+   * This is the element that we will listen to the real events on.
+   * @type {ol.Map}
+   * @private
+   */
+  this.map_ = map;
+
+  /**
+   * @type {number}
+   * @private
+   */
+  this.clickTimeoutId_ = 0;
+
+  /**
+   * @type {boolean}
+   * @private
+   */
+  this.dragging_ = false;
+
+  /**
+   * @type {!Array.<ol.EventsKey>}
+   * @private
+   */
+  this.dragListenerKeys_ = [];
+
+  /**
+   * The most recent "down" type event (or null if none have occurred).
+   * Set on pointerdown.
+   * @type {ol.pointer.PointerEvent}
+   * @private
+   */
+  this.down_ = null;
+
+  var element = this.map_.getViewport();
+
+  /**
+   * @type {number}
+   * @private
+   */
+  this.activePointers_ = 0;
+
+  /**
+   * @type {!Object.<number, boolean>}
+   * @private
+   */
+  this.trackedTouches_ = {};
+
+  /**
+   * Event handler which generates pointer events for
+   * the viewport element.
+   *
+   * @type {ol.pointer.PointerEventHandler}
+   * @private
+   */
+  this.pointerEventHandler_ = new ol.pointer.PointerEventHandler(element);
+
+  /**
+   * Event handler which generates pointer events for
+   * the document (used when dragging).
+   *
+   * @type {ol.pointer.PointerEventHandler}
+   * @private
+   */
+  this.documentPointerEventHandler_ = null;
+
+  /**
+   * @type {?ol.EventsKey}
+   * @private
+   */
+  this.pointerdownListenerKey_ = ol.events.listen(this.pointerEventHandler_,
+      ol.pointer.EventType.POINTERDOWN,
+      this.handlePointerDown_, this);
+
+  /**
+   * @type {?ol.EventsKey}
+   * @private
+   */
+  this.relayedListenerKey_ = ol.events.listen(this.pointerEventHandler_,
+      ol.pointer.EventType.POINTERMOVE,
+      this.relayEvent_, this);
+
+};
+ol.inherits(ol.MapBrowserEventHandler, ol.events.EventTarget);
+
+
+/**
+ * @param {ol.pointer.PointerEvent} pointerEvent Pointer event.
+ * @private
+ */
+ol.MapBrowserEventHandler.prototype.emulateClick_ = function(pointerEvent) {
+  var newEvent;
+  newEvent = new ol.MapBrowserPointerEvent(
+      ol.MapBrowserEvent.EventType.CLICK, this.map_, pointerEvent);
+  this.dispatchEvent(newEvent);
+  if (this.clickTimeoutId_ !== 0) {
+    // double-click
+    ol.global.clearTimeout(this.clickTimeoutId_);
+    this.clickTimeoutId_ = 0;
+    newEvent = new ol.MapBrowserPointerEvent(
+        ol.MapBrowserEvent.EventType.DBLCLICK, this.map_, pointerEvent);
+    this.dispatchEvent(newEvent);
+  } else {
+    // click
+    this.clickTimeoutId_ = ol.global.setTimeout(function() {
+      this.clickTimeoutId_ = 0;
+      var newEvent = new ol.MapBrowserPointerEvent(
+          ol.MapBrowserEvent.EventType.SINGLECLICK, this.map_, pointerEvent);
+      this.dispatchEvent(newEvent);
+    }.bind(this), 250);
+  }
+};
+
+
+/**
+ * Keeps track on how many pointers are currently active.
+ *
+ * @param {ol.pointer.PointerEvent} pointerEvent Pointer event.
+ * @private
+ */
+ol.MapBrowserEventHandler.prototype.updateActivePointers_ = function(pointerEvent) {
+  var event = pointerEvent;
+
+  if (event.type == ol.MapBrowserEvent.EventType.POINTERUP ||
+      event.type == ol.MapBrowserEvent.EventType.POINTERCANCEL) {
+    delete this.trackedTouches_[event.pointerId];
+  } else if (event.type == ol.MapBrowserEvent.EventType.POINTERDOWN) {
+    this.trackedTouches_[event.pointerId] = true;
+  }
+  this.activePointers_ = Object.keys(this.trackedTouches_).length;
+};
+
+
+/**
+ * @param {ol.pointer.PointerEvent} pointerEvent Pointer event.
+ * @private
+ */
+ol.MapBrowserEventHandler.prototype.handlePointerUp_ = function(pointerEvent) {
+  this.updateActivePointers_(pointerEvent);
+  var newEvent = new ol.MapBrowserPointerEvent(
+      ol.MapBrowserEvent.EventType.POINTERUP, this.map_, pointerEvent);
+  this.dispatchEvent(newEvent);
+
+  // We emulate click events on left mouse button click, touch contact, and pen
+  // contact. isMouseActionButton returns true in these cases (evt.button is set
+  // to 0).
+  // See http://www.w3.org/TR/pointerevents/#button-states
+  if (!this.dragging_ && this.isMouseActionButton_(pointerEvent)) {
+    goog.asserts.assert(this.down_, 'this.down_ must be truthy');
+    this.emulateClick_(this.down_);
+  }
+
+  goog.asserts.assert(this.activePointers_ >= 0,
+      'this.activePointers_ should be equal to or larger than 0');
+  if (this.activePointers_ === 0) {
+    this.dragListenerKeys_.forEach(ol.events.unlistenByKey);
+    this.dragListenerKeys_.length = 0;
+    this.dragging_ = false;
+    this.down_ = null;
+    this.documentPointerEventHandler_.dispose();
+    this.documentPointerEventHandler_ = null;
+  }
+};
+
+
+/**
+ * @param {ol.pointer.PointerEvent} pointerEvent Pointer event.
+ * @return {boolean} If the left mouse button was pressed.
+ * @private
+ */
+ol.MapBrowserEventHandler.prototype.isMouseActionButton_ = function(pointerEvent) {
+  return pointerEvent.button === 0;
+};
+
+
+/**
+ * @param {ol.pointer.PointerEvent} pointerEvent Pointer event.
+ * @private
+ */
+ol.MapBrowserEventHandler.prototype.handlePointerDown_ = function(pointerEvent) {
+  this.updateActivePointers_(pointerEvent);
+  var newEvent = new ol.MapBrowserPointerEvent(
+      ol.MapBrowserEvent.EventType.POINTERDOWN, this.map_, pointerEvent);
+  this.dispatchEvent(newEvent);
+
+  this.down_ = pointerEvent;
+
+  if (this.dragListenerKeys_.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 ol.pointer.PointerEventHandler(document);
+
+    this.dragListenerKeys_.push(
+      ol.events.listen(this.documentPointerEventHandler_,
+          ol.MapBrowserEvent.EventType.POINTERMOVE,
+          this.handlePointerMove_, this),
+      ol.events.listen(this.documentPointerEventHandler_,
+          ol.MapBrowserEvent.EventType.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 following: `TouchSource.vacuumTouches_()`
+       * issues `pointercancel` events, when there was no `touchend` for a
+       * `touchstart`. Now, let's say a first `touchstart` is registered on
+       * `pointerEventHandler_`. The `documentPointerEventHandler_` is set up.
+       * But `documentPointerEventHandler_` doesn't know about the first
+       * `touchstart`. If there is no `touchend` for the `touchstart`, we can
+       * only receive a `touchcancel` from `pointerEventHandler_`, because it is
+       * only registered there.
+       */
+      ol.events.listen(this.pointerEventHandler_,
+          ol.MapBrowserEvent.EventType.POINTERCANCEL,
+          this.handlePointerUp_, this)
+    );
+  }
+};
+
+
+/**
+ * @param {ol.pointer.PointerEvent} pointerEvent Pointer event.
+ * @private
+ */
+ol.MapBrowserEventHandler.prototype.handlePointerMove_ = function(pointerEvent) {
+  // Fix IE10 on windows Surface : When you tap the tablet, it triggers
+  // multiple pointermove events between pointerdown and pointerup with
+  // the exact same coordinates of the pointerdown event. To avoid a
+  // 'false' touchmove event to be dispatched , we test if the pointer
+  // effectively moved.
+  if (this.isMoving_(pointerEvent)) {
+    this.dragging_ = true;
+    var newEvent = new ol.MapBrowserPointerEvent(
+        ol.MapBrowserEvent.EventType.POINTERDRAG, this.map_, pointerEvent,
+        this.dragging_);
+    this.dispatchEvent(newEvent);
+  }
+
+  // Some native android browser triggers mousemove events during small period
+  // of time. See: https://code.google.com/p/android/issues/detail?id=5491 or
+  // https://code.google.com/p/android/issues/detail?id=19827
+  // ex: Galaxy Tab P3110 + Android 4.1.1
+  pointerEvent.preventDefault();
+};
+
+
+/**
+ * Wrap and relay a pointer event.  Note that this requires that the type
+ * string for the MapBrowserPointerEvent matches the PointerEvent type.
+ * @param {ol.pointer.PointerEvent} pointerEvent Pointer event.
+ * @private
+ */
+ol.MapBrowserEventHandler.prototype.relayEvent_ = function(pointerEvent) {
+  var dragging = !!(this.down_ && this.isMoving_(pointerEvent));
+  this.dispatchEvent(new ol.MapBrowserPointerEvent(
+      pointerEvent.type, this.map_, pointerEvent, dragging));
+};
+
+
+/**
+ * @param {ol.pointer.PointerEvent} pointerEvent Pointer event.
+ * @return {boolean} Is moving.
+ * @private
+ */
+ol.MapBrowserEventHandler.prototype.isMoving_ = function(pointerEvent) {
+  return pointerEvent.clientX != this.down_.clientX ||
+      pointerEvent.clientY != this.down_.clientY;
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.MapBrowserEventHandler.prototype.disposeInternal = function() {
+  if (this.relayedListenerKey_) {
+    ol.events.unlistenByKey(this.relayedListenerKey_);
+    this.relayedListenerKey_ = null;
+  }
+  if (this.pointerdownListenerKey_) {
+    ol.events.unlistenByKey(this.pointerdownListenerKey_);
+    this.pointerdownListenerKey_ = null;
+  }
+
+  this.dragListenerKeys_.forEach(ol.events.unlistenByKey);
+  this.dragListenerKeys_.length = 0;
+
+  if (this.documentPointerEventHandler_) {
+    this.documentPointerEventHandler_.dispose();
+    this.documentPointerEventHandler_ = null;
+  }
+  if (this.pointerEventHandler_) {
+    this.pointerEventHandler_.dispose();
+    this.pointerEventHandler_ = null;
+  }
+  ol.events.EventTarget.prototype.disposeInternal.call(this);
+};
+
+
+/**
+ * Constants for event names.
+ * @enum {string}
+ */
+ol.MapBrowserEvent.EventType = {
+
+  /**
+   * A true single click with no dragging and no double click. Note that this
+   * event is delayed by 250 ms to ensure that it is not a double click.
+   * @event ol.MapBrowserEvent#singleclick
+   * @api stable
+   */
+  SINGLECLICK: 'singleclick',
+
+  /**
+   * A click with no dragging. A double click will fire two of this.
+   * @event ol.MapBrowserEvent#click
+   * @api stable
+   */
+  CLICK: ol.events.EventType.CLICK,
+
+  /**
+   * A true double click, with no dragging.
+   * @event ol.MapBrowserEvent#dblclick
+   * @api stable
+   */
+  DBLCLICK: ol.events.EventType.DBLCLICK,
+
+  /**
+   * Triggered when a pointer is dragged.
+   * @event ol.MapBrowserEvent#pointerdrag
+   * @api
+   */
+  POINTERDRAG: 'pointerdrag',
+
+  /**
+   * Triggered when a pointer is moved. Note that on touch devices this is
+   * triggered when the map is panned, so is not the same as mousemove.
+   * @event ol.MapBrowserEvent#pointermove
+   * @api stable
+   */
+  POINTERMOVE: 'pointermove',
+
+  POINTERDOWN: 'pointerdown',
+  POINTERUP: 'pointerup',
+  POINTEROVER: 'pointerover',
+  POINTEROUT: 'pointerout',
+  POINTERENTER: 'pointerenter',
+  POINTERLEAVE: 'pointerleave',
+  POINTERCANCEL: 'pointercancel'
+};
+
+goog.provide('ol.layer.Base');
+goog.provide('ol.layer.LayerProperty');
+
+goog.require('ol');
+goog.require('ol.Object');
+goog.require('ol.math');
+goog.require('ol.object');
+goog.require('ol.source.State');
+
+
+/**
+ * @enum {string}
+ */
+ol.layer.LayerProperty = {
+  OPACITY: 'opacity',
+  VISIBLE: 'visible',
+  EXTENT: 'extent',
+  Z_INDEX: 'zIndex',
+  MAX_RESOLUTION: 'maxResolution',
+  MIN_RESOLUTION: 'minResolution',
+  SOURCE: 'source'
+};
+
+
+/**
+ * @classdesc
+ * Abstract base class; normally only used for creating subclasses and not
+ * instantiated in apps.
+ * Note that with `ol.layer.Base` and all its subclasses, any property set in
+ * the options is set as a {@link ol.Object} property on the layer object, so
+ * is observable, and has get/set accessors.
+ *
+ * @constructor
+ * @extends {ol.Object}
+ * @param {olx.layer.BaseOptions} options Layer options.
+ * @api stable
+ */
+ol.layer.Base = function(options) {
+
+  ol.Object.call(this);
+
+  /**
+   * @type {Object.<string, *>}
+   */
+  var properties = ol.object.assign({}, options);
+  properties[ol.layer.LayerProperty.OPACITY] =
+      options.opacity !== undefined ? options.opacity : 1;
+  properties[ol.layer.LayerProperty.VISIBLE] =
+      options.visible !== undefined ? options.visible : true;
+  properties[ol.layer.LayerProperty.Z_INDEX] =
+      options.zIndex !== undefined ? options.zIndex : 0;
+  properties[ol.layer.LayerProperty.MAX_RESOLUTION] =
+      options.maxResolution !== undefined ? options.maxResolution : Infinity;
+  properties[ol.layer.LayerProperty.MIN_RESOLUTION] =
+      options.minResolution !== undefined ? options.minResolution : 0;
+
+  this.setProperties(properties);
+};
+ol.inherits(ol.layer.Base, ol.Object);
+
+
+/**
+ * @return {ol.LayerState} Layer state.
+ */
+ol.layer.Base.prototype.getLayerState = function() {
+  var opacity = this.getOpacity();
+  var sourceState = this.getSourceState();
+  var visible = this.getVisible();
+  var extent = this.getExtent();
+  var zIndex = this.getZIndex();
+  var maxResolution = this.getMaxResolution();
+  var minResolution = this.getMinResolution();
+  return {
+    layer: /** @type {ol.layer.Layer} */ (this),
+    opacity: ol.math.clamp(opacity, 0, 1),
+    sourceState: sourceState,
+    visible: visible,
+    managed: true,
+    extent: extent,
+    zIndex: zIndex,
+    maxResolution: maxResolution,
+    minResolution: Math.max(minResolution, 0)
+  };
+};
+
+
+/**
+ * @param {Array.<ol.layer.Layer>=} opt_array Array of layers (to be
+ *     modified in place).
+ * @return {Array.<ol.layer.Layer>} Array of layers.
+ */
+ol.layer.Base.prototype.getLayersArray = goog.abstractMethod;
+
+
+/**
+ * @param {Array.<ol.LayerState>=} opt_states Optional list of layer
+ *     states (to be modified in place).
+ * @return {Array.<ol.LayerState>} List of layer states.
+ */
+ol.layer.Base.prototype.getLayerStatesArray = goog.abstractMethod;
+
+
+/**
+ * Return the {@link ol.Extent extent} of the layer or `undefined` if it
+ * will be visible regardless of extent.
+ * @return {ol.Extent|undefined} The layer extent.
+ * @observable
+ * @api stable
+ */
+ol.layer.Base.prototype.getExtent = function() {
+  return /** @type {ol.Extent|undefined} */ (
+      this.get(ol.layer.LayerProperty.EXTENT));
+};
+
+
+/**
+ * Return the maximum resolution of the layer.
+ * @return {number} The maximum resolution of the layer.
+ * @observable
+ * @api stable
+ */
+ol.layer.Base.prototype.getMaxResolution = function() {
+  return /** @type {number} */ (
+      this.get(ol.layer.LayerProperty.MAX_RESOLUTION));
+};
+
+
+/**
+ * Return the minimum resolution of the layer.
+ * @return {number} The minimum resolution of the layer.
+ * @observable
+ * @api stable
+ */
+ol.layer.Base.prototype.getMinResolution = function() {
+  return /** @type {number} */ (
+      this.get(ol.layer.LayerProperty.MIN_RESOLUTION));
+};
+
+
+/**
+ * Return the opacity of the layer (between 0 and 1).
+ * @return {number} The opacity of the layer.
+ * @observable
+ * @api stable
+ */
+ol.layer.Base.prototype.getOpacity = function() {
+  return /** @type {number} */ (this.get(ol.layer.LayerProperty.OPACITY));
+};
+
+
+/**
+ * @return {ol.source.State} Source state.
+ */
+ol.layer.Base.prototype.getSourceState = goog.abstractMethod;
+
+
+/**
+ * Return the visibility of the layer (`true` or `false`).
+ * @return {boolean} The visibility of the layer.
+ * @observable
+ * @api stable
+ */
+ol.layer.Base.prototype.getVisible = function() {
+  return /** @type {boolean} */ (this.get(ol.layer.LayerProperty.VISIBLE));
+};
+
+
+/**
+ * Return the Z-index of the layer, which is used to order layers before
+ * rendering. The default Z-index is 0.
+ * @return {number} The Z-index of the layer.
+ * @observable
+ * @api
+ */
+ol.layer.Base.prototype.getZIndex = function() {
+  return /** @type {number} */ (this.get(ol.layer.LayerProperty.Z_INDEX));
+};
+
+
+/**
+ * Set the extent at which the layer is visible.  If `undefined`, the layer
+ * will be visible at all extents.
+ * @param {ol.Extent|undefined} extent The extent of the layer.
+ * @observable
+ * @api stable
+ */
+ol.layer.Base.prototype.setExtent = function(extent) {
+  this.set(ol.layer.LayerProperty.EXTENT, extent);
+};
+
+
+/**
+ * Set the maximum resolution at which the layer is visible.
+ * @param {number} maxResolution The maximum resolution of the layer.
+ * @observable
+ * @api stable
+ */
+ol.layer.Base.prototype.setMaxResolution = function(maxResolution) {
+  this.set(ol.layer.LayerProperty.MAX_RESOLUTION, maxResolution);
+};
+
+
+/**
+ * Set the minimum resolution at which the layer is visible.
+ * @param {number} minResolution The minimum resolution of the layer.
+ * @observable
+ * @api stable
+ */
+ol.layer.Base.prototype.setMinResolution = function(minResolution) {
+  this.set(ol.layer.LayerProperty.MIN_RESOLUTION, minResolution);
+};
+
+
+/**
+ * Set the opacity of the layer, allowed values range from 0 to 1.
+ * @param {number} opacity The opacity of the layer.
+ * @observable
+ * @api stable
+ */
+ol.layer.Base.prototype.setOpacity = function(opacity) {
+  this.set(ol.layer.LayerProperty.OPACITY, opacity);
+};
+
+
+/**
+ * Set the visibility of the layer (`true` or `false`).
+ * @param {boolean} visible The visibility of the layer.
+ * @observable
+ * @api stable
+ */
+ol.layer.Base.prototype.setVisible = function(visible) {
+  this.set(ol.layer.LayerProperty.VISIBLE, visible);
+};
+
+
+/**
+ * Set Z-index of the layer, which is used to order layers before rendering.
+ * The default Z-index is 0.
+ * @param {number} zindex The z-index of the layer.
+ * @observable
+ * @api
+ */
+ol.layer.Base.prototype.setZIndex = function(zindex) {
+  this.set(ol.layer.LayerProperty.Z_INDEX, zindex);
+};
+
+goog.provide('ol.render.VectorContext');
+
+
+/**
+ * Context for drawing geometries.  A vector context is available on render
+ * events and does not need to be constructed directly.
+ * @constructor
+ * @struct
+ * @api
+ */
+ol.render.VectorContext = function() {
+};
+
+
+/**
+ * Render a geometry.
+ *
+ * @param {ol.geom.Geometry} geometry The geometry to render.
+ */
+ol.render.VectorContext.prototype.drawGeometry = goog.abstractMethod;
+
+
+/**
+ * Set the rendering style.
+ *
+ * @param {ol.style.Style} style The rendering style.
+ */
+ol.render.VectorContext.prototype.setStyle = goog.abstractMethod;
+
+
+/**
+ * @param {ol.geom.Circle} circleGeometry Circle geometry.
+ * @param {ol.Feature} feature Feature,
+ */
+ol.render.VectorContext.prototype.drawCircle = goog.abstractMethod;
+
+
+/**
+ * @param {ol.Feature} feature Feature.
+ * @param {ol.style.Style} style Style.
+ */
+ol.render.VectorContext.prototype.drawFeature = goog.abstractMethod;
+
+
+/**
+ * @param {ol.geom.GeometryCollection} geometryCollectionGeometry Geometry
+ *     collection.
+ * @param {ol.Feature} feature Feature.
+ */
+ol.render.VectorContext.prototype.drawGeometryCollection = goog.abstractMethod;
+
+
+/**
+ * @param {ol.geom.LineString|ol.render.Feature} lineStringGeometry Line
+ *     string geometry.
+ * @param {ol.Feature|ol.render.Feature} feature Feature.
+ */
+ol.render.VectorContext.prototype.drawLineString = goog.abstractMethod;
+
+
+/**
+ * @param {ol.geom.MultiLineString|ol.render.Feature} multiLineStringGeometry
+ *     MultiLineString geometry.
+ * @param {ol.Feature|ol.render.Feature} feature Feature.
+ */
+ol.render.VectorContext.prototype.drawMultiLineString = goog.abstractMethod;
+
+
+/**
+ * @param {ol.geom.MultiPoint|ol.render.Feature} multiPointGeometry MultiPoint
+ *     geometry.
+ * @param {ol.Feature|ol.render.Feature} feature Feature.
+ */
+ol.render.VectorContext.prototype.drawMultiPoint = goog.abstractMethod;
+
+
+/**
+ * @param {ol.geom.MultiPolygon} multiPolygonGeometry MultiPolygon geometry.
+ * @param {ol.Feature} feature Feature.
+ */
+ol.render.VectorContext.prototype.drawMultiPolygon = goog.abstractMethod;
+
+
+/**
+ * @param {ol.geom.Point|ol.render.Feature} pointGeometry Point geometry.
+ * @param {ol.Feature|ol.render.Feature} feature Feature.
+ */
+ol.render.VectorContext.prototype.drawPoint = goog.abstractMethod;
+
+
+/**
+ * @param {ol.geom.Polygon|ol.render.Feature} polygonGeometry Polygon
+ *     geometry.
+ * @param {ol.Feature|ol.render.Feature} feature Feature.
+ */
+ol.render.VectorContext.prototype.drawPolygon = goog.abstractMethod;
+
+
+/**
+ * @param {Array.<number>} flatCoordinates Flat coordinates.
+ * @param {number} offset Offset.
+ * @param {number} end End.
+ * @param {number} stride Stride.
+ * @param {ol.geom.Geometry|ol.render.Feature} geometry Geometry.
+ * @param {ol.Feature|ol.render.Feature} feature Feature.
+ */
+ol.render.VectorContext.prototype.drawText = goog.abstractMethod;
+
+
+/**
+ * @param {ol.style.Fill} fillStyle Fill style.
+ * @param {ol.style.Stroke} strokeStyle Stroke style.
+ */
+ol.render.VectorContext.prototype.setFillStrokeStyle = goog.abstractMethod;
+
+
+/**
+ * @param {ol.style.Image} imageStyle Image style.
+ */
+ol.render.VectorContext.prototype.setImageStyle = goog.abstractMethod;
+
+
+/**
+ * @param {ol.style.Text} textStyle Text style.
+ */
+ol.render.VectorContext.prototype.setTextStyle = goog.abstractMethod;
+
+goog.provide('ol.render.Event');
+goog.provide('ol.render.EventType');
+
+goog.require('ol.events.Event');
+goog.require('ol.render.VectorContext');
+
+
+/**
+ * @enum {string}
+ */
+ol.render.EventType = {
+  /**
+   * @event ol.render.Event#postcompose
+   * @api
+   */
+  POSTCOMPOSE: 'postcompose',
+  /**
+   * @event ol.render.Event#precompose
+   * @api
+   */
+  PRECOMPOSE: 'precompose',
+  /**
+   * @event ol.render.Event#render
+   * @api
+   */
+  RENDER: 'render'
+};
+
+
+/**
+ * @constructor
+ * @extends {ol.events.Event}
+ * @implements {oli.render.Event}
+ * @param {ol.render.EventType} type Type.
+ * @param {Object=} opt_target Target.
+ * @param {ol.render.VectorContext=} opt_vectorContext Vector context.
+ * @param {olx.FrameState=} opt_frameState Frame state.
+ * @param {?CanvasRenderingContext2D=} opt_context Context.
+ * @param {?ol.webgl.Context=} opt_glContext WebGL Context.
+ */
+ol.render.Event = function(
+    type, opt_target, opt_vectorContext, opt_frameState, opt_context,
+    opt_glContext) {
+
+  ol.events.Event.call(this, type, opt_target);
+
+  /**
+   * For canvas, this is an instance of {@link ol.render.canvas.Immediate}.
+   * @type {ol.render.VectorContext|undefined}
+   * @api
+   */
+  this.vectorContext = opt_vectorContext;
+
+  /**
+   * An object representing the current render frame state.
+   * @type {olx.FrameState|undefined}
+   * @api
+   */
+  this.frameState = opt_frameState;
+
+  /**
+   * Canvas context. Only available when a Canvas renderer is used, null
+   * otherwise.
+   * @type {CanvasRenderingContext2D|null|undefined}
+   * @api
+   */
+  this.context = opt_context;
+
+  /**
+   * WebGL context. Only available when a WebGL renderer is used, null
+   * otherwise.
+   * @type {ol.webgl.Context|null|undefined}
+   * @api
+   */
+  this.glContext = opt_glContext;
+
+};
+ol.inherits(ol.render.Event, ol.events.Event);
+
+goog.provide('ol.layer.Layer');
+
+goog.require('ol.events');
+goog.require('ol.events.EventType');
+goog.require('ol');
+goog.require('ol.Object');
+goog.require('ol.layer.Base');
+goog.require('ol.layer.LayerProperty');
+goog.require('ol.object');
+goog.require('ol.render.EventType');
+goog.require('ol.source.State');
+
+
+/**
+ * @classdesc
+ * Abstract base class; normally only used for creating subclasses and not
+ * instantiated in apps.
+ * A visual representation of raster or vector map data.
+ * Layers group together those properties that pertain to how the data is to be
+ * displayed, irrespective of the source of that data.
+ *
+ * Layers are usually added to a map with {@link ol.Map#addLayer}. Components
+ * like {@link ol.interaction.Select} use unmanaged layers internally. These
+ * unmanaged layers are associated with the map using
+ * {@link ol.layer.Layer#setMap} instead.
+ *
+ * A generic `change` event is fired when the state of the source changes.
+ *
+ * @constructor
+ * @extends {ol.layer.Base}
+ * @fires ol.render.Event
+ * @param {olx.layer.LayerOptions} options Layer options.
+ * @api stable
+ */
+ol.layer.Layer = function(options) {
+
+  var baseOptions = ol.object.assign({}, options);
+  delete baseOptions.source;
+
+  ol.layer.Base.call(this, /** @type {olx.layer.BaseOptions} */ (baseOptions));
+
+  /**
+   * @private
+   * @type {?ol.EventsKey}
+   */
+  this.mapPrecomposeKey_ = null;
+
+  /**
+   * @private
+   * @type {?ol.EventsKey}
+   */
+  this.mapRenderKey_ = null;
+
+  /**
+   * @private
+   * @type {?ol.EventsKey}
+   */
+  this.sourceChangeKey_ = null;
+
+  if (options.map) {
+    this.setMap(options.map);
+  }
+
+  ol.events.listen(this,
+      ol.Object.getChangeEventType(ol.layer.LayerProperty.SOURCE),
+      this.handleSourcePropertyChange_, this);
+
+  var source = options.source ? options.source : null;
+  this.setSource(source);
+};
+ol.inherits(ol.layer.Layer, ol.layer.Base);
+
+
+/**
+ * Return `true` if the layer is visible, and if the passed resolution is
+ * between the layer's minResolution and maxResolution. The comparison is
+ * inclusive for `minResolution` and exclusive for `maxResolution`.
+ * @param {ol.LayerState} layerState Layer state.
+ * @param {number} resolution Resolution.
+ * @return {boolean} The layer is visible at the given resolution.
+ */
+ol.layer.Layer.visibleAtResolution = function(layerState, resolution) {
+  return layerState.visible && resolution >= layerState.minResolution &&
+      resolution < layerState.maxResolution;
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.layer.Layer.prototype.getLayersArray = function(opt_array) {
+  var array = opt_array ? opt_array : [];
+  array.push(this);
+  return array;
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.layer.Layer.prototype.getLayerStatesArray = function(opt_states) {
+  var states = opt_states ? opt_states : [];
+  states.push(this.getLayerState());
+  return states;
+};
+
+
+/**
+ * Get the layer source.
+ * @return {ol.source.Source} The layer source (or `null` if not yet set).
+ * @observable
+ * @api stable
+ */
+ol.layer.Layer.prototype.getSource = function() {
+  var source = this.get(ol.layer.LayerProperty.SOURCE);
+  return /** @type {ol.source.Source} */ (source) || null;
+};
+
+
+/**
+  * @inheritDoc
+  */
+ol.layer.Layer.prototype.getSourceState = function() {
+  var source = this.getSource();
+  return !source ? ol.source.State.UNDEFINED : source.getState();
+};
+
+
+/**
+ * @private
+ */
+ol.layer.Layer.prototype.handleSourceChange_ = function() {
+  this.changed();
+};
+
+
+/**
+ * @private
+ */
+ol.layer.Layer.prototype.handleSourcePropertyChange_ = function() {
+  if (this.sourceChangeKey_) {
+    ol.events.unlistenByKey(this.sourceChangeKey_);
+    this.sourceChangeKey_ = null;
+  }
+  var source = this.getSource();
+  if (source) {
+    this.sourceChangeKey_ = ol.events.listen(source,
+        ol.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 collection, and the callback in
+ * {@link ol.Map#forEachLayerAtPixel} will receive `null` as layer. This
+ * is useful for temporary layers. To remove an unmanaged layer from the map,
+ * use `#setMap(null)`.
+ *
+ * To add the layer to a map and have it managed by the map, use
+ * {@link ol.Map#addLayer} instead.
+ * @param {ol.Map} map Map.
+ * @api
+ */
+ol.layer.Layer.prototype.setMap = function(map) {
+  if (this.mapPrecomposeKey_) {
+    ol.events.unlistenByKey(this.mapPrecomposeKey_);
+    this.mapPrecomposeKey_ = null;
+  }
+  if (!map) {
+    this.changed();
+  }
+  if (this.mapRenderKey_) {
+    ol.events.unlistenByKey(this.mapRenderKey_);
+    this.mapRenderKey_ = null;
+  }
+  if (map) {
+    this.mapPrecomposeKey_ = ol.events.listen(
+        map, ol.render.EventType.PRECOMPOSE, function(evt) {
+          var layerState = this.getLayerState();
+          layerState.managed = false;
+          layerState.zIndex = Infinity;
+          evt.frameState.layerStatesArray.push(layerState);
+          evt.frameState.layerStates[goog.getUid(this)] = layerState;
+        }, this);
+    this.mapRenderKey_ = ol.events.listen(
+        this, ol.events.EventType.CHANGE, map.render, map);
+    this.changed();
+  }
+};
+
+
+/**
+ * Set the layer source.
+ * @param {ol.source.Source} source The layer source.
+ * @observable
+ * @api stable
+ */
+ol.layer.Layer.prototype.setSource = function(source) {
+  this.set(ol.layer.LayerProperty.SOURCE, source);
+};
+
+goog.provide('ol.ImageBase');
+goog.provide('ol.ImageState');
+
+goog.require('goog.asserts');
+goog.require('ol.events.EventTarget');
+goog.require('ol.events.EventType');
+goog.require('ol.Attribution');
+
+
+/**
+ * @enum {number}
+ */
+ol.ImageState = {
+  IDLE: 0,
+  LOADING: 1,
+  LOADED: 2,
+  ERROR: 3
+};
+
+
+/**
+ * @constructor
+ * @extends {ol.events.EventTarget}
+ * @param {ol.Extent} extent Extent.
+ * @param {number|undefined} resolution Resolution.
+ * @param {number} pixelRatio Pixel ratio.
+ * @param {ol.ImageState} state State.
+ * @param {Array.<ol.Attribution>} attributions Attributions.
+ */
+ol.ImageBase = function(extent, resolution, pixelRatio, state, attributions) {
+
+  ol.events.EventTarget.call(this);
+
+  /**
+   * @private
+   * @type {Array.<ol.Attribution>}
+   */
+  this.attributions_ = attributions;
+
+  /**
+   * @protected
+   * @type {ol.Extent}
+   */
+  this.extent = extent;
+
+  /**
+   * @private
+   * @type {number}
+   */
+  this.pixelRatio_ = pixelRatio;
+
+  /**
+   * @protected
+   * @type {number|undefined}
+   */
+  this.resolution = resolution;
+
+  /**
+   * @protected
+   * @type {ol.ImageState}
+   */
+  this.state = state;
+
+};
+ol.inherits(ol.ImageBase, ol.events.EventTarget);
+
+
+/**
+ * @protected
+ */
+ol.ImageBase.prototype.changed = function() {
+  this.dispatchEvent(ol.events.EventType.CHANGE);
+};
+
+
+/**
+ * @return {Array.<ol.Attribution>} Attributions.
+ */
+ol.ImageBase.prototype.getAttributions = function() {
+  return this.attributions_;
+};
+
+
+/**
+ * @return {ol.Extent} Extent.
+ */
+ol.ImageBase.prototype.getExtent = function() {
+  return this.extent;
+};
+
+
+/**
+ * @param {Object=} opt_context Object.
+ * @return {HTMLCanvasElement|Image|HTMLVideoElement} Image.
+ */
+ol.ImageBase.prototype.getImage = goog.abstractMethod;
+
+
+/**
+ * @return {number} PixelRatio.
+ */
+ol.ImageBase.prototype.getPixelRatio = function() {
+  return this.pixelRatio_;
+};
+
+
+/**
+ * @return {number} Resolution.
+ */
+ol.ImageBase.prototype.getResolution = function() {
+  goog.asserts.assert(this.resolution !== undefined, 'resolution not yet set');
+  return this.resolution;
+};
+
+
+/**
+ * @return {ol.ImageState} State.
+ */
+ol.ImageBase.prototype.getState = function() {
+  return this.state;
+};
+
+
+/**
+ * Load not yet loaded URI.
+ */
+ol.ImageBase.prototype.load = goog.abstractMethod;
+
+goog.provide('ol.vec.Mat4');
+goog.provide('ol.vec.Mat4.Number');
+
+goog.require('goog.vec.Mat4');
+
+
+/**
+ * A alias for the goog.vec.Number type.
+ * @typedef {goog.vec.Number}
+ */
+ol.vec.Mat4.Number;
+
+
+/**
+ * @param {!goog.vec.Mat4.Number} mat Matrix.
+ * @param {number} translateX1 Translate X1.
+ * @param {number} translateY1 Translate Y1.
+ * @param {number} scaleX Scale X.
+ * @param {number} scaleY Scale Y.
+ * @param {number} rotation Rotation.
+ * @param {number} translateX2 Translate X2.
+ * @param {number} translateY2 Translate Y2.
+ * @return {!goog.vec.Mat4.Number} Matrix.
+ */
+ol.vec.Mat4.makeTransform2D = function(mat, translateX1, translateY1,
+    scaleX, scaleY, rotation, translateX2, translateY2) {
+  goog.vec.Mat4.makeIdentity(mat);
+  if (translateX1 !== 0 || translateY1 !== 0) {
+    goog.vec.Mat4.translate(mat, translateX1, translateY1, 0);
+  }
+  if (scaleX != 1 || scaleY != 1) {
+    goog.vec.Mat4.scale(mat, scaleX, scaleY, 1);
+  }
+  if (rotation !== 0) {
+    goog.vec.Mat4.rotateZ(mat, rotation);
+  }
+  if (translateX2 !== 0 || translateY2 !== 0) {
+    goog.vec.Mat4.translate(mat, translateX2, translateY2, 0);
+  }
+  return mat;
+};
+
+
+/**
+ * Returns true if mat1 and mat2 represent the same 2D transformation.
+ * @param {goog.vec.Mat4.Number} mat1 Matrix 1.
+ * @param {goog.vec.Mat4.Number} mat2 Matrix 2.
+ * @return {boolean} Equal 2D.
+ */
+ol.vec.Mat4.equals2D = function(mat1, mat2) {
+  return (
+      goog.vec.Mat4.getElement(mat1, 0, 0) ==
+      goog.vec.Mat4.getElement(mat2, 0, 0) &&
+      goog.vec.Mat4.getElement(mat1, 1, 0) ==
+      goog.vec.Mat4.getElement(mat2, 1, 0) &&
+      goog.vec.Mat4.getElement(mat1, 0, 1) ==
+      goog.vec.Mat4.getElement(mat2, 0, 1) &&
+      goog.vec.Mat4.getElement(mat1, 1, 1) ==
+      goog.vec.Mat4.getElement(mat2, 1, 1) &&
+      goog.vec.Mat4.getElement(mat1, 0, 3) ==
+      goog.vec.Mat4.getElement(mat2, 0, 3) &&
+      goog.vec.Mat4.getElement(mat1, 1, 3) ==
+      goog.vec.Mat4.getElement(mat2, 1, 3));
+};
+
+
+/**
+ * Transforms the given vector with the given matrix storing the resulting,
+ * transformed vector into resultVec. The input vector is multiplied against the
+ * upper 2x4 matrix omitting the projective component.
+ *
+ * @param {goog.vec.Mat4.Number} mat The matrix supplying the transformation.
+ * @param {Array.<number>} vec The 3 element vector to transform.
+ * @param {Array.<number>} resultVec The 3 element vector to receive the results
+ *     (may be vec).
+ * @return {Array.<number>} return resultVec so that operations can be
+ *     chained together.
+ */
+ol.vec.Mat4.multVec2 = function(mat, vec, resultVec) {
+  var m00 = goog.vec.Mat4.getElement(mat, 0, 0);
+  var m10 = goog.vec.Mat4.getElement(mat, 1, 0);
+  var m01 = goog.vec.Mat4.getElement(mat, 0, 1);
+  var m11 = goog.vec.Mat4.getElement(mat, 1, 1);
+  var m03 = goog.vec.Mat4.getElement(mat, 0, 3);
+  var m13 = goog.vec.Mat4.getElement(mat, 1, 3);
+  var x = vec[0], y = vec[1];
+  resultVec[0] = m00 * x + m01 * y + m03;
+  resultVec[1] = m10 * x + m11 * y + m13;
+  return resultVec;
+};
+
+goog.provide('ol.renderer.Layer');
+
+goog.require('goog.asserts');
+goog.require('ol.events');
+goog.require('ol.events.EventType');
+goog.require('ol');
+goog.require('ol.functions');
+goog.require('ol.ImageState');
+goog.require('ol.Observable');
+goog.require('ol.TileRange');
+goog.require('ol.TileState');
+goog.require('ol.layer.Layer');
+goog.require('ol.source.Source');
+goog.require('ol.source.State');
+goog.require('ol.source.Tile');
+goog.require('ol.vec.Mat4');
+
+
+/**
+ * @constructor
+ * @extends {ol.Observable}
+ * @param {ol.layer.Layer} layer Layer.
+ * @struct
+ */
+ol.renderer.Layer = function(layer) {
+
+  ol.Observable.call(this);
+
+  /**
+   * @private
+   * @type {ol.layer.Layer}
+   */
+  this.layer_ = layer;
+
+
+};
+ol.inherits(ol.renderer.Layer, ol.Observable);
+
+
+/**
+ * @param {ol.Coordinate} coordinate Coordinate.
+ * @param {olx.FrameState} frameState Frame state.
+ * @param {function(this: S, (ol.Feature|ol.render.Feature), ol.layer.Layer): T}
+ *     callback Feature callback.
+ * @param {S} thisArg Value to use as `this` when executing `callback`.
+ * @return {T|undefined} Callback result.
+ * @template S,T
+ */
+ol.renderer.Layer.prototype.forEachFeatureAtCoordinate = ol.nullFunction;
+
+
+/**
+ * @param {ol.Pixel} pixel Pixel.
+ * @param {olx.FrameState} frameState Frame state.
+ * @param {function(this: S, ol.layer.Layer): T} callback Layer callback.
+ * @param {S} thisArg Value to use as `this` when executing `callback`.
+ * @return {T|undefined} Callback result.
+ * @template S,T
+ */
+ol.renderer.Layer.prototype.forEachLayerAtPixel = function(pixel, frameState, callback, thisArg) {
+  var coordinate = pixel.slice();
+  ol.vec.Mat4.multVec2(
+      frameState.pixelToCoordinateMatrix, coordinate, coordinate);
+
+  var hasFeature = this.forEachFeatureAtCoordinate(
+      coordinate, frameState, ol.functions.TRUE, this);
+
+  if (hasFeature) {
+    return callback.call(thisArg, this.layer_);
+  } else {
+    return undefined;
+  }
+};
+
+
+/**
+ * @param {ol.Coordinate} coordinate Coordinate.
+ * @param {olx.FrameState} frameState Frame state.
+ * @return {boolean} Is there a feature at the given coordinate?
+ */
+ol.renderer.Layer.prototype.hasFeatureAtCoordinate = ol.functions.FALSE;
+
+
+/**
+ * Create a function that adds loaded tiles to the tile lookup.
+ * @param {ol.source.Tile} source Tile source.
+ * @param {ol.proj.Projection} projection Projection of the tiles.
+ * @param {Object.<number, Object.<string, ol.Tile>>} tiles Lookup of loaded
+ *     tiles by zoom level.
+ * @return {function(number, ol.TileRange):boolean} A function that can be
+ *     called with a zoom level and a tile range to add loaded tiles to the
+ *     lookup.
+ * @protected
+ */
+ol.renderer.Layer.prototype.createLoadedTileFinder = function(source, projection, tiles) {
+  return (
+      /**
+       * @param {number} zoom Zoom level.
+       * @param {ol.TileRange} tileRange Tile range.
+       * @return {boolean} The tile range is fully loaded.
+       */
+      function(zoom, tileRange) {
+        function callback(tile) {
+          if (!tiles[zoom]) {
+            tiles[zoom] = {};
+          }
+          tiles[zoom][tile.tileCoord.toString()] = tile;
+        }
+        return source.forEachLoadedTile(projection, zoom, tileRange, callback);
+      });
+};
+
+
+/**
+ * @return {ol.layer.Layer} Layer.
+ */
+ol.renderer.Layer.prototype.getLayer = function() {
+  return this.layer_;
+};
+
+
+/**
+ * Handle changes in image state.
+ * @param {ol.events.Event} event Image change event.
+ * @private
+ */
+ol.renderer.Layer.prototype.handleImageChange_ = function(event) {
+  var image = /** @type {ol.Image} */ (event.target);
+  if (image.getState() === ol.ImageState.LOADED) {
+    this.renderIfReadyAndVisible();
+  }
+};
+
+
+/**
+ * Load the image if not already loaded, and register the image change
+ * listener if needed.
+ * @param {ol.ImageBase} image Image.
+ * @return {boolean} `true` if the image is already loaded, `false`
+ *     otherwise.
+ * @protected
+ */
+ol.renderer.Layer.prototype.loadImage = function(image) {
+  var imageState = image.getState();
+  if (imageState != ol.ImageState.LOADED &&
+      imageState != ol.ImageState.ERROR) {
+    // the image is either "idle" or "loading", register the change
+    // listener (a noop if the listener was already registered)
+    goog.asserts.assert(imageState == ol.ImageState.IDLE ||
+        imageState == ol.ImageState.LOADING,
+        'imageState is "idle" or "loading"');
+    ol.events.listen(image, ol.events.EventType.CHANGE,
+        this.handleImageChange_, this);
+  }
+  if (imageState == ol.ImageState.IDLE) {
+    image.load();
+    imageState = image.getState();
+    goog.asserts.assert(imageState == ol.ImageState.LOADING ||
+        imageState == ol.ImageState.LOADED,
+        'imageState is "loading" or "loaded"');
+  }
+  return imageState == ol.ImageState.LOADED;
+};
+
+
+/**
+ * @protected
+ */
+ol.renderer.Layer.prototype.renderIfReadyAndVisible = function() {
+  var layer = this.getLayer();
+  if (layer.getVisible() && layer.getSourceState() == ol.source.State.READY) {
+    this.changed();
+  }
+};
+
+
+/**
+ * @param {olx.FrameState} frameState Frame state.
+ * @param {ol.source.Tile} tileSource Tile source.
+ * @protected
+ */
+ol.renderer.Layer.prototype.scheduleExpireCache = function(frameState, tileSource) {
+  if (tileSource.canExpireCache()) {
+    /**
+     * @param {ol.source.Tile} tileSource Tile source.
+     * @param {ol.Map} map Map.
+     * @param {olx.FrameState} frameState Frame state.
+     */
+    var postRenderFunction = function(tileSource, map, frameState) {
+      var tileSourceKey = goog.getUid(tileSource).toString();
+      tileSource.expireCache(frameState.viewState.projection,
+                             frameState.usedTiles[tileSourceKey]);
+    }.bind(null, tileSource);
+
+    frameState.postRenderFunctions.push(
+      /** @type {ol.PostRenderFunction} */ (postRenderFunction)
+    );
+  }
+};
+
+
+/**
+ * @param {Object.<string, ol.Attribution>} attributionsSet Attributions
+ *     set (target).
+ * @param {Array.<ol.Attribution>} attributions Attributions (source).
+ * @protected
+ */
+ol.renderer.Layer.prototype.updateAttributions = function(attributionsSet, attributions) {
+  if (attributions) {
+    var attribution, i, ii;
+    for (i = 0, ii = attributions.length; i < ii; ++i) {
+      attribution = attributions[i];
+      attributionsSet[goog.getUid(attribution).toString()] = attribution;
+    }
+  }
+};
+
+
+/**
+ * @param {olx.FrameState} frameState Frame state.
+ * @param {ol.source.Source} source Source.
+ * @protected
+ */
+ol.renderer.Layer.prototype.updateLogos = function(frameState, source) {
+  var logo = source.getLogo();
+  if (logo !== undefined) {
+    if (typeof logo === 'string') {
+      frameState.logos[logo] = '';
+    } else if (goog.isObject(logo)) {
+      goog.asserts.assertString(logo.href, 'logo.href is a string');
+      goog.asserts.assertString(logo.src, 'logo.src is a string');
+      frameState.logos[logo.src] = logo.href;
+    }
+  }
+};
+
+
+/**
+ * @param {Object.<string, Object.<string, ol.TileRange>>} usedTiles Used tiles.
+ * @param {ol.source.Tile} tileSource Tile source.
+ * @param {number} z Z.
+ * @param {ol.TileRange} tileRange Tile range.
+ * @protected
+ */
+ol.renderer.Layer.prototype.updateUsedTiles = function(usedTiles, tileSource, z, tileRange) {
+  // FIXME should we use tilesToDrawByZ instead?
+  var tileSourceKey = goog.getUid(tileSource).toString();
+  var zKey = z.toString();
+  if (tileSourceKey in usedTiles) {
+    if (zKey in usedTiles[tileSourceKey]) {
+      usedTiles[tileSourceKey][zKey].extend(tileRange);
+    } else {
+      usedTiles[tileSourceKey][zKey] = tileRange;
+    }
+  } else {
+    usedTiles[tileSourceKey] = {};
+    usedTiles[tileSourceKey][zKey] = tileRange;
+  }
+};
+
+
+/**
+ * @param {ol.Coordinate} center Center.
+ * @param {number} resolution Resolution.
+ * @param {ol.Size} size Size.
+ * @protected
+ * @return {ol.Coordinate} Snapped center.
+ */
+ol.renderer.Layer.prototype.snapCenterToPixel = function(center, resolution, size) {
+  return [
+    resolution * (Math.round(center[0] / resolution) + (size[0] % 2) / 2),
+    resolution * (Math.round(center[1] / resolution) + (size[1] % 2) / 2)
+  ];
+};
+
+
+/**
+ * Manage tile pyramid.
+ * This function performs a number of functions related to the tiles at the
+ * current zoom and lower zoom levels:
+ * - registers idle tiles in frameState.wantedTiles so that they are not
+ *   discarded by the tile queue
+ * - enqueues missing tiles
+ * @param {olx.FrameState} frameState Frame state.
+ * @param {ol.source.Tile} tileSource Tile source.
+ * @param {ol.tilegrid.TileGrid} tileGrid Tile grid.
+ * @param {number} pixelRatio Pixel ratio.
+ * @param {ol.proj.Projection} projection Projection.
+ * @param {ol.Extent} extent Extent.
+ * @param {number} currentZ Current Z.
+ * @param {number} preload Load low resolution tiles up to 'preload' levels.
+ * @param {function(this: T, ol.Tile)=} opt_tileCallback Tile callback.
+ * @param {T=} opt_this Object to use as `this` in `opt_tileCallback`.
+ * @protected
+ * @template T
+ */
+ol.renderer.Layer.prototype.manageTilePyramid = function(
+    frameState, tileSource, tileGrid, pixelRatio, projection, extent,
+    currentZ, preload, opt_tileCallback, opt_this) {
+  var tileSourceKey = goog.getUid(tileSource).toString();
+  if (!(tileSourceKey in frameState.wantedTiles)) {
+    frameState.wantedTiles[tileSourceKey] = {};
+  }
+  var wantedTiles = frameState.wantedTiles[tileSourceKey];
+  var tileQueue = frameState.tileQueue;
+  var minZoom = tileGrid.getMinZoom();
+  var tile, tileRange, tileResolution, x, y, z;
+  for (z = currentZ; z >= minZoom; --z) {
+    tileRange = tileGrid.getTileRangeForExtentAndZ(extent, z, tileRange);
+    tileResolution = tileGrid.getResolution(z);
+    for (x = tileRange.minX; x <= tileRange.maxX; ++x) {
+      for (y = tileRange.minY; y <= tileRange.maxY; ++y) {
+        if (currentZ - z <= preload) {
+          tile = tileSource.getTile(z, x, y, pixelRatio, projection);
+          if (tile.getState() == ol.TileState.IDLE) {
+            wantedTiles[tile.tileCoord.toString()] = true;
+            if (!tileQueue.isKeyQueued(tile.getKey())) {
+              tileQueue.enqueue([tile, tileSourceKey,
+                tileGrid.getTileCoordCenter(tile.tileCoord), tileResolution]);
+            }
+          }
+          if (opt_tileCallback !== undefined) {
+            opt_tileCallback.call(opt_this, tile);
+          }
+        } else {
+          tileSource.useTile(z, x, y, projection);
+        }
+      }
+    }
+  }
+};
+
+goog.provide('ol.style.Image');
+goog.provide('ol.style.ImageState');
+
+
+/**
+ * @enum {number}
+ */
+ol.style.ImageState = {
+  IDLE: 0,
+  LOADING: 1,
+  LOADED: 2,
+  ERROR: 3
+};
+
+
+/**
+ * @classdesc
+ * A base class used for creating subclasses and not instantiated in
+ * apps. Base class for {@link ol.style.Icon}, {@link ol.style.Circle} and
+ * {@link ol.style.RegularShape}.
+ *
+ * @constructor
+ * @param {ol.StyleImageOptions} options Options.
+ * @api
+ */
+ol.style.Image = function(options) {
+
+  /**
+   * @private
+   * @type {number}
+   */
+  this.opacity_ = options.opacity;
+
+  /**
+   * @private
+   * @type {boolean}
+   */
+  this.rotateWithView_ = options.rotateWithView;
+
+  /**
+   * @private
+   * @type {number}
+   */
+  this.rotation_ = options.rotation;
+
+  /**
+   * @private
+   * @type {number}
+   */
+  this.scale_ = options.scale;
+
+  /**
+   * @private
+   * @type {boolean}
+   */
+  this.snapToPixel_ = options.snapToPixel;
+
+};
+
+
+/**
+ * Get the symbolizer opacity.
+ * @return {number} Opacity.
+ * @api
+ */
+ol.style.Image.prototype.getOpacity = function() {
+  return this.opacity_;
+};
+
+
+/**
+ * Determine whether the symbolizer rotates with the map.
+ * @return {boolean} Rotate with map.
+ * @api
+ */
+ol.style.Image.prototype.getRotateWithView = function() {
+  return this.rotateWithView_;
+};
+
+
+/**
+ * Get the symoblizer rotation.
+ * @return {number} Rotation.
+ * @api
+ */
+ol.style.Image.prototype.getRotation = function() {
+  return this.rotation_;
+};
+
+
+/**
+ * Get the symbolizer scale.
+ * @return {number} Scale.
+ * @api
+ */
+ol.style.Image.prototype.getScale = function() {
+  return this.scale_;
+};
+
+
+/**
+ * Determine whether the symbolizer should be snapped to a pixel.
+ * @return {boolean} The symbolizer should snap to a pixel.
+ * @api
+ */
+ol.style.Image.prototype.getSnapToPixel = function() {
+  return this.snapToPixel_;
+};
+
+
+/**
+ * Get the anchor point.  The anchor determines the center point for the
+ * symbolizer.  Its units are determined by `anchorXUnits` and `anchorYUnits`.
+ * @function
+ * @return {Array.<number>} Anchor.
+ */
+ol.style.Image.prototype.getAnchor = goog.abstractMethod;
+
+
+/**
+ * Get the image element for the symbolizer.
+ * @function
+ * @param {number} pixelRatio Pixel ratio.
+ * @return {HTMLCanvasElement|HTMLVideoElement|Image} Image element.
+ */
+ol.style.Image.prototype.getImage = goog.abstractMethod;
+
+
+/**
+ * @param {number} pixelRatio Pixel ratio.
+ * @return {HTMLCanvasElement|HTMLVideoElement|Image} Image element.
+ */
+ol.style.Image.prototype.getHitDetectionImage = goog.abstractMethod;
+
+
+/**
+ * @return {ol.style.ImageState} Image state.
+ */
+ol.style.Image.prototype.getImageState = goog.abstractMethod;
+
+
+/**
+ * @return {ol.Size} Image size.
+ */
+ol.style.Image.prototype.getImageSize = goog.abstractMethod;
+
+
+/**
+ * @return {ol.Size} Size of the hit-detection image.
+ */
+ol.style.Image.prototype.getHitDetectionImageSize = goog.abstractMethod;
+
+
+/**
+ * Get the origin of the symbolizer.
+ * @function
+ * @return {Array.<number>} Origin.
+ */
+ol.style.Image.prototype.getOrigin = goog.abstractMethod;
+
+
+/**
+ * Get the size of the symbolizer (in pixels).
+ * @function
+ * @return {ol.Size} Size.
+ */
+ol.style.Image.prototype.getSize = goog.abstractMethod;
+
+
+/**
+ * Set the opacity.
+ *
+ * @param {number} opacity Opacity.
+ * @api
+ */
+ol.style.Image.prototype.setOpacity = function(opacity) {
+  this.opacity_ = opacity;
+};
+
+
+/**
+ * Set whether to rotate the style with the view.
+ *
+ * @param {boolean} rotateWithView Rotate with map.
+ */
+ol.style.Image.prototype.setRotateWithView = function(rotateWithView) {
+  this.rotateWithView_ = rotateWithView;
+};
+
+
+/**
+ * Set the rotation.
+ *
+ * @param {number} rotation Rotation.
+ * @api
+ */
+ol.style.Image.prototype.setRotation = function(rotation) {
+  this.rotation_ = rotation;
+};
+
+
+/**
+ * Set the scale.
+ *
+ * @param {number} scale Scale.
+ * @api
+ */
+ol.style.Image.prototype.setScale = function(scale) {
+  this.scale_ = scale;
+};
+
+
+/**
+ * Set whether to snap the image to the closest pixel.
+ *
+ * @param {boolean} snapToPixel Snap to pixel?
+ */
+ol.style.Image.prototype.setSnapToPixel = function(snapToPixel) {
+  this.snapToPixel_ = snapToPixel;
+};
+
+
+/**
+ * @param {function(this: T, ol.events.Event)} listener Listener function.
+ * @param {T} thisArg Value to use as `this` when executing `listener`.
+ * @return {ol.EventsKey|undefined} Listener key.
+ * @template T
+ */
+ol.style.Image.prototype.listenImageChange = goog.abstractMethod;
+
+
+/**
+ * Load not yet loaded URI.
+ */
+ol.style.Image.prototype.load = goog.abstractMethod;
+
+
+/**
+ * @param {function(this: T, ol.events.Event)} listener Listener function.
+ * @param {T} thisArg Value to use as `this` when executing `listener`.
+ * @template T
+ */
+ol.style.Image.prototype.unlistenImageChange = goog.abstractMethod;
+
+goog.provide('ol.style.Icon');
+goog.provide('ol.style.IconAnchorUnits');
+goog.provide('ol.style.IconImageCache');
+goog.provide('ol.style.IconOrigin');
+
+goog.require('goog.asserts');
+goog.require('ol.events');
+goog.require('ol.events.EventTarget');
+goog.require('ol.events.EventType');
+goog.require('ol.color');
+goog.require('ol.dom');
+goog.require('ol.style.Image');
+goog.require('ol.style.ImageState');
+
+
+/**
+ * Icon anchor units. One of 'fraction', 'pixels'.
+ * @enum {string}
+ */
+ol.style.IconAnchorUnits = {
+  FRACTION: 'fraction',
+  PIXELS: 'pixels'
+};
+
+
+/**
+ * Icon origin. One of 'bottom-left', 'bottom-right', 'top-left', 'top-right'.
+ * @enum {string}
+ */
+ol.style.IconOrigin = {
+  BOTTOM_LEFT: 'bottom-left',
+  BOTTOM_RIGHT: 'bottom-right',
+  TOP_LEFT: 'top-left',
+  TOP_RIGHT: 'top-right'
+};
+
+
+/**
+ * @classdesc
+ * Set icon style for vector features.
+ *
+ * @constructor
+ * @param {olx.style.IconOptions=} opt_options Options.
+ * @extends {ol.style.Image}
+ * @api
+ */
+ol.style.Icon = function(opt_options) {
+
+  var options = opt_options || {};
+
+  /**
+   * @private
+   * @type {Array.<number>}
+   */
+  this.anchor_ = options.anchor !== undefined ? options.anchor : [0.5, 0.5];
+
+  /**
+   * @private
+   * @type {Array.<number>}
+   */
+  this.normalizedAnchor_ = null;
+
+  /**
+   * @private
+   * @type {ol.style.IconOrigin}
+   */
+  this.anchorOrigin_ = options.anchorOrigin !== undefined ?
+      options.anchorOrigin : ol.style.IconOrigin.TOP_LEFT;
+
+  /**
+   * @private
+   * @type {ol.style.IconAnchorUnits}
+   */
+  this.anchorXUnits_ = options.anchorXUnits !== undefined ?
+      options.anchorXUnits : ol.style.IconAnchorUnits.FRACTION;
+
+  /**
+   * @private
+   * @type {ol.style.IconAnchorUnits}
+   */
+  this.anchorYUnits_ = options.anchorYUnits !== undefined ?
+      options.anchorYUnits : ol.style.IconAnchorUnits.FRACTION;
+
+  /**
+   * @type {?string}
+   */
+  var crossOrigin =
+      options.crossOrigin !== undefined ? options.crossOrigin : null;
+
+  /**
+   * @type {Image|HTMLCanvasElement}
+   */
+  var image = options.img !== undefined ? options.img : null;
+
+  /**
+   * @type {ol.Size}
+   */
+  var imgSize = options.imgSize !== undefined ? options.imgSize : null;
+
+  /**
+   * @type {string|undefined}
+   */
+  var src = options.src;
+
+  goog.asserts.assert(!(src !== undefined && image),
+      'image and src can not provided at the same time');
+  goog.asserts.assert(
+      !image || (image && imgSize),
+      'imgSize must be set when image is provided');
+
+  if ((src === undefined || src.length === 0) && image) {
+    src = image.src || goog.getUid(image).toString();
+  }
+  goog.asserts.assert(src !== undefined && src.length > 0,
+      'must provide a defined and non-empty src or image');
+
+  /**
+   * @type {ol.style.ImageState}
+   */
+  var imageState = options.src !== undefined ?
+      ol.style.ImageState.IDLE : ol.style.ImageState.LOADED;
+
+  /**
+   * @type {ol.Color}
+   */
+  var color = options.color !== undefined ? ol.color.asArray(options.color) :
+      null;
+
+  /**
+   * @private
+   * @type {ol.style.IconImage_}
+   */
+  this.iconImage_ = ol.style.IconImage_.get(
+      image, src, imgSize, crossOrigin, imageState, color);
+
+  /**
+   * @private
+   * @type {Array.<number>}
+   */
+  this.offset_ = options.offset !== undefined ? options.offset : [0, 0];
+
+  /**
+   * @private
+   * @type {ol.style.IconOrigin}
+   */
+  this.offsetOrigin_ = options.offsetOrigin !== undefined ?
+      options.offsetOrigin : ol.style.IconOrigin.TOP_LEFT;
+
+  /**
+   * @private
+   * @type {Array.<number>}
+   */
+  this.origin_ = null;
+
+  /**
+   * @private
+   * @type {ol.Size}
+   */
+  this.size_ = options.size !== undefined ? options.size : null;
+
+  /**
+   * @type {number}
+   */
+  var opacity = options.opacity !== undefined ? options.opacity : 1;
+
+  /**
+   * @type {boolean}
+   */
+  var rotateWithView = options.rotateWithView !== undefined ?
+      options.rotateWithView : false;
+
+  /**
+   * @type {number}
+   */
+  var rotation = options.rotation !== undefined ? options.rotation : 0;
+
+  /**
+   * @type {number}
+   */
+  var scale = options.scale !== undefined ? options.scale : 1;
+
+  /**
+   * @type {boolean}
+   */
+  var snapToPixel = options.snapToPixel !== undefined ?
+      options.snapToPixel : true;
+
+  ol.style.Image.call(this, {
+    opacity: opacity,
+    rotation: rotation,
+    scale: scale,
+    snapToPixel: snapToPixel,
+    rotateWithView: rotateWithView
+  });
+
+};
+ol.inherits(ol.style.Icon, ol.style.Image);
+
+
+/**
+ * @inheritDoc
+ * @api
+ */
+ol.style.Icon.prototype.getAnchor = function() {
+  if (this.normalizedAnchor_) {
+    return this.normalizedAnchor_;
+  }
+  var anchor = this.anchor_;
+  var size = this.getSize();
+  if (this.anchorXUnits_ == ol.style.IconAnchorUnits.FRACTION ||
+      this.anchorYUnits_ == ol.style.IconAnchorUnits.FRACTION) {
+    if (!size) {
+      return null;
+    }
+    anchor = this.anchor_.slice();
+    if (this.anchorXUnits_ == ol.style.IconAnchorUnits.FRACTION) {
+      anchor[0] *= size[0];
+    }
+    if (this.anchorYUnits_ == ol.style.IconAnchorUnits.FRACTION) {
+      anchor[1] *= size[1];
+    }
+  }
+
+  if (this.anchorOrigin_ != ol.style.IconOrigin.TOP_LEFT) {
+    if (!size) {
+      return null;
+    }
+    if (anchor === this.anchor_) {
+      anchor = this.anchor_.slice();
+    }
+    if (this.anchorOrigin_ == ol.style.IconOrigin.TOP_RIGHT ||
+        this.anchorOrigin_ == ol.style.IconOrigin.BOTTOM_RIGHT) {
+      anchor[0] = -anchor[0] + size[0];
+    }
+    if (this.anchorOrigin_ == ol.style.IconOrigin.BOTTOM_LEFT ||
+        this.anchorOrigin_ == ol.style.IconOrigin.BOTTOM_RIGHT) {
+      anchor[1] = -anchor[1] + size[1];
+    }
+  }
+  this.normalizedAnchor_ = anchor;
+  return this.normalizedAnchor_;
+};
+
+
+/**
+ * Get the image icon.
+ * @param {number} pixelRatio Pixel ratio.
+ * @return {Image|HTMLCanvasElement} Image or Canvas element.
+ * @api
+ */
+ol.style.Icon.prototype.getImage = function(pixelRatio) {
+  return this.iconImage_.getImage(pixelRatio);
+};
+
+
+/**
+ * Real Image size used.
+ * @return {ol.Size} Size.
+ */
+ol.style.Icon.prototype.getImageSize = function() {
+  return this.iconImage_.getSize();
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.style.Icon.prototype.getHitDetectionImageSize = function() {
+  return this.getImageSize();
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.style.Icon.prototype.getImageState = function() {
+  return this.iconImage_.getImageState();
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.style.Icon.prototype.getHitDetectionImage = function(pixelRatio) {
+  return this.iconImage_.getHitDetectionImage(pixelRatio);
+};
+
+
+/**
+ * @inheritDoc
+ * @api
+ */
+ol.style.Icon.prototype.getOrigin = function() {
+  if (this.origin_) {
+    return this.origin_;
+  }
+  var offset = this.offset_;
+
+  if (this.offsetOrigin_ != ol.style.IconOrigin.TOP_LEFT) {
+    var size = this.getSize();
+    var iconImageSize = this.iconImage_.getSize();
+    if (!size || !iconImageSize) {
+      return null;
+    }
+    offset = offset.slice();
+    if (this.offsetOrigin_ == ol.style.IconOrigin.TOP_RIGHT ||
+        this.offsetOrigin_ == ol.style.IconOrigin.BOTTOM_RIGHT) {
+      offset[0] = iconImageSize[0] - size[0] - offset[0];
+    }
+    if (this.offsetOrigin_ == ol.style.IconOrigin.BOTTOM_LEFT ||
+        this.offsetOrigin_ == ol.style.IconOrigin.BOTTOM_RIGHT) {
+      offset[1] = iconImageSize[1] - size[1] - offset[1];
+    }
+  }
+  this.origin_ = offset;
+  return this.origin_;
+};
+
+
+/**
+ * Get the image URL.
+ * @return {string|undefined} Image src.
+ * @api
+ */
+ol.style.Icon.prototype.getSrc = function() {
+  return this.iconImage_.getSrc();
+};
+
+
+/**
+ * @inheritDoc
+ * @api
+ */
+ol.style.Icon.prototype.getSize = function() {
+  return !this.size_ ? this.iconImage_.getSize() : this.size_;
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.style.Icon.prototype.listenImageChange = function(listener, thisArg) {
+  return ol.events.listen(this.iconImage_, ol.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.
+ * @api
+ */
+ol.style.Icon.prototype.load = function() {
+  this.iconImage_.load();
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.style.Icon.prototype.unlistenImageChange = function(listener, thisArg) {
+  ol.events.unlisten(this.iconImage_, ol.events.EventType.CHANGE,
+      listener, thisArg);
+};
+
+
+/**
+ * @constructor
+ * @param {Image|HTMLCanvasElement} image Image.
+ * @param {string|undefined} src Src.
+ * @param {ol.Size} size Size.
+ * @param {?string} crossOrigin Cross origin.
+ * @param {ol.style.ImageState} imageState Image state.
+ * @param {ol.Color} color Color.
+ * @extends {ol.events.EventTarget}
+ * @private
+ */
+ol.style.IconImage_ = function(image, src, size, crossOrigin, imageState,
+                               color) {
+
+  ol.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_ = color ?
+      /** @type {HTMLCanvasElement} */ (document.createElement('CANVAS')) :
+      null;
+
+  /**
+   * @private
+   * @type {ol.Color}
+   */
+  this.color_ = color;
+
+  /**
+   * @private
+   * @type {Array.<ol.EventsKey>}
+   */
+  this.imageListenerKeys_ = null;
+
+  /**
+   * @private
+   * @type {ol.style.ImageState}
+   */
+  this.imageState_ = imageState;
+
+  /**
+   * @private
+   * @type {ol.Size}
+   */
+  this.size_ = size;
+
+  /**
+   * @private
+   * @type {string|undefined}
+   */
+  this.src_ = src;
+
+  /**
+   * @private
+   * @type {boolean}
+   */
+  this.tainting_ = false;
+  if (this.imageState_ == ol.style.ImageState.LOADED) {
+    this.determineTainting_();
+  }
+
+};
+ol.inherits(ol.style.IconImage_, ol.events.EventTarget);
+
+
+/**
+ * @param {Image|HTMLCanvasElement} image Image.
+ * @param {string} src Src.
+ * @param {ol.Size} size Size.
+ * @param {?string} crossOrigin Cross origin.
+ * @param {ol.style.ImageState} imageState Image state.
+ * @param {ol.Color} color Color.
+ * @return {ol.style.IconImage_} Icon image.
+ */
+ol.style.IconImage_.get = function(image, src, size, crossOrigin, imageState,
+                                   color) {
+  var iconImageCache = ol.style.IconImageCache.getInstance();
+  var iconImage = iconImageCache.get(src, crossOrigin, color);
+  if (!iconImage) {
+    iconImage = new ol.style.IconImage_(
+        image, src, size, crossOrigin, imageState, color);
+    iconImageCache.set(src, crossOrigin, color, iconImage);
+  }
+  return iconImage;
+};
+
+
+/**
+ * @private
+ */
+ol.style.IconImage_.prototype.determineTainting_ = function() {
+  var context = ol.dom.createCanvasContext2D(1, 1);
+  try {
+    context.drawImage(this.image_, 0, 0);
+    context.getImageData(0, 0, 1, 1);
+  } catch (e) {
+    this.tainting_ = true;
+  }
+};
+
+
+/**
+ * @private
+ */
+ol.style.IconImage_.prototype.dispatchChangeEvent_ = function() {
+  this.dispatchEvent(ol.events.EventType.CHANGE);
+};
+
+
+/**
+ * @private
+ */
+ol.style.IconImage_.prototype.handleImageError_ = function() {
+  this.imageState_ = ol.style.ImageState.ERROR;
+  this.unlistenImage_();
+  this.dispatchChangeEvent_();
+};
+
+
+/**
+ * @private
+ */
+ol.style.IconImage_.prototype.handleImageLoad_ = function() {
+  this.imageState_ = ol.style.ImageState.LOADED;
+  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.replaceColor_();
+  this.dispatchChangeEvent_();
+};
+
+
+/**
+ * @param {number} pixelRatio Pixel ratio.
+ * @return {Image|HTMLCanvasElement} Image or Canvas element.
+ */
+ol.style.IconImage_.prototype.getImage = function(pixelRatio) {
+  return this.canvas_ ? this.canvas_ : this.image_;
+};
+
+
+/**
+ * @return {ol.style.ImageState} Image state.
+ */
+ol.style.IconImage_.prototype.getImageState = function() {
+  return this.imageState_;
+};
+
+
+/**
+ * @param {number} pixelRatio Pixel ratio.
+ * @return {Image|HTMLCanvasElement} Image element.
+ */
+ol.style.IconImage_.prototype.getHitDetectionImage = function(pixelRatio) {
+  if (!this.hitDetectionImage_) {
+    if (this.tainting_) {
+      var width = this.size_[0];
+      var height = this.size_[1];
+      var context = ol.dom.createCanvasContext2D(width, height);
+      context.fillRect(0, 0, width, height);
+      this.hitDetectionImage_ = context.canvas;
+    } else {
+      this.hitDetectionImage_ = this.image_;
+    }
+  }
+  return this.hitDetectionImage_;
+};
+
+
+/**
+ * @return {ol.Size} Image size.
+ */
+ol.style.IconImage_.prototype.getSize = function() {
+  return this.size_;
+};
+
+
+/**
+ * @return {string|undefined} Image src.
+ */
+ol.style.IconImage_.prototype.getSrc = function() {
+  return this.src_;
+};
+
+
+/**
+ * Load not yet loaded URI.
+ */
+ol.style.IconImage_.prototype.load = function() {
+  if (this.imageState_ == ol.style.ImageState.IDLE) {
+    goog.asserts.assert(this.src_ !== undefined,
+        'this.src_ must not be undefined');
+    goog.asserts.assert(!this.imageListenerKeys_,
+        'no listener keys existing');
+    this.imageState_ = ol.style.ImageState.LOADING;
+    this.imageListenerKeys_ = [
+      ol.events.listenOnce(this.image_, ol.events.EventType.ERROR,
+          this.handleImageError_, this),
+      ol.events.listenOnce(this.image_, ol.events.EventType.LOAD,
+          this.handleImageLoad_, this)
+    ];
+    try {
+      this.image_.src = this.src_;
+    } catch (e) {
+      this.handleImageError_();
+    }
+  }
+};
+
+
+/**
+ * @private
+ */
+ol.style.IconImage_.prototype.replaceColor_ = function() {
+  if (this.tainting_ || this.color_ === null) {
+    return;
+  }
+
+  goog.asserts.assert(this.canvas_ !== null,
+      'this.canvas_ must not be null');
+
+  this.canvas_.width = this.image_.width;
+  this.canvas_.height = this.image_.height;
+
+  var ctx = this.canvas_.getContext('2d');
+  ctx.drawImage(this.image_, 0, 0);
+
+  var imgData = ctx.getImageData(0, 0, this.image_.width, this.image_.height);
+  var data = imgData.data;
+  var r = this.color_[0] / 255.0;
+  var g = this.color_[1] / 255.0;
+  var b = this.color_[2] / 255.0;
+
+  for (var i = 0, ii = data.length; i < ii; i += 4) {
+    data[i] *= r;
+    data[i + 1] *= g;
+    data[i + 2] *= b;
+  }
+  ctx.putImageData(imgData, 0, 0);
+};
+
+
+/**
+ * Discards event handlers which listen for load completion or errors.
+ *
+ * @private
+ */
+ol.style.IconImage_.prototype.unlistenImage_ = function() {
+  goog.asserts.assert(this.imageListenerKeys_,
+      'we must have listeners registered');
+  this.imageListenerKeys_.forEach(ol.events.unlistenByKey);
+  this.imageListenerKeys_ = null;
+};
+
+
+/**
+ * @constructor
+ */
+ol.style.IconImageCache = function() {
+
+  /**
+   * @type {Object.<string, ol.style.IconImage_>}
+   * @private
+   */
+  this.cache_ = {};
+
+  /**
+   * @type {number}
+   * @private
+   */
+  this.cacheSize_ = 0;
+
+  /**
+   * @const
+   * @type {number}
+   * @private
+   */
+  this.maxCacheSize_ = 32;
+};
+goog.addSingletonGetter(ol.style.IconImageCache);
+
+
+/**
+ * @param {string} src Src.
+ * @param {?string} crossOrigin Cross origin.
+ * @param {ol.Color} color Color.
+ * @return {string} Cache key.
+ */
+ol.style.IconImageCache.getKey = function(src, crossOrigin, color) {
+  goog.asserts.assert(crossOrigin !== undefined,
+      'argument crossOrigin must be defined');
+  var colorString = color ? ol.color.asString(color) : 'null';
+  return crossOrigin + ':' + src + ':' + colorString;
+};
+
+
+/**
+ * FIXME empty description for jsdoc
+ */
+ol.style.IconImageCache.prototype.clear = function() {
+  this.cache_ = {};
+  this.cacheSize_ = 0;
+};
+
+
+/**
+ * FIXME empty description for jsdoc
+ */
+ol.style.IconImageCache.prototype.expire = function() {
+  if (this.cacheSize_ > this.maxCacheSize_) {
+    var i = 0;
+    var key, iconImage;
+    for (key in this.cache_) {
+      iconImage = this.cache_[key];
+      if ((i++ & 3) === 0 && !iconImage.hasListener()) {
+        delete this.cache_[key];
+        --this.cacheSize_;
+      }
+    }
+  }
+};
+
+
+/**
+ * @param {string} src Src.
+ * @param {?string} crossOrigin Cross origin.
+ * @param {ol.Color} color Color.
+ * @return {ol.style.IconImage_} Icon image.
+ */
+ol.style.IconImageCache.prototype.get = function(src, crossOrigin, color) {
+  var key = ol.style.IconImageCache.getKey(src, crossOrigin, color);
+  return key in this.cache_ ? this.cache_[key] : null;
+};
+
+
+/**
+ * @param {string} src Src.
+ * @param {?string} crossOrigin Cross origin.
+ * @param {ol.Color} color Color.
+ * @param {ol.style.IconImage_} iconImage Icon image.
+ */
+ol.style.IconImageCache.prototype.set = function(src, crossOrigin, color,
+                                                 iconImage) {
+  var key = ol.style.IconImageCache.getKey(src, crossOrigin, color);
+  this.cache_[key] = iconImage;
+  ++this.cacheSize_;
+};
+
+goog.provide('ol.RendererType');
+goog.provide('ol.renderer.Map');
+
+goog.require('goog.asserts');
+goog.require('goog.vec.Mat4');
+goog.require('ol');
+goog.require('ol.Disposable');
+goog.require('ol.events');
+goog.require('ol.events.EventType');
+goog.require('ol.extent');
+goog.require('ol.functions');
+goog.require('ol.layer.Layer');
+goog.require('ol.renderer.Layer');
+goog.require('ol.style.IconImageCache');
+goog.require('ol.vec.Mat4');
+
+
+/**
+ * Available renderers: `'canvas'`, `'dom'` or `'webgl'`.
+ * @enum {string}
+ */
+ol.RendererType = {
+  CANVAS: 'canvas',
+  DOM: 'dom',
+  WEBGL: 'webgl'
+};
+
+
+/**
+ * @constructor
+ * @extends {ol.Disposable}
+ * @param {Element} container Container.
+ * @param {ol.Map} map Map.
+ * @struct
+ */
+ol.renderer.Map = function(container, map) {
+
+  ol.Disposable.call(this);
+
+
+  /**
+   * @private
+   * @type {ol.Map}
+   */
+  this.map_ = map;
+
+  /**
+   * @private
+   * @type {Object.<string, ol.renderer.Layer>}
+   */
+  this.layerRenderers_ = {};
+
+  /**
+   * @private
+   * @type {Object.<string, ol.EventsKey>}
+   */
+  this.layerRendererListeners_ = {};
+
+};
+ol.inherits(ol.renderer.Map, ol.Disposable);
+
+
+/**
+ * @param {olx.FrameState} frameState FrameState.
+ * @protected
+ */
+ol.renderer.Map.prototype.calculateMatrices2D = function(frameState) {
+  var viewState = frameState.viewState;
+  var coordinateToPixelMatrix = frameState.coordinateToPixelMatrix;
+  goog.asserts.assert(coordinateToPixelMatrix,
+      'frameState has a coordinateToPixelMatrix');
+  ol.vec.Mat4.makeTransform2D(coordinateToPixelMatrix,
+      frameState.size[0] / 2, frameState.size[1] / 2,
+      1 / viewState.resolution, -1 / viewState.resolution,
+      -viewState.rotation,
+      -viewState.center[0], -viewState.center[1]);
+  var inverted = goog.vec.Mat4.invert(
+      coordinateToPixelMatrix, frameState.pixelToCoordinateMatrix);
+  goog.asserts.assert(inverted, 'matrix could be inverted');
+};
+
+
+/**
+ * @param {ol.layer.Layer} layer Layer.
+ * @protected
+ * @return {ol.renderer.Layer} layerRenderer Layer renderer.
+ */
+ol.renderer.Map.prototype.createLayerRenderer = goog.abstractMethod;
+
+
+/**
+ * @inheritDoc
+ */
+ol.renderer.Map.prototype.disposeInternal = function() {
+  for (var id in this.layerRenderers_) {
+    this.layerRenderers_[id].dispose();
+  }
+};
+
+
+/**
+ * @param {ol.Map} map Map.
+ * @param {olx.FrameState} frameState Frame state.
+ * @private
+ */
+ol.renderer.Map.expireIconCache_ = function(map, frameState) {
+  ol.style.IconImageCache.getInstance().expire();
+};
+
+
+/**
+ * @param {ol.Coordinate} coordinate Coordinate.
+ * @param {olx.FrameState} frameState FrameState.
+ * @param {function(this: S, (ol.Feature|ol.render.Feature),
+ *     ol.layer.Layer): T} callback Feature callback.
+ * @param {S} thisArg Value to use as `this` when executing `callback`.
+ * @param {function(this: U, ol.layer.Layer): boolean} layerFilter Layer filter
+ *     function, only layers which are visible and for which this function
+ *     returns `true` will be tested for features.  By default, all visible
+ *     layers will be tested.
+ * @param {U} thisArg2 Value to use as `this` when executing `layerFilter`.
+ * @return {T|undefined} Callback result.
+ * @template S,T,U
+ */
+ol.renderer.Map.prototype.forEachFeatureAtCoordinate = function(coordinate, frameState, callback, thisArg,
+        layerFilter, thisArg2) {
+  var result;
+  var viewState = frameState.viewState;
+  var viewResolution = viewState.resolution;
+
+  /**
+   * @param {ol.Feature|ol.render.Feature} feature Feature.
+   * @param {ol.layer.Layer} layer Layer.
+   * @return {?} Callback result.
+   */
+  function forEachFeatureAtCoordinate(feature, layer) {
+    goog.asserts.assert(feature !== undefined, 'received a feature');
+    var key = goog.getUid(feature).toString();
+    var managed = frameState.layerStates[goog.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 = ol.extent.getWidth(projectionExtent);
+    var x = coordinate[0];
+    if (x < projectionExtent[0] || x > projectionExtent[2]) {
+      var worldsAway = Math.ceil((projectionExtent[0] - x) / worldWidth);
+      translatedCoordinate = [x + worldWidth * worldsAway, coordinate[1]];
+    }
+  }
+
+  var layerStates = frameState.layerStatesArray;
+  var numLayers = layerStates.length;
+  var i;
+  for (i = numLayers - 1; i >= 0; --i) {
+    var layerState = layerStates[i];
+    var layer = layerState.layer;
+    if (ol.layer.Layer.visibleAtResolution(layerState, viewResolution) &&
+        layerFilter.call(thisArg2, layer)) {
+      var layerRenderer = this.getLayerRenderer(layer);
+      if (layer.getSource()) {
+        result = layerRenderer.forEachFeatureAtCoordinate(
+            layer.getSource().getWrapX() ? translatedCoordinate : coordinate,
+            frameState, forEachFeatureAtCoordinate, thisArg);
+      }
+      if (result) {
+        return result;
+      }
+    }
+  }
+  return undefined;
+};
+
+
+/**
+ * @param {ol.Pixel} pixel Pixel.
+ * @param {olx.FrameState} frameState FrameState.
+ * @param {function(this: S, ol.layer.Layer): T} callback Layer
+ *     callback.
+ * @param {S} thisArg Value to use as `this` when executing `callback`.
+ * @param {function(this: U, ol.layer.Layer): boolean} layerFilter Layer filter
+ *     function, only layers which are visible and for which this function
+ *     returns `true` will be tested for features.  By default, all visible
+ *     layers will be tested.
+ * @param {U} thisArg2 Value to use as `this` when executing `layerFilter`.
+ * @return {T|undefined} Callback result.
+ * @template S,T,U
+ */
+ol.renderer.Map.prototype.forEachLayerAtPixel = function(pixel, frameState, callback, thisArg,
+        layerFilter, thisArg2) {
+  var result;
+  var viewState = frameState.viewState;
+  var viewResolution = viewState.resolution;
+
+  var layerStates = frameState.layerStatesArray;
+  var numLayers = layerStates.length;
+  var i;
+  for (i = numLayers - 1; i >= 0; --i) {
+    var layerState = layerStates[i];
+    var layer = layerState.layer;
+    if (ol.layer.Layer.visibleAtResolution(layerState, viewResolution) &&
+        layerFilter.call(thisArg2, layer)) {
+      var layerRenderer = this.getLayerRenderer(layer);
+      result = layerRenderer.forEachLayerAtPixel(
+          pixel, frameState, callback, thisArg);
+      if (result) {
+        return result;
+      }
+    }
+  }
+  return undefined;
+};
+
+
+/**
+ * @param {ol.Coordinate} coordinate Coordinate.
+ * @param {olx.FrameState} frameState FrameState.
+ * @param {function(this: U, ol.layer.Layer): boolean} layerFilter Layer filter
+ *     function, only layers which are visible and for which this function
+ *     returns `true` will be tested for features.  By default, all visible
+ *     layers will be tested.
+ * @param {U} thisArg Value to use as `this` when executing `layerFilter`.
+ * @return {boolean} Is there a feature at the given coordinate?
+ * @template U
+ */
+ol.renderer.Map.prototype.hasFeatureAtCoordinate = function(coordinate, frameState, layerFilter, thisArg) {
+  var hasFeature = this.forEachFeatureAtCoordinate(
+      coordinate, frameState, ol.functions.TRUE, this, layerFilter, thisArg);
+
+  return hasFeature !== undefined;
+};
+
+
+/**
+ * @param {ol.layer.Layer} layer Layer.
+ * @protected
+ * @return {ol.renderer.Layer} Layer renderer.
+ */
+ol.renderer.Map.prototype.getLayerRenderer = function(layer) {
+  var layerKey = goog.getUid(layer).toString();
+  if (layerKey in this.layerRenderers_) {
+    return this.layerRenderers_[layerKey];
+  } else {
+    var layerRenderer = this.createLayerRenderer(layer);
+    this.layerRenderers_[layerKey] = layerRenderer;
+    this.layerRendererListeners_[layerKey] = ol.events.listen(layerRenderer,
+        ol.events.EventType.CHANGE, this.handleLayerRendererChange_, this);
+
+    return layerRenderer;
+  }
+};
+
+
+/**
+ * @param {string} layerKey Layer key.
+ * @protected
+ * @return {ol.renderer.Layer} Layer renderer.
+ */
+ol.renderer.Map.prototype.getLayerRendererByKey = function(layerKey) {
+  goog.asserts.assert(layerKey in this.layerRenderers_,
+      'given layerKey (%s) exists in layerRenderers', layerKey);
+  return this.layerRenderers_[layerKey];
+};
+
+
+/**
+ * @protected
+ * @return {Object.<string, ol.renderer.Layer>} Layer renderers.
+ */
+ol.renderer.Map.prototype.getLayerRenderers = function() {
+  return this.layerRenderers_;
+};
+
+
+/**
+ * @return {ol.Map} Map.
+ */
+ol.renderer.Map.prototype.getMap = function() {
+  return this.map_;
+};
+
+
+/**
+ * @return {string} Type
+ */
+ol.renderer.Map.prototype.getType = goog.abstractMethod;
+
+
+/**
+ * Handle changes in a layer renderer.
+ * @private
+ */
+ol.renderer.Map.prototype.handleLayerRendererChange_ = function() {
+  this.map_.render();
+};
+
+
+/**
+ * @param {string} layerKey Layer key.
+ * @return {ol.renderer.Layer} Layer renderer.
+ * @private
+ */
+ol.renderer.Map.prototype.removeLayerRendererByKey_ = function(layerKey) {
+  goog.asserts.assert(layerKey in this.layerRenderers_,
+      'given layerKey (%s) exists in layerRenderers', layerKey);
+  var layerRenderer = this.layerRenderers_[layerKey];
+  delete this.layerRenderers_[layerKey];
+
+  goog.asserts.assert(layerKey in this.layerRendererListeners_,
+      'given layerKey (%s) exists in layerRendererListeners', layerKey);
+  ol.events.unlistenByKey(this.layerRendererListeners_[layerKey]);
+  delete this.layerRendererListeners_[layerKey];
+
+  return layerRenderer;
+};
+
+
+/**
+ * Render.
+ * @param {?olx.FrameState} frameState Frame state.
+ */
+ol.renderer.Map.prototype.renderFrame = ol.nullFunction;
+
+
+/**
+ * @param {ol.Map} map Map.
+ * @param {olx.FrameState} frameState Frame state.
+ * @private
+ */
+ol.renderer.Map.prototype.removeUnusedLayerRenderers_ = function(map, frameState) {
+  var layerKey;
+  for (layerKey in this.layerRenderers_) {
+    if (!frameState || !(layerKey in frameState.layerStates)) {
+      this.removeLayerRendererByKey_(layerKey).dispose();
+    }
+  }
+};
+
+
+/**
+ * @param {olx.FrameState} frameState Frame state.
+ * @protected
+ */
+ol.renderer.Map.prototype.scheduleExpireIconCache = function(frameState) {
+  frameState.postRenderFunctions.push(
+    /** @type {ol.PostRenderFunction} */ (ol.renderer.Map.expireIconCache_)
+  );
+};
+
+
+/**
+ * @param {!olx.FrameState} frameState Frame state.
+ * @protected
+ */
+ol.renderer.Map.prototype.scheduleRemoveUnusedLayerRenderers = function(frameState) {
+  var layerKey;
+  for (layerKey in this.layerRenderers_) {
+    if (!(layerKey in frameState.layerStates)) {
+      frameState.postRenderFunctions.push(
+        /** @type {ol.PostRenderFunction} */ (this.removeUnusedLayerRenderers_.bind(this))
+      );
+      return;
+    }
+  }
+};
+
+
+/**
+ * @param {ol.LayerState} state1 First layer state.
+ * @param {ol.LayerState} state2 Second layer state.
+ * @return {number} The zIndex difference.
+ */
+ol.renderer.Map.sortByZIndex = function(state1, state2) {
+  return state1.zIndex - state2.zIndex;
+};
+
+goog.provide('ol.structs.PriorityQueue');
+
+goog.require('goog.asserts');
+goog.require('ol.object');
+
+
+/**
+ * Priority queue.
+ *
+ * The implementation is inspired from the Closure Library's Heap class and
+ * Python's heapq module.
+ *
+ * @see http://closure-library.googlecode.com/svn/docs/closure_goog_structs_heap.js.source.html
+ * @see http://hg.python.org/cpython/file/2.7/Lib/heapq.py
+ *
+ * @constructor
+ * @param {function(T): number} priorityFunction Priority function.
+ * @param {function(T): string} keyFunction Key function.
+ * @struct
+ * @template T
+ */
+ol.structs.PriorityQueue = function(priorityFunction, keyFunction) {
+
+  /**
+   * @type {function(T): number}
+   * @private
+   */
+  this.priorityFunction_ = priorityFunction;
+
+  /**
+   * @type {function(T): string}
+   * @private
+   */
+  this.keyFunction_ = keyFunction;
+
+  /**
+   * @type {Array.<T>}
+   * @private
+   */
+  this.elements_ = [];
+
+  /**
+   * @type {Array.<number>}
+   * @private
+   */
+  this.priorities_ = [];
+
+  /**
+   * @type {Object.<string, boolean>}
+   * @private
+   */
+  this.queuedElements_ = {};
+
+};
+
+
+/**
+ * @const
+ * @type {number}
+ */
+ol.structs.PriorityQueue.DROP = Infinity;
+
+
+/**
+ * FIXME empty description for jsdoc
+ */
+ol.structs.PriorityQueue.prototype.assertValid = function() {
+  var elements = this.elements_;
+  var priorities = this.priorities_;
+  var n = elements.length;
+  goog.asserts.assert(priorities.length == n);
+  var i, priority;
+  for (i = 0; i < (n >> 1) - 1; ++i) {
+    priority = priorities[i];
+    goog.asserts.assert(priority <= priorities[this.getLeftChildIndex_(i)],
+        'priority smaller than or equal to priority of left child (%s <= %s)',
+        priority, priorities[this.getLeftChildIndex_(i)]);
+    goog.asserts.assert(priority <= priorities[this.getRightChildIndex_(i)],
+        'priority smaller than or equal to priority of right child (%s <= %s)',
+        priority, priorities[this.getRightChildIndex_(i)]);
+  }
+};
+
+
+/**
+ * FIXME empty description for jsdoc
+ */
+ol.structs.PriorityQueue.prototype.clear = function() {
+  this.elements_.length = 0;
+  this.priorities_.length = 0;
+  ol.object.clear(this.queuedElements_);
+};
+
+
+/**
+ * Remove and return the highest-priority element. O(log N).
+ * @return {T} Element.
+ */
+ol.structs.PriorityQueue.prototype.dequeue = function() {
+  var elements = this.elements_;
+  goog.asserts.assert(elements.length > 0,
+      'must have elements in order to be able to dequeue');
+  var priorities = this.priorities_;
+  var element = elements[0];
+  if (elements.length == 1) {
+    elements.length = 0;
+    priorities.length = 0;
+  } else {
+    elements[0] = elements.pop();
+    priorities[0] = priorities.pop();
+    this.siftUp_(0);
+  }
+  var elementKey = this.keyFunction_(element);
+  goog.asserts.assert(elementKey in this.queuedElements_,
+      'key %s is not listed as queued', elementKey);
+  delete this.queuedElements_[elementKey];
+  return element;
+};
+
+
+/**
+ * Enqueue an element. O(log N).
+ * @param {T} element Element.
+ * @return {boolean} The element was added to the queue.
+ */
+ol.structs.PriorityQueue.prototype.enqueue = function(element) {
+  goog.asserts.assert(!(this.keyFunction_(element) in this.queuedElements_),
+      'key %s is already listed as queued', this.keyFunction_(element));
+  var priority = this.priorityFunction_(element);
+  if (priority != ol.structs.PriorityQueue.DROP) {
+    this.elements_.push(element);
+    this.priorities_.push(priority);
+    this.queuedElements_[this.keyFunction_(element)] = true;
+    this.siftDown_(0, this.elements_.length - 1);
+    return true;
+  }
+  return false;
+};
+
+
+/**
+ * @return {number} Count.
+ */
+ol.structs.PriorityQueue.prototype.getCount = function() {
+  return this.elements_.length;
+};
+
+
+/**
+ * Gets the index of the left child of the node at the given index.
+ * @param {number} index The index of the node to get the left child for.
+ * @return {number} The index of the left child.
+ * @private
+ */
+ol.structs.PriorityQueue.prototype.getLeftChildIndex_ = function(index) {
+  return index * 2 + 1;
+};
+
+
+/**
+ * Gets the index of the right child of the node at the given index.
+ * @param {number} index The index of the node to get the right child for.
+ * @return {number} The index of the right child.
+ * @private
+ */
+ol.structs.PriorityQueue.prototype.getRightChildIndex_ = function(index) {
+  return index * 2 + 2;
+};
+
+
+/**
+ * Gets the index of the parent of the node at the given index.
+ * @param {number} index The index of the node to get the parent for.
+ * @return {number} The index of the parent.
+ * @private
+ */
+ol.structs.PriorityQueue.prototype.getParentIndex_ = function(index) {
+  return (index - 1) >> 1;
+};
+
+
+/**
+ * Make this a heap. O(N).
+ * @private
+ */
+ol.structs.PriorityQueue.prototype.heapify_ = function() {
+  var i;
+  for (i = (this.elements_.length >> 1) - 1; i >= 0; i--) {
+    this.siftUp_(i);
+  }
+};
+
+
+/**
+ * @return {boolean} Is empty.
+ */
+ol.structs.PriorityQueue.prototype.isEmpty = function() {
+  return this.elements_.length === 0;
+};
+
+
+/**
+ * @param {string} key Key.
+ * @return {boolean} Is key queued.
+ */
+ol.structs.PriorityQueue.prototype.isKeyQueued = function(key) {
+  return key in this.queuedElements_;
+};
+
+
+/**
+ * @param {T} element Element.
+ * @return {boolean} Is queued.
+ */
+ol.structs.PriorityQueue.prototype.isQueued = function(element) {
+  return this.isKeyQueued(this.keyFunction_(element));
+};
+
+
+/**
+ * @param {number} index The index of the node to move down.
+ * @private
+ */
+ol.structs.PriorityQueue.prototype.siftUp_ = function(index) {
+  var elements = this.elements_;
+  var priorities = this.priorities_;
+  var count = elements.length;
+  var element = elements[index];
+  var priority = priorities[index];
+  var startIndex = index;
+
+  while (index < (count >> 1)) {
+    var lIndex = this.getLeftChildIndex_(index);
+    var rIndex = this.getRightChildIndex_(index);
+
+    var smallerChildIndex = rIndex < count &&
+        priorities[rIndex] < priorities[lIndex] ?
+        rIndex : lIndex;
+
+    elements[index] = elements[smallerChildIndex];
+    priorities[index] = priorities[smallerChildIndex];
+    index = smallerChildIndex;
+  }
+
+  elements[index] = element;
+  priorities[index] = priority;
+  this.siftDown_(startIndex, index);
+};
+
+
+/**
+ * @param {number} startIndex The index of the root.
+ * @param {number} index The index of the node to move up.
+ * @private
+ */
+ol.structs.PriorityQueue.prototype.siftDown_ = function(startIndex, index) {
+  var elements = this.elements_;
+  var priorities = this.priorities_;
+  var element = elements[index];
+  var priority = priorities[index];
+
+  while (index > startIndex) {
+    var parentIndex = this.getParentIndex_(index);
+    if (priorities[parentIndex] > priority) {
+      elements[index] = elements[parentIndex];
+      priorities[index] = priorities[parentIndex];
+      index = parentIndex;
+    } else {
+      break;
+    }
+  }
+  elements[index] = element;
+  priorities[index] = priority;
+};
+
+
+/**
+ * FIXME empty description for jsdoc
+ */
+ol.structs.PriorityQueue.prototype.reprioritize = function() {
+  var priorityFunction = this.priorityFunction_;
+  var elements = this.elements_;
+  var priorities = this.priorities_;
+  var index = 0;
+  var n = elements.length;
+  var element, i, priority;
+  for (i = 0; i < n; ++i) {
+    element = elements[i];
+    priority = priorityFunction(element);
+    if (priority == ol.structs.PriorityQueue.DROP) {
+      delete this.queuedElements_[this.keyFunction_(element)];
+    } else {
+      priorities[index] = priority;
+      elements[index++] = element;
+    }
+  }
+  elements.length = index;
+  priorities.length = index;
+  this.heapify_();
+};
+
+goog.provide('ol.TileQueue');
+
+goog.require('goog.asserts');
+goog.require('ol.events');
+goog.require('ol.events.EventType');
+goog.require('ol.TileState');
+goog.require('ol.structs.PriorityQueue');
+
+
+/**
+ * @constructor
+ * @extends {ol.structs.PriorityQueue.<Array>}
+ * @param {ol.TilePriorityFunction} tilePriorityFunction
+ *     Tile priority function.
+ * @param {function(): ?} tileChangeCallback
+ *     Function called on each tile change event.
+ * @struct
+ */
+ol.TileQueue = function(tilePriorityFunction, tileChangeCallback) {
+
+  ol.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 {ol.Tile} */ (element[0]).getKey();
+      });
+
+  /**
+   * @private
+   * @type {function(): ?}
+   */
+  this.tileChangeCallback_ = tileChangeCallback;
+
+  /**
+   * @private
+   * @type {number}
+   */
+  this.tilesLoading_ = 0;
+
+  /**
+   * @private
+   * @type {!Object.<string,boolean>}
+   */
+  this.tilesLoadingKeys_ = {};
+
+};
+ol.inherits(ol.TileQueue, ol.structs.PriorityQueue);
+
+
+/**
+ * @inheritDoc
+ */
+ol.TileQueue.prototype.enqueue = function(element) {
+  var added = ol.structs.PriorityQueue.prototype.enqueue.call(this, element);
+  if (added) {
+    var tile = element[0];
+    ol.events.listen(tile, ol.events.EventType.CHANGE,
+        this.handleTileChange, this);
+  }
+  return added;
+};
+
+
+/**
+ * @return {number} Number of tiles loading.
+ */
+ol.TileQueue.prototype.getTilesLoading = function() {
+  return this.tilesLoading_;
+};
+
+
+/**
+ * @param {ol.events.Event} event Event.
+ * @protected
+ */
+ol.TileQueue.prototype.handleTileChange = function(event) {
+  var tile = /** @type {ol.Tile} */ (event.target);
+  var state = tile.getState();
+  if (state === ol.TileState.LOADED || state === ol.TileState.ERROR ||
+      state === ol.TileState.EMPTY || state === ol.TileState.ABORT) {
+    ol.events.unlisten(tile, ol.events.EventType.CHANGE,
+        this.handleTileChange, this);
+    var tileKey = tile.getKey();
+    if (tileKey in this.tilesLoadingKeys_) {
+      delete this.tilesLoadingKeys_[tileKey];
+      --this.tilesLoading_;
+    }
+    this.tileChangeCallback_();
+  }
+  goog.asserts.assert(Object.keys(this.tilesLoadingKeys_).length === this.tilesLoading_);
+};
+
+
+/**
+ * @param {number} maxTotalLoading Maximum number tiles to load simultaneously.
+ * @param {number} maxNewLoads Maximum number of new tiles to load.
+ */
+ol.TileQueue.prototype.loadMoreTiles = function(maxTotalLoading, maxNewLoads) {
+  var newLoads = 0;
+  var tile, tileKey;
+  while (this.tilesLoading_ < maxTotalLoading && newLoads < maxNewLoads &&
+         this.getCount() > 0) {
+    tile = /** @type {ol.Tile} */ (this.dequeue()[0]);
+    tileKey = tile.getKey();
+    if (tile.getState() === ol.TileState.IDLE && !(tileKey in this.tilesLoadingKeys_)) {
+      this.tilesLoadingKeys_[tileKey] = true;
+      ++this.tilesLoading_;
+      ++newLoads;
+      tile.load();
+    }
+    goog.asserts.assert(Object.keys(this.tilesLoadingKeys_).length === this.tilesLoading_);
+  }
+};
+
+goog.provide('ol.Kinetic');
+
+goog.require('ol.animation');
+
+
+/**
+ * @classdesc
+ * Implementation of inertial deceleration for map movement.
+ *
+ * @constructor
+ * @param {number} decay Rate of decay (must be negative).
+ * @param {number} minVelocity Minimum velocity (pixels/millisecond).
+ * @param {number} delay Delay to consider to calculate the kinetic
+ *     initial values (milliseconds).
+ * @struct
+ * @api
+ */
+ol.Kinetic = function(decay, minVelocity, delay) {
+
+  /**
+   * @private
+   * @type {number}
+   */
+  this.decay_ = decay;
+
+  /**
+   * @private
+   * @type {number}
+   */
+  this.minVelocity_ = minVelocity;
+
+  /**
+   * @private
+   * @type {number}
+   */
+  this.delay_ = delay;
+
+  /**
+   * @private
+   * @type {Array.<number>}
+   */
+  this.points_ = [];
+
+  /**
+   * @private
+   * @type {number}
+   */
+  this.angle_ = 0;
+
+  /**
+   * @private
+   * @type {number}
+   */
+  this.initialVelocity_ = 0;
+};
+
+
+/**
+ * FIXME empty description for jsdoc
+ */
+ol.Kinetic.prototype.begin = function() {
+  this.points_.length = 0;
+  this.angle_ = 0;
+  this.initialVelocity_ = 0;
+};
+
+
+/**
+ * @param {number} x X.
+ * @param {number} y Y.
+ */
+ol.Kinetic.prototype.update = function(x, y) {
+  this.points_.push(x, y, Date.now());
+};
+
+
+/**
+ * @return {boolean} Whether we should do kinetic animation.
+ */
+ol.Kinetic.prototype.end = function() {
+  if (this.points_.length < 6) {
+    // at least 2 points are required (i.e. there must be at least 6 elements
+    // in the array)
+    return false;
+  }
+  var delay = Date.now() - this.delay_;
+  var lastIndex = this.points_.length - 3;
+  if (this.points_[lastIndex + 2] < delay) {
+    // the last tracked point is too old, which means that the user stopped
+    // panning before releasing the map
+    return false;
+  }
+
+  // get the first point which still falls into the delay time
+  var firstIndex = lastIndex - 3;
+  while (firstIndex > 0 && this.points_[firstIndex + 2] > delay) {
+    firstIndex -= 3;
+  }
+  var duration = this.points_[lastIndex + 2] - this.points_[firstIndex + 2];
+  var dx = this.points_[lastIndex] - this.points_[firstIndex];
+  var dy = this.points_[lastIndex + 1] - this.points_[firstIndex + 1];
+  this.angle_ = Math.atan2(dy, dx);
+  this.initialVelocity_ = Math.sqrt(dx * dx + dy * dy) / duration;
+  return this.initialVelocity_ > this.minVelocity_;
+};
+
+
+/**
+ * @param {ol.Coordinate} source Source coordinate for the animation.
+ * @return {ol.PreRenderFunction} Pre-render function for kinetic animation.
+ */
+ol.Kinetic.prototype.pan = function(source) {
+  var decay = this.decay_;
+  var initialVelocity = this.initialVelocity_;
+  var velocity = this.minVelocity_ - initialVelocity;
+  var duration = this.getDuration_();
+  var easingFunction = (
+      /**
+       * @param {number} t T.
+       * @return {number} Easing.
+       */
+      function(t) {
+        return initialVelocity * (Math.exp((decay * t) * duration) - 1) /
+            velocity;
+      });
+  return ol.animation.pan({
+    source: source,
+    duration: duration,
+    easing: easingFunction
+  });
+};
+
+
+/**
+ * @private
+ * @return {number} Duration of animation (milliseconds).
+ */
+ol.Kinetic.prototype.getDuration_ = function() {
+  return Math.log(this.minVelocity_ / this.initialVelocity_) / this.decay_;
+};
+
+
+/**
+ * @return {number} Total distance travelled (pixels).
+ */
+ol.Kinetic.prototype.getDistance = function() {
+  return (this.minVelocity_ - this.initialVelocity_) / this.decay_;
+};
+
+
+/**
+ * @return {number} Angle of the kinetic panning animation (radians).
+ */
+ol.Kinetic.prototype.getAngle = function() {
+  return this.angle_;
+};
+
+// FIXME factor out key precondition (shift et. al)
+
+goog.provide('ol.interaction.Interaction');
+goog.provide('ol.interaction.InteractionProperty');
+
+goog.require('ol');
+goog.require('ol.MapBrowserEvent');
+goog.require('ol.Object');
+goog.require('ol.animation');
+goog.require('ol.easing');
+
+
+/**
+ * @enum {string}
+ */
+ol.interaction.InteractionProperty = {
+  ACTIVE: 'active'
+};
+
+
+/**
+ * @classdesc
+ * Abstract base class; normally only used for creating subclasses and not
+ * instantiated in apps.
+ * User actions that change the state of the map. Some are similar to controls,
+ * but are not associated with a DOM element.
+ * For example, {@link ol.interaction.KeyboardZoom} is functionally the same as
+ * {@link ol.control.Zoom}, but triggered by a keyboard event not a button
+ * element event.
+ * Although interactions do not have a DOM element, some of them do render
+ * vectors and so are visible on the screen.
+ *
+ * @constructor
+ * @param {olx.interaction.InteractionOptions} options Options.
+ * @extends {ol.Object}
+ * @api
+ */
+ol.interaction.Interaction = function(options) {
+
+  ol.Object.call(this);
+
+  /**
+   * @private
+   * @type {ol.Map}
+   */
+  this.map_ = null;
+
+  this.setActive(true);
+
+  /**
+   * @type {function(ol.MapBrowserEvent):boolean}
+   */
+  this.handleEvent = options.handleEvent;
+
+};
+ol.inherits(ol.interaction.Interaction, ol.Object);
+
+
+/**
+ * Return whether the interaction is currently active.
+ * @return {boolean} `true` if the interaction is active, `false` otherwise.
+ * @observable
+ * @api
+ */
+ol.interaction.Interaction.prototype.getActive = function() {
+  return /** @type {boolean} */ (
+      this.get(ol.interaction.InteractionProperty.ACTIVE));
+};
+
+
+/**
+ * Get the map associated with this interaction.
+ * @return {ol.Map} Map.
+ * @api
+ */
+ol.interaction.Interaction.prototype.getMap = function() {
+  return this.map_;
+};
+
+
+/**
+ * Activate or deactivate the interaction.
+ * @param {boolean} active Active.
+ * @observable
+ * @api
+ */
+ol.interaction.Interaction.prototype.setActive = function(active) {
+  this.set(ol.interaction.InteractionProperty.ACTIVE, active);
+};
+
+
+/**
+ * Remove the interaction from its current map and attach it to the new map.
+ * Subclasses may set up event handlers to get notified about changes to
+ * the map here.
+ * @param {ol.Map} map Map.
+ */
+ol.interaction.Interaction.prototype.setMap = function(map) {
+  this.map_ = map;
+};
+
+
+/**
+ * @param {ol.Map} map Map.
+ * @param {ol.View} view View.
+ * @param {ol.Coordinate} delta Delta.
+ * @param {number=} opt_duration Duration.
+ */
+ol.interaction.Interaction.pan = function(map, view, delta, opt_duration) {
+  var currentCenter = view.getCenter();
+  if (currentCenter) {
+    if (opt_duration && opt_duration > 0) {
+      map.beforeRender(ol.animation.pan({
+        source: currentCenter,
+        duration: opt_duration,
+        easing: ol.easing.linear
+      }));
+    }
+    var center = view.constrainCenter(
+        [currentCenter[0] + delta[0], currentCenter[1] + delta[1]]);
+    view.setCenter(center);
+  }
+};
+
+
+/**
+ * @param {ol.Map} map Map.
+ * @param {ol.View} view View.
+ * @param {number|undefined} rotation Rotation.
+ * @param {ol.Coordinate=} opt_anchor Anchor coordinate.
+ * @param {number=} opt_duration Duration.
+ */
+ol.interaction.Interaction.rotate = function(map, view, rotation, opt_anchor, opt_duration) {
+  rotation = view.constrainRotation(rotation, 0);
+  ol.interaction.Interaction.rotateWithoutConstraints(
+      map, view, rotation, opt_anchor, opt_duration);
+};
+
+
+/**
+ * @param {ol.Map} map Map.
+ * @param {ol.View} view View.
+ * @param {number|undefined} rotation Rotation.
+ * @param {ol.Coordinate=} opt_anchor Anchor coordinate.
+ * @param {number=} opt_duration Duration.
+ */
+ol.interaction.Interaction.rotateWithoutConstraints = function(map, view, rotation, opt_anchor, opt_duration) {
+  if (rotation !== undefined) {
+    var currentRotation = view.getRotation();
+    var currentCenter = view.getCenter();
+    if (currentRotation !== undefined && currentCenter &&
+        opt_duration && opt_duration > 0) {
+      map.beforeRender(ol.animation.rotate({
+        rotation: currentRotation,
+        duration: opt_duration,
+        easing: ol.easing.easeOut
+      }));
+      if (opt_anchor) {
+        map.beforeRender(ol.animation.pan({
+          source: currentCenter,
+          duration: opt_duration,
+          easing: ol.easing.easeOut
+        }));
+      }
+    }
+    view.rotate(rotation, opt_anchor);
+  }
+};
+
+
+/**
+ * @param {ol.Map} map Map.
+ * @param {ol.View} view View.
+ * @param {number|undefined} resolution Resolution to go to.
+ * @param {ol.Coordinate=} opt_anchor Anchor coordinate.
+ * @param {number=} opt_duration Duration.
+ * @param {number=} opt_direction Zooming direction; > 0 indicates
+ *     zooming out, in which case the constraints system will select
+ *     the largest nearest resolution; < 0 indicates zooming in, in
+ *     which case the constraints system will select the smallest
+ *     nearest resolution; == 0 indicates that the zooming direction
+ *     is unknown/not relevant, in which case the constraints system
+ *     will select the nearest resolution. If not defined 0 is
+ *     assumed.
+ */
+ol.interaction.Interaction.zoom = function(map, view, resolution, opt_anchor, opt_duration, opt_direction) {
+  resolution = view.constrainResolution(resolution, 0, opt_direction);
+  ol.interaction.Interaction.zoomWithoutConstraints(
+      map, view, resolution, opt_anchor, opt_duration);
+};
+
+
+/**
+ * @param {ol.Map} map Map.
+ * @param {ol.View} view View.
+ * @param {number} delta Delta from previous zoom level.
+ * @param {ol.Coordinate=} opt_anchor Anchor coordinate.
+ * @param {number=} opt_duration Duration.
+ */
+ol.interaction.Interaction.zoomByDelta = function(map, view, delta, opt_anchor, opt_duration) {
+  var currentResolution = view.getResolution();
+  var resolution = view.constrainResolution(currentResolution, delta, 0);
+  ol.interaction.Interaction.zoomWithoutConstraints(
+      map, view, resolution, opt_anchor, opt_duration);
+};
+
+
+/**
+ * @param {ol.Map} map Map.
+ * @param {ol.View} view View.
+ * @param {number|undefined} resolution Resolution to go to.
+ * @param {ol.Coordinate=} opt_anchor Anchor coordinate.
+ * @param {number=} opt_duration Duration.
+ */
+ol.interaction.Interaction.zoomWithoutConstraints = function(map, view, resolution, opt_anchor, opt_duration) {
+  if (resolution) {
+    var currentResolution = view.getResolution();
+    var currentCenter = view.getCenter();
+    if (currentResolution !== undefined && currentCenter &&
+        resolution !== currentResolution &&
+        opt_duration && opt_duration > 0) {
+      map.beforeRender(ol.animation.zoom({
+        resolution: currentResolution,
+        duration: opt_duration,
+        easing: ol.easing.easeOut
+      }));
+      if (opt_anchor) {
+        map.beforeRender(ol.animation.pan({
+          source: currentCenter,
+          duration: opt_duration,
+          easing: ol.easing.easeOut
+        }));
+      }
+    }
+    if (opt_anchor) {
+      var center = view.calculateCenterZoom(resolution, opt_anchor);
+      view.setCenter(center);
+    }
+    view.setResolution(resolution);
+  }
+};
+
+goog.provide('ol.interaction.DoubleClickZoom');
+
+goog.require('goog.asserts');
+goog.require('ol.MapBrowserEvent');
+goog.require('ol.MapBrowserEvent.EventType');
+goog.require('ol.interaction.Interaction');
+
+
+/**
+ * @classdesc
+ * Allows the user to zoom by double-clicking on the map.
+ *
+ * @constructor
+ * @extends {ol.interaction.Interaction}
+ * @param {olx.interaction.DoubleClickZoomOptions=} opt_options Options.
+ * @api stable
+ */
+ol.interaction.DoubleClickZoom = function(opt_options) {
+
+  var options = opt_options ? opt_options : {};
+
+  /**
+   * @private
+   * @type {number}
+   */
+  this.delta_ = options.delta ? options.delta : 1;
+
+  ol.interaction.Interaction.call(this, {
+    handleEvent: ol.interaction.DoubleClickZoom.handleEvent
+  });
+
+  /**
+   * @private
+   * @type {number}
+   */
+  this.duration_ = options.duration !== undefined ? options.duration : 250;
+
+};
+ol.inherits(ol.interaction.DoubleClickZoom, ol.interaction.Interaction);
+
+
+/**
+ * Handles the {@link ol.MapBrowserEvent map browser event} (if it was a
+ * doubleclick) and eventually zooms the map.
+ * @param {ol.MapBrowserEvent} mapBrowserEvent Map browser event.
+ * @return {boolean} `false` to stop event propagation.
+ * @this {ol.interaction.DoubleClickZoom}
+ * @api
+ */
+ol.interaction.DoubleClickZoom.handleEvent = function(mapBrowserEvent) {
+  var stopEvent = false;
+  var browserEvent = mapBrowserEvent.originalEvent;
+  if (mapBrowserEvent.type == ol.MapBrowserEvent.EventType.DBLCLICK) {
+    var map = mapBrowserEvent.map;
+    var anchor = mapBrowserEvent.coordinate;
+    var delta = browserEvent.shiftKey ? -this.delta_ : this.delta_;
+    var view = map.getView();
+    goog.asserts.assert(view, 'map must have a view');
+    ol.interaction.Interaction.zoomByDelta(
+        map, view, delta, anchor, this.duration_);
+    mapBrowserEvent.preventDefault();
+    stopEvent = true;
+  }
+  return !stopEvent;
+};
+
+goog.provide('ol.events.condition');
+
+goog.require('goog.asserts');
+goog.require('ol.functions');
+goog.require('ol.MapBrowserEvent.EventType');
+goog.require('ol.MapBrowserPointerEvent');
+
+
+/**
+ * Return `true` if only the alt-key is pressed, `false` otherwise (e.g. when
+ * additionally the shift-key is pressed).
+ *
+ * @param {ol.MapBrowserEvent} mapBrowserEvent Map browser event.
+ * @return {boolean} True if only the alt key is pressed.
+ * @api stable
+ */
+ol.events.condition.altKeyOnly = function(mapBrowserEvent) {
+  var 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 {ol.MapBrowserEvent} mapBrowserEvent Map browser event.
+ * @return {boolean} True if only the alt and shift keys are pressed.
+ * @api stable
+ */
+ol.events.condition.altShiftKeysOnly = function(mapBrowserEvent) {
+  var originalEvent = mapBrowserEvent.originalEvent;
+  return (
+      originalEvent.altKey &&
+      !(originalEvent.metaKey || originalEvent.ctrlKey) &&
+      originalEvent.shiftKey);
+};
+
+
+/**
+ * Return always true.
+ *
+ * @param {ol.MapBrowserEvent} mapBrowserEvent Map browser event.
+ * @return {boolean} True.
+ * @function
+ * @api stable
+ */
+ol.events.condition.always = ol.functions.TRUE;
+
+
+/**
+ * Return `true` if the event is a `click` event, `false` otherwise.
+ *
+ * @param {ol.MapBrowserEvent} mapBrowserEvent Map browser event.
+ * @return {boolean} True if the event is a map `click` event.
+ * @api stable
+ */
+ol.events.condition.click = function(mapBrowserEvent) {
+  return mapBrowserEvent.type == ol.MapBrowserEvent.EventType.CLICK;
+};
+
+
+/**
+ * Return `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 {ol.MapBrowserEvent} mapBrowserEvent Map browser event.
+ * @return {boolean} The result.
+ */
+ol.events.condition.mouseActionButton = function(mapBrowserEvent) {
+  var originalEvent = mapBrowserEvent.originalEvent;
+  return originalEvent.button == 0 &&
+      !(goog.userAgent.WEBKIT && ol.has.MAC && originalEvent.ctrlKey);
+};
+
+
+/**
+ * Return always false.
+ *
+ * @param {ol.MapBrowserEvent} mapBrowserEvent Map browser event.
+ * @return {boolean} False.
+ * @function
+ * @api stable
+ */
+ol.events.condition.never = ol.functions.FALSE;
+
+
+/**
+ * Return `true` if the browser event is a `pointermove` event, `false`
+ * otherwise.
+ *
+ * @param {ol.MapBrowserEvent} mapBrowserEvent Map browser event.
+ * @return {boolean} True if the browser event is a `pointermove` event.
+ * @api
+ */
+ol.events.condition.pointerMove = function(mapBrowserEvent) {
+  return mapBrowserEvent.type == 'pointermove';
+};
+
+
+/**
+ * Return `true` if the event is a map `singleclick` event, `false` otherwise.
+ *
+ * @param {ol.MapBrowserEvent} mapBrowserEvent Map browser event.
+ * @return {boolean} True if the event is a map `singleclick` event.
+ * @api stable
+ */
+ol.events.condition.singleClick = function(mapBrowserEvent) {
+  return mapBrowserEvent.type == ol.MapBrowserEvent.EventType.SINGLECLICK;
+};
+
+
+/**
+ * Return `true` if the event is a map `dblclick` event, `false` otherwise.
+ *
+ * @param {ol.MapBrowserEvent} mapBrowserEvent Map browser event.
+ * @return {boolean} True if the event is a map `dblclick` event.
+ * @api stable
+ */
+ol.events.condition.doubleClick = function(mapBrowserEvent) {
+  return mapBrowserEvent.type == ol.MapBrowserEvent.EventType.DBLCLICK;
+};
+
+
+/**
+ * Return `true` if no modifier key (alt-, shift- or platform-modifier-key) is
+ * pressed.
+ *
+ * @param {ol.MapBrowserEvent} mapBrowserEvent Map browser event.
+ * @return {boolean} True only if there no modifier keys are pressed.
+ * @api stable
+ */
+ol.events.condition.noModifierKeys = function(mapBrowserEvent) {
+  var 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 {ol.MapBrowserEvent} mapBrowserEvent Map browser event.
+ * @return {boolean} True if only the platform modifier key is pressed.
+ * @api stable
+ */
+ol.events.condition.platformModifierKeyOnly = function(mapBrowserEvent) {
+  var originalEvent = mapBrowserEvent.originalEvent;
+  return (
+      !originalEvent.altKey &&
+      (ol.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 {ol.MapBrowserEvent} mapBrowserEvent Map browser event.
+ * @return {boolean} True if only the shift key is pressed.
+ * @api stable
+ */
+ol.events.condition.shiftKeyOnly = function(mapBrowserEvent) {
+  var 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 {ol.MapBrowserEvent} mapBrowserEvent Map browser event.
+ * @return {boolean} True only if the target element is not editable.
+ * @api
+ */
+ol.events.condition.targetNotEditable = function(mapBrowserEvent) {
+  var target = mapBrowserEvent.originalEvent.target;
+  goog.asserts.assertInstanceof(target, Element,
+      'target should be an Element');
+  var tagName = target.tagName;
+  return (
+      tagName !== 'INPUT' &&
+      tagName !== 'SELECT' &&
+      tagName !== 'TEXTAREA');
+};
+
+
+/**
+ * Return `true` if the event originates from a mouse device.
+ *
+ * @param {ol.MapBrowserEvent} mapBrowserEvent Map browser event.
+ * @return {boolean} True if the event originates from a mouse device.
+ * @api stable
+ */
+ol.events.condition.mouseOnly = function(mapBrowserEvent) {
+  // see http://www.w3.org/TR/pointerevents/#widl-PointerEvent-pointerType
+  goog.asserts.assertInstanceof(mapBrowserEvent, ol.MapBrowserPointerEvent,
+      'Requires an ol.MapBrowserPointerEvent to work.');
+  return 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 {ol.MapBrowserEvent} mapBrowserEvent Map browser event.
+ * @return {boolean} True if the event originates from a primary pointer.
+ * @api
+ */
+ol.events.condition.primaryAction = function(mapBrowserEvent) {
+  var pointerEvent = mapBrowserEvent.pointerEvent;
+  return pointerEvent.isPrimary && pointerEvent.button === 0;
+};
+
+goog.provide('ol.interaction.Pointer');
+
+goog.require('ol');
+goog.require('ol.MapBrowserEvent.EventType');
+goog.require('ol.MapBrowserPointerEvent');
+goog.require('ol.interaction.Interaction');
+goog.require('ol.object');
+
+
+/**
+ * @classdesc
+ * Base class that calls user-defined functions on `down`, `move` and `up`
+ * events. This class also manages "drag sequences".
+ *
+ * When the `handleDownEvent` user function returns `true` a drag sequence is
+ * started. During a drag sequence the `handleDragEvent` user function is
+ * called on `move` events. The drag sequence ends when the `handleUpEvent`
+ * user function is called and returns `false`.
+ *
+ * @constructor
+ * @param {olx.interaction.PointerOptions=} opt_options Options.
+ * @extends {ol.interaction.Interaction}
+ * @api
+ */
+ol.interaction.Pointer = function(opt_options) {
+
+  var options = opt_options ? opt_options : {};
+
+  var handleEvent = options.handleEvent ?
+      options.handleEvent : ol.interaction.Pointer.handleEvent;
+
+  ol.interaction.Interaction.call(this, {
+    handleEvent: handleEvent
+  });
+
+  /**
+   * @type {function(ol.MapBrowserPointerEvent):boolean}
+   * @private
+   */
+  this.handleDownEvent_ = options.handleDownEvent ?
+      options.handleDownEvent : ol.interaction.Pointer.handleDownEvent;
+
+  /**
+   * @type {function(ol.MapBrowserPointerEvent)}
+   * @private
+   */
+  this.handleDragEvent_ = options.handleDragEvent ?
+      options.handleDragEvent : ol.interaction.Pointer.handleDragEvent;
+
+  /**
+   * @type {function(ol.MapBrowserPointerEvent)}
+   * @private
+   */
+  this.handleMoveEvent_ = options.handleMoveEvent ?
+      options.handleMoveEvent : ol.interaction.Pointer.handleMoveEvent;
+
+  /**
+   * @type {function(ol.MapBrowserPointerEvent):boolean}
+   * @private
+   */
+  this.handleUpEvent_ = options.handleUpEvent ?
+      options.handleUpEvent : ol.interaction.Pointer.handleUpEvent;
+
+  /**
+   * @type {boolean}
+   * @protected
+   */
+  this.handlingDownUpSequence = false;
+
+  /**
+   * @type {Object.<number, ol.pointer.PointerEvent>}
+   * @private
+   */
+  this.trackedPointers_ = {};
+
+  /**
+   * @type {Array.<ol.pointer.PointerEvent>}
+   * @protected
+   */
+  this.targetPointers = [];
+
+};
+ol.inherits(ol.interaction.Pointer, ol.interaction.Interaction);
+
+
+/**
+ * @param {Array.<ol.pointer.PointerEvent>} pointerEvents List of events.
+ * @return {ol.Pixel} Centroid pixel.
+ */
+ol.interaction.Pointer.centroid = function(pointerEvents) {
+  var length = pointerEvents.length;
+  var clientX = 0;
+  var clientY = 0;
+  for (var i = 0; i < length; i++) {
+    clientX += pointerEvents[i].clientX;
+    clientY += pointerEvents[i].clientY;
+  }
+  return [clientX / length, clientY / length];
+};
+
+
+/**
+ * @param {ol.MapBrowserPointerEvent} mapBrowserEvent Event.
+ * @return {boolean} Whether the event is a pointerdown, pointerdrag
+ *     or pointerup event.
+ * @private
+ */
+ol.interaction.Pointer.prototype.isPointerDraggingEvent_ = function(mapBrowserEvent) {
+  var type = mapBrowserEvent.type;
+  return (
+      type === ol.MapBrowserEvent.EventType.POINTERDOWN ||
+      type === ol.MapBrowserEvent.EventType.POINTERDRAG ||
+      type === ol.MapBrowserEvent.EventType.POINTERUP);
+};
+
+
+/**
+ * @param {ol.MapBrowserPointerEvent} mapBrowserEvent Event.
+ * @private
+ */
+ol.interaction.Pointer.prototype.updateTrackedPointers_ = function(mapBrowserEvent) {
+  if (this.isPointerDraggingEvent_(mapBrowserEvent)) {
+    var event = mapBrowserEvent.pointerEvent;
+
+    if (mapBrowserEvent.type == ol.MapBrowserEvent.EventType.POINTERUP) {
+      delete this.trackedPointers_[event.pointerId];
+    } else if (mapBrowserEvent.type ==
+        ol.MapBrowserEvent.EventType.POINTERDOWN) {
+      this.trackedPointers_[event.pointerId] = event;
+    } else if (event.pointerId in this.trackedPointers_) {
+      // update only when there was a pointerdown event for this pointer
+      this.trackedPointers_[event.pointerId] = event;
+    }
+    this.targetPointers = ol.object.getValues(this.trackedPointers_);
+  }
+};
+
+
+/**
+ * @param {ol.MapBrowserPointerEvent} mapBrowserEvent Event.
+ * @this {ol.interaction.Pointer}
+ */
+ol.interaction.Pointer.handleDragEvent = ol.nullFunction;
+
+
+/**
+ * @param {ol.MapBrowserPointerEvent} mapBrowserEvent Event.
+ * @return {boolean} Capture dragging.
+ * @this {ol.interaction.Pointer}
+ */
+ol.interaction.Pointer.handleUpEvent = ol.functions.FALSE;
+
+
+/**
+ * @param {ol.MapBrowserPointerEvent} mapBrowserEvent Event.
+ * @return {boolean} Capture dragging.
+ * @this {ol.interaction.Pointer}
+ */
+ol.interaction.Pointer.handleDownEvent = ol.functions.FALSE;
+
+
+/**
+ * @param {ol.MapBrowserPointerEvent} mapBrowserEvent Event.
+ * @this {ol.interaction.Pointer}
+ */
+ol.interaction.Pointer.handleMoveEvent = ol.nullFunction;
+
+
+/**
+ * Handles the {@link ol.MapBrowserEvent map browser event} and may call into
+ * other functions, if event sequences like e.g. 'drag' or 'down-up' etc. are
+ * detected.
+ * @param {ol.MapBrowserEvent} mapBrowserEvent Map browser event.
+ * @return {boolean} `false` to stop event propagation.
+ * @this {ol.interaction.Pointer}
+ * @api
+ */
+ol.interaction.Pointer.handleEvent = function(mapBrowserEvent) {
+  if (!(mapBrowserEvent instanceof ol.MapBrowserPointerEvent)) {
+    return true;
+  }
+
+  var stopEvent = false;
+  this.updateTrackedPointers_(mapBrowserEvent);
+  if (this.handlingDownUpSequence) {
+    if (mapBrowserEvent.type == ol.MapBrowserEvent.EventType.POINTERDRAG) {
+      this.handleDragEvent_(mapBrowserEvent);
+    } else if (mapBrowserEvent.type == ol.MapBrowserEvent.EventType.POINTERUP) {
+      this.handlingDownUpSequence = this.handleUpEvent_(mapBrowserEvent);
+    }
+  }
+  if (mapBrowserEvent.type == ol.MapBrowserEvent.EventType.POINTERDOWN) {
+    var handled = this.handleDownEvent_(mapBrowserEvent);
+    this.handlingDownUpSequence = handled;
+    stopEvent = this.shouldStopEvent(handled);
+  } else if (mapBrowserEvent.type == ol.MapBrowserEvent.EventType.POINTERMOVE) {
+    this.handleMoveEvent_(mapBrowserEvent);
+  }
+  return !stopEvent;
+};
+
+
+/**
+ * This method is used to determine if "down" events should be propagated to
+ * other interactions or should be stopped.
+ *
+ * The method receives the return code of the "handleDownEvent" function.
+ *
+ * By default this function is the "identity" function. It's overidden in
+ * child classes.
+ *
+ * @param {boolean} handled Was the event handled by the interaction?
+ * @return {boolean} Should the event be stopped?
+ * @protected
+ */
+ol.interaction.Pointer.prototype.shouldStopEvent = function(handled) {
+  return handled;
+};
+
+goog.provide('ol.interaction.DragPan');
+
+goog.require('goog.asserts');
+goog.require('ol.Kinetic');
+
+goog.require('ol.ViewHint');
+goog.require('ol.coordinate');
+goog.require('ol.functions');
+goog.require('ol.events.condition');
+goog.require('ol.interaction.Pointer');
+
+
+/**
+ * @classdesc
+ * Allows the user to pan the map by dragging the map.
+ *
+ * @constructor
+ * @extends {ol.interaction.Pointer}
+ * @param {olx.interaction.DragPanOptions=} opt_options Options.
+ * @api stable
+ */
+ol.interaction.DragPan = function(opt_options) {
+
+  ol.interaction.Pointer.call(this, {
+    handleDownEvent: ol.interaction.DragPan.handleDownEvent_,
+    handleDragEvent: ol.interaction.DragPan.handleDragEvent_,
+    handleUpEvent: ol.interaction.DragPan.handleUpEvent_
+  });
+
+  var options = opt_options ? opt_options : {};
+
+  /**
+   * @private
+   * @type {ol.Kinetic|undefined}
+   */
+  this.kinetic_ = options.kinetic;
+
+  /**
+   * @private
+   * @type {?ol.PreRenderFunction}
+   */
+  this.kineticPreRenderFn_ = null;
+
+  /**
+   * @type {ol.Pixel}
+   */
+  this.lastCentroid = null;
+
+  /**
+   * @private
+   * @type {ol.EventsConditionType}
+   */
+  this.condition_ = options.condition ?
+      options.condition : ol.events.condition.noModifierKeys;
+
+  /**
+   * @private
+   * @type {boolean}
+   */
+  this.noKinetic_ = false;
+
+};
+ol.inherits(ol.interaction.DragPan, ol.interaction.Pointer);
+
+
+/**
+ * @param {ol.MapBrowserPointerEvent} mapBrowserEvent Event.
+ * @this {ol.interaction.DragPan}
+ * @private
+ */
+ol.interaction.DragPan.handleDragEvent_ = function(mapBrowserEvent) {
+  goog.asserts.assert(this.targetPointers.length >= 1,
+      'the length of this.targetPointers should be more than 1');
+  var centroid =
+      ol.interaction.Pointer.centroid(this.targetPointers);
+  if (this.kinetic_) {
+    this.kinetic_.update(centroid[0], centroid[1]);
+  }
+  if (this.lastCentroid) {
+    var deltaX = this.lastCentroid[0] - centroid[0];
+    var deltaY = centroid[1] - this.lastCentroid[1];
+    var map = mapBrowserEvent.map;
+    var view = map.getView();
+    var viewState = view.getState();
+    var center = [deltaX, deltaY];
+    ol.coordinate.scale(center, viewState.resolution);
+    ol.coordinate.rotate(center, viewState.rotation);
+    ol.coordinate.add(center, viewState.center);
+    center = view.constrainCenter(center);
+    map.render();
+    view.setCenter(center);
+  }
+  this.lastCentroid = centroid;
+};
+
+
+/**
+ * @param {ol.MapBrowserPointerEvent} mapBrowserEvent Event.
+ * @return {boolean} Stop drag sequence?
+ * @this {ol.interaction.DragPan}
+ * @private
+ */
+ol.interaction.DragPan.handleUpEvent_ = function(mapBrowserEvent) {
+  var map = mapBrowserEvent.map;
+  var view = map.getView();
+  if (this.targetPointers.length === 0) {
+    if (!this.noKinetic_ && this.kinetic_ && this.kinetic_.end()) {
+      var distance = this.kinetic_.getDistance();
+      var angle = this.kinetic_.getAngle();
+      var center = view.getCenter();
+      goog.asserts.assert(center !== undefined, 'center should be defined');
+      this.kineticPreRenderFn_ = this.kinetic_.pan(center);
+      map.beforeRender(this.kineticPreRenderFn_);
+      var centerpx = map.getPixelFromCoordinate(center);
+      var dest = map.getCoordinateFromPixel([
+        centerpx[0] - distance * Math.cos(angle),
+        centerpx[1] - distance * Math.sin(angle)
+      ]);
+      dest = view.constrainCenter(dest);
+      view.setCenter(dest);
+    }
+    view.setHint(ol.ViewHint.INTERACTING, -1);
+    map.render();
+    return false;
+  } else {
+    this.lastCentroid = null;
+    return true;
+  }
+};
+
+
+/**
+ * @param {ol.MapBrowserPointerEvent} mapBrowserEvent Event.
+ * @return {boolean} Start drag sequence?
+ * @this {ol.interaction.DragPan}
+ * @private
+ */
+ol.interaction.DragPan.handleDownEvent_ = function(mapBrowserEvent) {
+  if (this.targetPointers.length > 0 && this.condition_(mapBrowserEvent)) {
+    var map = mapBrowserEvent.map;
+    var view = map.getView();
+    this.lastCentroid = null;
+    if (!this.handlingDownUpSequence) {
+      view.setHint(ol.ViewHint.INTERACTING, 1);
+    }
+    map.render();
+    if (this.kineticPreRenderFn_ &&
+        map.removePreRenderFunction(this.kineticPreRenderFn_)) {
+      view.setCenter(mapBrowserEvent.frameState.viewState.center);
+      this.kineticPreRenderFn_ = null;
+    }
+    if (this.kinetic_) {
+      this.kinetic_.begin();
+    }
+    // No kinetic as soon as more than one pointer on the screen is
+    // detected. This is to prevent nasty pans after pinch.
+    this.noKinetic_ = this.targetPointers.length > 1;
+    return true;
+  } else {
+    return false;
+  }
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.interaction.DragPan.prototype.shouldStopEvent = ol.functions.FALSE;
+
+goog.provide('ol.interaction.DragRotate');
+
+goog.require('ol');
+goog.require('ol.ViewHint');
+goog.require('ol.functions');
+goog.require('ol.events.condition');
+goog.require('ol.interaction.Interaction');
+goog.require('ol.interaction.Pointer');
+
+
+/**
+ * @classdesc
+ * Allows the user to rotate the map by clicking and dragging on the map,
+ * normally combined with an {@link ol.events.condition} that limits
+ * it to when the alt and shift keys are held down.
+ *
+ * This interaction is only supported for mouse devices.
+ *
+ * @constructor
+ * @extends {ol.interaction.Pointer}
+ * @param {olx.interaction.DragRotateOptions=} opt_options Options.
+ * @api stable
+ */
+ol.interaction.DragRotate = function(opt_options) {
+
+  var options = opt_options ? opt_options : {};
+
+  ol.interaction.Pointer.call(this, {
+    handleDownEvent: ol.interaction.DragRotate.handleDownEvent_,
+    handleDragEvent: ol.interaction.DragRotate.handleDragEvent_,
+    handleUpEvent: ol.interaction.DragRotate.handleUpEvent_
+  });
+
+  /**
+   * @private
+   * @type {ol.EventsConditionType}
+   */
+  this.condition_ = options.condition ?
+      options.condition : ol.events.condition.altShiftKeysOnly;
+
+  /**
+   * @private
+   * @type {number|undefined}
+   */
+  this.lastAngle_ = undefined;
+
+  /**
+   * @private
+   * @type {number}
+   */
+  this.duration_ = options.duration !== undefined ? options.duration : 250;
+};
+ol.inherits(ol.interaction.DragRotate, ol.interaction.Pointer);
+
+
+/**
+ * @param {ol.MapBrowserPointerEvent} mapBrowserEvent Event.
+ * @this {ol.interaction.DragRotate}
+ * @private
+ */
+ol.interaction.DragRotate.handleDragEvent_ = function(mapBrowserEvent) {
+  if (!ol.events.condition.mouseOnly(mapBrowserEvent)) {
+    return;
+  }
+
+  var map = mapBrowserEvent.map;
+  var size = map.getSize();
+  var offset = mapBrowserEvent.pixel;
+  var theta =
+      Math.atan2(size[1] / 2 - offset[1], offset[0] - size[0] / 2);
+  if (this.lastAngle_ !== undefined) {
+    var delta = theta - this.lastAngle_;
+    var view = map.getView();
+    var rotation = view.getRotation();
+    map.render();
+    ol.interaction.Interaction.rotateWithoutConstraints(
+        map, view, rotation - delta);
+  }
+  this.lastAngle_ = theta;
+};
+
+
+/**
+ * @param {ol.MapBrowserPointerEvent} mapBrowserEvent Event.
+ * @return {boolean} Stop drag sequence?
+ * @this {ol.interaction.DragRotate}
+ * @private
+ */
+ol.interaction.DragRotate.handleUpEvent_ = function(mapBrowserEvent) {
+  if (!ol.events.condition.mouseOnly(mapBrowserEvent)) {
+    return true;
+  }
+
+  var map = mapBrowserEvent.map;
+  var view = map.getView();
+  view.setHint(ol.ViewHint.INTERACTING, -1);
+  var rotation = view.getRotation();
+  ol.interaction.Interaction.rotate(map, view, rotation,
+      undefined, this.duration_);
+  return false;
+};
+
+
+/**
+ * @param {ol.MapBrowserPointerEvent} mapBrowserEvent Event.
+ * @return {boolean} Start drag sequence?
+ * @this {ol.interaction.DragRotate}
+ * @private
+ */
+ol.interaction.DragRotate.handleDownEvent_ = function(mapBrowserEvent) {
+  if (!ol.events.condition.mouseOnly(mapBrowserEvent)) {
+    return false;
+  }
+
+  if (ol.events.condition.mouseActionButton(mapBrowserEvent) &&
+      this.condition_(mapBrowserEvent)) {
+    var map = mapBrowserEvent.map;
+    map.getView().setHint(ol.ViewHint.INTERACTING, 1);
+    map.render();
+    this.lastAngle_ = undefined;
+    return true;
+  } else {
+    return false;
+  }
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.interaction.DragRotate.prototype.shouldStopEvent = ol.functions.FALSE;
+
+// FIXME add rotation
+
+goog.provide('ol.render.Box');
+
+goog.require('goog.asserts');
+goog.require('ol.Disposable');
+goog.require('ol.geom.Polygon');
+
+
+/**
+ * @constructor
+ * @extends {ol.Disposable}
+ * @param {string} className CSS class name.
+ */
+ol.render.Box = function(className) {
+
+  /**
+   * @type {ol.geom.Polygon}
+   * @private
+   */
+  this.geometry_ = null;
+
+  /**
+   * @type {HTMLDivElement}
+   * @private
+   */
+  this.element_ = /** @type {HTMLDivElement} */ (document.createElement('div'));
+  this.element_.style.position = 'absolute';
+  this.element_.className = 'ol-box ' + className;
+
+  /**
+   * @private
+   * @type {ol.Map}
+   */
+  this.map_ = null;
+
+  /**
+   * @private
+   * @type {ol.Pixel}
+   */
+  this.startPixel_ = null;
+
+  /**
+   * @private
+   * @type {ol.Pixel}
+   */
+  this.endPixel_ = null;
+
+};
+ol.inherits(ol.render.Box, ol.Disposable);
+
+
+/**
+ * @inheritDoc
+ */
+ol.render.Box.prototype.disposeInternal = function() {
+  this.setMap(null);
+};
+
+
+/**
+ * @private
+ */
+ol.render.Box.prototype.render_ = function() {
+  var startPixel = this.startPixel_;
+  var endPixel = this.endPixel_;
+  goog.asserts.assert(startPixel, 'this.startPixel_ must be truthy');
+  goog.asserts.assert(endPixel, 'this.endPixel_ must be truthy');
+  var px = 'px';
+  var style = this.element_.style;
+  style.left = Math.min(startPixel[0], endPixel[0]) + px;
+  style.top = Math.min(startPixel[1], endPixel[1]) + px;
+  style.width = Math.abs(endPixel[0] - startPixel[0]) + px;
+  style.height = Math.abs(endPixel[1] - startPixel[1]) + px;
+};
+
+
+/**
+ * @param {ol.Map} map Map.
+ */
+ol.render.Box.prototype.setMap = function(map) {
+  if (this.map_) {
+    this.map_.getOverlayContainer().removeChild(this.element_);
+    var style = this.element_.style;
+    style.left = style.top = style.width = style.height = 'inherit';
+  }
+  this.map_ = map;
+  if (this.map_) {
+    this.map_.getOverlayContainer().appendChild(this.element_);
+  }
+};
+
+
+/**
+ * @param {ol.Pixel} startPixel Start pixel.
+ * @param {ol.Pixel} endPixel End pixel.
+ */
+ol.render.Box.prototype.setPixels = function(startPixel, endPixel) {
+  this.startPixel_ = startPixel;
+  this.endPixel_ = endPixel;
+  this.createOrUpdateGeometry();
+  this.render_();
+};
+
+
+/**
+ * Creates or updates the cached geometry.
+ */
+ol.render.Box.prototype.createOrUpdateGeometry = function() {
+  goog.asserts.assert(this.startPixel_,
+      'this.startPixel_ must be truthy');
+  goog.asserts.assert(this.endPixel_,
+      'this.endPixel_ must be truthy');
+  goog.asserts.assert(this.map_, 'this.map_ must be truthy');
+  var startPixel = this.startPixel_;
+  var endPixel = this.endPixel_;
+  var pixels = [
+    startPixel,
+    [startPixel[0], endPixel[1]],
+    endPixel,
+    [endPixel[0], startPixel[1]]
+  ];
+  var coordinates = pixels.map(this.map_.getCoordinateFromPixel, this.map_);
+  // close the polygon
+  coordinates[4] = coordinates[0].slice();
+  if (!this.geometry_) {
+    this.geometry_ = new ol.geom.Polygon([coordinates]);
+  } else {
+    this.geometry_.setCoordinates([coordinates]);
+  }
+};
+
+
+/**
+ * @return {ol.geom.Polygon} Geometry.
+ */
+ol.render.Box.prototype.getGeometry = function() {
+  return this.geometry_;
+};
+
+// FIXME draw drag box
+goog.provide('ol.DragBoxEvent');
+goog.provide('ol.interaction.DragBox');
+
+goog.require('ol.events.Event');
+goog.require('ol');
+goog.require('ol.events.condition');
+goog.require('ol.interaction.Pointer');
+goog.require('ol.render.Box');
+
+
+/**
+ * @const
+ * @type {number}
+ */
+ol.DRAG_BOX_HYSTERESIS_PIXELS_SQUARED =
+    ol.DRAG_BOX_HYSTERESIS_PIXELS *
+    ol.DRAG_BOX_HYSTERESIS_PIXELS;
+
+
+/**
+ * @enum {string}
+ */
+ol.DragBoxEventType = {
+  /**
+   * Triggered upon drag box start.
+   * @event ol.DragBoxEvent#boxstart
+   * @api stable
+   */
+  BOXSTART: 'boxstart',
+
+  /**
+   * Triggered on drag when box is active.
+   * @event ol.DragBoxEvent#boxdrag
+   * @api
+   */
+  BOXDRAG: 'boxdrag',
+
+  /**
+   * Triggered upon drag box end.
+   * @event ol.DragBoxEvent#boxend
+   * @api stable
+   */
+  BOXEND: 'boxend'
+};
+
+
+/**
+ * @classdesc
+ * Events emitted by {@link ol.interaction.DragBox} instances are instances of
+ * this type.
+ *
+ * @param {string} type The event type.
+ * @param {ol.Coordinate} coordinate The event coordinate.
+ * @param {ol.MapBrowserEvent} mapBrowserEvent Originating event.
+ * @extends {ol.events.Event}
+ * @constructor
+ * @implements {oli.DragBoxEvent}
+ */
+ol.DragBoxEvent = function(type, coordinate, mapBrowserEvent) {
+  ol.events.Event.call(this, type);
+
+  /**
+   * The coordinate of the drag event.
+   * @const
+   * @type {ol.Coordinate}
+   * @api stable
+   */
+  this.coordinate = coordinate;
+
+  /**
+   * @const
+   * @type {ol.MapBrowserEvent}
+   * @api
+   */
+  this.mapBrowserEvent = mapBrowserEvent;
+
+};
+ol.inherits(ol.DragBoxEvent, ol.events.Event);
+
+
+/**
+ * @classdesc
+ * Allows the user to draw a vector box by clicking and dragging on the map,
+ * normally combined with an {@link ol.events.condition} that limits
+ * it to when the shift or other key is held down. This is used, for example,
+ * for zooming to a specific area of the map
+ * (see {@link ol.interaction.DragZoom} and
+ * {@link ol.interaction.DragRotateAndZoom}).
+ *
+ * This interaction is only supported for mouse devices.
+ *
+ * @constructor
+ * @extends {ol.interaction.Pointer}
+ * @fires ol.DragBoxEvent
+ * @param {olx.interaction.DragBoxOptions=} opt_options Options.
+ * @api stable
+ */
+ol.interaction.DragBox = function(opt_options) {
+
+  ol.interaction.Pointer.call(this, {
+    handleDownEvent: ol.interaction.DragBox.handleDownEvent_,
+    handleDragEvent: ol.interaction.DragBox.handleDragEvent_,
+    handleUpEvent: ol.interaction.DragBox.handleUpEvent_
+  });
+
+  var options = opt_options ? opt_options : {};
+
+  /**
+   * @type {ol.render.Box}
+   * @private
+   */
+  this.box_ = new ol.render.Box(options.className || 'ol-dragbox');
+
+  /**
+   * @type {ol.Pixel}
+   * @private
+   */
+  this.startPixel_ = null;
+
+  /**
+   * @private
+   * @type {ol.EventsConditionType}
+   */
+  this.condition_ = options.condition ?
+      options.condition : ol.events.condition.always;
+
+  /**
+   * @private
+   * @type {ol.DragBoxEndConditionType}
+   */
+  this.boxEndCondition_ = options.boxEndCondition ?
+      options.boxEndCondition : ol.interaction.DragBox.defaultBoxEndCondition;
+};
+ol.inherits(ol.interaction.DragBox, ol.interaction.Pointer);
+
+
+/**
+ * The default condition for determining whether the boxend event
+ * should fire.
+ * @param  {ol.MapBrowserEvent} mapBrowserEvent The originating MapBrowserEvent
+ *  leading to the box end.
+ * @param  {ol.Pixel} startPixel      The starting pixel of the box.
+ * @param  {ol.Pixel} endPixel        The end pixel of the box.
+ * @return {boolean} Whether or not the boxend condition should be fired.
+ */
+ol.interaction.DragBox.defaultBoxEndCondition = function(mapBrowserEvent,
+    startPixel, endPixel) {
+  var width = endPixel[0] - startPixel[0];
+  var height = endPixel[1] - startPixel[1];
+  return width * width + height * height >=
+      ol.DRAG_BOX_HYSTERESIS_PIXELS_SQUARED;
+};
+
+
+/**
+ * @param {ol.MapBrowserPointerEvent} mapBrowserEvent Event.
+ * @this {ol.interaction.DragBox}
+ * @private
+ */
+ol.interaction.DragBox.handleDragEvent_ = function(mapBrowserEvent) {
+  if (!ol.events.condition.mouseOnly(mapBrowserEvent)) {
+    return;
+  }
+
+  this.box_.setPixels(this.startPixel_, mapBrowserEvent.pixel);
+
+  this.dispatchEvent(new ol.DragBoxEvent(ol.DragBoxEventType.BOXDRAG,
+    mapBrowserEvent.coordinate, mapBrowserEvent));
+};
+
+
+/**
+ * Returns geometry of last drawn box.
+ * @return {ol.geom.Polygon} Geometry.
+ * @api stable
+ */
+ol.interaction.DragBox.prototype.getGeometry = function() {
+  return this.box_.getGeometry();
+};
+
+
+/**
+ * To be overriden by child classes.
+ * FIXME: use constructor option instead of relying on overridding.
+ * @param {ol.MapBrowserEvent} mapBrowserEvent Map browser event.
+ * @protected
+ */
+ol.interaction.DragBox.prototype.onBoxEnd = ol.nullFunction;
+
+
+/**
+ * @param {ol.MapBrowserPointerEvent} mapBrowserEvent Event.
+ * @return {boolean} Stop drag sequence?
+ * @this {ol.interaction.DragBox}
+ * @private
+ */
+ol.interaction.DragBox.handleUpEvent_ = function(mapBrowserEvent) {
+  if (!ol.events.condition.mouseOnly(mapBrowserEvent)) {
+    return true;
+  }
+
+  this.box_.setMap(null);
+
+  if (this.boxEndCondition_(mapBrowserEvent,
+      this.startPixel_, mapBrowserEvent.pixel)) {
+    this.onBoxEnd(mapBrowserEvent);
+    this.dispatchEvent(new ol.DragBoxEvent(ol.DragBoxEventType.BOXEND,
+        mapBrowserEvent.coordinate, mapBrowserEvent));
+  }
+  return false;
+};
+
+
+/**
+ * @param {ol.MapBrowserPointerEvent} mapBrowserEvent Event.
+ * @return {boolean} Start drag sequence?
+ * @this {ol.interaction.DragBox}
+ * @private
+ */
+ol.interaction.DragBox.handleDownEvent_ = function(mapBrowserEvent) {
+  if (!ol.events.condition.mouseOnly(mapBrowserEvent)) {
+    return false;
+  }
+
+  if (ol.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 ol.DragBoxEvent(ol.DragBoxEventType.BOXSTART,
+        mapBrowserEvent.coordinate, mapBrowserEvent));
+    return true;
+  } else {
+    return false;
+  }
+};
+
+goog.provide('ol.interaction.DragZoom');
+
+goog.require('goog.asserts');
+goog.require('ol.animation');
+goog.require('ol.easing');
+goog.require('ol.events.condition');
+goog.require('ol.extent');
+goog.require('ol.interaction.DragBox');
+
+
+/**
+ * @classdesc
+ * Allows the user to zoom the map by clicking and dragging on the map,
+ * normally combined with an {@link ol.events.condition} that limits
+ * it to when a key, shift by default, is held down.
+ *
+ * To change the style of the box, use CSS and the `.ol-dragzoom` selector, or
+ * your custom one configured with `className`.
+ *
+ * @constructor
+ * @extends {ol.interaction.DragBox}
+ * @param {olx.interaction.DragZoomOptions=} opt_options Options.
+ * @api stable
+ */
+ol.interaction.DragZoom = function(opt_options) {
+  var options = opt_options ? opt_options : {};
+
+  var condition = options.condition ?
+      options.condition : ol.events.condition.shiftKeyOnly;
+
+  /**
+   * @private
+   * @type {number}
+   */
+  this.duration_ = options.duration !== undefined ? options.duration : 200;
+
+  /**
+   * @private
+   * @type {boolean}
+   */
+  this.out_ = options.out !== undefined ? options.out : false;
+
+  ol.interaction.DragBox.call(this, {
+    condition: condition,
+    className: options.className || 'ol-dragzoom'
+  });
+
+};
+ol.inherits(ol.interaction.DragZoom, ol.interaction.DragBox);
+
+
+/**
+ * @inheritDoc
+ */
+ol.interaction.DragZoom.prototype.onBoxEnd = function() {
+  var map = this.getMap();
+
+  var view = map.getView();
+  goog.asserts.assert(view, 'map must have view');
+
+  var size = map.getSize();
+  goog.asserts.assert(size !== undefined, 'size should be defined');
+
+  var extent = this.getGeometry().getExtent();
+
+  if (this.out_) {
+    var mapExtent = view.calculateExtent(size);
+    var boxPixelExtent = ol.extent.createOrUpdateFromCoordinates([
+      map.getPixelFromCoordinate(ol.extent.getBottomLeft(extent)),
+      map.getPixelFromCoordinate(ol.extent.getTopRight(extent))]);
+    var factor = view.getResolutionForExtent(boxPixelExtent, size);
+
+    ol.extent.scaleFromCenter(mapExtent, 1 / factor);
+    extent = mapExtent;
+  }
+
+  var resolution = view.constrainResolution(
+      view.getResolutionForExtent(extent, size));
+
+  var currentResolution = view.getResolution();
+  goog.asserts.assert(currentResolution !== undefined, 'res should be defined');
+
+  var currentCenter = view.getCenter();
+  goog.asserts.assert(currentCenter !== undefined, 'center should be defined');
+
+  map.beforeRender(ol.animation.zoom({
+    resolution: currentResolution,
+    duration: this.duration_,
+    easing: ol.easing.easeOut
+  }));
+  map.beforeRender(ol.animation.pan({
+    source: currentCenter,
+    duration: this.duration_,
+    easing: ol.easing.easeOut
+  }));
+
+  view.setCenter(ol.extent.getCenter(extent));
+  view.setResolution(resolution);
+};
+
+goog.provide('ol.interaction.KeyboardPan');
+
+goog.require('goog.asserts');
+goog.require('ol');
+goog.require('ol.coordinate');
+goog.require('ol.events.EventType');
+goog.require('ol.events.KeyCode');
+goog.require('ol.events.condition');
+goog.require('ol.interaction.Interaction');
+
+
+/**
+ * @classdesc
+ * Allows the user to pan the map using keyboard arrows.
+ * Note that, although this interaction is by default included in maps,
+ * the keys can only be used when browser focus is on the element to which
+ * the keyboard events are attached. By default, this is the map div,
+ * though you can change this with the `keyboardEventTarget` in
+ * {@link ol.Map}. `document` never loses focus but, for any other element,
+ * focus will have to be on, and returned to, this element if the keys are to
+ * function.
+ * See also {@link ol.interaction.KeyboardZoom}.
+ *
+ * @constructor
+ * @extends {ol.interaction.Interaction}
+ * @param {olx.interaction.KeyboardPanOptions=} opt_options Options.
+ * @api stable
+ */
+ol.interaction.KeyboardPan = function(opt_options) {
+
+  ol.interaction.Interaction.call(this, {
+    handleEvent: ol.interaction.KeyboardPan.handleEvent
+  });
+
+  var options = opt_options || {};
+
+  /**
+   * @private
+   * @param {ol.MapBrowserEvent} mapBrowserEvent Browser event.
+   * @return {boolean} Combined condition result.
+   */
+  this.defaultCondition_ = function(mapBrowserEvent) {
+    return ol.events.condition.noModifierKeys(mapBrowserEvent) &&
+      ol.events.condition.targetNotEditable(mapBrowserEvent);
+  };
+
+  /**
+   * @private
+   * @type {ol.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;
+
+};
+ol.inherits(ol.interaction.KeyboardPan, ol.interaction.Interaction);
+
+/**
+ * Handles the {@link ol.MapBrowserEvent map browser event} if it was a
+ * `KeyEvent`, and decides the direction to pan to (if an arrow key was
+ * pressed).
+ * @param {ol.MapBrowserEvent} mapBrowserEvent Map browser event.
+ * @return {boolean} `false` to stop event propagation.
+ * @this {ol.interaction.KeyboardPan}
+ * @api
+ */
+ol.interaction.KeyboardPan.handleEvent = function(mapBrowserEvent) {
+  var stopEvent = false;
+  if (mapBrowserEvent.type == ol.events.EventType.KEYDOWN) {
+    var keyEvent = mapBrowserEvent.originalEvent;
+    var keyCode = keyEvent.keyCode;
+    if (this.condition_(mapBrowserEvent) &&
+        (keyCode == ol.events.KeyCode.DOWN ||
+        keyCode == ol.events.KeyCode.LEFT ||
+        keyCode == ol.events.KeyCode.RIGHT ||
+        keyCode == ol.events.KeyCode.UP)) {
+      var map = mapBrowserEvent.map;
+      var view = map.getView();
+      goog.asserts.assert(view, 'map must have view');
+      var mapUnitsDelta = view.getResolution() * this.pixelDelta_;
+      var deltaX = 0, deltaY = 0;
+      if (keyCode == ol.events.KeyCode.DOWN) {
+        deltaY = -mapUnitsDelta;
+      } else if (keyCode == ol.events.KeyCode.LEFT) {
+        deltaX = -mapUnitsDelta;
+      } else if (keyCode == ol.events.KeyCode.RIGHT) {
+        deltaX = mapUnitsDelta;
+      } else {
+        deltaY = mapUnitsDelta;
+      }
+      var delta = [deltaX, deltaY];
+      ol.coordinate.rotate(delta, view.getRotation());
+      ol.interaction.Interaction.pan(map, view, delta, this.duration_);
+      mapBrowserEvent.preventDefault();
+      stopEvent = true;
+    }
+  }
+  return !stopEvent;
+};
+
+goog.provide('ol.interaction.KeyboardZoom');
+
+goog.require('goog.asserts');
+goog.require('ol.events.EventType');
+goog.require('ol.events.condition');
+goog.require('ol.interaction.Interaction');
+
+
+/**
+ * @classdesc
+ * Allows the user to zoom the map using keyboard + and -.
+ * Note that, although this interaction is by default included in maps,
+ * the keys can only be used when browser focus is on the element to which
+ * the keyboard events are attached. By default, this is the map div,
+ * though you can change this with the `keyboardEventTarget` in
+ * {@link ol.Map}. `document` never loses focus but, for any other element,
+ * focus will have to be on, and returned to, this element if the keys are to
+ * function.
+ * See also {@link ol.interaction.KeyboardPan}.
+ *
+ * @constructor
+ * @param {olx.interaction.KeyboardZoomOptions=} opt_options Options.
+ * @extends {ol.interaction.Interaction}
+ * @api stable
+ */
+ol.interaction.KeyboardZoom = function(opt_options) {
+
+  ol.interaction.Interaction.call(this, {
+    handleEvent: ol.interaction.KeyboardZoom.handleEvent
+  });
+
+  var options = opt_options ? opt_options : {};
+
+  /**
+   * @private
+   * @type {ol.EventsConditionType}
+   */
+  this.condition_ = options.condition ? options.condition :
+          ol.events.condition.targetNotEditable;
+
+  /**
+   * @private
+   * @type {number}
+   */
+  this.delta_ = options.delta ? options.delta : 1;
+
+  /**
+   * @private
+   * @type {number}
+   */
+  this.duration_ = options.duration !== undefined ? options.duration : 100;
+
+};
+ol.inherits(ol.interaction.KeyboardZoom, ol.interaction.Interaction);
+
+
+/**
+ * Handles the {@link ol.MapBrowserEvent map browser event} if it was a
+ * `KeyEvent`, and decides whether to zoom in or out (depending on whether the
+ * key pressed was '+' or '-').
+ * @param {ol.MapBrowserEvent} mapBrowserEvent Map browser event.
+ * @return {boolean} `false` to stop event propagation.
+ * @this {ol.interaction.KeyboardZoom}
+ * @api
+ */
+ol.interaction.KeyboardZoom.handleEvent = function(mapBrowserEvent) {
+  var stopEvent = false;
+  if (mapBrowserEvent.type == ol.events.EventType.KEYDOWN ||
+      mapBrowserEvent.type == ol.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_;
+      map.render();
+      var view = map.getView();
+      goog.asserts.assert(view, 'map must have view');
+      ol.interaction.Interaction.zoomByDelta(
+          map, view, delta, undefined, this.duration_);
+      mapBrowserEvent.preventDefault();
+      stopEvent = true;
+    }
+  }
+  return !stopEvent;
+};
+
+goog.provide('ol.interaction.MouseWheelZoom');
+
+goog.require('goog.asserts');
+goog.require('ol');
+goog.require('ol.events.EventType');
+goog.require('ol.interaction.Interaction');
+goog.require('ol.math');
+
+
+/**
+ * @classdesc
+ * Allows the user to zoom the map by scrolling the mouse wheel.
+ *
+ * @constructor
+ * @extends {ol.interaction.Interaction}
+ * @param {olx.interaction.MouseWheelZoomOptions=} opt_options Options.
+ * @api stable
+ */
+ol.interaction.MouseWheelZoom = function(opt_options) {
+
+  ol.interaction.Interaction.call(this, {
+    handleEvent: ol.interaction.MouseWheelZoom.handleEvent
+  });
+
+  var options = opt_options || {};
+
+  /**
+   * @private
+   * @type {number}
+   */
+  this.delta_ = 0;
+
+  /**
+   * @private
+   * @type {number}
+   */
+  this.duration_ = options.duration !== undefined ? options.duration : 250;
+
+  /**
+   * @private
+   * @type {boolean}
+   */
+  this.useAnchor_ = options.useAnchor !== undefined ? options.useAnchor : true;
+
+  /**
+   * @private
+   * @type {?ol.Coordinate}
+   */
+  this.lastAnchor_ = null;
+
+  /**
+   * @private
+   * @type {number|undefined}
+   */
+  this.startTime_ = undefined;
+
+  /**
+   * @private
+   * @type {number|undefined}
+   */
+  this.timeoutId_ = undefined;
+
+};
+ol.inherits(ol.interaction.MouseWheelZoom, ol.interaction.Interaction);
+
+
+/**
+ * Handles the {@link ol.MapBrowserEvent map browser event} (if it was a
+ * mousewheel-event) and eventually zooms the map.
+ * @param {ol.MapBrowserEvent} mapBrowserEvent Map browser event.
+ * @return {boolean} `false` to stop event propagation.
+ * @this {ol.interaction.MouseWheelZoom}
+ * @api
+ */
+ol.interaction.MouseWheelZoom.handleEvent = function(mapBrowserEvent) {
+  var stopEvent = false;
+  if (mapBrowserEvent.type == ol.events.EventType.WHEEL ||
+      mapBrowserEvent.type == ol.events.EventType.MOUSEWHEEL) {
+    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/scroll_zoom.js
+    //TODO There's more good stuff in there for inspiration to improve this interaction.
+    var delta;
+    if (mapBrowserEvent.type == ol.events.EventType.WHEEL) {
+      delta = wheelEvent.deltaY;
+      if (ol.has.FIREFOX &&
+          wheelEvent.deltaMode === ol.global.WheelEvent.DOM_DELTA_PIXEL) {
+        delta /= ol.has.DEVICE_PIXEL_RATIO;
+      }
+      if (wheelEvent.deltaMode === ol.global.WheelEvent.DOM_DELTA_LINE) {
+        delta *= 40;
+      }
+    } else if (mapBrowserEvent.type == ol.events.EventType.MOUSEWHEEL) {
+      delta = -wheelEvent.wheelDeltaY;
+      if (ol.has.SAFARI) {
+        delta /= 3;
+      }
+    }
+
+    this.delta_ += delta;
+
+    if (this.startTime_ === undefined) {
+      this.startTime_ = Date.now();
+    }
+
+    var duration = ol.MOUSEWHEELZOOM_TIMEOUT_DURATION;
+    var timeLeft = Math.max(duration - (Date.now() - this.startTime_), 0);
+
+    ol.global.clearTimeout(this.timeoutId_);
+    this.timeoutId_ = ol.global.setTimeout(
+        this.doZoom_.bind(this, map), timeLeft);
+
+    mapBrowserEvent.preventDefault();
+    stopEvent = true;
+  }
+  return !stopEvent;
+};
+
+
+/**
+ * @private
+ * @param {ol.Map} map Map.
+ */
+ol.interaction.MouseWheelZoom.prototype.doZoom_ = function(map) {
+  var maxDelta = ol.MOUSEWHEELZOOM_MAXDELTA;
+  var delta = ol.math.clamp(this.delta_, -maxDelta, maxDelta);
+
+  var view = map.getView();
+  goog.asserts.assert(view, 'map must have view');
+
+  map.render();
+  ol.interaction.Interaction.zoomByDelta(map, view, -delta, this.lastAnchor_,
+      this.duration_);
+
+  this.delta_ = 0;
+  this.lastAnchor_ = null;
+  this.startTime_ = undefined;
+  this.timeoutId_ = undefined;
+};
+
+
+/**
+ * Enable or disable using the mouse's location as an anchor when zooming
+ * @param {boolean} useAnchor true to zoom to the mouse's location, false
+ * to zoom to the center of the map
+ * @api
+ */
+ol.interaction.MouseWheelZoom.prototype.setMouseAnchor = function(useAnchor) {
+  this.useAnchor_ = useAnchor;
+  if (!useAnchor) {
+    this.lastAnchor_ = null;
+  }
+};
+
+goog.provide('ol.interaction.PinchRotate');
+
+goog.require('goog.asserts');
+goog.require('ol');
+goog.require('ol.functions');
+goog.require('ol.ViewHint');
+goog.require('ol.interaction.Interaction');
+goog.require('ol.interaction.Pointer');
+
+
+/**
+ * @classdesc
+ * Allows the user to rotate the map by twisting with two fingers
+ * on a touch screen.
+ *
+ * @constructor
+ * @extends {ol.interaction.Pointer}
+ * @param {olx.interaction.PinchRotateOptions=} opt_options Options.
+ * @api stable
+ */
+ol.interaction.PinchRotate = function(opt_options) {
+
+  ol.interaction.Pointer.call(this, {
+    handleDownEvent: ol.interaction.PinchRotate.handleDownEvent_,
+    handleDragEvent: ol.interaction.PinchRotate.handleDragEvent_,
+    handleUpEvent: ol.interaction.PinchRotate.handleUpEvent_
+  });
+
+  var options = opt_options || {};
+
+  /**
+   * @private
+   * @type {ol.Coordinate}
+   */
+  this.anchor_ = null;
+
+  /**
+   * @private
+   * @type {number|undefined}
+   */
+  this.lastAngle_ = undefined;
+
+  /**
+   * @private
+   * @type {boolean}
+   */
+  this.rotating_ = false;
+
+  /**
+   * @private
+   * @type {number}
+   */
+  this.rotationDelta_ = 0.0;
+
+  /**
+   * @private
+   * @type {number}
+   */
+  this.threshold_ = options.threshold !== undefined ? options.threshold : 0.3;
+
+  /**
+   * @private
+   * @type {number}
+   */
+  this.duration_ = options.duration !== undefined ? options.duration : 250;
+
+};
+ol.inherits(ol.interaction.PinchRotate, ol.interaction.Pointer);
+
+
+/**
+ * @param {ol.MapBrowserPointerEvent} mapBrowserEvent Event.
+ * @this {ol.interaction.PinchRotate}
+ * @private
+ */
+ol.interaction.PinchRotate.handleDragEvent_ = function(mapBrowserEvent) {
+  goog.asserts.assert(this.targetPointers.length >= 2,
+      'length of this.targetPointers should be greater than or equal to 2');
+  var rotationDelta = 0.0;
+
+  var touch0 = this.targetPointers[0];
+  var touch1 = this.targetPointers[1];
+
+  // angle between touches
+  var angle = Math.atan2(
+      touch1.clientY - touch0.clientY,
+      touch1.clientX - touch0.clientX);
+
+  if (this.lastAngle_ !== undefined) {
+    var delta = angle - this.lastAngle_;
+    this.rotationDelta_ += delta;
+    if (!this.rotating_ &&
+        Math.abs(this.rotationDelta_) > this.threshold_) {
+      this.rotating_ = true;
+    }
+    rotationDelta = delta;
+  }
+  this.lastAngle_ = angle;
+
+  var map = mapBrowserEvent.map;
+
+  // rotate anchor point.
+  // FIXME: should be the intersection point between the lines:
+  //     touch0,touch1 and previousTouch0,previousTouch1
+  var viewportPosition = map.getViewport().getBoundingClientRect();
+  var centroid = ol.interaction.Pointer.centroid(this.targetPointers);
+  centroid[0] -= viewportPosition.left;
+  centroid[1] -= viewportPosition.top;
+  this.anchor_ = map.getCoordinateFromPixel(centroid);
+
+  // rotate
+  if (this.rotating_) {
+    var view = map.getView();
+    var rotation = view.getRotation();
+    map.render();
+    ol.interaction.Interaction.rotateWithoutConstraints(map, view,
+        rotation + rotationDelta, this.anchor_);
+  }
+};
+
+
+/**
+ * @param {ol.MapBrowserPointerEvent} mapBrowserEvent Event.
+ * @return {boolean} Stop drag sequence?
+ * @this {ol.interaction.PinchRotate}
+ * @private
+ */
+ol.interaction.PinchRotate.handleUpEvent_ = function(mapBrowserEvent) {
+  if (this.targetPointers.length < 2) {
+    var map = mapBrowserEvent.map;
+    var view = map.getView();
+    view.setHint(ol.ViewHint.INTERACTING, -1);
+    if (this.rotating_) {
+      var rotation = view.getRotation();
+      ol.interaction.Interaction.rotate(
+          map, view, rotation, this.anchor_, this.duration_);
+    }
+    return false;
+  } else {
+    return true;
+  }
+};
+
+
+/**
+ * @param {ol.MapBrowserPointerEvent} mapBrowserEvent Event.
+ * @return {boolean} Start drag sequence?
+ * @this {ol.interaction.PinchRotate}
+ * @private
+ */
+ol.interaction.PinchRotate.handleDownEvent_ = function(mapBrowserEvent) {
+  if (this.targetPointers.length >= 2) {
+    var map = mapBrowserEvent.map;
+    this.anchor_ = null;
+    this.lastAngle_ = undefined;
+    this.rotating_ = false;
+    this.rotationDelta_ = 0.0;
+    if (!this.handlingDownUpSequence) {
+      map.getView().setHint(ol.ViewHint.INTERACTING, 1);
+    }
+    map.render();
+    return true;
+  } else {
+    return false;
+  }
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.interaction.PinchRotate.prototype.shouldStopEvent = ol.functions.FALSE;
+
+goog.provide('ol.interaction.PinchZoom');
+
+goog.require('goog.asserts');
+goog.require('ol');
+goog.require('ol.functions');
+goog.require('ol.ViewHint');
+goog.require('ol.interaction.Interaction');
+goog.require('ol.interaction.Pointer');
+
+
+/**
+ * @classdesc
+ * Allows the user to zoom the map by pinching with two fingers
+ * on a touch screen.
+ *
+ * @constructor
+ * @extends {ol.interaction.Pointer}
+ * @param {olx.interaction.PinchZoomOptions=} opt_options Options.
+ * @api stable
+ */
+ol.interaction.PinchZoom = function(opt_options) {
+
+  ol.interaction.Pointer.call(this, {
+    handleDownEvent: ol.interaction.PinchZoom.handleDownEvent_,
+    handleDragEvent: ol.interaction.PinchZoom.handleDragEvent_,
+    handleUpEvent: ol.interaction.PinchZoom.handleUpEvent_
+  });
+
+  var options = opt_options ? opt_options : {};
+
+  /**
+   * @private
+   * @type {ol.Coordinate}
+   */
+  this.anchor_ = null;
+
+  /**
+   * @private
+   * @type {number}
+   */
+  this.duration_ = options.duration !== undefined ? options.duration : 400;
+
+  /**
+   * @private
+   * @type {number|undefined}
+   */
+  this.lastDistance_ = undefined;
+
+  /**
+   * @private
+   * @type {number}
+   */
+  this.lastScaleDelta_ = 1;
+
+};
+ol.inherits(ol.interaction.PinchZoom, ol.interaction.Pointer);
+
+
+/**
+ * @param {ol.MapBrowserPointerEvent} mapBrowserEvent Event.
+ * @this {ol.interaction.PinchZoom}
+ * @private
+ */
+ol.interaction.PinchZoom.handleDragEvent_ = function(mapBrowserEvent) {
+  goog.asserts.assert(this.targetPointers.length >= 2,
+      'length of this.targetPointers should be 2 or more');
+  var scaleDelta = 1.0;
+
+  var touch0 = this.targetPointers[0];
+  var touch1 = this.targetPointers[1];
+  var dx = touch0.clientX - touch1.clientX;
+  var dy = touch0.clientY - touch1.clientY;
+
+  // distance between touches
+  var distance = Math.sqrt(dx * dx + dy * dy);
+
+  if (this.lastDistance_ !== undefined) {
+    scaleDelta = this.lastDistance_ / distance;
+  }
+  this.lastDistance_ = distance;
+  if (scaleDelta != 1.0) {
+    this.lastScaleDelta_ = scaleDelta;
+  }
+
+  var map = mapBrowserEvent.map;
+  var view = map.getView();
+  var resolution = view.getResolution();
+
+  // scale anchor point.
+  var viewportPosition = map.getViewport().getBoundingClientRect();
+  var centroid = ol.interaction.Pointer.centroid(this.targetPointers);
+  centroid[0] -= viewportPosition.left;
+  centroid[1] -= viewportPosition.top;
+  this.anchor_ = map.getCoordinateFromPixel(centroid);
+
+  // scale, bypass the resolution constraint
+  map.render();
+  ol.interaction.Interaction.zoomWithoutConstraints(
+      map, view, resolution * scaleDelta, this.anchor_);
+
+};
+
+
+/**
+ * @param {ol.MapBrowserPointerEvent} mapBrowserEvent Event.
+ * @return {boolean} Stop drag sequence?
+ * @this {ol.interaction.PinchZoom}
+ * @private
+ */
+ol.interaction.PinchZoom.handleUpEvent_ = function(mapBrowserEvent) {
+  if (this.targetPointers.length < 2) {
+    var map = mapBrowserEvent.map;
+    var view = map.getView();
+    view.setHint(ol.ViewHint.INTERACTING, -1);
+    var resolution = view.getResolution();
+    // Zoom to final resolution, with an animation, and provide a
+    // direction not to zoom out/in if user was pinching in/out.
+    // Direction is > 0 if pinching out, and < 0 if pinching in.
+    var direction = this.lastScaleDelta_ - 1;
+    ol.interaction.Interaction.zoom(map, view, resolution,
+        this.anchor_, this.duration_, direction);
+    return false;
+  } else {
+    return true;
+  }
+};
+
+
+/**
+ * @param {ol.MapBrowserPointerEvent} mapBrowserEvent Event.
+ * @return {boolean} Start drag sequence?
+ * @this {ol.interaction.PinchZoom}
+ * @private
+ */
+ol.interaction.PinchZoom.handleDownEvent_ = function(mapBrowserEvent) {
+  if (this.targetPointers.length >= 2) {
+    var map = mapBrowserEvent.map;
+    this.anchor_ = null;
+    this.lastDistance_ = undefined;
+    this.lastScaleDelta_ = 1;
+    if (!this.handlingDownUpSequence) {
+      map.getView().setHint(ol.ViewHint.INTERACTING, 1);
+    }
+    map.render();
+    return true;
+  } else {
+    return false;
+  }
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.interaction.PinchZoom.prototype.shouldStopEvent = ol.functions.FALSE;
+
+goog.provide('ol.interaction');
+
+goog.require('ol');
+goog.require('ol.Collection');
+goog.require('ol.Kinetic');
+goog.require('ol.interaction.DoubleClickZoom');
+goog.require('ol.interaction.DragPan');
+goog.require('ol.interaction.DragRotate');
+goog.require('ol.interaction.DragZoom');
+goog.require('ol.interaction.KeyboardPan');
+goog.require('ol.interaction.KeyboardZoom');
+goog.require('ol.interaction.MouseWheelZoom');
+goog.require('ol.interaction.PinchRotate');
+goog.require('ol.interaction.PinchZoom');
+
+
+/**
+ * Set of interactions included in maps by default. Specific interactions can be
+ * excluded by setting the appropriate option to false in the constructor
+ * options, but the order of the interactions is fixed.  If you want to specify
+ * a different order for interactions, you will need to create your own
+ * {@link ol.interaction.Interaction} instances and insert them into a
+ * {@link ol.Collection} in the order you want before creating your
+ * {@link ol.Map} instance. The default set of interactions, in sequence, is:
+ * * {@link ol.interaction.DragRotate}
+ * * {@link ol.interaction.DoubleClickZoom}
+ * * {@link ol.interaction.DragPan}
+ * * {@link ol.interaction.PinchRotate}
+ * * {@link ol.interaction.PinchZoom}
+ * * {@link ol.interaction.KeyboardPan}
+ * * {@link ol.interaction.KeyboardZoom}
+ * * {@link ol.interaction.MouseWheelZoom}
+ * * {@link ol.interaction.DragZoom}
+ *
+ * @param {olx.interaction.DefaultsOptions=} opt_options Defaults options.
+ * @return {ol.Collection.<ol.interaction.Interaction>} A collection of
+ * interactions to be used with the ol.Map constructor's interactions option.
+ * @api stable
+ */
+ol.interaction.defaults = function(opt_options) {
+
+  var options = opt_options ? opt_options : {};
+
+  var interactions = new ol.Collection();
+
+  var kinetic = new ol.Kinetic(-0.005, 0.05, 100);
+
+  var altShiftDragRotate = options.altShiftDragRotate !== undefined ?
+      options.altShiftDragRotate : true;
+  if (altShiftDragRotate) {
+    interactions.push(new ol.interaction.DragRotate());
+  }
+
+  var doubleClickZoom = options.doubleClickZoom !== undefined ?
+      options.doubleClickZoom : true;
+  if (doubleClickZoom) {
+    interactions.push(new ol.interaction.DoubleClickZoom({
+      delta: options.zoomDelta,
+      duration: options.zoomDuration
+    }));
+  }
+
+  var dragPan = options.dragPan !== undefined ? options.dragPan : true;
+  if (dragPan) {
+    interactions.push(new ol.interaction.DragPan({
+      kinetic: kinetic
+    }));
+  }
+
+  var pinchRotate = options.pinchRotate !== undefined ? options.pinchRotate :
+      true;
+  if (pinchRotate) {
+    interactions.push(new ol.interaction.PinchRotate());
+  }
+
+  var pinchZoom = options.pinchZoom !== undefined ? options.pinchZoom : true;
+  if (pinchZoom) {
+    interactions.push(new ol.interaction.PinchZoom({
+      duration: options.zoomDuration
+    }));
+  }
+
+  var keyboard = options.keyboard !== undefined ? options.keyboard : true;
+  if (keyboard) {
+    interactions.push(new ol.interaction.KeyboardPan());
+    interactions.push(new ol.interaction.KeyboardZoom({
+      delta: options.zoomDelta,
+      duration: options.zoomDuration
+    }));
+  }
+
+  var mouseWheelZoom = options.mouseWheelZoom !== undefined ?
+      options.mouseWheelZoom : true;
+  if (mouseWheelZoom) {
+    interactions.push(new ol.interaction.MouseWheelZoom({
+      duration: options.zoomDuration
+    }));
+  }
+
+  var shiftDragZoom = options.shiftDragZoom !== undefined ?
+      options.shiftDragZoom : true;
+  if (shiftDragZoom) {
+    interactions.push(new ol.interaction.DragZoom({
+      duration: options.zoomDuration
+    }));
+  }
+
+  return interactions;
+
+};
+
+goog.provide('ol.layer.Group');
+
+goog.require('goog.asserts');
+goog.require('ol.Collection');
+goog.require('ol.CollectionEvent');
+goog.require('ol.CollectionEventType');
+goog.require('ol.Object');
+goog.require('ol.ObjectEventType');
+goog.require('ol.events');
+goog.require('ol.events.EventType');
+goog.require('ol.extent');
+goog.require('ol.layer.Base');
+goog.require('ol.object');
+goog.require('ol.source.State');
+
+
+/**
+ * @enum {string}
+ */
+ol.layer.GroupProperty = {
+  LAYERS: 'layers'
+};
+
+
+/**
+ * @classdesc
+ * A {@link ol.Collection} of layers that are handled together.
+ *
+ * A generic `change` event is triggered when the group/Collection changes.
+ *
+ * @constructor
+ * @extends {ol.layer.Base}
+ * @param {olx.layer.GroupOptions=} opt_options Layer options.
+ * @api stable
+ */
+ol.layer.Group = function(opt_options) {
+
+  var options = opt_options || {};
+  var baseOptions = /** @type {olx.layer.GroupOptions} */
+      (ol.object.assign({}, options));
+  delete baseOptions.layers;
+
+  var layers = options.layers;
+
+  ol.layer.Base.call(this, baseOptions);
+
+  /**
+   * @private
+   * @type {Array.<ol.EventsKey>}
+   */
+  this.layersListenerKeys_ = [];
+
+  /**
+   * @private
+   * @type {Object.<string, Array.<ol.EventsKey>>}
+   */
+  this.listenerKeys_ = {};
+
+  ol.events.listen(this,
+      ol.Object.getChangeEventType(ol.layer.GroupProperty.LAYERS),
+      this.handleLayersChanged_, this);
+
+  if (layers) {
+    if (Array.isArray(layers)) {
+      layers = new ol.Collection(layers.slice());
+    } else {
+      goog.asserts.assertInstanceof(layers, ol.Collection,
+          'layers should be an ol.Collection');
+      layers = layers;
+    }
+  } else {
+    layers = new ol.Collection();
+  }
+
+  this.setLayers(layers);
+
+};
+ol.inherits(ol.layer.Group, ol.layer.Base);
+
+
+/**
+ * @private
+ */
+ol.layer.Group.prototype.handleLayerChange_ = function() {
+  if (this.getVisible()) {
+    this.changed();
+  }
+};
+
+
+/**
+ * @param {ol.events.Event} event Event.
+ * @private
+ */
+ol.layer.Group.prototype.handleLayersChanged_ = function(event) {
+  this.layersListenerKeys_.forEach(ol.events.unlistenByKey);
+  this.layersListenerKeys_.length = 0;
+
+  var layers = this.getLayers();
+  this.layersListenerKeys_.push(
+      ol.events.listen(layers, ol.CollectionEventType.ADD,
+          this.handleLayersAdd_, this),
+      ol.events.listen(layers, ol.CollectionEventType.REMOVE,
+          this.handleLayersRemove_, this));
+
+  for (var id in this.listenerKeys_) {
+    this.listenerKeys_[id].forEach(ol.events.unlistenByKey);
+  }
+  ol.object.clear(this.listenerKeys_);
+
+  var layersArray = layers.getArray();
+  var i, ii, layer;
+  for (i = 0, ii = layersArray.length; i < ii; i++) {
+    layer = layersArray[i];
+    this.listenerKeys_[goog.getUid(layer).toString()] = [
+      ol.events.listen(layer, ol.ObjectEventType.PROPERTYCHANGE,
+          this.handleLayerChange_, this),
+      ol.events.listen(layer, ol.events.EventType.CHANGE,
+          this.handleLayerChange_, this)
+    ];
+  }
+
+  this.changed();
+};
+
+
+/**
+ * @param {ol.CollectionEvent} collectionEvent Collection event.
+ * @private
+ */
+ol.layer.Group.prototype.handleLayersAdd_ = function(collectionEvent) {
+  var layer = /** @type {ol.layer.Base} */ (collectionEvent.element);
+  var key = goog.getUid(layer).toString();
+  goog.asserts.assert(!(key in this.listenerKeys_),
+      'listeners already registered');
+  this.listenerKeys_[key] = [
+    ol.events.listen(layer, ol.ObjectEventType.PROPERTYCHANGE,
+        this.handleLayerChange_, this),
+    ol.events.listen(layer, ol.events.EventType.CHANGE,
+        this.handleLayerChange_, this)
+  ];
+  this.changed();
+};
+
+
+/**
+ * @param {ol.CollectionEvent} collectionEvent Collection event.
+ * @private
+ */
+ol.layer.Group.prototype.handleLayersRemove_ = function(collectionEvent) {
+  var layer = /** @type {ol.layer.Base} */ (collectionEvent.element);
+  var key = goog.getUid(layer).toString();
+  goog.asserts.assert(key in this.listenerKeys_, 'no listeners to unregister');
+  this.listenerKeys_[key].forEach(ol.events.unlistenByKey);
+  delete this.listenerKeys_[key];
+  this.changed();
+};
+
+
+/**
+ * Returns the {@link ol.Collection collection} of {@link ol.layer.Layer layers}
+ * in this group.
+ * @return {!ol.Collection.<ol.layer.Base>} Collection of
+ *   {@link ol.layer.Base layers} that are part of this group.
+ * @observable
+ * @api stable
+ */
+ol.layer.Group.prototype.getLayers = function() {
+  return /** @type {!ol.Collection.<ol.layer.Base>} */ (this.get(
+      ol.layer.GroupProperty.LAYERS));
+};
+
+
+/**
+ * Set the {@link ol.Collection collection} of {@link ol.layer.Layer layers}
+ * in this group.
+ * @param {!ol.Collection.<ol.layer.Base>} layers Collection of
+ *   {@link ol.layer.Base layers} that are part of this group.
+ * @observable
+ * @api stable
+ */
+ol.layer.Group.prototype.setLayers = function(layers) {
+  this.set(ol.layer.GroupProperty.LAYERS, layers);
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.layer.Group.prototype.getLayersArray = function(opt_array) {
+  var array = opt_array !== undefined ? opt_array : [];
+  this.getLayers().forEach(function(layer) {
+    layer.getLayersArray(array);
+  });
+  return array;
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.layer.Group.prototype.getLayerStatesArray = function(opt_states) {
+  var states = opt_states !== undefined ? opt_states : [];
+
+  var pos = states.length;
+
+  this.getLayers().forEach(function(layer) {
+    layer.getLayerStatesArray(states);
+  });
+
+  var ownLayerState = this.getLayerState();
+  var i, ii, layerState;
+  for (i = pos, ii = states.length; i < ii; i++) {
+    layerState = states[i];
+    layerState.opacity *= ownLayerState.opacity;
+    layerState.visible = layerState.visible && ownLayerState.visible;
+    layerState.maxResolution = Math.min(
+        layerState.maxResolution, ownLayerState.maxResolution);
+    layerState.minResolution = Math.max(
+        layerState.minResolution, ownLayerState.minResolution);
+    if (ownLayerState.extent !== undefined) {
+      if (layerState.extent !== undefined) {
+        layerState.extent = ol.extent.getIntersection(
+            layerState.extent, ownLayerState.extent);
+      } else {
+        layerState.extent = ownLayerState.extent;
+      }
+    }
+  }
+
+  return states;
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.layer.Group.prototype.getSourceState = function() {
+  return ol.source.State.READY;
+};
+
+goog.provide('ol.proj.EPSG3857');
+
+goog.require('goog.asserts');
+goog.require('ol.math');
+goog.require('ol.proj');
+goog.require('ol.proj.Projection');
+goog.require('ol.proj.Units');
+
+
+/**
+ * @classdesc
+ * Projection object for web/spherical Mercator (EPSG:3857).
+ *
+ * @constructor
+ * @extends {ol.proj.Projection}
+ * @param {string} code Code.
+ * @private
+ */
+ol.proj.EPSG3857_ = function(code) {
+  ol.proj.Projection.call(this, {
+    code: code,
+    units: ol.proj.Units.METERS,
+    extent: ol.proj.EPSG3857.EXTENT,
+    global: true,
+    worldExtent: ol.proj.EPSG3857.WORLD_EXTENT
+  });
+};
+ol.inherits(ol.proj.EPSG3857_, ol.proj.Projection);
+
+
+/**
+ * @inheritDoc
+ */
+ol.proj.EPSG3857_.prototype.getPointResolution = function(resolution, point) {
+  return resolution / ol.math.cosh(point[1] / ol.proj.EPSG3857.RADIUS);
+};
+
+
+/**
+ * @const
+ * @type {number}
+ */
+ol.proj.EPSG3857.RADIUS = 6378137;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+ol.proj.EPSG3857.HALF_SIZE = Math.PI * ol.proj.EPSG3857.RADIUS;
+
+
+/**
+ * @const
+ * @type {ol.Extent}
+ */
+ol.proj.EPSG3857.EXTENT = [
+  -ol.proj.EPSG3857.HALF_SIZE, -ol.proj.EPSG3857.HALF_SIZE,
+  ol.proj.EPSG3857.HALF_SIZE, ol.proj.EPSG3857.HALF_SIZE
+];
+
+
+/**
+ * @const
+ * @type {ol.Extent}
+ */
+ol.proj.EPSG3857.WORLD_EXTENT = [-180, -85, 180, 85];
+
+
+/**
+ * Lists several projection codes with the same meaning as EPSG:3857.
+ *
+ * @type {Array.<string>}
+ */
+ol.proj.EPSG3857.CODES = [
+  'EPSG:3857',
+  'EPSG:102100',
+  'EPSG:102113',
+  'EPSG:900913',
+  'urn:ogc:def:crs:EPSG:6.18:3:3857',
+  'urn:ogc:def:crs:EPSG::3857',
+  'http://www.opengis.net/gml/srs/epsg.xml#3857'
+];
+
+
+/**
+ * Projections equal to EPSG:3857.
+ *
+ * @const
+ * @type {Array.<ol.proj.Projection>}
+ */
+ol.proj.EPSG3857.PROJECTIONS = ol.proj.EPSG3857.CODES.map(function(code) {
+  return new ol.proj.EPSG3857_(code);
+});
+
+
+/**
+ * Transformation from EPSG:4326 to EPSG:3857.
+ *
+ * @param {Array.<number>} input Input array of coordinate values.
+ * @param {Array.<number>=} opt_output Output array of coordinate values.
+ * @param {number=} opt_dimension Dimension (default is `2`).
+ * @return {Array.<number>} Output array of coordinate values.
+ */
+ol.proj.EPSG3857.fromEPSG4326 = function(input, opt_output, opt_dimension) {
+  var length = input.length,
+      dimension = opt_dimension > 1 ? opt_dimension : 2,
+      output = opt_output;
+  if (output === undefined) {
+    if (dimension > 2) {
+      // preserve values beyond second dimension
+      output = input.slice();
+    } else {
+      output = new Array(length);
+    }
+  }
+  goog.asserts.assert(output.length % dimension === 0,
+      'modulus of output.length with dimension should be 0');
+  for (var i = 0; i < length; i += dimension) {
+    output[i] = ol.proj.EPSG3857.RADIUS * Math.PI * input[i] / 180;
+    output[i + 1] = ol.proj.EPSG3857.RADIUS *
+        Math.log(Math.tan(Math.PI * (input[i + 1] + 90) / 360));
+  }
+  return output;
+};
+
+
+/**
+ * Transformation from EPSG:3857 to EPSG:4326.
+ *
+ * @param {Array.<number>} input Input array of coordinate values.
+ * @param {Array.<number>=} opt_output Output array of coordinate values.
+ * @param {number=} opt_dimension Dimension (default is `2`).
+ * @return {Array.<number>} Output array of coordinate values.
+ */
+ol.proj.EPSG3857.toEPSG4326 = function(input, opt_output, opt_dimension) {
+  var length = input.length,
+      dimension = opt_dimension > 1 ? opt_dimension : 2,
+      output = opt_output;
+  if (output === undefined) {
+    if (dimension > 2) {
+      // preserve values beyond second dimension
+      output = input.slice();
+    } else {
+      output = new Array(length);
+    }
+  }
+  goog.asserts.assert(output.length % dimension === 0,
+      'modulus of output.length with dimension should be 0');
+  for (var i = 0; i < length; i += dimension) {
+    output[i] = 180 * input[i] / (ol.proj.EPSG3857.RADIUS * Math.PI);
+    output[i + 1] = 360 * Math.atan(
+        Math.exp(input[i + 1] / ol.proj.EPSG3857.RADIUS)) / Math.PI - 90;
+  }
+  return output;
+};
+
+goog.provide('ol.sphere.WGS84');
+
+goog.require('ol.Sphere');
+
+
+/**
+ * A sphere with radius equal to the semi-major axis of the WGS84 ellipsoid.
+ * @const
+ * @type {ol.Sphere}
+ */
+ol.sphere.WGS84 = new ol.Sphere(6378137);
+
+goog.provide('ol.proj.EPSG4326');
+
+goog.require('ol.proj');
+goog.require('ol.proj.Projection');
+goog.require('ol.proj.Units');
+goog.require('ol.sphere.WGS84');
+
+
+/**
+ * @classdesc
+ * Projection object for WGS84 geographic coordinates (EPSG:4326).
+ *
+ * Note that OpenLayers does not strictly comply with the EPSG definition.
+ * The EPSG registry defines 4326 as a CRS for Latitude,Longitude (y,x).
+ * OpenLayers treats EPSG:4326 as a pseudo-projection, with x,y coordinates.
+ *
+ * @constructor
+ * @extends {ol.proj.Projection}
+ * @param {string} code Code.
+ * @param {string=} opt_axisOrientation Axis orientation.
+ * @private
+ */
+ol.proj.EPSG4326_ = function(code, opt_axisOrientation) {
+  ol.proj.Projection.call(this, {
+    code: code,
+    units: ol.proj.Units.DEGREES,
+    extent: ol.proj.EPSG4326.EXTENT,
+    axisOrientation: opt_axisOrientation,
+    global: true,
+    metersPerUnit: ol.proj.EPSG4326.METERS_PER_UNIT,
+    worldExtent: ol.proj.EPSG4326.EXTENT
+  });
+};
+ol.inherits(ol.proj.EPSG4326_, ol.proj.Projection);
+
+
+/**
+ * @inheritDoc
+ */
+ol.proj.EPSG4326_.prototype.getPointResolution = function(resolution, point) {
+  return resolution;
+};
+
+
+/**
+ * Extent of the EPSG:4326 projection which is the whole world.
+ *
+ * @const
+ * @type {ol.Extent}
+ */
+ol.proj.EPSG4326.EXTENT = [-180, -90, 180, 90];
+
+
+/**
+ * @const
+ * @type {number}
+ */
+ol.proj.EPSG4326.METERS_PER_UNIT = Math.PI * ol.sphere.WGS84.radius / 180;
+
+
+/**
+ * Projections equal to EPSG:4326.
+ *
+ * @const
+ * @type {Array.<ol.proj.Projection>}
+ */
+ol.proj.EPSG4326.PROJECTIONS = [
+  new ol.proj.EPSG4326_('CRS:84'),
+  new ol.proj.EPSG4326_('EPSG:4326', 'neu'),
+  new ol.proj.EPSG4326_('urn:ogc:def:crs:EPSG::4326', 'neu'),
+  new ol.proj.EPSG4326_('urn:ogc:def:crs:EPSG:6.6:4326', 'neu'),
+  new ol.proj.EPSG4326_('urn:ogc:def:crs:OGC:1.3:CRS84'),
+  new ol.proj.EPSG4326_('urn:ogc:def:crs:OGC:2:84'),
+  new ol.proj.EPSG4326_('http://www.opengis.net/gml/srs/epsg.xml#4326', 'neu'),
+  new ol.proj.EPSG4326_('urn:x-ogc:def:crs:EPSG:4326', 'neu')
+];
+
+goog.provide('ol.proj.common');
+
+goog.require('ol.proj');
+goog.require('ol.proj.EPSG3857');
+goog.require('ol.proj.EPSG4326');
+
+
+/**
+ * FIXME empty description for jsdoc
+ * @api
+ */
+ol.proj.common.add = function() {
+  // Add transformations that don't alter coordinates to convert within set of
+  // projections with equal meaning.
+  ol.proj.addEquivalentProjections(ol.proj.EPSG3857.PROJECTIONS);
+  ol.proj.addEquivalentProjections(ol.proj.EPSG4326.PROJECTIONS);
+  // Add transformations to convert EPSG:4326 like coordinates to EPSG:3857 like
+  // coordinates and back.
+  ol.proj.addEquivalentTransforms(
+      ol.proj.EPSG4326.PROJECTIONS,
+      ol.proj.EPSG3857.PROJECTIONS,
+      ol.proj.EPSG3857.fromEPSG4326,
+      ol.proj.EPSG3857.toEPSG4326);
+};
+
+goog.provide('ol.layer.Image');
+
+goog.require('ol.layer.Layer');
+
+
+/**
+ * @classdesc
+ * Server-rendered images that are available for arbitrary extents and
+ * resolutions.
+ * Note that any property set in the options is set as a {@link ol.Object}
+ * property on the layer object; for example, setting `title: 'My Title'` in the
+ * options means that `title` is observable, and has get/set accessors.
+ *
+ * @constructor
+ * @extends {ol.layer.Layer}
+ * @fires ol.render.Event
+ * @param {olx.layer.ImageOptions=} opt_options Layer options.
+ * @api stable
+ */
+ol.layer.Image = function(opt_options) {
+  var options = opt_options ? opt_options : {};
+  ol.layer.Layer.call(this,  /** @type {olx.layer.LayerOptions} */ (options));
+};
+ol.inherits(ol.layer.Image, ol.layer.Layer);
+
+
+/**
+ * Return the associated {@link ol.source.Image source} of the image layer.
+ * @function
+ * @return {ol.source.Image} Source.
+ * @api stable
+ */
+ol.layer.Image.prototype.getSource;
+
+goog.provide('ol.layer.Tile');
+
+goog.require('ol');
+goog.require('ol.layer.Layer');
+goog.require('ol.object');
+
+
+/**
+ * @enum {string}
+ */
+ol.layer.TileProperty = {
+  PRELOAD: 'preload',
+  USE_INTERIM_TILES_ON_ERROR: 'useInterimTilesOnError'
+};
+
+
+/**
+ * @classdesc
+ * For layer sources that provide pre-rendered, tiled images in grids that are
+ * organized by zoom levels for specific resolutions.
+ * Note that any property set in the options is set as a {@link ol.Object}
+ * property on the layer object; for example, setting `title: 'My Title'` in the
+ * options means that `title` is observable, and has get/set accessors.
+ *
+ * @constructor
+ * @extends {ol.layer.Layer}
+ * @fires ol.render.Event
+ * @param {olx.layer.TileOptions=} opt_options Tile layer options.
+ * @api stable
+ */
+ol.layer.Tile = function(opt_options) {
+  var options = opt_options ? opt_options : {};
+
+  var baseOptions = ol.object.assign({}, options);
+
+  delete baseOptions.preload;
+  delete baseOptions.useInterimTilesOnError;
+  ol.layer.Layer.call(this,  /** @type {olx.layer.LayerOptions} */ (baseOptions));
+
+  this.setPreload(options.preload !== undefined ? options.preload : 0);
+  this.setUseInterimTilesOnError(options.useInterimTilesOnError !== undefined ?
+      options.useInterimTilesOnError : true);
+};
+ol.inherits(ol.layer.Tile, ol.layer.Layer);
+
+
+/**
+ * Return the level as number to which we will preload tiles up to.
+ * @return {number} The level to preload tiles up to.
+ * @observable
+ * @api
+ */
+ol.layer.Tile.prototype.getPreload = function() {
+  return /** @type {number} */ (this.get(ol.layer.TileProperty.PRELOAD));
+};
+
+
+/**
+ * Return the associated {@link ol.source.Tile tilesource} of the layer.
+ * @function
+ * @return {ol.source.Tile} Source.
+ * @api stable
+ */
+ol.layer.Tile.prototype.getSource;
+
+
+/**
+ * Set the level as number to which we will preload tiles up to.
+ * @param {number} preload The level to preload tiles up to.
+ * @observable
+ * @api
+ */
+ol.layer.Tile.prototype.setPreload = function(preload) {
+  this.set(ol.layer.TileProperty.PRELOAD, preload);
+};
+
+
+/**
+ * Whether we use interim tiles on error.
+ * @return {boolean} Use interim tiles on error.
+ * @observable
+ * @api
+ */
+ol.layer.Tile.prototype.getUseInterimTilesOnError = function() {
+  return /** @type {boolean} */ (
+      this.get(ol.layer.TileProperty.USE_INTERIM_TILES_ON_ERROR));
+};
+
+
+/**
+ * Set whether we use interim tiles on error.
+ * @param {boolean} useInterimTilesOnError Use interim tiles on error.
+ * @observable
+ * @api
+ */
+ol.layer.Tile.prototype.setUseInterimTilesOnError = function(useInterimTilesOnError) {
+  this.set(
+      ol.layer.TileProperty.USE_INTERIM_TILES_ON_ERROR, useInterimTilesOnError);
+};
+
+goog.provide('ol.render.canvas');
+
+
+/**
+ * @const
+ * @type {string}
+ */
+ol.render.canvas.defaultFont = '10px sans-serif';
+
+
+/**
+ * @const
+ * @type {ol.Color}
+ */
+ol.render.canvas.defaultFillStyle = [0, 0, 0, 1];
+
+
+/**
+ * @const
+ * @type {string}
+ */
+ol.render.canvas.defaultLineCap = 'round';
+
+
+/**
+ * @const
+ * @type {Array.<number>}
+ */
+ol.render.canvas.defaultLineDash = [];
+
+
+/**
+ * @const
+ * @type {string}
+ */
+ol.render.canvas.defaultLineJoin = 'round';
+
+
+/**
+ * @const
+ * @type {number}
+ */
+ol.render.canvas.defaultMiterLimit = 10;
+
+
+/**
+ * @const
+ * @type {ol.Color}
+ */
+ol.render.canvas.defaultStrokeStyle = [0, 0, 0, 1];
+
+
+/**
+ * @const
+ * @type {string}
+ */
+ol.render.canvas.defaultTextAlign = 'center';
+
+
+/**
+ * @const
+ * @type {string}
+ */
+ol.render.canvas.defaultTextBaseline = 'middle';
+
+
+/**
+ * @const
+ * @type {number}
+ */
+ol.render.canvas.defaultLineWidth = 1;
+
+
+/**
+ * @param {CanvasRenderingContext2D} context Context.
+ * @param {number} rotation Rotation.
+ * @param {number} offsetX X offset.
+ * @param {number} offsetY Y offset.
+ */
+ol.render.canvas.rotateAtOffset = function(context, rotation, offsetX, offsetY) {
+  if (rotation !== 0) {
+    context.translate(offsetX, offsetY);
+    context.rotate(rotation);
+    context.translate(-offsetX, -offsetY);
+  }
+};
+
+goog.provide('ol.style.Fill');
+
+goog.require('ol.color');
+
+
+/**
+ * @classdesc
+ * Set fill style for vector features.
+ *
+ * @constructor
+ * @param {olx.style.FillOptions=} opt_options Options.
+ * @api
+ */
+ol.style.Fill = function(opt_options) {
+
+  var options = opt_options || {};
+
+  /**
+   * @private
+   * @type {ol.Color|ol.ColorLike}
+   */
+  this.color_ = options.color !== undefined ? options.color : null;
+
+  /**
+   * @private
+   * @type {string|undefined}
+   */
+  this.checksum_ = undefined;
+};
+
+
+/**
+ * Get the fill color.
+ * @return {ol.Color|ol.ColorLike} Color.
+ * @api
+ */
+ol.style.Fill.prototype.getColor = function() {
+  return this.color_;
+};
+
+
+/**
+ * Set the color.
+ *
+ * @param {ol.Color|ol.ColorLike} color Color.
+ * @api
+ */
+ol.style.Fill.prototype.setColor = function(color) {
+  this.color_ = color;
+  this.checksum_ = undefined;
+};
+
+
+/**
+ * @return {string} The checksum.
+ */
+ol.style.Fill.prototype.getChecksum = function() {
+  if (this.checksum_ === undefined) {
+    if (
+        this.color_ instanceof CanvasPattern ||
+        this.color_ instanceof CanvasGradient
+    ) {
+      this.checksum_ = goog.getUid(this.color_).toString();
+    } else {
+      this.checksum_ = 'f' + (this.color_ ?
+          ol.color.asString(this.color_) : '-');
+    }
+  }
+
+  return this.checksum_;
+};
+
+// Copyright 2008 The Closure Library Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS-IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+/**
+ * @fileoverview Namespace with crypto related helper functions.
+ */
+
+goog.provide('goog.crypt');
+
+goog.require('goog.array');
+goog.require('goog.asserts');
+
+
+/**
+ * Turns a string into an array of bytes; a "byte" being a JS number in the
+ * range 0-255.
+ * @param {string} str String value to arrify.
+ * @return {!Array<number>} Array of numbers corresponding to the
+ *     UCS character codes of each character in str.
+ */
+goog.crypt.stringToByteArray = function(str) {
+  var output = [], p = 0;
+  for (var i = 0; i < str.length; i++) {
+    var c = str.charCodeAt(i);
+    while (c > 0xff) {
+      output[p++] = c & 0xff;
+      c >>= 8;
+    }
+    output[p++] = c;
+  }
+  return output;
+};
+
+
+/**
+ * Turns an array of numbers into the string given by the concatenation of the
+ * characters to which the numbers correspond.
+ * @param {!Uint8Array|!Array<number>} bytes Array of numbers representing
+ *     characters.
+ * @return {string} Stringification of the array.
+ */
+goog.crypt.byteArrayToString = function(bytes) {
+  var CHUNK_SIZE = 8192;
+
+  // Special-case the simple case for speed's sake.
+  if (bytes.length <= CHUNK_SIZE) {
+    return String.fromCharCode.apply(null, bytes);
+  }
+
+  // The remaining logic splits conversion by chunks since
+  // Function#apply() has a maximum parameter count.
+  // See discussion: http://goo.gl/LrWmZ9
+
+  var str = '';
+  for (var i = 0; i < bytes.length; i += CHUNK_SIZE) {
+    var chunk = goog.array.slice(bytes, i, i + CHUNK_SIZE);
+    str += String.fromCharCode.apply(null, chunk);
+  }
+  return str;
+};
+
+
+/**
+ * Turns an array of numbers into the hex string given by the concatenation of
+ * the hex values to which the numbers correspond.
+ * @param {Uint8Array|Array<number>} array Array of numbers representing
+ *     characters.
+ * @return {string} Hex string.
+ */
+goog.crypt.byteArrayToHex = function(array) {
+  return goog.array
+      .map(
+          array,
+          function(numByte) {
+            var hexByte = numByte.toString(16);
+            return hexByte.length > 1 ? hexByte : '0' + hexByte;
+          })
+      .join('');
+};
+
+
+/**
+ * Converts a hex string into an integer array.
+ * @param {string} hexString Hex string of 16-bit integers (two characters
+ *     per integer).
+ * @return {!Array<number>} Array of {0,255} integers for the given string.
+ */
+goog.crypt.hexToByteArray = function(hexString) {
+  goog.asserts.assert(
+      hexString.length % 2 == 0, 'Key string length must be multiple of 2');
+  var arr = [];
+  for (var i = 0; i < hexString.length; i += 2) {
+    arr.push(parseInt(hexString.substring(i, i + 2), 16));
+  }
+  return arr;
+};
+
+
+/**
+ * Converts a JS string to a UTF-8 "byte" array.
+ * @param {string} str 16-bit unicode string.
+ * @return {!Array<number>} UTF-8 byte array.
+ */
+goog.crypt.stringToUtf8ByteArray = function(str) {
+  // TODO(user): Use native implementations if/when available
+  var out = [], p = 0;
+  for (var i = 0; i < str.length; i++) {
+    var c = str.charCodeAt(i);
+    if (c < 128) {
+      out[p++] = c;
+    } else if (c < 2048) {
+      out[p++] = (c >> 6) | 192;
+      out[p++] = (c & 63) | 128;
+    } else if (
+        ((c & 0xFC00) == 0xD800) && (i + 1) < str.length &&
+        ((str.charCodeAt(i + 1) & 0xFC00) == 0xDC00)) {
+      // Surrogate Pair
+      c = 0x10000 + ((c & 0x03FF) << 10) + (str.charCodeAt(++i) & 0x03FF);
+      out[p++] = (c >> 18) | 240;
+      out[p++] = ((c >> 12) & 63) | 128;
+      out[p++] = ((c >> 6) & 63) | 128;
+      out[p++] = (c & 63) | 128;
+    } else {
+      out[p++] = (c >> 12) | 224;
+      out[p++] = ((c >> 6) & 63) | 128;
+      out[p++] = (c & 63) | 128;
+    }
+  }
+  return out;
+};
+
+
+/**
+ * Converts a UTF-8 byte array to JavaScript's 16-bit Unicode.
+ * @param {Uint8Array|Array<number>} bytes UTF-8 byte array.
+ * @return {string} 16-bit Unicode string.
+ */
+goog.crypt.utf8ByteArrayToString = function(bytes) {
+  // TODO(user): Use native implementations if/when available
+  var out = [], pos = 0, c = 0;
+  while (pos < bytes.length) {
+    var c1 = bytes[pos++];
+    if (c1 < 128) {
+      out[c++] = String.fromCharCode(c1);
+    } else if (c1 > 191 && c1 < 224) {
+      var c2 = bytes[pos++];
+      out[c++] = String.fromCharCode((c1 & 31) << 6 | c2 & 63);
+    } else if (c1 > 239 && c1 < 365) {
+      // Surrogate Pair
+      var c2 = bytes[pos++];
+      var c3 = bytes[pos++];
+      var c4 = bytes[pos++];
+      var u = ((c1 & 7) << 18 | (c2 & 63) << 12 | (c3 & 63) << 6 | c4 & 63) -
+          0x10000;
+      out[c++] = String.fromCharCode(0xD800 + (u >> 10));
+      out[c++] = String.fromCharCode(0xDC00 + (u & 1023));
+    } else {
+      var c2 = bytes[pos++];
+      var c3 = bytes[pos++];
+      out[c++] =
+          String.fromCharCode((c1 & 15) << 12 | (c2 & 63) << 6 | c3 & 63);
+    }
+  }
+  return out.join('');
+};
+
+
+/**
+ * XOR two byte arrays.
+ * @param {!Uint8Array|!Int8Array|!Array<number>} bytes1 Byte array 1.
+ * @param {!Uint8Array|!Int8Array|!Array<number>} bytes2 Byte array 2.
+ * @return {!Array<number>} Resulting XOR of the two byte arrays.
+ */
+goog.crypt.xorByteArray = function(bytes1, bytes2) {
+  goog.asserts.assert(
+      bytes1.length == bytes2.length, 'XOR array lengths must match');
+
+  var result = [];
+  for (var i = 0; i < bytes1.length; i++) {
+    result.push(bytes1[i] ^ bytes2[i]);
+  }
+  return result;
+};
+
+// Copyright 2011 The Closure Library Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS-IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+/**
+ * @fileoverview Abstract cryptographic hash interface.
+ *
+ * See goog.crypt.Sha1 and goog.crypt.Md5 for sample implementations.
+ *
+ */
+
+goog.provide('goog.crypt.Hash');
+
+
+
+/**
+ * Create a cryptographic hash instance.
+ *
+ * @constructor
+ * @struct
+ */
+goog.crypt.Hash = function() {
+  /**
+   * The block size for the hasher.
+   * @type {number}
+   */
+  this.blockSize = -1;
+};
+
+
+/**
+ * Resets the internal accumulator.
+ */
+goog.crypt.Hash.prototype.reset = goog.abstractMethod;
+
+
+/**
+ * Adds a byte array (array with values in [0-255] range) or a string (might
+ * only contain 8-bit, i.e., Latin1 characters) to the internal accumulator.
+ *
+ * Many hash functions operate on blocks of data and implement optimizations
+ * when a full chunk of data is readily available. Hence it is often preferable
+ * to provide large chunks of data (a kilobyte or more) than to repeatedly
+ * call the update method with few tens of bytes. If this is not possible, or
+ * not feasible, it might be good to provide data in multiplies of hash block
+ * size (often 64 bytes). Please see the implementation and performance tests
+ * of your favourite hash.
+ *
+ * @param {Array<number>|Uint8Array|string} bytes Data used for the update.
+ * @param {number=} opt_length Number of bytes to use.
+ */
+goog.crypt.Hash.prototype.update = goog.abstractMethod;
+
+
+/**
+ * @return {!Array<number>} The finalized hash computed
+ *     from the internal accumulator.
+ */
+goog.crypt.Hash.prototype.digest = goog.abstractMethod;
+
+// Copyright 2011 The Closure Library Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS-IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+/**
+ * @fileoverview MD5 cryptographic hash.
+ * Implementation of http://tools.ietf.org/html/rfc1321 with common
+ * optimizations and tweaks (see http://en.wikipedia.org/wiki/MD5).
+ *
+ * Usage:
+ *   var md5 = new goog.crypt.Md5();
+ *   md5.update(bytes);
+ *   var hash = md5.digest();
+ *
+ * Performance:
+ *   Chrome 23              ~680 Mbit/s
+ *   Chrome 13 (in a VM)    ~250 Mbit/s
+ *   Firefox 6.0 (in a VM)  ~100 Mbit/s
+ *   IE9 (in a VM)           ~27 Mbit/s
+ *   Firefox 3.6             ~15 Mbit/s
+ *   IE8 (in a VM)           ~13 Mbit/s
+ *
+ */
+
+goog.provide('goog.crypt.Md5');
+
+goog.require('goog.crypt.Hash');
+
+
+
+/**
+ * MD5 cryptographic hash constructor.
+ * @constructor
+ * @extends {goog.crypt.Hash}
+ * @final
+ * @struct
+ */
+goog.crypt.Md5 = function() {
+  goog.crypt.Md5.base(this, 'constructor');
+
+  this.blockSize = 512 / 8;
+
+  /**
+   * Holds the current values of accumulated A-D variables (MD buffer).
+   * @type {!Array<number>}
+   * @private
+   */
+  this.chain_ = new Array(4);
+
+  /**
+   * A buffer holding the data until the whole block can be processed.
+   * @type {!Array<number>}
+   * @private
+   */
+  this.block_ = new Array(this.blockSize);
+
+  /**
+   * The length of yet-unprocessed data as collected in the block.
+   * @type {number}
+   * @private
+   */
+  this.blockLength_ = 0;
+
+  /**
+   * The total length of the message so far.
+   * @type {number}
+   * @private
+   */
+  this.totalLength_ = 0;
+
+  this.reset();
+};
+goog.inherits(goog.crypt.Md5, goog.crypt.Hash);
+
+
+/**
+ * Integer rotation constants used by the abbreviated implementation.
+ * They are hardcoded in the unrolled implementation, so it is left
+ * here commented out.
+ * @type {Array<number>}
+ * @private
+ *
+goog.crypt.Md5.S_ = [
+  7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22,
+  5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20,
+  4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23,
+  6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21
+];
+ */
+
+/**
+ * Sine function constants used by the abbreviated implementation.
+ * They are hardcoded in the unrolled implementation, so it is left
+ * here commented out.
+ * @type {Array<number>}
+ * @private
+ *
+goog.crypt.Md5.T_ = [
+  0xd76aa478, 0xe8c7b756, 0x242070db, 0xc1bdceee,
+  0xf57c0faf, 0x4787c62a, 0xa8304613, 0xfd469501,
+  0x698098d8, 0x8b44f7af, 0xffff5bb1, 0x895cd7be,
+  0x6b901122, 0xfd987193, 0xa679438e, 0x49b40821,
+  0xf61e2562, 0xc040b340, 0x265e5a51, 0xe9b6c7aa,
+  0xd62f105d, 0x02441453, 0xd8a1e681, 0xe7d3fbc8,
+  0x21e1cde6, 0xc33707d6, 0xf4d50d87, 0x455a14ed,
+  0xa9e3e905, 0xfcefa3f8, 0x676f02d9, 0x8d2a4c8a,
+  0xfffa3942, 0x8771f681, 0x6d9d6122, 0xfde5380c,
+  0xa4beea44, 0x4bdecfa9, 0xf6bb4b60, 0xbebfbc70,
+  0x289b7ec6, 0xeaa127fa, 0xd4ef3085, 0x04881d05,
+  0xd9d4d039, 0xe6db99e5, 0x1fa27cf8, 0xc4ac5665,
+  0xf4292244, 0x432aff97, 0xab9423a7, 0xfc93a039,
+  0x655b59c3, 0x8f0ccc92, 0xffeff47d, 0x85845dd1,
+  0x6fa87e4f, 0xfe2ce6e0, 0xa3014314, 0x4e0811a1,
+  0xf7537e82, 0xbd3af235, 0x2ad7d2bb, 0xeb86d391
+];
+ */
+
+
+/** @override */
+goog.crypt.Md5.prototype.reset = function() {
+  this.chain_[0] = 0x67452301;
+  this.chain_[1] = 0xefcdab89;
+  this.chain_[2] = 0x98badcfe;
+  this.chain_[3] = 0x10325476;
+
+  this.blockLength_ = 0;
+  this.totalLength_ = 0;
+};
+
+
+/**
+ * Internal compress helper function. It takes a block of data (64 bytes)
+ * and updates the accumulator.
+ * @param {Array<number>|Uint8Array|string} buf The block to compress.
+ * @param {number=} opt_offset Offset of the block in the buffer.
+ * @private
+ */
+goog.crypt.Md5.prototype.compress_ = function(buf, opt_offset) {
+  if (!opt_offset) {
+    opt_offset = 0;
+  }
+
+  // We allocate the array every time, but it's cheap in practice.
+  var X = new Array(16);
+
+  // Get 16 little endian words. It is not worth unrolling this for Chrome 11.
+  if (goog.isString(buf)) {
+    for (var i = 0; i < 16; ++i) {
+      X[i] = (buf.charCodeAt(opt_offset++)) |
+          (buf.charCodeAt(opt_offset++) << 8) |
+          (buf.charCodeAt(opt_offset++) << 16) |
+          (buf.charCodeAt(opt_offset++) << 24);
+    }
+  } else {
+    for (var i = 0; i < 16; ++i) {
+      X[i] = (buf[opt_offset++]) | (buf[opt_offset++] << 8) |
+          (buf[opt_offset++] << 16) | (buf[opt_offset++] << 24);
+    }
+  }
+
+  var A = this.chain_[0];
+  var B = this.chain_[1];
+  var C = this.chain_[2];
+  var D = this.chain_[3];
+  var sum = 0;
+
+  /*
+   * This is an abbreviated implementation, it is left here commented out for
+   * reference purposes. See below for an unrolled version in use.
+   *
+  var f, n, tmp;
+  for (var i = 0; i < 64; ++i) {
+
+    if (i < 16) {
+      f = (D ^ (B & (C ^ D)));
+      n = i;
+    } else if (i < 32) {
+      f = (C ^ (D & (B ^ C)));
+      n = (5 * i + 1) % 16;
+    } else if (i < 48) {
+      f = (B ^ C ^ D);
+      n = (3 * i + 5) % 16;
+    } else {
+      f = (C ^ (B | (~D)));
+      n = (7 * i) % 16;
+    }
+
+    tmp = D;
+    D = C;
+    C = B;
+    sum = (A + f + goog.crypt.Md5.T_[i] + X[n]) & 0xffffffff;
+    B += ((sum << goog.crypt.Md5.S_[i]) & 0xffffffff) |
+         (sum >>> (32 - goog.crypt.Md5.S_[i]));
+    A = tmp;
+  }
+   */
+
+  /*
+   * This is an unrolled MD5 implementation, which gives ~30% speedup compared
+   * to the abbreviated implementation above, as measured on Chrome 11. It is
+   * important to keep 32-bit croppings to minimum and inline the integer
+   * rotation.
+   */
+  sum = (A + (D ^ (B & (C ^ D))) + X[0] + 0xd76aa478) & 0xffffffff;
+  A = B + (((sum << 7) & 0xffffffff) | (sum >>> 25));
+  sum = (D + (C ^ (A & (B ^ C))) + X[1] + 0xe8c7b756) & 0xffffffff;
+  D = A + (((sum << 12) & 0xffffffff) | (sum >>> 20));
+  sum = (C + (B ^ (D & (A ^ B))) + X[2] + 0x242070db) & 0xffffffff;
+  C = D + (((sum << 17) & 0xffffffff) | (sum >>> 15));
+  sum = (B + (A ^ (C & (D ^ A))) + X[3] + 0xc1bdceee) & 0xffffffff;
+  B = C + (((sum << 22) & 0xffffffff) | (sum >>> 10));
+  sum = (A + (D ^ (B & (C ^ D))) + X[4] + 0xf57c0faf) & 0xffffffff;
+  A = B + (((sum << 7) & 0xffffffff) | (sum >>> 25));
+  sum = (D + (C ^ (A & (B ^ C))) + X[5] + 0x4787c62a) & 0xffffffff;
+  D = A + (((sum << 12) & 0xffffffff) | (sum >>> 20));
+  sum = (C + (B ^ (D & (A ^ B))) + X[6] + 0xa8304613) & 0xffffffff;
+  C = D + (((sum << 17) & 0xffffffff) | (sum >>> 15));
+  sum = (B + (A ^ (C & (D ^ A))) + X[7] + 0xfd469501) & 0xffffffff;
+  B = C + (((sum << 22) & 0xffffffff) | (sum >>> 10));
+  sum = (A + (D ^ (B & (C ^ D))) + X[8] + 0x698098d8) & 0xffffffff;
+  A = B + (((sum << 7) & 0xffffffff) | (sum >>> 25));
+  sum = (D + (C ^ (A & (B ^ C))) + X[9] + 0x8b44f7af) & 0xffffffff;
+  D = A + (((sum << 12) & 0xffffffff) | (sum >>> 20));
+  sum = (C + (B ^ (D & (A ^ B))) + X[10] + 0xffff5bb1) & 0xffffffff;
+  C = D + (((sum << 17) & 0xffffffff) | (sum >>> 15));
+  sum = (B + (A ^ (C & (D ^ A))) + X[11] + 0x895cd7be) & 0xffffffff;
+  B = C + (((sum << 22) & 0xffffffff) | (sum >>> 10));
+  sum = (A + (D ^ (B & (C ^ D))) + X[12] + 0x6b901122) & 0xffffffff;
+  A = B + (((sum << 7) & 0xffffffff) | (sum >>> 25));
+  sum = (D + (C ^ (A & (B ^ C))) + X[13] + 0xfd987193) & 0xffffffff;
+  D = A + (((sum << 12) & 0xffffffff) | (sum >>> 20));
+  sum = (C + (B ^ (D & (A ^ B))) + X[14] + 0xa679438e) & 0xffffffff;
+  C = D + (((sum << 17) & 0xffffffff) | (sum >>> 15));
+  sum = (B + (A ^ (C & (D ^ A))) + X[15] + 0x49b40821) & 0xffffffff;
+  B = C + (((sum << 22) & 0xffffffff) | (sum >>> 10));
+  sum = (A + (C ^ (D & (B ^ C))) + X[1] + 0xf61e2562) & 0xffffffff;
+  A = B + (((sum << 5) & 0xffffffff) | (sum >>> 27));
+  sum = (D + (B ^ (C & (A ^ B))) + X[6] + 0xc040b340) & 0xffffffff;
+  D = A + (((sum << 9) & 0xffffffff) | (sum >>> 23));
+  sum = (C + (A ^ (B & (D ^ A))) + X[11] + 0x265e5a51) & 0xffffffff;
+  C = D + (((sum << 14) & 0xffffffff) | (sum >>> 18));
+  sum = (B + (D ^ (A & (C ^ D))) + X[0] + 0xe9b6c7aa) & 0xffffffff;
+  B = C + (((sum << 20) & 0xffffffff) | (sum >>> 12));
+  sum = (A + (C ^ (D & (B ^ C))) + X[5] + 0xd62f105d) & 0xffffffff;
+  A = B + (((sum << 5) & 0xffffffff) | (sum >>> 27));
+  sum = (D + (B ^ (C & (A ^ B))) + X[10] + 0x02441453) & 0xffffffff;
+  D = A + (((sum << 9) & 0xffffffff) | (sum >>> 23));
+  sum = (C + (A ^ (B & (D ^ A))) + X[15] + 0xd8a1e681) & 0xffffffff;
+  C = D + (((sum << 14) & 0xffffffff) | (sum >>> 18));
+  sum = (B + (D ^ (A & (C ^ D))) + X[4] + 0xe7d3fbc8) & 0xffffffff;
+  B = C + (((sum << 20) & 0xffffffff) | (sum >>> 12));
+  sum = (A + (C ^ (D & (B ^ C))) + X[9] + 0x21e1cde6) & 0xffffffff;
+  A = B + (((sum << 5) & 0xffffffff) | (sum >>> 27));
+  sum = (D + (B ^ (C & (A ^ B))) + X[14] + 0xc33707d6) & 0xffffffff;
+  D = A + (((sum << 9) & 0xffffffff) | (sum >>> 23));
+  sum = (C + (A ^ (B & (D ^ A))) + X[3] + 0xf4d50d87) & 0xffffffff;
+  C = D + (((sum << 14) & 0xffffffff) | (sum >>> 18));
+  sum = (B + (D ^ (A & (C ^ D))) + X[8] + 0x455a14ed) & 0xffffffff;
+  B = C + (((sum << 20) & 0xffffffff) | (sum >>> 12));
+  sum = (A + (C ^ (D & (B ^ C))) + X[13] + 0xa9e3e905) & 0xffffffff;
+  A = B + (((sum << 5) & 0xffffffff) | (sum >>> 27));
+  sum = (D + (B ^ (C & (A ^ B))) + X[2] + 0xfcefa3f8) & 0xffffffff;
+  D = A + (((sum << 9) & 0xffffffff) | (sum >>> 23));
+  sum = (C + (A ^ (B & (D ^ A))) + X[7] + 0x676f02d9) & 0xffffffff;
+  C = D + (((sum << 14) & 0xffffffff) | (sum >>> 18));
+  sum = (B + (D ^ (A & (C ^ D))) + X[12] + 0x8d2a4c8a) & 0xffffffff;
+  B = C + (((sum << 20) & 0xffffffff) | (sum >>> 12));
+  sum = (A + (B ^ C ^ D) + X[5] + 0xfffa3942) & 0xffffffff;
+  A = B + (((sum << 4) & 0xffffffff) | (sum >>> 28));
+  sum = (D + (A ^ B ^ C) + X[8] + 0x8771f681) & 0xffffffff;
+  D = A + (((sum << 11) & 0xffffffff) | (sum >>> 21));
+  sum = (C + (D ^ A ^ B) + X[11] + 0x6d9d6122) & 0xffffffff;
+  C = D + (((sum << 16) & 0xffffffff) | (sum >>> 16));
+  sum = (B + (C ^ D ^ A) + X[14] + 0xfde5380c) & 0xffffffff;
+  B = C + (((sum << 23) & 0xffffffff) | (sum >>> 9));
+  sum = (A + (B ^ C ^ D) + X[1] + 0xa4beea44) & 0xffffffff;
+  A = B + (((sum << 4) & 0xffffffff) | (sum >>> 28));
+  sum = (D + (A ^ B ^ C) + X[4] + 0x4bdecfa9) & 0xffffffff;
+  D = A + (((sum << 11) & 0xffffffff) | (sum >>> 21));
+  sum = (C + (D ^ A ^ B) + X[7] + 0xf6bb4b60) & 0xffffffff;
+  C = D + (((sum << 16) & 0xffffffff) | (sum >>> 16));
+  sum = (B + (C ^ D ^ A) + X[10] + 0xbebfbc70) & 0xffffffff;
+  B = C + (((sum << 23) & 0xffffffff) | (sum >>> 9));
+  sum = (A + (B ^ C ^ D) + X[13] + 0x289b7ec6) & 0xffffffff;
+  A = B + (((sum << 4) & 0xffffffff) | (sum >>> 28));
+  sum = (D + (A ^ B ^ C) + X[0] + 0xeaa127fa) & 0xffffffff;
+  D = A + (((sum << 11) & 0xffffffff) | (sum >>> 21));
+  sum = (C + (D ^ A ^ B) + X[3] + 0xd4ef3085) & 0xffffffff;
+  C = D + (((sum << 16) & 0xffffffff) | (sum >>> 16));
+  sum = (B + (C ^ D ^ A) + X[6] + 0x04881d05) & 0xffffffff;
+  B = C + (((sum << 23) & 0xffffffff) | (sum >>> 9));
+  sum = (A + (B ^ C ^ D) + X[9] + 0xd9d4d039) & 0xffffffff;
+  A = B + (((sum << 4) & 0xffffffff) | (sum >>> 28));
+  sum = (D + (A ^ B ^ C) + X[12] + 0xe6db99e5) & 0xffffffff;
+  D = A + (((sum << 11) & 0xffffffff) | (sum >>> 21));
+  sum = (C + (D ^ A ^ B) + X[15] + 0x1fa27cf8) & 0xffffffff;
+  C = D + (((sum << 16) & 0xffffffff) | (sum >>> 16));
+  sum = (B + (C ^ D ^ A) + X[2] + 0xc4ac5665) & 0xffffffff;
+  B = C + (((sum << 23) & 0xffffffff) | (sum >>> 9));
+  sum = (A + (C ^ (B | (~D))) + X[0] + 0xf4292244) & 0xffffffff;
+  A = B + (((sum << 6) & 0xffffffff) | (sum >>> 26));
+  sum = (D + (B ^ (A | (~C))) + X[7] + 0x432aff97) & 0xffffffff;
+  D = A + (((sum << 10) & 0xffffffff) | (sum >>> 22));
+  sum = (C + (A ^ (D | (~B))) + X[14] + 0xab9423a7) & 0xffffffff;
+  C = D + (((sum << 15) & 0xffffffff) | (sum >>> 17));
+  sum = (B + (D ^ (C | (~A))) + X[5] + 0xfc93a039) & 0xffffffff;
+  B = C + (((sum << 21) & 0xffffffff) | (sum >>> 11));
+  sum = (A + (C ^ (B | (~D))) + X[12] + 0x655b59c3) & 0xffffffff;
+  A = B + (((sum << 6) & 0xffffffff) | (sum >>> 26));
+  sum = (D + (B ^ (A | (~C))) + X[3] + 0x8f0ccc92) & 0xffffffff;
+  D = A + (((sum << 10) & 0xffffffff) | (sum >>> 22));
+  sum = (C + (A ^ (D | (~B))) + X[10] + 0xffeff47d) & 0xffffffff;
+  C = D + (((sum << 15) & 0xffffffff) | (sum >>> 17));
+  sum = (B + (D ^ (C | (~A))) + X[1] + 0x85845dd1) & 0xffffffff;
+  B = C + (((sum << 21) & 0xffffffff) | (sum >>> 11));
+  sum = (A + (C ^ (B | (~D))) + X[8] + 0x6fa87e4f) & 0xffffffff;
+  A = B + (((sum << 6) & 0xffffffff) | (sum >>> 26));
+  sum = (D + (B ^ (A | (~C))) + X[15] + 0xfe2ce6e0) & 0xffffffff;
+  D = A + (((sum << 10) & 0xffffffff) | (sum >>> 22));
+  sum = (C + (A ^ (D | (~B))) + X[6] + 0xa3014314) & 0xffffffff;
+  C = D + (((sum << 15) & 0xffffffff) | (sum >>> 17));
+  sum = (B + (D ^ (C | (~A))) + X[13] + 0x4e0811a1) & 0xffffffff;
+  B = C + (((sum << 21) & 0xffffffff) | (sum >>> 11));
+  sum = (A + (C ^ (B | (~D))) + X[4] + 0xf7537e82) & 0xffffffff;
+  A = B + (((sum << 6) & 0xffffffff) | (sum >>> 26));
+  sum = (D + (B ^ (A | (~C))) + X[11] + 0xbd3af235) & 0xffffffff;
+  D = A + (((sum << 10) & 0xffffffff) | (sum >>> 22));
+  sum = (C + (A ^ (D | (~B))) + X[2] + 0x2ad7d2bb) & 0xffffffff;
+  C = D + (((sum << 15) & 0xffffffff) | (sum >>> 17));
+  sum = (B + (D ^ (C | (~A))) + X[9] + 0xeb86d391) & 0xffffffff;
+  B = C + (((sum << 21) & 0xffffffff) | (sum >>> 11));
+
+  this.chain_[0] = (this.chain_[0] + A) & 0xffffffff;
+  this.chain_[1] = (this.chain_[1] + B) & 0xffffffff;
+  this.chain_[2] = (this.chain_[2] + C) & 0xffffffff;
+  this.chain_[3] = (this.chain_[3] + D) & 0xffffffff;
+};
+
+
+/** @override */
+goog.crypt.Md5.prototype.update = function(bytes, opt_length) {
+  if (!goog.isDef(opt_length)) {
+    opt_length = bytes.length;
+  }
+  var lengthMinusBlock = opt_length - this.blockSize;
+
+  // Copy some object properties to local variables in order to save on access
+  // time from inside the loop (~10% speedup was observed on Chrome 11).
+  var block = this.block_;
+  var blockLength = this.blockLength_;
+  var i = 0;
+
+  // The outer while loop should execute at most twice.
+  while (i < opt_length) {
+    // When we have no data in the block to top up, we can directly process the
+    // input buffer (assuming it contains sufficient data). This gives ~30%
+    // speedup on Chrome 14 and ~70% speedup on Firefox 6.0, but requires that
+    // the data is provided in large chunks (or in multiples of 64 bytes).
+    if (blockLength == 0) {
+      while (i <= lengthMinusBlock) {
+        this.compress_(bytes, i);
+        i += this.blockSize;
+      }
+    }
+
+    if (goog.isString(bytes)) {
+      while (i < opt_length) {
+        block[blockLength++] = bytes.charCodeAt(i++);
+        if (blockLength == this.blockSize) {
+          this.compress_(block);
+          blockLength = 0;
+          // Jump to the outer loop so we use the full-block optimization.
+          break;
+        }
+      }
+    } else {
+      while (i < opt_length) {
+        block[blockLength++] = bytes[i++];
+        if (blockLength == this.blockSize) {
+          this.compress_(block);
+          blockLength = 0;
+          // Jump to the outer loop so we use the full-block optimization.
+          break;
+        }
+      }
+    }
+  }
+
+  this.blockLength_ = blockLength;
+  this.totalLength_ += opt_length;
+};
+
+
+/** @override */
+goog.crypt.Md5.prototype.digest = function() {
+  // This must accommodate at least 1 padding byte (0x80), 8 bytes of
+  // total bitlength, and must end at a 64-byte boundary.
+  var pad = new Array(
+      (this.blockLength_ < 56 ? this.blockSize : this.blockSize * 2) -
+      this.blockLength_);
+
+  // Add padding: 0x80 0x00*
+  pad[0] = 0x80;
+  for (var i = 1; i < pad.length - 8; ++i) {
+    pad[i] = 0;
+  }
+  // Add the total number of bits, little endian 64-bit integer.
+  var totalBits = this.totalLength_ * 8;
+  for (var i = pad.length - 8; i < pad.length; ++i) {
+    pad[i] = totalBits & 0xff;
+    totalBits /= 0x100;  // Don't use bit-shifting here!
+  }
+  this.update(pad);
+
+  var digest = new Array(16);
+  var n = 0;
+  for (var i = 0; i < 4; ++i) {
+    for (var j = 0; j < 32; j += 8) {
+      digest[n++] = (this.chain_[i] >>> j) & 0xff;
+    }
+  }
+  return digest;
+};
+
+goog.provide('ol.style.Stroke');
+
+goog.require('goog.crypt');
+goog.require('goog.crypt.Md5');
+goog.require('ol.color');
+
+
+/**
+ * @classdesc
+ * Set stroke style for vector features.
+ * Note that the defaults given are the Canvas defaults, which will be used if
+ * option is not defined. The `get` functions return whatever was entered in
+ * the options; they will not return the default.
+ *
+ * @constructor
+ * @param {olx.style.StrokeOptions=} opt_options Options.
+ * @api
+ */
+ol.style.Stroke = function(opt_options) {
+
+  var options = opt_options || {};
+
+  /**
+   * @private
+   * @type {ol.Color|string}
+   */
+  this.color_ = options.color !== undefined ? options.color : null;
+
+  /**
+   * @private
+   * @type {string|undefined}
+   */
+  this.lineCap_ = options.lineCap;
+
+  /**
+   * @private
+   * @type {Array.<number>}
+   */
+  this.lineDash_ = options.lineDash !== undefined ? options.lineDash : null;
+
+  /**
+   * @private
+   * @type {string|undefined}
+   */
+  this.lineJoin_ = options.lineJoin;
+
+  /**
+   * @private
+   * @type {number|undefined}
+   */
+  this.miterLimit_ = options.miterLimit;
+
+  /**
+   * @private
+   * @type {number|undefined}
+   */
+  this.width_ = options.width;
+
+  /**
+   * @private
+   * @type {string|undefined}
+   */
+  this.checksum_ = undefined;
+};
+
+
+/**
+ * Get the stroke color.
+ * @return {ol.Color|string} Color.
+ * @api
+ */
+ol.style.Stroke.prototype.getColor = function() {
+  return this.color_;
+};
+
+
+/**
+ * Get the line cap type for the stroke.
+ * @return {string|undefined} Line cap.
+ * @api
+ */
+ol.style.Stroke.prototype.getLineCap = function() {
+  return this.lineCap_;
+};
+
+
+/**
+ * Get the line dash style for the stroke.
+ * @return {Array.<number>} Line dash.
+ * @api
+ */
+ol.style.Stroke.prototype.getLineDash = function() {
+  return this.lineDash_;
+};
+
+
+/**
+ * Get the line join type for the stroke.
+ * @return {string|undefined} Line join.
+ * @api
+ */
+ol.style.Stroke.prototype.getLineJoin = function() {
+  return this.lineJoin_;
+};
+
+
+/**
+ * Get the miter limit for the stroke.
+ * @return {number|undefined} Miter limit.
+ * @api
+ */
+ol.style.Stroke.prototype.getMiterLimit = function() {
+  return this.miterLimit_;
+};
+
+
+/**
+ * Get the stroke width.
+ * @return {number|undefined} Width.
+ * @api
+ */
+ol.style.Stroke.prototype.getWidth = function() {
+  return this.width_;
+};
+
+
+/**
+ * Set the color.
+ *
+ * @param {ol.Color|string} color Color.
+ * @api
+ */
+ol.style.Stroke.prototype.setColor = function(color) {
+  this.color_ = color;
+  this.checksum_ = undefined;
+};
+
+
+/**
+ * Set the line cap.
+ *
+ * @param {string|undefined} lineCap Line cap.
+ * @api
+ */
+ol.style.Stroke.prototype.setLineCap = function(lineCap) {
+  this.lineCap_ = lineCap;
+  this.checksum_ = undefined;
+};
+
+
+/**
+ * Set the line dash.
+ *
+ * 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
+ */
+ol.style.Stroke.prototype.setLineDash = function(lineDash) {
+  this.lineDash_ = lineDash;
+  this.checksum_ = undefined;
+};
+
+
+/**
+ * Set the line join.
+ *
+ * @param {string|undefined} lineJoin Line join.
+ * @api
+ */
+ol.style.Stroke.prototype.setLineJoin = function(lineJoin) {
+  this.lineJoin_ = lineJoin;
+  this.checksum_ = undefined;
+};
+
+
+/**
+ * Set the miter limit.
+ *
+ * @param {number|undefined} miterLimit Miter limit.
+ * @api
+ */
+ol.style.Stroke.prototype.setMiterLimit = function(miterLimit) {
+  this.miterLimit_ = miterLimit;
+  this.checksum_ = undefined;
+};
+
+
+/**
+ * Set the width.
+ *
+ * @param {number|undefined} width Width.
+ * @api
+ */
+ol.style.Stroke.prototype.setWidth = function(width) {
+  this.width_ = width;
+  this.checksum_ = undefined;
+};
+
+
+/**
+ * @return {string} The checksum.
+ */
+ol.style.Stroke.prototype.getChecksum = function() {
+  if (this.checksum_ === undefined) {
+    var raw = 's' +
+        (this.color_ ?
+            ol.color.asString(this.color_) : '-') + ',' +
+        (this.lineCap_ !== undefined ?
+            this.lineCap_.toString() : '-') + ',' +
+        (this.lineDash_ ?
+            this.lineDash_.toString() : '-') + ',' +
+        (this.lineJoin_ !== undefined ?
+            this.lineJoin_ : '-') + ',' +
+        (this.miterLimit_ !== undefined ?
+            this.miterLimit_.toString() : '-') + ',' +
+        (this.width_ !== undefined ?
+            this.width_.toString() : '-');
+
+    var md5 = new goog.crypt.Md5();
+    md5.update(raw);
+    this.checksum_ = goog.crypt.byteArrayToString(md5.digest());
+  }
+
+  return this.checksum_;
+};
+
+goog.provide('ol.style.Circle');
+
+goog.require('goog.asserts');
+goog.require('ol');
+goog.require('ol.color');
+goog.require('ol.colorlike');
+goog.require('ol.dom');
+goog.require('ol.has');
+goog.require('ol.render.canvas');
+goog.require('ol.style.Fill');
+goog.require('ol.style.Image');
+goog.require('ol.style.ImageState');
+goog.require('ol.style.Stroke');
+
+
+/**
+ * @classdesc
+ * Set circle style for vector features.
+ *
+ * @constructor
+ * @param {olx.style.CircleOptions=} opt_options Options.
+ * @extends {ol.style.Image}
+ * @api
+ */
+ol.style.Circle = function(opt_options) {
+
+  var options = opt_options || {};
+
+  /**
+   * @private
+   * @type {Array.<string>}
+   */
+  this.checksums_ = null;
+
+  /**
+   * @private
+   * @type {HTMLCanvasElement}
+   */
+  this.canvas_ = null;
+
+  /**
+   * @private
+   * @type {HTMLCanvasElement}
+   */
+  this.hitDetectionCanvas_ = null;
+
+  /**
+   * @private
+   * @type {ol.style.Fill}
+   */
+  this.fill_ = options.fill !== undefined ? options.fill : null;
+
+  /**
+   * @private
+   * @type {ol.style.Stroke}
+   */
+  this.stroke_ = options.stroke !== undefined ? options.stroke : null;
+
+  /**
+   * @private
+   * @type {number}
+   */
+  this.radius_ = options.radius;
+
+  /**
+   * @private
+   * @type {Array.<number>}
+   */
+  this.origin_ = [0, 0];
+
+  /**
+   * @private
+   * @type {Array.<number>}
+   */
+  this.anchor_ = null;
+
+  /**
+   * @private
+   * @type {ol.Size}
+   */
+  this.size_ = null;
+
+  /**
+   * @private
+   * @type {ol.Size}
+   */
+  this.imageSize_ = null;
+
+  /**
+   * @private
+   * @type {ol.Size}
+   */
+  this.hitDetectionImageSize_ = null;
+
+  this.render_(options.atlasManager);
+
+  /**
+   * @type {boolean}
+   */
+  var snapToPixel = options.snapToPixel !== undefined ?
+      options.snapToPixel : true;
+
+  ol.style.Image.call(this, {
+    opacity: 1,
+    rotateWithView: false,
+    rotation: 0,
+    scale: 1,
+    snapToPixel: snapToPixel
+  });
+
+};
+ol.inherits(ol.style.Circle, ol.style.Image);
+
+
+/**
+ * @inheritDoc
+ */
+ol.style.Circle.prototype.getAnchor = function() {
+  return this.anchor_;
+};
+
+
+/**
+ * Get the fill style for the circle.
+ * @return {ol.style.Fill} Fill style.
+ * @api
+ */
+ol.style.Circle.prototype.getFill = function() {
+  return this.fill_;
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.style.Circle.prototype.getHitDetectionImage = function(pixelRatio) {
+  return this.hitDetectionCanvas_;
+};
+
+
+/**
+ * Get the image used to render the circle.
+ * @param {number} pixelRatio Pixel ratio.
+ * @return {HTMLCanvasElement} Canvas element.
+ * @api
+ */
+ol.style.Circle.prototype.getImage = function(pixelRatio) {
+  return this.canvas_;
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.style.Circle.prototype.getImageState = function() {
+  return ol.style.ImageState.LOADED;
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.style.Circle.prototype.getImageSize = function() {
+  return this.imageSize_;
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.style.Circle.prototype.getHitDetectionImageSize = function() {
+  return this.hitDetectionImageSize_;
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.style.Circle.prototype.getOrigin = function() {
+  return this.origin_;
+};
+
+
+/**
+ * Get the circle radius.
+ * @return {number} Radius.
+ * @api
+ */
+ol.style.Circle.prototype.getRadius = function() {
+  return this.radius_;
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.style.Circle.prototype.getSize = function() {
+  return this.size_;
+};
+
+
+/**
+ * Get the stroke style for the circle.
+ * @return {ol.style.Stroke} Stroke style.
+ * @api
+ */
+ol.style.Circle.prototype.getStroke = function() {
+  return this.stroke_;
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.style.Circle.prototype.listenImageChange = ol.nullFunction;
+
+
+/**
+ * @inheritDoc
+ */
+ol.style.Circle.prototype.load = ol.nullFunction;
+
+
+/**
+ * @inheritDoc
+ */
+ol.style.Circle.prototype.unlistenImageChange = ol.nullFunction;
+
+
+/**
+ * @private
+ * @param {ol.style.AtlasManager|undefined} atlasManager An atlas manager.
+ */
+ol.style.Circle.prototype.render_ = function(atlasManager) {
+  var imageSize;
+  var lineDash = null;
+  var strokeStyle;
+  var strokeWidth = 0;
+
+  if (this.stroke_) {
+    strokeStyle = ol.color.asString(this.stroke_.getColor());
+    strokeWidth = this.stroke_.getWidth();
+    if (strokeWidth === undefined) {
+      strokeWidth = ol.render.canvas.defaultLineWidth;
+    }
+    lineDash = this.stroke_.getLineDash();
+    if (!ol.has.CANVAS_LINE_DASH) {
+      lineDash = null;
+    }
+  }
+
+
+  var size = 2 * (this.radius_ + strokeWidth) + 1;
+
+  /** @type {ol.CircleRenderOptions} */
+  var renderOptions = {
+    strokeStyle: strokeStyle,
+    strokeWidth: strokeWidth,
+    size: size,
+    lineDash: lineDash
+  };
+
+  if (atlasManager === undefined) {
+    // no atlas manager is used, create a new canvas
+    var context = ol.dom.createCanvasContext2D(size, size);
+    this.canvas_ = context.canvas;
+
+    // canvas.width and height are rounded to the closest integer
+    size = this.canvas_.width;
+    imageSize = size;
+
+    // draw the circle on the canvas
+    this.draw_(renderOptions, context, 0, 0);
+
+    this.createHitDetectionCanvas_(renderOptions);
+  } else {
+    // an atlas manager is used, add the symbol to an atlas
+    size = Math.round(size);
+
+    var hasCustomHitDetectionImage = !this.fill_;
+    var renderHitDetectionCallback;
+    if (hasCustomHitDetectionImage) {
+      // render the hit-detection image into a separate atlas image
+      renderHitDetectionCallback =
+          this.drawHitDetectionCanvas_.bind(this, renderOptions);
+    }
+
+    var id = this.getChecksum();
+    var info = atlasManager.add(
+        id, size, size, this.draw_.bind(this, renderOptions),
+        renderHitDetectionCallback);
+    goog.asserts.assert(info, 'circle radius is too large');
+
+    this.canvas_ = info.image;
+    this.origin_ = [info.offsetX, info.offsetY];
+    imageSize = info.image.width;
+
+    if (hasCustomHitDetectionImage) {
+      this.hitDetectionCanvas_ = info.hitImage;
+      this.hitDetectionImageSize_ =
+          [info.hitImage.width, info.hitImage.height];
+    } else {
+      this.hitDetectionCanvas_ = this.canvas_;
+      this.hitDetectionImageSize_ = [imageSize, imageSize];
+    }
+  }
+
+  this.anchor_ = [size / 2, size / 2];
+  this.size_ = [size, size];
+  this.imageSize_ = [imageSize, imageSize];
+};
+
+
+/**
+ * @private
+ * @param {ol.CircleRenderOptions} renderOptions Render options.
+ * @param {CanvasRenderingContext2D} context The rendering context.
+ * @param {number} x The origin for the symbol (x).
+ * @param {number} y The origin for the symbol (y).
+ */
+ol.style.Circle.prototype.draw_ = function(renderOptions, context, x, y) {
+  // reset transform
+  context.setTransform(1, 0, 0, 1, 0, 0);
+
+  // then move to (x, y)
+  context.translate(x, y);
+
+  context.beginPath();
+  context.arc(
+      renderOptions.size / 2, renderOptions.size / 2,
+      this.radius_, 0, 2 * Math.PI, true);
+
+  if (this.fill_) {
+    context.fillStyle = ol.colorlike.asColorLike(this.fill_.getColor());
+    context.fill();
+  }
+  if (this.stroke_) {
+    context.strokeStyle = renderOptions.strokeStyle;
+    context.lineWidth = renderOptions.strokeWidth;
+    if (renderOptions.lineDash) {
+      context.setLineDash(renderOptions.lineDash);
+    }
+    context.stroke();
+  }
+  context.closePath();
+};
+
+
+/**
+ * @private
+ * @param {ol.CircleRenderOptions} renderOptions Render options.
+ */
+ol.style.Circle.prototype.createHitDetectionCanvas_ = function(renderOptions) {
+  this.hitDetectionImageSize_ = [renderOptions.size, renderOptions.size];
+  if (this.fill_) {
+    this.hitDetectionCanvas_ = this.canvas_;
+    return;
+  }
+
+  // if no fill style is set, create an extra hit-detection image with a
+  // default fill style
+  var context = ol.dom.createCanvasContext2D(renderOptions.size, renderOptions.size);
+  this.hitDetectionCanvas_ = context.canvas;
+
+  this.drawHitDetectionCanvas_(renderOptions, context, 0, 0);
+};
+
+
+/**
+ * @private
+ * @param {ol.CircleRenderOptions} renderOptions Render options.
+ * @param {CanvasRenderingContext2D} context The context.
+ * @param {number} x The origin for the symbol (x).
+ * @param {number} y The origin for the symbol (y).
+ */
+ol.style.Circle.prototype.drawHitDetectionCanvas_ = function(renderOptions, context, x, y) {
+  // reset transform
+  context.setTransform(1, 0, 0, 1, 0, 0);
+
+  // then move to (x, y)
+  context.translate(x, y);
+
+  context.beginPath();
+  context.arc(
+      renderOptions.size / 2, renderOptions.size / 2,
+      this.radius_, 0, 2 * Math.PI, true);
+
+  context.fillStyle = ol.color.asString(ol.render.canvas.defaultFillStyle);
+  context.fill();
+  if (this.stroke_) {
+    context.strokeStyle = renderOptions.strokeStyle;
+    context.lineWidth = renderOptions.strokeWidth;
+    if (renderOptions.lineDash) {
+      context.setLineDash(renderOptions.lineDash);
+    }
+    context.stroke();
+  }
+  context.closePath();
+};
+
+
+/**
+ * @return {string} The checksum.
+ */
+ol.style.Circle.prototype.getChecksum = function() {
+  var strokeChecksum = this.stroke_ ?
+      this.stroke_.getChecksum() : '-';
+  var fillChecksum = this.fill_ ?
+      this.fill_.getChecksum() : '-';
+
+  var recalculate = !this.checksums_ ||
+      (strokeChecksum != this.checksums_[1] ||
+      fillChecksum != this.checksums_[2] ||
+      this.radius_ != this.checksums_[3]);
+
+  if (recalculate) {
+    var checksum = 'c' + strokeChecksum + fillChecksum +
+        (this.radius_ !== undefined ? this.radius_.toString() : '-');
+    this.checksums_ = [checksum, strokeChecksum, fillChecksum, this.radius_];
+  }
+
+  return this.checksums_[0];
+};
+
+goog.provide('ol.style.Style');
+goog.provide('ol.style.defaultGeometryFunction');
+
+goog.require('goog.asserts');
+goog.require('ol.geom.Geometry');
+goog.require('ol.geom.GeometryType');
+goog.require('ol.style.Circle');
+goog.require('ol.style.Fill');
+goog.require('ol.style.Image');
+goog.require('ol.style.Stroke');
+
+
+/**
+ * @classdesc
+ * Container for vector feature rendering styles. Any changes made to the style
+ * or its children through `set*()` methods will not take effect until the
+ * feature or layer that uses the style is re-rendered.
+ *
+ * @constructor
+ * @struct
+ * @param {olx.style.StyleOptions=} opt_options Style options.
+ * @api
+ */
+ol.style.Style = function(opt_options) {
+
+  var options = opt_options || {};
+
+  /**
+   * @private
+   * @type {string|ol.geom.Geometry|ol.StyleGeometryFunction}
+   */
+  this.geometry_ = null;
+
+  /**
+   * @private
+   * @type {!ol.StyleGeometryFunction}
+   */
+  this.geometryFunction_ = ol.style.defaultGeometryFunction;
+
+  if (options.geometry !== undefined) {
+    this.setGeometry(options.geometry);
+  }
+
+  /**
+   * @private
+   * @type {ol.style.Fill}
+   */
+  this.fill_ = options.fill !== undefined ? options.fill : null;
+
+  /**
+   * @private
+   * @type {ol.style.Image}
+   */
+  this.image_ = options.image !== undefined ? options.image : null;
+
+  /**
+   * @private
+   * @type {ol.style.Stroke}
+   */
+  this.stroke_ = options.stroke !== undefined ? options.stroke : null;
+
+  /**
+   * @private
+   * @type {ol.style.Text}
+   */
+  this.text_ = options.text !== undefined ? options.text : null;
+
+  /**
+   * @private
+   * @type {number|undefined}
+   */
+  this.zIndex_ = options.zIndex;
+
+};
+
+
+/**
+ * Get the geometry to be rendered.
+ * @return {string|ol.geom.Geometry|ol.StyleGeometryFunction}
+ * Feature property or geometry or function that returns the geometry that will
+ * be rendered with this style.
+ * @api
+ */
+ol.style.Style.prototype.getGeometry = function() {
+  return this.geometry_;
+};
+
+
+/**
+ * Get the function used to generate a geometry for rendering.
+ * @return {!ol.StyleGeometryFunction} Function that is called with a feature
+ * and returns the geometry to render instead of the feature's geometry.
+ * @api
+ */
+ol.style.Style.prototype.getGeometryFunction = function() {
+  return this.geometryFunction_;
+};
+
+
+/**
+ * Get the fill style.
+ * @return {ol.style.Fill} Fill style.
+ * @api
+ */
+ol.style.Style.prototype.getFill = function() {
+  return this.fill_;
+};
+
+
+/**
+ * Get the image style.
+ * @return {ol.style.Image} Image style.
+ * @api
+ */
+ol.style.Style.prototype.getImage = function() {
+  return this.image_;
+};
+
+
+/**
+ * Get the stroke style.
+ * @return {ol.style.Stroke} Stroke style.
+ * @api
+ */
+ol.style.Style.prototype.getStroke = function() {
+  return this.stroke_;
+};
+
+
+/**
+ * Get the text style.
+ * @return {ol.style.Text} Text style.
+ * @api
+ */
+ol.style.Style.prototype.getText = function() {
+  return this.text_;
+};
+
+
+/**
+ * Get the z-index for the style.
+ * @return {number|undefined} ZIndex.
+ * @api
+ */
+ol.style.Style.prototype.getZIndex = function() {
+  return this.zIndex_;
+};
+
+
+/**
+ * Set a geometry that is rendered instead of the feature's geometry.
+ *
+ * @param {string|ol.geom.Geometry|ol.StyleGeometryFunction} geometry
+ *     Feature property or geometry or function returning a geometry to render
+ *     for this style.
+ * @api
+ */
+ol.style.Style.prototype.setGeometry = function(geometry) {
+  if (typeof geometry === 'function') {
+    this.geometryFunction_ = geometry;
+  } else if (typeof geometry === 'string') {
+    this.geometryFunction_ = function(feature) {
+      var result = feature.get(geometry);
+      if (result) {
+        goog.asserts.assertInstanceof(result, ol.geom.Geometry,
+            'feature geometry must be an ol.geom.Geometry instance');
+      }
+      return result;
+    };
+  } else if (!geometry) {
+    this.geometryFunction_ = ol.style.defaultGeometryFunction;
+  } else if (geometry !== undefined) {
+    goog.asserts.assertInstanceof(geometry, ol.geom.Geometry,
+        'geometry must be an ol.geom.Geometry instance');
+    this.geometryFunction_ = function() {
+      return geometry;
+    };
+  }
+  this.geometry_ = geometry;
+};
+
+
+/**
+ * Set the z-index.
+ *
+ * @param {number|undefined} zIndex ZIndex.
+ * @api
+ */
+ol.style.Style.prototype.setZIndex = function(zIndex) {
+  this.zIndex_ = zIndex;
+};
+
+
+/**
+ * Convert the provided object into a style function.  Functions passed through
+ * unchanged.  Arrays of ol.style.Style or single style objects wrapped in a
+ * new style function.
+ * @param {ol.StyleFunction|Array.<ol.style.Style>|ol.style.Style} obj
+ *     A style function, a single style, or an array of styles.
+ * @return {ol.StyleFunction} A style function.
+ */
+ol.style.createStyleFunction = function(obj) {
+  var styleFunction;
+
+  if (typeof obj === 'function') {
+    styleFunction = obj;
+  } else {
+    /**
+     * @type {Array.<ol.style.Style>}
+     */
+    var styles;
+    if (Array.isArray(obj)) {
+      styles = obj;
+    } else {
+      goog.asserts.assertInstanceof(obj, ol.style.Style,
+          'obj geometry must be an ol.style.Style instance');
+      styles = [obj];
+    }
+    styleFunction = function() {
+      return styles;
+    };
+  }
+  return styleFunction;
+};
+
+
+/**
+ * @type {Array.<ol.style.Style>}
+ * @private
+ */
+ol.style.defaultStyle_ = null;
+
+
+/**
+ * @param {ol.Feature|ol.render.Feature} feature Feature.
+ * @param {number} resolution Resolution.
+ * @return {Array.<ol.style.Style>} Style.
+ */
+ol.style.defaultStyleFunction = function(feature, resolution) {
+  // We don't use an immediately-invoked function
+  // and a closure so we don't get an error at script evaluation time in
+  // browsers that do not support Canvas. (ol.style.Circle does
+  // canvas.getContext('2d') at construction time, which will cause an.error
+  // in such browsers.)
+  if (!ol.style.defaultStyle_) {
+    var fill = new ol.style.Fill({
+      color: 'rgba(255,255,255,0.4)'
+    });
+    var stroke = new ol.style.Stroke({
+      color: '#3399CC',
+      width: 1.25
+    });
+    ol.style.defaultStyle_ = [
+      new ol.style.Style({
+        image: new ol.style.Circle({
+          fill: fill,
+          stroke: stroke,
+          radius: 5
+        }),
+        fill: fill,
+        stroke: stroke
+      })
+    ];
+  }
+  return ol.style.defaultStyle_;
+};
+
+
+/**
+ * Default styles for editing features.
+ * @return {Object.<ol.geom.GeometryType, Array.<ol.style.Style>>} Styles
+ */
+ol.style.createDefaultEditingStyles = function() {
+  /** @type {Object.<ol.geom.GeometryType, Array.<ol.style.Style>>} */
+  var styles = {};
+  var white = [255, 255, 255, 1];
+  var blue = [0, 153, 255, 1];
+  var width = 3;
+  styles[ol.geom.GeometryType.POLYGON] = [
+    new ol.style.Style({
+      fill: new ol.style.Fill({
+        color: [255, 255, 255, 0.5]
+      })
+    })
+  ];
+  styles[ol.geom.GeometryType.MULTI_POLYGON] =
+      styles[ol.geom.GeometryType.POLYGON];
+
+  styles[ol.geom.GeometryType.LINE_STRING] = [
+    new ol.style.Style({
+      stroke: new ol.style.Stroke({
+        color: white,
+        width: width + 2
+      })
+    }),
+    new ol.style.Style({
+      stroke: new ol.style.Stroke({
+        color: blue,
+        width: width
+      })
+    })
+  ];
+  styles[ol.geom.GeometryType.MULTI_LINE_STRING] =
+      styles[ol.geom.GeometryType.LINE_STRING];
+
+  styles[ol.geom.GeometryType.CIRCLE] =
+      styles[ol.geom.GeometryType.POLYGON].concat(
+          styles[ol.geom.GeometryType.LINE_STRING]
+      );
+
+
+  styles[ol.geom.GeometryType.POINT] = [
+    new ol.style.Style({
+      image: new ol.style.Circle({
+        radius: width * 2,
+        fill: new ol.style.Fill({
+          color: blue
+        }),
+        stroke: new ol.style.Stroke({
+          color: white,
+          width: width / 2
+        })
+      }),
+      zIndex: Infinity
+    })
+  ];
+  styles[ol.geom.GeometryType.MULTI_POINT] =
+      styles[ol.geom.GeometryType.POINT];
+
+  styles[ol.geom.GeometryType.GEOMETRY_COLLECTION] =
+      styles[ol.geom.GeometryType.POLYGON].concat(
+          styles[ol.geom.GeometryType.LINE_STRING],
+          styles[ol.geom.GeometryType.POINT]
+      );
+
+  return styles;
+};
+
+
+/**
+ * Function that is called with a feature and returns its default geometry.
+ * @param {ol.Feature|ol.render.Feature} feature Feature to get the geometry
+ *     for.
+ * @return {ol.geom.Geometry|ol.render.Feature|undefined} Geometry to render.
+ */
+ol.style.defaultGeometryFunction = function(feature) {
+  goog.asserts.assert(feature, 'feature must not be null');
+  return feature.getGeometry();
+};
+
+goog.provide('ol.layer.Vector');
+
+goog.require('goog.asserts');
+goog.require('ol');
+goog.require('ol.layer.Layer');
+goog.require('ol.object');
+goog.require('ol.style.Style');
+
+
+/**
+ * @enum {string}
+ */
+ol.layer.VectorProperty = {
+  RENDER_ORDER: 'renderOrder'
+};
+
+
+/**
+ * @classdesc
+ * Vector data that is rendered client-side.
+ * Note that any property set in the options is set as a {@link ol.Object}
+ * property on the layer object; for example, setting `title: 'My Title'` in the
+ * options means that `title` is observable, and has get/set accessors.
+ *
+ * @constructor
+ * @extends {ol.layer.Layer}
+ * @fires ol.render.Event
+ * @param {olx.layer.VectorOptions=} opt_options Options.
+ * @api stable
+ */
+ol.layer.Vector = function(opt_options) {
+
+  var options = opt_options ?
+      opt_options : /** @type {olx.layer.VectorOptions} */ ({});
+
+  goog.asserts.assert(
+      options.renderOrder === undefined || !options.renderOrder ||
+      typeof options.renderOrder === 'function',
+      'renderOrder must be a comparator function');
+
+  var baseOptions = ol.object.assign({}, options);
+
+  delete baseOptions.style;
+  delete baseOptions.renderBuffer;
+  delete baseOptions.updateWhileAnimating;
+  delete baseOptions.updateWhileInteracting;
+  ol.layer.Layer.call(this, /** @type {olx.layer.LayerOptions} */ (baseOptions));
+
+  /**
+   * @type {number}
+   * @private
+   */
+  this.renderBuffer_ = options.renderBuffer !== undefined ?
+      options.renderBuffer : 100;
+
+  /**
+   * User provided style.
+   * @type {ol.style.Style|Array.<ol.style.Style>|ol.StyleFunction}
+   * @private
+   */
+  this.style_ = null;
+
+  /**
+   * Style function for use within the library.
+   * @type {ol.StyleFunction|undefined}
+   * @private
+   */
+  this.styleFunction_ = undefined;
+
+  this.setStyle(options.style);
+
+  /**
+   * @type {boolean}
+   * @private
+   */
+  this.updateWhileAnimating_ = options.updateWhileAnimating !== undefined ?
+      options.updateWhileAnimating : false;
+
+  /**
+   * @type {boolean}
+   * @private
+   */
+  this.updateWhileInteracting_ = options.updateWhileInteracting !== undefined ?
+      options.updateWhileInteracting : false;
+
+};
+ol.inherits(ol.layer.Vector, ol.layer.Layer);
+
+
+/**
+ * @return {number|undefined} Render buffer.
+ */
+ol.layer.Vector.prototype.getRenderBuffer = function() {
+  return this.renderBuffer_;
+};
+
+
+/**
+ * @return {function(ol.Feature, ol.Feature): number|null|undefined} Render
+ *     order.
+ */
+ol.layer.Vector.prototype.getRenderOrder = function() {
+  return /** @type {function(ol.Feature, ol.Feature):number|null|undefined} */ (
+      this.get(ol.layer.VectorProperty.RENDER_ORDER));
+};
+
+
+/**
+ * Return the associated {@link ol.source.Vector vectorsource} of the layer.
+ * @function
+ * @return {ol.source.Vector} Source.
+ * @api stable
+ */
+ol.layer.Vector.prototype.getSource;
+
+
+/**
+ * Get the style for features.  This returns whatever was passed to the `style`
+ * option at construction or to the `setStyle` method.
+ * @return {ol.style.Style|Array.<ol.style.Style>|ol.StyleFunction}
+ *     Layer style.
+ * @api stable
+ */
+ol.layer.Vector.prototype.getStyle = function() {
+  return this.style_;
+};
+
+
+/**
+ * Get the style function.
+ * @return {ol.StyleFunction|undefined} Layer style function.
+ * @api stable
+ */
+ol.layer.Vector.prototype.getStyleFunction = function() {
+  return this.styleFunction_;
+};
+
+
+/**
+ * @return {boolean} Whether the rendered layer should be updated while
+ *     animating.
+ */
+ol.layer.Vector.prototype.getUpdateWhileAnimating = function() {
+  return this.updateWhileAnimating_;
+};
+
+
+/**
+ * @return {boolean} Whether the rendered layer should be updated while
+ *     interacting.
+ */
+ol.layer.Vector.prototype.getUpdateWhileInteracting = function() {
+  return this.updateWhileInteracting_;
+};
+
+
+/**
+ * @param {function(ol.Feature, ol.Feature):number|null|undefined} renderOrder
+ *     Render order.
+ */
+ol.layer.Vector.prototype.setRenderOrder = function(renderOrder) {
+  goog.asserts.assert(
+      renderOrder === undefined || !renderOrder ||
+      typeof renderOrder === 'function',
+      'renderOrder must be a comparator function');
+  this.set(ol.layer.VectorProperty.RENDER_ORDER, renderOrder);
+};
+
+
+/**
+ * Set the style for features.  This can be a single style object, an array
+ * of styles, or a function that takes a feature and resolution and returns
+ * an array of styles. If it is `undefined` the default style is used. If
+ * it is `null` the layer has no style (a `null` style), so only features
+ * that have their own styles will be rendered in the layer. See
+ * {@link ol.style} for information on the default style.
+ * @param {ol.style.Style|Array.<ol.style.Style>|ol.StyleFunction|null|undefined}
+ *     style Layer style.
+ * @api stable
+ */
+ol.layer.Vector.prototype.setStyle = function(style) {
+  this.style_ = style !== undefined ? style : ol.style.defaultStyleFunction;
+  this.styleFunction_ = style === null ?
+      undefined : ol.style.createStyleFunction(this.style_);
+  this.changed();
+};
+
+goog.provide('ol.layer.VectorTile');
+
+goog.require('goog.asserts');
+goog.require('ol.layer.Vector');
+goog.require('ol.object');
+
+
+/**
+ * @enum {string}
+ */
+ol.layer.VectorTileProperty = {
+  PRELOAD: 'preload',
+  USE_INTERIM_TILES_ON_ERROR: 'useInterimTilesOnError'
+};
+
+
+/**
+ * @enum {string}
+ * Render mode for vector tiles:
+ *  * `'image'`: Vector tiles are rendered as images. Great performance, but
+ *    point symbols and texts are always rotated with the view and pixels are
+ *    scaled during zoom animations.
+ *  * `'hybrid'`: Polygon and line elements are rendered as images, so pixels
+ *    are scaled during zoom animations. Point symbols 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
+ */
+ol.layer.VectorTileRenderType = {
+  IMAGE: 'image',
+  HYBRID: 'hybrid',
+  VECTOR: 'vector'
+};
+
+
+/**
+ * @classdesc
+ * Layer for vector tile data that is rendered client-side.
+ * Note that any property set in the options is set as a {@link ol.Object}
+ * property on the layer object; for example, setting `title: 'My Title'` in the
+ * options means that `title` is observable, and has get/set accessors.
+ *
+ * @constructor
+ * @extends {ol.layer.Vector}
+ * @param {olx.layer.VectorTileOptions=} opt_options Options.
+ * @api
+ */
+ol.layer.VectorTile = function(opt_options) {
+  var options = opt_options ? opt_options : {};
+
+  var baseOptions = ol.object.assign({}, options);
+
+  delete baseOptions.preload;
+  delete baseOptions.useInterimTilesOnError;
+  ol.layer.Vector.call(this,  /** @type {olx.layer.VectorOptions} */ (baseOptions));
+
+  this.setPreload(options.preload ? options.preload : 0);
+  this.setUseInterimTilesOnError(options.useInterimTilesOnError ?
+      options.useInterimTilesOnError : true);
+
+  goog.asserts.assert(options.renderMode == undefined ||
+      options.renderMode == ol.layer.VectorTileRenderType.IMAGE ||
+      options.renderMode == ol.layer.VectorTileRenderType.HYBRID ||
+      options.renderMode == ol.layer.VectorTileRenderType.VECTOR,
+      'renderMode needs to be \'image\', \'hybrid\' or \'vector\'');
+
+  /**
+   * @private
+   * @type {ol.layer.VectorTileRenderType|string}
+   */
+  this.renderMode_ = options.renderMode || ol.layer.VectorTileRenderType.HYBRID;
+
+};
+ol.inherits(ol.layer.VectorTile, ol.layer.Vector);
+
+
+/**
+ * Return the level as number to which we will preload tiles up to.
+ * @return {number} The level to preload tiles up to.
+ * @observable
+ * @api
+ */
+ol.layer.VectorTile.prototype.getPreload = function() {
+  return /** @type {number} */ (this.get(ol.layer.VectorTileProperty.PRELOAD));
+};
+
+
+/**
+ * @return {ol.layer.VectorTileRenderType|string} The render mode.
+ */
+ol.layer.VectorTile.prototype.getRenderMode = function() {
+  return this.renderMode_;
+};
+
+
+/**
+ * Whether we use interim tiles on error.
+ * @return {boolean} Use interim tiles on error.
+ * @observable
+ * @api
+ */
+ol.layer.VectorTile.prototype.getUseInterimTilesOnError = function() {
+  return /** @type {boolean} */ (
+      this.get(ol.layer.VectorTileProperty.USE_INTERIM_TILES_ON_ERROR));
+};
+
+
+/**
+ * Set the level as number to which we will preload tiles up to.
+ * @param {number} preload The level to preload tiles up to.
+ * @observable
+ * @api
+ */
+ol.layer.VectorTile.prototype.setPreload = function(preload) {
+  this.set(ol.layer.TileProperty.PRELOAD, preload);
+};
+
+
+/**
+ * Set whether we use interim tiles on error.
+ * @param {boolean} useInterimTilesOnError Use interim tiles on error.
+ * @observable
+ * @api
+ */
+ol.layer.VectorTile.prototype.setUseInterimTilesOnError = function(useInterimTilesOnError) {
+  this.set(
+      ol.layer.TileProperty.USE_INTERIM_TILES_ON_ERROR, useInterimTilesOnError);
+};
+
+// FIXME test, especially polygons with holes and multipolygons
+// FIXME need to handle large thick features (where pixel size matters)
+// FIXME add offset and end to ol.geom.flat.transform.transform2D?
+
+goog.provide('ol.render.canvas.Immediate');
+
+goog.require('goog.asserts');
+goog.require('goog.vec.Mat4');
+goog.require('ol.array');
+goog.require('ol.color');
+goog.require('ol.colorlike');
+goog.require('ol.extent');
+goog.require('ol.geom.GeometryType');
+goog.require('ol.geom.flat.transform');
+goog.require('ol.has');
+goog.require('ol.render.VectorContext');
+goog.require('ol.render.canvas');
+goog.require('ol.vec.Mat4');
+
+
+/**
+ * @classdesc
+ * A concrete subclass of {@link ol.render.VectorContext} that implements
+ * direct rendering of features and geometries to an HTML5 Canvas context.
+ * Instances of this class are created internally by the library and
+ * provided to application code as vectorContext member of the
+ * {@link ol.render.Event} object associated with postcompose, precompose and
+ * render events emitted by layers and maps.
+ *
+ * @constructor
+ * @extends {ol.render.VectorContext}
+ * @param {CanvasRenderingContext2D} context Context.
+ * @param {number} pixelRatio Pixel ratio.
+ * @param {ol.Extent} extent Extent.
+ * @param {goog.vec.Mat4.Number} transform Transform.
+ * @param {number} viewRotation View rotation.
+ * @struct
+ */
+ol.render.canvas.Immediate = function(context, pixelRatio, extent, transform, viewRotation) {
+  ol.render.VectorContext.call(this);
+
+  /**
+   * @private
+   * @type {CanvasRenderingContext2D}
+   */
+  this.context_ = context;
+
+  /**
+   * @private
+   * @type {number}
+   */
+  this.pixelRatio_ = pixelRatio;
+
+  /**
+   * @private
+   * @type {ol.Extent}
+   */
+  this.extent_ = extent;
+
+  /**
+   * @private
+   * @type {goog.vec.Mat4.Number}
+   */
+  this.transform_ = transform;
+
+  /**
+   * @private
+   * @type {number}
+   */
+  this.viewRotation_ = viewRotation;
+
+  /**
+   * @private
+   * @type {?ol.CanvasFillState}
+   */
+  this.contextFillState_ = null;
+
+  /**
+   * @private
+   * @type {?ol.CanvasStrokeState}
+   */
+  this.contextStrokeState_ = null;
+
+  /**
+   * @private
+   * @type {?ol.CanvasTextState}
+   */
+  this.contextTextState_ = null;
+
+  /**
+   * @private
+   * @type {?ol.CanvasFillState}
+   */
+  this.fillState_ = null;
+
+  /**
+   * @private
+   * @type {?ol.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 {boolean}
+   */
+  this.imageRotateWithView_ = false;
+
+  /**
+   * @private
+   * @type {number}
+   */
+  this.imageRotation_ = 0;
+
+  /**
+   * @private
+   * @type {number}
+   */
+  this.imageScale_ = 0;
+
+  /**
+   * @private
+   * @type {boolean}
+   */
+  this.imageSnapToPixel_ = false;
+
+  /**
+   * @private
+   * @type {number}
+   */
+  this.imageWidth_ = 0;
+
+  /**
+   * @private
+   * @type {string}
+   */
+  this.text_ = '';
+
+  /**
+   * @private
+   * @type {number}
+   */
+  this.textOffsetX_ = 0;
+
+  /**
+   * @private
+   * @type {number}
+   */
+  this.textOffsetY_ = 0;
+
+  /**
+   * @private
+   * @type {number}
+   */
+  this.textRotation_ = 0;
+
+  /**
+   * @private
+   * @type {number}
+   */
+  this.textScale_ = 0;
+
+  /**
+   * @private
+   * @type {?ol.CanvasFillState}
+   */
+  this.textFillState_ = null;
+
+  /**
+   * @private
+   * @type {?ol.CanvasStrokeState}
+   */
+  this.textStrokeState_ = null;
+
+  /**
+   * @private
+   * @type {?ol.CanvasTextState}
+   */
+  this.textState_ = null;
+
+  /**
+   * @private
+   * @type {Array.<number>}
+   */
+  this.pixelCoordinates_ = [];
+
+  /**
+   * @private
+   * @type {!goog.vec.Mat4.Number}
+   */
+  this.tmpLocalTransform_ = goog.vec.Mat4.createNumber();
+
+};
+ol.inherits(ol.render.canvas.Immediate, ol.render.VectorContext);
+
+
+/**
+ * @param {Array.<number>} flatCoordinates Flat coordinates.
+ * @param {number} offset Offset.
+ * @param {number} end End.
+ * @param {number} stride Stride.
+ * @private
+ */
+ol.render.canvas.Immediate.prototype.drawImages_ = function(flatCoordinates, offset, end, stride) {
+  if (!this.image_) {
+    return;
+  }
+  goog.asserts.assert(offset === 0, 'offset should be 0');
+  goog.asserts.assert(end == flatCoordinates.length,
+      'end should be equal to the length of flatCoordinates');
+  var pixelCoordinates = ol.geom.flat.transform.transform2D(
+      flatCoordinates, offset, end, 2, this.transform_,
+      this.pixelCoordinates_);
+  var context = this.context_;
+  var localTransform = this.tmpLocalTransform_;
+  var alpha = context.globalAlpha;
+  if (this.imageOpacity_ != 1) {
+    context.globalAlpha = alpha * this.imageOpacity_;
+  }
+  var rotation = this.imageRotation_;
+  if (this.imageRotateWithView_) {
+    rotation += this.viewRotation_;
+  }
+  var i, ii;
+  for (i = 0, ii = pixelCoordinates.length; i < ii; i += 2) {
+    var x = pixelCoordinates[i] - this.imageAnchorX_;
+    var y = pixelCoordinates[i + 1] - this.imageAnchorY_;
+    if (this.imageSnapToPixel_) {
+      x = Math.round(x);
+      y = Math.round(y);
+    }
+    if (rotation !== 0 || this.imageScale_ != 1) {
+      var centerX = x + this.imageAnchorX_;
+      var centerY = y + this.imageAnchorY_;
+      ol.vec.Mat4.makeTransform2D(localTransform,
+          centerX, centerY, this.imageScale_, this.imageScale_,
+          rotation, -centerX, -centerY);
+      context.setTransform(
+          goog.vec.Mat4.getElement(localTransform, 0, 0),
+          goog.vec.Mat4.getElement(localTransform, 1, 0),
+          goog.vec.Mat4.getElement(localTransform, 0, 1),
+          goog.vec.Mat4.getElement(localTransform, 1, 1),
+          goog.vec.Mat4.getElement(localTransform, 0, 3),
+          goog.vec.Mat4.getElement(localTransform, 1, 3));
+    }
+    context.drawImage(this.image_, this.imageOriginX_, this.imageOriginY_,
+        this.imageWidth_, this.imageHeight_, x, y,
+        this.imageWidth_, this.imageHeight_);
+  }
+  if (rotation !== 0 || this.imageScale_ != 1) {
+    context.setTransform(1, 0, 0, 1, 0, 0);
+  }
+  if (this.imageOpacity_ != 1) {
+    context.globalAlpha = alpha;
+  }
+};
+
+
+/**
+ * @param {Array.<number>} flatCoordinates Flat coordinates.
+ * @param {number} offset Offset.
+ * @param {number} end End.
+ * @param {number} stride Stride.
+ * @private
+ */
+ol.render.canvas.Immediate.prototype.drawText_ = function(flatCoordinates, offset, end, stride) {
+  if (!this.textState_ || this.text_ === '') {
+    return;
+  }
+  if (this.textFillState_) {
+    this.setContextFillState_(this.textFillState_);
+  }
+  if (this.textStrokeState_) {
+    this.setContextStrokeState_(this.textStrokeState_);
+  }
+  this.setContextTextState_(this.textState_);
+  goog.asserts.assert(offset === 0, 'offset should be 0');
+  goog.asserts.assert(end == flatCoordinates.length,
+      'end should be equal to the length of flatCoordinates');
+  var pixelCoordinates = ol.geom.flat.transform.transform2D(
+      flatCoordinates, offset, end, stride, this.transform_,
+      this.pixelCoordinates_);
+  var context = this.context_;
+  for (; offset < end; offset += stride) {
+    var x = pixelCoordinates[offset] + this.textOffsetX_;
+    var y = pixelCoordinates[offset + 1] + this.textOffsetY_;
+    if (this.textRotation_ !== 0 || this.textScale_ != 1) {
+      var localTransform = ol.vec.Mat4.makeTransform2D(this.tmpLocalTransform_,
+          x, y, this.textScale_, this.textScale_, this.textRotation_, -x, -y);
+      context.setTransform(
+          goog.vec.Mat4.getElement(localTransform, 0, 0),
+          goog.vec.Mat4.getElement(localTransform, 1, 0),
+          goog.vec.Mat4.getElement(localTransform, 0, 1),
+          goog.vec.Mat4.getElement(localTransform, 1, 1),
+          goog.vec.Mat4.getElement(localTransform, 0, 3),
+          goog.vec.Mat4.getElement(localTransform, 1, 3));
+    }
+    if (this.textStrokeState_) {
+      context.strokeText(this.text_, x, y);
+    }
+    if (this.textFillState_) {
+      context.fillText(this.text_, x, y);
+    }
+  }
+  if (this.textRotation_ !== 0 || this.textScale_ != 1) {
+    context.setTransform(1, 0, 0, 1, 0, 0);
+  }
+};
+
+
+/**
+ * @param {Array.<number>} flatCoordinates Flat coordinates.
+ * @param {number} offset Offset.
+ * @param {number} end End.
+ * @param {number} stride Stride.
+ * @param {boolean} close Close.
+ * @private
+ * @return {number} end End.
+ */
+ol.render.canvas.Immediate.prototype.moveToLineTo_ = function(flatCoordinates, offset, end, stride, close) {
+  var context = this.context_;
+  var pixelCoordinates = ol.geom.flat.transform.transform2D(
+      flatCoordinates, offset, end, stride, this.transform_,
+      this.pixelCoordinates_);
+  context.moveTo(pixelCoordinates[0], pixelCoordinates[1]);
+  var 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.
+ */
+ol.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 {ol.geom.Circle} geometry Circle geometry.
+ * @api
+ */
+ol.render.canvas.Immediate.prototype.drawCircle = function(geometry) {
+  if (!ol.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 = ol.geom.transformSimpleGeometry2D(
+        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 {ol.style.Style} style The rendering style.
+ * @api
+ */
+ol.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 ol.render.canvas.Immediate#setStyle} first to set the rendering style.
+ *
+ * @param {ol.geom.Geometry|ol.render.Feature} geometry The geometry to render.
+ * @api
+ */
+ol.render.canvas.Immediate.prototype.drawGeometry = function(geometry) {
+  var type = geometry.getType();
+  switch (type) {
+    case ol.geom.GeometryType.POINT:
+      this.drawPoint(/** @type {ol.geom.Point} */ (geometry));
+      break;
+    case ol.geom.GeometryType.LINE_STRING:
+      this.drawLineString(/** @type {ol.geom.LineString} */ (geometry));
+      break;
+    case ol.geom.GeometryType.POLYGON:
+      this.drawPolygon(/** @type {ol.geom.Polygon} */ (geometry));
+      break;
+    case ol.geom.GeometryType.MULTI_POINT:
+      this.drawMultiPoint(/** @type {ol.geom.MultiPoint} */ (geometry));
+      break;
+    case ol.geom.GeometryType.MULTI_LINE_STRING:
+      this.drawMultiLineString(/** @type {ol.geom.MultiLineString} */ (geometry));
+      break;
+    case ol.geom.GeometryType.MULTI_POLYGON:
+      this.drawMultiPolygon(/** @type {ol.geom.MultiPolygon} */ (geometry));
+      break;
+    case ol.geom.GeometryType.GEOMETRY_COLLECTION:
+      this.drawGeometryCollection(/** @type {ol.geom.GeometryCollection} */ (geometry));
+      break;
+    case ol.geom.GeometryType.CIRCLE:
+      this.drawCircle(/** @type {ol.geom.Circle} */ (geometry));
+      break;
+    default:
+      goog.asserts.fail('Unsupported geometry type: ' + type);
+  }
+};
+
+
+/**
+ * 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 ol.layer.Vector} instead.
+ *
+ * @param {ol.Feature} feature Feature.
+ * @param {ol.style.Style} style Style.
+ * @api
+ */
+ol.render.canvas.Immediate.prototype.drawFeature = function(feature, style) {
+  var geometry = style.getGeometryFunction()(feature);
+  if (!geometry ||
+      !ol.extent.intersects(this.extent_, geometry.getExtent())) {
+    return;
+  }
+  this.setStyle(style);
+  goog.asserts.assert(geometry, 'geometry must be truthy');
+  this.drawGeometry(geometry);
+};
+
+
+/**
+ * Render a GeometryCollection to the canvas.  Rendering is immediate and
+ * uses the current styles appropriate for each geometry in the collection.
+ *
+ * @param {ol.geom.GeometryCollection} geometry Geometry collection.
+ */
+ol.render.canvas.Immediate.prototype.drawGeometryCollection = 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 {ol.geom.Point|ol.render.Feature} geometry Point geometry.
+ */
+ol.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 {ol.geom.MultiPoint|ol.render.Feature} geometry MultiPoint geometry.
+ */
+ol.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 {ol.geom.LineString|ol.render.Feature} geometry LineString geometry.
+ */
+ol.render.canvas.Immediate.prototype.drawLineString = function(geometry) {
+  if (!ol.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 {ol.geom.MultiLineString|ol.render.Feature} geometry MultiLineString
+ *     geometry.
+ */
+ol.render.canvas.Immediate.prototype.drawMultiLineString = function(geometry) {
+  var geometryExtent = geometry.getExtent();
+  if (!ol.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 Polygon geometry into the canvas.  Rendering is immediate and uses
+ * the current style.
+ *
+ * @param {ol.geom.Polygon|ol.render.Feature} geometry Polygon geometry.
+ */
+ol.render.canvas.Immediate.prototype.drawPolygon = function(geometry) {
+  if (!ol.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 MultiPolygon geometry into the canvas.  Rendering is immediate and
+ * uses the current style.
+ * @param {ol.geom.MultiPolygon} geometry MultiPolygon geometry.
+ */
+ol.render.canvas.Immediate.prototype.drawMultiPolygon = function(geometry) {
+  if (!ol.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;
+    for (i = 0, ii = endss.length; i < ii; ++i) {
+      var ends = endss[i];
+      context.beginPath();
+      offset = this.drawRings_(flatCoordinates, offset, ends, stride);
+      if (this.fillState_) {
+        context.fill();
+      }
+      if (this.strokeState_) {
+        context.stroke();
+      }
+    }
+  }
+  if (this.text_ !== '') {
+    var flatInteriorPoints = geometry.getFlatInteriorPoints();
+    this.drawText_(flatInteriorPoints, 0, flatInteriorPoints.length, 2);
+  }
+};
+
+
+/**
+ * @param {ol.CanvasFillState} fillState Fill state.
+ * @private
+ */
+ol.render.canvas.Immediate.prototype.setContextFillState_ = function(fillState) {
+  var context = this.context_;
+  var contextFillState = this.contextFillState_;
+  if (!contextFillState) {
+    context.fillStyle = fillState.fillStyle;
+    this.contextFillState_ = {
+      fillStyle: fillState.fillStyle
+    };
+  } else {
+    if (contextFillState.fillStyle != fillState.fillStyle) {
+      contextFillState.fillStyle = context.fillStyle = fillState.fillStyle;
+    }
+  }
+};
+
+
+/**
+ * @param {ol.CanvasStrokeState} strokeState Stroke state.
+ * @private
+ */
+ol.render.canvas.Immediate.prototype.setContextStrokeState_ = function(strokeState) {
+  var context = this.context_;
+  var contextStrokeState = this.contextStrokeState_;
+  if (!contextStrokeState) {
+    context.lineCap = strokeState.lineCap;
+    if (ol.has.CANVAS_LINE_DASH) {
+      context.setLineDash(strokeState.lineDash);
+    }
+    context.lineJoin = strokeState.lineJoin;
+    context.lineWidth = strokeState.lineWidth;
+    context.miterLimit = strokeState.miterLimit;
+    context.strokeStyle = strokeState.strokeStyle;
+    this.contextStrokeState_ = {
+      lineCap: strokeState.lineCap,
+      lineDash: strokeState.lineDash,
+      lineJoin: strokeState.lineJoin,
+      lineWidth: strokeState.lineWidth,
+      miterLimit: strokeState.miterLimit,
+      strokeStyle: strokeState.strokeStyle
+    };
+  } else {
+    if (contextStrokeState.lineCap != strokeState.lineCap) {
+      contextStrokeState.lineCap = context.lineCap = strokeState.lineCap;
+    }
+    if (ol.has.CANVAS_LINE_DASH) {
+      if (!ol.array.equals(
+          contextStrokeState.lineDash, strokeState.lineDash)) {
+        context.setLineDash(contextStrokeState.lineDash = strokeState.lineDash);
+      }
+    }
+    if (contextStrokeState.lineJoin != strokeState.lineJoin) {
+      contextStrokeState.lineJoin = context.lineJoin = strokeState.lineJoin;
+    }
+    if (contextStrokeState.lineWidth != strokeState.lineWidth) {
+      contextStrokeState.lineWidth = context.lineWidth = strokeState.lineWidth;
+    }
+    if (contextStrokeState.miterLimit != strokeState.miterLimit) {
+      contextStrokeState.miterLimit = context.miterLimit =
+          strokeState.miterLimit;
+    }
+    if (contextStrokeState.strokeStyle != strokeState.strokeStyle) {
+      contextStrokeState.strokeStyle = context.strokeStyle =
+          strokeState.strokeStyle;
+    }
+  }
+};
+
+
+/**
+ * @param {ol.CanvasTextState} textState Text state.
+ * @private
+ */
+ol.render.canvas.Immediate.prototype.setContextTextState_ = function(textState) {
+  var context = this.context_;
+  var contextTextState = this.contextTextState_;
+  if (!contextTextState) {
+    context.font = textState.font;
+    context.textAlign = textState.textAlign;
+    context.textBaseline = textState.textBaseline;
+    this.contextTextState_ = {
+      font: textState.font,
+      textAlign: textState.textAlign,
+      textBaseline: textState.textBaseline
+    };
+  } else {
+    if (contextTextState.font != textState.font) {
+      contextTextState.font = context.font = textState.font;
+    }
+    if (contextTextState.textAlign != textState.textAlign) {
+      contextTextState.textAlign = context.textAlign = textState.textAlign;
+    }
+    if (contextTextState.textBaseline != textState.textBaseline) {
+      contextTextState.textBaseline = context.textBaseline =
+          textState.textBaseline;
+    }
+  }
+};
+
+
+/**
+ * Set the fill and stroke style for subsequent draw operations.  To clear
+ * either fill or stroke styles, pass null for the appropriate parameter.
+ *
+ * @param {ol.style.Fill} fillStyle Fill style.
+ * @param {ol.style.Stroke} strokeStyle Stroke style.
+ */
+ol.render.canvas.Immediate.prototype.setFillStrokeStyle = function(fillStyle, strokeStyle) {
+  if (!fillStyle) {
+    this.fillState_ = null;
+  } else {
+    var fillStyleColor = fillStyle.getColor();
+    this.fillState_ = {
+      fillStyle: ol.colorlike.asColorLike(fillStyleColor ?
+          fillStyleColor : ol.render.canvas.defaultFillStyle)
+    };
+  }
+  if (!strokeStyle) {
+    this.strokeState_ = null;
+  } else {
+    var strokeStyleColor = strokeStyle.getColor();
+    var strokeStyleLineCap = strokeStyle.getLineCap();
+    var strokeStyleLineDash = strokeStyle.getLineDash();
+    var strokeStyleLineJoin = strokeStyle.getLineJoin();
+    var strokeStyleWidth = strokeStyle.getWidth();
+    var strokeStyleMiterLimit = strokeStyle.getMiterLimit();
+    this.strokeState_ = {
+      lineCap: strokeStyleLineCap !== undefined ?
+          strokeStyleLineCap : ol.render.canvas.defaultLineCap,
+      lineDash: strokeStyleLineDash ?
+          strokeStyleLineDash : ol.render.canvas.defaultLineDash,
+      lineJoin: strokeStyleLineJoin !== undefined ?
+          strokeStyleLineJoin : ol.render.canvas.defaultLineJoin,
+      lineWidth: this.pixelRatio_ * (strokeStyleWidth !== undefined ?
+          strokeStyleWidth : ol.render.canvas.defaultLineWidth),
+      miterLimit: strokeStyleMiterLimit !== undefined ?
+          strokeStyleMiterLimit : ol.render.canvas.defaultMiterLimit,
+      strokeStyle: ol.color.asString(strokeStyleColor ?
+          strokeStyleColor : ol.render.canvas.defaultStrokeStyle)
+    };
+  }
+};
+
+
+/**
+ * Set the image style for subsequent draw operations.  Pass null to remove
+ * the image style.
+ *
+ * @param {ol.style.Image} imageStyle Image style.
+ */
+ol.render.canvas.Immediate.prototype.setImageStyle = function(imageStyle) {
+  if (!imageStyle) {
+    this.image_ = null;
+  } else {
+    var imageAnchor = imageStyle.getAnchor();
+    // FIXME pixel ratio
+    var imageImage = imageStyle.getImage(1);
+    var imageOrigin = imageStyle.getOrigin();
+    var imageSize = imageStyle.getSize();
+    goog.asserts.assert(imageAnchor, 'imageAnchor must be truthy');
+    goog.asserts.assert(imageImage, 'imageImage must be truthy');
+    goog.asserts.assert(imageOrigin, 'imageOrigin must be truthy');
+    goog.asserts.assert(imageSize, 'imageSize must be truthy');
+    this.imageAnchorX_ = imageAnchor[0];
+    this.imageAnchorY_ = imageAnchor[1];
+    this.imageHeight_ = imageSize[1];
+    this.image_ = imageImage;
+    this.imageOpacity_ = imageStyle.getOpacity();
+    this.imageOriginX_ = imageOrigin[0];
+    this.imageOriginY_ = imageOrigin[1];
+    this.imageRotateWithView_ = imageStyle.getRotateWithView();
+    this.imageRotation_ = imageStyle.getRotation();
+    this.imageScale_ = imageStyle.getScale();
+    this.imageSnapToPixel_ = imageStyle.getSnapToPixel();
+    this.imageWidth_ = imageSize[0];
+  }
+};
+
+
+/**
+ * Set the text style for subsequent draw operations.  Pass null to
+ * remove the text style.
+ *
+ * @param {ol.style.Text} textStyle Text style.
+ */
+ol.render.canvas.Immediate.prototype.setTextStyle = function(textStyle) {
+  if (!textStyle) {
+    this.text_ = '';
+  } else {
+    var textFillStyle = textStyle.getFill();
+    if (!textFillStyle) {
+      this.textFillState_ = null;
+    } else {
+      var textFillStyleColor = textFillStyle.getColor();
+      this.textFillState_ = {
+        fillStyle: ol.colorlike.asColorLike(textFillStyleColor ?
+            textFillStyleColor : ol.render.canvas.defaultFillStyle)
+      };
+    }
+    var textStrokeStyle = textStyle.getStroke();
+    if (!textStrokeStyle) {
+      this.textStrokeState_ = null;
+    } else {
+      var textStrokeStyleColor = textStrokeStyle.getColor();
+      var textStrokeStyleLineCap = textStrokeStyle.getLineCap();
+      var textStrokeStyleLineDash = textStrokeStyle.getLineDash();
+      var textStrokeStyleLineJoin = textStrokeStyle.getLineJoin();
+      var textStrokeStyleWidth = textStrokeStyle.getWidth();
+      var textStrokeStyleMiterLimit = textStrokeStyle.getMiterLimit();
+      this.textStrokeState_ = {
+        lineCap: textStrokeStyleLineCap !== undefined ?
+            textStrokeStyleLineCap : ol.render.canvas.defaultLineCap,
+        lineDash: textStrokeStyleLineDash ?
+            textStrokeStyleLineDash : ol.render.canvas.defaultLineDash,
+        lineJoin: textStrokeStyleLineJoin !== undefined ?
+            textStrokeStyleLineJoin : ol.render.canvas.defaultLineJoin,
+        lineWidth: textStrokeStyleWidth !== undefined ?
+            textStrokeStyleWidth : ol.render.canvas.defaultLineWidth,
+        miterLimit: textStrokeStyleMiterLimit !== undefined ?
+            textStrokeStyleMiterLimit : ol.render.canvas.defaultMiterLimit,
+        strokeStyle: ol.color.asString(textStrokeStyleColor ?
+            textStrokeStyleColor : ol.render.canvas.defaultStrokeStyle)
+      };
+    }
+    var textFont = textStyle.getFont();
+    var textOffsetX = textStyle.getOffsetX();
+    var textOffsetY = textStyle.getOffsetY();
+    var textRotation = textStyle.getRotation();
+    var textScale = textStyle.getScale();
+    var textText = textStyle.getText();
+    var textTextAlign = textStyle.getTextAlign();
+    var textTextBaseline = textStyle.getTextBaseline();
+    this.textState_ = {
+      font: textFont !== undefined ?
+          textFont : ol.render.canvas.defaultFont,
+      textAlign: textTextAlign !== undefined ?
+          textTextAlign : ol.render.canvas.defaultTextAlign,
+      textBaseline: textTextBaseline !== undefined ?
+          textTextBaseline : ol.render.canvas.defaultTextBaseline
+    };
+    this.text_ = textText !== undefined ? textText : '';
+    this.textOffsetX_ =
+        textOffsetX !== undefined ? (this.pixelRatio_ * textOffsetX) : 0;
+    this.textOffsetY_ =
+        textOffsetY !== undefined ? (this.pixelRatio_ * textOffsetY) : 0;
+    this.textRotation_ = textRotation !== undefined ? textRotation : 0;
+    this.textScale_ = this.pixelRatio_ * (textScale !== undefined ?
+        textScale : 1);
+  }
+};
+
+goog.provide('ol.renderer.canvas.Layer');
+
+goog.require('goog.asserts');
+goog.require('goog.vec.Mat4');
+goog.require('ol.extent');
+goog.require('ol.layer.Layer');
+goog.require('ol.render.Event');
+goog.require('ol.render.EventType');
+goog.require('ol.render.canvas');
+goog.require('ol.render.canvas.Immediate');
+goog.require('ol.renderer.Layer');
+goog.require('ol.vec.Mat4');
+
+
+/**
+ * @constructor
+ * @extends {ol.renderer.Layer}
+ * @param {ol.layer.Layer} layer Layer.
+ */
+ol.renderer.canvas.Layer = function(layer) {
+
+  ol.renderer.Layer.call(this, layer);
+
+  /**
+   * @private
+   * @type {!goog.vec.Mat4.Number}
+   */
+  this.transform_ = goog.vec.Mat4.createNumber();
+
+};
+ol.inherits(ol.renderer.canvas.Layer, ol.renderer.Layer);
+
+
+/**
+ * @param {olx.FrameState} frameState Frame state.
+ * @param {ol.LayerState} layerState Layer state.
+ * @param {CanvasRenderingContext2D} context Context.
+ */
+ol.renderer.canvas.Layer.prototype.composeFrame = function(frameState, layerState, context) {
+
+  this.dispatchPreComposeEvent(context, frameState);
+
+  var image = this.getImage();
+  if (image) {
+
+    // clipped rendering if layer extent is set
+    var extent = layerState.extent;
+    var clipped = extent !== undefined;
+    if (clipped) {
+      goog.asserts.assert(extent !== undefined,
+          'layerState extent is defined');
+      var pixelRatio = frameState.pixelRatio;
+      var width = frameState.size[0] * pixelRatio;
+      var height = frameState.size[1] * pixelRatio;
+      var rotation = frameState.viewState.rotation;
+      var topLeft = ol.extent.getTopLeft(extent);
+      var topRight = ol.extent.getTopRight(extent);
+      var bottomRight = ol.extent.getBottomRight(extent);
+      var bottomLeft = ol.extent.getBottomLeft(extent);
+
+      ol.vec.Mat4.multVec2(frameState.coordinateToPixelMatrix,
+          topLeft, topLeft);
+      ol.vec.Mat4.multVec2(frameState.coordinateToPixelMatrix,
+          topRight, topRight);
+      ol.vec.Mat4.multVec2(frameState.coordinateToPixelMatrix,
+          bottomRight, bottomRight);
+      ol.vec.Mat4.multVec2(frameState.coordinateToPixelMatrix,
+          bottomLeft, bottomLeft);
+
+      context.save();
+      ol.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();
+      ol.render.canvas.rotateAtOffset(context, rotation, width / 2, height / 2);
+    }
+
+    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 = goog.vec.Mat4.getElement(imageTransform, 0, 3);
+    var dy = goog.vec.Mat4.getElement(imageTransform, 1, 3);
+    var dw = image.width * goog.vec.Mat4.getElement(imageTransform, 0, 0);
+    var dh = image.height * goog.vec.Mat4.getElement(imageTransform, 1, 1);
+    context.drawImage(image, 0, 0, +image.width, +image.height,
+        Math.round(dx), Math.round(dy), Math.round(dw), Math.round(dh));
+    context.globalAlpha = alpha;
+
+    if (clipped) {
+      context.restore();
+    }
+  }
+
+  this.dispatchPostComposeEvent(context, frameState);
+
+};
+
+
+/**
+ * @param {ol.render.EventType} type Event type.
+ * @param {CanvasRenderingContext2D} context Context.
+ * @param {olx.FrameState} frameState Frame state.
+ * @param {goog.vec.Mat4.Number=} opt_transform Transform.
+ * @private
+ */
+ol.renderer.canvas.Layer.prototype.dispatchComposeEvent_ = function(type, context, frameState, opt_transform) {
+  var layer = this.getLayer();
+  if (layer.hasListener(type)) {
+    var width = frameState.size[0] * frameState.pixelRatio;
+    var height = frameState.size[1] * frameState.pixelRatio;
+    var rotation = frameState.viewState.rotation;
+    ol.render.canvas.rotateAtOffset(context, -rotation, width / 2, height / 2);
+    var transform = opt_transform !== undefined ?
+        opt_transform : this.getTransform(frameState, 0);
+    var render = new ol.render.canvas.Immediate(
+        context, frameState.pixelRatio, frameState.extent, transform,
+        frameState.viewState.rotation);
+    var composeEvent = new ol.render.Event(type, layer, render, frameState,
+        context, null);
+    layer.dispatchEvent(composeEvent);
+    ol.render.canvas.rotateAtOffset(context, rotation, width / 2, height / 2);
+  }
+};
+
+
+/**
+ * @param {CanvasRenderingContext2D} context Context.
+ * @param {olx.FrameState} frameState Frame state.
+ * @param {goog.vec.Mat4.Number=} opt_transform Transform.
+ * @protected
+ */
+ol.renderer.canvas.Layer.prototype.dispatchPostComposeEvent = function(context, frameState, opt_transform) {
+  this.dispatchComposeEvent_(ol.render.EventType.POSTCOMPOSE, context,
+      frameState, opt_transform);
+};
+
+
+/**
+ * @param {CanvasRenderingContext2D} context Context.
+ * @param {olx.FrameState} frameState Frame state.
+ * @param {goog.vec.Mat4.Number=} opt_transform Transform.
+ * @protected
+ */
+ol.renderer.canvas.Layer.prototype.dispatchPreComposeEvent = function(context, frameState, opt_transform) {
+  this.dispatchComposeEvent_(ol.render.EventType.PRECOMPOSE, context,
+      frameState, opt_transform);
+};
+
+
+/**
+ * @param {CanvasRenderingContext2D} context Context.
+ * @param {olx.FrameState} frameState Frame state.
+ * @param {goog.vec.Mat4.Number=} opt_transform Transform.
+ * @protected
+ */
+ol.renderer.canvas.Layer.prototype.dispatchRenderEvent = function(context, frameState, opt_transform) {
+  this.dispatchComposeEvent_(ol.render.EventType.RENDER, context,
+      frameState, opt_transform);
+};
+
+
+/**
+ * @return {HTMLCanvasElement|HTMLVideoElement|Image} Canvas.
+ */
+ol.renderer.canvas.Layer.prototype.getImage = goog.abstractMethod;
+
+
+/**
+ * @return {!goog.vec.Mat4.Number} Image transform.
+ */
+ol.renderer.canvas.Layer.prototype.getImageTransform = goog.abstractMethod;
+
+
+/**
+ * @param {olx.FrameState} frameState Frame state.
+ * @param {number} offsetX Offset on the x-axis in view coordinates.
+ * @protected
+ * @return {!goog.vec.Mat4.Number} Transform.
+ */
+ol.renderer.canvas.Layer.prototype.getTransform = function(frameState, offsetX) {
+  var viewState = frameState.viewState;
+  var pixelRatio = frameState.pixelRatio;
+  return ol.vec.Mat4.makeTransform2D(this.transform_,
+      pixelRatio * frameState.size[0] / 2,
+      pixelRatio * frameState.size[1] / 2,
+      pixelRatio / viewState.resolution,
+      -pixelRatio / viewState.resolution,
+      -viewState.rotation,
+      -viewState.center[0] + offsetX,
+      -viewState.center[1]);
+};
+
+
+/**
+ * @param {olx.FrameState} frameState Frame state.
+ * @param {ol.LayerState} layerState Layer state.
+ * @return {boolean} whether composeFrame should be called.
+ */
+ol.renderer.canvas.Layer.prototype.prepareFrame = goog.abstractMethod;
+
+
+/**
+ * @param {ol.Pixel} pixelOnMap Pixel.
+ * @param {goog.vec.Mat4.Number} imageTransformInv The transformation matrix
+ *        to convert from a map pixel to a canvas pixel.
+ * @return {ol.Pixel} The pixel.
+ * @protected
+ */
+ol.renderer.canvas.Layer.prototype.getPixelOnCanvas = function(pixelOnMap, imageTransformInv) {
+  var pixelOnCanvas = [0, 0];
+  ol.vec.Mat4.multVec2(imageTransformInv, pixelOnMap, pixelOnCanvas);
+  return pixelOnCanvas;
+};
+
+goog.provide('ol.render.IReplayGroup');
+
+goog.require('ol.render.VectorContext');
+
+
+/**
+ * @enum {string}
+ */
+ol.render.ReplayType = {
+  IMAGE: 'Image',
+  LINE_STRING: 'LineString',
+  POLYGON: 'Polygon',
+  TEXT: 'Text'
+};
+
+
+/**
+ * @const
+ * @type {Array.<ol.render.ReplayType>}
+ */
+ol.render.REPLAY_ORDER = [
+  ol.render.ReplayType.POLYGON,
+  ol.render.ReplayType.LINE_STRING,
+  ol.render.ReplayType.IMAGE,
+  ol.render.ReplayType.TEXT
+];
+
+
+/**
+ * @interface
+ */
+ol.render.IReplayGroup = function() {
+};
+
+
+/* eslint-disable valid-jsdoc */
+// TODO: enable valid-jsdoc for @interface methods when this issue is resolved
+// https://github.com/eslint/eslint/issues/4887
+
+
+/**
+ * @param {number|undefined} zIndex Z index.
+ * @param {ol.render.ReplayType} replayType Replay type.
+ * @return {ol.render.VectorContext} Replay.
+ */
+ol.render.IReplayGroup.prototype.getReplay = function(zIndex, replayType) {
+};
+
+
+/**
+ * @return {boolean} Is empty.
+ */
+ol.render.IReplayGroup.prototype.isEmpty = function() {
+};
+
+// FIXME add option to apply snapToPixel to all coordinates?
+// FIXME can eliminate empty set styles and strokes (when all geoms skipped)
+
+goog.provide('ol.render.canvas.ImageReplay');
+goog.provide('ol.render.canvas.LineStringReplay');
+goog.provide('ol.render.canvas.PolygonReplay');
+goog.provide('ol.render.canvas.Replay');
+goog.provide('ol.render.canvas.ReplayGroup');
+goog.provide('ol.render.canvas.TextReplay');
+
+goog.require('goog.asserts');
+goog.require('goog.vec.Mat4');
+goog.require('ol');
+goog.require('ol.array');
+goog.require('ol.color');
+goog.require('ol.colorlike');
+goog.require('ol.dom');
+goog.require('ol.extent');
+goog.require('ol.extent.Relationship');
+goog.require('ol.geom.flat.simplify');
+goog.require('ol.geom.flat.transform');
+goog.require('ol.has');
+goog.require('ol.object');
+goog.require('ol.render.IReplayGroup');
+goog.require('ol.render.VectorContext');
+goog.require('ol.render.canvas');
+goog.require('ol.vec.Mat4');
+
+
+/**
+ * @enum {number}
+ */
+ol.render.canvas.Instruction = {
+  BEGIN_GEOMETRY: 0,
+  BEGIN_PATH: 1,
+  CIRCLE: 2,
+  CLOSE_PATH: 3,
+  DRAW_IMAGE: 4,
+  DRAW_TEXT: 5,
+  END_GEOMETRY: 6,
+  FILL: 7,
+  MOVE_TO_LINE_TO: 8,
+  SET_FILL_STYLE: 9,
+  SET_STROKE_STYLE: 10,
+  SET_TEXT_STYLE: 11,
+  STROKE: 12
+};
+
+
+/**
+ * @constructor
+ * @extends {ol.render.VectorContext}
+ * @param {number} tolerance Tolerance.
+ * @param {ol.Extent} maxExtent Maximum extent.
+ * @param {number} resolution Resolution.
+ * @protected
+ * @struct
+ */
+ol.render.canvas.Replay = function(tolerance, maxExtent, resolution) {
+  ol.render.VectorContext.call(this);
+
+  /**
+   * @protected
+   * @type {number}
+   */
+  this.tolerance = tolerance;
+
+  /**
+   * @protected
+   * @const
+   * @type {ol.Extent}
+   */
+  this.maxExtent = maxExtent;
+
+  /**
+   * @private
+   * @type {ol.Extent}
+   */
+  this.bufferedMaxExtent_ = null;
+
+  /**
+   * @protected
+   * @type {number}
+   */
+  this.maxLineWidth = 0;
+
+  /**
+   * @protected
+   * @const
+   * @type {number}
+   */
+  this.resolution = resolution;
+
+  /**
+   * @private
+   * @type {Array.<*>}
+   */
+  this.beginGeometryInstruction1_ = null;
+
+  /**
+   * @private
+   * @type {Array.<*>}
+   */
+  this.beginGeometryInstruction2_ = null;
+
+  /**
+   * @protected
+   * @type {Array.<*>}
+   */
+  this.instructions = [];
+
+  /**
+   * @protected
+   * @type {Array.<number>}
+   */
+  this.coordinates = [];
+
+  /**
+   * @private
+   * @type {goog.vec.Mat4.Number}
+   */
+  this.renderedTransform_ = goog.vec.Mat4.createNumber();
+
+  /**
+   * @protected
+   * @type {Array.<*>}
+   */
+  this.hitDetectionInstructions = [];
+
+  /**
+   * @private
+   * @type {Array.<number>}
+   */
+  this.pixelCoordinates_ = [];
+
+  /**
+   * @private
+   * @type {!goog.vec.Mat4.Number}
+   */
+  this.tmpLocalTransform_ = goog.vec.Mat4.createNumber();
+
+  /**
+   * @private
+   * @type {!goog.vec.Mat4.Number}
+   */
+  this.tmpLocalTransformInv_ = goog.vec.Mat4.createNumber();
+};
+ol.inherits(ol.render.canvas.Replay, ol.render.VectorContext);
+
+
+/**
+ * @param {Array.<number>} flatCoordinates Flat coordinates.
+ * @param {number} offset Offset.
+ * @param {number} end End.
+ * @param {number} stride Stride.
+ * @param {boolean} close Close.
+ * @protected
+ * @return {number} My end.
+ */
+ol.render.canvas.Replay.prototype.appendFlatCoordinates = function(flatCoordinates, offset, end, stride, close) {
+
+  var myEnd = this.coordinates.length;
+  var extent = this.getBufferedMaxExtent();
+  var lastCoord = [flatCoordinates[offset], flatCoordinates[offset + 1]];
+  var nextCoord = [NaN, NaN];
+  var skipped = true;
+
+  var i, lastRel, nextRel;
+  for (i = offset + stride; i < end; i += stride) {
+    nextCoord[0] = flatCoordinates[i];
+    nextCoord[1] = flatCoordinates[i + 1];
+    nextRel = ol.extent.coordinateRelationship(extent, nextCoord);
+    if (nextRel !== lastRel) {
+      if (skipped) {
+        this.coordinates[myEnd++] = lastCoord[0];
+        this.coordinates[myEnd++] = lastCoord[1];
+      }
+      this.coordinates[myEnd++] = nextCoord[0];
+      this.coordinates[myEnd++] = nextCoord[1];
+      skipped = false;
+    } else if (nextRel === ol.extent.Relationship.INTERSECTING) {
+      this.coordinates[myEnd++] = nextCoord[0];
+      this.coordinates[myEnd++] = nextCoord[1];
+      skipped = false;
+    } else {
+      skipped = true;
+    }
+    lastCoord[0] = nextCoord[0];
+    lastCoord[1] = nextCoord[1];
+    lastRel = nextRel;
+  }
+
+  // handle case where there is only one point to append
+  if (i === offset + stride) {
+    this.coordinates[myEnd++] = lastCoord[0];
+    this.coordinates[myEnd++] = lastCoord[1];
+  }
+
+  if (close) {
+    this.coordinates[myEnd++] = flatCoordinates[offset];
+    this.coordinates[myEnd++] = flatCoordinates[offset + 1];
+  }
+  return myEnd;
+};
+
+
+/**
+ * @protected
+ * @param {ol.geom.Geometry|ol.render.Feature} geometry Geometry.
+ * @param {ol.Feature|ol.render.Feature} feature Feature.
+ */
+ol.render.canvas.Replay.prototype.beginGeometry = function(geometry, feature) {
+  this.beginGeometryInstruction1_ =
+      [ol.render.canvas.Instruction.BEGIN_GEOMETRY, feature, 0];
+  this.instructions.push(this.beginGeometryInstruction1_);
+  this.beginGeometryInstruction2_ =
+      [ol.render.canvas.Instruction.BEGIN_GEOMETRY, feature, 0];
+  this.hitDetectionInstructions.push(this.beginGeometryInstruction2_);
+};
+
+
+/**
+ * @private
+ * @param {CanvasRenderingContext2D} context Context.
+ * @param {number} pixelRatio Pixel ratio.
+ * @param {goog.vec.Mat4.Number} transform Transform.
+ * @param {number} viewRotation View rotation.
+ * @param {Object.<string, boolean>} skippedFeaturesHash Ids of features
+ *     to skip.
+ * @param {Array.<*>} instructions Instructions array.
+ * @param {function((ol.Feature|ol.render.Feature)): T|undefined}
+ *     featureCallback Feature callback.
+ * @param {ol.Extent=} opt_hitExtent Only check features that intersect this
+ *     extent.
+ * @return {T|undefined} Callback result.
+ * @template T
+ */
+ol.render.canvas.Replay.prototype.replay_ = function(
+    context, pixelRatio, transform, viewRotation, skippedFeaturesHash,
+    instructions, featureCallback, opt_hitExtent) {
+  /** @type {Array.<number>} */
+  var pixelCoordinates;
+  if (ol.vec.Mat4.equals2D(transform, this.renderedTransform_)) {
+    pixelCoordinates = this.pixelCoordinates_;
+  } else {
+    pixelCoordinates = ol.geom.flat.transform.transform2D(
+        this.coordinates, 0, this.coordinates.length, 2,
+        transform, this.pixelCoordinates_);
+    goog.vec.Mat4.setFromArray(this.renderedTransform_, transform);
+    goog.asserts.assert(pixelCoordinates === this.pixelCoordinates_,
+        'pixelCoordinates should be the same as this.pixelCoordinates_');
+  }
+  var skipFeatures = !ol.object.isEmpty(skippedFeaturesHash);
+  var i = 0; // instruction index
+  var ii = instructions.length; // end of instructions
+  var d = 0; // data index
+  var dd; // end of per-instruction data
+  var localTransform = this.tmpLocalTransform_;
+  var localTransformInv = this.tmpLocalTransformInv_;
+  var prevX, prevY, roundX, roundY;
+  while (i < ii) {
+    var instruction = instructions[i];
+    var type = /** @type {ol.render.canvas.Instruction} */ (instruction[0]);
+    var feature, fill, stroke, text, x, y;
+    switch (type) {
+      case ol.render.canvas.Instruction.BEGIN_GEOMETRY:
+        feature = /** @type {ol.Feature|ol.render.Feature} */ (instruction[1]);
+        if ((skipFeatures &&
+            skippedFeaturesHash[goog.getUid(feature).toString()]) ||
+            !feature.getGeometry()) {
+          i = /** @type {number} */ (instruction[2]);
+        } else if (opt_hitExtent !== undefined && !ol.extent.intersects(
+            opt_hitExtent, feature.getGeometry().getExtent())) {
+          i = /** @type {number} */ (instruction[2]);
+        } else {
+          ++i;
+        }
+        break;
+      case ol.render.canvas.Instruction.BEGIN_PATH:
+        context.beginPath();
+        ++i;
+        break;
+      case ol.render.canvas.Instruction.CIRCLE:
+        goog.asserts.assert(goog.isNumber(instruction[1]),
+            'second instruction should be a number');
+        d = /** @type {number} */ (instruction[1]);
+        var x1 = pixelCoordinates[d];
+        var y1 = pixelCoordinates[d + 1];
+        var x2 = pixelCoordinates[d + 2];
+        var y2 = pixelCoordinates[d + 3];
+        var dx = x2 - x1;
+        var dy = y2 - y1;
+        var r = Math.sqrt(dx * dx + dy * dy);
+        context.arc(x1, y1, r, 0, 2 * Math.PI, true);
+        ++i;
+        break;
+      case ol.render.canvas.Instruction.CLOSE_PATH:
+        context.closePath();
+        ++i;
+        break;
+      case ol.render.canvas.Instruction.DRAW_IMAGE:
+        goog.asserts.assert(goog.isNumber(instruction[1]),
+            'second instruction should be a number');
+        d = /** @type {number} */ (instruction[1]);
+        goog.asserts.assert(goog.isNumber(instruction[2]),
+            'third instruction should be a number');
+        dd = /** @type {number} */ (instruction[2]);
+        var image =  /** @type {HTMLCanvasElement|HTMLVideoElement|Image} */
+            (instruction[3]);
+        // Remaining arguments in DRAW_IMAGE are in alphabetical order
+        var anchorX = /** @type {number} */ (instruction[4]) * pixelRatio;
+        var anchorY = /** @type {number} */ (instruction[5]) * pixelRatio;
+        var height = /** @type {number} */ (instruction[6]);
+        var opacity = /** @type {number} */ (instruction[7]);
+        var originX = /** @type {number} */ (instruction[8]);
+        var originY = /** @type {number} */ (instruction[9]);
+        var rotateWithView = /** @type {boolean} */ (instruction[10]);
+        var rotation = /** @type {number} */ (instruction[11]);
+        var scale = /** @type {number} */ (instruction[12]);
+        var snapToPixel = /** @type {boolean} */ (instruction[13]);
+        var width = /** @type {number} */ (instruction[14]);
+        if (rotateWithView) {
+          rotation += viewRotation;
+        }
+        for (; d < dd; d += 2) {
+          x = pixelCoordinates[d] - anchorX;
+          y = pixelCoordinates[d + 1] - anchorY;
+          if (snapToPixel) {
+            x = Math.round(x);
+            y = Math.round(y);
+          }
+          if (scale != 1 || rotation !== 0) {
+            var centerX = x + anchorX;
+            var centerY = y + anchorY;
+            ol.vec.Mat4.makeTransform2D(
+                localTransform, centerX, centerY, scale, scale,
+                rotation, -centerX, -centerY);
+            context.transform(
+                goog.vec.Mat4.getElement(localTransform, 0, 0),
+                goog.vec.Mat4.getElement(localTransform, 1, 0),
+                goog.vec.Mat4.getElement(localTransform, 0, 1),
+                goog.vec.Mat4.getElement(localTransform, 1, 1),
+                goog.vec.Mat4.getElement(localTransform, 0, 3),
+                goog.vec.Mat4.getElement(localTransform, 1, 3));
+          }
+          var alpha = context.globalAlpha;
+          if (opacity != 1) {
+            context.globalAlpha = alpha * opacity;
+          }
+
+          var w = (width + originX > image.width) ? image.width - originX : width;
+          var h = (height + originY > image.height) ? image.height - originY : height;
+
+          context.drawImage(image, originX, originY, w, h,
+              x, y, w * pixelRatio, h * pixelRatio);
+
+          if (opacity != 1) {
+            context.globalAlpha = alpha;
+          }
+          if (scale != 1 || rotation !== 0) {
+            goog.vec.Mat4.invert(localTransform, localTransformInv);
+            context.transform(
+                goog.vec.Mat4.getElement(localTransformInv, 0, 0),
+                goog.vec.Mat4.getElement(localTransformInv, 1, 0),
+                goog.vec.Mat4.getElement(localTransformInv, 0, 1),
+                goog.vec.Mat4.getElement(localTransformInv, 1, 1),
+                goog.vec.Mat4.getElement(localTransformInv, 0, 3),
+                goog.vec.Mat4.getElement(localTransformInv, 1, 3));
+          }
+        }
+        ++i;
+        break;
+      case ol.render.canvas.Instruction.DRAW_TEXT:
+        goog.asserts.assert(goog.isNumber(instruction[1]),
+            '2nd instruction should be a number');
+        d = /** @type {number} */ (instruction[1]);
+        goog.asserts.assert(goog.isNumber(instruction[2]),
+            '3rd instruction should be a number');
+        dd = /** @type {number} */ (instruction[2]);
+        goog.asserts.assert(typeof instruction[3] === 'string',
+            '4th instruction should be a string');
+        text = /** @type {string} */ (instruction[3]);
+        goog.asserts.assert(goog.isNumber(instruction[4]),
+            '5th instruction should be a number');
+        var offsetX = /** @type {number} */ (instruction[4]) * pixelRatio;
+        goog.asserts.assert(goog.isNumber(instruction[5]),
+            '6th instruction should be a number');
+        var offsetY = /** @type {number} */ (instruction[5]) * pixelRatio;
+        goog.asserts.assert(goog.isNumber(instruction[6]),
+            '7th instruction should be a number');
+        rotation = /** @type {number} */ (instruction[6]);
+        goog.asserts.assert(goog.isNumber(instruction[7]),
+            '8th instruction should be a number');
+        scale = /** @type {number} */ (instruction[7]) * pixelRatio;
+        goog.asserts.assert(typeof instruction[8] === 'boolean',
+            '9th instruction should be a boolean');
+        fill = /** @type {boolean} */ (instruction[8]);
+        goog.asserts.assert(typeof instruction[9] === 'boolean',
+            '10th instruction should be a boolean');
+        stroke = /** @type {boolean} */ (instruction[9]);
+        for (; d < dd; d += 2) {
+          x = pixelCoordinates[d] + offsetX;
+          y = pixelCoordinates[d + 1] + offsetY;
+          if (scale != 1 || rotation !== 0) {
+            ol.vec.Mat4.makeTransform2D(
+                localTransform, x, y, scale, scale, rotation, -x, -y);
+            context.transform(
+                goog.vec.Mat4.getElement(localTransform, 0, 0),
+                goog.vec.Mat4.getElement(localTransform, 1, 0),
+                goog.vec.Mat4.getElement(localTransform, 0, 1),
+                goog.vec.Mat4.getElement(localTransform, 1, 1),
+                goog.vec.Mat4.getElement(localTransform, 0, 3),
+                goog.vec.Mat4.getElement(localTransform, 1, 3));
+          }
+
+          // Support multiple lines separated by \n
+          var lines = text.split('\n');
+          var numLines = lines.length;
+          var fontSize, lineY;
+          if (numLines > 1) {
+            // Estimate line height using width of capital M, and add padding
+            fontSize = Math.round(context.measureText('M').width * 1.5);
+            lineY = y - (((numLines - 1) / 2) * fontSize);
+          } else {
+            // No need to calculate line height/offset for a single line
+            fontSize = 0;
+            lineY = y;
+          }
+
+          for (var lineIndex = 0; lineIndex < numLines; lineIndex++) {
+            var line = lines[lineIndex];
+            if (stroke) {
+              context.strokeText(line, x, lineY);
+            }
+            if (fill) {
+              context.fillText(line, x, lineY);
+            }
+
+            // Move next line down by fontSize px
+            lineY = lineY + fontSize;
+          }
+
+          if (scale != 1 || rotation !== 0) {
+            goog.vec.Mat4.invert(localTransform, localTransformInv);
+            context.transform(
+                goog.vec.Mat4.getElement(localTransformInv, 0, 0),
+                goog.vec.Mat4.getElement(localTransformInv, 1, 0),
+                goog.vec.Mat4.getElement(localTransformInv, 0, 1),
+                goog.vec.Mat4.getElement(localTransformInv, 1, 1),
+                goog.vec.Mat4.getElement(localTransformInv, 0, 3),
+                goog.vec.Mat4.getElement(localTransformInv, 1, 3));
+          }
+        }
+        ++i;
+        break;
+      case ol.render.canvas.Instruction.END_GEOMETRY:
+        if (featureCallback !== undefined) {
+          feature =
+              /** @type {ol.Feature|ol.render.Feature} */ (instruction[1]);
+          var result = featureCallback(feature);
+          if (result) {
+            return result;
+          }
+        }
+        ++i;
+        break;
+      case ol.render.canvas.Instruction.FILL:
+        context.fill();
+        ++i;
+        break;
+      case ol.render.canvas.Instruction.MOVE_TO_LINE_TO:
+        goog.asserts.assert(goog.isNumber(instruction[1]),
+            '2nd instruction should be a number');
+        d = /** @type {number} */ (instruction[1]);
+        goog.asserts.assert(goog.isNumber(instruction[2]),
+            '3rd instruction should be a number');
+        dd = /** @type {number} */ (instruction[2]);
+        x = pixelCoordinates[d];
+        y = pixelCoordinates[d + 1];
+        roundX = (x + 0.5) | 0;
+        roundY = (y + 0.5) | 0;
+        if (roundX !== prevX || roundY !== prevY) {
+          context.moveTo(x, y);
+          prevX = roundX;
+          prevY = roundY;
+        }
+        for (d += 2; d < dd; d += 2) {
+          x = pixelCoordinates[d];
+          y = pixelCoordinates[d + 1];
+          roundX = (x + 0.5) | 0;
+          roundY = (y + 0.5) | 0;
+          if (d == dd - 2 || roundX !== prevX || roundY !== prevY) {
+            context.lineTo(x, y);
+            prevX = roundX;
+            prevY = roundY;
+          }
+        }
+        ++i;
+        break;
+      case ol.render.canvas.Instruction.SET_FILL_STYLE:
+        goog.asserts.assert(
+            ol.colorlike.isColorLike(instruction[1]),
+            '2nd instruction should be a string, ' +
+            'CanvasPattern, or CanvasGradient');
+        context.fillStyle = /** @type {ol.ColorLike} */ (instruction[1]);
+        ++i;
+        break;
+      case ol.render.canvas.Instruction.SET_STROKE_STYLE:
+        goog.asserts.assert(typeof instruction[1] === 'string',
+            '2nd instruction should be a string');
+        goog.asserts.assert(goog.isNumber(instruction[2]),
+            '3rd instruction should be a number');
+        goog.asserts.assert(typeof instruction[3] === 'string',
+            '4rd instruction should be a string');
+        goog.asserts.assert(typeof instruction[4] === 'string',
+            '5th instruction should be a string');
+        goog.asserts.assert(goog.isNumber(instruction[5]),
+            '6th instruction should be a number');
+        goog.asserts.assert(instruction[6],
+            '7th instruction should not be null');
+        var usePixelRatio = instruction[7] !== undefined ?
+            instruction[7] : true;
+        var lineWidth = /** @type {number} */ (instruction[2]);
+        context.strokeStyle = /** @type {string} */ (instruction[1]);
+        context.lineWidth = usePixelRatio ? lineWidth * pixelRatio : lineWidth;
+        context.lineCap = /** @type {string} */ (instruction[3]);
+        context.lineJoin = /** @type {string} */ (instruction[4]);
+        context.miterLimit = /** @type {number} */ (instruction[5]);
+        if (ol.has.CANVAS_LINE_DASH) {
+          context.setLineDash(/** @type {Array.<number>} */ (instruction[6]));
+        }
+        prevX = NaN;
+        prevY = NaN;
+        ++i;
+        break;
+      case ol.render.canvas.Instruction.SET_TEXT_STYLE:
+        goog.asserts.assert(typeof instruction[1] === 'string',
+            '2nd instruction should be a string');
+        goog.asserts.assert(typeof instruction[2] === 'string',
+            '3rd instruction should be a string');
+        goog.asserts.assert(typeof instruction[3] === 'string',
+            '4th instruction should be a string');
+        context.font = /** @type {string} */ (instruction[1]);
+        context.textAlign = /** @type {string} */ (instruction[2]);
+        context.textBaseline = /** @type {string} */ (instruction[3]);
+        ++i;
+        break;
+      case ol.render.canvas.Instruction.STROKE:
+        context.stroke();
+        ++i;
+        break;
+      default:
+        goog.asserts.fail('Unknown canvas render instruction');
+        ++i; // consume the instruction anyway, to avoid an infinite loop
+        break;
+    }
+  }
+  // assert that all instructions were consumed
+  goog.asserts.assert(i == instructions.length,
+      'all instructions should be consumed');
+  return undefined;
+};
+
+
+/**
+ * @param {CanvasRenderingContext2D} context Context.
+ * @param {number} pixelRatio Pixel ratio.
+ * @param {goog.vec.Mat4.Number} transform Transform.
+ * @param {number} viewRotation View rotation.
+ * @param {Object.<string, boolean>} skippedFeaturesHash Ids of features
+ *     to skip.
+ */
+ol.render.canvas.Replay.prototype.replay = function(
+    context, pixelRatio, transform, viewRotation, skippedFeaturesHash) {
+  var instructions = this.instructions;
+  this.replay_(context, pixelRatio, transform, viewRotation,
+      skippedFeaturesHash, instructions, undefined);
+};
+
+
+/**
+ * @param {CanvasRenderingContext2D} context Context.
+ * @param {goog.vec.Mat4.Number} transform Transform.
+ * @param {number} viewRotation View rotation.
+ * @param {Object.<string, boolean>} skippedFeaturesHash Ids of features
+ *     to skip.
+ * @param {function((ol.Feature|ol.render.Feature)): T=} opt_featureCallback
+ *     Feature callback.
+ * @param {ol.Extent=} opt_hitExtent Only check features that intersect this
+ *     extent.
+ * @return {T|undefined} Callback result.
+ * @template T
+ */
+ol.render.canvas.Replay.prototype.replayHitDetection = function(
+    context, transform, viewRotation, skippedFeaturesHash,
+    opt_featureCallback, opt_hitExtent) {
+  var instructions = this.hitDetectionInstructions;
+  return this.replay_(context, 1, transform, viewRotation,
+      skippedFeaturesHash, instructions, opt_featureCallback, opt_hitExtent);
+};
+
+
+/**
+ * @private
+ */
+ol.render.canvas.Replay.prototype.reverseHitDetectionInstructions_ = function() {
+  var hitDetectionInstructions = this.hitDetectionInstructions;
+  // step 1 - reverse array
+  hitDetectionInstructions.reverse();
+  // step 2 - reverse instructions within geometry blocks
+  var i;
+  var n = hitDetectionInstructions.length;
+  var instruction;
+  var type;
+  var begin = -1;
+  for (i = 0; i < n; ++i) {
+    instruction = hitDetectionInstructions[i];
+    type = /** @type {ol.render.canvas.Instruction} */ (instruction[0]);
+    if (type == ol.render.canvas.Instruction.END_GEOMETRY) {
+      goog.asserts.assert(begin == -1, 'begin should be -1');
+      begin = i;
+    } else if (type == ol.render.canvas.Instruction.BEGIN_GEOMETRY) {
+      instruction[2] = i;
+      goog.asserts.assert(begin >= 0,
+          'begin should be larger than or equal to 0');
+      ol.array.reverseSubArray(this.hitDetectionInstructions, begin, i);
+      begin = -1;
+    }
+  }
+};
+
+
+/**
+ * @param {ol.geom.Geometry|ol.render.Feature} geometry Geometry.
+ * @param {ol.Feature|ol.render.Feature} feature Feature.
+ */
+ol.render.canvas.Replay.prototype.endGeometry = function(geometry, feature) {
+  goog.asserts.assert(this.beginGeometryInstruction1_,
+      'this.beginGeometryInstruction1_ should not be null');
+  this.beginGeometryInstruction1_[2] = this.instructions.length;
+  this.beginGeometryInstruction1_ = null;
+  goog.asserts.assert(this.beginGeometryInstruction2_,
+      'this.beginGeometryInstruction2_ should not be null');
+  this.beginGeometryInstruction2_[2] = this.hitDetectionInstructions.length;
+  this.beginGeometryInstruction2_ = null;
+  var endGeometryInstruction =
+      [ol.render.canvas.Instruction.END_GEOMETRY, feature];
+  this.instructions.push(endGeometryInstruction);
+  this.hitDetectionInstructions.push(endGeometryInstruction);
+};
+
+
+/**
+ * FIXME empty description for jsdoc
+ */
+ol.render.canvas.Replay.prototype.finish = ol.nullFunction;
+
+
+/**
+ * Get the buffered rendering extent.  Rendering will be clipped to the extent
+ * provided to the constructor.  To account for symbolizers that may intersect
+ * this extent, we calculate a buffered extent (e.g. based on stroke width).
+ * @return {ol.Extent} The buffered rendering extent.
+ * @protected
+ */
+ol.render.canvas.Replay.prototype.getBufferedMaxExtent = function() {
+  return this.maxExtent;
+};
+
+
+/**
+ * @constructor
+ * @extends {ol.render.canvas.Replay}
+ * @param {number} tolerance Tolerance.
+ * @param {ol.Extent} maxExtent Maximum extent.
+ * @param {number} resolution Resolution.
+ * @protected
+ * @struct
+ */
+ol.render.canvas.ImageReplay = function(tolerance, maxExtent, resolution) {
+  ol.render.canvas.Replay.call(this, tolerance, maxExtent, resolution);
+
+  /**
+   * @private
+   * @type {HTMLCanvasElement|HTMLVideoElement|Image}
+   */
+  this.hitDetectionImage_ = null;
+
+  /**
+   * @private
+   * @type {HTMLCanvasElement|HTMLVideoElement|Image}
+   */
+  this.image_ = null;
+
+  /**
+   * @private
+   * @type {number|undefined}
+   */
+  this.anchorX_ = undefined;
+
+  /**
+   * @private
+   * @type {number|undefined}
+   */
+  this.anchorY_ = undefined;
+
+  /**
+   * @private
+   * @type {number|undefined}
+   */
+  this.height_ = undefined;
+
+  /**
+   * @private
+   * @type {number|undefined}
+   */
+  this.opacity_ = undefined;
+
+  /**
+   * @private
+   * @type {number|undefined}
+   */
+  this.originX_ = undefined;
+
+  /**
+   * @private
+   * @type {number|undefined}
+   */
+  this.originY_ = undefined;
+
+  /**
+   * @private
+   * @type {boolean|undefined}
+   */
+  this.rotateWithView_ = undefined;
+
+  /**
+   * @private
+   * @type {number|undefined}
+   */
+  this.rotation_ = undefined;
+
+  /**
+   * @private
+   * @type {number|undefined}
+   */
+  this.scale_ = undefined;
+
+  /**
+   * @private
+   * @type {boolean|undefined}
+   */
+  this.snapToPixel_ = undefined;
+
+  /**
+   * @private
+   * @type {number|undefined}
+   */
+  this.width_ = undefined;
+
+};
+ol.inherits(ol.render.canvas.ImageReplay, ol.render.canvas.Replay);
+
+
+/**
+ * @param {Array.<number>} flatCoordinates Flat coordinates.
+ * @param {number} offset Offset.
+ * @param {number} end End.
+ * @param {number} stride Stride.
+ * @private
+ * @return {number} My end.
+ */
+ol.render.canvas.ImageReplay.prototype.drawCoordinates_ = function(flatCoordinates, offset, end, stride) {
+  return this.appendFlatCoordinates(
+      flatCoordinates, offset, end, stride, false);
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.render.canvas.ImageReplay.prototype.drawPoint = function(pointGeometry, feature) {
+  if (!this.image_) {
+    return;
+  }
+  goog.asserts.assert(this.anchorX_ !== undefined,
+      'this.anchorX_ should be defined');
+  goog.asserts.assert(this.anchorY_ !== undefined,
+      'this.anchorY_ should be defined');
+  goog.asserts.assert(this.height_ !== undefined,
+      'this.height_ should be defined');
+  goog.asserts.assert(this.opacity_ !== undefined,
+      'this.opacity_ should be defined');
+  goog.asserts.assert(this.originX_ !== undefined,
+      'this.originX_ should be defined');
+  goog.asserts.assert(this.originY_ !== undefined,
+      'this.originY_ should be defined');
+  goog.asserts.assert(this.rotateWithView_ !== undefined,
+      'this.rotateWithView_ should be defined');
+  goog.asserts.assert(this.rotation_ !== undefined,
+      'this.rotation_ should be defined');
+  goog.asserts.assert(this.scale_ !== undefined,
+      'this.scale_ should be defined');
+  goog.asserts.assert(this.width_ !== undefined,
+      'this.width_ should be defined');
+  this.beginGeometry(pointGeometry, feature);
+  var flatCoordinates = pointGeometry.getFlatCoordinates();
+  var stride = pointGeometry.getStride();
+  var myBegin = this.coordinates.length;
+  var myEnd = this.drawCoordinates_(
+      flatCoordinates, 0, flatCoordinates.length, stride);
+  this.instructions.push([
+    ol.render.canvas.Instruction.DRAW_IMAGE, myBegin, myEnd, this.image_,
+    // Remaining arguments to DRAW_IMAGE are in alphabetical order
+    this.anchorX_, this.anchorY_, this.height_, this.opacity_,
+    this.originX_, this.originY_, this.rotateWithView_, this.rotation_,
+    this.scale_, this.snapToPixel_, this.width_
+  ]);
+  this.hitDetectionInstructions.push([
+    ol.render.canvas.Instruction.DRAW_IMAGE, myBegin, myEnd,
+    this.hitDetectionImage_,
+    // Remaining arguments to DRAW_IMAGE are in alphabetical order
+    this.anchorX_, this.anchorY_, this.height_, this.opacity_,
+    this.originX_, this.originY_, this.rotateWithView_, this.rotation_,
+    this.scale_, this.snapToPixel_, this.width_
+  ]);
+  this.endGeometry(pointGeometry, feature);
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.render.canvas.ImageReplay.prototype.drawMultiPoint = function(multiPointGeometry, feature) {
+  if (!this.image_) {
+    return;
+  }
+  goog.asserts.assert(this.anchorX_ !== undefined,
+      'this.anchorX_ should be defined');
+  goog.asserts.assert(this.anchorY_ !== undefined,
+      'this.anchorY_ should be defined');
+  goog.asserts.assert(this.height_ !== undefined,
+      'this.height_ should be defined');
+  goog.asserts.assert(this.opacity_ !== undefined,
+      'this.opacity_ should be defined');
+  goog.asserts.assert(this.originX_ !== undefined,
+      'this.originX_ should be defined');
+  goog.asserts.assert(this.originY_ !== undefined,
+      'this.originY_ should be defined');
+  goog.asserts.assert(this.rotateWithView_ !== undefined,
+      'this.rotateWithView_ should be defined');
+  goog.asserts.assert(this.rotation_ !== undefined,
+      'this.rotation_ should be defined');
+  goog.asserts.assert(this.scale_ !== undefined,
+      'this.scale_ should be defined');
+  goog.asserts.assert(this.width_ !== undefined,
+      'this.width_ should be defined');
+  this.beginGeometry(multiPointGeometry, feature);
+  var flatCoordinates = multiPointGeometry.getFlatCoordinates();
+  var stride = multiPointGeometry.getStride();
+  var myBegin = this.coordinates.length;
+  var myEnd = this.drawCoordinates_(
+      flatCoordinates, 0, flatCoordinates.length, stride);
+  this.instructions.push([
+    ol.render.canvas.Instruction.DRAW_IMAGE, myBegin, myEnd, this.image_,
+    // Remaining arguments to DRAW_IMAGE are in alphabetical order
+    this.anchorX_, this.anchorY_, this.height_, this.opacity_,
+    this.originX_, this.originY_, this.rotateWithView_, this.rotation_,
+    this.scale_, this.snapToPixel_, this.width_
+  ]);
+  this.hitDetectionInstructions.push([
+    ol.render.canvas.Instruction.DRAW_IMAGE, myBegin, myEnd,
+    this.hitDetectionImage_,
+    // Remaining arguments to DRAW_IMAGE are in alphabetical order
+    this.anchorX_, this.anchorY_, this.height_, this.opacity_,
+    this.originX_, this.originY_, this.rotateWithView_, this.rotation_,
+    this.scale_, this.snapToPixel_, this.width_
+  ]);
+  this.endGeometry(multiPointGeometry, feature);
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.render.canvas.ImageReplay.prototype.finish = function() {
+  this.reverseHitDetectionInstructions_();
+  // FIXME this doesn't really protect us against further calls to draw*Geometry
+  this.anchorX_ = undefined;
+  this.anchorY_ = undefined;
+  this.hitDetectionImage_ = null;
+  this.image_ = null;
+  this.height_ = undefined;
+  this.scale_ = undefined;
+  this.opacity_ = undefined;
+  this.originX_ = undefined;
+  this.originY_ = undefined;
+  this.rotateWithView_ = undefined;
+  this.rotation_ = undefined;
+  this.snapToPixel_ = undefined;
+  this.width_ = undefined;
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.render.canvas.ImageReplay.prototype.setImageStyle = function(imageStyle) {
+  goog.asserts.assert(imageStyle, 'imageStyle should not be null');
+  var anchor = imageStyle.getAnchor();
+  goog.asserts.assert(anchor, 'anchor should not be null');
+  var size = imageStyle.getSize();
+  goog.asserts.assert(size, 'size should not be null');
+  var hitDetectionImage = imageStyle.getHitDetectionImage(1);
+  goog.asserts.assert(hitDetectionImage,
+      'hitDetectionImage should not be null');
+  var image = imageStyle.getImage(1);
+  goog.asserts.assert(image, 'image should not be null');
+  var origin = imageStyle.getOrigin();
+  goog.asserts.assert(origin, 'origin should not be null');
+  this.anchorX_ = anchor[0];
+  this.anchorY_ = anchor[1];
+  this.hitDetectionImage_ = hitDetectionImage;
+  this.image_ = image;
+  this.height_ = size[1];
+  this.opacity_ = imageStyle.getOpacity();
+  this.originX_ = origin[0];
+  this.originY_ = origin[1];
+  this.rotateWithView_ = imageStyle.getRotateWithView();
+  this.rotation_ = imageStyle.getRotation();
+  this.scale_ = imageStyle.getScale();
+  this.snapToPixel_ = imageStyle.getSnapToPixel();
+  this.width_ = size[0];
+};
+
+
+/**
+ * @constructor
+ * @extends {ol.render.canvas.Replay}
+ * @param {number} tolerance Tolerance.
+ * @param {ol.Extent} maxExtent Maximum extent.
+ * @param {number} resolution Resolution.
+ * @protected
+ * @struct
+ */
+ol.render.canvas.LineStringReplay = function(tolerance, maxExtent, resolution) {
+
+  ol.render.canvas.Replay.call(this, tolerance, maxExtent, resolution);
+
+  /**
+   * @private
+   * @type {{currentStrokeStyle: (string|undefined),
+   *         currentLineCap: (string|undefined),
+   *         currentLineDash: Array.<number>,
+   *         currentLineJoin: (string|undefined),
+   *         currentLineWidth: (number|undefined),
+   *         currentMiterLimit: (number|undefined),
+   *         lastStroke: number,
+   *         strokeStyle: (string|undefined),
+   *         lineCap: (string|undefined),
+   *         lineDash: Array.<number>,
+   *         lineJoin: (string|undefined),
+   *         lineWidth: (number|undefined),
+   *         miterLimit: (number|undefined)}|null}
+   */
+  this.state_ = {
+    currentStrokeStyle: undefined,
+    currentLineCap: undefined,
+    currentLineDash: null,
+    currentLineJoin: undefined,
+    currentLineWidth: undefined,
+    currentMiterLimit: undefined,
+    lastStroke: 0,
+    strokeStyle: undefined,
+    lineCap: undefined,
+    lineDash: null,
+    lineJoin: undefined,
+    lineWidth: undefined,
+    miterLimit: undefined
+  };
+
+};
+ol.inherits(ol.render.canvas.LineStringReplay, ol.render.canvas.Replay);
+
+
+/**
+ * @param {Array.<number>} flatCoordinates Flat coordinates.
+ * @param {number} offset Offset.
+ * @param {number} end End.
+ * @param {number} stride Stride.
+ * @private
+ * @return {number} end.
+ */
+ol.render.canvas.LineStringReplay.prototype.drawFlatCoordinates_ = function(flatCoordinates, offset, end, stride) {
+  var myBegin = this.coordinates.length;
+  var myEnd = this.appendFlatCoordinates(
+      flatCoordinates, offset, end, stride, false);
+  var moveToLineToInstruction =
+      [ol.render.canvas.Instruction.MOVE_TO_LINE_TO, myBegin, myEnd];
+  this.instructions.push(moveToLineToInstruction);
+  this.hitDetectionInstructions.push(moveToLineToInstruction);
+  return end;
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.render.canvas.LineStringReplay.prototype.getBufferedMaxExtent = function() {
+  if (!this.bufferedMaxExtent_) {
+    this.bufferedMaxExtent_ = ol.extent.clone(this.maxExtent);
+    if (this.maxLineWidth > 0) {
+      var width = this.resolution * (this.maxLineWidth + 1) / 2;
+      ol.extent.buffer(this.bufferedMaxExtent_, width, this.bufferedMaxExtent_);
+    }
+  }
+  return this.bufferedMaxExtent_;
+};
+
+
+/**
+ * @private
+ */
+ol.render.canvas.LineStringReplay.prototype.setStrokeStyle_ = function() {
+  var state = this.state_;
+  var strokeStyle = state.strokeStyle;
+  var lineCap = state.lineCap;
+  var lineDash = state.lineDash;
+  var lineJoin = state.lineJoin;
+  var lineWidth = state.lineWidth;
+  var miterLimit = state.miterLimit;
+  goog.asserts.assert(strokeStyle !== undefined,
+      'strokeStyle should be defined');
+  goog.asserts.assert(lineCap !== undefined, 'lineCap should be defined');
+  goog.asserts.assert(lineDash, 'lineDash should not be null');
+  goog.asserts.assert(lineJoin !== undefined, 'lineJoin should be defined');
+  goog.asserts.assert(lineWidth !== undefined, 'lineWidth should be defined');
+  goog.asserts.assert(miterLimit !== undefined, 'miterLimit should be defined');
+  if (state.currentStrokeStyle != strokeStyle ||
+      state.currentLineCap != lineCap ||
+      !ol.array.equals(state.currentLineDash, lineDash) ||
+      state.currentLineJoin != lineJoin ||
+      state.currentLineWidth != lineWidth ||
+      state.currentMiterLimit != miterLimit) {
+    if (state.lastStroke != this.coordinates.length) {
+      this.instructions.push(
+          [ol.render.canvas.Instruction.STROKE]);
+      state.lastStroke = this.coordinates.length;
+    }
+    this.instructions.push(
+        [ol.render.canvas.Instruction.SET_STROKE_STYLE,
+         strokeStyle, lineWidth, lineCap, lineJoin, miterLimit, lineDash],
+        [ol.render.canvas.Instruction.BEGIN_PATH]);
+    state.currentStrokeStyle = strokeStyle;
+    state.currentLineCap = lineCap;
+    state.currentLineDash = lineDash;
+    state.currentLineJoin = lineJoin;
+    state.currentLineWidth = lineWidth;
+    state.currentMiterLimit = miterLimit;
+  }
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.render.canvas.LineStringReplay.prototype.drawLineString = function(lineStringGeometry, feature) {
+  var state = this.state_;
+  goog.asserts.assert(state, 'state should not be null');
+  var strokeStyle = state.strokeStyle;
+  var lineWidth = state.lineWidth;
+  if (strokeStyle === undefined || lineWidth === undefined) {
+    return;
+  }
+  this.setStrokeStyle_();
+  this.beginGeometry(lineStringGeometry, feature);
+  this.hitDetectionInstructions.push(
+      [ol.render.canvas.Instruction.SET_STROKE_STYLE,
+       state.strokeStyle, state.lineWidth, state.lineCap, state.lineJoin,
+       state.miterLimit, state.lineDash],
+      [ol.render.canvas.Instruction.BEGIN_PATH]);
+  var flatCoordinates = lineStringGeometry.getFlatCoordinates();
+  var stride = lineStringGeometry.getStride();
+  this.drawFlatCoordinates_(
+      flatCoordinates, 0, flatCoordinates.length, stride);
+  this.hitDetectionInstructions.push([ol.render.canvas.Instruction.STROKE]);
+  this.endGeometry(lineStringGeometry, feature);
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.render.canvas.LineStringReplay.prototype.drawMultiLineString = function(multiLineStringGeometry, feature) {
+  var state = this.state_;
+  goog.asserts.assert(state, 'state should not be null');
+  var strokeStyle = state.strokeStyle;
+  var lineWidth = state.lineWidth;
+  if (strokeStyle === undefined || lineWidth === undefined) {
+    return;
+  }
+  this.setStrokeStyle_();
+  this.beginGeometry(multiLineStringGeometry, feature);
+  this.hitDetectionInstructions.push(
+      [ol.render.canvas.Instruction.SET_STROKE_STYLE,
+       state.strokeStyle, state.lineWidth, state.lineCap, state.lineJoin,
+       state.miterLimit, state.lineDash],
+      [ol.render.canvas.Instruction.BEGIN_PATH]);
+  var ends = multiLineStringGeometry.getEnds();
+  var flatCoordinates = multiLineStringGeometry.getFlatCoordinates();
+  var stride = multiLineStringGeometry.getStride();
+  var offset = 0;
+  var i, ii;
+  for (i = 0, ii = ends.length; i < ii; ++i) {
+    offset = this.drawFlatCoordinates_(
+        flatCoordinates, offset, ends[i], stride);
+  }
+  this.hitDetectionInstructions.push([ol.render.canvas.Instruction.STROKE]);
+  this.endGeometry(multiLineStringGeometry, feature);
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.render.canvas.LineStringReplay.prototype.finish = function() {
+  var state = this.state_;
+  goog.asserts.assert(state, 'state should not be null');
+  if (state.lastStroke != this.coordinates.length) {
+    this.instructions.push([ol.render.canvas.Instruction.STROKE]);
+  }
+  this.reverseHitDetectionInstructions_();
+  this.state_ = null;
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.render.canvas.LineStringReplay.prototype.setFillStrokeStyle = function(fillStyle, strokeStyle) {
+  goog.asserts.assert(this.state_, 'this.state_ should not be null');
+  goog.asserts.assert(!fillStyle, 'fillStyle should be null');
+  goog.asserts.assert(strokeStyle, 'strokeStyle should not be null');
+  var strokeStyleColor = strokeStyle.getColor();
+  this.state_.strokeStyle = ol.color.asString(strokeStyleColor ?
+      strokeStyleColor : ol.render.canvas.defaultStrokeStyle);
+  var strokeStyleLineCap = strokeStyle.getLineCap();
+  this.state_.lineCap = strokeStyleLineCap !== undefined ?
+      strokeStyleLineCap : ol.render.canvas.defaultLineCap;
+  var strokeStyleLineDash = strokeStyle.getLineDash();
+  this.state_.lineDash = strokeStyleLineDash ?
+      strokeStyleLineDash : ol.render.canvas.defaultLineDash;
+  var strokeStyleLineJoin = strokeStyle.getLineJoin();
+  this.state_.lineJoin = strokeStyleLineJoin !== undefined ?
+      strokeStyleLineJoin : ol.render.canvas.defaultLineJoin;
+  var strokeStyleWidth = strokeStyle.getWidth();
+  this.state_.lineWidth = strokeStyleWidth !== undefined ?
+      strokeStyleWidth : ol.render.canvas.defaultLineWidth;
+  var strokeStyleMiterLimit = strokeStyle.getMiterLimit();
+  this.state_.miterLimit = strokeStyleMiterLimit !== undefined ?
+      strokeStyleMiterLimit : ol.render.canvas.defaultMiterLimit;
+
+  if (this.state_.lineWidth > this.maxLineWidth) {
+    this.maxLineWidth = this.state_.lineWidth;
+    // invalidate the buffered max extent cache
+    this.bufferedMaxExtent_ = null;
+  }
+};
+
+
+/**
+ * @constructor
+ * @extends {ol.render.canvas.Replay}
+ * @param {number} tolerance Tolerance.
+ * @param {ol.Extent} maxExtent Maximum extent.
+ * @param {number} resolution Resolution.
+ * @protected
+ * @struct
+ */
+ol.render.canvas.PolygonReplay = function(tolerance, maxExtent, resolution) {
+
+  ol.render.canvas.Replay.call(this, tolerance, maxExtent, resolution);
+
+  /**
+   * @private
+   * @type {{currentFillStyle: (ol.ColorLike|undefined),
+   *         currentStrokeStyle: (string|undefined),
+   *         currentLineCap: (string|undefined),
+   *         currentLineDash: Array.<number>,
+   *         currentLineJoin: (string|undefined),
+   *         currentLineWidth: (number|undefined),
+   *         currentMiterLimit: (number|undefined),
+   *         fillStyle: (ol.ColorLike|undefined),
+   *         strokeStyle: (string|undefined),
+   *         lineCap: (string|undefined),
+   *         lineDash: Array.<number>,
+   *         lineJoin: (string|undefined),
+   *         lineWidth: (number|undefined),
+   *         miterLimit: (number|undefined)}|null}
+   */
+  this.state_ = {
+    currentFillStyle: undefined,
+    currentStrokeStyle: undefined,
+    currentLineCap: undefined,
+    currentLineDash: null,
+    currentLineJoin: undefined,
+    currentLineWidth: undefined,
+    currentMiterLimit: undefined,
+    fillStyle: undefined,
+    strokeStyle: undefined,
+    lineCap: undefined,
+    lineDash: null,
+    lineJoin: undefined,
+    lineWidth: undefined,
+    miterLimit: undefined
+  };
+
+};
+ol.inherits(ol.render.canvas.PolygonReplay, ol.render.canvas.Replay);
+
+
+/**
+ * @param {Array.<number>} flatCoordinates Flat coordinates.
+ * @param {number} offset Offset.
+ * @param {Array.<number>} ends Ends.
+ * @param {number} stride Stride.
+ * @private
+ * @return {number} End.
+ */
+ol.render.canvas.PolygonReplay.prototype.drawFlatCoordinatess_ = function(flatCoordinates, offset, ends, stride) {
+  var state = this.state_;
+  var beginPathInstruction = [ol.render.canvas.Instruction.BEGIN_PATH];
+  this.instructions.push(beginPathInstruction);
+  this.hitDetectionInstructions.push(beginPathInstruction);
+  var i, ii;
+  for (i = 0, ii = ends.length; i < ii; ++i) {
+    var end = ends[i];
+    var myBegin = this.coordinates.length;
+    var myEnd = this.appendFlatCoordinates(
+        flatCoordinates, offset, end, stride, true);
+    var moveToLineToInstruction =
+        [ol.render.canvas.Instruction.MOVE_TO_LINE_TO, myBegin, myEnd];
+    var closePathInstruction = [ol.render.canvas.Instruction.CLOSE_PATH];
+    this.instructions.push(moveToLineToInstruction, closePathInstruction);
+    this.hitDetectionInstructions.push(moveToLineToInstruction,
+        closePathInstruction);
+    offset = end;
+  }
+  // FIXME is it quicker to fill and stroke each polygon individually,
+  // FIXME or all polygons together?
+  var fillInstruction = [ol.render.canvas.Instruction.FILL];
+  this.hitDetectionInstructions.push(fillInstruction);
+  if (state.fillStyle !== undefined) {
+    this.instructions.push(fillInstruction);
+  }
+  if (state.strokeStyle !== undefined) {
+    goog.asserts.assert(state.lineWidth !== undefined,
+        'state.lineWidth should be defined');
+    var strokeInstruction = [ol.render.canvas.Instruction.STROKE];
+    this.instructions.push(strokeInstruction);
+    this.hitDetectionInstructions.push(strokeInstruction);
+  }
+  return offset;
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.render.canvas.PolygonReplay.prototype.drawCircle = function(circleGeometry, feature) {
+  var state = this.state_;
+  goog.asserts.assert(state, 'state should not be null');
+  var fillStyle = state.fillStyle;
+  var strokeStyle = state.strokeStyle;
+  if (fillStyle === undefined && strokeStyle === undefined) {
+    return;
+  }
+  if (strokeStyle !== undefined) {
+    goog.asserts.assert(state.lineWidth !== undefined,
+        'state.lineWidth should be defined');
+  }
+  this.setFillStrokeStyles_();
+  this.beginGeometry(circleGeometry, feature);
+  // always fill the circle for hit detection
+  this.hitDetectionInstructions.push(
+      [ol.render.canvas.Instruction.SET_FILL_STYLE,
+       ol.color.asString(ol.render.canvas.defaultFillStyle)]);
+  if (state.strokeStyle !== undefined) {
+    this.hitDetectionInstructions.push(
+        [ol.render.canvas.Instruction.SET_STROKE_STYLE,
+         state.strokeStyle, state.lineWidth, state.lineCap, state.lineJoin,
+         state.miterLimit, state.lineDash]);
+  }
+  var flatCoordinates = circleGeometry.getFlatCoordinates();
+  var stride = circleGeometry.getStride();
+  var myBegin = this.coordinates.length;
+  this.appendFlatCoordinates(
+      flatCoordinates, 0, flatCoordinates.length, stride, false);
+  var beginPathInstruction = [ol.render.canvas.Instruction.BEGIN_PATH];
+  var circleInstruction = [ol.render.canvas.Instruction.CIRCLE, myBegin];
+  this.instructions.push(beginPathInstruction, circleInstruction);
+  this.hitDetectionInstructions.push(beginPathInstruction, circleInstruction);
+  var fillInstruction = [ol.render.canvas.Instruction.FILL];
+  this.hitDetectionInstructions.push(fillInstruction);
+  if (state.fillStyle !== undefined) {
+    this.instructions.push(fillInstruction);
+  }
+  if (state.strokeStyle !== undefined) {
+    goog.asserts.assert(state.lineWidth !== undefined,
+        'state.lineWidth should be defined');
+    var strokeInstruction = [ol.render.canvas.Instruction.STROKE];
+    this.instructions.push(strokeInstruction);
+    this.hitDetectionInstructions.push(strokeInstruction);
+  }
+  this.endGeometry(circleGeometry, feature);
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.render.canvas.PolygonReplay.prototype.drawPolygon = function(polygonGeometry, feature) {
+  var state = this.state_;
+  goog.asserts.assert(state, 'state should not be null');
+  var fillStyle = state.fillStyle;
+  var strokeStyle = state.strokeStyle;
+  if (fillStyle === undefined && strokeStyle === undefined) {
+    return;
+  }
+  if (strokeStyle !== undefined) {
+    goog.asserts.assert(state.lineWidth !== undefined,
+        'state.lineWidth should be defined');
+  }
+  this.setFillStrokeStyles_();
+  this.beginGeometry(polygonGeometry, feature);
+  // always fill the polygon for hit detection
+  this.hitDetectionInstructions.push(
+      [ol.render.canvas.Instruction.SET_FILL_STYLE,
+       ol.color.asString(ol.render.canvas.defaultFillStyle)]);
+  if (state.strokeStyle !== undefined) {
+    this.hitDetectionInstructions.push(
+        [ol.render.canvas.Instruction.SET_STROKE_STYLE,
+         state.strokeStyle, state.lineWidth, state.lineCap, state.lineJoin,
+         state.miterLimit, state.lineDash]);
+  }
+  var ends = polygonGeometry.getEnds();
+  var flatCoordinates = polygonGeometry.getOrientedFlatCoordinates();
+  var stride = polygonGeometry.getStride();
+  this.drawFlatCoordinatess_(flatCoordinates, 0, ends, stride);
+  this.endGeometry(polygonGeometry, feature);
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.render.canvas.PolygonReplay.prototype.drawMultiPolygon = function(multiPolygonGeometry, feature) {
+  var state = this.state_;
+  goog.asserts.assert(state, 'state should not be null');
+  var fillStyle = state.fillStyle;
+  var strokeStyle = state.strokeStyle;
+  if (fillStyle === undefined && strokeStyle === undefined) {
+    return;
+  }
+  if (strokeStyle !== undefined) {
+    goog.asserts.assert(state.lineWidth !== undefined,
+        'state.lineWidth should be defined');
+  }
+  this.setFillStrokeStyles_();
+  this.beginGeometry(multiPolygonGeometry, feature);
+  // always fill the multi-polygon for hit detection
+  this.hitDetectionInstructions.push(
+      [ol.render.canvas.Instruction.SET_FILL_STYLE,
+       ol.color.asString(ol.render.canvas.defaultFillStyle)]);
+  if (state.strokeStyle !== undefined) {
+    this.hitDetectionInstructions.push(
+        [ol.render.canvas.Instruction.SET_STROKE_STYLE,
+         state.strokeStyle, state.lineWidth, state.lineCap, state.lineJoin,
+         state.miterLimit, state.lineDash]);
+  }
+  var endss = multiPolygonGeometry.getEndss();
+  var flatCoordinates = multiPolygonGeometry.getOrientedFlatCoordinates();
+  var stride = multiPolygonGeometry.getStride();
+  var offset = 0;
+  var i, ii;
+  for (i = 0, ii = endss.length; i < ii; ++i) {
+    offset = this.drawFlatCoordinatess_(
+        flatCoordinates, offset, endss[i], stride);
+  }
+  this.endGeometry(multiPolygonGeometry, feature);
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.render.canvas.PolygonReplay.prototype.finish = function() {
+  goog.asserts.assert(this.state_, 'this.state_ should not be null');
+  this.reverseHitDetectionInstructions_();
+  this.state_ = null;
+  // We want to preserve topology when drawing polygons.  Polygons are
+  // simplified using quantization and point elimination. However, we might
+  // have received a mix of quantized and non-quantized geometries, so ensure
+  // that all are quantized by quantizing all coordinates in the batch.
+  var tolerance = this.tolerance;
+  if (tolerance !== 0) {
+    var coordinates = this.coordinates;
+    var i, ii;
+    for (i = 0, ii = coordinates.length; i < ii; ++i) {
+      coordinates[i] = ol.geom.flat.simplify.snap(coordinates[i], tolerance);
+    }
+  }
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.render.canvas.PolygonReplay.prototype.getBufferedMaxExtent = function() {
+  if (!this.bufferedMaxExtent_) {
+    this.bufferedMaxExtent_ = ol.extent.clone(this.maxExtent);
+    if (this.maxLineWidth > 0) {
+      var width = this.resolution * (this.maxLineWidth + 1) / 2;
+      ol.extent.buffer(this.bufferedMaxExtent_, width, this.bufferedMaxExtent_);
+    }
+  }
+  return this.bufferedMaxExtent_;
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.render.canvas.PolygonReplay.prototype.setFillStrokeStyle = function(fillStyle, strokeStyle) {
+  goog.asserts.assert(this.state_, 'this.state_ should not be null');
+  goog.asserts.assert(fillStyle || strokeStyle,
+      'fillStyle or strokeStyle should not be null');
+  var state = this.state_;
+  if (fillStyle) {
+    var fillStyleColor = fillStyle.getColor();
+    state.fillStyle = ol.colorlike.asColorLike(fillStyleColor ?
+        fillStyleColor : ol.render.canvas.defaultFillStyle);
+  } else {
+    state.fillStyle = undefined;
+  }
+  if (strokeStyle) {
+    var strokeStyleColor = strokeStyle.getColor();
+    state.strokeStyle = ol.color.asString(strokeStyleColor ?
+        strokeStyleColor : ol.render.canvas.defaultStrokeStyle);
+    var strokeStyleLineCap = strokeStyle.getLineCap();
+    state.lineCap = strokeStyleLineCap !== undefined ?
+        strokeStyleLineCap : ol.render.canvas.defaultLineCap;
+    var strokeStyleLineDash = strokeStyle.getLineDash();
+    state.lineDash = strokeStyleLineDash ?
+        strokeStyleLineDash.slice() : ol.render.canvas.defaultLineDash;
+    var strokeStyleLineJoin = strokeStyle.getLineJoin();
+    state.lineJoin = strokeStyleLineJoin !== undefined ?
+        strokeStyleLineJoin : ol.render.canvas.defaultLineJoin;
+    var strokeStyleWidth = strokeStyle.getWidth();
+    state.lineWidth = strokeStyleWidth !== undefined ?
+        strokeStyleWidth : ol.render.canvas.defaultLineWidth;
+    var strokeStyleMiterLimit = strokeStyle.getMiterLimit();
+    state.miterLimit = strokeStyleMiterLimit !== undefined ?
+        strokeStyleMiterLimit : ol.render.canvas.defaultMiterLimit;
+
+    if (state.lineWidth > this.maxLineWidth) {
+      this.maxLineWidth = state.lineWidth;
+      // invalidate the buffered max extent cache
+      this.bufferedMaxExtent_ = null;
+    }
+  } else {
+    state.strokeStyle = undefined;
+    state.lineCap = undefined;
+    state.lineDash = null;
+    state.lineJoin = undefined;
+    state.lineWidth = undefined;
+    state.miterLimit = undefined;
+  }
+};
+
+
+/**
+ * @private
+ */
+ol.render.canvas.PolygonReplay.prototype.setFillStrokeStyles_ = function() {
+  var state = this.state_;
+  var fillStyle = state.fillStyle;
+  var strokeStyle = state.strokeStyle;
+  var lineCap = state.lineCap;
+  var lineDash = state.lineDash;
+  var lineJoin = state.lineJoin;
+  var lineWidth = state.lineWidth;
+  var miterLimit = state.miterLimit;
+  if (fillStyle !== undefined && state.currentFillStyle != fillStyle) {
+    this.instructions.push(
+        [ol.render.canvas.Instruction.SET_FILL_STYLE, fillStyle]);
+    state.currentFillStyle = state.fillStyle;
+  }
+  if (strokeStyle !== undefined) {
+    goog.asserts.assert(lineCap !== undefined, 'lineCap should be defined');
+    goog.asserts.assert(lineDash, 'lineDash should not be null');
+    goog.asserts.assert(lineJoin !== undefined, 'lineJoin should be defined');
+    goog.asserts.assert(lineWidth !== undefined, 'lineWidth should be defined');
+    goog.asserts.assert(miterLimit !== undefined,
+        'miterLimit should be defined');
+    if (state.currentStrokeStyle != strokeStyle ||
+        state.currentLineCap != lineCap ||
+        state.currentLineDash != lineDash ||
+        state.currentLineJoin != lineJoin ||
+        state.currentLineWidth != lineWidth ||
+        state.currentMiterLimit != miterLimit) {
+      this.instructions.push(
+          [ol.render.canvas.Instruction.SET_STROKE_STYLE,
+           strokeStyle, lineWidth, lineCap, lineJoin, miterLimit, lineDash]);
+      state.currentStrokeStyle = strokeStyle;
+      state.currentLineCap = lineCap;
+      state.currentLineDash = lineDash;
+      state.currentLineJoin = lineJoin;
+      state.currentLineWidth = lineWidth;
+      state.currentMiterLimit = miterLimit;
+    }
+  }
+};
+
+
+/**
+ * @constructor
+ * @extends {ol.render.canvas.Replay}
+ * @param {number} tolerance Tolerance.
+ * @param {ol.Extent} maxExtent Maximum extent.
+ * @param {number} resolution Resolution.
+ * @protected
+ * @struct
+ */
+ol.render.canvas.TextReplay = function(tolerance, maxExtent, resolution) {
+
+  ol.render.canvas.Replay.call(this, tolerance, maxExtent, resolution);
+
+  /**
+   * @private
+   * @type {?ol.CanvasFillState}
+   */
+  this.replayFillState_ = null;
+
+  /**
+   * @private
+   * @type {?ol.CanvasStrokeState}
+   */
+  this.replayStrokeState_ = null;
+
+  /**
+   * @private
+   * @type {?ol.CanvasTextState}
+   */
+  this.replayTextState_ = null;
+
+  /**
+   * @private
+   * @type {string}
+   */
+  this.text_ = '';
+
+  /**
+   * @private
+   * @type {number}
+   */
+  this.textOffsetX_ = 0;
+
+  /**
+   * @private
+   * @type {number}
+   */
+  this.textOffsetY_ = 0;
+
+  /**
+   * @private
+   * @type {number}
+   */
+  this.textRotation_ = 0;
+
+  /**
+   * @private
+   * @type {number}
+   */
+  this.textScale_ = 0;
+
+  /**
+   * @private
+   * @type {?ol.CanvasFillState}
+   */
+  this.textFillState_ = null;
+
+  /**
+   * @private
+   * @type {?ol.CanvasStrokeState}
+   */
+  this.textStrokeState_ = null;
+
+  /**
+   * @private
+   * @type {?ol.CanvasTextState}
+   */
+  this.textState_ = null;
+
+};
+ol.inherits(ol.render.canvas.TextReplay, ol.render.canvas.Replay);
+
+
+/**
+ * @inheritDoc
+ */
+ol.render.canvas.TextReplay.prototype.drawText = function(flatCoordinates, offset, end, stride, geometry, feature) {
+  if (this.text_ === '' || !this.textState_ ||
+      (!this.textFillState_ && !this.textStrokeState_)) {
+    return;
+  }
+  if (this.textFillState_) {
+    this.setReplayFillState_(this.textFillState_);
+  }
+  if (this.textStrokeState_) {
+    this.setReplayStrokeState_(this.textStrokeState_);
+  }
+  this.setReplayTextState_(this.textState_);
+  this.beginGeometry(geometry, feature);
+  var myBegin = this.coordinates.length;
+  var myEnd =
+      this.appendFlatCoordinates(flatCoordinates, offset, end, stride, false);
+  var fill = !!this.textFillState_;
+  var stroke = !!this.textStrokeState_;
+  var drawTextInstruction = [
+    ol.render.canvas.Instruction.DRAW_TEXT, myBegin, myEnd, this.text_,
+    this.textOffsetX_, this.textOffsetY_, this.textRotation_, this.textScale_,
+    fill, stroke];
+  this.instructions.push(drawTextInstruction);
+  this.hitDetectionInstructions.push(drawTextInstruction);
+  this.endGeometry(geometry, feature);
+};
+
+
+/**
+ * @param {ol.CanvasFillState} fillState Fill state.
+ * @private
+ */
+ol.render.canvas.TextReplay.prototype.setReplayFillState_ = function(fillState) {
+  var replayFillState = this.replayFillState_;
+  if (replayFillState &&
+      replayFillState.fillStyle == fillState.fillStyle) {
+    return;
+  }
+  var setFillStyleInstruction =
+      [ol.render.canvas.Instruction.SET_FILL_STYLE, fillState.fillStyle];
+  this.instructions.push(setFillStyleInstruction);
+  this.hitDetectionInstructions.push(setFillStyleInstruction);
+  if (!replayFillState) {
+    this.replayFillState_ = {
+      fillStyle: fillState.fillStyle
+    };
+  } else {
+    replayFillState.fillStyle = fillState.fillStyle;
+  }
+};
+
+
+/**
+ * @param {ol.CanvasStrokeState} strokeState Stroke state.
+ * @private
+ */
+ol.render.canvas.TextReplay.prototype.setReplayStrokeState_ = function(strokeState) {
+  var replayStrokeState = this.replayStrokeState_;
+  if (replayStrokeState &&
+      replayStrokeState.lineCap == strokeState.lineCap &&
+      replayStrokeState.lineDash == strokeState.lineDash &&
+      replayStrokeState.lineJoin == strokeState.lineJoin &&
+      replayStrokeState.lineWidth == strokeState.lineWidth &&
+      replayStrokeState.miterLimit == strokeState.miterLimit &&
+      replayStrokeState.strokeStyle == strokeState.strokeStyle) {
+    return;
+  }
+  var setStrokeStyleInstruction = [
+    ol.render.canvas.Instruction.SET_STROKE_STYLE, strokeState.strokeStyle,
+    strokeState.lineWidth, strokeState.lineCap, strokeState.lineJoin,
+    strokeState.miterLimit, strokeState.lineDash, false
+  ];
+  this.instructions.push(setStrokeStyleInstruction);
+  this.hitDetectionInstructions.push(setStrokeStyleInstruction);
+  if (!replayStrokeState) {
+    this.replayStrokeState_ = {
+      lineCap: strokeState.lineCap,
+      lineDash: strokeState.lineDash,
+      lineJoin: strokeState.lineJoin,
+      lineWidth: strokeState.lineWidth,
+      miterLimit: strokeState.miterLimit,
+      strokeStyle: strokeState.strokeStyle
+    };
+  } else {
+    replayStrokeState.lineCap = strokeState.lineCap;
+    replayStrokeState.lineDash = strokeState.lineDash;
+    replayStrokeState.lineJoin = strokeState.lineJoin;
+    replayStrokeState.lineWidth = strokeState.lineWidth;
+    replayStrokeState.miterLimit = strokeState.miterLimit;
+    replayStrokeState.strokeStyle = strokeState.strokeStyle;
+  }
+};
+
+
+/**
+ * @param {ol.CanvasTextState} textState Text state.
+ * @private
+ */
+ol.render.canvas.TextReplay.prototype.setReplayTextState_ = function(textState) {
+  var replayTextState = this.replayTextState_;
+  if (replayTextState &&
+      replayTextState.font == textState.font &&
+      replayTextState.textAlign == textState.textAlign &&
+      replayTextState.textBaseline == textState.textBaseline) {
+    return;
+  }
+  var setTextStyleInstruction = [ol.render.canvas.Instruction.SET_TEXT_STYLE,
+    textState.font, textState.textAlign, textState.textBaseline];
+  this.instructions.push(setTextStyleInstruction);
+  this.hitDetectionInstructions.push(setTextStyleInstruction);
+  if (!replayTextState) {
+    this.replayTextState_ = {
+      font: textState.font,
+      textAlign: textState.textAlign,
+      textBaseline: textState.textBaseline
+    };
+  } else {
+    replayTextState.font = textState.font;
+    replayTextState.textAlign = textState.textAlign;
+    replayTextState.textBaseline = textState.textBaseline;
+  }
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.render.canvas.TextReplay.prototype.setTextStyle = function(textStyle) {
+  if (!textStyle) {
+    this.text_ = '';
+  } else {
+    var textFillStyle = textStyle.getFill();
+    if (!textFillStyle) {
+      this.textFillState_ = null;
+    } else {
+      var textFillStyleColor = textFillStyle.getColor();
+      var fillStyle = ol.colorlike.asColorLike(textFillStyleColor ?
+          textFillStyleColor : ol.render.canvas.defaultFillStyle);
+      if (!this.textFillState_) {
+        this.textFillState_ = {
+          fillStyle: fillStyle
+        };
+      } else {
+        var textFillState = this.textFillState_;
+        textFillState.fillStyle = fillStyle;
+      }
+    }
+    var textStrokeStyle = textStyle.getStroke();
+    if (!textStrokeStyle) {
+      this.textStrokeState_ = null;
+    } else {
+      var textStrokeStyleColor = textStrokeStyle.getColor();
+      var textStrokeStyleLineCap = textStrokeStyle.getLineCap();
+      var textStrokeStyleLineDash = textStrokeStyle.getLineDash();
+      var textStrokeStyleLineJoin = textStrokeStyle.getLineJoin();
+      var textStrokeStyleWidth = textStrokeStyle.getWidth();
+      var textStrokeStyleMiterLimit = textStrokeStyle.getMiterLimit();
+      var lineCap = textStrokeStyleLineCap !== undefined ?
+          textStrokeStyleLineCap : ol.render.canvas.defaultLineCap;
+      var lineDash = textStrokeStyleLineDash ?
+          textStrokeStyleLineDash.slice() : ol.render.canvas.defaultLineDash;
+      var lineJoin = textStrokeStyleLineJoin !== undefined ?
+          textStrokeStyleLineJoin : ol.render.canvas.defaultLineJoin;
+      var lineWidth = textStrokeStyleWidth !== undefined ?
+          textStrokeStyleWidth : ol.render.canvas.defaultLineWidth;
+      var miterLimit = textStrokeStyleMiterLimit !== undefined ?
+          textStrokeStyleMiterLimit : ol.render.canvas.defaultMiterLimit;
+      var strokeStyle = ol.color.asString(textStrokeStyleColor ?
+          textStrokeStyleColor : ol.render.canvas.defaultStrokeStyle);
+      if (!this.textStrokeState_) {
+        this.textStrokeState_ = {
+          lineCap: lineCap,
+          lineDash: lineDash,
+          lineJoin: lineJoin,
+          lineWidth: lineWidth,
+          miterLimit: miterLimit,
+          strokeStyle: strokeStyle
+        };
+      } else {
+        var textStrokeState = this.textStrokeState_;
+        textStrokeState.lineCap = lineCap;
+        textStrokeState.lineDash = lineDash;
+        textStrokeState.lineJoin = lineJoin;
+        textStrokeState.lineWidth = lineWidth;
+        textStrokeState.miterLimit = miterLimit;
+        textStrokeState.strokeStyle = strokeStyle;
+      }
+    }
+    var textFont = textStyle.getFont();
+    var textOffsetX = textStyle.getOffsetX();
+    var textOffsetY = textStyle.getOffsetY();
+    var textRotation = textStyle.getRotation();
+    var textScale = textStyle.getScale();
+    var textText = textStyle.getText();
+    var textTextAlign = textStyle.getTextAlign();
+    var textTextBaseline = textStyle.getTextBaseline();
+    var font = textFont !== undefined ?
+        textFont : ol.render.canvas.defaultFont;
+    var textAlign = textTextAlign !== undefined ?
+        textTextAlign : ol.render.canvas.defaultTextAlign;
+    var textBaseline = textTextBaseline !== undefined ?
+        textTextBaseline : ol.render.canvas.defaultTextBaseline;
+    if (!this.textState_) {
+      this.textState_ = {
+        font: font,
+        textAlign: textAlign,
+        textBaseline: textBaseline
+      };
+    } else {
+      var textState = this.textState_;
+      textState.font = font;
+      textState.textAlign = textAlign;
+      textState.textBaseline = textBaseline;
+    }
+    this.text_ = textText !== undefined ? textText : '';
+    this.textOffsetX_ = textOffsetX !== undefined ? textOffsetX : 0;
+    this.textOffsetY_ = textOffsetY !== undefined ? textOffsetY : 0;
+    this.textRotation_ = textRotation !== undefined ? textRotation : 0;
+    this.textScale_ = textScale !== undefined ? textScale : 1;
+  }
+};
+
+
+/**
+ * @constructor
+ * @implements {ol.render.IReplayGroup}
+ * @param {number} tolerance Tolerance.
+ * @param {ol.Extent} maxExtent Max extent.
+ * @param {number} resolution Resolution.
+ * @param {number=} opt_renderBuffer Optional rendering buffer.
+ * @struct
+ */
+ol.render.canvas.ReplayGroup = function(
+    tolerance, maxExtent, resolution, opt_renderBuffer) {
+
+  /**
+   * @private
+   * @type {number}
+   */
+  this.tolerance_ = tolerance;
+
+  /**
+   * @private
+   * @type {ol.Extent}
+   */
+  this.maxExtent_ = maxExtent;
+
+  /**
+   * @private
+   * @type {number}
+   */
+  this.resolution_ = resolution;
+
+  /**
+   * @private
+   * @type {number|undefined}
+   */
+  this.renderBuffer_ = opt_renderBuffer;
+
+  /**
+   * @private
+   * @type {!Object.<string,
+   *        Object.<ol.render.ReplayType, ol.render.canvas.Replay>>}
+   */
+  this.replaysByZIndex_ = {};
+
+  /**
+   * @private
+   * @type {CanvasRenderingContext2D}
+   */
+  this.hitDetectionContext_ = ol.dom.createCanvasContext2D(1, 1);
+
+  /**
+   * @private
+   * @type {!goog.vec.Mat4.Number}
+   */
+  this.hitDetectionTransform_ = goog.vec.Mat4.createNumber();
+
+};
+
+
+/**
+ * FIXME empty description for jsdoc
+ */
+ol.render.canvas.ReplayGroup.prototype.finish = function() {
+  var zKey;
+  for (zKey in this.replaysByZIndex_) {
+    var replays = this.replaysByZIndex_[zKey];
+    var replayKey;
+    for (replayKey in replays) {
+      replays[replayKey].finish();
+    }
+  }
+};
+
+
+/**
+ * @param {ol.Coordinate} coordinate Coordinate.
+ * @param {number} resolution Resolution.
+ * @param {number} rotation Rotation.
+ * @param {Object.<string, boolean>} skippedFeaturesHash Ids of features
+ *     to skip.
+ * @param {function((ol.Feature|ol.render.Feature)): T} callback Feature
+ *     callback.
+ * @return {T|undefined} Callback result.
+ * @template T
+ */
+ol.render.canvas.ReplayGroup.prototype.forEachFeatureAtCoordinate = function(
+    coordinate, resolution, rotation, skippedFeaturesHash, callback) {
+
+  var transform = this.hitDetectionTransform_;
+  ol.vec.Mat4.makeTransform2D(transform, 0.5, 0.5,
+      1 / resolution, -1 / resolution, -rotation,
+      -coordinate[0], -coordinate[1]);
+
+  var context = this.hitDetectionContext_;
+  context.clearRect(0, 0, 1, 1);
+
+  /**
+   * @type {ol.Extent}
+   */
+  var hitExtent;
+  if (this.renderBuffer_ !== undefined) {
+    hitExtent = ol.extent.createEmpty();
+    ol.extent.extendCoordinate(hitExtent, coordinate);
+    ol.extent.buffer(hitExtent, resolution * this.renderBuffer_, hitExtent);
+  }
+
+  return this.replayHitDetection_(context, transform, rotation,
+      skippedFeaturesHash,
+      /**
+       * @param {ol.Feature|ol.render.Feature} feature Feature.
+       * @return {?} Callback result.
+       */
+      function(feature) {
+        var imageData = context.getImageData(0, 0, 1, 1).data;
+        if (imageData[3] > 0) {
+          var result = callback(feature);
+          if (result) {
+            return result;
+          }
+          context.clearRect(0, 0, 1, 1);
+        }
+      }, hitExtent);
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.render.canvas.ReplayGroup.prototype.getReplay = function(zIndex, replayType) {
+  var zIndexKey = zIndex !== undefined ? zIndex.toString() : '0';
+  var replays = this.replaysByZIndex_[zIndexKey];
+  if (replays === undefined) {
+    replays = {};
+    this.replaysByZIndex_[zIndexKey] = replays;
+  }
+  var replay = replays[replayType];
+  if (replay === undefined) {
+    var Constructor = ol.render.canvas.BATCH_CONSTRUCTORS_[replayType];
+    goog.asserts.assert(Constructor !== undefined,
+        replayType +
+        ' constructor missing from ol.render.canvas.BATCH_CONSTRUCTORS_');
+    replay = new Constructor(this.tolerance_, this.maxExtent_,
+        this.resolution_);
+    replays[replayType] = replay;
+  }
+  return replay;
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.render.canvas.ReplayGroup.prototype.isEmpty = function() {
+  return ol.object.isEmpty(this.replaysByZIndex_);
+};
+
+
+/**
+ * @param {CanvasRenderingContext2D} context Context.
+ * @param {number} pixelRatio Pixel ratio.
+ * @param {goog.vec.Mat4.Number} transform Transform.
+ * @param {number} viewRotation View rotation.
+ * @param {Object.<string, boolean>} skippedFeaturesHash Ids of features
+ *     to skip.
+ * @param {Array.<ol.render.ReplayType>=} opt_replayTypes Ordered replay types
+ *     to replay. Default is {@link ol.render.REPLAY_ORDER}
+ */
+ol.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(ol.array.numberSafeCompareFunction);
+
+  // setup clipping so that the parts of over-simplified geometries are not
+  // visible outside the current extent when panning
+  var maxExtent = this.maxExtent_;
+  var minX = maxExtent[0];
+  var minY = maxExtent[1];
+  var maxX = maxExtent[2];
+  var maxY = maxExtent[3];
+  var flatClipCoords = [minX, minY, minX, maxY, maxX, maxY, maxX, minY];
+  ol.geom.flat.transform.transform2D(
+      flatClipCoords, 0, 8, 2, transform, flatClipCoords);
+  context.save();
+  context.beginPath();
+  context.moveTo(flatClipCoords[0], flatClipCoords[1]);
+  context.lineTo(flatClipCoords[2], flatClipCoords[3]);
+  context.lineTo(flatClipCoords[4], flatClipCoords[5]);
+  context.lineTo(flatClipCoords[6], flatClipCoords[7]);
+  context.closePath();
+  context.clip();
+
+  var replayTypes = opt_replayTypes ? opt_replayTypes : ol.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 {goog.vec.Mat4.Number} transform Transform.
+ * @param {number} viewRotation View rotation.
+ * @param {Object.<string, boolean>} skippedFeaturesHash Ids of features
+ *     to skip.
+ * @param {function((ol.Feature|ol.render.Feature)): T} featureCallback
+ *     Feature callback.
+ * @param {ol.Extent=} opt_hitExtent Only check features that intersect this
+ *     extent.
+ * @return {T|undefined} Callback result.
+ * @template T
+ */
+ol.render.canvas.ReplayGroup.prototype.replayHitDetection_ = function(
+    context, transform, viewRotation, skippedFeaturesHash,
+    featureCallback, opt_hitExtent) {
+  /** @type {Array.<number>} */
+  var zs = Object.keys(this.replaysByZIndex_).map(Number);
+  zs.sort(function(a, b) {
+    return b - a;
+  });
+
+  var i, ii, j, replays, replay, result;
+  for (i = 0, ii = zs.length; i < ii; ++i) {
+    replays = this.replaysByZIndex_[zs[i].toString()];
+    for (j = ol.render.REPLAY_ORDER.length - 1; j >= 0; --j) {
+      replay = replays[ol.render.REPLAY_ORDER[j]];
+      if (replay !== undefined) {
+        result = replay.replayHitDetection(context, transform, viewRotation,
+            skippedFeaturesHash, featureCallback, opt_hitExtent);
+        if (result) {
+          return result;
+        }
+      }
+    }
+  }
+  return undefined;
+};
+
+
+/**
+ * @const
+ * @private
+ * @type {Object.<ol.render.ReplayType,
+ *                function(new: ol.render.canvas.Replay, number, ol.Extent,
+ *                number)>}
+ */
+ol.render.canvas.BATCH_CONSTRUCTORS_ = {
+  'Image': ol.render.canvas.ImageReplay,
+  'LineString': ol.render.canvas.LineStringReplay,
+  'Polygon': ol.render.canvas.PolygonReplay,
+  'Text': ol.render.canvas.TextReplay
+};
+
+goog.provide('ol.render.Feature');
+
+
+goog.require('goog.asserts');
+goog.require('ol.extent');
+goog.require('ol.geom.GeometryType');
+
+
+/**
+ * Lightweight, read-only, {@link ol.Feature} and {@link ol.geom.Geometry} like
+ * structure, optimized for rendering and styling. Geometry access through the
+ * API is limited to getting the type and extent of the geometry.
+ *
+ * @constructor
+ * @param {ol.geom.GeometryType} type Geometry type.
+ * @param {Array.<number>} flatCoordinates Flat coordinates. These always need
+ *     to be right-handed for polygons.
+ * @param {Array.<number>|Array.<Array.<number>>} ends Ends or Endss.
+ * @param {Object.<string, *>} properties Properties.
+ */
+ol.render.Feature = function(type, flatCoordinates, ends, properties) {
+
+  /**
+   * @private
+   * @type {ol.Extent|undefined}
+   */
+  this.extent_;
+
+  goog.asserts.assert(type === ol.geom.GeometryType.POINT ||
+      type === ol.geom.GeometryType.MULTI_POINT ||
+      type === ol.geom.GeometryType.LINE_STRING ||
+      type === ol.geom.GeometryType.MULTI_LINE_STRING ||
+      type === ol.geom.GeometryType.POLYGON,
+      'Need a Point, MultiPoint, LineString, MultiLineString or Polygon type');
+
+  /**
+   * @private
+   * @type {ol.geom.GeometryType}
+   */
+  this.type_ = type;
+
+  /**
+   * @private
+   * @type {Array.<number>}
+   */
+  this.flatCoordinates_ = flatCoordinates;
+
+  /**
+   * @private
+   * @type {Array.<number>|Array.<Array.<number>>}
+   */
+  this.ends_ = ends;
+
+  /**
+   * @private
+   * @type {Object.<string, *>}
+   */
+  this.properties_ = properties;
+
+};
+
+
+/**
+ * Get a feature property by its key.
+ * @param {string} key Key
+ * @return {*} Value for the requested key.
+ * @api
+ */
+ol.render.Feature.prototype.get = function(key) {
+  return this.properties_[key];
+};
+
+
+/**
+ * @return {Array.<number>|Array.<Array.<number>>} Ends or endss.
+ */
+ol.render.Feature.prototype.getEnds = function() {
+  return this.ends_;
+};
+
+
+/**
+ * Get the extent of this feature's geometry.
+ * @return {ol.Extent} Extent.
+ * @api
+ */
+ol.render.Feature.prototype.getExtent = function() {
+  if (!this.extent_) {
+    this.extent_ = this.type_ === ol.geom.GeometryType.POINT ?
+        ol.extent.createOrUpdateFromCoordinate(this.flatCoordinates_) :
+        ol.extent.createOrUpdateFromFlatCoordinates(
+            this.flatCoordinates_, 0, this.flatCoordinates_.length, 2);
+
+  }
+  return this.extent_;
+};
+
+
+/**
+ * @return {Array.<number>} Flat coordinates.
+ */
+ol.render.Feature.prototype.getOrientedFlatCoordinates = function() {
+  return this.flatCoordinates_;
+};
+
+
+/**
+ * @return {Array.<number>} Flat coordinates.
+ */
+ol.render.Feature.prototype.getFlatCoordinates =
+    ol.render.Feature.prototype.getOrientedFlatCoordinates;
+
+
+/**
+ * Get the feature for working with its geometry.
+ * @return {ol.render.Feature} Feature.
+ * @api
+ */
+ol.render.Feature.prototype.getGeometry = function() {
+  return this;
+};
+
+
+/**
+ * Get the feature properties.
+ * @return {Object.<string, *>} Feature properties.
+ * @api
+ */
+ol.render.Feature.prototype.getProperties = function() {
+  return this.properties_;
+};
+
+
+/**
+ * Get the feature for working with its geometry.
+ * @return {ol.render.Feature} Feature.
+ */
+ol.render.Feature.prototype.getSimplifiedGeometry =
+    ol.render.Feature.prototype.getGeometry;
+
+
+/**
+ * @return {number} Stride.
+ */
+ol.render.Feature.prototype.getStride = function() {
+  return 2;
+};
+
+
+/**
+ * @return {undefined}
+ */
+ol.render.Feature.prototype.getStyleFunction = ol.nullFunction;
+
+
+/**
+ * Get the type of this feature's geometry.
+ * @return {ol.geom.GeometryType} Geometry type.
+ * @api
+ */
+ol.render.Feature.prototype.getType = function() {
+  return this.type_;
+};
+
+goog.provide('ol.renderer.vector');
+
+goog.require('goog.asserts');
+goog.require('ol.render.Feature');
+goog.require('ol.render.IReplayGroup');
+goog.require('ol.style.ImageState');
+goog.require('ol.style.Style');
+
+
+/**
+ * @param {ol.Feature|ol.render.Feature} feature1 Feature 1.
+ * @param {ol.Feature|ol.render.Feature} feature2 Feature 2.
+ * @return {number} Order.
+ */
+ol.renderer.vector.defaultOrder = function(feature1, feature2) {
+  return goog.getUid(feature1) - goog.getUid(feature2);
+};
+
+
+/**
+ * @param {number} resolution Resolution.
+ * @param {number} pixelRatio Pixel ratio.
+ * @return {number} Squared pixel tolerance.
+ */
+ol.renderer.vector.getSquaredTolerance = function(resolution, pixelRatio) {
+  var tolerance = ol.renderer.vector.getTolerance(resolution, pixelRatio);
+  return tolerance * tolerance;
+};
+
+
+/**
+ * @param {number} resolution Resolution.
+ * @param {number} pixelRatio Pixel ratio.
+ * @return {number} Pixel tolerance.
+ */
+ol.renderer.vector.getTolerance = function(resolution, pixelRatio) {
+  return ol.SIMPLIFY_TOLERANCE * resolution / pixelRatio;
+};
+
+
+/**
+ * @param {ol.render.IReplayGroup} replayGroup Replay group.
+ * @param {ol.geom.Circle} geometry Geometry.
+ * @param {ol.style.Style} style Style.
+ * @param {ol.Feature} feature Feature.
+ * @private
+ */
+ol.renderer.vector.renderCircleGeometry_ = function(replayGroup, geometry, style, feature) {
+  var fillStyle = style.getFill();
+  var strokeStyle = style.getStroke();
+  if (fillStyle || strokeStyle) {
+    var polygonReplay = replayGroup.getReplay(
+        style.getZIndex(), ol.render.ReplayType.POLYGON);
+    polygonReplay.setFillStrokeStyle(fillStyle, strokeStyle);
+    polygonReplay.drawCircle(geometry, feature);
+  }
+  var textStyle = style.getText();
+  if (textStyle) {
+    var textReplay = replayGroup.getReplay(
+        style.getZIndex(), ol.render.ReplayType.TEXT);
+    textReplay.setTextStyle(textStyle);
+    textReplay.drawText(geometry.getCenter(), 0, 2, 2, geometry, feature);
+  }
+};
+
+
+/**
+ * @param {ol.render.IReplayGroup} replayGroup Replay group.
+ * @param {ol.Feature|ol.render.Feature} feature Feature.
+ * @param {ol.style.Style} style Style.
+ * @param {number} squaredTolerance Squared tolerance.
+ * @param {function(this: T, ol.events.Event)} listener Listener function.
+ * @param {T} thisArg Value to use as `this` when executing `listener`.
+ * @return {boolean} `true` if style is loading.
+ * @template T
+ */
+ol.renderer.vector.renderFeature = function(
+    replayGroup, feature, style, squaredTolerance, listener, thisArg) {
+  var loading = false;
+  var imageStyle, imageState;
+  imageStyle = style.getImage();
+  if (imageStyle) {
+    imageState = imageStyle.getImageState();
+    if (imageState == ol.style.ImageState.LOADED ||
+        imageState == ol.style.ImageState.ERROR) {
+      imageStyle.unlistenImageChange(listener, thisArg);
+    } else {
+      if (imageState == ol.style.ImageState.IDLE) {
+        imageStyle.load();
+      }
+      imageState = imageStyle.getImageState();
+      goog.asserts.assert(imageState == ol.style.ImageState.LOADING,
+          'imageState should be LOADING');
+      imageStyle.listenImageChange(listener, thisArg);
+      loading = true;
+    }
+  }
+  ol.renderer.vector.renderFeature_(replayGroup, feature, style,
+      squaredTolerance);
+  return loading;
+};
+
+
+/**
+ * @param {ol.render.IReplayGroup} replayGroup Replay group.
+ * @param {ol.Feature|ol.render.Feature} feature Feature.
+ * @param {ol.style.Style} style Style.
+ * @param {number} squaredTolerance Squared tolerance.
+ * @private
+ */
+ol.renderer.vector.renderFeature_ = function(
+    replayGroup, feature, style, squaredTolerance) {
+  var geometry = style.getGeometryFunction()(feature);
+  if (!geometry) {
+    return;
+  }
+  var simplifiedGeometry = geometry.getSimplifiedGeometry(squaredTolerance);
+  var geometryRenderer =
+      ol.renderer.vector.GEOMETRY_RENDERERS_[simplifiedGeometry.getType()];
+  goog.asserts.assert(geometryRenderer !== undefined,
+      'geometryRenderer should be defined');
+  geometryRenderer(replayGroup, simplifiedGeometry, style, feature);
+};
+
+
+/**
+ * @param {ol.render.IReplayGroup} replayGroup Replay group.
+ * @param {ol.geom.GeometryCollection} geometry Geometry.
+ * @param {ol.style.Style} style Style.
+ * @param {ol.Feature} feature Feature.
+ * @private
+ */
+ol.renderer.vector.renderGeometryCollectionGeometry_ = function(replayGroup, geometry, style, feature) {
+  var geometries = geometry.getGeometriesArray();
+  var i, ii;
+  for (i = 0, ii = geometries.length; i < ii; ++i) {
+    var geometryRenderer =
+        ol.renderer.vector.GEOMETRY_RENDERERS_[geometries[i].getType()];
+    goog.asserts.assert(geometryRenderer !== undefined,
+        'geometryRenderer should be defined');
+    geometryRenderer(replayGroup, geometries[i], style, feature);
+  }
+};
+
+
+/**
+ * @param {ol.render.IReplayGroup} replayGroup Replay group.
+ * @param {ol.geom.LineString|ol.render.Feature} geometry Geometry.
+ * @param {ol.style.Style} style Style.
+ * @param {ol.Feature|ol.render.Feature} feature Feature.
+ * @private
+ */
+ol.renderer.vector.renderLineStringGeometry_ = function(replayGroup, geometry, style, feature) {
+  var strokeStyle = style.getStroke();
+  if (strokeStyle) {
+    var lineStringReplay = replayGroup.getReplay(
+        style.getZIndex(), ol.render.ReplayType.LINE_STRING);
+    lineStringReplay.setFillStrokeStyle(null, strokeStyle);
+    lineStringReplay.drawLineString(geometry, feature);
+  }
+  var textStyle = style.getText();
+  if (textStyle) {
+    var textReplay = replayGroup.getReplay(
+        style.getZIndex(), ol.render.ReplayType.TEXT);
+    textReplay.setTextStyle(textStyle);
+    textReplay.drawText(geometry.getFlatMidpoint(), 0, 2, 2, geometry, feature);
+  }
+};
+
+
+/**
+ * @param {ol.render.IReplayGroup} replayGroup Replay group.
+ * @param {ol.geom.MultiLineString|ol.render.Feature} geometry Geometry.
+ * @param {ol.style.Style} style Style.
+ * @param {ol.Feature|ol.render.Feature} feature Feature.
+ * @private
+ */
+ol.renderer.vector.renderMultiLineStringGeometry_ = function(replayGroup, geometry, style, feature) {
+  var strokeStyle = style.getStroke();
+  if (strokeStyle) {
+    var lineStringReplay = replayGroup.getReplay(
+        style.getZIndex(), ol.render.ReplayType.LINE_STRING);
+    lineStringReplay.setFillStrokeStyle(null, strokeStyle);
+    lineStringReplay.drawMultiLineString(geometry, feature);
+  }
+  var textStyle = style.getText();
+  if (textStyle) {
+    var textReplay = replayGroup.getReplay(
+        style.getZIndex(), ol.render.ReplayType.TEXT);
+    textReplay.setTextStyle(textStyle);
+    var flatMidpointCoordinates = geometry.getFlatMidpoints();
+    textReplay.drawText(flatMidpointCoordinates, 0,
+        flatMidpointCoordinates.length, 2, geometry, feature);
+  }
+};
+
+
+/**
+ * @param {ol.render.IReplayGroup} replayGroup Replay group.
+ * @param {ol.geom.MultiPolygon} geometry Geometry.
+ * @param {ol.style.Style} style Style.
+ * @param {ol.Feature} feature Feature.
+ * @private
+ */
+ol.renderer.vector.renderMultiPolygonGeometry_ = function(replayGroup, geometry, style, feature) {
+  var fillStyle = style.getFill();
+  var strokeStyle = style.getStroke();
+  if (strokeStyle || fillStyle) {
+    var polygonReplay = replayGroup.getReplay(
+        style.getZIndex(), ol.render.ReplayType.POLYGON);
+    polygonReplay.setFillStrokeStyle(fillStyle, strokeStyle);
+    polygonReplay.drawMultiPolygon(geometry, feature);
+  }
+  var textStyle = style.getText();
+  if (textStyle) {
+    var textReplay = replayGroup.getReplay(
+        style.getZIndex(), ol.render.ReplayType.TEXT);
+    textReplay.setTextStyle(textStyle);
+    var flatInteriorPointCoordinates = geometry.getFlatInteriorPoints();
+    textReplay.drawText(flatInteriorPointCoordinates, 0,
+        flatInteriorPointCoordinates.length, 2, geometry, feature);
+  }
+};
+
+
+/**
+ * @param {ol.render.IReplayGroup} replayGroup Replay group.
+ * @param {ol.geom.Point|ol.render.Feature} geometry Geometry.
+ * @param {ol.style.Style} style Style.
+ * @param {ol.Feature|ol.render.Feature} feature Feature.
+ * @private
+ */
+ol.renderer.vector.renderPointGeometry_ = function(replayGroup, geometry, style, feature) {
+  var imageStyle = style.getImage();
+  if (imageStyle) {
+    if (imageStyle.getImageState() != ol.style.ImageState.LOADED) {
+      return;
+    }
+    var imageReplay = replayGroup.getReplay(
+        style.getZIndex(), ol.render.ReplayType.IMAGE);
+    imageReplay.setImageStyle(imageStyle);
+    imageReplay.drawPoint(geometry, feature);
+  }
+  var textStyle = style.getText();
+  if (textStyle) {
+    var textReplay = replayGroup.getReplay(
+        style.getZIndex(), ol.render.ReplayType.TEXT);
+    textReplay.setTextStyle(textStyle);
+    textReplay.drawText(geometry.getFlatCoordinates(), 0, 2, 2, geometry,
+        feature);
+  }
+};
+
+
+/**
+ * @param {ol.render.IReplayGroup} replayGroup Replay group.
+ * @param {ol.geom.MultiPoint|ol.render.Feature} geometry Geometry.
+ * @param {ol.style.Style} style Style.
+ * @param {ol.Feature|ol.render.Feature} feature Feature.
+ * @private
+ */
+ol.renderer.vector.renderMultiPointGeometry_ = function(replayGroup, geometry, style, feature) {
+  var imageStyle = style.getImage();
+  if (imageStyle) {
+    if (imageStyle.getImageState() != ol.style.ImageState.LOADED) {
+      return;
+    }
+    var imageReplay = replayGroup.getReplay(
+        style.getZIndex(), ol.render.ReplayType.IMAGE);
+    imageReplay.setImageStyle(imageStyle);
+    imageReplay.drawMultiPoint(geometry, feature);
+  }
+  var textStyle = style.getText();
+  if (textStyle) {
+    var textReplay = replayGroup.getReplay(
+        style.getZIndex(), ol.render.ReplayType.TEXT);
+    textReplay.setTextStyle(textStyle);
+    var flatCoordinates = geometry.getFlatCoordinates();
+    textReplay.drawText(flatCoordinates, 0, flatCoordinates.length,
+        geometry.getStride(), geometry, feature);
+  }
+};
+
+
+/**
+ * @param {ol.render.IReplayGroup} replayGroup Replay group.
+ * @param {ol.geom.Polygon|ol.render.Feature} geometry Geometry.
+ * @param {ol.style.Style} style Style.
+ * @param {ol.Feature|ol.render.Feature} feature Feature.
+ * @private
+ */
+ol.renderer.vector.renderPolygonGeometry_ = function(replayGroup, geometry, style, feature) {
+  var fillStyle = style.getFill();
+  var strokeStyle = style.getStroke();
+  if (fillStyle || strokeStyle) {
+    var polygonReplay = replayGroup.getReplay(
+        style.getZIndex(), ol.render.ReplayType.POLYGON);
+    polygonReplay.setFillStrokeStyle(fillStyle, strokeStyle);
+    polygonReplay.drawPolygon(geometry, feature);
+  }
+  var textStyle = style.getText();
+  if (textStyle) {
+    var textReplay = replayGroup.getReplay(
+        style.getZIndex(), ol.render.ReplayType.TEXT);
+    textReplay.setTextStyle(textStyle);
+    textReplay.drawText(
+        geometry.getFlatInteriorPoint(), 0, 2, 2, geometry, feature);
+  }
+};
+
+
+/**
+ * @const
+ * @private
+ * @type {Object.<ol.geom.GeometryType,
+ *                function(ol.render.IReplayGroup, ol.geom.Geometry,
+ *                         ol.style.Style, Object)>}
+ */
+ol.renderer.vector.GEOMETRY_RENDERERS_ = {
+  'Point': ol.renderer.vector.renderPointGeometry_,
+  'LineString': ol.renderer.vector.renderLineStringGeometry_,
+  'Polygon': ol.renderer.vector.renderPolygonGeometry_,
+  'MultiPoint': ol.renderer.vector.renderMultiPointGeometry_,
+  'MultiLineString': ol.renderer.vector.renderMultiLineStringGeometry_,
+  'MultiPolygon': ol.renderer.vector.renderMultiPolygonGeometry_,
+  'GeometryCollection': ol.renderer.vector.renderGeometryCollectionGeometry_,
+  'Circle': ol.renderer.vector.renderCircleGeometry_
+};
+
+goog.provide('ol.ImageCanvas');
+
+goog.require('goog.asserts');
+goog.require('ol.ImageBase');
+goog.require('ol.ImageState');
+
+
+/**
+ * @constructor
+ * @extends {ol.ImageBase}
+ * @param {ol.Extent} extent Extent.
+ * @param {number} resolution Resolution.
+ * @param {number} pixelRatio Pixel ratio.
+ * @param {Array.<ol.Attribution>} attributions Attributions.
+ * @param {HTMLCanvasElement} canvas Canvas.
+ * @param {ol.ImageCanvasLoader=} opt_loader Optional loader function to
+ *     support asynchronous canvas drawing.
+ */
+ol.ImageCanvas = function(extent, resolution, pixelRatio, attributions,
+    canvas, opt_loader) {
+
+  /**
+   * Optional canvas loader function.
+   * @type {?ol.ImageCanvasLoader}
+   * @private
+   */
+  this.loader_ = opt_loader !== undefined ? opt_loader : null;
+
+  var state = opt_loader !== undefined ?
+      ol.ImageState.IDLE : ol.ImageState.LOADED;
+
+  ol.ImageBase.call(this, extent, resolution, pixelRatio, state, attributions);
+
+  /**
+   * @private
+   * @type {HTMLCanvasElement}
+   */
+  this.canvas_ = canvas;
+
+  /**
+   * @private
+   * @type {Error}
+   */
+  this.error_ = null;
+
+};
+ol.inherits(ol.ImageCanvas, ol.ImageBase);
+
+
+/**
+ * Get any error associated with asynchronous rendering.
+ * @return {Error} Any error that occurred during rendering.
+ */
+ol.ImageCanvas.prototype.getError = function() {
+  return this.error_;
+};
+
+
+/**
+ * Handle async drawing complete.
+ * @param {Error} err Any error during drawing.
+ * @private
+ */
+ol.ImageCanvas.prototype.handleLoad_ = function(err) {
+  if (err) {
+    this.error_ = err;
+    this.state = ol.ImageState.ERROR;
+  } else {
+    this.state = ol.ImageState.LOADED;
+  }
+  this.changed();
+};
+
+
+/**
+ * Trigger drawing on canvas.
+ */
+ol.ImageCanvas.prototype.load = function() {
+  if (this.state == ol.ImageState.IDLE) {
+    goog.asserts.assert(this.loader_, 'this.loader_ must be set');
+    this.state = ol.ImageState.LOADING;
+    this.changed();
+    this.loader_(this.handleLoad_.bind(this));
+  }
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.ImageCanvas.prototype.getImage = function(opt_context) {
+  return this.canvas_;
+};
+
+goog.provide('ol.reproj');
+
+goog.require('ol.dom');
+goog.require('ol.extent');
+goog.require('ol.math');
+goog.require('ol.proj');
+
+
+/**
+ * We need to employ more sophisticated solution
+ * if the web browser antialiases clipping edges on canvas.
+ *
+ * Currently only Chrome does not antialias the edges, but this is probably
+ * going to be "fixed" in the future: http://crbug.com/424291
+ *
+ * @type {boolean}
+ * @private
+ */
+ol.reproj.browserAntialiasesClip_ = (function(winNav, winChrome) {
+  // Adapted from http://stackoverflow.com/questions/4565112/javascript-how-to-find-out-if-the-user-browser-is-chrome
+  var isOpera = winNav.userAgent.indexOf('OPR') > -1;
+  var isIEedge = winNav.userAgent.indexOf('Edge') > -1;
+  return !(
+    !winNav.userAgent.match('CriOS') &&  // Not Chrome on iOS
+    winChrome !== null && winChrome !== undefined && // Has chrome in window
+    winNav.vendor === 'Google Inc.' && // Vendor is Google.
+    isOpera == false && // Not Opera
+    isIEedge == false // Not Edge
+  );
+})(ol.global.navigator, ol.global.chrome);
+
+
+/**
+ * Calculates ideal resolution to use from the source in order to achieve
+ * pixel mapping as close as possible to 1:1 during reprojection.
+ * The resolution is calculated regardless of what resolutions
+ * are actually available in the dataset (TileGrid, Image, ...).
+ *
+ * @param {ol.proj.Projection} sourceProj Source projection.
+ * @param {ol.proj.Projection} targetProj Target projection.
+ * @param {ol.Coordinate} targetCenter Target center.
+ * @param {number} targetResolution Target resolution.
+ * @return {number} The best resolution to use. Can be +-Infinity, NaN or 0.
+ */
+ol.reproj.calculateSourceResolution = function(sourceProj, targetProj,
+    targetCenter, targetResolution) {
+
+  var sourceCenter = ol.proj.transform(targetCenter, targetProj, sourceProj);
+
+  // calculate the ideal resolution of the source data
+  var sourceResolution =
+      targetProj.getPointResolution(targetResolution, targetCenter);
+
+  var targetMetersPerUnit = targetProj.getMetersPerUnit();
+  if (targetMetersPerUnit !== undefined) {
+    sourceResolution *= targetMetersPerUnit;
+  }
+  var sourceMetersPerUnit = sourceProj.getMetersPerUnit();
+  if (sourceMetersPerUnit !== undefined) {
+    sourceResolution /= sourceMetersPerUnit;
+  }
+
+  // Based on the projection properties, the point resolution at the specified
+  // coordinates may be slightly different. We need to reverse-compensate this
+  // in order to achieve optimal results.
+
+  var compensationFactor =
+      sourceProj.getPointResolution(sourceResolution, sourceCenter) /
+      sourceResolution;
+
+  if (isFinite(compensationFactor) && compensationFactor > 0) {
+    sourceResolution /= compensationFactor;
+  }
+
+  return sourceResolution;
+};
+
+
+/**
+ * Enlarge the clipping triangle point by 1 pixel to ensure the edges overlap
+ * in order to mask gaps caused by antialiasing.
+ *
+ * @param {number} centroidX Centroid of the triangle (x coordinate in pixels).
+ * @param {number} centroidY Centroid of the triangle (y coordinate in pixels).
+ * @param {number} x X coordinate of the point (in pixels).
+ * @param {number} y Y coordinate of the point (in pixels).
+ * @return {ol.Coordinate} New point 1 px farther from the centroid.
+ * @private
+ */
+ol.reproj.enlargeClipPoint_ = function(centroidX, centroidY, x, y) {
+  var dX = x - centroidX, dY = y - centroidY;
+  var distance = Math.sqrt(dX * dX + dY * dY);
+  return [Math.round(x + dX / distance), Math.round(y + dY / distance)];
+};
+
+
+/**
+ * Renders the source data into new canvas based on the triangulation.
+ *
+ * @param {number} width Width of the canvas.
+ * @param {number} height Height of the canvas.
+ * @param {number} pixelRatio Pixel ratio.
+ * @param {number} sourceResolution Source resolution.
+ * @param {ol.Extent} sourceExtent Extent of the data source.
+ * @param {number} targetResolution Target resolution.
+ * @param {ol.Extent} targetExtent Target extent.
+ * @param {ol.reproj.Triangulation} triangulation Calculated triangulation.
+ * @param {Array.<{extent: ol.Extent,
+ *                 image: (HTMLCanvasElement|Image|HTMLVideoElement)}>} sources
+ *             Array of sources.
+ * @param {number} gutter Gutter of the sources.
+ * @param {boolean=} opt_renderEdges Render reprojection edges.
+ * @return {HTMLCanvasElement} Canvas with reprojected data.
+ */
+ol.reproj.render = function(width, height, pixelRatio,
+    sourceResolution, sourceExtent, targetResolution, targetExtent,
+    triangulation, sources, gutter, opt_renderEdges) {
+
+  var context = ol.dom.createCanvasContext2D(Math.round(pixelRatio * width),
+                                             Math.round(pixelRatio * height));
+
+  if (sources.length === 0) {
+    return context.canvas;
+  }
+
+  context.scale(pixelRatio, pixelRatio);
+
+  var sourceDataExtent = ol.extent.createEmpty();
+  sources.forEach(function(src, i, arr) {
+    ol.extent.extend(sourceDataExtent, src.extent);
+  });
+
+  var canvasWidthInUnits = ol.extent.getWidth(sourceDataExtent);
+  var canvasHeightInUnits = ol.extent.getHeight(sourceDataExtent);
+  var stitchContext = ol.dom.createCanvasContext2D(
+      Math.round(pixelRatio * canvasWidthInUnits / sourceResolution),
+      Math.round(pixelRatio * canvasHeightInUnits / sourceResolution));
+
+  var stitchScale = pixelRatio / sourceResolution;
+
+  sources.forEach(function(src, i, arr) {
+    var xPos = src.extent[0] - sourceDataExtent[0];
+    var yPos = -(src.extent[3] - sourceDataExtent[3]);
+    var srcWidth = ol.extent.getWidth(src.extent);
+    var srcHeight = ol.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 = ol.extent.getTopLeft(targetExtent);
+
+  triangulation.getTriangles().forEach(function(triangle, i, arr) {
+    /* Calculate affine transform (src -> dst)
+     * Resulting matrix can be used to transform coordinate
+     * from `sourceProjection` to destination pixels.
+     *
+     * To optimize number of context calls and increase numerical stability,
+     * we also do the following operations:
+     * trans(-topLeftExtentCorner), scale(1 / targetResolution), scale(1, -1)
+     * here before solving the linear system so [ui, vi] are pixel coordinates.
+     *
+     * Src points: xi, yi
+     * Dst points: ui, vi
+     * Affine coefficients: aij
+     *
+     * | x0 y0 1  0  0 0 |   |a00|   |u0|
+     * | x1 y1 1  0  0 0 |   |a01|   |u1|
+     * | x2 y2 1  0  0 0 | x |a02| = |u2|
+     * |  0  0 0 x0 y0 1 |   |a10|   |v0|
+     * |  0  0 0 x1 y1 1 |   |a11|   |v1|
+     * |  0  0 0 x2 y2 1 |   |a12|   |v2|
+     */
+    var source = triangle.source, target = triangle.target;
+    var x0 = source[0][0], y0 = source[0][1],
+        x1 = source[1][0], y1 = source[1][1],
+        x2 = source[2][0], y2 = source[2][1];
+    var u0 = (target[0][0] - targetTopLeft[0]) / targetResolution,
+        v0 = -(target[0][1] - targetTopLeft[1]) / targetResolution;
+    var u1 = (target[1][0] - targetTopLeft[0]) / targetResolution,
+        v1 = -(target[1][1] - targetTopLeft[1]) / targetResolution;
+    var u2 = (target[2][0] - targetTopLeft[0]) / targetResolution,
+        v2 = -(target[2][1] - targetTopLeft[1]) / targetResolution;
+
+    // Shift all the source points to improve numerical stability
+    // of all the subsequent calculations. The [x0, y0] is used here.
+    // This is also used to simplify the linear system.
+    var sourceNumericalShiftX = x0, sourceNumericalShiftY = y0;
+    x0 = 0;
+    y0 = 0;
+    x1 -= sourceNumericalShiftX;
+    y1 -= sourceNumericalShiftY;
+    x2 -= sourceNumericalShiftX;
+    y2 -= sourceNumericalShiftY;
+
+    var augmentedMatrix = [
+      [x1, y1, 0, 0, u1 - u0],
+      [x2, y2, 0, 0, u2 - u0],
+      [0, 0, x1, y1, v1 - v0],
+      [0, 0, x2, y2, v2 - v0]
+    ];
+    var affineCoefs = ol.math.solveLinearSystem(augmentedMatrix);
+    if (!affineCoefs) {
+      return;
+    }
+
+    context.save();
+    context.beginPath();
+    if (ol.reproj.browserAntialiasesClip_) {
+      var centroidX = (u0 + u1 + u2) / 3, centroidY = (v0 + v1 + v2) / 3;
+      var p0 = ol.reproj.enlargeClipPoint_(centroidX, centroidY, u0, v0);
+      var p1 = ol.reproj.enlargeClipPoint_(centroidX, centroidY, u1, v1);
+      var p2 = ol.reproj.enlargeClipPoint_(centroidX, centroidY, u2, v2);
+
+      context.moveTo(p0[0], p0[1]);
+      context.lineTo(p1[0], p1[1]);
+      context.lineTo(p2[0], p2[1]);
+    } else {
+      context.moveTo(u0, v0);
+      context.lineTo(u1, v1);
+      context.lineTo(u2, v2);
+    }
+    context.closePath();
+    context.clip();
+
+    context.transform(
+        affineCoefs[0], affineCoefs[2], affineCoefs[1], affineCoefs[3], u0, v0);
+
+    context.translate(sourceDataExtent[0] - sourceNumericalShiftX,
+                      sourceDataExtent[3] - sourceNumericalShiftY);
+
+    context.scale(sourceResolution / pixelRatio,
+                  -sourceResolution / pixelRatio);
+
+    context.drawImage(stitchContext.canvas, 0, 0);
+    context.restore();
+  });
+
+  if (opt_renderEdges) {
+    context.save();
+
+    context.strokeStyle = 'black';
+    context.lineWidth = 1;
+
+    triangulation.getTriangles().forEach(function(triangle, i, arr) {
+      var target = triangle.target;
+      var u0 = (target[0][0] - targetTopLeft[0]) / targetResolution,
+          v0 = -(target[0][1] - targetTopLeft[1]) / targetResolution;
+      var u1 = (target[1][0] - targetTopLeft[0]) / targetResolution,
+          v1 = -(target[1][1] - targetTopLeft[1]) / targetResolution;
+      var u2 = (target[2][0] - targetTopLeft[0]) / targetResolution,
+          v2 = -(target[2][1] - targetTopLeft[1]) / targetResolution;
+
+      context.beginPath();
+      context.moveTo(u0, v0);
+      context.lineTo(u1, v1);
+      context.lineTo(u2, v2);
+      context.closePath();
+      context.stroke();
+    });
+
+    context.restore();
+  }
+  return context.canvas;
+};
+
+goog.provide('ol.reproj.Triangulation');
+
+goog.require('goog.asserts');
+goog.require('ol.extent');
+goog.require('ol.math');
+goog.require('ol.proj');
+
+
+/**
+ * @classdesc
+ * Class containing triangulation of the given target extent.
+ * Used for determining source data and the reprojection itself.
+ *
+ * @param {ol.proj.Projection} sourceProj Source projection.
+ * @param {ol.proj.Projection} targetProj Target projection.
+ * @param {ol.Extent} targetExtent Target extent to triangulate.
+ * @param {ol.Extent} maxSourceExtent Maximal source extent that can be used.
+ * @param {number} errorThreshold Acceptable error (in source units).
+ * @constructor
+ */
+ol.reproj.Triangulation = function(sourceProj, targetProj, targetExtent,
+    maxSourceExtent, errorThreshold) {
+
+  /**
+   * @type {ol.proj.Projection}
+   * @private
+   */
+  this.sourceProj_ = sourceProj;
+
+  /**
+   * @type {ol.proj.Projection}
+   * @private
+   */
+  this.targetProj_ = targetProj;
+
+  /** @type {!Object.<string, ol.Coordinate>} */
+  var transformInvCache = {};
+  var transformInv = ol.proj.getTransform(this.targetProj_, this.sourceProj_);
+
+  /**
+   * @param {ol.Coordinate} c A coordinate.
+   * @return {ol.Coordinate} Transformed coordinate.
+   * @private
+   */
+  this.transformInv_ = function(c) {
+    var key = c[0] + '/' + c[1];
+    if (!transformInvCache[key]) {
+      transformInvCache[key] = transformInv(c);
+    }
+    return transformInvCache[key];
+  };
+
+  /**
+   * @type {ol.Extent}
+   * @private
+   */
+  this.maxSourceExtent_ = maxSourceExtent;
+
+  /**
+   * @type {number}
+   * @private
+   */
+  this.errorThresholdSquared_ = errorThreshold * errorThreshold;
+
+  /**
+   * @type {Array.<ol.ReprojTriangle>}
+   * @private
+   */
+  this.triangles_ = [];
+
+  /**
+   * Indicates that the triangulation crosses edge of the source projection.
+   * @type {boolean}
+   * @private
+   */
+  this.wrapsXInSource_ = false;
+
+  /**
+   * @type {boolean}
+   * @private
+   */
+  this.canWrapXInSource_ = this.sourceProj_.canWrapX() &&
+      !!maxSourceExtent &&
+      !!this.sourceProj_.getExtent() &&
+      (ol.extent.getWidth(maxSourceExtent) ==
+       ol.extent.getWidth(this.sourceProj_.getExtent()));
+
+  /**
+   * @type {?number}
+   * @private
+   */
+  this.sourceWorldWidth_ = this.sourceProj_.getExtent() ?
+      ol.extent.getWidth(this.sourceProj_.getExtent()) : null;
+
+  /**
+   * @type {?number}
+   * @private
+   */
+  this.targetWorldWidth_ = this.targetProj_.getExtent() ?
+      ol.extent.getWidth(this.targetProj_.getExtent()) : null;
+
+  var destinationTopLeft = ol.extent.getTopLeft(targetExtent);
+  var destinationTopRight = ol.extent.getTopRight(targetExtent);
+  var destinationBottomRight = ol.extent.getBottomRight(targetExtent);
+  var destinationBottomLeft = ol.extent.getBottomLeft(targetExtent);
+  var sourceTopLeft = this.transformInv_(destinationTopLeft);
+  var sourceTopRight = this.transformInv_(destinationTopRight);
+  var sourceBottomRight = this.transformInv_(destinationBottomRight);
+  var sourceBottomLeft = this.transformInv_(destinationBottomLeft);
+
+  this.addQuad_(
+      destinationTopLeft, destinationTopRight,
+      destinationBottomRight, destinationBottomLeft,
+      sourceTopLeft, sourceTopRight, sourceBottomRight, sourceBottomLeft,
+      ol.RASTER_REPROJECTION_MAX_SUBDIVISION);
+
+  if (this.wrapsXInSource_) {
+    // Fix coordinates (ol.proj returns wrapped coordinates, "unwrap" here).
+    // This significantly simplifies the rest of the reprojection process.
+
+    goog.asserts.assert(this.sourceWorldWidth_ !== null);
+    var leftBound = Infinity;
+    this.triangles_.forEach(function(triangle, i, arr) {
+      leftBound = Math.min(leftBound,
+          triangle.source[0][0], triangle.source[1][0], triangle.source[2][0]);
+    });
+
+    // Shift triangles to be as close to `leftBound` as possible
+    // (if the distance is more than `worldWidth / 2` it can be closer.
+    this.triangles_.forEach(function(triangle) {
+      if (Math.max(triangle.source[0][0], triangle.source[1][0],
+          triangle.source[2][0]) - leftBound > this.sourceWorldWidth_ / 2) {
+        var newTriangle = [[triangle.source[0][0], triangle.source[0][1]],
+                           [triangle.source[1][0], triangle.source[1][1]],
+                           [triangle.source[2][0], triangle.source[2][1]]];
+        if ((newTriangle[0][0] - leftBound) > this.sourceWorldWidth_ / 2) {
+          newTriangle[0][0] -= this.sourceWorldWidth_;
+        }
+        if ((newTriangle[1][0] - leftBound) > this.sourceWorldWidth_ / 2) {
+          newTriangle[1][0] -= this.sourceWorldWidth_;
+        }
+        if ((newTriangle[2][0] - leftBound) > this.sourceWorldWidth_ / 2) {
+          newTriangle[2][0] -= this.sourceWorldWidth_;
+        }
+
+        // Rarely (if the extent contains both the dateline and prime meridian)
+        // the shift can in turn break some triangles.
+        // Detect this here and don't shift in such cases.
+        var minX = Math.min(
+            newTriangle[0][0], newTriangle[1][0], newTriangle[2][0]);
+        var maxX = Math.max(
+            newTriangle[0][0], newTriangle[1][0], newTriangle[2][0]);
+        if ((maxX - minX) < this.sourceWorldWidth_ / 2) {
+          triangle.source = newTriangle;
+        }
+      }
+    }, this);
+  }
+
+  transformInvCache = {};
+};
+
+
+/**
+ * Adds triangle to the triangulation.
+ * @param {ol.Coordinate} a The target a coordinate.
+ * @param {ol.Coordinate} b The target b coordinate.
+ * @param {ol.Coordinate} c The target c coordinate.
+ * @param {ol.Coordinate} aSrc The source a coordinate.
+ * @param {ol.Coordinate} bSrc The source b coordinate.
+ * @param {ol.Coordinate} cSrc The source c coordinate.
+ * @private
+ */
+ol.reproj.Triangulation.prototype.addTriangle_ = function(a, b, c,
+    aSrc, bSrc, cSrc) {
+  this.triangles_.push({
+    source: [aSrc, bSrc, cSrc],
+    target: [a, b, c]
+  });
+};
+
+
+/**
+ * Adds quad (points in clock-wise order) to the triangulation
+ * (and reprojects the vertices) if valid.
+ * Performs quad subdivision if needed to increase precision.
+ *
+ * @param {ol.Coordinate} a The target a coordinate.
+ * @param {ol.Coordinate} b The target b coordinate.
+ * @param {ol.Coordinate} c The target c coordinate.
+ * @param {ol.Coordinate} d The target d coordinate.
+ * @param {ol.Coordinate} aSrc The source a coordinate.
+ * @param {ol.Coordinate} bSrc The source b coordinate.
+ * @param {ol.Coordinate} cSrc The source c coordinate.
+ * @param {ol.Coordinate} dSrc The source d coordinate.
+ * @param {number} maxSubdivision Maximal allowed subdivision of the quad.
+ * @private
+ */
+ol.reproj.Triangulation.prototype.addQuad_ = function(a, b, c, d,
+    aSrc, bSrc, cSrc, dSrc, maxSubdivision) {
+
+  var sourceQuadExtent = ol.extent.boundingExtent([aSrc, bSrc, cSrc, dSrc]);
+  var sourceCoverageX = this.sourceWorldWidth_ ?
+      ol.extent.getWidth(sourceQuadExtent) / this.sourceWorldWidth_ : null;
+
+  // when the quad is wrapped in the source projection
+  // it covers most of the projection extent, but not fully
+  var wrapsX = this.sourceProj_.canWrapX() &&
+               sourceCoverageX > 0.5 && sourceCoverageX < 1;
+
+  var needsSubdivision = false;
+
+  if (maxSubdivision > 0) {
+    if (this.targetProj_.isGlobal() && this.targetWorldWidth_) {
+      var targetQuadExtent = ol.extent.boundingExtent([a, b, c, d]);
+      var targetCoverageX =
+          ol.extent.getWidth(targetQuadExtent) / this.targetWorldWidth_;
+      needsSubdivision |=
+          targetCoverageX > ol.RASTER_REPROJECTION_MAX_TRIANGLE_WIDTH;
+    }
+    if (!wrapsX && this.sourceProj_.isGlobal() && sourceCoverageX) {
+      needsSubdivision |=
+          sourceCoverageX > ol.RASTER_REPROJECTION_MAX_TRIANGLE_WIDTH;
+    }
+  }
+
+  if (!needsSubdivision && this.maxSourceExtent_) {
+    if (!ol.extent.intersects(sourceQuadExtent, this.maxSourceExtent_)) {
+      // whole quad outside source projection extent -> ignore
+      return;
+    }
+  }
+
+  if (!needsSubdivision) {
+    if (!isFinite(aSrc[0]) || !isFinite(aSrc[1]) ||
+        !isFinite(bSrc[0]) || !isFinite(bSrc[1]) ||
+        !isFinite(cSrc[0]) || !isFinite(cSrc[1]) ||
+        !isFinite(dSrc[0]) || !isFinite(dSrc[1])) {
+      if (maxSubdivision > 0) {
+        needsSubdivision = true;
+      } else {
+        return;
+      }
+    }
+  }
+
+  if (maxSubdivision > 0) {
+    if (!needsSubdivision) {
+      var center = [(a[0] + c[0]) / 2, (a[1] + c[1]) / 2];
+      var centerSrc = this.transformInv_(center);
+
+      var dx;
+      if (wrapsX) {
+        goog.asserts.assert(this.sourceWorldWidth_);
+        var centerSrcEstimX =
+            (ol.math.modulo(aSrc[0], this.sourceWorldWidth_) +
+             ol.math.modulo(cSrc[0], this.sourceWorldWidth_)) / 2;
+        dx = centerSrcEstimX -
+            ol.math.modulo(centerSrc[0], this.sourceWorldWidth_);
+      } else {
+        dx = (aSrc[0] + cSrc[0]) / 2 - centerSrc[0];
+      }
+      var dy = (aSrc[1] + cSrc[1]) / 2 - centerSrc[1];
+      var centerSrcErrorSquared = dx * dx + dy * dy;
+      needsSubdivision = centerSrcErrorSquared > this.errorThresholdSquared_;
+    }
+    if (needsSubdivision) {
+      if (Math.abs(a[0] - c[0]) <= Math.abs(a[1] - c[1])) {
+        // split horizontally (top & bottom)
+        var bc = [(b[0] + c[0]) / 2, (b[1] + c[1]) / 2];
+        var bcSrc = this.transformInv_(bc);
+        var da = [(d[0] + a[0]) / 2, (d[1] + a[1]) / 2];
+        var daSrc = this.transformInv_(da);
+
+        this.addQuad_(
+            a, b, bc, da, aSrc, bSrc, bcSrc, daSrc, maxSubdivision - 1);
+        this.addQuad_(
+            da, bc, c, d, daSrc, bcSrc, cSrc, dSrc, maxSubdivision - 1);
+      } else {
+        // split vertically (left & right)
+        var ab = [(a[0] + b[0]) / 2, (a[1] + b[1]) / 2];
+        var abSrc = this.transformInv_(ab);
+        var cd = [(c[0] + d[0]) / 2, (c[1] + d[1]) / 2];
+        var cdSrc = this.transformInv_(cd);
+
+        this.addQuad_(
+            a, ab, cd, d, aSrc, abSrc, cdSrc, dSrc, maxSubdivision - 1);
+        this.addQuad_(
+            ab, b, c, cd, abSrc, bSrc, cSrc, cdSrc, maxSubdivision - 1);
+      }
+      return;
+    }
+  }
+
+  if (wrapsX) {
+    if (!this.canWrapXInSource_) {
+      return;
+    }
+    this.wrapsXInSource_ = true;
+  }
+
+  this.addTriangle_(a, c, d, aSrc, cSrc, dSrc);
+  this.addTriangle_(a, b, c, aSrc, bSrc, cSrc);
+};
+
+
+/**
+ * Calculates extent of the 'source' coordinates from all the triangles.
+ *
+ * @return {ol.Extent} Calculated extent.
+ */
+ol.reproj.Triangulation.prototype.calculateSourceExtent = function() {
+  var extent = ol.extent.createEmpty();
+
+  this.triangles_.forEach(function(triangle, i, arr) {
+    var src = triangle.source;
+    ol.extent.extendCoordinate(extent, src[0]);
+    ol.extent.extendCoordinate(extent, src[1]);
+    ol.extent.extendCoordinate(extent, src[2]);
+  });
+
+  return extent;
+};
+
+
+/**
+ * @return {Array.<ol.ReprojTriangle>} Array of the calculated triangles.
+ */
+ol.reproj.Triangulation.prototype.getTriangles = function() {
+  return this.triangles_;
+};
+
+goog.provide('ol.reproj.Image');
+
+goog.require('goog.asserts');
+goog.require('ol.events');
+goog.require('ol.events.EventType');
+goog.require('ol.ImageBase');
+goog.require('ol.ImageState');
+goog.require('ol.extent');
+goog.require('ol.proj');
+goog.require('ol.reproj');
+goog.require('ol.reproj.Triangulation');
+
+
+/**
+ * @classdesc
+ * Class encapsulating single reprojected image.
+ * See {@link ol.source.Image}.
+ *
+ * @constructor
+ * @extends {ol.ImageBase}
+ * @param {ol.proj.Projection} sourceProj Source projection (of the data).
+ * @param {ol.proj.Projection} targetProj Target projection.
+ * @param {ol.Extent} targetExtent Target extent.
+ * @param {number} targetResolution Target resolution.
+ * @param {number} pixelRatio Pixel ratio.
+ * @param {ol.ReprojImageFunctionType} getImageFunction
+ *     Function returning source images (extent, resolution, pixelRatio).
+ */
+ol.reproj.Image = function(sourceProj, targetProj,
+    targetExtent, targetResolution, pixelRatio, getImageFunction) {
+
+  /**
+   * @private
+   * @type {ol.proj.Projection}
+   */
+  this.targetProj_ = targetProj;
+
+  /**
+   * @private
+   * @type {ol.Extent}
+   */
+  this.maxSourceExtent_ = sourceProj.getExtent();
+  var maxTargetExtent = targetProj.getExtent();
+
+  var limitedTargetExtent = maxTargetExtent ?
+      ol.extent.getIntersection(targetExtent, maxTargetExtent) : targetExtent;
+
+  var targetCenter = ol.extent.getCenter(limitedTargetExtent);
+  var sourceResolution = ol.reproj.calculateSourceResolution(
+      sourceProj, targetProj, targetCenter, targetResolution);
+
+  var errorThresholdInPixels = ol.DEFAULT_RASTER_REPROJECTION_ERROR_THRESHOLD;
+
+  /**
+   * @private
+   * @type {!ol.reproj.Triangulation}
+   */
+  this.triangulation_ = new ol.reproj.Triangulation(
+      sourceProj, targetProj, limitedTargetExtent, this.maxSourceExtent_,
+      sourceResolution * errorThresholdInPixels);
+
+  /**
+   * @private
+   * @type {number}
+   */
+  this.targetResolution_ = targetResolution;
+
+  /**
+   * @private
+   * @type {ol.Extent}
+   */
+  this.targetExtent_ = targetExtent;
+
+  var sourceExtent = this.triangulation_.calculateSourceExtent();
+
+  /**
+   * @private
+   * @type {ol.ImageBase}
+   */
+  this.sourceImage_ =
+      getImageFunction(sourceExtent, sourceResolution, pixelRatio);
+
+  /**
+   * @private
+   * @type {number}
+   */
+  this.sourcePixelRatio_ =
+      this.sourceImage_ ? this.sourceImage_.getPixelRatio() : 1;
+
+  /**
+   * @private
+   * @type {HTMLCanvasElement}
+   */
+  this.canvas_ = null;
+
+  /**
+   * @private
+   * @type {?ol.EventsKey}
+   */
+  this.sourceListenerKey_ = null;
+
+
+  var state = ol.ImageState.LOADED;
+  var attributions = [];
+
+  if (this.sourceImage_) {
+    state = ol.ImageState.IDLE;
+    attributions = this.sourceImage_.getAttributions();
+  }
+
+  ol.ImageBase.call(this, targetExtent, targetResolution, this.sourcePixelRatio_,
+            state, attributions);
+};
+ol.inherits(ol.reproj.Image, ol.ImageBase);
+
+
+/**
+ * @inheritDoc
+ */
+ol.reproj.Image.prototype.disposeInternal = function() {
+  if (this.state == ol.ImageState.LOADING) {
+    this.unlistenSource_();
+  }
+  ol.ImageBase.prototype.disposeInternal.call(this);
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.reproj.Image.prototype.getImage = function(opt_context) {
+  return this.canvas_;
+};
+
+
+/**
+ * @return {ol.proj.Projection} Projection.
+ */
+ol.reproj.Image.prototype.getProjection = function() {
+  return this.targetProj_;
+};
+
+
+/**
+ * @private
+ */
+ol.reproj.Image.prototype.reproject_ = function() {
+  var sourceState = this.sourceImage_.getState();
+  if (sourceState == ol.ImageState.LOADED) {
+    var width = ol.extent.getWidth(this.targetExtent_) / this.targetResolution_;
+    var height =
+        ol.extent.getHeight(this.targetExtent_) / this.targetResolution_;
+
+    this.canvas_ = ol.reproj.render(width, height, this.sourcePixelRatio_,
+        this.sourceImage_.getResolution(), this.maxSourceExtent_,
+        this.targetResolution_, this.targetExtent_, this.triangulation_, [{
+          extent: this.sourceImage_.getExtent(),
+          image: this.sourceImage_.getImage()
+        }], 0);
+  }
+  this.state = sourceState;
+  this.changed();
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.reproj.Image.prototype.load = function() {
+  if (this.state == ol.ImageState.IDLE) {
+    this.state = ol.ImageState.LOADING;
+    this.changed();
+
+    var sourceState = this.sourceImage_.getState();
+    if (sourceState == ol.ImageState.LOADED ||
+        sourceState == ol.ImageState.ERROR) {
+      this.reproject_();
+    } else {
+      this.sourceListenerKey_ = ol.events.listen(this.sourceImage_,
+          ol.events.EventType.CHANGE, function(e) {
+            var sourceState = this.sourceImage_.getState();
+            if (sourceState == ol.ImageState.LOADED ||
+                sourceState == ol.ImageState.ERROR) {
+              this.unlistenSource_();
+              this.reproject_();
+            }
+          }, this);
+      this.sourceImage_.load();
+    }
+  }
+};
+
+
+/**
+ * @private
+ */
+ol.reproj.Image.prototype.unlistenSource_ = function() {
+  goog.asserts.assert(this.sourceListenerKey_,
+      'this.sourceListenerKey_ should not be null');
+  ol.events.unlistenByKey(this.sourceListenerKey_);
+  this.sourceListenerKey_ = null;
+};
+
+goog.provide('ol.source.Image');
+goog.provide('ol.source.ImageEvent');
+
+goog.require('goog.asserts');
+goog.require('ol.events.Event');
+goog.require('ol.ImageState');
+goog.require('ol.array');
+goog.require('ol.extent');
+goog.require('ol.proj');
+goog.require('ol.reproj.Image');
+goog.require('ol.source.Source');
+
+
+/**
+ * @classdesc
+ * Abstract base class; normally only used for creating subclasses and not
+ * instantiated in apps.
+ * Base class for sources providing a single image.
+ *
+ * @constructor
+ * @extends {ol.source.Source}
+ * @param {ol.SourceImageOptions} options Single image source options.
+ * @api
+ */
+ol.source.Image = function(options) {
+
+  ol.source.Source.call(this, {
+    attributions: options.attributions,
+    extent: options.extent,
+    logo: options.logo,
+    projection: options.projection,
+    state: options.state
+  });
+
+  /**
+   * @private
+   * @type {Array.<number>}
+   */
+  this.resolutions_ = options.resolutions !== undefined ?
+      options.resolutions : null;
+  goog.asserts.assert(!this.resolutions_ ||
+      ol.array.isSorted(this.resolutions_,
+          function(a, b) {
+            return b - a;
+          }, true), 'resolutions must be null or sorted in descending order');
+
+
+  /**
+   * @private
+   * @type {ol.reproj.Image}
+   */
+  this.reprojectedImage_ = null;
+
+
+  /**
+   * @private
+   * @type {number}
+   */
+  this.reprojectedRevision_ = 0;
+
+};
+ol.inherits(ol.source.Image, ol.source.Source);
+
+
+/**
+ * @return {Array.<number>} Resolutions.
+ */
+ol.source.Image.prototype.getResolutions = function() {
+  return this.resolutions_;
+};
+
+
+/**
+ * @protected
+ * @param {number} resolution Resolution.
+ * @return {number} Resolution.
+ */
+ol.source.Image.prototype.findNearestResolution = function(resolution) {
+  if (this.resolutions_) {
+    var idx = ol.array.linearFindNearest(this.resolutions_, resolution, 0);
+    resolution = this.resolutions_[idx];
+  }
+  return resolution;
+};
+
+
+/**
+ * @param {ol.Extent} extent Extent.
+ * @param {number} resolution Resolution.
+ * @param {number} pixelRatio Pixel ratio.
+ * @param {ol.proj.Projection} projection Projection.
+ * @return {ol.ImageBase} Single image.
+ */
+ol.source.Image.prototype.getImage = function(extent, resolution, pixelRatio, projection) {
+  var sourceProjection = this.getProjection();
+  if (!ol.ENABLE_RASTER_REPROJECTION ||
+      !sourceProjection ||
+      !projection ||
+      ol.proj.equivalent(sourceProjection, projection)) {
+    if (sourceProjection) {
+      projection = sourceProjection;
+    }
+    return this.getImageInternal(extent, resolution, pixelRatio, projection);
+  } else {
+    if (this.reprojectedImage_) {
+      if (this.reprojectedRevision_ == this.getRevision() &&
+          ol.proj.equivalent(
+              this.reprojectedImage_.getProjection(), projection) &&
+          this.reprojectedImage_.getResolution() == resolution &&
+          this.reprojectedImage_.getPixelRatio() == pixelRatio &&
+          ol.extent.equals(this.reprojectedImage_.getExtent(), extent)) {
+        return this.reprojectedImage_;
+      }
+      this.reprojectedImage_.dispose();
+      this.reprojectedImage_ = null;
+    }
+
+    this.reprojectedImage_ = new ol.reproj.Image(
+        sourceProjection, projection, extent, resolution, pixelRatio,
+        function(extent, resolution, pixelRatio) {
+          return this.getImageInternal(extent, resolution,
+              pixelRatio, sourceProjection);
+        }.bind(this));
+    this.reprojectedRevision_ = this.getRevision();
+
+    return this.reprojectedImage_;
+  }
+};
+
+
+/**
+ * @param {ol.Extent} extent Extent.
+ * @param {number} resolution Resolution.
+ * @param {number} pixelRatio Pixel ratio.
+ * @param {ol.proj.Projection} projection Projection.
+ * @return {ol.ImageBase} Single image.
+ * @protected
+ */
+ol.source.Image.prototype.getImageInternal = goog.abstractMethod;
+
+
+/**
+ * Handle image change events.
+ * @param {ol.events.Event} event Event.
+ * @protected
+ */
+ol.source.Image.prototype.handleImageChange = function(event) {
+  var image = /** @type {ol.Image} */ (event.target);
+  switch (image.getState()) {
+    case ol.ImageState.LOADING:
+      this.dispatchEvent(
+          new ol.source.ImageEvent(ol.source.ImageEventType.IMAGELOADSTART,
+              image));
+      break;
+    case ol.ImageState.LOADED:
+      this.dispatchEvent(
+          new ol.source.ImageEvent(ol.source.ImageEventType.IMAGELOADEND,
+              image));
+      break;
+    case ol.ImageState.ERROR:
+      this.dispatchEvent(
+          new ol.source.ImageEvent(ol.source.ImageEventType.IMAGELOADERROR,
+              image));
+      break;
+    default:
+      // pass
+  }
+};
+
+
+/**
+ * Default image load function for image sources that use ol.Image image
+ * instances.
+ * @param {ol.Image} image Image.
+ * @param {string} src Source.
+ */
+ol.source.Image.defaultImageLoadFunction = function(image, src) {
+  image.getImage().src = src;
+};
+
+
+/**
+ * @classdesc
+ * Events emitted by {@link ol.source.Image} instances are instances of this
+ * type.
+ *
+ * @constructor
+ * @extends {ol.events.Event}
+ * @implements {oli.source.ImageEvent}
+ * @param {string} type Type.
+ * @param {ol.Image} image The image.
+ */
+ol.source.ImageEvent = function(type, image) {
+
+  ol.events.Event.call(this, type);
+
+  /**
+   * The image related to the event.
+   * @type {ol.Image}
+   * @api
+   */
+  this.image = image;
+
+};
+ol.inherits(ol.source.ImageEvent, ol.events.Event);
+
+
+/**
+ * @enum {string}
+ */
+ol.source.ImageEventType = {
+
+  /**
+   * Triggered when an image starts loading.
+   * @event ol.source.ImageEvent#imageloadstart
+   * @api
+   */
+  IMAGELOADSTART: 'imageloadstart',
+
+  /**
+   * Triggered when an image finishes loading.
+   * @event ol.source.ImageEvent#imageloadend
+   * @api
+   */
+  IMAGELOADEND: 'imageloadend',
+
+  /**
+   * Triggered if image loading results in an error.
+   * @event ol.source.ImageEvent#imageloaderror
+   * @api
+   */
+  IMAGELOADERROR: 'imageloaderror'
+
+};
+
+goog.provide('ol.source.ImageCanvas');
+
+goog.require('ol.ImageCanvas');
+goog.require('ol.extent');
+goog.require('ol.source.Image');
+
+
+/**
+ * @classdesc
+ * Base class for image sources where a canvas element is the image.
+ *
+ * @constructor
+ * @extends {ol.source.Image}
+ * @param {olx.source.ImageCanvasOptions} options Constructor options.
+ * @api
+ */
+ol.source.ImageCanvas = function(options) {
+
+  ol.source.Image.call(this, {
+    attributions: options.attributions,
+    logo: options.logo,
+    projection: options.projection,
+    resolutions: options.resolutions,
+    state: options.state
+  });
+
+  /**
+   * @private
+   * @type {ol.CanvasFunctionType}
+   */
+  this.canvasFunction_ = options.canvasFunction;
+
+  /**
+   * @private
+   * @type {ol.ImageCanvas}
+   */
+  this.canvas_ = null;
+
+  /**
+   * @private
+   * @type {number}
+   */
+  this.renderedRevision_ = 0;
+
+  /**
+   * @private
+   * @type {number}
+   */
+  this.ratio_ = options.ratio !== undefined ?
+      options.ratio : 1.5;
+
+};
+ol.inherits(ol.source.ImageCanvas, ol.source.Image);
+
+
+/**
+ * @inheritDoc
+ */
+ol.source.ImageCanvas.prototype.getImageInternal = function(extent, resolution, pixelRatio, projection) {
+  resolution = this.findNearestResolution(resolution);
+
+  var canvas = this.canvas_;
+  if (canvas &&
+      this.renderedRevision_ == this.getRevision() &&
+      canvas.getResolution() == resolution &&
+      canvas.getPixelRatio() == pixelRatio &&
+      ol.extent.containsExtent(canvas.getExtent(), extent)) {
+    return canvas;
+  }
+
+  extent = extent.slice();
+  ol.extent.scaleFromCenter(extent, this.ratio_);
+  var width = ol.extent.getWidth(extent) / resolution;
+  var height = ol.extent.getHeight(extent) / resolution;
+  var size = [width * pixelRatio, height * pixelRatio];
+
+  var canvasElement = this.canvasFunction_(
+      extent, resolution, pixelRatio, size, projection);
+  if (canvasElement) {
+    canvas = new ol.ImageCanvas(extent, resolution, pixelRatio,
+        this.getAttributions(), canvasElement);
+  }
+  this.canvas_ = canvas;
+  this.renderedRevision_ = this.getRevision();
+
+  return canvas;
+};
+
+goog.provide('ol.Feature');
+
+goog.require('goog.asserts');
+goog.require('ol.events');
+goog.require('ol.events.EventType');
+goog.require('ol');
+goog.require('ol.Object');
+goog.require('ol.geom.Geometry');
+goog.require('ol.style.Style');
+
+
+/**
+ * @classdesc
+ * A vector object for geographic features with a geometry and other
+ * attribute properties, similar to the features in vector file formats like
+ * GeoJSON.
+ *
+ * Features can be styled individually with `setStyle`; otherwise they use the
+ * style of their vector layer.
+ *
+ * Note that attribute properties are set as {@link ol.Object} properties on
+ * the feature object, so they are observable, and have get/set accessors.
+ *
+ * Typically, a feature has a single geometry property. You can set the
+ * geometry using the `setGeometry` method and get it with `getGeometry`.
+ * It is possible to store more than one geometry on a feature using attribute
+ * properties. By default, the geometry used for rendering is identified by
+ * the property name `geometry`. If you want to use another geometry property
+ * for rendering, use the `setGeometryName` method to change the attribute
+ * property associated with the geometry for the feature.  For example:
+ *
+ * ```js
+ * var feature = new ol.Feature({
+ *   geometry: new ol.geom.Polygon(polyCoords),
+ *   labelPoint: new ol.geom.Point(labelCoords),
+ *   name: 'My Polygon'
+ * });
+ *
+ * // get the polygon geometry
+ * var poly = feature.getGeometry();
+ *
+ * // Render the feature as a point using the coordinates from labelPoint
+ * feature.setGeometryName('labelPoint');
+ *
+ * // get the point geometry
+ * var point = feature.getGeometry();
+ * ```
+ *
+ * @constructor
+ * @extends {ol.Object}
+ * @param {ol.geom.Geometry|Object.<string, *>=} opt_geometryOrProperties
+ *     You may pass a Geometry object directly, or an object literal
+ *     containing properties.  If you pass an object literal, you may
+ *     include a Geometry associated with a `geometry` key.
+ * @api stable
+ */
+ol.Feature = function(opt_geometryOrProperties) {
+
+  ol.Object.call(this);
+
+  /**
+   * @private
+   * @type {number|string|undefined}
+   */
+  this.id_ = undefined;
+
+  /**
+   * @type {string}
+   * @private
+   */
+  this.geometryName_ = 'geometry';
+
+  /**
+   * User provided style.
+   * @private
+   * @type {ol.style.Style|Array.<ol.style.Style>|
+   *     ol.FeatureStyleFunction}
+   */
+  this.style_ = null;
+
+  /**
+   * @private
+   * @type {ol.FeatureStyleFunction|undefined}
+   */
+  this.styleFunction_ = undefined;
+
+  /**
+   * @private
+   * @type {?ol.EventsKey}
+   */
+  this.geometryChangeKey_ = null;
+
+  ol.events.listen(
+      this, ol.Object.getChangeEventType(this.geometryName_),
+      this.handleGeometryChanged_, this);
+
+  if (opt_geometryOrProperties !== undefined) {
+    if (opt_geometryOrProperties instanceof ol.geom.Geometry ||
+        !opt_geometryOrProperties) {
+      var geometry = opt_geometryOrProperties;
+      this.setGeometry(geometry);
+    } else {
+      goog.asserts.assert(goog.isObject(opt_geometryOrProperties),
+          'opt_geometryOrProperties should be an Object');
+      /** @type {Object.<string, *>} */
+      var properties = opt_geometryOrProperties;
+      this.setProperties(properties);
+    }
+  }
+};
+ol.inherits(ol.Feature, ol.Object);
+
+
+/**
+ * Clone this feature. If the original feature has a geometry it
+ * is also cloned. The feature id is not set in the clone.
+ * @return {ol.Feature} The clone.
+ * @api stable
+ */
+ol.Feature.prototype.clone = function() {
+  var clone = new ol.Feature(this.getProperties());
+  clone.setGeometryName(this.getGeometryName());
+  var geometry = this.getGeometry();
+  if (geometry) {
+    clone.setGeometry(geometry.clone());
+  }
+  var style = this.getStyle();
+  if (style) {
+    clone.setStyle(style);
+  }
+  return clone;
+};
+
+
+/**
+ * Get the feature's default geometry.  A feature may have any number of named
+ * geometries.  The "default" geometry (the one that is rendered by default) is
+ * set when calling {@link ol.Feature#setGeometry}.
+ * @return {ol.geom.Geometry|undefined} The default geometry for the feature.
+ * @api stable
+ * @observable
+ */
+ol.Feature.prototype.getGeometry = function() {
+  return /** @type {ol.geom.Geometry|undefined} */ (
+      this.get(this.geometryName_));
+};
+
+
+/**
+ * Get the feature identifier.  This is a stable identifier for the feature and
+ * is either set when reading data from a remote source or set explicitly by
+ * calling {@link ol.Feature#setId}.
+ * @return {number|string|undefined} Id.
+ * @api stable
+ * @observable
+ */
+ol.Feature.prototype.getId = function() {
+  return this.id_;
+};
+
+
+/**
+ * Get the name of the feature's default geometry.  By default, the default
+ * geometry is named `geometry`.
+ * @return {string} Get the property name associated with the default geometry
+ *     for this feature.
+ * @api stable
+ */
+ol.Feature.prototype.getGeometryName = function() {
+  return this.geometryName_;
+};
+
+
+/**
+ * Get the feature's style.  This return for this method depends on what was
+ * provided to the {@link ol.Feature#setStyle} method.
+ * @return {ol.style.Style|Array.<ol.style.Style>|
+ *     ol.FeatureStyleFunction} The feature style.
+ * @api stable
+ * @observable
+ */
+ol.Feature.prototype.getStyle = function() {
+  return this.style_;
+};
+
+
+/**
+ * Get the feature's style function.
+ * @return {ol.FeatureStyleFunction|undefined} Return a function
+ * representing the current style of this feature.
+ * @api stable
+ */
+ol.Feature.prototype.getStyleFunction = function() {
+  return this.styleFunction_;
+};
+
+
+/**
+ * @private
+ */
+ol.Feature.prototype.handleGeometryChange_ = function() {
+  this.changed();
+};
+
+
+/**
+ * @private
+ */
+ol.Feature.prototype.handleGeometryChanged_ = function() {
+  if (this.geometryChangeKey_) {
+    ol.events.unlistenByKey(this.geometryChangeKey_);
+    this.geometryChangeKey_ = null;
+  }
+  var geometry = this.getGeometry();
+  if (geometry) {
+    this.geometryChangeKey_ = ol.events.listen(geometry,
+        ol.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 ol.Feature#getGeometryName}.
+ * @param {ol.geom.Geometry|undefined} geometry The new geometry.
+ * @api stable
+ * @observable
+ */
+ol.Feature.prototype.setGeometry = function(geometry) {
+  this.set(this.geometryName_, geometry);
+};
+
+
+/**
+ * Set the style for the feature.  This can be a single style object, an array
+ * of styles, or a function that takes a resolution and returns an array of
+ * styles. If it is `null` the feature has no style (a `null` style).
+ * @param {ol.style.Style|Array.<ol.style.Style>|
+ *     ol.FeatureStyleFunction} style Style for this feature.
+ * @api stable
+ * @observable
+ */
+ol.Feature.prototype.setStyle = function(style) {
+  this.style_ = style;
+  this.styleFunction_ = !style ?
+      undefined : ol.Feature.createStyleFunction(style);
+  this.changed();
+};
+
+
+/**
+ * Set the feature id.  The feature id is considered stable and may be used when
+ * requesting features or comparing identifiers returned from a remote source.
+ * The feature id can be used with the {@link ol.source.Vector#getFeatureById}
+ * method.
+ * @param {number|string|undefined} id The feature id.
+ * @api stable
+ * @observable
+ */
+ol.Feature.prototype.setId = function(id) {
+  this.id_ = id;
+  this.changed();
+};
+
+
+/**
+ * Set the property name to be used when getting the feature's default geometry.
+ * When calling {@link ol.Feature#getGeometry}, the value of the property with
+ * this name will be returned.
+ * @param {string} name The property name of the default geometry.
+ * @api stable
+ */
+ol.Feature.prototype.setGeometryName = function(name) {
+  ol.events.unlisten(
+      this, ol.Object.getChangeEventType(this.geometryName_),
+      this.handleGeometryChanged_, this);
+  this.geometryName_ = name;
+  ol.events.listen(
+      this, ol.Object.getChangeEventType(this.geometryName_),
+      this.handleGeometryChanged_, this);
+  this.handleGeometryChanged_();
+};
+
+
+/**
+ * Convert the provided object into a feature style function.  Functions passed
+ * through unchanged.  Arrays of ol.style.Style or single style objects wrapped
+ * in a new feature style function.
+ * @param {ol.FeatureStyleFunction|!Array.<ol.style.Style>|!ol.style.Style} obj
+ *     A feature style function, a single style, or an array of styles.
+ * @return {ol.FeatureStyleFunction} A style function.
+ */
+ol.Feature.createStyleFunction = function(obj) {
+  var styleFunction;
+
+  if (typeof obj === 'function') {
+    styleFunction = obj;
+  } else {
+    /**
+     * @type {Array.<ol.style.Style>}
+     */
+    var styles;
+    if (Array.isArray(obj)) {
+      styles = obj;
+    } else {
+      goog.asserts.assertInstanceof(obj, ol.style.Style,
+          'obj should be an ol.style.Style');
+      styles = [obj];
+    }
+    styleFunction = function() {
+      return styles;
+    };
+  }
+  return styleFunction;
+};
+
+goog.provide('ol.VectorTile');
+
+goog.require('ol.Tile');
+goog.require('ol.TileState');
+goog.require('ol.dom');
+goog.require('ol.proj.Projection');
+
+
+/**
+ * @constructor
+ * @extends {ol.Tile}
+ * @param {ol.TileCoord} tileCoord Tile coordinate.
+ * @param {ol.TileState} state State.
+ * @param {string} src Data source url.
+ * @param {ol.format.Feature} format Feature format.
+ * @param {ol.TileLoadFunctionType} tileLoadFunction Tile load function.
+ */
+ol.VectorTile = function(tileCoord, state, src, format, tileLoadFunction) {
+
+  ol.Tile.call(this, tileCoord, state);
+
+  /**
+   * @private
+   * @type {CanvasRenderingContext2D}
+   */
+  this.context_ = ol.dom.createCanvasContext2D();
+
+  /**
+   * @private
+   * @type {ol.format.Feature}
+   */
+  this.format_ = format;
+
+  /**
+   * @private
+   * @type {Array.<ol.Feature>}
+   */
+  this.features_ = null;
+
+  /**
+   * @private
+   * @type {ol.FeatureLoader}
+   */
+  this.loader_;
+
+  /**
+   * Data projection
+   * @private
+   * @type {ol.proj.Projection}
+   */
+  this.projection_;
+
+  /**
+   * @private
+   * @type {ol.TileReplayState}
+   */
+  this.replayState_ = {
+    dirty: false,
+    renderedRenderOrder: null,
+    renderedRevision: -1,
+    renderedTileRevision: -1,
+    replayGroup: null,
+    skippedFeatures: []
+  };
+
+  /**
+   * @private
+   * @type {ol.TileLoadFunctionType}
+   */
+  this.tileLoadFunction_ = tileLoadFunction;
+
+  /**
+   * @private
+   * @type {string}
+   */
+  this.url_ = src;
+
+};
+ol.inherits(ol.VectorTile, ol.Tile);
+
+
+/**
+ * @return {CanvasRenderingContext2D} The rendering context.
+ */
+ol.VectorTile.prototype.getContext = function() {
+  return this.context_;
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.VectorTile.prototype.getImage = function() {
+  return this.replayState_.renderedTileRevision == -1 ?
+      null : this.context_.canvas;
+};
+
+
+/**
+ * Get the feature format assigned for reading this tile's features.
+ * @return {ol.format.Feature} Feature format.
+ * @api
+ */
+ol.VectorTile.prototype.getFormat = function() {
+  return this.format_;
+};
+
+
+/**
+ * @return {Array.<ol.Feature>} Features.
+ */
+ol.VectorTile.prototype.getFeatures = function() {
+  return this.features_;
+};
+
+
+/**
+ * @return {ol.TileReplayState} The replay state.
+ */
+ol.VectorTile.prototype.getReplayState = function() {
+  return this.replayState_;
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.VectorTile.prototype.getKey = function() {
+  return this.url_;
+};
+
+
+/**
+ * @return {ol.proj.Projection} Feature projection.
+ */
+ol.VectorTile.prototype.getProjection = function() {
+  return this.projection_;
+};
+
+
+/**
+ * Load the tile.
+ */
+ol.VectorTile.prototype.load = function() {
+  if (this.state == ol.TileState.IDLE) {
+    this.setState(ol.TileState.LOADING);
+    this.tileLoadFunction_(this, this.url_);
+    this.loader_(null, NaN, null);
+  }
+};
+
+
+/**
+ * @param {Array.<ol.Feature>} features Features.
+ * @api
+ */
+ol.VectorTile.prototype.setFeatures = function(features) {
+  this.features_ = features;
+  this.setState(ol.TileState.LOADED);
+};
+
+
+/**
+ * Set the projection of the features that were added with {@link #setFeatures}.
+ * @param {ol.proj.Projection} projection Feature projection.
+ * @api
+ */
+ol.VectorTile.prototype.setProjection = function(projection) {
+  this.projection_ = projection;
+};
+
+
+/**
+ * @param {ol.TileState} tileState Tile state.
+ */
+ol.VectorTile.prototype.setState = function(tileState) {
+  this.state = tileState;
+  this.changed();
+};
+
+
+/**
+ * Set the feature loader for reading this tile's features.
+ * @param {ol.FeatureLoader} loader Feature loader.
+ * @api
+ */
+ol.VectorTile.prototype.setLoader = function(loader) {
+  this.loader_ = loader;
+};
+
+goog.provide('ol.format.FormatType');
+
+
+/**
+ * @enum {string}
+ */
+ol.format.FormatType = {
+  ARRAY_BUFFER: 'arraybuffer',
+  JSON: 'json',
+  TEXT: 'text',
+  XML: 'xml'
+};
+
+goog.provide('ol.xml');
+
+goog.require('goog.asserts');
+goog.require('ol.array');
+
+
+/**
+ * This document should be used when creating nodes for XML serializations. This
+ * document is also used by {@link ol.xml.createElementNS} and
+ * {@link ol.xml.setAttributeNS}
+ * @const
+ * @type {Document}
+ */
+ol.xml.DOCUMENT = document.implementation.createDocument('', '', null);
+
+
+/**
+ * @param {string} namespaceURI Namespace URI.
+ * @param {string} qualifiedName Qualified name.
+ * @return {Node} Node.
+ */
+ol.xml.createElementNS = function(namespaceURI, qualifiedName) {
+  return ol.xml.DOCUMENT.createElementNS(namespaceURI, qualifiedName);
+};
+
+
+/**
+ * Recursively grab all text content of child nodes into a single string.
+ * @param {Node} node Node.
+ * @param {boolean} normalizeWhitespace Normalize whitespace: remove all line
+ * breaks.
+ * @return {string} All text content.
+ * @api
+ */
+ol.xml.getAllTextContent = function(node, normalizeWhitespace) {
+  return ol.xml.getAllTextContent_(node, normalizeWhitespace, []).join('');
+};
+
+
+/**
+ * Recursively grab all text content of child nodes into a single string.
+ * @param {Node} node Node.
+ * @param {boolean} normalizeWhitespace Normalize whitespace: remove all line
+ * breaks.
+ * @param {Array.<string>} accumulator Accumulator.
+ * @private
+ * @return {Array.<string>} Accumulator.
+ */
+ol.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) {
+      ol.xml.getAllTextContent_(n, normalizeWhitespace, accumulator);
+    }
+  }
+  return accumulator;
+};
+
+
+/**
+ * @param {?} value Value.
+ * @return {boolean} Is document.
+ */
+ol.xml.isDocument = function(value) {
+  return value instanceof Document;
+};
+
+
+/**
+ * @param {?} value Value.
+ * @return {boolean} Is node.
+ */
+ol.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
+ */
+ol.xml.getAttributeNS = function(node, namespaceURI, name) {
+  return node.getAttributeNS(namespaceURI, name) || '';
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {?string} namespaceURI Namespace URI.
+ * @param {string} name Attribute name.
+ * @param {string|number} value Value.
+ */
+ol.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
+ */
+ol.xml.parse = function(xml) {
+  return new DOMParser().parseFromString(xml, 'application/xml');
+};
+
+
+/**
+ * Make an array extender function for extending the array at the top of the
+ * object stack.
+ * @param {function(this: T, Node, Array.<*>): (Array.<*>|undefined)}
+ *     valueReader Value reader.
+ * @param {T=} opt_this The object to use as `this` in `valueReader`.
+ * @return {ol.XmlParser} Parser.
+ * @template T
+ */
+ol.xml.makeArrayExtender = function(valueReader, opt_this) {
+  return (
+      /**
+       * @param {Node} node Node.
+       * @param {Array.<*>} objectStack Object stack.
+       */
+      function(node, objectStack) {
+        var value = valueReader.call(opt_this, node, objectStack);
+        if (value !== undefined) {
+          goog.asserts.assert(Array.isArray(value),
+              'valueReader function is expected to return an array of values');
+          var array = /** @type {Array.<*>} */
+              (objectStack[objectStack.length - 1]);
+          goog.asserts.assert(Array.isArray(array),
+              'objectStack is supposed to be an array of arrays');
+          ol.array.extend(array, value);
+        }
+      });
+};
+
+
+/**
+ * Make an array pusher function for pushing to the array at the top of the
+ * object stack.
+ * @param {function(this: T, Node, Array.<*>): *} valueReader Value reader.
+ * @param {T=} opt_this The object to use as `this` in `valueReader`.
+ * @return {ol.XmlParser} Parser.
+ * @template T
+ */
+ol.xml.makeArrayPusher = function(valueReader, opt_this) {
+  return (
+      /**
+       * @param {Node} node Node.
+       * @param {Array.<*>} objectStack Object stack.
+       */
+      function(node, objectStack) {
+        var value = valueReader.call(opt_this !== undefined ? opt_this : this,
+            node, objectStack);
+        if (value !== undefined) {
+          var array = objectStack[objectStack.length - 1];
+          goog.asserts.assert(Array.isArray(array),
+              'objectStack is supposed to be an array of arrays');
+          array.push(value);
+        }
+      });
+};
+
+
+/**
+ * Make an object stack replacer function for replacing the object at the
+ * top of the stack.
+ * @param {function(this: T, Node, Array.<*>): *} valueReader Value reader.
+ * @param {T=} opt_this The object to use as `this` in `valueReader`.
+ * @return {ol.XmlParser} Parser.
+ * @template T
+ */
+ol.xml.makeReplacer = function(valueReader, opt_this) {
+  return (
+      /**
+       * @param {Node} node Node.
+       * @param {Array.<*>} objectStack Object stack.
+       */
+      function(node, objectStack) {
+        var value = valueReader.call(opt_this !== undefined ? opt_this : this,
+            node, objectStack);
+        if (value !== undefined) {
+          objectStack[objectStack.length - 1] = value;
+        }
+      });
+};
+
+
+/**
+ * Make an object property pusher function for adding a property to the
+ * object at the top of the stack.
+ * @param {function(this: T, Node, Array.<*>): *} valueReader Value reader.
+ * @param {string=} opt_property Property.
+ * @param {T=} opt_this The object to use as `this` in `valueReader`.
+ * @return {ol.XmlParser} Parser.
+ * @template T
+ */
+ol.xml.makeObjectPropertyPusher = function(valueReader, opt_property, opt_this) {
+  goog.asserts.assert(valueReader !== undefined,
+      'undefined valueReader, expected function(this: T, Node, Array.<*>)');
+  return (
+      /**
+       * @param {Node} node Node.
+       * @param {Array.<*>} objectStack Object stack.
+       */
+      function(node, objectStack) {
+        var value = valueReader.call(opt_this !== undefined ? opt_this : this,
+            node, objectStack);
+        if (value !== undefined) {
+          var object = /** @type {Object} */
+              (objectStack[objectStack.length - 1]);
+          var property = opt_property !== undefined ?
+              opt_property : node.localName;
+          goog.asserts.assert(goog.isObject(object),
+              'entity from stack was not an object');
+          var array;
+          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 {ol.XmlParser} Parser.
+ * @template T
+ */
+ol.xml.makeObjectPropertySetter = function(valueReader, opt_property, opt_this) {
+  goog.asserts.assert(valueReader !== undefined,
+      'undefined valueReader, expected function(this: T, Node, Array.<*>)');
+  return (
+      /**
+       * @param {Node} node Node.
+       * @param {Array.<*>} objectStack Object stack.
+       */
+      function(node, objectStack) {
+        var value = valueReader.call(opt_this !== undefined ? opt_this : this,
+            node, objectStack);
+        if (value !== undefined) {
+          var object = /** @type {Object} */
+              (objectStack[objectStack.length - 1]);
+          var property = opt_property !== undefined ?
+              opt_property : node.localName;
+          goog.asserts.assert(goog.isObject(object),
+              'entity from stack was not an object');
+          object[property] = value;
+        }
+      });
+};
+
+
+/**
+ * Create a serializer that appends nodes written by its `nodeWriter` to its
+ * designated parent. The parent is the `node` of the
+ * {@link ol.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 {ol.XmlSerializer} Serializer.
+ * @template T, V
+ */
+ol.xml.makeChildAppender = function(nodeWriter, opt_this) {
+  return function(node, value, objectStack) {
+    nodeWriter.call(opt_this !== undefined ? opt_this : this,
+        node, value, objectStack);
+    var parent = objectStack[objectStack.length - 1];
+    goog.asserts.assert(goog.isObject(parent),
+        'entity from stack was not an object');
+    var parentNode = parent.node;
+    goog.asserts.assert(ol.xml.isNode(parentNode) ||
+        ol.xml.isDocument(parentNode),
+        'expected parentNode %s to be a Node or a Document', parentNode);
+    parentNode.appendChild(node);
+  };
+};
+
+
+/**
+ * Create a serializer that calls the provided `nodeWriter` from
+ * {@link ol.xml.serialize}. This can be used by the parent writer to have the
+ * 'nodeWriter' called with an array of values when the `nodeWriter` was
+ * designed to serialize a single item. An example would be a LineString
+ * geometry writer, which could be reused for writing MultiLineString
+ * geometries.
+ * @param {function(this: T, Node, V, Array.<*>)}
+ *     nodeWriter Node writer.
+ * @param {T=} opt_this The object to use as `this` in `nodeWriter`.
+ * @return {ol.XmlSerializer} Serializer.
+ * @template T, V
+ */
+ol.xml.makeArraySerializer = function(nodeWriter, opt_this) {
+  var serializersNS, nodeFactory;
+  return function(node, value, objectStack) {
+    if (serializersNS === undefined) {
+      serializersNS = {};
+      var serializers = {};
+      serializers[node.localName] = nodeWriter;
+      serializersNS[node.namespaceURI] = serializers;
+      nodeFactory = ol.xml.makeSimpleNodeFactory(node.localName);
+    }
+    ol.xml.serialize(serializersNS, nodeFactory, value, objectStack);
+  };
+};
+
+
+/**
+ * Create a node factory which can use the `opt_keys` passed to
+ * {@link ol.xml.serialize} or {@link ol.xml.pushSerializeAndPop} as node names,
+ * or a fixed node name. The namespace of the created nodes can either be fixed,
+ * or the parent namespace will be used.
+ * @param {string=} opt_nodeName Fixed node name which will be used for all
+ *     created nodes. If not provided, the 3rd argument to the resulting node
+ *     factory needs to be provided and will be the nodeName.
+ * @param {string=} opt_namespaceURI Fixed namespace URI which will be used for
+ *     all created nodes. If not provided, the namespace of the parent node will
+ *     be used.
+ * @return {function(*, Array.<*>, string=): (Node|undefined)} Node factory.
+ */
+ol.xml.makeSimpleNodeFactory = function(opt_nodeName, opt_namespaceURI) {
+  var fixedNodeName = opt_nodeName;
+  return (
+      /**
+       * @param {*} value Value.
+       * @param {Array.<*>} objectStack Object stack.
+       * @param {string=} opt_nodeName Node name.
+       * @return {Node} Node.
+       */
+      function(value, objectStack, opt_nodeName) {
+        var context = objectStack[objectStack.length - 1];
+        var node = context.node;
+        goog.asserts.assert(ol.xml.isNode(node) || ol.xml.isDocument(node),
+            'expected node %s to be a Node or a Document', node);
+        var nodeName = fixedNodeName;
+        if (nodeName === undefined) {
+          nodeName = opt_nodeName;
+        }
+        var namespaceURI = opt_namespaceURI;
+        if (opt_namespaceURI === undefined) {
+          namespaceURI = node.namespaceURI;
+        }
+        goog.asserts.assert(nodeName !== undefined, 'nodeName was undefined');
+        return ol.xml.createElementNS(namespaceURI, nodeName);
+      }
+  );
+};
+
+
+/**
+ * A node factory that creates a node using the parent's `namespaceURI` and the
+ * `nodeName` passed by {@link ol.xml.serialize} or
+ * {@link ol.xml.pushSerializeAndPop} to the node factory.
+ * @const
+ * @type {function(*, Array.<*>, string=): (Node|undefined)}
+ */
+ol.xml.OBJECT_PROPERTY_NODE_FACTORY = ol.xml.makeSimpleNodeFactory();
+
+
+/**
+ * Create an array of `values` to be used with {@link ol.xml.serialize} or
+ * {@link ol.xml.pushSerializeAndPop}, where `orderedKeys` has to be provided as
+ * `opt_key` argument.
+ * @param {Object.<string, V>} object Key-value pairs for the sequence. Keys can
+ *     be a subset of the `orderedKeys`.
+ * @param {Array.<string>} orderedKeys Keys in the order of the sequence.
+ * @return {Array.<V>} Values in the order of the sequence. The resulting array
+ *     has the same length as the `orderedKeys` array. Values that are not
+ *     present in `object` will be `undefined` in the resulting array.
+ * @template V
+ */
+ol.xml.makeSequence = function(object, orderedKeys) {
+  var length = orderedKeys.length;
+  var sequence = new Array(length);
+  for (var i = 0; i < length; ++i) {
+    sequence[i] = object[orderedKeys[i]];
+  }
+  return sequence;
+};
+
+
+/**
+ * Create a namespaced structure, using the same values for each namespace.
+ * This can be used as a starting point for versioned parsers, when only a few
+ * values are version specific.
+ * @param {Array.<string>} namespaceURIs Namespace URIs.
+ * @param {T} structure Structure.
+ * @param {Object.<string, T>=} opt_structureNS Namespaced structure to add to.
+ * @return {Object.<string, T>} Namespaced structure.
+ * @template T
+ */
+ol.xml.makeStructureNS = function(namespaceURIs, structure, opt_structureNS) {
+  /**
+   * @type {Object.<string, *>}
+   */
+  var structureNS = opt_structureNS !== undefined ? opt_structureNS : {};
+  var i, ii;
+  for (i = 0, ii = namespaceURIs.length; i < ii; ++i) {
+    structureNS[namespaceURIs[i]] = structure;
+  }
+  return structureNS;
+};
+
+
+/**
+ * Parse a node using the parsers and object stack.
+ * @param {Object.<string, Object.<string, ol.XmlParser>>} parsersNS
+ *     Parsers by namespace.
+ * @param {Node} node Node.
+ * @param {Array.<*>} objectStack Object stack.
+ * @param {*=} opt_this The object to use as `this`.
+ */
+ol.xml.parseNode = function(parsersNS, node, objectStack, opt_this) {
+  var n;
+  for (n = node.firstElementChild; n; n = n.nextElementSibling) {
+    var parsers = parsersNS[n.namespaceURI];
+    if (parsers !== undefined) {
+      var parser = parsers[n.localName];
+      if (parser !== undefined) {
+        parser.call(opt_this, n, objectStack);
+      }
+    }
+  }
+};
+
+
+/**
+ * Push an object on top of the stack, parse and return the popped object.
+ * @param {T} object Object.
+ * @param {Object.<string, Object.<string, ol.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
+ */
+ol.xml.pushParseAndPop = function(
+    object, parsersNS, node, objectStack, opt_this) {
+  objectStack.push(object);
+  ol.xml.parseNode(parsersNS, node, objectStack, opt_this);
+  return objectStack.pop();
+};
+
+
+/**
+ * Walk through an array of `values` and call a serializer for each value.
+ * @param {Object.<string, Object.<string, ol.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 ol.Feature} instances.
+ * @param {Array.<*>} objectStack Node stack.
+ * @param {Array.<string>=} opt_keys Keys of the `values`. Will be passed to the
+ *     `nodeFactory`. This is used for serializing object literals where the
+ *     node name relates to the property key. The array length of `opt_keys` has
+ *     to match the length of `values`. For serializing a sequence, `opt_keys`
+ *     determines the order of the sequence.
+ * @param {T=} opt_this The object to use as `this` for the node factory and
+ *     serializers.
+ * @template T
+ */
+ol.xml.serialize = function(
+    serializersNS, nodeFactory, values, objectStack, opt_keys, opt_this) {
+  var length = (opt_keys !== undefined ? opt_keys : values).length;
+  var value, node;
+  for (var i = 0; i < length; ++i) {
+    value = values[i];
+    if (value !== undefined) {
+      node = nodeFactory.call(opt_this, value, objectStack,
+          opt_keys !== undefined ? opt_keys[i] : undefined);
+      if (node !== undefined) {
+        serializersNS[node.namespaceURI][node.localName]
+            .call(opt_this, node, value, objectStack);
+      }
+    }
+  }
+};
+
+
+/**
+ * @param {O} object Object.
+ * @param {Object.<string, Object.<string, ol.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 ol.Feature} instances.
+ * @param {Array.<*>} objectStack Node stack.
+ * @param {Array.<string>=} opt_keys Keys of the `values`. Will be passed to the
+ *     `nodeFactory`. This is used for serializing object literals where the
+ *     node name relates to the property key. The array length of `opt_keys` has
+ *     to match the length of `values`. For serializing a sequence, `opt_keys`
+ *     determines the order of the sequence.
+ * @param {T=} opt_this The object to use as `this` for the node factory and
+ *     serializers.
+ * @return {O|undefined} Object.
+ * @template O, T
+ */
+ol.xml.pushSerializeAndPop = function(object,
+    serializersNS, nodeFactory, values, objectStack, opt_keys, opt_this) {
+  objectStack.push(object);
+  ol.xml.serialize(
+      serializersNS, nodeFactory, values, objectStack, opt_keys, opt_this);
+  return objectStack.pop();
+};
+
+goog.provide('ol.featureloader');
+
+goog.require('goog.asserts');
+goog.require('ol.TileState');
+goog.require('ol.VectorTile');
+goog.require('ol.format.FormatType');
+goog.require('ol.proj');
+goog.require('ol.proj.Projection');
+goog.require('ol.xml');
+
+
+/**
+ * @param {string|ol.FeatureUrlFunction} url Feature URL service.
+ * @param {ol.format.Feature} format Feature format.
+ * @param {function(this:ol.VectorTile, Array.<ol.Feature>, ol.proj.Projection)|function(this:ol.source.Vector, Array.<ol.Feature>)} success
+ *     Function called with the loaded features and optionally with the data
+ *     projection. Called with the vector tile or source as `this`.
+ * @param {function(this:ol.VectorTile)|function(this:ol.source.Vector)} failure
+ *     Function called when loading failed. Called with the vector tile or
+ *     source as `this`.
+ * @return {ol.FeatureLoader} The feature loader.
+ */
+ol.featureloader.loadFeaturesXhr = function(url, format, success, failure) {
+  return (
+      /**
+       * @param {ol.Extent} extent Extent.
+       * @param {number} resolution Resolution.
+       * @param {ol.proj.Projection} projection Projection.
+       * @this {ol.source.Vector|ol.VectorTile}
+       */
+      function(extent, resolution, projection) {
+        var xhr = new XMLHttpRequest();
+        xhr.open('GET',
+            typeof url === 'function' ? url(extent, resolution, projection) : url,
+            true);
+        if (format.getType() == ol.format.FormatType.ARRAY_BUFFER) {
+          xhr.responseType = 'arraybuffer';
+        }
+        /**
+         * @param {Event} event Event.
+         * @private
+         */
+        xhr.onload = function(event) {
+          if (xhr.status >= 200 && xhr.status < 300) {
+            var type = format.getType();
+            /** @type {Document|Node|Object|string|undefined} */
+            var source;
+            if (type == ol.format.FormatType.JSON ||
+                type == ol.format.FormatType.TEXT) {
+              source = xhr.responseText;
+            } else if (type == ol.format.FormatType.XML) {
+              source = xhr.responseXML;
+              if (!source) {
+                source = ol.xml.parse(xhr.responseText);
+              }
+            } else if (type == ol.format.FormatType.ARRAY_BUFFER) {
+              source = /** @type {ArrayBuffer} */ (xhr.response);
+            } else {
+              goog.asserts.fail('unexpected format type');
+            }
+            if (source) {
+              success.call(this, format.readFeatures(source,
+                  {featureProjection: projection}),
+                  format.readProjection(source));
+            } else {
+              goog.asserts.fail('undefined or null source');
+            }
+          } else {
+            failure.call(this);
+          }
+        }.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 tile.
+ * @param {string|ol.FeatureUrlFunction} url Feature URL service.
+ * @param {ol.format.Feature} format Feature format.
+ * @return {ol.FeatureLoader} The feature loader.
+ * @api
+ */
+ol.featureloader.tile = function(url, format) {
+  return ol.featureloader.loadFeaturesXhr(url, format,
+      /**
+       * @param {Array.<ol.Feature>} features The loaded features.
+       * @param {ol.proj.Projection} dataProjection Data projection.
+       * @this {ol.VectorTile}
+       */
+      function(features, dataProjection) {
+        this.setProjection(dataProjection);
+        this.setFeatures(features);
+      },
+      /**
+       * @this {ol.VectorTile}
+       */
+      function() {
+        this.setState(ol.TileState.ERROR);
+      });
+};
+
+
+/**
+ * Create an XHR feature loader for a `url` and `format`. The feature loader
+ * loads features (with XHR), parses the features, and adds them to the
+ * vector source.
+ * @param {string|ol.FeatureUrlFunction} url Feature URL service.
+ * @param {ol.format.Feature} format Feature format.
+ * @return {ol.FeatureLoader} The feature loader.
+ * @api
+ */
+ol.featureloader.xhr = function(url, format) {
+  return ol.featureloader.loadFeaturesXhr(url, format,
+      /**
+       * @param {Array.<ol.Feature>} features The loaded features.
+       * @param {ol.proj.Projection} dataProjection Data projection.
+       * @this {ol.source.Vector}
+       */
+      function(features, dataProjection) {
+        this.addFeatures(features);
+      }, /* FIXME handle error */ ol.nullFunction);
+};
+
+goog.provide('ol.loadingstrategy');
+
+
+/**
+ * Strategy function for loading all features with a single request.
+ * @param {ol.Extent} extent Extent.
+ * @param {number} resolution Resolution.
+ * @return {Array.<ol.Extent>} Extents.
+ * @api
+ */
+ol.loadingstrategy.all = function(extent, resolution) {
+  return [[-Infinity, -Infinity, Infinity, Infinity]];
+};
+
+
+/**
+ * Strategy function for loading features based on the view's extent and
+ * resolution.
+ * @param {ol.Extent} extent Extent.
+ * @param {number} resolution Resolution.
+ * @return {Array.<ol.Extent>} Extents.
+ * @api
+ */
+ol.loadingstrategy.bbox = function(extent, resolution) {
+  return [extent];
+};
+
+
+/**
+ * Creates a strategy function for loading features based on a tile grid.
+ * @param {ol.tilegrid.TileGrid} tileGrid Tile grid.
+ * @return {function(ol.Extent, number): Array.<ol.Extent>} Loading strategy.
+ * @api
+ */
+ol.loadingstrategy.tile = function(tileGrid) {
+  return (
+      /**
+       * @param {ol.Extent} extent Extent.
+       * @param {number} resolution Resolution.
+       * @return {Array.<ol.Extent>} Extents.
+       */
+      function(extent, resolution) {
+        var z = tileGrid.getZForResolution(resolution);
+        var tileRange = tileGrid.getTileRangeForExtentAndZ(extent, z);
+        /** @type {Array.<ol.Extent>} */
+        var extents = [];
+        /** @type {ol.TileCoord} */
+        var tileCoord = [z, 0, 0];
+        for (tileCoord[1] = tileRange.minX; tileCoord[1] <= tileRange.maxX;
+             ++tileCoord[1]) {
+          for (tileCoord[2] = tileRange.minY; tileCoord[2] <= tileRange.maxY;
+               ++tileCoord[2]) {
+            extents.push(tileGrid.getTileCoordExtent(tileCoord));
+          }
+        }
+        return extents;
+      });
+};
+
+goog.provide('ol.ext.rbush');
+/** @typedef {function(*)} */
+ol.ext.rbush;
+(function() {
+var exports = {};
+var module = {exports: exports};
+var define;
+/**
+ * @fileoverview
+ * @suppress {accessControls, ambiguousFunctionDecl, checkDebuggerStatement, checkRegExp, checkTypes, checkVars, const, constantProperty, deprecated, duplicate, es5Strict, fileoverviewTags, missingProperties, nonStandardJsDocs, strictModuleDepCheck, suspiciousCode, undefinedNames, undefinedVars, unknownDefines, uselessCode, visibility}
+ */
+(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.rbush = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(_dereq_,module,exports){
+'use strict';
+
+module.exports = partialSort;
+
+// Floyd-Rivest selection algorithm:
+// Rearrange items so that all items in the [left, k] range are smaller than all items in (k, right];
+// The k-th element will have the (k - left + 1)th smallest value in [left, right]
+
+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;
+}
+
+},{}],2:[function(_dereq_,module,exports){
+'use strict';
+
+module.exports = rbush;
+
+var quickselect = _dereq_('quickselect');
+
+function rbush(maxEntries, format) {
+    if (!(this instanceof rbush)) return new rbush(maxEntries, format);
+
+    // max entries in a node is 9 by default; min node fill is 40% for best performance
+    this._maxEntries = Math.max(4, maxEntries || 9);
+    this._minEntries = Math.max(2, Math.ceil(this._maxEntries * 0.4));
+
+    if (format) {
+        this._initFormat(format);
+    }
+
+    this.clear();
+}
+
+rbush.prototype = {
+
+    all: function () {
+        return this._all(this.data, []);
+    },
+
+    search: function (bbox) {
+
+        var node = this.data,
+            result = [],
+            toBBox = this.toBBox;
+
+        if (!intersects(bbox, node)) 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;
+    },
+
+    collides: 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;
+        }
+
+        // recursively build the tree with the given data from stratch using OMT algorithm
+        var node = this._build(data.slice(), 0, data.length - 1, 0);
+
+        if (!this.data.children.length) {
+            // save as is if tree is empty
+            this.data = node;
+
+        } else if (this.data.height === node.height) {
+            // split root if trees have the same height
+            this._splitRoot(this.data, node);
+
+        } else {
+            if (this.data.height < node.height) {
+                // swap trees if inserted one is bigger
+                var tmpNode = this.data;
+                this.data = node;
+                node = tmpNode;
+            }
+
+            // insert the small tree into the large tree at appropriate level
+            this._insert(node, this.data.height - node.height - 1, true);
+        }
+
+        return this;
+    },
+
+    insert: function (item) {
+        if (item) this._insert(item, this.data.height - 1);
+        return this;
+    },
+
+    clear: function () {
+        this.data = 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;
+
+        // depth-first iterative tree traversal
+        while (node || path.length) {
+
+            if (!node) { // go up
+                node = path.pop();
+                parent = path[path.length - 1];
+                i = indexes.pop();
+                goingUp = true;
+            }
+
+            if (node.leaf) { // check current node
+                index = findItem(item, node.children, equalsFn);
+
+                if (index !== -1) {
+                    // item found, remove the item and condense tree upwards
+                    node.children.splice(index, 1);
+                    path.push(node);
+                    this._condense(path);
+                    return this;
+                }
+            }
+
+            if (!goingUp && !node.leaf && contains(node, bbox)) { // go down
+                path.push(node);
+                indexes.push(i);
+                i = 0;
+                parent = node;
+                node = node.children[0];
+
+            } else if (parent) { // go right
+                i++;
+                node = parent.children[i];
+                goingUp = false;
+
+            } else node = null; // nothing found
+        }
+
+        return this;
+    },
+
+    toBBox: function (item) { return item; },
+
+    compareMinX: 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) {
+            // reached leaf level; return leaf
+            node = createNode(items.slice(left, right + 1));
+            calcBBox(node, this.toBBox);
+            return node;
+        }
+
+        if (!height) {
+            // target height of the bulk-loaded tree
+            height = Math.ceil(Math.log(N) / Math.log(M));
+
+            // target number of root entries to maximize storage utilization
+            M = Math.ceil(N / Math.pow(M, height - 1));
+        }
+
+        node = createNode([]);
+        node.leaf = false;
+        node.height = height;
+
+        // split the items into M mostly square tiles
+
+        var N2 = Math.ceil(N / M),
+            N1 = N2 * Math.ceil(Math.sqrt(M)),
+            i, j, right2, right3;
+
+        multiSelect(items, left, right, N1, this.compareMinX);
+
+        for (i = left; i <= right; i += N1) {
+
+            right2 = Math.min(i + N1 - 1, right);
+
+            multiSelect(items, i, right2, N2, this.compareMinY);
+
+            for (j = i; j <= right2; j += N2) {
+
+                right3 = Math.min(j + N2 - 1, right2);
+
+                // pack each entry recursively
+                node.children.push(this._build(items, j, right3, height - 1));
+            }
+        }
+
+        calcBBox(node, this.toBBox);
+
+        return node;
+    },
+
+    _chooseSubtree: function (bbox, node, level, path) {
+
+        var i, len, child, targetNode, area, enlargement, minArea, minEnlargement;
+
+        while (true) {
+            path.push(node);
+
+            if (node.leaf || path.length - 1 === level) break;
+
+            minArea = minEnlargement = Infinity;
+
+            for (i = 0, len = node.children.length; i < len; i++) {
+                child = node.children[i];
+                area = bboxArea(child);
+                enlargement = enlargedArea(bbox, child) - area;
+
+                // choose entry with the least area enlargement
+                if (enlargement < minEnlargement) {
+                    minEnlargement = enlargement;
+                    minArea = area < minArea ? area : minArea;
+                    targetNode = child;
+
+                } else if (enlargement === minEnlargement) {
+                    // otherwise choose one with the smallest area
+                    if (area < minArea) {
+                        minArea = area;
+                        targetNode = child;
+                    }
+                }
+            }
+
+            node = targetNode || node.children[0];
+        }
+
+        return node;
+    },
+
+    _insert: function (item, level, isNode) {
+
+        var toBBox = this.toBBox,
+            bbox = isNode ? item : toBBox(item),
+            insertPath = [];
+
+        // find the best node for accommodating the item, saving all nodes along the path too
+        var node = this._chooseSubtree(bbox, this.data, level, insertPath);
+
+        // put the item into the node
+        node.children.push(item);
+        extend(node, bbox);
+
+        // split on node overflow; propagate upwards if necessary
+        while (level >= 0) {
+            if (insertPath[level].children.length > this._maxEntries) {
+                this._split(insertPath, level);
+                level--;
+            } else break;
+        }
+
+        // adjust bboxes along the insertion path
+        this._adjustParentBBoxes(bbox, insertPath, level);
+    },
+
+    // split overflowed node into two
+    _split: function (insertPath, level) {
+
+        var node = insertPath[level],
+            M = node.children.length,
+            m = this._minEntries;
+
+        this._chooseSplitAxis(node, m, M);
+
+        var splitIndex = this._chooseSplitIndex(node, m, M);
+
+        var newNode = 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) {
+        // split root node
+        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);
+
+            // choose distribution with minimum overlap
+            if (overlap < minOverlap) {
+                minOverlap = overlap;
+                index = i;
+
+                minArea = area < minArea ? area : minArea;
+
+            } else if (overlap === minOverlap) {
+                // otherwise choose distribution with minimum area
+                if (area < minArea) {
+                    minArea = area;
+                    index = i;
+                }
+            }
+        }
+
+        return index;
+    },
+
+    // sorts node children by the best axis for split
+    _chooseSplitAxis: function (node, m, M) {
+
+        var compareMinX = node.leaf ? this.compareMinX : compareNodeMinX,
+            compareMinY = node.leaf ? this.compareMinY : compareNodeMinY,
+            xMargin = this._allDistMargin(node, m, M, compareMinX),
+            yMargin = this._allDistMargin(node, m, M, compareMinY);
+
+        // if total distributions margin value is minimal for x, sort by minX,
+        // otherwise it's already sorted by minY
+        if (xMargin < yMargin) node.children.sort(compareMinX);
+    },
+
+    // total margin of all possible split distributions where each node is at least m full
+    _allDistMargin: function (node, m, M, compare) {
+
+        node.children.sort(compare);
+
+        var toBBox = this.toBBox,
+            leftBBox = distBBox(node, 0, m, toBBox),
+            rightBBox = distBBox(node, M - m, M, toBBox),
+            margin = bboxMargin(leftBBox) + bboxMargin(rightBBox),
+            i, child;
+
+        for (i = m; i < M - m; i++) {
+            child = node.children[i];
+            extend(leftBBox, node.leaf ? toBBox(child) : child);
+            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) {
+        // adjust bboxes along the given tree path
+        for (var i = level; i >= 0; i--) {
+            extend(path[i], bbox);
+        }
+    },
+
+    _condense: function (path) {
+        // go through the path, removing empty nodes and updating bboxes
+        for (var i = path.length - 1, siblings; i >= 0; i--) {
+            if (path[i].children.length === 0) {
+                if (i > 0) {
+                    siblings = path[i - 1].children;
+                    siblings.splice(siblings.indexOf(path[i]), 1);
+
+                } else this.clear();
+
+            } else calcBBox(path[i], this.toBBox);
+        }
+    },
+
+    _initFormat: function (format) {
+        // data format (minX, minY, maxX, maxY accessors)
+
+        // uses eval-type function compilation instead of just accepting a toBBox function
+        // because the algorithms are very sensitive to sorting functions performance,
+        // so they should be dead simple and without inner calls
+
+        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;
+}
+
+// calculate node's bbox from bboxes of its children
+function calcBBox(node, toBBox) {
+    distBBox(node, 0, node.children.length, toBBox, node);
+}
+
+// min bounding rectangle of node children from k to p-1
+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
+    };
+}
+
+// sort an array so that items come in groups of n unsorted items, with groups sorted between each other;
+// combines selection algorithm with binary divide & conquer approach
+
+function multiSelect(arr, left, right, n, compare) {
+    var stack = [left, right],
+        mid;
+
+    while (stack.length) {
+        right = stack.pop();
+        left = stack.pop();
+
+        if (right - left <= n) continue;
+
+        mid = left + Math.ceil((right - left) / n / 2) * n;
+        quickselect(arr, mid, left, right, compare);
+
+        stack.push(left, mid, mid, right);
+    }
+}
+
+},{"quickselect":1}]},{},[2])(2)
+});
+ol.ext.rbush = module.exports;
+})();
+
+goog.provide('ol.structs.RBush');
+
+goog.require('goog.asserts');
+goog.require('ol.ext.rbush');
+goog.require('ol.extent');
+goog.require('ol.object');
+
+
+/**
+ * Wrapper around the RBush by Vladimir Agafonkin.
+ *
+ * @constructor
+ * @param {number=} opt_maxEntries Max entries.
+ * @see https://github.com/mourner/rbush
+ * @struct
+ * @template T
+ */
+ol.structs.RBush = function(opt_maxEntries) {
+
+  /**
+   * @private
+   */
+  this.rbush_ = ol.ext.rbush(opt_maxEntries);
+
+  /**
+   * A mapping between the objects added to this rbush wrapper
+   * and the objects that are actually added to the internal rbush.
+   * @private
+   * @type {Object.<number, ol.RBushEntry>}
+   */
+  this.items_ = {};
+
+  if (goog.DEBUG) {
+    /**
+     * @private
+     * @type {number}
+     */
+    this.readers_ = 0;
+  }
+};
+
+
+/**
+ * Insert a value into the RBush.
+ * @param {ol.Extent} extent Extent.
+ * @param {T} value Value.
+ */
+ol.structs.RBush.prototype.insert = function(extent, value) {
+  if (goog.DEBUG && this.readers_) {
+    throw new Error('Can not insert value while reading');
+  }
+  /** @type {ol.RBushEntry} */
+  var item = {
+    minX: extent[0],
+    minY: extent[1],
+    maxX: extent[2],
+    maxY: extent[3],
+    value: value
+  };
+
+  this.rbush_.insert(item);
+  // remember the object that was added to the internal rbush
+  goog.asserts.assert(!(goog.getUid(value) in this.items_),
+      'uid (%s) of value (%s) already exists', goog.getUid(value), value);
+  this.items_[goog.getUid(value)] = item;
+};
+
+
+/**
+ * Bulk-insert values into the RBush.
+ * @param {Array.<ol.Extent>} extents Extents.
+ * @param {Array.<T>} values Values.
+ */
+ol.structs.RBush.prototype.load = function(extents, values) {
+  if (goog.DEBUG && this.readers_) {
+    throw new Error('Can not insert values while reading');
+  }
+  goog.asserts.assert(extents.length === values.length,
+      'extens and values must have same length (%s === %s)',
+      extents.length, values.length);
+
+  var items = new Array(values.length);
+  for (var i = 0, l = values.length; i < l; i++) {
+    var extent = extents[i];
+    var value = values[i];
+
+    /** @type {ol.RBushEntry} */
+    var item = {
+      minX: extent[0],
+      minY: extent[1],
+      maxX: extent[2],
+      maxY: extent[3],
+      value: value
+    };
+    items[i] = item;
+    goog.asserts.assert(!(goog.getUid(value) in this.items_),
+        'uid (%s) of value (%s) already exists', goog.getUid(value), value);
+    this.items_[goog.getUid(value)] = item;
+  }
+  this.rbush_.load(items);
+};
+
+
+/**
+ * Remove a value from the RBush.
+ * @param {T} value Value.
+ * @return {boolean} Removed.
+ */
+ol.structs.RBush.prototype.remove = function(value) {
+  if (goog.DEBUG && this.readers_) {
+    throw new Error('Can not remove value while reading');
+  }
+  var uid = goog.getUid(value);
+  goog.asserts.assert(uid in this.items_,
+      'uid (%s) of value (%s) does not exist', uid, value);
+
+  // get the object in which the value was wrapped when adding to the
+  // internal rbush. then use that object to do the removal.
+  var item = this.items_[uid];
+  delete this.items_[uid];
+  return this.rbush_.remove(item) !== null;
+};
+
+
+/**
+ * Update the extent of a value in the RBush.
+ * @param {ol.Extent} extent Extent.
+ * @param {T} value Value.
+ */
+ol.structs.RBush.prototype.update = function(extent, value) {
+  var uid = goog.getUid(value);
+  goog.asserts.assert(uid in this.items_,
+      'uid (%s) of value (%s) does not exist', uid, value);
+
+  var item = this.items_[uid];
+  var bbox = [item.minX, item.minY, item.maxX, item.maxY];
+  if (!ol.extent.equals(bbox, extent)) {
+    if (goog.DEBUG && this.readers_) {
+      throw new Error('Can not update extent while reading');
+    }
+    this.remove(value);
+    this.insert(extent, value);
+  }
+};
+
+
+/**
+ * Return all values in the RBush.
+ * @return {Array.<T>} All.
+ */
+ol.structs.RBush.prototype.getAll = function() {
+  var items = this.rbush_.all();
+  return items.map(function(item) {
+    return item.value;
+  });
+};
+
+
+/**
+ * Return all values in the given extent.
+ * @param {ol.Extent} extent Extent.
+ * @return {Array.<T>} All in extent.
+ */
+ol.structs.RBush.prototype.getInExtent = function(extent) {
+  /** @type {ol.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
+ */
+ol.structs.RBush.prototype.forEach = function(callback, opt_this) {
+  if (goog.DEBUG) {
+    ++this.readers_;
+    try {
+      return this.forEach_(this.getAll(), callback, opt_this);
+    } finally {
+      --this.readers_;
+    }
+  } else {
+    return this.forEach_(this.getAll(), callback, opt_this);
+  }
+};
+
+
+/**
+ * Calls a callback function with each value in the provided extent.
+ * @param {ol.Extent} extent Extent.
+ * @param {function(this: S, T): *} callback Callback.
+ * @param {S=} opt_this The object to use as `this` in `callback`.
+ * @return {*} Callback return value.
+ * @template S
+ */
+ol.structs.RBush.prototype.forEachInExtent = function(extent, callback, opt_this) {
+  if (goog.DEBUG) {
+    ++this.readers_;
+    try {
+      return this.forEach_(this.getInExtent(extent), callback, opt_this);
+    } finally {
+      --this.readers_;
+    }
+  } else {
+    return this.forEach_(this.getInExtent(extent), callback, opt_this);
+  }
+};
+
+
+/**
+ * @param {Array.<T>} values Values.
+ * @param {function(this: S, T): *} callback Callback.
+ * @param {S=} opt_this The object to use as `this` in `callback`.
+ * @private
+ * @return {*} Callback return value.
+ * @template S
+ */
+ol.structs.RBush.prototype.forEach_ = function(values, callback, opt_this) {
+  var result;
+  for (var i = 0, l = values.length; i < l; i++) {
+    result = callback.call(opt_this, values[i]);
+    if (result) {
+      return result;
+    }
+  }
+  return result;
+};
+
+
+/**
+ * @return {boolean} Is empty.
+ */
+ol.structs.RBush.prototype.isEmpty = function() {
+  return ol.object.isEmpty(this.items_);
+};
+
+
+/**
+ * Remove all values from the RBush.
+ */
+ol.structs.RBush.prototype.clear = function() {
+  this.rbush_.clear();
+  this.items_ = {};
+};
+
+
+/**
+ * @param {ol.Extent=} opt_extent Extent.
+ * @return {!ol.Extent} Extent.
+ */
+ol.structs.RBush.prototype.getExtent = function(opt_extent) {
+  // FIXME add getExtent() to rbush
+  var data = this.rbush_.data;
+  return [data.minX, data.minY, data.maxX, data.maxY];
+};
+
+// FIXME bulk feature upload - suppress events
+// FIXME make change-detection more refined (notably, geometry hint)
+
+goog.provide('ol.source.Vector');
+goog.provide('ol.source.VectorEvent');
+goog.provide('ol.source.VectorEventType');
+
+goog.require('goog.asserts');
+goog.require('ol');
+goog.require('ol.Collection');
+goog.require('ol.CollectionEventType');
+goog.require('ol.Feature');
+goog.require('ol.ObjectEventType');
+goog.require('ol.array');
+goog.require('ol.events');
+goog.require('ol.events.Event');
+goog.require('ol.events.EventType');
+goog.require('ol.extent');
+goog.require('ol.featureloader');
+goog.require('ol.loadingstrategy');
+goog.require('ol.object');
+goog.require('ol.proj');
+goog.require('ol.source.Source');
+goog.require('ol.source.State');
+goog.require('ol.structs.RBush');
+
+
+/**
+ * @enum {string}
+ */
+ol.source.VectorEventType = {
+  /**
+   * Triggered when a feature is added to the source.
+   * @event ol.source.VectorEvent#addfeature
+   * @api stable
+   */
+  ADDFEATURE: 'addfeature',
+
+  /**
+   * Triggered when a feature is updated.
+   * @event ol.source.VectorEvent#changefeature
+   * @api
+   */
+  CHANGEFEATURE: 'changefeature',
+
+  /**
+   * Triggered when the clear method is called on the source.
+   * @event ol.source.VectorEvent#clear
+   * @api
+   */
+  CLEAR: 'clear',
+
+  /**
+   * Triggered when a feature is removed from the source.
+   * See {@link ol.source.Vector#clear source.clear()} for exceptions.
+   * @event ol.source.VectorEvent#removefeature
+   * @api stable
+   */
+  REMOVEFEATURE: 'removefeature'
+};
+
+
+/**
+ * @classdesc
+ * Provides a source of features for vector layers. Vector features provided
+ * by this source are suitable for editing. See {@link ol.source.VectorTile} for
+ * vector data that is optimized for rendering.
+ *
+ * @constructor
+ * @extends {ol.source.Source}
+ * @fires ol.source.VectorEvent
+ * @param {olx.source.VectorOptions=} opt_options Vector source options.
+ * @api stable
+ */
+ol.source.Vector = function(opt_options) {
+
+  var options = opt_options || {};
+
+  ol.source.Source.call(this, {
+    attributions: options.attributions,
+    logo: options.logo,
+    projection: undefined,
+    state: ol.source.State.READY,
+    wrapX: options.wrapX !== undefined ? options.wrapX : true
+  });
+
+  /**
+   * @private
+   * @type {ol.FeatureLoader}
+   */
+  this.loader_ = ol.nullFunction;
+
+  /**
+   * @private
+   * @type {ol.format.Feature|undefined}
+   */
+  this.format_ = options.format;
+
+  /**
+   * @private
+   * @type {string|ol.FeatureUrlFunction|undefined}
+   */
+  this.url_ = options.url;
+
+  if (options.loader !== undefined) {
+    this.loader_ = options.loader;
+  } else if (this.url_ !== undefined) {
+    goog.asserts.assert(this.format_ !== undefined,
+        'format must be set when url is set');
+    // create a XHR feature loader for "url" and "format"
+    this.loader_ = ol.featureloader.xhr(this.url_, this.format_);
+  }
+
+  /**
+   * @private
+   * @type {ol.LoadingStrategy}
+   */
+  this.strategy_ = options.strategy !== undefined ? options.strategy :
+      ol.loadingstrategy.all;
+
+  var useSpatialIndex =
+      options.useSpatialIndex !== undefined ? options.useSpatialIndex : true;
+
+  /**
+   * @private
+   * @type {ol.structs.RBush.<ol.Feature>}
+   */
+  this.featuresRtree_ = useSpatialIndex ? new ol.structs.RBush() : null;
+
+  /**
+   * @private
+   * @type {ol.structs.RBush.<{extent: ol.Extent}>}
+   */
+  this.loadedExtentsRtree_ = new ol.structs.RBush();
+
+  /**
+   * @private
+   * @type {Object.<string, ol.Feature>}
+   */
+  this.nullGeometryFeatures_ = {};
+
+  /**
+   * A lookup of features by id (the return from feature.getId()).
+   * @private
+   * @type {Object.<string, ol.Feature>}
+   */
+  this.idIndex_ = {};
+
+  /**
+   * A lookup of features without id (keyed by goog.getUid(feature)).
+   * @private
+   * @type {Object.<string, ol.Feature>}
+   */
+  this.undefIdIndex_ = {};
+
+  /**
+   * @private
+   * @type {Object.<string, Array.<ol.EventsKey>>}
+   */
+  this.featureChangeKeys_ = {};
+
+  /**
+   * @private
+   * @type {ol.Collection.<ol.Feature>}
+   */
+  this.featuresCollection_ = null;
+
+  var collection, features;
+  if (options.features instanceof ol.Collection) {
+    collection = options.features;
+    features = collection.getArray();
+  } else if (Array.isArray(options.features)) {
+    features = options.features;
+  }
+  if (!useSpatialIndex && collection === undefined) {
+    collection = new ol.Collection(features);
+  }
+  if (features !== undefined) {
+    this.addFeaturesInternal(features);
+  }
+  if (collection !== undefined) {
+    this.bindFeaturesCollection_(collection);
+  }
+
+};
+ol.inherits(ol.source.Vector, ol.source.Source);
+
+
+/**
+ * Add a single feature to the source.  If you want to add a batch of features
+ * at once, call {@link ol.source.Vector#addFeatures source.addFeatures()}
+ * instead.
+ * @param {ol.Feature} feature Feature to add.
+ * @api stable
+ */
+ol.source.Vector.prototype.addFeature = function(feature) {
+  this.addFeatureInternal(feature);
+  this.changed();
+};
+
+
+/**
+ * Add a feature without firing a `change` event.
+ * @param {ol.Feature} feature Feature.
+ * @protected
+ */
+ol.source.Vector.prototype.addFeatureInternal = function(feature) {
+  var featureKey = goog.getUid(feature).toString();
+
+  if (!this.addToIndex_(featureKey, feature)) {
+    return;
+  }
+
+  this.setupChangeEvents_(featureKey, feature);
+
+  var geometry = feature.getGeometry();
+  if (geometry) {
+    var extent = geometry.getExtent();
+    if (this.featuresRtree_) {
+      this.featuresRtree_.insert(extent, feature);
+    }
+  } else {
+    this.nullGeometryFeatures_[featureKey] = feature;
+  }
+
+  this.dispatchEvent(
+      new ol.source.VectorEvent(ol.source.VectorEventType.ADDFEATURE, feature));
+};
+
+
+/**
+ * @param {string} featureKey Unique identifier for the feature.
+ * @param {ol.Feature} feature The feature.
+ * @private
+ */
+ol.source.Vector.prototype.setupChangeEvents_ = function(featureKey, feature) {
+  goog.asserts.assert(!(featureKey in this.featureChangeKeys_),
+      'key (%s) not yet registered in featureChangeKey', featureKey);
+  this.featureChangeKeys_[featureKey] = [
+    ol.events.listen(feature, ol.events.EventType.CHANGE,
+        this.handleFeatureChange_, this),
+    ol.events.listen(feature, ol.ObjectEventType.PROPERTYCHANGE,
+        this.handleFeatureChange_, this)
+  ];
+};
+
+
+/**
+ * @param {string} featureKey Unique identifier for the feature.
+ * @param {ol.Feature} feature The feature.
+ * @return {boolean} The feature is "valid", in the sense that it is also a
+ *     candidate for insertion into the Rtree.
+ * @private
+ */
+ol.source.Vector.prototype.addToIndex_ = function(featureKey, feature) {
+  var valid = true;
+  var id = feature.getId();
+  if (id !== undefined) {
+    if (!(id.toString() in this.idIndex_)) {
+      this.idIndex_[id.toString()] = feature;
+    } else {
+      valid = false;
+    }
+  } else {
+    goog.asserts.assert(!(featureKey in this.undefIdIndex_),
+        'Feature already added to the source');
+    this.undefIdIndex_[featureKey] = feature;
+  }
+  return valid;
+};
+
+
+/**
+ * Add a batch of features to the source.
+ * @param {Array.<ol.Feature>} features Features to add.
+ * @api stable
+ */
+ol.source.Vector.prototype.addFeatures = function(features) {
+  this.addFeaturesInternal(features);
+  this.changed();
+};
+
+
+/**
+ * Add features without firing a `change` event.
+ * @param {Array.<ol.Feature>} features Features.
+ * @protected
+ */
+ol.source.Vector.prototype.addFeaturesInternal = function(features) {
+  var featureKey, i, length, feature;
+
+  var extents = [];
+  var newFeatures = [];
+  var geometryFeatures = [];
+
+  for (i = 0, length = features.length; i < length; i++) {
+    feature = features[i];
+    featureKey = goog.getUid(feature).toString();
+    if (this.addToIndex_(featureKey, feature)) {
+      newFeatures.push(feature);
+    }
+  }
+
+  for (i = 0, length = newFeatures.length; i < length; i++) {
+    feature = newFeatures[i];
+    featureKey = goog.getUid(feature).toString();
+    this.setupChangeEvents_(featureKey, feature);
+
+    var geometry = feature.getGeometry();
+    if (geometry) {
+      var extent = geometry.getExtent();
+      extents.push(extent);
+      geometryFeatures.push(feature);
+    } else {
+      this.nullGeometryFeatures_[featureKey] = feature;
+    }
+  }
+  if (this.featuresRtree_) {
+    this.featuresRtree_.load(extents, geometryFeatures);
+  }
+
+  for (i = 0, length = newFeatures.length; i < length; i++) {
+    this.dispatchEvent(new ol.source.VectorEvent(
+        ol.source.VectorEventType.ADDFEATURE, newFeatures[i]));
+  }
+};
+
+
+/**
+ * @param {!ol.Collection.<ol.Feature>} collection Collection.
+ * @private
+ */
+ol.source.Vector.prototype.bindFeaturesCollection_ = function(collection) {
+  goog.asserts.assert(!this.featuresCollection_,
+      'bindFeaturesCollection can only be called once');
+  var modifyingCollection = false;
+  ol.events.listen(this, ol.source.VectorEventType.ADDFEATURE,
+      function(evt) {
+        if (!modifyingCollection) {
+          modifyingCollection = true;
+          collection.push(evt.feature);
+          modifyingCollection = false;
+        }
+      });
+  ol.events.listen(this, ol.source.VectorEventType.REMOVEFEATURE,
+      function(evt) {
+        if (!modifyingCollection) {
+          modifyingCollection = true;
+          collection.remove(evt.feature);
+          modifyingCollection = false;
+        }
+      });
+  ol.events.listen(collection, ol.CollectionEventType.ADD,
+      function(evt) {
+        if (!modifyingCollection) {
+          var feature = evt.element;
+          goog.asserts.assertInstanceof(feature, ol.Feature);
+          modifyingCollection = true;
+          this.addFeature(feature);
+          modifyingCollection = false;
+        }
+      }, this);
+  ol.events.listen(collection, ol.CollectionEventType.REMOVE,
+      function(evt) {
+        if (!modifyingCollection) {
+          var feature = evt.element;
+          goog.asserts.assertInstanceof(feature, ol.Feature);
+          modifyingCollection = true;
+          this.removeFeature(feature);
+          modifyingCollection = false;
+        }
+      }, this);
+  this.featuresCollection_ = collection;
+};
+
+
+/**
+ * Remove all features from the source.
+ * @param {boolean=} opt_fast Skip dispatching of {@link removefeature} events.
+ * @api stable
+ */
+ol.source.Vector.prototype.clear = function(opt_fast) {
+  if (opt_fast) {
+    for (var featureId in this.featureChangeKeys_) {
+      var keys = this.featureChangeKeys_[featureId];
+      keys.forEach(ol.events.unlistenByKey);
+    }
+    if (!this.featuresCollection_) {
+      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.featuresCollection_) {
+    this.featuresCollection_.clear();
+  }
+  goog.asserts.assert(ol.object.isEmpty(this.featureChangeKeys_),
+      'featureChangeKeys is an empty object now');
+  goog.asserts.assert(ol.object.isEmpty(this.idIndex_),
+      'idIndex is an empty object now');
+  goog.asserts.assert(ol.object.isEmpty(this.undefIdIndex_),
+      'undefIdIndex is an empty object now');
+
+  if (this.featuresRtree_) {
+    this.featuresRtree_.clear();
+  }
+  this.loadedExtentsRtree_.clear();
+  this.nullGeometryFeatures_ = {};
+
+  var clearEvent = new ol.source.VectorEvent(ol.source.VectorEventType.CLEAR);
+  this.dispatchEvent(clearEvent);
+  this.changed();
+};
+
+
+/**
+ * Iterate through all features on the source, calling the provided callback
+ * with each one.  If the callback returns any "truthy" value, iteration will
+ * stop and the function will return the same value.
+ *
+ * @param {function(this: T, ol.Feature): S} callback Called with each feature
+ *     on the source.  Return a truthy value to stop iteration.
+ * @param {T=} opt_this The object to use as `this` in the callback.
+ * @return {S|undefined} The return value from the last call to the callback.
+ * @template T,S
+ * @api stable
+ */
+ol.source.Vector.prototype.forEachFeature = function(callback, opt_this) {
+  if (this.featuresRtree_) {
+    return this.featuresRtree_.forEach(callback, opt_this);
+  } else if (this.featuresCollection_) {
+    return this.featuresCollection_.forEach(callback, opt_this);
+  }
+};
+
+
+/**
+ * Iterate through all features whose geometries contain the provided
+ * coordinate, calling the callback with each feature.  If the callback returns
+ * a "truthy" value, iteration will stop and the function will return the same
+ * value.
+ *
+ * @param {ol.Coordinate} coordinate Coordinate.
+ * @param {function(this: T, ol.Feature): S} callback Called with each feature
+ *     whose goemetry contains the provided coordinate.
+ * @param {T=} opt_this The object to use as `this` in the callback.
+ * @return {S|undefined} The return value from the last call to the callback.
+ * @template T,S
+ */
+ol.source.Vector.prototype.forEachFeatureAtCoordinateDirect = function(coordinate, callback, opt_this) {
+  var extent = [coordinate[0], coordinate[1], coordinate[0], coordinate[1]];
+  return this.forEachFeatureInExtent(extent, function(feature) {
+    var geometry = feature.getGeometry();
+    goog.asserts.assert(geometry, 'feature geometry is defined and not null');
+    if (geometry.containsCoordinate(coordinate)) {
+      return callback.call(opt_this, feature);
+    } else {
+      return undefined;
+    }
+  });
+};
+
+
+/**
+ * Iterate through all features whose bounding box intersects the provided
+ * extent (note that the feature's geometry may not intersect the extent),
+ * calling the callback with each feature.  If the callback returns a "truthy"
+ * value, iteration will stop and the function will return the same value.
+ *
+ * If you are interested in features whose geometry intersects an extent, call
+ * the {@link ol.source.Vector#forEachFeatureIntersectingExtent
+ * source.forEachFeatureIntersectingExtent()} method instead.
+ *
+ * When `useSpatialIndex` is set to false, this method will loop through all
+ * features, equivalent to {@link ol.source.Vector#forEachFeature}.
+ *
+ * @param {ol.Extent} extent Extent.
+ * @param {function(this: T, ol.Feature): S} callback Called with each feature
+ *     whose bounding box intersects the provided extent.
+ * @param {T=} opt_this The object to use as `this` in the callback.
+ * @return {S|undefined} The return value from the last call to the callback.
+ * @template T,S
+ * @api
+ */
+ol.source.Vector.prototype.forEachFeatureInExtent = function(extent, callback, opt_this) {
+  if (this.featuresRtree_) {
+    return this.featuresRtree_.forEachInExtent(extent, callback, opt_this);
+  } else if (this.featuresCollection_) {
+    return this.featuresCollection_.forEach(callback, opt_this);
+  }
+};
+
+
+/**
+ * Iterate through all features whose geometry intersects the provided extent,
+ * calling the callback with each feature.  If the callback returns a "truthy"
+ * value, iteration will stop and the function will return the same value.
+ *
+ * If you only want to test for bounding box intersection, call the
+ * {@link ol.source.Vector#forEachFeatureInExtent
+ * source.forEachFeatureInExtent()} method instead.
+ *
+ * @param {ol.Extent} extent Extent.
+ * @param {function(this: T, ol.Feature): S} callback Called with each feature
+ *     whose geometry intersects the provided extent.
+ * @param {T=} opt_this The object to use as `this` in the callback.
+ * @return {S|undefined} The return value from the last call to the callback.
+ * @template T,S
+ * @api
+ */
+ol.source.Vector.prototype.forEachFeatureIntersectingExtent = function(extent, callback, opt_this) {
+  return this.forEachFeatureInExtent(extent,
+      /**
+       * @param {ol.Feature} feature Feature.
+       * @return {S|undefined} The return value from the last call to the callback.
+       * @template S
+       */
+      function(feature) {
+        var geometry = feature.getGeometry();
+        goog.asserts.assert(geometry,
+            'feature geometry is defined and not null');
+        if (geometry.intersectsExtent(extent)) {
+          var result = callback.call(opt_this, feature);
+          if (result) {
+            return result;
+          }
+        }
+      });
+};
+
+
+/**
+ * Get the features collection associated with this source. Will be `null`
+ * unless the source was configured with `useSpatialIndex` set to `false`, or
+ * with an {@link ol.Collection} as `features`.
+ * @return {ol.Collection.<ol.Feature>} The collection of features.
+ * @api
+ */
+ol.source.Vector.prototype.getFeaturesCollection = function() {
+  return this.featuresCollection_;
+};
+
+
+/**
+ * Get all features on the source.
+ * @return {Array.<ol.Feature>} Features.
+ * @api stable
+ */
+ol.source.Vector.prototype.getFeatures = function() {
+  var features;
+  if (this.featuresCollection_) {
+    features = this.featuresCollection_.getArray();
+  } else if (this.featuresRtree_) {
+    features = this.featuresRtree_.getAll();
+    if (!ol.object.isEmpty(this.nullGeometryFeatures_)) {
+      ol.array.extend(
+          features, ol.object.getValues(this.nullGeometryFeatures_));
+    }
+  }
+  goog.asserts.assert(features !== undefined,
+      'Neither featuresRtree_ nor featuresCollection_ are available');
+  return features;
+};
+
+
+/**
+ * Get all features whose geometry intersects the provided coordinate.
+ * @param {ol.Coordinate} coordinate Coordinate.
+ * @return {Array.<ol.Feature>} Features.
+ * @api stable
+ */
+ol.source.Vector.prototype.getFeaturesAtCoordinate = function(coordinate) {
+  var features = [];
+  this.forEachFeatureAtCoordinateDirect(coordinate, function(feature) {
+    features.push(feature);
+  });
+  return features;
+};
+
+
+/**
+ * Get all features in the provided extent.  Note that this returns all features
+ * whose bounding boxes intersect the given extent (so it may include features
+ * whose geometries do not intersect the extent).
+ *
+ * This method is not available when the source is configured with
+ * `useSpatialIndex` set to `false`.
+ * @param {ol.Extent} extent Extent.
+ * @return {Array.<ol.Feature>} Features.
+ * @api
+ */
+ol.source.Vector.prototype.getFeaturesInExtent = function(extent) {
+  goog.asserts.assert(this.featuresRtree_,
+      'getFeaturesInExtent does not work when useSpatialIndex is set to false');
+  return this.featuresRtree_.getInExtent(extent);
+};
+
+
+/**
+ * Get the closest feature to the provided coordinate.
+ *
+ * This method is not available when the source is configured with
+ * `useSpatialIndex` set to `false`.
+ * @param {ol.Coordinate} coordinate Coordinate.
+ * @param {function(ol.Feature):boolean=} opt_filter Feature filter function.
+ *     The filter function will receive one argument, the {@link ol.Feature feature}
+ *     and it should return a boolean value. By default, no filtering is made.
+ * @return {ol.Feature} Closest feature.
+ * @api stable
+ */
+ol.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];
+  goog.asserts.assert(this.featuresRtree_,
+      'getClosestFeatureToCoordinate does not work with useSpatialIndex set ' +
+      'to false');
+  var filter = opt_filter ? opt_filter : ol.functions.TRUE;
+  this.featuresRtree_.forEachInExtent(extent,
+      /**
+       * @param {ol.Feature} feature Feature.
+       */
+      function(feature) {
+        if (filter(feature)) {
+          var geometry = feature.getGeometry();
+          goog.asserts.assert(geometry,
+              'feature geometry is defined and not null');
+          var previousMinSquaredDistance = minSquaredDistance;
+          minSquaredDistance = geometry.closestPointXY(
+              x, y, closestPoint, minSquaredDistance);
+          if (minSquaredDistance < previousMinSquaredDistance) {
+            closestFeature = feature;
+            // This is sneaky.  Reduce the extent that it is currently being
+            // searched while the R-Tree traversal using this same extent object
+            // is still in progress.  This is safe because the new extent is
+            // strictly contained by the old extent.
+            var minDistance = Math.sqrt(minSquaredDistance);
+            extent[0] = x - minDistance;
+            extent[1] = y - minDistance;
+            extent[2] = x + minDistance;
+            extent[3] = y + minDistance;
+          }
+        }
+      });
+  return closestFeature;
+};
+
+
+/**
+ * Get the extent of the features currently in the source.
+ *
+ * This method is not available when the source is configured with
+ * `useSpatialIndex` set to `false`.
+ * @return {!ol.Extent} Extent.
+ * @api stable
+ */
+ol.source.Vector.prototype.getExtent = function() {
+  goog.asserts.assert(this.featuresRtree_,
+      'getExtent does not work when useSpatialIndex is set to false');
+  return this.featuresRtree_.getExtent();
+};
+
+
+/**
+ * Get a feature by its identifier (the value returned by feature.getId()).
+ * Note that the index treats string and numeric identifiers as the same.  So
+ * `source.getFeatureById(2)` will return a feature with id `'2'` or `2`.
+ *
+ * @param {string|number} id Feature identifier.
+ * @return {ol.Feature} The feature (or `null` if not found).
+ * @api stable
+ */
+ol.source.Vector.prototype.getFeatureById = function(id) {
+  var feature = this.idIndex_[id.toString()];
+  return feature !== undefined ? feature : null;
+};
+
+
+/**
+ * Get the format associated with this source.
+ *
+ * @return {ol.format.Feature|undefined} The feature format.
+ * @api
+ */
+ol.source.Vector.prototype.getFormat = function() {
+  return this.format_;
+};
+
+
+/**
+ * Get the url associated with this source.
+ *
+ * @return {string|ol.FeatureUrlFunction|undefined} The url.
+ * @api
+ */
+ol.source.Vector.prototype.getUrl = function() {
+  return this.url_;
+};
+
+
+/**
+ * @param {ol.events.Event} event Event.
+ * @private
+ */
+ol.source.Vector.prototype.handleFeatureChange_ = function(event) {
+  var feature = /** @type {ol.Feature} */ (event.target);
+  var featureKey = goog.getUid(feature).toString();
+  var geometry = feature.getGeometry();
+  if (!geometry) {
+    if (!(featureKey in this.nullGeometryFeatures_)) {
+      if (this.featuresRtree_) {
+        this.featuresRtree_.remove(feature);
+      }
+      this.nullGeometryFeatures_[featureKey] = feature;
+    }
+  } else {
+    var extent = geometry.getExtent();
+    if (featureKey in this.nullGeometryFeatures_) {
+      delete this.nullGeometryFeatures_[featureKey];
+      if (this.featuresRtree_) {
+        this.featuresRtree_.insert(extent, feature);
+      }
+    } else {
+      if (this.featuresRtree_) {
+        this.featuresRtree_.update(extent, feature);
+      }
+    }
+  }
+  var id = feature.getId();
+  var removed;
+  if (id !== undefined) {
+    var sid = id.toString();
+    if (featureKey in this.undefIdIndex_) {
+      delete this.undefIdIndex_[featureKey];
+      this.idIndex_[sid] = feature;
+    } else {
+      if (this.idIndex_[sid] !== feature) {
+        removed = this.removeFromIdIndex_(feature);
+        goog.asserts.assert(removed,
+            'Expected feature to be removed from index');
+        this.idIndex_[sid] = feature;
+      }
+    }
+  } else {
+    if (!(featureKey in this.undefIdIndex_)) {
+      removed = this.removeFromIdIndex_(feature);
+      goog.asserts.assert(removed,
+          'Expected feature to be removed from index');
+      this.undefIdIndex_[featureKey] = feature;
+    } else {
+      goog.asserts.assert(this.undefIdIndex_[featureKey] === feature,
+          'feature keyed under %s in undefIdKeys', featureKey);
+    }
+  }
+  this.changed();
+  this.dispatchEvent(new ol.source.VectorEvent(
+      ol.source.VectorEventType.CHANGEFEATURE, feature));
+};
+
+
+/**
+ * @return {boolean} Is empty.
+ */
+ol.source.Vector.prototype.isEmpty = function() {
+  return this.featuresRtree_.isEmpty() &&
+      ol.object.isEmpty(this.nullGeometryFeatures_);
+};
+
+
+/**
+ * @param {ol.Extent} extent Extent.
+ * @param {number} resolution Resolution.
+ * @param {ol.proj.Projection} projection Projection.
+ */
+ol.source.Vector.prototype.loadFeatures = function(
+    extent, resolution, projection) {
+  var loadedExtentsRtree = this.loadedExtentsRtree_;
+  var extentsToLoad = this.strategy_(extent, resolution);
+  var i, ii;
+  for (i = 0, ii = extentsToLoad.length; i < ii; ++i) {
+    var extentToLoad = extentsToLoad[i];
+    var alreadyLoaded = loadedExtentsRtree.forEachInExtent(extentToLoad,
+        /**
+         * @param {{extent: ol.Extent}} object Object.
+         * @return {boolean} Contains.
+         */
+        function(object) {
+          return ol.extent.containsExtent(object.extent, extentToLoad);
+        });
+    if (!alreadyLoaded) {
+      this.loader_.call(this, extentToLoad, resolution, projection);
+      loadedExtentsRtree.insert(extentToLoad, {extent: extentToLoad.slice()});
+    }
+  }
+};
+
+
+/**
+ * Remove a single feature from the source.  If you want to remove all features
+ * at once, use the {@link ol.source.Vector#clear source.clear()} method
+ * instead.
+ * @param {ol.Feature} feature Feature to remove.
+ * @api stable
+ */
+ol.source.Vector.prototype.removeFeature = function(feature) {
+  var featureKey = goog.getUid(feature).toString();
+  if (featureKey in this.nullGeometryFeatures_) {
+    delete this.nullGeometryFeatures_[featureKey];
+  } else {
+    if (this.featuresRtree_) {
+      this.featuresRtree_.remove(feature);
+    }
+  }
+  this.removeFeatureInternal(feature);
+  this.changed();
+};
+
+
+/**
+ * Remove feature without firing a `change` event.
+ * @param {ol.Feature} feature Feature.
+ * @protected
+ */
+ol.source.Vector.prototype.removeFeatureInternal = function(feature) {
+  var featureKey = goog.getUid(feature).toString();
+  goog.asserts.assert(featureKey in this.featureChangeKeys_,
+      'featureKey exists in featureChangeKeys');
+  this.featureChangeKeys_[featureKey].forEach(ol.events.unlistenByKey);
+  delete this.featureChangeKeys_[featureKey];
+  var id = feature.getId();
+  if (id !== undefined) {
+    delete this.idIndex_[id.toString()];
+  } else {
+    delete this.undefIdIndex_[featureKey];
+  }
+  this.dispatchEvent(new ol.source.VectorEvent(
+      ol.source.VectorEventType.REMOVEFEATURE, feature));
+};
+
+
+/**
+ * Remove a feature from the id index.  Called internally when the feature id
+ * may have changed.
+ * @param {ol.Feature} feature The feature.
+ * @return {boolean} Removed the feature from the index.
+ * @private
+ */
+ol.source.Vector.prototype.removeFromIdIndex_ = function(feature) {
+  var removed = false;
+  for (var id in this.idIndex_) {
+    if (this.idIndex_[id] === feature) {
+      delete this.idIndex_[id];
+      removed = true;
+      break;
+    }
+  }
+  return removed;
+};
+
+
+/**
+ * @classdesc
+ * Events emitted by {@link ol.source.Vector} instances are instances of this
+ * type.
+ *
+ * @constructor
+ * @extends {ol.events.Event}
+ * @implements {oli.source.VectorEvent}
+ * @param {string} type Type.
+ * @param {ol.Feature=} opt_feature Feature.
+ */
+ol.source.VectorEvent = function(type, opt_feature) {
+
+  ol.events.Event.call(this, type);
+
+  /**
+   * The feature being added or removed.
+   * @type {ol.Feature|undefined}
+   * @api stable
+   */
+  this.feature = opt_feature;
+
+};
+ol.inherits(ol.source.VectorEvent, ol.events.Event);
+
+goog.provide('ol.source.ImageVector');
+
+goog.require('goog.asserts');
+goog.require('ol.events');
+goog.require('ol.events.EventType');
+goog.require('goog.vec.Mat4');
+goog.require('ol.dom');
+goog.require('ol.extent');
+goog.require('ol.render.canvas.ReplayGroup');
+goog.require('ol.renderer.vector');
+goog.require('ol.source.ImageCanvas');
+goog.require('ol.source.Vector');
+goog.require('ol.style.Style');
+goog.require('ol.vec.Mat4');
+
+
+/**
+ * @classdesc
+ * An image source whose images are canvas elements into which vector features
+ * read from a vector source (`ol.source.Vector`) are drawn. An
+ * `ol.source.ImageVector` object is to be used as the `source` of an image
+ * layer (`ol.layer.Image`). Image layers are rotated, scaled, and translated,
+ * as opposed to being re-rendered, during animations and interactions. So, like
+ * any other image layer, an image layer configured with an
+ * `ol.source.ImageVector` will exhibit this behaviour. This is in contrast to a
+ * vector layer, where vector features are re-drawn during animations and
+ * interactions.
+ *
+ * @constructor
+ * @extends {ol.source.ImageCanvas}
+ * @param {olx.source.ImageVectorOptions} options Options.
+ * @api
+ */
+ol.source.ImageVector = function(options) {
+
+  /**
+   * @private
+   * @type {ol.source.Vector}
+   */
+  this.source_ = options.source;
+
+  /**
+   * @private
+   * @type {!goog.vec.Mat4.Number}
+   */
+  this.transform_ = goog.vec.Mat4.createNumber();
+
+  /**
+   * @private
+   * @type {CanvasRenderingContext2D}
+   */
+  this.canvasContext_ = ol.dom.createCanvasContext2D();
+
+  /**
+   * @private
+   * @type {ol.Size}
+   */
+  this.canvasSize_ = [0, 0];
+
+  /**
+   * @private
+   * @type {ol.render.canvas.ReplayGroup}
+   */
+  this.replayGroup_ = null;
+
+  ol.source.ImageCanvas.call(this, {
+    attributions: options.attributions,
+    canvasFunction: this.canvasFunctionInternal_.bind(this),
+    logo: options.logo,
+    projection: options.projection,
+    ratio: options.ratio,
+    resolutions: options.resolutions,
+    state: this.source_.getState()
+  });
+
+  /**
+   * User provided style.
+   * @type {ol.style.Style|Array.<ol.style.Style>|ol.StyleFunction}
+   * @private
+   */
+  this.style_ = null;
+
+  /**
+   * Style function for use within the library.
+   * @type {ol.StyleFunction|undefined}
+   * @private
+   */
+  this.styleFunction_ = undefined;
+
+  this.setStyle(options.style);
+
+  ol.events.listen(this.source_, ol.events.EventType.CHANGE,
+      this.handleSourceChange_, this);
+
+};
+ol.inherits(ol.source.ImageVector, ol.source.ImageCanvas);
+
+
+/**
+ * @param {ol.Extent} extent Extent.
+ * @param {number} resolution Resolution.
+ * @param {number} pixelRatio Pixel ratio.
+ * @param {ol.Size} size Size.
+ * @param {ol.proj.Projection} projection Projection;
+ * @return {HTMLCanvasElement} Canvas element.
+ * @private
+ */
+ol.source.ImageVector.prototype.canvasFunctionInternal_ = function(extent, resolution, pixelRatio, size, projection) {
+
+  var replayGroup = new ol.render.canvas.ReplayGroup(
+      ol.renderer.vector.getTolerance(resolution, pixelRatio), extent,
+      resolution);
+
+  this.source_.loadFeatures(extent, resolution, projection);
+
+  var loading = false;
+  this.source_.forEachFeatureInExtent(extent,
+      /**
+       * @param {ol.Feature} feature Feature.
+       */
+      function(feature) {
+        loading = loading ||
+            this.renderFeature_(feature, resolution, pixelRatio, replayGroup);
+      }, this);
+  replayGroup.finish();
+
+  if (loading) {
+    return null;
+  }
+
+  if (this.canvasSize_[0] != size[0] || this.canvasSize_[1] != size[1]) {
+    this.canvasContext_.canvas.width = size[0];
+    this.canvasContext_.canvas.height = size[1];
+    this.canvasSize_[0] = size[0];
+    this.canvasSize_[1] = size[1];
+  } else {
+    this.canvasContext_.clearRect(0, 0, size[0], size[1]);
+  }
+
+  var transform = this.getTransform_(ol.extent.getCenter(extent),
+      resolution, pixelRatio, size);
+  replayGroup.replay(this.canvasContext_, pixelRatio, transform, 0, {});
+
+  this.replayGroup_ = replayGroup;
+
+  return this.canvasContext_.canvas;
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.source.ImageVector.prototype.forEachFeatureAtCoordinate = function(
+    coordinate, resolution, rotation, skippedFeatureUids, callback) {
+  if (!this.replayGroup_) {
+    return undefined;
+  } else {
+    /** @type {Object.<string, boolean>} */
+    var features = {};
+    return this.replayGroup_.forEachFeatureAtCoordinate(
+        coordinate, resolution, 0, skippedFeatureUids,
+        /**
+         * @param {ol.Feature|ol.render.Feature} feature Feature.
+         * @return {?} Callback result.
+         */
+        function(feature) {
+          goog.asserts.assert(feature !== undefined, 'passed a feature');
+          var key = goog.getUid(feature).toString();
+          if (!(key in features)) {
+            features[key] = true;
+            return callback(feature);
+          }
+        });
+  }
+};
+
+
+/**
+ * Get a reference to the wrapped source.
+ * @return {ol.source.Vector} Source.
+ * @api
+ */
+ol.source.ImageVector.prototype.getSource = function() {
+  return this.source_;
+};
+
+
+/**
+ * Get the style for features.  This returns whatever was passed to the `style`
+ * option at construction or to the `setStyle` method.
+ * @return {ol.style.Style|Array.<ol.style.Style>|ol.StyleFunction}
+ *     Layer style.
+ * @api stable
+ */
+ol.source.ImageVector.prototype.getStyle = function() {
+  return this.style_;
+};
+
+
+/**
+ * Get the style function.
+ * @return {ol.StyleFunction|undefined} Layer style function.
+ * @api stable
+ */
+ol.source.ImageVector.prototype.getStyleFunction = function() {
+  return this.styleFunction_;
+};
+
+
+/**
+ * @param {ol.Coordinate} center Center.
+ * @param {number} resolution Resolution.
+ * @param {number} pixelRatio Pixel ratio.
+ * @param {ol.Size} size Size.
+ * @return {!goog.vec.Mat4.Number} Transform.
+ * @private
+ */
+ol.source.ImageVector.prototype.getTransform_ = function(center, resolution, pixelRatio, size) {
+  return ol.vec.Mat4.makeTransform2D(this.transform_,
+      size[0] / 2, size[1] / 2,
+      pixelRatio / resolution, -pixelRatio / resolution,
+      0,
+      -center[0], -center[1]);
+};
+
+
+/**
+ * Handle changes in image style state.
+ * @param {ol.events.Event} event Image style change event.
+ * @private
+ */
+ol.source.ImageVector.prototype.handleImageChange_ = function(event) {
+  this.changed();
+};
+
+
+/**
+ * @private
+ */
+ol.source.ImageVector.prototype.handleSourceChange_ = function() {
+  // setState will trigger a CHANGE event, so we always rely
+  // change events by calling setState.
+  this.setState(this.source_.getState());
+};
+
+
+/**
+ * @param {ol.Feature} feature Feature.
+ * @param {number} resolution Resolution.
+ * @param {number} pixelRatio Pixel ratio.
+ * @param {ol.render.canvas.ReplayGroup} replayGroup Replay group.
+ * @return {boolean} `true` if an image is loading.
+ * @private
+ */
+ol.source.ImageVector.prototype.renderFeature_ = function(feature, resolution, pixelRatio, replayGroup) {
+  var styles;
+  var styleFunction = feature.getStyleFunction();
+  if (styleFunction) {
+    styles = styleFunction.call(feature, resolution);
+  } else if (this.styleFunction_) {
+    styles = this.styleFunction_(feature, resolution);
+  }
+  if (!styles) {
+    return false;
+  }
+  var i, ii, loading = false;
+  if (!Array.isArray(styles)) {
+    styles = [styles];
+  }
+  for (i = 0, ii = styles.length; i < ii; ++i) {
+    loading = ol.renderer.vector.renderFeature(
+        replayGroup, feature, styles[i],
+        ol.renderer.vector.getSquaredTolerance(resolution, pixelRatio),
+        this.handleImageChange_, this) || loading;
+  }
+  return loading;
+};
+
+
+/**
+ * Set the style for features.  This can be a single style object, an array
+ * of styles, or a function that takes a feature and resolution and returns
+ * an array of styles. If it is `undefined` the default style is used. If
+ * it is `null` the layer has no style (a `null` style), so only features
+ * that have their own styles will be rendered in the layer. See
+ * {@link ol.style} for information on the default style.
+ * @param {ol.style.Style|Array.<ol.style.Style>|ol.StyleFunction|undefined}
+ *     style Layer style.
+ * @api stable
+ */
+ol.source.ImageVector.prototype.setStyle = function(style) {
+  this.style_ = style !== undefined ? style : ol.style.defaultStyleFunction;
+  this.styleFunction_ = !style ?
+      undefined : ol.style.createStyleFunction(this.style_);
+  this.changed();
+};
+
+goog.provide('ol.renderer.canvas.ImageLayer');
+
+goog.require('goog.asserts');
+goog.require('goog.vec.Mat4');
+goog.require('ol.functions');
+goog.require('ol.ImageBase');
+goog.require('ol.ViewHint');
+goog.require('ol.dom');
+goog.require('ol.extent');
+goog.require('ol.layer.Image');
+goog.require('ol.proj');
+goog.require('ol.renderer.canvas.Layer');
+goog.require('ol.source.ImageVector');
+goog.require('ol.vec.Mat4');
+
+
+/**
+ * @constructor
+ * @extends {ol.renderer.canvas.Layer}
+ * @param {ol.layer.Image} imageLayer Single image layer.
+ */
+ol.renderer.canvas.ImageLayer = function(imageLayer) {
+
+  ol.renderer.canvas.Layer.call(this, imageLayer);
+
+  /**
+   * @private
+   * @type {?ol.ImageBase}
+   */
+  this.image_ = null;
+
+  /**
+   * @private
+   * @type {!goog.vec.Mat4.Number}
+   */
+  this.imageTransform_ = goog.vec.Mat4.createNumber();
+
+  /**
+   * @private
+   * @type {?goog.vec.Mat4.Number}
+   */
+  this.imageTransformInv_ = null;
+
+  /**
+   * @private
+   * @type {CanvasRenderingContext2D}
+   */
+  this.hitCanvasContext_ = null;
+
+};
+ol.inherits(ol.renderer.canvas.ImageLayer, ol.renderer.canvas.Layer);
+
+
+/**
+ * @inheritDoc
+ */
+ol.renderer.canvas.ImageLayer.prototype.forEachFeatureAtCoordinate = function(coordinate, frameState, callback, thisArg) {
+  var layer = this.getLayer();
+  var source = layer.getSource();
+  var resolution = frameState.viewState.resolution;
+  var rotation = frameState.viewState.rotation;
+  var skippedFeatureUids = frameState.skippedFeatureUids;
+  return source.forEachFeatureAtCoordinate(
+      coordinate, resolution, rotation, skippedFeatureUids,
+      /**
+       * @param {ol.Feature|ol.render.Feature} feature Feature.
+       * @return {?} Callback result.
+       */
+      function(feature) {
+        return callback.call(thisArg, feature, layer);
+      });
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.renderer.canvas.ImageLayer.prototype.forEachLayerAtPixel = function(pixel, frameState, callback, thisArg) {
+  if (!this.getImage()) {
+    return undefined;
+  }
+
+  if (this.getLayer().getSource() instanceof ol.source.ImageVector) {
+    // for ImageVector sources use the original hit-detection logic,
+    // so that for example also transparent polygons are detected
+    var coordinate = pixel.slice();
+    ol.vec.Mat4.multVec2(
+        frameState.pixelToCoordinateMatrix, coordinate, coordinate);
+    var hasFeature = this.forEachFeatureAtCoordinate(
+        coordinate, frameState, ol.functions.TRUE, this);
+
+    if (hasFeature) {
+      return callback.call(thisArg, this.getLayer());
+    } else {
+      return undefined;
+    }
+  } else {
+    // for all other image sources directly check the image
+    if (!this.imageTransformInv_) {
+      this.imageTransformInv_ = goog.vec.Mat4.createNumber();
+      goog.vec.Mat4.invert(this.imageTransform_, this.imageTransformInv_);
+    }
+
+    var pixelOnCanvas =
+        this.getPixelOnCanvas(pixel, this.imageTransformInv_);
+
+    if (!this.hitCanvasContext_) {
+      this.hitCanvasContext_ = ol.dom.createCanvasContext2D(1, 1);
+    }
+
+    this.hitCanvasContext_.clearRect(0, 0, 1, 1);
+    this.hitCanvasContext_.drawImage(
+        this.getImage(), pixelOnCanvas[0], pixelOnCanvas[1], 1, 1, 0, 0, 1, 1);
+
+    var imageData = this.hitCanvasContext_.getImageData(0, 0, 1, 1).data;
+    if (imageData[3] > 0) {
+      return callback.call(thisArg, this.getLayer());
+    } else {
+      return undefined;
+    }
+  }
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.renderer.canvas.ImageLayer.prototype.getImage = function() {
+  return !this.image_ ? null : this.image_.getImage();
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.renderer.canvas.ImageLayer.prototype.getImageTransform = function() {
+  return this.imageTransform_;
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.renderer.canvas.ImageLayer.prototype.prepareFrame = function(frameState, layerState) {
+
+  var pixelRatio = frameState.pixelRatio;
+  var viewState = frameState.viewState;
+  var viewCenter = viewState.center;
+  var viewResolution = viewState.resolution;
+
+  var image;
+  var imageLayer = this.getLayer();
+  goog.asserts.assertInstanceof(imageLayer, ol.layer.Image,
+      'layer is an instance of ol.layer.Image');
+  var imageSource = imageLayer.getSource();
+
+  var hints = frameState.viewHints;
+
+  var renderedExtent = frameState.extent;
+  if (layerState.extent !== undefined) {
+    renderedExtent = ol.extent.getIntersection(
+        renderedExtent, layerState.extent);
+  }
+
+  if (!hints[ol.ViewHint.ANIMATING] && !hints[ol.ViewHint.INTERACTING] &&
+      !ol.extent.isEmpty(renderedExtent)) {
+    var projection = viewState.projection;
+    if (!ol.ENABLE_RASTER_REPROJECTION) {
+      var sourceProjection = imageSource.getProjection();
+      if (sourceProjection) {
+        goog.asserts.assert(ol.proj.equivalent(projection, sourceProjection),
+            'projection and sourceProjection are equivalent');
+        projection = sourceProjection;
+      }
+    }
+    image = imageSource.getImage(
+        renderedExtent, viewResolution, pixelRatio, projection);
+    if (image) {
+      var loaded = this.loadImage(image);
+      if (loaded) {
+        this.image_ = image;
+      }
+    }
+  }
+
+  if (this.image_) {
+    image = this.image_;
+    var imageExtent = image.getExtent();
+    var imageResolution = image.getResolution();
+    var imagePixelRatio = image.getPixelRatio();
+    var scale = pixelRatio * imageResolution /
+        (viewResolution * imagePixelRatio);
+    ol.vec.Mat4.makeTransform2D(this.imageTransform_,
+        pixelRatio * frameState.size[0] / 2,
+        pixelRatio * frameState.size[1] / 2,
+        scale, scale,
+        0,
+        imagePixelRatio * (imageExtent[0] - viewCenter[0]) / imageResolution,
+        imagePixelRatio * (viewCenter[1] - imageExtent[3]) / imageResolution);
+    this.imageTransformInv_ = null;
+    this.updateAttributions(frameState.attributions, image.getAttributions());
+    this.updateLogos(frameState, imageSource);
+  }
+
+  return !!this.image_;
+};
+
+// FIXME find correct globalCompositeOperation
+
+goog.provide('ol.renderer.canvas.TileLayer');
+
+goog.require('goog.asserts');
+goog.require('goog.vec.Mat4');
+goog.require('ol.TileRange');
+goog.require('ol.TileState');
+goog.require('ol.array');
+goog.require('ol.dom');
+goog.require('ol.extent');
+goog.require('ol.render.EventType');
+goog.require('ol.renderer.canvas.Layer');
+goog.require('ol.size');
+goog.require('ol.source.Tile');
+goog.require('ol.vec.Mat4');
+
+
+/**
+ * @constructor
+ * @extends {ol.renderer.canvas.Layer}
+ * @param {ol.layer.Tile|ol.layer.VectorTile} tileLayer Tile layer.
+ */
+ol.renderer.canvas.TileLayer = function(tileLayer) {
+
+  ol.renderer.canvas.Layer.call(this, tileLayer);
+
+  /**
+   * @protected
+   * @type {CanvasRenderingContext2D}
+   */
+  this.context = ol.dom.createCanvasContext2D();
+
+  /**
+   * @protected
+   * @type {Array.<ol.Tile|undefined>}
+   */
+  this.renderedTiles = null;
+
+  /**
+   * @protected
+   * @type {ol.Extent}
+   */
+  this.tmpExtent = ol.extent.createEmpty();
+
+  /**
+   * @private
+   * @type {ol.TileCoord}
+   */
+  this.tmpTileCoord_ = [0, 0, 0];
+
+  /**
+   * @private
+   * @type {!goog.vec.Mat4.Number}
+   */
+  this.imageTransform_ = goog.vec.Mat4.createNumber();
+
+  /**
+   * @protected
+   * @type {number}
+   */
+  this.zDirection = 0;
+
+};
+ol.inherits(ol.renderer.canvas.TileLayer, ol.renderer.canvas.Layer);
+
+
+/**
+ * @inheritDoc
+ */
+ol.renderer.canvas.TileLayer.prototype.composeFrame = function(
+    frameState, layerState, context) {
+  var transform = this.getTransform(frameState, 0);
+  this.dispatchPreComposeEvent(context, frameState, transform);
+  this.renderTileImages(context, frameState, layerState);
+  this.dispatchPostComposeEvent(context, frameState, transform);
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.renderer.canvas.TileLayer.prototype.prepareFrame = function(
+    frameState, layerState) {
+
+  var pixelRatio = frameState.pixelRatio;
+  var viewState = frameState.viewState;
+  var projection = viewState.projection;
+
+  var tileLayer = this.getLayer();
+  var tileSource = tileLayer.getSource();
+  goog.asserts.assertInstanceof(tileSource, ol.source.Tile,
+      'source is an ol.source.Tile');
+  var tileGrid = tileSource.getTileGridForProjection(projection);
+  var z = tileGrid.getZForResolution(viewState.resolution, this.zDirection);
+  var tileResolution = tileGrid.getResolution(z);
+  var center = viewState.center;
+  var extent;
+  if (tileResolution == viewState.resolution) {
+    center = this.snapCenterToPixel(center, tileResolution, frameState.size);
+    extent = ol.extent.getForViewAndSize(
+        center, tileResolution, viewState.rotation, frameState.size);
+  } else {
+    extent = frameState.extent;
+  }
+
+  if (layerState.extent !== undefined) {
+    extent = ol.extent.getIntersection(extent, layerState.extent);
+  }
+  if (ol.extent.isEmpty(extent)) {
+    // Return false to prevent the rendering of the layer.
+    return false;
+  }
+
+  var tileRange = tileGrid.getTileRangeForExtentAndResolution(
+      extent, tileResolution);
+
+  /**
+   * @type {Object.<number, Object.<string, ol.Tile>>}
+   */
+  var tilesToDrawByZ = {};
+  tilesToDrawByZ[z] = {};
+
+  var findLoadedTiles = this.createLoadedTileFinder(
+      tileSource, projection, tilesToDrawByZ);
+
+  var useInterimTilesOnError = tileLayer.getUseInterimTilesOnError();
+
+  var tmpExtent = ol.extent.createEmpty();
+  var tmpTileRange = new ol.TileRange(0, 0, 0, 0);
+  var childTileRange, fullyLoaded, tile, x, y;
+  var drawableTile = (
+      /**
+       * @param {!ol.Tile} tile Tile.
+       * @return {boolean} Tile is selected.
+       */
+      function(tile) {
+        var tileState = tile.getState();
+        return tileState == ol.TileState.LOADED ||
+            tileState == ol.TileState.EMPTY ||
+            tileState == ol.TileState.ERROR && !useInterimTilesOnError;
+      });
+  for (x = tileRange.minX; x <= tileRange.maxX; ++x) {
+    for (y = tileRange.minY; y <= tileRange.maxY; ++y) {
+      tile = tileSource.getTile(z, x, y, pixelRatio, projection);
+      if (!drawableTile(tile) && tile.interimTile) {
+        tile = tile.interimTile;
+      }
+      goog.asserts.assert(tile);
+      if (drawableTile(tile)) {
+        tilesToDrawByZ[z][tile.tileCoord.toString()] = tile;
+        continue;
+      }
+      fullyLoaded = tileGrid.forEachTileCoordParentTileRange(
+          tile.tileCoord, findLoadedTiles, null, tmpTileRange, tmpExtent);
+      if (!fullyLoaded) {
+        childTileRange = tileGrid.getTileCoordChildTileRange(
+            tile.tileCoord, tmpTileRange, tmpExtent);
+        if (childTileRange) {
+          findLoadedTiles(z + 1, childTileRange);
+        }
+      }
+
+    }
+  }
+
+  /** @type {Array.<number>} */
+  var zs = Object.keys(tilesToDrawByZ).map(Number);
+  zs.sort(ol.array.numberSafeCompareFunction);
+  var renderables = [];
+  var i, ii, currentZ, tileCoordKey, tilesToDraw;
+  for (i = 0, ii = zs.length; i < ii; ++i) {
+    currentZ = zs[i];
+    tilesToDraw = tilesToDrawByZ[currentZ];
+    for (tileCoordKey in tilesToDraw) {
+      tile = tilesToDraw[tileCoordKey];
+      if (tile.getState() == ol.TileState.LOADED) {
+        renderables.push(tile);
+      }
+    }
+  }
+  this.renderedTiles = renderables;
+
+  this.updateUsedTiles(frameState.usedTiles, tileSource, z, tileRange);
+  this.manageTilePyramid(frameState, tileSource, tileGrid, pixelRatio,
+      projection, extent, z, tileLayer.getPreload());
+  this.scheduleExpireCache(frameState, tileSource);
+  this.updateLogos(frameState, tileSource);
+
+  return true;
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.renderer.canvas.TileLayer.prototype.forEachLayerAtPixel = function(
+    pixel, frameState, callback, thisArg) {
+  var canvas = this.context.canvas;
+  var size = frameState.size;
+  canvas.width = size[0];
+  canvas.height = size[1];
+  this.composeFrame(frameState, this.getLayer().getLayerState(), this.context);
+
+  var imageData = this.context.getImageData(
+      pixel[0], pixel[1], 1, 1).data;
+
+  if (imageData[3] > 0) {
+    return callback.call(thisArg, this.getLayer());
+  } else {
+    return undefined;
+  }
+};
+
+
+/**
+ * @param {CanvasRenderingContext2D} context Context.
+ * @param {olx.FrameState} frameState Frame state.
+ * @param {ol.LayerState} layerState Layer state.
+ * @protected
+ */
+ol.renderer.canvas.TileLayer.prototype.renderTileImages = function(context, frameState, layerState) {
+  var pixelRatio = frameState.pixelRatio;
+  var viewState = frameState.viewState;
+  var center = viewState.center;
+  var projection = viewState.projection;
+  var resolution = viewState.resolution;
+  var rotation = viewState.rotation;
+  var size = frameState.size;
+  var offsetX = Math.round(pixelRatio * size[0] / 2);
+  var offsetY = Math.round(pixelRatio * size[1] / 2);
+  var pixelScale = pixelRatio / resolution;
+  var layer = this.getLayer();
+  var source = layer.getSource();
+  goog.asserts.assertInstanceof(source, ol.source.Tile,
+      'source is an ol.source.Tile');
+  var tileGutter = source.getGutter(projection);
+  var tileGrid = source.getTileGridForProjection(projection);
+
+  var hasRenderListeners = layer.hasListener(ol.render.EventType.RENDER);
+  var renderContext = context;
+  var drawOffsetX, drawOffsetY, drawScale, drawSize;
+  if (rotation || hasRenderListeners) {
+    renderContext = this.context;
+    var renderCanvas = renderContext.canvas;
+    var drawZ = tileGrid.getZForResolution(resolution);
+    var drawTileSize = source.getTilePixelSize(drawZ, pixelRatio, projection);
+    var tileSize = ol.size.toSize(tileGrid.getTileSize(drawZ));
+    drawScale = drawTileSize[0] / tileSize[0];
+    var width = context.canvas.width * drawScale;
+    var height = context.canvas.height * drawScale;
+    // Make sure the canvas is big enough for all possible rotation angles
+    drawSize = Math.round(Math.sqrt(width * width + height * height));
+    if (renderCanvas.width != drawSize) {
+      renderCanvas.width = renderCanvas.height = drawSize;
+    } else {
+      renderContext.clearRect(0, 0, drawSize, drawSize);
+    }
+    drawOffsetX = (drawSize - width) / 2 / drawScale;
+    drawOffsetY = (drawSize - height) / 2 / drawScale;
+    pixelScale *= drawScale;
+    offsetX = Math.round(drawScale * (offsetX + drawOffsetX));
+    offsetY = Math.round(drawScale * (offsetY + drawOffsetY));
+  }
+  // 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 = renderContext.globalAlpha;
+  renderContext.globalAlpha = layerState.opacity;
+
+  var tilesToDraw = this.renderedTiles;
+
+  var pixelExtents;
+  var opaque = source.getOpaque(projection) && layerState.opacity == 1;
+  if (!opaque) {
+    tilesToDraw.reverse();
+    pixelExtents = [];
+  }
+
+  var extent = layerState.extent;
+  var clipped = extent !== undefined;
+  if (clipped) {
+    goog.asserts.assert(extent !== undefined,
+        'layerState extent is defined');
+    var topLeft = ol.extent.getTopLeft(extent);
+    var topRight = ol.extent.getTopRight(extent);
+    var bottomRight = ol.extent.getBottomRight(extent);
+    var bottomLeft = ol.extent.getBottomLeft(extent);
+
+    ol.vec.Mat4.multVec2(frameState.coordinateToPixelMatrix,
+        topLeft, topLeft);
+    ol.vec.Mat4.multVec2(frameState.coordinateToPixelMatrix,
+        topRight, topRight);
+    ol.vec.Mat4.multVec2(frameState.coordinateToPixelMatrix,
+        bottomRight, bottomRight);
+    ol.vec.Mat4.multVec2(frameState.coordinateToPixelMatrix,
+        bottomLeft, bottomLeft);
+
+    var ox = drawOffsetX || 0;
+    var oy = drawOffsetY || 0;
+    renderContext.save();
+    var cx = (renderContext.canvas.width * pixelRatio) / 2;
+    var cy = (renderContext.canvas.height * pixelRatio) / 2;
+    ol.render.canvas.rotateAtOffset(renderContext, -rotation, cx, cy);
+    renderContext.beginPath();
+    renderContext.moveTo(topLeft[0] * pixelRatio + ox, topLeft[1] * pixelRatio + oy);
+    renderContext.lineTo(topRight[0] * pixelRatio + ox, topRight[1] * pixelRatio + oy);
+    renderContext.lineTo(bottomRight[0] * pixelRatio + ox, bottomRight[1] * pixelRatio + oy);
+    renderContext.lineTo(bottomLeft[0] * pixelRatio + ox, bottomLeft[1] * pixelRatio + oy);
+    renderContext.clip();
+    ol.render.canvas.rotateAtOffset(renderContext, rotation, cx, cy);
+  }
+
+  for (var i = 0, ii = tilesToDraw.length; i < ii; ++i) {
+    var tile = tilesToDraw[i];
+    var tileCoord = tile.getTileCoord();
+    var tileExtent = tileGrid.getTileCoordExtent(tileCoord, this.tmpExtent);
+    var currentZ = tileCoord[0];
+    // Calculate all insert points by tile widths from a common origin to avoid
+    // gaps caused by rounding
+    var origin = ol.extent.getBottomLeft(tileGrid.getTileCoordExtent(
+        tileGrid.getTileCoordForCoordAndZ(center, currentZ, this.tmpTileCoord_)));
+    var w = Math.round(ol.extent.getWidth(tileExtent) * pixelScale);
+    var h = Math.round(ol.extent.getHeight(tileExtent) * pixelScale);
+    var left = Math.round((tileExtent[0] - origin[0]) * pixelScale / w) * w +
+        offsetX + Math.round((origin[0] - center[0]) * pixelScale);
+    var top = Math.round((origin[1] - tileExtent[3]) * pixelScale / h) * h +
+        offsetY + Math.round((center[1] - origin[1]) * pixelScale);
+    if (!opaque) {
+      var pixelExtent = [left, top, left + w, top + h];
+      // Create a clip mask for regions in this low resolution tile that are
+      // already filled by a higher resolution tile
+      renderContext.save();
+      for (var j = 0, jj = pixelExtents.length; j < jj; ++j) {
+        var clipExtent = pixelExtents[j];
+        if (ol.extent.intersects(pixelExtent, clipExtent)) {
+          renderContext.beginPath();
+          // counter-clockwise (outer ring) for current tile
+          renderContext.moveTo(pixelExtent[0], pixelExtent[1]);
+          renderContext.lineTo(pixelExtent[0], pixelExtent[3]);
+          renderContext.lineTo(pixelExtent[2], pixelExtent[3]);
+          renderContext.lineTo(pixelExtent[2], pixelExtent[1]);
+          // clockwise (inner ring) for higher resolution tile
+          renderContext.moveTo(clipExtent[0], clipExtent[1]);
+          renderContext.lineTo(clipExtent[2], clipExtent[1]);
+          renderContext.lineTo(clipExtent[2], clipExtent[3]);
+          renderContext.lineTo(clipExtent[0], clipExtent[3]);
+          renderContext.closePath();
+          renderContext.clip();
+        }
+      }
+      pixelExtents.push(pixelExtent);
+    }
+    var tilePixelSize = source.getTilePixelSize(currentZ, pixelRatio, projection);
+    renderContext.drawImage(tile.getImage(), tileGutter, tileGutter,
+        tilePixelSize[0], tilePixelSize[1], left, top, w, h);
+    if (!opaque) {
+      renderContext.restore();
+    }
+  }
+
+  if (clipped) {
+    renderContext.restore();
+  }
+
+  if (hasRenderListeners) {
+    var dX = drawOffsetX - offsetX / drawScale + offsetX;
+    var dY = drawOffsetY - offsetY / drawScale + offsetY;
+    var imageTransform = ol.vec.Mat4.makeTransform2D(this.imageTransform_,
+        drawSize / 2 - dX, drawSize / 2 - dY, pixelScale, -pixelScale,
+        -rotation, -center[0] + dX / pixelScale, -center[1] - dY / pixelScale);
+    this.dispatchRenderEvent(renderContext, frameState, imageTransform);
+  }
+  if (rotation || hasRenderListeners) {
+    context.drawImage(renderContext.canvas, -Math.round(drawOffsetX),
+        -Math.round(drawOffsetY), drawSize / drawScale, drawSize / drawScale);
+  }
+  renderContext.globalAlpha = alpha;
+};
+
+
+/**
+ * @function
+ * @return {ol.layer.Tile|ol.layer.VectorTile}
+ */
+ol.renderer.canvas.TileLayer.prototype.getLayer;
+
+goog.provide('ol.renderer.canvas.VectorLayer');
+
+goog.require('goog.asserts');
+goog.require('ol.events');
+goog.require('ol.ViewHint');
+goog.require('ol.dom');
+goog.require('ol.extent');
+goog.require('ol.layer.Vector');
+goog.require('ol.render.EventType');
+goog.require('ol.render.canvas');
+goog.require('ol.render.canvas.ReplayGroup');
+goog.require('ol.renderer.canvas.Layer');
+goog.require('ol.renderer.vector');
+goog.require('ol.source.Vector');
+
+
+/**
+ * @constructor
+ * @extends {ol.renderer.canvas.Layer}
+ * @param {ol.layer.Vector} vectorLayer Vector layer.
+ */
+ol.renderer.canvas.VectorLayer = function(vectorLayer) {
+
+  ol.renderer.canvas.Layer.call(this, vectorLayer);
+
+  /**
+   * @private
+   * @type {boolean}
+   */
+  this.dirty_ = false;
+
+  /**
+   * @private
+   * @type {number}
+   */
+  this.renderedRevision_ = -1;
+
+  /**
+   * @private
+   * @type {number}
+   */
+  this.renderedResolution_ = NaN;
+
+  /**
+   * @private
+   * @type {ol.Extent}
+   */
+  this.renderedExtent_ = ol.extent.createEmpty();
+
+  /**
+   * @private
+   * @type {function(ol.Feature, ol.Feature): number|null}
+   */
+  this.renderedRenderOrder_ = null;
+
+  /**
+   * @private
+   * @type {ol.render.canvas.ReplayGroup}
+   */
+  this.replayGroup_ = null;
+
+  /**
+   * @private
+   * @type {CanvasRenderingContext2D}
+   */
+  this.context_ = ol.dom.createCanvasContext2D();
+
+};
+ol.inherits(ol.renderer.canvas.VectorLayer, ol.renderer.canvas.Layer);
+
+
+/**
+ * @inheritDoc
+ */
+ol.renderer.canvas.VectorLayer.prototype.composeFrame = function(frameState, layerState, context) {
+
+  var extent = frameState.extent;
+  var pixelRatio = frameState.pixelRatio;
+  var skippedFeatureUids = layerState.managed ?
+      frameState.skippedFeatureUids : {};
+  var viewState = frameState.viewState;
+  var projection = viewState.projection;
+  var rotation = viewState.rotation;
+  var projectionExtent = projection.getExtent();
+  var vectorSource = this.getLayer().getSource();
+  goog.asserts.assertInstanceof(vectorSource, ol.source.Vector);
+
+  var transform = this.getTransform(frameState, 0);
+
+  this.dispatchPreComposeEvent(context, frameState, transform);
+
+  var replayGroup = this.replayGroup_;
+  if (replayGroup && !replayGroup.isEmpty()) {
+    var layer = this.getLayer();
+    var replayContext;
+    if (layer.hasListener(ol.render.EventType.RENDER)) {
+      // resize and clear
+      this.context_.canvas.width = context.canvas.width;
+      this.context_.canvas.height = context.canvas.height;
+      replayContext = this.context_;
+    } else {
+      replayContext = context;
+    }
+    // for performance reasons, context.save / context.restore is not used
+    // to save and restore the transformation matrix and the opacity.
+    // see http://jsperf.com/context-save-restore-versus-variable
+    var alpha = replayContext.globalAlpha;
+    replayContext.globalAlpha = layerState.opacity;
+
+    var width = frameState.size[0] * pixelRatio;
+    var height = frameState.size[1] * pixelRatio;
+    ol.render.canvas.rotateAtOffset(replayContext, -rotation,
+        width / 2, height / 2);
+    replayGroup.replay(replayContext, pixelRatio, transform, rotation,
+        skippedFeatureUids);
+    if (vectorSource.getWrapX() && projection.canWrapX() &&
+        !ol.extent.containsExtent(projectionExtent, extent)) {
+      var startX = extent[0];
+      var worldWidth = ol.extent.getWidth(projectionExtent);
+      var world = 0;
+      var offsetX;
+      while (startX < projectionExtent[0]) {
+        --world;
+        offsetX = worldWidth * world;
+        transform = this.getTransform(frameState, offsetX);
+        replayGroup.replay(replayContext, pixelRatio, transform, rotation,
+            skippedFeatureUids);
+        startX += worldWidth;
+      }
+      world = 0;
+      startX = extent[2];
+      while (startX > projectionExtent[2]) {
+        ++world;
+        offsetX = worldWidth * world;
+        transform = this.getTransform(frameState, offsetX);
+        replayGroup.replay(replayContext, pixelRatio, transform, rotation,
+            skippedFeatureUids);
+        startX -= worldWidth;
+      }
+      // restore original transform for render and compose events
+      transform = this.getTransform(frameState, 0);
+    }
+    ol.render.canvas.rotateAtOffset(replayContext, rotation,
+        width / 2, height / 2);
+
+    if (replayContext != context) {
+      this.dispatchRenderEvent(replayContext, frameState, transform);
+      context.drawImage(replayContext.canvas, 0, 0);
+    }
+    replayContext.globalAlpha = alpha;
+  }
+
+  this.dispatchPostComposeEvent(context, frameState, transform);
+
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.renderer.canvas.VectorLayer.prototype.forEachFeatureAtCoordinate = function(coordinate, frameState, callback, thisArg) {
+  if (!this.replayGroup_) {
+    return undefined;
+  } else {
+    var resolution = frameState.viewState.resolution;
+    var rotation = frameState.viewState.rotation;
+    var layer = this.getLayer();
+    /** @type {Object.<string, boolean>} */
+    var features = {};
+    return this.replayGroup_.forEachFeatureAtCoordinate(coordinate, resolution,
+        rotation, {},
+        /**
+         * @param {ol.Feature|ol.render.Feature} feature Feature.
+         * @return {?} Callback result.
+         */
+        function(feature) {
+          goog.asserts.assert(feature !== undefined, 'received a feature');
+          var key = goog.getUid(feature).toString();
+          if (!(key in features)) {
+            features[key] = true;
+            return callback.call(thisArg, feature, layer);
+          }
+        });
+  }
+};
+
+
+/**
+ * Handle changes in image style state.
+ * @param {ol.events.Event} event Image style change event.
+ * @private
+ */
+ol.renderer.canvas.VectorLayer.prototype.handleStyleImageChange_ = function(event) {
+  this.renderIfReadyAndVisible();
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.renderer.canvas.VectorLayer.prototype.prepareFrame = function(frameState, layerState) {
+
+  var vectorLayer = /** @type {ol.layer.Vector} */ (this.getLayer());
+  goog.asserts.assertInstanceof(vectorLayer, ol.layer.Vector,
+      'layer is an instance of ol.layer.Vector');
+  var vectorSource = vectorLayer.getSource();
+
+  this.updateAttributions(
+      frameState.attributions, vectorSource.getAttributions());
+  this.updateLogos(frameState, vectorSource);
+
+  var animating = frameState.viewHints[ol.ViewHint.ANIMATING];
+  var interacting = frameState.viewHints[ol.ViewHint.INTERACTING];
+  var updateWhileAnimating = vectorLayer.getUpdateWhileAnimating();
+  var updateWhileInteracting = vectorLayer.getUpdateWhileInteracting();
+
+  if (!this.dirty_ && (!updateWhileAnimating && animating) ||
+      (!updateWhileInteracting && interacting)) {
+    return true;
+  }
+
+  var frameStateExtent = frameState.extent;
+  var viewState = frameState.viewState;
+  var projection = viewState.projection;
+  var resolution = viewState.resolution;
+  var pixelRatio = frameState.pixelRatio;
+  var vectorLayerRevision = vectorLayer.getRevision();
+  var vectorLayerRenderBuffer = vectorLayer.getRenderBuffer();
+  var vectorLayerRenderOrder = vectorLayer.getRenderOrder();
+
+  if (vectorLayerRenderOrder === undefined) {
+    vectorLayerRenderOrder = ol.renderer.vector.defaultOrder;
+  }
+
+  var extent = ol.extent.buffer(frameStateExtent,
+      vectorLayerRenderBuffer * resolution);
+  var projectionExtent = viewState.projection.getExtent();
+
+  if (vectorSource.getWrapX() && viewState.projection.canWrapX() &&
+      !ol.extent.containsExtent(projectionExtent, frameState.extent)) {
+    // For the replay group, we need an extent that intersects the real world
+    // (-180° to +180°). To support geometries in a coordinate range from -540°
+    // to +540°, we add at least 1 world width on each side of the projection
+    // extent. If the viewport is wider than the world, we need to add half of
+    // the viewport width to make sure we cover the whole viewport.
+    var worldWidth = ol.extent.getWidth(projectionExtent);
+    var buffer = Math.max(ol.extent.getWidth(extent) / 2, worldWidth);
+    extent[0] = projectionExtent[0] - buffer;
+    extent[2] = projectionExtent[2] + buffer;
+  }
+
+  if (!this.dirty_ &&
+      this.renderedResolution_ == resolution &&
+      this.renderedRevision_ == vectorLayerRevision &&
+      this.renderedRenderOrder_ == vectorLayerRenderOrder &&
+      ol.extent.containsExtent(this.renderedExtent_, extent)) {
+    return true;
+  }
+
+  this.replayGroup_ = null;
+
+  this.dirty_ = false;
+
+  var replayGroup =
+      new ol.render.canvas.ReplayGroup(
+          ol.renderer.vector.getTolerance(resolution, pixelRatio), extent,
+          resolution, vectorLayer.getRenderBuffer());
+  vectorSource.loadFeatures(extent, resolution, projection);
+  /**
+   * @param {ol.Feature} feature Feature.
+   * @this {ol.renderer.canvas.VectorLayer}
+   */
+  var renderFeature = function(feature) {
+    var styles;
+    var styleFunction = feature.getStyleFunction();
+    if (styleFunction) {
+      styles = styleFunction.call(feature, resolution);
+    } else {
+      styleFunction = vectorLayer.getStyleFunction();
+      if (styleFunction) {
+        styles = styleFunction(feature, resolution);
+      }
+    }
+    if (styles) {
+      var dirty = this.renderFeature(
+          feature, resolution, pixelRatio, styles, replayGroup);
+      this.dirty_ = this.dirty_ || dirty;
+    }
+  };
+  if (vectorLayerRenderOrder) {
+    /** @type {Array.<ol.Feature>} */
+    var features = [];
+    vectorSource.forEachFeatureInExtent(extent,
+        /**
+         * @param {ol.Feature} feature Feature.
+         */
+        function(feature) {
+          features.push(feature);
+        }, this);
+    features.sort(vectorLayerRenderOrder);
+    features.forEach(renderFeature, this);
+  } else {
+    vectorSource.forEachFeatureInExtent(extent, renderFeature, this);
+  }
+  replayGroup.finish();
+
+  this.renderedResolution_ = resolution;
+  this.renderedRevision_ = vectorLayerRevision;
+  this.renderedRenderOrder_ = vectorLayerRenderOrder;
+  this.renderedExtent_ = extent;
+  this.replayGroup_ = replayGroup;
+
+  return true;
+};
+
+
+/**
+ * @param {ol.Feature} feature Feature.
+ * @param {number} resolution Resolution.
+ * @param {number} pixelRatio Pixel ratio.
+ * @param {(ol.style.Style|Array.<ol.style.Style>)} styles The style or array of
+ *     styles.
+ * @param {ol.render.canvas.ReplayGroup} replayGroup Replay group.
+ * @return {boolean} `true` if an image is loading.
+ */
+ol.renderer.canvas.VectorLayer.prototype.renderFeature = function(feature, resolution, pixelRatio, styles, replayGroup) {
+  if (!styles) {
+    return false;
+  }
+  var loading = false;
+  if (Array.isArray(styles)) {
+    for (var i = 0, ii = styles.length; i < ii; ++i) {
+      loading = ol.renderer.vector.renderFeature(
+          replayGroup, feature, styles[i],
+          ol.renderer.vector.getSquaredTolerance(resolution, pixelRatio),
+          this.handleStyleImageChange_, this) || loading;
+    }
+  } else {
+    loading = ol.renderer.vector.renderFeature(
+        replayGroup, feature, styles,
+        ol.renderer.vector.getSquaredTolerance(resolution, pixelRatio),
+        this.handleStyleImageChange_, this) || loading;
+  }
+  return loading;
+};
+
+goog.provide('ol.TileUrlFunction');
+
+goog.require('goog.asserts');
+goog.require('ol.math');
+goog.require('ol.tilecoord');
+
+
+/**
+ * @param {string} template Template.
+ * @param {ol.tilegrid.TileGrid} tileGrid Tile grid.
+ * @return {ol.TileUrlFunctionType} Tile URL function.
+ */
+ol.TileUrlFunction.createFromTemplate = function(template, tileGrid) {
+  var zRegEx = /\{z\}/g;
+  var xRegEx = /\{x\}/g;
+  var yRegEx = /\{y\}/g;
+  var dashYRegEx = /\{-y\}/g;
+  return (
+      /**
+       * @param {ol.TileCoord} tileCoord Tile Coordinate.
+       * @param {number} pixelRatio Pixel ratio.
+       * @param {ol.proj.Projection} projection Projection.
+       * @return {string|undefined} Tile URL.
+       */
+      function(tileCoord, pixelRatio, projection) {
+        if (!tileCoord) {
+          return undefined;
+        } else {
+          return template.replace(zRegEx, tileCoord[0].toString())
+              .replace(xRegEx, tileCoord[1].toString())
+              .replace(yRegEx, function() {
+                var y = -tileCoord[2] - 1;
+                return y.toString();
+              })
+              .replace(dashYRegEx, function() {
+                var z = tileCoord[0];
+                var range = tileGrid.getFullTileRange(z);
+                goog.asserts.assert(range,
+                    'The {-y} template requires a tile grid with extent');
+                var y = range.getHeight() + tileCoord[2];
+                return y.toString();
+              });
+        }
+      });
+};
+
+
+/**
+ * @param {Array.<string>} templates Templates.
+ * @param {ol.tilegrid.TileGrid} tileGrid Tile grid.
+ * @return {ol.TileUrlFunctionType} Tile URL function.
+ */
+ol.TileUrlFunction.createFromTemplates = function(templates, tileGrid) {
+  var len = templates.length;
+  var tileUrlFunctions = new Array(len);
+  for (var i = 0; i < len; ++i) {
+    tileUrlFunctions[i] = ol.TileUrlFunction.createFromTemplate(
+        templates[i], tileGrid);
+  }
+  return ol.TileUrlFunction.createFromTileUrlFunctions(tileUrlFunctions);
+};
+
+
+/**
+ * @param {Array.<ol.TileUrlFunctionType>} tileUrlFunctions Tile URL Functions.
+ * @return {ol.TileUrlFunctionType} Tile URL function.
+ */
+ol.TileUrlFunction.createFromTileUrlFunctions = function(tileUrlFunctions) {
+  goog.asserts.assert(tileUrlFunctions.length > 0,
+      'Length of tile url functions should be greater than 0');
+  if (tileUrlFunctions.length === 1) {
+    return tileUrlFunctions[0];
+  }
+  return (
+      /**
+       * @param {ol.TileCoord} tileCoord Tile Coordinate.
+       * @param {number} pixelRatio Pixel ratio.
+       * @param {ol.proj.Projection} projection Projection.
+       * @return {string|undefined} Tile URL.
+       */
+      function(tileCoord, pixelRatio, projection) {
+        if (!tileCoord) {
+          return undefined;
+        } else {
+          var h = ol.tilecoord.hash(tileCoord);
+          var index = ol.math.modulo(h, tileUrlFunctions.length);
+          return tileUrlFunctions[index](tileCoord, pixelRatio, projection);
+        }
+      });
+};
+
+
+/**
+ * @param {ol.TileCoord} tileCoord Tile coordinate.
+ * @param {number} pixelRatio Pixel ratio.
+ * @param {ol.proj.Projection} projection Projection.
+ * @return {string|undefined} Tile URL.
+ */
+ol.TileUrlFunction.nullTileUrlFunction = function(tileCoord, pixelRatio, projection) {
+  return undefined;
+};
+
+
+/**
+ * @param {string} url URL.
+ * @return {Array.<string>} Array of urls.
+ */
+ol.TileUrlFunction.expandUrl = function(url) {
+  var urls = [];
+  var match = /\{(\d)-(\d)\}/.exec(url) || /\{([a-z])-([a-z])\}/.exec(url);
+  if (match) {
+    var startCharCode = match[1].charCodeAt(0);
+    var stopCharCode = match[2].charCodeAt(0);
+    var charCode;
+    for (charCode = startCharCode; charCode <= stopCharCode; ++charCode) {
+      urls.push(url.replace(match[0], String.fromCharCode(charCode)));
+    }
+  } else {
+    urls.push(url);
+  }
+  return urls;
+};
+
+goog.provide('ol.source.UrlTile');
+
+goog.require('ol.events');
+goog.require('ol.TileState');
+goog.require('ol.TileUrlFunction');
+goog.require('ol.source.Tile');
+goog.require('ol.source.TileEvent');
+
+
+/**
+ * @classdesc
+ * Base class for sources providing tiles divided into a tile grid over http.
+ *
+ * @constructor
+ * @fires ol.source.TileEvent
+ * @extends {ol.source.Tile}
+ * @param {ol.SourceUrlTileOptions} options Image tile options.
+ */
+ol.source.UrlTile = function(options) {
+
+  ol.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 {ol.TileLoadFunctionType}
+   */
+  this.tileLoadFunction = options.tileLoadFunction;
+
+  /**
+   * @protected
+   * @type {ol.TileUrlFunctionType}
+   */
+  this.tileUrlFunction = this.fixedTileUrlFunction ?
+      this.fixedTileUrlFunction.bind(this) :
+      ol.TileUrlFunction.nullTileUrlFunction;
+
+  /**
+   * @protected
+   * @type {!Array.<string>|null}
+   */
+  this.urls = null;
+
+  if (options.urls) {
+    this.setUrls(options.urls);
+  } else if (options.url) {
+    this.setUrl(options.url);
+  }
+  if (options.tileUrlFunction) {
+    this.setTileUrlFunction(options.tileUrlFunction);
+  }
+
+};
+ol.inherits(ol.source.UrlTile, ol.source.Tile);
+
+
+/**
+ * @type {ol.TileUrlFunctionType|undefined}
+ * @protected
+ */
+ol.source.UrlTile.prototype.fixedTileUrlFunction;
+
+/**
+ * Return the tile load function of the source.
+ * @return {ol.TileLoadFunctionType} TileLoadFunction
+ * @api
+ */
+ol.source.UrlTile.prototype.getTileLoadFunction = function() {
+  return this.tileLoadFunction;
+};
+
+
+/**
+ * Return the tile URL function of the source.
+ * @return {ol.TileUrlFunctionType} TileUrlFunction
+ * @api
+ */
+ol.source.UrlTile.prototype.getTileUrlFunction = function() {
+  return this.tileUrlFunction;
+};
+
+
+/**
+ * Return the URLs used for this source.
+ * When a tileUrlFunction is used instead of url or urls,
+ * null will be returned.
+ * @return {!Array.<string>|null} URLs.
+ * @api
+ */
+ol.source.UrlTile.prototype.getUrls = function() {
+  return this.urls;
+};
+
+
+/**
+ * Handle tile change events.
+ * @param {ol.events.Event} event Event.
+ * @protected
+ */
+ol.source.UrlTile.prototype.handleTileChange = function(event) {
+  var tile = /** @type {ol.Tile} */ (event.target);
+  switch (tile.getState()) {
+    case ol.TileState.LOADING:
+      this.dispatchEvent(
+          new ol.source.TileEvent(ol.source.TileEventType.TILELOADSTART, tile));
+      break;
+    case ol.TileState.LOADED:
+      this.dispatchEvent(
+          new ol.source.TileEvent(ol.source.TileEventType.TILELOADEND, tile));
+      break;
+    case ol.TileState.ERROR:
+      this.dispatchEvent(
+          new ol.source.TileEvent(ol.source.TileEventType.TILELOADERROR, tile));
+      break;
+    default:
+      // pass
+  }
+};
+
+
+/**
+ * Set the tile load function of the source.
+ * @param {ol.TileLoadFunctionType} tileLoadFunction Tile load function.
+ * @api
+ */
+ol.source.UrlTile.prototype.setTileLoadFunction = function(tileLoadFunction) {
+  this.tileCache.clear();
+  this.tileLoadFunction = tileLoadFunction;
+  this.changed();
+};
+
+
+/**
+ * Set the tile URL function of the source.
+ * @param {ol.TileUrlFunctionType} tileUrlFunction Tile URL function.
+ * @param {string=} opt_key Optional new tile key for the source.
+ * @api
+ */
+ol.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 stable
+ */
+ol.source.UrlTile.prototype.setUrl = function(url) {
+  var urls = this.urls = ol.TileUrlFunction.expandUrl(url);
+  this.setTileUrlFunction(this.fixedTileUrlFunction ?
+      this.fixedTileUrlFunction.bind(this) :
+      ol.TileUrlFunction.createFromTemplates(urls, this.tileGrid), url);
+};
+
+
+/**
+ * Set the URLs to use for requests.
+ * @param {Array.<string>} urls URLs.
+ * @api stable
+ */
+ol.source.UrlTile.prototype.setUrls = function(urls) {
+  this.urls = urls;
+  var key = urls.join('\n');
+  this.setTileUrlFunction(this.fixedTileUrlFunction ?
+      this.fixedTileUrlFunction.bind(this) :
+      ol.TileUrlFunction.createFromTemplates(urls, this.tileGrid), key);
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.source.UrlTile.prototype.useTile = function(z, x, y) {
+  var tileCoordKey = this.getKeyZXY(z, x, y);
+  if (this.tileCache.containsKey(tileCoordKey)) {
+    this.tileCache.get(tileCoordKey);
+  }
+};
+
+goog.provide('ol.source.VectorTile');
+
+goog.require('ol.TileState');
+goog.require('ol.VectorTile');
+goog.require('ol.events');
+goog.require('ol.events.EventType');
+goog.require('ol.featureloader');
+goog.require('ol.size');
+goog.require('ol.source.UrlTile');
+
+
+/**
+ * @classdesc
+ * Class for layer sources providing vector data divided into a tile grid, to be
+ * used with {@link ol.layer.VectorTile}. Although this source receives tiles
+ * with vector features from the server, it is not meant for feature editing.
+ * Features are optimized for rendering, their geometries are clipped at or near
+ * tile boundaries and simplified for a view resolution. See
+ * {@link ol.source.Vector} for vector sources that are suitable for feature
+ * editing.
+ *
+ * @constructor
+ * @fires ol.source.TileEvent
+ * @extends {ol.source.UrlTile}
+ * @param {olx.source.VectorTileOptions} options Vector tile options.
+ * @api
+ */
+ol.source.VectorTile = function(options) {
+
+  ol.source.UrlTile.call(this, {
+    attributions: options.attributions,
+    cacheSize: options.cacheSize !== undefined ? options.cacheSize : 128,
+    extent: options.extent,
+    logo: options.logo,
+    opaque: false,
+    projection: options.projection,
+    state: options.state,
+    tileGrid: options.tileGrid,
+    tileLoadFunction: options.tileLoadFunction ?
+        options.tileLoadFunction : ol.source.VectorTile.defaultTileLoadFunction,
+    tileUrlFunction: options.tileUrlFunction,
+    tilePixelRatio: options.tilePixelRatio,
+    url: options.url,
+    urls: options.urls,
+    wrapX: options.wrapX === undefined ? true : options.wrapX
+  });
+
+  /**
+   * @private
+   * @type {ol.format.Feature}
+   */
+  this.format_ = options.format ? options.format : null;
+
+  /**
+   * @protected
+   * @type {function(new: ol.VectorTile, ol.TileCoord, ol.TileState, string,
+   *        ol.format.Feature, ol.TileLoadFunctionType)}
+   */
+  this.tileClass = options.tileClass ? options.tileClass : ol.VectorTile;
+
+};
+ol.inherits(ol.source.VectorTile, ol.source.UrlTile);
+
+
+/**
+ * @inheritDoc
+ */
+ol.source.VectorTile.prototype.getTile = function(z, x, y, pixelRatio, projection) {
+  var tileCoordKey = this.getKeyZXY(z, x, y);
+  if (this.tileCache.containsKey(tileCoordKey)) {
+    return /** @type {!ol.Tile} */ (this.tileCache.get(tileCoordKey));
+  } else {
+    var tileCoord = [z, x, y];
+    var urlTileCoord = this.getTileCoordForTileUrlFunction(
+        tileCoord, projection);
+    var tileUrl = urlTileCoord ?
+        this.tileUrlFunction(urlTileCoord, pixelRatio, projection) : undefined;
+    var tile = new this.tileClass(
+        tileCoord,
+        tileUrl !== undefined ? ol.TileState.IDLE : ol.TileState.EMPTY,
+        tileUrl !== undefined ? tileUrl : '',
+        this.format_, this.tileLoadFunction);
+    ol.events.listen(tile, ol.events.EventType.CHANGE,
+        this.handleTileChange, this);
+
+    this.tileCache.set(tileCoordKey, tile);
+    return tile;
+  }
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.source.VectorTile.prototype.getTilePixelSize = function(z, pixelRatio, projection) {
+  var tileSize = ol.size.toSize(this.tileGrid.getTileSize(z));
+  return [tileSize[0] * pixelRatio, tileSize[1] * pixelRatio];
+};
+
+
+/**
+ * @param {ol.VectorTile} vectorTile Vector tile.
+ * @param {string} url URL.
+ */
+ol.source.VectorTile.defaultTileLoadFunction = function(vectorTile, url) {
+  vectorTile.setLoader(ol.featureloader.tile(url, vectorTile.getFormat()));
+};
+
+goog.provide('ol.renderer.canvas.VectorTileLayer');
+
+goog.require('goog.asserts');
+goog.require('ol.events');
+goog.require('goog.vec.Mat4');
+goog.require('ol.Feature');
+goog.require('ol.VectorTile');
+goog.require('ol.array');
+goog.require('ol.extent');
+goog.require('ol.layer.VectorTile');
+goog.require('ol.proj');
+goog.require('ol.proj.Units');
+goog.require('ol.render.EventType');
+goog.require('ol.render.canvas');
+goog.require('ol.render.canvas.ReplayGroup');
+goog.require('ol.renderer.canvas.TileLayer');
+goog.require('ol.renderer.vector');
+goog.require('ol.size');
+goog.require('ol.source.VectorTile');
+goog.require('ol.vec.Mat4');
+
+
+/**
+ * @const
+ * @type {!Object.<string, Array.<ol.render.ReplayType>>}
+ */
+ol.renderer.canvas.IMAGE_REPLAYS = {
+  'image': ol.render.REPLAY_ORDER,
+  'hybrid': [ol.render.ReplayType.POLYGON, ol.render.ReplayType.LINE_STRING]
+};
+
+
+/**
+ * @const
+ * @type {!Object.<string, Array.<ol.render.ReplayType>>}
+ */
+ol.renderer.canvas.VECTOR_REPLAYS = {
+  'hybrid': [ol.render.ReplayType.IMAGE, ol.render.ReplayType.TEXT],
+  'vector': ol.render.REPLAY_ORDER
+};
+
+
+/**
+ * @constructor
+ * @extends {ol.renderer.canvas.TileLayer}
+ * @param {ol.layer.VectorTile} layer VectorTile layer.
+ */
+ol.renderer.canvas.VectorTileLayer = function(layer) {
+
+  ol.renderer.canvas.TileLayer.call(this, layer);
+
+  /**
+   * @private
+   * @type {boolean}
+   */
+  this.dirty_ = false;
+
+  /**
+   * @private
+   * @type {!goog.vec.Mat4.Number}
+   */
+  this.tmpTransform_ = goog.vec.Mat4.createNumber();
+
+  // Use lower resolution for pure vector rendering. Closest resolution otherwise.
+  this.zDirection =
+      layer.getRenderMode() == ol.layer.VectorTileRenderType.VECTOR ? 1 : 0;
+
+};
+ol.inherits(ol.renderer.canvas.VectorTileLayer, ol.renderer.canvas.TileLayer);
+
+
+/**
+ * @inheritDoc
+ */
+ol.renderer.canvas.VectorTileLayer.prototype.composeFrame = function(
+    frameState, layerState, context) {
+  var transform = this.getTransform(frameState, 0);
+  this.dispatchPreComposeEvent(context, frameState, transform);
+  var renderMode = this.getLayer().getRenderMode();
+  if (renderMode !== ol.layer.VectorTileRenderType.VECTOR) {
+    this.renderTileImages(context, frameState, layerState);
+  }
+  if (renderMode !== ol.layer.VectorTileRenderType.IMAGE) {
+    this.renderTileReplays_(context, frameState, layerState);
+  }
+  this.dispatchPostComposeEvent(context, frameState, transform);
+};
+
+
+/**
+ * @param {CanvasRenderingContext2D} context Context.
+ * @param {olx.FrameState} frameState Frame state.
+ * @param {ol.LayerState} layerState Layer state.
+ * @private
+ */
+ol.renderer.canvas.VectorTileLayer.prototype.renderTileReplays_ = function(
+    context, frameState, layerState) {
+
+  var layer = this.getLayer();
+  var replays = ol.renderer.canvas.VECTOR_REPLAYS[layer.getRenderMode()];
+  var pixelRatio = frameState.pixelRatio;
+  var skippedFeatureUids = layerState.managed ?
+      frameState.skippedFeatureUids : {};
+  var viewState = frameState.viewState;
+  var center = viewState.center;
+  var resolution = viewState.resolution;
+  var rotation = viewState.rotation;
+  var size = frameState.size;
+  var pixelScale = pixelRatio / resolution;
+  var source = layer.getSource();
+  goog.asserts.assertInstanceof(source, ol.source.VectorTile,
+      'Source is an ol.source.VectorTile');
+  var tilePixelRatio = source.getTilePixelRatio(pixelRatio);
+
+  var transform = this.getTransform(frameState, 0);
+
+  var replayContext;
+  if (layer.hasListener(ol.render.EventType.RENDER)) {
+    // resize and clear
+    this.context.canvas.width = context.canvas.width;
+    this.context.canvas.height = context.canvas.height;
+    replayContext = this.context;
+  } else {
+    replayContext = context;
+  }
+  // for performance reasons, context.save / context.restore is not used
+  // to save and restore the transformation matrix and the opacity.
+  // see http://jsperf.com/context-save-restore-versus-variable
+  var alpha = replayContext.globalAlpha;
+  replayContext.globalAlpha = layerState.opacity;
+
+  var tilesToDraw = this.renderedTiles;
+  var tileGrid = source.getTileGrid();
+
+  var currentZ, i, ii, offsetX, offsetY, origin, pixelSpace, replayState;
+  var tile, tileExtent, tilePixelResolution, tileResolution, tileTransform;
+  for (i = 0, ii = tilesToDraw.length; i < ii; ++i) {
+    tile = tilesToDraw[i];
+    replayState = tile.getReplayState();
+    tileExtent = tileGrid.getTileCoordExtent(
+        tile.getTileCoord(), this.tmpExtent);
+    currentZ = tile.getTileCoord()[0];
+    pixelSpace = tile.getProjection().getUnits() == ol.proj.Units.TILE_PIXELS;
+    tileResolution = tileGrid.getResolution(currentZ);
+    tilePixelResolution = tileResolution / tilePixelRatio;
+    offsetX = Math.round(pixelRatio * size[0] / 2);
+    offsetY = Math.round(pixelRatio * size[1] / 2);
+
+    if (pixelSpace) {
+      origin = ol.extent.getTopLeft(tileExtent);
+      tileTransform = ol.vec.Mat4.makeTransform2D(this.tmpTransform_,
+          offsetX, offsetY,
+          pixelScale * tilePixelResolution,
+          pixelScale * tilePixelResolution,
+          rotation,
+          (origin[0] - center[0]) / tilePixelResolution,
+          (center[1] - origin[1]) / tilePixelResolution);
+    } else {
+      tileTransform = transform;
+    }
+    ol.render.canvas.rotateAtOffset(replayContext, -rotation, offsetX, offsetY);
+    replayState.replayGroup.replay(replayContext, pixelRatio,
+        tileTransform, rotation, skippedFeatureUids, replays);
+    ol.render.canvas.rotateAtOffset(replayContext, rotation, offsetX, offsetY);
+  }
+
+  if (replayContext != context) {
+    this.dispatchRenderEvent(replayContext, frameState, transform);
+    context.drawImage(replayContext.canvas, 0, 0);
+  }
+  replayContext.globalAlpha = alpha;
+};
+
+
+/**
+ * @param {ol.VectorTile} tile Tile.
+ * @param {olx.FrameState} frameState Frame state.
+ */
+ol.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 = layer.getRenderOrder() || null;
+
+  var replayState = tile.getReplayState();
+  if (!replayState.dirty && replayState.renderedRevision == revision &&
+      replayState.renderedRenderOrder == renderOrder) {
+    return;
+  }
+
+  replayState.replayGroup = null;
+  replayState.dirty = false;
+
+  var source = layer.getSource();
+  goog.asserts.assertInstanceof(source, ol.source.VectorTile,
+      'Source is an ol.source.VectorTile');
+  var tileGrid = source.getTileGrid();
+  var tileCoord = tile.getTileCoord();
+  var tileProjection = tile.getProjection();
+  var pixelSpace = tileProjection.getUnits() == ol.proj.Units.TILE_PIXELS;
+  var resolution = tileGrid.getResolution(tileCoord[0]);
+  var extent, reproject, tileResolution;
+  if (pixelSpace) {
+    var tilePixelRatio = tileResolution = source.getTilePixelRatio(pixelRatio);
+    var tileSize = ol.size.toSize(tileGrid.getTileSize(tileCoord[0]));
+    extent = [0, 0, tileSize[0] * tilePixelRatio, tileSize[1] * tilePixelRatio];
+  } else {
+    tileResolution = resolution;
+    extent = tileGrid.getTileCoordExtent(tileCoord);
+    if (!ol.proj.equivalent(projection, tileProjection)) {
+      reproject = true;
+      tile.setProjection(projection);
+    }
+  }
+  replayState.dirty = false;
+  var replayGroup = new ol.render.canvas.ReplayGroup(0, extent,
+      tileResolution, layer.getRenderBuffer());
+  var squaredTolerance = ol.renderer.vector.getSquaredTolerance(
+      tileResolution, pixelRatio);
+
+  /**
+   * @param {ol.Feature|ol.render.Feature} feature Feature.
+   * @this {ol.renderer.canvas.VectorTileLayer}
+   */
+  function renderFeature(feature) {
+    var styles;
+    var styleFunction = feature.getStyleFunction();
+    if (styleFunction) {
+      goog.asserts.assertInstanceof(feature, ol.Feature, 'Got an ol.Feature');
+      styles = styleFunction.call(feature, resolution);
+    } else {
+      styleFunction = layer.getStyleFunction();
+      if (styleFunction) {
+        styles = styleFunction(feature, resolution);
+      }
+    }
+    if (styles) {
+      if (!Array.isArray(styles)) {
+        styles = [styles];
+      }
+      var dirty = this.renderFeature(feature, squaredTolerance, styles,
+          replayGroup);
+      this.dirty_ = this.dirty_ || dirty;
+      replayState.dirty = replayState.dirty || dirty;
+    }
+  }
+
+  var features = tile.getFeatures();
+  if (renderOrder && renderOrder !== replayState.renderedRenderOrder) {
+    features.sort(renderOrder);
+  }
+  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();
+
+  replayState.renderedRevision = revision;
+  replayState.renderedRenderOrder = renderOrder;
+  replayState.replayGroup = replayGroup;
+  replayState.resolution = NaN;
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.renderer.canvas.VectorTileLayer.prototype.forEachFeatureAtCoordinate = function(coordinate, frameState, callback, thisArg) {
+  var pixelRatio = frameState.pixelRatio;
+  var resolution = frameState.viewState.resolution;
+  var rotation = frameState.viewState.rotation;
+  var layer = this.getLayer();
+  /** @type {Object.<string, boolean>} */
+  var features = {};
+
+  var replayables = this.renderedTiles;
+  var source = layer.getSource();
+  goog.asserts.assertInstanceof(source, ol.source.VectorTile,
+      'Source is an ol.source.VectorTile');
+  var tileGrid = source.getTileGrid();
+  var found, tileSpaceCoordinate;
+  var i, ii, origin, replayGroup;
+  var tile, tileCoord, tileExtent, tilePixelRatio, tileResolution;
+  for (i = 0, ii = replayables.length; i < ii; ++i) {
+    tile = replayables[i];
+    tileCoord = tile.getTileCoord();
+    tileExtent = source.getTileGrid().getTileCoordExtent(tileCoord,
+        this.tmpExtent);
+    if (!ol.extent.containsCoordinate(tileExtent, coordinate)) {
+      continue;
+    }
+    if (tile.getProjection().getUnits() === ol.proj.Units.TILE_PIXELS) {
+      origin = ol.extent.getTopLeft(tileExtent);
+      tilePixelRatio = source.getTilePixelRatio(pixelRatio);
+      tileResolution = tileGrid.getResolution(tileCoord[0]) / tilePixelRatio;
+      tileSpaceCoordinate = [
+        (coordinate[0] - origin[0]) / tileResolution,
+        (origin[1] - coordinate[1]) / tileResolution
+      ];
+      resolution = tilePixelRatio;
+    } else {
+      tileSpaceCoordinate = coordinate;
+    }
+    replayGroup = tile.getReplayState().replayGroup;
+    found = found || replayGroup.forEachFeatureAtCoordinate(
+        tileSpaceCoordinate, resolution, rotation, {},
+        /**
+         * @param {ol.Feature|ol.render.Feature} feature Feature.
+         * @return {?} Callback result.
+         */
+        function(feature) {
+          goog.asserts.assert(feature, 'received a feature');
+          var key = goog.getUid(feature).toString();
+          if (!(key in features)) {
+            features[key] = true;
+            return callback.call(thisArg, feature, layer);
+          }
+        });
+  }
+  return found;
+};
+
+
+/**
+ * Handle changes in image style state.
+ * @param {ol.events.Event} event Image style change event.
+ * @private
+ */
+ol.renderer.canvas.VectorTileLayer.prototype.handleStyleImageChange_ = function(event) {
+  this.renderIfReadyAndVisible();
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.renderer.canvas.VectorTileLayer.prototype.prepareFrame = function(frameState, layerState) {
+  var prepared = ol.renderer.canvas.TileLayer.prototype.prepareFrame.call(this, frameState, layerState);
+  if (prepared) {
+    var skippedFeatures = Object.keys(frameState.skippedFeatureUids_ || {});
+    for (var i = 0, ii = this.renderedTiles.length; i < ii; ++i) {
+      var tile = this.renderedTiles[i];
+      goog.asserts.assertInstanceof(tile, ol.VectorTile, 'got an ol.VectorTile');
+      this.createReplayGroup(tile, frameState);
+      this.renderTileImage_(tile, frameState, layerState, skippedFeatures);
+    }
+  }
+  return prepared;
+};
+
+
+/**
+ * @param {ol.Feature|ol.render.Feature} feature Feature.
+ * @param {number} squaredTolerance Squared tolerance.
+ * @param {(ol.style.Style|Array.<ol.style.Style>)} styles The style or array of
+ *     styles.
+ * @param {ol.render.canvas.ReplayGroup} replayGroup Replay group.
+ * @return {boolean} `true` if an image is loading.
+ */
+ol.renderer.canvas.VectorTileLayer.prototype.renderFeature = function(feature, squaredTolerance, styles, replayGroup) {
+  if (!styles) {
+    return false;
+  }
+  var loading = false;
+  if (Array.isArray(styles)) {
+    for (var i = 0, ii = styles.length; i < ii; ++i) {
+      loading = ol.renderer.vector.renderFeature(
+          replayGroup, feature, styles[i], squaredTolerance,
+          this.handleStyleImageChange_, this) || loading;
+    }
+  } else {
+    loading = ol.renderer.vector.renderFeature(
+        replayGroup, feature, styles, squaredTolerance,
+        this.handleStyleImageChange_, this) || loading;
+  }
+  return loading;
+};
+
+
+/**
+ * @param {ol.VectorTile} tile Tile.
+ * @param {olx.FrameState} frameState Frame state.
+ * @param {ol.LayerState} layerState Layer state.
+ * @param {Array.<string>} skippedFeatures Skipped features.
+ * @private
+ */
+ol.renderer.canvas.VectorTileLayer.prototype.renderTileImage_ = function(
+    tile, frameState, layerState, skippedFeatures) {
+  var layer = this.getLayer();
+  var replays = ol.renderer.canvas.IMAGE_REPLAYS[layer.getRenderMode()];
+  if (!replays) {
+    // do not create an image in 'vector' mode
+    return;
+  }
+  var pixelRatio = frameState.pixelRatio;
+  var replayState = tile.getReplayState();
+  var revision = layer.getRevision();
+  if (!ol.array.equals(replayState.skippedFeatures, skippedFeatures) ||
+      replayState.renderedTileRevision !== revision) {
+    replayState.skippedFeatures = skippedFeatures;
+    replayState.renderedTileRevision = revision;
+    var tileContext = tile.getContext();
+    var source = layer.getSource();
+    var tileGrid = source.getTileGrid();
+    var currentZ = tile.getTileCoord()[0];
+    var resolution = tileGrid.getResolution(currentZ);
+    var tileSize = ol.size.toSize(tileGrid.getTileSize(currentZ));
+    var tileResolution = tileGrid.getResolution(currentZ);
+    var resolutionRatio = tileResolution / resolution;
+    var width = tileSize[0] * pixelRatio * resolutionRatio;
+    var height = tileSize[1] * pixelRatio * resolutionRatio;
+    tileContext.canvas.width = width / resolutionRatio + 0.5;
+    tileContext.canvas.height = height / resolutionRatio + 0.5;
+    tileContext.scale(1 / resolutionRatio, 1 / resolutionRatio);
+    tileContext.translate(width / 2, height / 2);
+    var pixelSpace = tile.getProjection().getUnits() == ol.proj.Units.TILE_PIXELS;
+    var pixelScale = pixelRatio / resolution;
+    var tilePixelRatio = source.getTilePixelRatio(pixelRatio);
+    var tilePixelResolution = tileResolution / tilePixelRatio;
+    var tileExtent = tileGrid.getTileCoordExtent(
+        tile.getTileCoord(), this.tmpExtent);
+    var tileTransform;
+    if (pixelSpace) {
+      tileTransform = ol.vec.Mat4.makeTransform2D(this.tmpTransform_,
+          0, 0,
+          pixelScale * tilePixelResolution, pixelScale * tilePixelResolution,
+          0,
+          -tileSize[0] * tilePixelRatio / 2, -tileSize[1] * tilePixelRatio / 2);
+    } else {
+      var tileCenter = ol.extent.getCenter(tileExtent);
+      tileTransform = ol.vec.Mat4.makeTransform2D(this.tmpTransform_,
+          0, 0,
+          pixelScale, -pixelScale,
+          0,
+          -tileCenter[0], -tileCenter[1]);
+    }
+
+    replayState.replayGroup.replay(tileContext, pixelRatio,
+        tileTransform, 0, frameState.skippedFeatureUids || {}, replays);
+  }
+};
+
+// FIXME offset panning
+
+goog.provide('ol.renderer.canvas.Map');
+
+goog.require('goog.asserts');
+goog.require('goog.vec.Mat4');
+goog.require('ol');
+goog.require('ol.RendererType');
+goog.require('ol.array');
+goog.require('ol.css');
+goog.require('ol.dom');
+goog.require('ol.layer.Image');
+goog.require('ol.layer.Layer');
+goog.require('ol.layer.Tile');
+goog.require('ol.layer.Vector');
+goog.require('ol.layer.VectorTile');
+goog.require('ol.render.Event');
+goog.require('ol.render.EventType');
+goog.require('ol.render.canvas');
+goog.require('ol.render.canvas.Immediate');
+goog.require('ol.renderer.Map');
+goog.require('ol.renderer.canvas.ImageLayer');
+goog.require('ol.renderer.canvas.Layer');
+goog.require('ol.renderer.canvas.TileLayer');
+goog.require('ol.renderer.canvas.VectorLayer');
+goog.require('ol.renderer.canvas.VectorTileLayer');
+goog.require('ol.source.State');
+goog.require('ol.vec.Mat4');
+
+
+/**
+ * @constructor
+ * @extends {ol.renderer.Map}
+ * @param {Element} container Container.
+ * @param {ol.Map} map Map.
+ */
+ol.renderer.canvas.Map = function(container, map) {
+
+  ol.renderer.Map.call(this, container, map);
+
+  /**
+   * @private
+   * @type {CanvasRenderingContext2D}
+   */
+  this.context_ = ol.dom.createCanvasContext2D();
+
+  /**
+   * @private
+   * @type {HTMLCanvasElement}
+   */
+  this.canvas_ = this.context_.canvas;
+
+  this.canvas_.style.width = '100%';
+  this.canvas_.style.height = '100%';
+  this.canvas_.className = ol.css.CLASS_UNSELECTABLE;
+  container.insertBefore(this.canvas_, container.childNodes[0] || null);
+
+  /**
+   * @private
+   * @type {boolean}
+   */
+  this.renderedVisible_ = true;
+
+  /**
+   * @private
+   * @type {!goog.vec.Mat4.Number}
+   */
+  this.transform_ = goog.vec.Mat4.createNumber();
+
+};
+ol.inherits(ol.renderer.canvas.Map, ol.renderer.Map);
+
+
+/**
+ * @inheritDoc
+ */
+ol.renderer.canvas.Map.prototype.createLayerRenderer = function(layer) {
+  if (ol.ENABLE_IMAGE && layer instanceof ol.layer.Image) {
+    return new ol.renderer.canvas.ImageLayer(layer);
+  } else if (ol.ENABLE_TILE && layer instanceof ol.layer.Tile) {
+    return new ol.renderer.canvas.TileLayer(layer);
+  } else if (ol.ENABLE_VECTOR_TILE && layer instanceof ol.layer.VectorTile) {
+    return new ol.renderer.canvas.VectorTileLayer(layer);
+  } else if (ol.ENABLE_VECTOR && layer instanceof ol.layer.Vector) {
+    return new ol.renderer.canvas.VectorLayer(layer);
+  } else {
+    goog.asserts.fail('unexpected layer configuration');
+    return null;
+  }
+};
+
+
+/**
+ * @param {ol.render.EventType} type Event type.
+ * @param {olx.FrameState} frameState Frame state.
+ * @private
+ */
+ol.renderer.canvas.Map.prototype.dispatchComposeEvent_ = function(type, frameState) {
+  var map = this.getMap();
+  var context = this.context_;
+  if (map.hasListener(type)) {
+    var extent = frameState.extent;
+    var pixelRatio = frameState.pixelRatio;
+    var viewState = frameState.viewState;
+    var rotation = viewState.rotation;
+
+    var transform = this.getTransform(frameState);
+
+    var vectorContext = new ol.render.canvas.Immediate(context, pixelRatio,
+        extent, transform, rotation);
+    var composeEvent = new ol.render.Event(type, map, vectorContext,
+        frameState, context, null);
+    map.dispatchEvent(composeEvent);
+  }
+};
+
+
+/**
+ * @param {olx.FrameState} frameState Frame state.
+ * @protected
+ * @return {!goog.vec.Mat4.Number} Transform.
+ */
+ol.renderer.canvas.Map.prototype.getTransform = function(frameState) {
+  var pixelRatio = frameState.pixelRatio;
+  var viewState = frameState.viewState;
+  var resolution = viewState.resolution;
+  return ol.vec.Mat4.makeTransform2D(this.transform_,
+      this.canvas_.width / 2, this.canvas_.height / 2,
+      pixelRatio / resolution, -pixelRatio / resolution,
+      -viewState.rotation,
+      -viewState.center[0], -viewState.center[1]);
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.renderer.canvas.Map.prototype.getType = function() {
+  return ol.RendererType.CANVAS;
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.renderer.canvas.Map.prototype.renderFrame = function(frameState) {
+
+  if (!frameState) {
+    if (this.renderedVisible_) {
+      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_(ol.render.EventType.PRECOMPOSE, frameState);
+
+  var layerStatesArray = frameState.layerStatesArray;
+  ol.array.stableSort(layerStatesArray, ol.renderer.Map.sortByZIndex);
+
+  ol.render.canvas.rotateAtOffset(context, rotation, width / 2, height / 2);
+
+  var viewResolution = frameState.viewState.resolution;
+  var i, ii, layer, layerRenderer, layerState;
+  for (i = 0, ii = layerStatesArray.length; i < ii; ++i) {
+    layerState = layerStatesArray[i];
+    layer = layerState.layer;
+    layerRenderer = this.getLayerRenderer(layer);
+    goog.asserts.assertInstanceof(layerRenderer, ol.renderer.canvas.Layer,
+        'layerRenderer is an instance of ol.renderer.canvas.Layer');
+    if (!ol.layer.Layer.visibleAtResolution(layerState, viewResolution) ||
+        layerState.sourceState != ol.source.State.READY) {
+      continue;
+    }
+    if (layerRenderer.prepareFrame(frameState, layerState)) {
+      layerRenderer.composeFrame(frameState, layerState, context);
+    }
+  }
+
+  ol.render.canvas.rotateAtOffset(context, -rotation, width / 2, height / 2);
+
+  this.dispatchComposeEvent_(
+      ol.render.EventType.POSTCOMPOSE, frameState);
+
+  if (!this.renderedVisible_) {
+    this.canvas_.style.display = '';
+    this.renderedVisible_ = true;
+  }
+
+  this.scheduleRemoveUnusedLayerRenderers(frameState);
+  this.scheduleExpireIconCache(frameState);
+};
+
+goog.provide('ol.renderer.dom.Layer');
+
+goog.require('ol');
+goog.require('ol.layer.Layer');
+goog.require('ol.renderer.Layer');
+
+
+/**
+ * @constructor
+ * @extends {ol.renderer.Layer}
+ * @param {ol.layer.Layer} layer Layer.
+ * @param {!Element} target Target.
+ */
+ol.renderer.dom.Layer = function(layer, target) {
+
+  ol.renderer.Layer.call(this, layer);
+
+  /**
+   * @type {!Element}
+   * @protected
+   */
+  this.target = target;
+
+};
+ol.inherits(ol.renderer.dom.Layer, ol.renderer.Layer);
+
+
+/**
+ * Clear rendered elements.
+ */
+ol.renderer.dom.Layer.prototype.clearFrame = ol.nullFunction;
+
+
+/**
+ * @param {olx.FrameState} frameState Frame state.
+ * @param {ol.LayerState} layerState Layer state.
+ */
+ol.renderer.dom.Layer.prototype.composeFrame = ol.nullFunction;
+
+
+/**
+ * @return {!Element} Target.
+ */
+ol.renderer.dom.Layer.prototype.getTarget = function() {
+  return this.target;
+};
+
+
+/**
+ * @param {olx.FrameState} frameState Frame state.
+ * @param {ol.LayerState} layerState Layer state.
+ * @return {boolean} whether composeFrame should be called.
+ */
+ol.renderer.dom.Layer.prototype.prepareFrame = goog.abstractMethod;
+
+goog.provide('ol.renderer.dom.ImageLayer');
+
+goog.require('goog.asserts');
+goog.require('goog.vec.Mat4');
+goog.require('ol.ImageBase');
+goog.require('ol.ViewHint');
+goog.require('ol.dom');
+goog.require('ol.extent');
+goog.require('ol.layer.Image');
+goog.require('ol.proj');
+goog.require('ol.renderer.dom.Layer');
+goog.require('ol.vec.Mat4');
+
+
+/**
+ * @constructor
+ * @extends {ol.renderer.dom.Layer}
+ * @param {ol.layer.Image} imageLayer Image layer.
+ */
+ol.renderer.dom.ImageLayer = function(imageLayer) {
+  var target = document.createElement('DIV');
+  target.style.position = 'absolute';
+
+  ol.renderer.dom.Layer.call(this, imageLayer, target);
+
+  /**
+   * The last rendered image.
+   * @private
+   * @type {?ol.ImageBase}
+   */
+  this.image_ = null;
+
+  /**
+   * @private
+   * @type {goog.vec.Mat4.Number}
+   */
+  this.transform_ = goog.vec.Mat4.createNumberIdentity();
+
+};
+ol.inherits(ol.renderer.dom.ImageLayer, ol.renderer.dom.Layer);
+
+
+/**
+ * @inheritDoc
+ */
+ol.renderer.dom.ImageLayer.prototype.forEachFeatureAtCoordinate = function(coordinate, frameState, callback, thisArg) {
+  var layer = this.getLayer();
+  var source = layer.getSource();
+  var resolution = frameState.viewState.resolution;
+  var rotation = frameState.viewState.rotation;
+  var skippedFeatureUids = frameState.skippedFeatureUids;
+  return source.forEachFeatureAtCoordinate(
+      coordinate, resolution, rotation, skippedFeatureUids,
+      /**
+       * @param {ol.Feature|ol.render.Feature} feature Feature.
+       * @return {?} Callback result.
+       */
+      function(feature) {
+        return callback.call(thisArg, feature, layer);
+      });
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.renderer.dom.ImageLayer.prototype.clearFrame = function() {
+  ol.dom.removeChildren(this.target);
+  this.image_ = null;
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.renderer.dom.ImageLayer.prototype.prepareFrame = function(frameState, layerState) {
+
+  var viewState = frameState.viewState;
+  var viewCenter = viewState.center;
+  var viewResolution = viewState.resolution;
+  var viewRotation = viewState.rotation;
+
+  var image = this.image_;
+  var imageLayer = this.getLayer();
+  goog.asserts.assertInstanceof(imageLayer, ol.layer.Image,
+      'layer is an instance of ol.layer.Image');
+  var imageSource = imageLayer.getSource();
+
+  var hints = frameState.viewHints;
+
+  var renderedExtent = frameState.extent;
+  if (layerState.extent !== undefined) {
+    renderedExtent = ol.extent.getIntersection(
+        renderedExtent, layerState.extent);
+  }
+
+  if (!hints[ol.ViewHint.ANIMATING] && !hints[ol.ViewHint.INTERACTING] &&
+      !ol.extent.isEmpty(renderedExtent)) {
+    var projection = viewState.projection;
+    if (!ol.ENABLE_RASTER_REPROJECTION) {
+      var sourceProjection = imageSource.getProjection();
+      if (sourceProjection) {
+        goog.asserts.assert(ol.proj.equivalent(projection, sourceProjection),
+            'projection and sourceProjection are equivalent');
+        projection = sourceProjection;
+      }
+    }
+    var image_ = imageSource.getImage(renderedExtent, viewResolution,
+        frameState.pixelRatio, projection);
+    if (image_) {
+      var loaded = this.loadImage(image_);
+      if (loaded) {
+        image = image_;
+      }
+    }
+  }
+
+  if (image) {
+    var imageExtent = image.getExtent();
+    var imageResolution = image.getResolution();
+    var transform = goog.vec.Mat4.createNumber();
+    ol.vec.Mat4.makeTransform2D(transform,
+        frameState.size[0] / 2, frameState.size[1] / 2,
+        imageResolution / viewResolution, imageResolution / viewResolution,
+        viewRotation,
+        (imageExtent[0] - viewCenter[0]) / imageResolution,
+        (viewCenter[1] - imageExtent[3]) / imageResolution);
+    if (image != this.image_) {
+      var imageElement = image.getImage(this);
+      // Bootstrap sets the style max-width: 100% for all images, which breaks
+      // prevents the image from being displayed in FireFox.  Workaround by
+      // overriding the max-width style.
+      imageElement.style.maxWidth = 'none';
+      imageElement.style.position = 'absolute';
+      ol.dom.removeChildren(this.target);
+      this.target.appendChild(imageElement);
+      this.image_ = image;
+    }
+    this.setTransform_(transform);
+    this.updateAttributions(frameState.attributions, image.getAttributions());
+    this.updateLogos(frameState, imageSource);
+  }
+
+  return true;
+};
+
+
+/**
+ * @param {goog.vec.Mat4.Number} transform Transform.
+ * @private
+ */
+ol.renderer.dom.ImageLayer.prototype.setTransform_ = function(transform) {
+  if (!ol.vec.Mat4.equals2D(transform, this.transform_)) {
+    ol.dom.transformElement2D(this.target, transform, 6);
+    goog.vec.Mat4.setFromArray(this.transform_, transform);
+  }
+};
+
+// FIXME probably need to reset TileLayerZ if offsets get too large
+// FIXME when zooming out, preserve higher Z divs to avoid white flash
+
+goog.provide('ol.renderer.dom.TileLayer');
+
+goog.require('goog.asserts');
+goog.require('goog.vec.Mat4');
+goog.require('ol');
+goog.require('ol.TileRange');
+goog.require('ol.TileState');
+goog.require('ol.ViewHint');
+goog.require('ol.array');
+goog.require('ol.dom');
+goog.require('ol.extent');
+goog.require('ol.layer.Tile');
+goog.require('ol.renderer.dom.Layer');
+goog.require('ol.size');
+goog.require('ol.tilegrid.TileGrid');
+goog.require('ol.vec.Mat4');
+
+
+/**
+ * @constructor
+ * @extends {ol.renderer.dom.Layer}
+ * @param {ol.layer.Tile} tileLayer Tile layer.
+ */
+ol.renderer.dom.TileLayer = function(tileLayer) {
+
+  var target = document.createElement('DIV');
+  target.style.position = 'absolute';
+
+  ol.renderer.dom.Layer.call(this, tileLayer, target);
+
+  /**
+   * @private
+   * @type {boolean}
+   */
+  this.renderedVisible_ = true;
+
+  /**
+   * @private
+   * @type {number}
+   */
+  this.renderedOpacity_ = 1;
+
+  /**
+   * @private
+   * @type {number}
+   */
+  this.renderedRevision_ = 0;
+
+  /**
+   * @private
+   * @type {!Object.<number, ol.renderer.dom.TileLayerZ_>}
+   */
+  this.tileLayerZs_ = {};
+
+};
+ol.inherits(ol.renderer.dom.TileLayer, ol.renderer.dom.Layer);
+
+
+/**
+ * @inheritDoc
+ */
+ol.renderer.dom.TileLayer.prototype.clearFrame = function() {
+  ol.dom.removeChildren(this.target);
+  this.renderedRevision_ = 0;
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.renderer.dom.TileLayer.prototype.prepareFrame = function(frameState, layerState) {
+
+  if (!layerState.visible) {
+    if (this.renderedVisible_) {
+      this.target.style.display = 'none';
+      this.renderedVisible_ = false;
+    }
+    return true;
+  }
+
+  var pixelRatio = frameState.pixelRatio;
+  var viewState = frameState.viewState;
+  var projection = viewState.projection;
+
+  var tileLayer = this.getLayer();
+  goog.asserts.assertInstanceof(tileLayer, ol.layer.Tile,
+      'layer is an instance of ol.layer.Tile');
+  var tileSource = tileLayer.getSource();
+  var tileGrid = tileSource.getTileGridForProjection(projection);
+  var tileGutter = tileSource.getGutter(projection);
+  var z = tileGrid.getZForResolution(viewState.resolution);
+  var tileResolution = tileGrid.getResolution(z);
+  var center = viewState.center;
+  var extent;
+  if (tileResolution == viewState.resolution) {
+    center = this.snapCenterToPixel(center, tileResolution, frameState.size);
+    extent = ol.extent.getForViewAndSize(
+        center, tileResolution, viewState.rotation, frameState.size);
+  } else {
+    extent = frameState.extent;
+  }
+
+  if (layerState.extent !== undefined) {
+    extent = ol.extent.getIntersection(extent, layerState.extent);
+  }
+
+  var tileRange = tileGrid.getTileRangeForExtentAndResolution(
+      extent, tileResolution);
+
+  /** @type {Object.<number, Object.<string, ol.Tile>>} */
+  var tilesToDrawByZ = {};
+  tilesToDrawByZ[z] = {};
+
+  var findLoadedTiles = this.createLoadedTileFinder(
+      tileSource, projection, tilesToDrawByZ);
+
+  var useInterimTilesOnError = tileLayer.getUseInterimTilesOnError();
+
+  var tmpExtent = ol.extent.createEmpty();
+  var tmpTileRange = new ol.TileRange(0, 0, 0, 0);
+  var childTileRange, drawable, fullyLoaded, tile, tileState, x, y;
+  for (x = tileRange.minX; x <= tileRange.maxX; ++x) {
+    for (y = tileRange.minY; y <= tileRange.maxY; ++y) {
+      tile = tileSource.getTile(z, x, y, pixelRatio, projection);
+      tileState = tile.getState();
+      drawable = tileState == ol.TileState.LOADED ||
+          tileState == ol.TileState.EMPTY ||
+          tileState == ol.TileState.ERROR && !useInterimTilesOnError;
+      if (!drawable && tile.interimTile) {
+        tile = tile.interimTile;
+      }
+      goog.asserts.assert(tile);
+      tileState = tile.getState();
+      if (tileState == ol.TileState.LOADED) {
+        tilesToDrawByZ[z][tile.tileCoord.toString()] = tile;
+        continue;
+      } else if (tileState == ol.TileState.EMPTY ||
+                 (tileState == ol.TileState.ERROR &&
+                  !useInterimTilesOnError)) {
+        continue;
+      }
+      fullyLoaded = tileGrid.forEachTileCoordParentTileRange(
+          tile.tileCoord, findLoadedTiles, null, tmpTileRange, tmpExtent);
+      if (!fullyLoaded) {
+        childTileRange = tileGrid.getTileCoordChildTileRange(
+            tile.tileCoord, tmpTileRange, tmpExtent);
+        if (childTileRange) {
+          findLoadedTiles(z + 1, childTileRange);
+        }
+      }
+
+    }
+
+  }
+
+  // If the tile source revision changes, we destroy the existing DOM structure
+  // so that a new one will be created.  It would be more efficient to modify
+  // the existing structure.
+  var tileLayerZ, tileLayerZKey;
+  if (this.renderedRevision_ != tileSource.getRevision()) {
+    for (tileLayerZKey in this.tileLayerZs_) {
+      tileLayerZ = this.tileLayerZs_[+tileLayerZKey];
+      ol.dom.removeNode(tileLayerZ.target);
+    }
+    this.tileLayerZs_ = {};
+    this.renderedRevision_ = tileSource.getRevision();
+  }
+
+  /** @type {Array.<number>} */
+  var zs = Object.keys(tilesToDrawByZ).map(Number);
+  zs.sort(ol.array.numberSafeCompareFunction);
+
+  /** @type {Object.<number, boolean>} */
+  var newTileLayerZKeys = {};
+
+  var iz, iziz, tileCoordKey, tileCoordOrigin, tilesToDraw;
+  for (iz = 0, iziz = zs.length; iz < iziz; ++iz) {
+    tileLayerZKey = zs[iz];
+    if (tileLayerZKey in this.tileLayerZs_) {
+      tileLayerZ = this.tileLayerZs_[tileLayerZKey];
+    } else {
+      tileCoordOrigin =
+          tileGrid.getTileCoordForCoordAndZ(center, tileLayerZKey);
+      tileLayerZ = new ol.renderer.dom.TileLayerZ_(tileGrid, tileCoordOrigin);
+      newTileLayerZKeys[tileLayerZKey] = true;
+      this.tileLayerZs_[tileLayerZKey] = tileLayerZ;
+    }
+    tilesToDraw = tilesToDrawByZ[tileLayerZKey];
+    for (tileCoordKey in tilesToDraw) {
+      tileLayerZ.addTile(tilesToDraw[tileCoordKey], tileGutter);
+    }
+    tileLayerZ.finalizeAddTiles();
+  }
+
+  /** @type {Array.<number>} */
+  var tileLayerZKeys = Object.keys(this.tileLayerZs_).map(Number);
+  tileLayerZKeys.sort(ol.array.numberSafeCompareFunction);
+
+  var i, ii, j, origin, resolution;
+  var transform = goog.vec.Mat4.createNumber();
+  for (i = 0, ii = tileLayerZKeys.length; i < ii; ++i) {
+    tileLayerZKey = tileLayerZKeys[i];
+    tileLayerZ = this.tileLayerZs_[tileLayerZKey];
+    if (!(tileLayerZKey in tilesToDrawByZ)) {
+      ol.dom.removeNode(tileLayerZ.target);
+      delete this.tileLayerZs_[tileLayerZKey];
+      continue;
+    }
+    resolution = tileLayerZ.getResolution();
+    origin = tileLayerZ.getOrigin();
+    ol.vec.Mat4.makeTransform2D(transform,
+        frameState.size[0] / 2, frameState.size[1] / 2,
+        resolution / viewState.resolution,
+        resolution / viewState.resolution,
+        viewState.rotation,
+        (origin[0] - center[0]) / resolution,
+        (center[1] - origin[1]) / resolution);
+    tileLayerZ.setTransform(transform);
+    if (tileLayerZKey in newTileLayerZKeys) {
+      for (j = tileLayerZKey - 1; j >= 0; --j) {
+        if (j in this.tileLayerZs_) {
+          if (this.tileLayerZs_[j].target.parentNode) {
+            this.tileLayerZs_[j].target.parentNode.insertBefore(tileLayerZ.target, this.tileLayerZs_[j].target.nextSibling);
+          }
+          break;
+        }
+      }
+      if (j < 0) {
+        this.target.insertBefore(tileLayerZ.target, this.target.childNodes[0] || null);
+      }
+    } else {
+      if (!frameState.viewHints[ol.ViewHint.ANIMATING] &&
+          !frameState.viewHints[ol.ViewHint.INTERACTING]) {
+        tileLayerZ.removeTilesOutsideExtent(extent, tmpTileRange);
+      }
+    }
+  }
+
+  if (layerState.opacity != this.renderedOpacity_) {
+    this.target.style.opacity = layerState.opacity;
+    this.renderedOpacity_ = layerState.opacity;
+  }
+
+  if (layerState.visible && !this.renderedVisible_) {
+    this.target.style.display = '';
+    this.renderedVisible_ = true;
+  }
+
+  this.updateUsedTiles(frameState.usedTiles, tileSource, z, tileRange);
+  this.manageTilePyramid(frameState, tileSource, tileGrid, pixelRatio,
+      projection, extent, z, tileLayer.getPreload());
+  this.scheduleExpireCache(frameState, tileSource);
+  this.updateLogos(frameState, tileSource);
+
+  return true;
+};
+
+
+/**
+ * @constructor
+ * @private
+ * @param {ol.tilegrid.TileGrid} tileGrid Tile grid.
+ * @param {ol.TileCoord} tileCoordOrigin Tile coord origin.
+ */
+ol.renderer.dom.TileLayerZ_ = function(tileGrid, tileCoordOrigin) {
+
+  /**
+   * @type {!Element}
+   */
+  this.target = document.createElement('DIV');
+  this.target.style.position = 'absolute';
+  this.target.style.width = '100%';
+  this.target.style.height = '100%';
+
+  /**
+   * @private
+   * @type {ol.tilegrid.TileGrid}
+   */
+  this.tileGrid_ = tileGrid;
+
+  /**
+   * @private
+   * @type {ol.TileCoord}
+   */
+  this.tileCoordOrigin_ = tileCoordOrigin;
+
+  /**
+   * @private
+   * @type {ol.Coordinate}
+   */
+  this.origin_ =
+      ol.extent.getTopLeft(tileGrid.getTileCoordExtent(tileCoordOrigin));
+
+  /**
+   * @private
+   * @type {number}
+   */
+  this.resolution_ = tileGrid.getResolution(tileCoordOrigin[0]);
+
+  /**
+   * @private
+   * @type {Object.<string, ol.Tile>}
+   */
+  this.tiles_ = {};
+
+  /**
+   * @private
+   * @type {DocumentFragment}
+   */
+  this.documentFragment_ = null;
+
+  /**
+   * @private
+   * @type {goog.vec.Mat4.Number}
+   */
+  this.transform_ = goog.vec.Mat4.createNumberIdentity();
+
+  /**
+   * @private
+   * @type {ol.Size}
+   */
+  this.tmpSize_ = [0, 0];
+
+};
+
+
+/**
+ * @param {ol.Tile} tile Tile.
+ * @param {number} tileGutter Tile gutter.
+ */
+ol.renderer.dom.TileLayerZ_.prototype.addTile = function(tile, tileGutter) {
+  var tileCoord = tile.tileCoord;
+  var tileCoordZ = tileCoord[0];
+  var tileCoordX = tileCoord[1];
+  var tileCoordY = tileCoord[2];
+  goog.asserts.assert(tileCoordZ == this.tileCoordOrigin_[0],
+      'tileCoordZ matches z of tileCoordOrigin');
+  var tileCoordKey = tileCoord.toString();
+  if (tileCoordKey in this.tiles_) {
+    return;
+  }
+  var tileSize = ol.size.toSize(
+      this.tileGrid_.getTileSize(tileCoordZ), this.tmpSize_);
+  var image = tile.getImage(this);
+  var imageStyle = image.style;
+  // Bootstrap sets the style max-width: 100% for all images, which
+  // prevents the tile from being displayed in FireFox.  Workaround
+  // by overriding the max-width style.
+  imageStyle.maxWidth = 'none';
+  var tileElement;
+  var tileElementStyle;
+  if (tileGutter > 0) {
+    tileElement = document.createElement('DIV');
+    tileElementStyle = tileElement.style;
+    tileElementStyle.overflow = 'hidden';
+    tileElementStyle.width = tileSize[0] + 'px';
+    tileElementStyle.height = tileSize[1] + 'px';
+    imageStyle.position = 'absolute';
+    imageStyle.left = -tileGutter + 'px';
+    imageStyle.top = -tileGutter + 'px';
+    imageStyle.width = (tileSize[0] + 2 * tileGutter) + 'px';
+    imageStyle.height = (tileSize[1] + 2 * tileGutter) + 'px';
+    tileElement.appendChild(image);
+  } else {
+    imageStyle.width = tileSize[0] + 'px';
+    imageStyle.height = tileSize[1] + 'px';
+    tileElement = image;
+    tileElementStyle = imageStyle;
+  }
+  tileElementStyle.position = 'absolute';
+  tileElementStyle.left =
+      ((tileCoordX - this.tileCoordOrigin_[1]) * tileSize[0]) + 'px';
+  tileElementStyle.top =
+      ((this.tileCoordOrigin_[2] - tileCoordY) * tileSize[1]) + 'px';
+  if (!this.documentFragment_) {
+    this.documentFragment_ = document.createDocumentFragment();
+  }
+  this.documentFragment_.appendChild(tileElement);
+  this.tiles_[tileCoordKey] = tile;
+};
+
+
+/**
+ * FIXME empty description for jsdoc
+ */
+ol.renderer.dom.TileLayerZ_.prototype.finalizeAddTiles = function() {
+  if (this.documentFragment_) {
+    this.target.appendChild(this.documentFragment_);
+    this.documentFragment_ = null;
+  }
+};
+
+
+/**
+ * @return {ol.Coordinate} Origin.
+ */
+ol.renderer.dom.TileLayerZ_.prototype.getOrigin = function() {
+  return this.origin_;
+};
+
+
+/**
+ * @return {number} Resolution.
+ */
+ol.renderer.dom.TileLayerZ_.prototype.getResolution = function() {
+  return this.resolution_;
+};
+
+
+/**
+ * @param {ol.Extent} extent Extent.
+ * @param {ol.TileRange=} opt_tileRange Temporary ol.TileRange object.
+ */
+ol.renderer.dom.TileLayerZ_.prototype.removeTilesOutsideExtent = function(extent, opt_tileRange) {
+  var tileRange = this.tileGrid_.getTileRangeForExtentAndZ(
+      extent, this.tileCoordOrigin_[0], opt_tileRange);
+  /** @type {Array.<ol.Tile>} */
+  var tilesToRemove = [];
+  var tile, tileCoordKey;
+  for (tileCoordKey in this.tiles_) {
+    tile = this.tiles_[tileCoordKey];
+    if (!tileRange.contains(tile.tileCoord)) {
+      tilesToRemove.push(tile);
+    }
+  }
+  var i, ii;
+  for (i = 0, ii = tilesToRemove.length; i < ii; ++i) {
+    tile = tilesToRemove[i];
+    tileCoordKey = tile.tileCoord.toString();
+    ol.dom.removeNode(tile.getImage(this));
+    delete this.tiles_[tileCoordKey];
+  }
+};
+
+
+/**
+ * @param {goog.vec.Mat4.Number} transform Transform.
+ */
+ol.renderer.dom.TileLayerZ_.prototype.setTransform = function(transform) {
+  if (!ol.vec.Mat4.equals2D(transform, this.transform_)) {
+    ol.dom.transformElement2D(this.target, transform, 6);
+    goog.vec.Mat4.setFromArray(this.transform_, transform);
+  }
+};
+
+goog.provide('ol.renderer.dom.VectorLayer');
+
+goog.require('goog.asserts');
+goog.require('ol.events');
+goog.require('goog.vec.Mat4');
+goog.require('ol.ViewHint');
+goog.require('ol.dom');
+goog.require('ol.extent');
+goog.require('ol.layer.Vector');
+goog.require('ol.render.Event');
+goog.require('ol.render.EventType');
+goog.require('ol.render.canvas.Immediate');
+goog.require('ol.render.canvas.ReplayGroup');
+goog.require('ol.renderer.dom.Layer');
+goog.require('ol.renderer.vector');
+goog.require('ol.vec.Mat4');
+
+
+/**
+ * @constructor
+ * @extends {ol.renderer.dom.Layer}
+ * @param {ol.layer.Vector} vectorLayer Vector layer.
+ */
+ol.renderer.dom.VectorLayer = function(vectorLayer) {
+
+  /**
+   * @private
+   * @type {CanvasRenderingContext2D}
+   */
+  this.context_ = ol.dom.createCanvasContext2D();
+
+  var target = this.context_.canvas;
+  // Bootstrap sets the style max-width: 100% for all images, which breaks
+  // prevents the image from being displayed in FireFox.  Workaround by
+  // overriding the max-width style.
+  target.style.maxWidth = 'none';
+  target.style.position = 'absolute';
+
+  ol.renderer.dom.Layer.call(this, vectorLayer, target);
+
+  /**
+   * @private
+   * @type {boolean}
+   */
+  this.dirty_ = false;
+
+  /**
+   * @private
+   * @type {number}
+   */
+  this.renderedRevision_ = -1;
+
+  /**
+   * @private
+   * @type {number}
+   */
+  this.renderedResolution_ = NaN;
+
+  /**
+   * @private
+   * @type {ol.Extent}
+   */
+  this.renderedExtent_ = ol.extent.createEmpty();
+
+  /**
+   * @private
+   * @type {function(ol.Feature, ol.Feature): number|null}
+   */
+  this.renderedRenderOrder_ = null;
+
+  /**
+   * @private
+   * @type {ol.render.canvas.ReplayGroup}
+   */
+  this.replayGroup_ = null;
+
+  /**
+   * @private
+   * @type {goog.vec.Mat4.Number}
+   */
+  this.transform_ = goog.vec.Mat4.createNumber();
+
+  /**
+   * @private
+   * @type {goog.vec.Mat4.Number}
+   */
+  this.elementTransform_ = goog.vec.Mat4.createNumber();
+
+};
+ol.inherits(ol.renderer.dom.VectorLayer, ol.renderer.dom.Layer);
+
+
+/**
+ * @inheritDoc
+ */
+ol.renderer.dom.VectorLayer.prototype.clearFrame = function() {
+  // Clear the canvas
+  var canvas = this.context_.canvas;
+  canvas.width = canvas.width;
+  this.renderedRevision_ = 0;
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.renderer.dom.VectorLayer.prototype.composeFrame = function(frameState, layerState) {
+
+  var vectorLayer = /** @type {ol.layer.Vector} */ (this.getLayer());
+  goog.asserts.assertInstanceof(vectorLayer, ol.layer.Vector,
+      'layer is an instance of ol.layer.Vector');
+
+  var viewState = frameState.viewState;
+  var viewCenter = viewState.center;
+  var viewRotation = viewState.rotation;
+  var viewResolution = viewState.resolution;
+  var pixelRatio = frameState.pixelRatio;
+  var viewWidth = frameState.size[0];
+  var viewHeight = frameState.size[1];
+  var imageWidth = viewWidth * pixelRatio;
+  var imageHeight = viewHeight * pixelRatio;
+
+  var transform = ol.vec.Mat4.makeTransform2D(this.transform_,
+      pixelRatio * viewWidth / 2,
+      pixelRatio * viewHeight / 2,
+      pixelRatio / viewResolution,
+      -pixelRatio / viewResolution,
+      -viewRotation,
+      -viewCenter[0], -viewCenter[1]);
+
+  var context = this.context_;
+
+  // Clear the canvas and set the correct size
+  context.canvas.width = imageWidth;
+  context.canvas.height = imageHeight;
+
+  var elementTransform = ol.vec.Mat4.makeTransform2D(this.elementTransform_,
+      0, 0,
+      1 / pixelRatio, 1 / pixelRatio,
+      0,
+      -(imageWidth - viewWidth) / 2 * pixelRatio,
+      -(imageHeight - viewHeight) / 2 * pixelRatio);
+  ol.dom.transformElement2D(context.canvas, elementTransform, 6);
+
+  this.dispatchEvent_(ol.render.EventType.PRECOMPOSE, frameState, transform);
+
+  var replayGroup = this.replayGroup_;
+
+  if (replayGroup && !replayGroup.isEmpty()) {
+
+    context.globalAlpha = layerState.opacity;
+    replayGroup.replay(context, pixelRatio, transform, viewRotation,
+        layerState.managed ? frameState.skippedFeatureUids : {});
+
+    this.dispatchEvent_(ol.render.EventType.RENDER, frameState, transform);
+  }
+
+  this.dispatchEvent_(ol.render.EventType.POSTCOMPOSE, frameState, transform);
+};
+
+
+/**
+ * @param {ol.render.EventType} type Event type.
+ * @param {olx.FrameState} frameState Frame state.
+ * @param {goog.vec.Mat4.Number} transform Transform.
+ * @private
+ */
+ol.renderer.dom.VectorLayer.prototype.dispatchEvent_ = function(type, frameState, transform) {
+  var context = this.context_;
+  var layer = this.getLayer();
+  if (layer.hasListener(type)) {
+    var render = new ol.render.canvas.Immediate(
+        context, frameState.pixelRatio, frameState.extent, transform,
+        frameState.viewState.rotation);
+    var event = new ol.render.Event(type, layer, render, frameState,
+        context, null);
+    layer.dispatchEvent(event);
+  }
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.renderer.dom.VectorLayer.prototype.forEachFeatureAtCoordinate = function(coordinate, frameState, callback, thisArg) {
+  if (!this.replayGroup_) {
+    return undefined;
+  } else {
+    var resolution = frameState.viewState.resolution;
+    var rotation = frameState.viewState.rotation;
+    var layer = this.getLayer();
+    /** @type {Object.<string, boolean>} */
+    var features = {};
+    return this.replayGroup_.forEachFeatureAtCoordinate(coordinate, resolution,
+        rotation, {},
+        /**
+         * @param {ol.Feature|ol.render.Feature} feature Feature.
+         * @return {?} Callback result.
+         */
+        function(feature) {
+          goog.asserts.assert(feature !== undefined, 'received a feature');
+          var key = goog.getUid(feature).toString();
+          if (!(key in features)) {
+            features[key] = true;
+            return callback.call(thisArg, feature, layer);
+          }
+        });
+  }
+};
+
+
+/**
+ * Handle changes in image style state.
+ * @param {ol.events.Event} event Image style change event.
+ * @private
+ */
+ol.renderer.dom.VectorLayer.prototype.handleStyleImageChange_ = function(event) {
+  this.renderIfReadyAndVisible();
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.renderer.dom.VectorLayer.prototype.prepareFrame = function(frameState, layerState) {
+
+  var vectorLayer = /** @type {ol.layer.Vector} */ (this.getLayer());
+  goog.asserts.assertInstanceof(vectorLayer, ol.layer.Vector,
+      'layer is an instance of ol.layer.Vector');
+  var vectorSource = vectorLayer.getSource();
+
+  this.updateAttributions(
+      frameState.attributions, vectorSource.getAttributions());
+  this.updateLogos(frameState, vectorSource);
+
+  var animating = frameState.viewHints[ol.ViewHint.ANIMATING];
+  var interacting = frameState.viewHints[ol.ViewHint.INTERACTING];
+  var updateWhileAnimating = vectorLayer.getUpdateWhileAnimating();
+  var updateWhileInteracting = vectorLayer.getUpdateWhileInteracting();
+
+  if (!this.dirty_ && (!updateWhileAnimating && animating) ||
+      (!updateWhileInteracting && interacting)) {
+    return true;
+  }
+
+  var frameStateExtent = frameState.extent;
+  var viewState = frameState.viewState;
+  var projection = viewState.projection;
+  var resolution = viewState.resolution;
+  var pixelRatio = frameState.pixelRatio;
+  var vectorLayerRevision = vectorLayer.getRevision();
+  var vectorLayerRenderBuffer = vectorLayer.getRenderBuffer();
+  var vectorLayerRenderOrder = vectorLayer.getRenderOrder();
+
+  if (vectorLayerRenderOrder === undefined) {
+    vectorLayerRenderOrder = ol.renderer.vector.defaultOrder;
+  }
+
+  var extent = ol.extent.buffer(frameStateExtent,
+      vectorLayerRenderBuffer * resolution);
+
+  if (!this.dirty_ &&
+      this.renderedResolution_ == resolution &&
+      this.renderedRevision_ == vectorLayerRevision &&
+      this.renderedRenderOrder_ == vectorLayerRenderOrder &&
+      ol.extent.containsExtent(this.renderedExtent_, extent)) {
+    return true;
+  }
+
+  this.replayGroup_ = null;
+
+  this.dirty_ = false;
+
+  var replayGroup =
+      new ol.render.canvas.ReplayGroup(
+          ol.renderer.vector.getTolerance(resolution, pixelRatio), extent,
+          resolution, vectorLayer.getRenderBuffer());
+  vectorSource.loadFeatures(extent, resolution, projection);
+  /**
+   * @param {ol.Feature} feature Feature.
+   * @this {ol.renderer.dom.VectorLayer}
+   */
+  var renderFeature = function(feature) {
+    var styles;
+    var styleFunction = feature.getStyleFunction();
+    if (styleFunction) {
+      styles = styleFunction.call(feature, resolution);
+    } else {
+      styleFunction = vectorLayer.getStyleFunction();
+      if (styleFunction) {
+        styles = styleFunction(feature, resolution);
+      }
+    }
+    if (styles) {
+      var dirty = this.renderFeature(
+          feature, resolution, pixelRatio, styles, replayGroup);
+      this.dirty_ = this.dirty_ || dirty;
+    }
+  };
+  if (vectorLayerRenderOrder) {
+    /** @type {Array.<ol.Feature>} */
+    var features = [];
+    vectorSource.forEachFeatureInExtent(extent,
+        /**
+         * @param {ol.Feature} feature Feature.
+         */
+        function(feature) {
+          features.push(feature);
+        }, this);
+    features.sort(vectorLayerRenderOrder);
+    features.forEach(renderFeature, this);
+  } else {
+    vectorSource.forEachFeatureInExtent(extent, renderFeature, this);
+  }
+  replayGroup.finish();
+
+  this.renderedResolution_ = resolution;
+  this.renderedRevision_ = vectorLayerRevision;
+  this.renderedRenderOrder_ = vectorLayerRenderOrder;
+  this.renderedExtent_ = extent;
+  this.replayGroup_ = replayGroup;
+
+  return true;
+};
+
+
+/**
+ * @param {ol.Feature} feature Feature.
+ * @param {number} resolution Resolution.
+ * @param {number} pixelRatio Pixel ratio.
+ * @param {(ol.style.Style|Array.<ol.style.Style>)} styles The style or array of
+ *     styles.
+ * @param {ol.render.canvas.ReplayGroup} replayGroup Replay group.
+ * @return {boolean} `true` if an image is loading.
+ */
+ol.renderer.dom.VectorLayer.prototype.renderFeature = function(feature, resolution, pixelRatio, styles, replayGroup) {
+  if (!styles) {
+    return false;
+  }
+  var loading = false;
+  if (Array.isArray(styles)) {
+    for (var i = 0, ii = styles.length; i < ii; ++i) {
+      loading = ol.renderer.vector.renderFeature(
+          replayGroup, feature, styles[i],
+          ol.renderer.vector.getSquaredTolerance(resolution, pixelRatio),
+          this.handleStyleImageChange_, this) || loading;
+    }
+  } else {
+    loading = ol.renderer.vector.renderFeature(
+        replayGroup, feature, styles,
+        ol.renderer.vector.getSquaredTolerance(resolution, pixelRatio),
+        this.handleStyleImageChange_, this) || loading;
+  }
+  return loading;
+};
+
+goog.provide('ol.renderer.dom.Map');
+
+goog.require('goog.asserts');
+goog.require('ol.events');
+goog.require('ol.events.Event');
+goog.require('ol.events.EventType');
+goog.require('goog.vec.Mat4');
+goog.require('ol');
+goog.require('ol.RendererType');
+goog.require('ol.array');
+goog.require('ol.css');
+goog.require('ol.dom');
+goog.require('ol.layer.Image');
+goog.require('ol.layer.Layer');
+goog.require('ol.layer.Tile');
+goog.require('ol.layer.Vector');
+goog.require('ol.render.Event');
+goog.require('ol.render.EventType');
+goog.require('ol.render.canvas.Immediate');
+goog.require('ol.renderer.Map');
+goog.require('ol.renderer.dom.ImageLayer');
+goog.require('ol.renderer.dom.Layer');
+goog.require('ol.renderer.dom.TileLayer');
+goog.require('ol.renderer.dom.VectorLayer');
+goog.require('ol.source.State');
+goog.require('ol.vec.Mat4');
+
+
+/**
+ * @constructor
+ * @extends {ol.renderer.Map}
+ * @param {Element} container Container.
+ * @param {ol.Map} map Map.
+ */
+ol.renderer.dom.Map = function(container, map) {
+
+  ol.renderer.Map.call(this, container, map);
+
+  /**
+   * @private
+   * @type {CanvasRenderingContext2D}
+   */
+  this.context_ = ol.dom.createCanvasContext2D();
+  var canvas = this.context_.canvas;
+  canvas.style.position = 'absolute';
+  canvas.style.width = '100%';
+  canvas.style.height = '100%';
+  canvas.className = ol.css.CLASS_UNSELECTABLE;
+  container.insertBefore(canvas, container.childNodes[0] || null);
+
+  /**
+   * @private
+   * @type {!goog.vec.Mat4.Number}
+   */
+  this.transform_ = goog.vec.Mat4.createNumber();
+
+  /**
+   * @type {!Element}
+   * @private
+   */
+  this.layersPane_ = document.createElement('DIV');
+  this.layersPane_.className = ol.css.CLASS_UNSELECTABLE;
+  var style = this.layersPane_.style;
+  style.position = 'absolute';
+  style.width = '100%';
+  style.height = '100%';
+
+  // prevent the img context menu on mobile devices
+  ol.events.listen(this.layersPane_, ol.events.EventType.TOUCHSTART,
+      ol.events.Event.preventDefault);
+
+  container.insertBefore(this.layersPane_, container.childNodes[0] || null);
+
+  /**
+   * @private
+   * @type {boolean}
+   */
+  this.renderedVisible_ = true;
+
+};
+ol.inherits(ol.renderer.dom.Map, ol.renderer.Map);
+
+
+/**
+ * @inheritDoc
+ */
+ol.renderer.dom.Map.prototype.disposeInternal = function() {
+  ol.dom.removeNode(this.layersPane_);
+  ol.renderer.Map.prototype.disposeInternal.call(this);
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.renderer.dom.Map.prototype.createLayerRenderer = function(layer) {
+  var layerRenderer;
+  if (ol.ENABLE_IMAGE && layer instanceof ol.layer.Image) {
+    layerRenderer = new ol.renderer.dom.ImageLayer(layer);
+  } else if (ol.ENABLE_TILE && layer instanceof ol.layer.Tile) {
+    layerRenderer = new ol.renderer.dom.TileLayer(layer);
+  } else if (ol.ENABLE_VECTOR && layer instanceof ol.layer.Vector) {
+    layerRenderer = new ol.renderer.dom.VectorLayer(layer);
+  } else {
+    goog.asserts.fail('unexpected layer configuration');
+    return null;
+  }
+  return layerRenderer;
+};
+
+
+/**
+ * @param {ol.render.EventType} type Event type.
+ * @param {olx.FrameState} frameState Frame state.
+ * @private
+ */
+ol.renderer.dom.Map.prototype.dispatchComposeEvent_ = function(type, frameState) {
+  var map = this.getMap();
+  if (map.hasListener(type)) {
+    var extent = frameState.extent;
+    var pixelRatio = frameState.pixelRatio;
+    var viewState = frameState.viewState;
+    var rotation = viewState.rotation;
+    var context = this.context_;
+    var canvas = context.canvas;
+
+    ol.vec.Mat4.makeTransform2D(this.transform_,
+        canvas.width / 2,
+        canvas.height / 2,
+        pixelRatio / viewState.resolution,
+        -pixelRatio / viewState.resolution,
+        -viewState.rotation,
+        -viewState.center[0], -viewState.center[1]);
+    var vectorContext = new ol.render.canvas.Immediate(context, pixelRatio,
+        extent, this.transform_, rotation);
+    var composeEvent = new ol.render.Event(type, map, vectorContext,
+        frameState, context, null);
+    map.dispatchEvent(composeEvent);
+  }
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.renderer.dom.Map.prototype.getType = function() {
+  return ol.RendererType.DOM;
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.renderer.dom.Map.prototype.renderFrame = function(frameState) {
+
+  if (!frameState) {
+    if (this.renderedVisible_) {
+      this.layersPane_.style.display = 'none';
+      this.renderedVisible_ = false;
+    }
+    return;
+  }
+
+  var map = this.getMap();
+  if (map.hasListener(ol.render.EventType.PRECOMPOSE) ||
+      map.hasListener(ol.render.EventType.POSTCOMPOSE)) {
+    var canvas = this.context_.canvas;
+    var pixelRatio = frameState.pixelRatio;
+    canvas.width = frameState.size[0] * pixelRatio;
+    canvas.height = frameState.size[1] * pixelRatio;
+  }
+
+  this.dispatchComposeEvent_(ol.render.EventType.PRECOMPOSE, frameState);
+
+  var layerStatesArray = frameState.layerStatesArray;
+  ol.array.stableSort(layerStatesArray, ol.renderer.Map.sortByZIndex);
+
+  var viewResolution = frameState.viewState.resolution;
+  var i, ii, layer, layerRenderer, layerState;
+  for (i = 0, ii = layerStatesArray.length; i < ii; ++i) {
+    layerState = layerStatesArray[i];
+    layer = layerState.layer;
+    layerRenderer = /** @type {ol.renderer.dom.Layer} */ (
+        this.getLayerRenderer(layer));
+    goog.asserts.assertInstanceof(layerRenderer, ol.renderer.dom.Layer,
+        'renderer is an instance of ol.renderer.dom.Layer');
+    this.layersPane_.insertBefore(layerRenderer.getTarget(), this.layersPane_.childNodes[i] || null);
+    if (ol.layer.Layer.visibleAtResolution(layerState, viewResolution) &&
+        layerState.sourceState == ol.source.State.READY) {
+      if (layerRenderer.prepareFrame(frameState, layerState)) {
+        layerRenderer.composeFrame(frameState, layerState);
+      }
+    } else {
+      layerRenderer.clearFrame();
+    }
+  }
+
+  var layerStates = frameState.layerStates;
+  var layerKey;
+  for (layerKey in this.getLayerRenderers()) {
+    if (!(layerKey in layerStates)) {
+      layerRenderer = this.getLayerRendererByKey(layerKey);
+      goog.asserts.assertInstanceof(layerRenderer, ol.renderer.dom.Layer,
+          'renderer is an instance of ol.renderer.dom.Layer');
+      ol.dom.removeNode(layerRenderer.getTarget());
+    }
+  }
+
+  if (!this.renderedVisible_) {
+    this.layersPane_.style.display = '';
+    this.renderedVisible_ = true;
+  }
+
+  this.calculateMatrices2D(frameState);
+  this.scheduleRemoveUnusedLayerRenderers(frameState);
+  this.scheduleExpireIconCache(frameState);
+
+  this.dispatchComposeEvent_(ol.render.EventType.POSTCOMPOSE, frameState);
+};
+
+// Copyright 2011 The Closure Library Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS-IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+
+/**
+ * @fileoverview Constants used by the WebGL rendering, including all of the
+ * constants used from the WebGL context.  For example, instead of using
+ * context.ARRAY_BUFFER, your code can use
+ * goog.webgl.ARRAY_BUFFER. The benefits for doing this include allowing
+ * the compiler to optimize your code so that the compiled code does not have to
+ * contain large strings to reference these properties, and reducing runtime
+ * property access.
+ *
+ * Values are taken from the WebGL Spec:
+ * https://www.khronos.org/registry/webgl/specs/1.0/#WEBGLRENDERINGCONTEXT
+ */
+
+goog.provide('goog.webgl');
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.DEPTH_BUFFER_BIT = 0x00000100;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.STENCIL_BUFFER_BIT = 0x00000400;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.COLOR_BUFFER_BIT = 0x00004000;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.POINTS = 0x0000;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.LINES = 0x0001;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.LINE_LOOP = 0x0002;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.LINE_STRIP = 0x0003;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.TRIANGLES = 0x0004;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.TRIANGLE_STRIP = 0x0005;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.TRIANGLE_FAN = 0x0006;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.ZERO = 0;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.ONE = 1;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.SRC_COLOR = 0x0300;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.ONE_MINUS_SRC_COLOR = 0x0301;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.SRC_ALPHA = 0x0302;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.ONE_MINUS_SRC_ALPHA = 0x0303;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.DST_ALPHA = 0x0304;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.ONE_MINUS_DST_ALPHA = 0x0305;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.DST_COLOR = 0x0306;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.ONE_MINUS_DST_COLOR = 0x0307;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.SRC_ALPHA_SATURATE = 0x0308;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.FUNC_ADD = 0x8006;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.BLEND_EQUATION = 0x8009;
+
+
+/**
+ * Same as BLEND_EQUATION
+ * @const
+ * @type {number}
+ */
+goog.webgl.BLEND_EQUATION_RGB = 0x8009;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.BLEND_EQUATION_ALPHA = 0x883D;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.FUNC_SUBTRACT = 0x800A;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.FUNC_REVERSE_SUBTRACT = 0x800B;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.BLEND_DST_RGB = 0x80C8;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.BLEND_SRC_RGB = 0x80C9;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.BLEND_DST_ALPHA = 0x80CA;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.BLEND_SRC_ALPHA = 0x80CB;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.CONSTANT_COLOR = 0x8001;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.ONE_MINUS_CONSTANT_COLOR = 0x8002;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.CONSTANT_ALPHA = 0x8003;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.ONE_MINUS_CONSTANT_ALPHA = 0x8004;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.BLEND_COLOR = 0x8005;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.ARRAY_BUFFER = 0x8892;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.ELEMENT_ARRAY_BUFFER = 0x8893;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.ARRAY_BUFFER_BINDING = 0x8894;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.ELEMENT_ARRAY_BUFFER_BINDING = 0x8895;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.STREAM_DRAW = 0x88E0;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.STATIC_DRAW = 0x88E4;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.DYNAMIC_DRAW = 0x88E8;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.BUFFER_SIZE = 0x8764;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.BUFFER_USAGE = 0x8765;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.CURRENT_VERTEX_ATTRIB = 0x8626;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.FRONT = 0x0404;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.BACK = 0x0405;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.FRONT_AND_BACK = 0x0408;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.CULL_FACE = 0x0B44;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.BLEND = 0x0BE2;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.DITHER = 0x0BD0;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.STENCIL_TEST = 0x0B90;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.DEPTH_TEST = 0x0B71;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.SCISSOR_TEST = 0x0C11;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.POLYGON_OFFSET_FILL = 0x8037;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.SAMPLE_ALPHA_TO_COVERAGE = 0x809E;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.SAMPLE_COVERAGE = 0x80A0;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.NO_ERROR = 0;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.INVALID_ENUM = 0x0500;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.INVALID_VALUE = 0x0501;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.INVALID_OPERATION = 0x0502;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.OUT_OF_MEMORY = 0x0505;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.CW = 0x0900;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.CCW = 0x0901;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.LINE_WIDTH = 0x0B21;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.ALIASED_POINT_SIZE_RANGE = 0x846D;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.ALIASED_LINE_WIDTH_RANGE = 0x846E;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.CULL_FACE_MODE = 0x0B45;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.FRONT_FACE = 0x0B46;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.DEPTH_RANGE = 0x0B70;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.DEPTH_WRITEMASK = 0x0B72;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.DEPTH_CLEAR_VALUE = 0x0B73;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.DEPTH_FUNC = 0x0B74;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.STENCIL_CLEAR_VALUE = 0x0B91;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.STENCIL_FUNC = 0x0B92;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.STENCIL_FAIL = 0x0B94;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.STENCIL_PASS_DEPTH_FAIL = 0x0B95;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.STENCIL_PASS_DEPTH_PASS = 0x0B96;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.STENCIL_REF = 0x0B97;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.STENCIL_VALUE_MASK = 0x0B93;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.STENCIL_WRITEMASK = 0x0B98;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.STENCIL_BACK_FUNC = 0x8800;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.STENCIL_BACK_FAIL = 0x8801;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.STENCIL_BACK_PASS_DEPTH_FAIL = 0x8802;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.STENCIL_BACK_PASS_DEPTH_PASS = 0x8803;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.STENCIL_BACK_REF = 0x8CA3;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.STENCIL_BACK_VALUE_MASK = 0x8CA4;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.STENCIL_BACK_WRITEMASK = 0x8CA5;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.VIEWPORT = 0x0BA2;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.SCISSOR_BOX = 0x0C10;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.COLOR_CLEAR_VALUE = 0x0C22;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.COLOR_WRITEMASK = 0x0C23;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.UNPACK_ALIGNMENT = 0x0CF5;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.PACK_ALIGNMENT = 0x0D05;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.MAX_TEXTURE_SIZE = 0x0D33;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.MAX_VIEWPORT_DIMS = 0x0D3A;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.SUBPIXEL_BITS = 0x0D50;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.RED_BITS = 0x0D52;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.GREEN_BITS = 0x0D53;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.BLUE_BITS = 0x0D54;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.ALPHA_BITS = 0x0D55;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.DEPTH_BITS = 0x0D56;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.STENCIL_BITS = 0x0D57;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.POLYGON_OFFSET_UNITS = 0x2A00;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.POLYGON_OFFSET_FACTOR = 0x8038;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.TEXTURE_BINDING_2D = 0x8069;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.SAMPLE_BUFFERS = 0x80A8;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.SAMPLES = 0x80A9;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.SAMPLE_COVERAGE_VALUE = 0x80AA;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.SAMPLE_COVERAGE_INVERT = 0x80AB;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.COMPRESSED_TEXTURE_FORMATS = 0x86A3;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.DONT_CARE = 0x1100;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.FASTEST = 0x1101;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.NICEST = 0x1102;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.GENERATE_MIPMAP_HINT = 0x8192;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.BYTE = 0x1400;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.UNSIGNED_BYTE = 0x1401;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.SHORT = 0x1402;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.UNSIGNED_SHORT = 0x1403;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.INT = 0x1404;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.UNSIGNED_INT = 0x1405;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.FLOAT = 0x1406;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.DEPTH_COMPONENT = 0x1902;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.ALPHA = 0x1906;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.RGB = 0x1907;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.RGBA = 0x1908;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.LUMINANCE = 0x1909;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.LUMINANCE_ALPHA = 0x190A;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.UNSIGNED_SHORT_4_4_4_4 = 0x8033;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.UNSIGNED_SHORT_5_5_5_1 = 0x8034;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.UNSIGNED_SHORT_5_6_5 = 0x8363;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.FRAGMENT_SHADER = 0x8B30;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.VERTEX_SHADER = 0x8B31;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.MAX_VERTEX_ATTRIBS = 0x8869;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.MAX_VERTEX_UNIFORM_VECTORS = 0x8DFB;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.MAX_VARYING_VECTORS = 0x8DFC;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.MAX_COMBINED_TEXTURE_IMAGE_UNITS = 0x8B4D;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.MAX_VERTEX_TEXTURE_IMAGE_UNITS = 0x8B4C;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.MAX_TEXTURE_IMAGE_UNITS = 0x8872;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.MAX_FRAGMENT_UNIFORM_VECTORS = 0x8DFD;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.SHADER_TYPE = 0x8B4F;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.DELETE_STATUS = 0x8B80;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.LINK_STATUS = 0x8B82;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.VALIDATE_STATUS = 0x8B83;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.ATTACHED_SHADERS = 0x8B85;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.ACTIVE_UNIFORMS = 0x8B86;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.ACTIVE_ATTRIBUTES = 0x8B89;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.SHADING_LANGUAGE_VERSION = 0x8B8C;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.CURRENT_PROGRAM = 0x8B8D;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.NEVER = 0x0200;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.LESS = 0x0201;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.EQUAL = 0x0202;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.LEQUAL = 0x0203;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.GREATER = 0x0204;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.NOTEQUAL = 0x0205;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.GEQUAL = 0x0206;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.ALWAYS = 0x0207;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.KEEP = 0x1E00;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.REPLACE = 0x1E01;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.INCR = 0x1E02;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.DECR = 0x1E03;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.INVERT = 0x150A;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.INCR_WRAP = 0x8507;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.DECR_WRAP = 0x8508;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.VENDOR = 0x1F00;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.RENDERER = 0x1F01;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.VERSION = 0x1F02;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.NEAREST = 0x2600;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.LINEAR = 0x2601;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.NEAREST_MIPMAP_NEAREST = 0x2700;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.LINEAR_MIPMAP_NEAREST = 0x2701;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.NEAREST_MIPMAP_LINEAR = 0x2702;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.LINEAR_MIPMAP_LINEAR = 0x2703;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.TEXTURE_MAG_FILTER = 0x2800;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.TEXTURE_MIN_FILTER = 0x2801;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.TEXTURE_WRAP_S = 0x2802;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.TEXTURE_WRAP_T = 0x2803;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.TEXTURE_2D = 0x0DE1;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.TEXTURE = 0x1702;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.TEXTURE_CUBE_MAP = 0x8513;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.TEXTURE_BINDING_CUBE_MAP = 0x8514;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.TEXTURE_CUBE_MAP_POSITIVE_X = 0x8515;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.TEXTURE_CUBE_MAP_NEGATIVE_X = 0x8516;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.TEXTURE_CUBE_MAP_POSITIVE_Y = 0x8517;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.TEXTURE_CUBE_MAP_NEGATIVE_Y = 0x8518;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.TEXTURE_CUBE_MAP_POSITIVE_Z = 0x8519;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.TEXTURE_CUBE_MAP_NEGATIVE_Z = 0x851A;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.MAX_CUBE_MAP_TEXTURE_SIZE = 0x851C;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.TEXTURE0 = 0x84C0;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.TEXTURE1 = 0x84C1;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.TEXTURE2 = 0x84C2;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.TEXTURE3 = 0x84C3;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.TEXTURE4 = 0x84C4;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.TEXTURE5 = 0x84C5;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.TEXTURE6 = 0x84C6;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.TEXTURE7 = 0x84C7;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.TEXTURE8 = 0x84C8;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.TEXTURE9 = 0x84C9;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.TEXTURE10 = 0x84CA;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.TEXTURE11 = 0x84CB;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.TEXTURE12 = 0x84CC;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.TEXTURE13 = 0x84CD;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.TEXTURE14 = 0x84CE;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.TEXTURE15 = 0x84CF;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.TEXTURE16 = 0x84D0;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.TEXTURE17 = 0x84D1;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.TEXTURE18 = 0x84D2;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.TEXTURE19 = 0x84D3;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.TEXTURE20 = 0x84D4;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.TEXTURE21 = 0x84D5;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.TEXTURE22 = 0x84D6;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.TEXTURE23 = 0x84D7;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.TEXTURE24 = 0x84D8;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.TEXTURE25 = 0x84D9;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.TEXTURE26 = 0x84DA;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.TEXTURE27 = 0x84DB;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.TEXTURE28 = 0x84DC;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.TEXTURE29 = 0x84DD;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.TEXTURE30 = 0x84DE;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.TEXTURE31 = 0x84DF;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.ACTIVE_TEXTURE = 0x84E0;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.REPEAT = 0x2901;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.CLAMP_TO_EDGE = 0x812F;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.MIRRORED_REPEAT = 0x8370;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.FLOAT_VEC2 = 0x8B50;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.FLOAT_VEC3 = 0x8B51;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.FLOAT_VEC4 = 0x8B52;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.INT_VEC2 = 0x8B53;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.INT_VEC3 = 0x8B54;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.INT_VEC4 = 0x8B55;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.BOOL = 0x8B56;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.BOOL_VEC2 = 0x8B57;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.BOOL_VEC3 = 0x8B58;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.BOOL_VEC4 = 0x8B59;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.FLOAT_MAT2 = 0x8B5A;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.FLOAT_MAT3 = 0x8B5B;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.FLOAT_MAT4 = 0x8B5C;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.SAMPLER_2D = 0x8B5E;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.SAMPLER_CUBE = 0x8B60;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.VERTEX_ATTRIB_ARRAY_ENABLED = 0x8622;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.VERTEX_ATTRIB_ARRAY_SIZE = 0x8623;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.VERTEX_ATTRIB_ARRAY_STRIDE = 0x8624;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.VERTEX_ATTRIB_ARRAY_TYPE = 0x8625;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.VERTEX_ATTRIB_ARRAY_NORMALIZED = 0x886A;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.VERTEX_ATTRIB_ARRAY_POINTER = 0x8645;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.VERTEX_ATTRIB_ARRAY_BUFFER_BINDING = 0x889F;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.COMPILE_STATUS = 0x8B81;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.LOW_FLOAT = 0x8DF0;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.MEDIUM_FLOAT = 0x8DF1;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.HIGH_FLOAT = 0x8DF2;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.LOW_INT = 0x8DF3;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.MEDIUM_INT = 0x8DF4;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.HIGH_INT = 0x8DF5;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.FRAMEBUFFER = 0x8D40;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.RENDERBUFFER = 0x8D41;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.RGBA4 = 0x8056;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.RGB5_A1 = 0x8057;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.RGB565 = 0x8D62;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.DEPTH_COMPONENT16 = 0x81A5;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.STENCIL_INDEX = 0x1901;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.STENCIL_INDEX8 = 0x8D48;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.DEPTH_STENCIL = 0x84F9;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.RENDERBUFFER_WIDTH = 0x8D42;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.RENDERBUFFER_HEIGHT = 0x8D43;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.RENDERBUFFER_INTERNAL_FORMAT = 0x8D44;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.RENDERBUFFER_RED_SIZE = 0x8D50;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.RENDERBUFFER_GREEN_SIZE = 0x8D51;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.RENDERBUFFER_BLUE_SIZE = 0x8D52;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.RENDERBUFFER_ALPHA_SIZE = 0x8D53;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.RENDERBUFFER_DEPTH_SIZE = 0x8D54;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.RENDERBUFFER_STENCIL_SIZE = 0x8D55;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE = 0x8CD0;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.FRAMEBUFFER_ATTACHMENT_OBJECT_NAME = 0x8CD1;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.FRAMEBUFFER_ATTACHMENT_TEXTURE_LEVEL = 0x8CD2;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.FRAMEBUFFER_ATTACHMENT_TEXTURE_CUBE_MAP_FACE = 0x8CD3;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.COLOR_ATTACHMENT0 = 0x8CE0;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.DEPTH_ATTACHMENT = 0x8D00;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.STENCIL_ATTACHMENT = 0x8D20;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.DEPTH_STENCIL_ATTACHMENT = 0x821A;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.NONE = 0;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.FRAMEBUFFER_COMPLETE = 0x8CD5;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.FRAMEBUFFER_INCOMPLETE_ATTACHMENT = 0x8CD6;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT = 0x8CD7;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.FRAMEBUFFER_INCOMPLETE_DIMENSIONS = 0x8CD9;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.FRAMEBUFFER_UNSUPPORTED = 0x8CDD;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.FRAMEBUFFER_BINDING = 0x8CA6;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.RENDERBUFFER_BINDING = 0x8CA7;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.MAX_RENDERBUFFER_SIZE = 0x84E8;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.INVALID_FRAMEBUFFER_OPERATION = 0x0506;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.UNPACK_FLIP_Y_WEBGL = 0x9240;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.UNPACK_PREMULTIPLY_ALPHA_WEBGL = 0x9241;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.CONTEXT_LOST_WEBGL = 0x9242;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.UNPACK_COLORSPACE_CONVERSION_WEBGL = 0x9243;
+
+
+/**
+ * @const
+ * @type {number}
+ */
+goog.webgl.BROWSER_DEFAULT_WEBGL = 0x9244;
+
+
+/**
+ * From the OES_texture_half_float extension.
+ * http://www.khronos.org/registry/webgl/extensions/OES_texture_half_float/
+ * @const
+ * @type {number}
+ */
+goog.webgl.HALF_FLOAT_OES = 0x8D61;
+
+
+/**
+ * From the OES_standard_derivatives extension.
+ * http://www.khronos.org/registry/webgl/extensions/OES_standard_derivatives/
+ * @const
+ * @type {number}
+ */
+goog.webgl.FRAGMENT_SHADER_DERIVATIVE_HINT_OES = 0x8B8B;
+
+
+/**
+ * From the OES_vertex_array_object extension.
+ * http://www.khronos.org/registry/webgl/extensions/OES_vertex_array_object/
+ * @const
+ * @type {number}
+ */
+goog.webgl.VERTEX_ARRAY_BINDING_OES = 0x85B5;
+
+
+/**
+ * From the WEBGL_debug_renderer_info extension.
+ * http://www.khronos.org/registry/webgl/extensions/WEBGL_debug_renderer_info/
+ * @const
+ * @type {number}
+ */
+goog.webgl.UNMASKED_VENDOR_WEBGL = 0x9245;
+
+
+/**
+ * From the WEBGL_debug_renderer_info extension.
+ * http://www.khronos.org/registry/webgl/extensions/WEBGL_debug_renderer_info/
+ * @const
+ * @type {number}
+ */
+goog.webgl.UNMASKED_RENDERER_WEBGL = 0x9246;
+
+
+/**
+ * From the WEBGL_compressed_texture_s3tc extension.
+ * http://www.khronos.org/registry/webgl/extensions/WEBGL_compressed_texture_s3tc/
+ * @const
+ * @type {number}
+ */
+goog.webgl.COMPRESSED_RGB_S3TC_DXT1_EXT = 0x83F0;
+
+
+/**
+ * From the WEBGL_compressed_texture_s3tc extension.
+ * http://www.khronos.org/registry/webgl/extensions/WEBGL_compressed_texture_s3tc/
+ * @const
+ * @type {number}
+ */
+goog.webgl.COMPRESSED_RGBA_S3TC_DXT1_EXT = 0x83F1;
+
+
+/**
+ * From the WEBGL_compressed_texture_s3tc extension.
+ * http://www.khronos.org/registry/webgl/extensions/WEBGL_compressed_texture_s3tc/
+ * @const
+ * @type {number}
+ */
+goog.webgl.COMPRESSED_RGBA_S3TC_DXT3_EXT = 0x83F2;
+
+
+/**
+ * From the WEBGL_compressed_texture_s3tc extension.
+ * http://www.khronos.org/registry/webgl/extensions/WEBGL_compressed_texture_s3tc/
+ * @const
+ * @type {number}
+ */
+goog.webgl.COMPRESSED_RGBA_S3TC_DXT5_EXT = 0x83F3;
+
+
+/**
+ * From the EXT_texture_filter_anisotropic extension.
+ * http://www.khronos.org/registry/webgl/extensions/EXT_texture_filter_anisotropic/
+ * @const
+ * @type {number}
+ */
+goog.webgl.TEXTURE_MAX_ANISOTROPY_EXT = 0x84FE;
+
+
+/**
+ * From the EXT_texture_filter_anisotropic extension.
+ * http://www.khronos.org/registry/webgl/extensions/EXT_texture_filter_anisotropic/
+ * @const
+ * @type {number}
+ */
+goog.webgl.MAX_TEXTURE_MAX_ANISOTROPY_EXT = 0x84FF;
+
+goog.provide('ol.webgl.Fragment');
+goog.provide('ol.webgl.Shader');
+goog.provide('ol.webgl.Vertex');
+goog.provide('ol.webgl.shader');
+
+goog.require('goog.webgl');
+goog.require('ol.functions');
+goog.require('ol.webgl');
+
+
+/**
+ * @constructor
+ * @param {string} source Source.
+ * @struct
+ */
+ol.webgl.Shader = function(source) {
+
+  /**
+   * @private
+   * @type {string}
+   */
+  this.source_ = source;
+
+};
+
+
+/**
+ * @return {number} Type.
+ */
+ol.webgl.Shader.prototype.getType = goog.abstractMethod;
+
+
+/**
+ * @return {string} Source.
+ */
+ol.webgl.Shader.prototype.getSource = function() {
+  return this.source_;
+};
+
+
+/**
+ * @return {boolean} Is animated?
+ */
+ol.webgl.Shader.prototype.isAnimated = ol.functions.FALSE;
+
+
+/**
+ * @constructor
+ * @extends {ol.webgl.Shader}
+ * @param {string} source Source.
+ * @struct
+ */
+ol.webgl.shader.Fragment = function(source) {
+  ol.webgl.Shader.call(this, source);
+};
+ol.inherits(ol.webgl.shader.Fragment, ol.webgl.Shader);
+
+
+/**
+ * @inheritDoc
+ */
+ol.webgl.shader.Fragment.prototype.getType = function() {
+  return goog.webgl.FRAGMENT_SHADER;
+};
+
+
+/**
+ * @constructor
+ * @extends {ol.webgl.Shader}
+ * @param {string} source Source.
+ * @struct
+ */
+ol.webgl.shader.Vertex = function(source) {
+  ol.webgl.Shader.call(this, source);
+};
+ol.inherits(ol.webgl.shader.Vertex, ol.webgl.Shader);
+
+
+/**
+ * @inheritDoc
+ */
+ol.webgl.shader.Vertex.prototype.getType = function() {
+  return goog.webgl.VERTEX_SHADER;
+};
+
+// This file is automatically generated, do not edit
+goog.provide('ol.render.webgl.imagereplay.shader.Default');
+goog.provide('ol.render.webgl.imagereplay.shader.Default.Locations');
+goog.provide('ol.render.webgl.imagereplay.shader.DefaultFragment');
+goog.provide('ol.render.webgl.imagereplay.shader.DefaultVertex');
+
+goog.require('ol.webgl.shader');
+
+
+/**
+ * @constructor
+ * @extends {ol.webgl.shader.Fragment}
+ * @struct
+ */
+ol.render.webgl.imagereplay.shader.DefaultFragment = function() {
+  ol.webgl.shader.Fragment.call(this, ol.render.webgl.imagereplay.shader.DefaultFragment.SOURCE);
+};
+ol.inherits(ol.render.webgl.imagereplay.shader.DefaultFragment, ol.webgl.shader.Fragment);
+goog.addSingletonGetter(ol.render.webgl.imagereplay.shader.DefaultFragment);
+
+
+/**
+ * @const
+ * @type {string}
+ */
+ol.render.webgl.imagereplay.shader.DefaultFragment.DEBUG_SOURCE = 'precision mediump float;\nvarying vec2 v_texCoord;\nvarying float v_opacity;\n\nuniform float u_opacity;\nuniform sampler2D u_image;\n\nvoid main(void) {\n  vec4 texColor = texture2D(u_image, v_texCoord);\n  gl_FragColor.rgb = texColor.rgb;\n  float alpha = texColor.a * v_opacity * u_opacity;\n  if (alpha == 0.0) {\n    discard;\n  }\n  gl_FragColor.a = alpha;\n}\n';
+
+
+/**
+ * @const
+ * @type {string}
+ */
+ol.render.webgl.imagereplay.shader.DefaultFragment.OPTIMIZED_SOURCE = 'precision mediump float;varying vec2 a;varying float b;uniform float k;uniform sampler2D l;void main(void){vec4 texColor=texture2D(l,a);gl_FragColor.rgb=texColor.rgb;float alpha=texColor.a*b*k;if(alpha==0.0){discard;}gl_FragColor.a=alpha;}';
+
+
+/**
+ * @const
+ * @type {string}
+ */
+ol.render.webgl.imagereplay.shader.DefaultFragment.SOURCE = goog.DEBUG ?
+    ol.render.webgl.imagereplay.shader.DefaultFragment.DEBUG_SOURCE :
+    ol.render.webgl.imagereplay.shader.DefaultFragment.OPTIMIZED_SOURCE;
+
+
+/**
+ * @constructor
+ * @extends {ol.webgl.shader.Vertex}
+ * @struct
+ */
+ol.render.webgl.imagereplay.shader.DefaultVertex = function() {
+  ol.webgl.shader.Vertex.call(this, ol.render.webgl.imagereplay.shader.DefaultVertex.SOURCE);
+};
+ol.inherits(ol.render.webgl.imagereplay.shader.DefaultVertex, ol.webgl.shader.Vertex);
+goog.addSingletonGetter(ol.render.webgl.imagereplay.shader.DefaultVertex);
+
+
+/**
+ * @const
+ * @type {string}
+ */
+ol.render.webgl.imagereplay.shader.DefaultVertex.DEBUG_SOURCE = 'varying vec2 v_texCoord;\nvarying float v_opacity;\n\nattribute vec2 a_position;\nattribute vec2 a_texCoord;\nattribute vec2 a_offsets;\nattribute float a_opacity;\nattribute float a_rotateWithView;\n\nuniform mat4 u_projectionMatrix;\nuniform mat4 u_offsetScaleMatrix;\nuniform mat4 u_offsetRotateMatrix;\n\nvoid main(void) {\n  mat4 offsetMatrix = u_offsetScaleMatrix;\n  if (a_rotateWithView == 1.0) {\n    offsetMatrix = u_offsetScaleMatrix * u_offsetRotateMatrix;\n  }\n  vec4 offsets = offsetMatrix * vec4(a_offsets, 0., 0.);\n  gl_Position = u_projectionMatrix * vec4(a_position, 0., 1.) + offsets;\n  v_texCoord = a_texCoord;\n  v_opacity = a_opacity;\n}\n\n\n';
+
+
+/**
+ * @const
+ * @type {string}
+ */
+ol.render.webgl.imagereplay.shader.DefaultVertex.OPTIMIZED_SOURCE = 'varying vec2 a;varying float b;attribute vec2 c;attribute vec2 d;attribute vec2 e;attribute float f;attribute float g;uniform mat4 h;uniform mat4 i;uniform mat4 j;void main(void){mat4 offsetMatrix=i;if(g==1.0){offsetMatrix=i*j;}vec4 offsets=offsetMatrix*vec4(e,0.,0.);gl_Position=h*vec4(c,0.,1.)+offsets;a=d;b=f;}';
+
+
+/**
+ * @const
+ * @type {string}
+ */
+ol.render.webgl.imagereplay.shader.DefaultVertex.SOURCE = goog.DEBUG ?
+    ol.render.webgl.imagereplay.shader.DefaultVertex.DEBUG_SOURCE :
+    ol.render.webgl.imagereplay.shader.DefaultVertex.OPTIMIZED_SOURCE;
+
+
+/**
+ * @constructor
+ * @param {WebGLRenderingContext} gl GL.
+ * @param {WebGLProgram} program Program.
+ * @struct
+ */
+ol.render.webgl.imagereplay.shader.Default.Locations = function(gl, program) {
+
+  /**
+   * @type {WebGLUniformLocation}
+   */
+  this.u_image = gl.getUniformLocation(
+      program, goog.DEBUG ? 'u_image' : 'l');
+
+  /**
+   * @type {WebGLUniformLocation}
+   */
+  this.u_offsetRotateMatrix = gl.getUniformLocation(
+      program, goog.DEBUG ? 'u_offsetRotateMatrix' : 'j');
+
+  /**
+   * @type {WebGLUniformLocation}
+   */
+  this.u_offsetScaleMatrix = gl.getUniformLocation(
+      program, goog.DEBUG ? 'u_offsetScaleMatrix' : 'i');
+
+  /**
+   * @type {WebGLUniformLocation}
+   */
+  this.u_opacity = gl.getUniformLocation(
+      program, goog.DEBUG ? 'u_opacity' : 'k');
+
+  /**
+   * @type {WebGLUniformLocation}
+   */
+  this.u_projectionMatrix = gl.getUniformLocation(
+      program, goog.DEBUG ? 'u_projectionMatrix' : 'h');
+
+  /**
+   * @type {number}
+   */
+  this.a_offsets = gl.getAttribLocation(
+      program, goog.DEBUG ? 'a_offsets' : 'e');
+
+  /**
+   * @type {number}
+   */
+  this.a_opacity = gl.getAttribLocation(
+      program, goog.DEBUG ? 'a_opacity' : 'f');
+
+  /**
+   * @type {number}
+   */
+  this.a_position = gl.getAttribLocation(
+      program, goog.DEBUG ? 'a_position' : 'c');
+
+  /**
+   * @type {number}
+   */
+  this.a_rotateWithView = gl.getAttribLocation(
+      program, goog.DEBUG ? 'a_rotateWithView' : 'g');
+
+  /**
+   * @type {number}
+   */
+  this.a_texCoord = gl.getAttribLocation(
+      program, goog.DEBUG ? 'a_texCoord' : 'd');
+};
+
+goog.provide('ol.webgl.Buffer');
+
+goog.require('goog.webgl');
+goog.require('ol');
+
+
+/**
+ * @enum {number}
+ */
+ol.webgl.BufferUsage = {
+  STATIC_DRAW: goog.webgl.STATIC_DRAW,
+  STREAM_DRAW: goog.webgl.STREAM_DRAW,
+  DYNAMIC_DRAW: goog.webgl.DYNAMIC_DRAW
+};
+
+
+/**
+ * @constructor
+ * @param {Array.<number>=} opt_arr Array.
+ * @param {number=} opt_usage Usage.
+ * @struct
+ */
+ol.webgl.Buffer = function(opt_arr, opt_usage) {
+
+  /**
+   * @private
+   * @type {Array.<number>}
+   */
+  this.arr_ = opt_arr !== undefined ? opt_arr : [];
+
+  /**
+   * @private
+   * @type {number}
+   */
+  this.usage_ = opt_usage !== undefined ?
+      opt_usage : ol.webgl.BufferUsage.STATIC_DRAW;
+
+};
+
+
+/**
+ * @return {Array.<number>} Array.
+ */
+ol.webgl.Buffer.prototype.getArray = function() {
+  return this.arr_;
+};
+
+
+/**
+ * @return {number} Usage.
+ */
+ol.webgl.Buffer.prototype.getUsage = function() {
+  return this.usage_;
+};
+
+goog.provide('ol.webgl.Context');
+
+goog.require('goog.asserts');
+goog.require('ol');
+goog.require('ol.Disposable');
+goog.require('ol.array');
+goog.require('ol.events');
+goog.require('ol.object');
+goog.require('ol.webgl.Buffer');
+goog.require('ol.webgl.WebGLContextEventType');
+
+
+/**
+ * @classdesc
+ * A WebGL context for accessing low-level WebGL capabilities.
+ *
+ * @constructor
+ * @extends {ol.Disposable}
+ * @param {HTMLCanvasElement} canvas Canvas.
+ * @param {WebGLRenderingContext} gl GL.
+ */
+ol.webgl.Context = function(canvas, gl) {
+
+  /**
+   * @private
+   * @type {HTMLCanvasElement}
+   */
+  this.canvas_ = canvas;
+
+  /**
+   * @private
+   * @type {WebGLRenderingContext}
+   */
+  this.gl_ = gl;
+
+  /**
+   * @private
+   * @type {Object.<string, ol.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 {boolean}
+   */
+  this.hasOESElementIndexUint = ol.array.includes(
+      ol.WEBGL_EXTENSIONS, 'OES_element_index_uint');
+
+  // use the OES_element_index_uint extension if available
+  if (this.hasOESElementIndexUint) {
+    var ext = gl.getExtension('OES_element_index_uint');
+    goog.asserts.assert(ext,
+        'Failed to get extension "OES_element_index_uint"');
+  }
+
+  ol.events.listen(this.canvas_, ol.webgl.WebGLContextEventType.LOST,
+      this.handleWebGLContextLost, this);
+  ol.events.listen(this.canvas_, ol.webgl.WebGLContextEventType.RESTORED,
+      this.handleWebGLContextRestored, this);
+
+};
+ol.inherits(ol.webgl.Context, ol.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 {ol.webgl.Buffer} buf Buffer.
+ */
+ol.webgl.Context.prototype.bindBuffer = function(target, buf) {
+  var gl = this.getGL();
+  var arr = buf.getArray();
+  var bufferKey = String(goog.getUid(buf));
+  if (bufferKey in this.bufferCache_) {
+    var bufferCacheEntry = this.bufferCache_[bufferKey];
+    gl.bindBuffer(target, bufferCacheEntry.buffer);
+  } else {
+    var buffer = gl.createBuffer();
+    gl.bindBuffer(target, buffer);
+    goog.asserts.assert(target == goog.webgl.ARRAY_BUFFER ||
+        target == goog.webgl.ELEMENT_ARRAY_BUFFER,
+        'target is supposed to be an ARRAY_BUFFER or ELEMENT_ARRAY_BUFFER');
+    var /** @type {ArrayBufferView} */ arrayBuffer;
+    if (target == goog.webgl.ARRAY_BUFFER) {
+      arrayBuffer = new Float32Array(arr);
+    } else if (target == goog.webgl.ELEMENT_ARRAY_BUFFER) {
+      arrayBuffer = this.hasOESElementIndexUint ?
+          new Uint32Array(arr) : new Uint16Array(arr);
+    } else {
+      goog.asserts.fail();
+    }
+    gl.bufferData(target, arrayBuffer, buf.getUsage());
+    this.bufferCache_[bufferKey] = {
+      buf: buf,
+      buffer: buffer
+    };
+  }
+};
+
+
+/**
+ * @param {ol.webgl.Buffer} buf Buffer.
+ */
+ol.webgl.Context.prototype.deleteBuffer = function(buf) {
+  var gl = this.getGL();
+  var bufferKey = String(goog.getUid(buf));
+  goog.asserts.assert(bufferKey in this.bufferCache_,
+      'attempted to delete uncached buffer');
+  var bufferCacheEntry = this.bufferCache_[bufferKey];
+  if (!gl.isContextLost()) {
+    gl.deleteBuffer(bufferCacheEntry.buffer);
+  }
+  delete this.bufferCache_[bufferKey];
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.webgl.Context.prototype.disposeInternal = function() {
+  ol.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.
+ */
+ol.webgl.Context.prototype.getCanvas = function() {
+  return this.canvas_;
+};
+
+
+/**
+ * Get the WebGL rendering context
+ * @return {WebGLRenderingContext} The rendering context.
+ * @api
+ */
+ol.webgl.Context.prototype.getGL = function() {
+  return this.gl_;
+};
+
+
+/**
+ * Get the frame buffer for hit detection.
+ * @return {WebGLFramebuffer} The hit detection frame buffer.
+ */
+ol.webgl.Context.prototype.getHitDetectionFramebuffer = function() {
+  if (!this.hitDetectionFramebuffer_) {
+    this.initHitDetectionFramebuffer_();
+  }
+  return this.hitDetectionFramebuffer_;
+};
+
+
+/**
+ * Get shader from the cache if it's in the cache. Otherwise, create
+ * the WebGL shader, compile it, and add entry to cache.
+ * @param {ol.webgl.Shader} shaderObject Shader object.
+ * @return {WebGLShader} Shader.
+ */
+ol.webgl.Context.prototype.getShader = function(shaderObject) {
+  var shaderKey = String(goog.getUid(shaderObject));
+  if (shaderKey in this.shaderCache_) {
+    return this.shaderCache_[shaderKey];
+  } else {
+    var gl = this.getGL();
+    var shader = gl.createShader(shaderObject.getType());
+    gl.shaderSource(shader, shaderObject.getSource());
+    gl.compileShader(shader);
+    goog.asserts.assert(
+        gl.getShaderParameter(shader, goog.webgl.COMPILE_STATUS) ||
+        gl.isContextLost(),
+        gl.getShaderInfoLog(shader) || 'illegal state, shader not compiled or context lost');
+    this.shaderCache_[shaderKey] = shader;
+    return shader;
+  }
+};
+
+
+/**
+ * Get the program from the cache if it's in the cache. Otherwise create
+ * the WebGL program, attach the shaders to it, and add an entry to the
+ * cache.
+ * @param {ol.webgl.shader.Fragment} fragmentShaderObject Fragment shader.
+ * @param {ol.webgl.shader.Vertex} vertexShaderObject Vertex shader.
+ * @return {WebGLProgram} Program.
+ */
+ol.webgl.Context.prototype.getProgram = function(
+    fragmentShaderObject, vertexShaderObject) {
+  var programKey =
+      goog.getUid(fragmentShaderObject) + '/' + goog.getUid(vertexShaderObject);
+  if (programKey in this.programCache_) {
+    return this.programCache_[programKey];
+  } else {
+    var gl = this.getGL();
+    var program = gl.createProgram();
+    gl.attachShader(program, this.getShader(fragmentShaderObject));
+    gl.attachShader(program, this.getShader(vertexShaderObject));
+    gl.linkProgram(program);
+    goog.asserts.assert(
+        gl.getProgramParameter(program, goog.webgl.LINK_STATUS) ||
+        gl.isContextLost(),
+        gl.getProgramInfoLog(program) || 'illegal state, shader not linked or context lost');
+    this.programCache_[programKey] = program;
+    return program;
+  }
+};
+
+
+/**
+ * FIXME empy description for jsdoc
+ */
+ol.webgl.Context.prototype.handleWebGLContextLost = function() {
+  ol.object.clear(this.bufferCache_);
+  ol.object.clear(this.shaderCache_);
+  ol.object.clear(this.programCache_);
+  this.currentProgram_ = null;
+  this.hitDetectionFramebuffer_ = null;
+  this.hitDetectionTexture_ = null;
+  this.hitDetectionRenderbuffer_ = null;
+};
+
+
+/**
+ * FIXME empy description for jsdoc
+ */
+ol.webgl.Context.prototype.handleWebGLContextRestored = function() {
+};
+
+
+/**
+ * Creates a 1x1 pixel framebuffer for the hit-detection.
+ * @private
+ */
+ol.webgl.Context.prototype.initHitDetectionFramebuffer_ = function() {
+  var gl = this.gl_;
+  var framebuffer = gl.createFramebuffer();
+  gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
+
+  var texture = ol.webgl.Context.createEmptyTexture(gl, 1, 1);
+  var renderbuffer = gl.createRenderbuffer();
+  gl.bindRenderbuffer(gl.RENDERBUFFER, renderbuffer);
+  gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, 1, 1);
+  gl.framebufferTexture2D(
+      gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);
+  gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT,
+      gl.RENDERBUFFER, renderbuffer);
+
+  gl.bindTexture(gl.TEXTURE_2D, null);
+  gl.bindRenderbuffer(gl.RENDERBUFFER, null);
+  gl.bindFramebuffer(gl.FRAMEBUFFER, null);
+
+  this.hitDetectionFramebuffer_ = framebuffer;
+  this.hitDetectionTexture_ = texture;
+  this.hitDetectionRenderbuffer_ = renderbuffer;
+};
+
+
+/**
+ * Use a program.  If the program is already in use, this will return `false`.
+ * @param {WebGLProgram} program Program.
+ * @return {boolean} Changed.
+ * @api
+ */
+ol.webgl.Context.prototype.useProgram = function(program) {
+  if (program == this.currentProgram_) {
+    return false;
+  } else {
+    var gl = this.getGL();
+    gl.useProgram(program);
+    this.currentProgram_ = program;
+    return true;
+  }
+};
+
+
+/**
+ * @param {WebGLRenderingContext} gl WebGL rendering context.
+ * @param {number=} opt_wrapS wrapS.
+ * @param {number=} opt_wrapT wrapT.
+ * @return {WebGLTexture} The texture.
+ * @private
+ */
+ol.webgl.Context.createTexture_ = function(gl, opt_wrapS, opt_wrapT) {
+  var texture = gl.createTexture();
+  gl.bindTexture(gl.TEXTURE_2D, texture);
+  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
+  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
+
+  if (opt_wrapS !== undefined) {
+    gl.texParameteri(
+        goog.webgl.TEXTURE_2D, goog.webgl.TEXTURE_WRAP_S, opt_wrapS);
+  }
+  if (opt_wrapT !== undefined) {
+    gl.texParameteri(
+        goog.webgl.TEXTURE_2D, goog.webgl.TEXTURE_WRAP_T, opt_wrapT);
+  }
+
+  return texture;
+};
+
+
+/**
+ * @param {WebGLRenderingContext} gl WebGL rendering context.
+ * @param {number} width Width.
+ * @param {number} height Height.
+ * @param {number=} opt_wrapS wrapS.
+ * @param {number=} opt_wrapT wrapT.
+ * @return {WebGLTexture} The texture.
+ */
+ol.webgl.Context.createEmptyTexture = function(
+    gl, width, height, opt_wrapS, opt_wrapT) {
+  var texture = ol.webgl.Context.createTexture_(gl, opt_wrapS, opt_wrapT);
+  gl.texImage2D(
+      gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE,
+      null);
+
+  return texture;
+};
+
+
+/**
+ * @param {WebGLRenderingContext} gl WebGL rendering context.
+ * @param {HTMLCanvasElement|HTMLImageElement|HTMLVideoElement} image Image.
+ * @param {number=} opt_wrapS wrapS.
+ * @param {number=} opt_wrapT wrapT.
+ * @return {WebGLTexture} The texture.
+ */
+ol.webgl.Context.createTexture = function(gl, image, opt_wrapS, opt_wrapT) {
+  var texture = ol.webgl.Context.createTexture_(gl, opt_wrapS, opt_wrapT);
+  gl.texImage2D(
+      gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
+
+  return texture;
+};
+
+goog.provide('ol.render.webgl.ImageReplay');
+goog.provide('ol.render.webgl.ReplayGroup');
+
+goog.require('goog.asserts');
+goog.require('goog.vec.Mat4');
+goog.require('ol.extent');
+goog.require('ol.object');
+goog.require('ol.render.IReplayGroup');
+goog.require('ol.render.VectorContext');
+goog.require('ol.render.webgl.imagereplay.shader.Default');
+goog.require('ol.render.webgl.imagereplay.shader.Default.Locations');
+goog.require('ol.render.webgl.imagereplay.shader.DefaultFragment');
+goog.require('ol.render.webgl.imagereplay.shader.DefaultVertex');
+goog.require('ol.vec.Mat4');
+goog.require('ol.webgl.Buffer');
+goog.require('ol.webgl.Context');
+
+
+/**
+ * @constructor
+ * @extends {ol.render.VectorContext}
+ * @param {number} tolerance Tolerance.
+ * @param {ol.Extent} maxExtent Max extent.
+ * @protected
+ * @struct
+ */
+ol.render.webgl.ImageReplay = function(tolerance, maxExtent) {
+  ol.render.VectorContext.call(this);
+
+  /**
+   * @type {number|undefined}
+   * @private
+   */
+  this.anchorX_ = undefined;
+
+  /**
+   * @type {number|undefined}
+   * @private
+   */
+  this.anchorY_ = undefined;
+
+  /**
+   * The origin of the coordinate system for the point coordinates sent to
+   * the GPU. To eliminate jitter caused by precision problems in the GPU
+   * we use the "Rendering Relative to Eye" technique described in the "3D
+   * Engine Design for Virtual Globes" book.
+   * @private
+   * @type {ol.Coordinate}
+   */
+  this.origin_ = ol.extent.getCenter(maxExtent);
+
+  /**
+   * @type {Array.<number>}
+   * @private
+   */
+  this.groupIndices_ = [];
+
+  /**
+   * @type {Array.<number>}
+   * @private
+   */
+  this.hitDetectionGroupIndices_ = [];
+
+  /**
+   * @type {number|undefined}
+   * @private
+   */
+  this.height_ = undefined;
+
+  /**
+   * @type {Array.<HTMLCanvasElement|HTMLImageElement|HTMLVideoElement>}
+   * @private
+   */
+  this.images_ = [];
+
+  /**
+   * @type {Array.<HTMLCanvasElement|HTMLImageElement|HTMLVideoElement>}
+   * @private
+   */
+  this.hitDetectionImages_ = [];
+
+  /**
+   * @type {number|undefined}
+   * @private
+   */
+  this.imageHeight_ = undefined;
+
+  /**
+   * @type {number|undefined}
+   * @private
+   */
+  this.imageWidth_ = undefined;
+
+  /**
+   * @type {Array.<number>}
+   * @private
+   */
+  this.indices_ = [];
+
+  /**
+   * @type {ol.webgl.Buffer}
+   * @private
+   */
+  this.indicesBuffer_ = null;
+
+  /**
+   * @private
+   * @type {ol.render.webgl.imagereplay.shader.Default.Locations}
+   */
+  this.defaultLocations_ = null;
+
+  /**
+   * @private
+   * @type {number|undefined}
+   */
+  this.opacity_ = undefined;
+
+  /**
+   * @type {!goog.vec.Mat4.Number}
+   * @private
+   */
+  this.offsetRotateMatrix_ = goog.vec.Mat4.createNumberIdentity();
+
+  /**
+   * @type {!goog.vec.Mat4.Number}
+   * @private
+   */
+  this.offsetScaleMatrix_ = goog.vec.Mat4.createNumberIdentity();
+
+  /**
+   * @type {number|undefined}
+   * @private
+   */
+  this.originX_ = undefined;
+
+  /**
+   * @type {number|undefined}
+   * @private
+   */
+  this.originY_ = undefined;
+
+  /**
+   * @type {!goog.vec.Mat4.Number}
+   * @private
+   */
+  this.projectionMatrix_ = goog.vec.Mat4.createNumberIdentity();
+
+  /**
+   * @private
+   * @type {boolean|undefined}
+   */
+  this.rotateWithView_ = undefined;
+
+  /**
+   * @private
+   * @type {number|undefined}
+   */
+  this.rotation_ = undefined;
+
+  /**
+   * @private
+   * @type {number|undefined}
+   */
+  this.scale_ = undefined;
+
+  /**
+   * @type {Array.<WebGLTexture>}
+   * @private
+   */
+  this.textures_ = [];
+
+  /**
+   * @type {Array.<WebGLTexture>}
+   * @private
+   */
+  this.hitDetectionTextures_ = [];
+
+  /**
+   * @type {Array.<number>}
+   * @private
+   */
+  this.vertices_ = [];
+
+  /**
+   * @type {ol.webgl.Buffer}
+   * @private
+   */
+  this.verticesBuffer_ = null;
+
+  /**
+   * Start index per feature (the index).
+   * @type {Array.<number>}
+   * @private
+   */
+  this.startIndices_ = [];
+
+  /**
+   * Start index per feature (the feature).
+   * @type {Array.<ol.Feature|ol.render.Feature>}
+   * @private
+   */
+  this.startIndicesFeature_ = [];
+
+  /**
+   * @type {number|undefined}
+   * @private
+   */
+  this.width_ = undefined;
+};
+ol.inherits(ol.render.webgl.ImageReplay, ol.render.VectorContext);
+
+
+/**
+ * @param {ol.webgl.Context} context WebGL context.
+ * @return {function()} Delete resources function.
+ */
+ol.render.webgl.ImageReplay.prototype.getDeleteResourcesFunction = function(context) {
+  // We only delete our stuff here. The shaders and the program may
+  // be used by other ImageReplay instances (for other layers). And
+  // they will be deleted when disposing of the ol.webgl.Context
+  // object.
+  goog.asserts.assert(this.verticesBuffer_,
+      'verticesBuffer must not be null');
+  goog.asserts.assert(this.indicesBuffer_,
+      'indicesBuffer must not be null');
+  var verticesBuffer = this.verticesBuffer_;
+  var indicesBuffer = this.indicesBuffer_;
+  var textures = this.textures_;
+  var hitDetectionTextures = this.hitDetectionTextures_;
+  var gl = context.getGL();
+  return function() {
+    if (!gl.isContextLost()) {
+      var i, ii;
+      for (i = 0, ii = textures.length; i < ii; ++i) {
+        gl.deleteTexture(textures[i]);
+      }
+      for (i = 0, ii = hitDetectionTextures.length; i < ii; ++i) {
+        gl.deleteTexture(hitDetectionTextures[i]);
+      }
+    }
+    context.deleteBuffer(verticesBuffer);
+    context.deleteBuffer(indicesBuffer);
+  };
+};
+
+
+/**
+ * @param {Array.<number>} flatCoordinates Flat coordinates.
+ * @param {number} offset Offset.
+ * @param {number} end End.
+ * @param {number} stride Stride.
+ * @return {number} My end.
+ * @private
+ */
+ol.render.webgl.ImageReplay.prototype.drawCoordinates_ = function(flatCoordinates, offset, end, stride) {
+  goog.asserts.assert(this.anchorX_ !== undefined, 'anchorX is defined');
+  goog.asserts.assert(this.anchorY_ !== undefined, 'anchorY is defined');
+  goog.asserts.assert(this.height_ !== undefined, 'height is defined');
+  goog.asserts.assert(this.imageHeight_ !== undefined,
+      'imageHeight is defined');
+  goog.asserts.assert(this.imageWidth_ !== undefined, 'imageWidth is defined');
+  goog.asserts.assert(this.opacity_ !== undefined, 'opacity is defined');
+  goog.asserts.assert(this.originX_ !== undefined, 'originX is defined');
+  goog.asserts.assert(this.originY_ !== undefined, 'originY is defined');
+  goog.asserts.assert(this.rotateWithView_ !== undefined,
+      'rotateWithView is defined');
+  goog.asserts.assert(this.rotation_ !== undefined, 'rotation is defined');
+  goog.asserts.assert(this.scale_ !== undefined, 'scale is defined');
+  goog.asserts.assert(this.width_ !== undefined, 'width is defined');
+  var anchorX = this.anchorX_;
+  var anchorY = this.anchorY_;
+  var height = this.height_;
+  var imageHeight = this.imageHeight_;
+  var imageWidth = this.imageWidth_;
+  var opacity = this.opacity_;
+  var originX = this.originX_;
+  var originY = this.originY_;
+  var rotateWithView = this.rotateWithView_ ? 1.0 : 0.0;
+  var rotation = this.rotation_;
+  var scale = this.scale_;
+  var width = this.width_;
+  var cos = Math.cos(rotation);
+  var sin = Math.sin(rotation);
+  var numIndices = this.indices_.length;
+  var numVertices = this.vertices_.length;
+  var i, n, offsetX, offsetY, x, y;
+  for (i = offset; i < end; i += stride) {
+    x = flatCoordinates[i] - this.origin_[0];
+    y = flatCoordinates[i + 1] - this.origin_[1];
+
+    // There are 4 vertices per [x, y] point, one for each corner of the
+    // rectangle we're going to draw. We'd use 1 vertex per [x, y] point if
+    // WebGL supported Geometry Shaders (which can emit new vertices), but that
+    // is not currently the case.
+    //
+    // And each vertex includes 8 values: the x and y coordinates, the x and
+    // y offsets used to calculate the position of the corner, the u and
+    // v texture coordinates for the corner, the opacity, and whether the
+    // the image should be rotated with the view (rotateWithView).
+
+    n = numVertices / 8;
+
+    // bottom-left corner
+    offsetX = -scale * anchorX;
+    offsetY = -scale * (height - anchorY);
+    this.vertices_[numVertices++] = x;
+    this.vertices_[numVertices++] = y;
+    this.vertices_[numVertices++] = offsetX * cos - offsetY * sin;
+    this.vertices_[numVertices++] = offsetX * sin + offsetY * cos;
+    this.vertices_[numVertices++] = originX / imageWidth;
+    this.vertices_[numVertices++] = (originY + height) / imageHeight;
+    this.vertices_[numVertices++] = opacity;
+    this.vertices_[numVertices++] = rotateWithView;
+
+    // bottom-right corner
+    offsetX = scale * (width - anchorX);
+    offsetY = -scale * (height - anchorY);
+    this.vertices_[numVertices++] = x;
+    this.vertices_[numVertices++] = y;
+    this.vertices_[numVertices++] = offsetX * cos - offsetY * sin;
+    this.vertices_[numVertices++] = offsetX * sin + offsetY * cos;
+    this.vertices_[numVertices++] = (originX + width) / imageWidth;
+    this.vertices_[numVertices++] = (originY + height) / imageHeight;
+    this.vertices_[numVertices++] = opacity;
+    this.vertices_[numVertices++] = rotateWithView;
+
+    // top-right corner
+    offsetX = scale * (width - anchorX);
+    offsetY = scale * anchorY;
+    this.vertices_[numVertices++] = x;
+    this.vertices_[numVertices++] = y;
+    this.vertices_[numVertices++] = offsetX * cos - offsetY * sin;
+    this.vertices_[numVertices++] = offsetX * sin + offsetY * cos;
+    this.vertices_[numVertices++] = (originX + width) / imageWidth;
+    this.vertices_[numVertices++] = originY / imageHeight;
+    this.vertices_[numVertices++] = opacity;
+    this.vertices_[numVertices++] = rotateWithView;
+
+    // top-left corner
+    offsetX = -scale * anchorX;
+    offsetY = scale * anchorY;
+    this.vertices_[numVertices++] = x;
+    this.vertices_[numVertices++] = y;
+    this.vertices_[numVertices++] = offsetX * cos - offsetY * sin;
+    this.vertices_[numVertices++] = offsetX * sin + offsetY * cos;
+    this.vertices_[numVertices++] = originX / imageWidth;
+    this.vertices_[numVertices++] = originY / imageHeight;
+    this.vertices_[numVertices++] = opacity;
+    this.vertices_[numVertices++] = rotateWithView;
+
+    this.indices_[numIndices++] = n;
+    this.indices_[numIndices++] = n + 1;
+    this.indices_[numIndices++] = n + 2;
+    this.indices_[numIndices++] = n;
+    this.indices_[numIndices++] = n + 2;
+    this.indices_[numIndices++] = n + 3;
+  }
+
+  return numVertices;
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.render.webgl.ImageReplay.prototype.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
+ */
+ol.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);
+};
+
+
+/**
+ * @param {ol.webgl.Context} context Context.
+ */
+ol.render.webgl.ImageReplay.prototype.finish = function(context) {
+  var gl = context.getGL();
+
+  this.groupIndices_.push(this.indices_.length);
+  goog.asserts.assert(this.images_.length === this.groupIndices_.length,
+      'number of images and groupIndices match');
+  this.hitDetectionGroupIndices_.push(this.indices_.length);
+  goog.asserts.assert(this.hitDetectionImages_.length ===
+      this.hitDetectionGroupIndices_.length,
+      'number of hitDetectionImages and hitDetectionGroupIndices match');
+
+  // create, bind, and populate the vertices buffer
+  this.verticesBuffer_ = new ol.webgl.Buffer(this.vertices_);
+  context.bindBuffer(goog.webgl.ARRAY_BUFFER, this.verticesBuffer_);
+
+  var indices = this.indices_;
+  var bits = context.hasOESElementIndexUint ? 32 : 16;
+  goog.asserts.assert(indices[indices.length - 1] < Math.pow(2, bits),
+      'Too large element index detected [%s] (OES_element_index_uint "%s")',
+      indices[indices.length - 1], context.hasOESElementIndexUint);
+
+  // create, bind, and populate the indices buffer
+  this.indicesBuffer_ = new ol.webgl.Buffer(indices);
+  context.bindBuffer(goog.webgl.ELEMENT_ARRAY_BUFFER, this.indicesBuffer_);
+
+  // create textures
+  /** @type {Object.<string, WebGLTexture>} */
+  var texturePerImage = {};
+
+  this.createTextures_(this.textures_, this.images_, texturePerImage, gl);
+  goog.asserts.assert(this.textures_.length === this.groupIndices_.length,
+      'number of textures and groupIndices match');
+
+  this.createTextures_(this.hitDetectionTextures_, this.hitDetectionImages_,
+      texturePerImage, gl);
+  goog.asserts.assert(this.hitDetectionTextures_.length ===
+      this.hitDetectionGroupIndices_.length,
+      'number of hitDetectionTextures and hitDetectionGroupIndices match');
+
+  this.anchorX_ = undefined;
+  this.anchorY_ = undefined;
+  this.height_ = undefined;
+  this.images_ = null;
+  this.hitDetectionImages_ = null;
+  this.imageHeight_ = undefined;
+  this.imageWidth_ = undefined;
+  this.indices_ = null;
+  this.opacity_ = undefined;
+  this.originX_ = undefined;
+  this.originY_ = undefined;
+  this.rotateWithView_ = undefined;
+  this.rotation_ = undefined;
+  this.scale_ = undefined;
+  this.vertices_ = null;
+  this.width_ = undefined;
+};
+
+
+/**
+ * @private
+ * @param {Array.<WebGLTexture>} textures Textures.
+ * @param {Array.<HTMLCanvasElement|HTMLImageElement|HTMLVideoElement>} images
+ *    Images.
+ * @param {Object.<string, WebGLTexture>} texturePerImage Texture cache.
+ * @param {WebGLRenderingContext} gl Gl.
+ */
+ol.render.webgl.ImageReplay.prototype.createTextures_ = function(textures, images, texturePerImage, gl) {
+  goog.asserts.assert(textures.length === 0,
+      'upon creation, textures is empty');
+
+  var texture, image, uid, i;
+  var ii = images.length;
+  for (i = 0; i < ii; ++i) {
+    image = images[i];
+
+    uid = goog.getUid(image).toString();
+    if (uid in texturePerImage) {
+      texture = texturePerImage[uid];
+    } else {
+      texture = ol.webgl.Context.createTexture(
+          gl, image, goog.webgl.CLAMP_TO_EDGE, goog.webgl.CLAMP_TO_EDGE);
+      texturePerImage[uid] = texture;
+    }
+    textures[i] = texture;
+  }
+};
+
+
+/**
+ * @param {ol.webgl.Context} context Context.
+ * @param {ol.Coordinate} center Center.
+ * @param {number} resolution Resolution.
+ * @param {number} rotation Rotation.
+ * @param {ol.Size} size Size.
+ * @param {number} pixelRatio Pixel ratio.
+ * @param {number} opacity Global opacity.
+ * @param {Object.<string, boolean>} skippedFeaturesHash Ids of features
+ *  to skip.
+ * @param {function((ol.Feature|ol.render.Feature)): T|undefined} featureCallback Feature callback.
+ * @param {boolean} oneByOne Draw features one-by-one for the hit-detecion.
+ * @param {ol.Extent=} opt_hitExtent Hit extent: Only features intersecting
+ *  this extent are checked.
+ * @return {T|undefined} Callback result.
+ * @template T
+ */
+ol.render.webgl.ImageReplay.prototype.replay = function(context,
+    center, resolution, rotation, size, pixelRatio,
+    opacity, skippedFeaturesHash,
+    featureCallback, oneByOne, opt_hitExtent) {
+  var gl = context.getGL();
+
+  // bind the vertices buffer
+  goog.asserts.assert(this.verticesBuffer_,
+      'verticesBuffer must not be null');
+  context.bindBuffer(goog.webgl.ARRAY_BUFFER, this.verticesBuffer_);
+
+  // bind the indices buffer
+  goog.asserts.assert(this.indicesBuffer_,
+      'indecesBuffer must not be null');
+  context.bindBuffer(goog.webgl.ELEMENT_ARRAY_BUFFER, this.indicesBuffer_);
+
+  // get the program
+  var fragmentShader =
+      ol.render.webgl.imagereplay.shader.DefaultFragment.getInstance();
+  var vertexShader =
+      ol.render.webgl.imagereplay.shader.DefaultVertex.getInstance();
+  var program = context.getProgram(fragmentShader, vertexShader);
+
+  // get the locations
+  var locations;
+  if (!this.defaultLocations_) {
+    locations =
+        new ol.render.webgl.imagereplay.shader.Default.Locations(gl, program);
+    this.defaultLocations_ = locations;
+  } else {
+    locations = this.defaultLocations_;
+  }
+
+  // use the program (FIXME: use the return value)
+  context.useProgram(program);
+
+  // enable the vertex attrib arrays
+  gl.enableVertexAttribArray(locations.a_position);
+  gl.vertexAttribPointer(locations.a_position, 2, goog.webgl.FLOAT,
+      false, 32, 0);
+
+  gl.enableVertexAttribArray(locations.a_offsets);
+  gl.vertexAttribPointer(locations.a_offsets, 2, goog.webgl.FLOAT,
+      false, 32, 8);
+
+  gl.enableVertexAttribArray(locations.a_texCoord);
+  gl.vertexAttribPointer(locations.a_texCoord, 2, goog.webgl.FLOAT,
+      false, 32, 16);
+
+  gl.enableVertexAttribArray(locations.a_opacity);
+  gl.vertexAttribPointer(locations.a_opacity, 1, goog.webgl.FLOAT,
+      false, 32, 24);
+
+  gl.enableVertexAttribArray(locations.a_rotateWithView);
+  gl.vertexAttribPointer(locations.a_rotateWithView, 1, goog.webgl.FLOAT,
+      false, 32, 28);
+
+  // set the "uniform" values
+  var projectionMatrix = this.projectionMatrix_;
+  ol.vec.Mat4.makeTransform2D(projectionMatrix,
+      0.0, 0.0,
+      2 / (resolution * size[0]),
+      2 / (resolution * size[1]),
+      -rotation,
+      -(center[0] - this.origin_[0]), -(center[1] - this.origin_[1]));
+
+  var offsetScaleMatrix = this.offsetScaleMatrix_;
+  goog.vec.Mat4.makeScale(offsetScaleMatrix, 2 / size[0], 2 / size[1], 1);
+
+  var offsetRotateMatrix = this.offsetRotateMatrix_;
+  goog.vec.Mat4.makeIdentity(offsetRotateMatrix);
+  if (rotation !== 0) {
+    goog.vec.Mat4.rotateZ(offsetRotateMatrix, -rotation);
+  }
+
+  gl.uniformMatrix4fv(locations.u_projectionMatrix, false, projectionMatrix);
+  gl.uniformMatrix4fv(locations.u_offsetScaleMatrix, false, offsetScaleMatrix);
+  gl.uniformMatrix4fv(locations.u_offsetRotateMatrix, false,
+      offsetRotateMatrix);
+  gl.uniform1f(locations.u_opacity, opacity);
+
+  // draw!
+  var result;
+  if (featureCallback === undefined) {
+    this.drawReplay_(gl, context, skippedFeaturesHash,
+        this.textures_, this.groupIndices_);
+  } else {
+    // draw feature by feature for the hit-detection
+    result = this.drawHitDetectionReplay_(gl, context, skippedFeaturesHash,
+        featureCallback, oneByOne, opt_hitExtent);
+  }
+
+  // disable the vertex attrib arrays
+  gl.disableVertexAttribArray(locations.a_position);
+  gl.disableVertexAttribArray(locations.a_offsets);
+  gl.disableVertexAttribArray(locations.a_texCoord);
+  gl.disableVertexAttribArray(locations.a_opacity);
+  gl.disableVertexAttribArray(locations.a_rotateWithView);
+
+  return result;
+};
+
+
+/**
+ * @private
+ * @param {WebGLRenderingContext} gl gl.
+ * @param {ol.webgl.Context} context Context.
+ * @param {Object.<string, boolean>} skippedFeaturesHash Ids of features
+ *  to skip.
+ * @param {Array.<WebGLTexture>} textures Textures.
+ * @param {Array.<number>} groupIndices Texture group indices.
+ */
+ol.render.webgl.ImageReplay.prototype.drawReplay_ = function(gl, context, skippedFeaturesHash, textures, groupIndices) {
+  goog.asserts.assert(textures.length === groupIndices.length,
+      'number of textures and groupIndeces match');
+  var elementType = context.hasOESElementIndexUint ?
+      goog.webgl.UNSIGNED_INT : goog.webgl.UNSIGNED_SHORT;
+  var elementSize = context.hasOESElementIndexUint ? 4 : 2;
+
+  if (!ol.object.isEmpty(skippedFeaturesHash)) {
+    this.drawReplaySkipping_(
+        gl, skippedFeaturesHash, textures, groupIndices,
+        elementType, elementSize);
+  } else {
+    var i, ii, start;
+    for (i = 0, ii = textures.length, start = 0; i < ii; ++i) {
+      gl.bindTexture(goog.webgl.TEXTURE_2D, textures[i]);
+      var end = groupIndices[i];
+      this.drawElements_(gl, start, end, elementType, elementSize);
+      start = end;
+    }
+  }
+};
+
+
+/**
+ * Draw the replay while paying attention to skipped features.
+ *
+ * This functions creates groups of features that can be drawn to together,
+ * so that the number of `drawElements` calls is minimized.
+ *
+ * For example given the following texture groups:
+ *
+ *    Group 1: A B C
+ *    Group 2: D [E] F G
+ *
+ * If feature E should be skipped, the following `drawElements` calls will be
+ * made:
+ *
+ *    drawElements with feature A, B and C
+ *    drawElements with feature D
+ *    drawElements with feature F and G
+ *
+ * @private
+ * @param {WebGLRenderingContext} gl gl.
+ * @param {Object.<string, boolean>} skippedFeaturesHash Ids of features
+ *  to skip.
+ * @param {Array.<WebGLTexture>} textures Textures.
+ * @param {Array.<number>} groupIndices Texture group indices.
+ * @param {number} elementType Element type.
+ * @param {number} elementSize Element Size.
+ */
+ol.render.webgl.ImageReplay.prototype.drawReplaySkipping_ = function(gl, skippedFeaturesHash, textures, groupIndices,
+    elementType, elementSize) {
+  var featureIndex = 0;
+
+  var i, ii;
+  for (i = 0, ii = textures.length; i < ii; ++i) {
+    gl.bindTexture(goog.webgl.TEXTURE_2D, textures[i]);
+    var groupStart = (i > 0) ? groupIndices[i - 1] : 0;
+    var groupEnd = groupIndices[i];
+
+    var start = groupStart;
+    var end = groupStart;
+    while (featureIndex < this.startIndices_.length &&
+        this.startIndices_[featureIndex] <= groupEnd) {
+      var feature = this.startIndicesFeature_[featureIndex];
+
+      var featureUid = goog.getUid(feature).toString();
+      if (skippedFeaturesHash[featureUid] !== undefined) {
+        // feature should be skipped
+        if (start !== end) {
+          // draw the features so far
+          this.drawElements_(gl, start, end, elementType, elementSize);
+        }
+        // continue with the next feature
+        start = (featureIndex === this.startIndices_.length - 1) ?
+            groupEnd : this.startIndices_[featureIndex + 1];
+        end = start;
+      } else {
+        // the feature is not skipped, augment the end index
+        end = (featureIndex === this.startIndices_.length - 1) ?
+            groupEnd : this.startIndices_[featureIndex + 1];
+      }
+      featureIndex++;
+    }
+
+    if (start !== end) {
+      // draw the remaining features (in case there was no skipped feature
+      // in this texture group, all features of a group are drawn together)
+      this.drawElements_(gl, start, end, elementType, elementSize);
+    }
+  }
+};
+
+
+/**
+ * @private
+ * @param {WebGLRenderingContext} gl gl.
+ * @param {number} start Start index.
+ * @param {number} end End index.
+ * @param {number} elementType Element type.
+ * @param {number} elementSize Element Size.
+ */
+ol.render.webgl.ImageReplay.prototype.drawElements_ = function(
+    gl, start, end, elementType, elementSize) {
+  var numItems = end - start;
+  var offsetInBytes = start * elementSize;
+  gl.drawElements(goog.webgl.TRIANGLES, numItems, elementType, offsetInBytes);
+};
+
+
+/**
+ * @private
+ * @param {WebGLRenderingContext} gl gl.
+ * @param {ol.webgl.Context} context Context.
+ * @param {Object.<string, boolean>} skippedFeaturesHash Ids of features
+ *  to skip.
+ * @param {function((ol.Feature|ol.render.Feature)): T|undefined} featureCallback Feature callback.
+ * @param {boolean} oneByOne Draw features one-by-one for the hit-detecion.
+ * @param {ol.Extent=} opt_hitExtent Hit extent: Only features intersecting
+ *  this extent are checked.
+ * @return {T|undefined} Callback result.
+ * @template T
+ */
+ol.render.webgl.ImageReplay.prototype.drawHitDetectionReplay_ = function(gl, context, skippedFeaturesHash, featureCallback, oneByOne,
+    opt_hitExtent) {
+  if (!oneByOne) {
+    // draw all hit-detection features in "once" (by texture group)
+    return this.drawHitDetectionReplayAll_(gl, context,
+        skippedFeaturesHash, featureCallback);
+  } else {
+    // draw hit-detection features one by one
+    return this.drawHitDetectionReplayOneByOne_(gl, context,
+        skippedFeaturesHash, featureCallback, opt_hitExtent);
+  }
+};
+
+
+/**
+ * @private
+ * @param {WebGLRenderingContext} gl gl.
+ * @param {ol.webgl.Context} context Context.
+ * @param {Object.<string, boolean>} skippedFeaturesHash Ids of features
+ *  to skip.
+ * @param {function((ol.Feature|ol.render.Feature)): T|undefined} featureCallback Feature callback.
+ * @return {T|undefined} Callback result.
+ * @template T
+ */
+ol.render.webgl.ImageReplay.prototype.drawHitDetectionReplayAll_ = function(gl, context, skippedFeaturesHash, featureCallback) {
+  gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
+  this.drawReplay_(gl, context, skippedFeaturesHash,
+      this.hitDetectionTextures_, this.hitDetectionGroupIndices_);
+
+  var result = featureCallback(null);
+  if (result) {
+    return result;
+  } else {
+    return undefined;
+  }
+};
+
+
+/**
+ * @private
+ * @param {WebGLRenderingContext} gl gl.
+ * @param {ol.webgl.Context} context Context.
+ * @param {Object.<string, boolean>} skippedFeaturesHash Ids of features
+ *  to skip.
+ * @param {function((ol.Feature|ol.render.Feature)): T|undefined} featureCallback Feature callback.
+ * @param {ol.Extent=} opt_hitExtent Hit extent: Only features intersecting
+ *  this extent are checked.
+ * @return {T|undefined} Callback result.
+ * @template T
+ */
+ol.render.webgl.ImageReplay.prototype.drawHitDetectionReplayOneByOne_ = function(gl, context, skippedFeaturesHash, featureCallback,
+    opt_hitExtent) {
+  goog.asserts.assert(this.hitDetectionTextures_.length ===
+      this.hitDetectionGroupIndices_.length,
+      'number of hitDetectionTextures and hitDetectionGroupIndices match');
+  var elementType = context.hasOESElementIndexUint ?
+      goog.webgl.UNSIGNED_INT : goog.webgl.UNSIGNED_SHORT;
+  var elementSize = context.hasOESElementIndexUint ? 4 : 2;
+
+  var i, groupStart, start, end, feature, featureUid;
+  var featureIndex = this.startIndices_.length - 1;
+  for (i = this.hitDetectionTextures_.length - 1; i >= 0; --i) {
+    gl.bindTexture(goog.webgl.TEXTURE_2D, this.hitDetectionTextures_[i]);
+    groupStart = (i > 0) ? this.hitDetectionGroupIndices_[i - 1] : 0;
+    end = this.hitDetectionGroupIndices_[i];
+
+    // draw all features for this texture group
+    while (featureIndex >= 0 &&
+        this.startIndices_[featureIndex] >= groupStart) {
+      start = this.startIndices_[featureIndex];
+      feature = this.startIndicesFeature_[featureIndex];
+      featureUid = goog.getUid(feature).toString();
+
+      if (skippedFeaturesHash[featureUid] === undefined &&
+          feature.getGeometry() &&
+          (opt_hitExtent === undefined || ol.extent.intersects(
+              /** @type {Array<number>} */ (opt_hitExtent),
+              feature.getGeometry().getExtent()))) {
+        gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
+        this.drawElements_(gl, start, end, elementType, elementSize);
+
+        var result = featureCallback(feature);
+        if (result) {
+          return result;
+        }
+      }
+
+      end = start;
+      featureIndex--;
+    }
+  }
+  return undefined;
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.render.webgl.ImageReplay.prototype.setFillStrokeStyle = goog.abstractMethod;
+
+
+/**
+ * @inheritDoc
+ */
+ol.render.webgl.ImageReplay.prototype.setImageStyle = function(imageStyle) {
+  var anchor = imageStyle.getAnchor();
+  var image = imageStyle.getImage(1);
+  var imageSize = imageStyle.getImageSize();
+  var hitDetectionImage = imageStyle.getHitDetectionImage(1);
+  var hitDetectionImageSize = imageStyle.getHitDetectionImageSize();
+  var opacity = imageStyle.getOpacity();
+  var origin = imageStyle.getOrigin();
+  var rotateWithView = imageStyle.getRotateWithView();
+  var rotation = imageStyle.getRotation();
+  var size = imageStyle.getSize();
+  var scale = imageStyle.getScale();
+  goog.asserts.assert(anchor, 'imageStyle anchor is not null');
+  goog.asserts.assert(image, 'imageStyle image is not null');
+  goog.asserts.assert(imageSize,
+      'imageStyle imageSize is not null');
+  goog.asserts.assert(hitDetectionImage,
+      'imageStyle hitDetectionImage is not null');
+  goog.asserts.assert(hitDetectionImageSize,
+      'imageStyle hitDetectionImageSize is not null');
+  goog.asserts.assert(opacity !== undefined, 'imageStyle opacity is defined');
+  goog.asserts.assert(origin, 'imageStyle origin is not null');
+  goog.asserts.assert(rotateWithView !== undefined,
+      'imageStyle rotateWithView is defined');
+  goog.asserts.assert(rotation !== undefined, 'imageStyle rotation is defined');
+  goog.asserts.assert(size, 'imageStyle size is not null');
+  goog.asserts.assert(scale !== undefined, 'imageStyle scale is defined');
+
+  var currentImage;
+  if (this.images_.length === 0) {
+    this.images_.push(image);
+  } else {
+    currentImage = this.images_[this.images_.length - 1];
+    if (goog.getUid(currentImage) != goog.getUid(image)) {
+      this.groupIndices_.push(this.indices_.length);
+      goog.asserts.assert(this.groupIndices_.length === this.images_.length,
+          'number of groupIndices and images match');
+      this.images_.push(image);
+    }
+  }
+
+  if (this.hitDetectionImages_.length === 0) {
+    this.hitDetectionImages_.push(hitDetectionImage);
+  } else {
+    currentImage =
+        this.hitDetectionImages_[this.hitDetectionImages_.length - 1];
+    if (goog.getUid(currentImage) != goog.getUid(hitDetectionImage)) {
+      this.hitDetectionGroupIndices_.push(this.indices_.length);
+      goog.asserts.assert(this.hitDetectionGroupIndices_.length ===
+          this.hitDetectionImages_.length,
+          'number of hitDetectionGroupIndices and hitDetectionImages match');
+      this.hitDetectionImages_.push(hitDetectionImage);
+    }
+  }
+
+  this.anchorX_ = anchor[0];
+  this.anchorY_ = anchor[1];
+  this.height_ = size[1];
+  this.imageHeight_ = imageSize[1];
+  this.imageWidth_ = imageSize[0];
+  this.opacity_ = opacity;
+  this.originX_ = origin[0];
+  this.originY_ = origin[1];
+  this.rotation_ = rotation;
+  this.rotateWithView_ = rotateWithView;
+  this.scale_ = scale;
+  this.width_ = size[0];
+};
+
+
+/**
+ * @constructor
+ * @implements {ol.render.IReplayGroup}
+ * @param {number} tolerance Tolerance.
+ * @param {ol.Extent} maxExtent Max extent.
+ * @param {number=} opt_renderBuffer Render buffer.
+ * @struct
+ */
+ol.render.webgl.ReplayGroup = function(
+    tolerance, maxExtent, opt_renderBuffer) {
+
+  /**
+   * @type {ol.Extent}
+   * @private
+   */
+  this.maxExtent_ = maxExtent;
+
+  /**
+   * @type {number}
+   * @private
+   */
+  this.tolerance_ = tolerance;
+
+  /**
+   * @type {number|undefined}
+   * @private
+   */
+  this.renderBuffer_ = opt_renderBuffer;
+
+  /**
+   * ImageReplay only is supported at this point.
+   * @type {Object.<ol.render.ReplayType, ol.render.webgl.ImageReplay>}
+   * @private
+   */
+  this.replays_ = {};
+
+};
+
+
+/**
+ * @param {ol.webgl.Context} context WebGL context.
+ * @return {function()} Delete resources function.
+ */
+ol.render.webgl.ReplayGroup.prototype.getDeleteResourcesFunction = function(context) {
+  var functions = [];
+  var replayKey;
+  for (replayKey in this.replays_) {
+    functions.push(
+        this.replays_[replayKey].getDeleteResourcesFunction(context));
+  }
+  return function() {
+    var length = functions.length;
+    var result;
+    for (var i = 0; i < length; i++) {
+      result = functions[i].apply(this, arguments);
+    }
+    return result;
+  };
+};
+
+
+/**
+ * @param {ol.webgl.Context} context Context.
+ */
+ol.render.webgl.ReplayGroup.prototype.finish = function(context) {
+  var replayKey;
+  for (replayKey in this.replays_) {
+    this.replays_[replayKey].finish(context);
+  }
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.render.webgl.ReplayGroup.prototype.getReplay = function(zIndex, replayType) {
+  var replay = this.replays_[replayType];
+  if (replay === undefined) {
+    var constructor = ol.render.webgl.BATCH_CONSTRUCTORS_[replayType];
+    goog.asserts.assert(constructor !== undefined,
+        replayType +
+        ' constructor missing from ol.render.webgl.BATCH_CONSTRUCTORS_');
+    replay = new constructor(this.tolerance_, this.maxExtent_);
+    this.replays_[replayType] = replay;
+  }
+  return replay;
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.render.webgl.ReplayGroup.prototype.isEmpty = function() {
+  return ol.object.isEmpty(this.replays_);
+};
+
+
+/**
+ * @param {ol.webgl.Context} context Context.
+ * @param {ol.Coordinate} center Center.
+ * @param {number} resolution Resolution.
+ * @param {number} rotation Rotation.
+ * @param {ol.Size} size Size.
+ * @param {number} pixelRatio Pixel ratio.
+ * @param {number} opacity Global opacity.
+ * @param {Object.<string, boolean>} skippedFeaturesHash Ids of features
+ *  to skip.
+ */
+ol.render.webgl.ReplayGroup.prototype.replay = function(context,
+    center, resolution, rotation, size, pixelRatio,
+    opacity, skippedFeaturesHash) {
+  var i, ii, replay;
+  for (i = 0, ii = ol.render.REPLAY_ORDER.length; i < ii; ++i) {
+    replay = this.replays_[ol.render.REPLAY_ORDER[i]];
+    if (replay !== undefined) {
+      replay.replay(context,
+          center, resolution, rotation, size, pixelRatio,
+          opacity, skippedFeaturesHash,
+          undefined, false);
+    }
+  }
+};
+
+
+/**
+ * @private
+ * @param {ol.webgl.Context} context Context.
+ * @param {ol.Coordinate} center Center.
+ * @param {number} resolution Resolution.
+ * @param {number} rotation Rotation.
+ * @param {ol.Size} size Size.
+ * @param {number} pixelRatio Pixel ratio.
+ * @param {number} opacity Global opacity.
+ * @param {Object.<string, boolean>} skippedFeaturesHash Ids of features
+ *  to skip.
+ * @param {function((ol.Feature|ol.render.Feature)): T|undefined} featureCallback Feature callback.
+ * @param {boolean} oneByOne Draw features one-by-one for the hit-detecion.
+ * @param {ol.Extent=} opt_hitExtent Hit extent: Only features intersecting
+ *  this extent are checked.
+ * @return {T|undefined} Callback result.
+ * @template T
+ */
+ol.render.webgl.ReplayGroup.prototype.replayHitDetection_ = function(context,
+    center, resolution, rotation, size, pixelRatio, opacity,
+    skippedFeaturesHash, featureCallback, oneByOne, opt_hitExtent) {
+  var i, replay, result;
+  for (i = ol.render.REPLAY_ORDER.length - 1; i >= 0; --i) {
+    replay = this.replays_[ol.render.REPLAY_ORDER[i]];
+    if (replay !== undefined) {
+      result = replay.replay(context,
+          center, resolution, rotation, size, pixelRatio, opacity,
+          skippedFeaturesHash, featureCallback, oneByOne, opt_hitExtent);
+      if (result) {
+        return result;
+      }
+    }
+  }
+  return undefined;
+};
+
+
+/**
+ * @param {ol.Coordinate} coordinate Coordinate.
+ * @param {ol.webgl.Context} context Context.
+ * @param {ol.Coordinate} center Center.
+ * @param {number} resolution Resolution.
+ * @param {number} rotation Rotation.
+ * @param {ol.Size} size Size.
+ * @param {number} pixelRatio Pixel ratio.
+ * @param {number} opacity Global opacity.
+ * @param {Object.<string, boolean>} skippedFeaturesHash Ids of features
+ *  to skip.
+ * @param {function((ol.Feature|ol.render.Feature)): T|undefined} callback Feature callback.
+ * @return {T|undefined} Callback result.
+ * @template T
+ */
+ol.render.webgl.ReplayGroup.prototype.forEachFeatureAtCoordinate = function(
+    coordinate, context, center, resolution, rotation, size, pixelRatio,
+    opacity, skippedFeaturesHash,
+    callback) {
+  var gl = context.getGL();
+  gl.bindFramebuffer(
+      gl.FRAMEBUFFER, context.getHitDetectionFramebuffer());
+
+
+  /**
+   * @type {ol.Extent}
+   */
+  var hitExtent;
+  if (this.renderBuffer_ !== undefined) {
+    // build an extent around the coordinate, so that only features that
+    // intersect this extent are checked
+    hitExtent = ol.extent.buffer(
+        ol.extent.createOrUpdateFromCoordinate(coordinate),
+        resolution * this.renderBuffer_);
+  }
+
+  return this.replayHitDetection_(context,
+      coordinate, resolution, rotation, ol.render.webgl.HIT_DETECTION_SIZE_,
+      pixelRatio, opacity, skippedFeaturesHash,
+      /**
+       * @param {ol.Feature|ol.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 {ol.Coordinate} coordinate Coordinate.
+ * @param {ol.webgl.Context} context Context.
+ * @param {ol.Coordinate} center Center.
+ * @param {number} resolution Resolution.
+ * @param {number} rotation Rotation.
+ * @param {ol.Size} size Size.
+ * @param {number} pixelRatio Pixel ratio.
+ * @param {number} opacity Global opacity.
+ * @param {Object.<string, boolean>} skippedFeaturesHash Ids of features
+ *  to skip.
+ * @return {boolean} Is there a feature at the given coordinate?
+ */
+ol.render.webgl.ReplayGroup.prototype.hasFeatureAtCoordinate = function(
+    coordinate, context, center, resolution, rotation, size, pixelRatio,
+    opacity, skippedFeaturesHash) {
+  var gl = context.getGL();
+  gl.bindFramebuffer(
+      gl.FRAMEBUFFER, context.getHitDetectionFramebuffer());
+
+  var hasFeature = this.replayHitDetection_(context,
+      coordinate, resolution, rotation, ol.render.webgl.HIT_DETECTION_SIZE_,
+      pixelRatio, opacity, skippedFeaturesHash,
+      /**
+       * @param {ol.Feature|ol.render.Feature} feature Feature.
+       * @return {boolean} Is there a feature?
+       */
+      function(feature) {
+        var imageData = new Uint8Array(4);
+        gl.readPixels(0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, imageData);
+        return imageData[3] > 0;
+      }, false);
+
+  return hasFeature !== undefined;
+};
+
+
+/**
+ * @const
+ * @private
+ * @type {Object.<ol.render.ReplayType,
+ *                function(new: ol.render.webgl.ImageReplay, number,
+ *                ol.Extent)>}
+ */
+ol.render.webgl.BATCH_CONSTRUCTORS_ = {
+  'Image': ol.render.webgl.ImageReplay
+};
+
+
+/**
+ * @const
+ * @private
+ * @type {Array.<number>}
+ */
+ol.render.webgl.HIT_DETECTION_SIZE_ = [1, 1];
+
+goog.provide('ol.render.webgl.Immediate');
+goog.require('goog.asserts');
+goog.require('ol.extent');
+goog.require('ol.geom.GeometryType');
+goog.require('ol.render.VectorContext');
+goog.require('ol.render.webgl.ImageReplay');
+goog.require('ol.render.webgl.ReplayGroup');
+
+
+/**
+ * @constructor
+ * @extends {ol.render.VectorContext}
+ * @param {ol.webgl.Context} context Context.
+ * @param {ol.Coordinate} center Center.
+ * @param {number} resolution Resolution.
+ * @param {number} rotation Rotation.
+ * @param {ol.Size} size Size.
+ * @param {ol.Extent} extent Extent.
+ * @param {number} pixelRatio Pixel ratio.
+ * @struct
+ */
+ol.render.webgl.Immediate = function(context, center, resolution, rotation, size, extent, pixelRatio) {
+  ol.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.resolution_ = resolution;
+
+  /**
+   * @private
+   * @type {ol.style.Image}
+   */
+  this.imageStyle_ = null;
+
+};
+ol.inherits(ol.render.webgl.Immediate, ol.render.VectorContext);
+
+
+/**
+ * Set the rendering style.  Note that since this is an immediate rendering API,
+ * any `zIndex` on the provided style will be ignored.
+ *
+ * @param {ol.style.Style} style The rendering style.
+ * @api
+ */
+ol.render.webgl.Immediate.prototype.setStyle = function(style) {
+  this.setImageStyle(style.getImage());
+};
+
+
+/**
+ * Render a geometry into the canvas.  Call
+ * {@link ol.render.webgl.Immediate#setStyle} first to set the rendering style.
+ *
+ * @param {ol.geom.Geometry|ol.render.Feature} geometry The geometry to render.
+ * @api
+ */
+ol.render.webgl.Immediate.prototype.drawGeometry = function(geometry) {
+  var type = geometry.getType();
+  switch (type) {
+    case ol.geom.GeometryType.POINT:
+      this.drawPoint(/** @type {ol.geom.Point} */ (geometry), null);
+      break;
+    case ol.geom.GeometryType.MULTI_POINT:
+      this.drawMultiPoint(/** @type {ol.geom.MultiPoint} */ (geometry), null);
+      break;
+    case ol.geom.GeometryType.GEOMETRY_COLLECTION:
+      this.drawGeometryCollection(/** @type {ol.geom.GeometryCollection} */ (geometry), null);
+      break;
+    default:
+      goog.asserts.fail('Unsupported geometry type: ' + type);
+  }
+};
+
+
+/**
+ * @inheritDoc
+ * @api
+ */
+ol.render.webgl.Immediate.prototype.drawFeature = function(feature, style) {
+  var geometry = style.getGeometryFunction()(feature);
+  if (!geometry ||
+      !ol.extent.intersects(this.extent_, geometry.getExtent())) {
+    return;
+  }
+  this.setStyle(style);
+  goog.asserts.assert(geometry, 'geometry must be truthy');
+  this.drawGeometry(geometry);
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.render.webgl.Immediate.prototype.drawGeometryCollection = function(geometry, data) {
+  var geometries = geometry.getGeometriesArray();
+  var i, ii;
+  for (i = 0, ii = geometries.length; i < ii; ++i) {
+    this.drawGeometry(geometries[i]);
+  }
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.render.webgl.Immediate.prototype.drawPoint = function(geometry, data) {
+  var context = this.context_;
+  var replayGroup = new ol.render.webgl.ReplayGroup(1, this.extent_);
+  var replay = /** @type {ol.render.webgl.ImageReplay} */ (
+      replayGroup.getReplay(0, ol.render.ReplayType.IMAGE));
+  replay.setImageStyle(this.imageStyle_);
+  replay.drawPoint(geometry, data);
+  replay.finish(context);
+  // default colors
+  var opacity = 1;
+  var skippedFeatures = {};
+  var featureCallback;
+  var oneByOne = false;
+  replay.replay(this.context_, this.center_, this.resolution_, this.rotation_,
+      this.size_, this.pixelRatio_, opacity, skippedFeatures, featureCallback,
+      oneByOne);
+  replay.getDeleteResourcesFunction(context)();
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.render.webgl.Immediate.prototype.drawMultiPoint = function(geometry, data) {
+  var context = this.context_;
+  var replayGroup = new ol.render.webgl.ReplayGroup(1, this.extent_);
+  var replay = /** @type {ol.render.webgl.ImageReplay} */ (
+      replayGroup.getReplay(0, ol.render.ReplayType.IMAGE));
+  replay.setImageStyle(this.imageStyle_);
+  replay.drawMultiPoint(geometry, data);
+  replay.finish(context);
+  var opacity = 1;
+  var skippedFeatures = {};
+  var featureCallback;
+  var oneByOne = false;
+  replay.replay(this.context_, this.center_, this.resolution_, this.rotation_,
+      this.size_, this.pixelRatio_, opacity, skippedFeatures, featureCallback,
+      oneByOne);
+  replay.getDeleteResourcesFunction(context)();
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.render.webgl.Immediate.prototype.setImageStyle = function(imageStyle) {
+  this.imageStyle_ = imageStyle;
+};
+
+// This file is automatically generated, do not edit
+goog.provide('ol.renderer.webgl.map.shader.Default');
+goog.provide('ol.renderer.webgl.map.shader.Default.Locations');
+goog.provide('ol.renderer.webgl.map.shader.DefaultFragment');
+goog.provide('ol.renderer.webgl.map.shader.DefaultVertex');
+
+goog.require('ol.webgl.shader');
+
+
+/**
+ * @constructor
+ * @extends {ol.webgl.shader.Fragment}
+ * @struct
+ */
+ol.renderer.webgl.map.shader.DefaultFragment = function() {
+  ol.webgl.shader.Fragment.call(this, ol.renderer.webgl.map.shader.DefaultFragment.SOURCE);
+};
+ol.inherits(ol.renderer.webgl.map.shader.DefaultFragment, ol.webgl.shader.Fragment);
+goog.addSingletonGetter(ol.renderer.webgl.map.shader.DefaultFragment);
+
+
+/**
+ * @const
+ * @type {string}
+ */
+ol.renderer.webgl.map.shader.DefaultFragment.DEBUG_SOURCE = 'precision mediump float;\nvarying vec2 v_texCoord;\n\n\nuniform float u_opacity;\nuniform sampler2D u_texture;\n\nvoid main(void) {\n  vec4 texColor = texture2D(u_texture, v_texCoord);\n  gl_FragColor.rgb = texColor.rgb;\n  gl_FragColor.a = texColor.a * u_opacity;\n}\n';
+
+
+/**
+ * @const
+ * @type {string}
+ */
+ol.renderer.webgl.map.shader.DefaultFragment.OPTIMIZED_SOURCE = 'precision mediump float;varying vec2 a;uniform float f;uniform sampler2D g;void main(void){vec4 texColor=texture2D(g,a);gl_FragColor.rgb=texColor.rgb;gl_FragColor.a=texColor.a*f;}';
+
+
+/**
+ * @const
+ * @type {string}
+ */
+ol.renderer.webgl.map.shader.DefaultFragment.SOURCE = goog.DEBUG ?
+    ol.renderer.webgl.map.shader.DefaultFragment.DEBUG_SOURCE :
+    ol.renderer.webgl.map.shader.DefaultFragment.OPTIMIZED_SOURCE;
+
+
+/**
+ * @constructor
+ * @extends {ol.webgl.shader.Vertex}
+ * @struct
+ */
+ol.renderer.webgl.map.shader.DefaultVertex = function() {
+  ol.webgl.shader.Vertex.call(this, ol.renderer.webgl.map.shader.DefaultVertex.SOURCE);
+};
+ol.inherits(ol.renderer.webgl.map.shader.DefaultVertex, ol.webgl.shader.Vertex);
+goog.addSingletonGetter(ol.renderer.webgl.map.shader.DefaultVertex);
+
+
+/**
+ * @const
+ * @type {string}
+ */
+ol.renderer.webgl.map.shader.DefaultVertex.DEBUG_SOURCE = 'varying vec2 v_texCoord;\n\n\nattribute vec2 a_position;\nattribute vec2 a_texCoord;\n\nuniform mat4 u_texCoordMatrix;\nuniform mat4 u_projectionMatrix;\n\nvoid main(void) {\n  gl_Position = u_projectionMatrix * vec4(a_position, 0., 1.);\n  v_texCoord = (u_texCoordMatrix * vec4(a_texCoord, 0., 1.)).st;\n}\n\n\n';
+
+
+/**
+ * @const
+ * @type {string}
+ */
+ol.renderer.webgl.map.shader.DefaultVertex.OPTIMIZED_SOURCE = 'varying vec2 a;attribute vec2 b;attribute vec2 c;uniform mat4 d;uniform mat4 e;void main(void){gl_Position=e*vec4(b,0.,1.);a=(d*vec4(c,0.,1.)).st;}';
+
+
+/**
+ * @const
+ * @type {string}
+ */
+ol.renderer.webgl.map.shader.DefaultVertex.SOURCE = goog.DEBUG ?
+    ol.renderer.webgl.map.shader.DefaultVertex.DEBUG_SOURCE :
+    ol.renderer.webgl.map.shader.DefaultVertex.OPTIMIZED_SOURCE;
+
+
+/**
+ * @constructor
+ * @param {WebGLRenderingContext} gl GL.
+ * @param {WebGLProgram} program Program.
+ * @struct
+ */
+ol.renderer.webgl.map.shader.Default.Locations = function(gl, program) {
+
+  /**
+   * @type {WebGLUniformLocation}
+   */
+  this.u_opacity = gl.getUniformLocation(
+      program, goog.DEBUG ? 'u_opacity' : 'f');
+
+  /**
+   * @type {WebGLUniformLocation}
+   */
+  this.u_projectionMatrix = gl.getUniformLocation(
+      program, goog.DEBUG ? 'u_projectionMatrix' : 'e');
+
+  /**
+   * @type {WebGLUniformLocation}
+   */
+  this.u_texCoordMatrix = gl.getUniformLocation(
+      program, goog.DEBUG ? 'u_texCoordMatrix' : 'd');
+
+  /**
+   * @type {WebGLUniformLocation}
+   */
+  this.u_texture = gl.getUniformLocation(
+      program, goog.DEBUG ? 'u_texture' : 'g');
+
+  /**
+   * @type {number}
+   */
+  this.a_position = gl.getAttribLocation(
+      program, goog.DEBUG ? 'a_position' : 'b');
+
+  /**
+   * @type {number}
+   */
+  this.a_texCoord = gl.getAttribLocation(
+      program, goog.DEBUG ? 'a_texCoord' : 'c');
+};
+
+goog.provide('ol.renderer.webgl.Layer');
+
+goog.require('goog.vec.Mat4');
+goog.require('goog.webgl');
+goog.require('ol.layer.Layer');
+goog.require('ol.render.Event');
+goog.require('ol.render.EventType');
+goog.require('ol.render.webgl.Immediate');
+goog.require('ol.renderer.Layer');
+goog.require('ol.renderer.webgl.map.shader.Default');
+goog.require('ol.renderer.webgl.map.shader.Default.Locations');
+goog.require('ol.renderer.webgl.map.shader.DefaultFragment');
+goog.require('ol.renderer.webgl.map.shader.DefaultVertex');
+goog.require('ol.webgl.Buffer');
+goog.require('ol.webgl.Context');
+
+
+/**
+ * @constructor
+ * @extends {ol.renderer.Layer}
+ * @param {ol.renderer.webgl.Map} mapRenderer Map renderer.
+ * @param {ol.layer.Layer} layer Layer.
+ */
+ol.renderer.webgl.Layer = function(mapRenderer, layer) {
+
+  ol.renderer.Layer.call(this, layer);
+
+  /**
+   * @protected
+   * @type {ol.renderer.webgl.Map}
+   */
+  this.mapRenderer = mapRenderer;
+
+  /**
+   * @private
+   * @type {ol.webgl.Buffer}
+   */
+  this.arrayBuffer_ = new ol.webgl.Buffer([
+    -1, -1, 0, 0,
+    1, -1, 1, 0,
+    -1, 1, 0, 1,
+    1, 1, 1, 1
+  ]);
+
+  /**
+   * @protected
+   * @type {WebGLTexture}
+   */
+  this.texture = null;
+
+  /**
+   * @protected
+   * @type {WebGLFramebuffer}
+   */
+  this.framebuffer = null;
+
+  /**
+   * @protected
+   * @type {number|undefined}
+   */
+  this.framebufferDimension = undefined;
+
+  /**
+   * @protected
+   * @type {!goog.vec.Mat4.Number}
+   */
+  this.texCoordMatrix = goog.vec.Mat4.createNumber();
+
+  /**
+   * @protected
+   * @type {!goog.vec.Mat4.Number}
+   */
+  this.projectionMatrix = goog.vec.Mat4.createNumberIdentity();
+
+  /**
+   * @private
+   * @type {ol.renderer.webgl.map.shader.Default.Locations}
+   */
+  this.defaultLocations_ = null;
+
+};
+ol.inherits(ol.renderer.webgl.Layer, ol.renderer.Layer);
+
+
+/**
+ * @param {olx.FrameState} frameState Frame state.
+ * @param {number} framebufferDimension Framebuffer dimension.
+ * @protected
+ */
+ol.renderer.webgl.Layer.prototype.bindFramebuffer = function(frameState, framebufferDimension) {
+
+  var gl = this.mapRenderer.getGL();
+
+  if (this.framebufferDimension === undefined ||
+      this.framebufferDimension != framebufferDimension) {
+    /**
+     * @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 {ol.PostRenderFunction} */ (postRenderFunction)
+    );
+
+    var texture = ol.webgl.Context.createEmptyTexture(
+        gl, framebufferDimension, framebufferDimension);
+
+    var framebuffer = gl.createFramebuffer();
+    gl.bindFramebuffer(goog.webgl.FRAMEBUFFER, framebuffer);
+    gl.framebufferTexture2D(goog.webgl.FRAMEBUFFER,
+        goog.webgl.COLOR_ATTACHMENT0, goog.webgl.TEXTURE_2D, texture, 0);
+
+    this.texture = texture;
+    this.framebuffer = framebuffer;
+    this.framebufferDimension = framebufferDimension;
+
+  } else {
+    gl.bindFramebuffer(goog.webgl.FRAMEBUFFER, this.framebuffer);
+  }
+
+};
+
+
+/**
+ * @param {olx.FrameState} frameState Frame state.
+ * @param {ol.LayerState} layerState Layer state.
+ * @param {ol.webgl.Context} context Context.
+ */
+ol.renderer.webgl.Layer.prototype.composeFrame = function(frameState, layerState, context) {
+
+  this.dispatchComposeEvent_(
+      ol.render.EventType.PRECOMPOSE, context, frameState);
+
+  context.bindBuffer(goog.webgl.ARRAY_BUFFER, this.arrayBuffer_);
+
+  var gl = context.getGL();
+
+  var fragmentShader =
+      ol.renderer.webgl.map.shader.DefaultFragment.getInstance();
+  var vertexShader = ol.renderer.webgl.map.shader.DefaultVertex.getInstance();
+
+  var program = context.getProgram(fragmentShader, vertexShader);
+
+  var locations;
+  if (!this.defaultLocations_) {
+    locations =
+        new ol.renderer.webgl.map.shader.Default.Locations(gl, program);
+    this.defaultLocations_ = locations;
+  } else {
+    locations = this.defaultLocations_;
+  }
+
+  if (context.useProgram(program)) {
+    gl.enableVertexAttribArray(locations.a_position);
+    gl.vertexAttribPointer(
+        locations.a_position, 2, goog.webgl.FLOAT, false, 16, 0);
+    gl.enableVertexAttribArray(locations.a_texCoord);
+    gl.vertexAttribPointer(
+        locations.a_texCoord, 2, goog.webgl.FLOAT, false, 16, 8);
+    gl.uniform1i(locations.u_texture, 0);
+  }
+
+  gl.uniformMatrix4fv(
+      locations.u_texCoordMatrix, false, this.getTexCoordMatrix());
+  gl.uniformMatrix4fv(locations.u_projectionMatrix, false,
+      this.getProjectionMatrix());
+  gl.uniform1f(locations.u_opacity, layerState.opacity);
+  gl.bindTexture(goog.webgl.TEXTURE_2D, this.getTexture());
+  gl.drawArrays(goog.webgl.TRIANGLE_STRIP, 0, 4);
+
+  this.dispatchComposeEvent_(
+      ol.render.EventType.POSTCOMPOSE, context, frameState);
+
+};
+
+
+/**
+ * @param {ol.render.EventType} type Event type.
+ * @param {ol.webgl.Context} context WebGL context.
+ * @param {olx.FrameState} frameState Frame state.
+ * @private
+ */
+ol.renderer.webgl.Layer.prototype.dispatchComposeEvent_ = function(type, context, frameState) {
+  var layer = this.getLayer();
+  if (layer.hasListener(type)) {
+    var viewState = frameState.viewState;
+    var resolution = viewState.resolution;
+    var pixelRatio = frameState.pixelRatio;
+    var extent = frameState.extent;
+    var center = viewState.center;
+    var rotation = viewState.rotation;
+    var size = frameState.size;
+
+    var render = new ol.render.webgl.Immediate(
+        context, center, resolution, rotation, size, extent, pixelRatio);
+    var composeEvent = new ol.render.Event(
+        type, layer, render, frameState, null, context);
+    layer.dispatchEvent(composeEvent);
+  }
+};
+
+
+/**
+ * @return {!goog.vec.Mat4.Number} Matrix.
+ */
+ol.renderer.webgl.Layer.prototype.getTexCoordMatrix = function() {
+  return this.texCoordMatrix;
+};
+
+
+/**
+ * @return {WebGLTexture} Texture.
+ */
+ol.renderer.webgl.Layer.prototype.getTexture = function() {
+  return this.texture;
+};
+
+
+/**
+ * @return {!goog.vec.Mat4.Number} Matrix.
+ */
+ol.renderer.webgl.Layer.prototype.getProjectionMatrix = function() {
+  return this.projectionMatrix;
+};
+
+
+/**
+ * Handle webglcontextlost.
+ */
+ol.renderer.webgl.Layer.prototype.handleWebGLContextLost = function() {
+  this.texture = null;
+  this.framebuffer = null;
+  this.framebufferDimension = undefined;
+};
+
+
+/**
+ * @param {olx.FrameState} frameState Frame state.
+ * @param {ol.LayerState} layerState Layer state.
+ * @param {ol.webgl.Context} context Context.
+ * @return {boolean} whether composeFrame should be called.
+ */
+ol.renderer.webgl.Layer.prototype.prepareFrame = goog.abstractMethod;
+
+goog.provide('ol.renderer.webgl.ImageLayer');
+
+goog.require('goog.asserts');
+goog.require('goog.vec.Mat4');
+goog.require('goog.webgl');
+goog.require('ol.ImageBase');
+goog.require('ol.ViewHint');
+goog.require('ol.dom');
+goog.require('ol.extent');
+goog.require('ol.functions');
+goog.require('ol.layer.Image');
+goog.require('ol.proj');
+goog.require('ol.renderer.webgl.Layer');
+goog.require('ol.source.ImageVector');
+goog.require('ol.vec.Mat4');
+goog.require('ol.webgl.Context');
+
+
+/**
+ * @constructor
+ * @extends {ol.renderer.webgl.Layer}
+ * @param {ol.renderer.webgl.Map} mapRenderer Map renderer.
+ * @param {ol.layer.Image} imageLayer Tile layer.
+ */
+ol.renderer.webgl.ImageLayer = function(mapRenderer, imageLayer) {
+
+  ol.renderer.webgl.Layer.call(this, mapRenderer, imageLayer);
+
+  /**
+   * The last rendered image.
+   * @private
+   * @type {?ol.ImageBase}
+   */
+  this.image_ = null;
+
+  /**
+   * @private
+   * @type {CanvasRenderingContext2D}
+   */
+  this.hitCanvasContext_ = null;
+
+  /**
+   * @private
+   * @type {?goog.vec.Mat4.Number}
+   */
+  this.hitTransformationMatrix_ = null;
+
+};
+ol.inherits(ol.renderer.webgl.ImageLayer, ol.renderer.webgl.Layer);
+
+
+/**
+ * @param {ol.ImageBase} image Image.
+ * @private
+ * @return {WebGLTexture} Texture.
+ */
+ol.renderer.webgl.ImageLayer.prototype.createTexture_ = function(image) {
+
+  // We meet the conditions to work with non-power of two textures.
+  // http://www.khronos.org/webgl/wiki/WebGL_and_OpenGL_Differences#Non-Power_of_Two_Texture_Support
+  // http://learningwebgl.com/blog/?p=2101
+
+  var imageElement = image.getImage();
+  var gl = this.mapRenderer.getGL();
+
+  return ol.webgl.Context.createTexture(
+      gl, imageElement, goog.webgl.CLAMP_TO_EDGE, goog.webgl.CLAMP_TO_EDGE);
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.renderer.webgl.ImageLayer.prototype.forEachFeatureAtCoordinate = function(coordinate, frameState, callback, thisArg) {
+  var layer = this.getLayer();
+  var source = layer.getSource();
+  var resolution = frameState.viewState.resolution;
+  var rotation = frameState.viewState.rotation;
+  var skippedFeatureUids = frameState.skippedFeatureUids;
+  return source.forEachFeatureAtCoordinate(
+      coordinate, resolution, rotation, skippedFeatureUids,
+
+      /**
+       * @param {ol.Feature|ol.render.Feature} feature Feature.
+       * @return {?} Callback result.
+       */
+      function(feature) {
+        return callback.call(thisArg, feature, layer);
+      });
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.renderer.webgl.ImageLayer.prototype.prepareFrame = function(frameState, layerState, context) {
+
+  var gl = this.mapRenderer.getGL();
+
+  var pixelRatio = frameState.pixelRatio;
+  var viewState = frameState.viewState;
+  var viewCenter = viewState.center;
+  var viewResolution = viewState.resolution;
+  var viewRotation = viewState.rotation;
+
+  var image = this.image_;
+  var texture = this.texture;
+  var imageLayer = this.getLayer();
+  goog.asserts.assertInstanceof(imageLayer, ol.layer.Image,
+      'layer is an instance of ol.layer.Image');
+  var imageSource = imageLayer.getSource();
+
+  var hints = frameState.viewHints;
+
+  var renderedExtent = frameState.extent;
+  if (layerState.extent !== undefined) {
+    renderedExtent = ol.extent.getIntersection(
+        renderedExtent, layerState.extent);
+  }
+  if (!hints[ol.ViewHint.ANIMATING] && !hints[ol.ViewHint.INTERACTING] &&
+      !ol.extent.isEmpty(renderedExtent)) {
+    var projection = viewState.projection;
+    if (!ol.ENABLE_RASTER_REPROJECTION) {
+      var sourceProjection = imageSource.getProjection();
+      if (sourceProjection) {
+        goog.asserts.assert(ol.proj.equivalent(projection, sourceProjection),
+            'projection and sourceProjection are equivalent');
+        projection = sourceProjection;
+      }
+    }
+    var image_ = imageSource.getImage(renderedExtent, viewResolution,
+        pixelRatio, projection);
+    if (image_) {
+      var loaded = this.loadImage(image_);
+      if (loaded) {
+        image = image_;
+        texture = this.createTexture_(image_);
+        if (this.texture) {
+          /**
+           * @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 {ol.PostRenderFunction} */ (postRenderFunction)
+          );
+        }
+      }
+    }
+  }
+
+  if (image) {
+    goog.asserts.assert(texture, 'texture is truthy');
+
+    var canvas = this.mapRenderer.getContext().getCanvas();
+
+    this.updateProjectionMatrix_(canvas.width, canvas.height,
+        pixelRatio, viewCenter, viewResolution, viewRotation,
+        image.getExtent());
+    this.hitTransformationMatrix_ = null;
+
+    // Translate and scale to flip the Y coord.
+    var texCoordMatrix = this.texCoordMatrix;
+    goog.vec.Mat4.makeIdentity(texCoordMatrix);
+    goog.vec.Mat4.scale(texCoordMatrix, 1, -1, 1);
+    goog.vec.Mat4.translate(texCoordMatrix, 0, -1, 0);
+
+    this.image_ = image;
+    this.texture = texture;
+
+    this.updateAttributions(frameState.attributions, image.getAttributions());
+    this.updateLogos(frameState, imageSource);
+  }
+
+  return true;
+};
+
+
+/**
+ * @param {number} canvasWidth Canvas width.
+ * @param {number} canvasHeight Canvas height.
+ * @param {number} pixelRatio Pixel ratio.
+ * @param {ol.Coordinate} viewCenter View center.
+ * @param {number} viewResolution View resolution.
+ * @param {number} viewRotation View rotation.
+ * @param {ol.Extent} imageExtent Image extent.
+ * @private
+ */
+ol.renderer.webgl.ImageLayer.prototype.updateProjectionMatrix_ = function(canvasWidth, canvasHeight, pixelRatio,
+        viewCenter, viewResolution, viewRotation, imageExtent) {
+
+  var canvasExtentWidth = canvasWidth * viewResolution;
+  var canvasExtentHeight = canvasHeight * viewResolution;
+
+  var projectionMatrix = this.projectionMatrix;
+  goog.vec.Mat4.makeIdentity(projectionMatrix);
+  goog.vec.Mat4.scale(projectionMatrix,
+      pixelRatio * 2 / canvasExtentWidth,
+      pixelRatio * 2 / canvasExtentHeight, 1);
+  goog.vec.Mat4.rotateZ(projectionMatrix, -viewRotation);
+  goog.vec.Mat4.translate(projectionMatrix,
+      imageExtent[0] - viewCenter[0],
+      imageExtent[1] - viewCenter[1],
+      0);
+  goog.vec.Mat4.scale(projectionMatrix,
+      (imageExtent[2] - imageExtent[0]) / 2,
+      (imageExtent[3] - imageExtent[1]) / 2,
+      1);
+  goog.vec.Mat4.translate(projectionMatrix, 1, 1, 0);
+
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.renderer.webgl.ImageLayer.prototype.hasFeatureAtCoordinate = function(coordinate, frameState) {
+  var hasFeature = this.forEachFeatureAtCoordinate(
+      coordinate, frameState, ol.functions.TRUE, this);
+  return hasFeature !== undefined;
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.renderer.webgl.ImageLayer.prototype.forEachLayerAtPixel = function(pixel, frameState, callback, thisArg) {
+  if (!this.image_ || !this.image_.getImage()) {
+    return undefined;
+  }
+
+  if (this.getLayer().getSource() instanceof ol.source.ImageVector) {
+    // for ImageVector sources use the original hit-detection logic,
+    // so that for example also transparent polygons are detected
+    var coordinate = pixel.slice();
+    ol.vec.Mat4.multVec2(
+        frameState.pixelToCoordinateMatrix, coordinate, coordinate);
+    var hasFeature = this.forEachFeatureAtCoordinate(
+        coordinate, frameState, ol.functions.TRUE, this);
+
+    if (hasFeature) {
+      return callback.call(thisArg, this.getLayer());
+    } else {
+      return undefined;
+    }
+  } else {
+    var imageSize =
+        [this.image_.getImage().width, this.image_.getImage().height];
+
+    if (!this.hitTransformationMatrix_) {
+      this.hitTransformationMatrix_ = this.getHitTransformationMatrix_(
+          frameState.size, imageSize);
+    }
+
+    var pixelOnFrameBuffer = [0, 0];
+    ol.vec.Mat4.multVec2(
+        this.hitTransformationMatrix_, pixel, pixelOnFrameBuffer);
+
+    if (pixelOnFrameBuffer[0] < 0 || pixelOnFrameBuffer[0] > imageSize[0] ||
+        pixelOnFrameBuffer[1] < 0 || pixelOnFrameBuffer[1] > imageSize[1]) {
+      // outside the image, no need to check
+      return undefined;
+    }
+
+    if (!this.hitCanvasContext_) {
+      this.hitCanvasContext_ = ol.dom.createCanvasContext2D(1, 1);
+    }
+
+    this.hitCanvasContext_.clearRect(0, 0, 1, 1);
+    this.hitCanvasContext_.drawImage(this.image_.getImage(),
+        pixelOnFrameBuffer[0], pixelOnFrameBuffer[1], 1, 1, 0, 0, 1, 1);
+
+    var imageData = this.hitCanvasContext_.getImageData(0, 0, 1, 1).data;
+    if (imageData[3] > 0) {
+      return callback.call(thisArg, this.getLayer());
+    } else {
+      return undefined;
+    }
+  }
+};
+
+
+/**
+ * The transformation matrix to get the pixel on the image for a
+ * pixel on the map.
+ * @param {ol.Size} mapSize The map size.
+ * @param {ol.Size} imageSize The image size.
+ * @return {goog.vec.Mat4.Number} The transformation matrix.
+ * @private
+ */
+ol.renderer.webgl.ImageLayer.prototype.getHitTransformationMatrix_ = function(mapSize, imageSize) {
+  // the first matrix takes a map pixel, flips the y-axis and scales to
+  // a range between -1 ... 1
+  var mapCoordMatrix = goog.vec.Mat4.createNumber();
+  goog.vec.Mat4.makeIdentity(mapCoordMatrix);
+  goog.vec.Mat4.translate(mapCoordMatrix, -1, -1, 0);
+  goog.vec.Mat4.scale(mapCoordMatrix, 2 / mapSize[0], 2 / mapSize[1], 1);
+  goog.vec.Mat4.translate(mapCoordMatrix, 0, mapSize[1], 0);
+  goog.vec.Mat4.scale(mapCoordMatrix, 1, -1, 1);
+
+  // the second matrix is the inverse of the projection matrix used in the
+  // shader for drawing
+  var projectionMatrixInv = goog.vec.Mat4.createNumber();
+  goog.vec.Mat4.invert(this.projectionMatrix, projectionMatrixInv);
+
+  // the third matrix scales to the image dimensions and flips the y-axis again
+  var imageCoordMatrix = goog.vec.Mat4.createNumber();
+  goog.vec.Mat4.makeIdentity(imageCoordMatrix);
+  goog.vec.Mat4.translate(imageCoordMatrix, 0, imageSize[1], 0);
+  goog.vec.Mat4.scale(imageCoordMatrix, 1, -1, 1);
+  goog.vec.Mat4.scale(imageCoordMatrix, imageSize[0] / 2, imageSize[1] / 2, 1);
+  goog.vec.Mat4.translate(imageCoordMatrix, 1, 1, 0);
+
+  var transformMatrix = goog.vec.Mat4.createNumber();
+  goog.vec.Mat4.multMat(
+      imageCoordMatrix, projectionMatrixInv, transformMatrix);
+  goog.vec.Mat4.multMat(
+      transformMatrix, mapCoordMatrix, transformMatrix);
+
+  return transformMatrix;
+};
+
+// This file is automatically generated, do not edit
+goog.provide('ol.renderer.webgl.tilelayer.shader');
+goog.provide('ol.renderer.webgl.tilelayer.shader.Locations');
+goog.provide('ol.renderer.webgl.tilelayer.shader.Fragment');
+goog.provide('ol.renderer.webgl.tilelayer.shader.Vertex');
+
+goog.require('ol.webgl.shader');
+
+
+/**
+ * @constructor
+ * @extends {ol.webgl.shader.Fragment}
+ * @struct
+ */
+ol.renderer.webgl.tilelayer.shader.Fragment = function() {
+  ol.webgl.shader.Fragment.call(this, ol.renderer.webgl.tilelayer.shader.Fragment.SOURCE);
+};
+ol.inherits(ol.renderer.webgl.tilelayer.shader.Fragment, ol.webgl.shader.Fragment);
+goog.addSingletonGetter(ol.renderer.webgl.tilelayer.shader.Fragment);
+
+
+/**
+ * @const
+ * @type {string}
+ */
+ol.renderer.webgl.tilelayer.shader.Fragment.DEBUG_SOURCE = 'precision mediump float;\nvarying vec2 v_texCoord;\n\n\nuniform sampler2D u_texture;\n\nvoid main(void) {\n  gl_FragColor = texture2D(u_texture, v_texCoord);\n}\n';
+
+
+/**
+ * @const
+ * @type {string}
+ */
+ol.renderer.webgl.tilelayer.shader.Fragment.OPTIMIZED_SOURCE = 'precision mediump float;varying vec2 a;uniform sampler2D e;void main(void){gl_FragColor=texture2D(e,a);}';
+
+
+/**
+ * @const
+ * @type {string}
+ */
+ol.renderer.webgl.tilelayer.shader.Fragment.SOURCE = goog.DEBUG ?
+    ol.renderer.webgl.tilelayer.shader.Fragment.DEBUG_SOURCE :
+    ol.renderer.webgl.tilelayer.shader.Fragment.OPTIMIZED_SOURCE;
+
+
+/**
+ * @constructor
+ * @extends {ol.webgl.shader.Vertex}
+ * @struct
+ */
+ol.renderer.webgl.tilelayer.shader.Vertex = function() {
+  ol.webgl.shader.Vertex.call(this, ol.renderer.webgl.tilelayer.shader.Vertex.SOURCE);
+};
+ol.inherits(ol.renderer.webgl.tilelayer.shader.Vertex, ol.webgl.shader.Vertex);
+goog.addSingletonGetter(ol.renderer.webgl.tilelayer.shader.Vertex);
+
+
+/**
+ * @const
+ * @type {string}
+ */
+ol.renderer.webgl.tilelayer.shader.Vertex.DEBUG_SOURCE = 'varying vec2 v_texCoord;\n\n\nattribute vec2 a_position;\nattribute vec2 a_texCoord;\nuniform vec4 u_tileOffset;\n\nvoid main(void) {\n  gl_Position = vec4(a_position * u_tileOffset.xy + u_tileOffset.zw, 0., 1.);\n  v_texCoord = a_texCoord;\n}\n\n\n';
+
+
+/**
+ * @const
+ * @type {string}
+ */
+ol.renderer.webgl.tilelayer.shader.Vertex.OPTIMIZED_SOURCE = 'varying vec2 a;attribute vec2 b;attribute vec2 c;uniform vec4 d;void main(void){gl_Position=vec4(b*d.xy+d.zw,0.,1.);a=c;}';
+
+
+/**
+ * @const
+ * @type {string}
+ */
+ol.renderer.webgl.tilelayer.shader.Vertex.SOURCE = goog.DEBUG ?
+    ol.renderer.webgl.tilelayer.shader.Vertex.DEBUG_SOURCE :
+    ol.renderer.webgl.tilelayer.shader.Vertex.OPTIMIZED_SOURCE;
+
+
+/**
+ * @constructor
+ * @param {WebGLRenderingContext} gl GL.
+ * @param {WebGLProgram} program Program.
+ * @struct
+ */
+ol.renderer.webgl.tilelayer.shader.Locations = function(gl, program) {
+
+  /**
+   * @type {WebGLUniformLocation}
+   */
+  this.u_texture = gl.getUniformLocation(
+      program, goog.DEBUG ? 'u_texture' : 'e');
+
+  /**
+   * @type {WebGLUniformLocation}
+   */
+  this.u_tileOffset = gl.getUniformLocation(
+      program, goog.DEBUG ? 'u_tileOffset' : 'd');
+
+  /**
+   * @type {number}
+   */
+  this.a_position = gl.getAttribLocation(
+      program, goog.DEBUG ? 'a_position' : 'b');
+
+  /**
+   * @type {number}
+   */
+  this.a_texCoord = gl.getAttribLocation(
+      program, goog.DEBUG ? 'a_texCoord' : 'c');
+};
+
+// FIXME large resolutions lead to too large framebuffers :-(
+// FIXME animated shaders! check in redraw
+
+goog.provide('ol.renderer.webgl.TileLayer');
+
+goog.require('goog.asserts');
+goog.require('goog.vec.Mat4');
+goog.require('goog.vec.Vec4');
+goog.require('goog.webgl');
+goog.require('ol.TileRange');
+goog.require('ol.TileState');
+goog.require('ol.array');
+goog.require('ol.extent');
+goog.require('ol.layer.Tile');
+goog.require('ol.math');
+goog.require('ol.renderer.webgl.Layer');
+goog.require('ol.renderer.webgl.tilelayer.shader.Fragment');
+goog.require('ol.renderer.webgl.tilelayer.shader.Locations');
+goog.require('ol.renderer.webgl.tilelayer.shader.Vertex');
+goog.require('ol.size');
+goog.require('ol.vec.Mat4');
+goog.require('ol.webgl.Buffer');
+
+
+/**
+ * @constructor
+ * @extends {ol.renderer.webgl.Layer}
+ * @param {ol.renderer.webgl.Map} mapRenderer Map renderer.
+ * @param {ol.layer.Tile} tileLayer Tile layer.
+ */
+ol.renderer.webgl.TileLayer = function(mapRenderer, tileLayer) {
+
+  ol.renderer.webgl.Layer.call(this, mapRenderer, tileLayer);
+
+  /**
+   * @private
+   * @type {ol.webgl.shader.Fragment}
+   */
+  this.fragmentShader_ =
+      ol.renderer.webgl.tilelayer.shader.Fragment.getInstance();
+
+  /**
+   * @private
+   * @type {ol.webgl.shader.Vertex}
+   */
+  this.vertexShader_ = ol.renderer.webgl.tilelayer.shader.Vertex.getInstance();
+
+  /**
+   * @private
+   * @type {ol.renderer.webgl.tilelayer.shader.Locations}
+   */
+  this.locations_ = null;
+
+  /**
+   * @private
+   * @type {ol.webgl.Buffer}
+   */
+  this.renderArrayBuffer_ = new ol.webgl.Buffer([
+    0, 0, 0, 1,
+    1, 0, 1, 1,
+    0, 1, 0, 0,
+    1, 1, 1, 0
+  ]);
+
+  /**
+   * @private
+   * @type {ol.TileRange}
+   */
+  this.renderedTileRange_ = null;
+
+  /**
+   * @private
+   * @type {ol.Extent}
+   */
+  this.renderedFramebufferExtent_ = null;
+
+  /**
+   * @private
+   * @type {number}
+   */
+  this.renderedRevision_ = -1;
+
+  /**
+   * @private
+   * @type {ol.Size}
+   */
+  this.tmpSize_ = [0, 0];
+
+};
+ol.inherits(ol.renderer.webgl.TileLayer, ol.renderer.webgl.Layer);
+
+
+/**
+ * @inheritDoc
+ */
+ol.renderer.webgl.TileLayer.prototype.disposeInternal = function() {
+  var context = this.mapRenderer.getContext();
+  context.deleteBuffer(this.renderArrayBuffer_);
+  ol.renderer.webgl.Layer.prototype.disposeInternal.call(this);
+};
+
+
+/**
+ * Create a function that adds loaded tiles to the tile lookup.
+ * @param {ol.source.Tile} source Tile source.
+ * @param {ol.proj.Projection} projection Projection of the tiles.
+ * @param {Object.<number, Object.<string, ol.Tile>>} tiles Lookup of loaded
+ *     tiles by zoom level.
+ * @return {function(number, ol.TileRange):boolean} A function that can be
+ *     called with a zoom level and a tile range to add loaded tiles to the
+ *     lookup.
+ * @protected
+ */
+ol.renderer.webgl.TileLayer.prototype.createLoadedTileFinder = function(source, projection, tiles) {
+  var mapRenderer = this.mapRenderer;
+
+  return (
+      /**
+       * @param {number} zoom Zoom level.
+       * @param {ol.TileRange} tileRange Tile range.
+       * @return {boolean} The tile range is fully loaded.
+       */
+      function(zoom, tileRange) {
+        function callback(tile) {
+          var loaded = mapRenderer.isTileTextureLoaded(tile);
+          if (loaded) {
+            if (!tiles[zoom]) {
+              tiles[zoom] = {};
+            }
+            tiles[zoom][tile.tileCoord.toString()] = tile;
+          }
+          return loaded;
+        }
+        return source.forEachLoadedTile(projection, zoom, tileRange, callback);
+      });
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.renderer.webgl.TileLayer.prototype.handleWebGLContextLost = function() {
+  ol.renderer.webgl.Layer.prototype.handleWebGLContextLost.call(this);
+  this.locations_ = null;
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.renderer.webgl.TileLayer.prototype.prepareFrame = function(frameState, layerState, context) {
+
+  var mapRenderer = this.mapRenderer;
+  var gl = context.getGL();
+
+  var viewState = frameState.viewState;
+  var projection = viewState.projection;
+
+  var tileLayer = this.getLayer();
+  goog.asserts.assertInstanceof(tileLayer, ol.layer.Tile,
+      'layer is an instance of ol.layer.Tile');
+  var tileSource = tileLayer.getSource();
+  var tileGrid = tileSource.getTileGridForProjection(projection);
+  var z = tileGrid.getZForResolution(viewState.resolution);
+  var tileResolution = tileGrid.getResolution(z);
+
+  var tilePixelSize =
+      tileSource.getTilePixelSize(z, frameState.pixelRatio, projection);
+  var pixelRatio = tilePixelSize[0] /
+      ol.size.toSize(tileGrid.getTileSize(z), this.tmpSize_)[0];
+  var tilePixelResolution = tileResolution / pixelRatio;
+  var tileGutter = tileSource.getGutter(projection);
+
+  var center = viewState.center;
+  var extent;
+  if (tileResolution == viewState.resolution) {
+    center = this.snapCenterToPixel(center, tileResolution, frameState.size);
+    extent = ol.extent.getForViewAndSize(
+        center, tileResolution, viewState.rotation, frameState.size);
+  } else {
+    extent = frameState.extent;
+  }
+  var tileRange = tileGrid.getTileRangeForExtentAndResolution(
+      extent, tileResolution);
+
+  var framebufferExtent;
+  if (this.renderedTileRange_ &&
+      this.renderedTileRange_.equals(tileRange) &&
+      this.renderedRevision_ == tileSource.getRevision()) {
+    framebufferExtent = this.renderedFramebufferExtent_;
+  } else {
+
+    var tileRangeSize = tileRange.getSize();
+
+    var maxDimension = Math.max(
+        tileRangeSize[0] * tilePixelSize[0],
+        tileRangeSize[1] * tilePixelSize[1]);
+    var framebufferDimension = ol.math.roundUpToPowerOfTwo(maxDimension);
+    var framebufferExtentDimension = tilePixelResolution * framebufferDimension;
+    var origin = tileGrid.getOrigin(z);
+    var minX = origin[0] +
+        tileRange.minX * tilePixelSize[0] * tilePixelResolution;
+    var minY = origin[1] +
+        tileRange.minY * tilePixelSize[1] * tilePixelResolution;
+    framebufferExtent = [
+      minX, minY,
+      minX + framebufferExtentDimension, minY + framebufferExtentDimension
+    ];
+
+    this.bindFramebuffer(frameState, framebufferDimension);
+    gl.viewport(0, 0, framebufferDimension, framebufferDimension);
+
+    gl.clearColor(0, 0, 0, 0);
+    gl.clear(goog.webgl.COLOR_BUFFER_BIT);
+    gl.disable(goog.webgl.BLEND);
+
+    var program = context.getProgram(this.fragmentShader_, this.vertexShader_);
+    context.useProgram(program);
+    if (!this.locations_) {
+      this.locations_ =
+          new ol.renderer.webgl.tilelayer.shader.Locations(gl, program);
+    }
+
+    context.bindBuffer(goog.webgl.ARRAY_BUFFER, this.renderArrayBuffer_);
+    gl.enableVertexAttribArray(this.locations_.a_position);
+    gl.vertexAttribPointer(
+        this.locations_.a_position, 2, goog.webgl.FLOAT, false, 16, 0);
+    gl.enableVertexAttribArray(this.locations_.a_texCoord);
+    gl.vertexAttribPointer(
+        this.locations_.a_texCoord, 2, goog.webgl.FLOAT, false, 16, 8);
+    gl.uniform1i(this.locations_.u_texture, 0);
+
+    /**
+     * @type {Object.<number, Object.<string, ol.Tile>>}
+     */
+    var tilesToDrawByZ = {};
+    tilesToDrawByZ[z] = {};
+
+    var findLoadedTiles = this.createLoadedTileFinder(
+        tileSource, projection, tilesToDrawByZ);
+
+    var useInterimTilesOnError = tileLayer.getUseInterimTilesOnError();
+    var allTilesLoaded = true;
+    var tmpExtent = ol.extent.createEmpty();
+    var tmpTileRange = new ol.TileRange(0, 0, 0, 0);
+    var childTileRange, drawable, fullyLoaded, tile, tileState;
+    var x, y, tileExtent;
+    for (x = tileRange.minX; x <= tileRange.maxX; ++x) {
+      for (y = tileRange.minY; y <= tileRange.maxY; ++y) {
+
+        tile = tileSource.getTile(z, x, y, pixelRatio, projection);
+        if (layerState.extent !== undefined) {
+          // ignore tiles outside layer extent
+          tileExtent = tileGrid.getTileCoordExtent(tile.tileCoord, tmpExtent);
+          if (!ol.extent.intersects(tileExtent, layerState.extent)) {
+            continue;
+          }
+        }
+        tileState = tile.getState();
+        drawable = tileState == ol.TileState.LOADED ||
+            tileState == ol.TileState.EMPTY ||
+            tileState == ol.TileState.ERROR && !useInterimTilesOnError;
+        if (!drawable && tile.interimTile) {
+          tile = tile.interimTile;
+        }
+        goog.asserts.assert(tile);
+        tileState = tile.getState();
+        if (tileState == ol.TileState.LOADED) {
+          if (mapRenderer.isTileTextureLoaded(tile)) {
+            tilesToDrawByZ[z][tile.tileCoord.toString()] = tile;
+            continue;
+          }
+        } else if (tileState == ol.TileState.EMPTY ||
+                   (tileState == ol.TileState.ERROR &&
+                    !useInterimTilesOnError)) {
+          continue;
+        }
+
+        allTilesLoaded = false;
+        fullyLoaded = tileGrid.forEachTileCoordParentTileRange(
+            tile.tileCoord, findLoadedTiles, null, tmpTileRange, tmpExtent);
+        if (!fullyLoaded) {
+          childTileRange = tileGrid.getTileCoordChildTileRange(
+              tile.tileCoord, tmpTileRange, tmpExtent);
+          if (childTileRange) {
+            findLoadedTiles(z + 1, childTileRange);
+          }
+        }
+
+      }
+
+    }
+
+    /** @type {Array.<number>} */
+    var zs = Object.keys(tilesToDrawByZ).map(Number);
+    zs.sort(ol.array.numberSafeCompareFunction);
+    var u_tileOffset = goog.vec.Vec4.createFloat32();
+    var i, ii, sx, sy, tileKey, tilesToDraw, tx, ty;
+    for (i = 0, ii = zs.length; i < ii; ++i) {
+      tilesToDraw = tilesToDrawByZ[zs[i]];
+      for (tileKey in tilesToDraw) {
+        tile = tilesToDraw[tileKey];
+        tileExtent = tileGrid.getTileCoordExtent(tile.tileCoord, tmpExtent);
+        sx = 2 * (tileExtent[2] - tileExtent[0]) /
+            framebufferExtentDimension;
+        sy = 2 * (tileExtent[3] - tileExtent[1]) /
+            framebufferExtentDimension;
+        tx = 2 * (tileExtent[0] - framebufferExtent[0]) /
+            framebufferExtentDimension - 1;
+        ty = 2 * (tileExtent[1] - framebufferExtent[1]) /
+            framebufferExtentDimension - 1;
+        goog.vec.Vec4.setFromValues(u_tileOffset, sx, sy, tx, ty);
+        gl.uniform4fv(this.locations_.u_tileOffset, u_tileOffset);
+        mapRenderer.bindTileTexture(tile, tilePixelSize,
+            tileGutter * pixelRatio, goog.webgl.LINEAR, goog.webgl.LINEAR);
+        gl.drawArrays(goog.webgl.TRIANGLE_STRIP, 0, 4);
+      }
+    }
+
+    if (allTilesLoaded) {
+      this.renderedTileRange_ = tileRange;
+      this.renderedFramebufferExtent_ = framebufferExtent;
+      this.renderedRevision_ = tileSource.getRevision();
+    } else {
+      this.renderedTileRange_ = null;
+      this.renderedFramebufferExtent_ = null;
+      this.renderedRevision_ = -1;
+      frameState.animate = true;
+    }
+
+  }
+
+  this.updateUsedTiles(frameState.usedTiles, tileSource, z, tileRange);
+  var tileTextureQueue = mapRenderer.getTileTextureQueue();
+  this.manageTilePyramid(
+      frameState, tileSource, tileGrid, pixelRatio, projection, extent, z,
+      tileLayer.getPreload(),
+      /**
+       * @param {ol.Tile} tile Tile.
+       */
+      function(tile) {
+        if (tile.getState() == ol.TileState.LOADED &&
+            !mapRenderer.isTileTextureLoaded(tile) &&
+            !tileTextureQueue.isKeyQueued(tile.getKey())) {
+          tileTextureQueue.enqueue([
+            tile,
+            tileGrid.getTileCoordCenter(tile.tileCoord),
+            tileGrid.getResolution(tile.tileCoord[0]),
+            tilePixelSize, tileGutter * pixelRatio
+          ]);
+        }
+      }, this);
+  this.scheduleExpireCache(frameState, tileSource);
+  this.updateLogos(frameState, tileSource);
+
+  var texCoordMatrix = this.texCoordMatrix;
+  goog.vec.Mat4.makeIdentity(texCoordMatrix);
+  goog.vec.Mat4.translate(texCoordMatrix,
+      (center[0] - framebufferExtent[0]) /
+          (framebufferExtent[2] - framebufferExtent[0]),
+      (center[1] - framebufferExtent[1]) /
+          (framebufferExtent[3] - framebufferExtent[1]),
+      0);
+  if (viewState.rotation !== 0) {
+    goog.vec.Mat4.rotateZ(texCoordMatrix, viewState.rotation);
+  }
+  goog.vec.Mat4.scale(texCoordMatrix,
+      frameState.size[0] * viewState.resolution /
+          (framebufferExtent[2] - framebufferExtent[0]),
+      frameState.size[1] * viewState.resolution /
+          (framebufferExtent[3] - framebufferExtent[1]),
+      1);
+  goog.vec.Mat4.translate(texCoordMatrix,
+      -0.5,
+      -0.5,
+      0);
+
+  return true;
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.renderer.webgl.TileLayer.prototype.forEachLayerAtPixel = function(pixel, frameState, callback, thisArg) {
+  if (!this.framebuffer) {
+    return undefined;
+  }
+
+  var pixelOnMapScaled = [
+    pixel[0] / frameState.size[0],
+    (frameState.size[1] - pixel[1]) / frameState.size[1]];
+
+  var pixelOnFrameBufferScaled = [0, 0];
+  ol.vec.Mat4.multVec2(
+      this.texCoordMatrix, pixelOnMapScaled, pixelOnFrameBufferScaled);
+  var pixelOnFrameBuffer = [
+    pixelOnFrameBufferScaled[0] * this.framebufferDimension,
+    pixelOnFrameBufferScaled[1] * this.framebufferDimension];
+
+  var gl = this.mapRenderer.getContext().getGL();
+  gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffer);
+  var imageData = new Uint8Array(4);
+  gl.readPixels(pixelOnFrameBuffer[0], pixelOnFrameBuffer[1], 1, 1,
+      gl.RGBA, gl.UNSIGNED_BYTE, imageData);
+
+  if (imageData[3] > 0) {
+    return callback.call(thisArg, this.getLayer());
+  } else {
+    return undefined;
+  }
+};
+
+goog.provide('ol.renderer.webgl.VectorLayer');
+
+goog.require('goog.asserts');
+goog.require('ol.events');
+goog.require('ol.ViewHint');
+goog.require('ol.extent');
+goog.require('ol.layer.Vector');
+goog.require('ol.render.webgl.ReplayGroup');
+goog.require('ol.renderer.vector');
+goog.require('ol.renderer.webgl.Layer');
+goog.require('ol.vec.Mat4');
+
+
+/**
+ * @constructor
+ * @extends {ol.renderer.webgl.Layer}
+ * @param {ol.renderer.webgl.Map} mapRenderer Map renderer.
+ * @param {ol.layer.Vector} vectorLayer Vector layer.
+ */
+ol.renderer.webgl.VectorLayer = function(mapRenderer, vectorLayer) {
+
+  ol.renderer.webgl.Layer.call(this, mapRenderer, vectorLayer);
+
+  /**
+   * @private
+   * @type {boolean}
+   */
+  this.dirty_ = false;
+
+  /**
+   * @private
+   * @type {number}
+   */
+  this.renderedRevision_ = -1;
+
+  /**
+   * @private
+   * @type {number}
+   */
+  this.renderedResolution_ = NaN;
+
+  /**
+   * @private
+   * @type {ol.Extent}
+   */
+  this.renderedExtent_ = ol.extent.createEmpty();
+
+  /**
+   * @private
+   * @type {function(ol.Feature, ol.Feature): number|null}
+   */
+  this.renderedRenderOrder_ = null;
+
+  /**
+   * @private
+   * @type {ol.render.webgl.ReplayGroup}
+   */
+  this.replayGroup_ = null;
+
+  /**
+   * The last layer state.
+   * @private
+   * @type {?ol.LayerState}
+   */
+  this.layerState_ = null;
+
+};
+ol.inherits(ol.renderer.webgl.VectorLayer, ol.renderer.webgl.Layer);
+
+
+/**
+ * @inheritDoc
+ */
+ol.renderer.webgl.VectorLayer.prototype.composeFrame = function(frameState, layerState, context) {
+  this.layerState_ = layerState;
+  var viewState = frameState.viewState;
+  var replayGroup = this.replayGroup_;
+  if (replayGroup && !replayGroup.isEmpty()) {
+    replayGroup.replay(context,
+        viewState.center, viewState.resolution, viewState.rotation,
+        frameState.size, frameState.pixelRatio, layerState.opacity,
+        layerState.managed ? frameState.skippedFeatureUids : {});
+  }
+
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.renderer.webgl.VectorLayer.prototype.disposeInternal = function() {
+  var replayGroup = this.replayGroup_;
+  if (replayGroup) {
+    var context = this.mapRenderer.getContext();
+    replayGroup.getDeleteResourcesFunction(context)();
+    this.replayGroup_ = null;
+  }
+  ol.renderer.webgl.Layer.prototype.disposeInternal.call(this);
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.renderer.webgl.VectorLayer.prototype.forEachFeatureAtCoordinate = function(coordinate, frameState, callback, thisArg) {
+  if (!this.replayGroup_ || !this.layerState_) {
+    return undefined;
+  } else {
+    var context = this.mapRenderer.getContext();
+    var viewState = frameState.viewState;
+    var layer = this.getLayer();
+    var layerState = this.layerState_;
+    /** @type {Object.<string, boolean>} */
+    var features = {};
+    return this.replayGroup_.forEachFeatureAtCoordinate(coordinate,
+        context, viewState.center, viewState.resolution, viewState.rotation,
+        frameState.size, frameState.pixelRatio, layerState.opacity,
+        {},
+        /**
+         * @param {ol.Feature|ol.render.Feature} feature Feature.
+         * @return {?} Callback result.
+         */
+        function(feature) {
+          goog.asserts.assert(feature !== undefined, 'received a feature');
+          var key = goog.getUid(feature).toString();
+          if (!(key in features)) {
+            features[key] = true;
+            return callback.call(thisArg, feature, layer);
+          }
+        });
+  }
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.renderer.webgl.VectorLayer.prototype.hasFeatureAtCoordinate = function(coordinate, frameState) {
+  if (!this.replayGroup_ || !this.layerState_) {
+    return false;
+  } else {
+    var context = this.mapRenderer.getContext();
+    var viewState = frameState.viewState;
+    var layerState = this.layerState_;
+    return this.replayGroup_.hasFeatureAtCoordinate(coordinate,
+        context, viewState.center, viewState.resolution, viewState.rotation,
+        frameState.size, frameState.pixelRatio, layerState.opacity,
+        frameState.skippedFeatureUids);
+  }
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.renderer.webgl.VectorLayer.prototype.forEachLayerAtPixel = function(pixel, frameState, callback, thisArg) {
+  var coordinate = pixel.slice();
+  ol.vec.Mat4.multVec2(
+      frameState.pixelToCoordinateMatrix, coordinate, coordinate);
+  var hasFeature = this.hasFeatureAtCoordinate(coordinate, frameState);
+
+  if (hasFeature) {
+    return callback.call(thisArg, this.getLayer());
+  } else {
+    return undefined;
+  }
+};
+
+
+/**
+ * Handle changes in image style state.
+ * @param {ol.events.Event} event Image style change event.
+ * @private
+ */
+ol.renderer.webgl.VectorLayer.prototype.handleStyleImageChange_ = function(event) {
+  this.renderIfReadyAndVisible();
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.renderer.webgl.VectorLayer.prototype.prepareFrame = function(frameState, layerState, context) {
+
+  var vectorLayer = /** @type {ol.layer.Vector} */ (this.getLayer());
+  goog.asserts.assertInstanceof(vectorLayer, ol.layer.Vector,
+      'layer is an instance of ol.layer.Vector');
+  var vectorSource = vectorLayer.getSource();
+
+  this.updateAttributions(
+      frameState.attributions, vectorSource.getAttributions());
+  this.updateLogos(frameState, vectorSource);
+
+  var animating = frameState.viewHints[ol.ViewHint.ANIMATING];
+  var interacting = frameState.viewHints[ol.ViewHint.INTERACTING];
+  var updateWhileAnimating = vectorLayer.getUpdateWhileAnimating();
+  var updateWhileInteracting = vectorLayer.getUpdateWhileInteracting();
+
+  if (!this.dirty_ && (!updateWhileAnimating && animating) ||
+      (!updateWhileInteracting && interacting)) {
+    return true;
+  }
+
+  var frameStateExtent = frameState.extent;
+  var viewState = frameState.viewState;
+  var projection = viewState.projection;
+  var resolution = viewState.resolution;
+  var pixelRatio = frameState.pixelRatio;
+  var vectorLayerRevision = vectorLayer.getRevision();
+  var vectorLayerRenderBuffer = vectorLayer.getRenderBuffer();
+  var vectorLayerRenderOrder = vectorLayer.getRenderOrder();
+
+  if (vectorLayerRenderOrder === undefined) {
+    vectorLayerRenderOrder = ol.renderer.vector.defaultOrder;
+  }
+
+  var extent = ol.extent.buffer(frameStateExtent,
+      vectorLayerRenderBuffer * resolution);
+
+  if (!this.dirty_ &&
+      this.renderedResolution_ == resolution &&
+      this.renderedRevision_ == vectorLayerRevision &&
+      this.renderedRenderOrder_ == vectorLayerRenderOrder &&
+      ol.extent.containsExtent(this.renderedExtent_, extent)) {
+    return true;
+  }
+
+  if (this.replayGroup_) {
+    frameState.postRenderFunctions.push(
+        this.replayGroup_.getDeleteResourcesFunction(context));
+  }
+
+  this.dirty_ = false;
+
+  var replayGroup = new ol.render.webgl.ReplayGroup(
+      ol.renderer.vector.getTolerance(resolution, pixelRatio),
+      extent, vectorLayer.getRenderBuffer());
+  vectorSource.loadFeatures(extent, resolution, projection);
+  /**
+   * @param {ol.Feature} feature Feature.
+   * @this {ol.renderer.webgl.VectorLayer}
+   */
+  var renderFeature = function(feature) {
+    var styles;
+    var styleFunction = feature.getStyleFunction();
+    if (styleFunction) {
+      styles = styleFunction.call(feature, resolution);
+    } else {
+      styleFunction = vectorLayer.getStyleFunction();
+      if (styleFunction) {
+        styles = styleFunction(feature, resolution);
+      }
+    }
+    if (styles) {
+      var dirty = this.renderFeature(
+          feature, resolution, pixelRatio, styles, replayGroup);
+      this.dirty_ = this.dirty_ || dirty;
+    }
+  };
+  if (vectorLayerRenderOrder) {
+    /** @type {Array.<ol.Feature>} */
+    var features = [];
+    vectorSource.forEachFeatureInExtent(extent,
+        /**
+         * @param {ol.Feature} feature Feature.
+         */
+        function(feature) {
+          features.push(feature);
+        }, this);
+    features.sort(vectorLayerRenderOrder);
+    features.forEach(renderFeature, this);
+  } else {
+    vectorSource.forEachFeatureInExtent(extent, renderFeature, this);
+  }
+  replayGroup.finish(context);
+
+  this.renderedResolution_ = resolution;
+  this.renderedRevision_ = vectorLayerRevision;
+  this.renderedRenderOrder_ = vectorLayerRenderOrder;
+  this.renderedExtent_ = extent;
+  this.replayGroup_ = replayGroup;
+
+  return true;
+};
+
+
+/**
+ * @param {ol.Feature} feature Feature.
+ * @param {number} resolution Resolution.
+ * @param {number} pixelRatio Pixel ratio.
+ * @param {(ol.style.Style|Array.<ol.style.Style>)} styles The style or array of
+ *     styles.
+ * @param {ol.render.webgl.ReplayGroup} replayGroup Replay group.
+ * @return {boolean} `true` if an image is loading.
+ */
+ol.renderer.webgl.VectorLayer.prototype.renderFeature = function(feature, resolution, pixelRatio, styles, replayGroup) {
+  if (!styles) {
+    return false;
+  }
+  var loading = false;
+  if (Array.isArray(styles)) {
+    for (var i = 0, ii = styles.length; i < ii; ++i) {
+      loading = ol.renderer.vector.renderFeature(
+          replayGroup, feature, styles[i],
+          ol.renderer.vector.getSquaredTolerance(resolution, pixelRatio),
+          this.handleStyleImageChange_, this) || loading;
+    }
+  } else {
+    loading = ol.renderer.vector.renderFeature(
+        replayGroup, feature, styles,
+        ol.renderer.vector.getSquaredTolerance(resolution, pixelRatio),
+        this.handleStyleImageChange_, this) || loading;
+  }
+  return loading;
+};
+
+// FIXME check against gl.getParameter(webgl.MAX_TEXTURE_SIZE)
+
+goog.provide('ol.renderer.webgl.Map');
+
+goog.require('goog.asserts');
+goog.require('goog.webgl');
+goog.require('ol');
+goog.require('ol.RendererType');
+goog.require('ol.array');
+goog.require('ol.css');
+goog.require('ol.dom');
+goog.require('ol.events');
+goog.require('ol.events.Event');
+goog.require('ol.layer.Image');
+goog.require('ol.layer.Layer');
+goog.require('ol.layer.Tile');
+goog.require('ol.layer.Vector');
+goog.require('ol.render.Event');
+goog.require('ol.render.EventType');
+goog.require('ol.render.webgl.Immediate');
+goog.require('ol.renderer.Map');
+goog.require('ol.renderer.webgl.ImageLayer');
+goog.require('ol.renderer.webgl.Layer');
+goog.require('ol.renderer.webgl.TileLayer');
+goog.require('ol.renderer.webgl.VectorLayer');
+goog.require('ol.source.State');
+goog.require('ol.structs.LRUCache');
+goog.require('ol.structs.PriorityQueue');
+goog.require('ol.webgl');
+goog.require('ol.webgl.Context');
+goog.require('ol.webgl.WebGLContextEventType');
+
+
+/**
+ * @constructor
+ * @extends {ol.renderer.Map}
+ * @param {Element} container Container.
+ * @param {ol.Map} map Map.
+ */
+ol.renderer.webgl.Map = function(container, map) {
+
+  ol.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_.className = ol.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_ = ol.dom.createCanvasContext2D();
+
+  /**
+   * @private
+   * @type {boolean}
+   */
+  this.renderedVisible_ = true;
+
+  /**
+   * @private
+   * @type {WebGLRenderingContext}
+   */
+  this.gl_ = ol.webgl.getContext(this.canvas_, {
+    antialias: true,
+    depth: false,
+    failIfMajorPerformanceCaveat: true,
+    preserveDrawingBuffer: false,
+    stencil: true
+  });
+  goog.asserts.assert(this.gl_, 'got a WebGLRenderingContext');
+
+  /**
+   * @private
+   * @type {ol.webgl.Context}
+   */
+  this.context_ = new ol.webgl.Context(this.canvas_, this.gl_);
+
+  ol.events.listen(this.canvas_, ol.webgl.WebGLContextEventType.LOST,
+      this.handleWebGLContextLost, this);
+  ol.events.listen(this.canvas_, ol.webgl.WebGLContextEventType.RESTORED,
+      this.handleWebGLContextRestored, this);
+
+  /**
+   * @private
+   * @type {ol.structs.LRUCache.<ol.WebglTextureCacheEntry|null>}
+   */
+  this.textureCache_ = new ol.structs.LRUCache();
+
+  /**
+   * @private
+   * @type {ol.Coordinate}
+   */
+  this.focus_ = null;
+
+  /**
+   * @private
+   * @type {ol.structs.PriorityQueue.<Array>}
+   */
+  this.tileTextureQueue_ = new ol.structs.PriorityQueue(
+      /**
+       * @param {Array.<*>} element Element.
+       * @return {number} Priority.
+       * @this {ol.renderer.webgl.Map}
+       */
+      (function(element) {
+        var tileCenter = /** @type {ol.Coordinate} */ (element[1]);
+        var tileResolution = /** @type {number} */ (element[2]);
+        var deltaX = tileCenter[0] - this.focus_[0];
+        var deltaY = tileCenter[1] - this.focus_[1];
+        return 65536 * Math.log(tileResolution) +
+            Math.sqrt(deltaX * deltaX + deltaY * deltaY) / tileResolution;
+      }).bind(this),
+      /**
+       * @param {Array.<*>} element Element.
+       * @return {string} Key.
+       */
+      function(element) {
+        return /** @type {ol.Tile} */ (element[0]).getKey();
+      });
+
+
+ /**
+  * @param {ol.Map} map Map.
+  * @param {?olx.FrameState} frameState Frame state.
+  * @return {boolean} false.
+  * @this {ol.renderer.webgl.Map}
+  */
+  this.loadNextTileTexture_ =
+      function(map, frameState) {
+        if (!this.tileTextureQueue_.isEmpty()) {
+          this.tileTextureQueue_.reprioritize();
+          var element = this.tileTextureQueue_.dequeue();
+          var tile = /** @type {ol.Tile} */ (element[0]);
+          var tileSize = /** @type {ol.Size} */ (element[3]);
+          var tileGutter = /** @type {number} */ (element[4]);
+          this.bindTileTexture(
+              tile, tileSize, tileGutter, goog.webgl.LINEAR, goog.webgl.LINEAR);
+        }
+        return false;
+      }.bind(this);
+
+
+  /**
+   * @private
+   * @type {number}
+   */
+  this.textureCacheFrameMarkerCount_ = 0;
+
+  this.initializeGL_();
+
+};
+ol.inherits(ol.renderer.webgl.Map, ol.renderer.Map);
+
+
+/**
+ * @param {ol.Tile} tile Tile.
+ * @param {ol.Size} tileSize Tile size.
+ * @param {number} tileGutter Tile gutter.
+ * @param {number} magFilter Mag filter.
+ * @param {number} minFilter Min filter.
+ */
+ol.renderer.webgl.Map.prototype.bindTileTexture = function(tile, tileSize, tileGutter, magFilter, minFilter) {
+  var gl = this.getGL();
+  var tileKey = tile.getKey();
+  if (this.textureCache_.containsKey(tileKey)) {
+    var textureCacheEntry = this.textureCache_.get(tileKey);
+    goog.asserts.assert(textureCacheEntry,
+        'a texture cache entry exists for key %s', tileKey);
+    gl.bindTexture(goog.webgl.TEXTURE_2D, textureCacheEntry.texture);
+    if (textureCacheEntry.magFilter != magFilter) {
+      gl.texParameteri(
+          goog.webgl.TEXTURE_2D, goog.webgl.TEXTURE_MAG_FILTER, magFilter);
+      textureCacheEntry.magFilter = magFilter;
+    }
+    if (textureCacheEntry.minFilter != minFilter) {
+      gl.texParameteri(
+          goog.webgl.TEXTURE_2D, goog.webgl.TEXTURE_MIN_FILTER, minFilter);
+      textureCacheEntry.minFilter = minFilter;
+    }
+  } else {
+    var texture = gl.createTexture();
+    gl.bindTexture(goog.webgl.TEXTURE_2D, texture);
+    if (tileGutter > 0) {
+      var clipTileCanvas = this.clipTileContext_.canvas;
+      var clipTileContext = this.clipTileContext_;
+      if (this.clipTileCanvasWidth_ !== tileSize[0] ||
+          this.clipTileCanvasHeight_ !== tileSize[1]) {
+        clipTileCanvas.width = tileSize[0];
+        clipTileCanvas.height = tileSize[1];
+        this.clipTileCanvasWidth_ = tileSize[0];
+        this.clipTileCanvasHeight_ = tileSize[1];
+      } else {
+        clipTileContext.clearRect(0, 0, tileSize[0], tileSize[1]);
+      }
+      clipTileContext.drawImage(tile.getImage(), tileGutter, tileGutter,
+          tileSize[0], tileSize[1], 0, 0, tileSize[0], tileSize[1]);
+      gl.texImage2D(goog.webgl.TEXTURE_2D, 0,
+          goog.webgl.RGBA, goog.webgl.RGBA,
+          goog.webgl.UNSIGNED_BYTE, clipTileCanvas);
+    } else {
+      gl.texImage2D(goog.webgl.TEXTURE_2D, 0,
+          goog.webgl.RGBA, goog.webgl.RGBA,
+          goog.webgl.UNSIGNED_BYTE, tile.getImage());
+    }
+    gl.texParameteri(
+        goog.webgl.TEXTURE_2D, goog.webgl.TEXTURE_MAG_FILTER, magFilter);
+    gl.texParameteri(
+        goog.webgl.TEXTURE_2D, goog.webgl.TEXTURE_MIN_FILTER, minFilter);
+    gl.texParameteri(goog.webgl.TEXTURE_2D, goog.webgl.TEXTURE_WRAP_S,
+        goog.webgl.CLAMP_TO_EDGE);
+    gl.texParameteri(goog.webgl.TEXTURE_2D, goog.webgl.TEXTURE_WRAP_T,
+        goog.webgl.CLAMP_TO_EDGE);
+    this.textureCache_.set(tileKey, {
+      texture: texture,
+      magFilter: magFilter,
+      minFilter: minFilter
+    });
+  }
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.renderer.webgl.Map.prototype.createLayerRenderer = function(layer) {
+  if (ol.ENABLE_IMAGE && layer instanceof ol.layer.Image) {
+    return new ol.renderer.webgl.ImageLayer(this, layer);
+  } else if (ol.ENABLE_TILE && layer instanceof ol.layer.Tile) {
+    return new ol.renderer.webgl.TileLayer(this, layer);
+  } else if (ol.ENABLE_VECTOR && layer instanceof ol.layer.Vector) {
+    return new ol.renderer.webgl.VectorLayer(this, layer);
+  } else {
+    goog.asserts.fail('unexpected layer configuration');
+    return null;
+  }
+};
+
+
+/**
+ * @param {ol.render.EventType} type Event type.
+ * @param {olx.FrameState} frameState Frame state.
+ * @private
+ */
+ol.renderer.webgl.Map.prototype.dispatchComposeEvent_ = function(type, frameState) {
+  var map = this.getMap();
+  if (map.hasListener(type)) {
+    var context = this.context_;
+
+    var extent = frameState.extent;
+    var size = frameState.size;
+    var viewState = frameState.viewState;
+    var pixelRatio = frameState.pixelRatio;
+
+    var resolution = viewState.resolution;
+    var center = viewState.center;
+    var rotation = viewState.rotation;
+
+    var vectorContext = new ol.render.webgl.Immediate(context,
+        center, resolution, rotation, size, extent, pixelRatio);
+    var composeEvent = new ol.render.Event(type, map, vectorContext,
+        frameState, null, context);
+    map.dispatchEvent(composeEvent);
+  }
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.renderer.webgl.Map.prototype.disposeInternal = function() {
+  var gl = this.getGL();
+  if (!gl.isContextLost()) {
+    this.textureCache_.forEach(
+        /**
+         * @param {?ol.WebglTextureCacheEntry} textureCacheEntry
+         *     Texture cache entry.
+         */
+        function(textureCacheEntry) {
+          if (textureCacheEntry) {
+            gl.deleteTexture(textureCacheEntry.texture);
+          }
+        });
+  }
+  this.context_.dispose();
+  ol.renderer.Map.prototype.disposeInternal.call(this);
+};
+
+
+/**
+ * @param {ol.Map} map Map.
+ * @param {olx.FrameState} frameState Frame state.
+ * @private
+ */
+ol.renderer.webgl.Map.prototype.expireCache_ = function(map, frameState) {
+  var gl = this.getGL();
+  var textureCacheEntry;
+  while (this.textureCache_.getCount() - this.textureCacheFrameMarkerCount_ >
+      ol.WEBGL_TEXTURE_CACHE_HIGH_WATER_MARK) {
+    textureCacheEntry = this.textureCache_.peekLast();
+    if (!textureCacheEntry) {
+      if (+this.textureCache_.peekLastKey() == frameState.index) {
+        break;
+      } else {
+        --this.textureCacheFrameMarkerCount_;
+      }
+    } else {
+      gl.deleteTexture(textureCacheEntry.texture);
+    }
+    this.textureCache_.pop();
+  }
+};
+
+
+/**
+ * @return {ol.webgl.Context} The context.
+ */
+ol.renderer.webgl.Map.prototype.getContext = function() {
+  return this.context_;
+};
+
+
+/**
+ * @return {WebGLRenderingContext} GL.
+ */
+ol.renderer.webgl.Map.prototype.getGL = function() {
+  return this.gl_;
+};
+
+
+/**
+ * @return {ol.structs.PriorityQueue.<Array>} Tile texture queue.
+ */
+ol.renderer.webgl.Map.prototype.getTileTextureQueue = function() {
+  return this.tileTextureQueue_;
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.renderer.webgl.Map.prototype.getType = function() {
+  return ol.RendererType.WEBGL;
+};
+
+
+/**
+ * @param {ol.events.Event} event Event.
+ * @protected
+ */
+ol.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 {ol.renderer.webgl.Layer} */ (renderers[id]);
+    renderer.handleWebGLContextLost();
+  }
+};
+
+
+/**
+ * @protected
+ */
+ol.renderer.webgl.Map.prototype.handleWebGLContextRestored = function() {
+  this.initializeGL_();
+  this.getMap().render();
+};
+
+
+/**
+ * @private
+ */
+ol.renderer.webgl.Map.prototype.initializeGL_ = function() {
+  var gl = this.gl_;
+  gl.activeTexture(goog.webgl.TEXTURE0);
+  gl.blendFuncSeparate(
+      goog.webgl.SRC_ALPHA, goog.webgl.ONE_MINUS_SRC_ALPHA,
+      goog.webgl.ONE, goog.webgl.ONE_MINUS_SRC_ALPHA);
+  gl.disable(goog.webgl.CULL_FACE);
+  gl.disable(goog.webgl.DEPTH_TEST);
+  gl.disable(goog.webgl.SCISSOR_TEST);
+  gl.disable(goog.webgl.STENCIL_TEST);
+};
+
+
+/**
+ * @param {ol.Tile} tile Tile.
+ * @return {boolean} Is tile texture loaded.
+ */
+ol.renderer.webgl.Map.prototype.isTileTextureLoaded = function(tile) {
+  return this.textureCache_.containsKey(tile.getKey());
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.renderer.webgl.Map.prototype.renderFrame = function(frameState) {
+
+  var context = this.getContext();
+  var gl = this.getGL();
+
+  if (gl.isContextLost()) {
+    return false;
+  }
+
+  if (!frameState) {
+    if (this.renderedVisible_) {
+      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_(ol.render.EventType.PRECOMPOSE, frameState);
+
+  /** @type {Array.<ol.LayerState>} */
+  var layerStatesToDraw = [];
+  var layerStatesArray = frameState.layerStatesArray;
+  ol.array.stableSort(layerStatesArray, ol.renderer.Map.sortByZIndex);
+
+  var viewResolution = frameState.viewState.resolution;
+  var i, ii, layerRenderer, layerState;
+  for (i = 0, ii = layerStatesArray.length; i < ii; ++i) {
+    layerState = layerStatesArray[i];
+    if (ol.layer.Layer.visibleAtResolution(layerState, viewResolution) &&
+        layerState.sourceState == ol.source.State.READY) {
+      layerRenderer = this.getLayerRenderer(layerState.layer);
+      goog.asserts.assertInstanceof(layerRenderer, ol.renderer.webgl.Layer,
+          'renderer is an instance of ol.renderer.webgl.Layer');
+      if (layerRenderer.prepareFrame(frameState, layerState, context)) {
+        layerStatesToDraw.push(layerState);
+      }
+    }
+  }
+
+  var width = frameState.size[0] * frameState.pixelRatio;
+  var height = frameState.size[1] * frameState.pixelRatio;
+  if (this.canvas_.width != width || this.canvas_.height != height) {
+    this.canvas_.width = width;
+    this.canvas_.height = height;
+  }
+
+  gl.bindFramebuffer(goog.webgl.FRAMEBUFFER, null);
+
+  gl.clearColor(0, 0, 0, 0);
+  gl.clear(goog.webgl.COLOR_BUFFER_BIT);
+  gl.enable(goog.webgl.BLEND);
+  gl.viewport(0, 0, this.canvas_.width, this.canvas_.height);
+
+  for (i = 0, ii = layerStatesToDraw.length; i < ii; ++i) {
+    layerState = layerStatesToDraw[i];
+    layerRenderer = this.getLayerRenderer(layerState.layer);
+    goog.asserts.assertInstanceof(layerRenderer, ol.renderer.webgl.Layer,
+        'renderer is an instance of ol.renderer.webgl.Layer');
+    layerRenderer.composeFrame(frameState, layerState, context);
+  }
+
+  if (!this.renderedVisible_) {
+    this.canvas_.style.display = '';
+    this.renderedVisible_ = true;
+  }
+
+  this.calculateMatrices2D(frameState);
+
+  if (this.textureCache_.getCount() - this.textureCacheFrameMarkerCount_ >
+      ol.WEBGL_TEXTURE_CACHE_HIGH_WATER_MARK) {
+    frameState.postRenderFunctions.push(
+      /** @type {ol.PostRenderFunction} */ (this.expireCache_.bind(this))
+    );
+  }
+
+  if (!this.tileTextureQueue_.isEmpty()) {
+    frameState.postRenderFunctions.push(this.loadNextTileTexture_);
+    frameState.animate = true;
+  }
+
+  this.dispatchComposeEvent_(ol.render.EventType.POSTCOMPOSE, frameState);
+
+  this.scheduleRemoveUnusedLayerRenderers(frameState);
+  this.scheduleExpireIconCache(frameState);
+
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.renderer.webgl.Map.prototype.forEachFeatureAtCoordinate = function(coordinate, frameState, callback, thisArg,
+        layerFilter, thisArg2) {
+  var result;
+
+  if (this.getGL().isContextLost()) {
+    return false;
+  }
+
+  var viewState = frameState.viewState;
+
+  var layerStates = frameState.layerStatesArray;
+  var numLayers = layerStates.length;
+  var i;
+  for (i = numLayers - 1; i >= 0; --i) {
+    var layerState = layerStates[i];
+    var layer = layerState.layer;
+    if (ol.layer.Layer.visibleAtResolution(layerState, viewState.resolution) &&
+        layerFilter.call(thisArg2, layer)) {
+      var layerRenderer = this.getLayerRenderer(layer);
+      result = layerRenderer.forEachFeatureAtCoordinate(
+          coordinate, frameState, callback, thisArg);
+      if (result) {
+        return result;
+      }
+    }
+  }
+  return undefined;
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.renderer.webgl.Map.prototype.hasFeatureAtCoordinate = function(coordinate, frameState, layerFilter, thisArg) {
+  var hasFeature = false;
+
+  if (this.getGL().isContextLost()) {
+    return false;
+  }
+
+  var viewState = frameState.viewState;
+
+  var layerStates = frameState.layerStatesArray;
+  var numLayers = layerStates.length;
+  var i;
+  for (i = numLayers - 1; i >= 0; --i) {
+    var layerState = layerStates[i];
+    var layer = layerState.layer;
+    if (ol.layer.Layer.visibleAtResolution(layerState, viewState.resolution) &&
+        layerFilter.call(thisArg, layer)) {
+      var layerRenderer = this.getLayerRenderer(layer);
+      hasFeature =
+          layerRenderer.hasFeatureAtCoordinate(coordinate, frameState);
+      if (hasFeature) {
+        return true;
+      }
+    }
+  }
+  return hasFeature;
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.renderer.webgl.Map.prototype.forEachLayerAtPixel = function(pixel, frameState, callback, thisArg,
+        layerFilter, thisArg2) {
+  if (this.getGL().isContextLost()) {
+    return false;
+  }
+
+  var viewState = frameState.viewState;
+  var result;
+
+  var layerStates = frameState.layerStatesArray;
+  var numLayers = layerStates.length;
+  var i;
+  for (i = numLayers - 1; i >= 0; --i) {
+    var layerState = layerStates[i];
+    var layer = layerState.layer;
+    if (ol.layer.Layer.visibleAtResolution(layerState, viewState.resolution) &&
+        layerFilter.call(thisArg, layer)) {
+      var layerRenderer = this.getLayerRenderer(layer);
+      result = layerRenderer.forEachLayerAtPixel(
+          pixel, frameState, callback, thisArg);
+      if (result) {
+        return result;
+      }
+    }
+  }
+  return undefined;
+};
+
+// FIXME recheck layer/map projection compatibility when projection changes
+// FIXME layer renderers should skip when they can't reproject
+// FIXME add tilt and height?
+
+goog.provide('ol.Map');
+goog.provide('ol.MapProperty');
+
+goog.require('goog.asserts');
+goog.require('goog.async.nextTick');
+goog.require('goog.vec.Mat4');
+goog.require('ol.Collection');
+goog.require('ol.CollectionEventType');
+goog.require('ol.MapBrowserEvent');
+goog.require('ol.MapBrowserEvent.EventType');
+goog.require('ol.MapBrowserEventHandler');
+goog.require('ol.MapEvent');
+goog.require('ol.MapEventType');
+goog.require('ol.Object');
+goog.require('ol.ObjectEvent');
+goog.require('ol.ObjectEventType');
+goog.require('ol.RendererType');
+goog.require('ol.TileQueue');
+goog.require('ol.View');
+goog.require('ol.ViewHint');
+goog.require('ol.array');
+goog.require('ol.control');
+goog.require('ol.dom');
+goog.require('ol.events');
+goog.require('ol.events.Event');
+goog.require('ol.events.EventType');
+goog.require('ol.extent');
+goog.require('ol.functions');
+goog.require('ol.has');
+goog.require('ol.interaction');
+goog.require('ol.layer.Base');
+goog.require('ol.layer.Group');
+goog.require('ol.object');
+goog.require('ol.proj');
+goog.require('ol.proj.common');
+goog.require('ol.renderer.Map');
+goog.require('ol.renderer.canvas.Map');
+goog.require('ol.renderer.dom.Map');
+goog.require('ol.renderer.webgl.Map');
+goog.require('ol.size');
+goog.require('ol.structs.PriorityQueue');
+goog.require('ol.vec.Mat4');
+
+
+/**
+ * @const
+ * @type {string}
+ */
+ol.OL3_URL = 'http://openlayers.org/';
+
+
+/**
+ * @const
+ * @type {string}
+ */
+ol.OL3_LOGO_URL = 'data:image/png;base64,' +
+    'iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAAA3NCSVQICAjb4U/gAAAACXBI' +
+    'WXMAAAHGAAABxgEXwfpGAAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAA' +
+    'AhNQTFRF////AP//AICAgP//AFVVQECA////K1VVSbbbYL/fJ05idsTYJFtbbcjbJllmZszW' +
+    'WMTOIFhoHlNiZszTa9DdUcHNHlNlV8XRIVdiasrUHlZjIVZjaMnVH1RlIFRkH1RkH1ZlasvY' +
+    'asvXVsPQH1VkacnVa8vWIVZjIFRjVMPQa8rXIVVkXsXRsNveIFVkIFZlIVVj3eDeh6GmbMvX' +
+    'H1ZkIFRka8rWbMvXIFVkIFVjIFVkbMvWH1VjbMvWIFVlbcvWIFVla8vVIFVkbMvWbMvVH1Vk' +
+    'bMvWIFVlbcvWIFVkbcvVbMvWjNPbIFVkU8LPwMzNIFVkbczWIFVkbsvWbMvXIFVkRnB8bcvW' +
+    '2+TkW8XRIFVkIlZlJVloJlpoKlxrLl9tMmJwOWd0Omh1RXF8TneCT3iDUHiDU8LPVMLPVcLP' +
+    'VcPQVsPPVsPQV8PQWMTQWsTQW8TQXMXSXsXRX4SNX8bSYMfTYcfTYsfTY8jUZcfSZsnUaIqT' +
+    'acrVasrVa8jTa8rWbI2VbMvWbcvWdJObdcvUdszUd8vVeJaee87Yfc3WgJyjhqGnitDYjaar' +
+    'ldPZnrK2oNbborW5o9bbo9fbpLa6q9ndrL3ArtndscDDutzfu8fJwN7gwt7gxc/QyuHhy+Hi' +
+    'zeHi0NfX0+Pj19zb1+Tj2uXk29/e3uLg3+Lh3+bl4uXj4ufl4+fl5Ofl5ufl5ujm5+jmySDn' +
+    'BAAAAFp0Uk5TAAECAgMEBAYHCA0NDg4UGRogIiMmKSssLzU7PkJJT1JTVFliY2hrdHZ3foSF' +
+    'hYeJjY2QkpugqbG1tre5w8zQ09XY3uXn6+zx8vT09vf4+Pj5+fr6/P39/f3+gz7SsAAAAVVJ' +
+    'REFUOMtjYKA7EBDnwCPLrObS1BRiLoJLnte6CQy8FLHLCzs2QUG4FjZ5GbcmBDDjxJBXDWxC' +
+    'Brb8aM4zbkIDzpLYnAcE9VXlJSWlZRU13koIeW57mGx5XjoMZEUqwxWYQaQbSzLSkYGfKFSe' +
+    '0QMsX5WbjgY0YS4MBplemI4BdGBW+DQ11eZiymfqQuXZIjqwyadPNoSZ4L+0FVM6e+oGI6g8' +
+    'a9iKNT3o8kVzNkzRg5lgl7p4wyRUL9Yt2jAxVh6mQCogae6GmflI8p0r13VFWTHBQ0rWPW7a' +
+    'hgWVcPm+9cuLoyy4kCJDzCm6d8PSFoh0zvQNC5OjDJhQopPPJqph1doJBUD5tnkbZiUEqaCn' +
+    'B3bTqLTFG1bPn71kw4b+GFdpLElKIzRxxgYgWNYc5SCENVHKeUaltHdXx0dZ8uBI1hJ2UUDg' +
+    'q82CM2MwKeibqAvSO7MCABq0wXEPiqWEAAAAAElFTkSuQmCC';
+
+
+/**
+ * @type {Array.<ol.RendererType>}
+ * @const
+ */
+ol.DEFAULT_RENDERER_TYPES = [
+  ol.RendererType.CANVAS,
+  ol.RendererType.WEBGL,
+  ol.RendererType.DOM
+];
+
+
+/**
+ * @enum {string}
+ */
+ol.MapProperty = {
+  LAYERGROUP: 'layergroup',
+  SIZE: 'size',
+  TARGET: 'target',
+  VIEW: 'view'
+};
+
+
+/**
+ * @classdesc
+ * The map is the core component of OpenLayers. For a map to render, a view,
+ * one or more layers, and a target container are needed:
+ *
+ *     var map = new ol.Map({
+ *       view: new ol.View({
+ *         center: [0, 0],
+ *         zoom: 1
+ *       }),
+ *       layers: [
+ *         new ol.layer.Tile({
+ *           source: new ol.source.OSM()
+ *         })
+ *       ],
+ *       target: 'map'
+ *     });
+ *
+ * The above snippet creates a map using a {@link ol.layer.Tile} to display
+ * {@link ol.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
+ * `ol-viewport`) in the target element (see `getViewport()`), and then two
+ * further elements within the viewport: one with CSS class name
+ * `ol-overlaycontainer-stopevent` for controls and some overlays, and one with
+ * CSS class name `ol-overlaycontainer` for other overlays (see the `stopEvent`
+ * option of {@link ol.Overlay} for the difference). The map itself is placed in
+ * a further element within the viewport, either DOM or Canvas, depending on the
+ * renderer.
+ *
+ * Layers are stored as a `ol.Collection` in layerGroups. A top-level group is
+ * provided by the library. This is what is accessed by `getLayerGroup` and
+ * `setLayerGroup`. Layers entered in the options are added to this group, and
+ * `addLayer` and `removeLayer` change the layer collection in the group.
+ * `getLayers` is a convenience function for `getLayerGroup().getLayers()`.
+ * Note that `ol.layer.Group` is a subclass of `ol.layer.Base`, so layers
+ * entered in the options or added with `addLayer` can be groups, which can
+ * contain further groups, and so on.
+ *
+ * @constructor
+ * @extends {ol.Object}
+ * @param {olx.MapOptions} options Map options.
+ * @fires ol.MapBrowserEvent
+ * @fires ol.MapEvent
+ * @fires ol.render.Event#postcompose
+ * @fires ol.render.Event#precompose
+ * @api stable
+ */
+ol.Map = function(options) {
+
+  ol.Object.call(this);
+
+  var optionsInternal = ol.Map.createOptionsInternal(options);
+
+  /**
+   * @type {boolean}
+   * @private
+   */
+  this.loadTilesWhileAnimating_ =
+      options.loadTilesWhileAnimating !== undefined ?
+          options.loadTilesWhileAnimating : false;
+
+  /**
+   * @type {boolean}
+   * @private
+   */
+  this.loadTilesWhileInteracting_ =
+      options.loadTilesWhileInteracting !== undefined ?
+          options.loadTilesWhileInteracting : false;
+
+  /**
+   * @private
+   * @type {number}
+   */
+  this.pixelRatio_ = options.pixelRatio !== undefined ?
+      options.pixelRatio : ol.has.DEVICE_PIXEL_RATIO;
+
+  /**
+   * @private
+   * @type {Object.<string, string>}
+   */
+  this.logos_ = optionsInternal.logos;
+
+  /**
+   * @private
+   * @type {number|undefined}
+   */
+  this.animationDelayKey_;
+
+  /**
+   * @private
+   */
+  this.animationDelay_ = function() {
+    this.animationDelayKey_ = undefined;
+    this.renderFrame_.call(this, Date.now());
+  }.bind(this);
+
+  /**
+   * @private
+   * @type {goog.vec.Mat4.Number}
+   */
+  this.coordinateToPixelMatrix_ = goog.vec.Mat4.createNumber();
+
+  /**
+   * @private
+   * @type {goog.vec.Mat4.Number}
+   */
+  this.pixelToCoordinateMatrix_ = goog.vec.Mat4.createNumber();
+
+  /**
+   * @private
+   * @type {number}
+   */
+  this.frameIndex_ = 0;
+
+  /**
+   * @private
+   * @type {?olx.FrameState}
+   */
+  this.frameState_ = null;
+
+  /**
+   * The extent at the previous 'moveend' event.
+   * @private
+   * @type {ol.Extent}
+   */
+  this.previousExtent_ = ol.extent.createEmpty();
+
+  /**
+   * @private
+   * @type {?ol.EventsKey}
+   */
+  this.viewPropertyListenerKey_ = null;
+
+  /**
+   * @private
+   * @type {Array.<ol.EventsKey>}
+   */
+  this.layerGroupPropertyListenerKeys_ = null;
+
+  /**
+   * @private
+   * @type {Element}
+   */
+  this.viewport_ = document.createElement('DIV');
+  this.viewport_.className = 'ol-viewport' + (ol.has.TOUCH ? ' ol-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 = 'ol-overlaycontainer';
+  this.viewport_.appendChild(this.overlayContainer_);
+
+  /**
+   * @private
+   * @type {!Element}
+   */
+  this.overlayContainerStopEvent_ = document.createElement('DIV');
+  this.overlayContainerStopEvent_.className = 'ol-overlaycontainer-stopevent';
+  var overlayEvents = [
+    ol.events.EventType.CLICK,
+    ol.events.EventType.DBLCLICK,
+    ol.events.EventType.MOUSEDOWN,
+    ol.events.EventType.TOUCHSTART,
+    ol.events.EventType.MSPOINTERDOWN,
+    ol.MapBrowserEvent.EventType.POINTERDOWN,
+    ol.events.EventType.MOUSEWHEEL,
+    ol.events.EventType.WHEEL
+  ];
+  for (var i = 0, ii = overlayEvents.length; i < ii; ++i) {
+    ol.events.listen(this.overlayContainerStopEvent_, overlayEvents[i],
+        ol.events.Event.stopPropagation);
+  }
+  this.viewport_.appendChild(this.overlayContainerStopEvent_);
+
+  /**
+   * @private
+   * @type {ol.MapBrowserEventHandler}
+   */
+  this.mapBrowserEventHandler_ = new ol.MapBrowserEventHandler(this);
+  for (var key in ol.MapBrowserEvent.EventType) {
+    ol.events.listen(this.mapBrowserEventHandler_, ol.MapBrowserEvent.EventType[key],
+        this.handleMapBrowserEvent, this);
+  }
+
+  /**
+   * @private
+   * @type {Element|Document}
+   */
+  this.keyboardEventTarget_ = optionsInternal.keyboardEventTarget;
+
+  /**
+   * @private
+   * @type {Array.<ol.EventsKey>}
+   */
+  this.keyHandlerKeys_ = null;
+
+  ol.events.listen(this.viewport_, ol.events.EventType.WHEEL,
+      this.handleBrowserEvent, this);
+  ol.events.listen(this.viewport_, ol.events.EventType.MOUSEWHEEL,
+      this.handleBrowserEvent, this);
+
+  /**
+   * @type {ol.Collection.<ol.control.Control>}
+   * @private
+   */
+  this.controls_ = optionsInternal.controls;
+
+  /**
+   * @type {ol.Collection.<ol.interaction.Interaction>}
+   * @private
+   */
+  this.interactions_ = optionsInternal.interactions;
+
+  /**
+   * @type {ol.Collection.<ol.Overlay>}
+   * @private
+   */
+  this.overlays_ = optionsInternal.overlays;
+
+  /**
+   * A lookup of overlays by id.
+   * @private
+   * @type {Object.<string, ol.Overlay>}
+   */
+  this.overlayIdIndex_ = {};
+
+  /**
+   * @type {ol.renderer.Map}
+   * @private
+   */
+  this.renderer_ = new optionsInternal.rendererConstructor(this.viewport_, this);
+
+  /**
+   * @type {function(Event)|undefined}
+   * @private
+   */
+  this.handleResize_;
+
+  /**
+   * @private
+   * @type {ol.Coordinate}
+   */
+  this.focus_ = null;
+
+  /**
+   * @private
+   * @type {Array.<ol.PreRenderFunction>}
+   */
+  this.preRenderFunctions_ = [];
+
+  /**
+   * @private
+   * @type {Array.<ol.PostRenderFunction>}
+   */
+  this.postRenderFunctions_ = [];
+
+  /**
+   * @private
+   * @type {ol.TileQueue}
+   */
+  this.tileQueue_ = new ol.TileQueue(
+      this.getTilePriority.bind(this),
+      this.handleTileChange_.bind(this));
+
+  /**
+   * Uids of features to skip at rendering time.
+   * @type {Object.<string, boolean>}
+   * @private
+   */
+  this.skippedFeatureUids_ = {};
+
+  ol.events.listen(
+      this, ol.Object.getChangeEventType(ol.MapProperty.LAYERGROUP),
+      this.handleLayerGroupChanged_, this);
+  ol.events.listen(this, ol.Object.getChangeEventType(ol.MapProperty.VIEW),
+      this.handleViewChanged_, this);
+  ol.events.listen(this, ol.Object.getChangeEventType(ol.MapProperty.SIZE),
+      this.handleSizeChanged_, this);
+  ol.events.listen(this, ol.Object.getChangeEventType(ol.MapProperty.TARGET),
+      this.handleTargetChanged_, this);
+
+  // setProperties will trigger the rendering of the map if the map
+  // is "defined" already.
+  this.setProperties(optionsInternal.values);
+
+  this.controls_.forEach(
+      /**
+       * @param {ol.control.Control} control Control.
+       * @this {ol.Map}
+       */
+      function(control) {
+        control.setMap(this);
+      }, this);
+
+  ol.events.listen(this.controls_, ol.CollectionEventType.ADD,
+      /**
+       * @param {ol.CollectionEvent} event Collection event.
+       */
+      function(event) {
+        event.element.setMap(this);
+      }, this);
+
+  ol.events.listen(this.controls_, ol.CollectionEventType.REMOVE,
+      /**
+       * @param {ol.CollectionEvent} event Collection event.
+       */
+      function(event) {
+        event.element.setMap(null);
+      }, this);
+
+  this.interactions_.forEach(
+      /**
+       * @param {ol.interaction.Interaction} interaction Interaction.
+       * @this {ol.Map}
+       */
+      function(interaction) {
+        interaction.setMap(this);
+      }, this);
+
+  ol.events.listen(this.interactions_, ol.CollectionEventType.ADD,
+      /**
+       * @param {ol.CollectionEvent} event Collection event.
+       */
+      function(event) {
+        event.element.setMap(this);
+      }, this);
+
+  ol.events.listen(this.interactions_, ol.CollectionEventType.REMOVE,
+      /**
+       * @param {ol.CollectionEvent} event Collection event.
+       */
+      function(event) {
+        event.element.setMap(null);
+      }, this);
+
+  this.overlays_.forEach(this.addOverlayInternal_, this);
+
+  ol.events.listen(this.overlays_, ol.CollectionEventType.ADD,
+      /**
+       * @param {ol.CollectionEvent} event Collection event.
+       */
+      function(event) {
+        this.addOverlayInternal_(/** @type {ol.Overlay} */ (event.element));
+      }, this);
+
+  ol.events.listen(this.overlays_, ol.CollectionEventType.REMOVE,
+      /**
+       * @param {ol.CollectionEvent} event Collection event.
+       */
+      function(event) {
+        var id = event.element.getId();
+        if (id !== undefined) {
+          delete this.overlayIdIndex_[id.toString()];
+        }
+        event.element.setMap(null);
+      }, this);
+
+};
+ol.inherits(ol.Map, ol.Object);
+
+
+/**
+ * Add the given control to the map.
+ * @param {ol.control.Control} control Control.
+ * @api stable
+ */
+ol.Map.prototype.addControl = function(control) {
+  var controls = this.getControls();
+  goog.asserts.assert(controls !== undefined, 'controls should be defined');
+  controls.push(control);
+};
+
+
+/**
+ * Add the given interaction to the map.
+ * @param {ol.interaction.Interaction} interaction Interaction to add.
+ * @api stable
+ */
+ol.Map.prototype.addInteraction = function(interaction) {
+  var interactions = this.getInteractions();
+  goog.asserts.assert(interactions !== undefined,
+      'interactions should be defined');
+  interactions.push(interaction);
+};
+
+
+/**
+ * Adds the given layer to the top of this map. If you want to add a layer
+ * elsewhere in the stack, use `getLayers()` and the methods available on
+ * {@link ol.Collection}.
+ * @param {ol.layer.Base} layer Layer.
+ * @api stable
+ */
+ol.Map.prototype.addLayer = function(layer) {
+  var layers = this.getLayerGroup().getLayers();
+  layers.push(layer);
+};
+
+
+/**
+ * Add the given overlay to the map.
+ * @param {ol.Overlay} overlay Overlay.
+ * @api stable
+ */
+ol.Map.prototype.addOverlay = function(overlay) {
+  var overlays = this.getOverlays();
+  goog.asserts.assert(overlays !== undefined, 'overlays should be defined');
+  overlays.push(overlay);
+};
+
+
+/**
+ * This deals with map's overlay collection changes.
+ * @param {ol.Overlay} overlay Overlay.
+ * @private
+ */
+ol.Map.prototype.addOverlayInternal_ = function(overlay) {
+  var id = overlay.getId();
+  if (id !== undefined) {
+    this.overlayIdIndex_[id.toString()] = overlay;
+  }
+  overlay.setMap(this);
+};
+
+
+/**
+ * Add functions to be called before rendering. This can be used for attaching
+ * animations before updating the map's view.  The {@link ol.animation}
+ * namespace provides several static methods for creating prerender functions.
+ * @param {...ol.PreRenderFunction} var_args Any number of pre-render functions.
+ * @api
+ */
+ol.Map.prototype.beforeRender = function(var_args) {
+  this.render();
+  Array.prototype.push.apply(this.preRenderFunctions_, arguments);
+};
+
+
+/**
+ * @param {ol.PreRenderFunction} preRenderFunction Pre-render function.
+ * @return {boolean} Whether the preRenderFunction has been found and removed.
+ */
+ol.Map.prototype.removePreRenderFunction = function(preRenderFunction) {
+  return ol.array.remove(this.preRenderFunctions_, preRenderFunction);
+};
+
+
+/**
+ *
+ * @inheritDoc
+ */
+ol.Map.prototype.disposeInternal = function() {
+  this.mapBrowserEventHandler_.dispose();
+  this.renderer_.dispose();
+  ol.events.unlisten(this.viewport_, ol.events.EventType.WHEEL,
+      this.handleBrowserEvent, this);
+  ol.events.unlisten(this.viewport_, ol.events.EventType.MOUSEWHEEL,
+      this.handleBrowserEvent, this);
+  if (this.handleResize_ !== undefined) {
+    ol.global.removeEventListener(ol.events.EventType.RESIZE,
+        this.handleResize_, false);
+    this.handleResize_ = undefined;
+  }
+  if (this.animationDelayKey_) {
+    ol.global.cancelAnimationFrame(this.animationDelayKey_);
+    this.animationDelayKey_ = undefined;
+  }
+  this.setTarget(null);
+  ol.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 `opt_layerFilter`.
+ * @param {ol.Pixel} pixel Pixel.
+ * @param {function(this: S, (ol.Feature|ol.render.Feature),
+ *     ol.layer.Layer): T} callback Feature callback. The callback will be
+ *     called with two arguments. The first argument is one
+ *     {@link ol.Feature feature} or
+ *     {@link ol.render.Feature render feature} at the pixel, the second is
+ *     the {@link ol.layer.Layer layer} of the feature and will be null for
+ *     unmanaged layers. To stop detection, callback functions can return a
+ *     truthy value.
+ * @param {S=} opt_this Value to use as `this` when executing `callback`.
+ * @param {(function(this: U, ol.layer.Layer): boolean)=} opt_layerFilter Layer
+ *     filter function. The filter function will receive one argument, the
+ *     {@link ol.layer.Layer layer-candidate} and it should return a boolean
+ *     value. Only layers which are visible and for which this function returns
+ *     `true` will be tested for features. By default, all visible layers will
+ *     be tested.
+ * @param {U=} opt_this2 Value to use as `this` when executing `layerFilter`.
+ * @return {T|undefined} Callback result, i.e. the return value of last
+ * callback execution, or the first truthy callback return value.
+ * @template S,T,U
+ * @api stable
+ */
+ol.Map.prototype.forEachFeatureAtPixel = function(pixel, callback, opt_this, opt_layerFilter, opt_this2) {
+  if (!this.frameState_) {
+    return;
+  }
+  var coordinate = this.getCoordinateFromPixel(pixel);
+  var thisArg = opt_this !== undefined ? opt_this : null;
+  var layerFilter = opt_layerFilter !== undefined ?
+      opt_layerFilter : ol.functions.TRUE;
+  var thisArg2 = opt_this2 !== undefined ? opt_this2 : null;
+  return this.renderer_.forEachFeatureAtCoordinate(
+      coordinate, this.frameState_, callback, thisArg,
+      layerFilter, thisArg2);
+};
+
+
+/**
+ * Detect layers that have a color value at a pixel on the viewport, and
+ * execute a callback with each matching layer. Layers included in the
+ * detection can be configured through `opt_layerFilter`.
+ * @param {ol.Pixel} pixel Pixel.
+ * @param {function(this: S, ol.layer.Layer): T} callback Layer
+ *     callback. Will receive one argument, the {@link ol.layer.Layer layer}
+ *     that contains the color pixel. To stop detection, callback functions can
+ *     return a truthy value.
+ * @param {S=} opt_this Value to use as `this` when executing `callback`.
+ * @param {(function(this: U, ol.layer.Layer): boolean)=} opt_layerFilter Layer
+ *     filter function. The filter function will receive one argument, the
+ *     {@link ol.layer.Layer layer-candidate} and it should return a boolean
+ *     value. Only layers which are visible and for which this function returns
+ *     `true` will be tested for features. By default, all visible layers will
+ *     be tested.
+ * @param {U=} opt_this2 Value to use as `this` when executing `layerFilter`.
+ * @return {T|undefined} Callback result, i.e. the return value of last
+ * callback execution, or the first truthy callback return value.
+ * @template S,T,U
+ * @api stable
+ */
+ol.Map.prototype.forEachLayerAtPixel = function(pixel, callback, opt_this, opt_layerFilter, opt_this2) {
+  if (!this.frameState_) {
+    return;
+  }
+  var thisArg = opt_this !== undefined ? opt_this : null;
+  var layerFilter = opt_layerFilter !== undefined ?
+      opt_layerFilter : ol.functions.TRUE;
+  var thisArg2 = opt_this2 !== undefined ? opt_this2 : null;
+  return this.renderer_.forEachLayerAtPixel(
+      pixel, this.frameState_, callback, thisArg,
+      layerFilter, thisArg2);
+};
+
+
+/**
+ * Detect if features intersect a pixel on the viewport. Layers included in the
+ * detection can be configured through `opt_layerFilter`.
+ * @param {ol.Pixel} pixel Pixel.
+ * @param {(function(this: U, ol.layer.Layer): boolean)=} opt_layerFilter Layer
+ *     filter function. The filter function will receive one argument, the
+ *     {@link ol.layer.Layer layer-candidate} and it should return a boolean
+ *     value. Only layers which are visible and for which this function returns
+ *     `true` will be tested for features. By default, all visible layers will
+ *     be tested.
+ * @param {U=} opt_this Value to use as `this` when executing `layerFilter`.
+ * @return {boolean} Is there a feature at the given pixel?
+ * @template U
+ * @api
+ */
+ol.Map.prototype.hasFeatureAtPixel = function(pixel, opt_layerFilter, opt_this) {
+  if (!this.frameState_) {
+    return false;
+  }
+  var coordinate = this.getCoordinateFromPixel(pixel);
+  var layerFilter = opt_layerFilter !== undefined ?
+      opt_layerFilter : ol.functions.TRUE;
+  var thisArg = opt_this !== undefined ? opt_this : null;
+  return this.renderer_.hasFeatureAtCoordinate(
+      coordinate, this.frameState_, layerFilter, thisArg);
+};
+
+
+/**
+ * Returns the geographical coordinate for a browser event.
+ * @param {Event} event Event.
+ * @return {ol.Coordinate} Coordinate.
+ * @api stable
+ */
+ol.Map.prototype.getEventCoordinate = function(event) {
+  return this.getCoordinateFromPixel(this.getEventPixel(event));
+};
+
+
+/**
+ * Returns the map pixel position for a browser event relative to the viewport.
+ * @param {Event} event Event.
+ * @return {ol.Pixel} Pixel.
+ * @api stable
+ */
+ol.Map.prototype.getEventPixel = function(event) {
+  var 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 stable
+ */
+ol.Map.prototype.getTarget = function() {
+  return /** @type {Element|string|undefined} */ (
+      this.get(ol.MapProperty.TARGET));
+};
+
+
+/**
+ * Get the DOM element into which this map is rendered. In contrast to
+ * `getTarget` this method always return an `Element`, or `null` if the
+ * map has no target.
+ * @return {Element} The element that the map is rendered in.
+ * @api
+ */
+ol.Map.prototype.getTargetElement = function() {
+  var target = this.getTarget();
+  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 {ol.Pixel} pixel Pixel position in the map viewport.
+ * @return {ol.Coordinate} The coordinate for the pixel position.
+ * @api stable
+ */
+ol.Map.prototype.getCoordinateFromPixel = function(pixel) {
+  var frameState = this.frameState_;
+  if (!frameState) {
+    return null;
+  } else {
+    var vec2 = pixel.slice();
+    return ol.vec.Mat4.multVec2(frameState.pixelToCoordinateMatrix, vec2, vec2);
+  }
+};
+
+
+/**
+ * Get the map controls. Modifying this collection changes the controls
+ * associated with the map.
+ * @return {ol.Collection.<ol.control.Control>} Controls.
+ * @api stable
+ */
+ol.Map.prototype.getControls = function() {
+  return this.controls_;
+};
+
+
+/**
+ * Get the map overlays. Modifying this collection changes the overlays
+ * associated with the map.
+ * @return {ol.Collection.<ol.Overlay>} Overlays.
+ * @api stable
+ */
+ol.Map.prototype.getOverlays = function() {
+  return this.overlays_;
+};
+
+
+/**
+ * Get an overlay by its identifier (the value returned by overlay.getId()).
+ * Note that the index treats string and numeric identifiers as the same. So
+ * `map.getOverlayById(2)` will return an overlay with id `'2'` or `2`.
+ * @param {string|number} id Overlay identifier.
+ * @return {ol.Overlay} Overlay.
+ * @api
+ */
+ol.Map.prototype.getOverlayById = function(id) {
+  var overlay = this.overlayIdIndex_[id.toString()];
+  return overlay !== undefined ? overlay : null;
+};
+
+
+/**
+ * Get the map interactions. Modifying this collection changes the interactions
+ * associated with the map.
+ *
+ * Interactions are used for e.g. pan, zoom and rotate.
+ * @return {ol.Collection.<ol.interaction.Interaction>} Interactions.
+ * @api stable
+ */
+ol.Map.prototype.getInteractions = function() {
+  return this.interactions_;
+};
+
+
+/**
+ * Get the layergroup associated with this map.
+ * @return {ol.layer.Group} A layer group containing the layers in this map.
+ * @observable
+ * @api stable
+ */
+ol.Map.prototype.getLayerGroup = function() {
+  return /** @type {ol.layer.Group} */ (this.get(ol.MapProperty.LAYERGROUP));
+};
+
+
+/**
+ * Get the collection of layers associated with this map.
+ * @return {!ol.Collection.<ol.layer.Base>} Layers.
+ * @api stable
+ */
+ol.Map.prototype.getLayers = function() {
+  var layers = this.getLayerGroup().getLayers();
+  return layers;
+};
+
+
+/**
+ * Get the pixel for a coordinate.  This takes a coordinate in the map view
+ * projection and returns the corresponding pixel.
+ * @param {ol.Coordinate} coordinate A map coordinate.
+ * @return {ol.Pixel} A pixel position in the map viewport.
+ * @api stable
+ */
+ol.Map.prototype.getPixelFromCoordinate = function(coordinate) {
+  var frameState = this.frameState_;
+  if (!frameState) {
+    return null;
+  } else {
+    var vec2 = coordinate.slice(0, 2);
+    return ol.vec.Mat4.multVec2(frameState.coordinateToPixelMatrix, vec2, vec2);
+  }
+};
+
+
+/**
+ * Get the map renderer.
+ * @return {ol.renderer.Map} Renderer
+ */
+ol.Map.prototype.getRenderer = function() {
+  return this.renderer_;
+};
+
+
+/**
+ * Get the size of this map.
+ * @return {ol.Size|undefined} The size in pixels of the map in the DOM.
+ * @observable
+ * @api stable
+ */
+ol.Map.prototype.getSize = function() {
+  return /** @type {ol.Size|undefined} */ (this.get(ol.MapProperty.SIZE));
+};
+
+
+/**
+ * Get the view associated with this map. A view manages properties such as
+ * center and resolution.
+ * @return {ol.View} The view that controls this map.
+ * @observable
+ * @api stable
+ */
+ol.Map.prototype.getView = function() {
+  return /** @type {ol.View} */ (this.get(ol.MapProperty.VIEW));
+};
+
+
+/**
+ * Get the element that serves as the map viewport.
+ * @return {Element} Viewport.
+ * @api stable
+ */
+ol.Map.prototype.getViewport = function() {
+  return this.viewport_;
+};
+
+
+/**
+ * Get the element that serves as the container for overlays.  Elements added to
+ * this container will let mousedown and touchstart events through to the map,
+ * so clicks and gestures on an overlay will trigger {@link ol.MapBrowserEvent}
+ * events.
+ * @return {!Element} The map's overlay container.
+ */
+ol.Map.prototype.getOverlayContainer = function() {
+  return this.overlayContainer_;
+};
+
+
+/**
+ * Get the element that serves as a container for overlays that don't allow
+ * event propagation. Elements added to this container won't let mousedown and
+ * touchstart events through to the map, so clicks and gestures on an overlay
+ * don't trigger any {@link ol.MapBrowserEvent}.
+ * @return {!Element} The map's overlay container that stops events.
+ */
+ol.Map.prototype.getOverlayContainerStopEvent = function() {
+  return this.overlayContainerStopEvent_;
+};
+
+
+/**
+ * @param {ol.Tile} tile Tile.
+ * @param {string} tileSourceKey Tile source key.
+ * @param {ol.Coordinate} tileCenter Tile center.
+ * @param {number} tileResolution Tile resolution.
+ * @return {number} Tile priority.
+ */
+ol.Map.prototype.getTilePriority = function(tile, tileSourceKey, tileCenter, tileResolution) {
+  // Filter out tiles at higher zoom levels than the current zoom level, or that
+  // are outside the visible extent.
+  var frameState = this.frameState_;
+  if (!frameState || !(tileSourceKey in frameState.wantedTiles)) {
+    return ol.structs.PriorityQueue.DROP;
+  }
+  var coordKey = tile.tileCoord.toString();
+  if (!frameState.wantedTiles[tileSourceKey][coordKey]) {
+    return ol.structs.PriorityQueue.DROP;
+  }
+  // Prioritize the highest zoom level tiles closest to the focus.
+  // Tiles at higher zoom levels are prioritized using Math.log(tileResolution).
+  // Within a zoom level, tiles are prioritized by the distance in pixels
+  // between the center of the tile and the focus.  The factor of 65536 means
+  // that the prioritization should behave as desired for tiles up to
+  // 65536 * Math.log(2) = 45426 pixels from the focus.
+  var deltaX = tileCenter[0] - frameState.focus[0];
+  var deltaY = tileCenter[1] - frameState.focus[1];
+  return 65536 * Math.log(tileResolution) +
+      Math.sqrt(deltaX * deltaX + deltaY * deltaY) / tileResolution;
+};
+
+
+/**
+ * @param {Event} browserEvent Browser event.
+ * @param {string=} opt_type Type.
+ */
+ol.Map.prototype.handleBrowserEvent = function(browserEvent, opt_type) {
+  var type = opt_type || browserEvent.type;
+  var mapBrowserEvent = new ol.MapBrowserEvent(type, this, browserEvent);
+  this.handleMapBrowserEvent(mapBrowserEvent);
+};
+
+
+/**
+ * @param {ol.MapBrowserEvent} mapBrowserEvent The event to handle.
+ */
+ol.Map.prototype.handleMapBrowserEvent = function(mapBrowserEvent) {
+  if (!this.frameState_) {
+    // With no view defined, we cannot translate pixels into geographical
+    // coordinates so interactions cannot be used.
+    return;
+  }
+  this.focus_ = mapBrowserEvent.coordinate;
+  mapBrowserEvent.frameState = this.frameState_;
+  var interactions = this.getInteractions();
+  goog.asserts.assert(interactions !== undefined,
+      'interactions should be defined');
+  var interactionsArray = interactions.getArray();
+  var i;
+  if (this.dispatchEvent(mapBrowserEvent) !== false) {
+    for (i = interactionsArray.length - 1; i >= 0; i--) {
+      var interaction = interactionsArray[i];
+      if (!interaction.getActive()) {
+        continue;
+      }
+      var cont = interaction.handleEvent(mapBrowserEvent);
+      if (!cont) {
+        break;
+      }
+    }
+  }
+};
+
+
+/**
+ * @protected
+ */
+ol.Map.prototype.handlePostRender = function() {
+
+  var frameState = this.frameState_;
+
+  // Manage the tile queue
+  // Image loads are expensive and a limited resource, so try to use them
+  // efficiently:
+  // * When the view is static we allow a large number of parallel tile loads
+  //   to complete the frame as quickly as possible.
+  // * When animating or interacting, image loads can cause janks, so we reduce
+  //   the maximum number of loads per frame and limit the number of parallel
+  //   tile loads to remain reactive to view changes and to reduce the chance of
+  //   loading tiles that will quickly disappear from view.
+  var tileQueue = this.tileQueue_;
+  if (!tileQueue.isEmpty()) {
+    var maxTotalLoading = 16;
+    var maxNewLoads = maxTotalLoading;
+    if (frameState) {
+      var hints = frameState.viewHints;
+      if (hints[ol.ViewHint.ANIMATING]) {
+        maxTotalLoading = this.loadTilesWhileAnimating_ ? 8 : 0;
+        maxNewLoads = 2;
+      }
+      if (hints[ol.ViewHint.INTERACTING]) {
+        maxTotalLoading = this.loadTilesWhileInteracting_ ? 8 : 0;
+        maxNewLoads = 2;
+      }
+    }
+    if (tileQueue.getTilesLoading() < maxTotalLoading) {
+      tileQueue.reprioritize(); // FIXME only call if view has changed
+      tileQueue.loadMoreTiles(maxTotalLoading, maxNewLoads);
+    }
+  }
+
+  var postRenderFunctions = this.postRenderFunctions_;
+  var i, ii;
+  for (i = 0, ii = postRenderFunctions.length; i < ii; ++i) {
+    postRenderFunctions[i](this, frameState);
+  }
+  postRenderFunctions.length = 0;
+};
+
+
+/**
+ * @private
+ */
+ol.Map.prototype.handleSizeChanged_ = function() {
+  this.render();
+};
+
+
+/**
+ * @private
+ */
+ol.Map.prototype.handleTargetChanged_ = function() {
+  // target may be undefined, null, a string or an Element.
+  // If it's a string we convert it to an Element before proceeding.
+  // If it's not now an Element we remove the viewport from the DOM.
+  // If it's an Element we append the viewport element to it.
+
+  var targetElement;
+  if (this.getTarget()) {
+    targetElement = this.getTargetElement();
+    goog.asserts.assert(targetElement !== null,
+        'expects a non-null value for targetElement');
+  }
+
+  if (this.keyHandlerKeys_) {
+    for (var i = 0, ii = this.keyHandlerKeys_.length; i < ii; ++i) {
+      ol.events.unlistenByKey(this.keyHandlerKeys_[i]);
+    }
+    this.keyHandlerKeys_ = null;
+  }
+
+  if (!targetElement) {
+    ol.dom.removeNode(this.viewport_);
+    if (this.handleResize_ !== undefined) {
+      ol.global.removeEventListener(ol.events.EventType.RESIZE,
+          this.handleResize_, false);
+      this.handleResize_ = undefined;
+    }
+  } else {
+    targetElement.appendChild(this.viewport_);
+
+    var keyboardEventTarget = !this.keyboardEventTarget_ ?
+        targetElement : this.keyboardEventTarget_;
+    this.keyHandlerKeys_ = [
+      ol.events.listen(keyboardEventTarget, ol.events.EventType.KEYDOWN,
+          this.handleBrowserEvent, this),
+      ol.events.listen(keyboardEventTarget, ol.events.EventType.KEYPRESS,
+          this.handleBrowserEvent, this)
+    ];
+
+    if (!this.handleResize_) {
+      this.handleResize_ = this.updateSize.bind(this);
+      ol.global.addEventListener(ol.events.EventType.RESIZE,
+          this.handleResize_, false);
+    }
+  }
+
+  this.updateSize();
+  // updateSize calls setSize, so no need to call this.render
+  // ourselves here.
+};
+
+
+/**
+ * @private
+ */
+ol.Map.prototype.handleTileChange_ = function() {
+  this.render();
+};
+
+
+/**
+ * @private
+ */
+ol.Map.prototype.handleViewPropertyChanged_ = function() {
+  this.render();
+};
+
+
+/**
+ * @private
+ */
+ol.Map.prototype.handleViewChanged_ = function() {
+  if (this.viewPropertyListenerKey_) {
+    ol.events.unlistenByKey(this.viewPropertyListenerKey_);
+    this.viewPropertyListenerKey_ = null;
+  }
+  var view = this.getView();
+  if (view) {
+    this.viewPropertyListenerKey_ = ol.events.listen(
+        view, ol.ObjectEventType.PROPERTYCHANGE,
+        this.handleViewPropertyChanged_, this);
+  }
+  this.render();
+};
+
+
+/**
+ * @param {ol.events.Event} event Event.
+ * @private
+ */
+ol.Map.prototype.handleLayerGroupMemberChanged_ = function(event) {
+  goog.asserts.assertInstanceof(event, ol.events.Event,
+      'event should be an Event');
+  this.render();
+};
+
+
+/**
+ * @param {ol.ObjectEvent} event Event.
+ * @private
+ */
+ol.Map.prototype.handleLayerGroupPropertyChanged_ = function(event) {
+  goog.asserts.assertInstanceof(event, ol.ObjectEvent,
+      'event should be an ol.ObjectEvent');
+  this.render();
+};
+
+
+/**
+ * @private
+ */
+ol.Map.prototype.handleLayerGroupChanged_ = function() {
+  if (this.layerGroupPropertyListenerKeys_) {
+    this.layerGroupPropertyListenerKeys_.forEach(ol.events.unlistenByKey);
+    this.layerGroupPropertyListenerKeys_ = null;
+  }
+  var layerGroup = this.getLayerGroup();
+  if (layerGroup) {
+    this.layerGroupPropertyListenerKeys_ = [
+      ol.events.listen(
+          layerGroup, ol.ObjectEventType.PROPERTYCHANGE,
+          this.handleLayerGroupPropertyChanged_, this),
+      ol.events.listen(
+          layerGroup, ol.events.EventType.CHANGE,
+          this.handleLayerGroupMemberChanged_, this)
+    ];
+  }
+  this.render();
+};
+
+
+/**
+ * @return {boolean} Is rendered.
+ */
+ol.Map.prototype.isRendered = function() {
+  return !!this.frameState_;
+};
+
+
+/**
+ * Requests an immediate render in a synchronous manner.
+ * @api stable
+ */
+ol.Map.prototype.renderSync = function() {
+  if (this.animationDelayKey_) {
+    ol.global.cancelAnimationFrame(this.animationDelayKey_);
+  }
+  this.animationDelay_();
+};
+
+
+/**
+ * Request a map rendering (at the next animation frame).
+ * @api stable
+ */
+ol.Map.prototype.render = function() {
+  if (this.animationDelayKey_ === undefined) {
+    this.animationDelayKey_ = ol.global.requestAnimationFrame(
+        this.animationDelay_);
+  }
+};
+
+
+/**
+ * Remove the given control from the map.
+ * @param {ol.control.Control} control Control.
+ * @return {ol.control.Control|undefined} The removed control (or undefined
+ *     if the control was not found).
+ * @api stable
+ */
+ol.Map.prototype.removeControl = function(control) {
+  var controls = this.getControls();
+  goog.asserts.assert(controls !== undefined, 'controls should be defined');
+  return controls.remove(control);
+};
+
+
+/**
+ * Remove the given interaction from the map.
+ * @param {ol.interaction.Interaction} interaction Interaction to remove.
+ * @return {ol.interaction.Interaction|undefined} The removed interaction (or
+ *     undefined if the interaction was not found).
+ * @api stable
+ */
+ol.Map.prototype.removeInteraction = function(interaction) {
+  var interactions = this.getInteractions();
+  goog.asserts.assert(interactions !== undefined,
+      'interactions should be defined');
+  return interactions.remove(interaction);
+};
+
+
+/**
+ * Removes the given layer from the map.
+ * @param {ol.layer.Base} layer Layer.
+ * @return {ol.layer.Base|undefined} The removed layer (or undefined if the
+ *     layer was not found).
+ * @api stable
+ */
+ol.Map.prototype.removeLayer = function(layer) {
+  var layers = this.getLayerGroup().getLayers();
+  return layers.remove(layer);
+};
+
+
+/**
+ * Remove the given overlay from the map.
+ * @param {ol.Overlay} overlay Overlay.
+ * @return {ol.Overlay|undefined} The removed overlay (or undefined
+ *     if the overlay was not found).
+ * @api stable
+ */
+ol.Map.prototype.removeOverlay = function(overlay) {
+  var overlays = this.getOverlays();
+  goog.asserts.assert(overlays !== undefined, 'overlays should be defined');
+  return overlays.remove(overlay);
+};
+
+
+/**
+ * @param {number} time Time.
+ * @private
+ */
+ol.Map.prototype.renderFrame_ = function(time) {
+
+  var i, ii, viewState;
+
+  var size = this.getSize();
+  var view = this.getView();
+  var extent = ol.extent.createEmpty();
+  /** @type {?olx.FrameState} */
+  var frameState = null;
+  if (size !== undefined && ol.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[goog.getUid(layerStatesArray[i].layer)] = layerStatesArray[i];
+    }
+    viewState = view.getState();
+    frameState = /** @type {olx.FrameState} */ ({
+      animate: false,
+      attributions: {},
+      coordinateToPixelMatrix: this.coordinateToPixelMatrix_,
+      extent: extent,
+      focus: !this.focus_ ? viewState.center : this.focus_,
+      index: this.frameIndex_++,
+      layerStates: layerStates,
+      layerStatesArray: layerStatesArray,
+      logos: ol.object.assign({}, this.logos_),
+      pixelRatio: this.pixelRatio_,
+      pixelToCoordinateMatrix: this.pixelToCoordinateMatrix_,
+      postRenderFunctions: [],
+      size: size,
+      skippedFeatureUids: this.skippedFeatureUids_,
+      tileQueue: this.tileQueue_,
+      time: time,
+      usedTiles: {},
+      viewState: viewState,
+      viewHints: viewHints,
+      wantedTiles: {}
+    });
+  }
+
+  if (frameState) {
+    var preRenderFunctions = this.preRenderFunctions_;
+    var n = 0, preRenderFunction;
+    for (i = 0, ii = preRenderFunctions.length; i < ii; ++i) {
+      preRenderFunction = preRenderFunctions[i];
+      if (preRenderFunction(this, frameState)) {
+        preRenderFunctions[n++] = preRenderFunction;
+      }
+    }
+    preRenderFunctions.length = n;
+
+    frameState.extent = ol.extent.getForViewAndSize(viewState.center,
+        viewState.resolution, viewState.rotation, frameState.size, extent);
+  }
+
+  this.frameState_ = frameState;
+  this.renderer_.renderFrame(frameState);
+
+  if (frameState) {
+    if (frameState.animate) {
+      this.render();
+    }
+    Array.prototype.push.apply(
+        this.postRenderFunctions_, frameState.postRenderFunctions);
+
+    var idle = this.preRenderFunctions_.length === 0 &&
+        !frameState.viewHints[ol.ViewHint.ANIMATING] &&
+        !frameState.viewHints[ol.ViewHint.INTERACTING] &&
+        !ol.extent.equals(frameState.extent, this.previousExtent_);
+
+    if (idle) {
+      this.dispatchEvent(
+          new ol.MapEvent(ol.MapEventType.MOVEEND, this, frameState));
+      ol.extent.clone(frameState.extent, this.previousExtent_);
+    }
+  }
+
+  this.dispatchEvent(
+      new ol.MapEvent(ol.MapEventType.POSTRENDER, this, frameState));
+
+  goog.async.nextTick(this.handlePostRender, this);
+
+};
+
+
+/**
+ * Sets the layergroup of this map.
+ * @param {ol.layer.Group} layerGroup A layer group containing the layers in
+ *     this map.
+ * @observable
+ * @api stable
+ */
+ol.Map.prototype.setLayerGroup = function(layerGroup) {
+  this.set(ol.MapProperty.LAYERGROUP, layerGroup);
+};
+
+
+/**
+ * Set the size of this map.
+ * @param {ol.Size|undefined} size The size in pixels of the map in the DOM.
+ * @observable
+ * @api
+ */
+ol.Map.prototype.setSize = function(size) {
+  this.set(ol.MapProperty.SIZE, size);
+};
+
+
+/**
+ * Set the target element to render this map into.
+ * @param {Element|string|undefined} target The Element or id of the Element
+ *     that the map is rendered in.
+ * @observable
+ * @api stable
+ */
+ol.Map.prototype.setTarget = function(target) {
+  this.set(ol.MapProperty.TARGET, target);
+};
+
+
+/**
+ * Set the view for this map.
+ * @param {ol.View} view The view that controls this map.
+ * @observable
+ * @api stable
+ */
+ol.Map.prototype.setView = function(view) {
+  this.set(ol.MapProperty.VIEW, view);
+};
+
+
+/**
+ * @param {ol.Feature} feature Feature.
+ */
+ol.Map.prototype.skipFeature = function(feature) {
+  var featureUid = goog.getUid(feature).toString();
+  this.skippedFeatureUids_[featureUid] = true;
+  this.render();
+};
+
+
+/**
+ * Force a recalculation of the map viewport size.  This should be called when
+ * third-party code changes the size of the map viewport.
+ * @api stable
+ */
+ol.Map.prototype.updateSize = function() {
+  var targetElement = this.getTargetElement();
+
+  if (!targetElement) {
+    this.setSize(undefined);
+  } else {
+    var computedStyle = ol.global.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 {ol.Feature} feature Feature.
+ */
+ol.Map.prototype.unskipFeature = function(feature) {
+  var featureUid = goog.getUid(feature).toString();
+  delete this.skippedFeatureUids_[featureUid];
+  this.render();
+};
+
+
+/**
+ * @param {olx.MapOptions} options Map options.
+ * @return {ol.MapOptionsInternal} Internal map options.
+ */
+ol.Map.createOptionsInternal = function(options) {
+
+  /**
+   * @type {Element|Document}
+   */
+  var keyboardEventTarget = null;
+  if (options.keyboardEventTarget !== undefined) {
+    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 === 'boolean' && options.logo)) {
+    logos[ol.OL3_LOGO_URL] = ol.OL3_URL;
+  } else {
+    var logo = options.logo;
+    if (typeof logo === 'string') {
+      logos[logo] = '';
+    } else if (logo instanceof HTMLElement) {
+      logos[goog.getUid(logo).toString()] = logo;
+    } else if (goog.isObject(logo)) {
+      goog.asserts.assertString(logo.href, 'logo.href should be a string');
+      goog.asserts.assertString(logo.src, 'logo.src should be a string');
+      logos[logo.src] = logo.href;
+    }
+  }
+
+  var layerGroup = (options.layers instanceof ol.layer.Group) ?
+      options.layers : new ol.layer.Group({layers: options.layers});
+  values[ol.MapProperty.LAYERGROUP] = layerGroup;
+
+  values[ol.MapProperty.TARGET] = options.target;
+
+  values[ol.MapProperty.VIEW] = options.view !== undefined ?
+      options.view : new ol.View();
+
+  /**
+   * @type {function(new: ol.renderer.Map, Element, ol.Map)}
+   */
+  var rendererConstructor = ol.renderer.Map;
+
+  /**
+   * @type {Array.<ol.RendererType>}
+   */
+  var rendererTypes;
+  if (options.renderer !== undefined) {
+    if (Array.isArray(options.renderer)) {
+      rendererTypes = options.renderer;
+    } else if (typeof options.renderer === 'string') {
+      rendererTypes = [options.renderer];
+    } else {
+      goog.asserts.fail('Incorrect format for renderer option');
+    }
+  } else {
+    rendererTypes = ol.DEFAULT_RENDERER_TYPES;
+  }
+
+  var i, ii;
+  for (i = 0, ii = rendererTypes.length; i < ii; ++i) {
+    /** @type {ol.RendererType} */
+    var rendererType = rendererTypes[i];
+    if (ol.ENABLE_CANVAS && rendererType == ol.RendererType.CANVAS) {
+      if (ol.has.CANVAS) {
+        rendererConstructor = ol.renderer.canvas.Map;
+        break;
+      }
+    } else if (ol.ENABLE_DOM && rendererType == ol.RendererType.DOM) {
+      if (ol.has.DOM) {
+        rendererConstructor = ol.renderer.dom.Map;
+        break;
+      }
+    } else if (ol.ENABLE_WEBGL && rendererType == ol.RendererType.WEBGL) {
+      if (ol.has.WEBGL) {
+        rendererConstructor = ol.renderer.webgl.Map;
+        break;
+      }
+    }
+  }
+
+  var controls;
+  if (options.controls !== undefined) {
+    if (Array.isArray(options.controls)) {
+      controls = new ol.Collection(options.controls.slice());
+    } else {
+      goog.asserts.assertInstanceof(options.controls, ol.Collection,
+          'options.controls should be an ol.Collection');
+      controls = options.controls;
+    }
+  } else {
+    controls = ol.control.defaults();
+  }
+
+  var interactions;
+  if (options.interactions !== undefined) {
+    if (Array.isArray(options.interactions)) {
+      interactions = new ol.Collection(options.interactions.slice());
+    } else {
+      goog.asserts.assertInstanceof(options.interactions, ol.Collection,
+          'options.interactions should be an ol.Collection');
+      interactions = options.interactions;
+    }
+  } else {
+    interactions = ol.interaction.defaults();
+  }
+
+  var overlays;
+  if (options.overlays !== undefined) {
+    if (Array.isArray(options.overlays)) {
+      overlays = new ol.Collection(options.overlays.slice());
+    } else {
+      goog.asserts.assertInstanceof(options.overlays, ol.Collection,
+          'options.overlays should be an ol.Collection');
+      overlays = options.overlays;
+    }
+  } else {
+    overlays = new ol.Collection();
+  }
+
+  return {
+    controls: controls,
+    interactions: interactions,
+    keyboardEventTarget: keyboardEventTarget,
+    logos: logos,
+    overlays: overlays,
+    rendererConstructor: rendererConstructor,
+    values: values
+  };
+
+};
+
+
+ol.proj.common.add();
+
+goog.provide('ol.Overlay');
+goog.provide('ol.OverlayPositioning');
+
+goog.require('goog.asserts');
+goog.require('ol.events');
+goog.require('ol.Map');
+goog.require('ol.MapEventType');
+goog.require('ol.Object');
+goog.require('ol.animation');
+goog.require('ol.dom');
+goog.require('ol.extent');
+
+
+/**
+ * @enum {string}
+ */
+ol.OverlayProperty = {
+  ELEMENT: 'element',
+  MAP: 'map',
+  OFFSET: 'offset',
+  POSITION: 'position',
+  POSITIONING: 'positioning'
+};
+
+
+/**
+ * Overlay position: `'bottom-left'`, `'bottom-center'`,  `'bottom-right'`,
+ * `'center-left'`, `'center-center'`, `'center-right'`, `'top-left'`,
+ * `'top-center'`, `'top-right'`
+ * @enum {string}
+ */
+ol.OverlayPositioning = {
+  BOTTOM_LEFT: 'bottom-left',
+  BOTTOM_CENTER: 'bottom-center',
+  BOTTOM_RIGHT: 'bottom-right',
+  CENTER_LEFT: 'center-left',
+  CENTER_CENTER: 'center-center',
+  CENTER_RIGHT: 'center-right',
+  TOP_LEFT: 'top-left',
+  TOP_CENTER: 'top-center',
+  TOP_RIGHT: 'top-right'
+};
+
+
+/**
+ * @classdesc
+ * An element to be displayed over the map and attached to a single map
+ * location.  Like {@link ol.control.Control}, Overlays are visible widgets.
+ * Unlike Controls, they are not in a fixed position on the screen, but are tied
+ * to a geographical coordinate, so panning the map will move an Overlay but not
+ * a Control.
+ *
+ * Example:
+ *
+ *     var popup = new ol.Overlay({
+ *       element: document.getElementById('popup')
+ *     });
+ *     popup.setPosition(coordinate);
+ *     map.addOverlay(popup);
+ *
+ * @constructor
+ * @extends {ol.Object}
+ * @param {olx.OverlayOptions} options Overlay options.
+ * @api stable
+ */
+ol.Overlay = function(options) {
+
+  ol.Object.call(this);
+
+  /**
+   * @private
+   * @type {number|string|undefined}
+   */
+  this.id_ = options.id;
+
+  /**
+   * @private
+   * @type {boolean}
+   */
+  this.insertFirst_ = options.insertFirst !== undefined ?
+      options.insertFirst : true;
+
+  /**
+   * @private
+   * @type {boolean}
+   */
+  this.stopEvent_ = options.stopEvent !== undefined ? options.stopEvent : true;
+
+  /**
+   * @private
+   * @type {Element}
+   */
+  this.element_ = document.createElement('DIV');
+  this.element_.className = 'ol-overlay-container';
+  this.element_.style.position = 'absolute';
+
+  /**
+   * @protected
+   * @type {boolean}
+   */
+  this.autoPan = options.autoPan !== undefined ? options.autoPan : false;
+
+  /**
+   * @private
+   * @type {olx.animation.PanOptions}
+   */
+  this.autoPanAnimation_ = options.autoPanAnimation !== undefined ?
+      options.autoPanAnimation : /** @type {olx.animation.PanOptions} */ ({});
+
+  /**
+   * @private
+   * @type {number}
+   */
+  this.autoPanMargin_ = options.autoPanMargin !== undefined ?
+      options.autoPanMargin : 20;
+
+  /**
+   * @private
+   * @type {{bottom_: string,
+   *         left_: string,
+   *         right_: string,
+   *         top_: string,
+   *         visible: boolean}}
+   */
+  this.rendered_ = {
+    bottom_: '',
+    left_: '',
+    right_: '',
+    top_: '',
+    visible: true
+  };
+
+  /**
+   * @private
+   * @type {?ol.EventsKey}
+   */
+  this.mapPostrenderListenerKey_ = null;
+
+  ol.events.listen(
+      this, ol.Object.getChangeEventType(ol.OverlayProperty.ELEMENT),
+      this.handleElementChanged, this);
+
+  ol.events.listen(
+      this, ol.Object.getChangeEventType(ol.OverlayProperty.MAP),
+      this.handleMapChanged, this);
+
+  ol.events.listen(
+      this, ol.Object.getChangeEventType(ol.OverlayProperty.OFFSET),
+      this.handleOffsetChanged, this);
+
+  ol.events.listen(
+      this, ol.Object.getChangeEventType(ol.OverlayProperty.POSITION),
+      this.handlePositionChanged, this);
+
+  ol.events.listen(
+      this, ol.Object.getChangeEventType(ol.OverlayProperty.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 {ol.OverlayPositioning} */ (options.positioning) :
+      ol.OverlayPositioning.TOP_LEFT);
+
+  if (options.position !== undefined) {
+    this.setPosition(options.position);
+  }
+
+};
+ol.inherits(ol.Overlay, ol.Object);
+
+
+/**
+ * Get the DOM element of this overlay.
+ * @return {Element|undefined} The Element containing the overlay.
+ * @observable
+ * @api stable
+ */
+ol.Overlay.prototype.getElement = function() {
+  return /** @type {Element|undefined} */ (
+      this.get(ol.OverlayProperty.ELEMENT));
+};
+
+
+/**
+ * Get the overlay identifier which is set on constructor.
+ * @return {number|string|undefined} Id.
+ * @api
+ */
+ol.Overlay.prototype.getId = function() {
+  return this.id_;
+};
+
+
+/**
+ * Get the map associated with this overlay.
+ * @return {ol.Map|undefined} The map that the overlay is part of.
+ * @observable
+ * @api stable
+ */
+ol.Overlay.prototype.getMap = function() {
+  return /** @type {ol.Map|undefined} */ (
+      this.get(ol.OverlayProperty.MAP));
+};
+
+
+/**
+ * Get the offset of this overlay.
+ * @return {Array.<number>} The offset.
+ * @observable
+ * @api stable
+ */
+ol.Overlay.prototype.getOffset = function() {
+  return /** @type {Array.<number>} */ (
+      this.get(ol.OverlayProperty.OFFSET));
+};
+
+
+/**
+ * Get the current position of this overlay.
+ * @return {ol.Coordinate|undefined} The spatial point that the overlay is
+ *     anchored at.
+ * @observable
+ * @api stable
+ */
+ol.Overlay.prototype.getPosition = function() {
+  return /** @type {ol.Coordinate|undefined} */ (
+      this.get(ol.OverlayProperty.POSITION));
+};
+
+
+/**
+ * Get the current positioning of this overlay.
+ * @return {ol.OverlayPositioning} How the overlay is positioned
+ *     relative to its point on the map.
+ * @observable
+ * @api stable
+ */
+ol.Overlay.prototype.getPositioning = function() {
+  return /** @type {ol.OverlayPositioning} */ (
+      this.get(ol.OverlayProperty.POSITIONING));
+};
+
+
+/**
+ * @protected
+ */
+ol.Overlay.prototype.handleElementChanged = function() {
+  ol.dom.removeChildren(this.element_);
+  var element = this.getElement();
+  if (element) {
+    this.element_.appendChild(element);
+  }
+};
+
+
+/**
+ * @protected
+ */
+ol.Overlay.prototype.handleMapChanged = function() {
+  if (this.mapPostrenderListenerKey_) {
+    ol.dom.removeNode(this.element_);
+    ol.events.unlistenByKey(this.mapPostrenderListenerKey_);
+    this.mapPostrenderListenerKey_ = null;
+  }
+  var map = this.getMap();
+  if (map) {
+    this.mapPostrenderListenerKey_ = ol.events.listen(map,
+        ol.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
+ */
+ol.Overlay.prototype.render = function() {
+  this.updatePixelPosition();
+};
+
+
+/**
+ * @protected
+ */
+ol.Overlay.prototype.handleOffsetChanged = function() {
+  this.updatePixelPosition();
+};
+
+
+/**
+ * @protected
+ */
+ol.Overlay.prototype.handlePositionChanged = function() {
+  this.updatePixelPosition();
+  if (this.get(ol.OverlayProperty.POSITION) !== undefined && this.autoPan) {
+    this.panIntoView_();
+  }
+};
+
+
+/**
+ * @protected
+ */
+ol.Overlay.prototype.handlePositioningChanged = function() {
+  this.updatePixelPosition();
+};
+
+
+/**
+ * Set the DOM element to be associated with this overlay.
+ * @param {Element|undefined} element The Element containing the overlay.
+ * @observable
+ * @api stable
+ */
+ol.Overlay.prototype.setElement = function(element) {
+  this.set(ol.OverlayProperty.ELEMENT, element);
+};
+
+
+/**
+ * Set the map to be associated with this overlay.
+ * @param {ol.Map|undefined} map The map that the overlay is part of.
+ * @observable
+ * @api stable
+ */
+ol.Overlay.prototype.setMap = function(map) {
+  this.set(ol.OverlayProperty.MAP, map);
+};
+
+
+/**
+ * Set the offset for this overlay.
+ * @param {Array.<number>} offset Offset.
+ * @observable
+ * @api stable
+ */
+ol.Overlay.prototype.setOffset = function(offset) {
+  this.set(ol.OverlayProperty.OFFSET, offset);
+};
+
+
+/**
+ * Set the position for this overlay. If the position is `undefined` the
+ * overlay is hidden.
+ * @param {ol.Coordinate|undefined} position The spatial point that the overlay
+ *     is anchored at.
+ * @observable
+ * @api stable
+ */
+ol.Overlay.prototype.setPosition = function(position) {
+  this.set(ol.OverlayProperty.POSITION, position);
+};
+
+
+/**
+ * Pan the map so that the overlay is entirely visible in the current viewport
+ * (if necessary).
+ * @private
+ */
+ol.Overlay.prototype.panIntoView_ = function() {
+  goog.asserts.assert(this.autoPan, 'this.autoPan should be true');
+  var map = this.getMap();
+
+  if (map === undefined || !map.getTargetElement()) {
+    return;
+  }
+
+  var mapRect = this.getRect_(map.getTargetElement(), map.getSize());
+  var element = this.getElement();
+  goog.asserts.assert(element, 'element should be defined');
+  var overlayRect = this.getRect_(element,
+      [ol.dom.outerWidth(element), ol.dom.outerHeight(element)]);
+
+  var margin = this.autoPanMargin_;
+  if (!ol.extent.containsExtent(mapRect, overlayRect)) {
+    // the overlay is not completely inside the viewport, so pan the map
+    var offsetLeft = overlayRect[0] - mapRect[0];
+    var offsetRight = mapRect[2] - overlayRect[2];
+    var offsetTop = overlayRect[1] - mapRect[1];
+    var offsetBottom = mapRect[3] - overlayRect[3];
+
+    var delta = [0, 0];
+    if (offsetLeft < 0) {
+      // move map to the left
+      delta[0] = offsetLeft - margin;
+    } else if (offsetRight < 0) {
+      // move map to the right
+      delta[0] = Math.abs(offsetRight) + margin;
+    }
+    if (offsetTop < 0) {
+      // move map up
+      delta[1] = offsetTop - margin;
+    } else if (offsetBottom < 0) {
+      // move map down
+      delta[1] = Math.abs(offsetBottom) + margin;
+    }
+
+    if (delta[0] !== 0 || delta[1] !== 0) {
+      var center = map.getView().getCenter();
+      goog.asserts.assert(center !== undefined, 'center should be defined');
+      var centerPx = map.getPixelFromCoordinate(center);
+      var newCenterPx = [
+        centerPx[0] + delta[0],
+        centerPx[1] + delta[1]
+      ];
+
+      if (this.autoPanAnimation_) {
+        this.autoPanAnimation_.source = center;
+        map.beforeRender(ol.animation.pan(this.autoPanAnimation_));
+      }
+      map.getView().setCenter(map.getCoordinateFromPixel(newCenterPx));
+    }
+  }
+};
+
+
+/**
+ * Get the extent of an element relative to the document
+ * @param {Element|undefined} element The element.
+ * @param {ol.Size|undefined} size The size of the element.
+ * @return {ol.Extent} The extent.
+ * @private
+ */
+ol.Overlay.prototype.getRect_ = function(element, size) {
+  goog.asserts.assert(element, 'element should be defined');
+  goog.asserts.assert(size !== undefined, 'size should be defined');
+
+  var box = element.getBoundingClientRect();
+  var offsetX = box.left + ol.global.pageXOffset;
+  var offsetY = box.top + ol.global.pageYOffset;
+  return [
+    offsetX,
+    offsetY,
+    offsetX + size[0],
+    offsetY + size[1]
+  ];
+};
+
+
+/**
+ * Set the positioning for this overlay.
+ * @param {ol.OverlayPositioning} positioning how the overlay is
+ *     positioned relative to its point on the map.
+ * @observable
+ * @api stable
+ */
+ol.Overlay.prototype.setPositioning = function(positioning) {
+  this.set(ol.OverlayProperty.POSITIONING, positioning);
+};
+
+
+/**
+ * Modify the visibility of the element.
+ * @param {boolean} visible Element visibility.
+ * @protected
+ */
+ol.Overlay.prototype.setVisible = function(visible) {
+  if (this.rendered_.visible !== visible) {
+    this.element_.style.display = visible ? '' : 'none';
+    this.rendered_.visible = visible;
+  }
+};
+
+
+/**
+ * Update pixel position.
+ * @protected
+ */
+ol.Overlay.prototype.updatePixelPosition = function() {
+  var map = this.getMap();
+  var position = this.getPosition();
+  if (map === undefined || !map.isRendered() || position === undefined) {
+    this.setVisible(false);
+    return;
+  }
+
+  var pixel = map.getPixelFromCoordinate(position);
+  var mapSize = map.getSize();
+  this.updateRenderedPosition(pixel, mapSize);
+};
+
+
+/**
+ * @param {ol.Pixel} pixel The pixel location.
+ * @param {ol.Size|undefined} mapSize The map size.
+ * @protected
+ */
+ol.Overlay.prototype.updateRenderedPosition = function(pixel, mapSize) {
+  goog.asserts.assert(pixel, 'pixel should not be null');
+  goog.asserts.assert(mapSize !== undefined, 'mapSize should be defined');
+  var style = this.element_.style;
+  var offset = this.getOffset();
+  goog.asserts.assert(Array.isArray(offset), 'offset should be an array');
+
+  var positioning = this.getPositioning();
+  goog.asserts.assert(positioning !== undefined,
+      'positioning should be defined');
+
+  var offsetX = offset[0];
+  var offsetY = offset[1];
+  if (positioning == ol.OverlayPositioning.BOTTOM_RIGHT ||
+      positioning == ol.OverlayPositioning.CENTER_RIGHT ||
+      positioning == ol.OverlayPositioning.TOP_RIGHT) {
+    if (this.rendered_.left_ !== '') {
+      this.rendered_.left_ = style.left = '';
+    }
+    var right = Math.round(mapSize[0] - pixel[0] - offsetX) + 'px';
+    if (this.rendered_.right_ != right) {
+      this.rendered_.right_ = style.right = right;
+    }
+  } else {
+    if (this.rendered_.right_ !== '') {
+      this.rendered_.right_ = style.right = '';
+    }
+    if (positioning == ol.OverlayPositioning.BOTTOM_CENTER ||
+        positioning == ol.OverlayPositioning.CENTER_CENTER ||
+        positioning == ol.OverlayPositioning.TOP_CENTER) {
+      offsetX -= 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 == ol.OverlayPositioning.BOTTOM_LEFT ||
+      positioning == ol.OverlayPositioning.BOTTOM_CENTER ||
+      positioning == ol.OverlayPositioning.BOTTOM_RIGHT) {
+    if (this.rendered_.top_ !== '') {
+      this.rendered_.top_ = style.top = '';
+    }
+    var bottom = Math.round(mapSize[1] - pixel[1] - offsetY) + 'px';
+    if (this.rendered_.bottom_ != bottom) {
+      this.rendered_.bottom_ = style.bottom = bottom;
+    }
+  } else {
+    if (this.rendered_.bottom_ !== '') {
+      this.rendered_.bottom_ = style.bottom = '';
+    }
+    if (positioning == ol.OverlayPositioning.CENTER_LEFT ||
+        positioning == ol.OverlayPositioning.CENTER_CENTER ||
+        positioning == ol.OverlayPositioning.CENTER_RIGHT) {
+      offsetY -= this.element_.offsetHeight / 2;
+    }
+    var top = Math.round(pixel[1] + offsetY) + 'px';
+    if (this.rendered_.top_ != top) {
+      this.rendered_.top_ = style.top = top;
+    }
+  }
+
+  this.setVisible(true);
+};
+
+goog.provide('ol.control.OverviewMap');
+
+goog.require('goog.asserts');
+goog.require('ol.events');
+goog.require('ol.events.EventType');
+goog.require('ol');
+goog.require('ol.Collection');
+goog.require('ol.Map');
+goog.require('ol.MapEventType');
+goog.require('ol.Object');
+goog.require('ol.ObjectEventType');
+goog.require('ol.Overlay');
+goog.require('ol.OverlayPositioning');
+goog.require('ol.View');
+goog.require('ol.ViewProperty');
+goog.require('ol.control.Control');
+goog.require('ol.coordinate');
+goog.require('ol.css');
+goog.require('ol.dom');
+goog.require('ol.extent');
+
+
+/**
+ * Create a new control with a map acting as an overview map for an other
+ * defined map.
+ * @constructor
+ * @extends {ol.control.Control}
+ * @param {olx.control.OverviewMapOptions=} opt_options OverviewMap options.
+ * @api
+ */
+ol.control.OverviewMap = function(opt_options) {
+
+  var options = opt_options ? opt_options : {};
+
+  /**
+   * @type {boolean}
+   * @private
+   */
+  this.collapsed_ = options.collapsed !== undefined ? options.collapsed : true;
+
+  /**
+   * @private
+   * @type {boolean}
+   */
+  this.collapsible_ = options.collapsible !== undefined ?
+      options.collapsible : true;
+
+  if (!this.collapsible_) {
+    this.collapsed_ = false;
+  }
+
+  var className = options.className !== undefined ? options.className : 'ol-overviewmap';
+
+  var tipLabel = options.tipLabel !== undefined ? options.tipLabel : 'Overview map';
+
+  var collapseLabel = options.collapseLabel !== undefined ? options.collapseLabel : '\u00AB';
+
+  if (typeof collapseLabel === 'string') {
+    /**
+     * @private
+     * @type {Node}
+     */
+    this.collapseLabel_ = document.createElement('span');
+    this.collapseLabel_.textContent = collapseLabel;
+  } else {
+    this.collapseLabel_ = collapseLabel;
+  }
+
+  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.collapsible_ && !this.collapsed_) ?
+      this.collapseLabel_ : this.label_;
+  var button = document.createElement('button');
+  button.setAttribute('type', 'button');
+  button.title = tipLabel;
+  button.appendChild(activeLabel);
+
+  ol.events.listen(button, ol.events.EventType.CLICK,
+      this.handleClick_, this);
+
+  var ovmapDiv = document.createElement('DIV');
+  ovmapDiv.className = 'ol-overviewmap-map';
+
+  /**
+   * @type {ol.Map}
+   * @private
+   */
+  this.ovmap_ = new ol.Map({
+    controls: new ol.Collection(),
+    interactions: new ol.Collection(),
+    target: ovmapDiv,
+    view: options.view
+  });
+  var ovmap = this.ovmap_;
+
+  if (options.layers) {
+    options.layers.forEach(
+        /**
+       * @param {ol.layer.Layer} layer Layer.
+       */
+        function(layer) {
+          ovmap.addLayer(layer);
+        }, this);
+  }
+
+  var box = document.createElement('DIV');
+  box.className = 'ol-overviewmap-box';
+  box.style.boxSizing = 'border-box';
+
+  /**
+   * @type {ol.Overlay}
+   * @private
+   */
+  this.boxOverlay_ = new ol.Overlay({
+    position: [0, 0],
+    positioning: ol.OverlayPositioning.BOTTOM_LEFT,
+    element: box
+  });
+  this.ovmap_.addOverlay(this.boxOverlay_);
+
+  var cssClasses = className + ' ' + ol.css.CLASS_UNSELECTABLE + ' ' +
+      ol.css.CLASS_CONTROL +
+      (this.collapsed_ && this.collapsible_ ? ' ol-collapsed' : '') +
+      (this.collapsible_ ? '' : ' ol-uncollapsible');
+  var element = document.createElement('div');
+  element.className = cssClasses;
+  element.appendChild(ovmapDiv);
+  element.appendChild(button);
+
+  var render = options.render ? options.render : ol.control.OverviewMap.render;
+
+  ol.control.Control.call(this, {
+    element: element,
+    render: render,
+    target: options.target
+  });
+};
+ol.inherits(ol.control.OverviewMap, ol.control.Control);
+
+
+/**
+ * @inheritDoc
+ * @api
+ */
+ol.control.OverviewMap.prototype.setMap = function(map) {
+  var oldMap = this.getMap();
+  if (map === oldMap) {
+    return;
+  }
+  if (oldMap) {
+    var oldView = oldMap.getView();
+    if (oldView) {
+      this.unbindView_(oldView);
+    }
+  }
+  ol.control.Control.prototype.setMap.call(this, map);
+
+  if (map) {
+    this.listenerKeys.push(ol.events.listen(
+        map, ol.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 {ol.ObjectEvent} event The propertychange event.
+ * @private
+ */
+ol.control.OverviewMap.prototype.handleMapPropertyChange_ = function(event) {
+  if (event.key === ol.MapProperty.VIEW) {
+    var oldView = /** @type {ol.View} */ (event.oldValue);
+    if (oldView) {
+      this.unbindView_(oldView);
+    }
+    var newView = this.getMap().getView();
+    this.bindView_(newView);
+  }
+};
+
+
+/**
+ * Register listeners for view property changes.
+ * @param {ol.View} view The view.
+ * @private
+ */
+ol.control.OverviewMap.prototype.bindView_ = function(view) {
+  ol.events.listen(view,
+      ol.Object.getChangeEventType(ol.ViewProperty.ROTATION),
+      this.handleRotationChanged_, this);
+};
+
+
+/**
+ * Unregister listeners for view property changes.
+ * @param {ol.View} view The view.
+ * @private
+ */
+ol.control.OverviewMap.prototype.unbindView_ = function(view) {
+  ol.events.unlisten(view,
+      ol.Object.getChangeEventType(ol.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
+ */
+ol.control.OverviewMap.prototype.handleRotationChanged_ = function() {
+  this.ovmap_.getView().setRotation(this.getMap().getView().getRotation());
+};
+
+
+/**
+ * Update the overview map element.
+ * @param {ol.MapEvent} mapEvent Map event.
+ * @this {ol.control.OverviewMap}
+ * @api
+ */
+ol.control.OverviewMap.render = function(mapEvent) {
+  this.validateExtent_();
+  this.updateBox_();
+};
+
+
+/**
+ * Reset the overview map extent if the box size (width or
+ * height) is less than the size of the overview map size times minRatio
+ * or is greater than the size of the overview size times maxRatio.
+ *
+ * If the map extent was not reset, the box size can fits in the defined
+ * ratio sizes. This method then checks if is contained inside the overview
+ * map current extent. If not, recenter the overview map to the current
+ * main map center location.
+ * @private
+ */
+ol.control.OverviewMap.prototype.validateExtent_ = function() {
+  var map = this.getMap();
+  var ovmap = this.ovmap_;
+
+  if (!map.isRendered() || !ovmap.isRendered()) {
+    return;
+  }
+
+  var mapSize = map.getSize();
+  goog.asserts.assertArray(mapSize, 'mapSize should be an array');
+
+  var view = map.getView();
+  goog.asserts.assert(view, 'view should be defined');
+  var extent = view.calculateExtent(mapSize);
+
+  var ovmapSize = ovmap.getSize();
+  goog.asserts.assertArray(ovmapSize, 'ovmapSize should be an array');
+
+  var ovview = ovmap.getView();
+  goog.asserts.assert(ovview, 'ovview should be defined');
+  var ovextent = ovview.calculateExtent(ovmapSize);
+
+  var topLeftPixel =
+      ovmap.getPixelFromCoordinate(ol.extent.getTopLeft(extent));
+  var bottomRightPixel =
+      ovmap.getPixelFromCoordinate(ol.extent.getBottomRight(extent));
+
+  var 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 * ol.OVERVIEWMAP_MIN_RATIO ||
+      boxHeight < ovmapHeight * ol.OVERVIEWMAP_MIN_RATIO ||
+      boxWidth > ovmapWidth * ol.OVERVIEWMAP_MAX_RATIO ||
+      boxHeight > ovmapHeight * ol.OVERVIEWMAP_MAX_RATIO) {
+    this.resetExtent_();
+  } else if (!ol.extent.containsExtent(ovextent, extent)) {
+    this.recenter_();
+  }
+};
+
+
+/**
+ * Reset the overview map extent to half calculated min and max ratio times
+ * the extent of the main map.
+ * @private
+ */
+ol.control.OverviewMap.prototype.resetExtent_ = function() {
+  if (ol.OVERVIEWMAP_MAX_RATIO === 0 || ol.OVERVIEWMAP_MIN_RATIO === 0) {
+    return;
+  }
+
+  var map = this.getMap();
+  var ovmap = this.ovmap_;
+
+  var mapSize = map.getSize();
+  goog.asserts.assertArray(mapSize, 'mapSize should be an array');
+
+  var view = map.getView();
+  goog.asserts.assert(view, 'view should be defined');
+  var extent = view.calculateExtent(mapSize);
+
+  var ovmapSize = ovmap.getSize();
+  goog.asserts.assertArray(ovmapSize, 'ovmapSize should be an array');
+
+  var ovview = ovmap.getView();
+  goog.asserts.assert(ovview, 'ovview should be defined');
+
+  // get how many times the current map overview could hold different
+  // box sizes using the min and max ratio, pick the step in the middle used
+  // to calculate the extent from the main map to set it to the overview map,
+  var steps = Math.log(
+      ol.OVERVIEWMAP_MAX_RATIO / ol.OVERVIEWMAP_MIN_RATIO) / Math.LN2;
+  var ratio = 1 / (Math.pow(2, steps / 2) * ol.OVERVIEWMAP_MIN_RATIO);
+  ol.extent.scaleFromCenter(extent, ratio);
+  ovview.fit(extent, ovmapSize);
+};
+
+
+/**
+ * Set the center of the overview map to the map center without changing its
+ * resolution.
+ * @private
+ */
+ol.control.OverviewMap.prototype.recenter_ = function() {
+  var map = this.getMap();
+  var ovmap = this.ovmap_;
+
+  var view = map.getView();
+  goog.asserts.assert(view, 'view should be defined');
+
+  var ovview = ovmap.getView();
+  goog.asserts.assert(ovview, 'ovview should be defined');
+
+  ovview.setCenter(view.getCenter());
+};
+
+
+/**
+ * Update the box using the main map extent
+ * @private
+ */
+ol.control.OverviewMap.prototype.updateBox_ = function() {
+  var map = this.getMap();
+  var ovmap = this.ovmap_;
+
+  if (!map.isRendered() || !ovmap.isRendered()) {
+    return;
+  }
+
+  var mapSize = map.getSize();
+  goog.asserts.assertArray(mapSize, 'mapSize should be an array');
+
+  var view = map.getView();
+  goog.asserts.assert(view, 'view should be defined');
+
+  var ovview = ovmap.getView();
+  goog.asserts.assert(ovview, 'ovview should be defined');
+
+  var ovmapSize = ovmap.getSize();
+  goog.asserts.assertArray(ovmapSize, 'ovmapSize should be an array');
+
+  var rotation = view.getRotation();
+  goog.asserts.assert(rotation !== undefined, 'rotation should be defined');
+
+  var overlay = this.boxOverlay_;
+  var box = this.boxOverlay_.getElement();
+  var extent = view.calculateExtent(mapSize);
+  var ovresolution = ovview.getResolution();
+  var bottomLeft = ol.extent.getBottomLeft(extent);
+  var topRight = ol.extent.getTopRight(extent);
+
+  // set position using bottom left coordinates
+  var rotateBottomLeft = this.calculateCoordinateRotate_(rotation, bottomLeft);
+  overlay.setPosition(rotateBottomLeft);
+
+  // set box size calculated from map extent size and overview map resolution
+  if (box) {
+    box.style.width = Math.abs((bottomLeft[0] - topRight[0]) / ovresolution) + 'px';
+    box.style.height = Math.abs((topRight[1] - bottomLeft[1]) / ovresolution) + 'px';
+  }
+};
+
+
+/**
+ * @param {number} rotation Target rotation.
+ * @param {ol.Coordinate} coordinate Coordinate.
+ * @return {ol.Coordinate|undefined} Coordinate for rotation and center anchor.
+ * @private
+ */
+ol.control.OverviewMap.prototype.calculateCoordinateRotate_ = function(
+    rotation, coordinate) {
+  var coordinateRotate;
+
+  var map = this.getMap();
+  var view = map.getView();
+  goog.asserts.assert(view, 'view should be defined');
+
+  var currentCenter = view.getCenter();
+
+  if (currentCenter) {
+    coordinateRotate = [
+      coordinate[0] - currentCenter[0],
+      coordinate[1] - currentCenter[1]
+    ];
+    ol.coordinate.rotate(coordinateRotate, rotation);
+    ol.coordinate.add(coordinateRotate, currentCenter);
+  }
+  return coordinateRotate;
+};
+
+
+/**
+ * @param {Event} event The event to handle
+ * @private
+ */
+ol.control.OverviewMap.prototype.handleClick_ = function(event) {
+  event.preventDefault();
+  this.handleToggle_();
+};
+
+
+/**
+ * @private
+ */
+ol.control.OverviewMap.prototype.handleToggle_ = function() {
+  this.element.classList.toggle('ol-collapsed');
+  if (this.collapsed_) {
+    ol.dom.replaceNode(this.collapseLabel_, this.label_);
+  } else {
+    ol.dom.replaceNode(this.label_, this.collapseLabel_);
+  }
+  this.collapsed_ = !this.collapsed_;
+
+  // manage overview map if it had not been rendered before and control
+  // is expanded
+  var ovmap = this.ovmap_;
+  if (!this.collapsed_ && !ovmap.isRendered()) {
+    ovmap.updateSize();
+    this.resetExtent_();
+    ol.events.listenOnce(ovmap, ol.MapEventType.POSTRENDER,
+        function(event) {
+          this.updateBox_();
+        },
+        this);
+  }
+};
+
+
+/**
+ * Return `true` if the overview map is collapsible, `false` otherwise.
+ * @return {boolean} True if the widget is collapsible.
+ * @api stable
+ */
+ol.control.OverviewMap.prototype.getCollapsible = function() {
+  return this.collapsible_;
+};
+
+
+/**
+ * Set whether the overview map should be collapsible.
+ * @param {boolean} collapsible True if the widget is collapsible.
+ * @api stable
+ */
+ol.control.OverviewMap.prototype.setCollapsible = function(collapsible) {
+  if (this.collapsible_ === collapsible) {
+    return;
+  }
+  this.collapsible_ = collapsible;
+  this.element.classList.toggle('ol-uncollapsible');
+  if (!collapsible && this.collapsed_) {
+    this.handleToggle_();
+  }
+};
+
+
+/**
+ * Collapse or expand the overview map according to the passed parameter. Will
+ * not do anything if the overview map isn't collapsible or if the current
+ * collapsed state is already the one requested.
+ * @param {boolean} collapsed True if the widget is collapsed.
+ * @api stable
+ */
+ol.control.OverviewMap.prototype.setCollapsed = function(collapsed) {
+  if (!this.collapsible_ || this.collapsed_ === collapsed) {
+    return;
+  }
+  this.handleToggle_();
+};
+
+
+/**
+ * Determine if the overview map is collapsed.
+ * @return {boolean} The overview map is collapsed.
+ * @api stable
+ */
+ol.control.OverviewMap.prototype.getCollapsed = function() {
+  return this.collapsed_;
+};
+
+
+/**
+ * Return the overview map.
+ * @return {ol.Map} Overview map.
+ * @api
+ */
+ol.control.OverviewMap.prototype.getOverviewMap = function() {
+  return this.ovmap_;
+};
+
+goog.provide('ol.control.ScaleLine');
+
+goog.require('goog.asserts');
+goog.require('ol.events');
+goog.require('ol');
+goog.require('ol.Object');
+goog.require('ol.control.Control');
+goog.require('ol.css');
+goog.require('ol.proj.METERS_PER_UNIT');
+goog.require('ol.proj.Units');
+
+
+/**
+ * @enum {string}
+ */
+ol.control.ScaleLineProperty = {
+  UNITS: 'units'
+};
+
+
+/**
+ * Units for the scale line. Supported values are `'degrees'`, `'imperial'`,
+ * `'nautical'`, `'metric'`, `'us'`.
+ * @enum {string}
+ */
+ol.control.ScaleLineUnits = {
+  DEGREES: 'degrees',
+  IMPERIAL: 'imperial',
+  NAUTICAL: 'nautical',
+  METRIC: 'metric',
+  US: 'us'
+};
+
+
+/**
+ * @classdesc
+ * A control displaying rough x-axis distances, calculated for the center of the
+ * viewport.
+ * No scale line will be shown when the x-axis distance cannot be calculated in
+ * the view projection (e.g. at or beyond the poles in EPSG:4326).
+ * By default the scale line will show in the bottom left portion of the map,
+ * but this can be changed by using the css selector `.ol-scale-line`.
+ *
+ * @constructor
+ * @extends {ol.control.Control}
+ * @param {olx.control.ScaleLineOptions=} opt_options Scale line options.
+ * @api stable
+ */
+ol.control.ScaleLine = function(opt_options) {
+
+  var options = opt_options ? opt_options : {};
+
+  var className = options.className !== undefined ? options.className : 'ol-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 + ' ' + ol.css.CLASS_UNSELECTABLE;
+  this.element_.appendChild(this.innerElement_);
+
+  /**
+   * @private
+   * @type {?olx.ViewState}
+   */
+  this.viewState_ = null;
+
+  /**
+   * @private
+   * @type {number}
+   */
+  this.minWidth_ = options.minWidth !== undefined ? options.minWidth : 64;
+
+  /**
+   * @private
+   * @type {boolean}
+   */
+  this.renderedVisible_ = false;
+
+  /**
+   * @private
+   * @type {number|undefined}
+   */
+  this.renderedWidth_ = undefined;
+
+  /**
+   * @private
+   * @type {string}
+   */
+  this.renderedHTML_ = '';
+
+  var render = options.render ? options.render : ol.control.ScaleLine.render;
+
+  ol.control.Control.call(this, {
+    element: this.element_,
+    render: render,
+    target: options.target
+  });
+
+  ol.events.listen(
+      this, ol.Object.getChangeEventType(ol.control.ScaleLineProperty.UNITS),
+      this.handleUnitsChanged_, this);
+
+  this.setUnits(/** @type {ol.control.ScaleLineUnits} */ (options.units) ||
+      ol.control.ScaleLineUnits.METRIC);
+
+};
+ol.inherits(ol.control.ScaleLine, ol.control.Control);
+
+
+/**
+ * @const
+ * @type {Array.<number>}
+ */
+ol.control.ScaleLine.LEADING_DIGITS = [1, 2, 5];
+
+
+/**
+ * Return the units to use in the scale line.
+ * @return {ol.control.ScaleLineUnits|undefined} The units to use in the scale
+ *     line.
+ * @observable
+ * @api stable
+ */
+ol.control.ScaleLine.prototype.getUnits = function() {
+  return /** @type {ol.control.ScaleLineUnits|undefined} */ (
+      this.get(ol.control.ScaleLineProperty.UNITS));
+};
+
+
+/**
+ * Update the scale line element.
+ * @param {ol.MapEvent} mapEvent Map event.
+ * @this {ol.control.ScaleLine}
+ * @api
+ */
+ol.control.ScaleLine.render = function(mapEvent) {
+  var frameState = mapEvent.frameState;
+  if (!frameState) {
+    this.viewState_ = null;
+  } else {
+    this.viewState_ = frameState.viewState;
+  }
+  this.updateElement_();
+};
+
+
+/**
+ * @private
+ */
+ol.control.ScaleLine.prototype.handleUnitsChanged_ = function() {
+  this.updateElement_();
+};
+
+
+/**
+ * Set the units to use in the scale line.
+ * @param {ol.control.ScaleLineUnits} units The units to use in the scale line.
+ * @observable
+ * @api stable
+ */
+ol.control.ScaleLine.prototype.setUnits = function(units) {
+  this.set(ol.control.ScaleLineProperty.UNITS, units);
+};
+
+
+/**
+ * @private
+ */
+ol.control.ScaleLine.prototype.updateElement_ = function() {
+  var viewState = this.viewState_;
+
+  if (!viewState) {
+    if (this.renderedVisible_) {
+      this.element_.style.display = 'none';
+      this.renderedVisible_ = false;
+    }
+    return;
+  }
+
+  var center = viewState.center;
+  var projection = viewState.projection;
+  var metersPerUnit = projection.getMetersPerUnit();
+  var pointResolution =
+      projection.getPointResolution(viewState.resolution, center) *
+      metersPerUnit;
+
+  var nominalCount = this.minWidth_ * pointResolution;
+  var suffix = '';
+  var units = this.getUnits();
+  if (units == ol.control.ScaleLineUnits.DEGREES) {
+    var metersPerDegree = ol.proj.METERS_PER_UNIT[ol.proj.Units.DEGREES];
+    pointResolution /= metersPerDegree;
+    if (nominalCount < metersPerDegree / 60) {
+      suffix = '\u2033'; // seconds
+      pointResolution *= 3600;
+    } else if (nominalCount < metersPerDegree) {
+      suffix = '\u2032'; // minutes
+      pointResolution *= 60;
+    } else {
+      suffix = '\u00b0'; // degrees
+    }
+  } else if (units == ol.control.ScaleLineUnits.IMPERIAL) {
+    if (nominalCount < 0.9144) {
+      suffix = 'in';
+      pointResolution /= 0.0254;
+    } else if (nominalCount < 1609.344) {
+      suffix = 'ft';
+      pointResolution /= 0.3048;
+    } else {
+      suffix = 'mi';
+      pointResolution /= 1609.344;
+    }
+  } else if (units == ol.control.ScaleLineUnits.NAUTICAL) {
+    pointResolution /= 1852;
+    suffix = 'nm';
+  } else if (units == ol.control.ScaleLineUnits.METRIC) {
+    if (nominalCount < 1) {
+      suffix = 'mm';
+      pointResolution *= 1000;
+    } else if (nominalCount < 1000) {
+      suffix = 'm';
+    } else {
+      suffix = 'km';
+      pointResolution /= 1000;
+    }
+  } else if (units == ol.control.ScaleLineUnits.US) {
+    if (nominalCount < 0.9144) {
+      suffix = 'in';
+      pointResolution *= 39.37;
+    } else if (nominalCount < 1609.344) {
+      suffix = 'ft';
+      pointResolution /= 0.30480061;
+    } else {
+      suffix = 'mi';
+      pointResolution /= 1609.3472;
+    }
+  } else {
+    goog.asserts.fail('Scale line element cannot be updated');
+  }
+
+  var i = 3 * Math.floor(
+      Math.log(this.minWidth_ * pointResolution) / Math.log(10));
+  var count, width;
+  while (true) {
+    count = ol.control.ScaleLine.LEADING_DIGITS[((i % 3) + 3) % 3] *
+        Math.pow(10, Math.floor(i / 3));
+    width = Math.round(count / pointResolution);
+    if (isNaN(width)) {
+      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;
+  }
+
+};
+
+// FIXME should possibly show tooltip when dragging?
+
+goog.provide('ol.control.ZoomSlider');
+
+goog.require('goog.asserts');
+goog.require('ol.events');
+goog.require('ol.events.Event');
+goog.require('ol.events.EventType');
+goog.require('ol.pointer.PointerEventHandler');
+goog.require('ol.ViewHint');
+goog.require('ol.animation');
+goog.require('ol.control.Control');
+goog.require('ol.css');
+goog.require('ol.easing');
+goog.require('ol.math');
+
+
+/**
+ * @classdesc
+ * A slider type of control for zooming.
+ *
+ * Example:
+ *
+ *     map.addControl(new ol.control.ZoomSlider());
+ *
+ * @constructor
+ * @extends {ol.control.Control}
+ * @param {olx.control.ZoomSliderOptions=} opt_options Zoom slider options.
+ * @api stable
+ */
+ol.control.ZoomSlider = function(opt_options) {
+
+  var options = opt_options ? opt_options : {};
+
+  /**
+   * Will hold the current resolution of the view.
+   *
+   * @type {number|undefined}
+   * @private
+   */
+  this.currentResolution_ = undefined;
+
+  /**
+   * The direction of the slider. Will be determined from actual display of the
+   * container and defaults to ol.control.ZoomSlider.direction.VERTICAL.
+   *
+   * @type {ol.control.ZoomSlider.direction}
+   * @private
+   */
+  this.direction_ = ol.control.ZoomSlider.direction.VERTICAL;
+
+  /**
+   * @type {boolean}
+   * @private
+   */
+  this.dragging_;
+
+  /**
+   * @type {!Array.<ol.EventsKey>}
+   * @private
+   */
+  this.dragListenerKeys_ = [];
+
+  /**
+   * @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 {ol.Size}
+   * @private
+   */
+  this.thumbSize_ = null;
+
+  /**
+   * Whether the slider is initialized.
+   * @type {boolean}
+   * @private
+   */
+  this.sliderInitialized_ = false;
+
+  /**
+   * @type {number}
+   * @private
+   */
+  this.duration_ = options.duration !== undefined ? options.duration : 200;
+
+  var className = options.className !== undefined ? options.className : 'ol-zoomslider';
+  var thumbElement = document.createElement('button');
+  thumbElement.setAttribute('type', 'button');
+  thumbElement.className = className + '-thumb ' + ol.css.CLASS_UNSELECTABLE;
+  var containerElement = document.createElement('div');
+  containerElement.className = className + ' ' + ol.css.CLASS_UNSELECTABLE + ' ' + ol.css.CLASS_CONTROL;
+  containerElement.appendChild(thumbElement);
+  /**
+   * @type {ol.pointer.PointerEventHandler}
+   * @private
+   */
+  this.dragger_ = new ol.pointer.PointerEventHandler(containerElement);
+
+  ol.events.listen(this.dragger_, ol.pointer.EventType.POINTERDOWN,
+      this.handleDraggerStart_, this);
+  ol.events.listen(this.dragger_, ol.pointer.EventType.POINTERMOVE,
+      this.handleDraggerDrag_, this);
+  ol.events.listen(this.dragger_, ol.pointer.EventType.POINTERUP,
+      this.handleDraggerEnd_, this);
+
+  ol.events.listen(containerElement, ol.events.EventType.CLICK,
+      this.handleContainerClick_, this);
+  ol.events.listen(thumbElement, ol.events.EventType.CLICK,
+      ol.events.Event.stopPropagation);
+
+  var render = options.render ? options.render : ol.control.ZoomSlider.render;
+
+  ol.control.Control.call(this, {
+    element: containerElement,
+    render: render
+  });
+};
+ol.inherits(ol.control.ZoomSlider, ol.control.Control);
+
+
+/**
+ * @inheritDoc
+ */
+ol.control.ZoomSlider.prototype.disposeInternal = function() {
+  this.dragger_.dispose();
+  ol.control.Control.prototype.disposeInternal.call(this);
+};
+
+
+/**
+ * The enum for available directions.
+ *
+ * @enum {number}
+ */
+ol.control.ZoomSlider.direction = {
+  VERTICAL: 0,
+  HORIZONTAL: 1
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.control.ZoomSlider.prototype.setMap = function(map) {
+  ol.control.Control.prototype.setMap.call(this, map);
+  if (map) {
+    map.render();
+  }
+};
+
+
+/**
+ * Initializes the slider element. This will determine and set this controls
+ * direction_ and also constrain the dragging of the thumb to always be within
+ * the bounds of the container.
+ *
+ * @private
+ */
+ol.control.ZoomSlider.prototype.initSlider_ = function() {
+  var container = this.element;
+  var containerSize = {
+    width: container.offsetWidth, height: container.offsetHeight
+  };
+
+  var thumb = container.firstElementChild;
+  var computedStyle = ol.global.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_ = ol.control.ZoomSlider.direction.HORIZONTAL;
+    this.widthLimit_ = containerSize.width - thumbWidth;
+  } else {
+    this.direction_ = ol.control.ZoomSlider.direction.VERTICAL;
+    this.heightLimit_ = containerSize.height - thumbHeight;
+  }
+  this.sliderInitialized_ = true;
+};
+
+
+/**
+ * Update the zoomslider element.
+ * @param {ol.MapEvent} mapEvent Map event.
+ * @this {ol.control.ZoomSlider}
+ * @api
+ */
+ol.control.ZoomSlider.render = function(mapEvent) {
+  if (!mapEvent.frameState) {
+    return;
+  }
+  goog.asserts.assert(mapEvent.frameState.viewState,
+      'viewState should be defined');
+  if (!this.sliderInitialized_) {
+    this.initSlider_();
+  }
+  var res = mapEvent.frameState.viewState.resolution;
+  if (res !== this.currentResolution_) {
+    this.currentResolution_ = res;
+    this.setThumbPosition_(res);
+  }
+};
+
+
+/**
+ * @param {Event} event The browser event to handle.
+ * @private
+ */
+ol.control.ZoomSlider.prototype.handleContainerClick_ = function(event) {
+  var map = this.getMap();
+  var view = map.getView();
+  var currentResolution = view.getResolution();
+  goog.asserts.assert(currentResolution,
+      'currentResolution should be defined');
+  map.beforeRender(ol.animation.zoom({
+    resolution: currentResolution,
+    duration: this.duration_,
+    easing: ol.easing.easeOut
+  }));
+  var relativePosition = this.getRelativePosition_(
+      event.offsetX - this.thumbSize_[0] / 2,
+      event.offsetY - this.thumbSize_[1] / 2);
+  var resolution = this.getResolutionForPosition_(relativePosition);
+  view.setResolution(view.constrainResolution(resolution));
+};
+
+
+/**
+ * Handle dragger start events.
+ * @param {ol.pointer.PointerEvent} event The drag event.
+ * @private
+ */
+ol.control.ZoomSlider.prototype.handleDraggerStart_ = function(event) {
+  if (!this.dragging_ &&
+      event.originalEvent.target === this.element.firstElementChild) {
+    this.getMap().getView().setHint(ol.ViewHint.INTERACTING, 1);
+    this.previousX_ = event.clientX;
+    this.previousY_ = event.clientY;
+    this.dragging_ = true;
+
+    if (this.dragListenerKeys_.length === 0) {
+      var drag = this.handleDraggerDrag_;
+      var end = this.handleDraggerEnd_;
+      this.dragListenerKeys_.push(
+        ol.events.listen(document, ol.events.EventType.MOUSEMOVE, drag, this),
+        ol.events.listen(document, ol.events.EventType.TOUCHMOVE, drag, this),
+        ol.events.listen(document, ol.pointer.EventType.POINTERMOVE, drag, this),
+        ol.events.listen(document, ol.events.EventType.MOUSEUP, end, this),
+        ol.events.listen(document, ol.events.EventType.TOUCHEND, end, this),
+        ol.events.listen(document, ol.pointer.EventType.POINTERUP, end, this)
+      );
+    }
+  }
+};
+
+
+/**
+ * Handle dragger drag events.
+ *
+ * @param {ol.pointer.PointerEvent|Event} event The drag event.
+ * @private
+ */
+ol.control.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.currentResolution_ = this.getResolutionForPosition_(relativePosition);
+    this.getMap().getView().setResolution(this.currentResolution_);
+    this.setThumbPosition_(this.currentResolution_);
+    this.previousX_ = event.clientX;
+    this.previousY_ = event.clientY;
+  }
+};
+
+
+/**
+ * Handle dragger end events.
+ * @param {ol.pointer.PointerEvent|Event} event The drag event.
+ * @private
+ */
+ol.control.ZoomSlider.prototype.handleDraggerEnd_ = function(event) {
+  if (this.dragging_) {
+    var map = this.getMap();
+    var view = map.getView();
+    view.setHint(ol.ViewHint.INTERACTING, -1);
+    goog.asserts.assert(this.currentResolution_,
+        'this.currentResolution_ should be defined');
+    map.beforeRender(ol.animation.zoom({
+      resolution: this.currentResolution_,
+      duration: this.duration_,
+      easing: ol.easing.easeOut
+    }));
+    var resolution = view.constrainResolution(this.currentResolution_);
+    view.setResolution(resolution);
+    this.dragging_ = false;
+    this.previousX_ = undefined;
+    this.previousY_ = undefined;
+    this.dragListenerKeys_.forEach(ol.events.unlistenByKey);
+    this.dragListenerKeys_.length = 0;
+  }
+};
+
+
+/**
+ * Positions the thumb inside its container according to the given resolution.
+ *
+ * @param {number} res The res.
+ * @private
+ */
+ol.control.ZoomSlider.prototype.setThumbPosition_ = function(res) {
+  var position = this.getPositionForResolution_(res);
+  var thumb = this.element.firstElementChild;
+
+  if (this.direction_ == ol.control.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
+ */
+ol.control.ZoomSlider.prototype.getRelativePosition_ = function(x, y) {
+  var amount;
+  if (this.direction_ === ol.control.ZoomSlider.direction.HORIZONTAL) {
+    amount = x / this.widthLimit_;
+  } else {
+    amount = y / this.heightLimit_;
+  }
+  return ol.math.clamp(amount, 0, 1);
+};
+
+
+/**
+ * Calculates the corresponding resolution of the thumb given its relative
+ * position (where 0 is the minimum and 1 is the maximum).
+ *
+ * @param {number} position The relative position of the thumb.
+ * @return {number} The corresponding resolution.
+ * @private
+ */
+ol.control.ZoomSlider.prototype.getResolutionForPosition_ = function(position) {
+  var fn = this.getMap().getView().getResolutionForValueFunction();
+  return fn(1 - position);
+};
+
+
+/**
+ * Determines the relative position of the slider for the given resolution.  A
+ * relative position of 0 corresponds to the minimum view resolution.  A
+ * relative position of 1 corresponds to the maximum view resolution.
+ *
+ * @param {number} res The resolution.
+ * @return {number} The relative position value (between 0 and 1).
+ * @private
+ */
+ol.control.ZoomSlider.prototype.getPositionForResolution_ = function(res) {
+  var fn = this.getMap().getView().getValueForResolutionFunction();
+  return 1 - fn(res);
+};
+
+goog.provide('ol.control.ZoomToExtent');
+
+goog.require('goog.asserts');
+goog.require('ol.events');
+goog.require('ol.events.EventType');
+goog.require('ol.control.Control');
+goog.require('ol.css');
+
+
+/**
+ * @classdesc
+ * A button control which, when pressed, changes the map view to a specific
+ * extent. To style this control use the css selector `.ol-zoom-extent`.
+ *
+ * @constructor
+ * @extends {ol.control.Control}
+ * @param {olx.control.ZoomToExtentOptions=} opt_options Options.
+ * @api stable
+ */
+ol.control.ZoomToExtent = function(opt_options) {
+  var options = opt_options ? opt_options : {};
+
+  /**
+   * @type {ol.Extent}
+   * @private
+   */
+  this.extent_ = options.extent ? options.extent : null;
+
+  var className = options.className !== undefined ? options.className :
+      'ol-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
+  );
+
+  ol.events.listen(button, ol.events.EventType.CLICK,
+      this.handleClick_, this);
+
+  var cssClasses = className + ' ' + ol.css.CLASS_UNSELECTABLE + ' ' +
+      ol.css.CLASS_CONTROL;
+  var element = document.createElement('div');
+  element.className = cssClasses;
+  element.appendChild(button);
+
+  ol.control.Control.call(this, {
+    element: element,
+    target: options.target
+  });
+};
+ol.inherits(ol.control.ZoomToExtent, ol.control.Control);
+
+
+/**
+ * @param {Event} event The event to handle
+ * @private
+ */
+ol.control.ZoomToExtent.prototype.handleClick_ = function(event) {
+  event.preventDefault();
+  this.handleZoomToExtent_();
+};
+
+
+/**
+ * @private
+ */
+ol.control.ZoomToExtent.prototype.handleZoomToExtent_ = function() {
+  var map = this.getMap();
+  var view = map.getView();
+  var extent = !this.extent_ ? view.getProjection().getExtent() : this.extent_;
+  var size = map.getSize();
+  goog.asserts.assert(size, 'size should be defined');
+  view.fit(extent, size);
+};
+
+goog.provide('ol.DeviceOrientation');
+
+goog.require('ol.events');
+goog.require('ol');
+goog.require('ol.Object');
+goog.require('ol.has');
+goog.require('ol.math');
+
+
+/**
+ * @enum {string}
+ */
+ol.DeviceOrientationProperty = {
+  ALPHA: 'alpha',
+  BETA: 'beta',
+  GAMMA: 'gamma',
+  HEADING: 'heading',
+  TRACKING: 'tracking'
+};
+
+
+/**
+ * @classdesc
+ * The ol.DeviceOrientation class provides access to information from
+ * DeviceOrientation events.  See the [HTML 5 DeviceOrientation Specification](
+ * http://www.w3.org/TR/orientation-event/) for more details.
+ *
+ * Many new computers, and especially mobile phones
+ * and tablets, provide hardware support for device orientation. Web
+ * developers targeting mobile devices will be especially interested in this
+ * class.
+ *
+ * Device orientation data are relative to a common starting point. For mobile
+ * devices, the starting point is to lay your phone face up on a table with the
+ * top of the phone pointing north. This represents the zero state. All
+ * angles are then relative to this state. For computers, it is the same except
+ * the screen is open at 90 degrees.
+ *
+ * Device orientation is reported as three angles - `alpha`, `beta`, and
+ * `gamma` - relative to the starting position along the three planar axes X, Y
+ * and Z. The X axis runs from the left edge to the right edge through the
+ * middle of the device. Similarly, the Y axis runs from the bottom to the top
+ * of the device through the middle. The Z axis runs from the back to the front
+ * through the middle. In the starting position, the X axis points to the
+ * right, the Y axis points away from you and the Z axis points straight up
+ * from the device lying flat.
+ *
+ * The three angles representing the device orientation are relative to the
+ * three axes. `alpha` indicates how much the device has been rotated around the
+ * Z axis, which is commonly interpreted as the compass heading (see note
+ * below). `beta` indicates how much the device has been rotated around the X
+ * axis, or how much it is tilted from front to back.  `gamma` indicates how
+ * much the device has been rotated around the Y axis, or how much it is tilted
+ * from left to right.
+ *
+ * For most browsers, the `alpha` value returns the compass heading so if the
+ * device points north, it will be 0.  With Safari on iOS, the 0 value of
+ * `alpha` is calculated from when device orientation was first requested.
+ * ol.DeviceOrientation provides the `heading` property which normalizes this
+ * behavior across all browsers for you.
+ *
+ * It is important to note that the HTML 5 DeviceOrientation specification
+ * indicates that `alpha`, `beta` and `gamma` are in degrees while the
+ * equivalent properties in ol.DeviceOrientation are in radians for consistency
+ * with all other uses of angles throughout OpenLayers.
+ *
+ * To get notified of device orientation changes, register a listener for the
+ * generic `change` event on your `ol.DeviceOrientation` instance.
+ *
+ * @see {@link http://www.w3.org/TR/orientation-event/}
+ *
+ * @constructor
+ * @extends {ol.Object}
+ * @param {olx.DeviceOrientationOptions=} opt_options Options.
+ * @api
+ */
+ol.DeviceOrientation = function(opt_options) {
+
+  ol.Object.call(this);
+
+  var options = opt_options ? opt_options : {};
+
+  /**
+   * @private
+   * @type {?ol.EventsKey}
+   */
+  this.listenerKey_ = null;
+
+  ol.events.listen(this,
+      ol.Object.getChangeEventType(ol.DeviceOrientationProperty.TRACKING),
+      this.handleTrackingChanged_, this);
+
+  this.setTracking(options.tracking !== undefined ? options.tracking : false);
+
+};
+ol.inherits(ol.DeviceOrientation, ol.Object);
+
+
+/**
+ * @inheritDoc
+ */
+ol.DeviceOrientation.prototype.disposeInternal = function() {
+  this.setTracking(false);
+  ol.Object.prototype.disposeInternal.call(this);
+};
+
+
+/**
+ * @private
+ * @param {Event} originalEvent Event.
+ */
+ol.DeviceOrientation.prototype.orientationChange_ = function(originalEvent) {
+  var event = /** @type {DeviceOrientationEvent} */ (originalEvent);
+  if (event.alpha !== null) {
+    var alpha = ol.math.toRadians(event.alpha);
+    this.set(ol.DeviceOrientationProperty.ALPHA, alpha);
+    // event.absolute is undefined in iOS.
+    if (typeof event.absolute === 'boolean' && event.absolute) {
+      this.set(ol.DeviceOrientationProperty.HEADING, alpha);
+    } else if (goog.isNumber(event.webkitCompassHeading) &&
+               event.webkitCompassAccuracy != -1) {
+      var heading = ol.math.toRadians(event.webkitCompassHeading);
+      this.set(ol.DeviceOrientationProperty.HEADING, heading);
+    }
+  }
+  if (event.beta !== null) {
+    this.set(ol.DeviceOrientationProperty.BETA,
+        ol.math.toRadians(event.beta));
+  }
+  if (event.gamma !== null) {
+    this.set(ol.DeviceOrientationProperty.GAMMA,
+        ol.math.toRadians(event.gamma));
+  }
+  this.changed();
+};
+
+
+/**
+ * Rotation around the device z-axis (in radians).
+ * @return {number|undefined} The euler angle in radians of the device from the
+ *     standard Z axis.
+ * @observable
+ * @api
+ */
+ol.DeviceOrientation.prototype.getAlpha = function() {
+  return /** @type {number|undefined} */ (
+      this.get(ol.DeviceOrientationProperty.ALPHA));
+};
+
+
+/**
+ * Rotation around the device x-axis (in radians).
+ * @return {number|undefined} The euler angle in radians of the device from the
+ *     planar X axis.
+ * @observable
+ * @api
+ */
+ol.DeviceOrientation.prototype.getBeta = function() {
+  return /** @type {number|undefined} */ (
+      this.get(ol.DeviceOrientationProperty.BETA));
+};
+
+
+/**
+ * Rotation around the device y-axis (in radians).
+ * @return {number|undefined} The euler angle in radians of the device from the
+ *     planar Y axis.
+ * @observable
+ * @api
+ */
+ol.DeviceOrientation.prototype.getGamma = function() {
+  return /** @type {number|undefined} */ (
+      this.get(ol.DeviceOrientationProperty.GAMMA));
+};
+
+
+/**
+ * The heading of the device relative to north (in radians).
+ * @return {number|undefined} The heading of the device relative to north, in
+ *     radians, normalizing for different browser behavior.
+ * @observable
+ * @api
+ */
+ol.DeviceOrientation.prototype.getHeading = function() {
+  return /** @type {number|undefined} */ (
+      this.get(ol.DeviceOrientationProperty.HEADING));
+};
+
+
+/**
+ * Determine if orientation is being tracked.
+ * @return {boolean} Changes in device orientation are being tracked.
+ * @observable
+ * @api
+ */
+ol.DeviceOrientation.prototype.getTracking = function() {
+  return /** @type {boolean} */ (
+      this.get(ol.DeviceOrientationProperty.TRACKING));
+};
+
+
+/**
+ * @private
+ */
+ol.DeviceOrientation.prototype.handleTrackingChanged_ = function() {
+  if (ol.has.DEVICE_ORIENTATION) {
+    var tracking = this.getTracking();
+    if (tracking && !this.listenerKey_) {
+      this.listenerKey_ = ol.events.listen(ol.global, 'deviceorientation',
+          this.orientationChange_, this);
+    } else if (!tracking && this.listenerKey_ !== null) {
+      ol.events.unlistenByKey(this.listenerKey_);
+      this.listenerKey_ = null;
+    }
+  }
+};
+
+
+/**
+ * Enable or disable tracking of device orientation events.
+ * @param {boolean} tracking The status of tracking changes to alpha, beta and
+ *     gamma. If true, changes are tracked and reported immediately.
+ * @observable
+ * @api
+ */
+ol.DeviceOrientation.prototype.setTracking = function(tracking) {
+  this.set(ol.DeviceOrientationProperty.TRACKING, tracking);
+};
+
+goog.provide('ol.format.Feature');
+
+goog.require('ol.geom.Geometry');
+goog.require('ol.proj');
+
+
+/**
+ * @classdesc
+ * Abstract base class; normally only used for creating subclasses and not
+ * instantiated in apps.
+ * Base class for feature formats.
+ * {ol.format.Feature} subclasses provide the ability to decode and encode
+ * {@link ol.Feature} objects from a variety of commonly used geospatial
+ * file formats.  See the documentation for each format for more details.
+ *
+ * @constructor
+ * @api stable
+ */
+ol.format.Feature = function() {
+
+  /**
+   * @protected
+   * @type {ol.proj.Projection}
+   */
+  this.defaultDataProjection = null;
+
+};
+
+
+/**
+ * @return {Array.<string>} Extensions.
+ */
+ol.format.Feature.prototype.getExtensions = goog.abstractMethod;
+
+
+/**
+ * Adds the data projection to the read options.
+ * @param {Document|Node|Object|string} source Source.
+ * @param {olx.format.ReadOptions=} opt_options Options.
+ * @return {olx.format.ReadOptions|undefined} Options.
+ * @protected
+ */
+ol.format.Feature.prototype.getReadOptions = function(source, opt_options) {
+  var options;
+  if (opt_options) {
+    options = {
+      dataProjection: opt_options.dataProjection ?
+          opt_options.dataProjection : this.readProjection(source),
+      featureProjection: opt_options.featureProjection
+    };
+  }
+  return this.adaptOptions(options);
+};
+
+
+/**
+ * Sets the `defaultDataProjection` on the options, if no `dataProjection`
+ * is set.
+ * @param {olx.format.WriteOptions|olx.format.ReadOptions|undefined} options
+ *     Options.
+ * @protected
+ * @return {olx.format.WriteOptions|olx.format.ReadOptions|undefined}
+ *     Updated options.
+ */
+ol.format.Feature.prototype.adaptOptions = function(options) {
+  var updatedOptions;
+  if (options) {
+    updatedOptions = {
+      featureProjection: options.featureProjection,
+      dataProjection: options.dataProjection ?
+          options.dataProjection : this.defaultDataProjection,
+      rightHanded: options.rightHanded
+    };
+    if (options.decimals) {
+      updatedOptions.decimals = options.decimals;
+    }
+  }
+  return updatedOptions;
+};
+
+
+/**
+ * @return {ol.format.FormatType} Format.
+ */
+ol.format.Feature.prototype.getType = goog.abstractMethod;
+
+
+/**
+ * Read a single feature from a source.
+ *
+ * @param {Document|Node|Object|string} source Source.
+ * @param {olx.format.ReadOptions=} opt_options Read options.
+ * @return {ol.Feature} Feature.
+ */
+ol.format.Feature.prototype.readFeature = goog.abstractMethod;
+
+
+/**
+ * Read all features from a source.
+ *
+ * @param {Document|Node|ArrayBuffer|Object|string} source Source.
+ * @param {olx.format.ReadOptions=} opt_options Read options.
+ * @return {Array.<ol.Feature>} Features.
+ */
+ol.format.Feature.prototype.readFeatures = goog.abstractMethod;
+
+
+/**
+ * Read a single geometry from a source.
+ *
+ * @param {Document|Node|Object|string} source Source.
+ * @param {olx.format.ReadOptions=} opt_options Read options.
+ * @return {ol.geom.Geometry} Geometry.
+ */
+ol.format.Feature.prototype.readGeometry = goog.abstractMethod;
+
+
+/**
+ * Read the projection from a source.
+ *
+ * @param {Document|Node|Object|string} source Source.
+ * @return {ol.proj.Projection} Projection.
+ */
+ol.format.Feature.prototype.readProjection = goog.abstractMethod;
+
+
+/**
+ * Encode a feature in this format.
+ *
+ * @param {ol.Feature} feature Feature.
+ * @param {olx.format.WriteOptions=} opt_options Write options.
+ * @return {string} Result.
+ */
+ol.format.Feature.prototype.writeFeature = goog.abstractMethod;
+
+
+/**
+ * Encode an array of features in this format.
+ *
+ * @param {Array.<ol.Feature>} features Features.
+ * @param {olx.format.WriteOptions=} opt_options Write options.
+ * @return {string} Result.
+ */
+ol.format.Feature.prototype.writeFeatures = goog.abstractMethod;
+
+
+/**
+ * Write a single geometry in this format.
+ *
+ * @param {ol.geom.Geometry} geometry Geometry.
+ * @param {olx.format.WriteOptions=} opt_options Write options.
+ * @return {string} Result.
+ */
+ol.format.Feature.prototype.writeGeometry = goog.abstractMethod;
+
+
+/**
+ * @param {ol.geom.Geometry|ol.Extent} geometry Geometry.
+ * @param {boolean} write Set to true for writing, false for reading.
+ * @param {(olx.format.WriteOptions|olx.format.ReadOptions)=} opt_options
+ *     Options.
+ * @return {ol.geom.Geometry|ol.Extent} Transformed geometry.
+ * @protected
+ */
+ol.format.Feature.transformWithOptions = function(
+    geometry, write, opt_options) {
+  var featureProjection = opt_options ?
+      ol.proj.get(opt_options.featureProjection) : null;
+  var dataProjection = opt_options ?
+      ol.proj.get(opt_options.dataProjection) : null;
+  /**
+   * @type {ol.geom.Geometry|ol.Extent}
+   */
+  var transformed;
+  if (featureProjection && dataProjection &&
+      !ol.proj.equivalent(featureProjection, dataProjection)) {
+    if (geometry instanceof ol.geom.Geometry) {
+      transformed = (write ? geometry.clone() : geometry).transform(
+          write ? featureProjection : dataProjection,
+          write ? dataProjection : featureProjection);
+    } else {
+      // FIXME this is necessary because ol.format.GML treats extents
+      // as geometries
+      transformed = ol.proj.transformExtent(
+          write ? geometry.slice() : geometry,
+          write ? featureProjection : dataProjection,
+          write ? dataProjection : featureProjection);
+    }
+  } else {
+    transformed = geometry;
+  }
+  if (write && opt_options && opt_options.decimals) {
+    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 (Array.isArray(transformed)) {
+      transform(transformed);
+    } else {
+      transformed.applyTransform(transform);
+    }
+  }
+  return transformed;
+};
+
+goog.provide('ol.format.JSONFeature');
+
+goog.require('goog.asserts');
+goog.require('ol.format.Feature');
+goog.require('ol.format.FormatType');
+
+
+/**
+ * @classdesc
+ * Abstract base class; normally only used for creating subclasses and not
+ * instantiated in apps.
+ * Base class for JSON feature formats.
+ *
+ * @constructor
+ * @extends {ol.format.Feature}
+ */
+ol.format.JSONFeature = function() {
+  ol.format.Feature.call(this);
+};
+ol.inherits(ol.format.JSONFeature, ol.format.Feature);
+
+
+/**
+ * @param {Document|Node|Object|string} source Source.
+ * @private
+ * @return {Object} Object.
+ */
+ol.format.JSONFeature.prototype.getObject_ = function(source) {
+  if (goog.isObject(source)) {
+    return source;
+  } else if (typeof source === 'string') {
+    var object = JSON.parse(source);
+    return object ? /** @type {Object} */ (object) : null;
+  } else {
+    goog.asserts.fail();
+    return null;
+  }
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.format.JSONFeature.prototype.getType = function() {
+  return ol.format.FormatType.JSON;
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.format.JSONFeature.prototype.readFeature = function(source, opt_options) {
+  return this.readFeatureFromObject(
+      this.getObject_(source), this.getReadOptions(source, opt_options));
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.format.JSONFeature.prototype.readFeatures = function(source, opt_options) {
+  return this.readFeaturesFromObject(
+      this.getObject_(source), this.getReadOptions(source, opt_options));
+};
+
+
+/**
+ * @param {Object} object Object.
+ * @param {olx.format.ReadOptions=} opt_options Read options.
+ * @protected
+ * @return {ol.Feature} Feature.
+ */
+ol.format.JSONFeature.prototype.readFeatureFromObject = goog.abstractMethod;
+
+
+/**
+ * @param {Object} object Object.
+ * @param {olx.format.ReadOptions=} opt_options Read options.
+ * @protected
+ * @return {Array.<ol.Feature>} Features.
+ */
+ol.format.JSONFeature.prototype.readFeaturesFromObject = goog.abstractMethod;
+
+
+/**
+ * @inheritDoc
+ */
+ol.format.JSONFeature.prototype.readGeometry = function(source, opt_options) {
+  return this.readGeometryFromObject(
+      this.getObject_(source), this.getReadOptions(source, opt_options));
+};
+
+
+/**
+ * @param {Object} object Object.
+ * @param {olx.format.ReadOptions=} opt_options Read options.
+ * @protected
+ * @return {ol.geom.Geometry} Geometry.
+ */
+ol.format.JSONFeature.prototype.readGeometryFromObject = goog.abstractMethod;
+
+
+/**
+ * @inheritDoc
+ */
+ol.format.JSONFeature.prototype.readProjection = function(source) {
+  return this.readProjectionFromObject(this.getObject_(source));
+};
+
+
+/**
+ * @param {Object} object Object.
+ * @protected
+ * @return {ol.proj.Projection} Projection.
+ */
+ol.format.JSONFeature.prototype.readProjectionFromObject = goog.abstractMethod;
+
+
+/**
+ * @inheritDoc
+ */
+ol.format.JSONFeature.prototype.writeFeature = function(feature, opt_options) {
+  return JSON.stringify(this.writeFeatureObject(feature, opt_options));
+};
+
+
+/**
+ * @param {ol.Feature} feature Feature.
+ * @param {olx.format.WriteOptions=} opt_options Write options.
+ * @return {Object} Object.
+ */
+ol.format.JSONFeature.prototype.writeFeatureObject = goog.abstractMethod;
+
+
+/**
+ * @inheritDoc
+ */
+ol.format.JSONFeature.prototype.writeFeatures = function(features, opt_options) {
+  return JSON.stringify(this.writeFeaturesObject(features, opt_options));
+};
+
+
+/**
+ * @param {Array.<ol.Feature>} features Features.
+ * @param {olx.format.WriteOptions=} opt_options Write options.
+ * @return {Object} Object.
+ */
+ol.format.JSONFeature.prototype.writeFeaturesObject = goog.abstractMethod;
+
+
+/**
+ * @inheritDoc
+ */
+ol.format.JSONFeature.prototype.writeGeometry = function(geometry, opt_options) {
+  return JSON.stringify(this.writeGeometryObject(geometry, opt_options));
+};
+
+
+/**
+ * @param {ol.geom.Geometry} geometry Geometry.
+ * @param {olx.format.WriteOptions=} opt_options Write options.
+ * @return {Object} Object.
+ */
+ol.format.JSONFeature.prototype.writeGeometryObject = goog.abstractMethod;
+
+goog.provide('ol.geom.flat.interpolate');
+
+goog.require('goog.asserts');
+goog.require('ol.array');
+goog.require('ol.math');
+
+
+/**
+ * @param {Array.<number>} flatCoordinates Flat coordinates.
+ * @param {number} offset Offset.
+ * @param {number} end End.
+ * @param {number} stride Stride.
+ * @param {number} fraction Fraction.
+ * @param {Array.<number>=} opt_dest Destination.
+ * @return {Array.<number>} Destination.
+ */
+ol.geom.flat.interpolate.lineString = function(flatCoordinates, offset, end, stride, fraction, opt_dest) {
+  // FIXME does not work when vertices are repeated
+  // FIXME interpolate extra dimensions
+  goog.asserts.assert(0 <= fraction && fraction <= 1,
+      'fraction should be in between 0 and 1');
+  var pointX = NaN;
+  var pointY = NaN;
+  var n = (end - offset) / stride;
+  if (n === 0) {
+    goog.asserts.fail('n cannot be 0');
+  } else if (n == 1) {
+    pointX = flatCoordinates[offset];
+    pointY = flatCoordinates[offset + 1];
+  } else if (n == 2) {
+    pointX = (1 - fraction) * flatCoordinates[offset] +
+        fraction * flatCoordinates[offset + stride];
+    pointY = (1 - fraction) * flatCoordinates[offset + 1] +
+        fraction * flatCoordinates[offset + stride + 1];
+  } else {
+    var x1 = flatCoordinates[offset];
+    var y1 = flatCoordinates[offset + 1];
+    var length = 0;
+    var cumulativeLengths = [0];
+    var i;
+    for (i = offset + stride; i < end; i += stride) {
+      var x2 = flatCoordinates[i];
+      var y2 = flatCoordinates[i + 1];
+      length += Math.sqrt((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1));
+      cumulativeLengths.push(length);
+      x1 = x2;
+      y1 = y2;
+    }
+    var target = fraction * length;
+    var index = ol.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 = ol.math.lerp(
+          flatCoordinates[o], flatCoordinates[o + stride], t);
+      pointY = ol.math.lerp(
+          flatCoordinates[o + 1], flatCoordinates[o + stride + 1], t);
+    } else {
+      pointX = flatCoordinates[offset + index * stride];
+      pointY = flatCoordinates[offset + index * stride + 1];
+    }
+  }
+  if (opt_dest) {
+    opt_dest[0] = pointX;
+    opt_dest[1] = pointY;
+    return opt_dest;
+  } else {
+    return [pointX, pointY];
+  }
+};
+
+
+/**
+ * @param {Array.<number>} flatCoordinates Flat coordinates.
+ * @param {number} offset Offset.
+ * @param {number} end End.
+ * @param {number} stride Stride.
+ * @param {number} m M.
+ * @param {boolean} extrapolate Extrapolate.
+ * @return {ol.Coordinate} Coordinate.
+ */
+ol.geom.flat.lineStringCoordinateAtM = function(flatCoordinates, offset, end, stride, m, extrapolate) {
+  if (end == offset) {
+    return null;
+  }
+  var coordinate;
+  if (m < flatCoordinates[offset + stride - 1]) {
+    if (extrapolate) {
+      coordinate = flatCoordinates.slice(offset, offset + stride);
+      coordinate[stride - 1] = m;
+      return coordinate;
+    } else {
+      return null;
+    }
+  } else if (flatCoordinates[end - 1] < m) {
+    if (extrapolate) {
+      coordinate = flatCoordinates.slice(end - stride, end);
+      coordinate[stride - 1] = m;
+      return coordinate;
+    } else {
+      return null;
+    }
+  }
+  // FIXME use O(1) search
+  if (m == flatCoordinates[offset + stride - 1]) {
+    return flatCoordinates.slice(offset, offset + stride);
+  }
+  var lo = offset / stride;
+  var hi = end / stride;
+  while (lo < hi) {
+    var mid = (lo + hi) >> 1;
+    if (m < flatCoordinates[(mid + 1) * stride - 1]) {
+      hi = mid;
+    } else {
+      lo = mid + 1;
+    }
+  }
+  var m0 = flatCoordinates[lo * stride - 1];
+  if (m == m0) {
+    return flatCoordinates.slice((lo - 1) * stride, (lo - 1) * stride + stride);
+  }
+  var m1 = flatCoordinates[(lo + 1) * stride - 1];
+  goog.asserts.assert(m0 < m, 'm0 should be less than m');
+  goog.asserts.assert(m <= m1, 'm should be less than or equal to m1');
+  var t = (m - m0) / (m1 - m0);
+  coordinate = [];
+  var i;
+  for (i = 0; i < stride - 1; ++i) {
+    coordinate.push(ol.math.lerp(flatCoordinates[(lo - 1) * stride + i],
+        flatCoordinates[lo * stride + i], t));
+  }
+  coordinate.push(m);
+  goog.asserts.assert(coordinate.length == stride,
+      'length of coordinate array should match stride');
+  return coordinate;
+};
+
+
+/**
+ * @param {Array.<number>} flatCoordinates Flat coordinates.
+ * @param {number} offset Offset.
+ * @param {Array.<number>} ends Ends.
+ * @param {number} stride Stride.
+ * @param {number} m M.
+ * @param {boolean} extrapolate Extrapolate.
+ * @param {boolean} interpolate Interpolate.
+ * @return {ol.Coordinate} Coordinate.
+ */
+ol.geom.flat.lineStringsCoordinateAtM = function(
+    flatCoordinates, offset, ends, stride, m, extrapolate, interpolate) {
+  if (interpolate) {
+    return ol.geom.flat.lineStringCoordinateAtM(
+        flatCoordinates, offset, ends[ends.length - 1], stride, m, extrapolate);
+  }
+  var coordinate;
+  if (m < flatCoordinates[stride - 1]) {
+    if (extrapolate) {
+      coordinate = flatCoordinates.slice(0, stride);
+      coordinate[stride - 1] = m;
+      return coordinate;
+    } else {
+      return null;
+    }
+  }
+  if (flatCoordinates[flatCoordinates.length - 1] < m) {
+    if (extrapolate) {
+      coordinate = flatCoordinates.slice(flatCoordinates.length - stride);
+      coordinate[stride - 1] = m;
+      return coordinate;
+    } else {
+      return null;
+    }
+  }
+  var i, ii;
+  for (i = 0, ii = ends.length; i < ii; ++i) {
+    var end = ends[i];
+    if (offset == end) {
+      continue;
+    }
+    if (m < flatCoordinates[offset + stride - 1]) {
+      return null;
+    } else if (m <= flatCoordinates[end - 1]) {
+      return ol.geom.flat.lineStringCoordinateAtM(
+          flatCoordinates, offset, end, stride, m, false);
+    }
+    offset = end;
+  }
+  goog.asserts.fail(
+      'ol.geom.flat.lineStringsCoordinateAtM should have returned');
+  return null;
+};
+
+goog.provide('ol.geom.flat.length');
+
+
+/**
+ * @param {Array.<number>} flatCoordinates Flat coordinates.
+ * @param {number} offset Offset.
+ * @param {number} end End.
+ * @param {number} stride Stride.
+ * @return {number} Length.
+ */
+ol.geom.flat.length.lineString = function(flatCoordinates, offset, end, stride) {
+  var x1 = flatCoordinates[offset];
+  var y1 = flatCoordinates[offset + 1];
+  var length = 0;
+  var i;
+  for (i = offset + stride; i < end; i += stride) {
+    var x2 = flatCoordinates[i];
+    var y2 = flatCoordinates[i + 1];
+    length += Math.sqrt((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1));
+    x1 = x2;
+    y1 = y2;
+  }
+  return length;
+};
+
+
+/**
+ * @param {Array.<number>} flatCoordinates Flat coordinates.
+ * @param {number} offset Offset.
+ * @param {number} end End.
+ * @param {number} stride Stride.
+ * @return {number} Perimeter.
+ */
+ol.geom.flat.length.linearRing = function(flatCoordinates, offset, end, stride) {
+  var perimeter =
+      ol.geom.flat.length.lineString(flatCoordinates, offset, end, stride);
+  var dx = flatCoordinates[end - stride] - flatCoordinates[offset];
+  var dy = flatCoordinates[end - stride + 1] - flatCoordinates[offset + 1];
+  perimeter += Math.sqrt(dx * dx + dy * dy);
+  return perimeter;
+};
+
+goog.provide('ol.geom.LineString');
+
+goog.require('goog.asserts');
+goog.require('ol');
+goog.require('ol.array');
+goog.require('ol.extent');
+goog.require('ol.geom.GeometryLayout');
+goog.require('ol.geom.GeometryType');
+goog.require('ol.geom.SimpleGeometry');
+goog.require('ol.geom.flat.closest');
+goog.require('ol.geom.flat.deflate');
+goog.require('ol.geom.flat.inflate');
+goog.require('ol.geom.flat.interpolate');
+goog.require('ol.geom.flat.intersectsextent');
+goog.require('ol.geom.flat.length');
+goog.require('ol.geom.flat.segments');
+goog.require('ol.geom.flat.simplify');
+
+
+/**
+ * @classdesc
+ * Linestring geometry.
+ *
+ * @constructor
+ * @extends {ol.geom.SimpleGeometry}
+ * @param {Array.<ol.Coordinate>} coordinates Coordinates.
+ * @param {ol.geom.GeometryLayout=} opt_layout Layout.
+ * @api stable
+ */
+ol.geom.LineString = function(coordinates, opt_layout) {
+
+  ol.geom.SimpleGeometry.call(this);
+
+  /**
+   * @private
+   * @type {ol.Coordinate}
+   */
+  this.flatMidpoint_ = null;
+
+  /**
+   * @private
+   * @type {number}
+   */
+  this.flatMidpointRevision_ = -1;
+
+  /**
+   * @private
+   * @type {number}
+   */
+  this.maxDelta_ = -1;
+
+  /**
+   * @private
+   * @type {number}
+   */
+  this.maxDeltaRevision_ = -1;
+
+  this.setCoordinates(coordinates, opt_layout);
+
+};
+ol.inherits(ol.geom.LineString, ol.geom.SimpleGeometry);
+
+
+/**
+ * Append the passed coordinate to the coordinates of the linestring.
+ * @param {ol.Coordinate} coordinate Coordinate.
+ * @api stable
+ */
+ol.geom.LineString.prototype.appendCoordinate = function(coordinate) {
+  goog.asserts.assert(coordinate.length == this.stride,
+      'length of coordinate array should match stride');
+  if (!this.flatCoordinates) {
+    this.flatCoordinates = coordinate.slice();
+  } else {
+    ol.array.extend(this.flatCoordinates, coordinate);
+  }
+  this.changed();
+};
+
+
+/**
+ * Make a complete copy of the geometry.
+ * @return {!ol.geom.LineString} Clone.
+ * @api stable
+ */
+ol.geom.LineString.prototype.clone = function() {
+  var lineString = new ol.geom.LineString(null);
+  lineString.setFlatCoordinates(this.layout, this.flatCoordinates.slice());
+  return lineString;
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.geom.LineString.prototype.closestPointXY = function(x, y, closestPoint, minSquaredDistance) {
+  if (minSquaredDistance <
+      ol.extent.closestSquaredDistanceXY(this.getExtent(), x, y)) {
+    return minSquaredDistance;
+  }
+  if (this.maxDeltaRevision_ != this.getRevision()) {
+    this.maxDelta_ = Math.sqrt(ol.geom.flat.closest.getMaxSquaredDelta(
+        this.flatCoordinates, 0, this.flatCoordinates.length, this.stride, 0));
+    this.maxDeltaRevision_ = this.getRevision();
+  }
+  return ol.geom.flat.closest.getClosestPoint(
+      this.flatCoordinates, 0, this.flatCoordinates.length, this.stride,
+      this.maxDelta_, false, x, y, closestPoint, minSquaredDistance);
+};
+
+
+/**
+ * Iterate over each segment, calling the provided callback.
+ * If the callback returns a truthy value the function returns that
+ * value immediately. Otherwise the function returns `false`.
+ *
+ * @param {function(this: S, ol.Coordinate, ol.Coordinate): T} callback Function
+ *     called for each segment.
+ * @param {S=} opt_this The object to be used as the value of 'this'
+ *     within callback.
+ * @return {T|boolean} Value.
+ * @template T,S
+ * @api
+ */
+ol.geom.LineString.prototype.forEachSegment = function(callback, opt_this) {
+  return ol.geom.flat.segments.forEach(this.flatCoordinates, 0,
+      this.flatCoordinates.length, this.stride, callback, opt_this);
+};
+
+
+/**
+ * Returns the coordinate at `m` using linear interpolation, or `null` if no
+ * such coordinate exists.
+ *
+ * `opt_extrapolate` controls extrapolation beyond the range of Ms in the
+ * MultiLineString. If `opt_extrapolate` is `true` then Ms less than the first
+ * M will return the first coordinate and Ms greater than the last M will
+ * return the last coordinate.
+ *
+ * @param {number} m M.
+ * @param {boolean=} opt_extrapolate Extrapolate. Default is `false`.
+ * @return {ol.Coordinate} Coordinate.
+ * @api stable
+ */
+ol.geom.LineString.prototype.getCoordinateAtM = function(m, opt_extrapolate) {
+  if (this.layout != ol.geom.GeometryLayout.XYM &&
+      this.layout != ol.geom.GeometryLayout.XYZM) {
+    return null;
+  }
+  var extrapolate = opt_extrapolate !== undefined ? opt_extrapolate : false;
+  return ol.geom.flat.lineStringCoordinateAtM(this.flatCoordinates, 0,
+      this.flatCoordinates.length, this.stride, m, extrapolate);
+};
+
+
+/**
+ * Return the coordinates of the linestring.
+ * @return {Array.<ol.Coordinate>} Coordinates.
+ * @api stable
+ */
+ol.geom.LineString.prototype.getCoordinates = function() {
+  return ol.geom.flat.inflate.coordinates(
+      this.flatCoordinates, 0, this.flatCoordinates.length, this.stride);
+};
+
+
+/**
+ * Return the coordinate at the provided fraction along the linestring.
+ * The `fraction` is a number between 0 and 1, where 0 is the start of the
+ * linestring and 1 is the end.
+ * @param {number} fraction Fraction.
+ * @param {ol.Coordinate=} opt_dest Optional coordinate whose values will
+ *     be modified. If not provided, a new coordinate will be returned.
+ * @return {ol.Coordinate} Coordinate of the interpolated point.
+ * @api
+ */
+ol.geom.LineString.prototype.getCoordinateAt = function(fraction, opt_dest) {
+  return ol.geom.flat.interpolate.lineString(
+      this.flatCoordinates, 0, this.flatCoordinates.length, this.stride,
+      fraction, opt_dest);
+};
+
+
+/**
+ * Return the length of the linestring on projected plane.
+ * @return {number} Length (on projected plane).
+ * @api stable
+ */
+ol.geom.LineString.prototype.getLength = function() {
+  return ol.geom.flat.length.lineString(
+      this.flatCoordinates, 0, this.flatCoordinates.length, this.stride);
+};
+
+
+/**
+ * @return {Array.<number>} Flat midpoint.
+ */
+ol.geom.LineString.prototype.getFlatMidpoint = function() {
+  if (this.flatMidpointRevision_ != this.getRevision()) {
+    this.flatMidpoint_ = this.getCoordinateAt(0.5, this.flatMidpoint_);
+    this.flatMidpointRevision_ = this.getRevision();
+  }
+  return this.flatMidpoint_;
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.geom.LineString.prototype.getSimplifiedGeometryInternal = function(squaredTolerance) {
+  var simplifiedFlatCoordinates = [];
+  simplifiedFlatCoordinates.length = ol.geom.flat.simplify.douglasPeucker(
+      this.flatCoordinates, 0, this.flatCoordinates.length, this.stride,
+      squaredTolerance, simplifiedFlatCoordinates, 0);
+  var simplifiedLineString = new ol.geom.LineString(null);
+  simplifiedLineString.setFlatCoordinates(
+      ol.geom.GeometryLayout.XY, simplifiedFlatCoordinates);
+  return simplifiedLineString;
+};
+
+
+/**
+ * @inheritDoc
+ * @api stable
+ */
+ol.geom.LineString.prototype.getType = function() {
+  return ol.geom.GeometryType.LINE_STRING;
+};
+
+
+/**
+ * @inheritDoc
+ * @api stable
+ */
+ol.geom.LineString.prototype.intersectsExtent = function(extent) {
+  return ol.geom.flat.intersectsextent.lineString(
+      this.flatCoordinates, 0, this.flatCoordinates.length, this.stride,
+      extent);
+};
+
+
+/**
+ * Set the coordinates of the linestring.
+ * @param {Array.<ol.Coordinate>} coordinates Coordinates.
+ * @param {ol.geom.GeometryLayout=} opt_layout Layout.
+ * @api stable
+ */
+ol.geom.LineString.prototype.setCoordinates = function(coordinates, opt_layout) {
+  if (!coordinates) {
+    this.setFlatCoordinates(ol.geom.GeometryLayout.XY, null);
+  } else {
+    this.setLayout(opt_layout, coordinates, 1);
+    if (!this.flatCoordinates) {
+      this.flatCoordinates = [];
+    }
+    this.flatCoordinates.length = ol.geom.flat.deflate.coordinates(
+        this.flatCoordinates, 0, coordinates, this.stride);
+    this.changed();
+  }
+};
+
+
+/**
+ * @param {ol.geom.GeometryLayout} layout Layout.
+ * @param {Array.<number>} flatCoordinates Flat coordinates.
+ */
+ol.geom.LineString.prototype.setFlatCoordinates = function(layout, flatCoordinates) {
+  this.setFlatCoordinatesInternal(layout, flatCoordinates);
+  this.changed();
+};
+
+goog.provide('ol.geom.MultiLineString');
+
+goog.require('goog.asserts');
+goog.require('ol');
+goog.require('ol.array');
+goog.require('ol.extent');
+goog.require('ol.geom.GeometryLayout');
+goog.require('ol.geom.GeometryType');
+goog.require('ol.geom.LineString');
+goog.require('ol.geom.SimpleGeometry');
+goog.require('ol.geom.flat.closest');
+goog.require('ol.geom.flat.deflate');
+goog.require('ol.geom.flat.inflate');
+goog.require('ol.geom.flat.interpolate');
+goog.require('ol.geom.flat.intersectsextent');
+goog.require('ol.geom.flat.simplify');
+
+
+/**
+ * @classdesc
+ * Multi-linestring geometry.
+ *
+ * @constructor
+ * @extends {ol.geom.SimpleGeometry}
+ * @param {Array.<Array.<ol.Coordinate>>} coordinates Coordinates.
+ * @param {ol.geom.GeometryLayout=} opt_layout Layout.
+ * @api stable
+ */
+ol.geom.MultiLineString = function(coordinates, opt_layout) {
+
+  ol.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);
+
+};
+ol.inherits(ol.geom.MultiLineString, ol.geom.SimpleGeometry);
+
+
+/**
+ * Append the passed linestring to the multilinestring.
+ * @param {ol.geom.LineString} lineString LineString.
+ * @api stable
+ */
+ol.geom.MultiLineString.prototype.appendLineString = function(lineString) {
+  goog.asserts.assert(lineString.getLayout() == this.layout,
+      'layout of lineString should match the layout');
+  if (!this.flatCoordinates) {
+    this.flatCoordinates = lineString.getFlatCoordinates().slice();
+  } else {
+    ol.array.extend(
+        this.flatCoordinates, lineString.getFlatCoordinates().slice());
+  }
+  this.ends_.push(this.flatCoordinates.length);
+  this.changed();
+};
+
+
+/**
+ * Make a complete copy of the geometry.
+ * @return {!ol.geom.MultiLineString} Clone.
+ * @api stable
+ */
+ol.geom.MultiLineString.prototype.clone = function() {
+  var multiLineString = new ol.geom.MultiLineString(null);
+  multiLineString.setFlatCoordinates(
+      this.layout, this.flatCoordinates.slice(), this.ends_.slice());
+  return multiLineString;
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.geom.MultiLineString.prototype.closestPointXY = function(x, y, closestPoint, minSquaredDistance) {
+  if (minSquaredDistance <
+      ol.extent.closestSquaredDistanceXY(this.getExtent(), x, y)) {
+    return minSquaredDistance;
+  }
+  if (this.maxDeltaRevision_ != this.getRevision()) {
+    this.maxDelta_ = Math.sqrt(ol.geom.flat.closest.getsMaxSquaredDelta(
+        this.flatCoordinates, 0, this.ends_, this.stride, 0));
+    this.maxDeltaRevision_ = this.getRevision();
+  }
+  return ol.geom.flat.closest.getsClosestPoint(
+      this.flatCoordinates, 0, this.ends_, this.stride,
+      this.maxDelta_, false, x, y, closestPoint, minSquaredDistance);
+};
+
+
+/**
+ * Returns the coordinate at `m` using linear interpolation, or `null` if no
+ * such coordinate exists.
+ *
+ * `opt_extrapolate` controls extrapolation beyond the range of Ms in the
+ * MultiLineString. If `opt_extrapolate` is `true` then Ms less than the first
+ * M will return the first coordinate and Ms greater than the last M will
+ * return the last coordinate.
+ *
+ * `opt_interpolate` controls interpolation between consecutive LineStrings
+ * within the MultiLineString. If `opt_interpolate` is `true` the coordinates
+ * will be linearly interpolated between the last coordinate of one LineString
+ * and the first coordinate of the next LineString.  If `opt_interpolate` is
+ * `false` then the function will return `null` for Ms falling between
+ * LineStrings.
+ *
+ * @param {number} m M.
+ * @param {boolean=} opt_extrapolate Extrapolate. Default is `false`.
+ * @param {boolean=} opt_interpolate Interpolate. Default is `false`.
+ * @return {ol.Coordinate} Coordinate.
+ * @api stable
+ */
+ol.geom.MultiLineString.prototype.getCoordinateAtM = function(m, opt_extrapolate, opt_interpolate) {
+  if ((this.layout != ol.geom.GeometryLayout.XYM &&
+       this.layout != ol.geom.GeometryLayout.XYZM) ||
+      this.flatCoordinates.length === 0) {
+    return null;
+  }
+  var extrapolate = opt_extrapolate !== undefined ? opt_extrapolate : false;
+  var interpolate = opt_interpolate !== undefined ? opt_interpolate : false;
+  return ol.geom.flat.lineStringsCoordinateAtM(this.flatCoordinates, 0,
+      this.ends_, this.stride, m, extrapolate, interpolate);
+};
+
+
+/**
+ * Return the coordinates of the multilinestring.
+ * @return {Array.<Array.<ol.Coordinate>>} Coordinates.
+ * @api stable
+ */
+ol.geom.MultiLineString.prototype.getCoordinates = function() {
+  return ol.geom.flat.inflate.coordinatess(
+      this.flatCoordinates, 0, this.ends_, this.stride);
+};
+
+
+/**
+ * @return {Array.<number>} Ends.
+ */
+ol.geom.MultiLineString.prototype.getEnds = function() {
+  return this.ends_;
+};
+
+
+/**
+ * Return the linestring at the specified index.
+ * @param {number} index Index.
+ * @return {ol.geom.LineString} LineString.
+ * @api stable
+ */
+ol.geom.MultiLineString.prototype.getLineString = function(index) {
+  goog.asserts.assert(0 <= index && index < this.ends_.length,
+      'index should be in between 0 and length of the this.ends_ array');
+  if (index < 0 || this.ends_.length <= index) {
+    return null;
+  }
+  var lineString = new ol.geom.LineString(null);
+  lineString.setFlatCoordinates(this.layout, this.flatCoordinates.slice(
+      index === 0 ? 0 : this.ends_[index - 1], this.ends_[index]));
+  return lineString;
+};
+
+
+/**
+ * Return the linestrings of this multilinestring.
+ * @return {Array.<ol.geom.LineString>} LineStrings.
+ * @api stable
+ */
+ol.geom.MultiLineString.prototype.getLineStrings = function() {
+  var flatCoordinates = this.flatCoordinates;
+  var ends = this.ends_;
+  var layout = this.layout;
+  /** @type {Array.<ol.geom.LineString>} */
+  var lineStrings = [];
+  var offset = 0;
+  var i, ii;
+  for (i = 0, ii = ends.length; i < ii; ++i) {
+    var end = ends[i];
+    var lineString = new ol.geom.LineString(null);
+    lineString.setFlatCoordinates(layout, flatCoordinates.slice(offset, end));
+    lineStrings.push(lineString);
+    offset = end;
+  }
+  return lineStrings;
+};
+
+
+/**
+ * @return {Array.<number>} Flat midpoints.
+ */
+ol.geom.MultiLineString.prototype.getFlatMidpoints = function() {
+  var midpoints = [];
+  var flatCoordinates = this.flatCoordinates;
+  var offset = 0;
+  var ends = this.ends_;
+  var stride = this.stride;
+  var i, ii;
+  for (i = 0, ii = ends.length; i < ii; ++i) {
+    var end = ends[i];
+    var midpoint = ol.geom.flat.interpolate.lineString(
+        flatCoordinates, offset, end, stride, 0.5);
+    ol.array.extend(midpoints, midpoint);
+    offset = end;
+  }
+  return midpoints;
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.geom.MultiLineString.prototype.getSimplifiedGeometryInternal = function(squaredTolerance) {
+  var simplifiedFlatCoordinates = [];
+  var simplifiedEnds = [];
+  simplifiedFlatCoordinates.length = ol.geom.flat.simplify.douglasPeuckers(
+      this.flatCoordinates, 0, this.ends_, this.stride, squaredTolerance,
+      simplifiedFlatCoordinates, 0, simplifiedEnds);
+  var simplifiedMultiLineString = new ol.geom.MultiLineString(null);
+  simplifiedMultiLineString.setFlatCoordinates(
+      ol.geom.GeometryLayout.XY, simplifiedFlatCoordinates, simplifiedEnds);
+  return simplifiedMultiLineString;
+};
+
+
+/**
+ * @inheritDoc
+ * @api stable
+ */
+ol.geom.MultiLineString.prototype.getType = function() {
+  return ol.geom.GeometryType.MULTI_LINE_STRING;
+};
+
+
+/**
+ * @inheritDoc
+ * @api stable
+ */
+ol.geom.MultiLineString.prototype.intersectsExtent = function(extent) {
+  return ol.geom.flat.intersectsextent.lineStrings(
+      this.flatCoordinates, 0, this.ends_, this.stride, extent);
+};
+
+
+/**
+ * Set the coordinates of the multilinestring.
+ * @param {Array.<Array.<ol.Coordinate>>} coordinates Coordinates.
+ * @param {ol.geom.GeometryLayout=} opt_layout Layout.
+ * @api stable
+ */
+ol.geom.MultiLineString.prototype.setCoordinates = function(coordinates, opt_layout) {
+  if (!coordinates) {
+    this.setFlatCoordinates(ol.geom.GeometryLayout.XY, null, this.ends_);
+  } else {
+    this.setLayout(opt_layout, coordinates, 2);
+    if (!this.flatCoordinates) {
+      this.flatCoordinates = [];
+    }
+    var ends = ol.geom.flat.deflate.coordinatess(
+        this.flatCoordinates, 0, coordinates, this.stride, this.ends_);
+    this.flatCoordinates.length = ends.length === 0 ? 0 : ends[ends.length - 1];
+    this.changed();
+  }
+};
+
+
+/**
+ * @param {ol.geom.GeometryLayout} layout Layout.
+ * @param {Array.<number>} flatCoordinates Flat coordinates.
+ * @param {Array.<number>} ends Ends.
+ */
+ol.geom.MultiLineString.prototype.setFlatCoordinates = function(layout, flatCoordinates, ends) {
+  if (!flatCoordinates) {
+    goog.asserts.assert(ends && ends.length === 0,
+        'ends must be truthy and ends.length should be 0');
+  } else if (ends.length === 0) {
+    goog.asserts.assert(flatCoordinates.length === 0,
+        'flatCoordinates should be an empty array');
+  } else {
+    goog.asserts.assert(flatCoordinates.length == ends[ends.length - 1],
+        'length of flatCoordinates array should match the last value of ends');
+  }
+  this.setFlatCoordinatesInternal(layout, flatCoordinates);
+  this.ends_ = ends;
+  this.changed();
+};
+
+
+/**
+ * @param {Array.<ol.geom.LineString>} lineStrings LineStrings.
+ */
+ol.geom.MultiLineString.prototype.setLineStrings = function(lineStrings) {
+  var layout = this.getLayout();
+  var flatCoordinates = [];
+  var ends = [];
+  var i, ii;
+  for (i = 0, ii = lineStrings.length; i < ii; ++i) {
+    var lineString = lineStrings[i];
+    if (i === 0) {
+      layout = lineString.getLayout();
+    } else {
+      // FIXME better handle the case of non-matching layouts
+      goog.asserts.assert(lineString.getLayout() == layout,
+          'layout of lineString should match layout');
+    }
+    ol.array.extend(flatCoordinates, lineString.getFlatCoordinates());
+    ends.push(flatCoordinates.length);
+  }
+  this.setFlatCoordinates(layout, flatCoordinates, ends);
+};
+
+goog.provide('ol.geom.MultiPoint');
+
+goog.require('goog.asserts');
+goog.require('ol.array');
+goog.require('ol.extent');
+goog.require('ol.geom.GeometryLayout');
+goog.require('ol.geom.GeometryType');
+goog.require('ol.geom.Point');
+goog.require('ol.geom.SimpleGeometry');
+goog.require('ol.geom.flat.deflate');
+goog.require('ol.geom.flat.inflate');
+goog.require('ol.math');
+
+
+/**
+ * @classdesc
+ * Multi-point geometry.
+ *
+ * @constructor
+ * @extends {ol.geom.SimpleGeometry}
+ * @param {Array.<ol.Coordinate>} coordinates Coordinates.
+ * @param {ol.geom.GeometryLayout=} opt_layout Layout.
+ * @api stable
+ */
+ol.geom.MultiPoint = function(coordinates, opt_layout) {
+  ol.geom.SimpleGeometry.call(this);
+  this.setCoordinates(coordinates, opt_layout);
+};
+ol.inherits(ol.geom.MultiPoint, ol.geom.SimpleGeometry);
+
+
+/**
+ * Append the passed point to this multipoint.
+ * @param {ol.geom.Point} point Point.
+ * @api stable
+ */
+ol.geom.MultiPoint.prototype.appendPoint = function(point) {
+  goog.asserts.assert(point.getLayout() == this.layout,
+      'the layout of point should match layout');
+  if (!this.flatCoordinates) {
+    this.flatCoordinates = point.getFlatCoordinates().slice();
+  } else {
+    ol.array.extend(this.flatCoordinates, point.getFlatCoordinates());
+  }
+  this.changed();
+};
+
+
+/**
+ * Make a complete copy of the geometry.
+ * @return {!ol.geom.MultiPoint} Clone.
+ * @api stable
+ */
+ol.geom.MultiPoint.prototype.clone = function() {
+  var multiPoint = new ol.geom.MultiPoint(null);
+  multiPoint.setFlatCoordinates(this.layout, this.flatCoordinates.slice());
+  return multiPoint;
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.geom.MultiPoint.prototype.closestPointXY = function(x, y, closestPoint, minSquaredDistance) {
+  if (minSquaredDistance <
+      ol.extent.closestSquaredDistanceXY(this.getExtent(), x, y)) {
+    return minSquaredDistance;
+  }
+  var flatCoordinates = this.flatCoordinates;
+  var stride = this.stride;
+  var i, ii, j;
+  for (i = 0, ii = flatCoordinates.length; i < ii; i += stride) {
+    var squaredDistance = ol.math.squaredDistance(
+        x, y, flatCoordinates[i], flatCoordinates[i + 1]);
+    if (squaredDistance < minSquaredDistance) {
+      minSquaredDistance = squaredDistance;
+      for (j = 0; j < stride; ++j) {
+        closestPoint[j] = flatCoordinates[i + j];
+      }
+      closestPoint.length = stride;
+    }
+  }
+  return minSquaredDistance;
+};
+
+
+/**
+ * Return the coordinates of the multipoint.
+ * @return {Array.<ol.Coordinate>} Coordinates.
+ * @api stable
+ */
+ol.geom.MultiPoint.prototype.getCoordinates = function() {
+  return ol.geom.flat.inflate.coordinates(
+      this.flatCoordinates, 0, this.flatCoordinates.length, this.stride);
+};
+
+
+/**
+ * Return the point at the specified index.
+ * @param {number} index Index.
+ * @return {ol.geom.Point} Point.
+ * @api stable
+ */
+ol.geom.MultiPoint.prototype.getPoint = function(index) {
+  var n = !this.flatCoordinates ?
+      0 : this.flatCoordinates.length / this.stride;
+  goog.asserts.assert(0 <= index && index < n,
+      'index should be in between 0 and n');
+  if (index < 0 || n <= index) {
+    return null;
+  }
+  var point = new ol.geom.Point(null);
+  point.setFlatCoordinates(this.layout, this.flatCoordinates.slice(
+      index * this.stride, (index + 1) * this.stride));
+  return point;
+};
+
+
+/**
+ * Return the points of this multipoint.
+ * @return {Array.<ol.geom.Point>} Points.
+ * @api stable
+ */
+ol.geom.MultiPoint.prototype.getPoints = function() {
+  var flatCoordinates = this.flatCoordinates;
+  var layout = this.layout;
+  var stride = this.stride;
+  /** @type {Array.<ol.geom.Point>} */
+  var points = [];
+  var i, ii;
+  for (i = 0, ii = flatCoordinates.length; i < ii; i += stride) {
+    var point = new ol.geom.Point(null);
+    point.setFlatCoordinates(layout, flatCoordinates.slice(i, i + stride));
+    points.push(point);
+  }
+  return points;
+};
+
+
+/**
+ * @inheritDoc
+ * @api stable
+ */
+ol.geom.MultiPoint.prototype.getType = function() {
+  return ol.geom.GeometryType.MULTI_POINT;
+};
+
+
+/**
+ * @inheritDoc
+ * @api stable
+ */
+ol.geom.MultiPoint.prototype.intersectsExtent = function(extent) {
+  var flatCoordinates = this.flatCoordinates;
+  var stride = this.stride;
+  var i, ii, x, y;
+  for (i = 0, ii = flatCoordinates.length; i < ii; i += stride) {
+    x = flatCoordinates[i];
+    y = flatCoordinates[i + 1];
+    if (ol.extent.containsXY(extent, x, y)) {
+      return true;
+    }
+  }
+  return false;
+};
+
+
+/**
+ * Set the coordinates of the multipoint.
+ * @param {Array.<ol.Coordinate>} coordinates Coordinates.
+ * @param {ol.geom.GeometryLayout=} opt_layout Layout.
+ * @api stable
+ */
+ol.geom.MultiPoint.prototype.setCoordinates = function(coordinates, opt_layout) {
+  if (!coordinates) {
+    this.setFlatCoordinates(ol.geom.GeometryLayout.XY, null);
+  } else {
+    this.setLayout(opt_layout, coordinates, 1);
+    if (!this.flatCoordinates) {
+      this.flatCoordinates = [];
+    }
+    this.flatCoordinates.length = ol.geom.flat.deflate.coordinates(
+        this.flatCoordinates, 0, coordinates, this.stride);
+    this.changed();
+  }
+};
+
+
+/**
+ * @param {ol.geom.GeometryLayout} layout Layout.
+ * @param {Array.<number>} flatCoordinates Flat coordinates.
+ */
+ol.geom.MultiPoint.prototype.setFlatCoordinates = function(layout, flatCoordinates) {
+  this.setFlatCoordinatesInternal(layout, flatCoordinates);
+  this.changed();
+};
+
+goog.provide('ol.geom.flat.center');
+
+goog.require('ol.extent');
+
+
+/**
+ * @param {Array.<number>} flatCoordinates Flat coordinates.
+ * @param {number} offset Offset.
+ * @param {Array.<Array.<number>>} endss Endss.
+ * @param {number} stride Stride.
+ * @return {Array.<number>} Flat centers.
+ */
+ol.geom.flat.center.linearRingss = function(flatCoordinates, offset, endss, stride) {
+  var flatCenters = [];
+  var i, ii;
+  var extent = ol.extent.createEmpty();
+  for (i = 0, ii = endss.length; i < ii; ++i) {
+    var ends = endss[i];
+    extent = ol.extent.createOrUpdateFromFlatCoordinates(
+        flatCoordinates, offset, ends[0], stride);
+    flatCenters.push((extent[0] + extent[2]) / 2, (extent[1] + extent[3]) / 2);
+    offset = ends[ends.length - 1];
+  }
+  return flatCenters;
+};
+
+goog.provide('ol.geom.MultiPolygon');
+
+goog.require('goog.asserts');
+goog.require('ol');
+goog.require('ol.array');
+goog.require('ol.extent');
+goog.require('ol.geom.GeometryLayout');
+goog.require('ol.geom.GeometryType');
+goog.require('ol.geom.MultiPoint');
+goog.require('ol.geom.Polygon');
+goog.require('ol.geom.SimpleGeometry');
+goog.require('ol.geom.flat.area');
+goog.require('ol.geom.flat.center');
+goog.require('ol.geom.flat.closest');
+goog.require('ol.geom.flat.contains');
+goog.require('ol.geom.flat.deflate');
+goog.require('ol.geom.flat.inflate');
+goog.require('ol.geom.flat.interiorpoint');
+goog.require('ol.geom.flat.intersectsextent');
+goog.require('ol.geom.flat.orient');
+goog.require('ol.geom.flat.simplify');
+
+
+/**
+ * @classdesc
+ * Multi-polygon geometry.
+ *
+ * @constructor
+ * @extends {ol.geom.SimpleGeometry}
+ * @param {Array.<Array.<Array.<ol.Coordinate>>>} coordinates Coordinates.
+ * @param {ol.geom.GeometryLayout=} opt_layout Layout.
+ * @api stable
+ */
+ol.geom.MultiPolygon = function(coordinates, opt_layout) {
+
+  ol.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);
+
+};
+ol.inherits(ol.geom.MultiPolygon, ol.geom.SimpleGeometry);
+
+
+/**
+ * Append the passed polygon to this multipolygon.
+ * @param {ol.geom.Polygon} polygon Polygon.
+ * @api stable
+ */
+ol.geom.MultiPolygon.prototype.appendPolygon = function(polygon) {
+  goog.asserts.assert(polygon.getLayout() == this.layout,
+      'layout of polygon should match layout');
+  /** @type {Array.<number>} */
+  var ends;
+  if (!this.flatCoordinates) {
+    this.flatCoordinates = polygon.getFlatCoordinates().slice();
+    ends = polygon.getEnds().slice();
+    this.endss_.push();
+  } else {
+    var offset = this.flatCoordinates.length;
+    ol.array.extend(this.flatCoordinates, polygon.getFlatCoordinates());
+    ends = polygon.getEnds().slice();
+    var i, ii;
+    for (i = 0, ii = ends.length; i < ii; ++i) {
+      ends[i] += offset;
+    }
+  }
+  this.endss_.push(ends);
+  this.changed();
+};
+
+
+/**
+ * Make a complete copy of the geometry.
+ * @return {!ol.geom.MultiPolygon} Clone.
+ * @api stable
+ */
+ol.geom.MultiPolygon.prototype.clone = function() {
+  var multiPolygon = new ol.geom.MultiPolygon(null);
+
+  var len = this.endss_.length;
+  var newEndss = new Array(len);
+  for (var i = 0; i < len; ++i) {
+    newEndss[i] = this.endss_[i].slice();
+  }
+
+  multiPolygon.setFlatCoordinates(
+      this.layout, this.flatCoordinates.slice(), newEndss);
+  return multiPolygon;
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.geom.MultiPolygon.prototype.closestPointXY = function(x, y, closestPoint, minSquaredDistance) {
+  if (minSquaredDistance <
+      ol.extent.closestSquaredDistanceXY(this.getExtent(), x, y)) {
+    return minSquaredDistance;
+  }
+  if (this.maxDeltaRevision_ != this.getRevision()) {
+    this.maxDelta_ = Math.sqrt(ol.geom.flat.closest.getssMaxSquaredDelta(
+        this.flatCoordinates, 0, this.endss_, this.stride, 0));
+    this.maxDeltaRevision_ = this.getRevision();
+  }
+  return ol.geom.flat.closest.getssClosestPoint(
+      this.getOrientedFlatCoordinates(), 0, this.endss_, this.stride,
+      this.maxDelta_, true, x, y, closestPoint, minSquaredDistance);
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.geom.MultiPolygon.prototype.containsXY = function(x, y) {
+  return ol.geom.flat.contains.linearRingssContainsXY(
+      this.getOrientedFlatCoordinates(), 0, this.endss_, this.stride, x, y);
+};
+
+
+/**
+ * Return the area of the multipolygon on projected plane.
+ * @return {number} Area (on projected plane).
+ * @api stable
+ */
+ol.geom.MultiPolygon.prototype.getArea = function() {
+  return ol.geom.flat.area.linearRingss(
+      this.getOrientedFlatCoordinates(), 0, this.endss_, this.stride);
+};
+
+
+/**
+ * Get the coordinate array for this geometry.  This array has the structure
+ * of a GeoJSON coordinate array for multi-polygons.
+ *
+ * @param {boolean=} opt_right Orient coordinates according to the right-hand
+ *     rule (counter-clockwise for exterior and clockwise for interior rings).
+ *     If `false`, coordinates will be oriented according to the left-hand rule
+ *     (clockwise for exterior and counter-clockwise for interior rings).
+ *     By default, coordinate orientation will depend on how the geometry was
+ *     constructed.
+ * @return {Array.<Array.<Array.<ol.Coordinate>>>} Coordinates.
+ * @api stable
+ */
+ol.geom.MultiPolygon.prototype.getCoordinates = function(opt_right) {
+  var flatCoordinates;
+  if (opt_right !== undefined) {
+    flatCoordinates = this.getOrientedFlatCoordinates().slice();
+    ol.geom.flat.orient.orientLinearRingss(
+        flatCoordinates, 0, this.endss_, this.stride, opt_right);
+  } else {
+    flatCoordinates = this.flatCoordinates;
+  }
+
+  return ol.geom.flat.inflate.coordinatesss(
+      flatCoordinates, 0, this.endss_, this.stride);
+};
+
+
+/**
+ * @return {Array.<Array.<number>>} Endss.
+ */
+ol.geom.MultiPolygon.prototype.getEndss = function() {
+  return this.endss_;
+};
+
+
+/**
+ * @return {Array.<number>} Flat interior points.
+ */
+ol.geom.MultiPolygon.prototype.getFlatInteriorPoints = function() {
+  if (this.flatInteriorPointsRevision_ != this.getRevision()) {
+    var flatCenters = ol.geom.flat.center.linearRingss(
+        this.flatCoordinates, 0, this.endss_, this.stride);
+    this.flatInteriorPoints_ = ol.geom.flat.interiorpoint.linearRingss(
+        this.getOrientedFlatCoordinates(), 0, this.endss_, this.stride,
+        flatCenters);
+    this.flatInteriorPointsRevision_ = this.getRevision();
+  }
+  return this.flatInteriorPoints_;
+};
+
+
+/**
+ * Return the interior points as {@link ol.geom.MultiPoint multipoint}.
+ * @return {ol.geom.MultiPoint} Interior points.
+ * @api stable
+ */
+ol.geom.MultiPolygon.prototype.getInteriorPoints = function() {
+  var interiorPoints = new ol.geom.MultiPoint(null);
+  interiorPoints.setFlatCoordinates(ol.geom.GeometryLayout.XY,
+      this.getFlatInteriorPoints().slice());
+  return interiorPoints;
+};
+
+
+/**
+ * @return {Array.<number>} Oriented flat coordinates.
+ */
+ol.geom.MultiPolygon.prototype.getOrientedFlatCoordinates = function() {
+  if (this.orientedRevision_ != this.getRevision()) {
+    var flatCoordinates = this.flatCoordinates;
+    if (ol.geom.flat.orient.linearRingssAreOriented(
+        flatCoordinates, 0, this.endss_, this.stride)) {
+      this.orientedFlatCoordinates_ = flatCoordinates;
+    } else {
+      this.orientedFlatCoordinates_ = flatCoordinates.slice();
+      this.orientedFlatCoordinates_.length =
+          ol.geom.flat.orient.orientLinearRingss(
+              this.orientedFlatCoordinates_, 0, this.endss_, this.stride);
+    }
+    this.orientedRevision_ = this.getRevision();
+  }
+  return this.orientedFlatCoordinates_;
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.geom.MultiPolygon.prototype.getSimplifiedGeometryInternal = function(squaredTolerance) {
+  var simplifiedFlatCoordinates = [];
+  var simplifiedEndss = [];
+  simplifiedFlatCoordinates.length = ol.geom.flat.simplify.quantizess(
+      this.flatCoordinates, 0, this.endss_, this.stride,
+      Math.sqrt(squaredTolerance),
+      simplifiedFlatCoordinates, 0, simplifiedEndss);
+  var simplifiedMultiPolygon = new ol.geom.MultiPolygon(null);
+  simplifiedMultiPolygon.setFlatCoordinates(
+      ol.geom.GeometryLayout.XY, simplifiedFlatCoordinates, simplifiedEndss);
+  return simplifiedMultiPolygon;
+};
+
+
+/**
+ * Return the polygon at the specified index.
+ * @param {number} index Index.
+ * @return {ol.geom.Polygon} Polygon.
+ * @api stable
+ */
+ol.geom.MultiPolygon.prototype.getPolygon = function(index) {
+  goog.asserts.assert(0 <= index && index < this.endss_.length,
+      'index should be in between 0 and the length of this.endss_');
+  if (index < 0 || this.endss_.length <= index) {
+    return null;
+  }
+  var offset;
+  if (index === 0) {
+    offset = 0;
+  } else {
+    var prevEnds = this.endss_[index - 1];
+    offset = prevEnds[prevEnds.length - 1];
+  }
+  var ends = this.endss_[index].slice();
+  var end = ends[ends.length - 1];
+  if (offset !== 0) {
+    var i, ii;
+    for (i = 0, ii = ends.length; i < ii; ++i) {
+      ends[i] -= offset;
+    }
+  }
+  var polygon = new ol.geom.Polygon(null);
+  polygon.setFlatCoordinates(
+      this.layout, this.flatCoordinates.slice(offset, end), ends);
+  return polygon;
+};
+
+
+/**
+ * Return the polygons of this multipolygon.
+ * @return {Array.<ol.geom.Polygon>} Polygons.
+ * @api stable
+ */
+ol.geom.MultiPolygon.prototype.getPolygons = function() {
+  var layout = this.layout;
+  var flatCoordinates = this.flatCoordinates;
+  var endss = this.endss_;
+  var polygons = [];
+  var offset = 0;
+  var i, ii, j, jj;
+  for (i = 0, ii = endss.length; i < ii; ++i) {
+    var ends = endss[i].slice();
+    var end = ends[ends.length - 1];
+    if (offset !== 0) {
+      for (j = 0, jj = ends.length; j < jj; ++j) {
+        ends[j] -= offset;
+      }
+    }
+    var polygon = new ol.geom.Polygon(null);
+    polygon.setFlatCoordinates(
+        layout, flatCoordinates.slice(offset, end), ends);
+    polygons.push(polygon);
+    offset = end;
+  }
+  return polygons;
+};
+
+
+/**
+ * @inheritDoc
+ * @api stable
+ */
+ol.geom.MultiPolygon.prototype.getType = function() {
+  return ol.geom.GeometryType.MULTI_POLYGON;
+};
+
+
+/**
+ * @inheritDoc
+ * @api stable
+ */
+ol.geom.MultiPolygon.prototype.intersectsExtent = function(extent) {
+  return ol.geom.flat.intersectsextent.linearRingss(
+      this.getOrientedFlatCoordinates(), 0, this.endss_, this.stride, extent);
+};
+
+
+/**
+ * Set the coordinates of the multipolygon.
+ * @param {Array.<Array.<Array.<ol.Coordinate>>>} coordinates Coordinates.
+ * @param {ol.geom.GeometryLayout=} opt_layout Layout.
+ * @api stable
+ */
+ol.geom.MultiPolygon.prototype.setCoordinates = function(coordinates, opt_layout) {
+  if (!coordinates) {
+    this.setFlatCoordinates(ol.geom.GeometryLayout.XY, null, this.endss_);
+  } else {
+    this.setLayout(opt_layout, coordinates, 3);
+    if (!this.flatCoordinates) {
+      this.flatCoordinates = [];
+    }
+    var endss = ol.geom.flat.deflate.coordinatesss(
+        this.flatCoordinates, 0, coordinates, this.stride, this.endss_);
+    if (endss.length === 0) {
+      this.flatCoordinates.length = 0;
+    } else {
+      var lastEnds = endss[endss.length - 1];
+      this.flatCoordinates.length = lastEnds.length === 0 ?
+          0 : lastEnds[lastEnds.length - 1];
+    }
+    this.changed();
+  }
+};
+
+
+/**
+ * @param {ol.geom.GeometryLayout} layout Layout.
+ * @param {Array.<number>} flatCoordinates Flat coordinates.
+ * @param {Array.<Array.<number>>} endss Endss.
+ */
+ol.geom.MultiPolygon.prototype.setFlatCoordinates = function(layout, flatCoordinates, endss) {
+  goog.asserts.assert(endss, 'endss must be truthy');
+  if (!flatCoordinates || flatCoordinates.length === 0) {
+    goog.asserts.assert(endss.length === 0, 'the length of endss should be 0');
+  } else {
+    goog.asserts.assert(endss.length > 0, 'endss cannot be an empty array');
+    var ends = endss[endss.length - 1];
+    goog.asserts.assert(flatCoordinates.length == ends[ends.length - 1],
+        'the length of flatCoordinates should be the last value of ends');
+  }
+  this.setFlatCoordinatesInternal(layout, flatCoordinates);
+  this.endss_ = endss;
+  this.changed();
+};
+
+
+/**
+ * @param {Array.<ol.geom.Polygon>} polygons Polygons.
+ */
+ol.geom.MultiPolygon.prototype.setPolygons = function(polygons) {
+  var layout = this.getLayout();
+  var flatCoordinates = [];
+  var endss = [];
+  var i, ii, ends;
+  for (i = 0, ii = polygons.length; i < ii; ++i) {
+    var polygon = polygons[i];
+    if (i === 0) {
+      layout = polygon.getLayout();
+    } else {
+      // FIXME better handle the case of non-matching layouts
+      goog.asserts.assert(polygon.getLayout() == layout,
+          'layout of polygon should be layout');
+    }
+    var offset = flatCoordinates.length;
+    ends = polygon.getEnds();
+    var j, jj;
+    for (j = 0, jj = ends.length; j < jj; ++j) {
+      ends[j] += offset;
+    }
+    ol.array.extend(flatCoordinates, polygon.getFlatCoordinates());
+    endss.push(ends);
+  }
+  this.setFlatCoordinates(layout, flatCoordinates, endss);
+};
+
+goog.provide('ol.format.EsriJSON');
+
+goog.require('goog.asserts');
+goog.require('ol.Feature');
+goog.require('ol.array');
+goog.require('ol.extent');
+goog.require('ol.format.Feature');
+goog.require('ol.format.JSONFeature');
+goog.require('ol.geom.GeometryLayout');
+goog.require('ol.geom.GeometryType');
+goog.require('ol.geom.LineString');
+goog.require('ol.geom.LinearRing');
+goog.require('ol.geom.MultiLineString');
+goog.require('ol.geom.MultiPoint');
+goog.require('ol.geom.MultiPolygon');
+goog.require('ol.geom.Point');
+goog.require('ol.geom.Polygon');
+goog.require('ol.geom.flat.orient');
+goog.require('ol.object');
+goog.require('ol.proj');
+
+
+/**
+ * @classdesc
+ * Feature format for reading and writing data in the EsriJSON format.
+ *
+ * @constructor
+ * @extends {ol.format.JSONFeature}
+ * @param {olx.format.EsriJSONOptions=} opt_options Options.
+ * @api
+ */
+ol.format.EsriJSON = function(opt_options) {
+
+  var options = opt_options ? opt_options : {};
+
+  ol.format.JSONFeature.call(this);
+
+  /**
+   * Name of the geometry attribute for features.
+   * @type {string|undefined}
+   * @private
+   */
+  this.geometryName_ = options.geometryName;
+
+};
+ol.inherits(ol.format.EsriJSON, ol.format.JSONFeature);
+
+
+/**
+ * @param {EsriJSONGeometry} object Object.
+ * @param {olx.format.ReadOptions=} opt_options Read options.
+ * @private
+ * @return {ol.geom.Geometry} Geometry.
+ */
+ol.format.EsriJSON.readGeometry_ = function(object, opt_options) {
+  if (!object) {
+    return null;
+  }
+  var type;
+  if (goog.isNumber(object.x) && goog.isNumber(object.y)) {
+    type = ol.geom.GeometryType.POINT;
+  } else if (object.points) {
+    type = ol.geom.GeometryType.MULTI_POINT;
+  } else if (object.paths) {
+    if (object.paths.length === 1) {
+      type = ol.geom.GeometryType.LINE_STRING;
+    } else {
+      type = ol.geom.GeometryType.MULTI_LINE_STRING;
+    }
+  } else if (object.rings) {
+    var layout = ol.format.EsriJSON.getGeometryLayout_(object);
+    var rings = ol.format.EsriJSON.convertRings_(object.rings, layout);
+    object = /** @type {EsriJSONGeometry} */(ol.object.assign({}, object));
+    if (rings.length === 1) {
+      type = ol.geom.GeometryType.POLYGON;
+      object.rings = rings[0];
+    } else {
+      type = ol.geom.GeometryType.MULTI_POLYGON;
+      object.rings = rings;
+    }
+  }
+  goog.asserts.assert(type, 'geometry type should be defined');
+  var geometryReader = ol.format.EsriJSON.GEOMETRY_READERS_[type];
+  goog.asserts.assert(geometryReader,
+      'geometryReader should be defined');
+  return /** @type {ol.geom.Geometry} */ (
+      ol.format.Feature.transformWithOptions(
+          geometryReader(object), false, opt_options));
+};
+
+
+/**
+ * Determines inner and outer rings.
+ * Checks if any polygons in this array contain any other polygons in this
+ * array. It is used for checking for holes.
+ * Logic inspired by: https://github.com/Esri/terraformer-arcgis-parser
+ * @param {Array.<!Array.<!Array.<number>>>} rings Rings.
+ * @param {ol.geom.GeometryLayout} layout Geometry layout.
+ * @private
+ * @return {Array.<!Array.<!Array.<number>>>} Transoformed rings.
+ */
+ol.format.EsriJSON.convertRings_ = function(rings, layout) {
+  var outerRings = [];
+  var holes = [];
+  var i, ii;
+  for (i = 0, ii = rings.length; i < ii; ++i) {
+    var flatRing = ol.array.flatten(rings[i]);
+    // is this ring an outer ring? is it clockwise?
+    var clockwise = ol.geom.flat.orient.linearRingIsClockwise(flatRing, 0,
+        flatRing.length, layout.length);
+    if (clockwise) {
+      outerRings.push([rings[i]]);
+    } else {
+      holes.push(rings[i]);
+    }
+  }
+  while (holes.length) {
+    var hole = holes.shift();
+    var matched = false;
+    // loop over all outer rings and see if they contain our hole.
+    for (i = outerRings.length - 1; i >= 0; i--) {
+      var outerRing = outerRings[i][0];
+      if (ol.extent.containsExtent(new ol.geom.LinearRing(
+          outerRing).getExtent(),
+          new ol.geom.LinearRing(hole).getExtent())) {
+        // the hole is contained push it into our polygon
+        outerRings[i].push(hole);
+        matched = true;
+        break;
+      }
+    }
+    if (!matched) {
+      // no outer rings contain this hole turn it into and outer
+      // ring (reverse it)
+      outerRings.push([hole.reverse()]);
+    }
+  }
+  return outerRings;
+};
+
+
+/**
+ * @param {EsriJSONGeometry} object Object.
+ * @private
+ * @return {ol.geom.Geometry} Point.
+ */
+ol.format.EsriJSON.readPointGeometry_ = function(object) {
+  goog.asserts.assert(goog.isNumber(object.x), 'object.x should be number');
+  goog.asserts.assert(goog.isNumber(object.y), 'object.y should be number');
+  var point;
+  if (object.m !== undefined && object.z !== undefined) {
+    point = new ol.geom.Point([object.x, object.y, object.z, object.m],
+        ol.geom.GeometryLayout.XYZM);
+  } else if (object.z !== undefined) {
+    point = new ol.geom.Point([object.x, object.y, object.z],
+        ol.geom.GeometryLayout.XYZ);
+  } else if (object.m !== undefined) {
+    point = new ol.geom.Point([object.x, object.y, object.m],
+        ol.geom.GeometryLayout.XYM);
+  } else {
+    point = new ol.geom.Point([object.x, object.y]);
+  }
+  return point;
+};
+
+
+/**
+ * @param {EsriJSONGeometry} object Object.
+ * @private
+ * @return {ol.geom.Geometry} LineString.
+ */
+ol.format.EsriJSON.readLineStringGeometry_ = function(object) {
+  goog.asserts.assert(Array.isArray(object.paths),
+      'object.paths should be an array');
+  goog.asserts.assert(object.paths.length === 1,
+      'object.paths array length should be 1');
+  var layout = ol.format.EsriJSON.getGeometryLayout_(object);
+  return new ol.geom.LineString(object.paths[0], layout);
+};
+
+
+/**
+ * @param {EsriJSONGeometry} object Object.
+ * @private
+ * @return {ol.geom.Geometry} MultiLineString.
+ */
+ol.format.EsriJSON.readMultiLineStringGeometry_ = function(object) {
+  goog.asserts.assert(Array.isArray(object.paths),
+      'object.paths should be an array');
+  goog.asserts.assert(object.paths.length > 1,
+      'object.paths array length should be more than 1');
+  var layout = ol.format.EsriJSON.getGeometryLayout_(object);
+  return new ol.geom.MultiLineString(object.paths, layout);
+};
+
+
+/**
+ * @param {EsriJSONGeometry} object Object.
+ * @private
+ * @return {ol.geom.GeometryLayout} The geometry layout to use.
+ */
+ol.format.EsriJSON.getGeometryLayout_ = function(object) {
+  var layout = ol.geom.GeometryLayout.XY;
+  if (object.hasZ === true && object.hasM === true) {
+    layout = ol.geom.GeometryLayout.XYZM;
+  } else if (object.hasZ === true) {
+    layout = ol.geom.GeometryLayout.XYZ;
+  } else if (object.hasM === true) {
+    layout = ol.geom.GeometryLayout.XYM;
+  }
+  return layout;
+};
+
+
+/**
+ * @param {EsriJSONGeometry} object Object.
+ * @private
+ * @return {ol.geom.Geometry} MultiPoint.
+ */
+ol.format.EsriJSON.readMultiPointGeometry_ = function(object) {
+  goog.asserts.assert(object.points, 'object.points should be defined');
+  var layout = ol.format.EsriJSON.getGeometryLayout_(object);
+  return new ol.geom.MultiPoint(object.points, layout);
+};
+
+
+/**
+ * @param {EsriJSONGeometry} object Object.
+ * @private
+ * @return {ol.geom.Geometry} MultiPolygon.
+ */
+ol.format.EsriJSON.readMultiPolygonGeometry_ = function(object) {
+  goog.asserts.assert(object.rings);
+  goog.asserts.assert(object.rings.length > 1,
+      'object.rings should have length larger than 1');
+  var layout = ol.format.EsriJSON.getGeometryLayout_(object);
+  return new ol.geom.MultiPolygon(
+      /** @type {Array.<Array.<Array.<Array.<number>>>>} */(object.rings),
+      layout);
+};
+
+
+/**
+ * @param {EsriJSONGeometry} object Object.
+ * @private
+ * @return {ol.geom.Geometry} Polygon.
+ */
+ol.format.EsriJSON.readPolygonGeometry_ = function(object) {
+  goog.asserts.assert(object.rings);
+  var layout = ol.format.EsriJSON.getGeometryLayout_(object);
+  return new ol.geom.Polygon(object.rings, layout);
+};
+
+
+/**
+ * @param {ol.geom.Geometry} geometry Geometry.
+ * @param {olx.format.WriteOptions=} opt_options Write options.
+ * @private
+ * @return {EsriJSONGeometry} EsriJSON geometry.
+ */
+ol.format.EsriJSON.writePointGeometry_ = function(geometry, opt_options) {
+  goog.asserts.assertInstanceof(geometry, ol.geom.Point,
+      'geometry should be an ol.geom.Point');
+  var coordinates = geometry.getCoordinates();
+  var layout = geometry.getLayout();
+  if (layout === ol.geom.GeometryLayout.XYZ) {
+    return /** @type {EsriJSONPoint} */ ({
+      x: coordinates[0],
+      y: coordinates[1],
+      z: coordinates[2]
+    });
+  } else if (layout === ol.geom.GeometryLayout.XYM) {
+    return /** @type {EsriJSONPoint} */ ({
+      x: coordinates[0],
+      y: coordinates[1],
+      m: coordinates[2]
+    });
+  } else if (layout === ol.geom.GeometryLayout.XYZM) {
+    return /** @type {EsriJSONPoint} */ ({
+      x: coordinates[0],
+      y: coordinates[1],
+      z: coordinates[2],
+      m: coordinates[3]
+    });
+  } else if (layout === ol.geom.GeometryLayout.XY) {
+    return /** @type {EsriJSONPoint} */ ({
+      x: coordinates[0],
+      y: coordinates[1]
+    });
+  } else {
+    goog.asserts.fail('Unknown geometry layout');
+  }
+};
+
+
+/**
+ * @param {ol.geom.SimpleGeometry} geometry Geometry.
+ * @private
+ * @return {Object} Object with boolean hasZ and hasM keys.
+ */
+ol.format.EsriJSON.getHasZM_ = function(geometry) {
+  var layout = geometry.getLayout();
+  return {
+    hasZ: (layout === ol.geom.GeometryLayout.XYZ ||
+        layout === ol.geom.GeometryLayout.XYZM),
+    hasM: (layout === ol.geom.GeometryLayout.XYM ||
+        layout === ol.geom.GeometryLayout.XYZM)
+  };
+};
+
+
+/**
+ * @param {ol.geom.Geometry} geometry Geometry.
+ * @param {olx.format.WriteOptions=} opt_options Write options.
+ * @private
+ * @return {EsriJSONPolyline} EsriJSON geometry.
+ */
+ol.format.EsriJSON.writeLineStringGeometry_ = function(geometry, opt_options) {
+  goog.asserts.assertInstanceof(geometry, ol.geom.LineString,
+      'geometry should be an ol.geom.LineString');
+  var hasZM = ol.format.EsriJSON.getHasZM_(geometry);
+  return /** @type {EsriJSONPolyline} */ ({
+    hasZ: hasZM.hasZ,
+    hasM: hasZM.hasM,
+    paths: [geometry.getCoordinates()]
+  });
+};
+
+
+/**
+ * @param {ol.geom.Geometry} geometry Geometry.
+ * @param {olx.format.WriteOptions=} opt_options Write options.
+ * @private
+ * @return {EsriJSONPolygon} EsriJSON geometry.
+ */
+ol.format.EsriJSON.writePolygonGeometry_ = function(geometry, opt_options) {
+  goog.asserts.assertInstanceof(geometry, ol.geom.Polygon,
+      'geometry should be an ol.geom.Polygon');
+  // Esri geometries use the left-hand rule
+  var hasZM = ol.format.EsriJSON.getHasZM_(geometry);
+  return /** @type {EsriJSONPolygon} */ ({
+    hasZ: hasZM.hasZ,
+    hasM: hasZM.hasM,
+    rings: geometry.getCoordinates(false)
+  });
+};
+
+
+/**
+ * @param {ol.geom.Geometry} geometry Geometry.
+ * @param {olx.format.WriteOptions=} opt_options Write options.
+ * @private
+ * @return {EsriJSONPolyline} EsriJSON geometry.
+ */
+ol.format.EsriJSON.writeMultiLineStringGeometry_ = function(geometry, opt_options) {
+  goog.asserts.assertInstanceof(geometry, ol.geom.MultiLineString,
+      'geometry should be an ol.geom.MultiLineString');
+  var hasZM = ol.format.EsriJSON.getHasZM_(geometry);
+  return /** @type {EsriJSONPolyline} */ ({
+    hasZ: hasZM.hasZ,
+    hasM: hasZM.hasM,
+    paths: geometry.getCoordinates()
+  });
+};
+
+
+/**
+ * @param {ol.geom.Geometry} geometry Geometry.
+ * @param {olx.format.WriteOptions=} opt_options Write options.
+ * @private
+ * @return {EsriJSONMultipoint} EsriJSON geometry.
+ */
+ol.format.EsriJSON.writeMultiPointGeometry_ = function(geometry, opt_options) {
+  goog.asserts.assertInstanceof(geometry, ol.geom.MultiPoint,
+      'geometry should be an ol.geom.MultiPoint');
+  var hasZM = ol.format.EsriJSON.getHasZM_(geometry);
+  return /** @type {EsriJSONMultipoint} */ ({
+    hasZ: hasZM.hasZ,
+    hasM: hasZM.hasM,
+    points: geometry.getCoordinates()
+  });
+};
+
+
+/**
+ * @param {ol.geom.Geometry} geometry Geometry.
+ * @param {olx.format.WriteOptions=} opt_options Write options.
+ * @private
+ * @return {EsriJSONPolygon} EsriJSON geometry.
+ */
+ol.format.EsriJSON.writeMultiPolygonGeometry_ = function(geometry,
+    opt_options) {
+  goog.asserts.assertInstanceof(geometry, ol.geom.MultiPolygon,
+      'geometry should be an ol.geom.MultiPolygon');
+  var hasZM = ol.format.EsriJSON.getHasZM_(geometry);
+  var coordinates = geometry.getCoordinates(false);
+  var output = [];
+  for (var i = 0; i < coordinates.length; i++) {
+    for (var x = coordinates[i].length - 1; x >= 0; x--) {
+      output.push(coordinates[i][x]);
+    }
+  }
+  return /** @type {EsriJSONPolygon} */ ({
+    hasZ: hasZM.hasZ,
+    hasM: hasZM.hasM,
+    rings: output
+  });
+};
+
+
+/**
+ * @const
+ * @private
+ * @type {Object.<ol.geom.GeometryType, function(EsriJSONGeometry): ol.geom.Geometry>}
+ */
+ol.format.EsriJSON.GEOMETRY_READERS_ = {};
+ol.format.EsriJSON.GEOMETRY_READERS_[ol.geom.GeometryType.POINT] =
+    ol.format.EsriJSON.readPointGeometry_;
+ol.format.EsriJSON.GEOMETRY_READERS_[ol.geom.GeometryType.LINE_STRING] =
+    ol.format.EsriJSON.readLineStringGeometry_;
+ol.format.EsriJSON.GEOMETRY_READERS_[ol.geom.GeometryType.POLYGON] =
+    ol.format.EsriJSON.readPolygonGeometry_;
+ol.format.EsriJSON.GEOMETRY_READERS_[ol.geom.GeometryType.MULTI_POINT] =
+    ol.format.EsriJSON.readMultiPointGeometry_;
+ol.format.EsriJSON.GEOMETRY_READERS_[ol.geom.GeometryType.MULTI_LINE_STRING] =
+    ol.format.EsriJSON.readMultiLineStringGeometry_;
+ol.format.EsriJSON.GEOMETRY_READERS_[ol.geom.GeometryType.MULTI_POLYGON] =
+    ol.format.EsriJSON.readMultiPolygonGeometry_;
+
+
+/**
+ * @const
+ * @private
+ * @type {Object.<string, function(ol.geom.Geometry, olx.format.WriteOptions=): (EsriJSONGeometry)>}
+ */
+ol.format.EsriJSON.GEOMETRY_WRITERS_ = {};
+ol.format.EsriJSON.GEOMETRY_WRITERS_[ol.geom.GeometryType.POINT] =
+    ol.format.EsriJSON.writePointGeometry_;
+ol.format.EsriJSON.GEOMETRY_WRITERS_[ol.geom.GeometryType.LINE_STRING] =
+    ol.format.EsriJSON.writeLineStringGeometry_;
+ol.format.EsriJSON.GEOMETRY_WRITERS_[ol.geom.GeometryType.POLYGON] =
+    ol.format.EsriJSON.writePolygonGeometry_;
+ol.format.EsriJSON.GEOMETRY_WRITERS_[ol.geom.GeometryType.MULTI_POINT] =
+    ol.format.EsriJSON.writeMultiPointGeometry_;
+ol.format.EsriJSON.GEOMETRY_WRITERS_[ol.geom.GeometryType.MULTI_LINE_STRING] =
+    ol.format.EsriJSON.writeMultiLineStringGeometry_;
+ol.format.EsriJSON.GEOMETRY_WRITERS_[ol.geom.GeometryType.MULTI_POLYGON] =
+    ol.format.EsriJSON.writeMultiPolygonGeometry_;
+
+
+/**
+ * Read a feature from a EsriJSON Feature source.  Only works for Feature,
+ * use `readFeatures` to read FeatureCollection source.
+ *
+ * @function
+ * @param {ArrayBuffer|Document|Node|Object|string} source Source.
+ * @param {olx.format.ReadOptions=} opt_options Read options.
+ * @return {ol.Feature} Feature.
+ * @api
+ */
+ol.format.EsriJSON.prototype.readFeature;
+
+
+/**
+ * Read all features from a EsriJSON source.  Works with both Feature and
+ * FeatureCollection sources.
+ *
+ * @function
+ * @param {ArrayBuffer|Document|Node|Object|string} source Source.
+ * @param {olx.format.ReadOptions=} opt_options Read options.
+ * @return {Array.<ol.Feature>} Features.
+ * @api
+ */
+ol.format.EsriJSON.prototype.readFeatures;
+
+
+/**
+ * @inheritDoc
+ */
+ol.format.EsriJSON.prototype.readFeatureFromObject = function(
+    object, opt_options) {
+  var esriJSONFeature = /** @type {EsriJSONFeature} */ (object);
+  goog.asserts.assert(esriJSONFeature.geometry ||
+      esriJSONFeature.attributes,
+      'geometry or attributes should be defined');
+  var geometry = ol.format.EsriJSON.readGeometry_(esriJSONFeature.geometry,
+      opt_options);
+  var feature = new ol.Feature();
+  if (this.geometryName_) {
+    feature.setGeometryName(this.geometryName_);
+  }
+  feature.setGeometry(geometry);
+  if (opt_options && opt_options.idField &&
+      esriJSONFeature.attributes[opt_options.idField]) {
+    goog.asserts.assert(
+        goog.isNumber(esriJSONFeature.attributes[opt_options.idField]),
+        'objectIdFieldName value should be a number');
+    feature.setId(/** @type {number} */(
+        esriJSONFeature.attributes[opt_options.idField]));
+  }
+  if (esriJSONFeature.attributes) {
+    feature.setProperties(esriJSONFeature.attributes);
+  }
+  return feature;
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.format.EsriJSON.prototype.readFeaturesFromObject = function(
+    object, opt_options) {
+  var esriJSONObject = /** @type {EsriJSONObject} */ (object);
+  var options = opt_options ? opt_options : {};
+  if (esriJSONObject.features) {
+    var esriJSONFeatureCollection = /** @type {EsriJSONFeatureCollection} */
+        (object);
+    /** @type {Array.<ol.Feature>} */
+    var features = [];
+    var esriJSONFeatures = esriJSONFeatureCollection.features;
+    var i, ii;
+    options.idField = object.objectIdFieldName;
+    for (i = 0, ii = esriJSONFeatures.length; i < ii; ++i) {
+      features.push(this.readFeatureFromObject(esriJSONFeatures[i],
+          options));
+    }
+    return features;
+  } else {
+    return [this.readFeatureFromObject(object, options)];
+  }
+};
+
+
+/**
+ * Read a geometry from a EsriJSON source.
+ *
+ * @function
+ * @param {ArrayBuffer|Document|Node|Object|string} source Source.
+ * @param {olx.format.ReadOptions=} opt_options Read options.
+ * @return {ol.geom.Geometry} Geometry.
+ * @api
+ */
+ol.format.EsriJSON.prototype.readGeometry;
+
+
+/**
+ * @inheritDoc
+ */
+ol.format.EsriJSON.prototype.readGeometryFromObject = function(
+    object, opt_options) {
+  return ol.format.EsriJSON.readGeometry_(
+      /** @type {EsriJSONGeometry} */ (object), opt_options);
+};
+
+
+/**
+ * Read the projection from a EsriJSON source.
+ *
+ * @function
+ * @param {ArrayBuffer|Document|Node|Object|string} source Source.
+ * @return {ol.proj.Projection} Projection.
+ * @api
+ */
+ol.format.EsriJSON.prototype.readProjection;
+
+
+/**
+ * @inheritDoc
+ */
+ol.format.EsriJSON.prototype.readProjectionFromObject = function(object) {
+  var esriJSONObject = /** @type {EsriJSONObject} */ (object);
+  if (esriJSONObject.spatialReference && esriJSONObject.spatialReference.wkid) {
+    var crs = esriJSONObject.spatialReference.wkid;
+    return ol.proj.get('EPSG:' + crs);
+  } else {
+    return null;
+  }
+};
+
+
+/**
+ * @param {ol.geom.Geometry} geometry Geometry.
+ * @param {olx.format.WriteOptions=} opt_options Write options.
+ * @private
+ * @return {EsriJSONGeometry} EsriJSON geometry.
+ */
+ol.format.EsriJSON.writeGeometry_ = function(geometry, opt_options) {
+  var geometryWriter = ol.format.EsriJSON.GEOMETRY_WRITERS_[geometry.getType()];
+  goog.asserts.assert(geometryWriter, 'geometryWriter should be defined');
+  return geometryWriter(/** @type {ol.geom.Geometry} */ (
+      ol.format.Feature.transformWithOptions(geometry, true, opt_options)),
+      opt_options);
+};
+
+
+/**
+ * Encode a geometry as a EsriJSON string.
+ *
+ * @function
+ * @param {ol.geom.Geometry} geometry Geometry.
+ * @param {olx.format.WriteOptions=} opt_options Write options.
+ * @return {string} EsriJSON.
+ * @api
+ */
+ol.format.EsriJSON.prototype.writeGeometry;
+
+
+/**
+ * Encode a geometry as a EsriJSON object.
+ *
+ * @param {ol.geom.Geometry} geometry Geometry.
+ * @param {olx.format.WriteOptions=} opt_options Write options.
+ * @return {EsriJSONGeometry} Object.
+ * @api
+ */
+ol.format.EsriJSON.prototype.writeGeometryObject = function(geometry,
+    opt_options) {
+  return ol.format.EsriJSON.writeGeometry_(geometry,
+      this.adaptOptions(opt_options));
+};
+
+
+/**
+ * Encode a feature as a EsriJSON Feature string.
+ *
+ * @function
+ * @param {ol.Feature} feature Feature.
+ * @param {olx.format.WriteOptions=} opt_options Write options.
+ * @return {string} EsriJSON.
+ * @api
+ */
+ol.format.EsriJSON.prototype.writeFeature;
+
+
+/**
+ * Encode a feature as a esriJSON Feature object.
+ *
+ * @param {ol.Feature} feature Feature.
+ * @param {olx.format.WriteOptions=} opt_options Write options.
+ * @return {Object} Object.
+ * @api
+ */
+ol.format.EsriJSON.prototype.writeFeatureObject = function(
+    feature, opt_options) {
+  opt_options = this.adaptOptions(opt_options);
+  var object = {};
+  var geometry = feature.getGeometry();
+  if (geometry) {
+    object['geometry'] =
+        ol.format.EsriJSON.writeGeometry_(geometry, opt_options);
+  }
+  var properties = feature.getProperties();
+  delete properties[feature.getGeometryName()];
+  if (!ol.object.isEmpty(properties)) {
+    object['attributes'] = properties;
+  } else {
+    object['attributes'] = {};
+  }
+  if (opt_options && opt_options.featureProjection) {
+    object['spatialReference'] = /** @type {EsriJSONCRS} */({
+      wkid: ol.proj.get(
+          opt_options.featureProjection).getCode().split(':').pop()
+    });
+  }
+  return object;
+};
+
+
+/**
+ * Encode an array of features as EsriJSON.
+ *
+ * @function
+ * @param {Array.<ol.Feature>} features Features.
+ * @param {olx.format.WriteOptions=} opt_options Write options.
+ * @return {string} EsriJSON.
+ * @api
+ */
+ol.format.EsriJSON.prototype.writeFeatures;
+
+
+/**
+ * Encode an array of features as a EsriJSON object.
+ *
+ * @param {Array.<ol.Feature>} features Features.
+ * @param {olx.format.WriteOptions=} opt_options Write options.
+ * @return {Object} EsriJSON Object.
+ * @api
+ */
+ol.format.EsriJSON.prototype.writeFeaturesObject = function(features, opt_options) {
+  opt_options = this.adaptOptions(opt_options);
+  var objects = [];
+  var i, ii;
+  for (i = 0, ii = features.length; i < ii; ++i) {
+    objects.push(this.writeFeatureObject(features[i], opt_options));
+  }
+  return /** @type {EsriJSONFeatureCollection} */ ({
+    'features': objects
+  });
+};
+
+goog.provide('ol.geom.GeometryCollection');
+
+goog.require('ol.events');
+goog.require('ol.events.EventType');
+goog.require('ol.extent');
+goog.require('ol.geom.Geometry');
+goog.require('ol.geom.GeometryType');
+goog.require('ol.object');
+
+
+/**
+ * @classdesc
+ * An array of {@link ol.geom.Geometry} objects.
+ *
+ * @constructor
+ * @extends {ol.geom.Geometry}
+ * @param {Array.<ol.geom.Geometry>=} opt_geometries Geometries.
+ * @api stable
+ */
+ol.geom.GeometryCollection = function(opt_geometries) {
+
+  ol.geom.Geometry.call(this);
+
+  /**
+   * @private
+   * @type {Array.<ol.geom.Geometry>}
+   */
+  this.geometries_ = opt_geometries ? opt_geometries : null;
+
+  this.listenGeometriesChange_();
+};
+ol.inherits(ol.geom.GeometryCollection, ol.geom.Geometry);
+
+
+/**
+ * @param {Array.<ol.geom.Geometry>} geometries Geometries.
+ * @private
+ * @return {Array.<ol.geom.Geometry>} Cloned geometries.
+ */
+ol.geom.GeometryCollection.cloneGeometries_ = function(geometries) {
+  var clonedGeometries = [];
+  var i, ii;
+  for (i = 0, ii = geometries.length; i < ii; ++i) {
+    clonedGeometries.push(geometries[i].clone());
+  }
+  return clonedGeometries;
+};
+
+
+/**
+ * @private
+ */
+ol.geom.GeometryCollection.prototype.unlistenGeometriesChange_ = function() {
+  var i, ii;
+  if (!this.geometries_) {
+    return;
+  }
+  for (i = 0, ii = this.geometries_.length; i < ii; ++i) {
+    ol.events.unlisten(
+        this.geometries_[i], ol.events.EventType.CHANGE,
+        this.changed, this);
+  }
+};
+
+
+/**
+ * @private
+ */
+ol.geom.GeometryCollection.prototype.listenGeometriesChange_ = function() {
+  var i, ii;
+  if (!this.geometries_) {
+    return;
+  }
+  for (i = 0, ii = this.geometries_.length; i < ii; ++i) {
+    ol.events.listen(
+        this.geometries_[i], ol.events.EventType.CHANGE,
+        this.changed, this);
+  }
+};
+
+
+/**
+ * Make a complete copy of the geometry.
+ * @return {!ol.geom.GeometryCollection} Clone.
+ * @api stable
+ */
+ol.geom.GeometryCollection.prototype.clone = function() {
+  var geometryCollection = new ol.geom.GeometryCollection(null);
+  geometryCollection.setGeometries(this.geometries_);
+  return geometryCollection;
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.geom.GeometryCollection.prototype.closestPointXY = function(x, y, closestPoint, minSquaredDistance) {
+  if (minSquaredDistance <
+      ol.extent.closestSquaredDistanceXY(this.getExtent(), x, y)) {
+    return minSquaredDistance;
+  }
+  var geometries = this.geometries_;
+  var i, ii;
+  for (i = 0, ii = geometries.length; i < ii; ++i) {
+    minSquaredDistance = geometries[i].closestPointXY(
+        x, y, closestPoint, minSquaredDistance);
+  }
+  return minSquaredDistance;
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.geom.GeometryCollection.prototype.containsXY = function(x, y) {
+  var geometries = this.geometries_;
+  var i, ii;
+  for (i = 0, ii = geometries.length; i < ii; ++i) {
+    if (geometries[i].containsXY(x, y)) {
+      return true;
+    }
+  }
+  return false;
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.geom.GeometryCollection.prototype.computeExtent = function(extent) {
+  ol.extent.createOrUpdateEmpty(extent);
+  var geometries = this.geometries_;
+  for (var i = 0, ii = geometries.length; i < ii; ++i) {
+    ol.extent.extend(extent, geometries[i].getExtent());
+  }
+  return extent;
+};
+
+
+/**
+ * Return the geometries that make up this geometry collection.
+ * @return {Array.<ol.geom.Geometry>} Geometries.
+ * @api stable
+ */
+ol.geom.GeometryCollection.prototype.getGeometries = function() {
+  return ol.geom.GeometryCollection.cloneGeometries_(this.geometries_);
+};
+
+
+/**
+ * @return {Array.<ol.geom.Geometry>} Geometries.
+ */
+ol.geom.GeometryCollection.prototype.getGeometriesArray = function() {
+  return this.geometries_;
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.geom.GeometryCollection.prototype.getSimplifiedGeometry = function(squaredTolerance) {
+  if (this.simplifiedGeometryRevision != this.getRevision()) {
+    ol.object.clear(this.simplifiedGeometryCache);
+    this.simplifiedGeometryMaxMinSquaredTolerance = 0;
+    this.simplifiedGeometryRevision = this.getRevision();
+  }
+  if (squaredTolerance < 0 ||
+      (this.simplifiedGeometryMaxMinSquaredTolerance !== 0 &&
+       squaredTolerance < this.simplifiedGeometryMaxMinSquaredTolerance)) {
+    return this;
+  }
+  var key = squaredTolerance.toString();
+  if (this.simplifiedGeometryCache.hasOwnProperty(key)) {
+    return this.simplifiedGeometryCache[key];
+  } else {
+    var simplifiedGeometries = [];
+    var geometries = this.geometries_;
+    var simplified = false;
+    var i, ii;
+    for (i = 0, ii = geometries.length; i < ii; ++i) {
+      var geometry = geometries[i];
+      var simplifiedGeometry = geometry.getSimplifiedGeometry(squaredTolerance);
+      simplifiedGeometries.push(simplifiedGeometry);
+      if (simplifiedGeometry !== geometry) {
+        simplified = true;
+      }
+    }
+    if (simplified) {
+      var simplifiedGeometryCollection = new ol.geom.GeometryCollection(null);
+      simplifiedGeometryCollection.setGeometriesArray(simplifiedGeometries);
+      this.simplifiedGeometryCache[key] = simplifiedGeometryCollection;
+      return simplifiedGeometryCollection;
+    } else {
+      this.simplifiedGeometryMaxMinSquaredTolerance = squaredTolerance;
+      return this;
+    }
+  }
+};
+
+
+/**
+ * @inheritDoc
+ * @api stable
+ */
+ol.geom.GeometryCollection.prototype.getType = function() {
+  return ol.geom.GeometryType.GEOMETRY_COLLECTION;
+};
+
+
+/**
+ * @inheritDoc
+ * @api stable
+ */
+ol.geom.GeometryCollection.prototype.intersectsExtent = function(extent) {
+  var geometries = this.geometries_;
+  var i, ii;
+  for (i = 0, ii = geometries.length; i < ii; ++i) {
+    if (geometries[i].intersectsExtent(extent)) {
+      return true;
+    }
+  }
+  return false;
+};
+
+
+/**
+ * @return {boolean} Is empty.
+ */
+ol.geom.GeometryCollection.prototype.isEmpty = function() {
+  return this.geometries_.length === 0;
+};
+
+
+/**
+ * @inheritDoc
+ * @api
+ */
+ol.geom.GeometryCollection.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();
+};
+
+
+/**
+ * Set the geometries that make up this geometry collection.
+ * @param {Array.<ol.geom.Geometry>} geometries Geometries.
+ * @api stable
+ */
+ol.geom.GeometryCollection.prototype.setGeometries = function(geometries) {
+  this.setGeometriesArray(
+      ol.geom.GeometryCollection.cloneGeometries_(geometries));
+};
+
+
+/**
+ * @param {Array.<ol.geom.Geometry>} geometries Geometries.
+ */
+ol.geom.GeometryCollection.prototype.setGeometriesArray = function(geometries) {
+  this.unlistenGeometriesChange_();
+  this.geometries_ = geometries;
+  this.listenGeometriesChange_();
+  this.changed();
+};
+
+
+/**
+ * @inheritDoc
+ * @api stable
+ */
+ol.geom.GeometryCollection.prototype.applyTransform = function(transformFn) {
+  var geometries = this.geometries_;
+  var i, ii;
+  for (i = 0, ii = geometries.length; i < ii; ++i) {
+    geometries[i].applyTransform(transformFn);
+  }
+  this.changed();
+};
+
+
+/**
+ * Translate the geometry.
+ * @param {number} deltaX Delta X.
+ * @param {number} deltaY Delta Y.
+ * @api
+ */
+ol.geom.GeometryCollection.prototype.translate = function(deltaX, deltaY) {
+  var geometries = this.geometries_;
+  var i, ii;
+  for (i = 0, ii = geometries.length; i < ii; ++i) {
+    geometries[i].translate(deltaX, deltaY);
+  }
+  this.changed();
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.geom.GeometryCollection.prototype.disposeInternal = function() {
+  this.unlistenGeometriesChange_();
+  ol.geom.Geometry.prototype.disposeInternal.call(this);
+};
+
+// TODO: serialize dataProjection as crs member when writing
+// see https://github.com/openlayers/ol3/issues/2078
+
+goog.provide('ol.format.GeoJSON');
+
+goog.require('goog.asserts');
+goog.require('ol.Feature');
+goog.require('ol.format.Feature');
+goog.require('ol.format.JSONFeature');
+goog.require('ol.geom.GeometryCollection');
+goog.require('ol.geom.LineString');
+goog.require('ol.geom.MultiLineString');
+goog.require('ol.geom.MultiPoint');
+goog.require('ol.geom.MultiPolygon');
+goog.require('ol.geom.Point');
+goog.require('ol.geom.Polygon');
+goog.require('ol.object');
+goog.require('ol.proj');
+
+
+/**
+ * @classdesc
+ * Feature format for reading and writing data in the GeoJSON format.
+ *
+ * @constructor
+ * @extends {ol.format.JSONFeature}
+ * @param {olx.format.GeoJSONOptions=} opt_options Options.
+ * @api stable
+ */
+ol.format.GeoJSON = function(opt_options) {
+
+  var options = opt_options ? opt_options : {};
+
+  ol.format.JSONFeature.call(this);
+
+  /**
+   * @inheritDoc
+   */
+  this.defaultDataProjection = ol.proj.get(
+      options.defaultDataProjection ?
+          options.defaultDataProjection : 'EPSG:4326');
+
+
+  /**
+   * Name of the geometry attribute for features.
+   * @type {string|undefined}
+   * @private
+   */
+  this.geometryName_ = options.geometryName;
+
+};
+ol.inherits(ol.format.GeoJSON, ol.format.JSONFeature);
+
+
+/**
+ * @const
+ * @type {Array.<string>}
+ * @private
+ */
+ol.format.GeoJSON.EXTENSIONS_ = ['.geojson'];
+
+
+/**
+ * @param {GeoJSONGeometry|GeoJSONGeometryCollection} object Object.
+ * @param {olx.format.ReadOptions=} opt_options Read options.
+ * @private
+ * @return {ol.geom.Geometry} Geometry.
+ */
+ol.format.GeoJSON.readGeometry_ = function(object, opt_options) {
+  if (!object) {
+    return null;
+  }
+  var geometryReader = ol.format.GeoJSON.GEOMETRY_READERS_[object.type];
+  goog.asserts.assert(geometryReader, 'geometryReader should be defined');
+  return /** @type {ol.geom.Geometry} */ (
+      ol.format.Feature.transformWithOptions(
+          geometryReader(object), false, opt_options));
+};
+
+
+/**
+ * @param {GeoJSONGeometryCollection} object Object.
+ * @param {olx.format.ReadOptions=} opt_options Read options.
+ * @private
+ * @return {ol.geom.GeometryCollection} Geometry collection.
+ */
+ol.format.GeoJSON.readGeometryCollectionGeometry_ = function(
+    object, opt_options) {
+  goog.asserts.assert(object.type == 'GeometryCollection',
+      'object.type should be GeometryCollection');
+  var geometries = object.geometries.map(
+      /**
+       * @param {GeoJSONGeometry} geometry Geometry.
+       * @return {ol.geom.Geometry} geometry Geometry.
+       */
+      function(geometry) {
+        return ol.format.GeoJSON.readGeometry_(geometry, opt_options);
+      });
+  return new ol.geom.GeometryCollection(geometries);
+};
+
+
+/**
+ * @param {GeoJSONGeometry} object Object.
+ * @private
+ * @return {ol.geom.Point} Point.
+ */
+ol.format.GeoJSON.readPointGeometry_ = function(object) {
+  goog.asserts.assert(object.type == 'Point',
+      'object.type should be Point');
+  return new ol.geom.Point(object.coordinates);
+};
+
+
+/**
+ * @param {GeoJSONGeometry} object Object.
+ * @private
+ * @return {ol.geom.LineString} LineString.
+ */
+ol.format.GeoJSON.readLineStringGeometry_ = function(object) {
+  goog.asserts.assert(object.type == 'LineString',
+      'object.type should be LineString');
+  return new ol.geom.LineString(object.coordinates);
+};
+
+
+/**
+ * @param {GeoJSONGeometry} object Object.
+ * @private
+ * @return {ol.geom.MultiLineString} MultiLineString.
+ */
+ol.format.GeoJSON.readMultiLineStringGeometry_ = function(object) {
+  goog.asserts.assert(object.type == 'MultiLineString',
+      'object.type should be MultiLineString');
+  return new ol.geom.MultiLineString(object.coordinates);
+};
+
+
+/**
+ * @param {GeoJSONGeometry} object Object.
+ * @private
+ * @return {ol.geom.MultiPoint} MultiPoint.
+ */
+ol.format.GeoJSON.readMultiPointGeometry_ = function(object) {
+  goog.asserts.assert(object.type == 'MultiPoint',
+      'object.type should be MultiPoint');
+  return new ol.geom.MultiPoint(object.coordinates);
+};
+
+
+/**
+ * @param {GeoJSONGeometry} object Object.
+ * @private
+ * @return {ol.geom.MultiPolygon} MultiPolygon.
+ */
+ol.format.GeoJSON.readMultiPolygonGeometry_ = function(object) {
+  goog.asserts.assert(object.type == 'MultiPolygon',
+      'object.type should be MultiPolygon');
+  return new ol.geom.MultiPolygon(object.coordinates);
+};
+
+
+/**
+ * @param {GeoJSONGeometry} object Object.
+ * @private
+ * @return {ol.geom.Polygon} Polygon.
+ */
+ol.format.GeoJSON.readPolygonGeometry_ = function(object) {
+  goog.asserts.assert(object.type == 'Polygon',
+      'object.type should be Polygon');
+  return new ol.geom.Polygon(object.coordinates);
+};
+
+
+/**
+ * @param {ol.geom.Geometry} geometry Geometry.
+ * @param {olx.format.WriteOptions=} opt_options Write options.
+ * @private
+ * @return {GeoJSONGeometry|GeoJSONGeometryCollection} GeoJSON geometry.
+ */
+ol.format.GeoJSON.writeGeometry_ = function(geometry, opt_options) {
+  var geometryWriter = ol.format.GeoJSON.GEOMETRY_WRITERS_[geometry.getType()];
+  goog.asserts.assert(geometryWriter, 'geometryWriter should be defined');
+  return geometryWriter(/** @type {ol.geom.Geometry} */ (
+      ol.format.Feature.transformWithOptions(geometry, true, opt_options)),
+      opt_options);
+};
+
+
+/**
+ * @param {ol.geom.Geometry} geometry Geometry.
+ * @private
+ * @return {GeoJSONGeometryCollection} Empty GeoJSON geometry collection.
+ */
+ol.format.GeoJSON.writeEmptyGeometryCollectionGeometry_ = function(geometry) {
+  return /** @type {GeoJSONGeometryCollection} */ ({
+    type: 'GeometryCollection',
+    geometries: []
+  });
+};
+
+
+/**
+ * @param {ol.geom.Geometry} geometry Geometry.
+ * @param {olx.format.WriteOptions=} opt_options Write options.
+ * @private
+ * @return {GeoJSONGeometryCollection} GeoJSON geometry collection.
+ */
+ol.format.GeoJSON.writeGeometryCollectionGeometry_ = function(
+    geometry, opt_options) {
+  goog.asserts.assertInstanceof(geometry, ol.geom.GeometryCollection,
+      'geometry should be an ol.geom.GeometryCollection');
+  var geometries = geometry.getGeometriesArray().map(function(geometry) {
+    var options = ol.object.assign({}, opt_options);
+    delete options.featureProjection;
+    return ol.format.GeoJSON.writeGeometry_(geometry, options);
+  });
+  return /** @type {GeoJSONGeometryCollection} */ ({
+    type: 'GeometryCollection',
+    geometries: geometries
+  });
+};
+
+
+/**
+ * @param {ol.geom.Geometry} geometry Geometry.
+ * @param {olx.format.WriteOptions=} opt_options Write options.
+ * @private
+ * @return {GeoJSONGeometry} GeoJSON geometry.
+ */
+ol.format.GeoJSON.writeLineStringGeometry_ = function(geometry, opt_options) {
+  goog.asserts.assertInstanceof(geometry, ol.geom.LineString,
+      'geometry should be an ol.geom.LineString');
+  return /** @type {GeoJSONGeometry} */ ({
+    type: 'LineString',
+    coordinates: geometry.getCoordinates()
+  });
+};
+
+
+/**
+ * @param {ol.geom.Geometry} geometry Geometry.
+ * @param {olx.format.WriteOptions=} opt_options Write options.
+ * @private
+ * @return {GeoJSONGeometry} GeoJSON geometry.
+ */
+ol.format.GeoJSON.writeMultiLineStringGeometry_ = function(geometry, opt_options) {
+  goog.asserts.assertInstanceof(geometry, ol.geom.MultiLineString,
+      'geometry should be an ol.geom.MultiLineString');
+  return /** @type {GeoJSONGeometry} */ ({
+    type: 'MultiLineString',
+    coordinates: geometry.getCoordinates()
+  });
+};
+
+
+/**
+ * @param {ol.geom.Geometry} geometry Geometry.
+ * @param {olx.format.WriteOptions=} opt_options Write options.
+ * @private
+ * @return {GeoJSONGeometry} GeoJSON geometry.
+ */
+ol.format.GeoJSON.writeMultiPointGeometry_ = function(geometry, opt_options) {
+  goog.asserts.assertInstanceof(geometry, ol.geom.MultiPoint,
+      'geometry should be an ol.geom.MultiPoint');
+  return /** @type {GeoJSONGeometry} */ ({
+    type: 'MultiPoint',
+    coordinates: geometry.getCoordinates()
+  });
+};
+
+
+/**
+ * @param {ol.geom.Geometry} geometry Geometry.
+ * @param {olx.format.WriteOptions=} opt_options Write options.
+ * @private
+ * @return {GeoJSONGeometry} GeoJSON geometry.
+ */
+ol.format.GeoJSON.writeMultiPolygonGeometry_ = function(geometry, opt_options) {
+  goog.asserts.assertInstanceof(geometry, ol.geom.MultiPolygon,
+      'geometry should be an ol.geom.MultiPolygon');
+  var right;
+  if (opt_options) {
+    right = opt_options.rightHanded;
+  }
+  return /** @type {GeoJSONGeometry} */ ({
+    type: 'MultiPolygon',
+    coordinates: geometry.getCoordinates(right)
+  });
+};
+
+
+/**
+ * @param {ol.geom.Geometry} geometry Geometry.
+ * @param {olx.format.WriteOptions=} opt_options Write options.
+ * @private
+ * @return {GeoJSONGeometry} GeoJSON geometry.
+ */
+ol.format.GeoJSON.writePointGeometry_ = function(geometry, opt_options) {
+  goog.asserts.assertInstanceof(geometry, ol.geom.Point,
+      'geometry should be an ol.geom.Point');
+  return /** @type {GeoJSONGeometry} */ ({
+    type: 'Point',
+    coordinates: geometry.getCoordinates()
+  });
+};
+
+
+/**
+ * @param {ol.geom.Geometry} geometry Geometry.
+ * @param {olx.format.WriteOptions=} opt_options Write options.
+ * @private
+ * @return {GeoJSONGeometry} GeoJSON geometry.
+ */
+ol.format.GeoJSON.writePolygonGeometry_ = function(geometry, opt_options) {
+  goog.asserts.assertInstanceof(geometry, ol.geom.Polygon,
+      'geometry should be an ol.geom.Polygon');
+  var right;
+  if (opt_options) {
+    right = opt_options.rightHanded;
+  }
+  return /** @type {GeoJSONGeometry} */ ({
+    type: 'Polygon',
+    coordinates: geometry.getCoordinates(right)
+  });
+};
+
+
+/**
+ * @const
+ * @private
+ * @type {Object.<string, function(GeoJSONObject): ol.geom.Geometry>}
+ */
+ol.format.GeoJSON.GEOMETRY_READERS_ = {
+  'Point': ol.format.GeoJSON.readPointGeometry_,
+  'LineString': ol.format.GeoJSON.readLineStringGeometry_,
+  'Polygon': ol.format.GeoJSON.readPolygonGeometry_,
+  'MultiPoint': ol.format.GeoJSON.readMultiPointGeometry_,
+  'MultiLineString': ol.format.GeoJSON.readMultiLineStringGeometry_,
+  'MultiPolygon': ol.format.GeoJSON.readMultiPolygonGeometry_,
+  'GeometryCollection': ol.format.GeoJSON.readGeometryCollectionGeometry_
+};
+
+
+/**
+ * @const
+ * @private
+ * @type {Object.<string, function(ol.geom.Geometry, olx.format.WriteOptions=): (GeoJSONGeometry|GeoJSONGeometryCollection)>}
+ */
+ol.format.GeoJSON.GEOMETRY_WRITERS_ = {
+  'Point': ol.format.GeoJSON.writePointGeometry_,
+  'LineString': ol.format.GeoJSON.writeLineStringGeometry_,
+  'Polygon': ol.format.GeoJSON.writePolygonGeometry_,
+  'MultiPoint': ol.format.GeoJSON.writeMultiPointGeometry_,
+  'MultiLineString': ol.format.GeoJSON.writeMultiLineStringGeometry_,
+  'MultiPolygon': ol.format.GeoJSON.writeMultiPolygonGeometry_,
+  'GeometryCollection': ol.format.GeoJSON.writeGeometryCollectionGeometry_,
+  'Circle': ol.format.GeoJSON.writeEmptyGeometryCollectionGeometry_
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.format.GeoJSON.prototype.getExtensions = function() {
+  return ol.format.GeoJSON.EXTENSIONS_;
+};
+
+
+/**
+ * Read a feature from a GeoJSON Feature source.  Only works for Feature,
+ * use `readFeatures` to read FeatureCollection source.
+ *
+ * @function
+ * @param {Document|Node|Object|string} source Source.
+ * @param {olx.format.ReadOptions=} opt_options Read options.
+ * @return {ol.Feature} Feature.
+ * @api stable
+ */
+ol.format.GeoJSON.prototype.readFeature;
+
+
+/**
+ * Read all features from a GeoJSON source.  Works with both Feature and
+ * FeatureCollection sources.
+ *
+ * @function
+ * @param {Document|Node|Object|string} source Source.
+ * @param {olx.format.ReadOptions=} opt_options Read options.
+ * @return {Array.<ol.Feature>} Features.
+ * @api stable
+ */
+ol.format.GeoJSON.prototype.readFeatures;
+
+
+/**
+ * @inheritDoc
+ */
+ol.format.GeoJSON.prototype.readFeatureFromObject = function(
+    object, opt_options) {
+  var geoJSONFeature = /** @type {GeoJSONFeature} */ (object);
+  goog.asserts.assert(geoJSONFeature.type == 'Feature',
+      'geoJSONFeature.type should be Feature');
+  var geometry = ol.format.GeoJSON.readGeometry_(geoJSONFeature.geometry,
+      opt_options);
+  var feature = new ol.Feature();
+  if (this.geometryName_) {
+    feature.setGeometryName(this.geometryName_);
+  }
+  feature.setGeometry(geometry);
+  if (geoJSONFeature.id !== undefined) {
+    feature.setId(geoJSONFeature.id);
+  }
+  if (geoJSONFeature.properties) {
+    feature.setProperties(geoJSONFeature.properties);
+  }
+  return feature;
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.format.GeoJSON.prototype.readFeaturesFromObject = function(
+    object, opt_options) {
+  var geoJSONObject = /** @type {GeoJSONObject} */ (object);
+  if (geoJSONObject.type == 'Feature') {
+    return [this.readFeatureFromObject(object, opt_options)];
+  } else if (geoJSONObject.type == 'FeatureCollection') {
+    var geoJSONFeatureCollection = /** @type {GeoJSONFeatureCollection} */
+        (object);
+    /** @type {Array.<ol.Feature>} */
+    var features = [];
+    var geoJSONFeatures = geoJSONFeatureCollection.features;
+    var i, ii;
+    for (i = 0, ii = geoJSONFeatures.length; i < ii; ++i) {
+      features.push(this.readFeatureFromObject(geoJSONFeatures[i],
+          opt_options));
+    }
+    return features;
+  } else {
+    goog.asserts.fail('Unknown geoJSONObject.type: ' + geoJSONObject.type);
+    return [];
+  }
+};
+
+
+/**
+ * Read a geometry from a GeoJSON source.
+ *
+ * @function
+ * @param {Document|Node|Object|string} source Source.
+ * @param {olx.format.ReadOptions=} opt_options Read options.
+ * @return {ol.geom.Geometry} Geometry.
+ * @api stable
+ */
+ol.format.GeoJSON.prototype.readGeometry;
+
+
+/**
+ * @inheritDoc
+ */
+ol.format.GeoJSON.prototype.readGeometryFromObject = function(
+    object, opt_options) {
+  return ol.format.GeoJSON.readGeometry_(
+      /** @type {GeoJSONGeometry} */ (object), opt_options);
+};
+
+
+/**
+ * Read the projection from a GeoJSON source.
+ *
+ * @function
+ * @param {Document|Node|Object|string} source Source.
+ * @return {ol.proj.Projection} Projection.
+ * @api stable
+ */
+ol.format.GeoJSON.prototype.readProjection;
+
+
+/**
+ * @inheritDoc
+ */
+ol.format.GeoJSON.prototype.readProjectionFromObject = function(object) {
+  var geoJSONObject = /** @type {GeoJSONObject} */ (object);
+  var crs = geoJSONObject.crs;
+  if (crs) {
+    if (crs.type == 'name') {
+      return ol.proj.get(crs.properties.name);
+    } else if (crs.type == 'EPSG') {
+      // 'EPSG' is not part of the GeoJSON specification, but is generated by
+      // GeoServer.
+      // TODO: remove this when http://jira.codehaus.org/browse/GEOS-5996
+      // is fixed and widely deployed.
+      return ol.proj.get('EPSG:' + crs.properties.code);
+    } else {
+      goog.asserts.fail('Unknown crs.type: ' + crs.type);
+      return null;
+    }
+  } else {
+    return this.defaultDataProjection;
+  }
+};
+
+
+/**
+ * Encode a feature as a GeoJSON Feature string.
+ *
+ * @function
+ * @param {ol.Feature} feature Feature.
+ * @param {olx.format.WriteOptions=} opt_options Write options.
+ * @return {string} GeoJSON.
+ * @api stable
+ */
+ol.format.GeoJSON.prototype.writeFeature;
+
+
+/**
+ * Encode a feature as a GeoJSON Feature object.
+ *
+ * @param {ol.Feature} feature Feature.
+ * @param {olx.format.WriteOptions=} opt_options Write options.
+ * @return {GeoJSONFeature} Object.
+ * @api stable
+ */
+ol.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 =
+        ol.format.GeoJSON.writeGeometry_(geometry, opt_options);
+  } else {
+    object.geometry = null;
+  }
+  var properties = feature.getProperties();
+  delete properties[feature.getGeometryName()];
+  if (!ol.object.isEmpty(properties)) {
+    object.properties = properties;
+  } else {
+    object.properties = null;
+  }
+  return object;
+};
+
+
+/**
+ * Encode an array of features as GeoJSON.
+ *
+ * @function
+ * @param {Array.<ol.Feature>} features Features.
+ * @param {olx.format.WriteOptions=} opt_options Write options.
+ * @return {string} GeoJSON.
+ * @api stable
+ */
+ol.format.GeoJSON.prototype.writeFeatures;
+
+
+/**
+ * Encode an array of features as a GeoJSON object.
+ *
+ * @param {Array.<ol.Feature>} features Features.
+ * @param {olx.format.WriteOptions=} opt_options Write options.
+ * @return {GeoJSONFeatureCollection} GeoJSON Object.
+ * @api stable
+ */
+ol.format.GeoJSON.prototype.writeFeaturesObject = function(features, opt_options) {
+  opt_options = this.adaptOptions(opt_options);
+  var objects = [];
+  var i, ii;
+  for (i = 0, ii = features.length; i < ii; ++i) {
+    objects.push(this.writeFeatureObject(features[i], opt_options));
+  }
+  return /** @type {GeoJSONFeatureCollection} */ ({
+    type: 'FeatureCollection',
+    features: objects
+  });
+};
+
+
+/**
+ * Encode a geometry as a GeoJSON string.
+ *
+ * @function
+ * @param {ol.geom.Geometry} geometry Geometry.
+ * @param {olx.format.WriteOptions=} opt_options Write options.
+ * @return {string} GeoJSON.
+ * @api stable
+ */
+ol.format.GeoJSON.prototype.writeGeometry;
+
+
+/**
+ * Encode a geometry as a GeoJSON object.
+ *
+ * @param {ol.geom.Geometry} geometry Geometry.
+ * @param {olx.format.WriteOptions=} opt_options Write options.
+ * @return {GeoJSONGeometry|GeoJSONGeometryCollection} Object.
+ * @api stable
+ */
+ol.format.GeoJSON.prototype.writeGeometryObject = function(geometry,
+    opt_options) {
+  return ol.format.GeoJSON.writeGeometry_(geometry,
+      this.adaptOptions(opt_options));
+};
+
+goog.provide('ol.format.XMLFeature');
+
+goog.require('goog.asserts');
+goog.require('ol.array');
+goog.require('ol.format.Feature');
+goog.require('ol.format.FormatType');
+goog.require('ol.proj');
+goog.require('ol.xml');
+
+
+/**
+ * @classdesc
+ * Abstract base class; normally only used for creating subclasses and not
+ * instantiated in apps.
+ * Base class for XML feature formats.
+ *
+ * @constructor
+ * @extends {ol.format.Feature}
+ */
+ol.format.XMLFeature = function() {
+
+  /**
+   * @type {XMLSerializer}
+   * @private
+   */
+  this.xmlSerializer_ = new XMLSerializer();
+
+  ol.format.Feature.call(this);
+};
+ol.inherits(ol.format.XMLFeature, ol.format.Feature);
+
+
+/**
+ * @inheritDoc
+ */
+ol.format.XMLFeature.prototype.getType = function() {
+  return ol.format.FormatType.XML;
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.format.XMLFeature.prototype.readFeature = function(source, opt_options) {
+  if (ol.xml.isDocument(source)) {
+    return this.readFeatureFromDocument(
+        /** @type {Document} */ (source), opt_options);
+  } else if (ol.xml.isNode(source)) {
+    return this.readFeatureFromNode(/** @type {Node} */ (source), opt_options);
+  } else if (typeof source === 'string') {
+    var doc = ol.xml.parse(source);
+    return this.readFeatureFromDocument(doc, opt_options);
+  } else {
+    goog.asserts.fail('Unknown source type');
+    return null;
+  }
+};
+
+
+/**
+ * @param {Document} doc Document.
+ * @param {olx.format.ReadOptions=} opt_options Options.
+ * @return {ol.Feature} Feature.
+ */
+ol.format.XMLFeature.prototype.readFeatureFromDocument = function(
+    doc, opt_options) {
+  var features = this.readFeaturesFromDocument(doc, opt_options);
+  if (features.length > 0) {
+    return features[0];
+  } else {
+    return null;
+  }
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {olx.format.ReadOptions=} opt_options Options.
+ * @return {ol.Feature} Feature.
+ */
+ol.format.XMLFeature.prototype.readFeatureFromNode = goog.abstractMethod;
+
+
+/**
+ * @inheritDoc
+ */
+ol.format.XMLFeature.prototype.readFeatures = function(source, opt_options) {
+  if (ol.xml.isDocument(source)) {
+    return this.readFeaturesFromDocument(
+        /** @type {Document} */ (source), opt_options);
+  } else if (ol.xml.isNode(source)) {
+    return this.readFeaturesFromNode(/** @type {Node} */ (source), opt_options);
+  } else if (typeof source === 'string') {
+    var doc = ol.xml.parse(source);
+    return this.readFeaturesFromDocument(doc, opt_options);
+  } else {
+    goog.asserts.fail('Unknown source type');
+    return [];
+  }
+};
+
+
+/**
+ * @param {Document} doc Document.
+ * @param {olx.format.ReadOptions=} opt_options Options.
+ * @protected
+ * @return {Array.<ol.Feature>} Features.
+ */
+ol.format.XMLFeature.prototype.readFeaturesFromDocument = function(
+    doc, opt_options) {
+  /** @type {Array.<ol.Feature>} */
+  var features = [];
+  var n;
+  for (n = doc.firstChild; n; n = n.nextSibling) {
+    if (n.nodeType == Node.ELEMENT_NODE) {
+      ol.array.extend(features, this.readFeaturesFromNode(n, opt_options));
+    }
+  }
+  return features;
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {olx.format.ReadOptions=} opt_options Options.
+ * @protected
+ * @return {Array.<ol.Feature>} Features.
+ */
+ol.format.XMLFeature.prototype.readFeaturesFromNode = goog.abstractMethod;
+
+
+/**
+ * @inheritDoc
+ */
+ol.format.XMLFeature.prototype.readGeometry = function(source, opt_options) {
+  if (ol.xml.isDocument(source)) {
+    return this.readGeometryFromDocument(
+        /** @type {Document} */ (source), opt_options);
+  } else if (ol.xml.isNode(source)) {
+    return this.readGeometryFromNode(/** @type {Node} */ (source), opt_options);
+  } else if (typeof source === 'string') {
+    var doc = ol.xml.parse(source);
+    return this.readGeometryFromDocument(doc, opt_options);
+  } else {
+    goog.asserts.fail('Unknown source type');
+    return null;
+  }
+};
+
+
+/**
+ * @param {Document} doc Document.
+ * @param {olx.format.ReadOptions=} opt_options Options.
+ * @protected
+ * @return {ol.geom.Geometry} Geometry.
+ */
+ol.format.XMLFeature.prototype.readGeometryFromDocument = goog.abstractMethod;
+
+
+/**
+ * @param {Node} node Node.
+ * @param {olx.format.ReadOptions=} opt_options Options.
+ * @protected
+ * @return {ol.geom.Geometry} Geometry.
+ */
+ol.format.XMLFeature.prototype.readGeometryFromNode = goog.abstractMethod;
+
+
+/**
+ * @inheritDoc
+ */
+ol.format.XMLFeature.prototype.readProjection = function(source) {
+  if (ol.xml.isDocument(source)) {
+    return this.readProjectionFromDocument(/** @type {Document} */ (source));
+  } else if (ol.xml.isNode(source)) {
+    return this.readProjectionFromNode(/** @type {Node} */ (source));
+  } else if (typeof source === 'string') {
+    var doc = ol.xml.parse(source);
+    return this.readProjectionFromDocument(doc);
+  } else {
+    goog.asserts.fail('Unknown source type');
+    return null;
+  }
+};
+
+
+/**
+ * @param {Document} doc Document.
+ * @protected
+ * @return {ol.proj.Projection} Projection.
+ */
+ol.format.XMLFeature.prototype.readProjectionFromDocument = function(doc) {
+  return this.defaultDataProjection;
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @protected
+ * @return {ol.proj.Projection} Projection.
+ */
+ol.format.XMLFeature.prototype.readProjectionFromNode = function(node) {
+  return this.defaultDataProjection;
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.format.XMLFeature.prototype.writeFeature = function(feature, opt_options) {
+  var node = this.writeFeatureNode(feature, opt_options);
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'node.nodeType should be ELEMENT');
+  return this.xmlSerializer_.serializeToString(node);
+};
+
+
+/**
+ * @param {ol.Feature} feature Feature.
+ * @param {olx.format.WriteOptions=} opt_options Options.
+ * @protected
+ * @return {Node} Node.
+ */
+ol.format.XMLFeature.prototype.writeFeatureNode = goog.abstractMethod;
+
+
+/**
+ * @inheritDoc
+ */
+ol.format.XMLFeature.prototype.writeFeatures = function(features, opt_options) {
+  var node = this.writeFeaturesNode(features, opt_options);
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'node.nodeType should be ELEMENT');
+  return this.xmlSerializer_.serializeToString(node);
+};
+
+
+/**
+ * @param {Array.<ol.Feature>} features Features.
+ * @param {olx.format.WriteOptions=} opt_options Options.
+ * @return {Node} Node.
+ */
+ol.format.XMLFeature.prototype.writeFeaturesNode = goog.abstractMethod;
+
+
+/**
+ * @inheritDoc
+ */
+ol.format.XMLFeature.prototype.writeGeometry = function(geometry, opt_options) {
+  var node = this.writeGeometryNode(geometry, opt_options);
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'node.nodeType should be ELEMENT');
+  return this.xmlSerializer_.serializeToString(node);
+};
+
+
+/**
+ * @param {ol.geom.Geometry} geometry Geometry.
+ * @param {olx.format.WriteOptions=} opt_options Options.
+ * @return {Node} Node.
+ */
+ol.format.XMLFeature.prototype.writeGeometryNode = goog.abstractMethod;
+
+// FIXME Envelopes should not be treated as geometries! readEnvelope_ is part
+// of GEOMETRY_PARSERS_ and methods using GEOMETRY_PARSERS_ do not expect
+// envelopes/extents, only geometries!
+goog.provide('ol.format.GMLBase');
+
+goog.require('goog.asserts');
+goog.require('ol.array');
+goog.require('ol.Feature');
+goog.require('ol.format.Feature');
+goog.require('ol.format.XMLFeature');
+goog.require('ol.geom.Geometry');
+goog.require('ol.geom.GeometryLayout');
+goog.require('ol.geom.LineString');
+goog.require('ol.geom.LinearRing');
+goog.require('ol.geom.MultiLineString');
+goog.require('ol.geom.MultiPoint');
+goog.require('ol.geom.MultiPolygon');
+goog.require('ol.geom.Point');
+goog.require('ol.geom.Polygon');
+goog.require('ol.object');
+goog.require('ol.proj');
+goog.require('ol.xml');
+
+
+/**
+ * @classdesc
+ * Abstract base class; normally only used for creating subclasses and not
+ * instantiated in apps.
+ * Feature base format for reading and writing data in the GML format.
+ * This class cannot be instantiated, it contains only base content that
+ * is shared with versioned format classes ol.format.GML2 and
+ * ol.format.GML3.
+ *
+ * @constructor
+ * @param {olx.format.GMLOptions=} opt_options
+ *     Optional configuration object.
+ * @extends {ol.format.XMLFeature}
+ */
+ol.format.GMLBase = function(opt_options) {
+  var options = /** @type {olx.format.GMLOptions} */
+      (opt_options ? opt_options : {});
+
+  /**
+   * @protected
+   * @type {Array.<string>|string|undefined}
+   */
+  this.featureType = options.featureType;
+
+  /**
+   * @protected
+   * @type {Object.<string, string>|string|undefined}
+   */
+  this.featureNS = options.featureNS;
+
+  /**
+   * @protected
+   * @type {string}
+   */
+  this.srsName = options.srsName;
+
+  /**
+   * @protected
+   * @type {string}
+   */
+  this.schemaLocation = '';
+
+  /**
+   * @type {Object.<string, Object.<string, Object>>}
+   */
+  this.FEATURE_COLLECTION_PARSERS = {};
+  this.FEATURE_COLLECTION_PARSERS[ol.format.GMLBase.GMLNS] = {
+    'featureMember': ol.xml.makeReplacer(
+        ol.format.GMLBase.prototype.readFeaturesInternal),
+    'featureMembers': ol.xml.makeReplacer(
+        ol.format.GMLBase.prototype.readFeaturesInternal)
+  };
+
+  ol.format.XMLFeature.call(this);
+};
+ol.inherits(ol.format.GMLBase, ol.format.XMLFeature);
+
+
+/**
+ * @const
+ * @type {string}
+ */
+ol.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
+ */
+ol.format.GMLBase.ONLY_WHITESPACE_RE_ = /^[\s\xa0]*$/;
+
+
+/**
+ * @param {Node} node Node.
+ * @param {Array.<*>} objectStack Object stack.
+ * @return {Array.<ol.Feature> | undefined} Features.
+ */
+ol.format.GMLBase.prototype.readFeaturesInternal = function(node, objectStack) {
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'node.nodeType should be ELEMENT');
+  var localName = node.localName;
+  var features = null;
+  if (localName == 'FeatureCollection') {
+    if (node.namespaceURI === 'http://www.opengis.net/wfs') {
+      features = ol.xml.pushParseAndPop([],
+          this.FEATURE_COLLECTION_PARSERS, node,
+          objectStack, this);
+    } else {
+      features = ol.xml.pushParseAndPop(null,
+          this.FEATURE_COLLECTION_PARSERS, node,
+          objectStack, this);
+    }
+  } else if (localName == 'featureMembers' || localName == 'featureMember') {
+    var context = objectStack[0];
+    goog.asserts.assert(goog.isObject(context), 'context should be an Object');
+    var featureType = context['featureType'];
+    var featureNS = context['featureNS'];
+    var i, ii, prefix = 'p', defaultPrefix = 'p0';
+    if (!featureType && node.childNodes) {
+      featureType = [], featureNS = {};
+      for (i = 0, ii = node.childNodes.length; i < ii; ++i) {
+        var child = node.childNodes[i];
+        if (child.nodeType === 1) {
+          var ft = child.nodeName.split(':').pop();
+          if (featureType.indexOf(ft) === -1) {
+            var key = '';
+            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') ?
+              ol.xml.makeArrayPusher(this.readFeatureElement, this) :
+              ol.xml.makeReplacer(this.readFeatureElement, this);
+        }
+      }
+      parsersNS[featureNS[p]] = parsers;
+    }
+    if (localName == 'featureMember') {
+      features = ol.xml.pushParseAndPop(undefined, parsersNS, node, objectStack);
+    } else {
+      features = ol.xml.pushParseAndPop([], parsersNS, node, objectStack);
+    }
+  }
+  if (features === null) {
+    features = [];
+  }
+  return features;
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {Array.<*>} objectStack Object stack.
+ * @return {ol.geom.Geometry|undefined} Geometry.
+ */
+ol.format.GMLBase.prototype.readGeometryElement = function(node, objectStack) {
+  var context = objectStack[0];
+  goog.asserts.assert(goog.isObject(context), 'context should be an Object');
+  context['srsName'] = node.firstElementChild.getAttribute('srsName');
+  /** @type {ol.geom.Geometry} */
+  var geometry = ol.xml.pushParseAndPop(null,
+      this.GEOMETRY_PARSERS_, node, objectStack, this);
+  if (geometry) {
+    return /** @type {ol.geom.Geometry} */ (
+        ol.format.Feature.transformWithOptions(geometry, false, context));
+  } else {
+    return undefined;
+  }
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {Array.<*>} objectStack Object stack.
+ * @return {ol.Feature} Feature.
+ */
+ol.format.GMLBase.prototype.readFeatureElement = function(node, objectStack) {
+  var n;
+  var fid = node.getAttribute('fid') ||
+      ol.xml.getAttributeNS(node, ol.format.GMLBase.GMLNS, 'id');
+  var values = {}, geometryName;
+  for (n = node.firstElementChild; n; n = n.nextElementSibling) {
+    var localName = 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 = ol.xml.getAllTextContent(n, false);
+      if (ol.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 ol.Feature(values);
+  if (geometryName) {
+    feature.setGeometryName(geometryName);
+  }
+  if (fid) {
+    feature.setId(fid);
+  }
+  return feature;
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {Array.<*>} objectStack Object stack.
+ * @return {ol.geom.Point|undefined} Point.
+ */
+ol.format.GMLBase.prototype.readPoint = function(node, objectStack) {
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'node.nodeType should be ELEMENT');
+  goog.asserts.assert(node.localName == 'Point', 'localName should be Point');
+  var flatCoordinates =
+      this.readFlatCoordinatesFromNode_(node, objectStack);
+  if (flatCoordinates) {
+    var point = new ol.geom.Point(null);
+    goog.asserts.assert(flatCoordinates.length == 3,
+        'flatCoordinates should have a length of 3');
+    point.setFlatCoordinates(ol.geom.GeometryLayout.XYZ, flatCoordinates);
+    return point;
+  }
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {Array.<*>} objectStack Object stack.
+ * @return {ol.geom.MultiPoint|undefined} MultiPoint.
+ */
+ol.format.GMLBase.prototype.readMultiPoint = function(node, objectStack) {
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'node.nodeType should be ELEMENT');
+  goog.asserts.assert(node.localName == 'MultiPoint',
+      'localName should be MultiPoint');
+  /** @type {Array.<Array.<number>>} */
+  var coordinates = ol.xml.pushParseAndPop([],
+      this.MULTIPOINT_PARSERS_, node, objectStack, this);
+  if (coordinates) {
+    return new ol.geom.MultiPoint(coordinates);
+  } else {
+    return undefined;
+  }
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {Array.<*>} objectStack Object stack.
+ * @return {ol.geom.MultiLineString|undefined} MultiLineString.
+ */
+ol.format.GMLBase.prototype.readMultiLineString = function(node, objectStack) {
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'node.nodeType should be ELEMENT');
+  goog.asserts.assert(node.localName == 'MultiLineString',
+      'localName should be MultiLineString');
+  /** @type {Array.<ol.geom.LineString>} */
+  var lineStrings = ol.xml.pushParseAndPop([],
+      this.MULTILINESTRING_PARSERS_, node, objectStack, this);
+  if (lineStrings) {
+    var multiLineString = new ol.geom.MultiLineString(null);
+    multiLineString.setLineStrings(lineStrings);
+    return multiLineString;
+  } else {
+    return undefined;
+  }
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {Array.<*>} objectStack Object stack.
+ * @return {ol.geom.MultiPolygon|undefined} MultiPolygon.
+ */
+ol.format.GMLBase.prototype.readMultiPolygon = function(node, objectStack) {
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'node.nodeType should be ELEMENT');
+  goog.asserts.assert(node.localName == 'MultiPolygon',
+      'localName should be MultiPolygon');
+  /** @type {Array.<ol.geom.Polygon>} */
+  var polygons = ol.xml.pushParseAndPop([],
+      this.MULTIPOLYGON_PARSERS_, node, objectStack, this);
+  if (polygons) {
+    var multiPolygon = new ol.geom.MultiPolygon(null);
+    multiPolygon.setPolygons(polygons);
+    return multiPolygon;
+  } else {
+    return undefined;
+  }
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {Array.<*>} objectStack Object stack.
+ * @private
+ */
+ol.format.GMLBase.prototype.pointMemberParser_ = function(node, objectStack) {
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'node.nodeType should be ELEMENT');
+  goog.asserts.assert(node.localName == 'pointMember' ||
+      node.localName == 'pointMembers',
+      'localName should be pointMember or pointMembers');
+  ol.xml.parseNode(this.POINTMEMBER_PARSERS_,
+      node, objectStack, this);
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {Array.<*>} objectStack Object stack.
+ * @private
+ */
+ol.format.GMLBase.prototype.lineStringMemberParser_ = function(node, objectStack) {
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'node.nodeType should be ELEMENT');
+  goog.asserts.assert(node.localName == 'lineStringMember' ||
+      node.localName == 'lineStringMembers',
+      'localName should be LineStringMember or LineStringMembers');
+  ol.xml.parseNode(this.LINESTRINGMEMBER_PARSERS_,
+      node, objectStack, this);
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {Array.<*>} objectStack Object stack.
+ * @private
+ */
+ol.format.GMLBase.prototype.polygonMemberParser_ = function(node, objectStack) {
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'node.nodeType should be ELEMENT');
+  goog.asserts.assert(node.localName == 'polygonMember' ||
+      node.localName == 'polygonMembers',
+      'localName should be polygonMember or polygonMembers');
+  ol.xml.parseNode(this.POLYGONMEMBER_PARSERS_, node,
+      objectStack, this);
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {Array.<*>} objectStack Object stack.
+ * @return {ol.geom.LineString|undefined} LineString.
+ */
+ol.format.GMLBase.prototype.readLineString = function(node, objectStack) {
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'node.nodeType should be ELEMENT');
+  goog.asserts.assert(node.localName == 'LineString',
+      'localName should be LineString');
+  var flatCoordinates =
+      this.readFlatCoordinatesFromNode_(node, objectStack);
+  if (flatCoordinates) {
+    var lineString = new ol.geom.LineString(null);
+    lineString.setFlatCoordinates(ol.geom.GeometryLayout.XYZ, flatCoordinates);
+    return lineString;
+  } else {
+    return undefined;
+  }
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {Array.<*>} objectStack Object stack.
+ * @private
+ * @return {Array.<number>|undefined} LinearRing flat coordinates.
+ */
+ol.format.GMLBase.prototype.readFlatLinearRing_ = function(node, objectStack) {
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'node.nodeType should be ELEMENT');
+  goog.asserts.assert(node.localName == 'LinearRing',
+      'localName should be LinearRing');
+  var ring = ol.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 {ol.geom.LinearRing|undefined} LinearRing.
+ */
+ol.format.GMLBase.prototype.readLinearRing = function(node, objectStack) {
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'node.nodeType should be ELEMENT');
+  goog.asserts.assert(node.localName == 'LinearRing',
+      'localName should be LinearRing');
+  var flatCoordinates =
+      this.readFlatCoordinatesFromNode_(node, objectStack);
+  if (flatCoordinates) {
+    var ring = new ol.geom.LinearRing(null);
+    ring.setFlatCoordinates(ol.geom.GeometryLayout.XYZ, flatCoordinates);
+    return ring;
+  } else {
+    return undefined;
+  }
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {Array.<*>} objectStack Object stack.
+ * @return {ol.geom.Polygon|undefined} Polygon.
+ */
+ol.format.GMLBase.prototype.readPolygon = function(node, objectStack) {
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'node.nodeType should be ELEMENT');
+  goog.asserts.assert(node.localName == 'Polygon',
+      'localName should be Polygon');
+  /** @type {Array.<Array.<number>>} */
+  var flatLinearRings = ol.xml.pushParseAndPop([null],
+      this.FLAT_LINEAR_RINGS_PARSERS_, node, objectStack, this);
+  if (flatLinearRings && flatLinearRings[0]) {
+    var polygon = new ol.geom.Polygon(null);
+    var flatCoordinates = flatLinearRings[0];
+    var ends = [flatCoordinates.length];
+    var i, ii;
+    for (i = 1, ii = flatLinearRings.length; i < ii; ++i) {
+      ol.array.extend(flatCoordinates, flatLinearRings[i]);
+      ends.push(flatCoordinates.length);
+    }
+    polygon.setFlatCoordinates(
+        ol.geom.GeometryLayout.XYZ, flatCoordinates, ends);
+    return polygon;
+  } else {
+    return undefined;
+  }
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {Array.<*>} objectStack Object stack.
+ * @private
+ * @return {Array.<number>} Flat coordinates.
+ */
+ol.format.GMLBase.prototype.readFlatCoordinatesFromNode_ = function(node, objectStack) {
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'node.nodeType should be ELEMENT');
+  return ol.xml.pushParseAndPop(null,
+      this.GEOMETRY_FLAT_COORDINATES_PARSERS_, node,
+      objectStack, this);
+};
+
+
+/**
+ * @const
+ * @type {Object.<string, Object.<string, ol.XmlParser>>}
+ * @private
+ */
+ol.format.GMLBase.prototype.MULTIPOINT_PARSERS_ = {
+  'http://www.opengis.net/gml' : {
+    'pointMember': ol.xml.makeArrayPusher(
+        ol.format.GMLBase.prototype.pointMemberParser_),
+    'pointMembers': ol.xml.makeArrayPusher(
+        ol.format.GMLBase.prototype.pointMemberParser_)
+  }
+};
+
+
+/**
+ * @const
+ * @type {Object.<string, Object.<string, ol.XmlParser>>}
+ * @private
+ */
+ol.format.GMLBase.prototype.MULTILINESTRING_PARSERS_ = {
+  'http://www.opengis.net/gml' : {
+    'lineStringMember': ol.xml.makeArrayPusher(
+        ol.format.GMLBase.prototype.lineStringMemberParser_),
+    'lineStringMembers': ol.xml.makeArrayPusher(
+        ol.format.GMLBase.prototype.lineStringMemberParser_)
+  }
+};
+
+
+/**
+ * @const
+ * @type {Object.<string, Object.<string, ol.XmlParser>>}
+ * @private
+ */
+ol.format.GMLBase.prototype.MULTIPOLYGON_PARSERS_ = {
+  'http://www.opengis.net/gml' : {
+    'polygonMember': ol.xml.makeArrayPusher(
+        ol.format.GMLBase.prototype.polygonMemberParser_),
+    'polygonMembers': ol.xml.makeArrayPusher(
+        ol.format.GMLBase.prototype.polygonMemberParser_)
+  }
+};
+
+
+/**
+ * @const
+ * @type {Object.<string, Object.<string, ol.XmlParser>>}
+ * @private
+ */
+ol.format.GMLBase.prototype.POINTMEMBER_PARSERS_ = {
+  'http://www.opengis.net/gml' : {
+    'Point': ol.xml.makeArrayPusher(
+        ol.format.GMLBase.prototype.readFlatCoordinatesFromNode_)
+  }
+};
+
+
+/**
+ * @const
+ * @type {Object.<string, Object.<string, ol.XmlParser>>}
+ * @private
+ */
+ol.format.GMLBase.prototype.LINESTRINGMEMBER_PARSERS_ = {
+  'http://www.opengis.net/gml' : {
+    'LineString': ol.xml.makeArrayPusher(
+        ol.format.GMLBase.prototype.readLineString)
+  }
+};
+
+
+/**
+ * @const
+ * @type {Object.<string, Object.<string, ol.XmlParser>>}
+ * @private
+ */
+ol.format.GMLBase.prototype.POLYGONMEMBER_PARSERS_ = {
+  'http://www.opengis.net/gml' : {
+    'Polygon': ol.xml.makeArrayPusher(
+        ol.format.GMLBase.prototype.readPolygon)
+  }
+};
+
+
+/**
+ * @const
+ * @type {Object.<string, Object.<string, ol.XmlParser>>}
+ * @protected
+ */
+ol.format.GMLBase.prototype.RING_PARSERS = {
+  'http://www.opengis.net/gml' : {
+    'LinearRing': ol.xml.makeReplacer(
+        ol.format.GMLBase.prototype.readFlatLinearRing_)
+  }
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.format.GMLBase.prototype.readGeometryFromNode = function(node, opt_options) {
+  var geometry = this.readGeometryElement(node,
+      [this.getReadOptions(node, opt_options ? opt_options : {})]);
+  return geometry ? geometry : null;
+};
+
+
+/**
+ * Read all features from a GML FeatureCollection.
+ *
+ * @function
+ * @param {Document|Node|Object|string} source Source.
+ * @param {olx.format.ReadOptions=} opt_options Options.
+ * @return {Array.<ol.Feature>} Features.
+ * @api stable
+ */
+ol.format.GMLBase.prototype.readFeatures;
+
+
+/**
+ * @inheritDoc
+ */
+ol.format.GMLBase.prototype.readFeaturesFromNode = function(node, opt_options) {
+  var options = {
+    featureType: this.featureType,
+    featureNS: this.featureNS
+  };
+  if (opt_options) {
+    ol.object.assign(options, this.getReadOptions(node, opt_options));
+  }
+  var features = this.readFeaturesInternal(node, [options]);
+  return features || [];
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.format.GMLBase.prototype.readProjectionFromNode = function(node) {
+  return ol.proj.get(this.srsName ? this.srsName :
+      node.firstElementChild.getAttribute('srsName'));
+};
+
+goog.provide('ol.format.XSD');
+
+goog.require('goog.asserts');
+goog.require('ol');
+goog.require('ol.xml');
+goog.require('ol.string');
+
+
+/**
+ * @const
+ * @type {string}
+ */
+ol.format.XSD.NAMESPACE_URI = 'http://www.w3.org/2001/XMLSchema';
+
+
+/**
+ * @param {Node} node Node.
+ * @return {boolean|undefined} Boolean.
+ */
+ol.format.XSD.readBoolean = function(node) {
+  var s = ol.xml.getAllTextContent(node, false);
+  return ol.format.XSD.readBooleanString(s);
+};
+
+
+/**
+ * @param {string} string String.
+ * @return {boolean|undefined} Boolean.
+ */
+ol.format.XSD.readBooleanString = function(string) {
+  var m = /^\s*(true|1)|(false|0)\s*$/.exec(string);
+  if (m) {
+    return m[1] !== undefined || false;
+  } else {
+    return undefined;
+  }
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @return {number|undefined} DateTime in seconds.
+ */
+ol.format.XSD.readDateTime = function(node) {
+  var s = ol.xml.getAllTextContent(node, false);
+  var re =
+      /^\s*(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(Z|(?:([+\-])(\d{2})(?::(\d{2}))?))\s*$/;
+  var m = re.exec(s);
+  if (m) {
+    var year = parseInt(m[1], 10);
+    var month = parseInt(m[2], 10) - 1;
+    var day = parseInt(m[3], 10);
+    var hour = parseInt(m[4], 10);
+    var minute = parseInt(m[5], 10);
+    var second = parseInt(m[6], 10);
+    var dateTime = Date.UTC(year, month, day, hour, minute, second) / 1000;
+    if (m[7] != 'Z') {
+      var sign = m[8] == '-' ? -1 : 1;
+      dateTime += sign * 60 * parseInt(m[9], 10);
+      if (m[10] !== undefined) {
+        dateTime += sign * 60 * 60 * parseInt(m[10], 10);
+      }
+    }
+    return dateTime;
+  } else {
+    return undefined;
+  }
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @return {number|undefined} Decimal.
+ */
+ol.format.XSD.readDecimal = function(node) {
+  var s = ol.xml.getAllTextContent(node, false);
+  return ol.format.XSD.readDecimalString(s);
+};
+
+
+/**
+ * @param {string} string String.
+ * @return {number|undefined} Decimal.
+ */
+ol.format.XSD.readDecimalString = function(string) {
+  // FIXME check spec
+  var m = /^\s*([+\-]?\d*\.?\d+(?:e[+\-]?\d+)?)\s*$/i.exec(string);
+  if (m) {
+    return parseFloat(m[1]);
+  } else {
+    return undefined;
+  }
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @return {number|undefined} Non negative integer.
+ */
+ol.format.XSD.readNonNegativeInteger = function(node) {
+  var s = ol.xml.getAllTextContent(node, false);
+  return ol.format.XSD.readNonNegativeIntegerString(s);
+};
+
+
+/**
+ * @param {string} string String.
+ * @return {number|undefined} Non negative integer.
+ */
+ol.format.XSD.readNonNegativeIntegerString = function(string) {
+  var m = /^\s*(\d+)\s*$/.exec(string);
+  if (m) {
+    return parseInt(m[1], 10);
+  } else {
+    return undefined;
+  }
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @return {string|undefined} String.
+ */
+ol.format.XSD.readString = function(node) {
+  return ol.xml.getAllTextContent(node, false).trim();
+};
+
+
+/**
+ * @param {Node} node Node to append a TextNode with the boolean to.
+ * @param {boolean} bool Boolean.
+ */
+ol.format.XSD.writeBooleanTextNode = function(node, bool) {
+  ol.format.XSD.writeStringTextNode(node, (bool) ? '1' : '0');
+};
+
+
+/**
+ * @param {Node} node Node to append a TextNode with the dateTime to.
+ * @param {number} dateTime DateTime in seconds.
+ */
+ol.format.XSD.writeDateTimeTextNode = function(node, dateTime) {
+  var date = new Date(dateTime * 1000);
+  var string = date.getUTCFullYear() + '-' +
+      ol.string.padNumber(date.getUTCMonth() + 1, 2) + '-' +
+      ol.string.padNumber(date.getUTCDate(), 2) + 'T' +
+      ol.string.padNumber(date.getUTCHours(), 2) + ':' +
+      ol.string.padNumber(date.getUTCMinutes(), 2) + ':' +
+      ol.string.padNumber(date.getUTCSeconds(), 2) + 'Z';
+  node.appendChild(ol.xml.DOCUMENT.createTextNode(string));
+};
+
+
+/**
+ * @param {Node} node Node to append a TextNode with the decimal to.
+ * @param {number} decimal Decimal.
+ */
+ol.format.XSD.writeDecimalTextNode = function(node, decimal) {
+  var string = decimal.toPrecision();
+  node.appendChild(ol.xml.DOCUMENT.createTextNode(string));
+};
+
+
+/**
+ * @param {Node} node Node to append a TextNode with the decimal to.
+ * @param {number} nonNegativeInteger Non negative integer.
+ */
+ol.format.XSD.writeNonNegativeIntegerTextNode = function(node, nonNegativeInteger) {
+  goog.asserts.assert(nonNegativeInteger >= 0, 'value should be more than 0');
+  goog.asserts.assert(nonNegativeInteger == (nonNegativeInteger | 0),
+      'value should be an integer value');
+  var string = nonNegativeInteger.toString();
+  node.appendChild(ol.xml.DOCUMENT.createTextNode(string));
+};
+
+
+/**
+ * @param {Node} node Node to append a TextNode with the string to.
+ * @param {string} string String.
+ */
+ol.format.XSD.writeStringTextNode = function(node, string) {
+  node.appendChild(ol.xml.DOCUMENT.createTextNode(string));
+};
+
+goog.provide('ol.format.GML2');
+
+goog.require('goog.asserts');
+goog.require('ol.extent');
+goog.require('ol.format.GMLBase');
+goog.require('ol.format.XSD');
+goog.require('ol.proj');
+goog.require('ol.xml');
+
+
+/**
+ * @classdesc
+ * Feature format for reading and writing data in the GML format,
+ * version 2.1.2.
+ *
+ * @constructor
+ * @param {olx.format.GMLOptions=} opt_options Optional configuration object.
+ * @extends {ol.format.GMLBase}
+ * @api
+ */
+ol.format.GML2 = function(opt_options) {
+  var options = /** @type {olx.format.GMLOptions} */
+      (opt_options ? opt_options : {});
+
+  ol.format.GMLBase.call(this, options);
+
+  this.FEATURE_COLLECTION_PARSERS[ol.format.GMLBase.GMLNS][
+      'featureMember'] =
+      ol.xml.makeArrayPusher(ol.format.GMLBase.prototype.readFeaturesInternal);
+
+  /**
+   * @inheritDoc
+   */
+  this.schemaLocation = options.schemaLocation ?
+      options.schemaLocation : ol.format.GML2.schemaLocation_;
+
+};
+ol.inherits(ol.format.GML2, ol.format.GMLBase);
+
+
+/**
+ * @const
+ * @type {string}
+ * @private
+ */
+ol.format.GML2.schemaLocation_ = ol.format.GMLBase.GMLNS +
+    ' http://schemas.opengis.net/gml/2.1.2/feature.xsd';
+
+
+/**
+ * @param {Node} node Node.
+ * @param {Array.<*>} objectStack Object stack.
+ * @private
+ * @return {Array.<number>|undefined} Flat coordinates.
+ */
+ol.format.GML2.prototype.readFlatCoordinates_ = function(node, objectStack) {
+  var s = ol.xml.getAllTextContent(node, false).replace(/^\s*|\s*$/g, '');
+  var context = /** @type {ol.XmlNodeStackItem} */ (objectStack[0]);
+  goog.asserts.assert(goog.isObject(context), 'context should be an Object');
+  var containerSrs = context['srsName'];
+  var containerDimension = node.parentNode.getAttribute('srsDimension');
+  var axisOrientation = 'enu';
+  if (containerSrs) {
+    var proj = ol.proj.get(containerSrs);
+    if (proj) {
+      axisOrientation = proj.getAxisOrientation();
+    }
+  }
+  var coords = s.split(/[\s,]+/);
+  // The "dimension" attribute is from the GML 3.0.1 spec.
+  var dim = 2;
+  if (node.getAttribute('srsDimension')) {
+    dim = ol.format.XSD.readNonNegativeIntegerString(
+        node.getAttribute('srsDimension'));
+  } else if (node.getAttribute('dimension')) {
+    dim = ol.format.XSD.readNonNegativeIntegerString(
+        node.getAttribute('dimension'));
+  } else if (containerDimension) {
+    dim = ol.format.XSD.readNonNegativeIntegerString(containerDimension);
+  }
+  var x, y, z;
+  var flatCoordinates = [];
+  for (var i = 0, ii = coords.length; i < ii; i += dim) {
+    x = parseFloat(coords[i]);
+    y = parseFloat(coords[i + 1]);
+    z = (dim === 3) ? parseFloat(coords[i + 2]) : 0;
+    if (axisOrientation.substr(0, 2) === 'en') {
+      flatCoordinates.push(x, y, z);
+    } else {
+      flatCoordinates.push(y, x, z);
+    }
+  }
+  return flatCoordinates;
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {Array.<*>} objectStack Object stack.
+ * @private
+ * @return {ol.Extent|undefined} Envelope.
+ */
+ol.format.GML2.prototype.readBox_ = function(node, objectStack) {
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'node.nodeType should be ELEMENT');
+  goog.asserts.assert(node.localName == 'Box', 'localName should be Box');
+  /** @type {Array.<number>} */
+  var flatCoordinates = ol.xml.pushParseAndPop([null],
+      this.BOX_PARSERS_, node, objectStack, this);
+  return ol.extent.createOrUpdate(flatCoordinates[1][0],
+      flatCoordinates[1][1], flatCoordinates[1][3],
+      flatCoordinates[1][4]);
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {Array.<*>} objectStack Object stack.
+ * @private
+ */
+ol.format.GML2.prototype.innerBoundaryIsParser_ = function(node, objectStack) {
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'node.nodeType should be ELEMENT');
+  goog.asserts.assert(node.localName == 'innerBoundaryIs',
+      'localName should be innerBoundaryIs');
+  /** @type {Array.<number>|undefined} */
+  var flatLinearRing = ol.xml.pushParseAndPop(undefined,
+      this.RING_PARSERS, node, objectStack, this);
+  if (flatLinearRing) {
+    var flatLinearRings = /** @type {Array.<Array.<number>>} */
+        (objectStack[objectStack.length - 1]);
+    goog.asserts.assert(Array.isArray(flatLinearRings),
+        'flatLinearRings should be an array');
+    goog.asserts.assert(flatLinearRings.length > 0,
+        'flatLinearRings should have an array length larger than 0');
+    flatLinearRings.push(flatLinearRing);
+  }
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {Array.<*>} objectStack Object stack.
+ * @private
+ */
+ol.format.GML2.prototype.outerBoundaryIsParser_ = function(node, objectStack) {
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'node.nodeType should be ELEMENT');
+  goog.asserts.assert(node.localName == 'outerBoundaryIs',
+      'localName should be outerBoundaryIs');
+  /** @type {Array.<number>|undefined} */
+  var flatLinearRing = ol.xml.pushParseAndPop(undefined,
+      this.RING_PARSERS, node, objectStack, this);
+  if (flatLinearRing) {
+    var flatLinearRings = /** @type {Array.<Array.<number>>} */
+        (objectStack[objectStack.length - 1]);
+    goog.asserts.assert(Array.isArray(flatLinearRings),
+        'flatLinearRings should be an array');
+    goog.asserts.assert(flatLinearRings.length > 0,
+        'flatLinearRings should have an array length larger than 0');
+    flatLinearRings[0] = flatLinearRing;
+  }
+};
+
+
+/**
+ * @const
+ * @type {Object.<string, Object.<string, ol.XmlParser>>}
+ * @private
+ */
+ol.format.GML2.prototype.GEOMETRY_FLAT_COORDINATES_PARSERS_ = {
+  'http://www.opengis.net/gml' : {
+    'coordinates': ol.xml.makeReplacer(
+        ol.format.GML2.prototype.readFlatCoordinates_)
+  }
+};
+
+
+/**
+ * @const
+ * @type {Object.<string, Object.<string, ol.XmlParser>>}
+ * @private
+ */
+ol.format.GML2.prototype.FLAT_LINEAR_RINGS_PARSERS_ = {
+  'http://www.opengis.net/gml' : {
+    'innerBoundaryIs': ol.format.GML2.prototype.innerBoundaryIsParser_,
+    'outerBoundaryIs': ol.format.GML2.prototype.outerBoundaryIsParser_
+  }
+};
+
+
+/**
+ * @const
+ * @type {Object.<string, Object.<string, ol.XmlParser>>}
+ * @private
+ */
+ol.format.GML2.prototype.BOX_PARSERS_ = {
+  'http://www.opengis.net/gml' : {
+    'coordinates': ol.xml.makeArrayPusher(
+        ol.format.GML2.prototype.readFlatCoordinates_)
+  }
+};
+
+
+/**
+ * @const
+ * @type {Object.<string, Object.<string, ol.XmlParser>>}
+ * @private
+ */
+ol.format.GML2.prototype.GEOMETRY_PARSERS_ = {
+  'http://www.opengis.net/gml' : {
+    'Point': ol.xml.makeReplacer(ol.format.GMLBase.prototype.readPoint),
+    'MultiPoint': ol.xml.makeReplacer(
+        ol.format.GMLBase.prototype.readMultiPoint),
+    'LineString': ol.xml.makeReplacer(
+        ol.format.GMLBase.prototype.readLineString),
+    'MultiLineString': ol.xml.makeReplacer(
+        ol.format.GMLBase.prototype.readMultiLineString),
+    'LinearRing' : ol.xml.makeReplacer(
+        ol.format.GMLBase.prototype.readLinearRing),
+    'Polygon': ol.xml.makeReplacer(ol.format.GMLBase.prototype.readPolygon),
+    'MultiPolygon': ol.xml.makeReplacer(
+        ol.format.GMLBase.prototype.readMultiPolygon),
+    'Box': ol.xml.makeReplacer(ol.format.GML2.prototype.readBox_)
+  }
+};
+
+goog.provide('ol.format.GML');
+goog.provide('ol.format.GML3');
+
+goog.require('goog.asserts');
+goog.require('ol');
+goog.require('ol.array');
+goog.require('ol.Feature');
+goog.require('ol.extent');
+goog.require('ol.format.Feature');
+goog.require('ol.format.GMLBase');
+goog.require('ol.format.XSD');
+goog.require('ol.geom.Geometry');
+goog.require('ol.geom.GeometryLayout');
+goog.require('ol.geom.LineString');
+goog.require('ol.geom.LinearRing');
+goog.require('ol.geom.MultiLineString');
+goog.require('ol.geom.MultiPolygon');
+goog.require('ol.geom.Point');
+goog.require('ol.geom.Polygon');
+goog.require('ol.object');
+goog.require('ol.proj');
+goog.require('ol.xml');
+
+
+/**
+ * @classdesc
+ * Feature format for reading and writing data in the GML format
+ * version 3.1.1.
+ * Currently only supports GML 3.1.1 Simple Features profile.
+ *
+ * @constructor
+ * @param {olx.format.GMLOptions=} opt_options
+ *     Optional configuration object.
+ * @extends {ol.format.GMLBase}
+ * @api
+ */
+ol.format.GML3 = function(opt_options) {
+  var options = /** @type {olx.format.GMLOptions} */
+      (opt_options ? opt_options : {});
+
+  ol.format.GMLBase.call(this, options);
+
+  /**
+   * @private
+   * @type {boolean}
+   */
+  this.surface_ = options.surface !== undefined ? options.surface : false;
+
+  /**
+   * @private
+   * @type {boolean}
+   */
+  this.curve_ = options.curve !== undefined ? options.curve : false;
+
+  /**
+   * @private
+   * @type {boolean}
+   */
+  this.multiCurve_ = options.multiCurve !== undefined ?
+      options.multiCurve : true;
+
+  /**
+   * @private
+   * @type {boolean}
+   */
+  this.multiSurface_ = options.multiSurface !== undefined ?
+      options.multiSurface : true;
+
+  /**
+   * @inheritDoc
+   */
+  this.schemaLocation = options.schemaLocation ?
+      options.schemaLocation : ol.format.GML3.schemaLocation_;
+
+};
+ol.inherits(ol.format.GML3, ol.format.GMLBase);
+
+
+/**
+ * @const
+ * @type {string}
+ * @private
+ */
+ol.format.GML3.schemaLocation_ = ol.format.GMLBase.GMLNS +
+    ' http://schemas.opengis.net/gml/3.1.1/profiles/gmlsfProfile/' +
+    '1.0.0/gmlsf.xsd';
+
+
+/**
+ * @param {Node} node Node.
+ * @param {Array.<*>} objectStack Object stack.
+ * @private
+ * @return {ol.geom.MultiLineString|undefined} MultiLineString.
+ */
+ol.format.GML3.prototype.readMultiCurve_ = function(node, objectStack) {
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'node.nodeType should be ELEMENT');
+  goog.asserts.assert(node.localName == 'MultiCurve',
+      'localName should be MultiCurve');
+  /** @type {Array.<ol.geom.LineString>} */
+  var lineStrings = ol.xml.pushParseAndPop([],
+      this.MULTICURVE_PARSERS_, node, objectStack, this);
+  if (lineStrings) {
+    var multiLineString = new ol.geom.MultiLineString(null);
+    multiLineString.setLineStrings(lineStrings);
+    return multiLineString;
+  } else {
+    return undefined;
+  }
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {Array.<*>} objectStack Object stack.
+ * @private
+ * @return {ol.geom.MultiPolygon|undefined} MultiPolygon.
+ */
+ol.format.GML3.prototype.readMultiSurface_ = function(node, objectStack) {
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'node.nodeType should be ELEMENT');
+  goog.asserts.assert(node.localName == 'MultiSurface',
+      'localName should be MultiSurface');
+  /** @type {Array.<ol.geom.Polygon>} */
+  var polygons = ol.xml.pushParseAndPop([],
+      this.MULTISURFACE_PARSERS_, node, objectStack, this);
+  if (polygons) {
+    var multiPolygon = new ol.geom.MultiPolygon(null);
+    multiPolygon.setPolygons(polygons);
+    return multiPolygon;
+  } else {
+    return undefined;
+  }
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {Array.<*>} objectStack Object stack.
+ * @private
+ */
+ol.format.GML3.prototype.curveMemberParser_ = function(node, objectStack) {
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'node.nodeType should be ELEMENT');
+  goog.asserts.assert(node.localName == 'curveMember' ||
+      node.localName == 'curveMembers',
+      'localName should be curveMember or curveMembers');
+  ol.xml.parseNode(this.CURVEMEMBER_PARSERS_, node, objectStack, this);
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {Array.<*>} objectStack Object stack.
+ * @private
+ */
+ol.format.GML3.prototype.surfaceMemberParser_ = function(node, objectStack) {
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'node.nodeType should be ELEMENT');
+  goog.asserts.assert(node.localName == 'surfaceMember' ||
+      node.localName == 'surfaceMembers',
+      'localName should be surfaceMember or surfaceMembers');
+  ol.xml.parseNode(this.SURFACEMEMBER_PARSERS_,
+      node, objectStack, this);
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {Array.<*>} objectStack Object stack.
+ * @private
+ * @return {Array.<(Array.<number>)>|undefined} flat coordinates.
+ */
+ol.format.GML3.prototype.readPatch_ = function(node, objectStack) {
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'node.nodeType should be ELEMENT');
+  goog.asserts.assert(node.localName == 'patches',
+      'localName should be patches');
+  return ol.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.
+ */
+ol.format.GML3.prototype.readSegment_ = function(node, objectStack) {
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'node.nodeType should be ELEMENT');
+  goog.asserts.assert(node.localName == 'segments',
+      'localName should be segments');
+  return ol.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.
+ */
+ol.format.GML3.prototype.readPolygonPatch_ = function(node, objectStack) {
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'npde.nodeType should be ELEMENT');
+  goog.asserts.assert(node.localName == 'PolygonPatch',
+      'localName should be PolygonPatch');
+  return ol.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.
+ */
+ol.format.GML3.prototype.readLineStringSegment_ = function(node, objectStack) {
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'node.nodeType should be ELEMENT');
+  goog.asserts.assert(node.localName == 'LineStringSegment',
+      'localName should be LineStringSegment');
+  return ol.xml.pushParseAndPop([null],
+      this.GEOMETRY_FLAT_COORDINATES_PARSERS_,
+      node, objectStack, this);
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {Array.<*>} objectStack Object stack.
+ * @private
+ */
+ol.format.GML3.prototype.interiorParser_ = function(node, objectStack) {
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'node.nodeType should be ELEMENT');
+  goog.asserts.assert(node.localName == 'interior',
+      'localName should be interior');
+  /** @type {Array.<number>|undefined} */
+  var flatLinearRing = ol.xml.pushParseAndPop(undefined,
+      this.RING_PARSERS, node, objectStack, this);
+  if (flatLinearRing) {
+    var flatLinearRings = /** @type {Array.<Array.<number>>} */
+        (objectStack[objectStack.length - 1]);
+    goog.asserts.assert(Array.isArray(flatLinearRings),
+        'flatLinearRings should be an array');
+    goog.asserts.assert(flatLinearRings.length > 0,
+        'flatLinearRings should have an array length of 1 or more');
+    flatLinearRings.push(flatLinearRing);
+  }
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {Array.<*>} objectStack Object stack.
+ * @private
+ */
+ol.format.GML3.prototype.exteriorParser_ = function(node, objectStack) {
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'node.nodeType should be ELEMENT');
+  goog.asserts.assert(node.localName == 'exterior',
+      'localName should be exterior');
+   /** @type {Array.<number>|undefined} */
+  var flatLinearRing = ol.xml.pushParseAndPop(undefined,
+      this.RING_PARSERS, node, objectStack, this);
+  if (flatLinearRing) {
+    var flatLinearRings = /** @type {Array.<Array.<number>>} */
+        (objectStack[objectStack.length - 1]);
+    goog.asserts.assert(Array.isArray(flatLinearRings),
+        'flatLinearRings should be an array');
+    goog.asserts.assert(flatLinearRings.length > 0,
+        'flatLinearRings should have an array length of 1 or more');
+    flatLinearRings[0] = flatLinearRing;
+  }
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {Array.<*>} objectStack Object stack.
+ * @private
+ * @return {ol.geom.Polygon|undefined} Polygon.
+ */
+ol.format.GML3.prototype.readSurface_ = function(node, objectStack) {
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'node.nodeType should be ELEMENT');
+  goog.asserts.assert(node.localName == 'Surface',
+      'localName should be Surface');
+  /** @type {Array.<Array.<number>>} */
+  var flatLinearRings = ol.xml.pushParseAndPop([null],
+      this.SURFACE_PARSERS_, node, objectStack, this);
+  if (flatLinearRings && flatLinearRings[0]) {
+    var polygon = new ol.geom.Polygon(null);
+    var flatCoordinates = flatLinearRings[0];
+    var ends = [flatCoordinates.length];
+    var i, ii;
+    for (i = 1, ii = flatLinearRings.length; i < ii; ++i) {
+      ol.array.extend(flatCoordinates, flatLinearRings[i]);
+      ends.push(flatCoordinates.length);
+    }
+    polygon.setFlatCoordinates(
+        ol.geom.GeometryLayout.XYZ, flatCoordinates, ends);
+    return polygon;
+  } else {
+    return undefined;
+  }
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {Array.<*>} objectStack Object stack.
+ * @private
+ * @return {ol.geom.LineString|undefined} LineString.
+ */
+ol.format.GML3.prototype.readCurve_ = function(node, objectStack) {
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'node.nodeType should be ELEMENT');
+  goog.asserts.assert(node.localName == 'Curve', 'localName should be Curve');
+  /** @type {Array.<number>} */
+  var flatCoordinates = ol.xml.pushParseAndPop([null],
+      this.CURVE_PARSERS_, node, objectStack, this);
+  if (flatCoordinates) {
+    var lineString = new ol.geom.LineString(null);
+    lineString.setFlatCoordinates(ol.geom.GeometryLayout.XYZ, flatCoordinates);
+    return lineString;
+  } else {
+    return undefined;
+  }
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {Array.<*>} objectStack Object stack.
+ * @private
+ * @return {ol.Extent|undefined} Envelope.
+ */
+ol.format.GML3.prototype.readEnvelope_ = function(node, objectStack) {
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'node.nodeType should be ELEMENT');
+  goog.asserts.assert(node.localName == 'Envelope',
+      'localName should be Envelope');
+  /** @type {Array.<number>} */
+  var flatCoordinates = ol.xml.pushParseAndPop([null],
+      this.ENVELOPE_PARSERS_, node, objectStack, this);
+  return ol.extent.createOrUpdate(flatCoordinates[1][0],
+      flatCoordinates[1][1], flatCoordinates[2][0],
+      flatCoordinates[2][1]);
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {Array.<*>} objectStack Object stack.
+ * @private
+ * @return {Array.<number>|undefined} Flat coordinates.
+ */
+ol.format.GML3.prototype.readFlatPos_ = function(node, objectStack) {
+  var s = ol.xml.getAllTextContent(node, false);
+  var re = /^\s*([+\-]?\d*\.?\d+(?:[eE][+\-]?\d+)?)\s*/;
+  /** @type {Array.<number>} */
+  var flatCoordinates = [];
+  var m;
+  while ((m = re.exec(s))) {
+    flatCoordinates.push(parseFloat(m[1]));
+    s = s.substr(m[0].length);
+  }
+  if (s !== '') {
+    return undefined;
+  }
+  var context = objectStack[0];
+  goog.asserts.assert(goog.isObject(context), 'context should be an Object');
+  var containerSrs = context['srsName'];
+  var axisOrientation = 'enu';
+  if (containerSrs) {
+    var proj = ol.proj.get(containerSrs);
+    axisOrientation = proj.getAxisOrientation();
+  }
+  if (axisOrientation === 'neu') {
+    var i, ii;
+    for (i = 0, ii = flatCoordinates.length; i < ii; i += 3) {
+      var y = flatCoordinates[i];
+      var x = flatCoordinates[i + 1];
+      flatCoordinates[i] = x;
+      flatCoordinates[i + 1] = y;
+    }
+  }
+  var len = flatCoordinates.length;
+  if (len == 2) {
+    flatCoordinates.push(0);
+  }
+  if (len === 0) {
+    return undefined;
+  }
+  return flatCoordinates;
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {Array.<*>} objectStack Object stack.
+ * @private
+ * @return {Array.<number>|undefined} Flat coordinates.
+ */
+ol.format.GML3.prototype.readFlatPosList_ = function(node, objectStack) {
+  var s = ol.xml.getAllTextContent(node, false).replace(/^\s*|\s*$/g, '');
+  var context = objectStack[0];
+  goog.asserts.assert(goog.isObject(context), 'context should be an Object');
+  var containerSrs = context['srsName'];
+  var containerDimension = node.parentNode.getAttribute('srsDimension');
+  var axisOrientation = 'enu';
+  if (containerSrs) {
+    var proj = ol.proj.get(containerSrs);
+    axisOrientation = proj.getAxisOrientation();
+  }
+  var coords = s.split(/\s+/);
+  // The "dimension" attribute is from the GML 3.0.1 spec.
+  var dim = 2;
+  if (node.getAttribute('srsDimension')) {
+    dim = ol.format.XSD.readNonNegativeIntegerString(
+        node.getAttribute('srsDimension'));
+  } else if (node.getAttribute('dimension')) {
+    dim = ol.format.XSD.readNonNegativeIntegerString(
+        node.getAttribute('dimension'));
+  } else if (containerDimension) {
+    dim = ol.format.XSD.readNonNegativeIntegerString(containerDimension);
+  }
+  var x, y, z;
+  var flatCoordinates = [];
+  for (var i = 0, ii = coords.length; i < ii; i += dim) {
+    x = parseFloat(coords[i]);
+    y = parseFloat(coords[i + 1]);
+    z = (dim === 3) ? parseFloat(coords[i + 2]) : 0;
+    if (axisOrientation.substr(0, 2) === 'en') {
+      flatCoordinates.push(x, y, z);
+    } else {
+      flatCoordinates.push(y, x, z);
+    }
+  }
+  return flatCoordinates;
+};
+
+
+/**
+ * @const
+ * @type {Object.<string, Object.<string, ol.XmlParser>>}
+ * @private
+ */
+ol.format.GML3.prototype.GEOMETRY_FLAT_COORDINATES_PARSERS_ = {
+  'http://www.opengis.net/gml' : {
+    'pos': ol.xml.makeReplacer(ol.format.GML3.prototype.readFlatPos_),
+    'posList': ol.xml.makeReplacer(ol.format.GML3.prototype.readFlatPosList_)
+  }
+};
+
+
+/**
+ * @const
+ * @type {Object.<string, Object.<string, ol.XmlParser>>}
+ * @private
+ */
+ol.format.GML3.prototype.FLAT_LINEAR_RINGS_PARSERS_ = {
+  'http://www.opengis.net/gml' : {
+    'interior': ol.format.GML3.prototype.interiorParser_,
+    'exterior': ol.format.GML3.prototype.exteriorParser_
+  }
+};
+
+
+/**
+ * @const
+ * @type {Object.<string, Object.<string, ol.XmlParser>>}
+ * @private
+ */
+ol.format.GML3.prototype.GEOMETRY_PARSERS_ = {
+  'http://www.opengis.net/gml' : {
+    'Point': ol.xml.makeReplacer(ol.format.GMLBase.prototype.readPoint),
+    'MultiPoint': ol.xml.makeReplacer(
+        ol.format.GMLBase.prototype.readMultiPoint),
+    'LineString': ol.xml.makeReplacer(
+        ol.format.GMLBase.prototype.readLineString),
+    'MultiLineString': ol.xml.makeReplacer(
+        ol.format.GMLBase.prototype.readMultiLineString),
+    'LinearRing' : ol.xml.makeReplacer(
+        ol.format.GMLBase.prototype.readLinearRing),
+    'Polygon': ol.xml.makeReplacer(ol.format.GMLBase.prototype.readPolygon),
+    'MultiPolygon': ol.xml.makeReplacer(
+        ol.format.GMLBase.prototype.readMultiPolygon),
+    'Surface': ol.xml.makeReplacer(ol.format.GML3.prototype.readSurface_),
+    'MultiSurface': ol.xml.makeReplacer(
+        ol.format.GML3.prototype.readMultiSurface_),
+    'Curve': ol.xml.makeReplacer(ol.format.GML3.prototype.readCurve_),
+    'MultiCurve': ol.xml.makeReplacer(
+        ol.format.GML3.prototype.readMultiCurve_),
+    'Envelope': ol.xml.makeReplacer(ol.format.GML3.prototype.readEnvelope_)
+  }
+};
+
+
+/**
+ * @const
+ * @type {Object.<string, Object.<string, ol.XmlParser>>}
+ * @private
+ */
+ol.format.GML3.prototype.MULTICURVE_PARSERS_ = {
+  'http://www.opengis.net/gml' : {
+    'curveMember': ol.xml.makeArrayPusher(
+        ol.format.GML3.prototype.curveMemberParser_),
+    'curveMembers': ol.xml.makeArrayPusher(
+        ol.format.GML3.prototype.curveMemberParser_)
+  }
+};
+
+
+/**
+ * @const
+ * @type {Object.<string, Object.<string, ol.XmlParser>>}
+ * @private
+ */
+ol.format.GML3.prototype.MULTISURFACE_PARSERS_ = {
+  'http://www.opengis.net/gml' : {
+    'surfaceMember': ol.xml.makeArrayPusher(
+        ol.format.GML3.prototype.surfaceMemberParser_),
+    'surfaceMembers': ol.xml.makeArrayPusher(
+        ol.format.GML3.prototype.surfaceMemberParser_)
+  }
+};
+
+
+/**
+ * @const
+ * @type {Object.<string, Object.<string, ol.XmlParser>>}
+ * @private
+ */
+ol.format.GML3.prototype.CURVEMEMBER_PARSERS_ = {
+  'http://www.opengis.net/gml' : {
+    'LineString': ol.xml.makeArrayPusher(
+        ol.format.GMLBase.prototype.readLineString),
+    'Curve': ol.xml.makeArrayPusher(ol.format.GML3.prototype.readCurve_)
+  }
+};
+
+
+/**
+ * @const
+ * @type {Object.<string, Object.<string, ol.XmlParser>>}
+ * @private
+ */
+ol.format.GML3.prototype.SURFACEMEMBER_PARSERS_ = {
+  'http://www.opengis.net/gml' : {
+    'Polygon': ol.xml.makeArrayPusher(ol.format.GMLBase.prototype.readPolygon),
+    'Surface': ol.xml.makeArrayPusher(ol.format.GML3.prototype.readSurface_)
+  }
+};
+
+
+/**
+ * @const
+ * @type {Object.<string, Object.<string, ol.XmlParser>>}
+ * @private
+ */
+ol.format.GML3.prototype.SURFACE_PARSERS_ = {
+  'http://www.opengis.net/gml' : {
+    'patches': ol.xml.makeReplacer(ol.format.GML3.prototype.readPatch_)
+  }
+};
+
+
+/**
+ * @const
+ * @type {Object.<string, Object.<string, ol.XmlParser>>}
+ * @private
+ */
+ol.format.GML3.prototype.CURVE_PARSERS_ = {
+  'http://www.opengis.net/gml' : {
+    'segments': ol.xml.makeReplacer(ol.format.GML3.prototype.readSegment_)
+  }
+};
+
+
+/**
+ * @const
+ * @type {Object.<string, Object.<string, ol.XmlParser>>}
+ * @private
+ */
+ol.format.GML3.prototype.ENVELOPE_PARSERS_ = {
+  'http://www.opengis.net/gml' : {
+    'lowerCorner': ol.xml.makeArrayPusher(
+        ol.format.GML3.prototype.readFlatPosList_),
+    'upperCorner': ol.xml.makeArrayPusher(
+        ol.format.GML3.prototype.readFlatPosList_)
+  }
+};
+
+
+/**
+ * @const
+ * @type {Object.<string, Object.<string, ol.XmlParser>>}
+ * @private
+ */
+ol.format.GML3.prototype.PATCHES_PARSERS_ = {
+  'http://www.opengis.net/gml' : {
+    'PolygonPatch': ol.xml.makeReplacer(
+        ol.format.GML3.prototype.readPolygonPatch_)
+  }
+};
+
+
+/**
+ * @const
+ * @type {Object.<string, Object.<string, ol.XmlParser>>}
+ * @private
+ */
+ol.format.GML3.prototype.SEGMENTS_PARSERS_ = {
+  'http://www.opengis.net/gml' : {
+    'LineStringSegment': ol.xml.makeReplacer(
+        ol.format.GML3.prototype.readLineStringSegment_)
+  }
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {ol.geom.Point} value Point geometry.
+ * @param {Array.<*>} objectStack Node stack.
+ * @private
+ */
+ol.format.GML3.prototype.writePos_ = function(node, value, objectStack) {
+  var context = objectStack[objectStack.length - 1];
+  goog.asserts.assert(goog.isObject(context), 'context should be an Object');
+  var srsName = context['srsName'];
+  var axisOrientation = 'enu';
+  if (srsName) {
+    axisOrientation = ol.proj.get(srsName).getAxisOrientation();
+  }
+  var point = value.getCoordinates();
+  var coords;
+  // only 2d for simple features profile
+  if (axisOrientation.substr(0, 2) === 'en') {
+    coords = (point[0] + ' ' + point[1]);
+  } else {
+    coords = (point[1] + ' ' + point[0]);
+  }
+  ol.format.XSD.writeStringTextNode(node, coords);
+};
+
+
+/**
+ * @param {Array.<number>} point Point geometry.
+ * @param {string=} opt_srsName Optional srsName
+ * @return {string} The coords string.
+ * @private
+ */
+ol.format.GML3.prototype.getCoords_ = function(point, opt_srsName) {
+  var axisOrientation = 'enu';
+  if (opt_srsName) {
+    axisOrientation = ol.proj.get(opt_srsName).getAxisOrientation();
+  }
+  return ((axisOrientation.substr(0, 2) === 'en') ?
+      point[0] + ' ' + point[1] :
+      point[1] + ' ' + point[0]);
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {ol.geom.LineString|ol.geom.LinearRing} value Geometry.
+ * @param {Array.<*>} objectStack Node stack.
+ * @private
+ */
+ol.format.GML3.prototype.writePosList_ = function(node, value, objectStack) {
+  var context = objectStack[objectStack.length - 1];
+  goog.asserts.assert(goog.isObject(context), 'context should be an Object');
+  var srsName = context['srsName'];
+  // only 2d for simple features profile
+  var points = value.getCoordinates();
+  var len = points.length;
+  var parts = new Array(len);
+  var point;
+  for (var i = 0; i < len; ++i) {
+    point = points[i];
+    parts[i] = this.getCoords_(point, srsName);
+  }
+  ol.format.XSD.writeStringTextNode(node, parts.join(' '));
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {ol.geom.Point} geometry Point geometry.
+ * @param {Array.<*>} objectStack Node stack.
+ * @private
+ */
+ol.format.GML3.prototype.writePoint_ = function(node, geometry, objectStack) {
+  var context = objectStack[objectStack.length - 1];
+  goog.asserts.assert(goog.isObject(context), 'context should be an Object');
+  var srsName = context['srsName'];
+  if (srsName) {
+    node.setAttribute('srsName', srsName);
+  }
+  var pos = ol.xml.createElementNS(node.namespaceURI, 'pos');
+  node.appendChild(pos);
+  this.writePos_(pos, geometry, objectStack);
+};
+
+
+/**
+ * @type {Object.<string, Object.<string, ol.XmlSerializer>>}
+ * @private
+ */
+ol.format.GML3.ENVELOPE_SERIALIZERS_ = {
+  'http://www.opengis.net/gml': {
+    'lowerCorner': ol.xml.makeChildAppender(ol.format.XSD.writeStringTextNode),
+    'upperCorner': ol.xml.makeChildAppender(ol.format.XSD.writeStringTextNode)
+  }
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {ol.Extent} extent Extent.
+ * @param {Array.<*>} objectStack Node stack.
+ */
+ol.format.GML3.prototype.writeEnvelope = function(node, extent, objectStack) {
+  goog.asserts.assert(extent.length == 4, 'extent should have 4 items');
+  var context = objectStack[objectStack.length - 1];
+  goog.asserts.assert(goog.isObject(context), 'context should be an Object');
+  var srsName = context['srsName'];
+  if (srsName) {
+    node.setAttribute('srsName', srsName);
+  }
+  var keys = ['lowerCorner', 'upperCorner'];
+  var values = [extent[0] + ' ' + extent[1], extent[2] + ' ' + extent[3]];
+  ol.xml.pushSerializeAndPop(/** @type {ol.XmlNodeStackItem} */
+      ({node: node}), ol.format.GML3.ENVELOPE_SERIALIZERS_,
+      ol.xml.OBJECT_PROPERTY_NODE_FACTORY,
+      values,
+      objectStack, keys, this);
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {ol.geom.LinearRing} geometry LinearRing geometry.
+ * @param {Array.<*>} objectStack Node stack.
+ * @private
+ */
+ol.format.GML3.prototype.writeLinearRing_ = function(node, geometry, objectStack) {
+  var context = objectStack[objectStack.length - 1];
+  goog.asserts.assert(goog.isObject(context), 'context should be an Object');
+  var srsName = context['srsName'];
+  if (srsName) {
+    node.setAttribute('srsName', srsName);
+  }
+  var posList = ol.xml.createElementNS(node.namespaceURI, 'posList');
+  node.appendChild(posList);
+  this.writePosList_(posList, geometry, objectStack);
+};
+
+
+/**
+ * @param {*} value Value.
+ * @param {Array.<*>} objectStack Object stack.
+ * @param {string=} opt_nodeName Node name.
+ * @return {Node} Node.
+ * @private
+ */
+ol.format.GML3.prototype.RING_NODE_FACTORY_ = function(value, objectStack, opt_nodeName) {
+  var context = objectStack[objectStack.length - 1];
+  var parentNode = context.node;
+  goog.asserts.assert(goog.isObject(context), 'context should be an Object');
+  var exteriorWritten = context['exteriorWritten'];
+  if (exteriorWritten === undefined) {
+    context['exteriorWritten'] = true;
+  }
+  return ol.xml.createElementNS(parentNode.namespaceURI,
+      exteriorWritten !== undefined ? 'interior' : 'exterior');
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {ol.geom.Polygon} geometry Polygon geometry.
+ * @param {Array.<*>} objectStack Node stack.
+ * @private
+ */
+ol.format.GML3.prototype.writeSurfaceOrPolygon_ = function(node, geometry, objectStack) {
+  var context = objectStack[objectStack.length - 1];
+  goog.asserts.assert(goog.isObject(context), 'context should be an Object');
+  var srsName = context['srsName'];
+  if (node.nodeName !== 'PolygonPatch' && srsName) {
+    node.setAttribute('srsName', srsName);
+  }
+  if (node.nodeName === 'Polygon' || node.nodeName === 'PolygonPatch') {
+    var rings = geometry.getLinearRings();
+    ol.xml.pushSerializeAndPop(
+        {node: node, srsName: srsName},
+        ol.format.GML3.RING_SERIALIZERS_,
+        this.RING_NODE_FACTORY_,
+        rings, objectStack, undefined, this);
+  } else if (node.nodeName === 'Surface') {
+    var patches = ol.xml.createElementNS(node.namespaceURI, 'patches');
+    node.appendChild(patches);
+    this.writeSurfacePatches_(
+        patches, geometry, objectStack);
+  }
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {ol.geom.LineString} geometry LineString geometry.
+ * @param {Array.<*>} objectStack Node stack.
+ * @private
+ */
+ol.format.GML3.prototype.writeCurveOrLineString_ = function(node, geometry, objectStack) {
+  var context = objectStack[objectStack.length - 1];
+  goog.asserts.assert(goog.isObject(context), 'context should be an Object');
+  var srsName = context['srsName'];
+  if (node.nodeName !== 'LineStringSegment' && srsName) {
+    node.setAttribute('srsName', srsName);
+  }
+  if (node.nodeName === 'LineString' ||
+      node.nodeName === 'LineStringSegment') {
+    var posList = ol.xml.createElementNS(node.namespaceURI, 'posList');
+    node.appendChild(posList);
+    this.writePosList_(posList, geometry, objectStack);
+  } else if (node.nodeName === 'Curve') {
+    var segments = ol.xml.createElementNS(node.namespaceURI, 'segments');
+    node.appendChild(segments);
+    this.writeCurveSegments_(segments,
+        geometry, objectStack);
+  }
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {ol.geom.MultiPolygon} geometry MultiPolygon geometry.
+ * @param {Array.<*>} objectStack Node stack.
+ * @private
+ */
+ol.format.GML3.prototype.writeMultiSurfaceOrPolygon_ = function(node, geometry, objectStack) {
+  var context = objectStack[objectStack.length - 1];
+  goog.asserts.assert(goog.isObject(context), 'context should be an Object');
+  var srsName = context['srsName'];
+  var surface = context['surface'];
+  if (srsName) {
+    node.setAttribute('srsName', srsName);
+  }
+  var polygons = geometry.getPolygons();
+  ol.xml.pushSerializeAndPop({node: node, srsName: srsName, surface: surface},
+      ol.format.GML3.SURFACEORPOLYGONMEMBER_SERIALIZERS_,
+      this.MULTIGEOMETRY_MEMBER_NODE_FACTORY_, polygons,
+      objectStack, undefined, this);
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {ol.geom.MultiPoint} geometry MultiPoint geometry.
+ * @param {Array.<*>} objectStack Node stack.
+ * @private
+ */
+ol.format.GML3.prototype.writeMultiPoint_ = function(node, geometry,
+    objectStack) {
+  var context = objectStack[objectStack.length - 1];
+  goog.asserts.assert(goog.isObject(context), 'context should be an Object');
+  var srsName = context['srsName'];
+  if (srsName) {
+    node.setAttribute('srsName', srsName);
+  }
+  var points = geometry.getPoints();
+  ol.xml.pushSerializeAndPop({node: node, srsName: srsName},
+      ol.format.GML3.POINTMEMBER_SERIALIZERS_,
+      ol.xml.makeSimpleNodeFactory('pointMember'), points,
+      objectStack, undefined, this);
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {ol.geom.MultiLineString} geometry MultiLineString geometry.
+ * @param {Array.<*>} objectStack Node stack.
+ * @private
+ */
+ol.format.GML3.prototype.writeMultiCurveOrLineString_ = function(node, geometry, objectStack) {
+  var context = objectStack[objectStack.length - 1];
+  goog.asserts.assert(goog.isObject(context), 'context should be an Object');
+  var srsName = context['srsName'];
+  var curve = context['curve'];
+  if (srsName) {
+    node.setAttribute('srsName', srsName);
+  }
+  var lines = geometry.getLineStrings();
+  ol.xml.pushSerializeAndPop({node: node, srsName: srsName, curve: curve},
+      ol.format.GML3.LINESTRINGORCURVEMEMBER_SERIALIZERS_,
+      this.MULTIGEOMETRY_MEMBER_NODE_FACTORY_, lines,
+      objectStack, undefined, this);
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {ol.geom.LinearRing} ring LinearRing geometry.
+ * @param {Array.<*>} objectStack Node stack.
+ * @private
+ */
+ol.format.GML3.prototype.writeRing_ = function(node, ring, objectStack) {
+  var linearRing = ol.xml.createElementNS(node.namespaceURI, 'LinearRing');
+  node.appendChild(linearRing);
+  this.writeLinearRing_(linearRing, ring, objectStack);
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {ol.geom.Polygon} polygon Polygon geometry.
+ * @param {Array.<*>} objectStack Node stack.
+ * @private
+ */
+ol.format.GML3.prototype.writeSurfaceOrPolygonMember_ = function(node, polygon, objectStack) {
+  var context = objectStack[objectStack.length - 1];
+  goog.asserts.assert(goog.isObject(context), 'context should be an Object');
+  var child = this.GEOMETRY_NODE_FACTORY_(
+      polygon, objectStack);
+  if (child) {
+    node.appendChild(child);
+    this.writeSurfaceOrPolygon_(child, polygon, objectStack);
+  }
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {ol.geom.Point} point Point geometry.
+ * @param {Array.<*>} objectStack Node stack.
+ * @private
+ */
+ol.format.GML3.prototype.writePointMember_ = function(node, point, objectStack) {
+  var child = ol.xml.createElementNS(node.namespaceURI, 'Point');
+  node.appendChild(child);
+  this.writePoint_(child, point, objectStack);
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {ol.geom.LineString} line LineString geometry.
+ * @param {Array.<*>} objectStack Node stack.
+ * @private
+ */
+ol.format.GML3.prototype.writeLineStringOrCurveMember_ = function(node, line, objectStack) {
+  var context = objectStack[objectStack.length - 1];
+  goog.asserts.assert(goog.isObject(context), 'context should be an Object');
+  var child = this.GEOMETRY_NODE_FACTORY_(line, objectStack);
+  if (child) {
+    node.appendChild(child);
+    this.writeCurveOrLineString_(child, line, objectStack);
+  }
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {ol.geom.Polygon} polygon Polygon geometry.
+ * @param {Array.<*>} objectStack Node stack.
+ * @private
+ */
+ol.format.GML3.prototype.writeSurfacePatches_ = function(node, polygon, objectStack) {
+  var child = ol.xml.createElementNS(node.namespaceURI, 'PolygonPatch');
+  node.appendChild(child);
+  this.writeSurfaceOrPolygon_(child, polygon, objectStack);
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {ol.geom.LineString} line LineString geometry.
+ * @param {Array.<*>} objectStack Node stack.
+ * @private
+ */
+ol.format.GML3.prototype.writeCurveSegments_ = function(node, line, objectStack) {
+  var child = ol.xml.createElementNS(node.namespaceURI,
+      'LineStringSegment');
+  node.appendChild(child);
+  this.writeCurveOrLineString_(child, line, objectStack);
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {ol.geom.Geometry|ol.Extent} geometry Geometry.
+ * @param {Array.<*>} objectStack Node stack.
+ */
+ol.format.GML3.prototype.writeGeometryElement = function(node, geometry, objectStack) {
+  var context = objectStack[objectStack.length - 1];
+  goog.asserts.assert(goog.isObject(context), 'context should be an Object');
+  var item = ol.object.assign({}, context);
+  item.node = node;
+  var value;
+  if (Array.isArray(geometry)) {
+    if (context.dataProjection) {
+      value = ol.proj.transformExtent(
+          geometry, context.featureProjection, context.dataProjection);
+    } else {
+      value = geometry;
+    }
+  } else {
+    goog.asserts.assertInstanceof(geometry, ol.geom.Geometry,
+        'geometry should be an ol.geom.Geometry');
+    value =
+        ol.format.Feature.transformWithOptions(geometry, true, context);
+  }
+  ol.xml.pushSerializeAndPop(/** @type {ol.XmlNodeStackItem} */
+      (item), ol.format.GML3.GEOMETRY_SERIALIZERS_,
+      this.GEOMETRY_NODE_FACTORY_, [value],
+      objectStack, undefined, this);
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {ol.Feature} feature Feature.
+ * @param {Array.<*>} objectStack Node stack.
+ */
+ol.format.GML3.prototype.writeFeatureElement = function(node, feature, objectStack) {
+  var fid = feature.getId();
+  if (fid) {
+    node.setAttribute('fid', fid);
+  }
+  var context = objectStack[objectStack.length - 1];
+  goog.asserts.assert(goog.isObject(context), 'context should be an Object');
+  var featureNS = context['featureNS'];
+  var geometryName = feature.getGeometryName();
+  if (!context.serializers) {
+    context.serializers = {};
+    context.serializers[featureNS] = {};
+  }
+  var properties = feature.getProperties();
+  var keys = [], values = [];
+  for (var key in properties) {
+    var value = properties[key];
+    if (value !== null) {
+      keys.push(key);
+      values.push(value);
+      if (key == geometryName || value instanceof ol.geom.Geometry) {
+        if (!(key in context.serializers[featureNS])) {
+          context.serializers[featureNS][key] = ol.xml.makeChildAppender(
+              this.writeGeometryElement, this);
+        }
+      } else {
+        if (!(key in context.serializers[featureNS])) {
+          context.serializers[featureNS][key] = ol.xml.makeChildAppender(
+              ol.format.XSD.writeStringTextNode);
+        }
+      }
+    }
+  }
+  var item = ol.object.assign({}, context);
+  item.node = node;
+  ol.xml.pushSerializeAndPop(/** @type {ol.XmlNodeStackItem} */
+      (item), context.serializers,
+      ol.xml.makeSimpleNodeFactory(undefined, featureNS),
+      values,
+      objectStack, keys);
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {Array.<ol.Feature>} features Features.
+ * @param {Array.<*>} objectStack Node stack.
+ * @private
+ */
+ol.format.GML3.prototype.writeFeatureMembers_ = function(node, features, objectStack) {
+  var context = objectStack[objectStack.length - 1];
+  goog.asserts.assert(goog.isObject(context), 'context should be an Object');
+  var featureType = context['featureType'];
+  var featureNS = context['featureNS'];
+  var serializers = {};
+  serializers[featureNS] = {};
+  serializers[featureNS][featureType] = ol.xml.makeChildAppender(
+      this.writeFeatureElement, this);
+  var item = ol.object.assign({}, context);
+  item.node = node;
+  ol.xml.pushSerializeAndPop(/** @type {ol.XmlNodeStackItem} */
+      (item),
+      serializers,
+      ol.xml.makeSimpleNodeFactory(featureType, featureNS), features,
+      objectStack);
+};
+
+
+/**
+ * @type {Object.<string, Object.<string, ol.XmlSerializer>>}
+ * @private
+ */
+ol.format.GML3.SURFACEORPOLYGONMEMBER_SERIALIZERS_ = {
+  'http://www.opengis.net/gml': {
+    'surfaceMember': ol.xml.makeChildAppender(
+        ol.format.GML3.prototype.writeSurfaceOrPolygonMember_),
+    'polygonMember': ol.xml.makeChildAppender(
+        ol.format.GML3.prototype.writeSurfaceOrPolygonMember_)
+  }
+};
+
+
+/**
+ * @type {Object.<string, Object.<string, ol.XmlSerializer>>}
+ * @private
+ */
+ol.format.GML3.POINTMEMBER_SERIALIZERS_ = {
+  'http://www.opengis.net/gml': {
+    'pointMember': ol.xml.makeChildAppender(
+        ol.format.GML3.prototype.writePointMember_)
+  }
+};
+
+
+/**
+ * @type {Object.<string, Object.<string, ol.XmlSerializer>>}
+ * @private
+ */
+ol.format.GML3.LINESTRINGORCURVEMEMBER_SERIALIZERS_ = {
+  'http://www.opengis.net/gml': {
+    'lineStringMember': ol.xml.makeChildAppender(
+        ol.format.GML3.prototype.writeLineStringOrCurveMember_),
+    'curveMember': ol.xml.makeChildAppender(
+        ol.format.GML3.prototype.writeLineStringOrCurveMember_)
+  }
+};
+
+
+/**
+ * @type {Object.<string, Object.<string, ol.XmlSerializer>>}
+ * @private
+ */
+ol.format.GML3.RING_SERIALIZERS_ = {
+  'http://www.opengis.net/gml': {
+    'exterior': ol.xml.makeChildAppender(ol.format.GML3.prototype.writeRing_),
+    'interior': ol.xml.makeChildAppender(ol.format.GML3.prototype.writeRing_)
+  }
+};
+
+
+/**
+ * @type {Object.<string, Object.<string, ol.XmlSerializer>>}
+ * @private
+ */
+ol.format.GML3.GEOMETRY_SERIALIZERS_ = {
+  'http://www.opengis.net/gml': {
+    'Curve': ol.xml.makeChildAppender(
+        ol.format.GML3.prototype.writeCurveOrLineString_),
+    'MultiCurve': ol.xml.makeChildAppender(
+        ol.format.GML3.prototype.writeMultiCurveOrLineString_),
+    'Point': ol.xml.makeChildAppender(ol.format.GML3.prototype.writePoint_),
+    'MultiPoint': ol.xml.makeChildAppender(
+        ol.format.GML3.prototype.writeMultiPoint_),
+    'LineString': ol.xml.makeChildAppender(
+        ol.format.GML3.prototype.writeCurveOrLineString_),
+    'MultiLineString': ol.xml.makeChildAppender(
+        ol.format.GML3.prototype.writeMultiCurveOrLineString_),
+    'LinearRing': ol.xml.makeChildAppender(
+        ol.format.GML3.prototype.writeLinearRing_),
+    'Polygon': ol.xml.makeChildAppender(
+        ol.format.GML3.prototype.writeSurfaceOrPolygon_),
+    'MultiPolygon': ol.xml.makeChildAppender(
+        ol.format.GML3.prototype.writeMultiSurfaceOrPolygon_),
+    'Surface': ol.xml.makeChildAppender(
+        ol.format.GML3.prototype.writeSurfaceOrPolygon_),
+    'MultiSurface': ol.xml.makeChildAppender(
+        ol.format.GML3.prototype.writeMultiSurfaceOrPolygon_),
+    'Envelope': ol.xml.makeChildAppender(
+        ol.format.GML3.prototype.writeEnvelope)
+  }
+};
+
+
+/**
+ * @const
+ * @type {Object.<string, string>}
+ * @private
+ */
+ol.format.GML3.MULTIGEOMETRY_TO_MEMBER_NODENAME_ = {
+  'MultiLineString': 'lineStringMember',
+  'MultiCurve': 'curveMember',
+  'MultiPolygon': 'polygonMember',
+  'MultiSurface': 'surfaceMember'
+};
+
+
+/**
+ * @const
+ * @param {*} value Value.
+ * @param {Array.<*>} objectStack Object stack.
+ * @param {string=} opt_nodeName Node name.
+ * @return {Node|undefined} Node.
+ * @private
+ */
+ol.format.GML3.prototype.MULTIGEOMETRY_MEMBER_NODE_FACTORY_ = function(value, objectStack, opt_nodeName) {
+  var parentNode = objectStack[objectStack.length - 1].node;
+  goog.asserts.assert(ol.xml.isNode(parentNode),
+      'parentNode should be a node');
+  return ol.xml.createElementNS('http://www.opengis.net/gml',
+      ol.format.GML3.MULTIGEOMETRY_TO_MEMBER_NODENAME_[parentNode.nodeName]);
+};
+
+
+/**
+ * @const
+ * @param {*} value Value.
+ * @param {Array.<*>} objectStack Object stack.
+ * @param {string=} opt_nodeName Node name.
+ * @return {Node|undefined} Node.
+ * @private
+ */
+ol.format.GML3.prototype.GEOMETRY_NODE_FACTORY_ = function(value, objectStack, opt_nodeName) {
+  var context = objectStack[objectStack.length - 1];
+  goog.asserts.assert(goog.isObject(context), 'context should be an Object');
+  var multiSurface = context['multiSurface'];
+  var surface = context['surface'];
+  var curve = context['curve'];
+  var multiCurve = context['multiCurve'];
+  var parentNode = objectStack[objectStack.length - 1].node;
+  goog.asserts.assert(ol.xml.isNode(parentNode),
+      'parentNode should be a node');
+  var nodeName;
+  if (!Array.isArray(value)) {
+    goog.asserts.assertInstanceof(value, ol.geom.Geometry,
+        'value should be an ol.geom.Geometry');
+    nodeName = value.getType();
+    if (nodeName === 'MultiPolygon' && multiSurface === true) {
+      nodeName = 'MultiSurface';
+    } else if (nodeName === 'Polygon' && surface === true) {
+      nodeName = 'Surface';
+    } else if (nodeName === 'LineString' && curve === true) {
+      nodeName = 'Curve';
+    } else if (nodeName === 'MultiLineString' && multiCurve === true) {
+      nodeName = 'MultiCurve';
+    }
+  } else {
+    nodeName = 'Envelope';
+  }
+  return ol.xml.createElementNS('http://www.opengis.net/gml',
+      nodeName);
+};
+
+
+/**
+ * Encode a geometry in GML 3.1.1 Simple Features.
+ *
+ * @param {ol.geom.Geometry} geometry Geometry.
+ * @param {olx.format.WriteOptions=} opt_options Options.
+ * @return {Node} Node.
+ * @api
+ */
+ol.format.GML3.prototype.writeGeometryNode = function(geometry, opt_options) {
+  opt_options = this.adaptOptions(opt_options);
+  var geom = ol.xml.createElementNS('http://www.opengis.net/gml', 'geom');
+  var context = {node: geom, srsName: this.srsName,
+    curve: this.curve_, surface: this.surface_,
+    multiSurface: this.multiSurface_, multiCurve: this.multiCurve_};
+  if (opt_options) {
+    ol.object.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.<ol.Feature>} features Features.
+ * @param {olx.format.WriteOptions=} opt_options Options.
+ * @return {string} Result.
+ * @api stable
+ */
+ol.format.GML3.prototype.writeFeatures;
+
+
+/**
+ * Encode an array of features in the GML 3.1.1 format as an XML node.
+ *
+ * @param {Array.<ol.Feature>} features Features.
+ * @param {olx.format.WriteOptions=} opt_options Options.
+ * @return {Node} Node.
+ * @api
+ */
+ol.format.GML3.prototype.writeFeaturesNode = function(features, opt_options) {
+  opt_options = this.adaptOptions(opt_options);
+  var node = ol.xml.createElementNS('http://www.opengis.net/gml',
+      'featureMembers');
+  ol.xml.setAttributeNS(node, 'http://www.w3.org/2001/XMLSchema-instance',
+      'xsi:schemaLocation', this.schemaLocation);
+  var context = {
+    srsName: this.srsName,
+    curve: this.curve_,
+    surface: this.surface_,
+    multiSurface: this.multiSurface_,
+    multiCurve: this.multiCurve_,
+    featureNS: this.featureNS,
+    featureType: this.featureType
+  };
+  if (opt_options) {
+    ol.object.assign(context, opt_options);
+  }
+  this.writeFeatureMembers_(node, features, [context]);
+  return node;
+};
+
+
+/**
+ * @classdesc
+ * Feature format for reading and writing data in the GML format
+ * version 3.1.1.
+ * Currently only supports GML 3.1.1 Simple Features profile.
+ *
+ * @constructor
+ * @param {olx.format.GMLOptions=} opt_options
+ *     Optional configuration object.
+ * @extends {ol.format.GMLBase}
+ * @api stable
+ */
+ol.format.GML = ol.format.GML3;
+
+
+/**
+ * Encode an array of features in GML 3.1.1 Simple Features.
+ *
+ * @function
+ * @param {Array.<ol.Feature>} features Features.
+ * @param {olx.format.WriteOptions=} opt_options Options.
+ * @return {string} Result.
+ * @api stable
+ */
+ol.format.GML.prototype.writeFeatures;
+
+
+/**
+ * Encode an array of features in the GML 3.1.1 format as an XML node.
+ *
+ * @function
+ * @param {Array.<ol.Feature>} features Features.
+ * @param {olx.format.WriteOptions=} opt_options Options.
+ * @return {Node} Node.
+ * @api
+ */
+ol.format.GML.prototype.writeFeaturesNode;
+
+goog.provide('ol.format.GPX');
+
+goog.require('goog.asserts');
+goog.require('ol.Feature');
+goog.require('ol.array');
+goog.require('ol.format.Feature');
+goog.require('ol.format.XMLFeature');
+goog.require('ol.format.XSD');
+goog.require('ol.geom.GeometryLayout');
+goog.require('ol.geom.LineString');
+goog.require('ol.geom.MultiLineString');
+goog.require('ol.geom.Point');
+goog.require('ol.proj');
+goog.require('ol.xml');
+
+
+/**
+ * @classdesc
+ * Feature format for reading and writing data in the GPX format.
+ *
+ * @constructor
+ * @extends {ol.format.XMLFeature}
+ * @param {olx.format.GPXOptions=} opt_options Options.
+ * @api stable
+ */
+ol.format.GPX = function(opt_options) {
+
+  var options = opt_options ? opt_options : {};
+
+  ol.format.XMLFeature.call(this);
+
+  /**
+   * @inheritDoc
+   */
+  this.defaultDataProjection = ol.proj.get('EPSG:4326');
+
+  /**
+   * @type {function(ol.Feature, Node)|undefined}
+   * @private
+   */
+  this.readExtensions_ = options.readExtensions;
+};
+ol.inherits(ol.format.GPX, ol.format.XMLFeature);
+
+
+/**
+ * @const
+ * @private
+ * @type {Array.<string>}
+ */
+ol.format.GPX.NAMESPACE_URIS_ = [
+  null,
+  'http://www.topografix.com/GPX/1/0',
+  'http://www.topografix.com/GPX/1/1'
+];
+
+
+/**
+ * @const
+ * @type {string}
+ * @private
+ */
+ol.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 {Node} node Node.
+ * @param {Object} values Values.
+ * @private
+ * @return {Array.<number>} Flat coordinates.
+ */
+ol.format.GPX.appendCoordinate_ = function(flatCoordinates, node, values) {
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'node.nodeType should be ELEMENT');
+  flatCoordinates.push(
+      parseFloat(node.getAttribute('lon')),
+      parseFloat(node.getAttribute('lat')));
+  if ('ele' in values) {
+    flatCoordinates.push(/** @type {number} */ (values['ele']));
+    delete values['ele'];
+  } else {
+    flatCoordinates.push(0);
+  }
+  if ('time' in values) {
+    flatCoordinates.push(/** @type {number} */ (values['time']));
+    delete values['time'];
+  } else {
+    flatCoordinates.push(0);
+  }
+  return flatCoordinates;
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {Array.<*>} objectStack Object stack.
+ * @private
+ */
+ol.format.GPX.parseLink_ = function(node, objectStack) {
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'node.nodeType should be ELEMENT');
+  goog.asserts.assert(node.localName == 'link', 'localName should be link');
+  var values = /** @type {Object} */ (objectStack[objectStack.length - 1]);
+  var href = node.getAttribute('href');
+  if (href !== null) {
+    values['link'] = href;
+  }
+  ol.xml.parseNode(ol.format.GPX.LINK_PARSERS_, node, objectStack);
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {Array.<*>} objectStack Object stack.
+ * @private
+ */
+ol.format.GPX.parseExtensions_ = function(node, objectStack) {
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'node.nodeType should be ELEMENT');
+  goog.asserts.assert(node.localName == 'extensions',
+      'localName should be extensions');
+  var values = /** @type {Object} */ (objectStack[objectStack.length - 1]);
+  values['extensionsNode_'] = node;
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {Array.<*>} objectStack Object stack.
+ * @private
+ */
+ol.format.GPX.parseRtePt_ = function(node, objectStack) {
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'node.nodeType should be ELEMENT');
+  goog.asserts.assert(node.localName == 'rtept', 'localName should be rtept');
+  var values = ol.xml.pushParseAndPop(
+      {}, ol.format.GPX.RTEPT_PARSERS_, node, objectStack);
+  if (values) {
+    var rteValues = /** @type {Object} */ (objectStack[objectStack.length - 1]);
+    var flatCoordinates = /** @type {Array.<number>} */
+        (rteValues['flatCoordinates']);
+    ol.format.GPX.appendCoordinate_(flatCoordinates, node, values);
+  }
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {Array.<*>} objectStack Object stack.
+ * @private
+ */
+ol.format.GPX.parseTrkPt_ = function(node, objectStack) {
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'node.nodeType should be ELEMENT');
+  goog.asserts.assert(node.localName == 'trkpt', 'localName should be trkpt');
+  var values = ol.xml.pushParseAndPop(
+      {}, ol.format.GPX.TRKPT_PARSERS_, node, objectStack);
+  if (values) {
+    var trkValues = /** @type {Object} */ (objectStack[objectStack.length - 1]);
+    var flatCoordinates = /** @type {Array.<number>} */
+        (trkValues['flatCoordinates']);
+    ol.format.GPX.appendCoordinate_(flatCoordinates, node, values);
+  }
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {Array.<*>} objectStack Object stack.
+ * @private
+ */
+ol.format.GPX.parseTrkSeg_ = function(node, objectStack) {
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'node.nodeType should be ELEMENT');
+  goog.asserts.assert(node.localName == 'trkseg',
+      'localName should be trkseg');
+  var values = /** @type {Object} */ (objectStack[objectStack.length - 1]);
+  ol.xml.parseNode(ol.format.GPX.TRKSEG_PARSERS_, node, objectStack);
+  var flatCoordinates = /** @type {Array.<number>} */
+      (values['flatCoordinates']);
+  var ends = /** @type {Array.<number>} */ (values['ends']);
+  ends.push(flatCoordinates.length);
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {Array.<*>} objectStack Object stack.
+ * @private
+ * @return {ol.Feature|undefined} Track.
+ */
+ol.format.GPX.readRte_ = function(node, objectStack) {
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'node.nodeType should be ELEMENT');
+  goog.asserts.assert(node.localName == 'rte', 'localName should be rte');
+  var options = /** @type {olx.format.ReadOptions} */ (objectStack[0]);
+  var values = ol.xml.pushParseAndPop({
+    'flatCoordinates': []
+  }, ol.format.GPX.RTE_PARSERS_, node, objectStack);
+  if (!values) {
+    return undefined;
+  }
+  var flatCoordinates = /** @type {Array.<number>} */
+      (values['flatCoordinates']);
+  delete values['flatCoordinates'];
+  var geometry = new ol.geom.LineString(null);
+  geometry.setFlatCoordinates(ol.geom.GeometryLayout.XYZM, flatCoordinates);
+  ol.format.Feature.transformWithOptions(geometry, false, options);
+  var feature = new ol.Feature(geometry);
+  feature.setProperties(values);
+  return feature;
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {Array.<*>} objectStack Object stack.
+ * @private
+ * @return {ol.Feature|undefined} Track.
+ */
+ol.format.GPX.readTrk_ = function(node, objectStack) {
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'node.nodeType should be ELEMENT');
+  goog.asserts.assert(node.localName == 'trk', 'localName should be trk');
+  var options = /** @type {olx.format.ReadOptions} */ (objectStack[0]);
+  var values = ol.xml.pushParseAndPop({
+    'flatCoordinates': [],
+    'ends': []
+  }, ol.format.GPX.TRK_PARSERS_, node, objectStack);
+  if (!values) {
+    return undefined;
+  }
+  var flatCoordinates = /** @type {Array.<number>} */
+      (values['flatCoordinates']);
+  delete values['flatCoordinates'];
+  var ends = /** @type {Array.<number>} */ (values['ends']);
+  delete values['ends'];
+  var geometry = new ol.geom.MultiLineString(null);
+  geometry.setFlatCoordinates(
+      ol.geom.GeometryLayout.XYZM, flatCoordinates, ends);
+  ol.format.Feature.transformWithOptions(geometry, false, options);
+  var feature = new ol.Feature(geometry);
+  feature.setProperties(values);
+  return feature;
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {Array.<*>} objectStack Object stack.
+ * @private
+ * @return {ol.Feature|undefined} Waypoint.
+ */
+ol.format.GPX.readWpt_ = function(node, objectStack) {
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'node.nodeType should be ELEMENT');
+  goog.asserts.assert(node.localName == 'wpt', 'localName should be wpt');
+  var options = /** @type {olx.format.ReadOptions} */ (objectStack[0]);
+  var values = ol.xml.pushParseAndPop(
+      {}, ol.format.GPX.WPT_PARSERS_, node, objectStack);
+  if (!values) {
+    return undefined;
+  }
+  var coordinates = ol.format.GPX.appendCoordinate_([], node, values);
+  var geometry = new ol.geom.Point(
+      coordinates, ol.geom.GeometryLayout.XYZM);
+  ol.format.Feature.transformWithOptions(geometry, false, options);
+  var feature = new ol.Feature(geometry);
+  feature.setProperties(values);
+  return feature;
+};
+
+
+/**
+ * @const
+ * @type {Object.<string, function(Node, Array.<*>): (ol.Feature|undefined)>}
+ * @private
+ */
+ol.format.GPX.FEATURE_READER_ = {
+  'rte': ol.format.GPX.readRte_,
+  'trk': ol.format.GPX.readTrk_,
+  'wpt': ol.format.GPX.readWpt_
+};
+
+
+/**
+ * @const
+ * @type {Object.<string, Object.<string, ol.XmlParser>>}
+ * @private
+ */
+ol.format.GPX.GPX_PARSERS_ = ol.xml.makeStructureNS(
+    ol.format.GPX.NAMESPACE_URIS_, {
+      'rte': ol.xml.makeArrayPusher(ol.format.GPX.readRte_),
+      'trk': ol.xml.makeArrayPusher(ol.format.GPX.readTrk_),
+      'wpt': ol.xml.makeArrayPusher(ol.format.GPX.readWpt_)
+    });
+
+
+/**
+ * @const
+ * @type {Object.<string, Object.<string, ol.XmlParser>>}
+ * @private
+ */
+ol.format.GPX.LINK_PARSERS_ = ol.xml.makeStructureNS(
+    ol.format.GPX.NAMESPACE_URIS_, {
+      'text':
+          ol.xml.makeObjectPropertySetter(ol.format.XSD.readString, 'linkText'),
+      'type':
+          ol.xml.makeObjectPropertySetter(ol.format.XSD.readString, 'linkType')
+    });
+
+
+/**
+ * @const
+ * @type {Object.<string, Object.<string, ol.XmlParser>>}
+ * @private
+ */
+ol.format.GPX.RTE_PARSERS_ = ol.xml.makeStructureNS(
+    ol.format.GPX.NAMESPACE_URIS_, {
+      'name': ol.xml.makeObjectPropertySetter(ol.format.XSD.readString),
+      'cmt': ol.xml.makeObjectPropertySetter(ol.format.XSD.readString),
+      'desc': ol.xml.makeObjectPropertySetter(ol.format.XSD.readString),
+      'src': ol.xml.makeObjectPropertySetter(ol.format.XSD.readString),
+      'link': ol.format.GPX.parseLink_,
+      'number':
+          ol.xml.makeObjectPropertySetter(ol.format.XSD.readNonNegativeInteger),
+      'extensions': ol.format.GPX.parseExtensions_,
+      'type': ol.xml.makeObjectPropertySetter(ol.format.XSD.readString),
+      'rtept': ol.format.GPX.parseRtePt_
+    });
+
+
+/**
+ * @const
+ * @type {Object.<string, Object.<string, ol.XmlParser>>}
+ * @private
+ */
+ol.format.GPX.RTEPT_PARSERS_ = ol.xml.makeStructureNS(
+    ol.format.GPX.NAMESPACE_URIS_, {
+      'ele': ol.xml.makeObjectPropertySetter(ol.format.XSD.readDecimal),
+      'time': ol.xml.makeObjectPropertySetter(ol.format.XSD.readDateTime)
+    });
+
+
+/**
+ * @const
+ * @type {Object.<string, Object.<string, ol.XmlParser>>}
+ * @private
+ */
+ol.format.GPX.TRK_PARSERS_ = ol.xml.makeStructureNS(
+    ol.format.GPX.NAMESPACE_URIS_, {
+      'name': ol.xml.makeObjectPropertySetter(ol.format.XSD.readString),
+      'cmt': ol.xml.makeObjectPropertySetter(ol.format.XSD.readString),
+      'desc': ol.xml.makeObjectPropertySetter(ol.format.XSD.readString),
+      'src': ol.xml.makeObjectPropertySetter(ol.format.XSD.readString),
+      'link': ol.format.GPX.parseLink_,
+      'number':
+          ol.xml.makeObjectPropertySetter(ol.format.XSD.readNonNegativeInteger),
+      'type': ol.xml.makeObjectPropertySetter(ol.format.XSD.readString),
+      'extensions': ol.format.GPX.parseExtensions_,
+      'trkseg': ol.format.GPX.parseTrkSeg_
+    });
+
+
+/**
+ * @const
+ * @type {Object.<string, Object.<string, ol.XmlParser>>}
+ * @private
+ */
+ol.format.GPX.TRKSEG_PARSERS_ = ol.xml.makeStructureNS(
+    ol.format.GPX.NAMESPACE_URIS_, {
+      'trkpt': ol.format.GPX.parseTrkPt_
+    });
+
+
+/**
+ * @const
+ * @type {Object.<string, Object.<string, ol.XmlParser>>}
+ * @private
+ */
+ol.format.GPX.TRKPT_PARSERS_ = ol.xml.makeStructureNS(
+    ol.format.GPX.NAMESPACE_URIS_, {
+      'ele': ol.xml.makeObjectPropertySetter(ol.format.XSD.readDecimal),
+      'time': ol.xml.makeObjectPropertySetter(ol.format.XSD.readDateTime)
+    });
+
+
+/**
+ * @const
+ * @type {Object.<string, Object.<string, ol.XmlParser>>}
+ * @private
+ */
+ol.format.GPX.WPT_PARSERS_ = ol.xml.makeStructureNS(
+    ol.format.GPX.NAMESPACE_URIS_, {
+      'ele': ol.xml.makeObjectPropertySetter(ol.format.XSD.readDecimal),
+      'time': ol.xml.makeObjectPropertySetter(ol.format.XSD.readDateTime),
+      'magvar': ol.xml.makeObjectPropertySetter(ol.format.XSD.readDecimal),
+      'geoidheight': ol.xml.makeObjectPropertySetter(ol.format.XSD.readDecimal),
+      'name': ol.xml.makeObjectPropertySetter(ol.format.XSD.readString),
+      'cmt': ol.xml.makeObjectPropertySetter(ol.format.XSD.readString),
+      'desc': ol.xml.makeObjectPropertySetter(ol.format.XSD.readString),
+      'src': ol.xml.makeObjectPropertySetter(ol.format.XSD.readString),
+      'link': ol.format.GPX.parseLink_,
+      'sym': ol.xml.makeObjectPropertySetter(ol.format.XSD.readString),
+      'type': ol.xml.makeObjectPropertySetter(ol.format.XSD.readString),
+      'fix': ol.xml.makeObjectPropertySetter(ol.format.XSD.readString),
+      'sat': ol.xml.makeObjectPropertySetter(
+          ol.format.XSD.readNonNegativeInteger),
+      'hdop': ol.xml.makeObjectPropertySetter(ol.format.XSD.readDecimal),
+      'vdop': ol.xml.makeObjectPropertySetter(ol.format.XSD.readDecimal),
+      'pdop': ol.xml.makeObjectPropertySetter(ol.format.XSD.readDecimal),
+      'ageofdgpsdata':
+          ol.xml.makeObjectPropertySetter(ol.format.XSD.readDecimal),
+      'dgpsid':
+          ol.xml.makeObjectPropertySetter(ol.format.XSD.readNonNegativeInteger),
+      'extensions': ol.format.GPX.parseExtensions_
+    });
+
+
+/**
+ * @param {Array.<ol.Feature>} features List of features.
+ * @private
+ */
+ol.format.GPX.prototype.handleReadExtensions_ = function(features) {
+  if (!features) {
+    features = [];
+  }
+  for (var i = 0, ii = features.length; i < ii; ++i) {
+    var feature = features[i];
+    if (this.readExtensions_) {
+      var extensionsNode = feature.get('extensionsNode_') || null;
+      this.readExtensions_(feature, extensionsNode);
+    }
+    feature.set('extensionsNode_', undefined);
+  }
+};
+
+
+/**
+ * Read the first feature from a GPX source.
+ * 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 {olx.format.ReadOptions=} opt_options Read options.
+ * @return {ol.Feature} Feature.
+ * @api stable
+ */
+ol.format.GPX.prototype.readFeature;
+
+
+/**
+ * @inheritDoc
+ */
+ol.format.GPX.prototype.readFeatureFromNode = function(node, opt_options) {
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'node.nodeType should be ELEMENT');
+  if (!ol.array.includes(ol.format.GPX.NAMESPACE_URIS_, node.namespaceURI)) {
+    return null;
+  }
+  var featureReader = ol.format.GPX.FEATURE_READER_[node.localName];
+  if (!featureReader) {
+    return null;
+  }
+  var feature = featureReader(node, [this.getReadOptions(node, opt_options)]);
+  if (!feature) {
+    return null;
+  }
+  this.handleReadExtensions_([feature]);
+  return feature;
+};
+
+
+/**
+ * Read all features from a GPX source.
+ * 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 {olx.format.ReadOptions=} opt_options Read options.
+ * @return {Array.<ol.Feature>} Features.
+ * @api stable
+ */
+ol.format.GPX.prototype.readFeatures;
+
+
+/**
+ * @inheritDoc
+ */
+ol.format.GPX.prototype.readFeaturesFromNode = function(node, opt_options) {
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'node.nodeType should be ELEMENT');
+  if (!ol.array.includes(ol.format.GPX.NAMESPACE_URIS_, node.namespaceURI)) {
+    return [];
+  }
+  if (node.localName == 'gpx') {
+    /** @type {Array.<ol.Feature>} */
+    var features = ol.xml.pushParseAndPop([], ol.format.GPX.GPX_PARSERS_,
+        node, [this.getReadOptions(node, opt_options)]);
+    if (features) {
+      this.handleReadExtensions_(features);
+      return features;
+    } else {
+      return [];
+    }
+  }
+  return [];
+};
+
+
+/**
+ * Read the projection from a GPX source.
+ *
+ * @function
+ * @param {Document|Node|Object|string} source Source.
+ * @return {ol.proj.Projection} Projection.
+ * @api stable
+ */
+ol.format.GPX.prototype.readProjection;
+
+
+/**
+ * @param {Node} node Node.
+ * @param {string} value Value for the link's `href` attribute.
+ * @param {Array.<*>} objectStack Node stack.
+ * @private
+ */
+ol.format.GPX.writeLink_ = function(node, value, objectStack) {
+  node.setAttribute('href', value);
+  var context = objectStack[objectStack.length - 1];
+  goog.asserts.assert(goog.isObject(context), 'context should be an Object');
+  var properties = context['properties'];
+  var link = [
+    properties['linkText'],
+    properties['linkType']
+  ];
+  ol.xml.pushSerializeAndPop(/** @type {ol.XmlNodeStackItem} */ ({node: node}),
+      ol.format.GPX.LINK_SERIALIZERS_, ol.xml.OBJECT_PROPERTY_NODE_FACTORY,
+      link, objectStack, ol.format.GPX.LINK_SEQUENCE_);
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {ol.Coordinate} coordinate Coordinate.
+ * @param {Array.<*>} objectStack Object stack.
+ * @private
+ */
+ol.format.GPX.writeWptType_ = function(node, coordinate, objectStack) {
+  var context = objectStack[objectStack.length - 1];
+  goog.asserts.assert(goog.isObject(context), 'context should be an Object');
+  var parentNode = context.node;
+  goog.asserts.assert(ol.xml.isNode(parentNode),
+      'parentNode should be an XML node');
+  var namespaceURI = parentNode.namespaceURI;
+  var properties = context['properties'];
+  //FIXME Projection handling
+  ol.xml.setAttributeNS(node, null, 'lat', coordinate[1]);
+  ol.xml.setAttributeNS(node, null, 'lon', coordinate[0]);
+  var geometryLayout = context['geometryLayout'];
+  switch (geometryLayout) {
+    case ol.geom.GeometryLayout.XYZM:
+      if (coordinate[3] !== 0) {
+        properties['time'] = coordinate[3];
+      }
+      // fall through
+    case ol.geom.GeometryLayout.XYZ:
+      if (coordinate[2] !== 0) {
+        properties['ele'] = coordinate[2];
+      }
+      break;
+    case ol.geom.GeometryLayout.XYM:
+      if (coordinate[2] !== 0) {
+        properties['time'] = coordinate[2];
+      }
+      break;
+    default:
+      // pass
+  }
+  var orderedKeys = (node.nodeName == 'rtept') ?
+      ol.format.GPX.RTEPT_TYPE_SEQUENCE_[namespaceURI] :
+      ol.format.GPX.WPT_TYPE_SEQUENCE_[namespaceURI];
+  var values = ol.xml.makeSequence(properties, orderedKeys);
+  ol.xml.pushSerializeAndPop(/** @type {ol.XmlNodeStackItem} */
+      ({node: node, 'properties': properties}),
+      ol.format.GPX.WPT_TYPE_SERIALIZERS_, ol.xml.OBJECT_PROPERTY_NODE_FACTORY,
+      values, objectStack, orderedKeys);
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {ol.Feature} feature Feature.
+ * @param {Array.<*>} objectStack Object stack.
+ * @private
+ */
+ol.format.GPX.writeRte_ = function(node, feature, objectStack) {
+  var options = /** @type {olx.format.WriteOptions} */ (objectStack[0]);
+  var properties = feature.getProperties();
+  var context = {node: node, 'properties': properties};
+  var geometry = feature.getGeometry();
+  if (geometry) {
+    goog.asserts.assertInstanceof(geometry, ol.geom.LineString,
+        'geometry should be an ol.geom.LineString');
+    geometry = /** @type {ol.geom.LineString} */
+        (ol.format.Feature.transformWithOptions(geometry, true, options));
+    context['geometryLayout'] = geometry.getLayout();
+    properties['rtept'] = geometry.getCoordinates();
+  }
+  var parentNode = objectStack[objectStack.length - 1].node;
+  var orderedKeys = ol.format.GPX.RTE_SEQUENCE_[parentNode.namespaceURI];
+  var values = ol.xml.makeSequence(properties, orderedKeys);
+  ol.xml.pushSerializeAndPop(context,
+      ol.format.GPX.RTE_SERIALIZERS_, ol.xml.OBJECT_PROPERTY_NODE_FACTORY,
+      values, objectStack, orderedKeys);
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {ol.Feature} feature Feature.
+ * @param {Array.<*>} objectStack Object stack.
+ * @private
+ */
+ol.format.GPX.writeTrk_ = function(node, feature, objectStack) {
+  var options = /** @type {olx.format.WriteOptions} */ (objectStack[0]);
+  var properties = feature.getProperties();
+  /** @type {ol.XmlNodeStackItem} */
+  var context = {node: node, 'properties': properties};
+  var geometry = feature.getGeometry();
+  if (geometry) {
+    goog.asserts.assertInstanceof(geometry, ol.geom.MultiLineString,
+        'geometry should be an ol.geom.MultiLineString');
+    geometry = /** @type {ol.geom.MultiLineString} */
+        (ol.format.Feature.transformWithOptions(geometry, true, options));
+    properties['trkseg'] = geometry.getLineStrings();
+  }
+  var parentNode = objectStack[objectStack.length - 1].node;
+  var orderedKeys = ol.format.GPX.TRK_SEQUENCE_[parentNode.namespaceURI];
+  var values = ol.xml.makeSequence(properties, orderedKeys);
+  ol.xml.pushSerializeAndPop(context,
+      ol.format.GPX.TRK_SERIALIZERS_, ol.xml.OBJECT_PROPERTY_NODE_FACTORY,
+      values, objectStack, orderedKeys);
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {ol.geom.LineString} lineString LineString.
+ * @param {Array.<*>} objectStack Object stack.
+ * @private
+ */
+ol.format.GPX.writeTrkSeg_ = function(node, lineString, objectStack) {
+  /** @type {ol.XmlNodeStackItem} */
+  var context = {node: node, 'geometryLayout': lineString.getLayout(),
+    'properties': {}};
+  ol.xml.pushSerializeAndPop(context,
+      ol.format.GPX.TRKSEG_SERIALIZERS_, ol.format.GPX.TRKSEG_NODE_FACTORY_,
+      lineString.getCoordinates(), objectStack);
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {ol.Feature} feature Feature.
+ * @param {Array.<*>} objectStack Object stack.
+ * @private
+ */
+ol.format.GPX.writeWpt_ = function(node, feature, objectStack) {
+  var options = /** @type {olx.format.WriteOptions} */ (objectStack[0]);
+  var context = objectStack[objectStack.length - 1];
+  goog.asserts.assert(goog.isObject(context), 'context should be an Object');
+  context['properties'] = feature.getProperties();
+  var geometry = feature.getGeometry();
+  if (geometry) {
+    goog.asserts.assertInstanceof(geometry, ol.geom.Point,
+        'geometry should be an ol.geom.Point');
+    geometry = /** @type {ol.geom.Point} */
+        (ol.format.Feature.transformWithOptions(geometry, true, options));
+    context['geometryLayout'] = geometry.getLayout();
+    ol.format.GPX.writeWptType_(node, geometry.getCoordinates(), objectStack);
+  }
+};
+
+
+/**
+ * @const
+ * @type {Array.<string>}
+ * @private
+ */
+ol.format.GPX.LINK_SEQUENCE_ = ['text', 'type'];
+
+
+/**
+ * @type {Object.<string, Object.<string, ol.XmlSerializer>>}
+ * @private
+ */
+ol.format.GPX.LINK_SERIALIZERS_ = ol.xml.makeStructureNS(
+    ol.format.GPX.NAMESPACE_URIS_, {
+      'text': ol.xml.makeChildAppender(ol.format.XSD.writeStringTextNode),
+      'type': ol.xml.makeChildAppender(ol.format.XSD.writeStringTextNode)
+    });
+
+
+/**
+ * @const
+ * @type {Object.<string, Array.<string>>}
+ * @private
+ */
+ol.format.GPX.RTE_SEQUENCE_ = ol.xml.makeStructureNS(
+    ol.format.GPX.NAMESPACE_URIS_, [
+      'name', 'cmt', 'desc', 'src', 'link', 'number', 'type', 'rtept'
+    ]);
+
+
+/**
+ * @const
+ * @type {Object.<string, Object.<string, ol.XmlSerializer>>}
+ * @private
+ */
+ol.format.GPX.RTE_SERIALIZERS_ = ol.xml.makeStructureNS(
+    ol.format.GPX.NAMESPACE_URIS_, {
+      'name': ol.xml.makeChildAppender(ol.format.XSD.writeStringTextNode),
+      'cmt': ol.xml.makeChildAppender(ol.format.XSD.writeStringTextNode),
+      'desc': ol.xml.makeChildAppender(ol.format.XSD.writeStringTextNode),
+      'src': ol.xml.makeChildAppender(ol.format.XSD.writeStringTextNode),
+      'link': ol.xml.makeChildAppender(ol.format.GPX.writeLink_),
+      'number': ol.xml.makeChildAppender(
+          ol.format.XSD.writeNonNegativeIntegerTextNode),
+      'type': ol.xml.makeChildAppender(ol.format.XSD.writeStringTextNode),
+      'rtept': ol.xml.makeArraySerializer(ol.xml.makeChildAppender(
+          ol.format.GPX.writeWptType_))
+    });
+
+
+/**
+ * @const
+ * @type {Object.<string, Array.<string>>}
+ * @private
+ */
+ol.format.GPX.RTEPT_TYPE_SEQUENCE_ = ol.xml.makeStructureNS(
+    ol.format.GPX.NAMESPACE_URIS_, [
+      'ele', 'time'
+    ]);
+
+
+/**
+ * @const
+ * @type {Object.<string, Array.<string>>}
+ * @private
+ */
+ol.format.GPX.TRK_SEQUENCE_ = ol.xml.makeStructureNS(
+    ol.format.GPX.NAMESPACE_URIS_, [
+      'name', 'cmt', 'desc', 'src', 'link', 'number', 'type', 'trkseg'
+    ]);
+
+
+/**
+ * @const
+ * @type {Object.<string, Object.<string, ol.XmlSerializer>>}
+ * @private
+ */
+ol.format.GPX.TRK_SERIALIZERS_ = ol.xml.makeStructureNS(
+    ol.format.GPX.NAMESPACE_URIS_, {
+      'name': ol.xml.makeChildAppender(ol.format.XSD.writeStringTextNode),
+      'cmt': ol.xml.makeChildAppender(ol.format.XSD.writeStringTextNode),
+      'desc': ol.xml.makeChildAppender(ol.format.XSD.writeStringTextNode),
+      'src': ol.xml.makeChildAppender(ol.format.XSD.writeStringTextNode),
+      'link': ol.xml.makeChildAppender(ol.format.GPX.writeLink_),
+      'number': ol.xml.makeChildAppender(
+          ol.format.XSD.writeNonNegativeIntegerTextNode),
+      'type': ol.xml.makeChildAppender(ol.format.XSD.writeStringTextNode),
+      'trkseg': ol.xml.makeArraySerializer(ol.xml.makeChildAppender(
+          ol.format.GPX.writeTrkSeg_))
+    });
+
+
+/**
+ * @const
+ * @type {function(*, Array.<*>, string=): (Node|undefined)}
+ * @private
+ */
+ol.format.GPX.TRKSEG_NODE_FACTORY_ = ol.xml.makeSimpleNodeFactory('trkpt');
+
+
+/**
+ * @const
+ * @type {Object.<string, Object.<string, ol.XmlSerializer>>}
+ * @private
+ */
+ol.format.GPX.TRKSEG_SERIALIZERS_ = ol.xml.makeStructureNS(
+    ol.format.GPX.NAMESPACE_URIS_, {
+      'trkpt': ol.xml.makeChildAppender(ol.format.GPX.writeWptType_)
+    });
+
+
+/**
+ * @const
+ * @type {Object.<string, Array.<string>>}
+ * @private
+ */
+ol.format.GPX.WPT_TYPE_SEQUENCE_ = ol.xml.makeStructureNS(
+    ol.format.GPX.NAMESPACE_URIS_, [
+      'ele', 'time', 'magvar', 'geoidheight', 'name', 'cmt', 'desc', 'src',
+      'link', 'sym', 'type', 'fix', 'sat', 'hdop', 'vdop', 'pdop',
+      'ageofdgpsdata', 'dgpsid'
+    ]);
+
+
+/**
+ * @type {Object.<string, Object.<string, ol.XmlSerializer>>}
+ * @private
+ */
+ol.format.GPX.WPT_TYPE_SERIALIZERS_ = ol.xml.makeStructureNS(
+    ol.format.GPX.NAMESPACE_URIS_, {
+      'ele': ol.xml.makeChildAppender(ol.format.XSD.writeDecimalTextNode),
+      'time': ol.xml.makeChildAppender(ol.format.XSD.writeDateTimeTextNode),
+      'magvar': ol.xml.makeChildAppender(ol.format.XSD.writeDecimalTextNode),
+      'geoidheight': ol.xml.makeChildAppender(
+          ol.format.XSD.writeDecimalTextNode),
+      'name': ol.xml.makeChildAppender(ol.format.XSD.writeStringTextNode),
+      'cmt': ol.xml.makeChildAppender(ol.format.XSD.writeStringTextNode),
+      'desc': ol.xml.makeChildAppender(ol.format.XSD.writeStringTextNode),
+      'src': ol.xml.makeChildAppender(ol.format.XSD.writeStringTextNode),
+      'link': ol.xml.makeChildAppender(ol.format.GPX.writeLink_),
+      'sym': ol.xml.makeChildAppender(ol.format.XSD.writeStringTextNode),
+      'type': ol.xml.makeChildAppender(ol.format.XSD.writeStringTextNode),
+      'fix': ol.xml.makeChildAppender(ol.format.XSD.writeStringTextNode),
+      'sat': ol.xml.makeChildAppender(
+          ol.format.XSD.writeNonNegativeIntegerTextNode),
+      'hdop': ol.xml.makeChildAppender(ol.format.XSD.writeDecimalTextNode),
+      'vdop': ol.xml.makeChildAppender(ol.format.XSD.writeDecimalTextNode),
+      'pdop': ol.xml.makeChildAppender(ol.format.XSD.writeDecimalTextNode),
+      'ageofdgpsdata': ol.xml.makeChildAppender(
+          ol.format.XSD.writeDecimalTextNode),
+      'dgpsid': ol.xml.makeChildAppender(
+          ol.format.XSD.writeNonNegativeIntegerTextNode)
+    });
+
+
+/**
+ * @const
+ * @type {Object.<string, string>}
+ * @private
+ */
+ol.format.GPX.GEOMETRY_TYPE_TO_NODENAME_ = {
+  'Point': 'wpt',
+  'LineString': 'rte',
+  'MultiLineString': 'trk'
+};
+
+
+/**
+ * @const
+ * @param {*} value Value.
+ * @param {Array.<*>} objectStack Object stack.
+ * @param {string=} opt_nodeName Node name.
+ * @return {Node|undefined} Node.
+ * @private
+ */
+ol.format.GPX.GPX_NODE_FACTORY_ = function(value, objectStack, opt_nodeName) {
+  goog.asserts.assertInstanceof(value, ol.Feature,
+      'value should be an ol.Feature');
+  var geometry = value.getGeometry();
+  if (geometry) {
+    var nodeName = ol.format.GPX.GEOMETRY_TYPE_TO_NODENAME_[geometry.getType()];
+    if (nodeName) {
+      var parentNode = objectStack[objectStack.length - 1].node;
+      goog.asserts.assert(ol.xml.isNode(parentNode),
+          'parentNode should be an XML node');
+      return ol.xml.createElementNS(parentNode.namespaceURI, nodeName);
+    }
+  }
+};
+
+
+/**
+ * @const
+ * @type {Object.<string, Object.<string, ol.XmlSerializer>>}
+ * @private
+ */
+ol.format.GPX.GPX_SERIALIZERS_ = ol.xml.makeStructureNS(
+    ol.format.GPX.NAMESPACE_URIS_, {
+      'rte': ol.xml.makeChildAppender(ol.format.GPX.writeRte_),
+      'trk': ol.xml.makeChildAppender(ol.format.GPX.writeTrk_),
+      'wpt': ol.xml.makeChildAppender(ol.format.GPX.writeWpt_)
+    });
+
+
+/**
+ * Encode an array of features in the GPX format.
+ * LineString geometries are output as routes (`<rte>`), and MultiLineString
+ * as tracks (`<trk>`).
+ *
+ * @function
+ * @param {Array.<ol.Feature>} features Features.
+ * @param {olx.format.WriteOptions=} opt_options Write options.
+ * @return {string} Result.
+ * @api stable
+ */
+ol.format.GPX.prototype.writeFeatures;
+
+
+/**
+ * Encode an array of features in the GPX format as an XML node.
+ * LineString geometries are output as routes (`<rte>`), and MultiLineString
+ * as tracks (`<trk>`).
+ *
+ * @param {Array.<ol.Feature>} features Features.
+ * @param {olx.format.WriteOptions=} opt_options Options.
+ * @return {Node} Node.
+ * @api
+ */
+ol.format.GPX.prototype.writeFeaturesNode = function(features, opt_options) {
+  opt_options = this.adaptOptions(opt_options);
+  //FIXME Serialize metadata
+  var gpx = ol.xml.createElementNS('http://www.topografix.com/GPX/1/1', 'gpx');
+  var xmlnsUri = 'http://www.w3.org/2000/xmlns/';
+  var xmlSchemaInstanceUri = 'http://www.w3.org/2001/XMLSchema-instance';
+  ol.xml.setAttributeNS(gpx, xmlnsUri, 'xmlns:xsi', xmlSchemaInstanceUri);
+  ol.xml.setAttributeNS(gpx, xmlSchemaInstanceUri, 'xsi:schemaLocation',
+      ol.format.GPX.SCHEMA_LOCATION_);
+  gpx.setAttribute('version', '1.1');
+  gpx.setAttribute('creator', 'OpenLayers 3');
+
+  ol.xml.pushSerializeAndPop(/** @type {ol.XmlNodeStackItem} */
+      ({node: gpx}), ol.format.GPX.GPX_SERIALIZERS_,
+      ol.format.GPX.GPX_NODE_FACTORY_, features, [opt_options]);
+  return gpx;
+};
+
+goog.provide('ol.format.TextFeature');
+
+goog.require('goog.asserts');
+goog.require('ol.format.Feature');
+goog.require('ol.format.FormatType');
+
+
+/**
+ * @classdesc
+ * Abstract base class; normally only used for creating subclasses and not
+ * instantiated in apps.
+ * Base class for text feature formats.
+ *
+ * @constructor
+ * @extends {ol.format.Feature}
+ */
+ol.format.TextFeature = function() {
+  ol.format.Feature.call(this);
+};
+ol.inherits(ol.format.TextFeature, ol.format.Feature);
+
+
+/**
+ * @param {Document|Node|Object|string} source Source.
+ * @private
+ * @return {string} Text.
+ */
+ol.format.TextFeature.prototype.getText_ = function(source) {
+  if (typeof source === 'string') {
+    return source;
+  } else {
+    goog.asserts.fail();
+    return '';
+  }
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.format.TextFeature.prototype.getType = function() {
+  return ol.format.FormatType.TEXT;
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.format.TextFeature.prototype.readFeature = function(source, opt_options) {
+  return this.readFeatureFromText(
+      this.getText_(source), this.adaptOptions(opt_options));
+};
+
+
+/**
+ * @param {string} text Text.
+ * @param {olx.format.ReadOptions=} opt_options Read options.
+ * @protected
+ * @return {ol.Feature} Feature.
+ */
+ol.format.TextFeature.prototype.readFeatureFromText = goog.abstractMethod;
+
+
+/**
+ * @inheritDoc
+ */
+ol.format.TextFeature.prototype.readFeatures = function(source, opt_options) {
+  return this.readFeaturesFromText(
+      this.getText_(source), this.adaptOptions(opt_options));
+};
+
+
+/**
+ * @param {string} text Text.
+ * @param {olx.format.ReadOptions=} opt_options Read options.
+ * @protected
+ * @return {Array.<ol.Feature>} Features.
+ */
+ol.format.TextFeature.prototype.readFeaturesFromText = goog.abstractMethod;
+
+
+/**
+ * @inheritDoc
+ */
+ol.format.TextFeature.prototype.readGeometry = function(source, opt_options) {
+  return this.readGeometryFromText(
+      this.getText_(source), this.adaptOptions(opt_options));
+};
+
+
+/**
+ * @param {string} text Text.
+ * @param {olx.format.ReadOptions=} opt_options Read options.
+ * @protected
+ * @return {ol.geom.Geometry} Geometry.
+ */
+ol.format.TextFeature.prototype.readGeometryFromText = goog.abstractMethod;
+
+
+/**
+ * @inheritDoc
+ */
+ol.format.TextFeature.prototype.readProjection = function(source) {
+  return this.readProjectionFromText(this.getText_(source));
+};
+
+
+/**
+ * @param {string} text Text.
+ * @protected
+ * @return {ol.proj.Projection} Projection.
+ */
+ol.format.TextFeature.prototype.readProjectionFromText = function(text) {
+  return this.defaultDataProjection;
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.format.TextFeature.prototype.writeFeature = function(feature, opt_options) {
+  return this.writeFeatureText(feature, this.adaptOptions(opt_options));
+};
+
+
+/**
+ * @param {ol.Feature} feature Features.
+ * @param {olx.format.WriteOptions=} opt_options Write options.
+ * @protected
+ * @return {string} Text.
+ */
+ol.format.TextFeature.prototype.writeFeatureText = goog.abstractMethod;
+
+
+/**
+ * @inheritDoc
+ */
+ol.format.TextFeature.prototype.writeFeatures = function(
+    features, opt_options) {
+  return this.writeFeaturesText(features, this.adaptOptions(opt_options));
+};
+
+
+/**
+ * @param {Array.<ol.Feature>} features Features.
+ * @param {olx.format.WriteOptions=} opt_options Write options.
+ * @protected
+ * @return {string} Text.
+ */
+ol.format.TextFeature.prototype.writeFeaturesText = goog.abstractMethod;
+
+
+/**
+ * @inheritDoc
+ */
+ol.format.TextFeature.prototype.writeGeometry = function(
+    geometry, opt_options) {
+  return this.writeGeometryText(geometry, this.adaptOptions(opt_options));
+};
+
+
+/**
+ * @param {ol.geom.Geometry} geometry Geometry.
+ * @param {olx.format.WriteOptions=} opt_options Write options.
+ * @protected
+ * @return {string} Text.
+ */
+ol.format.TextFeature.prototype.writeGeometryText = goog.abstractMethod;
+
+goog.provide('ol.format.IGC');
+
+goog.require('goog.asserts');
+goog.require('ol.Feature');
+goog.require('ol.format.Feature');
+goog.require('ol.format.TextFeature');
+goog.require('ol.geom.GeometryLayout');
+goog.require('ol.geom.LineString');
+goog.require('ol.proj');
+
+
+/**
+ * IGC altitude/z. One of 'barometric', 'gps', 'none'.
+ * @enum {string}
+ */
+ol.format.IGCZ = {
+  BAROMETRIC: 'barometric',
+  GPS: 'gps',
+  NONE: 'none'
+};
+
+
+/**
+ * @classdesc
+ * Feature format for `*.igc` flight recording files.
+ *
+ * @constructor
+ * @extends {ol.format.TextFeature}
+ * @param {olx.format.IGCOptions=} opt_options Options.
+ * @api
+ */
+ol.format.IGC = function(opt_options) {
+
+  var options = opt_options ? opt_options : {};
+
+  ol.format.TextFeature.call(this);
+
+  /**
+   * @inheritDoc
+   */
+  this.defaultDataProjection = ol.proj.get('EPSG:4326');
+
+  /**
+   * @private
+   * @type {ol.format.IGCZ}
+   */
+  this.altitudeMode_ = options.altitudeMode ?
+      options.altitudeMode : ol.format.IGCZ.NONE;
+
+};
+ol.inherits(ol.format.IGC, ol.format.TextFeature);
+
+
+/**
+ * @const
+ * @type {Array.<string>}
+ * @private
+ */
+ol.format.IGC.EXTENSIONS_ = ['.igc'];
+
+
+/**
+ * @const
+ * @type {RegExp}
+ * @private
+ */
+ol.format.IGC.B_RECORD_RE_ =
+    /^B(\d{2})(\d{2})(\d{2})(\d{2})(\d{5})([NS])(\d{3})(\d{5})([EW])([AV])(\d{5})(\d{5})/;
+
+
+/**
+ * @const
+ * @type {RegExp}
+ * @private
+ */
+ol.format.IGC.H_RECORD_RE_ = /^H.([A-Z]{3}).*?:(.*)/;
+
+
+/**
+ * @const
+ * @type {RegExp}
+ * @private
+ */
+ol.format.IGC.HFDTE_RECORD_RE_ = /^HFDTE(\d{2})(\d{2})(\d{2})/;
+
+
+/**
+ * A regular expression matching the newline characters `\r\n`, `\r` and `\n`.
+ *
+ * @const
+ * @type {RegExp}
+ * @private
+ */
+ol.format.IGC.NEWLINE_RE_ = /\r\n|\r|\n/;
+
+
+/**
+ * @inheritDoc
+ */
+ol.format.IGC.prototype.getExtensions = function() {
+  return ol.format.IGC.EXTENSIONS_;
+};
+
+
+/**
+ * Read the feature from the IGC source.
+ *
+ * @function
+ * @param {Document|Node|Object|string} source Source.
+ * @param {olx.format.ReadOptions=} opt_options Read options.
+ * @return {ol.Feature} Feature.
+ * @api
+ */
+ol.format.IGC.prototype.readFeature;
+
+
+/**
+ * @inheritDoc
+ */
+ol.format.IGC.prototype.readFeatureFromText = function(text, opt_options) {
+  var altitudeMode = this.altitudeMode_;
+  var lines = text.split(ol.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 = ol.format.IGC.B_RECORD_RE_.exec(line);
+      if (m) {
+        var hour = parseInt(m[1], 10);
+        var minute = parseInt(m[2], 10);
+        var second = parseInt(m[3], 10);
+        var y = parseInt(m[4], 10) + parseInt(m[5], 10) / 60000;
+        if (m[6] == 'S') {
+          y = -y;
+        }
+        var x = parseInt(m[7], 10) + parseInt(m[8], 10) / 60000;
+        if (m[9] == 'W') {
+          x = -x;
+        }
+        flatCoordinates.push(x, y);
+        if (altitudeMode != ol.format.IGCZ.NONE) {
+          var z;
+          if (altitudeMode == ol.format.IGCZ.GPS) {
+            z = parseInt(m[11], 10);
+          } else if (altitudeMode == ol.format.IGCZ.BAROMETRIC) {
+            z = parseInt(m[12], 10);
+          } else {
+            goog.asserts.fail();
+            z = 0;
+          }
+          flatCoordinates.push(z);
+        }
+        var dateTime = Date.UTC(year, month, day, hour, minute, second);
+        // 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 = ol.format.IGC.HFDTE_RECORD_RE_.exec(line);
+      if (m) {
+        day = parseInt(m[1], 10);
+        month = parseInt(m[2], 10) - 1;
+        year = 2000 + parseInt(m[3], 10);
+      } else {
+        m = ol.format.IGC.H_RECORD_RE_.exec(line);
+        if (m) {
+          properties[m[1]] = m[2].trim();
+        }
+      }
+    }
+  }
+  if (flatCoordinates.length === 0) {
+    return null;
+  }
+  var lineString = new ol.geom.LineString(null);
+  var layout = altitudeMode == ol.format.IGCZ.NONE ?
+      ol.geom.GeometryLayout.XYM : ol.geom.GeometryLayout.XYZM;
+  lineString.setFlatCoordinates(layout, flatCoordinates);
+  var feature = new ol.Feature(ol.format.Feature.transformWithOptions(
+      lineString, false, opt_options));
+  feature.setProperties(properties);
+  return feature;
+};
+
+
+/**
+ * Read the feature from the source. As IGC sources contain a single
+ * feature, this will return the feature in an array.
+ *
+ * @function
+ * @param {Document|Node|Object|string} source Source.
+ * @param {olx.format.ReadOptions=} opt_options Read options.
+ * @return {Array.<ol.Feature>} Features.
+ * @api
+ */
+ol.format.IGC.prototype.readFeatures;
+
+
+/**
+ * @inheritDoc
+ */
+ol.format.IGC.prototype.readFeaturesFromText = function(text, opt_options) {
+  var feature = this.readFeatureFromText(text, opt_options);
+  if (feature) {
+    return [feature];
+  } else {
+    return [];
+  }
+};
+
+
+/**
+ * Read the projection from the IGC source.
+ *
+ * @function
+ * @param {Document|Node|Object|string} source Source.
+ * @return {ol.proj.Projection} Projection.
+ * @api
+ */
+ol.format.IGC.prototype.readProjection;
+
+// Copyright 2006 The Closure Library Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS-IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+/**
+ * @fileoverview Generics method for collection-like classes and objects.
+ *
+ * @author arv@google.com (Erik Arvidsson)
+ *
+ * This file contains functions to work with collections. It supports using
+ * Map, Set, Array and Object and other classes that implement collection-like
+ * methods.
+ */
+
+
+goog.provide('goog.structs');
+
+goog.require('goog.array');
+goog.require('goog.object');
+
+
+// We treat an object as a dictionary if it has getKeys or it is an object that
+// isn't arrayLike.
+
+
+/**
+ * Returns the number of values in the collection-like object.
+ * @param {Object} col The collection-like object.
+ * @return {number} The number of values in the collection-like object.
+ */
+goog.structs.getCount = function(col) {
+  if (col.getCount && typeof col.getCount == 'function') {
+    return col.getCount();
+  }
+  if (goog.isArrayLike(col) || goog.isString(col)) {
+    return col.length;
+  }
+  return goog.object.getCount(col);
+};
+
+
+/**
+ * Returns the values of the collection-like object.
+ * @param {Object} col The collection-like object.
+ * @return {!Array<?>} The values in the collection-like object.
+ */
+goog.structs.getValues = function(col) {
+  if (col.getValues && typeof col.getValues == 'function') {
+    return col.getValues();
+  }
+  if (goog.isString(col)) {
+    return col.split('');
+  }
+  if (goog.isArrayLike(col)) {
+    var rv = [];
+    var l = col.length;
+    for (var i = 0; i < l; i++) {
+      rv.push(col[i]);
+    }
+    return rv;
+  }
+  return goog.object.getValues(col);
+};
+
+
+/**
+ * Returns the keys of the collection. Some collections have no notion of
+ * keys/indexes and this function will return undefined in those cases.
+ * @param {Object} col The collection-like object.
+ * @return {!Array|undefined} The keys in the collection.
+ */
+goog.structs.getKeys = function(col) {
+  if (col.getKeys && typeof col.getKeys == 'function') {
+    return col.getKeys();
+  }
+  // if we have getValues but no getKeys we know this is a key-less collection
+  if (col.getValues && typeof col.getValues == 'function') {
+    return undefined;
+  }
+  if (goog.isArrayLike(col) || goog.isString(col)) {
+    var rv = [];
+    var l = col.length;
+    for (var i = 0; i < l; i++) {
+      rv.push(i);
+    }
+    return rv;
+  }
+
+  return goog.object.getKeys(col);
+};
+
+
+/**
+ * Whether the collection contains the given value. This is O(n) and uses
+ * equals (==) to test the existence.
+ * @param {Object} col The collection-like object.
+ * @param {*} val The value to check for.
+ * @return {boolean} True if the map contains the value.
+ */
+goog.structs.contains = function(col, val) {
+  if (col.contains && typeof col.contains == 'function') {
+    return col.contains(val);
+  }
+  if (col.containsValue && typeof col.containsValue == 'function') {
+    return col.containsValue(val);
+  }
+  if (goog.isArrayLike(col) || goog.isString(col)) {
+    return goog.array.contains(/** @type {!Array<?>} */ (col), val);
+  }
+  return goog.object.containsValue(col, val);
+};
+
+
+/**
+ * Whether the collection is empty.
+ * @param {Object} col The collection-like object.
+ * @return {boolean} True if empty.
+ */
+goog.structs.isEmpty = function(col) {
+  if (col.isEmpty && typeof col.isEmpty == 'function') {
+    return col.isEmpty();
+  }
+
+  // We do not use goog.string.isEmptyOrWhitespace because here we treat the
+  // string as
+  // collection and as such even whitespace matters
+
+  if (goog.isArrayLike(col) || goog.isString(col)) {
+    return goog.array.isEmpty(/** @type {!Array<?>} */ (col));
+  }
+  return goog.object.isEmpty(col);
+};
+
+
+/**
+ * Removes all the elements from the collection.
+ * @param {Object} col The collection-like object.
+ */
+goog.structs.clear = function(col) {
+  // NOTE(arv): This should not contain strings because strings are immutable
+  if (col.clear && typeof col.clear == 'function') {
+    col.clear();
+  } else if (goog.isArrayLike(col)) {
+    goog.array.clear(/** @type {IArrayLike<?>} */ (col));
+  } else {
+    goog.object.clear(col);
+  }
+};
+
+
+/**
+ * Calls a function for each value in a collection. The function takes
+ * three arguments; the value, the key and the collection.
+ *
+ * NOTE: This will be deprecated soon! Please use a more specific method if
+ * possible, e.g. goog.array.forEach, goog.object.forEach, etc.
+ *
+ * @param {S} col The collection-like object.
+ * @param {function(this:T,?,?,S):?} f The function to call for every value.
+ *     This function takes
+ *     3 arguments (the value, the key or undefined if the collection has no
+ *     notion of keys, and the collection) and the return value is irrelevant.
+ * @param {T=} opt_obj The object to be used as the value of 'this'
+ *     within {@code f}.
+ * @template T,S
+ */
+goog.structs.forEach = function(col, f, opt_obj) {
+  if (col.forEach && typeof col.forEach == 'function') {
+    col.forEach(f, opt_obj);
+  } else if (goog.isArrayLike(col) || goog.isString(col)) {
+    goog.array.forEach(/** @type {!Array<?>} */ (col), f, opt_obj);
+  } else {
+    var keys = goog.structs.getKeys(col);
+    var values = goog.structs.getValues(col);
+    var l = values.length;
+    for (var i = 0; i < l; i++) {
+      f.call(/** @type {?} */ (opt_obj), values[i], keys && keys[i], col);
+    }
+  }
+};
+
+
+/**
+ * Calls a function for every value in the collection. When a call returns true,
+ * adds the value to a new collection (Array is returned by default).
+ *
+ * @param {S} col The collection-like object.
+ * @param {function(this:T,?,?,S):boolean} f The function to call for every
+ *     value. This function takes
+ *     3 arguments (the value, the key or undefined if the collection has no
+ *     notion of keys, and the collection) and should return a Boolean. If the
+ *     return value is true the value is added to the result collection. If it
+ *     is false the value is not included.
+ * @param {T=} opt_obj The object to be used as the value of 'this'
+ *     within {@code f}.
+ * @return {!Object|!Array<?>} A new collection where the passed values are
+ *     present. If col is a key-less collection an array is returned.  If col
+ *     has keys and values a plain old JS object is returned.
+ * @template T,S
+ */
+goog.structs.filter = function(col, f, opt_obj) {
+  if (typeof col.filter == 'function') {
+    return col.filter(f, opt_obj);
+  }
+  if (goog.isArrayLike(col) || goog.isString(col)) {
+    return goog.array.filter(/** @type {!Array<?>} */ (col), f, opt_obj);
+  }
+
+  var rv;
+  var keys = goog.structs.getKeys(col);
+  var values = goog.structs.getValues(col);
+  var l = values.length;
+  if (keys) {
+    rv = {};
+    for (var i = 0; i < l; i++) {
+      if (f.call(/** @type {?} */ (opt_obj), values[i], keys[i], col)) {
+        rv[keys[i]] = values[i];
+      }
+    }
+  } else {
+    // We should not use goog.array.filter here since we want to make sure that
+    // the index is undefined as well as make sure that col is passed to the
+    // function.
+    rv = [];
+    for (var i = 0; i < l; i++) {
+      if (f.call(opt_obj, values[i], undefined, col)) {
+        rv.push(values[i]);
+      }
+    }
+  }
+  return rv;
+};
+
+
+/**
+ * Calls a function for every value in the collection and adds the result into a
+ * new collection (defaults to creating a new Array).
+ *
+ * @param {S} col The collection-like object.
+ * @param {function(this:T,?,?,S):V} f The function to call for every value.
+ *     This function takes 3 arguments (the value, the key or undefined if the
+ *     collection has no notion of keys, and the collection) and should return
+ *     something. The result will be used as the value in the new collection.
+ * @param {T=} opt_obj  The object to be used as the value of 'this'
+ *     within {@code f}.
+ * @return {!Object<V>|!Array<V>} A new collection with the new values.  If
+ *     col is a key-less collection an array is returned.  If col has keys and
+ *     values a plain old JS object is returned.
+ * @template T,S,V
+ */
+goog.structs.map = function(col, f, opt_obj) {
+  if (typeof col.map == 'function') {
+    return col.map(f, opt_obj);
+  }
+  if (goog.isArrayLike(col) || goog.isString(col)) {
+    return goog.array.map(/** @type {!Array<?>} */ (col), f, opt_obj);
+  }
+
+  var rv;
+  var keys = goog.structs.getKeys(col);
+  var values = goog.structs.getValues(col);
+  var l = values.length;
+  if (keys) {
+    rv = {};
+    for (var i = 0; i < l; i++) {
+      rv[keys[i]] = f.call(/** @type {?} */ (opt_obj), values[i], keys[i], col);
+    }
+  } else {
+    // We should not use goog.array.map here since we want to make sure that
+    // the index is undefined as well as make sure that col is passed to the
+    // function.
+    rv = [];
+    for (var i = 0; i < l; i++) {
+      rv[i] = f.call(/** @type {?} */ (opt_obj), values[i], undefined, col);
+    }
+  }
+  return rv;
+};
+
+
+/**
+ * Calls f for each value in a collection. If any call returns true this returns
+ * true (without checking the rest). If all returns false this returns false.
+ *
+ * @param {S} col The collection-like object.
+ * @param {function(this:T,?,?,S):boolean} f The function to call for every
+ *     value. This function takes 3 arguments (the value, the key or undefined
+ *     if the collection has no notion of keys, and the collection) and should
+ *     return a boolean.
+ * @param {T=} opt_obj  The object to be used as the value of 'this'
+ *     within {@code f}.
+ * @return {boolean} True if any value passes the test.
+ * @template T,S
+ */
+goog.structs.some = function(col, f, opt_obj) {
+  if (typeof col.some == 'function') {
+    return col.some(f, opt_obj);
+  }
+  if (goog.isArrayLike(col) || goog.isString(col)) {
+    return goog.array.some(/** @type {!Array<?>} */ (col), f, opt_obj);
+  }
+  var keys = goog.structs.getKeys(col);
+  var values = goog.structs.getValues(col);
+  var l = values.length;
+  for (var i = 0; i < l; i++) {
+    if (f.call(/** @type {?} */ (opt_obj), values[i], keys && keys[i], col)) {
+      return true;
+    }
+  }
+  return false;
+};
+
+
+/**
+ * Calls f for each value in a collection. If all calls return true this return
+ * true this returns true. If any returns false this returns false at this point
+ *  and does not continue to check the remaining values.
+ *
+ * @param {S} col The collection-like object.
+ * @param {function(this:T,?,?,S):boolean} f The function to call for every
+ *     value. This function takes 3 arguments (the value, the key or
+ *     undefined if the collection has no notion of keys, and the collection)
+ *     and should return a boolean.
+ * @param {T=} opt_obj  The object to be used as the value of 'this'
+ *     within {@code f}.
+ * @return {boolean} True if all key-value pairs pass the test.
+ * @template T,S
+ */
+goog.structs.every = function(col, f, opt_obj) {
+  if (typeof col.every == 'function') {
+    return col.every(f, opt_obj);
+  }
+  if (goog.isArrayLike(col) || goog.isString(col)) {
+    return goog.array.every(/** @type {!Array<?>} */ (col), f, opt_obj);
+  }
+  var keys = goog.structs.getKeys(col);
+  var values = goog.structs.getValues(col);
+  var l = values.length;
+  for (var i = 0; i < l; i++) {
+    if (!f.call(/** @type {?} */ (opt_obj), values[i], keys && keys[i], col)) {
+      return false;
+    }
+  }
+  return true;
+};
+
+// Copyright 2007 The Closure Library Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS-IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+/**
+ * @fileoverview Python style iteration utilities.
+ * @author arv@google.com (Erik Arvidsson)
+ */
+
+
+goog.provide('goog.iter');
+goog.provide('goog.iter.Iterable');
+goog.provide('goog.iter.Iterator');
+goog.provide('goog.iter.StopIteration');
+
+goog.require('goog.array');
+goog.require('goog.asserts');
+goog.require('goog.functions');
+goog.require('goog.math');
+
+
+/**
+ * @typedef {goog.iter.Iterator|{length:number}|{__iterator__}}
+ */
+goog.iter.Iterable;
+
+
+/**
+ * Singleton Error object that is used to terminate iterations.
+ * @const {!Error}
+ */
+goog.iter.StopIteration = ('StopIteration' in goog.global) ?
+    // For script engines that support legacy iterators.
+    goog.global['StopIteration'] :
+    {message: 'StopIteration', stack: ''};
+
+
+
+/**
+ * Class/interface for iterators.  An iterator needs to implement a {@code next}
+ * method and it needs to throw a {@code goog.iter.StopIteration} when the
+ * iteration passes beyond the end.  Iterators have no {@code hasNext} method.
+ * It is recommended to always use the helper functions to iterate over the
+ * iterator or in case you are only targeting JavaScript 1.7 for in loops.
+ * @constructor
+ * @template VALUE
+ */
+goog.iter.Iterator = function() {};
+
+
+/**
+ * Returns the next value of the iteration.  This will throw the object
+ * {@see goog.iter#StopIteration} when the iteration passes the end.
+ * @return {VALUE} Any object or value.
+ */
+goog.iter.Iterator.prototype.next = function() {
+  throw goog.iter.StopIteration;
+};
+
+
+/**
+ * Returns the {@code Iterator} object itself.  This is used to implement
+ * the iterator protocol in JavaScript 1.7
+ * @param {boolean=} opt_keys  Whether to return the keys or values. Default is
+ *     to only return the values.  This is being used by the for-in loop (true)
+ *     and the for-each-in loop (false).  Even though the param gives a hint
+ *     about what the iterator will return there is no guarantee that it will
+ *     return the keys when true is passed.
+ * @return {!goog.iter.Iterator<VALUE>} The object itself.
+ */
+goog.iter.Iterator.prototype.__iterator__ = function(opt_keys) {
+  return this;
+};
+
+
+/**
+ * Returns an iterator that knows how to iterate over the values in the object.
+ * @param {goog.iter.Iterator<VALUE>|goog.iter.Iterable} iterable  If the
+ *     object is an iterator it will be returned as is.  If the object has an
+ *     {@code __iterator__} method that will be called to get the value
+ *     iterator.  If the object is an array-like object we create an iterator
+ *     for that.
+ * @return {!goog.iter.Iterator<VALUE>} An iterator that knows how to iterate
+ *     over the values in {@code iterable}.
+ * @template VALUE
+ */
+goog.iter.toIterator = function(iterable) {
+  if (iterable instanceof goog.iter.Iterator) {
+    return iterable;
+  }
+  if (typeof iterable.__iterator__ == 'function') {
+    return iterable.__iterator__(false);
+  }
+  if (goog.isArrayLike(iterable)) {
+    var i = 0;
+    var newIter = new goog.iter.Iterator;
+    newIter.next = function() {
+      while (true) {
+        if (i >= iterable.length) {
+          throw goog.iter.StopIteration;
+        }
+        // Don't include deleted elements.
+        if (!(i in iterable)) {
+          i++;
+          continue;
+        }
+        return iterable[i++];
+      }
+    };
+    return newIter;
+  }
+
+
+  // TODO(arv): Should we fall back on goog.structs.getValues()?
+  throw Error('Not implemented');
+};
+
+
+/**
+ * Calls a function for each element in the iterator with the element of the
+ * iterator passed as argument.
+ *
+ * @param {goog.iter.Iterator<VALUE>|goog.iter.Iterable} iterable  The iterator
+ *     to iterate over. If the iterable is an object {@code toIterator} will be
+ *     called on it.
+ * @param {function(this:THIS,VALUE,?,!goog.iter.Iterator<VALUE>)} f
+ *     The function to call for every element.  This function takes 3 arguments
+ *     (the element, undefined, and the iterator) and the return value is
+ *     irrelevant.  The reason for passing undefined as the second argument is
+ *     so that the same function can be used in {@see goog.array#forEach} as
+ *     well as others.  The third parameter is of type "number" for
+ *     arraylike objects, undefined, otherwise.
+ * @param {THIS=} opt_obj  The object to be used as the value of 'this' within
+ *     {@code f}.
+ * @template THIS, VALUE
+ */
+goog.iter.forEach = function(iterable, f, opt_obj) {
+  if (goog.isArrayLike(iterable)) {
+    /** @preserveTry */
+    try {
+      // NOTES: this passes the index number to the second parameter
+      // of the callback contrary to the documentation above.
+      goog.array.forEach(
+          /** @type {IArrayLike<?>} */ (iterable), f, opt_obj);
+    } catch (ex) {
+      if (ex !== goog.iter.StopIteration) {
+        throw ex;
+      }
+    }
+  } else {
+    iterable = goog.iter.toIterator(iterable);
+    /** @preserveTry */
+    try {
+      while (true) {
+        f.call(opt_obj, iterable.next(), undefined, iterable);
+      }
+    } catch (ex) {
+      if (ex !== goog.iter.StopIteration) {
+        throw ex;
+      }
+    }
+  }
+};
+
+
+/**
+ * Calls a function for every element in the iterator, and if the function
+ * returns true adds the element to a new iterator.
+ *
+ * @param {goog.iter.Iterator<VALUE>|goog.iter.Iterable} iterable The iterator
+ *     to iterate over.
+ * @param {
+ *     function(this:THIS,VALUE,undefined,!goog.iter.Iterator<VALUE>):boolean} f
+ *     The function to call for every element. This function takes 3 arguments
+ *     (the element, undefined, and the iterator) and should return a boolean.
+ *     If the return value is true the element will be included in the returned
+ *     iterator.  If it is false the element is not included.
+ * @param {THIS=} opt_obj The object to be used as the value of 'this' within
+ *     {@code f}.
+ * @return {!goog.iter.Iterator<VALUE>} A new iterator in which only elements
+ *     that passed the test are present.
+ * @template THIS, VALUE
+ */
+goog.iter.filter = function(iterable, f, opt_obj) {
+  var iterator = goog.iter.toIterator(iterable);
+  var newIter = new goog.iter.Iterator;
+  newIter.next = function() {
+    while (true) {
+      var val = iterator.next();
+      if (f.call(opt_obj, val, undefined, iterator)) {
+        return val;
+      }
+    }
+  };
+  return newIter;
+};
+
+
+/**
+ * Calls a function for every element in the iterator, and if the function
+ * returns false adds the element to a new iterator.
+ *
+ * @param {goog.iter.Iterator<VALUE>|goog.iter.Iterable} iterable The iterator
+ *     to iterate over.
+ * @param {
+ *     function(this:THIS,VALUE,undefined,!goog.iter.Iterator<VALUE>):boolean} f
+ *     The function to call for every element. This function takes 3 arguments
+ *     (the element, undefined, and the iterator) and should return a boolean.
+ *     If the return value is false the element will be included in the returned
+ *     iterator.  If it is true the element is not included.
+ * @param {THIS=} opt_obj The object to be used as the value of 'this' within
+ *     {@code f}.
+ * @return {!goog.iter.Iterator<VALUE>} A new iterator in which only elements
+ *     that did not pass the test are present.
+ * @template THIS, VALUE
+ */
+goog.iter.filterFalse = function(iterable, f, opt_obj) {
+  return goog.iter.filter(iterable, goog.functions.not(f), opt_obj);
+};
+
+
+/**
+ * Creates a new iterator that returns the values in a range.  This function
+ * can take 1, 2 or 3 arguments:
+ * <pre>
+ * range(5) same as range(0, 5, 1)
+ * range(2, 5) same as range(2, 5, 1)
+ * </pre>
+ *
+ * @param {number} startOrStop  The stop value if only one argument is provided.
+ *     The start value if 2 or more arguments are provided.  If only one
+ *     argument is used the start value is 0.
+ * @param {number=} opt_stop  The stop value.  If left out then the first
+ *     argument is used as the stop value.
+ * @param {number=} opt_step  The number to increment with between each call to
+ *     next.  This can be negative.
+ * @return {!goog.iter.Iterator<number>} A new iterator that returns the values
+ *     in the range.
+ */
+goog.iter.range = function(startOrStop, opt_stop, opt_step) {
+  var start = 0;
+  var stop = startOrStop;
+  var step = opt_step || 1;
+  if (arguments.length > 1) {
+    start = startOrStop;
+    stop = opt_stop;
+  }
+  if (step == 0) {
+    throw Error('Range step argument must not be zero');
+  }
+
+  var newIter = new goog.iter.Iterator;
+  newIter.next = function() {
+    if (step > 0 && start >= stop || step < 0 && start <= stop) {
+      throw goog.iter.StopIteration;
+    }
+    var rv = start;
+    start += step;
+    return rv;
+  };
+  return newIter;
+};
+
+
+/**
+ * Joins the values in a iterator with a delimiter.
+ * @param {goog.iter.Iterator<VALUE>|goog.iter.Iterable} iterable The iterator
+ *     to get the values from.
+ * @param {string} deliminator  The text to put between the values.
+ * @return {string} The joined value string.
+ * @template VALUE
+ */
+goog.iter.join = function(iterable, deliminator) {
+  return goog.iter.toArray(iterable).join(deliminator);
+};
+
+
+/**
+ * For every element in the iterator call a function and return a new iterator
+ * with that value.
+ *
+ * @param {!goog.iter.Iterator<VALUE>|!goog.iter.Iterable} iterable The
+ *     iterator to iterate over.
+ * @param {
+ *     function(this:THIS,VALUE,undefined,!goog.iter.Iterator<VALUE>):RESULT} f
+ *     The function to call for every element.  This function takes 3 arguments
+ *     (the element, undefined, and the iterator) and should return a new value.
+ * @param {THIS=} opt_obj The object to be used as the value of 'this' within
+ *     {@code f}.
+ * @return {!goog.iter.Iterator<RESULT>} A new iterator that returns the
+ *     results of applying the function to each element in the original
+ *     iterator.
+ * @template THIS, VALUE, RESULT
+ */
+goog.iter.map = function(iterable, f, opt_obj) {
+  var iterator = goog.iter.toIterator(iterable);
+  var newIter = new goog.iter.Iterator;
+  newIter.next = function() {
+    var val = iterator.next();
+    return f.call(opt_obj, val, undefined, iterator);
+  };
+  return newIter;
+};
+
+
+/**
+ * Passes every element of an iterator into a function and accumulates the
+ * result.
+ *
+ * @param {goog.iter.Iterator<VALUE>|goog.iter.Iterable} iterable The iterator
+ *     to iterate over.
+ * @param {function(this:THIS,VALUE,VALUE):VALUE} f The function to call for
+ *     every element. This function takes 2 arguments (the function's previous
+ *     result or the initial value, and the value of the current element).
+ *     function(previousValue, currentElement) : newValue.
+ * @param {VALUE} val The initial value to pass into the function on the first
+ *     call.
+ * @param {THIS=} opt_obj  The object to be used as the value of 'this' within
+ *     f.
+ * @return {VALUE} Result of evaluating f repeatedly across the values of
+ *     the iterator.
+ * @template THIS, VALUE
+ */
+goog.iter.reduce = function(iterable, f, val, opt_obj) {
+  var rval = val;
+  goog.iter.forEach(
+      iterable, function(val) { rval = f.call(opt_obj, rval, val); });
+  return rval;
+};
+
+
+/**
+ * Goes through the values in the iterator. Calls f for each of these, and if
+ * any of them returns true, this returns true (without checking the rest). If
+ * all return false this will return false.
+ *
+ * @param {goog.iter.Iterator<VALUE>|goog.iter.Iterable} iterable The iterator
+ *     object.
+ * @param {
+ *     function(this:THIS,VALUE,undefined,!goog.iter.Iterator<VALUE>):boolean} f
+ *     The function to call for every value. This function takes 3 arguments
+ *     (the value, undefined, and the iterator) and should return a boolean.
+ * @param {THIS=} opt_obj The object to be used as the value of 'this' within
+ *     {@code f}.
+ * @return {boolean} true if any value passes the test.
+ * @template THIS, VALUE
+ */
+goog.iter.some = function(iterable, f, opt_obj) {
+  iterable = goog.iter.toIterator(iterable);
+  /** @preserveTry */
+  try {
+    while (true) {
+      if (f.call(opt_obj, iterable.next(), undefined, iterable)) {
+        return true;
+      }
+    }
+  } catch (ex) {
+    if (ex !== goog.iter.StopIteration) {
+      throw ex;
+    }
+  }
+  return false;
+};
+
+
+/**
+ * Goes through the values in the iterator. Calls f for each of these and if any
+ * of them returns false this returns false (without checking the rest). If all
+ * return true this will return true.
+ *
+ * @param {goog.iter.Iterator<VALUE>|goog.iter.Iterable} iterable The iterator
+ *     object.
+ * @param {
+ *     function(this:THIS,VALUE,undefined,!goog.iter.Iterator<VALUE>):boolean} f
+ *     The function to call for every value. This function takes 3 arguments
+ *     (the value, undefined, and the iterator) and should return a boolean.
+ * @param {THIS=} opt_obj The object to be used as the value of 'this' within
+ *     {@code f}.
+ * @return {boolean} true if every value passes the test.
+ * @template THIS, VALUE
+ */
+goog.iter.every = function(iterable, f, opt_obj) {
+  iterable = goog.iter.toIterator(iterable);
+  /** @preserveTry */
+  try {
+    while (true) {
+      if (!f.call(opt_obj, iterable.next(), undefined, iterable)) {
+        return false;
+      }
+    }
+  } catch (ex) {
+    if (ex !== goog.iter.StopIteration) {
+      throw ex;
+    }
+  }
+  return true;
+};
+
+
+/**
+ * Takes zero or more iterables and returns one iterator that will iterate over
+ * them in the order chained.
+ * @param {...!goog.iter.Iterator<VALUE>|!goog.iter.Iterable} var_args Any
+ *     number of iterable objects.
+ * @return {!goog.iter.Iterator<VALUE>} Returns a new iterator that will
+ *     iterate over all the given iterables' contents.
+ * @template VALUE
+ */
+goog.iter.chain = function(var_args) {
+  return goog.iter.chainFromIterable(arguments);
+};
+
+
+/**
+ * Takes a single iterable containing zero or more iterables and returns one
+ * iterator that will iterate over each one in the order given.
+ * @see https://goo.gl/5NRp5d
+ * @param {goog.iter.Iterable} iterable The iterable of iterables to chain.
+ * @return {!goog.iter.Iterator<VALUE>} Returns a new iterator that will
+ *     iterate over all the contents of the iterables contained within
+ *     {@code iterable}.
+ * @template VALUE
+ */
+goog.iter.chainFromIterable = function(iterable) {
+  var iterator = goog.iter.toIterator(iterable);
+  var iter = new goog.iter.Iterator();
+  var current = null;
+
+  iter.next = function() {
+    while (true) {
+      if (current == null) {
+        var it = iterator.next();
+        current = goog.iter.toIterator(it);
+      }
+      try {
+        return current.next();
+      } catch (ex) {
+        if (ex !== goog.iter.StopIteration) {
+          throw ex;
+        }
+        current = null;
+      }
+    }
+  };
+
+  return iter;
+};
+
+
+/**
+ * Builds a new iterator that iterates over the original, but skips elements as
+ * long as a supplied function returns true.
+ * @param {goog.iter.Iterator<VALUE>|goog.iter.Iterable} iterable The iterator
+ *     object.
+ * @param {
+ *     function(this:THIS,VALUE,undefined,!goog.iter.Iterator<VALUE>):boolean} f
+ *     The function to call for every value. This function takes 3 arguments
+ *     (the value, undefined, and the iterator) and should return a boolean.
+ * @param {THIS=} opt_obj The object to be used as the value of 'this' within
+ *     {@code f}.
+ * @return {!goog.iter.Iterator<VALUE>} A new iterator that drops elements from
+ *     the original iterator as long as {@code f} is true.
+ * @template THIS, VALUE
+ */
+goog.iter.dropWhile = function(iterable, f, opt_obj) {
+  var iterator = goog.iter.toIterator(iterable);
+  var newIter = new goog.iter.Iterator;
+  var dropping = true;
+  newIter.next = function() {
+    while (true) {
+      var val = iterator.next();
+      if (dropping && f.call(opt_obj, val, undefined, iterator)) {
+        continue;
+      } else {
+        dropping = false;
+      }
+      return val;
+    }
+  };
+  return newIter;
+};
+
+
+/**
+ * Builds a new iterator that iterates over the original, but only as long as a
+ * supplied function returns true.
+ * @param {goog.iter.Iterator<VALUE>|goog.iter.Iterable} iterable The iterator
+ *     object.
+ * @param {
+ *     function(this:THIS,VALUE,undefined,!goog.iter.Iterator<VALUE>):boolean} f
+ *     The function to call for every value. This function takes 3 arguments
+ *     (the value, undefined, and the iterator) and should return a boolean.
+ * @param {THIS=} opt_obj This is used as the 'this' object in f when called.
+ * @return {!goog.iter.Iterator<VALUE>} A new iterator that keeps elements in
+ *     the original iterator as long as the function is true.
+ * @template THIS, VALUE
+ */
+goog.iter.takeWhile = function(iterable, f, opt_obj) {
+  var iterator = goog.iter.toIterator(iterable);
+  var iter = new goog.iter.Iterator();
+  iter.next = function() {
+    var val = iterator.next();
+    if (f.call(opt_obj, val, undefined, iterator)) {
+      return val;
+    }
+    throw goog.iter.StopIteration;
+  };
+  return iter;
+};
+
+
+/**
+ * Converts the iterator to an array
+ * @param {goog.iter.Iterator<VALUE>|goog.iter.Iterable} iterable The iterator
+ *     to convert to an array.
+ * @return {!Array<VALUE>} An array of the elements the iterator iterates over.
+ * @template VALUE
+ */
+goog.iter.toArray = function(iterable) {
+  // Fast path for array-like.
+  if (goog.isArrayLike(iterable)) {
+    return goog.array.toArray(/** @type {!IArrayLike<?>} */ (iterable));
+  }
+  iterable = goog.iter.toIterator(iterable);
+  var array = [];
+  goog.iter.forEach(iterable, function(val) { array.push(val); });
+  return array;
+};
+
+
+/**
+ * Iterates over two iterables and returns true if they contain the same
+ * sequence of elements and have the same length.
+ * @param {!goog.iter.Iterator<VALUE>|!goog.iter.Iterable} iterable1 The first
+ *     iterable object.
+ * @param {!goog.iter.Iterator<VALUE>|!goog.iter.Iterable} iterable2 The second
+ *     iterable object.
+ * @param {function(VALUE,VALUE):boolean=} opt_equalsFn Optional comparison
+ *     function.
+ *     Should take two arguments to compare, and return true if the arguments
+ *     are equal. Defaults to {@link goog.array.defaultCompareEquality} which
+ *     compares the elements using the built-in '===' operator.
+ * @return {boolean} true if the iterables contain the same sequence of elements
+ *     and have the same length.
+ * @template VALUE
+ */
+goog.iter.equals = function(iterable1, iterable2, opt_equalsFn) {
+  var fillValue = {};
+  var pairs = goog.iter.zipLongest(fillValue, iterable1, iterable2);
+  var equalsFn = opt_equalsFn || goog.array.defaultCompareEquality;
+  return goog.iter.every(
+      pairs, function(pair) { return equalsFn(pair[0], pair[1]); });
+};
+
+
+/**
+ * Advances the iterator to the next position, returning the given default value
+ * instead of throwing an exception if the iterator has no more entries.
+ * @param {goog.iter.Iterator<VALUE>|goog.iter.Iterable} iterable The iterable
+ *     object.
+ * @param {VALUE} defaultValue The value to return if the iterator is empty.
+ * @return {VALUE} The next item in the iteration, or defaultValue if the
+ *     iterator was empty.
+ * @template VALUE
+ */
+goog.iter.nextOrValue = function(iterable, defaultValue) {
+  try {
+    return goog.iter.toIterator(iterable).next();
+  } catch (e) {
+    if (e != goog.iter.StopIteration) {
+      throw e;
+    }
+    return defaultValue;
+  }
+};
+
+
+/**
+ * Cartesian product of zero or more sets.  Gives an iterator that gives every
+ * combination of one element chosen from each set.  For example,
+ * ([1, 2], [3, 4]) gives ([1, 3], [1, 4], [2, 3], [2, 4]).
+ * @see http://docs.python.org/library/itertools.html#itertools.product
+ * @param {...!IArrayLike<VALUE>} var_args Zero or more sets, as
+ *     arrays.
+ * @return {!goog.iter.Iterator<!Array<VALUE>>} An iterator that gives each
+ *     n-tuple (as an array).
+ * @template VALUE
+ */
+goog.iter.product = function(var_args) {
+  var someArrayEmpty =
+      goog.array.some(arguments, function(arr) { return !arr.length; });
+
+  // An empty set in a cartesian product gives an empty set.
+  if (someArrayEmpty || !arguments.length) {
+    return new goog.iter.Iterator();
+  }
+
+  var iter = new goog.iter.Iterator();
+  var arrays = arguments;
+
+  // The first indices are [0, 0, ...]
+  var indicies = goog.array.repeat(0, arrays.length);
+
+  iter.next = function() {
+
+    if (indicies) {
+      var retVal = goog.array.map(indicies, function(valueIndex, arrayIndex) {
+        return arrays[arrayIndex][valueIndex];
+      });
+
+      // Generate the next-largest indices for the next call.
+      // Increase the rightmost index. If it goes over, increase the next
+      // rightmost (like carry-over addition).
+      for (var i = indicies.length - 1; i >= 0; i--) {
+        // Assertion prevents compiler warning below.
+        goog.asserts.assert(indicies);
+        if (indicies[i] < arrays[i].length - 1) {
+          indicies[i]++;
+          break;
+        }
+
+        // We're at the last indices (the last element of every array), so
+        // the iteration is over on the next call.
+        if (i == 0) {
+          indicies = null;
+          break;
+        }
+        // Reset the index in this column and loop back to increment the
+        // next one.
+        indicies[i] = 0;
+      }
+      return retVal;
+    }
+
+    throw goog.iter.StopIteration;
+  };
+
+  return iter;
+};
+
+
+/**
+ * Create an iterator to cycle over the iterable's elements indefinitely.
+ * For example, ([1, 2, 3]) would return : 1, 2, 3, 1, 2, 3, ...
+ * @see: http://docs.python.org/library/itertools.html#itertools.cycle.
+ * @param {!goog.iter.Iterator<VALUE>|!goog.iter.Iterable} iterable The
+ *     iterable object.
+ * @return {!goog.iter.Iterator<VALUE>} An iterator that iterates indefinitely
+ *     over the values in {@code iterable}.
+ * @template VALUE
+ */
+goog.iter.cycle = function(iterable) {
+  var baseIterator = goog.iter.toIterator(iterable);
+
+  // We maintain a cache to store the iterable elements as we iterate
+  // over them. The cache is used to return elements once we have
+  // iterated over the iterable once.
+  var cache = [];
+  var cacheIndex = 0;
+
+  var iter = new goog.iter.Iterator();
+
+  // This flag is set after the iterable is iterated over once
+  var useCache = false;
+
+  iter.next = function() {
+    var returnElement = null;
+
+    // Pull elements off the original iterator if not using cache
+    if (!useCache) {
+      try {
+        // Return the element from the iterable
+        returnElement = baseIterator.next();
+        cache.push(returnElement);
+        return returnElement;
+      } catch (e) {
+        // If an exception other than StopIteration is thrown
+        // or if there are no elements to iterate over (the iterable was empty)
+        // throw an exception
+        if (e != goog.iter.StopIteration || goog.array.isEmpty(cache)) {
+          throw e;
+        }
+        // set useCache to true after we know that a 'StopIteration' exception
+        // was thrown and the cache is not empty (to handle the 'empty iterable'
+        // use case)
+        useCache = true;
+      }
+    }
+
+    returnElement = cache[cacheIndex];
+    cacheIndex = (cacheIndex + 1) % cache.length;
+
+    return returnElement;
+  };
+
+  return iter;
+};
+
+
+/**
+ * Creates an iterator that counts indefinitely from a starting value.
+ * @see http://docs.python.org/2/library/itertools.html#itertools.count
+ * @param {number=} opt_start The starting value. Default is 0.
+ * @param {number=} opt_step The number to increment with between each call to
+ *     next. Negative and floating point numbers are allowed. Default is 1.
+ * @return {!goog.iter.Iterator<number>} A new iterator that returns the values
+ *     in the series.
+ */
+goog.iter.count = function(opt_start, opt_step) {
+  var counter = opt_start || 0;
+  var step = goog.isDef(opt_step) ? opt_step : 1;
+  var iter = new goog.iter.Iterator();
+
+  iter.next = function() {
+    var returnValue = counter;
+    counter += step;
+    return returnValue;
+  };
+
+  return iter;
+};
+
+
+/**
+ * Creates an iterator that returns the same object or value repeatedly.
+ * @param {VALUE} value Any object or value to repeat.
+ * @return {!goog.iter.Iterator<VALUE>} A new iterator that returns the
+ *     repeated value.
+ * @template VALUE
+ */
+goog.iter.repeat = function(value) {
+  var iter = new goog.iter.Iterator();
+
+  iter.next = goog.functions.constant(value);
+
+  return iter;
+};
+
+
+/**
+ * Creates an iterator that returns running totals from the numbers in
+ * {@code iterable}. For example, the array {@code [1, 2, 3, 4, 5]} yields
+ * {@code 1 -> 3 -> 6 -> 10 -> 15}.
+ * @see http://docs.python.org/3.2/library/itertools.html#itertools.accumulate
+ * @param {!goog.iter.Iterable<number>} iterable The iterable of numbers to
+ *     accumulate.
+ * @return {!goog.iter.Iterator<number>} A new iterator that returns the
+ *     numbers in the series.
+ */
+goog.iter.accumulate = function(iterable) {
+  var iterator = goog.iter.toIterator(iterable);
+  var total = 0;
+  var iter = new goog.iter.Iterator();
+
+  iter.next = function() {
+    total += iterator.next();
+    return total;
+  };
+
+  return iter;
+};
+
+
+/**
+ * Creates an iterator that returns arrays containing the ith elements from the
+ * provided iterables. The returned arrays will be the same size as the number
+ * of iterables given in {@code var_args}. Once the shortest iterable is
+ * exhausted, subsequent calls to {@code next()} will throw
+ * {@code goog.iter.StopIteration}.
+ * @see http://docs.python.org/2/library/itertools.html#itertools.izip
+ * @param {...!goog.iter.Iterator<VALUE>|!goog.iter.Iterable} var_args Any
+ *     number of iterable objects.
+ * @return {!goog.iter.Iterator<!Array<VALUE>>} A new iterator that returns
+ *     arrays of elements from the provided iterables.
+ * @template VALUE
+ */
+goog.iter.zip = function(var_args) {
+  var args = arguments;
+  var iter = new goog.iter.Iterator();
+
+  if (args.length > 0) {
+    var iterators = goog.array.map(args, goog.iter.toIterator);
+    iter.next = function() {
+      var arr = goog.array.map(iterators, function(it) { return it.next(); });
+      return arr;
+    };
+  }
+
+  return iter;
+};
+
+
+/**
+ * Creates an iterator that returns arrays containing the ith elements from the
+ * provided iterables. The returned arrays will be the same size as the number
+ * of iterables given in {@code var_args}. Shorter iterables will be extended
+ * with {@code fillValue}. Once the longest iterable is exhausted, subsequent
+ * calls to {@code next()} will throw {@code goog.iter.StopIteration}.
+ * @see http://docs.python.org/2/library/itertools.html#itertools.izip_longest
+ * @param {VALUE} fillValue The object or value used to fill shorter iterables.
+ * @param {...!goog.iter.Iterator<VALUE>|!goog.iter.Iterable} var_args Any
+ *     number of iterable objects.
+ * @return {!goog.iter.Iterator<!Array<VALUE>>} A new iterator that returns
+ *     arrays of elements from the provided iterables.
+ * @template VALUE
+ */
+goog.iter.zipLongest = function(fillValue, var_args) {
+  var args = goog.array.slice(arguments, 1);
+  var iter = new goog.iter.Iterator();
+
+  if (args.length > 0) {
+    var iterators = goog.array.map(args, goog.iter.toIterator);
+
+    iter.next = function() {
+      var iteratorsHaveValues = false;  // false when all iterators are empty.
+      var arr = goog.array.map(iterators, function(it) {
+        var returnValue;
+        try {
+          returnValue = it.next();
+          // Iterator had a value, so we've not exhausted the iterators.
+          // Set flag accordingly.
+          iteratorsHaveValues = true;
+        } catch (ex) {
+          if (ex !== goog.iter.StopIteration) {
+            throw ex;
+          }
+          returnValue = fillValue;
+        }
+        return returnValue;
+      });
+
+      if (!iteratorsHaveValues) {
+        throw goog.iter.StopIteration;
+      }
+      return arr;
+    };
+  }
+
+  return iter;
+};
+
+
+/**
+ * Creates an iterator that filters {@code iterable} based on a series of
+ * {@code selectors}. On each call to {@code next()}, one item is taken from
+ * both the {@code iterable} and {@code selectors} iterators. If the item from
+ * {@code selectors} evaluates to true, the item from {@code iterable} is given.
+ * Otherwise, it is skipped. Once either {@code iterable} or {@code selectors}
+ * is exhausted, subsequent calls to {@code next()} will throw
+ * {@code goog.iter.StopIteration}.
+ * @see http://docs.python.org/2/library/itertools.html#itertools.compress
+ * @param {!goog.iter.Iterator<VALUE>|!goog.iter.Iterable} iterable The
+ *     iterable to filter.
+ * @param {!goog.iter.Iterator<VALUE>|!goog.iter.Iterable} selectors An
+ *     iterable of items to be evaluated in a boolean context to determine if
+ *     the corresponding element in {@code iterable} should be included in the
+ *     result.
+ * @return {!goog.iter.Iterator<VALUE>} A new iterator that returns the
+ *     filtered values.
+ * @template VALUE
+ */
+goog.iter.compress = function(iterable, selectors) {
+  var selectorIterator = goog.iter.toIterator(selectors);
+
+  return goog.iter.filter(
+      iterable, function() { return !!selectorIterator.next(); });
+};
+
+
+
+/**
+ * Implements the {@code goog.iter.groupBy} iterator.
+ * @param {!goog.iter.Iterator<VALUE>|!goog.iter.Iterable} iterable The
+ *     iterable to group.
+ * @param {function(VALUE): KEY=} opt_keyFunc  Optional function for
+ *     determining the key value for each group in the {@code iterable}. Default
+ *     is the identity function.
+ * @constructor
+ * @extends {goog.iter.Iterator<!Array<?>>}
+ * @template KEY, VALUE
+ * @private
+ */
+goog.iter.GroupByIterator_ = function(iterable, opt_keyFunc) {
+
+  /**
+   * The iterable to group, coerced to an iterator.
+   * @type {!goog.iter.Iterator}
+   */
+  this.iterator = goog.iter.toIterator(iterable);
+
+  /**
+   * A function for determining the key value for each element in the iterable.
+   * If no function is provided, the identity function is used and returns the
+   * element unchanged.
+   * @type {function(VALUE): KEY}
+   */
+  this.keyFunc = opt_keyFunc || goog.functions.identity;
+
+  /**
+   * The target key for determining the start of a group.
+   * @type {KEY}
+   */
+  this.targetKey;
+
+  /**
+   * The current key visited during iteration.
+   * @type {KEY}
+   */
+  this.currentKey;
+
+  /**
+   * The current value being added to the group.
+   * @type {VALUE}
+   */
+  this.currentValue;
+};
+goog.inherits(goog.iter.GroupByIterator_, goog.iter.Iterator);
+
+
+/** @override */
+goog.iter.GroupByIterator_.prototype.next = function() {
+  while (this.currentKey == this.targetKey) {
+    this.currentValue = this.iterator.next();  // Exits on StopIteration
+    this.currentKey = this.keyFunc(this.currentValue);
+  }
+  this.targetKey = this.currentKey;
+  return [this.currentKey, this.groupItems_(this.targetKey)];
+};
+
+
+/**
+ * Performs the grouping of objects using the given key.
+ * @param {KEY} targetKey  The target key object for the group.
+ * @return {!Array<VALUE>} An array of grouped objects.
+ * @private
+ */
+goog.iter.GroupByIterator_.prototype.groupItems_ = function(targetKey) {
+  var arr = [];
+  while (this.currentKey == targetKey) {
+    arr.push(this.currentValue);
+    try {
+      this.currentValue = this.iterator.next();
+    } catch (ex) {
+      if (ex !== goog.iter.StopIteration) {
+        throw ex;
+      }
+      break;
+    }
+    this.currentKey = this.keyFunc(this.currentValue);
+  }
+  return arr;
+};
+
+
+/**
+ * Creates an iterator that returns arrays containing elements from the
+ * {@code iterable} grouped by a key value. For iterables with repeated
+ * elements (i.e. sorted according to a particular key function), this function
+ * has a {@code uniq}-like effect. For example, grouping the array:
+ * {@code [A, B, B, C, C, A]} produces
+ * {@code [A, [A]], [B, [B, B]], [C, [C, C]], [A, [A]]}.
+ * @see http://docs.python.org/2/library/itertools.html#itertools.groupby
+ * @param {!goog.iter.Iterator<VALUE>|!goog.iter.Iterable} iterable The
+ *     iterable to group.
+ * @param {function(VALUE): KEY=} opt_keyFunc  Optional function for
+ *     determining the key value for each group in the {@code iterable}. Default
+ *     is the identity function.
+ * @return {!goog.iter.Iterator<!Array<?>>} A new iterator that returns
+ *     arrays of consecutive key and groups.
+ * @template KEY, VALUE
+ */
+goog.iter.groupBy = function(iterable, opt_keyFunc) {
+  return new goog.iter.GroupByIterator_(iterable, opt_keyFunc);
+};
+
+
+/**
+ * Gives an iterator that gives the result of calling the given function
+ * <code>f</code> with the arguments taken from the next element from
+ * <code>iterable</code> (the elements are expected to also be iterables).
+ *
+ * Similar to {@see goog.iter#map} but allows the function to accept multiple
+ * arguments from the iterable.
+ *
+ * @param {!goog.iter.Iterable<!goog.iter.Iterable>} iterable The iterable of
+ *     iterables to iterate over.
+ * @param {function(this:THIS,...*):RESULT} f The function to call for every
+ *     element.  This function takes N+2 arguments, where N represents the
+ *     number of items from the next element of the iterable. The two
+ *     additional arguments passed to the function are undefined and the
+ *     iterator itself. The function should return a new value.
+ * @param {THIS=} opt_obj The object to be used as the value of 'this' within
+ *     {@code f}.
+ * @return {!goog.iter.Iterator<RESULT>} A new iterator that returns the
+ *     results of applying the function to each element in the original
+ *     iterator.
+ * @template THIS, RESULT
+ */
+goog.iter.starMap = function(iterable, f, opt_obj) {
+  var iterator = goog.iter.toIterator(iterable);
+  var iter = new goog.iter.Iterator();
+
+  iter.next = function() {
+    var args = goog.iter.toArray(iterator.next());
+    return f.apply(opt_obj, goog.array.concat(args, undefined, iterator));
+  };
+
+  return iter;
+};
+
+
+/**
+ * Returns an array of iterators each of which can iterate over the values in
+ * {@code iterable} without advancing the others.
+ * @see http://docs.python.org/2/library/itertools.html#itertools.tee
+ * @param {!goog.iter.Iterator<VALUE>|!goog.iter.Iterable} iterable The
+ *     iterable to tee.
+ * @param {number=} opt_num  The number of iterators to create. Default is 2.
+ * @return {!Array<goog.iter.Iterator<VALUE>>} An array of iterators.
+ * @template VALUE
+ */
+goog.iter.tee = function(iterable, opt_num) {
+  var iterator = goog.iter.toIterator(iterable);
+  var num = goog.isNumber(opt_num) ? opt_num : 2;
+  var buffers =
+      goog.array.map(goog.array.range(num), function() { return []; });
+
+  var addNextIteratorValueToBuffers = function() {
+    var val = iterator.next();
+    goog.array.forEach(buffers, function(buffer) { buffer.push(val); });
+  };
+
+  var createIterator = function(buffer) {
+    // Each tee'd iterator has an associated buffer (initially empty). When a
+    // tee'd iterator's buffer is empty, it calls
+    // addNextIteratorValueToBuffers(), adding the next value to all tee'd
+    // iterators' buffers, and then returns that value. This allows each
+    // iterator to be advanced independently.
+    var iter = new goog.iter.Iterator();
+
+    iter.next = function() {
+      if (goog.array.isEmpty(buffer)) {
+        addNextIteratorValueToBuffers();
+      }
+      goog.asserts.assert(!goog.array.isEmpty(buffer));
+      return buffer.shift();
+    };
+
+    return iter;
+  };
+
+  return goog.array.map(buffers, createIterator);
+};
+
+
+/**
+ * Creates an iterator that returns arrays containing a count and an element
+ * obtained from the given {@code iterable}.
+ * @see http://docs.python.org/2/library/functions.html#enumerate
+ * @param {!goog.iter.Iterator<VALUE>|!goog.iter.Iterable} iterable The
+ *     iterable to enumerate.
+ * @param {number=} opt_start  Optional starting value. Default is 0.
+ * @return {!goog.iter.Iterator<!Array<?>>} A new iterator containing
+ *     count/item pairs.
+ * @template VALUE
+ */
+goog.iter.enumerate = function(iterable, opt_start) {
+  return goog.iter.zip(goog.iter.count(opt_start), iterable);
+};
+
+
+/**
+ * Creates an iterator that returns the first {@code limitSize} elements from an
+ * iterable. If this number is greater than the number of elements in the
+ * iterable, all the elements are returned.
+ * @see http://goo.gl/V0sihp Inspired by the limit iterator in Guava.
+ * @param {!goog.iter.Iterator<VALUE>|!goog.iter.Iterable} iterable The
+ *     iterable to limit.
+ * @param {number} limitSize  The maximum number of elements to return.
+ * @return {!goog.iter.Iterator<VALUE>} A new iterator containing
+ *     {@code limitSize} elements.
+ * @template VALUE
+ */
+goog.iter.limit = function(iterable, limitSize) {
+  goog.asserts.assert(goog.math.isInt(limitSize) && limitSize >= 0);
+
+  var iterator = goog.iter.toIterator(iterable);
+
+  var iter = new goog.iter.Iterator();
+  var remaining = limitSize;
+
+  iter.next = function() {
+    if (remaining-- > 0) {
+      return iterator.next();
+    }
+    throw goog.iter.StopIteration;
+  };
+
+  return iter;
+};
+
+
+/**
+ * Creates an iterator that is advanced {@code count} steps ahead. Consumed
+ * values are silently discarded. If {@code count} is greater than the number
+ * of elements in {@code iterable}, an empty iterator is returned. Subsequent
+ * calls to {@code next()} will throw {@code goog.iter.StopIteration}.
+ * @param {!goog.iter.Iterator<VALUE>|!goog.iter.Iterable} iterable The
+ *     iterable to consume.
+ * @param {number} count  The number of elements to consume from the iterator.
+ * @return {!goog.iter.Iterator<VALUE>} An iterator advanced zero or more steps
+ *     ahead.
+ * @template VALUE
+ */
+goog.iter.consume = function(iterable, count) {
+  goog.asserts.assert(goog.math.isInt(count) && count >= 0);
+
+  var iterator = goog.iter.toIterator(iterable);
+
+  while (count-- > 0) {
+    goog.iter.nextOrValue(iterator, null);
+  }
+
+  return iterator;
+};
+
+
+/**
+ * Creates an iterator that returns a range of elements from an iterable.
+ * Similar to {@see goog.array#slice} but does not support negative indexes.
+ * @param {!goog.iter.Iterator<VALUE>|!goog.iter.Iterable} iterable The
+ *     iterable to slice.
+ * @param {number} start  The index of the first element to return.
+ * @param {number=} opt_end  The index after the last element to return. If
+ *     defined, must be greater than or equal to {@code start}.
+ * @return {!goog.iter.Iterator<VALUE>} A new iterator containing a slice of
+ *     the original.
+ * @template VALUE
+ */
+goog.iter.slice = function(iterable, start, opt_end) {
+  goog.asserts.assert(goog.math.isInt(start) && start >= 0);
+
+  var iterator = goog.iter.consume(iterable, start);
+
+  if (goog.isNumber(opt_end)) {
+    goog.asserts.assert(goog.math.isInt(opt_end) && opt_end >= start);
+    iterator = goog.iter.limit(iterator, opt_end - start /* limitSize */);
+  }
+
+  return iterator;
+};
+
+
+/**
+ * Checks an array for duplicate elements.
+ * @param {?IArrayLike<VALUE>} arr The array to check for
+ *     duplicates.
+ * @return {boolean} True, if the array contains duplicates, false otherwise.
+ * @private
+ * @template VALUE
+ */
+// TODO(user): Consider moving this into goog.array as a public function.
+goog.iter.hasDuplicates_ = function(arr) {
+  var deduped = [];
+  goog.array.removeDuplicates(arr, deduped);
+  return arr.length != deduped.length;
+};
+
+
+/**
+ * Creates an iterator that returns permutations of elements in
+ * {@code iterable}.
+ *
+ * Permutations are obtained by taking the Cartesian product of
+ * {@code opt_length} iterables and filtering out those with repeated
+ * elements. For example, the permutations of {@code [1,2,3]} are
+ * {@code [[1,2,3], [1,3,2], [2,1,3], [2,3,1], [3,1,2], [3,2,1]]}.
+ * @see http://docs.python.org/2/library/itertools.html#itertools.permutations
+ * @param {!goog.iter.Iterator<VALUE>|!goog.iter.Iterable} iterable The
+ *     iterable from which to generate permutations.
+ * @param {number=} opt_length Length of each permutation. If omitted, defaults
+ *     to the length of {@code iterable}.
+ * @return {!goog.iter.Iterator<!Array<VALUE>>} A new iterator containing the
+ *     permutations of {@code iterable}.
+ * @template VALUE
+ */
+goog.iter.permutations = function(iterable, opt_length) {
+  var elements = goog.iter.toArray(iterable);
+  var length = goog.isNumber(opt_length) ? opt_length : elements.length;
+
+  var sets = goog.array.repeat(elements, length);
+  var product = goog.iter.product.apply(undefined, sets);
+
+  return goog.iter.filter(
+      product, function(arr) { return !goog.iter.hasDuplicates_(arr); });
+};
+
+
+/**
+ * Creates an iterator that returns combinations of elements from
+ * {@code iterable}.
+ *
+ * Combinations are obtained by taking the {@see goog.iter#permutations} of
+ * {@code iterable} and filtering those whose elements appear in the order they
+ * are encountered in {@code iterable}. For example, the 3-length combinations
+ * of {@code [0,1,2,3]} are {@code [[0,1,2], [0,1,3], [0,2,3], [1,2,3]]}.
+ * @see http://docs.python.org/2/library/itertools.html#itertools.combinations
+ * @param {!goog.iter.Iterator<VALUE>|!goog.iter.Iterable} iterable The
+ *     iterable from which to generate combinations.
+ * @param {number} length The length of each combination.
+ * @return {!goog.iter.Iterator<!Array<VALUE>>} A new iterator containing
+ *     combinations from the {@code iterable}.
+ * @template VALUE
+ */
+goog.iter.combinations = function(iterable, length) {
+  var elements = goog.iter.toArray(iterable);
+  var indexes = goog.iter.range(elements.length);
+  var indexIterator = goog.iter.permutations(indexes, length);
+  // sortedIndexIterator will now give arrays of with the given length that
+  // indicate what indexes into "elements" should be returned on each iteration.
+  var sortedIndexIterator = goog.iter.filter(
+      indexIterator, function(arr) { return goog.array.isSorted(arr); });
+
+  var iter = new goog.iter.Iterator();
+
+  function getIndexFromElements(index) { return elements[index]; }
+
+  iter.next = function() {
+    return goog.array.map(sortedIndexIterator.next(), getIndexFromElements);
+  };
+
+  return iter;
+};
+
+
+/**
+ * Creates an iterator that returns combinations of elements from
+ * {@code iterable}, with repeated elements possible.
+ *
+ * Combinations are obtained by taking the Cartesian product of {@code length}
+ * iterables and filtering those whose elements appear in the order they are
+ * encountered in {@code iterable}. For example, the 2-length combinations of
+ * {@code [1,2,3]} are {@code [[1,1], [1,2], [1,3], [2,2], [2,3], [3,3]]}.
+ * @see https://goo.gl/C0yXe4
+ * @see https://goo.gl/djOCsk
+ * @param {!goog.iter.Iterator<VALUE>|!goog.iter.Iterable} iterable The
+ *     iterable to combine.
+ * @param {number} length The length of each combination.
+ * @return {!goog.iter.Iterator<!Array<VALUE>>} A new iterator containing
+ *     combinations from the {@code iterable}.
+ * @template VALUE
+ */
+goog.iter.combinationsWithReplacement = function(iterable, length) {
+  var elements = goog.iter.toArray(iterable);
+  var indexes = goog.array.range(elements.length);
+  var sets = goog.array.repeat(indexes, length);
+  var indexIterator = goog.iter.product.apply(undefined, sets);
+  // sortedIndexIterator will now give arrays of with the given length that
+  // indicate what indexes into "elements" should be returned on each iteration.
+  var sortedIndexIterator = goog.iter.filter(
+      indexIterator, function(arr) { return goog.array.isSorted(arr); });
+
+  var iter = new goog.iter.Iterator();
+
+  function getIndexFromElements(index) { return elements[index]; }
+
+  iter.next = function() {
+    return goog.array.map(
+        /** @type {!Array<number>} */
+        (sortedIndexIterator.next()), getIndexFromElements);
+  };
+
+  return iter;
+};
+
+// Copyright 2006 The Closure Library Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS-IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+/**
+ * @fileoverview Datastructure: Hash Map.
+ *
+ * @author arv@google.com (Erik Arvidsson)
+ *
+ * This file contains an implementation of a Map structure. It implements a lot
+ * of the methods used in goog.structs so those functions work on hashes. This
+ * is best suited for complex key types. For simple keys such as numbers and
+ * strings consider using the lighter-weight utilities in goog.object.
+ */
+
+
+goog.provide('goog.structs.Map');
+
+goog.require('goog.iter.Iterator');
+goog.require('goog.iter.StopIteration');
+goog.require('goog.object');
+
+
+
+/**
+ * Class for Hash Map datastructure.
+ * @param {*=} opt_map Map or Object to initialize the map with.
+ * @param {...*} var_args If 2 or more arguments are present then they
+ *     will be used as key-value pairs.
+ * @constructor
+ * @template K, V
+ */
+goog.structs.Map = function(opt_map, var_args) {
+
+  /**
+   * Underlying JS object used to implement the map.
+   * @private {!Object}
+   */
+  this.map_ = {};
+
+  /**
+   * An array of keys. This is necessary for two reasons:
+   *   1. Iterating the keys using for (var key in this.map_) allocates an
+   *      object for every key in IE which is really bad for IE6 GC perf.
+   *   2. Without a side data structure, we would need to escape all the keys
+   *      as that would be the only way we could tell during iteration if the
+   *      key was an internal key or a property of the object.
+   *
+   * This array can contain deleted keys so it's necessary to check the map
+   * as well to see if the key is still in the map (this doesn't require a
+   * memory allocation in IE).
+   * @private {!Array<string>}
+   */
+  this.keys_ = [];
+
+  /**
+   * The number of key value pairs in the map.
+   * @private {number}
+   */
+  this.count_ = 0;
+
+  /**
+   * Version used to detect changes while iterating.
+   * @private {number}
+   */
+  this.version_ = 0;
+
+  var argLength = arguments.length;
+
+  if (argLength > 1) {
+    if (argLength % 2) {
+      throw Error('Uneven number of arguments');
+    }
+    for (var i = 0; i < argLength; i += 2) {
+      this.set(arguments[i], arguments[i + 1]);
+    }
+  } else if (opt_map) {
+    this.addAll(/** @type {Object} */ (opt_map));
+  }
+};
+
+
+/**
+ * @return {number} The number of key-value pairs in the map.
+ */
+goog.structs.Map.prototype.getCount = function() {
+  return this.count_;
+};
+
+
+/**
+ * Returns the values of the map.
+ * @return {!Array<V>} The values in the map.
+ */
+goog.structs.Map.prototype.getValues = function() {
+  this.cleanupKeysArray_();
+
+  var rv = [];
+  for (var i = 0; i < this.keys_.length; i++) {
+    var key = this.keys_[i];
+    rv.push(this.map_[key]);
+  }
+  return rv;
+};
+
+
+/**
+ * Returns the keys of the map.
+ * @return {!Array<string>} Array of string values.
+ */
+goog.structs.Map.prototype.getKeys = function() {
+  this.cleanupKeysArray_();
+  return /** @type {!Array<string>} */ (this.keys_.concat());
+};
+
+
+/**
+ * Whether the map contains the given key.
+ * @param {*} key The key to check for.
+ * @return {boolean} Whether the map contains the key.
+ */
+goog.structs.Map.prototype.containsKey = function(key) {
+  return goog.structs.Map.hasKey_(this.map_, key);
+};
+
+
+/**
+ * Whether the map contains the given value. This is O(n).
+ * @param {V} val The value to check for.
+ * @return {boolean} Whether the map contains the value.
+ */
+goog.structs.Map.prototype.containsValue = function(val) {
+  for (var i = 0; i < this.keys_.length; i++) {
+    var key = this.keys_[i];
+    if (goog.structs.Map.hasKey_(this.map_, key) && this.map_[key] == val) {
+      return true;
+    }
+  }
+  return false;
+};
+
+
+/**
+ * Whether this map is equal to the argument map.
+ * @param {goog.structs.Map} otherMap The map against which to test equality.
+ * @param {function(V, V): boolean=} opt_equalityFn Optional equality function
+ *     to test equality of values. If not specified, this will test whether
+ *     the values contained in each map are identical objects.
+ * @return {boolean} Whether the maps are equal.
+ */
+goog.structs.Map.prototype.equals = function(otherMap, opt_equalityFn) {
+  if (this === otherMap) {
+    return true;
+  }
+
+  if (this.count_ != otherMap.getCount()) {
+    return false;
+  }
+
+  var equalityFn = opt_equalityFn || goog.structs.Map.defaultEquals;
+
+  this.cleanupKeysArray_();
+  for (var key, i = 0; key = this.keys_[i]; i++) {
+    if (!equalityFn(this.get(key), otherMap.get(key))) {
+      return false;
+    }
+  }
+
+  return true;
+};
+
+
+/**
+ * Default equality test for values.
+ * @param {*} a The first value.
+ * @param {*} b The second value.
+ * @return {boolean} Whether a and b reference the same object.
+ */
+goog.structs.Map.defaultEquals = function(a, b) {
+  return a === b;
+};
+
+
+/**
+ * @return {boolean} Whether the map is empty.
+ */
+goog.structs.Map.prototype.isEmpty = function() {
+  return this.count_ == 0;
+};
+
+
+/**
+ * Removes all key-value pairs from the map.
+ */
+goog.structs.Map.prototype.clear = function() {
+  this.map_ = {};
+  this.keys_.length = 0;
+  this.count_ = 0;
+  this.version_ = 0;
+};
+
+
+/**
+ * Removes a key-value pair based on the key. This is O(logN) amortized due to
+ * updating the keys array whenever the count becomes half the size of the keys
+ * in the keys array.
+ * @param {*} key  The key to remove.
+ * @return {boolean} Whether object was removed.
+ */
+goog.structs.Map.prototype.remove = function(key) {
+  if (goog.structs.Map.hasKey_(this.map_, key)) {
+    delete this.map_[key];
+    this.count_--;
+    this.version_++;
+
+    // clean up the keys array if the threshold is hit
+    if (this.keys_.length > 2 * this.count_) {
+      this.cleanupKeysArray_();
+    }
+
+    return true;
+  }
+  return false;
+};
+
+
+/**
+ * Cleans up the temp keys array by removing entries that are no longer in the
+ * map.
+ * @private
+ */
+goog.structs.Map.prototype.cleanupKeysArray_ = function() {
+  if (this.count_ != this.keys_.length) {
+    // First remove keys that are no longer in the map.
+    var srcIndex = 0;
+    var destIndex = 0;
+    while (srcIndex < this.keys_.length) {
+      var key = this.keys_[srcIndex];
+      if (goog.structs.Map.hasKey_(this.map_, key)) {
+        this.keys_[destIndex++] = key;
+      }
+      srcIndex++;
+    }
+    this.keys_.length = destIndex;
+  }
+
+  if (this.count_ != this.keys_.length) {
+    // If the count still isn't correct, that means we have duplicates. This can
+    // happen when the same key is added and removed multiple times. Now we have
+    // to allocate one extra Object to remove the duplicates. This could have
+    // been done in the first pass, but in the common case, we can avoid
+    // allocating an extra object by only doing this when necessary.
+    var seen = {};
+    var srcIndex = 0;
+    var destIndex = 0;
+    while (srcIndex < this.keys_.length) {
+      var key = this.keys_[srcIndex];
+      if (!(goog.structs.Map.hasKey_(seen, key))) {
+        this.keys_[destIndex++] = key;
+        seen[key] = 1;
+      }
+      srcIndex++;
+    }
+    this.keys_.length = destIndex;
+  }
+};
+
+
+/**
+ * Returns the value for the given key.  If the key is not found and the default
+ * value is not given this will return {@code undefined}.
+ * @param {*} key The key to get the value for.
+ * @param {DEFAULT=} opt_val The value to return if no item is found for the
+ *     given key, defaults to undefined.
+ * @return {V|DEFAULT} The value for the given key.
+ * @template DEFAULT
+ */
+goog.structs.Map.prototype.get = function(key, opt_val) {
+  if (goog.structs.Map.hasKey_(this.map_, key)) {
+    return this.map_[key];
+  }
+  return opt_val;
+};
+
+
+/**
+ * Adds a key-value pair to the map.
+ * @param {*} key The key.
+ * @param {V} value The value to add.
+ * @return {*} Some subclasses return a value.
+ */
+goog.structs.Map.prototype.set = function(key, value) {
+  if (!(goog.structs.Map.hasKey_(this.map_, key))) {
+    this.count_++;
+    // TODO(johnlenz): This class lies, it claims to return an array of string
+    // keys, but instead returns the original object used.
+    this.keys_.push(/** @type {?} */ (key));
+    // Only change the version if we add a new key.
+    this.version_++;
+  }
+  this.map_[key] = value;
+};
+
+
+/**
+ * Adds multiple key-value pairs from another goog.structs.Map or Object.
+ * @param {Object} map  Object containing the data to add.
+ */
+goog.structs.Map.prototype.addAll = function(map) {
+  var keys, values;
+  if (map instanceof goog.structs.Map) {
+    keys = map.getKeys();
+    values = map.getValues();
+  } else {
+    keys = goog.object.getKeys(map);
+    values = goog.object.getValues(map);
+  }
+  // we could use goog.array.forEach here but I don't want to introduce that
+  // dependency just for this.
+  for (var i = 0; i < keys.length; i++) {
+    this.set(keys[i], values[i]);
+  }
+};
+
+
+/**
+ * Calls the given function on each entry in the map.
+ * @param {function(this:T, V, K, goog.structs.Map<K,V>)} f
+ * @param {T=} opt_obj The value of "this" inside f.
+ * @template T
+ */
+goog.structs.Map.prototype.forEach = function(f, opt_obj) {
+  var keys = this.getKeys();
+  for (var i = 0; i < keys.length; i++) {
+    var key = keys[i];
+    var value = this.get(key);
+    f.call(opt_obj, value, key, this);
+  }
+};
+
+
+/**
+ * Clones a map and returns a new map.
+ * @return {!goog.structs.Map} A new map with the same key-value pairs.
+ */
+goog.structs.Map.prototype.clone = function() {
+  return new goog.structs.Map(this);
+};
+
+
+/**
+ * Returns a new map in which all the keys and values are interchanged
+ * (keys become values and values become keys). If multiple keys map to the
+ * same value, the chosen transposed value is implementation-dependent.
+ *
+ * It acts very similarly to {goog.object.transpose(Object)}.
+ *
+ * @return {!goog.structs.Map} The transposed map.
+ */
+goog.structs.Map.prototype.transpose = function() {
+  var transposed = new goog.structs.Map();
+  for (var i = 0; i < this.keys_.length; i++) {
+    var key = this.keys_[i];
+    var value = this.map_[key];
+    transposed.set(value, key);
+  }
+
+  return transposed;
+};
+
+
+/**
+ * @return {!Object} Object representation of the map.
+ */
+goog.structs.Map.prototype.toObject = function() {
+  this.cleanupKeysArray_();
+  var obj = {};
+  for (var i = 0; i < this.keys_.length; i++) {
+    var key = this.keys_[i];
+    obj[key] = this.map_[key];
+  }
+  return obj;
+};
+
+
+/**
+ * Returns an iterator that iterates over the keys in the map.  Removal of keys
+ * while iterating might have undesired side effects.
+ * @return {!goog.iter.Iterator} An iterator over the keys in the map.
+ */
+goog.structs.Map.prototype.getKeyIterator = function() {
+  return this.__iterator__(true);
+};
+
+
+/**
+ * Returns an iterator that iterates over the values in the map.  Removal of
+ * keys while iterating might have undesired side effects.
+ * @return {!goog.iter.Iterator} An iterator over the values in the map.
+ */
+goog.structs.Map.prototype.getValueIterator = function() {
+  return this.__iterator__(false);
+};
+
+
+/**
+ * Returns an iterator that iterates over the values or the keys in the map.
+ * This throws an exception if the map was mutated since the iterator was
+ * created.
+ * @param {boolean=} opt_keys True to iterate over the keys. False to iterate
+ *     over the values.  The default value is false.
+ * @return {!goog.iter.Iterator} An iterator over the values or keys in the map.
+ */
+goog.structs.Map.prototype.__iterator__ = function(opt_keys) {
+  // Clean up keys to minimize the risk of iterating over dead keys.
+  this.cleanupKeysArray_();
+
+  var i = 0;
+  var version = this.version_;
+  var selfObj = this;
+
+  var newIter = new goog.iter.Iterator;
+  newIter.next = function() {
+    if (version != selfObj.version_) {
+      throw Error('The map has changed since the iterator was created');
+    }
+    if (i >= selfObj.keys_.length) {
+      throw goog.iter.StopIteration;
+    }
+    var key = selfObj.keys_[i++];
+    return opt_keys ? key : selfObj.map_[key];
+  };
+  return newIter;
+};
+
+
+/**
+ * Safe way to test for hasOwnProperty.  It even allows testing for
+ * 'hasOwnProperty'.
+ * @param {Object} obj The object to test for presence of the given key.
+ * @param {*} key The key to check for.
+ * @return {boolean} Whether the object has the key.
+ * @private
+ */
+goog.structs.Map.hasKey_ = function(obj, key) {
+  return Object.prototype.hasOwnProperty.call(obj, key);
+};
+
+// Copyright 2008 The Closure Library Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS-IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+/**
+ * @fileoverview Simple utilities for dealing with URI strings.
+ *
+ * This is intended to be a lightweight alternative to constructing goog.Uri
+ * objects.  Whereas goog.Uri adds several kilobytes to the binary regardless
+ * of how much of its functionality you use, this is designed to be a set of
+ * mostly-independent utilities so that the compiler includes only what is
+ * necessary for the task.  Estimated savings of porting is 5k pre-gzip and
+ * 1.5k post-gzip.  To ensure the savings remain, future developers should
+ * avoid adding new functionality to existing functions, but instead create
+ * new ones and factor out shared code.
+ *
+ * Many of these utilities have limited functionality, tailored to common
+ * cases.  The query parameter utilities assume that the parameter keys are
+ * already encoded, since most keys are compile-time alphanumeric strings.  The
+ * query parameter mutation utilities also do not tolerate fragment identifiers.
+ *
+ * By design, these functions can be slower than goog.Uri equivalents.
+ * Repeated calls to some of functions may be quadratic in behavior for IE,
+ * although the effect is somewhat limited given the 2kb limit.
+ *
+ * One advantage of the limited functionality here is that this approach is
+ * less sensitive to differences in URI encodings than goog.Uri, since these
+ * functions operate on strings directly, rather than decoding them and
+ * then re-encoding.
+ *
+ * Uses features of RFC 3986 for parsing/formatting URIs:
+ *   http://www.ietf.org/rfc/rfc3986.txt
+ *
+ * @author gboyer@google.com (Garrett Boyer) - The "lightened" design.
+ */
+
+goog.provide('goog.uri.utils');
+goog.provide('goog.uri.utils.ComponentIndex');
+goog.provide('goog.uri.utils.QueryArray');
+goog.provide('goog.uri.utils.QueryValue');
+goog.provide('goog.uri.utils.StandardQueryParam');
+
+goog.require('goog.asserts');
+goog.require('goog.string');
+
+
+/**
+ * Character codes inlined to avoid object allocations due to charCode.
+ * @enum {number}
+ * @private
+ */
+goog.uri.utils.CharCode_ = {
+  AMPERSAND: 38,
+  EQUAL: 61,
+  HASH: 35,
+  QUESTION: 63
+};
+
+
+/**
+ * Builds a URI string from already-encoded parts.
+ *
+ * No encoding is performed.  Any component may be omitted as either null or
+ * undefined.
+ *
+ * @param {?string=} opt_scheme The scheme such as 'http'.
+ * @param {?string=} opt_userInfo The user name before the '@'.
+ * @param {?string=} opt_domain The domain such as 'www.google.com', already
+ *     URI-encoded.
+ * @param {(string|number|null)=} opt_port The port number.
+ * @param {?string=} opt_path The path, already URI-encoded.  If it is not
+ *     empty, it must begin with a slash.
+ * @param {?string=} opt_queryData The URI-encoded query data.
+ * @param {?string=} opt_fragment The URI-encoded fragment identifier.
+ * @return {string} The fully combined URI.
+ */
+goog.uri.utils.buildFromEncodedParts = function(
+    opt_scheme, opt_userInfo, opt_domain, opt_port, opt_path, opt_queryData,
+    opt_fragment) {
+  var out = '';
+
+  if (opt_scheme) {
+    out += opt_scheme + ':';
+  }
+
+  if (opt_domain) {
+    out += '//';
+
+    if (opt_userInfo) {
+      out += opt_userInfo + '@';
+    }
+
+    out += opt_domain;
+
+    if (opt_port) {
+      out += ':' + opt_port;
+    }
+  }
+
+  if (opt_path) {
+    out += opt_path;
+  }
+
+  if (opt_queryData) {
+    out += '?' + opt_queryData;
+  }
+
+  if (opt_fragment) {
+    out += '#' + opt_fragment;
+  }
+
+  return out;
+};
+
+
+/**
+ * A regular expression for breaking a URI into its component parts.
+ *
+ * {@link http://www.ietf.org/rfc/rfc3986.txt} says in Appendix B
+ * As the "first-match-wins" algorithm is identical to the "greedy"
+ * disambiguation method used by POSIX regular expressions, it is natural and
+ * commonplace to use a regular expression for parsing the potential five
+ * components of a URI reference.
+ *
+ * The following line is the regular expression for breaking-down a
+ * well-formed URI reference into its components.
+ *
+ * <pre>
+ * ^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?
+ *  12            3  4          5       6  7        8 9
+ * </pre>
+ *
+ * The numbers in the second line above are only to assist readability; they
+ * indicate the reference points for each subexpression (i.e., each paired
+ * parenthesis). We refer to the value matched for subexpression <n> as $<n>.
+ * For example, matching the above expression to
+ * <pre>
+ *     http://www.ics.uci.edu/pub/ietf/uri/#Related
+ * </pre>
+ * results in the following subexpression matches:
+ * <pre>
+ *    $1 = http:
+ *    $2 = http
+ *    $3 = //www.ics.uci.edu
+ *    $4 = www.ics.uci.edu
+ *    $5 = /pub/ietf/uri/
+ *    $6 = <undefined>
+ *    $7 = <undefined>
+ *    $8 = #Related
+ *    $9 = Related
+ * </pre>
+ * where <undefined> indicates that the component is not present, as is the
+ * case for the query component in the above example. Therefore, we can
+ * determine the value of the five components as
+ * <pre>
+ *    scheme    = $2
+ *    authority = $4
+ *    path      = $5
+ *    query     = $7
+ *    fragment  = $9
+ * </pre>
+ *
+ * The regular expression has been modified slightly to expose the
+ * userInfo, domain, and port separately from the authority.
+ * The modified version yields
+ * <pre>
+ *    $1 = http              scheme
+ *    $2 = <undefined>       userInfo -\
+ *    $3 = www.ics.uci.edu   domain     | authority
+ *    $4 = <undefined>       port     -/
+ *    $5 = /pub/ietf/uri/    path
+ *    $6 = <undefined>       query without ?
+ *    $7 = Related           fragment without #
+ * </pre>
+ * @type {!RegExp}
+ * @private
+ */
+goog.uri.utils.splitRe_ = new RegExp(
+    '^' +
+    '(?:' +
+    '([^:/?#.]+)' +  // scheme - ignore special characters
+                     // used by other URL parts such as :,
+                     // ?, /, #, and .
+    ':)?' +
+    '(?://' +
+    '(?:([^/?#]*)@)?' +  // userInfo
+    '([^/#?]*?)' +       // domain
+    '(?::([0-9]+))?' +   // port
+    '(?=[/#?]|$)' +      // authority-terminating character
+    ')?' +
+    '([^?#]+)?' +        // path
+    '(?:\\?([^#]*))?' +  // query
+    '(?:#(.*))?' +       // fragment
+    '$');
+
+
+/**
+ * The index of each URI component in the return value of goog.uri.utils.split.
+ * @enum {number}
+ */
+goog.uri.utils.ComponentIndex = {
+  SCHEME: 1,
+  USER_INFO: 2,
+  DOMAIN: 3,
+  PORT: 4,
+  PATH: 5,
+  QUERY_DATA: 6,
+  FRAGMENT: 7
+};
+
+
+/**
+ * Splits a URI into its component parts.
+ *
+ * Each component can be accessed via the component indices; for example:
+ * <pre>
+ * goog.uri.utils.split(someStr)[goog.uri.utils.CompontentIndex.QUERY_DATA];
+ * </pre>
+ *
+ * @param {string} uri The URI string to examine.
+ * @return {!Array<string|undefined>} Each component still URI-encoded.
+ *     Each component that is present will contain the encoded value, whereas
+ *     components that are not present will be undefined or empty, depending
+ *     on the browser's regular expression implementation.  Never null, since
+ *     arbitrary strings may still look like path names.
+ */
+goog.uri.utils.split = function(uri) {
+  // See @return comment -- never null.
+  return /** @type {!Array<string|undefined>} */ (
+      uri.match(goog.uri.utils.splitRe_));
+};
+
+
+/**
+ * @param {?string} uri A possibly null string.
+ * @param {boolean=} opt_preserveReserved If true, percent-encoding of RFC-3986
+ *     reserved characters will not be removed.
+ * @return {?string} The string URI-decoded, or null if uri is null.
+ * @private
+ */
+goog.uri.utils.decodeIfPossible_ = function(uri, opt_preserveReserved) {
+  if (!uri) {
+    return uri;
+  }
+
+  return opt_preserveReserved ? decodeURI(uri) : decodeURIComponent(uri);
+};
+
+
+/**
+ * Gets a URI component by index.
+ *
+ * It is preferred to use the getPathEncoded() variety of functions ahead,
+ * since they are more readable.
+ *
+ * @param {goog.uri.utils.ComponentIndex} componentIndex The component index.
+ * @param {string} uri The URI to examine.
+ * @return {?string} The still-encoded component, or null if the component
+ *     is not present.
+ * @private
+ */
+goog.uri.utils.getComponentByIndex_ = function(componentIndex, uri) {
+  // Convert undefined, null, and empty string into null.
+  return goog.uri.utils.split(uri)[componentIndex] || null;
+};
+
+
+/**
+ * @param {string} uri The URI to examine.
+ * @return {?string} The protocol or scheme, or null if none.  Does not
+ *     include trailing colons or slashes.
+ */
+goog.uri.utils.getScheme = function(uri) {
+  return goog.uri.utils.getComponentByIndex_(
+      goog.uri.utils.ComponentIndex.SCHEME, uri);
+};
+
+
+/**
+ * Gets the effective scheme for the URL.  If the URL is relative then the
+ * scheme is derived from the page's location.
+ * @param {string} uri The URI to examine.
+ * @return {string} The protocol or scheme, always lower case.
+ */
+goog.uri.utils.getEffectiveScheme = function(uri) {
+  var scheme = goog.uri.utils.getScheme(uri);
+  if (!scheme && goog.global.self && goog.global.self.location) {
+    var protocol = goog.global.self.location.protocol;
+    scheme = protocol.substr(0, protocol.length - 1);
+  }
+  // NOTE: When called from a web worker in Firefox 3.5, location maybe null.
+  // All other browsers with web workers support self.location from the worker.
+  return scheme ? scheme.toLowerCase() : '';
+};
+
+
+/**
+ * @param {string} uri The URI to examine.
+ * @return {?string} The user name still encoded, or null if none.
+ */
+goog.uri.utils.getUserInfoEncoded = function(uri) {
+  return goog.uri.utils.getComponentByIndex_(
+      goog.uri.utils.ComponentIndex.USER_INFO, uri);
+};
+
+
+/**
+ * @param {string} uri The URI to examine.
+ * @return {?string} The decoded user info, or null if none.
+ */
+goog.uri.utils.getUserInfo = function(uri) {
+  return goog.uri.utils.decodeIfPossible_(
+      goog.uri.utils.getUserInfoEncoded(uri));
+};
+
+
+/**
+ * @param {string} uri The URI to examine.
+ * @return {?string} The domain name still encoded, or null if none.
+ */
+goog.uri.utils.getDomainEncoded = function(uri) {
+  return goog.uri.utils.getComponentByIndex_(
+      goog.uri.utils.ComponentIndex.DOMAIN, uri);
+};
+
+
+/**
+ * @param {string} uri The URI to examine.
+ * @return {?string} The decoded domain, or null if none.
+ */
+goog.uri.utils.getDomain = function(uri) {
+  return goog.uri.utils.decodeIfPossible_(
+      goog.uri.utils.getDomainEncoded(uri), true /* opt_preserveReserved */);
+};
+
+
+/**
+ * @param {string} uri The URI to examine.
+ * @return {?number} The port number, or null if none.
+ */
+goog.uri.utils.getPort = function(uri) {
+  // Coerce to a number.  If the result of getComponentByIndex_ is null or
+  // non-numeric, the number coersion yields NaN.  This will then return
+  // null for all non-numeric cases (though also zero, which isn't a relevant
+  // port number).
+  return Number(
+             goog.uri.utils.getComponentByIndex_(
+                 goog.uri.utils.ComponentIndex.PORT, uri)) ||
+      null;
+};
+
+
+/**
+ * @param {string} uri The URI to examine.
+ * @return {?string} The path still encoded, or null if none. Includes the
+ *     leading slash, if any.
+ */
+goog.uri.utils.getPathEncoded = function(uri) {
+  return goog.uri.utils.getComponentByIndex_(
+      goog.uri.utils.ComponentIndex.PATH, uri);
+};
+
+
+/**
+ * @param {string} uri The URI to examine.
+ * @return {?string} The decoded path, or null if none.  Includes the leading
+ *     slash, if any.
+ */
+goog.uri.utils.getPath = function(uri) {
+  return goog.uri.utils.decodeIfPossible_(
+      goog.uri.utils.getPathEncoded(uri), true /* opt_preserveReserved */);
+};
+
+
+/**
+ * @param {string} uri The URI to examine.
+ * @return {?string} The query data still encoded, or null if none.  Does not
+ *     include the question mark itself.
+ */
+goog.uri.utils.getQueryData = function(uri) {
+  return goog.uri.utils.getComponentByIndex_(
+      goog.uri.utils.ComponentIndex.QUERY_DATA, uri);
+};
+
+
+/**
+ * @param {string} uri The URI to examine.
+ * @return {?string} The fragment identifier, or null if none.  Does not
+ *     include the hash mark itself.
+ */
+goog.uri.utils.getFragmentEncoded = function(uri) {
+  // The hash mark may not appear in any other part of the URL.
+  var hashIndex = uri.indexOf('#');
+  return hashIndex < 0 ? null : uri.substr(hashIndex + 1);
+};
+
+
+/**
+ * @param {string} uri The URI to examine.
+ * @param {?string} fragment The encoded fragment identifier, or null if none.
+ *     Does not include the hash mark itself.
+ * @return {string} The URI with the fragment set.
+ */
+goog.uri.utils.setFragmentEncoded = function(uri, fragment) {
+  return goog.uri.utils.removeFragment(uri) + (fragment ? '#' + fragment : '');
+};
+
+
+/**
+ * @param {string} uri The URI to examine.
+ * @return {?string} The decoded fragment identifier, or null if none.  Does
+ *     not include the hash mark.
+ */
+goog.uri.utils.getFragment = function(uri) {
+  return goog.uri.utils.decodeIfPossible_(
+      goog.uri.utils.getFragmentEncoded(uri));
+};
+
+
+/**
+ * Extracts everything up to the port of the URI.
+ * @param {string} uri The URI string.
+ * @return {string} Everything up to and including the port.
+ */
+goog.uri.utils.getHost = function(uri) {
+  var pieces = goog.uri.utils.split(uri);
+  return goog.uri.utils.buildFromEncodedParts(
+      pieces[goog.uri.utils.ComponentIndex.SCHEME],
+      pieces[goog.uri.utils.ComponentIndex.USER_INFO],
+      pieces[goog.uri.utils.ComponentIndex.DOMAIN],
+      pieces[goog.uri.utils.ComponentIndex.PORT]);
+};
+
+
+/**
+ * Extracts the path of the URL and everything after.
+ * @param {string} uri The URI string.
+ * @return {string} The URI, starting at the path and including the query
+ *     parameters and fragment identifier.
+ */
+goog.uri.utils.getPathAndAfter = function(uri) {
+  var pieces = goog.uri.utils.split(uri);
+  return goog.uri.utils.buildFromEncodedParts(
+      null, null, null, null, pieces[goog.uri.utils.ComponentIndex.PATH],
+      pieces[goog.uri.utils.ComponentIndex.QUERY_DATA],
+      pieces[goog.uri.utils.ComponentIndex.FRAGMENT]);
+};
+
+
+/**
+ * Gets the URI with the fragment identifier removed.
+ * @param {string} uri The URI to examine.
+ * @return {string} Everything preceding the hash mark.
+ */
+goog.uri.utils.removeFragment = function(uri) {
+  // The hash mark may not appear in any other part of the URL.
+  var hashIndex = uri.indexOf('#');
+  return hashIndex < 0 ? uri : uri.substr(0, hashIndex);
+};
+
+
+/**
+ * Ensures that two URI's have the exact same domain, scheme, and port.
+ *
+ * Unlike the version in goog.Uri, this checks protocol, and therefore is
+ * suitable for checking against the browser's same-origin policy.
+ *
+ * @param {string} uri1 The first URI.
+ * @param {string} uri2 The second URI.
+ * @return {boolean} Whether they have the same scheme, domain and port.
+ */
+goog.uri.utils.haveSameDomain = function(uri1, uri2) {
+  var pieces1 = goog.uri.utils.split(uri1);
+  var pieces2 = goog.uri.utils.split(uri2);
+  return pieces1[goog.uri.utils.ComponentIndex.DOMAIN] ==
+      pieces2[goog.uri.utils.ComponentIndex.DOMAIN] &&
+      pieces1[goog.uri.utils.ComponentIndex.SCHEME] ==
+      pieces2[goog.uri.utils.ComponentIndex.SCHEME] &&
+      pieces1[goog.uri.utils.ComponentIndex.PORT] ==
+      pieces2[goog.uri.utils.ComponentIndex.PORT];
+};
+
+
+/**
+ * Asserts that there are no fragment or query identifiers, only in uncompiled
+ * mode.
+ * @param {string} uri The URI to examine.
+ * @private
+ */
+goog.uri.utils.assertNoFragmentsOrQueries_ = function(uri) {
+  // NOTE: would use goog.asserts here, but jscompiler doesn't know that
+  // indexOf has no side effects.
+  if (goog.DEBUG && (uri.indexOf('#') >= 0 || uri.indexOf('?') >= 0)) {
+    throw Error(
+        'goog.uri.utils: Fragment or query identifiers are not ' +
+        'supported: [' + uri + ']');
+  }
+};
+
+
+/**
+ * Supported query parameter values by the parameter serializing utilities.
+ *
+ * If a value is null or undefined, the key-value pair is skipped, as an easy
+ * way to omit parameters conditionally.  Non-array parameters are converted
+ * to a string and URI encoded.  Array values are expanded into multiple
+ * &key=value pairs, with each element stringized and URI-encoded.
+ *
+ * @typedef {*}
+ */
+goog.uri.utils.QueryValue;
+
+
+/**
+ * An array representing a set of query parameters with alternating keys
+ * and values.
+ *
+ * Keys are assumed to be URI encoded already and live at even indices.  See
+ * goog.uri.utils.QueryValue for details on how parameter values are encoded.
+ *
+ * Example:
+ * <pre>
+ * var data = [
+ *   // Simple param: ?name=BobBarker
+ *   'name', 'BobBarker',
+ *   // Conditional param -- may be omitted entirely.
+ *   'specialDietaryNeeds', hasDietaryNeeds() ? getDietaryNeeds() : null,
+ *   // Multi-valued param: &house=LosAngeles&house=NewYork&house=null
+ *   'house', ['LosAngeles', 'NewYork', null]
+ * ];
+ * </pre>
+ *
+ * @typedef {!Array<string|goog.uri.utils.QueryValue>}
+ */
+goog.uri.utils.QueryArray;
+
+
+/**
+ * Parses encoded query parameters and calls callback function for every
+ * parameter found in the string.
+ *
+ * Missing value of parameter (e.g. “…&key&…”) is treated as if the value was an
+ * empty string.  Keys may be empty strings (e.g. “…&=value&…”) which also means
+ * that “…&=&…” and “…&&…” will result in an empty key and value.
+ *
+ * @param {string} encodedQuery Encoded query string excluding question mark at
+ *     the beginning.
+ * @param {function(string, string)} callback Function called for every
+ *     parameter found in query string.  The first argument (name) will not be
+ *     urldecoded (so the function is consistent with buildQueryData), but the
+ *     second will.  If the parameter has no value (i.e. “=” was not present)
+ *     the second argument (value) will be an empty string.
+ */
+goog.uri.utils.parseQueryData = function(encodedQuery, callback) {
+  if (!encodedQuery) {
+    return;
+  }
+  var pairs = encodedQuery.split('&');
+  for (var i = 0; i < pairs.length; i++) {
+    var indexOfEquals = pairs[i].indexOf('=');
+    var name = null;
+    var value = null;
+    if (indexOfEquals >= 0) {
+      name = pairs[i].substring(0, indexOfEquals);
+      value = pairs[i].substring(indexOfEquals + 1);
+    } else {
+      name = pairs[i];
+    }
+    callback(name, value ? goog.string.urlDecode(value) : '');
+  }
+};
+
+
+/**
+ * Appends a URI and query data in a string buffer with special preconditions.
+ *
+ * Internal implementation utility, performing very few object allocations.
+ *
+ * @param {!Array<string|undefined>} buffer A string buffer.  The first element
+ *     must be the base URI, and may have a fragment identifier.  If the array
+ *     contains more than one element, the second element must be an ampersand,
+ *     and may be overwritten, depending on the base URI.  Undefined elements
+ *     are treated as empty-string.
+ * @return {string} The concatenated URI and query data.
+ * @private
+ */
+goog.uri.utils.appendQueryData_ = function(buffer) {
+  if (buffer[1]) {
+    // At least one query parameter was added.  We need to check the
+    // punctuation mark, which is currently an ampersand, and also make sure
+    // there aren't any interfering fragment identifiers.
+    var baseUri = /** @type {string} */ (buffer[0]);
+    var hashIndex = baseUri.indexOf('#');
+    if (hashIndex >= 0) {
+      // Move the fragment off the base part of the URI into the end.
+      buffer.push(baseUri.substr(hashIndex));
+      buffer[0] = baseUri = baseUri.substr(0, hashIndex);
+    }
+    var questionIndex = baseUri.indexOf('?');
+    if (questionIndex < 0) {
+      // No question mark, so we need a question mark instead of an ampersand.
+      buffer[1] = '?';
+    } else if (questionIndex == baseUri.length - 1) {
+      // Question mark is the very last character of the existing URI, so don't
+      // append an additional delimiter.
+      buffer[1] = undefined;
+    }
+  }
+
+  return buffer.join('');
+};
+
+
+/**
+ * Appends key=value pairs to an array, supporting multi-valued objects.
+ * @param {string} key The key prefix.
+ * @param {goog.uri.utils.QueryValue} value The value to serialize.
+ * @param {!Array<string>} pairs The array to which the 'key=value' strings
+ *     should be appended.
+ * @private
+ */
+goog.uri.utils.appendKeyValuePairs_ = function(key, value, pairs) {
+  if (goog.isArray(value)) {
+    // Convince the compiler it's an array.
+    goog.asserts.assertArray(value);
+    for (var j = 0; j < value.length; j++) {
+      // Convert to string explicitly, to short circuit the null and array
+      // logic in this function -- this ensures that null and undefined get
+      // written as literal 'null' and 'undefined', and arrays don't get
+      // expanded out but instead encoded in the default way.
+      goog.uri.utils.appendKeyValuePairs_(key, String(value[j]), pairs);
+    }
+  } else if (value != null) {
+    // Skip a top-level null or undefined entirely.
+    pairs.push(
+        '&', key,
+        // Check for empty string. Zero gets encoded into the url as literal
+        // strings.  For empty string, skip the equal sign, to be consistent
+        // with UriBuilder.java.
+        value === '' ? '' : '=', goog.string.urlEncode(value));
+  }
+};
+
+
+/**
+ * Builds a buffer of query data from a sequence of alternating keys and values.
+ *
+ * @param {!Array<string|undefined>} buffer A string buffer to append to.  The
+ *     first element appended will be an '&', and may be replaced by the caller.
+ * @param {!goog.uri.utils.QueryArray|!Arguments} keysAndValues An array with
+ *     alternating keys and values -- see the typedef.
+ * @param {number=} opt_startIndex A start offset into the arary, defaults to 0.
+ * @return {!Array<string|undefined>} The buffer argument.
+ * @private
+ */
+goog.uri.utils.buildQueryDataBuffer_ = function(
+    buffer, keysAndValues, opt_startIndex) {
+  goog.asserts.assert(
+      Math.max(keysAndValues.length - (opt_startIndex || 0), 0) % 2 == 0,
+      'goog.uri.utils: Key/value lists must be even in length.');
+
+  for (var i = opt_startIndex || 0; i < keysAndValues.length; i += 2) {
+    goog.uri.utils.appendKeyValuePairs_(
+        keysAndValues[i], keysAndValues[i + 1], buffer);
+  }
+
+  return buffer;
+};
+
+
+/**
+ * Builds a query data string from a sequence of alternating keys and values.
+ * Currently generates "&key&" for empty args.
+ *
+ * @param {goog.uri.utils.QueryArray} keysAndValues Alternating keys and
+ *     values.  See the typedef.
+ * @param {number=} opt_startIndex A start offset into the arary, defaults to 0.
+ * @return {string} The encoded query string, in the form 'a=1&b=2'.
+ */
+goog.uri.utils.buildQueryData = function(keysAndValues, opt_startIndex) {
+  var buffer =
+      goog.uri.utils.buildQueryDataBuffer_([], keysAndValues, opt_startIndex);
+  buffer[0] = '';  // Remove the leading ampersand.
+  return buffer.join('');
+};
+
+
+/**
+ * Builds a buffer of query data from a map.
+ *
+ * @param {!Array<string|undefined>} buffer A string buffer to append to.  The
+ *     first element appended will be an '&', and may be replaced by the caller.
+ * @param {!Object<string, goog.uri.utils.QueryValue>} map An object where keys
+ *     are URI-encoded parameter keys, and the values conform to the contract
+ *     specified in the goog.uri.utils.QueryValue typedef.
+ * @return {!Array<string|undefined>} The buffer argument.
+ * @private
+ */
+goog.uri.utils.buildQueryDataBufferFromMap_ = function(buffer, map) {
+  for (var key in map) {
+    goog.uri.utils.appendKeyValuePairs_(key, map[key], buffer);
+  }
+
+  return buffer;
+};
+
+
+/**
+ * Builds a query data string from a map.
+ * Currently generates "&key&" for empty args.
+ *
+ * @param {!Object<string, goog.uri.utils.QueryValue>} map An object where keys
+ *     are URI-encoded parameter keys, and the values are arbitrary types
+ *     or arrays. Keys with a null value are dropped.
+ * @return {string} The encoded query string, in the form 'a=1&b=2'.
+ */
+goog.uri.utils.buildQueryDataFromMap = function(map) {
+  var buffer = goog.uri.utils.buildQueryDataBufferFromMap_([], map);
+  buffer[0] = '';
+  return buffer.join('');
+};
+
+
+/**
+ * Appends URI parameters to an existing URI.
+ *
+ * The variable arguments may contain alternating keys and values.  Keys are
+ * assumed to be already URI encoded.  The values should not be URI-encoded,
+ * and will instead be encoded by this function.
+ * <pre>
+ * appendParams('http://www.foo.com?existing=true',
+ *     'key1', 'value1',
+ *     'key2', 'value?willBeEncoded',
+ *     'key3', ['valueA', 'valueB', 'valueC'],
+ *     'key4', null);
+ * result: 'http://www.foo.com?existing=true&' +
+ *     'key1=value1&' +
+ *     'key2=value%3FwillBeEncoded&' +
+ *     'key3=valueA&key3=valueB&key3=valueC'
+ * </pre>
+ *
+ * A single call to this function will not exhibit quadratic behavior in IE,
+ * whereas multiple repeated calls may, although the effect is limited by
+ * fact that URL's generally can't exceed 2kb.
+ *
+ * @param {string} uri The original URI, which may already have query data.
+ * @param {...(goog.uri.utils.QueryArray|string|goog.uri.utils.QueryValue)}
+ * var_args
+ *     An array or argument list conforming to goog.uri.utils.QueryArray.
+ * @return {string} The URI with all query parameters added.
+ */
+goog.uri.utils.appendParams = function(uri, var_args) {
+  return goog.uri.utils.appendQueryData_(
+      arguments.length == 2 ?
+          goog.uri.utils.buildQueryDataBuffer_([uri], arguments[1], 0) :
+          goog.uri.utils.buildQueryDataBuffer_([uri], arguments, 1));
+};
+
+
+/**
+ * Appends query parameters from a map.
+ *
+ * @param {string} uri The original URI, which may already have query data.
+ * @param {!Object<goog.uri.utils.QueryValue>} map An object where keys are
+ *     URI-encoded parameter keys, and the values are arbitrary types or arrays.
+ *     Keys with a null value are dropped.
+ * @return {string} The new parameters.
+ */
+goog.uri.utils.appendParamsFromMap = function(uri, map) {
+  return goog.uri.utils.appendQueryData_(
+      goog.uri.utils.buildQueryDataBufferFromMap_([uri], map));
+};
+
+
+/**
+ * Appends a single URI parameter.
+ *
+ * Repeated calls to this can exhibit quadratic behavior in IE6 due to the
+ * way string append works, though it should be limited given the 2kb limit.
+ *
+ * @param {string} uri The original URI, which may already have query data.
+ * @param {string} key The key, which must already be URI encoded.
+ * @param {*=} opt_value The value, which will be stringized and encoded
+ *     (assumed not already to be encoded).  If omitted, undefined, or null, the
+ *     key will be added as a valueless parameter.
+ * @return {string} The URI with the query parameter added.
+ */
+goog.uri.utils.appendParam = function(uri, key, opt_value) {
+  var paramArr = [uri, '&', key];
+  if (goog.isDefAndNotNull(opt_value)) {
+    paramArr.push('=', goog.string.urlEncode(opt_value));
+  }
+  return goog.uri.utils.appendQueryData_(paramArr);
+};
+
+
+/**
+ * Finds the next instance of a query parameter with the specified name.
+ *
+ * Does not instantiate any objects.
+ *
+ * @param {string} uri The URI to search.  May contain a fragment identifier
+ *     if opt_hashIndex is specified.
+ * @param {number} startIndex The index to begin searching for the key at.  A
+ *     match may be found even if this is one character after the ampersand.
+ * @param {string} keyEncoded The URI-encoded key.
+ * @param {number} hashOrEndIndex Index to stop looking at.  If a hash
+ *     mark is present, it should be its index, otherwise it should be the
+ *     length of the string.
+ * @return {number} The position of the first character in the key's name,
+ *     immediately after either a question mark or a dot.
+ * @private
+ */
+goog.uri.utils.findParam_ = function(
+    uri, startIndex, keyEncoded, hashOrEndIndex) {
+  var index = startIndex;
+  var keyLength = keyEncoded.length;
+
+  // Search for the key itself and post-filter for surronuding punctuation,
+  // rather than expensively building a regexp.
+  while ((index = uri.indexOf(keyEncoded, index)) >= 0 &&
+         index < hashOrEndIndex) {
+    var precedingChar = uri.charCodeAt(index - 1);
+    // Ensure that the preceding character is '&' or '?'.
+    if (precedingChar == goog.uri.utils.CharCode_.AMPERSAND ||
+        precedingChar == goog.uri.utils.CharCode_.QUESTION) {
+      // Ensure the following character is '&', '=', '#', or NaN
+      // (end of string).
+      var followingChar = uri.charCodeAt(index + keyLength);
+      if (!followingChar || followingChar == goog.uri.utils.CharCode_.EQUAL ||
+          followingChar == goog.uri.utils.CharCode_.AMPERSAND ||
+          followingChar == goog.uri.utils.CharCode_.HASH) {
+        return index;
+      }
+    }
+    index += keyLength + 1;
+  }
+
+  return -1;
+};
+
+
+/**
+ * Regular expression for finding a hash mark or end of string.
+ * @type {RegExp}
+ * @private
+ */
+goog.uri.utils.hashOrEndRe_ = /#|$/;
+
+
+/**
+ * Determines if the URI contains a specific key.
+ *
+ * Performs no object instantiations.
+ *
+ * @param {string} uri The URI to process.  May contain a fragment
+ *     identifier.
+ * @param {string} keyEncoded The URI-encoded key.  Case-sensitive.
+ * @return {boolean} Whether the key is present.
+ */
+goog.uri.utils.hasParam = function(uri, keyEncoded) {
+  return goog.uri.utils.findParam_(
+             uri, 0, keyEncoded, uri.search(goog.uri.utils.hashOrEndRe_)) >= 0;
+};
+
+
+/**
+ * Gets the first value of a query parameter.
+ * @param {string} uri The URI to process.  May contain a fragment.
+ * @param {string} keyEncoded The URI-encoded key.  Case-sensitive.
+ * @return {?string} The first value of the parameter (URI-decoded), or null
+ *     if the parameter is not found.
+ */
+goog.uri.utils.getParamValue = function(uri, keyEncoded) {
+  var hashOrEndIndex = uri.search(goog.uri.utils.hashOrEndRe_);
+  var foundIndex =
+      goog.uri.utils.findParam_(uri, 0, keyEncoded, hashOrEndIndex);
+
+  if (foundIndex < 0) {
+    return null;
+  } else {
+    var endPosition = uri.indexOf('&', foundIndex);
+    if (endPosition < 0 || endPosition > hashOrEndIndex) {
+      endPosition = hashOrEndIndex;
+    }
+    // Progress forth to the end of the "key=" or "key&" substring.
+    foundIndex += keyEncoded.length + 1;
+    // Use substr, because it (unlike substring) will return empty string
+    // if foundIndex > endPosition.
+    return goog.string.urlDecode(
+        uri.substr(foundIndex, endPosition - foundIndex));
+  }
+};
+
+
+/**
+ * Gets all values of a query parameter.
+ * @param {string} uri The URI to process.  May contain a fragment.
+ * @param {string} keyEncoded The URI-encoded key.  Case-sensitive.
+ * @return {!Array<string>} All URI-decoded values with the given key.
+ *     If the key is not found, this will have length 0, but never be null.
+ */
+goog.uri.utils.getParamValues = function(uri, keyEncoded) {
+  var hashOrEndIndex = uri.search(goog.uri.utils.hashOrEndRe_);
+  var position = 0;
+  var foundIndex;
+  var result = [];
+
+  while ((foundIndex = goog.uri.utils.findParam_(
+              uri, position, keyEncoded, hashOrEndIndex)) >= 0) {
+    // Find where this parameter ends, either the '&' or the end of the
+    // query parameters.
+    position = uri.indexOf('&', foundIndex);
+    if (position < 0 || position > hashOrEndIndex) {
+      position = hashOrEndIndex;
+    }
+
+    // Progress forth to the end of the "key=" or "key&" substring.
+    foundIndex += keyEncoded.length + 1;
+    // Use substr, because it (unlike substring) will return empty string
+    // if foundIndex > position.
+    result.push(
+        goog.string.urlDecode(uri.substr(foundIndex, position - foundIndex)));
+  }
+
+  return result;
+};
+
+
+/**
+ * Regexp to find trailing question marks and ampersands.
+ * @type {RegExp}
+ * @private
+ */
+goog.uri.utils.trailingQueryPunctuationRe_ = /[?&]($|#)/;
+
+
+/**
+ * Removes all instances of a query parameter.
+ * @param {string} uri The URI to process.  Must not contain a fragment.
+ * @param {string} keyEncoded The URI-encoded key.
+ * @return {string} The URI with all instances of the parameter removed.
+ */
+goog.uri.utils.removeParam = function(uri, keyEncoded) {
+  var hashOrEndIndex = uri.search(goog.uri.utils.hashOrEndRe_);
+  var position = 0;
+  var foundIndex;
+  var buffer = [];
+
+  // Look for a query parameter.
+  while ((foundIndex = goog.uri.utils.findParam_(
+              uri, position, keyEncoded, hashOrEndIndex)) >= 0) {
+    // Get the portion of the query string up to, but not including, the ?
+    // or & starting the parameter.
+    buffer.push(uri.substring(position, foundIndex));
+    // Progress to immediately after the '&'.  If not found, go to the end.
+    // Avoid including the hash mark.
+    position = Math.min(
+        (uri.indexOf('&', foundIndex) + 1) || hashOrEndIndex, hashOrEndIndex);
+  }
+
+  // Append everything that is remaining.
+  buffer.push(uri.substr(position));
+
+  // Join the buffer, and remove trailing punctuation that remains.
+  return buffer.join('').replace(
+      goog.uri.utils.trailingQueryPunctuationRe_, '$1');
+};
+
+
+/**
+ * Replaces all existing definitions of a parameter with a single definition.
+ *
+ * Repeated calls to this can exhibit quadratic behavior due to the need to
+ * find existing instances and reconstruct the string, though it should be
+ * limited given the 2kb limit.  Consider using appendParams to append multiple
+ * parameters in bulk.
+ *
+ * @param {string} uri The original URI, which may already have query data.
+ * @param {string} keyEncoded The key, which must already be URI encoded.
+ * @param {*} value The value, which will be stringized and encoded (assumed
+ *     not already to be encoded).
+ * @return {string} The URI with the query parameter added.
+ */
+goog.uri.utils.setParam = function(uri, keyEncoded, value) {
+  return goog.uri.utils.appendParam(
+      goog.uri.utils.removeParam(uri, keyEncoded), keyEncoded, value);
+};
+
+
+/**
+ * Generates a URI path using a given URI and a path with checks to
+ * prevent consecutive "//". The baseUri passed in must not contain
+ * query or fragment identifiers. The path to append may not contain query or
+ * fragment identifiers.
+ *
+ * @param {string} baseUri URI to use as the base.
+ * @param {string} path Path to append.
+ * @return {string} Updated URI.
+ */
+goog.uri.utils.appendPath = function(baseUri, path) {
+  goog.uri.utils.assertNoFragmentsOrQueries_(baseUri);
+
+  // Remove any trailing '/'
+  if (goog.string.endsWith(baseUri, '/')) {
+    baseUri = baseUri.substr(0, baseUri.length - 1);
+  }
+  // Remove any leading '/'
+  if (goog.string.startsWith(path, '/')) {
+    path = path.substr(1);
+  }
+  return goog.string.buildString(baseUri, '/', path);
+};
+
+
+/**
+ * Replaces the path.
+ * @param {string} uri URI to use as the base.
+ * @param {string} path New path.
+ * @return {string} Updated URI.
+ */
+goog.uri.utils.setPath = function(uri, path) {
+  // Add any missing '/'.
+  if (!goog.string.startsWith(path, '/')) {
+    path = '/' + path;
+  }
+  var parts = goog.uri.utils.split(uri);
+  return goog.uri.utils.buildFromEncodedParts(
+      parts[goog.uri.utils.ComponentIndex.SCHEME],
+      parts[goog.uri.utils.ComponentIndex.USER_INFO],
+      parts[goog.uri.utils.ComponentIndex.DOMAIN],
+      parts[goog.uri.utils.ComponentIndex.PORT], path,
+      parts[goog.uri.utils.ComponentIndex.QUERY_DATA],
+      parts[goog.uri.utils.ComponentIndex.FRAGMENT]);
+};
+
+
+/**
+ * Standard supported query parameters.
+ * @enum {string}
+ */
+goog.uri.utils.StandardQueryParam = {
+
+  /** Unused parameter for unique-ifying. */
+  RANDOM: 'zx'
+};
+
+
+/**
+ * Sets the zx parameter of a URI to a random value.
+ * @param {string} uri Any URI.
+ * @return {string} That URI with the "zx" parameter added or replaced to
+ *     contain a random string.
+ */
+goog.uri.utils.makeUnique = function(uri) {
+  return goog.uri.utils.setParam(
+      uri, goog.uri.utils.StandardQueryParam.RANDOM,
+      goog.string.getRandomString());
+};
+
+// Copyright 2006 The Closure Library Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS-IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+/**
+ * @fileoverview Class for parsing and formatting URIs.
+ *
+ * Use goog.Uri(string) to parse a URI string.  Use goog.Uri.create(...) to
+ * create a new instance of the goog.Uri object from Uri parts.
+ *
+ * e.g: <code>var myUri = new goog.Uri(window.location);</code>
+ *
+ * Implements RFC 3986 for parsing/formatting URIs.
+ * http://www.ietf.org/rfc/rfc3986.txt
+ *
+ * Some changes have been made to the interface (more like .NETs), though the
+ * internal representation is now of un-encoded parts, this will change the
+ * behavior slightly.
+ *
+ */
+
+goog.provide('goog.Uri');
+goog.provide('goog.Uri.QueryData');
+
+goog.require('goog.array');
+goog.require('goog.asserts');
+goog.require('goog.string');
+goog.require('goog.structs');
+goog.require('goog.structs.Map');
+goog.require('goog.uri.utils');
+goog.require('goog.uri.utils.ComponentIndex');
+goog.require('goog.uri.utils.StandardQueryParam');
+
+
+
+/**
+ * This class contains setters and getters for the parts of the URI.
+ * The <code>getXyz</code>/<code>setXyz</code> methods return the decoded part
+ * -- so<code>goog.Uri.parse('/foo%20bar').getPath()</code> will return the
+ * decoded path, <code>/foo bar</code>.
+ *
+ * Reserved characters (see RFC 3986 section 2.2) can be present in
+ * their percent-encoded form in scheme, domain, and path URI components and
+ * will not be auto-decoded. For example:
+ * <code>goog.Uri.parse('rel%61tive/path%2fto/resource').getPath()</code> will
+ * return <code>relative/path%2fto/resource</code>.
+ *
+ * The constructor accepts an optional unparsed, raw URI string.  The parser
+ * is relaxed, so special characters that aren't escaped but don't cause
+ * ambiguities will not cause parse failures.
+ *
+ * All setters return <code>this</code> and so may be chained, a la
+ * <code>goog.Uri.parse('/foo').setFragment('part').toString()</code>.
+ *
+ * @param {*=} opt_uri Optional string URI to parse
+ *        (use goog.Uri.create() to create a URI from parts), or if
+ *        a goog.Uri is passed, a clone is created.
+ * @param {boolean=} opt_ignoreCase If true, #getParameterValue will ignore
+ * the case of the parameter name.
+ *
+ * @throws URIError If opt_uri is provided and URI is malformed (that is,
+ *     if decodeURIComponent fails on any of the URI components).
+ * @constructor
+ * @struct
+ */
+goog.Uri = function(opt_uri, opt_ignoreCase) {
+  /**
+   * Scheme such as "http".
+   * @private {string}
+   */
+  this.scheme_ = '';
+
+  /**
+   * User credentials in the form "username:password".
+   * @private {string}
+   */
+  this.userInfo_ = '';
+
+  /**
+   * Domain part, e.g. "www.google.com".
+   * @private {string}
+   */
+  this.domain_ = '';
+
+  /**
+   * Port, e.g. 8080.
+   * @private {?number}
+   */
+  this.port_ = null;
+
+  /**
+   * Path, e.g. "/tests/img.png".
+   * @private {string}
+   */
+  this.path_ = '';
+
+  /**
+   * The fragment without the #.
+   * @private {string}
+   */
+  this.fragment_ = '';
+
+  /**
+   * Whether or not this Uri should be treated as Read Only.
+   * @private {boolean}
+   */
+  this.isReadOnly_ = false;
+
+  /**
+   * Whether or not to ignore case when comparing query params.
+   * @private {boolean}
+   */
+  this.ignoreCase_ = false;
+
+  /**
+   * Object representing query data.
+   * @private {!goog.Uri.QueryData}
+   */
+  this.queryData_;
+
+  // Parse in the uri string
+  var m;
+  if (opt_uri instanceof goog.Uri) {
+    this.ignoreCase_ =
+        goog.isDef(opt_ignoreCase) ? opt_ignoreCase : opt_uri.getIgnoreCase();
+    this.setScheme(opt_uri.getScheme());
+    this.setUserInfo(opt_uri.getUserInfo());
+    this.setDomain(opt_uri.getDomain());
+    this.setPort(opt_uri.getPort());
+    this.setPath(opt_uri.getPath());
+    this.setQueryData(opt_uri.getQueryData().clone());
+    this.setFragment(opt_uri.getFragment());
+  } else if (opt_uri && (m = goog.uri.utils.split(String(opt_uri)))) {
+    this.ignoreCase_ = !!opt_ignoreCase;
+
+    // Set the parts -- decoding as we do so.
+    // COMPATIBILITY NOTE - In IE, unmatched fields may be empty strings,
+    // whereas in other browsers they will be undefined.
+    this.setScheme(m[goog.uri.utils.ComponentIndex.SCHEME] || '', true);
+    this.setUserInfo(m[goog.uri.utils.ComponentIndex.USER_INFO] || '', true);
+    this.setDomain(m[goog.uri.utils.ComponentIndex.DOMAIN] || '', true);
+    this.setPort(m[goog.uri.utils.ComponentIndex.PORT]);
+    this.setPath(m[goog.uri.utils.ComponentIndex.PATH] || '', true);
+    this.setQueryData(m[goog.uri.utils.ComponentIndex.QUERY_DATA] || '', true);
+    this.setFragment(m[goog.uri.utils.ComponentIndex.FRAGMENT] || '', true);
+
+  } else {
+    this.ignoreCase_ = !!opt_ignoreCase;
+    this.queryData_ = new goog.Uri.QueryData(null, null, this.ignoreCase_);
+  }
+};
+
+
+/**
+ * If true, we preserve the type of query parameters set programmatically.
+ *
+ * This means that if you set a parameter to a boolean, and then call
+ * getParameterValue, you will get a boolean back.
+ *
+ * If false, we will coerce parameters to strings, just as they would
+ * appear in real URIs.
+ *
+ * TODO(nicksantos): Remove this once people have time to fix all tests.
+ *
+ * @type {boolean}
+ */
+goog.Uri.preserveParameterTypesCompatibilityFlag = false;
+
+
+/**
+ * Parameter name added to stop caching.
+ * @type {string}
+ */
+goog.Uri.RANDOM_PARAM = goog.uri.utils.StandardQueryParam.RANDOM;
+
+
+/**
+ * @return {string} The string form of the url.
+ * @override
+ */
+goog.Uri.prototype.toString = function() {
+  var out = [];
+
+  var scheme = this.getScheme();
+  if (scheme) {
+    out.push(
+        goog.Uri.encodeSpecialChars_(
+            scheme, goog.Uri.reDisallowedInSchemeOrUserInfo_, true),
+        ':');
+  }
+
+  var domain = this.getDomain();
+  if (domain || scheme == 'file') {
+    out.push('//');
+
+    var userInfo = this.getUserInfo();
+    if (userInfo) {
+      out.push(
+          goog.Uri.encodeSpecialChars_(
+              userInfo, goog.Uri.reDisallowedInSchemeOrUserInfo_, true),
+          '@');
+    }
+
+    out.push(goog.Uri.removeDoubleEncoding_(goog.string.urlEncode(domain)));
+
+    var port = this.getPort();
+    if (port != null) {
+      out.push(':', String(port));
+    }
+  }
+
+  var path = this.getPath();
+  if (path) {
+    if (this.hasDomain() && path.charAt(0) != '/') {
+      out.push('/');
+    }
+    out.push(
+        goog.Uri.encodeSpecialChars_(
+            path, path.charAt(0) == '/' ? goog.Uri.reDisallowedInAbsolutePath_ :
+                                          goog.Uri.reDisallowedInRelativePath_,
+            true));
+  }
+
+  var query = this.getEncodedQuery();
+  if (query) {
+    out.push('?', query);
+  }
+
+  var fragment = this.getFragment();
+  if (fragment) {
+    out.push(
+        '#', goog.Uri.encodeSpecialChars_(
+                 fragment, goog.Uri.reDisallowedInFragment_));
+  }
+  return out.join('');
+};
+
+
+/**
+ * Resolves the given relative URI (a goog.Uri object), using the URI
+ * represented by this instance as the base URI.
+ *
+ * There are several kinds of relative URIs:<br>
+ * 1. foo - replaces the last part of the path, the whole query and fragment<br>
+ * 2. /foo - replaces the the path, the query and fragment<br>
+ * 3. //foo - replaces everything from the domain on.  foo is a domain name<br>
+ * 4. ?foo - replace the query and fragment<br>
+ * 5. #foo - replace the fragment only
+ *
+ * Additionally, if relative URI has a non-empty path, all ".." and "."
+ * segments will be resolved, as described in RFC 3986.
+ *
+ * @param {!goog.Uri} relativeUri The relative URI to resolve.
+ * @return {!goog.Uri} The resolved URI.
+ */
+goog.Uri.prototype.resolve = function(relativeUri) {
+
+  var absoluteUri = this.clone();
+
+  // we satisfy these conditions by looking for the first part of relativeUri
+  // that is not blank and applying defaults to the rest
+
+  var overridden = relativeUri.hasScheme();
+
+  if (overridden) {
+    absoluteUri.setScheme(relativeUri.getScheme());
+  } else {
+    overridden = relativeUri.hasUserInfo();
+  }
+
+  if (overridden) {
+    absoluteUri.setUserInfo(relativeUri.getUserInfo());
+  } else {
+    overridden = relativeUri.hasDomain();
+  }
+
+  if (overridden) {
+    absoluteUri.setDomain(relativeUri.getDomain());
+  } else {
+    overridden = relativeUri.hasPort();
+  }
+
+  var path = relativeUri.getPath();
+  if (overridden) {
+    absoluteUri.setPort(relativeUri.getPort());
+  } else {
+    overridden = relativeUri.hasPath();
+    if (overridden) {
+      // resolve path properly
+      if (path.charAt(0) != '/') {
+        // path is relative
+        if (this.hasDomain() && !this.hasPath()) {
+          // RFC 3986, section 5.2.3, case 1
+          path = '/' + path;
+        } else {
+          // RFC 3986, section 5.2.3, case 2
+          var lastSlashIndex = absoluteUri.getPath().lastIndexOf('/');
+          if (lastSlashIndex != -1) {
+            path = absoluteUri.getPath().substr(0, lastSlashIndex + 1) + path;
+          }
+        }
+      }
+      path = goog.Uri.removeDotSegments(path);
+    }
+  }
+
+  if (overridden) {
+    absoluteUri.setPath(path);
+  } else {
+    overridden = relativeUri.hasQuery();
+  }
+
+  if (overridden) {
+    absoluteUri.setQueryData(relativeUri.getDecodedQuery());
+  } else {
+    overridden = relativeUri.hasFragment();
+  }
+
+  if (overridden) {
+    absoluteUri.setFragment(relativeUri.getFragment());
+  }
+
+  return absoluteUri;
+};
+
+
+/**
+ * Clones the URI instance.
+ * @return {!goog.Uri} New instance of the URI object.
+ */
+goog.Uri.prototype.clone = function() {
+  return new goog.Uri(this);
+};
+
+
+/**
+ * @return {string} The encoded scheme/protocol for the URI.
+ */
+goog.Uri.prototype.getScheme = function() {
+  return this.scheme_;
+};
+
+
+/**
+ * Sets the scheme/protocol.
+ * @throws URIError If opt_decode is true and newScheme is malformed (that is,
+ *     if decodeURIComponent fails).
+ * @param {string} newScheme New scheme value.
+ * @param {boolean=} opt_decode Optional param for whether to decode new value.
+ * @return {!goog.Uri} Reference to this URI object.
+ */
+goog.Uri.prototype.setScheme = function(newScheme, opt_decode) {
+  this.enforceReadOnly();
+  this.scheme_ =
+      opt_decode ? goog.Uri.decodeOrEmpty_(newScheme, true) : newScheme;
+
+  // remove an : at the end of the scheme so somebody can pass in
+  // window.location.protocol
+  if (this.scheme_) {
+    this.scheme_ = this.scheme_.replace(/:$/, '');
+  }
+  return this;
+};
+
+
+/**
+ * @return {boolean} Whether the scheme has been set.
+ */
+goog.Uri.prototype.hasScheme = function() {
+  return !!this.scheme_;
+};
+
+
+/**
+ * @return {string} The decoded user info.
+ */
+goog.Uri.prototype.getUserInfo = function() {
+  return this.userInfo_;
+};
+
+
+/**
+ * Sets the userInfo.
+ * @throws URIError If opt_decode is true and newUserInfo is malformed (that is,
+ *     if decodeURIComponent fails).
+ * @param {string} newUserInfo New userInfo value.
+ * @param {boolean=} opt_decode Optional param for whether to decode new value.
+ * @return {!goog.Uri} Reference to this URI object.
+ */
+goog.Uri.prototype.setUserInfo = function(newUserInfo, opt_decode) {
+  this.enforceReadOnly();
+  this.userInfo_ =
+      opt_decode ? goog.Uri.decodeOrEmpty_(newUserInfo) : newUserInfo;
+  return this;
+};
+
+
+/**
+ * @return {boolean} Whether the user info has been set.
+ */
+goog.Uri.prototype.hasUserInfo = function() {
+  return !!this.userInfo_;
+};
+
+
+/**
+ * @return {string} The decoded domain.
+ */
+goog.Uri.prototype.getDomain = function() {
+  return this.domain_;
+};
+
+
+/**
+ * Sets the domain.
+ * @throws URIError If opt_decode is true and newDomain is malformed (that is,
+ *     if decodeURIComponent fails).
+ * @param {string} newDomain New domain value.
+ * @param {boolean=} opt_decode Optional param for whether to decode new value.
+ * @return {!goog.Uri} Reference to this URI object.
+ */
+goog.Uri.prototype.setDomain = function(newDomain, opt_decode) {
+  this.enforceReadOnly();
+  this.domain_ =
+      opt_decode ? goog.Uri.decodeOrEmpty_(newDomain, true) : newDomain;
+  return this;
+};
+
+
+/**
+ * @return {boolean} Whether the domain has been set.
+ */
+goog.Uri.prototype.hasDomain = function() {
+  return !!this.domain_;
+};
+
+
+/**
+ * @return {?number} The port number.
+ */
+goog.Uri.prototype.getPort = function() {
+  return this.port_;
+};
+
+
+/**
+ * Sets the port number.
+ * @param {*} newPort Port number. Will be explicitly casted to a number.
+ * @return {!goog.Uri} Reference to this URI object.
+ */
+goog.Uri.prototype.setPort = function(newPort) {
+  this.enforceReadOnly();
+
+  if (newPort) {
+    newPort = Number(newPort);
+    if (isNaN(newPort) || newPort < 0) {
+      throw Error('Bad port number ' + newPort);
+    }
+    this.port_ = newPort;
+  } else {
+    this.port_ = null;
+  }
+
+  return this;
+};
+
+
+/**
+ * @return {boolean} Whether the port has been set.
+ */
+goog.Uri.prototype.hasPort = function() {
+  return this.port_ != null;
+};
+
+
+/**
+  * @return {string} The decoded path.
+ */
+goog.Uri.prototype.getPath = function() {
+  return this.path_;
+};
+
+
+/**
+ * Sets the path.
+ * @throws URIError If opt_decode is true and newPath is malformed (that is,
+ *     if decodeURIComponent fails).
+ * @param {string} newPath New path value.
+ * @param {boolean=} opt_decode Optional param for whether to decode new value.
+ * @return {!goog.Uri} Reference to this URI object.
+ */
+goog.Uri.prototype.setPath = function(newPath, opt_decode) {
+  this.enforceReadOnly();
+  this.path_ = opt_decode ? goog.Uri.decodeOrEmpty_(newPath, true) : newPath;
+  return this;
+};
+
+
+/**
+ * @return {boolean} Whether the path has been set.
+ */
+goog.Uri.prototype.hasPath = function() {
+  return !!this.path_;
+};
+
+
+/**
+ * @return {boolean} Whether the query string has been set.
+ */
+goog.Uri.prototype.hasQuery = function() {
+  return this.queryData_.toString() !== '';
+};
+
+
+/**
+ * Sets the query data.
+ * @param {goog.Uri.QueryData|string|undefined} queryData QueryData object.
+ * @param {boolean=} opt_decode Optional param for whether to decode new value.
+ *     Applies only if queryData is a string.
+ * @return {!goog.Uri} Reference to this URI object.
+ */
+goog.Uri.prototype.setQueryData = function(queryData, opt_decode) {
+  this.enforceReadOnly();
+
+  if (queryData instanceof goog.Uri.QueryData) {
+    this.queryData_ = queryData;
+    this.queryData_.setIgnoreCase(this.ignoreCase_);
+  } else {
+    if (!opt_decode) {
+      // QueryData accepts encoded query string, so encode it if
+      // opt_decode flag is not true.
+      queryData = goog.Uri.encodeSpecialChars_(
+          queryData, goog.Uri.reDisallowedInQuery_);
+    }
+    this.queryData_ = new goog.Uri.QueryData(queryData, null, this.ignoreCase_);
+  }
+
+  return this;
+};
+
+
+/**
+ * Sets the URI query.
+ * @param {string} newQuery New query value.
+ * @param {boolean=} opt_decode Optional param for whether to decode new value.
+ * @return {!goog.Uri} Reference to this URI object.
+ */
+goog.Uri.prototype.setQuery = function(newQuery, opt_decode) {
+  return this.setQueryData(newQuery, opt_decode);
+};
+
+
+/**
+ * @return {string} The encoded URI query, not including the ?.
+ */
+goog.Uri.prototype.getEncodedQuery = function() {
+  return this.queryData_.toString();
+};
+
+
+/**
+ * @return {string} The decoded URI query, not including the ?.
+ */
+goog.Uri.prototype.getDecodedQuery = function() {
+  return this.queryData_.toDecodedString();
+};
+
+
+/**
+ * Returns the query data.
+ * @return {!goog.Uri.QueryData} QueryData object.
+ */
+goog.Uri.prototype.getQueryData = function() {
+  return this.queryData_;
+};
+
+
+/**
+ * @return {string} The encoded URI query, not including the ?.
+ *
+ * Warning: This method, unlike other getter methods, returns encoded
+ * value, instead of decoded one.
+ */
+goog.Uri.prototype.getQuery = function() {
+  return this.getEncodedQuery();
+};
+
+
+/**
+ * Sets the value of the named query parameters, clearing previous values for
+ * that key.
+ *
+ * @param {string} key The parameter to set.
+ * @param {*} value The new value.
+ * @return {!goog.Uri} Reference to this URI object.
+ */
+goog.Uri.prototype.setParameterValue = function(key, value) {
+  this.enforceReadOnly();
+  this.queryData_.set(key, value);
+  return this;
+};
+
+
+/**
+ * Sets the values of the named query parameters, clearing previous values for
+ * that key.  Not new values will currently be moved to the end of the query
+ * string.
+ *
+ * So, <code>goog.Uri.parse('foo?a=b&c=d&e=f').setParameterValues('c', ['new'])
+ * </code> yields <tt>foo?a=b&e=f&c=new</tt>.</p>
+ *
+ * @param {string} key The parameter to set.
+ * @param {*} values The new values. If values is a single
+ *     string then it will be treated as the sole value.
+ * @return {!goog.Uri} Reference to this URI object.
+ */
+goog.Uri.prototype.setParameterValues = function(key, values) {
+  this.enforceReadOnly();
+
+  if (!goog.isArray(values)) {
+    values = [String(values)];
+  }
+
+  this.queryData_.setValues(key, values);
+
+  return this;
+};
+
+
+/**
+ * Returns the value<b>s</b> for a given cgi parameter as a list of decoded
+ * query parameter values.
+ * @param {string} name The parameter to get values for.
+ * @return {!Array<?>} The values for a given cgi parameter as a list of
+ *     decoded query parameter values.
+ */
+goog.Uri.prototype.getParameterValues = function(name) {
+  return this.queryData_.getValues(name);
+};
+
+
+/**
+ * Returns the first value for a given cgi parameter or undefined if the given
+ * parameter name does not appear in the query string.
+ * @param {string} paramName Unescaped parameter name.
+ * @return {string|undefined} The first value for a given cgi parameter or
+ *     undefined if the given parameter name does not appear in the query
+ *     string.
+ */
+goog.Uri.prototype.getParameterValue = function(paramName) {
+  // NOTE(nicksantos): This type-cast is a lie when
+  // preserveParameterTypesCompatibilityFlag is set to true.
+  // But this should only be set to true in tests.
+  return /** @type {string|undefined} */ (this.queryData_.get(paramName));
+};
+
+
+/**
+ * @return {string} The URI fragment, not including the #.
+ */
+goog.Uri.prototype.getFragment = function() {
+  return this.fragment_;
+};
+
+
+/**
+ * Sets the URI fragment.
+ * @throws URIError If opt_decode is true and newFragment is malformed (that is,
+ *     if decodeURIComponent fails).
+ * @param {string} newFragment New fragment value.
+ * @param {boolean=} opt_decode Optional param for whether to decode new value.
+ * @return {!goog.Uri} Reference to this URI object.
+ */
+goog.Uri.prototype.setFragment = function(newFragment, opt_decode) {
+  this.enforceReadOnly();
+  this.fragment_ =
+      opt_decode ? goog.Uri.decodeOrEmpty_(newFragment) : newFragment;
+  return this;
+};
+
+
+/**
+ * @return {boolean} Whether the URI has a fragment set.
+ */
+goog.Uri.prototype.hasFragment = function() {
+  return !!this.fragment_;
+};
+
+
+/**
+ * Returns true if this has the same domain as that of uri2.
+ * @param {!goog.Uri} uri2 The URI object to compare to.
+ * @return {boolean} true if same domain; false otherwise.
+ */
+goog.Uri.prototype.hasSameDomainAs = function(uri2) {
+  return ((!this.hasDomain() && !uri2.hasDomain()) ||
+          this.getDomain() == uri2.getDomain()) &&
+      ((!this.hasPort() && !uri2.hasPort()) ||
+       this.getPort() == uri2.getPort());
+};
+
+
+/**
+ * Adds a random parameter to the Uri.
+ * @return {!goog.Uri} Reference to this Uri object.
+ */
+goog.Uri.prototype.makeUnique = function() {
+  this.enforceReadOnly();
+  this.setParameterValue(goog.Uri.RANDOM_PARAM, goog.string.getRandomString());
+
+  return this;
+};
+
+
+/**
+ * Removes the named query parameter.
+ *
+ * @param {string} key The parameter to remove.
+ * @return {!goog.Uri} Reference to this URI object.
+ */
+goog.Uri.prototype.removeParameter = function(key) {
+  this.enforceReadOnly();
+  this.queryData_.remove(key);
+  return this;
+};
+
+
+/**
+ * Sets whether Uri is read only. If this goog.Uri is read-only,
+ * enforceReadOnly_ will be called at the start of any function that may modify
+ * this Uri.
+ * @param {boolean} isReadOnly whether this goog.Uri should be read only.
+ * @return {!goog.Uri} Reference to this Uri object.
+ */
+goog.Uri.prototype.setReadOnly = function(isReadOnly) {
+  this.isReadOnly_ = isReadOnly;
+  return this;
+};
+
+
+/**
+ * @return {boolean} Whether the URI is read only.
+ */
+goog.Uri.prototype.isReadOnly = function() {
+  return this.isReadOnly_;
+};
+
+
+/**
+ * Checks if this Uri has been marked as read only, and if so, throws an error.
+ * This should be called whenever any modifying function is called.
+ */
+goog.Uri.prototype.enforceReadOnly = function() {
+  if (this.isReadOnly_) {
+    throw Error('Tried to modify a read-only Uri');
+  }
+};
+
+
+/**
+ * Sets whether to ignore case.
+ * NOTE: If there are already key/value pairs in the QueryData, and
+ * ignoreCase_ is set to false, the keys will all be lower-cased.
+ * @param {boolean} ignoreCase whether this goog.Uri should ignore case.
+ * @return {!goog.Uri} Reference to this Uri object.
+ */
+goog.Uri.prototype.setIgnoreCase = function(ignoreCase) {
+  this.ignoreCase_ = ignoreCase;
+  if (this.queryData_) {
+    this.queryData_.setIgnoreCase(ignoreCase);
+  }
+  return this;
+};
+
+
+/**
+ * @return {boolean} Whether to ignore case.
+ */
+goog.Uri.prototype.getIgnoreCase = function() {
+  return this.ignoreCase_;
+};
+
+
+//==============================================================================
+// Static members
+//==============================================================================
+
+
+/**
+ * Creates a uri from the string form.  Basically an alias of new goog.Uri().
+ * If a Uri object is passed to parse then it will return a clone of the object.
+ *
+ * @throws URIError If parsing the URI is malformed. The passed URI components
+ *     should all be parseable by decodeURIComponent.
+ * @param {*} uri Raw URI string or instance of Uri
+ *     object.
+ * @param {boolean=} opt_ignoreCase Whether to ignore the case of parameter
+ * names in #getParameterValue.
+ * @return {!goog.Uri} The new URI object.
+ */
+goog.Uri.parse = function(uri, opt_ignoreCase) {
+  return uri instanceof goog.Uri ? uri.clone() :
+                                   new goog.Uri(uri, opt_ignoreCase);
+};
+
+
+/**
+ * Creates a new goog.Uri object from unencoded parts.
+ *
+ * @param {?string=} opt_scheme Scheme/protocol or full URI to parse.
+ * @param {?string=} opt_userInfo username:password.
+ * @param {?string=} opt_domain www.google.com.
+ * @param {?number=} opt_port 9830.
+ * @param {?string=} opt_path /some/path/to/a/file.html.
+ * @param {string|goog.Uri.QueryData=} opt_query a=1&b=2.
+ * @param {?string=} opt_fragment The fragment without the #.
+ * @param {boolean=} opt_ignoreCase Whether to ignore parameter name case in
+ *     #getParameterValue.
+ *
+ * @return {!goog.Uri} The new URI object.
+ */
+goog.Uri.create = function(
+    opt_scheme, opt_userInfo, opt_domain, opt_port, opt_path, opt_query,
+    opt_fragment, opt_ignoreCase) {
+
+  var uri = new goog.Uri(null, opt_ignoreCase);
+
+  // Only set the parts if they are defined and not empty strings.
+  opt_scheme && uri.setScheme(opt_scheme);
+  opt_userInfo && uri.setUserInfo(opt_userInfo);
+  opt_domain && uri.setDomain(opt_domain);
+  opt_port && uri.setPort(opt_port);
+  opt_path && uri.setPath(opt_path);
+  opt_query && uri.setQueryData(opt_query);
+  opt_fragment && uri.setFragment(opt_fragment);
+
+  return uri;
+};
+
+
+/**
+ * Resolves a relative Uri against a base Uri, accepting both strings and
+ * Uri objects.
+ *
+ * @param {*} base Base Uri.
+ * @param {*} rel Relative Uri.
+ * @return {!goog.Uri} Resolved uri.
+ */
+goog.Uri.resolve = function(base, rel) {
+  if (!(base instanceof goog.Uri)) {
+    base = goog.Uri.parse(base);
+  }
+
+  if (!(rel instanceof goog.Uri)) {
+    rel = goog.Uri.parse(rel);
+  }
+
+  return base.resolve(rel);
+};
+
+
+/**
+ * Removes dot segments in given path component, as described in
+ * RFC 3986, section 5.2.4.
+ *
+ * @param {string} path A non-empty path component.
+ * @return {string} Path component with removed dot segments.
+ */
+goog.Uri.removeDotSegments = function(path) {
+  if (path == '..' || path == '.') {
+    return '';
+
+  } else if (
+      !goog.string.contains(path, './') && !goog.string.contains(path, '/.')) {
+    // This optimization detects uris which do not contain dot-segments,
+    // and as a consequence do not require any processing.
+    return path;
+
+  } else {
+    var leadingSlash = goog.string.startsWith(path, '/');
+    var segments = path.split('/');
+    var out = [];
+
+    for (var pos = 0; pos < segments.length;) {
+      var segment = segments[pos++];
+
+      if (segment == '.') {
+        if (leadingSlash && pos == segments.length) {
+          out.push('');
+        }
+      } else if (segment == '..') {
+        if (out.length > 1 || out.length == 1 && out[0] != '') {
+          out.pop();
+        }
+        if (leadingSlash && pos == segments.length) {
+          out.push('');
+        }
+      } else {
+        out.push(segment);
+        leadingSlash = true;
+      }
+    }
+
+    return out.join('/');
+  }
+};
+
+
+/**
+ * Decodes a value or returns the empty string if it isn't defined or empty.
+ * @throws URIError If decodeURIComponent fails to decode val.
+ * @param {string|undefined} val Value to decode.
+ * @param {boolean=} opt_preserveReserved If true, restricted characters will
+ *     not be decoded.
+ * @return {string} Decoded value.
+ * @private
+ */
+goog.Uri.decodeOrEmpty_ = function(val, opt_preserveReserved) {
+  // Don't use UrlDecode() here because val is not a query parameter.
+  if (!val) {
+    return '';
+  }
+
+  // decodeURI has the same output for '%2f' and '%252f'. We double encode %25
+  // so that we can distinguish between the 2 inputs. This is later undone by
+  // removeDoubleEncoding_.
+  return opt_preserveReserved ? decodeURI(val.replace(/%25/g, '%2525')) :
+                                decodeURIComponent(val);
+};
+
+
+/**
+ * If unescapedPart is non null, then escapes any characters in it that aren't
+ * valid characters in a url and also escapes any special characters that
+ * appear in extra.
+ *
+ * @param {*} unescapedPart The string to encode.
+ * @param {RegExp} extra A character set of characters in [\01-\177].
+ * @param {boolean=} opt_removeDoubleEncoding If true, remove double percent
+ *     encoding.
+ * @return {?string} null iff unescapedPart == null.
+ * @private
+ */
+goog.Uri.encodeSpecialChars_ = function(
+    unescapedPart, extra, opt_removeDoubleEncoding) {
+  if (goog.isString(unescapedPart)) {
+    var encoded = encodeURI(unescapedPart).replace(extra, goog.Uri.encodeChar_);
+    if (opt_removeDoubleEncoding) {
+      // encodeURI double-escapes %XX sequences used to represent restricted
+      // characters in some URI components, remove the double escaping here.
+      encoded = goog.Uri.removeDoubleEncoding_(encoded);
+    }
+    return encoded;
+  }
+  return null;
+};
+
+
+/**
+ * Converts a character in [\01-\177] to its unicode character equivalent.
+ * @param {string} ch One character string.
+ * @return {string} Encoded string.
+ * @private
+ */
+goog.Uri.encodeChar_ = function(ch) {
+  var n = ch.charCodeAt(0);
+  return '%' + ((n >> 4) & 0xf).toString(16) + (n & 0xf).toString(16);
+};
+
+
+/**
+ * Removes double percent-encoding from a string.
+ * @param  {string} doubleEncodedString String
+ * @return {string} String with double encoding removed.
+ * @private
+ */
+goog.Uri.removeDoubleEncoding_ = function(doubleEncodedString) {
+  return doubleEncodedString.replace(/%25([0-9a-fA-F]{2})/g, '%$1');
+};
+
+
+/**
+ * Regular expression for characters that are disallowed in the scheme or
+ * userInfo part of the URI.
+ * @type {RegExp}
+ * @private
+ */
+goog.Uri.reDisallowedInSchemeOrUserInfo_ = /[#\/\?@]/g;
+
+
+/**
+ * Regular expression for characters that are disallowed in a relative path.
+ * Colon is included due to RFC 3986 3.3.
+ * @type {RegExp}
+ * @private
+ */
+goog.Uri.reDisallowedInRelativePath_ = /[\#\?:]/g;
+
+
+/**
+ * Regular expression for characters that are disallowed in an absolute path.
+ * @type {RegExp}
+ * @private
+ */
+goog.Uri.reDisallowedInAbsolutePath_ = /[\#\?]/g;
+
+
+/**
+ * Regular expression for characters that are disallowed in the query.
+ * @type {RegExp}
+ * @private
+ */
+goog.Uri.reDisallowedInQuery_ = /[\#\?@]/g;
+
+
+/**
+ * Regular expression for characters that are disallowed in the fragment.
+ * @type {RegExp}
+ * @private
+ */
+goog.Uri.reDisallowedInFragment_ = /#/g;
+
+
+/**
+ * Checks whether two URIs have the same domain.
+ * @param {string} uri1String First URI string.
+ * @param {string} uri2String Second URI string.
+ * @return {boolean} true if the two URIs have the same domain; false otherwise.
+ */
+goog.Uri.haveSameDomain = function(uri1String, uri2String) {
+  // Differs from goog.uri.utils.haveSameDomain, since this ignores scheme.
+  // TODO(gboyer): Have this just call goog.uri.util.haveSameDomain.
+  var pieces1 = goog.uri.utils.split(uri1String);
+  var pieces2 = goog.uri.utils.split(uri2String);
+  return pieces1[goog.uri.utils.ComponentIndex.DOMAIN] ==
+      pieces2[goog.uri.utils.ComponentIndex.DOMAIN] &&
+      pieces1[goog.uri.utils.ComponentIndex.PORT] ==
+      pieces2[goog.uri.utils.ComponentIndex.PORT];
+};
+
+
+
+/**
+ * Class used to represent URI query parameters.  It is essentially a hash of
+ * name-value pairs, though a name can be present more than once.
+ *
+ * Has the same interface as the collections in goog.structs.
+ *
+ * @param {?string=} opt_query Optional encoded query string to parse into
+ *     the object.
+ * @param {goog.Uri=} opt_uri Optional uri object that should have its
+ *     cache invalidated when this object updates. Deprecated -- this
+ *     is no longer required.
+ * @param {boolean=} opt_ignoreCase If true, ignore the case of the parameter
+ *     name in #get.
+ * @constructor
+ * @struct
+ * @final
+ */
+goog.Uri.QueryData = function(opt_query, opt_uri, opt_ignoreCase) {
+  /**
+   * The map containing name/value or name/array-of-values pairs.
+   * May be null if it requires parsing from the query string.
+   *
+   * We need to use a Map because we cannot guarantee that the key names will
+   * not be problematic for IE.
+   *
+   * @private {goog.structs.Map<string, !Array<*>>}
+   */
+  this.keyMap_ = null;
+
+  /**
+   * The number of params, or null if it requires computing.
+   * @private {?number}
+   */
+  this.count_ = null;
+
+  /**
+   * Encoded query string, or null if it requires computing from the key map.
+   * @private {?string}
+   */
+  this.encodedQuery_ = opt_query || null;
+
+  /**
+   * If true, ignore the case of the parameter name in #get.
+   * @private {boolean}
+   */
+  this.ignoreCase_ = !!opt_ignoreCase;
+};
+
+
+/**
+ * If the underlying key map is not yet initialized, it parses the
+ * query string and fills the map with parsed data.
+ * @private
+ */
+goog.Uri.QueryData.prototype.ensureKeyMapInitialized_ = function() {
+  if (!this.keyMap_) {
+    this.keyMap_ = new goog.structs.Map();
+    this.count_ = 0;
+    if (this.encodedQuery_) {
+      var self = this;
+      goog.uri.utils.parseQueryData(this.encodedQuery_, function(name, value) {
+        self.add(goog.string.urlDecode(name), value);
+      });
+    }
+  }
+};
+
+
+/**
+ * Creates a new query data instance from a map of names and values.
+ *
+ * @param {!goog.structs.Map<string, ?>|!Object} map Map of string parameter
+ *     names to parameter value. If parameter value is an array, it is
+ *     treated as if the key maps to each individual value in the
+ *     array.
+ * @param {goog.Uri=} opt_uri URI object that should have its cache
+ *     invalidated when this object updates.
+ * @param {boolean=} opt_ignoreCase If true, ignore the case of the parameter
+ *     name in #get.
+ * @return {!goog.Uri.QueryData} The populated query data instance.
+ */
+goog.Uri.QueryData.createFromMap = function(map, opt_uri, opt_ignoreCase) {
+  var keys = goog.structs.getKeys(map);
+  if (typeof keys == 'undefined') {
+    throw Error('Keys are undefined');
+  }
+
+  var queryData = new goog.Uri.QueryData(null, null, opt_ignoreCase);
+  var values = goog.structs.getValues(map);
+  for (var i = 0; i < keys.length; i++) {
+    var key = keys[i];
+    var value = values[i];
+    if (!goog.isArray(value)) {
+      queryData.add(key, value);
+    } else {
+      queryData.setValues(key, value);
+    }
+  }
+  return queryData;
+};
+
+
+/**
+ * Creates a new query data instance from parallel arrays of parameter names
+ * and values. Allows for duplicate parameter names. Throws an error if the
+ * lengths of the arrays differ.
+ *
+ * @param {!Array<string>} keys Parameter names.
+ * @param {!Array<?>} values Parameter values.
+ * @param {goog.Uri=} opt_uri URI object that should have its cache
+ *     invalidated when this object updates.
+ * @param {boolean=} opt_ignoreCase If true, ignore the case of the parameter
+ *     name in #get.
+ * @return {!goog.Uri.QueryData} The populated query data instance.
+ */
+goog.Uri.QueryData.createFromKeysValues = function(
+    keys, values, opt_uri, opt_ignoreCase) {
+  if (keys.length != values.length) {
+    throw Error('Mismatched lengths for keys/values');
+  }
+  var queryData = new goog.Uri.QueryData(null, null, opt_ignoreCase);
+  for (var i = 0; i < keys.length; i++) {
+    queryData.add(keys[i], values[i]);
+  }
+  return queryData;
+};
+
+
+/**
+ * @return {?number} The number of parameters.
+ */
+goog.Uri.QueryData.prototype.getCount = function() {
+  this.ensureKeyMapInitialized_();
+  return this.count_;
+};
+
+
+/**
+ * Adds a key value pair.
+ * @param {string} key Name.
+ * @param {*} value Value.
+ * @return {!goog.Uri.QueryData} Instance of this object.
+ */
+goog.Uri.QueryData.prototype.add = function(key, value) {
+  this.ensureKeyMapInitialized_();
+  this.invalidateCache_();
+
+  key = this.getKeyName_(key);
+  var values = this.keyMap_.get(key);
+  if (!values) {
+    this.keyMap_.set(key, (values = []));
+  }
+  values.push(value);
+  this.count_ = goog.asserts.assertNumber(this.count_) + 1;
+  return this;
+};
+
+
+/**
+ * Removes all the params with the given key.
+ * @param {string} key Name.
+ * @return {boolean} Whether any parameter was removed.
+ */
+goog.Uri.QueryData.prototype.remove = function(key) {
+  this.ensureKeyMapInitialized_();
+
+  key = this.getKeyName_(key);
+  if (this.keyMap_.containsKey(key)) {
+    this.invalidateCache_();
+
+    // Decrement parameter count.
+    this.count_ =
+        goog.asserts.assertNumber(this.count_) - this.keyMap_.get(key).length;
+    return this.keyMap_.remove(key);
+  }
+  return false;
+};
+
+
+/**
+ * Clears the parameters.
+ */
+goog.Uri.QueryData.prototype.clear = function() {
+  this.invalidateCache_();
+  this.keyMap_ = null;
+  this.count_ = 0;
+};
+
+
+/**
+ * @return {boolean} Whether we have any parameters.
+ */
+goog.Uri.QueryData.prototype.isEmpty = function() {
+  this.ensureKeyMapInitialized_();
+  return this.count_ == 0;
+};
+
+
+/**
+ * Whether there is a parameter with the given name
+ * @param {string} key The parameter name to check for.
+ * @return {boolean} Whether there is a parameter with the given name.
+ */
+goog.Uri.QueryData.prototype.containsKey = function(key) {
+  this.ensureKeyMapInitialized_();
+  key = this.getKeyName_(key);
+  return this.keyMap_.containsKey(key);
+};
+
+
+/**
+ * Whether there is a parameter with the given value.
+ * @param {*} value The value to check for.
+ * @return {boolean} Whether there is a parameter with the given value.
+ */
+goog.Uri.QueryData.prototype.containsValue = function(value) {
+  // NOTE(arv): This solution goes through all the params even if it was the
+  // first param. We can get around this by not reusing code or by switching to
+  // iterators.
+  var vals = this.getValues();
+  return goog.array.contains(vals, value);
+};
+
+
+/**
+ * Returns all the keys of the parameters. If a key is used multiple times
+ * it will be included multiple times in the returned array
+ * @return {!Array<string>} All the keys of the parameters.
+ */
+goog.Uri.QueryData.prototype.getKeys = function() {
+  this.ensureKeyMapInitialized_();
+  // We need to get the values to know how many keys to add.
+  var vals = this.keyMap_.getValues();
+  var keys = this.keyMap_.getKeys();
+  var rv = [];
+  for (var i = 0; i < keys.length; i++) {
+    var val = vals[i];
+    for (var j = 0; j < val.length; j++) {
+      rv.push(keys[i]);
+    }
+  }
+  return rv;
+};
+
+
+/**
+ * Returns all the values of the parameters with the given name. If the query
+ * data has no such key this will return an empty array. If no key is given
+ * all values wil be returned.
+ * @param {string=} opt_key The name of the parameter to get the values for.
+ * @return {!Array<?>} All the values of the parameters with the given name.
+ */
+goog.Uri.QueryData.prototype.getValues = function(opt_key) {
+  this.ensureKeyMapInitialized_();
+  var rv = [];
+  if (goog.isString(opt_key)) {
+    if (this.containsKey(opt_key)) {
+      rv = goog.array.concat(rv, this.keyMap_.get(this.getKeyName_(opt_key)));
+    }
+  } else {
+    // Return all values.
+    var values = this.keyMap_.getValues();
+    for (var i = 0; i < values.length; i++) {
+      rv = goog.array.concat(rv, values[i]);
+    }
+  }
+  return rv;
+};
+
+
+/**
+ * Sets a key value pair and removes all other keys with the same value.
+ *
+ * @param {string} key Name.
+ * @param {*} value Value.
+ * @return {!goog.Uri.QueryData} Instance of this object.
+ */
+goog.Uri.QueryData.prototype.set = function(key, value) {
+  this.ensureKeyMapInitialized_();
+  this.invalidateCache_();
+
+  // TODO(chrishenry): This could be better written as
+  // this.remove(key), this.add(key, value), but that would reorder
+  // the key (since the key is first removed and then added at the
+  // end) and we would have to fix unit tests that depend on key
+  // ordering.
+  key = this.getKeyName_(key);
+  if (this.containsKey(key)) {
+    this.count_ =
+        goog.asserts.assertNumber(this.count_) - this.keyMap_.get(key).length;
+  }
+  this.keyMap_.set(key, [value]);
+  this.count_ = goog.asserts.assertNumber(this.count_) + 1;
+  return this;
+};
+
+
+/**
+ * Returns the first value associated with the key. If the query data has no
+ * such key this will return undefined or the optional default.
+ * @param {string} key The name of the parameter to get the value for.
+ * @param {*=} opt_default The default value to return if the query data
+ *     has no such key.
+ * @return {*} The first string value associated with the key, or opt_default
+ *     if there's no value.
+ */
+goog.Uri.QueryData.prototype.get = function(key, opt_default) {
+  var values = key ? this.getValues(key) : [];
+  if (goog.Uri.preserveParameterTypesCompatibilityFlag) {
+    return values.length > 0 ? values[0] : opt_default;
+  } else {
+    return values.length > 0 ? String(values[0]) : opt_default;
+  }
+};
+
+
+/**
+ * Sets the values for a key. If the key already exists, this will
+ * override all of the existing values that correspond to the key.
+ * @param {string} key The key to set values for.
+ * @param {!Array<?>} values The values to set.
+ */
+goog.Uri.QueryData.prototype.setValues = function(key, values) {
+  this.remove(key);
+
+  if (values.length > 0) {
+    this.invalidateCache_();
+    this.keyMap_.set(this.getKeyName_(key), goog.array.clone(values));
+    this.count_ = goog.asserts.assertNumber(this.count_) + values.length;
+  }
+};
+
+
+/**
+ * @return {string} Encoded query string.
+ * @override
+ */
+goog.Uri.QueryData.prototype.toString = function() {
+  if (this.encodedQuery_) {
+    return this.encodedQuery_;
+  }
+
+  if (!this.keyMap_) {
+    return '';
+  }
+
+  var sb = [];
+
+  // In the past, we use this.getKeys() and this.getVals(), but that
+  // generates a lot of allocations as compared to simply iterating
+  // over the keys.
+  var keys = this.keyMap_.getKeys();
+  for (var i = 0; i < keys.length; i++) {
+    var key = keys[i];
+    var encodedKey = goog.string.urlEncode(key);
+    var val = this.getValues(key);
+    for (var j = 0; j < val.length; j++) {
+      var param = encodedKey;
+      // Ensure that null and undefined are encoded into the url as
+      // literal strings.
+      if (val[j] !== '') {
+        param += '=' + goog.string.urlEncode(val[j]);
+      }
+      sb.push(param);
+    }
+  }
+
+  return this.encodedQuery_ = sb.join('&');
+};
+
+
+/**
+ * @throws URIError If URI is malformed (that is, if decodeURIComponent fails on
+ *     any of the URI components).
+ * @return {string} Decoded query string.
+ */
+goog.Uri.QueryData.prototype.toDecodedString = function() {
+  return goog.Uri.decodeOrEmpty_(this.toString());
+};
+
+
+/**
+ * Invalidate the cache.
+ * @private
+ */
+goog.Uri.QueryData.prototype.invalidateCache_ = function() {
+  this.encodedQuery_ = null;
+};
+
+
+/**
+ * Removes all keys that are not in the provided list. (Modifies this object.)
+ * @param {Array<string>} keys The desired keys.
+ * @return {!goog.Uri.QueryData} a reference to this object.
+ */
+goog.Uri.QueryData.prototype.filterKeys = function(keys) {
+  this.ensureKeyMapInitialized_();
+  this.keyMap_.forEach(function(value, key) {
+    if (!goog.array.contains(keys, key)) {
+      this.remove(key);
+    }
+  }, this);
+  return this;
+};
+
+
+/**
+ * Clone the query data instance.
+ * @return {!goog.Uri.QueryData} New instance of the QueryData object.
+ */
+goog.Uri.QueryData.prototype.clone = function() {
+  var rv = new goog.Uri.QueryData();
+  rv.encodedQuery_ = this.encodedQuery_;
+  if (this.keyMap_) {
+    rv.keyMap_ = this.keyMap_.clone();
+    rv.count_ = this.count_;
+  }
+  return rv;
+};
+
+
+/**
+ * Helper function to get the key name from a JavaScript object. Converts
+ * the object to a string, and to lower case if necessary.
+ * @private
+ * @param {*} arg The object to get a key name from.
+ * @return {string} valid key name which can be looked up in #keyMap_.
+ */
+goog.Uri.QueryData.prototype.getKeyName_ = function(arg) {
+  var keyName = String(arg);
+  if (this.ignoreCase_) {
+    keyName = keyName.toLowerCase();
+  }
+  return keyName;
+};
+
+
+/**
+ * Ignore case in parameter names.
+ * NOTE: If there are already key/value pairs in the QueryData, and
+ * ignoreCase_ is set to false, the keys will all be lower-cased.
+ * @param {boolean} ignoreCase whether this goog.Uri should ignore case.
+ */
+goog.Uri.QueryData.prototype.setIgnoreCase = function(ignoreCase) {
+  var resetKeys = ignoreCase && !this.ignoreCase_;
+  if (resetKeys) {
+    this.ensureKeyMapInitialized_();
+    this.invalidateCache_();
+    this.keyMap_.forEach(function(value, key) {
+      var lowerCase = key.toLowerCase();
+      if (key != lowerCase) {
+        this.remove(key);
+        this.setValues(lowerCase, value);
+      }
+    }, this);
+  }
+  this.ignoreCase_ = ignoreCase;
+};
+
+
+/**
+ * Extends a query data object with another query data or map like object. This
+ * operates 'in-place', it does not create a new QueryData object.
+ *
+ * @param {...(goog.Uri.QueryData|goog.structs.Map<?, ?>|Object)} var_args
+ *     The object from which key value pairs will be copied.
+ */
+goog.Uri.QueryData.prototype.extend = function(var_args) {
+  for (var i = 0; i < arguments.length; i++) {
+    var data = arguments[i];
+    goog.structs.forEach(
+        data, function(value, key) { this.add(key, value); }, this);
+  }
+};
+
+goog.provide('ol.style.Text');
+
+
+goog.require('ol.style.Fill');
+
+
+/**
+ * @classdesc
+ * Set text style for vector features.
+ *
+ * @constructor
+ * @param {olx.style.TextOptions=} opt_options Options.
+ * @api
+ */
+ol.style.Text = function(opt_options) {
+
+  var options = opt_options || {};
+
+  /**
+   * @private
+   * @type {string|undefined}
+   */
+  this.font_ = options.font;
+
+  /**
+   * @private
+   * @type {number|undefined}
+   */
+  this.rotation_ = options.rotation;
+
+  /**
+   * @private
+   * @type {number|undefined}
+   */
+  this.scale_ = options.scale;
+
+  /**
+   * @private
+   * @type {string|undefined}
+   */
+  this.text_ = options.text;
+
+  /**
+   * @private
+   * @type {string|undefined}
+   */
+  this.textAlign_ = options.textAlign;
+
+  /**
+   * @private
+   * @type {string|undefined}
+   */
+  this.textBaseline_ = options.textBaseline;
+
+  /**
+   * @private
+   * @type {ol.style.Fill}
+   */
+  this.fill_ = options.fill !== undefined ? options.fill :
+      new ol.style.Fill({color: ol.style.Text.DEFAULT_FILL_COLOR_});
+
+  /**
+   * @private
+   * @type {ol.style.Stroke}
+   */
+  this.stroke_ = options.stroke !== undefined ? options.stroke : null;
+
+  /**
+   * @private
+   * @type {number}
+   */
+  this.offsetX_ = options.offsetX !== undefined ? options.offsetX : 0;
+
+  /**
+   * @private
+   * @type {number}
+   */
+  this.offsetY_ = options.offsetY !== undefined ? options.offsetY : 0;
+};
+
+
+/**
+ * The default fill color to use if no fill was set at construction time; a
+ * blackish `#333`.
+ *
+ * @const {string}
+ * @private
+ */
+ol.style.Text.DEFAULT_FILL_COLOR_ = '#333';
+
+
+/**
+ * Get the font name.
+ * @return {string|undefined} Font.
+ * @api
+ */
+ol.style.Text.prototype.getFont = function() {
+  return this.font_;
+};
+
+
+/**
+ * Get the x-offset for the text.
+ * @return {number} Horizontal text offset.
+ * @api
+ */
+ol.style.Text.prototype.getOffsetX = function() {
+  return this.offsetX_;
+};
+
+
+/**
+ * Get the y-offset for the text.
+ * @return {number} Vertical text offset.
+ * @api
+ */
+ol.style.Text.prototype.getOffsetY = function() {
+  return this.offsetY_;
+};
+
+
+/**
+ * Get the fill style for the text.
+ * @return {ol.style.Fill} Fill style.
+ * @api
+ */
+ol.style.Text.prototype.getFill = function() {
+  return this.fill_;
+};
+
+
+/**
+ * Get the text rotation.
+ * @return {number|undefined} Rotation.
+ * @api
+ */
+ol.style.Text.prototype.getRotation = function() {
+  return this.rotation_;
+};
+
+
+/**
+ * Get the text scale.
+ * @return {number|undefined} Scale.
+ * @api
+ */
+ol.style.Text.prototype.getScale = function() {
+  return this.scale_;
+};
+
+
+/**
+ * Get the stroke style for the text.
+ * @return {ol.style.Stroke} Stroke style.
+ * @api
+ */
+ol.style.Text.prototype.getStroke = function() {
+  return this.stroke_;
+};
+
+
+/**
+ * Get the text to be rendered.
+ * @return {string|undefined} Text.
+ * @api
+ */
+ol.style.Text.prototype.getText = function() {
+  return this.text_;
+};
+
+
+/**
+ * Get the text alignment.
+ * @return {string|undefined} Text align.
+ * @api
+ */
+ol.style.Text.prototype.getTextAlign = function() {
+  return this.textAlign_;
+};
+
+
+/**
+ * Get the text baseline.
+ * @return {string|undefined} Text baseline.
+ * @api
+ */
+ol.style.Text.prototype.getTextBaseline = function() {
+  return this.textBaseline_;
+};
+
+
+/**
+ * Set the font.
+ *
+ * @param {string|undefined} font Font.
+ * @api
+ */
+ol.style.Text.prototype.setFont = function(font) {
+  this.font_ = font;
+};
+
+
+/**
+ * Set the x offset.
+ *
+ * @param {number} offsetX Horizontal text offset.
+ * @api
+ */
+ol.style.Text.prototype.setOffsetX = function(offsetX) {
+  this.offsetX_ = offsetX;
+};
+
+
+/**
+ * Set the y offset.
+ *
+ * @param {number} offsetY Vertical text offset.
+ * @api
+ */
+ol.style.Text.prototype.setOffsetY = function(offsetY) {
+  this.offsetY_ = offsetY;
+};
+
+
+/**
+ * Set the fill.
+ *
+ * @param {ol.style.Fill} fill Fill style.
+ * @api
+ */
+ol.style.Text.prototype.setFill = function(fill) {
+  this.fill_ = fill;
+};
+
+
+/**
+ * Set the rotation.
+ *
+ * @param {number|undefined} rotation Rotation.
+ * @api
+ */
+ol.style.Text.prototype.setRotation = function(rotation) {
+  this.rotation_ = rotation;
+};
+
+
+/**
+ * Set the scale.
+ *
+ * @param {number|undefined} scale Scale.
+ * @api
+ */
+ol.style.Text.prototype.setScale = function(scale) {
+  this.scale_ = scale;
+};
+
+
+/**
+ * Set the stroke.
+ *
+ * @param {ol.style.Stroke} stroke Stroke style.
+ * @api
+ */
+ol.style.Text.prototype.setStroke = function(stroke) {
+  this.stroke_ = stroke;
+};
+
+
+/**
+ * Set the text.
+ *
+ * @param {string|undefined} text Text.
+ * @api
+ */
+ol.style.Text.prototype.setText = function(text) {
+  this.text_ = text;
+};
+
+
+/**
+ * Set the text alignment.
+ *
+ * @param {string|undefined} textAlign Text align.
+ * @api
+ */
+ol.style.Text.prototype.setTextAlign = function(textAlign) {
+  this.textAlign_ = textAlign;
+};
+
+
+/**
+ * Set the text baseline.
+ *
+ * @param {string|undefined} textBaseline Text baseline.
+ * @api
+ */
+ol.style.Text.prototype.setTextBaseline = function(textBaseline) {
+  this.textBaseline_ = textBaseline;
+};
+
+// FIXME http://earth.google.com/kml/1.0 namespace?
+// FIXME why does node.getAttribute return an unknown type?
+// FIXME serialize arbitrary feature properties
+// FIXME don't parse style if extractStyles is false
+
+goog.provide('ol.format.KML');
+
+goog.require('goog.Uri');
+goog.require('goog.asserts');
+goog.require('goog.object');
+goog.require('ol');
+goog.require('ol.Feature');
+goog.require('ol.array');
+goog.require('ol.color');
+goog.require('ol.format.Feature');
+goog.require('ol.format.XMLFeature');
+goog.require('ol.format.XSD');
+goog.require('ol.geom.Geometry');
+goog.require('ol.geom.GeometryCollection');
+goog.require('ol.geom.GeometryLayout');
+goog.require('ol.geom.GeometryType');
+goog.require('ol.geom.LineString');
+goog.require('ol.geom.LinearRing');
+goog.require('ol.geom.MultiLineString');
+goog.require('ol.geom.MultiPoint');
+goog.require('ol.geom.MultiPolygon');
+goog.require('ol.geom.Point');
+goog.require('ol.geom.Polygon');
+goog.require('ol.math');
+goog.require('ol.object');
+goog.require('ol.proj');
+goog.require('ol.style.Fill');
+goog.require('ol.style.Icon');
+goog.require('ol.style.IconAnchorUnits');
+goog.require('ol.style.IconOrigin');
+goog.require('ol.style.Image');
+goog.require('ol.style.Stroke');
+goog.require('ol.style.Style');
+goog.require('ol.style.Text');
+goog.require('ol.xml');
+
+
+/**
+ * @classdesc
+ * Feature format for reading and writing data in the KML format.
+ *
+ * @constructor
+ * @extends {ol.format.XMLFeature}
+ * @param {olx.format.KMLOptions=} opt_options Options.
+ * @api stable
+ */
+ol.format.KML = function(opt_options) {
+
+  var options = opt_options ? opt_options : {};
+
+  ol.format.XMLFeature.call(this);
+
+  /**
+   * @inheritDoc
+   */
+  this.defaultDataProjection = ol.proj.get('EPSG:4326');
+
+  /**
+   * @private
+   * @type {Array.<ol.style.Style>}
+   */
+  this.defaultStyle_ = options.defaultStyle ?
+      options.defaultStyle : ol.format.KML.DEFAULT_STYLE_ARRAY_;
+
+  /**
+   * @private
+   * @type {boolean}
+   */
+  this.extractStyles_ = options.extractStyles !== undefined ?
+      options.extractStyles : true;
+
+  /**
+   * @private
+   * @type {boolean}
+   */
+  this.writeStyles_ = options.writeStyles !== undefined ?
+      options.writeStyles : true;
+
+  /**
+   * @private
+   * @type {Object.<string, (Array.<ol.style.Style>|string)>}
+   */
+  this.sharedStyles_ = {};
+
+  /**
+   * @private
+   * @type {boolean}
+   */
+  this.showPointNames_ = options.showPointNames !== undefined ?
+      options.showPointNames : true;
+
+};
+ol.inherits(ol.format.KML, ol.format.XMLFeature);
+
+
+/**
+ * @const
+ * @type {Array.<string>}
+ * @private
+ */
+ol.format.KML.EXTENSIONS_ = ['.kml'];
+
+
+/**
+ * @const
+ * @type {Array.<string>}
+ * @private
+ */
+ol.format.KML.GX_NAMESPACE_URIS_ = [
+  'http://www.google.com/kml/ext/2.2'
+];
+
+
+/**
+ * @const
+ * @type {Array.<string>}
+ * @private
+ */
+ol.format.KML.NAMESPACE_URIS_ = [
+  null,
+  'http://earth.google.com/kml/2.0',
+  'http://earth.google.com/kml/2.1',
+  'http://earth.google.com/kml/2.2',
+  'http://www.opengis.net/kml/2.2'
+];
+
+
+/**
+ * @const
+ * @type {string}
+ * @private
+ */
+ol.format.KML.SCHEMA_LOCATION_ = 'http://www.opengis.net/kml/2.2 ' +
+    'https://developers.google.com/kml/schema/kml22gx.xsd';
+
+
+/**
+ * @const
+ * @type {ol.Color}
+ * @private
+ */
+ol.format.KML.DEFAULT_COLOR_ = [255, 255, 255, 1];
+
+
+/**
+ * @const
+ * @type {ol.style.Fill}
+ * @private
+ */
+ol.format.KML.DEFAULT_FILL_STYLE_ = new ol.style.Fill({
+  color: ol.format.KML.DEFAULT_COLOR_
+});
+
+
+/**
+ * @const
+ * @type {ol.Size}
+ * @private
+ */
+ol.format.KML.DEFAULT_IMAGE_STYLE_ANCHOR_ = [20, 2]; // FIXME maybe [8, 32] ?
+
+
+/**
+ * @const
+ * @type {ol.style.IconAnchorUnits}
+ * @private
+ */
+ol.format.KML.DEFAULT_IMAGE_STYLE_ANCHOR_X_UNITS_ =
+    ol.style.IconAnchorUnits.PIXELS;
+
+
+/**
+ * @const
+ * @type {ol.style.IconAnchorUnits}
+ * @private
+ */
+ol.format.KML.DEFAULT_IMAGE_STYLE_ANCHOR_Y_UNITS_ =
+    ol.style.IconAnchorUnits.PIXELS;
+
+
+/**
+ * @const
+ * @type {ol.Size}
+ * @private
+ */
+ol.format.KML.DEFAULT_IMAGE_STYLE_SIZE_ = [64, 64];
+
+
+/**
+ * @const
+ * @type {string}
+ * @private
+ */
+ol.format.KML.DEFAULT_IMAGE_STYLE_SRC_ =
+    'https://maps.google.com/mapfiles/kml/pushpin/ylw-pushpin.png';
+
+
+/**
+ * @const
+ * @type {number}
+ * @private
+ */
+ol.format.KML.DEFAULT_IMAGE_SCALE_MULTIPLIER_ = 0.5;
+
+
+/**
+ * @const
+ * @type {ol.style.Image}
+ * @private
+ */
+ol.format.KML.DEFAULT_IMAGE_STYLE_ = new ol.style.Icon({
+  anchor: ol.format.KML.DEFAULT_IMAGE_STYLE_ANCHOR_,
+  anchorOrigin: ol.style.IconOrigin.BOTTOM_LEFT,
+  anchorXUnits: ol.format.KML.DEFAULT_IMAGE_STYLE_ANCHOR_X_UNITS_,
+  anchorYUnits: ol.format.KML.DEFAULT_IMAGE_STYLE_ANCHOR_Y_UNITS_,
+  crossOrigin: 'anonymous',
+  rotation: 0,
+  scale: ol.format.KML.DEFAULT_IMAGE_SCALE_MULTIPLIER_,
+  size: ol.format.KML.DEFAULT_IMAGE_STYLE_SIZE_,
+  src: ol.format.KML.DEFAULT_IMAGE_STYLE_SRC_
+});
+
+
+/**
+ * @const
+ * @type {ol.style.Stroke}
+ * @private
+ */
+ol.format.KML.DEFAULT_STROKE_STYLE_ = new ol.style.Stroke({
+  color: ol.format.KML.DEFAULT_COLOR_,
+  width: 1
+});
+
+
+/**
+ * @const
+ * @type {ol.style.Stroke}
+ * @private
+ */
+ol.format.KML.DEFAULT_TEXT_STROKE_STYLE_ = new ol.style.Stroke({
+  color: [51, 51, 51, 1],
+  width: 2
+});
+
+
+/**
+ * @const
+ * @type {ol.style.Text}
+ * @private
+ */
+ol.format.KML.DEFAULT_TEXT_STYLE_ = new ol.style.Text({
+  font: 'bold 16px Helvetica',
+  fill: ol.format.KML.DEFAULT_FILL_STYLE_,
+  stroke: ol.format.KML.DEFAULT_TEXT_STROKE_STYLE_,
+  scale: 0.8
+});
+
+
+/**
+ * @const
+ * @type {ol.style.Style}
+ * @private
+ */
+ol.format.KML.DEFAULT_STYLE_ = new ol.style.Style({
+  fill: ol.format.KML.DEFAULT_FILL_STYLE_,
+  image: ol.format.KML.DEFAULT_IMAGE_STYLE_,
+  text: ol.format.KML.DEFAULT_TEXT_STYLE_,
+  stroke: ol.format.KML.DEFAULT_STROKE_STYLE_,
+  zIndex: 0
+});
+
+
+/**
+ * @const
+ * @type {Array.<ol.style.Style>}
+ * @private
+ */
+ol.format.KML.DEFAULT_STYLE_ARRAY_ = [ol.format.KML.DEFAULT_STYLE_];
+
+
+/**
+ * @const
+ * @type {Object.<string, ol.style.IconAnchorUnits>}
+ * @private
+ */
+ol.format.KML.ICON_ANCHOR_UNITS_MAP_ = {
+  'fraction': ol.style.IconAnchorUnits.FRACTION,
+  'pixels': ol.style.IconAnchorUnits.PIXELS
+};
+
+
+/**
+ * @param {ol.style.Style|undefined} foundStyle Style.
+ * @param {string} name Name.
+ * @return {ol.style.Style} style Style.
+ * @private
+ */
+ol.format.KML.createNameStyleFunction_ = function(foundStyle, name) {
+  var textStyle = null;
+  var textOffset = [0, 0];
+  var textAlign = 'start';
+  if (foundStyle.getImage()) {
+    var imageSize = foundStyle.getImage().getImageSize();
+    if (imageSize && imageSize.length == 2) {
+      // Offset the label to be centered to the right of the icon, if there is
+      // one.
+      textOffset[0] = foundStyle.getImage().getScale() * imageSize[0] / 2;
+      textOffset[1] = -foundStyle.getImage().getScale() * imageSize[1] / 2;
+      textAlign = 'left';
+    }
+  }
+  if (!ol.object.isEmpty(foundStyle.getText())) {
+    textStyle = /** @type {ol.style.Text} */
+        (goog.object.clone(foundStyle.getText()));
+    textStyle.setText(name);
+    textStyle.setTextAlign(textAlign);
+    textStyle.setOffsetX(textOffset[0]);
+    textStyle.setOffsetY(textOffset[1]);
+  } else {
+    textStyle = new ol.style.Text({
+      text: name,
+      offsetX: textOffset[0],
+      offsetY: textOffset[1],
+      textAlign: textAlign
+    });
+  }
+  var nameStyle = new ol.style.Style({
+    text: textStyle
+  });
+  return nameStyle;
+};
+
+
+/**
+ * @param {Array.<ol.style.Style>|undefined} style Style.
+ * @param {string} styleUrl Style URL.
+ * @param {Array.<ol.style.Style>} defaultStyle Default style.
+ * @param {Object.<string, (Array.<ol.style.Style>|string)>} sharedStyles Shared
+ *          styles.
+ * @param {boolean|undefined} showPointNames true to show names for point
+ *          placemarks.
+ * @return {ol.FeatureStyleFunction} Feature style function.
+ * @private
+ */
+ol.format.KML.createFeatureStyleFunction_ = function(style, styleUrl,
+    defaultStyle, sharedStyles, showPointNames) {
+
+  return (
+      /**
+       * @param {number} resolution Resolution.
+       * @return {Array.<ol.style.Style>} Style.
+       * @this {ol.Feature}
+       */
+      function(resolution) {
+        var drawName = showPointNames;
+        /** @type {ol.style.Style|undefined} */
+        var nameStyle;
+        var name = '';
+        if (drawName) {
+          if (this.getGeometry()) {
+            drawName = (this.getGeometry().getType() ===
+                        ol.geom.GeometryType.POINT);
+          }
+        }
+
+        if (drawName) {
+          name = /** @type {string} */ (this.get('name'));
+          drawName = drawName && name;
+        }
+
+        if (style) {
+          if (drawName) {
+            nameStyle = ol.format.KML.createNameStyleFunction_(style[0],
+                name);
+            return style.concat(nameStyle);
+          }
+          return style;
+        }
+        if (styleUrl) {
+          var foundStyle = ol.format.KML.findStyle_(styleUrl, defaultStyle,
+              sharedStyles);
+          if (drawName) {
+            nameStyle = ol.format.KML.createNameStyleFunction_(foundStyle[0],
+                name);
+            return foundStyle.concat(nameStyle);
+          }
+          return foundStyle;
+        }
+        if (drawName) {
+          nameStyle = ol.format.KML.createNameStyleFunction_(defaultStyle[0],
+              name);
+          return defaultStyle.concat(nameStyle);
+        }
+        return defaultStyle;
+      });
+};
+
+
+/**
+ * @param {Array.<ol.style.Style>|string|undefined} styleValue Style value.
+ * @param {Array.<ol.style.Style>} defaultStyle Default style.
+ * @param {Object.<string, (Array.<ol.style.Style>|string)>} sharedStyles
+ * Shared styles.
+ * @return {Array.<ol.style.Style>} Style.
+ * @private
+ */
+ol.format.KML.findStyle_ = function(styleValue, defaultStyle, sharedStyles) {
+  if (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 ol.format.KML.findStyle_(
+        sharedStyles[styleValue], defaultStyle, sharedStyles);
+  } else {
+    return defaultStyle;
+  }
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @private
+ * @return {ol.Color|undefined} Color.
+ */
+ol.format.KML.readColor_ = function(node) {
+  var s = ol.xml.getAllTextContent(node, false);
+  // The KML specification states that colors should not include a leading `#`
+  // but we tolerate them.
+  var m = /^\s*#?\s*([0-9A-Fa-f]{8})\s*$/.exec(s);
+  if (m) {
+    var hexColor = m[1];
+    return [
+      parseInt(hexColor.substr(6, 2), 16),
+      parseInt(hexColor.substr(4, 2), 16),
+      parseInt(hexColor.substr(2, 2), 16),
+      parseInt(hexColor.substr(0, 2), 16) / 255
+    ];
+
+  } else {
+    return undefined;
+  }
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @private
+ * @return {Array.<number>|undefined} Flat coordinates.
+ */
+ol.format.KML.readFlatCoordinates_ = function(node) {
+  var s = ol.xml.getAllTextContent(node, false);
+  var flatCoordinates = [];
+  // The KML specification states that coordinate tuples should not include
+  // spaces, but we tolerate them.
+  var re =
+      /^\s*([+\-]?\d*\.?\d+(?:e[+\-]?\d+)?)\s*,\s*([+\-]?\d*\.?\d+(?:e[+\-]?\d+)?)(?:\s*,\s*([+\-]?\d*\.?\d+(?:e[+\-]?\d+)?))?\s*/i;
+  var m;
+  while ((m = re.exec(s))) {
+    var x = parseFloat(m[1]);
+    var y = parseFloat(m[2]);
+    var z = m[3] ? parseFloat(m[3]) : 0;
+    flatCoordinates.push(x, y, z);
+    s = s.substr(m[0].length);
+  }
+  if (s !== '') {
+    return undefined;
+  }
+  return flatCoordinates;
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @private
+ * @return {string|undefined} Style URL.
+ */
+ol.format.KML.readStyleUrl_ = function(node) {
+  var s = ol.xml.getAllTextContent(node, false).trim();
+  if (node.baseURI) {
+    return goog.Uri.resolve(node.baseURI, s).toString();
+  } else {
+    return s;
+  }
+
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @private
+ * @return {string} URI.
+ */
+ol.format.KML.readURI_ = function(node) {
+  var s = ol.xml.getAllTextContent(node, false);
+  if (node.baseURI) {
+    return goog.Uri.resolve(node.baseURI, s.trim()).toString();
+  } else {
+    return s.trim();
+  }
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @private
+ * @return {ol.KMLVec2_} Vec2.
+ */
+ol.format.KML.readVec2_ = function(node) {
+  var xunits = node.getAttribute('xunits');
+  var yunits = node.getAttribute('yunits');
+  return {
+    x: parseFloat(node.getAttribute('x')),
+    xunits: ol.format.KML.ICON_ANCHOR_UNITS_MAP_[xunits],
+    y: parseFloat(node.getAttribute('y')),
+    yunits: ol.format.KML.ICON_ANCHOR_UNITS_MAP_[yunits]
+  };
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @private
+ * @return {number|undefined} Scale.
+ */
+ol.format.KML.readScale_ = function(node) {
+  var number = ol.format.XSD.readDecimal(node);
+  if (number !== undefined) {
+    return Math.sqrt(number);
+  } else {
+    return undefined;
+  }
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {Array.<*>} objectStack Object stack.
+ * @private
+ * @return {Array.<ol.style.Style>|string|undefined} StyleMap.
+ */
+ol.format.KML.readStyleMapValue_ = function(node, objectStack) {
+  return ol.xml.pushParseAndPop(undefined,
+      ol.format.KML.STYLE_MAP_PARSERS_, node, objectStack);
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {Array.<*>} objectStack Object stack.
+ * @private
+ */
+ol.format.KML.IconStyleParser_ = function(node, objectStack) {
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'node.nodeType should be an ELEMENT');
+  goog.asserts.assert(node.localName == 'IconStyle',
+      'localName should be IconStyle');
+  // FIXME refreshMode
+  // FIXME refreshInterval
+  // FIXME viewRefreshTime
+  // FIXME viewBoundScale
+  // FIXME viewFormat
+  // FIXME httpQuery
+  var object = ol.xml.pushParseAndPop(
+      {}, ol.format.KML.ICON_STYLE_PARSERS_, node, objectStack);
+  if (!object) {
+    return;
+  }
+  var styleObject = /** @type {Object} */ (objectStack[objectStack.length - 1]);
+  goog.asserts.assert(goog.isObject(styleObject),
+      'styleObject should be an Object');
+  var IconObject = 'Icon' in object ? object['Icon'] : {};
+  var src;
+  var href = /** @type {string|undefined} */
+      (IconObject['href']);
+  if (href) {
+    src = href;
+  } else {
+    src = ol.format.KML.DEFAULT_IMAGE_STYLE_SRC_;
+  }
+  var anchor, anchorXUnits, anchorYUnits;
+  var hotSpot = /** @type {ol.KMLVec2_|undefined} */
+      (object['hotSpot']);
+  if (hotSpot) {
+    anchor = [hotSpot.x, hotSpot.y];
+    anchorXUnits = hotSpot.xunits;
+    anchorYUnits = hotSpot.yunits;
+  } else if (src === ol.format.KML.DEFAULT_IMAGE_STYLE_SRC_) {
+    anchor = ol.format.KML.DEFAULT_IMAGE_STYLE_ANCHOR_;
+    anchorXUnits = ol.format.KML.DEFAULT_IMAGE_STYLE_ANCHOR_X_UNITS_;
+    anchorYUnits = ol.format.KML.DEFAULT_IMAGE_STYLE_ANCHOR_Y_UNITS_;
+  } else if (/^http:\/\/maps\.(?:google|gstatic)\.com\//.test(src)) {
+    anchor = [0.5, 0];
+    anchorXUnits = ol.style.IconAnchorUnits.FRACTION;
+    anchorYUnits = ol.style.IconAnchorUnits.FRACTION;
+  }
+
+  var offset;
+  var x = /** @type {number|undefined} */
+      (IconObject['x']);
+  var y = /** @type {number|undefined} */
+      (IconObject['y']);
+  if (x !== undefined && y !== undefined) {
+    offset = [x, y];
+  }
+
+  var size;
+  var w = /** @type {number|undefined} */
+      (IconObject['w']);
+  var h = /** @type {number|undefined} */
+      (IconObject['h']);
+  if (w !== undefined && h !== undefined) {
+    size = [w, h];
+  }
+
+  var rotation;
+  var heading = /** @type {number} */
+      (object['heading']);
+  if (heading !== undefined) {
+    rotation = ol.math.toRadians(heading);
+  }
+
+  var scale = /** @type {number|undefined} */
+      (object['scale']);
+  if (src == ol.format.KML.DEFAULT_IMAGE_STYLE_SRC_) {
+    size = ol.format.KML.DEFAULT_IMAGE_STYLE_SIZE_;
+    if (scale === undefined) {
+      scale = ol.format.KML.DEFAULT_IMAGE_SCALE_MULTIPLIER_;
+    }
+  }
+
+  var imageStyle = new ol.style.Icon({
+    anchor: anchor,
+    anchorOrigin: ol.style.IconOrigin.BOTTOM_LEFT,
+    anchorXUnits: anchorXUnits,
+    anchorYUnits: anchorYUnits,
+    crossOrigin: 'anonymous', // FIXME should this be configurable?
+    offset: offset,
+    offsetOrigin: ol.style.IconOrigin.BOTTOM_LEFT,
+    rotation: rotation,
+    scale: scale,
+    size: size,
+    src: src
+  });
+  styleObject['imageStyle'] = imageStyle;
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {Array.<*>} objectStack Object stack.
+ * @private
+ */
+ol.format.KML.LabelStyleParser_ = function(node, objectStack) {
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'node.nodeType should be ELEMENT');
+  goog.asserts.assert(node.localName == 'LabelStyle',
+      'localName should be LabelStyle');
+  // FIXME colorMode
+  var object = ol.xml.pushParseAndPop(
+      {}, ol.format.KML.LABEL_STYLE_PARSERS_, node, objectStack);
+  if (!object) {
+    return;
+  }
+  var styleObject = objectStack[objectStack.length - 1];
+  goog.asserts.assert(goog.isObject(styleObject),
+      'styleObject should be an Object');
+  var textStyle = new ol.style.Text({
+    fill: new ol.style.Fill({
+      color: /** @type {ol.Color} */
+          ('color' in object ? object['color'] : ol.format.KML.DEFAULT_COLOR_)
+    }),
+    scale: /** @type {number|undefined} */
+        (object['scale'])
+  });
+  styleObject['textStyle'] = textStyle;
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {Array.<*>} objectStack Object stack.
+ * @private
+ */
+ol.format.KML.LineStyleParser_ = function(node, objectStack) {
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'node.nodeType should be ELEMENT');
+  goog.asserts.assert(node.localName == 'LineStyle',
+      'localName should be LineStyle');
+  // FIXME colorMode
+  // FIXME gx:outerColor
+  // FIXME gx:outerWidth
+  // FIXME gx:physicalWidth
+  // FIXME gx:labelVisibility
+  var object = ol.xml.pushParseAndPop(
+      {}, ol.format.KML.LINE_STYLE_PARSERS_, node, objectStack);
+  if (!object) {
+    return;
+  }
+  var styleObject = objectStack[objectStack.length - 1];
+  goog.asserts.assert(goog.isObject(styleObject),
+      'styleObject should be an Object');
+  var strokeStyle = new ol.style.Stroke({
+    color: /** @type {ol.Color} */
+        ('color' in object ? object['color'] : ol.format.KML.DEFAULT_COLOR_),
+    width: /** @type {number} */ ('width' in object ? object['width'] : 1)
+  });
+  styleObject['strokeStyle'] = strokeStyle;
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {Array.<*>} objectStack Object stack.
+ * @private
+ */
+ol.format.KML.PolyStyleParser_ = function(node, objectStack) {
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'node.nodeType should be ELEMENT');
+  goog.asserts.assert(node.localName == 'PolyStyle',
+      'localName should be PolyStyle');
+  // FIXME colorMode
+  var object = ol.xml.pushParseAndPop(
+      {}, ol.format.KML.POLY_STYLE_PARSERS_, node, objectStack);
+  if (!object) {
+    return;
+  }
+  var styleObject = objectStack[objectStack.length - 1];
+  goog.asserts.assert(goog.isObject(styleObject),
+      'styleObject should be an Object');
+  var fillStyle = new ol.style.Fill({
+    color: /** @type {ol.Color} */
+        ('color' in object ? object['color'] : ol.format.KML.DEFAULT_COLOR_)
+  });
+  styleObject['fillStyle'] = fillStyle;
+  var fill = /** @type {boolean|undefined} */ (object['fill']);
+  if (fill !== undefined) {
+    styleObject['fill'] = fill;
+  }
+  var outline =
+      /** @type {boolean|undefined} */ (object['outline']);
+  if (outline !== undefined) {
+    styleObject['outline'] = outline;
+  }
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {Array.<*>} objectStack Object stack.
+ * @private
+ * @return {Array.<number>} LinearRing flat coordinates.
+ */
+ol.format.KML.readFlatLinearRing_ = function(node, objectStack) {
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'node.nodeType should be ELEMENT');
+  goog.asserts.assert(node.localName == 'LinearRing',
+      'localName should be LinearRing');
+  return ol.xml.pushParseAndPop(null,
+      ol.format.KML.FLAT_LINEAR_RING_PARSERS_, node, objectStack);
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {Array.<*>} objectStack Object stack.
+ * @private
+ */
+ol.format.KML.gxCoordParser_ = function(node, objectStack) {
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'node.nodeType should be ELEMENT');
+  goog.asserts.assert(ol.array.includes(
+      ol.format.KML.GX_NAMESPACE_URIS_, node.namespaceURI),
+      'namespaceURI of the node should be known to the KML parser');
+  goog.asserts.assert(node.localName == 'coord', 'localName should be coord');
+  var gxTrackObject = /** @type {ol.KMLGxTrackObject_} */
+      (objectStack[objectStack.length - 1]);
+  goog.asserts.assert(goog.isObject(gxTrackObject),
+      'gxTrackObject should be an Object');
+  var flatCoordinates = gxTrackObject.flatCoordinates;
+  var s = ol.xml.getAllTextContent(node, false);
+  var re =
+      /^\s*([+\-]?\d+(?:\.\d*)?(?:e[+\-]?\d*)?)\s+([+\-]?\d+(?:\.\d*)?(?:e[+\-]?\d*)?)\s+([+\-]?\d+(?:\.\d*)?(?:e[+\-]?\d*)?)\s*$/i;
+  var m = re.exec(s);
+  if (m) {
+    var x = parseFloat(m[1]);
+    var y = parseFloat(m[2]);
+    var z = parseFloat(m[3]);
+    flatCoordinates.push(x, y, z, 0);
+  } else {
+    flatCoordinates.push(0, 0, 0, 0);
+  }
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {Array.<*>} objectStack Object stack.
+ * @private
+ * @return {ol.geom.MultiLineString|undefined} MultiLineString.
+ */
+ol.format.KML.readGxMultiTrack_ = function(node, objectStack) {
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'node.nodeType should be ELEMENT');
+  goog.asserts.assert(ol.array.includes(
+      ol.format.KML.GX_NAMESPACE_URIS_, node.namespaceURI),
+      'namespaceURI of the node should be known to the KML parser');
+  goog.asserts.assert(node.localName == 'MultiTrack',
+      'localName should be MultiTrack');
+  var lineStrings = ol.xml.pushParseAndPop([],
+      ol.format.KML.GX_MULTITRACK_GEOMETRY_PARSERS_, node, objectStack);
+  if (!lineStrings) {
+    return undefined;
+  }
+  var multiLineString = new ol.geom.MultiLineString(null);
+  multiLineString.setLineStrings(lineStrings);
+  return multiLineString;
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {Array.<*>} objectStack Object stack.
+ * @private
+ * @return {ol.geom.LineString|undefined} LineString.
+ */
+ol.format.KML.readGxTrack_ = function(node, objectStack) {
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'node.nodeType should be ELEMENT');
+  goog.asserts.assert(ol.array.includes(
+      ol.format.KML.GX_NAMESPACE_URIS_, node.namespaceURI),
+      'namespaceURI of the node should be known to the KML parser');
+  goog.asserts.assert(node.localName == 'Track', 'localName should be Track');
+  var gxTrackObject = ol.xml.pushParseAndPop(
+      /** @type {ol.KMLGxTrackObject_} */ ({
+        flatCoordinates: [],
+        whens: []
+      }), ol.format.KML.GX_TRACK_PARSERS_, node, objectStack);
+  if (!gxTrackObject) {
+    return undefined;
+  }
+  var flatCoordinates = gxTrackObject.flatCoordinates;
+  var whens = gxTrackObject.whens;
+  goog.asserts.assert(flatCoordinates.length / 4 == whens.length,
+      'the length of the flatCoordinates array divided by 4 should be the ' +
+      'length of the whens array');
+  var i, ii;
+  for (i = 0, ii = Math.min(flatCoordinates.length, whens.length); i < ii;
+       ++i) {
+    flatCoordinates[4 * i + 3] = whens[i];
+  }
+  var lineString = new ol.geom.LineString(null);
+  lineString.setFlatCoordinates(ol.geom.GeometryLayout.XYZM, flatCoordinates);
+  return lineString;
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {Array.<*>} objectStack Object stack.
+ * @private
+ * @return {Object} Icon object.
+ */
+ol.format.KML.readIcon_ = function(node, objectStack) {
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'node.nodeType should be ELEMENT');
+  goog.asserts.assert(node.localName == 'Icon', 'localName should be Icon');
+  var iconObject = ol.xml.pushParseAndPop(
+      {}, ol.format.KML.ICON_PARSERS_, node, objectStack);
+  if (iconObject) {
+    return iconObject;
+  } else {
+    return null;
+  }
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {Array.<*>} objectStack Object stack.
+ * @private
+ * @return {Array.<number>} Flat coordinates.
+ */
+ol.format.KML.readFlatCoordinatesFromNode_ = function(node, objectStack) {
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'node.nodeType should be ELEMENT');
+  return ol.xml.pushParseAndPop(null,
+      ol.format.KML.GEOMETRY_FLAT_COORDINATES_PARSERS_, node, objectStack);
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {Array.<*>} objectStack Object stack.
+ * @private
+ * @return {ol.geom.LineString|undefined} LineString.
+ */
+ol.format.KML.readLineString_ = function(node, objectStack) {
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'node.nodeType should be ELEMENT');
+  goog.asserts.assert(node.localName == 'LineString',
+      'localName should be LineString');
+  var properties = ol.xml.pushParseAndPop({},
+      ol.format.KML.EXTRUDE_AND_ALTITUDE_MODE_PARSERS_, node,
+      objectStack);
+  var flatCoordinates =
+      ol.format.KML.readFlatCoordinatesFromNode_(node, objectStack);
+  if (flatCoordinates) {
+    var lineString = new ol.geom.LineString(null);
+    lineString.setFlatCoordinates(ol.geom.GeometryLayout.XYZ, flatCoordinates);
+    lineString.setProperties(properties);
+    return lineString;
+  } else {
+    return undefined;
+  }
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {Array.<*>} objectStack Object stack.
+ * @private
+ * @return {ol.geom.Polygon|undefined} Polygon.
+ */
+ol.format.KML.readLinearRing_ = function(node, objectStack) {
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'node.nodeType should be ELEMENT');
+  goog.asserts.assert(node.localName == 'LinearRing',
+      'localName should be LinearRing');
+  var properties = ol.xml.pushParseAndPop({},
+      ol.format.KML.EXTRUDE_AND_ALTITUDE_MODE_PARSERS_, node,
+      objectStack);
+  var flatCoordinates =
+      ol.format.KML.readFlatCoordinatesFromNode_(node, objectStack);
+  if (flatCoordinates) {
+    var polygon = new ol.geom.Polygon(null);
+    polygon.setFlatCoordinates(ol.geom.GeometryLayout.XYZ, flatCoordinates,
+        [flatCoordinates.length]);
+    polygon.setProperties(properties);
+    return polygon;
+  } else {
+    return undefined;
+  }
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {Array.<*>} objectStack Object stack.
+ * @private
+ * @return {ol.geom.Geometry} Geometry.
+ */
+ol.format.KML.readMultiGeometry_ = function(node, objectStack) {
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'node.nodeType should be ELEMENT');
+  goog.asserts.assert(node.localName == 'MultiGeometry',
+      'localName should be MultiGeometry');
+  var geometries = ol.xml.pushParseAndPop([],
+      ol.format.KML.MULTI_GEOMETRY_PARSERS_, node, objectStack);
+  if (!geometries) {
+    return null;
+  }
+  if (geometries.length === 0) {
+    return new ol.geom.GeometryCollection(geometries);
+  }
+  var homogeneous = true;
+  var type = geometries[0].getType();
+  var geometry, i, ii;
+  for (i = 1, ii = geometries.length; i < ii; ++i) {
+    geometry = geometries[i];
+    if (geometry.getType() != type) {
+      homogeneous = false;
+      break;
+    }
+  }
+  if (homogeneous) {
+    var layout;
+    var flatCoordinates;
+    if (type == ol.geom.GeometryType.POINT) {
+      var point = geometries[0];
+      goog.asserts.assertInstanceof(point, ol.geom.Point,
+          'point should be an ol.geom.Point');
+      layout = point.getLayout();
+      flatCoordinates = point.getFlatCoordinates();
+      for (i = 1, ii = geometries.length; i < ii; ++i) {
+        geometry = geometries[i];
+        goog.asserts.assertInstanceof(geometry, ol.geom.Point,
+            'geometry should be an ol.geom.Point');
+        goog.asserts.assert(geometry.getLayout() == layout,
+            'geometry layout should be consistent');
+        ol.array.extend(flatCoordinates, geometry.getFlatCoordinates());
+      }
+      var multiPoint = new ol.geom.MultiPoint(null);
+      multiPoint.setFlatCoordinates(layout, flatCoordinates);
+      ol.format.KML.setCommonGeometryProperties_(multiPoint, geometries);
+      return multiPoint;
+    } else if (type == ol.geom.GeometryType.LINE_STRING) {
+      var multiLineString = new ol.geom.MultiLineString(null);
+      multiLineString.setLineStrings(geometries);
+      ol.format.KML.setCommonGeometryProperties_(multiLineString, geometries);
+      return multiLineString;
+    } else if (type == ol.geom.GeometryType.POLYGON) {
+      var multiPolygon = new ol.geom.MultiPolygon(null);
+      multiPolygon.setPolygons(geometries);
+      ol.format.KML.setCommonGeometryProperties_(multiPolygon, geometries);
+      return multiPolygon;
+    } else if (type == ol.geom.GeometryType.GEOMETRY_COLLECTION) {
+      return new ol.geom.GeometryCollection(geometries);
+    } else {
+      goog.asserts.fail('Unexpected type: ' + type);
+      return null;
+    }
+  } else {
+    return new ol.geom.GeometryCollection(geometries);
+  }
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {Array.<*>} objectStack Object stack.
+ * @private
+ * @return {ol.geom.Point|undefined} Point.
+ */
+ol.format.KML.readPoint_ = function(node, objectStack) {
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'node.nodeType should be ELEMENT');
+  goog.asserts.assert(node.localName == 'Point', 'localName should be Point');
+  var properties = ol.xml.pushParseAndPop({},
+      ol.format.KML.EXTRUDE_AND_ALTITUDE_MODE_PARSERS_, node,
+      objectStack);
+  var flatCoordinates =
+      ol.format.KML.readFlatCoordinatesFromNode_(node, objectStack);
+  if (flatCoordinates) {
+    var point = new ol.geom.Point(null);
+    goog.asserts.assert(flatCoordinates.length == 3,
+        'flatCoordinates should have a length of 3');
+    point.setFlatCoordinates(ol.geom.GeometryLayout.XYZ, flatCoordinates);
+    point.setProperties(properties);
+    return point;
+  } else {
+    return undefined;
+  }
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {Array.<*>} objectStack Object stack.
+ * @private
+ * @return {ol.geom.Polygon|undefined} Polygon.
+ */
+ol.format.KML.readPolygon_ = function(node, objectStack) {
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'node.nodeType should be ELEMENT');
+  goog.asserts.assert(node.localName == 'Polygon',
+      'localName should be Polygon');
+  var properties = ol.xml.pushParseAndPop(/** @type {Object<string,*>} */ ({}),
+      ol.format.KML.EXTRUDE_AND_ALTITUDE_MODE_PARSERS_, node,
+      objectStack);
+  var flatLinearRings = ol.xml.pushParseAndPop([null],
+      ol.format.KML.FLAT_LINEAR_RINGS_PARSERS_, node, objectStack);
+  if (flatLinearRings && flatLinearRings[0]) {
+    var polygon = new ol.geom.Polygon(null);
+    var flatCoordinates = flatLinearRings[0];
+    var ends = [flatCoordinates.length];
+    var i, ii;
+    for (i = 1, ii = flatLinearRings.length; i < ii; ++i) {
+      ol.array.extend(flatCoordinates, flatLinearRings[i]);
+      ends.push(flatCoordinates.length);
+    }
+    polygon.setFlatCoordinates(
+        ol.geom.GeometryLayout.XYZ, flatCoordinates, ends);
+    polygon.setProperties(properties);
+    return polygon;
+  } else {
+    return undefined;
+  }
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {Array.<*>} objectStack Object stack.
+ * @private
+ * @return {Array.<ol.style.Style>} Style.
+ */
+ol.format.KML.readStyle_ = function(node, objectStack) {
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'node.nodeType should be ELEMENT');
+  goog.asserts.assert(node.localName == 'Style', 'localName should be Style');
+  var styleObject = ol.xml.pushParseAndPop(
+      {}, ol.format.KML.STYLE_PARSERS_, node, objectStack);
+  if (!styleObject) {
+    return null;
+  }
+  var fillStyle = /** @type {ol.style.Fill} */
+      ('fillStyle' in styleObject ?
+          styleObject['fillStyle'] : ol.format.KML.DEFAULT_FILL_STYLE_);
+  var fill = /** @type {boolean|undefined} */ (styleObject['fill']);
+  if (fill !== undefined && !fill) {
+    fillStyle = null;
+  }
+  var imageStyle = /** @type {ol.style.Image} */
+      ('imageStyle' in styleObject ?
+          styleObject['imageStyle'] : ol.format.KML.DEFAULT_IMAGE_STYLE_);
+  var textStyle = /** @type {ol.style.Text} */
+      ('textStyle' in styleObject ?
+          styleObject['textStyle'] : ol.format.KML.DEFAULT_TEXT_STYLE_);
+  var strokeStyle = /** @type {ol.style.Stroke} */
+      ('strokeStyle' in styleObject ?
+          styleObject['strokeStyle'] : ol.format.KML.DEFAULT_STROKE_STYLE_);
+  var outline = /** @type {boolean|undefined} */
+      (styleObject['outline']);
+  if (outline !== undefined && !outline) {
+    strokeStyle = null;
+  }
+  return [new ol.style.Style({
+    fill: fillStyle,
+    image: imageStyle,
+    stroke: strokeStyle,
+    text: textStyle,
+    zIndex: undefined // FIXME
+  })];
+};
+
+
+/**
+ * Reads an array of geometries and creates arrays for common geometry
+ * properties. Then sets them to the multi geometry.
+ * @param {ol.geom.MultiPoint|ol.geom.MultiLineString|ol.geom.MultiPolygon}
+ *     multiGeometry A multi-geometry.
+ * @param {Array.<ol.geom.Geometry>} geometries List of geometries.
+ * @private
+ */
+ol.format.KML.setCommonGeometryProperties_ = function(multiGeometry,
+    geometries) {
+  var ii = geometries.length;
+  var extrudes = new Array(geometries.length);
+  var altitudeModes = new Array(geometries.length);
+  var geometry, i, hasExtrude, hasAltitudeMode;
+  hasExtrude = hasAltitudeMode = false;
+  for (i = 0; i < ii; ++i) {
+    geometry = geometries[i];
+    extrudes[i] = geometry.get('extrude');
+    altitudeModes[i] = geometry.get('altitudeMode');
+    hasExtrude = hasExtrude || extrudes[i] !== undefined;
+    hasAltitudeMode = hasAltitudeMode || altitudeModes[i];
+  }
+  if (hasExtrude) {
+    multiGeometry.set('extrude', extrudes);
+  }
+  if (hasAltitudeMode) {
+    multiGeometry.set('altitudeMode', altitudeModes);
+  }
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {Array.<*>} objectStack Object stack.
+ * @private
+ */
+ol.format.KML.DataParser_ = function(node, objectStack) {
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'node.nodeType should be ELEMENT');
+  goog.asserts.assert(node.localName == 'Data', 'localName should be Data');
+  var name = node.getAttribute('name');
+  if (name !== null) {
+    var data = ol.xml.pushParseAndPop(
+        undefined, ol.format.KML.DATA_PARSERS_, node, objectStack);
+    if (data) {
+      var featureObject =
+          /** @type {Object} */ (objectStack[objectStack.length - 1]);
+      goog.asserts.assert(goog.isObject(featureObject),
+          'featureObject should be an Object');
+      featureObject[name] = data;
+    }
+  }
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {Array.<*>} objectStack Object stack.
+ * @private
+ */
+ol.format.KML.ExtendedDataParser_ = function(node, objectStack) {
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'node.nodeType should be ELEMENT');
+  goog.asserts.assert(node.localName == 'ExtendedData',
+      'localName should be ExtendedData');
+  ol.xml.parseNode(ol.format.KML.EXTENDED_DATA_PARSERS_, node, objectStack);
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {Array.<*>} objectStack Object stack.
+ * @private
+ */
+ol.format.KML.PairDataParser_ = function(node, objectStack) {
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'node.nodeType should be ELEMENT');
+  goog.asserts.assert(node.localName == 'Pair', 'localName should be Pair');
+  var pairObject = ol.xml.pushParseAndPop(
+      {}, ol.format.KML.PAIR_PARSERS_, node, objectStack);
+  if (!pairObject) {
+    return;
+  }
+  var key = /** @type {string|undefined} */
+      (pairObject['key']);
+  if (key && key == 'normal') {
+    var styleUrl = /** @type {string|undefined} */
+        (pairObject['styleUrl']);
+    if (styleUrl) {
+      objectStack[objectStack.length - 1] = styleUrl;
+    }
+    var Style = /** @type {ol.style.Style} */
+        (pairObject['Style']);
+    if (Style) {
+      objectStack[objectStack.length - 1] = Style;
+    }
+  }
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {Array.<*>} objectStack Object stack.
+ * @private
+ */
+ol.format.KML.PlacemarkStyleMapParser_ = function(node, objectStack) {
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'node.nodeType should be ELEMENT');
+  goog.asserts.assert(node.localName == 'StyleMap',
+      'localName should be StyleMap');
+  var styleMapValue = ol.format.KML.readStyleMapValue_(node, objectStack);
+  if (!styleMapValue) {
+    return;
+  }
+  var placemarkObject = objectStack[objectStack.length - 1];
+  goog.asserts.assert(goog.isObject(placemarkObject),
+      'placemarkObject should be an Object');
+  if (Array.isArray(styleMapValue)) {
+    placemarkObject['Style'] = styleMapValue;
+  } else if (typeof styleMapValue === 'string') {
+    placemarkObject['styleUrl'] = styleMapValue;
+  } else {
+    goog.asserts.fail('styleMapValue has an unknown type');
+  }
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {Array.<*>} objectStack Object stack.
+ * @private
+ */
+ol.format.KML.SchemaDataParser_ = function(node, objectStack) {
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'node.nodeType should be ELEMENT');
+  goog.asserts.assert(node.localName == 'SchemaData',
+      'localName should be SchemaData');
+  ol.xml.parseNode(ol.format.KML.SCHEMA_DATA_PARSERS_, node, objectStack);
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {Array.<*>} objectStack Object stack.
+ * @private
+ */
+ol.format.KML.SimpleDataParser_ = function(node, objectStack) {
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'node.nodeType should be ELEMENT');
+  goog.asserts.assert(node.localName == 'SimpleData',
+      'localName should be SimpleData');
+  var name = node.getAttribute('name');
+  if (name !== null) {
+    var data = ol.format.XSD.readString(node);
+    var featureObject =
+        /** @type {Object} */ (objectStack[objectStack.length - 1]);
+    featureObject[name] = data;
+  }
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {Array.<*>} objectStack Object stack.
+ * @private
+ */
+ol.format.KML.innerBoundaryIsParser_ = function(node, objectStack) {
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'node.nodeType should be ELEMENT');
+  goog.asserts.assert(node.localName == 'innerBoundaryIs',
+      'localName should be innerBoundaryIs');
+  /** @type {Array.<number>|undefined} */
+  var flatLinearRing = ol.xml.pushParseAndPop(undefined,
+      ol.format.KML.INNER_BOUNDARY_IS_PARSERS_, node, objectStack);
+  if (flatLinearRing) {
+    var flatLinearRings = /** @type {Array.<Array.<number>>} */
+        (objectStack[objectStack.length - 1]);
+    goog.asserts.assert(Array.isArray(flatLinearRings),
+        'flatLinearRings should be an array');
+    goog.asserts.assert(flatLinearRings.length > 0,
+        'flatLinearRings array should not be empty');
+    flatLinearRings.push(flatLinearRing);
+  }
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {Array.<*>} objectStack Object stack.
+ * @private
+ */
+ol.format.KML.outerBoundaryIsParser_ = function(node, objectStack) {
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'node.nodeType should be ELEMENT');
+  goog.asserts.assert(node.localName == 'outerBoundaryIs',
+      'localName should be outerBoundaryIs');
+  /** @type {Array.<number>|undefined} */
+  var flatLinearRing = ol.xml.pushParseAndPop(undefined,
+      ol.format.KML.OUTER_BOUNDARY_IS_PARSERS_, node, objectStack);
+  if (flatLinearRing) {
+    var flatLinearRings = /** @type {Array.<Array.<number>>} */
+        (objectStack[objectStack.length - 1]);
+    goog.asserts.assert(Array.isArray(flatLinearRings),
+        'flatLinearRings should be an array');
+    goog.asserts.assert(flatLinearRings.length > 0,
+        'flatLinearRings array should not be empty');
+    flatLinearRings[0] = flatLinearRing;
+  }
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {Array.<*>} objectStack Object stack.
+ * @private
+ */
+ol.format.KML.LinkParser_ = function(node, objectStack) {
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'node.nodeType should be ELEMENT');
+  goog.asserts.assert(node.localName == 'Link', 'localName should be Link');
+  ol.xml.parseNode(ol.format.KML.LINK_PARSERS_, node, objectStack);
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {Array.<*>} objectStack Object stack.
+ * @private
+ */
+ol.format.KML.whenParser_ = function(node, objectStack) {
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'node.nodeType should be ELEMENT');
+  goog.asserts.assert(node.localName == 'when', 'localName should be when');
+  var gxTrackObject = /** @type {ol.KMLGxTrackObject_} */
+      (objectStack[objectStack.length - 1]);
+  goog.asserts.assert(goog.isObject(gxTrackObject),
+      'gxTrackObject should be an Object');
+  var whens = gxTrackObject.whens;
+  var s = ol.xml.getAllTextContent(node, false);
+  var re =
+      /^\s*(\d{4})($|-(\d{2})($|-(\d{2})($|T(\d{2}):(\d{2}):(\d{2})(Z|(?:([+\-])(\d{2})(?::(\d{2}))?)))))\s*$/;
+  var m = re.exec(s);
+  if (m) {
+    var year = parseInt(m[1], 10);
+    var month = m[3] ? parseInt(m[3], 10) - 1 : 0;
+    var day = m[5] ? parseInt(m[5], 10) : 1;
+    var hour = m[7] ? parseInt(m[7], 10) : 0;
+    var minute = m[8] ? parseInt(m[8], 10) : 0;
+    var second = m[9] ? parseInt(m[9], 10) : 0;
+    var when = Date.UTC(year, month, day, hour, minute, second);
+    if (m[10] && m[10] != 'Z') {
+      var sign = m[11] == '-' ? -1 : 1;
+      when += sign * 60 * parseInt(m[12], 10);
+      if (m[13]) {
+        when += sign * 60 * 60 * parseInt(m[13], 10);
+      }
+    }
+    whens.push(when);
+  } else {
+    whens.push(0);
+  }
+};
+
+
+/**
+ * @const
+ * @type {Object.<string, Object.<string, ol.XmlParser>>}
+ * @private
+ */
+ol.format.KML.DATA_PARSERS_ = ol.xml.makeStructureNS(
+    ol.format.KML.NAMESPACE_URIS_, {
+      'value': ol.xml.makeReplacer(ol.format.XSD.readString)
+    });
+
+
+/**
+ * @const
+ * @type {Object.<string, Object.<string, ol.XmlParser>>}
+ * @private
+ */
+ol.format.KML.EXTENDED_DATA_PARSERS_ = ol.xml.makeStructureNS(
+    ol.format.KML.NAMESPACE_URIS_, {
+      'Data': ol.format.KML.DataParser_,
+      'SchemaData': ol.format.KML.SchemaDataParser_
+    });
+
+
+/**
+ * @const
+ * @type {Object.<string, Object.<string, ol.XmlParser>>}
+ * @private
+ */
+ol.format.KML.EXTRUDE_AND_ALTITUDE_MODE_PARSERS_ = ol.xml.makeStructureNS(
+    ol.format.KML.NAMESPACE_URIS_, {
+      'extrude': ol.xml.makeObjectPropertySetter(ol.format.XSD.readBoolean),
+      'altitudeMode': ol.xml.makeObjectPropertySetter(ol.format.XSD.readString)
+    });
+
+
+/**
+ * @const
+ * @type {Object.<string, Object.<string, ol.XmlParser>>}
+ * @private
+ */
+ol.format.KML.FLAT_LINEAR_RING_PARSERS_ = ol.xml.makeStructureNS(
+    ol.format.KML.NAMESPACE_URIS_, {
+      'coordinates': ol.xml.makeReplacer(ol.format.KML.readFlatCoordinates_)
+    });
+
+
+/**
+ * @const
+ * @type {Object.<string, Object.<string, ol.XmlParser>>}
+ * @private
+ */
+ol.format.KML.FLAT_LINEAR_RINGS_PARSERS_ = ol.xml.makeStructureNS(
+    ol.format.KML.NAMESPACE_URIS_, {
+      'innerBoundaryIs': ol.format.KML.innerBoundaryIsParser_,
+      'outerBoundaryIs': ol.format.KML.outerBoundaryIsParser_
+    });
+
+
+/**
+ * @const
+ * @type {Object.<string, Object.<string, ol.XmlParser>>}
+ * @private
+ */
+ol.format.KML.GX_TRACK_PARSERS_ = ol.xml.makeStructureNS(
+    ol.format.KML.NAMESPACE_URIS_, {
+      'when': ol.format.KML.whenParser_
+    }, ol.xml.makeStructureNS(
+        ol.format.KML.GX_NAMESPACE_URIS_, {
+          'coord': ol.format.KML.gxCoordParser_
+        }));
+
+
+/**
+ * @const
+ * @type {Object.<string, Object.<string, ol.XmlParser>>}
+ * @private
+ */
+ol.format.KML.GEOMETRY_FLAT_COORDINATES_PARSERS_ = ol.xml.makeStructureNS(
+    ol.format.KML.NAMESPACE_URIS_, {
+      'coordinates': ol.xml.makeReplacer(ol.format.KML.readFlatCoordinates_)
+    });
+
+
+/**
+ * @const
+ * @type {Object.<string, Object.<string, ol.XmlParser>>}
+ * @private
+ */
+ol.format.KML.ICON_PARSERS_ = ol.xml.makeStructureNS(
+    ol.format.KML.NAMESPACE_URIS_, {
+      'href': ol.xml.makeObjectPropertySetter(ol.format.KML.readURI_)
+    }, ol.xml.makeStructureNS(
+        ol.format.KML.GX_NAMESPACE_URIS_, {
+          'x': ol.xml.makeObjectPropertySetter(ol.format.XSD.readDecimal),
+          'y': ol.xml.makeObjectPropertySetter(ol.format.XSD.readDecimal),
+          'w': ol.xml.makeObjectPropertySetter(ol.format.XSD.readDecimal),
+          'h': ol.xml.makeObjectPropertySetter(ol.format.XSD.readDecimal)
+        }));
+
+
+/**
+ * @const
+ * @type {Object.<string, Object.<string, ol.XmlParser>>}
+ * @private
+ */
+ol.format.KML.ICON_STYLE_PARSERS_ = ol.xml.makeStructureNS(
+    ol.format.KML.NAMESPACE_URIS_, {
+      'Icon': ol.xml.makeObjectPropertySetter(ol.format.KML.readIcon_),
+      'heading': ol.xml.makeObjectPropertySetter(ol.format.XSD.readDecimal),
+      'hotSpot': ol.xml.makeObjectPropertySetter(ol.format.KML.readVec2_),
+      'scale': ol.xml.makeObjectPropertySetter(ol.format.KML.readScale_)
+    });
+
+
+/**
+ * @const
+ * @type {Object.<string, Object.<string, ol.XmlParser>>}
+ * @private
+ */
+ol.format.KML.INNER_BOUNDARY_IS_PARSERS_ = ol.xml.makeStructureNS(
+    ol.format.KML.NAMESPACE_URIS_, {
+      'LinearRing': ol.xml.makeReplacer(ol.format.KML.readFlatLinearRing_)
+    });
+
+
+/**
+ * @const
+ * @type {Object.<string, Object.<string, ol.XmlParser>>}
+ * @private
+ */
+ol.format.KML.LABEL_STYLE_PARSERS_ = ol.xml.makeStructureNS(
+    ol.format.KML.NAMESPACE_URIS_, {
+      'color': ol.xml.makeObjectPropertySetter(ol.format.KML.readColor_),
+      'scale': ol.xml.makeObjectPropertySetter(ol.format.KML.readScale_)
+    });
+
+
+/**
+ * @const
+ * @type {Object.<string, Object.<string, ol.XmlParser>>}
+ * @private
+ */
+ol.format.KML.LINE_STYLE_PARSERS_ = ol.xml.makeStructureNS(
+    ol.format.KML.NAMESPACE_URIS_, {
+      'color': ol.xml.makeObjectPropertySetter(ol.format.KML.readColor_),
+      'width': ol.xml.makeObjectPropertySetter(ol.format.XSD.readDecimal)
+    });
+
+
+/**
+ * @const
+ * @type {Object.<string, Object.<string, ol.XmlParser>>}
+ * @private
+ */
+ol.format.KML.MULTI_GEOMETRY_PARSERS_ = ol.xml.makeStructureNS(
+    ol.format.KML.NAMESPACE_URIS_, {
+      'LineString': ol.xml.makeArrayPusher(ol.format.KML.readLineString_),
+      'LinearRing': ol.xml.makeArrayPusher(ol.format.KML.readLinearRing_),
+      'MultiGeometry': ol.xml.makeArrayPusher(ol.format.KML.readMultiGeometry_),
+      'Point': ol.xml.makeArrayPusher(ol.format.KML.readPoint_),
+      'Polygon': ol.xml.makeArrayPusher(ol.format.KML.readPolygon_)
+    });
+
+
+/**
+ * @const
+ * @type {Object.<string, Object.<string, ol.XmlParser>>}
+ * @private
+ */
+ol.format.KML.GX_MULTITRACK_GEOMETRY_PARSERS_ = ol.xml.makeStructureNS(
+    ol.format.KML.GX_NAMESPACE_URIS_, {
+      'Track': ol.xml.makeArrayPusher(ol.format.KML.readGxTrack_)
+    });
+
+
+/**
+ * @const
+ * @type {Object.<string, Object.<string, ol.XmlParser>>}
+ * @private
+ */
+ol.format.KML.NETWORK_LINK_PARSERS_ = ol.xml.makeStructureNS(
+    ol.format.KML.NAMESPACE_URIS_, {
+      'ExtendedData': ol.format.KML.ExtendedDataParser_,
+      'Link': ol.format.KML.LinkParser_,
+      'address': ol.xml.makeObjectPropertySetter(ol.format.XSD.readString),
+      'description': ol.xml.makeObjectPropertySetter(ol.format.XSD.readString),
+      'name': ol.xml.makeObjectPropertySetter(ol.format.XSD.readString),
+      'open': ol.xml.makeObjectPropertySetter(ol.format.XSD.readBoolean),
+      'phoneNumber': ol.xml.makeObjectPropertySetter(ol.format.XSD.readString),
+      'visibility': ol.xml.makeObjectPropertySetter(ol.format.XSD.readBoolean)
+    });
+
+
+/**
+ * @const
+ * @type {Object.<string, Object.<string, ol.XmlParser>>}
+ * @private
+ */
+ol.format.KML.LINK_PARSERS_ = ol.xml.makeStructureNS(
+    ol.format.KML.NAMESPACE_URIS_, {
+      'href': ol.xml.makeObjectPropertySetter(ol.format.KML.readURI_)
+    });
+
+
+/**
+ * @const
+ * @type {Object.<string, Object.<string, ol.XmlParser>>}
+ * @private
+ */
+ol.format.KML.OUTER_BOUNDARY_IS_PARSERS_ = ol.xml.makeStructureNS(
+    ol.format.KML.NAMESPACE_URIS_, {
+      'LinearRing': ol.xml.makeReplacer(ol.format.KML.readFlatLinearRing_)
+    });
+
+
+/**
+ * @const
+ * @type {Object.<string, Object.<string, ol.XmlParser>>}
+ * @private
+ */
+ol.format.KML.PAIR_PARSERS_ = ol.xml.makeStructureNS(
+    ol.format.KML.NAMESPACE_URIS_, {
+      'Style': ol.xml.makeObjectPropertySetter(ol.format.KML.readStyle_),
+      'key': ol.xml.makeObjectPropertySetter(ol.format.XSD.readString),
+      'styleUrl': ol.xml.makeObjectPropertySetter(ol.format.KML.readStyleUrl_)
+    });
+
+
+/**
+ * @const
+ * @type {Object.<string, Object.<string, ol.XmlParser>>}
+ * @private
+ */
+ol.format.KML.PLACEMARK_PARSERS_ = ol.xml.makeStructureNS(
+    ol.format.KML.NAMESPACE_URIS_, {
+      'ExtendedData': ol.format.KML.ExtendedDataParser_,
+      'MultiGeometry': ol.xml.makeObjectPropertySetter(
+          ol.format.KML.readMultiGeometry_, 'geometry'),
+      'LineString': ol.xml.makeObjectPropertySetter(
+          ol.format.KML.readLineString_, 'geometry'),
+      'LinearRing': ol.xml.makeObjectPropertySetter(
+          ol.format.KML.readLinearRing_, 'geometry'),
+      'Point': ol.xml.makeObjectPropertySetter(
+          ol.format.KML.readPoint_, 'geometry'),
+      'Polygon': ol.xml.makeObjectPropertySetter(
+          ol.format.KML.readPolygon_, 'geometry'),
+      'Style': ol.xml.makeObjectPropertySetter(ol.format.KML.readStyle_),
+      'StyleMap': ol.format.KML.PlacemarkStyleMapParser_,
+      'address': ol.xml.makeObjectPropertySetter(ol.format.XSD.readString),
+      'description': ol.xml.makeObjectPropertySetter(ol.format.XSD.readString),
+      'name': ol.xml.makeObjectPropertySetter(ol.format.XSD.readString),
+      'open': ol.xml.makeObjectPropertySetter(ol.format.XSD.readBoolean),
+      'phoneNumber': ol.xml.makeObjectPropertySetter(ol.format.XSD.readString),
+      'styleUrl': ol.xml.makeObjectPropertySetter(ol.format.KML.readURI_),
+      'visibility': ol.xml.makeObjectPropertySetter(ol.format.XSD.readBoolean)
+    }, ol.xml.makeStructureNS(
+        ol.format.KML.GX_NAMESPACE_URIS_, {
+          'MultiTrack': ol.xml.makeObjectPropertySetter(
+              ol.format.KML.readGxMultiTrack_, 'geometry'),
+          'Track': ol.xml.makeObjectPropertySetter(
+              ol.format.KML.readGxTrack_, 'geometry')
+        }
+    ));
+
+
+/**
+ * @const
+ * @type {Object.<string, Object.<string, ol.XmlParser>>}
+ * @private
+ */
+ol.format.KML.POLY_STYLE_PARSERS_ = ol.xml.makeStructureNS(
+    ol.format.KML.NAMESPACE_URIS_, {
+      'color': ol.xml.makeObjectPropertySetter(ol.format.KML.readColor_),
+      'fill': ol.xml.makeObjectPropertySetter(ol.format.XSD.readBoolean),
+      'outline': ol.xml.makeObjectPropertySetter(ol.format.XSD.readBoolean)
+    });
+
+
+/**
+ * @const
+ * @type {Object.<string, Object.<string, ol.XmlParser>>}
+ * @private
+ */
+ol.format.KML.SCHEMA_DATA_PARSERS_ = ol.xml.makeStructureNS(
+    ol.format.KML.NAMESPACE_URIS_, {
+      'SimpleData': ol.format.KML.SimpleDataParser_
+    });
+
+
+/**
+ * @const
+ * @type {Object.<string, Object.<string, ol.XmlParser>>}
+ * @private
+ */
+ol.format.KML.STYLE_PARSERS_ = ol.xml.makeStructureNS(
+    ol.format.KML.NAMESPACE_URIS_, {
+      'IconStyle': ol.format.KML.IconStyleParser_,
+      'LabelStyle': ol.format.KML.LabelStyleParser_,
+      'LineStyle': ol.format.KML.LineStyleParser_,
+      'PolyStyle': ol.format.KML.PolyStyleParser_
+    });
+
+
+/**
+ * @const
+ * @type {Object.<string, Object.<string, ol.XmlParser>>}
+ * @private
+ */
+ol.format.KML.STYLE_MAP_PARSERS_ = ol.xml.makeStructureNS(
+    ol.format.KML.NAMESPACE_URIS_, {
+      'Pair': ol.format.KML.PairDataParser_
+    });
+
+
+/**
+ * @inheritDoc
+ */
+ol.format.KML.prototype.getExtensions = function() {
+  return ol.format.KML.EXTENSIONS_;
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {Array.<*>} objectStack Object stack.
+ * @private
+ * @return {Array.<ol.Feature>|undefined} Features.
+ */
+ol.format.KML.prototype.readDocumentOrFolder_ = function(node, objectStack) {
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'node.nodeType should be ELEMENT');
+  var localName = node.localName;
+  goog.asserts.assert(localName == 'Document' || localName == 'Folder',
+      'localName should be Document or Folder');
+  // FIXME use scope somehow
+  var parsersNS = ol.xml.makeStructureNS(
+      ol.format.KML.NAMESPACE_URIS_, {
+        'Document': ol.xml.makeArrayExtender(this.readDocumentOrFolder_, this),
+        'Folder': ol.xml.makeArrayExtender(this.readDocumentOrFolder_, this),
+        'Placemark': ol.xml.makeArrayPusher(this.readPlacemark_, this),
+        'Style': this.readSharedStyle_.bind(this),
+        'StyleMap': this.readSharedStyleMap_.bind(this)
+      });
+  /** @type {Array.<ol.Feature>} */
+  var features = ol.xml.pushParseAndPop([], parsersNS, node, objectStack, this);
+  if (features) {
+    return features;
+  } else {
+    return undefined;
+  }
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {Array.<*>} objectStack Object stack.
+ * @private
+ * @return {ol.Feature|undefined} Feature.
+ */
+ol.format.KML.prototype.readPlacemark_ = function(node, objectStack) {
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'node.nodeType should be ELEMENT');
+  goog.asserts.assert(node.localName == 'Placemark',
+      'localName should be Placemark');
+  var object = ol.xml.pushParseAndPop({'geometry': null},
+      ol.format.KML.PLACEMARK_PARSERS_, node, objectStack);
+  if (!object) {
+    return undefined;
+  }
+  var feature = new ol.Feature();
+  var id = node.getAttribute('id');
+  if (id !== null) {
+    feature.setId(id);
+  }
+  var options = /** @type {olx.format.ReadOptions} */ (objectStack[0]);
+
+  var geometry = object['geometry'];
+  if (geometry) {
+    ol.format.Feature.transformWithOptions(geometry, false, options);
+  }
+  feature.setGeometry(geometry);
+  delete object['geometry'];
+
+  if (this.extractStyles_) {
+    var style = object['Style'];
+    var styleUrl = object['styleUrl'];
+    var styleFunction = ol.format.KML.createFeatureStyleFunction_(
+        style, styleUrl, this.defaultStyle_, this.sharedStyles_,
+        this.showPointNames_);
+    feature.setStyle(styleFunction);
+  }
+  delete object['Style'];
+  // we do not remove the styleUrl property from the object, so it
+  // gets stored on feature when setProperties is called
+
+  feature.setProperties(object);
+
+  return feature;
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {Array.<*>} objectStack Object stack.
+ * @private
+ */
+ol.format.KML.prototype.readSharedStyle_ = function(node, objectStack) {
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'node.nodeType should be ELEMENT');
+  goog.asserts.assert(node.localName == 'Style', 'localName should be Style');
+  var id = node.getAttribute('id');
+  if (id !== null) {
+    var style = ol.format.KML.readStyle_(node, objectStack);
+    if (style) {
+      var styleUri;
+      if (node.baseURI) {
+        styleUri = goog.Uri.resolve(node.baseURI, '#' + id).toString();
+      } else {
+        styleUri = '#' + id;
+      }
+      this.sharedStyles_[styleUri] = style;
+    }
+  }
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {Array.<*>} objectStack Object stack.
+ * @private
+ */
+ol.format.KML.prototype.readSharedStyleMap_ = function(node, objectStack) {
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'node.nodeType should be ELEMENT');
+  goog.asserts.assert(node.localName == 'StyleMap',
+      'localName should be StyleMap');
+  var id = node.getAttribute('id');
+  if (id === null) {
+    return;
+  }
+  var styleMapValue = ol.format.KML.readStyleMapValue_(node, objectStack);
+  if (!styleMapValue) {
+    return;
+  }
+  var styleUri;
+  if (node.baseURI) {
+    styleUri = goog.Uri.resolve(node.baseURI, '#' + id).toString();
+  } else {
+    styleUri = '#' + id;
+  }
+  this.sharedStyles_[styleUri] = styleMapValue;
+};
+
+
+/**
+ * Read the first feature from a KML source. MultiGeometries are converted into
+ * GeometryCollections if they are a mix of geometry types, and into MultiPoint/
+ * MultiLineString/MultiPolygon if they are all of the same type.
+ *
+ * @function
+ * @param {Document|Node|Object|string} source Source.
+ * @param {olx.format.ReadOptions=} opt_options Read options.
+ * @return {ol.Feature} Feature.
+ * @api stable
+ */
+ol.format.KML.prototype.readFeature;
+
+
+/**
+ * @inheritDoc
+ */
+ol.format.KML.prototype.readFeatureFromNode = function(node, opt_options) {
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'node.nodeType should be ELEMENT');
+  if (!ol.array.includes(ol.format.KML.NAMESPACE_URIS_, node.namespaceURI)) {
+    return null;
+  }
+  goog.asserts.assert(node.localName == 'Placemark',
+      'localName should be Placemark');
+  var feature = this.readPlacemark_(
+      node, [this.getReadOptions(node, opt_options)]);
+  if (feature) {
+    return feature;
+  } else {
+    return null;
+  }
+};
+
+
+/**
+ * Read all features from a KML source. MultiGeometries are converted into
+ * GeometryCollections if they are a mix of geometry types, and into MultiPoint/
+ * MultiLineString/MultiPolygon if they are all of the same type.
+ *
+ * @function
+ * @param {Document|Node|Object|string} source Source.
+ * @param {olx.format.ReadOptions=} opt_options Read options.
+ * @return {Array.<ol.Feature>} Features.
+ * @api stable
+ */
+ol.format.KML.prototype.readFeatures;
+
+
+/**
+ * @inheritDoc
+ */
+ol.format.KML.prototype.readFeaturesFromNode = function(node, opt_options) {
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'node.nodeType should be ELEMENT');
+  if (!ol.array.includes(ol.format.KML.NAMESPACE_URIS_, node.namespaceURI)) {
+    return [];
+  }
+  var features;
+  var localName = node.localName;
+  if (localName == 'Document' || localName == 'Folder') {
+    features = this.readDocumentOrFolder_(
+        node, [this.getReadOptions(node, opt_options)]);
+    if (features) {
+      return features;
+    } else {
+      return [];
+    }
+  } else if (localName == 'Placemark') {
+    var feature = this.readPlacemark_(
+        node, [this.getReadOptions(node, opt_options)]);
+    if (feature) {
+      return [feature];
+    } else {
+      return [];
+    }
+  } else if (localName == 'kml') {
+    features = [];
+    var n;
+    for (n = node.firstElementChild; n; n = n.nextElementSibling) {
+      var fs = this.readFeaturesFromNode(n, opt_options);
+      if (fs) {
+        ol.array.extend(features, fs);
+      }
+    }
+    return features;
+  } else {
+    return [];
+  }
+};
+
+
+/**
+ * Read the name of the KML.
+ *
+ * @param {Document|Node|string} source Souce.
+ * @return {string|undefined} Name.
+ * @api stable
+ */
+ol.format.KML.prototype.readName = function(source) {
+  if (ol.xml.isDocument(source)) {
+    return this.readNameFromDocument(/** @type {Document} */ (source));
+  } else if (ol.xml.isNode(source)) {
+    return this.readNameFromNode(/** @type {Node} */ (source));
+  } else if (typeof source === 'string') {
+    var doc = ol.xml.parse(source);
+    return this.readNameFromDocument(doc);
+  } else {
+    goog.asserts.fail('Unknown type for source');
+    return undefined;
+  }
+};
+
+
+/**
+ * @param {Document} doc Document.
+ * @return {string|undefined} Name.
+ */
+ol.format.KML.prototype.readNameFromDocument = function(doc) {
+  var n;
+  for (n = doc.firstChild; n; n = n.nextSibling) {
+    if (n.nodeType == Node.ELEMENT_NODE) {
+      var name = this.readNameFromNode(n);
+      if (name) {
+        return name;
+      }
+    }
+  }
+  return undefined;
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @return {string|undefined} Name.
+ */
+ol.format.KML.prototype.readNameFromNode = function(node) {
+  var n;
+  for (n = node.firstElementChild; n; n = n.nextElementSibling) {
+    if (ol.array.includes(ol.format.KML.NAMESPACE_URIS_, n.namespaceURI) &&
+        n.localName == 'name') {
+      return ol.format.XSD.readString(n);
+    }
+  }
+  for (n = node.firstElementChild; n; n = n.nextElementSibling) {
+    var localName = n.localName;
+    if (ol.array.includes(ol.format.KML.NAMESPACE_URIS_, n.namespaceURI) &&
+        (localName == 'Document' ||
+         localName == 'Folder' ||
+         localName == 'Placemark' ||
+         localName == 'kml')) {
+      var name = this.readNameFromNode(n);
+      if (name) {
+        return name;
+      }
+    }
+  }
+  return undefined;
+};
+
+
+/**
+ * Read the network links of the KML.
+ *
+ * @param {Document|Node|string} source Source.
+ * @return {Array.<Object>} Network links.
+ * @api
+ */
+ol.format.KML.prototype.readNetworkLinks = function(source) {
+  var networkLinks = [];
+  if (ol.xml.isDocument(source)) {
+    ol.array.extend(networkLinks, this.readNetworkLinksFromDocument(
+        /** @type {Document} */ (source)));
+  } else if (ol.xml.isNode(source)) {
+    ol.array.extend(networkLinks, this.readNetworkLinksFromNode(
+        /** @type {Node} */ (source)));
+  } else if (typeof source === 'string') {
+    var doc = ol.xml.parse(source);
+    ol.array.extend(networkLinks, this.readNetworkLinksFromDocument(doc));
+  } else {
+    goog.asserts.fail('unknown type for source');
+  }
+  return networkLinks;
+};
+
+
+/**
+ * @param {Document} doc Document.
+ * @return {Array.<Object>} Network links.
+ */
+ol.format.KML.prototype.readNetworkLinksFromDocument = function(doc) {
+  var n, networkLinks = [];
+  for (n = doc.firstChild; n; n = n.nextSibling) {
+    if (n.nodeType == Node.ELEMENT_NODE) {
+      ol.array.extend(networkLinks, this.readNetworkLinksFromNode(n));
+    }
+  }
+  return networkLinks;
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @return {Array.<Object>} Network links.
+ */
+ol.format.KML.prototype.readNetworkLinksFromNode = function(node) {
+  var n, networkLinks = [];
+  for (n = node.firstElementChild; n; n = n.nextElementSibling) {
+    if (ol.array.includes(ol.format.KML.NAMESPACE_URIS_, n.namespaceURI) &&
+        n.localName == 'NetworkLink') {
+      var obj = ol.xml.pushParseAndPop({}, ol.format.KML.NETWORK_LINK_PARSERS_,
+          n, []);
+      networkLinks.push(obj);
+    }
+  }
+  for (n = node.firstElementChild; n; n = n.nextElementSibling) {
+    var localName = n.localName;
+    if (ol.array.includes(ol.format.KML.NAMESPACE_URIS_, n.namespaceURI) &&
+        (localName == 'Document' ||
+         localName == 'Folder' ||
+         localName == 'kml')) {
+      ol.array.extend(networkLinks, this.readNetworkLinksFromNode(n));
+    }
+  }
+  return networkLinks;
+};
+
+
+/**
+ * Read the projection from a KML source.
+ *
+ * @function
+ * @param {Document|Node|Object|string} source Source.
+ * @return {ol.proj.Projection} Projection.
+ * @api stable
+ */
+ol.format.KML.prototype.readProjection;
+
+
+/**
+ * @param {Node} node Node to append a TextNode with the color to.
+ * @param {ol.Color|string} color Color.
+ * @private
+ */
+ol.format.KML.writeColorTextNode_ = function(node, color) {
+  var rgba = ol.color.asArray(color);
+  var opacity = (rgba.length == 4) ? rgba[3] : 1;
+  var abgr = [opacity * 255, rgba[2], rgba[1], rgba[0]];
+  var i;
+  for (i = 0; i < 4; ++i) {
+    var hex = parseInt(abgr[i], 10).toString(16);
+    abgr[i] = (hex.length == 1) ? '0' + hex : hex;
+  }
+  ol.format.XSD.writeStringTextNode(node, abgr.join(''));
+};
+
+
+/**
+ * @param {Node} node Node to append a TextNode with the coordinates to.
+ * @param {Array.<number>} coordinates Coordinates.
+ * @param {Array.<*>} objectStack Object stack.
+ * @private
+ */
+ol.format.KML.writeCoordinatesTextNode_ = function(node, coordinates, objectStack) {
+  var context = objectStack[objectStack.length - 1];
+  goog.asserts.assert(goog.isObject(context), 'context should be an Object');
+
+  var layout = context['layout'];
+  var stride = context['stride'];
+
+  var dimension;
+  if (layout == ol.geom.GeometryLayout.XY ||
+      layout == ol.geom.GeometryLayout.XYM) {
+    dimension = 2;
+  } else if (layout == ol.geom.GeometryLayout.XYZ ||
+      layout == ol.geom.GeometryLayout.XYZM) {
+    dimension = 3;
+  } else {
+    goog.asserts.fail('Unknown geometry layout');
+  }
+
+  var d, i;
+  var ii = coordinates.length;
+  var text = '';
+  if (ii > 0) {
+    text += coordinates[0];
+    for (d = 1; d < dimension; ++d) {
+      text += ',' + coordinates[d];
+    }
+    for (i = stride; i < ii; i += stride) {
+      text += ' ' + coordinates[i];
+      for (d = 1; d < dimension; ++d) {
+        text += ',' + coordinates[i + d];
+      }
+    }
+  }
+  ol.format.XSD.writeStringTextNode(node, text);
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {Array.<ol.Feature>} features Features.
+ * @param {Array.<*>} objectStack Object stack.
+ * @this {ol.format.KML}
+ * @private
+ */
+ol.format.KML.writeDocument_ = function(node, features, objectStack) {
+  var /** @type {ol.XmlNodeStackItem} */ context = {node: node};
+  ol.xml.pushSerializeAndPop(context, ol.format.KML.DOCUMENT_SERIALIZERS_,
+      ol.format.KML.DOCUMENT_NODE_FACTORY_, features, objectStack, undefined,
+      this);
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {Object} icon Icon object.
+ * @param {Array.<*>} objectStack Object stack.
+ * @private
+ */
+ol.format.KML.writeIcon_ = function(node, icon, objectStack) {
+  var /** @type {ol.XmlNodeStackItem} */ context = {node: node};
+  var parentNode = objectStack[objectStack.length - 1].node;
+  var orderedKeys = ol.format.KML.ICON_SEQUENCE_[parentNode.namespaceURI];
+  var values = ol.xml.makeSequence(icon, orderedKeys);
+  ol.xml.pushSerializeAndPop(context,
+      ol.format.KML.ICON_SERIALIZERS_, ol.xml.OBJECT_PROPERTY_NODE_FACTORY,
+      values, objectStack, orderedKeys);
+  orderedKeys =
+      ol.format.KML.ICON_SEQUENCE_[ol.format.KML.GX_NAMESPACE_URIS_[0]];
+  values = ol.xml.makeSequence(icon, orderedKeys);
+  ol.xml.pushSerializeAndPop(context, ol.format.KML.ICON_SERIALIZERS_,
+      ol.format.KML.GX_NODE_FACTORY_, values, objectStack, orderedKeys);
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {ol.style.Icon} style Icon style.
+ * @param {Array.<*>} objectStack Object stack.
+ * @private
+ */
+ol.format.KML.writeIconStyle_ = function(node, style, objectStack) {
+  var /** @type {ol.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] !== 0 && anchor[1] !== size[1]) {
+      var /** @type {ol.KMLVec2_} */ hotSpot = {
+        x: anchor[0],
+        xunits: ol.style.IconAnchorUnits.PIXELS,
+        y: size[1] - anchor[1],
+        yunits: ol.style.IconAnchorUnits.PIXELS
+      };
+      properties['hotSpot'] = hotSpot;
+    }
+  }
+
+  properties['Icon'] = iconProperties;
+
+  var scale = style.getScale();
+  if (scale !== 1) {
+    properties['scale'] = scale;
+  }
+
+  var rotation = style.getRotation();
+  if (rotation !== 0) {
+    properties['heading'] = rotation; // 0-360
+  }
+
+  var parentNode = objectStack[objectStack.length - 1].node;
+  var orderedKeys = ol.format.KML.ICON_STYLE_SEQUENCE_[parentNode.namespaceURI];
+  var values = ol.xml.makeSequence(properties, orderedKeys);
+  ol.xml.pushSerializeAndPop(context, ol.format.KML.ICON_STYLE_SERIALIZERS_,
+      ol.xml.OBJECT_PROPERTY_NODE_FACTORY, values, objectStack, orderedKeys);
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {ol.style.Text} style style.
+ * @param {Array.<*>} objectStack Object stack.
+ * @private
+ */
+ol.format.KML.writeLabelStyle_ = function(node, style, objectStack) {
+  var /** @type {ol.XmlNodeStackItem} */ context = {node: node};
+  var properties = {};
+  var fill = style.getFill();
+  if (fill) {
+    properties['color'] = fill.getColor();
+  }
+  var scale = style.getScale();
+  if (scale && scale !== 1) {
+    properties['scale'] = scale;
+  }
+  var parentNode = objectStack[objectStack.length - 1].node;
+  var orderedKeys =
+      ol.format.KML.LABEL_STYLE_SEQUENCE_[parentNode.namespaceURI];
+  var values = ol.xml.makeSequence(properties, orderedKeys);
+  ol.xml.pushSerializeAndPop(context, ol.format.KML.LABEL_STYLE_SERIALIZERS_,
+      ol.xml.OBJECT_PROPERTY_NODE_FACTORY, values, objectStack, orderedKeys);
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {ol.style.Stroke} style style.
+ * @param {Array.<*>} objectStack Object stack.
+ * @private
+ */
+ol.format.KML.writeLineStyle_ = function(node, style, objectStack) {
+  var /** @type {ol.XmlNodeStackItem} */ context = {node: node};
+  var properties = {
+    'color': style.getColor(),
+    'width': style.getWidth()
+  };
+  var parentNode = objectStack[objectStack.length - 1].node;
+  var orderedKeys = ol.format.KML.LINE_STYLE_SEQUENCE_[parentNode.namespaceURI];
+  var values = ol.xml.makeSequence(properties, orderedKeys);
+  ol.xml.pushSerializeAndPop(context, ol.format.KML.LINE_STYLE_SERIALIZERS_,
+      ol.xml.OBJECT_PROPERTY_NODE_FACTORY, values, objectStack, orderedKeys);
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {ol.geom.Geometry} geometry Geometry.
+ * @param {Array.<*>} objectStack Object stack.
+ * @private
+ */
+ol.format.KML.writeMultiGeometry_ = function(node, geometry, objectStack) {
+  goog.asserts.assert(
+      (geometry instanceof ol.geom.GeometryCollection) ||
+      (geometry instanceof ol.geom.MultiPoint) ||
+      (geometry instanceof ol.geom.MultiLineString) ||
+      (geometry instanceof ol.geom.MultiPolygon),
+      'geometry should be one of: ol.geom.GeometryCollection, ' +
+      'ol.geom.MultiPoint, ol.geom.MultiLineString or ol.geom.MultiPolygon');
+  /** @type {ol.XmlNodeStackItem} */
+  var context = {node: node};
+  var type = geometry.getType();
+  /** @type {Array.<ol.geom.Geometry>} */
+  var geometries;
+  /** @type {function(*, Array.<*>, string=): (Node|undefined)} */
+  var factory;
+  if (type == ol.geom.GeometryType.GEOMETRY_COLLECTION) {
+    geometries = geometry.getGeometries();
+    factory = ol.format.KML.GEOMETRY_NODE_FACTORY_;
+  } else if (type == ol.geom.GeometryType.MULTI_POINT) {
+    geometries =
+        (/** @type {ol.geom.MultiPoint} */ (geometry)).getPoints();
+    factory = ol.format.KML.POINT_NODE_FACTORY_;
+  } else if (type == ol.geom.GeometryType.MULTI_LINE_STRING) {
+    geometries =
+        (/** @type {ol.geom.MultiLineString} */ (geometry)).getLineStrings();
+    factory = ol.format.KML.LINE_STRING_NODE_FACTORY_;
+  } else if (type == ol.geom.GeometryType.MULTI_POLYGON) {
+    geometries =
+        (/** @type {ol.geom.MultiPolygon} */ (geometry)).getPolygons();
+    factory = ol.format.KML.POLYGON_NODE_FACTORY_;
+  } else {
+    goog.asserts.fail('Unknown geometry type: ' + type);
+  }
+  ol.xml.pushSerializeAndPop(context,
+      ol.format.KML.MULTI_GEOMETRY_SERIALIZERS_, factory,
+      geometries, objectStack);
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {ol.geom.LinearRing} linearRing Linear ring.
+ * @param {Array.<*>} objectStack Object stack.
+ * @private
+ */
+ol.format.KML.writeBoundaryIs_ = function(node, linearRing, objectStack) {
+  var /** @type {ol.XmlNodeStackItem} */ context = {node: node};
+  ol.xml.pushSerializeAndPop(context,
+      ol.format.KML.BOUNDARY_IS_SERIALIZERS_,
+      ol.format.KML.LINEAR_RING_NODE_FACTORY_, [linearRing], objectStack);
+};
+
+
+/**
+ * FIXME currently we do serialize arbitrary/custom feature properties
+ * (ExtendedData).
+ * @param {Node} node Node.
+ * @param {ol.Feature} feature Feature.
+ * @param {Array.<*>} objectStack Object stack.
+ * @this {ol.format.KML}
+ * @private
+ */
+ol.format.KML.writePlacemark_ = function(node, feature, objectStack) {
+  var /** @type {ol.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();
+
+  var styleFunction = feature.getStyleFunction();
+  if (styleFunction) {
+    // FIXME the styles returned by the style function are supposed to be
+    // resolution-independent here
+    var styles = styleFunction.call(feature, 0);
+    if (styles) {
+      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 = ol.format.KML.PLACEMARK_SEQUENCE_[parentNode.namespaceURI];
+  var values = ol.xml.makeSequence(properties, orderedKeys);
+  ol.xml.pushSerializeAndPop(context, ol.format.KML.PLACEMARK_SERIALIZERS_,
+      ol.xml.OBJECT_PROPERTY_NODE_FACTORY, values, objectStack, orderedKeys);
+
+  // serialize geometry
+  var options = /** @type {olx.format.WriteOptions} */ (objectStack[0]);
+  var geometry = feature.getGeometry();
+  if (geometry) {
+    geometry =
+        ol.format.Feature.transformWithOptions(geometry, true, options);
+  }
+  ol.xml.pushSerializeAndPop(context, ol.format.KML.PLACEMARK_SERIALIZERS_,
+      ol.format.KML.GEOMETRY_NODE_FACTORY_, [geometry], objectStack);
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {ol.geom.SimpleGeometry} geometry Geometry.
+ * @param {Array.<*>} objectStack Object stack.
+ * @private
+ */
+ol.format.KML.writePrimitiveGeometry_ = function(node, geometry, objectStack) {
+  goog.asserts.assert(
+      (geometry instanceof ol.geom.Point) ||
+      (geometry instanceof ol.geom.LineString) ||
+      (geometry instanceof ol.geom.LinearRing),
+      'geometry should be one of ol.geom.Point, ol.geom.LineString ' +
+      'or ol.geom.LinearRing');
+  var flatCoordinates = geometry.getFlatCoordinates();
+  var /** @type {ol.XmlNodeStackItem} */ context = {node: node};
+  context['layout'] = geometry.getLayout();
+  context['stride'] = geometry.getStride();
+  ol.xml.pushSerializeAndPop(context,
+      ol.format.KML.PRIMITIVE_GEOMETRY_SERIALIZERS_,
+      ol.format.KML.COORDINATES_NODE_FACTORY_,
+      [flatCoordinates], objectStack);
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {ol.geom.Polygon} polygon Polygon.
+ * @param {Array.<*>} objectStack Object stack.
+ * @private
+ */
+ol.format.KML.writePolygon_ = function(node, polygon, objectStack) {
+  goog.asserts.assertInstanceof(polygon, ol.geom.Polygon,
+      'polygon should be an ol.geom.Polygon');
+  var linearRings = polygon.getLinearRings();
+  goog.asserts.assert(linearRings.length > 0,
+      'linearRings should not be empty');
+  var outerRing = linearRings.shift();
+  var /** @type {ol.XmlNodeStackItem} */ context = {node: node};
+  // inner rings
+  ol.xml.pushSerializeAndPop(context,
+      ol.format.KML.POLYGON_SERIALIZERS_,
+      ol.format.KML.INNER_BOUNDARY_NODE_FACTORY_,
+      linearRings, objectStack);
+  // outer ring
+  ol.xml.pushSerializeAndPop(context,
+      ol.format.KML.POLYGON_SERIALIZERS_,
+      ol.format.KML.OUTER_BOUNDARY_NODE_FACTORY_,
+      [outerRing], objectStack);
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {ol.style.Fill} style Style.
+ * @param {Array.<*>} objectStack Object stack.
+ * @private
+ */
+ol.format.KML.writePolyStyle_ = function(node, style, objectStack) {
+  var /** @type {ol.XmlNodeStackItem} */ context = {node: node};
+  ol.xml.pushSerializeAndPop(context, ol.format.KML.POLY_STYLE_SERIALIZERS_,
+      ol.format.KML.COLOR_NODE_FACTORY_, [style.getColor()], objectStack);
+};
+
+
+/**
+ * @param {Node} node Node to append a TextNode with the scale to.
+ * @param {number|undefined} scale Scale.
+ * @private
+ */
+ol.format.KML.writeScaleTextNode_ = function(node, scale) {
+  // the Math is to remove any excess decimals created by float arithmetic
+  ol.format.XSD.writeDecimalTextNode(node,
+      Math.round(scale * scale * 1e6) / 1e6);
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {ol.style.Style} style Style.
+ * @param {Array.<*>} objectStack Object stack.
+ * @private
+ */
+ol.format.KML.writeStyle_ = function(node, style, objectStack) {
+  var /** @type {ol.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 ol.style.Icon) {
+    properties['IconStyle'] = imageStyle;
+  }
+  if (textStyle) {
+    properties['LabelStyle'] = textStyle;
+  }
+  if (strokeStyle) {
+    properties['LineStyle'] = strokeStyle;
+  }
+  if (fillStyle) {
+    properties['PolyStyle'] = fillStyle;
+  }
+  var parentNode = objectStack[objectStack.length - 1].node;
+  var orderedKeys = ol.format.KML.STYLE_SEQUENCE_[parentNode.namespaceURI];
+  var values = ol.xml.makeSequence(properties, orderedKeys);
+  ol.xml.pushSerializeAndPop(context, ol.format.KML.STYLE_SERIALIZERS_,
+      ol.xml.OBJECT_PROPERTY_NODE_FACTORY, values, objectStack, orderedKeys);
+};
+
+
+/**
+ * @param {Node} node Node to append a TextNode with the Vec2 to.
+ * @param {ol.KMLVec2_} vec2 Vec2.
+ * @private
+ */
+ol.format.KML.writeVec2_ = function(node, vec2) {
+  node.setAttribute('x', vec2.x);
+  node.setAttribute('y', vec2.y);
+  node.setAttribute('xunits', vec2.xunits);
+  node.setAttribute('yunits', vec2.yunits);
+};
+
+
+/**
+ * @const
+ * @type {Object.<string, Array.<string>>}
+ * @private
+ */
+ol.format.KML.KML_SEQUENCE_ = ol.xml.makeStructureNS(
+    ol.format.KML.NAMESPACE_URIS_, [
+      'Document', 'Placemark'
+    ]);
+
+
+/**
+ * @const
+ * @type {Object.<string, Object.<string, ol.XmlSerializer>>}
+ * @private
+ */
+ol.format.KML.KML_SERIALIZERS_ = ol.xml.makeStructureNS(
+    ol.format.KML.NAMESPACE_URIS_, {
+      'Document': ol.xml.makeChildAppender(ol.format.KML.writeDocument_),
+      'Placemark': ol.xml.makeChildAppender(ol.format.KML.writePlacemark_)
+    });
+
+
+/**
+ * @const
+ * @type {Object.<string, Object.<string, ol.XmlSerializer>>}
+ * @private
+ */
+ol.format.KML.DOCUMENT_SERIALIZERS_ = ol.xml.makeStructureNS(
+    ol.format.KML.NAMESPACE_URIS_, {
+      'Placemark': ol.xml.makeChildAppender(ol.format.KML.writePlacemark_)
+    });
+
+
+/**
+ * @const
+ * @type {Object.<string, string>}
+ * @private
+ */
+ol.format.KML.GEOMETRY_TYPE_TO_NODENAME_ = {
+  'Point': 'Point',
+  'LineString': 'LineString',
+  'LinearRing': 'LinearRing',
+  'Polygon': 'Polygon',
+  'MultiPoint': 'MultiGeometry',
+  'MultiLineString': 'MultiGeometry',
+  'MultiPolygon': 'MultiGeometry',
+  'GeometryCollection': 'MultiGeometry'
+};
+
+
+/**
+ * @const
+ * @type {Object.<string, Array.<string>>}
+ * @private
+ */
+ol.format.KML.ICON_SEQUENCE_ = ol.xml.makeStructureNS(
+    ol.format.KML.NAMESPACE_URIS_, [
+      'href'
+    ],
+    ol.xml.makeStructureNS(ol.format.KML.GX_NAMESPACE_URIS_, [
+      'x', 'y', 'w', 'h'
+    ]));
+
+
+/**
+ * @const
+ * @type {Object.<string, Object.<string, ol.XmlSerializer>>}
+ * @private
+ */
+ol.format.KML.ICON_SERIALIZERS_ = ol.xml.makeStructureNS(
+    ol.format.KML.NAMESPACE_URIS_, {
+      'href': ol.xml.makeChildAppender(ol.format.XSD.writeStringTextNode)
+    }, ol.xml.makeStructureNS(
+        ol.format.KML.GX_NAMESPACE_URIS_, {
+          'x': ol.xml.makeChildAppender(ol.format.XSD.writeDecimalTextNode),
+          'y': ol.xml.makeChildAppender(ol.format.XSD.writeDecimalTextNode),
+          'w': ol.xml.makeChildAppender(ol.format.XSD.writeDecimalTextNode),
+          'h': ol.xml.makeChildAppender(ol.format.XSD.writeDecimalTextNode)
+        }));
+
+
+/**
+ * @const
+ * @type {Object.<string, Array.<string>>}
+ * @private
+ */
+ol.format.KML.ICON_STYLE_SEQUENCE_ = ol.xml.makeStructureNS(
+    ol.format.KML.NAMESPACE_URIS_, [
+      'scale', 'heading', 'Icon', 'hotSpot'
+    ]);
+
+
+/**
+ * @const
+ * @type {Object.<string, Object.<string, ol.XmlSerializer>>}
+ * @private
+ */
+ol.format.KML.ICON_STYLE_SERIALIZERS_ = ol.xml.makeStructureNS(
+    ol.format.KML.NAMESPACE_URIS_, {
+      'Icon': ol.xml.makeChildAppender(ol.format.KML.writeIcon_),
+      'heading': ol.xml.makeChildAppender(ol.format.XSD.writeDecimalTextNode),
+      'hotSpot': ol.xml.makeChildAppender(ol.format.KML.writeVec2_),
+      'scale': ol.xml.makeChildAppender(ol.format.KML.writeScaleTextNode_)
+    });
+
+
+/**
+ * @const
+ * @type {Object.<string, Array.<string>>}
+ * @private
+ */
+ol.format.KML.LABEL_STYLE_SEQUENCE_ = ol.xml.makeStructureNS(
+    ol.format.KML.NAMESPACE_URIS_, [
+      'color', 'scale'
+    ]);
+
+
+/**
+ * @const
+ * @type {Object.<string, Object.<string, ol.XmlSerializer>>}
+ * @private
+ */
+ol.format.KML.LABEL_STYLE_SERIALIZERS_ = ol.xml.makeStructureNS(
+    ol.format.KML.NAMESPACE_URIS_, {
+      'color': ol.xml.makeChildAppender(ol.format.KML.writeColorTextNode_),
+      'scale': ol.xml.makeChildAppender(ol.format.KML.writeScaleTextNode_)
+    });
+
+
+/**
+ * @const
+ * @type {Object.<string, Array.<string>>}
+ * @private
+ */
+ol.format.KML.LINE_STYLE_SEQUENCE_ = ol.xml.makeStructureNS(
+    ol.format.KML.NAMESPACE_URIS_, [
+      'color', 'width'
+    ]);
+
+
+/**
+ * @const
+ * @type {Object.<string, Object.<string, ol.XmlSerializer>>}
+ * @private
+ */
+ol.format.KML.LINE_STYLE_SERIALIZERS_ = ol.xml.makeStructureNS(
+    ol.format.KML.NAMESPACE_URIS_, {
+      'color': ol.xml.makeChildAppender(ol.format.KML.writeColorTextNode_),
+      'width': ol.xml.makeChildAppender(ol.format.XSD.writeDecimalTextNode)
+    });
+
+
+/**
+ * @const
+ * @type {Object.<string, Object.<string, ol.XmlSerializer>>}
+ * @private
+ */
+ol.format.KML.BOUNDARY_IS_SERIALIZERS_ = ol.xml.makeStructureNS(
+    ol.format.KML.NAMESPACE_URIS_, {
+      'LinearRing': ol.xml.makeChildAppender(
+          ol.format.KML.writePrimitiveGeometry_)
+    });
+
+
+/**
+ * @const
+ * @type {Object.<string, Object.<string, ol.XmlSerializer>>}
+ * @private
+ */
+ol.format.KML.MULTI_GEOMETRY_SERIALIZERS_ = ol.xml.makeStructureNS(
+    ol.format.KML.NAMESPACE_URIS_, {
+      'LineString': ol.xml.makeChildAppender(
+          ol.format.KML.writePrimitiveGeometry_),
+      'Point': ol.xml.makeChildAppender(
+          ol.format.KML.writePrimitiveGeometry_),
+      'Polygon': ol.xml.makeChildAppender(ol.format.KML.writePolygon_),
+      'GeometryCollection': ol.xml.makeChildAppender(
+          ol.format.KML.writeMultiGeometry_)
+    });
+
+
+/**
+ * @const
+ * @type {Object.<string, Array.<string>>}
+ * @private
+ */
+ol.format.KML.PLACEMARK_SEQUENCE_ = ol.xml.makeStructureNS(
+    ol.format.KML.NAMESPACE_URIS_, [
+      'name', 'open', 'visibility', 'address', 'phoneNumber', 'description',
+      'styleUrl', 'Style'
+    ]);
+
+
+/**
+ * @const
+ * @type {Object.<string, Object.<string, ol.XmlSerializer>>}
+ * @private
+ */
+ol.format.KML.PLACEMARK_SERIALIZERS_ = ol.xml.makeStructureNS(
+    ol.format.KML.NAMESPACE_URIS_, {
+      'MultiGeometry': ol.xml.makeChildAppender(
+          ol.format.KML.writeMultiGeometry_),
+      'LineString': ol.xml.makeChildAppender(
+          ol.format.KML.writePrimitiveGeometry_),
+      'LinearRing': ol.xml.makeChildAppender(
+          ol.format.KML.writePrimitiveGeometry_),
+      'Point': ol.xml.makeChildAppender(
+          ol.format.KML.writePrimitiveGeometry_),
+      'Polygon': ol.xml.makeChildAppender(ol.format.KML.writePolygon_),
+      'Style': ol.xml.makeChildAppender(ol.format.KML.writeStyle_),
+      'address': ol.xml.makeChildAppender(ol.format.XSD.writeStringTextNode),
+      'description': ol.xml.makeChildAppender(
+          ol.format.XSD.writeStringTextNode),
+      'name': ol.xml.makeChildAppender(ol.format.XSD.writeStringTextNode),
+      'open': ol.xml.makeChildAppender(ol.format.XSD.writeBooleanTextNode),
+      'phoneNumber': ol.xml.makeChildAppender(
+          ol.format.XSD.writeStringTextNode),
+      'styleUrl': ol.xml.makeChildAppender(ol.format.XSD.writeStringTextNode),
+      'visibility': ol.xml.makeChildAppender(
+          ol.format.XSD.writeBooleanTextNode)
+    });
+
+
+/**
+ * @const
+ * @type {Object.<string, Object.<string, ol.XmlSerializer>>}
+ * @private
+ */
+ol.format.KML.PRIMITIVE_GEOMETRY_SERIALIZERS_ = ol.xml.makeStructureNS(
+    ol.format.KML.NAMESPACE_URIS_, {
+      'coordinates': ol.xml.makeChildAppender(
+          ol.format.KML.writeCoordinatesTextNode_)
+    });
+
+
+/**
+ * @const
+ * @type {Object.<string, Object.<string, ol.XmlSerializer>>}
+ * @private
+ */
+ol.format.KML.POLYGON_SERIALIZERS_ = ol.xml.makeStructureNS(
+    ol.format.KML.NAMESPACE_URIS_, {
+      'outerBoundaryIs': ol.xml.makeChildAppender(
+          ol.format.KML.writeBoundaryIs_),
+      'innerBoundaryIs': ol.xml.makeChildAppender(
+          ol.format.KML.writeBoundaryIs_)
+    });
+
+
+/**
+ * @const
+ * @type {Object.<string, Object.<string, ol.XmlSerializer>>}
+ * @private
+ */
+ol.format.KML.POLY_STYLE_SERIALIZERS_ = ol.xml.makeStructureNS(
+    ol.format.KML.NAMESPACE_URIS_, {
+      'color': ol.xml.makeChildAppender(ol.format.KML.writeColorTextNode_)
+    });
+
+
+/**
+ * @const
+ * @type {Object.<string, Array.<string>>}
+ * @private
+ */
+ol.format.KML.STYLE_SEQUENCE_ = ol.xml.makeStructureNS(
+    ol.format.KML.NAMESPACE_URIS_, [
+      'IconStyle', 'LabelStyle', 'LineStyle', 'PolyStyle'
+    ]);
+
+
+/**
+ * @const
+ * @type {Object.<string, Object.<string, ol.XmlSerializer>>}
+ * @private
+ */
+ol.format.KML.STYLE_SERIALIZERS_ = ol.xml.makeStructureNS(
+    ol.format.KML.NAMESPACE_URIS_, {
+      'IconStyle': ol.xml.makeChildAppender(ol.format.KML.writeIconStyle_),
+      'LabelStyle': ol.xml.makeChildAppender(ol.format.KML.writeLabelStyle_),
+      'LineStyle': ol.xml.makeChildAppender(ol.format.KML.writeLineStyle_),
+      'PolyStyle': ol.xml.makeChildAppender(ol.format.KML.writePolyStyle_)
+    });
+
+
+/**
+ * @const
+ * @param {*} value Value.
+ * @param {Array.<*>} objectStack Object stack.
+ * @param {string=} opt_nodeName Node name.
+ * @return {Node|undefined} Node.
+ * @private
+ */
+ol.format.KML.GX_NODE_FACTORY_ = function(value, objectStack, opt_nodeName) {
+  return ol.xml.createElementNS(ol.format.KML.GX_NAMESPACE_URIS_[0],
+      'gx:' + opt_nodeName);
+};
+
+
+/**
+ * @const
+ * @param {*} value Value.
+ * @param {Array.<*>} objectStack Object stack.
+ * @param {string=} opt_nodeName Node name.
+ * @return {Node|undefined} Node.
+ * @private
+ */
+ol.format.KML.DOCUMENT_NODE_FACTORY_ = function(value, objectStack,
+    opt_nodeName) {
+  goog.asserts.assertInstanceof(value, ol.Feature,
+      'value should be an ol.Feature');
+  var parentNode = objectStack[objectStack.length - 1].node;
+  goog.asserts.assert(ol.xml.isNode(parentNode),
+      'parentNode should be an XML node');
+  return ol.xml.createElementNS(parentNode.namespaceURI, 'Placemark');
+};
+
+
+/**
+ * @const
+ * @param {*} value Value.
+ * @param {Array.<*>} objectStack Object stack.
+ * @param {string=} opt_nodeName Node name.
+ * @return {Node|undefined} Node.
+ * @private
+ */
+ol.format.KML.GEOMETRY_NODE_FACTORY_ = function(value, objectStack,
+    opt_nodeName) {
+  if (value) {
+    goog.asserts.assertInstanceof(value, ol.geom.Geometry,
+        'value should be an ol.geom.Geometry');
+    var parentNode = objectStack[objectStack.length - 1].node;
+    goog.asserts.assert(ol.xml.isNode(parentNode),
+        'parentNode should be an XML node');
+    return ol.xml.createElementNS(parentNode.namespaceURI,
+        ol.format.KML.GEOMETRY_TYPE_TO_NODENAME_[value.getType()]);
+  }
+};
+
+
+/**
+ * A factory for creating coordinates nodes.
+ * @const
+ * @type {function(*, Array.<*>, string=): (Node|undefined)}
+ * @private
+ */
+ol.format.KML.COLOR_NODE_FACTORY_ = ol.xml.makeSimpleNodeFactory('color');
+
+
+/**
+ * A factory for creating coordinates nodes.
+ * @const
+ * @type {function(*, Array.<*>, string=): (Node|undefined)}
+ * @private
+ */
+ol.format.KML.COORDINATES_NODE_FACTORY_ =
+    ol.xml.makeSimpleNodeFactory('coordinates');
+
+
+/**
+ * A factory for creating innerBoundaryIs nodes.
+ * @const
+ * @type {function(*, Array.<*>, string=): (Node|undefined)}
+ * @private
+ */
+ol.format.KML.INNER_BOUNDARY_NODE_FACTORY_ =
+    ol.xml.makeSimpleNodeFactory('innerBoundaryIs');
+
+
+/**
+ * A factory for creating Point nodes.
+ * @const
+ * @type {function(*, Array.<*>, string=): (Node|undefined)}
+ * @private
+ */
+ol.format.KML.POINT_NODE_FACTORY_ =
+    ol.xml.makeSimpleNodeFactory('Point');
+
+
+/**
+ * A factory for creating LineString nodes.
+ * @const
+ * @type {function(*, Array.<*>, string=): (Node|undefined)}
+ * @private
+ */
+ol.format.KML.LINE_STRING_NODE_FACTORY_ =
+    ol.xml.makeSimpleNodeFactory('LineString');
+
+
+/**
+ * A factory for creating LinearRing nodes.
+ * @const
+ * @type {function(*, Array.<*>, string=): (Node|undefined)}
+ * @private
+ */
+ol.format.KML.LINEAR_RING_NODE_FACTORY_ =
+    ol.xml.makeSimpleNodeFactory('LinearRing');
+
+
+/**
+ * A factory for creating Polygon nodes.
+ * @const
+ * @type {function(*, Array.<*>, string=): (Node|undefined)}
+ * @private
+ */
+ol.format.KML.POLYGON_NODE_FACTORY_ =
+    ol.xml.makeSimpleNodeFactory('Polygon');
+
+
+/**
+ * A factory for creating outerBoundaryIs nodes.
+ * @const
+ * @type {function(*, Array.<*>, string=): (Node|undefined)}
+ * @private
+ */
+ol.format.KML.OUTER_BOUNDARY_NODE_FACTORY_ =
+    ol.xml.makeSimpleNodeFactory('outerBoundaryIs');
+
+
+/**
+ * Encode an array of features in the KML format. GeometryCollections, MultiPoints,
+ * MultiLineStrings, and MultiPolygons are output as MultiGeometries.
+ *
+ * @function
+ * @param {Array.<ol.Feature>} features Features.
+ * @param {olx.format.WriteOptions=} opt_options Options.
+ * @return {string} Result.
+ * @api stable
+ */
+ol.format.KML.prototype.writeFeatures;
+
+
+/**
+ * Encode an array of features in the KML format as an XML node. GeometryCollections,
+ * MultiPoints, MultiLineStrings, and MultiPolygons are output as MultiGeometries.
+ *
+ * @param {Array.<ol.Feature>} features Features.
+ * @param {olx.format.WriteOptions=} opt_options Options.
+ * @return {Node} Node.
+ * @api
+ */
+ol.format.KML.prototype.writeFeaturesNode = function(features, opt_options) {
+  opt_options = this.adaptOptions(opt_options);
+  var kml = ol.xml.createElementNS(ol.format.KML.NAMESPACE_URIS_[4], 'kml');
+  var xmlnsUri = 'http://www.w3.org/2000/xmlns/';
+  var xmlSchemaInstanceUri = 'http://www.w3.org/2001/XMLSchema-instance';
+  ol.xml.setAttributeNS(kml, xmlnsUri, 'xmlns:gx',
+      ol.format.KML.GX_NAMESPACE_URIS_[0]);
+  ol.xml.setAttributeNS(kml, xmlnsUri, 'xmlns:xsi', xmlSchemaInstanceUri);
+  ol.xml.setAttributeNS(kml, xmlSchemaInstanceUri, 'xsi:schemaLocation',
+      ol.format.KML.SCHEMA_LOCATION_);
+
+  var /** @type {ol.XmlNodeStackItem} */ context = {node: kml};
+  var properties = {};
+  if (features.length > 1) {
+    properties['Document'] = features;
+  } else if (features.length == 1) {
+    properties['Placemark'] = features[0];
+  }
+  var orderedKeys = ol.format.KML.KML_SEQUENCE_[kml.namespaceURI];
+  var values = ol.xml.makeSequence(properties, orderedKeys);
+  ol.xml.pushSerializeAndPop(context, ol.format.KML.KML_SERIALIZERS_,
+      ol.xml.OBJECT_PROPERTY_NODE_FACTORY, values, [opt_options], orderedKeys,
+      this);
+  return kml;
+};
+
+goog.provide('ol.ext.pbf');
+/** @typedef {function(*)} */
+ol.ext.pbf;
+(function() {
+var exports = {};
+var module = {exports: exports};
+var define;
+/**
+ * @fileoverview
+ * @suppress {accessControls, ambiguousFunctionDecl, checkDebuggerStatement, checkRegExp, checkTypes, checkVars, const, constantProperty, deprecated, duplicate, es5Strict, fileoverviewTags, missingProperties, nonStandardJsDocs, strictModuleDepCheck, suspiciousCode, undefinedNames, undefinedVars, unknownDefines, uselessCode, visibility}
+ */
+(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.pbf = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(_dereq_,module,exports){
+exports.read = function (buffer, offset, isLE, mLen, nBytes) {
+  var e, m
+  var eLen = nBytes * 8 - mLen - 1
+  var eMax = (1 << eLen) - 1
+  var eBias = eMax >> 1
+  var nBits = -7
+  var i = isLE ? (nBytes - 1) : 0
+  var d = isLE ? -1 : 1
+  var s = buffer[offset + i]
+
+  i += d
+
+  e = s & ((1 << (-nBits)) - 1)
+  s >>= (-nBits)
+  nBits += eLen
+  for (; nBits > 0; e = e * 256 + buffer[offset + i], i += d, nBits -= 8) {}
+
+  m = e & ((1 << (-nBits)) - 1)
+  e >>= (-nBits)
+  nBits += mLen
+  for (; nBits > 0; m = m * 256 + buffer[offset + i], i += d, nBits -= 8) {}
+
+  if (e === 0) {
+    e = 1 - eBias
+  } else if (e === eMax) {
+    return m ? NaN : ((s ? -1 : 1) * Infinity)
+  } else {
+    m = m + Math.pow(2, mLen)
+    e = e - eBias
+  }
+  return (s ? -1 : 1) * m * Math.pow(2, e - mLen)
+}
+
+exports.write = function (buffer, value, offset, isLE, mLen, nBytes) {
+  var e, m, c
+  var eLen = nBytes * 8 - mLen - 1
+  var eMax = (1 << eLen) - 1
+  var eBias = eMax >> 1
+  var rt = (mLen === 23 ? Math.pow(2, -24) - Math.pow(2, -77) : 0)
+  var i = isLE ? 0 : (nBytes - 1)
+  var d = isLE ? 1 : -1
+  var s = value < 0 || (value === 0 && 1 / value < 0) ? 1 : 0
+
+  value = Math.abs(value)
+
+  if (isNaN(value) || value === Infinity) {
+    m = isNaN(value) ? 1 : 0
+    e = eMax
+  } else {
+    e = Math.floor(Math.log(value) / Math.LN2)
+    if (value * (c = Math.pow(2, -e)) < 1) {
+      e--
+      c *= 2
+    }
+    if (e + eBias >= 1) {
+      value += rt / c
+    } else {
+      value += rt * Math.pow(2, 1 - eBias)
+    }
+    if (value * c >= 2) {
+      e++
+      c /= 2
+    }
+
+    if (e + eBias >= eMax) {
+      m = 0
+      e = eMax
+    } else if (e + eBias >= 1) {
+      m = (value * c - 1) * Math.pow(2, mLen)
+      e = e + eBias
+    } else {
+      m = value * Math.pow(2, eBias - 1) * Math.pow(2, mLen)
+      e = 0
+    }
+  }
+
+  for (; mLen >= 8; buffer[offset + i] = m & 0xff, i += d, m /= 256, mLen -= 8) {}
+
+  e = (e << mLen) | m
+  eLen += mLen
+  for (; eLen > 0; buffer[offset + i] = e & 0xff, i += d, e /= 256, eLen -= 8) {}
+
+  buffer[offset + i - d] |= s * 128
+}
+
+},{}],2:[function(_dereq_,module,exports){
+'use strict';
+
+// lightweight Buffer shim for pbf browser build
+// based on code from github.com/feross/buffer (MIT-licensed)
+
+module.exports = Buffer;
+
+var ieee754 = _dereq_('ieee754');
+
+var BufferMethods;
+
+function Buffer(length) {
+    var arr;
+    if (length && length.length) {
+        arr = length;
+        length = arr.length;
+    }
+    var buf = new Uint8Array(length || 0);
+    if (arr) buf.set(arr);
+
+    buf.readUInt32LE = BufferMethods.readUInt32LE;
+    buf.writeUInt32LE = BufferMethods.writeUInt32LE;
+    buf.readInt32LE = BufferMethods.readInt32LE;
+    buf.writeInt32LE = BufferMethods.writeInt32LE;
+    buf.readFloatLE = BufferMethods.readFloatLE;
+    buf.writeFloatLE = BufferMethods.writeFloatLE;
+    buf.readDoubleLE = BufferMethods.readDoubleLE;
+    buf.writeDoubleLE = BufferMethods.writeDoubleLE;
+    buf.toString = BufferMethods.toString;
+    buf.write = BufferMethods.write;
+    buf.slice = BufferMethods.slice;
+    buf.copy = BufferMethods.copy;
+
+    buf._isBuffer = true;
+    return buf;
+}
+
+var lastStr, lastStrEncoded;
+
+BufferMethods = {
+    readUInt32LE: function(pos) {
+        return ((this[pos]) |
+            (this[pos + 1] << 8) |
+            (this[pos + 2] << 16)) +
+            (this[pos + 3] * 0x1000000);
+    },
+
+    writeUInt32LE: function(val, pos) {
+        this[pos] = val;
+        this[pos + 1] = (val >>> 8);
+        this[pos + 2] = (val >>> 16);
+        this[pos + 3] = (val >>> 24);
+    },
+
+    readInt32LE: function(pos) {
+        return ((this[pos]) |
+            (this[pos + 1] << 8) |
+            (this[pos + 2] << 16)) +
+            (this[pos + 3] << 24);
+    },
+
+    readFloatLE:  function(pos) { return ieee754.read(this, pos, true, 23, 4); },
+    readDoubleLE: function(pos) { return ieee754.read(this, pos, true, 52, 8); },
+
+    writeFloatLE:  function(val, pos) { return ieee754.write(this, val, pos, true, 23, 4); },
+    writeDoubleLE: function(val, pos) { return ieee754.write(this, val, pos, true, 52, 8); },
+
+    toString: function(encoding, start, end) {
+        var str = '',
+            tmp = '';
+
+        start = start || 0;
+        end = Math.min(this.length, end || this.length);
+
+        for (var i = start; i < end; i++) {
+            var ch = this[i];
+            if (ch <= 0x7F) {
+                str += decodeURIComponent(tmp) + String.fromCharCode(ch);
+                tmp = '';
+            } else {
+                tmp += '%' + ch.toString(16);
+            }
+        }
+
+        str += decodeURIComponent(tmp);
+
+        return str;
+    },
+
+    write: function(str, pos) {
+        var bytes = str === lastStr ? lastStrEncoded : encodeString(str);
+        for (var i = 0; i < bytes.length; i++) {
+            this[pos + i] = bytes[i];
+        }
+    },
+
+    slice: function(start, end) {
+        return this.subarray(start, end);
+    },
+
+    copy: function(buf, pos) {
+        pos = pos || 0;
+        for (var i = 0; i < this.length; i++) {
+            buf[pos + i] = this[i];
+        }
+    }
+};
+
+BufferMethods.writeInt32LE = BufferMethods.writeUInt32LE;
+
+Buffer.byteLength = function(str) {
+    lastStr = str;
+    lastStrEncoded = encodeString(str);
+    return lastStrEncoded.length;
+};
+
+Buffer.isBuffer = function(buf) {
+    return !!(buf && buf._isBuffer);
+};
+
+function encodeString(str) {
+    var length = str.length,
+        bytes = [];
+
+    for (var i = 0, c, lead; i < length; i++) {
+        c = str.charCodeAt(i); // code point
+
+        if (c > 0xD7FF && c < 0xE000) {
+
+            if (lead) {
+                if (c < 0xDC00) {
+                    bytes.push(0xEF, 0xBF, 0xBD);
+                    lead = c;
+                    continue;
+
+                } else {
+                    c = lead - 0xD800 << 10 | c - 0xDC00 | 0x10000;
+                    lead = null;
+                }
+
+            } else {
+                if (c > 0xDBFF || (i + 1 === length)) bytes.push(0xEF, 0xBF, 0xBD);
+                else lead = c;
+
+                continue;
+            }
+
+        } else if (lead) {
+            bytes.push(0xEF, 0xBF, 0xBD);
+            lead = null;
+        }
+
+        if (c < 0x80) bytes.push(c);
+        else if (c < 0x800) bytes.push(c >> 0x6 | 0xC0, c & 0x3F | 0x80);
+        else if (c < 0x10000) bytes.push(c >> 0xC | 0xE0, c >> 0x6 & 0x3F | 0x80, c & 0x3F | 0x80);
+        else bytes.push(c >> 0x12 | 0xF0, c >> 0xC & 0x3F | 0x80, c >> 0x6 & 0x3F | 0x80, c & 0x3F | 0x80);
+    }
+    return bytes;
+}
+
+},{"ieee754":1}],3:[function(_dereq_,module,exports){
+(function (global){
+'use strict';
+
+module.exports = Pbf;
+
+var Buffer = global.Buffer || _dereq_('./buffer');
+
+function Pbf(buf) {
+    this.buf = !Buffer.isBuffer(buf) ? new Buffer(buf || 0) : buf;
+    this.pos = 0;
+    this.length = this.buf.length;
+}
+
+Pbf.Varint  = 0; // varint: int32, int64, uint32, uint64, sint32, sint64, bool, enum
+Pbf.Fixed64 = 1; // 64-bit: double, fixed64, sfixed64
+Pbf.Bytes   = 2; // length-delimited: string, bytes, embedded messages, packed repeated fields
+Pbf.Fixed32 = 5; // 32-bit: float, fixed32, sfixed32
+
+var SHIFT_LEFT_32 = (1 << 16) * (1 << 16),
+    SHIFT_RIGHT_32 = 1 / SHIFT_LEFT_32,
+    POW_2_63 = Math.pow(2, 63);
+
+Pbf.prototype = {
+
+    destroy: function() {
+        this.buf = null;
+    },
+
+    // === READING =================================================================
+
+    readFields: function(readField, result, end) {
+        end = end || this.length;
+
+        while (this.pos < end) {
+            var val = this.readVarint(),
+                tag = val >> 3,
+                startPos = this.pos;
+
+            readField(tag, result, this);
+
+            if (this.pos === startPos) this.skip(val);
+        }
+        return result;
+    },
+
+    readMessage: function(readField, result) {
+        return this.readFields(readField, result, this.readVarint() + this.pos);
+    },
+
+    readFixed32: function() {
+        var val = this.buf.readUInt32LE(this.pos);
+        this.pos += 4;
+        return val;
+    },
+
+    readSFixed32: function() {
+        var val = this.buf.readInt32LE(this.pos);
+        this.pos += 4;
+        return val;
+    },
+
+    // 64-bit int handling is based on github.com/dpw/node-buffer-more-ints (MIT-licensed)
+
+    readFixed64: function() {
+        var val = this.buf.readUInt32LE(this.pos) + this.buf.readUInt32LE(this.pos + 4) * SHIFT_LEFT_32;
+        this.pos += 8;
+        return val;
+    },
+
+    readSFixed64: function() {
+        var val = this.buf.readUInt32LE(this.pos) + this.buf.readInt32LE(this.pos + 4) * SHIFT_LEFT_32;
+        this.pos += 8;
+        return val;
+    },
+
+    readFloat: function() {
+        var val = this.buf.readFloatLE(this.pos);
+        this.pos += 4;
+        return val;
+    },
+
+    readDouble: function() {
+        var val = this.buf.readDoubleLE(this.pos);
+        this.pos += 8;
+        return val;
+    },
+
+    readVarint: function() {
+        var buf = this.buf,
+            val, b;
+
+        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;
+
+        return readVarintRemainder(val, this);
+    },
+
+    readVarint64: function() {
+        var startPos = this.pos,
+            val = this.readVarint();
+
+        if (val < POW_2_63) return val;
+
+        var pos = this.pos - 2;
+        while (this.buf[pos] === 0xff) pos--;
+        if (pos < startPos) pos = startPos;
+
+        val = 0;
+        for (var i = 0; i < pos - startPos + 1; i++) {
+            var b = ~this.buf[startPos + i] & 0x7f;
+            val += i < 4 ? b << i * 7 : b * Math.pow(2, i * 7);
+        }
+
+        return -val - 1;
+    },
+
+    readSVarint: function() {
+        var num = this.readVarint();
+        return num % 2 === 1 ? (num + 1) / -2 : num / 2; // zigzag encoding
+    },
+
+    readBoolean: function() {
+        return Boolean(this.readVarint());
+    },
+
+    readString: function() {
+        var end = this.readVarint() + this.pos,
+            str = this.buf.toString('utf8', this.pos, end);
+        this.pos = end;
+        return str;
+    },
+
+    readBytes: function() {
+        var end = this.readVarint() + this.pos,
+            buffer = this.buf.slice(this.pos, end);
+        this.pos = end;
+        return buffer;
+    },
+
+    // verbose for performance reasons; doesn't affect gzipped size
+
+    readPackedVarint: function() {
+        var end = this.readVarint() + this.pos, arr = [];
+        while (this.pos < end) arr.push(this.readVarint());
+        return arr;
+    },
+    readPackedSVarint: function() {
+        var end = this.readVarint() + this.pos, arr = [];
+        while (this.pos < end) arr.push(this.readSVarint());
+        return arr;
+    },
+    readPackedBoolean: function() {
+        var end = this.readVarint() + this.pos, arr = [];
+        while (this.pos < end) arr.push(this.readBoolean());
+        return arr;
+    },
+    readPackedFloat: function() {
+        var end = this.readVarint() + this.pos, arr = [];
+        while (this.pos < end) arr.push(this.readFloat());
+        return arr;
+    },
+    readPackedDouble: function() {
+        var end = this.readVarint() + this.pos, arr = [];
+        while (this.pos < end) arr.push(this.readDouble());
+        return arr;
+    },
+    readPackedFixed32: function() {
+        var end = this.readVarint() + this.pos, arr = [];
+        while (this.pos < end) arr.push(this.readFixed32());
+        return arr;
+    },
+    readPackedSFixed32: function() {
+        var end = this.readVarint() + this.pos, arr = [];
+        while (this.pos < end) arr.push(this.readSFixed32());
+        return arr;
+    },
+    readPackedFixed64: function() {
+        var end = this.readVarint() + this.pos, arr = [];
+        while (this.pos < end) arr.push(this.readFixed64());
+        return arr;
+    },
+    readPackedSFixed64: function() {
+        var end = this.readVarint() + this.pos, arr = [];
+        while (this.pos < end) arr.push(this.readSFixed64());
+        return arr;
+    },
+
+    skip: function(val) {
+        var type = val & 0x7;
+        if (type === Pbf.Varint) while (this.buf[this.pos++] > 0x7f) {}
+        else if (type === Pbf.Bytes) this.pos = this.readVarint() + this.pos;
+        else if (type === Pbf.Fixed32) this.pos += 4;
+        else if (type === Pbf.Fixed64) this.pos += 8;
+        else throw new Error('Unimplemented type: ' + type);
+    },
+
+    // === WRITING =================================================================
+
+    writeTag: function(tag, type) {
+        this.writeVarint((tag << 3) | type);
+    },
+
+    realloc: function(min) {
+        var length = this.length || 16;
+
+        while (length < this.pos + min) length *= 2;
+
+        if (length !== this.length) {
+            var buf = new Buffer(length);
+            this.buf.copy(buf);
+            this.buf = buf;
+            this.length = length;
+        }
+    },
+
+    finish: function() {
+        this.length = this.pos;
+        this.pos = 0;
+        return this.buf.slice(0, this.length);
+    },
+
+    writeFixed32: function(val) {
+        this.realloc(4);
+        this.buf.writeUInt32LE(val, this.pos);
+        this.pos += 4;
+    },
+
+    writeSFixed32: function(val) {
+        this.realloc(4);
+        this.buf.writeInt32LE(val, this.pos);
+        this.pos += 4;
+    },
+
+    writeFixed64: function(val) {
+        this.realloc(8);
+        this.buf.writeInt32LE(val & -1, this.pos);
+        this.buf.writeUInt32LE(Math.floor(val * SHIFT_RIGHT_32), this.pos + 4);
+        this.pos += 8;
+    },
+
+    writeSFixed64: function(val) {
+        this.realloc(8);
+        this.buf.writeInt32LE(val & -1, this.pos);
+        this.buf.writeInt32LE(Math.floor(val * SHIFT_RIGHT_32), this.pos + 4);
+        this.pos += 8;
+    },
+
+    writeVarint: function(val) {
+        val = +val;
+
+        if (val > 0xfffffff) {
+            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);
+    },
+
+    writeBoolean: function(val) {
+        this.writeVarint(Boolean(val));
+    },
+
+    writeString: function(str) {
+        str = String(str);
+        var bytes = Buffer.byteLength(str);
+        this.writeVarint(bytes);
+        this.realloc(bytes);
+        this.buf.write(str, this.pos);
+        this.pos += bytes;
+    },
+
+    writeFloat: function(val) {
+        this.realloc(4);
+        this.buf.writeFloatLE(val, this.pos);
+        this.pos += 4;
+    },
+
+    writeDouble: function(val) {
+        this.realloc(8);
+        this.buf.writeDoubleLE(val, this.pos);
+        this.pos += 8;
+    },
+
+    writeBytes: function(buffer) {
+        var len = buffer.length;
+        this.writeVarint(len);
+        this.realloc(len);
+        for (var i = 0; i < len; i++) this.buf[this.pos++] = buffer[i];
+    },
+
+    writeRawMessage: function(fn, obj) {
+        this.pos++; // reserve 1 byte for short message length
+
+        // write the message directly to the buffer and see how much was written
+        var startPos = this.pos;
+        fn(obj, this);
+        var len = this.pos - startPos;
+
+        if (len >= 0x80) reallocForRawMessage(startPos, len, this);
+
+        // finally, write the message length in the reserved place and restore the position
+        this.pos = startPos - 1;
+        this.writeVarint(len);
+        this.pos += len;
+    },
+
+    writeMessage: function(tag, fn, obj) {
+        this.writeTag(tag, Pbf.Bytes);
+        this.writeRawMessage(fn, obj);
+    },
+
+    writePackedVarint:   function(tag, arr) { this.writeMessage(tag, writePackedVarint, arr);   },
+    writePackedSVarint:  function(tag, arr) { this.writeMessage(tag, writePackedSVarint, arr);  },
+    writePackedBoolean:  function(tag, arr) { this.writeMessage(tag, writePackedBoolean, arr);  },
+    writePackedFloat:    function(tag, arr) { this.writeMessage(tag, writePackedFloat, arr);    },
+    writePackedDouble:   function(tag, arr) { this.writeMessage(tag, writePackedDouble, arr);   },
+    writePackedFixed32:  function(tag, arr) { this.writeMessage(tag, writePackedFixed32, arr);  },
+    writePackedSFixed32: function(tag, arr) { this.writeMessage(tag, writePackedSFixed32, arr); },
+    writePackedFixed64:  function(tag, arr) { this.writeMessage(tag, writePackedFixed64, arr);  },
+    writePackedSFixed64: function(tag, arr) { this.writeMessage(tag, writePackedSFixed64, arr); },
+
+    writeBytesField: function(tag, buffer) {
+        this.writeTag(tag, Pbf.Bytes);
+        this.writeBytes(buffer);
+    },
+    writeFixed32Field: function(tag, val) {
+        this.writeTag(tag, Pbf.Fixed32);
+        this.writeFixed32(val);
+    },
+    writeSFixed32Field: function(tag, val) {
+        this.writeTag(tag, Pbf.Fixed32);
+        this.writeSFixed32(val);
+    },
+    writeFixed64Field: function(tag, val) {
+        this.writeTag(tag, Pbf.Fixed64);
+        this.writeFixed64(val);
+    },
+    writeSFixed64Field: function(tag, val) {
+        this.writeTag(tag, Pbf.Fixed64);
+        this.writeSFixed64(val);
+    },
+    writeVarintField: function(tag, val) {
+        this.writeTag(tag, Pbf.Varint);
+        this.writeVarint(val);
+    },
+    writeSVarintField: function(tag, val) {
+        this.writeTag(tag, Pbf.Varint);
+        this.writeSVarint(val);
+    },
+    writeStringField: function(tag, str) {
+        this.writeTag(tag, Pbf.Bytes);
+        this.writeString(str);
+    },
+    writeFloatField: function(tag, val) {
+        this.writeTag(tag, Pbf.Fixed32);
+        this.writeFloat(val);
+    },
+    writeDoubleField: function(tag, val) {
+        this.writeTag(tag, Pbf.Fixed64);
+        this.writeDouble(val);
+    },
+    writeBooleanField: function(tag, val) {
+        this.writeVarintField(tag, Boolean(val));
+    }
+};
+
+function readVarintRemainder(val, pbf) {
+    var buf = pbf.buf, b;
+
+    b = buf[pbf.pos++]; val += (b & 0x7f) * 0x10000000;         if (b < 0x80) return val;
+    b = buf[pbf.pos++]; val += (b & 0x7f) * 0x800000000;        if (b < 0x80) return val;
+    b = buf[pbf.pos++]; val += (b & 0x7f) * 0x40000000000;      if (b < 0x80) return val;
+    b = buf[pbf.pos++]; val += (b & 0x7f) * 0x2000000000000;    if (b < 0x80) return val;
+    b = buf[pbf.pos++]; val += (b & 0x7f) * 0x100000000000000;  if (b < 0x80) return val;
+    b = buf[pbf.pos++]; val += (b & 0x7f) * 0x8000000000000000; if (b < 0x80) return val;
+
+    throw new Error('Expected varint not more than 10 bytes');
+}
+
+function writeBigVarint(val, pbf) {
+    pbf.realloc(10);
+
+    var maxPos = pbf.pos + 10;
+
+    while (val >= 1) {
+        if (pbf.pos >= maxPos) throw new Error('Given varint doesn\'t fit into 10 bytes');
+        var b = val & 0xff;
+        pbf.buf[pbf.pos++] = b | (val >= 0x80 ? 0x80 : 0);
+        val /= 0x80;
+    }
+}
+
+function reallocForRawMessage(startPos, len, pbf) {
+    var extraLen =
+        len <= 0x3fff ? 1 :
+        len <= 0x1fffff ? 2 :
+        len <= 0xfffffff ? 3 : Math.ceil(Math.log(len) / (Math.LN2 * 7));
+
+    // if 1 byte isn't enough for encoding message length, shift the data to the right
+    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 writePackedBoolean(arr, pbf)  { for (var i = 0; i < arr.length; i++) pbf.writeBoolean(arr[i]);  }
+function writePackedFixed32(arr, pbf)  { for (var i = 0; i < arr.length; i++) pbf.writeFixed32(arr[i]);  }
+function writePackedSFixed32(arr, pbf) { for (var i = 0; i < arr.length; i++) pbf.writeSFixed32(arr[i]); }
+function writePackedFixed64(arr, pbf)  { for (var i = 0; i < arr.length; i++) pbf.writeFixed64(arr[i]);  }
+function writePackedSFixed64(arr, pbf) { for (var i = 0; i < arr.length; i++) pbf.writeSFixed64(arr[i]); }
+
+}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
+},{"./buffer":2}]},{},[3])(3)
+});
+ol.ext.pbf = module.exports;
+})();
+
+goog.provide('ol.ext.vectortile');
+/** @typedef {function(*)} */
+ol.ext.vectortile;
+(function() {
+var exports = {};
+var module = {exports: exports};
+var define;
+/**
+ * @fileoverview
+ * @suppress {accessControls, ambiguousFunctionDecl, checkDebuggerStatement, checkRegExp, checkTypes, checkVars, const, constantProperty, deprecated, duplicate, es5Strict, fileoverviewTags, missingProperties, nonStandardJsDocs, strictModuleDepCheck, suspiciousCode, undefinedNames, undefinedVars, unknownDefines, uselessCode, visibility}
+ */
+(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.vectortile = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(_dereq_,module,exports){
+'use strict';
+
+module.exports = Point;
+
+function Point(x, y) {
+    this.x = x;
+    this.y = y;
+}
+
+Point.prototype = {
+    clone: function() { return new Point(this.x, this.y); },
+
+    add:     function(p) { return this.clone()._add(p);     },
+    sub:     function(p) { return this.clone()._sub(p);     },
+    mult:    function(k) { return this.clone()._mult(k);    },
+    div:     function(k) { return this.clone()._div(k);     },
+    rotate:  function(a) { return this.clone()._rotate(a);  },
+    matMult: function(m) { return this.clone()._matMult(m); },
+    unit:    function() { return this.clone()._unit(); },
+    perp:    function() { return this.clone()._perp(); },
+    round:   function() { return this.clone()._round(); },
+
+    mag: function() {
+        return Math.sqrt(this.x * this.x + this.y * this.y);
+    },
+
+    equals: function(p) {
+        return this.x === p.x &&
+               this.y === p.y;
+    },
+
+    dist: function(p) {
+        return Math.sqrt(this.distSqr(p));
+    },
+
+    distSqr: function(p) {
+        var dx = p.x - this.x,
+            dy = p.y - this.y;
+        return dx * dx + dy * dy;
+    },
+
+    angle: function() {
+        return Math.atan2(this.y, this.x);
+    },
+
+    angleTo: function(b) {
+        return Math.atan2(this.y - b.y, this.x - b.x);
+    },
+
+    angleWith: function(b) {
+        return this.angleWithSep(b.x, b.y);
+    },
+
+    // Find the angle of the two vectors, solving the formula for the cross product a x b = |a||b|sin(θ) for θ.
+    angleWithSep: function(x, y) {
+        return Math.atan2(
+            this.x * y - this.y * x,
+            this.x * x + this.y * y);
+    },
+
+    _matMult: function(m) {
+        var x = m[0] * this.x + m[1] * this.y,
+            y = m[2] * this.x + m[3] * this.y;
+        this.x = x;
+        this.y = y;
+        return this;
+    },
+
+    _add: function(p) {
+        this.x += p.x;
+        this.y += p.y;
+        return this;
+    },
+
+    _sub: function(p) {
+        this.x -= p.x;
+        this.y -= p.y;
+        return this;
+    },
+
+    _mult: function(k) {
+        this.x *= k;
+        this.y *= k;
+        return this;
+    },
+
+    _div: function(k) {
+        this.x /= k;
+        this.y /= k;
+        return this;
+    },
+
+    _unit: function() {
+        this._div(this.mag());
+        return this;
+    },
+
+    _perp: function() {
+        var y = this.y;
+        this.y = this.x;
+        this.x = -y;
+        return this;
+    },
+
+    _rotate: function(angle) {
+        var cos = Math.cos(angle),
+            sin = Math.sin(angle),
+            x = cos * this.x - sin * this.y,
+            y = sin * this.x + cos * this.y;
+        this.x = x;
+        this.y = y;
+        return this;
+    },
+
+    _round: function() {
+        this.x = Math.round(this.x);
+        this.y = Math.round(this.y);
+        return this;
+    }
+};
+
+// constructs Point from an array if necessary
+Point.convert = function (a) {
+    if (a instanceof Point) {
+        return a;
+    }
+    if (Array.isArray(a)) {
+        return new Point(a[0], a[1]);
+    }
+    return a;
+};
+
+},{}],2:[function(_dereq_,module,exports){
+module.exports.VectorTile = _dereq_('./lib/vectortile.js');
+module.exports.VectorTileFeature = _dereq_('./lib/vectortilefeature.js');
+module.exports.VectorTileLayer = _dereq_('./lib/vectortilelayer.js');
+
+},{"./lib/vectortile.js":3,"./lib/vectortilefeature.js":4,"./lib/vectortilelayer.js":5}],3:[function(_dereq_,module,exports){
+'use strict';
+
+var VectorTileLayer = _dereq_('./vectortilelayer');
+
+module.exports = VectorTile;
+
+function VectorTile(pbf, end) {
+    this.layers = pbf.readFields(readTile, {}, end);
+}
+
+function readTile(tag, layers, pbf) {
+    if (tag === 3) {
+        var layer = new VectorTileLayer(pbf, pbf.readVarint() + pbf.pos);
+        if (layer.length) layers[layer.name] = layer;
+    }
+}
+
+
+},{"./vectortilelayer":5}],4:[function(_dereq_,module,exports){
+'use strict';
+
+var Point = _dereq_('point-geometry');
+
+module.exports = VectorTileFeature;
+
+function VectorTileFeature(pbf, end, extent, keys, values) {
+    // Public
+    this.properties = {};
+    this.extent = extent;
+    this.type = 0;
+
+    // Private
+    this._pbf = pbf;
+    this._geometry = -1;
+    this._keys = keys;
+    this._values = values;
+
+    pbf.readFields(readFeature, this, end);
+}
+
+function readFeature(tag, feature, pbf) {
+    if (tag == 1) feature._id = pbf.readVarint();
+    else if (tag == 2) readTag(pbf, feature);
+    else if (tag == 3) feature.type = pbf.readVarint();
+    else if (tag == 4) feature._geometry = pbf.pos;
+}
+
+function readTag(pbf, feature) {
+    var end = pbf.readVarint() + pbf.pos;
+
+    while (pbf.pos < end) {
+        var key = feature._keys[pbf.readVarint()],
+            value = feature._values[pbf.readVarint()];
+        feature.properties[key] = value;
+    }
+}
+
+VectorTileFeature.types = ['Unknown', 'Point', 'LineString', 'Polygon'];
+
+VectorTileFeature.prototype.loadGeometry = function() {
+    var pbf = this._pbf;
+    pbf.pos = this._geometry;
+
+    var end = pbf.readVarint() + pbf.pos,
+        cmd = 1,
+        length = 0,
+        x = 0,
+        y = 0,
+        lines = [],
+        line;
+
+    while (pbf.pos < end) {
+        if (!length) {
+            var cmdLen = pbf.readVarint();
+            cmd = cmdLen & 0x7;
+            length = cmdLen >> 3;
+        }
+
+        length--;
+
+        if (cmd === 1 || cmd === 2) {
+            x += pbf.readSVarint();
+            y += pbf.readSVarint();
+
+            if (cmd === 1) { // moveTo
+                if (line) lines.push(line);
+                line = [];
+            }
+
+            line.push(new Point(x, y));
+
+        } else if (cmd === 7) {
+
+            // Workaround for https://github.com/mapbox/mapnik-vector-tile/issues/90
+            if (line) {
+                line.push(line[0].clone()); // closePolygon
+            }
+
+        } else {
+            throw new Error('unknown command ' + cmd);
+        }
+    }
+
+    if (line) lines.push(line);
+
+    return lines;
+};
+
+VectorTileFeature.prototype.bbox = function() {
+    var pbf = this._pbf;
+    pbf.pos = this._geometry;
+
+    var end = pbf.readVarint() + pbf.pos,
+        cmd = 1,
+        length = 0,
+        x = 0,
+        y = 0,
+        x1 = Infinity,
+        x2 = -Infinity,
+        y1 = Infinity,
+        y2 = -Infinity;
+
+    while (pbf.pos < end) {
+        if (!length) {
+            var cmdLen = pbf.readVarint();
+            cmd = cmdLen & 0x7;
+            length = cmdLen >> 3;
+        }
+
+        length--;
+
+        if (cmd === 1 || cmd === 2) {
+            x += pbf.readSVarint();
+            y += pbf.readSVarint();
+            if (x < x1) x1 = x;
+            if (x > x2) x2 = x;
+            if (y < y1) y1 = y;
+            if (y > y2) y2 = y;
+
+        } else if (cmd !== 7) {
+            throw new Error('unknown command ' + cmd);
+        }
+    }
+
+    return [x1, y1, x2, y2];
+};
+
+VectorTileFeature.prototype.toGeoJSON = function(x, y, z) {
+    var size = this.extent * Math.pow(2, z),
+        x0 = this.extent * x,
+        y0 = this.extent * y,
+        coords = this.loadGeometry(),
+        type = VectorTileFeature.types[this.type],
+        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;
+};
+
+// classifies an array of rings into polygons with outer rings and holes
+
+function classifyRings(rings) {
+    var len = rings.length;
+
+    if (len <= 1) return [rings];
+
+    var polygons = [],
+        polygon,
+        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 (polygon) polygons.push(polygon);
+            polygon = [rings[i]];
+
+        } else {
+            polygon.push(rings[i]);
+        }
+    }
+    if (polygon) polygons.push(polygon);
+
+    return polygons;
+}
+
+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;
+}
+
+},{"point-geometry":1}],5:[function(_dereq_,module,exports){
+'use strict';
+
+var VectorTileFeature = _dereq_('./vectortilefeature.js');
+
+module.exports = VectorTileLayer;
+
+function VectorTileLayer(pbf, end) {
+    // Public
+    this.version = 1;
+    this.name = null;
+    this.extent = 4096;
+    this.length = 0;
+
+    // Private
+    this._pbf = pbf;
+    this._keys = [];
+    this._values = [];
+    this._features = [];
+
+    pbf.readFields(readLayer, this, end);
+
+    this.length = this._features.length;
+}
+
+function readLayer(tag, layer, pbf) {
+    if (tag === 15) layer.version = pbf.readVarint();
+    else if (tag === 1) layer.name = pbf.readString();
+    else if (tag === 5) layer.extent = pbf.readVarint();
+    else if (tag === 2) layer._features.push(pbf.pos);
+    else if (tag === 3) layer._keys.push(pbf.readString());
+    else if (tag === 4) layer._values.push(readValueMessage(pbf));
+}
+
+function readValueMessage(pbf) {
+    var value = null,
+        end = pbf.readVarint() + pbf.pos;
+
+    while (pbf.pos < end) {
+        var tag = pbf.readVarint() >> 3;
+
+        value = tag === 1 ? pbf.readString() :
+            tag === 2 ? pbf.readFloat() :
+            tag === 3 ? pbf.readDouble() :
+            tag === 4 ? pbf.readVarint64() :
+            tag === 5 ? pbf.readVarint() :
+            tag === 6 ? pbf.readSVarint() :
+            tag === 7 ? pbf.readBoolean() : null;
+    }
+
+    return value;
+}
+
+// return feature `i` from this layer as a `VectorTileFeature`
+VectorTileLayer.prototype.feature = function(i) {
+    if (i < 0 || i >= this._features.length) throw new Error('feature index out of bounds');
+
+    this._pbf.pos = this._features[i];
+
+    var end = this._pbf.readVarint() + this._pbf.pos;
+    return new VectorTileFeature(this._pbf, end, this.extent, this._keys, this._values);
+};
+
+},{"./vectortilefeature.js":4}]},{},[2])(2)
+});
+ol.ext.vectortile = module.exports;
+})();
+
+//FIXME Implement projection handling
+
+goog.provide('ol.format.MVT');
+
+goog.require('goog.asserts');
+goog.require('ol.Feature');
+goog.require('ol.ext.pbf');
+goog.require('ol.ext.vectortile');
+goog.require('ol.format.Feature');
+goog.require('ol.format.FormatType');
+goog.require('ol.geom.Geometry');
+goog.require('ol.geom.GeometryLayout');
+goog.require('ol.geom.GeometryType');
+goog.require('ol.geom.LineString');
+goog.require('ol.geom.MultiLineString');
+goog.require('ol.geom.MultiPoint');
+goog.require('ol.geom.Point');
+goog.require('ol.geom.Polygon');
+goog.require('ol.proj');
+goog.require('ol.proj.Projection');
+goog.require('ol.proj.Units');
+goog.require('ol.render.Feature');
+
+
+/**
+ * @classdesc
+ * Feature format for reading data in the Mapbox MVT format.
+ *
+ * @constructor
+ * @extends {ol.format.Feature}
+ * @param {olx.format.MVTOptions=} opt_options Options.
+ * @api
+ */
+ol.format.MVT = function(opt_options) {
+
+  ol.format.Feature.call(this);
+
+  var options = opt_options ? opt_options : {};
+
+  /**
+   * @type {ol.proj.Projection}
+   */
+  this.defaultDataProjection = new ol.proj.Projection({
+    code: '',
+    units: ol.proj.Units.TILE_PIXELS
+  });
+
+  /**
+   * @private
+   * @type {function((ol.geom.Geometry|Object.<string, *>)=)|
+   *     function(ol.geom.GeometryType,Array.<number>,
+   *         (Array.<number>|Array.<Array.<number>>),Object.<string, *>)}
+   */
+  this.featureClass_ = options.featureClass ?
+      options.featureClass : ol.render.Feature;
+
+  /**
+   * @private
+   * @type {string}
+   */
+  this.geometryName_ = options.geometryName ?
+      options.geometryName : 'geometry';
+
+  /**
+   * @private
+   * @type {string}
+   */
+  this.layerName_ = options.layerName ? options.layerName : 'layer';
+
+  /**
+   * @private
+   * @type {Array.<string>}
+   */
+  this.layers_ = options.layers ? options.layers : null;
+
+};
+ol.inherits(ol.format.MVT, ol.format.Feature);
+
+
+/**
+ * @inheritDoc
+ */
+ol.format.MVT.prototype.getType = function() {
+  return ol.format.FormatType.ARRAY_BUFFER;
+};
+
+
+/**
+ * @private
+ * @param {Object} rawFeature Raw Mapbox feature.
+ * @param {string} layer Layer.
+ * @param {olx.format.ReadOptions=} opt_options Read options.
+ * @return {ol.Feature} Feature.
+ */
+ol.format.MVT.prototype.readFeature_ = function(
+    rawFeature, layer, opt_options) {
+  var feature = new this.featureClass_();
+  var values = rawFeature.properties;
+  values[this.layerName_] = layer;
+  var geometry = ol.format.Feature.transformWithOptions(
+      ol.format.MVT.readGeometry_(rawFeature), false,
+      this.adaptOptions(opt_options));
+  if (geometry) {
+    goog.asserts.assertInstanceof(geometry, ol.geom.Geometry);
+    values[this.geometryName_] = geometry;
+  }
+  feature.setProperties(values);
+  feature.setGeometryName(this.geometryName_);
+  return feature;
+};
+
+
+/**
+ * @private
+ * @param {Object} rawFeature Raw Mapbox feature.
+ * @param {string} layer Layer.
+ * @return {ol.render.Feature} Feature.
+ */
+ol.format.MVT.prototype.readRenderFeature_ = function(rawFeature, layer) {
+  var coords = rawFeature.loadGeometry();
+  var ends = [];
+  var flatCoordinates = [];
+  ol.format.MVT.calculateFlatCoordinates_(coords, flatCoordinates, ends);
+
+  var type = rawFeature.type;
+  /** @type {ol.geom.GeometryType} */
+  var geometryType;
+  if (type === 1) {
+    geometryType = coords.length === 1 ?
+        ol.geom.GeometryType.POINT : ol.geom.GeometryType.MULTI_POINT;
+  } else if (type === 2) {
+    if (coords.length === 1) {
+      geometryType = ol.geom.GeometryType.LINE_STRING;
+    } else {
+      geometryType = ol.geom.GeometryType.MULTI_LINE_STRING;
+    }
+  } else if (type === 3) {
+    geometryType = ol.geom.GeometryType.POLYGON;
+  }
+
+  var values = rawFeature.properties;
+  values[this.layerName_] = layer;
+
+  return new this.featureClass_(geometryType, flatCoordinates, ends, values);
+};
+
+
+/**
+ * @inheritDoc
+ * @api
+ */
+ol.format.MVT.prototype.readFeatures = function(source, opt_options) {
+  goog.asserts.assertInstanceof(source, ArrayBuffer);
+
+  var layers = this.layers_;
+
+  var pbf = new ol.ext.pbf(source);
+  var tile = new ol.ext.vectortile.VectorTile(pbf);
+  var features = [];
+  var featureClass = this.featureClass_;
+  var layer, feature;
+  for (var name in tile.layers) {
+    if (layers && layers.indexOf(name) == -1) {
+      continue;
+    }
+    layer = tile.layers[name];
+
+    for (var i = 0, ii = layer.length; i < ii; ++i) {
+      if (featureClass === ol.render.Feature) {
+        feature = this.readRenderFeature_(layer.feature(i), name);
+      } else {
+        feature = this.readFeature_(layer.feature(i), name, opt_options);
+      }
+      features.push(feature);
+    }
+  }
+
+  return features;
+};
+
+
+/**
+ * @inheritDoc
+ * @api
+ */
+ol.format.MVT.prototype.readProjection = function(source) {
+  return this.defaultDataProjection;
+};
+
+
+/**
+ * Sets the layers that features will be read from.
+ * @param {Array.<string>} layers Layers.
+ * @api
+ */
+ol.format.MVT.prototype.setLayers = function(layers) {
+  this.layers_ = layers;
+};
+
+
+/**
+ * @private
+ * @param {Object} coords Raw feature coordinates.
+ * @param {Array.<number>} flatCoordinates Flat coordinates to be populated by
+ *     this function.
+ * @param {Array.<number>} ends Ends to be populated by this function.
+ */
+ol.format.MVT.calculateFlatCoordinates_ = function(
+    coords, flatCoordinates, ends) {
+  var end = 0;
+  for (var i = 0, ii = coords.length; i < ii; ++i) {
+    var line = coords[i];
+    var j, jj;
+    for (j = 0, jj = line.length; j < jj; ++j) {
+      var coord = line[j];
+      // Non-tilespace coords can be calculated here when a TileGrid and
+      // TileCoord are known.
+      flatCoordinates.push(coord.x, coord.y);
+    }
+    end += 2 * j;
+    ends.push(end);
+  }
+};
+
+
+/**
+ * @private
+ * @param {Object} rawFeature Raw Mapbox feature.
+ * @return {ol.geom.Geometry} Geometry.
+ */
+ol.format.MVT.readGeometry_ = function(rawFeature) {
+  var type = rawFeature.type;
+  if (type === 0) {
+    return null;
+  }
+
+  var coords = rawFeature.loadGeometry();
+  var ends = [];
+  var flatCoordinates = [];
+  ol.format.MVT.calculateFlatCoordinates_(coords, flatCoordinates, ends);
+
+  var geom;
+  if (type === 1) {
+    geom = coords.length === 1 ?
+        new ol.geom.Point(null) : new ol.geom.MultiPoint(null);
+  } else if (type === 2) {
+    if (coords.length === 1) {
+      geom = new ol.geom.LineString(null);
+    } else {
+      geom = new ol.geom.MultiLineString(null);
+    }
+  } else if (type === 3) {
+    geom = new ol.geom.Polygon(null);
+  }
+
+  geom.setFlatCoordinates(ol.geom.GeometryLayout.XY, flatCoordinates,
+      ends);
+
+  return geom;
+};
+
+goog.provide('ol.format.ogc.filter');
+goog.provide('ol.format.ogc.filter.Filter');
+goog.provide('ol.format.ogc.filter.Logical');
+goog.provide('ol.format.ogc.filter.LogicalBinary');
+goog.provide('ol.format.ogc.filter.And');
+goog.provide('ol.format.ogc.filter.Or');
+goog.provide('ol.format.ogc.filter.Not');
+goog.provide('ol.format.ogc.filter.Bbox');
+goog.provide('ol.format.ogc.filter.Comparison');
+goog.provide('ol.format.ogc.filter.ComparisonBinary');
+goog.provide('ol.format.ogc.filter.EqualTo');
+goog.provide('ol.format.ogc.filter.NotEqualTo');
+goog.provide('ol.format.ogc.filter.LessThan');
+goog.provide('ol.format.ogc.filter.LessThanOrEqualTo');
+goog.provide('ol.format.ogc.filter.GreaterThan');
+goog.provide('ol.format.ogc.filter.GreaterThanOrEqualTo');
+goog.provide('ol.format.ogc.filter.IsNull');
+goog.provide('ol.format.ogc.filter.IsBetween');
+goog.provide('ol.format.ogc.filter.IsLike');
+
+
+/**
+ * Create a logical `<And>` operator between two filter conditions.
+ *
+ * @param {!ol.format.ogc.filter.Filter} conditionA First filter condition.
+ * @param {!ol.format.ogc.filter.Filter} conditionB Second filter condition.
+ * @returns {!ol.format.ogc.filter.And} `<And>` operator.
+ * @api
+ */
+ol.format.ogc.filter.and = function(conditionA, conditionB) {
+  return new ol.format.ogc.filter.And(conditionA, conditionB);
+};
+
+
+/**
+ * Create a logical `<Or>` operator between two filter conditions.
+ *
+ * @param {!ol.format.ogc.filter.Filter} conditionA First filter condition.
+ * @param {!ol.format.ogc.filter.Filter} conditionB Second filter condition.
+ * @returns {!ol.format.ogc.filter.Or} `<Or>` operator.
+ * @api
+ */
+ol.format.ogc.filter.or = function(conditionA, conditionB) {
+  return new ol.format.ogc.filter.Or(conditionA, conditionB);
+};
+
+
+/**
+ * Represents a logical `<Not>` operator for a filter condition.
+ *
+ * @param {!ol.format.ogc.filter.Filter} condition Filter condition.
+ * @returns {!ol.format.ogc.filter.Not} `<Not>` operator.
+ * @api
+ */
+ol.format.ogc.filter.not = function(condition) {
+  return new ol.format.ogc.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 {!ol.Extent} extent Extent.
+ * @param {string=} opt_srsName SRS name. No srsName attribute will be
+ *    set on geometries when this is not provided.
+ * @returns {!ol.format.ogc.filter.Bbox} `<BBOX>` operator.
+ * @api
+ */
+ol.format.ogc.filter.bbox = function(geometryName, extent, opt_srsName) {
+  return new ol.format.ogc.filter.Bbox(geometryName, extent, 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 {boolean=} opt_matchCase Case-sensitive?
+ * @returns {!ol.format.ogc.filter.EqualTo} `<PropertyIsEqualTo>` operator.
+ * @api
+ */
+ol.format.ogc.filter.equalTo = function(propertyName, expression, opt_matchCase) {
+  return new ol.format.ogc.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 {boolean=} opt_matchCase Case-sensitive?
+ * @returns {!ol.format.ogc.filter.NotEqualTo} `<PropertyIsNotEqualTo>` operator.
+ * @api
+ */
+ol.format.ogc.filter.notEqualTo = function(propertyName, expression, opt_matchCase) {
+  return new ol.format.ogc.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 {!ol.format.ogc.filter.LessThan} `<PropertyIsLessThan>` operator.
+ * @api
+ */
+ol.format.ogc.filter.lessThan = function(propertyName, expression) {
+  return new ol.format.ogc.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 {!ol.format.ogc.filter.LessThanOrEqualTo} `<PropertyIsLessThanOrEqualTo>` operator.
+ * @api
+ */
+ol.format.ogc.filter.lessThanOrEqualTo = function(propertyName, expression) {
+  return new ol.format.ogc.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 {!ol.format.ogc.filter.GreaterThan} `<PropertyIsGreaterThan>` operator.
+ * @api
+ */
+ol.format.ogc.filter.greaterThan = function(propertyName, expression) {
+  return new ol.format.ogc.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 {!ol.format.ogc.filter.GreaterThanOrEqualTo} `<PropertyIsGreaterThanOrEqualTo>` operator.
+ * @api
+ */
+ol.format.ogc.filter.greaterThanOrEqualTo = function(propertyName, expression) {
+  return new ol.format.ogc.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 {!ol.format.ogc.filter.IsNull} `<PropertyIsNull>` operator.
+ * @api
+ */
+ol.format.ogc.filter.isNull = function(propertyName) {
+  return new ol.format.ogc.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 {!ol.format.ogc.filter.IsBetween} `<PropertyIsBetween>` operator.
+ * @api
+ */
+ol.format.ogc.filter.between = function(propertyName, lowerBoundary, upperBoundary) {
+  return new ol.format.ogc.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 {boolean=} opt_matchCase Case-sensitive?
+ * @returns {!ol.format.ogc.filter.IsLike} `<PropertyIsLike>` operator.
+ * @api
+ */
+ol.format.ogc.filter.like = function(propertyName, pattern,
+    opt_wildCard, opt_singleChar, opt_escapeChar, opt_matchCase) {
+  return new ol.format.ogc.filter.IsLike(propertyName, pattern,
+    opt_wildCard, opt_singleChar, opt_escapeChar, opt_matchCase);
+};
+
+
+/**
+ * @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
+ */
+ol.format.ogc.filter.Filter = function(tagName) {
+
+  /**
+   * @private
+   * @type {!string}
+   */
+  this.tagName_ = tagName;
+};
+
+/**
+ * The XML tag name for a filter.
+ * @returns {!string} Name.
+ */
+ol.format.ogc.filter.Filter.prototype.getTagName = function() {
+  return this.tagName_;
+};
+
+
+// Logical filters
+
+
+/**
+ * @classdesc
+ * Abstract class; normally only used for creating subclasses and not instantiated in apps.
+ * Base class for WFS GetFeature logical filters.
+ *
+ * @constructor
+ * @param {!string} tagName The XML tag name for this filter.
+ * @extends {ol.format.ogc.filter.Filter}
+ */
+ol.format.ogc.filter.Logical = function(tagName) {
+  ol.format.ogc.filter.Filter.call(this, tagName);
+};
+ol.inherits(ol.format.ogc.filter.Logical, ol.format.ogc.filter.Filter);
+
+
+/**
+ * @classdesc
+ * Abstract class; normally only used for creating subclasses and not instantiated in apps.
+ * Base class for WFS GetFeature binary logical filters.
+ *
+ * @constructor
+ * @param {!string} tagName The XML tag name for this filter.
+ * @param {!ol.format.ogc.filter.Filter} conditionA First filter condition.
+ * @param {!ol.format.ogc.filter.Filter} conditionB Second filter condition.
+ * @extends {ol.format.ogc.filter.Logical}
+ */
+ol.format.ogc.filter.LogicalBinary = function(tagName, conditionA, conditionB) {
+
+  ol.format.ogc.filter.Logical.call(this, tagName);
+
+  /**
+   * @public
+   * @type {!ol.format.ogc.filter.Filter}
+   */
+  this.conditionA = conditionA;
+
+  /**
+   * @public
+   * @type {!ol.format.ogc.filter.Filter}
+   */
+  this.conditionB = conditionB;
+
+};
+ol.inherits(ol.format.ogc.filter.LogicalBinary, ol.format.ogc.filter.Logical);
+
+
+/**
+ * @classdesc
+ * Represents a logical `<And>` operator between two filter conditions.
+ *
+ * @constructor
+ * @param {!ol.format.ogc.filter.Filter} conditionA First filter condition.
+ * @param {!ol.format.ogc.filter.Filter} conditionB Second filter condition.
+ * @extends {ol.format.ogc.filter.LogicalBinary}
+ * @api
+ */
+ol.format.ogc.filter.And = function(conditionA, conditionB) {
+  ol.format.ogc.filter.LogicalBinary.call(this, 'And', conditionA, conditionB);
+};
+ol.inherits(ol.format.ogc.filter.And, ol.format.ogc.filter.LogicalBinary);
+
+
+/**
+ * @classdesc
+ * Represents a logical `<Or>` operator between two filter conditions.
+ *
+ * @constructor
+ * @param {!ol.format.ogc.filter.Filter} conditionA First filter condition.
+ * @param {!ol.format.ogc.filter.Filter} conditionB Second filter condition.
+ * @extends {ol.format.ogc.filter.LogicalBinary}
+ * @api
+ */
+ol.format.ogc.filter.Or = function(conditionA, conditionB) {
+  ol.format.ogc.filter.LogicalBinary.call(this, 'Or', conditionA, conditionB);
+};
+ol.inherits(ol.format.ogc.filter.Or, ol.format.ogc.filter.LogicalBinary);
+
+
+/**
+ * @classdesc
+ * Represents a logical `<Not>` operator for a filter condition.
+ *
+ * @constructor
+ * @param {!ol.format.ogc.filter.Filter} condition Filter condition.
+ * @extends {ol.format.ogc.filter.Logical}
+ * @api
+ */
+ol.format.ogc.filter.Not = function(condition) {
+
+  ol.format.ogc.filter.Logical.call(this, 'Not');
+
+  /**
+   * @public
+   * @type {!ol.format.ogc.filter.Filter}
+   */
+  this.condition = condition;
+};
+ol.inherits(ol.format.ogc.filter.Not, ol.format.ogc.filter.Logical);
+
+
+// Spatial filters
+
+
+/**
+ * @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 {!ol.Extent} extent Extent.
+ * @param {string=} opt_srsName SRS name. No srsName attribute will be
+ *    set on geometries when this is not provided.
+ * @extends {ol.format.ogc.filter.Filter}
+ * @api
+ */
+ol.format.ogc.filter.Bbox = function(geometryName, extent, opt_srsName) {
+
+  ol.format.ogc.filter.Filter.call(this, 'BBOX');
+
+  /**
+   * @public
+   * @type {!string}
+   */
+  this.geometryName = geometryName;
+
+  /**
+   * @public
+   * @type {ol.Extent}
+   */
+  this.extent = extent;
+
+  /**
+   * @public
+   * @type {string|undefined}
+   */
+  this.srsName = opt_srsName;
+};
+ol.inherits(ol.format.ogc.filter.Bbox, ol.format.ogc.filter.Filter);
+
+
+// Property comparison filters
+
+
+/**
+ * @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 {ol.format.ogc.filter.Filter}
+ * @api
+ */
+ol.format.ogc.filter.Comparison = function(tagName, propertyName) {
+
+  ol.format.ogc.filter.Filter.call(this, tagName);
+
+  /**
+   * @public
+   * @type {!string}
+   */
+  this.propertyName = propertyName;
+};
+ol.inherits(ol.format.ogc.filter.Comparison, ol.format.ogc.filter.Filter);
+
+
+/**
+ * @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 {boolean=} opt_matchCase Case-sensitive?
+ * @extends {ol.format.ogc.filter.Comparison}
+ * @api
+ */
+ol.format.ogc.filter.ComparisonBinary = function(
+    tagName, propertyName, expression, opt_matchCase) {
+
+  ol.format.ogc.filter.Comparison.call(this, tagName, propertyName);
+
+  /**
+   * @public
+   * @type {!(string|number)}
+   */
+  this.expression = expression;
+
+  /**
+   * @public
+   * @type {boolean|undefined}
+   */
+  this.matchCase = opt_matchCase;
+};
+ol.inherits(ol.format.ogc.filter.ComparisonBinary, ol.format.ogc.filter.Comparison);
+
+
+/**
+ * @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 {boolean=} opt_matchCase Case-sensitive?
+ * @extends {ol.format.ogc.filter.ComparisonBinary}
+ * @api
+ */
+ol.format.ogc.filter.EqualTo = function(propertyName, expression, opt_matchCase) {
+  ol.format.ogc.filter.ComparisonBinary.call(this, 'PropertyIsEqualTo', propertyName, expression, opt_matchCase);
+};
+ol.inherits(ol.format.ogc.filter.EqualTo, ol.format.ogc.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 {boolean=} opt_matchCase Case-sensitive?
+ * @extends {ol.format.ogc.filter.ComparisonBinary}
+ * @api
+ */
+ol.format.ogc.filter.NotEqualTo = function(propertyName, expression, opt_matchCase) {
+  ol.format.ogc.filter.ComparisonBinary.call(this, 'PropertyIsNotEqualTo', propertyName, expression, opt_matchCase);
+};
+ol.inherits(ol.format.ogc.filter.NotEqualTo, ol.format.ogc.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 {ol.format.ogc.filter.ComparisonBinary}
+ * @api
+ */
+ol.format.ogc.filter.LessThan = function(propertyName, expression) {
+  ol.format.ogc.filter.ComparisonBinary.call(this, 'PropertyIsLessThan', propertyName, expression);
+};
+ol.inherits(ol.format.ogc.filter.LessThan, ol.format.ogc.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 {ol.format.ogc.filter.ComparisonBinary}
+ * @api
+ */
+ol.format.ogc.filter.LessThanOrEqualTo = function(propertyName, expression) {
+  ol.format.ogc.filter.ComparisonBinary.call(this, 'PropertyIsLessThanOrEqualTo', propertyName, expression);
+};
+ol.inherits(ol.format.ogc.filter.LessThanOrEqualTo, ol.format.ogc.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 {ol.format.ogc.filter.ComparisonBinary}
+ * @api
+ */
+ol.format.ogc.filter.GreaterThan = function(propertyName, expression) {
+  ol.format.ogc.filter.ComparisonBinary.call(this, 'PropertyIsGreaterThan', propertyName, expression);
+};
+ol.inherits(ol.format.ogc.filter.GreaterThan, ol.format.ogc.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 {ol.format.ogc.filter.ComparisonBinary}
+ * @api
+ */
+ol.format.ogc.filter.GreaterThanOrEqualTo = function(propertyName, expression) {
+  ol.format.ogc.filter.ComparisonBinary.call(this, 'PropertyIsGreaterThanOrEqualTo', propertyName, expression);
+};
+ol.inherits(ol.format.ogc.filter.GreaterThanOrEqualTo, ol.format.ogc.filter.ComparisonBinary);
+
+
+/**
+ * @classdesc
+ * Represents a `<PropertyIsNull>` comparison operator.
+ *
+ * @constructor
+ * @param {!string} propertyName Name of the context property to compare.
+ * @extends {ol.format.ogc.filter.Comparison}
+ * @api
+ */
+ol.format.ogc.filter.IsNull = function(propertyName) {
+  ol.format.ogc.filter.Comparison.call(this, 'PropertyIsNull', propertyName);
+};
+ol.inherits(ol.format.ogc.filter.IsNull, ol.format.ogc.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 {ol.format.ogc.filter.Comparison}
+ * @api
+ */
+ol.format.ogc.filter.IsBetween = function(propertyName, lowerBoundary, upperBoundary) {
+  ol.format.ogc.filter.Comparison.call(this, 'PropertyIsBetween', propertyName);
+
+  /**
+   * @public
+   * @type {!number}
+   */
+  this.lowerBoundary = lowerBoundary;
+
+  /**
+   * @public
+   * @type {!number}
+   */
+  this.upperBoundary = upperBoundary;
+};
+ol.inherits(ol.format.ogc.filter.IsBetween, ol.format.ogc.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 {boolean=} opt_matchCase Case-sensitive?
+ * @extends {ol.format.ogc.filter.Comparison}
+ * @api
+ */
+ol.format.ogc.filter.IsLike = function(propertyName, pattern,
+    opt_wildCard, opt_singleChar, opt_escapeChar, opt_matchCase) {
+  ol.format.ogc.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 {boolean|undefined}
+   */
+  this.matchCase = opt_matchCase;
+};
+ol.inherits(ol.format.ogc.filter.IsLike, ol.format.ogc.filter.Comparison);
+
+// FIXME add typedef for stack state objects
+goog.provide('ol.format.OSMXML');
+
+goog.require('goog.asserts');
+goog.require('ol.array');
+goog.require('ol.Feature');
+goog.require('ol.format.Feature');
+goog.require('ol.format.XMLFeature');
+goog.require('ol.geom.GeometryLayout');
+goog.require('ol.geom.LineString');
+goog.require('ol.geom.Point');
+goog.require('ol.geom.Polygon');
+goog.require('ol.object');
+goog.require('ol.proj');
+goog.require('ol.xml');
+
+
+/**
+ * @classdesc
+ * Feature format for reading data in the
+ * [OSMXML format](http://wiki.openstreetmap.org/wiki/OSM_XML).
+ *
+ * @constructor
+ * @extends {ol.format.XMLFeature}
+ * @api stable
+ */
+ol.format.OSMXML = function() {
+  ol.format.XMLFeature.call(this);
+
+  /**
+   * @inheritDoc
+   */
+  this.defaultDataProjection = ol.proj.get('EPSG:4326');
+};
+ol.inherits(ol.format.OSMXML, ol.format.XMLFeature);
+
+
+/**
+ * @const
+ * @type {Array.<string>}
+ * @private
+ */
+ol.format.OSMXML.EXTENSIONS_ = ['.osm'];
+
+
+/**
+ * @inheritDoc
+ */
+ol.format.OSMXML.prototype.getExtensions = function() {
+  return ol.format.OSMXML.EXTENSIONS_;
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {Array.<*>} objectStack Object stack.
+ * @private
+ */
+ol.format.OSMXML.readNode_ = function(node, objectStack) {
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'node.nodeType should be ELEMENT');
+  goog.asserts.assert(node.localName == 'node', 'localName should be node');
+  var options = /** @type {olx.format.ReadOptions} */ (objectStack[0]);
+  var state = /** @type {Object} */ (objectStack[objectStack.length - 1]);
+  var id = node.getAttribute('id');
+  /** @type {ol.Coordinate} */
+  var coordinates = [
+    parseFloat(node.getAttribute('lon')),
+    parseFloat(node.getAttribute('lat'))
+  ];
+  state.nodes[id] = coordinates;
+
+  var values = ol.xml.pushParseAndPop({
+    tags: {}
+  }, ol.format.OSMXML.NODE_PARSERS_, node, objectStack);
+  if (!ol.object.isEmpty(values.tags)) {
+    var geometry = new ol.geom.Point(coordinates);
+    ol.format.Feature.transformWithOptions(geometry, false, options);
+    var feature = new ol.Feature(geometry);
+    feature.setId(id);
+    feature.setProperties(values.tags);
+    state.features.push(feature);
+  }
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {Array.<*>} objectStack Object stack.
+ * @private
+ */
+ol.format.OSMXML.readWay_ = function(node, objectStack) {
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'node.nodeType should be ELEMENT');
+  goog.asserts.assert(node.localName == 'way', 'localName should be way');
+  var options = /** @type {olx.format.ReadOptions} */ (objectStack[0]);
+  var id = node.getAttribute('id');
+  var values = ol.xml.pushParseAndPop({
+    ndrefs: [],
+    tags: {}
+  }, ol.format.OSMXML.WAY_PARSERS_, node, objectStack);
+  var state = /** @type {Object} */ (objectStack[objectStack.length - 1]);
+  /** @type {Array.<number>} */
+  var flatCoordinates = [];
+  for (var i = 0, ii = values.ndrefs.length; i < ii; i++) {
+    var point = state.nodes[values.ndrefs[i]];
+    ol.array.extend(flatCoordinates, point);
+  }
+  var geometry;
+  if (values.ndrefs[0] == values.ndrefs[values.ndrefs.length - 1]) {
+    // closed way
+    geometry = new ol.geom.Polygon(null);
+    geometry.setFlatCoordinates(ol.geom.GeometryLayout.XY, flatCoordinates,
+        [flatCoordinates.length]);
+  } else {
+    geometry = new ol.geom.LineString(null);
+    geometry.setFlatCoordinates(ol.geom.GeometryLayout.XY, flatCoordinates);
+  }
+  ol.format.Feature.transformWithOptions(geometry, false, options);
+  var feature = new ol.Feature(geometry);
+  feature.setId(id);
+  feature.setProperties(values.tags);
+  state.features.push(feature);
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {Array.<*>} objectStack Object stack.
+ * @private
+ */
+ol.format.OSMXML.readNd_ = function(node, objectStack) {
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'node.nodeType should be ELEMENT');
+  goog.asserts.assert(node.localName == 'nd', 'localName should be nd');
+  var values = /** @type {Object} */ (objectStack[objectStack.length - 1]);
+  values.ndrefs.push(node.getAttribute('ref'));
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {Array.<*>} objectStack Object stack.
+ * @private
+ */
+ol.format.OSMXML.readTag_ = function(node, objectStack) {
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'node.nodeType should be ELEMENT');
+  goog.asserts.assert(node.localName == 'tag', 'localName should be tag');
+  var values = /** @type {Object} */ (objectStack[objectStack.length - 1]);
+  values.tags[node.getAttribute('k')] = node.getAttribute('v');
+};
+
+
+/**
+ * @const
+ * @private
+ * @type {Array.<string>}
+ */
+ol.format.OSMXML.NAMESPACE_URIS_ = [
+  null
+];
+
+
+/**
+ * @const
+ * @type {Object.<string, Object.<string, ol.XmlParser>>}
+ * @private
+ */
+ol.format.OSMXML.WAY_PARSERS_ = ol.xml.makeStructureNS(
+    ol.format.OSMXML.NAMESPACE_URIS_, {
+      'nd': ol.format.OSMXML.readNd_,
+      'tag': ol.format.OSMXML.readTag_
+    });
+
+
+/**
+ * @const
+ * @type {Object.<string, Object.<string, ol.XmlParser>>}
+ * @private
+ */
+ol.format.OSMXML.PARSERS_ = ol.xml.makeStructureNS(
+    ol.format.OSMXML.NAMESPACE_URIS_, {
+      'node': ol.format.OSMXML.readNode_,
+      'way': ol.format.OSMXML.readWay_
+    });
+
+
+/**
+ * @const
+ * @type {Object.<string, Object.<string, ol.XmlParser>>}
+ * @private
+ */
+ol.format.OSMXML.NODE_PARSERS_ = ol.xml.makeStructureNS(
+    ol.format.OSMXML.NAMESPACE_URIS_, {
+      'tag': ol.format.OSMXML.readTag_
+    });
+
+
+/**
+ * Read all features from an OSM source.
+ *
+ * @function
+ * @param {Document|Node|Object|string} source Source.
+ * @param {olx.format.ReadOptions=} opt_options Read options.
+ * @return {Array.<ol.Feature>} Features.
+ * @api stable
+ */
+ol.format.OSMXML.prototype.readFeatures;
+
+
+/**
+ * @inheritDoc
+ */
+ol.format.OSMXML.prototype.readFeaturesFromNode = function(node, opt_options) {
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'node.nodeType should be ELEMENT');
+  var options = this.getReadOptions(node, opt_options);
+  if (node.localName == 'osm') {
+    var state = ol.xml.pushParseAndPop({
+      nodes: {},
+      features: []
+    }, ol.format.OSMXML.PARSERS_, node, [options]);
+    if (state.features) {
+      return state.features;
+    }
+  }
+  return [];
+};
+
+
+/**
+ * Read the projection from an OSM source.
+ *
+ * @function
+ * @param {Document|Node|Object|string} source Source.
+ * @return {ol.proj.Projection} Projection.
+ * @api stable
+ */
+ol.format.OSMXML.prototype.readProjection;
+
+goog.provide('ol.format.XLink');
+
+
+/**
+ * @const
+ * @type {string}
+ */
+ol.format.XLink.NAMESPACE_URI = 'http://www.w3.org/1999/xlink';
+
+
+/**
+ * @param {Node} node Node.
+ * @return {boolean|undefined} Boolean.
+ */
+ol.format.XLink.readHref = function(node) {
+  return node.getAttributeNS(ol.format.XLink.NAMESPACE_URI, 'href');
+};
+
+goog.provide('ol.format.XML');
+
+goog.require('goog.asserts');
+goog.require('ol.xml');
+
+
+/**
+ * @classdesc
+ * Generic format for reading non-feature XML data
+ *
+ * @constructor
+ * @struct
+ */
+ol.format.XML = function() {
+};
+
+
+/**
+ * @param {Document|Node|string} source Source.
+ * @return {Object} The parsed result.
+ */
+ol.format.XML.prototype.read = function(source) {
+  if (ol.xml.isDocument(source)) {
+    return this.readFromDocument(/** @type {Document} */ (source));
+  } else if (ol.xml.isNode(source)) {
+    return this.readFromNode(/** @type {Node} */ (source));
+  } else if (typeof source === 'string') {
+    var doc = ol.xml.parse(source);
+    return this.readFromDocument(doc);
+  } else {
+    goog.asserts.fail();
+    return null;
+  }
+};
+
+
+/**
+ * @param {Document} doc Document.
+ * @return {Object}
+ */
+ol.format.XML.prototype.readFromDocument = goog.abstractMethod;
+
+
+/**
+ * @param {Node} node Node.
+ * @return {Object}
+ */
+ol.format.XML.prototype.readFromNode = goog.abstractMethod;
+
+goog.provide('ol.format.OWS');
+
+goog.require('goog.asserts');
+goog.require('ol.format.XLink');
+goog.require('ol.format.XML');
+goog.require('ol.format.XSD');
+goog.require('ol.xml');
+
+
+/**
+ * @constructor
+ * @extends {ol.format.XML}
+ */
+ol.format.OWS = function() {
+  ol.format.XML.call(this);
+};
+ol.inherits(ol.format.OWS, ol.format.XML);
+
+
+/**
+ * @param {Document} doc Document.
+ * @return {Object} OWS object.
+ */
+ol.format.OWS.prototype.readFromDocument = function(doc) {
+  goog.asserts.assert(doc.nodeType == Node.DOCUMENT_NODE,
+      'doc.nodeType should be DOCUMENT');
+  for (var n = doc.firstChild; n; n = n.nextSibling) {
+    if (n.nodeType == Node.ELEMENT_NODE) {
+      return this.readFromNode(n);
+    }
+  }
+  return null;
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @return {Object} OWS object.
+ */
+ol.format.OWS.prototype.readFromNode = function(node) {
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'node.nodeType should be ELEMENT');
+  var owsObject = ol.xml.pushParseAndPop({},
+      ol.format.OWS.PARSERS_, node, []);
+  return owsObject ? owsObject : null;
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {Array.<*>} objectStack Object stack.
+ * @private
+ * @return {Object|undefined} The address.
+ */
+ol.format.OWS.readAddress_ = function(node, objectStack) {
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'node.nodeType should be ELEMENT');
+  goog.asserts.assert(node.localName == 'Address',
+      'localName should be Address');
+  return ol.xml.pushParseAndPop({},
+      ol.format.OWS.ADDRESS_PARSERS_, node, objectStack);
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {Array.<*>} objectStack Object stack.
+ * @private
+ * @return {Object|undefined} The values.
+ */
+ol.format.OWS.readAllowedValues_ = function(node, objectStack) {
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'node.nodeType should be ELEMENT');
+  goog.asserts.assert(node.localName == 'AllowedValues',
+      'localName should be AllowedValues');
+  return ol.xml.pushParseAndPop({},
+      ol.format.OWS.ALLOWED_VALUES_PARSERS_, node, objectStack);
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {Array.<*>} objectStack Object stack.
+ * @private
+ * @return {Object|undefined} The constraint.
+ */
+ol.format.OWS.readConstraint_ = function(node, objectStack) {
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'node.nodeType should be ELEMENT');
+  goog.asserts.assert(node.localName == 'Constraint',
+      'localName should be Constraint');
+  var name = node.getAttribute('name');
+  if (!name) {
+    return undefined;
+  }
+  return ol.xml.pushParseAndPop({'name': name},
+      ol.format.OWS.CONSTRAINT_PARSERS_, node,
+      objectStack);
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {Array.<*>} objectStack Object stack.
+ * @private
+ * @return {Object|undefined} The contact info.
+ */
+ol.format.OWS.readContactInfo_ = function(node, objectStack) {
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'node.nodeType should be ELEMENT');
+  goog.asserts.assert(node.localName == 'ContactInfo',
+      'localName should be ContactInfo');
+  return ol.xml.pushParseAndPop({},
+      ol.format.OWS.CONTACT_INFO_PARSERS_, node, objectStack);
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {Array.<*>} objectStack Object stack.
+ * @private
+ * @return {Object|undefined} The DCP.
+ */
+ol.format.OWS.readDcp_ = function(node, objectStack) {
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'node.nodeType should be ELEMENT');
+  goog.asserts.assert(node.localName == 'DCP', 'localName should be DCP');
+  return ol.xml.pushParseAndPop({},
+      ol.format.OWS.DCP_PARSERS_, node, objectStack);
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {Array.<*>} objectStack Object stack.
+ * @private
+ * @return {Object|undefined} The GET object.
+ */
+ol.format.OWS.readGet_ = function(node, objectStack) {
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'node.nodeType should be ELEMENT');
+  goog.asserts.assert(node.localName == 'Get', 'localName should be Get');
+  var href = ol.format.XLink.readHref(node);
+  if (!href) {
+    return undefined;
+  }
+  return ol.xml.pushParseAndPop({'href': href},
+      ol.format.OWS.REQUEST_METHOD_PARSERS_, node, objectStack);
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {Array.<*>} objectStack Object stack.
+ * @private
+ * @return {Object|undefined} The HTTP object.
+ */
+ol.format.OWS.readHttp_ = function(node, objectStack) {
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'node.nodeType should be ELEMENT');
+  goog.asserts.assert(node.localName == 'HTTP', 'localName should be HTTP');
+  return ol.xml.pushParseAndPop({}, ol.format.OWS.HTTP_PARSERS_,
+      node, objectStack);
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {Array.<*>} objectStack Object stack.
+ * @private
+ * @return {Object|undefined} The operation.
+ */
+ol.format.OWS.readOperation_ = function(node, objectStack) {
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'node.nodeType should be ELEMENT');
+  goog.asserts.assert(node.localName == 'Operation',
+      'localName should be Operation');
+  var name = node.getAttribute('name');
+  var value = ol.xml.pushParseAndPop({},
+      ol.format.OWS.OPERATION_PARSERS_, node, objectStack);
+  if (!value) {
+    return undefined;
+  }
+  var object = /** @type {Object} */
+      (objectStack[objectStack.length - 1]);
+  goog.asserts.assert(goog.isObject(object), 'object should be an Object');
+  object[name] = value;
+
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {Array.<*>} objectStack Object stack.
+ * @private
+ * @return {Object|undefined} The operations metadata.
+ */
+ol.format.OWS.readOperationsMetadata_ = function(node,
+    objectStack) {
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'node.nodeType should be ELEMENT');
+  goog.asserts.assert(node.localName == 'OperationsMetadata',
+      'localName should be OperationsMetadata');
+  return ol.xml.pushParseAndPop({},
+      ol.format.OWS.OPERATIONS_METADATA_PARSERS_, node,
+      objectStack);
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {Array.<*>} objectStack Object stack.
+ * @private
+ * @return {Object|undefined} The phone.
+ */
+ol.format.OWS.readPhone_ = function(node, objectStack) {
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'node.nodeType should be ELEMENT');
+  goog.asserts.assert(node.localName == 'Phone', 'localName should be Phone');
+  return ol.xml.pushParseAndPop({},
+      ol.format.OWS.PHONE_PARSERS_, node, objectStack);
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {Array.<*>} objectStack Object stack.
+ * @private
+ * @return {Object|undefined} The service identification.
+ */
+ol.format.OWS.readServiceIdentification_ = function(node,
+    objectStack) {
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'node.nodeType should be ELEMENT');
+  goog.asserts.assert(node.localName == 'ServiceIdentification',
+      'localName should be ServiceIdentification');
+  return ol.xml.pushParseAndPop(
+      {}, ol.format.OWS.SERVICE_IDENTIFICATION_PARSERS_, node,
+      objectStack);
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {Array.<*>} objectStack Object stack.
+ * @private
+ * @return {Object|undefined} The service contact.
+ */
+ol.format.OWS.readServiceContact_ = function(node, objectStack) {
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'node.nodeType should be ELEMENT');
+  goog.asserts.assert(node.localName == 'ServiceContact',
+      'localName should be ServiceContact');
+  return ol.xml.pushParseAndPop(
+      {}, ol.format.OWS.SERVICE_CONTACT_PARSERS_, node,
+      objectStack);
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {Array.<*>} objectStack Object stack.
+ * @private
+ * @return {Object|undefined} The service provider.
+ */
+ol.format.OWS.readServiceProvider_ = function(node, objectStack) {
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'node.nodeType should be ELEMENT');
+  goog.asserts.assert(node.localName == 'ServiceProvider',
+      'localName should be ServiceProvider');
+  return ol.xml.pushParseAndPop(
+      {}, ol.format.OWS.SERVICE_PROVIDER_PARSERS_, node,
+      objectStack);
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {Array.<*>} objectStack Object stack.
+ * @private
+ * @return {string|undefined} The value.
+ */
+ol.format.OWS.readValue_ = function(node, objectStack) {
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'node.nodeType should be ELEMENT');
+  goog.asserts.assert(node.localName == 'Value', 'localName should be Value');
+  return ol.format.XSD.readString(node);
+};
+
+
+/**
+ * @const
+ * @type {Array.<string>}
+ * @private
+ */
+ol.format.OWS.NAMESPACE_URIS_ = [
+  null,
+  'http://www.opengis.net/ows/1.1'
+];
+
+
+/**
+ * @const
+ * @type {Object.<string, Object.<string, ol.XmlParser>>}
+ * @private
+ */
+ol.format.OWS.PARSERS_ = ol.xml.makeStructureNS(
+    ol.format.OWS.NAMESPACE_URIS_, {
+      'ServiceIdentification': ol.xml.makeObjectPropertySetter(
+          ol.format.OWS.readServiceIdentification_),
+      'ServiceProvider': ol.xml.makeObjectPropertySetter(
+          ol.format.OWS.readServiceProvider_),
+      'OperationsMetadata': ol.xml.makeObjectPropertySetter(
+          ol.format.OWS.readOperationsMetadata_)
+    });
+
+
+/**
+ * @const
+ * @type {Object.<string, Object.<string, ol.XmlParser>>}
+ * @private
+ */
+ol.format.OWS.ADDRESS_PARSERS_ = ol.xml.makeStructureNS(
+    ol.format.OWS.NAMESPACE_URIS_, {
+      'DeliveryPoint': ol.xml.makeObjectPropertySetter(
+          ol.format.XSD.readString),
+      'City': ol.xml.makeObjectPropertySetter(ol.format.XSD.readString),
+      'AdministrativeArea': ol.xml.makeObjectPropertySetter(
+          ol.format.XSD.readString),
+      'PostalCode': ol.xml.makeObjectPropertySetter(ol.format.XSD.readString),
+      'Country': ol.xml.makeObjectPropertySetter(ol.format.XSD.readString),
+      'ElectronicMailAddress': ol.xml.makeObjectPropertySetter(
+          ol.format.XSD.readString)
+    });
+
+
+/**
+ * @const
+ * @type {Object.<string, Object.<string, ol.XmlParser>>}
+ * @private
+ */
+ol.format.OWS.ALLOWED_VALUES_PARSERS_ = ol.xml.makeStructureNS(
+    ol.format.OWS.NAMESPACE_URIS_, {
+      'Value': ol.xml.makeObjectPropertyPusher(ol.format.OWS.readValue_)
+    });
+
+
+/**
+ * @const
+ * @type {Object.<string, Object.<string, ol.XmlParser>>}
+ * @private
+ */
+ol.format.OWS.CONSTRAINT_PARSERS_ = ol.xml.makeStructureNS(
+    ol.format.OWS.NAMESPACE_URIS_, {
+      'AllowedValues': ol.xml.makeObjectPropertySetter(
+          ol.format.OWS.readAllowedValues_)
+    });
+
+
+/**
+ * @const
+ * @type {Object.<string, Object.<string, ol.XmlParser>>}
+ * @private
+ */
+ol.format.OWS.CONTACT_INFO_PARSERS_ = ol.xml.makeStructureNS(
+    ol.format.OWS.NAMESPACE_URIS_, {
+      'Phone': ol.xml.makeObjectPropertySetter(ol.format.OWS.readPhone_),
+      'Address': ol.xml.makeObjectPropertySetter(ol.format.OWS.readAddress_)
+    });
+
+
+/**
+ * @const
+ * @type {Object.<string, Object.<string, ol.XmlParser>>}
+ * @private
+ */
+ol.format.OWS.DCP_PARSERS_ = ol.xml.makeStructureNS(
+    ol.format.OWS.NAMESPACE_URIS_, {
+      'HTTP': ol.xml.makeObjectPropertySetter(ol.format.OWS.readHttp_)
+    });
+
+
+/**
+ * @const
+ * @type {Object.<string, Object.<string, ol.XmlParser>>}
+ * @private
+ */
+ol.format.OWS.HTTP_PARSERS_ = ol.xml.makeStructureNS(
+    ol.format.OWS.NAMESPACE_URIS_, {
+      'Get': ol.xml.makeObjectPropertyPusher(ol.format.OWS.readGet_),
+      'Post': undefined // TODO
+    });
+
+
+/**
+ * @const
+ * @type {Object.<string, Object.<string, ol.XmlParser>>}
+ * @private
+ */
+ol.format.OWS.OPERATION_PARSERS_ = ol.xml.makeStructureNS(
+    ol.format.OWS.NAMESPACE_URIS_, {
+      'DCP': ol.xml.makeObjectPropertySetter(ol.format.OWS.readDcp_)
+    });
+
+
+/**
+ * @const
+ * @type {Object.<string, Object.<string, ol.XmlParser>>}
+ * @private
+ */
+ol.format.OWS.OPERATIONS_METADATA_PARSERS_ = ol.xml.makeStructureNS(
+    ol.format.OWS.NAMESPACE_URIS_, {
+      'Operation': ol.format.OWS.readOperation_
+    });
+
+
+/**
+ * @const
+ * @type {Object.<string, Object.<string, ol.XmlParser>>}
+ * @private
+ */
+ol.format.OWS.PHONE_PARSERS_ = ol.xml.makeStructureNS(
+    ol.format.OWS.NAMESPACE_URIS_, {
+      'Voice': ol.xml.makeObjectPropertySetter(ol.format.XSD.readString),
+      'Facsimile': ol.xml.makeObjectPropertySetter(ol.format.XSD.readString)
+    });
+
+
+/**
+ * @const
+ * @type {Object.<string, Object.<string, ol.XmlParser>>}
+ * @private
+ */
+ol.format.OWS.REQUEST_METHOD_PARSERS_ = ol.xml.makeStructureNS(
+    ol.format.OWS.NAMESPACE_URIS_, {
+      'Constraint': ol.xml.makeObjectPropertyPusher(
+          ol.format.OWS.readConstraint_)
+    });
+
+
+/**
+ * @const
+ * @type {Object.<string, Object.<string, ol.XmlParser>>}
+ * @private
+ */
+ol.format.OWS.SERVICE_CONTACT_PARSERS_ =
+    ol.xml.makeStructureNS(
+    ol.format.OWS.NAMESPACE_URIS_, {
+      'IndividualName': ol.xml.makeObjectPropertySetter(
+          ol.format.XSD.readString),
+      'PositionName': ol.xml.makeObjectPropertySetter(ol.format.XSD.readString),
+      'ContactInfo': ol.xml.makeObjectPropertySetter(
+          ol.format.OWS.readContactInfo_)
+    });
+
+
+/**
+ * @const
+ * @type {Object.<string, Object.<string, ol.XmlParser>>}
+ * @private
+ */
+ol.format.OWS.SERVICE_IDENTIFICATION_PARSERS_ =
+    ol.xml.makeStructureNS(
+    ol.format.OWS.NAMESPACE_URIS_, {
+      'Title': ol.xml.makeObjectPropertySetter(ol.format.XSD.readString),
+      'ServiceTypeVersion': ol.xml.makeObjectPropertySetter(
+          ol.format.XSD.readString),
+      'ServiceType': ol.xml.makeObjectPropertySetter(ol.format.XSD.readString)
+    });
+
+
+/**
+ * @const
+ * @type {Object.<string, Object.<string, ol.XmlParser>>}
+ * @private
+ */
+ol.format.OWS.SERVICE_PROVIDER_PARSERS_ =
+    ol.xml.makeStructureNS(
+    ol.format.OWS.NAMESPACE_URIS_, {
+      'ProviderName': ol.xml.makeObjectPropertySetter(ol.format.XSD.readString),
+      'ProviderSite': ol.xml.makeObjectPropertySetter(ol.format.XLink.readHref),
+      'ServiceContact': ol.xml.makeObjectPropertySetter(
+          ol.format.OWS.readServiceContact_)
+    });
+
+goog.provide('ol.geom.flat.flip');
+
+goog.require('goog.asserts');
+
+
+/**
+ * @param {Array.<number>} flatCoordinates Flat coordinates.
+ * @param {number} offset Offset.
+ * @param {number} end End.
+ * @param {number} stride Stride.
+ * @param {Array.<number>=} opt_dest Destination.
+ * @param {number=} opt_destOffset Destination offset.
+ * @return {Array.<number>} Flat coordinates.
+ */
+ol.geom.flat.flip.flipXY = function(flatCoordinates, offset, end, stride, opt_dest, opt_destOffset) {
+  var dest, destOffset;
+  if (opt_dest !== undefined) {
+    dest = opt_dest;
+    destOffset = opt_destOffset !== undefined ? opt_destOffset : 0;
+  } else {
+    goog.asserts.assert(opt_destOffset === undefined,
+        'opt_destOffSet should be defined');
+    dest = [];
+    destOffset = 0;
+  }
+  var j = offset;
+  while (j < end) {
+    var x = flatCoordinates[j++];
+    dest[destOffset++] = flatCoordinates[j++];
+    dest[destOffset++] = x;
+    for (var k = 2; k < stride; ++k) {
+      dest[destOffset++] = flatCoordinates[j++];
+    }
+  }
+  dest.length = destOffset;
+  return dest;
+};
+
+goog.provide('ol.format.Polyline');
+
+goog.require('goog.asserts');
+goog.require('ol.Feature');
+goog.require('ol.format.Feature');
+goog.require('ol.format.TextFeature');
+goog.require('ol.geom.GeometryLayout');
+goog.require('ol.geom.LineString');
+goog.require('ol.geom.SimpleGeometry');
+goog.require('ol.geom.flat.flip');
+goog.require('ol.geom.flat.inflate');
+goog.require('ol.proj');
+
+
+/**
+ * @classdesc
+ * Feature format for reading and writing data in the Encoded
+ * Polyline Algorithm Format.
+ *
+ * @constructor
+ * @extends {ol.format.TextFeature}
+ * @param {olx.format.PolylineOptions=} opt_options
+ *     Optional configuration object.
+ * @api stable
+ */
+ol.format.Polyline = function(opt_options) {
+
+  var options = opt_options ? opt_options : {};
+
+  ol.format.TextFeature.call(this);
+
+  /**
+   * @inheritDoc
+   */
+  this.defaultDataProjection = ol.proj.get('EPSG:4326');
+
+  /**
+   * @private
+   * @type {number}
+   */
+  this.factor_ = options.factor ? options.factor : 1e5;
+
+  /**
+   * @private
+   * @type {ol.geom.GeometryLayout}
+   */
+  this.geometryLayout_ = options.geometryLayout ?
+      options.geometryLayout : ol.geom.GeometryLayout.XY;
+};
+ol.inherits(ol.format.Polyline, ol.format.TextFeature);
+
+
+/**
+ * Encode a list of n-dimensional points and return an encoded string
+ *
+ * Attention: This function will modify the passed array!
+ *
+ * @param {Array.<number>} numbers A list of n-dimensional points.
+ * @param {number} stride The number of dimension of the points in the list.
+ * @param {number=} opt_factor The factor by which the numbers will be
+ *     multiplied. The remaining decimal places will get rounded away.
+ *     Default is `1e5`.
+ * @return {string} The encoded string.
+ * @api
+ */
+ol.format.Polyline.encodeDeltas = function(numbers, stride, opt_factor) {
+  var factor = opt_factor ? opt_factor : 1e5;
+  var d;
+
+  var lastNumbers = new Array(stride);
+  for (d = 0; d < stride; ++d) {
+    lastNumbers[d] = 0;
+  }
+
+  var i, ii;
+  for (i = 0, ii = numbers.length; i < ii;) {
+    for (d = 0; d < stride; ++d, ++i) {
+      var num = numbers[i];
+      var delta = num - lastNumbers[d];
+      lastNumbers[d] = num;
+
+      numbers[i] = delta;
+    }
+  }
+
+  return ol.format.Polyline.encodeFloats(numbers, factor);
+};
+
+
+/**
+ * Decode a list of n-dimensional points from an encoded string
+ *
+ * @param {string} encoded An encoded string.
+ * @param {number} stride The number of dimension of the points in the
+ *     encoded string.
+ * @param {number=} opt_factor The factor by which the resulting numbers will
+ *     be divided. Default is `1e5`.
+ * @return {Array.<number>} A list of n-dimensional points.
+ * @api
+ */
+ol.format.Polyline.decodeDeltas = function(encoded, stride, opt_factor) {
+  var factor = opt_factor ? opt_factor : 1e5;
+  var d;
+
+  /** @type {Array.<number>} */
+  var lastNumbers = new Array(stride);
+  for (d = 0; d < stride; ++d) {
+    lastNumbers[d] = 0;
+  }
+
+  var numbers = ol.format.Polyline.decodeFloats(encoded, factor);
+
+  var i, ii;
+  for (i = 0, ii = numbers.length; i < ii;) {
+    for (d = 0; d < stride; ++d, ++i) {
+      lastNumbers[d] += numbers[i];
+
+      numbers[i] = lastNumbers[d];
+    }
+  }
+
+  return numbers;
+};
+
+
+/**
+ * Encode a list of floating point numbers and return an encoded string
+ *
+ * Attention: This function will modify the passed array!
+ *
+ * @param {Array.<number>} numbers A list of floating point numbers.
+ * @param {number=} opt_factor The factor by which the numbers will be
+ *     multiplied. The remaining decimal places will get rounded away.
+ *     Default is `1e5`.
+ * @return {string} The encoded string.
+ * @api
+ */
+ol.format.Polyline.encodeFloats = function(numbers, opt_factor) {
+  var factor = opt_factor ? opt_factor : 1e5;
+  var i, ii;
+  for (i = 0, ii = numbers.length; i < ii; ++i) {
+    numbers[i] = Math.round(numbers[i] * factor);
+  }
+
+  return ol.format.Polyline.encodeSignedIntegers(numbers);
+};
+
+
+/**
+ * Decode a list of floating point numbers from an encoded string
+ *
+ * @param {string} encoded An encoded string.
+ * @param {number=} opt_factor The factor by which the result will be divided.
+ *     Default is `1e5`.
+ * @return {Array.<number>} A list of floating point numbers.
+ * @api
+ */
+ol.format.Polyline.decodeFloats = function(encoded, opt_factor) {
+  var factor = opt_factor ? opt_factor : 1e5;
+  var numbers = ol.format.Polyline.decodeSignedIntegers(encoded);
+  var i, ii;
+  for (i = 0, ii = numbers.length; i < ii; ++i) {
+    numbers[i] /= factor;
+  }
+  return numbers;
+};
+
+
+/**
+ * Encode a list of signed integers and return an encoded string
+ *
+ * Attention: This function will modify the passed array!
+ *
+ * @param {Array.<number>} numbers A list of signed integers.
+ * @return {string} The encoded string.
+ */
+ol.format.Polyline.encodeSignedIntegers = function(numbers) {
+  var i, ii;
+  for (i = 0, ii = numbers.length; i < ii; ++i) {
+    var num = numbers[i];
+    numbers[i] = (num < 0) ? ~(num << 1) : (num << 1);
+  }
+  return ol.format.Polyline.encodeUnsignedIntegers(numbers);
+};
+
+
+/**
+ * Decode a list of signed integers from an encoded string
+ *
+ * @param {string} encoded An encoded string.
+ * @return {Array.<number>} A list of signed integers.
+ */
+ol.format.Polyline.decodeSignedIntegers = function(encoded) {
+  var numbers = ol.format.Polyline.decodeUnsignedIntegers(encoded);
+  var i, ii;
+  for (i = 0, ii = numbers.length; i < ii; ++i) {
+    var num = numbers[i];
+    numbers[i] = (num & 1) ? ~(num >> 1) : (num >> 1);
+  }
+  return numbers;
+};
+
+
+/**
+ * Encode a list of unsigned integers and return an encoded string
+ *
+ * @param {Array.<number>} numbers A list of unsigned integers.
+ * @return {string} The encoded string.
+ */
+ol.format.Polyline.encodeUnsignedIntegers = function(numbers) {
+  var encoded = '';
+  var i, ii;
+  for (i = 0, ii = numbers.length; i < ii; ++i) {
+    encoded += ol.format.Polyline.encodeUnsignedInteger(numbers[i]);
+  }
+  return encoded;
+};
+
+
+/**
+ * Decode a list of unsigned integers from an encoded string
+ *
+ * @param {string} encoded An encoded string.
+ * @return {Array.<number>} A list of unsigned integers.
+ */
+ol.format.Polyline.decodeUnsignedIntegers = function(encoded) {
+  var numbers = [];
+  var current = 0;
+  var shift = 0;
+  var i, ii;
+  for (i = 0, ii = encoded.length; i < ii; ++i) {
+    var b = encoded.charCodeAt(i) - 63;
+    current |= (b & 0x1f) << shift;
+    if (b < 0x20) {
+      numbers.push(current);
+      current = 0;
+      shift = 0;
+    } else {
+      shift += 5;
+    }
+  }
+  return numbers;
+};
+
+
+/**
+ * Encode one single unsigned integer and return an encoded string
+ *
+ * @param {number} num Unsigned integer that should be encoded.
+ * @return {string} The encoded string.
+ */
+ol.format.Polyline.encodeUnsignedInteger = function(num) {
+  var value, encoded = '';
+  while (num >= 0x20) {
+    value = (0x20 | (num & 0x1f)) + 63;
+    encoded += String.fromCharCode(value);
+    num >>= 5;
+  }
+  value = num + 63;
+  encoded += String.fromCharCode(value);
+  return encoded;
+};
+
+
+/**
+ * Read the feature from the Polyline source. The coordinates are assumed to be
+ * in two dimensions and in latitude, longitude order.
+ *
+ * @function
+ * @param {Document|Node|Object|string} source Source.
+ * @param {olx.format.ReadOptions=} opt_options Read options.
+ * @return {ol.Feature} Feature.
+ * @api stable
+ */
+ol.format.Polyline.prototype.readFeature;
+
+
+/**
+ * @inheritDoc
+ */
+ol.format.Polyline.prototype.readFeatureFromText = function(text, opt_options) {
+  var geometry = this.readGeometryFromText(text, opt_options);
+  return new ol.Feature(geometry);
+};
+
+
+/**
+ * Read the feature from the source. As Polyline sources contain a single
+ * feature, this will return the feature in an array.
+ *
+ * @function
+ * @param {Document|Node|Object|string} source Source.
+ * @param {olx.format.ReadOptions=} opt_options Read options.
+ * @return {Array.<ol.Feature>} Features.
+ * @api stable
+ */
+ol.format.Polyline.prototype.readFeatures;
+
+
+/**
+ * @inheritDoc
+ */
+ol.format.Polyline.prototype.readFeaturesFromText = function(text, opt_options) {
+  var feature = this.readFeatureFromText(text, opt_options);
+  return [feature];
+};
+
+
+/**
+ * Read the geometry from the source.
+ *
+ * @function
+ * @param {Document|Node|Object|string} source Source.
+ * @param {olx.format.ReadOptions=} opt_options Read options.
+ * @return {ol.geom.Geometry} Geometry.
+ * @api stable
+ */
+ol.format.Polyline.prototype.readGeometry;
+
+
+/**
+ * @inheritDoc
+ */
+ol.format.Polyline.prototype.readGeometryFromText = function(text, opt_options) {
+  var stride = ol.geom.SimpleGeometry.getStrideForLayout(this.geometryLayout_);
+  var flatCoordinates = ol.format.Polyline.decodeDeltas(
+      text, stride, this.factor_);
+  ol.geom.flat.flip.flipXY(
+      flatCoordinates, 0, flatCoordinates.length, stride, flatCoordinates);
+  var coordinates = ol.geom.flat.inflate.coordinates(
+      flatCoordinates, 0, flatCoordinates.length, stride);
+
+  return /** @type {ol.geom.Geometry} */ (
+      ol.format.Feature.transformWithOptions(
+          new ol.geom.LineString(coordinates, this.geometryLayout_), false,
+          this.adaptOptions(opt_options)));
+};
+
+
+/**
+ * Read the projection from a Polyline source.
+ *
+ * @function
+ * @param {Document|Node|Object|string} source Source.
+ * @return {ol.proj.Projection} Projection.
+ * @api stable
+ */
+ol.format.Polyline.prototype.readProjection;
+
+
+/**
+ * @inheritDoc
+ */
+ol.format.Polyline.prototype.writeFeatureText = function(feature, opt_options) {
+  var geometry = feature.getGeometry();
+  if (geometry) {
+    return this.writeGeometryText(geometry, opt_options);
+  } else {
+    goog.asserts.fail('geometry needs to be defined');
+    return '';
+  }
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.format.Polyline.prototype.writeFeaturesText = function(features, opt_options) {
+  goog.asserts.assert(features.length == 1,
+      'features array should have 1 item');
+  return this.writeFeatureText(features[0], opt_options);
+};
+
+
+/**
+ * Write a single geometry in Polyline format.
+ *
+ * @function
+ * @param {ol.geom.Geometry} geometry Geometry.
+ * @param {olx.format.WriteOptions=} opt_options Write options.
+ * @return {string} Geometry.
+ * @api stable
+ */
+ol.format.Polyline.prototype.writeGeometry;
+
+
+/**
+ * @inheritDoc
+ */
+ol.format.Polyline.prototype.writeGeometryText = function(geometry, opt_options) {
+  goog.asserts.assertInstanceof(geometry, ol.geom.LineString,
+      'geometry should be an ol.geom.LineString');
+  geometry = /** @type {ol.geom.LineString} */
+      (ol.format.Feature.transformWithOptions(
+          geometry, true, this.adaptOptions(opt_options)));
+  var flatCoordinates = geometry.getFlatCoordinates();
+  var stride = geometry.getStride();
+  ol.geom.flat.flip.flipXY(
+      flatCoordinates, 0, flatCoordinates.length, stride, flatCoordinates);
+  return ol.format.Polyline.encodeDeltas(flatCoordinates, stride, this.factor_);
+};
+
+goog.provide('ol.format.TopoJSON');
+
+goog.require('goog.asserts');
+goog.require('ol.Feature');
+goog.require('ol.format.Feature');
+goog.require('ol.format.JSONFeature');
+goog.require('ol.geom.LineString');
+goog.require('ol.geom.MultiLineString');
+goog.require('ol.geom.MultiPoint');
+goog.require('ol.geom.MultiPolygon');
+goog.require('ol.geom.Point');
+goog.require('ol.geom.Polygon');
+goog.require('ol.object');
+goog.require('ol.proj');
+
+
+/**
+ * @classdesc
+ * Feature format for reading data in the TopoJSON format.
+ *
+ * @constructor
+ * @extends {ol.format.JSONFeature}
+ * @param {olx.format.TopoJSONOptions=} opt_options Options.
+ * @api stable
+ */
+ol.format.TopoJSON = function(opt_options) {
+
+  var options = opt_options ? opt_options : {};
+
+  ol.format.JSONFeature.call(this);
+
+  /**
+   * @inheritDoc
+   */
+  this.defaultDataProjection = ol.proj.get(
+      options.defaultDataProjection ?
+          options.defaultDataProjection : 'EPSG:4326');
+
+};
+ol.inherits(ol.format.TopoJSON, ol.format.JSONFeature);
+
+
+/**
+ * @const {Array.<string>}
+ * @private
+ */
+ol.format.TopoJSON.EXTENSIONS_ = ['.topojson'];
+
+
+/**
+ * Concatenate arcs into a coordinate array.
+ * @param {Array.<number>} indices Indices of arcs to concatenate.  Negative
+ *     values indicate arcs need to be reversed.
+ * @param {Array.<Array.<ol.Coordinate>>} arcs Array of arcs (already
+ *     transformed).
+ * @return {Array.<ol.Coordinate>} Coordinates array.
+ * @private
+ */
+ol.format.TopoJSON.concatenateArcs_ = function(indices, arcs) {
+  /** @type {Array.<ol.Coordinate>} */
+  var coordinates = [];
+  var index, arc;
+  var i, ii;
+  var j, jj;
+  for (i = 0, ii = indices.length; i < ii; ++i) {
+    index = indices[i];
+    if (i > 0) {
+      // splicing together arcs, discard last point
+      coordinates.pop();
+    }
+    if (index >= 0) {
+      // forward arc
+      arc = arcs[index];
+    } else {
+      // reverse arc
+      arc = arcs[~index].slice().reverse();
+    }
+    coordinates.push.apply(coordinates, arc);
+  }
+  // provide fresh copies of coordinate arrays
+  for (j = 0, jj = coordinates.length; j < jj; ++j) {
+    coordinates[j] = coordinates[j].slice();
+  }
+  return coordinates;
+};
+
+
+/**
+ * Create a point from a TopoJSON geometry object.
+ *
+ * @param {TopoJSONGeometry} object TopoJSON object.
+ * @param {Array.<number>} scale Scale for each dimension.
+ * @param {Array.<number>} translate Translation for each dimension.
+ * @return {ol.geom.Point} Geometry.
+ * @private
+ */
+ol.format.TopoJSON.readPointGeometry_ = function(object, scale, translate) {
+  var coordinates = object.coordinates;
+  if (scale && translate) {
+    ol.format.TopoJSON.transformVertex_(coordinates, scale, translate);
+  }
+  return new ol.geom.Point(coordinates);
+};
+
+
+/**
+ * Create a multi-point from a TopoJSON geometry object.
+ *
+ * @param {TopoJSONGeometry} object TopoJSON object.
+ * @param {Array.<number>} scale Scale for each dimension.
+ * @param {Array.<number>} translate Translation for each dimension.
+ * @return {ol.geom.MultiPoint} Geometry.
+ * @private
+ */
+ol.format.TopoJSON.readMultiPointGeometry_ = function(object, scale,
+    translate) {
+  var coordinates = object.coordinates;
+  var i, ii;
+  if (scale && translate) {
+    for (i = 0, ii = coordinates.length; i < ii; ++i) {
+      ol.format.TopoJSON.transformVertex_(coordinates[i], scale, translate);
+    }
+  }
+  return new ol.geom.MultiPoint(coordinates);
+};
+
+
+/**
+ * Create a linestring from a TopoJSON geometry object.
+ *
+ * @param {TopoJSONGeometry} object TopoJSON object.
+ * @param {Array.<Array.<ol.Coordinate>>} arcs Array of arcs.
+ * @return {ol.geom.LineString} Geometry.
+ * @private
+ */
+ol.format.TopoJSON.readLineStringGeometry_ = function(object, arcs) {
+  var coordinates = ol.format.TopoJSON.concatenateArcs_(object.arcs, arcs);
+  return new ol.geom.LineString(coordinates);
+};
+
+
+/**
+ * Create a multi-linestring from a TopoJSON geometry object.
+ *
+ * @param {TopoJSONGeometry} object TopoJSON object.
+ * @param {Array.<Array.<ol.Coordinate>>} arcs Array of arcs.
+ * @return {ol.geom.MultiLineString} Geometry.
+ * @private
+ */
+ol.format.TopoJSON.readMultiLineStringGeometry_ = function(object, arcs) {
+  var coordinates = [];
+  var i, ii;
+  for (i = 0, ii = object.arcs.length; i < ii; ++i) {
+    coordinates[i] = ol.format.TopoJSON.concatenateArcs_(object.arcs[i], arcs);
+  }
+  return new ol.geom.MultiLineString(coordinates);
+};
+
+
+/**
+ * Create a polygon from a TopoJSON geometry object.
+ *
+ * @param {TopoJSONGeometry} object TopoJSON object.
+ * @param {Array.<Array.<ol.Coordinate>>} arcs Array of arcs.
+ * @return {ol.geom.Polygon} Geometry.
+ * @private
+ */
+ol.format.TopoJSON.readPolygonGeometry_ = function(object, arcs) {
+  var coordinates = [];
+  var i, ii;
+  for (i = 0, ii = object.arcs.length; i < ii; ++i) {
+    coordinates[i] = ol.format.TopoJSON.concatenateArcs_(object.arcs[i], arcs);
+  }
+  return new ol.geom.Polygon(coordinates);
+};
+
+
+/**
+ * Create a multi-polygon from a TopoJSON geometry object.
+ *
+ * @param {TopoJSONGeometry} object TopoJSON object.
+ * @param {Array.<Array.<ol.Coordinate>>} arcs Array of arcs.
+ * @return {ol.geom.MultiPolygon} Geometry.
+ * @private
+ */
+ol.format.TopoJSON.readMultiPolygonGeometry_ = function(object, arcs) {
+  var coordinates = [];
+  var polyArray, ringCoords, j, jj;
+  var i, ii;
+  for (i = 0, ii = object.arcs.length; i < ii; ++i) {
+    // for each polygon
+    polyArray = object.arcs[i];
+    ringCoords = [];
+    for (j = 0, jj = polyArray.length; j < jj; ++j) {
+      // for each ring
+      ringCoords[j] = ol.format.TopoJSON.concatenateArcs_(polyArray[j], arcs);
+    }
+    coordinates[i] = ringCoords;
+  }
+  return new ol.geom.MultiPolygon(coordinates);
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.format.TopoJSON.prototype.getExtensions = function() {
+  return ol.format.TopoJSON.EXTENSIONS_;
+};
+
+
+/**
+ * Create features from a TopoJSON GeometryCollection object.
+ *
+ * @param {TopoJSONGeometryCollection} collection TopoJSON Geometry
+ *     object.
+ * @param {Array.<Array.<ol.Coordinate>>} arcs Array of arcs.
+ * @param {Array.<number>} scale Scale for each dimension.
+ * @param {Array.<number>} translate Translation for each dimension.
+ * @param {olx.format.ReadOptions=} opt_options Read options.
+ * @return {Array.<ol.Feature>} Array of features.
+ * @private
+ */
+ol.format.TopoJSON.readFeaturesFromGeometryCollection_ = function(
+    collection, arcs, scale, translate, opt_options) {
+  var geometries = collection.geometries;
+  var features = [];
+  var i, ii;
+  for (i = 0, ii = geometries.length; i < ii; ++i) {
+    features[i] = ol.format.TopoJSON.readFeatureFromGeometry_(
+        geometries[i], arcs, scale, translate, opt_options);
+  }
+  return features;
+};
+
+
+/**
+ * Create a feature from a TopoJSON geometry object.
+ *
+ * @param {TopoJSONGeometry} object TopoJSON geometry object.
+ * @param {Array.<Array.<ol.Coordinate>>} arcs Array of arcs.
+ * @param {Array.<number>} scale Scale for each dimension.
+ * @param {Array.<number>} translate Translation for each dimension.
+ * @param {olx.format.ReadOptions=} opt_options Read options.
+ * @return {ol.Feature} Feature.
+ * @private
+ */
+ol.format.TopoJSON.readFeatureFromGeometry_ = function(object, arcs,
+    scale, translate, opt_options) {
+  var geometry;
+  var type = object.type;
+  var geometryReader = ol.format.TopoJSON.GEOMETRY_READERS_[type];
+  goog.asserts.assert(geometryReader, 'geometryReader should be defined');
+  if ((type === 'Point') || (type === 'MultiPoint')) {
+    geometry = geometryReader(object, scale, translate);
+  } else {
+    geometry = geometryReader(object, arcs);
+  }
+  var feature = new ol.Feature();
+  feature.setGeometry(/** @type {ol.geom.Geometry} */ (
+      ol.format.Feature.transformWithOptions(geometry, false, opt_options)));
+  if (object.id !== undefined) {
+    feature.setId(object.id);
+  }
+  if (object.properties) {
+    feature.setProperties(object.properties);
+  }
+  return feature;
+};
+
+
+/**
+ * Read all features from a TopoJSON source.
+ *
+ * @function
+ * @param {Document|Node|Object|string} source Source.
+ * @return {Array.<ol.Feature>} Features.
+ * @api stable
+ */
+ol.format.TopoJSON.prototype.readFeatures;
+
+
+/**
+ * @inheritDoc
+ */
+ol.format.TopoJSON.prototype.readFeaturesFromObject = function(
+    object, opt_options) {
+  if (object.type == 'Topology') {
+    var topoJSONTopology = /** @type {TopoJSONTopology} */ (object);
+    var transform, scale = null, translate = null;
+    if (topoJSONTopology.transform) {
+      transform = topoJSONTopology.transform;
+      scale = transform.scale;
+      translate = transform.translate;
+    }
+    var arcs = topoJSONTopology.arcs;
+    if (transform) {
+      ol.format.TopoJSON.transformArcs_(arcs, scale, translate);
+    }
+    /** @type {Array.<ol.Feature>} */
+    var features = [];
+    var topoJSONFeatures = ol.object.getValues(topoJSONTopology.objects);
+    var i, ii;
+    var feature;
+    for (i = 0, ii = topoJSONFeatures.length; i < ii; ++i) {
+      if (topoJSONFeatures[i].type === 'GeometryCollection') {
+        feature = /** @type {TopoJSONGeometryCollection} */
+            (topoJSONFeatures[i]);
+        features.push.apply(features,
+            ol.format.TopoJSON.readFeaturesFromGeometryCollection_(
+                feature, arcs, scale, translate, opt_options));
+      } else {
+        feature = /** @type {TopoJSONGeometry} */
+            (topoJSONFeatures[i]);
+        features.push(ol.format.TopoJSON.readFeatureFromGeometry_(
+            feature, arcs, scale, translate, opt_options));
+      }
+    }
+    return features;
+  } else {
+    return [];
+  }
+};
+
+
+/**
+ * Apply a linear transform to array of arcs.  The provided array of arcs is
+ * modified in place.
+ *
+ * @param {Array.<Array.<ol.Coordinate>>} arcs Array of arcs.
+ * @param {Array.<number>} scale Scale for each dimension.
+ * @param {Array.<number>} translate Translation for each dimension.
+ * @private
+ */
+ol.format.TopoJSON.transformArcs_ = function(arcs, scale, translate) {
+  var i, ii;
+  for (i = 0, ii = arcs.length; i < ii; ++i) {
+    ol.format.TopoJSON.transformArc_(arcs[i], scale, translate);
+  }
+};
+
+
+/**
+ * Apply a linear transform to an arc.  The provided arc is modified in place.
+ *
+ * @param {Array.<ol.Coordinate>} arc Arc.
+ * @param {Array.<number>} scale Scale for each dimension.
+ * @param {Array.<number>} translate Translation for each dimension.
+ * @private
+ */
+ol.format.TopoJSON.transformArc_ = function(arc, scale, translate) {
+  var x = 0;
+  var y = 0;
+  var vertex;
+  var i, ii;
+  for (i = 0, ii = arc.length; i < ii; ++i) {
+    vertex = arc[i];
+    x += vertex[0];
+    y += vertex[1];
+    vertex[0] = x;
+    vertex[1] = y;
+    ol.format.TopoJSON.transformVertex_(vertex, scale, translate);
+  }
+};
+
+
+/**
+ * Apply a linear transform to a vertex.  The provided vertex is modified in
+ * place.
+ *
+ * @param {ol.Coordinate} vertex Vertex.
+ * @param {Array.<number>} scale Scale for each dimension.
+ * @param {Array.<number>} translate Translation for each dimension.
+ * @private
+ */
+ol.format.TopoJSON.transformVertex_ = function(vertex, scale, translate) {
+  vertex[0] = vertex[0] * scale[0] + translate[0];
+  vertex[1] = vertex[1] * scale[1] + translate[1];
+};
+
+
+/**
+ * Read the projection from a TopoJSON source.
+ *
+ * @function
+ * @param {Document|Node|Object|string} object Source.
+ * @return {ol.proj.Projection} Projection.
+ * @api stable
+ */
+ol.format.TopoJSON.prototype.readProjection = function(object) {
+  return this.defaultDataProjection;
+};
+
+
+/**
+ * @const
+ * @private
+ * @type {Object.<string, function(TopoJSONGeometry, Array, ...Array): ol.geom.Geometry>}
+ */
+ol.format.TopoJSON.GEOMETRY_READERS_ = {
+  'Point': ol.format.TopoJSON.readPointGeometry_,
+  'LineString': ol.format.TopoJSON.readLineStringGeometry_,
+  'Polygon': ol.format.TopoJSON.readPolygonGeometry_,
+  'MultiPoint': ol.format.TopoJSON.readMultiPointGeometry_,
+  'MultiLineString': ol.format.TopoJSON.readMultiLineStringGeometry_,
+  'MultiPolygon': ol.format.TopoJSON.readMultiPolygonGeometry_
+};
+
+goog.provide('ol.format.WFS');
+
+goog.require('goog.asserts');
+goog.require('ol');
+goog.require('ol.format.GML3');
+goog.require('ol.format.GMLBase');
+goog.require('ol.format.ogc.filter');
+goog.require('ol.format.ogc.filter.Bbox');
+goog.require('ol.format.ogc.filter.ComparisonBinary');
+goog.require('ol.format.ogc.filter.LogicalBinary');
+goog.require('ol.format.ogc.filter.Not');
+goog.require('ol.format.ogc.filter.IsBetween');
+goog.require('ol.format.ogc.filter.IsNull');
+goog.require('ol.format.ogc.filter.IsLike');
+goog.require('ol.format.XMLFeature');
+goog.require('ol.format.XSD');
+goog.require('ol.geom.Geometry');
+goog.require('ol.object');
+goog.require('ol.proj');
+goog.require('ol.xml');
+
+
+/**
+ * @classdesc
+ * Feature format for reading and writing data in the WFS format.
+ * By default, supports WFS version 1.1.0. You can pass a GML format
+ * as option if you want to read a WFS that contains GML2 (WFS 1.0.0).
+ * Also see {@link ol.format.GMLBase} which is used by this format.
+ *
+ * @constructor
+ * @param {olx.format.WFSOptions=} opt_options
+ *     Optional configuration object.
+ * @extends {ol.format.XMLFeature}
+ * @api stable
+ */
+ol.format.WFS = function(opt_options) {
+  var options = opt_options ? opt_options : {};
+
+  /**
+   * @private
+   * @type {Array.<string>|string|undefined}
+   */
+  this.featureType_ = options.featureType;
+
+  /**
+   * @private
+   * @type {Object.<string, string>|string|undefined}
+   */
+  this.featureNS_ = options.featureNS;
+
+  /**
+   * @private
+   * @type {ol.format.GMLBase}
+   */
+  this.gmlFormat_ = options.gmlFormat ?
+      options.gmlFormat : new ol.format.GML3();
+
+  /**
+   * @private
+   * @type {string}
+   */
+  this.schemaLocation_ = options.schemaLocation ?
+      options.schemaLocation : ol.format.WFS.SCHEMA_LOCATION;
+
+  ol.format.XMLFeature.call(this);
+};
+ol.inherits(ol.format.WFS, ol.format.XMLFeature);
+
+
+/**
+ * @const
+ * @type {string}
+ */
+ol.format.WFS.FEATURE_PREFIX = 'feature';
+
+
+/**
+ * @const
+ * @type {string}
+ */
+ol.format.WFS.XMLNS = 'http://www.w3.org/2000/xmlns/';
+
+
+/**
+ * @const
+ * @type {string}
+ */
+ol.format.WFS.SCHEMA_LOCATION = 'http://www.opengis.net/wfs ' +
+    'http://schemas.opengis.net/wfs/1.1.0/wfs.xsd';
+
+
+/**
+ * Read all features from a WFS FeatureCollection.
+ *
+ * @function
+ * @param {Document|Node|Object|string} source Source.
+ * @param {olx.format.ReadOptions=} opt_options Read options.
+ * @return {Array.<ol.Feature>} Features.
+ * @api stable
+ */
+ol.format.WFS.prototype.readFeatures;
+
+
+/**
+ * @inheritDoc
+ */
+ol.format.WFS.prototype.readFeaturesFromNode = function(node, opt_options) {
+  var context = /** @type {ol.XmlNodeStackItem} */ ({
+    'featureType': this.featureType_,
+    'featureNS': this.featureNS_
+  });
+  ol.object.assign(context, this.getReadOptions(node,
+      opt_options ? opt_options : {}));
+  var objectStack = [context];
+  this.gmlFormat_.FEATURE_COLLECTION_PARSERS[ol.format.GMLBase.GMLNS][
+      'featureMember'] =
+      ol.xml.makeArrayPusher(ol.format.GMLBase.prototype.readFeaturesInternal);
+  var features = ol.xml.pushParseAndPop([],
+      this.gmlFormat_.FEATURE_COLLECTION_PARSERS, node,
+      objectStack, this.gmlFormat_);
+  if (!features) {
+    features = [];
+  }
+  return features;
+};
+
+
+/**
+ * Read transaction response of the source.
+ *
+ * @param {Document|Node|Object|string} source Source.
+ * @return {ol.WFSTransactionResponse|undefined} Transaction response.
+ * @api stable
+ */
+ol.format.WFS.prototype.readTransactionResponse = function(source) {
+  if (ol.xml.isDocument(source)) {
+    return this.readTransactionResponseFromDocument(
+        /** @type {Document} */ (source));
+  } else if (ol.xml.isNode(source)) {
+    return this.readTransactionResponseFromNode(/** @type {Node} */ (source));
+  } else if (typeof source === 'string') {
+    var doc = ol.xml.parse(source);
+    return this.readTransactionResponseFromDocument(doc);
+  } else {
+    goog.asserts.fail('Unknown source type');
+    return undefined;
+  }
+};
+
+
+/**
+ * Read feature collection metadata of the source.
+ *
+ * @param {Document|Node|Object|string} source Source.
+ * @return {ol.WFSFeatureCollectionMetadata|undefined}
+ *     FeatureCollection metadata.
+ * @api stable
+ */
+ol.format.WFS.prototype.readFeatureCollectionMetadata = function(source) {
+  if (ol.xml.isDocument(source)) {
+    return this.readFeatureCollectionMetadataFromDocument(
+        /** @type {Document} */ (source));
+  } else if (ol.xml.isNode(source)) {
+    return this.readFeatureCollectionMetadataFromNode(
+        /** @type {Node} */ (source));
+  } else if (typeof source === 'string') {
+    var doc = ol.xml.parse(source);
+    return this.readFeatureCollectionMetadataFromDocument(doc);
+  } else {
+    goog.asserts.fail('Unknown source type');
+    return undefined;
+  }
+};
+
+
+/**
+ * @param {Document} doc Document.
+ * @return {ol.WFSFeatureCollectionMetadata|undefined}
+ *     FeatureCollection metadata.
+ */
+ol.format.WFS.prototype.readFeatureCollectionMetadataFromDocument = function(doc) {
+  goog.asserts.assert(doc.nodeType == Node.DOCUMENT_NODE,
+      'doc.nodeType should be DOCUMENT');
+  for (var n = doc.firstChild; n; n = n.nextSibling) {
+    if (n.nodeType == Node.ELEMENT_NODE) {
+      return this.readFeatureCollectionMetadataFromNode(n);
+    }
+  }
+  return undefined;
+};
+
+
+/**
+ * @const
+ * @type {Object.<string, Object.<string, ol.XmlParser>>}
+ * @private
+ */
+ol.format.WFS.FEATURE_COLLECTION_PARSERS_ = {
+  'http://www.opengis.net/gml': {
+    'boundedBy': ol.xml.makeObjectPropertySetter(
+        ol.format.GMLBase.prototype.readGeometryElement, 'bounds')
+  }
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @return {ol.WFSFeatureCollectionMetadata|undefined}
+ *     FeatureCollection metadata.
+ */
+ol.format.WFS.prototype.readFeatureCollectionMetadataFromNode = function(node) {
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'node.nodeType should be ELEMENT');
+  goog.asserts.assert(node.localName == 'FeatureCollection',
+      'localName should be FeatureCollection');
+  var result = {};
+  var value = ol.format.XSD.readNonNegativeIntegerString(
+      node.getAttribute('numberOfFeatures'));
+  result['numberOfFeatures'] = value;
+  return ol.xml.pushParseAndPop(
+      /** @type {ol.WFSFeatureCollectionMetadata} */ (result),
+      ol.format.WFS.FEATURE_COLLECTION_PARSERS_, node, [], this.gmlFormat_);
+};
+
+
+/**
+ * @const
+ * @type {Object.<string, Object.<string, ol.XmlParser>>}
+ * @private
+ */
+ol.format.WFS.TRANSACTION_SUMMARY_PARSERS_ = {
+  'http://www.opengis.net/wfs': {
+    'totalInserted': ol.xml.makeObjectPropertySetter(
+        ol.format.XSD.readNonNegativeInteger),
+    'totalUpdated': ol.xml.makeObjectPropertySetter(
+        ol.format.XSD.readNonNegativeInteger),
+    'totalDeleted': ol.xml.makeObjectPropertySetter(
+        ol.format.XSD.readNonNegativeInteger)
+  }
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {Array.<*>} objectStack Object stack.
+ * @return {Object|undefined} Transaction Summary.
+ * @private
+ */
+ol.format.WFS.readTransactionSummary_ = function(node, objectStack) {
+  return ol.xml.pushParseAndPop(
+      {}, ol.format.WFS.TRANSACTION_SUMMARY_PARSERS_, node, objectStack);
+};
+
+
+/**
+ * @const
+ * @type {Object.<string, Object.<string, ol.XmlParser>>}
+ * @private
+ */
+ol.format.WFS.OGC_FID_PARSERS_ = {
+  'http://www.opengis.net/ogc': {
+    'FeatureId': ol.xml.makeArrayPusher(function(node, objectStack) {
+      return node.getAttribute('fid');
+    })
+  }
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {Array.<*>} objectStack Object stack.
+ * @private
+ */
+ol.format.WFS.fidParser_ = function(node, objectStack) {
+  ol.xml.parseNode(ol.format.WFS.OGC_FID_PARSERS_, node, objectStack);
+};
+
+
+/**
+ * @const
+ * @type {Object.<string, Object.<string, ol.XmlParser>>}
+ * @private
+ */
+ol.format.WFS.INSERT_RESULTS_PARSERS_ = {
+  'http://www.opengis.net/wfs': {
+    'Feature': ol.format.WFS.fidParser_
+  }
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {Array.<*>} objectStack Object stack.
+ * @return {Array.<string>|undefined} Insert results.
+ * @private
+ */
+ol.format.WFS.readInsertResults_ = function(node, objectStack) {
+  return ol.xml.pushParseAndPop(
+      [], ol.format.WFS.INSERT_RESULTS_PARSERS_, node, objectStack);
+};
+
+
+/**
+ * @const
+ * @type {Object.<string, Object.<string, ol.XmlParser>>}
+ * @private
+ */
+ol.format.WFS.TRANSACTION_RESPONSE_PARSERS_ = {
+  'http://www.opengis.net/wfs': {
+    'TransactionSummary': ol.xml.makeObjectPropertySetter(
+        ol.format.WFS.readTransactionSummary_, 'transactionSummary'),
+    'InsertResults': ol.xml.makeObjectPropertySetter(
+        ol.format.WFS.readInsertResults_, 'insertIds')
+  }
+};
+
+
+/**
+ * @param {Document} doc Document.
+ * @return {ol.WFSTransactionResponse|undefined} Transaction response.
+ */
+ol.format.WFS.prototype.readTransactionResponseFromDocument = function(doc) {
+  goog.asserts.assert(doc.nodeType == Node.DOCUMENT_NODE,
+      'doc.nodeType should be DOCUMENT');
+  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 {ol.WFSTransactionResponse|undefined} Transaction response.
+ */
+ol.format.WFS.prototype.readTransactionResponseFromNode = function(node) {
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'node.nodeType should  be ELEMENT');
+  goog.asserts.assert(node.localName == 'TransactionResponse',
+      'localName should be TransactionResponse');
+  return ol.xml.pushParseAndPop(
+      /** @type {ol.WFSTransactionResponse} */({}),
+      ol.format.WFS.TRANSACTION_RESPONSE_PARSERS_, node, []);
+};
+
+
+/**
+ * @type {Object.<string, Object.<string, ol.XmlSerializer>>}
+ * @private
+ */
+ol.format.WFS.QUERY_SERIALIZERS_ = {
+  'http://www.opengis.net/wfs': {
+    'PropertyName': ol.xml.makeChildAppender(ol.format.XSD.writeStringTextNode)
+  }
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {ol.Feature} feature Feature.
+ * @param {Array.<*>} objectStack Node stack.
+ * @private
+ */
+ol.format.WFS.writeFeature_ = function(node, feature, objectStack) {
+  var context = objectStack[objectStack.length - 1];
+  goog.asserts.assert(goog.isObject(context), 'context should be an Object');
+  var featureType = context['featureType'];
+  var featureNS = context['featureNS'];
+  var child = ol.xml.createElementNS(featureNS, featureType);
+  node.appendChild(child);
+  ol.format.GML3.prototype.writeFeatureElement(child, feature, objectStack);
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {number|string} fid Feature identifier.
+ * @param {Array.<*>} objectStack Node stack.
+ * @private
+ */
+ol.format.WFS.writeOgcFidFilter_ = function(node, fid, objectStack) {
+  var filter = ol.xml.createElementNS('http://www.opengis.net/ogc', 'Filter');
+  var child = ol.xml.createElementNS('http://www.opengis.net/ogc', 'FeatureId');
+  filter.appendChild(child);
+  child.setAttribute('fid', fid);
+  node.appendChild(filter);
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {ol.Feature} feature Feature.
+ * @param {Array.<*>} objectStack Node stack.
+ * @private
+ */
+ol.format.WFS.writeDelete_ = function(node, feature, objectStack) {
+  var context = objectStack[objectStack.length - 1];
+  goog.asserts.assert(goog.isObject(context), 'context should be an Object');
+  goog.asserts.assert(feature.getId() !== undefined, 'feature should have an id');
+  var featureType = context['featureType'];
+  var featurePrefix = context['featurePrefix'];
+  featurePrefix = featurePrefix ? featurePrefix :
+      ol.format.WFS.FEATURE_PREFIX;
+  var featureNS = context['featureNS'];
+  node.setAttribute('typeName', featurePrefix + ':' + featureType);
+  ol.xml.setAttributeNS(node, ol.format.WFS.XMLNS, 'xmlns:' + featurePrefix,
+      featureNS);
+  var fid = feature.getId();
+  if (fid !== undefined) {
+    ol.format.WFS.writeOgcFidFilter_(node, fid, objectStack);
+  }
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {ol.Feature} feature Feature.
+ * @param {Array.<*>} objectStack Node stack.
+ * @private
+ */
+ol.format.WFS.writeUpdate_ = function(node, feature, objectStack) {
+  var context = objectStack[objectStack.length - 1];
+  goog.asserts.assert(goog.isObject(context), 'context should be an Object');
+  goog.asserts.assert(feature.getId() !== undefined, 'feature should have an id');
+  var featureType = context['featureType'];
+  var featurePrefix = context['featurePrefix'];
+  featurePrefix = featurePrefix ? featurePrefix :
+      ol.format.WFS.FEATURE_PREFIX;
+  var featureNS = context['featureNS'];
+  node.setAttribute('typeName', featurePrefix + ':' + featureType);
+  ol.xml.setAttributeNS(node, ol.format.WFS.XMLNS, 'xmlns:' + featurePrefix,
+      featureNS);
+  var fid = feature.getId();
+  if (fid !== 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});
+      }
+    }
+    ol.xml.pushSerializeAndPop(/** @type {ol.XmlNodeStackItem} */ (
+        {node: node, 'srsName': context['srsName']}),
+        ol.format.WFS.TRANSACTION_SERIALIZERS_,
+        ol.xml.makeSimpleNodeFactory('Property'), values,
+        objectStack);
+    ol.format.WFS.writeOgcFidFilter_(node, fid, objectStack);
+  }
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {Object} pair Property name and value.
+ * @param {Array.<*>} objectStack Node stack.
+ * @private
+ */
+ol.format.WFS.writeProperty_ = function(node, pair, objectStack) {
+  var name = ol.xml.createElementNS('http://www.opengis.net/wfs', 'Name');
+  node.appendChild(name);
+  ol.format.XSD.writeStringTextNode(name, pair.name);
+  if (pair.value !== undefined && pair.value !== null) {
+    var value = ol.xml.createElementNS('http://www.opengis.net/wfs', 'Value');
+    node.appendChild(value);
+    if (pair.value instanceof ol.geom.Geometry) {
+      ol.format.GML3.prototype.writeGeometryElement(value,
+          pair.value, objectStack);
+    } else {
+      ol.format.XSD.writeStringTextNode(value, pair.value);
+    }
+  }
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {{vendorId: string, safeToIgnore: boolean, value: string}}
+ *     nativeElement The native element.
+ * @param {Array.<*>} objectStack Node stack.
+ * @private
+ */
+ol.format.WFS.writeNative_ = function(node, nativeElement, objectStack) {
+  if (nativeElement.vendorId) {
+    node.setAttribute('vendorId', nativeElement.vendorId);
+  }
+  if (nativeElement.safeToIgnore !== undefined) {
+    node.setAttribute('safeToIgnore', nativeElement.safeToIgnore);
+  }
+  if (nativeElement.value !== undefined) {
+    ol.format.XSD.writeStringTextNode(node, nativeElement.value);
+  }
+};
+
+
+/**
+ * @type {Object.<string, Object.<string, ol.XmlSerializer>>}
+ * @private
+ */
+ol.format.WFS.TRANSACTION_SERIALIZERS_ = {
+  'http://www.opengis.net/wfs': {
+    'Insert': ol.xml.makeChildAppender(ol.format.WFS.writeFeature_),
+    'Update': ol.xml.makeChildAppender(ol.format.WFS.writeUpdate_),
+    'Delete': ol.xml.makeChildAppender(ol.format.WFS.writeDelete_),
+    'Property': ol.xml.makeChildAppender(ol.format.WFS.writeProperty_),
+    'Native': ol.xml.makeChildAppender(ol.format.WFS.writeNative_)
+  }
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {string} featureType Feature type.
+ * @param {Array.<*>} objectStack Node stack.
+ * @private
+ */
+ol.format.WFS.writeQuery_ = function(node, featureType, objectStack) {
+  var context = objectStack[objectStack.length - 1];
+  goog.asserts.assert(goog.isObject(context), 'context should be an Object');
+  var featurePrefix = context['featurePrefix'];
+  var featureNS = context['featureNS'];
+  var propertyNames = context['propertyNames'];
+  var srsName = context['srsName'];
+  var prefix = featurePrefix ? featurePrefix + ':' : '';
+  node.setAttribute('typeName', prefix + featureType);
+  if (srsName) {
+    node.setAttribute('srsName', srsName);
+  }
+  if (featureNS) {
+    ol.xml.setAttributeNS(node, ol.format.WFS.XMLNS, 'xmlns:' + featurePrefix,
+        featureNS);
+  }
+  var item = /** @type {ol.XmlNodeStackItem} */ (ol.object.assign({}, context));
+  item.node = node;
+  ol.xml.pushSerializeAndPop(item,
+      ol.format.WFS.QUERY_SERIALIZERS_,
+      ol.xml.makeSimpleNodeFactory('PropertyName'), propertyNames,
+      objectStack);
+  var filter = context['filter'];
+  if (filter) {
+    var child = ol.xml.createElementNS('http://www.opengis.net/ogc', 'Filter');
+    node.appendChild(child);
+    ol.format.WFS.writeFilterCondition_(child, filter, objectStack);
+  }
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {ol.format.ogc.filter.Filter} filter Filter.
+ * @param {Array.<*>} objectStack Node stack.
+ * @private
+ */
+ol.format.WFS.writeFilterCondition_ = function(node, filter, objectStack) {
+  /** @type {ol.XmlNodeStackItem} */
+  var item = {node: node};
+  ol.xml.pushSerializeAndPop(item,
+      ol.format.WFS.GETFEATURE_SERIALIZERS_,
+      ol.xml.makeSimpleNodeFactory(filter.getTagName()),
+      [filter], objectStack);
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {ol.format.ogc.filter.Filter} filter Filter.
+ * @param {Array.<*>} objectStack Node stack.
+ * @private
+ */
+ol.format.WFS.writeBboxFilter_ = function(node, filter, objectStack) {
+  goog.asserts.assertInstanceof(filter, ol.format.ogc.filter.Bbox,
+    'must be bbox filter');
+
+  var context = objectStack[objectStack.length - 1];
+  goog.asserts.assert(goog.isObject(context), 'context should be an Object');
+  context['srsName'] = filter.srsName;
+
+  ol.format.WFS.writeOgcPropertyName_(node, filter.geometryName);
+  ol.format.GML3.prototype.writeGeometryElement(node, filter.extent, objectStack);
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {ol.format.ogc.filter.Filter} filter Filter.
+ * @param {Array.<*>} objectStack Node stack.
+ * @private
+ */
+ol.format.WFS.writeLogicalFilter_ = function(node, filter, objectStack) {
+  goog.asserts.assertInstanceof(filter, ol.format.ogc.filter.LogicalBinary,
+    'must be logical filter');
+  /** @type {ol.XmlNodeStackItem} */
+  var item = {node: node};
+  var conditionA = filter.conditionA;
+  ol.xml.pushSerializeAndPop(item,
+      ol.format.WFS.GETFEATURE_SERIALIZERS_,
+      ol.xml.makeSimpleNodeFactory(conditionA.getTagName()),
+      [conditionA], objectStack);
+  var conditionB = filter.conditionB;
+  ol.xml.pushSerializeAndPop(item,
+      ol.format.WFS.GETFEATURE_SERIALIZERS_,
+      ol.xml.makeSimpleNodeFactory(conditionB.getTagName()),
+      [conditionB], objectStack);
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {ol.format.ogc.filter.Filter} filter Filter.
+ * @param {Array.<*>} objectStack Node stack.
+ * @private
+ */
+ol.format.WFS.writeNotFilter_ = function(node, filter, objectStack) {
+  goog.asserts.assertInstanceof(filter, ol.format.ogc.filter.Not,
+    'must be Not filter');
+  /** @type {ol.XmlNodeStackItem} */
+  var item = {node: node};
+  var condition = filter.condition;
+  ol.xml.pushSerializeAndPop(item,
+      ol.format.WFS.GETFEATURE_SERIALIZERS_,
+      ol.xml.makeSimpleNodeFactory(condition.getTagName()),
+      [condition], objectStack);
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {ol.format.ogc.filter.Filter} filter Filter.
+ * @param {Array.<*>} objectStack Node stack.
+ * @private
+ */
+ol.format.WFS.writeComparisonFilter_ = function(node, filter, objectStack) {
+  goog.asserts.assertInstanceof(filter, ol.format.ogc.filter.ComparisonBinary,
+    'must be binary comparison filter');
+  if (filter.matchCase !== undefined) {
+    node.setAttribute('matchCase', filter.matchCase.toString());
+  }
+  ol.format.WFS.writeOgcPropertyName_(node, filter.propertyName);
+  ol.format.WFS.writeOgcLiteral_(node, '' + filter.expression);
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {ol.format.ogc.filter.Filter} filter Filter.
+ * @param {Array.<*>} objectStack Node stack.
+ * @private
+ */
+ol.format.WFS.writeIsNullFilter_ = function(node, filter, objectStack) {
+  goog.asserts.assertInstanceof(filter, ol.format.ogc.filter.IsNull,
+    'must be IsNull comparison filter');
+  ol.format.WFS.writeOgcPropertyName_(node, filter.propertyName);
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {ol.format.ogc.filter.Filter} filter Filter.
+ * @param {Array.<*>} objectStack Node stack.
+ * @private
+ */
+ol.format.WFS.writeIsBetweenFilter_ = function(node, filter, objectStack) {
+  goog.asserts.assertInstanceof(filter, ol.format.ogc.filter.IsBetween,
+    'must be IsBetween comparison filter');
+  ol.format.WFS.writeOgcPropertyName_(node, filter.propertyName);
+  ol.format.WFS.writeOgcExpression_('LowerBoundary', node, '' + filter.lowerBoundary);
+  ol.format.WFS.writeOgcExpression_('UpperBoundary', node, '' + filter.upperBoundary);
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {ol.format.ogc.filter.Filter} filter Filter.
+ * @param {Array.<*>} objectStack Node stack.
+ * @private
+ */
+ol.format.WFS.writeIsLikeFilter_ = function(node, filter, objectStack) {
+  goog.asserts.assertInstanceof(filter, ol.format.ogc.filter.IsLike,
+    'must be IsLike comparison filter');
+  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());
+  }
+  ol.format.WFS.writeOgcPropertyName_(node, filter.propertyName);
+  ol.format.WFS.writeOgcLiteral_(node, '' + filter.pattern);
+};
+
+
+/**
+ * @param {string} tagName Tag name.
+ * @param {Node} node Node.
+ * @param {string} value Value.
+ * @private
+ */
+ol.format.WFS.writeOgcExpression_ = function(tagName, node, value) {
+  var property = ol.xml.createElementNS('http://www.opengis.net/ogc', tagName);
+  ol.format.XSD.writeStringTextNode(property, value);
+  node.appendChild(property);
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {string} value PropertyName value.
+ * @private
+ */
+ol.format.WFS.writeOgcPropertyName_ = function(node, value) {
+  ol.format.WFS.writeOgcExpression_('PropertyName', node, value);
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {string} value PropertyName value.
+ * @private
+ */
+ol.format.WFS.writeOgcLiteral_ = function(node, value) {
+  ol.format.WFS.writeOgcExpression_('Literal', node, value);
+};
+
+
+/**
+ * @type {Object.<string, Object.<string, ol.XmlSerializer>>}
+ * @private
+ */
+ol.format.WFS.GETFEATURE_SERIALIZERS_ = {
+  'http://www.opengis.net/wfs': {
+    'Query': ol.xml.makeChildAppender(ol.format.WFS.writeQuery_)
+  },
+  'http://www.opengis.net/ogc': {
+    'And': ol.xml.makeChildAppender(ol.format.WFS.writeLogicalFilter_),
+    'Or': ol.xml.makeChildAppender(ol.format.WFS.writeLogicalFilter_),
+    'Not': ol.xml.makeChildAppender(ol.format.WFS.writeNotFilter_),
+    'BBOX': ol.xml.makeChildAppender(ol.format.WFS.writeBboxFilter_),
+    'PropertyIsEqualTo': ol.xml.makeChildAppender(ol.format.WFS.writeComparisonFilter_),
+    'PropertyIsNotEqualTo': ol.xml.makeChildAppender(ol.format.WFS.writeComparisonFilter_),
+    'PropertyIsLessThan': ol.xml.makeChildAppender(ol.format.WFS.writeComparisonFilter_),
+    'PropertyIsLessThanOrEqualTo': ol.xml.makeChildAppender(ol.format.WFS.writeComparisonFilter_),
+    'PropertyIsGreaterThan': ol.xml.makeChildAppender(ol.format.WFS.writeComparisonFilter_),
+    'PropertyIsGreaterThanOrEqualTo': ol.xml.makeChildAppender(ol.format.WFS.writeComparisonFilter_),
+    'PropertyIsNull': ol.xml.makeChildAppender(ol.format.WFS.writeIsNullFilter_),
+    'PropertyIsBetween': ol.xml.makeChildAppender(ol.format.WFS.writeIsBetweenFilter_),
+    'PropertyIsLike': ol.xml.makeChildAppender(ol.format.WFS.writeIsLikeFilter_)
+  }
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {Array.<{string}>} featureTypes Feature types.
+ * @param {Array.<*>} objectStack Node stack.
+ * @private
+ */
+ol.format.WFS.writeGetFeature_ = function(node, featureTypes, objectStack) {
+  var context = objectStack[objectStack.length - 1];
+  goog.asserts.assert(goog.isObject(context), 'context should be an Object');
+  var item = /** @type {ol.XmlNodeStackItem} */ (ol.object.assign({}, context));
+  item.node = node;
+  ol.xml.pushSerializeAndPop(item,
+      ol.format.WFS.GETFEATURE_SERIALIZERS_,
+      ol.xml.makeSimpleNodeFactory('Query'), featureTypes,
+      objectStack);
+};
+
+
+/**
+ * Encode format as WFS `GetFeature` and return the Node.
+ *
+ * @param {olx.format.WFSWriteGetFeatureOptions} options Options.
+ * @return {Node} Result.
+ * @api stable
+ */
+ol.format.WFS.prototype.writeGetFeature = function(options) {
+  var node = ol.xml.createElementNS('http://www.opengis.net/wfs',
+      'GetFeature');
+  node.setAttribute('service', 'WFS');
+  node.setAttribute('version', '1.1.0');
+  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) {
+      goog.asserts.assert(options.geometryName,
+        'geometryName must be set when using bbox filter');
+      var bbox = ol.format.ogc.filter.bbox(
+          options.geometryName, options.bbox, options.srsName);
+      if (filter) {
+        // if bbox and filter are both set, combine the two into a single filter
+        filter = ol.format.ogc.filter.and(filter, bbox);
+      } else {
+        filter = bbox;
+      }
+    }
+  }
+  ol.xml.setAttributeNS(node, 'http://www.w3.org/2001/XMLSchema-instance',
+      'xsi:schemaLocation', this.schemaLocation_);
+  /** @type {ol.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 : []
+  };
+  goog.asserts.assert(Array.isArray(options.featureTypes),
+      'options.featureTypes should be an array');
+  ol.format.WFS.writeGetFeature_(node, options.featureTypes, [context]);
+  return node;
+};
+
+
+/**
+ * Encode format as WFS `Transaction` and return the Node.
+ *
+ * @param {Array.<ol.Feature>} inserts The features to insert.
+ * @param {Array.<ol.Feature>} updates The features to update.
+ * @param {Array.<ol.Feature>} deletes The features to delete.
+ * @param {olx.format.WFSWriteTransactionOptions} options Write options.
+ * @return {Node} Result.
+ * @api stable
+ */
+ol.format.WFS.prototype.writeTransaction = function(inserts, updates, deletes,
+    options) {
+  var objectStack = [];
+  var node = ol.xml.createElementNS('http://www.opengis.net/wfs',
+      'Transaction');
+  node.setAttribute('service', 'WFS');
+  node.setAttribute('version', '1.1.0');
+  var baseObj;
+  /** @type {ol.XmlNodeStackItem} */
+  var obj;
+  if (options) {
+    baseObj = options.gmlOptions ? options.gmlOptions : {};
+    if (options.handle) {
+      node.setAttribute('handle', options.handle);
+    }
+  }
+  ol.xml.setAttributeNS(node, 'http://www.w3.org/2001/XMLSchema-instance',
+      'xsi:schemaLocation', this.schemaLocation_);
+  if (inserts) {
+    obj = {node: node, 'featureNS': options.featureNS,
+      'featureType': options.featureType, 'featurePrefix': options.featurePrefix,
+      'srsName': options.srsName};
+    ol.object.assign(obj, baseObj);
+    ol.xml.pushSerializeAndPop(obj,
+        ol.format.WFS.TRANSACTION_SERIALIZERS_,
+        ol.xml.makeSimpleNodeFactory('Insert'), inserts,
+        objectStack);
+  }
+  if (updates) {
+    obj = {node: node, 'featureNS': options.featureNS,
+      'featureType': options.featureType, 'featurePrefix': options.featurePrefix,
+      'srsName': options.srsName};
+    ol.object.assign(obj, baseObj);
+    ol.xml.pushSerializeAndPop(obj,
+        ol.format.WFS.TRANSACTION_SERIALIZERS_,
+        ol.xml.makeSimpleNodeFactory('Update'), updates,
+        objectStack);
+  }
+  if (deletes) {
+    ol.xml.pushSerializeAndPop({node: node, 'featureNS': options.featureNS,
+      'featureType': options.featureType, 'featurePrefix': options.featurePrefix,
+      'srsName': options.srsName},
+    ol.format.WFS.TRANSACTION_SERIALIZERS_,
+    ol.xml.makeSimpleNodeFactory('Delete'), deletes,
+    objectStack);
+  }
+  if (options.nativeElements) {
+    ol.xml.pushSerializeAndPop({node: node, 'featureNS': options.featureNS,
+      'featureType': options.featureType, 'featurePrefix': options.featurePrefix,
+      'srsName': options.srsName},
+    ol.format.WFS.TRANSACTION_SERIALIZERS_,
+    ol.xml.makeSimpleNodeFactory('Native'), options.nativeElements,
+    objectStack);
+  }
+  return node;
+};
+
+
+/**
+ * Read the projection from a WFS source.
+ *
+ * @function
+ * @param {Document|Node|Object|string} source Source.
+ * @return {?ol.proj.Projection} Projection.
+ * @api stable
+ */
+ol.format.WFS.prototype.readProjection;
+
+
+/**
+ * @inheritDoc
+ */
+ol.format.WFS.prototype.readProjectionFromDocument = function(doc) {
+  goog.asserts.assert(doc.nodeType == Node.DOCUMENT_NODE,
+      'doc.nodeType should be a DOCUMENT');
+  for (var n = doc.firstChild; n; n = n.nextSibling) {
+    if (n.nodeType == Node.ELEMENT_NODE) {
+      return this.readProjectionFromNode(n);
+    }
+  }
+  return null;
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.format.WFS.prototype.readProjectionFromNode = function(node) {
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'node.nodeType should be ELEMENT');
+  goog.asserts.assert(node.localName == 'FeatureCollection',
+      'localName should be FeatureCollection');
+
+  if (node.firstElementChild &&
+      node.firstElementChild.firstElementChild) {
+    node = node.firstElementChild.firstElementChild;
+    for (var n = node.firstElementChild; n; n = n.nextElementSibling) {
+      if (!(n.childNodes.length === 0 ||
+          (n.childNodes.length === 1 &&
+          n.firstChild.nodeType === 3))) {
+        var objectStack = [{}];
+        this.gmlFormat_.readGeometryElement(n, objectStack);
+        return ol.proj.get(objectStack.pop().srsName);
+      }
+    }
+  }
+
+  return null;
+};
+
+goog.provide('ol.format.WKT');
+
+goog.require('goog.asserts');
+goog.require('ol');
+goog.require('ol.Feature');
+goog.require('ol.format.Feature');
+goog.require('ol.format.TextFeature');
+goog.require('ol.geom.Geometry');
+goog.require('ol.geom.GeometryCollection');
+goog.require('ol.geom.GeometryType');
+goog.require('ol.geom.LineString');
+goog.require('ol.geom.MultiLineString');
+goog.require('ol.geom.MultiPoint');
+goog.require('ol.geom.MultiPolygon');
+goog.require('ol.geom.Point');
+goog.require('ol.geom.Polygon');
+
+
+/**
+ * @classdesc
+ * Geometry format for reading and writing data in the `WellKnownText` (WKT)
+ * format.
+ *
+ * @constructor
+ * @extends {ol.format.TextFeature}
+ * @param {olx.format.WKTOptions=} opt_options Options.
+ * @api stable
+ */
+ol.format.WKT = function(opt_options) {
+
+  var options = opt_options ? opt_options : {};
+
+  ol.format.TextFeature.call(this);
+
+  /**
+   * Split GeometryCollection into multiple features.
+   * @type {boolean}
+   * @private
+   */
+  this.splitCollection_ = options.splitCollection !== undefined ?
+      options.splitCollection : false;
+
+};
+ol.inherits(ol.format.WKT, ol.format.TextFeature);
+
+
+/**
+ * @const
+ * @type {string}
+ */
+ol.format.WKT.EMPTY = 'EMPTY';
+
+
+/**
+ * @param {ol.geom.Point} geom Point geometry.
+ * @return {string} Coordinates part of Point as WKT.
+ * @private
+ */
+ol.format.WKT.encodePointGeometry_ = function(geom) {
+  var coordinates = geom.getCoordinates();
+  if (coordinates.length === 0) {
+    return '';
+  }
+  return coordinates[0] + ' ' + coordinates[1];
+};
+
+
+/**
+ * @param {ol.geom.MultiPoint} geom MultiPoint geometry.
+ * @return {string} Coordinates part of MultiPoint as WKT.
+ * @private
+ */
+ol.format.WKT.encodeMultiPointGeometry_ = function(geom) {
+  var array = [];
+  var components = geom.getPoints();
+  for (var i = 0, ii = components.length; i < ii; ++i) {
+    array.push('(' + ol.format.WKT.encodePointGeometry_(components[i]) + ')');
+  }
+  return array.join(',');
+};
+
+
+/**
+ * @param {ol.geom.GeometryCollection} geom GeometryCollection geometry.
+ * @return {string} Coordinates part of GeometryCollection as WKT.
+ * @private
+ */
+ol.format.WKT.encodeGeometryCollectionGeometry_ = function(geom) {
+  var array = [];
+  var geoms = geom.getGeometries();
+  for (var i = 0, ii = geoms.length; i < ii; ++i) {
+    array.push(ol.format.WKT.encode_(geoms[i]));
+  }
+  return array.join(',');
+};
+
+
+/**
+ * @param {ol.geom.LineString|ol.geom.LinearRing} geom LineString geometry.
+ * @return {string} Coordinates part of LineString as WKT.
+ * @private
+ */
+ol.format.WKT.encodeLineStringGeometry_ = function(geom) {
+  var coordinates = geom.getCoordinates();
+  var array = [];
+  for (var i = 0, ii = coordinates.length; i < ii; ++i) {
+    array.push(coordinates[i][0] + ' ' + coordinates[i][1]);
+  }
+  return array.join(',');
+};
+
+
+/**
+ * @param {ol.geom.MultiLineString} geom MultiLineString geometry.
+ * @return {string} Coordinates part of MultiLineString as WKT.
+ * @private
+ */
+ol.format.WKT.encodeMultiLineStringGeometry_ = function(geom) {
+  var array = [];
+  var components = geom.getLineStrings();
+  for (var i = 0, ii = components.length; i < ii; ++i) {
+    array.push('(' + ol.format.WKT.encodeLineStringGeometry_(
+        components[i]) + ')');
+  }
+  return array.join(',');
+};
+
+
+/**
+ * @param {ol.geom.Polygon} geom Polygon geometry.
+ * @return {string} Coordinates part of Polygon as WKT.
+ * @private
+ */
+ol.format.WKT.encodePolygonGeometry_ = function(geom) {
+  var array = [];
+  var rings = geom.getLinearRings();
+  for (var i = 0, ii = rings.length; i < ii; ++i) {
+    array.push('(' + ol.format.WKT.encodeLineStringGeometry_(
+        rings[i]) + ')');
+  }
+  return array.join(',');
+};
+
+
+/**
+ * @param {ol.geom.MultiPolygon} geom MultiPolygon geometry.
+ * @return {string} Coordinates part of MultiPolygon as WKT.
+ * @private
+ */
+ol.format.WKT.encodeMultiPolygonGeometry_ = function(geom) {
+  var array = [];
+  var components = geom.getPolygons();
+  for (var i = 0, ii = components.length; i < ii; ++i) {
+    array.push('(' + ol.format.WKT.encodePolygonGeometry_(
+        components[i]) + ')');
+  }
+  return array.join(',');
+};
+
+
+/**
+ * Encode a geometry as WKT.
+ * @param {ol.geom.Geometry} geom The geometry to encode.
+ * @return {string} WKT string for the geometry.
+ * @private
+ */
+ol.format.WKT.encode_ = function(geom) {
+  var type = geom.getType();
+  var geometryEncoder = ol.format.WKT.GeometryEncoder_[type];
+  goog.asserts.assert(geometryEncoder, 'geometryEncoder should be defined');
+  var enc = geometryEncoder(geom);
+  type = type.toUpperCase();
+  if (enc.length === 0) {
+    return type + ' ' + ol.format.WKT.EMPTY;
+  }
+  return type + '(' + enc + ')';
+};
+
+
+/**
+ * @const
+ * @type {Object.<string, function(ol.geom.Geometry): string>}
+ * @private
+ */
+ol.format.WKT.GeometryEncoder_ = {
+  'Point': ol.format.WKT.encodePointGeometry_,
+  'LineString': ol.format.WKT.encodeLineStringGeometry_,
+  'Polygon': ol.format.WKT.encodePolygonGeometry_,
+  'MultiPoint': ol.format.WKT.encodeMultiPointGeometry_,
+  'MultiLineString': ol.format.WKT.encodeMultiLineStringGeometry_,
+  'MultiPolygon': ol.format.WKT.encodeMultiPolygonGeometry_,
+  'GeometryCollection': ol.format.WKT.encodeGeometryCollectionGeometry_
+};
+
+
+/**
+ * Parse a WKT string.
+ * @param {string} wkt WKT string.
+ * @return {ol.geom.Geometry|ol.geom.GeometryCollection|undefined}
+ *     The geometry created.
+ * @private
+ */
+ol.format.WKT.prototype.parse_ = function(wkt) {
+  var lexer = new ol.format.WKT.Lexer(wkt);
+  var parser = new ol.format.WKT.Parser(lexer);
+  return parser.parse();
+};
+
+
+/**
+ * Read a feature from a WKT source.
+ *
+ * @function
+ * @param {Document|Node|Object|string} source Source.
+ * @param {olx.format.ReadOptions=} opt_options Read options.
+ * @return {ol.Feature} Feature.
+ * @api stable
+ */
+ol.format.WKT.prototype.readFeature;
+
+
+/**
+ * @inheritDoc
+ */
+ol.format.WKT.prototype.readFeatureFromText = function(text, opt_options) {
+  var geom = this.readGeometryFromText(text, opt_options);
+  if (geom) {
+    var feature = new ol.Feature();
+    feature.setGeometry(geom);
+    return feature;
+  }
+  return null;
+};
+
+
+/**
+ * Read all features from a WKT source.
+ *
+ * @function
+ * @param {Document|Node|Object|string} source Source.
+ * @param {olx.format.ReadOptions=} opt_options Read options.
+ * @return {Array.<ol.Feature>} Features.
+ * @api stable
+ */
+ol.format.WKT.prototype.readFeatures;
+
+
+/**
+ * @inheritDoc
+ */
+ol.format.WKT.prototype.readFeaturesFromText = function(text, opt_options) {
+  var geometries = [];
+  var geometry = this.readGeometryFromText(text, opt_options);
+  if (this.splitCollection_ &&
+      geometry.getType() == ol.geom.GeometryType.GEOMETRY_COLLECTION) {
+    geometries = (/** @type {ol.geom.GeometryCollection} */ (geometry))
+        .getGeometriesArray();
+  } else {
+    geometries = [geometry];
+  }
+  var feature, features = [];
+  for (var i = 0, ii = geometries.length; i < ii; ++i) {
+    feature = new ol.Feature();
+    feature.setGeometry(geometries[i]);
+    features.push(feature);
+  }
+  return features;
+};
+
+
+/**
+ * Read a single geometry from a WKT source.
+ *
+ * @function
+ * @param {Document|Node|Object|string} source Source.
+ * @param {olx.format.ReadOptions=} opt_options Read options.
+ * @return {ol.geom.Geometry} Geometry.
+ * @api stable
+ */
+ol.format.WKT.prototype.readGeometry;
+
+
+/**
+ * @inheritDoc
+ */
+ol.format.WKT.prototype.readGeometryFromText = function(text, opt_options) {
+  var geometry = this.parse_(text);
+  if (geometry) {
+    return /** @type {ol.geom.Geometry} */ (
+        ol.format.Feature.transformWithOptions(geometry, false, opt_options));
+  } else {
+    return null;
+  }
+};
+
+
+/**
+ * Encode a feature as a WKT string.
+ *
+ * @function
+ * @param {ol.Feature} feature Feature.
+ * @param {olx.format.WriteOptions=} opt_options Write options.
+ * @return {string} WKT string.
+ * @api stable
+ */
+ol.format.WKT.prototype.writeFeature;
+
+
+/**
+ * @inheritDoc
+ */
+ol.format.WKT.prototype.writeFeatureText = function(feature, opt_options) {
+  var geometry = feature.getGeometry();
+  if (geometry) {
+    return this.writeGeometryText(geometry, opt_options);
+  }
+  return '';
+};
+
+
+/**
+ * Encode an array of features as a WKT string.
+ *
+ * @function
+ * @param {Array.<ol.Feature>} features Features.
+ * @param {olx.format.WriteOptions=} opt_options Write options.
+ * @return {string} WKT string.
+ * @api stable
+ */
+ol.format.WKT.prototype.writeFeatures;
+
+
+/**
+ * @inheritDoc
+ */
+ol.format.WKT.prototype.writeFeaturesText = function(features, opt_options) {
+  if (features.length == 1) {
+    return this.writeFeatureText(features[0], opt_options);
+  }
+  var geometries = [];
+  for (var i = 0, ii = features.length; i < ii; ++i) {
+    geometries.push(features[i].getGeometry());
+  }
+  var collection = new ol.geom.GeometryCollection(geometries);
+  return this.writeGeometryText(collection, opt_options);
+};
+
+
+/**
+ * Write a single geometry as a WKT string.
+ *
+ * @function
+ * @param {ol.geom.Geometry} geometry Geometry.
+ * @return {string} WKT string.
+ * @api stable
+ */
+ol.format.WKT.prototype.writeGeometry;
+
+
+/**
+ * @inheritDoc
+ */
+ol.format.WKT.prototype.writeGeometryText = function(geometry, opt_options) {
+  return ol.format.WKT.encode_(/** @type {ol.geom.Geometry} */ (
+      ol.format.Feature.transformWithOptions(geometry, true, opt_options)));
+};
+
+
+/**
+ * @const
+ * @enum {number}
+ */
+ol.format.WKT.TokenType = {
+  TEXT: 1,
+  LEFT_PAREN: 2,
+  RIGHT_PAREN: 3,
+  NUMBER: 4,
+  COMMA: 5,
+  EOF: 6
+};
+
+
+/**
+ * Class to tokenize a WKT string.
+ * @param {string} wkt WKT string.
+ * @constructor
+ * @protected
+ */
+ol.format.WKT.Lexer = function(wkt) {
+
+  /**
+   * @type {string}
+   */
+  this.wkt = wkt;
+
+  /**
+   * @type {number}
+   * @private
+   */
+  this.index_ = -1;
+};
+
+
+/**
+ * @param {string} c Character.
+ * @return {boolean} Whether the character is alphabetic.
+ * @private
+ */
+ol.format.WKT.Lexer.prototype.isAlpha_ = function(c) {
+  return c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z';
+};
+
+
+/**
+ * @param {string} c Character.
+ * @param {boolean=} opt_decimal Whether the string number
+ *     contains a dot, i.e. is a decimal number.
+ * @return {boolean} Whether the character is numeric.
+ * @private
+ */
+ol.format.WKT.Lexer.prototype.isNumeric_ = function(c, opt_decimal) {
+  var decimal = opt_decimal !== undefined ? opt_decimal : false;
+  return c >= '0' && c <= '9' || c == '.' && !decimal;
+};
+
+
+/**
+ * @param {string} c Character.
+ * @return {boolean} Whether the character is whitespace.
+ * @private
+ */
+ol.format.WKT.Lexer.prototype.isWhiteSpace_ = function(c) {
+  return c == ' ' || c == '\t' || c == '\r' || c == '\n';
+};
+
+
+/**
+ * @return {string} Next string character.
+ * @private
+ */
+ol.format.WKT.Lexer.prototype.nextChar_ = function() {
+  return this.wkt.charAt(++this.index_);
+};
+
+
+/**
+ * Fetch and return the next token.
+ * @return {!ol.WKTToken} Next string token.
+ */
+ol.format.WKT.Lexer.prototype.nextToken = function() {
+  var c = this.nextChar_();
+  var token = {position: this.index_, value: c};
+
+  if (c == '(') {
+    token.type = ol.format.WKT.TokenType.LEFT_PAREN;
+  } else if (c == ',') {
+    token.type = ol.format.WKT.TokenType.COMMA;
+  } else if (c == ')') {
+    token.type = ol.format.WKT.TokenType.RIGHT_PAREN;
+  } else if (this.isNumeric_(c) || c == '-') {
+    token.type = ol.format.WKT.TokenType.NUMBER;
+    token.value = this.readNumber_();
+  } else if (this.isAlpha_(c)) {
+    token.type = ol.format.WKT.TokenType.TEXT;
+    token.value = this.readText_();
+  } else if (this.isWhiteSpace_(c)) {
+    return this.nextToken();
+  } else if (c === '') {
+    token.type = ol.format.WKT.TokenType.EOF;
+  } else {
+    throw new Error('Unexpected character: ' + c);
+  }
+
+  return token;
+};
+
+
+/**
+ * @return {number} Numeric token value.
+ * @private
+ */
+ol.format.WKT.Lexer.prototype.readNumber_ = function() {
+  var c, index = this.index_;
+  var decimal = false;
+  var scientificNotation = false;
+  do {
+    if (c == '.') {
+      decimal = true;
+    } else if (c == 'e' || c == 'E') {
+      scientificNotation = true;
+    }
+    c = this.nextChar_();
+  } while (
+      this.isNumeric_(c, decimal) ||
+      // if we haven't detected a scientific number before, 'e' or 'E'
+      // hint that we should continue to read
+      !scientificNotation && (c == 'e' || c == 'E') ||
+      // once we know that we have a scientific number, both '-' and '+'
+      // are allowed
+      scientificNotation && (c == '-' || c == '+')
+  );
+  return parseFloat(this.wkt.substring(index, this.index_--));
+};
+
+
+/**
+ * @return {string} String token value.
+ * @private
+ */
+ol.format.WKT.Lexer.prototype.readText_ = function() {
+  var c, index = this.index_;
+  do {
+    c = this.nextChar_();
+  } while (this.isAlpha_(c));
+  return this.wkt.substring(index, this.index_--).toUpperCase();
+};
+
+
+/**
+ * Class to parse the tokens from the WKT string.
+ * @param {ol.format.WKT.Lexer} lexer The lexer.
+ * @constructor
+ * @protected
+ */
+ol.format.WKT.Parser = function(lexer) {
+
+  /**
+   * @type {ol.format.WKT.Lexer}
+   * @private
+   */
+  this.lexer_ = lexer;
+
+  /**
+   * @type {ol.WKTToken}
+   * @private
+   */
+  this.token_;
+
+  /**
+   * @type {number}
+   * @private
+   */
+  this.dimension_ = 2;
+};
+
+
+/**
+ * Fetch the next token form the lexer and replace the active token.
+ * @private
+ */
+ol.format.WKT.Parser.prototype.consume_ = function() {
+  this.token_ = this.lexer_.nextToken();
+};
+
+
+/**
+ * If the given type matches the current token, consume it.
+ * @param {ol.format.WKT.TokenType} type Token type.
+ * @return {boolean} Whether the token matches the given type.
+ */
+ol.format.WKT.Parser.prototype.match = function(type) {
+  var isMatch = this.token_.type == type;
+  if (isMatch) {
+    this.consume_();
+  }
+  return isMatch;
+};
+
+
+/**
+ * Try to parse the tokens provided by the lexer.
+ * @return {ol.geom.Geometry|ol.geom.GeometryCollection} The geometry.
+ */
+ol.format.WKT.Parser.prototype.parse = function() {
+  this.consume_();
+  var geometry = this.parseGeometry_();
+  goog.asserts.assert(this.token_.type == ol.format.WKT.TokenType.EOF,
+      'token type should be end of file');
+  return geometry;
+};
+
+
+/**
+ * @return {!(ol.geom.Geometry|ol.geom.GeometryCollection)} The geometry.
+ * @private
+ */
+ol.format.WKT.Parser.prototype.parseGeometry_ = function() {
+  var token = this.token_;
+  if (this.match(ol.format.WKT.TokenType.TEXT)) {
+    var geomType = token.value;
+    if (geomType == ol.geom.GeometryType.GEOMETRY_COLLECTION.toUpperCase()) {
+      var geometries = this.parseGeometryCollectionText_();
+      return new ol.geom.GeometryCollection(geometries);
+    } else {
+      var parser = ol.format.WKT.Parser.GeometryParser_[geomType];
+      var ctor = ol.format.WKT.Parser.GeometryConstructor_[geomType];
+      if (!parser || !ctor) {
+        throw new Error('Invalid geometry type: ' + geomType);
+      }
+      var coordinates = parser.call(this);
+      return new ctor(coordinates);
+    }
+  }
+  throw new Error(this.formatErrorMessage_());
+};
+
+
+/**
+ * @return {!Array.<ol.geom.Geometry>} A collection of geometries.
+ * @private
+ */
+ol.format.WKT.Parser.prototype.parseGeometryCollectionText_ = function() {
+  if (this.match(ol.format.WKT.TokenType.LEFT_PAREN)) {
+    var geometries = [];
+    do {
+      geometries.push(this.parseGeometry_());
+    } while (this.match(ol.format.WKT.TokenType.COMMA));
+    if (this.match(ol.format.WKT.TokenType.RIGHT_PAREN)) {
+      return geometries;
+    }
+  } else if (this.isEmptyGeometry_()) {
+    return [];
+  }
+  throw new Error(this.formatErrorMessage_());
+};
+
+
+/**
+ * @return {Array.<number>} All values in a point.
+ * @private
+ */
+ol.format.WKT.Parser.prototype.parsePointText_ = function() {
+  if (this.match(ol.format.WKT.TokenType.LEFT_PAREN)) {
+    var coordinates = this.parsePoint_();
+    if (this.match(ol.format.WKT.TokenType.RIGHT_PAREN)) {
+      return coordinates;
+    }
+  } else if (this.isEmptyGeometry_()) {
+    return null;
+  }
+  throw new Error(this.formatErrorMessage_());
+};
+
+
+/**
+ * @return {!Array.<!Array.<number>>} All points in a linestring.
+ * @private
+ */
+ol.format.WKT.Parser.prototype.parseLineStringText_ = function() {
+  if (this.match(ol.format.WKT.TokenType.LEFT_PAREN)) {
+    var coordinates = this.parsePointList_();
+    if (this.match(ol.format.WKT.TokenType.RIGHT_PAREN)) {
+      return coordinates;
+    }
+  } else if (this.isEmptyGeometry_()) {
+    return [];
+  }
+  throw new Error(this.formatErrorMessage_());
+};
+
+
+/**
+ * @return {!Array.<!Array.<number>>} All points in a polygon.
+ * @private
+ */
+ol.format.WKT.Parser.prototype.parsePolygonText_ = function() {
+  if (this.match(ol.format.WKT.TokenType.LEFT_PAREN)) {
+    var coordinates = this.parseLineStringTextList_();
+    if (this.match(ol.format.WKT.TokenType.RIGHT_PAREN)) {
+      return coordinates;
+    }
+  } else if (this.isEmptyGeometry_()) {
+    return [];
+  }
+  throw new Error(this.formatErrorMessage_());
+};
+
+
+/**
+ * @return {!Array.<!Array.<number>>} All points in a multipoint.
+ * @private
+ */
+ol.format.WKT.Parser.prototype.parseMultiPointText_ = function() {
+  if (this.match(ol.format.WKT.TokenType.LEFT_PAREN)) {
+    var coordinates;
+    if (this.token_.type == ol.format.WKT.TokenType.LEFT_PAREN) {
+      coordinates = this.parsePointTextList_();
+    } else {
+      coordinates = this.parsePointList_();
+    }
+    if (this.match(ol.format.WKT.TokenType.RIGHT_PAREN)) {
+      return coordinates;
+    }
+  } else if (this.isEmptyGeometry_()) {
+    return [];
+  }
+  throw new Error(this.formatErrorMessage_());
+};
+
+
+/**
+ * @return {!Array.<!Array.<number>>} All linestring points
+ *                                        in a multilinestring.
+ * @private
+ */
+ol.format.WKT.Parser.prototype.parseMultiLineStringText_ = function() {
+  if (this.match(ol.format.WKT.TokenType.LEFT_PAREN)) {
+    var coordinates = this.parseLineStringTextList_();
+    if (this.match(ol.format.WKT.TokenType.RIGHT_PAREN)) {
+      return coordinates;
+    }
+  } else if (this.isEmptyGeometry_()) {
+    return [];
+  }
+  throw new Error(this.formatErrorMessage_());
+};
+
+
+/**
+ * @return {!Array.<!Array.<number>>} All polygon points in a multipolygon.
+ * @private
+ */
+ol.format.WKT.Parser.prototype.parseMultiPolygonText_ = function() {
+  if (this.match(ol.format.WKT.TokenType.LEFT_PAREN)) {
+    var coordinates = this.parsePolygonTextList_();
+    if (this.match(ol.format.WKT.TokenType.RIGHT_PAREN)) {
+      return coordinates;
+    }
+  } else if (this.isEmptyGeometry_()) {
+    return [];
+  }
+  throw new Error(this.formatErrorMessage_());
+};
+
+
+/**
+ * @return {!Array.<number>} A point.
+ * @private
+ */
+ol.format.WKT.Parser.prototype.parsePoint_ = function() {
+  var coordinates = [];
+  for (var i = 0; i < this.dimension_; ++i) {
+    var token = this.token_;
+    if (this.match(ol.format.WKT.TokenType.NUMBER)) {
+      coordinates.push(token.value);
+    } else {
+      break;
+    }
+  }
+  if (coordinates.length == this.dimension_) {
+    return coordinates;
+  }
+  throw new Error(this.formatErrorMessage_());
+};
+
+
+/**
+ * @return {!Array.<!Array.<number>>} An array of points.
+ * @private
+ */
+ol.format.WKT.Parser.prototype.parsePointList_ = function() {
+  var coordinates = [this.parsePoint_()];
+  while (this.match(ol.format.WKT.TokenType.COMMA)) {
+    coordinates.push(this.parsePoint_());
+  }
+  return coordinates;
+};
+
+
+/**
+ * @return {!Array.<!Array.<number>>} An array of points.
+ * @private
+ */
+ol.format.WKT.Parser.prototype.parsePointTextList_ = function() {
+  var coordinates = [this.parsePointText_()];
+  while (this.match(ol.format.WKT.TokenType.COMMA)) {
+    coordinates.push(this.parsePointText_());
+  }
+  return coordinates;
+};
+
+
+/**
+ * @return {!Array.<!Array.<number>>} An array of points.
+ * @private
+ */
+ol.format.WKT.Parser.prototype.parseLineStringTextList_ = function() {
+  var coordinates = [this.parseLineStringText_()];
+  while (this.match(ol.format.WKT.TokenType.COMMA)) {
+    coordinates.push(this.parseLineStringText_());
+  }
+  return coordinates;
+};
+
+
+/**
+ * @return {!Array.<!Array.<number>>} An array of points.
+ * @private
+ */
+ol.format.WKT.Parser.prototype.parsePolygonTextList_ = function() {
+  var coordinates = [this.parsePolygonText_()];
+  while (this.match(ol.format.WKT.TokenType.COMMA)) {
+    coordinates.push(this.parsePolygonText_());
+  }
+  return coordinates;
+};
+
+
+/**
+ * @return {boolean} Whether the token implies an empty geometry.
+ * @private
+ */
+ol.format.WKT.Parser.prototype.isEmptyGeometry_ = function() {
+  var isEmpty = this.token_.type == ol.format.WKT.TokenType.TEXT &&
+      this.token_.value == ol.format.WKT.EMPTY;
+  if (isEmpty) {
+    this.consume_();
+  }
+  return isEmpty;
+};
+
+
+/**
+ * Create an error message for an unexpected token error.
+ * @return {string} Error message.
+ * @private
+ */
+ol.format.WKT.Parser.prototype.formatErrorMessage_ = function() {
+  return 'Unexpected `' + this.token_.value + '` at position ' +
+      this.token_.position + ' in `' + this.lexer_.wkt + '`';
+};
+
+
+/**
+ * @enum {function (new:ol.geom.Geometry, Array, ol.geom.GeometryLayout)}
+ * @private
+ */
+ol.format.WKT.Parser.GeometryConstructor_ = {
+  'POINT': ol.geom.Point,
+  'LINESTRING': ol.geom.LineString,
+  'POLYGON': ol.geom.Polygon,
+  'MULTIPOINT': ol.geom.MultiPoint,
+  'MULTILINESTRING': ol.geom.MultiLineString,
+  'MULTIPOLYGON': ol.geom.MultiPolygon
+};
+
+
+/**
+ * @enum {(function(): Array)}
+ * @private
+ */
+ol.format.WKT.Parser.GeometryParser_ = {
+  'POINT': ol.format.WKT.Parser.prototype.parsePointText_,
+  'LINESTRING': ol.format.WKT.Parser.prototype.parseLineStringText_,
+  'POLYGON': ol.format.WKT.Parser.prototype.parsePolygonText_,
+  'MULTIPOINT': ol.format.WKT.Parser.prototype.parseMultiPointText_,
+  'MULTILINESTRING': ol.format.WKT.Parser.prototype.parseMultiLineStringText_,
+  'MULTIPOLYGON': ol.format.WKT.Parser.prototype.parseMultiPolygonText_
+};
+
+goog.provide('ol.format.WMSCapabilities');
+
+goog.require('goog.asserts');
+goog.require('ol');
+goog.require('ol.format.XLink');
+goog.require('ol.format.XML');
+goog.require('ol.format.XSD');
+goog.require('ol.xml');
+
+
+/**
+ * @classdesc
+ * Format for reading WMS capabilities data
+ *
+ * @constructor
+ * @extends {ol.format.XML}
+ * @api
+ */
+ol.format.WMSCapabilities = function() {
+
+  ol.format.XML.call(this);
+
+  /**
+   * @type {string|undefined}
+   */
+  this.version = undefined;
+};
+ol.inherits(ol.format.WMSCapabilities, ol.format.XML);
+
+
+/**
+ * Read a WMS capabilities document.
+ *
+ * @function
+ * @param {Document|Node|string} source The XML source.
+ * @return {Object} An object representing the WMS capabilities.
+ * @api
+ */
+ol.format.WMSCapabilities.prototype.read;
+
+
+/**
+ * @param {Document} doc Document.
+ * @return {Object} WMS Capability object.
+ */
+ol.format.WMSCapabilities.prototype.readFromDocument = function(doc) {
+  goog.asserts.assert(doc.nodeType == Node.DOCUMENT_NODE,
+      'doc.nodeType should be DOCUMENT');
+  for (var n = doc.firstChild; n; n = n.nextSibling) {
+    if (n.nodeType == Node.ELEMENT_NODE) {
+      return this.readFromNode(n);
+    }
+  }
+  return null;
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @return {Object} WMS Capability object.
+ */
+ol.format.WMSCapabilities.prototype.readFromNode = function(node) {
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'node.nodeType should be ELEMENT');
+  goog.asserts.assert(node.localName == 'WMS_Capabilities' ||
+      node.localName == 'WMT_MS_Capabilities',
+      'localName should be WMS_Capabilities or WMT_MS_Capabilities');
+  this.version = node.getAttribute('version').trim();
+  goog.asserts.assertString(this.version, 'this.version should be a string');
+  var wmsCapabilityObject = ol.xml.pushParseAndPop({
+    'version': this.version
+  }, ol.format.WMSCapabilities.PARSERS_, node, []);
+  return wmsCapabilityObject ? wmsCapabilityObject : null;
+};
+
+
+/**
+ * @private
+ * @param {Node} node Node.
+ * @param {Array.<*>} objectStack Object stack.
+ * @return {Object|undefined} Attribution object.
+ */
+ol.format.WMSCapabilities.readAttribution_ = function(node, objectStack) {
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'node.nodeType should be ELEMENT');
+  goog.asserts.assert(node.localName == 'Attribution',
+      'localName should be Attribution');
+  return ol.xml.pushParseAndPop(
+      {}, ol.format.WMSCapabilities.ATTRIBUTION_PARSERS_, node, objectStack);
+};
+
+
+/**
+ * @private
+ * @param {Node} node Node.
+ * @param {Array.<*>} objectStack Object stack.
+ * @return {Object} Bounding box object.
+ */
+ol.format.WMSCapabilities.readBoundingBox_ = function(node, objectStack) {
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'node.nodeType should be ELEMENT');
+  goog.asserts.assert(node.localName == 'BoundingBox',
+      'localName should be BoundingBox');
+
+  var extent = [
+    ol.format.XSD.readDecimalString(node.getAttribute('minx')),
+    ol.format.XSD.readDecimalString(node.getAttribute('miny')),
+    ol.format.XSD.readDecimalString(node.getAttribute('maxx')),
+    ol.format.XSD.readDecimalString(node.getAttribute('maxy'))
+  ];
+
+  var resolutions = [
+    ol.format.XSD.readDecimalString(node.getAttribute('resx')),
+    ol.format.XSD.readDecimalString(node.getAttribute('resy'))
+  ];
+
+  return {
+    'crs': node.getAttribute('CRS'),
+    'extent': extent,
+    'res': resolutions
+  };
+};
+
+
+/**
+ * @private
+ * @param {Node} node Node.
+ * @param {Array.<*>} objectStack Object stack.
+ * @return {ol.Extent|undefined} Bounding box object.
+ */
+ol.format.WMSCapabilities.readEXGeographicBoundingBox_ = function(node, objectStack) {
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'node.nodeType should be ELEMENT');
+  goog.asserts.assert(node.localName == 'EX_GeographicBoundingBox',
+      'localName should be EX_GeographicBoundingBox');
+  var geographicBoundingBox = ol.xml.pushParseAndPop(
+      {},
+      ol.format.WMSCapabilities.EX_GEOGRAPHIC_BOUNDING_BOX_PARSERS_,
+      node, objectStack);
+  if (!geographicBoundingBox) {
+    return undefined;
+  }
+  var westBoundLongitude = /** @type {number|undefined} */
+      (geographicBoundingBox['westBoundLongitude']);
+  var southBoundLatitude = /** @type {number|undefined} */
+      (geographicBoundingBox['southBoundLatitude']);
+  var eastBoundLongitude = /** @type {number|undefined} */
+      (geographicBoundingBox['eastBoundLongitude']);
+  var northBoundLatitude = /** @type {number|undefined} */
+      (geographicBoundingBox['northBoundLatitude']);
+  if (westBoundLongitude === undefined || southBoundLatitude === undefined ||
+      eastBoundLongitude === undefined || northBoundLatitude === undefined) {
+    return undefined;
+  }
+  return [
+    westBoundLongitude, southBoundLatitude,
+    eastBoundLongitude, northBoundLatitude
+  ];
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {Array.<*>} objectStack Object stack.
+ * @private
+ * @return {Object|undefined} Capability object.
+ */
+ol.format.WMSCapabilities.readCapability_ = function(node, objectStack) {
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'node.nodeType should be ELEMENT');
+  goog.asserts.assert(node.localName == 'Capability',
+      'localName should be Capability');
+  return ol.xml.pushParseAndPop(
+      {}, ol.format.WMSCapabilities.CAPABILITY_PARSERS_, node, objectStack);
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {Array.<*>} objectStack Object stack.
+ * @private
+ * @return {Object|undefined} Service object.
+ */
+ol.format.WMSCapabilities.readService_ = function(node, objectStack) {
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'node.nodeType should be ELEMENT');
+  goog.asserts.assert(node.localName == 'Service',
+      'localName should be Service');
+  return ol.xml.pushParseAndPop(
+      {}, ol.format.WMSCapabilities.SERVICE_PARSERS_, node, objectStack);
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {Array.<*>} objectStack Object stack.
+ * @private
+ * @return {Object|undefined} Contact information object.
+ */
+ol.format.WMSCapabilities.readContactInformation_ = function(node, objectStack) {
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'node.nodeType shpuld be ELEMENT');
+  goog.asserts.assert(node.localName == 'ContactInformation',
+      'localName should be ContactInformation');
+  return ol.xml.pushParseAndPop(
+      {}, ol.format.WMSCapabilities.CONTACT_INFORMATION_PARSERS_,
+      node, objectStack);
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {Array.<*>} objectStack Object stack.
+ * @private
+ * @return {Object|undefined} Contact person object.
+ */
+ol.format.WMSCapabilities.readContactPersonPrimary_ = function(node, objectStack) {
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'node.nodeType should be ELEMENT');
+  goog.asserts.assert(node.localName == 'ContactPersonPrimary',
+      'localName should be ContactPersonPrimary');
+  return ol.xml.pushParseAndPop(
+      {}, ol.format.WMSCapabilities.CONTACT_PERSON_PARSERS_,
+      node, objectStack);
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {Array.<*>} objectStack Object stack.
+ * @private
+ * @return {Object|undefined} Contact address object.
+ */
+ol.format.WMSCapabilities.readContactAddress_ = function(node, objectStack) {
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'node.nodeType should be ELEMENT');
+  goog.asserts.assert(node.localName == 'ContactAddress',
+      'localName should be ContactAddress');
+  return ol.xml.pushParseAndPop(
+      {}, ol.format.WMSCapabilities.CONTACT_ADDRESS_PARSERS_,
+      node, objectStack);
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {Array.<*>} objectStack Object stack.
+ * @private
+ * @return {Array.<string>|undefined} Format array.
+ */
+ol.format.WMSCapabilities.readException_ = function(node, objectStack) {
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'node.nodeType should be ELEMENT');
+  goog.asserts.assert(node.localName == 'Exception',
+      'localName should be Exception');
+  return ol.xml.pushParseAndPop(
+      [], ol.format.WMSCapabilities.EXCEPTION_PARSERS_, node, objectStack);
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @param {Array.<*>} objectStack Object stack.
+ * @private
+ * @return {Object|undefined} Layer object.
+ */
+ol.format.WMSCapabilities.readCapabilityLayer_ = function(node, objectStack) {
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'node.nodeType should be ELEMENT');
+  goog.asserts.assert(node.localName == 'Layer', 'localName should be Layer');
+  return ol.xml.pushParseAndPop(
+      {}, ol.format.WMSCapabilities.LAYER_PARSERS_, node, objectStack);
+};
+
+
+/**
+ * @private
+ * @param {Node} node Node.
+ * @param {Array.<*>} objectStack Object stack.
+ * @return {Object|undefined} Layer object.
+ */
+ol.format.WMSCapabilities.readLayer_ = function(node, objectStack) {
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'node.nodeType should be ELEMENT');
+  goog.asserts.assert(node.localName == 'Layer', 'localName should be Layer');
+  var parentLayerObject = /**  @type {Object.<string,*>} */
+      (objectStack[objectStack.length - 1]);
+
+  var layerObject = ol.xml.pushParseAndPop(
+      {}, ol.format.WMSCapabilities.LAYER_PARSERS_, node, objectStack);
+
+  if (!layerObject) {
+    return undefined;
+  }
+  var queryable =
+      ol.format.XSD.readBooleanString(node.getAttribute('queryable'));
+  if (queryable === undefined) {
+    queryable = parentLayerObject['queryable'];
+  }
+  layerObject['queryable'] = queryable !== undefined ? queryable : false;
+
+  var cascaded = ol.format.XSD.readNonNegativeIntegerString(
+      node.getAttribute('cascaded'));
+  if (cascaded === undefined) {
+    cascaded = parentLayerObject['cascaded'];
+  }
+  layerObject['cascaded'] = cascaded;
+
+  var opaque = ol.format.XSD.readBooleanString(node.getAttribute('opaque'));
+  if (opaque === undefined) {
+    opaque = parentLayerObject['opaque'];
+  }
+  layerObject['opaque'] = opaque !== undefined ? opaque : false;
+
+  var noSubsets =
+      ol.format.XSD.readBooleanString(node.getAttribute('noSubsets'));
+  if (noSubsets === undefined) {
+    noSubsets = parentLayerObject['noSubsets'];
+  }
+  layerObject['noSubsets'] = noSubsets !== undefined ? noSubsets : false;
+
+  var fixedWidth =
+      ol.format.XSD.readDecimalString(node.getAttribute('fixedWidth'));
+  if (!fixedWidth) {
+    fixedWidth = parentLayerObject['fixedWidth'];
+  }
+  layerObject['fixedWidth'] = fixedWidth;
+
+  var fixedHeight =
+      ol.format.XSD.readDecimalString(node.getAttribute('fixedHeight'));
+  if (!fixedHeight) {
+    fixedHeight = parentLayerObject['fixedHeight'];
+  }
+  layerObject['fixedHeight'] = fixedHeight;
+
+  // See 7.2.4.8
+  var addKeys = ['Style', 'CRS', 'AuthorityURL'];
+  addKeys.forEach(function(key) {
+    if (key in parentLayerObject) {
+      var childValue = 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.
+ */
+ol.format.WMSCapabilities.readDimension_ = function(node, objectStack) {
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'node.nodeType should be ELEMENT');
+  goog.asserts.assert(node.localName == 'Dimension',
+      'localName should be Dimension');
+  var dimensionObject = {
+    'name': node.getAttribute('name'),
+    'units': node.getAttribute('units'),
+    'unitSymbol': node.getAttribute('unitSymbol'),
+    'default': node.getAttribute('default'),
+    'multipleValues': ol.format.XSD.readBooleanString(
+        node.getAttribute('multipleValues')),
+    'nearestValue': ol.format.XSD.readBooleanString(
+        node.getAttribute('nearestValue')),
+    'current': ol.format.XSD.readBooleanString(node.getAttribute('current')),
+    'values': ol.format.XSD.readString(node)
+  };
+  return dimensionObject;
+};
+
+
+/**
+ * @private
+ * @param {Node} node Node.
+ * @param {Array.<*>} objectStack Object stack.
+ * @return {Object|undefined} Online resource object.
+ */
+ol.format.WMSCapabilities.readFormatOnlineresource_ = function(node, objectStack) {
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'node.nodeType should be ELEMENT');
+  return ol.xml.pushParseAndPop(
+      {}, ol.format.WMSCapabilities.FORMAT_ONLINERESOURCE_PARSERS_,
+      node, objectStack);
+};
+
+
+/**
+ * @private
+ * @param {Node} node Node.
+ * @param {Array.<*>} objectStack Object stack.
+ * @return {Object|undefined} Request object.
+ */
+ol.format.WMSCapabilities.readRequest_ = function(node, objectStack) {
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'node.nodeType should be ELEMENT');
+  goog.asserts.assert(node.localName == 'Request',
+      'localName should be Request');
+  return ol.xml.pushParseAndPop(
+      {}, ol.format.WMSCapabilities.REQUEST_PARSERS_, node, objectStack);
+};
+
+
+/**
+ * @private
+ * @param {Node} node Node.
+ * @param {Array.<*>} objectStack Object stack.
+ * @return {Object|undefined} DCP type object.
+ */
+ol.format.WMSCapabilities.readDCPType_ = function(node, objectStack) {
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'node.nodeType should be ELEMENT');
+  goog.asserts.assert(node.localName == 'DCPType',
+      'localName should be DCPType');
+  return ol.xml.pushParseAndPop(
+      {}, ol.format.WMSCapabilities.DCPTYPE_PARSERS_, node, objectStack);
+};
+
+
+/**
+ * @private
+ * @param {Node} node Node.
+ * @param {Array.<*>} objectStack Object stack.
+ * @return {Object|undefined} HTTP object.
+ */
+ol.format.WMSCapabilities.readHTTP_ = function(node, objectStack) {
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'node.nodeType should be ELEMENT');
+  goog.asserts.assert(node.localName == 'HTTP', 'localName should be HTTP');
+  return ol.xml.pushParseAndPop(
+      {}, ol.format.WMSCapabilities.HTTP_PARSERS_, node, objectStack);
+};
+
+
+/**
+ * @private
+ * @param {Node} node Node.
+ * @param {Array.<*>} objectStack Object stack.
+ * @return {Object|undefined} Operation type object.
+ */
+ol.format.WMSCapabilities.readOperationType_ = function(node, objectStack) {
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'node.nodeType should be ELEMENT');
+  return ol.xml.pushParseAndPop(
+      {}, ol.format.WMSCapabilities.OPERATIONTYPE_PARSERS_, node, objectStack);
+};
+
+
+/**
+ * @private
+ * @param {Node} node Node.
+ * @param {Array.<*>} objectStack Object stack.
+ * @return {Object|undefined} Online resource object.
+ */
+ol.format.WMSCapabilities.readSizedFormatOnlineresource_ = function(node, objectStack) {
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'node.nodeType should be ELEMENT');
+  var formatOnlineresource =
+      ol.format.WMSCapabilities.readFormatOnlineresource_(node, objectStack);
+  if (formatOnlineresource) {
+    var size = [
+      ol.format.XSD.readNonNegativeIntegerString(node.getAttribute('width')),
+      ol.format.XSD.readNonNegativeIntegerString(node.getAttribute('height'))
+    ];
+    formatOnlineresource['size'] = size;
+    return formatOnlineresource;
+  }
+  return undefined;
+};
+
+
+/**
+ * @private
+ * @param {Node} node Node.
+ * @param {Array.<*>} objectStack Object stack.
+ * @return {Object|undefined} Authority URL object.
+ */
+ol.format.WMSCapabilities.readAuthorityURL_ = function(node, objectStack) {
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'node.nodeType should be ELEMENT');
+  goog.asserts.assert(node.localName == 'AuthorityURL',
+      'localName should be AuthorityURL');
+  var authorityObject =
+      ol.format.WMSCapabilities.readFormatOnlineresource_(node, objectStack);
+  if (authorityObject) {
+    authorityObject['name'] = node.getAttribute('name');
+    return authorityObject;
+  }
+  return undefined;
+};
+
+
+/**
+ * @private
+ * @param {Node} node Node.
+ * @param {Array.<*>} objectStack Object stack.
+ * @return {Object|undefined} Metadata URL object.
+ */
+ol.format.WMSCapabilities.readMetadataURL_ = function(node, objectStack) {
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'node.nodeType should be ELEMENT');
+  goog.asserts.assert(node.localName == 'MetadataURL',
+      'localName should be MetadataURL');
+  var metadataObject =
+      ol.format.WMSCapabilities.readFormatOnlineresource_(node, objectStack);
+  if (metadataObject) {
+    metadataObject['type'] = node.getAttribute('type');
+    return metadataObject;
+  }
+  return undefined;
+};
+
+
+/**
+ * @private
+ * @param {Node} node Node.
+ * @param {Array.<*>} objectStack Object stack.
+ * @return {Object|undefined} Style object.
+ */
+ol.format.WMSCapabilities.readStyle_ = function(node, objectStack) {
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'node.nodeType should be ELEMENT');
+  goog.asserts.assert(node.localName == 'Style', 'localName should be Style');
+  return ol.xml.pushParseAndPop(
+      {}, ol.format.WMSCapabilities.STYLE_PARSERS_, node, objectStack);
+};
+
+
+/**
+ * @private
+ * @param {Node} node Node.
+ * @param {Array.<*>} objectStack Object stack.
+ * @return {Array.<string>|undefined} Keyword list.
+ */
+ol.format.WMSCapabilities.readKeywordList_ = function(node, objectStack) {
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'node.nodeType should be ELEMENT');
+  goog.asserts.assert(node.localName == 'KeywordList',
+      'localName should be KeywordList');
+  return ol.xml.pushParseAndPop(
+      [], ol.format.WMSCapabilities.KEYWORDLIST_PARSERS_, node, objectStack);
+};
+
+
+/**
+ * @const
+ * @private
+ * @type {Array.<string>}
+ */
+ol.format.WMSCapabilities.NAMESPACE_URIS_ = [
+  null,
+  'http://www.opengis.net/wms'
+];
+
+
+/**
+ * @const
+ * @type {Object.<string, Object.<string, ol.XmlParser>>}
+ * @private
+ */
+ol.format.WMSCapabilities.PARSERS_ = ol.xml.makeStructureNS(
+    ol.format.WMSCapabilities.NAMESPACE_URIS_, {
+      'Service': ol.xml.makeObjectPropertySetter(
+          ol.format.WMSCapabilities.readService_),
+      'Capability': ol.xml.makeObjectPropertySetter(
+          ol.format.WMSCapabilities.readCapability_)
+    });
+
+
+/**
+ * @const
+ * @type {Object.<string, Object.<string, ol.XmlParser>>}
+ * @private
+ */
+ol.format.WMSCapabilities.CAPABILITY_PARSERS_ = ol.xml.makeStructureNS(
+    ol.format.WMSCapabilities.NAMESPACE_URIS_, {
+      'Request': ol.xml.makeObjectPropertySetter(
+          ol.format.WMSCapabilities.readRequest_),
+      'Exception': ol.xml.makeObjectPropertySetter(
+          ol.format.WMSCapabilities.readException_),
+      'Layer': ol.xml.makeObjectPropertySetter(
+          ol.format.WMSCapabilities.readCapabilityLayer_)
+    });
+
+
+/**
+ * @const
+ * @type {Object.<string, Object.<string, ol.XmlParser>>}
+ * @private
+ */
+ol.format.WMSCapabilities.SERVICE_PARSERS_ = ol.xml.makeStructureNS(
+    ol.format.WMSCapabilities.NAMESPACE_URIS_, {
+      'Name': ol.xml.makeObjectPropertySetter(ol.format.XSD.readString),
+      'Title': ol.xml.makeObjectPropertySetter(ol.format.XSD.readString),
+      'Abstract': ol.xml.makeObjectPropertySetter(ol.format.XSD.readString),
+      'KeywordList': ol.xml.makeObjectPropertySetter(
+          ol.format.WMSCapabilities.readKeywordList_),
+      'OnlineResource': ol.xml.makeObjectPropertySetter(
+          ol.format.XLink.readHref),
+      'ContactInformation': ol.xml.makeObjectPropertySetter(
+          ol.format.WMSCapabilities.readContactInformation_),
+      'Fees': ol.xml.makeObjectPropertySetter(ol.format.XSD.readString),
+      'AccessConstraints': ol.xml.makeObjectPropertySetter(
+          ol.format.XSD.readString),
+      'LayerLimit': ol.xml.makeObjectPropertySetter(
+          ol.format.XSD.readNonNegativeInteger),
+      'MaxWidth': ol.xml.makeObjectPropertySetter(
+          ol.format.XSD.readNonNegativeInteger),
+      'MaxHeight': ol.xml.makeObjectPropertySetter(
+          ol.format.XSD.readNonNegativeInteger)
+    });
+
+
+/**
+ * @const
+ * @type {Object.<string, Object.<string, ol.XmlParser>>}
+ * @private
+ */
+ol.format.WMSCapabilities.CONTACT_INFORMATION_PARSERS_ = ol.xml.makeStructureNS(
+    ol.format.WMSCapabilities.NAMESPACE_URIS_, {
+      'ContactPersonPrimary': ol.xml.makeObjectPropertySetter(
+          ol.format.WMSCapabilities.readContactPersonPrimary_),
+      'ContactPosition': ol.xml.makeObjectPropertySetter(
+          ol.format.XSD.readString),
+      'ContactAddress': ol.xml.makeObjectPropertySetter(
+          ol.format.WMSCapabilities.readContactAddress_),
+      'ContactVoiceTelephone': ol.xml.makeObjectPropertySetter(
+          ol.format.XSD.readString),
+      'ContactFacsimileTelephone': ol.xml.makeObjectPropertySetter(
+          ol.format.XSD.readString),
+      'ContactElectronicMailAddress': ol.xml.makeObjectPropertySetter(
+          ol.format.XSD.readString)
+    });
+
+
+/**
+ * @const
+ * @type {Object.<string, Object.<string, ol.XmlParser>>}
+ * @private
+ */
+ol.format.WMSCapabilities.CONTACT_PERSON_PARSERS_ = ol.xml.makeStructureNS(
+    ol.format.WMSCapabilities.NAMESPACE_URIS_, {
+      'ContactPerson': ol.xml.makeObjectPropertySetter(
+          ol.format.XSD.readString),
+      'ContactOrganization': ol.xml.makeObjectPropertySetter(
+          ol.format.XSD.readString)
+    });
+
+
+/**
+ * @const
+ * @type {Object.<string, Object.<string, ol.XmlParser>>}
+ * @private
+ */
+ol.format.WMSCapabilities.CONTACT_ADDRESS_PARSERS_ = ol.xml.makeStructureNS(
+    ol.format.WMSCapabilities.NAMESPACE_URIS_, {
+      'AddressType': ol.xml.makeObjectPropertySetter(ol.format.XSD.readString),
+      'Address': ol.xml.makeObjectPropertySetter(ol.format.XSD.readString),
+      'City': ol.xml.makeObjectPropertySetter(ol.format.XSD.readString),
+      'StateOrProvince': ol.xml.makeObjectPropertySetter(
+          ol.format.XSD.readString),
+      'PostCode': ol.xml.makeObjectPropertySetter(ol.format.XSD.readString),
+      'Country': ol.xml.makeObjectPropertySetter(ol.format.XSD.readString)
+    });
+
+
+/**
+ * @const
+ * @type {Object.<string, Object.<string, ol.XmlParser>>}
+ * @private
+ */
+ol.format.WMSCapabilities.EXCEPTION_PARSERS_ = ol.xml.makeStructureNS(
+    ol.format.WMSCapabilities.NAMESPACE_URIS_, {
+      'Format': ol.xml.makeArrayPusher(ol.format.XSD.readString)
+    });
+
+
+/**
+ * @const
+ * @type {Object.<string, Object.<string, ol.XmlParser>>}
+ * @private
+ */
+ol.format.WMSCapabilities.LAYER_PARSERS_ = ol.xml.makeStructureNS(
+    ol.format.WMSCapabilities.NAMESPACE_URIS_, {
+      'Name': ol.xml.makeObjectPropertySetter(ol.format.XSD.readString),
+      'Title': ol.xml.makeObjectPropertySetter(ol.format.XSD.readString),
+      'Abstract': ol.xml.makeObjectPropertySetter(ol.format.XSD.readString),
+      'KeywordList': ol.xml.makeObjectPropertySetter(
+          ol.format.WMSCapabilities.readKeywordList_),
+      'CRS': ol.xml.makeObjectPropertyPusher(ol.format.XSD.readString),
+      'EX_GeographicBoundingBox': ol.xml.makeObjectPropertySetter(
+          ol.format.WMSCapabilities.readEXGeographicBoundingBox_),
+      'BoundingBox': ol.xml.makeObjectPropertyPusher(
+          ol.format.WMSCapabilities.readBoundingBox_),
+      'Dimension': ol.xml.makeObjectPropertyPusher(
+          ol.format.WMSCapabilities.readDimension_),
+      'Attribution': ol.xml.makeObjectPropertySetter(
+          ol.format.WMSCapabilities.readAttribution_),
+      'AuthorityURL': ol.xml.makeObjectPropertyPusher(
+          ol.format.WMSCapabilities.readAuthorityURL_),
+      'Identifier': ol.xml.makeObjectPropertyPusher(ol.format.XSD.readString),
+      'MetadataURL': ol.xml.makeObjectPropertyPusher(
+          ol.format.WMSCapabilities.readMetadataURL_),
+      'DataURL': ol.xml.makeObjectPropertyPusher(
+          ol.format.WMSCapabilities.readFormatOnlineresource_),
+      'FeatureListURL': ol.xml.makeObjectPropertyPusher(
+          ol.format.WMSCapabilities.readFormatOnlineresource_),
+      'Style': ol.xml.makeObjectPropertyPusher(
+          ol.format.WMSCapabilities.readStyle_),
+      'MinScaleDenominator': ol.xml.makeObjectPropertySetter(
+          ol.format.XSD.readDecimal),
+      'MaxScaleDenominator': ol.xml.makeObjectPropertySetter(
+          ol.format.XSD.readDecimal),
+      'Layer': ol.xml.makeObjectPropertyPusher(
+          ol.format.WMSCapabilities.readLayer_)
+    });
+
+
+/**
+ * @const
+ * @type {Object.<string, Object.<string, ol.XmlParser>>}
+ * @private
+ */
+ol.format.WMSCapabilities.ATTRIBUTION_PARSERS_ = ol.xml.makeStructureNS(
+    ol.format.WMSCapabilities.NAMESPACE_URIS_, {
+      'Title': ol.xml.makeObjectPropertySetter(ol.format.XSD.readString),
+      'OnlineResource': ol.xml.makeObjectPropertySetter(
+          ol.format.XLink.readHref),
+      'LogoURL': ol.xml.makeObjectPropertySetter(
+          ol.format.WMSCapabilities.readSizedFormatOnlineresource_)
+    });
+
+
+/**
+ * @const
+ * @type {Object.<string, Object.<string, ol.XmlParser>>}
+ * @private
+ */
+ol.format.WMSCapabilities.EX_GEOGRAPHIC_BOUNDING_BOX_PARSERS_ =
+    ol.xml.makeStructureNS(ol.format.WMSCapabilities.NAMESPACE_URIS_, {
+      'westBoundLongitude': ol.xml.makeObjectPropertySetter(
+          ol.format.XSD.readDecimal),
+      'eastBoundLongitude': ol.xml.makeObjectPropertySetter(
+          ol.format.XSD.readDecimal),
+      'southBoundLatitude': ol.xml.makeObjectPropertySetter(
+          ol.format.XSD.readDecimal),
+      'northBoundLatitude': ol.xml.makeObjectPropertySetter(
+          ol.format.XSD.readDecimal)
+    });
+
+
+/**
+ * @const
+ * @type {Object.<string, Object.<string, ol.XmlParser>>}
+ * @private
+ */
+ol.format.WMSCapabilities.REQUEST_PARSERS_ = ol.xml.makeStructureNS(
+    ol.format.WMSCapabilities.NAMESPACE_URIS_, {
+      'GetCapabilities': ol.xml.makeObjectPropertySetter(
+          ol.format.WMSCapabilities.readOperationType_),
+      'GetMap': ol.xml.makeObjectPropertySetter(
+          ol.format.WMSCapabilities.readOperationType_),
+      'GetFeatureInfo': ol.xml.makeObjectPropertySetter(
+          ol.format.WMSCapabilities.readOperationType_)
+    });
+
+
+/**
+ * @const
+ * @type {Object.<string, Object.<string, ol.XmlParser>>}
+ * @private
+ */
+ol.format.WMSCapabilities.OPERATIONTYPE_PARSERS_ = ol.xml.makeStructureNS(
+    ol.format.WMSCapabilities.NAMESPACE_URIS_, {
+      'Format': ol.xml.makeObjectPropertyPusher(ol.format.XSD.readString),
+      'DCPType': ol.xml.makeObjectPropertyPusher(
+          ol.format.WMSCapabilities.readDCPType_)
+    });
+
+
+/**
+ * @const
+ * @type {Object.<string, Object.<string, ol.XmlParser>>}
+ * @private
+ */
+ol.format.WMSCapabilities.DCPTYPE_PARSERS_ = ol.xml.makeStructureNS(
+    ol.format.WMSCapabilities.NAMESPACE_URIS_, {
+      'HTTP': ol.xml.makeObjectPropertySetter(
+          ol.format.WMSCapabilities.readHTTP_)
+    });
+
+
+/**
+ * @const
+ * @type {Object.<string, Object.<string, ol.XmlParser>>}
+ * @private
+ */
+ol.format.WMSCapabilities.HTTP_PARSERS_ = ol.xml.makeStructureNS(
+    ol.format.WMSCapabilities.NAMESPACE_URIS_, {
+      'Get': ol.xml.makeObjectPropertySetter(
+          ol.format.WMSCapabilities.readFormatOnlineresource_),
+      'Post': ol.xml.makeObjectPropertySetter(
+          ol.format.WMSCapabilities.readFormatOnlineresource_)
+    });
+
+
+/**
+ * @const
+ * @type {Object.<string, Object.<string, ol.XmlParser>>}
+ * @private
+ */
+ol.format.WMSCapabilities.STYLE_PARSERS_ = ol.xml.makeStructureNS(
+    ol.format.WMSCapabilities.NAMESPACE_URIS_, {
+      'Name': ol.xml.makeObjectPropertySetter(ol.format.XSD.readString),
+      'Title': ol.xml.makeObjectPropertySetter(ol.format.XSD.readString),
+      'Abstract': ol.xml.makeObjectPropertySetter(ol.format.XSD.readString),
+      'LegendURL': ol.xml.makeObjectPropertyPusher(
+          ol.format.WMSCapabilities.readSizedFormatOnlineresource_),
+      'StyleSheetURL': ol.xml.makeObjectPropertySetter(
+          ol.format.WMSCapabilities.readFormatOnlineresource_),
+      'StyleURL': ol.xml.makeObjectPropertySetter(
+          ol.format.WMSCapabilities.readFormatOnlineresource_)
+    });
+
+
+/**
+ * @const
+ * @type {Object.<string, Object.<string, ol.XmlParser>>}
+ * @private
+ */
+ol.format.WMSCapabilities.FORMAT_ONLINERESOURCE_PARSERS_ =
+    ol.xml.makeStructureNS(ol.format.WMSCapabilities.NAMESPACE_URIS_, {
+      'Format': ol.xml.makeObjectPropertySetter(ol.format.XSD.readString),
+      'OnlineResource': ol.xml.makeObjectPropertySetter(
+          ol.format.XLink.readHref)
+    });
+
+
+/**
+ * @const
+ * @type {Object.<string, Object.<string, ol.XmlParser>>}
+ * @private
+ */
+ol.format.WMSCapabilities.KEYWORDLIST_PARSERS_ = ol.xml.makeStructureNS(
+    ol.format.WMSCapabilities.NAMESPACE_URIS_, {
+      'Keyword': ol.xml.makeArrayPusher(ol.format.XSD.readString)
+    });
+
+goog.provide('ol.format.WMSGetFeatureInfo');
+
+goog.require('goog.asserts');
+goog.require('ol.array');
+goog.require('ol.format.GML2');
+goog.require('ol.format.XMLFeature');
+goog.require('ol.object');
+goog.require('ol.xml');
+
+
+/**
+ * @classdesc
+ * Format for reading WMSGetFeatureInfo format. It uses
+ * {@link ol.format.GML2} to read features.
+ *
+ * @constructor
+ * @extends {ol.format.XMLFeature}
+ * @param {olx.format.WMSGetFeatureInfoOptions=} opt_options Options.
+ * @api
+ */
+ol.format.WMSGetFeatureInfo = function(opt_options) {
+
+  var options = opt_options ? opt_options : {};
+
+  /**
+   * @private
+   * @type {string}
+   */
+  this.featureNS_ = 'http://mapserver.gis.umn.edu/mapserver';
+
+
+  /**
+   * @private
+   * @type {ol.format.GML2}
+   */
+  this.gmlFormat_ = new ol.format.GML2();
+
+
+  /**
+   * @private
+   * @type {Array.<string>}
+   */
+  this.layers_ = options.layers ? options.layers : null;
+
+  ol.format.XMLFeature.call(this);
+};
+ol.inherits(ol.format.WMSGetFeatureInfo, ol.format.XMLFeature);
+
+
+/**
+ * @const
+ * @type {string}
+ * @private
+ */
+ol.format.WMSGetFeatureInfo.featureIdentifier_ = '_feature';
+
+
+/**
+ * @const
+ * @type {string}
+ * @private
+ */
+ol.format.WMSGetFeatureInfo.layerIdentifier_ = '_layer';
+
+
+/**
+ * @param {Node} node Node.
+ * @param {Array.<*>} objectStack Object stack.
+ * @return {Array.<ol.Feature>} Features.
+ * @private
+ */
+ol.format.WMSGetFeatureInfo.prototype.readFeatures_ = function(node, objectStack) {
+
+  node.setAttribute('namespaceURI', this.featureNS_);
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'node.nodeType should be ELEMENT');
+  var localName = node.localName;
+  /** @type {Array.<ol.Feature>} */
+  var features = [];
+  if (node.childNodes.length === 0) {
+    return features;
+  }
+  if (localName == 'msGMLOutput') {
+    for (var i = 0, ii = node.childNodes.length; i < ii; i++) {
+      var layer = node.childNodes[i];
+      if (layer.nodeType !== Node.ELEMENT_NODE) {
+        continue;
+      }
+      var context = objectStack[0];
+      goog.asserts.assert(goog.isObject(context),
+          'context should be an Object');
+
+      goog.asserts.assert(layer.localName.indexOf(
+          ol.format.WMSGetFeatureInfo.layerIdentifier_) >= 0,
+          'localName of layer node should match layerIdentifier');
+
+      var toRemove = ol.format.WMSGetFeatureInfo.layerIdentifier_;
+      var layerName = layer.localName.replace(toRemove, '');
+
+      if (this.layers_ && !ol.array.includes(this.layers_, layerName)) {
+        continue;
+      }
+
+      var featureType = layerName +
+          ol.format.WMSGetFeatureInfo.featureIdentifier_;
+
+      context['featureType'] = featureType;
+      context['featureNS'] = this.featureNS_;
+
+      var parsers = {};
+      parsers[featureType] = ol.xml.makeArrayPusher(
+          this.gmlFormat_.readFeatureElement, this.gmlFormat_);
+      var parsersNS = ol.xml.makeStructureNS(
+          [context['featureNS'], null], parsers);
+      layer.setAttribute('namespaceURI', this.featureNS_);
+      var layerFeatures = ol.xml.pushParseAndPop(
+          [], parsersNS, layer, objectStack, this.gmlFormat_);
+      if (layerFeatures) {
+        ol.array.extend(features, layerFeatures);
+      }
+    }
+  }
+  if (localName == 'FeatureCollection') {
+    var gmlFeatures = ol.xml.pushParseAndPop([],
+        this.gmlFormat_.FEATURE_COLLECTION_PARSERS, node,
+        [{}], this.gmlFormat_);
+    if (gmlFeatures) {
+      features = gmlFeatures;
+    }
+  }
+  return features;
+};
+
+
+/**
+ * Read all features from a WMSGetFeatureInfo response.
+ *
+ * @function
+ * @param {Document|Node|Object|string} source Source.
+ * @param {olx.format.ReadOptions=} opt_options Options.
+ * @return {Array.<ol.Feature>} Features.
+ * @api stable
+ */
+ol.format.WMSGetFeatureInfo.prototype.readFeatures;
+
+
+/**
+ * @inheritDoc
+ */
+ol.format.WMSGetFeatureInfo.prototype.readFeaturesFromNode = function(node, opt_options) {
+  var options = {};
+  if (opt_options) {
+    ol.object.assign(options, this.getReadOptions(node, opt_options));
+  }
+  return this.readFeatures_(node, [options]);
+};
+
+goog.provide('ol.format.WMTSCapabilities');
+
+goog.require('goog.asserts');
+goog.require('ol.extent');
+goog.require('ol.format.OWS');
+goog.require('ol.format.XLink');
+goog.require('ol.format.XML');
+goog.require('ol.format.XSD');
+goog.require('ol.xml');
+
+
+/**
+ * @classdesc
+ * Format for reading WMTS capabilities data.
+ *
+ * @constructor
+ * @extends {ol.format.XML}
+ * @api
+ */
+ol.format.WMTSCapabilities = function() {
+  ol.format.XML.call(this);
+
+  /**
+   * @type {ol.format.OWS}
+   * @private
+   */
+  this.owsParser_ = new ol.format.OWS();
+};
+ol.inherits(ol.format.WMTSCapabilities, ol.format.XML);
+
+
+/**
+ * Read a WMTS capabilities document.
+ *
+ * @function
+ * @param {Document|Node|string} source The XML source.
+ * @return {Object} An object representing the WMTS capabilities.
+ * @api
+ */
+ol.format.WMTSCapabilities.prototype.read;
+
+
+/**
+ * @param {Document} doc Document.
+ * @return {Object} WMTS Capability object.
+ */
+ol.format.WMTSCapabilities.prototype.readFromDocument = function(doc) {
+  goog.asserts.assert(doc.nodeType == Node.DOCUMENT_NODE,
+      'doc.nodeType should be DOCUMENT');
+  for (var n = doc.firstChild; n; n = n.nextSibling) {
+    if (n.nodeType == Node.ELEMENT_NODE) {
+      return this.readFromNode(n);
+    }
+  }
+  return null;
+};
+
+
+/**
+ * @param {Node} node Node.
+ * @return {Object} WMTS Capability object.
+ */
+ol.format.WMTSCapabilities.prototype.readFromNode = function(node) {
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'node.nodeType should be ELEMENT');
+  goog.asserts.assert(node.localName == 'Capabilities',
+      'localName should be Capabilities');
+  var version = node.getAttribute('version').trim();
+  goog.asserts.assertString(version, 'version should be a string');
+  var WMTSCapabilityObject = this.owsParser_.readFromNode(node);
+  if (!WMTSCapabilityObject) {
+    return null;
+  }
+  WMTSCapabilityObject['version'] = version;
+  WMTSCapabilityObject = ol.xml.pushParseAndPop(WMTSCapabilityObject,
+      ol.format.WMTSCapabilities.PARSERS_, node, []);
+  return WMTSCapabilityObject ? WMTSCapabilityObject : null;
+};
+
+
+/**
+ * @private
+ * @param {Node} node Node.
+ * @param {Array.<*>} objectStack Object stack.
+ * @return {Object|undefined} Attribution object.
+ */
+ol.format.WMTSCapabilities.readContents_ = function(node, objectStack) {
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'node.nodeType should be ELEMENT');
+  goog.asserts.assert(node.localName == 'Contents',
+      'localName should be Contents');
+
+  return ol.xml.pushParseAndPop({},
+      ol.format.WMTSCapabilities.CONTENTS_PARSERS_, node, objectStack);
+};
+
+
+/**
+ * @private
+ * @param {Node} node Node.
+ * @param {Array.<*>} objectStack Object stack.
+ * @return {Object|undefined} Layers object.
+ */
+ol.format.WMTSCapabilities.readLayer_ = function(node, objectStack) {
+  goog.asserts.assert(node.nodeType == Node.ELEMENT_NODE,
+      'node.nodeType should be ELEMENT');
+  goog.asserts.assert(node.localName == 'Layer', 'localName should be Layer');
+  return ol.xml.pushParseAndPop({},
+      ol.format.WMTSCapabilities.LAYER_PARSERS_, node, objectStack);
+};
+
+
+/**
+ * @private
+ * @param {Node} node Node.
+ * @param {Array.<*>} objectStack Object stack.
+ * @return {Object|undefined} Tile Matrix Set object.
+ */
+ol.format.WMTSCapabilities.readTileMatrixSet_ = function(node, objectStack) {
+  return ol.xml.pushParseAndPop({},
+      ol.format.WMTSCapabilities.TMS_PARSERS_, node, objectStack);
+};
+
+
+/**
+ * @private
+ * @param {Node} node Node.
+ * @param {Array.<*>} objectStack Object stack.
+ * @return {Object|undefined} Style object.
+ */
+ol.format.WMTSCapabilities.readStyle_ = function(node, objectStack) {
+  var style = ol.xml.pushParseAndPop({},
+      ol.format.WMTSCapabilities.STYLE_PARSERS_, node, objectStack);
+  if (!style) {
+    return undefined;
+  }
+  var isDefault = node.getAttribute('isDefault') === 'true';
+  style['isDefault'] = isDefault;
+  return style;
+
+};
+
+
+/**
+ * @private
+ * @param {Node} node Node.
+ * @param {Array.<*>} objectStack Object stack.
+ * @return {Object|undefined} Tile Matrix Set Link object.
+ */
+ol.format.WMTSCapabilities.readTileMatrixSetLink_ = function(node,
+    objectStack) {
+  return ol.xml.pushParseAndPop({},
+      ol.format.WMTSCapabilities.TMS_LINKS_PARSERS_, node, objectStack);
+};
+
+
+/**
+ * @private
+ * @param {Node} node Node.
+ * @param {Array.<*>} objectStack Object stack.
+ * @return {Object|undefined} Dimension object.
+ */
+ol.format.WMTSCapabilities.readDimensions_ = function(node, objectStack) {
+  return ol.xml.pushParseAndPop({},
+      ol.format.WMTSCapabilities.DIMENSION_PARSERS_, node, objectStack);
+};
+
+
+/**
+ * @private
+ * @param {Node} node Node.
+ * @param {Array.<*>} objectStack Object stack.
+ * @return {Object|undefined} Resource URL object.
+ */
+ol.format.WMTSCapabilities.readResourceUrl_ = function(node, objectStack) {
+  var format = node.getAttribute('format');
+  var template = node.getAttribute('template');
+  var resourceType = node.getAttribute('resourceType');
+  var resource = {};
+  if (format) {
+    resource['format'] = format;
+  }
+  if (template) {
+    resource['template'] = template;
+  }
+  if (resourceType) {
+    resource['resourceType'] = resourceType;
+  }
+  return resource;
+};
+
+
+/**
+ * @private
+ * @param {Node} node Node.
+ * @param {Array.<*>} objectStack Object stack.
+ * @return {Object|undefined} WGS84 BBox object.
+ */
+ol.format.WMTSCapabilities.readWgs84BoundingBox_ = function(node, objectStack) {
+  var coordinates = ol.xml.pushParseAndPop([],
+      ol.format.WMTSCapabilities.WGS84_BBOX_READERS_, node, objectStack);
+  if (coordinates.length != 2) {
+    return undefined;
+  }
+  return ol.extent.boundingExtent(coordinates);
+};
+
+
+/**
+ * @private
+ * @param {Node} node Node.
+ * @param {Array.<*>} objectStack Object stack.
+ * @return {Object|undefined} Legend object.
+ */
+ol.format.WMTSCapabilities.readLegendUrl_ = function(node, objectStack) {
+  var legend = {};
+  legend['format'] = node.getAttribute('format');
+  legend['href'] = ol.format.XLink.readHref(node);
+  return legend;
+};
+
+
+/**
+ * @private
+ * @param {Node} node Node.
+ * @param {Array.<*>} objectStack Object stack.
+ * @return {Object|undefined} Coordinates object.
+ */
+ol.format.WMTSCapabilities.readCoordinates_ = function(node, objectStack) {
+  var coordinates = ol.format.XSD.readString(node).split(' ');
+  if (!coordinates || coordinates.length != 2) {
+    return undefined;
+  }
+  var x = +coordinates[0];
+  var y = +coordinates[1];
+  if (isNaN(x) || isNaN(y)) {
+    return undefined;
+  }
+  return [x, y];
+};
+
+
+/**
+ * @private
+ * @param {Node} node Node.
+ * @param {Array.<*>} objectStack Object stack.
+ * @return {Object|undefined} TileMatrix object.
+ */
+ol.format.WMTSCapabilities.readTileMatrix_ = function(node, objectStack) {
+  return ol.xml.pushParseAndPop({},
+      ol.format.WMTSCapabilities.TM_PARSERS_, node, objectStack);
+};
+
+
+/**
+ * @const
+ * @private
+ * @type {Array.<string>}
+ */
+ol.format.WMTSCapabilities.NAMESPACE_URIS_ = [
+  null,
+  'http://www.opengis.net/wmts/1.0'
+];
+
+
+/**
+ * @const
+ * @private
+ * @type {Array.<string>}
+ */
+ol.format.WMTSCapabilities.OWS_NAMESPACE_URIS_ = [
+  null,
+  'http://www.opengis.net/ows/1.1'
+];
+
+
+/**
+ * @const
+ * @type {Object.<string, Object.<string, ol.XmlParser>>}
+ * @private
+ */
+ol.format.WMTSCapabilities.PARSERS_ = ol.xml.makeStructureNS(
+    ol.format.WMTSCapabilities.NAMESPACE_URIS_, {
+      'Contents': ol.xml.makeObjectPropertySetter(
+          ol.format.WMTSCapabilities.readContents_)
+    });
+
+
+/**
+ * @const
+ * @type {Object.<string, Object.<string, ol.XmlParser>>}
+ * @private
+ */
+ol.format.WMTSCapabilities.CONTENTS_PARSERS_ = ol.xml.makeStructureNS(
+    ol.format.WMTSCapabilities.NAMESPACE_URIS_, {
+      'Layer': ol.xml.makeObjectPropertyPusher(
+          ol.format.WMTSCapabilities.readLayer_),
+      'TileMatrixSet': ol.xml.makeObjectPropertyPusher(
+          ol.format.WMTSCapabilities.readTileMatrixSet_)
+    });
+
+
+/**
+ * @const
+ * @type {Object.<string, Object.<string, ol.XmlParser>>}
+ * @private
+ */
+ol.format.WMTSCapabilities.LAYER_PARSERS_ = ol.xml.makeStructureNS(
+    ol.format.WMTSCapabilities.NAMESPACE_URIS_, {
+      'Style': ol.xml.makeObjectPropertyPusher(
+          ol.format.WMTSCapabilities.readStyle_),
+      'Format': ol.xml.makeObjectPropertyPusher(
+          ol.format.XSD.readString),
+      'TileMatrixSetLink': ol.xml.makeObjectPropertyPusher(
+          ol.format.WMTSCapabilities.readTileMatrixSetLink_),
+      'Dimension': ol.xml.makeObjectPropertyPusher(
+          ol.format.WMTSCapabilities.readDimensions_),
+      'ResourceURL': ol.xml.makeObjectPropertyPusher(
+          ol.format.WMTSCapabilities.readResourceUrl_)
+    }, ol.xml.makeStructureNS(ol.format.WMTSCapabilities.OWS_NAMESPACE_URIS_, {
+      'Title': ol.xml.makeObjectPropertySetter(
+          ol.format.XSD.readString),
+      'Abstract': ol.xml.makeObjectPropertySetter(
+          ol.format.XSD.readString),
+      'WGS84BoundingBox': ol.xml.makeObjectPropertySetter(
+          ol.format.WMTSCapabilities.readWgs84BoundingBox_),
+      'Identifier': ol.xml.makeObjectPropertySetter(
+          ol.format.XSD.readString)
+    }));
+
+
+/**
+ * @const
+ * @type {Object.<string, Object.<string, ol.XmlParser>>}
+ * @private
+ */
+ol.format.WMTSCapabilities.STYLE_PARSERS_ = ol.xml.makeStructureNS(
+    ol.format.WMTSCapabilities.NAMESPACE_URIS_, {
+      'LegendURL': ol.xml.makeObjectPropertyPusher(
+          ol.format.WMTSCapabilities.readLegendUrl_)
+    }, ol.xml.makeStructureNS(ol.format.WMTSCapabilities.OWS_NAMESPACE_URIS_, {
+      'Title': ol.xml.makeObjectPropertySetter(
+          ol.format.XSD.readString),
+      'Identifier': ol.xml.makeObjectPropertySetter(
+          ol.format.XSD.readString)
+    }));
+
+
+/**
+ * @const
+ * @type {Object.<string, Object.<string, ol.XmlParser>>}
+ * @private
+ */
+ol.format.WMTSCapabilities.TMS_LINKS_PARSERS_ = ol.xml.makeStructureNS(
+    ol.format.WMTSCapabilities.NAMESPACE_URIS_, {
+      'TileMatrixSet': ol.xml.makeObjectPropertySetter(
+          ol.format.XSD.readString)
+    });
+
+
+/**
+ * @const
+ * @type {Object.<string, Object.<string, ol.XmlParser>>}
+ * @private
+ */
+ol.format.WMTSCapabilities.DIMENSION_PARSERS_ = ol.xml.makeStructureNS(
+    ol.format.WMTSCapabilities.NAMESPACE_URIS_, {
+      'Default': ol.xml.makeObjectPropertySetter(
+          ol.format.XSD.readString),
+      'Value': ol.xml.makeObjectPropertyPusher(
+          ol.format.XSD.readString)
+    }, ol.xml.makeStructureNS(ol.format.WMTSCapabilities.OWS_NAMESPACE_URIS_, {
+      'Identifier': ol.xml.makeObjectPropertySetter(
+          ol.format.XSD.readString)
+    }));
+
+
+/**
+ * @const
+ * @type {Object.<string, Object.<string, ol.XmlParser>>}
+ * @private
+ */
+ol.format.WMTSCapabilities.WGS84_BBOX_READERS_ = ol.xml.makeStructureNS(
+    ol.format.WMTSCapabilities.OWS_NAMESPACE_URIS_, {
+      'LowerCorner': ol.xml.makeArrayPusher(
+          ol.format.WMTSCapabilities.readCoordinates_),
+      'UpperCorner': ol.xml.makeArrayPusher(
+          ol.format.WMTSCapabilities.readCoordinates_)
+    });
+
+
+/**
+ * @const
+ * @type {Object.<string, Object.<string, ol.XmlParser>>}
+ * @private
+ */
+ol.format.WMTSCapabilities.TMS_PARSERS_ = ol.xml.makeStructureNS(
+    ol.format.WMTSCapabilities.NAMESPACE_URIS_, {
+      'WellKnownScaleSet': ol.xml.makeObjectPropertySetter(
+          ol.format.XSD.readString),
+      'TileMatrix': ol.xml.makeObjectPropertyPusher(
+          ol.format.WMTSCapabilities.readTileMatrix_)
+    }, ol.xml.makeStructureNS(ol.format.WMTSCapabilities.OWS_NAMESPACE_URIS_, {
+      'SupportedCRS': ol.xml.makeObjectPropertySetter(
+          ol.format.XSD.readString),
+      'Identifier': ol.xml.makeObjectPropertySetter(
+          ol.format.XSD.readString)
+    }));
+
+
+/**
+ * @const
+ * @type {Object.<string, Object.<string, ol.XmlParser>>}
+ * @private
+ */
+ol.format.WMTSCapabilities.TM_PARSERS_ = ol.xml.makeStructureNS(
+    ol.format.WMTSCapabilities.NAMESPACE_URIS_, {
+      'TopLeftCorner': ol.xml.makeObjectPropertySetter(
+          ol.format.WMTSCapabilities.readCoordinates_),
+      'ScaleDenominator': ol.xml.makeObjectPropertySetter(
+          ol.format.XSD.readDecimal),
+      'TileWidth': ol.xml.makeObjectPropertySetter(
+          ol.format.XSD.readNonNegativeInteger),
+      'TileHeight': ol.xml.makeObjectPropertySetter(
+          ol.format.XSD.readNonNegativeInteger),
+      'MatrixWidth': ol.xml.makeObjectPropertySetter(
+          ol.format.XSD.readNonNegativeInteger),
+      'MatrixHeight': ol.xml.makeObjectPropertySetter(
+          ol.format.XSD.readNonNegativeInteger)
+    }, ol.xml.makeStructureNS(ol.format.WMTSCapabilities.OWS_NAMESPACE_URIS_, {
+      'Identifier': ol.xml.makeObjectPropertySetter(
+          ol.format.XSD.readString)
+    }));
+
+// FIXME handle geolocation not supported
+
+goog.provide('ol.Geolocation');
+
+goog.require('ol.events');
+goog.require('ol.events.EventType');
+goog.require('ol.Object');
+goog.require('ol.geom.Geometry');
+goog.require('ol.geom.Polygon');
+goog.require('ol.has');
+goog.require('ol.math');
+goog.require('ol.proj');
+goog.require('ol.sphere.WGS84');
+
+
+/**
+ * @enum {string}
+ */
+ol.GeolocationProperty = {
+  ACCURACY: 'accuracy',
+  ACCURACY_GEOMETRY: 'accuracyGeometry',
+  ALTITUDE: 'altitude',
+  ALTITUDE_ACCURACY: 'altitudeAccuracy',
+  HEADING: 'heading',
+  POSITION: 'position',
+  PROJECTION: 'projection',
+  SPEED: 'speed',
+  TRACKING: 'tracking',
+  TRACKING_OPTIONS: 'trackingOptions'
+};
+
+
+/**
+ * @classdesc
+ * Helper class for providing HTML5 Geolocation capabilities.
+ * The [Geolocation API](http://www.w3.org/TR/geolocation-API/)
+ * is used to locate a user's position.
+ *
+ * To get notified of position changes, register a listener for the generic
+ * `change` event on your instance of `ol.Geolocation`.
+ *
+ * Example:
+ *
+ *     var geolocation = new ol.Geolocation({
+ *       // take the projection to use from the map's view
+ *       projection: view.getProjection()
+ *     });
+ *     // listen to changes in position
+ *     geolocation.on('change', function(evt) {
+ *       window.console.log(geolocation.getPosition());
+ *     });
+ *
+ * @fires error
+ * @constructor
+ * @extends {ol.Object}
+ * @param {olx.GeolocationOptions=} opt_options Options.
+ * @api stable
+ */
+ol.Geolocation = function(opt_options) {
+
+  ol.Object.call(this);
+
+  var options = opt_options || {};
+
+  /**
+   * The unprojected (EPSG:4326) device position.
+   * @private
+   * @type {ol.Coordinate}
+   */
+  this.position_ = null;
+
+  /**
+   * @private
+   * @type {ol.TransformFunction}
+   */
+  this.transform_ = ol.proj.identityTransform;
+
+  /**
+   * @private
+   * @type {number|undefined}
+   */
+  this.watchId_ = undefined;
+
+  ol.events.listen(
+      this, ol.Object.getChangeEventType(ol.GeolocationProperty.PROJECTION),
+      this.handleProjectionChanged_, this);
+  ol.events.listen(
+      this, ol.Object.getChangeEventType(ol.GeolocationProperty.TRACKING),
+      this.handleTrackingChanged_, this);
+
+  if (options.projection !== undefined) {
+    this.setProjection(ol.proj.get(options.projection));
+  }
+  if (options.trackingOptions !== undefined) {
+    this.setTrackingOptions(options.trackingOptions);
+  }
+
+  this.setTracking(options.tracking !== undefined ? options.tracking : false);
+
+};
+ol.inherits(ol.Geolocation, ol.Object);
+
+
+/**
+ * @inheritDoc
+ */
+ol.Geolocation.prototype.disposeInternal = function() {
+  this.setTracking(false);
+  ol.Object.prototype.disposeInternal.call(this);
+};
+
+
+/**
+ * @private
+ */
+ol.Geolocation.prototype.handleProjectionChanged_ = function() {
+  var projection = this.getProjection();
+  if (projection) {
+    this.transform_ = ol.proj.getTransformFromProjections(
+        ol.proj.get('EPSG:4326'), projection);
+    if (this.position_) {
+      this.set(
+          ol.GeolocationProperty.POSITION, this.transform_(this.position_));
+    }
+  }
+};
+
+
+/**
+ * @private
+ */
+ol.Geolocation.prototype.handleTrackingChanged_ = function() {
+  if (ol.has.GEOLOCATION) {
+    var tracking = this.getTracking();
+    if (tracking && this.watchId_ === undefined) {
+      this.watchId_ = ol.global.navigator.geolocation.watchPosition(
+          this.positionChange_.bind(this),
+          this.positionError_.bind(this),
+          this.getTrackingOptions());
+    } else if (!tracking && this.watchId_ !== undefined) {
+      ol.global.navigator.geolocation.clearWatch(this.watchId_);
+      this.watchId_ = undefined;
+    }
+  }
+};
+
+
+/**
+ * @private
+ * @param {GeolocationPosition} position position event.
+ */
+ol.Geolocation.prototype.positionChange_ = function(position) {
+  var coords = position.coords;
+  this.set(ol.GeolocationProperty.ACCURACY, coords.accuracy);
+  this.set(ol.GeolocationProperty.ALTITUDE,
+      coords.altitude === null ? undefined : coords.altitude);
+  this.set(ol.GeolocationProperty.ALTITUDE_ACCURACY,
+      coords.altitudeAccuracy === null ?
+      undefined : coords.altitudeAccuracy);
+  this.set(ol.GeolocationProperty.HEADING, coords.heading === null ?
+      undefined : ol.math.toRadians(coords.heading));
+  if (!this.position_) {
+    this.position_ = [coords.longitude, coords.latitude];
+  } else {
+    this.position_[0] = coords.longitude;
+    this.position_[1] = coords.latitude;
+  }
+  var projectedPosition = this.transform_(this.position_);
+  this.set(ol.GeolocationProperty.POSITION, projectedPosition);
+  this.set(ol.GeolocationProperty.SPEED,
+      coords.speed === null ? undefined : coords.speed);
+  var geometry = ol.geom.Polygon.circular(
+      ol.sphere.WGS84, this.position_, coords.accuracy);
+  geometry.applyTransform(this.transform_);
+  this.set(ol.GeolocationProperty.ACCURACY_GEOMETRY, geometry);
+  this.changed();
+};
+
+/**
+ * Triggered when the Geolocation returns an error.
+ * @event error
+ * @api
+ */
+
+/**
+ * @private
+ * @param {GeolocationPositionError} error error object.
+ */
+ol.Geolocation.prototype.positionError_ = function(error) {
+  error.type = ol.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 stable
+ */
+ol.Geolocation.prototype.getAccuracy = function() {
+  return /** @type {number|undefined} */ (
+      this.get(ol.GeolocationProperty.ACCURACY));
+};
+
+
+/**
+ * Get a geometry of the position accuracy.
+ * @return {?ol.geom.Geometry} A geometry of the position accuracy.
+ * @observable
+ * @api stable
+ */
+ol.Geolocation.prototype.getAccuracyGeometry = function() {
+  return /** @type {?ol.geom.Geometry} */ (
+      this.get(ol.GeolocationProperty.ACCURACY_GEOMETRY) || null);
+};
+
+
+/**
+ * Get the altitude associated with the position.
+ * @return {number|undefined} The altitude of the position in meters above mean
+ *     sea level.
+ * @observable
+ * @api stable
+ */
+ol.Geolocation.prototype.getAltitude = function() {
+  return /** @type {number|undefined} */ (
+      this.get(ol.GeolocationProperty.ALTITUDE));
+};
+
+
+/**
+ * Get the altitude accuracy of the position.
+ * @return {number|undefined} The accuracy of the altitude measurement in
+ *     meters.
+ * @observable
+ * @api stable
+ */
+ol.Geolocation.prototype.getAltitudeAccuracy = function() {
+  return /** @type {number|undefined} */ (
+      this.get(ol.GeolocationProperty.ALTITUDE_ACCURACY));
+};
+
+
+/**
+ * Get the heading as radians clockwise from North.
+ * @return {number|undefined} The heading of the device in radians from north.
+ * @observable
+ * @api stable
+ */
+ol.Geolocation.prototype.getHeading = function() {
+  return /** @type {number|undefined} */ (
+      this.get(ol.GeolocationProperty.HEADING));
+};
+
+
+/**
+ * Get the position of the device.
+ * @return {ol.Coordinate|undefined} The current position of the device reported
+ *     in the current projection.
+ * @observable
+ * @api stable
+ */
+ol.Geolocation.prototype.getPosition = function() {
+  return /** @type {ol.Coordinate|undefined} */ (
+      this.get(ol.GeolocationProperty.POSITION));
+};
+
+
+/**
+ * Get the projection associated with the position.
+ * @return {ol.proj.Projection|undefined} The projection the position is
+ *     reported in.
+ * @observable
+ * @api stable
+ */
+ol.Geolocation.prototype.getProjection = function() {
+  return /** @type {ol.proj.Projection|undefined} */ (
+      this.get(ol.GeolocationProperty.PROJECTION));
+};
+
+
+/**
+ * Get the speed in meters per second.
+ * @return {number|undefined} The instantaneous speed of the device in meters
+ *     per second.
+ * @observable
+ * @api stable
+ */
+ol.Geolocation.prototype.getSpeed = function() {
+  return /** @type {number|undefined} */ (
+      this.get(ol.GeolocationProperty.SPEED));
+};
+
+
+/**
+ * Determine if the device location is being tracked.
+ * @return {boolean} The device location is being tracked.
+ * @observable
+ * @api stable
+ */
+ol.Geolocation.prototype.getTracking = function() {
+  return /** @type {boolean} */ (
+      this.get(ol.GeolocationProperty.TRACKING));
+};
+
+
+/**
+ * Get the tracking options.
+ * @see http://www.w3.org/TR/geolocation-API/#position-options
+ * @return {GeolocationPositionOptions|undefined} PositionOptions as defined by
+ *     the [HTML5 Geolocation spec
+ *     ](http://www.w3.org/TR/geolocation-API/#position_options_interface).
+ * @observable
+ * @api stable
+ */
+ol.Geolocation.prototype.getTrackingOptions = function() {
+  return /** @type {GeolocationPositionOptions|undefined} */ (
+      this.get(ol.GeolocationProperty.TRACKING_OPTIONS));
+};
+
+
+/**
+ * Set the projection to use for transforming the coordinates.
+ * @param {ol.proj.Projection} projection The projection the position is
+ *     reported in.
+ * @observable
+ * @api stable
+ */
+ol.Geolocation.prototype.setProjection = function(projection) {
+  this.set(ol.GeolocationProperty.PROJECTION, projection);
+};
+
+
+/**
+ * Enable or disable tracking.
+ * @param {boolean} tracking Enable tracking.
+ * @observable
+ * @api stable
+ */
+ol.Geolocation.prototype.setTracking = function(tracking) {
+  this.set(ol.GeolocationProperty.TRACKING, tracking);
+};
+
+
+/**
+ * Set the tracking options.
+ * @see http://www.w3.org/TR/geolocation-API/#position-options
+ * @param {GeolocationPositionOptions} options PositionOptions as defined by the
+ *     [HTML5 Geolocation spec
+ *     ](http://www.w3.org/TR/geolocation-API/#position_options_interface).
+ * @observable
+ * @api stable
+ */
+ol.Geolocation.prototype.setTrackingOptions = function(options) {
+  this.set(ol.GeolocationProperty.TRACKING_OPTIONS, options);
+};
+
+goog.provide('ol.geom.Circle');
+
+goog.require('goog.asserts');
+goog.require('ol.extent');
+goog.require('ol.geom.GeometryLayout');
+goog.require('ol.geom.GeometryType');
+goog.require('ol.geom.SimpleGeometry');
+goog.require('ol.geom.flat.deflate');
+
+
+/**
+ * @classdesc
+ * Circle geometry.
+ *
+ * @constructor
+ * @extends {ol.geom.SimpleGeometry}
+ * @param {ol.Coordinate} center Center.
+ * @param {number=} opt_radius Radius.
+ * @param {ol.geom.GeometryLayout=} opt_layout Layout.
+ * @api
+ */
+ol.geom.Circle = function(center, opt_radius, opt_layout) {
+  ol.geom.SimpleGeometry.call(this);
+  var radius = opt_radius ? opt_radius : 0;
+  this.setCenterAndRadius(center, radius, opt_layout);
+};
+ol.inherits(ol.geom.Circle, ol.geom.SimpleGeometry);
+
+
+/**
+ * Make a complete copy of the geometry.
+ * @return {!ol.geom.Circle} Clone.
+ * @api
+ */
+ol.geom.Circle.prototype.clone = function() {
+  var circle = new ol.geom.Circle(null);
+  circle.setFlatCoordinates(this.layout, this.flatCoordinates.slice());
+  return circle;
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.geom.Circle.prototype.closestPointXY = function(x, y, closestPoint, minSquaredDistance) {
+  var flatCoordinates = this.flatCoordinates;
+  var dx = x - flatCoordinates[0];
+  var dy = y - flatCoordinates[1];
+  var squaredDistance = dx * dx + dy * dy;
+  if (squaredDistance < minSquaredDistance) {
+    var i;
+    if (squaredDistance === 0) {
+      for (i = 0; i < this.stride; ++i) {
+        closestPoint[i] = flatCoordinates[i];
+      }
+    } else {
+      var delta = this.getRadius() / Math.sqrt(squaredDistance);
+      closestPoint[0] = flatCoordinates[0] + delta * dx;
+      closestPoint[1] = flatCoordinates[1] + delta * dy;
+      for (i = 2; i < this.stride; ++i) {
+        closestPoint[i] = flatCoordinates[i];
+      }
+    }
+    closestPoint.length = this.stride;
+    return squaredDistance;
+  } else {
+    return minSquaredDistance;
+  }
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.geom.Circle.prototype.containsXY = function(x, y) {
+  var flatCoordinates = this.flatCoordinates;
+  var dx = x - flatCoordinates[0];
+  var dy = y - flatCoordinates[1];
+  return dx * dx + dy * dy <= this.getRadiusSquared_();
+};
+
+
+/**
+ * Return the center of the circle as {@link ol.Coordinate coordinate}.
+ * @return {ol.Coordinate} Center.
+ * @api
+ */
+ol.geom.Circle.prototype.getCenter = function() {
+  return this.flatCoordinates.slice(0, this.stride);
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.geom.Circle.prototype.computeExtent = function(extent) {
+  var flatCoordinates = this.flatCoordinates;
+  var radius = flatCoordinates[this.stride] - flatCoordinates[0];
+  return ol.extent.createOrUpdate(
+      flatCoordinates[0] - radius, flatCoordinates[1] - radius,
+      flatCoordinates[0] + radius, flatCoordinates[1] + radius,
+      extent);
+};
+
+
+/**
+ * Return the radius of the circle.
+ * @return {number} Radius.
+ * @api
+ */
+ol.geom.Circle.prototype.getRadius = function() {
+  return Math.sqrt(this.getRadiusSquared_());
+};
+
+
+/**
+ * @private
+ * @return {number} Radius squared.
+ */
+ol.geom.Circle.prototype.getRadiusSquared_ = function() {
+  var dx = this.flatCoordinates[this.stride] - this.flatCoordinates[0];
+  var dy = this.flatCoordinates[this.stride + 1] - this.flatCoordinates[1];
+  return dx * dx + dy * dy;
+};
+
+
+/**
+ * @inheritDoc
+ * @api
+ */
+ol.geom.Circle.prototype.getType = function() {
+  return ol.geom.GeometryType.CIRCLE;
+};
+
+
+/**
+ * @inheritDoc
+ * @api stable
+ */
+ol.geom.Circle.prototype.intersectsExtent = function(extent) {
+  var circleExtent = this.getExtent();
+  if (ol.extent.intersects(extent, circleExtent)) {
+    var center = this.getCenter();
+
+    if (extent[0] <= center[0] && extent[2] >= center[0]) {
+      return true;
+    }
+    if (extent[1] <= center[1] && extent[3] >= center[1]) {
+      return true;
+    }
+
+    return ol.extent.forEachCorner(extent, this.containsCoordinate, this);
+  }
+  return false;
+
+};
+
+
+/**
+ * Set the center of the circle as {@link ol.Coordinate coordinate}.
+ * @param {ol.Coordinate} center Center.
+ * @api
+ */
+ol.geom.Circle.prototype.setCenter = function(center) {
+  var stride = this.stride;
+  goog.asserts.assert(center.length == stride,
+      'center array length should match stride');
+  var radius = this.flatCoordinates[stride] - this.flatCoordinates[0];
+  var flatCoordinates = center.slice();
+  flatCoordinates[stride] = flatCoordinates[0] + radius;
+  var i;
+  for (i = 1; i < stride; ++i) {
+    flatCoordinates[stride + i] = center[i];
+  }
+  this.setFlatCoordinates(this.layout, flatCoordinates);
+};
+
+
+/**
+ * Set the center (as {@link ol.Coordinate coordinate}) and the radius (as
+ * number) of the circle.
+ * @param {ol.Coordinate} center Center.
+ * @param {number} radius Radius.
+ * @param {ol.geom.GeometryLayout=} opt_layout Layout.
+ * @api
+ */
+ol.geom.Circle.prototype.setCenterAndRadius = function(center, radius, opt_layout) {
+  if (!center) {
+    this.setFlatCoordinates(ol.geom.GeometryLayout.XY, null);
+  } else {
+    this.setLayout(opt_layout, center, 0);
+    if (!this.flatCoordinates) {
+      this.flatCoordinates = [];
+    }
+    /** @type {Array.<number>} */
+    var flatCoordinates = this.flatCoordinates;
+    var offset = ol.geom.flat.deflate.coordinate(
+        flatCoordinates, 0, center, this.stride);
+    flatCoordinates[offset++] = flatCoordinates[0] + radius;
+    var i, ii;
+    for (i = 1, ii = this.stride; i < ii; ++i) {
+      flatCoordinates[offset++] = flatCoordinates[i];
+    }
+    flatCoordinates.length = offset;
+    this.changed();
+  }
+};
+
+
+/**
+ * @param {ol.geom.GeometryLayout} layout Layout.
+ * @param {Array.<number>} flatCoordinates Flat coordinates.
+ */
+ol.geom.Circle.prototype.setFlatCoordinates = function(layout, flatCoordinates) {
+  this.setFlatCoordinatesInternal(layout, flatCoordinates);
+  this.changed();
+};
+
+
+/**
+ * Set the radius of the circle. The radius is in the units of the projection.
+ * @param {number} radius Radius.
+ * @api
+ */
+ol.geom.Circle.prototype.setRadius = function(radius) {
+  goog.asserts.assert(this.flatCoordinates,
+      'truthy this.flatCoordinates expected');
+  this.flatCoordinates[this.stride] = this.flatCoordinates[0] + radius;
+  this.changed();
+};
+
+
+/**
+ * Transform each coordinate of the circle from one coordinate reference system
+ * to another. The geometry is modified in place.
+ * If you do not want the geometry modified in place, first clone() it and
+ * then use this function on the clone.
+ *
+ * Internally a circle is currently represented by two points: the center of
+ * the circle `[cx, cy]`, and the point to the right of the circle
+ * `[cx + r, cy]`. This `transform` function just transforms these two points.
+ * So the resulting geometry is also a circle, and that circle does not
+ * correspond to the shape that would be obtained by transforming every point
+ * of the original circle.
+ *
+ * @param {ol.ProjectionLike} source The current projection.  Can be a
+ *     string identifier or a {@link ol.proj.Projection} object.
+ * @param {ol.ProjectionLike} destination The desired projection.  Can be a
+ *     string identifier or a {@link ol.proj.Projection} object.
+ * @return {ol.geom.Circle} This geometry.  Note that original geometry is
+ *     modified in place.
+ * @function
+ * @api stable
+ */
+ol.geom.Circle.prototype.transform;
+
+goog.provide('ol.geom.flat.geodesic');
+
+goog.require('goog.asserts');
+goog.require('ol.math');
+goog.require('ol.proj');
+
+
+/**
+ * @private
+ * @param {function(number): ol.Coordinate} interpolate Interpolate function.
+ * @param {ol.TransformFunction} transform Transform from longitude/latitude to
+ *     projected coordinates.
+ * @param {number} squaredTolerance Squared tolerance.
+ * @return {Array.<number>} Flat coordinates.
+ */
+ol.geom.flat.geodesic.line_ = function(interpolate, transform, squaredTolerance) {
+  // FIXME reduce garbage generation
+  // FIXME optimize stack operations
+
+  /** @type {Array.<number>} */
+  var flatCoordinates = [];
+
+  var geoA = interpolate(0);
+  var geoB = interpolate(1);
+
+  var a = transform(geoA);
+  var b = transform(geoB);
+
+  /** @type {Array.<ol.Coordinate>} */
+  var geoStack = [geoB, geoA];
+  /** @type {Array.<ol.Coordinate>} */
+  var stack = [b, a];
+  /** @type {Array.<number>} */
+  var fractionStack = [1, 0];
+
+  /** @type {Object.<string, boolean>} */
+  var fractions = {};
+
+  var maxIterations = 1e5;
+  var geoM, m, fracA, fracB, fracM, key;
+
+  while (--maxIterations > 0 && fractionStack.length > 0) {
+    // Pop the a coordinate off the stack
+    fracA = fractionStack.pop();
+    geoA = geoStack.pop();
+    a = stack.pop();
+    // Add the a coordinate if it has not been added yet
+    key = fracA.toString();
+    if (!(key in fractions)) {
+      flatCoordinates.push(a[0], a[1]);
+      fractions[key] = true;
+    }
+    // Pop the b coordinate off the stack
+    fracB = fractionStack.pop();
+    geoB = geoStack.pop();
+    b = stack.pop();
+    // Find the m point between the a and b coordinates
+    fracM = (fracA + fracB) / 2;
+    geoM = interpolate(fracM);
+    m = transform(geoM);
+    if (ol.math.squaredSegmentDistance(m[0], m[1], a[0], a[1],
+        b[0], b[1]) < squaredTolerance) {
+      // If the m point is sufficiently close to the straight line, then we
+      // discard it.  Just use the b coordinate and move on to the next line
+      // segment.
+      flatCoordinates.push(b[0], b[1]);
+      key = fracB.toString();
+      goog.asserts.assert(!(key in fractions),
+          'fractions object should contain key : ' + key);
+      fractions[key] = true;
+    } else {
+      // Otherwise, we need to subdivide the current line segment.  Split it
+      // into two and push the two line segments onto the stack.
+      fractionStack.push(fracB, fracM, fracM, fracA);
+      stack.push(b, m, m, a);
+      geoStack.push(geoB, geoM, geoM, geoA);
+    }
+  }
+  goog.asserts.assert(maxIterations > 0,
+      'maxIterations should be more than 0');
+
+  return flatCoordinates;
+};
+
+
+/**
+* Generate a great-circle arcs between two lat/lon points.
+* @param {number} lon1 Longitude 1 in degrees.
+* @param {number} lat1 Latitude 1 in degrees.
+* @param {number} lon2 Longitude 2 in degrees.
+* @param {number} lat2 Latitude 2 in degrees.
+ * @param {ol.proj.Projection} projection Projection.
+* @param {number} squaredTolerance Squared tolerance.
+* @return {Array.<number>} Flat coordinates.
+*/
+ol.geom.flat.geodesic.greatCircleArc = function(
+    lon1, lat1, lon2, lat2, projection, squaredTolerance) {
+
+  var geoProjection = ol.proj.get('EPSG:4326');
+
+  var cosLat1 = Math.cos(ol.math.toRadians(lat1));
+  var sinLat1 = Math.sin(ol.math.toRadians(lat1));
+  var cosLat2 = Math.cos(ol.math.toRadians(lat2));
+  var sinLat2 = Math.sin(ol.math.toRadians(lat2));
+  var cosDeltaLon = Math.cos(ol.math.toRadians(lon2 - lon1));
+  var sinDeltaLon = Math.sin(ol.math.toRadians(lon2 - lon1));
+  var d = sinLat1 * sinLat2 + cosLat1 * cosLat2 * cosDeltaLon;
+
+  return ol.geom.flat.geodesic.line_(
+      /**
+       * @param {number} frac Fraction.
+       * @return {ol.Coordinate} Coordinate.
+       */
+      function(frac) {
+        if (1 <= d) {
+          return [lon2, lat2];
+        }
+        var D = frac * Math.acos(d);
+        var cosD = Math.cos(D);
+        var sinD = Math.sin(D);
+        var y = sinDeltaLon * cosLat2;
+        var x = cosLat1 * sinLat2 - sinLat1 * cosLat2 * cosDeltaLon;
+        var theta = Math.atan2(y, x);
+        var lat = Math.asin(sinLat1 * cosD + cosLat1 * sinD * Math.cos(theta));
+        var lon = ol.math.toRadians(lon1) +
+            Math.atan2(Math.sin(theta) * sinD * cosLat1,
+                       cosD - sinLat1 * Math.sin(lat));
+        return [ol.math.toDegrees(lon), ol.math.toDegrees(lat)];
+      }, ol.proj.getTransform(geoProjection, projection), squaredTolerance);
+};
+
+
+/**
+ * Generate a meridian (line at constant longitude).
+ * @param {number} lon Longitude.
+ * @param {number} lat1 Latitude 1.
+ * @param {number} lat2 Latitude 2.
+ * @param {ol.proj.Projection} projection Projection.
+ * @param {number} squaredTolerance Squared tolerance.
+ * @return {Array.<number>} Flat coordinates.
+ */
+ol.geom.flat.geodesic.meridian = function(lon, lat1, lat2, projection, squaredTolerance) {
+  var epsg4326Projection = ol.proj.get('EPSG:4326');
+  return ol.geom.flat.geodesic.line_(
+      /**
+       * @param {number} frac Fraction.
+       * @return {ol.Coordinate} Coordinate.
+       */
+      function(frac) {
+        return [lon, lat1 + ((lat2 - lat1) * frac)];
+      },
+      ol.proj.getTransform(epsg4326Projection, projection), squaredTolerance);
+};
+
+
+/**
+ * Generate a parallel (line at constant latitude).
+ * @param {number} lat Latitude.
+ * @param {number} lon1 Longitude 1.
+ * @param {number} lon2 Longitude 2.
+ * @param {ol.proj.Projection} projection Projection.
+ * @param {number} squaredTolerance Squared tolerance.
+ * @return {Array.<number>} Flat coordinates.
+ */
+ol.geom.flat.geodesic.parallel = function(lat, lon1, lon2, projection, squaredTolerance) {
+  var epsg4326Projection = ol.proj.get('EPSG:4326');
+  return ol.geom.flat.geodesic.line_(
+      /**
+       * @param {number} frac Fraction.
+       * @return {ol.Coordinate} Coordinate.
+       */
+      function(frac) {
+        return [lon1 + ((lon2 - lon1) * frac), lat];
+      },
+      ol.proj.getTransform(epsg4326Projection, projection), squaredTolerance);
+};
+
+goog.provide('ol.Graticule');
+
+goog.require('goog.asserts');
+goog.require('ol.extent');
+goog.require('ol.geom.GeometryLayout');
+goog.require('ol.geom.LineString');
+goog.require('ol.geom.flat.geodesic');
+goog.require('ol.math');
+goog.require('ol.proj');
+goog.require('ol.render.EventType');
+goog.require('ol.style.Stroke');
+
+
+/**
+ * Render a grid for a coordinate system on a map.
+ * @constructor
+ * @param {olx.GraticuleOptions=} opt_options Options.
+ * @api
+ */
+ol.Graticule = function(opt_options) {
+
+  var options = opt_options || {};
+
+  /**
+   * @type {ol.Map}
+   * @private
+   */
+  this.map_ = null;
+
+  /**
+   * @type {ol.proj.Projection}
+   * @private
+   */
+  this.projection_ = null;
+
+  /**
+   * @type {number}
+   * @private
+   */
+  this.maxLat_ = Infinity;
+
+  /**
+   * @type {number}
+   * @private
+   */
+  this.maxLon_ = Infinity;
+
+  /**
+   * @type {number}
+   * @private
+   */
+  this.minLat_ = -Infinity;
+
+  /**
+   * @type {number}
+   * @private
+   */
+  this.minLon_ = -Infinity;
+
+  /**
+   * @type {number}
+   * @private
+   */
+  this.maxLatP_ = Infinity;
+
+  /**
+   * @type {number}
+   * @private
+   */
+  this.maxLonP_ = Infinity;
+
+  /**
+   * @type {number}
+   * @private
+   */
+  this.minLatP_ = -Infinity;
+
+  /**
+   * @type {number}
+   * @private
+   */
+  this.minLonP_ = -Infinity;
+
+  /**
+   * @type {number}
+   * @private
+   */
+  this.targetSize_ = options.targetSize !== undefined ?
+      options.targetSize : 100;
+
+  /**
+   * @type {number}
+   * @private
+   */
+  this.maxLines_ = options.maxLines !== undefined ? options.maxLines : 100;
+  goog.asserts.assert(this.maxLines_ > 0,
+      'this.maxLines_ should be more than 0');
+
+  /**
+   * @type {Array.<ol.geom.LineString>}
+   * @private
+   */
+  this.meridians_ = [];
+
+  /**
+   * @type {Array.<ol.geom.LineString>}
+   * @private
+   */
+  this.parallels_ = [];
+
+  /**
+   * @type {ol.style.Stroke}
+   * @private
+   */
+  this.strokeStyle_ = options.strokeStyle !== undefined ?
+      options.strokeStyle : ol.Graticule.DEFAULT_STROKE_STYLE_;
+
+  /**
+   * @type {ol.TransformFunction|undefined}
+   * @private
+   */
+  this.fromLonLatTransform_ = undefined;
+
+  /**
+   * @type {ol.TransformFunction|undefined}
+   * @private
+   */
+  this.toLonLatTransform_ = undefined;
+
+  /**
+   * @type {ol.Coordinate}
+   * @private
+   */
+  this.projectionCenterLonLat_ = null;
+
+  this.setMap(options.map !== undefined ? options.map : null);
+};
+
+
+/**
+ * @type {ol.style.Stroke}
+ * @private
+ * @const
+ */
+ol.Graticule.DEFAULT_STROKE_STYLE_ = new ol.style.Stroke({
+  color: 'rgba(0,0,0,0.2)'
+});
+
+
+/**
+ * TODO can be configurable
+ * @type {Array.<number>}
+ * @private
+ */
+ol.Graticule.intervals_ = [90, 45, 30, 20, 10, 5, 2, 1, 0.5, 0.2, 0.1, 0.05,
+  0.01, 0.005, 0.002, 0.001];
+
+
+/**
+ * @param {number} lon Longitude.
+ * @param {number} minLat Minimal latitude.
+ * @param {number} maxLat Maximal latitude.
+ * @param {number} squaredTolerance Squared tolerance.
+ * @param {ol.Extent} extent Extent.
+ * @param {number} index Index.
+ * @return {number} Index.
+ * @private
+ */
+ol.Graticule.prototype.addMeridian_ = function(lon, minLat, maxLat, squaredTolerance, extent, index) {
+  var lineString = this.getMeridian_(lon, minLat, maxLat,
+      squaredTolerance, index);
+  if (ol.extent.intersects(lineString.getExtent(), extent)) {
+    this.meridians_[index++] = lineString;
+  }
+  return index;
+};
+
+
+/**
+ * @param {number} lat Latitude.
+ * @param {number} minLon Minimal longitude.
+ * @param {number} maxLon Maximal longitude.
+ * @param {number} squaredTolerance Squared tolerance.
+ * @param {ol.Extent} extent Extent.
+ * @param {number} index Index.
+ * @return {number} Index.
+ * @private
+ */
+ol.Graticule.prototype.addParallel_ = function(lat, minLon, maxLon, squaredTolerance, extent, index) {
+  var lineString = this.getParallel_(lat, minLon, maxLon, squaredTolerance,
+      index);
+  if (ol.extent.intersects(lineString.getExtent(), extent)) {
+    this.parallels_[index++] = lineString;
+  }
+  return index;
+};
+
+
+/**
+ * @param {ol.Extent} extent Extent.
+ * @param {ol.Coordinate} center Center.
+ * @param {number} resolution Resolution.
+ * @param {number} squaredTolerance Squared tolerance.
+ * @private
+ */
+ol.Graticule.prototype.createGraticule_ = function(extent, center, resolution, squaredTolerance) {
+
+  var interval = this.getInterval_(resolution);
+  if (interval == -1) {
+    this.meridians_.length = this.parallels_.length = 0;
+    return;
+  }
+
+  var centerLonLat = this.toLonLatTransform_(center);
+  var centerLon = centerLonLat[0];
+  var centerLat = centerLonLat[1];
+  var maxLines = this.maxLines_;
+  var cnt, idx, lat, lon;
+
+  var validExtent = [
+    Math.max(extent[0], this.minLonP_),
+    Math.max(extent[1], this.minLatP_),
+    Math.min(extent[2], this.maxLonP_),
+    Math.min(extent[3], this.maxLatP_)
+  ];
+
+  validExtent = ol.proj.transformExtent(validExtent, this.projection_,
+      'EPSG:4326');
+  var maxLat = validExtent[3];
+  var maxLon = validExtent[2];
+  var minLat = validExtent[1];
+  var minLon = validExtent[0];
+
+  // Create meridians
+
+  centerLon = Math.floor(centerLon / interval) * interval;
+  lon = ol.math.clamp(centerLon, this.minLon_, this.maxLon_);
+
+  idx = this.addMeridian_(lon, minLat, maxLat, squaredTolerance, extent, 0);
+
+  cnt = 0;
+  while (lon != this.minLon_ && cnt++ < maxLines) {
+    lon = Math.max(lon - interval, this.minLon_);
+    idx = this.addMeridian_(lon, minLat, maxLat, squaredTolerance, extent, idx);
+  }
+
+  lon = ol.math.clamp(centerLon, this.minLon_, this.maxLon_);
+
+  cnt = 0;
+  while (lon != this.maxLon_ && cnt++ < maxLines) {
+    lon = Math.min(lon + interval, this.maxLon_);
+    idx = this.addMeridian_(lon, minLat, maxLat, squaredTolerance, extent, idx);
+  }
+
+  this.meridians_.length = idx;
+
+  // Create parallels
+
+  centerLat = Math.floor(centerLat / interval) * interval;
+  lat = ol.math.clamp(centerLat, this.minLat_, this.maxLat_);
+
+  idx = this.addParallel_(lat, minLon, maxLon, squaredTolerance, extent, 0);
+
+  cnt = 0;
+  while (lat != this.minLat_ && cnt++ < maxLines) {
+    lat = Math.max(lat - interval, this.minLat_);
+    idx = this.addParallel_(lat, minLon, maxLon, squaredTolerance, extent, idx);
+  }
+
+  lat = ol.math.clamp(centerLat, this.minLat_, this.maxLat_);
+
+  cnt = 0;
+  while (lat != this.maxLat_ && cnt++ < maxLines) {
+    lat = Math.min(lat + interval, this.maxLat_);
+    idx = this.addParallel_(lat, minLon, maxLon, squaredTolerance, extent, idx);
+  }
+
+  this.parallels_.length = idx;
+
+};
+
+
+/**
+ * @param {number} resolution Resolution.
+ * @return {number} The interval in degrees.
+ * @private
+ */
+ol.Graticule.prototype.getInterval_ = function(resolution) {
+  var centerLon = this.projectionCenterLonLat_[0];
+  var centerLat = this.projectionCenterLonLat_[1];
+  var interval = -1;
+  var i, ii, delta, dist;
+  var target = Math.pow(this.targetSize_ * resolution, 2);
+  /** @type {Array.<number>} **/
+  var p1 = [];
+  /** @type {Array.<number>} **/
+  var p2 = [];
+  for (i = 0, ii = ol.Graticule.intervals_.length; i < ii; ++i) {
+    delta = ol.Graticule.intervals_[i] / 2;
+    p1[0] = centerLon - delta;
+    p1[1] = centerLat - delta;
+    p2[0] = centerLon + delta;
+    p2[1] = centerLat + delta;
+    this.fromLonLatTransform_(p1, p1);
+    this.fromLonLatTransform_(p2, p2);
+    dist = Math.pow(p2[0] - p1[0], 2) + Math.pow(p2[1] - p1[1], 2);
+    if (dist <= target) {
+      break;
+    }
+    interval = ol.Graticule.intervals_[i];
+  }
+  return interval;
+};
+
+
+/**
+ * Get the map associated with this graticule.
+ * @return {ol.Map} The map.
+ * @api
+ */
+ol.Graticule.prototype.getMap = function() {
+  return this.map_;
+};
+
+
+/**
+ * @param {number} lon Longitude.
+ * @param {number} minLat Minimal latitude.
+ * @param {number} maxLat Maximal latitude.
+ * @param {number} squaredTolerance Squared tolerance.
+ * @return {ol.geom.LineString} The meridian line string.
+ * @param {number} index Index.
+ * @private
+ */
+ol.Graticule.prototype.getMeridian_ = function(lon, minLat, maxLat,
+                                               squaredTolerance, index) {
+  goog.asserts.assert(lon >= this.minLon_,
+      'lon should be larger than or equal to this.minLon_');
+  goog.asserts.assert(lon <= this.maxLon_,
+      'lon should be smaller than or equal to this.maxLon_');
+  var flatCoordinates = ol.geom.flat.geodesic.meridian(lon,
+      minLat, maxLat, this.projection_, squaredTolerance);
+  goog.asserts.assert(flatCoordinates.length > 0,
+      'flatCoordinates cannot be empty');
+  var lineString = this.meridians_[index] !== undefined ?
+      this.meridians_[index] : new ol.geom.LineString(null);
+  lineString.setFlatCoordinates(ol.geom.GeometryLayout.XY, flatCoordinates);
+  return lineString;
+};
+
+
+/**
+ * Get the list of meridians.  Meridians are lines of equal longitude.
+ * @return {Array.<ol.geom.LineString>} The meridians.
+ * @api
+ */
+ol.Graticule.prototype.getMeridians = function() {
+  return this.meridians_;
+};
+
+
+/**
+ * @param {number} lat Latitude.
+ * @param {number} minLon Minimal longitude.
+ * @param {number} maxLon Maximal longitude.
+ * @param {number} squaredTolerance Squared tolerance.
+ * @return {ol.geom.LineString} The parallel line string.
+ * @param {number} index Index.
+ * @private
+ */
+ol.Graticule.prototype.getParallel_ = function(lat, minLon, maxLon,
+                                               squaredTolerance, index) {
+  goog.asserts.assert(lat >= this.minLat_,
+      'lat should be larger than or equal to this.minLat_');
+  goog.asserts.assert(lat <= this.maxLat_,
+      'lat should be smaller than or equal to this.maxLat_');
+  var flatCoordinates = ol.geom.flat.geodesic.parallel(lat,
+      this.minLon_, this.maxLon_, this.projection_, squaredTolerance);
+  goog.asserts.assert(flatCoordinates.length > 0,
+      'flatCoordinates cannot be empty');
+  var lineString = this.parallels_[index] !== undefined ?
+      this.parallels_[index] : new ol.geom.LineString(null);
+  lineString.setFlatCoordinates(ol.geom.GeometryLayout.XY, flatCoordinates);
+  return lineString;
+};
+
+
+/**
+ * Get the list of parallels.  Pallels are lines of equal latitude.
+ * @return {Array.<ol.geom.LineString>} The parallels.
+ * @api
+ */
+ol.Graticule.prototype.getParallels = function() {
+  return this.parallels_;
+};
+
+
+/**
+ * @param {ol.render.Event} e Event.
+ * @private
+ */
+ol.Graticule.prototype.handlePostCompose_ = function(e) {
+  var vectorContext = e.vectorContext;
+  var frameState = e.frameState;
+  var extent = frameState.extent;
+  var viewState = frameState.viewState;
+  var center = viewState.center;
+  var projection = viewState.projection;
+  var resolution = viewState.resolution;
+  var pixelRatio = frameState.pixelRatio;
+  var squaredTolerance =
+      resolution * resolution / (4 * pixelRatio * pixelRatio);
+
+  var updateProjectionInfo = !this.projection_ ||
+      !ol.proj.equivalent(this.projection_, projection);
+
+  if (updateProjectionInfo) {
+    this.updateProjectionInfo_(projection);
+  }
+
+  //Fix the extent if wrapped.
+  //(note: this is the same extent as vectorContext.extent_)
+  var offsetX = 0;
+  if (projection.canWrapX()) {
+    var projectionExtent = projection.getExtent();
+    var worldWidth = ol.extent.getWidth(projectionExtent);
+    var x = frameState.focus[0];
+    if (x < projectionExtent[0] || x > projectionExtent[2]) {
+      var worldsAway = Math.ceil((projectionExtent[0] - x) / worldWidth);
+      offsetX = worldWidth * worldsAway;
+      extent = [
+        extent[0] + offsetX, extent[1],
+        extent[2] + offsetX, extent[3]
+      ];
+    }
+  }
+
+  this.createGraticule_(extent, center, resolution, squaredTolerance);
+
+  // Draw the lines
+  vectorContext.setFillStrokeStyle(null, this.strokeStyle_);
+  var i, l, line;
+  for (i = 0, l = this.meridians_.length; i < l; ++i) {
+    line = this.meridians_[i];
+    vectorContext.drawLineString(line, null);
+  }
+  for (i = 0, l = this.parallels_.length; i < l; ++i) {
+    line = this.parallels_[i];
+    vectorContext.drawLineString(line, null);
+  }
+};
+
+
+/**
+ * @param {ol.proj.Projection} projection Projection.
+ * @private
+ */
+ol.Graticule.prototype.updateProjectionInfo_ = function(projection) {
+  goog.asserts.assert(projection, 'projection cannot be null');
+
+  var epsg4326Projection = ol.proj.get('EPSG:4326');
+
+  var extent = projection.getExtent();
+  var worldExtent = projection.getWorldExtent();
+  var worldExtentP = ol.proj.transformExtent(worldExtent,
+      epsg4326Projection, projection);
+
+  var maxLat = worldExtent[3];
+  var maxLon = worldExtent[2];
+  var minLat = worldExtent[1];
+  var minLon = worldExtent[0];
+
+  var maxLatP = worldExtentP[3];
+  var maxLonP = worldExtentP[2];
+  var minLatP = worldExtentP[1];
+  var minLonP = worldExtentP[0];
+
+  goog.asserts.assert(extent, 'extent cannot be null');
+  goog.asserts.assert(maxLat !== undefined, 'maxLat should be defined');
+  goog.asserts.assert(maxLon !== undefined, 'maxLon should be defined');
+  goog.asserts.assert(minLat !== undefined, 'minLat should be defined');
+  goog.asserts.assert(minLon !== undefined, 'minLon should be defined');
+
+  goog.asserts.assert(maxLatP !== undefined,
+      'projected maxLat should be defined');
+  goog.asserts.assert(maxLonP !== undefined,
+      'projected maxLon should be defined');
+  goog.asserts.assert(minLatP !== undefined,
+      'projected minLat should be defined');
+  goog.asserts.assert(minLonP !== undefined,
+      'projected minLon should be defined');
+
+  this.maxLat_ = maxLat;
+  this.maxLon_ = maxLon;
+  this.minLat_ = minLat;
+  this.minLon_ = minLon;
+
+  this.maxLatP_ = maxLatP;
+  this.maxLonP_ = maxLonP;
+  this.minLatP_ = minLatP;
+  this.minLonP_ = minLonP;
+
+
+  this.fromLonLatTransform_ = ol.proj.getTransform(
+      epsg4326Projection, projection);
+
+  this.toLonLatTransform_ = ol.proj.getTransform(
+      projection, epsg4326Projection);
+
+  this.projectionCenterLonLat_ = this.toLonLatTransform_(
+      ol.extent.getCenter(extent));
+
+  this.projection_ = projection;
+};
+
+
+/**
+ * Set the map for this graticule.  The graticule will be rendered on the
+ * provided map.
+ * @param {ol.Map} map Map.
+ * @api
+ */
+ol.Graticule.prototype.setMap = function(map) {
+  if (this.map_) {
+    this.map_.un(ol.render.EventType.POSTCOMPOSE,
+        this.handlePostCompose_, this);
+    this.map_.render();
+  }
+  if (map) {
+    map.on(ol.render.EventType.POSTCOMPOSE,
+        this.handlePostCompose_, this);
+    map.render();
+  }
+  this.map_ = map;
+};
+
+goog.provide('ol.Image');
+
+goog.require('goog.asserts');
+goog.require('ol.ImageBase');
+goog.require('ol.ImageState');
+goog.require('ol.events');
+goog.require('ol.events.EventType');
+goog.require('ol.extent');
+goog.require('ol.object');
+
+
+/**
+ * @constructor
+ * @extends {ol.ImageBase}
+ * @param {ol.Extent} extent Extent.
+ * @param {number|undefined} resolution Resolution.
+ * @param {number} pixelRatio Pixel ratio.
+ * @param {Array.<ol.Attribution>} attributions Attributions.
+ * @param {string} src Image source URI.
+ * @param {?string} crossOrigin Cross origin.
+ * @param {ol.ImageLoadFunctionType} imageLoadFunction Image load function.
+ */
+ol.Image = function(extent, resolution, pixelRatio, attributions, src,
+    crossOrigin, imageLoadFunction) {
+
+  ol.ImageBase.call(this, extent, resolution, pixelRatio, ol.ImageState.IDLE,
+      attributions);
+
+  /**
+   * @private
+   * @type {string}
+   */
+  this.src_ = src;
+
+  /**
+   * @private
+   * @type {HTMLCanvasElement|Image|HTMLVideoElement}
+   */
+  this.image_ = new Image();
+  if (crossOrigin !== null) {
+    this.image_.crossOrigin = crossOrigin;
+  }
+
+  /**
+   * @private
+   * @type {Object.<number, (HTMLCanvasElement|Image|HTMLVideoElement)>}
+   */
+  this.imageByContext_ = {};
+
+  /**
+   * @private
+   * @type {Array.<ol.EventsKey>}
+   */
+  this.imageListenerKeys_ = null;
+
+  /**
+   * @protected
+   * @type {ol.ImageState}
+   */
+  this.state = ol.ImageState.IDLE;
+
+  /**
+   * @private
+   * @type {ol.ImageLoadFunctionType}
+   */
+  this.imageLoadFunction_ = imageLoadFunction;
+
+};
+ol.inherits(ol.Image, ol.ImageBase);
+
+
+/**
+ * Get the HTML image element (may be a Canvas, Image, or Video).
+ * @param {Object=} opt_context Object.
+ * @return {HTMLCanvasElement|Image|HTMLVideoElement} Image.
+ * @api
+ */
+ol.Image.prototype.getImage = function(opt_context) {
+  if (opt_context !== undefined) {
+    var image;
+    var key = goog.getUid(opt_context);
+    if (key in this.imageByContext_) {
+      return this.imageByContext_[key];
+    } else if (ol.object.isEmpty(this.imageByContext_)) {
+      image = this.image_;
+    } else {
+      image = /** @type {Image} */ (this.image_.cloneNode(false));
+    }
+    this.imageByContext_[key] = image;
+    return image;
+  } else {
+    return this.image_;
+  }
+};
+
+
+/**
+ * Tracks loading or read errors.
+ *
+ * @private
+ */
+ol.Image.prototype.handleImageError_ = function() {
+  this.state = ol.ImageState.ERROR;
+  this.unlistenImage_();
+  this.changed();
+};
+
+
+/**
+ * Tracks successful image load.
+ *
+ * @private
+ */
+ol.Image.prototype.handleImageLoad_ = function() {
+  if (this.resolution === undefined) {
+    this.resolution = ol.extent.getHeight(this.extent) / this.image_.height;
+  }
+  this.state = ol.ImageState.LOADED;
+  this.unlistenImage_();
+  this.changed();
+};
+
+
+/**
+ * Load 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.
+ * @api
+ */
+ol.Image.prototype.load = function() {
+  if (this.state == ol.ImageState.IDLE || this.state == ol.ImageState.ERROR) {
+    this.state = ol.ImageState.LOADING;
+    this.changed();
+    goog.asserts.assert(!this.imageListenerKeys_,
+        'this.imageListenerKeys_ should be null');
+    this.imageListenerKeys_ = [
+      ol.events.listenOnce(this.image_, ol.events.EventType.ERROR,
+          this.handleImageError_, this),
+      ol.events.listenOnce(this.image_, ol.events.EventType.LOAD,
+          this.handleImageLoad_, this)
+    ];
+    this.imageLoadFunction_(this, this.src_);
+  }
+};
+
+
+/**
+ * @param {HTMLCanvasElement|Image|HTMLVideoElement} image Image.
+ */
+ol.Image.prototype.setImage = function(image) {
+  this.image_ = image;
+};
+
+
+/**
+ * Discards event handlers which listen for load completion or errors.
+ *
+ * @private
+ */
+ol.Image.prototype.unlistenImage_ = function() {
+  goog.asserts.assert(this.imageListenerKeys_,
+      'this.imageListenerKeys_ should not be null');
+  this.imageListenerKeys_.forEach(ol.events.unlistenByKey);
+  this.imageListenerKeys_ = null;
+};
+
+goog.provide('ol.ImageTile');
+
+goog.require('goog.asserts');
+goog.require('ol.Tile');
+goog.require('ol.TileState');
+goog.require('ol.events');
+goog.require('ol.events.EventType');
+goog.require('ol.object');
+
+
+/**
+ * @constructor
+ * @extends {ol.Tile}
+ * @param {ol.TileCoord} tileCoord Tile coordinate.
+ * @param {ol.TileState} state State.
+ * @param {string} src Image source URI.
+ * @param {?string} crossOrigin Cross origin.
+ * @param {ol.TileLoadFunctionType} tileLoadFunction Tile load function.
+ */
+ol.ImageTile = function(tileCoord, state, src, crossOrigin, tileLoadFunction) {
+
+  ol.Tile.call(this, tileCoord, state);
+
+  /**
+   * Image URI
+   *
+   * @private
+   * @type {string}
+   */
+  this.src_ = src;
+
+  /**
+   * @private
+   * @type {Image}
+   */
+  this.image_ = new Image();
+  if (crossOrigin !== null) {
+    this.image_.crossOrigin = crossOrigin;
+  }
+
+  /**
+   * @private
+   * @type {Object.<number, Image>}
+   */
+  this.imageByContext_ = {};
+
+  /**
+   * @private
+   * @type {Array.<ol.EventsKey>}
+   */
+  this.imageListenerKeys_ = null;
+
+  /**
+   * @private
+   * @type {ol.TileLoadFunctionType}
+   */
+  this.tileLoadFunction_ = tileLoadFunction;
+
+};
+ol.inherits(ol.ImageTile, ol.Tile);
+
+
+/**
+ * @inheritDoc
+ */
+ol.ImageTile.prototype.disposeInternal = function() {
+  if (this.state == ol.TileState.LOADING) {
+    this.unlistenImage_();
+  }
+  if (this.interimTile) {
+    this.interimTile.dispose();
+  }
+  this.state = ol.TileState.ABORT;
+  this.changed();
+  ol.Tile.prototype.disposeInternal.call(this);
+};
+
+
+/**
+ * Get the image element for this tile.
+ * @inheritDoc
+ * @api
+ */
+ol.ImageTile.prototype.getImage = function(opt_context) {
+  if (opt_context !== undefined) {
+    var image;
+    var key = goog.getUid(opt_context);
+    if (key in this.imageByContext_) {
+      return this.imageByContext_[key];
+    } else if (ol.object.isEmpty(this.imageByContext_)) {
+      image = this.image_;
+    } else {
+      image = /** @type {Image} */ (this.image_.cloneNode(false));
+    }
+    this.imageByContext_[key] = image;
+    return image;
+  } else {
+    return this.image_;
+  }
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.ImageTile.prototype.getKey = function() {
+  return this.src_;
+};
+
+
+/**
+ * Tracks loading or read errors.
+ *
+ * @private
+ */
+ol.ImageTile.prototype.handleImageError_ = function() {
+  this.state = ol.TileState.ERROR;
+  this.unlistenImage_();
+  this.changed();
+};
+
+
+/**
+ * Tracks successful image load.
+ *
+ * @private
+ */
+ol.ImageTile.prototype.handleImageLoad_ = function() {
+  if (this.image_.naturalWidth && this.image_.naturalHeight) {
+    this.state = ol.TileState.LOADED;
+  } else {
+    this.state = ol.TileState.EMPTY;
+  }
+  this.unlistenImage_();
+  this.changed();
+};
+
+
+/**
+ * Load 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.
+ * @api
+ */
+ol.ImageTile.prototype.load = function() {
+  if (this.state == ol.TileState.IDLE || this.state == ol.TileState.ERROR) {
+    this.state = ol.TileState.LOADING;
+    this.changed();
+    goog.asserts.assert(!this.imageListenerKeys_,
+        'this.imageListenerKeys_ should be null');
+    this.imageListenerKeys_ = [
+      ol.events.listenOnce(this.image_, ol.events.EventType.ERROR,
+          this.handleImageError_, this),
+      ol.events.listenOnce(this.image_, ol.events.EventType.LOAD,
+          this.handleImageLoad_, this)
+    ];
+    this.tileLoadFunction_(this, this.src_);
+  }
+};
+
+
+/**
+ * Discards event handlers which listen for load completion or errors.
+ *
+ * @private
+ */
+ol.ImageTile.prototype.unlistenImage_ = function() {
+  goog.asserts.assert(this.imageListenerKeys_,
+      'this.imageListenerKeys_ should not be null');
+  this.imageListenerKeys_.forEach(ol.events.unlistenByKey);
+  this.imageListenerKeys_ = null;
+};
+
+// FIXME should handle all geo-referenced data, not just vector data
+
+goog.provide('ol.interaction.DragAndDrop');
+goog.provide('ol.interaction.DragAndDropEvent');
+
+goog.require('goog.asserts');
+goog.require('ol.functions');
+goog.require('ol.events');
+goog.require('ol.events.Event');
+goog.require('ol.events.EventType');
+goog.require('ol.interaction.Interaction');
+goog.require('ol.proj');
+
+
+/**
+ * @classdesc
+ * Handles input of vector data by drag and drop.
+ *
+ * @constructor
+ * @extends {ol.interaction.Interaction}
+ * @fires ol.interaction.DragAndDropEvent
+ * @param {olx.interaction.DragAndDropOptions=} opt_options Options.
+ * @api stable
+ */
+ol.interaction.DragAndDrop = function(opt_options) {
+
+  var options = opt_options ? opt_options : {};
+
+  ol.interaction.Interaction.call(this, {
+    handleEvent: ol.interaction.DragAndDrop.handleEvent
+  });
+
+  /**
+   * @private
+   * @type {Array.<function(new: ol.format.Feature)>}
+   */
+  this.formatConstructors_ = options.formatConstructors ?
+      options.formatConstructors : [];
+
+  /**
+   * @private
+   * @type {ol.proj.Projection}
+   */
+  this.projection_ = options.projection ?
+      ol.proj.get(options.projection) : null;
+
+  /**
+   * @private
+   * @type {Array.<ol.EventsKey>}
+   */
+  this.dropListenKeys_ = null;
+
+  /**
+   * @private
+   * @type {Element}
+   */
+  this.target = options.target ? options.target : null;
+
+};
+ol.inherits(ol.interaction.DragAndDrop, ol.interaction.Interaction);
+
+
+/**
+ * @param {Event} event Event.
+ * @this {ol.interaction.DragAndDrop}
+ * @private
+ */
+ol.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(ol.events.EventType.LOAD,
+        this.handleResult_.bind(this, file));
+    reader.readAsText(file);
+  }
+};
+
+
+/**
+ * @param {Event} event Event.
+ * @private
+ */
+ol.interaction.DragAndDrop.handleStop_ = function(event) {
+  event.stopPropagation();
+  event.preventDefault();
+  event.dataTransfer.dropEffect = 'copy';
+};
+
+
+/**
+ * @param {File} file File.
+ * @param {Event} event Load event.
+ * @private
+ */
+ol.interaction.DragAndDrop.prototype.handleResult_ = function(file, event) {
+  var result = event.target.result;
+  var map = this.getMap();
+  goog.asserts.assert(map, 'map must be set');
+  var projection = this.projection_;
+  if (!projection) {
+    var view = map.getView();
+    goog.asserts.assert(view, 'map must have view');
+    projection = view.getProjection();
+    goog.asserts.assert(projection !== undefined,
+        'projection should be defined');
+  }
+  var formatConstructors = this.formatConstructors_;
+  var features = [];
+  var i, ii;
+  for (i = 0, ii = formatConstructors.length; i < ii; ++i) {
+    var formatConstructor = formatConstructors[i];
+    var format = new formatConstructor();
+    features = this.tryReadFeatures_(format, result, {
+      featureProjection: projection
+    });
+    if (features && features.length > 0) {
+      break;
+    }
+  }
+  this.dispatchEvent(
+      new ol.interaction.DragAndDropEvent(
+          ol.interaction.DragAndDropEventType.ADD_FEATURES, this, file,
+          features, projection));
+};
+
+
+/**
+ * Handles the {@link ol.MapBrowserEvent map browser event} unconditionally and
+ * neither prevents the browser default nor stops event propagation.
+ * @param {ol.MapBrowserEvent} mapBrowserEvent Map browser event.
+ * @return {boolean} `false` to stop event propagation.
+ * @this {ol.interaction.DragAndDrop}
+ * @api
+ */
+ol.interaction.DragAndDrop.handleEvent = ol.functions.TRUE;
+
+
+/**
+ * @inheritDoc
+ */
+ol.interaction.DragAndDrop.prototype.setMap = function(map) {
+  if (this.dropListenKeys_) {
+    this.dropListenKeys_.forEach(ol.events.unlistenByKey);
+    this.dropListenKeys_ = null;
+  }
+  ol.interaction.Interaction.prototype.setMap.call(this, map);
+  if (map) {
+    var dropArea = this.target ? this.target : map.getViewport();
+    this.dropListenKeys_ = [
+      ol.events.listen(dropArea, ol.events.EventType.DROP,
+          ol.interaction.DragAndDrop.handleDrop_, this),
+      ol.events.listen(dropArea, ol.events.EventType.DRAGENTER,
+          ol.interaction.DragAndDrop.handleStop_, this),
+      ol.events.listen(dropArea, ol.events.EventType.DRAGOVER,
+          ol.interaction.DragAndDrop.handleStop_, this),
+      ol.events.listen(dropArea, ol.events.EventType.DROP,
+          ol.interaction.DragAndDrop.handleStop_, this)
+    ];
+  }
+};
+
+
+/**
+ * @param {ol.format.Feature} format Format.
+ * @param {string} text Text.
+ * @param {olx.format.ReadOptions} options Read options.
+ * @private
+ * @return {Array.<ol.Feature>} Features.
+ */
+ol.interaction.DragAndDrop.prototype.tryReadFeatures_ = function(format, text, options) {
+  try {
+    return format.readFeatures(text, options);
+  } catch (e) {
+    return null;
+  }
+};
+
+
+/**
+ * @enum {string}
+ */
+ol.interaction.DragAndDropEventType = {
+  /**
+   * Triggered when features are added
+   * @event ol.interaction.DragAndDropEvent#addfeatures
+   * @api stable
+   */
+  ADD_FEATURES: 'addfeatures'
+};
+
+
+/**
+ * @classdesc
+ * Events emitted by {@link ol.interaction.DragAndDrop} instances are instances
+ * of this type.
+ *
+ * @constructor
+ * @extends {ol.events.Event}
+ * @implements {oli.interaction.DragAndDropEvent}
+ * @param {ol.interaction.DragAndDropEventType} type Type.
+ * @param {Object} target Target.
+ * @param {File} file File.
+ * @param {Array.<ol.Feature>=} opt_features Features.
+ * @param {ol.proj.Projection=} opt_projection Projection.
+ */
+ol.interaction.DragAndDropEvent = function(type, target, file, opt_features, opt_projection) {
+
+  ol.events.Event.call(this, type, target);
+
+  /**
+   * The features parsed from dropped data.
+   * @type {Array.<ol.Feature>|undefined}
+   * @api stable
+   */
+  this.features = opt_features;
+
+  /**
+   * The dropped file.
+   * @type {File}
+   * @api stable
+   */
+  this.file = file;
+
+  /**
+   * The feature projection.
+   * @type {ol.proj.Projection|undefined}
+   * @api
+   */
+  this.projection = opt_projection;
+
+};
+ol.inherits(ol.interaction.DragAndDropEvent, ol.events.Event);
+
+goog.provide('ol.interaction.DragRotateAndZoom');
+
+goog.require('ol');
+goog.require('ol.ViewHint');
+goog.require('ol.events.condition');
+goog.require('ol.interaction.Interaction');
+goog.require('ol.interaction.Pointer');
+
+
+/**
+ * @classdesc
+ * Allows the user to zoom and rotate the map by clicking and dragging
+ * on the map.  By default, this interaction is limited to when the shift
+ * key is held down.
+ *
+ * This interaction is only supported for mouse devices.
+ *
+ * And this interaction is not included in the default interactions.
+ *
+ * @constructor
+ * @extends {ol.interaction.Pointer}
+ * @param {olx.interaction.DragRotateAndZoomOptions=} opt_options Options.
+ * @api stable
+ */
+ol.interaction.DragRotateAndZoom = function(opt_options) {
+
+  var options = opt_options ? opt_options : {};
+
+  ol.interaction.Pointer.call(this, {
+    handleDownEvent: ol.interaction.DragRotateAndZoom.handleDownEvent_,
+    handleDragEvent: ol.interaction.DragRotateAndZoom.handleDragEvent_,
+    handleUpEvent: ol.interaction.DragRotateAndZoom.handleUpEvent_
+  });
+
+  /**
+   * @private
+   * @type {ol.EventsConditionType}
+   */
+  this.condition_ = options.condition ?
+      options.condition : ol.events.condition.shiftKeyOnly;
+
+  /**
+   * @private
+   * @type {number|undefined}
+   */
+  this.lastAngle_ = undefined;
+
+  /**
+   * @private
+   * @type {number|undefined}
+   */
+  this.lastMagnitude_ = undefined;
+
+  /**
+   * @private
+   * @type {number}
+   */
+  this.lastScaleDelta_ = 0;
+
+  /**
+   * @private
+   * @type {number}
+   */
+  this.duration_ = options.duration !== undefined ? options.duration : 400;
+
+};
+ol.inherits(ol.interaction.DragRotateAndZoom, ol.interaction.Pointer);
+
+
+/**
+ * @param {ol.MapBrowserPointerEvent} mapBrowserEvent Event.
+ * @this {ol.interaction.DragRotateAndZoom}
+ * @private
+ */
+ol.interaction.DragRotateAndZoom.handleDragEvent_ = function(mapBrowserEvent) {
+  if (!ol.events.condition.mouseOnly(mapBrowserEvent)) {
+    return;
+  }
+
+  var map = mapBrowserEvent.map;
+  var size = map.getSize();
+  var offset = mapBrowserEvent.pixel;
+  var 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();
+  map.render();
+  if (this.lastAngle_ !== undefined) {
+    var angleDelta = theta - this.lastAngle_;
+    ol.interaction.Interaction.rotateWithoutConstraints(
+        map, view, view.getRotation() - angleDelta);
+  }
+  this.lastAngle_ = theta;
+  if (this.lastMagnitude_ !== undefined) {
+    var resolution = this.lastMagnitude_ * (view.getResolution() / magnitude);
+    ol.interaction.Interaction.zoomWithoutConstraints(map, view, resolution);
+  }
+  if (this.lastMagnitude_ !== undefined) {
+    this.lastScaleDelta_ = this.lastMagnitude_ / magnitude;
+  }
+  this.lastMagnitude_ = magnitude;
+};
+
+
+/**
+ * @param {ol.MapBrowserPointerEvent} mapBrowserEvent Event.
+ * @return {boolean} Stop drag sequence?
+ * @this {ol.interaction.DragRotateAndZoom}
+ * @private
+ */
+ol.interaction.DragRotateAndZoom.handleUpEvent_ = function(mapBrowserEvent) {
+  if (!ol.events.condition.mouseOnly(mapBrowserEvent)) {
+    return true;
+  }
+
+  var map = mapBrowserEvent.map;
+  var view = map.getView();
+  view.setHint(ol.ViewHint.INTERACTING, -1);
+  var direction = this.lastScaleDelta_ - 1;
+  ol.interaction.Interaction.rotate(map, view, view.getRotation());
+  ol.interaction.Interaction.zoom(map, view, view.getResolution(),
+      undefined, this.duration_, direction);
+  this.lastScaleDelta_ = 0;
+  return false;
+};
+
+
+/**
+ * @param {ol.MapBrowserPointerEvent} mapBrowserEvent Event.
+ * @return {boolean} Start drag sequence?
+ * @this {ol.interaction.DragRotateAndZoom}
+ * @private
+ */
+ol.interaction.DragRotateAndZoom.handleDownEvent_ = function(mapBrowserEvent) {
+  if (!ol.events.condition.mouseOnly(mapBrowserEvent)) {
+    return false;
+  }
+
+  if (this.condition_(mapBrowserEvent)) {
+    mapBrowserEvent.map.getView().setHint(ol.ViewHint.INTERACTING, 1);
+    this.lastAngle_ = undefined;
+    this.lastMagnitude_ = undefined;
+    return true;
+  } else {
+    return false;
+  }
+};
+
+goog.provide('ol.interaction.Draw');
+goog.provide('ol.interaction.DrawEvent');
+goog.provide('ol.interaction.DrawEventType');
+goog.provide('ol.interaction.DrawMode');
+
+goog.require('goog.asserts');
+goog.require('ol.events');
+goog.require('ol.events.Event');
+goog.require('ol.Collection');
+goog.require('ol.Feature');
+goog.require('ol.MapBrowserEvent');
+goog.require('ol.MapBrowserEvent.EventType');
+goog.require('ol.Object');
+goog.require('ol.coordinate');
+goog.require('ol.functions');
+goog.require('ol.events.condition');
+goog.require('ol.geom.Circle');
+goog.require('ol.geom.GeometryType');
+goog.require('ol.geom.LineString');
+goog.require('ol.geom.MultiLineString');
+goog.require('ol.geom.MultiPoint');
+goog.require('ol.geom.MultiPolygon');
+goog.require('ol.geom.Point');
+goog.require('ol.geom.Polygon');
+goog.require('ol.geom.SimpleGeometry');
+goog.require('ol.interaction.InteractionProperty');
+goog.require('ol.interaction.Pointer');
+goog.require('ol.layer.Vector');
+goog.require('ol.source.Vector');
+
+
+/**
+ * @enum {string}
+ */
+ol.interaction.DrawEventType = {
+  /**
+   * Triggered upon feature draw start
+   * @event ol.interaction.DrawEvent#drawstart
+   * @api stable
+   */
+  DRAWSTART: 'drawstart',
+  /**
+   * Triggered upon feature draw end
+   * @event ol.interaction.DrawEvent#drawend
+   * @api stable
+   */
+  DRAWEND: 'drawend'
+};
+
+
+/**
+ * @classdesc
+ * Events emitted by {@link ol.interaction.Draw} instances are instances of
+ * this type.
+ *
+ * @constructor
+ * @extends {ol.events.Event}
+ * @implements {oli.DrawEvent}
+ * @param {ol.interaction.DrawEventType} type Type.
+ * @param {ol.Feature} feature The feature drawn.
+ */
+ol.interaction.DrawEvent = function(type, feature) {
+
+  ol.events.Event.call(this, type);
+
+  /**
+   * The feature being drawn.
+   * @type {ol.Feature}
+   * @api stable
+   */
+  this.feature = feature;
+
+};
+ol.inherits(ol.interaction.DrawEvent, ol.events.Event);
+
+
+/**
+ * @classdesc
+ * Interaction for drawing feature geometries.
+ *
+ * @constructor
+ * @extends {ol.interaction.Pointer}
+ * @fires ol.interaction.DrawEvent
+ * @param {olx.interaction.DrawOptions} options Options.
+ * @api stable
+ */
+ol.interaction.Draw = function(options) {
+
+  ol.interaction.Pointer.call(this, {
+    handleDownEvent: ol.interaction.Draw.handleDownEvent_,
+    handleEvent: ol.interaction.Draw.handleEvent,
+    handleUpEvent: ol.interaction.Draw.handleUpEvent_
+  });
+
+  /**
+   * @type {ol.Pixel}
+   * @private
+   */
+  this.downPx_ = null;
+
+  /**
+   * @type {boolean}
+   * @private
+   */
+  this.freehand_ = false;
+
+  /**
+   * Target source for drawn features.
+   * @type {ol.source.Vector}
+   * @private
+   */
+  this.source_ = options.source ? options.source : null;
+
+  /**
+   * Target collection for drawn features.
+   * @type {ol.Collection.<ol.Feature>}
+   * @private
+   */
+  this.features_ = options.features ? options.features : null;
+
+  /**
+   * Pixel distance for snapping.
+   * @type {number}
+   * @private
+   */
+  this.snapTolerance_ = options.snapTolerance ? options.snapTolerance : 12;
+
+  /**
+   * Geometry type.
+   * @type {ol.geom.GeometryType}
+   * @private
+   */
+  this.type_ = options.type;
+
+  /**
+   * Drawing mode (derived from geometry type.
+   * @type {ol.interaction.DrawMode}
+   * @private
+   */
+  this.mode_ = ol.interaction.Draw.getMode_(this.type_);
+
+  /**
+   * The number of points that must be drawn before a polygon ring or line
+   * string can be finished.  The default is 3 for polygon rings and 2 for
+   * line strings.
+   * @type {number}
+   * @private
+   */
+  this.minPoints_ = options.minPoints ?
+      options.minPoints :
+      (this.mode_ === ol.interaction.DrawMode.POLYGON ? 3 : 2);
+
+  /**
+   * The number of points that can be drawn before a polygon ring or line string
+   * is finished. The default is no restriction.
+   * @type {number}
+   * @private
+   */
+  this.maxPoints_ = options.maxPoints ? options.maxPoints : Infinity;
+
+  /**
+   * A function to decide if a potential finish coordinate is permissable
+   * @private
+   * @type {ol.EventsConditionType}
+   */
+  this.finishCondition_ = options.finishCondition ? options.finishCondition : ol.functions.TRUE;
+
+  var geometryFunction = options.geometryFunction;
+  if (!geometryFunction) {
+    if (this.type_ === ol.geom.GeometryType.CIRCLE) {
+      /**
+       * @param {ol.Coordinate|Array.<ol.Coordinate>|Array.<Array.<ol.Coordinate>>} coordinates
+       *     The coordinates.
+       * @param {ol.geom.SimpleGeometry=} opt_geometry Optional geometry.
+       * @return {ol.geom.SimpleGeometry} A geometry.
+       */
+      geometryFunction = function(coordinates, opt_geometry) {
+        var circle = opt_geometry ? opt_geometry :
+            new ol.geom.Circle([NaN, NaN]);
+        goog.asserts.assertInstanceof(circle, ol.geom.Circle,
+            'geometry must be an ol.geom.Circle');
+        var squaredLength = ol.coordinate.squaredDistance(
+            coordinates[0], coordinates[1]);
+        circle.setCenterAndRadius(coordinates[0], Math.sqrt(squaredLength));
+        return circle;
+      };
+    } else {
+      var Constructor;
+      var mode = this.mode_;
+      if (mode === ol.interaction.DrawMode.POINT) {
+        Constructor = ol.geom.Point;
+      } else if (mode === ol.interaction.DrawMode.LINE_STRING) {
+        Constructor = ol.geom.LineString;
+      } else if (mode === ol.interaction.DrawMode.POLYGON) {
+        Constructor = ol.geom.Polygon;
+      }
+      /**
+       * @param {ol.Coordinate|Array.<ol.Coordinate>|Array.<Array.<ol.Coordinate>>} coordinates
+       *     The coordinates.
+       * @param {ol.geom.SimpleGeometry=} opt_geometry Optional geometry.
+       * @return {ol.geom.SimpleGeometry} A geometry.
+       */
+      geometryFunction = function(coordinates, opt_geometry) {
+        var geometry = opt_geometry;
+        if (geometry) {
+          geometry.setCoordinates(coordinates);
+        } else {
+          geometry = new Constructor(coordinates);
+        }
+        return geometry;
+      };
+    }
+  }
+
+  /**
+   * @type {ol.DrawGeometryFunctionType}
+   * @private
+   */
+  this.geometryFunction_ = geometryFunction;
+
+  /**
+   * Finish coordinate for the feature (first point for polygons, last point for
+   * linestrings).
+   * @type {ol.Coordinate}
+   * @private
+   */
+  this.finishCoordinate_ = null;
+
+  /**
+   * Sketch feature.
+   * @type {ol.Feature}
+   * @private
+   */
+  this.sketchFeature_ = null;
+
+  /**
+   * Sketch point.
+   * @type {ol.Feature}
+   * @private
+   */
+  this.sketchPoint_ = null;
+
+  /**
+   * Sketch coordinates. Used when drawing a line or polygon.
+   * @type {ol.Coordinate|Array.<ol.Coordinate>|Array.<Array.<ol.Coordinate>>}
+   * @private
+   */
+  this.sketchCoords_ = null;
+
+  /**
+   * Sketch line. Used when drawing polygon.
+   * @type {ol.Feature}
+   * @private
+   */
+  this.sketchLine_ = null;
+
+  /**
+   * Sketch line coordinates. Used when drawing a polygon or circle.
+   * @type {Array.<ol.Coordinate>}
+   * @private
+   */
+  this.sketchLineCoords_ = null;
+
+  /**
+   * Squared tolerance for handling up events.  If the squared distance
+   * between a down and up event is greater than this tolerance, up events
+   * will not be handled.
+   * @type {number}
+   * @private
+   */
+  this.squaredClickTolerance_ = options.clickTolerance ?
+      options.clickTolerance * options.clickTolerance : 36;
+
+  /**
+   * Draw overlay where our sketch features are drawn.
+   * @type {ol.layer.Vector}
+   * @private
+   */
+  this.overlay_ = new ol.layer.Vector({
+    source: new ol.source.Vector({
+      useSpatialIndex: false,
+      wrapX: options.wrapX ? options.wrapX : false
+    }),
+    style: options.style ? options.style :
+        ol.interaction.Draw.getDefaultStyleFunction()
+  });
+
+  /**
+   * Name of the geometry attribute for newly created features.
+   * @type {string|undefined}
+   * @private
+   */
+  this.geometryName_ = options.geometryName;
+
+  /**
+   * @private
+   * @type {ol.EventsConditionType}
+   */
+  this.condition_ = options.condition ?
+      options.condition : ol.events.condition.noModifierKeys;
+
+  /**
+   * @private
+   * @type {ol.EventsConditionType}
+   */
+  this.freehandCondition_ = options.freehandCondition ?
+      options.freehandCondition : ol.events.condition.shiftKeyOnly;
+
+  ol.events.listen(this,
+      ol.Object.getChangeEventType(ol.interaction.InteractionProperty.ACTIVE),
+      this.updateState_, this);
+
+};
+ol.inherits(ol.interaction.Draw, ol.interaction.Pointer);
+
+
+/**
+ * @return {ol.StyleFunction} Styles.
+ */
+ol.interaction.Draw.getDefaultStyleFunction = function() {
+  var styles = ol.style.createDefaultEditingStyles();
+  return function(feature, resolution) {
+    return styles[feature.getGeometry().getType()];
+  };
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.interaction.Draw.prototype.setMap = function(map) {
+  ol.interaction.Pointer.prototype.setMap.call(this, map);
+  this.updateState_();
+};
+
+
+/**
+ * Handles the {@link ol.MapBrowserEvent map browser event} and may actually
+ * draw or finish the drawing.
+ * @param {ol.MapBrowserEvent} mapBrowserEvent Map browser event.
+ * @return {boolean} `false` to stop event propagation.
+ * @this {ol.interaction.Draw}
+ * @api
+ */
+ol.interaction.Draw.handleEvent = function(mapBrowserEvent) {
+  if ((this.mode_ === ol.interaction.DrawMode.LINE_STRING ||
+    this.mode_ === ol.interaction.DrawMode.POLYGON) &&
+    this.freehandCondition_(mapBrowserEvent)) {
+    this.freehand_ = true;
+  }
+  var pass = !this.freehand_;
+  if (this.freehand_ &&
+      mapBrowserEvent.type === ol.MapBrowserEvent.EventType.POINTERDRAG) {
+    this.addToDrawing_(mapBrowserEvent);
+    pass = false;
+  } else if (mapBrowserEvent.type ===
+      ol.MapBrowserEvent.EventType.POINTERMOVE) {
+    pass = this.handlePointerMove_(mapBrowserEvent);
+  } else if (mapBrowserEvent.type === ol.MapBrowserEvent.EventType.DBLCLICK) {
+    pass = false;
+  }
+  return ol.interaction.Pointer.handleEvent.call(this, mapBrowserEvent) && pass;
+};
+
+
+/**
+ * @param {ol.MapBrowserPointerEvent} event Event.
+ * @return {boolean} Start drag sequence?
+ * @this {ol.interaction.Draw}
+ * @private
+ */
+ol.interaction.Draw.handleDownEvent_ = function(event) {
+  if (this.condition_(event)) {
+    this.downPx_ = event.pixel;
+    return true;
+  } else if (this.freehand_) {
+    this.downPx_ = event.pixel;
+    if (!this.finishCoordinate_) {
+      this.startDrawing_(event);
+    }
+    return true;
+  } else {
+    return false;
+  }
+};
+
+
+/**
+ * @param {ol.MapBrowserPointerEvent} event Event.
+ * @return {boolean} Stop drag sequence?
+ * @this {ol.interaction.Draw}
+ * @private
+ */
+ol.interaction.Draw.handleUpEvent_ = function(event) {
+  this.freehand_ = false;
+  var downPx = this.downPx_;
+  var clickPx = event.pixel;
+  var dx = downPx[0] - clickPx[0];
+  var dy = downPx[1] - clickPx[1];
+  var squaredDistance = dx * dx + dy * dy;
+  var pass = true;
+  if (squaredDistance <= this.squaredClickTolerance_) {
+    this.handlePointerMove_(event);
+    if (!this.finishCoordinate_) {
+      this.startDrawing_(event);
+      if (this.mode_ === ol.interaction.DrawMode.POINT) {
+        this.finishDrawing();
+      }
+    } else if (this.mode_ === ol.interaction.DrawMode.CIRCLE) {
+      this.finishDrawing();
+    } else if (this.atFinish_(event)) {
+      if (this.finishCondition_(event)) {
+        this.finishDrawing();
+      }
+    } else {
+      this.addToDrawing_(event);
+    }
+    pass = false;
+  }
+  return pass;
+};
+
+
+/**
+ * Handle move events.
+ * @param {ol.MapBrowserEvent} event A move event.
+ * @return {boolean} Pass the event to other interactions.
+ * @private
+ */
+ol.interaction.Draw.prototype.handlePointerMove_ = function(event) {
+  if (this.finishCoordinate_) {
+    this.modifyDrawing_(event);
+  } else {
+    this.createOrUpdateSketchPoint_(event);
+  }
+  return true;
+};
+
+
+/**
+ * Determine if an event is within the snapping tolerance of the start coord.
+ * @param {ol.MapBrowserEvent} event Event.
+ * @return {boolean} The event is within the snapping tolerance of the start.
+ * @private
+ */
+ol.interaction.Draw.prototype.atFinish_ = function(event) {
+  var at = false;
+  if (this.sketchFeature_) {
+    var potentiallyDone = false;
+    var potentiallyFinishCoordinates = [this.finishCoordinate_];
+    if (this.mode_ === ol.interaction.DrawMode.LINE_STRING) {
+      potentiallyDone = this.sketchCoords_.length > this.minPoints_;
+    } else if (this.mode_ === ol.interaction.DrawMode.POLYGON) {
+      potentiallyDone = this.sketchCoords_[0].length >
+          this.minPoints_;
+      potentiallyFinishCoordinates = [this.sketchCoords_[0][0],
+        this.sketchCoords_[0][this.sketchCoords_[0].length - 2]];
+    }
+    if (potentiallyDone) {
+      var map = event.map;
+      for (var i = 0, ii = potentiallyFinishCoordinates.length; i < ii; i++) {
+        var finishCoordinate = potentiallyFinishCoordinates[i];
+        var finishPixel = map.getPixelFromCoordinate(finishCoordinate);
+        var pixel = event.pixel;
+        var dx = pixel[0] - finishPixel[0];
+        var dy = pixel[1] - finishPixel[1];
+        var freehand = this.freehand_ && this.freehandCondition_(event);
+        var snapTolerance = freehand ? 1 : this.snapTolerance_;
+        at = Math.sqrt(dx * dx + dy * dy) <= snapTolerance;
+        if (at) {
+          this.finishCoordinate_ = finishCoordinate;
+          break;
+        }
+      }
+    }
+  }
+  return at;
+};
+
+
+/**
+ * @param {ol.MapBrowserEvent} event Event.
+ * @private
+ */
+ol.interaction.Draw.prototype.createOrUpdateSketchPoint_ = function(event) {
+  var coordinates = event.coordinate.slice();
+  if (!this.sketchPoint_) {
+    this.sketchPoint_ = new ol.Feature(new ol.geom.Point(coordinates));
+    this.updateSketchFeatures_();
+  } else {
+    var sketchPointGeom = this.sketchPoint_.getGeometry();
+    goog.asserts.assertInstanceof(sketchPointGeom, ol.geom.Point,
+        'sketchPointGeom should be an ol.geom.Point');
+    sketchPointGeom.setCoordinates(coordinates);
+  }
+};
+
+
+/**
+ * Start the drawing.
+ * @param {ol.MapBrowserEvent} event Event.
+ * @private
+ */
+ol.interaction.Draw.prototype.startDrawing_ = function(event) {
+  var start = event.coordinate;
+  this.finishCoordinate_ = start;
+  if (this.mode_ === ol.interaction.DrawMode.POINT) {
+    this.sketchCoords_ = start.slice();
+  } else if (this.mode_ === ol.interaction.DrawMode.POLYGON) {
+    this.sketchCoords_ = [[start.slice(), start.slice()]];
+    this.sketchLineCoords_ = this.sketchCoords_[0];
+  } else {
+    this.sketchCoords_ = [start.slice(), start.slice()];
+    if (this.mode_ === ol.interaction.DrawMode.CIRCLE) {
+      this.sketchLineCoords_ = this.sketchCoords_;
+    }
+  }
+  if (this.sketchLineCoords_) {
+    this.sketchLine_ = new ol.Feature(
+        new ol.geom.LineString(this.sketchLineCoords_));
+  }
+  var geometry = this.geometryFunction_(this.sketchCoords_);
+  goog.asserts.assert(geometry !== undefined, 'geometry should be defined');
+  this.sketchFeature_ = new ol.Feature();
+  if (this.geometryName_) {
+    this.sketchFeature_.setGeometryName(this.geometryName_);
+  }
+  this.sketchFeature_.setGeometry(geometry);
+  this.updateSketchFeatures_();
+  this.dispatchEvent(new ol.interaction.DrawEvent(
+      ol.interaction.DrawEventType.DRAWSTART, this.sketchFeature_));
+};
+
+
+/**
+ * Modify the drawing.
+ * @param {ol.MapBrowserEvent} event Event.
+ * @private
+ */
+ol.interaction.Draw.prototype.modifyDrawing_ = function(event) {
+  var coordinate = event.coordinate;
+  var geometry = this.sketchFeature_.getGeometry();
+  goog.asserts.assertInstanceof(geometry, ol.geom.SimpleGeometry,
+      'geometry should be ol.geom.SimpleGeometry or subclass');
+  var coordinates, last;
+  if (this.mode_ === ol.interaction.DrawMode.POINT) {
+    last = this.sketchCoords_;
+  } else if (this.mode_ === ol.interaction.DrawMode.POLYGON) {
+    coordinates = this.sketchCoords_[0];
+    last = coordinates[coordinates.length - 1];
+    if (this.atFinish_(event)) {
+      // snap to finish
+      coordinate = this.finishCoordinate_.slice();
+    }
+  } else {
+    coordinates = this.sketchCoords_;
+    last = coordinates[coordinates.length - 1];
+  }
+  last[0] = coordinate[0];
+  last[1] = coordinate[1];
+  goog.asserts.assert(this.sketchCoords_, 'sketchCoords_ expected');
+  this.geometryFunction_(this.sketchCoords_, geometry);
+  if (this.sketchPoint_) {
+    var sketchPointGeom = this.sketchPoint_.getGeometry();
+    goog.asserts.assertInstanceof(sketchPointGeom, ol.geom.Point,
+        'sketchPointGeom should be an ol.geom.Point');
+    sketchPointGeom.setCoordinates(coordinate);
+  }
+  var sketchLineGeom;
+  if (geometry instanceof ol.geom.Polygon &&
+      this.mode_ !== ol.interaction.DrawMode.POLYGON) {
+    if (!this.sketchLine_) {
+      this.sketchLine_ = new ol.Feature(new ol.geom.LineString(null));
+    }
+    var ring = geometry.getLinearRing(0);
+    sketchLineGeom = this.sketchLine_.getGeometry();
+    goog.asserts.assertInstanceof(sketchLineGeom, ol.geom.LineString,
+        'sketchLineGeom must be an ol.geom.LineString');
+    sketchLineGeom.setFlatCoordinates(
+        ring.getLayout(), ring.getFlatCoordinates());
+  } else if (this.sketchLineCoords_) {
+    sketchLineGeom = this.sketchLine_.getGeometry();
+    goog.asserts.assertInstanceof(sketchLineGeom, ol.geom.LineString,
+        'sketchLineGeom must be an ol.geom.LineString');
+    sketchLineGeom.setCoordinates(this.sketchLineCoords_);
+  }
+  this.updateSketchFeatures_();
+};
+
+
+/**
+ * Add a new coordinate to the drawing.
+ * @param {ol.MapBrowserEvent} event Event.
+ * @private
+ */
+ol.interaction.Draw.prototype.addToDrawing_ = function(event) {
+  var coordinate = event.coordinate;
+  var geometry = this.sketchFeature_.getGeometry();
+  goog.asserts.assertInstanceof(geometry, ol.geom.SimpleGeometry,
+      'geometry must be an ol.geom.SimpleGeometry');
+  var done;
+  var coordinates;
+  if (this.mode_ === ol.interaction.DrawMode.LINE_STRING) {
+    this.finishCoordinate_ = coordinate.slice();
+    coordinates = this.sketchCoords_;
+    coordinates.push(coordinate.slice());
+    done = coordinates.length > this.maxPoints_;
+    this.geometryFunction_(coordinates, geometry);
+  } else if (this.mode_ === ol.interaction.DrawMode.POLYGON) {
+    coordinates = this.sketchCoords_[0];
+    coordinates.push(coordinate.slice());
+    done = coordinates.length > this.maxPoints_;
+    if (done) {
+      this.finishCoordinate_ = coordinates[0];
+    }
+    this.geometryFunction_(this.sketchCoords_, geometry);
+  }
+  this.updateSketchFeatures_();
+  if (done) {
+    this.finishDrawing();
+  }
+};
+
+
+/**
+ * Remove last point of the feature currently being drawn.
+ * @api
+ */
+ol.interaction.Draw.prototype.removeLastPoint = function() {
+  var geometry = this.sketchFeature_.getGeometry();
+  goog.asserts.assertInstanceof(geometry, ol.geom.SimpleGeometry,
+      'geometry must be an ol.geom.SimpleGeometry');
+  var coordinates, sketchLineGeom;
+  if (this.mode_ === ol.interaction.DrawMode.LINE_STRING) {
+    coordinates = this.sketchCoords_;
+    coordinates.splice(-2, 1);
+    this.geometryFunction_(coordinates, geometry);
+  } else if (this.mode_ === ol.interaction.DrawMode.POLYGON) {
+    coordinates = this.sketchCoords_[0];
+    coordinates.splice(-2, 1);
+    sketchLineGeom = this.sketchLine_.getGeometry();
+    goog.asserts.assertInstanceof(sketchLineGeom, ol.geom.LineString,
+        'sketchLineGeom must be an ol.geom.LineString');
+    sketchLineGeom.setCoordinates(coordinates);
+    this.geometryFunction_(this.sketchCoords_, geometry);
+  }
+
+  if (coordinates.length === 0) {
+    this.finishCoordinate_ = null;
+  }
+
+  this.updateSketchFeatures_();
+};
+
+
+/**
+ * Stop drawing and add the sketch feature to the target layer.
+ * The {@link ol.interaction.DrawEventType.DRAWEND} event is dispatched before
+ * inserting the feature.
+ * @api
+ */
+ol.interaction.Draw.prototype.finishDrawing = function() {
+  var sketchFeature = this.abortDrawing_();
+  goog.asserts.assert(sketchFeature, 'sketchFeature expected to be truthy');
+  var coordinates = this.sketchCoords_;
+  var geometry = sketchFeature.getGeometry();
+  goog.asserts.assertInstanceof(geometry, ol.geom.SimpleGeometry,
+      'geometry must be an ol.geom.SimpleGeometry');
+  if (this.mode_ === ol.interaction.DrawMode.LINE_STRING) {
+    // remove the redundant last point
+    coordinates.pop();
+    this.geometryFunction_(coordinates, geometry);
+  } else if (this.mode_ === ol.interaction.DrawMode.POLYGON) {
+    // When we finish drawing a polygon on the last point,
+    // the last coordinate is duplicated as for LineString
+    // we force the replacement by the first point
+    coordinates[0].pop();
+    coordinates[0].push(coordinates[0][0]);
+    this.geometryFunction_(coordinates, geometry);
+  }
+
+  // cast multi-part geometries
+  if (this.type_ === ol.geom.GeometryType.MULTI_POINT) {
+    sketchFeature.setGeometry(new ol.geom.MultiPoint([coordinates]));
+  } else if (this.type_ === ol.geom.GeometryType.MULTI_LINE_STRING) {
+    sketchFeature.setGeometry(new ol.geom.MultiLineString([coordinates]));
+  } else if (this.type_ === ol.geom.GeometryType.MULTI_POLYGON) {
+    sketchFeature.setGeometry(new ol.geom.MultiPolygon([coordinates]));
+  }
+
+  // First dispatch event to allow full set up of feature
+  this.dispatchEvent(new ol.interaction.DrawEvent(
+      ol.interaction.DrawEventType.DRAWEND, sketchFeature));
+
+  // Then insert feature
+  if (this.features_) {
+    this.features_.push(sketchFeature);
+  }
+  if (this.source_) {
+    this.source_.addFeature(sketchFeature);
+  }
+};
+
+
+/**
+ * Stop drawing without adding the sketch feature to the target layer.
+ * @return {ol.Feature} The sketch feature (or null if none).
+ * @private
+ */
+ol.interaction.Draw.prototype.abortDrawing_ = function() {
+  this.finishCoordinate_ = null;
+  var sketchFeature = this.sketchFeature_;
+  if (sketchFeature) {
+    this.sketchFeature_ = null;
+    this.sketchPoint_ = null;
+    this.sketchLine_ = null;
+    this.overlay_.getSource().clear(true);
+  }
+  return sketchFeature;
+};
+
+
+/**
+ * Extend an existing geometry by adding additional points. This only works
+ * on features with `LineString` geometries, where the interaction will
+ * extend lines by adding points to the end of the coordinates array.
+ * @param {!ol.Feature} feature Feature to be extended.
+ * @api
+ */
+ol.interaction.Draw.prototype.extend = function(feature) {
+  var geometry = feature.getGeometry();
+  goog.asserts.assert(this.mode_ == ol.interaction.DrawMode.LINE_STRING,
+      'interaction mode must be "line"');
+  goog.asserts.assert(geometry, 'feature must have a geometry');
+  goog.asserts.assert(geometry.getType() == ol.geom.GeometryType.LINE_STRING,
+      'feature geometry must be a line string');
+  var lineString = /** @type {ol.geom.LineString} */ (geometry);
+  this.sketchFeature_ = feature;
+  this.sketchCoords_ = lineString.getCoordinates();
+  var last = this.sketchCoords_[this.sketchCoords_.length - 1];
+  this.finishCoordinate_ = last.slice();
+  this.sketchCoords_.push(last.slice());
+  this.updateSketchFeatures_();
+  this.dispatchEvent(new ol.interaction.DrawEvent(
+      ol.interaction.DrawEventType.DRAWSTART, this.sketchFeature_));
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.interaction.Draw.prototype.shouldStopEvent = ol.functions.FALSE;
+
+
+/**
+ * Redraw the sketch features.
+ * @private
+ */
+ol.interaction.Draw.prototype.updateSketchFeatures_ = function() {
+  var sketchFeatures = [];
+  if (this.sketchFeature_) {
+    sketchFeatures.push(this.sketchFeature_);
+  }
+  if (this.sketchLine_) {
+    sketchFeatures.push(this.sketchLine_);
+  }
+  if (this.sketchPoint_) {
+    sketchFeatures.push(this.sketchPoint_);
+  }
+  var overlaySource = this.overlay_.getSource();
+  overlaySource.clear(true);
+  overlaySource.addFeatures(sketchFeatures);
+};
+
+
+/**
+ * @private
+ */
+ol.interaction.Draw.prototype.updateState_ = function() {
+  var map = this.getMap();
+  var active = this.getActive();
+  if (!map || !active) {
+    this.abortDrawing_();
+  }
+  this.overlay_.setMap(active ? map : null);
+};
+
+
+/**
+ * Create a `geometryFunction` for `mode: 'Circle'` that will create a regular
+ * polygon with a user specified number of sides and start angle instead of an
+ * `ol.geom.Circle` geometry.
+ * @param {number=} opt_sides Number of sides of the regular polygon. Default is
+ *     32.
+ * @param {number=} opt_angle Angle of the first point in radians. 0 means East.
+ *     Default is the angle defined by the heading from the center of the
+ *     regular polygon to the current pointer position.
+ * @return {ol.DrawGeometryFunctionType} Function that draws a
+ *     polygon.
+ * @api
+ */
+ol.interaction.Draw.createRegularPolygon = function(opt_sides, opt_angle) {
+  return (
+      /**
+       * @param {ol.Coordinate|Array.<ol.Coordinate>|Array.<Array.<ol.Coordinate>>} coordinates
+       * @param {ol.geom.SimpleGeometry=} opt_geometry
+       * @return {ol.geom.SimpleGeometry}
+       */
+      function(coordinates, opt_geometry) {
+        var center = coordinates[0];
+        var end = coordinates[1];
+        var radius = Math.sqrt(
+            ol.coordinate.squaredDistance(center, end));
+        var geometry = opt_geometry ? opt_geometry :
+            ol.geom.Polygon.fromCircle(new ol.geom.Circle(center), opt_sides);
+        goog.asserts.assertInstanceof(geometry, ol.geom.Polygon,
+            'geometry must be a polygon');
+        var angle = opt_angle ? opt_angle :
+            Math.atan((end[1] - center[1]) / (end[0] - center[0]));
+        ol.geom.Polygon.makeRegular(geometry, center, radius, angle);
+        return geometry;
+      }
+  );
+};
+
+
+/**
+ * Get the drawing mode.  The mode for mult-part geometries is the same as for
+ * their single-part cousins.
+ * @param {ol.geom.GeometryType} type Geometry type.
+ * @return {ol.interaction.DrawMode} Drawing mode.
+ * @private
+ */
+ol.interaction.Draw.getMode_ = function(type) {
+  var mode;
+  if (type === ol.geom.GeometryType.POINT ||
+      type === ol.geom.GeometryType.MULTI_POINT) {
+    mode = ol.interaction.DrawMode.POINT;
+  } else if (type === ol.geom.GeometryType.LINE_STRING ||
+      type === ol.geom.GeometryType.MULTI_LINE_STRING) {
+    mode = ol.interaction.DrawMode.LINE_STRING;
+  } else if (type === ol.geom.GeometryType.POLYGON ||
+      type === ol.geom.GeometryType.MULTI_POLYGON) {
+    mode = ol.interaction.DrawMode.POLYGON;
+  } else if (type === ol.geom.GeometryType.CIRCLE) {
+    mode = ol.interaction.DrawMode.CIRCLE;
+  }
+  goog.asserts.assert(mode !== undefined, 'mode should be defined');
+  return mode;
+};
+
+
+/**
+ * Draw mode.  This collapses multi-part geometry types with their single-part
+ * cousins.
+ * @enum {string}
+ */
+ol.interaction.DrawMode = {
+  POINT: 'Point',
+  LINE_STRING: 'LineString',
+  POLYGON: 'Polygon',
+  CIRCLE: 'Circle'
+};
+
+goog.provide('ol.interaction.Modify');
+goog.provide('ol.interaction.ModifyEvent');
+
+goog.require('goog.asserts');
+goog.require('ol.events');
+goog.require('ol.events.Event');
+goog.require('ol.events.EventType');
+goog.require('ol');
+goog.require('ol.Collection');
+goog.require('ol.CollectionEventType');
+goog.require('ol.Feature');
+goog.require('ol.MapBrowserEvent.EventType');
+goog.require('ol.MapBrowserPointerEvent');
+goog.require('ol.ViewHint');
+goog.require('ol.array');
+goog.require('ol.coordinate');
+goog.require('ol.events.condition');
+goog.require('ol.extent');
+goog.require('ol.geom.GeometryType');
+goog.require('ol.geom.LineString');
+goog.require('ol.geom.MultiLineString');
+goog.require('ol.geom.MultiPoint');
+goog.require('ol.geom.MultiPolygon');
+goog.require('ol.geom.Point');
+goog.require('ol.geom.Polygon');
+goog.require('ol.interaction.Pointer');
+goog.require('ol.layer.Vector');
+goog.require('ol.source.Vector');
+goog.require('ol.structs.RBush');
+
+
+/**
+ * @enum {string}
+ */
+ol.ModifyEventType = {
+  /**
+   * Triggered upon feature modification start
+   * @event ol.interaction.ModifyEvent#modifystart
+   * @api
+   */
+  MODIFYSTART: 'modifystart',
+  /**
+   * Triggered upon feature modification end
+   * @event ol.interaction.ModifyEvent#modifyend
+   * @api
+   */
+  MODIFYEND: 'modifyend'
+};
+
+
+/**
+ * @classdesc
+ * Events emitted by {@link ol.interaction.Modify} instances are instances of
+ * this type.
+ *
+ * @constructor
+ * @extends {ol.events.Event}
+ * @implements {oli.ModifyEvent}
+ * @param {ol.ModifyEventType} type Type.
+ * @param {ol.Collection.<ol.Feature>} features The features modified.
+ * @param {ol.MapBrowserPointerEvent} mapBrowserPointerEvent Associated
+ *     {@link ol.MapBrowserPointerEvent}.
+ */
+ol.interaction.ModifyEvent = function(type, features, mapBrowserPointerEvent) {
+
+  ol.events.Event.call(this, type);
+
+  /**
+   * The features being modified.
+   * @type {ol.Collection.<ol.Feature>}
+   * @api
+   */
+  this.features = features;
+
+  /**
+   * Associated {@link ol.MapBrowserEvent}.
+   * @type {ol.MapBrowserEvent}
+   * @api
+   */
+  this.mapBrowserEvent = mapBrowserPointerEvent;
+};
+ol.inherits(ol.interaction.ModifyEvent, ol.events.Event);
+
+
+/**
+ * @classdesc
+ * Interaction for modifying feature geometries.
+ *
+ * @constructor
+ * @extends {ol.interaction.Pointer}
+ * @param {olx.interaction.ModifyOptions} options Options.
+ * @fires ol.interaction.ModifyEvent
+ * @api
+ */
+ol.interaction.Modify = function(options) {
+
+  ol.interaction.Pointer.call(this, {
+    handleDownEvent: ol.interaction.Modify.handleDownEvent_,
+    handleDragEvent: ol.interaction.Modify.handleDragEvent_,
+    handleEvent: ol.interaction.Modify.handleEvent,
+    handleUpEvent: ol.interaction.Modify.handleUpEvent_
+  });
+
+  /**
+   * @private
+   * @type {ol.EventsConditionType}
+   */
+  this.condition_ = options.condition ?
+      options.condition : ol.events.condition.primaryAction;
+
+
+  /**
+   * @private
+   * @param {ol.MapBrowserEvent} mapBrowserEvent Browser event.
+   * @return {boolean} Combined condition result.
+   */
+  this.defaultDeleteCondition_ = function(mapBrowserEvent) {
+    return ol.events.condition.noModifierKeys(mapBrowserEvent) &&
+      ol.events.condition.singleClick(mapBrowserEvent);
+  };
+
+  /**
+   * @type {ol.EventsConditionType}
+   * @private
+   */
+  this.deleteCondition_ = options.deleteCondition ?
+      options.deleteCondition : this.defaultDeleteCondition_;
+
+  /**
+   * Editing vertex.
+   * @type {ol.Feature}
+   * @private
+   */
+  this.vertexFeature_ = null;
+
+  /**
+   * Segments intersecting {@link this.vertexFeature_} by segment uid.
+   * @type {Object.<string, boolean>}
+   * @private
+   */
+  this.vertexSegments_ = null;
+
+  /**
+   * @type {ol.Pixel}
+   * @private
+   */
+  this.lastPixel_ = [0, 0];
+
+  /**
+   * Tracks if the next `singleclick` event should be ignored to prevent
+   * accidental deletion right after vertex creation.
+   * @type {boolean}
+   * @private
+   */
+  this.ignoreNextSingleClick_ = false;
+
+  /**
+   * @type {boolean}
+   * @private
+   */
+  this.modified_ = false;
+
+  /**
+   * Segment RTree for each layer
+   * @type {ol.structs.RBush.<ol.ModifySegmentDataType>}
+   * @private
+   */
+  this.rBush_ = new ol.structs.RBush();
+
+  /**
+   * @type {number}
+   * @private
+   */
+  this.pixelTolerance_ = options.pixelTolerance !== undefined ?
+      options.pixelTolerance : 10;
+
+  /**
+   * @type {boolean}
+   * @private
+   */
+  this.snappedToVertex_ = false;
+
+  /**
+   * Indicate whether the interaction is currently changing a feature's
+   * coordinates.
+   * @type {boolean}
+   * @private
+   */
+  this.changingFeature_ = false;
+
+  /**
+   * @type {Array}
+   * @private
+   */
+  this.dragSegments_ = [];
+
+  /**
+   * Draw overlay where sketch features are drawn.
+   * @type {ol.layer.Vector}
+   * @private
+   */
+  this.overlay_ = new ol.layer.Vector({
+    source: new ol.source.Vector({
+      useSpatialIndex: false,
+      wrapX: !!options.wrapX
+    }),
+    style: options.style ? options.style :
+        ol.interaction.Modify.getDefaultStyleFunction(),
+    updateWhileAnimating: true,
+    updateWhileInteracting: true
+  });
+
+  /**
+  * @const
+  * @private
+  * @type {Object.<string, function(ol.Feature, ol.geom.Geometry)>}
+  */
+  this.SEGMENT_WRITERS_ = {
+    'Point': this.writePointGeometry_,
+    'LineString': this.writeLineStringGeometry_,
+    'LinearRing': this.writeLineStringGeometry_,
+    'Polygon': this.writePolygonGeometry_,
+    'MultiPoint': this.writeMultiPointGeometry_,
+    'MultiLineString': this.writeMultiLineStringGeometry_,
+    'MultiPolygon': this.writeMultiPolygonGeometry_,
+    'GeometryCollection': this.writeGeometryCollectionGeometry_
+  };
+
+  /**
+   * @type {ol.Collection.<ol.Feature>}
+   * @private
+   */
+  this.features_ = options.features;
+
+  this.features_.forEach(this.addFeature_, this);
+  ol.events.listen(this.features_, ol.CollectionEventType.ADD,
+      this.handleFeatureAdd_, this);
+  ol.events.listen(this.features_, ol.CollectionEventType.REMOVE,
+      this.handleFeatureRemove_, this);
+
+  /**
+   * @type {ol.MapBrowserPointerEvent}
+   * @private
+   */
+  this.lastPointerEvent_ = null;
+
+};
+ol.inherits(ol.interaction.Modify, ol.interaction.Pointer);
+
+
+/**
+ * @param {ol.Feature} feature Feature.
+ * @private
+ */
+ol.interaction.Modify.prototype.addFeature_ = function(feature) {
+  var geometry = feature.getGeometry();
+  if (geometry.getType() in this.SEGMENT_WRITERS_) {
+    this.SEGMENT_WRITERS_[geometry.getType()].call(this, feature, geometry);
+  }
+  var map = this.getMap();
+  if (map) {
+    this.handlePointerAtPixel_(this.lastPixel_, map);
+  }
+  ol.events.listen(feature, ol.events.EventType.CHANGE,
+      this.handleFeatureChange_, this);
+};
+
+
+/**
+ * @param {ol.MapBrowserPointerEvent} evt Map browser event
+ * @private
+ */
+ol.interaction.Modify.prototype.willModifyFeatures_ = function(evt) {
+  if (!this.modified_) {
+    this.modified_ = true;
+    this.dispatchEvent(new ol.interaction.ModifyEvent(
+        ol.ModifyEventType.MODIFYSTART, this.features_, evt));
+  }
+};
+
+
+/**
+ * @param {ol.Feature} feature Feature.
+ * @private
+ */
+ol.interaction.Modify.prototype.removeFeature_ = function(feature) {
+  this.removeFeatureSegmentData_(feature);
+  // Remove the vertex feature if the collection of canditate features
+  // is empty.
+  if (this.vertexFeature_ && this.features_.getLength() === 0) {
+    this.overlay_.getSource().removeFeature(this.vertexFeature_);
+    this.vertexFeature_ = null;
+  }
+  ol.events.unlisten(feature, ol.events.EventType.CHANGE,
+      this.handleFeatureChange_, this);
+};
+
+
+/**
+ * @param {ol.Feature} feature Feature.
+ * @private
+ */
+ol.interaction.Modify.prototype.removeFeatureSegmentData_ = function(feature) {
+  var rBush = this.rBush_;
+  var /** @type {Array.<ol.ModifySegmentDataType>} */ nodesToRemove = [];
+  rBush.forEach(
+      /**
+       * @param {ol.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
+ */
+ol.interaction.Modify.prototype.setMap = function(map) {
+  this.overlay_.setMap(map);
+  ol.interaction.Pointer.prototype.setMap.call(this, map);
+};
+
+
+/**
+ * @param {ol.CollectionEvent} evt Event.
+ * @private
+ */
+ol.interaction.Modify.prototype.handleFeatureAdd_ = function(evt) {
+  var feature = evt.element;
+  goog.asserts.assertInstanceof(feature, ol.Feature,
+      'feature should be an ol.Feature');
+  this.addFeature_(feature);
+};
+
+
+/**
+ * @param {ol.events.Event} evt Event.
+ * @private
+ */
+ol.interaction.Modify.prototype.handleFeatureChange_ = function(evt) {
+  if (!this.changingFeature_) {
+    var feature = /** @type {ol.Feature} */ (evt.target);
+    this.removeFeature_(feature);
+    this.addFeature_(feature);
+  }
+};
+
+
+/**
+ * @param {ol.CollectionEvent} evt Event.
+ * @private
+ */
+ol.interaction.Modify.prototype.handleFeatureRemove_ = function(evt) {
+  var feature = /** @type {ol.Feature} */ (evt.element);
+  this.removeFeature_(feature);
+};
+
+
+/**
+ * @param {ol.Feature} feature Feature
+ * @param {ol.geom.Point} geometry Geometry.
+ * @private
+ */
+ol.interaction.Modify.prototype.writePointGeometry_ = function(feature, geometry) {
+  var coordinates = geometry.getCoordinates();
+  var segmentData = /** @type {ol.ModifySegmentDataType} */ ({
+    feature: feature,
+    geometry: geometry,
+    segment: [coordinates, coordinates]
+  });
+  this.rBush_.insert(geometry.getExtent(), segmentData);
+};
+
+
+/**
+ * @param {ol.Feature} feature Feature
+ * @param {ol.geom.MultiPoint} geometry Geometry.
+ * @private
+ */
+ol.interaction.Modify.prototype.writeMultiPointGeometry_ = function(feature, geometry) {
+  var points = geometry.getCoordinates();
+  var coordinates, i, ii, segmentData;
+  for (i = 0, ii = points.length; i < ii; ++i) {
+    coordinates = points[i];
+    segmentData = /** @type {ol.ModifySegmentDataType} */ ({
+      feature: feature,
+      geometry: geometry,
+      depth: [i],
+      index: i,
+      segment: [coordinates, coordinates]
+    });
+    this.rBush_.insert(geometry.getExtent(), segmentData);
+  }
+};
+
+
+/**
+ * @param {ol.Feature} feature Feature
+ * @param {ol.geom.LineString} geometry Geometry.
+ * @private
+ */
+ol.interaction.Modify.prototype.writeLineStringGeometry_ = function(feature, geometry) {
+  var coordinates = geometry.getCoordinates();
+  var i, ii, segment, segmentData;
+  for (i = 0, ii = coordinates.length - 1; i < ii; ++i) {
+    segment = coordinates.slice(i, i + 2);
+    segmentData = /** @type {ol.ModifySegmentDataType} */ ({
+      feature: feature,
+      geometry: geometry,
+      index: i,
+      segment: segment
+    });
+    this.rBush_.insert(ol.extent.boundingExtent(segment), segmentData);
+  }
+};
+
+
+/**
+ * @param {ol.Feature} feature Feature
+ * @param {ol.geom.MultiLineString} geometry Geometry.
+ * @private
+ */
+ol.interaction.Modify.prototype.writeMultiLineStringGeometry_ = function(feature, geometry) {
+  var lines = geometry.getCoordinates();
+  var coordinates, i, ii, j, jj, segment, segmentData;
+  for (j = 0, jj = lines.length; j < jj; ++j) {
+    coordinates = lines[j];
+    for (i = 0, ii = coordinates.length - 1; i < ii; ++i) {
+      segment = coordinates.slice(i, i + 2);
+      segmentData = /** @type {ol.ModifySegmentDataType} */ ({
+        feature: feature,
+        geometry: geometry,
+        depth: [j],
+        index: i,
+        segment: segment
+      });
+      this.rBush_.insert(ol.extent.boundingExtent(segment), segmentData);
+    }
+  }
+};
+
+
+/**
+ * @param {ol.Feature} feature Feature
+ * @param {ol.geom.Polygon} geometry Geometry.
+ * @private
+ */
+ol.interaction.Modify.prototype.writePolygonGeometry_ = function(feature, geometry) {
+  var rings = geometry.getCoordinates();
+  var coordinates, i, ii, j, jj, segment, segmentData;
+  for (j = 0, jj = rings.length; j < jj; ++j) {
+    coordinates = rings[j];
+    for (i = 0, ii = coordinates.length - 1; i < ii; ++i) {
+      segment = coordinates.slice(i, i + 2);
+      segmentData = /** @type {ol.ModifySegmentDataType} */ ({
+        feature: feature,
+        geometry: geometry,
+        depth: [j],
+        index: i,
+        segment: segment
+      });
+      this.rBush_.insert(ol.extent.boundingExtent(segment), segmentData);
+    }
+  }
+};
+
+
+/**
+ * @param {ol.Feature} feature Feature
+ * @param {ol.geom.MultiPolygon} geometry Geometry.
+ * @private
+ */
+ol.interaction.Modify.prototype.writeMultiPolygonGeometry_ = function(feature, geometry) {
+  var polygons = geometry.getCoordinates();
+  var coordinates, i, ii, j, jj, k, kk, rings, segment, segmentData;
+  for (k = 0, kk = polygons.length; k < kk; ++k) {
+    rings = polygons[k];
+    for (j = 0, jj = rings.length; j < jj; ++j) {
+      coordinates = rings[j];
+      for (i = 0, ii = coordinates.length - 1; i < ii; ++i) {
+        segment = coordinates.slice(i, i + 2);
+        segmentData = /** @type {ol.ModifySegmentDataType} */ ({
+          feature: feature,
+          geometry: geometry,
+          depth: [j, k],
+          index: i,
+          segment: segment
+        });
+        this.rBush_.insert(ol.extent.boundingExtent(segment), segmentData);
+      }
+    }
+  }
+};
+
+
+/**
+ * @param {ol.Feature} feature Feature
+ * @param {ol.geom.GeometryCollection} geometry Geometry.
+ * @private
+ */
+ol.interaction.Modify.prototype.writeGeometryCollectionGeometry_ = function(feature, geometry) {
+  var i, geometries = geometry.getGeometriesArray();
+  for (i = 0; i < geometries.length; ++i) {
+    this.SEGMENT_WRITERS_[geometries[i].getType()].call(
+        this, feature, geometries[i]);
+  }
+};
+
+
+/**
+ * @param {ol.Coordinate} coordinates Coordinates.
+ * @return {ol.Feature} Vertex feature.
+ * @private
+ */
+ol.interaction.Modify.prototype.createOrUpdateVertexFeature_ = function(coordinates) {
+  var vertexFeature = this.vertexFeature_;
+  if (!vertexFeature) {
+    vertexFeature = new ol.Feature(new ol.geom.Point(coordinates));
+    this.vertexFeature_ = vertexFeature;
+    this.overlay_.getSource().addFeature(vertexFeature);
+  } else {
+    var geometry = /** @type {ol.geom.Point} */ (vertexFeature.getGeometry());
+    geometry.setCoordinates(coordinates);
+  }
+  return vertexFeature;
+};
+
+
+/**
+ * @param {ol.ModifySegmentDataType} a The first segment data.
+ * @param {ol.ModifySegmentDataType} b The second segment data.
+ * @return {number} The difference in indexes.
+ * @private
+ */
+ol.interaction.Modify.compareIndexes_ = function(a, b) {
+  return a.index - b.index;
+};
+
+
+/**
+ * @param {ol.MapBrowserPointerEvent} evt Event.
+ * @return {boolean} Start drag sequence?
+ * @this {ol.interaction.Modify}
+ * @private
+ */
+ol.interaction.Modify.handleDownEvent_ = function(evt) {
+  if (!this.condition_(evt)) {
+    return false;
+  }
+  this.handlePointerAtPixel_(evt.pixel, evt.map);
+  this.dragSegments_.length = 0;
+  this.modified_ = false;
+  var vertexFeature = this.vertexFeature_;
+  if (vertexFeature) {
+    var insertVertices = [];
+    var geometry = /** @type {ol.geom.Point} */ (vertexFeature.getGeometry());
+    var vertex = geometry.getCoordinates();
+    var vertexExtent = ol.extent.boundingExtent([vertex]);
+    var segmentDataMatches = this.rBush_.getInExtent(vertexExtent);
+    var componentSegments = {};
+    segmentDataMatches.sort(ol.interaction.Modify.compareIndexes_);
+    for (var i = 0, ii = segmentDataMatches.length; i < ii; ++i) {
+      var segmentDataMatch = segmentDataMatches[i];
+      var segment = segmentDataMatch.segment;
+      var uid = goog.getUid(segmentDataMatch.feature);
+      var depth = segmentDataMatch.depth;
+      if (depth) {
+        uid += '-' + depth.join('-'); // separate feature components
+      }
+      if (!componentSegments[uid]) {
+        componentSegments[uid] = new Array(2);
+      }
+      if (ol.coordinate.equals(segment[0], vertex) &&
+          !componentSegments[uid][0]) {
+        this.dragSegments_.push([segmentDataMatch, 0]);
+        componentSegments[uid][0] = segmentDataMatch;
+      } else if (ol.coordinate.equals(segment[1], vertex) &&
+          !componentSegments[uid][1]) {
+
+        // prevent dragging closed linestrings by the connecting node
+        if ((segmentDataMatch.geometry.getType() ===
+            ol.geom.GeometryType.LINE_STRING ||
+            segmentDataMatch.geometry.getType() ===
+            ol.geom.GeometryType.MULTI_LINE_STRING) &&
+            componentSegments[uid][0] &&
+            componentSegments[uid][0].index === 0) {
+          continue;
+        }
+
+        this.dragSegments_.push([segmentDataMatch, 1]);
+        componentSegments[uid][1] = segmentDataMatch;
+      } else if (goog.getUid(segment) in this.vertexSegments_ &&
+          (!componentSegments[uid][0] && !componentSegments[uid][1])) {
+        insertVertices.push([segmentDataMatch, vertex]);
+      }
+    }
+    if (insertVertices.length) {
+      this.willModifyFeatures_(evt);
+    }
+    for (var j = insertVertices.length - 1; j >= 0; --j) {
+      this.insertVertex_.apply(this, insertVertices[j]);
+    }
+  }
+  return !!this.vertexFeature_;
+};
+
+
+/**
+ * @param {ol.MapBrowserPointerEvent} evt Event.
+ * @this {ol.interaction.Modify}
+ * @private
+ */
+ol.interaction.Modify.handleDragEvent_ = function(evt) {
+  this.ignoreNextSingleClick_ = false;
+  this.willModifyFeatures_(evt);
+
+  var vertex = evt.coordinate;
+  for (var i = 0, ii = this.dragSegments_.length; i < ii; ++i) {
+    var dragSegment = this.dragSegments_[i];
+    var segmentData = dragSegment[0];
+    var depth = segmentData.depth;
+    var geometry = segmentData.geometry;
+    var coordinates = geometry.getCoordinates();
+    var segment = segmentData.segment;
+    var index = dragSegment[1];
+
+    while (vertex.length < geometry.getStride()) {
+      vertex.push(0);
+    }
+
+    switch (geometry.getType()) {
+      case ol.geom.GeometryType.POINT:
+        coordinates = vertex;
+        segment[0] = segment[1] = vertex;
+        break;
+      case ol.geom.GeometryType.MULTI_POINT:
+        coordinates[segmentData.index] = vertex;
+        segment[0] = segment[1] = vertex;
+        break;
+      case ol.geom.GeometryType.LINE_STRING:
+        coordinates[segmentData.index + index] = vertex;
+        segment[index] = vertex;
+        break;
+      case ol.geom.GeometryType.MULTI_LINE_STRING:
+        coordinates[depth[0]][segmentData.index + index] = vertex;
+        segment[index] = vertex;
+        break;
+      case ol.geom.GeometryType.POLYGON:
+        coordinates[depth[0]][segmentData.index + index] = vertex;
+        segment[index] = vertex;
+        break;
+      case ol.geom.GeometryType.MULTI_POLYGON:
+        coordinates[depth[1]][depth[0]][segmentData.index + index] = vertex;
+        segment[index] = vertex;
+        break;
+      default:
+        // pass
+    }
+
+    this.setGeometryCoordinates_(geometry, coordinates);
+  }
+  this.createOrUpdateVertexFeature_(vertex);
+};
+
+
+/**
+ * @param {ol.MapBrowserPointerEvent} evt Event.
+ * @return {boolean} Stop drag sequence?
+ * @this {ol.interaction.Modify}
+ * @private
+ */
+ol.interaction.Modify.handleUpEvent_ = function(evt) {
+  var segmentData;
+  for (var i = this.dragSegments_.length - 1; i >= 0; --i) {
+    segmentData = this.dragSegments_[i][0];
+    this.rBush_.update(ol.extent.boundingExtent(segmentData.segment),
+        segmentData);
+  }
+  if (this.modified_) {
+    this.dispatchEvent(new ol.interaction.ModifyEvent(
+        ol.ModifyEventType.MODIFYEND, this.features_, evt));
+    this.modified_ = false;
+  }
+  return false;
+};
+
+
+/**
+ * Handles the {@link ol.MapBrowserEvent map browser event} and may modify the
+ * geometry.
+ * @param {ol.MapBrowserEvent} mapBrowserEvent Map browser event.
+ * @return {boolean} `false` to stop event propagation.
+ * @this {ol.interaction.Modify}
+ * @api
+ */
+ol.interaction.Modify.handleEvent = function(mapBrowserEvent) {
+  if (!(mapBrowserEvent instanceof ol.MapBrowserPointerEvent)) {
+    return true;
+  }
+  this.lastPointerEvent_ = mapBrowserEvent;
+
+  var handled;
+  if (!mapBrowserEvent.map.getView().getHints()[ol.ViewHint.INTERACTING] &&
+      mapBrowserEvent.type == ol.MapBrowserEvent.EventType.POINTERMOVE &&
+      !this.handlingDownUpSequence) {
+    this.handlePointerMove_(mapBrowserEvent);
+  }
+  if (this.vertexFeature_ && this.deleteCondition_(mapBrowserEvent)) {
+    if (mapBrowserEvent.type != ol.MapBrowserEvent.EventType.SINGLECLICK ||
+        !this.ignoreNextSingleClick_) {
+      var geometry = this.vertexFeature_.getGeometry();
+      goog.asserts.assertInstanceof(geometry, ol.geom.Point,
+          'geometry should be an ol.geom.Point');
+      handled = this.removePoint();
+    } else {
+      handled = true;
+    }
+  }
+
+  if (mapBrowserEvent.type == ol.MapBrowserEvent.EventType.SINGLECLICK) {
+    this.ignoreNextSingleClick_ = false;
+  }
+
+  return ol.interaction.Pointer.handleEvent.call(this, mapBrowserEvent) &&
+      !handled;
+};
+
+
+/**
+ * @param {ol.MapBrowserEvent} evt Event.
+ * @private
+ */
+ol.interaction.Modify.prototype.handlePointerMove_ = function(evt) {
+  this.lastPixel_ = evt.pixel;
+  this.handlePointerAtPixel_(evt.pixel, evt.map);
+};
+
+
+/**
+ * @param {ol.Pixel} pixel Pixel
+ * @param {ol.Map} map Map.
+ * @private
+ */
+ol.interaction.Modify.prototype.handlePointerAtPixel_ = function(pixel, map) {
+  var pixelCoordinate = map.getCoordinateFromPixel(pixel);
+  var sortByDistance = function(a, b) {
+    return ol.coordinate.squaredDistanceToSegment(pixelCoordinate, a.segment) -
+        ol.coordinate.squaredDistanceToSegment(pixelCoordinate, b.segment);
+  };
+
+  var lowerLeft = map.getCoordinateFromPixel(
+      [pixel[0] - this.pixelTolerance_, pixel[1] + this.pixelTolerance_]);
+  var upperRight = map.getCoordinateFromPixel(
+      [pixel[0] + this.pixelTolerance_, pixel[1] - this.pixelTolerance_]);
+  var box = ol.extent.boundingExtent([lowerLeft, upperRight]);
+
+  var rBush = this.rBush_;
+  var nodes = rBush.getInExtent(box);
+  if (nodes.length > 0) {
+    nodes.sort(sortByDistance);
+    var node = nodes[0];
+    var closestSegment = node.segment;
+    var vertex = (ol.coordinate.closestOnSegment(pixelCoordinate,
+        closestSegment));
+    var vertexPixel = map.getPixelFromCoordinate(vertex);
+    if (Math.sqrt(ol.coordinate.squaredDistance(pixel, vertexPixel)) <=
+        this.pixelTolerance_) {
+      var pixel1 = map.getPixelFromCoordinate(closestSegment[0]);
+      var pixel2 = map.getPixelFromCoordinate(closestSegment[1]);
+      var squaredDist1 = ol.coordinate.squaredDistance(vertexPixel, pixel1);
+      var squaredDist2 = ol.coordinate.squaredDistance(vertexPixel, pixel2);
+      var dist = Math.sqrt(Math.min(squaredDist1, squaredDist2));
+      this.snappedToVertex_ = dist <= this.pixelTolerance_;
+      if (this.snappedToVertex_) {
+        vertex = squaredDist1 > squaredDist2 ?
+            closestSegment[1] : closestSegment[0];
+      }
+      this.createOrUpdateVertexFeature_(vertex);
+      var vertexSegments = {};
+      vertexSegments[goog.getUid(closestSegment)] = true;
+      var segment;
+      for (var i = 1, ii = nodes.length; i < ii; ++i) {
+        segment = nodes[i].segment;
+        if ((ol.coordinate.equals(closestSegment[0], segment[0]) &&
+            ol.coordinate.equals(closestSegment[1], segment[1]) ||
+            (ol.coordinate.equals(closestSegment[0], segment[1]) &&
+            ol.coordinate.equals(closestSegment[1], segment[0])))) {
+          vertexSegments[goog.getUid(segment)] = true;
+        } else {
+          break;
+        }
+      }
+      this.vertexSegments_ = vertexSegments;
+      return;
+    }
+  }
+  if (this.vertexFeature_) {
+    this.overlay_.getSource().removeFeature(this.vertexFeature_);
+    this.vertexFeature_ = null;
+  }
+};
+
+
+/**
+ * @param {ol.ModifySegmentDataType} segmentData Segment data.
+ * @param {ol.Coordinate} vertex Vertex.
+ * @private
+ */
+ol.interaction.Modify.prototype.insertVertex_ = function(segmentData, vertex) {
+  var segment = segmentData.segment;
+  var feature = segmentData.feature;
+  var geometry = segmentData.geometry;
+  var depth = segmentData.depth;
+  var index = segmentData.index;
+  var coordinates;
+
+  while (vertex.length < geometry.getStride()) {
+    vertex.push(0);
+  }
+
+  switch (geometry.getType()) {
+    case ol.geom.GeometryType.MULTI_LINE_STRING:
+      goog.asserts.assertInstanceof(geometry, ol.geom.MultiLineString,
+          'geometry should be an ol.geom.MultiLineString');
+      coordinates = geometry.getCoordinates();
+      coordinates[depth[0]].splice(index + 1, 0, vertex);
+      break;
+    case ol.geom.GeometryType.POLYGON:
+      goog.asserts.assertInstanceof(geometry, ol.geom.Polygon,
+          'geometry should be an ol.geom.Polygon');
+      coordinates = geometry.getCoordinates();
+      coordinates[depth[0]].splice(index + 1, 0, vertex);
+      break;
+    case ol.geom.GeometryType.MULTI_POLYGON:
+      goog.asserts.assertInstanceof(geometry, ol.geom.MultiPolygon,
+          'geometry should be an ol.geom.MultiPolygon');
+      coordinates = geometry.getCoordinates();
+      coordinates[depth[1]][depth[0]].splice(index + 1, 0, vertex);
+      break;
+    case ol.geom.GeometryType.LINE_STRING:
+      goog.asserts.assertInstanceof(geometry, ol.geom.LineString,
+          'geometry should be an ol.geom.LineString');
+      coordinates = geometry.getCoordinates();
+      coordinates.splice(index + 1, 0, vertex);
+      break;
+    default:
+      return;
+  }
+
+  this.setGeometryCoordinates_(geometry, coordinates);
+  var rTree = this.rBush_;
+  goog.asserts.assert(segment !== undefined, 'segment should be defined');
+  rTree.remove(segmentData);
+  goog.asserts.assert(index !== undefined, 'index should be defined');
+  this.updateSegmentIndices_(geometry, index, depth, 1);
+  var newSegmentData = /** @type {ol.ModifySegmentDataType} */ ({
+    segment: [segment[0], vertex],
+    feature: feature,
+    geometry: geometry,
+    depth: depth,
+    index: index
+  });
+  rTree.insert(ol.extent.boundingExtent(newSegmentData.segment),
+      newSegmentData);
+  this.dragSegments_.push([newSegmentData, 1]);
+
+  var newSegmentData2 = /** @type {ol.ModifySegmentDataType} */ ({
+    segment: [vertex, segment[1]],
+    feature: feature,
+    geometry: geometry,
+    depth: depth,
+    index: index + 1
+  });
+  rTree.insert(ol.extent.boundingExtent(newSegmentData2.segment),
+      newSegmentData2);
+  this.dragSegments_.push([newSegmentData2, 0]);
+  this.ignoreNextSingleClick_ = true;
+};
+
+/**
+ * Removes the vertex currently being pointed.
+ * @return {boolean} True when a vertex was removed.
+ * @api
+ */
+ol.interaction.Modify.prototype.removePoint = function() {
+  var handled = false;
+  if (this.lastPointerEvent_ && this.lastPointerEvent_.type != ol.MapBrowserEvent.EventType.POINTERDRAG) {
+    var evt = this.lastPointerEvent_;
+    this.willModifyFeatures_(evt);
+    handled = this.removeVertex_();
+    this.dispatchEvent(new ol.interaction.ModifyEvent(
+        ol.ModifyEventType.MODIFYEND, this.features_, evt));
+    this.modified_ = false;
+  }
+  return handled;
+};
+
+/**
+ * Removes a vertex from all matching features.
+ * @return {boolean} True when a vertex was removed.
+ * @private
+ */
+ol.interaction.Modify.prototype.removeVertex_ = function() {
+  var dragSegments = this.dragSegments_;
+  var segmentsByFeature = {};
+  var component, coordinates, dragSegment, geometry, i, index, left;
+  var newIndex, right, segmentData, uid, deleted;
+  for (i = dragSegments.length - 1; i >= 0; --i) {
+    dragSegment = dragSegments[i];
+    segmentData = dragSegment[0];
+    uid = goog.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 ol.geom.GeometryType.MULTI_LINE_STRING:
+        if (coordinates[segmentData.depth[0]].length > 2) {
+          coordinates[segmentData.depth[0]].splice(index, 1);
+          deleted = true;
+        }
+        break;
+      case ol.geom.GeometryType.LINE_STRING:
+        if (coordinates.length > 2) {
+          coordinates.splice(index, 1);
+          deleted = true;
+        }
+        break;
+      case ol.geom.GeometryType.MULTI_POLYGON:
+        component = component[segmentData.depth[1]];
+        /* falls through */
+      case ol.geom.GeometryType.POLYGON:
+        component = component[segmentData.depth[0]];
+        if (component.length > 4) {
+          if (index == component.length - 1) {
+            index = 0;
+          }
+          component.splice(index, 1);
+          deleted = true;
+          if (index === 0) {
+            // close the ring again
+            component.pop();
+            component.push(component[0]);
+            newIndex = component.length - 1;
+          }
+        }
+        break;
+      default:
+        // pass
+    }
+
+    if (deleted) {
+      this.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) {
+        goog.asserts.assert(newIndex >= 0, 'newIndex should be larger than 0');
+
+        var newSegmentData = /** @type {ol.ModifySegmentDataType} */ ({
+          depth: segmentData.depth,
+          feature: segmentData.feature,
+          geometry: segmentData.geometry,
+          index: newIndex,
+          segment: segments
+        });
+        this.rBush_.insert(ol.extent.boundingExtent(newSegmentData.segment),
+            newSegmentData);
+      }
+      this.updateSegmentIndices_(geometry, index, segmentData.depth, -1);
+      if (this.vertexFeature_) {
+        this.overlay_.getSource().removeFeature(this.vertexFeature_);
+        this.vertexFeature_ = null;
+      }
+    }
+
+  }
+  return true;
+};
+
+
+/**
+ * @param {ol.geom.SimpleGeometry} geometry Geometry.
+ * @param {Array} coordinates Coordinates.
+ * @private
+ */
+ol.interaction.Modify.prototype.setGeometryCoordinates_ = function(geometry, coordinates) {
+  this.changingFeature_ = true;
+  geometry.setCoordinates(coordinates);
+  this.changingFeature_ = false;
+};
+
+
+/**
+ * @param {ol.geom.SimpleGeometry} geometry Geometry.
+ * @param {number} index Index.
+ * @param {Array.<number>|undefined} depth Depth.
+ * @param {number} delta Delta (1 or -1).
+ * @private
+ */
+ol.interaction.Modify.prototype.updateSegmentIndices_ = function(
+    geometry, index, depth, delta) {
+  this.rBush_.forEachInExtent(geometry.getExtent(), function(segmentDataMatch) {
+    if (segmentDataMatch.geometry === geometry &&
+        (depth === undefined || segmentDataMatch.depth === undefined ||
+        ol.array.equals(segmentDataMatch.depth, depth)) &&
+        segmentDataMatch.index > index) {
+      segmentDataMatch.index += delta;
+    }
+  });
+};
+
+
+/**
+ * @return {ol.StyleFunction} Styles.
+ */
+ol.interaction.Modify.getDefaultStyleFunction = function() {
+  var style = ol.style.createDefaultEditingStyles();
+  return function(feature, resolution) {
+    return style[ol.geom.GeometryType.POINT];
+  };
+};
+
+goog.provide('ol.interaction.Select');
+goog.provide('ol.interaction.SelectEvent');
+goog.provide('ol.interaction.SelectEventType');
+
+goog.require('goog.asserts');
+goog.require('ol.functions');
+goog.require('ol.CollectionEventType');
+goog.require('ol.Feature');
+goog.require('ol.array');
+goog.require('ol.events');
+goog.require('ol.events.Event');
+goog.require('ol.events.condition');
+goog.require('ol.geom.GeometryType');
+goog.require('ol.interaction.Interaction');
+goog.require('ol.layer.Vector');
+goog.require('ol.object');
+goog.require('ol.source.Vector');
+
+
+/**
+ * @enum {string}
+ */
+ol.interaction.SelectEventType = {
+  /**
+   * Triggered when feature(s) has been (de)selected.
+   * @event ol.interaction.SelectEvent#select
+   * @api
+   */
+  SELECT: 'select'
+};
+
+
+/**
+ * @classdesc
+ * Events emitted by {@link ol.interaction.Select} instances are instances of
+ * this type.
+ *
+ * @param {string} type The event type.
+ * @param {Array.<ol.Feature>} selected Selected features.
+ * @param {Array.<ol.Feature>} deselected Deselected features.
+ * @param {ol.MapBrowserEvent} mapBrowserEvent Associated
+ *     {@link ol.MapBrowserEvent}.
+ * @implements {oli.SelectEvent}
+ * @extends {ol.events.Event}
+ * @constructor
+ */
+ol.interaction.SelectEvent = function(type, selected, deselected, mapBrowserEvent) {
+  ol.events.Event.call(this, type);
+
+  /**
+   * Selected features array.
+   * @type {Array.<ol.Feature>}
+   * @api
+   */
+  this.selected = selected;
+
+  /**
+   * Deselected features array.
+   * @type {Array.<ol.Feature>}
+   * @api
+   */
+  this.deselected = deselected;
+
+  /**
+   * Associated {@link ol.MapBrowserEvent}.
+   * @type {ol.MapBrowserEvent}
+   * @api
+   */
+  this.mapBrowserEvent = mapBrowserEvent;
+};
+ol.inherits(ol.interaction.SelectEvent, ol.events.Event);
+
+
+/**
+ * @classdesc
+ * Interaction for selecting vector features. By default, selected features are
+ * styled differently, so this interaction can be used for visual highlighting,
+ * as well as selecting features for other actions, such as modification or
+ * output. There are three ways of controlling which features are selected:
+ * using the browser event as defined by the `condition` and optionally the
+ * `toggle`, `add`/`remove`, and `multi` options; a `layers` filter; and a
+ * further feature filter using the `filter` option.
+ *
+ * Selected features are added to an internal unmanaged layer.
+ *
+ * @constructor
+ * @extends {ol.interaction.Interaction}
+ * @param {olx.interaction.SelectOptions=} opt_options Options.
+ * @fires ol.interaction.SelectEvent
+ * @api stable
+ */
+ol.interaction.Select = function(opt_options) {
+
+  ol.interaction.Interaction.call(this, {
+    handleEvent: ol.interaction.Select.handleEvent
+  });
+
+  var options = opt_options ? opt_options : {};
+
+  /**
+   * @private
+   * @type {ol.EventsConditionType}
+   */
+  this.condition_ = options.condition ?
+      options.condition : ol.events.condition.singleClick;
+
+  /**
+   * @private
+   * @type {ol.EventsConditionType}
+   */
+  this.addCondition_ = options.addCondition ?
+      options.addCondition : ol.events.condition.never;
+
+  /**
+   * @private
+   * @type {ol.EventsConditionType}
+   */
+  this.removeCondition_ = options.removeCondition ?
+      options.removeCondition : ol.events.condition.never;
+
+  /**
+   * @private
+   * @type {ol.EventsConditionType}
+   */
+  this.toggleCondition_ = options.toggleCondition ?
+      options.toggleCondition : ol.events.condition.shiftKeyOnly;
+
+  /**
+   * @private
+   * @type {boolean}
+   */
+  this.multi_ = options.multi ? options.multi : false;
+
+  /**
+   * @private
+   * @type {ol.SelectFilterFunction}
+   */
+  this.filter_ = options.filter ? options.filter :
+      ol.functions.TRUE;
+
+  var featureOverlay = new ol.layer.Vector({
+    source: new ol.source.Vector({
+      useSpatialIndex: false,
+      features: options.features,
+      wrapX: options.wrapX
+    }),
+    style: options.style ? options.style :
+        ol.interaction.Select.getDefaultStyleFunction(),
+    updateWhileAnimating: true,
+    updateWhileInteracting: true
+  });
+
+  /**
+   * @private
+   * @type {ol.layer.Vector}
+   */
+  this.featureOverlay_ = featureOverlay;
+
+  var layerFilter;
+  if (options.layers) {
+    if (typeof options.layers === 'function') {
+      /**
+       * @param {ol.layer.Layer} layer Layer.
+       * @return {boolean} Include.
+       */
+      layerFilter = function(layer) {
+        goog.asserts.assertFunction(options.layers);
+        return options.layers(layer);
+      };
+    } else {
+      var layers = options.layers;
+      /**
+       * @param {ol.layer.Layer} layer Layer.
+       * @return {boolean} Include.
+       */
+      layerFilter = function(layer) {
+        return ol.array.includes(layers, layer);
+      };
+    }
+  } else {
+    layerFilter = ol.functions.TRUE;
+  }
+
+  /**
+   * @private
+   * @type {function(ol.layer.Layer): boolean}
+   */
+  this.layerFilter_ = layerFilter;
+
+  /**
+   * An association between selected feature (key)
+   * and layer (value)
+   * @private
+   * @type {Object.<number, ol.layer.Layer>}
+   */
+  this.featureLayerAssociation_ = {};
+
+  var features = this.featureOverlay_.getSource().getFeaturesCollection();
+  ol.events.listen(features, ol.CollectionEventType.ADD,
+      this.addFeature_, this);
+  ol.events.listen(features, ol.CollectionEventType.REMOVE,
+      this.removeFeature_, this);
+
+};
+ol.inherits(ol.interaction.Select, ol.interaction.Interaction);
+
+
+/**
+ * @param {ol.Feature|ol.render.Feature} feature Feature.
+ * @param {ol.layer.Layer} layer Layer.
+ * @private
+ */
+ol.interaction.Select.prototype.addFeatureLayerAssociation_ = function(feature, layer) {
+  var key = goog.getUid(feature);
+  this.featureLayerAssociation_[key] = layer;
+};
+
+
+/**
+ * Get the selected features.
+ * @return {ol.Collection.<ol.Feature>} Features collection.
+ * @api stable
+ */
+ol.interaction.Select.prototype.getFeatures = function() {
+  return this.featureOverlay_.getSource().getFeaturesCollection();
+};
+
+
+/**
+ * Returns the associated {@link ol.layer.Vector vectorlayer} of
+ * the (last) selected feature. Note that this will not work with any
+ * programmatic method like pushing features to
+ * {@link ol.interaction.Select#getFeatures collection}.
+ * @param {ol.Feature|ol.render.Feature} feature Feature
+ * @return {ol.layer.Vector} Layer.
+ * @api
+ */
+ol.interaction.Select.prototype.getLayer = function(feature) {
+  goog.asserts.assertInstanceof(feature, ol.Feature,
+      'feature should be an ol.Feature');
+  var key = goog.getUid(feature);
+  return /** @type {ol.layer.Vector} */ (this.featureLayerAssociation_[key]);
+};
+
+
+/**
+ * Handles the {@link ol.MapBrowserEvent map browser event} and may change the
+ * selected state of features.
+ * @param {ol.MapBrowserEvent} mapBrowserEvent Map browser event.
+ * @return {boolean} `false` to stop event propagation.
+ * @this {ol.interaction.Select}
+ * @api
+ */
+ol.interaction.Select.handleEvent = function(mapBrowserEvent) {
+  if (!this.condition_(mapBrowserEvent)) {
+    return true;
+  }
+  var add = this.addCondition_(mapBrowserEvent);
+  var remove = this.removeCondition_(mapBrowserEvent);
+  var toggle = this.toggleCondition_(mapBrowserEvent);
+  var set = !add && !remove && !toggle;
+  var map = mapBrowserEvent.map;
+  var features = this.featureOverlay_.getSource().getFeaturesCollection();
+  var 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.
+    ol.object.clear(this.featureLayerAssociation_);
+    map.forEachFeatureAtPixel(mapBrowserEvent.pixel,
+        /**
+         * @param {ol.Feature|ol.render.Feature} feature Feature.
+         * @param {ol.layer.Layer} layer Layer.
+         * @return {boolean|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_;
+          }
+        }, this, this.layerFilter_);
+    if (selected.length > 0 && features.getLength() == 1 && features.item(0) == selected[0]) {
+      // No change; an already selected feature is selected again
+      selected.length = 0;
+    } else {
+      if (features.getLength() !== 0) {
+        deselected = Array.prototype.concat(features.getArray());
+        features.clear();
+      }
+      features.extend(selected);
+    }
+  } else {
+    // Modify the currently selected feature(s).
+    map.forEachFeatureAtPixel(mapBrowserEvent.pixel,
+        /**
+         * @param {ol.Feature|ol.render.Feature} feature Feature.
+         * @param {ol.layer.Layer} layer Layer.
+         * @return {boolean|undefined} Continue to iterate over the features.
+         */
+        function(feature, layer) {
+          if (this.filter_(feature, layer)) {
+            if ((add || toggle) &&
+                !ol.array.includes(features.getArray(), feature)) {
+              selected.push(feature);
+              this.addFeatureLayerAssociation_(feature, layer);
+            } else if ((remove || toggle) &&
+                ol.array.includes(features.getArray(), feature)) {
+              deselected.push(feature);
+              this.removeFeatureLayerAssociation_(feature);
+            }
+            return !this.multi_;
+          }
+        }, this, this.layerFilter_);
+    var i;
+    for (i = deselected.length - 1; i >= 0; --i) {
+      features.remove(deselected[i]);
+    }
+    features.extend(selected);
+  }
+  if (selected.length > 0 || deselected.length > 0) {
+    this.dispatchEvent(
+        new ol.interaction.SelectEvent(ol.interaction.SelectEventType.SELECT,
+            selected, deselected, mapBrowserEvent));
+  }
+  return ol.events.condition.pointerMove(mapBrowserEvent);
+};
+
+
+/**
+ * Remove the interaction from its current map, if any,  and attach it to a new
+ * map, if any. Pass `null` to just remove the interaction from the current map.
+ * @param {ol.Map} map Map.
+ * @api stable
+ */
+ol.interaction.Select.prototype.setMap = function(map) {
+  var currentMap = this.getMap();
+  var selectedFeatures =
+      this.featureOverlay_.getSource().getFeaturesCollection();
+  if (currentMap) {
+    selectedFeatures.forEach(currentMap.unskipFeature, currentMap);
+  }
+  ol.interaction.Interaction.prototype.setMap.call(this, map);
+  this.featureOverlay_.setMap(map);
+  if (map) {
+    selectedFeatures.forEach(map.skipFeature, map);
+  }
+};
+
+
+/**
+ * @return {ol.StyleFunction} Styles.
+ */
+ol.interaction.Select.getDefaultStyleFunction = function() {
+  var styles = ol.style.createDefaultEditingStyles();
+  ol.array.extend(styles[ol.geom.GeometryType.POLYGON],
+      styles[ol.geom.GeometryType.LINE_STRING]);
+  ol.array.extend(styles[ol.geom.GeometryType.GEOMETRY_COLLECTION],
+      styles[ol.geom.GeometryType.LINE_STRING]);
+
+  return function(feature, resolution) {
+    return styles[feature.getGeometry().getType()];
+  };
+};
+
+
+/**
+ * @param {ol.CollectionEvent} evt Event.
+ * @private
+ */
+ol.interaction.Select.prototype.addFeature_ = function(evt) {
+  var feature = evt.element;
+  var map = this.getMap();
+  goog.asserts.assertInstanceof(feature, ol.Feature,
+      'feature should be an ol.Feature');
+  if (map) {
+    map.skipFeature(feature);
+  }
+};
+
+
+/**
+ * @param {ol.CollectionEvent} evt Event.
+ * @private
+ */
+ol.interaction.Select.prototype.removeFeature_ = function(evt) {
+  var feature = evt.element;
+  var map = this.getMap();
+  goog.asserts.assertInstanceof(feature, ol.Feature,
+      'feature should be an ol.Feature');
+  if (map) {
+    map.unskipFeature(feature);
+  }
+};
+
+
+/**
+ * @param {ol.Feature|ol.render.Feature} feature Feature.
+ * @private
+ */
+ol.interaction.Select.prototype.removeFeatureLayerAssociation_ = function(feature) {
+  var key = goog.getUid(feature);
+  delete this.featureLayerAssociation_[key];
+};
+
+goog.provide('ol.interaction.Snap');
+goog.provide('ol.interaction.SnapProperty');
+
+goog.require('goog.asserts');
+goog.require('ol');
+goog.require('ol.Collection');
+goog.require('ol.CollectionEvent');
+goog.require('ol.CollectionEventType');
+goog.require('ol.Feature');
+goog.require('ol.Object');
+goog.require('ol.Observable');
+goog.require('ol.coordinate');
+goog.require('ol.events');
+goog.require('ol.events.EventType');
+goog.require('ol.extent');
+goog.require('ol.geom.Geometry');
+goog.require('ol.interaction.Pointer');
+goog.require('ol.functions');
+goog.require('ol.object');
+goog.require('ol.source.Vector');
+goog.require('ol.source.VectorEvent');
+goog.require('ol.source.VectorEventType');
+goog.require('ol.structs.RBush');
+
+
+/**
+ * @classdesc
+ * Handles snapping of vector features while modifying or drawing them.  The
+ * features can come from a {@link ol.source.Vector} or {@link ol.Collection}
+ * Any interaction object that allows the user to interact
+ * with the features using the mouse can benefit from the snapping, as long
+ * as it is added before.
+ *
+ * The snap interaction modifies map browser event `coordinate` and `pixel`
+ * properties to force the snap to occur to any interaction that them.
+ *
+ * Example:
+ *
+ *     var snap = new ol.interaction.Snap({
+ *       source: source
+ *     });
+ *
+ * @constructor
+ * @extends {ol.interaction.Pointer}
+ * @param {olx.interaction.SnapOptions=} opt_options Options.
+ * @api
+ */
+ol.interaction.Snap = function(opt_options) {
+
+  ol.interaction.Pointer.call(this, {
+    handleEvent: ol.interaction.Snap.handleEvent_,
+    handleDownEvent: ol.functions.TRUE,
+    handleUpEvent: ol.interaction.Snap.handleUpEvent_
+  });
+
+  var options = opt_options ? opt_options : {};
+
+  /**
+   * @type {ol.source.Vector}
+   * @private
+   */
+  this.source_ = options.source ? options.source : null;
+
+  /**
+   * @private
+   * @type {boolean}
+   */
+  this.vertex_ = options.vertex !== undefined ? options.vertex : true;
+
+  /**
+   * @private
+   * @type {boolean}
+   */
+  this.edge_ = options.edge !== undefined ? options.edge : true;
+
+  /**
+   * @type {ol.Collection.<ol.Feature>}
+   * @private
+   */
+  this.features_ = options.features ? options.features : null;
+
+  /**
+   * @type {Array.<ol.EventsKey>}
+   * @private
+   */
+  this.featuresListenerKeys_ = [];
+
+  /**
+   * @type {Object.<number, ol.EventsKey>}
+   * @private
+   */
+  this.geometryChangeListenerKeys_ = {};
+
+  /**
+   * @type {Object.<number, ol.EventsKey>}
+   * @private
+   */
+  this.geometryModifyListenerKeys_ = {};
+
+  /**
+   * Extents are preserved so indexed segment can be quickly removed
+   * when its feature geometry changes
+   * @type {Object.<number, ol.Extent>}
+   * @private
+   */
+  this.indexedFeaturesExtents_ = {};
+
+  /**
+   * If a feature geometry changes while a pointer drag|move event occurs, the
+   * feature doesn't get updated right away.  It will be at the next 'pointerup'
+   * event fired.
+   * @type {Object.<number, ol.Feature>}
+   * @private
+   */
+  this.pendingFeatures_ = {};
+
+  /**
+   * Used for distance sorting in sortByDistance_
+   * @type {ol.Coordinate}
+   * @private
+   */
+  this.pixelCoordinate_ = null;
+
+  /**
+   * @type {number}
+   * @private
+   */
+  this.pixelTolerance_ = options.pixelTolerance !== undefined ?
+      options.pixelTolerance : 10;
+
+  /**
+   * @type {function(ol.SnapSegmentDataType, ol.SnapSegmentDataType): number}
+   * @private
+   */
+  this.sortByDistance_ = ol.interaction.Snap.sortByDistance.bind(this);
+
+
+  /**
+  * Segment RTree for each layer
+  * @type {ol.structs.RBush.<ol.SnapSegmentDataType>}
+  * @private
+  */
+  this.rBush_ = new ol.structs.RBush();
+
+
+  /**
+  * @const
+  * @private
+  * @type {Object.<string, function(ol.Feature, ol.geom.Geometry)>}
+  */
+  this.SEGMENT_WRITERS_ = {
+    'Point': this.writePointGeometry_,
+    'LineString': this.writeLineStringGeometry_,
+    'LinearRing': this.writeLineStringGeometry_,
+    'Polygon': this.writePolygonGeometry_,
+    'MultiPoint': this.writeMultiPointGeometry_,
+    'MultiLineString': this.writeMultiLineStringGeometry_,
+    'MultiPolygon': this.writeMultiPolygonGeometry_,
+    'GeometryCollection': this.writeGeometryCollectionGeometry_
+  };
+};
+ol.inherits(ol.interaction.Snap, ol.interaction.Pointer);
+
+
+/**
+ * Add a feature to the collection of features that we may snap to.
+ * @param {ol.Feature} feature Feature.
+ * @param {boolean=} opt_listen Whether to listen to the geometry change or not
+ *     Defaults to `true`.
+ * @api
+ */
+ol.interaction.Snap.prototype.addFeature = function(feature, opt_listen) {
+  var listen = opt_listen !== undefined ? opt_listen : true;
+  var feature_uid = goog.getUid(feature);
+  var geometry = feature.getGeometry();
+  if (geometry) {
+    var segmentWriter = this.SEGMENT_WRITERS_[geometry.getType()];
+    if (segmentWriter) {
+      this.indexedFeaturesExtents_[feature_uid] = geometry.getExtent(
+          ol.extent.createEmpty());
+      segmentWriter.call(this, feature, geometry);
+
+      if (listen) {
+        this.geometryModifyListenerKeys_[feature_uid] = ol.events.listen(
+            geometry,
+            ol.events.EventType.CHANGE,
+            this.handleGeometryModify_.bind(this, feature),
+            this);
+      }
+    }
+  }
+
+  if (listen) {
+    this.geometryChangeListenerKeys_[feature_uid] = ol.events.listen(
+        feature,
+        ol.Object.getChangeEventType(feature.getGeometryName()),
+        this.handleGeometryChange_, this);
+  }
+};
+
+
+/**
+ * @param {ol.Feature} feature Feature.
+ * @private
+ */
+ol.interaction.Snap.prototype.forEachFeatureAdd_ = function(feature) {
+  this.addFeature(feature);
+};
+
+
+/**
+ * @param {ol.Feature} feature Feature.
+ * @private
+ */
+ol.interaction.Snap.prototype.forEachFeatureRemove_ = function(feature) {
+  this.removeFeature(feature);
+};
+
+
+/**
+ * @return {ol.Collection.<ol.Feature>|Array.<ol.Feature>} Features.
+ * @private
+ */
+ol.interaction.Snap.prototype.getFeatures_ = function() {
+  var features;
+  if (this.features_) {
+    features = this.features_;
+  } else if (this.source_) {
+    features = this.source_.getFeatures();
+  }
+  goog.asserts.assert(features !== undefined, 'features should be defined');
+  return features;
+};
+
+
+/**
+ * @param {ol.source.VectorEvent|ol.CollectionEvent} evt Event.
+ * @private
+ */
+ol.interaction.Snap.prototype.handleFeatureAdd_ = function(evt) {
+  var feature;
+  if (evt instanceof ol.source.VectorEvent) {
+    feature = evt.feature;
+  } else if (evt instanceof ol.CollectionEvent) {
+    feature = evt.element;
+  }
+  goog.asserts.assertInstanceof(feature, ol.Feature,
+      'feature should be an ol.Feature');
+  this.addFeature(feature);
+};
+
+
+/**
+ * @param {ol.source.VectorEvent|ol.CollectionEvent} evt Event.
+ * @private
+ */
+ol.interaction.Snap.prototype.handleFeatureRemove_ = function(evt) {
+  var feature;
+  if (evt instanceof ol.source.VectorEvent) {
+    feature = evt.feature;
+  } else if (evt instanceof ol.CollectionEvent) {
+    feature = evt.element;
+  }
+  goog.asserts.assertInstanceof(feature, ol.Feature,
+      'feature should be an ol.Feature');
+  this.removeFeature(feature);
+};
+
+
+/**
+ * @param {ol.events.Event} evt Event.
+ * @private
+ */
+ol.interaction.Snap.prototype.handleGeometryChange_ = function(evt) {
+  var feature = evt.target;
+  goog.asserts.assertInstanceof(feature, ol.Feature);
+  this.removeFeature(feature, true);
+  this.addFeature(feature, true);
+};
+
+
+/**
+ * @param {ol.Feature} feature Feature which geometry was modified.
+ * @param {ol.events.Event} evt Event.
+ * @private
+ */
+ol.interaction.Snap.prototype.handleGeometryModify_ = function(feature, evt) {
+  if (this.handlingDownUpSequence) {
+    var uid = goog.getUid(feature);
+    if (!(uid in this.pendingFeatures_)) {
+      this.pendingFeatures_[uid] = feature;
+    }
+  } else {
+    this.updateFeature_(feature);
+  }
+};
+
+
+/**
+ * Remove a feature from the collection of features that we may snap to.
+ * @param {ol.Feature} feature Feature
+ * @param {boolean=} opt_unlisten Whether to unlisten to the geometry change
+ *     or not. Defaults to `true`.
+ * @api
+ */
+ol.interaction.Snap.prototype.removeFeature = function(feature, opt_unlisten) {
+  var unlisten = opt_unlisten !== undefined ? opt_unlisten : true;
+  var feature_uid = goog.getUid(feature);
+  var extent = this.indexedFeaturesExtents_[feature_uid];
+  if (extent) {
+    var rBush = this.rBush_;
+    var i, nodesToRemove = [];
+    rBush.forEachInExtent(extent, function(node) {
+      if (feature === node.feature) {
+        nodesToRemove.push(node);
+      }
+    });
+    for (i = nodesToRemove.length - 1; i >= 0; --i) {
+      rBush.remove(nodesToRemove[i]);
+    }
+    if (unlisten) {
+      ol.Observable.unByKey(this.geometryModifyListenerKeys_[feature_uid]);
+      delete this.geometryModifyListenerKeys_[feature_uid];
+    }
+  }
+
+  if (unlisten) {
+    ol.Observable.unByKey(this.geometryChangeListenerKeys_[feature_uid]);
+    delete this.geometryChangeListenerKeys_[feature_uid];
+  }
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.interaction.Snap.prototype.setMap = function(map) {
+  var currentMap = this.getMap();
+  var keys = this.featuresListenerKeys_;
+  var features = this.getFeatures_();
+
+  if (currentMap) {
+    keys.forEach(ol.Observable.unByKey);
+    keys.length = 0;
+    features.forEach(this.forEachFeatureRemove_, this);
+  }
+  ol.interaction.Pointer.prototype.setMap.call(this, map);
+
+  if (map) {
+    if (this.features_) {
+      keys.push(
+        ol.events.listen(this.features_, ol.CollectionEventType.ADD,
+            this.handleFeatureAdd_, this),
+        ol.events.listen(this.features_, ol.CollectionEventType.REMOVE,
+            this.handleFeatureRemove_, this)
+      );
+    } else if (this.source_) {
+      keys.push(
+        ol.events.listen(this.source_, ol.source.VectorEventType.ADDFEATURE,
+            this.handleFeatureAdd_, this),
+        ol.events.listen(this.source_, ol.source.VectorEventType.REMOVEFEATURE,
+            this.handleFeatureRemove_, this)
+      );
+    }
+    features.forEach(this.forEachFeatureAdd_, this);
+  }
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.interaction.Snap.prototype.shouldStopEvent = ol.functions.FALSE;
+
+
+/**
+ * @param {ol.Pixel} pixel Pixel
+ * @param {ol.Coordinate} pixelCoordinate Coordinate
+ * @param {ol.Map} map Map.
+ * @return {ol.SnapResultType} Snap result
+ */
+ol.interaction.Snap.prototype.snapTo = function(pixel, pixelCoordinate, map) {
+
+  var lowerLeft = map.getCoordinateFromPixel(
+      [pixel[0] - this.pixelTolerance_, pixel[1] + this.pixelTolerance_]);
+  var upperRight = map.getCoordinateFromPixel(
+      [pixel[0] + this.pixelTolerance_, pixel[1] - this.pixelTolerance_]);
+  var box = ol.extent.boundingExtent([lowerLeft, upperRight]);
+
+  var segments = this.rBush_.getInExtent(box);
+  var snappedToVertex = false;
+  var snapped = false;
+  var vertex = null;
+  var vertexPixel = null;
+  var dist, pixel1, pixel2, squaredDist1, squaredDist2;
+  if (segments.length > 0) {
+    this.pixelCoordinate_ = pixelCoordinate;
+    segments.sort(this.sortByDistance_);
+    var closestSegment = segments[0].segment;
+    if (this.vertex_ && !this.edge_) {
+      pixel1 = map.getPixelFromCoordinate(closestSegment[0]);
+      pixel2 = map.getPixelFromCoordinate(closestSegment[1]);
+      squaredDist1 = ol.coordinate.squaredDistance(pixel, pixel1);
+      squaredDist2 = ol.coordinate.squaredDistance(pixel, pixel2);
+      dist = Math.sqrt(Math.min(squaredDist1, squaredDist2));
+      snappedToVertex = dist <= this.pixelTolerance_;
+      if (snappedToVertex) {
+        snapped = true;
+        vertex = squaredDist1 > squaredDist2 ?
+            closestSegment[1] : closestSegment[0];
+        vertexPixel = map.getPixelFromCoordinate(vertex);
+      }
+    } else if (this.edge_) {
+      vertex = (ol.coordinate.closestOnSegment(pixelCoordinate,
+          closestSegment));
+      vertexPixel = map.getPixelFromCoordinate(vertex);
+      if (Math.sqrt(ol.coordinate.squaredDistance(pixel, vertexPixel)) <=
+          this.pixelTolerance_) {
+        snapped = true;
+        if (this.vertex_) {
+          pixel1 = map.getPixelFromCoordinate(closestSegment[0]);
+          pixel2 = map.getPixelFromCoordinate(closestSegment[1]);
+          squaredDist1 = ol.coordinate.squaredDistance(vertexPixel, pixel1);
+          squaredDist2 = ol.coordinate.squaredDistance(vertexPixel, pixel2);
+          dist = Math.sqrt(Math.min(squaredDist1, squaredDist2));
+          snappedToVertex = dist <= this.pixelTolerance_;
+          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 {ol.SnapResultType} */ ({
+    snapped: snapped,
+    vertex: vertex,
+    vertexPixel: vertexPixel
+  });
+};
+
+
+/**
+ * @param {ol.Feature} feature Feature
+ * @private
+ */
+ol.interaction.Snap.prototype.updateFeature_ = function(feature) {
+  this.removeFeature(feature, false);
+  this.addFeature(feature, false);
+};
+
+
+/**
+ * @param {ol.Feature} feature Feature
+ * @param {ol.geom.GeometryCollection} geometry Geometry.
+ * @private
+ */
+ol.interaction.Snap.prototype.writeGeometryCollectionGeometry_ = function(feature, geometry) {
+  var i, geometries = geometry.getGeometriesArray();
+  for (i = 0; i < geometries.length; ++i) {
+    this.SEGMENT_WRITERS_[geometries[i].getType()].call(
+        this, feature, geometries[i]);
+  }
+};
+
+
+/**
+ * @param {ol.Feature} feature Feature
+ * @param {ol.geom.LineString} geometry Geometry.
+ * @private
+ */
+ol.interaction.Snap.prototype.writeLineStringGeometry_ = function(feature, geometry) {
+  var coordinates = geometry.getCoordinates();
+  var i, ii, segment, segmentData;
+  for (i = 0, ii = coordinates.length - 1; i < ii; ++i) {
+    segment = coordinates.slice(i, i + 2);
+    segmentData = /** @type {ol.SnapSegmentDataType} */ ({
+      feature: feature,
+      segment: segment
+    });
+    this.rBush_.insert(ol.extent.boundingExtent(segment), segmentData);
+  }
+};
+
+
+/**
+ * @param {ol.Feature} feature Feature
+ * @param {ol.geom.MultiLineString} geometry Geometry.
+ * @private
+ */
+ol.interaction.Snap.prototype.writeMultiLineStringGeometry_ = function(feature, geometry) {
+  var lines = geometry.getCoordinates();
+  var coordinates, i, ii, j, jj, segment, segmentData;
+  for (j = 0, jj = lines.length; j < jj; ++j) {
+    coordinates = lines[j];
+    for (i = 0, ii = coordinates.length - 1; i < ii; ++i) {
+      segment = coordinates.slice(i, i + 2);
+      segmentData = /** @type {ol.SnapSegmentDataType} */ ({
+        feature: feature,
+        segment: segment
+      });
+      this.rBush_.insert(ol.extent.boundingExtent(segment), segmentData);
+    }
+  }
+};
+
+
+/**
+ * @param {ol.Feature} feature Feature
+ * @param {ol.geom.MultiPoint} geometry Geometry.
+ * @private
+ */
+ol.interaction.Snap.prototype.writeMultiPointGeometry_ = function(feature, geometry) {
+  var points = geometry.getCoordinates();
+  var coordinates, i, ii, segmentData;
+  for (i = 0, ii = points.length; i < ii; ++i) {
+    coordinates = points[i];
+    segmentData = /** @type {ol.SnapSegmentDataType} */ ({
+      feature: feature,
+      segment: [coordinates, coordinates]
+    });
+    this.rBush_.insert(geometry.getExtent(), segmentData);
+  }
+};
+
+
+/**
+ * @param {ol.Feature} feature Feature
+ * @param {ol.geom.MultiPolygon} geometry Geometry.
+ * @private
+ */
+ol.interaction.Snap.prototype.writeMultiPolygonGeometry_ = function(feature, geometry) {
+  var polygons = geometry.getCoordinates();
+  var coordinates, i, ii, j, jj, k, kk, rings, segment, segmentData;
+  for (k = 0, kk = polygons.length; k < kk; ++k) {
+    rings = polygons[k];
+    for (j = 0, jj = rings.length; j < jj; ++j) {
+      coordinates = rings[j];
+      for (i = 0, ii = coordinates.length - 1; i < ii; ++i) {
+        segment = coordinates.slice(i, i + 2);
+        segmentData = /** @type {ol.SnapSegmentDataType} */ ({
+          feature: feature,
+          segment: segment
+        });
+        this.rBush_.insert(ol.extent.boundingExtent(segment), segmentData);
+      }
+    }
+  }
+};
+
+
+/**
+ * @param {ol.Feature} feature Feature
+ * @param {ol.geom.Point} geometry Geometry.
+ * @private
+ */
+ol.interaction.Snap.prototype.writePointGeometry_ = function(feature, geometry) {
+  var coordinates = geometry.getCoordinates();
+  var segmentData = /** @type {ol.SnapSegmentDataType} */ ({
+    feature: feature,
+    segment: [coordinates, coordinates]
+  });
+  this.rBush_.insert(geometry.getExtent(), segmentData);
+};
+
+
+/**
+ * @param {ol.Feature} feature Feature
+ * @param {ol.geom.Polygon} geometry Geometry.
+ * @private
+ */
+ol.interaction.Snap.prototype.writePolygonGeometry_ = function(feature, geometry) {
+  var rings = geometry.getCoordinates();
+  var coordinates, i, ii, j, jj, segment, segmentData;
+  for (j = 0, jj = rings.length; j < jj; ++j) {
+    coordinates = rings[j];
+    for (i = 0, ii = coordinates.length - 1; i < ii; ++i) {
+      segment = coordinates.slice(i, i + 2);
+      segmentData = /** @type {ol.SnapSegmentDataType} */ ({
+        feature: feature,
+        segment: segment
+      });
+      this.rBush_.insert(ol.extent.boundingExtent(segment), segmentData);
+    }
+  }
+};
+
+
+/**
+ * Handle all pointer events events.
+ * @param {ol.MapBrowserEvent} evt A move event.
+ * @return {boolean} Pass the event to other interactions.
+ * @this {ol.interaction.Snap}
+ * @private
+ */
+ol.interaction.Snap.handleEvent_ = function(evt) {
+  var result = this.snapTo(evt.pixel, evt.coordinate, evt.map);
+  if (result.snapped) {
+    evt.coordinate = result.vertex.slice(0, 2);
+    evt.pixel = result.vertexPixel;
+  }
+  return ol.interaction.Pointer.handleEvent.call(this, evt);
+};
+
+
+/**
+ * @param {ol.MapBrowserPointerEvent} evt Event.
+ * @return {boolean} Stop drag sequence?
+ * @this {ol.interaction.Snap}
+ * @private
+ */
+ol.interaction.Snap.handleUpEvent_ = function(evt) {
+  var featuresToUpdate = ol.object.getValues(this.pendingFeatures_);
+  if (featuresToUpdate.length) {
+    featuresToUpdate.forEach(this.updateFeature_, this);
+    this.pendingFeatures_ = {};
+  }
+  return false;
+};
+
+
+/**
+ * Sort segments by distance, helper function
+ * @param {ol.SnapSegmentDataType} a The first segment data.
+ * @param {ol.SnapSegmentDataType} b The second segment data.
+ * @return {number} The difference in distance.
+ * @this {ol.interaction.Snap}
+ */
+ol.interaction.Snap.sortByDistance = function(a, b) {
+  return ol.coordinate.squaredDistanceToSegment(
+      this.pixelCoordinate_, a.segment) -
+      ol.coordinate.squaredDistanceToSegment(
+      this.pixelCoordinate_, b.segment);
+};
+
+goog.provide('ol.interaction.Translate');
+goog.provide('ol.interaction.TranslateEvent');
+
+goog.require('goog.asserts');
+goog.require('ol.events');
+goog.require('ol.events.Event');
+goog.require('ol.array');
+goog.require('ol.interaction.Pointer');
+
+
+/**
+ * @enum {string}
+ */
+ol.interaction.TranslateEventType = {
+  /**
+   * Triggered upon feature translation start.
+   * @event ol.interaction.TranslateEvent#translatestart
+   * @api
+   */
+  TRANSLATESTART: 'translatestart',
+  /**
+   * Triggered upon feature translation.
+   * @event ol.interaction.TranslateEvent#translating
+   * @api
+   */
+  TRANSLATING: 'translating',
+  /**
+   * Triggered upon feature translation end.
+   * @event ol.interaction.TranslateEvent#translateend
+   * @api
+   */
+  TRANSLATEEND: 'translateend'
+};
+
+
+/**
+ * @classdesc
+ * Events emitted by {@link ol.interaction.Translate} instances are instances of
+ * this type.
+ *
+ * @constructor
+ * @extends {ol.events.Event}
+ * @implements {oli.interaction.TranslateEvent}
+ * @param {ol.interaction.TranslateEventType} type Type.
+ * @param {ol.Collection.<ol.Feature>} features The features translated.
+ * @param {ol.Coordinate} coordinate The event coordinate.
+ */
+ol.interaction.TranslateEvent = function(type, features, coordinate) {
+
+  ol.events.Event.call(this, type);
+
+  /**
+   * The features being translated.
+   * @type {ol.Collection.<ol.Feature>}
+   * @api
+   */
+  this.features = features;
+
+  /**
+   * The coordinate of the drag event.
+   * @const
+   * @type {ol.Coordinate}
+   * @api
+   */
+  this.coordinate = coordinate;
+};
+ol.inherits(ol.interaction.TranslateEvent, ol.events.Event);
+
+
+/**
+ * @classdesc
+ * Interaction for translating (moving) features.
+ *
+ * @constructor
+ * @extends {ol.interaction.Pointer}
+ * @fires ol.interaction.TranslateEvent
+ * @param {olx.interaction.TranslateOptions} options Options.
+ * @api
+ */
+ol.interaction.Translate = function(options) {
+  ol.interaction.Pointer.call(this, {
+    handleDownEvent: ol.interaction.Translate.handleDownEvent_,
+    handleDragEvent: ol.interaction.Translate.handleDragEvent_,
+    handleMoveEvent: ol.interaction.Translate.handleMoveEvent_,
+    handleUpEvent: ol.interaction.Translate.handleUpEvent_
+  });
+
+
+  /**
+   * @type {string|undefined}
+   * @private
+   */
+  this.previousCursor_ = undefined;
+
+
+  /**
+   * The last position we translated to.
+   * @type {ol.Coordinate}
+   * @private
+   */
+  this.lastCoordinate_ = null;
+
+
+  /**
+   * @type {ol.Collection.<ol.Feature>}
+   * @private
+   */
+  this.features_ = options.features !== undefined ? options.features : null;
+
+  var layerFilter;
+  if (options.layers) {
+    if (typeof options.layers === 'function') {
+      /**
+       * @param {ol.layer.Layer} layer Layer.
+       * @return {boolean} Include.
+       */
+      layerFilter = function(layer) {
+        goog.asserts.assertFunction(options.layers);
+        return options.layers(layer);
+      };
+    } else {
+      var layers = options.layers;
+      /**
+       * @param {ol.layer.Layer} layer Layer.
+       * @return {boolean} Include.
+       */
+      layerFilter = function(layer) {
+        return ol.array.includes(layers, layer);
+      };
+    }
+  } else {
+    layerFilter = ol.functions.TRUE;
+  }
+
+  /**
+   * @private
+   * @type {function(ol.layer.Layer): boolean}
+   */
+  this.layerFilter_ = layerFilter;
+
+  /**
+   * @type {ol.Feature}
+   * @private
+   */
+  this.lastFeature_ = null;
+};
+ol.inherits(ol.interaction.Translate, ol.interaction.Pointer);
+
+
+/**
+ * @param {ol.MapBrowserPointerEvent} event Event.
+ * @return {boolean} Start drag sequence?
+ * @this {ol.interaction.Translate}
+ * @private
+ */
+ol.interaction.Translate.handleDownEvent_ = function(event) {
+  this.lastFeature_ = this.featuresAtPixel_(event.pixel, event.map);
+  if (!this.lastCoordinate_ && this.lastFeature_) {
+    this.lastCoordinate_ = event.coordinate;
+    ol.interaction.Translate.handleMoveEvent_.call(this, event);
+    this.dispatchEvent(
+        new ol.interaction.TranslateEvent(
+            ol.interaction.TranslateEventType.TRANSLATESTART, this.features_,
+            event.coordinate));
+    return true;
+  }
+  return false;
+};
+
+
+/**
+ * @param {ol.MapBrowserPointerEvent} event Event.
+ * @return {boolean} Stop drag sequence?
+ * @this {ol.interaction.Translate}
+ * @private
+ */
+ol.interaction.Translate.handleUpEvent_ = function(event) {
+  if (this.lastCoordinate_) {
+    this.lastCoordinate_ = null;
+    ol.interaction.Translate.handleMoveEvent_.call(this, event);
+    this.dispatchEvent(
+        new ol.interaction.TranslateEvent(
+            ol.interaction.TranslateEventType.TRANSLATEEND, this.features_,
+            event.coordinate));
+    return true;
+  }
+  return false;
+};
+
+
+/**
+ * @param {ol.MapBrowserPointerEvent} event Event.
+ * @this {ol.interaction.Translate}
+ * @private
+ */
+ol.interaction.Translate.handleDragEvent_ = function(event) {
+  if (this.lastCoordinate_) {
+    var newCoordinate = event.coordinate;
+    var deltaX = newCoordinate[0] - this.lastCoordinate_[0];
+    var deltaY = newCoordinate[1] - this.lastCoordinate_[1];
+
+    if (this.features_) {
+      this.features_.forEach(function(feature) {
+        var geom = feature.getGeometry();
+        geom.translate(deltaX, deltaY);
+        feature.setGeometry(geom);
+      });
+    } else if (this.lastFeature_) {
+      var geom = this.lastFeature_.getGeometry();
+      geom.translate(deltaX, deltaY);
+      this.lastFeature_.setGeometry(geom);
+    }
+
+    this.lastCoordinate_ = newCoordinate;
+    this.dispatchEvent(
+        new ol.interaction.TranslateEvent(
+            ol.interaction.TranslateEventType.TRANSLATING, this.features_,
+            newCoordinate));
+  }
+};
+
+
+/**
+ * @param {ol.MapBrowserEvent} event Event.
+ * @this {ol.interaction.Translate}
+ * @private
+ */
+ol.interaction.Translate.handleMoveEvent_ = function(event) {
+  var elem = event.map.getTargetElement();
+  var intersectingFeature = event.map.forEachFeatureAtPixel(event.pixel,
+      function(feature) {
+        return feature;
+      });
+
+  if (intersectingFeature) {
+    var isSelected = false;
+
+    if (this.features_ &&
+        ol.array.includes(this.features_.getArray(), intersectingFeature)) {
+      isSelected = true;
+    }
+
+    this.previousCursor_ = elem.style.cursor;
+
+    // WebKit browsers don't support the grab icons without a prefix
+    elem.style.cursor = this.lastCoordinate_ ?
+        '-webkit-grabbing' : (isSelected ? '-webkit-grab' : 'pointer');
+
+    // Thankfully, attempting to set the standard ones will silently fail,
+    // keeping the prefixed icons
+    elem.style.cursor = !this.lastCoordinate_ ?
+        'grabbing' : (isSelected ? 'grab' : 'pointer');
+
+  } else {
+    elem.style.cursor = this.previousCursor_ !== undefined ?
+        this.previousCursor_ : '';
+    this.previousCursor_ = undefined;
+  }
+};
+
+
+/**
+ * Tests to see if the given coordinates intersects any of our selected
+ * features.
+ * @param {ol.Pixel} pixel Pixel coordinate to test for intersection.
+ * @param {ol.Map} map Map to test the intersection on.
+ * @return {ol.Feature} Returns the feature found at the specified pixel
+ * coordinates.
+ * @private
+ */
+ol.interaction.Translate.prototype.featuresAtPixel_ = function(pixel, map) {
+  var found = null;
+
+  var intersectingFeature = map.forEachFeatureAtPixel(pixel,
+      function(feature) {
+        return feature;
+      }, this, this.layerFilter_);
+
+  if (this.features_ &&
+      ol.array.includes(this.features_.getArray(), intersectingFeature)) {
+    found = intersectingFeature;
+  }
+
+  return found;
+};
+
+goog.provide('ol.layer.Heatmap');
+
+goog.require('goog.asserts');
+goog.require('ol.events');
+goog.require('ol');
+goog.require('ol.Object');
+goog.require('ol.dom');
+goog.require('ol.layer.Vector');
+goog.require('ol.math');
+goog.require('ol.object');
+goog.require('ol.render.EventType');
+goog.require('ol.style.Icon');
+goog.require('ol.style.Style');
+
+
+/**
+ * @enum {string}
+ */
+ol.layer.HeatmapLayerProperty = {
+  BLUR: 'blur',
+  GRADIENT: 'gradient',
+  RADIUS: 'radius'
+};
+
+
+/**
+ * @classdesc
+ * Layer for rendering vector data as a heatmap.
+ * Note that any property set in the options is set as a {@link ol.Object}
+ * property on the layer object; for example, setting `title: 'My Title'` in the
+ * options means that `title` is observable, and has get/set accessors.
+ *
+ * @constructor
+ * @extends {ol.layer.Vector}
+ * @fires ol.render.Event
+ * @param {olx.layer.HeatmapOptions=} opt_options Options.
+ * @api
+ */
+ol.layer.Heatmap = function(opt_options) {
+  var options = opt_options ? opt_options : {};
+
+  var baseOptions = ol.object.assign({}, options);
+
+  delete baseOptions.gradient;
+  delete baseOptions.radius;
+  delete baseOptions.blur;
+  delete baseOptions.shadow;
+  delete baseOptions.weight;
+  ol.layer.Vector.call(this, /** @type {olx.layer.VectorOptions} */ (baseOptions));
+
+  /**
+   * @private
+   * @type {Uint8ClampedArray}
+   */
+  this.gradient_ = null;
+
+  /**
+   * @private
+   * @type {number}
+   */
+  this.shadow_ = options.shadow !== undefined ? options.shadow : 250;
+
+  /**
+   * @private
+   * @type {string|undefined}
+   */
+  this.circleImage_ = undefined;
+
+  /**
+   * @private
+   * @type {Array.<Array.<ol.style.Style>>}
+   */
+  this.styleCache_ = null;
+
+  ol.events.listen(this,
+      ol.Object.getChangeEventType(ol.layer.HeatmapLayerProperty.GRADIENT),
+      this.handleGradientChanged_, this);
+
+  this.setGradient(options.gradient ?
+      options.gradient : ol.layer.Heatmap.DEFAULT_GRADIENT);
+
+  this.setBlur(options.blur !== undefined ? options.blur : 15);
+
+  this.setRadius(options.radius !== undefined ? options.radius : 8);
+
+  ol.events.listen(this,
+      ol.Object.getChangeEventType(ol.layer.HeatmapLayerProperty.BLUR),
+      this.handleStyleChanged_, this);
+  ol.events.listen(this,
+      ol.Object.getChangeEventType(ol.layer.HeatmapLayerProperty.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;
+  }
+  goog.asserts.assert(typeof weightFunction === 'function',
+      'weightFunction should be a function');
+
+  this.setStyle(function(feature, resolution) {
+    goog.asserts.assert(this.styleCache_, 'this.styleCache_ expected');
+    goog.asserts.assert(this.circleImage_ !== undefined,
+        'this.circleImage_ should be defined');
+    var weight = weightFunction(feature);
+    var opacity = weight !== undefined ? ol.math.clamp(weight, 0, 1) : 1;
+    // cast to 8 bits
+    var index = (255 * opacity) | 0;
+    var style = this.styleCache_[index];
+    if (!style) {
+      style = [
+        new ol.style.Style({
+          image: new ol.style.Icon({
+            opacity: opacity,
+            src: this.circleImage_
+          })
+        })
+      ];
+      this.styleCache_[index] = style;
+    }
+    return style;
+  }.bind(this));
+
+  // For performance reasons, don't sort the features before rendering.
+  // The render order is not relevant for a heatmap representation.
+  this.setRenderOrder(null);
+
+  ol.events.listen(this, ol.render.EventType.RENDER, this.handleRender_, this);
+
+};
+ol.inherits(ol.layer.Heatmap, ol.layer.Vector);
+
+
+/**
+ * @const
+ * @type {Array.<string>}
+ */
+ol.layer.Heatmap.DEFAULT_GRADIENT = ['#00f', '#0ff', '#0f0', '#ff0', '#f00'];
+
+
+/**
+ * @param {Array.<string>} colors A list of colored.
+ * @return {Uint8ClampedArray} An array.
+ * @private
+ */
+ol.layer.Heatmap.createGradient_ = function(colors) {
+  var width = 1;
+  var height = 256;
+  var context = ol.dom.createCanvasContext2D(width, height);
+
+  var gradient = context.createLinearGradient(0, 0, width, height);
+  var step = 1 / (colors.length - 1);
+  for (var i = 0, ii = colors.length; i < ii; ++i) {
+    gradient.addColorStop(i * step, colors[i]);
+  }
+
+  context.fillStyle = gradient;
+  context.fillRect(0, 0, width, height);
+
+  return context.getImageData(0, 0, width, height).data;
+};
+
+
+/**
+ * @return {string} Data URL for a circle.
+ * @private
+ */
+ol.layer.Heatmap.prototype.createCircle_ = function() {
+  var radius = this.getRadius();
+  var blur = this.getBlur();
+  goog.asserts.assert(radius !== undefined && blur !== undefined,
+      'radius and blur should be defined');
+  var halfSize = radius + blur + 1;
+  var size = 2 * halfSize;
+  var context = ol.dom.createCanvasContext2D(size, size);
+  context.shadowOffsetX = context.shadowOffsetY = this.shadow_;
+  context.shadowBlur = blur;
+  context.shadowColor = '#000';
+  context.beginPath();
+  var center = halfSize - this.shadow_;
+  context.arc(center, center, radius, 0, Math.PI * 2, true);
+  context.fill();
+  return context.canvas.toDataURL();
+};
+
+
+/**
+ * Return the blur size in pixels.
+ * @return {number} Blur size in pixels.
+ * @api
+ * @observable
+ */
+ol.layer.Heatmap.prototype.getBlur = function() {
+  return /** @type {number} */ (this.get(ol.layer.HeatmapLayerProperty.BLUR));
+};
+
+
+/**
+ * Return the gradient colors as array of strings.
+ * @return {Array.<string>} Colors.
+ * @api
+ * @observable
+ */
+ol.layer.Heatmap.prototype.getGradient = function() {
+  return /** @type {Array.<string>} */ (
+      this.get(ol.layer.HeatmapLayerProperty.GRADIENT));
+};
+
+
+/**
+ * Return the size of the radius in pixels.
+ * @return {number} Radius size in pixel.
+ * @api
+ * @observable
+ */
+ol.layer.Heatmap.prototype.getRadius = function() {
+  return /** @type {number} */ (this.get(ol.layer.HeatmapLayerProperty.RADIUS));
+};
+
+
+/**
+ * @private
+ */
+ol.layer.Heatmap.prototype.handleGradientChanged_ = function() {
+  this.gradient_ = ol.layer.Heatmap.createGradient_(this.getGradient());
+};
+
+
+/**
+ * @private
+ */
+ol.layer.Heatmap.prototype.handleStyleChanged_ = function() {
+  this.circleImage_ = this.createCircle_();
+  this.styleCache_ = new Array(256);
+  this.changed();
+};
+
+
+/**
+ * @param {ol.render.Event} event Post compose event
+ * @private
+ */
+ol.layer.Heatmap.prototype.handleRender_ = function(event) {
+  goog.asserts.assert(event.type == ol.render.EventType.RENDER,
+      'event.type should be RENDER');
+  goog.asserts.assert(this.gradient_, 'this.gradient_ expected');
+  var context = event.context;
+  var canvas = context.canvas;
+  var image = context.getImageData(0, 0, canvas.width, canvas.height);
+  var view8 = image.data;
+  var i, ii, alpha;
+  for (i = 0, ii = view8.length; i < ii; i += 4) {
+    alpha = view8[i + 3] * 4;
+    if (alpha) {
+      view8[i] = this.gradient_[alpha];
+      view8[i + 1] = this.gradient_[alpha + 1];
+      view8[i + 2] = this.gradient_[alpha + 2];
+    }
+  }
+  context.putImageData(image, 0, 0);
+};
+
+
+/**
+ * Set the blur size in pixels.
+ * @param {number} blur Blur size in pixels.
+ * @api
+ * @observable
+ */
+ol.layer.Heatmap.prototype.setBlur = function(blur) {
+  this.set(ol.layer.HeatmapLayerProperty.BLUR, blur);
+};
+
+
+/**
+ * Set the gradient colors as array of strings.
+ * @param {Array.<string>} colors Gradient.
+ * @api
+ * @observable
+ */
+ol.layer.Heatmap.prototype.setGradient = function(colors) {
+  this.set(ol.layer.HeatmapLayerProperty.GRADIENT, colors);
+};
+
+
+/**
+ * Set the size of the radius in pixels.
+ * @param {number} radius Radius size in pixel.
+ * @api
+ * @observable
+ */
+ol.layer.Heatmap.prototype.setRadius = function(radius) {
+  this.set(ol.layer.HeatmapLayerProperty.RADIUS, radius);
+};
+
+goog.provide('ol.net');
+
+
+/**
+ * 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'.
+ */
+ol.net.jsonp = function(url, callback, opt_errback, opt_callbackParam) {
+  var script = ol.global.document.createElement('script');
+  var key = 'olc_' + goog.getUid(callback);
+  function cleanup() {
+    delete ol.global[key];
+    script.parentNode.removeChild(script);
+  }
+  script.async = true;
+  script.src = url + (url.indexOf('?') == -1 ? '?' : '&') +
+      (opt_callbackParam || 'callback') + '=' + key;
+  var timer = ol.global.setTimeout(function() {
+    cleanup();
+    if (opt_errback) {
+      opt_errback();
+    }
+  }, 10000);
+  ol.global[key] = function(data) {
+    ol.global.clearTimeout(timer);
+    cleanup();
+    callback(data);
+  };
+  ol.global.document.getElementsByTagName('head')[0].appendChild(script);
+};
+
+goog.provide('ol.render');
+
+goog.require('goog.vec.Mat4');
+goog.require('ol.render.canvas.Immediate');
+goog.require('ol.vec.Mat4');
+
+
+/**
+ * Binds a Canvas Immediate API to a canvas context, to allow drawing geometries
+ * to the context's canvas.
+ *
+ * The units for geometry coordinates are css pixels relative to the top left
+ * corner of the canvas element.
+ * ```js
+ * var canvas = document.createElement('canvas');
+ * var render = ol.render.toContext(canvas.getContext('2d'),
+ *     { size: [100, 100] });
+ * render.setFillStrokeStyle(new ol.style.Fill({ color: blue }));
+ * render.drawPolygon(
+ *     new ol.geom.Polygon([[[0, 0], [100, 100], [100, 0], [0, 0]]]));
+ * ```
+ *
+ * @param {CanvasRenderingContext2D} context Canvas context.
+ * @param {olx.render.ToContextOptions=} opt_options Options.
+ * @return {ol.render.canvas.Immediate} Canvas Immediate.
+ * @api
+ */
+ol.render.toContext = function(context, opt_options) {
+  var canvas = context.canvas;
+  var options = opt_options ? opt_options : {};
+  var pixelRatio = options.pixelRatio || ol.has.DEVICE_PIXEL_RATIO;
+  var size = options.size;
+  if (size) {
+    canvas.width = size[0] * pixelRatio;
+    canvas.height = size[1] * pixelRatio;
+    canvas.style.width = size[0] + 'px';
+    canvas.style.height = size[1] + 'px';
+  }
+  var extent = [0, 0, canvas.width, canvas.height];
+  var transform = ol.vec.Mat4.makeTransform2D(goog.vec.Mat4.createNumber(),
+      0, 0, pixelRatio, pixelRatio, 0, 0, 0);
+  return new ol.render.canvas.Immediate(context, pixelRatio, extent, transform,
+      0);
+};
+
+goog.provide('ol.reproj.Tile');
+
+goog.require('goog.asserts');
+goog.require('ol.Tile');
+goog.require('ol.TileState');
+goog.require('ol.events');
+goog.require('ol.events.EventType');
+goog.require('ol.extent');
+goog.require('ol.math');
+goog.require('ol.object');
+goog.require('ol.proj');
+goog.require('ol.reproj');
+goog.require('ol.reproj.Triangulation');
+
+
+/**
+ * @classdesc
+ * Class encapsulating single reprojected tile.
+ * See {@link ol.source.TileImage}.
+ *
+ * @constructor
+ * @extends {ol.Tile}
+ * @param {ol.proj.Projection} sourceProj Source projection.
+ * @param {ol.tilegrid.TileGrid} sourceTileGrid Source tile grid.
+ * @param {ol.proj.Projection} targetProj Target projection.
+ * @param {ol.tilegrid.TileGrid} targetTileGrid Target tile grid.
+ * @param {ol.TileCoord} tileCoord Coordinate of the tile.
+ * @param {ol.TileCoord} wrappedTileCoord Coordinate of the tile wrapped in X.
+ * @param {number} pixelRatio Pixel ratio.
+ * @param {number} gutter Gutter of the source tiles.
+ * @param {ol.ReprojTileFunctionType} getTileFunction
+ *     Function returning source tiles (z, x, y, pixelRatio).
+ * @param {number=} opt_errorThreshold Acceptable reprojection error (in px).
+ * @param {boolean=} opt_renderEdges Render reprojection edges.
+ */
+ol.reproj.Tile = function(sourceProj, sourceTileGrid,
+    targetProj, targetTileGrid, tileCoord, wrappedTileCoord,
+    pixelRatio, gutter, getTileFunction,
+    opt_errorThreshold,
+    opt_renderEdges) {
+  ol.Tile.call(this, tileCoord, ol.TileState.IDLE);
+
+  /**
+   * @private
+   * @type {boolean}
+   */
+  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 {Object.<number, HTMLCanvasElement>}
+   */
+  this.canvasByContext_ = {};
+
+  /**
+   * @private
+   * @type {ol.tilegrid.TileGrid}
+   */
+  this.sourceTileGrid_ = sourceTileGrid;
+
+  /**
+   * @private
+   * @type {ol.tilegrid.TileGrid}
+   */
+  this.targetTileGrid_ = targetTileGrid;
+
+  /**
+   * @private
+   * @type {ol.TileCoord}
+   */
+  this.wrappedTileCoord_ = wrappedTileCoord ? wrappedTileCoord : tileCoord;
+
+  /**
+   * @private
+   * @type {!Array.<ol.Tile>}
+   */
+  this.sourceTiles_ = [];
+
+  /**
+   * @private
+   * @type {Array.<ol.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 ?
+      ol.extent.getIntersection(targetExtent, maxTargetExtent) : targetExtent;
+
+  if (ol.extent.getArea(limitedTargetExtent) === 0) {
+    // Tile is completely outside range -> EMPTY
+    // TODO: is it actually correct that the source even creates the tile ?
+    this.state = ol.TileState.EMPTY;
+    return;
+  }
+
+  var sourceProjExtent = sourceProj.getExtent();
+  if (sourceProjExtent) {
+    if (!maxSourceExtent) {
+      maxSourceExtent = sourceProjExtent;
+    } else {
+      maxSourceExtent = ol.extent.getIntersection(
+          maxSourceExtent, sourceProjExtent);
+    }
+  }
+
+  var targetResolution = targetTileGrid.getResolution(
+      this.wrappedTileCoord_[0]);
+
+  var targetCenter = ol.extent.getCenter(limitedTargetExtent);
+  var sourceResolution = ol.reproj.calculateSourceResolution(
+      sourceProj, targetProj, targetCenter, targetResolution);
+
+  if (!isFinite(sourceResolution) || sourceResolution <= 0) {
+    // invalid sourceResolution -> EMPTY
+    // probably edges of the projections when no extent is defined
+    this.state = ol.TileState.EMPTY;
+    return;
+  }
+
+  var errorThresholdInPixels = opt_errorThreshold !== undefined ?
+      opt_errorThreshold : ol.DEFAULT_RASTER_REPROJECTION_ERROR_THRESHOLD;
+
+  /**
+   * @private
+   * @type {!ol.reproj.Triangulation}
+   */
+  this.triangulation_ = new ol.reproj.Triangulation(
+      sourceProj, targetProj, limitedTargetExtent, maxSourceExtent,
+      sourceResolution * errorThresholdInPixels);
+
+  if (this.triangulation_.getTriangles().length === 0) {
+    // no valid triangles -> EMPTY
+    this.state = ol.TileState.EMPTY;
+    return;
+  }
+
+  this.sourceZ_ = sourceTileGrid.getZForResolution(sourceResolution);
+  var sourceExtent = this.triangulation_.calculateSourceExtent();
+
+  if (maxSourceExtent) {
+    if (sourceProj.canWrapX()) {
+      sourceExtent[1] = ol.math.clamp(
+          sourceExtent[1], maxSourceExtent[1], maxSourceExtent[3]);
+      sourceExtent[3] = ol.math.clamp(
+          sourceExtent[3], maxSourceExtent[1], maxSourceExtent[3]);
+    } else {
+      sourceExtent = ol.extent.getIntersection(sourceExtent, maxSourceExtent);
+    }
+  }
+
+  if (!ol.extent.getArea(sourceExtent)) {
+    this.state = ol.TileState.EMPTY;
+  } else {
+    var sourceRange = sourceTileGrid.getTileRangeForExtentAndZ(
+        sourceExtent, this.sourceZ_);
+
+    var tilesRequired = sourceRange.getWidth() * sourceRange.getHeight();
+    if (!goog.asserts.assert(
+        tilesRequired < ol.RASTER_REPROJECTION_MAX_SOURCE_TILES,
+        'reasonable number of tiles is required')) {
+      this.state = ol.TileState.ERROR;
+      return;
+    }
+    for (var srcX = sourceRange.minX; srcX <= sourceRange.maxX; srcX++) {
+      for (var srcY = sourceRange.minY; srcY <= sourceRange.maxY; srcY++) {
+        var tile = getTileFunction(this.sourceZ_, srcX, srcY, pixelRatio);
+        if (tile) {
+          this.sourceTiles_.push(tile);
+        }
+      }
+    }
+
+    if (this.sourceTiles_.length === 0) {
+      this.state = ol.TileState.EMPTY;
+    }
+  }
+};
+ol.inherits(ol.reproj.Tile, ol.Tile);
+
+
+/**
+ * @inheritDoc
+ */
+ol.reproj.Tile.prototype.disposeInternal = function() {
+  if (this.state == ol.TileState.LOADING) {
+    this.unlistenSources_();
+  }
+  ol.Tile.prototype.disposeInternal.call(this);
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.reproj.Tile.prototype.getImage = function(opt_context) {
+  if (opt_context !== undefined) {
+    var image;
+    var key = goog.getUid(opt_context);
+    if (key in this.canvasByContext_) {
+      return this.canvasByContext_[key];
+    } else if (ol.object.isEmpty(this.canvasByContext_)) {
+      image = this.canvas_;
+    } else {
+      image = /** @type {HTMLCanvasElement} */ (this.canvas_.cloneNode(false));
+    }
+    this.canvasByContext_[key] = image;
+    return image;
+  } else {
+    return this.canvas_;
+  }
+};
+
+
+/**
+ * @private
+ */
+ol.reproj.Tile.prototype.reproject_ = function() {
+  var sources = [];
+  this.sourceTiles_.forEach(function(tile, i, arr) {
+    if (tile && tile.getState() == ol.TileState.LOADED) {
+      sources.push({
+        extent: this.sourceTileGrid_.getTileCoordExtent(tile.tileCoord),
+        image: tile.getImage()
+      });
+    }
+  }, this);
+  this.sourceTiles_.length = 0;
+
+  if (sources.length === 0) {
+    this.state = ol.TileState.ERROR;
+  } else {
+    var z = this.wrappedTileCoord_[0];
+    var size = this.targetTileGrid_.getTileSize(z);
+    var width = goog.isNumber(size) ? size : size[0];
+    var height = goog.isNumber(size) ? size : size[1];
+    var targetResolution = this.targetTileGrid_.getResolution(z);
+    var sourceResolution = this.sourceTileGrid_.getResolution(this.sourceZ_);
+
+    var targetExtent = this.targetTileGrid_.getTileCoordExtent(
+        this.wrappedTileCoord_);
+    this.canvas_ = ol.reproj.render(width, height, this.pixelRatio_,
+        sourceResolution, this.sourceTileGrid_.getExtent(),
+        targetResolution, targetExtent, this.triangulation_, sources,
+        this.gutter_, this.renderEdges_);
+
+    this.state = ol.TileState.LOADED;
+  }
+  this.changed();
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.reproj.Tile.prototype.load = function() {
+  if (this.state == ol.TileState.IDLE) {
+    this.state = ol.TileState.LOADING;
+    this.changed();
+
+    var leftToLoad = 0;
+
+    goog.asserts.assert(!this.sourcesListenerKeys_,
+        'this.sourcesListenerKeys_ should be null');
+
+    this.sourcesListenerKeys_ = [];
+    this.sourceTiles_.forEach(function(tile, i, arr) {
+      var state = tile.getState();
+      if (state == ol.TileState.IDLE || state == ol.TileState.LOADING) {
+        leftToLoad++;
+
+        var sourceListenKey;
+        sourceListenKey = ol.events.listen(tile, ol.events.EventType.CHANGE,
+            function(e) {
+              var state = tile.getState();
+              if (state == ol.TileState.LOADED ||
+                  state == ol.TileState.ERROR ||
+                  state == ol.TileState.EMPTY) {
+                ol.events.unlistenByKey(sourceListenKey);
+                leftToLoad--;
+                goog.asserts.assert(leftToLoad >= 0,
+                    'leftToLoad should not be negative');
+                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 == ol.TileState.IDLE) {
+        tile.load();
+      }
+    });
+
+    if (leftToLoad === 0) {
+      ol.global.setTimeout(this.reproject_.bind(this), 0);
+    }
+  }
+};
+
+
+/**
+ * @private
+ */
+ol.reproj.Tile.prototype.unlistenSources_ = function() {
+  goog.asserts.assert(this.sourcesListenerKeys_,
+      'this.sourcesListenerKeys_ should not be null');
+  this.sourcesListenerKeys_.forEach(ol.events.unlistenByKey);
+  this.sourcesListenerKeys_ = null;
+};
+
+goog.provide('ol.source.TileImage');
+
+goog.require('goog.asserts');
+goog.require('ol.ImageTile');
+goog.require('ol.TileCache');
+goog.require('ol.TileState');
+goog.require('ol.events');
+goog.require('ol.events.EventType');
+goog.require('ol.proj');
+goog.require('ol.reproj.Tile');
+goog.require('ol.source.UrlTile');
+
+
+/**
+ * @classdesc
+ * Base class for sources providing images divided into a tile grid.
+ *
+ * @constructor
+ * @fires ol.source.TileEvent
+ * @extends {ol.source.UrlTile}
+ * @param {olx.source.TileImageOptions} options Image tile options.
+ * @api
+ */
+ol.source.TileImage = function(options) {
+
+  ol.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 : ol.source.TileImage.defaultTileLoadFunction,
+    tilePixelRatio: options.tilePixelRatio,
+    tileUrlFunction: options.tileUrlFunction,
+    url: options.url,
+    urls: options.urls,
+    wrapX: options.wrapX
+  });
+
+  /**
+   * @protected
+   * @type {?string}
+   */
+  this.crossOrigin =
+      options.crossOrigin !== undefined ? options.crossOrigin : null;
+
+  /**
+   * @protected
+   * @type {function(new: ol.ImageTile, ol.TileCoord, ol.TileState, string,
+   *        ?string, ol.TileLoadFunctionType)}
+   */
+  this.tileClass = options.tileClass !== undefined ?
+      options.tileClass : ol.ImageTile;
+
+  /**
+   * @protected
+   * @type {Object.<string, ol.TileCache>}
+   */
+  this.tileCacheForProjection = {};
+
+  /**
+   * @protected
+   * @type {Object.<string, ol.tilegrid.TileGrid>}
+   */
+  this.tileGridForProjection = {};
+
+  /**
+   * @private
+   * @type {number|undefined}
+   */
+  this.reprojectionErrorThreshold_ = options.reprojectionErrorThreshold;
+
+  /**
+   * @private
+   * @type {boolean}
+   */
+  this.renderReprojectionEdges_ = false;
+};
+ol.inherits(ol.source.TileImage, ol.source.UrlTile);
+
+
+/**
+ * @inheritDoc
+ */
+ol.source.TileImage.prototype.canExpireCache = function() {
+  if (!ol.ENABLE_RASTER_REPROJECTION) {
+    return ol.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
+ */
+ol.source.TileImage.prototype.expireCache = function(projection, usedTiles) {
+  if (!ol.ENABLE_RASTER_REPROJECTION) {
+    ol.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
+ */
+ol.source.TileImage.prototype.getGutter = function(projection) {
+  if (ol.ENABLE_RASTER_REPROJECTION &&
+      this.getProjection() && projection &&
+      !ol.proj.equivalent(this.getProjection(), projection)) {
+    return 0;
+  } else {
+    return this.getGutterInternal();
+  }
+};
+
+
+/**
+ * @protected
+ * @return {number} Gutter.
+ */
+ol.source.TileImage.prototype.getGutterInternal = function() {
+  return 0;
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.source.TileImage.prototype.getOpaque = function(projection) {
+  if (ol.ENABLE_RASTER_REPROJECTION &&
+      this.getProjection() && projection &&
+      !ol.proj.equivalent(this.getProjection(), projection)) {
+    return false;
+  } else {
+    return ol.source.UrlTile.prototype.getOpaque.call(this, projection);
+  }
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.source.TileImage.prototype.getTileGridForProjection = function(projection) {
+  if (!ol.ENABLE_RASTER_REPROJECTION) {
+    return ol.source.UrlTile.prototype.getTileGridForProjection.call(this, projection);
+  }
+  var thisProj = this.getProjection();
+  if (this.tileGrid &&
+      (!thisProj || ol.proj.equivalent(thisProj, projection))) {
+    return this.tileGrid;
+  } else {
+    var projKey = goog.getUid(projection).toString();
+    if (!(projKey in this.tileGridForProjection)) {
+      this.tileGridForProjection[projKey] =
+          ol.tilegrid.getForProjection(projection);
+    }
+    return this.tileGridForProjection[projKey];
+  }
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.source.TileImage.prototype.getTileCacheForProjection = function(projection) {
+  if (!ol.ENABLE_RASTER_REPROJECTION) {
+    return ol.source.UrlTile.prototype.getTileCacheForProjection.call(this, projection);
+  }
+  var thisProj = this.getProjection();
+  if (!thisProj || ol.proj.equivalent(thisProj, projection)) {
+    return this.tileCache;
+  } else {
+    var projKey = goog.getUid(projection).toString();
+    if (!(projKey in this.tileCacheForProjection)) {
+      this.tileCacheForProjection[projKey] = new ol.TileCache();
+    }
+    return this.tileCacheForProjection[projKey];
+  }
+};
+
+
+/**
+ * @param {number} z Tile coordinate z.
+ * @param {number} x Tile coordinate x.
+ * @param {number} y Tile coordinate y.
+ * @param {number} pixelRatio Pixel ratio.
+ * @param {ol.proj.Projection} projection Projection.
+ * @param {string} key The key set on the tile.
+ * @return {ol.Tile} Tile.
+ * @private
+ */
+ol.source.TileImage.prototype.createTile_ = function(z, x, y, pixelRatio, projection, key) {
+  var tileCoord = [z, x, y];
+  var urlTileCoord = this.getTileCoordForTileUrlFunction(
+      tileCoord, projection);
+  var tileUrl = urlTileCoord ?
+      this.tileUrlFunction(urlTileCoord, pixelRatio, projection) : undefined;
+  var tile = new this.tileClass(
+      tileCoord,
+      tileUrl !== undefined ? ol.TileState.IDLE : ol.TileState.EMPTY,
+      tileUrl !== undefined ? tileUrl : '',
+      this.crossOrigin,
+      this.tileLoadFunction);
+  tile.key = key;
+  ol.events.listen(tile, ol.events.EventType.CHANGE,
+      this.handleTileChange, this);
+  return tile;
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.source.TileImage.prototype.getTile = function(z, x, y, pixelRatio, projection) {
+  if (!ol.ENABLE_RASTER_REPROJECTION ||
+      !this.getProjection() ||
+      !projection ||
+      ol.proj.equivalent(this.getProjection(), projection)) {
+    return this.getTileInternal(z, x, y, pixelRatio, projection);
+  } else {
+    var cache = this.getTileCacheForProjection(projection);
+    var tileCoord = [z, x, y];
+    var tileCoordKey = this.getKeyZXY.apply(this, tileCoord);
+    if (cache.containsKey(tileCoordKey)) {
+      return /** @type {!ol.Tile} */ (cache.get(tileCoordKey));
+    } else {
+      var sourceProjection = this.getProjection();
+      var sourceTileGrid = this.getTileGridForProjection(sourceProjection);
+      var targetTileGrid = this.getTileGridForProjection(projection);
+      var wrappedTileCoord =
+          this.getTileCoordForTileUrlFunction(tileCoord, projection);
+      var tile = new ol.reproj.Tile(
+          sourceProjection, sourceTileGrid,
+          projection, targetTileGrid,
+          tileCoord, wrappedTileCoord, this.getTilePixelRatio(pixelRatio),
+          this.getGutterInternal(),
+          function(z, x, y, pixelRatio) {
+            return this.getTileInternal(z, x, y, pixelRatio, sourceProjection);
+          }.bind(this), this.reprojectionErrorThreshold_,
+          this.renderReprojectionEdges_);
+
+      cache.set(tileCoordKey, tile);
+      return tile;
+    }
+  }
+};
+
+
+/**
+ * @param {number} z Tile coordinate z.
+ * @param {number} x Tile coordinate x.
+ * @param {number} y Tile coordinate y.
+ * @param {number} pixelRatio Pixel ratio.
+ * @param {ol.proj.Projection} projection Projection.
+ * @return {!ol.Tile} Tile.
+ * @protected
+ */
+ol.source.TileImage.prototype.getTileInternal = function(z, x, y, pixelRatio, projection) {
+  var /** @type {ol.Tile} */ tile = null;
+  var tileCoordKey = this.getKeyZXY(z, x, y);
+  var key = this.getKey();
+  if (!this.tileCache.containsKey(tileCoordKey)) {
+    goog.asserts.assert(projection, 'argument projection is truthy');
+    tile = this.createTile_(z, x, y, pixelRatio, projection, key);
+    this.tileCache.set(tileCoordKey, tile);
+  } else {
+    tile = /** @type {!ol.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 /** @type {ol.Tile} */ interimTile = tile;
+      if (tile.interimTile && tile.interimTile.key == key) {
+        goog.asserts.assert(tile.interimTile.getState() == ol.TileState.LOADED);
+        goog.asserts.assert(tile.interimTile.interimTile === null);
+        tile = tile.interimTile;
+        if (interimTile.getState() == ol.TileState.LOADED) {
+          tile.interimTile = interimTile;
+        }
+      } else {
+        tile = this.createTile_(z, x, y, pixelRatio, projection, key);
+        if (interimTile.getState() == ol.TileState.LOADED) {
+          tile.interimTile = interimTile;
+        } else if (interimTile.interimTile &&
+            interimTile.interimTile.getState() == ol.TileState.LOADED) {
+          tile.interimTile = interimTile.interimTile;
+          interimTile.interimTile = null;
+        }
+      }
+      if (tile.interimTile) {
+        tile.interimTile.interimTile = null;
+      }
+      this.tileCache.replace(tileCoordKey, tile);
+    }
+  }
+  goog.asserts.assert(tile);
+  return tile;
+};
+
+
+/**
+ * Sets whether to render reprojection edges or not (usually for debugging).
+ * @param {boolean} render Render the edges.
+ * @api
+ */
+ol.source.TileImage.prototype.setRenderReprojectionEdges = function(render) {
+  if (!ol.ENABLE_RASTER_REPROJECTION ||
+      this.renderReprojectionEdges_ == render) {
+    return;
+  }
+  this.renderReprojectionEdges_ = render;
+  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, resolutions, ...).
+ *
+ * @param {ol.ProjectionLike} projection Projection.
+ * @param {ol.tilegrid.TileGrid} tilegrid Tile grid to use for the projection.
+ * @api
+ */
+ol.source.TileImage.prototype.setTileGridForProjection = function(projection, tilegrid) {
+  if (ol.ENABLE_RASTER_REPROJECTION) {
+    var proj = ol.proj.get(projection);
+    if (proj) {
+      var projKey = goog.getUid(proj).toString();
+      if (!(projKey in this.tileGridForProjection)) {
+        this.tileGridForProjection[projKey] = tilegrid;
+      }
+    }
+  }
+};
+
+
+/**
+ * @param {ol.ImageTile} imageTile Image tile.
+ * @param {string} src Source.
+ */
+ol.source.TileImage.defaultTileLoadFunction = function(imageTile, src) {
+  imageTile.getImage().src = src;
+};
+
+goog.provide('ol.source.BingMaps');
+
+goog.require('goog.asserts');
+goog.require('ol.Attribution');
+goog.require('ol.TileRange');
+goog.require('ol.TileUrlFunction');
+goog.require('ol.extent');
+goog.require('ol.net');
+goog.require('ol.proj');
+goog.require('ol.source.State');
+goog.require('ol.source.TileImage');
+goog.require('ol.tilecoord');
+
+
+/**
+ * @classdesc
+ * Layer source for Bing Maps tile data.
+ *
+ * @constructor
+ * @extends {ol.source.TileImage}
+ * @param {olx.source.BingMapsOptions} options Bing Maps options.
+ * @api stable
+ */
+ol.source.BingMaps = function(options) {
+
+  ol.source.TileImage.call(this, {
+    cacheSize: options.cacheSize,
+    crossOrigin: 'anonymous',
+    opaque: true,
+    projection: ol.proj.get('EPSG:3857'),
+    reprojectionErrorThreshold: options.reprojectionErrorThreshold,
+    state: ol.source.State.LOADING,
+    tileLoadFunction: options.tileLoadFunction,
+    wrapX: options.wrapX !== undefined ? options.wrapX : true
+  });
+
+  /**
+   * @private
+   * @type {string}
+   */
+  this.culture_ = options.culture !== undefined ? options.culture : 'en-us';
+
+  /**
+   * @private
+   * @type {number}
+   */
+  this.maxZoom_ = options.maxZoom !== undefined ? options.maxZoom : -1;
+
+  var url = 'https://dev.virtualearth.net/REST/v1/Imagery/Metadata/' +
+      options.imagerySet +
+      '?uriScheme=https&include=ImageryProviders&key=' + options.key;
+
+  ol.net.jsonp(url, this.handleImageryMetadataResponse.bind(this), undefined,
+      'jsonp');
+
+};
+ol.inherits(ol.source.BingMaps, ol.source.TileImage);
+
+
+/**
+ * The attribution containing a link to the Microsoft® Bing™ Maps Platform APIs’
+ * Terms Of Use.
+ * @const
+ * @type {ol.Attribution}
+ * @api
+ */
+ol.source.BingMaps.TOS_ATTRIBUTION = new ol.Attribution({
+  html: '<a class="ol-attribution-bing-tos" ' +
+      'href="http://www.microsoft.com/maps/product/terms.html">' +
+      'Terms of Use</a>'
+});
+
+
+/**
+ * @param {BingMapsImageryMetadataResponse} response Response.
+ */
+ol.source.BingMaps.prototype.handleImageryMetadataResponse = function(response) {
+
+  if (response.statusCode != 200 ||
+      response.statusDescription != 'OK' ||
+      response.authenticationResultCode != 'ValidCredentials' ||
+      response.resourceSets.length != 1 ||
+      response.resourceSets[0].resources.length != 1) {
+    this.setState(ol.source.State.ERROR);
+    return;
+  }
+
+  var brandLogoUri = response.brandLogoUri;
+  if (brandLogoUri.indexOf('https') == -1) {
+    brandLogoUri = brandLogoUri.replace('http', 'https');
+  }
+  //var copyright = response.copyright;  // FIXME do we need to display this?
+  var resource = response.resourceSets[0].resources[0];
+  goog.asserts.assert(resource.imageWidth == resource.imageHeight,
+      'resource has imageWidth equal to imageHeight, i.e. is square');
+  var maxZoom = this.maxZoom_ == -1 ? resource.zoomMax : this.maxZoom_;
+
+  var sourceProjection = this.getProjection();
+  var extent = ol.tilegrid.extentFromProjection(sourceProjection);
+  var tileSize = resource.imageWidth == resource.imageHeight ?
+      resource.imageWidth : [resource.imageWidth, resource.imageHeight];
+  var tileGrid = ol.tilegrid.createXYZ({
+    extent: extent,
+    minZoom: resource.zoomMin,
+    maxZoom: maxZoom,
+    tileSize: tileSize
+  });
+  this.tileGrid = tileGrid;
+
+  var culture = this.culture_;
+  this.tileUrlFunction = ol.TileUrlFunction.createFromTileUrlFunctions(
+      resource.imageUrlSubdomains.map(function(subdomain) {
+        var quadKeyTileCoord = [0, 0, 0];
+        var imageUrl = resource.imageUrl
+            .replace('{subdomain}', subdomain)
+            .replace('{culture}', culture);
+        return (
+            /**
+             * @param {ol.TileCoord} tileCoord Tile coordinate.
+             * @param {number} pixelRatio Pixel ratio.
+             * @param {ol.proj.Projection} projection Projection.
+             * @return {string|undefined} Tile URL.
+             */
+            function(tileCoord, pixelRatio, projection) {
+              goog.asserts.assert(ol.proj.equivalent(
+                  projection, sourceProjection),
+                  'projections are equivalent');
+              if (!tileCoord) {
+                return undefined;
+              } else {
+                ol.tilecoord.createOrUpdate(tileCoord[0], tileCoord[1],
+                    -tileCoord[2] - 1, quadKeyTileCoord);
+                return imageUrl.replace('{quadkey}', ol.tilecoord.quadKey(
+                    quadKeyTileCoord));
+              }
+            });
+      }));
+
+  if (resource.imageryProviders) {
+    var transform = ol.proj.getTransformFromProjections(
+        ol.proj.get('EPSG:4326'), this.getProjection());
+
+    var attributions = resource.imageryProviders.map(function(imageryProvider) {
+      var html = imageryProvider.attribution;
+      /** @type {Object.<string, Array.<ol.TileRange>>} */
+      var tileRanges = {};
+      imageryProvider.coverageAreas.forEach(function(coverageArea) {
+        var minZ = coverageArea.zoomMin;
+        var maxZ = Math.min(coverageArea.zoomMax, maxZoom);
+        var bbox = coverageArea.bbox;
+        var epsg4326Extent = [bbox[1], bbox[0], bbox[3], bbox[2]];
+        var extent = ol.extent.applyTransform(epsg4326Extent, transform);
+        var tileRange, z, zKey;
+        for (z = minZ; z <= maxZ; ++z) {
+          zKey = z.toString();
+          tileRange = tileGrid.getTileRangeForExtentAndZ(extent, z);
+          if (zKey in tileRanges) {
+            tileRanges[zKey].push(tileRange);
+          } else {
+            tileRanges[zKey] = [tileRange];
+          }
+        }
+      });
+      return new ol.Attribution({html: html, tileRanges: tileRanges});
+    });
+    attributions.push(ol.source.BingMaps.TOS_ATTRIBUTION);
+    this.setAttributions(attributions);
+  }
+
+  this.setLogo(brandLogoUri);
+
+  this.setState(ol.source.State.READY);
+
+};
+
+goog.provide('ol.source.XYZ');
+
+goog.require('ol.source.TileImage');
+
+
+/**
+ * @classdesc
+ * Layer source for tile data with URLs in a set XYZ format that are
+ * defined in a URL template. By default, this follows the widely-used
+ * Google grid where `x` 0 and `y` 0 are in the top left. Grids like
+ * TMS where `x` 0 and `y` 0 are in the bottom left can be used by
+ * using the `{-y}` placeholder in the URL template, so long as the
+ * source does not have a custom tile grid. In this case,
+ * {@link ol.source.TileImage} can be used with a `tileUrlFunction`
+ * such as:
+ *
+ *  tileUrlFunction: function(coordinate) {
+ *    return 'http://mapserver.com/' + coordinate[0] + '/' +
+ *        coordinate[1] + '/' + coordinate[2] + '.png';
+ *    }
+ *
+ *
+ * @constructor
+ * @extends {ol.source.TileImage}
+ * @param {olx.source.XYZOptions=} opt_options XYZ options.
+ * @api stable
+ */
+ol.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 :
+      ol.tilegrid.createXYZ({
+        extent: ol.tilegrid.extentFromProjection(projection),
+        maxZoom: options.maxZoom,
+        minZoom: options.minZoom,
+        tileSize: options.tileSize
+      });
+
+  ol.source.TileImage.call(this, {
+    attributions: options.attributions,
+    cacheSize: options.cacheSize,
+    crossOrigin: options.crossOrigin,
+    logo: options.logo,
+    opaque: options.opaque,
+    projection: projection,
+    reprojectionErrorThreshold: options.reprojectionErrorThreshold,
+    tileGrid: tileGrid,
+    tileLoadFunction: options.tileLoadFunction,
+    tilePixelRatio: options.tilePixelRatio,
+    tileUrlFunction: options.tileUrlFunction,
+    url: options.url,
+    urls: options.urls,
+    wrapX: options.wrapX !== undefined ? options.wrapX : true
+  });
+
+};
+ol.inherits(ol.source.XYZ, ol.source.TileImage);
+
+goog.provide('ol.source.CartoDB');
+
+goog.require('ol.object');
+goog.require('ol.source.State');
+goog.require('ol.source.XYZ');
+
+
+/**
+ * @classdesc
+ * Layer source for the CartoDB tiles.
+ *
+ * @constructor
+ * @extends {ol.source.XYZ}
+ * @param {olx.source.CartoDBOptions} options CartoDB options.
+ * @api
+ */
+ol.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_ = {};
+
+  ol.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: ol.source.State.LOADING,
+    wrapX: options.wrapX
+  });
+  this.initializeMap_();
+};
+ol.inherits(ol.source.CartoDB, ol.source.XYZ);
+
+
+/**
+ * Returns the current config.
+ * @return {!Object} The current configuration.
+ * @api
+ */
+ol.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
+ */
+ol.source.CartoDB.prototype.updateConfig = function(config) {
+  ol.object.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
+ */
+ol.source.CartoDB.prototype.setConfig = function(config) {
+  this.config_ = config || {};
+  this.initializeMap_();
+};
+
+
+/**
+ * Issue a request to initialize the CartoDB map.
+ * @private
+ */
+ol.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
+ */
+ol.source.CartoDB.prototype.handleInitResponse_ = function(paramHash, event) {
+  var client = /** @type {XMLHttpRequest} */ (event.target);
+  if (client.status >= 200 && client.status < 300) {
+    var response;
+    try {
+      response = /** @type {CartoDBLayerInfo} */(JSON.parse(client.responseText));
+    } catch (err) {
+      this.setState(ol.source.State.ERROR);
+      return;
+    }
+    this.applyTemplate_(response);
+    this.templateCache_[paramHash] = response;
+    this.setState(ol.source.State.READY);
+  } else {
+    this.setState(ol.source.State.ERROR);
+  }
+};
+
+
+/**
+ * @private
+ * @param {Event} event Event.
+ */
+ol.source.CartoDB.prototype.handleInitError_ = function(event) {
+  this.setState(ol.source.State.ERROR);
+};
+
+
+/**
+ * Apply the new tile urls returned by carto db
+ * @param {CartoDBLayerInfo} data Result of carto db call.
+ * @private
+ */
+ol.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 resolution ?
+// FIXME distance not respected because of the centroid
+
+goog.provide('ol.source.Cluster');
+
+goog.require('goog.asserts');
+goog.require('ol.Feature');
+goog.require('ol.coordinate');
+goog.require('ol.events.EventType');
+goog.require('ol.extent');
+goog.require('ol.geom.Point');
+goog.require('ol.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 {olx.source.ClusterOptions} options Constructor options.
+ * @extends {ol.source.Vector}
+ * @api
+ */
+ol.source.Cluster = function(options) {
+  ol.source.Vector.call(this, {
+    attributions: options.attributions,
+    extent: options.extent,
+    logo: options.logo,
+    projection: options.projection,
+    wrapX: options.wrapX
+  });
+
+  /**
+   * @type {number|undefined}
+   * @private
+   */
+  this.resolution_ = undefined;
+
+  /**
+   * @type {number}
+   * @private
+   */
+  this.distance_ = options.distance !== undefined ? options.distance : 20;
+
+  /**
+   * @type {Array.<ol.Feature>}
+   * @private
+   */
+  this.features_ = [];
+
+  /**
+   * @param {ol.Feature} feature Feature.
+   * @return {ol.geom.Point} Cluster calculation point.
+   */
+  this.geometryFunction_ = options.geometryFunction || function(feature) {
+    var geometry = feature.getGeometry();
+    goog.asserts.assert(geometry instanceof ol.geom.Point,
+        'feature geometry is a ol.geom.Point instance');
+    return geometry;
+  };
+
+  /**
+   * @type {ol.source.Vector}
+   * @private
+   */
+  this.source_ = options.source;
+
+  this.source_.on(ol.events.EventType.CHANGE,
+      ol.source.Cluster.prototype.onSourceChange_, this);
+};
+ol.inherits(ol.source.Cluster, ol.source.Vector);
+
+
+/**
+ * Get a reference to the wrapped source.
+ * @return {ol.source.Vector} Source.
+ * @api
+ */
+ol.source.Cluster.prototype.getSource = function() {
+  return this.source_;
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.source.Cluster.prototype.loadFeatures = function(extent, resolution,
+    projection) {
+  this.source_.loadFeatures(extent, resolution, projection);
+  if (resolution !== this.resolution_) {
+    this.clear();
+    this.resolution_ = resolution;
+    this.cluster_();
+    this.addFeatures(this.features_);
+  }
+};
+
+
+/**
+ * handle the source changing
+ * @private
+ */
+ol.source.Cluster.prototype.onSourceChange_ = function() {
+  this.clear();
+  this.cluster_();
+  this.addFeatures(this.features_);
+  this.changed();
+};
+
+
+/**
+ * @private
+ */
+ol.source.Cluster.prototype.cluster_ = function() {
+  if (this.resolution_ === undefined) {
+    return;
+  }
+  this.features_.length = 0;
+  var extent = ol.extent.createEmpty();
+  var mapDistance = this.distance_ * this.resolution_;
+  var features = this.source_.getFeatures();
+
+  /**
+   * @type {!Object.<string, boolean>}
+   */
+  var clustered = {};
+
+  for (var i = 0, ii = features.length; i < ii; i++) {
+    var feature = features[i];
+    if (!(goog.getUid(feature).toString() in clustered)) {
+      var geometry = this.geometryFunction_(feature);
+      if (geometry) {
+        var coordinates = geometry.getCoordinates();
+        ol.extent.createOrUpdateFromCoordinate(coordinates, extent);
+        ol.extent.buffer(extent, mapDistance, extent);
+
+        var neighbors = this.source_.getFeaturesInExtent(extent);
+        goog.asserts.assert(neighbors.length >= 1, 'at least one neighbor found');
+        neighbors = neighbors.filter(function(neighbor) {
+          var uid = goog.getUid(neighbor).toString();
+          if (!(uid in clustered)) {
+            clustered[uid] = true;
+            return true;
+          } else {
+            return false;
+          }
+        });
+        this.features_.push(this.createCluster_(neighbors));
+      }
+    }
+  }
+  goog.asserts.assert(
+      Object.keys(clustered).length == this.source_.getFeatures().length,
+      'number of clustered equals number of features in the source');
+};
+
+
+/**
+ * @param {Array.<ol.Feature>} features Features
+ * @return {ol.Feature} The cluster feature.
+ * @private
+ */
+ol.source.Cluster.prototype.createCluster_ = function(features) {
+  var centroid = [0, 0];
+  for (var i = features.length - 1; i >= 0; --i) {
+    var geometry = this.geometryFunction_(features[i]);
+    if (geometry) {
+      ol.coordinate.add(centroid, geometry.getCoordinates());
+    } else {
+      features.splice(i, 1);
+    }
+  }
+  ol.coordinate.scale(centroid, 1 / features.length);
+
+  var cluster = new ol.Feature(new ol.geom.Point(centroid));
+  cluster.set('features', features);
+  return cluster;
+};
+
+goog.provide('ol.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.
+ */
+ol.uri.appendParams = function(uri, params) {
+  var qs = Object.keys(params).map(function(k) {
+    return k + '=' + encodeURIComponent(params[k]);
+  }).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('ol.source.ImageArcGISRest');
+
+goog.require('goog.asserts');
+goog.require('ol');
+goog.require('ol.Image');
+goog.require('ol.events');
+goog.require('ol.events.EventType');
+goog.require('ol.extent');
+goog.require('ol.object');
+goog.require('ol.proj');
+goog.require('ol.source.Image');
+goog.require('ol.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 ol image caching and use
+ * {@link ol.source.TileArcGISRest} data source.
+ *
+ * @constructor
+ * @fires ol.source.ImageEvent
+ * @extends {ol.source.Image}
+ * @param {olx.source.ImageArcGISRestOptions=} opt_options Image ArcGIS Rest Options.
+ * @api
+ */
+ol.source.ImageArcGISRest = function(opt_options) {
+
+  var options = opt_options || {};
+
+  ol.source.Image.call(this, {
+    attributions: options.attributions,
+    logo: options.logo,
+    projection: options.projection,
+    resolutions: options.resolutions
+  });
+
+  /**
+   * @private
+   * @type {?string}
+   */
+  this.crossOrigin_ =
+      options.crossOrigin !== undefined ? options.crossOrigin : null;
+
+  /**
+   * @private
+   * @type {string|undefined}
+   */
+  this.url_ = options.url;
+
+  /**
+   * @private
+   * @type {ol.ImageLoadFunctionType}
+   */
+  this.imageLoadFunction_ = options.imageLoadFunction !== undefined ?
+      options.imageLoadFunction : ol.source.Image.defaultImageLoadFunction;
+
+
+  /**
+   * @private
+   * @type {!Object}
+   */
+  this.params_ = options.params || {};
+
+  /**
+   * @private
+   * @type {ol.Image}
+   */
+  this.image_ = null;
+
+  /**
+   * @private
+   * @type {ol.Size}
+   */
+  this.imageSize_ = [0, 0];
+
+
+  /**
+   * @private
+   * @type {number}
+   */
+  this.renderedRevision_ = 0;
+
+  /**
+   * @private
+   * @type {number}
+   */
+  this.ratio_ = options.ratio !== undefined ? options.ratio : 1.5;
+
+};
+ol.inherits(ol.source.ImageArcGISRest, ol.source.Image);
+
+
+/**
+ * Get the user-provided params, i.e. those passed to the constructor through
+ * the "params" option, and possibly updated using the updateParams method.
+ * @return {Object} Params.
+ * @api stable
+ */
+ol.source.ImageArcGISRest.prototype.getParams = function() {
+  return this.params_;
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.source.ImageArcGISRest.prototype.getImageInternal = function(extent, resolution, pixelRatio, projection) {
+
+  if (this.url_ === undefined) {
+    return null;
+  }
+
+  resolution = this.findNearestResolution(resolution);
+
+  var image = this.image_;
+  if (image &&
+      this.renderedRevision_ == this.getRevision() &&
+      image.getResolution() == resolution &&
+      image.getPixelRatio() == pixelRatio &&
+      ol.extent.containsExtent(image.getExtent(), extent)) {
+    return image;
+  }
+
+  var params = {
+    'F': 'image',
+    'FORMAT': 'PNG32',
+    'TRANSPARENT': true
+  };
+  ol.object.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_ * ol.extent.getWidth(extent) / 2;
+    var halfHeight = this.ratio_ * ol.extent.getHeight(extent) / 2;
+    extent[0] = centerX - halfWidth;
+    extent[1] = centerY - halfHeight;
+    extent[2] = centerX + halfWidth;
+    extent[3] = centerY + halfHeight;
+  }
+
+  var imageResolution = resolution / pixelRatio;
+
+  // Compute an integer width and height.
+  var width = Math.ceil(ol.extent.getWidth(extent) / imageResolution);
+  var height = Math.ceil(ol.extent.getHeight(extent) / imageResolution);
+
+  // Modify the extent to match the integer width and height.
+  extent[0] = centerX - imageResolution * width / 2;
+  extent[2] = centerX + imageResolution * width / 2;
+  extent[1] = centerY - imageResolution * height / 2;
+  extent[3] = centerY + imageResolution * height / 2;
+
+  this.imageSize_[0] = width;
+  this.imageSize_[1] = height;
+
+  var url = this.getRequestUrl_(extent, this.imageSize_, pixelRatio,
+      projection, params);
+
+  this.image_ = new ol.Image(extent, resolution, pixelRatio,
+      this.getAttributions(), url, this.crossOrigin_, this.imageLoadFunction_);
+
+  this.renderedRevision_ = this.getRevision();
+
+  ol.events.listen(this.image_, ol.events.EventType.CHANGE,
+      this.handleImageChange, this);
+
+  return this.image_;
+
+};
+
+
+/**
+ * Return the image load function of the source.
+ * @return {ol.ImageLoadFunctionType} The image load function.
+ * @api
+ */
+ol.source.ImageArcGISRest.prototype.getImageLoadFunction = function() {
+  return this.imageLoadFunction_;
+};
+
+
+/**
+ * @param {ol.Extent} extent Extent.
+ * @param {ol.Size} size Size.
+ * @param {number} pixelRatio Pixel ratio.
+ * @param {ol.proj.Projection} projection Projection.
+ * @param {Object} params Params.
+ * @return {string} Request URL.
+ * @private
+ */
+ol.source.ImageArcGISRest.prototype.getRequestUrl_ = function(extent, size, pixelRatio, projection, params) {
+
+  goog.asserts.assert(this.url_ !== undefined, 'url is defined');
+
+  // 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'] = 90 * pixelRatio;
+
+  var url = this.url_;
+
+  var modifiedUrl = url
+    .replace(/MapServer\/?$/, 'MapServer/export')
+    .replace(/ImageServer\/?$/, 'ImageServer/exportImage');
+  if (modifiedUrl == url) {
+    goog.asserts.fail('Unknown Rest Service', url);
+  }
+  return ol.uri.appendParams(modifiedUrl, params);
+};
+
+
+/**
+ * Return the URL used for this ArcGIS source.
+ * @return {string|undefined} URL.
+ * @api stable
+ */
+ol.source.ImageArcGISRest.prototype.getUrl = function() {
+  return this.url_;
+};
+
+
+/**
+ * Set the image load function of the source.
+ * @param {ol.ImageLoadFunctionType} imageLoadFunction Image load function.
+ * @api
+ */
+ol.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 stable
+ */
+ol.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 stable
+ */
+ol.source.ImageArcGISRest.prototype.updateParams = function(params) {
+  ol.object.assign(this.params_, params);
+  this.image_ = null;
+  this.changed();
+};
+
+goog.provide('ol.source.ImageMapGuide');
+
+goog.require('ol.events');
+goog.require('ol.events.EventType');
+goog.require('ol.Image');
+goog.require('ol.extent');
+goog.require('ol.object');
+goog.require('ol.source.Image');
+goog.require('ol.uri');
+
+
+/**
+ * @classdesc
+ * Source for images from Mapguide servers
+ *
+ * @constructor
+ * @fires ol.source.ImageEvent
+ * @extends {ol.source.Image}
+ * @param {olx.source.ImageMapGuideOptions} options Options.
+ * @api stable
+ */
+ol.source.ImageMapGuide = function(options) {
+
+  ol.source.Image.call(this, {
+    projection: options.projection,
+    resolutions: options.resolutions
+  });
+
+  /**
+   * @private
+   * @type {?string}
+   */
+  this.crossOrigin_ =
+      options.crossOrigin !== undefined ? options.crossOrigin : null;
+
+  /**
+   * @private
+   * @type {number}
+   */
+  this.displayDpi_ = options.displayDpi !== undefined ?
+      options.displayDpi : 96;
+
+  /**
+   * @private
+   * @type {!Object}
+   */
+  this.params_ = options.params || {};
+
+  /**
+   * @private
+   * @type {string|undefined}
+   */
+  this.url_ = options.url;
+
+  /**
+   * @private
+   * @type {ol.ImageLoadFunctionType}
+   */
+  this.imageLoadFunction_ = options.imageLoadFunction !== undefined ?
+      options.imageLoadFunction : ol.source.Image.defaultImageLoadFunction;
+
+  /**
+   * @private
+   * @type {boolean}
+   */
+  this.hidpi_ = options.hidpi !== undefined ? options.hidpi : true;
+
+  /**
+   * @private
+   * @type {number}
+   */
+  this.metersPerUnit_ = options.metersPerUnit !== undefined ?
+      options.metersPerUnit : 1;
+
+  /**
+   * @private
+   * @type {number}
+   */
+  this.ratio_ = options.ratio !== undefined ? options.ratio : 1;
+
+  /**
+   * @private
+   * @type {boolean}
+   */
+  this.useOverlay_ = options.useOverlay !== undefined ?
+      options.useOverlay : false;
+
+  /**
+   * @private
+   * @type {ol.Image}
+   */
+  this.image_ = null;
+
+  /**
+   * @private
+   * @type {number}
+   */
+  this.renderedRevision_ = 0;
+
+};
+ol.inherits(ol.source.ImageMapGuide, ol.source.Image);
+
+
+/**
+ * Get the user-provided params, i.e. those passed to the constructor through
+ * the "params" option, and possibly updated using the updateParams method.
+ * @return {Object} Params.
+ * @api stable
+ */
+ol.source.ImageMapGuide.prototype.getParams = function() {
+  return this.params_;
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.source.ImageMapGuide.prototype.getImageInternal = function(extent, resolution, pixelRatio, projection) {
+  resolution = this.findNearestResolution(resolution);
+  pixelRatio = this.hidpi_ ? pixelRatio : 1;
+
+  var image = this.image_;
+  if (image &&
+      this.renderedRevision_ == this.getRevision() &&
+      image.getResolution() == resolution &&
+      image.getPixelRatio() == pixelRatio &&
+      ol.extent.containsExtent(image.getExtent(), extent)) {
+    return image;
+  }
+
+  if (this.ratio_ != 1) {
+    extent = extent.slice();
+    ol.extent.scaleFromCenter(extent, this.ratio_);
+  }
+  var width = ol.extent.getWidth(extent) / resolution;
+  var height = ol.extent.getHeight(extent) / resolution;
+  var size = [width * pixelRatio, height * pixelRatio];
+
+  if (this.url_ !== undefined) {
+    var imageUrl = this.getUrl(this.url_, this.params_, extent, size,
+        projection);
+    image = new ol.Image(extent, resolution, pixelRatio,
+        this.getAttributions(), imageUrl, this.crossOrigin_,
+        this.imageLoadFunction_);
+    ol.events.listen(image, ol.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 {ol.ImageLoadFunctionType} The image load function.
+ * @api
+ */
+ol.source.ImageMapGuide.prototype.getImageLoadFunction = function() {
+  return this.imageLoadFunction_;
+};
+
+
+/**
+ * @param {ol.Extent} extent The map extents.
+ * @param {ol.Size} size The viewport size.
+ * @param {number} metersPerUnit The meters-per-unit value.
+ * @param {number} dpi The display resolution.
+ * @return {number} The computed map scale.
+ */
+ol.source.ImageMapGuide.getScale = function(extent, size, metersPerUnit, dpi) {
+  var mcsW = ol.extent.getWidth(extent);
+  var mcsH = ol.extent.getHeight(extent);
+  var devW = size[0];
+  var devH = size[1];
+  var mpp = 0.0254 / dpi;
+  if (devH * mcsW > devW * mcsH) {
+    return mcsW * metersPerUnit / (devW * mpp); // width limited
+  } else {
+    return mcsH * metersPerUnit / (devH * mpp); // height limited
+  }
+};
+
+
+/**
+ * Update the user-provided params.
+ * @param {Object} params Params.
+ * @api stable
+ */
+ol.source.ImageMapGuide.prototype.updateParams = function(params) {
+  ol.object.assign(this.params_, params);
+  this.changed();
+};
+
+
+/**
+ * @param {string} baseUrl The mapagent url.
+ * @param {Object.<string, string|number>} params Request parameters.
+ * @param {ol.Extent} extent Extent.
+ * @param {ol.Size} size Size.
+ * @param {ol.proj.Projection} projection Projection.
+ * @return {string} The mapagent map image request URL.
+ */
+ol.source.ImageMapGuide.prototype.getUrl = function(baseUrl, params, extent, size, projection) {
+  var scale = ol.source.ImageMapGuide.getScale(extent, size,
+      this.metersPerUnit_, this.displayDpi_);
+  var center = ol.extent.getCenter(extent);
+  var baseParams = {
+    'OPERATION': this.useOverlay_ ? 'GETDYNAMICMAPOVERLAYIMAGE' : 'GETMAPIMAGE',
+    'VERSION': '2.0.0',
+    'LOCALE': 'en',
+    'CLIENTAGENT': 'ol.source.ImageMapGuide source',
+    'CLIP': '1',
+    'SETDISPLAYDPI': this.displayDpi_,
+    'SETDISPLAYWIDTH': Math.round(size[0]),
+    'SETDISPLAYHEIGHT': Math.round(size[1]),
+    'SETVIEWSCALE': scale,
+    'SETVIEWCENTERX': center[0],
+    'SETVIEWCENTERY': center[1]
+  };
+  ol.object.assign(baseParams, params);
+  return ol.uri.appendParams(baseUrl, baseParams);
+};
+
+
+/**
+ * Set the image load function of the MapGuide source.
+ * @param {ol.ImageLoadFunctionType} imageLoadFunction Image load function.
+ * @api
+ */
+ol.source.ImageMapGuide.prototype.setImageLoadFunction = function(
+    imageLoadFunction) {
+  this.image_ = null;
+  this.imageLoadFunction_ = imageLoadFunction;
+  this.changed();
+};
+
+goog.provide('ol.source.ImageStatic');
+
+goog.require('ol.events');
+goog.require('ol.events.EventType');
+goog.require('ol.Image');
+goog.require('ol.ImageState');
+goog.require('ol.dom');
+goog.require('ol.extent');
+goog.require('ol.proj');
+goog.require('ol.source.Image');
+
+
+/**
+ * @classdesc
+ * A layer source for displaying a single, static image.
+ *
+ * @constructor
+ * @extends {ol.source.Image}
+ * @param {olx.source.ImageStaticOptions} options Options.
+ * @api stable
+ */
+ol.source.ImageStatic = function(options) {
+  var imageExtent = options.imageExtent;
+
+  var crossOrigin = options.crossOrigin !== undefined ?
+      options.crossOrigin : null;
+
+  var /** @type {ol.ImageLoadFunctionType} */ imageLoadFunction =
+      options.imageLoadFunction !== undefined ?
+      options.imageLoadFunction : ol.source.Image.defaultImageLoadFunction;
+
+  ol.source.Image.call(this, {
+    attributions: options.attributions,
+    logo: options.logo,
+    projection: ol.proj.get(options.projection)
+  });
+
+  /**
+   * @private
+   * @type {ol.Image}
+   */
+  this.image_ = new ol.Image(imageExtent, undefined, 1, this.getAttributions(),
+      options.url, crossOrigin, imageLoadFunction);
+
+  /**
+   * @private
+   * @type {ol.Size}
+   */
+  this.imageSize_ = options.imageSize ? options.imageSize : null;
+
+  ol.events.listen(this.image_, ol.events.EventType.CHANGE,
+      this.handleImageChange, this);
+
+};
+ol.inherits(ol.source.ImageStatic, ol.source.Image);
+
+
+/**
+ * @inheritDoc
+ */
+ol.source.ImageStatic.prototype.getImageInternal = function(extent, resolution, pixelRatio, projection) {
+  if (ol.extent.intersects(extent, this.image_.getExtent())) {
+    return this.image_;
+  }
+  return null;
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.source.ImageStatic.prototype.handleImageChange = function(evt) {
+  if (this.image_.getState() == ol.ImageState.LOADED) {
+    var imageExtent = this.image_.getExtent();
+    var image = this.image_.getImage();
+    var imageWidth, imageHeight;
+    if (this.imageSize_) {
+      imageWidth = this.imageSize_[0];
+      imageHeight = this.imageSize_[1];
+    } else {
+      // TODO: remove the type cast when a closure-compiler > 20160315 is used.
+      // see: https://github.com/google/closure-compiler/pull/1664
+      imageWidth = /** @type {number} */ (image.width);
+      imageHeight = /** @type {number} */ (image.height);
+    }
+    var resolution = ol.extent.getHeight(imageExtent) / imageHeight;
+    var targetWidth = Math.ceil(ol.extent.getWidth(imageExtent) / resolution);
+    if (targetWidth != imageWidth) {
+      var context = ol.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);
+    }
+  }
+  ol.source.Image.prototype.handleImageChange.call(this, evt);
+};
+
+goog.provide('ol.source.wms');
+goog.provide('ol.source.wms.ServerType');
+
+
+/**
+ * Available server types: `'carmentaserver'`, `'geoserver'`, `'mapserver'`,
+ *     `'qgis'`. These are servers that have vendor parameters beyond the WMS
+ *     specification that OpenLayers can make use of.
+ * @enum {string}
+ */
+ol.source.wms.ServerType = {
+  CARMENTA_SERVER: 'carmentaserver',
+  GEOSERVER: 'geoserver',
+  MAPSERVER: 'mapserver',
+  QGIS: 'qgis'
+};
+
+// FIXME cannot be shared between maps with different projections
+
+goog.provide('ol.source.ImageWMS');
+
+goog.require('goog.asserts');
+goog.require('ol');
+goog.require('ol.Image');
+goog.require('ol.events');
+goog.require('ol.events.EventType');
+goog.require('ol.extent');
+goog.require('ol.object');
+goog.require('ol.proj');
+goog.require('ol.source.Image');
+goog.require('ol.source.wms');
+goog.require('ol.source.wms.ServerType');
+goog.require('ol.string');
+goog.require('ol.uri');
+
+
+/**
+ * @classdesc
+ * Source for WMS servers providing single, untiled images.
+ *
+ * @constructor
+ * @fires ol.source.ImageEvent
+ * @extends {ol.source.Image}
+ * @param {olx.source.ImageWMSOptions=} opt_options Options.
+ * @api stable
+ */
+ol.source.ImageWMS = function(opt_options) {
+
+  var options = opt_options || {};
+
+  ol.source.Image.call(this, {
+    attributions: options.attributions,
+    logo: options.logo,
+    projection: options.projection,
+    resolutions: options.resolutions
+  });
+
+  /**
+   * @private
+   * @type {?string}
+   */
+  this.crossOrigin_ =
+      options.crossOrigin !== undefined ? options.crossOrigin : null;
+
+  /**
+   * @private
+   * @type {string|undefined}
+   */
+  this.url_ = options.url;
+
+  /**
+   * @private
+   * @type {ol.ImageLoadFunctionType}
+   */
+  this.imageLoadFunction_ = options.imageLoadFunction !== undefined ?
+      options.imageLoadFunction : ol.source.Image.defaultImageLoadFunction;
+
+  /**
+   * @private
+   * @type {!Object}
+   */
+  this.params_ = options.params || {};
+
+  /**
+   * @private
+   * @type {boolean}
+   */
+  this.v13_ = true;
+  this.updateV13_();
+
+  /**
+   * @private
+   * @type {ol.source.wms.ServerType|undefined}
+   */
+  this.serverType_ =
+      /** @type {ol.source.wms.ServerType|undefined} */ (options.serverType);
+
+  /**
+   * @private
+   * @type {boolean}
+   */
+  this.hidpi_ = options.hidpi !== undefined ? options.hidpi : true;
+
+  /**
+   * @private
+   * @type {ol.Image}
+   */
+  this.image_ = null;
+
+  /**
+   * @private
+   * @type {ol.Size}
+   */
+  this.imageSize_ = [0, 0];
+
+  /**
+   * @private
+   * @type {number}
+   */
+  this.renderedRevision_ = 0;
+
+  /**
+   * @private
+   * @type {number}
+   */
+  this.ratio_ = options.ratio !== undefined ? options.ratio : 1.5;
+
+};
+ol.inherits(ol.source.ImageWMS, ol.source.Image);
+
+
+/**
+ * @const
+ * @type {ol.Size}
+ * @private
+ */
+ol.source.ImageWMS.GETFEATUREINFO_IMAGE_SIZE_ = [101, 101];
+
+
+/**
+ * Return the GetFeatureInfo URL for the passed coordinate, resolution, and
+ * projection. Return `undefined` if the GetFeatureInfo URL cannot be
+ * constructed.
+ * @param {ol.Coordinate} coordinate Coordinate.
+ * @param {number} resolution Resolution.
+ * @param {ol.ProjectionLike} projection Projection.
+ * @param {!Object} params GetFeatureInfo params. `INFO_FORMAT` at least should
+ *     be provided. If `QUERY_LAYERS` is not provided then the layers specified
+ *     in the `LAYERS` parameter will be used. `VERSION` should not be
+ *     specified here.
+ * @return {string|undefined} GetFeatureInfo URL.
+ * @api stable
+ */
+ol.source.ImageWMS.prototype.getGetFeatureInfoUrl = function(coordinate, resolution, projection, params) {
+
+  goog.asserts.assert(!('VERSION' in params),
+      'key VERSION is not allowed in params');
+
+  if (this.url_ === undefined) {
+    return undefined;
+  }
+
+  var extent = ol.extent.getForViewAndSize(
+      coordinate, resolution, 0,
+      ol.source.ImageWMS.GETFEATUREINFO_IMAGE_SIZE_);
+
+  var baseParams = {
+    'SERVICE': 'WMS',
+    'VERSION': ol.DEFAULT_WMS_VERSION,
+    'REQUEST': 'GetFeatureInfo',
+    'FORMAT': 'image/png',
+    'TRANSPARENT': true,
+    'QUERY_LAYERS': this.params_['LAYERS']
+  };
+  ol.object.assign(baseParams, this.params_, params);
+
+  var x = Math.floor((coordinate[0] - extent[0]) / resolution);
+  var y = Math.floor((extent[3] - coordinate[1]) / resolution);
+  baseParams[this.v13_ ? 'I' : 'X'] = x;
+  baseParams[this.v13_ ? 'J' : 'Y'] = y;
+
+  return this.getRequestUrl_(
+      extent, ol.source.ImageWMS.GETFEATUREINFO_IMAGE_SIZE_,
+      1, ol.proj.get(projection), baseParams);
+};
+
+
+/**
+ * Get the user-provided params, i.e. those passed to the constructor through
+ * the "params" option, and possibly updated using the updateParams method.
+ * @return {Object} Params.
+ * @api stable
+ */
+ol.source.ImageWMS.prototype.getParams = function() {
+  return this.params_;
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.source.ImageWMS.prototype.getImageInternal = function(extent, resolution, pixelRatio, projection) {
+
+  if (this.url_ === undefined) {
+    return null;
+  }
+
+  resolution = this.findNearestResolution(resolution);
+
+  if (pixelRatio != 1 && (!this.hidpi_ || this.serverType_ === undefined)) {
+    pixelRatio = 1;
+  }
+
+  extent = extent.slice();
+  var centerX = (extent[0] + extent[2]) / 2;
+  var centerY = (extent[1] + extent[3]) / 2;
+
+  var imageResolution = resolution / pixelRatio;
+  var imageWidth = ol.extent.getWidth(extent) / imageResolution;
+  var imageHeight = ol.extent.getHeight(extent) / imageResolution;
+
+  var image = this.image_;
+  if (image &&
+      this.renderedRevision_ == this.getRevision() &&
+      image.getResolution() == resolution &&
+      image.getPixelRatio() == pixelRatio &&
+      ol.extent.containsExtent(image.getExtent(), extent)) {
+    return image;
+  }
+
+  if (this.ratio_ != 1) {
+    var halfWidth = this.ratio_ * ol.extent.getWidth(extent) / 2;
+    var halfHeight = this.ratio_ * ol.extent.getHeight(extent) / 2;
+    extent[0] = centerX - halfWidth;
+    extent[1] = centerY - halfHeight;
+    extent[2] = centerX + halfWidth;
+    extent[3] = centerY + halfHeight;
+  }
+
+  var params = {
+    'SERVICE': 'WMS',
+    'VERSION': ol.DEFAULT_WMS_VERSION,
+    'REQUEST': 'GetMap',
+    'FORMAT': 'image/png',
+    'TRANSPARENT': true
+  };
+  ol.object.assign(params, this.params_);
+
+  this.imageSize_[0] = Math.ceil(imageWidth * this.ratio_);
+  this.imageSize_[1] = Math.ceil(imageHeight * this.ratio_);
+
+  var url = this.getRequestUrl_(extent, this.imageSize_, pixelRatio,
+      projection, params);
+
+  this.image_ = new ol.Image(extent, resolution, pixelRatio,
+      this.getAttributions(), url, this.crossOrigin_, this.imageLoadFunction_);
+
+  this.renderedRevision_ = this.getRevision();
+
+  ol.events.listen(this.image_, ol.events.EventType.CHANGE,
+      this.handleImageChange, this);
+
+  return this.image_;
+
+};
+
+
+/**
+ * Return the image load function of the source.
+ * @return {ol.ImageLoadFunctionType} The image load function.
+ * @api
+ */
+ol.source.ImageWMS.prototype.getImageLoadFunction = function() {
+  return this.imageLoadFunction_;
+};
+
+
+/**
+ * @param {ol.Extent} extent Extent.
+ * @param {ol.Size} size Size.
+ * @param {number} pixelRatio Pixel ratio.
+ * @param {ol.proj.Projection} projection Projection.
+ * @param {Object} params Params.
+ * @return {string} Request URL.
+ * @private
+ */
+ol.source.ImageWMS.prototype.getRequestUrl_ = function(extent, size, pixelRatio, projection, params) {
+
+  goog.asserts.assert(this.url_ !== undefined, 'url is defined');
+
+  params[this.v13_ ? 'CRS' : 'SRS'] = projection.getCode();
+
+  if (!('STYLES' in this.params_)) {
+    params['STYLES'] = '';
+  }
+
+  if (pixelRatio != 1) {
+    switch (this.serverType_) {
+      case ol.source.wms.ServerType.GEOSERVER:
+        var dpi = (90 * pixelRatio + 0.5) | 0;
+        if ('FORMAT_OPTIONS' in params) {
+          params['FORMAT_OPTIONS'] += ';dpi:' + dpi;
+        } else {
+          params['FORMAT_OPTIONS'] = 'dpi:' + dpi;
+        }
+        break;
+      case ol.source.wms.ServerType.MAPSERVER:
+        params['MAP_RESOLUTION'] = 90 * pixelRatio;
+        break;
+      case ol.source.wms.ServerType.CARMENTA_SERVER:
+      case ol.source.wms.ServerType.QGIS:
+        params['DPI'] = 90 * pixelRatio;
+        break;
+      default:
+        goog.asserts.fail('unknown serverType configured');
+        break;
+    }
+  }
+
+  params['WIDTH'] = size[0];
+  params['HEIGHT'] = size[1];
+
+  var axisOrientation = projection.getAxisOrientation();
+  var bbox;
+  if (this.v13_ && axisOrientation.substr(0, 2) == 'ne') {
+    bbox = [extent[1], extent[0], extent[3], extent[2]];
+  } else {
+    bbox = extent;
+  }
+  params['BBOX'] = bbox.join(',');
+
+  return ol.uri.appendParams(this.url_, params);
+};
+
+
+/**
+ * Return the URL used for this WMS source.
+ * @return {string|undefined} URL.
+ * @api stable
+ */
+ol.source.ImageWMS.prototype.getUrl = function() {
+  return this.url_;
+};
+
+
+/**
+ * Set the image load function of the source.
+ * @param {ol.ImageLoadFunctionType} imageLoadFunction Image load function.
+ * @api
+ */
+ol.source.ImageWMS.prototype.setImageLoadFunction = function(
+    imageLoadFunction) {
+  this.image_ = null;
+  this.imageLoadFunction_ = imageLoadFunction;
+  this.changed();
+};
+
+
+/**
+ * Set the URL to use for requests.
+ * @param {string|undefined} url URL.
+ * @api stable
+ */
+ol.source.ImageWMS.prototype.setUrl = function(url) {
+  if (url != this.url_) {
+    this.url_ = url;
+    this.image_ = null;
+    this.changed();
+  }
+};
+
+
+/**
+ * Update the user-provided params.
+ * @param {Object} params Params.
+ * @api stable
+ */
+ol.source.ImageWMS.prototype.updateParams = function(params) {
+  ol.object.assign(this.params_, params);
+  this.updateV13_();
+  this.image_ = null;
+  this.changed();
+};
+
+
+/**
+ * @private
+ */
+ol.source.ImageWMS.prototype.updateV13_ = function() {
+  var version = this.params_['VERSION'] || ol.DEFAULT_WMS_VERSION;
+  this.v13_ = ol.string.compareVersions(version, '1.3') >= 0;
+};
+
+goog.provide('ol.source.OSM');
+
+goog.require('ol.Attribution');
+goog.require('ol.source.XYZ');
+
+
+/**
+ * @classdesc
+ * Layer source for the OpenStreetMap tile server.
+ *
+ * @constructor
+ * @extends {ol.source.XYZ}
+ * @param {olx.source.OSMOptions=} opt_options Open Street Map options.
+ * @api stable
+ */
+ol.source.OSM = function(opt_options) {
+
+  var options = opt_options || {};
+
+  var attributions;
+  if (options.attributions !== undefined) {
+    attributions = options.attributions;
+  } else {
+    attributions = [ol.source.OSM.ATTRIBUTION];
+  }
+
+  var crossOrigin = options.crossOrigin !== undefined ?
+      options.crossOrigin : 'anonymous';
+
+  var url = options.url !== undefined ?
+      options.url : 'https://{a-c}.tile.openstreetmap.org/{z}/{x}/{y}.png';
+
+  ol.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,
+    reprojectionErrorThreshold: options.reprojectionErrorThreshold,
+    tileLoadFunction: options.tileLoadFunction,
+    url: url,
+    wrapX: options.wrapX
+  });
+
+};
+ol.inherits(ol.source.OSM, ol.source.XYZ);
+
+
+/**
+ * The attribution containing a link to the OpenStreetMap Copyright and License
+ * page.
+ * @const
+ * @type {ol.Attribution}
+ * @api
+ */
+ol.source.OSM.ATTRIBUTION = new ol.Attribution({
+  html: '&copy; ' +
+      '<a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a> ' +
+      'contributors.'
+});
+
+goog.provide('ol.ext.pixelworks');
+/** @typedef {function(*)} */
+ol.ext.pixelworks;
+(function() {
+var exports = {};
+var module = {exports: exports};
+var define;
+/**
+ * @fileoverview
+ * @suppress {accessControls, ambiguousFunctionDecl, checkDebuggerStatement, checkRegExp, checkTypes, checkVars, const, constantProperty, deprecated, duplicate, es5Strict, fileoverviewTags, missingProperties, nonStandardJsDocs, strictModuleDepCheck, suspiciousCode, undefinedNames, undefinedVars, unknownDefines, uselessCode, visibility}
+ */
+(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.pixelworks = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(_dereq_,module,exports){
+var Processor = _dereq_('./processor');
+
+exports.Processor = Processor;
+
+},{"./processor":2}],2:[function(_dereq_,module,exports){
+var newImageData = _dereq_('./util').newImageData;
+
+/**
+ * Create a function for running operations.  This function is serialized for
+ * use in a worker.
+ * @param {function(Array, Object):*} operation The operation.
+ * @return {function(Object):ArrayBuffer} A function that takes an object with
+ * buffers, meta, imageOps, width, and height properties and returns an array
+ * buffer.
+ */
+function createMinion(operation) {
+  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) {
+    // bracket notation for minification support
+    var buffers = data['buffers'];
+    var meta = data['meta'];
+    var imageOps = data['imageOps'];
+    var width = data['width'];
+    var height = data['height'];
+
+    var numBuffers = buffers.length;
+    var numBytes = buffers[0].byteLength;
+    var output, b;
+
+    if (imageOps) {
+      var images = new Array(numBuffers);
+      for (b = 0; b < numBuffers; ++b) {
+        images[b] = 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;
+  };
+}
+
+/**
+ * Create a worker for running operations.
+ * @param {Object} config Configuration.
+ * @param {function(MessageEvent)} onMessage Called with a message event.
+ * @return {Worker} The worker.
+ */
+function createWorker(config, onMessage) {
+  var lib = Object.keys(config.lib || {}).map(function(name) {
+    return 'var ' + name + ' = ' + config.lib[name].toString() + ';';
+  });
+
+  var lines = lib.concat([
+    'var __minion__ = (' + createMinion.toString() + ')(', config.operation.toString(), ');',
+    'self.addEventListener("message", function(event) {',
+    '  var buffer = __minion__(event.data);',
+    '  self.postMessage({buffer: buffer, meta: event.data.meta}, [buffer]);',
+    '});'
+  ]);
+
+  var blob = new Blob(lines, {type: 'text/javascript'});
+  var source = URL.createObjectURL(blob);
+  var worker = new Worker(source);
+  worker.addEventListener('message', onMessage);
+  return worker;
+}
+
+/**
+ * Create a faux worker for running operations.
+ * @param {Object} config Configuration.
+ * @param {function(MessageEvent)} onMessage Called with a message event.
+ * @return {Object} The faux worker.
+ */
+function createFauxWorker(config, onMessage) {
+  var minion = createMinion(config.operation);
+  return {
+    postMessage: function(data) {
+      setTimeout(function() {
+        onMessage({'data': {'buffer': minion(data), 'meta': data['meta']}});
+      }, 0);
+    }
+  };
+}
+
+/**
+ * A processor runs pixel or image operations in workers.
+ * @param {Object} config Configuration.
+ */
+function Processor(config) {
+  this._imageOps = !!config.imageOps;
+  var threads;
+  if (config.threads === 0) {
+    threads = 0;
+  } else if (this._imageOps) {
+    threads = 1;
+  } else {
+    threads = config.threads || 1;
+  }
+  var workers = [];
+  if (threads) {
+    for (var i = 0; i < threads; ++i) {
+      workers[i] = createWorker(config, this._onWorkerMessage.bind(this, i));
+    }
+  } else {
+    workers[0] = createFauxWorker(config, this._onWorkerMessage.bind(this, 0));
+  }
+  this._workers = workers;
+  this._queue = [];
+  this._maxQueueLength = config.queue || Infinity;
+  this._running = 0;
+  this._dataLookup = {};
+  this._job = null;
+}
+
+/**
+ * Run operation on input data.
+ * @param {Array.<Array|ImageData>} inputs Array of pixels or image data
+ *     (depending on the operation type).
+ * @param {Object} meta A user data object.  This is passed to all operations
+ *     and must be serializable.
+ * @param {function(Error, ImageData, Object)} callback Called when work
+ *     completes.  The first argument is any error.  The second is the ImageData
+ *     generated by operations.  The third is the user data object.
+ */
+Processor.prototype.process = function(inputs, meta, callback) {
+  this._enqueue({
+    inputs: inputs,
+    meta: meta,
+    callback: callback
+  });
+  this._dispatch();
+};
+
+/**
+ * Stop responding to any completed work and destroy the processor.
+ */
+Processor.prototype.destroy = function() {
+  for (var key in this) {
+    this[key] = null;
+  }
+  this._destroyed = true;
+};
+
+/**
+ * Add a job to the queue.
+ * @param {Object} job The job.
+ */
+Processor.prototype._enqueue = function(job) {
+  this._queue.push(job);
+  while (this._queue.length > this._maxQueueLength) {
+    this._queue.shift().callback(null, null);
+  }
+};
+
+/**
+ * Dispatch a job.
+ */
+Processor.prototype._dispatch = function() {
+  if (this._running === 0 && this._queue.length > 0) {
+    var job = this._job = this._queue.shift();
+    var width = job.inputs[0].width;
+    var height = job.inputs[0].height;
+    var buffers = job.inputs.map(function(input) {
+      return input.data.buffer;
+    });
+    var threads = this._workers.length;
+    this._running = threads;
+    if (threads === 1) {
+      this._workers[0].postMessage({
+        'buffers': buffers,
+        'meta': job.meta,
+        'imageOps': this._imageOps,
+        'width': width,
+        'height': height
+      }, buffers);
+    } else {
+      var length = job.inputs[0].data.length;
+      var segmentLength = 4 * Math.ceil(length / 4 / threads);
+      for (var i = 0; i < threads; ++i) {
+        var offset = i * segmentLength;
+        var slices = [];
+        for (var j = 0, jj = buffers.length; j < jj; ++j) {
+          slices.push(buffers[i].slice(offset, offset + segmentLength));
+        }
+        this._workers[i].postMessage({
+          'buffers': slices,
+          'meta': job.meta,
+          'imageOps': this._imageOps,
+          'width': width,
+          'height': height
+        }, slices);
+      }
+    }
+  }
+};
+
+/**
+ * Handle messages from the worker.
+ * @param {number} index The worker index.
+ * @param {MessageEvent} event The message event.
+ */
+Processor.prototype._onWorkerMessage = function(index, event) {
+  if (this._destroyed) {
+    return;
+  }
+  this._dataLookup[index] = event.data;
+  --this._running;
+  if (this._running === 0) {
+    this._resolveJob();
+  }
+};
+
+/**
+ * Resolve a job.  If there are no more worker threads, the processor callback
+ * will be called.
+ */
+Processor.prototype._resolveJob = function() {
+  var job = this._job;
+  var threads = this._workers.length;
+  var data, meta;
+  if (threads === 1) {
+    data = new Uint8ClampedArray(this._dataLookup[0]['buffer']);
+    meta = this._dataLookup[0]['meta'];
+  } else {
+    var length = job.inputs[0].data.length;
+    data = new Uint8ClampedArray(length);
+    meta = new Array(length);
+    var segmentLength = 4 * Math.ceil(length / 4 / threads);
+    for (var i = 0; i < threads; ++i) {
+      var buffer = this._dataLookup[i]['buffer'];
+      var offset = i * segmentLength;
+      data.set(new Uint8ClampedArray(buffer), offset);
+      meta[i] = this._dataLookup[i]['meta'];
+    }
+  }
+  this._job = null;
+  this._dataLookup = {};
+  job.callback(null,
+      newImageData(data, job.inputs[0].width, job.inputs[0].height), meta);
+  this._dispatch();
+};
+
+module.exports = Processor;
+
+},{"./util":3}],3:[function(_dereq_,module,exports){
+var hasImageData = true;
+try {
+  new ImageData(10, 10);
+} catch (_) {
+  hasImageData = false;
+}
+
+var context = document.createElement('canvas').getContext('2d');
+
+function newImageData(data, width, height) {
+  if (hasImageData) {
+    return new ImageData(data, width, height);
+  } else {
+    var imageData = context.createImageData(width, height);
+    imageData.data.set(data);
+    return imageData;
+  }
+}
+
+exports.newImageData = newImageData;
+
+},{}]},{},[1])(1)
+});
+ol.ext.pixelworks = module.exports;
+})();
+
+goog.provide('ol.RasterOperationType');
+goog.provide('ol.source.Raster');
+goog.provide('ol.source.RasterEvent');
+goog.provide('ol.source.RasterEventType');
+
+goog.require('goog.asserts');
+goog.require('goog.vec.Mat4');
+goog.require('ol.ImageCanvas');
+goog.require('ol.TileQueue');
+goog.require('ol.dom');
+goog.require('ol.events');
+goog.require('ol.events.Event');
+goog.require('ol.events.EventType');
+goog.require('ol.ext.pixelworks');
+goog.require('ol.extent');
+goog.require('ol.layer.Image');
+goog.require('ol.layer.Tile');
+goog.require('ol.object');
+goog.require('ol.renderer.canvas.ImageLayer');
+goog.require('ol.renderer.canvas.TileLayer');
+goog.require('ol.source.Image');
+goog.require('ol.source.State');
+goog.require('ol.source.Tile');
+
+
+/**
+ * Raster operation type. Supported values are `'pixel'` and `'image'`.
+ * @enum {string}
+ */
+ol.RasterOperationType = {
+  PIXEL: 'pixel',
+  IMAGE: 'image'
+};
+
+
+/**
+ * @classdesc
+ * A source that transforms data from any number of input sources using an array
+ * of {@link ol.RasterOperation} functions to transform input pixel values into
+ * output pixel values.
+ *
+ * @constructor
+ * @extends {ol.source.Image}
+ * @fires ol.source.RasterEvent
+ * @param {olx.source.RasterOptions} options Options.
+ * @api
+ */
+ol.source.Raster = function(options) {
+
+  /**
+   * @private
+   * @type {*}
+   */
+  this.worker_ = null;
+
+  /**
+   * @private
+   * @type {ol.RasterOperationType}
+   */
+  this.operationType_ = options.operationType !== undefined ?
+      options.operationType : ol.RasterOperationType.PIXEL;
+
+  /**
+   * @private
+   * @type {number}
+   */
+  this.threads_ = options.threads !== undefined ? options.threads : 1;
+
+  /**
+   * @private
+   * @type {Array.<ol.renderer.canvas.Layer>}
+   */
+  this.renderers_ = ol.source.Raster.createRenderers_(options.sources);
+
+  for (var r = 0, rr = this.renderers_.length; r < rr; ++r) {
+    ol.events.listen(this.renderers_[r], ol.events.EventType.CHANGE,
+        this.changed, this);
+  }
+
+  /**
+   * @private
+   * @type {CanvasRenderingContext2D}
+   */
+  this.canvasContext_ = ol.dom.createCanvasContext2D();
+
+  /**
+   * @private
+   * @type {ol.TileQueue}
+   */
+  this.tileQueue_ = new ol.TileQueue(
+      function() {
+        return 1;
+      },
+      this.changed.bind(this));
+
+  var layerStatesArray = ol.source.Raster.getLayerStatesArray_(this.renderers_);
+  var layerStates = {};
+  for (var i = 0, ii = layerStatesArray.length; i < ii; ++i) {
+    layerStates[goog.getUid(layerStatesArray[i].layer)] = layerStatesArray[i];
+  }
+
+  /**
+   * The most recently rendered state.
+   * @type {?ol.SourceRasterRenderedState}
+   * @private
+   */
+  this.renderedState_ = null;
+
+  /**
+   * The most recently rendered image canvas.
+   * @type {ol.ImageCanvas}
+   * @private
+   */
+  this.renderedImageCanvas_ = null;
+
+  /**
+   * @private
+   * @type {olx.FrameState}
+   */
+  this.frameState_ = {
+    animate: false,
+    attributions: {},
+    coordinateToPixelMatrix: goog.vec.Mat4.createNumber(),
+    extent: null,
+    focus: null,
+    index: 0,
+    layerStates: layerStates,
+    layerStatesArray: layerStatesArray,
+    logos: {},
+    pixelRatio: 1,
+    pixelToCoordinateMatrix: goog.vec.Mat4.createNumber(),
+    postRenderFunctions: [],
+    size: [0, 0],
+    skippedFeatureUids: {},
+    tileQueue: this.tileQueue_,
+    time: Date.now(),
+    usedTiles: {},
+    viewState: /** @type {olx.ViewState} */ ({
+      rotation: 0
+    }),
+    viewHints: [],
+    wantedTiles: {}
+  };
+
+  ol.source.Image.call(this, {});
+
+  if (options.operation !== undefined) {
+    this.setOperation(options.operation, options.lib);
+  }
+
+};
+ol.inherits(ol.source.Raster, ol.source.Image);
+
+
+/**
+ * Set the operation.
+ * @param {ol.RasterOperation} operation New operation.
+ * @param {Object=} opt_lib Functions that will be available to operations run
+ *     in a worker.
+ * @api
+ */
+ol.source.Raster.prototype.setOperation = function(operation, opt_lib) {
+  this.worker_ = new ol.ext.pixelworks.Processor({
+    operation: operation,
+    imageOps: this.operationType_ === ol.RasterOperationType.IMAGE,
+    queue: 1,
+    lib: opt_lib,
+    threads: this.threads_
+  });
+  this.changed();
+};
+
+
+/**
+ * Update the stored frame state.
+ * @param {ol.Extent} extent The view extent (in map units).
+ * @param {number} resolution The view resolution.
+ * @param {ol.proj.Projection} projection The view projection.
+ * @return {olx.FrameState} The updated frame state.
+ * @private
+ */
+ol.source.Raster.prototype.updateFrameState_ = function(extent, resolution, projection) {
+
+  var frameState = /** @type {olx.FrameState} */ (
+      ol.object.assign({}, this.frameState_));
+
+  frameState.viewState = /** @type {olx.ViewState} */ (
+      ol.object.assign({}, frameState.viewState));
+
+  var center = ol.extent.getCenter(extent);
+  var width = Math.round(ol.extent.getWidth(extent) / resolution);
+  var height = Math.round(ol.extent.getHeight(extent) / resolution);
+
+  frameState.extent = extent;
+  frameState.focus = ol.extent.getCenter(extent);
+  frameState.size[0] = width;
+  frameState.size[1] = height;
+
+  var viewState = frameState.viewState;
+  viewState.center = center;
+  viewState.projection = projection;
+  viewState.resolution = resolution;
+  return frameState;
+};
+
+
+/**
+ * Determine if the most recently rendered image canvas is dirty.
+ * @param {ol.Extent} extent The requested extent.
+ * @param {number} resolution The requested resolution.
+ * @return {boolean} The image is dirty.
+ * @private
+ */
+ol.source.Raster.prototype.isDirty_ = function(extent, resolution) {
+  var state = this.renderedState_;
+  return !state ||
+      this.getRevision() !== state.revision ||
+      resolution !== state.resolution ||
+      !ol.extent.equals(extent, state.extent);
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.source.Raster.prototype.getImage = function(extent, resolution, pixelRatio, projection) {
+
+  if (!this.allSourcesReady_()) {
+    return null;
+  }
+
+  var currentExtent = extent.slice();
+  if (!this.isDirty_(currentExtent, resolution)) {
+    return this.renderedImageCanvas_;
+  }
+
+  var context = this.canvasContext_;
+  var canvas = context.canvas;
+
+  var width = Math.round(ol.extent.getWidth(currentExtent) / resolution);
+  var height = Math.round(ol.extent.getHeight(currentExtent) / resolution);
+
+  if (width !== canvas.width ||
+      height !== canvas.height) {
+    canvas.width = width;
+    canvas.height = height;
+  }
+
+  var frameState = this.updateFrameState_(currentExtent, resolution, projection);
+
+  var imageCanvas = new ol.ImageCanvas(
+      currentExtent, resolution, 1, this.getAttributions(), canvas,
+      this.composeFrame_.bind(this, frameState));
+
+  this.renderedImageCanvas_ = imageCanvas;
+
+  this.renderedState_ = {
+    extent: currentExtent,
+    resolution: resolution,
+    revision: this.getRevision()
+  };
+
+  return imageCanvas;
+};
+
+
+/**
+ * Determine if all sources are ready.
+ * @return {boolean} All sources are ready.
+ * @private
+ */
+ol.source.Raster.prototype.allSourcesReady_ = function() {
+  var ready = true;
+  var source;
+  for (var i = 0, ii = this.renderers_.length; i < ii; ++i) {
+    source = this.renderers_[i].getLayer().getSource();
+    if (source.getState() !== ol.source.State.READY) {
+      ready = false;
+      break;
+    }
+  }
+  return ready;
+};
+
+
+/**
+ * Compose the frame.  This renders data from all sources, runs pixel-wise
+ * operations, and renders the result to the stored canvas context.
+ * @param {olx.FrameState} frameState The frame state.
+ * @param {function(Error)} callback Called when composition is complete.
+ * @private
+ */
+ol.source.Raster.prototype.composeFrame_ = function(frameState, callback) {
+  var len = this.renderers_.length;
+  var imageDatas = new Array(len);
+  for (var i = 0; i < len; ++i) {
+    var imageData = ol.source.Raster.getImageData_(
+        this.renderers_[i], frameState, frameState.layerStatesArray[i]);
+    if (imageData) {
+      imageDatas[i] = imageData;
+    } else {
+      // image not yet ready
+      return;
+    }
+  }
+
+  var data = {};
+  this.dispatchEvent(new ol.source.RasterEvent(
+      ol.source.RasterEventType.BEFOREOPERATIONS, frameState, data));
+
+  this.worker_.process(imageDatas, data,
+      this.onWorkerComplete_.bind(this, frameState, callback));
+
+  frameState.tileQueue.loadMoreTiles(16, 16);
+};
+
+
+/**
+ * Called when pixel processing is complete.
+ * @param {olx.FrameState} frameState The frame state.
+ * @param {function(Error)} callback Called when rendering is complete.
+ * @param {Error} err Any error during processing.
+ * @param {ImageData} output The output image data.
+ * @param {Object} data The user data.
+ * @private
+ */
+ol.source.Raster.prototype.onWorkerComplete_ = function(frameState, callback, err, output, data) {
+  if (err) {
+    callback(err);
+    return;
+  }
+  if (!output) {
+    // job aborted
+    return;
+  }
+
+  this.dispatchEvent(new ol.source.RasterEvent(
+      ol.source.RasterEventType.AFTEROPERATIONS, frameState, data));
+
+  var resolution = frameState.viewState.resolution / frameState.pixelRatio;
+  if (!this.isDirty_(frameState.extent, resolution)) {
+    this.canvasContext_.putImageData(output, 0, 0);
+  }
+
+  callback(null);
+};
+
+
+/**
+ * Get image data from a renderer.
+ * @param {ol.renderer.canvas.Layer} renderer Layer renderer.
+ * @param {olx.FrameState} frameState The frame state.
+ * @param {ol.LayerState} layerState The layer state.
+ * @return {ImageData} The image data.
+ * @private
+ */
+ol.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 (!ol.source.Raster.context_) {
+    ol.source.Raster.context_ = ol.dom.createCanvasContext2D(width, height);
+  } else {
+    var canvas = ol.source.Raster.context_.canvas;
+    if (canvas.width !== width || canvas.height !== height) {
+      ol.source.Raster.context_ = ol.dom.createCanvasContext2D(width, height);
+    } else {
+      ol.source.Raster.context_.clearRect(0, 0, width, height);
+    }
+  }
+  renderer.composeFrame(frameState, layerState, ol.source.Raster.context_);
+  return ol.source.Raster.context_.getImageData(0, 0, width, height);
+};
+
+
+/**
+ * A reusable canvas context.
+ * @type {CanvasRenderingContext2D}
+ * @private
+ */
+ol.source.Raster.context_ = null;
+
+
+/**
+ * Get a list of layer states from a list of renderers.
+ * @param {Array.<ol.renderer.canvas.Layer>} renderers Layer renderers.
+ * @return {Array.<ol.LayerState>} The layer states.
+ * @private
+ */
+ol.source.Raster.getLayerStatesArray_ = function(renderers) {
+  return renderers.map(function(renderer) {
+    return renderer.getLayer().getLayerState();
+  });
+};
+
+
+/**
+ * Create renderers for all sources.
+ * @param {Array.<ol.source.Source>} sources The sources.
+ * @return {Array.<ol.renderer.canvas.Layer>} Array of layer renderers.
+ * @private
+ */
+ol.source.Raster.createRenderers_ = function(sources) {
+  var len = sources.length;
+  var renderers = new Array(len);
+  for (var i = 0; i < len; ++i) {
+    renderers[i] = ol.source.Raster.createRenderer_(sources[i]);
+  }
+  return renderers;
+};
+
+
+/**
+ * Create a renderer for the provided source.
+ * @param {ol.source.Source} source The source.
+ * @return {ol.renderer.canvas.Layer} The renderer.
+ * @private
+ */
+ol.source.Raster.createRenderer_ = function(source) {
+  var renderer = null;
+  if (source instanceof ol.source.Tile) {
+    renderer = ol.source.Raster.createTileRenderer_(source);
+  } else if (source instanceof ol.source.Image) {
+    renderer = ol.source.Raster.createImageRenderer_(source);
+  } else {
+    goog.asserts.fail('Unsupported source type: ' + source);
+  }
+  return renderer;
+};
+
+
+/**
+ * Create an image renderer for the provided source.
+ * @param {ol.source.Image} source The source.
+ * @return {ol.renderer.canvas.Layer} The renderer.
+ * @private
+ */
+ol.source.Raster.createImageRenderer_ = function(source) {
+  var layer = new ol.layer.Image({source: source});
+  return new ol.renderer.canvas.ImageLayer(layer);
+};
+
+
+/**
+ * Create a tile renderer for the provided source.
+ * @param {ol.source.Tile} source The source.
+ * @return {ol.renderer.canvas.Layer} The renderer.
+ * @private
+ */
+ol.source.Raster.createTileRenderer_ = function(source) {
+  var layer = new ol.layer.Tile({source: source});
+  return new ol.renderer.canvas.TileLayer(layer);
+};
+
+
+/**
+ * @classdesc
+ * Events emitted by {@link ol.source.Raster} instances are instances of this
+ * type.
+ *
+ * @constructor
+ * @extends {ol.events.Event}
+ * @implements {oli.source.RasterEvent}
+ * @param {string} type Type.
+ * @param {olx.FrameState} frameState The frame state.
+ * @param {Object} data An object made available to operations.
+ */
+ol.source.RasterEvent = function(type, frameState, data) {
+  ol.events.Event.call(this, type);
+
+  /**
+   * The raster extent.
+   * @type {ol.Extent}
+   * @api
+   */
+  this.extent = frameState.extent;
+
+  /**
+   * The pixel resolution (map units per pixel).
+   * @type {number}
+   * @api
+   */
+  this.resolution = frameState.viewState.resolution / frameState.pixelRatio;
+
+  /**
+   * An object made available to all operations.  This can be used by operations
+   * as a storage object (e.g. for calculating statistics).
+   * @type {Object}
+   * @api
+   */
+  this.data = data;
+
+};
+ol.inherits(ol.source.RasterEvent, ol.events.Event);
+
+
+/**
+ * @enum {string}
+ */
+ol.source.RasterEventType = {
+  /**
+   * Triggered before operations are run.
+   * @event ol.source.RasterEvent#beforeoperations
+   * @api
+   */
+  BEFOREOPERATIONS: 'beforeoperations',
+
+  /**
+   * Triggered after operations are run.
+   * @event ol.source.RasterEvent#afteroperations
+   * @api
+   */
+  AFTEROPERATIONS: 'afteroperations'
+};
+
+goog.provide('ol.source.Stamen');
+
+goog.require('goog.asserts');
+goog.require('ol.Attribution');
+goog.require('ol.source.OSM');
+goog.require('ol.source.XYZ');
+
+
+/**
+ * @type {Object.<string, {extension: string, opaque: boolean}>}
+ */
+ol.source.StamenLayerConfig = {
+  'terrain': {
+    extension: 'jpg',
+    opaque: true
+  },
+  'terrain-background': {
+    extension: 'jpg',
+    opaque: true
+  },
+  'terrain-labels': {
+    extension: 'png',
+    opaque: false
+  },
+  'terrain-lines': {
+    extension: 'png',
+    opaque: false
+  },
+  'toner-background': {
+    extension: 'png',
+    opaque: true
+  },
+  'toner': {
+    extension: 'png',
+    opaque: true
+  },
+  'toner-hybrid': {
+    extension: 'png',
+    opaque: false
+  },
+  'toner-labels': {
+    extension: 'png',
+    opaque: false
+  },
+  'toner-lines': {
+    extension: 'png',
+    opaque: false
+  },
+  'toner-lite': {
+    extension: 'png',
+    opaque: true
+  },
+  'watercolor': {
+    extension: 'jpg',
+    opaque: true
+  }
+};
+
+
+/**
+ * @type {Object.<string, {minZoom: number, maxZoom: number}>}
+ */
+ol.source.StamenProviderConfig = {
+  'terrain': {
+    minZoom: 4,
+    maxZoom: 18
+  },
+  'toner': {
+    minZoom: 0,
+    maxZoom: 20
+  },
+  'watercolor': {
+    minZoom: 1,
+    maxZoom: 16
+  }
+};
+
+
+/**
+ * @classdesc
+ * Layer source for the Stamen tile server.
+ *
+ * @constructor
+ * @extends {ol.source.XYZ}
+ * @param {olx.source.StamenOptions} options Stamen options.
+ * @api stable
+ */
+ol.source.Stamen = function(options) {
+
+  var i = options.layer.indexOf('-');
+  var provider = i == -1 ? options.layer : options.layer.slice(0, i);
+  goog.asserts.assert(provider in ol.source.StamenProviderConfig,
+      'known provider configured');
+  var providerConfig = ol.source.StamenProviderConfig[provider];
+
+  goog.asserts.assert(options.layer in ol.source.StamenLayerConfig,
+      'known layer configured');
+  var layerConfig = ol.source.StamenLayerConfig[options.layer];
+
+  var url = options.url !== undefined ? options.url :
+      'https://stamen-tiles-{a-d}.a.ssl.fastly.net/' + options.layer +
+      '/{z}/{x}/{y}.' + layerConfig.extension;
+
+  ol.source.XYZ.call(this, {
+    attributions: ol.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,
+    reprojectionErrorThreshold: options.reprojectionErrorThreshold,
+    tileLoadFunction: options.tileLoadFunction,
+    url: url
+  });
+
+};
+ol.inherits(ol.source.Stamen, ol.source.XYZ);
+
+
+/**
+ * @const
+ * @type {Array.<ol.Attribution>}
+ */
+ol.source.Stamen.ATTRIBUTIONS = [
+  new ol.Attribution({
+    html: 'Map tiles by <a href="http://stamen.com/">Stamen Design</a>, ' +
+        'under <a href="http://creativecommons.org/licenses/by/3.0/">CC BY' +
+        ' 3.0</a>.'
+  }),
+  ol.source.OSM.ATTRIBUTION
+];
+
+goog.provide('ol.source.TileArcGISRest');
+
+goog.require('goog.asserts');
+goog.require('ol');
+goog.require('ol.extent');
+goog.require('ol.object');
+goog.require('ol.math');
+goog.require('ol.proj');
+goog.require('ol.size');
+goog.require('ol.source.TileImage');
+goog.require('ol.tilecoord');
+goog.require('ol.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 ol.source.XYZ} data source.
+ *
+ * @constructor
+ * @extends {ol.source.TileImage}
+ * @param {olx.source.TileArcGISRestOptions=} opt_options Tile ArcGIS Rest
+ *     options.
+ * @api
+ */
+ol.source.TileArcGISRest = function(opt_options) {
+
+  var options = opt_options || {};
+
+  ol.source.TileImage.call(this, {
+    attributions: options.attributions,
+    cacheSize: options.cacheSize,
+    crossOrigin: options.crossOrigin,
+    logo: options.logo,
+    projection: options.projection,
+    reprojectionErrorThreshold: options.reprojectionErrorThreshold,
+    tileGrid: options.tileGrid,
+    tileLoadFunction: options.tileLoadFunction,
+    url: options.url,
+    urls: options.urls,
+    wrapX: options.wrapX !== undefined ? options.wrapX : true
+  });
+
+  /**
+   * @private
+   * @type {!Object}
+   */
+  this.params_ = options.params || {};
+
+  /**
+   * @private
+   * @type {ol.Extent}
+   */
+  this.tmpExtent_ = ol.extent.createEmpty();
+
+};
+ol.inherits(ol.source.TileArcGISRest, ol.source.TileImage);
+
+
+/**
+ * Get the user-provided params, i.e. those passed to the constructor through
+ * the "params" option, and possibly updated using the updateParams method.
+ * @return {Object} Params.
+ * @api
+ */
+ol.source.TileArcGISRest.prototype.getParams = function() {
+  return this.params_;
+};
+
+
+/**
+ * @param {ol.TileCoord} tileCoord Tile coordinate.
+ * @param {ol.Size} tileSize Tile size.
+ * @param {ol.Extent} tileExtent Tile extent.
+ * @param {number} pixelRatio Pixel ratio.
+ * @param {ol.proj.Projection} projection Projection.
+ * @param {Object} params Params.
+ * @return {string|undefined} Request URL.
+ * @private
+ */
+ol.source.TileArcGISRest.prototype.getRequestUrl_ = function(tileCoord, tileSize, tileExtent,
+        pixelRatio, projection, params) {
+
+  var urls = this.urls;
+  if (!urls) {
+    return undefined;
+  }
+
+  // ArcGIS Server only wants the numeric portion of the projection ID.
+  var srid = projection.getCode().split(':').pop();
+
+  params['SIZE'] = tileSize[0] + ',' + tileSize[1];
+  params['BBOX'] = tileExtent.join(',');
+  params['BBOXSR'] = srid;
+  params['IMAGESR'] = srid;
+  params['DPI'] = Math.round(
+      params['DPI'] ? params['DPI'] * pixelRatio : 90 * pixelRatio
+      );
+
+  var url;
+  if (urls.length == 1) {
+    url = urls[0];
+  } else {
+    var index = ol.math.modulo(ol.tilecoord.hash(tileCoord), urls.length);
+    url = urls[index];
+  }
+
+  var modifiedUrl = url
+      .replace(/MapServer\/?$/, 'MapServer/export')
+      .replace(/ImageServer\/?$/, 'ImageServer/exportImage');
+  if (modifiedUrl == url) {
+    goog.asserts.fail('Unknown Rest Service', url);
+  }
+  return ol.uri.appendParams(modifiedUrl, params);
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.source.TileArcGISRest.prototype.getTilePixelRatio = function(pixelRatio) {
+  return pixelRatio;
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.source.TileArcGISRest.prototype.fixedTileUrlFunction = function(tileCoord, pixelRatio, projection) {
+
+  var tileGrid = this.getTileGrid();
+  if (!tileGrid) {
+    tileGrid = this.getTileGridForProjection(projection);
+  }
+
+  if (tileGrid.getResolutions().length <= tileCoord[0]) {
+    return undefined;
+  }
+
+  var tileExtent = tileGrid.getTileCoordExtent(
+      tileCoord, this.tmpExtent_);
+  var tileSize = ol.size.toSize(
+      tileGrid.getTileSize(tileCoord[0]), this.tmpSize);
+
+  if (pixelRatio != 1) {
+    tileSize = ol.size.scale(tileSize, pixelRatio, this.tmpSize);
+  }
+
+  // Apply default params and override with user specified values.
+  var baseParams = {
+    'F': 'image',
+    'FORMAT': 'PNG32',
+    'TRANSPARENT': true
+  };
+  ol.object.assign(baseParams, this.params_);
+
+  return this.getRequestUrl_(tileCoord, tileSize, tileExtent,
+      pixelRatio, projection, baseParams);
+};
+
+
+/**
+ * Update the user-provided params.
+ * @param {Object} params Params.
+ * @api stable
+ */
+ol.source.TileArcGISRest.prototype.updateParams = function(params) {
+  ol.object.assign(this.params_, params);
+  this.changed();
+};
+
+goog.provide('ol.source.TileDebug');
+
+goog.require('ol.Tile');
+goog.require('ol.TileState');
+goog.require('ol.dom');
+goog.require('ol.size');
+goog.require('ol.source.Tile');
+
+
+/**
+ * @constructor
+ * @extends {ol.Tile}
+ * @param {ol.TileCoord} tileCoord Tile coordinate.
+ * @param {ol.Size} tileSize Tile size.
+ * @param {string} text Text.
+ * @private
+ */
+ol.DebugTile_ = function(tileCoord, tileSize, text) {
+
+  ol.Tile.call(this, tileCoord, ol.TileState.LOADED);
+
+  /**
+   * @private
+   * @type {ol.Size}
+   */
+  this.tileSize_ = tileSize;
+
+  /**
+   * @private
+   * @type {string}
+   */
+  this.text_ = text;
+
+  /**
+   * @private
+   * @type {Object.<number, HTMLCanvasElement>}
+   */
+  this.canvasByContext_ = {};
+
+};
+ol.inherits(ol.DebugTile_, ol.Tile);
+
+
+/**
+ * Get the image element for this tile.
+ * @param {Object=} opt_context Optional context. Only used by the DOM
+ *     renderer.
+ * @return {HTMLCanvasElement} Image.
+ */
+ol.DebugTile_.prototype.getImage = function(opt_context) {
+  var key = opt_context !== undefined ? goog.getUid(opt_context) : -1;
+  if (key in this.canvasByContext_) {
+    return this.canvasByContext_[key];
+  } else {
+
+    var tileSize = this.tileSize_;
+    var context = ol.dom.createCanvasContext2D(tileSize[0], tileSize[1]);
+
+    context.strokeStyle = 'black';
+    context.strokeRect(0.5, 0.5, tileSize[0] + 0.5, tileSize[1] + 0.5);
+
+    context.fillStyle = 'black';
+    context.textAlign = 'center';
+    context.textBaseline = 'middle';
+    context.font = '24px sans-serif';
+    context.fillText(this.text_, tileSize[0] / 2, tileSize[1] / 2);
+
+    this.canvasByContext_[key] = context.canvas;
+    return context.canvas;
+
+  }
+};
+
+
+/**
+ * @classdesc
+ * A pseudo tile source, which does not fetch tiles from a server, but renders
+ * a grid outline for the tile grid/projection along with the coordinates for
+ * each tile. See examples/canvas-tiles for an example.
+ *
+ * Uses Canvas context2d, so requires Canvas support.
+ *
+ * @constructor
+ * @extends {ol.source.Tile}
+ * @param {olx.source.TileDebugOptions} options Debug tile options.
+ * @api
+ */
+ol.source.TileDebug = function(options) {
+
+  ol.source.Tile.call(this, {
+    opaque: false,
+    projection: options.projection,
+    tileGrid: options.tileGrid,
+    wrapX: options.wrapX !== undefined ? options.wrapX : true
+  });
+
+};
+ol.inherits(ol.source.TileDebug, ol.source.Tile);
+
+
+/**
+ * @inheritDoc
+ */
+ol.source.TileDebug.prototype.getTile = function(z, x, y) {
+  var tileCoordKey = this.getKeyZXY(z, x, y);
+  if (this.tileCache.containsKey(tileCoordKey)) {
+    return /** @type {!ol.DebugTile_} */ (this.tileCache.get(tileCoordKey));
+  } else {
+    var tileSize = ol.size.toSize(this.tileGrid.getTileSize(z));
+    var tileCoord = [z, x, y];
+    var textTileCoord = this.getTileCoordForTileUrlFunction(tileCoord);
+    var text = !textTileCoord ? '' :
+        this.getTileCoordForTileUrlFunction(textTileCoord).toString();
+    var tile = new ol.DebugTile_(tileCoord, tileSize, text);
+    this.tileCache.set(tileCoordKey, tile);
+    return tile;
+  }
+};
+
+// FIXME check order of async callbacks
+
+/**
+ * @see http://mapbox.com/developers/api/
+ */
+
+goog.provide('ol.source.TileJSON');
+goog.provide('ol.tilejson');
+
+goog.require('goog.asserts');
+goog.require('ol.Attribution');
+goog.require('ol.TileRange');
+goog.require('ol.TileUrlFunction');
+goog.require('ol.extent');
+goog.require('ol.net');
+goog.require('ol.proj');
+goog.require('ol.source.State');
+goog.require('ol.source.TileImage');
+
+
+/**
+ * @classdesc
+ * Layer source for tile data in TileJSON format.
+ *
+ * @constructor
+ * @extends {ol.source.TileImage}
+ * @param {olx.source.TileJSONOptions} options TileJSON options.
+ * @api stable
+ */
+ol.source.TileJSON = function(options) {
+
+  /**
+   * @type {TileJSON}
+   * @private
+   */
+  this.tileJSON_ = null;
+
+  ol.source.TileImage.call(this, {
+    attributions: options.attributions,
+    cacheSize: options.cacheSize,
+    crossOrigin: options.crossOrigin,
+    projection: ol.proj.get('EPSG:3857'),
+    reprojectionErrorThreshold: options.reprojectionErrorThreshold,
+    state: ol.source.State.LOADING,
+    tileLoadFunction: options.tileLoadFunction,
+    wrapX: options.wrapX !== undefined ? options.wrapX : true
+  });
+
+  if (options.jsonp) {
+    ol.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();
+  }
+
+};
+ol.inherits(ol.source.TileJSON, ol.source.TileImage);
+
+
+/**
+ * @private
+ * @param {Event} event The load event.
+ */
+ol.source.TileJSON.prototype.onXHRLoad_ = function(event) {
+  var client = /** @type {XMLHttpRequest} */ (event.target);
+  if (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.
+ */
+ol.source.TileJSON.prototype.onXHRError_ = function(event) {
+  this.handleTileJSONError();
+};
+
+
+/**
+ * @return {TileJSON} The tilejson object.
+ * @api
+ */
+ol.source.TileJSON.prototype.getTileJSON = function() {
+  return this.tileJSON_;
+};
+
+
+/**
+ * @protected
+ * @param {TileJSON} tileJSON Tile JSON.
+ */
+ol.source.TileJSON.prototype.handleTileJSONResponse = function(tileJSON) {
+
+  var epsg4326Projection = ol.proj.get('EPSG:4326');
+
+  var sourceProjection = this.getProjection();
+  var extent;
+  if (tileJSON.bounds !== undefined) {
+    var transform = ol.proj.getTransformFromProjections(
+        epsg4326Projection, sourceProjection);
+    extent = ol.extent.applyTransform(tileJSON.bounds, transform);
+  }
+
+  if (tileJSON.scheme !== undefined) {
+    goog.asserts.assert(tileJSON.scheme == 'xyz', 'tileJSON-scheme is "xyz"');
+  }
+  var minZoom = tileJSON.minzoom || 0;
+  var maxZoom = tileJSON.maxzoom || 22;
+  var tileGrid = ol.tilegrid.createXYZ({
+    extent: ol.tilegrid.extentFromProjection(sourceProjection),
+    maxZoom: maxZoom,
+    minZoom: minZoom
+  });
+  this.tileGrid = tileGrid;
+
+  this.tileUrlFunction =
+      ol.TileUrlFunction.createFromTemplates(tileJSON.tiles, tileGrid);
+
+  if (tileJSON.attribution !== undefined && !this.getAttributions()) {
+    var attributionExtent = extent !== undefined ?
+        extent : epsg4326Projection.getExtent();
+    /** @type {Object.<string, Array.<ol.TileRange>>} */
+    var tileRanges = {};
+    var z, zKey;
+    for (z = minZoom; z <= maxZoom; ++z) {
+      zKey = z.toString();
+      tileRanges[zKey] =
+          [tileGrid.getTileRangeForExtentAndZ(attributionExtent, z)];
+    }
+    this.setAttributions([
+      new ol.Attribution({
+        html: tileJSON.attribution,
+        tileRanges: tileRanges
+      })
+    ]);
+  }
+  this.tileJSON_ = tileJSON;
+  this.setState(ol.source.State.READY);
+
+};
+
+
+/**
+ * @protected
+ */
+ol.source.TileJSON.prototype.handleTileJSONError = function() {
+  this.setState(ol.source.State.ERROR);
+};
+
+goog.provide('ol.source.TileUTFGrid');
+
+goog.require('goog.asserts');
+goog.require('goog.async.nextTick');
+goog.require('ol.Attribution');
+goog.require('ol.Tile');
+goog.require('ol.TileState');
+goog.require('ol.TileUrlFunction');
+goog.require('ol.events');
+goog.require('ol.events.EventType');
+goog.require('ol.extent');
+goog.require('ol.net');
+goog.require('ol.proj');
+goog.require('ol.source.State');
+goog.require('ol.source.Tile');
+
+
+/**
+ * @classdesc
+ * Layer source for UTFGrid interaction data loaded from TileJSON format.
+ *
+ * @constructor
+ * @extends {ol.source.Tile}
+ * @param {olx.source.TileUTFGridOptions} options Source options.
+ * @api
+ */
+ol.source.TileUTFGrid = function(options) {
+  ol.source.Tile.call(this, {
+    projection: ol.proj.get('EPSG:3857'),
+    state: ol.source.State.LOADING
+  });
+
+  /**
+   * @private
+   * @type {boolean}
+   */
+  this.preemptive_ = options.preemptive !== undefined ?
+      options.preemptive : true;
+
+  /**
+   * @private
+   * @type {!ol.TileUrlFunctionType}
+   */
+  this.tileUrlFunction_ = ol.TileUrlFunction.nullTileUrlFunction;
+
+  /**
+   * @private
+   * @type {string|undefined}
+   */
+  this.template_ = undefined;
+
+  /**
+   * @private
+   * @type {boolean}
+   */
+  this.jsonp_ = options.jsonp || false;
+
+  if (options.url) {
+    if (this.jsonp_) {
+      ol.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 {
+    goog.asserts.fail('Either url or tileJSON options must be provided');
+  }
+};
+ol.inherits(ol.source.TileUTFGrid, ol.source.Tile);
+
+
+/**
+ * @private
+ * @param {Event} event The load event.
+ */
+ol.source.TileUTFGrid.prototype.onXHRLoad_ = function(event) {
+  var client = /** @type {XMLHttpRequest} */ (event.target);
+  if (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.
+ */
+ol.source.TileUTFGrid.prototype.onXHRError_ = function(event) {
+  this.handleTileJSONError();
+};
+
+
+/**
+ * Return the template from TileJSON.
+ * @return {string|undefined} The template from TileJSON.
+ * @api
+ */
+ol.source.TileUTFGrid.prototype.getTemplate = function() {
+  return this.template_;
+};
+
+
+/**
+ * Calls the callback (synchronously by default) with the available data
+ * for given coordinate and resolution (or `null` if not yet loaded or
+ * in case of an error).
+ * @param {ol.Coordinate} coordinate Coordinate.
+ * @param {number} resolution Resolution.
+ * @param {function(this: T, *)} callback Callback.
+ * @param {T=} opt_this The object to use as `this` in the callback.
+ * @param {boolean=} opt_request If `true` the callback is always async.
+ *                               The tile data is requested if not yet loaded.
+ * @template T
+ * @api
+ */
+ol.source.TileUTFGrid.prototype.forDataAtCoordinateAndResolution = function(
+    coordinate, resolution, callback, opt_this, opt_request) {
+  if (this.tileGrid) {
+    var tileCoord = this.tileGrid.getTileCoordForCoordAndResolution(
+        coordinate, resolution);
+    var tile = /** @type {!ol.source.TileUTFGridTile_} */(this.getTile(
+        tileCoord[0], tileCoord[1], tileCoord[2], 1, this.getProjection()));
+    tile.forDataAtCoordinate(coordinate, callback, opt_this, opt_request);
+  } else {
+    if (opt_request === true) {
+      goog.async.nextTick(function() {
+        callback.call(opt_this, null);
+      });
+    } else {
+      callback.call(opt_this, null);
+    }
+  }
+};
+
+
+/**
+ * @protected
+ */
+ol.source.TileUTFGrid.prototype.handleTileJSONError = function() {
+  this.setState(ol.source.State.ERROR);
+};
+
+
+/**
+ * TODO: very similar to ol.source.TileJSON#handleTileJSONResponse
+ * @protected
+ * @param {TileJSON} tileJSON Tile JSON.
+ */
+ol.source.TileUTFGrid.prototype.handleTileJSONResponse = function(tileJSON) {
+
+  var epsg4326Projection = ol.proj.get('EPSG:4326');
+
+  var sourceProjection = this.getProjection();
+  var extent;
+  if (tileJSON.bounds !== undefined) {
+    var transform = ol.proj.getTransformFromProjections(
+        epsg4326Projection, sourceProjection);
+    extent = ol.extent.applyTransform(tileJSON.bounds, transform);
+  }
+
+  if (tileJSON.scheme !== undefined) {
+    goog.asserts.assert(tileJSON.scheme == 'xyz', 'tileJSON-scheme is "xyz"');
+  }
+  var minZoom = tileJSON.minzoom || 0;
+  var maxZoom = tileJSON.maxzoom || 22;
+  var tileGrid = ol.tilegrid.createXYZ({
+    extent: ol.tilegrid.extentFromProjection(sourceProjection),
+    maxZoom: maxZoom,
+    minZoom: minZoom
+  });
+  this.tileGrid = tileGrid;
+
+  this.template_ = tileJSON.template;
+
+  var grids = tileJSON.grids;
+  if (!grids) {
+    this.setState(ol.source.State.ERROR);
+    return;
+  }
+
+  this.tileUrlFunction_ =
+      ol.TileUrlFunction.createFromTemplates(grids, tileGrid);
+
+  if (tileJSON.attribution !== undefined) {
+    var attributionExtent = extent !== undefined ?
+        extent : epsg4326Projection.getExtent();
+    /** @type {Object.<string, Array.<ol.TileRange>>} */
+    var tileRanges = {};
+    var z, zKey;
+    for (z = minZoom; z <= maxZoom; ++z) {
+      zKey = z.toString();
+      tileRanges[zKey] =
+          [tileGrid.getTileRangeForExtentAndZ(attributionExtent, z)];
+    }
+    this.setAttributions([
+      new ol.Attribution({
+        html: tileJSON.attribution,
+        tileRanges: tileRanges
+      })
+    ]);
+  }
+
+  this.setState(ol.source.State.READY);
+
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.source.TileUTFGrid.prototype.getTile = function(z, x, y, pixelRatio, projection) {
+  var tileCoordKey = this.getKeyZXY(z, x, y);
+  if (this.tileCache.containsKey(tileCoordKey)) {
+    return /** @type {!ol.Tile} */ (this.tileCache.get(tileCoordKey));
+  } else {
+    goog.asserts.assert(projection, 'argument projection is truthy');
+    var tileCoord = [z, x, y];
+    var urlTileCoord =
+        this.getTileCoordForTileUrlFunction(tileCoord, projection);
+    var tileUrl = this.tileUrlFunction_(urlTileCoord, pixelRatio, projection);
+    var tile = new ol.source.TileUTFGridTile_(
+        tileCoord,
+        tileUrl !== undefined ? ol.TileState.IDLE : ol.TileState.EMPTY,
+        tileUrl !== undefined ? tileUrl : '',
+        this.tileGrid.getTileCoordExtent(tileCoord),
+        this.preemptive_,
+        this.jsonp_);
+    this.tileCache.set(tileCoordKey, tile);
+    return tile;
+  }
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.source.TileUTFGrid.prototype.useTile = function(z, x, y) {
+  var tileCoordKey = this.getKeyZXY(z, x, y);
+  if (this.tileCache.containsKey(tileCoordKey)) {
+    this.tileCache.get(tileCoordKey);
+  }
+};
+
+
+/**
+ * @constructor
+ * @extends {ol.Tile}
+ * @param {ol.TileCoord} tileCoord Tile coordinate.
+ * @param {ol.TileState} state State.
+ * @param {string} src Image source URI.
+ * @param {ol.Extent} extent Extent of the tile.
+ * @param {boolean} preemptive Load the tile when visible (before it's needed).
+ * @param {boolean} jsonp Load the tile as a script.
+ * @private
+ */
+ol.source.TileUTFGridTile_ = function(tileCoord, state, src, extent, preemptive, jsonp) {
+
+  ol.Tile.call(this, tileCoord, state);
+
+  /**
+   * @private
+   * @type {string}
+   */
+  this.src_ = src;
+
+  /**
+   * @private
+   * @type {ol.Extent}
+   */
+  this.extent_ = extent;
+
+  /**
+   * @private
+   * @type {boolean}
+   */
+  this.preemptive_ = preemptive;
+
+  /**
+   * @private
+   * @type {Array.<string>}
+   */
+  this.grid_ = null;
+
+  /**
+   * @private
+   * @type {Array.<string>}
+   */
+  this.keys_ = null;
+
+  /**
+   * @private
+   * @type {Object.<string, Object>|undefined}
+   */
+  this.data_ = null;
+
+
+  /**
+   * @private
+   * @type {boolean}
+   */
+  this.jsonp_ = jsonp;
+
+};
+ol.inherits(ol.source.TileUTFGridTile_, ol.Tile);
+
+
+/**
+ * Get the image element for this tile.
+ * @param {Object=} opt_context Optional context. Only used for the DOM
+ *     renderer.
+ * @return {Image} Image.
+ */
+ol.source.TileUTFGridTile_.prototype.getImage = function(opt_context) {
+  return null;
+};
+
+
+/**
+ * Synchronously returns data at given coordinate (if available).
+ * @param {ol.Coordinate} coordinate Coordinate.
+ * @return {*} The data.
+ */
+ol.source.TileUTFGridTile_.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 {ol.Coordinate} coordinate Coordinate.
+ * @param {function(this: T, *)} callback Callback.
+ * @param {T=} opt_this The object to use as `this` in the callback.
+ * @param {boolean=} opt_request If `true` the callback is always async.
+ *                               The tile data is requested if not yet loaded.
+ * @template T
+ */
+ol.source.TileUTFGridTile_.prototype.forDataAtCoordinate = function(coordinate, callback, opt_this, opt_request) {
+  if (this.state == ol.TileState.IDLE && opt_request === true) {
+    ol.events.listenOnce(this, ol.events.EventType.CHANGE, function(e) {
+      callback.call(opt_this, this.getData(coordinate));
+    }, this);
+    this.loadInternal_();
+  } else {
+    if (opt_request === true) {
+      goog.async.nextTick(function() {
+        callback.call(opt_this, this.getData(coordinate));
+      }, this);
+    } else {
+      callback.call(opt_this, this.getData(coordinate));
+    }
+  }
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.source.TileUTFGridTile_.prototype.getKey = function() {
+  return this.src_;
+};
+
+
+/**
+ * @private
+ */
+ol.source.TileUTFGridTile_.prototype.handleError_ = function() {
+  this.state = ol.TileState.ERROR;
+  this.changed();
+};
+
+
+/**
+ * @param {!UTFGridJSON} json UTFGrid data.
+ * @private
+ */
+ol.source.TileUTFGridTile_.prototype.handleLoad_ = function(json) {
+  this.grid_ = json.grid;
+  this.keys_ = json.keys;
+  this.data_ = json.data;
+
+  this.state = ol.TileState.EMPTY;
+  this.changed();
+};
+
+
+/**
+ * @private
+ */
+ol.source.TileUTFGridTile_.prototype.loadInternal_ = function() {
+  if (this.state == ol.TileState.IDLE) {
+    this.state = ol.TileState.LOADING;
+    if (this.jsonp_) {
+      ol.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.
+ */
+ol.source.TileUTFGridTile_.prototype.onXHRLoad_ = function(event) {
+  var client = /** @type {XMLHttpRequest} */ (event.target);
+  if (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.
+ */
+ol.source.TileUTFGridTile_.prototype.onXHRError_ = function(event) {
+  this.handleError_();
+};
+
+
+/**
+ * Load not yet loaded URI.
+ */
+ol.source.TileUTFGridTile_.prototype.load = function() {
+  if (this.preemptive_) {
+    this.loadInternal_();
+  }
+};
+
+// FIXME add minZoom support
+// FIXME add date line wrap (tile coord transform)
+// FIXME cannot be shared between maps with different projections
+
+goog.provide('ol.source.TileWMS');
+
+goog.require('goog.asserts');
+goog.require('ol');
+goog.require('ol.extent');
+goog.require('ol.object');
+goog.require('ol.math');
+goog.require('ol.proj');
+goog.require('ol.size');
+goog.require('ol.source.TileImage');
+goog.require('ol.source.wms');
+goog.require('ol.source.wms.ServerType');
+goog.require('ol.tilecoord');
+goog.require('ol.string');
+goog.require('ol.uri');
+
+/**
+ * @classdesc
+ * Layer source for tile data from WMS servers.
+ *
+ * @constructor
+ * @extends {ol.source.TileImage}
+ * @param {olx.source.TileWMSOptions=} opt_options Tile WMS options.
+ * @api stable
+ */
+ol.source.TileWMS = function(opt_options) {
+
+  var options = opt_options || {};
+
+  var params = options.params || {};
+
+  var transparent = 'TRANSPARENT' in params ? params['TRANSPARENT'] : true;
+
+  ol.source.TileImage.call(this, {
+    attributions: options.attributions,
+    cacheSize: options.cacheSize,
+    crossOrigin: options.crossOrigin,
+    logo: options.logo,
+    opaque: !transparent,
+    projection: options.projection,
+    reprojectionErrorThreshold: options.reprojectionErrorThreshold,
+    tileGrid: options.tileGrid,
+    tileLoadFunction: options.tileLoadFunction,
+    url: options.url,
+    urls: options.urls,
+    wrapX: options.wrapX !== undefined ? options.wrapX : true
+  });
+
+  /**
+   * @private
+   * @type {number}
+   */
+  this.gutter_ = options.gutter !== undefined ? options.gutter : 0;
+
+  /**
+   * @private
+   * @type {!Object}
+   */
+  this.params_ = params;
+
+  /**
+   * @private
+   * @type {boolean}
+   */
+  this.v13_ = true;
+
+  /**
+   * @private
+   * @type {ol.source.wms.ServerType|undefined}
+   */
+  this.serverType_ =
+      /** @type {ol.source.wms.ServerType|undefined} */ (options.serverType);
+
+  /**
+   * @private
+   * @type {boolean}
+   */
+  this.hidpi_ = options.hidpi !== undefined ? options.hidpi : true;
+
+  /**
+   * @private
+   * @type {string}
+   */
+  this.coordKeyPrefix_ = '';
+  this.resetCoordKeyPrefix_();
+
+  /**
+   * @private
+   * @type {ol.Extent}
+   */
+  this.tmpExtent_ = ol.extent.createEmpty();
+
+  this.updateV13_();
+  this.setKey(this.getKeyForParams_());
+
+};
+ol.inherits(ol.source.TileWMS, ol.source.TileImage);
+
+
+/**
+ * Return the GetFeatureInfo URL for the passed coordinate, resolution, and
+ * projection. Return `undefined` if the GetFeatureInfo URL cannot be
+ * constructed.
+ * @param {ol.Coordinate} coordinate Coordinate.
+ * @param {number} resolution Resolution.
+ * @param {ol.ProjectionLike} projection Projection.
+ * @param {!Object} params GetFeatureInfo params. `INFO_FORMAT` at least should
+ *     be provided. If `QUERY_LAYERS` is not provided then the layers specified
+ *     in the `LAYERS` parameter will be used. `VERSION` should not be
+ *     specified here.
+ * @return {string|undefined} GetFeatureInfo URL.
+ * @api stable
+ */
+ol.source.TileWMS.prototype.getGetFeatureInfoUrl = function(coordinate, resolution, projection, params) {
+
+  goog.asserts.assert(!('VERSION' in params),
+      'key VERSION is not allowed in params');
+
+  var projectionObj = ol.proj.get(projection);
+
+  var tileGrid = this.getTileGrid();
+  if (!tileGrid) {
+    tileGrid = this.getTileGridForProjection(projectionObj);
+  }
+
+  var tileCoord = tileGrid.getTileCoordForCoordAndResolution(
+      coordinate, resolution);
+
+  if (tileGrid.getResolutions().length <= tileCoord[0]) {
+    return undefined;
+  }
+
+  var tileResolution = tileGrid.getResolution(tileCoord[0]);
+  var tileExtent = tileGrid.getTileCoordExtent(tileCoord, this.tmpExtent_);
+  var tileSize = ol.size.toSize(
+      tileGrid.getTileSize(tileCoord[0]), this.tmpSize);
+
+  var gutter = this.gutter_;
+  if (gutter !== 0) {
+    tileSize = ol.size.buffer(tileSize, gutter, this.tmpSize);
+    tileExtent = ol.extent.buffer(tileExtent,
+        tileResolution * gutter, tileExtent);
+  }
+
+  var baseParams = {
+    'SERVICE': 'WMS',
+    'VERSION': ol.DEFAULT_WMS_VERSION,
+    'REQUEST': 'GetFeatureInfo',
+    'FORMAT': 'image/png',
+    'TRANSPARENT': true,
+    'QUERY_LAYERS': this.params_['LAYERS']
+  };
+  ol.object.assign(baseParams, this.params_, params);
+
+  var x = Math.floor((coordinate[0] - tileExtent[0]) / tileResolution);
+  var y = Math.floor((tileExtent[3] - coordinate[1]) / tileResolution);
+
+  baseParams[this.v13_ ? 'I' : 'X'] = x;
+  baseParams[this.v13_ ? 'J' : 'Y'] = y;
+
+  return this.getRequestUrl_(tileCoord, tileSize, tileExtent,
+      1, projectionObj, baseParams);
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.source.TileWMS.prototype.getGutterInternal = function() {
+  return this.gutter_;
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.source.TileWMS.prototype.getKeyZXY = function(z, x, y) {
+  return this.coordKeyPrefix_ + ol.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 stable
+ */
+ol.source.TileWMS.prototype.getParams = function() {
+  return this.params_;
+};
+
+
+/**
+ * @param {ol.TileCoord} tileCoord Tile coordinate.
+ * @param {ol.Size} tileSize Tile size.
+ * @param {ol.Extent} tileExtent Tile extent.
+ * @param {number} pixelRatio Pixel ratio.
+ * @param {ol.proj.Projection} projection Projection.
+ * @param {Object} params Params.
+ * @return {string|undefined} Request URL.
+ * @private
+ */
+ol.source.TileWMS.prototype.getRequestUrl_ = function(tileCoord, tileSize, tileExtent,
+        pixelRatio, projection, params) {
+
+  var urls = this.urls;
+  if (!urls) {
+    return undefined;
+  }
+
+  params['WIDTH'] = tileSize[0];
+  params['HEIGHT'] = tileSize[1];
+
+  params[this.v13_ ? 'CRS' : 'SRS'] = projection.getCode();
+
+  if (!('STYLES' in this.params_)) {
+    params['STYLES'] = '';
+  }
+
+  if (pixelRatio != 1) {
+    switch (this.serverType_) {
+      case ol.source.wms.ServerType.GEOSERVER:
+        var dpi = (90 * pixelRatio + 0.5) | 0;
+        if ('FORMAT_OPTIONS' in params) {
+          params['FORMAT_OPTIONS'] += ';dpi:' + dpi;
+        } else {
+          params['FORMAT_OPTIONS'] = 'dpi:' + dpi;
+        }
+        break;
+      case ol.source.wms.ServerType.MAPSERVER:
+        params['MAP_RESOLUTION'] = 90 * pixelRatio;
+        break;
+      case ol.source.wms.ServerType.CARMENTA_SERVER:
+      case ol.source.wms.ServerType.QGIS:
+        params['DPI'] = 90 * pixelRatio;
+        break;
+      default:
+        goog.asserts.fail('unknown serverType configured');
+        break;
+    }
+  }
+
+  var axisOrientation = projection.getAxisOrientation();
+  var bbox = tileExtent;
+  if (this.v13_ && axisOrientation.substr(0, 2) == 'ne') {
+    var tmp;
+    tmp = tileExtent[0];
+    bbox[0] = tileExtent[1];
+    bbox[1] = tmp;
+    tmp = tileExtent[2];
+    bbox[2] = tileExtent[3];
+    bbox[3] = tmp;
+  }
+  params['BBOX'] = bbox.join(',');
+
+  var url;
+  if (urls.length == 1) {
+    url = urls[0];
+  } else {
+    var index = ol.math.modulo(ol.tilecoord.hash(tileCoord), urls.length);
+    url = urls[index];
+  }
+  return ol.uri.appendParams(url, params);
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.source.TileWMS.prototype.getTilePixelRatio = function(pixelRatio) {
+  return (!this.hidpi_ || this.serverType_ === undefined) ? 1 : pixelRatio;
+};
+
+
+/**
+ * @private
+ */
+ol.source.TileWMS.prototype.resetCoordKeyPrefix_ = function() {
+  var i = 0;
+  var res = [];
+
+  if (this.urls) {
+    var j, jj;
+    for (j = 0, jj = this.urls.length; j < jj; ++j) {
+      res[i++] = this.urls[j];
+    }
+  }
+
+  this.coordKeyPrefix_ = res.join('#');
+};
+
+
+/**
+ * @private
+ * @return {string} The key for the current params.
+ */
+ol.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
+ */
+ol.source.TileWMS.prototype.fixedTileUrlFunction = function(tileCoord, pixelRatio, projection) {
+
+  var tileGrid = this.getTileGrid();
+  if (!tileGrid) {
+    tileGrid = this.getTileGridForProjection(projection);
+  }
+
+  if (tileGrid.getResolutions().length <= tileCoord[0]) {
+    return undefined;
+  }
+
+  if (pixelRatio != 1 && (!this.hidpi_ || this.serverType_ === undefined)) {
+    pixelRatio = 1;
+  }
+
+  var tileResolution = tileGrid.getResolution(tileCoord[0]);
+  var tileExtent = tileGrid.getTileCoordExtent(tileCoord, this.tmpExtent_);
+  var tileSize = ol.size.toSize(
+      tileGrid.getTileSize(tileCoord[0]), this.tmpSize);
+
+  var gutter = this.gutter_;
+  if (gutter !== 0) {
+    tileSize = ol.size.buffer(tileSize, gutter, this.tmpSize);
+    tileExtent = ol.extent.buffer(tileExtent,
+        tileResolution * gutter, tileExtent);
+  }
+
+  if (pixelRatio != 1) {
+    tileSize = ol.size.scale(tileSize, pixelRatio, this.tmpSize);
+  }
+
+  var baseParams = {
+    'SERVICE': 'WMS',
+    'VERSION': ol.DEFAULT_WMS_VERSION,
+    'REQUEST': 'GetMap',
+    'FORMAT': 'image/png',
+    'TRANSPARENT': true
+  };
+  ol.object.assign(baseParams, this.params_);
+
+  return this.getRequestUrl_(tileCoord, tileSize, tileExtent,
+      pixelRatio, projection, baseParams);
+};
+
+
+/**
+ * Update the user-provided params.
+ * @param {Object} params Params.
+ * @api stable
+ */
+ol.source.TileWMS.prototype.updateParams = function(params) {
+  ol.object.assign(this.params_, params);
+  this.resetCoordKeyPrefix_();
+  this.updateV13_();
+  this.setKey(this.getKeyForParams_());
+};
+
+
+/**
+ * @private
+ */
+ol.source.TileWMS.prototype.updateV13_ = function() {
+  var version = this.params_['VERSION'] || ol.DEFAULT_WMS_VERSION;
+  this.v13_ = ol.string.compareVersions(version, '1.3') >= 0;
+};
+
+goog.provide('ol.tilegrid.WMTS');
+
+goog.require('goog.asserts');
+goog.require('ol.proj');
+goog.require('ol.tilegrid.TileGrid');
+
+
+/**
+ * @classdesc
+ * Set the grid pattern for sources accessing WMTS tiled-image servers.
+ *
+ * @constructor
+ * @extends {ol.tilegrid.TileGrid}
+ * @param {olx.tilegrid.WMTSOptions} options WMTS options.
+ * @struct
+ * @api
+ */
+ol.tilegrid.WMTS = function(options) {
+
+  goog.asserts.assert(
+      options.resolutions.length == options.matrixIds.length,
+      'options resolutions and matrixIds must have equal length (%s == %s)',
+      options.resolutions.length, options.matrixIds.length);
+
+  /**
+   * @private
+   * @type {!Array.<string>}
+   */
+  this.matrixIds_ = options.matrixIds;
+  // FIXME: should the matrixIds become optionnal?
+
+  ol.tilegrid.TileGrid.call(this, {
+    extent: options.extent,
+    origin: options.origin,
+    origins: options.origins,
+    resolutions: options.resolutions,
+    tileSize: options.tileSize,
+    tileSizes: options.tileSizes,
+    sizes: options.sizes
+  });
+
+};
+ol.inherits(ol.tilegrid.WMTS, ol.tilegrid.TileGrid);
+
+
+/**
+ * @param {number} z Z.
+ * @return {string} MatrixId..
+ */
+ol.tilegrid.WMTS.prototype.getMatrixId = function(z) {
+  goog.asserts.assert(0 <= z && z < this.matrixIds_.length,
+      'attempted to retrive matrixId for illegal z (%s)', z);
+  return this.matrixIds_[z];
+};
+
+
+/**
+ * Get the list of matrix identifiers.
+ * @return {Array.<string>} MatrixIds.
+ * @api
+ */
+ol.tilegrid.WMTS.prototype.getMatrixIds = function() {
+  return this.matrixIds_;
+};
+
+
+/**
+ * Create a tile grid from a WMTS capabilities matrix set.
+ * @param {Object} matrixSet An object representing a matrixSet in the
+ *     capabilities document.
+ * @param {ol.Extent=} opt_extent An optional extent to restrict the tile
+ *     ranges the server provides.
+ * @return {ol.tilegrid.WMTS} WMTS tileGrid instance.
+ * @api
+ */
+ol.tilegrid.WMTS.createFromCapabilitiesMatrixSet = function(matrixSet, opt_extent) {
+
+  /** @type {!Array.<number>} */
+  var resolutions = [];
+  /** @type {!Array.<string>} */
+  var matrixIds = [];
+  /** @type {!Array.<ol.Coordinate>} */
+  var origins = [];
+  /** @type {!Array.<ol.Size>} */
+  var tileSizes = [];
+  /** @type {!Array.<ol.Size>} */
+  var sizes = [];
+
+  var supportedCRSPropName = 'SupportedCRS';
+  var matrixIdsPropName = 'TileMatrix';
+  var identifierPropName = 'Identifier';
+  var scaleDenominatorPropName = 'ScaleDenominator';
+  var topLeftCornerPropName = 'TopLeftCorner';
+  var tileWidthPropName = 'TileWidth';
+  var tileHeightPropName = 'TileHeight';
+
+  var projection;
+  projection = ol.proj.get(matrixSet[supportedCRSPropName].replace(
+      /urn:ogc:def:crs:(\w+):(.*:)?(\w+)$/, '$1:$3'));
+  var metersPerUnit = projection.getMetersPerUnit();
+  // swap origin x and y coordinates if axis orientation is lat/long
+  var switchOriginXY = projection.getAxisOrientation().substr(0, 2) == 'ne';
+
+  matrixSet[matrixIdsPropName].sort(function(a, b) {
+    return b[scaleDenominatorPropName] - a[scaleDenominatorPropName];
+  });
+
+  matrixSet[matrixIdsPropName].forEach(function(elt, index, array) {
+    matrixIds.push(elt[identifierPropName]);
+    var resolution = elt[scaleDenominatorPropName] * 0.28E-3 / metersPerUnit;
+    var tileWidth = elt[tileWidthPropName];
+    var tileHeight = elt[tileHeightPropName];
+    if (switchOriginXY) {
+      origins.push([elt[topLeftCornerPropName][1],
+        elt[topLeftCornerPropName][0]]);
+    } else {
+      origins.push(elt[topLeftCornerPropName]);
+    }
+    resolutions.push(resolution);
+    tileSizes.push(tileWidth == tileHeight ?
+        tileWidth : [tileWidth, tileHeight]);
+    // top-left origin, so height is negative
+    sizes.push([elt['MatrixWidth'], -elt['MatrixHeight']]);
+  });
+
+  return new ol.tilegrid.WMTS({
+    extent: opt_extent,
+    origins: origins,
+    resolutions: resolutions,
+    matrixIds: matrixIds,
+    tileSizes: tileSizes,
+    sizes: sizes
+  });
+};
+
+goog.provide('ol.source.WMTS');
+
+goog.require('goog.asserts');
+goog.require('ol.TileUrlFunction');
+goog.require('ol.array');
+goog.require('ol.extent');
+goog.require('ol.object');
+goog.require('ol.proj');
+goog.require('ol.source.TileImage');
+goog.require('ol.tilegrid.WMTS');
+goog.require('ol.uri');
+
+
+/**
+ * Request encoding. One of 'KVP', 'REST'.
+ * @enum {string}
+ */
+ol.source.WMTSRequestEncoding = {
+  KVP: 'KVP',  // see spec §8
+  REST: 'REST' // see spec §10
+};
+
+
+/**
+ * @classdesc
+ * Layer source for tile data from WMTS servers.
+ *
+ * @constructor
+ * @extends {ol.source.TileImage}
+ * @param {olx.source.WMTSOptions} options WMTS options.
+ * @api stable
+ */
+ol.source.WMTS = function(options) {
+
+  // TODO: add support for TileMatrixLimits
+
+  /**
+   * @private
+   * @type {string}
+   */
+  this.version_ = options.version !== undefined ? options.version : '1.0.0';
+
+  /**
+   * @private
+   * @type {string}
+   */
+  this.format_ = options.format !== undefined ? options.format : 'image/jpeg';
+
+  /**
+   * @private
+   * @type {!Object}
+   */
+  this.dimensions_ = options.dimensions !== undefined ? options.dimensions : {};
+
+  /**
+   * @private
+   * @type {string}
+   */
+  this.layer_ = options.layer;
+
+  /**
+   * @private
+   * @type {string}
+   */
+  this.matrixSet_ = options.matrixSet;
+
+  /**
+   * @private
+   * @type {string}
+   */
+  this.style_ = options.style;
+
+  var urls = options.urls;
+  if (urls === undefined && options.url !== undefined) {
+    urls = ol.TileUrlFunction.expandUrl(options.url);
+  }
+
+  // FIXME: should we guess this requestEncoding from options.url(s)
+  //        structure? that would mean KVP only if a template is not provided.
+
+  /**
+   * @private
+   * @type {ol.source.WMTSRequestEncoding}
+   */
+  this.requestEncoding_ = options.requestEncoding !== undefined ?
+      /** @type {ol.source.WMTSRequestEncoding} */ (options.requestEncoding) :
+      ol.source.WMTSRequestEncoding.KVP;
+
+  var requestEncoding = this.requestEncoding_;
+
+  // FIXME: should we create a default tileGrid?
+  // we could issue a getCapabilities xhr to retrieve missing configuration
+  var tileGrid = options.tileGrid;
+
+  // context property names are lower case to allow for a case insensitive
+  // replacement as some services use different naming conventions
+  var context = {
+    'layer': this.layer_,
+    'style': this.style_,
+    'tilematrixset': this.matrixSet_
+  };
+
+  if (requestEncoding == ol.source.WMTSRequestEncoding.KVP) {
+    ol.object.assign(context, {
+      'Service': 'WMTS',
+      'Request': 'GetTile',
+      'Version': this.version_,
+      'Format': this.format_
+    });
+  }
+
+  var dimensions = this.dimensions_;
+
+  /**
+   * @param {string} template Template.
+   * @return {ol.TileUrlFunctionType} Tile URL function.
+   */
+  function createFromWMTSTemplate(template) {
+
+    // TODO: we may want to create our own appendParams function so that params
+    // order conforms to wmts spec guidance, and so that we can avoid to escape
+    // special template params
+
+    template = (requestEncoding == ol.source.WMTSRequestEncoding.KVP) ?
+        ol.uri.appendParams(template, context) :
+        template.replace(/\{(\w+?)\}/g, function(m, p) {
+          return (p.toLowerCase() in context) ? context[p.toLowerCase()] : m;
+        });
+
+    return (
+        /**
+         * @param {ol.TileCoord} tileCoord Tile coordinate.
+         * @param {number} pixelRatio Pixel ratio.
+         * @param {ol.proj.Projection} projection Projection.
+         * @return {string|undefined} Tile URL.
+         */
+        function(tileCoord, pixelRatio, projection) {
+          if (!tileCoord) {
+            return undefined;
+          } else {
+            var localContext = {
+              'TileMatrix': tileGrid.getMatrixId(tileCoord[0]),
+              'TileCol': tileCoord[1],
+              'TileRow': -tileCoord[2] - 1
+            };
+            ol.object.assign(localContext, dimensions);
+            var url = template;
+            if (requestEncoding == ol.source.WMTSRequestEncoding.KVP) {
+              url = ol.uri.appendParams(url, localContext);
+            } else {
+              url = url.replace(/\{(\w+?)\}/g, function(m, p) {
+                return localContext[p];
+              });
+            }
+            return url;
+          }
+        });
+  }
+
+  var tileUrlFunction = (urls && urls.length > 0) ?
+      ol.TileUrlFunction.createFromTileUrlFunctions(
+          urls.map(createFromWMTSTemplate)) :
+      ol.TileUrlFunction.nullTileUrlFunction;
+
+  ol.source.TileImage.call(this, {
+    attributions: options.attributions,
+    cacheSize: options.cacheSize,
+    crossOrigin: options.crossOrigin,
+    logo: options.logo,
+    projection: options.projection,
+    reprojectionErrorThreshold: options.reprojectionErrorThreshold,
+    tileClass: options.tileClass,
+    tileGrid: tileGrid,
+    tileLoadFunction: options.tileLoadFunction,
+    tilePixelRatio: options.tilePixelRatio,
+    tileUrlFunction: tileUrlFunction,
+    urls: urls,
+    wrapX: options.wrapX !== undefined ? options.wrapX : false
+  });
+
+  this.setKey(this.getKeyForDimensions_());
+
+};
+ol.inherits(ol.source.WMTS, ol.source.TileImage);
+
+
+/**
+ * Get the dimensions, i.e. those passed to the constructor through the
+ * "dimensions" option, and possibly updated using the updateDimensions
+ * method.
+ * @return {!Object} Dimensions.
+ * @api
+ */
+ol.source.WMTS.prototype.getDimensions = function() {
+  return this.dimensions_;
+};
+
+
+/**
+ * Return the image format of the WMTS source.
+ * @return {string} Format.
+ * @api
+ */
+ol.source.WMTS.prototype.getFormat = function() {
+  return this.format_;
+};
+
+
+/**
+ * Return the layer of the WMTS source.
+ * @return {string} Layer.
+ * @api
+ */
+ol.source.WMTS.prototype.getLayer = function() {
+  return this.layer_;
+};
+
+
+/**
+ * Return the matrix set of the WMTS source.
+ * @return {string} MatrixSet.
+ * @api
+ */
+ol.source.WMTS.prototype.getMatrixSet = function() {
+  return this.matrixSet_;
+};
+
+
+/**
+ * Return the request encoding, either "KVP" or "REST".
+ * @return {ol.source.WMTSRequestEncoding} Request encoding.
+ * @api
+ */
+ol.source.WMTS.prototype.getRequestEncoding = function() {
+  return this.requestEncoding_;
+};
+
+
+/**
+ * Return the style of the WMTS source.
+ * @return {string} Style.
+ * @api
+ */
+ol.source.WMTS.prototype.getStyle = function() {
+  return this.style_;
+};
+
+
+/**
+ * Return the version of the WMTS source.
+ * @return {string} Version.
+ * @api
+ */
+ol.source.WMTS.prototype.getVersion = function() {
+  return this.version_;
+};
+
+
+/**
+ * @private
+ * @return {string} The key for the current dimensions.
+ */
+ol.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
+ */
+ol.source.WMTS.prototype.updateDimensions = function(dimensions) {
+  ol.object.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.
+ * @return {olx.source.WMTSOptions} WMTS source options object.
+ * @api
+ */
+ol.source.WMTS.optionsFromCapabilities = function(wmtsCap, config) {
+
+  // TODO: add support for TileMatrixLimits
+  goog.asserts.assert(config['layer'],
+      'config "layer" must not be null');
+
+  var layers = wmtsCap['Contents']['Layer'];
+  var l = ol.array.find(layers, function(elt, index, array) {
+    return elt['Identifier'] == config['layer'];
+  });
+  goog.asserts.assert(l, 'found a matching layer in Contents/Layer');
+
+  goog.asserts.assert(l['TileMatrixSetLink'].length > 0,
+      'layer has TileMatrixSetLink');
+  var tileMatrixSets = wmtsCap['Contents']['TileMatrixSet'];
+  var idx, matrixSet;
+  if (l['TileMatrixSetLink'].length > 1) {
+    if ('projection' in config) {
+      idx = ol.array.findIndex(l['TileMatrixSetLink'],
+          function(elt, index, array) {
+            var tileMatrixSet = ol.array.find(tileMatrixSets, function(el) {
+              return el['Identifier'] == elt['TileMatrixSet'];
+            });
+            return tileMatrixSet['SupportedCRS'].replace(
+                /urn:ogc:def:crs:(\w+):(.*:)?(\w+)$/, '$1:$3'
+                   ) == config['projection'];
+          });
+    } else {
+      idx = ol.array.findIndex(l['TileMatrixSetLink'],
+          function(elt, index, array) {
+            return elt['TileMatrixSet'] == config['matrixSet'];
+          });
+    }
+  } else {
+    idx = 0;
+  }
+  if (idx < 0) {
+    idx = 0;
+  }
+  matrixSet = /** @type {string} */
+      (l['TileMatrixSetLink'][idx]['TileMatrixSet']);
+
+  goog.asserts.assert(matrixSet, 'TileMatrixSet must not be null');
+
+  var format = /** @type {string} */ (l['Format'][0]);
+  if ('format' in config) {
+    format = config['format'];
+  }
+  idx = ol.array.findIndex(l['Style'], function(elt, index, array) {
+    if ('style' in config) {
+      return elt['Title'] == config['style'];
+    } else {
+      return elt['isDefault'];
+    }
+  });
+  if (idx < 0) {
+    idx = 0;
+  }
+  var style = /** @type {string} */ (l['Style'][idx]['Identifier']);
+
+  var dimensions = {};
+  if ('Dimension' in l) {
+    l['Dimension'].forEach(function(elt, index, array) {
+      var key = elt['Identifier'];
+      var value = elt['Default'];
+      if (value !== undefined) {
+        goog.asserts.assert(ol.array.includes(elt['Value'], value),
+            'default value contained in values');
+      } else {
+        value = elt['Value'][0];
+      }
+      goog.asserts.assert(value !== undefined, 'value could be found');
+      dimensions[key] = value;
+    });
+  }
+
+  var matrixSets = wmtsCap['Contents']['TileMatrixSet'];
+  var matrixSetObj = ol.array.find(matrixSets, function(elt, index, array) {
+    return elt['Identifier'] == matrixSet;
+  });
+  goog.asserts.assert(matrixSetObj,
+      'found matrixSet in Contents/TileMatrixSet');
+
+  var projection;
+  if ('projection' in config) {
+    projection = ol.proj.get(config['projection']);
+  } else {
+    projection = ol.proj.get(matrixSetObj['SupportedCRS'].replace(
+        /urn:ogc:def:crs:(\w+):(.*:)?(\w+)$/, '$1:$3'));
+  }
+
+  var wgs84BoundingBox = l['WGS84BoundingBox'];
+  var extent, wrapX;
+  if (wgs84BoundingBox !== undefined) {
+    var wgs84ProjectionExtent = ol.proj.get('EPSG:4326').getExtent();
+    wrapX = (wgs84BoundingBox[0] == wgs84ProjectionExtent[0] &&
+        wgs84BoundingBox[2] == wgs84ProjectionExtent[2]);
+    extent = ol.proj.transformExtent(
+        wgs84BoundingBox, 'EPSG:4326', projection);
+    var projectionExtent = projection.getExtent();
+    if (projectionExtent) {
+      // If possible, do a sanity check on the extent - it should never be
+      // bigger than the validity extent of the projection of a matrix set.
+      if (!ol.extent.containsExtent(projectionExtent, extent)) {
+        extent = undefined;
+      }
+    }
+  }
+
+  var tileGrid = ol.tilegrid.WMTS.createFromCapabilitiesMatrixSet(
+      matrixSetObj, extent);
+
+  /** @type {!Array.<string>} */
+  var urls = [];
+  var requestEncoding = config['requestEncoding'];
+  requestEncoding = requestEncoding !== undefined ? requestEncoding : '';
+
+  goog.asserts.assert(
+      ol.array.includes(['REST', 'RESTful', 'KVP', ''], requestEncoding),
+      'requestEncoding (%s) is one of "REST", "RESTful", "KVP" or ""',
+      requestEncoding);
+
+  if ('OperationsMetadata' in wmtsCap && 'GetTile' in wmtsCap['OperationsMetadata']) {
+    var gets = wmtsCap['OperationsMetadata']['GetTile']['DCP']['HTTP']['Get'];
+    goog.asserts.assert(gets.length >= 1);
+
+    for (var i = 0, ii = gets.length; i < ii; ++i) {
+      var constraint = ol.array.find(gets[i]['Constraint'], function(element) {
+        return element['name'] == 'GetEncoding';
+      });
+      var encodings = constraint['AllowedValues']['Value'];
+      goog.asserts.assert(encodings.length >= 1);
+
+      if (requestEncoding === '') {
+        // requestEncoding not provided, use the first encoding from the list
+        requestEncoding = encodings[0];
+      }
+      if (requestEncoding === ol.source.WMTSRequestEncoding.KVP) {
+        if (ol.array.includes(encodings, ol.source.WMTSRequestEncoding.KVP)) {
+          urls.push(/** @type {string} */ (gets[i]['href']));
+        }
+      } else {
+        break;
+      }
+    }
+  }
+  if (urls.length === 0) {
+    requestEncoding = ol.source.WMTSRequestEncoding.REST;
+    l['ResourceURL'].forEach(function(element) {
+      if (element['resourceType'] === 'tile') {
+        format = element['format'];
+        urls.push(/** @type {string} */ (element['template']));
+      }
+    });
+  }
+  goog.asserts.assert(urls.length > 0, 'At least one URL found');
+
+  return {
+    urls: urls,
+    layer: config['layer'],
+    matrixSet: matrixSet,
+    format: format,
+    projection: projection,
+    requestEncoding: requestEncoding,
+    tileGrid: tileGrid,
+    style: style,
+    dimensions: dimensions,
+    wrapX: wrapX
+  };
+
+};
+
+goog.provide('ol.source.Zoomify');
+
+goog.require('goog.asserts');
+goog.require('ol');
+goog.require('ol.ImageTile');
+goog.require('ol.TileState');
+goog.require('ol.dom');
+goog.require('ol.extent');
+goog.require('ol.proj');
+goog.require('ol.source.TileImage');
+goog.require('ol.tilegrid.TileGrid');
+
+
+/**
+ * @enum {string}
+ */
+ol.source.ZoomifyTierSizeCalculation = {
+  DEFAULT: 'default',
+  TRUNCATED: 'truncated'
+};
+
+
+/**
+ * @classdesc
+ * Layer source for tile data in Zoomify format.
+ *
+ * @constructor
+ * @extends {ol.source.TileImage}
+ * @param {olx.source.ZoomifyOptions=} opt_options Options.
+ * @api stable
+ */
+ol.source.Zoomify = function(opt_options) {
+
+  var options = opt_options || {};
+
+  var size = options.size;
+  var tierSizeCalculation = options.tierSizeCalculation !== undefined ?
+      options.tierSizeCalculation :
+      ol.source.ZoomifyTierSizeCalculation.DEFAULT;
+
+  var imageWidth = size[0];
+  var imageHeight = size[1];
+  var tierSizeInTiles = [];
+  var tileSize = ol.DEFAULT_TILE_SIZE;
+
+  switch (tierSizeCalculation) {
+    case ol.source.ZoomifyTierSizeCalculation.DEFAULT:
+      while (imageWidth > tileSize || imageHeight > tileSize) {
+        tierSizeInTiles.push([
+          Math.ceil(imageWidth / tileSize),
+          Math.ceil(imageHeight / tileSize)
+        ]);
+        tileSize += tileSize;
+      }
+      break;
+    case ol.source.ZoomifyTierSizeCalculation.TRUNCATED:
+      var width = imageWidth;
+      var height = imageHeight;
+      while (width > tileSize || height > tileSize) {
+        tierSizeInTiles.push([
+          Math.ceil(width / tileSize),
+          Math.ceil(height / tileSize)
+        ]);
+        width >>= 1;
+        height >>= 1;
+      }
+      break;
+    default:
+      goog.asserts.fail();
+      break;
+  }
+
+  tierSizeInTiles.push([1, 1]);
+  tierSizeInTiles.reverse();
+
+  var resolutions = [1];
+  var tileCountUpToTier = [0];
+  var i, ii;
+  for (i = 1, ii = tierSizeInTiles.length; i < ii; i++) {
+    resolutions.push(1 << i);
+    tileCountUpToTier.push(
+        tierSizeInTiles[i - 1][0] * tierSizeInTiles[i - 1][1] +
+        tileCountUpToTier[i - 1]
+    );
+  }
+  resolutions.reverse();
+
+  var extent = [0, -size[1], size[0], 0];
+  var tileGrid = new ol.tilegrid.TileGrid({
+    extent: extent,
+    origin: ol.extent.getTopLeft(extent),
+    resolutions: resolutions
+  });
+
+  var url = options.url;
+
+  /**
+   * @this {ol.source.TileImage}
+   * @param {ol.TileCoord} tileCoord Tile Coordinate.
+   * @param {number} pixelRatio Pixel ratio.
+   * @param {ol.proj.Projection} projection Projection.
+   * @return {string|undefined} Tile URL.
+   */
+  function tileUrlFunction(tileCoord, pixelRatio, projection) {
+    if (!tileCoord) {
+      return undefined;
+    } else {
+      var tileCoordZ = tileCoord[0];
+      var tileCoordX = tileCoord[1];
+      var tileCoordY = -tileCoord[2] - 1;
+      var tileIndex =
+          tileCoordX +
+          tileCoordY * tierSizeInTiles[tileCoordZ][0] +
+          tileCountUpToTier[tileCoordZ];
+      var tileGroup = (tileIndex / ol.DEFAULT_TILE_SIZE) | 0;
+      return url + 'TileGroup' + tileGroup + '/' +
+          tileCoordZ + '-' + tileCoordX + '-' + tileCoordY + '.jpg';
+    }
+  }
+
+  ol.source.TileImage.call(this, {
+    attributions: options.attributions,
+    cacheSize: options.cacheSize,
+    crossOrigin: options.crossOrigin,
+    logo: options.logo,
+    reprojectionErrorThreshold: options.reprojectionErrorThreshold,
+    tileClass: ol.source.ZoomifyTile_,
+    tileGrid: tileGrid,
+    tileUrlFunction: tileUrlFunction
+  });
+
+};
+ol.inherits(ol.source.Zoomify, ol.source.TileImage);
+
+
+/**
+ * @constructor
+ * @extends {ol.ImageTile}
+ * @param {ol.TileCoord} tileCoord Tile coordinate.
+ * @param {ol.TileState} state State.
+ * @param {string} src Image source URI.
+ * @param {?string} crossOrigin Cross origin.
+ * @param {ol.TileLoadFunctionType} tileLoadFunction Tile load function.
+ * @private
+ */
+ol.source.ZoomifyTile_ = function(
+    tileCoord, state, src, crossOrigin, tileLoadFunction) {
+
+  ol.ImageTile.call(this, tileCoord, state, src, crossOrigin, tileLoadFunction);
+
+  /**
+   * @private
+   * @type {Object.<string,
+   *                HTMLCanvasElement|HTMLImageElement|HTMLVideoElement>}
+   */
+  this.zoomifyImageByContext_ = {};
+
+};
+ol.inherits(ol.source.ZoomifyTile_, ol.ImageTile);
+
+
+/**
+ * @inheritDoc
+ */
+ol.source.ZoomifyTile_.prototype.getImage = function(opt_context) {
+  var tileSize = ol.DEFAULT_TILE_SIZE;
+  var key = opt_context !== undefined ?
+      goog.getUid(opt_context).toString() : '';
+  if (key in this.zoomifyImageByContext_) {
+    return this.zoomifyImageByContext_[key];
+  } else {
+    var image = ol.ImageTile.prototype.getImage.call(this, opt_context);
+    if (this.state == ol.TileState.LOADED) {
+      if (image.width == tileSize && image.height == tileSize) {
+        this.zoomifyImageByContext_[key] = image;
+        return image;
+      } else {
+        var context = ol.dom.createCanvasContext2D(tileSize, tileSize);
+        context.drawImage(image, 0, 0);
+        this.zoomifyImageByContext_[key] = context.canvas;
+        return context.canvas;
+      }
+    } else {
+      return image;
+    }
+  }
+};
+
+goog.provide('ol.style.Atlas');
+goog.provide('ol.style.AtlasManager');
+
+goog.require('goog.asserts');
+goog.require('ol');
+goog.require('ol.dom');
+
+
+/**
+ * Manages the creation of image atlases.
+ *
+ * Images added to this manager will be inserted into an atlas, which
+ * will be used for rendering.
+ * The `size` given in the constructor is the size for the first
+ * atlas. After that, when new atlases are created, they will have
+ * twice the size as the latest atlas (until `maxSize` is reached).
+ *
+ * If an application uses many images or very large images, it is recommended
+ * to set a higher `size` value to avoid the creation of too many atlases.
+ *
+ * @constructor
+ * @struct
+ * @api
+ * @param {olx.style.AtlasManagerOptions=} opt_options Options.
+ */
+ol.style.AtlasManager = function(opt_options) {
+
+  var options = opt_options || {};
+
+  /**
+   * The size in pixels of the latest atlas image.
+   * @private
+   * @type {number}
+   */
+  this.currentSize_ = options.initialSize !== undefined ?
+      options.initialSize : ol.INITIAL_ATLAS_SIZE;
+
+  /**
+   * The maximum size in pixels of atlas images.
+   * @private
+   * @type {number}
+   */
+  this.maxSize_ = options.maxSize !== undefined ?
+      options.maxSize : ol.MAX_ATLAS_SIZE != -1 ?
+          ol.MAX_ATLAS_SIZE : ol.WEBGL_MAX_TEXTURE_SIZE !== undefined ?
+              ol.WEBGL_MAX_TEXTURE_SIZE : 2048;
+
+  /**
+   * The size in pixels between images.
+   * @private
+   * @type {number}
+   */
+  this.space_ = options.space !== undefined ? options.space : 1;
+
+  /**
+   * @private
+   * @type {Array.<ol.style.Atlas>}
+   */
+  this.atlases_ = [new ol.style.Atlas(this.currentSize_, this.space_)];
+
+  /**
+   * The size in pixels of the latest atlas image for hit-detection images.
+   * @private
+   * @type {number}
+   */
+  this.currentHitSize_ = this.currentSize_;
+
+  /**
+   * @private
+   * @type {Array.<ol.style.Atlas>}
+   */
+  this.hitAtlases_ = [new ol.style.Atlas(this.currentHitSize_, this.space_)];
+};
+
+
+/**
+ * @param {string} id The identifier of the entry to check.
+ * @return {?ol.AtlasManagerInfo} The position and atlas image for the
+ *    entry, or `null` if the entry is not part of the atlas manager.
+ */
+ol.style.AtlasManager.prototype.getInfo = function(id) {
+  /** @type {?ol.AtlasInfo} */
+  var info = this.getInfo_(this.atlases_, id);
+
+  if (!info) {
+    return null;
+  }
+  /** @type {?ol.AtlasInfo} */
+  var hitInfo = this.getInfo_(this.hitAtlases_, id);
+  goog.asserts.assert(hitInfo, 'hitInfo must not be null');
+
+  return this.mergeInfos_(info, hitInfo);
+};
+
+
+/**
+ * @private
+ * @param {Array.<ol.style.Atlas>} atlases The atlases to search.
+ * @param {string} id The identifier of the entry to check.
+ * @return {?ol.AtlasInfo} The position and atlas image for the entry,
+ *    or `null` if the entry is not part of the atlases.
+ */
+ol.style.AtlasManager.prototype.getInfo_ = function(atlases, id) {
+  var atlas, info, i, ii;
+  for (i = 0, ii = atlases.length; i < ii; ++i) {
+    atlas = atlases[i];
+    info = atlas.get(id);
+    if (info) {
+      return info;
+    }
+  }
+  return null;
+};
+
+
+/**
+ * @private
+ * @param {ol.AtlasInfo} info The info for the real image.
+ * @param {ol.AtlasInfo} hitInfo The info for the hit-detection
+ *    image.
+ * @return {?ol.AtlasManagerInfo} The position and atlas image for the
+ *    entry, or `null` if the entry is not part of the atlases.
+ */
+ol.style.AtlasManager.prototype.mergeInfos_ = function(info, hitInfo) {
+  goog.asserts.assert(info.offsetX === hitInfo.offsetX,
+      'in order to merge, offsetX of info and hitInfo must be equal');
+  goog.asserts.assert(info.offsetY === hitInfo.offsetY,
+      'in order to merge, offsetY of info and hitInfo must be equal');
+  return /** @type {ol.AtlasManagerInfo} */ ({
+    offsetX: info.offsetX,
+    offsetY: info.offsetY,
+    image: info.image,
+    hitImage: hitInfo.image
+  });
+};
+
+
+/**
+ * Add an image to the atlas manager.
+ *
+ * If an entry for the given id already exists, the entry will
+ * be overridden (but the space on the atlas graphic will not be freed).
+ *
+ * If `renderHitCallback` is provided, the image (or the hit-detection version
+ * of the image) will be rendered into a separate hit-detection atlas image.
+ *
+ * @param {string} id The identifier of the entry to add.
+ * @param {number} width The width.
+ * @param {number} height The height.
+ * @param {function(CanvasRenderingContext2D, number, number)} renderCallback
+ *    Called to render the new image onto an atlas image.
+ * @param {function(CanvasRenderingContext2D, number, number)=}
+ *    opt_renderHitCallback Called to render a hit-detection image onto a hit
+ *    detection atlas image.
+ * @param {Object=} opt_this Value to use as `this` when executing
+ *    `renderCallback` and `renderHitCallback`.
+ * @return {?ol.AtlasManagerInfo}  The position and atlas image for the
+ *    entry, or `null` if the image is too big.
+ */
+ol.style.AtlasManager.prototype.add = function(id, width, height,
+        renderCallback, opt_renderHitCallback, opt_this) {
+  if (width + this.space_ > this.maxSize_ ||
+      height + this.space_ > this.maxSize_) {
+    return null;
+  }
+
+  /** @type {?ol.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 : ol.nullFunction;
+
+  /** @type {?ol.AtlasInfo} */
+  var hitInfo = this.add_(true,
+      id, width, height, renderHitCallback, opt_this);
+  goog.asserts.assert(hitInfo, 'hitInfo must not be null');
+
+  return this.mergeInfos_(info, hitInfo);
+};
+
+
+/**
+ * @private
+ * @param {boolean} isHitAtlas If the hit-detection atlases are used.
+ * @param {string} id The identifier of the entry to add.
+ * @param {number} width The width.
+ * @param {number} height The height.
+ * @param {function(CanvasRenderingContext2D, number, number)} renderCallback
+ *    Called to render the new image onto an atlas image.
+ * @param {Object=} opt_this Value to use as `this` when executing
+ *    `renderCallback` and `renderHitCallback`.
+ * @return {?ol.AtlasInfo}  The position and atlas image for the entry,
+ *    or `null` if the image is too big.
+ */
+ol.style.AtlasManager.prototype.add_ = function(isHitAtlas, id, width, height,
+        renderCallback, opt_this) {
+  var atlases = (isHitAtlas) ? this.hitAtlases_ : this.atlases_;
+  var atlas, info, i, ii;
+  for (i = 0, ii = atlases.length; i < ii; ++i) {
+    atlas = atlases[i];
+    info = atlas.add(id, width, height, renderCallback, opt_this);
+    if (info) {
+      return info;
+    } else if (!info && i === ii - 1) {
+      // the entry could not be added to one of the existing atlases,
+      // create a new atlas that is twice as big and try to add to this one.
+      var size;
+      if (isHitAtlas) {
+        size = Math.min(this.currentHitSize_ * 2, this.maxSize_);
+        this.currentHitSize_ = size;
+      } else {
+        size = Math.min(this.currentSize_ * 2, this.maxSize_);
+        this.currentSize_ = size;
+      }
+      atlas = new ol.style.Atlas(size, this.space_);
+      atlases.push(atlas);
+      // run the loop another time
+      ++ii;
+    }
+  }
+  goog.asserts.fail('Failed to add to atlasmanager');
+};
+
+
+/**
+ * This class facilitates the creation of image atlases.
+ *
+ * Images added to an atlas will be rendered onto a single
+ * atlas canvas. The distribution of images on the canvas is
+ * managed with the bin packing algorithm described in:
+ * http://www.blackpawn.com/texts/lightmaps/
+ *
+ * @constructor
+ * @struct
+ * @param {number} size The size in pixels of the sprite image.
+ * @param {number} space The space in pixels between images.
+ *    Because texture coordinates are float values, the edges of
+ *    images might not be completely correct (in a way that the
+ *    edges overlap when being rendered). To avoid this we add a
+ *    padding around each image.
+ */
+ol.style.Atlas = function(size, space) {
+
+  /**
+   * @private
+   * @type {number}
+   */
+  this.space_ = space;
+
+  /**
+   * @private
+   * @type {Array.<ol.AtlasBlock>}
+   */
+  this.emptyBlocks_ = [{x: 0, y: 0, width: size, height: size}];
+
+  /**
+   * @private
+   * @type {Object.<string, ol.AtlasInfo>}
+   */
+  this.entries_ = {};
+
+  /**
+   * @private
+   * @type {CanvasRenderingContext2D}
+   */
+  this.context_ = ol.dom.createCanvasContext2D(size, size);
+
+  /**
+   * @private
+   * @type {HTMLCanvasElement}
+   */
+  this.canvas_ = this.context_.canvas;
+};
+
+
+/**
+ * @param {string} id The identifier of the entry to check.
+ * @return {?ol.AtlasInfo} The atlas info.
+ */
+ol.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 {?ol.AtlasInfo} The position and atlas image for the entry.
+ */
+ol.style.Atlas.prototype.add = function(id, width, height, renderCallback, opt_this) {
+  var block, i, ii;
+  for (i = 0, ii = this.emptyBlocks_.length; i < ii; ++i) {
+    block = this.emptyBlocks_[i];
+    if (block.width >= width + this.space_ &&
+        block.height >= height + this.space_) {
+      // we found a block that is big enough for our entry
+      var entry = {
+        offsetX: block.x + this.space_,
+        offsetY: block.y + this.space_,
+        image: this.canvas_
+      };
+      this.entries_[id] = entry;
+
+      // render the image on the atlas image
+      renderCallback.call(opt_this, this.context_,
+          block.x + this.space_, block.y + this.space_);
+
+      // split the block after the insertion, either horizontally or vertically
+      this.split_(i, block, width + this.space_, height + this.space_);
+
+      return entry;
+    }
+  }
+
+  // there is no space for the new entry in this atlas
+  return null;
+};
+
+
+/**
+ * @private
+ * @param {number} index The index of the block.
+ * @param {ol.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.
+ */
+ol.style.Atlas.prototype.split_ = function(index, block, width, height) {
+  var deltaWidth = block.width - width;
+  var deltaHeight = block.height - height;
+
+  /** @type {ol.AtlasBlock} */
+  var newBlock1;
+  /** @type {ol.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 old block and insert new blocks at the same array position.
+ * The new blocks are inserted at the same position, so that splitted
+ * blocks (that are potentially smaller) are filled first.
+ * @private
+ * @param {number} index The index of the block to remove.
+ * @param {ol.AtlasBlock} newBlock1 The 1st block to add.
+ * @param {ol.AtlasBlock} newBlock2 The 2nd block to add.
+ */
+ol.style.Atlas.prototype.updateBlocks_ = function(index, newBlock1, newBlock2) {
+  var args = [index, 1];
+  if (newBlock1.width > 0 && newBlock1.height > 0) {
+    args.push(newBlock1);
+  }
+  if (newBlock2.width > 0 && newBlock2.height > 0) {
+    args.push(newBlock2);
+  }
+  this.emptyBlocks_.splice.apply(this.emptyBlocks_, args);
+};
+
+goog.provide('ol.style.RegularShape');
+
+goog.require('goog.asserts');
+goog.require('ol');
+goog.require('ol.color');
+goog.require('ol.colorlike');
+goog.require('ol.dom');
+goog.require('ol.has');
+goog.require('ol.render.canvas');
+goog.require('ol.style.AtlasManager');
+goog.require('ol.style.Fill');
+goog.require('ol.style.Image');
+goog.require('ol.style.ImageState');
+goog.require('ol.style.Stroke');
+
+
+/**
+ * @classdesc
+ * Set regular shape style for vector features. The resulting shape will be
+ * a regular polygon when `radius` is provided, or a star when `radius1` and
+ * `radius2` are provided.
+ *
+ * @constructor
+ * @param {olx.style.RegularShapeOptions} options Options.
+ * @extends {ol.style.Image}
+ * @api
+ */
+ol.style.RegularShape = function(options) {
+
+  goog.asserts.assert(
+      options.radius !== undefined || options.radius1 !== undefined,
+      'must provide either "radius" or "radius1"');
+
+  /**
+   * @private
+   * @type {Array.<string>}
+   */
+  this.checksums_ = null;
+
+  /**
+   * @private
+   * @type {HTMLCanvasElement}
+   */
+  this.canvas_ = null;
+
+  /**
+   * @private
+   * @type {HTMLCanvasElement}
+   */
+  this.hitDetectionCanvas_ = null;
+
+  /**
+   * @private
+   * @type {ol.style.Fill}
+   */
+  this.fill_ = options.fill !== undefined ? options.fill : null;
+
+  /**
+   * @private
+   * @type {Array.<number>}
+   */
+  this.origin_ = [0, 0];
+
+  /**
+   * @private
+   * @type {number}
+   */
+  this.points_ = options.points;
+
+  /**
+   * @private
+   * @type {number}
+   */
+  this.radius_ = /** @type {number} */ (options.radius !== undefined ?
+      options.radius : options.radius1);
+
+  /**
+   * @private
+   * @type {number}
+   */
+  this.radius2_ =
+      options.radius2 !== undefined ? options.radius2 : this.radius_;
+
+  /**
+   * @private
+   * @type {number}
+   */
+  this.angle_ = options.angle !== undefined ? options.angle : 0;
+
+  /**
+   * @private
+   * @type {ol.style.Stroke}
+   */
+  this.stroke_ = options.stroke !== undefined ? options.stroke : null;
+
+  /**
+   * @private
+   * @type {Array.<number>}
+   */
+  this.anchor_ = null;
+
+  /**
+   * @private
+   * @type {ol.Size}
+   */
+  this.size_ = null;
+
+  /**
+   * @private
+   * @type {ol.Size}
+   */
+  this.imageSize_ = null;
+
+  /**
+   * @private
+   * @type {ol.Size}
+   */
+  this.hitDetectionImageSize_ = null;
+
+  this.render_(options.atlasManager);
+
+  /**
+   * @type {boolean}
+   */
+  var snapToPixel = options.snapToPixel !== undefined ?
+      options.snapToPixel : true;
+
+  /**
+   * @type {boolean}
+   */
+  var rotateWithView = options.rotateWithView !== undefined ?
+      options.rotateWithView : false;
+
+  ol.style.Image.call(this, {
+    opacity: 1,
+    rotateWithView: rotateWithView,
+    rotation: options.rotation !== undefined ? options.rotation : 0,
+    scale: 1,
+    snapToPixel: snapToPixel
+  });
+
+};
+ol.inherits(ol.style.RegularShape, ol.style.Image);
+
+
+/**
+ * @inheritDoc
+ * @api
+ */
+ol.style.RegularShape.prototype.getAnchor = function() {
+  return this.anchor_;
+};
+
+
+/**
+ * Get the angle used in generating the shape.
+ * @return {number} Shape's rotation in radians.
+ * @api
+ */
+ol.style.RegularShape.prototype.getAngle = function() {
+  return this.angle_;
+};
+
+
+/**
+ * Get the fill style for the shape.
+ * @return {ol.style.Fill} Fill style.
+ * @api
+ */
+ol.style.RegularShape.prototype.getFill = function() {
+  return this.fill_;
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.style.RegularShape.prototype.getHitDetectionImage = function(pixelRatio) {
+  return this.hitDetectionCanvas_;
+};
+
+
+/**
+ * @inheritDoc
+ * @api
+ */
+ol.style.RegularShape.prototype.getImage = function(pixelRatio) {
+  return this.canvas_;
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.style.RegularShape.prototype.getImageSize = function() {
+  return this.imageSize_;
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.style.RegularShape.prototype.getHitDetectionImageSize = function() {
+  return this.hitDetectionImageSize_;
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.style.RegularShape.prototype.getImageState = function() {
+  return ol.style.ImageState.LOADED;
+};
+
+
+/**
+ * @inheritDoc
+ * @api
+ */
+ol.style.RegularShape.prototype.getOrigin = function() {
+  return this.origin_;
+};
+
+
+/**
+ * Get the number of points for generating the shape.
+ * @return {number} Number of points for stars and regular polygons.
+ * @api
+ */
+ol.style.RegularShape.prototype.getPoints = function() {
+  return this.points_;
+};
+
+
+/**
+ * Get the (primary) radius for the shape.
+ * @return {number} Radius.
+ * @api
+ */
+ol.style.RegularShape.prototype.getRadius = function() {
+  return this.radius_;
+};
+
+
+/**
+ * Get the secondary radius for the shape.
+ * @return {number} Radius2.
+ * @api
+ */
+ol.style.RegularShape.prototype.getRadius2 = function() {
+  return this.radius2_;
+};
+
+
+/**
+ * @inheritDoc
+ * @api
+ */
+ol.style.RegularShape.prototype.getSize = function() {
+  return this.size_;
+};
+
+
+/**
+ * Get the stroke style for the shape.
+ * @return {ol.style.Stroke} Stroke style.
+ * @api
+ */
+ol.style.RegularShape.prototype.getStroke = function() {
+  return this.stroke_;
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.style.RegularShape.prototype.listenImageChange = ol.nullFunction;
+
+
+/**
+ * @inheritDoc
+ */
+ol.style.RegularShape.prototype.load = ol.nullFunction;
+
+
+/**
+ * @inheritDoc
+ */
+ol.style.RegularShape.prototype.unlistenImageChange = ol.nullFunction;
+
+
+/**
+ * @private
+ * @param {ol.style.AtlasManager|undefined} atlasManager An atlas manager.
+ */
+ol.style.RegularShape.prototype.render_ = function(atlasManager) {
+  var imageSize;
+  var lineCap = '';
+  var lineJoin = '';
+  var miterLimit = 0;
+  var lineDash = null;
+  var strokeStyle;
+  var strokeWidth = 0;
+
+  if (this.stroke_) {
+    strokeStyle = ol.color.asString(this.stroke_.getColor());
+    strokeWidth = this.stroke_.getWidth();
+    if (strokeWidth === undefined) {
+      strokeWidth = ol.render.canvas.defaultLineWidth;
+    }
+    lineDash = this.stroke_.getLineDash();
+    if (!ol.has.CANVAS_LINE_DASH) {
+      lineDash = null;
+    }
+    lineJoin = this.stroke_.getLineJoin();
+    if (lineJoin === undefined) {
+      lineJoin = ol.render.canvas.defaultLineJoin;
+    }
+    lineCap = this.stroke_.getLineCap();
+    if (lineCap === undefined) {
+      lineCap = ol.render.canvas.defaultLineCap;
+    }
+    miterLimit = this.stroke_.getMiterLimit();
+    if (miterLimit === undefined) {
+      miterLimit = ol.render.canvas.defaultMiterLimit;
+    }
+  }
+
+  var size = 2 * (this.radius_ + strokeWidth) + 1;
+
+  /** @type {ol.RegularShapeRenderOptions} */
+  var renderOptions = {
+    strokeStyle: strokeStyle,
+    strokeWidth: strokeWidth,
+    size: size,
+    lineCap: lineCap,
+    lineDash: lineDash,
+    lineJoin: lineJoin,
+    miterLimit: miterLimit
+  };
+
+  if (atlasManager === undefined) {
+    // no atlas manager is used, create a new canvas
+    var context = ol.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 symbol to an atlas
+    size = Math.round(size);
+
+    var hasCustomHitDetectionImage = !this.fill_;
+    var renderHitDetectionCallback;
+    if (hasCustomHitDetectionImage) {
+      // render the hit-detection image into a separate atlas image
+      renderHitDetectionCallback =
+          this.drawHitDetectionCanvas_.bind(this, renderOptions);
+    }
+
+    var id = this.getChecksum();
+    var info = atlasManager.add(
+        id, size, size, this.draw_.bind(this, renderOptions),
+        renderHitDetectionCallback);
+    goog.asserts.assert(info, 'shape size is too large');
+
+    this.canvas_ = info.image;
+    this.origin_ = [info.offsetX, info.offsetY];
+    imageSize = info.image.width;
+
+    if (hasCustomHitDetectionImage) {
+      this.hitDetectionCanvas_ = info.hitImage;
+      this.hitDetectionImageSize_ =
+          [info.hitImage.width, info.hitImage.height];
+    } else {
+      this.hitDetectionCanvas_ = this.canvas_;
+      this.hitDetectionImageSize_ = [imageSize, imageSize];
+    }
+  }
+
+  this.anchor_ = [size / 2, size / 2];
+  this.size_ = [size, size];
+  this.imageSize_ = [imageSize, imageSize];
+};
+
+
+/**
+ * @private
+ * @param {ol.RegularShapeRenderOptions} renderOptions Render options.
+ * @param {CanvasRenderingContext2D} context The rendering context.
+ * @param {number} x The origin for the symbol (x).
+ * @param {number} y The origin for the symbol (y).
+ */
+ol.style.RegularShape.prototype.draw_ = function(renderOptions, context, x, y) {
+  var i, angle0, radiusC;
+  // reset transform
+  context.setTransform(1, 0, 0, 1, 0, 0);
+
+  // then move to (x, y)
+  context.translate(x, y);
+
+  context.beginPath();
+  if (this.radius2_ !== this.radius_) {
+    this.points_ = 2 * this.points_;
+  }
+  for (i = 0; i <= this.points_; i++) {
+    angle0 = i * 2 * Math.PI / this.points_ - Math.PI / 2 + this.angle_;
+    radiusC = i % 2 === 0 ? this.radius_ : this.radius2_;
+    context.lineTo(renderOptions.size / 2 + radiusC * Math.cos(angle0),
+                   renderOptions.size / 2 + radiusC * Math.sin(angle0));
+  }
+
+  if (this.fill_) {
+    context.fillStyle = ol.colorlike.asColorLike(this.fill_.getColor());
+    context.fill();
+  }
+  if (this.stroke_) {
+    context.strokeStyle = renderOptions.strokeStyle;
+    context.lineWidth = renderOptions.strokeWidth;
+    if (renderOptions.lineDash) {
+      context.setLineDash(renderOptions.lineDash);
+    }
+    context.lineCap = renderOptions.lineCap;
+    context.lineJoin = renderOptions.lineJoin;
+    context.miterLimit = renderOptions.miterLimit;
+    context.stroke();
+  }
+  context.closePath();
+};
+
+
+/**
+ * @private
+ * @param {ol.RegularShapeRenderOptions} renderOptions Render options.
+ */
+ol.style.RegularShape.prototype.createHitDetectionCanvas_ = function(renderOptions) {
+  this.hitDetectionImageSize_ = [renderOptions.size, renderOptions.size];
+  if (this.fill_) {
+    this.hitDetectionCanvas_ = this.canvas_;
+    return;
+  }
+
+  // if no fill style is set, create an extra hit-detection image with a
+  // default fill style
+  var context = ol.dom.createCanvasContext2D(renderOptions.size, renderOptions.size);
+  this.hitDetectionCanvas_ = context.canvas;
+
+  this.drawHitDetectionCanvas_(renderOptions, context, 0, 0);
+};
+
+
+/**
+ * @private
+ * @param {ol.RegularShapeRenderOptions} renderOptions Render options.
+ * @param {CanvasRenderingContext2D} context The context.
+ * @param {number} x The origin for the symbol (x).
+ * @param {number} y The origin for the symbol (y).
+ */
+ol.style.RegularShape.prototype.drawHitDetectionCanvas_ = function(renderOptions, context, x, y) {
+  // reset transform
+  context.setTransform(1, 0, 0, 1, 0, 0);
+
+  // then move to (x, y)
+  context.translate(x, y);
+
+  context.beginPath();
+  if (this.radius2_ !== this.radius_) {
+    this.points_ = 2 * this.points_;
+  }
+  var i, radiusC, angle0;
+  for (i = 0; i <= this.points_; i++) {
+    angle0 = i * 2 * Math.PI / this.points_ - Math.PI / 2 + this.angle_;
+    radiusC = i % 2 === 0 ? this.radius_ : this.radius2_;
+    context.lineTo(renderOptions.size / 2 + radiusC * Math.cos(angle0),
+                   renderOptions.size / 2 + radiusC * Math.sin(angle0));
+  }
+
+  context.fillStyle = ol.render.canvas.defaultFillStyle;
+  context.fill();
+  if (this.stroke_) {
+    context.strokeStyle = renderOptions.strokeStyle;
+    context.lineWidth = renderOptions.strokeWidth;
+    if (renderOptions.lineDash) {
+      context.setLineDash(renderOptions.lineDash);
+    }
+    context.stroke();
+  }
+  context.closePath();
+};
+
+
+/**
+ * @return {string} The checksum.
+ */
+ol.style.RegularShape.prototype.getChecksum = function() {
+  var strokeChecksum = this.stroke_ ?
+      this.stroke_.getChecksum() : '-';
+  var fillChecksum = this.fill_ ?
+      this.fill_.getChecksum() : '-';
+
+  var recalculate = !this.checksums_ ||
+      (strokeChecksum != this.checksums_[1] ||
+      fillChecksum != this.checksums_[2] ||
+      this.radius_ != this.checksums_[3] ||
+      this.radius2_ != this.checksums_[4] ||
+      this.angle_ != this.checksums_[5] ||
+      this.points_ != this.checksums_[6]);
+
+  if (recalculate) {
+    var checksum = 'r' + strokeChecksum + fillChecksum +
+        (this.radius_ !== undefined ? this.radius_.toString() : '-') +
+        (this.radius2_ !== undefined ? this.radius2_.toString() : '-') +
+        (this.angle_ !== undefined ? this.angle_.toString() : '-') +
+        (this.points_ !== undefined ? this.points_.toString() : '-');
+    this.checksums_ = [checksum, strokeChecksum, fillChecksum,
+      this.radius_, this.radius2_, this.angle_, this.points_];
+  }
+
+  return this.checksums_[0];
+};
+
+// Copyright 2009 The Closure Library Authors.
+// All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS-IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// This file has been auto-generated by GenJsDeps, please do not edit.
+
+goog.addDependency(
+    'demos/editor/equationeditor.js', ['goog.demos.editor.EquationEditor'],
+    ['goog.ui.equation.EquationEditorDialog']);
+goog.addDependency(
+    'demos/editor/helloworld.js', ['goog.demos.editor.HelloWorld'],
+    ['goog.dom', 'goog.dom.TagName', 'goog.editor.Plugin']);
+goog.addDependency(
+    'demos/editor/helloworlddialog.js',
+    [
+      'goog.demos.editor.HelloWorldDialog',
+      'goog.demos.editor.HelloWorldDialog.OkEvent'
+    ],
+    [
+      'goog.dom.TagName', 'goog.events.Event', 'goog.string',
+      'goog.ui.editor.AbstractDialog', 'goog.ui.editor.AbstractDialog.Builder',
+      'goog.ui.editor.AbstractDialog.EventType'
+    ]);
+goog.addDependency(
+    'demos/editor/helloworlddialogplugin.js',
+    [
+      'goog.demos.editor.HelloWorldDialogPlugin',
+      'goog.demos.editor.HelloWorldDialogPlugin.Command'
+    ],
+    [
+      'goog.demos.editor.HelloWorldDialog', 'goog.dom.TagName',
+      'goog.editor.plugins.AbstractDialogPlugin', 'goog.editor.range',
+      'goog.functions', 'goog.ui.editor.AbstractDialog.EventType'
+    ]);
+
+/**
+ * @fileoverview Custom exports file.
+ * @suppress {checkVars,extraRequire}
+ */
+
+goog.require('ol');
+goog.require('ol.Attribution');
+goog.require('ol.Collection');
+goog.require('ol.CollectionEvent');
+goog.require('ol.CollectionEventType');
+goog.require('ol.DeviceOrientation');
+goog.require('ol.DragBoxEvent');
+goog.require('ol.Feature');
+goog.require('ol.Geolocation');
+goog.require('ol.Graticule');
+goog.require('ol.Image');
+goog.require('ol.ImageTile');
+goog.require('ol.Kinetic');
+goog.require('ol.Map');
+goog.require('ol.MapBrowserEvent');
+goog.require('ol.MapBrowserEvent.EventType');
+goog.require('ol.MapBrowserEventHandler');
+goog.require('ol.MapBrowserPointerEvent');
+goog.require('ol.MapEvent');
+goog.require('ol.MapEventType');
+goog.require('ol.MapProperty');
+goog.require('ol.Object');
+goog.require('ol.ObjectEvent');
+goog.require('ol.ObjectEventType');
+goog.require('ol.Observable');
+goog.require('ol.Overlay');
+goog.require('ol.OverlayPositioning');
+goog.require('ol.RasterOperationType');
+goog.require('ol.Sphere');
+goog.require('ol.Tile');
+goog.require('ol.TileState');
+goog.require('ol.VectorTile');
+goog.require('ol.View');
+goog.require('ol.ViewHint');
+goog.require('ol.ViewProperty');
+goog.require('ol.animation');
+goog.require('ol.color');
+goog.require('ol.colorlike');
+goog.require('ol.control');
+goog.require('ol.control.Attribution');
+goog.require('ol.control.Control');
+goog.require('ol.control.FullScreen');
+goog.require('ol.control.MousePosition');
+goog.require('ol.control.OverviewMap');
+goog.require('ol.control.Rotate');
+goog.require('ol.control.ScaleLine');
+goog.require('ol.control.Zoom');
+goog.require('ol.control.ZoomSlider');
+goog.require('ol.control.ZoomToExtent');
+goog.require('ol.coordinate');
+goog.require('ol.easing');
+goog.require('ol.events.Event');
+goog.require('ol.events.condition');
+goog.require('ol.extent');
+goog.require('ol.extent.Corner');
+goog.require('ol.extent.Relationship');
+goog.require('ol.featureloader');
+goog.require('ol.format.EsriJSON');
+goog.require('ol.format.Feature');
+goog.require('ol.format.GML');
+goog.require('ol.format.GML2');
+goog.require('ol.format.GML3');
+goog.require('ol.format.GMLBase');
+goog.require('ol.format.GPX');
+goog.require('ol.format.GeoJSON');
+goog.require('ol.format.IGC');
+goog.require('ol.format.KML');
+goog.require('ol.format.MVT');
+goog.require('ol.format.OSMXML');
+goog.require('ol.format.Polyline');
+goog.require('ol.format.TopoJSON');
+goog.require('ol.format.WFS');
+goog.require('ol.format.WKT');
+goog.require('ol.format.WMSCapabilities');
+goog.require('ol.format.WMSGetFeatureInfo');
+goog.require('ol.format.WMTSCapabilities');
+goog.require('ol.format.ogc.filter');
+goog.require('ol.format.ogc.filter.And');
+goog.require('ol.format.ogc.filter.Bbox');
+goog.require('ol.format.ogc.filter.Comparison');
+goog.require('ol.format.ogc.filter.ComparisonBinary');
+goog.require('ol.format.ogc.filter.EqualTo');
+goog.require('ol.format.ogc.filter.Filter');
+goog.require('ol.format.ogc.filter.GreaterThan');
+goog.require('ol.format.ogc.filter.GreaterThanOrEqualTo');
+goog.require('ol.format.ogc.filter.IsBetween');
+goog.require('ol.format.ogc.filter.IsLike');
+goog.require('ol.format.ogc.filter.IsNull');
+goog.require('ol.format.ogc.filter.LessThan');
+goog.require('ol.format.ogc.filter.LessThanOrEqualTo');
+goog.require('ol.format.ogc.filter.Logical');
+goog.require('ol.format.ogc.filter.LogicalBinary');
+goog.require('ol.format.ogc.filter.Not');
+goog.require('ol.format.ogc.filter.NotEqualTo');
+goog.require('ol.format.ogc.filter.Or');
+goog.require('ol.geom.Circle');
+goog.require('ol.geom.Geometry');
+goog.require('ol.geom.GeometryCollection');
+goog.require('ol.geom.GeometryLayout');
+goog.require('ol.geom.GeometryType');
+goog.require('ol.geom.LineString');
+goog.require('ol.geom.LinearRing');
+goog.require('ol.geom.MultiLineString');
+goog.require('ol.geom.MultiPoint');
+goog.require('ol.geom.MultiPolygon');
+goog.require('ol.geom.Point');
+goog.require('ol.geom.Polygon');
+goog.require('ol.geom.SimpleGeometry');
+goog.require('ol.has');
+goog.require('ol.interaction');
+goog.require('ol.interaction.DoubleClickZoom');
+goog.require('ol.interaction.DragAndDrop');
+goog.require('ol.interaction.DragAndDropEvent');
+goog.require('ol.interaction.DragBox');
+goog.require('ol.interaction.DragPan');
+goog.require('ol.interaction.DragRotate');
+goog.require('ol.interaction.DragRotateAndZoom');
+goog.require('ol.interaction.DragZoom');
+goog.require('ol.interaction.Draw');
+goog.require('ol.interaction.DrawEvent');
+goog.require('ol.interaction.DrawEventType');
+goog.require('ol.interaction.DrawMode');
+goog.require('ol.interaction.Interaction');
+goog.require('ol.interaction.InteractionProperty');
+goog.require('ol.interaction.KeyboardPan');
+goog.require('ol.interaction.KeyboardZoom');
+goog.require('ol.interaction.Modify');
+goog.require('ol.interaction.ModifyEvent');
+goog.require('ol.interaction.MouseWheelZoom');
+goog.require('ol.interaction.PinchRotate');
+goog.require('ol.interaction.PinchZoom');
+goog.require('ol.interaction.Pointer');
+goog.require('ol.interaction.Select');
+goog.require('ol.interaction.SelectEvent');
+goog.require('ol.interaction.SelectEventType');
+goog.require('ol.interaction.Snap');
+goog.require('ol.interaction.SnapProperty');
+goog.require('ol.interaction.Translate');
+goog.require('ol.interaction.TranslateEvent');
+goog.require('ol.layer.Base');
+goog.require('ol.layer.Group');
+goog.require('ol.layer.Heatmap');
+goog.require('ol.layer.Image');
+goog.require('ol.layer.Layer');
+goog.require('ol.layer.LayerProperty');
+goog.require('ol.layer.Tile');
+goog.require('ol.layer.Vector');
+goog.require('ol.layer.VectorTile');
+goog.require('ol.loadingstrategy');
+goog.require('ol.proj');
+goog.require('ol.proj.METERS_PER_UNIT');
+goog.require('ol.proj.Projection');
+goog.require('ol.proj.Units');
+goog.require('ol.proj.common');
+goog.require('ol.render');
+goog.require('ol.render.Event');
+goog.require('ol.render.EventType');
+goog.require('ol.render.Feature');
+goog.require('ol.render.VectorContext');
+goog.require('ol.render.canvas.Immediate');
+goog.require('ol.render.webgl.Immediate');
+goog.require('ol.size');
+goog.require('ol.source.BingMaps');
+goog.require('ol.source.CartoDB');
+goog.require('ol.source.Cluster');
+goog.require('ol.source.Image');
+goog.require('ol.source.ImageArcGISRest');
+goog.require('ol.source.ImageCanvas');
+goog.require('ol.source.ImageEvent');
+goog.require('ol.source.ImageMapGuide');
+goog.require('ol.source.ImageStatic');
+goog.require('ol.source.ImageVector');
+goog.require('ol.source.ImageWMS');
+goog.require('ol.source.OSM');
+goog.require('ol.source.Raster');
+goog.require('ol.source.RasterEvent');
+goog.require('ol.source.RasterEventType');
+goog.require('ol.source.Source');
+goog.require('ol.source.Stamen');
+goog.require('ol.source.State');
+goog.require('ol.source.Tile');
+goog.require('ol.source.TileArcGISRest');
+goog.require('ol.source.TileDebug');
+goog.require('ol.source.TileEvent');
+goog.require('ol.source.TileImage');
+goog.require('ol.source.TileJSON');
+goog.require('ol.source.TileUTFGrid');
+goog.require('ol.source.TileWMS');
+goog.require('ol.source.UrlTile');
+goog.require('ol.source.Vector');
+goog.require('ol.source.VectorEvent');
+goog.require('ol.source.VectorEventType');
+goog.require('ol.source.VectorTile');
+goog.require('ol.source.WMTS');
+goog.require('ol.source.XYZ');
+goog.require('ol.source.Zoomify');
+goog.require('ol.style.Atlas');
+goog.require('ol.style.AtlasManager');
+goog.require('ol.style.Circle');
+goog.require('ol.style.Fill');
+goog.require('ol.style.Icon');
+goog.require('ol.style.IconAnchorUnits');
+goog.require('ol.style.IconImageCache');
+goog.require('ol.style.IconOrigin');
+goog.require('ol.style.Image');
+goog.require('ol.style.ImageState');
+goog.require('ol.style.RegularShape');
+goog.require('ol.style.Stroke');
+goog.require('ol.style.Style');
+goog.require('ol.style.Text');
+goog.require('ol.style.defaultGeometryFunction');
+goog.require('ol.tilegrid.TileGrid');
+goog.require('ol.tilegrid.WMTS');
+goog.require('ol.tilejson');
+goog.require('ol.webgl.Context');
+goog.require('ol.xml');
+
+
+goog.exportSymbol(
+    'ol.animation.bounce',
+    ol.animation.bounce,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.animation.pan',
+    ol.animation.pan,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.animation.rotate',
+    ol.animation.rotate,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.animation.zoom',
+    ol.animation.zoom,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.Attribution',
+    ol.Attribution,
+    OPENLAYERS);
+
+goog.exportProperty(
+    ol.Attribution.prototype,
+    'getHTML',
+    ol.Attribution.prototype.getHTML);
+
+goog.exportProperty(
+    ol.CollectionEvent.prototype,
+    'element',
+    ol.CollectionEvent.prototype.element);
+
+goog.exportSymbol(
+    'ol.Collection',
+    ol.Collection,
+    OPENLAYERS);
+
+goog.exportProperty(
+    ol.Collection.prototype,
+    'clear',
+    ol.Collection.prototype.clear);
+
+goog.exportProperty(
+    ol.Collection.prototype,
+    'extend',
+    ol.Collection.prototype.extend);
+
+goog.exportProperty(
+    ol.Collection.prototype,
+    'forEach',
+    ol.Collection.prototype.forEach);
+
+goog.exportProperty(
+    ol.Collection.prototype,
+    'getArray',
+    ol.Collection.prototype.getArray);
+
+goog.exportProperty(
+    ol.Collection.prototype,
+    'item',
+    ol.Collection.prototype.item);
+
+goog.exportProperty(
+    ol.Collection.prototype,
+    'getLength',
+    ol.Collection.prototype.getLength);
+
+goog.exportProperty(
+    ol.Collection.prototype,
+    'insertAt',
+    ol.Collection.prototype.insertAt);
+
+goog.exportProperty(
+    ol.Collection.prototype,
+    'pop',
+    ol.Collection.prototype.pop);
+
+goog.exportProperty(
+    ol.Collection.prototype,
+    'push',
+    ol.Collection.prototype.push);
+
+goog.exportProperty(
+    ol.Collection.prototype,
+    'remove',
+    ol.Collection.prototype.remove);
+
+goog.exportProperty(
+    ol.Collection.prototype,
+    'removeAt',
+    ol.Collection.prototype.removeAt);
+
+goog.exportProperty(
+    ol.Collection.prototype,
+    'setAt',
+    ol.Collection.prototype.setAt);
+
+goog.exportSymbol(
+    'ol.colorlike.asColorLike',
+    ol.colorlike.asColorLike,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.coordinate.add',
+    ol.coordinate.add,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.coordinate.createStringXY',
+    ol.coordinate.createStringXY,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.coordinate.format',
+    ol.coordinate.format,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.coordinate.rotate',
+    ol.coordinate.rotate,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.coordinate.toStringHDMS',
+    ol.coordinate.toStringHDMS,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.coordinate.toStringXY',
+    ol.coordinate.toStringXY,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.DeviceOrientation',
+    ol.DeviceOrientation,
+    OPENLAYERS);
+
+goog.exportProperty(
+    ol.DeviceOrientation.prototype,
+    'getAlpha',
+    ol.DeviceOrientation.prototype.getAlpha);
+
+goog.exportProperty(
+    ol.DeviceOrientation.prototype,
+    'getBeta',
+    ol.DeviceOrientation.prototype.getBeta);
+
+goog.exportProperty(
+    ol.DeviceOrientation.prototype,
+    'getGamma',
+    ol.DeviceOrientation.prototype.getGamma);
+
+goog.exportProperty(
+    ol.DeviceOrientation.prototype,
+    'getHeading',
+    ol.DeviceOrientation.prototype.getHeading);
+
+goog.exportProperty(
+    ol.DeviceOrientation.prototype,
+    'getTracking',
+    ol.DeviceOrientation.prototype.getTracking);
+
+goog.exportProperty(
+    ol.DeviceOrientation.prototype,
+    'setTracking',
+    ol.DeviceOrientation.prototype.setTracking);
+
+goog.exportSymbol(
+    'ol.easing.easeIn',
+    ol.easing.easeIn,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.easing.easeOut',
+    ol.easing.easeOut,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.easing.inAndOut',
+    ol.easing.inAndOut,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.easing.linear',
+    ol.easing.linear,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.easing.upAndDown',
+    ol.easing.upAndDown,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.extent.boundingExtent',
+    ol.extent.boundingExtent,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.extent.buffer',
+    ol.extent.buffer,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.extent.containsCoordinate',
+    ol.extent.containsCoordinate,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.extent.containsExtent',
+    ol.extent.containsExtent,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.extent.containsXY',
+    ol.extent.containsXY,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.extent.createEmpty',
+    ol.extent.createEmpty,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.extent.equals',
+    ol.extent.equals,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.extent.extend',
+    ol.extent.extend,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.extent.getBottomLeft',
+    ol.extent.getBottomLeft,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.extent.getBottomRight',
+    ol.extent.getBottomRight,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.extent.getCenter',
+    ol.extent.getCenter,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.extent.getHeight',
+    ol.extent.getHeight,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.extent.getIntersection',
+    ol.extent.getIntersection,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.extent.getSize',
+    ol.extent.getSize,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.extent.getTopLeft',
+    ol.extent.getTopLeft,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.extent.getTopRight',
+    ol.extent.getTopRight,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.extent.getWidth',
+    ol.extent.getWidth,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.extent.intersects',
+    ol.extent.intersects,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.extent.isEmpty',
+    ol.extent.isEmpty,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.extent.applyTransform',
+    ol.extent.applyTransform,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.Feature',
+    ol.Feature,
+    OPENLAYERS);
+
+goog.exportProperty(
+    ol.Feature.prototype,
+    'clone',
+    ol.Feature.prototype.clone);
+
+goog.exportProperty(
+    ol.Feature.prototype,
+    'getGeometry',
+    ol.Feature.prototype.getGeometry);
+
+goog.exportProperty(
+    ol.Feature.prototype,
+    'getId',
+    ol.Feature.prototype.getId);
+
+goog.exportProperty(
+    ol.Feature.prototype,
+    'getGeometryName',
+    ol.Feature.prototype.getGeometryName);
+
+goog.exportProperty(
+    ol.Feature.prototype,
+    'getStyle',
+    ol.Feature.prototype.getStyle);
+
+goog.exportProperty(
+    ol.Feature.prototype,
+    'getStyleFunction',
+    ol.Feature.prototype.getStyleFunction);
+
+goog.exportProperty(
+    ol.Feature.prototype,
+    'setGeometry',
+    ol.Feature.prototype.setGeometry);
+
+goog.exportProperty(
+    ol.Feature.prototype,
+    'setStyle',
+    ol.Feature.prototype.setStyle);
+
+goog.exportProperty(
+    ol.Feature.prototype,
+    'setId',
+    ol.Feature.prototype.setId);
+
+goog.exportProperty(
+    ol.Feature.prototype,
+    'setGeometryName',
+    ol.Feature.prototype.setGeometryName);
+
+goog.exportSymbol(
+    'ol.featureloader.tile',
+    ol.featureloader.tile,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.featureloader.xhr',
+    ol.featureloader.xhr,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.Geolocation',
+    ol.Geolocation,
+    OPENLAYERS);
+
+goog.exportProperty(
+    ol.Geolocation.prototype,
+    'getAccuracy',
+    ol.Geolocation.prototype.getAccuracy);
+
+goog.exportProperty(
+    ol.Geolocation.prototype,
+    'getAccuracyGeometry',
+    ol.Geolocation.prototype.getAccuracyGeometry);
+
+goog.exportProperty(
+    ol.Geolocation.prototype,
+    'getAltitude',
+    ol.Geolocation.prototype.getAltitude);
+
+goog.exportProperty(
+    ol.Geolocation.prototype,
+    'getAltitudeAccuracy',
+    ol.Geolocation.prototype.getAltitudeAccuracy);
+
+goog.exportProperty(
+    ol.Geolocation.prototype,
+    'getHeading',
+    ol.Geolocation.prototype.getHeading);
+
+goog.exportProperty(
+    ol.Geolocation.prototype,
+    'getPosition',
+    ol.Geolocation.prototype.getPosition);
+
+goog.exportProperty(
+    ol.Geolocation.prototype,
+    'getProjection',
+    ol.Geolocation.prototype.getProjection);
+
+goog.exportProperty(
+    ol.Geolocation.prototype,
+    'getSpeed',
+    ol.Geolocation.prototype.getSpeed);
+
+goog.exportProperty(
+    ol.Geolocation.prototype,
+    'getTracking',
+    ol.Geolocation.prototype.getTracking);
+
+goog.exportProperty(
+    ol.Geolocation.prototype,
+    'getTrackingOptions',
+    ol.Geolocation.prototype.getTrackingOptions);
+
+goog.exportProperty(
+    ol.Geolocation.prototype,
+    'setProjection',
+    ol.Geolocation.prototype.setProjection);
+
+goog.exportProperty(
+    ol.Geolocation.prototype,
+    'setTracking',
+    ol.Geolocation.prototype.setTracking);
+
+goog.exportProperty(
+    ol.Geolocation.prototype,
+    'setTrackingOptions',
+    ol.Geolocation.prototype.setTrackingOptions);
+
+goog.exportSymbol(
+    'ol.Graticule',
+    ol.Graticule,
+    OPENLAYERS);
+
+goog.exportProperty(
+    ol.Graticule.prototype,
+    'getMap',
+    ol.Graticule.prototype.getMap);
+
+goog.exportProperty(
+    ol.Graticule.prototype,
+    'getMeridians',
+    ol.Graticule.prototype.getMeridians);
+
+goog.exportProperty(
+    ol.Graticule.prototype,
+    'getParallels',
+    ol.Graticule.prototype.getParallels);
+
+goog.exportProperty(
+    ol.Graticule.prototype,
+    'setMap',
+    ol.Graticule.prototype.setMap);
+
+goog.exportSymbol(
+    'ol.has.DEVICE_PIXEL_RATIO',
+    ol.has.DEVICE_PIXEL_RATIO,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.has.CANVAS',
+    ol.has.CANVAS,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.has.DEVICE_ORIENTATION',
+    ol.has.DEVICE_ORIENTATION,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.has.GEOLOCATION',
+    ol.has.GEOLOCATION,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.has.TOUCH',
+    ol.has.TOUCH,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.has.WEBGL',
+    ol.has.WEBGL,
+    OPENLAYERS);
+
+goog.exportProperty(
+    ol.Image.prototype,
+    'getImage',
+    ol.Image.prototype.getImage);
+
+goog.exportProperty(
+    ol.Image.prototype,
+    'load',
+    ol.Image.prototype.load);
+
+goog.exportProperty(
+    ol.ImageTile.prototype,
+    'getImage',
+    ol.ImageTile.prototype.getImage);
+
+goog.exportProperty(
+    ol.ImageTile.prototype,
+    'load',
+    ol.ImageTile.prototype.load);
+
+goog.exportSymbol(
+    'ol.Kinetic',
+    ol.Kinetic,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.loadingstrategy.all',
+    ol.loadingstrategy.all,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.loadingstrategy.bbox',
+    ol.loadingstrategy.bbox,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.loadingstrategy.tile',
+    ol.loadingstrategy.tile,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.Map',
+    ol.Map,
+    OPENLAYERS);
+
+goog.exportProperty(
+    ol.Map.prototype,
+    'addControl',
+    ol.Map.prototype.addControl);
+
+goog.exportProperty(
+    ol.Map.prototype,
+    'addInteraction',
+    ol.Map.prototype.addInteraction);
+
+goog.exportProperty(
+    ol.Map.prototype,
+    'addLayer',
+    ol.Map.prototype.addLayer);
+
+goog.exportProperty(
+    ol.Map.prototype,
+    'addOverlay',
+    ol.Map.prototype.addOverlay);
+
+goog.exportProperty(
+    ol.Map.prototype,
+    'beforeRender',
+    ol.Map.prototype.beforeRender);
+
+goog.exportProperty(
+    ol.Map.prototype,
+    'forEachFeatureAtPixel',
+    ol.Map.prototype.forEachFeatureAtPixel);
+
+goog.exportProperty(
+    ol.Map.prototype,
+    'forEachLayerAtPixel',
+    ol.Map.prototype.forEachLayerAtPixel);
+
+goog.exportProperty(
+    ol.Map.prototype,
+    'hasFeatureAtPixel',
+    ol.Map.prototype.hasFeatureAtPixel);
+
+goog.exportProperty(
+    ol.Map.prototype,
+    'getEventCoordinate',
+    ol.Map.prototype.getEventCoordinate);
+
+goog.exportProperty(
+    ol.Map.prototype,
+    'getEventPixel',
+    ol.Map.prototype.getEventPixel);
+
+goog.exportProperty(
+    ol.Map.prototype,
+    'getTarget',
+    ol.Map.prototype.getTarget);
+
+goog.exportProperty(
+    ol.Map.prototype,
+    'getTargetElement',
+    ol.Map.prototype.getTargetElement);
+
+goog.exportProperty(
+    ol.Map.prototype,
+    'getCoordinateFromPixel',
+    ol.Map.prototype.getCoordinateFromPixel);
+
+goog.exportProperty(
+    ol.Map.prototype,
+    'getControls',
+    ol.Map.prototype.getControls);
+
+goog.exportProperty(
+    ol.Map.prototype,
+    'getOverlays',
+    ol.Map.prototype.getOverlays);
+
+goog.exportProperty(
+    ol.Map.prototype,
+    'getOverlayById',
+    ol.Map.prototype.getOverlayById);
+
+goog.exportProperty(
+    ol.Map.prototype,
+    'getInteractions',
+    ol.Map.prototype.getInteractions);
+
+goog.exportProperty(
+    ol.Map.prototype,
+    'getLayerGroup',
+    ol.Map.prototype.getLayerGroup);
+
+goog.exportProperty(
+    ol.Map.prototype,
+    'getLayers',
+    ol.Map.prototype.getLayers);
+
+goog.exportProperty(
+    ol.Map.prototype,
+    'getPixelFromCoordinate',
+    ol.Map.prototype.getPixelFromCoordinate);
+
+goog.exportProperty(
+    ol.Map.prototype,
+    'getSize',
+    ol.Map.prototype.getSize);
+
+goog.exportProperty(
+    ol.Map.prototype,
+    'getView',
+    ol.Map.prototype.getView);
+
+goog.exportProperty(
+    ol.Map.prototype,
+    'getViewport',
+    ol.Map.prototype.getViewport);
+
+goog.exportProperty(
+    ol.Map.prototype,
+    'renderSync',
+    ol.Map.prototype.renderSync);
+
+goog.exportProperty(
+    ol.Map.prototype,
+    'render',
+    ol.Map.prototype.render);
+
+goog.exportProperty(
+    ol.Map.prototype,
+    'removeControl',
+    ol.Map.prototype.removeControl);
+
+goog.exportProperty(
+    ol.Map.prototype,
+    'removeInteraction',
+    ol.Map.prototype.removeInteraction);
+
+goog.exportProperty(
+    ol.Map.prototype,
+    'removeLayer',
+    ol.Map.prototype.removeLayer);
+
+goog.exportProperty(
+    ol.Map.prototype,
+    'removeOverlay',
+    ol.Map.prototype.removeOverlay);
+
+goog.exportProperty(
+    ol.Map.prototype,
+    'setLayerGroup',
+    ol.Map.prototype.setLayerGroup);
+
+goog.exportProperty(
+    ol.Map.prototype,
+    'setSize',
+    ol.Map.prototype.setSize);
+
+goog.exportProperty(
+    ol.Map.prototype,
+    'setTarget',
+    ol.Map.prototype.setTarget);
+
+goog.exportProperty(
+    ol.Map.prototype,
+    'setView',
+    ol.Map.prototype.setView);
+
+goog.exportProperty(
+    ol.Map.prototype,
+    'updateSize',
+    ol.Map.prototype.updateSize);
+
+goog.exportProperty(
+    ol.MapBrowserEvent.prototype,
+    'originalEvent',
+    ol.MapBrowserEvent.prototype.originalEvent);
+
+goog.exportProperty(
+    ol.MapBrowserEvent.prototype,
+    'pixel',
+    ol.MapBrowserEvent.prototype.pixel);
+
+goog.exportProperty(
+    ol.MapBrowserEvent.prototype,
+    'coordinate',
+    ol.MapBrowserEvent.prototype.coordinate);
+
+goog.exportProperty(
+    ol.MapBrowserEvent.prototype,
+    'dragging',
+    ol.MapBrowserEvent.prototype.dragging);
+
+goog.exportProperty(
+    ol.MapEvent.prototype,
+    'map',
+    ol.MapEvent.prototype.map);
+
+goog.exportProperty(
+    ol.MapEvent.prototype,
+    'frameState',
+    ol.MapEvent.prototype.frameState);
+
+goog.exportProperty(
+    ol.ObjectEvent.prototype,
+    'key',
+    ol.ObjectEvent.prototype.key);
+
+goog.exportProperty(
+    ol.ObjectEvent.prototype,
+    'oldValue',
+    ol.ObjectEvent.prototype.oldValue);
+
+goog.exportSymbol(
+    'ol.Object',
+    ol.Object,
+    OPENLAYERS);
+
+goog.exportProperty(
+    ol.Object.prototype,
+    'get',
+    ol.Object.prototype.get);
+
+goog.exportProperty(
+    ol.Object.prototype,
+    'getKeys',
+    ol.Object.prototype.getKeys);
+
+goog.exportProperty(
+    ol.Object.prototype,
+    'getProperties',
+    ol.Object.prototype.getProperties);
+
+goog.exportProperty(
+    ol.Object.prototype,
+    'set',
+    ol.Object.prototype.set);
+
+goog.exportProperty(
+    ol.Object.prototype,
+    'setProperties',
+    ol.Object.prototype.setProperties);
+
+goog.exportProperty(
+    ol.Object.prototype,
+    'unset',
+    ol.Object.prototype.unset);
+
+goog.exportSymbol(
+    'ol.Observable',
+    ol.Observable,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.Observable.unByKey',
+    ol.Observable.unByKey,
+    OPENLAYERS);
+
+goog.exportProperty(
+    ol.Observable.prototype,
+    'changed',
+    ol.Observable.prototype.changed);
+
+goog.exportProperty(
+    ol.Observable.prototype,
+    'dispatchEvent',
+    ol.Observable.prototype.dispatchEvent);
+
+goog.exportProperty(
+    ol.Observable.prototype,
+    'getRevision',
+    ol.Observable.prototype.getRevision);
+
+goog.exportProperty(
+    ol.Observable.prototype,
+    'on',
+    ol.Observable.prototype.on);
+
+goog.exportProperty(
+    ol.Observable.prototype,
+    'once',
+    ol.Observable.prototype.once);
+
+goog.exportProperty(
+    ol.Observable.prototype,
+    'un',
+    ol.Observable.prototype.un);
+
+goog.exportProperty(
+    ol.Observable.prototype,
+    'unByKey',
+    ol.Observable.prototype.unByKey);
+
+goog.exportSymbol(
+    'ol.inherits',
+    ol.inherits,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.Overlay',
+    ol.Overlay,
+    OPENLAYERS);
+
+goog.exportProperty(
+    ol.Overlay.prototype,
+    'getElement',
+    ol.Overlay.prototype.getElement);
+
+goog.exportProperty(
+    ol.Overlay.prototype,
+    'getId',
+    ol.Overlay.prototype.getId);
+
+goog.exportProperty(
+    ol.Overlay.prototype,
+    'getMap',
+    ol.Overlay.prototype.getMap);
+
+goog.exportProperty(
+    ol.Overlay.prototype,
+    'getOffset',
+    ol.Overlay.prototype.getOffset);
+
+goog.exportProperty(
+    ol.Overlay.prototype,
+    'getPosition',
+    ol.Overlay.prototype.getPosition);
+
+goog.exportProperty(
+    ol.Overlay.prototype,
+    'getPositioning',
+    ol.Overlay.prototype.getPositioning);
+
+goog.exportProperty(
+    ol.Overlay.prototype,
+    'setElement',
+    ol.Overlay.prototype.setElement);
+
+goog.exportProperty(
+    ol.Overlay.prototype,
+    'setMap',
+    ol.Overlay.prototype.setMap);
+
+goog.exportProperty(
+    ol.Overlay.prototype,
+    'setOffset',
+    ol.Overlay.prototype.setOffset);
+
+goog.exportProperty(
+    ol.Overlay.prototype,
+    'setPosition',
+    ol.Overlay.prototype.setPosition);
+
+goog.exportProperty(
+    ol.Overlay.prototype,
+    'setPositioning',
+    ol.Overlay.prototype.setPositioning);
+
+goog.exportSymbol(
+    'ol.render.toContext',
+    ol.render.toContext,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.size.toSize',
+    ol.size.toSize,
+    OPENLAYERS);
+
+goog.exportProperty(
+    ol.Tile.prototype,
+    'getTileCoord',
+    ol.Tile.prototype.getTileCoord);
+
+goog.exportProperty(
+    ol.Tile.prototype,
+    'load',
+    ol.Tile.prototype.load);
+
+goog.exportProperty(
+    ol.VectorTile.prototype,
+    'getFormat',
+    ol.VectorTile.prototype.getFormat);
+
+goog.exportProperty(
+    ol.VectorTile.prototype,
+    'setFeatures',
+    ol.VectorTile.prototype.setFeatures);
+
+goog.exportProperty(
+    ol.VectorTile.prototype,
+    'setProjection',
+    ol.VectorTile.prototype.setProjection);
+
+goog.exportProperty(
+    ol.VectorTile.prototype,
+    'setLoader',
+    ol.VectorTile.prototype.setLoader);
+
+goog.exportSymbol(
+    'ol.View',
+    ol.View,
+    OPENLAYERS);
+
+goog.exportProperty(
+    ol.View.prototype,
+    'constrainCenter',
+    ol.View.prototype.constrainCenter);
+
+goog.exportProperty(
+    ol.View.prototype,
+    'constrainResolution',
+    ol.View.prototype.constrainResolution);
+
+goog.exportProperty(
+    ol.View.prototype,
+    'constrainRotation',
+    ol.View.prototype.constrainRotation);
+
+goog.exportProperty(
+    ol.View.prototype,
+    'getCenter',
+    ol.View.prototype.getCenter);
+
+goog.exportProperty(
+    ol.View.prototype,
+    'calculateExtent',
+    ol.View.prototype.calculateExtent);
+
+goog.exportProperty(
+    ol.View.prototype,
+    'getMaxResolution',
+    ol.View.prototype.getMaxResolution);
+
+goog.exportProperty(
+    ol.View.prototype,
+    'getMinResolution',
+    ol.View.prototype.getMinResolution);
+
+goog.exportProperty(
+    ol.View.prototype,
+    'getProjection',
+    ol.View.prototype.getProjection);
+
+goog.exportProperty(
+    ol.View.prototype,
+    'getResolution',
+    ol.View.prototype.getResolution);
+
+goog.exportProperty(
+    ol.View.prototype,
+    'getResolutions',
+    ol.View.prototype.getResolutions);
+
+goog.exportProperty(
+    ol.View.prototype,
+    'getRotation',
+    ol.View.prototype.getRotation);
+
+goog.exportProperty(
+    ol.View.prototype,
+    'getZoom',
+    ol.View.prototype.getZoom);
+
+goog.exportProperty(
+    ol.View.prototype,
+    'fit',
+    ol.View.prototype.fit);
+
+goog.exportProperty(
+    ol.View.prototype,
+    'centerOn',
+    ol.View.prototype.centerOn);
+
+goog.exportProperty(
+    ol.View.prototype,
+    'rotate',
+    ol.View.prototype.rotate);
+
+goog.exportProperty(
+    ol.View.prototype,
+    'setCenter',
+    ol.View.prototype.setCenter);
+
+goog.exportProperty(
+    ol.View.prototype,
+    'setResolution',
+    ol.View.prototype.setResolution);
+
+goog.exportProperty(
+    ol.View.prototype,
+    'setRotation',
+    ol.View.prototype.setRotation);
+
+goog.exportProperty(
+    ol.View.prototype,
+    'setZoom',
+    ol.View.prototype.setZoom);
+
+goog.exportSymbol(
+    'ol.xml.getAllTextContent',
+    ol.xml.getAllTextContent,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.xml.parse',
+    ol.xml.parse,
+    OPENLAYERS);
+
+goog.exportProperty(
+    ol.webgl.Context.prototype,
+    'getGL',
+    ol.webgl.Context.prototype.getGL);
+
+goog.exportProperty(
+    ol.webgl.Context.prototype,
+    'useProgram',
+    ol.webgl.Context.prototype.useProgram);
+
+goog.exportSymbol(
+    'ol.tilegrid.TileGrid',
+    ol.tilegrid.TileGrid,
+    OPENLAYERS);
+
+goog.exportProperty(
+    ol.tilegrid.TileGrid.prototype,
+    'forEachTileCoord',
+    ol.tilegrid.TileGrid.prototype.forEachTileCoord);
+
+goog.exportProperty(
+    ol.tilegrid.TileGrid.prototype,
+    'getMaxZoom',
+    ol.tilegrid.TileGrid.prototype.getMaxZoom);
+
+goog.exportProperty(
+    ol.tilegrid.TileGrid.prototype,
+    'getMinZoom',
+    ol.tilegrid.TileGrid.prototype.getMinZoom);
+
+goog.exportProperty(
+    ol.tilegrid.TileGrid.prototype,
+    'getOrigin',
+    ol.tilegrid.TileGrid.prototype.getOrigin);
+
+goog.exportProperty(
+    ol.tilegrid.TileGrid.prototype,
+    'getResolution',
+    ol.tilegrid.TileGrid.prototype.getResolution);
+
+goog.exportProperty(
+    ol.tilegrid.TileGrid.prototype,
+    'getResolutions',
+    ol.tilegrid.TileGrid.prototype.getResolutions);
+
+goog.exportProperty(
+    ol.tilegrid.TileGrid.prototype,
+    'getTileCoordExtent',
+    ol.tilegrid.TileGrid.prototype.getTileCoordExtent);
+
+goog.exportProperty(
+    ol.tilegrid.TileGrid.prototype,
+    'getTileCoordForCoordAndResolution',
+    ol.tilegrid.TileGrid.prototype.getTileCoordForCoordAndResolution);
+
+goog.exportProperty(
+    ol.tilegrid.TileGrid.prototype,
+    'getTileCoordForCoordAndZ',
+    ol.tilegrid.TileGrid.prototype.getTileCoordForCoordAndZ);
+
+goog.exportProperty(
+    ol.tilegrid.TileGrid.prototype,
+    'getTileSize',
+    ol.tilegrid.TileGrid.prototype.getTileSize);
+
+goog.exportProperty(
+    ol.tilegrid.TileGrid.prototype,
+    'getZForResolution',
+    ol.tilegrid.TileGrid.prototype.getZForResolution);
+
+goog.exportSymbol(
+    'ol.tilegrid.createXYZ',
+    ol.tilegrid.createXYZ,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.tilegrid.WMTS',
+    ol.tilegrid.WMTS,
+    OPENLAYERS);
+
+goog.exportProperty(
+    ol.tilegrid.WMTS.prototype,
+    'getMatrixIds',
+    ol.tilegrid.WMTS.prototype.getMatrixIds);
+
+goog.exportSymbol(
+    'ol.tilegrid.WMTS.createFromCapabilitiesMatrixSet',
+    ol.tilegrid.WMTS.createFromCapabilitiesMatrixSet,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.style.AtlasManager',
+    ol.style.AtlasManager,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.style.Circle',
+    ol.style.Circle,
+    OPENLAYERS);
+
+goog.exportProperty(
+    ol.style.Circle.prototype,
+    'getFill',
+    ol.style.Circle.prototype.getFill);
+
+goog.exportProperty(
+    ol.style.Circle.prototype,
+    'getImage',
+    ol.style.Circle.prototype.getImage);
+
+goog.exportProperty(
+    ol.style.Circle.prototype,
+    'getRadius',
+    ol.style.Circle.prototype.getRadius);
+
+goog.exportProperty(
+    ol.style.Circle.prototype,
+    'getStroke',
+    ol.style.Circle.prototype.getStroke);
+
+goog.exportSymbol(
+    'ol.style.Fill',
+    ol.style.Fill,
+    OPENLAYERS);
+
+goog.exportProperty(
+    ol.style.Fill.prototype,
+    'getColor',
+    ol.style.Fill.prototype.getColor);
+
+goog.exportProperty(
+    ol.style.Fill.prototype,
+    'setColor',
+    ol.style.Fill.prototype.setColor);
+
+goog.exportSymbol(
+    'ol.style.Icon',
+    ol.style.Icon,
+    OPENLAYERS);
+
+goog.exportProperty(
+    ol.style.Icon.prototype,
+    'getAnchor',
+    ol.style.Icon.prototype.getAnchor);
+
+goog.exportProperty(
+    ol.style.Icon.prototype,
+    'getImage',
+    ol.style.Icon.prototype.getImage);
+
+goog.exportProperty(
+    ol.style.Icon.prototype,
+    'getOrigin',
+    ol.style.Icon.prototype.getOrigin);
+
+goog.exportProperty(
+    ol.style.Icon.prototype,
+    'getSrc',
+    ol.style.Icon.prototype.getSrc);
+
+goog.exportProperty(
+    ol.style.Icon.prototype,
+    'getSize',
+    ol.style.Icon.prototype.getSize);
+
+goog.exportProperty(
+    ol.style.Icon.prototype,
+    'load',
+    ol.style.Icon.prototype.load);
+
+goog.exportSymbol(
+    'ol.style.Image',
+    ol.style.Image,
+    OPENLAYERS);
+
+goog.exportProperty(
+    ol.style.Image.prototype,
+    'getOpacity',
+    ol.style.Image.prototype.getOpacity);
+
+goog.exportProperty(
+    ol.style.Image.prototype,
+    'getRotateWithView',
+    ol.style.Image.prototype.getRotateWithView);
+
+goog.exportProperty(
+    ol.style.Image.prototype,
+    'getRotation',
+    ol.style.Image.prototype.getRotation);
+
+goog.exportProperty(
+    ol.style.Image.prototype,
+    'getScale',
+    ol.style.Image.prototype.getScale);
+
+goog.exportProperty(
+    ol.style.Image.prototype,
+    'getSnapToPixel',
+    ol.style.Image.prototype.getSnapToPixel);
+
+goog.exportProperty(
+    ol.style.Image.prototype,
+    'setOpacity',
+    ol.style.Image.prototype.setOpacity);
+
+goog.exportProperty(
+    ol.style.Image.prototype,
+    'setRotation',
+    ol.style.Image.prototype.setRotation);
+
+goog.exportProperty(
+    ol.style.Image.prototype,
+    'setScale',
+    ol.style.Image.prototype.setScale);
+
+goog.exportSymbol(
+    'ol.style.RegularShape',
+    ol.style.RegularShape,
+    OPENLAYERS);
+
+goog.exportProperty(
+    ol.style.RegularShape.prototype,
+    'getAnchor',
+    ol.style.RegularShape.prototype.getAnchor);
+
+goog.exportProperty(
+    ol.style.RegularShape.prototype,
+    'getAngle',
+    ol.style.RegularShape.prototype.getAngle);
+
+goog.exportProperty(
+    ol.style.RegularShape.prototype,
+    'getFill',
+    ol.style.RegularShape.prototype.getFill);
+
+goog.exportProperty(
+    ol.style.RegularShape.prototype,
+    'getImage',
+    ol.style.RegularShape.prototype.getImage);
+
+goog.exportProperty(
+    ol.style.RegularShape.prototype,
+    'getOrigin',
+    ol.style.RegularShape.prototype.getOrigin);
+
+goog.exportProperty(
+    ol.style.RegularShape.prototype,
+    'getPoints',
+    ol.style.RegularShape.prototype.getPoints);
+
+goog.exportProperty(
+    ol.style.RegularShape.prototype,
+    'getRadius',
+    ol.style.RegularShape.prototype.getRadius);
+
+goog.exportProperty(
+    ol.style.RegularShape.prototype,
+    'getRadius2',
+    ol.style.RegularShape.prototype.getRadius2);
+
+goog.exportProperty(
+    ol.style.RegularShape.prototype,
+    'getSize',
+    ol.style.RegularShape.prototype.getSize);
+
+goog.exportProperty(
+    ol.style.RegularShape.prototype,
+    'getStroke',
+    ol.style.RegularShape.prototype.getStroke);
+
+goog.exportSymbol(
+    'ol.style.Stroke',
+    ol.style.Stroke,
+    OPENLAYERS);
+
+goog.exportProperty(
+    ol.style.Stroke.prototype,
+    'getColor',
+    ol.style.Stroke.prototype.getColor);
+
+goog.exportProperty(
+    ol.style.Stroke.prototype,
+    'getLineCap',
+    ol.style.Stroke.prototype.getLineCap);
+
+goog.exportProperty(
+    ol.style.Stroke.prototype,
+    'getLineDash',
+    ol.style.Stroke.prototype.getLineDash);
+
+goog.exportProperty(
+    ol.style.Stroke.prototype,
+    'getLineJoin',
+    ol.style.Stroke.prototype.getLineJoin);
+
+goog.exportProperty(
+    ol.style.Stroke.prototype,
+    'getMiterLimit',
+    ol.style.Stroke.prototype.getMiterLimit);
+
+goog.exportProperty(
+    ol.style.Stroke.prototype,
+    'getWidth',
+    ol.style.Stroke.prototype.getWidth);
+
+goog.exportProperty(
+    ol.style.Stroke.prototype,
+    'setColor',
+    ol.style.Stroke.prototype.setColor);
+
+goog.exportProperty(
+    ol.style.Stroke.prototype,
+    'setLineCap',
+    ol.style.Stroke.prototype.setLineCap);
+
+goog.exportProperty(
+    ol.style.Stroke.prototype,
+    'setLineDash',
+    ol.style.Stroke.prototype.setLineDash);
+
+goog.exportProperty(
+    ol.style.Stroke.prototype,
+    'setLineJoin',
+    ol.style.Stroke.prototype.setLineJoin);
+
+goog.exportProperty(
+    ol.style.Stroke.prototype,
+    'setMiterLimit',
+    ol.style.Stroke.prototype.setMiterLimit);
+
+goog.exportProperty(
+    ol.style.Stroke.prototype,
+    'setWidth',
+    ol.style.Stroke.prototype.setWidth);
+
+goog.exportSymbol(
+    'ol.style.Style',
+    ol.style.Style,
+    OPENLAYERS);
+
+goog.exportProperty(
+    ol.style.Style.prototype,
+    'getGeometry',
+    ol.style.Style.prototype.getGeometry);
+
+goog.exportProperty(
+    ol.style.Style.prototype,
+    'getGeometryFunction',
+    ol.style.Style.prototype.getGeometryFunction);
+
+goog.exportProperty(
+    ol.style.Style.prototype,
+    'getFill',
+    ol.style.Style.prototype.getFill);
+
+goog.exportProperty(
+    ol.style.Style.prototype,
+    'getImage',
+    ol.style.Style.prototype.getImage);
+
+goog.exportProperty(
+    ol.style.Style.prototype,
+    'getStroke',
+    ol.style.Style.prototype.getStroke);
+
+goog.exportProperty(
+    ol.style.Style.prototype,
+    'getText',
+    ol.style.Style.prototype.getText);
+
+goog.exportProperty(
+    ol.style.Style.prototype,
+    'getZIndex',
+    ol.style.Style.prototype.getZIndex);
+
+goog.exportProperty(
+    ol.style.Style.prototype,
+    'setGeometry',
+    ol.style.Style.prototype.setGeometry);
+
+goog.exportProperty(
+    ol.style.Style.prototype,
+    'setZIndex',
+    ol.style.Style.prototype.setZIndex);
+
+goog.exportSymbol(
+    'ol.style.Text',
+    ol.style.Text,
+    OPENLAYERS);
+
+goog.exportProperty(
+    ol.style.Text.prototype,
+    'getFont',
+    ol.style.Text.prototype.getFont);
+
+goog.exportProperty(
+    ol.style.Text.prototype,
+    'getOffsetX',
+    ol.style.Text.prototype.getOffsetX);
+
+goog.exportProperty(
+    ol.style.Text.prototype,
+    'getOffsetY',
+    ol.style.Text.prototype.getOffsetY);
+
+goog.exportProperty(
+    ol.style.Text.prototype,
+    'getFill',
+    ol.style.Text.prototype.getFill);
+
+goog.exportProperty(
+    ol.style.Text.prototype,
+    'getRotation',
+    ol.style.Text.prototype.getRotation);
+
+goog.exportProperty(
+    ol.style.Text.prototype,
+    'getScale',
+    ol.style.Text.prototype.getScale);
+
+goog.exportProperty(
+    ol.style.Text.prototype,
+    'getStroke',
+    ol.style.Text.prototype.getStroke);
+
+goog.exportProperty(
+    ol.style.Text.prototype,
+    'getText',
+    ol.style.Text.prototype.getText);
+
+goog.exportProperty(
+    ol.style.Text.prototype,
+    'getTextAlign',
+    ol.style.Text.prototype.getTextAlign);
+
+goog.exportProperty(
+    ol.style.Text.prototype,
+    'getTextBaseline',
+    ol.style.Text.prototype.getTextBaseline);
+
+goog.exportProperty(
+    ol.style.Text.prototype,
+    'setFont',
+    ol.style.Text.prototype.setFont);
+
+goog.exportProperty(
+    ol.style.Text.prototype,
+    'setOffsetX',
+    ol.style.Text.prototype.setOffsetX);
+
+goog.exportProperty(
+    ol.style.Text.prototype,
+    'setOffsetY',
+    ol.style.Text.prototype.setOffsetY);
+
+goog.exportProperty(
+    ol.style.Text.prototype,
+    'setFill',
+    ol.style.Text.prototype.setFill);
+
+goog.exportProperty(
+    ol.style.Text.prototype,
+    'setRotation',
+    ol.style.Text.prototype.setRotation);
+
+goog.exportProperty(
+    ol.style.Text.prototype,
+    'setScale',
+    ol.style.Text.prototype.setScale);
+
+goog.exportProperty(
+    ol.style.Text.prototype,
+    'setStroke',
+    ol.style.Text.prototype.setStroke);
+
+goog.exportProperty(
+    ol.style.Text.prototype,
+    'setText',
+    ol.style.Text.prototype.setText);
+
+goog.exportProperty(
+    ol.style.Text.prototype,
+    'setTextAlign',
+    ol.style.Text.prototype.setTextAlign);
+
+goog.exportProperty(
+    ol.style.Text.prototype,
+    'setTextBaseline',
+    ol.style.Text.prototype.setTextBaseline);
+
+goog.exportSymbol(
+    'ol.Sphere',
+    ol.Sphere,
+    OPENLAYERS);
+
+goog.exportProperty(
+    ol.Sphere.prototype,
+    'geodesicArea',
+    ol.Sphere.prototype.geodesicArea);
+
+goog.exportProperty(
+    ol.Sphere.prototype,
+    'haversineDistance',
+    ol.Sphere.prototype.haversineDistance);
+
+goog.exportSymbol(
+    'ol.source.BingMaps',
+    ol.source.BingMaps,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.source.BingMaps.TOS_ATTRIBUTION',
+    ol.source.BingMaps.TOS_ATTRIBUTION,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.source.CartoDB',
+    ol.source.CartoDB,
+    OPENLAYERS);
+
+goog.exportProperty(
+    ol.source.CartoDB.prototype,
+    'getConfig',
+    ol.source.CartoDB.prototype.getConfig);
+
+goog.exportProperty(
+    ol.source.CartoDB.prototype,
+    'updateConfig',
+    ol.source.CartoDB.prototype.updateConfig);
+
+goog.exportProperty(
+    ol.source.CartoDB.prototype,
+    'setConfig',
+    ol.source.CartoDB.prototype.setConfig);
+
+goog.exportSymbol(
+    'ol.source.Cluster',
+    ol.source.Cluster,
+    OPENLAYERS);
+
+goog.exportProperty(
+    ol.source.Cluster.prototype,
+    'getSource',
+    ol.source.Cluster.prototype.getSource);
+
+goog.exportSymbol(
+    'ol.source.ImageArcGISRest',
+    ol.source.ImageArcGISRest,
+    OPENLAYERS);
+
+goog.exportProperty(
+    ol.source.ImageArcGISRest.prototype,
+    'getParams',
+    ol.source.ImageArcGISRest.prototype.getParams);
+
+goog.exportProperty(
+    ol.source.ImageArcGISRest.prototype,
+    'getImageLoadFunction',
+    ol.source.ImageArcGISRest.prototype.getImageLoadFunction);
+
+goog.exportProperty(
+    ol.source.ImageArcGISRest.prototype,
+    'getUrl',
+    ol.source.ImageArcGISRest.prototype.getUrl);
+
+goog.exportProperty(
+    ol.source.ImageArcGISRest.prototype,
+    'setImageLoadFunction',
+    ol.source.ImageArcGISRest.prototype.setImageLoadFunction);
+
+goog.exportProperty(
+    ol.source.ImageArcGISRest.prototype,
+    'setUrl',
+    ol.source.ImageArcGISRest.prototype.setUrl);
+
+goog.exportProperty(
+    ol.source.ImageArcGISRest.prototype,
+    'updateParams',
+    ol.source.ImageArcGISRest.prototype.updateParams);
+
+goog.exportSymbol(
+    'ol.source.ImageCanvas',
+    ol.source.ImageCanvas,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.source.ImageMapGuide',
+    ol.source.ImageMapGuide,
+    OPENLAYERS);
+
+goog.exportProperty(
+    ol.source.ImageMapGuide.prototype,
+    'getParams',
+    ol.source.ImageMapGuide.prototype.getParams);
+
+goog.exportProperty(
+    ol.source.ImageMapGuide.prototype,
+    'getImageLoadFunction',
+    ol.source.ImageMapGuide.prototype.getImageLoadFunction);
+
+goog.exportProperty(
+    ol.source.ImageMapGuide.prototype,
+    'updateParams',
+    ol.source.ImageMapGuide.prototype.updateParams);
+
+goog.exportProperty(
+    ol.source.ImageMapGuide.prototype,
+    'setImageLoadFunction',
+    ol.source.ImageMapGuide.prototype.setImageLoadFunction);
+
+goog.exportSymbol(
+    'ol.source.Image',
+    ol.source.Image,
+    OPENLAYERS);
+
+goog.exportProperty(
+    ol.source.ImageEvent.prototype,
+    'image',
+    ol.source.ImageEvent.prototype.image);
+
+goog.exportSymbol(
+    'ol.source.ImageStatic',
+    ol.source.ImageStatic,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.source.ImageVector',
+    ol.source.ImageVector,
+    OPENLAYERS);
+
+goog.exportProperty(
+    ol.source.ImageVector.prototype,
+    'getSource',
+    ol.source.ImageVector.prototype.getSource);
+
+goog.exportProperty(
+    ol.source.ImageVector.prototype,
+    'getStyle',
+    ol.source.ImageVector.prototype.getStyle);
+
+goog.exportProperty(
+    ol.source.ImageVector.prototype,
+    'getStyleFunction',
+    ol.source.ImageVector.prototype.getStyleFunction);
+
+goog.exportProperty(
+    ol.source.ImageVector.prototype,
+    'setStyle',
+    ol.source.ImageVector.prototype.setStyle);
+
+goog.exportSymbol(
+    'ol.source.ImageWMS',
+    ol.source.ImageWMS,
+    OPENLAYERS);
+
+goog.exportProperty(
+    ol.source.ImageWMS.prototype,
+    'getGetFeatureInfoUrl',
+    ol.source.ImageWMS.prototype.getGetFeatureInfoUrl);
+
+goog.exportProperty(
+    ol.source.ImageWMS.prototype,
+    'getParams',
+    ol.source.ImageWMS.prototype.getParams);
+
+goog.exportProperty(
+    ol.source.ImageWMS.prototype,
+    'getImageLoadFunction',
+    ol.source.ImageWMS.prototype.getImageLoadFunction);
+
+goog.exportProperty(
+    ol.source.ImageWMS.prototype,
+    'getUrl',
+    ol.source.ImageWMS.prototype.getUrl);
+
+goog.exportProperty(
+    ol.source.ImageWMS.prototype,
+    'setImageLoadFunction',
+    ol.source.ImageWMS.prototype.setImageLoadFunction);
+
+goog.exportProperty(
+    ol.source.ImageWMS.prototype,
+    'setUrl',
+    ol.source.ImageWMS.prototype.setUrl);
+
+goog.exportProperty(
+    ol.source.ImageWMS.prototype,
+    'updateParams',
+    ol.source.ImageWMS.prototype.updateParams);
+
+goog.exportSymbol(
+    'ol.source.OSM',
+    ol.source.OSM,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.source.OSM.ATTRIBUTION',
+    ol.source.OSM.ATTRIBUTION,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.source.Raster',
+    ol.source.Raster,
+    OPENLAYERS);
+
+goog.exportProperty(
+    ol.source.Raster.prototype,
+    'setOperation',
+    ol.source.Raster.prototype.setOperation);
+
+goog.exportProperty(
+    ol.source.RasterEvent.prototype,
+    'extent',
+    ol.source.RasterEvent.prototype.extent);
+
+goog.exportProperty(
+    ol.source.RasterEvent.prototype,
+    'resolution',
+    ol.source.RasterEvent.prototype.resolution);
+
+goog.exportProperty(
+    ol.source.RasterEvent.prototype,
+    'data',
+    ol.source.RasterEvent.prototype.data);
+
+goog.exportSymbol(
+    'ol.source.Source',
+    ol.source.Source,
+    OPENLAYERS);
+
+goog.exportProperty(
+    ol.source.Source.prototype,
+    'getAttributions',
+    ol.source.Source.prototype.getAttributions);
+
+goog.exportProperty(
+    ol.source.Source.prototype,
+    'getLogo',
+    ol.source.Source.prototype.getLogo);
+
+goog.exportProperty(
+    ol.source.Source.prototype,
+    'getProjection',
+    ol.source.Source.prototype.getProjection);
+
+goog.exportProperty(
+    ol.source.Source.prototype,
+    'getState',
+    ol.source.Source.prototype.getState);
+
+goog.exportProperty(
+    ol.source.Source.prototype,
+    'refresh',
+    ol.source.Source.prototype.refresh);
+
+goog.exportProperty(
+    ol.source.Source.prototype,
+    'setAttributions',
+    ol.source.Source.prototype.setAttributions);
+
+goog.exportSymbol(
+    'ol.source.Stamen',
+    ol.source.Stamen,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.source.TileArcGISRest',
+    ol.source.TileArcGISRest,
+    OPENLAYERS);
+
+goog.exportProperty(
+    ol.source.TileArcGISRest.prototype,
+    'getParams',
+    ol.source.TileArcGISRest.prototype.getParams);
+
+goog.exportProperty(
+    ol.source.TileArcGISRest.prototype,
+    'updateParams',
+    ol.source.TileArcGISRest.prototype.updateParams);
+
+goog.exportSymbol(
+    'ol.source.TileDebug',
+    ol.source.TileDebug,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.source.TileImage',
+    ol.source.TileImage,
+    OPENLAYERS);
+
+goog.exportProperty(
+    ol.source.TileImage.prototype,
+    'setRenderReprojectionEdges',
+    ol.source.TileImage.prototype.setRenderReprojectionEdges);
+
+goog.exportProperty(
+    ol.source.TileImage.prototype,
+    'setTileGridForProjection',
+    ol.source.TileImage.prototype.setTileGridForProjection);
+
+goog.exportSymbol(
+    'ol.source.TileJSON',
+    ol.source.TileJSON,
+    OPENLAYERS);
+
+goog.exportProperty(
+    ol.source.TileJSON.prototype,
+    'getTileJSON',
+    ol.source.TileJSON.prototype.getTileJSON);
+
+goog.exportSymbol(
+    'ol.source.Tile',
+    ol.source.Tile,
+    OPENLAYERS);
+
+goog.exportProperty(
+    ol.source.Tile.prototype,
+    'getTileGrid',
+    ol.source.Tile.prototype.getTileGrid);
+
+goog.exportProperty(
+    ol.source.TileEvent.prototype,
+    'tile',
+    ol.source.TileEvent.prototype.tile);
+
+goog.exportSymbol(
+    'ol.source.TileUTFGrid',
+    ol.source.TileUTFGrid,
+    OPENLAYERS);
+
+goog.exportProperty(
+    ol.source.TileUTFGrid.prototype,
+    'getTemplate',
+    ol.source.TileUTFGrid.prototype.getTemplate);
+
+goog.exportProperty(
+    ol.source.TileUTFGrid.prototype,
+    'forDataAtCoordinateAndResolution',
+    ol.source.TileUTFGrid.prototype.forDataAtCoordinateAndResolution);
+
+goog.exportSymbol(
+    'ol.source.TileWMS',
+    ol.source.TileWMS,
+    OPENLAYERS);
+
+goog.exportProperty(
+    ol.source.TileWMS.prototype,
+    'getGetFeatureInfoUrl',
+    ol.source.TileWMS.prototype.getGetFeatureInfoUrl);
+
+goog.exportProperty(
+    ol.source.TileWMS.prototype,
+    'getParams',
+    ol.source.TileWMS.prototype.getParams);
+
+goog.exportProperty(
+    ol.source.TileWMS.prototype,
+    'updateParams',
+    ol.source.TileWMS.prototype.updateParams);
+
+goog.exportProperty(
+    ol.source.UrlTile.prototype,
+    'getTileLoadFunction',
+    ol.source.UrlTile.prototype.getTileLoadFunction);
+
+goog.exportProperty(
+    ol.source.UrlTile.prototype,
+    'getTileUrlFunction',
+    ol.source.UrlTile.prototype.getTileUrlFunction);
+
+goog.exportProperty(
+    ol.source.UrlTile.prototype,
+    'getUrls',
+    ol.source.UrlTile.prototype.getUrls);
+
+goog.exportProperty(
+    ol.source.UrlTile.prototype,
+    'setTileLoadFunction',
+    ol.source.UrlTile.prototype.setTileLoadFunction);
+
+goog.exportProperty(
+    ol.source.UrlTile.prototype,
+    'setTileUrlFunction',
+    ol.source.UrlTile.prototype.setTileUrlFunction);
+
+goog.exportProperty(
+    ol.source.UrlTile.prototype,
+    'setUrl',
+    ol.source.UrlTile.prototype.setUrl);
+
+goog.exportProperty(
+    ol.source.UrlTile.prototype,
+    'setUrls',
+    ol.source.UrlTile.prototype.setUrls);
+
+goog.exportSymbol(
+    'ol.source.Vector',
+    ol.source.Vector,
+    OPENLAYERS);
+
+goog.exportProperty(
+    ol.source.Vector.prototype,
+    'addFeature',
+    ol.source.Vector.prototype.addFeature);
+
+goog.exportProperty(
+    ol.source.Vector.prototype,
+    'addFeatures',
+    ol.source.Vector.prototype.addFeatures);
+
+goog.exportProperty(
+    ol.source.Vector.prototype,
+    'clear',
+    ol.source.Vector.prototype.clear);
+
+goog.exportProperty(
+    ol.source.Vector.prototype,
+    'forEachFeature',
+    ol.source.Vector.prototype.forEachFeature);
+
+goog.exportProperty(
+    ol.source.Vector.prototype,
+    'forEachFeatureInExtent',
+    ol.source.Vector.prototype.forEachFeatureInExtent);
+
+goog.exportProperty(
+    ol.source.Vector.prototype,
+    'forEachFeatureIntersectingExtent',
+    ol.source.Vector.prototype.forEachFeatureIntersectingExtent);
+
+goog.exportProperty(
+    ol.source.Vector.prototype,
+    'getFeaturesCollection',
+    ol.source.Vector.prototype.getFeaturesCollection);
+
+goog.exportProperty(
+    ol.source.Vector.prototype,
+    'getFeatures',
+    ol.source.Vector.prototype.getFeatures);
+
+goog.exportProperty(
+    ol.source.Vector.prototype,
+    'getFeaturesAtCoordinate',
+    ol.source.Vector.prototype.getFeaturesAtCoordinate);
+
+goog.exportProperty(
+    ol.source.Vector.prototype,
+    'getFeaturesInExtent',
+    ol.source.Vector.prototype.getFeaturesInExtent);
+
+goog.exportProperty(
+    ol.source.Vector.prototype,
+    'getClosestFeatureToCoordinate',
+    ol.source.Vector.prototype.getClosestFeatureToCoordinate);
+
+goog.exportProperty(
+    ol.source.Vector.prototype,
+    'getExtent',
+    ol.source.Vector.prototype.getExtent);
+
+goog.exportProperty(
+    ol.source.Vector.prototype,
+    'getFeatureById',
+    ol.source.Vector.prototype.getFeatureById);
+
+goog.exportProperty(
+    ol.source.Vector.prototype,
+    'getFormat',
+    ol.source.Vector.prototype.getFormat);
+
+goog.exportProperty(
+    ol.source.Vector.prototype,
+    'getUrl',
+    ol.source.Vector.prototype.getUrl);
+
+goog.exportProperty(
+    ol.source.Vector.prototype,
+    'removeFeature',
+    ol.source.Vector.prototype.removeFeature);
+
+goog.exportProperty(
+    ol.source.VectorEvent.prototype,
+    'feature',
+    ol.source.VectorEvent.prototype.feature);
+
+goog.exportSymbol(
+    'ol.source.VectorTile',
+    ol.source.VectorTile,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.source.WMTS',
+    ol.source.WMTS,
+    OPENLAYERS);
+
+goog.exportProperty(
+    ol.source.WMTS.prototype,
+    'getDimensions',
+    ol.source.WMTS.prototype.getDimensions);
+
+goog.exportProperty(
+    ol.source.WMTS.prototype,
+    'getFormat',
+    ol.source.WMTS.prototype.getFormat);
+
+goog.exportProperty(
+    ol.source.WMTS.prototype,
+    'getLayer',
+    ol.source.WMTS.prototype.getLayer);
+
+goog.exportProperty(
+    ol.source.WMTS.prototype,
+    'getMatrixSet',
+    ol.source.WMTS.prototype.getMatrixSet);
+
+goog.exportProperty(
+    ol.source.WMTS.prototype,
+    'getRequestEncoding',
+    ol.source.WMTS.prototype.getRequestEncoding);
+
+goog.exportProperty(
+    ol.source.WMTS.prototype,
+    'getStyle',
+    ol.source.WMTS.prototype.getStyle);
+
+goog.exportProperty(
+    ol.source.WMTS.prototype,
+    'getVersion',
+    ol.source.WMTS.prototype.getVersion);
+
+goog.exportProperty(
+    ol.source.WMTS.prototype,
+    'updateDimensions',
+    ol.source.WMTS.prototype.updateDimensions);
+
+goog.exportSymbol(
+    'ol.source.WMTS.optionsFromCapabilities',
+    ol.source.WMTS.optionsFromCapabilities,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.source.XYZ',
+    ol.source.XYZ,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.source.Zoomify',
+    ol.source.Zoomify,
+    OPENLAYERS);
+
+goog.exportProperty(
+    ol.render.Event.prototype,
+    'vectorContext',
+    ol.render.Event.prototype.vectorContext);
+
+goog.exportProperty(
+    ol.render.Event.prototype,
+    'frameState',
+    ol.render.Event.prototype.frameState);
+
+goog.exportProperty(
+    ol.render.Event.prototype,
+    'context',
+    ol.render.Event.prototype.context);
+
+goog.exportProperty(
+    ol.render.Event.prototype,
+    'glContext',
+    ol.render.Event.prototype.glContext);
+
+goog.exportProperty(
+    ol.render.Feature.prototype,
+    'get',
+    ol.render.Feature.prototype.get);
+
+goog.exportProperty(
+    ol.render.Feature.prototype,
+    'getExtent',
+    ol.render.Feature.prototype.getExtent);
+
+goog.exportProperty(
+    ol.render.Feature.prototype,
+    'getGeometry',
+    ol.render.Feature.prototype.getGeometry);
+
+goog.exportProperty(
+    ol.render.Feature.prototype,
+    'getProperties',
+    ol.render.Feature.prototype.getProperties);
+
+goog.exportProperty(
+    ol.render.Feature.prototype,
+    'getType',
+    ol.render.Feature.prototype.getType);
+
+goog.exportSymbol(
+    'ol.render.VectorContext',
+    ol.render.VectorContext,
+    OPENLAYERS);
+
+goog.exportProperty(
+    ol.render.webgl.Immediate.prototype,
+    'setStyle',
+    ol.render.webgl.Immediate.prototype.setStyle);
+
+goog.exportProperty(
+    ol.render.webgl.Immediate.prototype,
+    'drawGeometry',
+    ol.render.webgl.Immediate.prototype.drawGeometry);
+
+goog.exportProperty(
+    ol.render.webgl.Immediate.prototype,
+    'drawFeature',
+    ol.render.webgl.Immediate.prototype.drawFeature);
+
+goog.exportProperty(
+    ol.render.canvas.Immediate.prototype,
+    'drawCircle',
+    ol.render.canvas.Immediate.prototype.drawCircle);
+
+goog.exportProperty(
+    ol.render.canvas.Immediate.prototype,
+    'setStyle',
+    ol.render.canvas.Immediate.prototype.setStyle);
+
+goog.exportProperty(
+    ol.render.canvas.Immediate.prototype,
+    'drawGeometry',
+    ol.render.canvas.Immediate.prototype.drawGeometry);
+
+goog.exportProperty(
+    ol.render.canvas.Immediate.prototype,
+    'drawFeature',
+    ol.render.canvas.Immediate.prototype.drawFeature);
+
+goog.exportSymbol(
+    'ol.proj.common.add',
+    ol.proj.common.add,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.proj.METERS_PER_UNIT',
+    ol.proj.METERS_PER_UNIT,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.proj.Projection',
+    ol.proj.Projection,
+    OPENLAYERS);
+
+goog.exportProperty(
+    ol.proj.Projection.prototype,
+    'getCode',
+    ol.proj.Projection.prototype.getCode);
+
+goog.exportProperty(
+    ol.proj.Projection.prototype,
+    'getExtent',
+    ol.proj.Projection.prototype.getExtent);
+
+goog.exportProperty(
+    ol.proj.Projection.prototype,
+    'getUnits',
+    ol.proj.Projection.prototype.getUnits);
+
+goog.exportProperty(
+    ol.proj.Projection.prototype,
+    'getMetersPerUnit',
+    ol.proj.Projection.prototype.getMetersPerUnit);
+
+goog.exportProperty(
+    ol.proj.Projection.prototype,
+    'getWorldExtent',
+    ol.proj.Projection.prototype.getWorldExtent);
+
+goog.exportProperty(
+    ol.proj.Projection.prototype,
+    'isGlobal',
+    ol.proj.Projection.prototype.isGlobal);
+
+goog.exportProperty(
+    ol.proj.Projection.prototype,
+    'setGlobal',
+    ol.proj.Projection.prototype.setGlobal);
+
+goog.exportProperty(
+    ol.proj.Projection.prototype,
+    'setExtent',
+    ol.proj.Projection.prototype.setExtent);
+
+goog.exportProperty(
+    ol.proj.Projection.prototype,
+    'setWorldExtent',
+    ol.proj.Projection.prototype.setWorldExtent);
+
+goog.exportProperty(
+    ol.proj.Projection.prototype,
+    'setGetPointResolution',
+    ol.proj.Projection.prototype.setGetPointResolution);
+
+goog.exportProperty(
+    ol.proj.Projection.prototype,
+    'getPointResolution',
+    ol.proj.Projection.prototype.getPointResolution);
+
+goog.exportSymbol(
+    'ol.proj.setProj4',
+    ol.proj.setProj4,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.proj.addEquivalentProjections',
+    ol.proj.addEquivalentProjections,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.proj.addProjection',
+    ol.proj.addProjection,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.proj.addCoordinateTransforms',
+    ol.proj.addCoordinateTransforms,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.proj.fromLonLat',
+    ol.proj.fromLonLat,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.proj.toLonLat',
+    ol.proj.toLonLat,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.proj.get',
+    ol.proj.get,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.proj.equivalent',
+    ol.proj.equivalent,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.proj.getTransform',
+    ol.proj.getTransform,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.proj.transform',
+    ol.proj.transform,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.proj.transformExtent',
+    ol.proj.transformExtent,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.layer.Heatmap',
+    ol.layer.Heatmap,
+    OPENLAYERS);
+
+goog.exportProperty(
+    ol.layer.Heatmap.prototype,
+    'getBlur',
+    ol.layer.Heatmap.prototype.getBlur);
+
+goog.exportProperty(
+    ol.layer.Heatmap.prototype,
+    'getGradient',
+    ol.layer.Heatmap.prototype.getGradient);
+
+goog.exportProperty(
+    ol.layer.Heatmap.prototype,
+    'getRadius',
+    ol.layer.Heatmap.prototype.getRadius);
+
+goog.exportProperty(
+    ol.layer.Heatmap.prototype,
+    'setBlur',
+    ol.layer.Heatmap.prototype.setBlur);
+
+goog.exportProperty(
+    ol.layer.Heatmap.prototype,
+    'setGradient',
+    ol.layer.Heatmap.prototype.setGradient);
+
+goog.exportProperty(
+    ol.layer.Heatmap.prototype,
+    'setRadius',
+    ol.layer.Heatmap.prototype.setRadius);
+
+goog.exportSymbol(
+    'ol.layer.Image',
+    ol.layer.Image,
+    OPENLAYERS);
+
+goog.exportProperty(
+    ol.layer.Image.prototype,
+    'getSource',
+    ol.layer.Image.prototype.getSource);
+
+goog.exportSymbol(
+    'ol.layer.Layer',
+    ol.layer.Layer,
+    OPENLAYERS);
+
+goog.exportProperty(
+    ol.layer.Layer.prototype,
+    'getSource',
+    ol.layer.Layer.prototype.getSource);
+
+goog.exportProperty(
+    ol.layer.Layer.prototype,
+    'setMap',
+    ol.layer.Layer.prototype.setMap);
+
+goog.exportProperty(
+    ol.layer.Layer.prototype,
+    'setSource',
+    ol.layer.Layer.prototype.setSource);
+
+goog.exportSymbol(
+    'ol.layer.Base',
+    ol.layer.Base,
+    OPENLAYERS);
+
+goog.exportProperty(
+    ol.layer.Base.prototype,
+    'getExtent',
+    ol.layer.Base.prototype.getExtent);
+
+goog.exportProperty(
+    ol.layer.Base.prototype,
+    'getMaxResolution',
+    ol.layer.Base.prototype.getMaxResolution);
+
+goog.exportProperty(
+    ol.layer.Base.prototype,
+    'getMinResolution',
+    ol.layer.Base.prototype.getMinResolution);
+
+goog.exportProperty(
+    ol.layer.Base.prototype,
+    'getOpacity',
+    ol.layer.Base.prototype.getOpacity);
+
+goog.exportProperty(
+    ol.layer.Base.prototype,
+    'getVisible',
+    ol.layer.Base.prototype.getVisible);
+
+goog.exportProperty(
+    ol.layer.Base.prototype,
+    'getZIndex',
+    ol.layer.Base.prototype.getZIndex);
+
+goog.exportProperty(
+    ol.layer.Base.prototype,
+    'setExtent',
+    ol.layer.Base.prototype.setExtent);
+
+goog.exportProperty(
+    ol.layer.Base.prototype,
+    'setMaxResolution',
+    ol.layer.Base.prototype.setMaxResolution);
+
+goog.exportProperty(
+    ol.layer.Base.prototype,
+    'setMinResolution',
+    ol.layer.Base.prototype.setMinResolution);
+
+goog.exportProperty(
+    ol.layer.Base.prototype,
+    'setOpacity',
+    ol.layer.Base.prototype.setOpacity);
+
+goog.exportProperty(
+    ol.layer.Base.prototype,
+    'setVisible',
+    ol.layer.Base.prototype.setVisible);
+
+goog.exportProperty(
+    ol.layer.Base.prototype,
+    'setZIndex',
+    ol.layer.Base.prototype.setZIndex);
+
+goog.exportSymbol(
+    'ol.layer.Group',
+    ol.layer.Group,
+    OPENLAYERS);
+
+goog.exportProperty(
+    ol.layer.Group.prototype,
+    'getLayers',
+    ol.layer.Group.prototype.getLayers);
+
+goog.exportProperty(
+    ol.layer.Group.prototype,
+    'setLayers',
+    ol.layer.Group.prototype.setLayers);
+
+goog.exportSymbol(
+    'ol.layer.Tile',
+    ol.layer.Tile,
+    OPENLAYERS);
+
+goog.exportProperty(
+    ol.layer.Tile.prototype,
+    'getPreload',
+    ol.layer.Tile.prototype.getPreload);
+
+goog.exportProperty(
+    ol.layer.Tile.prototype,
+    'getSource',
+    ol.layer.Tile.prototype.getSource);
+
+goog.exportProperty(
+    ol.layer.Tile.prototype,
+    'setPreload',
+    ol.layer.Tile.prototype.setPreload);
+
+goog.exportProperty(
+    ol.layer.Tile.prototype,
+    'getUseInterimTilesOnError',
+    ol.layer.Tile.prototype.getUseInterimTilesOnError);
+
+goog.exportProperty(
+    ol.layer.Tile.prototype,
+    'setUseInterimTilesOnError',
+    ol.layer.Tile.prototype.setUseInterimTilesOnError);
+
+goog.exportSymbol(
+    'ol.layer.Vector',
+    ol.layer.Vector,
+    OPENLAYERS);
+
+goog.exportProperty(
+    ol.layer.Vector.prototype,
+    'getSource',
+    ol.layer.Vector.prototype.getSource);
+
+goog.exportProperty(
+    ol.layer.Vector.prototype,
+    'getStyle',
+    ol.layer.Vector.prototype.getStyle);
+
+goog.exportProperty(
+    ol.layer.Vector.prototype,
+    'getStyleFunction',
+    ol.layer.Vector.prototype.getStyleFunction);
+
+goog.exportProperty(
+    ol.layer.Vector.prototype,
+    'setStyle',
+    ol.layer.Vector.prototype.setStyle);
+
+goog.exportSymbol(
+    'ol.layer.VectorTile',
+    ol.layer.VectorTile,
+    OPENLAYERS);
+
+goog.exportProperty(
+    ol.layer.VectorTile.prototype,
+    'getPreload',
+    ol.layer.VectorTile.prototype.getPreload);
+
+goog.exportProperty(
+    ol.layer.VectorTile.prototype,
+    'getUseInterimTilesOnError',
+    ol.layer.VectorTile.prototype.getUseInterimTilesOnError);
+
+goog.exportProperty(
+    ol.layer.VectorTile.prototype,
+    'setPreload',
+    ol.layer.VectorTile.prototype.setPreload);
+
+goog.exportProperty(
+    ol.layer.VectorTile.prototype,
+    'setUseInterimTilesOnError',
+    ol.layer.VectorTile.prototype.setUseInterimTilesOnError);
+
+goog.exportSymbol(
+    'ol.interaction.DoubleClickZoom',
+    ol.interaction.DoubleClickZoom,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.interaction.DoubleClickZoom.handleEvent',
+    ol.interaction.DoubleClickZoom.handleEvent,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.interaction.DragAndDrop',
+    ol.interaction.DragAndDrop,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.interaction.DragAndDrop.handleEvent',
+    ol.interaction.DragAndDrop.handleEvent,
+    OPENLAYERS);
+
+goog.exportProperty(
+    ol.interaction.DragAndDropEvent.prototype,
+    'features',
+    ol.interaction.DragAndDropEvent.prototype.features);
+
+goog.exportProperty(
+    ol.interaction.DragAndDropEvent.prototype,
+    'file',
+    ol.interaction.DragAndDropEvent.prototype.file);
+
+goog.exportProperty(
+    ol.interaction.DragAndDropEvent.prototype,
+    'projection',
+    ol.interaction.DragAndDropEvent.prototype.projection);
+
+goog.exportProperty(
+    ol.DragBoxEvent.prototype,
+    'coordinate',
+    ol.DragBoxEvent.prototype.coordinate);
+
+goog.exportProperty(
+    ol.DragBoxEvent.prototype,
+    'mapBrowserEvent',
+    ol.DragBoxEvent.prototype.mapBrowserEvent);
+
+goog.exportSymbol(
+    'ol.interaction.DragBox',
+    ol.interaction.DragBox,
+    OPENLAYERS);
+
+goog.exportProperty(
+    ol.interaction.DragBox.prototype,
+    'getGeometry',
+    ol.interaction.DragBox.prototype.getGeometry);
+
+goog.exportSymbol(
+    'ol.interaction.DragPan',
+    ol.interaction.DragPan,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.interaction.DragRotateAndZoom',
+    ol.interaction.DragRotateAndZoom,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.interaction.DragRotate',
+    ol.interaction.DragRotate,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.interaction.DragZoom',
+    ol.interaction.DragZoom,
+    OPENLAYERS);
+
+goog.exportProperty(
+    ol.interaction.DrawEvent.prototype,
+    'feature',
+    ol.interaction.DrawEvent.prototype.feature);
+
+goog.exportSymbol(
+    'ol.interaction.Draw',
+    ol.interaction.Draw,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.interaction.Draw.handleEvent',
+    ol.interaction.Draw.handleEvent,
+    OPENLAYERS);
+
+goog.exportProperty(
+    ol.interaction.Draw.prototype,
+    'removeLastPoint',
+    ol.interaction.Draw.prototype.removeLastPoint);
+
+goog.exportProperty(
+    ol.interaction.Draw.prototype,
+    'finishDrawing',
+    ol.interaction.Draw.prototype.finishDrawing);
+
+goog.exportProperty(
+    ol.interaction.Draw.prototype,
+    'extend',
+    ol.interaction.Draw.prototype.extend);
+
+goog.exportSymbol(
+    'ol.interaction.Draw.createRegularPolygon',
+    ol.interaction.Draw.createRegularPolygon,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.interaction.Interaction',
+    ol.interaction.Interaction,
+    OPENLAYERS);
+
+goog.exportProperty(
+    ol.interaction.Interaction.prototype,
+    'getActive',
+    ol.interaction.Interaction.prototype.getActive);
+
+goog.exportProperty(
+    ol.interaction.Interaction.prototype,
+    'getMap',
+    ol.interaction.Interaction.prototype.getMap);
+
+goog.exportProperty(
+    ol.interaction.Interaction.prototype,
+    'setActive',
+    ol.interaction.Interaction.prototype.setActive);
+
+goog.exportSymbol(
+    'ol.interaction.defaults',
+    ol.interaction.defaults,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.interaction.KeyboardPan',
+    ol.interaction.KeyboardPan,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.interaction.KeyboardPan.handleEvent',
+    ol.interaction.KeyboardPan.handleEvent,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.interaction.KeyboardZoom',
+    ol.interaction.KeyboardZoom,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.interaction.KeyboardZoom.handleEvent',
+    ol.interaction.KeyboardZoom.handleEvent,
+    OPENLAYERS);
+
+goog.exportProperty(
+    ol.interaction.ModifyEvent.prototype,
+    'features',
+    ol.interaction.ModifyEvent.prototype.features);
+
+goog.exportProperty(
+    ol.interaction.ModifyEvent.prototype,
+    'mapBrowserEvent',
+    ol.interaction.ModifyEvent.prototype.mapBrowserEvent);
+
+goog.exportSymbol(
+    'ol.interaction.Modify',
+    ol.interaction.Modify,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.interaction.Modify.handleEvent',
+    ol.interaction.Modify.handleEvent,
+    OPENLAYERS);
+
+goog.exportProperty(
+    ol.interaction.Modify.prototype,
+    'removePoint',
+    ol.interaction.Modify.prototype.removePoint);
+
+goog.exportSymbol(
+    'ol.interaction.MouseWheelZoom',
+    ol.interaction.MouseWheelZoom,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.interaction.MouseWheelZoom.handleEvent',
+    ol.interaction.MouseWheelZoom.handleEvent,
+    OPENLAYERS);
+
+goog.exportProperty(
+    ol.interaction.MouseWheelZoom.prototype,
+    'setMouseAnchor',
+    ol.interaction.MouseWheelZoom.prototype.setMouseAnchor);
+
+goog.exportSymbol(
+    'ol.interaction.PinchRotate',
+    ol.interaction.PinchRotate,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.interaction.PinchZoom',
+    ol.interaction.PinchZoom,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.interaction.Pointer',
+    ol.interaction.Pointer,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.interaction.Pointer.handleEvent',
+    ol.interaction.Pointer.handleEvent,
+    OPENLAYERS);
+
+goog.exportProperty(
+    ol.interaction.SelectEvent.prototype,
+    'selected',
+    ol.interaction.SelectEvent.prototype.selected);
+
+goog.exportProperty(
+    ol.interaction.SelectEvent.prototype,
+    'deselected',
+    ol.interaction.SelectEvent.prototype.deselected);
+
+goog.exportProperty(
+    ol.interaction.SelectEvent.prototype,
+    'mapBrowserEvent',
+    ol.interaction.SelectEvent.prototype.mapBrowserEvent);
+
+goog.exportSymbol(
+    'ol.interaction.Select',
+    ol.interaction.Select,
+    OPENLAYERS);
+
+goog.exportProperty(
+    ol.interaction.Select.prototype,
+    'getFeatures',
+    ol.interaction.Select.prototype.getFeatures);
+
+goog.exportProperty(
+    ol.interaction.Select.prototype,
+    'getLayer',
+    ol.interaction.Select.prototype.getLayer);
+
+goog.exportSymbol(
+    'ol.interaction.Select.handleEvent',
+    ol.interaction.Select.handleEvent,
+    OPENLAYERS);
+
+goog.exportProperty(
+    ol.interaction.Select.prototype,
+    'setMap',
+    ol.interaction.Select.prototype.setMap);
+
+goog.exportSymbol(
+    'ol.interaction.Snap',
+    ol.interaction.Snap,
+    OPENLAYERS);
+
+goog.exportProperty(
+    ol.interaction.Snap.prototype,
+    'addFeature',
+    ol.interaction.Snap.prototype.addFeature);
+
+goog.exportProperty(
+    ol.interaction.Snap.prototype,
+    'removeFeature',
+    ol.interaction.Snap.prototype.removeFeature);
+
+goog.exportProperty(
+    ol.interaction.TranslateEvent.prototype,
+    'features',
+    ol.interaction.TranslateEvent.prototype.features);
+
+goog.exportProperty(
+    ol.interaction.TranslateEvent.prototype,
+    'coordinate',
+    ol.interaction.TranslateEvent.prototype.coordinate);
+
+goog.exportSymbol(
+    'ol.interaction.Translate',
+    ol.interaction.Translate,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.geom.Circle',
+    ol.geom.Circle,
+    OPENLAYERS);
+
+goog.exportProperty(
+    ol.geom.Circle.prototype,
+    'clone',
+    ol.geom.Circle.prototype.clone);
+
+goog.exportProperty(
+    ol.geom.Circle.prototype,
+    'getCenter',
+    ol.geom.Circle.prototype.getCenter);
+
+goog.exportProperty(
+    ol.geom.Circle.prototype,
+    'getRadius',
+    ol.geom.Circle.prototype.getRadius);
+
+goog.exportProperty(
+    ol.geom.Circle.prototype,
+    'getType',
+    ol.geom.Circle.prototype.getType);
+
+goog.exportProperty(
+    ol.geom.Circle.prototype,
+    'intersectsExtent',
+    ol.geom.Circle.prototype.intersectsExtent);
+
+goog.exportProperty(
+    ol.geom.Circle.prototype,
+    'setCenter',
+    ol.geom.Circle.prototype.setCenter);
+
+goog.exportProperty(
+    ol.geom.Circle.prototype,
+    'setCenterAndRadius',
+    ol.geom.Circle.prototype.setCenterAndRadius);
+
+goog.exportProperty(
+    ol.geom.Circle.prototype,
+    'setRadius',
+    ol.geom.Circle.prototype.setRadius);
+
+goog.exportProperty(
+    ol.geom.Circle.prototype,
+    'transform',
+    ol.geom.Circle.prototype.transform);
+
+goog.exportSymbol(
+    'ol.geom.Geometry',
+    ol.geom.Geometry,
+    OPENLAYERS);
+
+goog.exportProperty(
+    ol.geom.Geometry.prototype,
+    'getClosestPoint',
+    ol.geom.Geometry.prototype.getClosestPoint);
+
+goog.exportProperty(
+    ol.geom.Geometry.prototype,
+    'getExtent',
+    ol.geom.Geometry.prototype.getExtent);
+
+goog.exportProperty(
+    ol.geom.Geometry.prototype,
+    'rotate',
+    ol.geom.Geometry.prototype.rotate);
+
+goog.exportProperty(
+    ol.geom.Geometry.prototype,
+    'simplify',
+    ol.geom.Geometry.prototype.simplify);
+
+goog.exportProperty(
+    ol.geom.Geometry.prototype,
+    'transform',
+    ol.geom.Geometry.prototype.transform);
+
+goog.exportSymbol(
+    'ol.geom.GeometryCollection',
+    ol.geom.GeometryCollection,
+    OPENLAYERS);
+
+goog.exportProperty(
+    ol.geom.GeometryCollection.prototype,
+    'clone',
+    ol.geom.GeometryCollection.prototype.clone);
+
+goog.exportProperty(
+    ol.geom.GeometryCollection.prototype,
+    'getGeometries',
+    ol.geom.GeometryCollection.prototype.getGeometries);
+
+goog.exportProperty(
+    ol.geom.GeometryCollection.prototype,
+    'getType',
+    ol.geom.GeometryCollection.prototype.getType);
+
+goog.exportProperty(
+    ol.geom.GeometryCollection.prototype,
+    'intersectsExtent',
+    ol.geom.GeometryCollection.prototype.intersectsExtent);
+
+goog.exportProperty(
+    ol.geom.GeometryCollection.prototype,
+    'setGeometries',
+    ol.geom.GeometryCollection.prototype.setGeometries);
+
+goog.exportProperty(
+    ol.geom.GeometryCollection.prototype,
+    'applyTransform',
+    ol.geom.GeometryCollection.prototype.applyTransform);
+
+goog.exportProperty(
+    ol.geom.GeometryCollection.prototype,
+    'translate',
+    ol.geom.GeometryCollection.prototype.translate);
+
+goog.exportSymbol(
+    'ol.geom.LinearRing',
+    ol.geom.LinearRing,
+    OPENLAYERS);
+
+goog.exportProperty(
+    ol.geom.LinearRing.prototype,
+    'clone',
+    ol.geom.LinearRing.prototype.clone);
+
+goog.exportProperty(
+    ol.geom.LinearRing.prototype,
+    'getArea',
+    ol.geom.LinearRing.prototype.getArea);
+
+goog.exportProperty(
+    ol.geom.LinearRing.prototype,
+    'getCoordinates',
+    ol.geom.LinearRing.prototype.getCoordinates);
+
+goog.exportProperty(
+    ol.geom.LinearRing.prototype,
+    'getType',
+    ol.geom.LinearRing.prototype.getType);
+
+goog.exportProperty(
+    ol.geom.LinearRing.prototype,
+    'setCoordinates',
+    ol.geom.LinearRing.prototype.setCoordinates);
+
+goog.exportSymbol(
+    'ol.geom.LineString',
+    ol.geom.LineString,
+    OPENLAYERS);
+
+goog.exportProperty(
+    ol.geom.LineString.prototype,
+    'appendCoordinate',
+    ol.geom.LineString.prototype.appendCoordinate);
+
+goog.exportProperty(
+    ol.geom.LineString.prototype,
+    'clone',
+    ol.geom.LineString.prototype.clone);
+
+goog.exportProperty(
+    ol.geom.LineString.prototype,
+    'forEachSegment',
+    ol.geom.LineString.prototype.forEachSegment);
+
+goog.exportProperty(
+    ol.geom.LineString.prototype,
+    'getCoordinateAtM',
+    ol.geom.LineString.prototype.getCoordinateAtM);
+
+goog.exportProperty(
+    ol.geom.LineString.prototype,
+    'getCoordinates',
+    ol.geom.LineString.prototype.getCoordinates);
+
+goog.exportProperty(
+    ol.geom.LineString.prototype,
+    'getCoordinateAt',
+    ol.geom.LineString.prototype.getCoordinateAt);
+
+goog.exportProperty(
+    ol.geom.LineString.prototype,
+    'getLength',
+    ol.geom.LineString.prototype.getLength);
+
+goog.exportProperty(
+    ol.geom.LineString.prototype,
+    'getType',
+    ol.geom.LineString.prototype.getType);
+
+goog.exportProperty(
+    ol.geom.LineString.prototype,
+    'intersectsExtent',
+    ol.geom.LineString.prototype.intersectsExtent);
+
+goog.exportProperty(
+    ol.geom.LineString.prototype,
+    'setCoordinates',
+    ol.geom.LineString.prototype.setCoordinates);
+
+goog.exportSymbol(
+    'ol.geom.MultiLineString',
+    ol.geom.MultiLineString,
+    OPENLAYERS);
+
+goog.exportProperty(
+    ol.geom.MultiLineString.prototype,
+    'appendLineString',
+    ol.geom.MultiLineString.prototype.appendLineString);
+
+goog.exportProperty(
+    ol.geom.MultiLineString.prototype,
+    'clone',
+    ol.geom.MultiLineString.prototype.clone);
+
+goog.exportProperty(
+    ol.geom.MultiLineString.prototype,
+    'getCoordinateAtM',
+    ol.geom.MultiLineString.prototype.getCoordinateAtM);
+
+goog.exportProperty(
+    ol.geom.MultiLineString.prototype,
+    'getCoordinates',
+    ol.geom.MultiLineString.prototype.getCoordinates);
+
+goog.exportProperty(
+    ol.geom.MultiLineString.prototype,
+    'getLineString',
+    ol.geom.MultiLineString.prototype.getLineString);
+
+goog.exportProperty(
+    ol.geom.MultiLineString.prototype,
+    'getLineStrings',
+    ol.geom.MultiLineString.prototype.getLineStrings);
+
+goog.exportProperty(
+    ol.geom.MultiLineString.prototype,
+    'getType',
+    ol.geom.MultiLineString.prototype.getType);
+
+goog.exportProperty(
+    ol.geom.MultiLineString.prototype,
+    'intersectsExtent',
+    ol.geom.MultiLineString.prototype.intersectsExtent);
+
+goog.exportProperty(
+    ol.geom.MultiLineString.prototype,
+    'setCoordinates',
+    ol.geom.MultiLineString.prototype.setCoordinates);
+
+goog.exportSymbol(
+    'ol.geom.MultiPoint',
+    ol.geom.MultiPoint,
+    OPENLAYERS);
+
+goog.exportProperty(
+    ol.geom.MultiPoint.prototype,
+    'appendPoint',
+    ol.geom.MultiPoint.prototype.appendPoint);
+
+goog.exportProperty(
+    ol.geom.MultiPoint.prototype,
+    'clone',
+    ol.geom.MultiPoint.prototype.clone);
+
+goog.exportProperty(
+    ol.geom.MultiPoint.prototype,
+    'getCoordinates',
+    ol.geom.MultiPoint.prototype.getCoordinates);
+
+goog.exportProperty(
+    ol.geom.MultiPoint.prototype,
+    'getPoint',
+    ol.geom.MultiPoint.prototype.getPoint);
+
+goog.exportProperty(
+    ol.geom.MultiPoint.prototype,
+    'getPoints',
+    ol.geom.MultiPoint.prototype.getPoints);
+
+goog.exportProperty(
+    ol.geom.MultiPoint.prototype,
+    'getType',
+    ol.geom.MultiPoint.prototype.getType);
+
+goog.exportProperty(
+    ol.geom.MultiPoint.prototype,
+    'intersectsExtent',
+    ol.geom.MultiPoint.prototype.intersectsExtent);
+
+goog.exportProperty(
+    ol.geom.MultiPoint.prototype,
+    'setCoordinates',
+    ol.geom.MultiPoint.prototype.setCoordinates);
+
+goog.exportSymbol(
+    'ol.geom.MultiPolygon',
+    ol.geom.MultiPolygon,
+    OPENLAYERS);
+
+goog.exportProperty(
+    ol.geom.MultiPolygon.prototype,
+    'appendPolygon',
+    ol.geom.MultiPolygon.prototype.appendPolygon);
+
+goog.exportProperty(
+    ol.geom.MultiPolygon.prototype,
+    'clone',
+    ol.geom.MultiPolygon.prototype.clone);
+
+goog.exportProperty(
+    ol.geom.MultiPolygon.prototype,
+    'getArea',
+    ol.geom.MultiPolygon.prototype.getArea);
+
+goog.exportProperty(
+    ol.geom.MultiPolygon.prototype,
+    'getCoordinates',
+    ol.geom.MultiPolygon.prototype.getCoordinates);
+
+goog.exportProperty(
+    ol.geom.MultiPolygon.prototype,
+    'getInteriorPoints',
+    ol.geom.MultiPolygon.prototype.getInteriorPoints);
+
+goog.exportProperty(
+    ol.geom.MultiPolygon.prototype,
+    'getPolygon',
+    ol.geom.MultiPolygon.prototype.getPolygon);
+
+goog.exportProperty(
+    ol.geom.MultiPolygon.prototype,
+    'getPolygons',
+    ol.geom.MultiPolygon.prototype.getPolygons);
+
+goog.exportProperty(
+    ol.geom.MultiPolygon.prototype,
+    'getType',
+    ol.geom.MultiPolygon.prototype.getType);
+
+goog.exportProperty(
+    ol.geom.MultiPolygon.prototype,
+    'intersectsExtent',
+    ol.geom.MultiPolygon.prototype.intersectsExtent);
+
+goog.exportProperty(
+    ol.geom.MultiPolygon.prototype,
+    'setCoordinates',
+    ol.geom.MultiPolygon.prototype.setCoordinates);
+
+goog.exportSymbol(
+    'ol.geom.Point',
+    ol.geom.Point,
+    OPENLAYERS);
+
+goog.exportProperty(
+    ol.geom.Point.prototype,
+    'clone',
+    ol.geom.Point.prototype.clone);
+
+goog.exportProperty(
+    ol.geom.Point.prototype,
+    'getCoordinates',
+    ol.geom.Point.prototype.getCoordinates);
+
+goog.exportProperty(
+    ol.geom.Point.prototype,
+    'getType',
+    ol.geom.Point.prototype.getType);
+
+goog.exportProperty(
+    ol.geom.Point.prototype,
+    'intersectsExtent',
+    ol.geom.Point.prototype.intersectsExtent);
+
+goog.exportProperty(
+    ol.geom.Point.prototype,
+    'setCoordinates',
+    ol.geom.Point.prototype.setCoordinates);
+
+goog.exportSymbol(
+    'ol.geom.Polygon',
+    ol.geom.Polygon,
+    OPENLAYERS);
+
+goog.exportProperty(
+    ol.geom.Polygon.prototype,
+    'appendLinearRing',
+    ol.geom.Polygon.prototype.appendLinearRing);
+
+goog.exportProperty(
+    ol.geom.Polygon.prototype,
+    'clone',
+    ol.geom.Polygon.prototype.clone);
+
+goog.exportProperty(
+    ol.geom.Polygon.prototype,
+    'getArea',
+    ol.geom.Polygon.prototype.getArea);
+
+goog.exportProperty(
+    ol.geom.Polygon.prototype,
+    'getCoordinates',
+    ol.geom.Polygon.prototype.getCoordinates);
+
+goog.exportProperty(
+    ol.geom.Polygon.prototype,
+    'getInteriorPoint',
+    ol.geom.Polygon.prototype.getInteriorPoint);
+
+goog.exportProperty(
+    ol.geom.Polygon.prototype,
+    'getLinearRingCount',
+    ol.geom.Polygon.prototype.getLinearRingCount);
+
+goog.exportProperty(
+    ol.geom.Polygon.prototype,
+    'getLinearRing',
+    ol.geom.Polygon.prototype.getLinearRing);
+
+goog.exportProperty(
+    ol.geom.Polygon.prototype,
+    'getLinearRings',
+    ol.geom.Polygon.prototype.getLinearRings);
+
+goog.exportProperty(
+    ol.geom.Polygon.prototype,
+    'getType',
+    ol.geom.Polygon.prototype.getType);
+
+goog.exportProperty(
+    ol.geom.Polygon.prototype,
+    'intersectsExtent',
+    ol.geom.Polygon.prototype.intersectsExtent);
+
+goog.exportProperty(
+    ol.geom.Polygon.prototype,
+    'setCoordinates',
+    ol.geom.Polygon.prototype.setCoordinates);
+
+goog.exportSymbol(
+    'ol.geom.Polygon.circular',
+    ol.geom.Polygon.circular,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.geom.Polygon.fromExtent',
+    ol.geom.Polygon.fromExtent,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.geom.Polygon.fromCircle',
+    ol.geom.Polygon.fromCircle,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.geom.SimpleGeometry',
+    ol.geom.SimpleGeometry,
+    OPENLAYERS);
+
+goog.exportProperty(
+    ol.geom.SimpleGeometry.prototype,
+    'getFirstCoordinate',
+    ol.geom.SimpleGeometry.prototype.getFirstCoordinate);
+
+goog.exportProperty(
+    ol.geom.SimpleGeometry.prototype,
+    'getLastCoordinate',
+    ol.geom.SimpleGeometry.prototype.getLastCoordinate);
+
+goog.exportProperty(
+    ol.geom.SimpleGeometry.prototype,
+    'getLayout',
+    ol.geom.SimpleGeometry.prototype.getLayout);
+
+goog.exportProperty(
+    ol.geom.SimpleGeometry.prototype,
+    'applyTransform',
+    ol.geom.SimpleGeometry.prototype.applyTransform);
+
+goog.exportProperty(
+    ol.geom.SimpleGeometry.prototype,
+    'translate',
+    ol.geom.SimpleGeometry.prototype.translate);
+
+goog.exportSymbol(
+    'ol.format.EsriJSON',
+    ol.format.EsriJSON,
+    OPENLAYERS);
+
+goog.exportProperty(
+    ol.format.EsriJSON.prototype,
+    'readFeature',
+    ol.format.EsriJSON.prototype.readFeature);
+
+goog.exportProperty(
+    ol.format.EsriJSON.prototype,
+    'readFeatures',
+    ol.format.EsriJSON.prototype.readFeatures);
+
+goog.exportProperty(
+    ol.format.EsriJSON.prototype,
+    'readGeometry',
+    ol.format.EsriJSON.prototype.readGeometry);
+
+goog.exportProperty(
+    ol.format.EsriJSON.prototype,
+    'readProjection',
+    ol.format.EsriJSON.prototype.readProjection);
+
+goog.exportProperty(
+    ol.format.EsriJSON.prototype,
+    'writeGeometry',
+    ol.format.EsriJSON.prototype.writeGeometry);
+
+goog.exportProperty(
+    ol.format.EsriJSON.prototype,
+    'writeGeometryObject',
+    ol.format.EsriJSON.prototype.writeGeometryObject);
+
+goog.exportProperty(
+    ol.format.EsriJSON.prototype,
+    'writeFeature',
+    ol.format.EsriJSON.prototype.writeFeature);
+
+goog.exportProperty(
+    ol.format.EsriJSON.prototype,
+    'writeFeatureObject',
+    ol.format.EsriJSON.prototype.writeFeatureObject);
+
+goog.exportProperty(
+    ol.format.EsriJSON.prototype,
+    'writeFeatures',
+    ol.format.EsriJSON.prototype.writeFeatures);
+
+goog.exportProperty(
+    ol.format.EsriJSON.prototype,
+    'writeFeaturesObject',
+    ol.format.EsriJSON.prototype.writeFeaturesObject);
+
+goog.exportSymbol(
+    'ol.format.Feature',
+    ol.format.Feature,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.format.GeoJSON',
+    ol.format.GeoJSON,
+    OPENLAYERS);
+
+goog.exportProperty(
+    ol.format.GeoJSON.prototype,
+    'readFeature',
+    ol.format.GeoJSON.prototype.readFeature);
+
+goog.exportProperty(
+    ol.format.GeoJSON.prototype,
+    'readFeatures',
+    ol.format.GeoJSON.prototype.readFeatures);
+
+goog.exportProperty(
+    ol.format.GeoJSON.prototype,
+    'readGeometry',
+    ol.format.GeoJSON.prototype.readGeometry);
+
+goog.exportProperty(
+    ol.format.GeoJSON.prototype,
+    'readProjection',
+    ol.format.GeoJSON.prototype.readProjection);
+
+goog.exportProperty(
+    ol.format.GeoJSON.prototype,
+    'writeFeature',
+    ol.format.GeoJSON.prototype.writeFeature);
+
+goog.exportProperty(
+    ol.format.GeoJSON.prototype,
+    'writeFeatureObject',
+    ol.format.GeoJSON.prototype.writeFeatureObject);
+
+goog.exportProperty(
+    ol.format.GeoJSON.prototype,
+    'writeFeatures',
+    ol.format.GeoJSON.prototype.writeFeatures);
+
+goog.exportProperty(
+    ol.format.GeoJSON.prototype,
+    'writeFeaturesObject',
+    ol.format.GeoJSON.prototype.writeFeaturesObject);
+
+goog.exportProperty(
+    ol.format.GeoJSON.prototype,
+    'writeGeometry',
+    ol.format.GeoJSON.prototype.writeGeometry);
+
+goog.exportProperty(
+    ol.format.GeoJSON.prototype,
+    'writeGeometryObject',
+    ol.format.GeoJSON.prototype.writeGeometryObject);
+
+goog.exportSymbol(
+    'ol.format.GPX',
+    ol.format.GPX,
+    OPENLAYERS);
+
+goog.exportProperty(
+    ol.format.GPX.prototype,
+    'readFeature',
+    ol.format.GPX.prototype.readFeature);
+
+goog.exportProperty(
+    ol.format.GPX.prototype,
+    'readFeatures',
+    ol.format.GPX.prototype.readFeatures);
+
+goog.exportProperty(
+    ol.format.GPX.prototype,
+    'readProjection',
+    ol.format.GPX.prototype.readProjection);
+
+goog.exportProperty(
+    ol.format.GPX.prototype,
+    'writeFeatures',
+    ol.format.GPX.prototype.writeFeatures);
+
+goog.exportProperty(
+    ol.format.GPX.prototype,
+    'writeFeaturesNode',
+    ol.format.GPX.prototype.writeFeaturesNode);
+
+goog.exportSymbol(
+    'ol.format.IGC',
+    ol.format.IGC,
+    OPENLAYERS);
+
+goog.exportProperty(
+    ol.format.IGC.prototype,
+    'readFeature',
+    ol.format.IGC.prototype.readFeature);
+
+goog.exportProperty(
+    ol.format.IGC.prototype,
+    'readFeatures',
+    ol.format.IGC.prototype.readFeatures);
+
+goog.exportProperty(
+    ol.format.IGC.prototype,
+    'readProjection',
+    ol.format.IGC.prototype.readProjection);
+
+goog.exportSymbol(
+    'ol.format.KML',
+    ol.format.KML,
+    OPENLAYERS);
+
+goog.exportProperty(
+    ol.format.KML.prototype,
+    'readFeature',
+    ol.format.KML.prototype.readFeature);
+
+goog.exportProperty(
+    ol.format.KML.prototype,
+    'readFeatures',
+    ol.format.KML.prototype.readFeatures);
+
+goog.exportProperty(
+    ol.format.KML.prototype,
+    'readName',
+    ol.format.KML.prototype.readName);
+
+goog.exportProperty(
+    ol.format.KML.prototype,
+    'readNetworkLinks',
+    ol.format.KML.prototype.readNetworkLinks);
+
+goog.exportProperty(
+    ol.format.KML.prototype,
+    'readProjection',
+    ol.format.KML.prototype.readProjection);
+
+goog.exportProperty(
+    ol.format.KML.prototype,
+    'writeFeatures',
+    ol.format.KML.prototype.writeFeatures);
+
+goog.exportProperty(
+    ol.format.KML.prototype,
+    'writeFeaturesNode',
+    ol.format.KML.prototype.writeFeaturesNode);
+
+goog.exportSymbol(
+    'ol.format.MVT',
+    ol.format.MVT,
+    OPENLAYERS);
+
+goog.exportProperty(
+    ol.format.MVT.prototype,
+    'readFeatures',
+    ol.format.MVT.prototype.readFeatures);
+
+goog.exportProperty(
+    ol.format.MVT.prototype,
+    'readProjection',
+    ol.format.MVT.prototype.readProjection);
+
+goog.exportProperty(
+    ol.format.MVT.prototype,
+    'setLayers',
+    ol.format.MVT.prototype.setLayers);
+
+goog.exportSymbol(
+    'ol.format.OSMXML',
+    ol.format.OSMXML,
+    OPENLAYERS);
+
+goog.exportProperty(
+    ol.format.OSMXML.prototype,
+    'readFeatures',
+    ol.format.OSMXML.prototype.readFeatures);
+
+goog.exportProperty(
+    ol.format.OSMXML.prototype,
+    'readProjection',
+    ol.format.OSMXML.prototype.readProjection);
+
+goog.exportSymbol(
+    'ol.format.Polyline',
+    ol.format.Polyline,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.format.Polyline.encodeDeltas',
+    ol.format.Polyline.encodeDeltas,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.format.Polyline.decodeDeltas',
+    ol.format.Polyline.decodeDeltas,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.format.Polyline.encodeFloats',
+    ol.format.Polyline.encodeFloats,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.format.Polyline.decodeFloats',
+    ol.format.Polyline.decodeFloats,
+    OPENLAYERS);
+
+goog.exportProperty(
+    ol.format.Polyline.prototype,
+    'readFeature',
+    ol.format.Polyline.prototype.readFeature);
+
+goog.exportProperty(
+    ol.format.Polyline.prototype,
+    'readFeatures',
+    ol.format.Polyline.prototype.readFeatures);
+
+goog.exportProperty(
+    ol.format.Polyline.prototype,
+    'readGeometry',
+    ol.format.Polyline.prototype.readGeometry);
+
+goog.exportProperty(
+    ol.format.Polyline.prototype,
+    'readProjection',
+    ol.format.Polyline.prototype.readProjection);
+
+goog.exportProperty(
+    ol.format.Polyline.prototype,
+    'writeGeometry',
+    ol.format.Polyline.prototype.writeGeometry);
+
+goog.exportSymbol(
+    'ol.format.TopoJSON',
+    ol.format.TopoJSON,
+    OPENLAYERS);
+
+goog.exportProperty(
+    ol.format.TopoJSON.prototype,
+    'readFeatures',
+    ol.format.TopoJSON.prototype.readFeatures);
+
+goog.exportProperty(
+    ol.format.TopoJSON.prototype,
+    'readProjection',
+    ol.format.TopoJSON.prototype.readProjection);
+
+goog.exportSymbol(
+    'ol.format.WFS',
+    ol.format.WFS,
+    OPENLAYERS);
+
+goog.exportProperty(
+    ol.format.WFS.prototype,
+    'readFeatures',
+    ol.format.WFS.prototype.readFeatures);
+
+goog.exportProperty(
+    ol.format.WFS.prototype,
+    'readTransactionResponse',
+    ol.format.WFS.prototype.readTransactionResponse);
+
+goog.exportProperty(
+    ol.format.WFS.prototype,
+    'readFeatureCollectionMetadata',
+    ol.format.WFS.prototype.readFeatureCollectionMetadata);
+
+goog.exportProperty(
+    ol.format.WFS.prototype,
+    'writeGetFeature',
+    ol.format.WFS.prototype.writeGetFeature);
+
+goog.exportProperty(
+    ol.format.WFS.prototype,
+    'writeTransaction',
+    ol.format.WFS.prototype.writeTransaction);
+
+goog.exportProperty(
+    ol.format.WFS.prototype,
+    'readProjection',
+    ol.format.WFS.prototype.readProjection);
+
+goog.exportSymbol(
+    'ol.format.WKT',
+    ol.format.WKT,
+    OPENLAYERS);
+
+goog.exportProperty(
+    ol.format.WKT.prototype,
+    'readFeature',
+    ol.format.WKT.prototype.readFeature);
+
+goog.exportProperty(
+    ol.format.WKT.prototype,
+    'readFeatures',
+    ol.format.WKT.prototype.readFeatures);
+
+goog.exportProperty(
+    ol.format.WKT.prototype,
+    'readGeometry',
+    ol.format.WKT.prototype.readGeometry);
+
+goog.exportProperty(
+    ol.format.WKT.prototype,
+    'writeFeature',
+    ol.format.WKT.prototype.writeFeature);
+
+goog.exportProperty(
+    ol.format.WKT.prototype,
+    'writeFeatures',
+    ol.format.WKT.prototype.writeFeatures);
+
+goog.exportProperty(
+    ol.format.WKT.prototype,
+    'writeGeometry',
+    ol.format.WKT.prototype.writeGeometry);
+
+goog.exportSymbol(
+    'ol.format.WMSCapabilities',
+    ol.format.WMSCapabilities,
+    OPENLAYERS);
+
+goog.exportProperty(
+    ol.format.WMSCapabilities.prototype,
+    'read',
+    ol.format.WMSCapabilities.prototype.read);
+
+goog.exportSymbol(
+    'ol.format.WMSGetFeatureInfo',
+    ol.format.WMSGetFeatureInfo,
+    OPENLAYERS);
+
+goog.exportProperty(
+    ol.format.WMSGetFeatureInfo.prototype,
+    'readFeatures',
+    ol.format.WMSGetFeatureInfo.prototype.readFeatures);
+
+goog.exportSymbol(
+    'ol.format.WMTSCapabilities',
+    ol.format.WMTSCapabilities,
+    OPENLAYERS);
+
+goog.exportProperty(
+    ol.format.WMTSCapabilities.prototype,
+    'read',
+    ol.format.WMTSCapabilities.prototype.read);
+
+goog.exportSymbol(
+    'ol.format.ogc.filter.and',
+    ol.format.ogc.filter.and,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.format.ogc.filter.or',
+    ol.format.ogc.filter.or,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.format.ogc.filter.not',
+    ol.format.ogc.filter.not,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.format.ogc.filter.bbox',
+    ol.format.ogc.filter.bbox,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.format.ogc.filter.equalTo',
+    ol.format.ogc.filter.equalTo,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.format.ogc.filter.notEqualTo',
+    ol.format.ogc.filter.notEqualTo,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.format.ogc.filter.lessThan',
+    ol.format.ogc.filter.lessThan,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.format.ogc.filter.lessThanOrEqualTo',
+    ol.format.ogc.filter.lessThanOrEqualTo,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.format.ogc.filter.greaterThan',
+    ol.format.ogc.filter.greaterThan,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.format.ogc.filter.greaterThanOrEqualTo',
+    ol.format.ogc.filter.greaterThanOrEqualTo,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.format.ogc.filter.isNull',
+    ol.format.ogc.filter.isNull,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.format.ogc.filter.between',
+    ol.format.ogc.filter.between,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.format.ogc.filter.like',
+    ol.format.ogc.filter.like,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.format.ogc.filter.Filter',
+    ol.format.ogc.filter.Filter,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.format.ogc.filter.And',
+    ol.format.ogc.filter.And,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.format.ogc.filter.Or',
+    ol.format.ogc.filter.Or,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.format.ogc.filter.Not',
+    ol.format.ogc.filter.Not,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.format.ogc.filter.Bbox',
+    ol.format.ogc.filter.Bbox,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.format.ogc.filter.Comparison',
+    ol.format.ogc.filter.Comparison,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.format.ogc.filter.ComparisonBinary',
+    ol.format.ogc.filter.ComparisonBinary,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.format.ogc.filter.EqualTo',
+    ol.format.ogc.filter.EqualTo,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.format.ogc.filter.NotEqualTo',
+    ol.format.ogc.filter.NotEqualTo,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.format.ogc.filter.LessThan',
+    ol.format.ogc.filter.LessThan,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.format.ogc.filter.LessThanOrEqualTo',
+    ol.format.ogc.filter.LessThanOrEqualTo,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.format.ogc.filter.GreaterThan',
+    ol.format.ogc.filter.GreaterThan,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.format.ogc.filter.GreaterThanOrEqualTo',
+    ol.format.ogc.filter.GreaterThanOrEqualTo,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.format.ogc.filter.IsNull',
+    ol.format.ogc.filter.IsNull,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.format.ogc.filter.IsBetween',
+    ol.format.ogc.filter.IsBetween,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.format.ogc.filter.IsLike',
+    ol.format.ogc.filter.IsLike,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.format.GML2',
+    ol.format.GML2,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.format.GML3',
+    ol.format.GML3,
+    OPENLAYERS);
+
+goog.exportProperty(
+    ol.format.GML3.prototype,
+    'writeGeometryNode',
+    ol.format.GML3.prototype.writeGeometryNode);
+
+goog.exportProperty(
+    ol.format.GML3.prototype,
+    'writeFeatures',
+    ol.format.GML3.prototype.writeFeatures);
+
+goog.exportProperty(
+    ol.format.GML3.prototype,
+    'writeFeaturesNode',
+    ol.format.GML3.prototype.writeFeaturesNode);
+
+goog.exportSymbol(
+    'ol.format.GML',
+    ol.format.GML,
+    OPENLAYERS);
+
+goog.exportProperty(
+    ol.format.GML.prototype,
+    'writeFeatures',
+    ol.format.GML.prototype.writeFeatures);
+
+goog.exportProperty(
+    ol.format.GML.prototype,
+    'writeFeaturesNode',
+    ol.format.GML.prototype.writeFeaturesNode);
+
+goog.exportProperty(
+    ol.format.GMLBase.prototype,
+    'readFeatures',
+    ol.format.GMLBase.prototype.readFeatures);
+
+goog.exportSymbol(
+    'ol.events.condition.altKeyOnly',
+    ol.events.condition.altKeyOnly,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.events.condition.altShiftKeysOnly',
+    ol.events.condition.altShiftKeysOnly,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.events.condition.always',
+    ol.events.condition.always,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.events.condition.click',
+    ol.events.condition.click,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.events.condition.never',
+    ol.events.condition.never,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.events.condition.pointerMove',
+    ol.events.condition.pointerMove,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.events.condition.singleClick',
+    ol.events.condition.singleClick,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.events.condition.doubleClick',
+    ol.events.condition.doubleClick,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.events.condition.noModifierKeys',
+    ol.events.condition.noModifierKeys,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.events.condition.platformModifierKeyOnly',
+    ol.events.condition.platformModifierKeyOnly,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.events.condition.shiftKeyOnly',
+    ol.events.condition.shiftKeyOnly,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.events.condition.targetNotEditable',
+    ol.events.condition.targetNotEditable,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.events.condition.mouseOnly',
+    ol.events.condition.mouseOnly,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.events.condition.primaryAction',
+    ol.events.condition.primaryAction,
+    OPENLAYERS);
+
+goog.exportProperty(
+    ol.events.Event.prototype,
+    'type',
+    ol.events.Event.prototype.type);
+
+goog.exportProperty(
+    ol.events.Event.prototype,
+    'target',
+    ol.events.Event.prototype.target);
+
+goog.exportProperty(
+    ol.events.Event.prototype,
+    'preventDefault',
+    ol.events.Event.prototype.preventDefault);
+
+goog.exportProperty(
+    ol.events.Event.prototype,
+    'stopPropagation',
+    ol.events.Event.prototype.stopPropagation);
+
+goog.exportSymbol(
+    'ol.control.Attribution',
+    ol.control.Attribution,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.control.Attribution.render',
+    ol.control.Attribution.render,
+    OPENLAYERS);
+
+goog.exportProperty(
+    ol.control.Attribution.prototype,
+    'getCollapsible',
+    ol.control.Attribution.prototype.getCollapsible);
+
+goog.exportProperty(
+    ol.control.Attribution.prototype,
+    'setCollapsible',
+    ol.control.Attribution.prototype.setCollapsible);
+
+goog.exportProperty(
+    ol.control.Attribution.prototype,
+    'setCollapsed',
+    ol.control.Attribution.prototype.setCollapsed);
+
+goog.exportProperty(
+    ol.control.Attribution.prototype,
+    'getCollapsed',
+    ol.control.Attribution.prototype.getCollapsed);
+
+goog.exportSymbol(
+    'ol.control.Control',
+    ol.control.Control,
+    OPENLAYERS);
+
+goog.exportProperty(
+    ol.control.Control.prototype,
+    'getMap',
+    ol.control.Control.prototype.getMap);
+
+goog.exportProperty(
+    ol.control.Control.prototype,
+    'setMap',
+    ol.control.Control.prototype.setMap);
+
+goog.exportProperty(
+    ol.control.Control.prototype,
+    'setTarget',
+    ol.control.Control.prototype.setTarget);
+
+goog.exportSymbol(
+    'ol.control.defaults',
+    ol.control.defaults,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.control.FullScreen',
+    ol.control.FullScreen,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.control.MousePosition',
+    ol.control.MousePosition,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.control.MousePosition.render',
+    ol.control.MousePosition.render,
+    OPENLAYERS);
+
+goog.exportProperty(
+    ol.control.MousePosition.prototype,
+    'getCoordinateFormat',
+    ol.control.MousePosition.prototype.getCoordinateFormat);
+
+goog.exportProperty(
+    ol.control.MousePosition.prototype,
+    'getProjection',
+    ol.control.MousePosition.prototype.getProjection);
+
+goog.exportProperty(
+    ol.control.MousePosition.prototype,
+    'setCoordinateFormat',
+    ol.control.MousePosition.prototype.setCoordinateFormat);
+
+goog.exportProperty(
+    ol.control.MousePosition.prototype,
+    'setProjection',
+    ol.control.MousePosition.prototype.setProjection);
+
+goog.exportSymbol(
+    'ol.control.OverviewMap',
+    ol.control.OverviewMap,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.control.OverviewMap.render',
+    ol.control.OverviewMap.render,
+    OPENLAYERS);
+
+goog.exportProperty(
+    ol.control.OverviewMap.prototype,
+    'getCollapsible',
+    ol.control.OverviewMap.prototype.getCollapsible);
+
+goog.exportProperty(
+    ol.control.OverviewMap.prototype,
+    'setCollapsible',
+    ol.control.OverviewMap.prototype.setCollapsible);
+
+goog.exportProperty(
+    ol.control.OverviewMap.prototype,
+    'setCollapsed',
+    ol.control.OverviewMap.prototype.setCollapsed);
+
+goog.exportProperty(
+    ol.control.OverviewMap.prototype,
+    'getCollapsed',
+    ol.control.OverviewMap.prototype.getCollapsed);
+
+goog.exportProperty(
+    ol.control.OverviewMap.prototype,
+    'getOverviewMap',
+    ol.control.OverviewMap.prototype.getOverviewMap);
+
+goog.exportSymbol(
+    'ol.control.Rotate',
+    ol.control.Rotate,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.control.Rotate.render',
+    ol.control.Rotate.render,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.control.ScaleLine',
+    ol.control.ScaleLine,
+    OPENLAYERS);
+
+goog.exportProperty(
+    ol.control.ScaleLine.prototype,
+    'getUnits',
+    ol.control.ScaleLine.prototype.getUnits);
+
+goog.exportSymbol(
+    'ol.control.ScaleLine.render',
+    ol.control.ScaleLine.render,
+    OPENLAYERS);
+
+goog.exportProperty(
+    ol.control.ScaleLine.prototype,
+    'setUnits',
+    ol.control.ScaleLine.prototype.setUnits);
+
+goog.exportSymbol(
+    'ol.control.Zoom',
+    ol.control.Zoom,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.control.ZoomSlider',
+    ol.control.ZoomSlider,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.control.ZoomSlider.render',
+    ol.control.ZoomSlider.render,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.control.ZoomToExtent',
+    ol.control.ZoomToExtent,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.color.asArray',
+    ol.color.asArray,
+    OPENLAYERS);
+
+goog.exportSymbol(
+    'ol.color.asString',
+    ol.color.asString,
+    OPENLAYERS);
+
+goog.exportProperty(
+    ol.CollectionEvent.prototype,
+    'type',
+    ol.CollectionEvent.prototype.type);
+
+goog.exportProperty(
+    ol.CollectionEvent.prototype,
+    'target',
+    ol.CollectionEvent.prototype.target);
+
+goog.exportProperty(
+    ol.CollectionEvent.prototype,
+    'preventDefault',
+    ol.CollectionEvent.prototype.preventDefault);
+
+goog.exportProperty(
+    ol.CollectionEvent.prototype,
+    'stopPropagation',
+    ol.CollectionEvent.prototype.stopPropagation);
+
+goog.exportProperty(
+    ol.Object.prototype,
+    'changed',
+    ol.Object.prototype.changed);
+
+goog.exportProperty(
+    ol.Object.prototype,
+    'dispatchEvent',
+    ol.Object.prototype.dispatchEvent);
+
+goog.exportProperty(
+    ol.Object.prototype,
+    'getRevision',
+    ol.Object.prototype.getRevision);
+
+goog.exportProperty(
+    ol.Object.prototype,
+    'on',
+    ol.Object.prototype.on);
+
+goog.exportProperty(
+    ol.Object.prototype,
+    'once',
+    ol.Object.prototype.once);
+
+goog.exportProperty(
+    ol.Object.prototype,
+    'un',
+    ol.Object.prototype.un);
+
+goog.exportProperty(
+    ol.Object.prototype,
+    'unByKey',
+    ol.Object.prototype.unByKey);
+
+goog.exportProperty(
+    ol.Collection.prototype,
+    'get',
+    ol.Collection.prototype.get);
+
+goog.exportProperty(
+    ol.Collection.prototype,
+    'getKeys',
+    ol.Collection.prototype.getKeys);
+
+goog.exportProperty(
+    ol.Collection.prototype,
+    'getProperties',
+    ol.Collection.prototype.getProperties);
+
+goog.exportProperty(
+    ol.Collection.prototype,
+    'set',
+    ol.Collection.prototype.set);
+
+goog.exportProperty(
+    ol.Collection.prototype,
+    'setProperties',
+    ol.Collection.prototype.setProperties);
+
+goog.exportProperty(
+    ol.Collection.prototype,
+    'unset',
+    ol.Collection.prototype.unset);
+
+goog.exportProperty(
+    ol.Collection.prototype,
+    'changed',
+    ol.Collection.prototype.changed);
+
+goog.exportProperty(
+    ol.Collection.prototype,
+    'dispatchEvent',
+    ol.Collection.prototype.dispatchEvent);
+
+goog.exportProperty(
+    ol.Collection.prototype,
+    'getRevision',
+    ol.Collection.prototype.getRevision);
+
+goog.exportProperty(
+    ol.Collection.prototype,
+    'on',
+    ol.Collection.prototype.on);
+
+goog.exportProperty(
+    ol.Collection.prototype,
+    'once',
+    ol.Collection.prototype.once);
+
+goog.exportProperty(
+    ol.Collection.prototype,
+    'un',
+    ol.Collection.prototype.un);
+
+goog.exportProperty(
+    ol.Collection.prototype,
+    'unByKey',
+    ol.Collection.prototype.unByKey);
+
+goog.exportProperty(
+    ol.DeviceOrientation.prototype,
+    'get',
+    ol.DeviceOrientation.prototype.get);
+
+goog.exportProperty(
+    ol.DeviceOrientation.prototype,
+    'getKeys',
+    ol.DeviceOrientation.prototype.getKeys);
+
+goog.exportProperty(
+    ol.DeviceOrientation.prototype,
+    'getProperties',
+    ol.DeviceOrientation.prototype.getProperties);
+
+goog.exportProperty(
+    ol.DeviceOrientation.prototype,
+    'set',
+    ol.DeviceOrientation.prototype.set);
+
+goog.exportProperty(
+    ol.DeviceOrientation.prototype,
+    'setProperties',
+    ol.DeviceOrientation.prototype.setProperties);
+
+goog.exportProperty(
+    ol.DeviceOrientation.prototype,
+    'unset',
+    ol.DeviceOrientation.prototype.unset);
+
+goog.exportProperty(
+    ol.DeviceOrientation.prototype,
+    'changed',
+    ol.DeviceOrientation.prototype.changed);
+
+goog.exportProperty(
+    ol.DeviceOrientation.prototype,
+    'dispatchEvent',
+    ol.DeviceOrientation.prototype.dispatchEvent);
+
+goog.exportProperty(
+    ol.DeviceOrientation.prototype,
+    'getRevision',
+    ol.DeviceOrientation.prototype.getRevision);
+
+goog.exportProperty(
+    ol.DeviceOrientation.prototype,
+    'on',
+    ol.DeviceOrientation.prototype.on);
+
+goog.exportProperty(
+    ol.DeviceOrientation.prototype,
+    'once',
+    ol.DeviceOrientation.prototype.once);
+
+goog.exportProperty(
+    ol.DeviceOrientation.prototype,
+    'un',
+    ol.DeviceOrientation.prototype.un);
+
+goog.exportProperty(
+    ol.DeviceOrientation.prototype,
+    'unByKey',
+    ol.DeviceOrientation.prototype.unByKey);
+
+goog.exportProperty(
+    ol.Feature.prototype,
+    'get',
+    ol.Feature.prototype.get);
+
+goog.exportProperty(
+    ol.Feature.prototype,
+    'getKeys',
+    ol.Feature.prototype.getKeys);
+
+goog.exportProperty(
+    ol.Feature.prototype,
+    'getProperties',
+    ol.Feature.prototype.getProperties);
+
+goog.exportProperty(
+    ol.Feature.prototype,
+    'set',
+    ol.Feature.prototype.set);
+
+goog.exportProperty(
+    ol.Feature.prototype,
+    'setProperties',
+    ol.Feature.prototype.setProperties);
+
+goog.exportProperty(
+    ol.Feature.prototype,
+    'unset',
+    ol.Feature.prototype.unset);
+
+goog.exportProperty(
+    ol.Feature.prototype,
+    'changed',
+    ol.Feature.prototype.changed);
+
+goog.exportProperty(
+    ol.Feature.prototype,
+    'dispatchEvent',
+    ol.Feature.prototype.dispatchEvent);
+
+goog.exportProperty(
+    ol.Feature.prototype,
+    'getRevision',
+    ol.Feature.prototype.getRevision);
+
+goog.exportProperty(
+    ol.Feature.prototype,
+    'on',
+    ol.Feature.prototype.on);
+
+goog.exportProperty(
+    ol.Feature.prototype,
+    'once',
+    ol.Feature.prototype.once);
+
+goog.exportProperty(
+    ol.Feature.prototype,
+    'un',
+    ol.Feature.prototype.un);
+
+goog.exportProperty(
+    ol.Feature.prototype,
+    'unByKey',
+    ol.Feature.prototype.unByKey);
+
+goog.exportProperty(
+    ol.Geolocation.prototype,
+    'get',
+    ol.Geolocation.prototype.get);
+
+goog.exportProperty(
+    ol.Geolocation.prototype,
+    'getKeys',
+    ol.Geolocation.prototype.getKeys);
+
+goog.exportProperty(
+    ol.Geolocation.prototype,
+    'getProperties',
+    ol.Geolocation.prototype.getProperties);
+
+goog.exportProperty(
+    ol.Geolocation.prototype,
+    'set',
+    ol.Geolocation.prototype.set);
+
+goog.exportProperty(
+    ol.Geolocation.prototype,
+    'setProperties',
+    ol.Geolocation.prototype.setProperties);
+
+goog.exportProperty(
+    ol.Geolocation.prototype,
+    'unset',
+    ol.Geolocation.prototype.unset);
+
+goog.exportProperty(
+    ol.Geolocation.prototype,
+    'changed',
+    ol.Geolocation.prototype.changed);
+
+goog.exportProperty(
+    ol.Geolocation.prototype,
+    'dispatchEvent',
+    ol.Geolocation.prototype.dispatchEvent);
+
+goog.exportProperty(
+    ol.Geolocation.prototype,
+    'getRevision',
+    ol.Geolocation.prototype.getRevision);
+
+goog.exportProperty(
+    ol.Geolocation.prototype,
+    'on',
+    ol.Geolocation.prototype.on);
+
+goog.exportProperty(
+    ol.Geolocation.prototype,
+    'once',
+    ol.Geolocation.prototype.once);
+
+goog.exportProperty(
+    ol.Geolocation.prototype,
+    'un',
+    ol.Geolocation.prototype.un);
+
+goog.exportProperty(
+    ol.Geolocation.prototype,
+    'unByKey',
+    ol.Geolocation.prototype.unByKey);
+
+goog.exportProperty(
+    ol.ImageTile.prototype,
+    'getTileCoord',
+    ol.ImageTile.prototype.getTileCoord);
+
+goog.exportProperty(
+    ol.Map.prototype,
+    'get',
+    ol.Map.prototype.get);
+
+goog.exportProperty(
+    ol.Map.prototype,
+    'getKeys',
+    ol.Map.prototype.getKeys);
+
+goog.exportProperty(
+    ol.Map.prototype,
+    'getProperties',
+    ol.Map.prototype.getProperties);
+
+goog.exportProperty(
+    ol.Map.prototype,
+    'set',
+    ol.Map.prototype.set);
+
+goog.exportProperty(
+    ol.Map.prototype,
+    'setProperties',
+    ol.Map.prototype.setProperties);
+
+goog.exportProperty(
+    ol.Map.prototype,
+    'unset',
+    ol.Map.prototype.unset);
+
+goog.exportProperty(
+    ol.Map.prototype,
+    'changed',
+    ol.Map.prototype.changed);
+
+goog.exportProperty(
+    ol.Map.prototype,
+    'dispatchEvent',
+    ol.Map.prototype.dispatchEvent);
+
+goog.exportProperty(
+    ol.Map.prototype,
+    'getRevision',
+    ol.Map.prototype.getRevision);
+
+goog.exportProperty(
+    ol.Map.prototype,
+    'on',
+    ol.Map.prototype.on);
+
+goog.exportProperty(
+    ol.Map.prototype,
+    'once',
+    ol.Map.prototype.once);
+
+goog.exportProperty(
+    ol.Map.prototype,
+    'un',
+    ol.Map.prototype.un);
+
+goog.exportProperty(
+    ol.Map.prototype,
+    'unByKey',
+    ol.Map.prototype.unByKey);
+
+goog.exportProperty(
+    ol.MapEvent.prototype,
+    'type',
+    ol.MapEvent.prototype.type);
+
+goog.exportProperty(
+    ol.MapEvent.prototype,
+    'target',
+    ol.MapEvent.prototype.target);
+
+goog.exportProperty(
+    ol.MapEvent.prototype,
+    'preventDefault',
+    ol.MapEvent.prototype.preventDefault);
+
+goog.exportProperty(
+    ol.MapEvent.prototype,
+    'stopPropagation',
+    ol.MapEvent.prototype.stopPropagation);
+
+goog.exportProperty(
+    ol.MapBrowserEvent.prototype,
+    'map',
+    ol.MapBrowserEvent.prototype.map);
+
+goog.exportProperty(
+    ol.MapBrowserEvent.prototype,
+    'frameState',
+    ol.MapBrowserEvent.prototype.frameState);
+
+goog.exportProperty(
+    ol.MapBrowserEvent.prototype,
+    'type',
+    ol.MapBrowserEvent.prototype.type);
+
+goog.exportProperty(
+    ol.MapBrowserEvent.prototype,
+    'target',
+    ol.MapBrowserEvent.prototype.target);
+
+goog.exportProperty(
+    ol.MapBrowserEvent.prototype,
+    'preventDefault',
+    ol.MapBrowserEvent.prototype.preventDefault);
+
+goog.exportProperty(
+    ol.MapBrowserEvent.prototype,
+    'stopPropagation',
+    ol.MapBrowserEvent.prototype.stopPropagation);
+
+goog.exportProperty(
+    ol.MapBrowserPointerEvent.prototype,
+    'originalEvent',
+    ol.MapBrowserPointerEvent.prototype.originalEvent);
+
+goog.exportProperty(
+    ol.MapBrowserPointerEvent.prototype,
+    'pixel',
+    ol.MapBrowserPointerEvent.prototype.pixel);
+
+goog.exportProperty(
+    ol.MapBrowserPointerEvent.prototype,
+    'coordinate',
+    ol.MapBrowserPointerEvent.prototype.coordinate);
+
+goog.exportProperty(
+    ol.MapBrowserPointerEvent.prototype,
+    'dragging',
+    ol.MapBrowserPointerEvent.prototype.dragging);
+
+goog.exportProperty(
+    ol.MapBrowserPointerEvent.prototype,
+    'preventDefault',
+    ol.MapBrowserPointerEvent.prototype.preventDefault);
+
+goog.exportProperty(
+    ol.MapBrowserPointerEvent.prototype,
+    'stopPropagation',
+    ol.MapBrowserPointerEvent.prototype.stopPropagation);
+
+goog.exportProperty(
+    ol.MapBrowserPointerEvent.prototype,
+    'map',
+    ol.MapBrowserPointerEvent.prototype.map);
+
+goog.exportProperty(
+    ol.MapBrowserPointerEvent.prototype,
+    'frameState',
+    ol.MapBrowserPointerEvent.prototype.frameState);
+
+goog.exportProperty(
+    ol.MapBrowserPointerEvent.prototype,
+    'type',
+    ol.MapBrowserPointerEvent.prototype.type);
+
+goog.exportProperty(
+    ol.MapBrowserPointerEvent.prototype,
+    'target',
+    ol.MapBrowserPointerEvent.prototype.target);
+
+goog.exportProperty(
+    ol.ObjectEvent.prototype,
+    'type',
+    ol.ObjectEvent.prototype.type);
+
+goog.exportProperty(
+    ol.ObjectEvent.prototype,
+    'target',
+    ol.ObjectEvent.prototype.target);
+
+goog.exportProperty(
+    ol.ObjectEvent.prototype,
+    'preventDefault',
+    ol.ObjectEvent.prototype.preventDefault);
+
+goog.exportProperty(
+    ol.ObjectEvent.prototype,
+    'stopPropagation',
+    ol.ObjectEvent.prototype.stopPropagation);
+
+goog.exportProperty(
+    ol.Overlay.prototype,
+    'get',
+    ol.Overlay.prototype.get);
+
+goog.exportProperty(
+    ol.Overlay.prototype,
+    'getKeys',
+    ol.Overlay.prototype.getKeys);
+
+goog.exportProperty(
+    ol.Overlay.prototype,
+    'getProperties',
+    ol.Overlay.prototype.getProperties);
+
+goog.exportProperty(
+    ol.Overlay.prototype,
+    'set',
+    ol.Overlay.prototype.set);
+
+goog.exportProperty(
+    ol.Overlay.prototype,
+    'setProperties',
+    ol.Overlay.prototype.setProperties);
+
+goog.exportProperty(
+    ol.Overlay.prototype,
+    'unset',
+    ol.Overlay.prototype.unset);
+
+goog.exportProperty(
+    ol.Overlay.prototype,
+    'changed',
+    ol.Overlay.prototype.changed);
+
+goog.exportProperty(
+    ol.Overlay.prototype,
+    'dispatchEvent',
+    ol.Overlay.prototype.dispatchEvent);
+
+goog.exportProperty(
+    ol.Overlay.prototype,
+    'getRevision',
+    ol.Overlay.prototype.getRevision);
+
+goog.exportProperty(
+    ol.Overlay.prototype,
+    'on',
+    ol.Overlay.prototype.on);
+
+goog.exportProperty(
+    ol.Overlay.prototype,
+    'once',
+    ol.Overlay.prototype.once);
+
+goog.exportProperty(
+    ol.Overlay.prototype,
+    'un',
+    ol.Overlay.prototype.un);
+
+goog.exportProperty(
+    ol.Overlay.prototype,
+    'unByKey',
+    ol.Overlay.prototype.unByKey);
+
+goog.exportProperty(
+    ol.VectorTile.prototype,
+    'getTileCoord',
+    ol.VectorTile.prototype.getTileCoord);
+
+goog.exportProperty(
+    ol.View.prototype,
+    'get',
+    ol.View.prototype.get);
+
+goog.exportProperty(
+    ol.View.prototype,
+    'getKeys',
+    ol.View.prototype.getKeys);
+
+goog.exportProperty(
+    ol.View.prototype,
+    'getProperties',
+    ol.View.prototype.getProperties);
+
+goog.exportProperty(
+    ol.View.prototype,
+    'set',
+    ol.View.prototype.set);
+
+goog.exportProperty(
+    ol.View.prototype,
+    'setProperties',
+    ol.View.prototype.setProperties);
+
+goog.exportProperty(
+    ol.View.prototype,
+    'unset',
+    ol.View.prototype.unset);
+
+goog.exportProperty(
+    ol.View.prototype,
+    'changed',
+    ol.View.prototype.changed);
+
+goog.exportProperty(
+    ol.View.prototype,
+    'dispatchEvent',
+    ol.View.prototype.dispatchEvent);
+
+goog.exportProperty(
+    ol.View.prototype,
+    'getRevision',
+    ol.View.prototype.getRevision);
+
+goog.exportProperty(
+    ol.View.prototype,
+    'on',
+    ol.View.prototype.on);
+
+goog.exportProperty(
+    ol.View.prototype,
+    'once',
+    ol.View.prototype.once);
+
+goog.exportProperty(
+    ol.View.prototype,
+    'un',
+    ol.View.prototype.un);
+
+goog.exportProperty(
+    ol.View.prototype,
+    'unByKey',
+    ol.View.prototype.unByKey);
+
+goog.exportProperty(
+    ol.tilegrid.WMTS.prototype,
+    'forEachTileCoord',
+    ol.tilegrid.WMTS.prototype.forEachTileCoord);
+
+goog.exportProperty(
+    ol.tilegrid.WMTS.prototype,
+    'getMaxZoom',
+    ol.tilegrid.WMTS.prototype.getMaxZoom);
+
+goog.exportProperty(
+    ol.tilegrid.WMTS.prototype,
+    'getMinZoom',
+    ol.tilegrid.WMTS.prototype.getMinZoom);
+
+goog.exportProperty(
+    ol.tilegrid.WMTS.prototype,
+    'getOrigin',
+    ol.tilegrid.WMTS.prototype.getOrigin);
+
+goog.exportProperty(
+    ol.tilegrid.WMTS.prototype,
+    'getResolution',
+    ol.tilegrid.WMTS.prototype.getResolution);
+
+goog.exportProperty(
+    ol.tilegrid.WMTS.prototype,
+    'getResolutions',
+    ol.tilegrid.WMTS.prototype.getResolutions);
+
+goog.exportProperty(
+    ol.tilegrid.WMTS.prototype,
+    'getTileCoordExtent',
+    ol.tilegrid.WMTS.prototype.getTileCoordExtent);
+
+goog.exportProperty(
+    ol.tilegrid.WMTS.prototype,
+    'getTileCoordForCoordAndResolution',
+    ol.tilegrid.WMTS.prototype.getTileCoordForCoordAndResolution);
+
+goog.exportProperty(
+    ol.tilegrid.WMTS.prototype,
+    'getTileCoordForCoordAndZ',
+    ol.tilegrid.WMTS.prototype.getTileCoordForCoordAndZ);
+
+goog.exportProperty(
+    ol.tilegrid.WMTS.prototype,
+    'getTileSize',
+    ol.tilegrid.WMTS.prototype.getTileSize);
+
+goog.exportProperty(
+    ol.tilegrid.WMTS.prototype,
+    'getZForResolution',
+    ol.tilegrid.WMTS.prototype.getZForResolution);
+
+goog.exportProperty(
+    ol.style.Circle.prototype,
+    'getOpacity',
+    ol.style.Circle.prototype.getOpacity);
+
+goog.exportProperty(
+    ol.style.Circle.prototype,
+    'getRotateWithView',
+    ol.style.Circle.prototype.getRotateWithView);
+
+goog.exportProperty(
+    ol.style.Circle.prototype,
+    'getRotation',
+    ol.style.Circle.prototype.getRotation);
+
+goog.exportProperty(
+    ol.style.Circle.prototype,
+    'getScale',
+    ol.style.Circle.prototype.getScale);
+
+goog.exportProperty(
+    ol.style.Circle.prototype,
+    'getSnapToPixel',
+    ol.style.Circle.prototype.getSnapToPixel);
+
+goog.exportProperty(
+    ol.style.Circle.prototype,
+    'setOpacity',
+    ol.style.Circle.prototype.setOpacity);
+
+goog.exportProperty(
+    ol.style.Circle.prototype,
+    'setRotation',
+    ol.style.Circle.prototype.setRotation);
+
+goog.exportProperty(
+    ol.style.Circle.prototype,
+    'setScale',
+    ol.style.Circle.prototype.setScale);
+
+goog.exportProperty(
+    ol.style.Icon.prototype,
+    'getOpacity',
+    ol.style.Icon.prototype.getOpacity);
+
+goog.exportProperty(
+    ol.style.Icon.prototype,
+    'getRotateWithView',
+    ol.style.Icon.prototype.getRotateWithView);
+
+goog.exportProperty(
+    ol.style.Icon.prototype,
+    'getRotation',
+    ol.style.Icon.prototype.getRotation);
+
+goog.exportProperty(
+    ol.style.Icon.prototype,
+    'getScale',
+    ol.style.Icon.prototype.getScale);
+
+goog.exportProperty(
+    ol.style.Icon.prototype,
+    'getSnapToPixel',
+    ol.style.Icon.prototype.getSnapToPixel);
+
+goog.exportProperty(
+    ol.style.Icon.prototype,
+    'setOpacity',
+    ol.style.Icon.prototype.setOpacity);
+
+goog.exportProperty(
+    ol.style.Icon.prototype,
+    'setRotation',
+    ol.style.Icon.prototype.setRotation);
+
+goog.exportProperty(
+    ol.style.Icon.prototype,
+    'setScale',
+    ol.style.Icon.prototype.setScale);
+
+goog.exportProperty(
+    ol.style.RegularShape.prototype,
+    'getOpacity',
+    ol.style.RegularShape.prototype.getOpacity);
+
+goog.exportProperty(
+    ol.style.RegularShape.prototype,
+    'getRotateWithView',
+    ol.style.RegularShape.prototype.getRotateWithView);
+
+goog.exportProperty(
+    ol.style.RegularShape.prototype,
+    'getRotation',
+    ol.style.RegularShape.prototype.getRotation);
+
+goog.exportProperty(
+    ol.style.RegularShape.prototype,
+    'getScale',
+    ol.style.RegularShape.prototype.getScale);
+
+goog.exportProperty(
+    ol.style.RegularShape.prototype,
+    'getSnapToPixel',
+    ol.style.RegularShape.prototype.getSnapToPixel);
+
+goog.exportProperty(
+    ol.style.RegularShape.prototype,
+    'setOpacity',
+    ol.style.RegularShape.prototype.setOpacity);
+
+goog.exportProperty(
+    ol.style.RegularShape.prototype,
+    'setRotation',
+    ol.style.RegularShape.prototype.setRotation);
+
+goog.exportProperty(
+    ol.style.RegularShape.prototype,
+    'setScale',
+    ol.style.RegularShape.prototype.setScale);
+
+goog.exportProperty(
+    ol.source.Source.prototype,
+    'get',
+    ol.source.Source.prototype.get);
+
+goog.exportProperty(
+    ol.source.Source.prototype,
+    'getKeys',
+    ol.source.Source.prototype.getKeys);
+
+goog.exportProperty(
+    ol.source.Source.prototype,
+    'getProperties',
+    ol.source.Source.prototype.getProperties);
+
+goog.exportProperty(
+    ol.source.Source.prototype,
+    'set',
+    ol.source.Source.prototype.set);
+
+goog.exportProperty(
+    ol.source.Source.prototype,
+    'setProperties',
+    ol.source.Source.prototype.setProperties);
+
+goog.exportProperty(
+    ol.source.Source.prototype,
+    'unset',
+    ol.source.Source.prototype.unset);
+
+goog.exportProperty(
+    ol.source.Source.prototype,
+    'changed',
+    ol.source.Source.prototype.changed);
+
+goog.exportProperty(
+    ol.source.Source.prototype,
+    'dispatchEvent',
+    ol.source.Source.prototype.dispatchEvent);
+
+goog.exportProperty(
+    ol.source.Source.prototype,
+    'getRevision',
+    ol.source.Source.prototype.getRevision);
+
+goog.exportProperty(
+    ol.source.Source.prototype,
+    'on',
+    ol.source.Source.prototype.on);
+
+goog.exportProperty(
+    ol.source.Source.prototype,
+    'once',
+    ol.source.Source.prototype.once);
+
+goog.exportProperty(
+    ol.source.Source.prototype,
+    'un',
+    ol.source.Source.prototype.un);
+
+goog.exportProperty(
+    ol.source.Source.prototype,
+    'unByKey',
+    ol.source.Source.prototype.unByKey);
+
+goog.exportProperty(
+    ol.source.Tile.prototype,
+    'getAttributions',
+    ol.source.Tile.prototype.getAttributions);
+
+goog.exportProperty(
+    ol.source.Tile.prototype,
+    'getLogo',
+    ol.source.Tile.prototype.getLogo);
+
+goog.exportProperty(
+    ol.source.Tile.prototype,
+    'getProjection',
+    ol.source.Tile.prototype.getProjection);
+
+goog.exportProperty(
+    ol.source.Tile.prototype,
+    'getState',
+    ol.source.Tile.prototype.getState);
+
+goog.exportProperty(
+    ol.source.Tile.prototype,
+    'refresh',
+    ol.source.Tile.prototype.refresh);
+
+goog.exportProperty(
+    ol.source.Tile.prototype,
+    'setAttributions',
+    ol.source.Tile.prototype.setAttributions);
+
+goog.exportProperty(
+    ol.source.Tile.prototype,
+    'get',
+    ol.source.Tile.prototype.get);
+
+goog.exportProperty(
+    ol.source.Tile.prototype,
+    'getKeys',
+    ol.source.Tile.prototype.getKeys);
+
+goog.exportProperty(
+    ol.source.Tile.prototype,
+    'getProperties',
+    ol.source.Tile.prototype.getProperties);
+
+goog.exportProperty(
+    ol.source.Tile.prototype,
+    'set',
+    ol.source.Tile.prototype.set);
+
+goog.exportProperty(
+    ol.source.Tile.prototype,
+    'setProperties',
+    ol.source.Tile.prototype.setProperties);
+
+goog.exportProperty(
+    ol.source.Tile.prototype,
+    'unset',
+    ol.source.Tile.prototype.unset);
+
+goog.exportProperty(
+    ol.source.Tile.prototype,
+    'changed',
+    ol.source.Tile.prototype.changed);
+
+goog.exportProperty(
+    ol.source.Tile.prototype,
+    'dispatchEvent',
+    ol.source.Tile.prototype.dispatchEvent);
+
+goog.exportProperty(
+    ol.source.Tile.prototype,
+    'getRevision',
+    ol.source.Tile.prototype.getRevision);
+
+goog.exportProperty(
+    ol.source.Tile.prototype,
+    'on',
+    ol.source.Tile.prototype.on);
+
+goog.exportProperty(
+    ol.source.Tile.prototype,
+    'once',
+    ol.source.Tile.prototype.once);
+
+goog.exportProperty(
+    ol.source.Tile.prototype,
+    'un',
+    ol.source.Tile.prototype.un);
+
+goog.exportProperty(
+    ol.source.Tile.prototype,
+    'unByKey',
+    ol.source.Tile.prototype.unByKey);
+
+goog.exportProperty(
+    ol.source.UrlTile.prototype,
+    'getTileGrid',
+    ol.source.UrlTile.prototype.getTileGrid);
+
+goog.exportProperty(
+    ol.source.UrlTile.prototype,
+    'refresh',
+    ol.source.UrlTile.prototype.refresh);
+
+goog.exportProperty(
+    ol.source.UrlTile.prototype,
+    'getAttributions',
+    ol.source.UrlTile.prototype.getAttributions);
+
+goog.exportProperty(
+    ol.source.UrlTile.prototype,
+    'getLogo',
+    ol.source.UrlTile.prototype.getLogo);
+
+goog.exportProperty(
+    ol.source.UrlTile.prototype,
+    'getProjection',
+    ol.source.UrlTile.prototype.getProjection);
+
+goog.exportProperty(
+    ol.source.UrlTile.prototype,
+    'getState',
+    ol.source.UrlTile.prototype.getState);
+
+goog.exportProperty(
+    ol.source.UrlTile.prototype,
+    'setAttributions',
+    ol.source.UrlTile.prototype.setAttributions);
+
+goog.exportProperty(
+    ol.source.UrlTile.prototype,
+    'get',
+    ol.source.UrlTile.prototype.get);
+
+goog.exportProperty(
+    ol.source.UrlTile.prototype,
+    'getKeys',
+    ol.source.UrlTile.prototype.getKeys);
+
+goog.exportProperty(
+    ol.source.UrlTile.prototype,
+    'getProperties',
+    ol.source.UrlTile.prototype.getProperties);
+
+goog.exportProperty(
+    ol.source.UrlTile.prototype,
+    'set',
+    ol.source.UrlTile.prototype.set);
+
+goog.exportProperty(
+    ol.source.UrlTile.prototype,
+    'setProperties',
+    ol.source.UrlTile.prototype.setProperties);
+
+goog.exportProperty(
+    ol.source.UrlTile.prototype,
+    'unset',
+    ol.source.UrlTile.prototype.unset);
+
+goog.exportProperty(
+    ol.source.UrlTile.prototype,
+    'changed',
+    ol.source.UrlTile.prototype.changed);
+
+goog.exportProperty(
+    ol.source.UrlTile.prototype,
+    'dispatchEvent',
+    ol.source.UrlTile.prototype.dispatchEvent);
+
+goog.exportProperty(
+    ol.source.UrlTile.prototype,
+    'getRevision',
+    ol.source.UrlTile.prototype.getRevision);
+
+goog.exportProperty(
+    ol.source.UrlTile.prototype,
+    'on',
+    ol.source.UrlTile.prototype.on);
+
+goog.exportProperty(
+    ol.source.UrlTile.prototype,
+    'once',
+    ol.source.UrlTile.prototype.once);
+
+goog.exportProperty(
+    ol.source.UrlTile.prototype,
+    'un',
+    ol.source.UrlTile.prototype.un);
+
+goog.exportProperty(
+    ol.source.UrlTile.prototype,
+    'unByKey',
+    ol.source.UrlTile.prototype.unByKey);
+
+goog.exportProperty(
+    ol.source.TileImage.prototype,
+    'getTileLoadFunction',
+    ol.source.TileImage.prototype.getTileLoadFunction);
+
+goog.exportProperty(
+    ol.source.TileImage.prototype,
+    'getTileUrlFunction',
+    ol.source.TileImage.prototype.getTileUrlFunction);
+
+goog.exportProperty(
+    ol.source.TileImage.prototype,
+    'getUrls',
+    ol.source.TileImage.prototype.getUrls);
+
+goog.exportProperty(
+    ol.source.TileImage.prototype,
+    'setTileLoadFunction',
+    ol.source.TileImage.prototype.setTileLoadFunction);
+
+goog.exportProperty(
+    ol.source.TileImage.prototype,
+    'setTileUrlFunction',
+    ol.source.TileImage.prototype.setTileUrlFunction);
+
+goog.exportProperty(
+    ol.source.TileImage.prototype,
+    'setUrl',
+    ol.source.TileImage.prototype.setUrl);
+
+goog.exportProperty(
+    ol.source.TileImage.prototype,
+    'setUrls',
+    ol.source.TileImage.prototype.setUrls);
+
+goog.exportProperty(
+    ol.source.TileImage.prototype,
+    'getTileGrid',
+    ol.source.TileImage.prototype.getTileGrid);
+
+goog.exportProperty(
+    ol.source.TileImage.prototype,
+    'refresh',
+    ol.source.TileImage.prototype.refresh);
+
+goog.exportProperty(
+    ol.source.TileImage.prototype,
+    'getAttributions',
+    ol.source.TileImage.prototype.getAttributions);
+
+goog.exportProperty(
+    ol.source.TileImage.prototype,
+    'getLogo',
+    ol.source.TileImage.prototype.getLogo);
+
+goog.exportProperty(
+    ol.source.TileImage.prototype,
+    'getProjection',
+    ol.source.TileImage.prototype.getProjection);
+
+goog.exportProperty(
+    ol.source.TileImage.prototype,
+    'getState',
+    ol.source.TileImage.prototype.getState);
+
+goog.exportProperty(
+    ol.source.TileImage.prototype,
+    'setAttributions',
+    ol.source.TileImage.prototype.setAttributions);
+
+goog.exportProperty(
+    ol.source.TileImage.prototype,
+    'get',
+    ol.source.TileImage.prototype.get);
+
+goog.exportProperty(
+    ol.source.TileImage.prototype,
+    'getKeys',
+    ol.source.TileImage.prototype.getKeys);
+
+goog.exportProperty(
+    ol.source.TileImage.prototype,
+    'getProperties',
+    ol.source.TileImage.prototype.getProperties);
+
+goog.exportProperty(
+    ol.source.TileImage.prototype,
+    'set',
+    ol.source.TileImage.prototype.set);
+
+goog.exportProperty(
+    ol.source.TileImage.prototype,
+    'setProperties',
+    ol.source.TileImage.prototype.setProperties);
+
+goog.exportProperty(
+    ol.source.TileImage.prototype,
+    'unset',
+    ol.source.TileImage.prototype.unset);
+
+goog.exportProperty(
+    ol.source.TileImage.prototype,
+    'changed',
+    ol.source.TileImage.prototype.changed);
+
+goog.exportProperty(
+    ol.source.TileImage.prototype,
+    'dispatchEvent',
+    ol.source.TileImage.prototype.dispatchEvent);
+
+goog.exportProperty(
+    ol.source.TileImage.prototype,
+    'getRevision',
+    ol.source.TileImage.prototype.getRevision);
+
+goog.exportProperty(
+    ol.source.TileImage.prototype,
+    'on',
+    ol.source.TileImage.prototype.on);
+
+goog.exportProperty(
+    ol.source.TileImage.prototype,
+    'once',
+    ol.source.TileImage.prototype.once);
+
+goog.exportProperty(
+    ol.source.TileImage.prototype,
+    'un',
+    ol.source.TileImage.prototype.un);
+
+goog.exportProperty(
+    ol.source.TileImage.prototype,
+    'unByKey',
+    ol.source.TileImage.prototype.unByKey);
+
+goog.exportProperty(
+    ol.source.BingMaps.prototype,
+    'setRenderReprojectionEdges',
+    ol.source.BingMaps.prototype.setRenderReprojectionEdges);
+
+goog.exportProperty(
+    ol.source.BingMaps.prototype,
+    'setTileGridForProjection',
+    ol.source.BingMaps.prototype.setTileGridForProjection);
+
+goog.exportProperty(
+    ol.source.BingMaps.prototype,
+    'getTileLoadFunction',
+    ol.source.BingMaps.prototype.getTileLoadFunction);
+
+goog.exportProperty(
+    ol.source.BingMaps.prototype,
+    'getTileUrlFunction',
+    ol.source.BingMaps.prototype.getTileUrlFunction);
+
+goog.exportProperty(
+    ol.source.BingMaps.prototype,
+    'getUrls',
+    ol.source.BingMaps.prototype.getUrls);
+
+goog.exportProperty(
+    ol.source.BingMaps.prototype,
+    'setTileLoadFunction',
+    ol.source.BingMaps.prototype.setTileLoadFunction);
+
+goog.exportProperty(
+    ol.source.BingMaps.prototype,
+    'setTileUrlFunction',
+    ol.source.BingMaps.prototype.setTileUrlFunction);
+
+goog.exportProperty(
+    ol.source.BingMaps.prototype,
+    'setUrl',
+    ol.source.BingMaps.prototype.setUrl);
+
+goog.exportProperty(
+    ol.source.BingMaps.prototype,
+    'setUrls',
+    ol.source.BingMaps.prototype.setUrls);
+
+goog.exportProperty(
+    ol.source.BingMaps.prototype,
+    'getTileGrid',
+    ol.source.BingMaps.prototype.getTileGrid);
+
+goog.exportProperty(
+    ol.source.BingMaps.prototype,
+    'refresh',
+    ol.source.BingMaps.prototype.refresh);
+
+goog.exportProperty(
+    ol.source.BingMaps.prototype,
+    'getAttributions',
+    ol.source.BingMaps.prototype.getAttributions);
+
+goog.exportProperty(
+    ol.source.BingMaps.prototype,
+    'getLogo',
+    ol.source.BingMaps.prototype.getLogo);
+
+goog.exportProperty(
+    ol.source.BingMaps.prototype,
+    'getProjection',
+    ol.source.BingMaps.prototype.getProjection);
+
+goog.exportProperty(
+    ol.source.BingMaps.prototype,
+    'getState',
+    ol.source.BingMaps.prototype.getState);
+
+goog.exportProperty(
+    ol.source.BingMaps.prototype,
+    'setAttributions',
+    ol.source.BingMaps.prototype.setAttributions);
+
+goog.exportProperty(
+    ol.source.BingMaps.prototype,
+    'get',
+    ol.source.BingMaps.prototype.get);
+
+goog.exportProperty(
+    ol.source.BingMaps.prototype,
+    'getKeys',
+    ol.source.BingMaps.prototype.getKeys);
+
+goog.exportProperty(
+    ol.source.BingMaps.prototype,
+    'getProperties',
+    ol.source.BingMaps.prototype.getProperties);
+
+goog.exportProperty(
+    ol.source.BingMaps.prototype,
+    'set',
+    ol.source.BingMaps.prototype.set);
+
+goog.exportProperty(
+    ol.source.BingMaps.prototype,
+    'setProperties',
+    ol.source.BingMaps.prototype.setProperties);
+
+goog.exportProperty(
+    ol.source.BingMaps.prototype,
+    'unset',
+    ol.source.BingMaps.prototype.unset);
+
+goog.exportProperty(
+    ol.source.BingMaps.prototype,
+    'changed',
+    ol.source.BingMaps.prototype.changed);
+
+goog.exportProperty(
+    ol.source.BingMaps.prototype,
+    'dispatchEvent',
+    ol.source.BingMaps.prototype.dispatchEvent);
+
+goog.exportProperty(
+    ol.source.BingMaps.prototype,
+    'getRevision',
+    ol.source.BingMaps.prototype.getRevision);
+
+goog.exportProperty(
+    ol.source.BingMaps.prototype,
+    'on',
+    ol.source.BingMaps.prototype.on);
+
+goog.exportProperty(
+    ol.source.BingMaps.prototype,
+    'once',
+    ol.source.BingMaps.prototype.once);
+
+goog.exportProperty(
+    ol.source.BingMaps.prototype,
+    'un',
+    ol.source.BingMaps.prototype.un);
+
+goog.exportProperty(
+    ol.source.BingMaps.prototype,
+    'unByKey',
+    ol.source.BingMaps.prototype.unByKey);
+
+goog.exportProperty(
+    ol.source.XYZ.prototype,
+    'setRenderReprojectionEdges',
+    ol.source.XYZ.prototype.setRenderReprojectionEdges);
+
+goog.exportProperty(
+    ol.source.XYZ.prototype,
+    'setTileGridForProjection',
+    ol.source.XYZ.prototype.setTileGridForProjection);
+
+goog.exportProperty(
+    ol.source.XYZ.prototype,
+    'getTileLoadFunction',
+    ol.source.XYZ.prototype.getTileLoadFunction);
+
+goog.exportProperty(
+    ol.source.XYZ.prototype,
+    'getTileUrlFunction',
+    ol.source.XYZ.prototype.getTileUrlFunction);
+
+goog.exportProperty(
+    ol.source.XYZ.prototype,
+    'getUrls',
+    ol.source.XYZ.prototype.getUrls);
+
+goog.exportProperty(
+    ol.source.XYZ.prototype,
+    'setTileLoadFunction',
+    ol.source.XYZ.prototype.setTileLoadFunction);
+
+goog.exportProperty(
+    ol.source.XYZ.prototype,
+    'setTileUrlFunction',
+    ol.source.XYZ.prototype.setTileUrlFunction);
+
+goog.exportProperty(
+    ol.source.XYZ.prototype,
+    'setUrl',
+    ol.source.XYZ.prototype.setUrl);
+
+goog.exportProperty(
+    ol.source.XYZ.prototype,
+    'setUrls',
+    ol.source.XYZ.prototype.setUrls);
+
+goog.exportProperty(
+    ol.source.XYZ.prototype,
+    'getTileGrid',
+    ol.source.XYZ.prototype.getTileGrid);
+
+goog.exportProperty(
+    ol.source.XYZ.prototype,
+    'refresh',
+    ol.source.XYZ.prototype.refresh);
+
+goog.exportProperty(
+    ol.source.XYZ.prototype,
+    'getAttributions',
+    ol.source.XYZ.prototype.getAttributions);
+
+goog.exportProperty(
+    ol.source.XYZ.prototype,
+    'getLogo',
+    ol.source.XYZ.prototype.getLogo);
+
+goog.exportProperty(
+    ol.source.XYZ.prototype,
+    'getProjection',
+    ol.source.XYZ.prototype.getProjection);
+
+goog.exportProperty(
+    ol.source.XYZ.prototype,
+    'getState',
+    ol.source.XYZ.prototype.getState);
+
+goog.exportProperty(
+    ol.source.XYZ.prototype,
+    'setAttributions',
+    ol.source.XYZ.prototype.setAttributions);
+
+goog.exportProperty(
+    ol.source.XYZ.prototype,
+    'get',
+    ol.source.XYZ.prototype.get);
+
+goog.exportProperty(
+    ol.source.XYZ.prototype,
+    'getKeys',
+    ol.source.XYZ.prototype.getKeys);
+
+goog.exportProperty(
+    ol.source.XYZ.prototype,
+    'getProperties',
+    ol.source.XYZ.prototype.getProperties);
+
+goog.exportProperty(
+    ol.source.XYZ.prototype,
+    'set',
+    ol.source.XYZ.prototype.set);
+
+goog.exportProperty(
+    ol.source.XYZ.prototype,
+    'setProperties',
+    ol.source.XYZ.prototype.setProperties);
+
+goog.exportProperty(
+    ol.source.XYZ.prototype,
+    'unset',
+    ol.source.XYZ.prototype.unset);
+
+goog.exportProperty(
+    ol.source.XYZ.prototype,
+    'changed',
+    ol.source.XYZ.prototype.changed);
+
+goog.exportProperty(
+    ol.source.XYZ.prototype,
+    'dispatchEvent',
+    ol.source.XYZ.prototype.dispatchEvent);
+
+goog.exportProperty(
+    ol.source.XYZ.prototype,
+    'getRevision',
+    ol.source.XYZ.prototype.getRevision);
+
+goog.exportProperty(
+    ol.source.XYZ.prototype,
+    'on',
+    ol.source.XYZ.prototype.on);
+
+goog.exportProperty(
+    ol.source.XYZ.prototype,
+    'once',
+    ol.source.XYZ.prototype.once);
+
+goog.exportProperty(
+    ol.source.XYZ.prototype,
+    'un',
+    ol.source.XYZ.prototype.un);
+
+goog.exportProperty(
+    ol.source.XYZ.prototype,
+    'unByKey',
+    ol.source.XYZ.prototype.unByKey);
+
+goog.exportProperty(
+    ol.source.CartoDB.prototype,
+    'setRenderReprojectionEdges',
+    ol.source.CartoDB.prototype.setRenderReprojectionEdges);
+
+goog.exportProperty(
+    ol.source.CartoDB.prototype,
+    'setTileGridForProjection',
+    ol.source.CartoDB.prototype.setTileGridForProjection);
+
+goog.exportProperty(
+    ol.source.CartoDB.prototype,
+    'getTileLoadFunction',
+    ol.source.CartoDB.prototype.getTileLoadFunction);
+
+goog.exportProperty(
+    ol.source.CartoDB.prototype,
+    'getTileUrlFunction',
+    ol.source.CartoDB.prototype.getTileUrlFunction);
+
+goog.exportProperty(
+    ol.source.CartoDB.prototype,
+    'getUrls',
+    ol.source.CartoDB.prototype.getUrls);
+
+goog.exportProperty(
+    ol.source.CartoDB.prototype,
+    'setTileLoadFunction',
+    ol.source.CartoDB.prototype.setTileLoadFunction);
+
+goog.exportProperty(
+    ol.source.CartoDB.prototype,
+    'setTileUrlFunction',
+    ol.source.CartoDB.prototype.setTileUrlFunction);
+
+goog.exportProperty(
+    ol.source.CartoDB.prototype,
+    'setUrl',
+    ol.source.CartoDB.prototype.setUrl);
+
+goog.exportProperty(
+    ol.source.CartoDB.prototype,
+    'setUrls',
+    ol.source.CartoDB.prototype.setUrls);
+
+goog.exportProperty(
+    ol.source.CartoDB.prototype,
+    'getTileGrid',
+    ol.source.CartoDB.prototype.getTileGrid);
+
+goog.exportProperty(
+    ol.source.CartoDB.prototype,
+    'refresh',
+    ol.source.CartoDB.prototype.refresh);
+
+goog.exportProperty(
+    ol.source.CartoDB.prototype,
+    'getAttributions',
+    ol.source.CartoDB.prototype.getAttributions);
+
+goog.exportProperty(
+    ol.source.CartoDB.prototype,
+    'getLogo',
+    ol.source.CartoDB.prototype.getLogo);
+
+goog.exportProperty(
+    ol.source.CartoDB.prototype,
+    'getProjection',
+    ol.source.CartoDB.prototype.getProjection);
+
+goog.exportProperty(
+    ol.source.CartoDB.prototype,
+    'getState',
+    ol.source.CartoDB.prototype.getState);
+
+goog.exportProperty(
+    ol.source.CartoDB.prototype,
+    'setAttributions',
+    ol.source.CartoDB.prototype.setAttributions);
+
+goog.exportProperty(
+    ol.source.CartoDB.prototype,
+    'get',
+    ol.source.CartoDB.prototype.get);
+
+goog.exportProperty(
+    ol.source.CartoDB.prototype,
+    'getKeys',
+    ol.source.CartoDB.prototype.getKeys);
+
+goog.exportProperty(
+    ol.source.CartoDB.prototype,
+    'getProperties',
+    ol.source.CartoDB.prototype.getProperties);
+
+goog.exportProperty(
+    ol.source.CartoDB.prototype,
+    'set',
+    ol.source.CartoDB.prototype.set);
+
+goog.exportProperty(
+    ol.source.CartoDB.prototype,
+    'setProperties',
+    ol.source.CartoDB.prototype.setProperties);
+
+goog.exportProperty(
+    ol.source.CartoDB.prototype,
+    'unset',
+    ol.source.CartoDB.prototype.unset);
+
+goog.exportProperty(
+    ol.source.CartoDB.prototype,
+    'changed',
+    ol.source.CartoDB.prototype.changed);
+
+goog.exportProperty(
+    ol.source.CartoDB.prototype,
+    'dispatchEvent',
+    ol.source.CartoDB.prototype.dispatchEvent);
+
+goog.exportProperty(
+    ol.source.CartoDB.prototype,
+    'getRevision',
+    ol.source.CartoDB.prototype.getRevision);
+
+goog.exportProperty(
+    ol.source.CartoDB.prototype,
+    'on',
+    ol.source.CartoDB.prototype.on);
+
+goog.exportProperty(
+    ol.source.CartoDB.prototype,
+    'once',
+    ol.source.CartoDB.prototype.once);
+
+goog.exportProperty(
+    ol.source.CartoDB.prototype,
+    'un',
+    ol.source.CartoDB.prototype.un);
+
+goog.exportProperty(
+    ol.source.CartoDB.prototype,
+    'unByKey',
+    ol.source.CartoDB.prototype.unByKey);
+
+goog.exportProperty(
+    ol.source.Vector.prototype,
+    'getAttributions',
+    ol.source.Vector.prototype.getAttributions);
+
+goog.exportProperty(
+    ol.source.Vector.prototype,
+    'getLogo',
+    ol.source.Vector.prototype.getLogo);
+
+goog.exportProperty(
+    ol.source.Vector.prototype,
+    'getProjection',
+    ol.source.Vector.prototype.getProjection);
+
+goog.exportProperty(
+    ol.source.Vector.prototype,
+    'getState',
+    ol.source.Vector.prototype.getState);
+
+goog.exportProperty(
+    ol.source.Vector.prototype,
+    'refresh',
+    ol.source.Vector.prototype.refresh);
+
+goog.exportProperty(
+    ol.source.Vector.prototype,
+    'setAttributions',
+    ol.source.Vector.prototype.setAttributions);
+
+goog.exportProperty(
+    ol.source.Vector.prototype,
+    'get',
+    ol.source.Vector.prototype.get);
+
+goog.exportProperty(
+    ol.source.Vector.prototype,
+    'getKeys',
+    ol.source.Vector.prototype.getKeys);
+
+goog.exportProperty(
+    ol.source.Vector.prototype,
+    'getProperties',
+    ol.source.Vector.prototype.getProperties);
+
+goog.exportProperty(
+    ol.source.Vector.prototype,
+    'set',
+    ol.source.Vector.prototype.set);
+
+goog.exportProperty(
+    ol.source.Vector.prototype,
+    'setProperties',
+    ol.source.Vector.prototype.setProperties);
+
+goog.exportProperty(
+    ol.source.Vector.prototype,
+    'unset',
+    ol.source.Vector.prototype.unset);
+
+goog.exportProperty(
+    ol.source.Vector.prototype,
+    'changed',
+    ol.source.Vector.prototype.changed);
+
+goog.exportProperty(
+    ol.source.Vector.prototype,
+    'dispatchEvent',
+    ol.source.Vector.prototype.dispatchEvent);
+
+goog.exportProperty(
+    ol.source.Vector.prototype,
+    'getRevision',
+    ol.source.Vector.prototype.getRevision);
+
+goog.exportProperty(
+    ol.source.Vector.prototype,
+    'on',
+    ol.source.Vector.prototype.on);
+
+goog.exportProperty(
+    ol.source.Vector.prototype,
+    'once',
+    ol.source.Vector.prototype.once);
+
+goog.exportProperty(
+    ol.source.Vector.prototype,
+    'un',
+    ol.source.Vector.prototype.un);
+
+goog.exportProperty(
+    ol.source.Vector.prototype,
+    'unByKey',
+    ol.source.Vector.prototype.unByKey);
+
+goog.exportProperty(
+    ol.source.Cluster.prototype,
+    'addFeature',
+    ol.source.Cluster.prototype.addFeature);
+
+goog.exportProperty(
+    ol.source.Cluster.prototype,
+    'addFeatures',
+    ol.source.Cluster.prototype.addFeatures);
+
+goog.exportProperty(
+    ol.source.Cluster.prototype,
+    'clear',
+    ol.source.Cluster.prototype.clear);
+
+goog.exportProperty(
+    ol.source.Cluster.prototype,
+    'forEachFeature',
+    ol.source.Cluster.prototype.forEachFeature);
+
+goog.exportProperty(
+    ol.source.Cluster.prototype,
+    'forEachFeatureInExtent',
+    ol.source.Cluster.prototype.forEachFeatureInExtent);
+
+goog.exportProperty(
+    ol.source.Cluster.prototype,
+    'forEachFeatureIntersectingExtent',
+    ol.source.Cluster.prototype.forEachFeatureIntersectingExtent);
+
+goog.exportProperty(
+    ol.source.Cluster.prototype,
+    'getFeaturesCollection',
+    ol.source.Cluster.prototype.getFeaturesCollection);
+
+goog.exportProperty(
+    ol.source.Cluster.prototype,
+    'getFeatures',
+    ol.source.Cluster.prototype.getFeatures);
+
+goog.exportProperty(
+    ol.source.Cluster.prototype,
+    'getFeaturesAtCoordinate',
+    ol.source.Cluster.prototype.getFeaturesAtCoordinate);
+
+goog.exportProperty(
+    ol.source.Cluster.prototype,
+    'getFeaturesInExtent',
+    ol.source.Cluster.prototype.getFeaturesInExtent);
+
+goog.exportProperty(
+    ol.source.Cluster.prototype,
+    'getClosestFeatureToCoordinate',
+    ol.source.Cluster.prototype.getClosestFeatureToCoordinate);
+
+goog.exportProperty(
+    ol.source.Cluster.prototype,
+    'getExtent',
+    ol.source.Cluster.prototype.getExtent);
+
+goog.exportProperty(
+    ol.source.Cluster.prototype,
+    'getFeatureById',
+    ol.source.Cluster.prototype.getFeatureById);
+
+goog.exportProperty(
+    ol.source.Cluster.prototype,
+    'getFormat',
+    ol.source.Cluster.prototype.getFormat);
+
+goog.exportProperty(
+    ol.source.Cluster.prototype,
+    'getUrl',
+    ol.source.Cluster.prototype.getUrl);
+
+goog.exportProperty(
+    ol.source.Cluster.prototype,
+    'removeFeature',
+    ol.source.Cluster.prototype.removeFeature);
+
+goog.exportProperty(
+    ol.source.Cluster.prototype,
+    'getAttributions',
+    ol.source.Cluster.prototype.getAttributions);
+
+goog.exportProperty(
+    ol.source.Cluster.prototype,
+    'getLogo',
+    ol.source.Cluster.prototype.getLogo);
+
+goog.exportProperty(
+    ol.source.Cluster.prototype,
+    'getProjection',
+    ol.source.Cluster.prototype.getProjection);
+
+goog.exportProperty(
+    ol.source.Cluster.prototype,
+    'getState',
+    ol.source.Cluster.prototype.getState);
+
+goog.exportProperty(
+    ol.source.Cluster.prototype,
+    'refresh',
+    ol.source.Cluster.prototype.refresh);
+
+goog.exportProperty(
+    ol.source.Cluster.prototype,
+    'setAttributions',
+    ol.source.Cluster.prototype.setAttributions);
+
+goog.exportProperty(
+    ol.source.Cluster.prototype,
+    'get',
+    ol.source.Cluster.prototype.get);
+
+goog.exportProperty(
+    ol.source.Cluster.prototype,
+    'getKeys',
+    ol.source.Cluster.prototype.getKeys);
+
+goog.exportProperty(
+    ol.source.Cluster.prototype,
+    'getProperties',
+    ol.source.Cluster.prototype.getProperties);
+
+goog.exportProperty(
+    ol.source.Cluster.prototype,
+    'set',
+    ol.source.Cluster.prototype.set);
+
+goog.exportProperty(
+    ol.source.Cluster.prototype,
+    'setProperties',
+    ol.source.Cluster.prototype.setProperties);
+
+goog.exportProperty(
+    ol.source.Cluster.prototype,
+    'unset',
+    ol.source.Cluster.prototype.unset);
+
+goog.exportProperty(
+    ol.source.Cluster.prototype,
+    'changed',
+    ol.source.Cluster.prototype.changed);
+
+goog.exportProperty(
+    ol.source.Cluster.prototype,
+    'dispatchEvent',
+    ol.source.Cluster.prototype.dispatchEvent);
+
+goog.exportProperty(
+    ol.source.Cluster.prototype,
+    'getRevision',
+    ol.source.Cluster.prototype.getRevision);
+
+goog.exportProperty(
+    ol.source.Cluster.prototype,
+    'on',
+    ol.source.Cluster.prototype.on);
+
+goog.exportProperty(
+    ol.source.Cluster.prototype,
+    'once',
+    ol.source.Cluster.prototype.once);
+
+goog.exportProperty(
+    ol.source.Cluster.prototype,
+    'un',
+    ol.source.Cluster.prototype.un);
+
+goog.exportProperty(
+    ol.source.Cluster.prototype,
+    'unByKey',
+    ol.source.Cluster.prototype.unByKey);
+
+goog.exportProperty(
+    ol.source.Image.prototype,
+    'getAttributions',
+    ol.source.Image.prototype.getAttributions);
+
+goog.exportProperty(
+    ol.source.Image.prototype,
+    'getLogo',
+    ol.source.Image.prototype.getLogo);
+
+goog.exportProperty(
+    ol.source.Image.prototype,
+    'getProjection',
+    ol.source.Image.prototype.getProjection);
+
+goog.exportProperty(
+    ol.source.Image.prototype,
+    'getState',
+    ol.source.Image.prototype.getState);
+
+goog.exportProperty(
+    ol.source.Image.prototype,
+    'refresh',
+    ol.source.Image.prototype.refresh);
+
+goog.exportProperty(
+    ol.source.Image.prototype,
+    'setAttributions',
+    ol.source.Image.prototype.setAttributions);
+
+goog.exportProperty(
+    ol.source.Image.prototype,
+    'get',
+    ol.source.Image.prototype.get);
+
+goog.exportProperty(
+    ol.source.Image.prototype,
+    'getKeys',
+    ol.source.Image.prototype.getKeys);
+
+goog.exportProperty(
+    ol.source.Image.prototype,
+    'getProperties',
+    ol.source.Image.prototype.getProperties);
+
+goog.exportProperty(
+    ol.source.Image.prototype,
+    'set',
+    ol.source.Image.prototype.set);
+
+goog.exportProperty(
+    ol.source.Image.prototype,
+    'setProperties',
+    ol.source.Image.prototype.setProperties);
+
+goog.exportProperty(
+    ol.source.Image.prototype,
+    'unset',
+    ol.source.Image.prototype.unset);
+
+goog.exportProperty(
+    ol.source.Image.prototype,
+    'changed',
+    ol.source.Image.prototype.changed);
+
+goog.exportProperty(
+    ol.source.Image.prototype,
+    'dispatchEvent',
+    ol.source.Image.prototype.dispatchEvent);
+
+goog.exportProperty(
+    ol.source.Image.prototype,
+    'getRevision',
+    ol.source.Image.prototype.getRevision);
+
+goog.exportProperty(
+    ol.source.Image.prototype,
+    'on',
+    ol.source.Image.prototype.on);
+
+goog.exportProperty(
+    ol.source.Image.prototype,
+    'once',
+    ol.source.Image.prototype.once);
+
+goog.exportProperty(
+    ol.source.Image.prototype,
+    'un',
+    ol.source.Image.prototype.un);
+
+goog.exportProperty(
+    ol.source.Image.prototype,
+    'unByKey',
+    ol.source.Image.prototype.unByKey);
+
+goog.exportProperty(
+    ol.source.ImageArcGISRest.prototype,
+    'getAttributions',
+    ol.source.ImageArcGISRest.prototype.getAttributions);
+
+goog.exportProperty(
+    ol.source.ImageArcGISRest.prototype,
+    'getLogo',
+    ol.source.ImageArcGISRest.prototype.getLogo);
+
+goog.exportProperty(
+    ol.source.ImageArcGISRest.prototype,
+    'getProjection',
+    ol.source.ImageArcGISRest.prototype.getProjection);
+
+goog.exportProperty(
+    ol.source.ImageArcGISRest.prototype,
+    'getState',
+    ol.source.ImageArcGISRest.prototype.getState);
+
+goog.exportProperty(
+    ol.source.ImageArcGISRest.prototype,
+    'refresh',
+    ol.source.ImageArcGISRest.prototype.refresh);
+
+goog.exportProperty(
+    ol.source.ImageArcGISRest.prototype,
+    'setAttributions',
+    ol.source.ImageArcGISRest.prototype.setAttributions);
+
+goog.exportProperty(
+    ol.source.ImageArcGISRest.prototype,
+    'get',
+    ol.source.ImageArcGISRest.prototype.get);
+
+goog.exportProperty(
+    ol.source.ImageArcGISRest.prototype,
+    'getKeys',
+    ol.source.ImageArcGISRest.prototype.getKeys);
+
+goog.exportProperty(
+    ol.source.ImageArcGISRest.prototype,
+    'getProperties',
+    ol.source.ImageArcGISRest.prototype.getProperties);
+
+goog.exportProperty(
+    ol.source.ImageArcGISRest.prototype,
+    'set',
+    ol.source.ImageArcGISRest.prototype.set);
+
+goog.exportProperty(
+    ol.source.ImageArcGISRest.prototype,
+    'setProperties',
+    ol.source.ImageArcGISRest.prototype.setProperties);
+
+goog.exportProperty(
+    ol.source.ImageArcGISRest.prototype,
+    'unset',
+    ol.source.ImageArcGISRest.prototype.unset);
+
+goog.exportProperty(
+    ol.source.ImageArcGISRest.prototype,
+    'changed',
+    ol.source.ImageArcGISRest.prototype.changed);
+
+goog.exportProperty(
+    ol.source.ImageArcGISRest.prototype,
+    'dispatchEvent',
+    ol.source.ImageArcGISRest.prototype.dispatchEvent);
+
+goog.exportProperty(
+    ol.source.ImageArcGISRest.prototype,
+    'getRevision',
+    ol.source.ImageArcGISRest.prototype.getRevision);
+
+goog.exportProperty(
+    ol.source.ImageArcGISRest.prototype,
+    'on',
+    ol.source.ImageArcGISRest.prototype.on);
+
+goog.exportProperty(
+    ol.source.ImageArcGISRest.prototype,
+    'once',
+    ol.source.ImageArcGISRest.prototype.once);
+
+goog.exportProperty(
+    ol.source.ImageArcGISRest.prototype,
+    'un',
+    ol.source.ImageArcGISRest.prototype.un);
+
+goog.exportProperty(
+    ol.source.ImageArcGISRest.prototype,
+    'unByKey',
+    ol.source.ImageArcGISRest.prototype.unByKey);
+
+goog.exportProperty(
+    ol.source.ImageCanvas.prototype,
+    'getAttributions',
+    ol.source.ImageCanvas.prototype.getAttributions);
+
+goog.exportProperty(
+    ol.source.ImageCanvas.prototype,
+    'getLogo',
+    ol.source.ImageCanvas.prototype.getLogo);
+
+goog.exportProperty(
+    ol.source.ImageCanvas.prototype,
+    'getProjection',
+    ol.source.ImageCanvas.prototype.getProjection);
+
+goog.exportProperty(
+    ol.source.ImageCanvas.prototype,
+    'getState',
+    ol.source.ImageCanvas.prototype.getState);
+
+goog.exportProperty(
+    ol.source.ImageCanvas.prototype,
+    'refresh',
+    ol.source.ImageCanvas.prototype.refresh);
+
+goog.exportProperty(
+    ol.source.ImageCanvas.prototype,
+    'setAttributions',
+    ol.source.ImageCanvas.prototype.setAttributions);
+
+goog.exportProperty(
+    ol.source.ImageCanvas.prototype,
+    'get',
+    ol.source.ImageCanvas.prototype.get);
+
+goog.exportProperty(
+    ol.source.ImageCanvas.prototype,
+    'getKeys',
+    ol.source.ImageCanvas.prototype.getKeys);
+
+goog.exportProperty(
+    ol.source.ImageCanvas.prototype,
+    'getProperties',
+    ol.source.ImageCanvas.prototype.getProperties);
+
+goog.exportProperty(
+    ol.source.ImageCanvas.prototype,
+    'set',
+    ol.source.ImageCanvas.prototype.set);
+
+goog.exportProperty(
+    ol.source.ImageCanvas.prototype,
+    'setProperties',
+    ol.source.ImageCanvas.prototype.setProperties);
+
+goog.exportProperty(
+    ol.source.ImageCanvas.prototype,
+    'unset',
+    ol.source.ImageCanvas.prototype.unset);
+
+goog.exportProperty(
+    ol.source.ImageCanvas.prototype,
+    'changed',
+    ol.source.ImageCanvas.prototype.changed);
+
+goog.exportProperty(
+    ol.source.ImageCanvas.prototype,
+    'dispatchEvent',
+    ol.source.ImageCanvas.prototype.dispatchEvent);
+
+goog.exportProperty(
+    ol.source.ImageCanvas.prototype,
+    'getRevision',
+    ol.source.ImageCanvas.prototype.getRevision);
+
+goog.exportProperty(
+    ol.source.ImageCanvas.prototype,
+    'on',
+    ol.source.ImageCanvas.prototype.on);
+
+goog.exportProperty(
+    ol.source.ImageCanvas.prototype,
+    'once',
+    ol.source.ImageCanvas.prototype.once);
+
+goog.exportProperty(
+    ol.source.ImageCanvas.prototype,
+    'un',
+    ol.source.ImageCanvas.prototype.un);
+
+goog.exportProperty(
+    ol.source.ImageCanvas.prototype,
+    'unByKey',
+    ol.source.ImageCanvas.prototype.unByKey);
+
+goog.exportProperty(
+    ol.source.ImageMapGuide.prototype,
+    'getAttributions',
+    ol.source.ImageMapGuide.prototype.getAttributions);
+
+goog.exportProperty(
+    ol.source.ImageMapGuide.prototype,
+    'getLogo',
+    ol.source.ImageMapGuide.prototype.getLogo);
+
+goog.exportProperty(
+    ol.source.ImageMapGuide.prototype,
+    'getProjection',
+    ol.source.ImageMapGuide.prototype.getProjection);
+
+goog.exportProperty(
+    ol.source.ImageMapGuide.prototype,
+    'getState',
+    ol.source.ImageMapGuide.prototype.getState);
+
+goog.exportProperty(
+    ol.source.ImageMapGuide.prototype,
+    'refresh',
+    ol.source.ImageMapGuide.prototype.refresh);
+
+goog.exportProperty(
+    ol.source.ImageMapGuide.prototype,
+    'setAttributions',
+    ol.source.ImageMapGuide.prototype.setAttributions);
+
+goog.exportProperty(
+    ol.source.ImageMapGuide.prototype,
+    'get',
+    ol.source.ImageMapGuide.prototype.get);
+
+goog.exportProperty(
+    ol.source.ImageMapGuide.prototype,
+    'getKeys',
+    ol.source.ImageMapGuide.prototype.getKeys);
+
+goog.exportProperty(
+    ol.source.ImageMapGuide.prototype,
+    'getProperties',
+    ol.source.ImageMapGuide.prototype.getProperties);
+
+goog.exportProperty(
+    ol.source.ImageMapGuide.prototype,
+    'set',
+    ol.source.ImageMapGuide.prototype.set);
+
+goog.exportProperty(
+    ol.source.ImageMapGuide.prototype,
+    'setProperties',
+    ol.source.ImageMapGuide.prototype.setProperties);
+
+goog.exportProperty(
+    ol.source.ImageMapGuide.prototype,
+    'unset',
+    ol.source.ImageMapGuide.prototype.unset);
+
+goog.exportProperty(
+    ol.source.ImageMapGuide.prototype,
+    'changed',
+    ol.source.ImageMapGuide.prototype.changed);
+
+goog.exportProperty(
+    ol.source.ImageMapGuide.prototype,
+    'dispatchEvent',
+    ol.source.ImageMapGuide.prototype.dispatchEvent);
+
+goog.exportProperty(
+    ol.source.ImageMapGuide.prototype,
+    'getRevision',
+    ol.source.ImageMapGuide.prototype.getRevision);
+
+goog.exportProperty(
+    ol.source.ImageMapGuide.prototype,
+    'on',
+    ol.source.ImageMapGuide.prototype.on);
+
+goog.exportProperty(
+    ol.source.ImageMapGuide.prototype,
+    'once',
+    ol.source.ImageMapGuide.prototype.once);
+
+goog.exportProperty(
+    ol.source.ImageMapGuide.prototype,
+    'un',
+    ol.source.ImageMapGuide.prototype.un);
+
+goog.exportProperty(
+    ol.source.ImageMapGuide.prototype,
+    'unByKey',
+    ol.source.ImageMapGuide.prototype.unByKey);
+
+goog.exportProperty(
+    ol.source.ImageEvent.prototype,
+    'type',
+    ol.source.ImageEvent.prototype.type);
+
+goog.exportProperty(
+    ol.source.ImageEvent.prototype,
+    'target',
+    ol.source.ImageEvent.prototype.target);
+
+goog.exportProperty(
+    ol.source.ImageEvent.prototype,
+    'preventDefault',
+    ol.source.ImageEvent.prototype.preventDefault);
+
+goog.exportProperty(
+    ol.source.ImageEvent.prototype,
+    'stopPropagation',
+    ol.source.ImageEvent.prototype.stopPropagation);
+
+goog.exportProperty(
+    ol.source.ImageStatic.prototype,
+    'getAttributions',
+    ol.source.ImageStatic.prototype.getAttributions);
+
+goog.exportProperty(
+    ol.source.ImageStatic.prototype,
+    'getLogo',
+    ol.source.ImageStatic.prototype.getLogo);
+
+goog.exportProperty(
+    ol.source.ImageStatic.prototype,
+    'getProjection',
+    ol.source.ImageStatic.prototype.getProjection);
+
+goog.exportProperty(
+    ol.source.ImageStatic.prototype,
+    'getState',
+    ol.source.ImageStatic.prototype.getState);
+
+goog.exportProperty(
+    ol.source.ImageStatic.prototype,
+    'refresh',
+    ol.source.ImageStatic.prototype.refresh);
+
+goog.exportProperty(
+    ol.source.ImageStatic.prototype,
+    'setAttributions',
+    ol.source.ImageStatic.prototype.setAttributions);
+
+goog.exportProperty(
+    ol.source.ImageStatic.prototype,
+    'get',
+    ol.source.ImageStatic.prototype.get);
+
+goog.exportProperty(
+    ol.source.ImageStatic.prototype,
+    'getKeys',
+    ol.source.ImageStatic.prototype.getKeys);
+
+goog.exportProperty(
+    ol.source.ImageStatic.prototype,
+    'getProperties',
+    ol.source.ImageStatic.prototype.getProperties);
+
+goog.exportProperty(
+    ol.source.ImageStatic.prototype,
+    'set',
+    ol.source.ImageStatic.prototype.set);
+
+goog.exportProperty(
+    ol.source.ImageStatic.prototype,
+    'setProperties',
+    ol.source.ImageStatic.prototype.setProperties);
+
+goog.exportProperty(
+    ol.source.ImageStatic.prototype,
+    'unset',
+    ol.source.ImageStatic.prototype.unset);
+
+goog.exportProperty(
+    ol.source.ImageStatic.prototype,
+    'changed',
+    ol.source.ImageStatic.prototype.changed);
+
+goog.exportProperty(
+    ol.source.ImageStatic.prototype,
+    'dispatchEvent',
+    ol.source.ImageStatic.prototype.dispatchEvent);
+
+goog.exportProperty(
+    ol.source.ImageStatic.prototype,
+    'getRevision',
+    ol.source.ImageStatic.prototype.getRevision);
+
+goog.exportProperty(
+    ol.source.ImageStatic.prototype,
+    'on',
+    ol.source.ImageStatic.prototype.on);
+
+goog.exportProperty(
+    ol.source.ImageStatic.prototype,
+    'once',
+    ol.source.ImageStatic.prototype.once);
+
+goog.exportProperty(
+    ol.source.ImageStatic.prototype,
+    'un',
+    ol.source.ImageStatic.prototype.un);
+
+goog.exportProperty(
+    ol.source.ImageStatic.prototype,
+    'unByKey',
+    ol.source.ImageStatic.prototype.unByKey);
+
+goog.exportProperty(
+    ol.source.ImageVector.prototype,
+    'getAttributions',
+    ol.source.ImageVector.prototype.getAttributions);
+
+goog.exportProperty(
+    ol.source.ImageVector.prototype,
+    'getLogo',
+    ol.source.ImageVector.prototype.getLogo);
+
+goog.exportProperty(
+    ol.source.ImageVector.prototype,
+    'getProjection',
+    ol.source.ImageVector.prototype.getProjection);
+
+goog.exportProperty(
+    ol.source.ImageVector.prototype,
+    'getState',
+    ol.source.ImageVector.prototype.getState);
+
+goog.exportProperty(
+    ol.source.ImageVector.prototype,
+    'refresh',
+    ol.source.ImageVector.prototype.refresh);
+
+goog.exportProperty(
+    ol.source.ImageVector.prototype,
+    'setAttributions',
+    ol.source.ImageVector.prototype.setAttributions);
+
+goog.exportProperty(
+    ol.source.ImageVector.prototype,
+    'get',
+    ol.source.ImageVector.prototype.get);
+
+goog.exportProperty(
+    ol.source.ImageVector.prototype,
+    'getKeys',
+    ol.source.ImageVector.prototype.getKeys);
+
+goog.exportProperty(
+    ol.source.ImageVector.prototype,
+    'getProperties',
+    ol.source.ImageVector.prototype.getProperties);
+
+goog.exportProperty(
+    ol.source.ImageVector.prototype,
+    'set',
+    ol.source.ImageVector.prototype.set);
+
+goog.exportProperty(
+    ol.source.ImageVector.prototype,
+    'setProperties',
+    ol.source.ImageVector.prototype.setProperties);
+
+goog.exportProperty(
+    ol.source.ImageVector.prototype,
+    'unset',
+    ol.source.ImageVector.prototype.unset);
+
+goog.exportProperty(
+    ol.source.ImageVector.prototype,
+    'changed',
+    ol.source.ImageVector.prototype.changed);
+
+goog.exportProperty(
+    ol.source.ImageVector.prototype,
+    'dispatchEvent',
+    ol.source.ImageVector.prototype.dispatchEvent);
+
+goog.exportProperty(
+    ol.source.ImageVector.prototype,
+    'getRevision',
+    ol.source.ImageVector.prototype.getRevision);
+
+goog.exportProperty(
+    ol.source.ImageVector.prototype,
+    'on',
+    ol.source.ImageVector.prototype.on);
+
+goog.exportProperty(
+    ol.source.ImageVector.prototype,
+    'once',
+    ol.source.ImageVector.prototype.once);
+
+goog.exportProperty(
+    ol.source.ImageVector.prototype,
+    'un',
+    ol.source.ImageVector.prototype.un);
+
+goog.exportProperty(
+    ol.source.ImageVector.prototype,
+    'unByKey',
+    ol.source.ImageVector.prototype.unByKey);
+
+goog.exportProperty(
+    ol.source.ImageWMS.prototype,
+    'getAttributions',
+    ol.source.ImageWMS.prototype.getAttributions);
+
+goog.exportProperty(
+    ol.source.ImageWMS.prototype,
+    'getLogo',
+    ol.source.ImageWMS.prototype.getLogo);
+
+goog.exportProperty(
+    ol.source.ImageWMS.prototype,
+    'getProjection',
+    ol.source.ImageWMS.prototype.getProjection);
+
+goog.exportProperty(
+    ol.source.ImageWMS.prototype,
+    'getState',
+    ol.source.ImageWMS.prototype.getState);
+
+goog.exportProperty(
+    ol.source.ImageWMS.prototype,
+    'refresh',
+    ol.source.ImageWMS.prototype.refresh);
+
+goog.exportProperty(
+    ol.source.ImageWMS.prototype,
+    'setAttributions',
+    ol.source.ImageWMS.prototype.setAttributions);
+
+goog.exportProperty(
+    ol.source.ImageWMS.prototype,
+    'get',
+    ol.source.ImageWMS.prototype.get);
+
+goog.exportProperty(
+    ol.source.ImageWMS.prototype,
+    'getKeys',
+    ol.source.ImageWMS.prototype.getKeys);
+
+goog.exportProperty(
+    ol.source.ImageWMS.prototype,
+    'getProperties',
+    ol.source.ImageWMS.prototype.getProperties);
+
+goog.exportProperty(
+    ol.source.ImageWMS.prototype,
+    'set',
+    ol.source.ImageWMS.prototype.set);
+
+goog.exportProperty(
+    ol.source.ImageWMS.prototype,
+    'setProperties',
+    ol.source.ImageWMS.prototype.setProperties);
+
+goog.exportProperty(
+    ol.source.ImageWMS.prototype,
+    'unset',
+    ol.source.ImageWMS.prototype.unset);
+
+goog.exportProperty(
+    ol.source.ImageWMS.prototype,
+    'changed',
+    ol.source.ImageWMS.prototype.changed);
+
+goog.exportProperty(
+    ol.source.ImageWMS.prototype,
+    'dispatchEvent',
+    ol.source.ImageWMS.prototype.dispatchEvent);
+
+goog.exportProperty(
+    ol.source.ImageWMS.prototype,
+    'getRevision',
+    ol.source.ImageWMS.prototype.getRevision);
+
+goog.exportProperty(
+    ol.source.ImageWMS.prototype,
+    'on',
+    ol.source.ImageWMS.prototype.on);
+
+goog.exportProperty(
+    ol.source.ImageWMS.prototype,
+    'once',
+    ol.source.ImageWMS.prototype.once);
+
+goog.exportProperty(
+    ol.source.ImageWMS.prototype,
+    'un',
+    ol.source.ImageWMS.prototype.un);
+
+goog.exportProperty(
+    ol.source.ImageWMS.prototype,
+    'unByKey',
+    ol.source.ImageWMS.prototype.unByKey);
+
+goog.exportProperty(
+    ol.source.OSM.prototype,
+    'setRenderReprojectionEdges',
+    ol.source.OSM.prototype.setRenderReprojectionEdges);
+
+goog.exportProperty(
+    ol.source.OSM.prototype,
+    'setTileGridForProjection',
+    ol.source.OSM.prototype.setTileGridForProjection);
+
+goog.exportProperty(
+    ol.source.OSM.prototype,
+    'getTileLoadFunction',
+    ol.source.OSM.prototype.getTileLoadFunction);
+
+goog.exportProperty(
+    ol.source.OSM.prototype,
+    'getTileUrlFunction',
+    ol.source.OSM.prototype.getTileUrlFunction);
+
+goog.exportProperty(
+    ol.source.OSM.prototype,
+    'getUrls',
+    ol.source.OSM.prototype.getUrls);
+
+goog.exportProperty(
+    ol.source.OSM.prototype,
+    'setTileLoadFunction',
+    ol.source.OSM.prototype.setTileLoadFunction);
+
+goog.exportProperty(
+    ol.source.OSM.prototype,
+    'setTileUrlFunction',
+    ol.source.OSM.prototype.setTileUrlFunction);
+
+goog.exportProperty(
+    ol.source.OSM.prototype,
+    'setUrl',
+    ol.source.OSM.prototype.setUrl);
+
+goog.exportProperty(
+    ol.source.OSM.prototype,
+    'setUrls',
+    ol.source.OSM.prototype.setUrls);
+
+goog.exportProperty(
+    ol.source.OSM.prototype,
+    'getTileGrid',
+    ol.source.OSM.prototype.getTileGrid);
+
+goog.exportProperty(
+    ol.source.OSM.prototype,
+    'refresh',
+    ol.source.OSM.prototype.refresh);
+
+goog.exportProperty(
+    ol.source.OSM.prototype,
+    'getAttributions',
+    ol.source.OSM.prototype.getAttributions);
+
+goog.exportProperty(
+    ol.source.OSM.prototype,
+    'getLogo',
+    ol.source.OSM.prototype.getLogo);
+
+goog.exportProperty(
+    ol.source.OSM.prototype,
+    'getProjection',
+    ol.source.OSM.prototype.getProjection);
+
+goog.exportProperty(
+    ol.source.OSM.prototype,
+    'getState',
+    ol.source.OSM.prototype.getState);
+
+goog.exportProperty(
+    ol.source.OSM.prototype,
+    'setAttributions',
+    ol.source.OSM.prototype.setAttributions);
+
+goog.exportProperty(
+    ol.source.OSM.prototype,
+    'get',
+    ol.source.OSM.prototype.get);
+
+goog.exportProperty(
+    ol.source.OSM.prototype,
+    'getKeys',
+    ol.source.OSM.prototype.getKeys);
+
+goog.exportProperty(
+    ol.source.OSM.prototype,
+    'getProperties',
+    ol.source.OSM.prototype.getProperties);
+
+goog.exportProperty(
+    ol.source.OSM.prototype,
+    'set',
+    ol.source.OSM.prototype.set);
+
+goog.exportProperty(
+    ol.source.OSM.prototype,
+    'setProperties',
+    ol.source.OSM.prototype.setProperties);
+
+goog.exportProperty(
+    ol.source.OSM.prototype,
+    'unset',
+    ol.source.OSM.prototype.unset);
+
+goog.exportProperty(
+    ol.source.OSM.prototype,
+    'changed',
+    ol.source.OSM.prototype.changed);
+
+goog.exportProperty(
+    ol.source.OSM.prototype,
+    'dispatchEvent',
+    ol.source.OSM.prototype.dispatchEvent);
+
+goog.exportProperty(
+    ol.source.OSM.prototype,
+    'getRevision',
+    ol.source.OSM.prototype.getRevision);
+
+goog.exportProperty(
+    ol.source.OSM.prototype,
+    'on',
+    ol.source.OSM.prototype.on);
+
+goog.exportProperty(
+    ol.source.OSM.prototype,
+    'once',
+    ol.source.OSM.prototype.once);
+
+goog.exportProperty(
+    ol.source.OSM.prototype,
+    'un',
+    ol.source.OSM.prototype.un);
+
+goog.exportProperty(
+    ol.source.OSM.prototype,
+    'unByKey',
+    ol.source.OSM.prototype.unByKey);
+
+goog.exportProperty(
+    ol.source.Raster.prototype,
+    'getAttributions',
+    ol.source.Raster.prototype.getAttributions);
+
+goog.exportProperty(
+    ol.source.Raster.prototype,
+    'getLogo',
+    ol.source.Raster.prototype.getLogo);
+
+goog.exportProperty(
+    ol.source.Raster.prototype,
+    'getProjection',
+    ol.source.Raster.prototype.getProjection);
+
+goog.exportProperty(
+    ol.source.Raster.prototype,
+    'getState',
+    ol.source.Raster.prototype.getState);
+
+goog.exportProperty(
+    ol.source.Raster.prototype,
+    'refresh',
+    ol.source.Raster.prototype.refresh);
+
+goog.exportProperty(
+    ol.source.Raster.prototype,
+    'setAttributions',
+    ol.source.Raster.prototype.setAttributions);
+
+goog.exportProperty(
+    ol.source.Raster.prototype,
+    'get',
+    ol.source.Raster.prototype.get);
+
+goog.exportProperty(
+    ol.source.Raster.prototype,
+    'getKeys',
+    ol.source.Raster.prototype.getKeys);
+
+goog.exportProperty(
+    ol.source.Raster.prototype,
+    'getProperties',
+    ol.source.Raster.prototype.getProperties);
+
+goog.exportProperty(
+    ol.source.Raster.prototype,
+    'set',
+    ol.source.Raster.prototype.set);
+
+goog.exportProperty(
+    ol.source.Raster.prototype,
+    'setProperties',
+    ol.source.Raster.prototype.setProperties);
+
+goog.exportProperty(
+    ol.source.Raster.prototype,
+    'unset',
+    ol.source.Raster.prototype.unset);
+
+goog.exportProperty(
+    ol.source.Raster.prototype,
+    'changed',
+    ol.source.Raster.prototype.changed);
+
+goog.exportProperty(
+    ol.source.Raster.prototype,
+    'dispatchEvent',
+    ol.source.Raster.prototype.dispatchEvent);
+
+goog.exportProperty(
+    ol.source.Raster.prototype,
+    'getRevision',
+    ol.source.Raster.prototype.getRevision);
+
+goog.exportProperty(
+    ol.source.Raster.prototype,
+    'on',
+    ol.source.Raster.prototype.on);
+
+goog.exportProperty(
+    ol.source.Raster.prototype,
+    'once',
+    ol.source.Raster.prototype.once);
+
+goog.exportProperty(
+    ol.source.Raster.prototype,
+    'un',
+    ol.source.Raster.prototype.un);
+
+goog.exportProperty(
+    ol.source.Raster.prototype,
+    'unByKey',
+    ol.source.Raster.prototype.unByKey);
+
+goog.exportProperty(
+    ol.source.RasterEvent.prototype,
+    'type',
+    ol.source.RasterEvent.prototype.type);
+
+goog.exportProperty(
+    ol.source.RasterEvent.prototype,
+    'target',
+    ol.source.RasterEvent.prototype.target);
+
+goog.exportProperty(
+    ol.source.RasterEvent.prototype,
+    'preventDefault',
+    ol.source.RasterEvent.prototype.preventDefault);
+
+goog.exportProperty(
+    ol.source.RasterEvent.prototype,
+    'stopPropagation',
+    ol.source.RasterEvent.prototype.stopPropagation);
+
+goog.exportProperty(
+    ol.source.Stamen.prototype,
+    'setRenderReprojectionEdges',
+    ol.source.Stamen.prototype.setRenderReprojectionEdges);
+
+goog.exportProperty(
+    ol.source.Stamen.prototype,
+    'setTileGridForProjection',
+    ol.source.Stamen.prototype.setTileGridForProjection);
+
+goog.exportProperty(
+    ol.source.Stamen.prototype,
+    'getTileLoadFunction',
+    ol.source.Stamen.prototype.getTileLoadFunction);
+
+goog.exportProperty(
+    ol.source.Stamen.prototype,
+    'getTileUrlFunction',
+    ol.source.Stamen.prototype.getTileUrlFunction);
+
+goog.exportProperty(
+    ol.source.Stamen.prototype,
+    'getUrls',
+    ol.source.Stamen.prototype.getUrls);
+
+goog.exportProperty(
+    ol.source.Stamen.prototype,
+    'setTileLoadFunction',
+    ol.source.Stamen.prototype.setTileLoadFunction);
+
+goog.exportProperty(
+    ol.source.Stamen.prototype,
+    'setTileUrlFunction',
+    ol.source.Stamen.prototype.setTileUrlFunction);
+
+goog.exportProperty(
+    ol.source.Stamen.prototype,
+    'setUrl',
+    ol.source.Stamen.prototype.setUrl);
+
+goog.exportProperty(
+    ol.source.Stamen.prototype,
+    'setUrls',
+    ol.source.Stamen.prototype.setUrls);
+
+goog.exportProperty(
+    ol.source.Stamen.prototype,
+    'getTileGrid',
+    ol.source.Stamen.prototype.getTileGrid);
+
+goog.exportProperty(
+    ol.source.Stamen.prototype,
+    'refresh',
+    ol.source.Stamen.prototype.refresh);
+
+goog.exportProperty(
+    ol.source.Stamen.prototype,
+    'getAttributions',
+    ol.source.Stamen.prototype.getAttributions);
+
+goog.exportProperty(
+    ol.source.Stamen.prototype,
+    'getLogo',
+    ol.source.Stamen.prototype.getLogo);
+
+goog.exportProperty(
+    ol.source.Stamen.prototype,
+    'getProjection',
+    ol.source.Stamen.prototype.getProjection);
+
+goog.exportProperty(
+    ol.source.Stamen.prototype,
+    'getState',
+    ol.source.Stamen.prototype.getState);
+
+goog.exportProperty(
+    ol.source.Stamen.prototype,
+    'setAttributions',
+    ol.source.Stamen.prototype.setAttributions);
+
+goog.exportProperty(
+    ol.source.Stamen.prototype,
+    'get',
+    ol.source.Stamen.prototype.get);
+
+goog.exportProperty(
+    ol.source.Stamen.prototype,
+    'getKeys',
+    ol.source.Stamen.prototype.getKeys);
+
+goog.exportProperty(
+    ol.source.Stamen.prototype,
+    'getProperties',
+    ol.source.Stamen.prototype.getProperties);
+
+goog.exportProperty(
+    ol.source.Stamen.prototype,
+    'set',
+    ol.source.Stamen.prototype.set);
+
+goog.exportProperty(
+    ol.source.Stamen.prototype,
+    'setProperties',
+    ol.source.Stamen.prototype.setProperties);
+
+goog.exportProperty(
+    ol.source.Stamen.prototype,
+    'unset',
+    ol.source.Stamen.prototype.unset);
+
+goog.exportProperty(
+    ol.source.Stamen.prototype,
+    'changed',
+    ol.source.Stamen.prototype.changed);
+
+goog.exportProperty(
+    ol.source.Stamen.prototype,
+    'dispatchEvent',
+    ol.source.Stamen.prototype.dispatchEvent);
+
+goog.exportProperty(
+    ol.source.Stamen.prototype,
+    'getRevision',
+    ol.source.Stamen.prototype.getRevision);
+
+goog.exportProperty(
+    ol.source.Stamen.prototype,
+    'on',
+    ol.source.Stamen.prototype.on);
+
+goog.exportProperty(
+    ol.source.Stamen.prototype,
+    'once',
+    ol.source.Stamen.prototype.once);
+
+goog.exportProperty(
+    ol.source.Stamen.prototype,
+    'un',
+    ol.source.Stamen.prototype.un);
+
+goog.exportProperty(
+    ol.source.Stamen.prototype,
+    'unByKey',
+    ol.source.Stamen.prototype.unByKey);
+
+goog.exportProperty(
+    ol.source.TileArcGISRest.prototype,
+    'setRenderReprojectionEdges',
+    ol.source.TileArcGISRest.prototype.setRenderReprojectionEdges);
+
+goog.exportProperty(
+    ol.source.TileArcGISRest.prototype,
+    'setTileGridForProjection',
+    ol.source.TileArcGISRest.prototype.setTileGridForProjection);
+
+goog.exportProperty(
+    ol.source.TileArcGISRest.prototype,
+    'getTileLoadFunction',
+    ol.source.TileArcGISRest.prototype.getTileLoadFunction);
+
+goog.exportProperty(
+    ol.source.TileArcGISRest.prototype,
+    'getTileUrlFunction',
+    ol.source.TileArcGISRest.prototype.getTileUrlFunction);
+
+goog.exportProperty(
+    ol.source.TileArcGISRest.prototype,
+    'getUrls',
+    ol.source.TileArcGISRest.prototype.getUrls);
+
+goog.exportProperty(
+    ol.source.TileArcGISRest.prototype,
+    'setTileLoadFunction',
+    ol.source.TileArcGISRest.prototype.setTileLoadFunction);
+
+goog.exportProperty(
+    ol.source.TileArcGISRest.prototype,
+    'setTileUrlFunction',
+    ol.source.TileArcGISRest.prototype.setTileUrlFunction);
+
+goog.exportProperty(
+    ol.source.TileArcGISRest.prototype,
+    'setUrl',
+    ol.source.TileArcGISRest.prototype.setUrl);
+
+goog.exportProperty(
+    ol.source.TileArcGISRest.prototype,
+    'setUrls',
+    ol.source.TileArcGISRest.prototype.setUrls);
+
+goog.exportProperty(
+    ol.source.TileArcGISRest.prototype,
+    'getTileGrid',
+    ol.source.TileArcGISRest.prototype.getTileGrid);
+
+goog.exportProperty(
+    ol.source.TileArcGISRest.prototype,
+    'refresh',
+    ol.source.TileArcGISRest.prototype.refresh);
+
+goog.exportProperty(
+    ol.source.TileArcGISRest.prototype,
+    'getAttributions',
+    ol.source.TileArcGISRest.prototype.getAttributions);
+
+goog.exportProperty(
+    ol.source.TileArcGISRest.prototype,
+    'getLogo',
+    ol.source.TileArcGISRest.prototype.getLogo);
+
+goog.exportProperty(
+    ol.source.TileArcGISRest.prototype,
+    'getProjection',
+    ol.source.TileArcGISRest.prototype.getProjection);
+
+goog.exportProperty(
+    ol.source.TileArcGISRest.prototype,
+    'getState',
+    ol.source.TileArcGISRest.prototype.getState);
+
+goog.exportProperty(
+    ol.source.TileArcGISRest.prototype,
+    'setAttributions',
+    ol.source.TileArcGISRest.prototype.setAttributions);
+
+goog.exportProperty(
+    ol.source.TileArcGISRest.prototype,
+    'get',
+    ol.source.TileArcGISRest.prototype.get);
+
+goog.exportProperty(
+    ol.source.TileArcGISRest.prototype,
+    'getKeys',
+    ol.source.TileArcGISRest.prototype.getKeys);
+
+goog.exportProperty(
+    ol.source.TileArcGISRest.prototype,
+    'getProperties',
+    ol.source.TileArcGISRest.prototype.getProperties);
+
+goog.exportProperty(
+    ol.source.TileArcGISRest.prototype,
+    'set',
+    ol.source.TileArcGISRest.prototype.set);
+
+goog.exportProperty(
+    ol.source.TileArcGISRest.prototype,
+    'setProperties',
+    ol.source.TileArcGISRest.prototype.setProperties);
+
+goog.exportProperty(
+    ol.source.TileArcGISRest.prototype,
+    'unset',
+    ol.source.TileArcGISRest.prototype.unset);
+
+goog.exportProperty(
+    ol.source.TileArcGISRest.prototype,
+    'changed',
+    ol.source.TileArcGISRest.prototype.changed);
+
+goog.exportProperty(
+    ol.source.TileArcGISRest.prototype,
+    'dispatchEvent',
+    ol.source.TileArcGISRest.prototype.dispatchEvent);
+
+goog.exportProperty(
+    ol.source.TileArcGISRest.prototype,
+    'getRevision',
+    ol.source.TileArcGISRest.prototype.getRevision);
+
+goog.exportProperty(
+    ol.source.TileArcGISRest.prototype,
+    'on',
+    ol.source.TileArcGISRest.prototype.on);
+
+goog.exportProperty(
+    ol.source.TileArcGISRest.prototype,
+    'once',
+    ol.source.TileArcGISRest.prototype.once);
+
+goog.exportProperty(
+    ol.source.TileArcGISRest.prototype,
+    'un',
+    ol.source.TileArcGISRest.prototype.un);
+
+goog.exportProperty(
+    ol.source.TileArcGISRest.prototype,
+    'unByKey',
+    ol.source.TileArcGISRest.prototype.unByKey);
+
+goog.exportProperty(
+    ol.source.TileDebug.prototype,
+    'getTileGrid',
+    ol.source.TileDebug.prototype.getTileGrid);
+
+goog.exportProperty(
+    ol.source.TileDebug.prototype,
+    'refresh',
+    ol.source.TileDebug.prototype.refresh);
+
+goog.exportProperty(
+    ol.source.TileDebug.prototype,
+    'getAttributions',
+    ol.source.TileDebug.prototype.getAttributions);
+
+goog.exportProperty(
+    ol.source.TileDebug.prototype,
+    'getLogo',
+    ol.source.TileDebug.prototype.getLogo);
+
+goog.exportProperty(
+    ol.source.TileDebug.prototype,
+    'getProjection',
+    ol.source.TileDebug.prototype.getProjection);
+
+goog.exportProperty(
+    ol.source.TileDebug.prototype,
+    'getState',
+    ol.source.TileDebug.prototype.getState);
+
+goog.exportProperty(
+    ol.source.TileDebug.prototype,
+    'setAttributions',
+    ol.source.TileDebug.prototype.setAttributions);
+
+goog.exportProperty(
+    ol.source.TileDebug.prototype,
+    'get',
+    ol.source.TileDebug.prototype.get);
+
+goog.exportProperty(
+    ol.source.TileDebug.prototype,
+    'getKeys',
+    ol.source.TileDebug.prototype.getKeys);
+
+goog.exportProperty(
+    ol.source.TileDebug.prototype,
+    'getProperties',
+    ol.source.TileDebug.prototype.getProperties);
+
+goog.exportProperty(
+    ol.source.TileDebug.prototype,
+    'set',
+    ol.source.TileDebug.prototype.set);
+
+goog.exportProperty(
+    ol.source.TileDebug.prototype,
+    'setProperties',
+    ol.source.TileDebug.prototype.setProperties);
+
+goog.exportProperty(
+    ol.source.TileDebug.prototype,
+    'unset',
+    ol.source.TileDebug.prototype.unset);
+
+goog.exportProperty(
+    ol.source.TileDebug.prototype,
+    'changed',
+    ol.source.TileDebug.prototype.changed);
+
+goog.exportProperty(
+    ol.source.TileDebug.prototype,
+    'dispatchEvent',
+    ol.source.TileDebug.prototype.dispatchEvent);
+
+goog.exportProperty(
+    ol.source.TileDebug.prototype,
+    'getRevision',
+    ol.source.TileDebug.prototype.getRevision);
+
+goog.exportProperty(
+    ol.source.TileDebug.prototype,
+    'on',
+    ol.source.TileDebug.prototype.on);
+
+goog.exportProperty(
+    ol.source.TileDebug.prototype,
+    'once',
+    ol.source.TileDebug.prototype.once);
+
+goog.exportProperty(
+    ol.source.TileDebug.prototype,
+    'un',
+    ol.source.TileDebug.prototype.un);
+
+goog.exportProperty(
+    ol.source.TileDebug.prototype,
+    'unByKey',
+    ol.source.TileDebug.prototype.unByKey);
+
+goog.exportProperty(
+    ol.source.TileJSON.prototype,
+    'setRenderReprojectionEdges',
+    ol.source.TileJSON.prototype.setRenderReprojectionEdges);
+
+goog.exportProperty(
+    ol.source.TileJSON.prototype,
+    'setTileGridForProjection',
+    ol.source.TileJSON.prototype.setTileGridForProjection);
+
+goog.exportProperty(
+    ol.source.TileJSON.prototype,
+    'getTileLoadFunction',
+    ol.source.TileJSON.prototype.getTileLoadFunction);
+
+goog.exportProperty(
+    ol.source.TileJSON.prototype,
+    'getTileUrlFunction',
+    ol.source.TileJSON.prototype.getTileUrlFunction);
+
+goog.exportProperty(
+    ol.source.TileJSON.prototype,
+    'getUrls',
+    ol.source.TileJSON.prototype.getUrls);
+
+goog.exportProperty(
+    ol.source.TileJSON.prototype,
+    'setTileLoadFunction',
+    ol.source.TileJSON.prototype.setTileLoadFunction);
+
+goog.exportProperty(
+    ol.source.TileJSON.prototype,
+    'setTileUrlFunction',
+    ol.source.TileJSON.prototype.setTileUrlFunction);
+
+goog.exportProperty(
+    ol.source.TileJSON.prototype,
+    'setUrl',
+    ol.source.TileJSON.prototype.setUrl);
+
+goog.exportProperty(
+    ol.source.TileJSON.prototype,
+    'setUrls',
+    ol.source.TileJSON.prototype.setUrls);
+
+goog.exportProperty(
+    ol.source.TileJSON.prototype,
+    'getTileGrid',
+    ol.source.TileJSON.prototype.getTileGrid);
+
+goog.exportProperty(
+    ol.source.TileJSON.prototype,
+    'refresh',
+    ol.source.TileJSON.prototype.refresh);
+
+goog.exportProperty(
+    ol.source.TileJSON.prototype,
+    'getAttributions',
+    ol.source.TileJSON.prototype.getAttributions);
+
+goog.exportProperty(
+    ol.source.TileJSON.prototype,
+    'getLogo',
+    ol.source.TileJSON.prototype.getLogo);
+
+goog.exportProperty(
+    ol.source.TileJSON.prototype,
+    'getProjection',
+    ol.source.TileJSON.prototype.getProjection);
+
+goog.exportProperty(
+    ol.source.TileJSON.prototype,
+    'getState',
+    ol.source.TileJSON.prototype.getState);
+
+goog.exportProperty(
+    ol.source.TileJSON.prototype,
+    'setAttributions',
+    ol.source.TileJSON.prototype.setAttributions);
+
+goog.exportProperty(
+    ol.source.TileJSON.prototype,
+    'get',
+    ol.source.TileJSON.prototype.get);
+
+goog.exportProperty(
+    ol.source.TileJSON.prototype,
+    'getKeys',
+    ol.source.TileJSON.prototype.getKeys);
+
+goog.exportProperty(
+    ol.source.TileJSON.prototype,
+    'getProperties',
+    ol.source.TileJSON.prototype.getProperties);
+
+goog.exportProperty(
+    ol.source.TileJSON.prototype,
+    'set',
+    ol.source.TileJSON.prototype.set);
+
+goog.exportProperty(
+    ol.source.TileJSON.prototype,
+    'setProperties',
+    ol.source.TileJSON.prototype.setProperties);
+
+goog.exportProperty(
+    ol.source.TileJSON.prototype,
+    'unset',
+    ol.source.TileJSON.prototype.unset);
+
+goog.exportProperty(
+    ol.source.TileJSON.prototype,
+    'changed',
+    ol.source.TileJSON.prototype.changed);
+
+goog.exportProperty(
+    ol.source.TileJSON.prototype,
+    'dispatchEvent',
+    ol.source.TileJSON.prototype.dispatchEvent);
+
+goog.exportProperty(
+    ol.source.TileJSON.prototype,
+    'getRevision',
+    ol.source.TileJSON.prototype.getRevision);
+
+goog.exportProperty(
+    ol.source.TileJSON.prototype,
+    'on',
+    ol.source.TileJSON.prototype.on);
+
+goog.exportProperty(
+    ol.source.TileJSON.prototype,
+    'once',
+    ol.source.TileJSON.prototype.once);
+
+goog.exportProperty(
+    ol.source.TileJSON.prototype,
+    'un',
+    ol.source.TileJSON.prototype.un);
+
+goog.exportProperty(
+    ol.source.TileJSON.prototype,
+    'unByKey',
+    ol.source.TileJSON.prototype.unByKey);
+
+goog.exportProperty(
+    ol.source.TileEvent.prototype,
+    'type',
+    ol.source.TileEvent.prototype.type);
+
+goog.exportProperty(
+    ol.source.TileEvent.prototype,
+    'target',
+    ol.source.TileEvent.prototype.target);
+
+goog.exportProperty(
+    ol.source.TileEvent.prototype,
+    'preventDefault',
+    ol.source.TileEvent.prototype.preventDefault);
+
+goog.exportProperty(
+    ol.source.TileEvent.prototype,
+    'stopPropagation',
+    ol.source.TileEvent.prototype.stopPropagation);
+
+goog.exportProperty(
+    ol.source.TileUTFGrid.prototype,
+    'getTileGrid',
+    ol.source.TileUTFGrid.prototype.getTileGrid);
+
+goog.exportProperty(
+    ol.source.TileUTFGrid.prototype,
+    'refresh',
+    ol.source.TileUTFGrid.prototype.refresh);
+
+goog.exportProperty(
+    ol.source.TileUTFGrid.prototype,
+    'getAttributions',
+    ol.source.TileUTFGrid.prototype.getAttributions);
+
+goog.exportProperty(
+    ol.source.TileUTFGrid.prototype,
+    'getLogo',
+    ol.source.TileUTFGrid.prototype.getLogo);
+
+goog.exportProperty(
+    ol.source.TileUTFGrid.prototype,
+    'getProjection',
+    ol.source.TileUTFGrid.prototype.getProjection);
+
+goog.exportProperty(
+    ol.source.TileUTFGrid.prototype,
+    'getState',
+    ol.source.TileUTFGrid.prototype.getState);
+
+goog.exportProperty(
+    ol.source.TileUTFGrid.prototype,
+    'setAttributions',
+    ol.source.TileUTFGrid.prototype.setAttributions);
+
+goog.exportProperty(
+    ol.source.TileUTFGrid.prototype,
+    'get',
+    ol.source.TileUTFGrid.prototype.get);
+
+goog.exportProperty(
+    ol.source.TileUTFGrid.prototype,
+    'getKeys',
+    ol.source.TileUTFGrid.prototype.getKeys);
+
+goog.exportProperty(
+    ol.source.TileUTFGrid.prototype,
+    'getProperties',
+    ol.source.TileUTFGrid.prototype.getProperties);
+
+goog.exportProperty(
+    ol.source.TileUTFGrid.prototype,
+    'set',
+    ol.source.TileUTFGrid.prototype.set);
+
+goog.exportProperty(
+    ol.source.TileUTFGrid.prototype,
+    'setProperties',
+    ol.source.TileUTFGrid.prototype.setProperties);
+
+goog.exportProperty(
+    ol.source.TileUTFGrid.prototype,
+    'unset',
+    ol.source.TileUTFGrid.prototype.unset);
+
+goog.exportProperty(
+    ol.source.TileUTFGrid.prototype,
+    'changed',
+    ol.source.TileUTFGrid.prototype.changed);
+
+goog.exportProperty(
+    ol.source.TileUTFGrid.prototype,
+    'dispatchEvent',
+    ol.source.TileUTFGrid.prototype.dispatchEvent);
+
+goog.exportProperty(
+    ol.source.TileUTFGrid.prototype,
+    'getRevision',
+    ol.source.TileUTFGrid.prototype.getRevision);
+
+goog.exportProperty(
+    ol.source.TileUTFGrid.prototype,
+    'on',
+    ol.source.TileUTFGrid.prototype.on);
+
+goog.exportProperty(
+    ol.source.TileUTFGrid.prototype,
+    'once',
+    ol.source.TileUTFGrid.prototype.once);
+
+goog.exportProperty(
+    ol.source.TileUTFGrid.prototype,
+    'un',
+    ol.source.TileUTFGrid.prototype.un);
+
+goog.exportProperty(
+    ol.source.TileUTFGrid.prototype,
+    'unByKey',
+    ol.source.TileUTFGrid.prototype.unByKey);
+
+goog.exportProperty(
+    ol.source.TileWMS.prototype,
+    'setRenderReprojectionEdges',
+    ol.source.TileWMS.prototype.setRenderReprojectionEdges);
+
+goog.exportProperty(
+    ol.source.TileWMS.prototype,
+    'setTileGridForProjection',
+    ol.source.TileWMS.prototype.setTileGridForProjection);
+
+goog.exportProperty(
+    ol.source.TileWMS.prototype,
+    'getTileLoadFunction',
+    ol.source.TileWMS.prototype.getTileLoadFunction);
+
+goog.exportProperty(
+    ol.source.TileWMS.prototype,
+    'getTileUrlFunction',
+    ol.source.TileWMS.prototype.getTileUrlFunction);
+
+goog.exportProperty(
+    ol.source.TileWMS.prototype,
+    'getUrls',
+    ol.source.TileWMS.prototype.getUrls);
+
+goog.exportProperty(
+    ol.source.TileWMS.prototype,
+    'setTileLoadFunction',
+    ol.source.TileWMS.prototype.setTileLoadFunction);
+
+goog.exportProperty(
+    ol.source.TileWMS.prototype,
+    'setTileUrlFunction',
+    ol.source.TileWMS.prototype.setTileUrlFunction);
+
+goog.exportProperty(
+    ol.source.TileWMS.prototype,
+    'setUrl',
+    ol.source.TileWMS.prototype.setUrl);
+
+goog.exportProperty(
+    ol.source.TileWMS.prototype,
+    'setUrls',
+    ol.source.TileWMS.prototype.setUrls);
+
+goog.exportProperty(
+    ol.source.TileWMS.prototype,
+    'getTileGrid',
+    ol.source.TileWMS.prototype.getTileGrid);
+
+goog.exportProperty(
+    ol.source.TileWMS.prototype,
+    'refresh',
+    ol.source.TileWMS.prototype.refresh);
+
+goog.exportProperty(
+    ol.source.TileWMS.prototype,
+    'getAttributions',
+    ol.source.TileWMS.prototype.getAttributions);
+
+goog.exportProperty(
+    ol.source.TileWMS.prototype,
+    'getLogo',
+    ol.source.TileWMS.prototype.getLogo);
+
+goog.exportProperty(
+    ol.source.TileWMS.prototype,
+    'getProjection',
+    ol.source.TileWMS.prototype.getProjection);
+
+goog.exportProperty(
+    ol.source.TileWMS.prototype,
+    'getState',
+    ol.source.TileWMS.prototype.getState);
+
+goog.exportProperty(
+    ol.source.TileWMS.prototype,
+    'setAttributions',
+    ol.source.TileWMS.prototype.setAttributions);
+
+goog.exportProperty(
+    ol.source.TileWMS.prototype,
+    'get',
+    ol.source.TileWMS.prototype.get);
+
+goog.exportProperty(
+    ol.source.TileWMS.prototype,
+    'getKeys',
+    ol.source.TileWMS.prototype.getKeys);
+
+goog.exportProperty(
+    ol.source.TileWMS.prototype,
+    'getProperties',
+    ol.source.TileWMS.prototype.getProperties);
+
+goog.exportProperty(
+    ol.source.TileWMS.prototype,
+    'set',
+    ol.source.TileWMS.prototype.set);
+
+goog.exportProperty(
+    ol.source.TileWMS.prototype,
+    'setProperties',
+    ol.source.TileWMS.prototype.setProperties);
+
+goog.exportProperty(
+    ol.source.TileWMS.prototype,
+    'unset',
+    ol.source.TileWMS.prototype.unset);
+
+goog.exportProperty(
+    ol.source.TileWMS.prototype,
+    'changed',
+    ol.source.TileWMS.prototype.changed);
+
+goog.exportProperty(
+    ol.source.TileWMS.prototype,
+    'dispatchEvent',
+    ol.source.TileWMS.prototype.dispatchEvent);
+
+goog.exportProperty(
+    ol.source.TileWMS.prototype,
+    'getRevision',
+    ol.source.TileWMS.prototype.getRevision);
+
+goog.exportProperty(
+    ol.source.TileWMS.prototype,
+    'on',
+    ol.source.TileWMS.prototype.on);
+
+goog.exportProperty(
+    ol.source.TileWMS.prototype,
+    'once',
+    ol.source.TileWMS.prototype.once);
+
+goog.exportProperty(
+    ol.source.TileWMS.prototype,
+    'un',
+    ol.source.TileWMS.prototype.un);
+
+goog.exportProperty(
+    ol.source.TileWMS.prototype,
+    'unByKey',
+    ol.source.TileWMS.prototype.unByKey);
+
+goog.exportProperty(
+    ol.source.VectorEvent.prototype,
+    'type',
+    ol.source.VectorEvent.prototype.type);
+
+goog.exportProperty(
+    ol.source.VectorEvent.prototype,
+    'target',
+    ol.source.VectorEvent.prototype.target);
+
+goog.exportProperty(
+    ol.source.VectorEvent.prototype,
+    'preventDefault',
+    ol.source.VectorEvent.prototype.preventDefault);
+
+goog.exportProperty(
+    ol.source.VectorEvent.prototype,
+    'stopPropagation',
+    ol.source.VectorEvent.prototype.stopPropagation);
+
+goog.exportProperty(
+    ol.source.VectorTile.prototype,
+    'getTileLoadFunction',
+    ol.source.VectorTile.prototype.getTileLoadFunction);
+
+goog.exportProperty(
+    ol.source.VectorTile.prototype,
+    'getTileUrlFunction',
+    ol.source.VectorTile.prototype.getTileUrlFunction);
+
+goog.exportProperty(
+    ol.source.VectorTile.prototype,
+    'getUrls',
+    ol.source.VectorTile.prototype.getUrls);
+
+goog.exportProperty(
+    ol.source.VectorTile.prototype,
+    'setTileLoadFunction',
+    ol.source.VectorTile.prototype.setTileLoadFunction);
+
+goog.exportProperty(
+    ol.source.VectorTile.prototype,
+    'setTileUrlFunction',
+    ol.source.VectorTile.prototype.setTileUrlFunction);
+
+goog.exportProperty(
+    ol.source.VectorTile.prototype,
+    'setUrl',
+    ol.source.VectorTile.prototype.setUrl);
+
+goog.exportProperty(
+    ol.source.VectorTile.prototype,
+    'setUrls',
+    ol.source.VectorTile.prototype.setUrls);
+
+goog.exportProperty(
+    ol.source.VectorTile.prototype,
+    'getTileGrid',
+    ol.source.VectorTile.prototype.getTileGrid);
+
+goog.exportProperty(
+    ol.source.VectorTile.prototype,
+    'refresh',
+    ol.source.VectorTile.prototype.refresh);
+
+goog.exportProperty(
+    ol.source.VectorTile.prototype,
+    'getAttributions',
+    ol.source.VectorTile.prototype.getAttributions);
+
+goog.exportProperty(
+    ol.source.VectorTile.prototype,
+    'getLogo',
+    ol.source.VectorTile.prototype.getLogo);
+
+goog.exportProperty(
+    ol.source.VectorTile.prototype,
+    'getProjection',
+    ol.source.VectorTile.prototype.getProjection);
+
+goog.exportProperty(
+    ol.source.VectorTile.prototype,
+    'getState',
+    ol.source.VectorTile.prototype.getState);
+
+goog.exportProperty(
+    ol.source.VectorTile.prototype,
+    'setAttributions',
+    ol.source.VectorTile.prototype.setAttributions);
+
+goog.exportProperty(
+    ol.source.VectorTile.prototype,
+    'get',
+    ol.source.VectorTile.prototype.get);
+
+goog.exportProperty(
+    ol.source.VectorTile.prototype,
+    'getKeys',
+    ol.source.VectorTile.prototype.getKeys);
+
+goog.exportProperty(
+    ol.source.VectorTile.prototype,
+    'getProperties',
+    ol.source.VectorTile.prototype.getProperties);
+
+goog.exportProperty(
+    ol.source.VectorTile.prototype,
+    'set',
+    ol.source.VectorTile.prototype.set);
+
+goog.exportProperty(
+    ol.source.VectorTile.prototype,
+    'setProperties',
+    ol.source.VectorTile.prototype.setProperties);
+
+goog.exportProperty(
+    ol.source.VectorTile.prototype,
+    'unset',
+    ol.source.VectorTile.prototype.unset);
+
+goog.exportProperty(
+    ol.source.VectorTile.prototype,
+    'changed',
+    ol.source.VectorTile.prototype.changed);
+
+goog.exportProperty(
+    ol.source.VectorTile.prototype,
+    'dispatchEvent',
+    ol.source.VectorTile.prototype.dispatchEvent);
+
+goog.exportProperty(
+    ol.source.VectorTile.prototype,
+    'getRevision',
+    ol.source.VectorTile.prototype.getRevision);
+
+goog.exportProperty(
+    ol.source.VectorTile.prototype,
+    'on',
+    ol.source.VectorTile.prototype.on);
+
+goog.exportProperty(
+    ol.source.VectorTile.prototype,
+    'once',
+    ol.source.VectorTile.prototype.once);
+
+goog.exportProperty(
+    ol.source.VectorTile.prototype,
+    'un',
+    ol.source.VectorTile.prototype.un);
+
+goog.exportProperty(
+    ol.source.VectorTile.prototype,
+    'unByKey',
+    ol.source.VectorTile.prototype.unByKey);
+
+goog.exportProperty(
+    ol.source.WMTS.prototype,
+    'setRenderReprojectionEdges',
+    ol.source.WMTS.prototype.setRenderReprojectionEdges);
+
+goog.exportProperty(
+    ol.source.WMTS.prototype,
+    'setTileGridForProjection',
+    ol.source.WMTS.prototype.setTileGridForProjection);
+
+goog.exportProperty(
+    ol.source.WMTS.prototype,
+    'getTileLoadFunction',
+    ol.source.WMTS.prototype.getTileLoadFunction);
+
+goog.exportProperty(
+    ol.source.WMTS.prototype,
+    'getTileUrlFunction',
+    ol.source.WMTS.prototype.getTileUrlFunction);
+
+goog.exportProperty(
+    ol.source.WMTS.prototype,
+    'getUrls',
+    ol.source.WMTS.prototype.getUrls);
+
+goog.exportProperty(
+    ol.source.WMTS.prototype,
+    'setTileLoadFunction',
+    ol.source.WMTS.prototype.setTileLoadFunction);
+
+goog.exportProperty(
+    ol.source.WMTS.prototype,
+    'setTileUrlFunction',
+    ol.source.WMTS.prototype.setTileUrlFunction);
+
+goog.exportProperty(
+    ol.source.WMTS.prototype,
+    'setUrl',
+    ol.source.WMTS.prototype.setUrl);
+
+goog.exportProperty(
+    ol.source.WMTS.prototype,
+    'setUrls',
+    ol.source.WMTS.prototype.setUrls);
+
+goog.exportProperty(
+    ol.source.WMTS.prototype,
+    'getTileGrid',
+    ol.source.WMTS.prototype.getTileGrid);
+
+goog.exportProperty(
+    ol.source.WMTS.prototype,
+    'refresh',
+    ol.source.WMTS.prototype.refresh);
+
+goog.exportProperty(
+    ol.source.WMTS.prototype,
+    'getAttributions',
+    ol.source.WMTS.prototype.getAttributions);
+
+goog.exportProperty(
+    ol.source.WMTS.prototype,
+    'getLogo',
+    ol.source.WMTS.prototype.getLogo);
+
+goog.exportProperty(
+    ol.source.WMTS.prototype,
+    'getProjection',
+    ol.source.WMTS.prototype.getProjection);
+
+goog.exportProperty(
+    ol.source.WMTS.prototype,
+    'getState',
+    ol.source.WMTS.prototype.getState);
+
+goog.exportProperty(
+    ol.source.WMTS.prototype,
+    'setAttributions',
+    ol.source.WMTS.prototype.setAttributions);
+
+goog.exportProperty(
+    ol.source.WMTS.prototype,
+    'get',
+    ol.source.WMTS.prototype.get);
+
+goog.exportProperty(
+    ol.source.WMTS.prototype,
+    'getKeys',
+    ol.source.WMTS.prototype.getKeys);
+
+goog.exportProperty(
+    ol.source.WMTS.prototype,
+    'getProperties',
+    ol.source.WMTS.prototype.getProperties);
+
+goog.exportProperty(
+    ol.source.WMTS.prototype,
+    'set',
+    ol.source.WMTS.prototype.set);
+
+goog.exportProperty(
+    ol.source.WMTS.prototype,
+    'setProperties',
+    ol.source.WMTS.prototype.setProperties);
+
+goog.exportProperty(
+    ol.source.WMTS.prototype,
+    'unset',
+    ol.source.WMTS.prototype.unset);
+
+goog.exportProperty(
+    ol.source.WMTS.prototype,
+    'changed',
+    ol.source.WMTS.prototype.changed);
+
+goog.exportProperty(
+    ol.source.WMTS.prototype,
+    'dispatchEvent',
+    ol.source.WMTS.prototype.dispatchEvent);
+
+goog.exportProperty(
+    ol.source.WMTS.prototype,
+    'getRevision',
+    ol.source.WMTS.prototype.getRevision);
+
+goog.exportProperty(
+    ol.source.WMTS.prototype,
+    'on',
+    ol.source.WMTS.prototype.on);
+
+goog.exportProperty(
+    ol.source.WMTS.prototype,
+    'once',
+    ol.source.WMTS.prototype.once);
+
+goog.exportProperty(
+    ol.source.WMTS.prototype,
+    'un',
+    ol.source.WMTS.prototype.un);
+
+goog.exportProperty(
+    ol.source.WMTS.prototype,
+    'unByKey',
+    ol.source.WMTS.prototype.unByKey);
+
+goog.exportProperty(
+    ol.source.Zoomify.prototype,
+    'setRenderReprojectionEdges',
+    ol.source.Zoomify.prototype.setRenderReprojectionEdges);
+
+goog.exportProperty(
+    ol.source.Zoomify.prototype,
+    'setTileGridForProjection',
+    ol.source.Zoomify.prototype.setTileGridForProjection);
+
+goog.exportProperty(
+    ol.source.Zoomify.prototype,
+    'getTileLoadFunction',
+    ol.source.Zoomify.prototype.getTileLoadFunction);
+
+goog.exportProperty(
+    ol.source.Zoomify.prototype,
+    'getTileUrlFunction',
+    ol.source.Zoomify.prototype.getTileUrlFunction);
+
+goog.exportProperty(
+    ol.source.Zoomify.prototype,
+    'getUrls',
+    ol.source.Zoomify.prototype.getUrls);
+
+goog.exportProperty(
+    ol.source.Zoomify.prototype,
+    'setTileLoadFunction',
+    ol.source.Zoomify.prototype.setTileLoadFunction);
+
+goog.exportProperty(
+    ol.source.Zoomify.prototype,
+    'setTileUrlFunction',
+    ol.source.Zoomify.prototype.setTileUrlFunction);
+
+goog.exportProperty(
+    ol.source.Zoomify.prototype,
+    'setUrl',
+    ol.source.Zoomify.prototype.setUrl);
+
+goog.exportProperty(
+    ol.source.Zoomify.prototype,
+    'setUrls',
+    ol.source.Zoomify.prototype.setUrls);
+
+goog.exportProperty(
+    ol.source.Zoomify.prototype,
+    'getTileGrid',
+    ol.source.Zoomify.prototype.getTileGrid);
+
+goog.exportProperty(
+    ol.source.Zoomify.prototype,
+    'refresh',
+    ol.source.Zoomify.prototype.refresh);
+
+goog.exportProperty(
+    ol.source.Zoomify.prototype,
+    'getAttributions',
+    ol.source.Zoomify.prototype.getAttributions);
+
+goog.exportProperty(
+    ol.source.Zoomify.prototype,
+    'getLogo',
+    ol.source.Zoomify.prototype.getLogo);
+
+goog.exportProperty(
+    ol.source.Zoomify.prototype,
+    'getProjection',
+    ol.source.Zoomify.prototype.getProjection);
+
+goog.exportProperty(
+    ol.source.Zoomify.prototype,
+    'getState',
+    ol.source.Zoomify.prototype.getState);
+
+goog.exportProperty(
+    ol.source.Zoomify.prototype,
+    'setAttributions',
+    ol.source.Zoomify.prototype.setAttributions);
+
+goog.exportProperty(
+    ol.source.Zoomify.prototype,
+    'get',
+    ol.source.Zoomify.prototype.get);
+
+goog.exportProperty(
+    ol.source.Zoomify.prototype,
+    'getKeys',
+    ol.source.Zoomify.prototype.getKeys);
+
+goog.exportProperty(
+    ol.source.Zoomify.prototype,
+    'getProperties',
+    ol.source.Zoomify.prototype.getProperties);
+
+goog.exportProperty(
+    ol.source.Zoomify.prototype,
+    'set',
+    ol.source.Zoomify.prototype.set);
+
+goog.exportProperty(
+    ol.source.Zoomify.prototype,
+    'setProperties',
+    ol.source.Zoomify.prototype.setProperties);
+
+goog.exportProperty(
+    ol.source.Zoomify.prototype,
+    'unset',
+    ol.source.Zoomify.prototype.unset);
+
+goog.exportProperty(
+    ol.source.Zoomify.prototype,
+    'changed',
+    ol.source.Zoomify.prototype.changed);
+
+goog.exportProperty(
+    ol.source.Zoomify.prototype,
+    'dispatchEvent',
+    ol.source.Zoomify.prototype.dispatchEvent);
+
+goog.exportProperty(
+    ol.source.Zoomify.prototype,
+    'getRevision',
+    ol.source.Zoomify.prototype.getRevision);
+
+goog.exportProperty(
+    ol.source.Zoomify.prototype,
+    'on',
+    ol.source.Zoomify.prototype.on);
+
+goog.exportProperty(
+    ol.source.Zoomify.prototype,
+    'once',
+    ol.source.Zoomify.prototype.once);
+
+goog.exportProperty(
+    ol.source.Zoomify.prototype,
+    'un',
+    ol.source.Zoomify.prototype.un);
+
+goog.exportProperty(
+    ol.source.Zoomify.prototype,
+    'unByKey',
+    ol.source.Zoomify.prototype.unByKey);
+
+goog.exportProperty(
+    ol.reproj.Tile.prototype,
+    'getTileCoord',
+    ol.reproj.Tile.prototype.getTileCoord);
+
+goog.exportProperty(
+    ol.reproj.Tile.prototype,
+    'load',
+    ol.reproj.Tile.prototype.load);
+
+goog.exportProperty(
+    ol.renderer.Layer.prototype,
+    'changed',
+    ol.renderer.Layer.prototype.changed);
+
+goog.exportProperty(
+    ol.renderer.Layer.prototype,
+    'dispatchEvent',
+    ol.renderer.Layer.prototype.dispatchEvent);
+
+goog.exportProperty(
+    ol.renderer.Layer.prototype,
+    'getRevision',
+    ol.renderer.Layer.prototype.getRevision);
+
+goog.exportProperty(
+    ol.renderer.Layer.prototype,
+    'on',
+    ol.renderer.Layer.prototype.on);
+
+goog.exportProperty(
+    ol.renderer.Layer.prototype,
+    'once',
+    ol.renderer.Layer.prototype.once);
+
+goog.exportProperty(
+    ol.renderer.Layer.prototype,
+    'un',
+    ol.renderer.Layer.prototype.un);
+
+goog.exportProperty(
+    ol.renderer.Layer.prototype,
+    'unByKey',
+    ol.renderer.Layer.prototype.unByKey);
+
+goog.exportProperty(
+    ol.renderer.webgl.Layer.prototype,
+    'changed',
+    ol.renderer.webgl.Layer.prototype.changed);
+
+goog.exportProperty(
+    ol.renderer.webgl.Layer.prototype,
+    'dispatchEvent',
+    ol.renderer.webgl.Layer.prototype.dispatchEvent);
+
+goog.exportProperty(
+    ol.renderer.webgl.Layer.prototype,
+    'getRevision',
+    ol.renderer.webgl.Layer.prototype.getRevision);
+
+goog.exportProperty(
+    ol.renderer.webgl.Layer.prototype,
+    'on',
+    ol.renderer.webgl.Layer.prototype.on);
+
+goog.exportProperty(
+    ol.renderer.webgl.Layer.prototype,
+    'once',
+    ol.renderer.webgl.Layer.prototype.once);
+
+goog.exportProperty(
+    ol.renderer.webgl.Layer.prototype,
+    'un',
+    ol.renderer.webgl.Layer.prototype.un);
+
+goog.exportProperty(
+    ol.renderer.webgl.Layer.prototype,
+    'unByKey',
+    ol.renderer.webgl.Layer.prototype.unByKey);
+
+goog.exportProperty(
+    ol.renderer.webgl.ImageLayer.prototype,
+    'changed',
+    ol.renderer.webgl.ImageLayer.prototype.changed);
+
+goog.exportProperty(
+    ol.renderer.webgl.ImageLayer.prototype,
+    'dispatchEvent',
+    ol.renderer.webgl.ImageLayer.prototype.dispatchEvent);
+
+goog.exportProperty(
+    ol.renderer.webgl.ImageLayer.prototype,
+    'getRevision',
+    ol.renderer.webgl.ImageLayer.prototype.getRevision);
+
+goog.exportProperty(
+    ol.renderer.webgl.ImageLayer.prototype,
+    'on',
+    ol.renderer.webgl.ImageLayer.prototype.on);
+
+goog.exportProperty(
+    ol.renderer.webgl.ImageLayer.prototype,
+    'once',
+    ol.renderer.webgl.ImageLayer.prototype.once);
+
+goog.exportProperty(
+    ol.renderer.webgl.ImageLayer.prototype,
+    'un',
+    ol.renderer.webgl.ImageLayer.prototype.un);
+
+goog.exportProperty(
+    ol.renderer.webgl.ImageLayer.prototype,
+    'unByKey',
+    ol.renderer.webgl.ImageLayer.prototype.unByKey);
+
+goog.exportProperty(
+    ol.renderer.webgl.TileLayer.prototype,
+    'changed',
+    ol.renderer.webgl.TileLayer.prototype.changed);
+
+goog.exportProperty(
+    ol.renderer.webgl.TileLayer.prototype,
+    'dispatchEvent',
+    ol.renderer.webgl.TileLayer.prototype.dispatchEvent);
+
+goog.exportProperty(
+    ol.renderer.webgl.TileLayer.prototype,
+    'getRevision',
+    ol.renderer.webgl.TileLayer.prototype.getRevision);
+
+goog.exportProperty(
+    ol.renderer.webgl.TileLayer.prototype,
+    'on',
+    ol.renderer.webgl.TileLayer.prototype.on);
+
+goog.exportProperty(
+    ol.renderer.webgl.TileLayer.prototype,
+    'once',
+    ol.renderer.webgl.TileLayer.prototype.once);
+
+goog.exportProperty(
+    ol.renderer.webgl.TileLayer.prototype,
+    'un',
+    ol.renderer.webgl.TileLayer.prototype.un);
+
+goog.exportProperty(
+    ol.renderer.webgl.TileLayer.prototype,
+    'unByKey',
+    ol.renderer.webgl.TileLayer.prototype.unByKey);
+
+goog.exportProperty(
+    ol.renderer.webgl.VectorLayer.prototype,
+    'changed',
+    ol.renderer.webgl.VectorLayer.prototype.changed);
+
+goog.exportProperty(
+    ol.renderer.webgl.VectorLayer.prototype,
+    'dispatchEvent',
+    ol.renderer.webgl.VectorLayer.prototype.dispatchEvent);
+
+goog.exportProperty(
+    ol.renderer.webgl.VectorLayer.prototype,
+    'getRevision',
+    ol.renderer.webgl.VectorLayer.prototype.getRevision);
+
+goog.exportProperty(
+    ol.renderer.webgl.VectorLayer.prototype,
+    'on',
+    ol.renderer.webgl.VectorLayer.prototype.on);
+
+goog.exportProperty(
+    ol.renderer.webgl.VectorLayer.prototype,
+    'once',
+    ol.renderer.webgl.VectorLayer.prototype.once);
+
+goog.exportProperty(
+    ol.renderer.webgl.VectorLayer.prototype,
+    'un',
+    ol.renderer.webgl.VectorLayer.prototype.un);
+
+goog.exportProperty(
+    ol.renderer.webgl.VectorLayer.prototype,
+    'unByKey',
+    ol.renderer.webgl.VectorLayer.prototype.unByKey);
+
+goog.exportProperty(
+    ol.renderer.dom.Layer.prototype,
+    'changed',
+    ol.renderer.dom.Layer.prototype.changed);
+
+goog.exportProperty(
+    ol.renderer.dom.Layer.prototype,
+    'dispatchEvent',
+    ol.renderer.dom.Layer.prototype.dispatchEvent);
+
+goog.exportProperty(
+    ol.renderer.dom.Layer.prototype,
+    'getRevision',
+    ol.renderer.dom.Layer.prototype.getRevision);
+
+goog.exportProperty(
+    ol.renderer.dom.Layer.prototype,
+    'on',
+    ol.renderer.dom.Layer.prototype.on);
+
+goog.exportProperty(
+    ol.renderer.dom.Layer.prototype,
+    'once',
+    ol.renderer.dom.Layer.prototype.once);
+
+goog.exportProperty(
+    ol.renderer.dom.Layer.prototype,
+    'un',
+    ol.renderer.dom.Layer.prototype.un);
+
+goog.exportProperty(
+    ol.renderer.dom.Layer.prototype,
+    'unByKey',
+    ol.renderer.dom.Layer.prototype.unByKey);
+
+goog.exportProperty(
+    ol.renderer.dom.ImageLayer.prototype,
+    'changed',
+    ol.renderer.dom.ImageLayer.prototype.changed);
+
+goog.exportProperty(
+    ol.renderer.dom.ImageLayer.prototype,
+    'dispatchEvent',
+    ol.renderer.dom.ImageLayer.prototype.dispatchEvent);
+
+goog.exportProperty(
+    ol.renderer.dom.ImageLayer.prototype,
+    'getRevision',
+    ol.renderer.dom.ImageLayer.prototype.getRevision);
+
+goog.exportProperty(
+    ol.renderer.dom.ImageLayer.prototype,
+    'on',
+    ol.renderer.dom.ImageLayer.prototype.on);
+
+goog.exportProperty(
+    ol.renderer.dom.ImageLayer.prototype,
+    'once',
+    ol.renderer.dom.ImageLayer.prototype.once);
+
+goog.exportProperty(
+    ol.renderer.dom.ImageLayer.prototype,
+    'un',
+    ol.renderer.dom.ImageLayer.prototype.un);
+
+goog.exportProperty(
+    ol.renderer.dom.ImageLayer.prototype,
+    'unByKey',
+    ol.renderer.dom.ImageLayer.prototype.unByKey);
+
+goog.exportProperty(
+    ol.renderer.dom.TileLayer.prototype,
+    'changed',
+    ol.renderer.dom.TileLayer.prototype.changed);
+
+goog.exportProperty(
+    ol.renderer.dom.TileLayer.prototype,
+    'dispatchEvent',
+    ol.renderer.dom.TileLayer.prototype.dispatchEvent);
+
+goog.exportProperty(
+    ol.renderer.dom.TileLayer.prototype,
+    'getRevision',
+    ol.renderer.dom.TileLayer.prototype.getRevision);
+
+goog.exportProperty(
+    ol.renderer.dom.TileLayer.prototype,
+    'on',
+    ol.renderer.dom.TileLayer.prototype.on);
+
+goog.exportProperty(
+    ol.renderer.dom.TileLayer.prototype,
+    'once',
+    ol.renderer.dom.TileLayer.prototype.once);
+
+goog.exportProperty(
+    ol.renderer.dom.TileLayer.prototype,
+    'un',
+    ol.renderer.dom.TileLayer.prototype.un);
+
+goog.exportProperty(
+    ol.renderer.dom.TileLayer.prototype,
+    'unByKey',
+    ol.renderer.dom.TileLayer.prototype.unByKey);
+
+goog.exportProperty(
+    ol.renderer.dom.VectorLayer.prototype,
+    'changed',
+    ol.renderer.dom.VectorLayer.prototype.changed);
+
+goog.exportProperty(
+    ol.renderer.dom.VectorLayer.prototype,
+    'dispatchEvent',
+    ol.renderer.dom.VectorLayer.prototype.dispatchEvent);
+
+goog.exportProperty(
+    ol.renderer.dom.VectorLayer.prototype,
+    'getRevision',
+    ol.renderer.dom.VectorLayer.prototype.getRevision);
+
+goog.exportProperty(
+    ol.renderer.dom.VectorLayer.prototype,
+    'on',
+    ol.renderer.dom.VectorLayer.prototype.on);
+
+goog.exportProperty(
+    ol.renderer.dom.VectorLayer.prototype,
+    'once',
+    ol.renderer.dom.VectorLayer.prototype.once);
+
+goog.exportProperty(
+    ol.renderer.dom.VectorLayer.prototype,
+    'un',
+    ol.renderer.dom.VectorLayer.prototype.un);
+
+goog.exportProperty(
+    ol.renderer.dom.VectorLayer.prototype,
+    'unByKey',
+    ol.renderer.dom.VectorLayer.prototype.unByKey);
+
+goog.exportProperty(
+    ol.renderer.canvas.Layer.prototype,
+    'changed',
+    ol.renderer.canvas.Layer.prototype.changed);
+
+goog.exportProperty(
+    ol.renderer.canvas.Layer.prototype,
+    'dispatchEvent',
+    ol.renderer.canvas.Layer.prototype.dispatchEvent);
+
+goog.exportProperty(
+    ol.renderer.canvas.Layer.prototype,
+    'getRevision',
+    ol.renderer.canvas.Layer.prototype.getRevision);
+
+goog.exportProperty(
+    ol.renderer.canvas.Layer.prototype,
+    'on',
+    ol.renderer.canvas.Layer.prototype.on);
+
+goog.exportProperty(
+    ol.renderer.canvas.Layer.prototype,
+    'once',
+    ol.renderer.canvas.Layer.prototype.once);
+
+goog.exportProperty(
+    ol.renderer.canvas.Layer.prototype,
+    'un',
+    ol.renderer.canvas.Layer.prototype.un);
+
+goog.exportProperty(
+    ol.renderer.canvas.Layer.prototype,
+    'unByKey',
+    ol.renderer.canvas.Layer.prototype.unByKey);
+
+goog.exportProperty(
+    ol.renderer.canvas.ImageLayer.prototype,
+    'changed',
+    ol.renderer.canvas.ImageLayer.prototype.changed);
+
+goog.exportProperty(
+    ol.renderer.canvas.ImageLayer.prototype,
+    'dispatchEvent',
+    ol.renderer.canvas.ImageLayer.prototype.dispatchEvent);
+
+goog.exportProperty(
+    ol.renderer.canvas.ImageLayer.prototype,
+    'getRevision',
+    ol.renderer.canvas.ImageLayer.prototype.getRevision);
+
+goog.exportProperty(
+    ol.renderer.canvas.ImageLayer.prototype,
+    'on',
+    ol.renderer.canvas.ImageLayer.prototype.on);
+
+goog.exportProperty(
+    ol.renderer.canvas.ImageLayer.prototype,
+    'once',
+    ol.renderer.canvas.ImageLayer.prototype.once);
+
+goog.exportProperty(
+    ol.renderer.canvas.ImageLayer.prototype,
+    'un',
+    ol.renderer.canvas.ImageLayer.prototype.un);
+
+goog.exportProperty(
+    ol.renderer.canvas.ImageLayer.prototype,
+    'unByKey',
+    ol.renderer.canvas.ImageLayer.prototype.unByKey);
+
+goog.exportProperty(
+    ol.renderer.canvas.TileLayer.prototype,
+    'changed',
+    ol.renderer.canvas.TileLayer.prototype.changed);
+
+goog.exportProperty(
+    ol.renderer.canvas.TileLayer.prototype,
+    'dispatchEvent',
+    ol.renderer.canvas.TileLayer.prototype.dispatchEvent);
+
+goog.exportProperty(
+    ol.renderer.canvas.TileLayer.prototype,
+    'getRevision',
+    ol.renderer.canvas.TileLayer.prototype.getRevision);
+
+goog.exportProperty(
+    ol.renderer.canvas.TileLayer.prototype,
+    'on',
+    ol.renderer.canvas.TileLayer.prototype.on);
+
+goog.exportProperty(
+    ol.renderer.canvas.TileLayer.prototype,
+    'once',
+    ol.renderer.canvas.TileLayer.prototype.once);
+
+goog.exportProperty(
+    ol.renderer.canvas.TileLayer.prototype,
+    'un',
+    ol.renderer.canvas.TileLayer.prototype.un);
+
+goog.exportProperty(
+    ol.renderer.canvas.TileLayer.prototype,
+    'unByKey',
+    ol.renderer.canvas.TileLayer.prototype.unByKey);
+
+goog.exportProperty(
+    ol.renderer.canvas.VectorLayer.prototype,
+    'changed',
+    ol.renderer.canvas.VectorLayer.prototype.changed);
+
+goog.exportProperty(
+    ol.renderer.canvas.VectorLayer.prototype,
+    'dispatchEvent',
+    ol.renderer.canvas.VectorLayer.prototype.dispatchEvent);
+
+goog.exportProperty(
+    ol.renderer.canvas.VectorLayer.prototype,
+    'getRevision',
+    ol.renderer.canvas.VectorLayer.prototype.getRevision);
+
+goog.exportProperty(
+    ol.renderer.canvas.VectorLayer.prototype,
+    'on',
+    ol.renderer.canvas.VectorLayer.prototype.on);
+
+goog.exportProperty(
+    ol.renderer.canvas.VectorLayer.prototype,
+    'once',
+    ol.renderer.canvas.VectorLayer.prototype.once);
+
+goog.exportProperty(
+    ol.renderer.canvas.VectorLayer.prototype,
+    'un',
+    ol.renderer.canvas.VectorLayer.prototype.un);
+
+goog.exportProperty(
+    ol.renderer.canvas.VectorLayer.prototype,
+    'unByKey',
+    ol.renderer.canvas.VectorLayer.prototype.unByKey);
+
+goog.exportProperty(
+    ol.renderer.canvas.VectorTileLayer.prototype,
+    'changed',
+    ol.renderer.canvas.VectorTileLayer.prototype.changed);
+
+goog.exportProperty(
+    ol.renderer.canvas.VectorTileLayer.prototype,
+    'dispatchEvent',
+    ol.renderer.canvas.VectorTileLayer.prototype.dispatchEvent);
+
+goog.exportProperty(
+    ol.renderer.canvas.VectorTileLayer.prototype,
+    'getRevision',
+    ol.renderer.canvas.VectorTileLayer.prototype.getRevision);
+
+goog.exportProperty(
+    ol.renderer.canvas.VectorTileLayer.prototype,
+    'on',
+    ol.renderer.canvas.VectorTileLayer.prototype.on);
+
+goog.exportProperty(
+    ol.renderer.canvas.VectorTileLayer.prototype,
+    'once',
+    ol.renderer.canvas.VectorTileLayer.prototype.once);
+
+goog.exportProperty(
+    ol.renderer.canvas.VectorTileLayer.prototype,
+    'un',
+    ol.renderer.canvas.VectorTileLayer.prototype.un);
+
+goog.exportProperty(
+    ol.renderer.canvas.VectorTileLayer.prototype,
+    'unByKey',
+    ol.renderer.canvas.VectorTileLayer.prototype.unByKey);
+
+goog.exportProperty(
+    ol.render.Event.prototype,
+    'type',
+    ol.render.Event.prototype.type);
+
+goog.exportProperty(
+    ol.render.Event.prototype,
+    'target',
+    ol.render.Event.prototype.target);
+
+goog.exportProperty(
+    ol.render.Event.prototype,
+    'preventDefault',
+    ol.render.Event.prototype.preventDefault);
+
+goog.exportProperty(
+    ol.render.Event.prototype,
+    'stopPropagation',
+    ol.render.Event.prototype.stopPropagation);
+
+goog.exportProperty(
+    ol.pointer.PointerEvent.prototype,
+    'type',
+    ol.pointer.PointerEvent.prototype.type);
+
+goog.exportProperty(
+    ol.pointer.PointerEvent.prototype,
+    'target',
+    ol.pointer.PointerEvent.prototype.target);
+
+goog.exportProperty(
+    ol.pointer.PointerEvent.prototype,
+    'preventDefault',
+    ol.pointer.PointerEvent.prototype.preventDefault);
+
+goog.exportProperty(
+    ol.pointer.PointerEvent.prototype,
+    'stopPropagation',
+    ol.pointer.PointerEvent.prototype.stopPropagation);
+
+goog.exportProperty(
+    ol.layer.Base.prototype,
+    'get',
+    ol.layer.Base.prototype.get);
+
+goog.exportProperty(
+    ol.layer.Base.prototype,
+    'getKeys',
+    ol.layer.Base.prototype.getKeys);
+
+goog.exportProperty(
+    ol.layer.Base.prototype,
+    'getProperties',
+    ol.layer.Base.prototype.getProperties);
+
+goog.exportProperty(
+    ol.layer.Base.prototype,
+    'set',
+    ol.layer.Base.prototype.set);
+
+goog.exportProperty(
+    ol.layer.Base.prototype,
+    'setProperties',
+    ol.layer.Base.prototype.setProperties);
+
+goog.exportProperty(
+    ol.layer.Base.prototype,
+    'unset',
+    ol.layer.Base.prototype.unset);
+
+goog.exportProperty(
+    ol.layer.Base.prototype,
+    'changed',
+    ol.layer.Base.prototype.changed);
+
+goog.exportProperty(
+    ol.layer.Base.prototype,
+    'dispatchEvent',
+    ol.layer.Base.prototype.dispatchEvent);
+
+goog.exportProperty(
+    ol.layer.Base.prototype,
+    'getRevision',
+    ol.layer.Base.prototype.getRevision);
+
+goog.exportProperty(
+    ol.layer.Base.prototype,
+    'on',
+    ol.layer.Base.prototype.on);
+
+goog.exportProperty(
+    ol.layer.Base.prototype,
+    'once',
+    ol.layer.Base.prototype.once);
+
+goog.exportProperty(
+    ol.layer.Base.prototype,
+    'un',
+    ol.layer.Base.prototype.un);
+
+goog.exportProperty(
+    ol.layer.Base.prototype,
+    'unByKey',
+    ol.layer.Base.prototype.unByKey);
+
+goog.exportProperty(
+    ol.layer.Layer.prototype,
+    'getExtent',
+    ol.layer.Layer.prototype.getExtent);
+
+goog.exportProperty(
+    ol.layer.Layer.prototype,
+    'getMaxResolution',
+    ol.layer.Layer.prototype.getMaxResolution);
+
+goog.exportProperty(
+    ol.layer.Layer.prototype,
+    'getMinResolution',
+    ol.layer.Layer.prototype.getMinResolution);
+
+goog.exportProperty(
+    ol.layer.Layer.prototype,
+    'getOpacity',
+    ol.layer.Layer.prototype.getOpacity);
+
+goog.exportProperty(
+    ol.layer.Layer.prototype,
+    'getVisible',
+    ol.layer.Layer.prototype.getVisible);
+
+goog.exportProperty(
+    ol.layer.Layer.prototype,
+    'getZIndex',
+    ol.layer.Layer.prototype.getZIndex);
+
+goog.exportProperty(
+    ol.layer.Layer.prototype,
+    'setExtent',
+    ol.layer.Layer.prototype.setExtent);
+
+goog.exportProperty(
+    ol.layer.Layer.prototype,
+    'setMaxResolution',
+    ol.layer.Layer.prototype.setMaxResolution);
+
+goog.exportProperty(
+    ol.layer.Layer.prototype,
+    'setMinResolution',
+    ol.layer.Layer.prototype.setMinResolution);
+
+goog.exportProperty(
+    ol.layer.Layer.prototype,
+    'setOpacity',
+    ol.layer.Layer.prototype.setOpacity);
+
+goog.exportProperty(
+    ol.layer.Layer.prototype,
+    'setVisible',
+    ol.layer.Layer.prototype.setVisible);
+
+goog.exportProperty(
+    ol.layer.Layer.prototype,
+    'setZIndex',
+    ol.layer.Layer.prototype.setZIndex);
+
+goog.exportProperty(
+    ol.layer.Layer.prototype,
+    'get',
+    ol.layer.Layer.prototype.get);
+
+goog.exportProperty(
+    ol.layer.Layer.prototype,
+    'getKeys',
+    ol.layer.Layer.prototype.getKeys);
+
+goog.exportProperty(
+    ol.layer.Layer.prototype,
+    'getProperties',
+    ol.layer.Layer.prototype.getProperties);
+
+goog.exportProperty(
+    ol.layer.Layer.prototype,
+    'set',
+    ol.layer.Layer.prototype.set);
+
+goog.exportProperty(
+    ol.layer.Layer.prototype,
+    'setProperties',
+    ol.layer.Layer.prototype.setProperties);
+
+goog.exportProperty(
+    ol.layer.Layer.prototype,
+    'unset',
+    ol.layer.Layer.prototype.unset);
+
+goog.exportProperty(
+    ol.layer.Layer.prototype,
+    'changed',
+    ol.layer.Layer.prototype.changed);
+
+goog.exportProperty(
+    ol.layer.Layer.prototype,
+    'dispatchEvent',
+    ol.layer.Layer.prototype.dispatchEvent);
+
+goog.exportProperty(
+    ol.layer.Layer.prototype,
+    'getRevision',
+    ol.layer.Layer.prototype.getRevision);
+
+goog.exportProperty(
+    ol.layer.Layer.prototype,
+    'on',
+    ol.layer.Layer.prototype.on);
+
+goog.exportProperty(
+    ol.layer.Layer.prototype,
+    'once',
+    ol.layer.Layer.prototype.once);
+
+goog.exportProperty(
+    ol.layer.Layer.prototype,
+    'un',
+    ol.layer.Layer.prototype.un);
+
+goog.exportProperty(
+    ol.layer.Layer.prototype,
+    'unByKey',
+    ol.layer.Layer.prototype.unByKey);
+
+goog.exportProperty(
+    ol.layer.Vector.prototype,
+    'setMap',
+    ol.layer.Vector.prototype.setMap);
+
+goog.exportProperty(
+    ol.layer.Vector.prototype,
+    'setSource',
+    ol.layer.Vector.prototype.setSource);
+
+goog.exportProperty(
+    ol.layer.Vector.prototype,
+    'getExtent',
+    ol.layer.Vector.prototype.getExtent);
+
+goog.exportProperty(
+    ol.layer.Vector.prototype,
+    'getMaxResolution',
+    ol.layer.Vector.prototype.getMaxResolution);
+
+goog.exportProperty(
+    ol.layer.Vector.prototype,
+    'getMinResolution',
+    ol.layer.Vector.prototype.getMinResolution);
+
+goog.exportProperty(
+    ol.layer.Vector.prototype,
+    'getOpacity',
+    ol.layer.Vector.prototype.getOpacity);
+
+goog.exportProperty(
+    ol.layer.Vector.prototype,
+    'getVisible',
+    ol.layer.Vector.prototype.getVisible);
+
+goog.exportProperty(
+    ol.layer.Vector.prototype,
+    'getZIndex',
+    ol.layer.Vector.prototype.getZIndex);
+
+goog.exportProperty(
+    ol.layer.Vector.prototype,
+    'setExtent',
+    ol.layer.Vector.prototype.setExtent);
+
+goog.exportProperty(
+    ol.layer.Vector.prototype,
+    'setMaxResolution',
+    ol.layer.Vector.prototype.setMaxResolution);
+
+goog.exportProperty(
+    ol.layer.Vector.prototype,
+    'setMinResolution',
+    ol.layer.Vector.prototype.setMinResolution);
+
+goog.exportProperty(
+    ol.layer.Vector.prototype,
+    'setOpacity',
+    ol.layer.Vector.prototype.setOpacity);
+
+goog.exportProperty(
+    ol.layer.Vector.prototype,
+    'setVisible',
+    ol.layer.Vector.prototype.setVisible);
+
+goog.exportProperty(
+    ol.layer.Vector.prototype,
+    'setZIndex',
+    ol.layer.Vector.prototype.setZIndex);
+
+goog.exportProperty(
+    ol.layer.Vector.prototype,
+    'get',
+    ol.layer.Vector.prototype.get);
+
+goog.exportProperty(
+    ol.layer.Vector.prototype,
+    'getKeys',
+    ol.layer.Vector.prototype.getKeys);
+
+goog.exportProperty(
+    ol.layer.Vector.prototype,
+    'getProperties',
+    ol.layer.Vector.prototype.getProperties);
+
+goog.exportProperty(
+    ol.layer.Vector.prototype,
+    'set',
+    ol.layer.Vector.prototype.set);
+
+goog.exportProperty(
+    ol.layer.Vector.prototype,
+    'setProperties',
+    ol.layer.Vector.prototype.setProperties);
+
+goog.exportProperty(
+    ol.layer.Vector.prototype,
+    'unset',
+    ol.layer.Vector.prototype.unset);
+
+goog.exportProperty(
+    ol.layer.Vector.prototype,
+    'changed',
+    ol.layer.Vector.prototype.changed);
+
+goog.exportProperty(
+    ol.layer.Vector.prototype,
+    'dispatchEvent',
+    ol.layer.Vector.prototype.dispatchEvent);
+
+goog.exportProperty(
+    ol.layer.Vector.prototype,
+    'getRevision',
+    ol.layer.Vector.prototype.getRevision);
+
+goog.exportProperty(
+    ol.layer.Vector.prototype,
+    'on',
+    ol.layer.Vector.prototype.on);
+
+goog.exportProperty(
+    ol.layer.Vector.prototype,
+    'once',
+    ol.layer.Vector.prototype.once);
+
+goog.exportProperty(
+    ol.layer.Vector.prototype,
+    'un',
+    ol.layer.Vector.prototype.un);
+
+goog.exportProperty(
+    ol.layer.Vector.prototype,
+    'unByKey',
+    ol.layer.Vector.prototype.unByKey);
+
+goog.exportProperty(
+    ol.layer.Heatmap.prototype,
+    'getSource',
+    ol.layer.Heatmap.prototype.getSource);
+
+goog.exportProperty(
+    ol.layer.Heatmap.prototype,
+    'getStyle',
+    ol.layer.Heatmap.prototype.getStyle);
+
+goog.exportProperty(
+    ol.layer.Heatmap.prototype,
+    'getStyleFunction',
+    ol.layer.Heatmap.prototype.getStyleFunction);
+
+goog.exportProperty(
+    ol.layer.Heatmap.prototype,
+    'setStyle',
+    ol.layer.Heatmap.prototype.setStyle);
+
+goog.exportProperty(
+    ol.layer.Heatmap.prototype,
+    'setMap',
+    ol.layer.Heatmap.prototype.setMap);
+
+goog.exportProperty(
+    ol.layer.Heatmap.prototype,
+    'setSource',
+    ol.layer.Heatmap.prototype.setSource);
+
+goog.exportProperty(
+    ol.layer.Heatmap.prototype,
+    'getExtent',
+    ol.layer.Heatmap.prototype.getExtent);
+
+goog.exportProperty(
+    ol.layer.Heatmap.prototype,
+    'getMaxResolution',
+    ol.layer.Heatmap.prototype.getMaxResolution);
+
+goog.exportProperty(
+    ol.layer.Heatmap.prototype,
+    'getMinResolution',
+    ol.layer.Heatmap.prototype.getMinResolution);
+
+goog.exportProperty(
+    ol.layer.Heatmap.prototype,
+    'getOpacity',
+    ol.layer.Heatmap.prototype.getOpacity);
+
+goog.exportProperty(
+    ol.layer.Heatmap.prototype,
+    'getVisible',
+    ol.layer.Heatmap.prototype.getVisible);
+
+goog.exportProperty(
+    ol.layer.Heatmap.prototype,
+    'getZIndex',
+    ol.layer.Heatmap.prototype.getZIndex);
+
+goog.exportProperty(
+    ol.layer.Heatmap.prototype,
+    'setExtent',
+    ol.layer.Heatmap.prototype.setExtent);
+
+goog.exportProperty(
+    ol.layer.Heatmap.prototype,
+    'setMaxResolution',
+    ol.layer.Heatmap.prototype.setMaxResolution);
+
+goog.exportProperty(
+    ol.layer.Heatmap.prototype,
+    'setMinResolution',
+    ol.layer.Heatmap.prototype.setMinResolution);
+
+goog.exportProperty(
+    ol.layer.Heatmap.prototype,
+    'setOpacity',
+    ol.layer.Heatmap.prototype.setOpacity);
+
+goog.exportProperty(
+    ol.layer.Heatmap.prototype,
+    'setVisible',
+    ol.layer.Heatmap.prototype.setVisible);
+
+goog.exportProperty(
+    ol.layer.Heatmap.prototype,
+    'setZIndex',
+    ol.layer.Heatmap.prototype.setZIndex);
+
+goog.exportProperty(
+    ol.layer.Heatmap.prototype,
+    'get',
+    ol.layer.Heatmap.prototype.get);
+
+goog.exportProperty(
+    ol.layer.Heatmap.prototype,
+    'getKeys',
+    ol.layer.Heatmap.prototype.getKeys);
+
+goog.exportProperty(
+    ol.layer.Heatmap.prototype,
+    'getProperties',
+    ol.layer.Heatmap.prototype.getProperties);
+
+goog.exportProperty(
+    ol.layer.Heatmap.prototype,
+    'set',
+    ol.layer.Heatmap.prototype.set);
+
+goog.exportProperty(
+    ol.layer.Heatmap.prototype,
+    'setProperties',
+    ol.layer.Heatmap.prototype.setProperties);
+
+goog.exportProperty(
+    ol.layer.Heatmap.prototype,
+    'unset',
+    ol.layer.Heatmap.prototype.unset);
+
+goog.exportProperty(
+    ol.layer.Heatmap.prototype,
+    'changed',
+    ol.layer.Heatmap.prototype.changed);
+
+goog.exportProperty(
+    ol.layer.Heatmap.prototype,
+    'dispatchEvent',
+    ol.layer.Heatmap.prototype.dispatchEvent);
+
+goog.exportProperty(
+    ol.layer.Heatmap.prototype,
+    'getRevision',
+    ol.layer.Heatmap.prototype.getRevision);
+
+goog.exportProperty(
+    ol.layer.Heatmap.prototype,
+    'on',
+    ol.layer.Heatmap.prototype.on);
+
+goog.exportProperty(
+    ol.layer.Heatmap.prototype,
+    'once',
+    ol.layer.Heatmap.prototype.once);
+
+goog.exportProperty(
+    ol.layer.Heatmap.prototype,
+    'un',
+    ol.layer.Heatmap.prototype.un);
+
+goog.exportProperty(
+    ol.layer.Heatmap.prototype,
+    'unByKey',
+    ol.layer.Heatmap.prototype.unByKey);
+
+goog.exportProperty(
+    ol.layer.Image.prototype,
+    'setMap',
+    ol.layer.Image.prototype.setMap);
+
+goog.exportProperty(
+    ol.layer.Image.prototype,
+    'setSource',
+    ol.layer.Image.prototype.setSource);
+
+goog.exportProperty(
+    ol.layer.Image.prototype,
+    'getExtent',
+    ol.layer.Image.prototype.getExtent);
+
+goog.exportProperty(
+    ol.layer.Image.prototype,
+    'getMaxResolution',
+    ol.layer.Image.prototype.getMaxResolution);
+
+goog.exportProperty(
+    ol.layer.Image.prototype,
+    'getMinResolution',
+    ol.layer.Image.prototype.getMinResolution);
+
+goog.exportProperty(
+    ol.layer.Image.prototype,
+    'getOpacity',
+    ol.layer.Image.prototype.getOpacity);
+
+goog.exportProperty(
+    ol.layer.Image.prototype,
+    'getVisible',
+    ol.layer.Image.prototype.getVisible);
+
+goog.exportProperty(
+    ol.layer.Image.prototype,
+    'getZIndex',
+    ol.layer.Image.prototype.getZIndex);
+
+goog.exportProperty(
+    ol.layer.Image.prototype,
+    'setExtent',
+    ol.layer.Image.prototype.setExtent);
+
+goog.exportProperty(
+    ol.layer.Image.prototype,
+    'setMaxResolution',
+    ol.layer.Image.prototype.setMaxResolution);
+
+goog.exportProperty(
+    ol.layer.Image.prototype,
+    'setMinResolution',
+    ol.layer.Image.prototype.setMinResolution);
+
+goog.exportProperty(
+    ol.layer.Image.prototype,
+    'setOpacity',
+    ol.layer.Image.prototype.setOpacity);
+
+goog.exportProperty(
+    ol.layer.Image.prototype,
+    'setVisible',
+    ol.layer.Image.prototype.setVisible);
+
+goog.exportProperty(
+    ol.layer.Image.prototype,
+    'setZIndex',
+    ol.layer.Image.prototype.setZIndex);
+
+goog.exportProperty(
+    ol.layer.Image.prototype,
+    'get',
+    ol.layer.Image.prototype.get);
+
+goog.exportProperty(
+    ol.layer.Image.prototype,
+    'getKeys',
+    ol.layer.Image.prototype.getKeys);
+
+goog.exportProperty(
+    ol.layer.Image.prototype,
+    'getProperties',
+    ol.layer.Image.prototype.getProperties);
+
+goog.exportProperty(
+    ol.layer.Image.prototype,
+    'set',
+    ol.layer.Image.prototype.set);
+
+goog.exportProperty(
+    ol.layer.Image.prototype,
+    'setProperties',
+    ol.layer.Image.prototype.setProperties);
+
+goog.exportProperty(
+    ol.layer.Image.prototype,
+    'unset',
+    ol.layer.Image.prototype.unset);
+
+goog.exportProperty(
+    ol.layer.Image.prototype,
+    'changed',
+    ol.layer.Image.prototype.changed);
+
+goog.exportProperty(
+    ol.layer.Image.prototype,
+    'dispatchEvent',
+    ol.layer.Image.prototype.dispatchEvent);
+
+goog.exportProperty(
+    ol.layer.Image.prototype,
+    'getRevision',
+    ol.layer.Image.prototype.getRevision);
+
+goog.exportProperty(
+    ol.layer.Image.prototype,
+    'on',
+    ol.layer.Image.prototype.on);
+
+goog.exportProperty(
+    ol.layer.Image.prototype,
+    'once',
+    ol.layer.Image.prototype.once);
+
+goog.exportProperty(
+    ol.layer.Image.prototype,
+    'un',
+    ol.layer.Image.prototype.un);
+
+goog.exportProperty(
+    ol.layer.Image.prototype,
+    'unByKey',
+    ol.layer.Image.prototype.unByKey);
+
+goog.exportProperty(
+    ol.layer.Group.prototype,
+    'getExtent',
+    ol.layer.Group.prototype.getExtent);
+
+goog.exportProperty(
+    ol.layer.Group.prototype,
+    'getMaxResolution',
+    ol.layer.Group.prototype.getMaxResolution);
+
+goog.exportProperty(
+    ol.layer.Group.prototype,
+    'getMinResolution',
+    ol.layer.Group.prototype.getMinResolution);
+
+goog.exportProperty(
+    ol.layer.Group.prototype,
+    'getOpacity',
+    ol.layer.Group.prototype.getOpacity);
+
+goog.exportProperty(
+    ol.layer.Group.prototype,
+    'getVisible',
+    ol.layer.Group.prototype.getVisible);
+
+goog.exportProperty(
+    ol.layer.Group.prototype,
+    'getZIndex',
+    ol.layer.Group.prototype.getZIndex);
+
+goog.exportProperty(
+    ol.layer.Group.prototype,
+    'setExtent',
+    ol.layer.Group.prototype.setExtent);
+
+goog.exportProperty(
+    ol.layer.Group.prototype,
+    'setMaxResolution',
+    ol.layer.Group.prototype.setMaxResolution);
+
+goog.exportProperty(
+    ol.layer.Group.prototype,
+    'setMinResolution',
+    ol.layer.Group.prototype.setMinResolution);
+
+goog.exportProperty(
+    ol.layer.Group.prototype,
+    'setOpacity',
+    ol.layer.Group.prototype.setOpacity);
+
+goog.exportProperty(
+    ol.layer.Group.prototype,
+    'setVisible',
+    ol.layer.Group.prototype.setVisible);
+
+goog.exportProperty(
+    ol.layer.Group.prototype,
+    'setZIndex',
+    ol.layer.Group.prototype.setZIndex);
+
+goog.exportProperty(
+    ol.layer.Group.prototype,
+    'get',
+    ol.layer.Group.prototype.get);
+
+goog.exportProperty(
+    ol.layer.Group.prototype,
+    'getKeys',
+    ol.layer.Group.prototype.getKeys);
+
+goog.exportProperty(
+    ol.layer.Group.prototype,
+    'getProperties',
+    ol.layer.Group.prototype.getProperties);
+
+goog.exportProperty(
+    ol.layer.Group.prototype,
+    'set',
+    ol.layer.Group.prototype.set);
+
+goog.exportProperty(
+    ol.layer.Group.prototype,
+    'setProperties',
+    ol.layer.Group.prototype.setProperties);
+
+goog.exportProperty(
+    ol.layer.Group.prototype,
+    'unset',
+    ol.layer.Group.prototype.unset);
+
+goog.exportProperty(
+    ol.layer.Group.prototype,
+    'changed',
+    ol.layer.Group.prototype.changed);
+
+goog.exportProperty(
+    ol.layer.Group.prototype,
+    'dispatchEvent',
+    ol.layer.Group.prototype.dispatchEvent);
+
+goog.exportProperty(
+    ol.layer.Group.prototype,
+    'getRevision',
+    ol.layer.Group.prototype.getRevision);
+
+goog.exportProperty(
+    ol.layer.Group.prototype,
+    'on',
+    ol.layer.Group.prototype.on);
+
+goog.exportProperty(
+    ol.layer.Group.prototype,
+    'once',
+    ol.layer.Group.prototype.once);
+
+goog.exportProperty(
+    ol.layer.Group.prototype,
+    'un',
+    ol.layer.Group.prototype.un);
+
+goog.exportProperty(
+    ol.layer.Group.prototype,
+    'unByKey',
+    ol.layer.Group.prototype.unByKey);
+
+goog.exportProperty(
+    ol.layer.Tile.prototype,
+    'setMap',
+    ol.layer.Tile.prototype.setMap);
+
+goog.exportProperty(
+    ol.layer.Tile.prototype,
+    'setSource',
+    ol.layer.Tile.prototype.setSource);
+
+goog.exportProperty(
+    ol.layer.Tile.prototype,
+    'getExtent',
+    ol.layer.Tile.prototype.getExtent);
+
+goog.exportProperty(
+    ol.layer.Tile.prototype,
+    'getMaxResolution',
+    ol.layer.Tile.prototype.getMaxResolution);
+
+goog.exportProperty(
+    ol.layer.Tile.prototype,
+    'getMinResolution',
+    ol.layer.Tile.prototype.getMinResolution);
+
+goog.exportProperty(
+    ol.layer.Tile.prototype,
+    'getOpacity',
+    ol.layer.Tile.prototype.getOpacity);
+
+goog.exportProperty(
+    ol.layer.Tile.prototype,
+    'getVisible',
+    ol.layer.Tile.prototype.getVisible);
+
+goog.exportProperty(
+    ol.layer.Tile.prototype,
+    'getZIndex',
+    ol.layer.Tile.prototype.getZIndex);
+
+goog.exportProperty(
+    ol.layer.Tile.prototype,
+    'setExtent',
+    ol.layer.Tile.prototype.setExtent);
+
+goog.exportProperty(
+    ol.layer.Tile.prototype,
+    'setMaxResolution',
+    ol.layer.Tile.prototype.setMaxResolution);
+
+goog.exportProperty(
+    ol.layer.Tile.prototype,
+    'setMinResolution',
+    ol.layer.Tile.prototype.setMinResolution);
+
+goog.exportProperty(
+    ol.layer.Tile.prototype,
+    'setOpacity',
+    ol.layer.Tile.prototype.setOpacity);
+
+goog.exportProperty(
+    ol.layer.Tile.prototype,
+    'setVisible',
+    ol.layer.Tile.prototype.setVisible);
+
+goog.exportProperty(
+    ol.layer.Tile.prototype,
+    'setZIndex',
+    ol.layer.Tile.prototype.setZIndex);
+
+goog.exportProperty(
+    ol.layer.Tile.prototype,
+    'get',
+    ol.layer.Tile.prototype.get);
+
+goog.exportProperty(
+    ol.layer.Tile.prototype,
+    'getKeys',
+    ol.layer.Tile.prototype.getKeys);
+
+goog.exportProperty(
+    ol.layer.Tile.prototype,
+    'getProperties',
+    ol.layer.Tile.prototype.getProperties);
+
+goog.exportProperty(
+    ol.layer.Tile.prototype,
+    'set',
+    ol.layer.Tile.prototype.set);
+
+goog.exportProperty(
+    ol.layer.Tile.prototype,
+    'setProperties',
+    ol.layer.Tile.prototype.setProperties);
+
+goog.exportProperty(
+    ol.layer.Tile.prototype,
+    'unset',
+    ol.layer.Tile.prototype.unset);
+
+goog.exportProperty(
+    ol.layer.Tile.prototype,
+    'changed',
+    ol.layer.Tile.prototype.changed);
+
+goog.exportProperty(
+    ol.layer.Tile.prototype,
+    'dispatchEvent',
+    ol.layer.Tile.prototype.dispatchEvent);
+
+goog.exportProperty(
+    ol.layer.Tile.prototype,
+    'getRevision',
+    ol.layer.Tile.prototype.getRevision);
+
+goog.exportProperty(
+    ol.layer.Tile.prototype,
+    'on',
+    ol.layer.Tile.prototype.on);
+
+goog.exportProperty(
+    ol.layer.Tile.prototype,
+    'once',
+    ol.layer.Tile.prototype.once);
+
+goog.exportProperty(
+    ol.layer.Tile.prototype,
+    'un',
+    ol.layer.Tile.prototype.un);
+
+goog.exportProperty(
+    ol.layer.Tile.prototype,
+    'unByKey',
+    ol.layer.Tile.prototype.unByKey);
+
+goog.exportProperty(
+    ol.layer.VectorTile.prototype,
+    'getSource',
+    ol.layer.VectorTile.prototype.getSource);
+
+goog.exportProperty(
+    ol.layer.VectorTile.prototype,
+    'getStyle',
+    ol.layer.VectorTile.prototype.getStyle);
+
+goog.exportProperty(
+    ol.layer.VectorTile.prototype,
+    'getStyleFunction',
+    ol.layer.VectorTile.prototype.getStyleFunction);
+
+goog.exportProperty(
+    ol.layer.VectorTile.prototype,
+    'setStyle',
+    ol.layer.VectorTile.prototype.setStyle);
+
+goog.exportProperty(
+    ol.layer.VectorTile.prototype,
+    'setMap',
+    ol.layer.VectorTile.prototype.setMap);
+
+goog.exportProperty(
+    ol.layer.VectorTile.prototype,
+    'setSource',
+    ol.layer.VectorTile.prototype.setSource);
+
+goog.exportProperty(
+    ol.layer.VectorTile.prototype,
+    'getExtent',
+    ol.layer.VectorTile.prototype.getExtent);
+
+goog.exportProperty(
+    ol.layer.VectorTile.prototype,
+    'getMaxResolution',
+    ol.layer.VectorTile.prototype.getMaxResolution);
+
+goog.exportProperty(
+    ol.layer.VectorTile.prototype,
+    'getMinResolution',
+    ol.layer.VectorTile.prototype.getMinResolution);
+
+goog.exportProperty(
+    ol.layer.VectorTile.prototype,
+    'getOpacity',
+    ol.layer.VectorTile.prototype.getOpacity);
+
+goog.exportProperty(
+    ol.layer.VectorTile.prototype,
+    'getVisible',
+    ol.layer.VectorTile.prototype.getVisible);
+
+goog.exportProperty(
+    ol.layer.VectorTile.prototype,
+    'getZIndex',
+    ol.layer.VectorTile.prototype.getZIndex);
+
+goog.exportProperty(
+    ol.layer.VectorTile.prototype,
+    'setExtent',
+    ol.layer.VectorTile.prototype.setExtent);
+
+goog.exportProperty(
+    ol.layer.VectorTile.prototype,
+    'setMaxResolution',
+    ol.layer.VectorTile.prototype.setMaxResolution);
+
+goog.exportProperty(
+    ol.layer.VectorTile.prototype,
+    'setMinResolution',
+    ol.layer.VectorTile.prototype.setMinResolution);
+
+goog.exportProperty(
+    ol.layer.VectorTile.prototype,
+    'setOpacity',
+    ol.layer.VectorTile.prototype.setOpacity);
+
+goog.exportProperty(
+    ol.layer.VectorTile.prototype,
+    'setVisible',
+    ol.layer.VectorTile.prototype.setVisible);
+
+goog.exportProperty(
+    ol.layer.VectorTile.prototype,
+    'setZIndex',
+    ol.layer.VectorTile.prototype.setZIndex);
+
+goog.exportProperty(
+    ol.layer.VectorTile.prototype,
+    'get',
+    ol.layer.VectorTile.prototype.get);
+
+goog.exportProperty(
+    ol.layer.VectorTile.prototype,
+    'getKeys',
+    ol.layer.VectorTile.prototype.getKeys);
+
+goog.exportProperty(
+    ol.layer.VectorTile.prototype,
+    'getProperties',
+    ol.layer.VectorTile.prototype.getProperties);
+
+goog.exportProperty(
+    ol.layer.VectorTile.prototype,
+    'set',
+    ol.layer.VectorTile.prototype.set);
+
+goog.exportProperty(
+    ol.layer.VectorTile.prototype,
+    'setProperties',
+    ol.layer.VectorTile.prototype.setProperties);
+
+goog.exportProperty(
+    ol.layer.VectorTile.prototype,
+    'unset',
+    ol.layer.VectorTile.prototype.unset);
+
+goog.exportProperty(
+    ol.layer.VectorTile.prototype,
+    'changed',
+    ol.layer.VectorTile.prototype.changed);
+
+goog.exportProperty(
+    ol.layer.VectorTile.prototype,
+    'dispatchEvent',
+    ol.layer.VectorTile.prototype.dispatchEvent);
+
+goog.exportProperty(
+    ol.layer.VectorTile.prototype,
+    'getRevision',
+    ol.layer.VectorTile.prototype.getRevision);
+
+goog.exportProperty(
+    ol.layer.VectorTile.prototype,
+    'on',
+    ol.layer.VectorTile.prototype.on);
+
+goog.exportProperty(
+    ol.layer.VectorTile.prototype,
+    'once',
+    ol.layer.VectorTile.prototype.once);
+
+goog.exportProperty(
+    ol.layer.VectorTile.prototype,
+    'un',
+    ol.layer.VectorTile.prototype.un);
+
+goog.exportProperty(
+    ol.layer.VectorTile.prototype,
+    'unByKey',
+    ol.layer.VectorTile.prototype.unByKey);
+
+goog.exportProperty(
+    ol.interaction.Interaction.prototype,
+    'get',
+    ol.interaction.Interaction.prototype.get);
+
+goog.exportProperty(
+    ol.interaction.Interaction.prototype,
+    'getKeys',
+    ol.interaction.Interaction.prototype.getKeys);
+
+goog.exportProperty(
+    ol.interaction.Interaction.prototype,
+    'getProperties',
+    ol.interaction.Interaction.prototype.getProperties);
+
+goog.exportProperty(
+    ol.interaction.Interaction.prototype,
+    'set',
+    ol.interaction.Interaction.prototype.set);
+
+goog.exportProperty(
+    ol.interaction.Interaction.prototype,
+    'setProperties',
+    ol.interaction.Interaction.prototype.setProperties);
+
+goog.exportProperty(
+    ol.interaction.Interaction.prototype,
+    'unset',
+    ol.interaction.Interaction.prototype.unset);
+
+goog.exportProperty(
+    ol.interaction.Interaction.prototype,
+    'changed',
+    ol.interaction.Interaction.prototype.changed);
+
+goog.exportProperty(
+    ol.interaction.Interaction.prototype,
+    'dispatchEvent',
+    ol.interaction.Interaction.prototype.dispatchEvent);
+
+goog.exportProperty(
+    ol.interaction.Interaction.prototype,
+    'getRevision',
+    ol.interaction.Interaction.prototype.getRevision);
+
+goog.exportProperty(
+    ol.interaction.Interaction.prototype,
+    'on',
+    ol.interaction.Interaction.prototype.on);
+
+goog.exportProperty(
+    ol.interaction.Interaction.prototype,
+    'once',
+    ol.interaction.Interaction.prototype.once);
+
+goog.exportProperty(
+    ol.interaction.Interaction.prototype,
+    'un',
+    ol.interaction.Interaction.prototype.un);
+
+goog.exportProperty(
+    ol.interaction.Interaction.prototype,
+    'unByKey',
+    ol.interaction.Interaction.prototype.unByKey);
+
+goog.exportProperty(
+    ol.interaction.DoubleClickZoom.prototype,
+    'getActive',
+    ol.interaction.DoubleClickZoom.prototype.getActive);
+
+goog.exportProperty(
+    ol.interaction.DoubleClickZoom.prototype,
+    'getMap',
+    ol.interaction.DoubleClickZoom.prototype.getMap);
+
+goog.exportProperty(
+    ol.interaction.DoubleClickZoom.prototype,
+    'setActive',
+    ol.interaction.DoubleClickZoom.prototype.setActive);
+
+goog.exportProperty(
+    ol.interaction.DoubleClickZoom.prototype,
+    'get',
+    ol.interaction.DoubleClickZoom.prototype.get);
+
+goog.exportProperty(
+    ol.interaction.DoubleClickZoom.prototype,
+    'getKeys',
+    ol.interaction.DoubleClickZoom.prototype.getKeys);
+
+goog.exportProperty(
+    ol.interaction.DoubleClickZoom.prototype,
+    'getProperties',
+    ol.interaction.DoubleClickZoom.prototype.getProperties);
+
+goog.exportProperty(
+    ol.interaction.DoubleClickZoom.prototype,
+    'set',
+    ol.interaction.DoubleClickZoom.prototype.set);
+
+goog.exportProperty(
+    ol.interaction.DoubleClickZoom.prototype,
+    'setProperties',
+    ol.interaction.DoubleClickZoom.prototype.setProperties);
+
+goog.exportProperty(
+    ol.interaction.DoubleClickZoom.prototype,
+    'unset',
+    ol.interaction.DoubleClickZoom.prototype.unset);
+
+goog.exportProperty(
+    ol.interaction.DoubleClickZoom.prototype,
+    'changed',
+    ol.interaction.DoubleClickZoom.prototype.changed);
+
+goog.exportProperty(
+    ol.interaction.DoubleClickZoom.prototype,
+    'dispatchEvent',
+    ol.interaction.DoubleClickZoom.prototype.dispatchEvent);
+
+goog.exportProperty(
+    ol.interaction.DoubleClickZoom.prototype,
+    'getRevision',
+    ol.interaction.DoubleClickZoom.prototype.getRevision);
+
+goog.exportProperty(
+    ol.interaction.DoubleClickZoom.prototype,
+    'on',
+    ol.interaction.DoubleClickZoom.prototype.on);
+
+goog.exportProperty(
+    ol.interaction.DoubleClickZoom.prototype,
+    'once',
+    ol.interaction.DoubleClickZoom.prototype.once);
+
+goog.exportProperty(
+    ol.interaction.DoubleClickZoom.prototype,
+    'un',
+    ol.interaction.DoubleClickZoom.prototype.un);
+
+goog.exportProperty(
+    ol.interaction.DoubleClickZoom.prototype,
+    'unByKey',
+    ol.interaction.DoubleClickZoom.prototype.unByKey);
+
+goog.exportProperty(
+    ol.interaction.DragAndDrop.prototype,
+    'getActive',
+    ol.interaction.DragAndDrop.prototype.getActive);
+
+goog.exportProperty(
+    ol.interaction.DragAndDrop.prototype,
+    'getMap',
+    ol.interaction.DragAndDrop.prototype.getMap);
+
+goog.exportProperty(
+    ol.interaction.DragAndDrop.prototype,
+    'setActive',
+    ol.interaction.DragAndDrop.prototype.setActive);
+
+goog.exportProperty(
+    ol.interaction.DragAndDrop.prototype,
+    'get',
+    ol.interaction.DragAndDrop.prototype.get);
+
+goog.exportProperty(
+    ol.interaction.DragAndDrop.prototype,
+    'getKeys',
+    ol.interaction.DragAndDrop.prototype.getKeys);
+
+goog.exportProperty(
+    ol.interaction.DragAndDrop.prototype,
+    'getProperties',
+    ol.interaction.DragAndDrop.prototype.getProperties);
+
+goog.exportProperty(
+    ol.interaction.DragAndDrop.prototype,
+    'set',
+    ol.interaction.DragAndDrop.prototype.set);
+
+goog.exportProperty(
+    ol.interaction.DragAndDrop.prototype,
+    'setProperties',
+    ol.interaction.DragAndDrop.prototype.setProperties);
+
+goog.exportProperty(
+    ol.interaction.DragAndDrop.prototype,
+    'unset',
+    ol.interaction.DragAndDrop.prototype.unset);
+
+goog.exportProperty(
+    ol.interaction.DragAndDrop.prototype,
+    'changed',
+    ol.interaction.DragAndDrop.prototype.changed);
+
+goog.exportProperty(
+    ol.interaction.DragAndDrop.prototype,
+    'dispatchEvent',
+    ol.interaction.DragAndDrop.prototype.dispatchEvent);
+
+goog.exportProperty(
+    ol.interaction.DragAndDrop.prototype,
+    'getRevision',
+    ol.interaction.DragAndDrop.prototype.getRevision);
+
+goog.exportProperty(
+    ol.interaction.DragAndDrop.prototype,
+    'on',
+    ol.interaction.DragAndDrop.prototype.on);
+
+goog.exportProperty(
+    ol.interaction.DragAndDrop.prototype,
+    'once',
+    ol.interaction.DragAndDrop.prototype.once);
+
+goog.exportProperty(
+    ol.interaction.DragAndDrop.prototype,
+    'un',
+    ol.interaction.DragAndDrop.prototype.un);
+
+goog.exportProperty(
+    ol.interaction.DragAndDrop.prototype,
+    'unByKey',
+    ol.interaction.DragAndDrop.prototype.unByKey);
+
+goog.exportProperty(
+    ol.interaction.DragAndDropEvent.prototype,
+    'type',
+    ol.interaction.DragAndDropEvent.prototype.type);
+
+goog.exportProperty(
+    ol.interaction.DragAndDropEvent.prototype,
+    'target',
+    ol.interaction.DragAndDropEvent.prototype.target);
+
+goog.exportProperty(
+    ol.interaction.DragAndDropEvent.prototype,
+    'preventDefault',
+    ol.interaction.DragAndDropEvent.prototype.preventDefault);
+
+goog.exportProperty(
+    ol.interaction.DragAndDropEvent.prototype,
+    'stopPropagation',
+    ol.interaction.DragAndDropEvent.prototype.stopPropagation);
+
+goog.exportProperty(
+    ol.DragBoxEvent.prototype,
+    'type',
+    ol.DragBoxEvent.prototype.type);
+
+goog.exportProperty(
+    ol.DragBoxEvent.prototype,
+    'target',
+    ol.DragBoxEvent.prototype.target);
+
+goog.exportProperty(
+    ol.DragBoxEvent.prototype,
+    'preventDefault',
+    ol.DragBoxEvent.prototype.preventDefault);
+
+goog.exportProperty(
+    ol.DragBoxEvent.prototype,
+    'stopPropagation',
+    ol.DragBoxEvent.prototype.stopPropagation);
+
+goog.exportProperty(
+    ol.interaction.Pointer.prototype,
+    'getActive',
+    ol.interaction.Pointer.prototype.getActive);
+
+goog.exportProperty(
+    ol.interaction.Pointer.prototype,
+    'getMap',
+    ol.interaction.Pointer.prototype.getMap);
+
+goog.exportProperty(
+    ol.interaction.Pointer.prototype,
+    'setActive',
+    ol.interaction.Pointer.prototype.setActive);
+
+goog.exportProperty(
+    ol.interaction.Pointer.prototype,
+    'get',
+    ol.interaction.Pointer.prototype.get);
+
+goog.exportProperty(
+    ol.interaction.Pointer.prototype,
+    'getKeys',
+    ol.interaction.Pointer.prototype.getKeys);
+
+goog.exportProperty(
+    ol.interaction.Pointer.prototype,
+    'getProperties',
+    ol.interaction.Pointer.prototype.getProperties);
+
+goog.exportProperty(
+    ol.interaction.Pointer.prototype,
+    'set',
+    ol.interaction.Pointer.prototype.set);
+
+goog.exportProperty(
+    ol.interaction.Pointer.prototype,
+    'setProperties',
+    ol.interaction.Pointer.prototype.setProperties);
+
+goog.exportProperty(
+    ol.interaction.Pointer.prototype,
+    'unset',
+    ol.interaction.Pointer.prototype.unset);
+
+goog.exportProperty(
+    ol.interaction.Pointer.prototype,
+    'changed',
+    ol.interaction.Pointer.prototype.changed);
+
+goog.exportProperty(
+    ol.interaction.Pointer.prototype,
+    'dispatchEvent',
+    ol.interaction.Pointer.prototype.dispatchEvent);
+
+goog.exportProperty(
+    ol.interaction.Pointer.prototype,
+    'getRevision',
+    ol.interaction.Pointer.prototype.getRevision);
+
+goog.exportProperty(
+    ol.interaction.Pointer.prototype,
+    'on',
+    ol.interaction.Pointer.prototype.on);
+
+goog.exportProperty(
+    ol.interaction.Pointer.prototype,
+    'once',
+    ol.interaction.Pointer.prototype.once);
+
+goog.exportProperty(
+    ol.interaction.Pointer.prototype,
+    'un',
+    ol.interaction.Pointer.prototype.un);
+
+goog.exportProperty(
+    ol.interaction.Pointer.prototype,
+    'unByKey',
+    ol.interaction.Pointer.prototype.unByKey);
+
+goog.exportProperty(
+    ol.interaction.DragBox.prototype,
+    'getActive',
+    ol.interaction.DragBox.prototype.getActive);
+
+goog.exportProperty(
+    ol.interaction.DragBox.prototype,
+    'getMap',
+    ol.interaction.DragBox.prototype.getMap);
+
+goog.exportProperty(
+    ol.interaction.DragBox.prototype,
+    'setActive',
+    ol.interaction.DragBox.prototype.setActive);
+
+goog.exportProperty(
+    ol.interaction.DragBox.prototype,
+    'get',
+    ol.interaction.DragBox.prototype.get);
+
+goog.exportProperty(
+    ol.interaction.DragBox.prototype,
+    'getKeys',
+    ol.interaction.DragBox.prototype.getKeys);
+
+goog.exportProperty(
+    ol.interaction.DragBox.prototype,
+    'getProperties',
+    ol.interaction.DragBox.prototype.getProperties);
+
+goog.exportProperty(
+    ol.interaction.DragBox.prototype,
+    'set',
+    ol.interaction.DragBox.prototype.set);
+
+goog.exportProperty(
+    ol.interaction.DragBox.prototype,
+    'setProperties',
+    ol.interaction.DragBox.prototype.setProperties);
+
+goog.exportProperty(
+    ol.interaction.DragBox.prototype,
+    'unset',
+    ol.interaction.DragBox.prototype.unset);
+
+goog.exportProperty(
+    ol.interaction.DragBox.prototype,
+    'changed',
+    ol.interaction.DragBox.prototype.changed);
+
+goog.exportProperty(
+    ol.interaction.DragBox.prototype,
+    'dispatchEvent',
+    ol.interaction.DragBox.prototype.dispatchEvent);
+
+goog.exportProperty(
+    ol.interaction.DragBox.prototype,
+    'getRevision',
+    ol.interaction.DragBox.prototype.getRevision);
+
+goog.exportProperty(
+    ol.interaction.DragBox.prototype,
+    'on',
+    ol.interaction.DragBox.prototype.on);
+
+goog.exportProperty(
+    ol.interaction.DragBox.prototype,
+    'once',
+    ol.interaction.DragBox.prototype.once);
+
+goog.exportProperty(
+    ol.interaction.DragBox.prototype,
+    'un',
+    ol.interaction.DragBox.prototype.un);
+
+goog.exportProperty(
+    ol.interaction.DragBox.prototype,
+    'unByKey',
+    ol.interaction.DragBox.prototype.unByKey);
+
+goog.exportProperty(
+    ol.interaction.DragPan.prototype,
+    'getActive',
+    ol.interaction.DragPan.prototype.getActive);
+
+goog.exportProperty(
+    ol.interaction.DragPan.prototype,
+    'getMap',
+    ol.interaction.DragPan.prototype.getMap);
+
+goog.exportProperty(
+    ol.interaction.DragPan.prototype,
+    'setActive',
+    ol.interaction.DragPan.prototype.setActive);
+
+goog.exportProperty(
+    ol.interaction.DragPan.prototype,
+    'get',
+    ol.interaction.DragPan.prototype.get);
+
+goog.exportProperty(
+    ol.interaction.DragPan.prototype,
+    'getKeys',
+    ol.interaction.DragPan.prototype.getKeys);
+
+goog.exportProperty(
+    ol.interaction.DragPan.prototype,
+    'getProperties',
+    ol.interaction.DragPan.prototype.getProperties);
+
+goog.exportProperty(
+    ol.interaction.DragPan.prototype,
+    'set',
+    ol.interaction.DragPan.prototype.set);
+
+goog.exportProperty(
+    ol.interaction.DragPan.prototype,
+    'setProperties',
+    ol.interaction.DragPan.prototype.setProperties);
+
+goog.exportProperty(
+    ol.interaction.DragPan.prototype,
+    'unset',
+    ol.interaction.DragPan.prototype.unset);
+
+goog.exportProperty(
+    ol.interaction.DragPan.prototype,
+    'changed',
+    ol.interaction.DragPan.prototype.changed);
+
+goog.exportProperty(
+    ol.interaction.DragPan.prototype,
+    'dispatchEvent',
+    ol.interaction.DragPan.prototype.dispatchEvent);
+
+goog.exportProperty(
+    ol.interaction.DragPan.prototype,
+    'getRevision',
+    ol.interaction.DragPan.prototype.getRevision);
+
+goog.exportProperty(
+    ol.interaction.DragPan.prototype,
+    'on',
+    ol.interaction.DragPan.prototype.on);
+
+goog.exportProperty(
+    ol.interaction.DragPan.prototype,
+    'once',
+    ol.interaction.DragPan.prototype.once);
+
+goog.exportProperty(
+    ol.interaction.DragPan.prototype,
+    'un',
+    ol.interaction.DragPan.prototype.un);
+
+goog.exportProperty(
+    ol.interaction.DragPan.prototype,
+    'unByKey',
+    ol.interaction.DragPan.prototype.unByKey);
+
+goog.exportProperty(
+    ol.interaction.DragRotateAndZoom.prototype,
+    'getActive',
+    ol.interaction.DragRotateAndZoom.prototype.getActive);
+
+goog.exportProperty(
+    ol.interaction.DragRotateAndZoom.prototype,
+    'getMap',
+    ol.interaction.DragRotateAndZoom.prototype.getMap);
+
+goog.exportProperty(
+    ol.interaction.DragRotateAndZoom.prototype,
+    'setActive',
+    ol.interaction.DragRotateAndZoom.prototype.setActive);
+
+goog.exportProperty(
+    ol.interaction.DragRotateAndZoom.prototype,
+    'get',
+    ol.interaction.DragRotateAndZoom.prototype.get);
+
+goog.exportProperty(
+    ol.interaction.DragRotateAndZoom.prototype,
+    'getKeys',
+    ol.interaction.DragRotateAndZoom.prototype.getKeys);
+
+goog.exportProperty(
+    ol.interaction.DragRotateAndZoom.prototype,
+    'getProperties',
+    ol.interaction.DragRotateAndZoom.prototype.getProperties);
+
+goog.exportProperty(
+    ol.interaction.DragRotateAndZoom.prototype,
+    'set',
+    ol.interaction.DragRotateAndZoom.prototype.set);
+
+goog.exportProperty(
+    ol.interaction.DragRotateAndZoom.prototype,
+    'setProperties',
+    ol.interaction.DragRotateAndZoom.prototype.setProperties);
+
+goog.exportProperty(
+    ol.interaction.DragRotateAndZoom.prototype,
+    'unset',
+    ol.interaction.DragRotateAndZoom.prototype.unset);
+
+goog.exportProperty(
+    ol.interaction.DragRotateAndZoom.prototype,
+    'changed',
+    ol.interaction.DragRotateAndZoom.prototype.changed);
+
+goog.exportProperty(
+    ol.interaction.DragRotateAndZoom.prototype,
+    'dispatchEvent',
+    ol.interaction.DragRotateAndZoom.prototype.dispatchEvent);
+
+goog.exportProperty(
+    ol.interaction.DragRotateAndZoom.prototype,
+    'getRevision',
+    ol.interaction.DragRotateAndZoom.prototype.getRevision);
+
+goog.exportProperty(
+    ol.interaction.DragRotateAndZoom.prototype,
+    'on',
+    ol.interaction.DragRotateAndZoom.prototype.on);
+
+goog.exportProperty(
+    ol.interaction.DragRotateAndZoom.prototype,
+    'once',
+    ol.interaction.DragRotateAndZoom.prototype.once);
+
+goog.exportProperty(
+    ol.interaction.DragRotateAndZoom.prototype,
+    'un',
+    ol.interaction.DragRotateAndZoom.prototype.un);
+
+goog.exportProperty(
+    ol.interaction.DragRotateAndZoom.prototype,
+    'unByKey',
+    ol.interaction.DragRotateAndZoom.prototype.unByKey);
+
+goog.exportProperty(
+    ol.interaction.DragRotate.prototype,
+    'getActive',
+    ol.interaction.DragRotate.prototype.getActive);
+
+goog.exportProperty(
+    ol.interaction.DragRotate.prototype,
+    'getMap',
+    ol.interaction.DragRotate.prototype.getMap);
+
+goog.exportProperty(
+    ol.interaction.DragRotate.prototype,
+    'setActive',
+    ol.interaction.DragRotate.prototype.setActive);
+
+goog.exportProperty(
+    ol.interaction.DragRotate.prototype,
+    'get',
+    ol.interaction.DragRotate.prototype.get);
+
+goog.exportProperty(
+    ol.interaction.DragRotate.prototype,
+    'getKeys',
+    ol.interaction.DragRotate.prototype.getKeys);
+
+goog.exportProperty(
+    ol.interaction.DragRotate.prototype,
+    'getProperties',
+    ol.interaction.DragRotate.prototype.getProperties);
+
+goog.exportProperty(
+    ol.interaction.DragRotate.prototype,
+    'set',
+    ol.interaction.DragRotate.prototype.set);
+
+goog.exportProperty(
+    ol.interaction.DragRotate.prototype,
+    'setProperties',
+    ol.interaction.DragRotate.prototype.setProperties);
+
+goog.exportProperty(
+    ol.interaction.DragRotate.prototype,
+    'unset',
+    ol.interaction.DragRotate.prototype.unset);
+
+goog.exportProperty(
+    ol.interaction.DragRotate.prototype,
+    'changed',
+    ol.interaction.DragRotate.prototype.changed);
+
+goog.exportProperty(
+    ol.interaction.DragRotate.prototype,
+    'dispatchEvent',
+    ol.interaction.DragRotate.prototype.dispatchEvent);
+
+goog.exportProperty(
+    ol.interaction.DragRotate.prototype,
+    'getRevision',
+    ol.interaction.DragRotate.prototype.getRevision);
+
+goog.exportProperty(
+    ol.interaction.DragRotate.prototype,
+    'on',
+    ol.interaction.DragRotate.prototype.on);
+
+goog.exportProperty(
+    ol.interaction.DragRotate.prototype,
+    'once',
+    ol.interaction.DragRotate.prototype.once);
+
+goog.exportProperty(
+    ol.interaction.DragRotate.prototype,
+    'un',
+    ol.interaction.DragRotate.prototype.un);
+
+goog.exportProperty(
+    ol.interaction.DragRotate.prototype,
+    'unByKey',
+    ol.interaction.DragRotate.prototype.unByKey);
+
+goog.exportProperty(
+    ol.interaction.DragZoom.prototype,
+    'getGeometry',
+    ol.interaction.DragZoom.prototype.getGeometry);
+
+goog.exportProperty(
+    ol.interaction.DragZoom.prototype,
+    'getActive',
+    ol.interaction.DragZoom.prototype.getActive);
+
+goog.exportProperty(
+    ol.interaction.DragZoom.prototype,
+    'getMap',
+    ol.interaction.DragZoom.prototype.getMap);
+
+goog.exportProperty(
+    ol.interaction.DragZoom.prototype,
+    'setActive',
+    ol.interaction.DragZoom.prototype.setActive);
+
+goog.exportProperty(
+    ol.interaction.DragZoom.prototype,
+    'get',
+    ol.interaction.DragZoom.prototype.get);
+
+goog.exportProperty(
+    ol.interaction.DragZoom.prototype,
+    'getKeys',
+    ol.interaction.DragZoom.prototype.getKeys);
+
+goog.exportProperty(
+    ol.interaction.DragZoom.prototype,
+    'getProperties',
+    ol.interaction.DragZoom.prototype.getProperties);
+
+goog.exportProperty(
+    ol.interaction.DragZoom.prototype,
+    'set',
+    ol.interaction.DragZoom.prototype.set);
+
+goog.exportProperty(
+    ol.interaction.DragZoom.prototype,
+    'setProperties',
+    ol.interaction.DragZoom.prototype.setProperties);
+
+goog.exportProperty(
+    ol.interaction.DragZoom.prototype,
+    'unset',
+    ol.interaction.DragZoom.prototype.unset);
+
+goog.exportProperty(
+    ol.interaction.DragZoom.prototype,
+    'changed',
+    ol.interaction.DragZoom.prototype.changed);
+
+goog.exportProperty(
+    ol.interaction.DragZoom.prototype,
+    'dispatchEvent',
+    ol.interaction.DragZoom.prototype.dispatchEvent);
+
+goog.exportProperty(
+    ol.interaction.DragZoom.prototype,
+    'getRevision',
+    ol.interaction.DragZoom.prototype.getRevision);
+
+goog.exportProperty(
+    ol.interaction.DragZoom.prototype,
+    'on',
+    ol.interaction.DragZoom.prototype.on);
+
+goog.exportProperty(
+    ol.interaction.DragZoom.prototype,
+    'once',
+    ol.interaction.DragZoom.prototype.once);
+
+goog.exportProperty(
+    ol.interaction.DragZoom.prototype,
+    'un',
+    ol.interaction.DragZoom.prototype.un);
+
+goog.exportProperty(
+    ol.interaction.DragZoom.prototype,
+    'unByKey',
+    ol.interaction.DragZoom.prototype.unByKey);
+
+goog.exportProperty(
+    ol.interaction.DrawEvent.prototype,
+    'type',
+    ol.interaction.DrawEvent.prototype.type);
+
+goog.exportProperty(
+    ol.interaction.DrawEvent.prototype,
+    'target',
+    ol.interaction.DrawEvent.prototype.target);
+
+goog.exportProperty(
+    ol.interaction.DrawEvent.prototype,
+    'preventDefault',
+    ol.interaction.DrawEvent.prototype.preventDefault);
+
+goog.exportProperty(
+    ol.interaction.DrawEvent.prototype,
+    'stopPropagation',
+    ol.interaction.DrawEvent.prototype.stopPropagation);
+
+goog.exportProperty(
+    ol.interaction.Draw.prototype,
+    'getActive',
+    ol.interaction.Draw.prototype.getActive);
+
+goog.exportProperty(
+    ol.interaction.Draw.prototype,
+    'getMap',
+    ol.interaction.Draw.prototype.getMap);
+
+goog.exportProperty(
+    ol.interaction.Draw.prototype,
+    'setActive',
+    ol.interaction.Draw.prototype.setActive);
+
+goog.exportProperty(
+    ol.interaction.Draw.prototype,
+    'get',
+    ol.interaction.Draw.prototype.get);
+
+goog.exportProperty(
+    ol.interaction.Draw.prototype,
+    'getKeys',
+    ol.interaction.Draw.prototype.getKeys);
+
+goog.exportProperty(
+    ol.interaction.Draw.prototype,
+    'getProperties',
+    ol.interaction.Draw.prototype.getProperties);
+
+goog.exportProperty(
+    ol.interaction.Draw.prototype,
+    'set',
+    ol.interaction.Draw.prototype.set);
+
+goog.exportProperty(
+    ol.interaction.Draw.prototype,
+    'setProperties',
+    ol.interaction.Draw.prototype.setProperties);
+
+goog.exportProperty(
+    ol.interaction.Draw.prototype,
+    'unset',
+    ol.interaction.Draw.prototype.unset);
+
+goog.exportProperty(
+    ol.interaction.Draw.prototype,
+    'changed',
+    ol.interaction.Draw.prototype.changed);
+
+goog.exportProperty(
+    ol.interaction.Draw.prototype,
+    'dispatchEvent',
+    ol.interaction.Draw.prototype.dispatchEvent);
+
+goog.exportProperty(
+    ol.interaction.Draw.prototype,
+    'getRevision',
+    ol.interaction.Draw.prototype.getRevision);
+
+goog.exportProperty(
+    ol.interaction.Draw.prototype,
+    'on',
+    ol.interaction.Draw.prototype.on);
+
+goog.exportProperty(
+    ol.interaction.Draw.prototype,
+    'once',
+    ol.interaction.Draw.prototype.once);
+
+goog.exportProperty(
+    ol.interaction.Draw.prototype,
+    'un',
+    ol.interaction.Draw.prototype.un);
+
+goog.exportProperty(
+    ol.interaction.Draw.prototype,
+    'unByKey',
+    ol.interaction.Draw.prototype.unByKey);
+
+goog.exportProperty(
+    ol.interaction.KeyboardPan.prototype,
+    'getActive',
+    ol.interaction.KeyboardPan.prototype.getActive);
+
+goog.exportProperty(
+    ol.interaction.KeyboardPan.prototype,
+    'getMap',
+    ol.interaction.KeyboardPan.prototype.getMap);
+
+goog.exportProperty(
+    ol.interaction.KeyboardPan.prototype,
+    'setActive',
+    ol.interaction.KeyboardPan.prototype.setActive);
+
+goog.exportProperty(
+    ol.interaction.KeyboardPan.prototype,
+    'get',
+    ol.interaction.KeyboardPan.prototype.get);
+
+goog.exportProperty(
+    ol.interaction.KeyboardPan.prototype,
+    'getKeys',
+    ol.interaction.KeyboardPan.prototype.getKeys);
+
+goog.exportProperty(
+    ol.interaction.KeyboardPan.prototype,
+    'getProperties',
+    ol.interaction.KeyboardPan.prototype.getProperties);
+
+goog.exportProperty(
+    ol.interaction.KeyboardPan.prototype,
+    'set',
+    ol.interaction.KeyboardPan.prototype.set);
+
+goog.exportProperty(
+    ol.interaction.KeyboardPan.prototype,
+    'setProperties',
+    ol.interaction.KeyboardPan.prototype.setProperties);
+
+goog.exportProperty(
+    ol.interaction.KeyboardPan.prototype,
+    'unset',
+    ol.interaction.KeyboardPan.prototype.unset);
+
+goog.exportProperty(
+    ol.interaction.KeyboardPan.prototype,
+    'changed',
+    ol.interaction.KeyboardPan.prototype.changed);
+
+goog.exportProperty(
+    ol.interaction.KeyboardPan.prototype,
+    'dispatchEvent',
+    ol.interaction.KeyboardPan.prototype.dispatchEvent);
+
+goog.exportProperty(
+    ol.interaction.KeyboardPan.prototype,
+    'getRevision',
+    ol.interaction.KeyboardPan.prototype.getRevision);
+
+goog.exportProperty(
+    ol.interaction.KeyboardPan.prototype,
+    'on',
+    ol.interaction.KeyboardPan.prototype.on);
+
+goog.exportProperty(
+    ol.interaction.KeyboardPan.prototype,
+    'once',
+    ol.interaction.KeyboardPan.prototype.once);
+
+goog.exportProperty(
+    ol.interaction.KeyboardPan.prototype,
+    'un',
+    ol.interaction.KeyboardPan.prototype.un);
+
+goog.exportProperty(
+    ol.interaction.KeyboardPan.prototype,
+    'unByKey',
+    ol.interaction.KeyboardPan.prototype.unByKey);
+
+goog.exportProperty(
+    ol.interaction.KeyboardZoom.prototype,
+    'getActive',
+    ol.interaction.KeyboardZoom.prototype.getActive);
+
+goog.exportProperty(
+    ol.interaction.KeyboardZoom.prototype,
+    'getMap',
+    ol.interaction.KeyboardZoom.prototype.getMap);
+
+goog.exportProperty(
+    ol.interaction.KeyboardZoom.prototype,
+    'setActive',
+    ol.interaction.KeyboardZoom.prototype.setActive);
+
+goog.exportProperty(
+    ol.interaction.KeyboardZoom.prototype,
+    'get',
+    ol.interaction.KeyboardZoom.prototype.get);
+
+goog.exportProperty(
+    ol.interaction.KeyboardZoom.prototype,
+    'getKeys',
+    ol.interaction.KeyboardZoom.prototype.getKeys);
+
+goog.exportProperty(
+    ol.interaction.KeyboardZoom.prototype,
+    'getProperties',
+    ol.interaction.KeyboardZoom.prototype.getProperties);
+
+goog.exportProperty(
+    ol.interaction.KeyboardZoom.prototype,
+    'set',
+    ol.interaction.KeyboardZoom.prototype.set);
+
+goog.exportProperty(
+    ol.interaction.KeyboardZoom.prototype,
+    'setProperties',
+    ol.interaction.KeyboardZoom.prototype.setProperties);
+
+goog.exportProperty(
+    ol.interaction.KeyboardZoom.prototype,
+    'unset',
+    ol.interaction.KeyboardZoom.prototype.unset);
+
+goog.exportProperty(
+    ol.interaction.KeyboardZoom.prototype,
+    'changed',
+    ol.interaction.KeyboardZoom.prototype.changed);
+
+goog.exportProperty(
+    ol.interaction.KeyboardZoom.prototype,
+    'dispatchEvent',
+    ol.interaction.KeyboardZoom.prototype.dispatchEvent);
+
+goog.exportProperty(
+    ol.interaction.KeyboardZoom.prototype,
+    'getRevision',
+    ol.interaction.KeyboardZoom.prototype.getRevision);
+
+goog.exportProperty(
+    ol.interaction.KeyboardZoom.prototype,
+    'on',
+    ol.interaction.KeyboardZoom.prototype.on);
+
+goog.exportProperty(
+    ol.interaction.KeyboardZoom.prototype,
+    'once',
+    ol.interaction.KeyboardZoom.prototype.once);
+
+goog.exportProperty(
+    ol.interaction.KeyboardZoom.prototype,
+    'un',
+    ol.interaction.KeyboardZoom.prototype.un);
+
+goog.exportProperty(
+    ol.interaction.KeyboardZoom.prototype,
+    'unByKey',
+    ol.interaction.KeyboardZoom.prototype.unByKey);
+
+goog.exportProperty(
+    ol.interaction.ModifyEvent.prototype,
+    'type',
+    ol.interaction.ModifyEvent.prototype.type);
+
+goog.exportProperty(
+    ol.interaction.ModifyEvent.prototype,
+    'target',
+    ol.interaction.ModifyEvent.prototype.target);
+
+goog.exportProperty(
+    ol.interaction.ModifyEvent.prototype,
+    'preventDefault',
+    ol.interaction.ModifyEvent.prototype.preventDefault);
+
+goog.exportProperty(
+    ol.interaction.ModifyEvent.prototype,
+    'stopPropagation',
+    ol.interaction.ModifyEvent.prototype.stopPropagation);
+
+goog.exportProperty(
+    ol.interaction.Modify.prototype,
+    'getActive',
+    ol.interaction.Modify.prototype.getActive);
+
+goog.exportProperty(
+    ol.interaction.Modify.prototype,
+    'getMap',
+    ol.interaction.Modify.prototype.getMap);
+
+goog.exportProperty(
+    ol.interaction.Modify.prototype,
+    'setActive',
+    ol.interaction.Modify.prototype.setActive);
+
+goog.exportProperty(
+    ol.interaction.Modify.prototype,
+    'get',
+    ol.interaction.Modify.prototype.get);
+
+goog.exportProperty(
+    ol.interaction.Modify.prototype,
+    'getKeys',
+    ol.interaction.Modify.prototype.getKeys);
+
+goog.exportProperty(
+    ol.interaction.Modify.prototype,
+    'getProperties',
+    ol.interaction.Modify.prototype.getProperties);
+
+goog.exportProperty(
+    ol.interaction.Modify.prototype,
+    'set',
+    ol.interaction.Modify.prototype.set);
+
+goog.exportProperty(
+    ol.interaction.Modify.prototype,
+    'setProperties',
+    ol.interaction.Modify.prototype.setProperties);
+
+goog.exportProperty(
+    ol.interaction.Modify.prototype,
+    'unset',
+    ol.interaction.Modify.prototype.unset);
+
+goog.exportProperty(
+    ol.interaction.Modify.prototype,
+    'changed',
+    ol.interaction.Modify.prototype.changed);
+
+goog.exportProperty(
+    ol.interaction.Modify.prototype,
+    'dispatchEvent',
+    ol.interaction.Modify.prototype.dispatchEvent);
+
+goog.exportProperty(
+    ol.interaction.Modify.prototype,
+    'getRevision',
+    ol.interaction.Modify.prototype.getRevision);
+
+goog.exportProperty(
+    ol.interaction.Modify.prototype,
+    'on',
+    ol.interaction.Modify.prototype.on);
+
+goog.exportProperty(
+    ol.interaction.Modify.prototype,
+    'once',
+    ol.interaction.Modify.prototype.once);
+
+goog.exportProperty(
+    ol.interaction.Modify.prototype,
+    'un',
+    ol.interaction.Modify.prototype.un);
+
+goog.exportProperty(
+    ol.interaction.Modify.prototype,
+    'unByKey',
+    ol.interaction.Modify.prototype.unByKey);
+
+goog.exportProperty(
+    ol.interaction.MouseWheelZoom.prototype,
+    'getActive',
+    ol.interaction.MouseWheelZoom.prototype.getActive);
+
+goog.exportProperty(
+    ol.interaction.MouseWheelZoom.prototype,
+    'getMap',
+    ol.interaction.MouseWheelZoom.prototype.getMap);
+
+goog.exportProperty(
+    ol.interaction.MouseWheelZoom.prototype,
+    'setActive',
+    ol.interaction.MouseWheelZoom.prototype.setActive);
+
+goog.exportProperty(
+    ol.interaction.MouseWheelZoom.prototype,
+    'get',
+    ol.interaction.MouseWheelZoom.prototype.get);
+
+goog.exportProperty(
+    ol.interaction.MouseWheelZoom.prototype,
+    'getKeys',
+    ol.interaction.MouseWheelZoom.prototype.getKeys);
+
+goog.exportProperty(
+    ol.interaction.MouseWheelZoom.prototype,
+    'getProperties',
+    ol.interaction.MouseWheelZoom.prototype.getProperties);
+
+goog.exportProperty(
+    ol.interaction.MouseWheelZoom.prototype,
+    'set',
+    ol.interaction.MouseWheelZoom.prototype.set);
+
+goog.exportProperty(
+    ol.interaction.MouseWheelZoom.prototype,
+    'setProperties',
+    ol.interaction.MouseWheelZoom.prototype.setProperties);
+
+goog.exportProperty(
+    ol.interaction.MouseWheelZoom.prototype,
+    'unset',
+    ol.interaction.MouseWheelZoom.prototype.unset);
+
+goog.exportProperty(
+    ol.interaction.MouseWheelZoom.prototype,
+    'changed',
+    ol.interaction.MouseWheelZoom.prototype.changed);
+
+goog.exportProperty(
+    ol.interaction.MouseWheelZoom.prototype,
+    'dispatchEvent',
+    ol.interaction.MouseWheelZoom.prototype.dispatchEvent);
+
+goog.exportProperty(
+    ol.interaction.MouseWheelZoom.prototype,
+    'getRevision',
+    ol.interaction.MouseWheelZoom.prototype.getRevision);
+
+goog.exportProperty(
+    ol.interaction.MouseWheelZoom.prototype,
+    'on',
+    ol.interaction.MouseWheelZoom.prototype.on);
+
+goog.exportProperty(
+    ol.interaction.MouseWheelZoom.prototype,
+    'once',
+    ol.interaction.MouseWheelZoom.prototype.once);
+
+goog.exportProperty(
+    ol.interaction.MouseWheelZoom.prototype,
+    'un',
+    ol.interaction.MouseWheelZoom.prototype.un);
+
+goog.exportProperty(
+    ol.interaction.MouseWheelZoom.prototype,
+    'unByKey',
+    ol.interaction.MouseWheelZoom.prototype.unByKey);
+
+goog.exportProperty(
+    ol.interaction.PinchRotate.prototype,
+    'getActive',
+    ol.interaction.PinchRotate.prototype.getActive);
+
+goog.exportProperty(
+    ol.interaction.PinchRotate.prototype,
+    'getMap',
+    ol.interaction.PinchRotate.prototype.getMap);
+
+goog.exportProperty(
+    ol.interaction.PinchRotate.prototype,
+    'setActive',
+    ol.interaction.PinchRotate.prototype.setActive);
+
+goog.exportProperty(
+    ol.interaction.PinchRotate.prototype,
+    'get',
+    ol.interaction.PinchRotate.prototype.get);
+
+goog.exportProperty(
+    ol.interaction.PinchRotate.prototype,
+    'getKeys',
+    ol.interaction.PinchRotate.prototype.getKeys);
+
+goog.exportProperty(
+    ol.interaction.PinchRotate.prototype,
+    'getProperties',
+    ol.interaction.PinchRotate.prototype.getProperties);
+
+goog.exportProperty(
+    ol.interaction.PinchRotate.prototype,
+    'set',
+    ol.interaction.PinchRotate.prototype.set);
+
+goog.exportProperty(
+    ol.interaction.PinchRotate.prototype,
+    'setProperties',
+    ol.interaction.PinchRotate.prototype.setProperties);
+
+goog.exportProperty(
+    ol.interaction.PinchRotate.prototype,
+    'unset',
+    ol.interaction.PinchRotate.prototype.unset);
+
+goog.exportProperty(
+    ol.interaction.PinchRotate.prototype,
+    'changed',
+    ol.interaction.PinchRotate.prototype.changed);
+
+goog.exportProperty(
+    ol.interaction.PinchRotate.prototype,
+    'dispatchEvent',
+    ol.interaction.PinchRotate.prototype.dispatchEvent);
+
+goog.exportProperty(
+    ol.interaction.PinchRotate.prototype,
+    'getRevision',
+    ol.interaction.PinchRotate.prototype.getRevision);
+
+goog.exportProperty(
+    ol.interaction.PinchRotate.prototype,
+    'on',
+    ol.interaction.PinchRotate.prototype.on);
+
+goog.exportProperty(
+    ol.interaction.PinchRotate.prototype,
+    'once',
+    ol.interaction.PinchRotate.prototype.once);
+
+goog.exportProperty(
+    ol.interaction.PinchRotate.prototype,
+    'un',
+    ol.interaction.PinchRotate.prototype.un);
+
+goog.exportProperty(
+    ol.interaction.PinchRotate.prototype,
+    'unByKey',
+    ol.interaction.PinchRotate.prototype.unByKey);
+
+goog.exportProperty(
+    ol.interaction.PinchZoom.prototype,
+    'getActive',
+    ol.interaction.PinchZoom.prototype.getActive);
+
+goog.exportProperty(
+    ol.interaction.PinchZoom.prototype,
+    'getMap',
+    ol.interaction.PinchZoom.prototype.getMap);
+
+goog.exportProperty(
+    ol.interaction.PinchZoom.prototype,
+    'setActive',
+    ol.interaction.PinchZoom.prototype.setActive);
+
+goog.exportProperty(
+    ol.interaction.PinchZoom.prototype,
+    'get',
+    ol.interaction.PinchZoom.prototype.get);
+
+goog.exportProperty(
+    ol.interaction.PinchZoom.prototype,
+    'getKeys',
+    ol.interaction.PinchZoom.prototype.getKeys);
+
+goog.exportProperty(
+    ol.interaction.PinchZoom.prototype,
+    'getProperties',
+    ol.interaction.PinchZoom.prototype.getProperties);
+
+goog.exportProperty(
+    ol.interaction.PinchZoom.prototype,
+    'set',
+    ol.interaction.PinchZoom.prototype.set);
+
+goog.exportProperty(
+    ol.interaction.PinchZoom.prototype,
+    'setProperties',
+    ol.interaction.PinchZoom.prototype.setProperties);
+
+goog.exportProperty(
+    ol.interaction.PinchZoom.prototype,
+    'unset',
+    ol.interaction.PinchZoom.prototype.unset);
+
+goog.exportProperty(
+    ol.interaction.PinchZoom.prototype,
+    'changed',
+    ol.interaction.PinchZoom.prototype.changed);
+
+goog.exportProperty(
+    ol.interaction.PinchZoom.prototype,
+    'dispatchEvent',
+    ol.interaction.PinchZoom.prototype.dispatchEvent);
+
+goog.exportProperty(
+    ol.interaction.PinchZoom.prototype,
+    'getRevision',
+    ol.interaction.PinchZoom.prototype.getRevision);
+
+goog.exportProperty(
+    ol.interaction.PinchZoom.prototype,
+    'on',
+    ol.interaction.PinchZoom.prototype.on);
+
+goog.exportProperty(
+    ol.interaction.PinchZoom.prototype,
+    'once',
+    ol.interaction.PinchZoom.prototype.once);
+
+goog.exportProperty(
+    ol.interaction.PinchZoom.prototype,
+    'un',
+    ol.interaction.PinchZoom.prototype.un);
+
+goog.exportProperty(
+    ol.interaction.PinchZoom.prototype,
+    'unByKey',
+    ol.interaction.PinchZoom.prototype.unByKey);
+
+goog.exportProperty(
+    ol.interaction.SelectEvent.prototype,
+    'type',
+    ol.interaction.SelectEvent.prototype.type);
+
+goog.exportProperty(
+    ol.interaction.SelectEvent.prototype,
+    'target',
+    ol.interaction.SelectEvent.prototype.target);
+
+goog.exportProperty(
+    ol.interaction.SelectEvent.prototype,
+    'preventDefault',
+    ol.interaction.SelectEvent.prototype.preventDefault);
+
+goog.exportProperty(
+    ol.interaction.SelectEvent.prototype,
+    'stopPropagation',
+    ol.interaction.SelectEvent.prototype.stopPropagation);
+
+goog.exportProperty(
+    ol.interaction.Select.prototype,
+    'getActive',
+    ol.interaction.Select.prototype.getActive);
+
+goog.exportProperty(
+    ol.interaction.Select.prototype,
+    'getMap',
+    ol.interaction.Select.prototype.getMap);
+
+goog.exportProperty(
+    ol.interaction.Select.prototype,
+    'setActive',
+    ol.interaction.Select.prototype.setActive);
+
+goog.exportProperty(
+    ol.interaction.Select.prototype,
+    'get',
+    ol.interaction.Select.prototype.get);
+
+goog.exportProperty(
+    ol.interaction.Select.prototype,
+    'getKeys',
+    ol.interaction.Select.prototype.getKeys);
+
+goog.exportProperty(
+    ol.interaction.Select.prototype,
+    'getProperties',
+    ol.interaction.Select.prototype.getProperties);
+
+goog.exportProperty(
+    ol.interaction.Select.prototype,
+    'set',
+    ol.interaction.Select.prototype.set);
+
+goog.exportProperty(
+    ol.interaction.Select.prototype,
+    'setProperties',
+    ol.interaction.Select.prototype.setProperties);
+
+goog.exportProperty(
+    ol.interaction.Select.prototype,
+    'unset',
+    ol.interaction.Select.prototype.unset);
+
+goog.exportProperty(
+    ol.interaction.Select.prototype,
+    'changed',
+    ol.interaction.Select.prototype.changed);
+
+goog.exportProperty(
+    ol.interaction.Select.prototype,
+    'dispatchEvent',
+    ol.interaction.Select.prototype.dispatchEvent);
+
+goog.exportProperty(
+    ol.interaction.Select.prototype,
+    'getRevision',
+    ol.interaction.Select.prototype.getRevision);
+
+goog.exportProperty(
+    ol.interaction.Select.prototype,
+    'on',
+    ol.interaction.Select.prototype.on);
+
+goog.exportProperty(
+    ol.interaction.Select.prototype,
+    'once',
+    ol.interaction.Select.prototype.once);
+
+goog.exportProperty(
+    ol.interaction.Select.prototype,
+    'un',
+    ol.interaction.Select.prototype.un);
+
+goog.exportProperty(
+    ol.interaction.Select.prototype,
+    'unByKey',
+    ol.interaction.Select.prototype.unByKey);
+
+goog.exportProperty(
+    ol.interaction.Snap.prototype,
+    'getActive',
+    ol.interaction.Snap.prototype.getActive);
+
+goog.exportProperty(
+    ol.interaction.Snap.prototype,
+    'getMap',
+    ol.interaction.Snap.prototype.getMap);
+
+goog.exportProperty(
+    ol.interaction.Snap.prototype,
+    'setActive',
+    ol.interaction.Snap.prototype.setActive);
+
+goog.exportProperty(
+    ol.interaction.Snap.prototype,
+    'get',
+    ol.interaction.Snap.prototype.get);
+
+goog.exportProperty(
+    ol.interaction.Snap.prototype,
+    'getKeys',
+    ol.interaction.Snap.prototype.getKeys);
+
+goog.exportProperty(
+    ol.interaction.Snap.prototype,
+    'getProperties',
+    ol.interaction.Snap.prototype.getProperties);
+
+goog.exportProperty(
+    ol.interaction.Snap.prototype,
+    'set',
+    ol.interaction.Snap.prototype.set);
+
+goog.exportProperty(
+    ol.interaction.Snap.prototype,
+    'setProperties',
+    ol.interaction.Snap.prototype.setProperties);
+
+goog.exportProperty(
+    ol.interaction.Snap.prototype,
+    'unset',
+    ol.interaction.Snap.prototype.unset);
+
+goog.exportProperty(
+    ol.interaction.Snap.prototype,
+    'changed',
+    ol.interaction.Snap.prototype.changed);
+
+goog.exportProperty(
+    ol.interaction.Snap.prototype,
+    'dispatchEvent',
+    ol.interaction.Snap.prototype.dispatchEvent);
+
+goog.exportProperty(
+    ol.interaction.Snap.prototype,
+    'getRevision',
+    ol.interaction.Snap.prototype.getRevision);
+
+goog.exportProperty(
+    ol.interaction.Snap.prototype,
+    'on',
+    ol.interaction.Snap.prototype.on);
+
+goog.exportProperty(
+    ol.interaction.Snap.prototype,
+    'once',
+    ol.interaction.Snap.prototype.once);
+
+goog.exportProperty(
+    ol.interaction.Snap.prototype,
+    'un',
+    ol.interaction.Snap.prototype.un);
+
+goog.exportProperty(
+    ol.interaction.Snap.prototype,
+    'unByKey',
+    ol.interaction.Snap.prototype.unByKey);
+
+goog.exportProperty(
+    ol.interaction.TranslateEvent.prototype,
+    'type',
+    ol.interaction.TranslateEvent.prototype.type);
+
+goog.exportProperty(
+    ol.interaction.TranslateEvent.prototype,
+    'target',
+    ol.interaction.TranslateEvent.prototype.target);
+
+goog.exportProperty(
+    ol.interaction.TranslateEvent.prototype,
+    'preventDefault',
+    ol.interaction.TranslateEvent.prototype.preventDefault);
+
+goog.exportProperty(
+    ol.interaction.TranslateEvent.prototype,
+    'stopPropagation',
+    ol.interaction.TranslateEvent.prototype.stopPropagation);
+
+goog.exportProperty(
+    ol.interaction.Translate.prototype,
+    'getActive',
+    ol.interaction.Translate.prototype.getActive);
+
+goog.exportProperty(
+    ol.interaction.Translate.prototype,
+    'getMap',
+    ol.interaction.Translate.prototype.getMap);
+
+goog.exportProperty(
+    ol.interaction.Translate.prototype,
+    'setActive',
+    ol.interaction.Translate.prototype.setActive);
+
+goog.exportProperty(
+    ol.interaction.Translate.prototype,
+    'get',
+    ol.interaction.Translate.prototype.get);
+
+goog.exportProperty(
+    ol.interaction.Translate.prototype,
+    'getKeys',
+    ol.interaction.Translate.prototype.getKeys);
+
+goog.exportProperty(
+    ol.interaction.Translate.prototype,
+    'getProperties',
+    ol.interaction.Translate.prototype.getProperties);
+
+goog.exportProperty(
+    ol.interaction.Translate.prototype,
+    'set',
+    ol.interaction.Translate.prototype.set);
+
+goog.exportProperty(
+    ol.interaction.Translate.prototype,
+    'setProperties',
+    ol.interaction.Translate.prototype.setProperties);
+
+goog.exportProperty(
+    ol.interaction.Translate.prototype,
+    'unset',
+    ol.interaction.Translate.prototype.unset);
+
+goog.exportProperty(
+    ol.interaction.Translate.prototype,
+    'changed',
+    ol.interaction.Translate.prototype.changed);
+
+goog.exportProperty(
+    ol.interaction.Translate.prototype,
+    'dispatchEvent',
+    ol.interaction.Translate.prototype.dispatchEvent);
+
+goog.exportProperty(
+    ol.interaction.Translate.prototype,
+    'getRevision',
+    ol.interaction.Translate.prototype.getRevision);
+
+goog.exportProperty(
+    ol.interaction.Translate.prototype,
+    'on',
+    ol.interaction.Translate.prototype.on);
+
+goog.exportProperty(
+    ol.interaction.Translate.prototype,
+    'once',
+    ol.interaction.Translate.prototype.once);
+
+goog.exportProperty(
+    ol.interaction.Translate.prototype,
+    'un',
+    ol.interaction.Translate.prototype.un);
+
+goog.exportProperty(
+    ol.interaction.Translate.prototype,
+    'unByKey',
+    ol.interaction.Translate.prototype.unByKey);
+
+goog.exportProperty(
+    ol.geom.Geometry.prototype,
+    'get',
+    ol.geom.Geometry.prototype.get);
+
+goog.exportProperty(
+    ol.geom.Geometry.prototype,
+    'getKeys',
+    ol.geom.Geometry.prototype.getKeys);
+
+goog.exportProperty(
+    ol.geom.Geometry.prototype,
+    'getProperties',
+    ol.geom.Geometry.prototype.getProperties);
+
+goog.exportProperty(
+    ol.geom.Geometry.prototype,
+    'set',
+    ol.geom.Geometry.prototype.set);
+
+goog.exportProperty(
+    ol.geom.Geometry.prototype,
+    'setProperties',
+    ol.geom.Geometry.prototype.setProperties);
+
+goog.exportProperty(
+    ol.geom.Geometry.prototype,
+    'unset',
+    ol.geom.Geometry.prototype.unset);
+
+goog.exportProperty(
+    ol.geom.Geometry.prototype,
+    'changed',
+    ol.geom.Geometry.prototype.changed);
+
+goog.exportProperty(
+    ol.geom.Geometry.prototype,
+    'dispatchEvent',
+    ol.geom.Geometry.prototype.dispatchEvent);
+
+goog.exportProperty(
+    ol.geom.Geometry.prototype,
+    'getRevision',
+    ol.geom.Geometry.prototype.getRevision);
+
+goog.exportProperty(
+    ol.geom.Geometry.prototype,
+    'on',
+    ol.geom.Geometry.prototype.on);
+
+goog.exportProperty(
+    ol.geom.Geometry.prototype,
+    'once',
+    ol.geom.Geometry.prototype.once);
+
+goog.exportProperty(
+    ol.geom.Geometry.prototype,
+    'un',
+    ol.geom.Geometry.prototype.un);
+
+goog.exportProperty(
+    ol.geom.Geometry.prototype,
+    'unByKey',
+    ol.geom.Geometry.prototype.unByKey);
+
+goog.exportProperty(
+    ol.geom.SimpleGeometry.prototype,
+    'getClosestPoint',
+    ol.geom.SimpleGeometry.prototype.getClosestPoint);
+
+goog.exportProperty(
+    ol.geom.SimpleGeometry.prototype,
+    'getExtent',
+    ol.geom.SimpleGeometry.prototype.getExtent);
+
+goog.exportProperty(
+    ol.geom.SimpleGeometry.prototype,
+    'rotate',
+    ol.geom.SimpleGeometry.prototype.rotate);
+
+goog.exportProperty(
+    ol.geom.SimpleGeometry.prototype,
+    'simplify',
+    ol.geom.SimpleGeometry.prototype.simplify);
+
+goog.exportProperty(
+    ol.geom.SimpleGeometry.prototype,
+    'transform',
+    ol.geom.SimpleGeometry.prototype.transform);
+
+goog.exportProperty(
+    ol.geom.SimpleGeometry.prototype,
+    'get',
+    ol.geom.SimpleGeometry.prototype.get);
+
+goog.exportProperty(
+    ol.geom.SimpleGeometry.prototype,
+    'getKeys',
+    ol.geom.SimpleGeometry.prototype.getKeys);
+
+goog.exportProperty(
+    ol.geom.SimpleGeometry.prototype,
+    'getProperties',
+    ol.geom.SimpleGeometry.prototype.getProperties);
+
+goog.exportProperty(
+    ol.geom.SimpleGeometry.prototype,
+    'set',
+    ol.geom.SimpleGeometry.prototype.set);
+
+goog.exportProperty(
+    ol.geom.SimpleGeometry.prototype,
+    'setProperties',
+    ol.geom.SimpleGeometry.prototype.setProperties);
+
+goog.exportProperty(
+    ol.geom.SimpleGeometry.prototype,
+    'unset',
+    ol.geom.SimpleGeometry.prototype.unset);
+
+goog.exportProperty(
+    ol.geom.SimpleGeometry.prototype,
+    'changed',
+    ol.geom.SimpleGeometry.prototype.changed);
+
+goog.exportProperty(
+    ol.geom.SimpleGeometry.prototype,
+    'dispatchEvent',
+    ol.geom.SimpleGeometry.prototype.dispatchEvent);
+
+goog.exportProperty(
+    ol.geom.SimpleGeometry.prototype,
+    'getRevision',
+    ol.geom.SimpleGeometry.prototype.getRevision);
+
+goog.exportProperty(
+    ol.geom.SimpleGeometry.prototype,
+    'on',
+    ol.geom.SimpleGeometry.prototype.on);
+
+goog.exportProperty(
+    ol.geom.SimpleGeometry.prototype,
+    'once',
+    ol.geom.SimpleGeometry.prototype.once);
+
+goog.exportProperty(
+    ol.geom.SimpleGeometry.prototype,
+    'un',
+    ol.geom.SimpleGeometry.prototype.un);
+
+goog.exportProperty(
+    ol.geom.SimpleGeometry.prototype,
+    'unByKey',
+    ol.geom.SimpleGeometry.prototype.unByKey);
+
+goog.exportProperty(
+    ol.geom.Circle.prototype,
+    'getFirstCoordinate',
+    ol.geom.Circle.prototype.getFirstCoordinate);
+
+goog.exportProperty(
+    ol.geom.Circle.prototype,
+    'getLastCoordinate',
+    ol.geom.Circle.prototype.getLastCoordinate);
+
+goog.exportProperty(
+    ol.geom.Circle.prototype,
+    'getLayout',
+    ol.geom.Circle.prototype.getLayout);
+
+goog.exportProperty(
+    ol.geom.Circle.prototype,
+    'rotate',
+    ol.geom.Circle.prototype.rotate);
+
+goog.exportProperty(
+    ol.geom.Circle.prototype,
+    'getClosestPoint',
+    ol.geom.Circle.prototype.getClosestPoint);
+
+goog.exportProperty(
+    ol.geom.Circle.prototype,
+    'getExtent',
+    ol.geom.Circle.prototype.getExtent);
+
+goog.exportProperty(
+    ol.geom.Circle.prototype,
+    'simplify',
+    ol.geom.Circle.prototype.simplify);
+
+goog.exportProperty(
+    ol.geom.Circle.prototype,
+    'get',
+    ol.geom.Circle.prototype.get);
+
+goog.exportProperty(
+    ol.geom.Circle.prototype,
+    'getKeys',
+    ol.geom.Circle.prototype.getKeys);
+
+goog.exportProperty(
+    ol.geom.Circle.prototype,
+    'getProperties',
+    ol.geom.Circle.prototype.getProperties);
+
+goog.exportProperty(
+    ol.geom.Circle.prototype,
+    'set',
+    ol.geom.Circle.prototype.set);
+
+goog.exportProperty(
+    ol.geom.Circle.prototype,
+    'setProperties',
+    ol.geom.Circle.prototype.setProperties);
+
+goog.exportProperty(
+    ol.geom.Circle.prototype,
+    'unset',
+    ol.geom.Circle.prototype.unset);
+
+goog.exportProperty(
+    ol.geom.Circle.prototype,
+    'changed',
+    ol.geom.Circle.prototype.changed);
+
+goog.exportProperty(
+    ol.geom.Circle.prototype,
+    'dispatchEvent',
+    ol.geom.Circle.prototype.dispatchEvent);
+
+goog.exportProperty(
+    ol.geom.Circle.prototype,
+    'getRevision',
+    ol.geom.Circle.prototype.getRevision);
+
+goog.exportProperty(
+    ol.geom.Circle.prototype,
+    'on',
+    ol.geom.Circle.prototype.on);
+
+goog.exportProperty(
+    ol.geom.Circle.prototype,
+    'once',
+    ol.geom.Circle.prototype.once);
+
+goog.exportProperty(
+    ol.geom.Circle.prototype,
+    'un',
+    ol.geom.Circle.prototype.un);
+
+goog.exportProperty(
+    ol.geom.Circle.prototype,
+    'unByKey',
+    ol.geom.Circle.prototype.unByKey);
+
+goog.exportProperty(
+    ol.geom.GeometryCollection.prototype,
+    'getClosestPoint',
+    ol.geom.GeometryCollection.prototype.getClosestPoint);
+
+goog.exportProperty(
+    ol.geom.GeometryCollection.prototype,
+    'getExtent',
+    ol.geom.GeometryCollection.prototype.getExtent);
+
+goog.exportProperty(
+    ol.geom.GeometryCollection.prototype,
+    'rotate',
+    ol.geom.GeometryCollection.prototype.rotate);
+
+goog.exportProperty(
+    ol.geom.GeometryCollection.prototype,
+    'simplify',
+    ol.geom.GeometryCollection.prototype.simplify);
+
+goog.exportProperty(
+    ol.geom.GeometryCollection.prototype,
+    'transform',
+    ol.geom.GeometryCollection.prototype.transform);
+
+goog.exportProperty(
+    ol.geom.GeometryCollection.prototype,
+    'get',
+    ol.geom.GeometryCollection.prototype.get);
+
+goog.exportProperty(
+    ol.geom.GeometryCollection.prototype,
+    'getKeys',
+    ol.geom.GeometryCollection.prototype.getKeys);
+
+goog.exportProperty(
+    ol.geom.GeometryCollection.prototype,
+    'getProperties',
+    ol.geom.GeometryCollection.prototype.getProperties);
+
+goog.exportProperty(
+    ol.geom.GeometryCollection.prototype,
+    'set',
+    ol.geom.GeometryCollection.prototype.set);
+
+goog.exportProperty(
+    ol.geom.GeometryCollection.prototype,
+    'setProperties',
+    ol.geom.GeometryCollection.prototype.setProperties);
+
+goog.exportProperty(
+    ol.geom.GeometryCollection.prototype,
+    'unset',
+    ol.geom.GeometryCollection.prototype.unset);
+
+goog.exportProperty(
+    ol.geom.GeometryCollection.prototype,
+    'changed',
+    ol.geom.GeometryCollection.prototype.changed);
+
+goog.exportProperty(
+    ol.geom.GeometryCollection.prototype,
+    'dispatchEvent',
+    ol.geom.GeometryCollection.prototype.dispatchEvent);
+
+goog.exportProperty(
+    ol.geom.GeometryCollection.prototype,
+    'getRevision',
+    ol.geom.GeometryCollection.prototype.getRevision);
+
+goog.exportProperty(
+    ol.geom.GeometryCollection.prototype,
+    'on',
+    ol.geom.GeometryCollection.prototype.on);
+
+goog.exportProperty(
+    ol.geom.GeometryCollection.prototype,
+    'once',
+    ol.geom.GeometryCollection.prototype.once);
+
+goog.exportProperty(
+    ol.geom.GeometryCollection.prototype,
+    'un',
+    ol.geom.GeometryCollection.prototype.un);
+
+goog.exportProperty(
+    ol.geom.GeometryCollection.prototype,
+    'unByKey',
+    ol.geom.GeometryCollection.prototype.unByKey);
+
+goog.exportProperty(
+    ol.geom.LinearRing.prototype,
+    'getFirstCoordinate',
+    ol.geom.LinearRing.prototype.getFirstCoordinate);
+
+goog.exportProperty(
+    ol.geom.LinearRing.prototype,
+    'getLastCoordinate',
+    ol.geom.LinearRing.prototype.getLastCoordinate);
+
+goog.exportProperty(
+    ol.geom.LinearRing.prototype,
+    'getLayout',
+    ol.geom.LinearRing.prototype.getLayout);
+
+goog.exportProperty(
+    ol.geom.LinearRing.prototype,
+    'rotate',
+    ol.geom.LinearRing.prototype.rotate);
+
+goog.exportProperty(
+    ol.geom.LinearRing.prototype,
+    'getClosestPoint',
+    ol.geom.LinearRing.prototype.getClosestPoint);
+
+goog.exportProperty(
+    ol.geom.LinearRing.prototype,
+    'getExtent',
+    ol.geom.LinearRing.prototype.getExtent);
+
+goog.exportProperty(
+    ol.geom.LinearRing.prototype,
+    'simplify',
+    ol.geom.LinearRing.prototype.simplify);
+
+goog.exportProperty(
+    ol.geom.LinearRing.prototype,
+    'transform',
+    ol.geom.LinearRing.prototype.transform);
+
+goog.exportProperty(
+    ol.geom.LinearRing.prototype,
+    'get',
+    ol.geom.LinearRing.prototype.get);
+
+goog.exportProperty(
+    ol.geom.LinearRing.prototype,
+    'getKeys',
+    ol.geom.LinearRing.prototype.getKeys);
+
+goog.exportProperty(
+    ol.geom.LinearRing.prototype,
+    'getProperties',
+    ol.geom.LinearRing.prototype.getProperties);
+
+goog.exportProperty(
+    ol.geom.LinearRing.prototype,
+    'set',
+    ol.geom.LinearRing.prototype.set);
+
+goog.exportProperty(
+    ol.geom.LinearRing.prototype,
+    'setProperties',
+    ol.geom.LinearRing.prototype.setProperties);
+
+goog.exportProperty(
+    ol.geom.LinearRing.prototype,
+    'unset',
+    ol.geom.LinearRing.prototype.unset);
+
+goog.exportProperty(
+    ol.geom.LinearRing.prototype,
+    'changed',
+    ol.geom.LinearRing.prototype.changed);
+
+goog.exportProperty(
+    ol.geom.LinearRing.prototype,
+    'dispatchEvent',
+    ol.geom.LinearRing.prototype.dispatchEvent);
+
+goog.exportProperty(
+    ol.geom.LinearRing.prototype,
+    'getRevision',
+    ol.geom.LinearRing.prototype.getRevision);
+
+goog.exportProperty(
+    ol.geom.LinearRing.prototype,
+    'on',
+    ol.geom.LinearRing.prototype.on);
+
+goog.exportProperty(
+    ol.geom.LinearRing.prototype,
+    'once',
+    ol.geom.LinearRing.prototype.once);
+
+goog.exportProperty(
+    ol.geom.LinearRing.prototype,
+    'un',
+    ol.geom.LinearRing.prototype.un);
+
+goog.exportProperty(
+    ol.geom.LinearRing.prototype,
+    'unByKey',
+    ol.geom.LinearRing.prototype.unByKey);
+
+goog.exportProperty(
+    ol.geom.LineString.prototype,
+    'getFirstCoordinate',
+    ol.geom.LineString.prototype.getFirstCoordinate);
+
+goog.exportProperty(
+    ol.geom.LineString.prototype,
+    'getLastCoordinate',
+    ol.geom.LineString.prototype.getLastCoordinate);
+
+goog.exportProperty(
+    ol.geom.LineString.prototype,
+    'getLayout',
+    ol.geom.LineString.prototype.getLayout);
+
+goog.exportProperty(
+    ol.geom.LineString.prototype,
+    'rotate',
+    ol.geom.LineString.prototype.rotate);
+
+goog.exportProperty(
+    ol.geom.LineString.prototype,
+    'getClosestPoint',
+    ol.geom.LineString.prototype.getClosestPoint);
+
+goog.exportProperty(
+    ol.geom.LineString.prototype,
+    'getExtent',
+    ol.geom.LineString.prototype.getExtent);
+
+goog.exportProperty(
+    ol.geom.LineString.prototype,
+    'simplify',
+    ol.geom.LineString.prototype.simplify);
+
+goog.exportProperty(
+    ol.geom.LineString.prototype,
+    'transform',
+    ol.geom.LineString.prototype.transform);
+
+goog.exportProperty(
+    ol.geom.LineString.prototype,
+    'get',
+    ol.geom.LineString.prototype.get);
+
+goog.exportProperty(
+    ol.geom.LineString.prototype,
+    'getKeys',
+    ol.geom.LineString.prototype.getKeys);
+
+goog.exportProperty(
+    ol.geom.LineString.prototype,
+    'getProperties',
+    ol.geom.LineString.prototype.getProperties);
+
+goog.exportProperty(
+    ol.geom.LineString.prototype,
+    'set',
+    ol.geom.LineString.prototype.set);
+
+goog.exportProperty(
+    ol.geom.LineString.prototype,
+    'setProperties',
+    ol.geom.LineString.prototype.setProperties);
+
+goog.exportProperty(
+    ol.geom.LineString.prototype,
+    'unset',
+    ol.geom.LineString.prototype.unset);
+
+goog.exportProperty(
+    ol.geom.LineString.prototype,
+    'changed',
+    ol.geom.LineString.prototype.changed);
+
+goog.exportProperty(
+    ol.geom.LineString.prototype,
+    'dispatchEvent',
+    ol.geom.LineString.prototype.dispatchEvent);
+
+goog.exportProperty(
+    ol.geom.LineString.prototype,
+    'getRevision',
+    ol.geom.LineString.prototype.getRevision);
+
+goog.exportProperty(
+    ol.geom.LineString.prototype,
+    'on',
+    ol.geom.LineString.prototype.on);
+
+goog.exportProperty(
+    ol.geom.LineString.prototype,
+    'once',
+    ol.geom.LineString.prototype.once);
+
+goog.exportProperty(
+    ol.geom.LineString.prototype,
+    'un',
+    ol.geom.LineString.prototype.un);
+
+goog.exportProperty(
+    ol.geom.LineString.prototype,
+    'unByKey',
+    ol.geom.LineString.prototype.unByKey);
+
+goog.exportProperty(
+    ol.geom.MultiLineString.prototype,
+    'getFirstCoordinate',
+    ol.geom.MultiLineString.prototype.getFirstCoordinate);
+
+goog.exportProperty(
+    ol.geom.MultiLineString.prototype,
+    'getLastCoordinate',
+    ol.geom.MultiLineString.prototype.getLastCoordinate);
+
+goog.exportProperty(
+    ol.geom.MultiLineString.prototype,
+    'getLayout',
+    ol.geom.MultiLineString.prototype.getLayout);
+
+goog.exportProperty(
+    ol.geom.MultiLineString.prototype,
+    'rotate',
+    ol.geom.MultiLineString.prototype.rotate);
+
+goog.exportProperty(
+    ol.geom.MultiLineString.prototype,
+    'getClosestPoint',
+    ol.geom.MultiLineString.prototype.getClosestPoint);
+
+goog.exportProperty(
+    ol.geom.MultiLineString.prototype,
+    'getExtent',
+    ol.geom.MultiLineString.prototype.getExtent);
+
+goog.exportProperty(
+    ol.geom.MultiLineString.prototype,
+    'simplify',
+    ol.geom.MultiLineString.prototype.simplify);
+
+goog.exportProperty(
+    ol.geom.MultiLineString.prototype,
+    'transform',
+    ol.geom.MultiLineString.prototype.transform);
+
+goog.exportProperty(
+    ol.geom.MultiLineString.prototype,
+    'get',
+    ol.geom.MultiLineString.prototype.get);
+
+goog.exportProperty(
+    ol.geom.MultiLineString.prototype,
+    'getKeys',
+    ol.geom.MultiLineString.prototype.getKeys);
+
+goog.exportProperty(
+    ol.geom.MultiLineString.prototype,
+    'getProperties',
+    ol.geom.MultiLineString.prototype.getProperties);
+
+goog.exportProperty(
+    ol.geom.MultiLineString.prototype,
+    'set',
+    ol.geom.MultiLineString.prototype.set);
+
+goog.exportProperty(
+    ol.geom.MultiLineString.prototype,
+    'setProperties',
+    ol.geom.MultiLineString.prototype.setProperties);
+
+goog.exportProperty(
+    ol.geom.MultiLineString.prototype,
+    'unset',
+    ol.geom.MultiLineString.prototype.unset);
+
+goog.exportProperty(
+    ol.geom.MultiLineString.prototype,
+    'changed',
+    ol.geom.MultiLineString.prototype.changed);
+
+goog.exportProperty(
+    ol.geom.MultiLineString.prototype,
+    'dispatchEvent',
+    ol.geom.MultiLineString.prototype.dispatchEvent);
+
+goog.exportProperty(
+    ol.geom.MultiLineString.prototype,
+    'getRevision',
+    ol.geom.MultiLineString.prototype.getRevision);
+
+goog.exportProperty(
+    ol.geom.MultiLineString.prototype,
+    'on',
+    ol.geom.MultiLineString.prototype.on);
+
+goog.exportProperty(
+    ol.geom.MultiLineString.prototype,
+    'once',
+    ol.geom.MultiLineString.prototype.once);
+
+goog.exportProperty(
+    ol.geom.MultiLineString.prototype,
+    'un',
+    ol.geom.MultiLineString.prototype.un);
+
+goog.exportProperty(
+    ol.geom.MultiLineString.prototype,
+    'unByKey',
+    ol.geom.MultiLineString.prototype.unByKey);
+
+goog.exportProperty(
+    ol.geom.MultiPoint.prototype,
+    'getFirstCoordinate',
+    ol.geom.MultiPoint.prototype.getFirstCoordinate);
+
+goog.exportProperty(
+    ol.geom.MultiPoint.prototype,
+    'getLastCoordinate',
+    ol.geom.MultiPoint.prototype.getLastCoordinate);
+
+goog.exportProperty(
+    ol.geom.MultiPoint.prototype,
+    'getLayout',
+    ol.geom.MultiPoint.prototype.getLayout);
+
+goog.exportProperty(
+    ol.geom.MultiPoint.prototype,
+    'rotate',
+    ol.geom.MultiPoint.prototype.rotate);
+
+goog.exportProperty(
+    ol.geom.MultiPoint.prototype,
+    'getClosestPoint',
+    ol.geom.MultiPoint.prototype.getClosestPoint);
+
+goog.exportProperty(
+    ol.geom.MultiPoint.prototype,
+    'getExtent',
+    ol.geom.MultiPoint.prototype.getExtent);
+
+goog.exportProperty(
+    ol.geom.MultiPoint.prototype,
+    'simplify',
+    ol.geom.MultiPoint.prototype.simplify);
+
+goog.exportProperty(
+    ol.geom.MultiPoint.prototype,
+    'transform',
+    ol.geom.MultiPoint.prototype.transform);
+
+goog.exportProperty(
+    ol.geom.MultiPoint.prototype,
+    'get',
+    ol.geom.MultiPoint.prototype.get);
+
+goog.exportProperty(
+    ol.geom.MultiPoint.prototype,
+    'getKeys',
+    ol.geom.MultiPoint.prototype.getKeys);
+
+goog.exportProperty(
+    ol.geom.MultiPoint.prototype,
+    'getProperties',
+    ol.geom.MultiPoint.prototype.getProperties);
+
+goog.exportProperty(
+    ol.geom.MultiPoint.prototype,
+    'set',
+    ol.geom.MultiPoint.prototype.set);
+
+goog.exportProperty(
+    ol.geom.MultiPoint.prototype,
+    'setProperties',
+    ol.geom.MultiPoint.prototype.setProperties);
+
+goog.exportProperty(
+    ol.geom.MultiPoint.prototype,
+    'unset',
+    ol.geom.MultiPoint.prototype.unset);
+
+goog.exportProperty(
+    ol.geom.MultiPoint.prototype,
+    'changed',
+    ol.geom.MultiPoint.prototype.changed);
+
+goog.exportProperty(
+    ol.geom.MultiPoint.prototype,
+    'dispatchEvent',
+    ol.geom.MultiPoint.prototype.dispatchEvent);
+
+goog.exportProperty(
+    ol.geom.MultiPoint.prototype,
+    'getRevision',
+    ol.geom.MultiPoint.prototype.getRevision);
+
+goog.exportProperty(
+    ol.geom.MultiPoint.prototype,
+    'on',
+    ol.geom.MultiPoint.prototype.on);
+
+goog.exportProperty(
+    ol.geom.MultiPoint.prototype,
+    'once',
+    ol.geom.MultiPoint.prototype.once);
+
+goog.exportProperty(
+    ol.geom.MultiPoint.prototype,
+    'un',
+    ol.geom.MultiPoint.prototype.un);
+
+goog.exportProperty(
+    ol.geom.MultiPoint.prototype,
+    'unByKey',
+    ol.geom.MultiPoint.prototype.unByKey);
+
+goog.exportProperty(
+    ol.geom.MultiPolygon.prototype,
+    'getFirstCoordinate',
+    ol.geom.MultiPolygon.prototype.getFirstCoordinate);
+
+goog.exportProperty(
+    ol.geom.MultiPolygon.prototype,
+    'getLastCoordinate',
+    ol.geom.MultiPolygon.prototype.getLastCoordinate);
+
+goog.exportProperty(
+    ol.geom.MultiPolygon.prototype,
+    'getLayout',
+    ol.geom.MultiPolygon.prototype.getLayout);
+
+goog.exportProperty(
+    ol.geom.MultiPolygon.prototype,
+    'rotate',
+    ol.geom.MultiPolygon.prototype.rotate);
+
+goog.exportProperty(
+    ol.geom.MultiPolygon.prototype,
+    'getClosestPoint',
+    ol.geom.MultiPolygon.prototype.getClosestPoint);
+
+goog.exportProperty(
+    ol.geom.MultiPolygon.prototype,
+    'getExtent',
+    ol.geom.MultiPolygon.prototype.getExtent);
+
+goog.exportProperty(
+    ol.geom.MultiPolygon.prototype,
+    'simplify',
+    ol.geom.MultiPolygon.prototype.simplify);
+
+goog.exportProperty(
+    ol.geom.MultiPolygon.prototype,
+    'transform',
+    ol.geom.MultiPolygon.prototype.transform);
+
+goog.exportProperty(
+    ol.geom.MultiPolygon.prototype,
+    'get',
+    ol.geom.MultiPolygon.prototype.get);
+
+goog.exportProperty(
+    ol.geom.MultiPolygon.prototype,
+    'getKeys',
+    ol.geom.MultiPolygon.prototype.getKeys);
+
+goog.exportProperty(
+    ol.geom.MultiPolygon.prototype,
+    'getProperties',
+    ol.geom.MultiPolygon.prototype.getProperties);
+
+goog.exportProperty(
+    ol.geom.MultiPolygon.prototype,
+    'set',
+    ol.geom.MultiPolygon.prototype.set);
+
+goog.exportProperty(
+    ol.geom.MultiPolygon.prototype,
+    'setProperties',
+    ol.geom.MultiPolygon.prototype.setProperties);
+
+goog.exportProperty(
+    ol.geom.MultiPolygon.prototype,
+    'unset',
+    ol.geom.MultiPolygon.prototype.unset);
+
+goog.exportProperty(
+    ol.geom.MultiPolygon.prototype,
+    'changed',
+    ol.geom.MultiPolygon.prototype.changed);
+
+goog.exportProperty(
+    ol.geom.MultiPolygon.prototype,
+    'dispatchEvent',
+    ol.geom.MultiPolygon.prototype.dispatchEvent);
+
+goog.exportProperty(
+    ol.geom.MultiPolygon.prototype,
+    'getRevision',
+    ol.geom.MultiPolygon.prototype.getRevision);
+
+goog.exportProperty(
+    ol.geom.MultiPolygon.prototype,
+    'on',
+    ol.geom.MultiPolygon.prototype.on);
+
+goog.exportProperty(
+    ol.geom.MultiPolygon.prototype,
+    'once',
+    ol.geom.MultiPolygon.prototype.once);
+
+goog.exportProperty(
+    ol.geom.MultiPolygon.prototype,
+    'un',
+    ol.geom.MultiPolygon.prototype.un);
+
+goog.exportProperty(
+    ol.geom.MultiPolygon.prototype,
+    'unByKey',
+    ol.geom.MultiPolygon.prototype.unByKey);
+
+goog.exportProperty(
+    ol.geom.Point.prototype,
+    'getFirstCoordinate',
+    ol.geom.Point.prototype.getFirstCoordinate);
+
+goog.exportProperty(
+    ol.geom.Point.prototype,
+    'getLastCoordinate',
+    ol.geom.Point.prototype.getLastCoordinate);
+
+goog.exportProperty(
+    ol.geom.Point.prototype,
+    'getLayout',
+    ol.geom.Point.prototype.getLayout);
+
+goog.exportProperty(
+    ol.geom.Point.prototype,
+    'rotate',
+    ol.geom.Point.prototype.rotate);
+
+goog.exportProperty(
+    ol.geom.Point.prototype,
+    'getClosestPoint',
+    ol.geom.Point.prototype.getClosestPoint);
+
+goog.exportProperty(
+    ol.geom.Point.prototype,
+    'getExtent',
+    ol.geom.Point.prototype.getExtent);
+
+goog.exportProperty(
+    ol.geom.Point.prototype,
+    'simplify',
+    ol.geom.Point.prototype.simplify);
+
+goog.exportProperty(
+    ol.geom.Point.prototype,
+    'transform',
+    ol.geom.Point.prototype.transform);
+
+goog.exportProperty(
+    ol.geom.Point.prototype,
+    'get',
+    ol.geom.Point.prototype.get);
+
+goog.exportProperty(
+    ol.geom.Point.prototype,
+    'getKeys',
+    ol.geom.Point.prototype.getKeys);
+
+goog.exportProperty(
+    ol.geom.Point.prototype,
+    'getProperties',
+    ol.geom.Point.prototype.getProperties);
+
+goog.exportProperty(
+    ol.geom.Point.prototype,
+    'set',
+    ol.geom.Point.prototype.set);
+
+goog.exportProperty(
+    ol.geom.Point.prototype,
+    'setProperties',
+    ol.geom.Point.prototype.setProperties);
+
+goog.exportProperty(
+    ol.geom.Point.prototype,
+    'unset',
+    ol.geom.Point.prototype.unset);
+
+goog.exportProperty(
+    ol.geom.Point.prototype,
+    'changed',
+    ol.geom.Point.prototype.changed);
+
+goog.exportProperty(
+    ol.geom.Point.prototype,
+    'dispatchEvent',
+    ol.geom.Point.prototype.dispatchEvent);
+
+goog.exportProperty(
+    ol.geom.Point.prototype,
+    'getRevision',
+    ol.geom.Point.prototype.getRevision);
+
+goog.exportProperty(
+    ol.geom.Point.prototype,
+    'on',
+    ol.geom.Point.prototype.on);
+
+goog.exportProperty(
+    ol.geom.Point.prototype,
+    'once',
+    ol.geom.Point.prototype.once);
+
+goog.exportProperty(
+    ol.geom.Point.prototype,
+    'un',
+    ol.geom.Point.prototype.un);
+
+goog.exportProperty(
+    ol.geom.Point.prototype,
+    'unByKey',
+    ol.geom.Point.prototype.unByKey);
+
+goog.exportProperty(
+    ol.geom.Polygon.prototype,
+    'getFirstCoordinate',
+    ol.geom.Polygon.prototype.getFirstCoordinate);
+
+goog.exportProperty(
+    ol.geom.Polygon.prototype,
+    'getLastCoordinate',
+    ol.geom.Polygon.prototype.getLastCoordinate);
+
+goog.exportProperty(
+    ol.geom.Polygon.prototype,
+    'getLayout',
+    ol.geom.Polygon.prototype.getLayout);
+
+goog.exportProperty(
+    ol.geom.Polygon.prototype,
+    'rotate',
+    ol.geom.Polygon.prototype.rotate);
+
+goog.exportProperty(
+    ol.geom.Polygon.prototype,
+    'getClosestPoint',
+    ol.geom.Polygon.prototype.getClosestPoint);
+
+goog.exportProperty(
+    ol.geom.Polygon.prototype,
+    'getExtent',
+    ol.geom.Polygon.prototype.getExtent);
+
+goog.exportProperty(
+    ol.geom.Polygon.prototype,
+    'simplify',
+    ol.geom.Polygon.prototype.simplify);
+
+goog.exportProperty(
+    ol.geom.Polygon.prototype,
+    'transform',
+    ol.geom.Polygon.prototype.transform);
+
+goog.exportProperty(
+    ol.geom.Polygon.prototype,
+    'get',
+    ol.geom.Polygon.prototype.get);
+
+goog.exportProperty(
+    ol.geom.Polygon.prototype,
+    'getKeys',
+    ol.geom.Polygon.prototype.getKeys);
+
+goog.exportProperty(
+    ol.geom.Polygon.prototype,
+    'getProperties',
+    ol.geom.Polygon.prototype.getProperties);
+
+goog.exportProperty(
+    ol.geom.Polygon.prototype,
+    'set',
+    ol.geom.Polygon.prototype.set);
+
+goog.exportProperty(
+    ol.geom.Polygon.prototype,
+    'setProperties',
+    ol.geom.Polygon.prototype.setProperties);
+
+goog.exportProperty(
+    ol.geom.Polygon.prototype,
+    'unset',
+    ol.geom.Polygon.prototype.unset);
+
+goog.exportProperty(
+    ol.geom.Polygon.prototype,
+    'changed',
+    ol.geom.Polygon.prototype.changed);
+
+goog.exportProperty(
+    ol.geom.Polygon.prototype,
+    'dispatchEvent',
+    ol.geom.Polygon.prototype.dispatchEvent);
+
+goog.exportProperty(
+    ol.geom.Polygon.prototype,
+    'getRevision',
+    ol.geom.Polygon.prototype.getRevision);
+
+goog.exportProperty(
+    ol.geom.Polygon.prototype,
+    'on',
+    ol.geom.Polygon.prototype.on);
+
+goog.exportProperty(
+    ol.geom.Polygon.prototype,
+    'once',
+    ol.geom.Polygon.prototype.once);
+
+goog.exportProperty(
+    ol.geom.Polygon.prototype,
+    'un',
+    ol.geom.Polygon.prototype.un);
+
+goog.exportProperty(
+    ol.geom.Polygon.prototype,
+    'unByKey',
+    ol.geom.Polygon.prototype.unByKey);
+
+goog.exportProperty(
+    ol.format.GML2.prototype,
+    'readFeatures',
+    ol.format.GML2.prototype.readFeatures);
+
+goog.exportProperty(
+    ol.format.GML3.prototype,
+    'readFeatures',
+    ol.format.GML3.prototype.readFeatures);
+
+goog.exportProperty(
+    ol.format.GML.prototype,
+    'readFeatures',
+    ol.format.GML.prototype.readFeatures);
+
+goog.exportProperty(
+    ol.control.Control.prototype,
+    'get',
+    ol.control.Control.prototype.get);
+
+goog.exportProperty(
+    ol.control.Control.prototype,
+    'getKeys',
+    ol.control.Control.prototype.getKeys);
+
+goog.exportProperty(
+    ol.control.Control.prototype,
+    'getProperties',
+    ol.control.Control.prototype.getProperties);
+
+goog.exportProperty(
+    ol.control.Control.prototype,
+    'set',
+    ol.control.Control.prototype.set);
+
+goog.exportProperty(
+    ol.control.Control.prototype,
+    'setProperties',
+    ol.control.Control.prototype.setProperties);
+
+goog.exportProperty(
+    ol.control.Control.prototype,
+    'unset',
+    ol.control.Control.prototype.unset);
+
+goog.exportProperty(
+    ol.control.Control.prototype,
+    'changed',
+    ol.control.Control.prototype.changed);
+
+goog.exportProperty(
+    ol.control.Control.prototype,
+    'dispatchEvent',
+    ol.control.Control.prototype.dispatchEvent);
+
+goog.exportProperty(
+    ol.control.Control.prototype,
+    'getRevision',
+    ol.control.Control.prototype.getRevision);
+
+goog.exportProperty(
+    ol.control.Control.prototype,
+    'on',
+    ol.control.Control.prototype.on);
+
+goog.exportProperty(
+    ol.control.Control.prototype,
+    'once',
+    ol.control.Control.prototype.once);
+
+goog.exportProperty(
+    ol.control.Control.prototype,
+    'un',
+    ol.control.Control.prototype.un);
+
+goog.exportProperty(
+    ol.control.Control.prototype,
+    'unByKey',
+    ol.control.Control.prototype.unByKey);
+
+goog.exportProperty(
+    ol.control.Attribution.prototype,
+    'getMap',
+    ol.control.Attribution.prototype.getMap);
+
+goog.exportProperty(
+    ol.control.Attribution.prototype,
+    'setMap',
+    ol.control.Attribution.prototype.setMap);
+
+goog.exportProperty(
+    ol.control.Attribution.prototype,
+    'setTarget',
+    ol.control.Attribution.prototype.setTarget);
+
+goog.exportProperty(
+    ol.control.Attribution.prototype,
+    'get',
+    ol.control.Attribution.prototype.get);
+
+goog.exportProperty(
+    ol.control.Attribution.prototype,
+    'getKeys',
+    ol.control.Attribution.prototype.getKeys);
+
+goog.exportProperty(
+    ol.control.Attribution.prototype,
+    'getProperties',
+    ol.control.Attribution.prototype.getProperties);
+
+goog.exportProperty(
+    ol.control.Attribution.prototype,
+    'set',
+    ol.control.Attribution.prototype.set);
+
+goog.exportProperty(
+    ol.control.Attribution.prototype,
+    'setProperties',
+    ol.control.Attribution.prototype.setProperties);
+
+goog.exportProperty(
+    ol.control.Attribution.prototype,
+    'unset',
+    ol.control.Attribution.prototype.unset);
+
+goog.exportProperty(
+    ol.control.Attribution.prototype,
+    'changed',
+    ol.control.Attribution.prototype.changed);
+
+goog.exportProperty(
+    ol.control.Attribution.prototype,
+    'dispatchEvent',
+    ol.control.Attribution.prototype.dispatchEvent);
+
+goog.exportProperty(
+    ol.control.Attribution.prototype,
+    'getRevision',
+    ol.control.Attribution.prototype.getRevision);
+
+goog.exportProperty(
+    ol.control.Attribution.prototype,
+    'on',
+    ol.control.Attribution.prototype.on);
+
+goog.exportProperty(
+    ol.control.Attribution.prototype,
+    'once',
+    ol.control.Attribution.prototype.once);
+
+goog.exportProperty(
+    ol.control.Attribution.prototype,
+    'un',
+    ol.control.Attribution.prototype.un);
+
+goog.exportProperty(
+    ol.control.Attribution.prototype,
+    'unByKey',
+    ol.control.Attribution.prototype.unByKey);
+
+goog.exportProperty(
+    ol.control.FullScreen.prototype,
+    'getMap',
+    ol.control.FullScreen.prototype.getMap);
+
+goog.exportProperty(
+    ol.control.FullScreen.prototype,
+    'setMap',
+    ol.control.FullScreen.prototype.setMap);
+
+goog.exportProperty(
+    ol.control.FullScreen.prototype,
+    'setTarget',
+    ol.control.FullScreen.prototype.setTarget);
+
+goog.exportProperty(
+    ol.control.FullScreen.prototype,
+    'get',
+    ol.control.FullScreen.prototype.get);
+
+goog.exportProperty(
+    ol.control.FullScreen.prototype,
+    'getKeys',
+    ol.control.FullScreen.prototype.getKeys);
+
+goog.exportProperty(
+    ol.control.FullScreen.prototype,
+    'getProperties',
+    ol.control.FullScreen.prototype.getProperties);
+
+goog.exportProperty(
+    ol.control.FullScreen.prototype,
+    'set',
+    ol.control.FullScreen.prototype.set);
+
+goog.exportProperty(
+    ol.control.FullScreen.prototype,
+    'setProperties',
+    ol.control.FullScreen.prototype.setProperties);
+
+goog.exportProperty(
+    ol.control.FullScreen.prototype,
+    'unset',
+    ol.control.FullScreen.prototype.unset);
+
+goog.exportProperty(
+    ol.control.FullScreen.prototype,
+    'changed',
+    ol.control.FullScreen.prototype.changed);
+
+goog.exportProperty(
+    ol.control.FullScreen.prototype,
+    'dispatchEvent',
+    ol.control.FullScreen.prototype.dispatchEvent);
+
+goog.exportProperty(
+    ol.control.FullScreen.prototype,
+    'getRevision',
+    ol.control.FullScreen.prototype.getRevision);
+
+goog.exportProperty(
+    ol.control.FullScreen.prototype,
+    'on',
+    ol.control.FullScreen.prototype.on);
+
+goog.exportProperty(
+    ol.control.FullScreen.prototype,
+    'once',
+    ol.control.FullScreen.prototype.once);
+
+goog.exportProperty(
+    ol.control.FullScreen.prototype,
+    'un',
+    ol.control.FullScreen.prototype.un);
+
+goog.exportProperty(
+    ol.control.FullScreen.prototype,
+    'unByKey',
+    ol.control.FullScreen.prototype.unByKey);
+
+goog.exportProperty(
+    ol.control.MousePosition.prototype,
+    'getMap',
+    ol.control.MousePosition.prototype.getMap);
+
+goog.exportProperty(
+    ol.control.MousePosition.prototype,
+    'setMap',
+    ol.control.MousePosition.prototype.setMap);
+
+goog.exportProperty(
+    ol.control.MousePosition.prototype,
+    'setTarget',
+    ol.control.MousePosition.prototype.setTarget);
+
+goog.exportProperty(
+    ol.control.MousePosition.prototype,
+    'get',
+    ol.control.MousePosition.prototype.get);
+
+goog.exportProperty(
+    ol.control.MousePosition.prototype,
+    'getKeys',
+    ol.control.MousePosition.prototype.getKeys);
+
+goog.exportProperty(
+    ol.control.MousePosition.prototype,
+    'getProperties',
+    ol.control.MousePosition.prototype.getProperties);
+
+goog.exportProperty(
+    ol.control.MousePosition.prototype,
+    'set',
+    ol.control.MousePosition.prototype.set);
+
+goog.exportProperty(
+    ol.control.MousePosition.prototype,
+    'setProperties',
+    ol.control.MousePosition.prototype.setProperties);
+
+goog.exportProperty(
+    ol.control.MousePosition.prototype,
+    'unset',
+    ol.control.MousePosition.prototype.unset);
+
+goog.exportProperty(
+    ol.control.MousePosition.prototype,
+    'changed',
+    ol.control.MousePosition.prototype.changed);
+
+goog.exportProperty(
+    ol.control.MousePosition.prototype,
+    'dispatchEvent',
+    ol.control.MousePosition.prototype.dispatchEvent);
+
+goog.exportProperty(
+    ol.control.MousePosition.prototype,
+    'getRevision',
+    ol.control.MousePosition.prototype.getRevision);
+
+goog.exportProperty(
+    ol.control.MousePosition.prototype,
+    'on',
+    ol.control.MousePosition.prototype.on);
+
+goog.exportProperty(
+    ol.control.MousePosition.prototype,
+    'once',
+    ol.control.MousePosition.prototype.once);
+
+goog.exportProperty(
+    ol.control.MousePosition.prototype,
+    'un',
+    ol.control.MousePosition.prototype.un);
+
+goog.exportProperty(
+    ol.control.MousePosition.prototype,
+    'unByKey',
+    ol.control.MousePosition.prototype.unByKey);
+
+goog.exportProperty(
+    ol.control.OverviewMap.prototype,
+    'getMap',
+    ol.control.OverviewMap.prototype.getMap);
+
+goog.exportProperty(
+    ol.control.OverviewMap.prototype,
+    'setMap',
+    ol.control.OverviewMap.prototype.setMap);
+
+goog.exportProperty(
+    ol.control.OverviewMap.prototype,
+    'setTarget',
+    ol.control.OverviewMap.prototype.setTarget);
+
+goog.exportProperty(
+    ol.control.OverviewMap.prototype,
+    'get',
+    ol.control.OverviewMap.prototype.get);
+
+goog.exportProperty(
+    ol.control.OverviewMap.prototype,
+    'getKeys',
+    ol.control.OverviewMap.prototype.getKeys);
+
+goog.exportProperty(
+    ol.control.OverviewMap.prototype,
+    'getProperties',
+    ol.control.OverviewMap.prototype.getProperties);
+
+goog.exportProperty(
+    ol.control.OverviewMap.prototype,
+    'set',
+    ol.control.OverviewMap.prototype.set);
+
+goog.exportProperty(
+    ol.control.OverviewMap.prototype,
+    'setProperties',
+    ol.control.OverviewMap.prototype.setProperties);
+
+goog.exportProperty(
+    ol.control.OverviewMap.prototype,
+    'unset',
+    ol.control.OverviewMap.prototype.unset);
+
+goog.exportProperty(
+    ol.control.OverviewMap.prototype,
+    'changed',
+    ol.control.OverviewMap.prototype.changed);
+
+goog.exportProperty(
+    ol.control.OverviewMap.prototype,
+    'dispatchEvent',
+    ol.control.OverviewMap.prototype.dispatchEvent);
+
+goog.exportProperty(
+    ol.control.OverviewMap.prototype,
+    'getRevision',
+    ol.control.OverviewMap.prototype.getRevision);
+
+goog.exportProperty(
+    ol.control.OverviewMap.prototype,
+    'on',
+    ol.control.OverviewMap.prototype.on);
+
+goog.exportProperty(
+    ol.control.OverviewMap.prototype,
+    'once',
+    ol.control.OverviewMap.prototype.once);
+
+goog.exportProperty(
+    ol.control.OverviewMap.prototype,
+    'un',
+    ol.control.OverviewMap.prototype.un);
+
+goog.exportProperty(
+    ol.control.OverviewMap.prototype,
+    'unByKey',
+    ol.control.OverviewMap.prototype.unByKey);
+
+goog.exportProperty(
+    ol.control.Rotate.prototype,
+    'getMap',
+    ol.control.Rotate.prototype.getMap);
+
+goog.exportProperty(
+    ol.control.Rotate.prototype,
+    'setMap',
+    ol.control.Rotate.prototype.setMap);
+
+goog.exportProperty(
+    ol.control.Rotate.prototype,
+    'setTarget',
+    ol.control.Rotate.prototype.setTarget);
+
+goog.exportProperty(
+    ol.control.Rotate.prototype,
+    'get',
+    ol.control.Rotate.prototype.get);
+
+goog.exportProperty(
+    ol.control.Rotate.prototype,
+    'getKeys',
+    ol.control.Rotate.prototype.getKeys);
+
+goog.exportProperty(
+    ol.control.Rotate.prototype,
+    'getProperties',
+    ol.control.Rotate.prototype.getProperties);
+
+goog.exportProperty(
+    ol.control.Rotate.prototype,
+    'set',
+    ol.control.Rotate.prototype.set);
+
+goog.exportProperty(
+    ol.control.Rotate.prototype,
+    'setProperties',
+    ol.control.Rotate.prototype.setProperties);
+
+goog.exportProperty(
+    ol.control.Rotate.prototype,
+    'unset',
+    ol.control.Rotate.prototype.unset);
+
+goog.exportProperty(
+    ol.control.Rotate.prototype,
+    'changed',
+    ol.control.Rotate.prototype.changed);
+
+goog.exportProperty(
+    ol.control.Rotate.prototype,
+    'dispatchEvent',
+    ol.control.Rotate.prototype.dispatchEvent);
+
+goog.exportProperty(
+    ol.control.Rotate.prototype,
+    'getRevision',
+    ol.control.Rotate.prototype.getRevision);
+
+goog.exportProperty(
+    ol.control.Rotate.prototype,
+    'on',
+    ol.control.Rotate.prototype.on);
+
+goog.exportProperty(
+    ol.control.Rotate.prototype,
+    'once',
+    ol.control.Rotate.prototype.once);
+
+goog.exportProperty(
+    ol.control.Rotate.prototype,
+    'un',
+    ol.control.Rotate.prototype.un);
+
+goog.exportProperty(
+    ol.control.Rotate.prototype,
+    'unByKey',
+    ol.control.Rotate.prototype.unByKey);
+
+goog.exportProperty(
+    ol.control.ScaleLine.prototype,
+    'getMap',
+    ol.control.ScaleLine.prototype.getMap);
+
+goog.exportProperty(
+    ol.control.ScaleLine.prototype,
+    'setMap',
+    ol.control.ScaleLine.prototype.setMap);
+
+goog.exportProperty(
+    ol.control.ScaleLine.prototype,
+    'setTarget',
+    ol.control.ScaleLine.prototype.setTarget);
+
+goog.exportProperty(
+    ol.control.ScaleLine.prototype,
+    'get',
+    ol.control.ScaleLine.prototype.get);
+
+goog.exportProperty(
+    ol.control.ScaleLine.prototype,
+    'getKeys',
+    ol.control.ScaleLine.prototype.getKeys);
+
+goog.exportProperty(
+    ol.control.ScaleLine.prototype,
+    'getProperties',
+    ol.control.ScaleLine.prototype.getProperties);
+
+goog.exportProperty(
+    ol.control.ScaleLine.prototype,
+    'set',
+    ol.control.ScaleLine.prototype.set);
+
+goog.exportProperty(
+    ol.control.ScaleLine.prototype,
+    'setProperties',
+    ol.control.ScaleLine.prototype.setProperties);
+
+goog.exportProperty(
+    ol.control.ScaleLine.prototype,
+    'unset',
+    ol.control.ScaleLine.prototype.unset);
+
+goog.exportProperty(
+    ol.control.ScaleLine.prototype,
+    'changed',
+    ol.control.ScaleLine.prototype.changed);
+
+goog.exportProperty(
+    ol.control.ScaleLine.prototype,
+    'dispatchEvent',
+    ol.control.ScaleLine.prototype.dispatchEvent);
+
+goog.exportProperty(
+    ol.control.ScaleLine.prototype,
+    'getRevision',
+    ol.control.ScaleLine.prototype.getRevision);
+
+goog.exportProperty(
+    ol.control.ScaleLine.prototype,
+    'on',
+    ol.control.ScaleLine.prototype.on);
+
+goog.exportProperty(
+    ol.control.ScaleLine.prototype,
+    'once',
+    ol.control.ScaleLine.prototype.once);
+
+goog.exportProperty(
+    ol.control.ScaleLine.prototype,
+    'un',
+    ol.control.ScaleLine.prototype.un);
+
+goog.exportProperty(
+    ol.control.ScaleLine.prototype,
+    'unByKey',
+    ol.control.ScaleLine.prototype.unByKey);
+
+goog.exportProperty(
+    ol.control.Zoom.prototype,
+    'getMap',
+    ol.control.Zoom.prototype.getMap);
+
+goog.exportProperty(
+    ol.control.Zoom.prototype,
+    'setMap',
+    ol.control.Zoom.prototype.setMap);
+
+goog.exportProperty(
+    ol.control.Zoom.prototype,
+    'setTarget',
+    ol.control.Zoom.prototype.setTarget);
+
+goog.exportProperty(
+    ol.control.Zoom.prototype,
+    'get',
+    ol.control.Zoom.prototype.get);
+
+goog.exportProperty(
+    ol.control.Zoom.prototype,
+    'getKeys',
+    ol.control.Zoom.prototype.getKeys);
+
+goog.exportProperty(
+    ol.control.Zoom.prototype,
+    'getProperties',
+    ol.control.Zoom.prototype.getProperties);
+
+goog.exportProperty(
+    ol.control.Zoom.prototype,
+    'set',
+    ol.control.Zoom.prototype.set);
+
+goog.exportProperty(
+    ol.control.Zoom.prototype,
+    'setProperties',
+    ol.control.Zoom.prototype.setProperties);
+
+goog.exportProperty(
+    ol.control.Zoom.prototype,
+    'unset',
+    ol.control.Zoom.prototype.unset);
+
+goog.exportProperty(
+    ol.control.Zoom.prototype,
+    'changed',
+    ol.control.Zoom.prototype.changed);
+
+goog.exportProperty(
+    ol.control.Zoom.prototype,
+    'dispatchEvent',
+    ol.control.Zoom.prototype.dispatchEvent);
+
+goog.exportProperty(
+    ol.control.Zoom.prototype,
+    'getRevision',
+    ol.control.Zoom.prototype.getRevision);
+
+goog.exportProperty(
+    ol.control.Zoom.prototype,
+    'on',
+    ol.control.Zoom.prototype.on);
+
+goog.exportProperty(
+    ol.control.Zoom.prototype,
+    'once',
+    ol.control.Zoom.prototype.once);
+
+goog.exportProperty(
+    ol.control.Zoom.prototype,
+    'un',
+    ol.control.Zoom.prototype.un);
+
+goog.exportProperty(
+    ol.control.Zoom.prototype,
+    'unByKey',
+    ol.control.Zoom.prototype.unByKey);
+
+goog.exportProperty(
+    ol.control.ZoomSlider.prototype,
+    'getMap',
+    ol.control.ZoomSlider.prototype.getMap);
+
+goog.exportProperty(
+    ol.control.ZoomSlider.prototype,
+    'setMap',
+    ol.control.ZoomSlider.prototype.setMap);
+
+goog.exportProperty(
+    ol.control.ZoomSlider.prototype,
+    'setTarget',
+    ol.control.ZoomSlider.prototype.setTarget);
+
+goog.exportProperty(
+    ol.control.ZoomSlider.prototype,
+    'get',
+    ol.control.ZoomSlider.prototype.get);
+
+goog.exportProperty(
+    ol.control.ZoomSlider.prototype,
+    'getKeys',
+    ol.control.ZoomSlider.prototype.getKeys);
+
+goog.exportProperty(
+    ol.control.ZoomSlider.prototype,
+    'getProperties',
+    ol.control.ZoomSlider.prototype.getProperties);
+
+goog.exportProperty(
+    ol.control.ZoomSlider.prototype,
+    'set',
+    ol.control.ZoomSlider.prototype.set);
+
+goog.exportProperty(
+    ol.control.ZoomSlider.prototype,
+    'setProperties',
+    ol.control.ZoomSlider.prototype.setProperties);
+
+goog.exportProperty(
+    ol.control.ZoomSlider.prototype,
+    'unset',
+    ol.control.ZoomSlider.prototype.unset);
+
+goog.exportProperty(
+    ol.control.ZoomSlider.prototype,
+    'changed',
+    ol.control.ZoomSlider.prototype.changed);
+
+goog.exportProperty(
+    ol.control.ZoomSlider.prototype,
+    'dispatchEvent',
+    ol.control.ZoomSlider.prototype.dispatchEvent);
+
+goog.exportProperty(
+    ol.control.ZoomSlider.prototype,
+    'getRevision',
+    ol.control.ZoomSlider.prototype.getRevision);
+
+goog.exportProperty(
+    ol.control.ZoomSlider.prototype,
+    'on',
+    ol.control.ZoomSlider.prototype.on);
+
+goog.exportProperty(
+    ol.control.ZoomSlider.prototype,
+    'once',
+    ol.control.ZoomSlider.prototype.once);
+
+goog.exportProperty(
+    ol.control.ZoomSlider.prototype,
+    'un',
+    ol.control.ZoomSlider.prototype.un);
+
+goog.exportProperty(
+    ol.control.ZoomSlider.prototype,
+    'unByKey',
+    ol.control.ZoomSlider.prototype.unByKey);
+
+goog.exportProperty(
+    ol.control.ZoomToExtent.prototype,
+    'getMap',
+    ol.control.ZoomToExtent.prototype.getMap);
+
+goog.exportProperty(
+    ol.control.ZoomToExtent.prototype,
+    'setMap',
+    ol.control.ZoomToExtent.prototype.setMap);
+
+goog.exportProperty(
+    ol.control.ZoomToExtent.prototype,
+    'setTarget',
+    ol.control.ZoomToExtent.prototype.setTarget);
+
+goog.exportProperty(
+    ol.control.ZoomToExtent.prototype,
+    'get',
+    ol.control.ZoomToExtent.prototype.get);
+
+goog.exportProperty(
+    ol.control.ZoomToExtent.prototype,
+    'getKeys',
+    ol.control.ZoomToExtent.prototype.getKeys);
+
+goog.exportProperty(
+    ol.control.ZoomToExtent.prototype,
+    'getProperties',
+    ol.control.ZoomToExtent.prototype.getProperties);
+
+goog.exportProperty(
+    ol.control.ZoomToExtent.prototype,
+    'set',
+    ol.control.ZoomToExtent.prototype.set);
+
+goog.exportProperty(
+    ol.control.ZoomToExtent.prototype,
+    'setProperties',
+    ol.control.ZoomToExtent.prototype.setProperties);
+
+goog.exportProperty(
+    ol.control.ZoomToExtent.prototype,
+    'unset',
+    ol.control.ZoomToExtent.prototype.unset);
+
+goog.exportProperty(
+    ol.control.ZoomToExtent.prototype,
+    'changed',
+    ol.control.ZoomToExtent.prototype.changed);
+
+goog.exportProperty(
+    ol.control.ZoomToExtent.prototype,
+    'dispatchEvent',
+    ol.control.ZoomToExtent.prototype.dispatchEvent);
+
+goog.exportProperty(
+    ol.control.ZoomToExtent.prototype,
+    'getRevision',
+    ol.control.ZoomToExtent.prototype.getRevision);
+
+goog.exportProperty(
+    ol.control.ZoomToExtent.prototype,
+    'on',
+    ol.control.ZoomToExtent.prototype.on);
+
+goog.exportProperty(
+    ol.control.ZoomToExtent.prototype,
+    'once',
+    ol.control.ZoomToExtent.prototype.once);
+
+goog.exportProperty(
+    ol.control.ZoomToExtent.prototype,
+    'un',
+    ol.control.ZoomToExtent.prototype.un);
+
+goog.exportProperty(
+    ol.control.ZoomToExtent.prototype,
+    'unByKey',
+    ol.control.ZoomToExtent.prototype.unByKey);
+OPENLAYERS.ol = ol;
+
+  return OPENLAYERS.ol;
+}));
diff --git a/themes/bootstrap3/js/vendor/ol/ol.js b/themes/bootstrap3/js/vendor/ol/ol.js
new file mode 100644
index 0000000000000000000000000000000000000000..c552dcc751e6fe7a71c595bf9b4342d4b319aeea
--- /dev/null
+++ b/themes/bootstrap3/js/vendor/ol/ol.js
@@ -0,0 +1,998 @@
+// OpenLayers 3. See http://openlayers.org/
+// License: https://raw.githubusercontent.com/openlayers/ol3/master/LICENSE.md
+// Version: v3.17.1
+
+(function (root, factory) {
+  if (typeof exports === "object") {
+    module.exports = factory();
+  } else if (typeof define === "function" && define.amd) {
+    define([], factory);
+  } else {
+    root.ol = factory();
+  }
+}(this, function () {
+  var OPENLAYERS = {};
+  var k,aa=this;function t(a,b,c){a=a.split(".");c=c||aa;a[0]in c||!c.execScript||c.execScript("var "+a[0]);for(var d;a.length&&(d=a.shift());)a.length||void 0===b?c[d]?c=c[d]:c=c[d]={}:c[d]=b}function ba(a){a.Zb=function(){return a.Tg?a.Tg:a.Tg=new a}}
+function ca(a){var b=typeof a;if("object"==b)if(a){if(a instanceof Array)return"array";if(a instanceof Object)return b;var c=Object.prototype.toString.call(a);if("[object Window]"==c)return"object";if("[object Array]"==c||"number"==typeof a.length&&"undefined"!=typeof a.splice&&"undefined"!=typeof a.propertyIsEnumerable&&!a.propertyIsEnumerable("splice"))return"array";if("[object Function]"==c||"undefined"!=typeof a.call&&"undefined"!=typeof a.propertyIsEnumerable&&!a.propertyIsEnumerable("call"))return"function"}else return"null";
+else if("function"==b&&"undefined"==typeof a.call)return"object";return b}function da(a){return"string"==typeof a}function ea(a){return"number"==typeof a}function fa(a){var b=typeof a;return"object"==b&&null!=a||"function"==b}function w(a){return a[ga]||(a[ga]=++ha)}var ga="closure_uid_"+(1E9*Math.random()>>>0),ha=0;function ia(a,b,c){return a.call.apply(a.bind,arguments)}
+function ja(a,b,c){if(!a)throw Error();if(2<arguments.length){var d=Array.prototype.slice.call(arguments,2);return function(){var c=Array.prototype.slice.call(arguments);Array.prototype.unshift.apply(c,d);return a.apply(b,c)}}return function(){return a.apply(b,arguments)}}function ka(a,b,c){ka=Function.prototype.bind&&-1!=Function.prototype.bind.toString().indexOf("native code")?ia:ja;return ka.apply(null,arguments)};var la,ma;function y(a,b){a.prototype=Object.create(b.prototype);a.prototype.constructor=a}function na(){}var pa=Function("return this")();var qa=String.prototype.trim?function(a){return a.trim()}:function(a){return a.replace(/^[\s\xa0]+|[\s\xa0]+$/g,"")};function ra(a,b){return a<b?-1:a>b?1:0};function sa(a,b,c){return Math.min(Math.max(a,b),c)}var ta=function(){var a;"cosh"in Math?a=Math.cosh:a=function(a){a=Math.exp(a);return(a+1/a)/2};return a}();function ua(a,b,c,d,e,f){var g=e-c,h=f-d;if(0!==g||0!==h){var l=((a-c)*g+(b-d)*h)/(g*g+h*h);1<l?(c=e,d=f):0<l&&(c+=g*l,d+=h*l)}return va(a,b,c,d)}function va(a,b,c,d){a=c-a;b=d-b;return a*a+b*b}function wa(a){return a*Math.PI/180}function xa(a,b){var c=a%b;return 0>c*b?c+b:c}function za(a,b,c){return a+c*(b-a)};function Ba(a){return function(b){if(b)return[sa(b[0],a[0],a[2]),sa(b[1],a[1],a[3])]}}function Ca(a){return a};function Da(a,b,c){this.center=a;this.resolution=b;this.rotation=c};var Ea="function"===typeof Object.assign?Object.assign:function(a,b){if(!a||!a)throw new TypeError("Cannot convert undefined or null to object");for(var c=Object(a),d=1,e=arguments.length;d<e;++d){var f=arguments[d];if(void 0!==f&&null!==f)for(var g in f)f.hasOwnProperty(g)&&(c[g]=f[g])}return c};function Fa(a){for(var b in a)delete a[b]}function Ga(a){var b=[],c;for(c in a)b.push(a[c]);return b}function Ha(a){for(var b in a)return!1;return!b};var Ia="olm_"+(1E4*Math.random()|0);function Ja(a){function b(b){var d=a.listener,e=a.ng||a.target;a.pg&&Ka(a);return d.call(e,b)}return a.og=b}function La(a,b,c,d){for(var e,f=0,g=a.length;f<g;++f)if(e=a[f],e.listener===b&&e.ng===c)return d&&(e.deleteIndex=f),e}function Ma(a,b){var c=a[Ia];return c?c[b]:void 0}function Na(a){var b=a[Ia];b||(b=a[Ia]={});return b}
+function Oa(a,b){var c=Ma(a,b);if(c){for(var d=0,e=c.length;d<e;++d)a.removeEventListener(b,c[d].og),Fa(c[d]);c.length=0;if(c=a[Ia])delete c[b],0===Object.keys(c).length&&delete a[Ia]}}function B(a,b,c,d,e){var f=Na(a),g=f[b];g||(g=f[b]=[]);(f=La(g,c,d,!1))?e||(f.pg=!1):(f={ng:d,pg:!!e,listener:c,target:a,type:b},a.addEventListener(b,Ja(f)),g.push(f));return f}function Pa(a,b,c,d){return B(a,b,c,d,!0)}function Qa(a,b,c,d){(a=Ma(a,b))&&(c=La(a,c,d,!0))&&Ka(c)}
+function Ka(a){if(a&&a.target){a.target.removeEventListener(a.type,a.og);var b=Ma(a.target,a.type);if(b){var c="deleteIndex"in a?a.deleteIndex:b.indexOf(a);-1!==c&&b.splice(c,1);0===b.length&&Oa(a.target,a.type)}Fa(a)}}function Ra(a){var b=Na(a),c;for(c in b)Oa(a,c)};function Sa(){}Sa.prototype.Gb=!1;function Ta(a){a.Gb||(a.Gb=!0,a.ka())}Sa.prototype.ka=na;function Wa(a,b){this.type=a;this.target=b||null}Wa.prototype.preventDefault=Wa.prototype.stopPropagation=function(){this.to=!0};function Ya(a){a.stopPropagation()}function Za(a){a.preventDefault()};function $a(){this.Ra={};this.Ba={};this.ya={}}y($a,Sa);$a.prototype.addEventListener=function(a,b){var c=this.ya[a];c||(c=this.ya[a]=[]);-1===c.indexOf(b)&&c.push(b)};
+$a.prototype.b=function(a){var b="string"===typeof a?new Wa(a):a;a=b.type;b.target=this;var c=this.ya[a],d;if(c){a in this.Ba||(this.Ba[a]=0,this.Ra[a]=0);++this.Ba[a];for(var e=0,f=c.length;e<f;++e)if(!1===c[e].call(this,b)||b.to){d=!1;break}--this.Ba[a];if(0===this.Ba[a]){b=this.Ra[a];for(delete this.Ra[a];b--;)this.removeEventListener(a,na);delete this.Ba[a]}return d}};$a.prototype.ka=function(){Ra(this)};function ab(a,b){return b?b in a.ya:0<Object.keys(a.ya).length}
+$a.prototype.removeEventListener=function(a,b){var c=this.ya[a];if(c){var d=c.indexOf(b);a in this.Ra?(c[d]=na,++this.Ra[a]):(c.splice(d,1),0===c.length&&delete this.ya[a])}};function bb(){$a.call(this);this.g=0}y(bb,$a);function cb(a){if(Array.isArray(a))for(var b=0,c=a.length;b<c;++b)Ka(a[b]);else Ka(a)}k=bb.prototype;k.u=function(){++this.g;this.b("change")};k.K=function(){return this.g};k.I=function(a,b,c){if(Array.isArray(a)){for(var d=a.length,e=Array(d),f=0;f<d;++f)e[f]=B(this,a[f],b,c);return e}return B(this,a,b,c)};k.L=function(a,b,c){if(Array.isArray(a)){for(var d=a.length,e=Array(d),f=0;f<d;++f)e[f]=Pa(this,a[f],b,c);return e}return Pa(this,a,b,c)};
+k.J=function(a,b,c){if(Array.isArray(a))for(var d=0,e=a.length;d<e;++d)Qa(this,a[d],b,c);else Qa(this,a,b,c)};k.M=cb;function db(a,b,c){Wa.call(this,a);this.key=b;this.oldValue=c}y(db,Wa);function eb(a){bb.call(this);w(this);this.U={};void 0!==a&&this.G(a)}y(eb,bb);var fb={};function gb(a){return fb.hasOwnProperty(a)?fb[a]:fb[a]="change:"+a}k=eb.prototype;k.get=function(a){var b;this.U.hasOwnProperty(a)&&(b=this.U[a]);return b};k.N=function(){return Object.keys(this.U)};k.O=function(){return Ea({},this.U)};function hb(a,b,c){var d;d=gb(b);a.b(new db(d,b,c));a.b(new db("propertychange",b,c))}
+k.set=function(a,b,c){c?this.U[a]=b:(c=this.U[a],this.U[a]=b,c!==b&&hb(this,a,c))};k.G=function(a,b){for(var c in a)this.set(c,a[c],b)};k.P=function(a,b){if(a in this.U){var c=this.U[a];delete this.U[a];b||hb(this,a,c)}};function ib(a,b){return a>b?1:a<b?-1:0}function jb(a,b){return 0<=a.indexOf(b)}function kb(a,b,c){var d=a.length;if(a[0]<=b)return 0;if(!(b<=a[d-1]))if(0<c)for(c=1;c<d;++c){if(a[c]<b)return c-1}else if(0>c)for(c=1;c<d;++c){if(a[c]<=b)return c}else for(c=1;c<d;++c){if(a[c]==b)return c;if(a[c]<b)return a[c-1]-b<b-a[c]?c-1:c}return d-1}function lb(a){return a.reduce(function(a,c){return Array.isArray(c)?a.concat(lb(c)):a.concat(c)},[])}
+function mb(a,b){var c;c=ca(b);var d="array"==c||"object"==c&&"number"==typeof b.length?b:[b],e=d.length;for(c=0;c<e;c++)a[a.length]=d[c]}function nb(a,b){var c=a.indexOf(b),d=-1<c;d&&a.splice(c,1);return d}function ob(a,b){for(var c=a.length>>>0,d,e=0;e<c;e++)if(d=a[e],b(d,e,a))return d;return null}function pb(a,b){var c=a.length;if(c!==b.length)return!1;for(var d=0;d<c;d++)if(a[d]!==b[d])return!1;return!0}
+function qb(a){var b=rb,c=a.length,d=Array(a.length),e;for(e=0;e<c;e++)d[e]={index:e,value:a[e]};d.sort(function(a,c){return b(a.value,c.value)||a.index-c.index});for(e=0;e<a.length;e++)a[e]=d[e].value}function sb(a,b){var c;return a.every(function(d,e){c=e;return!b(d,e,a)})?-1:c};function tb(a){return function(b,c,d){if(void 0!==b)return b=kb(a,b,d),b=sa(b+c,0,a.length-1),a[b]}}function ub(a,b,c){return function(d,e,f){if(void 0!==d)return d=Math.max(Math.floor(Math.log(b/d)/Math.log(a)+(0<f?0:0>f?1:.5))+e,0),void 0!==c&&(d=Math.min(d,c)),b/Math.pow(a,d)}};function vb(a){if(void 0!==a)return 0}function wb(a,b){if(void 0!==a)return a+b}function xb(a){var b=2*Math.PI/a;return function(a,d){if(void 0!==a)return a=Math.floor((a+d)/b+.5)*b}}function yb(){var a=wa(5);return function(b,c){if(void 0!==b)return Math.abs(b+c)<=a?0:b+c}};function zb(a,b){var c=void 0!==b?a.toFixed(b):""+a,d=c.indexOf("."),d=-1===d?c.length:d;return 2<d?c:Array(3-d).join("0")+c}function Ab(a){a=(""+a).split(".");for(var b=["1","3"],c=0;c<Math.max(a.length,b.length);c++){var d=parseInt(a[c]||"0",10),e=parseInt(b[c]||"0",10);if(d>e)return 1;if(e>d)return-1}return 0};function Bb(a,b){a[0]+=b[0];a[1]+=b[1];return a}function Cb(a,b){var c=a[0],d=a[1],e=b[0],f=b[1],g=e[0],e=e[1],h=f[0],f=f[1],l=h-g,m=f-e,c=0===l&&0===m?0:(l*(c-g)+m*(d-e))/(l*l+m*m||0);0>=c||(1<=c?(g=h,e=f):(g+=c*l,e+=c*m));return[g,e]}function Db(a,b,c){a=xa(a+180,360)-180;var d=Math.abs(3600*a);return Math.floor(d/3600)+"\u00b0 "+zb(Math.floor(d/60%60))+"\u2032 "+zb(d%60,c||0)+"\u2033 "+b.charAt(0>a?1:0)}
+function Eb(a,b,c){return a?b.replace("{x}",a[0].toFixed(c)).replace("{y}",a[1].toFixed(c)):""}function Fb(a,b){for(var c=!0,d=a.length-1;0<=d;--d)if(a[d]!=b[d]){c=!1;break}return c}function Gb(a,b){var c=Math.cos(b),d=Math.sin(b),e=a[1]*c+a[0]*d;a[0]=a[0]*c-a[1]*d;a[1]=e;return a}function Hb(a,b){var c=a[0]-b[0],d=a[1]-b[1];return c*c+d*d}function Ib(a,b){return Hb(a,Cb(a,b))}function Jb(a,b){return Eb(a,"{x}, {y}",b)};function Kb(a){for(var b=Lb(),c=0,d=a.length;c<d;++c)Mb(b,a[c]);return b}function Ob(a,b,c){return c?(c[0]=a[0]-b,c[1]=a[1]-b,c[2]=a[2]+b,c[3]=a[3]+b,c):[a[0]-b,a[1]-b,a[2]+b,a[3]+b]}function Pb(a,b){return b?(b[0]=a[0],b[1]=a[1],b[2]=a[2],b[3]=a[3],b):a.slice()}function Rb(a,b,c){b=b<a[0]?a[0]-b:a[2]<b?b-a[2]:0;a=c<a[1]?a[1]-c:a[3]<c?c-a[3]:0;return b*b+a*a}function Sb(a,b){return Tb(a,b[0],b[1])}function Ub(a,b){return a[0]<=b[0]&&b[2]<=a[2]&&a[1]<=b[1]&&b[3]<=a[3]}
+function Tb(a,b,c){return a[0]<=b&&b<=a[2]&&a[1]<=c&&c<=a[3]}function Vb(a,b){var c=a[1],d=a[2],e=a[3],f=b[0],g=b[1],h=0;f<a[0]?h|=16:f>d&&(h|=4);g<c?h|=8:g>e&&(h|=2);0===h&&(h=1);return h}function Lb(){return[Infinity,Infinity,-Infinity,-Infinity]}function Wb(a,b,c,d,e){return e?(e[0]=a,e[1]=b,e[2]=c,e[3]=d,e):[a,b,c,d]}function Xb(a,b){var c=a[0],d=a[1];return Wb(c,d,c,d,b)}function Yb(a,b,c,d,e){e=Wb(Infinity,Infinity,-Infinity,-Infinity,e);return Zb(e,a,b,c,d)}
+function $b(a,b){return a[0]==b[0]&&a[2]==b[2]&&a[1]==b[1]&&a[3]==b[3]}function ac(a,b){b[0]<a[0]&&(a[0]=b[0]);b[2]>a[2]&&(a[2]=b[2]);b[1]<a[1]&&(a[1]=b[1]);b[3]>a[3]&&(a[3]=b[3]);return a}function Mb(a,b){b[0]<a[0]&&(a[0]=b[0]);b[0]>a[2]&&(a[2]=b[0]);b[1]<a[1]&&(a[1]=b[1]);b[1]>a[3]&&(a[3]=b[1])}function Zb(a,b,c,d,e){for(;c<d;c+=e){var f=a,g=b[c],h=b[c+1];f[0]=Math.min(f[0],g);f[1]=Math.min(f[1],h);f[2]=Math.max(f[2],g);f[3]=Math.max(f[3],h)}return a}
+function bc(a,b,c){var d;return(d=b.call(c,cc(a)))||(d=b.call(c,dc(a)))||(d=b.call(c,ec(a)))?d:(d=b.call(c,fc(a)))?d:!1}function gc(a){var b=0;hc(a)||(b=ic(a)*jc(a));return b}function cc(a){return[a[0],a[1]]}function dc(a){return[a[2],a[1]]}function kc(a){return[(a[0]+a[2])/2,(a[1]+a[3])/2]}
+function lc(a,b,c,d,e){var f=b*d[0]/2;d=b*d[1]/2;b=Math.cos(c);var g=Math.sin(c);c=f*b;f*=g;b*=d;var h=d*g,l=a[0],m=a[1];a=l-c+h;d=l-c-h;g=l+c-h;c=l+c+h;var h=m-f-b,l=m-f+b,n=m+f+b,f=m+f-b;return Wb(Math.min(a,d,g,c),Math.min(h,l,n,f),Math.max(a,d,g,c),Math.max(h,l,n,f),e)}function jc(a){return a[3]-a[1]}function mc(a,b,c){c=c?c:Lb();nc(a,b)&&(c[0]=a[0]>b[0]?a[0]:b[0],c[1]=a[1]>b[1]?a[1]:b[1],c[2]=a[2]<b[2]?a[2]:b[2],c[3]=a[3]<b[3]?a[3]:b[3]);return c}function fc(a){return[a[0],a[3]]}
+function ec(a){return[a[2],a[3]]}function ic(a){return a[2]-a[0]}function nc(a,b){return a[0]<=b[2]&&a[2]>=b[0]&&a[1]<=b[3]&&a[3]>=b[1]}function hc(a){return a[2]<a[0]||a[3]<a[1]}function oc(a,b){var c=(a[2]-a[0])/2*(b-1),d=(a[3]-a[1])/2*(b-1);a[0]-=c;a[2]+=c;a[1]-=d;a[3]+=d}
+function pc(a,b,c){a=[a[0],a[1],a[0],a[3],a[2],a[1],a[2],a[3]];b(a,a,2);var d=[a[0],a[2],a[4],a[6]],e=[a[1],a[3],a[5],a[7]];b=Math.min.apply(null,d);a=Math.min.apply(null,e);d=Math.max.apply(null,d);e=Math.max.apply(null,e);return Wb(b,a,d,e,c)};function qc(){return!0}function rc(){return!1};/*
+
+ Latitude/longitude spherical geodesy formulae taken from
+ http://www.movable-type.co.uk/scripts/latlong.html
+ Licensed under CC-BY-3.0.
+*/
+function sc(a){this.radius=a}sc.prototype.a=function(a){for(var b=0,c=a.length,d=a[c-1][0],e=a[c-1][1],f=0;f<c;f++)var g=a[f][0],h=a[f][1],b=b+wa(g-d)*(2+Math.sin(wa(e))+Math.sin(wa(h))),d=g,e=h;return b*this.radius*this.radius/2};sc.prototype.b=function(a,b){var c=wa(a[1]),d=wa(b[1]),e=(d-c)/2,f=wa(b[0]-a[0])/2,c=Math.sin(e)*Math.sin(e)+Math.sin(f)*Math.sin(f)*Math.cos(c)*Math.cos(d);return 2*this.radius*Math.atan2(Math.sqrt(c),Math.sqrt(1-c))};
+sc.prototype.offset=function(a,b,c){var d=wa(a[1]);b/=this.radius;var e=Math.asin(Math.sin(d)*Math.cos(b)+Math.cos(d)*Math.sin(b)*Math.cos(c));return[180*(wa(a[0])+Math.atan2(Math.sin(c)*Math.sin(b)*Math.cos(d),Math.cos(b)-Math.sin(d)*Math.sin(e)))/Math.PI,180*e/Math.PI]};var tc=new sc(6370997);var uc={};uc.degrees=2*Math.PI*tc.radius/360;uc.ft=.3048;uc.m=1;uc["us-ft"]=1200/3937;
+function vc(a){this.cb=a.code;this.c=a.units;this.f=void 0!==a.extent?a.extent:null;this.i=void 0!==a.worldExtent?a.worldExtent:null;this.b=void 0!==a.axisOrientation?a.axisOrientation:"enu";this.g=void 0!==a.global?a.global:!1;this.a=!(!this.g||!this.f);this.o=void 0!==a.getPointResolution?a.getPointResolution:this.sk;this.l=null;this.j=a.metersPerUnit;var b=wc,c=a.code,d=xc||pa.proj4;if("function"==typeof d&&void 0===b[c]){var e=d.defs(c);if(void 0!==e){void 0!==e.axis&&void 0===a.axisOrientation&&
+(this.b=e.axis);void 0===a.metersPerUnit&&(this.j=e.to_meter);void 0===a.units&&(this.c=e.units);for(var f in b)b=d.defs(f),void 0!==b&&(a=yc(f),b===e?zc([a,this]):(b=d(f,c),Ac(a,this,b.forward,b.inverse)))}}}k=vc.prototype;k.Sj=function(){return this.cb};k.H=function(){return this.f};k.wb=function(){return this.c};k.$b=function(){return this.j||uc[this.c]};k.Ek=function(){return this.i};k.pl=function(){return this.g};k.ep=function(a){this.g=a;this.a=!(!a||!this.f)};
+k.Mm=function(a){this.f=a;this.a=!(!this.g||!a)};k.mp=function(a){this.i=a};k.cp=function(a){this.o=a};k.sk=function(a,b){if("degrees"==this.wb())return a;var c=Bc(this,yc("EPSG:4326")),d=[b[0]-a/2,b[1],b[0]+a/2,b[1],b[0],b[1]-a/2,b[0],b[1]+a/2],d=c(d,d,2),c=tc.b(d.slice(0,2),d.slice(2,4)),d=tc.b(d.slice(4,6),d.slice(6,8)),d=(c+d)/2,c=this.$b();void 0!==c&&(d/=c);return d};k.getPointResolution=function(a,b){return this.o(a,b)};var wc={},Cc={},xc=null;
+function zc(a){Dc(a);a.forEach(function(b){a.forEach(function(a){b!==a&&Ec(b,a,Fc)})})}function Gc(){var a=Hc,b=Ic,c=Jc;Kc.forEach(function(d){a.forEach(function(a){Ec(d,a,b);Ec(a,d,c)})})}function Lc(a){wc[a.cb]=a;Ec(a,a,Fc)}function Dc(a){var b=[];a.forEach(function(a){b.push(Lc(a))})}function Mc(a){return a?"string"===typeof a?yc(a):a:yc("EPSG:3857")}function Ec(a,b,c){a=a.cb;b=b.cb;a in Cc||(Cc[a]={});Cc[a][b]=c}function Ac(a,b,c,d){a=yc(a);b=yc(b);Ec(a,b,Nc(c));Ec(b,a,Nc(d))}
+function Nc(a){return function(b,c,d){var e=b.length;d=void 0!==d?d:2;c=void 0!==c?c:Array(e);var f,g;for(g=0;g<e;g+=d)for(f=a([b[g],b[g+1]]),c[g]=f[0],c[g+1]=f[1],f=d-1;2<=f;--f)c[g+f]=b[g+f];return c}}function yc(a){var b;if(a instanceof vc)b=a;else if("string"===typeof a){b=wc[a];var c=xc||pa.proj4;void 0===b&&"function"==typeof c&&void 0!==c.defs(a)&&(b=new vc({code:a}),Lc(b))}else b=null;return b}function Oc(a,b){if(a===b)return!0;var c=a.wb()===b.wb();return a.cb===b.cb?c:Bc(a,b)===Fc&&c}
+function Pc(a,b){var c=yc(a),d=yc(b);return Bc(c,d)}function Bc(a,b){var c=a.cb,d=b.cb,e;c in Cc&&d in Cc[c]&&(e=Cc[c][d]);void 0===e&&(e=Qc);return e}function Qc(a,b){if(void 0!==b&&a!==b){for(var c=0,d=a.length;c<d;++c)b[c]=a[c];a=b}return a}function Fc(a,b){var c;if(void 0!==b){c=0;for(var d=a.length;c<d;++c)b[c]=a[c];c=b}else c=a.slice();return c}function Rc(a,b,c){return Pc(b,c)(a,void 0,a.length)}function Sc(a,b,c){b=Pc(b,c);return pc(a,b)};function Tc(){eb.call(this);this.v=Lb();this.A=-1;this.l={};this.s=this.o=0}y(Tc,eb);k=Tc.prototype;k.vb=function(a,b){var c=b?b:[NaN,NaN];this.sb(a[0],a[1],c,Infinity);return c};k.sg=function(a){return this.Bc(a[0],a[1])};k.Bc=rc;k.H=function(a){this.A!=this.g&&(this.v=this.Od(this.v),this.A=this.g);var b=this.v;a?(a[0]=b[0],a[1]=b[1],a[2]=b[2],a[3]=b[3]):a=b;return a};k.Bb=function(a){return this.od(a*a)};k.jb=function(a,b){this.rc(Pc(a,b));return this};function Uc(a){this.length=a.length||a;for(var b=0;b<this.length;b++)this[b]=a[b]||0}Uc.prototype.BYTES_PER_ELEMENT=4;Uc.prototype.set=function(a,b){b=b||0;for(var c=0;c<a.length&&b+c<this.length;c++)this[b+c]=a[c]};Uc.prototype.toString=Array.prototype.join;"undefined"==typeof Float32Array&&(Uc.BYTES_PER_ELEMENT=4,Uc.prototype.BYTES_PER_ELEMENT=Uc.prototype.BYTES_PER_ELEMENT,Uc.prototype.set=Uc.prototype.set,Uc.prototype.toString=Uc.prototype.toString,t("Float32Array",Uc,void 0));function Vc(a){this.length=a.length||a;for(var b=0;b<this.length;b++)this[b]=a[b]||0}Vc.prototype.BYTES_PER_ELEMENT=8;Vc.prototype.set=function(a,b){b=b||0;for(var c=0;c<a.length&&b+c<this.length;c++)this[b+c]=a[c]};Vc.prototype.toString=Array.prototype.join;if("undefined"==typeof Float64Array){try{Vc.BYTES_PER_ELEMENT=8}catch(a){}Vc.prototype.BYTES_PER_ELEMENT=Vc.prototype.BYTES_PER_ELEMENT;Vc.prototype.set=Vc.prototype.set;Vc.prototype.toString=Vc.prototype.toString;t("Float64Array",Vc,void 0)};function Wc(a,b,c,d,e){a[0]=b;a[1]=c;a[2]=d;a[3]=e};function Xc(){var a=Array(16);Yc(a,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0);return a}function Zc(){var a=Array(16);Yc(a,1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1);return a}function Yc(a,b,c,d,e,f,g,h,l,m,n,p,q,r,u,x,v){a[0]=b;a[1]=c;a[2]=d;a[3]=e;a[4]=f;a[5]=g;a[6]=h;a[7]=l;a[8]=m;a[9]=n;a[10]=p;a[11]=q;a[12]=r;a[13]=u;a[14]=x;a[15]=v}
+function $c(a,b){a[0]=b[0];a[1]=b[1];a[2]=b[2];a[3]=b[3];a[4]=b[4];a[5]=b[5];a[6]=b[6];a[7]=b[7];a[8]=b[8];a[9]=b[9];a[10]=b[10];a[11]=b[11];a[12]=b[12];a[13]=b[13];a[14]=b[14];a[15]=b[15]}function ad(a){a[0]=1;a[1]=0;a[2]=0;a[3]=0;a[4]=0;a[5]=1;a[6]=0;a[7]=0;a[8]=0;a[9]=0;a[10]=1;a[11]=0;a[12]=0;a[13]=0;a[14]=0;a[15]=1}
+function bd(a,b,c){var d=a[0],e=a[1],f=a[2],g=a[3],h=a[4],l=a[5],m=a[6],n=a[7],p=a[8],q=a[9],r=a[10],u=a[11],x=a[12],v=a[13],D=a[14];a=a[15];var A=b[0],z=b[1],F=b[2],N=b[3],K=b[4],X=b[5],oa=b[6],H=b[7],ya=b[8],Ua=b[9],Xa=b[10],Va=b[11],Aa=b[12],Qb=b[13],Nb=b[14];b=b[15];c[0]=d*A+h*z+p*F+x*N;c[1]=e*A+l*z+q*F+v*N;c[2]=f*A+m*z+r*F+D*N;c[3]=g*A+n*z+u*F+a*N;c[4]=d*K+h*X+p*oa+x*H;c[5]=e*K+l*X+q*oa+v*H;c[6]=f*K+m*X+r*oa+D*H;c[7]=g*K+n*X+u*oa+a*H;c[8]=d*ya+h*Ua+p*Xa+x*Va;c[9]=e*ya+l*Ua+q*Xa+v*Va;c[10]=f*
+ya+m*Ua+r*Xa+D*Va;c[11]=g*ya+n*Ua+u*Xa+a*Va;c[12]=d*Aa+h*Qb+p*Nb+x*b;c[13]=e*Aa+l*Qb+q*Nb+v*b;c[14]=f*Aa+m*Qb+r*Nb+D*b;c[15]=g*Aa+n*Qb+u*Nb+a*b}
+function cd(a,b){var c=a[0],d=a[1],e=a[2],f=a[3],g=a[4],h=a[5],l=a[6],m=a[7],n=a[8],p=a[9],q=a[10],r=a[11],u=a[12],x=a[13],v=a[14],D=a[15],A=c*h-d*g,z=c*l-e*g,F=c*m-f*g,N=d*l-e*h,K=d*m-f*h,X=e*m-f*l,oa=n*x-p*u,H=n*v-q*u,ya=n*D-r*u,Ua=p*v-q*x,Xa=p*D-r*x,Va=q*D-r*v,Aa=A*Va-z*Xa+F*Ua+N*ya-K*H+X*oa;0!=Aa&&(Aa=1/Aa,b[0]=(h*Va-l*Xa+m*Ua)*Aa,b[1]=(-d*Va+e*Xa-f*Ua)*Aa,b[2]=(x*X-v*K+D*N)*Aa,b[3]=(-p*X+q*K-r*N)*Aa,b[4]=(-g*Va+l*ya-m*H)*Aa,b[5]=(c*Va-e*ya+f*H)*Aa,b[6]=(-u*X+v*F-D*z)*Aa,b[7]=(n*X-q*F+r*z)*Aa,
+b[8]=(g*Xa-h*ya+m*oa)*Aa,b[9]=(-c*Xa+d*ya-f*oa)*Aa,b[10]=(u*K-x*F+D*A)*Aa,b[11]=(-n*K+p*F-r*A)*Aa,b[12]=(-g*Ua+h*H-l*oa)*Aa,b[13]=(c*Ua-d*H+e*oa)*Aa,b[14]=(-u*N+x*z-v*A)*Aa,b[15]=(n*N-p*z+q*A)*Aa)}function dd(a,b,c){var d=a[1]*b+a[5]*c+0*a[9]+a[13],e=a[2]*b+a[6]*c+0*a[10]+a[14],f=a[3]*b+a[7]*c+0*a[11]+a[15];a[12]=a[0]*b+a[4]*c+0*a[8]+a[12];a[13]=d;a[14]=e;a[15]=f}
+function ed(a,b,c){Yc(a,a[0]*b,a[1]*b,a[2]*b,a[3]*b,a[4]*c,a[5]*c,a[6]*c,a[7]*c,1*a[8],1*a[9],1*a[10],1*a[11],a[12],a[13],a[14],a[15])}function fd(a,b){var c=a[0],d=a[1],e=a[2],f=a[3],g=a[4],h=a[5],l=a[6],m=a[7],n=Math.cos(b),p=Math.sin(b);a[0]=c*n+g*p;a[1]=d*n+h*p;a[2]=e*n+l*p;a[3]=f*n+m*p;a[4]=c*-p+g*n;a[5]=d*-p+h*n;a[6]=e*-p+l*n;a[7]=f*-p+m*n}new Float64Array(3);new Float64Array(3);new Float64Array(4);new Float64Array(4);new Float64Array(4);new Float64Array(16);function gd(a,b,c,d,e,f){var g=e[0],h=e[1],l=e[4],m=e[5],n=e[12];e=e[13];for(var p=f?f:[],q=0;b<c;b+=d){var r=a[b],u=a[b+1];p[q++]=g*r+l*u+n;p[q++]=h*r+m*u+e}f&&p.length!=q&&(p.length=q);return p};function hd(){Tc.call(this);this.f="XY";this.a=2;this.B=null}y(hd,Tc);function id(a){if("XY"==a)return 2;if("XYZ"==a||"XYM"==a)return 3;if("XYZM"==a)return 4}k=hd.prototype;k.Bc=rc;k.Od=function(a){return Yb(this.B,0,this.B.length,this.a,a)};k.Ib=function(){return this.B.slice(0,this.a)};k.la=function(){return this.B};k.Jb=function(){return this.B.slice(this.B.length-this.a)};k.Kb=function(){return this.f};
+k.od=function(a){this.s!=this.g&&(Fa(this.l),this.o=0,this.s=this.g);if(0>a||0!==this.o&&a<=this.o)return this;var b=a.toString();if(this.l.hasOwnProperty(b))return this.l[b];var c=this.Nc(a);if(c.la().length<this.B.length)return this.l[b]=c;this.o=a;return this};k.Nc=function(){return this};k.va=function(){return this.a};function jd(a,b,c){a.a=id(b);a.f=b;a.B=c}
+function kd(a,b,c,d){if(b)c=id(b);else{for(b=0;b<d;++b){if(0===c.length){a.f="XY";a.a=2;return}c=c[0]}c=c.length;b=2==c?"XY":3==c?"XYZ":4==c?"XYZM":void 0}a.f=b;a.a=c}k.rc=function(a){this.B&&(a(this.B,this.B,this.a),this.u())};k.rotate=function(a,b){var c=this.la();if(c){for(var d=c.length,e=this.va(),f=c?c:[],g=Math.cos(a),h=Math.sin(a),l=b[0],m=b[1],n=0,p=0;p<d;p+=e){var q=c[p]-l,r=c[p+1]-m;f[n++]=l+q*g-r*h;f[n++]=m+q*h+r*g;for(q=p+2;q<p+e;++q)f[n++]=c[q]}c&&f.length!=n&&(f.length=n);this.u()}};
+k.Sc=function(a,b){var c=this.la();if(c){var d=c.length,e=this.va(),f=c?c:[],g=0,h,l;for(h=0;h<d;h+=e)for(f[g++]=c[h]+a,f[g++]=c[h+1]+b,l=h+2;l<h+e;++l)f[g++]=c[l];c&&f.length!=g&&(f.length=g);this.u()}};function ld(a,b,c,d){for(var e=0,f=a[c-d],g=a[c-d+1];b<c;b+=d)var h=a[b],l=a[b+1],e=e+(g*h-f*l),f=h,g=l;return e/2}function md(a,b,c,d){var e=0,f,g;f=0;for(g=c.length;f<g;++f){var h=c[f],e=e+ld(a,b,h,d);b=h}return e};function nd(a,b,c,d,e,f,g){var h=a[b],l=a[b+1],m=a[c]-h,n=a[c+1]-l;if(0!==m||0!==n)if(f=((e-h)*m+(f-l)*n)/(m*m+n*n),1<f)b=c;else if(0<f){for(e=0;e<d;++e)g[e]=za(a[b+e],a[c+e],f);g.length=d;return}for(e=0;e<d;++e)g[e]=a[b+e];g.length=d}function od(a,b,c,d,e){var f=a[b],g=a[b+1];for(b+=d;b<c;b+=d){var h=a[b],l=a[b+1],f=va(f,g,h,l);f>e&&(e=f);f=h;g=l}return e}function pd(a,b,c,d,e){var f,g;f=0;for(g=c.length;f<g;++f){var h=c[f];e=od(a,b,h,d,e);b=h}return e}
+function qd(a,b,c,d,e,f,g,h,l,m,n){if(b==c)return m;var p;if(0===e){p=va(g,h,a[b],a[b+1]);if(p<m){for(n=0;n<d;++n)l[n]=a[b+n];l.length=d;return p}return m}for(var q=n?n:[NaN,NaN],r=b+d;r<c;)if(nd(a,r-d,r,d,g,h,q),p=va(g,h,q[0],q[1]),p<m){m=p;for(n=0;n<d;++n)l[n]=q[n];l.length=d;r+=d}else r+=d*Math.max((Math.sqrt(p)-Math.sqrt(m))/e|0,1);if(f&&(nd(a,c-d,b,d,g,h,q),p=va(g,h,q[0],q[1]),p<m)){m=p;for(n=0;n<d;++n)l[n]=q[n];l.length=d}return m}
+function rd(a,b,c,d,e,f,g,h,l,m,n){n=n?n:[NaN,NaN];var p,q;p=0;for(q=c.length;p<q;++p){var r=c[p];m=qd(a,b,r,d,e,f,g,h,l,m,n);b=r}return m};function sd(a,b){var c=0,d,e;d=0;for(e=b.length;d<e;++d)a[c++]=b[d];return c}function td(a,b,c,d){var e,f;e=0;for(f=c.length;e<f;++e){var g=c[e],h;for(h=0;h<d;++h)a[b++]=g[h]}return b}function ud(a,b,c,d,e){e=e?e:[];var f=0,g,h;g=0;for(h=c.length;g<h;++g)b=td(a,b,c[g],d),e[f++]=b;e.length=f;return e};function vd(a,b,c,d,e){e=void 0!==e?e:[];for(var f=0;b<c;b+=d)e[f++]=a.slice(b,b+d);e.length=f;return e}function wd(a,b,c,d,e){e=void 0!==e?e:[];var f=0,g,h;g=0;for(h=c.length;g<h;++g){var l=c[g];e[f++]=vd(a,b,l,d,e[f]);b=l}e.length=f;return e};function xd(a,b,c,d,e,f,g){var h=(c-b)/d;if(3>h){for(;b<c;b+=d)f[g++]=a[b],f[g++]=a[b+1];return g}var l=Array(h);l[0]=1;l[h-1]=1;c=[b,c-d];for(var m=0,n;0<c.length;){var p=c.pop(),q=c.pop(),r=0,u=a[q],x=a[q+1],v=a[p],D=a[p+1];for(n=q+d;n<p;n+=d){var A=ua(a[n],a[n+1],u,x,v,D);A>r&&(m=n,r=A)}r>e&&(l[(m-b)/d]=1,q+d<m&&c.push(q,m),m+d<p&&c.push(m,p))}for(n=0;n<h;++n)l[n]&&(f[g++]=a[b+n*d],f[g++]=a[b+n*d+1]);return g}
+function yd(a,b,c,d,e,f,g,h){var l,m;l=0;for(m=c.length;l<m;++l){var n=c[l];a:{var p=a,q=n,r=d,u=e,x=f;if(b!=q){var v=u*Math.round(p[b]/u),D=u*Math.round(p[b+1]/u);b+=r;x[g++]=v;x[g++]=D;var A,z;do if(A=u*Math.round(p[b]/u),z=u*Math.round(p[b+1]/u),b+=r,b==q){x[g++]=A;x[g++]=z;break a}while(A==v&&z==D);for(;b<q;){var F,N;F=u*Math.round(p[b]/u);N=u*Math.round(p[b+1]/u);b+=r;if(F!=A||N!=z){var K=A-v,X=z-D,oa=F-v,H=N-D;K*H==X*oa&&(0>K&&oa<K||K==oa||0<K&&oa>K)&&(0>X&&H<X||X==H||0<X&&H>X)||(x[g++]=A,x[g++]=
+z,v=A,D=z);A=F;z=N}}x[g++]=A;x[g++]=z}}h.push(g);b=n}return g};function zd(a,b){hd.call(this);this.i=this.j=-1;this.pa(a,b)}y(zd,hd);k=zd.prototype;k.clone=function(){var a=new zd(null);Ad(a,this.f,this.B.slice());return a};k.sb=function(a,b,c,d){if(d<Rb(this.H(),a,b))return d;this.i!=this.g&&(this.j=Math.sqrt(od(this.B,0,this.B.length,this.a,0)),this.i=this.g);return qd(this.B,0,this.B.length,this.a,this.j,!0,a,b,c,d)};k.nm=function(){return ld(this.B,0,this.B.length,this.a)};k.Z=function(){return vd(this.B,0,this.B.length,this.a)};
+k.Nc=function(a){var b=[];b.length=xd(this.B,0,this.B.length,this.a,a,b,0);a=new zd(null);Ad(a,"XY",b);return a};k.X=function(){return"LinearRing"};k.pa=function(a,b){a?(kd(this,b,a,1),this.B||(this.B=[]),this.B.length=td(this.B,0,a,this.a),this.u()):Ad(this,"XY",null)};function Ad(a,b,c){jd(a,b,c);a.u()};function C(a,b){hd.call(this);this.pa(a,b)}y(C,hd);k=C.prototype;k.clone=function(){var a=new C(null);a.ba(this.f,this.B.slice());return a};k.sb=function(a,b,c,d){var e=this.B;a=va(a,b,e[0],e[1]);if(a<d){d=this.a;for(b=0;b<d;++b)c[b]=e[b];c.length=d;return a}return d};k.Z=function(){return this.B?this.B.slice():[]};k.Od=function(a){return Xb(this.B,a)};k.X=function(){return"Point"};k.Ka=function(a){return Tb(a,this.B[0],this.B[1])};
+k.pa=function(a,b){a?(kd(this,b,a,0),this.B||(this.B=[]),this.B.length=sd(this.B,a),this.u()):this.ba("XY",null)};k.ba=function(a,b){jd(this,a,b);this.u()};function Bd(a,b,c,d,e){return!bc(e,function(e){return!Cd(a,b,c,d,e[0],e[1])})}function Cd(a,b,c,d,e,f){for(var g=!1,h=a[c-d],l=a[c-d+1];b<c;b+=d){var m=a[b],n=a[b+1];l>f!=n>f&&e<(m-h)*(f-l)/(n-l)+h&&(g=!g);h=m;l=n}return g}function Dd(a,b,c,d,e,f){if(0===c.length||!Cd(a,b,c[0],d,e,f))return!1;var g;b=1;for(g=c.length;b<g;++b)if(Cd(a,c[b-1],c[b],d,e,f))return!1;return!0};function Ed(a,b,c,d,e,f,g){var h,l,m,n,p,q=e[f+1],r=[],u=c[0];m=a[u-d];p=a[u-d+1];for(h=b;h<u;h+=d){n=a[h];l=a[h+1];if(q<=p&&l<=q||p<=q&&q<=l)m=(q-p)/(l-p)*(n-m)+m,r.push(m);m=n;p=l}u=NaN;p=-Infinity;r.sort(ib);m=r[0];h=1;for(l=r.length;h<l;++h){n=r[h];var x=Math.abs(n-m);x>p&&(m=(m+n)/2,Dd(a,b,c,d,m,q)&&(u=m,p=x));m=n}isNaN(u)&&(u=e[f]);return g?(g.push(u,q),g):[u,q]};function Fd(a,b,c,d,e,f){for(var g=[a[b],a[b+1]],h=[],l;b+d<c;b+=d){h[0]=a[b+d];h[1]=a[b+d+1];if(l=e.call(f,g,h))return l;g[0]=h[0];g[1]=h[1]}return!1};function Gd(a,b,c,d,e){var f=Zb(Lb(),a,b,c,d);return nc(e,f)?Ub(e,f)||f[0]>=e[0]&&f[2]<=e[2]||f[1]>=e[1]&&f[3]<=e[3]?!0:Fd(a,b,c,d,function(a,b){var c=!1,d=Vb(e,a),f=Vb(e,b);if(1===d||1===f)c=!0;else{var p=e[0],q=e[1],r=e[2],u=e[3],x=b[0],v=b[1],D=(v-a[1])/(x-a[0]);f&2&&!(d&2)&&(c=x-(v-u)/D,c=c>=p&&c<=r);c||!(f&4)||d&4||(c=v-(x-r)*D,c=c>=q&&c<=u);c||!(f&8)||d&8||(c=x-(v-q)/D,c=c>=p&&c<=r);c||!(f&16)||d&16||(c=v-(x-p)*D,c=c>=q&&c<=u)}return c}):!1}
+function Hd(a,b,c,d,e){var f=c[0];if(!(Gd(a,b,f,d,e)||Cd(a,b,f,d,e[0],e[1])||Cd(a,b,f,d,e[0],e[3])||Cd(a,b,f,d,e[2],e[1])||Cd(a,b,f,d,e[2],e[3])))return!1;if(1===c.length)return!0;b=1;for(f=c.length;b<f;++b)if(Bd(a,c[b-1],c[b],d,e))return!1;return!0};function Id(a,b,c,d){for(var e=0,f=a[c-d],g=a[c-d+1];b<c;b+=d)var h=a[b],l=a[b+1],e=e+(h-f)*(l+g),f=h,g=l;return 0<e}function Jd(a,b,c,d){var e=0;d=void 0!==d?d:!1;var f,g;f=0;for(g=b.length;f<g;++f){var h=b[f],e=Id(a,e,h,c);if(0===f){if(d&&e||!d&&!e)return!1}else if(d&&!e||!d&&e)return!1;e=h}return!0}
+function Kd(a,b,c,d,e){e=void 0!==e?e:!1;var f,g;f=0;for(g=c.length;f<g;++f){var h=c[f],l=Id(a,b,h,d);if(0===f?e&&l||!e&&!l:e&&!l||!e&&l)for(var l=a,m=h,n=d;b<m-n;){var p;for(p=0;p<n;++p){var q=l[b+p];l[b+p]=l[m-n+p];l[m-n+p]=q}b+=n;m-=n}b=h}return b}function Ld(a,b,c,d){var e=0,f,g;f=0;for(g=b.length;f<g;++f)e=Kd(a,e,b[f],c,d);return e};function E(a,b){hd.call(this);this.i=[];this.C=-1;this.D=null;this.T=this.R=this.S=-1;this.j=null;this.pa(a,b)}y(E,hd);k=E.prototype;k.yj=function(a){this.B?mb(this.B,a.la()):this.B=a.la().slice();this.i.push(this.B.length);this.u()};k.clone=function(){var a=new E(null);a.ba(this.f,this.B.slice(),this.i.slice());return a};
+k.sb=function(a,b,c,d){if(d<Rb(this.H(),a,b))return d;this.R!=this.g&&(this.S=Math.sqrt(pd(this.B,0,this.i,this.a,0)),this.R=this.g);return rd(this.B,0,this.i,this.a,this.S,!0,a,b,c,d)};k.Bc=function(a,b){return Dd(this.Mb(),0,this.i,this.a,a,b)};k.qm=function(){return md(this.Mb(),0,this.i,this.a)};k.Z=function(a){var b;void 0!==a?(b=this.Mb().slice(),Kd(b,0,this.i,this.a,a)):b=this.B;return wd(b,0,this.i,this.a)};k.Db=function(){return this.i};
+function Md(a){if(a.C!=a.g){var b=kc(a.H());a.D=Ed(a.Mb(),0,a.i,a.a,b,0);a.C=a.g}return a.D}k.bk=function(){return new C(Md(this))};k.gk=function(){return this.i.length};k.Hg=function(a){if(0>a||this.i.length<=a)return null;var b=new zd(null);Ad(b,this.f,this.B.slice(0===a?0:this.i[a-1],this.i[a]));return b};k.Vd=function(){var a=this.f,b=this.B,c=this.i,d=[],e=0,f,g;f=0;for(g=c.length;f<g;++f){var h=c[f],l=new zd(null);Ad(l,a,b.slice(e,h));d.push(l);e=h}return d};
+k.Mb=function(){if(this.T!=this.g){var a=this.B;Jd(a,this.i,this.a)?this.j=a:(this.j=a.slice(),this.j.length=Kd(this.j,0,this.i,this.a));this.T=this.g}return this.j};k.Nc=function(a){var b=[],c=[];b.length=yd(this.B,0,this.i,this.a,Math.sqrt(a),b,0,c);a=new E(null);a.ba("XY",b,c);return a};k.X=function(){return"Polygon"};k.Ka=function(a){return Hd(this.Mb(),0,this.i,this.a,a)};
+k.pa=function(a,b){if(a){kd(this,b,a,2);this.B||(this.B=[]);var c=ud(this.B,0,a,this.a,this.i);this.B.length=0===c.length?0:c[c.length-1];this.u()}else this.ba("XY",null,this.i)};k.ba=function(a,b,c){jd(this,a,b);this.i=c;this.u()};function Nd(a,b,c,d){var e=d?d:32;d=[];var f;for(f=0;f<e;++f)mb(d,a.offset(b,c,2*Math.PI*f/e));d.push(d[0],d[1]);a=new E(null);a.ba("XY",d,[d.length]);return a}
+function Od(a){var b=a[0],c=a[1],d=a[2];a=a[3];b=[b,c,b,a,d,a,d,c,b,c];c=new E(null);c.ba("XY",b,[b.length]);return c}function Pd(a,b,c){var d=b?b:32,e=a.va();b=a.f;for(var f=new E(null,b),d=e*(d+1),e=Array(d),g=0;g<d;g++)e[g]=0;f.ba(b,e,[e.length]);Qd(f,a.rd(),a.wf(),c);return f}function Qd(a,b,c,d){var e=a.la(),f=a.f,g=a.va(),h=a.Db(),l=e.length/g-1;d=d?d:0;for(var m,n,p=0;p<=l;++p)n=p*g,m=d+2*xa(p,l)*Math.PI/l,e[n]=b[0]+c*Math.cos(m),e[n+1]=b[1]+c*Math.sin(m);a.ba(f,e,h)};function Rd(a){eb.call(this);a=a||{};this.f=[0,0];var b={};b.center=void 0!==a.center?a.center:null;this.l=Mc(a.projection);var c,d,e,f=void 0!==a.minZoom?a.minZoom:0;c=void 0!==a.maxZoom?a.maxZoom:28;var g=void 0!==a.zoomFactor?a.zoomFactor:2;if(void 0!==a.resolutions)c=a.resolutions,d=c[0],e=c[c.length-1],c=tb(c);else{d=Mc(a.projection);e=d.H();var h=(e?Math.max(ic(e),jc(e)):360*uc.degrees/d.$b())/256/Math.pow(2,0),l=h/Math.pow(2,28);d=a.maxResolution;void 0!==d?f=0:d=h/Math.pow(g,f);e=a.minResolution;
+void 0===e&&(e=void 0!==a.maxZoom?void 0!==a.maxResolution?d/Math.pow(g,c):h/Math.pow(g,c):l);c=f+Math.floor(Math.log(d/e)/Math.log(g));e=d/Math.pow(g,c-f);c=ub(g,d,c-f)}this.a=d;this.c=e;this.j=a.resolutions;this.i=f;f=void 0!==a.extent?Ba(a.extent):Ca;(void 0!==a.enableRotation?a.enableRotation:1)?(d=a.constrainRotation,d=void 0===d||!0===d?yb():!1===d?wb:ea(d)?xb(d):wb):d=vb;this.o=new Da(f,c,d);void 0!==a.resolution?b.resolution=a.resolution:void 0!==a.zoom&&(b.resolution=this.constrainResolution(this.a,
+a.zoom-this.i));b.rotation=void 0!==a.rotation?a.rotation:0;this.G(b)}y(Rd,eb);k=Rd.prototype;k.Pd=function(a){return this.o.center(a)};k.constrainResolution=function(a,b,c){return this.o.resolution(a,b||0,c||0)};k.constrainRotation=function(a,b){return this.o.rotation(a,b||0)};k.ab=function(){return this.get("center")};function Sd(a,b){return void 0!==b?(b[0]=a.f[0],b[1]=a.f[1],b):a.f.slice()}k.Kc=function(a){var b=this.ab(),c=this.$(),d=this.La();return lc(b,c,d,a)};k.Vl=function(){return this.a};
+k.Wl=function(){return this.c};k.Xl=function(){return this.l};k.$=function(){return this.get("resolution")};k.Yl=function(){return this.j};function Td(a,b){return Math.max(ic(a)/b[0],jc(a)/b[1])}function Ud(a){var b=a.a,c=Math.log(b/a.c)/Math.log(2);return function(a){return b/Math.pow(2,a*c)}}k.La=function(){return this.get("rotation")};function Vd(a){var b=a.a,c=Math.log(b/a.c)/Math.log(2);return function(a){return Math.log(b/a)/Math.log(2)/c}}
+k.V=function(){var a=this.ab(),b=this.l,c=this.$(),d=this.La();return{center:[Math.round(a[0]/c)*c,Math.round(a[1]/c)*c],projection:void 0!==b?b:null,resolution:c,rotation:d}};k.Fk=function(){var a,b=this.$();if(void 0!==b){var c,d=0;do{c=this.constrainResolution(this.a,d);if(c==b){a=d;break}++d}while(c>this.c)}return void 0!==a?this.i+a:a};
+k.cf=function(a,b,c){a instanceof hd||(a=Od(a));var d=c||{};c=void 0!==d.padding?d.padding:[0,0,0,0];var e=void 0!==d.constrainResolution?d.constrainResolution:!0,f=void 0!==d.nearest?d.nearest:!1,g;void 0!==d.minResolution?g=d.minResolution:void 0!==d.maxZoom?g=this.constrainResolution(this.a,d.maxZoom-this.i,0):g=0;var h=a.la(),l=this.La(),d=Math.cos(-l),l=Math.sin(-l),m=Infinity,n=Infinity,p=-Infinity,q=-Infinity;a=a.va();for(var r=0,u=h.length;r<u;r+=a)var x=h[r]*d-h[r+1]*l,v=h[r]*l+h[r+1]*d,
+m=Math.min(m,x),n=Math.min(n,v),p=Math.max(p,x),q=Math.max(q,v);b=Td([m,n,p,q],[b[0]-c[1]-c[3],b[1]-c[0]-c[2]]);b=isNaN(b)?g:Math.max(b,g);e&&(g=this.constrainResolution(b,0,0),!f&&g<b&&(g=this.constrainResolution(g,-1,0)),b=g);this.Ub(b);l=-l;f=(m+p)/2+(c[1]-c[3])/2*b;c=(n+q)/2+(c[0]-c[2])/2*b;this.mb([f*d-c*l,c*d+f*l])};
+k.Ej=function(a,b,c){var d=this.La(),e=Math.cos(-d),d=Math.sin(-d),f=a[0]*e-a[1]*d;a=a[1]*e+a[0]*d;var g=this.$(),f=f+(b[0]/2-c[0])*g;a+=(c[1]-b[1]/2)*g;d=-d;this.mb([f*e-a*d,a*e+f*d])};function Wd(a){return!!a.ab()&&void 0!==a.$()}k.rotate=function(a,b){if(void 0!==b){var c,d=this.ab();void 0!==d&&(c=[d[0]-b[0],d[1]-b[1]],Gb(c,a-this.La()),Bb(c,b));this.mb(c)}this.ie(a)};k.mb=function(a){this.set("center",a)};function Xd(a,b){a.f[1]+=b}k.Ub=function(a){this.set("resolution",a)};
+k.ie=function(a){this.set("rotation",a)};k.np=function(a){a=this.constrainResolution(this.a,a-this.i,0);this.Ub(a)};function Yd(a){return Math.pow(a,3)}function Zd(a){return 1-Yd(1-a)}function $d(a){return 3*a*a-2*a*a*a}function ae(a){return a}function be(a){return.5>a?$d(2*a):1-$d(2*(a-.5))};function ce(a){var b=a.source,c=a.start?a.start:Date.now(),d=b[0],e=b[1],f=void 0!==a.duration?a.duration:1E3,g=a.easing?a.easing:$d;return function(a,b){if(b.time<c)return b.animate=!0,b.viewHints[0]+=1,!0;if(b.time<c+f){var m=1-g((b.time-c)/f),n=d-b.viewState.center[0],p=e-b.viewState.center[1];b.animate=!0;b.viewState.center[0]+=m*n;b.viewState.center[1]+=m*p;b.viewHints[0]+=1;return!0}return!1}}
+function de(a){var b=a.rotation?a.rotation:0,c=a.start?a.start:Date.now(),d=void 0!==a.duration?a.duration:1E3,e=a.easing?a.easing:$d,f=a.anchor?a.anchor:null;return function(a,h){if(h.time<c)return h.animate=!0,h.viewHints[0]+=1,!0;if(h.time<c+d){var l=1-e((h.time-c)/d),l=(b-h.viewState.rotation)*l;h.animate=!0;h.viewState.rotation+=l;if(f){var m=h.viewState.center;m[0]-=f[0];m[1]-=f[1];Gb(m,l);Bb(m,f)}h.viewHints[0]+=1;return!0}return!1}}
+function ee(a){var b=a.resolution,c=a.start?a.start:Date.now(),d=void 0!==a.duration?a.duration:1E3,e=a.easing?a.easing:$d;return function(a,g){if(g.time<c)return g.animate=!0,g.viewHints[0]+=1,!0;if(g.time<c+d){var h=1-e((g.time-c)/d),l=b-g.viewState.resolution;g.animate=!0;g.viewState.resolution+=h*l;g.viewHints[0]+=1;return!0}return!1}};function fe(a,b,c,d){this.ca=a;this.ea=b;this.fa=c;this.ga=d}fe.prototype.contains=function(a){return ge(this,a[1],a[2])};function ge(a,b,c){return a.ca<=b&&b<=a.ea&&a.fa<=c&&c<=a.ga}function he(a,b){return a.ca==b.ca&&a.fa==b.fa&&a.ea==b.ea&&a.ga==b.ga}function ie(a,b){return a.ca<=b.ea&&a.ea>=b.ca&&a.fa<=b.ga&&a.ga>=b.fa};function je(a){this.a=a.html;this.b=a.tileRanges?a.tileRanges:null}je.prototype.g=function(){return this.a};function ke(a,b,c){Wa.call(this,a,c);this.element=b}y(ke,Wa);function le(a){eb.call(this);this.a=a?a:[];me(this)}y(le,eb);k=le.prototype;k.clear=function(){for(;0<this.dc();)this.pop()};k.qf=function(a){var b,c;b=0;for(c=a.length;b<c;++b)this.push(a[b]);return this};k.forEach=function(a,b){this.a.forEach(a,b)};k.Gl=function(){return this.a};k.item=function(a){return this.a[a]};k.dc=function(){return this.get("length")};k.ee=function(a,b){this.a.splice(a,0,b);me(this);this.b(new ke("add",b,this))};
+k.pop=function(){return this.Rf(this.dc()-1)};k.push=function(a){var b=this.a.length;this.ee(b,a);return b};k.remove=function(a){var b=this.a,c,d;c=0;for(d=b.length;c<d;++c)if(b[c]===a)return this.Rf(c)};k.Rf=function(a){var b=this.a[a];this.a.splice(a,1);me(this);this.b(new ke("remove",b,this));return b};k.Zo=function(a,b){var c=this.dc();if(a<c)c=this.a[a],this.a[a]=b,this.b(new ke("remove",c,this)),this.b(new ke("add",b,this));else{for(;c<a;++c)this.ee(c,void 0);this.ee(a,b)}};
+function me(a){a.set("length",a.a.length)};function ne(a){return Array.prototype.concat.apply(Array.prototype,arguments)}function oe(a){var b=a.length;if(0<b){for(var c=Array(b),d=0;d<b;d++)c[d]=a[d];return c}return[]}function pe(a,b,c){return 2>=arguments.length?Array.prototype.slice.call(a,b):Array.prototype.slice.call(a,b,c)};var qe=/^#(?:[0-9a-f]{3}){1,2}$/i,re=/^(?:rgb)?\((0|[1-9]\d{0,2}),\s?(0|[1-9]\d{0,2}),\s?(0|[1-9]\d{0,2})\)$/i,se=/^(?:rgba)?\((0|[1-9]\d{0,2}),\s?(0|[1-9]\d{0,2}),\s?(0|[1-9]\d{0,2}),\s?(0|1|0\.\d{0,10})\)$/i;function te(a){return Array.isArray(a)?a:ue(a)}function ve(a){if("string"!==typeof a){var b=a[0];b!=(b|0)&&(b=b+.5|0);var c=a[1];c!=(c|0)&&(c=c+.5|0);var d=a[2];d!=(d|0)&&(d=d+.5|0);a="rgba("+b+","+c+","+d+","+(void 0===a[3]?1:a[3])+")"}return a}
+var ue=function(){var a={},b=0;return function(c){var d;if(a.hasOwnProperty(c))d=a[c];else{if(1024<=b){d=0;for(var e in a)0===(d++&3)&&(delete a[e],--b)}var f,g;qe.exec(c)?(g=3==c.length-1?1:2,d=parseInt(c.substr(1+0*g,g),16),e=parseInt(c.substr(1+1*g,g),16),f=parseInt(c.substr(1+2*g,g),16),1==g&&(d=(d<<4)+d,e=(e<<4)+e,f=(f<<4)+f),d=[d,e,f,1]):(g=se.exec(c))?(d=Number(g[1]),e=Number(g[2]),f=Number(g[3]),g=Number(g[4]),d=[d,e,f,g],d=we(d,d)):(g=re.exec(c))?(d=Number(g[1]),e=Number(g[2]),f=Number(g[3]),
+d=[d,e,f,1],d=we(d,d)):d=void 0;a[c]=d;++b}return d}}();function we(a,b){var c=b||[];c[0]=sa(a[0]+.5|0,0,255);c[1]=sa(a[1]+.5|0,0,255);c[2]=sa(a[2]+.5|0,0,255);c[3]=sa(a[3],0,1);return c};function xe(a){return"string"===typeof a||a instanceof CanvasPattern||a instanceof CanvasGradient?a:ve(a)};var ye;a:{var ze=aa.navigator;if(ze){var Ae=ze.userAgent;if(Ae){ye=Ae;break a}}ye=""}function Be(a){return-1!=ye.indexOf(a)};var Ce=Be("Opera"),Ee=Be("Trident")||Be("MSIE"),Fe=Be("Edge"),Ge=Be("Gecko")&&!(-1!=ye.toLowerCase().indexOf("webkit")&&!Be("Edge"))&&!(Be("Trident")||Be("MSIE"))&&!Be("Edge"),He=-1!=ye.toLowerCase().indexOf("webkit")&&!Be("Edge"),Ie;
+a:{var Je="",Ke=function(){var a=ye;if(Ge)return/rv\:([^\);]+)(\)|;)/.exec(a);if(Fe)return/Edge\/([\d\.]+)/.exec(a);if(Ee)return/\b(?:MSIE|rv)[: ]([^\);]+)(\)|;)/.exec(a);if(He)return/WebKit\/(\S+)/.exec(a);if(Ce)return/(?:Version)[ \/]?(\S+)/.exec(a)}();Ke&&(Je=Ke?Ke[1]:"");if(Ee){var Le,Me=aa.document;Le=Me?Me.documentMode:void 0;if(null!=Le&&Le>parseFloat(Je)){Ie=String(Le);break a}}Ie=Je}var Ne={};function Oe(a,b){var c=document.createElement("CANVAS");a&&(c.width=a);b&&(c.height=b);return c.getContext("2d")}
+var Pe=function(){var a;return function(){if(void 0===a){var b=document.createElement("P"),c,d={webkitTransform:"-webkit-transform",OTransform:"-o-transform",msTransform:"-ms-transform",MozTransform:"-moz-transform",transform:"transform"};document.body.appendChild(b);for(var e in d)e in b.style&&(b.style[e]="translate(1px,1px)",c=pa.getComputedStyle(b).getPropertyValue(d[e]));document.body.removeChild(b);a=c&&"none"!==c}return a}}(),Qe=function(){var a;return function(){if(void 0===a){var b=document.createElement("P"),
+c,d={webkitTransform:"-webkit-transform",OTransform:"-o-transform",msTransform:"-ms-transform",MozTransform:"-moz-transform",transform:"transform"};document.body.appendChild(b);for(var e in d)e in b.style&&(b.style[e]="translate3d(1px,1px,1px)",c=pa.getComputedStyle(b).getPropertyValue(d[e]));document.body.removeChild(b);a=c&&"none"!==c}return a}}();
+function Re(a,b){var c=a.style;c.WebkitTransform=b;c.MozTransform=b;c.b=b;c.msTransform=b;c.transform=b;if((c=Ee)&&!(c=Ne["9.0"])){for(var c=0,d=qa(String(Ie)).split("."),e=qa("9.0").split("."),f=Math.max(d.length,e.length),g=0;0==c&&g<f;g++){var h=d[g]||"",l=e[g]||"",m=RegExp("(\\d*)(\\D*)","g"),n=RegExp("(\\d*)(\\D*)","g");do{var p=m.exec(h)||["","",""],q=n.exec(l)||["","",""];if(0==p[0].length&&0==q[0].length)break;c=ra(0==p[1].length?0:parseInt(p[1],10),0==q[1].length?0:parseInt(q[1],10))||ra(0==
+p[2].length,0==q[2].length)||ra(p[2],q[2])}while(0==c)}c=Ne["9.0"]=0<=c}c&&(a.style.transformOrigin="0 0")}function Se(a,b){var c;if(Qe()){var d=Array(16);for(c=0;16>c;++c)d[c]=b[c].toFixed(6);Re(a,"matrix3d("+d.join(",")+")")}else if(Pe()){var d=[b[0],b[1],b[4],b[5],b[12],b[13]],e=Array(6);for(c=0;6>c;++c)e[c]=d[c].toFixed(6);Re(a,"matrix("+e.join(",")+")")}else a.style.left=Math.round(b[12])+"px",a.style.top=Math.round(b[13])+"px"}function Te(a,b){var c=b.parentNode;c&&c.replaceChild(a,b)}
+function Ue(a){a&&a.parentNode&&a.parentNode.removeChild(a)}function Ve(a){for(;a.lastChild;)a.removeChild(a.lastChild)};function We(a,b,c){Wa.call(this,a);this.map=b;this.frameState=void 0!==c?c:null}y(We,Wa);function Xe(a){eb.call(this);this.element=a.element?a.element:null;this.a=this.S=null;this.s=[];this.render=a.render?a.render:na;a.target&&this.c(a.target)}y(Xe,eb);Xe.prototype.ka=function(){Ue(this.element);eb.prototype.ka.call(this)};Xe.prototype.i=function(){return this.a};
+Xe.prototype.setMap=function(a){this.a&&Ue(this.element);for(var b=0,c=this.s.length;b<c;++b)Ka(this.s[b]);this.s.length=0;if(this.a=a)(this.S?this.S:a.v).appendChild(this.element),this.render!==na&&this.s.push(B(a,"postrender",this.render,this)),a.render()};Xe.prototype.c=function(a){this.S="string"===typeof a?document.getElementById(a):a};function Ye(){this.g=0;this.f={};this.a=this.b=null}k=Ye.prototype;k.clear=function(){this.g=0;this.f={};this.a=this.b=null};function Ze(a,b){return a.f.hasOwnProperty(b)}k.forEach=function(a,b){for(var c=this.b;c;)a.call(b,c.pc,c.cc,this),c=c.yb};k.get=function(a){a=this.f[a];if(a===this.a)return a.pc;a===this.b?(this.b=this.b.yb,this.b.kc=null):(a.yb.kc=a.kc,a.kc.yb=a.yb);a.yb=null;a.kc=this.a;this.a=this.a.yb=a;return a.pc};k.wc=function(){return this.g};
+k.N=function(){var a=Array(this.g),b=0,c;for(c=this.a;c;c=c.kc)a[b++]=c.cc;return a};k.zc=function(){var a=Array(this.g),b=0,c;for(c=this.a;c;c=c.kc)a[b++]=c.pc;return a};k.pop=function(){var a=this.b;delete this.f[a.cc];a.yb&&(a.yb.kc=null);this.b=a.yb;this.b||(this.a=null);--this.g;return a.pc};k.replace=function(a,b){this.get(a);this.f[a].pc=b};k.set=function(a,b){var c={cc:a,yb:null,kc:this.a,pc:b};this.a?this.a.yb=c:this.b=c;this.a=c;this.f[a]=c;++this.g};function $e(a,b,c,d){return void 0!==d?(d[0]=a,d[1]=b,d[2]=c,d):[a,b,c]}function af(a){var b=a[0],c=Array(b),d=1<<b-1,e,f;for(e=0;e<b;++e)f=48,a[1]&d&&(f+=1),a[2]&d&&(f+=2),c[e]=String.fromCharCode(f),d>>=1;return c.join("")};function bf(a){Ye.call(this);this.c=void 0!==a?a:2048}y(bf,Ye);function cf(a){return a.wc()>a.c}bf.prototype.Lc=function(a){for(var b,c;cf(this)&&!(b=this.b.pc,c=b.ma[0].toString(),c in a&&a[c].contains(b.ma));)Ta(this.pop())};function df(a,b){$a.call(this);this.ma=a;this.state=b;this.a=null;this.key=""}y(df,$a);function ef(a){a.b("change")}df.prototype.ib=function(){return w(this).toString()};df.prototype.i=function(){return this.ma};df.prototype.V=function(){return this.state};function ff(a,b,c){void 0===c&&(c=[0,0]);c[0]=a[0]+2*b;c[1]=a[1]+2*b;return c}function gf(a,b,c){void 0===c&&(c=[0,0]);c[0]=a[0]*b+.5|0;c[1]=a[1]*b+.5|0;return c}function hf(a,b){if(Array.isArray(a))return a;void 0===b?b=[a,a]:(b[0]=a,b[1]=a);return b};function jf(a){eb.call(this);this.f=yc(a.projection);this.l=kf(a.attributions);this.R=a.logo;this.za=void 0!==a.state?a.state:"ready";this.D=void 0!==a.wrapX?a.wrapX:!1}y(jf,eb);function kf(a){if("string"===typeof a)return[new je({html:a})];if(a instanceof je)return[a];if(Array.isArray(a)){for(var b=a.length,c=Array(b),d=0;d<b;d++){var e=a[d];c[d]="string"===typeof e?new je({html:e}):e}return c}return null}k=jf.prototype;k.ra=na;k.wa=function(){return this.l};k.ua=function(){return this.R};k.xa=function(){return this.f};
+k.V=function(){return this.za};k.sa=function(){this.u()};k.oa=function(a){this.l=kf(a);this.u()};function lf(a,b){a.za=b;a.u()};function mf(a){this.minZoom=void 0!==a.minZoom?a.minZoom:0;this.b=a.resolutions;this.maxZoom=this.b.length-1;this.g=void 0!==a.origin?a.origin:null;this.c=null;void 0!==a.origins&&(this.c=a.origins);var b=a.extent;void 0===b||this.g||this.c||(this.g=fc(b));this.i=null;void 0!==a.tileSizes&&(this.i=a.tileSizes);this.o=void 0!==a.tileSize?a.tileSize:this.i?null:256;this.s=void 0!==b?b:null;this.a=null;this.f=[0,0];void 0!==a.sizes?this.a=a.sizes.map(function(a){return new fe(Math.min(0,a[0]),Math.max(a[0]-
+1,-1),Math.min(0,a[1]),Math.max(a[1]-1,-1))},this):b&&nf(this,b)}var of=[0,0,0];k=mf.prototype;k.yg=function(a,b,c){a=pf(this,a,b);for(var d=a.ca,e=a.ea;d<=e;++d)for(var f=a.fa,g=a.ga;f<=g;++f)c([b,d,f])};function qf(a,b,c,d,e){e=a.Ea(b,e);for(b=b[0]-1;b>=a.minZoom;){if(c.call(null,b,pf(a,e,b,d)))return!0;--b}return!1}k.H=function(){return this.s};k.Ig=function(){return this.maxZoom};k.Jg=function(){return this.minZoom};k.Ia=function(a){return this.g?this.g:this.c[a]};k.$=function(a){return this.b[a]};
+k.Kh=function(){return this.b};function rf(a,b,c,d){return b[0]<a.maxZoom?(d=a.Ea(b,d),pf(a,d,b[0]+1,c)):null}function sf(a,b,c,d){tf(a,b[0],b[1],c,!1,of);var e=of[1],f=of[2];tf(a,b[2],b[3],c,!0,of);a=of[1];b=of[2];void 0!==d?(d.ca=e,d.ea=a,d.fa=f,d.ga=b):d=new fe(e,a,f,b);return d}function pf(a,b,c,d){c=a.$(c);return sf(a,b,c,d)}function uf(a,b){var c=a.Ia(b[0]),d=a.$(b[0]),e=hf(a.Ja(b[0]),a.f);return[c[0]+(b[1]+.5)*e[0]*d,c[1]+(b[2]+.5)*e[1]*d]}
+k.Ea=function(a,b){var c=this.Ia(a[0]),d=this.$(a[0]),e=hf(this.Ja(a[0]),this.f),f=c[0]+a[1]*e[0]*d,c=c[1]+a[2]*e[1]*d;return Wb(f,c,f+e[0]*d,c+e[1]*d,b)};k.Zd=function(a,b,c){return tf(this,a[0],a[1],b,!1,c)};function tf(a,b,c,d,e,f){var g=a.Lb(d),h=d/a.$(g),l=a.Ia(g);a=hf(a.Ja(g),a.f);b=h*Math.floor((b-l[0])/d+(e?.5:0))/a[0];c=h*Math.floor((c-l[1])/d+(e?0:.5))/a[1];e?(b=Math.ceil(b)-1,c=Math.ceil(c)-1):(b=Math.floor(b),c=Math.floor(c));return $e(g,b,c,f)}
+k.qd=function(a,b,c){b=this.$(b);return tf(this,a[0],a[1],b,!1,c)};k.Ja=function(a){return this.o?this.o:this.i[a]};k.Lb=function(a,b){var c=kb(this.b,a,b||0);return sa(c,this.minZoom,this.maxZoom)};function nf(a,b){for(var c=a.b.length,d=Array(c),e=a.minZoom;e<c;++e)d[e]=pf(a,b,e);a.a=d}function vf(a){var b=a.l;if(!b){var b=wf(a),c=xf(b,void 0,void 0),b=new mf({extent:b,origin:fc(b),resolutions:c,tileSize:void 0});a.l=b}return b}
+function yf(a){var b={};Ea(b,void 0!==a?a:{});void 0===b.extent&&(b.extent=yc("EPSG:3857").H());b.resolutions=xf(b.extent,b.maxZoom,b.tileSize);delete b.maxZoom;return new mf(b)}function xf(a,b,c){b=void 0!==b?b:42;var d=jc(a);a=ic(a);c=hf(void 0!==c?c:256);c=Math.max(a/c[0],d/c[1]);b+=1;d=Array(b);for(a=0;a<b;++a)d[a]=c/Math.pow(2,a);return d}function wf(a){a=yc(a);var b=a.H();b||(a=180*uc.degrees/a.$b(),b=Wb(-a,-a,a,a));return b};function zf(a){jf.call(this,{attributions:a.attributions,extent:a.extent,logo:a.logo,projection:a.projection,state:a.state,wrapX:a.wrapX});this.ia=void 0!==a.opaque?a.opaque:!1;this.ta=void 0!==a.tilePixelRatio?a.tilePixelRatio:1;this.tileGrid=void 0!==a.tileGrid?a.tileGrid:null;this.a=new bf(a.cacheSize);this.o=[0,0];this.cc=""}y(zf,jf);k=zf.prototype;k.Ah=function(){return cf(this.a)};k.Lc=function(a,b){var c=this.pd(a);c&&c.Lc(b)};
+function Af(a,b,c,d,e){b=a.pd(b);if(!b)return!1;for(var f=!0,g,h,l=d.ca;l<=d.ea;++l)for(var m=d.fa;m<=d.ga;++m)g=a.Eb(c,l,m),h=!1,Ze(b,g)&&(g=b.get(g),(h=2===g.V())&&(h=!1!==e(g))),h||(f=!1);return f}k.Ud=function(){return 0};function Bf(a,b){a.cc!==b&&(a.cc=b,a.u())}k.Eb=function(a,b,c){return a+"/"+b+"/"+c};k.jf=function(){return this.ia};k.Na=function(){return this.tileGrid};k.eb=function(a){return this.tileGrid?this.tileGrid:vf(a)};k.pd=function(a){var b=this.f;return b&&!Oc(b,a)?null:this.a};
+k.bc=function(){return this.ta};k.$d=function(a,b,c){c=this.eb(c);b=this.bc(b);a=hf(c.Ja(a),this.o);return 1==b?a:gf(a,b,this.o)};function Cf(a,b,c){var d=void 0!==c?c:a.f;c=a.eb(d);if(a.D&&d.g){var e=b;b=e[0];a=uf(c,e);d=wf(d);Sb(d,a)?b=e:(e=ic(d),a[0]+=e*Math.ceil((d[0]-a[0])/e),b=c.qd(a,b))}e=b[0];d=b[1];a=b[2];if(c.minZoom>e||e>c.maxZoom)c=!1;else{var f=c.H();c=(c=f?pf(c,f,e):c.a?c.a[e]:null)?ge(c,d,a):!0}return c?b:null}k.sa=function(){this.a.clear();this.u()};k.Yf=na;
+function Df(a,b){Wa.call(this,a);this.tile=b}y(Df,Wa);function Ef(a){a=a?a:{};this.R=document.createElement("UL");this.v=document.createElement("LI");this.R.appendChild(this.v);this.v.style.display="none";this.f=void 0!==a.collapsed?a.collapsed:!0;this.o=void 0!==a.collapsible?a.collapsible:!0;this.o||(this.f=!1);var b=void 0!==a.className?a.className:"ol-attribution",c=void 0!==a.tipLabel?a.tipLabel:"Attributions",d=void 0!==a.collapseLabel?a.collapseLabel:"\u00bb";"string"===typeof d?(this.A=document.createElement("span"),this.A.textContent=d):this.A=
+d;d=void 0!==a.label?a.label:"i";"string"===typeof d?(this.C=document.createElement("span"),this.C.textContent=d):this.C=d;var e=this.o&&!this.f?this.A:this.C,d=document.createElement("button");d.setAttribute("type","button");d.title=c;d.appendChild(e);B(d,"click",this.am,this);c=document.createElement("div");c.className=b+" ol-unselectable ol-control"+(this.f&&this.o?" ol-collapsed":"")+(this.o?"":" ol-uncollapsible");c.appendChild(this.R);c.appendChild(d);Xe.call(this,{element:c,render:a.render?
+a.render:Ff,target:a.target});this.D=!0;this.j={};this.l={};this.T={}}y(Ef,Xe);
+function Ff(a){if(a=a.frameState){var b,c,d,e,f,g,h,l,m,n,p,q=a.layerStatesArray,r=Ea({},a.attributions),u={},x=a.viewState.projection;c=0;for(b=q.length;c<b;c++)if(g=q[c].layer.ha())if(n=w(g).toString(),m=g.l)for(d=0,e=m.length;d<e;d++)if(h=m[d],l=w(h).toString(),!(l in r)){if(f=a.usedTiles[n]){var v=g.eb(x);a:{p=h;var D=x;if(p.b){var A,z,F,N=void 0;for(N in f)if(N in p.b){F=f[N];var K;A=0;for(z=p.b[N].length;A<z;++A){K=p.b[N][A];if(ie(K,F)){p=!0;break a}var X=pf(v,wf(D),parseInt(N,10)),oa=X.ea-
+X.ca+1;if(F.ca<X.ca||F.ea>X.ea)if(ie(K,new fe(xa(F.ca,oa),xa(F.ea,oa),F.fa,F.ga))||F.ea-F.ca+1>oa&&ie(K,X)){p=!0;break a}}}p=!1}else p=!0}}else p=!1;p?(l in u&&delete u[l],r[l]=h):u[l]=h}b=[r,u];c=b[0];b=b[1];for(var H in this.j)H in c?(this.l[H]||(this.j[H].style.display="",this.l[H]=!0),delete c[H]):H in b?(this.l[H]&&(this.j[H].style.display="none",delete this.l[H]),delete b[H]):(Ue(this.j[H]),delete this.j[H],delete this.l[H]);for(H in c)d=document.createElement("LI"),d.innerHTML=c[H].a,this.R.appendChild(d),
+this.j[H]=d,this.l[H]=!0;for(H in b)d=document.createElement("LI"),d.innerHTML=b[H].a,d.style.display="none",this.R.appendChild(d),this.j[H]=d;H=!Ha(this.l)||!Ha(a.logos);this.D!=H&&(this.element.style.display=H?"":"none",this.D=H);H&&Ha(this.l)?this.element.classList.add("ol-logo-only"):this.element.classList.remove("ol-logo-only");var ya;a=a.logos;H=this.T;for(ya in H)ya in a||(Ue(H[ya]),delete H[ya]);for(var Ua in a)b=a[Ua],b instanceof HTMLElement&&(this.v.appendChild(b),H[Ua]=b),Ua in H||(ya=
+new Image,ya.src=Ua,""===b?c=ya:(c=document.createElement("a"),c.href=b,c.appendChild(ya)),this.v.appendChild(c),H[Ua]=c);this.v.style.display=Ha(a)?"none":""}else this.D&&(this.element.style.display="none",this.D=!1)}k=Ef.prototype;k.am=function(a){a.preventDefault();Gf(this)};function Gf(a){a.element.classList.toggle("ol-collapsed");a.f?Te(a.A,a.C):Te(a.C,a.A);a.f=!a.f}k.$l=function(){return this.o};
+k.cm=function(a){this.o!==a&&(this.o=a,this.element.classList.toggle("ol-uncollapsible"),!a&&this.f&&Gf(this))};k.bm=function(a){this.o&&this.f!==a&&Gf(this)};k.Zl=function(){return this.f};function Hf(a){a=a?a:{};var b=void 0!==a.className?a.className:"ol-rotate",c=void 0!==a.label?a.label:"\u21e7";this.f=null;"string"===typeof c?(this.f=document.createElement("span"),this.f.className="ol-compass",this.f.textContent=c):(this.f=c,this.f.classList.add("ol-compass"));var d=a.tipLabel?a.tipLabel:"Reset rotation",c=document.createElement("button");c.className=b+"-reset";c.setAttribute("type","button");c.title=d;c.appendChild(this.f);B(c,"click",Hf.prototype.A,this);d=document.createElement("div");
+d.className=b+" ol-unselectable ol-control";d.appendChild(c);b=a.render?a.render:If;this.o=a.resetNorth?a.resetNorth:void 0;Xe.call(this,{element:d,render:b,target:a.target});this.j=void 0!==a.duration?a.duration:250;this.l=void 0!==a.autoHide?a.autoHide:!0;this.v=void 0;this.l&&this.element.classList.add("ol-hidden")}y(Hf,Xe);
+Hf.prototype.A=function(a){a.preventDefault();if(void 0!==this.o)this.o();else{a=this.a;var b=a.aa();if(b){var c=b.La();void 0!==c&&(0<this.j&&(c%=2*Math.PI,c<-Math.PI&&(c+=2*Math.PI),c>Math.PI&&(c-=2*Math.PI),a.Wa(de({rotation:c,duration:this.j,easing:Zd}))),b.ie(0))}}};
+function If(a){if(a=a.frameState){a=a.viewState.rotation;if(a!=this.v){var b="rotate("+a+"rad)";if(this.l){var c=this.element.classList.contains("ol-hidden");c||0!==a?c&&0!==a&&this.element.classList.remove("ol-hidden"):this.element.classList.add("ol-hidden")}this.f.style.msTransform=b;this.f.style.webkitTransform=b;this.f.style.transform=b}this.v=a}};function Jf(a){a=a?a:{};var b=void 0!==a.className?a.className:"ol-zoom",c=void 0!==a.delta?a.delta:1,d=void 0!==a.zoomInLabel?a.zoomInLabel:"+",e=void 0!==a.zoomOutLabel?a.zoomOutLabel:"\u2212",f=void 0!==a.zoomInTipLabel?a.zoomInTipLabel:"Zoom in",g=void 0!==a.zoomOutTipLabel?a.zoomOutTipLabel:"Zoom out",h=document.createElement("button");h.className=b+"-in";h.setAttribute("type","button");h.title=f;h.appendChild("string"===typeof d?document.createTextNode(d):d);B(h,"click",Jf.prototype.l.bind(this,
+c));d=document.createElement("button");d.className=b+"-out";d.setAttribute("type","button");d.title=g;d.appendChild("string"===typeof e?document.createTextNode(e):e);B(d,"click",Jf.prototype.l.bind(this,-c));c=document.createElement("div");c.className=b+" ol-unselectable ol-control";c.appendChild(h);c.appendChild(d);Xe.call(this,{element:c,target:a.target});this.f=void 0!==a.duration?a.duration:250}y(Jf,Xe);
+Jf.prototype.l=function(a,b){b.preventDefault();var c=this.a,d=c.aa();if(d){var e=d.$();e&&(0<this.f&&c.Wa(ee({resolution:e,duration:this.f,easing:Zd})),c=d.constrainResolution(e,a),d.Ub(c))}};function Kf(a){a=a?a:{};var b=new le;(void 0!==a.zoom?a.zoom:1)&&b.push(new Jf(a.zoomOptions));(void 0!==a.rotate?a.rotate:1)&&b.push(new Hf(a.rotateOptions));(void 0!==a.attribution?a.attribution:1)&&b.push(new Ef(a.attributionOptions));return b};function Lf(a){a=a?a:{};this.f=void 0!==a.className?a.className:"ol-full-screen";var b=void 0!==a.label?a.label:"\u2922";this.o="string"===typeof b?document.createTextNode(b):b;b=void 0!==a.labelActive?a.labelActive:"\u00d7";this.j="string"===typeof b?document.createTextNode(b):b;var c=a.tipLabel?a.tipLabel:"Toggle full-screen",b=document.createElement("button");b.className=this.f+"-"+Mf();b.setAttribute("type","button");b.title=c;b.appendChild(this.o);B(b,"click",this.C,this);c=document.createElement("div");
+c.className=this.f+" ol-unselectable ol-control "+(Nf()?"":"ol-unsupported");c.appendChild(b);Xe.call(this,{element:c,target:a.target});this.A=void 0!==a.keys?a.keys:!1;this.l=a.source}y(Lf,Xe);
+Lf.prototype.C=function(a){a.preventDefault();Nf()&&(a=this.a)&&(Mf()?document.exitFullscreen?document.exitFullscreen():document.msExitFullscreen?document.msExitFullscreen():document.mozCancelFullScreen?document.mozCancelFullScreen():document.webkitExitFullscreen&&document.webkitExitFullscreen():(a=this.l?"string"===typeof this.l?document.getElementById(this.l):this.l:a.yc(),this.A?a.mozRequestFullScreenWithKeys?a.mozRequestFullScreenWithKeys():a.webkitRequestFullscreen?a.webkitRequestFullscreen(Element.ALLOW_KEYBOARD_INPUT):
+Of(a):Of(a)))};Lf.prototype.v=function(){var a=this.element.firstElementChild,b=this.a;Mf()?(a.className=this.f+"-true",Te(this.j,this.o)):(a.className=this.f+"-false",Te(this.o,this.j));b&&b.Xc()};Lf.prototype.setMap=function(a){Xe.prototype.setMap.call(this,a);a&&this.s.push(B(pa.document,Pf(),this.v,this))};
+function Nf(){var a=document.body;return!!(a.webkitRequestFullscreen||a.mozRequestFullScreen&&document.mozFullScreenEnabled||a.msRequestFullscreen&&document.msFullscreenEnabled||a.requestFullscreen&&document.fullscreenEnabled)}function Mf(){return!!(document.webkitIsFullScreen||document.mozFullScreen||document.msFullscreenElement||document.fullscreenElement)}
+function Of(a){a.requestFullscreen?a.requestFullscreen():a.msRequestFullscreen?a.msRequestFullscreen():a.mozRequestFullScreen?a.mozRequestFullScreen():a.webkitRequestFullscreen&&a.webkitRequestFullscreen()}var Pf=function(){var a;return function(){if(!a){var b=document.body;b.webkitRequestFullscreen?a="webkitfullscreenchange":b.mozRequestFullScreen?a="mozfullscreenchange":b.msRequestFullscreen?a="MSFullscreenChange":b.requestFullscreen&&(a="fullscreenchange")}return a}}();function Qf(a){a=a?a:{};var b=document.createElement("DIV");b.className=void 0!==a.className?a.className:"ol-mouse-position";Xe.call(this,{element:b,render:a.render?a.render:Rf,target:a.target});B(this,gb("projection"),this.dm,this);a.coordinateFormat&&this.ei(a.coordinateFormat);a.projection&&this.ih(yc(a.projection));this.v=void 0!==a.undefinedHTML?a.undefinedHTML:"";this.j=b.innerHTML;this.o=this.l=this.f=null}y(Qf,Xe);
+function Rf(a){a=a.frameState;a?this.f!=a.viewState.projection&&(this.f=a.viewState.projection,this.l=null):this.f=null;Sf(this,this.o)}k=Qf.prototype;k.dm=function(){this.l=null};k.Cg=function(){return this.get("coordinateFormat")};k.hh=function(){return this.get("projection")};k.Xk=function(a){this.o=this.a.Td(a);Sf(this,this.o)};k.Yk=function(){Sf(this,null);this.o=null};
+k.setMap=function(a){Xe.prototype.setMap.call(this,a);a&&(a=a.a,this.s.push(B(a,"mousemove",this.Xk,this),B(a,"mouseout",this.Yk,this)))};k.ei=function(a){this.set("coordinateFormat",a)};k.ih=function(a){this.set("projection",a)};function Sf(a,b){var c=a.v;if(b&&a.f){if(!a.l){var d=a.hh();a.l=d?Bc(a.f,d):Qc}if(d=a.a.Ma(b))a.l(d,d),c=(c=a.Cg())?c(d):d.toString()}a.j&&c==a.j||(a.element.innerHTML=c,a.j=c)};function Tf(a,b){var c=a;b&&(c=ka(a,b));"function"!=ca(aa.setImmediate)||aa.Window&&aa.Window.prototype&&!Be("Edge")&&aa.Window.prototype.setImmediate==aa.setImmediate?(Uf||(Uf=Vf()),Uf(c)):aa.setImmediate(c)}var Uf;
+function Vf(){var a=aa.MessageChannel;"undefined"===typeof a&&"undefined"!==typeof window&&window.postMessage&&window.addEventListener&&!Be("Presto")&&(a=function(){var a=document.createElement("IFRAME");a.style.display="none";a.src="";document.documentElement.appendChild(a);var b=a.contentWindow,a=b.document;a.open();a.write("");a.close();var c="callImmediate"+Math.random(),d="file:"==b.location.protocol?"*":b.location.protocol+"//"+b.location.host,a=ka(function(a){if(("*"==d||a.origin==d)&&a.data==
+c)this.port1.onmessage()},this);b.addEventListener("message",a,!1);this.port1={};this.port2={postMessage:function(){b.postMessage(c,d)}}});if("undefined"!==typeof a&&!Be("Trident")&&!Be("MSIE")){var b=new a,c={},d=c;b.port1.onmessage=function(){if(void 0!==c.next){c=c.next;var a=c.rg;c.rg=null;a()}};return function(a){d.next={rg:a};d=d.next;b.port2.postMessage(0)}}return"undefined"!==typeof document&&"onreadystatechange"in document.createElement("SCRIPT")?function(a){var b=document.createElement("SCRIPT");
+b.onreadystatechange=function(){b.onreadystatechange=null;b.parentNode.removeChild(b);b=null;a();a=null};document.documentElement.appendChild(b)}:function(a){aa.setTimeout(a,0)}};function Wf(a,b,c){Wa.call(this,a);this.b=b;a=c?c:{};this.buttons=Xf(a);this.pressure=Yf(a,this.buttons);this.bubbles="bubbles"in a?a.bubbles:!1;this.cancelable="cancelable"in a?a.cancelable:!1;this.view="view"in a?a.view:null;this.detail="detail"in a?a.detail:null;this.screenX="screenX"in a?a.screenX:0;this.screenY="screenY"in a?a.screenY:0;this.clientX="clientX"in a?a.clientX:0;this.clientY="clientY"in a?a.clientY:0;this.button="button"in a?a.button:0;this.relatedTarget="relatedTarget"in a?a.relatedTarget:
+null;this.pointerId="pointerId"in a?a.pointerId:0;this.width="width"in a?a.width:0;this.height="height"in a?a.height:0;this.pointerType="pointerType"in a?a.pointerType:"";this.isPrimary="isPrimary"in a?a.isPrimary:!1;b.preventDefault&&(this.preventDefault=function(){b.preventDefault()})}y(Wf,Wa);function Xf(a){if(a.buttons||Zf)a=a.buttons;else switch(a.which){case 1:a=1;break;case 2:a=4;break;case 3:a=2;break;default:a=0}return a}
+function Yf(a,b){var c=0;a.pressure?c=a.pressure:c=b?.5:0;return c}var Zf=!1;try{Zf=1===(new MouseEvent("click",{buttons:1})).buttons}catch(a){};var $f=["experimental-webgl","webgl","webkit-3d","moz-webgl"];function ag(a,b){var c,d,e=$f.length;for(d=0;d<e;++d)try{if(c=a.getContext($f[d],b))return c}catch(f){}return null};var bg,cg="undefined"!==typeof navigator?navigator.userAgent.toLowerCase():"",dg=-1!==cg.indexOf("firefox"),eg=-1!==cg.indexOf("safari")&&-1===cg.indexOf("chrom"),fg=-1!==cg.indexOf("macintosh"),gg=pa.devicePixelRatio||1,hg=!1,ig=function(){if(!("HTMLCanvasElement"in pa))return!1;try{var a=Oe();return a?(a.setLineDash&&(hg=!0),!0):!1}catch(b){return!1}}(),jg="DeviceOrientationEvent"in pa,kg="geolocation"in pa.navigator,lg="ontouchstart"in pa,mg="PointerEvent"in pa,ng=!!pa.navigator.msPointerEnabled,
+og=!1,pg,qg=[];if("WebGLRenderingContext"in pa)try{var rg=ag(document.createElement("CANVAS"),{failIfMajorPerformanceCaveat:!0});rg&&(og=!0,pg=rg.getParameter(rg.MAX_TEXTURE_SIZE),qg=rg.getSupportedExtensions())}catch(a){}bg=og;ma=qg;la=pg;function sg(a,b){this.b=a;this.c=b};function tg(a){sg.call(this,a,{mousedown:this.rl,mousemove:this.sl,mouseup:this.vl,mouseover:this.ul,mouseout:this.tl});this.a=a.g;this.g=[]}y(tg,sg);function ug(a,b){for(var c=a.g,d=b.clientX,e=b.clientY,f=0,g=c.length,h;f<g&&(h=c[f]);f++){var l=Math.abs(e-h[1]);if(25>=Math.abs(d-h[0])&&25>=l)return!0}return!1}function vg(a){var b=wg(a,a),c=b.preventDefault;b.preventDefault=function(){a.preventDefault();c()};b.pointerId=1;b.isPrimary=!0;b.pointerType="mouse";return b}k=tg.prototype;
+k.rl=function(a){if(!ug(this,a)){if((1).toString()in this.a){var b=vg(a);xg(this.b,yg,b,a);delete this.a[(1).toString()]}b=vg(a);this.a[(1).toString()]=a;xg(this.b,zg,b,a)}};k.sl=function(a){if(!ug(this,a)){var b=vg(a);xg(this.b,Ag,b,a)}};k.vl=function(a){if(!ug(this,a)){var b=this.a[(1).toString()];b&&b.button===a.button&&(b=vg(a),xg(this.b,Bg,b,a),delete this.a[(1).toString()])}};k.ul=function(a){if(!ug(this,a)){var b=vg(a);Cg(this.b,b,a)}};
+k.tl=function(a){if(!ug(this,a)){var b=vg(a);Dg(this.b,b,a)}};function Eg(a){sg.call(this,a,{MSPointerDown:this.Al,MSPointerMove:this.Bl,MSPointerUp:this.El,MSPointerOut:this.Cl,MSPointerOver:this.Dl,MSPointerCancel:this.zl,MSGotPointerCapture:this.xl,MSLostPointerCapture:this.yl});this.a=a.g;this.g=["","unavailable","touch","pen","mouse"]}y(Eg,sg);function Fg(a,b){var c=b;ea(b.pointerType)&&(c=wg(b,b),c.pointerType=a.g[b.pointerType]);return c}k=Eg.prototype;k.Al=function(a){this.a[a.pointerId.toString()]=a;var b=Fg(this,a);xg(this.b,zg,b,a)};
+k.Bl=function(a){var b=Fg(this,a);xg(this.b,Ag,b,a)};k.El=function(a){var b=Fg(this,a);xg(this.b,Bg,b,a);delete this.a[a.pointerId.toString()]};k.Cl=function(a){var b=Fg(this,a);Dg(this.b,b,a)};k.Dl=function(a){var b=Fg(this,a);Cg(this.b,b,a)};k.zl=function(a){var b=Fg(this,a);xg(this.b,yg,b,a);delete this.a[a.pointerId.toString()]};k.yl=function(a){this.b.b(new Wf("lostpointercapture",a,a))};k.xl=function(a){this.b.b(new Wf("gotpointercapture",a,a))};function Gg(a){sg.call(this,a,{pointerdown:this.lo,pointermove:this.mo,pointerup:this.po,pointerout:this.no,pointerover:this.oo,pointercancel:this.ko,gotpointercapture:this.Gk,lostpointercapture:this.ql})}y(Gg,sg);k=Gg.prototype;k.lo=function(a){Hg(this.b,a)};k.mo=function(a){Hg(this.b,a)};k.po=function(a){Hg(this.b,a)};k.no=function(a){Hg(this.b,a)};k.oo=function(a){Hg(this.b,a)};k.ko=function(a){Hg(this.b,a)};k.ql=function(a){Hg(this.b,a)};k.Gk=function(a){Hg(this.b,a)};function Ig(a,b){sg.call(this,a,{touchstart:this.sp,touchmove:this.rp,touchend:this.qp,touchcancel:this.pp});this.a=a.g;this.l=b;this.g=void 0;this.i=0;this.f=void 0}y(Ig,sg);k=Ig.prototype;k.ci=function(){this.i=0;this.f=void 0};
+function Jg(a,b,c){b=wg(b,c);b.pointerId=c.identifier+2;b.bubbles=!0;b.cancelable=!0;b.detail=a.i;b.button=0;b.buttons=1;b.width=c.webkitRadiusX||c.radiusX||0;b.height=c.webkitRadiusY||c.radiusY||0;b.pressure=c.webkitForce||c.force||.5;b.isPrimary=a.g===c.identifier;b.pointerType="touch";b.clientX=c.clientX;b.clientY=c.clientY;b.screenX=c.screenX;b.screenY=c.screenY;return b}
+function Kg(a,b,c){function d(){b.preventDefault()}var e=Array.prototype.slice.call(b.changedTouches),f=e.length,g,h;for(g=0;g<f;++g)h=Jg(a,b,e[g]),h.preventDefault=d,c.call(a,b,h)}
+k.sp=function(a){var b=a.touches,c=Object.keys(this.a),d=c.length;if(d>=b.length){var e=[],f,g,h;for(f=0;f<d;++f){g=c[f];h=this.a[g];var l;if(!(l=1==g))a:{l=b.length;for(var m,n=0;n<l;n++)if(m=b[n],m.identifier===g-2){l=!0;break a}l=!1}l||e.push(h.out)}for(f=0;f<e.length;++f)this.Ue(a,e[f])}b=a.changedTouches[0];c=Object.keys(this.a).length;if(0===c||1===c&&(1).toString()in this.a)this.g=b.identifier,void 0!==this.f&&pa.clearTimeout(this.f);Lg(this,a);this.i++;Kg(this,a,this.fo)};
+k.fo=function(a,b){this.a[b.pointerId]={target:b.target,out:b,Lh:b.target};var c=this.b;b.bubbles=!0;xg(c,Mg,b,a);c=this.b;b.bubbles=!1;xg(c,Ng,b,a);xg(this.b,zg,b,a)};k.rp=function(a){a.preventDefault();Kg(this,a,this.wl)};k.wl=function(a,b){var c=this.a[b.pointerId];if(c){var d=c.out,e=c.Lh;xg(this.b,Ag,b,a);d&&e!==b.target&&(d.relatedTarget=b.target,b.relatedTarget=e,d.target=e,b.target?(Dg(this.b,d,a),Cg(this.b,b,a)):(b.target=e,b.relatedTarget=null,this.Ue(a,b)));c.out=b;c.Lh=b.target}};
+k.qp=function(a){Lg(this,a);Kg(this,a,this.tp)};k.tp=function(a,b){xg(this.b,Bg,b,a);this.b.out(b,a);var c=this.b;b.bubbles=!1;xg(c,Og,b,a);delete this.a[b.pointerId];b.isPrimary&&(this.g=void 0,this.f=pa.setTimeout(this.ci.bind(this),200))};k.pp=function(a){Kg(this,a,this.Ue)};k.Ue=function(a,b){xg(this.b,yg,b,a);this.b.out(b,a);var c=this.b;b.bubbles=!1;xg(c,Og,b,a);delete this.a[b.pointerId];b.isPrimary&&(this.g=void 0,this.f=pa.setTimeout(this.ci.bind(this),200))};
+function Lg(a,b){var c=a.l.g,d=b.changedTouches[0];if(a.g===d.identifier){var e=[d.clientX,d.clientY];c.push(e);pa.setTimeout(function(){nb(c,e)},2500)}};function Pg(a){$a.call(this);this.i=a;this.g={};this.c={};this.a=[];mg?Qg(this,new Gg(this)):ng?Qg(this,new Eg(this)):(a=new tg(this),Qg(this,a),lg&&Qg(this,new Ig(this,a)));a=this.a.length;for(var b,c=0;c<a;c++)b=this.a[c],Rg(this,Object.keys(b.c))}y(Pg,$a);function Qg(a,b){var c=Object.keys(b.c);c&&(c.forEach(function(a){var c=b.c[a];c&&(this.c[a]=c.bind(b))},a),a.a.push(b))}Pg.prototype.f=function(a){var b=this.c[a.type];b&&b(a)};
+function Rg(a,b){b.forEach(function(a){B(this.i,a,this.f,this)},a)}function Sg(a,b){b.forEach(function(a){Qa(this.i,a,this.f,this)},a)}function wg(a,b){for(var c={},d,e=0,f=Tg.length;e<f;e++)d=Tg[e][0],c[d]=a[d]||b[d]||Tg[e][1];return c}Pg.prototype.out=function(a,b){a.bubbles=!0;xg(this,Ug,a,b)};function Dg(a,b,c){a.out(b,c);var d=b.target,e=b.relatedTarget;d&&e&&d.contains(e)||(b.bubbles=!1,xg(a,Og,b,c))}
+function Cg(a,b,c){b.bubbles=!0;xg(a,Mg,b,c);var d=b.target,e=b.relatedTarget;d&&e&&d.contains(e)||(b.bubbles=!1,xg(a,Ng,b,c))}function xg(a,b,c,d){a.b(new Wf(b,d,c))}function Hg(a,b){a.b(new Wf(b.type,b,b))}Pg.prototype.ka=function(){for(var a=this.a.length,b,c=0;c<a;c++)b=this.a[c],Sg(this,Object.keys(b.c));$a.prototype.ka.call(this)};
+var Ag="pointermove",zg="pointerdown",Bg="pointerup",Mg="pointerover",Ug="pointerout",Ng="pointerenter",Og="pointerleave",yg="pointercancel",Tg=[["bubbles",!1],["cancelable",!1],["view",null],["detail",null],["screenX",0],["screenY",0],["clientX",0],["clientY",0],["ctrlKey",!1],["altKey",!1],["shiftKey",!1],["metaKey",!1],["button",0],["relatedTarget",null],["buttons",0],["pointerId",0],["width",0],["height",0],["pressure",0],["tiltX",0],["tiltY",0],["pointerType",""],["hwTimestamp",0],["isPrimary",
+!1],["type",""],["target",null],["currentTarget",null],["which",0]];function Vg(a,b,c,d,e){We.call(this,a,b,e);this.originalEvent=c;this.pixel=b.Td(c);this.coordinate=b.Ma(this.pixel);this.dragging=void 0!==d?d:!1}y(Vg,We);Vg.prototype.preventDefault=function(){We.prototype.preventDefault.call(this);this.originalEvent.preventDefault()};Vg.prototype.stopPropagation=function(){We.prototype.stopPropagation.call(this);this.originalEvent.stopPropagation()};function Wg(a,b,c,d,e){Vg.call(this,a,b,c.b,d,e);this.b=c}y(Wg,Vg);
+function Xg(a){$a.call(this);this.f=a;this.l=0;this.o=!1;this.c=[];this.g=null;a=this.f.a;this.U=0;this.v={};this.i=new Pg(a);this.a=null;this.j=B(this.i,zg,this.$k,this);this.s=B(this.i,Ag,this.No,this)}y(Xg,$a);function Yg(a,b){var c;c=new Wg(Zg,a.f,b);a.b(c);0!==a.l?(pa.clearTimeout(a.l),a.l=0,c=new Wg($g,a.f,b),a.b(c)):a.l=pa.setTimeout(function(){this.l=0;var a=new Wg(ah,this.f,b);this.b(a)}.bind(a),250)}
+function bh(a,b){b.type==ch||b.type==dh?delete a.v[b.pointerId]:b.type==eh&&(a.v[b.pointerId]=!0);a.U=Object.keys(a.v).length}k=Xg.prototype;k.Qg=function(a){bh(this,a);var b=new Wg(ch,this.f,a);this.b(b);!this.o&&0===a.button&&Yg(this,this.g);0===this.U&&(this.c.forEach(Ka),this.c.length=0,this.o=!1,this.g=null,Ta(this.a),this.a=null)};
+k.$k=function(a){bh(this,a);var b=new Wg(eh,this.f,a);this.b(b);this.g=a;0===this.c.length&&(this.a=new Pg(document),this.c.push(B(this.a,fh,this.Sl,this),B(this.a,ch,this.Qg,this),B(this.i,dh,this.Qg,this)))};k.Sl=function(a){if(a.clientX!=this.g.clientX||a.clientY!=this.g.clientY){this.o=!0;var b=new Wg(gh,this.f,a,this.o);this.b(b)}a.preventDefault()};k.No=function(a){this.b(new Wg(a.type,this.f,a,!(!this.g||a.clientX==this.g.clientX&&a.clientY==this.g.clientY)))};
+k.ka=function(){this.s&&(Ka(this.s),this.s=null);this.j&&(Ka(this.j),this.j=null);this.c.forEach(Ka);this.c.length=0;this.a&&(Ta(this.a),this.a=null);this.i&&(Ta(this.i),this.i=null);$a.prototype.ka.call(this)};var ah="singleclick",Zg="click",$g="dblclick",gh="pointerdrag",fh="pointermove",eh="pointerdown",ch="pointerup",dh="pointercancel",hh={Mp:ah,Bp:Zg,Cp:$g,Fp:gh,Ip:fh,Ep:eh,Lp:ch,Kp:"pointerover",Jp:"pointerout",Gp:"pointerenter",Hp:"pointerleave",Dp:dh};function ih(a){eb.call(this);var b=Ea({},a);b.opacity=void 0!==a.opacity?a.opacity:1;b.visible=void 0!==a.visible?a.visible:!0;b.zIndex=void 0!==a.zIndex?a.zIndex:0;b.maxResolution=void 0!==a.maxResolution?a.maxResolution:Infinity;b.minResolution=void 0!==a.minResolution?a.minResolution:0;this.G(b)}y(ih,eb);
+function jh(a){var b=a.Pb(),c=a.kf(),d=a.xb(),e=a.H(),f=a.Qb(),g=a.Nb(),h=a.Ob();return{layer:a,opacity:sa(b,0,1),R:c,visible:d,Qc:!0,extent:e,zIndex:f,maxResolution:g,minResolution:Math.max(h,0)}}k=ih.prototype;k.H=function(){return this.get("extent")};k.Nb=function(){return this.get("maxResolution")};k.Ob=function(){return this.get("minResolution")};k.Pb=function(){return this.get("opacity")};k.xb=function(){return this.get("visible")};k.Qb=function(){return this.get("zIndex")};
+k.fc=function(a){this.set("extent",a)};k.nc=function(a){this.set("maxResolution",a)};k.oc=function(a){this.set("minResolution",a)};k.gc=function(a){this.set("opacity",a)};k.hc=function(a){this.set("visible",a)};k.ic=function(a){this.set("zIndex",a)};function kh(){};function lh(a,b,c,d,e,f){Wa.call(this,a,b);this.vectorContext=c;this.frameState=d;this.context=e;this.glContext=f}y(lh,Wa);function mh(a){var b=Ea({},a);delete b.source;ih.call(this,b);this.v=this.j=this.o=null;a.map&&this.setMap(a.map);B(this,gb("source"),this.fl,this);this.Fc(a.source?a.source:null)}y(mh,ih);function nh(a,b){return a.visible&&b>=a.minResolution&&b<a.maxResolution}k=mh.prototype;k.hf=function(a){a=a?a:[];a.push(jh(this));return a};k.ha=function(){return this.get("source")||null};k.kf=function(){var a=this.ha();return a?a.V():"undefined"};k.Lm=function(){this.u()};
+k.fl=function(){this.v&&(Ka(this.v),this.v=null);var a=this.ha();a&&(this.v=B(a,"change",this.Lm,this));this.u()};k.setMap=function(a){this.o&&(Ka(this.o),this.o=null);a||this.u();this.j&&(Ka(this.j),this.j=null);a&&(this.o=B(a,"precompose",function(a){var c=jh(this);c.Qc=!1;c.zIndex=Infinity;a.frameState.layerStatesArray.push(c);a.frameState.layerStates[w(this)]=c},this),this.j=B(this,"change",a.render,a),this.u())};k.Fc=function(a){this.set("source",a)};function oh(a,b,c,d,e){$a.call(this);this.l=e;this.extent=a;this.f=c;this.resolution=b;this.state=d}y(oh,$a);function ph(a){a.b("change")}oh.prototype.H=function(){return this.extent};oh.prototype.$=function(){return this.resolution};oh.prototype.V=function(){return this.state};function qh(a,b,c,d,e,f,g,h){ad(a);0===b&&0===c||dd(a,b,c);1==d&&1==e||ed(a,d,e);0!==f&&fd(a,f);0===g&&0===h||dd(a,g,h);return a}function rh(a,b){return a[0]==b[0]&&a[1]==b[1]&&a[4]==b[4]&&a[5]==b[5]&&a[12]==b[12]&&a[13]==b[13]}function sh(a,b,c){var d=a[1],e=a[5],f=a[13],g=b[0];b=b[1];c[0]=a[0]*g+a[4]*b+a[12];c[1]=d*g+e*b+f;return c};function th(a){bb.call(this);this.a=a}y(th,bb);k=th.prototype;k.ra=na;k.Cc=function(a,b,c,d){a=a.slice();sh(b.pixelToCoordinateMatrix,a,a);if(this.ra(a,b,qc,this))return c.call(d,this.a)};k.le=rc;k.Qd=function(a,b,c){return function(d,e){return Af(a,b,d,e,function(a){c[d]||(c[d]={});c[d][a.ma.toString()]=a})}};k.Om=function(a){2===a.target.V()&&uh(this)};function vh(a,b){var c=b.V();2!=c&&3!=c&&B(b,"change",a.Om,a);0==c&&(b.load(),c=b.V());return 2==c}
+function uh(a){var b=a.a;b.xb()&&"ready"==b.kf()&&a.u()}function wh(a,b){b.Ah()&&a.postRenderFunctions.push(function(a,b,e){b=w(a).toString();a.Lc(e.viewState.projection,e.usedTiles[b])}.bind(null,b))}function xh(a,b){if(b){var c,d,e;d=0;for(e=b.length;d<e;++d)c=b[d],a[w(c).toString()]=c}}function yh(a,b){var c=b.R;void 0!==c&&("string"===typeof c?a.logos[c]="":fa(c)&&(a.logos[c.src]=c.href))}
+function zh(a,b,c,d){b=w(b).toString();c=c.toString();b in a?c in a[b]?(a=a[b][c],d.ca<a.ca&&(a.ca=d.ca),d.ea>a.ea&&(a.ea=d.ea),d.fa<a.fa&&(a.fa=d.fa),d.ga>a.ga&&(a.ga=d.ga)):a[b][c]=d:(a[b]={},a[b][c]=d)}function Ah(a,b,c){return[b*(Math.round(a[0]/b)+c[0]%2/2),b*(Math.round(a[1]/b)+c[1]%2/2)]}
+function Bh(a,b,c,d,e,f,g,h,l,m){var n=w(b).toString();n in a.wantedTiles||(a.wantedTiles[n]={});var p=a.wantedTiles[n];a=a.tileQueue;var q=c.minZoom,r,u,x,v,D,A;for(A=g;A>=q;--A)for(u=pf(c,f,A,u),x=c.$(A),v=u.ca;v<=u.ea;++v)for(D=u.fa;D<=u.ga;++D)g-A<=h?(r=b.ac(A,v,D,d,e),0==r.V()&&(p[r.ma.toString()]=!0,r.ib()in a.g||a.f([r,n,uf(c,r.ma),x])),void 0!==l&&l.call(m,r)):b.Yf(A,v,D,e)};function Ch(a){this.v=a.opacity;this.U=a.rotateWithView;this.j=a.rotation;this.i=a.scale;this.C=a.snapToPixel}k=Ch.prototype;k.qe=function(){return this.v};k.Xd=function(){return this.U};k.re=function(){return this.j};k.se=function(){return this.i};k.Yd=function(){return this.C};k.te=function(a){this.v=a};k.ue=function(a){this.j=a};k.ve=function(a){this.i=a};function Dh(a){a=a||{};this.c=void 0!==a.anchor?a.anchor:[.5,.5];this.f=null;this.a=void 0!==a.anchorOrigin?a.anchorOrigin:"top-left";this.o=void 0!==a.anchorXUnits?a.anchorXUnits:"fraction";this.s=void 0!==a.anchorYUnits?a.anchorYUnits:"fraction";var b=void 0!==a.crossOrigin?a.crossOrigin:null,c=void 0!==a.img?a.img:null,d=void 0!==a.imgSize?a.imgSize:null,e=a.src;void 0!==e&&0!==e.length||!c||(e=c.src||w(c).toString());var f=void 0!==a.src?0:2,g=void 0!==a.color?te(a.color):null,h=Eh.Zb(),l=h.get(e,
+b,g);l||(l=new Fh(c,e,d,b,f,g),h.set(e,b,g,l));this.b=l;this.D=void 0!==a.offset?a.offset:[0,0];this.g=void 0!==a.offsetOrigin?a.offsetOrigin:"top-left";this.l=null;this.A=void 0!==a.size?a.size:null;Ch.call(this,{opacity:void 0!==a.opacity?a.opacity:1,rotation:void 0!==a.rotation?a.rotation:0,scale:void 0!==a.scale?a.scale:1,snapToPixel:void 0!==a.snapToPixel?a.snapToPixel:!0,rotateWithView:void 0!==a.rotateWithView?a.rotateWithView:!1})}y(Dh,Ch);k=Dh.prototype;
+k.Yb=function(){if(this.f)return this.f;var a=this.c,b=this.Fb();if("fraction"==this.o||"fraction"==this.s){if(!b)return null;a=this.c.slice();"fraction"==this.o&&(a[0]*=b[0]);"fraction"==this.s&&(a[1]*=b[1])}if("top-left"!=this.a){if(!b)return null;a===this.c&&(a=this.c.slice());if("top-right"==this.a||"bottom-right"==this.a)a[0]=-a[0]+b[0];if("bottom-left"==this.a||"bottom-right"==this.a)a[1]=-a[1]+b[1]}return this.f=a};k.jc=function(){var a=this.b;return a.c?a.c:a.a};k.ld=function(){return this.b.g};
+k.td=function(){return this.b.f};k.pe=function(){var a=this.b;if(!a.o)if(a.s){var b=a.g[0],c=a.g[1],d=Oe(b,c);d.fillRect(0,0,b,c);a.o=d.canvas}else a.o=a.a;return a.o};k.Ia=function(){if(this.l)return this.l;var a=this.D;if("top-left"!=this.g){var b=this.Fb(),c=this.b.g;if(!b||!c)return null;a=a.slice();if("top-right"==this.g||"bottom-right"==this.g)a[0]=c[0]-b[0]-a[0];if("bottom-left"==this.g||"bottom-right"==this.g)a[1]=c[1]-b[1]-a[1]}return this.l=a};k.En=function(){return this.b.j};
+k.Fb=function(){return this.A?this.A:this.b.g};k.pf=function(a,b){return B(this.b,"change",a,b)};k.load=function(){this.b.load()};k.Xf=function(a,b){Qa(this.b,"change",a,b)};function Fh(a,b,c,d,e,f){$a.call(this);this.o=null;this.a=a?a:new Image;null!==d&&(this.a.crossOrigin=d);this.c=f?document.createElement("CANVAS"):null;this.l=f;this.i=null;this.f=e;this.g=c;this.j=b;this.s=!1;2==this.f&&Gh(this)}y(Fh,$a);
+function Gh(a){var b=Oe(1,1);try{b.drawImage(a.a,0,0),b.getImageData(0,0,1,1)}catch(c){a.s=!0}}Fh.prototype.v=function(){this.f=3;this.i.forEach(Ka);this.i=null;this.b("change")};
+Fh.prototype.U=function(){this.f=2;this.g&&(this.a.width=this.g[0],this.a.height=this.g[1]);this.g=[this.a.width,this.a.height];this.i.forEach(Ka);this.i=null;Gh(this);if(!this.s&&null!==this.l){this.c.width=this.a.width;this.c.height=this.a.height;var a=this.c.getContext("2d");a.drawImage(this.a,0,0);for(var b=a.getImageData(0,0,this.a.width,this.a.height),c=b.data,d=this.l[0]/255,e=this.l[1]/255,f=this.l[2]/255,g=0,h=c.length;g<h;g+=4)c[g]*=d,c[g+1]*=e,c[g+2]*=f;a.putImageData(b,0,0)}this.b("change")};
+Fh.prototype.load=function(){if(0==this.f){this.f=1;this.i=[Pa(this.a,"error",this.v,this),Pa(this.a,"load",this.U,this)];try{this.a.src=this.j}catch(a){this.v()}}};function Eh(){this.b={};this.a=0}ba(Eh);Eh.prototype.clear=function(){this.b={};this.a=0};Eh.prototype.get=function(a,b,c){a=b+":"+a+":"+(c?ve(c):"null");return a in this.b?this.b[a]:null};Eh.prototype.set=function(a,b,c,d){this.b[b+":"+a+":"+(c?ve(c):"null")]=d;++this.a};function Hh(a,b){this.i=b;this.g={};this.s={}}y(Hh,Sa);function Ih(a){var b=a.viewState,c=a.coordinateToPixelMatrix;qh(c,a.size[0]/2,a.size[1]/2,1/b.resolution,-1/b.resolution,-b.rotation,-b.center[0],-b.center[1]);cd(c,a.pixelToCoordinateMatrix)}k=Hh.prototype;k.ka=function(){for(var a in this.g)Ta(this.g[a])};function Jh(){var a=Eh.Zb();if(32<a.a){var b=0,c,d;for(c in a.b)d=a.b[c],0!==(b++&3)||ab(d)||(delete a.b[c],--a.a)}}
+k.ra=function(a,b,c,d,e,f){function g(a,e){var f=w(a).toString(),g=b.layerStates[w(e)].Qc;if(!(f in b.skippedFeatureUids)||g)return c.call(d,a,g?e:null)}var h,l=b.viewState,m=l.resolution,n=l.projection,l=a;if(n.a){var n=n.H(),p=ic(n),q=a[0];if(q<n[0]||q>n[2])l=[q+p*Math.ceil((n[0]-q)/p),a[1]]}n=b.layerStatesArray;for(p=n.length-1;0<=p;--p){var r=n[p],q=r.layer;if(nh(r,m)&&e.call(f,q)&&(r=Kh(this,q),q.ha()&&(h=r.ra(q.ha().D?l:a,b,g,d)),h))return h}};
+k.rh=function(a,b,c,d,e,f){var g,h=b.viewState.resolution,l=b.layerStatesArray,m;for(m=l.length-1;0<=m;--m){g=l[m];var n=g.layer;if(nh(g,h)&&e.call(f,n)&&(g=Kh(this,n).Cc(a,b,c,d)))return g}};k.sh=function(a,b,c,d){return void 0!==this.ra(a,b,qc,this,c,d)};function Kh(a,b){var c=w(b).toString();if(c in a.g)return a.g[c];var d=a.Xe(b);a.g[c]=d;a.s[c]=B(d,"change",a.Rk,a);return d}k.Rk=function(){this.i.render()};k.Ce=na;
+k.To=function(a,b){for(var c in this.g)if(!(b&&c in b.layerStates)){var d=c,e=this.g[d];delete this.g[d];Ka(this.s[d]);delete this.s[d];Ta(e)}};function Lh(a,b){for(var c in a.g)if(!(c in b.layerStates)){b.postRenderFunctions.push(a.To.bind(a));break}}function rb(a,b){return a.zIndex-b.zIndex};function Mh(a,b){this.j=a;this.l=b;this.b=[];this.a=[];this.g={}}Mh.prototype.clear=function(){this.b.length=0;this.a.length=0;Fa(this.g)};function Nh(a){var b=a.b,c=a.a,d=b[0];1==b.length?(b.length=0,c.length=0):(b[0]=b.pop(),c[0]=c.pop(),Oh(a,0));b=a.l(d);delete a.g[b];return d}Mh.prototype.f=function(a){var b=this.j(a);return Infinity!=b?(this.b.push(a),this.a.push(b),this.g[this.l(a)]=!0,Ph(this,0,this.b.length-1),!0):!1};Mh.prototype.wc=function(){return this.b.length};
+Mh.prototype.Ya=function(){return 0===this.b.length};function Oh(a,b){for(var c=a.b,d=a.a,e=c.length,f=c[b],g=d[b],h=b;b<e>>1;){var l=2*b+1,m=2*b+2,l=m<e&&d[m]<d[l]?m:l;c[b]=c[l];d[b]=d[l];b=l}c[b]=f;d[b]=g;Ph(a,h,b)}function Ph(a,b,c){var d=a.b;a=a.a;for(var e=d[c],f=a[c];c>b;){var g=c-1>>1;if(a[g]>f)d[c]=d[g],a[c]=a[g],c=g;else break}d[c]=e;a[c]=f}
+function Qh(a){var b=a.j,c=a.b,d=a.a,e=0,f=c.length,g,h,l;for(h=0;h<f;++h)g=c[h],l=b(g),Infinity==l?delete a.g[a.l(g)]:(d[e]=l,c[e++]=g);c.length=e;d.length=e;for(b=(a.b.length>>1)-1;0<=b;b--)Oh(a,b)};function Rh(a,b){Mh.call(this,function(b){return a.apply(null,b)},function(a){return a[0].ib()});this.s=b;this.i=0;this.c={}}y(Rh,Mh);Rh.prototype.f=function(a){var b=Mh.prototype.f.call(this,a);b&&B(a[0],"change",this.o,this);return b};Rh.prototype.o=function(a){a=a.target;var b=a.V();if(2===b||3===b||4===b||5===b)Qa(a,"change",this.o,this),a=a.ib(),a in this.c&&(delete this.c[a],--this.i),this.s()};
+function Sh(a,b,c){for(var d=0,e,f;a.i<b&&d<c&&0<a.wc();)e=Nh(a)[0],f=e.ib(),0!==e.V()||f in a.c||(a.c[f]=!0,++a.i,++d,e.load())};function Th(a,b,c){this.f=a;this.g=b;this.i=c;this.b=[];this.a=this.c=0}function Uh(a,b){var c=a.f,d=a.a,e=a.g-d,f=Math.log(a.g/a.a)/a.f;return ce({source:b,duration:f,easing:function(a){return d*(Math.exp(c*a*f)-1)/e}})};function Vh(a){eb.call(this);this.v=null;this.i(!0);this.handleEvent=a.handleEvent}y(Vh,eb);Vh.prototype.f=function(){return this.get("active")};Vh.prototype.l=function(){return this.v};Vh.prototype.i=function(a){this.set("active",a)};Vh.prototype.setMap=function(a){this.v=a};function Wh(a,b,c,d,e){if(void 0!==c){var f=b.La(),g=b.ab();void 0!==f&&g&&e&&0<e&&(a.Wa(de({rotation:f,duration:e,easing:Zd})),d&&a.Wa(ce({source:g,duration:e,easing:Zd})));b.rotate(c,d)}}
+function Xh(a,b,c,d,e){var f=b.$();c=b.constrainResolution(f,c,0);Yh(a,b,c,d,e)}function Yh(a,b,c,d,e){if(c){var f=b.$(),g=b.ab();void 0!==f&&g&&c!==f&&e&&0<e&&(a.Wa(ee({resolution:f,duration:e,easing:Zd})),d&&a.Wa(ce({source:g,duration:e,easing:Zd})));if(d){var h;a=b.ab();e=b.$();void 0!==a&&void 0!==e&&(h=[d[0]-c*(d[0]-a[0])/e,d[1]-c*(d[1]-a[1])/e]);b.mb(h)}b.Ub(c)}};function Zh(a){a=a?a:{};this.a=a.delta?a.delta:1;Vh.call(this,{handleEvent:$h});this.c=void 0!==a.duration?a.duration:250}y(Zh,Vh);function $h(a){var b=!1,c=a.originalEvent;if(a.type==$g){var b=a.map,d=a.coordinate,c=c.shiftKey?-this.a:this.a,e=b.aa();Xh(b,e,c,d,this.c);a.preventDefault();b=!0}return!b};function ai(a){a=a.originalEvent;return a.altKey&&!(a.metaKey||a.ctrlKey)&&a.shiftKey}function bi(a){a=a.originalEvent;return 0==a.button&&!(He&&fg&&a.ctrlKey)}function ci(a){return"pointermove"==a.type}function di(a){return a.type==ah}function ei(a){a=a.originalEvent;return!a.altKey&&!(a.metaKey||a.ctrlKey)&&!a.shiftKey}function fi(a){a=a.originalEvent;return!a.altKey&&!(a.metaKey||a.ctrlKey)&&a.shiftKey}
+function gi(a){a=a.originalEvent.target.tagName;return"INPUT"!==a&&"SELECT"!==a&&"TEXTAREA"!==a}function hi(a){return"mouse"==a.b.pointerType}function ii(a){a=a.b;return a.isPrimary&&0===a.button};function ji(a){a=a?a:{};Vh.call(this,{handleEvent:a.handleEvent?a.handleEvent:ki});this.Oe=a.handleDownEvent?a.handleDownEvent:rc;this.Pe=a.handleDragEvent?a.handleDragEvent:na;this.Mi=a.handleMoveEvent?a.handleMoveEvent:na;this.tj=a.handleUpEvent?a.handleUpEvent:rc;this.C=!1;this.ia={};this.o=[]}y(ji,Vh);function li(a){for(var b=a.length,c=0,d=0,e=0;e<b;e++)c+=a[e].clientX,d+=a[e].clientY;return[c/b,d/b]}
+function ki(a){if(!(a instanceof Wg))return!0;var b=!1,c=a.type;if(c===eh||c===gh||c===ch)c=a.b,a.type==ch?delete this.ia[c.pointerId]:a.type==eh?this.ia[c.pointerId]=c:c.pointerId in this.ia&&(this.ia[c.pointerId]=c),this.o=Ga(this.ia);this.C&&(a.type==gh?this.Pe(a):a.type==ch&&(this.C=this.tj(a)));a.type==eh?(this.C=a=this.Oe(a),b=this.Gc(a)):a.type==fh&&this.Mi(a);return!b}ji.prototype.Gc=function(a){return a};function mi(a){ji.call(this,{handleDownEvent:ni,handleDragEvent:oi,handleUpEvent:pi});a=a?a:{};this.a=a.kinetic;this.c=this.j=null;this.A=a.condition?a.condition:ei;this.s=!1}y(mi,ji);function oi(a){var b=li(this.o);this.a&&this.a.b.push(b[0],b[1],Date.now());if(this.c){var c=this.c[0]-b[0],d=b[1]-this.c[1];a=a.map;var e=a.aa(),f=e.V(),d=c=[c,d],g=f.resolution;d[0]*=g;d[1]*=g;Gb(c,f.rotation);Bb(c,f.center);c=e.Pd(c);a.render();e.mb(c)}this.c=b}
+function pi(a){a=a.map;var b=a.aa();if(0===this.o.length){var c;if(c=!this.s&&this.a)if(c=this.a,6>c.b.length)c=!1;else{var d=Date.now()-c.i,e=c.b.length-3;if(c.b[e+2]<d)c=!1;else{for(var f=e-3;0<f&&c.b[f+2]>d;)f-=3;var d=c.b[e+2]-c.b[f+2],g=c.b[e]-c.b[f],e=c.b[e+1]-c.b[f+1];c.c=Math.atan2(e,g);c.a=Math.sqrt(g*g+e*e)/d;c=c.a>c.g}}c&&(c=this.a,c=(c.g-c.a)/c.f,e=this.a.c,f=b.ab(),this.j=Uh(this.a,f),a.Wa(this.j),f=a.Ga(f),c=a.Ma([f[0]-c*Math.cos(e),f[1]-c*Math.sin(e)]),c=b.Pd(c),b.mb(c));Xd(b,-1);a.render();
+return!1}this.c=null;return!0}function ni(a){if(0<this.o.length&&this.A(a)){var b=a.map,c=b.aa();this.c=null;this.C||Xd(c,1);b.render();this.j&&nb(b.R,this.j)&&(c.mb(a.frameState.viewState.center),this.j=null);this.a&&(a=this.a,a.b.length=0,a.c=0,a.a=0);this.s=1<this.o.length;return!0}return!1}mi.prototype.Gc=rc;function qi(a){a=a?a:{};ji.call(this,{handleDownEvent:ri,handleDragEvent:si,handleUpEvent:ti});this.c=a.condition?a.condition:ai;this.a=void 0;this.j=void 0!==a.duration?a.duration:250}y(qi,ji);function si(a){if(hi(a)){var b=a.map,c=b.Za();a=a.pixel;c=Math.atan2(c[1]/2-a[1],a[0]-c[0]/2);if(void 0!==this.a){a=c-this.a;var d=b.aa(),e=d.La();b.render();Wh(b,d,e-a)}this.a=c}}
+function ti(a){if(!hi(a))return!0;a=a.map;var b=a.aa();Xd(b,-1);var c=b.La(),d=this.j,c=b.constrainRotation(c,0);Wh(a,b,c,void 0,d);return!1}function ri(a){return hi(a)&&bi(a)&&this.c(a)?(a=a.map,Xd(a.aa(),1),a.render(),this.a=void 0,!0):!1}qi.prototype.Gc=rc;function ui(a){this.f=null;this.a=document.createElement("div");this.a.style.position="absolute";this.a.className="ol-box "+a;this.g=this.c=this.b=null}y(ui,Sa);ui.prototype.ka=function(){this.setMap(null)};function vi(a){var b=a.c,c=a.g;a=a.a.style;a.left=Math.min(b[0],c[0])+"px";a.top=Math.min(b[1],c[1])+"px";a.width=Math.abs(c[0]-b[0])+"px";a.height=Math.abs(c[1]-b[1])+"px"}
+ui.prototype.setMap=function(a){if(this.b){this.b.A.removeChild(this.a);var b=this.a.style;b.left=b.top=b.width=b.height="inherit"}(this.b=a)&&this.b.A.appendChild(this.a)};function wi(a){var b=a.c,c=a.g,b=[b,[b[0],c[1]],c,[c[0],b[1]]].map(a.b.Ma,a.b);b[4]=b[0].slice();a.f?a.f.pa([b]):a.f=new E([b])}ui.prototype.W=function(){return this.f};function xi(a,b,c){Wa.call(this,a);this.coordinate=b;this.mapBrowserEvent=c}y(xi,Wa);function yi(a){ji.call(this,{handleDownEvent:zi,handleDragEvent:Ai,handleUpEvent:Bi});a=a?a:{};this.a=new ui(a.className||"ol-dragbox");this.c=null;this.D=a.condition?a.condition:qc;this.A=a.boxEndCondition?a.boxEndCondition:Ci}y(yi,ji);function Ci(a,b,c){a=c[0]-b[0];b=c[1]-b[1];return 64<=a*a+b*b}
+function Ai(a){if(hi(a)){var b=this.a,c=a.pixel;b.c=this.c;b.g=c;wi(b);vi(b);this.b(new xi("boxdrag",a.coordinate,a))}}yi.prototype.W=function(){return this.a.W()};yi.prototype.s=na;function Bi(a){if(!hi(a))return!0;this.a.setMap(null);this.A(a,this.c,a.pixel)&&(this.s(a),this.b(new xi("boxend",a.coordinate,a)));return!1}
+function zi(a){if(hi(a)&&bi(a)&&this.D(a)){this.c=a.pixel;this.a.setMap(a.map);var b=this.a,c=this.c;b.c=this.c;b.g=c;wi(b);vi(b);this.b(new xi("boxstart",a.coordinate,a));return!0}return!1};function Di(a){a=a?a:{};var b=a.condition?a.condition:fi;this.j=void 0!==a.duration?a.duration:200;this.R=void 0!==a.out?a.out:!1;yi.call(this,{condition:b,className:a.className||"ol-dragzoom"})}y(Di,yi);
+Di.prototype.s=function(){var a=this.v,b=a.aa(),c=a.Za(),d=this.W().H();if(this.R){var e=b.Kc(c),d=[a.Ga(cc(d)),a.Ga(ec(d))],f=Wb(Infinity,Infinity,-Infinity,-Infinity,void 0),g,h;g=0;for(h=d.length;g<h;++g)Mb(f,d[g]);oc(e,1/Td(f,c));d=e}c=b.constrainResolution(Td(d,c));e=b.$();f=b.ab();a.Wa(ee({resolution:e,duration:this.j,easing:Zd}));a.Wa(ce({source:f,duration:this.j,easing:Zd}));b.mb(kc(d));b.Ub(c)};function Ei(a){Vh.call(this,{handleEvent:Fi});a=a||{};this.a=function(a){return ei(a)&&gi(a)};this.c=void 0!==a.condition?a.condition:this.a;this.o=void 0!==a.duration?a.duration:100;this.j=void 0!==a.pixelDelta?a.pixelDelta:128}y(Ei,Vh);
+function Fi(a){var b=!1;if("keydown"==a.type){var c=a.originalEvent.keyCode;if(this.c(a)&&(40==c||37==c||39==c||38==c)){var d=a.map,b=d.aa(),e=b.$()*this.j,f=0,g=0;40==c?g=-e:37==c?f=-e:39==c?f=e:g=e;c=[f,g];Gb(c,b.La());e=this.o;if(f=b.ab())e&&0<e&&d.Wa(ce({source:f,duration:e,easing:ae})),d=b.Pd([f[0]+c[0],f[1]+c[1]]),b.mb(d);a.preventDefault();b=!0}}return!b};function Gi(a){Vh.call(this,{handleEvent:Hi});a=a?a:{};this.c=a.condition?a.condition:gi;this.a=a.delta?a.delta:1;this.o=void 0!==a.duration?a.duration:100}y(Gi,Vh);function Hi(a){var b=!1;if("keydown"==a.type||"keypress"==a.type){var c=a.originalEvent.charCode;if(this.c(a)&&(43==c||45==c)){b=a.map;c=43==c?this.a:-this.a;b.render();var d=b.aa();Xh(b,d,c,void 0,this.o);a.preventDefault();b=!0}}return!b};function Ii(a){Vh.call(this,{handleEvent:Ji});a=a||{};this.c=0;this.C=void 0!==a.duration?a.duration:250;this.s=void 0!==a.useAnchor?a.useAnchor:!0;this.a=null;this.j=this.o=void 0}y(Ii,Vh);
+function Ji(a){var b=!1;if("wheel"==a.type||"mousewheel"==a.type){var b=a.map,c=a.originalEvent;this.s&&(this.a=a.coordinate);var d;"wheel"==a.type?(d=c.deltaY,dg&&c.deltaMode===pa.WheelEvent.DOM_DELTA_PIXEL&&(d/=gg),c.deltaMode===pa.WheelEvent.DOM_DELTA_LINE&&(d*=40)):"mousewheel"==a.type&&(d=-c.wheelDeltaY,eg&&(d/=3));this.c+=d;void 0===this.o&&(this.o=Date.now());d=Math.max(80-(Date.now()-this.o),0);pa.clearTimeout(this.j);this.j=pa.setTimeout(this.A.bind(this,b),d);a.preventDefault();b=!0}return!b}
+Ii.prototype.A=function(a){var b=sa(this.c,-1,1),c=a.aa();a.render();Xh(a,c,-b,this.a,this.C);this.c=0;this.a=null;this.j=this.o=void 0};Ii.prototype.D=function(a){this.s=a;a||(this.a=null)};function Ki(a){ji.call(this,{handleDownEvent:Li,handleDragEvent:Mi,handleUpEvent:Ni});a=a||{};this.c=null;this.j=void 0;this.a=!1;this.s=0;this.D=void 0!==a.threshold?a.threshold:.3;this.A=void 0!==a.duration?a.duration:250}y(Ki,ji);
+function Mi(a){var b=0,c=this.o[0],d=this.o[1],c=Math.atan2(d.clientY-c.clientY,d.clientX-c.clientX);void 0!==this.j&&(b=c-this.j,this.s+=b,!this.a&&Math.abs(this.s)>this.D&&(this.a=!0));this.j=c;a=a.map;c=a.a.getBoundingClientRect();d=li(this.o);d[0]-=c.left;d[1]-=c.top;this.c=a.Ma(d);this.a&&(c=a.aa(),d=c.La(),a.render(),Wh(a,c,d+b,this.c))}
+function Ni(a){if(2>this.o.length){a=a.map;var b=a.aa();Xd(b,-1);if(this.a){var c=b.La(),d=this.c,e=this.A,c=b.constrainRotation(c,0);Wh(a,b,c,d,e)}return!1}return!0}function Li(a){return 2<=this.o.length?(a=a.map,this.c=null,this.j=void 0,this.a=!1,this.s=0,this.C||Xd(a.aa(),1),a.render(),!0):!1}Ki.prototype.Gc=rc;function Oi(a){ji.call(this,{handleDownEvent:Pi,handleDragEvent:Qi,handleUpEvent:Ri});a=a?a:{};this.c=null;this.s=void 0!==a.duration?a.duration:400;this.a=void 0;this.j=1}y(Oi,ji);function Qi(a){var b=1,c=this.o[0],d=this.o[1],e=c.clientX-d.clientX,c=c.clientY-d.clientY,e=Math.sqrt(e*e+c*c);void 0!==this.a&&(b=this.a/e);this.a=e;1!=b&&(this.j=b);a=a.map;var e=a.aa(),c=e.$(),d=a.a.getBoundingClientRect(),f=li(this.o);f[0]-=d.left;f[1]-=d.top;this.c=a.Ma(f);a.render();Yh(a,e,c*b,this.c)}
+function Ri(a){if(2>this.o.length){a=a.map;var b=a.aa();Xd(b,-1);var c=b.$(),d=this.c,e=this.s,c=b.constrainResolution(c,0,this.j-1);Yh(a,b,c,d,e);return!1}return!0}function Pi(a){return 2<=this.o.length?(a=a.map,this.c=null,this.a=void 0,this.j=1,this.C||Xd(a.aa(),1),a.render(),!0):!1}Oi.prototype.Gc=rc;function Si(a){a=a?a:{};var b=new le,c=new Th(-.005,.05,100);(void 0!==a.altShiftDragRotate?a.altShiftDragRotate:1)&&b.push(new qi);(void 0!==a.doubleClickZoom?a.doubleClickZoom:1)&&b.push(new Zh({delta:a.zoomDelta,duration:a.zoomDuration}));(void 0!==a.dragPan?a.dragPan:1)&&b.push(new mi({kinetic:c}));(void 0!==a.pinchRotate?a.pinchRotate:1)&&b.push(new Ki);(void 0!==a.pinchZoom?a.pinchZoom:1)&&b.push(new Oi({duration:a.zoomDuration}));if(void 0!==a.keyboard?a.keyboard:1)b.push(new Ei),b.push(new Gi({delta:a.zoomDelta,
+duration:a.zoomDuration}));(void 0!==a.mouseWheelZoom?a.mouseWheelZoom:1)&&b.push(new Ii({duration:a.zoomDuration}));(void 0!==a.shiftDragZoom?a.shiftDragZoom:1)&&b.push(new Di({duration:a.zoomDuration}));return b};function Ti(a){var b=a||{};a=Ea({},b);delete a.layers;b=b.layers;ih.call(this,a);this.f=[];this.a={};B(this,gb("layers"),this.Tk,this);b?Array.isArray(b)&&(b=new le(b.slice())):b=new le;this.oh(b)}y(Ti,ih);k=Ti.prototype;k.ce=function(){this.xb()&&this.u()};
+k.Tk=function(){this.f.forEach(Ka);this.f.length=0;var a=this.Tc();this.f.push(B(a,"add",this.Sk,this),B(a,"remove",this.Uk,this));for(var b in this.a)this.a[b].forEach(Ka);Fa(this.a);var a=a.a,c,d;b=0;for(c=a.length;b<c;b++)d=a[b],this.a[w(d).toString()]=[B(d,"propertychange",this.ce,this),B(d,"change",this.ce,this)];this.u()};k.Sk=function(a){a=a.element;var b=w(a).toString();this.a[b]=[B(a,"propertychange",this.ce,this),B(a,"change",this.ce,this)];this.u()};
+k.Uk=function(a){a=w(a.element).toString();this.a[a].forEach(Ka);delete this.a[a];this.u()};k.Tc=function(){return this.get("layers")};k.oh=function(a){this.set("layers",a)};
+k.hf=function(a){var b=void 0!==a?a:[],c=b.length;this.Tc().forEach(function(a){a.hf(b)});a=jh(this);var d,e;for(d=b.length;c<d;c++)e=b[c],e.opacity*=a.opacity,e.visible=e.visible&&a.visible,e.maxResolution=Math.min(e.maxResolution,a.maxResolution),e.minResolution=Math.max(e.minResolution,a.minResolution),void 0!==a.extent&&(e.extent=void 0!==e.extent?mc(e.extent,a.extent):a.extent);return b};k.kf=function(){return"ready"};function Ui(a){vc.call(this,{code:a,units:"m",extent:Vi,global:!0,worldExtent:Wi})}y(Ui,vc);Ui.prototype.getPointResolution=function(a,b){return a/ta(b[1]/6378137)};var Xi=6378137*Math.PI,Vi=[-Xi,-Xi,Xi,Xi],Wi=[-180,-85,180,85],Hc="EPSG:3857 EPSG:102100 EPSG:102113 EPSG:900913 urn:ogc:def:crs:EPSG:6.18:3:3857 urn:ogc:def:crs:EPSG::3857 http://www.opengis.net/gml/srs/epsg.xml#3857".split(" ").map(function(a){return new Ui(a)});
+function Ic(a,b,c){var d=a.length;c=1<c?c:2;void 0===b&&(2<c?b=a.slice():b=Array(d));for(var e=0;e<d;e+=c)b[e]=6378137*Math.PI*a[e]/180,b[e+1]=6378137*Math.log(Math.tan(Math.PI*(a[e+1]+90)/360));return b}function Jc(a,b,c){var d=a.length;c=1<c?c:2;void 0===b&&(2<c?b=a.slice():b=Array(d));for(var e=0;e<d;e+=c)b[e]=180*a[e]/(6378137*Math.PI),b[e+1]=360*Math.atan(Math.exp(a[e+1]/6378137))/Math.PI-90;return b};var Yi=new sc(6378137);function Zi(a,b){vc.call(this,{code:a,units:"degrees",extent:$i,axisOrientation:b,global:!0,metersPerUnit:aj,worldExtent:$i})}y(Zi,vc);Zi.prototype.getPointResolution=function(a){return a};
+var $i=[-180,-90,180,90],aj=Math.PI*Yi.radius/180,Kc=[new Zi("CRS:84"),new Zi("EPSG:4326","neu"),new Zi("urn:ogc:def:crs:EPSG::4326","neu"),new Zi("urn:ogc:def:crs:EPSG:6.6:4326","neu"),new Zi("urn:ogc:def:crs:OGC:1.3:CRS84"),new Zi("urn:ogc:def:crs:OGC:2:84"),new Zi("http://www.opengis.net/gml/srs/epsg.xml#4326","neu"),new Zi("urn:x-ogc:def:crs:EPSG:4326","neu")];function bj(){zc(Hc);zc(Kc);Gc()};function cj(a){mh.call(this,a?a:{})}y(cj,mh);function dj(a){a=a?a:{};var b=Ea({},a);delete b.preload;delete b.useInterimTilesOnError;mh.call(this,b);this.l(void 0!==a.preload?a.preload:0);this.A(void 0!==a.useInterimTilesOnError?a.useInterimTilesOnError:!0)}y(dj,mh);dj.prototype.f=function(){return this.get("preload")};dj.prototype.l=function(a){this.set("preload",a)};dj.prototype.c=function(){return this.get("useInterimTilesOnError")};dj.prototype.A=function(a){this.set("useInterimTilesOnError",a)};var ej=[0,0,0,1],fj=[],gj=[0,0,0,1];function hj(a,b,c,d){0!==b&&(a.translate(c,d),a.rotate(b),a.translate(-c,-d))};function ij(a){a=a||{};this.b=void 0!==a.color?a.color:null;this.a=void 0}ij.prototype.g=function(){return this.b};ij.prototype.f=function(a){this.b=a;this.a=void 0};function jj(a){void 0===a.a&&(a.a=a.b instanceof CanvasPattern||a.b instanceof CanvasGradient?w(a.b).toString():"f"+(a.b?ve(a.b):"-"));return a.a};function kj(){this.a=-1};function lj(){this.a=64;this.b=Array(4);this.c=Array(this.a);this.b[0]=1732584193;this.b[1]=4023233417;this.b[2]=2562383102;this.b[3]=271733878;this.f=this.g=0}(function(){function a(){}a.prototype=kj.prototype;lj.a=kj.prototype;lj.prototype=new a;lj.prototype.constructor=lj;lj.b=function(a,c,d){for(var e=Array(arguments.length-2),f=2;f<arguments.length;f++)e[f-2]=arguments[f];return kj.prototype[c].apply(a,e)}})();
+function mj(a,b,c){c||(c=0);var d=Array(16);if(da(b))for(var e=0;16>e;++e)d[e]=b.charCodeAt(c++)|b.charCodeAt(c++)<<8|b.charCodeAt(c++)<<16|b.charCodeAt(c++)<<24;else for(e=0;16>e;++e)d[e]=b[c++]|b[c++]<<8|b[c++]<<16|b[c++]<<24;b=a.b[0];c=a.b[1];var e=a.b[2],f=a.b[3],g;g=b+(f^c&(e^f))+d[0]+3614090360&4294967295;b=c+(g<<7&4294967295|g>>>25);g=f+(e^b&(c^e))+d[1]+3905402710&4294967295;f=b+(g<<12&4294967295|g>>>20);g=e+(c^f&(b^c))+d[2]+606105819&4294967295;e=f+(g<<17&4294967295|g>>>15);g=c+(b^e&(f^b))+
+d[3]+3250441966&4294967295;c=e+(g<<22&4294967295|g>>>10);g=b+(f^c&(e^f))+d[4]+4118548399&4294967295;b=c+(g<<7&4294967295|g>>>25);g=f+(e^b&(c^e))+d[5]+1200080426&4294967295;f=b+(g<<12&4294967295|g>>>20);g=e+(c^f&(b^c))+d[6]+2821735955&4294967295;e=f+(g<<17&4294967295|g>>>15);g=c+(b^e&(f^b))+d[7]+4249261313&4294967295;c=e+(g<<22&4294967295|g>>>10);g=b+(f^c&(e^f))+d[8]+1770035416&4294967295;b=c+(g<<7&4294967295|g>>>25);g=f+(e^b&(c^e))+d[9]+2336552879&4294967295;f=b+(g<<12&4294967295|g>>>20);g=e+(c^f&
+(b^c))+d[10]+4294925233&4294967295;e=f+(g<<17&4294967295|g>>>15);g=c+(b^e&(f^b))+d[11]+2304563134&4294967295;c=e+(g<<22&4294967295|g>>>10);g=b+(f^c&(e^f))+d[12]+1804603682&4294967295;b=c+(g<<7&4294967295|g>>>25);g=f+(e^b&(c^e))+d[13]+4254626195&4294967295;f=b+(g<<12&4294967295|g>>>20);g=e+(c^f&(b^c))+d[14]+2792965006&4294967295;e=f+(g<<17&4294967295|g>>>15);g=c+(b^e&(f^b))+d[15]+1236535329&4294967295;c=e+(g<<22&4294967295|g>>>10);g=b+(e^f&(c^e))+d[1]+4129170786&4294967295;b=c+(g<<5&4294967295|g>>>
+27);g=f+(c^e&(b^c))+d[6]+3225465664&4294967295;f=b+(g<<9&4294967295|g>>>23);g=e+(b^c&(f^b))+d[11]+643717713&4294967295;e=f+(g<<14&4294967295|g>>>18);g=c+(f^b&(e^f))+d[0]+3921069994&4294967295;c=e+(g<<20&4294967295|g>>>12);g=b+(e^f&(c^e))+d[5]+3593408605&4294967295;b=c+(g<<5&4294967295|g>>>27);g=f+(c^e&(b^c))+d[10]+38016083&4294967295;f=b+(g<<9&4294967295|g>>>23);g=e+(b^c&(f^b))+d[15]+3634488961&4294967295;e=f+(g<<14&4294967295|g>>>18);g=c+(f^b&(e^f))+d[4]+3889429448&4294967295;c=e+(g<<20&4294967295|
+g>>>12);g=b+(e^f&(c^e))+d[9]+568446438&4294967295;b=c+(g<<5&4294967295|g>>>27);g=f+(c^e&(b^c))+d[14]+3275163606&4294967295;f=b+(g<<9&4294967295|g>>>23);g=e+(b^c&(f^b))+d[3]+4107603335&4294967295;e=f+(g<<14&4294967295|g>>>18);g=c+(f^b&(e^f))+d[8]+1163531501&4294967295;c=e+(g<<20&4294967295|g>>>12);g=b+(e^f&(c^e))+d[13]+2850285829&4294967295;b=c+(g<<5&4294967295|g>>>27);g=f+(c^e&(b^c))+d[2]+4243563512&4294967295;f=b+(g<<9&4294967295|g>>>23);g=e+(b^c&(f^b))+d[7]+1735328473&4294967295;e=f+(g<<14&4294967295|
+g>>>18);g=c+(f^b&(e^f))+d[12]+2368359562&4294967295;c=e+(g<<20&4294967295|g>>>12);g=b+(c^e^f)+d[5]+4294588738&4294967295;b=c+(g<<4&4294967295|g>>>28);g=f+(b^c^e)+d[8]+2272392833&4294967295;f=b+(g<<11&4294967295|g>>>21);g=e+(f^b^c)+d[11]+1839030562&4294967295;e=f+(g<<16&4294967295|g>>>16);g=c+(e^f^b)+d[14]+4259657740&4294967295;c=e+(g<<23&4294967295|g>>>9);g=b+(c^e^f)+d[1]+2763975236&4294967295;b=c+(g<<4&4294967295|g>>>28);g=f+(b^c^e)+d[4]+1272893353&4294967295;f=b+(g<<11&4294967295|g>>>21);g=e+(f^
+b^c)+d[7]+4139469664&4294967295;e=f+(g<<16&4294967295|g>>>16);g=c+(e^f^b)+d[10]+3200236656&4294967295;c=e+(g<<23&4294967295|g>>>9);g=b+(c^e^f)+d[13]+681279174&4294967295;b=c+(g<<4&4294967295|g>>>28);g=f+(b^c^e)+d[0]+3936430074&4294967295;f=b+(g<<11&4294967295|g>>>21);g=e+(f^b^c)+d[3]+3572445317&4294967295;e=f+(g<<16&4294967295|g>>>16);g=c+(e^f^b)+d[6]+76029189&4294967295;c=e+(g<<23&4294967295|g>>>9);g=b+(c^e^f)+d[9]+3654602809&4294967295;b=c+(g<<4&4294967295|g>>>28);g=f+(b^c^e)+d[12]+3873151461&4294967295;
+f=b+(g<<11&4294967295|g>>>21);g=e+(f^b^c)+d[15]+530742520&4294967295;e=f+(g<<16&4294967295|g>>>16);g=c+(e^f^b)+d[2]+3299628645&4294967295;c=e+(g<<23&4294967295|g>>>9);g=b+(e^(c|~f))+d[0]+4096336452&4294967295;b=c+(g<<6&4294967295|g>>>26);g=f+(c^(b|~e))+d[7]+1126891415&4294967295;f=b+(g<<10&4294967295|g>>>22);g=e+(b^(f|~c))+d[14]+2878612391&4294967295;e=f+(g<<15&4294967295|g>>>17);g=c+(f^(e|~b))+d[5]+4237533241&4294967295;c=e+(g<<21&4294967295|g>>>11);g=b+(e^(c|~f))+d[12]+1700485571&4294967295;b=c+
+(g<<6&4294967295|g>>>26);g=f+(c^(b|~e))+d[3]+2399980690&4294967295;f=b+(g<<10&4294967295|g>>>22);g=e+(b^(f|~c))+d[10]+4293915773&4294967295;e=f+(g<<15&4294967295|g>>>17);g=c+(f^(e|~b))+d[1]+2240044497&4294967295;c=e+(g<<21&4294967295|g>>>11);g=b+(e^(c|~f))+d[8]+1873313359&4294967295;b=c+(g<<6&4294967295|g>>>26);g=f+(c^(b|~e))+d[15]+4264355552&4294967295;f=b+(g<<10&4294967295|g>>>22);g=e+(b^(f|~c))+d[6]+2734768916&4294967295;e=f+(g<<15&4294967295|g>>>17);g=c+(f^(e|~b))+d[13]+1309151649&4294967295;
+c=e+(g<<21&4294967295|g>>>11);g=b+(e^(c|~f))+d[4]+4149444226&4294967295;b=c+(g<<6&4294967295|g>>>26);g=f+(c^(b|~e))+d[11]+3174756917&4294967295;f=b+(g<<10&4294967295|g>>>22);g=e+(b^(f|~c))+d[2]+718787259&4294967295;e=f+(g<<15&4294967295|g>>>17);g=c+(f^(e|~b))+d[9]+3951481745&4294967295;a.b[0]=a.b[0]+b&4294967295;a.b[1]=a.b[1]+(e+(g<<21&4294967295|g>>>11))&4294967295;a.b[2]=a.b[2]+e&4294967295;a.b[3]=a.b[3]+f&4294967295}
+function nj(a,b){var c;void 0===c&&(c=b.length);for(var d=c-a.a,e=a.c,f=a.g,g=0;g<c;){if(0==f)for(;g<=d;)mj(a,b,g),g+=a.a;if(da(b))for(;g<c;){if(e[f++]=b.charCodeAt(g++),f==a.a){mj(a,e);f=0;break}}else for(;g<c;)if(e[f++]=b[g++],f==a.a){mj(a,e);f=0;break}}a.g=f;a.f+=c};function oj(a){a=a||{};this.b=void 0!==a.color?a.color:null;this.f=a.lineCap;this.g=void 0!==a.lineDash?a.lineDash:null;this.c=a.lineJoin;this.i=a.miterLimit;this.a=a.width;this.l=void 0}k=oj.prototype;k.Kn=function(){return this.b};k.dk=function(){return this.f};k.Ln=function(){return this.g};k.ek=function(){return this.c};k.jk=function(){return this.i};k.Mn=function(){return this.a};k.Nn=function(a){this.b=a;this.l=void 0};k.fp=function(a){this.f=a;this.l=void 0};
+k.On=function(a){this.g=a;this.l=void 0};k.gp=function(a){this.c=a;this.l=void 0};k.hp=function(a){this.i=a;this.l=void 0};k.lp=function(a){this.a=a;this.l=void 0};
+function pj(a){if(void 0===a.l){var b="s"+(a.b?ve(a.b):"-")+","+(void 0!==a.f?a.f.toString():"-")+","+(a.g?a.g.toString():"-")+","+(void 0!==a.c?a.c:"-")+","+(void 0!==a.i?a.i.toString():"-")+","+(void 0!==a.a?a.a.toString():"-"),c=new lj;nj(c,b);b=Array((56>c.g?c.a:2*c.a)-c.g);b[0]=128;for(var d=1;d<b.length-8;++d)b[d]=0;for(var e=8*c.f,d=b.length-8;d<b.length;++d)b[d]=e&255,e/=256;nj(c,b);b=Array(16);for(d=e=0;4>d;++d)for(var f=0;32>f;f+=8)b[e++]=c.b[d]>>>f&255;if(8192>=b.length)c=String.fromCharCode.apply(null,
+b);else for(c="",d=0;d<b.length;d+=8192)e=pe(b,d,d+8192),c+=String.fromCharCode.apply(null,e);a.l=c}return a.l};function qj(a){a=a||{};this.l=this.f=this.c=null;this.g=void 0!==a.fill?a.fill:null;this.b=void 0!==a.stroke?a.stroke:null;this.a=a.radius;this.A=[0,0];this.s=this.D=this.o=null;var b=a.atlasManager,c,d=null,e,f=0;this.b&&(e=ve(this.b.b),f=this.b.a,void 0===f&&(f=1),d=this.b.g,hg||(d=null));var g=2*(this.a+f)+1;e={strokeStyle:e,Bd:f,size:g,lineDash:d};if(void 0===b)b=Oe(g,g),this.f=b.canvas,c=g=this.f.width,this.Gh(e,b,0,0),this.g?this.l=this.f:(b=Oe(e.size,e.size),this.l=b.canvas,this.Fh(e,b,0,0));
+else{g=Math.round(g);(d=!this.g)&&(c=this.Fh.bind(this,e));var f=this.b?pj(this.b):"-",h=this.g?jj(this.g):"-";this.c&&f==this.c[1]&&h==this.c[2]&&this.a==this.c[3]||(this.c=["c"+f+h+(void 0!==this.a?this.a.toString():"-"),f,h,this.a]);b=b.add(this.c[0],g,g,this.Gh.bind(this,e),c);this.f=b.image;this.A=[b.offsetX,b.offsetY];c=b.image.width;this.l=d?b.Sg:this.f}this.o=[g/2,g/2];this.D=[g,g];this.s=[c,c];Ch.call(this,{opacity:1,rotateWithView:!1,rotation:0,scale:1,snapToPixel:void 0!==a.snapToPixel?
+a.snapToPixel:!0})}y(qj,Ch);k=qj.prototype;k.Yb=function(){return this.o};k.Bn=function(){return this.g};k.pe=function(){return this.l};k.jc=function(){return this.f};k.td=function(){return 2};k.ld=function(){return this.s};k.Ia=function(){return this.A};k.Cn=function(){return this.a};k.Fb=function(){return this.D};k.Dn=function(){return this.b};k.pf=na;k.load=na;k.Xf=na;
+k.Gh=function(a,b,c,d){b.setTransform(1,0,0,1,0,0);b.translate(c,d);b.beginPath();b.arc(a.size/2,a.size/2,this.a,0,2*Math.PI,!0);this.g&&(b.fillStyle=xe(this.g.b),b.fill());this.b&&(b.strokeStyle=a.strokeStyle,b.lineWidth=a.Bd,a.lineDash&&b.setLineDash(a.lineDash),b.stroke());b.closePath()};
+k.Fh=function(a,b,c,d){b.setTransform(1,0,0,1,0,0);b.translate(c,d);b.beginPath();b.arc(a.size/2,a.size/2,this.a,0,2*Math.PI,!0);b.fillStyle=ve(ej);b.fill();this.b&&(b.strokeStyle=a.strokeStyle,b.lineWidth=a.Bd,a.lineDash&&b.setLineDash(a.lineDash),b.stroke());b.closePath()};function rj(a){a=a||{};this.i=null;this.g=sj;void 0!==a.geometry&&this.Jh(a.geometry);this.c=void 0!==a.fill?a.fill:null;this.a=void 0!==a.image?a.image:null;this.f=void 0!==a.stroke?a.stroke:null;this.l=void 0!==a.text?a.text:null;this.b=a.zIndex}k=rj.prototype;k.W=function(){return this.i};k.Zj=function(){return this.g};k.Pn=function(){return this.c};k.Qn=function(){return this.a};k.Rn=function(){return this.f};k.Ha=function(){return this.l};k.Sn=function(){return this.b};
+k.Jh=function(a){"function"===typeof a?this.g=a:"string"===typeof a?this.g=function(b){return b.get(a)}:a?a&&(this.g=function(){return a}):this.g=sj;this.i=a};k.Tn=function(a){this.b=a};function tj(a){if("function"!==typeof a){var b;b=Array.isArray(a)?a:[a];a=function(){return b}}return a}var uj=null;function vj(){if(!uj){var a=new ij({color:"rgba(255,255,255,0.4)"}),b=new oj({color:"#3399CC",width:1.25});uj=[new rj({image:new qj({fill:a,stroke:b,radius:5}),fill:a,stroke:b})]}return uj}
+function wj(){var a={},b=[255,255,255,1],c=[0,153,255,1];a.Polygon=[new rj({fill:new ij({color:[255,255,255,.5]})})];a.MultiPolygon=a.Polygon;a.LineString=[new rj({stroke:new oj({color:b,width:5})}),new rj({stroke:new oj({color:c,width:3})})];a.MultiLineString=a.LineString;a.Circle=a.Polygon.concat(a.LineString);a.Point=[new rj({image:new qj({radius:6,fill:new ij({color:c}),stroke:new oj({color:b,width:1.5})}),zIndex:Infinity})];a.MultiPoint=a.Point;a.GeometryCollection=a.Polygon.concat(a.LineString,
+a.Point);return a}function sj(a){return a.W()};function G(a){a=a?a:{};var b=Ea({},a);delete b.style;delete b.renderBuffer;delete b.updateWhileAnimating;delete b.updateWhileInteracting;mh.call(this,b);this.a=void 0!==a.renderBuffer?a.renderBuffer:100;this.A=null;this.i=void 0;this.l(a.style);this.S=void 0!==a.updateWhileAnimating?a.updateWhileAnimating:!1;this.T=void 0!==a.updateWhileInteracting?a.updateWhileInteracting:!1}y(G,mh);function xj(a){return a.get("renderOrder")}G.prototype.C=function(){return this.A};G.prototype.D=function(){return this.i};
+G.prototype.l=function(a){this.A=void 0!==a?a:vj;this.i=null===a?void 0:tj(this.A);this.u()};function I(a){a=a?a:{};var b=Ea({},a);delete b.preload;delete b.useInterimTilesOnError;G.call(this,b);this.Y(a.preload?a.preload:0);this.ia(a.useInterimTilesOnError?a.useInterimTilesOnError:!0);this.s=a.renderMode||"hybrid"}y(I,G);I.prototype.f=function(){return this.get("preload")};I.prototype.c=function(){return this.get("useInterimTilesOnError")};I.prototype.Y=function(a){this.set("preload",a)};I.prototype.ia=function(a){this.set("useInterimTilesOnError",a)};function yj(a,b,c,d,e){this.f=a;this.A=b;this.c=c;this.D=d;this.Hc=e;this.i=this.b=this.a=this.ia=this.Ra=this.Y=null;this.qa=this.za=this.v=this.Ba=this.ya=this.R=0;this.Gb=!1;this.l=this.ta=0;this.Aa=!1;this.S=0;this.g="";this.j=this.C=this.qb=this.Sa=0;this.T=this.s=this.o=null;this.U=[];this.Hb=Xc()}y(yj,kh);
+function zj(a,b,c){if(a.i){b=gd(b,0,c,2,a.D,a.U);c=a.f;var d=a.Hb,e=c.globalAlpha;1!=a.v&&(c.globalAlpha=e*a.v);var f=a.ta;a.Gb&&(f+=a.Hc);var g,h;g=0;for(h=b.length;g<h;g+=2){var l=b[g]-a.R,m=b[g+1]-a.ya;a.Aa&&(l=Math.round(l),m=Math.round(m));if(0!==f||1!=a.l){var n=l+a.R,p=m+a.ya;qh(d,n,p,a.l,a.l,f,-n,-p);c.setTransform(d[0],d[1],d[4],d[5],d[12],d[13])}c.drawImage(a.i,a.za,a.qa,a.S,a.Ba,l,m,a.S,a.Ba)}0===f&&1==a.l||c.setTransform(1,0,0,1,0,0);1!=a.v&&(c.globalAlpha=e)}}
+function Aj(a,b,c,d){var e=0;if(a.T&&""!==a.g){a.o&&Bj(a,a.o);a.s&&Cj(a,a.s);var f=a.T,g=a.f,h=a.ia;h?(h.font!=f.font&&(h.font=g.font=f.font),h.textAlign!=f.textAlign&&(h.textAlign=g.textAlign=f.textAlign),h.textBaseline!=f.textBaseline&&(h.textBaseline=g.textBaseline=f.textBaseline)):(g.font=f.font,g.textAlign=f.textAlign,g.textBaseline=f.textBaseline,a.ia={font:f.font,textAlign:f.textAlign,textBaseline:f.textBaseline});b=gd(b,e,c,d,a.D,a.U);for(f=a.f;e<c;e+=d){g=b[e]+a.Sa;h=b[e+1]+a.qb;if(0!==a.C||
+1!=a.j){var l=qh(a.Hb,g,h,a.j,a.j,a.C,-g,-h);f.setTransform(l[0],l[1],l[4],l[5],l[12],l[13])}a.s&&f.strokeText(a.g,g,h);a.o&&f.fillText(a.g,g,h)}0===a.C&&1==a.j||f.setTransform(1,0,0,1,0,0)}}function Dj(a,b,c,d,e,f){var g=a.f;a=gd(b,c,d,e,a.D,a.U);g.moveTo(a[0],a[1]);b=a.length;f&&(b-=2);for(c=2;c<b;c+=2)g.lineTo(a[c],a[c+1]);f&&g.closePath();return d}function Ej(a,b,c,d,e){var f,g;f=0;for(g=d.length;f<g;++f)c=Dj(a,b,c,d[f],e,!0);return c}k=yj.prototype;
+k.Rd=function(a){if(nc(this.c,a.H())){if(this.a||this.b){this.a&&Bj(this,this.a);this.b&&Cj(this,this.b);var b;b=this.D;var c=this.U,d=a.la();b=d?gd(d,0,d.length,a.va(),b,c):null;c=b[2]-b[0];d=b[3]-b[1];c=Math.sqrt(c*c+d*d);d=this.f;d.beginPath();d.arc(b[0],b[1],c,0,2*Math.PI);this.a&&d.fill();this.b&&d.stroke()}""!==this.g&&Aj(this,a.rd(),2,2)}};k.sd=function(a){this.Sb(a.c,a.f);this.Tb(a.a);this.Vb(a.Ha())};
+k.sc=function(a){switch(a.X()){case "Point":this.uc(a);break;case "LineString":this.hd(a);break;case "Polygon":this.bf(a);break;case "MultiPoint":this.tc(a);break;case "MultiLineString":this.$e(a);break;case "MultiPolygon":this.af(a);break;case "GeometryCollection":this.Ze(a);break;case "Circle":this.Rd(a)}};k.Ye=function(a,b){var c=(0,b.g)(a);c&&nc(this.c,c.H())&&(this.sd(b),this.sc(c))};k.Ze=function(a){a=a.c;var b,c;b=0;for(c=a.length;b<c;++b)this.sc(a[b])};
+k.uc=function(a){var b=a.la();a=a.va();this.i&&zj(this,b,b.length);""!==this.g&&Aj(this,b,b.length,a)};k.tc=function(a){var b=a.la();a=a.va();this.i&&zj(this,b,b.length);""!==this.g&&Aj(this,b,b.length,a)};k.hd=function(a){if(nc(this.c,a.H())){if(this.b){Cj(this,this.b);var b=this.f,c=a.la();b.beginPath();Dj(this,c,0,c.length,a.va(),!1);b.stroke()}""!==this.g&&(a=Fj(a),Aj(this,a,2,2))}};
+k.$e=function(a){var b=a.H();if(nc(this.c,b)){if(this.b){Cj(this,this.b);var b=this.f,c=a.la(),d=0,e=a.Db(),f=a.va();b.beginPath();var g,h;g=0;for(h=e.length;g<h;++g)d=Dj(this,c,d,e[g],f,!1);b.stroke()}""!==this.g&&(a=Gj(a),Aj(this,a,a.length,2))}};k.bf=function(a){if(nc(this.c,a.H())){if(this.b||this.a){this.a&&Bj(this,this.a);this.b&&Cj(this,this.b);var b=this.f;b.beginPath();Ej(this,a.Mb(),0,a.Db(),a.va());this.a&&b.fill();this.b&&b.stroke()}""!==this.g&&(a=Md(a),Aj(this,a,2,2))}};
+k.af=function(a){if(nc(this.c,a.H())){if(this.b||this.a){this.a&&Bj(this,this.a);this.b&&Cj(this,this.b);var b=this.f,c=Hj(a),d=0,e=a.i,f=a.va(),g,h;g=0;for(h=e.length;g<h;++g){var l=e[g];b.beginPath();d=Ej(this,c,d,l,f);this.a&&b.fill();this.b&&b.stroke()}}""!==this.g&&(a=Ij(a),Aj(this,a,a.length,2))}};function Bj(a,b){var c=a.f,d=a.Y;d?d.fillStyle!=b.fillStyle&&(d.fillStyle=c.fillStyle=b.fillStyle):(c.fillStyle=b.fillStyle,a.Y={fillStyle:b.fillStyle})}
+function Cj(a,b){var c=a.f,d=a.Ra;d?(d.lineCap!=b.lineCap&&(d.lineCap=c.lineCap=b.lineCap),hg&&!pb(d.lineDash,b.lineDash)&&c.setLineDash(d.lineDash=b.lineDash),d.lineJoin!=b.lineJoin&&(d.lineJoin=c.lineJoin=b.lineJoin),d.lineWidth!=b.lineWidth&&(d.lineWidth=c.lineWidth=b.lineWidth),d.miterLimit!=b.miterLimit&&(d.miterLimit=c.miterLimit=b.miterLimit),d.strokeStyle!=b.strokeStyle&&(d.strokeStyle=c.strokeStyle=b.strokeStyle)):(c.lineCap=b.lineCap,hg&&c.setLineDash(b.lineDash),c.lineJoin=b.lineJoin,c.lineWidth=
+b.lineWidth,c.miterLimit=b.miterLimit,c.strokeStyle=b.strokeStyle,a.Ra={lineCap:b.lineCap,lineDash:b.lineDash,lineJoin:b.lineJoin,lineWidth:b.lineWidth,miterLimit:b.miterLimit,strokeStyle:b.strokeStyle})}
+k.Sb=function(a,b){if(a){var c=a.b;this.a={fillStyle:xe(c?c:ej)}}else this.a=null;if(b){var c=b.b,d=b.f,e=b.g,f=b.c,g=b.a,h=b.i;this.b={lineCap:void 0!==d?d:"round",lineDash:e?e:fj,lineJoin:void 0!==f?f:"round",lineWidth:this.A*(void 0!==g?g:1),miterLimit:void 0!==h?h:10,strokeStyle:ve(c?c:gj)}}else this.b=null};
+k.Tb=function(a){if(a){var b=a.Yb(),c=a.jc(1),d=a.Ia(),e=a.Fb();this.R=b[0];this.ya=b[1];this.Ba=e[1];this.i=c;this.v=a.v;this.za=d[0];this.qa=d[1];this.Gb=a.U;this.ta=a.j;this.l=a.i;this.Aa=a.C;this.S=e[0]}else this.i=null};
+k.Vb=function(a){if(a){var b=a.b;b?(b=b.b,this.o={fillStyle:xe(b?b:ej)}):this.o=null;var c=a.l;if(c){var b=c.b,d=c.f,e=c.g,f=c.c,g=c.a,c=c.i;this.s={lineCap:void 0!==d?d:"round",lineDash:e?e:fj,lineJoin:void 0!==f?f:"round",lineWidth:void 0!==g?g:1,miterLimit:void 0!==c?c:10,strokeStyle:ve(b?b:gj)}}else this.s=null;var b=a.g,d=a.f,e=a.c,f=a.i,g=a.a,c=a.Ha(),h=a.o;a=a.j;this.T={font:void 0!==b?b:"10px sans-serif",textAlign:void 0!==h?h:"center",textBaseline:void 0!==a?a:"middle"};this.g=void 0!==c?
+c:"";this.Sa=void 0!==d?this.A*d:0;this.qb=void 0!==e?this.A*e:0;this.C=void 0!==f?f:0;this.j=this.A*(void 0!==g?g:1)}else this.g=""};function Jj(a){th.call(this,a);this.R=Xc()}y(Jj,th);
+Jj.prototype.i=function(a,b,c){Kj(this,"precompose",c,a,void 0);var d=this.f?this.f.a():null;if(d){var e=b.extent,f=void 0!==e;if(f){var g=a.pixelRatio,h=a.size[0]*g,l=a.size[1]*g,m=a.viewState.rotation,n=fc(e),p=ec(e),q=dc(e),e=cc(e);sh(a.coordinateToPixelMatrix,n,n);sh(a.coordinateToPixelMatrix,p,p);sh(a.coordinateToPixelMatrix,q,q);sh(a.coordinateToPixelMatrix,e,e);c.save();hj(c,-m,h/2,l/2);c.beginPath();c.moveTo(n[0]*g,n[1]*g);c.lineTo(p[0]*g,p[1]*g);c.lineTo(q[0]*g,q[1]*g);c.lineTo(e[0]*g,e[1]*
+g);c.clip();hj(c,m,h/2,l/2)}g=this.s;h=c.globalAlpha;c.globalAlpha=b.opacity;c.drawImage(d,0,0,+d.width,+d.height,Math.round(g[12]),Math.round(g[13]),Math.round(d.width*g[0]),Math.round(d.height*g[5]));c.globalAlpha=h;f&&c.restore()}Lj(this,c,a)};
+function Kj(a,b,c,d,e){var f=a.a;if(ab(f,b)){var g=d.size[0]*d.pixelRatio,h=d.size[1]*d.pixelRatio,l=d.viewState.rotation;hj(c,-l,g/2,h/2);a=void 0!==e?e:Mj(a,d,0);a=new yj(c,d.pixelRatio,d.extent,a,d.viewState.rotation);f.b(new lh(b,f,a,d,c,null));hj(c,l,g/2,h/2)}}function Lj(a,b,c,d){Kj(a,"postcompose",b,c,d)}function Mj(a,b,c){var d=b.viewState,e=b.pixelRatio;return qh(a.R,e*b.size[0]/2,e*b.size[1]/2,e/d.resolution,-e/d.resolution,-d.rotation,-d.center[0]+c,-d.center[1])};var Nj=["Polygon","LineString","Image","Text"];function Oj(a,b,c){this.qa=a;this.T=b;this.f=null;this.c=0;this.resolution=c;this.Ba=this.ya=null;this.a=[];this.coordinates=[];this.Ra=Xc();this.b=[];this.Y=[];this.ia=Xc();this.za=Xc()}y(Oj,kh);
+function Pj(a,b,c,d,e,f){var g=a.coordinates.length,h=a.df(),l=[b[c],b[c+1]],m=[NaN,NaN],n=!0,p,q,r;for(p=c+e;p<d;p+=e)m[0]=b[p],m[1]=b[p+1],r=Vb(h,m),r!==q?(n&&(a.coordinates[g++]=l[0],a.coordinates[g++]=l[1]),a.coordinates[g++]=m[0],a.coordinates[g++]=m[1],n=!1):1===r?(a.coordinates[g++]=m[0],a.coordinates[g++]=m[1],n=!1):n=!0,l[0]=m[0],l[1]=m[1],q=r;p===c+e&&(a.coordinates[g++]=l[0],a.coordinates[g++]=l[1]);f&&(a.coordinates[g++]=b[c],a.coordinates[g++]=b[c+1]);return g}
+function Qj(a,b){a.ya=[0,b,0];a.a.push(a.ya);a.Ba=[0,b,0];a.b.push(a.Ba)}
+function Rj(a,b,c,d,e,f,g,h,l){var m;rh(d,a.Ra)?m=a.Y:(m=gd(a.coordinates,0,a.coordinates.length,2,d,a.Y),$c(a.Ra,d));d=!Ha(f);var n=0,p=g.length,q,r,u=a.ia;a=a.za;for(var x,v,D,A;n<p;){var z=g[n],F,N,K,X;switch(z[0]){case 0:q=z[1];d&&f[w(q).toString()]||!q.W()?n=z[2]:void 0===l||nc(l,q.W().H())?++n:n=z[2];break;case 1:b.beginPath();++n;break;case 2:q=z[1];r=m[q];z=m[q+1];D=m[q+2]-r;q=m[q+3]-z;b.arc(r,z,Math.sqrt(D*D+q*q),0,2*Math.PI,!0);++n;break;case 3:b.closePath();++n;break;case 4:q=z[1];r=z[2];
+F=z[3];K=z[4]*c;var oa=z[5]*c,H=z[6];N=z[7];var ya=z[8],Ua=z[9];D=z[11];A=z[12];var Xa=z[13],Va=z[14];for(z[10]&&(D+=e);q<r;q+=2){z=m[q]-K;X=m[q+1]-oa;Xa&&(z=Math.round(z),X=Math.round(X));if(1!=A||0!==D){var Aa=z+K,Qb=X+oa;qh(u,Aa,Qb,A,A,D,-Aa,-Qb);b.transform(u[0],u[1],u[4],u[5],u[12],u[13])}Aa=b.globalAlpha;1!=N&&(b.globalAlpha=Aa*N);var Qb=Va+ya>F.width?F.width-ya:Va,Nb=H+Ua>F.height?F.height-Ua:H;b.drawImage(F,ya,Ua,Qb,Nb,z,X,Qb*c,Nb*c);1!=N&&(b.globalAlpha=Aa);if(1!=A||0!==D)cd(u,a),b.transform(a[0],
+a[1],a[4],a[5],a[12],a[13])}++n;break;case 5:q=z[1];r=z[2];K=z[3];oa=z[4]*c;H=z[5]*c;D=z[6];A=z[7]*c;F=z[8];for(N=z[9];q<r;q+=2){z=m[q]+oa;X=m[q+1]+H;if(1!=A||0!==D)qh(u,z,X,A,A,D,-z,-X),b.transform(u[0],u[1],u[4],u[5],u[12],u[13]);ya=K.split("\n");Ua=ya.length;1<Ua?(Xa=Math.round(1.5*b.measureText("M").width),X-=(Ua-1)/2*Xa):Xa=0;for(Va=0;Va<Ua;Va++)Aa=ya[Va],N&&b.strokeText(Aa,z,X),F&&b.fillText(Aa,z,X),X+=Xa;if(1!=A||0!==D)cd(u,a),b.transform(a[0],a[1],a[4],a[5],a[12],a[13])}++n;break;case 6:if(void 0!==
+h&&(q=z[1],q=h(q)))return q;++n;break;case 7:b.fill();++n;break;case 8:q=z[1];r=z[2];z=m[q];X=m[q+1];D=z+.5|0;A=X+.5|0;if(D!==x||A!==v)b.moveTo(z,X),x=D,v=A;for(q+=2;q<r;q+=2)if(z=m[q],X=m[q+1],D=z+.5|0,A=X+.5|0,q==r-2||D!==x||A!==v)b.lineTo(z,X),x=D,v=A;++n;break;case 9:b.fillStyle=z[1];++n;break;case 10:x=void 0!==z[7]?z[7]:!0;v=z[2];b.strokeStyle=z[1];b.lineWidth=x?v*c:v;b.lineCap=z[3];b.lineJoin=z[4];b.miterLimit=z[5];hg&&b.setLineDash(z[6]);v=x=NaN;++n;break;case 11:b.font=z[1];b.textAlign=z[2];
+b.textBaseline=z[3];++n;break;case 12:b.stroke();++n;break;default:++n}}}Oj.prototype.Pa=function(a,b,c,d,e){Rj(this,a,b,c,d,e,this.a,void 0)};function Sj(a){var b=a.b;b.reverse();var c,d=b.length,e,f,g=-1;for(c=0;c<d;++c)if(e=b[c],f=e[0],6==f)g=c;else if(0==f){e[2]=c;e=a.b;for(f=c;g<f;){var h=e[g];e[g]=e[f];e[f]=h;++g;--f}g=-1}}function Tj(a,b){a.ya[2]=a.a.length;a.ya=null;a.Ba[2]=a.b.length;a.Ba=null;var c=[6,b];a.a.push(c);a.b.push(c)}Oj.prototype.ke=na;Oj.prototype.df=function(){return this.T};
+function Uj(a,b,c){Oj.call(this,a,b,c);this.o=this.S=null;this.R=this.D=this.C=this.A=this.U=this.v=this.s=this.j=this.l=this.i=this.g=void 0}y(Uj,Oj);Uj.prototype.uc=function(a,b){if(this.o){Qj(this,b);var c=a.la(),d=this.coordinates.length,c=Pj(this,c,0,c.length,a.va(),!1);this.a.push([4,d,c,this.o,this.g,this.i,this.l,this.j,this.s,this.v,this.U,this.A,this.C,this.D,this.R]);this.b.push([4,d,c,this.S,this.g,this.i,this.l,this.j,this.s,this.v,this.U,this.A,this.C,this.D,this.R]);Tj(this,b)}};
+Uj.prototype.tc=function(a,b){if(this.o){Qj(this,b);var c=a.la(),d=this.coordinates.length,c=Pj(this,c,0,c.length,a.va(),!1);this.a.push([4,d,c,this.o,this.g,this.i,this.l,this.j,this.s,this.v,this.U,this.A,this.C,this.D,this.R]);this.b.push([4,d,c,this.S,this.g,this.i,this.l,this.j,this.s,this.v,this.U,this.A,this.C,this.D,this.R]);Tj(this,b)}};Uj.prototype.ke=function(){Sj(this);this.i=this.g=void 0;this.o=this.S=null;this.R=this.D=this.A=this.U=this.v=this.s=this.j=this.C=this.l=void 0};
+Uj.prototype.Tb=function(a){var b=a.Yb(),c=a.Fb(),d=a.pe(1),e=a.jc(1),f=a.Ia();this.g=b[0];this.i=b[1];this.S=d;this.o=e;this.l=c[1];this.j=a.v;this.s=f[0];this.v=f[1];this.U=a.U;this.A=a.j;this.C=a.i;this.D=a.C;this.R=c[0]};function Vj(a,b,c){Oj.call(this,a,b,c);this.g={fd:void 0,ad:void 0,bd:null,cd:void 0,dd:void 0,ed:void 0,nf:0,strokeStyle:void 0,lineCap:void 0,lineDash:null,lineJoin:void 0,lineWidth:void 0,miterLimit:void 0}}y(Vj,Oj);
+function Wj(a,b,c,d,e){var f=a.coordinates.length;b=Pj(a,b,c,d,e,!1);f=[8,f,b];a.a.push(f);a.b.push(f);return d}k=Vj.prototype;k.df=function(){this.f||(this.f=Pb(this.T),0<this.c&&Ob(this.f,this.resolution*(this.c+1)/2,this.f));return this.f};
+function Xj(a){var b=a.g,c=b.strokeStyle,d=b.lineCap,e=b.lineDash,f=b.lineJoin,g=b.lineWidth,h=b.miterLimit;b.fd==c&&b.ad==d&&pb(b.bd,e)&&b.cd==f&&b.dd==g&&b.ed==h||(b.nf!=a.coordinates.length&&(a.a.push([12]),b.nf=a.coordinates.length),a.a.push([10,c,g,d,f,h,e],[1]),b.fd=c,b.ad=d,b.bd=e,b.cd=f,b.dd=g,b.ed=h)}
+k.hd=function(a,b){var c=this.g,d=c.lineWidth;void 0!==c.strokeStyle&&void 0!==d&&(Xj(this),Qj(this,b),this.b.push([10,c.strokeStyle,c.lineWidth,c.lineCap,c.lineJoin,c.miterLimit,c.lineDash],[1]),c=a.la(),Wj(this,c,0,c.length,a.va()),this.b.push([12]),Tj(this,b))};
+k.$e=function(a,b){var c=this.g,d=c.lineWidth;if(void 0!==c.strokeStyle&&void 0!==d){Xj(this);Qj(this,b);this.b.push([10,c.strokeStyle,c.lineWidth,c.lineCap,c.lineJoin,c.miterLimit,c.lineDash],[1]);var c=a.Db(),d=a.la(),e=a.va(),f=0,g,h;g=0;for(h=c.length;g<h;++g)f=Wj(this,d,f,c[g],e);this.b.push([12]);Tj(this,b)}};k.ke=function(){this.g.nf!=this.coordinates.length&&this.a.push([12]);Sj(this);this.g=null};
+k.Sb=function(a,b){var c=b.b;this.g.strokeStyle=ve(c?c:gj);c=b.f;this.g.lineCap=void 0!==c?c:"round";c=b.g;this.g.lineDash=c?c:fj;c=b.c;this.g.lineJoin=void 0!==c?c:"round";c=b.a;this.g.lineWidth=void 0!==c?c:1;c=b.i;this.g.miterLimit=void 0!==c?c:10;this.g.lineWidth>this.c&&(this.c=this.g.lineWidth,this.f=null)};
+function Yj(a,b,c){Oj.call(this,a,b,c);this.g={ug:void 0,fd:void 0,ad:void 0,bd:null,cd:void 0,dd:void 0,ed:void 0,fillStyle:void 0,strokeStyle:void 0,lineCap:void 0,lineDash:null,lineJoin:void 0,lineWidth:void 0,miterLimit:void 0}}y(Yj,Oj);
+function Zj(a,b,c,d,e){var f=a.g,g=[1];a.a.push(g);a.b.push(g);var h,g=0;for(h=d.length;g<h;++g){var l=d[g],m=a.coordinates.length;c=Pj(a,b,c,l,e,!0);c=[8,m,c];m=[3];a.a.push(c,m);a.b.push(c,m);c=l}b=[7];a.b.push(b);void 0!==f.fillStyle&&a.a.push(b);void 0!==f.strokeStyle&&(f=[12],a.a.push(f),a.b.push(f));return c}k=Yj.prototype;
+k.Rd=function(a,b){var c=this.g,d=c.strokeStyle;if(void 0!==c.fillStyle||void 0!==d){ak(this);Qj(this,b);this.b.push([9,ve(ej)]);void 0!==c.strokeStyle&&this.b.push([10,c.strokeStyle,c.lineWidth,c.lineCap,c.lineJoin,c.miterLimit,c.lineDash]);var e=a.la(),d=this.coordinates.length;Pj(this,e,0,e.length,a.va(),!1);e=[1];d=[2,d];this.a.push(e,d);this.b.push(e,d);d=[7];this.b.push(d);void 0!==c.fillStyle&&this.a.push(d);void 0!==c.strokeStyle&&(c=[12],this.a.push(c),this.b.push(c));Tj(this,b)}};
+k.bf=function(a,b){var c=this.g,d=c.strokeStyle;if(void 0!==c.fillStyle||void 0!==d)ak(this),Qj(this,b),this.b.push([9,ve(ej)]),void 0!==c.strokeStyle&&this.b.push([10,c.strokeStyle,c.lineWidth,c.lineCap,c.lineJoin,c.miterLimit,c.lineDash]),c=a.Db(),d=a.Mb(),Zj(this,d,0,c,a.va()),Tj(this,b)};
+k.af=function(a,b){var c=this.g,d=c.strokeStyle;if(void 0!==c.fillStyle||void 0!==d){ak(this);Qj(this,b);this.b.push([9,ve(ej)]);void 0!==c.strokeStyle&&this.b.push([10,c.strokeStyle,c.lineWidth,c.lineCap,c.lineJoin,c.miterLimit,c.lineDash]);var c=a.i,d=Hj(a),e=a.va(),f=0,g,h;g=0;for(h=c.length;g<h;++g)f=Zj(this,d,f,c[g],e);Tj(this,b)}};k.ke=function(){Sj(this);this.g=null;var a=this.qa;if(0!==a){var b=this.coordinates,c,d;c=0;for(d=b.length;c<d;++c)b[c]=a*Math.round(b[c]/a)}};
+k.df=function(){this.f||(this.f=Pb(this.T),0<this.c&&Ob(this.f,this.resolution*(this.c+1)/2,this.f));return this.f};
+k.Sb=function(a,b){var c=this.g;if(a){var d=a.b;c.fillStyle=xe(d?d:ej)}else c.fillStyle=void 0;b?(d=b.b,c.strokeStyle=ve(d?d:gj),d=b.f,c.lineCap=void 0!==d?d:"round",d=b.g,c.lineDash=d?d.slice():fj,d=b.c,c.lineJoin=void 0!==d?d:"round",d=b.a,c.lineWidth=void 0!==d?d:1,d=b.i,c.miterLimit=void 0!==d?d:10,c.lineWidth>this.c&&(this.c=c.lineWidth,this.f=null)):(c.strokeStyle=void 0,c.lineCap=void 0,c.lineDash=null,c.lineJoin=void 0,c.lineWidth=void 0,c.miterLimit=void 0)};
+function ak(a){var b=a.g,c=b.fillStyle,d=b.strokeStyle,e=b.lineCap,f=b.lineDash,g=b.lineJoin,h=b.lineWidth,l=b.miterLimit;void 0!==c&&b.ug!=c&&(a.a.push([9,c]),b.ug=b.fillStyle);void 0===d||b.fd==d&&b.ad==e&&b.bd==f&&b.cd==g&&b.dd==h&&b.ed==l||(a.a.push([10,d,h,e,g,l,f]),b.fd=d,b.ad=e,b.bd=f,b.cd=g,b.dd=h,b.ed=l)}function bk(a,b,c){Oj.call(this,a,b,c);this.D=this.C=this.A=null;this.o="";this.U=this.v=this.s=this.j=0;this.l=this.i=this.g=null}y(bk,Oj);
+function ck(a,b,c,d,e){if(""!==a.o&&a.l&&(a.g||a.i)){if(a.g){var f=a.g,g=a.A;if(!g||g.fillStyle!=f.fillStyle){var h=[9,f.fillStyle];a.a.push(h);a.b.push(h);g?g.fillStyle=f.fillStyle:a.A={fillStyle:f.fillStyle}}}a.i&&(f=a.i,g=a.C,g&&g.lineCap==f.lineCap&&g.lineDash==f.lineDash&&g.lineJoin==f.lineJoin&&g.lineWidth==f.lineWidth&&g.miterLimit==f.miterLimit&&g.strokeStyle==f.strokeStyle||(h=[10,f.strokeStyle,f.lineWidth,f.lineCap,f.lineJoin,f.miterLimit,f.lineDash,!1],a.a.push(h),a.b.push(h),g?(g.lineCap=
+f.lineCap,g.lineDash=f.lineDash,g.lineJoin=f.lineJoin,g.lineWidth=f.lineWidth,g.miterLimit=f.miterLimit,g.strokeStyle=f.strokeStyle):a.C={lineCap:f.lineCap,lineDash:f.lineDash,lineJoin:f.lineJoin,lineWidth:f.lineWidth,miterLimit:f.miterLimit,strokeStyle:f.strokeStyle}));f=a.l;g=a.D;g&&g.font==f.font&&g.textAlign==f.textAlign&&g.textBaseline==f.textBaseline||(h=[11,f.font,f.textAlign,f.textBaseline],a.a.push(h),a.b.push(h),g?(g.font=f.font,g.textAlign=f.textAlign,g.textBaseline=f.textBaseline):a.D=
+{font:f.font,textAlign:f.textAlign,textBaseline:f.textBaseline});Qj(a,e);f=a.coordinates.length;b=Pj(a,b,0,c,d,!1);b=[5,f,b,a.o,a.j,a.s,a.v,a.U,!!a.g,!!a.i];a.a.push(b);a.b.push(b);Tj(a,e)}}
+bk.prototype.Vb=function(a){if(a){var b=a.b;b?(b=b.b,b=xe(b?b:ej),this.g?this.g.fillStyle=b:this.g={fillStyle:b}):this.g=null;var c=a.l;if(c){var b=c.b,d=c.f,e=c.g,f=c.c,g=c.a,c=c.i,d=void 0!==d?d:"round",e=e?e.slice():fj,f=void 0!==f?f:"round",g=void 0!==g?g:1,c=void 0!==c?c:10,b=ve(b?b:gj);if(this.i){var h=this.i;h.lineCap=d;h.lineDash=e;h.lineJoin=f;h.lineWidth=g;h.miterLimit=c;h.strokeStyle=b}else this.i={lineCap:d,lineDash:e,lineJoin:f,lineWidth:g,miterLimit:c,strokeStyle:b}}else this.i=null;
+var l=a.g,b=a.f,d=a.c,e=a.i,g=a.a,c=a.Ha(),f=a.o,h=a.j;a=void 0!==l?l:"10px sans-serif";f=void 0!==f?f:"center";h=void 0!==h?h:"middle";this.l?(l=this.l,l.font=a,l.textAlign=f,l.textBaseline=h):this.l={font:a,textAlign:f,textBaseline:h};this.o=void 0!==c?c:"";this.j=void 0!==b?b:0;this.s=void 0!==d?d:0;this.v=void 0!==e?e:0;this.U=void 0!==g?g:1}else this.o=""};function dk(a,b,c,d){this.o=a;this.g=b;this.l=c;this.f=d;this.a={};this.c=Oe(1,1);this.i=Xc()}
+function ek(a){for(var b in a.a){var c=a.a[b],d;for(d in c)c[d].ke()}}dk.prototype.ra=function(a,b,c,d,e){var f=this.i;qh(f,.5,.5,1/b,-1/b,-c,-a[0],-a[1]);var g=this.c;g.clearRect(0,0,1,1);var h;void 0!==this.f&&(h=Lb(),Mb(h,a),Ob(h,b*this.f,h));return fk(this,g,f,c,d,function(a){if(0<g.getImageData(0,0,1,1).data[3]){if(a=e(a))return a;g.clearRect(0,0,1,1)}},h)};
+dk.prototype.b=function(a,b){var c=void 0!==a?a.toString():"0",d=this.a[c];void 0===d&&(d={},this.a[c]=d);c=d[b];void 0===c&&(c=new gk[b](this.o,this.g,this.l),d[b]=c);return c};dk.prototype.Ya=function(){return Ha(this.a)};
+dk.prototype.Pa=function(a,b,c,d,e,f){var g=Object.keys(this.a).map(Number);g.sort(ib);var h=this.g,l=h[0],m=h[1],n=h[2],h=h[3],l=[l,m,l,h,n,h,n,m];gd(l,0,8,2,c,l);a.save();a.beginPath();a.moveTo(l[0],l[1]);a.lineTo(l[2],l[3]);a.lineTo(l[4],l[5]);a.lineTo(l[6],l[7]);a.closePath();a.clip();f=f?f:Nj;for(var p,q,l=0,m=g.length;l<m;++l)for(p=this.a[g[l].toString()],n=0,h=f.length;n<h;++n)q=p[f[n]],void 0!==q&&q.Pa(a,b,c,d,e);a.restore()};
+function fk(a,b,c,d,e,f,g){var h=Object.keys(a.a).map(Number);h.sort(function(a,b){return b-a});var l,m,n,p,q;l=0;for(m=h.length;l<m;++l)for(p=a.a[h[l].toString()],n=Nj.length-1;0<=n;--n)if(q=p[Nj[n]],void 0!==q&&(q=Rj(q,b,1,c,d,e,q.b,f,g)))return q}var gk={Image:Uj,LineString:Vj,Polygon:Yj,Text:bk};function hk(a,b,c,d){this.g=a;this.b=b;this.c=c;this.f=d}k=hk.prototype;k.get=function(a){return this.f[a]};k.Db=function(){return this.c};k.H=function(){this.a||(this.a="Point"===this.g?Xb(this.b):Yb(this.b,0,this.b.length,2));return this.a};k.Mb=function(){return this.b};k.la=hk.prototype.Mb;k.W=function(){return this};k.Nm=function(){return this.f};k.od=hk.prototype.W;k.va=function(){return 2};k.ec=na;k.X=function(){return this.g};function ik(a,b){return w(a)-w(b)}function jk(a,b){var c=.5*a/b;return c*c}function lk(a,b,c,d,e,f){var g=!1,h,l;if(h=c.a)l=h.td(),2==l||3==l?h.Xf(e,f):(0==l&&h.load(),h.pf(e,f),g=!0);if(e=(0,c.g)(b))d=e.od(d),(0,mk[d.X()])(a,d,c,b);return g}
+var mk={Point:function(a,b,c,d){var e=c.a;if(e){if(2!=e.td())return;var f=a.b(c.b,"Image");f.Tb(e);f.uc(b,d)}if(e=c.Ha())a=a.b(c.b,"Text"),a.Vb(e),ck(a,b.la(),2,2,d)},LineString:function(a,b,c,d){var e=c.f;if(e){var f=a.b(c.b,"LineString");f.Sb(null,e);f.hd(b,d)}if(e=c.Ha())a=a.b(c.b,"Text"),a.Vb(e),ck(a,Fj(b),2,2,d)},Polygon:function(a,b,c,d){var e=c.c,f=c.f;if(e||f){var g=a.b(c.b,"Polygon");g.Sb(e,f);g.bf(b,d)}if(e=c.Ha())a=a.b(c.b,"Text"),a.Vb(e),ck(a,Md(b),2,2,d)},MultiPoint:function(a,b,c,d){var e=
+c.a;if(e){if(2!=e.td())return;var f=a.b(c.b,"Image");f.Tb(e);f.tc(b,d)}if(e=c.Ha())a=a.b(c.b,"Text"),a.Vb(e),c=b.la(),ck(a,c,c.length,b.va(),d)},MultiLineString:function(a,b,c,d){var e=c.f;if(e){var f=a.b(c.b,"LineString");f.Sb(null,e);f.$e(b,d)}if(e=c.Ha())a=a.b(c.b,"Text"),a.Vb(e),b=Gj(b),ck(a,b,b.length,2,d)},MultiPolygon:function(a,b,c,d){var e=c.c,f=c.f;if(f||e){var g=a.b(c.b,"Polygon");g.Sb(e,f);g.af(b,d)}if(e=c.Ha())a=a.b(c.b,"Text"),a.Vb(e),b=Ij(b),ck(a,b,b.length,2,d)},GeometryCollection:function(a,
+b,c,d){b=b.c;var e,f;e=0;for(f=b.length;e<f;++e)(0,mk[b[e].X()])(a,b[e],c,d)},Circle:function(a,b,c,d){var e=c.c,f=c.f;if(e||f){var g=a.b(c.b,"Polygon");g.Sb(e,f);g.Rd(b,d)}if(e=c.Ha())a=a.b(c.b,"Text"),a.Vb(e),ck(a,b.rd(),2,2,d)}};function nk(a,b,c,d,e,f){this.c=void 0!==f?f:null;oh.call(this,a,b,c,void 0!==f?0:2,d);this.g=e}y(nk,oh);nk.prototype.i=function(a){this.state=a?3:2;ph(this)};nk.prototype.load=function(){0==this.state&&(this.state=1,ph(this),this.c(this.i.bind(this)))};nk.prototype.a=function(){return this.g};var ok,pk=pa.navigator,qk=pa.chrome,rk=-1<pk.userAgent.indexOf("OPR"),sk=-1<pk.userAgent.indexOf("Edge");ok=!(!pk.userAgent.match("CriOS")&&null!==qk&&void 0!==qk&&"Google Inc."===pk.vendor&&0==rk&&0==sk);function tk(a,b,c,d){var e=Rc(c,b,a);c=b.getPointResolution(d,c);b=b.$b();void 0!==b&&(c*=b);b=a.$b();void 0!==b&&(c/=b);a=a.getPointResolution(c,e)/c;isFinite(a)&&0<a&&(c/=a);return c}function uk(a,b,c,d){a=c-a;b=d-b;var e=Math.sqrt(a*a+b*b);return[Math.round(c+a/e),Math.round(d+b/e)]}
+function vk(a,b,c,d,e,f,g,h,l,m,n){var p=Oe(Math.round(c*a),Math.round(c*b));if(0===l.length)return p.canvas;p.scale(c,c);var q=Lb();l.forEach(function(a){ac(q,a.extent)});var r=Oe(Math.round(c*ic(q)/d),Math.round(c*jc(q)/d)),u=c/d;l.forEach(function(a){r.drawImage(a.image,m,m,a.image.width-2*m,a.image.height-2*m,(a.extent[0]-q[0])*u,-(a.extent[3]-q[3])*u,ic(a.extent)*u,jc(a.extent)*u)});var x=fc(g);h.f.forEach(function(a){var b=a.source,e=a.target,g=b[1][0],h=b[1][1],l=b[2][0],m=b[2][1];a=(e[0][0]-
+x[0])/f;var n=-(e[0][1]-x[1])/f,u=(e[1][0]-x[0])/f,H=-(e[1][1]-x[1])/f,ya=(e[2][0]-x[0])/f,Ua=-(e[2][1]-x[1])/f,e=b[0][0],b=b[0][1],g=g-e,h=h-b,l=l-e,m=m-b;a:{g=[[g,h,0,0,u-a],[l,m,0,0,ya-a],[0,0,g,h,H-n],[0,0,l,m,Ua-n]];h=g.length;for(l=0;l<h;l++){for(var m=l,Xa=Math.abs(g[l][l]),Va=l+1;Va<h;Va++){var Aa=Math.abs(g[Va][l]);Aa>Xa&&(Xa=Aa,m=Va)}if(0===Xa){g=null;break a}Xa=g[m];g[m]=g[l];g[l]=Xa;for(m=l+1;m<h;m++)for(Xa=-g[m][l]/g[l][l],Va=l;Va<h+1;Va++)g[m][Va]=l==Va?0:g[m][Va]+Xa*g[l][Va]}l=Array(h);
+for(m=h-1;0<=m;m--)for(l[m]=g[m][h]/g[m][m],Xa=m-1;0<=Xa;Xa--)g[Xa][h]-=g[Xa][m]*l[m];g=l}g&&(p.save(),p.beginPath(),ok?(l=(a+u+ya)/3,m=(n+H+Ua)/3,h=uk(l,m,a,n),u=uk(l,m,u,H),ya=uk(l,m,ya,Ua),p.moveTo(h[0],h[1]),p.lineTo(u[0],u[1]),p.lineTo(ya[0],ya[1])):(p.moveTo(a,n),p.lineTo(u,H),p.lineTo(ya,Ua)),p.closePath(),p.clip(),p.transform(g[0],g[2],g[1],g[3],a,n),p.translate(q[0]-e,q[3]-b),p.scale(d/c,-d/c),p.drawImage(r.canvas,0,0),p.restore())});n&&(p.save(),p.strokeStyle="black",p.lineWidth=1,h.f.forEach(function(a){var b=
+a.target;a=(b[0][0]-x[0])/f;var c=-(b[0][1]-x[1])/f,d=(b[1][0]-x[0])/f,e=-(b[1][1]-x[1])/f,g=(b[2][0]-x[0])/f,b=-(b[2][1]-x[1])/f;p.beginPath();p.moveTo(a,c);p.lineTo(d,e);p.lineTo(g,b);p.closePath();p.stroke()}),p.restore());return p.canvas};function wk(a,b,c,d,e){this.g=a;this.c=b;var f={},g=Pc(this.c,this.g);this.a=function(a){var b=a[0]+"/"+a[1];f[b]||(f[b]=g(a));return f[b]};this.i=d;this.s=e*e;this.f=[];this.o=!1;this.j=this.g.a&&!!d&&!!this.g.H()&&ic(d)==ic(this.g.H());this.b=this.g.H()?ic(this.g.H()):null;this.l=this.c.H()?ic(this.c.H()):null;a=fc(c);b=ec(c);d=dc(c);c=cc(c);e=this.a(a);var h=this.a(b),l=this.a(d),m=this.a(c);xk(this,a,b,d,c,e,h,l,m,10);if(this.o){var n=Infinity;this.f.forEach(function(a){n=Math.min(n,a.source[0][0],
+a.source[1][0],a.source[2][0])});this.f.forEach(function(a){if(Math.max(a.source[0][0],a.source[1][0],a.source[2][0])-n>this.b/2){var b=[[a.source[0][0],a.source[0][1]],[a.source[1][0],a.source[1][1]],[a.source[2][0],a.source[2][1]]];b[0][0]-n>this.b/2&&(b[0][0]-=this.b);b[1][0]-n>this.b/2&&(b[1][0]-=this.b);b[2][0]-n>this.b/2&&(b[2][0]-=this.b);Math.max(b[0][0],b[1][0],b[2][0])-Math.min(b[0][0],b[1][0],b[2][0])<this.b/2&&(a.source=b)}},this)}f={}}
+function xk(a,b,c,d,e,f,g,h,l,m){var n=Kb([f,g,h,l]),p=a.b?ic(n)/a.b:null,q=a.g.a&&.5<p&&1>p,r=!1;if(0<m){if(a.c.g&&a.l)var u=Kb([b,c,d,e]),r=r|.25<ic(u)/a.l;!q&&a.g.g&&p&&(r|=.25<p)}if(r||!a.i||nc(n,a.i)){if(!(r||isFinite(f[0])&&isFinite(f[1])&&isFinite(g[0])&&isFinite(g[1])&&isFinite(h[0])&&isFinite(h[1])&&isFinite(l[0])&&isFinite(l[1])))if(0<m)r=!0;else return;if(0<m&&(r||(p=a.a([(b[0]+d[0])/2,(b[1]+d[1])/2]),n=q?(xa(f[0],a.b)+xa(h[0],a.b))/2-xa(p[0],a.b):(f[0]+h[0])/2-p[0],p=(f[1]+h[1])/2-p[1],
+r=n*n+p*p>a.s),r)){Math.abs(b[0]-d[0])<=Math.abs(b[1]-d[1])?(q=[(c[0]+d[0])/2,(c[1]+d[1])/2],n=a.a(q),p=[(e[0]+b[0])/2,(e[1]+b[1])/2],r=a.a(p),xk(a,b,c,q,p,f,g,n,r,m-1),xk(a,p,q,d,e,r,n,h,l,m-1)):(q=[(b[0]+c[0])/2,(b[1]+c[1])/2],n=a.a(q),p=[(d[0]+e[0])/2,(d[1]+e[1])/2],r=a.a(p),xk(a,b,q,p,e,f,n,r,l,m-1),xk(a,q,c,d,p,n,g,h,r,m-1));return}if(q){if(!a.j)return;a.o=!0}a.f.push({source:[f,h,l],target:[b,d,e]});a.f.push({source:[f,g,h],target:[b,c,d]})}}
+function yk(a){var b=Lb();a.f.forEach(function(a){a=a.source;Mb(b,a[0]);Mb(b,a[1]);Mb(b,a[2])});return b};function zk(a,b,c,d,e,f){this.v=b;this.s=a.H();var g=b.H(),h=g?mc(c,g):c,g=tk(a,b,kc(h),d);this.o=new wk(a,b,h,this.s,.5*g);this.c=d;this.g=c;a=yk(this.o);this.j=(this.ob=f(a,g,e))?this.ob.f:1;this.Ad=this.i=null;e=2;f=[];this.ob&&(e=0,f=this.ob.l);oh.call(this,c,d,this.j,e,f)}y(zk,oh);zk.prototype.ka=function(){1==this.state&&(Ka(this.Ad),this.Ad=null);oh.prototype.ka.call(this)};zk.prototype.a=function(){return this.i};
+zk.prototype.zd=function(){var a=this.ob.V();2==a&&(this.i=vk(ic(this.g)/this.c,jc(this.g)/this.c,this.j,this.ob.$(),0,this.c,this.g,this.o,[{extent:this.ob.H(),image:this.ob.a()}],0));this.state=a;ph(this)};zk.prototype.load=function(){if(0==this.state){this.state=1;ph(this);var a=this.ob.V();2==a||3==a?this.zd():(this.Ad=B(this.ob,"change",function(){var a=this.ob.V();if(2==a||3==a)Ka(this.Ad),this.Ad=null,this.zd()},this),this.ob.load())}};function Ak(a){jf.call(this,{attributions:a.attributions,extent:a.extent,logo:a.logo,projection:a.projection,state:a.state});this.C=void 0!==a.resolutions?a.resolutions:null;this.a=null;this.qa=0}y(Ak,jf);function Bk(a,b){if(a.C){var c=kb(a.C,b,0);b=a.C[c]}return b}
+Ak.prototype.A=function(a,b,c,d){var e=this.f;if(e&&d&&!Oc(e,d)){if(this.a){if(this.qa==this.g&&Oc(this.a.v,d)&&this.a.$()==b&&this.a.f==c&&$b(this.a.H(),a))return this.a;Ta(this.a);this.a=null}this.a=new zk(e,d,a,b,c,function(a,b,c){return this.Mc(a,b,c,e)}.bind(this));this.qa=this.g;return this.a}e&&(d=e);return this.Mc(a,b,c,d)};Ak.prototype.o=function(a){a=a.target;switch(a.V()){case 1:this.b(new Ck(Dk,a));break;case 2:this.b(new Ck(Ek,a));break;case 3:this.b(new Ck(Fk,a))}};
+function Gk(a,b){a.a().src=b}function Ck(a,b){Wa.call(this,a);this.image=b}y(Ck,Wa);var Dk="imageloadstart",Ek="imageloadend",Fk="imageloaderror";function Hk(a){Ak.call(this,{attributions:a.attributions,logo:a.logo,projection:a.projection,resolutions:a.resolutions,state:a.state});this.ia=a.canvasFunction;this.T=null;this.Y=0;this.ta=void 0!==a.ratio?a.ratio:1.5}y(Hk,Ak);Hk.prototype.Mc=function(a,b,c,d){b=Bk(this,b);var e=this.T;if(e&&this.Y==this.g&&e.$()==b&&e.f==c&&Ub(e.H(),a))return e;a=a.slice();oc(a,this.ta);(d=this.ia(a,b,c,[ic(a)/b*c,jc(a)/b*c],d))&&(e=new nk(a,b,c,this.l,d));this.T=e;this.Y=this.g;return e};function Ik(a){eb.call(this);this.i=void 0;this.a="geometry";this.c=null;this.l=void 0;this.f=null;B(this,gb(this.a),this.be,this);void 0!==a&&(a instanceof Tc||!a?this.Ua(a):this.G(a))}y(Ik,eb);k=Ik.prototype;k.clone=function(){var a=new Ik(this.O());a.Ec(this.a);var b=this.W();b&&a.Ua(b.clone());(b=this.c)&&a.sf(b);return a};k.W=function(){return this.get(this.a)};k.Xa=function(){return this.i};k.$j=function(){return this.a};k.Jl=function(){return this.c};k.ec=function(){return this.l};k.Kl=function(){this.u()};
+k.be=function(){this.f&&(Ka(this.f),this.f=null);var a=this.W();a&&(this.f=B(a,"change",this.Kl,this));this.u()};k.Ua=function(a){this.set(this.a,a)};k.sf=function(a){this.l=(this.c=a)?Jk(a):void 0;this.u()};k.mc=function(a){this.i=a;this.u()};k.Ec=function(a){Qa(this,gb(this.a),this.be,this);this.a=a;B(this,gb(this.a),this.be,this);this.be()};function Jk(a){if("function"!==typeof a){var b;b=Array.isArray(a)?a:[a];a=function(){return b}}return a};function Kk(a,b,c,d,e){df.call(this,a,b);this.g=Oe();this.l=d;this.c=null;this.f={gd:!1,Tf:null,bi:-1,Uf:-1,yd:null,ui:[]};this.v=e;this.j=c}y(Kk,df);k=Kk.prototype;k.$a=function(){return-1==this.f.Uf?null:this.g.canvas};k.Ul=function(){return this.l};k.ib=function(){return this.j};k.load=function(){0==this.state&&(this.state=1,ef(this),this.v(this,this.j),this.s(null,NaN,null))};k.gi=function(a){this.c=a;this.state=2;ef(this)};k.vf=function(a){this.o=a};k.ki=function(a){this.s=a};var Lk=document.implementation.createDocument("","",null);function Mk(a,b){return Lk.createElementNS(a,b)}function Nk(a,b){return Ok(a,b,[]).join("")}function Ok(a,b,c){if(a.nodeType==Node.CDATA_SECTION_NODE||a.nodeType==Node.TEXT_NODE)b?c.push(String(a.nodeValue).replace(/(\r\n|\r|\n)/g,"")):c.push(a.nodeValue);else for(a=a.firstChild;a;a=a.nextSibling)Ok(a,b,c);return c}function Pk(a){return a instanceof Document}function Qk(a){return a instanceof Node}
+function Rk(a){return(new DOMParser).parseFromString(a,"application/xml")}function Sk(a,b){return function(c,d){var e=a.call(b,c,d);void 0!==e&&mb(d[d.length-1],e)}}function Tk(a,b){return function(c,d){var e=a.call(void 0!==b?b:this,c,d);void 0!==e&&d[d.length-1].push(e)}}function Uk(a,b){return function(c,d){var e=a.call(void 0!==b?b:this,c,d);void 0!==e&&(d[d.length-1]=e)}}
+function Vk(a){return function(b,c){var d=a.call(this,b,c);if(void 0!==d){var e=c[c.length-1],f=b.localName,g;f in e?g=e[f]:g=e[f]=[];g.push(d)}}}function J(a,b){return function(c,d){var e=a.call(this,c,d);void 0!==e&&(d[d.length-1][void 0!==b?b:c.localName]=e)}}function L(a,b){return function(c,d,e){a.call(void 0!==b?b:this,c,d,e);e[e.length-1].node.appendChild(c)}}
+function Wk(a){var b,c;return function(d,e,f){if(!b){b={};var g={};g[d.localName]=a;b[d.namespaceURI]=g;c=Xk(d.localName)}Yk(b,c,e,f)}}function Xk(a,b){return function(c,d,e){c=d[d.length-1].node;d=a;void 0===d&&(d=e);e=b;void 0===b&&(e=c.namespaceURI);return Mk(e,d)}}var Zk=Xk();function $k(a,b){for(var c=b.length,d=Array(c),e=0;e<c;++e)d[e]=a[b[e]];return d}function M(a,b,c){c=void 0!==c?c:{};var d,e;d=0;for(e=a.length;d<e;++d)c[a[d]]=b;return c}
+function al(a,b,c,d){for(b=b.firstElementChild;b;b=b.nextElementSibling){var e=a[b.namespaceURI];void 0!==e&&(e=e[b.localName])&&e.call(d,b,c)}}function O(a,b,c,d,e){d.push(a);al(b,c,d,e);return d.pop()}function Yk(a,b,c,d,e,f){for(var g=(void 0!==e?e:c).length,h,l,m=0;m<g;++m)h=c[m],void 0!==h&&(l=b.call(f,h,d,void 0!==e?e[m]:void 0),void 0!==l&&a[l.namespaceURI][l.localName].call(f,l,h,d))}function bl(a,b,c,d,e,f,g){e.push(a);Yk(b,c,d,e,f,g);e.pop()};function cl(a,b,c,d){return function(e,f,g){var h=new XMLHttpRequest;h.open("GET","function"===typeof a?a(e,f,g):a,!0);"arraybuffer"==b.X()&&(h.responseType="arraybuffer");h.onload=function(){if(200<=h.status&&300>h.status){var a=b.X(),e;"json"==a||"text"==a?e=h.responseText:"xml"==a?(e=h.responseXML)||(e=Rk(h.responseText)):"arraybuffer"==a&&(e=h.response);e&&c.call(this,b.Fa(e,{featureProjection:g}),b.Oa(e))}else d.call(this)}.bind(this);h.send()}}
+function dl(a,b){return cl(a,b,function(a,b){this.vf(b);this.gi(a)},function(){this.state=3;ef(this)})}function el(a,b){return cl(a,b,function(a){this.Jc(a)},na)};function fl(){return[[-Infinity,-Infinity,Infinity,Infinity]]};var gl,hl,il,jl;
+(function(){var a={},b={ja:a};(function(c){if("object"===typeof a&&"undefined"!==typeof b)b.ja=c();else{var d;"undefined"!==typeof window?d=window:"undefined"!==typeof global?d=global:"undefined"!==typeof self?d=self:d=this;d.Tp=c()}})(function(){return function d(a,b,g){function h(m,p){if(!b[m]){if(!a[m]){var q="function"==typeof require&&require;if(!p&&q)return q(m,!0);if(l)return l(m,!0);q=Error("Cannot find module '"+m+"'");throw q.code="MODULE_NOT_FOUND",q;}q=b[m]={ja:{}};a[m][0].call(q.ja,function(b){var d=
+a[m][1][b];return h(d?d:b)},q,q.ja,d,a,b,g)}return b[m].ja}for(var l="function"==typeof require&&require,m=0;m<g.length;m++)h(g[m]);return h}({1:[function(a,b){function f(a,b,d,e,q){d=d||0;e=e||a.length-1;for(q=q||h;e>d;){if(600<e-d){var r=e-d+1,u=b-d+1,x=Math.log(r),v=.5*Math.exp(2*x/3),x=.5*Math.sqrt(x*v*(r-v)/r)*(0>u-r/2?-1:1);f(a,b,Math.max(d,Math.floor(b-u*v/r+x)),Math.min(e,Math.floor(b+(r-u)*v/r+x)),q)}r=a[b];u=d;v=e;g(a,d,b);for(0<q(a[e],r)&&g(a,d,e);u<v;){g(a,u,v);u++;for(v--;0>q(a[u],r);)u++;
+for(;0<q(a[v],r);)v--}0===q(a[d],r)?g(a,d,v):(v++,g(a,v,e));v<=b&&(d=v+1);b<=v&&(e=v-1)}}function g(a,b,d){var e=a[b];a[b]=a[d];a[d]=e}function h(a,b){return a<b?-1:a>b?1:0}b.ja=f},{}],2:[function(a,b){function f(a,b){if(!(this instanceof f))return new f(a,b);this.Te=Math.max(4,a||9);this.hg=Math.max(2,Math.ceil(.4*this.Te));b&&this.mj(b);this.clear()}function g(a,b){h(a,0,a.children.length,b,a)}function h(a,b,d,e,f){f||(f=x(null));f.ca=Infinity;f.fa=Infinity;f.ea=-Infinity;f.ga=-Infinity;for(var g;b<
+d;b++)g=a.children[b],l(f,a.Ta?e(g):g);return f}function l(a,b){a.ca=Math.min(a.ca,b.ca);a.fa=Math.min(a.fa,b.fa);a.ea=Math.max(a.ea,b.ea);a.ga=Math.max(a.ga,b.ga)}function m(a,b){return a.ca-b.ca}function n(a,b){return a.fa-b.fa}function p(a){return(a.ea-a.ca)*(a.ga-a.fa)}function q(a){return a.ea-a.ca+(a.ga-a.fa)}function r(a,b){return a.ca<=b.ca&&a.fa<=b.fa&&b.ea<=a.ea&&b.ga<=a.ga}function u(a,b){return b.ca<=a.ea&&b.fa<=a.ga&&b.ea>=a.ca&&b.ga>=a.fa}function x(a){return{children:a,height:1,Ta:!0,
+ca:Infinity,fa:Infinity,ea:-Infinity,ga:-Infinity}}function v(a,b,d,e,f){for(var g=[b,d],h;g.length;)d=g.pop(),b=g.pop(),d-b<=e||(h=b+Math.ceil((d-b)/e/2)*e,D(a,h,b,d,f),g.push(b,h,h,d))}b.ja=f;var D=a("quickselect");f.prototype={all:function(){return this.cg(this.data,[])},search:function(a){var b=this.data,d=[],e=this.lb;if(!u(a,b))return d;for(var f=[],g,h,l,m;b;){g=0;for(h=b.children.length;g<h;g++)l=b.children[g],m=b.Ta?e(l):l,u(a,m)&&(b.Ta?d.push(l):r(a,m)?this.cg(l,d):f.push(l));b=f.pop()}return d},
+load:function(a){if(!a||!a.length)return this;if(a.length<this.hg){for(var b=0,d=a.length;b<d;b++)this.Ca(a[b]);return this}a=this.eg(a.slice(),0,a.length-1,0);this.data.children.length?this.data.height===a.height?this.jg(this.data,a):(this.data.height<a.height&&(b=this.data,this.data=a,a=b),this.gg(a,this.data.height-a.height-1,!0)):this.data=a;return this},Ca:function(a){a&&this.gg(a,this.data.height-1);return this},clear:function(){this.data=x([]);return this},remove:function(a,b){if(!a)return this;
+for(var d=this.data,e=this.lb(a),f=[],g=[],h,l,m,n;d||f.length;){d||(d=f.pop(),l=f[f.length-1],h=g.pop(),n=!0);if(d.Ta){a:{m=a;var p=d.children,q=b;if(q){for(var u=0;u<p.length;u++)if(q(m,p[u])){m=u;break a}m=-1}else m=p.indexOf(m)}if(-1!==m){d.children.splice(m,1);f.push(d);this.kj(f);break}}n||d.Ta||!r(d,e)?l?(h++,d=l.children[h],n=!1):d=null:(f.push(d),g.push(h),h=0,l=d,d=d.children[0])}return this},lb:function(a){return a},Ve:m,We:n,toJSON:function(){return this.data},cg:function(a,b){for(var d=
+[];a;)a.Ta?b.push.apply(b,a.children):d.push.apply(d,a.children),a=d.pop();return b},eg:function(a,b,d,e){var f=d-b+1,h=this.Te,l;if(f<=h)return l=x(a.slice(b,d+1)),g(l,this.lb),l;e||(e=Math.ceil(Math.log(f)/Math.log(h)),h=Math.ceil(f/Math.pow(h,e-1)));l=x([]);l.Ta=!1;l.height=e;var f=Math.ceil(f/h),h=f*Math.ceil(Math.sqrt(h)),m,n,p;for(v(a,b,d,h,this.Ve);b<=d;b+=h)for(n=Math.min(b+h-1,d),v(a,b,n,f,this.We),m=b;m<=n;m+=f)p=Math.min(m+f-1,n),l.children.push(this.eg(a,m,p,e-1));g(l,this.lb);return l},
+jj:function(a,b,d,e){for(var f,g,h,l,m,n,q,r;;){e.push(b);if(b.Ta||e.length-1===d)break;q=r=Infinity;f=0;for(g=b.children.length;f<g;f++)h=b.children[f],m=p(h),n=(Math.max(h.ea,a.ea)-Math.min(h.ca,a.ca))*(Math.max(h.ga,a.ga)-Math.min(h.fa,a.fa))-m,n<r?(r=n,q=m<q?m:q,l=h):n===r&&m<q&&(q=m,l=h);b=l||b.children[0]}return b},gg:function(a,b,d){var e=this.lb;d=d?a:e(a);var e=[],f=this.jj(d,this.data,b,e);f.children.push(a);for(l(f,d);0<=b;)if(e[b].children.length>this.Te)this.sj(e,b),b--;else break;this.gj(d,
+e,b)},sj:function(a,b){var d=a[b],e=d.children.length,f=this.hg;this.hj(d,f,e);e=this.ij(d,f,e);e=x(d.children.splice(e,d.children.length-e));e.height=d.height;e.Ta=d.Ta;g(d,this.lb);g(e,this.lb);b?a[b-1].children.push(e):this.jg(d,e)},jg:function(a,b){this.data=x([a,b]);this.data.height=a.height+1;this.data.Ta=!1;g(this.data,this.lb)},ij:function(a,b,d){var e,f,g,l,m,n,q;m=n=Infinity;for(e=b;e<=d-b;e++)f=h(a,0,e,this.lb),g=h(a,e,d,this.lb),l=Math.max(0,Math.min(f.ea,g.ea)-Math.max(f.ca,g.ca))*Math.max(0,
+Math.min(f.ga,g.ga)-Math.max(f.fa,g.fa)),f=p(f)+p(g),l<m?(m=l,q=e,n=f<n?f:n):l===m&&f<n&&(n=f,q=e);return q},hj:function(a,b,d){var e=a.Ta?this.Ve:m,f=a.Ta?this.We:n,g=this.dg(a,b,d,e);b=this.dg(a,b,d,f);g<b&&a.children.sort(e)},dg:function(a,b,d,e){a.children.sort(e);e=this.lb;var f=h(a,0,b,e),g=h(a,d-b,d,e),m=q(f)+q(g),n,p;for(n=b;n<d-b;n++)p=a.children[n],l(f,a.Ta?e(p):p),m+=q(f);for(n=d-b-1;n>=b;n--)p=a.children[n],l(g,a.Ta?e(p):p),m+=q(g);return m},gj:function(a,b,d){for(;0<=d;d--)l(b[d],a)},
+kj:function(a){for(var b=a.length-1,d;0<=b;b--)0===a[b].children.length?0<b?(d=a[b-1].children,d.splice(d.indexOf(a[b]),1)):this.clear():g(a[b],this.lb)},mj:function(a){var b=["return a"," - b",";"];this.Ve=new Function("a","b",b.join(a[0]));this.We=new Function("a","b",b.join(a[1]));this.lb=new Function("a","return {minX: a"+a[0]+", minY: a"+a[1]+", maxX: a"+a[2]+", maxY: a"+a[3]+"};")}}},{quickselect:1}]},{},[2])(2)});gl=b.ja})();function kl(a){this.a=gl(a);this.b={}}k=kl.prototype;k.Ca=function(a,b){var c={ca:a[0],fa:a[1],ea:a[2],ga:a[3],value:b};this.a.Ca(c);this.b[w(b)]=c};k.load=function(a,b){for(var c=Array(b.length),d=0,e=b.length;d<e;d++){var f=a[d],g=b[d],f={ca:f[0],fa:f[1],ea:f[2],ga:f[3],value:g};c[d]=f;this.b[w(g)]=f}this.a.load(c)};k.remove=function(a){a=w(a);var b=this.b[a];delete this.b[a];return null!==this.a.remove(b)};
+function ll(a,b,c){var d=w(c),d=a.b[d];$b([d.ca,d.fa,d.ea,d.ga],b)||(a.remove(c),a.Ca(b,c))}function ml(a){return a.a.all().map(function(a){return a.value})}function nl(a,b){return a.a.search({ca:b[0],fa:b[1],ea:b[2],ga:b[3]}).map(function(a){return a.value})}k.forEach=function(a,b){return pl(ml(this),a,b)};function ql(a,b,c,d){return pl(nl(a,b),c,d)}function pl(a,b,c){for(var d,e=0,f=a.length;e<f&&!(d=b.call(c,a[e]));e++);return d}k.Ya=function(){return Ha(this.b)};
+k.clear=function(){this.a.clear();this.b={}};k.H=function(){var a=this.a.data;return[a.ca,a.fa,a.ea,a.ga]};function P(a){a=a||{};jf.call(this,{attributions:a.attributions,logo:a.logo,projection:void 0,state:"ready",wrapX:void 0!==a.wrapX?a.wrapX:!0});this.S=na;this.qa=a.format;this.T=a.url;void 0!==a.loader?this.S=a.loader:void 0!==this.T&&(this.S=el(this.T,this.qa));this.qb=void 0!==a.strategy?a.strategy:fl;var b=void 0!==a.useSpatialIndex?a.useSpatialIndex:!0;this.a=b?new kl:null;this.Y=new kl;this.i={};this.o={};this.j={};this.s={};this.c=null;var c,d;a.features instanceof le?(c=a.features,d=c.a):Array.isArray(a.features)&&
+(d=a.features);b||void 0!==c||(c=new le(d));void 0!==d&&rl(this,d);void 0!==c&&sl(this,c)}y(P,jf);k=P.prototype;k.rb=function(a){var b=w(a).toString();if(tl(this,b,a)){ul(this,b,a);var c=a.W();c?(b=c.H(),this.a&&this.a.Ca(b,a)):this.i[b]=a;this.b(new vl("addfeature",a))}this.u()};function ul(a,b,c){a.s[b]=[B(c,"change",a.Eh,a),B(c,"propertychange",a.Eh,a)]}function tl(a,b,c){var d=!0,e=c.Xa();void 0!==e?e.toString()in a.o?d=!1:a.o[e.toString()]=c:a.j[b]=c;return d}k.Jc=function(a){rl(this,a);this.u()};
+function rl(a,b){var c,d,e,f,g=[],h=[],l=[];d=0;for(e=b.length;d<e;d++)f=b[d],c=w(f).toString(),tl(a,c,f)&&h.push(f);d=0;for(e=h.length;d<e;d++){f=h[d];c=w(f).toString();ul(a,c,f);var m=f.W();m?(c=m.H(),g.push(c),l.push(f)):a.i[c]=f}a.a&&a.a.load(g,l);d=0;for(e=h.length;d<e;d++)a.b(new vl("addfeature",h[d]))}
+function sl(a,b){var c=!1;B(a,"addfeature",function(a){c||(c=!0,b.push(a.feature),c=!1)});B(a,"removefeature",function(a){c||(c=!0,b.remove(a.feature),c=!1)});B(b,"add",function(a){c||(a=a.element,c=!0,this.rb(a),c=!1)},a);B(b,"remove",function(a){c||(a=a.element,c=!0,this.nb(a),c=!1)},a);a.c=b}
+k.clear=function(a){if(a){for(var b in this.s)this.s[b].forEach(Ka);this.c||(this.s={},this.o={},this.j={})}else if(this.a){this.a.forEach(this.Sf,this);for(var c in this.i)this.Sf(this.i[c])}this.c&&this.c.clear();this.a&&this.a.clear();this.Y.clear();this.i={};this.b(new vl("clear"));this.u()};k.wg=function(a,b){if(this.a)return this.a.forEach(a,b);if(this.c)return this.c.forEach(a,b)};function wl(a,b,c){a.ub([b[0],b[1],b[0],b[1]],function(a){if(a.W().sg(b))return c.call(void 0,a)})}
+k.ub=function(a,b,c){if(this.a)return ql(this.a,a,b,c);if(this.c)return this.c.forEach(b,c)};k.xg=function(a,b,c){return this.ub(a,function(d){if(d.W().Ka(a)&&(d=b.call(c,d)))return d})};k.Fg=function(){return this.c};k.oe=function(){var a;this.c?a=this.c.a:this.a&&(a=ml(this.a),Ha(this.i)||mb(a,Ga(this.i)));return a};k.Eg=function(a){var b=[];wl(this,a,function(a){b.push(a)});return b};k.ef=function(a){return nl(this.a,a)};
+k.Ag=function(a,b){var c=a[0],d=a[1],e=null,f=[NaN,NaN],g=Infinity,h=[-Infinity,-Infinity,Infinity,Infinity],l=b?b:qc;ql(this.a,h,function(a){if(l(a)){var b=a.W(),p=g;g=b.sb(c,d,f,g);g<p&&(e=a,a=Math.sqrt(g),h[0]=c-a,h[1]=d-a,h[2]=c+a,h[3]=d+a)}});return e};k.H=function(){return this.a.H()};k.Dg=function(a){a=this.o[a.toString()];return void 0!==a?a:null};k.Ch=function(){return this.qa};k.Dh=function(){return this.T};
+k.Eh=function(a){a=a.target;var b=w(a).toString(),c=a.W();c?(c=c.H(),b in this.i?(delete this.i[b],this.a&&this.a.Ca(c,a)):this.a&&ll(this.a,c,a)):b in this.i||(this.a&&this.a.remove(a),this.i[b]=a);c=a.Xa();void 0!==c?(c=c.toString(),b in this.j?(delete this.j[b],this.o[c]=a):this.o[c]!==a&&(xl(this,a),this.o[c]=a)):b in this.j||(xl(this,a),this.j[b]=a);this.u();this.b(new vl("changefeature",a))};k.Ya=function(){return this.a.Ya()&&Ha(this.i)};
+k.Pc=function(a,b,c){var d=this.Y;a=this.qb(a,b);var e,f;e=0;for(f=a.length;e<f;++e){var g=a[e];ql(d,g,function(a){return Ub(a.extent,g)})||(this.S.call(this,g,b,c),d.Ca(g,{extent:g.slice()}))}};k.nb=function(a){var b=w(a).toString();b in this.i?delete this.i[b]:this.a&&this.a.remove(a);this.Sf(a);this.u()};k.Sf=function(a){var b=w(a).toString();this.s[b].forEach(Ka);delete this.s[b];var c=a.Xa();void 0!==c?delete this.o[c.toString()]:delete this.j[b];this.b(new vl("removefeature",a))};
+function xl(a,b){for(var c in a.o)if(a.o[c]===b){delete a.o[c];break}}function vl(a,b){Wa.call(this,a);this.feature=b}y(vl,Wa);function yl(a){this.c=a.source;this.Aa=Xc();this.i=Oe();this.j=[0,0];this.v=null;Hk.call(this,{attributions:a.attributions,canvasFunction:this.Dj.bind(this),logo:a.logo,projection:a.projection,ratio:a.ratio,resolutions:a.resolutions,state:this.c.V()});this.S=null;this.s=void 0;this.xh(a.style);B(this.c,"change",this.en,this)}y(yl,Hk);k=yl.prototype;
+k.Dj=function(a,b,c,d,e){var f=new dk(.5*b/c,a,b);this.c.Pc(a,b,e);var g=!1;this.c.ub(a,function(a){var d;if(!(d=g)){var e;(d=a.ec())?e=d.call(a,b):this.s&&(e=this.s(a,b));if(e){var n,p=!1;Array.isArray(e)||(e=[e]);d=0;for(n=e.length;d<n;++d)p=lk(f,a,e[d],jk(b,c),this.dn,this)||p;d=p}else d=!1}g=d},this);ek(f);if(g)return null;this.j[0]!=d[0]||this.j[1]!=d[1]?(this.i.canvas.width=d[0],this.i.canvas.height=d[1],this.j[0]=d[0],this.j[1]=d[1]):this.i.clearRect(0,0,d[0],d[1]);a=zl(this,kc(a),b,c,d);f.Pa(this.i,
+c,a,0,{});this.v=f;return this.i.canvas};k.ra=function(a,b,c,d,e){if(this.v){var f={};return this.v.ra(a,b,0,d,function(a){var b=w(a).toString();if(!(b in f))return f[b]=!0,e(a)})}};k.an=function(){return this.c};k.bn=function(){return this.S};k.cn=function(){return this.s};function zl(a,b,c,d,e){return qh(a.Aa,e[0]/2,e[1]/2,d/c,-d/c,0,-b[0],-b[1])}k.dn=function(){this.u()};k.en=function(){lf(this,this.c.V())};k.xh=function(a){this.S=void 0!==a?a:vj;this.s=a?tj(this.S):void 0;this.u()};function Al(a){Jj.call(this,a);this.f=null;this.s=Xc();this.o=this.c=null}y(Al,Jj);Al.prototype.ra=function(a,b,c,d){var e=this.a;return e.ha().ra(a,b.viewState.resolution,b.viewState.rotation,b.skippedFeatureUids,function(a){return c.call(d,a,e)})};
+Al.prototype.Cc=function(a,b,c,d){if(this.f&&this.f.a())if(this.a.ha()instanceof yl){if(a=a.slice(),sh(b.pixelToCoordinateMatrix,a,a),this.ra(a,b,qc,this))return c.call(d,this.a)}else if(this.c||(this.c=Xc(),cd(this.s,this.c)),b=[0,0],sh(this.c,a,b),this.o||(this.o=Oe(1,1)),this.o.clearRect(0,0,1,1),this.o.drawImage(this.f?this.f.a():null,b[0],b[1],1,1,0,0,1,1),0<this.o.getImageData(0,0,1,1).data[3])return c.call(d,this.a)};
+Al.prototype.l=function(a,b){var c=a.pixelRatio,d=a.viewState,e=d.center,f=d.resolution,g=this.a.ha(),h=a.viewHints,l=a.extent;void 0!==b.extent&&(l=mc(l,b.extent));h[0]||h[1]||hc(l)||(d=g.A(l,f,c,d.projection))&&vh(this,d)&&(this.f=d);if(this.f){var d=this.f,h=d.H(),l=d.$(),m=d.f,f=c*l/(f*m);qh(this.s,c*a.size[0]/2,c*a.size[1]/2,f,f,0,m*(h[0]-e[0])/l,m*(e[1]-h[3])/l);this.c=null;xh(a.attributions,d.l);yh(a,g)}return!!this.f};function Bl(a){Jj.call(this,a);this.c=Oe();this.o=null;this.j=Lb();this.S=[0,0,0];this.D=Xc();this.C=0}y(Bl,Jj);Bl.prototype.i=function(a,b,c){var d=Mj(this,a,0);Kj(this,"precompose",c,a,d);Cl(this,c,a,b);Lj(this,c,a,d)};
+Bl.prototype.l=function(a,b){function c(a){a=a.V();return 2==a||4==a||3==a&&!r}var d=a.pixelRatio,e=a.viewState,f=e.projection,g=this.a,h=g.ha(),l=h.eb(f),m=l.Lb(e.resolution,this.C),n=l.$(m),p=e.center;n==e.resolution?(p=Ah(p,n,a.size),e=lc(p,n,e.rotation,a.size)):e=a.extent;void 0!==b.extent&&(e=mc(e,b.extent));if(hc(e))return!1;n=sf(l,e,n);p={};p[m]={};var q=this.Qd(h,f,p),r=g.c(),u=Lb(),x=new fe(0,0,0,0),v,D,A,z;for(A=n.ca;A<=n.ea;++A)for(z=n.fa;z<=n.ga;++z)v=h.ac(m,A,z,d,f),!c(v)&&v.a&&(v=v.a),
+c(v)?p[m][v.ma.toString()]=v:(D=qf(l,v.ma,q,x,u),D||(v=rf(l,v.ma,x,u))&&q(m+1,v));q=Object.keys(p).map(Number);q.sort(ib);var u=[],F,x=0;for(A=q.length;x<A;++x)for(F in v=q[x],z=p[v],z)v=z[F],2==v.V()&&u.push(v);this.o=u;zh(a.usedTiles,h,m,n);Bh(a,h,l,d,f,e,m,g.f());wh(a,h);yh(a,h);return!0};Bl.prototype.Cc=function(a,b,c,d){var e=this.c.canvas,f=b.size;e.width=f[0];e.height=f[1];this.i(b,jh(this.a),this.c);if(0<this.c.getImageData(a[0],a[1],1,1).data[3])return c.call(d,this.a)};
+function Cl(a,b,c,d){var e=c.pixelRatio,f=c.viewState,g=f.center,h=f.projection,l=f.resolution,f=f.rotation,m=c.size,n=Math.round(e*m[0]/2),p=Math.round(e*m[1]/2),m=e/l,q=a.a,r=q.ha(),u=r.Ud(h),x=r.eb(h),q=ab(q,"render"),v=b,D,A,z,F;if(f||q)v=a.c,D=v.canvas,F=x.Lb(l),z=r.$d(F,e,h),F=hf(x.Ja(F)),z=z[0]/F[0],l=b.canvas.width*z,A=b.canvas.height*z,F=Math.round(Math.sqrt(l*l+A*A)),D.width!=F?D.width=D.height=F:v.clearRect(0,0,F,F),D=(F-l)/2/z,A=(F-A)/2/z,m*=z,n=Math.round(z*(n+D)),p=Math.round(z*(p+A));
+l=v.globalAlpha;v.globalAlpha=d.opacity;var N=a.o,K,X=r.jf(h)&&1==d.opacity;X||(N.reverse(),K=[]);var oa=d.extent;if(d=void 0!==oa){var H=fc(oa),ya=ec(oa),Ua=dc(oa),oa=cc(oa);sh(c.coordinateToPixelMatrix,H,H);sh(c.coordinateToPixelMatrix,ya,ya);sh(c.coordinateToPixelMatrix,Ua,Ua);sh(c.coordinateToPixelMatrix,oa,oa);var Xa=D||0,Va=A||0;v.save();var Aa=v.canvas.width*e/2,Qb=v.canvas.height*e/2;hj(v,-f,Aa,Qb);v.beginPath();v.moveTo(H[0]*e+Xa,H[1]*e+Va);v.lineTo(ya[0]*e+Xa,ya[1]*e+Va);v.lineTo(Ua[0]*
+e+Xa,Ua[1]*e+Va);v.lineTo(oa[0]*e+Xa,oa[1]*e+Va);v.clip();hj(v,f,Aa,Qb)}H=0;for(ya=N.length;H<ya;++H){var Ua=N[H],oa=Ua.ma,Qb=x.Ea(oa,a.j),Aa=oa[0],Nb=cc(x.Ea(x.qd(g,Aa,a.S))),oa=Math.round(ic(Qb)*m),Xa=Math.round(jc(Qb)*m),Va=Math.round((Qb[0]-Nb[0])*m/oa)*oa+n+Math.round((Nb[0]-g[0])*m),Qb=Math.round((Nb[1]-Qb[3])*m/Xa)*Xa+p+Math.round((g[1]-Nb[1])*m);if(!X){Nb=[Va,Qb,Va+oa,Qb+Xa];v.save();for(var kk=0,vs=K.length;kk<vs;++kk){var De=K[kk];nc(Nb,De)&&(v.beginPath(),v.moveTo(Nb[0],Nb[1]),v.lineTo(Nb[0],
+Nb[3]),v.lineTo(Nb[2],Nb[3]),v.lineTo(Nb[2],Nb[1]),v.moveTo(De[0],De[1]),v.lineTo(De[2],De[1]),v.lineTo(De[2],De[3]),v.lineTo(De[0],De[3]),v.closePath(),v.clip())}K.push(Nb)}Aa=r.$d(Aa,e,h);v.drawImage(Ua.$a(),u,u,Aa[0],Aa[1],Va,Qb,oa,Xa);X||v.restore()}d&&v.restore();q&&(e=D-n/z+n,h=A-p/z+p,g=qh(a.D,F/2-e,F/2-h,m,-m,-f,-g[0]+e/m,-g[1]-h/m),Kj(a,"render",v,c,g));(f||q)&&b.drawImage(v.canvas,-Math.round(D),-Math.round(A),F/z,F/z);v.globalAlpha=l};function Dl(a){Jj.call(this,a);this.c=!1;this.C=-1;this.A=NaN;this.v=Lb();this.o=this.U=null;this.j=Oe()}y(Dl,Jj);
+Dl.prototype.i=function(a,b,c){var d=a.extent,e=a.pixelRatio,f=b.Qc?a.skippedFeatureUids:{},g=a.viewState,h=g.projection,g=g.rotation,l=h.H(),m=this.a.ha(),n=Mj(this,a,0);Kj(this,"precompose",c,a,n);var p=this.o;if(p&&!p.Ya()){var q;ab(this.a,"render")?(this.j.canvas.width=c.canvas.width,this.j.canvas.height=c.canvas.height,q=this.j):q=c;var r=q.globalAlpha;q.globalAlpha=b.opacity;b=a.size[0]*e;var u=a.size[1]*e;hj(q,-g,b/2,u/2);p.Pa(q,e,n,g,f);if(m.D&&h.a&&!Ub(l,d)){for(var h=d[0],m=ic(l),x=0;h<
+l[0];)--x,n=m*x,n=Mj(this,a,n),p.Pa(q,e,n,g,f),h+=m;x=0;for(h=d[2];h>l[2];)++x,n=m*x,n=Mj(this,a,n),p.Pa(q,e,n,g,f),h-=m;n=Mj(this,a,0)}hj(q,g,b/2,u/2);q!=c&&(Kj(this,"render",q,a,n),c.drawImage(q.canvas,0,0));q.globalAlpha=r}Lj(this,c,a,n)};Dl.prototype.ra=function(a,b,c,d){if(this.o){var e=this.a,f={};return this.o.ra(a,b.viewState.resolution,b.viewState.rotation,{},function(a){var b=w(a).toString();if(!(b in f))return f[b]=!0,c.call(d,a,e)})}};Dl.prototype.D=function(){uh(this)};
+Dl.prototype.l=function(a){function b(a){var b,d=a.ec();d?b=d.call(a,m):(d=c.i)&&(b=d(a,m));if(b){if(b){d=!1;if(Array.isArray(b))for(var e=0,f=b.length;e<f;++e)d=lk(q,a,b[e],jk(m,n),this.D,this)||d;else d=lk(q,a,b,jk(m,n),this.D,this)||d;a=d}else a=!1;this.c=this.c||a}}var c=this.a,d=c.ha();xh(a.attributions,d.l);yh(a,d);var e=a.viewHints[0],f=a.viewHints[1],g=c.S,h=c.T;if(!this.c&&!g&&e||!h&&f)return!0;var l=a.extent,h=a.viewState,e=h.projection,m=h.resolution,n=a.pixelRatio,f=c.g,p=c.a,g=xj(c);
+void 0===g&&(g=ik);l=Ob(l,p*m);p=h.projection.H();d.D&&h.projection.a&&!Ub(p,a.extent)&&(a=Math.max(ic(l)/2,ic(p)),l[0]=p[0]-a,l[2]=p[2]+a);if(!this.c&&this.A==m&&this.C==f&&this.U==g&&Ub(this.v,l))return!0;this.o=null;this.c=!1;var q=new dk(.5*m/n,l,m,c.a);d.Pc(l,m,e);if(g){var r=[];d.ub(l,function(a){r.push(a)},this);r.sort(g);r.forEach(b,this)}else d.ub(l,b,this);ek(q);this.A=m;this.C=f;this.U=g;this.v=l;this.o=q;return!0};function El(a,b){var c=/\{z\}/g,d=/\{x\}/g,e=/\{y\}/g,f=/\{-y\}/g;return function(g){if(g)return a.replace(c,g[0].toString()).replace(d,g[1].toString()).replace(e,function(){return(-g[2]-1).toString()}).replace(f,function(){var a=b.a?b.a[g[0]]:null;return(a.ga-a.fa+1+g[2]).toString()})}}function Fl(a,b){for(var c=a.length,d=Array(c),e=0;e<c;++e)d[e]=El(a[e],b);return Gl(d)}function Gl(a){return 1===a.length?a[0]:function(b,c,d){if(b)return a[xa((b[1]<<b[0])+b[2],a.length)](b,c,d)}}
+function Hl(){}function Il(a){var b=[],c=/\{(\d)-(\d)\}/.exec(a)||/\{([a-z])-([a-z])\}/.exec(a);if(c){var d=c[2].charCodeAt(0),e;for(e=c[1].charCodeAt(0);e<=d;++e)b.push(a.replace(c[0],String.fromCharCode(e)))}else b.push(a);return b};function Jl(a){zf.call(this,{attributions:a.attributions,cacheSize:a.cacheSize,extent:a.extent,logo:a.logo,opaque:a.opaque,projection:a.projection,state:a.state,tileGrid:a.tileGrid,tilePixelRatio:a.tilePixelRatio,wrapX:a.wrapX});this.tileLoadFunction=a.tileLoadFunction;this.tileUrlFunction=this.vc?this.vc.bind(this):Hl;this.urls=null;a.urls?this.bb(a.urls):a.url&&this.Va(a.url);a.tileUrlFunction&&this.Qa(a.tileUrlFunction)}y(Jl,zf);k=Jl.prototype;k.fb=function(){return this.tileLoadFunction};
+k.gb=function(){return this.tileUrlFunction};k.hb=function(){return this.urls};k.Bh=function(a){a=a.target;switch(a.V()){case 1:this.b(new Df("tileloadstart",a));break;case 2:this.b(new Df("tileloadend",a));break;case 3:this.b(new Df("tileloaderror",a))}};k.kb=function(a){this.a.clear();this.tileLoadFunction=a;this.u()};k.Qa=function(a,b){this.tileUrlFunction=a;"undefined"!==typeof b?Bf(this,b):this.u()};
+k.Va=function(a){var b=this.urls=Il(a);this.Qa(this.vc?this.vc.bind(this):Fl(b,this.tileGrid),a)};k.bb=function(a){this.urls=a;var b=a.join("\n");this.Qa(this.vc?this.vc.bind(this):Fl(a,this.tileGrid),b)};k.Yf=function(a,b,c){a=this.Eb(a,b,c);Ze(this.a,a)&&this.a.get(a)};function Kl(a){Jl.call(this,{attributions:a.attributions,cacheSize:void 0!==a.cacheSize?a.cacheSize:128,extent:a.extent,logo:a.logo,opaque:!1,projection:a.projection,state:a.state,tileGrid:a.tileGrid,tileLoadFunction:a.tileLoadFunction?a.tileLoadFunction:Ll,tileUrlFunction:a.tileUrlFunction,tilePixelRatio:a.tilePixelRatio,url:a.url,urls:a.urls,wrapX:void 0===a.wrapX?!0:a.wrapX});this.c=a.format?a.format:null;this.tileClass=a.tileClass?a.tileClass:Kk}y(Kl,Jl);
+Kl.prototype.ac=function(a,b,c,d,e){var f=this.Eb(a,b,c);if(Ze(this.a,f))return this.a.get(f);a=[a,b,c];d=(b=Cf(this,a,e))?this.tileUrlFunction(b,d,e):void 0;d=new this.tileClass(a,void 0!==d?0:4,void 0!==d?d:"",this.c,this.tileLoadFunction);B(d,"change",this.Bh,this);this.a.set(f,d);return d};Kl.prototype.$d=function(a,b){var c=hf(this.tileGrid.Ja(a));return[c[0]*b,c[1]*b]};function Ll(a,b){a.ki(dl(b,a.l))};var Ml={image:Nj,hybrid:["Polygon","LineString"]},Nl={hybrid:["Image","Text"],vector:Nj};function Ol(a){Bl.call(this,a);this.U=!1;this.v=Xc();this.C="vector"==a.s?1:0}y(Ol,Bl);
+Ol.prototype.i=function(a,b,c){var d=Mj(this,a,0);Kj(this,"precompose",c,a,d);var e=this.a.s;"vector"!==e&&Cl(this,c,a,b);if("image"!==e){var f=this.a,e=Nl[f.s],g=a.pixelRatio,h=b.Qc?a.skippedFeatureUids:{},l=a.viewState,m=l.center,n=l.rotation,p=a.size,l=g/l.resolution,q=f.ha(),r=q.bc(g),u=Mj(this,a,0);ab(f,"render")?(this.c.canvas.width=c.canvas.width,this.c.canvas.height=c.canvas.height,f=this.c):f=c;var x=f.globalAlpha;f.globalAlpha=b.opacity;b=this.o;var q=q.tileGrid,v,D,A,z,F,N,K,X;D=0;for(A=
+b.length;D<A;++D)z=b[D],K=z.f,F=q.Ea(z.ma,this.j),v=z.ma[0],N="tile-pixels"==z.o.wb(),v=q.$(v),X=v/r,v=Math.round(g*p[0]/2),z=Math.round(g*p[1]/2),N?(F=fc(F),F=qh(this.v,v,z,l*X,l*X,n,(F[0]-m[0])/X,(m[1]-F[1])/X)):F=u,hj(f,-n,v,z),K.yd.Pa(f,g,F,n,h,e),hj(f,n,v,z);f!=c&&(Kj(this,"render",f,a,u),c.drawImage(f.canvas,0,0));f.globalAlpha=x}Lj(this,c,a,d)};
+function Pl(a,b,c){function d(a){var b,c=a.ec();c?b=c.call(a,u):(c=e.i)&&(b=c(a,u));if(b){Array.isArray(b)||(b=[b]);var c=D,d=v;if(b){var f=!1;if(Array.isArray(b))for(var g=0,h=b.length;g<h;++g)f=lk(d,a,b[g],c,this.A,this)||f;else f=lk(d,a,b,c,this.A,this)||f;a=f}else a=!1;this.U=this.U||a;l.gd=l.gd||a}}var e=a.a,f=c.pixelRatio;c=c.viewState.projection;var g=e.g,h=xj(e)||null,l=b.f;if(l.gd||l.bi!=g||l.Tf!=h){l.yd=null;l.gd=!1;var m=e.ha(),n=m.tileGrid,p=b.ma,q=b.o,r="tile-pixels"==q.wb(),u=n.$(p[0]),
+x;r?(r=m=m.bc(f),n=hf(n.Ja(p[0])),n=[0,0,n[0]*r,n[1]*r]):(m=u,n=n.Ea(p),Oc(c,q)||(x=!0,b.vf(c)));l.gd=!1;var v=new dk(0,n,m,e.a),D=jk(m,f);b=b.c;h&&h!==l.Tf&&b.sort(h);n=0;for(p=b.length;n<p;++n)f=b[n],x&&f.W().jb(q,c),d.call(a,f);ek(v);l.bi=g;l.Tf=h;l.yd=v;l.resolution=NaN}}
+Ol.prototype.ra=function(a,b,c,d){var e=b.pixelRatio,f=b.viewState.resolution;b=b.viewState.rotation;var g=this.a,h={},l=this.o,m=g.ha(),n=m.tileGrid,p,q,r,u,x,v;r=0;for(u=l.length;r<u;++r)v=l[r],q=v.ma,x=m.tileGrid.Ea(q,this.j),Sb(x,a)&&("tile-pixels"===v.o.wb()?(x=fc(x),f=m.bc(e),q=n.$(q[0])/f,q=[(a[0]-x[0])/q,(x[1]-a[1])/q]):q=a,v=v.f.yd,p=p||v.ra(q,f,b,{},function(a){var b=w(a).toString();if(!(b in h))return h[b]=!0,c.call(d,a,g)}));return p};Ol.prototype.A=function(){uh(this)};
+Ol.prototype.l=function(a,b){var c=Bl.prototype.l.call(this,a,b);if(c)for(var d=Object.keys(a.Ee||{}),e=0,f=this.o.length;e<f;++e){var g=this.o[e];Pl(this,g,a);var h=g,g=a,l=this.a,m=Ml[l.s];if(m){var n=g.pixelRatio,p=h.f,q=l.g;if(!pb(p.ui,d)||p.Uf!==q){p.ui=d;p.Uf=q;var q=h.g,r=l.ha(),u=r.tileGrid,x=h.ma[0],v=u.$(x),l=hf(u.Ja(x)),x=u.$(x),D=x/v,A=l[0]*n*D,z=l[1]*n*D;q.canvas.width=A/D+.5;q.canvas.height=z/D+.5;q.scale(1/D,1/D);q.translate(A/2,z/2);D="tile-pixels"==h.o.wb();v=n/v;r=r.bc(n);x/=r;h=
+u.Ea(h.ma,this.j);D?h=qh(this.v,0,0,v*x,v*x,0,-l[0]*r/2,-l[1]*r/2):(h=kc(h),h=qh(this.v,0,0,v,-v,0,-h[0],-h[1]));p.yd.Pa(q,n,h,0,g.skippedFeatureUids||{},m)}}}return c};function Ql(a,b){Hh.call(this,0,b);this.f=Oe();this.b=this.f.canvas;this.b.style.width="100%";this.b.style.height="100%";this.b.className="ol-unselectable";a.insertBefore(this.b,a.childNodes[0]||null);this.a=!0;this.c=Xc()}y(Ql,Hh);Ql.prototype.Xe=function(a){return a instanceof cj?new Al(a):a instanceof dj?new Bl(a):a instanceof I?new Ol(a):a instanceof G?new Dl(a):null};
+function Rl(a,b,c){var d=a.i,e=a.f;if(ab(d,b)){var f=c.extent,g=c.pixelRatio,h=c.viewState.rotation,l=c.pixelRatio,m=c.viewState,n=m.resolution;a=qh(a.c,a.b.width/2,a.b.height/2,l/n,-l/n,-m.rotation,-m.center[0],-m.center[1]);f=new yj(e,g,f,a,h);d.b(new lh(b,d,f,c,e,null))}}Ql.prototype.X=function(){return"canvas"};
+Ql.prototype.Ce=function(a){if(a){var b=this.f,c=a.pixelRatio,d=Math.round(a.size[0]*c),c=Math.round(a.size[1]*c);this.b.width!=d||this.b.height!=c?(this.b.width=d,this.b.height=c):b.clearRect(0,0,d,c);var e=a.viewState.rotation;Ih(a);Rl(this,"precompose",a);var f=a.layerStatesArray;qb(f);hj(b,e,d/2,c/2);var g=a.viewState.resolution,h,l,m,n;h=0;for(l=f.length;h<l;++h)n=f[h],m=n.layer,m=Kh(this,m),nh(n,g)&&"ready"==n.R&&m.l(a,n)&&m.i(a,n,b);hj(b,-e,d/2,c/2);Rl(this,"postcompose",a);this.a||(this.b.style.display=
+"",this.a=!0);Lh(this,a);a.postRenderFunctions.push(Jh)}else this.a&&(this.b.style.display="none",this.a=!1)};function Sl(a,b){th.call(this,a);this.target=b}y(Sl,th);Sl.prototype.Nd=na;Sl.prototype.th=na;function Tl(a){var b=document.createElement("DIV");b.style.position="absolute";Sl.call(this,a,b);this.f=null;this.c=Zc()}y(Tl,Sl);Tl.prototype.ra=function(a,b,c,d){var e=this.a;return e.ha().ra(a,b.viewState.resolution,b.viewState.rotation,b.skippedFeatureUids,function(a){return c.call(d,a,e)})};Tl.prototype.Nd=function(){Ve(this.target);this.f=null};
+Tl.prototype.yf=function(a,b){var c=a.viewState,d=c.center,e=c.resolution,f=c.rotation,g=this.f,h=this.a.ha(),l=a.viewHints,m=a.extent;void 0!==b.extent&&(m=mc(m,b.extent));l[0]||l[1]||hc(m)||(c=h.A(m,e,a.pixelRatio,c.projection))&&vh(this,c)&&(g=c);g&&(l=g.H(),m=g.$(),c=Xc(),qh(c,a.size[0]/2,a.size[1]/2,m/e,m/e,f,(l[0]-d[0])/m,(d[1]-l[3])/m),g!=this.f&&(d=g.a(this),d.style.maxWidth="none",d.style.position="absolute",Ve(this.target),this.target.appendChild(d),this.f=g),rh(c,this.c)||(Se(this.target,
+c),$c(this.c,c)),xh(a.attributions,g.l),yh(a,h));return!0};function Ul(a){var b=document.createElement("DIV");b.style.position="absolute";Sl.call(this,a,b);this.c=!0;this.l=1;this.i=0;this.f={}}y(Ul,Sl);Ul.prototype.Nd=function(){Ve(this.target);this.i=0};
+Ul.prototype.yf=function(a,b){if(!b.visible)return this.c&&(this.target.style.display="none",this.c=!1),!0;var c=a.pixelRatio,d=a.viewState,e=d.projection,f=this.a,g=f.ha(),h=g.eb(e),l=g.Ud(e),m=h.Lb(d.resolution),n=h.$(m),p=d.center,q;n==d.resolution?(p=Ah(p,n,a.size),q=lc(p,n,d.rotation,a.size)):q=a.extent;void 0!==b.extent&&(q=mc(q,b.extent));var n=sf(h,q,n),r={};r[m]={};var u=this.Qd(g,e,r),x=f.c(),v=Lb(),D=new fe(0,0,0,0),A,z,F,N;for(F=n.ca;F<=n.ea;++F)for(N=n.fa;N<=n.ga;++N)A=g.ac(m,F,N,c,e),
+z=A.V(),z=2==z||4==z||3==z&&!x,!z&&A.a&&(A=A.a),z=A.V(),2==z?r[m][A.ma.toString()]=A:4==z||3==z&&!x||(z=qf(h,A.ma,u,D,v),z||(A=rf(h,A.ma,D,v))&&u(m+1,A));var K;if(this.i!=g.g){for(K in this.f)x=this.f[+K],Ue(x.target);this.f={};this.i=g.g}v=Object.keys(r).map(Number);v.sort(ib);var u={},X;F=0;for(N=v.length;F<N;++F){K=v[F];K in this.f?x=this.f[K]:(x=h.qd(p,K),x=new Vl(h,x),u[K]=!0,this.f[K]=x);K=r[K];for(X in K){A=x;z=K[X];var oa=l,H=z.ma,ya=H[0],Ua=H[1],Xa=H[2],H=H.toString();if(!(H in A.a)){var ya=
+hf(A.c.Ja(ya),A.o),Va=z.$a(A),Aa=Va.style;Aa.maxWidth="none";var Qb,Nb;0<oa?(Qb=document.createElement("DIV"),Nb=Qb.style,Nb.overflow="hidden",Nb.width=ya[0]+"px",Nb.height=ya[1]+"px",Aa.position="absolute",Aa.left=-oa+"px",Aa.top=-oa+"px",Aa.width=ya[0]+2*oa+"px",Aa.height=ya[1]+2*oa+"px",Qb.appendChild(Va)):(Aa.width=ya[0]+"px",Aa.height=ya[1]+"px",Qb=Va,Nb=Aa);Nb.position="absolute";Nb.left=(Ua-A.g[1])*ya[0]+"px";Nb.top=(A.g[2]-Xa)*ya[1]+"px";A.b||(A.b=document.createDocumentFragment());A.b.appendChild(Qb);
+A.a[H]=z}}x.b&&(x.target.appendChild(x.b),x.b=null)}l=Object.keys(this.f).map(Number);l.sort(ib);F=Xc();X=0;for(v=l.length;X<v;++X)if(K=l[X],x=this.f[K],K in r)if(A=x.$(),N=x.Ia(),qh(F,a.size[0]/2,a.size[1]/2,A/d.resolution,A/d.resolution,d.rotation,(N[0]-p[0])/A,(p[1]-N[1])/A),x.setTransform(F),K in u){for(--K;0<=K;--K)if(K in this.f){this.f[K].target.parentNode&&this.f[K].target.parentNode.insertBefore(x.target,this.f[K].target.nextSibling);break}0>K&&this.target.insertBefore(x.target,this.target.childNodes[0]||
+null)}else{if(!a.viewHints[0]&&!a.viewHints[1]){z=pf(x.c,q,x.g[0],D);K=[];A=void 0;for(A in x.a)N=x.a[A],z.contains(N.ma)||K.push(N);z=0;for(oa=K.length;z<oa;++z)N=K[z],A=N.ma.toString(),Ue(N.$a(x)),delete x.a[A]}}else Ue(x.target),delete this.f[K];b.opacity!=this.l&&(this.l=this.target.style.opacity=b.opacity);b.visible&&!this.c&&(this.target.style.display="",this.c=!0);zh(a.usedTiles,g,m,n);Bh(a,g,h,c,e,q,m,f.f());wh(a,g);yh(a,g);return!0};
+function Vl(a,b){this.target=document.createElement("DIV");this.target.style.position="absolute";this.target.style.width="100%";this.target.style.height="100%";this.c=a;this.g=b;this.i=fc(a.Ea(b));this.l=a.$(b[0]);this.a={};this.b=null;this.f=Zc();this.o=[0,0]}Vl.prototype.Ia=function(){return this.i};Vl.prototype.$=function(){return this.l};Vl.prototype.setTransform=function(a){rh(a,this.f)||(Se(this.target,a),$c(this.f,a))};function Wl(a){this.i=Oe();var b=this.i.canvas;b.style.maxWidth="none";b.style.position="absolute";Sl.call(this,a,b);this.f=!1;this.l=-1;this.s=NaN;this.o=Lb();this.c=this.j=null;this.U=Xc();this.v=Xc()}y(Wl,Sl);k=Wl.prototype;k.Nd=function(){var a=this.i.canvas;a.width=a.width;this.l=0};
+k.th=function(a,b){var c=a.viewState,d=c.center,e=c.rotation,f=c.resolution,c=a.pixelRatio,g=a.size[0],h=a.size[1],l=g*c,m=h*c,d=qh(this.U,c*g/2,c*h/2,c/f,-c/f,-e,-d[0],-d[1]),f=this.i;f.canvas.width=l;f.canvas.height=m;g=qh(this.v,0,0,1/c,1/c,0,-(l-g)/2*c,-(m-h)/2*c);Se(f.canvas,g);Xl(this,"precompose",a,d);(g=this.c)&&!g.Ya()&&(f.globalAlpha=b.opacity,g.Pa(f,c,d,e,b.Qc?a.skippedFeatureUids:{}),Xl(this,"render",a,d));Xl(this,"postcompose",a,d)};
+function Xl(a,b,c,d){var e=a.i;a=a.a;ab(a,b)&&(d=new yj(e,c.pixelRatio,c.extent,d,c.viewState.rotation),a.b(new lh(b,a,d,c,e,null)))}k.ra=function(a,b,c,d){if(this.c){var e=this.a,f={};return this.c.ra(a,b.viewState.resolution,b.viewState.rotation,{},function(a){var b=w(a).toString();if(!(b in f))return f[b]=!0,c.call(d,a,e)})}};k.uh=function(){uh(this)};
+k.yf=function(a){function b(a){var b,d=a.ec();d?b=d.call(a,l):(d=c.i)&&(b=d(a,l));if(b){if(b){d=!1;if(Array.isArray(b))for(var e=0,f=b.length;e<f;++e)d=lk(n,a,b[e],jk(l,m),this.uh,this)||d;else d=lk(n,a,b,jk(l,m),this.uh,this)||d;a=d}else a=!1;this.f=this.f||a}}var c=this.a,d=c.ha();xh(a.attributions,d.l);yh(a,d);var e=a.viewHints[0],f=a.viewHints[1],g=c.S,h=c.T;if(!this.f&&!g&&e||!h&&f)return!0;var f=a.extent,g=a.viewState,e=g.projection,l=g.resolution,m=a.pixelRatio;a=c.g;h=c.a;g=xj(c);void 0===
+g&&(g=ik);f=Ob(f,h*l);if(!this.f&&this.s==l&&this.l==a&&this.j==g&&Ub(this.o,f))return!0;this.c=null;this.f=!1;var n=new dk(.5*l/m,f,l,c.a);d.Pc(f,l,e);if(g){var p=[];d.ub(f,function(a){p.push(a)},this);p.sort(g);p.forEach(b,this)}else d.ub(f,b,this);ek(n);this.s=l;this.l=a;this.j=g;this.o=f;this.c=n;return!0};function Yl(a,b){Hh.call(this,0,b);this.f=Oe();var c=this.f.canvas;c.style.position="absolute";c.style.width="100%";c.style.height="100%";c.className="ol-unselectable";a.insertBefore(c,a.childNodes[0]||null);this.c=Xc();this.b=document.createElement("DIV");this.b.className="ol-unselectable";c=this.b.style;c.position="absolute";c.width="100%";c.height="100%";B(this.b,"touchstart",Za);a.insertBefore(this.b,a.childNodes[0]||null);this.a=!0}y(Yl,Hh);Yl.prototype.ka=function(){Ue(this.b);Hh.prototype.ka.call(this)};
+Yl.prototype.Xe=function(a){if(a instanceof cj)a=new Tl(a);else if(a instanceof dj)a=new Ul(a);else if(a instanceof G)a=new Wl(a);else return null;return a};function Zl(a,b,c){var d=a.i;if(ab(d,b)){var e=c.extent,f=c.pixelRatio,g=c.viewState,h=g.rotation,l=a.f,m=l.canvas;qh(a.c,m.width/2,m.height/2,f/g.resolution,-f/g.resolution,-g.rotation,-g.center[0],-g.center[1]);a=new yj(l,f,e,a.c,h);d.b(new lh(b,d,a,c,l,null))}}Yl.prototype.X=function(){return"dom"};
+Yl.prototype.Ce=function(a){if(a){var b=this.i;if(ab(b,"precompose")||ab(b,"postcompose")){var b=this.f.canvas,c=a.pixelRatio;b.width=a.size[0]*c;b.height=a.size[1]*c}Zl(this,"precompose",a);b=a.layerStatesArray;qb(b);var c=a.viewState.resolution,d,e,f,g;d=0;for(e=b.length;d<e;++d)g=b[d],f=g.layer,f=Kh(this,f),this.b.insertBefore(f.target,this.b.childNodes[d]||null),nh(g,c)&&"ready"==g.R?f.yf(a,g)&&f.th(a,g):f.Nd();var b=a.layerStates,h;for(h in this.g)h in b||(f=this.g[h],Ue(f.target));this.a||(this.b.style.display=
+"",this.a=!0);Ih(a);Lh(this,a);a.postRenderFunctions.push(Jh);Zl(this,"postcompose",a)}else this.a&&(this.b.style.display="none",this.a=!1)};function $l(a){this.b=a}function am(a){this.b=a}y(am,$l);am.prototype.X=function(){return 35632};function bm(a){this.b=a}y(bm,$l);bm.prototype.X=function(){return 35633};function cm(){this.b="precision mediump float;varying vec2 a;varying float b;uniform float k;uniform sampler2D l;void main(void){vec4 texColor=texture2D(l,a);gl_FragColor.rgb=texColor.rgb;float alpha=texColor.a*b*k;if(alpha==0.0){discard;}gl_FragColor.a=alpha;}"}y(cm,am);ba(cm);
+function dm(){this.b="varying vec2 a;varying float b;attribute vec2 c;attribute vec2 d;attribute vec2 e;attribute float f;attribute float g;uniform mat4 h;uniform mat4 i;uniform mat4 j;void main(void){mat4 offsetMatrix=i;if(g==1.0){offsetMatrix=i*j;}vec4 offsets=offsetMatrix*vec4(e,0.,0.);gl_Position=h*vec4(c,0.,1.)+offsets;a=d;b=f;}"}y(dm,bm);ba(dm);
+function em(a,b){this.o=a.getUniformLocation(b,"j");this.j=a.getUniformLocation(b,"i");this.i=a.getUniformLocation(b,"k");this.l=a.getUniformLocation(b,"h");this.b=a.getAttribLocation(b,"e");this.a=a.getAttribLocation(b,"f");this.f=a.getAttribLocation(b,"c");this.g=a.getAttribLocation(b,"g");this.c=a.getAttribLocation(b,"d")};function fm(a){this.b=void 0!==a?a:[]};function gm(a,b){this.l=a;this.b=b;this.a={};this.c={};this.f={};this.j=this.s=this.i=this.o=null;(this.g=jb(ma,"OES_element_index_uint"))&&b.getExtension("OES_element_index_uint");B(this.l,"webglcontextlost",this.ao,this);B(this.l,"webglcontextrestored",this.bo,this)}y(gm,Sa);
+function hm(a,b,c){var d=a.b,e=c.b,f=String(w(c));if(f in a.a)d.bindBuffer(b,a.a[f].buffer);else{var g=d.createBuffer();d.bindBuffer(b,g);var h;34962==b?h=new Float32Array(e):34963==b&&(h=a.g?new Uint32Array(e):new Uint16Array(e));d.bufferData(b,h,35044);a.a[f]={Cb:c,buffer:g}}}function im(a,b){var c=a.b,d=String(w(b)),e=a.a[d];c.isContextLost()||c.deleteBuffer(e.buffer);delete a.a[d]}k=gm.prototype;
+k.ka=function(){Ra(this.l);var a=this.b;if(!a.isContextLost()){for(var b in this.a)a.deleteBuffer(this.a[b].buffer);for(b in this.f)a.deleteProgram(this.f[b]);for(b in this.c)a.deleteShader(this.c[b]);a.deleteFramebuffer(this.i);a.deleteRenderbuffer(this.j);a.deleteTexture(this.s)}};k.$n=function(){return this.b};
+function jm(a){if(!a.i){var b=a.b,c=b.createFramebuffer();b.bindFramebuffer(b.FRAMEBUFFER,c);var d=km(b,1,1),e=b.createRenderbuffer();b.bindRenderbuffer(b.RENDERBUFFER,e);b.renderbufferStorage(b.RENDERBUFFER,b.DEPTH_COMPONENT16,1,1);b.framebufferTexture2D(b.FRAMEBUFFER,b.COLOR_ATTACHMENT0,b.TEXTURE_2D,d,0);b.framebufferRenderbuffer(b.FRAMEBUFFER,b.DEPTH_ATTACHMENT,b.RENDERBUFFER,e);b.bindTexture(b.TEXTURE_2D,null);b.bindRenderbuffer(b.RENDERBUFFER,null);b.bindFramebuffer(b.FRAMEBUFFER,null);a.i=c;
+a.s=d;a.j=e}return a.i}function lm(a,b){var c=String(w(b));if(c in a.c)return a.c[c];var d=a.b,e=d.createShader(b.X());d.shaderSource(e,b.b);d.compileShader(e);return a.c[c]=e}function mm(a,b,c){var d=w(b)+"/"+w(c);if(d in a.f)return a.f[d];var e=a.b,f=e.createProgram();e.attachShader(f,lm(a,b));e.attachShader(f,lm(a,c));e.linkProgram(f);return a.f[d]=f}k.ao=function(){Fa(this.a);Fa(this.c);Fa(this.f);this.j=this.s=this.i=this.o=null};k.bo=function(){};
+k.we=function(a){if(a==this.o)return!1;this.b.useProgram(a);this.o=a;return!0};function nm(a,b,c){var d=a.createTexture();a.bindTexture(a.TEXTURE_2D,d);a.texParameteri(a.TEXTURE_2D,a.TEXTURE_MAG_FILTER,a.LINEAR);a.texParameteri(a.TEXTURE_2D,a.TEXTURE_MIN_FILTER,a.LINEAR);void 0!==b&&a.texParameteri(3553,10242,b);void 0!==c&&a.texParameteri(3553,10243,c);return d}function km(a,b,c){var d=nm(a,void 0,void 0);a.texImage2D(a.TEXTURE_2D,0,a.RGBA,b,c,0,a.RGBA,a.UNSIGNED_BYTE,null);return d}
+function om(a,b){var c=nm(a,33071,33071);a.texImage2D(a.TEXTURE_2D,0,a.RGBA,a.RGBA,a.UNSIGNED_BYTE,b);return c};function pm(a,b){this.C=this.A=void 0;this.j=kc(b);this.U=[];this.i=[];this.R=void 0;this.c=[];this.f=[];this.Ba=this.ya=void 0;this.a=[];this.D=this.o=null;this.S=void 0;this.ta=Zc();this.Aa=Zc();this.Y=this.T=void 0;this.Sa=Zc();this.za=this.ia=this.Ra=void 0;this.Gb=[];this.l=[];this.b=[];this.v=null;this.g=[];this.s=[];this.qa=void 0}y(pm,kh);
+function qm(a,b){var c=a.v,d=a.o,e=a.Gb,f=a.l,g=b.b;return function(){if(!g.isContextLost()){var a,l;a=0;for(l=e.length;a<l;++a)g.deleteTexture(e[a]);a=0;for(l=f.length;a<l;++a)g.deleteTexture(f[a])}im(b,c);im(b,d)}}
+function rm(a,b,c,d){var e=a.A,f=a.C,g=a.R,h=a.ya,l=a.Ba,m=a.S,n=a.T,p=a.Y,q=a.Ra?1:0,r=a.ia,u=a.za,x=a.qa,v=Math.cos(r),r=Math.sin(r),D=a.a.length,A=a.b.length,z,F,N,K,X,oa;for(z=0;z<c;z+=d)X=b[z]-a.j[0],oa=b[z+1]-a.j[1],F=A/8,N=-u*e,K=-u*(g-f),a.b[A++]=X,a.b[A++]=oa,a.b[A++]=N*v-K*r,a.b[A++]=N*r+K*v,a.b[A++]=n/l,a.b[A++]=(p+g)/h,a.b[A++]=m,a.b[A++]=q,N=u*(x-e),K=-u*(g-f),a.b[A++]=X,a.b[A++]=oa,a.b[A++]=N*v-K*r,a.b[A++]=N*r+K*v,a.b[A++]=(n+x)/l,a.b[A++]=(p+g)/h,a.b[A++]=m,a.b[A++]=q,N=u*(x-e),K=
+u*f,a.b[A++]=X,a.b[A++]=oa,a.b[A++]=N*v-K*r,a.b[A++]=N*r+K*v,a.b[A++]=(n+x)/l,a.b[A++]=p/h,a.b[A++]=m,a.b[A++]=q,N=-u*e,K=u*f,a.b[A++]=X,a.b[A++]=oa,a.b[A++]=N*v-K*r,a.b[A++]=N*r+K*v,a.b[A++]=n/l,a.b[A++]=p/h,a.b[A++]=m,a.b[A++]=q,a.a[D++]=F,a.a[D++]=F+1,a.a[D++]=F+2,a.a[D++]=F,a.a[D++]=F+2,a.a[D++]=F+3}pm.prototype.tc=function(a,b){this.g.push(this.a.length);this.s.push(b);var c=a.la();rm(this,c,c.length,a.va())};
+pm.prototype.uc=function(a,b){this.g.push(this.a.length);this.s.push(b);var c=a.la();rm(this,c,c.length,a.va())};function sm(a,b){var c=b.b;a.U.push(a.a.length);a.i.push(a.a.length);a.v=new fm(a.b);hm(b,34962,a.v);a.o=new fm(a.a);hm(b,34963,a.o);var d={};tm(a.Gb,a.c,d,c);tm(a.l,a.f,d,c);a.A=void 0;a.C=void 0;a.R=void 0;a.c=null;a.f=null;a.ya=void 0;a.Ba=void 0;a.a=null;a.S=void 0;a.T=void 0;a.Y=void 0;a.Ra=void 0;a.ia=void 0;a.za=void 0;a.b=null;a.qa=void 0}
+function tm(a,b,c,d){var e,f,g,h=b.length;for(g=0;g<h;++g)e=b[g],f=w(e).toString(),f in c?e=c[f]:(e=om(d,e),c[f]=e),a[g]=e}
+pm.prototype.Pa=function(a,b,c,d,e,f,g,h,l,m,n){f=a.b;hm(a,34962,this.v);hm(a,34963,this.o);var p=cm.Zb(),q=dm.Zb(),q=mm(a,p,q);this.D?p=this.D:this.D=p=new em(f,q);a.we(q);f.enableVertexAttribArray(p.f);f.vertexAttribPointer(p.f,2,5126,!1,32,0);f.enableVertexAttribArray(p.b);f.vertexAttribPointer(p.b,2,5126,!1,32,8);f.enableVertexAttribArray(p.c);f.vertexAttribPointer(p.c,2,5126,!1,32,16);f.enableVertexAttribArray(p.a);f.vertexAttribPointer(p.a,1,5126,!1,32,24);f.enableVertexAttribArray(p.g);f.vertexAttribPointer(p.g,
+1,5126,!1,32,28);q=this.Sa;qh(q,0,0,2/(c*e[0]),2/(c*e[1]),-d,-(b[0]-this.j[0]),-(b[1]-this.j[1]));b=this.Aa;c=2/e[0];e=2/e[1];ad(b);b[0]=c;b[5]=e;b[10]=1;b[15]=1;e=this.ta;ad(e);0!==d&&fd(e,-d);f.uniformMatrix4fv(p.l,!1,q);f.uniformMatrix4fv(p.j,!1,b);f.uniformMatrix4fv(p.o,!1,e);f.uniform1f(p.i,g);var r;if(void 0===l)um(this,f,a,h,this.Gb,this.U);else{if(m)a:{d=a.g?5125:5123;a=a.g?4:2;e=this.g.length-1;for(g=this.l.length-1;0<=g;--g)for(f.bindTexture(3553,this.l[g]),m=0<g?this.i[g-1]:0,b=this.i[g];0<=
+e&&this.g[e]>=m;){r=this.g[e];c=this.s[e];q=w(c).toString();if(void 0===h[q]&&c.W()&&(void 0===n||nc(n,c.W().H()))&&(f.clear(f.COLOR_BUFFER_BIT|f.DEPTH_BUFFER_BIT),f.drawElements(4,b-r,d,r*a),b=l(c))){h=b;break a}b=r;e--}h=void 0}else f.clear(f.COLOR_BUFFER_BIT|f.DEPTH_BUFFER_BIT),um(this,f,a,h,this.l,this.i),h=(h=l(null))?h:void 0;r=h}f.disableVertexAttribArray(p.f);f.disableVertexAttribArray(p.b);f.disableVertexAttribArray(p.c);f.disableVertexAttribArray(p.a);f.disableVertexAttribArray(p.g);return r};
+function um(a,b,c,d,e,f){var g=c.g?5125:5123;c=c.g?4:2;if(Ha(d)){var h;a=0;d=e.length;for(h=0;a<d;++a){b.bindTexture(3553,e[a]);var l=f[a];b.drawElements(4,l-h,g,h*c);h=l}}else{h=0;var m,l=0;for(m=e.length;l<m;++l){b.bindTexture(3553,e[l]);for(var n=0<l?f[l-1]:0,p=f[l],q=n;h<a.g.length&&a.g[h]<=p;){var r=w(a.s[h]).toString();void 0!==d[r]?(q!==n&&b.drawElements(4,n-q,g,q*c),n=q=h===a.g.length-1?p:a.g[h+1]):n=h===a.g.length-1?p:a.g[h+1];h++}q!==n&&b.drawElements(4,n-q,g,q*c)}}}
+pm.prototype.Tb=function(a){var b=a.Yb(),c=a.jc(1),d=a.ld(),e=a.pe(1),f=a.v,g=a.Ia(),h=a.U,l=a.j,m=a.Fb();a=a.i;var n;0===this.c.length?this.c.push(c):(n=this.c[this.c.length-1],w(n)!=w(c)&&(this.U.push(this.a.length),this.c.push(c)));0===this.f.length?this.f.push(e):(n=this.f[this.f.length-1],w(n)!=w(e)&&(this.i.push(this.a.length),this.f.push(e)));this.A=b[0];this.C=b[1];this.R=m[1];this.ya=d[1];this.Ba=d[0];this.S=f;this.T=g[0];this.Y=g[1];this.ia=l;this.Ra=h;this.za=a;this.qa=m[0]};
+function vm(a,b,c){this.f=b;this.c=a;this.g=c;this.a={}}function wm(a,b){var c=[],d;for(d in a.a)c.push(qm(a.a[d],b));return function(){for(var a=c.length,b,d=0;d<a;d++)b=c[d].apply(this,arguments);return b}}function xm(a,b){for(var c in a.a)sm(a.a[c],b)}vm.prototype.b=function(a,b){var c=this.a[b];void 0===c&&(c=new ym[b](this.c,this.f),this.a[b]=c);return c};vm.prototype.Ya=function(){return Ha(this.a)};
+vm.prototype.Pa=function(a,b,c,d,e,f,g,h){var l,m,n;l=0;for(m=Nj.length;l<m;++l)n=this.a[Nj[l]],void 0!==n&&n.Pa(a,b,c,d,e,f,g,h,void 0,!1)};function zm(a,b,c,d,e,f,g,h,l,m,n){var p=Am,q,r;for(q=Nj.length-1;0<=q;--q)if(r=a.a[Nj[q]],void 0!==r&&(r=r.Pa(b,c,d,e,p,f,g,h,l,m,n)))return r}
+vm.prototype.ra=function(a,b,c,d,e,f,g,h,l,m){var n=b.b;n.bindFramebuffer(n.FRAMEBUFFER,jm(b));var p;void 0!==this.g&&(p=Ob(Xb(a),d*this.g));return zm(this,b,a,d,e,g,h,l,function(a){var b=new Uint8Array(4);n.readPixels(0,0,1,1,n.RGBA,n.UNSIGNED_BYTE,b);if(0<b[3]&&(a=m(a)))return a},!0,p)};
+function Bm(a,b,c,d,e,f,g,h){var l=c.b;l.bindFramebuffer(l.FRAMEBUFFER,jm(c));return void 0!==zm(a,c,b,d,e,f,g,h,function(){var a=new Uint8Array(4);l.readPixels(0,0,1,1,l.RGBA,l.UNSIGNED_BYTE,a);return 0<a[3]},!1)}var ym={Image:pm},Am=[1,1];function Cm(a,b,c,d,e,f,g){this.b=a;this.f=b;this.g=f;this.c=g;this.o=e;this.l=d;this.i=c;this.a=null}y(Cm,kh);k=Cm.prototype;k.sd=function(a){this.Tb(a.a)};k.sc=function(a){switch(a.X()){case "Point":this.uc(a,null);break;case "MultiPoint":this.tc(a,null);break;case "GeometryCollection":this.Ze(a,null)}};k.Ye=function(a,b){var c=(0,b.g)(a);c&&nc(this.g,c.H())&&(this.sd(b),this.sc(c))};k.Ze=function(a){a=a.c;var b,c;b=0;for(c=a.length;b<c;++b)this.sc(a[b])};
+k.uc=function(a,b){var c=this.b,d=(new vm(1,this.g)).b(0,"Image");d.Tb(this.a);d.uc(a,b);sm(d,c);d.Pa(this.b,this.f,this.i,this.l,this.o,this.c,1,{},void 0,!1);qm(d,c)()};k.tc=function(a,b){var c=this.b,d=(new vm(1,this.g)).b(0,"Image");d.Tb(this.a);d.tc(a,b);sm(d,c);d.Pa(this.b,this.f,this.i,this.l,this.o,this.c,1,{},void 0,!1);qm(d,c)()};k.Tb=function(a){this.a=a};function Dm(){this.b="precision mediump float;varying vec2 a;uniform float f;uniform sampler2D g;void main(void){vec4 texColor=texture2D(g,a);gl_FragColor.rgb=texColor.rgb;gl_FragColor.a=texColor.a*f;}"}y(Dm,am);ba(Dm);function Em(){this.b="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;}"}y(Em,bm);ba(Em);
+function Fm(a,b){this.g=a.getUniformLocation(b,"f");this.f=a.getUniformLocation(b,"e");this.i=a.getUniformLocation(b,"d");this.c=a.getUniformLocation(b,"g");this.b=a.getAttribLocation(b,"b");this.a=a.getAttribLocation(b,"c")};function Gm(a,b){th.call(this,b);this.f=a;this.S=new fm([-1,-1,0,0,1,-1,1,0,-1,1,0,1,1,1,1,1]);this.i=this.pb=null;this.l=void 0;this.s=Xc();this.U=Zc();this.v=null}y(Gm,th);
+function Hm(a,b,c){var d=a.f.f;if(void 0===a.l||a.l!=c){b.postRenderFunctions.push(function(a,b,c){a.isContextLost()||(a.deleteFramebuffer(b),a.deleteTexture(c))}.bind(null,d,a.i,a.pb));b=km(d,c,c);var e=d.createFramebuffer();d.bindFramebuffer(36160,e);d.framebufferTexture2D(36160,36064,3553,b,0);a.pb=b;a.i=e;a.l=c}else d.bindFramebuffer(36160,a.i)}
+Gm.prototype.vh=function(a,b,c){Im(this,"precompose",c,a);hm(c,34962,this.S);var d=c.b,e=Dm.Zb(),f=Em.Zb(),e=mm(c,e,f);this.v?f=this.v:this.v=f=new Fm(d,e);c.we(e)&&(d.enableVertexAttribArray(f.b),d.vertexAttribPointer(f.b,2,5126,!1,16,0),d.enableVertexAttribArray(f.a),d.vertexAttribPointer(f.a,2,5126,!1,16,8),d.uniform1i(f.c,0));d.uniformMatrix4fv(f.i,!1,this.s);d.uniformMatrix4fv(f.f,!1,this.U);d.uniform1f(f.g,b.opacity);d.bindTexture(3553,this.pb);d.drawArrays(5,0,4);Im(this,"postcompose",c,a)};
+function Im(a,b,c,d){a=a.a;if(ab(a,b)){var e=d.viewState;a.b(new lh(b,a,new Cm(c,e.center,e.resolution,e.rotation,d.size,d.extent,d.pixelRatio),d,null,c))}}Gm.prototype.zf=function(){this.i=this.pb=null;this.l=void 0};function Jm(a,b){Gm.call(this,a,b);this.j=this.o=this.c=null}y(Jm,Gm);function Km(a,b){var c=b.a();return om(a.f.f,c)}Jm.prototype.ra=function(a,b,c,d){var e=this.a;return e.ha().ra(a,b.viewState.resolution,b.viewState.rotation,b.skippedFeatureUids,function(a){return c.call(d,a,e)})};
+Jm.prototype.Af=function(a,b){var c=this.f.f,d=a.pixelRatio,e=a.viewState,f=e.center,g=e.resolution,h=e.rotation,l=this.c,m=this.pb,n=this.a.ha(),p=a.viewHints,q=a.extent;void 0!==b.extent&&(q=mc(q,b.extent));p[0]||p[1]||hc(q)||(e=n.A(q,g,d,e.projection))&&vh(this,e)&&(l=e,m=Km(this,e),this.pb&&a.postRenderFunctions.push(function(a,b){a.isContextLost()||a.deleteTexture(b)}.bind(null,c,this.pb)));l&&(c=this.f.c.l,Lm(this,c.width,c.height,d,f,g,h,l.H()),this.j=null,d=this.s,ad(d),ed(d,1,-1),dd(d,0,
+-1),this.c=l,this.pb=m,xh(a.attributions,l.l),yh(a,n));return!0};function Lm(a,b,c,d,e,f,g,h){b*=f;c*=f;a=a.U;ad(a);ed(a,2*d/b,2*d/c);fd(a,-g);dd(a,h[0]-e[0],h[1]-e[1]);ed(a,(h[2]-h[0])/2,(h[3]-h[1])/2);dd(a,1,1)}Jm.prototype.le=function(a,b){return void 0!==this.ra(a,b,qc,this)};
+Jm.prototype.Cc=function(a,b,c,d){if(this.c&&this.c.a())if(this.a.ha()instanceof yl){if(a=a.slice(),sh(b.pixelToCoordinateMatrix,a,a),this.ra(a,b,qc,this))return c.call(d,this.a)}else{var e=[this.c.a().width,this.c.a().height];if(!this.j){var f=b.size;b=Xc();ad(b);dd(b,-1,-1);ed(b,2/f[0],2/f[1]);dd(b,0,f[1]);ed(b,1,-1);f=Xc();cd(this.U,f);var g=Xc();ad(g);dd(g,0,e[1]);ed(g,1,-1);ed(g,e[0]/2,e[1]/2);dd(g,1,1);var h=Xc();bd(g,f,h);bd(h,b,h);this.j=h}b=[0,0];sh(this.j,a,b);if(!(0>b[0]||b[0]>e[0]||0>
+b[1]||b[1]>e[1])&&(this.o||(this.o=Oe(1,1)),this.o.clearRect(0,0,1,1),this.o.drawImage(this.c.a(),b[0],b[1],1,1,0,0,1,1),0<this.o.getImageData(0,0,1,1).data[3]))return c.call(d,this.a)}};function Mm(){this.b="precision mediump float;varying vec2 a;uniform sampler2D e;void main(void){gl_FragColor=texture2D(e,a);}"}y(Mm,am);ba(Mm);function Nm(){this.b="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;}"}y(Nm,bm);ba(Nm);function Om(a,b){this.g=a.getUniformLocation(b,"e");this.f=a.getUniformLocation(b,"d");this.b=a.getAttribLocation(b,"b");this.a=a.getAttribLocation(b,"c")};function Pm(a,b){Gm.call(this,a,b);this.D=Mm.Zb();this.T=Nm.Zb();this.c=null;this.C=new fm([0,0,0,1,1,0,1,1,0,1,0,0,1,1,1,0]);this.A=this.o=null;this.j=-1;this.R=[0,0]}y(Pm,Gm);k=Pm.prototype;k.ka=function(){im(this.f.c,this.C);Gm.prototype.ka.call(this)};k.Qd=function(a,b,c){var d=this.f;return function(e,f){return Af(a,b,e,f,function(a){var b=Ze(d.a,a.ib());b&&(c[e]||(c[e]={}),c[e][a.ma.toString()]=a);return b})}};k.zf=function(){Gm.prototype.zf.call(this);this.c=null};
+k.Af=function(a,b,c){var d=this.f,e=c.b,f=a.viewState,g=f.projection,h=this.a,l=h.ha(),m=l.eb(g),n=m.Lb(f.resolution),p=m.$(n),q=l.$d(n,a.pixelRatio,g),r=q[0]/hf(m.Ja(n),this.R)[0],u=p/r,x=l.Ud(g),v=f.center,D;p==f.resolution?(v=Ah(v,p,a.size),D=lc(v,p,f.rotation,a.size)):D=a.extent;p=sf(m,D,p);if(this.o&&he(this.o,p)&&this.j==l.g)u=this.A;else{var A=[p.ea-p.ca+1,p.ga-p.fa+1],z=Math.pow(2,Math.ceil(Math.log(Math.max(A[0]*q[0],A[1]*q[1]))/Math.LN2)),A=u*z,F=m.Ia(n),N=F[0]+p.ca*q[0]*u,u=F[1]+p.fa*q[1]*
+u,u=[N,u,N+A,u+A];Hm(this,a,z);e.viewport(0,0,z,z);e.clearColor(0,0,0,0);e.clear(16384);e.disable(3042);z=mm(c,this.D,this.T);c.we(z);this.c||(this.c=new Om(e,z));hm(c,34962,this.C);e.enableVertexAttribArray(this.c.b);e.vertexAttribPointer(this.c.b,2,5126,!1,16,0);e.enableVertexAttribArray(this.c.a);e.vertexAttribPointer(this.c.a,2,5126,!1,16,8);e.uniform1i(this.c.g,0);c={};c[n]={};var K=this.Qd(l,g,c),X=h.c(),z=!0,N=Lb(),oa=new fe(0,0,0,0),H,ya,Ua;for(ya=p.ca;ya<=p.ea;++ya)for(Ua=p.fa;Ua<=p.ga;++Ua){F=
+l.ac(n,ya,Ua,r,g);if(void 0!==b.extent&&(H=m.Ea(F.ma,N),!nc(H,b.extent)))continue;H=F.V();H=2==H||4==H||3==H&&!X;!H&&F.a&&(F=F.a);H=F.V();if(2==H){if(Ze(d.a,F.ib())){c[n][F.ma.toString()]=F;continue}}else if(4==H||3==H&&!X)continue;z=!1;H=qf(m,F.ma,K,oa,N);H||(F=rf(m,F.ma,oa,N))&&K(n+1,F)}b=Object.keys(c).map(Number);b.sort(ib);for(var K=new Float32Array(4),Xa,Va,Aa,X=0,oa=b.length;X<oa;++X)for(Xa in Va=c[b[X]],Va)F=Va[Xa],H=m.Ea(F.ma,N),ya=2*(H[2]-H[0])/A,Ua=2*(H[3]-H[1])/A,Aa=2*(H[0]-u[0])/A-1,
+H=2*(H[1]-u[1])/A-1,Wc(K,ya,Ua,Aa,H),e.uniform4fv(this.c.f,K),Qm(d,F,q,x*r),e.drawArrays(5,0,4);z?(this.o=p,this.A=u,this.j=l.g):(this.A=this.o=null,this.j=-1,a.animate=!0)}zh(a.usedTiles,l,n,p);var Qb=d.o;Bh(a,l,m,r,g,D,n,h.f(),function(a){var b;(b=2!=a.V()||Ze(d.a,a.ib()))||(b=a.ib()in Qb.g);b||Qb.f([a,uf(m,a.ma),m.$(a.ma[0]),q,x*r])},this);wh(a,l);yh(a,l);e=this.s;ad(e);dd(e,(v[0]-u[0])/(u[2]-u[0]),(v[1]-u[1])/(u[3]-u[1]));0!==f.rotation&&fd(e,f.rotation);ed(e,a.size[0]*f.resolution/(u[2]-u[0]),
+a.size[1]*f.resolution/(u[3]-u[1]));dd(e,-.5,-.5);return!0};k.Cc=function(a,b,c,d){if(this.i){var e=[0,0];sh(this.s,[a[0]/b.size[0],(b.size[1]-a[1])/b.size[1]],e);a=[e[0]*this.l,e[1]*this.l];b=this.f.c.b;b.bindFramebuffer(b.FRAMEBUFFER,this.i);e=new Uint8Array(4);b.readPixels(a[0],a[1],1,1,b.RGBA,b.UNSIGNED_BYTE,e);if(0<e[3])return c.call(d,this.a)}};function Rm(a,b){Gm.call(this,a,b);this.j=!1;this.R=-1;this.D=NaN;this.A=Lb();this.o=this.c=this.C=null}y(Rm,Gm);k=Rm.prototype;k.vh=function(a,b,c){this.o=b;var d=a.viewState,e=this.c;e&&!e.Ya()&&e.Pa(c,d.center,d.resolution,d.rotation,a.size,a.pixelRatio,b.opacity,b.Qc?a.skippedFeatureUids:{})};k.ka=function(){var a=this.c;a&&(wm(a,this.f.c)(),this.c=null);Gm.prototype.ka.call(this)};
+k.ra=function(a,b,c,d){if(this.c&&this.o){var e=b.viewState,f=this.a,g={};return this.c.ra(a,this.f.c,e.center,e.resolution,e.rotation,b.size,b.pixelRatio,this.o.opacity,{},function(a){var b=w(a).toString();if(!(b in g))return g[b]=!0,c.call(d,a,f)})}};k.le=function(a,b){if(this.c&&this.o){var c=b.viewState;return Bm(this.c,a,this.f.c,c.resolution,c.rotation,b.pixelRatio,this.o.opacity,b.skippedFeatureUids)}return!1};
+k.Cc=function(a,b,c,d){a=a.slice();sh(b.pixelToCoordinateMatrix,a,a);if(this.le(a,b))return c.call(d,this.a)};k.wh=function(){uh(this)};
+k.Af=function(a,b,c){function d(a){var b,c=a.ec();c?b=c.call(a,m):(c=e.i)&&(b=c(a,m));if(b){if(b){c=!1;if(Array.isArray(b))for(var d=0,f=b.length;d<f;++d)c=lk(q,a,b[d],jk(m,n),this.wh,this)||c;else c=lk(q,a,b,jk(m,n),this.wh,this)||c;a=c}else a=!1;this.j=this.j||a}}var e=this.a;b=e.ha();xh(a.attributions,b.l);yh(a,b);var f=a.viewHints[0],g=a.viewHints[1],h=e.S,l=e.T;if(!this.j&&!h&&f||!l&&g)return!0;var g=a.extent,h=a.viewState,f=h.projection,m=h.resolution,n=a.pixelRatio,h=e.g,p=e.a,l=xj(e);void 0===
+l&&(l=ik);g=Ob(g,p*m);if(!this.j&&this.D==m&&this.R==h&&this.C==l&&Ub(this.A,g))return!0;this.c&&a.postRenderFunctions.push(wm(this.c,c));this.j=!1;var q=new vm(.5*m/n,g,e.a);b.Pc(g,m,f);if(l){var r=[];b.ub(g,function(a){r.push(a)},this);r.sort(l);r.forEach(d,this)}else b.ub(g,d,this);xm(q,c);this.D=m;this.R=h;this.C=l;this.A=g;this.c=q;return!0};function Sm(a,b){Hh.call(this,0,b);this.b=document.createElement("CANVAS");this.b.style.width="100%";this.b.style.height="100%";this.b.className="ol-unselectable";a.insertBefore(this.b,a.childNodes[0]||null);this.U=this.A=0;this.C=Oe();this.j=!0;this.f=ag(this.b,{antialias:!0,depth:!1,failIfMajorPerformanceCaveat:!0,preserveDrawingBuffer:!1,stencil:!0});this.c=new gm(this.b,this.f);B(this.b,"webglcontextlost",this.Pm,this);B(this.b,"webglcontextrestored",this.Qm,this);this.a=new Ye;this.v=null;this.o=
+new Mh(function(a){var b=a[1];a=a[2];var e=b[0]-this.v[0],b=b[1]-this.v[1];return 65536*Math.log(a)+Math.sqrt(e*e+b*b)/a}.bind(this),function(a){return a[0].ib()});this.D=function(){if(!this.o.Ya()){Qh(this.o);var a=Nh(this.o);Qm(this,a[0],a[3],a[4])}return!1}.bind(this);this.l=0;Tm(this)}y(Sm,Hh);
+function Qm(a,b,c,d){var e=a.f,f=b.ib();if(Ze(a.a,f))a=a.a.get(f),e.bindTexture(3553,a.pb),9729!=a.Wg&&(e.texParameteri(3553,10240,9729),a.Wg=9729),9729!=a.Yg&&(e.texParameteri(3553,10241,9729),a.Yg=9729);else{var g=e.createTexture();e.bindTexture(3553,g);if(0<d){var h=a.C.canvas,l=a.C;a.A!==c[0]||a.U!==c[1]?(h.width=c[0],h.height=c[1],a.A=c[0],a.U=c[1]):l.clearRect(0,0,c[0],c[1]);l.drawImage(b.$a(),d,d,c[0],c[1],0,0,c[0],c[1]);e.texImage2D(3553,0,6408,6408,5121,h)}else e.texImage2D(3553,0,6408,6408,
+5121,b.$a());e.texParameteri(3553,10240,9729);e.texParameteri(3553,10241,9729);e.texParameteri(3553,10242,33071);e.texParameteri(3553,10243,33071);a.a.set(f,{pb:g,Wg:9729,Yg:9729})}}k=Sm.prototype;k.Xe=function(a){return a instanceof cj?new Jm(this,a):a instanceof dj?new Pm(this,a):a instanceof G?new Rm(this,a):null};function Um(a,b,c){var d=a.i;if(ab(d,b)){a=a.c;var e=c.viewState;d.b(new lh(b,d,new Cm(a,e.center,e.resolution,e.rotation,c.size,c.extent,c.pixelRatio),c,null,a))}}
+k.ka=function(){var a=this.f;a.isContextLost()||this.a.forEach(function(b){b&&a.deleteTexture(b.pb)});Ta(this.c);Hh.prototype.ka.call(this)};k.Gj=function(a,b){for(var c=this.f,d;1024<this.a.wc()-this.l;){if(d=this.a.b.pc)c.deleteTexture(d.pb);else if(+this.a.b.cc==b.index)break;else--this.l;this.a.pop()}};k.X=function(){return"webgl"};k.Pm=function(a){a.preventDefault();this.a.clear();this.l=0;a=this.g;for(var b in a)a[b].zf()};k.Qm=function(){Tm(this);this.i.render()};
+function Tm(a){a=a.f;a.activeTexture(33984);a.blendFuncSeparate(770,771,1,771);a.disable(2884);a.disable(2929);a.disable(3089);a.disable(2960)}
+k.Ce=function(a){var b=this.c,c=this.f;if(c.isContextLost())return!1;if(!a)return this.j&&(this.b.style.display="none",this.j=!1),!1;this.v=a.focus;this.a.set((-a.index).toString(),null);++this.l;Um(this,"precompose",a);var d=[],e=a.layerStatesArray;qb(e);var f=a.viewState.resolution,g,h,l,m;g=0;for(h=e.length;g<h;++g)m=e[g],nh(m,f)&&"ready"==m.R&&(l=Kh(this,m.layer),l.Af(a,m,b)&&d.push(m));e=a.size[0]*a.pixelRatio;f=a.size[1]*a.pixelRatio;if(this.b.width!=e||this.b.height!=f)this.b.width=e,this.b.height=
+f;c.bindFramebuffer(36160,null);c.clearColor(0,0,0,0);c.clear(16384);c.enable(3042);c.viewport(0,0,this.b.width,this.b.height);g=0;for(h=d.length;g<h;++g)m=d[g],l=Kh(this,m.layer),l.vh(a,m,b);this.j||(this.b.style.display="",this.j=!0);Ih(a);1024<this.a.wc()-this.l&&a.postRenderFunctions.push(this.Gj.bind(this));this.o.Ya()||(a.postRenderFunctions.push(this.D),a.animate=!0);Um(this,"postcompose",a);Lh(this,a);a.postRenderFunctions.push(Jh)};
+k.ra=function(a,b,c,d,e,f){var g;if(this.f.isContextLost())return!1;var h=b.viewState,l=b.layerStatesArray,m;for(m=l.length-1;0<=m;--m){g=l[m];var n=g.layer;if(nh(g,h.resolution)&&e.call(f,n)&&(g=Kh(this,n).ra(a,b,c,d)))return g}};k.sh=function(a,b,c,d){var e=!1;if(this.f.isContextLost())return!1;var f=b.viewState,g=b.layerStatesArray,h;for(h=g.length-1;0<=h;--h){var l=g[h],m=l.layer;if(nh(l,f.resolution)&&c.call(d,m)&&(e=Kh(this,m).le(a,b)))return!0}return e};
+k.rh=function(a,b,c,d,e){if(this.f.isContextLost())return!1;var f=b.viewState,g,h=b.layerStatesArray,l;for(l=h.length-1;0<=l;--l){g=h[l];var m=g.layer;if(nh(g,f.resolution)&&e.call(d,m)&&(g=Kh(this,m).Cc(a,b,c,d)))return g}};var Vm=["canvas","webgl","dom"];
+function Q(a){eb.call(this);var b=Wm(a);this.Hb=void 0!==a.loadTilesWhileAnimating?a.loadTilesWhileAnimating:!1;this.Hc=void 0!==a.loadTilesWhileInteracting?a.loadTilesWhileInteracting:!1;this.Oe=void 0!==a.pixelRatio?a.pixelRatio:gg;this.Ne=b.logos;this.Y=function(){this.i=void 0;this.Uo.call(this,Date.now())}.bind(this);this.Sa=Xc();this.Pe=Xc();this.qb=0;this.f=null;this.Aa=Lb();this.D=this.S=null;this.a=document.createElement("DIV");this.a.className="ol-viewport"+(lg?" ol-touch":"");this.a.style.position=
+"relative";this.a.style.overflow="hidden";this.a.style.width="100%";this.a.style.height="100%";this.a.style.msTouchAction="none";this.a.style.touchAction="none";this.A=document.createElement("DIV");this.A.className="ol-overlaycontainer";this.a.appendChild(this.A);this.v=document.createElement("DIV");this.v.className="ol-overlaycontainer-stopevent";a=["click","dblclick","mousedown","touchstart","mspointerdown",eh,"mousewheel","wheel"];for(var c=0,d=a.length;c<d;++c)B(this.v,a[c],Ya);this.a.appendChild(this.v);
+this.za=new Xg(this);for(var e in hh)B(this.za,hh[e],this.Pg,this);this.ia=b.keyboardEventTarget;this.s=null;B(this.a,"wheel",this.Oc,this);B(this.a,"mousewheel",this.Oc,this);this.o=b.controls;this.l=b.interactions;this.j=b.overlays;this.Cf={};this.C=new b.Wo(this.a,this);this.T=null;this.R=[];this.ta=[];this.qa=new Rh(this.Bk.bind(this),this.hl.bind(this));this.Ee={};B(this,gb("layergroup"),this.Ok,this);B(this,gb("view"),this.il,this);B(this,gb("size"),this.el,this);B(this,gb("target"),this.gl,
+this);this.G(b.values);this.o.forEach(function(a){a.setMap(this)},this);B(this.o,"add",function(a){a.element.setMap(this)},this);B(this.o,"remove",function(a){a.element.setMap(null)},this);this.l.forEach(function(a){a.setMap(this)},this);B(this.l,"add",function(a){a.element.setMap(this)},this);B(this.l,"remove",function(a){a.element.setMap(null)},this);this.j.forEach(this.mg,this);B(this.j,"add",function(a){this.mg(a.element)},this);B(this.j,"remove",function(a){var b=a.element.Xa();void 0!==b&&delete this.Cf[b.toString()];
+a.element.setMap(null)},this)}y(Q,eb);k=Q.prototype;k.uj=function(a){this.o.push(a)};k.vj=function(a){this.l.push(a)};k.kg=function(a){this.xc().Tc().push(a)};k.lg=function(a){this.j.push(a)};k.mg=function(a){var b=a.Xa();void 0!==b&&(this.Cf[b.toString()]=a);a.setMap(this)};k.Wa=function(a){this.render();Array.prototype.push.apply(this.R,arguments)};
+k.ka=function(){Ta(this.za);Ta(this.C);Qa(this.a,"wheel",this.Oc,this);Qa(this.a,"mousewheel",this.Oc,this);void 0!==this.c&&(pa.removeEventListener("resize",this.c,!1),this.c=void 0);this.i&&(pa.cancelAnimationFrame(this.i),this.i=void 0);this.fh(null);eb.prototype.ka.call(this)};k.kd=function(a,b,c,d,e){if(this.f)return a=this.Ma(a),this.C.ra(a,this.f,b,void 0!==c?c:null,void 0!==d?d:qc,void 0!==e?e:null)};
+k.Tl=function(a,b,c,d,e){if(this.f)return this.C.rh(a,this.f,b,void 0!==c?c:null,void 0!==d?d:qc,void 0!==e?e:null)};k.kl=function(a,b,c){if(!this.f)return!1;a=this.Ma(a);return this.C.sh(a,this.f,void 0!==b?b:qc,void 0!==c?c:null)};k.Wj=function(a){return this.Ma(this.Td(a))};k.Td=function(a){var b=this.a.getBoundingClientRect();a=a.changedTouches?a.changedTouches[0]:a;return[a.clientX-b.left,a.clientY-b.top]};k.tf=function(){return this.get("target")};
+k.yc=function(){var a=this.tf();return void 0!==a?"string"===typeof a?document.getElementById(a):a:null};k.Ma=function(a){var b=this.f;return b?(a=a.slice(),sh(b.pixelToCoordinateMatrix,a,a)):null};k.Uj=function(){return this.o};k.nk=function(){return this.j};k.mk=function(a){a=this.Cf[a.toString()];return void 0!==a?a:null};k.ak=function(){return this.l};k.xc=function(){return this.get("layergroup")};k.eh=function(){return this.xc().Tc()};
+k.Ga=function(a){var b=this.f;return b?(a=a.slice(0,2),sh(b.coordinateToPixelMatrix,a,a)):null};k.Za=function(){return this.get("size")};k.aa=function(){return this.get("view")};k.Dk=function(){return this.a};k.Bk=function(a,b,c,d){var e=this.f;if(!(e&&b in e.wantedTiles&&e.wantedTiles[b][a.ma.toString()]))return Infinity;a=c[0]-e.focus[0];c=c[1]-e.focus[1];return 65536*Math.log(d)+Math.sqrt(a*a+c*c)/d};k.Oc=function(a,b){var c=new Vg(b||a.type,this,a);this.Pg(c)};
+k.Pg=function(a){if(this.f){this.T=a.coordinate;a.frameState=this.f;var b=this.l.a,c;if(!1!==this.b(a))for(c=b.length-1;0<=c;c--){var d=b[c];if(d.f()&&!d.handleEvent(a))break}}};k.cl=function(){var a=this.f,b=this.qa;if(!b.Ya()){var c=16,d=c;if(a){var e=a.viewHints;e[0]&&(c=this.Hb?8:0,d=2);e[1]&&(c=this.Hc?8:0,d=2)}b.i<c&&(Qh(b),Sh(b,c,d))}b=this.ta;c=0;for(d=b.length;c<d;++c)b[c](this,a);b.length=0};k.el=function(){this.render()};
+k.gl=function(){var a;this.tf()&&(a=this.yc());if(this.s){for(var b=0,c=this.s.length;b<c;++b)Ka(this.s[b]);this.s=null}a?(a.appendChild(this.a),a=this.ia?this.ia:a,this.s=[B(a,"keydown",this.Oc,this),B(a,"keypress",this.Oc,this)],this.c||(this.c=this.Xc.bind(this),pa.addEventListener("resize",this.c,!1))):(Ue(this.a),void 0!==this.c&&(pa.removeEventListener("resize",this.c,!1),this.c=void 0));this.Xc()};k.hl=function(){this.render()};k.jl=function(){this.render()};
+k.il=function(){this.S&&(Ka(this.S),this.S=null);var a=this.aa();a&&(this.S=B(a,"propertychange",this.jl,this));this.render()};k.Pk=function(){this.render()};k.Qk=function(){this.render()};k.Ok=function(){this.D&&(this.D.forEach(Ka),this.D=null);var a=this.xc();a&&(this.D=[B(a,"propertychange",this.Qk,this),B(a,"change",this.Pk,this)]);this.render()};k.Vo=function(){this.i&&pa.cancelAnimationFrame(this.i);this.Y()};k.render=function(){void 0===this.i&&(this.i=pa.requestAnimationFrame(this.Y))};
+k.Oo=function(a){return this.o.remove(a)};k.Po=function(a){return this.l.remove(a)};k.Ro=function(a){return this.xc().Tc().remove(a)};k.So=function(a){return this.j.remove(a)};
+k.Uo=function(a){var b,c,d,e=this.Za(),f=this.aa(),g=Lb(),h=null;if(void 0!==e&&0<e[0]&&0<e[1]&&f&&Wd(f)){var h=Sd(f,this.f?this.f.viewHints:void 0),l=this.xc().hf(),m={};b=0;for(c=l.length;b<c;++b)m[w(l[b].layer)]=l[b];d=f.V();h={animate:!1,attributions:{},coordinateToPixelMatrix:this.Sa,extent:g,focus:this.T?this.T:d.center,index:this.qb++,layerStates:m,layerStatesArray:l,logos:Ea({},this.Ne),pixelRatio:this.Oe,pixelToCoordinateMatrix:this.Pe,postRenderFunctions:[],size:e,skippedFeatureUids:this.Ee,
+tileQueue:this.qa,time:a,usedTiles:{},viewState:d,viewHints:h,wantedTiles:{}}}if(h){a=this.R;b=e=0;for(c=a.length;b<c;++b)f=a[b],f(this,h)&&(a[e++]=f);a.length=e;h.extent=lc(d.center,d.resolution,d.rotation,h.size,g)}this.f=h;this.C.Ce(h);h&&(h.animate&&this.render(),Array.prototype.push.apply(this.ta,h.postRenderFunctions),0!==this.R.length||h.viewHints[0]||h.viewHints[1]||$b(h.extent,this.Aa)||(this.b(new We("moveend",this,h)),Pb(h.extent,this.Aa)));this.b(new We("postrender",this,h));Tf(this.cl,
+this)};k.ji=function(a){this.set("layergroup",a)};k.Wf=function(a){this.set("size",a)};k.fh=function(a){this.set("target",a)};k.kp=function(a){this.set("view",a)};k.ti=function(a){a=w(a).toString();this.Ee[a]=!0;this.render()};
+k.Xc=function(){var a=this.yc();if(a){var b=pa.getComputedStyle(a);this.Wf([a.offsetWidth-parseFloat(b.borderLeftWidth)-parseFloat(b.paddingLeft)-parseFloat(b.paddingRight)-parseFloat(b.borderRightWidth),a.offsetHeight-parseFloat(b.borderTopWidth)-parseFloat(b.paddingTop)-parseFloat(b.paddingBottom)-parseFloat(b.borderBottomWidth)])}else this.Wf(void 0)};k.wi=function(a){a=w(a).toString();delete this.Ee[a];this.render()};
+function Wm(a){var b=null;void 0!==a.keyboardEventTarget&&(b="string"===typeof a.keyboardEventTarget?document.getElementById(a.keyboardEventTarget):a.keyboardEventTarget);var c={},d={};if(void 0===a.logo||"boolean"===typeof a.logo&&a.logo)d["data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAAA3NCSVQICAjb4U/gAAAACXBIWXMAAAHGAAABxgEXwfpGAAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAAhNQTFRF////AP//AICAgP//AFVVQECA////K1VVSbbbYL/fJ05idsTYJFtbbcjbJllmZszWWMTOIFhoHlNiZszTa9DdUcHNHlNlV8XRIVdiasrUHlZjIVZjaMnVH1RlIFRkH1RkH1ZlasvYasvXVsPQH1VkacnVa8vWIVZjIFRjVMPQa8rXIVVkXsXRsNveIFVkIFZlIVVj3eDeh6GmbMvXH1ZkIFRka8rWbMvXIFVkIFVjIFVkbMvWH1VjbMvWIFVlbcvWIFVla8vVIFVkbMvWbMvVH1VkbMvWIFVlbcvWIFVkbcvVbMvWjNPbIFVkU8LPwMzNIFVkbczWIFVkbsvWbMvXIFVkRnB8bcvW2+TkW8XRIFVkIlZlJVloJlpoKlxrLl9tMmJwOWd0Omh1RXF8TneCT3iDUHiDU8LPVMLPVcLPVcPQVsPPVsPQV8PQWMTQWsTQW8TQXMXSXsXRX4SNX8bSYMfTYcfTYsfTY8jUZcfSZsnUaIqTacrVasrVa8jTa8rWbI2VbMvWbcvWdJObdcvUdszUd8vVeJaee87Yfc3WgJyjhqGnitDYjaarldPZnrK2oNbborW5o9bbo9fbpLa6q9ndrL3ArtndscDDutzfu8fJwN7gwt7gxc/QyuHhy+HizeHi0NfX0+Pj19zb1+Tj2uXk29/e3uLg3+Lh3+bl4uXj4ufl4+fl5Ofl5ufl5ujm5+jmySDnBAAAAFp0Uk5TAAECAgMEBAYHCA0NDg4UGRogIiMmKSssLzU7PkJJT1JTVFliY2hrdHZ3foSFhYeJjY2QkpugqbG1tre5w8zQ09XY3uXn6+zx8vT09vf4+Pj5+fr6/P39/f3+gz7SsAAAAVVJREFUOMtjYKA7EBDnwCPLrObS1BRiLoJLnte6CQy8FLHLCzs2QUG4FjZ5GbcmBDDjxJBXDWxCBrb8aM4zbkIDzpLYnAcE9VXlJSWlZRU13koIeW57mGx5XjoMZEUqwxWYQaQbSzLSkYGfKFSe0QMsX5WbjgY0YS4MBplemI4BdGBW+DQ11eZiymfqQuXZIjqwyadPNoSZ4L+0FVM6e+oGI6g8a9iKNT3o8kVzNkzRg5lgl7p4wyRUL9Yt2jAxVh6mQCogae6GmflI8p0r13VFWTHBQ0rWPW7ahgWVcPm+9cuLoyy4kCJDzCm6d8PSFoh0zvQNC5OjDJhQopPPJqph1doJBUD5tnkbZiUEqaCnB3bTqLTFG1bPn71kw4b+GFdpLElKIzRxxgYgWNYc5SCENVHKeUaltHdXx0dZ8uBI1hJ2UUDgq82CM2MwKeibqAvSO7MCABq0wXEPiqWEAAAAAElFTkSuQmCC"]=
+"http://openlayers.org/";else{var e=a.logo;"string"===typeof e?d[e]="":e instanceof HTMLElement?d[w(e).toString()]=e:fa(e)&&(d[e.src]=e.href)}e=a.layers instanceof Ti?a.layers:new Ti({layers:a.layers});c.layergroup=e;c.target=a.target;c.view=void 0!==a.view?a.view:new Rd;var e=Hh,f;void 0!==a.renderer?Array.isArray(a.renderer)?f=a.renderer:"string"===typeof a.renderer&&(f=[a.renderer]):f=Vm;var g,h;g=0;for(h=f.length;g<h;++g){var l=f[g];if("canvas"==l){if(ig){e=Ql;break}}else if("dom"==l){e=Yl;break}else if("webgl"==
+l&&bg){e=Sm;break}}var m;void 0!==a.controls?m=Array.isArray(a.controls)?new le(a.controls.slice()):a.controls:m=Kf();var n;void 0!==a.interactions?n=Array.isArray(a.interactions)?new le(a.interactions.slice()):a.interactions:n=Si();a=void 0!==a.overlays?Array.isArray(a.overlays)?new le(a.overlays.slice()):a.overlays:new le;return{controls:m,interactions:n,keyboardEventTarget:b,logos:d,overlays:a,Wo:e,values:c}}bj();function Xm(a){eb.call(this);this.j=a.id;this.o=void 0!==a.insertFirst?a.insertFirst:!0;this.s=void 0!==a.stopEvent?a.stopEvent:!0;this.f=document.createElement("DIV");this.f.className="ol-overlay-container";this.f.style.position="absolute";this.autoPan=void 0!==a.autoPan?a.autoPan:!1;this.i=void 0!==a.autoPanAnimation?a.autoPanAnimation:{};this.l=void 0!==a.autoPanMargin?a.autoPanMargin:20;this.a={Md:"",fe:"",De:"",Fe:"",visible:!0};this.c=null;B(this,gb("element"),this.Jk,this);B(this,gb("map"),
+this.Vk,this);B(this,gb("offset"),this.Zk,this);B(this,gb("position"),this.al,this);B(this,gb("positioning"),this.bl,this);void 0!==a.element&&this.fi(a.element);this.li(void 0!==a.offset?a.offset:[0,0]);this.oi(void 0!==a.positioning?a.positioning:"top-left");void 0!==a.position&&this.uf(a.position)}y(Xm,eb);k=Xm.prototype;k.Sd=function(){return this.get("element")};k.Xa=function(){return this.j};k.he=function(){return this.get("map")};k.Kg=function(){return this.get("offset")};k.gh=function(){return this.get("position")};
+k.Lg=function(){return this.get("positioning")};k.Jk=function(){Ve(this.f);var a=this.Sd();a&&this.f.appendChild(a)};k.Vk=function(){this.c&&(Ue(this.f),Ka(this.c),this.c=null);var a=this.he();a&&(this.c=B(a,"postrender",this.render,this),Ym(this),a=this.s?a.v:a.A,this.o?a.insertBefore(this.f,a.childNodes[0]||null):a.appendChild(this.f))};k.render=function(){Ym(this)};k.Zk=function(){Ym(this)};
+k.al=function(){Ym(this);if(void 0!==this.get("position")&&this.autoPan){var a=this.he();if(void 0!==a&&a.yc()){var b=Zm(a.yc(),a.Za()),c=this.Sd(),d=c.offsetWidth,e=c.currentStyle||pa.getComputedStyle(c),d=d+(parseInt(e.marginLeft,10)+parseInt(e.marginRight,10)),e=c.offsetHeight,f=c.currentStyle||pa.getComputedStyle(c),e=e+(parseInt(f.marginTop,10)+parseInt(f.marginBottom,10)),g=Zm(c,[d,e]),c=this.l;Ub(b,g)||(d=g[0]-b[0],e=b[2]-g[2],f=g[1]-b[1],g=b[3]-g[3],b=[0,0],0>d?b[0]=d-c:0>e&&(b[0]=Math.abs(e)+
+c),0>f?b[1]=f-c:0>g&&(b[1]=Math.abs(g)+c),0===b[0]&&0===b[1])||(c=a.aa().ab(),d=a.Ga(c),b=[d[0]+b[0],d[1]+b[1]],this.i&&(this.i.source=c,a.Wa(ce(this.i))),a.aa().mb(a.Ma(b)))}}};k.bl=function(){Ym(this)};k.fi=function(a){this.set("element",a)};k.setMap=function(a){this.set("map",a)};k.li=function(a){this.set("offset",a)};k.uf=function(a){this.set("position",a)};function Zm(a,b){var c=a.getBoundingClientRect(),d=c.left+pa.pageXOffset,c=c.top+pa.pageYOffset;return[d,c,d+b[0],c+b[1]]}
+k.oi=function(a){this.set("positioning",a)};function $m(a,b){a.a.visible!==b&&(a.f.style.display=b?"":"none",a.a.visible=b)}
+function Ym(a){var b=a.he(),c=a.gh();if(void 0!==b&&b.f&&void 0!==c){var c=b.Ga(c),d=b.Za(),b=a.f.style,e=a.Kg(),f=a.Lg(),g=e[0],e=e[1];if("bottom-right"==f||"center-right"==f||"top-right"==f)""!==a.a.fe&&(a.a.fe=b.left=""),g=Math.round(d[0]-c[0]-g)+"px",a.a.De!=g&&(a.a.De=b.right=g);else{""!==a.a.De&&(a.a.De=b.right="");if("bottom-center"==f||"center-center"==f||"top-center"==f)g-=a.f.offsetWidth/2;g=Math.round(c[0]+g)+"px";a.a.fe!=g&&(a.a.fe=b.left=g)}if("bottom-left"==f||"bottom-center"==f||"bottom-right"==
+f)""!==a.a.Fe&&(a.a.Fe=b.top=""),c=Math.round(d[1]-c[1]-e)+"px",a.a.Md!=c&&(a.a.Md=b.bottom=c);else{""!==a.a.Md&&(a.a.Md=b.bottom="");if("center-left"==f||"center-center"==f||"center-right"==f)e-=a.f.offsetHeight/2;c=Math.round(c[1]+e)+"px";a.a.Fe!=c&&(a.a.Fe=b.top=c)}$m(a,!0)}else $m(a,!1)};function an(a){a=a?a:{};this.l=void 0!==a.collapsed?a.collapsed:!0;this.o=void 0!==a.collapsible?a.collapsible:!0;this.o||(this.l=!1);var b=void 0!==a.className?a.className:"ol-overviewmap",c=void 0!==a.tipLabel?a.tipLabel:"Overview map",d=void 0!==a.collapseLabel?a.collapseLabel:"\u00ab";"string"===typeof d?(this.j=document.createElement("span"),this.j.textContent=d):this.j=d;d=void 0!==a.label?a.label:"\u00bb";"string"===typeof d?(this.v=document.createElement("span"),this.v.textContent=d):this.v=
+d;var e=this.o&&!this.l?this.j:this.v,d=document.createElement("button");d.setAttribute("type","button");d.title=c;d.appendChild(e);B(d,"click",this.gm,this);c=document.createElement("DIV");c.className="ol-overviewmap-map";var f=this.f=new Q({controls:new le,interactions:new le,target:c,view:a.view});a.layers&&a.layers.forEach(function(a){f.kg(a)},this);e=document.createElement("DIV");e.className="ol-overviewmap-box";e.style.boxSizing="border-box";this.A=new Xm({position:[0,0],positioning:"bottom-left",
+element:e});this.f.lg(this.A);e=document.createElement("div");e.className=b+" ol-unselectable ol-control"+(this.l&&this.o?" ol-collapsed":"")+(this.o?"":" ol-uncollapsible");e.appendChild(c);e.appendChild(d);Xe.call(this,{element:e,render:a.render?a.render:bn,target:a.target})}y(an,Xe);k=an.prototype;
+k.setMap=function(a){var b=this.a;a!==b&&(b&&(b=b.aa())&&Qa(b,gb("rotation"),this.de,this),Xe.prototype.setMap.call(this,a),a&&(this.s.push(B(a,"propertychange",this.Wk,this)),0===this.f.eh().dc()&&this.f.ji(a.xc()),a=a.aa()))&&(B(a,gb("rotation"),this.de,this),Wd(a)&&(this.f.Xc(),cn(this)))};k.Wk=function(a){"view"===a.key&&((a=a.oldValue)&&Qa(a,gb("rotation"),this.de,this),a=this.a.aa(),B(a,gb("rotation"),this.de,this))};k.de=function(){this.f.aa().ie(this.a.aa().La())};
+function bn(){var a=this.a,b=this.f;if(a.f&&b.f){var c=a.Za(),a=a.aa().Kc(c),d=b.Za(),c=b.aa().Kc(d),e=b.Ga(fc(a)),f=b.Ga(dc(a)),b=Math.abs(e[0]-f[0]),e=Math.abs(e[1]-f[1]),f=d[0],d=d[1];b<.1*f||e<.1*d||b>.75*f||e>.75*d?cn(this):Ub(c,a)||(a=this.f,c=this.a.aa(),a.aa().mb(c.ab()))}dn(this)}function cn(a){var b=a.a;a=a.f;var c=b.Za(),b=b.aa().Kc(c),c=a.Za();a=a.aa();oc(b,1/(.1*Math.pow(2,Math.log(7.5)/Math.LN2/2)));a.cf(b,c)}
+function dn(a){var b=a.a,c=a.f;if(b.f&&c.f){var d=b.Za(),e=b.aa(),f=c.aa();c.Za();var c=e.La(),b=a.A,g=a.A.Sd(),e=e.Kc(d),d=f.$(),f=cc(e),e=ec(e),h;if(a=a.a.aa().ab())h=[f[0]-a[0],f[1]-a[1]],Gb(h,c),Bb(h,a);b.uf(h);g&&(g.style.width=Math.abs((f[0]-e[0])/d)+"px",g.style.height=Math.abs((e[1]-f[1])/d)+"px")}}k.gm=function(a){a.preventDefault();en(this)};
+function en(a){a.element.classList.toggle("ol-collapsed");a.l?Te(a.j,a.v):Te(a.v,a.j);a.l=!a.l;var b=a.f;a.l||b.f||(b.Xc(),cn(a),Pa(b,"postrender",function(){dn(this)},a))}k.fm=function(){return this.o};k.im=function(a){this.o!==a&&(this.o=a,this.element.classList.toggle("ol-uncollapsible"),!a&&this.l&&en(this))};k.hm=function(a){this.o&&this.l!==a&&en(this)};k.em=function(){return this.l};k.pk=function(){return this.f};function fn(a){a=a?a:{};var b=void 0!==a.className?a.className:"ol-scale-line";this.o=document.createElement("DIV");this.o.className=b+"-inner";this.f=document.createElement("DIV");this.f.className=b+" ol-unselectable";this.f.appendChild(this.o);this.v=null;this.j=void 0!==a.minWidth?a.minWidth:64;this.l=!1;this.C=void 0;this.A="";Xe.call(this,{element:this.f,render:a.render?a.render:gn,target:a.target});B(this,gb("units"),this.R,this);this.D(a.units||"metric")}y(fn,Xe);var hn=[1,2,5];
+fn.prototype.wb=function(){return this.get("units")};function gn(a){(a=a.frameState)?this.v=a.viewState:this.v=null;jn(this)}fn.prototype.R=function(){jn(this)};fn.prototype.D=function(a){this.set("units",a)};
+function jn(a){var b=a.v;if(b){var c=b.projection,d=c.$b(),b=c.getPointResolution(b.resolution,b.center)*d,d=a.j*b,c="",e=a.wb();"degrees"==e?(c=uc.degrees,b/=c,d<c/60?(c="\u2033",b*=3600):d<c?(c="\u2032",b*=60):c="\u00b0"):"imperial"==e?.9144>d?(c="in",b/=.0254):1609.344>d?(c="ft",b/=.3048):(c="mi",b/=1609.344):"nautical"==e?(b/=1852,c="nm"):"metric"==e?1>d?(c="mm",b*=1E3):1E3>d?c="m":(c="km",b/=1E3):"us"==e&&(.9144>d?(c="in",b*=39.37):1609.344>d?(c="ft",b/=.30480061):(c="mi",b/=1609.3472));for(var e=
+3*Math.floor(Math.log(a.j*b)/Math.log(10)),f;;){f=hn[(e%3+3)%3]*Math.pow(10,Math.floor(e/3));d=Math.round(f/b);if(isNaN(d)){a.f.style.display="none";a.l=!1;return}if(d>=a.j)break;++e}b=f+" "+c;a.A!=b&&(a.o.innerHTML=b,a.A=b);a.C!=d&&(a.o.style.width=d+"px",a.C=d);a.l||(a.f.style.display="",a.l=!0)}else a.l&&(a.f.style.display="none",a.l=!1)};function kn(a){a=a?a:{};this.f=void 0;this.l=ln;this.v=[];this.C=this.j=0;this.T=null;this.ia=!1;this.Y=void 0!==a.duration?a.duration:200;var b=void 0!==a.className?a.className:"ol-zoomslider",c=document.createElement("button");c.setAttribute("type","button");c.className=b+"-thumb ol-unselectable";var d=document.createElement("div");d.className=b+" ol-unselectable ol-control";d.appendChild(c);this.o=new Pg(d);B(this.o,zg,this.Ik,this);B(this.o,Ag,this.Ng,this);B(this.o,Bg,this.Og,this);B(d,"click",
+this.Hk,this);B(c,"click",Ya);Xe.call(this,{element:d,render:a.render?a.render:mn})}y(kn,Xe);kn.prototype.ka=function(){Ta(this.o);Xe.prototype.ka.call(this)};var ln=0;k=kn.prototype;k.setMap=function(a){Xe.prototype.setMap.call(this,a);a&&a.render()};
+function mn(a){if(a.frameState){if(!this.ia){var b=this.element,c=b.offsetWidth,d=b.offsetHeight,e=b.firstElementChild,f=pa.getComputedStyle(e),b=e.offsetWidth+parseFloat(f.marginRight)+parseFloat(f.marginLeft),e=e.offsetHeight+parseFloat(f.marginTop)+parseFloat(f.marginBottom);this.T=[b,e];c>d?(this.l=1,this.C=c-b):(this.l=ln,this.j=d-e);this.ia=!0}a=a.frameState.viewState.resolution;a!==this.f&&(this.f=a,nn(this,a))}}
+k.Hk=function(a){var b=this.a,c=b.aa(),d=c.$();b.Wa(ee({resolution:d,duration:this.Y,easing:Zd}));a=on(this,sa(1===this.l?(a.offsetX-this.T[0]/2)/this.C:(a.offsetY-this.T[1]/2)/this.j,0,1));c.Ub(c.constrainResolution(a))};
+k.Ik=function(a){if(!this.A&&a.b.target===this.element.firstElementChild&&(Xd(this.a.aa(),1),this.D=a.clientX,this.R=a.clientY,this.A=!0,0===this.v.length)){a=this.Ng;var b=this.Og;this.v.push(B(document,"mousemove",a,this),B(document,"touchmove",a,this),B(document,Ag,a,this),B(document,"mouseup",b,this),B(document,"touchend",b,this),B(document,Bg,b,this))}};
+k.Ng=function(a){if(this.A){var b=this.element.firstElementChild;this.f=on(this,sa(1===this.l?(a.clientX-this.D+parseInt(b.style.left,10))/this.C:(a.clientY-this.R+parseInt(b.style.top,10))/this.j,0,1));this.a.aa().Ub(this.f);nn(this,this.f);this.D=a.clientX;this.R=a.clientY}};k.Og=function(){if(this.A){var a=this.a,b=a.aa();Xd(b,-1);a.Wa(ee({resolution:this.f,duration:this.Y,easing:Zd}));a=b.constrainResolution(this.f);b.Ub(a);this.A=!1;this.R=this.D=void 0;this.v.forEach(Ka);this.v.length=0}};
+function nn(a,b){var c;c=1-Vd(a.a.aa())(b);var d=a.element.firstElementChild;1==a.l?d.style.left=a.C*c+"px":d.style.top=a.j*c+"px"}function on(a,b){return Ud(a.a.aa())(1-b)};function pn(a){a=a?a:{};this.f=a.extent?a.extent:null;var b=void 0!==a.className?a.className:"ol-zoom-extent",c=void 0!==a.label?a.label:"E",d=void 0!==a.tipLabel?a.tipLabel:"Fit to extent",e=document.createElement("button");e.setAttribute("type","button");e.title=d;e.appendChild("string"===typeof c?document.createTextNode(c):c);B(e,"click",this.l,this);c=document.createElement("div");c.className=b+" ol-unselectable ol-control";c.appendChild(e);Xe.call(this,{element:c,target:a.target})}y(pn,Xe);
+pn.prototype.l=function(a){a.preventDefault();var b=this.a;a=b.aa();var c=this.f?this.f:a.l.H(),b=b.Za();a.cf(c,b)};function qn(a){eb.call(this);a=a?a:{};this.a=null;B(this,gb("tracking"),this.Il,this);this.rf(void 0!==a.tracking?a.tracking:!1)}y(qn,eb);k=qn.prototype;k.ka=function(){this.rf(!1);eb.prototype.ka.call(this)};
+k.co=function(a){if(null!==a.alpha){var b=wa(a.alpha);this.set("alpha",b);"boolean"===typeof a.absolute&&a.absolute?this.set("heading",b):ea(a.webkitCompassHeading)&&-1!=a.webkitCompassAccuracy&&this.set("heading",wa(a.webkitCompassHeading))}null!==a.beta&&this.set("beta",wa(a.beta));null!==a.gamma&&this.set("gamma",wa(a.gamma));this.u()};k.Oj=function(){return this.get("alpha")};k.Rj=function(){return this.get("beta")};k.Yj=function(){return this.get("gamma")};k.Hl=function(){return this.get("heading")};
+k.$g=function(){return this.get("tracking")};k.Il=function(){if(jg){var a=this.$g();a&&!this.a?this.a=B(pa,"deviceorientation",this.co,this):a||null===this.a||(Ka(this.a),this.a=null)}};k.rf=function(a){this.set("tracking",a)};function rn(){this.defaultDataProjection=null}function sn(a,b,c){var d;c&&(d={dataProjection:c.dataProjection?c.dataProjection:a.Oa(b),featureProjection:c.featureProjection});return tn(a,d)}function tn(a,b){var c;b&&(c={featureProjection:b.featureProjection,dataProjection:b.dataProjection?b.dataProjection:a.defaultDataProjection,rightHanded:b.rightHanded},b.decimals&&(c.decimals=b.decimals));return c}
+function un(a,b,c){var d=c?yc(c.featureProjection):null,e=c?yc(c.dataProjection):null,f;d&&e&&!Oc(d,e)?a instanceof Tc?f=(b?a.clone():a).jb(b?d:e,b?e:d):f=Sc(b?a.slice():a,b?d:e,b?e:d):f=a;if(b&&c&&c.decimals){var g=Math.pow(10,c.decimals);a=function(a){for(var b=0,c=a.length;b<c;++b)a[b]=Math.round(a[b]*g)/g;return a};Array.isArray(f)?a(f):f.rc(a)}return f};function vn(){this.defaultDataProjection=null}y(vn,rn);function wn(a){return fa(a)?a:"string"===typeof a?(a=JSON.parse(a))?a:null:null}k=vn.prototype;k.X=function(){return"json"};k.Rb=function(a,b){return this.Uc(wn(a),sn(this,a,b))};k.Fa=function(a,b){return this.Jf(wn(a),sn(this,a,b))};k.Vc=function(a,b){return this.Rh(wn(a),sn(this,a,b))};k.Oa=function(a){return this.Yh(wn(a))};k.Dd=function(a,b){return JSON.stringify(this.Yc(a,b))};k.Xb=function(a,b){return JSON.stringify(this.Ie(a,b))};
+k.Zc=function(a,b){return JSON.stringify(this.Ke(a,b))};function xn(a,b,c,d,e,f){var g=NaN,h=NaN,l=(c-b)/d;if(0!==l)if(1==l)g=a[b],h=a[b+1];else if(2==l)g=(1-e)*a[b]+e*a[b+d],h=(1-e)*a[b+1]+e*a[b+d+1];else{var h=a[b],l=a[b+1],m=0,g=[0],n;for(n=b+d;n<c;n+=d){var p=a[n],q=a[n+1],m=m+Math.sqrt((p-h)*(p-h)+(q-l)*(q-l));g.push(m);h=p;l=q}c=e*m;l=0;m=g.length;for(n=!1;l<m;)e=l+(m-l>>1),h=+ib(g[e],c),0>h?l=e+1:(m=e,n=!h);e=n?l:~l;0>e?(c=(c-g[-e-2])/(g[-e-1]-g[-e-2]),b+=(-e-2)*d,g=za(a[b],a[b+d],c),h=za(a[b+1],a[b+d+1],c)):(g=a[b+e*d],h=a[b+e*d+1])}return f?(f[0]=
+g,f[1]=h,f):[g,h]}function yn(a,b,c,d,e,f){if(c==b)return null;if(e<a[b+d-1])return f?(c=a.slice(b,b+d),c[d-1]=e,c):null;if(a[c-1]<e)return f?(c=a.slice(c-d,c),c[d-1]=e,c):null;if(e==a[b+d-1])return a.slice(b,b+d);b/=d;for(c/=d;b<c;)f=b+c>>1,e<a[(f+1)*d-1]?c=f:b=f+1;c=a[b*d-1];if(e==c)return a.slice((b-1)*d,(b-1)*d+d);f=(e-c)/(a[(b+1)*d-1]-c);c=[];var g;for(g=0;g<d-1;++g)c.push(za(a[(b-1)*d+g],a[b*d+g],f));c.push(e);return c}
+function zn(a,b,c,d,e,f){var g=0;if(f)return yn(a,g,b[b.length-1],c,d,e);if(d<a[c-1])return e?(a=a.slice(0,c),a[c-1]=d,a):null;if(a[a.length-1]<d)return e?(a=a.slice(a.length-c),a[c-1]=d,a):null;e=0;for(f=b.length;e<f;++e){var h=b[e];if(g!=h){if(d<a[g+c-1])break;if(d<=a[h-1])return yn(a,g,h,c,d,!1);g=h}}return null};function R(a,b){hd.call(this);this.i=null;this.C=this.D=this.j=-1;this.pa(a,b)}y(R,hd);k=R.prototype;k.wj=function(a){this.B?mb(this.B,a):this.B=a.slice();this.u()};k.clone=function(){var a=new R(null);a.ba(this.f,this.B.slice());return a};k.sb=function(a,b,c,d){if(d<Rb(this.H(),a,b))return d;this.C!=this.g&&(this.D=Math.sqrt(od(this.B,0,this.B.length,this.a,0)),this.C=this.g);return qd(this.B,0,this.B.length,this.a,this.D,!1,a,b,c,d)};
+k.Lj=function(a,b){return Fd(this.B,0,this.B.length,this.a,a,b)};k.lm=function(a,b){return"XYM"!=this.f&&"XYZM"!=this.f?null:yn(this.B,0,this.B.length,this.a,a,void 0!==b?b:!1)};k.Z=function(){return vd(this.B,0,this.B.length,this.a)};k.Bg=function(a,b){return xn(this.B,0,this.B.length,this.a,a,b)};k.mm=function(){var a=this.B,b=this.a,c=a[0],d=a[1],e=0,f;for(f=0+b;f<this.B.length;f+=b)var g=a[f],h=a[f+1],e=e+Math.sqrt((g-c)*(g-c)+(h-d)*(h-d)),c=g,d=h;return e};
+function Fj(a){a.j!=a.g&&(a.i=a.Bg(.5,a.i),a.j=a.g);return a.i}k.Nc=function(a){var b=[];b.length=xd(this.B,0,this.B.length,this.a,a,b,0);a=new R(null);a.ba("XY",b);return a};k.X=function(){return"LineString"};k.Ka=function(a){return Gd(this.B,0,this.B.length,this.a,a)};k.pa=function(a,b){a?(kd(this,b,a,1),this.B||(this.B=[]),this.B.length=td(this.B,0,a,this.a),this.u()):this.ba("XY",null)};k.ba=function(a,b){jd(this,a,b);this.u()};function S(a,b){hd.call(this);this.i=[];this.j=this.C=-1;this.pa(a,b)}y(S,hd);k=S.prototype;k.xj=function(a){this.B?mb(this.B,a.la().slice()):this.B=a.la().slice();this.i.push(this.B.length);this.u()};k.clone=function(){var a=new S(null);a.ba(this.f,this.B.slice(),this.i.slice());return a};k.sb=function(a,b,c,d){if(d<Rb(this.H(),a,b))return d;this.j!=this.g&&(this.C=Math.sqrt(pd(this.B,0,this.i,this.a,0)),this.j=this.g);return rd(this.B,0,this.i,this.a,this.C,!1,a,b,c,d)};
+k.om=function(a,b,c){return"XYM"!=this.f&&"XYZM"!=this.f||0===this.B.length?null:zn(this.B,this.i,this.a,a,void 0!==b?b:!1,void 0!==c?c:!1)};k.Z=function(){return wd(this.B,0,this.i,this.a)};k.Db=function(){return this.i};k.fk=function(a){if(0>a||this.i.length<=a)return null;var b=new R(null);b.ba(this.f,this.B.slice(0===a?0:this.i[a-1],this.i[a]));return b};
+k.md=function(){var a=this.B,b=this.i,c=this.f,d=[],e=0,f,g;f=0;for(g=b.length;f<g;++f){var h=b[f],l=new R(null);l.ba(c,a.slice(e,h));d.push(l);e=h}return d};function Gj(a){var b=[],c=a.B,d=0,e=a.i;a=a.a;var f,g;f=0;for(g=e.length;f<g;++f){var h=e[f],d=xn(c,d,h,a,.5);mb(b,d);d=h}return b}k.Nc=function(a){var b=[],c=[],d=this.B,e=this.i,f=this.a,g=0,h=0,l,m;l=0;for(m=e.length;l<m;++l){var n=e[l],h=xd(d,g,n,f,a,b,h);c.push(h);g=n}b.length=h;a=new S(null);a.ba("XY",b,c);return a};k.X=function(){return"MultiLineString"};
+k.Ka=function(a){a:{var b=this.B,c=this.i,d=this.a,e=0,f,g;f=0;for(g=c.length;f<g;++f){if(Gd(b,e,c[f],d,a)){a=!0;break a}e=c[f]}a=!1}return a};k.pa=function(a,b){if(a){kd(this,b,a,2);this.B||(this.B=[]);var c=ud(this.B,0,a,this.a,this.i);this.B.length=0===c.length?0:c[c.length-1];this.u()}else this.ba("XY",null,this.i)};k.ba=function(a,b,c){jd(this,a,b);this.i=c;this.u()};
+function An(a,b){var c=a.f,d=[],e=[],f,g;f=0;for(g=b.length;f<g;++f){var h=b[f];0===f&&(c=h.f);mb(d,h.la());e.push(d.length)}a.ba(c,d,e)};function Bn(a,b){hd.call(this);this.pa(a,b)}y(Bn,hd);k=Bn.prototype;k.zj=function(a){this.B?mb(this.B,a.la()):this.B=a.la().slice();this.u()};k.clone=function(){var a=new Bn(null);a.ba(this.f,this.B.slice());return a};k.sb=function(a,b,c,d){if(d<Rb(this.H(),a,b))return d;var e=this.B,f=this.a,g,h,l;g=0;for(h=e.length;g<h;g+=f)if(l=va(a,b,e[g],e[g+1]),l<d){d=l;for(l=0;l<f;++l)c[l]=e[g+l];c.length=f}return d};k.Z=function(){return vd(this.B,0,this.B.length,this.a)};
+k.rk=function(a){var b=this.B?this.B.length/this.a:0;if(0>a||b<=a)return null;b=new C(null);b.ba(this.f,this.B.slice(a*this.a,(a+1)*this.a));return b};k.je=function(){var a=this.B,b=this.f,c=this.a,d=[],e,f;e=0;for(f=a.length;e<f;e+=c){var g=new C(null);g.ba(b,a.slice(e,e+c));d.push(g)}return d};k.X=function(){return"MultiPoint"};k.Ka=function(a){var b=this.B,c=this.a,d,e,f,g;d=0;for(e=b.length;d<e;d+=c)if(f=b[d],g=b[d+1],Tb(a,f,g))return!0;return!1};
+k.pa=function(a,b){a?(kd(this,b,a,1),this.B||(this.B=[]),this.B.length=td(this.B,0,a,this.a),this.u()):this.ba("XY",null)};k.ba=function(a,b){jd(this,a,b);this.u()};function T(a,b){hd.call(this);this.i=[];this.C=-1;this.D=null;this.T=this.R=this.S=-1;this.j=null;this.pa(a,b)}y(T,hd);k=T.prototype;k.Aj=function(a){if(this.B){var b=this.B.length;mb(this.B,a.la());a=a.Db().slice();var c,d;c=0;for(d=a.length;c<d;++c)a[c]+=b}else this.B=a.la().slice(),a=a.Db().slice(),this.i.push();this.i.push(a);this.u()};k.clone=function(){for(var a=new T(null),b=this.i.length,c=Array(b),d=0;d<b;++d)c[d]=this.i[d].slice();Cn(a,this.f,this.B.slice(),c);return a};
+k.sb=function(a,b,c,d){if(d<Rb(this.H(),a,b))return d;if(this.R!=this.g){var e=this.i,f=0,g=0,h,l;h=0;for(l=e.length;h<l;++h)var m=e[h],g=pd(this.B,f,m,this.a,g),f=m[m.length-1];this.S=Math.sqrt(g);this.R=this.g}e=Hj(this);f=this.i;g=this.a;h=this.S;l=0;var m=[NaN,NaN],n,p;n=0;for(p=f.length;n<p;++n){var q=f[n];d=rd(e,l,q,g,h,!0,a,b,c,d,m);l=q[q.length-1]}return d};
+k.Bc=function(a,b){var c;a:{c=Hj(this);var d=this.i,e=0;if(0!==d.length){var f,g;f=0;for(g=d.length;f<g;++f){var h=d[f];if(Dd(c,e,h,this.a,a,b)){c=!0;break a}e=h[h.length-1]}}c=!1}return c};k.pm=function(){var a=Hj(this),b=this.i,c=0,d=0,e,f;e=0;for(f=b.length;e<f;++e)var g=b[e],d=d+md(a,c,g,this.a),c=g[g.length-1];return d};
+k.Z=function(a){var b;void 0!==a?(b=Hj(this).slice(),Ld(b,this.i,this.a,a)):b=this.B;a=b;b=this.i;var c=this.a,d=0,e=[],f=0,g,h;g=0;for(h=b.length;g<h;++g){var l=b[g];e[f++]=wd(a,d,l,c,e[f]);d=l[l.length-1]}e.length=f;return e};
+function Ij(a){if(a.C!=a.g){var b=a.B,c=a.i,d=a.a,e=0,f=[],g,h;g=0;for(h=c.length;g<h;++g){var l=c[g],e=Yb(b,e,l[0],d);f.push((e[0]+e[2])/2,(e[1]+e[3])/2);e=l[l.length-1]}b=Hj(a);c=a.i;d=a.a;g=0;h=[];l=0;for(e=c.length;l<e;++l){var m=c[l];h=Ed(b,g,m,d,f,2*l,h);g=m[m.length-1]}a.D=h;a.C=a.g}return a.D}k.ck=function(){var a=new Bn(null);a.ba("XY",Ij(this).slice());return a};
+function Hj(a){if(a.T!=a.g){var b=a.B,c;a:{c=a.i;var d,e;d=0;for(e=c.length;d<e;++d)if(!Jd(b,c[d],a.a,void 0)){c=!1;break a}c=!0}c?a.j=b:(a.j=b.slice(),a.j.length=Ld(a.j,a.i,a.a));a.T=a.g}return a.j}k.Nc=function(a){var b=[],c=[],d=this.B,e=this.i,f=this.a;a=Math.sqrt(a);var g=0,h=0,l,m;l=0;for(m=e.length;l<m;++l){var n=e[l],p=[],h=yd(d,g,n,f,a,b,h,p);c.push(p);g=n[n.length-1]}b.length=h;d=new T(null);Cn(d,"XY",b,c);return d};
+k.tk=function(a){if(0>a||this.i.length<=a)return null;var b;0===a?b=0:(b=this.i[a-1],b=b[b.length-1]);a=this.i[a].slice();var c=a[a.length-1];if(0!==b){var d,e;d=0;for(e=a.length;d<e;++d)a[d]-=b}d=new E(null);d.ba(this.f,this.B.slice(b,c),a);return d};k.Wd=function(){var a=this.f,b=this.B,c=this.i,d=[],e=0,f,g,h,l;f=0;for(g=c.length;f<g;++f){var m=c[f].slice(),n=m[m.length-1];if(0!==e)for(h=0,l=m.length;h<l;++h)m[h]-=e;h=new E(null);h.ba(a,b.slice(e,n),m);d.push(h);e=n}return d};k.X=function(){return"MultiPolygon"};
+k.Ka=function(a){a:{var b=Hj(this),c=this.i,d=this.a,e=0,f,g;f=0;for(g=c.length;f<g;++f){var h=c[f];if(Hd(b,e,h,d,a)){a=!0;break a}e=h[h.length-1]}a=!1}return a};k.pa=function(a,b){if(a){kd(this,b,a,3);this.B||(this.B=[]);var c=this.B,d=this.a,e=this.i,f=0,e=e?e:[],g=0,h,l;h=0;for(l=a.length;h<l;++h)f=ud(c,f,a[h],d,e[g]),e[g++]=f,f=f[f.length-1];e.length=g;0===e.length?this.B.length=0:(c=e[e.length-1],this.B.length=0===c.length?0:c[c.length-1]);this.u()}else Cn(this,"XY",null,this.i)};
+function Cn(a,b,c,d){jd(a,b,c);a.i=d;a.u()}function Dn(a,b){var c=a.f,d=[],e=[],f,g,h;f=0;for(g=b.length;f<g;++f){var l=b[f];0===f&&(c=l.f);var m=d.length;h=l.Db();var n,p;n=0;for(p=h.length;n<p;++n)h[n]+=m;mb(d,l.la());e.push(h)}Cn(a,c,d,e)};function En(a){a=a?a:{};this.defaultDataProjection=null;this.b=a.geometryName}y(En,vn);
+function Fn(a,b){if(!a)return null;var c;if(ea(a.x)&&ea(a.y))c="Point";else if(a.points)c="MultiPoint";else if(a.paths)c=1===a.paths.length?"LineString":"MultiLineString";else if(a.rings){var d=a.rings,e=Gn(a),f=[];c=[];var g,h;g=0;for(h=d.length;g<h;++g){var l=lb(d[g]);Id(l,0,l.length,e.length)?f.push([d[g]]):c.push(d[g])}for(;c.length;){d=c.shift();e=!1;for(g=f.length-1;0<=g;g--)if(Ub((new zd(f[g][0])).H(),(new zd(d)).H())){f[g].push(d);e=!0;break}e||f.push([d.reverse()])}a=Ea({},a);1===f.length?
+(c="Polygon",a.rings=f[0]):(c="MultiPolygon",a.rings=f)}return un((0,Hn[c])(a),!1,b)}function Gn(a){var b="XY";!0===a.hasZ&&!0===a.hasM?b="XYZM":!0===a.hasZ?b="XYZ":!0===a.hasM&&(b="XYM");return b}function In(a){a=a.f;return{hasZ:"XYZ"===a||"XYZM"===a,hasM:"XYM"===a||"XYZM"===a}}
+var Hn={Point:function(a){return void 0!==a.m&&void 0!==a.z?new C([a.x,a.y,a.z,a.m],"XYZM"):void 0!==a.z?new C([a.x,a.y,a.z],"XYZ"):void 0!==a.m?new C([a.x,a.y,a.m],"XYM"):new C([a.x,a.y])},LineString:function(a){return new R(a.paths[0],Gn(a))},Polygon:function(a){return new E(a.rings,Gn(a))},MultiPoint:function(a){return new Bn(a.points,Gn(a))},MultiLineString:function(a){return new S(a.paths,Gn(a))},MultiPolygon:function(a){return new T(a.rings,Gn(a))}},Jn={Point:function(a){var b=a.Z();a=a.f;if("XYZ"===
+a)return{x:b[0],y:b[1],z:b[2]};if("XYM"===a)return{x:b[0],y:b[1],m:b[2]};if("XYZM"===a)return{x:b[0],y:b[1],z:b[2],m:b[3]};if("XY"===a)return{x:b[0],y:b[1]}},LineString:function(a){var b=In(a);return{hasZ:b.hasZ,hasM:b.hasM,paths:[a.Z()]}},Polygon:function(a){var b=In(a);return{hasZ:b.hasZ,hasM:b.hasM,rings:a.Z(!1)}},MultiPoint:function(a){var b=In(a);return{hasZ:b.hasZ,hasM:b.hasM,points:a.Z()}},MultiLineString:function(a){var b=In(a);return{hasZ:b.hasZ,hasM:b.hasM,paths:a.Z()}},MultiPolygon:function(a){var b=
+In(a);a=a.Z(!1);for(var c=[],d=0;d<a.length;d++)for(var e=a[d].length-1;0<=e;e--)c.push(a[d][e]);return{hasZ:b.hasZ,hasM:b.hasM,rings:c}}};k=En.prototype;k.Uc=function(a,b){var c=Fn(a.geometry,b),d=new Ik;this.b&&d.Ec(this.b);d.Ua(c);b&&b.mf&&a.attributes[b.mf]&&d.mc(a.attributes[b.mf]);a.attributes&&d.G(a.attributes);return d};
+k.Jf=function(a,b){var c=b?b:{};if(a.features){var d=[],e=a.features,f,g;c.mf=a.objectIdFieldName;f=0;for(g=e.length;f<g;++f)d.push(this.Uc(e[f],c));return d}return[this.Uc(a,c)]};k.Rh=function(a,b){return Fn(a,b)};k.Yh=function(a){return a.spatialReference&&a.spatialReference.wkid?yc("EPSG:"+a.spatialReference.wkid):null};function Kn(a,b){return(0,Jn[a.X()])(un(a,!0,b),b)}k.Ke=function(a,b){return Kn(a,tn(this,b))};
+k.Yc=function(a,b){b=tn(this,b);var c={},d=a.W();d&&(c.geometry=Kn(d,b));d=a.O();delete d[a.a];c.attributes=Ha(d)?{}:d;b&&b.featureProjection&&(c.spatialReference={wkid:yc(b.featureProjection).cb.split(":").pop()});return c};k.Ie=function(a,b){b=tn(this,b);var c=[],d,e;d=0;for(e=a.length;d<e;++d)c.push(this.Yc(a[d],b));return{features:c}};function Ln(a){Tc.call(this);this.c=a?a:null;Mn(this)}y(Ln,Tc);function Nn(a){var b=[],c,d;c=0;for(d=a.length;c<d;++c)b.push(a[c].clone());return b}function On(a){var b,c;if(a.c)for(b=0,c=a.c.length;b<c;++b)Qa(a.c[b],"change",a.u,a)}function Mn(a){var b,c;if(a.c)for(b=0,c=a.c.length;b<c;++b)B(a.c[b],"change",a.u,a)}k=Ln.prototype;k.clone=function(){var a=new Ln(null);a.hi(this.c);return a};
+k.sb=function(a,b,c,d){if(d<Rb(this.H(),a,b))return d;var e=this.c,f,g;f=0;for(g=e.length;f<g;++f)d=e[f].sb(a,b,c,d);return d};k.Bc=function(a,b){var c=this.c,d,e;d=0;for(e=c.length;d<e;++d)if(c[d].Bc(a,b))return!0;return!1};k.Od=function(a){Wb(Infinity,Infinity,-Infinity,-Infinity,a);for(var b=this.c,c=0,d=b.length;c<d;++c)ac(a,b[c].H());return a};k.ff=function(){return Nn(this.c)};
+k.od=function(a){this.s!=this.g&&(Fa(this.l),this.o=0,this.s=this.g);if(0>a||0!==this.o&&a<this.o)return this;var b=a.toString();if(this.l.hasOwnProperty(b))return this.l[b];var c=[],d=this.c,e=!1,f,g;f=0;for(g=d.length;f<g;++f){var h=d[f],l=h.od(a);c.push(l);l!==h&&(e=!0)}if(e)return a=new Ln(null),On(a),a.c=c,Mn(a),a.u(),this.l[b]=a;this.o=a;return this};k.X=function(){return"GeometryCollection"};k.Ka=function(a){var b=this.c,c,d;c=0;for(d=b.length;c<d;++c)if(b[c].Ka(a))return!0;return!1};
+k.Ya=function(){return 0===this.c.length};k.rotate=function(a,b){for(var c=this.c,d=0,e=c.length;d<e;++d)c[d].rotate(a,b);this.u()};k.hi=function(a){a=Nn(a);On(this);this.c=a;Mn(this);this.u()};k.rc=function(a){var b=this.c,c,d;c=0;for(d=b.length;c<d;++c)b[c].rc(a);this.u()};k.Sc=function(a,b){var c=this.c,d,e;d=0;for(e=c.length;d<e;++d)c[d].Sc(a,b);this.u()};k.ka=function(){On(this);Tc.prototype.ka.call(this)};function Pn(a){a=a?a:{};this.defaultDataProjection=null;this.defaultDataProjection=yc(a.defaultDataProjection?a.defaultDataProjection:"EPSG:4326");this.b=a.geometryName}y(Pn,vn);function Qn(a,b){return a?un((0,Rn[a.type])(a),!1,b):null}function Sn(a,b){return(0,Tn[a.X()])(un(a,!0,b),b)}
+var Rn={Point:function(a){return new C(a.coordinates)},LineString:function(a){return new R(a.coordinates)},Polygon:function(a){return new E(a.coordinates)},MultiPoint:function(a){return new Bn(a.coordinates)},MultiLineString:function(a){return new S(a.coordinates)},MultiPolygon:function(a){return new T(a.coordinates)},GeometryCollection:function(a,b){var c=a.geometries.map(function(a){return Qn(a,b)});return new Ln(c)}},Tn={Point:function(a){return{type:"Point",coordinates:a.Z()}},LineString:function(a){return{type:"LineString",
+coordinates:a.Z()}},Polygon:function(a,b){var c;b&&(c=b.rightHanded);return{type:"Polygon",coordinates:a.Z(c)}},MultiPoint:function(a){return{type:"MultiPoint",coordinates:a.Z()}},MultiLineString:function(a){return{type:"MultiLineString",coordinates:a.Z()}},MultiPolygon:function(a,b){var c;b&&(c=b.rightHanded);return{type:"MultiPolygon",coordinates:a.Z(c)}},GeometryCollection:function(a,b){return{type:"GeometryCollection",geometries:a.c.map(function(a){var d=Ea({},b);delete d.featureProjection;return Sn(a,
+d)})}},Circle:function(){return{type:"GeometryCollection",geometries:[]}}};k=Pn.prototype;k.Uc=function(a,b){var c=Qn(a.geometry,b),d=new Ik;this.b&&d.Ec(this.b);d.Ua(c);void 0!==a.id&&d.mc(a.id);a.properties&&d.G(a.properties);return d};k.Jf=function(a,b){if("Feature"==a.type)return[this.Uc(a,b)];if("FeatureCollection"==a.type){var c=[],d=a.features,e,f;e=0;for(f=d.length;e<f;++e)c.push(this.Uc(d[e],b));return c}return[]};k.Rh=function(a,b){return Qn(a,b)};
+k.Yh=function(a){return(a=a.crs)?"name"==a.type?yc(a.properties.name):"EPSG"==a.type?yc("EPSG:"+a.properties.code):null:this.defaultDataProjection};k.Yc=function(a,b){b=tn(this,b);var c={type:"Feature"},d=a.Xa();void 0!==d&&(c.id=d);(d=a.W())?c.geometry=Sn(d,b):c.geometry=null;d=a.O();delete d[a.a];Ha(d)?c.properties=null:c.properties=d;return c};k.Ie=function(a,b){b=tn(this,b);var c=[],d,e;d=0;for(e=a.length;d<e;++d)c.push(this.Yc(a[d],b));return{type:"FeatureCollection",features:c}};
+k.Ke=function(a,b){return Sn(a,tn(this,b))};function Un(){this.f=new XMLSerializer;this.defaultDataProjection=null}y(Un,rn);k=Un.prototype;k.X=function(){return"xml"};k.Rb=function(a,b){if(Pk(a))return Vn(this,a,b);if(Qk(a))return this.Ph(a,b);if("string"===typeof a){var c=Rk(a);return Vn(this,c,b)}return null};function Vn(a,b,c){a=Wn(a,b,c);return 0<a.length?a[0]:null}k.Fa=function(a,b){if(Pk(a))return Wn(this,a,b);if(Qk(a))return this.lc(a,b);if("string"===typeof a){var c=Rk(a);return Wn(this,c,b)}return[]};
+function Wn(a,b,c){var d=[];for(b=b.firstChild;b;b=b.nextSibling)b.nodeType==Node.ELEMENT_NODE&&mb(d,a.lc(b,c));return d}k.Vc=function(a,b){if(Pk(a))return this.v(a,b);if(Qk(a)){var c=this.ye(a,[sn(this,a,b?b:{})]);return c?c:null}return"string"===typeof a?(c=Rk(a),this.v(c,b)):null};k.Oa=function(a){return Pk(a)?this.Pf(a):Qk(a)?this.Be(a):"string"===typeof a?(a=Rk(a),this.Pf(a)):null};k.Pf=function(){return this.defaultDataProjection};k.Be=function(){return this.defaultDataProjection};
+k.Dd=function(a,b){var c=this.A(a,b);return this.f.serializeToString(c)};k.Xb=function(a,b){var c=this.a(a,b);return this.f.serializeToString(c)};k.Zc=function(a,b){var c=this.s(a,b);return this.f.serializeToString(c)};function Xn(a){a=a?a:{};this.featureType=a.featureType;this.featureNS=a.featureNS;this.srsName=a.srsName;this.schemaLocation="";this.b={};this.b["http://www.opengis.net/gml"]={featureMember:Uk(Xn.prototype.vd),featureMembers:Uk(Xn.prototype.vd)};Un.call(this)}y(Xn,Un);var Yn=/^[\s\xa0]*$/;k=Xn.prototype;
+k.vd=function(a,b){var c=a.localName,d=null;if("FeatureCollection"==c)"http://www.opengis.net/wfs"===a.namespaceURI?d=O([],this.b,a,b,this):d=O(null,this.b,a,b,this);else if("featureMembers"==c||"featureMember"==c){var e=b[0],f=e.featureType,g=e.featureNS,h,l;if(!f&&a.childNodes){f=[];g={};h=0;for(l=a.childNodes.length;h<l;++h){var m=a.childNodes[h];if(1===m.nodeType){var n=m.nodeName.split(":").pop();if(-1===f.indexOf(n)){var p="",q=0,m=m.namespaceURI,r;for(r in g){if(g[r]===m){p=r;break}++q}p||
+(p="p"+q,g[p]=m);f.push(p+":"+n)}}}"featureMember"!=c&&(e.featureType=f,e.featureNS=g)}"string"===typeof g&&(h=g,g={},g.p0=h);var e={},f=Array.isArray(f)?f:[f],u;for(u in g){n={};h=0;for(l=f.length;h<l;++h)(-1===f[h].indexOf(":")?"p0":f[h].split(":")[0])===u&&(n[f[h].split(":").pop()]="featureMembers"==c?Tk(this.If,this):Uk(this.If,this));e[g[u]]=n}"featureMember"==c?d=O(void 0,e,a,b):d=O([],e,a,b)}null===d&&(d=[]);return d};
+k.ye=function(a,b){var c=b[0];c.srsName=a.firstElementChild.getAttribute("srsName");var d=O(null,this.bg,a,b,this);if(d)return un(d,!1,c)};
+k.If=function(a,b){var c,d;(d=a.getAttribute("fid"))||(d=a.getAttributeNS("http://www.opengis.net/gml","id")||"");var e={},f;for(c=a.firstElementChild;c;c=c.nextElementSibling){var g=c.localName;if(0===c.childNodes.length||1===c.childNodes.length&&(3===c.firstChild.nodeType||4===c.firstChild.nodeType)){var h=Nk(c,!1);Yn.test(h)&&(h=void 0);e[g]=h}else"boundedBy"!==g&&(f=g),e[g]=this.ye(c,b)}c=new Ik(e);f&&c.Ec(f);d&&c.mc(d);return c};
+k.Xh=function(a,b){var c=this.xe(a,b);if(c){var d=new C(null);d.ba("XYZ",c);return d}};k.Vh=function(a,b){var c=O([],this.Ui,a,b,this);if(c)return new Bn(c)};k.Uh=function(a,b){var c=O([],this.Ti,a,b,this);if(c){var d=new S(null);An(d,c);return d}};k.Wh=function(a,b){var c=O([],this.Vi,a,b,this);if(c){var d=new T(null);Dn(d,c);return d}};k.Mh=function(a,b){al(this.Yi,a,b,this)};k.Ug=function(a,b){al(this.Ri,a,b,this)};k.Nh=function(a,b){al(this.Zi,a,b,this)};
+k.ze=function(a,b){var c=this.xe(a,b);if(c){var d=new R(null);d.ba("XYZ",c);return d}};k.zo=function(a,b){var c=O(null,this.Fd,a,b,this);if(c)return c};k.Th=function(a,b){var c=this.xe(a,b);if(c){var d=new zd(null);Ad(d,"XYZ",c);return d}};k.Ae=function(a,b){var c=O([null],this.Me,a,b,this);if(c&&c[0]){var d=new E(null),e=c[0],f=[e.length],g,h;g=1;for(h=c.length;g<h;++g)mb(e,c[g]),f.push(e.length);d.ba("XYZ",e,f);return d}};k.xe=function(a,b){return O(null,this.Fd,a,b,this)};
+k.Ui={"http://www.opengis.net/gml":{pointMember:Tk(Xn.prototype.Mh),pointMembers:Tk(Xn.prototype.Mh)}};k.Ti={"http://www.opengis.net/gml":{lineStringMember:Tk(Xn.prototype.Ug),lineStringMembers:Tk(Xn.prototype.Ug)}};k.Vi={"http://www.opengis.net/gml":{polygonMember:Tk(Xn.prototype.Nh),polygonMembers:Tk(Xn.prototype.Nh)}};k.Yi={"http://www.opengis.net/gml":{Point:Tk(Xn.prototype.xe)}};k.Ri={"http://www.opengis.net/gml":{LineString:Tk(Xn.prototype.ze)}};k.Zi={"http://www.opengis.net/gml":{Polygon:Tk(Xn.prototype.Ae)}};
+k.Gd={"http://www.opengis.net/gml":{LinearRing:Uk(Xn.prototype.zo)}};k.lc=function(a,b){var c={featureType:this.featureType,featureNS:this.featureNS};b&&Ea(c,sn(this,a,b));return this.vd(a,[c])||[]};k.Be=function(a){return yc(this.srsName?this.srsName:a.firstElementChild.getAttribute("srsName"))};function Zn(a){a=Nk(a,!1);return $n(a)}function $n(a){if(a=/^\s*(true|1)|(false|0)\s*$/.exec(a))return void 0!==a[1]||!1}
+function ao(a){a=Nk(a,!1);if(a=/^\s*(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(Z|(?:([+\-])(\d{2})(?::(\d{2}))?))\s*$/.exec(a)){var b=Date.UTC(parseInt(a[1],10),parseInt(a[2],10)-1,parseInt(a[3],10),parseInt(a[4],10),parseInt(a[5],10),parseInt(a[6],10))/1E3;if("Z"!=a[7]){var c="-"==a[8]?-1:1,b=b+60*c*parseInt(a[9],10);void 0!==a[10]&&(b+=3600*c*parseInt(a[10],10))}return b}}function bo(a){a=Nk(a,!1);return co(a)}
+function co(a){if(a=/^\s*([+\-]?\d*\.?\d+(?:e[+\-]?\d+)?)\s*$/i.exec(a))return parseFloat(a[1])}function eo(a){a=Nk(a,!1);return fo(a)}function fo(a){if(a=/^\s*(\d+)\s*$/.exec(a))return parseInt(a[1],10)}function U(a){return Nk(a,!1).trim()}function go(a,b){ho(a,b?"1":"0")}function io(a,b){a.appendChild(Lk.createTextNode(b.toPrecision()))}function jo(a,b){a.appendChild(Lk.createTextNode(b.toString()))}function ho(a,b){a.appendChild(Lk.createTextNode(b))};function ko(a){a=a?a:{};Xn.call(this,a);this.b["http://www.opengis.net/gml"].featureMember=Tk(Xn.prototype.vd);this.schemaLocation=a.schemaLocation?a.schemaLocation:"http://www.opengis.net/gml http://schemas.opengis.net/gml/2.1.2/feature.xsd"}y(ko,Xn);k=ko.prototype;
+k.Qh=function(a,b){var c=Nk(a,!1).replace(/^\s*|\s*$/g,""),d=b[0].srsName,e=a.parentNode.getAttribute("srsDimension"),f="enu";d&&(d=yc(d))&&(f=d.b);c=c.split(/[\s,]+/);d=2;a.getAttribute("srsDimension")?d=fo(a.getAttribute("srsDimension")):a.getAttribute("dimension")?d=fo(a.getAttribute("dimension")):e&&(d=fo(e));for(var g,h,l=[],m=0,n=c.length;m<n;m+=d)e=parseFloat(c[m]),g=parseFloat(c[m+1]),h=3===d?parseFloat(c[m+2]):0,"en"===f.substr(0,2)?l.push(e,g,h):l.push(g,e,h);return l};
+k.wo=function(a,b){var c=O([null],this.Ni,a,b,this);return Wb(c[1][0],c[1][1],c[1][3],c[1][4])};k.ml=function(a,b){var c=O(void 0,this.Gd,a,b,this);c&&b[b.length-1].push(c)};k.eo=function(a,b){var c=O(void 0,this.Gd,a,b,this);c&&(b[b.length-1][0]=c)};k.Fd={"http://www.opengis.net/gml":{coordinates:Uk(ko.prototype.Qh)}};k.Me={"http://www.opengis.net/gml":{innerBoundaryIs:ko.prototype.ml,outerBoundaryIs:ko.prototype.eo}};k.Ni={"http://www.opengis.net/gml":{coordinates:Tk(ko.prototype.Qh)}};
+k.bg={"http://www.opengis.net/gml":{Point:Uk(Xn.prototype.Xh),MultiPoint:Uk(Xn.prototype.Vh),LineString:Uk(Xn.prototype.ze),MultiLineString:Uk(Xn.prototype.Uh),LinearRing:Uk(Xn.prototype.Th),Polygon:Uk(Xn.prototype.Ae),MultiPolygon:Uk(Xn.prototype.Wh),Box:Uk(ko.prototype.wo)}};function lo(a){a=a?a:{};Xn.call(this,a);this.j=void 0!==a.surface?a.surface:!1;this.i=void 0!==a.curve?a.curve:!1;this.l=void 0!==a.multiCurve?a.multiCurve:!0;this.o=void 0!==a.multiSurface?a.multiSurface:!0;this.schemaLocation=a.schemaLocation?a.schemaLocation:"http://www.opengis.net/gml http://schemas.opengis.net/gml/3.1.1/profiles/gmlsfProfile/1.0.0/gmlsf.xsd"}y(lo,Xn);k=lo.prototype;k.Do=function(a,b){var c=O([],this.Si,a,b,this);if(c){var d=new S(null);An(d,c);return d}};
+k.Eo=function(a,b){var c=O([],this.Wi,a,b,this);if(c){var d=new T(null);Dn(d,c);return d}};k.vg=function(a,b){al(this.Oi,a,b,this)};k.vi=function(a,b){al(this.cj,a,b,this)};k.Ho=function(a,b){return O([null],this.Xi,a,b,this)};k.Jo=function(a,b){return O([null],this.bj,a,b,this)};k.Io=function(a,b){return O([null],this.Me,a,b,this)};k.Co=function(a,b){return O([null],this.Fd,a,b,this)};k.ol=function(a,b){var c=O(void 0,this.Gd,a,b,this);c&&b[b.length-1].push(c)};
+k.Hj=function(a,b){var c=O(void 0,this.Gd,a,b,this);c&&(b[b.length-1][0]=c)};k.Zh=function(a,b){var c=O([null],this.dj,a,b,this);if(c&&c[0]){var d=new E(null),e=c[0],f=[e.length],g,h;g=1;for(h=c.length;g<h;++g)mb(e,c[g]),f.push(e.length);d.ba("XYZ",e,f);return d}};k.Oh=function(a,b){var c=O([null],this.Pi,a,b,this);if(c){var d=new R(null);d.ba("XYZ",c);return d}};k.yo=function(a,b){var c=O([null],this.Qi,a,b,this);return Wb(c[1][0],c[1][1],c[2][0],c[2][1])};
+k.Ao=function(a,b){for(var c=Nk(a,!1),d=/^\s*([+\-]?\d*\.?\d+(?:[eE][+\-]?\d+)?)\s*/,e=[],f;f=d.exec(c);)e.push(parseFloat(f[1])),c=c.substr(f[0].length);if(""===c){c=b[0].srsName;d="enu";c&&(d=yc(c).b);if("neu"===d)for(c=0,d=e.length;c<d;c+=3)f=e[c],e[c]=e[c+1],e[c+1]=f;c=e.length;2==c&&e.push(0);return 0===c?void 0:e}};
+k.Mf=function(a,b){var c=Nk(a,!1).replace(/^\s*|\s*$/g,""),d=b[0].srsName,e=a.parentNode.getAttribute("srsDimension"),f="enu";d&&(f=yc(d).b);c=c.split(/\s+/);d=2;a.getAttribute("srsDimension")?d=fo(a.getAttribute("srsDimension")):a.getAttribute("dimension")?d=fo(a.getAttribute("dimension")):e&&(d=fo(e));for(var g,h,l=[],m=0,n=c.length;m<n;m+=d)e=parseFloat(c[m]),g=parseFloat(c[m+1]),h=3===d?parseFloat(c[m+2]):0,"en"===f.substr(0,2)?l.push(e,g,h):l.push(g,e,h);return l};
+k.Fd={"http://www.opengis.net/gml":{pos:Uk(lo.prototype.Ao),posList:Uk(lo.prototype.Mf)}};k.Me={"http://www.opengis.net/gml":{interior:lo.prototype.ol,exterior:lo.prototype.Hj}};
+k.bg={"http://www.opengis.net/gml":{Point:Uk(Xn.prototype.Xh),MultiPoint:Uk(Xn.prototype.Vh),LineString:Uk(Xn.prototype.ze),MultiLineString:Uk(Xn.prototype.Uh),LinearRing:Uk(Xn.prototype.Th),Polygon:Uk(Xn.prototype.Ae),MultiPolygon:Uk(Xn.prototype.Wh),Surface:Uk(lo.prototype.Zh),MultiSurface:Uk(lo.prototype.Eo),Curve:Uk(lo.prototype.Oh),MultiCurve:Uk(lo.prototype.Do),Envelope:Uk(lo.prototype.yo)}};k.Si={"http://www.opengis.net/gml":{curveMember:Tk(lo.prototype.vg),curveMembers:Tk(lo.prototype.vg)}};
+k.Wi={"http://www.opengis.net/gml":{surfaceMember:Tk(lo.prototype.vi),surfaceMembers:Tk(lo.prototype.vi)}};k.Oi={"http://www.opengis.net/gml":{LineString:Tk(Xn.prototype.ze),Curve:Tk(lo.prototype.Oh)}};k.cj={"http://www.opengis.net/gml":{Polygon:Tk(Xn.prototype.Ae),Surface:Tk(lo.prototype.Zh)}};k.dj={"http://www.opengis.net/gml":{patches:Uk(lo.prototype.Ho)}};k.Pi={"http://www.opengis.net/gml":{segments:Uk(lo.prototype.Jo)}};k.Qi={"http://www.opengis.net/gml":{lowerCorner:Tk(lo.prototype.Mf),upperCorner:Tk(lo.prototype.Mf)}};
+k.Xi={"http://www.opengis.net/gml":{PolygonPatch:Uk(lo.prototype.Io)}};k.bj={"http://www.opengis.net/gml":{LineStringSegment:Uk(lo.prototype.Co)}};function mo(a,b,c){c=c[c.length-1].srsName;b=b.Z();for(var d=b.length,e=Array(d),f,g=0;g<d;++g){f=b[g];var h=g,l="enu";c&&(l=yc(c).b);e[h]="en"===l.substr(0,2)?f[0]+" "+f[1]:f[1]+" "+f[0]}ho(a,e.join(" "))}
+k.Ji=function(a,b,c){var d=c[c.length-1].srsName;d&&a.setAttribute("srsName",d);d=Mk(a.namespaceURI,"pos");a.appendChild(d);c=c[c.length-1].srsName;a="enu";c&&(a=yc(c).b);b=b.Z();ho(d,"en"===a.substr(0,2)?b[0]+" "+b[1]:b[1]+" "+b[0])};var no={"http://www.opengis.net/gml":{lowerCorner:L(ho),upperCorner:L(ho)}};k=lo.prototype;k.xp=function(a,b,c){var d=c[c.length-1].srsName;d&&a.setAttribute("srsName",d);bl({node:a},no,Zk,[b[0]+" "+b[1],b[2]+" "+b[3]],c,["lowerCorner","upperCorner"],this)};
+k.Gi=function(a,b,c){var d=c[c.length-1].srsName;d&&a.setAttribute("srsName",d);d=Mk(a.namespaceURI,"posList");a.appendChild(d);mo(d,b,c)};k.aj=function(a,b){var c=b[b.length-1],d=c.node,e=c.exteriorWritten;void 0===e&&(c.exteriorWritten=!0);return Mk(d.namespaceURI,void 0!==e?"interior":"exterior")};
+k.Le=function(a,b,c){var d=c[c.length-1].srsName;"PolygonPatch"!==a.nodeName&&d&&a.setAttribute("srsName",d);"Polygon"===a.nodeName||"PolygonPatch"===a.nodeName?(b=b.Vd(),bl({node:a,srsName:d},oo,this.aj,b,c,void 0,this)):"Surface"===a.nodeName&&(d=Mk(a.namespaceURI,"patches"),a.appendChild(d),a=Mk(d.namespaceURI,"PolygonPatch"),d.appendChild(a),this.Le(a,b,c))};
+k.Ge=function(a,b,c){var d=c[c.length-1].srsName;"LineStringSegment"!==a.nodeName&&d&&a.setAttribute("srsName",d);"LineString"===a.nodeName||"LineStringSegment"===a.nodeName?(d=Mk(a.namespaceURI,"posList"),a.appendChild(d),mo(d,b,c)):"Curve"===a.nodeName&&(d=Mk(a.namespaceURI,"segments"),a.appendChild(d),a=Mk(d.namespaceURI,"LineStringSegment"),d.appendChild(a),this.Ge(a,b,c))};
+k.Ii=function(a,b,c){var d=c[c.length-1],e=d.srsName,d=d.surface;e&&a.setAttribute("srsName",e);b=b.Wd();bl({node:a,srsName:e,surface:d},po,this.c,b,c,void 0,this)};k.yp=function(a,b,c){var d=c[c.length-1].srsName;d&&a.setAttribute("srsName",d);b=b.je();bl({node:a,srsName:d},qo,Xk("pointMember"),b,c,void 0,this)};k.Hi=function(a,b,c){var d=c[c.length-1],e=d.srsName,d=d.curve;e&&a.setAttribute("srsName",e);b=b.md();bl({node:a,srsName:e,curve:d},ro,this.c,b,c,void 0,this)};
+k.Ki=function(a,b,c){var d=Mk(a.namespaceURI,"LinearRing");a.appendChild(d);this.Gi(d,b,c)};k.Li=function(a,b,c){var d=this.g(b,c);d&&(a.appendChild(d),this.Le(d,b,c))};k.zp=function(a,b,c){var d=Mk(a.namespaceURI,"Point");a.appendChild(d);this.Ji(d,b,c)};k.Fi=function(a,b,c){var d=this.g(b,c);d&&(a.appendChild(d),this.Ge(d,b,c))};
+k.Je=function(a,b,c){var d=c[c.length-1],e=Ea({},d);e.node=a;var f;Array.isArray(b)?d.dataProjection?f=Sc(b,d.featureProjection,d.dataProjection):f=b:f=un(b,!0,d);bl(e,so,this.g,[f],c,void 0,this)};
+k.Bi=function(a,b,c){var d=b.Xa();d&&a.setAttribute("fid",d);var d=c[c.length-1],e=d.featureNS,f=b.a;d.Dc||(d.Dc={},d.Dc[e]={});var g=b.O();b=[];var h=[],l;for(l in g){var m=g[l];null!==m&&(b.push(l),h.push(m),l==f||m instanceof Tc?l in d.Dc[e]||(d.Dc[e][l]=L(this.Je,this)):l in d.Dc[e]||(d.Dc[e][l]=L(ho)))}l=Ea({},d);l.node=a;bl(l,d.Dc,Xk(void 0,e),h,c,b)};
+var po={"http://www.opengis.net/gml":{surfaceMember:L(lo.prototype.Li),polygonMember:L(lo.prototype.Li)}},qo={"http://www.opengis.net/gml":{pointMember:L(lo.prototype.zp)}},ro={"http://www.opengis.net/gml":{lineStringMember:L(lo.prototype.Fi),curveMember:L(lo.prototype.Fi)}},oo={"http://www.opengis.net/gml":{exterior:L(lo.prototype.Ki),interior:L(lo.prototype.Ki)}},so={"http://www.opengis.net/gml":{Curve:L(lo.prototype.Ge),MultiCurve:L(lo.prototype.Hi),Point:L(lo.prototype.Ji),MultiPoint:L(lo.prototype.yp),
+LineString:L(lo.prototype.Ge),MultiLineString:L(lo.prototype.Hi),LinearRing:L(lo.prototype.Gi),Polygon:L(lo.prototype.Le),MultiPolygon:L(lo.prototype.Ii),Surface:L(lo.prototype.Le),MultiSurface:L(lo.prototype.Ii),Envelope:L(lo.prototype.xp)}},to={MultiLineString:"lineStringMember",MultiCurve:"curveMember",MultiPolygon:"polygonMember",MultiSurface:"surfaceMember"};lo.prototype.c=function(a,b){return Mk("http://www.opengis.net/gml",to[b[b.length-1].node.nodeName])};
+lo.prototype.g=function(a,b){var c=b[b.length-1],d=c.multiSurface,e=c.surface,f=c.curve,c=c.multiCurve,g;Array.isArray(a)?g="Envelope":(g=a.X(),"MultiPolygon"===g&&!0===d?g="MultiSurface":"Polygon"===g&&!0===e?g="Surface":"LineString"===g&&!0===f?g="Curve":"MultiLineString"===g&&!0===c&&(g="MultiCurve"));return Mk("http://www.opengis.net/gml",g)};
+lo.prototype.s=function(a,b){b=tn(this,b);var c=Mk("http://www.opengis.net/gml","geom"),d={node:c,srsName:this.srsName,curve:this.i,surface:this.j,multiSurface:this.o,multiCurve:this.l};b&&Ea(d,b);this.Je(c,a,[d]);return c};
+lo.prototype.a=function(a,b){b=tn(this,b);var c=Mk("http://www.opengis.net/gml","featureMembers");c.setAttributeNS("http://www.w3.org/2001/XMLSchema-instance","xsi:schemaLocation",this.schemaLocation);var d={srsName:this.srsName,curve:this.i,surface:this.j,multiSurface:this.o,multiCurve:this.l,featureNS:this.featureNS,featureType:this.featureType};b&&Ea(d,b);var d=[d],e=d[d.length-1],f=e.featureType,g=e.featureNS,h={};h[g]={};h[g][f]=L(this.Bi,this);e=Ea({},e);e.node=c;bl(e,h,Xk(f,g),a,d);return c};function uo(a){a=a?a:{};Un.call(this);this.defaultDataProjection=yc("EPSG:4326");this.b=a.readExtensions}y(uo,Un);var vo=[null,"http://www.topografix.com/GPX/1/0","http://www.topografix.com/GPX/1/1"];function wo(a,b,c){a.push(parseFloat(b.getAttribute("lon")),parseFloat(b.getAttribute("lat")));"ele"in c?(a.push(c.ele),delete c.ele):a.push(0);"time"in c?(a.push(c.time),delete c.time):a.push(0);return a}function xo(a,b){var c=b[b.length-1],d=a.getAttribute("href");null!==d&&(c.link=d);al(yo,a,b)}
+function zo(a,b){b[b.length-1].extensionsNode_=a}function Ao(a,b){var c=b[0],d=O({flatCoordinates:[]},Bo,a,b);if(d){var e=d.flatCoordinates;delete d.flatCoordinates;var f=new R(null);f.ba("XYZM",e);un(f,!1,c);c=new Ik(f);c.G(d);return c}}function Co(a,b){var c=b[0],d=O({flatCoordinates:[],ends:[]},Do,a,b);if(d){var e=d.flatCoordinates;delete d.flatCoordinates;var f=d.ends;delete d.ends;var g=new S(null);g.ba("XYZM",e,f);un(g,!1,c);c=new Ik(g);c.G(d);return c}}
+function Eo(a,b){var c=b[0],d=O({},Fo,a,b);if(d){var e=wo([],a,d),e=new C(e,"XYZM");un(e,!1,c);c=new Ik(e);c.G(d);return c}}
+var Go={rte:Ao,trk:Co,wpt:Eo},Ho=M(vo,{rte:Tk(Ao),trk:Tk(Co),wpt:Tk(Eo)}),yo=M(vo,{text:J(U,"linkText"),type:J(U,"linkType")}),Bo=M(vo,{name:J(U),cmt:J(U),desc:J(U),src:J(U),link:xo,number:J(eo),extensions:zo,type:J(U),rtept:function(a,b){var c=O({},Io,a,b);c&&wo(b[b.length-1].flatCoordinates,a,c)}}),Io=M(vo,{ele:J(bo),time:J(ao)}),Do=M(vo,{name:J(U),cmt:J(U),desc:J(U),src:J(U),link:xo,number:J(eo),type:J(U),extensions:zo,trkseg:function(a,b){var c=b[b.length-1];al(Jo,a,b);c.ends.push(c.flatCoordinates.length)}}),
+Jo=M(vo,{trkpt:function(a,b){var c=O({},Ko,a,b);c&&wo(b[b.length-1].flatCoordinates,a,c)}}),Ko=M(vo,{ele:J(bo),time:J(ao)}),Fo=M(vo,{ele:J(bo),time:J(ao),magvar:J(bo),geoidheight:J(bo),name:J(U),cmt:J(U),desc:J(U),src:J(U),link:xo,sym:J(U),type:J(U),fix:J(U),sat:J(eo),hdop:J(bo),vdop:J(bo),pdop:J(bo),ageofdgpsdata:J(bo),dgpsid:J(eo),extensions:zo});
+function Lo(a,b){b||(b=[]);for(var c=0,d=b.length;c<d;++c){var e=b[c];if(a.b){var f=e.get("extensionsNode_")||null;a.b(e,f)}e.set("extensionsNode_",void 0)}}uo.prototype.Ph=function(a,b){if(!jb(vo,a.namespaceURI))return null;var c=Go[a.localName];if(!c)return null;c=c(a,[sn(this,a,b)]);if(!c)return null;Lo(this,[c]);return c};uo.prototype.lc=function(a,b){if(!jb(vo,a.namespaceURI))return[];if("gpx"==a.localName){var c=O([],Ho,a,[sn(this,a,b)]);if(c)return Lo(this,c),c}return[]};
+function Mo(a,b,c){a.setAttribute("href",b);b=c[c.length-1].properties;bl({node:a},No,Zk,[b.linkText,b.linkType],c,Oo)}function Po(a,b,c){var d=c[c.length-1],e=d.node.namespaceURI,f=d.properties;a.setAttributeNS(null,"lat",b[1]);a.setAttributeNS(null,"lon",b[0]);switch(d.geometryLayout){case "XYZM":0!==b[3]&&(f.time=b[3]);case "XYZ":0!==b[2]&&(f.ele=b[2]);break;case "XYM":0!==b[2]&&(f.time=b[2])}b="rtept"==a.nodeName?Qo[e]:Ro[e];d=$k(f,b);bl({node:a,properties:f},So,Zk,d,c,b)}
+var Oo=["text","type"],No=M(vo,{text:L(ho),type:L(ho)}),To=M(vo,"name cmt desc src link number type rtept".split(" ")),Uo=M(vo,{name:L(ho),cmt:L(ho),desc:L(ho),src:L(ho),link:L(Mo),number:L(jo),type:L(ho),rtept:Wk(L(Po))}),Qo=M(vo,["ele","time"]),Vo=M(vo,"name cmt desc src link number type trkseg".split(" ")),Yo=M(vo,{name:L(ho),cmt:L(ho),desc:L(ho),src:L(ho),link:L(Mo),number:L(jo),type:L(ho),trkseg:Wk(L(function(a,b,c){bl({node:a,geometryLayout:b.f,properties:{}},Wo,Xo,b.Z(),c)}))}),Xo=Xk("trkpt"),
+Wo=M(vo,{trkpt:L(Po)}),Ro=M(vo,"ele time magvar geoidheight name cmt desc src link sym type fix sat hdop vdop pdop ageofdgpsdata dgpsid".split(" ")),So=M(vo,{ele:L(io),time:L(function(a,b){var c=new Date(1E3*b);a.appendChild(Lk.createTextNode(c.getUTCFullYear()+"-"+zb(c.getUTCMonth()+1)+"-"+zb(c.getUTCDate())+"T"+zb(c.getUTCHours())+":"+zb(c.getUTCMinutes())+":"+zb(c.getUTCSeconds())+"Z"))}),magvar:L(io),geoidheight:L(io),name:L(ho),cmt:L(ho),desc:L(ho),src:L(ho),link:L(Mo),sym:L(ho),type:L(ho),fix:L(ho),
+sat:L(jo),hdop:L(io),vdop:L(io),pdop:L(io),ageofdgpsdata:L(io),dgpsid:L(jo)}),Zo={Point:"wpt",LineString:"rte",MultiLineString:"trk"};function $o(a,b){var c=a.W();if(c&&(c=Zo[c.X()]))return Mk(b[b.length-1].node.namespaceURI,c)}
+var ap=M(vo,{rte:L(function(a,b,c){var d=c[0],e=b.O();a={node:a,properties:e};if(b=b.W())b=un(b,!0,d),a.geometryLayout=b.f,e.rtept=b.Z();d=To[c[c.length-1].node.namespaceURI];e=$k(e,d);bl(a,Uo,Zk,e,c,d)}),trk:L(function(a,b,c){var d=c[0],e=b.O();a={node:a,properties:e};if(b=b.W())b=un(b,!0,d),e.trkseg=b.md();d=Vo[c[c.length-1].node.namespaceURI];e=$k(e,d);bl(a,Yo,Zk,e,c,d)}),wpt:L(function(a,b,c){var d=c[0],e=c[c.length-1];e.properties=b.O();if(b=b.W())b=un(b,!0,d),e.geometryLayout=b.f,Po(a,b.Z(),
+c)})});uo.prototype.a=function(a,b){b=tn(this,b);var c=Mk("http://www.topografix.com/GPX/1/1","gpx");c.setAttributeNS("http://www.w3.org/2000/xmlns/","xmlns:xsi","http://www.w3.org/2001/XMLSchema-instance");c.setAttributeNS("http://www.w3.org/2001/XMLSchema-instance","xsi:schemaLocation","http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd");c.setAttribute("version","1.1");c.setAttribute("creator","OpenLayers 3");bl({node:c},ap,$o,a,[b]);return c};function bp(){this.defaultDataProjection=null}y(bp,rn);function cp(a){return"string"===typeof a?a:""}k=bp.prototype;k.X=function(){return"text"};k.Rb=function(a,b){return this.ud(cp(a),tn(this,b))};k.Fa=function(a,b){return this.Kf(cp(a),tn(this,b))};k.Vc=function(a,b){return this.wd(cp(a),tn(this,b))};k.Oa=function(a){cp(a);return this.defaultDataProjection};k.Dd=function(a,b){return this.He(a,tn(this,b))};k.Xb=function(a,b){return this.Ci(a,tn(this,b))};
+k.Zc=function(a,b){return this.Ed(a,tn(this,b))};function dp(a){a=a?a:{};this.defaultDataProjection=null;this.defaultDataProjection=yc("EPSG:4326");this.b=a.altitudeMode?a.altitudeMode:"none"}y(dp,bp);var ep=/^B(\d{2})(\d{2})(\d{2})(\d{2})(\d{5})([NS])(\d{3})(\d{5})([EW])([AV])(\d{5})(\d{5})/,fp=/^H.([A-Z]{3}).*?:(.*)/,gp=/^HFDTE(\d{2})(\d{2})(\d{2})/,hp=/\r\n|\r|\n/;
+dp.prototype.ud=function(a,b){var c=this.b,d=a.split(hp),e={},f=[],g=2E3,h=0,l=1,m=-1,n,p;n=0;for(p=d.length;n<p;++n){var q=d[n],r;if("B"==q.charAt(0)){if(r=ep.exec(q)){var q=parseInt(r[1],10),u=parseInt(r[2],10),x=parseInt(r[3],10),v=parseInt(r[4],10)+parseInt(r[5],10)/6E4;"S"==r[6]&&(v=-v);var D=parseInt(r[7],10)+parseInt(r[8],10)/6E4;"W"==r[9]&&(D=-D);f.push(D,v);"none"!=c&&f.push("gps"==c?parseInt(r[11],10):"barometric"==c?parseInt(r[12],10):0);r=Date.UTC(g,h,l,q,u,x);r<m&&(r=Date.UTC(g,h,l+1,
+q,u,x));f.push(r/1E3);m=r}}else"H"==q.charAt(0)&&((r=gp.exec(q))?(l=parseInt(r[1],10),h=parseInt(r[2],10)-1,g=2E3+parseInt(r[3],10)):(r=fp.exec(q))&&(e[r[1]]=r[2].trim()))}if(0===f.length)return null;d=new R(null);d.ba("none"==c?"XYM":"XYZM",f);c=new Ik(un(d,!1,b));c.G(e);return c};dp.prototype.Kf=function(a,b){var c=this.ud(a,b);return c?[c]:[]};function ip(a,b){this.a={};this.b=[];this.g=0;var c=arguments.length;if(1<c){if(c%2)throw Error("Uneven number of arguments");for(var d=0;d<c;d+=2)this.set(arguments[d],arguments[d+1])}else if(a){var e;if(a instanceof ip)e=a.N(),d=a.zc();else{var c=[],f=0;for(e in a)c[f++]=e;e=c;c=[];f=0;for(d in a)c[f++]=a[d];d=c}for(c=0;c<e.length;c++)this.set(e[c],d[c])}}k=ip.prototype;k.wc=function(){return this.g};k.zc=function(){jp(this);for(var a=[],b=0;b<this.b.length;b++)a.push(this.a[this.b[b]]);return a};
+k.N=function(){jp(this);return this.b.concat()};k.Ya=function(){return 0==this.g};k.clear=function(){this.a={};this.g=this.b.length=0};k.remove=function(a){return kp(this.a,a)?(delete this.a[a],this.g--,this.b.length>2*this.g&&jp(this),!0):!1};function jp(a){if(a.g!=a.b.length){for(var b=0,c=0;b<a.b.length;){var d=a.b[b];kp(a.a,d)&&(a.b[c++]=d);b++}a.b.length=c}if(a.g!=a.b.length){for(var e={},c=b=0;b<a.b.length;)d=a.b[b],kp(e,d)||(a.b[c++]=d,e[d]=1),b++;a.b.length=c}}
+k.get=function(a,b){return kp(this.a,a)?this.a[a]:b};k.set=function(a,b){kp(this.a,a)||(this.g++,this.b.push(a));this.a[a]=b};k.forEach=function(a,b){for(var c=this.N(),d=0;d<c.length;d++){var e=c[d],f=this.get(e);a.call(b,f,e,this)}};k.clone=function(){return new ip(this)};function kp(a,b){return Object.prototype.hasOwnProperty.call(a,b)};var lp=/^(?:([^:/?#.]+):)?(?:\/\/(?:([^/?#]*)@)?([^/#?]*?)(?::([0-9]+))?(?=[/#?]|$))?([^?#]+)?(?:\?([^#]*))?(?:#(.*))?$/;function mp(a,b){if(a)for(var c=a.split("&"),d=0;d<c.length;d++){var e=c[d].indexOf("="),f,g=null;0<=e?(f=c[d].substring(0,e),g=c[d].substring(e+1)):f=c[d];b(f,g?decodeURIComponent(g.replace(/\+/g," ")):"")}};function np(a,b){this.a=this.l=this.g="";this.o=null;this.f=this.b="";this.c=!1;var c;a instanceof np?(this.c=void 0!==b?b:a.c,op(this,a.g),this.l=a.l,this.a=a.a,pp(this,a.o),this.b=a.b,qp(this,a.i.clone()),this.f=a.f):a&&(c=String(a).match(lp))?(this.c=!!b,op(this,c[1]||"",!0),this.l=rp(c[2]||""),this.a=rp(c[3]||"",!0),pp(this,c[4]),this.b=rp(c[5]||"",!0),qp(this,c[6]||"",!0),this.f=rp(c[7]||"")):(this.c=!!b,this.i=new sp(null,0,this.c))}
+np.prototype.toString=function(){var a=[],b=this.g;b&&a.push(tp(b,up,!0),":");var c=this.a;if(c||"file"==b)a.push("//"),(b=this.l)&&a.push(tp(b,up,!0),"@"),a.push(encodeURIComponent(String(c)).replace(/%25([0-9a-fA-F]{2})/g,"%$1")),c=this.o,null!=c&&a.push(":",String(c));if(c=this.b)this.a&&"/"!=c.charAt(0)&&a.push("/"),a.push(tp(c,"/"==c.charAt(0)?vp:wp,!0));(c=this.i.toString())&&a.push("?",c);(c=this.f)&&a.push("#",tp(c,xp));return a.join("")};np.prototype.clone=function(){return new np(this)};
+function op(a,b,c){a.g=c?rp(b,!0):b;a.g&&(a.g=a.g.replace(/:$/,""))}function pp(a,b){if(b){b=Number(b);if(isNaN(b)||0>b)throw Error("Bad port number "+b);a.o=b}else a.o=null}function qp(a,b,c){b instanceof sp?(a.i=b,yp(a.i,a.c)):(c||(b=tp(b,zp)),a.i=new sp(b,0,a.c))}function Ap(a){return a instanceof np?a.clone():new np(a,void 0)}
+function Bp(a,b){a instanceof np||(a=Ap(a));b instanceof np||(b=Ap(b));var c=a,d=b,e=c.clone(),f=!!d.g;f?op(e,d.g):f=!!d.l;f?e.l=d.l:f=!!d.a;f?e.a=d.a:f=null!=d.o;var g=d.b;if(f)pp(e,d.o);else if(f=!!d.b)if("/"!=g.charAt(0)&&(c.a&&!c.b?g="/"+g:(c=e.b.lastIndexOf("/"),-1!=c&&(g=e.b.substr(0,c+1)+g))),c=g,".."==c||"."==c)g="";else if(-1!=c.indexOf("./")||-1!=c.indexOf("/.")){for(var g=0==c.lastIndexOf("/",0),c=c.split("/"),h=[],l=0;l<c.length;){var m=c[l++];"."==m?g&&l==c.length&&h.push(""):".."==m?
+((1<h.length||1==h.length&&""!=h[0])&&h.pop(),g&&l==c.length&&h.push("")):(h.push(m),g=!0)}g=h.join("/")}else g=c;f?e.b=g:f=""!==d.i.toString();f?qp(e,rp(d.i.toString())):f=!!d.f;f&&(e.f=d.f);return e}function rp(a,b){return a?b?decodeURI(a.replace(/%25/g,"%2525")):decodeURIComponent(a):""}function tp(a,b,c){return da(a)?(a=encodeURI(a).replace(b,Cp),c&&(a=a.replace(/%25([0-9a-fA-F]{2})/g,"%$1")),a):null}function Cp(a){a=a.charCodeAt(0);return"%"+(a>>4&15).toString(16)+(a&15).toString(16)}
+var up=/[#\/\?@]/g,wp=/[\#\?:]/g,vp=/[\#\?]/g,zp=/[\#\?@]/g,xp=/#/g;function sp(a,b,c){this.a=this.b=null;this.g=a||null;this.f=!!c}function Dp(a){a.b||(a.b=new ip,a.a=0,a.g&&mp(a.g,function(b,c){a.add(decodeURIComponent(b.replace(/\+/g," ")),c)}))}k=sp.prototype;k.wc=function(){Dp(this);return this.a};k.add=function(a,b){Dp(this);this.g=null;a=Ep(this,a);var c=this.b.get(a);c||this.b.set(a,c=[]);c.push(b);this.a=this.a+1;return this};
+k.remove=function(a){Dp(this);a=Ep(this,a);return kp(this.b.a,a)?(this.g=null,this.a=this.a-this.b.get(a).length,this.b.remove(a)):!1};k.clear=function(){this.b=this.g=null;this.a=0};k.Ya=function(){Dp(this);return 0==this.a};function Fp(a,b){Dp(a);b=Ep(a,b);return kp(a.b.a,b)}k.N=function(){Dp(this);for(var a=this.b.zc(),b=this.b.N(),c=[],d=0;d<b.length;d++)for(var e=a[d],f=0;f<e.length;f++)c.push(b[d]);return c};
+k.zc=function(a){Dp(this);var b=[];if(da(a))Fp(this,a)&&(b=ne(b,this.b.get(Ep(this,a))));else{a=this.b.zc();for(var c=0;c<a.length;c++)b=ne(b,a[c])}return b};k.set=function(a,b){Dp(this);this.g=null;a=Ep(this,a);Fp(this,a)&&(this.a=this.a-this.b.get(a).length);this.b.set(a,[b]);this.a=this.a+1;return this};k.get=function(a,b){var c=a?this.zc(a):[];return 0<c.length?String(c[0]):b};
+k.toString=function(){if(this.g)return this.g;if(!this.b)return"";for(var a=[],b=this.b.N(),c=0;c<b.length;c++)for(var d=b[c],e=encodeURIComponent(String(d)),d=this.zc(d),f=0;f<d.length;f++){var g=e;""!==d[f]&&(g+="="+encodeURIComponent(String(d[f])));a.push(g)}return this.g=a.join("&")};k.clone=function(){var a=new sp;a.g=this.g;this.b&&(a.b=this.b.clone(),a.a=this.a);return a};function Ep(a,b){var c=String(b);a.f&&(c=c.toLowerCase());return c}
+function yp(a,b){b&&!a.f&&(Dp(a),a.g=null,a.b.forEach(function(a,b){var e=b.toLowerCase();b!=e&&(this.remove(b),this.remove(e),0<a.length&&(this.g=null,this.b.set(Ep(this,e),oe(a)),this.a=this.a+a.length))},a));a.f=b};function Gp(a){a=a||{};this.g=a.font;this.i=a.rotation;this.a=a.scale;this.s=a.text;this.o=a.textAlign;this.j=a.textBaseline;this.b=void 0!==a.fill?a.fill:new ij({color:"#333"});this.l=void 0!==a.stroke?a.stroke:null;this.f=void 0!==a.offsetX?a.offsetX:0;this.c=void 0!==a.offsetY?a.offsetY:0}k=Gp.prototype;k.Xj=function(){return this.g};k.kk=function(){return this.f};k.lk=function(){return this.c};k.Un=function(){return this.b};k.Vn=function(){return this.i};k.Wn=function(){return this.a};k.Xn=function(){return this.l};
+k.Ha=function(){return this.s};k.yk=function(){return this.o};k.zk=function(){return this.j};k.bp=function(a){this.g=a};k.mi=function(a){this.f=a};k.ni=function(a){this.c=a};k.ap=function(a){this.b=a};k.Yn=function(a){this.i=a};k.Zn=function(a){this.a=a};k.ip=function(a){this.l=a};k.pi=function(a){this.s=a};k.ri=function(a){this.o=a};k.jp=function(a){this.j=a};function Hp(a){a=a?a:{};Un.call(this);this.defaultDataProjection=yc("EPSG:4326");this.g=a.defaultStyle?a.defaultStyle:Ip;this.c=void 0!==a.extractStyles?a.extractStyles:!0;this.l=void 0!==a.writeStyles?a.writeStyles:!0;this.b={};this.i=void 0!==a.showPointNames?a.showPointNames:!0}y(Hp,Un);
+var Jp=["http://www.google.com/kml/ext/2.2"],Kp=[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"],Lp=[255,255,255,1],Mp=new ij({color:Lp}),Np=[20,2],Op=[64,64],Pp=new Dh({anchor:Np,anchorOrigin:"bottom-left",anchorXUnits:"pixels",anchorYUnits:"pixels",crossOrigin:"anonymous",rotation:0,scale:.5,size:Op,src:"https://maps.google.com/mapfiles/kml/pushpin/ylw-pushpin.png"}),Qp=new oj({color:Lp,width:1}),Rp=new Gp({font:"bold 16px Helvetica",
+fill:Mp,stroke:new oj({color:[51,51,51,1],width:2}),scale:.8}),Ip=[new rj({fill:Mp,image:Pp,text:Rp,stroke:Qp,zIndex:0})],Sp={fraction:"fraction",pixels:"pixels"};function Tp(a,b){var c,d=[0,0],e="start";if(a.a){var f=a.a.ld();f&&2==f.length&&(d[0]=a.a.i*f[0]/2,d[1]=-a.a.i*f[1]/2,e="left")}if(Ha(a.Ha()))c=new Gp({text:b,offsetX:d[0],offsetY:d[1],textAlign:e});else{var f=a.Ha(),g={};for(c in f)g[c]=f[c];c=g;c.pi(b);c.ri(e);c.mi(d[0]);c.ni(d[1])}return new rj({text:c})}
+function Up(a,b,c,d,e){return function(){var f=e,g="";f&&this.W()&&(f="Point"===this.W().X());f&&(g=this.get("name"),f=f&&g);if(a)return f?(f=Tp(a[0],g),a.concat(f)):a;if(b){var h=Vp(b,c,d);return f?(f=Tp(h[0],g),h.concat(f)):h}return f?(f=Tp(c[0],g),c.concat(f)):c}}function Vp(a,b,c){return Array.isArray(a)?a:"string"===typeof a?(!(a in c)&&"#"+a in c&&(a="#"+a),Vp(c[a],b,c)):b}
+function Wp(a){a=Nk(a,!1);if(a=/^\s*#?\s*([0-9A-Fa-f]{8})\s*$/.exec(a))return a=a[1],[parseInt(a.substr(6,2),16),parseInt(a.substr(4,2),16),parseInt(a.substr(2,2),16),parseInt(a.substr(0,2),16)/255]}function Xp(a){a=Nk(a,!1);for(var b=[],c=/^\s*([+\-]?\d*\.?\d+(?:e[+\-]?\d+)?)\s*,\s*([+\-]?\d*\.?\d+(?:e[+\-]?\d+)?)(?:\s*,\s*([+\-]?\d*\.?\d+(?:e[+\-]?\d+)?))?\s*/i,d;d=c.exec(a);)b.push(parseFloat(d[1]),parseFloat(d[2]),d[3]?parseFloat(d[3]):0),a=a.substr(d[0].length);return""!==a?void 0:b}
+function Yp(a){var b=Nk(a,!1);return a.baseURI?Bp(a.baseURI,b.trim()).toString():b.trim()}function Zp(a){a=bo(a);if(void 0!==a)return Math.sqrt(a)}function $p(a,b){return O(null,aq,a,b)}function bq(a,b){var c=O({B:[],zi:[]},cq,a,b);if(c){var d=c.B,c=c.zi,e,f;e=0;for(f=Math.min(d.length,c.length);e<f;++e)d[4*e+3]=c[e];c=new R(null);c.ba("XYZM",d);return c}}function dq(a,b){var c=O({},eq,a,b),d=O(null,fq,a,b);if(d){var e=new R(null);e.ba("XYZ",d);e.G(c);return e}}
+function gq(a,b){var c=O({},eq,a,b),d=O(null,fq,a,b);if(d){var e=new E(null);e.ba("XYZ",d,[d.length]);e.G(c);return e}}
+function hq(a,b){var c=O([],iq,a,b);if(!c)return null;if(0===c.length)return new Ln(c);var d=!0,e=c[0].X(),f,g,h;g=1;for(h=c.length;g<h;++g)if(f=c[g],f.X()!=e){d=!1;break}if(d){if("Point"==e){f=c[0];d=f.f;e=f.la();g=1;for(h=c.length;g<h;++g)f=c[g],mb(e,f.la());f=new Bn(null);f.ba(d,e);jq(f,c);return f}return"LineString"==e?(f=new S(null),An(f,c),jq(f,c),f):"Polygon"==e?(f=new T(null),Dn(f,c),jq(f,c),f):"GeometryCollection"==e?new Ln(c):null}return new Ln(c)}
+function kq(a,b){var c=O({},eq,a,b),d=O(null,fq,a,b);if(d){var e=new C(null);e.ba("XYZ",d);e.G(c);return e}}function lq(a,b){var c=O({},eq,a,b),d=O([null],mq,a,b);if(d&&d[0]){var e=new E(null),f=d[0],g=[f.length],h,l;h=1;for(l=d.length;h<l;++h)mb(f,d[h]),g.push(f.length);e.ba("XYZ",f,g);e.G(c);return e}}
+function nq(a,b){var c=O({},oq,a,b);if(!c)return null;var d="fillStyle"in c?c.fillStyle:Mp,e=c.fill;void 0===e||e||(d=null);var e="imageStyle"in c?c.imageStyle:Pp,f="textStyle"in c?c.textStyle:Rp,g="strokeStyle"in c?c.strokeStyle:Qp,c=c.outline;void 0===c||c||(g=null);return[new rj({fill:d,image:e,stroke:g,text:f,zIndex:void 0})]}
+function jq(a,b){var c=b.length,d=Array(b.length),e=Array(b.length),f,g,h,l;h=l=!1;for(g=0;g<c;++g)f=b[g],d[g]=f.get("extrude"),e[g]=f.get("altitudeMode"),h=h||void 0!==d[g],l=l||e[g];h&&a.set("extrude",d);l&&a.set("altitudeMode",e)}function pq(a,b){al(qq,a,b)}
+var rq=M(Kp,{value:Uk(U)}),qq=M(Kp,{Data:function(a,b){var c=a.getAttribute("name");if(null!==c){var d=O(void 0,rq,a,b);d&&(b[b.length-1][c]=d)}},SchemaData:function(a,b){al(sq,a,b)}}),eq=M(Kp,{extrude:J(Zn),altitudeMode:J(U)}),aq=M(Kp,{coordinates:Uk(Xp)}),mq=M(Kp,{innerBoundaryIs:function(a,b){var c=O(void 0,tq,a,b);c&&b[b.length-1].push(c)},outerBoundaryIs:function(a,b){var c=O(void 0,uq,a,b);c&&(b[b.length-1][0]=c)}}),cq=M(Kp,{when:function(a,b){var c=b[b.length-1].zi,d=Nk(a,!1);if(d=/^\s*(\d{4})($|-(\d{2})($|-(\d{2})($|T(\d{2}):(\d{2}):(\d{2})(Z|(?:([+\-])(\d{2})(?::(\d{2}))?)))))\s*$/.exec(d)){var e=
+Date.UTC(parseInt(d[1],10),d[3]?parseInt(d[3],10)-1:0,d[5]?parseInt(d[5],10):1,d[7]?parseInt(d[7],10):0,d[8]?parseInt(d[8],10):0,d[9]?parseInt(d[9],10):0);if(d[10]&&"Z"!=d[10]){var f="-"==d[11]?-1:1,e=e+60*f*parseInt(d[12],10);d[13]&&(e+=3600*f*parseInt(d[13],10))}c.push(e)}else c.push(0)}},M(Jp,{coord:function(a,b){var c=b[b.length-1].B,d=Nk(a,!1);(d=/^\s*([+\-]?\d+(?:\.\d*)?(?:e[+\-]?\d*)?)\s+([+\-]?\d+(?:\.\d*)?(?:e[+\-]?\d*)?)\s+([+\-]?\d+(?:\.\d*)?(?:e[+\-]?\d*)?)\s*$/i.exec(d))?c.push(parseFloat(d[1]),
+parseFloat(d[2]),parseFloat(d[3]),0):c.push(0,0,0,0)}})),fq=M(Kp,{coordinates:Uk(Xp)}),vq=M(Kp,{href:J(Yp)},M(Jp,{x:J(bo),y:J(bo),w:J(bo),h:J(bo)})),wq=M(Kp,{Icon:J(function(a,b){var c=O({},vq,a,b);return c?c:null}),heading:J(bo),hotSpot:J(function(a){var b=a.getAttribute("xunits"),c=a.getAttribute("yunits");return{x:parseFloat(a.getAttribute("x")),$f:Sp[b],y:parseFloat(a.getAttribute("y")),ag:Sp[c]}}),scale:J(Zp)}),tq=M(Kp,{LinearRing:Uk($p)}),xq=M(Kp,{color:J(Wp),scale:J(Zp)}),yq=M(Kp,{color:J(Wp),
+width:J(bo)}),iq=M(Kp,{LineString:Tk(dq),LinearRing:Tk(gq),MultiGeometry:Tk(hq),Point:Tk(kq),Polygon:Tk(lq)}),zq=M(Jp,{Track:Tk(bq)}),Bq=M(Kp,{ExtendedData:pq,Link:function(a,b){al(Aq,a,b)},address:J(U),description:J(U),name:J(U),open:J(Zn),phoneNumber:J(U),visibility:J(Zn)}),Aq=M(Kp,{href:J(Yp)}),uq=M(Kp,{LinearRing:Uk($p)}),Cq=M(Kp,{Style:J(nq),key:J(U),styleUrl:J(function(a){var b=Nk(a,!1).trim();return a.baseURI?Bp(a.baseURI,b).toString():b})}),Eq=M(Kp,{ExtendedData:pq,MultiGeometry:J(hq,"geometry"),
+LineString:J(dq,"geometry"),LinearRing:J(gq,"geometry"),Point:J(kq,"geometry"),Polygon:J(lq,"geometry"),Style:J(nq),StyleMap:function(a,b){var c=O(void 0,Dq,a,b);if(c){var d=b[b.length-1];Array.isArray(c)?d.Style=c:"string"===typeof c&&(d.styleUrl=c)}},address:J(U),description:J(U),name:J(U),open:J(Zn),phoneNumber:J(U),styleUrl:J(Yp),visibility:J(Zn)},M(Jp,{MultiTrack:J(function(a,b){var c=O([],zq,a,b);if(c){var d=new S(null);An(d,c);return d}},"geometry"),Track:J(bq,"geometry")})),Fq=M(Kp,{color:J(Wp),
+fill:J(Zn),outline:J(Zn)}),sq=M(Kp,{SimpleData:function(a,b){var c=a.getAttribute("name");if(null!==c){var d=U(a);b[b.length-1][c]=d}}}),oq=M(Kp,{IconStyle:function(a,b){var c=O({},wq,a,b);if(c){var d=b[b.length-1],e="Icon"in c?c.Icon:{},f;f=(f=e.href)?f:"https://maps.google.com/mapfiles/kml/pushpin/ylw-pushpin.png";var g,h,l,m=c.hotSpot;m?(g=[m.x,m.y],h=m.$f,l=m.ag):"https://maps.google.com/mapfiles/kml/pushpin/ylw-pushpin.png"===f?(g=Np,l=h="pixels"):/^http:\/\/maps\.(?:google|gstatic)\.com\//.test(f)&&
+(g=[.5,0],l=h="fraction");var n,m=e.x,p=e.y;void 0!==m&&void 0!==p&&(n=[m,p]);var q,m=e.w,e=e.h;void 0!==m&&void 0!==e&&(q=[m,e]);var r,e=c.heading;void 0!==e&&(r=wa(e));c=c.scale;"https://maps.google.com/mapfiles/kml/pushpin/ylw-pushpin.png"==f&&(q=Op,void 0===c&&(c=.5));g=new Dh({anchor:g,anchorOrigin:"bottom-left",anchorXUnits:h,anchorYUnits:l,crossOrigin:"anonymous",offset:n,offsetOrigin:"bottom-left",rotation:r,scale:c,size:q,src:f});d.imageStyle=g}},LabelStyle:function(a,b){var c=O({},xq,a,
+b);c&&(b[b.length-1].textStyle=new Gp({fill:new ij({color:"color"in c?c.color:Lp}),scale:c.scale}))},LineStyle:function(a,b){var c=O({},yq,a,b);c&&(b[b.length-1].strokeStyle=new oj({color:"color"in c?c.color:Lp,width:"width"in c?c.width:1}))},PolyStyle:function(a,b){var c=O({},Fq,a,b);if(c){var d=b[b.length-1];d.fillStyle=new ij({color:"color"in c?c.color:Lp});var e=c.fill;void 0!==e&&(d.fill=e);c=c.outline;void 0!==c&&(d.outline=c)}}}),Dq=M(Kp,{Pair:function(a,b){var c=O({},Cq,a,b);if(c){var d=c.key;
+d&&"normal"==d&&((d=c.styleUrl)&&(b[b.length-1]=d),(c=c.Style)&&(b[b.length-1]=c))}}});k=Hp.prototype;k.Gf=function(a,b){var c=M(Kp,{Document:Sk(this.Gf,this),Folder:Sk(this.Gf,this),Placemark:Tk(this.Of,this),Style:this.Lo.bind(this),StyleMap:this.Ko.bind(this)});if(c=O([],c,a,b,this))return c};
+k.Of=function(a,b){var c=O({geometry:null},Eq,a,b);if(c){var d=new Ik,e=a.getAttribute("id");null!==e&&d.mc(e);var e=b[0],f=c.geometry;f&&un(f,!1,e);d.Ua(f);delete c.geometry;this.c&&d.sf(Up(c.Style,c.styleUrl,this.g,this.b,this.i));delete c.Style;d.G(c);return d}};k.Lo=function(a,b){var c=a.getAttribute("id");if(null!==c){var d=nq(a,b);d&&(c=a.baseURI?Bp(a.baseURI,"#"+c).toString():"#"+c,this.b[c]=d)}};
+k.Ko=function(a,b){var c=a.getAttribute("id");if(null!==c){var d=O(void 0,Dq,a,b);d&&(c=a.baseURI?Bp(a.baseURI,"#"+c).toString():"#"+c,this.b[c]=d)}};k.Ph=function(a,b){if(!jb(Kp,a.namespaceURI))return null;var c=this.Of(a,[sn(this,a,b)]);return c?c:null};
+k.lc=function(a,b){if(!jb(Kp,a.namespaceURI))return[];var c;c=a.localName;if("Document"==c||"Folder"==c)return(c=this.Gf(a,[sn(this,a,b)]))?c:[];if("Placemark"==c)return(c=this.Of(a,[sn(this,a,b)]))?[c]:[];if("kml"==c){c=[];var d;for(d=a.firstElementChild;d;d=d.nextElementSibling){var e=this.lc(d,b);e&&mb(c,e)}return c}return[]};k.Fo=function(a){if(Pk(a))return Gq(this,a);if(Qk(a))return Hq(this,a);if("string"===typeof a)return a=Rk(a),Gq(this,a)};
+function Gq(a,b){var c;for(c=b.firstChild;c;c=c.nextSibling)if(c.nodeType==Node.ELEMENT_NODE){var d=Hq(a,c);if(d)return d}}function Hq(a,b){var c;for(c=b.firstElementChild;c;c=c.nextElementSibling)if(jb(Kp,c.namespaceURI)&&"name"==c.localName)return U(c);for(c=b.firstElementChild;c;c=c.nextElementSibling){var d=c.localName;if(jb(Kp,c.namespaceURI)&&("Document"==d||"Folder"==d||"Placemark"==d||"kml"==d)&&(d=Hq(a,c)))return d}}
+k.Go=function(a){var b=[];Pk(a)?mb(b,Iq(this,a)):Qk(a)?mb(b,Jq(this,a)):"string"===typeof a&&(a=Rk(a),mb(b,Iq(this,a)));return b};function Iq(a,b){var c,d=[];for(c=b.firstChild;c;c=c.nextSibling)c.nodeType==Node.ELEMENT_NODE&&mb(d,Jq(a,c));return d}
+function Jq(a,b){var c,d=[];for(c=b.firstElementChild;c;c=c.nextElementSibling)if(jb(Kp,c.namespaceURI)&&"NetworkLink"==c.localName){var e=O({},Bq,c,[]);d.push(e)}for(c=b.firstElementChild;c;c=c.nextElementSibling)e=c.localName,!jb(Kp,c.namespaceURI)||"Document"!=e&&"Folder"!=e&&"kml"!=e||mb(d,Jq(a,c));return d}function Kq(a,b){var c=te(b),c=[255*(4==c.length?c[3]:1),c[2],c[1],c[0]],d;for(d=0;4>d;++d){var e=parseInt(c[d],10).toString(16);c[d]=1==e.length?"0"+e:e}ho(a,c.join(""))}
+function Lq(a,b,c){a={node:a};var d=b.X(),e,f;"GeometryCollection"==d?(e=b.ff(),f=Mq):"MultiPoint"==d?(e=b.je(),f=Nq):"MultiLineString"==d?(e=b.md(),f=Oq):"MultiPolygon"==d&&(e=b.Wd(),f=Pq);bl(a,Qq,f,e,c)}function Rq(a,b,c){bl({node:a},Sq,Tq,[b],c)}
+function Uq(a,b,c){var d={node:a};b.Xa()&&a.setAttribute("id",b.Xa());a=b.O();var e=b.ec();e&&(e=e.call(b,0))&&(e=Array.isArray(e)?e[0]:e,this.l&&(a.Style=e),(e=e.Ha())&&(a.name=e.Ha()));e=Vq[c[c.length-1].node.namespaceURI];a=$k(a,e);bl(d,Wq,Zk,a,c,e);a=c[0];(b=b.W())&&(b=un(b,!0,a));bl(d,Wq,Mq,[b],c)}function Xq(a,b,c){var d=b.la();a={node:a};a.layout=b.f;a.stride=b.va();bl(a,Yq,Zq,[d],c)}function $q(a,b,c){b=b.Vd();var d=b.shift();a={node:a};bl(a,ar,br,b,c);bl(a,ar,cr,[d],c)}
+function dr(a,b){io(a,Math.round(b*b*1E6)/1E6)}
+var er=M(Kp,["Document","Placemark"]),hr=M(Kp,{Document:L(function(a,b,c){bl({node:a},fr,gr,b,c,void 0,this)}),Placemark:L(Uq)}),fr=M(Kp,{Placemark:L(Uq)}),ir={Point:"Point",LineString:"LineString",LinearRing:"LinearRing",Polygon:"Polygon",MultiPoint:"MultiGeometry",MultiLineString:"MultiGeometry",MultiPolygon:"MultiGeometry",GeometryCollection:"MultiGeometry"},jr=M(Kp,["href"],M(Jp,["x","y","w","h"])),kr=M(Kp,{href:L(ho)},M(Jp,{x:L(io),y:L(io),w:L(io),h:L(io)})),lr=M(Kp,["scale","heading","Icon",
+"hotSpot"]),nr=M(Kp,{Icon:L(function(a,b,c){a={node:a};var d=jr[c[c.length-1].node.namespaceURI],e=$k(b,d);bl(a,kr,Zk,e,c,d);d=jr[Jp[0]];e=$k(b,d);bl(a,kr,mr,e,c,d)}),heading:L(io),hotSpot:L(function(a,b){a.setAttribute("x",b.x);a.setAttribute("y",b.y);a.setAttribute("xunits",b.$f);a.setAttribute("yunits",b.ag)}),scale:L(dr)}),or=M(Kp,["color","scale"]),pr=M(Kp,{color:L(Kq),scale:L(dr)}),qr=M(Kp,["color","width"]),rr=M(Kp,{color:L(Kq),width:L(io)}),Sq=M(Kp,{LinearRing:L(Xq)}),Qq=M(Kp,{LineString:L(Xq),
+Point:L(Xq),Polygon:L($q),GeometryCollection:L(Lq)}),Vq=M(Kp,"name open visibility address phoneNumber description styleUrl Style".split(" ")),Wq=M(Kp,{MultiGeometry:L(Lq),LineString:L(Xq),LinearRing:L(Xq),Point:L(Xq),Polygon:L($q),Style:L(function(a,b,c){a={node:a};var d={},e=b.c,f=b.f,g=b.a;b=b.Ha();g instanceof Dh&&(d.IconStyle=g);b&&(d.LabelStyle=b);f&&(d.LineStyle=f);e&&(d.PolyStyle=e);b=sr[c[c.length-1].node.namespaceURI];d=$k(d,b);bl(a,tr,Zk,d,c,b)}),address:L(ho),description:L(ho),name:L(ho),
+open:L(go),phoneNumber:L(ho),styleUrl:L(ho),visibility:L(go)}),Yq=M(Kp,{coordinates:L(function(a,b,c){c=c[c.length-1];var d=c.layout;c=c.stride;var e;"XY"==d||"XYM"==d?e=2:("XYZ"==d||"XYZM"==d)&&(e=3);var f,g=b.length,h="";if(0<g){h+=b[0];for(d=1;d<e;++d)h+=","+b[d];for(f=c;f<g;f+=c)for(h+=" "+b[f],d=1;d<e;++d)h+=","+b[f+d]}ho(a,h)})}),ar=M(Kp,{outerBoundaryIs:L(Rq),innerBoundaryIs:L(Rq)}),ur=M(Kp,{color:L(Kq)}),sr=M(Kp,["IconStyle","LabelStyle","LineStyle","PolyStyle"]),tr=M(Kp,{IconStyle:L(function(a,
+b,c){a={node:a};var d={},e=b.Fb(),f=b.ld(),g={href:b.b.j};if(e){g.w=e[0];g.h=e[1];var h=b.Yb(),l=b.Ia();l&&f&&0!==l[0]&&l[1]!==e[1]&&(g.x=l[0],g.y=f[1]-(l[1]+e[1]));h&&0!==h[0]&&h[1]!==e[1]&&(d.hotSpot={x:h[0],$f:"pixels",y:e[1]-h[1],ag:"pixels"})}d.Icon=g;e=b.i;1!==e&&(d.scale=e);b=b.j;0!==b&&(d.heading=b);b=lr[c[c.length-1].node.namespaceURI];d=$k(d,b);bl(a,nr,Zk,d,c,b)}),LabelStyle:L(function(a,b,c){a={node:a};var d={},e=b.b;e&&(d.color=e.b);(b=b.a)&&1!==b&&(d.scale=b);b=or[c[c.length-1].node.namespaceURI];
+d=$k(d,b);bl(a,pr,Zk,d,c,b)}),LineStyle:L(function(a,b,c){a={node:a};var d=qr[c[c.length-1].node.namespaceURI];b=$k({color:b.b,width:b.a},d);bl(a,rr,Zk,b,c,d)}),PolyStyle:L(function(a,b,c){bl({node:a},ur,vr,[b.b],c)})});function mr(a,b,c){return Mk(Jp[0],"gx:"+c)}function gr(a,b){return Mk(b[b.length-1].node.namespaceURI,"Placemark")}function Mq(a,b){if(a)return Mk(b[b.length-1].node.namespaceURI,ir[a.X()])}
+var vr=Xk("color"),Zq=Xk("coordinates"),br=Xk("innerBoundaryIs"),Nq=Xk("Point"),Oq=Xk("LineString"),Tq=Xk("LinearRing"),Pq=Xk("Polygon"),cr=Xk("outerBoundaryIs");
+Hp.prototype.a=function(a,b){b=tn(this,b);var c=Mk(Kp[4],"kml");c.setAttributeNS("http://www.w3.org/2000/xmlns/","xmlns:gx",Jp[0]);c.setAttributeNS("http://www.w3.org/2000/xmlns/","xmlns:xsi","http://www.w3.org/2001/XMLSchema-instance");c.setAttributeNS("http://www.w3.org/2001/XMLSchema-instance","xsi:schemaLocation","http://www.opengis.net/kml/2.2 https://developers.google.com/kml/schema/kml22gx.xsd");var d={node:c},e={};1<a.length?e.Document=a:1==a.length&&(e.Placemark=a[0]);var f=er[c.namespaceURI],
+e=$k(e,f);bl(d,hr,Zk,e,[b],f,this);return c};(function(){var a={},b={ja:a};(function(c){if("object"===typeof a&&"undefined"!==typeof b)b.ja=c();else{var d;"undefined"!==typeof window?d=window:"undefined"!==typeof global?d=global:"undefined"!==typeof self?d=self:d=this;d.Rp=c()}})(function(){return function d(a,b,g){function h(m,p){if(!b[m]){if(!a[m]){var q="function"==typeof require&&require;if(!p&&q)return q(m,!0);if(l)return l(m,!0);q=Error("Cannot find module '"+m+"'");throw q.code="MODULE_NOT_FOUND",q;}q=b[m]={ja:{}};a[m][0].call(q.ja,function(b){var d=
+a[m][1][b];return h(d?d:b)},q,q.ja,d,a,b,g)}return b[m].ja}for(var l="function"==typeof require&&require,m=0;m<g.length;m++)h(g[m]);return h}({1:[function(a,b,f){f.read=function(a,b,d,e,f){var p;p=8*f-e-1;var q=(1<<p)-1,r=q>>1,u=-7;f=d?f-1:0;var x=d?-1:1,v=a[b+f];f+=x;d=v&(1<<-u)-1;v>>=-u;for(u+=p;0<u;d=256*d+a[b+f],f+=x,u-=8);p=d&(1<<-u)-1;d>>=-u;for(u+=e;0<u;p=256*p+a[b+f],f+=x,u-=8);if(0===d)d=1-r;else{if(d===q)return p?NaN:Infinity*(v?-1:1);p+=Math.pow(2,e);d-=r}return(v?-1:1)*p*Math.pow(2,d-
+e)};f.write=function(a,b,d,e,f,p){var q,r=8*p-f-1,u=(1<<r)-1,x=u>>1,v=23===f?Math.pow(2,-24)-Math.pow(2,-77):0;p=e?0:p-1;var D=e?1:-1,A=0>b||0===b&&0>1/b?1:0;b=Math.abs(b);isNaN(b)||Infinity===b?(b=isNaN(b)?1:0,e=u):(e=Math.floor(Math.log(b)/Math.LN2),1>b*(q=Math.pow(2,-e))&&(e--,q*=2),b=1<=e+x?b+v/q:b+v*Math.pow(2,1-x),2<=b*q&&(e++,q/=2),e+x>=u?(b=0,e=u):1<=e+x?(b=(b*q-1)*Math.pow(2,f),e+=x):(b=b*Math.pow(2,x-1)*Math.pow(2,f),e=0));for(;8<=f;a[d+p]=b&255,p+=D,b/=256,f-=8);e=e<<f|b;for(r+=f;0<r;a[d+
+p]=e&255,p+=D,e/=256,r-=8);a[d+p-D]|=128*A}},{}],2:[function(a,b){function f(a){var b;a&&a.length&&(b=a,a=b.length);a=new Uint8Array(a||0);b&&a.set(b);a.$h=l.$h;a.Zf=l.Zf;a.Sh=l.Sh;a.Ei=l.Ei;a.Nf=l.Nf;a.Di=l.Di;a.Hf=l.Hf;a.Ai=l.Ai;a.toString=l.toString;a.write=l.write;a.slice=l.slice;a.tg=l.tg;a.nj=!0;return a}function g(a){for(var b=a.length,d=[],e=0,f,g;e<b;e++){f=a.charCodeAt(e);if(55295<f&&57344>f)if(g)if(56320>f){d.push(239,191,189);g=f;continue}else f=g-55296<<10|f-56320|65536,g=null;else{56319<
+f||e+1===b?d.push(239,191,189):g=f;continue}else g&&(d.push(239,191,189),g=null);128>f?d.push(f):2048>f?d.push(f>>6|192,f&63|128):65536>f?d.push(f>>12|224,f>>6&63|128,f&63|128):d.push(f>>18|240,f>>12&63|128,f>>6&63|128,f&63|128)}return d}b.ja=f;var h=a("ieee754"),l,m,n;l={$h:function(a){return(this[a]|this[a+1]<<8|this[a+2]<<16)+16777216*this[a+3]},Zf:function(a,b){this[b]=a;this[b+1]=a>>>8;this[b+2]=a>>>16;this[b+3]=a>>>24},Sh:function(a){return(this[a]|this[a+1]<<8|this[a+2]<<16)+(this[a+3]<<24)},
+Nf:function(a){return h.read(this,a,!0,23,4)},Hf:function(a){return h.read(this,a,!0,52,8)},Di:function(a,b){return h.write(this,a,b,!0,23,4)},Ai:function(a,b){return h.write(this,a,b,!0,52,8)},toString:function(a,b,d){var e=a="";d=Math.min(this.length,d||this.length);for(b=b||0;b<d;b++){var f=this[b];127>=f?(a+=decodeURIComponent(e)+String.fromCharCode(f),e=""):e+="%"+f.toString(16)}return a+=decodeURIComponent(e)},write:function(a,b){for(var d=a===m?n:g(a),e=0;e<d.length;e++)this[b+e]=d[e]},slice:function(a,
+b){return this.subarray(a,b)},tg:function(a,b){b=b||0;for(var d=0;d<this.length;d++)a[b+d]=this[d]}};l.Ei=l.Zf;f.byteLength=function(a){m=a;n=g(a);return n.length};f.isBuffer=function(a){return!(!a||!a.nj)}},{ieee754:1}],3:[function(a,b){(function(f){function g(a){this.Cb=l.isBuffer(a)?a:new l(a||0);this.da=0;this.length=this.Cb.length}function h(a,b){var d=b.Cb,e;e=d[b.da++];a+=268435456*(e&127);if(128>e)return a;e=d[b.da++];a+=34359738368*(e&127);if(128>e)return a;e=d[b.da++];a+=4398046511104*(e&
+127);if(128>e)return a;e=d[b.da++];a+=562949953421312*(e&127);if(128>e)return a;e=d[b.da++];a+=72057594037927936*(e&127);if(128>e)return a;e=d[b.da++];if(128>e)return a+0x7fffffffffffffff*(e&127);throw Error("Expected varint not more than 10 bytes");}b.ja=g;var l=f.Ap||a("./buffer");g.f=0;g.g=1;g.b=2;g.a=5;var m=Math.pow(2,63);g.prototype={Lf:function(a,b,d){for(d=d||this.length;this.da<d;){var e=this.Da(),f=this.da;a(e>>3,b,this);this.da===f&&this.op(e)}return b},Bo:function(){var a=this.Cb.Nf(this.da);
+this.da+=4;return a},xo:function(){var a=this.Cb.Hf(this.da);this.da+=8;return a},Da:function(){var a=this.Cb,b,d;d=a[this.da++];b=d&127;if(128>d)return b;d=a[this.da++];b|=(d&127)<<7;if(128>d)return b;d=a[this.da++];b|=(d&127)<<14;if(128>d)return b;d=a[this.da++];b|=(d&127)<<21;return 128>d?b:h(b,this)},Mo:function(){var a=this.da,b=this.Da();if(b<m)return b;for(var d=this.da-2;255===this.Cb[d];)d--;d<a&&(d=a);for(var e=b=0;e<d-a+1;e++)var f=~this.Cb[a+e]&127,b=b+(4>e?f<<7*e:f*Math.pow(2,7*e));return-b-
+1},xd:function(){var a=this.Da();return 1===a%2?(a+1)/-2:a/2},vo:function(){return!!this.Da()},Qf:function(){var a=this.Da()+this.da,b=this.Cb.toString("utf8",this.da,a);this.da=a;return b},op:function(a){a&=7;if(a===g.f)for(;127<this.Cb[this.da++];);else if(a===g.b)this.da=this.Da()+this.da;else if(a===g.a)this.da+=4;else if(a===g.g)this.da+=8;else throw Error("Unimplemented type: "+a);}}}).call(this,"undefined"!==typeof global?global:"undefined"!==typeof self?self:"undefined"!==typeof window?window:
+{})},{"./buffer":2}]},{},[3])(3)});hl=b.ja})();(function(){var a={},b={ja:a};(function(c){if("object"===typeof a&&"undefined"!==typeof b)b.ja=c();else{var d;"undefined"!==typeof window?d=window:"undefined"!==typeof global?d=global:"undefined"!==typeof self?d=self:d=this;d.Up=c()}})(function(){return function d(a,b,g){function h(m,p){if(!b[m]){if(!a[m]){var q="function"==typeof require&&require;if(!p&&q)return q(m,!0);if(l)return l(m,!0);q=Error("Cannot find module '"+m+"'");throw q.code="MODULE_NOT_FOUND",q;}q=b[m]={ja:{}};a[m][0].call(q.ja,function(b){var d=
+a[m][1][b];return h(d?d:b)},q,q.ja,d,a,b,g)}return b[m].ja}for(var l="function"==typeof require&&require,m=0;m<g.length;m++)h(g[m]);return h}({1:[function(a,b){function f(a,b){this.x=a;this.y=b}b.ja=f;f.prototype={clone:function(){return new f(this.x,this.y)},add:function(a){return this.clone().fj(a)},rotate:function(a){return this.clone().qj(a)},round:function(){return this.clone().rj()},angle:function(){return Math.atan2(this.y,this.x)},fj:function(a){this.x+=a.x;this.y+=a.y;return this},qj:function(a){var b=
+Math.cos(a);a=Math.sin(a);var d=a*this.x+b*this.y;this.x=b*this.x-a*this.y;this.y=d;return this},rj:function(){this.x=Math.round(this.x);this.y=Math.round(this.y);return this}};f.b=function(a){return a instanceof f?a:Array.isArray(a)?new f(a[0],a[1]):a}},{}],2:[function(a,b){b.ja.ej=a("./lib/vectortile.js");b.ja.Np=a("./lib/vectortilefeature.js");b.ja.Op=a("./lib/vectortilelayer.js")},{"./lib/vectortile.js":3,"./lib/vectortilefeature.js":4,"./lib/vectortilelayer.js":5}],3:[function(a,b){function f(a,
+b,d){3===a&&(a=new g(d,d.Da()+d.da),a.length&&(b[a.name]=a))}var g=a("./vectortilelayer");b.ja=function(a,b){this.layers=a.Lf(f,{},b)}},{"./vectortilelayer":5}],4:[function(a,b){function f(a,b,d,e,f){this.properties={};this.extent=d;this.type=0;this.qc=a;this.Qe=-1;this.Id=e;this.Kd=f;a.Lf(g,this,b)}function g(a,b,d){if(1==a)b.Qp=d.Da();else if(2==a)for(a=d.Da()+d.da;d.da<a;){var e=b.Id[d.Da()],f=b.Kd[d.Da()];b.properties[e]=f}else 3==a?b.type=d.Da():4==a&&(b.Qe=d.da)}var h=a("point-geometry");b.ja=
+f;f.b=["Unknown","Point","LineString","Polygon"];f.prototype.Vg=function(){var a=this.qc;a.da=this.Qe;for(var b=a.Da()+a.da,d=1,e=0,f=0,g=0,u=[],x;a.da<b;)if(e||(e=a.Da(),d=e&7,e>>=3),e--,1===d||2===d)f+=a.xd(),g+=a.xd(),1===d&&(x&&u.push(x),x=[]),x.push(new h(f,g));else if(7===d)x&&x.push(x[0].clone());else throw Error("unknown command "+d);x&&u.push(x);return u};f.prototype.bbox=function(){var a=this.qc;a.da=this.Qe;for(var b=a.Da()+a.da,d=1,e=0,f=0,g=0,h=Infinity,x=-Infinity,v=Infinity,D=-Infinity;a.da<
+b;)if(e||(e=a.Da(),d=e&7,e>>=3),e--,1===d||2===d)f+=a.xd(),g+=a.xd(),f<h&&(h=f),f>x&&(x=f),g<v&&(v=g),g>D&&(D=g);else if(7!==d)throw Error("unknown command "+d);return[h,v,x,D]}},{"point-geometry":1}],5:[function(a,b){function f(a,b){this.version=1;this.name=null;this.extent=4096;this.length=0;this.qc=a;this.Id=[];this.Kd=[];this.Hd=[];a.Lf(g,this,b);this.length=this.Hd.length}function g(a,b,d){15===a?b.version=d.Da():1===a?b.name=d.Qf():5===a?b.extent=d.Da():2===a?b.Hd.push(d.da):3===a?b.Id.push(d.Qf()):
+4===a&&b.Kd.push(h(d))}function h(a){for(var b=null,d=a.Da()+a.da;a.da<d;)b=a.Da()>>3,b=1===b?a.Qf():2===b?a.Bo():3===b?a.xo():4===b?a.Mo():5===b?a.Da():6===b?a.xd():7===b?a.vo():null;return b}var l=a("./vectortilefeature.js");b.ja=f;f.prototype.feature=function(a){if(0>a||a>=this.Hd.length)throw Error("feature index out of bounds");this.qc.da=this.Hd[a];a=this.qc.Da()+this.qc.da;return new l(this.qc,a,this.extent,this.Id,this.Kd)}},{"./vectortilefeature.js":4}]},{},[2])(2)});il=b.ja})();function wr(a){this.defaultDataProjection=null;a=a?a:{};this.defaultDataProjection=new vc({code:"",units:"tile-pixels"});this.b=a.featureClass?a.featureClass:hk;this.g=a.geometryName?a.geometryName:"geometry";this.a=a.layerName?a.layerName:"layer";this.f=a.layers?a.layers:null}y(wr,rn);wr.prototype.X=function(){return"arraybuffer"};
+wr.prototype.Fa=function(a,b){var c=this.f,d=new hl(a),d=new il.ej(d),e=[],f=this.b,g,h,l;for(l in d.layers)if(!c||-1!=c.indexOf(l)){g=d.layers[l];for(var m=0,n=g.length;m<n;++m){if(f===hk){var p=g.feature(m);h=l;var q=p.Vg(),r=[],u=[];xr(q,u,r);var x=p.type,v=void 0;1===x?v=1===q.length?"Point":"MultiPoint":2===x?v=1===q.length?"LineString":"MultiLineString":3===x&&(v="Polygon");p=p.properties;p[this.a]=h;h=new this.b(v,u,r,p)}else{p=g.feature(m);v=l;u=b;h=new this.b;r=p.properties;r[this.a]=v;v=
+p.type;if(0===v)v=null;else{p=p.Vg();q=[];x=[];xr(p,x,q);var D=void 0;1===v?D=1===p.length?new C(null):new Bn(null):2===v?1===p.length?D=new R(null):D=new S(null):3===v&&(D=new E(null));D.ba("XY",x,q);v=D}(u=un(v,!1,tn(this,u)))&&(r[this.g]=u);h.G(r);h.Ec(this.g)}e.push(h)}}return e};wr.prototype.Oa=function(){return this.defaultDataProjection};wr.prototype.c=function(a){this.f=a};
+function xr(a,b,c){for(var d=0,e=0,f=a.length;e<f;++e){var g=a[e],h,l;h=0;for(l=g.length;h<l;++h){var m=g[h];b.push(m.x,m.y)}d+=2*h;c.push(d)}};function yr(a,b){return new zr(a,b)}function Ar(a,b,c){return new Br(a,b,c)}function Cr(a){this.Wb=a}function Dr(a){this.Wb=a}y(Dr,Cr);function Er(a,b,c){this.Wb=a;this.b=b;this.a=c}y(Er,Dr);function zr(a,b){Er.call(this,"And",a,b)}y(zr,Er);function Fr(a,b){Er.call(this,"Or",a,b)}y(Fr,Er);function Gr(a){this.Wb="Not";this.condition=a}y(Gr,Dr);function Br(a,b,c){this.Wb="BBOX";this.geometryName=a;this.extent=b;this.srsName=c}y(Br,Cr);function Hr(a,b){this.Wb=a;this.b=b}y(Hr,Cr);
+function Ir(a,b,c,d){Hr.call(this,a,b);this.g=c;this.a=d}y(Ir,Hr);function Jr(a,b,c){Ir.call(this,"PropertyIsEqualTo",a,b,c)}y(Jr,Ir);function Kr(a,b,c){Ir.call(this,"PropertyIsNotEqualTo",a,b,c)}y(Kr,Ir);function Lr(a,b){Ir.call(this,"PropertyIsLessThan",a,b)}y(Lr,Ir);function Mr(a,b){Ir.call(this,"PropertyIsLessThanOrEqualTo",a,b)}y(Mr,Ir);function Nr(a,b){Ir.call(this,"PropertyIsGreaterThan",a,b)}y(Nr,Ir);function Or(a,b){Ir.call(this,"PropertyIsGreaterThanOrEqualTo",a,b)}y(Or,Ir);
+function Pr(a){Hr.call(this,"PropertyIsNull",a)}y(Pr,Hr);function Qr(a,b,c){Hr.call(this,"PropertyIsBetween",a);this.a=b;this.g=c}y(Qr,Hr);function Rr(a,b,c,d,e,f){Hr.call(this,"PropertyIsLike",a);this.f=b;this.i=void 0!==c?c:"*";this.c=void 0!==d?d:".";this.g=void 0!==e?e:"!";this.a=f}y(Rr,Hr);function Sr(){Un.call(this);this.defaultDataProjection=yc("EPSG:4326")}y(Sr,Un);function Tr(a,b){b[b.length-1].Cd[a.getAttribute("k")]=a.getAttribute("v")}
+var Ur=[null],Vr=M(Ur,{nd:function(a,b){b[b.length-1].Rc.push(a.getAttribute("ref"))},tag:Tr}),Xr=M(Ur,{node:function(a,b){var c=b[0],d=b[b.length-1],e=a.getAttribute("id"),f=[parseFloat(a.getAttribute("lon")),parseFloat(a.getAttribute("lat"))];d.Zg[e]=f;var g=O({Cd:{}},Wr,a,b);Ha(g.Cd)||(f=new C(f),un(f,!1,c),c=new Ik(f),c.mc(e),c.G(g.Cd),d.features.push(c))},way:function(a,b){for(var c=b[0],d=a.getAttribute("id"),e=O({Rc:[],Cd:{}},Vr,a,b),f=b[b.length-1],g=[],h=0,l=e.Rc.length;h<l;h++)mb(g,f.Zg[e.Rc[h]]);
+e.Rc[0]==e.Rc[e.Rc.length-1]?(h=new E(null),h.ba("XY",g,[g.length])):(h=new R(null),h.ba("XY",g));un(h,!1,c);c=new Ik(h);c.mc(d);c.G(e.Cd);f.features.push(c)}}),Wr=M(Ur,{tag:Tr});Sr.prototype.lc=function(a,b){var c=sn(this,a,b);return"osm"==a.localName&&(c=O({Zg:{},features:[]},Xr,a,[c]),c.features)?c.features:[]};function Yr(a){return a.getAttributeNS("http://www.w3.org/1999/xlink","href")};function Zr(){}Zr.prototype.read=function(a){return Pk(a)?this.a(a):Qk(a)?this.b(a):"string"===typeof a?(a=Rk(a),this.a(a)):null};function $r(){}y($r,Zr);$r.prototype.a=function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType==Node.ELEMENT_NODE)return this.b(a);return null};$r.prototype.b=function(a){return(a=O({},as,a,[]))?a:null};
+var bs=[null,"http://www.opengis.net/ows/1.1"],as=M(bs,{ServiceIdentification:J(function(a,b){return O({},cs,a,b)}),ServiceProvider:J(function(a,b){return O({},ds,a,b)}),OperationsMetadata:J(function(a,b){return O({},es,a,b)})}),fs=M(bs,{DeliveryPoint:J(U),City:J(U),AdministrativeArea:J(U),PostalCode:J(U),Country:J(U),ElectronicMailAddress:J(U)}),gs=M(bs,{Value:Vk(function(a){return U(a)})}),hs=M(bs,{AllowedValues:J(function(a,b){return O({},gs,a,b)})}),js=M(bs,{Phone:J(function(a,b){return O({},
+is,a,b)}),Address:J(function(a,b){return O({},fs,a,b)})}),ls=M(bs,{HTTP:J(function(a,b){return O({},ks,a,b)})}),ks=M(bs,{Get:Vk(function(a,b){var c=Yr(a);return c?O({href:c},ms,a,b):void 0}),Post:void 0}),ns=M(bs,{DCP:J(function(a,b){return O({},ls,a,b)})}),es=M(bs,{Operation:function(a,b){var c=a.getAttribute("name"),d=O({},ns,a,b);d&&(b[b.length-1][c]=d)}}),is=M(bs,{Voice:J(U),Facsimile:J(U)}),ms=M(bs,{Constraint:Vk(function(a,b){var c=a.getAttribute("name");return c?O({name:c},hs,a,b):void 0})}),
+os=M(bs,{IndividualName:J(U),PositionName:J(U),ContactInfo:J(function(a,b){return O({},js,a,b)})}),cs=M(bs,{Title:J(U),ServiceTypeVersion:J(U),ServiceType:J(U)}),ds=M(bs,{ProviderName:J(U),ProviderSite:J(Yr),ServiceContact:J(function(a,b){return O({},os,a,b)})});function ps(a,b,c,d){var e;void 0!==d?e=d:e=[];for(var f=d=0;f<b;){var g=a[f++];e[d++]=a[f++];e[d++]=g;for(g=2;g<c;++g)e[d++]=a[f++]}e.length=d};function qs(a){a=a?a:{};this.defaultDataProjection=null;this.defaultDataProjection=yc("EPSG:4326");this.b=a.factor?a.factor:1E5;this.a=a.geometryLayout?a.geometryLayout:"XY"}y(qs,bp);function rs(a,b,c){var d,e=Array(b);for(d=0;d<b;++d)e[d]=0;var f,g;f=0;for(g=a.length;f<g;)for(d=0;d<b;++d,++f){var h=a[f],l=h-e[d];e[d]=h;a[f]=l}return ss(a,c?c:1E5)}
+function ts(a,b,c){var d,e=Array(b);for(d=0;d<b;++d)e[d]=0;a=us(a,c?c:1E5);var f;c=0;for(f=a.length;c<f;)for(d=0;d<b;++d,++c)e[d]+=a[c],a[c]=e[d];return a}function ss(a,b){var c=b?b:1E5,d,e;d=0;for(e=a.length;d<e;++d)a[d]=Math.round(a[d]*c);c=0;for(d=a.length;c<d;++c)e=a[c],a[c]=0>e?~(e<<1):e<<1;c="";d=0;for(e=a.length;d<e;++d){for(var f=a[d],g,h="";32<=f;)g=(32|f&31)+63,h+=String.fromCharCode(g),f>>=5;h+=String.fromCharCode(f+63);c+=h}return c}
+function us(a,b){var c=b?b:1E5,d=[],e=0,f=0,g,h;g=0;for(h=a.length;g<h;++g){var l=a.charCodeAt(g)-63,e=e|(l&31)<<f;32>l?(d.push(e),f=e=0):f+=5}e=0;for(f=d.length;e<f;++e)g=d[e],d[e]=g&1?~(g>>1):g>>1;e=0;for(f=d.length;e<f;++e)d[e]/=c;return d}k=qs.prototype;k.ud=function(a,b){var c=this.wd(a,b);return new Ik(c)};k.Kf=function(a,b){return[this.ud(a,b)]};k.wd=function(a,b){var c=id(this.a),d=ts(a,c,this.b);ps(d,d.length,c,d);c=vd(d,0,d.length,c);return un(new R(c,this.a),!1,tn(this,b))};
+k.He=function(a,b){var c=a.W();return c?this.Ed(c,b):""};k.Ci=function(a,b){return this.He(a[0],b)};k.Ed=function(a,b){a=un(a,!0,tn(this,b));var c=a.la(),d=a.va();ps(c,c.length,d,c);return rs(c,d,this.b)};function ws(a){a=a?a:{};this.defaultDataProjection=null;this.defaultDataProjection=yc(a.defaultDataProjection?a.defaultDataProjection:"EPSG:4326")}y(ws,vn);function xs(a,b){var c=[],d,e,f,g;f=0;for(g=a.length;f<g;++f)d=a[f],0<f&&c.pop(),0<=d?e=b[d]:e=b[~d].slice().reverse(),c.push.apply(c,e);d=0;for(e=c.length;d<e;++d)c[d]=c[d].slice();return c}function ys(a,b,c,d,e){a=a.geometries;var f=[],g,h;g=0;for(h=a.length;g<h;++g)f[g]=zs(a[g],b,c,d,e);return f}
+function zs(a,b,c,d,e){var f=a.type,g=As[f];b="Point"===f||"MultiPoint"===f?g(a,c,d):g(a,b);c=new Ik;c.Ua(un(b,!1,e));void 0!==a.id&&c.mc(a.id);a.properties&&c.G(a.properties);return c}
+ws.prototype.Jf=function(a,b){if("Topology"==a.type){var c,d=null,e=null;a.transform&&(c=a.transform,d=c.scale,e=c.translate);var f=a.arcs;if(c){c=d;var g=e,h,l;h=0;for(l=f.length;h<l;++h){var m=f[h],n=c,p=g,q=0,r=0,u,x,v;x=0;for(v=m.length;x<v;++x)u=m[x],q+=u[0],r+=u[1],u[0]=q,u[1]=r,Bs(u,n,p)}}c=[];g=Ga(a.objects);h=0;for(l=g.length;h<l;++h)"GeometryCollection"===g[h].type?(m=g[h],c.push.apply(c,ys(m,f,d,e,b))):(m=g[h],c.push(zs(m,f,d,e,b)));return c}return[]};
+function Bs(a,b,c){a[0]=a[0]*b[0]+c[0];a[1]=a[1]*b[1]+c[1]}ws.prototype.Oa=function(){return this.defaultDataProjection};
+var As={Point:function(a,b,c){a=a.coordinates;b&&c&&Bs(a,b,c);return new C(a)},LineString:function(a,b){var c=xs(a.arcs,b);return new R(c)},Polygon:function(a,b){var c=[],d,e;d=0;for(e=a.arcs.length;d<e;++d)c[d]=xs(a.arcs[d],b);return new E(c)},MultiPoint:function(a,b,c){a=a.coordinates;var d,e;if(b&&c)for(d=0,e=a.length;d<e;++d)Bs(a[d],b,c);return new Bn(a)},MultiLineString:function(a,b){var c=[],d,e;d=0;for(e=a.arcs.length;d<e;++d)c[d]=xs(a.arcs[d],b);return new S(c)},MultiPolygon:function(a,b){var c=
+[],d,e,f,g,h,l;h=0;for(l=a.arcs.length;h<l;++h){d=a.arcs[h];e=[];f=0;for(g=d.length;f<g;++f)e[f]=xs(d[f],b);c[h]=e}return new T(c)}};function Cs(a){a=a?a:{};this.i=a.featureType;this.g=a.featureNS;this.b=a.gmlFormat?a.gmlFormat:new lo;this.c=a.schemaLocation?a.schemaLocation:"http://www.opengis.net/wfs http://schemas.opengis.net/wfs/1.1.0/wfs.xsd";Un.call(this)}y(Cs,Un);Cs.prototype.lc=function(a,b){var c={featureType:this.i,featureNS:this.g};Ea(c,sn(this,a,b?b:{}));c=[c];this.b.b["http://www.opengis.net/gml"].featureMember=Tk(Xn.prototype.vd);(c=O([],this.b.b,a,c,this.b))||(c=[]);return c};
+Cs.prototype.o=function(a){if(Pk(a))return Ds(a);if(Qk(a))return O({},Es,a,[]);if("string"===typeof a)return a=Rk(a),Ds(a)};Cs.prototype.l=function(a){if(Pk(a))return Fs(this,a);if(Qk(a))return Gs(this,a);if("string"===typeof a)return a=Rk(a),Fs(this,a)};function Fs(a,b){for(var c=b.firstChild;c;c=c.nextSibling)if(c.nodeType==Node.ELEMENT_NODE)return Gs(a,c)}var Hs={"http://www.opengis.net/gml":{boundedBy:J(Xn.prototype.ye,"bounds")}};
+function Gs(a,b){var c={},d=fo(b.getAttribute("numberOfFeatures"));c.numberOfFeatures=d;return O(c,Hs,b,[],a.b)}
+var Is={"http://www.opengis.net/wfs":{totalInserted:J(eo),totalUpdated:J(eo),totalDeleted:J(eo)}},Js={"http://www.opengis.net/ogc":{FeatureId:Tk(function(a){return a.getAttribute("fid")})}},Ks={"http://www.opengis.net/wfs":{Feature:function(a,b){al(Js,a,b)}}},Es={"http://www.opengis.net/wfs":{TransactionSummary:J(function(a,b){return O({},Is,a,b)},"transactionSummary"),InsertResults:J(function(a,b){return O([],Ks,a,b)},"insertIds")}};
+function Ds(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType==Node.ELEMENT_NODE)return O({},Es,a,[])}var Ls={"http://www.opengis.net/wfs":{PropertyName:L(ho)}};function Ms(a,b){var c=Mk("http://www.opengis.net/ogc","Filter"),d=Mk("http://www.opengis.net/ogc","FeatureId");c.appendChild(d);d.setAttribute("fid",b);a.appendChild(c)}
+var Ns={"http://www.opengis.net/wfs":{Insert:L(function(a,b,c){var d=c[c.length-1],d=Mk(d.featureNS,d.featureType);a.appendChild(d);lo.prototype.Bi(d,b,c)}),Update:L(function(a,b,c){var d=c[c.length-1],e=d.featureType,f=d.featurePrefix,f=f?f:"feature",g=d.featureNS;a.setAttribute("typeName",f+":"+e);a.setAttributeNS("http://www.w3.org/2000/xmlns/","xmlns:"+f,g);e=b.Xa();if(void 0!==e){for(var f=b.N(),g=[],h=0,l=f.length;h<l;h++){var m=b.get(f[h]);void 0!==m&&g.push({name:f[h],value:m})}bl({node:a,
+srsName:d.srsName},Ns,Xk("Property"),g,c);Ms(a,e)}}),Delete:L(function(a,b,c){var d=c[c.length-1];c=d.featureType;var e=d.featurePrefix,e=e?e:"feature",d=d.featureNS;a.setAttribute("typeName",e+":"+c);a.setAttributeNS("http://www.w3.org/2000/xmlns/","xmlns:"+e,d);b=b.Xa();void 0!==b&&Ms(a,b)}),Property:L(function(a,b,c){var d=Mk("http://www.opengis.net/wfs","Name");a.appendChild(d);ho(d,b.name);void 0!==b.value&&null!==b.value&&(d=Mk("http://www.opengis.net/wfs","Value"),a.appendChild(d),b.value instanceof
+Tc?lo.prototype.Je(d,b.value,c):ho(d,b.value))}),Native:L(function(a,b){b.wp&&a.setAttribute("vendorId",b.wp);void 0!==b.Yo&&a.setAttribute("safeToIgnore",b.Yo);void 0!==b.value&&ho(a,b.value)})}};function Os(a,b,c){a={node:a};var d=b.b;bl(a,Ps,Xk(d.Wb),[d],c);b=b.a;bl(a,Ps,Xk(b.Wb),[b],c)}function Qs(a,b){void 0!==b.a&&a.setAttribute("matchCase",b.a.toString());Rs(a,b.b);Ss("Literal",a,""+b.g)}function Ss(a,b,c){a=Mk("http://www.opengis.net/ogc",a);ho(a,c);b.appendChild(a)}
+function Rs(a,b){Ss("PropertyName",a,b)}
+var Ps={"http://www.opengis.net/wfs":{Query:L(function(a,b,c){var d=c[c.length-1],e=d.featurePrefix,f=d.featureNS,g=d.propertyNames,h=d.srsName;a.setAttribute("typeName",(e?e+":":"")+b);h&&a.setAttribute("srsName",h);f&&a.setAttributeNS("http://www.w3.org/2000/xmlns/","xmlns:"+e,f);b=Ea({},d);b.node=a;bl(b,Ls,Xk("PropertyName"),g,c);if(d=d.filter)g=Mk("http://www.opengis.net/ogc","Filter"),a.appendChild(g),bl({node:g},Ps,Xk(d.Wb),[d],c)})},"http://www.opengis.net/ogc":{And:L(Os),Or:L(Os),Not:L(function(a,
+b,c){b=b.condition;bl({node:a},Ps,Xk(b.Wb),[b],c)}),BBOX:L(function(a,b,c){c[c.length-1].srsName=b.srsName;Rs(a,b.geometryName);lo.prototype.Je(a,b.extent,c)}),PropertyIsEqualTo:L(Qs),PropertyIsNotEqualTo:L(Qs),PropertyIsLessThan:L(Qs),PropertyIsLessThanOrEqualTo:L(Qs),PropertyIsGreaterThan:L(Qs),PropertyIsGreaterThanOrEqualTo:L(Qs),PropertyIsNull:L(function(a,b){Rs(a,b.b)}),PropertyIsBetween:L(function(a,b){Rs(a,b.b);Ss("LowerBoundary",a,""+b.a);Ss("UpperBoundary",a,""+b.g)}),PropertyIsLike:L(function(a,
+b){a.setAttribute("wildCard",b.i);a.setAttribute("singleChar",b.c);a.setAttribute("escapeChar",b.g);void 0!==b.a&&a.setAttribute("matchCase",b.a.toString());Rs(a,b.b);Ss("Literal",a,""+b.f)})}};
+Cs.prototype.j=function(a){var b=Mk("http://www.opengis.net/wfs","GetFeature");b.setAttribute("service","WFS");b.setAttribute("version","1.1.0");var c;if(a&&(a.handle&&b.setAttribute("handle",a.handle),a.outputFormat&&b.setAttribute("outputFormat",a.outputFormat),void 0!==a.maxFeatures&&b.setAttribute("maxFeatures",a.maxFeatures),a.resultType&&b.setAttribute("resultType",a.resultType),void 0!==a.startIndex&&b.setAttribute("startIndex",a.startIndex),void 0!==a.count&&b.setAttribute("count",a.count),
+c=a.filter,a.bbox)){var d=Ar(a.geometryName,a.bbox,a.srsName);c?c=yr(c,d):c=d}b.setAttributeNS("http://www.w3.org/2001/XMLSchema-instance","xsi:schemaLocation",this.c);d=a.featureTypes;a=[{node:b,srsName:a.srsName,featureNS:a.featureNS?a.featureNS:this.g,featurePrefix:a.featurePrefix,geometryName:a.geometryName,filter:c,propertyNames:a.propertyNames?a.propertyNames:[]}];c=Ea({},a[a.length-1]);c.node=b;bl(c,Ps,Xk("Query"),d,a);return b};
+Cs.prototype.U=function(a,b,c,d){var e=[],f=Mk("http://www.opengis.net/wfs","Transaction");f.setAttribute("service","WFS");f.setAttribute("version","1.1.0");var g,h;d&&(g=d.gmlOptions?d.gmlOptions:{},d.handle&&f.setAttribute("handle",d.handle));f.setAttributeNS("http://www.w3.org/2001/XMLSchema-instance","xsi:schemaLocation",this.c);a&&(h={node:f,featureNS:d.featureNS,featureType:d.featureType,featurePrefix:d.featurePrefix,srsName:d.srsName},Ea(h,g),bl(h,Ns,Xk("Insert"),a,e));b&&(h={node:f,featureNS:d.featureNS,
+featureType:d.featureType,featurePrefix:d.featurePrefix,srsName:d.srsName},Ea(h,g),bl(h,Ns,Xk("Update"),b,e));c&&bl({node:f,featureNS:d.featureNS,featureType:d.featureType,featurePrefix:d.featurePrefix,srsName:d.srsName},Ns,Xk("Delete"),c,e);d.nativeElements&&bl({node:f,featureNS:d.featureNS,featureType:d.featureType,featurePrefix:d.featurePrefix,srsName:d.srsName},Ns,Xk("Native"),d.nativeElements,e);return f};
+Cs.prototype.Pf=function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType==Node.ELEMENT_NODE)return this.Be(a);return null};Cs.prototype.Be=function(a){if(a.firstElementChild&&a.firstElementChild.firstElementChild)for(a=a.firstElementChild.firstElementChild,a=a.firstElementChild;a;a=a.nextElementSibling)if(0!==a.childNodes.length&&(1!==a.childNodes.length||3!==a.firstChild.nodeType)){var b=[{}];this.b.ye(a,b);return yc(b.pop().srsName)}return null};function Ts(a){a=a?a:{};this.defaultDataProjection=null;this.b=void 0!==a.splitCollection?a.splitCollection:!1}y(Ts,bp);function Us(a){a=a.Z();return 0===a.length?"":a[0]+" "+a[1]}function Vs(a){a=a.Z();for(var b=[],c=0,d=a.length;c<d;++c)b.push(a[c][0]+" "+a[c][1]);return b.join(",")}function Ws(a){var b=[];a=a.Vd();for(var c=0,d=a.length;c<d;++c)b.push("("+Vs(a[c])+")");return b.join(",")}function Xs(a){var b=a.X();a=(0,Ys[b])(a);b=b.toUpperCase();return 0===a.length?b+" EMPTY":b+"("+a+")"}
+var Ys={Point:Us,LineString:Vs,Polygon:Ws,MultiPoint:function(a){var b=[];a=a.je();for(var c=0,d=a.length;c<d;++c)b.push("("+Us(a[c])+")");return b.join(",")},MultiLineString:function(a){var b=[];a=a.md();for(var c=0,d=a.length;c<d;++c)b.push("("+Vs(a[c])+")");return b.join(",")},MultiPolygon:function(a){var b=[];a=a.Wd();for(var c=0,d=a.length;c<d;++c)b.push("("+Ws(a[c])+")");return b.join(",")},GeometryCollection:function(a){var b=[];a=a.ff();for(var c=0,d=a.length;c<d;++c)b.push(Xs(a[c]));return b.join(",")}};
+k=Ts.prototype;k.ud=function(a,b){var c=this.wd(a,b);if(c){var d=new Ik;d.Ua(c);return d}return null};k.Kf=function(a,b){var c=[],d=this.wd(a,b);this.b&&"GeometryCollection"==d.X()?c=d.c:c=[d];for(var e=[],f=0,g=c.length;f<g;++f)d=new Ik,d.Ua(c[f]),e.push(d);return e};k.wd=function(a,b){var c;c=new Zs(new $s(a));c.b=at(c.a);return(c=bt(c))?un(c,!1,b):null};k.He=function(a,b){var c=a.W();return c?this.Ed(c,b):""};
+k.Ci=function(a,b){if(1==a.length)return this.He(a[0],b);for(var c=[],d=0,e=a.length;d<e;++d)c.push(a[d].W());c=new Ln(c);return this.Ed(c,b)};k.Ed=function(a,b){return Xs(un(a,!0,b))};function $s(a){this.a=a;this.b=-1}
+function at(a){var b=a.a.charAt(++a.b),c={position:a.b,value:b};if("("==b)c.type=2;else if(","==b)c.type=5;else if(")"==b)c.type=3;else if("0"<=b&&"9">=b||"."==b||"-"==b){c.type=4;var d,b=a.b,e=!1,f=!1;do{if("."==d)e=!0;else if("e"==d||"E"==d)f=!0;d=a.a.charAt(++a.b)}while("0"<=d&&"9">=d||"."==d&&(void 0===e||!e)||!f&&("e"==d||"E"==d)||f&&("-"==d||"+"==d));a=parseFloat(a.a.substring(b,a.b--));c.value=a}else if("a"<=b&&"z">=b||"A"<=b&&"Z">=b){c.type=1;b=a.b;do d=a.a.charAt(++a.b);while("a"<=d&&"z">=
+d||"A"<=d&&"Z">=d);a=a.a.substring(b,a.b--).toUpperCase();c.value=a}else{if(" "==b||"\t"==b||"\r"==b||"\n"==b)return at(a);if(""===b)c.type=6;else throw Error("Unexpected character: "+b);}return c}function Zs(a){this.a=a}k=Zs.prototype;k.match=function(a){if(a=this.b.type==a)this.b=at(this.a);return a};
+function bt(a){var b=a.b;if(a.match(1)){var c=b.value;if("GEOMETRYCOLLECTION"==c){a:{if(a.match(2)){b=[];do b.push(bt(a));while(a.match(5));if(a.match(3)){a=b;break a}}else if(ct(a)){a=[];break a}throw Error(dt(a));}return new Ln(a)}var d=et[c],b=ft[c];if(!d||!b)throw Error("Invalid geometry type: "+c);a=d.call(a);return new b(a)}throw Error(dt(a));}k.Ef=function(){if(this.match(2)){var a=gt(this);if(this.match(3))return a}else if(ct(this))return null;throw Error(dt(this));};
+k.Df=function(){if(this.match(2)){var a=ht(this);if(this.match(3))return a}else if(ct(this))return[];throw Error(dt(this));};k.Ff=function(){if(this.match(2)){var a=it(this);if(this.match(3))return a}else if(ct(this))return[];throw Error(dt(this));};k.io=function(){if(this.match(2)){var a;if(2==this.b.type)for(a=[this.Ef()];this.match(5);)a.push(this.Ef());else a=ht(this);if(this.match(3))return a}else if(ct(this))return[];throw Error(dt(this));};
+k.ho=function(){if(this.match(2)){var a=it(this);if(this.match(3))return a}else if(ct(this))return[];throw Error(dt(this));};k.jo=function(){if(this.match(2)){for(var a=[this.Ff()];this.match(5);)a.push(this.Ff());if(this.match(3))return a}else if(ct(this))return[];throw Error(dt(this));};function gt(a){for(var b=[],c=0;2>c;++c){var d=a.b;if(a.match(4))b.push(d.value);else break}if(2==b.length)return b;throw Error(dt(a));}function ht(a){for(var b=[gt(a)];a.match(5);)b.push(gt(a));return b}
+function it(a){for(var b=[a.Df()];a.match(5);)b.push(a.Df());return b}function ct(a){var b=1==a.b.type&&"EMPTY"==a.b.value;b&&(a.b=at(a.a));return b}function dt(a){return"Unexpected `"+a.b.value+"` at position "+a.b.position+" in `"+a.a.a+"`"}var ft={POINT:C,LINESTRING:R,POLYGON:E,MULTIPOINT:Bn,MULTILINESTRING:S,MULTIPOLYGON:T},et={POINT:Zs.prototype.Ef,LINESTRING:Zs.prototype.Df,POLYGON:Zs.prototype.Ff,MULTIPOINT:Zs.prototype.io,MULTILINESTRING:Zs.prototype.ho,MULTIPOLYGON:Zs.prototype.jo};function jt(){this.version=void 0}y(jt,Zr);jt.prototype.a=function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType==Node.ELEMENT_NODE)return this.b(a);return null};jt.prototype.b=function(a){this.version=a.getAttribute("version").trim();return(a=O({version:this.version},kt,a,[]))?a:null};function lt(a,b){return O({},mt,a,b)}function nt(a,b){return O({},ot,a,b)}function pt(a,b){var c=lt(a,b);if(c){var d=[fo(a.getAttribute("width")),fo(a.getAttribute("height"))];c.size=d;return c}}
+function qt(a,b){return O([],rt,a,b)}
+var st=[null,"http://www.opengis.net/wms"],kt=M(st,{Service:J(function(a,b){return O({},tt,a,b)}),Capability:J(function(a,b){return O({},ut,a,b)})}),ut=M(st,{Request:J(function(a,b){return O({},vt,a,b)}),Exception:J(function(a,b){return O([],wt,a,b)}),Layer:J(function(a,b){return O({},xt,a,b)})}),tt=M(st,{Name:J(U),Title:J(U),Abstract:J(U),KeywordList:J(qt),OnlineResource:J(Yr),ContactInformation:J(function(a,b){return O({},yt,a,b)}),Fees:J(U),AccessConstraints:J(U),LayerLimit:J(eo),MaxWidth:J(eo),
+MaxHeight:J(eo)}),yt=M(st,{ContactPersonPrimary:J(function(a,b){return O({},zt,a,b)}),ContactPosition:J(U),ContactAddress:J(function(a,b){return O({},At,a,b)}),ContactVoiceTelephone:J(U),ContactFacsimileTelephone:J(U),ContactElectronicMailAddress:J(U)}),zt=M(st,{ContactPerson:J(U),ContactOrganization:J(U)}),At=M(st,{AddressType:J(U),Address:J(U),City:J(U),StateOrProvince:J(U),PostCode:J(U),Country:J(U)}),wt=M(st,{Format:Tk(U)}),xt=M(st,{Name:J(U),Title:J(U),Abstract:J(U),KeywordList:J(qt),CRS:Vk(U),
+EX_GeographicBoundingBox:J(function(a,b){var c=O({},Bt,a,b);if(c){var d=c.westBoundLongitude,e=c.southBoundLatitude,f=c.eastBoundLongitude,c=c.northBoundLatitude;return void 0===d||void 0===e||void 0===f||void 0===c?void 0:[d,e,f,c]}}),BoundingBox:Vk(function(a){var b=[co(a.getAttribute("minx")),co(a.getAttribute("miny")),co(a.getAttribute("maxx")),co(a.getAttribute("maxy"))],c=[co(a.getAttribute("resx")),co(a.getAttribute("resy"))];return{crs:a.getAttribute("CRS"),extent:b,res:c}}),Dimension:Vk(function(a){return{name:a.getAttribute("name"),
+units:a.getAttribute("units"),unitSymbol:a.getAttribute("unitSymbol"),"default":a.getAttribute("default"),multipleValues:$n(a.getAttribute("multipleValues")),nearestValue:$n(a.getAttribute("nearestValue")),current:$n(a.getAttribute("current")),values:U(a)}}),Attribution:J(function(a,b){return O({},Ct,a,b)}),AuthorityURL:Vk(function(a,b){var c=lt(a,b);if(c)return c.name=a.getAttribute("name"),c}),Identifier:Vk(U),MetadataURL:Vk(function(a,b){var c=lt(a,b);if(c)return c.type=a.getAttribute("type"),
+c}),DataURL:Vk(lt),FeatureListURL:Vk(lt),Style:Vk(function(a,b){return O({},Dt,a,b)}),MinScaleDenominator:J(bo),MaxScaleDenominator:J(bo),Layer:Vk(function(a,b){var c=b[b.length-1],d=O({},xt,a,b);if(d){var e=$n(a.getAttribute("queryable"));void 0===e&&(e=c.queryable);d.queryable=void 0!==e?e:!1;e=fo(a.getAttribute("cascaded"));void 0===e&&(e=c.cascaded);d.cascaded=e;e=$n(a.getAttribute("opaque"));void 0===e&&(e=c.opaque);d.opaque=void 0!==e?e:!1;e=$n(a.getAttribute("noSubsets"));void 0===e&&(e=c.noSubsets);
+d.noSubsets=void 0!==e?e:!1;(e=co(a.getAttribute("fixedWidth")))||(e=c.fixedWidth);d.fixedWidth=e;(e=co(a.getAttribute("fixedHeight")))||(e=c.fixedHeight);d.fixedHeight=e;["Style","CRS","AuthorityURL"].forEach(function(a){a in c&&(d[a]=(d[a]||[]).concat(c[a]))});"EX_GeographicBoundingBox BoundingBox Dimension Attribution MinScaleDenominator MaxScaleDenominator".split(" ").forEach(function(a){a in d||(d[a]=c[a])});return d}})}),Ct=M(st,{Title:J(U),OnlineResource:J(Yr),LogoURL:J(pt)}),Bt=M(st,{westBoundLongitude:J(bo),
+eastBoundLongitude:J(bo),southBoundLatitude:J(bo),northBoundLatitude:J(bo)}),vt=M(st,{GetCapabilities:J(nt),GetMap:J(nt),GetFeatureInfo:J(nt)}),ot=M(st,{Format:Vk(U),DCPType:Vk(function(a,b){return O({},Et,a,b)})}),Et=M(st,{HTTP:J(function(a,b){return O({},Ft,a,b)})}),Ft=M(st,{Get:J(lt),Post:J(lt)}),Dt=M(st,{Name:J(U),Title:J(U),Abstract:J(U),LegendURL:Vk(pt),StyleSheetURL:J(lt),StyleURL:J(lt)}),mt=M(st,{Format:J(U),OnlineResource:J(Yr)}),rt=M(st,{Keyword:Tk(U)});function Gt(a){a=a?a:{};this.g="http://mapserver.gis.umn.edu/mapserver";this.b=new ko;this.c=a.layers?a.layers:null;Un.call(this)}y(Gt,Un);
+Gt.prototype.lc=function(a,b){var c={};b&&Ea(c,sn(this,a,b));var d=[c];a.setAttribute("namespaceURI",this.g);var e=a.localName,c=[];if(0!==a.childNodes.length){if("msGMLOutput"==e)for(var f=0,g=a.childNodes.length;f<g;f++){var h=a.childNodes[f];if(h.nodeType===Node.ELEMENT_NODE){var l=d[0],m=h.localName.replace("_layer","");if(!this.c||jb(this.c,m)){m+="_feature";l.featureType=m;l.featureNS=this.g;var n={};n[m]=Tk(this.b.If,this.b);l=M([l.featureNS,null],n);h.setAttribute("namespaceURI",this.g);(h=
+O([],l,h,d,this.b))&&mb(c,h)}}}"FeatureCollection"==e&&(d=O([],this.b.b,a,[{}],this.b))&&(c=d)}return c};function Ht(){this.g=new $r}y(Ht,Zr);Ht.prototype.a=function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType==Node.ELEMENT_NODE)return this.b(a);return null};Ht.prototype.b=function(a){var b=a.getAttribute("version").trim(),c=this.g.b(a);if(!c)return null;c.version=b;return(c=O(c,It,a,[]))?c:null};function Jt(a){var b=U(a).split(" ");if(b&&2==b.length)return a=+b[0],b=+b[1],isNaN(a)||isNaN(b)?void 0:[a,b]}
+var Kt=[null,"http://www.opengis.net/wmts/1.0"],Lt=[null,"http://www.opengis.net/ows/1.1"],It=M(Kt,{Contents:J(function(a,b){return O({},Mt,a,b)})}),Mt=M(Kt,{Layer:Vk(function(a,b){return O({},Nt,a,b)}),TileMatrixSet:Vk(function(a,b){return O({},Ot,a,b)})}),Nt=M(Kt,{Style:Vk(function(a,b){var c=O({},Pt,a,b);if(c){var d="true"===a.getAttribute("isDefault");c.isDefault=d;return c}}),Format:Vk(U),TileMatrixSetLink:Vk(function(a,b){return O({},Qt,a,b)}),Dimension:Vk(function(a,b){return O({},Rt,a,b)}),
+ResourceURL:Vk(function(a){var b=a.getAttribute("format"),c=a.getAttribute("template");a=a.getAttribute("resourceType");var d={};b&&(d.format=b);c&&(d.template=c);a&&(d.resourceType=a);return d})},M(Lt,{Title:J(U),Abstract:J(U),WGS84BoundingBox:J(function(a,b){var c=O([],St,a,b);return 2!=c.length?void 0:Kb(c)}),Identifier:J(U)})),Pt=M(Kt,{LegendURL:Vk(function(a){var b={};b.format=a.getAttribute("format");b.href=Yr(a);return b})},M(Lt,{Title:J(U),Identifier:J(U)})),Qt=M(Kt,{TileMatrixSet:J(U)}),
+Rt=M(Kt,{Default:J(U),Value:Vk(U)},M(Lt,{Identifier:J(U)})),St=M(Lt,{LowerCorner:Tk(Jt),UpperCorner:Tk(Jt)}),Ot=M(Kt,{WellKnownScaleSet:J(U),TileMatrix:Vk(function(a,b){return O({},Tt,a,b)})},M(Lt,{SupportedCRS:J(U),Identifier:J(U)})),Tt=M(Kt,{TopLeftCorner:J(Jt),ScaleDenominator:J(bo),TileWidth:J(eo),TileHeight:J(eo),MatrixWidth:J(eo),MatrixHeight:J(eo)},M(Lt,{Identifier:J(U)}));function Ut(a){eb.call(this);a=a||{};this.a=null;this.c=Qc;this.f=void 0;B(this,gb("projection"),this.Nl,this);B(this,gb("tracking"),this.Ol,this);void 0!==a.projection&&this.dh(yc(a.projection));void 0!==a.trackingOptions&&this.si(a.trackingOptions);this.ge(void 0!==a.tracking?a.tracking:!1)}y(Ut,eb);k=Ut.prototype;k.ka=function(){this.ge(!1);eb.prototype.ka.call(this)};k.Nl=function(){var a=this.ah();a&&(this.c=Bc(yc("EPSG:4326"),a),this.a&&this.set("position",this.c(this.a)))};
+k.Ol=function(){if(kg){var a=this.bh();a&&void 0===this.f?this.f=pa.navigator.geolocation.watchPosition(this.qo.bind(this),this.ro.bind(this),this.Mg()):a||void 0===this.f||(pa.navigator.geolocation.clearWatch(this.f),this.f=void 0)}};
+k.qo=function(a){a=a.coords;this.set("accuracy",a.accuracy);this.set("altitude",null===a.altitude?void 0:a.altitude);this.set("altitudeAccuracy",null===a.altitudeAccuracy?void 0:a.altitudeAccuracy);this.set("heading",null===a.heading?void 0:wa(a.heading));this.a?(this.a[0]=a.longitude,this.a[1]=a.latitude):this.a=[a.longitude,a.latitude];var b=this.c(this.a);this.set("position",b);this.set("speed",null===a.speed?void 0:a.speed);a=Nd(Yi,this.a,a.accuracy);a.rc(this.c);this.set("accuracyGeometry",a);
+this.u()};k.ro=function(a){a.type="error";this.ge(!1);this.b(a)};k.Mj=function(){return this.get("accuracy")};k.Nj=function(){return this.get("accuracyGeometry")||null};k.Pj=function(){return this.get("altitude")};k.Qj=function(){return this.get("altitudeAccuracy")};k.Ll=function(){return this.get("heading")};k.Ml=function(){return this.get("position")};k.ah=function(){return this.get("projection")};k.wk=function(){return this.get("speed")};k.bh=function(){return this.get("tracking")};k.Mg=function(){return this.get("trackingOptions")};
+k.dh=function(a){this.set("projection",a)};k.ge=function(a){this.set("tracking",a)};k.si=function(a){this.set("trackingOptions",a)};function Vt(a,b,c){hd.call(this);this.Vf(a,b?b:0,c)}y(Vt,hd);k=Vt.prototype;k.clone=function(){var a=new Vt(null),b=this.B.slice();jd(a,this.f,b);a.u();return a};k.sb=function(a,b,c,d){var e=this.B;a-=e[0];var f=b-e[1];b=a*a+f*f;if(b<d){if(0===b)for(d=0;d<this.a;++d)c[d]=e[d];else for(d=this.wf()/Math.sqrt(b),c[0]=e[0]+d*a,c[1]=e[1]+d*f,d=2;d<this.a;++d)c[d]=e[d];c.length=this.a;return b}return d};k.Bc=function(a,b){var c=this.B,d=a-c[0],c=b-c[1];return d*d+c*c<=Wt(this)};
+k.rd=function(){return this.B.slice(0,this.a)};k.Od=function(a){var b=this.B,c=b[this.a]-b[0];return Wb(b[0]-c,b[1]-c,b[0]+c,b[1]+c,a)};k.wf=function(){return Math.sqrt(Wt(this))};function Wt(a){var b=a.B[a.a]-a.B[0];a=a.B[a.a+1]-a.B[1];return b*b+a*a}k.X=function(){return"Circle"};k.Ka=function(a){var b=this.H();return nc(a,b)?(b=this.rd(),a[0]<=b[0]&&a[2]>=b[0]||a[1]<=b[1]&&a[3]>=b[1]?!0:bc(a,this.sg,this)):!1};
+k.jm=function(a){var b=this.a,c=this.B[b]-this.B[0],d=a.slice();d[b]=d[0]+c;for(c=1;c<b;++c)d[b+c]=a[c];jd(this,this.f,d);this.u()};k.Vf=function(a,b,c){if(a){kd(this,c,a,0);this.B||(this.B=[]);c=this.B;a=sd(c,a);c[a++]=c[0]+b;var d;b=1;for(d=this.a;b<d;++b)c[a++]=c[b];c.length=a}else jd(this,"XY",null);this.u()};k.km=function(a){this.B[this.a]=this.B[0]+a;this.u()};function Xt(a,b,c){for(var d=[],e=a(0),f=a(1),g=b(e),h=b(f),l=[f,e],m=[h,g],n=[1,0],p={},q=1E5,r,u,x,v,D;0<--q&&0<n.length;)x=n.pop(),e=l.pop(),g=m.pop(),f=x.toString(),f in p||(d.push(g[0],g[1]),p[f]=!0),v=n.pop(),f=l.pop(),h=m.pop(),D=(x+v)/2,r=a(D),u=b(r),ua(u[0],u[1],g[0],g[1],h[0],h[1])<c?(d.push(h[0],h[1]),f=v.toString(),p[f]=!0):(n.push(v,D,D,x),m.push(h,u,u,g),l.push(f,r,r,e));return d}function Yt(a,b,c,d,e){var f=yc("EPSG:4326");return Xt(function(d){return[a,b+(c-b)*d]},Pc(f,d),e)}
+function Zt(a,b,c,d,e){var f=yc("EPSG:4326");return Xt(function(d){return[b+(c-b)*d,a]},Pc(f,d),e)};function $t(a){a=a||{};this.c=this.o=null;this.g=this.i=Infinity;this.f=this.l=-Infinity;this.A=this.U=Infinity;this.D=this.C=-Infinity;this.Ba=void 0!==a.targetSize?a.targetSize:100;this.R=void 0!==a.maxLines?a.maxLines:100;this.b=[];this.a=[];this.ya=void 0!==a.strokeStyle?a.strokeStyle:au;this.v=this.j=void 0;this.s=null;this.setMap(void 0!==a.map?a.map:null)}var au=new oj({color:"rgba(0,0,0,0.2)"}),bu=[90,45,30,20,10,5,2,1,.5,.2,.1,.05,.01,.005,.002,.001];
+function cu(a,b,c,d,e,f,g){var h=g;b=Yt(b,c,d,a.c,e);h=void 0!==a.b[h]?a.b[h]:new R(null);h.ba("XY",b);nc(h.H(),f)&&(a.b[g++]=h);return g}function du(a,b,c,d,e){var f=e;b=Zt(b,a.f,a.g,a.c,c);f=void 0!==a.a[f]?a.a[f]:new R(null);f.ba("XY",b);nc(f.H(),d)&&(a.a[e++]=f);return e}k=$t.prototype;k.Pl=function(){return this.o};k.ik=function(){return this.b};k.qk=function(){return this.a};
+k.Rg=function(a){var b=a.vectorContext,c=a.frameState,d=c.extent;a=c.viewState;var e=a.center,f=a.projection,g=a.resolution;a=c.pixelRatio;a=g*g/(4*a*a);if(!this.c||!Oc(this.c,f)){var h=yc("EPSG:4326"),l=f.H(),m=f.i,n=Sc(m,h,f),p=m[2],q=m[1],r=m[0],u=n[3],x=n[2],v=n[1],n=n[0];this.i=m[3];this.g=p;this.l=q;this.f=r;this.U=u;this.A=x;this.C=v;this.D=n;this.j=Pc(h,f);this.v=Pc(f,h);this.s=this.v(kc(l));this.c=f}f.a&&(f=f.H(),h=ic(f),c=c.focus[0],c<f[0]||c>f[2])&&(c=h*Math.ceil((f[0]-c)/h),d=[d[0]+c,
+d[1],d[2]+c,d[3]]);c=this.s[0];f=this.s[1];h=-1;m=Math.pow(this.Ba*g,2);p=[];q=[];g=0;for(l=bu.length;g<l;++g){r=bu[g]/2;p[0]=c-r;p[1]=f-r;q[0]=c+r;q[1]=f+r;this.j(p,p);this.j(q,q);r=Math.pow(q[0]-p[0],2)+Math.pow(q[1]-p[1],2);if(r<=m)break;h=bu[g]}g=h;if(-1==g)this.b.length=this.a.length=0;else{c=this.v(e);e=c[0];c=c[1];f=this.R;h=[Math.max(d[0],this.D),Math.max(d[1],this.C),Math.min(d[2],this.A),Math.min(d[3],this.U)];h=Sc(h,this.c,"EPSG:4326");m=h[3];q=h[1];e=Math.floor(e/g)*g;p=sa(e,this.f,this.g);
+l=cu(this,p,q,m,a,d,0);for(h=0;p!=this.f&&h++<f;)p=Math.max(p-g,this.f),l=cu(this,p,q,m,a,d,l);p=sa(e,this.f,this.g);for(h=0;p!=this.g&&h++<f;)p=Math.min(p+g,this.g),l=cu(this,p,q,m,a,d,l);this.b.length=l;c=Math.floor(c/g)*g;e=sa(c,this.l,this.i);l=du(this,e,a,d,0);for(h=0;e!=this.l&&h++<f;)e=Math.max(e-g,this.l),l=du(this,e,a,d,l);e=sa(c,this.l,this.i);for(h=0;e!=this.i&&h++<f;)e=Math.min(e+g,this.i),l=du(this,e,a,d,l);this.a.length=l}b.Sb(null,this.ya);a=0;for(e=this.b.length;a<e;++a)g=this.b[a],
+b.hd(g,null);a=0;for(e=this.a.length;a<e;++a)g=this.a[a],b.hd(g,null)};k.setMap=function(a){this.o&&(this.o.J("postcompose",this.Rg,this),this.o.render());a&&(a.I("postcompose",this.Rg,this),a.render());this.o=a};function eu(a,b,c,d,e,f,g){oh.call(this,a,b,c,0,d);this.j=e;this.g=new Image;null!==f&&(this.g.crossOrigin=f);this.i={};this.c=null;this.state=0;this.o=g}y(eu,oh);eu.prototype.a=function(a){if(void 0!==a){var b;a=w(a);if(a in this.i)return this.i[a];Ha(this.i)?b=this.g:b=this.g.cloneNode(!1);return this.i[a]=b}return this.g};eu.prototype.s=function(){this.state=3;this.c.forEach(Ka);this.c=null;ph(this)};
+eu.prototype.v=function(){void 0===this.resolution&&(this.resolution=jc(this.extent)/this.g.height);this.state=2;this.c.forEach(Ka);this.c=null;ph(this)};eu.prototype.load=function(){if(0==this.state||3==this.state)this.state=1,ph(this),this.c=[Pa(this.g,"error",this.s,this),Pa(this.g,"load",this.v,this)],this.o(this,this.j)};function fu(a,b,c,d,e){df.call(this,a,b);this.s=c;this.g=new Image;null!==d&&(this.g.crossOrigin=d);this.c={};this.j=null;this.v=e}y(fu,df);k=fu.prototype;k.ka=function(){1==this.state&&gu(this);this.a&&Ta(this.a);this.state=5;ef(this);df.prototype.ka.call(this)};k.$a=function(a){if(void 0!==a){var b=w(a);if(b in this.c)return this.c[b];a=Ha(this.c)?this.g:this.g.cloneNode(!1);return this.c[b]=a}return this.g};k.ib=function(){return this.s};k.Ql=function(){this.state=3;gu(this);ef(this)};
+k.Rl=function(){this.state=this.g.naturalWidth&&this.g.naturalHeight?2:4;gu(this);ef(this)};k.load=function(){if(0==this.state||3==this.state)this.state=1,ef(this),this.j=[Pa(this.g,"error",this.Ql,this),Pa(this.g,"load",this.Rl,this)],this.v(this,this.s)};function gu(a){a.j.forEach(Ka);a.j=null};function hu(a){a=a?a:{};Vh.call(this,{handleEvent:qc});this.c=a.formatConstructors?a.formatConstructors:[];this.j=a.projection?yc(a.projection):null;this.a=null;this.target=a.target?a.target:null}y(hu,Vh);function iu(a){a=a.dataTransfer.files;var b,c,d;b=0;for(c=a.length;b<c;++b){d=a.item(b);var e=new FileReader;e.addEventListener("load",this.o.bind(this,d));e.readAsText(d)}}function ju(a){a.stopPropagation();a.preventDefault();a.dataTransfer.dropEffect="copy"}
+hu.prototype.o=function(a,b){var c=b.target.result,d=this.v,e=this.j;e||(e=d.aa().l);var d=this.c,f=[],g,h;g=0;for(h=d.length;g<h;++g){var l=new d[g];var m={featureProjection:e};try{f=l.Fa(c,m)}catch(n){f=null}if(f&&0<f.length)break}this.b(new ku(lu,this,a,f,e))};hu.prototype.setMap=function(a){this.a&&(this.a.forEach(Ka),this.a=null);Vh.prototype.setMap.call(this,a);a&&(a=this.target?this.target:a.a,this.a=[B(a,"drop",iu,this),B(a,"dragenter",ju,this),B(a,"dragover",ju,this),B(a,"drop",ju,this)])};
+var lu="addfeatures";function ku(a,b,c,d,e){Wa.call(this,a,b);this.features=d;this.file=c;this.projection=e}y(ku,Wa);function mu(a){a=a?a:{};ji.call(this,{handleDownEvent:nu,handleDragEvent:ou,handleUpEvent:pu});this.s=a.condition?a.condition:fi;this.a=this.c=void 0;this.j=0;this.A=void 0!==a.duration?a.duration:400}y(mu,ji);
+function ou(a){if(hi(a)){var b=a.map,c=b.Za(),d=a.pixel;a=d[0]-c[0]/2;d=c[1]/2-d[1];c=Math.atan2(d,a);a=Math.sqrt(a*a+d*d);d=b.aa();b.render();if(void 0!==this.c){var e=c-this.c;Wh(b,d,d.La()-e)}this.c=c;void 0!==this.a&&(c=this.a*(d.$()/a),Yh(b,d,c));void 0!==this.a&&(this.j=this.a/a);this.a=a}}
+function pu(a){if(!hi(a))return!0;a=a.map;var b=a.aa();Xd(b,-1);var c=this.j-1,d=b.La(),d=b.constrainRotation(d,0);Wh(a,b,d,void 0,void 0);var d=b.$(),e=this.A,d=b.constrainResolution(d,0,c);Yh(a,b,d,void 0,e);this.j=0;return!1}function nu(a){return hi(a)&&this.s(a)?(Xd(a.map.aa(),1),this.a=this.c=void 0,!0):!1};function qu(a,b){Wa.call(this,a);this.feature=b}y(qu,Wa);
+function ru(a){ji.call(this,{handleDownEvent:su,handleEvent:tu,handleUpEvent:uu});this.za=null;this.S=!1;this.Hc=a.source?a.source:null;this.qb=a.features?a.features:null;this.Cj=a.snapTolerance?a.snapTolerance:12;this.Y=a.type;this.c=vu(this.Y);this.Sa=a.minPoints?a.minPoints:this.c===wu?3:2;this.Aa=a.maxPoints?a.maxPoints:Infinity;this.Ne=a.finishCondition?a.finishCondition:qc;var b=a.geometryFunction;if(!b)if("Circle"===this.Y)b=function(a,b){var c=b?b:new Vt([NaN,NaN]);c.Vf(a[0],Math.sqrt(Hb(a[0],
+a[1])));return c};else{var c,b=this.c;b===xu?c=C:b===yu?c=R:b===wu&&(c=E);b=function(a,b){var f=b;f?f.pa(a):f=new c(a);return f}}this.D=b;this.T=this.A=this.a=this.R=this.j=this.s=null;this.Fj=a.clickTolerance?a.clickTolerance*a.clickTolerance:36;this.qa=new G({source:new P({useSpatialIndex:!1,wrapX:a.wrapX?a.wrapX:!1}),style:a.style?a.style:zu()});this.Hb=a.geometryName;this.Bj=a.condition?a.condition:ei;this.ta=a.freehandCondition?a.freehandCondition:fi;B(this,gb("active"),this.yi,this)}y(ru,ji);
+function zu(){var a=wj();return function(b){return a[b.W().X()]}}k=ru.prototype;k.setMap=function(a){ji.prototype.setMap.call(this,a);this.yi()};function tu(a){this.c!==yu&&this.c!==wu||!this.ta(a)||(this.S=!0);var b=!this.S;this.S&&a.type===gh?(Au(this,a),b=!1):a.type===fh?b=Bu(this,a):a.type===$g&&(b=!1);return ki.call(this,a)&&b}function su(a){return this.Bj(a)?(this.za=a.pixel,!0):this.S?(this.za=a.pixel,this.s||Cu(this,a),!0):!1}
+function uu(a){this.S=!1;var b=this.za,c=a.pixel,d=b[0]-c[0],b=b[1]-c[1],c=!0;d*d+b*b<=this.Fj&&(Bu(this,a),this.s?this.c===Du?this.jd():Eu(this,a)?this.Ne(a)&&this.jd():Au(this,a):(Cu(this,a),this.c===xu&&this.jd()),c=!1);return c}
+function Bu(a,b){if(a.s){var c=b.coordinate,d=a.j.W(),e;a.c===xu?e=a.a:a.c===wu?(e=a.a[0],e=e[e.length-1],Eu(a,b)&&(c=a.s.slice())):(e=a.a,e=e[e.length-1]);e[0]=c[0];e[1]=c[1];a.D(a.a,d);a.R&&a.R.W().pa(c);d instanceof E&&a.c!==wu?(a.A||(a.A=new Ik(new R(null))),d=d.Hg(0),c=a.A.W(),c.ba(d.f,d.la())):a.T&&(c=a.A.W(),c.pa(a.T));Fu(a)}else c=b.coordinate.slice(),a.R?a.R.W().pa(c):(a.R=new Ik(new C(c)),Fu(a));return!0}
+function Eu(a,b){var c=!1;if(a.j){var d=!1,e=[a.s];a.c===yu?d=a.a.length>a.Sa:a.c===wu&&(d=a.a[0].length>a.Sa,e=[a.a[0][0],a.a[0][a.a[0].length-2]]);if(d)for(var d=b.map,f=0,g=e.length;f<g;f++){var h=e[f],l=d.Ga(h),m=b.pixel,c=m[0]-l[0],l=m[1]-l[1],m=a.S&&a.ta(b)?1:a.Cj;if(c=Math.sqrt(c*c+l*l)<=m){a.s=h;break}}}return c}
+function Cu(a,b){var c=b.coordinate;a.s=c;a.c===xu?a.a=c.slice():a.c===wu?(a.a=[[c.slice(),c.slice()]],a.T=a.a[0]):(a.a=[c.slice(),c.slice()],a.c===Du&&(a.T=a.a));a.T&&(a.A=new Ik(new R(a.T)));c=a.D(a.a);a.j=new Ik;a.Hb&&a.j.Ec(a.Hb);a.j.Ua(c);Fu(a);a.b(new qu("drawstart",a.j))}
+function Au(a,b){var c=b.coordinate,d=a.j.W(),e,f;if(a.c===yu)a.s=c.slice(),f=a.a,f.push(c.slice()),e=f.length>a.Aa,a.D(f,d);else if(a.c===wu){f=a.a[0];f.push(c.slice());if(e=f.length>a.Aa)a.s=f[0];a.D(a.a,d)}Fu(a);e&&a.jd()}k.Qo=function(){var a=this.j.W(),b,c;this.c===yu?(b=this.a,b.splice(-2,1),this.D(b,a)):this.c===wu&&(b=this.a[0],b.splice(-2,1),c=this.A.W(),c.pa(b),this.D(this.a,a));0===b.length&&(this.s=null);Fu(this)};
+k.jd=function(){var a=Gu(this),b=this.a,c=a.W();this.c===yu?(b.pop(),this.D(b,c)):this.c===wu&&(b[0].pop(),b[0].push(b[0][0]),this.D(b,c));"MultiPoint"===this.Y?a.Ua(new Bn([b])):"MultiLineString"===this.Y?a.Ua(new S([b])):"MultiPolygon"===this.Y&&a.Ua(new T([b]));this.b(new qu("drawend",a));this.qb&&this.qb.push(a);this.Hc&&this.Hc.rb(a)};function Gu(a){a.s=null;var b=a.j;b&&(a.j=null,a.R=null,a.A=null,a.qa.ha().clear(!0));return b}
+k.rm=function(a){var b=a.W();this.j=a;this.a=b.Z();a=this.a[this.a.length-1];this.s=a.slice();this.a.push(a.slice());Fu(this);this.b(new qu("drawstart",this.j))};k.Gc=rc;function Fu(a){var b=[];a.j&&b.push(a.j);a.A&&b.push(a.A);a.R&&b.push(a.R);a=a.qa.ha();a.clear(!0);a.Jc(b)}k.yi=function(){var a=this.v,b=this.f();a&&b||Gu(this);this.qa.setMap(b?a:null)};
+function vu(a){var b;"Point"===a||"MultiPoint"===a?b=xu:"LineString"===a||"MultiLineString"===a?b=yu:"Polygon"===a||"MultiPolygon"===a?b=wu:"Circle"===a&&(b=Du);return b}var xu="Point",yu="LineString",wu="Polygon",Du="Circle";function Hu(a,b,c){Wa.call(this,a);this.features=b;this.mapBrowserEvent=c}y(Hu,Wa);
+function Iu(a){ji.call(this,{handleDownEvent:Ju,handleDragEvent:Ku,handleEvent:Lu,handleUpEvent:Mu});this.Hb=a.condition?a.condition:ii;this.Sa=function(a){return ei(a)&&di(a)};this.qb=a.deleteCondition?a.deleteCondition:this.Sa;this.Aa=this.c=null;this.qa=[0,0];this.D=this.T=!1;this.a=new kl;this.R=void 0!==a.pixelTolerance?a.pixelTolerance:10;this.s=this.ta=!1;this.j=[];this.S=new G({source:new P({useSpatialIndex:!1,wrapX:!!a.wrapX}),style:a.style?a.style:Nu(),updateWhileAnimating:!0,updateWhileInteracting:!0});
+this.za={Point:this.ym,LineString:this.kh,LinearRing:this.kh,Polygon:this.zm,MultiPoint:this.wm,MultiLineString:this.vm,MultiPolygon:this.xm,GeometryCollection:this.um};this.A=a.features;this.A.forEach(this.xf,this);B(this.A,"add",this.sm,this);B(this.A,"remove",this.tm,this);this.Y=null}y(Iu,ji);k=Iu.prototype;k.xf=function(a){var b=a.W();b.X()in this.za&&this.za[b.X()].call(this,a,b);(b=this.v)&&Ou(this,this.qa,b);B(a,"change",this.jh,this)};
+function Pu(a,b){a.D||(a.D=!0,a.b(new Hu("modifystart",a.A,b)))}function Qu(a,b){Ru(a,b);a.c&&0===a.A.dc()&&(a.S.ha().nb(a.c),a.c=null);Qa(b,"change",a.jh,a)}function Ru(a,b){var c=a.a,d=[];c.forEach(function(a){b===a.feature&&d.push(a)});for(var e=d.length-1;0<=e;--e)c.remove(d[e])}k.setMap=function(a){this.S.setMap(a);ji.prototype.setMap.call(this,a)};k.sm=function(a){this.xf(a.element)};k.jh=function(a){this.s||(a=a.target,Qu(this,a),this.xf(a))};k.tm=function(a){Qu(this,a.element)};
+k.ym=function(a,b){var c=b.Z(),c={feature:a,geometry:b,na:[c,c]};this.a.Ca(b.H(),c)};k.wm=function(a,b){var c=b.Z(),d,e,f;e=0;for(f=c.length;e<f;++e)d=c[e],d={feature:a,geometry:b,depth:[e],index:e,na:[d,d]},this.a.Ca(b.H(),d)};k.kh=function(a,b){var c=b.Z(),d,e,f,g;d=0;for(e=c.length-1;d<e;++d)f=c.slice(d,d+2),g={feature:a,geometry:b,index:d,na:f},this.a.Ca(Kb(f),g)};
+k.vm=function(a,b){var c=b.Z(),d,e,f,g,h,l,m;g=0;for(h=c.length;g<h;++g)for(d=c[g],e=0,f=d.length-1;e<f;++e)l=d.slice(e,e+2),m={feature:a,geometry:b,depth:[g],index:e,na:l},this.a.Ca(Kb(l),m)};k.zm=function(a,b){var c=b.Z(),d,e,f,g,h,l,m;g=0;for(h=c.length;g<h;++g)for(d=c[g],e=0,f=d.length-1;e<f;++e)l=d.slice(e,e+2),m={feature:a,geometry:b,depth:[g],index:e,na:l},this.a.Ca(Kb(l),m)};
+k.xm=function(a,b){var c=b.Z(),d,e,f,g,h,l,m,n,p,q;l=0;for(m=c.length;l<m;++l)for(n=c[l],g=0,h=n.length;g<h;++g)for(d=n[g],e=0,f=d.length-1;e<f;++e)p=d.slice(e,e+2),q={feature:a,geometry:b,depth:[g,l],index:e,na:p},this.a.Ca(Kb(p),q)};k.um=function(a,b){var c,d=b.c;for(c=0;c<d.length;++c)this.za[d[c].X()].call(this,a,d[c])};function Su(a,b){var c=a.c;c?c.W().pa(b):(c=new Ik(new C(b)),a.c=c,a.S.ha().rb(c))}function Tu(a,b){return a.index-b.index}
+function Ju(a){if(!this.Hb(a))return!1;Ou(this,a.pixel,a.map);this.j.length=0;this.D=!1;var b=this.c;if(b){var c=[],b=b.W().Z(),d=Kb([b]),d=nl(this.a,d),e={};d.sort(Tu);for(var f=0,g=d.length;f<g;++f){var h=d[f],l=h.na,m=w(h.feature),n=h.depth;n&&(m+="-"+n.join("-"));e[m]||(e[m]=Array(2));if(Fb(l[0],b)&&!e[m][0])this.j.push([h,0]),e[m][0]=h;else if(Fb(l[1],b)&&!e[m][1]){if("LineString"!==h.geometry.X()&&"MultiLineString"!==h.geometry.X()||!e[m][0]||0!==e[m][0].index)this.j.push([h,1]),e[m][1]=h}else w(l)in
+this.Aa&&!e[m][0]&&!e[m][1]&&c.push([h,b])}c.length&&Pu(this,a);for(a=c.length-1;0<=a;--a)this.nl.apply(this,c[a])}return!!this.c}
+function Ku(a){this.T=!1;Pu(this,a);a=a.coordinate;for(var b=0,c=this.j.length;b<c;++b){for(var d=this.j[b],e=d[0],f=e.depth,g=e.geometry,h=g.Z(),l=e.na,d=d[1];a.length<g.va();)a.push(0);switch(g.X()){case "Point":h=a;l[0]=l[1]=a;break;case "MultiPoint":h[e.index]=a;l[0]=l[1]=a;break;case "LineString":h[e.index+d]=a;l[d]=a;break;case "MultiLineString":h[f[0]][e.index+d]=a;l[d]=a;break;case "Polygon":h[f[0]][e.index+d]=a;l[d]=a;break;case "MultiPolygon":h[f[1]][f[0]][e.index+d]=a,l[d]=a}e=g;this.s=
+!0;e.pa(h);this.s=!1}Su(this,a)}function Mu(a){for(var b,c=this.j.length-1;0<=c;--c)b=this.j[c][0],ll(this.a,Kb(b.na),b);this.D&&(this.b(new Hu("modifyend",this.A,a)),this.D=!1);return!1}function Lu(a){if(!(a instanceof Wg))return!0;this.Y=a;var b;Sd(a.map.aa())[1]||a.type!=fh||this.C||(this.qa=a.pixel,Ou(this,a.pixel,a.map));this.c&&this.qb(a)&&(a.type==ah&&this.T?b=!0:(this.c.W(),b=this.ai()));a.type==ah&&(this.T=!1);return ki.call(this,a)&&!b}
+function Ou(a,b,c){function d(a,b){return Ib(e,a.na)-Ib(e,b.na)}var e=c.Ma(b),f=c.Ma([b[0]-a.R,b[1]+a.R]),g=c.Ma([b[0]+a.R,b[1]-a.R]),f=Kb([f,g]),f=nl(a.a,f);if(0<f.length){f.sort(d);var g=f[0].na,h=Cb(e,g),l=c.Ga(h);if(Math.sqrt(Hb(b,l))<=a.R){b=c.Ga(g[0]);c=c.Ga(g[1]);b=Hb(l,b);c=Hb(l,c);a.ta=Math.sqrt(Math.min(b,c))<=a.R;a.ta&&(h=b>c?g[1]:g[0]);Su(a,h);c={};c[w(g)]=!0;b=1;for(l=f.length;b<l;++b)if(h=f[b].na,Fb(g[0],h[0])&&Fb(g[1],h[1])||Fb(g[0],h[1])&&Fb(g[1],h[0]))c[w(h)]=!0;else break;a.Aa=c;
+return}}a.c&&(a.S.ha().nb(a.c),a.c=null)}
+k.nl=function(a,b){for(var c=a.na,d=a.feature,e=a.geometry,f=a.depth,g=a.index,h;b.length<e.va();)b.push(0);switch(e.X()){case "MultiLineString":h=e.Z();h[f[0]].splice(g+1,0,b);break;case "Polygon":h=e.Z();h[f[0]].splice(g+1,0,b);break;case "MultiPolygon":h=e.Z();h[f[1]][f[0]].splice(g+1,0,b);break;case "LineString":h=e.Z();h.splice(g+1,0,b);break;default:return}this.s=!0;e.pa(h);this.s=!1;h=this.a;h.remove(a);Uu(this,e,g,f,1);var l={na:[c[0],b],feature:d,geometry:e,depth:f,index:g};h.Ca(Kb(l.na),
+l);this.j.push([l,1]);c={na:[b,c[1]],feature:d,geometry:e,depth:f,index:g+1};h.Ca(Kb(c.na),c);this.j.push([c,0]);this.T=!0};
+k.ai=function(){var a=!1;if(this.Y&&this.Y.type!=gh){var b=this.Y;Pu(this,b);var c=this.j,a={},d,e,f,g,h,l,m,n,p;for(g=c.length-1;0<=g;--g)f=c[g],m=f[0],n=w(m.feature),m.depth&&(n+="-"+m.depth.join("-")),n in a||(a[n]={}),0===f[1]?(a[n].right=m,a[n].index=m.index):1==f[1]&&(a[n].left=m,a[n].index=m.index+1);for(n in a){l=a[n].right;g=a[n].left;f=a[n].index;h=f-1;m=void 0!==g?g:l;0>h&&(h=0);c=m.geometry;d=e=c.Z();p=!1;switch(c.X()){case "MultiLineString":2<e[m.depth[0]].length&&(e[m.depth[0]].splice(f,
+1),p=!0);break;case "LineString":2<e.length&&(e.splice(f,1),p=!0);break;case "MultiPolygon":d=d[m.depth[1]];case "Polygon":d=d[m.depth[0]],4<d.length&&(f==d.length-1&&(f=0),d.splice(f,1),p=!0,0===f&&(d.pop(),d.push(d[0]),h=d.length-1))}p&&(d=c,this.s=!0,d.pa(e),this.s=!1,e=[],void 0!==g&&(this.a.remove(g),e.push(g.na[0])),void 0!==l&&(this.a.remove(l),e.push(l.na[1])),void 0!==g&&void 0!==l&&(g={depth:m.depth,feature:m.feature,geometry:m.geometry,index:h,na:e},this.a.Ca(Kb(g.na),g)),Uu(this,c,f,m.depth,
+-1),this.c&&(this.S.ha().nb(this.c),this.c=null))}a=!0;this.b(new Hu("modifyend",this.A,b));this.D=!1}return a};function Uu(a,b,c,d,e){ql(a.a,b.H(),function(a){a.geometry===b&&(void 0===d||void 0===a.depth||pb(a.depth,d))&&a.index>c&&(a.index+=e)})}function Nu(){var a=wj();return function(){return a.Point}};function Vu(a,b,c,d){Wa.call(this,a);this.selected=b;this.deselected=c;this.mapBrowserEvent=d}y(Vu,Wa);
+function Wu(a){Vh.call(this,{handleEvent:Xu});var b=a?a:{};this.C=b.condition?b.condition:di;this.A=b.addCondition?b.addCondition:rc;this.D=b.removeCondition?b.removeCondition:rc;this.R=b.toggleCondition?b.toggleCondition:fi;this.j=b.multi?b.multi:!1;this.o=b.filter?b.filter:qc;this.c=new G({source:new P({useSpatialIndex:!1,features:b.features,wrapX:b.wrapX}),style:b.style?b.style:Yu(),updateWhileAnimating:!0,updateWhileInteracting:!0});if(b.layers)if("function"===typeof b.layers)a=function(a){return b.layers(a)};
+else{var c=b.layers;a=function(a){return jb(c,a)}}else a=qc;this.s=a;this.a={};a=this.c.ha().c;B(a,"add",this.Am,this);B(a,"remove",this.Dm,this)}y(Wu,Vh);k=Wu.prototype;k.Bm=function(){return this.c.ha().c};k.Cm=function(a){a=w(a);return this.a[a]};
+function Xu(a){if(!this.C(a))return!0;var b=this.A(a),c=this.D(a),d=this.R(a),e=!b&&!c&&!d,f=a.map,g=this.c.ha().c,h=[],l=[];if(e)Fa(this.a),f.kd(a.pixel,function(a,b){if(this.o(a,b)){l.push(a);var c=w(a);this.a[c]=b;return!this.j}},this,this.s),0<l.length&&1==g.dc()&&g.item(0)==l[0]?l.length=0:(0!==g.dc()&&(h=Array.prototype.concat(g.a),g.clear()),g.qf(l));else{f.kd(a.pixel,function(a,e){if(this.o(a,e)){if(!b&&!d||jb(g.a,a))(c||d)&&jb(g.a,a)&&(h.push(a),f=w(a),delete this.a[f]);else{l.push(a);var f=
+w(a);this.a[f]=e}return!this.j}},this,this.s);for(e=h.length-1;0<=e;--e)g.remove(h[e]);g.qf(l)}(0<l.length||0<h.length)&&this.b(new Vu("select",l,h,a));return ci(a)}k.setMap=function(a){var b=this.v,c=this.c.ha().c;b&&c.forEach(b.wi,b);Vh.prototype.setMap.call(this,a);this.c.setMap(a);a&&c.forEach(a.ti,a)};function Yu(){var a=wj();mb(a.Polygon,a.LineString);mb(a.GeometryCollection,a.LineString);return function(b){return a[b.W().X()]}}k.Am=function(a){a=a.element;var b=this.v;b&&b.ti(a)};
+k.Dm=function(a){a=a.element;var b=this.v;b&&b.wi(a)};function Zu(a){ji.call(this,{handleEvent:$u,handleDownEvent:qc,handleUpEvent:av});a=a?a:{};this.s=a.source?a.source:null;this.qa=void 0!==a.vertex?a.vertex:!0;this.T=void 0!==a.edge?a.edge:!0;this.j=a.features?a.features:null;this.ta=[];this.D={};this.R={};this.Y={};this.A={};this.S=null;this.c=void 0!==a.pixelTolerance?a.pixelTolerance:10;this.Aa=bv.bind(this);this.a=new kl;this.za={Point:this.Jm,LineString:this.nh,LinearRing:this.nh,Polygon:this.Km,MultiPoint:this.Hm,MultiLineString:this.Gm,MultiPolygon:this.Im,
+GeometryCollection:this.Fm}}y(Zu,ji);k=Zu.prototype;k.rb=function(a,b){var c=void 0!==b?b:!0,d=w(a),e=a.W();if(e){var f=this.za[e.X()];f&&(this.Y[d]=e.H(Lb()),f.call(this,a,e),c&&(this.R[d]=B(e,"change",this.Kk.bind(this,a),this)))}c&&(this.D[d]=B(a,gb(a.a),this.Em,this))};k.Jj=function(a){this.rb(a)};k.Kj=function(a){this.nb(a)};k.lh=function(a){var b;a instanceof vl?b=a.feature:a instanceof ke&&(b=a.element);this.rb(b)};
+k.mh=function(a){var b;a instanceof vl?b=a.feature:a instanceof ke&&(b=a.element);this.nb(b)};k.Em=function(a){a=a.target;this.nb(a,!0);this.rb(a,!0)};k.Kk=function(a){if(this.C){var b=w(a);b in this.A||(this.A[b]=a)}else this.xi(a)};k.nb=function(a,b){var c=void 0!==b?b:!0,d=w(a),e=this.Y[d];if(e){var f=this.a,g=[];ql(f,e,function(b){a===b.feature&&g.push(b)});for(e=g.length-1;0<=e;--e)f.remove(g[e]);c&&(cb(this.R[d]),delete this.R[d])}c&&(cb(this.D[d]),delete this.D[d])};
+k.setMap=function(a){var b=this.v,c=this.ta,d;this.j?d=this.j:this.s&&(d=this.s.oe());b&&(c.forEach(cb),c.length=0,d.forEach(this.Kj,this));ji.prototype.setMap.call(this,a);a&&(this.j?c.push(B(this.j,"add",this.lh,this),B(this.j,"remove",this.mh,this)):this.s&&c.push(B(this.s,"addfeature",this.lh,this),B(this.s,"removefeature",this.mh,this)),d.forEach(this.Jj,this))};k.Gc=rc;k.xi=function(a){this.nb(a,!1);this.rb(a,!1)};
+k.Fm=function(a,b){var c,d=b.c;for(c=0;c<d.length;++c)this.za[d[c].X()].call(this,a,d[c])};k.nh=function(a,b){var c=b.Z(),d,e,f,g;d=0;for(e=c.length-1;d<e;++d)f=c.slice(d,d+2),g={feature:a,na:f},this.a.Ca(Kb(f),g)};k.Gm=function(a,b){var c=b.Z(),d,e,f,g,h,l,m;g=0;for(h=c.length;g<h;++g)for(d=c[g],e=0,f=d.length-1;e<f;++e)l=d.slice(e,e+2),m={feature:a,na:l},this.a.Ca(Kb(l),m)};k.Hm=function(a,b){var c=b.Z(),d,e,f;e=0;for(f=c.length;e<f;++e)d=c[e],d={feature:a,na:[d,d]},this.a.Ca(b.H(),d)};
+k.Im=function(a,b){var c=b.Z(),d,e,f,g,h,l,m,n,p,q;l=0;for(m=c.length;l<m;++l)for(n=c[l],g=0,h=n.length;g<h;++g)for(d=n[g],e=0,f=d.length-1;e<f;++e)p=d.slice(e,e+2),q={feature:a,na:p},this.a.Ca(Kb(p),q)};k.Jm=function(a,b){var c=b.Z(),c={feature:a,na:[c,c]};this.a.Ca(b.H(),c)};k.Km=function(a,b){var c=b.Z(),d,e,f,g,h,l,m;g=0;for(h=c.length;g<h;++g)for(d=c[g],e=0,f=d.length-1;e<f;++e)l=d.slice(e,e+2),m={feature:a,na:l},this.a.Ca(Kb(l),m)};
+function $u(a){var b,c,d=a.pixel,e=a.coordinate;b=a.map;var f=b.Ma([d[0]-this.c,d[1]+this.c]);c=b.Ma([d[0]+this.c,d[1]-this.c]);var f=Kb([f,c]),g=nl(this.a,f),h,f=!1,l=null;c=null;if(0<g.length){this.S=e;g.sort(this.Aa);g=g[0].na;if(this.qa&&!this.T){if(e=b.Ga(g[0]),h=b.Ga(g[1]),e=Hb(d,e),d=Hb(d,h),h=Math.sqrt(Math.min(e,d)),h=h<=this.c)f=!0,l=e>d?g[1]:g[0],c=b.Ga(l)}else this.T&&(l=Cb(e,g),c=b.Ga(l),Math.sqrt(Hb(d,c))<=this.c&&(f=!0,this.qa&&(e=b.Ga(g[0]),h=b.Ga(g[1]),e=Hb(c,e),d=Hb(c,h),h=Math.sqrt(Math.min(e,
+d)),h=h<=this.c)))&&(l=e>d?g[1]:g[0],c=b.Ga(l));f&&(c=[Math.round(c[0]),Math.round(c[1])])}b=l;f&&(a.coordinate=b.slice(0,2),a.pixel=c);return ki.call(this,a)}function av(){var a=Ga(this.A);a.length&&(a.forEach(this.xi,this),this.A={});return!1}function bv(a,b){return Ib(this.S,a.na)-Ib(this.S,b.na)};function cv(a,b,c){Wa.call(this,a);this.features=b;this.coordinate=c}y(cv,Wa);function dv(a){ji.call(this,{handleDownEvent:ev,handleDragEvent:fv,handleMoveEvent:gv,handleUpEvent:hv});this.s=void 0;this.a=null;this.c=void 0!==a.features?a.features:null;var b;if(a.layers)if("function"===typeof a.layers)b=function(b){return a.layers(b)};else{var c=a.layers;b=function(a){return jb(c,a)}}else b=qc;this.A=b;this.j=null}y(dv,ji);
+function ev(a){this.j=iv(this,a.pixel,a.map);return!this.a&&this.j?(this.a=a.coordinate,gv.call(this,a),this.b(new cv("translatestart",this.c,a.coordinate)),!0):!1}function hv(a){return this.a?(this.a=null,gv.call(this,a),this.b(new cv("translateend",this.c,a.coordinate)),!0):!1}
+function fv(a){if(this.a){a=a.coordinate;var b=a[0]-this.a[0],c=a[1]-this.a[1];if(this.c)this.c.forEach(function(a){var d=a.W();d.Sc(b,c);a.Ua(d)});else if(this.j){var d=this.j.W();d.Sc(b,c);this.j.Ua(d)}this.a=a;this.b(new cv("translating",this.c,a))}}
+function gv(a){var b=a.map.yc();if(a=a.map.kd(a.pixel,function(a){return a})){var c=!1;this.c&&jb(this.c.a,a)&&(c=!0);this.s=b.style.cursor;b.style.cursor=this.a?"-webkit-grabbing":c?"-webkit-grab":"pointer";b.style.cursor=this.a?c?"grab":"pointer":"grabbing"}else b.style.cursor=void 0!==this.s?this.s:"",this.s=void 0}function iv(a,b,c){var d=null;b=c.kd(b,function(a){return a},a,a.A);a.c&&jb(a.c.a,b)&&(d=b);return d};function V(a){a=a?a:{};var b=Ea({},a);delete b.gradient;delete b.radius;delete b.blur;delete b.shadow;delete b.weight;G.call(this,b);this.f=null;this.ia=void 0!==a.shadow?a.shadow:250;this.Y=void 0;this.c=null;B(this,gb("gradient"),this.Lk,this);this.ii(a.gradient?a.gradient:jv);this.di(void 0!==a.blur?a.blur:15);this.qh(void 0!==a.radius?a.radius:8);B(this,gb("blur"),this.lf,this);B(this,gb("radius"),this.lf,this);this.lf();var c=a.weight?a.weight:"weight",d;"string"===typeof c?d=function(a){return a.get(c)}:
+d=c;this.l(function(a){a=d(a);a=void 0!==a?sa(a,0,1):1;var b=255*a|0,c=this.c[b];c||(c=[new rj({image:new Dh({opacity:a,src:this.Y})})],this.c[b]=c);return c}.bind(this));this.set("renderOrder",null);B(this,"render",this.dl,this)}y(V,G);var jv=["#00f","#0ff","#0f0","#ff0","#f00"];k=V.prototype;k.zg=function(){return this.get("blur")};k.Gg=function(){return this.get("gradient")};k.ph=function(){return this.get("radius")};
+k.Lk=function(){for(var a=this.Gg(),b=Oe(1,256),c=b.createLinearGradient(0,0,1,256),d=1/(a.length-1),e=0,f=a.length;e<f;++e)c.addColorStop(e*d,a[e]);b.fillStyle=c;b.fillRect(0,0,1,256);this.f=b.getImageData(0,0,1,256).data};k.lf=function(){var a=this.ph(),b=this.zg(),c=a+b+1,d=2*c,d=Oe(d,d);d.shadowOffsetX=d.shadowOffsetY=this.ia;d.shadowBlur=b;d.shadowColor="#000";d.beginPath();b=c-this.ia;d.arc(b,b,a,0,2*Math.PI,!0);d.fill();this.Y=d.canvas.toDataURL();this.c=Array(256);this.u()};
+k.dl=function(a){a=a.context;var b=a.canvas,b=a.getImageData(0,0,b.width,b.height),c=b.data,d,e,f;d=0;for(e=c.length;d<e;d+=4)if(f=4*c[d+3])c[d]=this.f[f],c[d+1]=this.f[f+1],c[d+2]=this.f[f+2];a.putImageData(b,0,0)};k.di=function(a){this.set("blur",a)};k.ii=function(a){this.set("gradient",a)};k.qh=function(a){this.set("radius",a)};function kv(a,b,c,d){function e(){delete pa[g];f.parentNode.removeChild(f)}var f=pa.document.createElement("script"),g="olc_"+w(b);f.async=!0;f.src=a+(-1==a.indexOf("?")?"?":"&")+(d||"callback")+"="+g;var h=pa.setTimeout(function(){e();c&&c()},1E4);pa[g]=function(a){pa.clearTimeout(h);e();b(a)};pa.document.getElementsByTagName("head")[0].appendChild(f)};function lv(a,b,c,d,e,f,g,h,l,m,n){df.call(this,e,0);this.R=void 0!==n?n:!1;this.D=g;this.C=h;this.l=null;this.c={};this.j=b;this.v=d;this.U=f?f:e;this.g=[];this.Wc=null;this.s=0;f=d.Ea(this.U);h=this.v.H();e=this.j.H();f=h?mc(f,h):f;if(0===gc(f))this.state=4;else if((h=a.H())&&(e?e=mc(e,h):e=h),d=d.$(this.U[0]),d=tk(a,c,kc(f),d),!isFinite(d)||0>=d)this.state=4;else if(this.A=new wk(a,c,f,e,d*(void 0!==m?m:.5)),0===this.A.f.length)this.state=4;else if(this.s=b.Lb(d),c=yk(this.A),e&&(a.a?(c[1]=sa(c[1],
+e[1],e[3]),c[3]=sa(c[3],e[1],e[3])):c=mc(c,e)),gc(c))if(a=pf(b,c,this.s),100>(a.ea-a.ca+1)*(a.ga-a.fa+1)){for(b=a.ca;b<=a.ea;b++)for(c=a.fa;c<=a.ga;c++)(m=l(this.s,b,c,g))&&this.g.push(m);0===this.g.length&&(this.state=4)}else this.state=3;else this.state=4}y(lv,df);lv.prototype.ka=function(){1==this.state&&(this.Wc.forEach(Ka),this.Wc=null);df.prototype.ka.call(this)};
+lv.prototype.$a=function(a){if(void 0!==a){var b=w(a);if(b in this.c)return this.c[b];a=Ha(this.c)?this.l:this.l.cloneNode(!1);return this.c[b]=a}return this.l};
+lv.prototype.zd=function(){var a=[];this.g.forEach(function(b){b&&2==b.V()&&a.push({extent:this.j.Ea(b.ma),image:b.$a()})},this);this.g.length=0;if(0===a.length)this.state=3;else{var b=this.U[0],c=this.v.Ja(b),d=ea(c)?c:c[0],c=ea(c)?c:c[1],b=this.v.$(b),e=this.j.$(this.s),f=this.v.Ea(this.U);this.l=vk(d,c,this.D,e,this.j.H(),b,f,this.A,a,this.C,this.R);this.state=2}ef(this)};
+lv.prototype.load=function(){if(0==this.state){this.state=1;ef(this);var a=0;this.Wc=[];this.g.forEach(function(b){var c=b.V();if(0==c||1==c){a++;var d;d=B(b,"change",function(){var c=b.V();if(2==c||3==c||4==c)Ka(d),a--,0===a&&(this.Wc.forEach(Ka),this.Wc=null,this.zd())},this);this.Wc.push(d)}},this);this.g.forEach(function(a){0==a.V()&&a.load()});0===a&&pa.setTimeout(this.zd.bind(this),0)}};function W(a){Jl.call(this,{attributions:a.attributions,cacheSize:a.cacheSize,extent:a.extent,logo:a.logo,opaque:a.opaque,projection:a.projection,state:a.state,tileGrid:a.tileGrid,tileLoadFunction:a.tileLoadFunction?a.tileLoadFunction:mv,tilePixelRatio:a.tilePixelRatio,tileUrlFunction:a.tileUrlFunction,url:a.url,urls:a.urls,wrapX:a.wrapX});this.crossOrigin=void 0!==a.crossOrigin?a.crossOrigin:null;this.tileClass=void 0!==a.tileClass?a.tileClass:fu;this.i={};this.s={};this.qa=a.reprojectionErrorThreshold;
+this.C=!1}y(W,Jl);k=W.prototype;k.Ah=function(){if(cf(this.a))return!0;for(var a in this.i)if(cf(this.i[a]))return!0;return!1};k.Lc=function(a,b){var c=this.pd(a);this.a.Lc(this.a==c?b:{});for(var d in this.i){var e=this.i[d];e.Lc(e==c?b:{})}};k.Ud=function(a){return this.f&&a&&!Oc(this.f,a)?0:this.gf()};k.gf=function(){return 0};k.jf=function(a){return this.f&&a&&!Oc(this.f,a)?!1:Jl.prototype.jf.call(this,a)};
+k.eb=function(a){var b=this.f;return!this.tileGrid||b&&!Oc(b,a)?(b=w(a).toString(),b in this.s||(this.s[b]=vf(a)),this.s[b]):this.tileGrid};k.pd=function(a){var b=this.f;if(!b||Oc(b,a))return this.a;a=w(a).toString();a in this.i||(this.i[a]=new bf);return this.i[a]};function nv(a,b,c,d,e,f,g){b=[b,c,d];e=(c=Cf(a,b,f))?a.tileUrlFunction(c,e,f):void 0;e=new a.tileClass(b,void 0!==e?0:4,void 0!==e?e:"",a.crossOrigin,a.tileLoadFunction);e.key=g;B(e,"change",a.Bh,a);return e}
+k.ac=function(a,b,c,d,e){if(this.f&&e&&!Oc(this.f,e)){var f=this.pd(e);b=[a,b,c];a=this.Eb.apply(this,b);if(Ze(f,a))return f.get(a);var g=this.f;c=this.eb(g);var h=this.eb(e),l=Cf(this,b,e);d=new lv(g,c,e,h,b,l,this.bc(d),this.gf(),function(a,b,c,d){return ov(this,a,b,c,d,g)}.bind(this),this.qa,this.C);f.set(a,d);return d}return ov(this,a,b,c,d,e)};
+function ov(a,b,c,d,e,f){var g,h=a.Eb(b,c,d),l=a.cc;if(Ze(a.a,h)){if(g=a.a.get(h),g.key!=l){var m=g;g.a&&g.a.key==l?(g=g.a,2==m.V()&&(g.a=m)):(g=nv(a,b,c,d,e,f,l),2==m.V()?g.a=m:m.a&&2==m.a.V()&&(g.a=m.a,m.a=null));g.a&&(g.a.a=null);a.a.replace(h,g)}}else g=nv(a,b,c,d,e,f,l),a.a.set(h,g);return g}k.zb=function(a){if(this.C!=a){this.C=a;for(var b in this.i)this.i[b].clear();this.u()}};k.Ab=function(a,b){var c=yc(a);c&&(c=w(c).toString(),c in this.s||(this.s[c]=b))};function mv(a,b){a.$a().src=b};function pv(a){W.call(this,{cacheSize:a.cacheSize,crossOrigin:"anonymous",opaque:!0,projection:yc("EPSG:3857"),reprojectionErrorThreshold:a.reprojectionErrorThreshold,state:"loading",tileLoadFunction:a.tileLoadFunction,wrapX:void 0!==a.wrapX?a.wrapX:!0});this.j=void 0!==a.culture?a.culture:"en-us";this.c=void 0!==a.maxZoom?a.maxZoom:-1;kv("https://dev.virtualearth.net/REST/v1/Imagery/Metadata/"+a.imagerySet+"?uriScheme=https&include=ImageryProviders&key="+a.key,this.v.bind(this),void 0,"jsonp")}
+y(pv,W);var qv=new je({html:'<a class="ol-attribution-bing-tos" href="http://www.microsoft.com/maps/product/terms.html">Terms of Use</a>'});
+pv.prototype.v=function(a){if(200!=a.statusCode||"OK"!=a.statusDescription||"ValidCredentials"!=a.authenticationResultCode||1!=a.resourceSets.length||1!=a.resourceSets[0].resources.length)lf(this,"error");else{var b=a.brandLogoUri;-1==b.indexOf("https")&&(b=b.replace("http","https"));var c=a.resourceSets[0].resources[0],d=-1==this.c?c.zoomMax:this.c;a=wf(this.f);var e=yf({extent:a,minZoom:c.zoomMin,maxZoom:d,tileSize:c.imageWidth==c.imageHeight?c.imageWidth:[c.imageWidth,c.imageHeight]});this.tileGrid=
+e;var f=this.j;this.tileUrlFunction=Gl(c.imageUrlSubdomains.map(function(a){var b=[0,0,0],d=c.imageUrl.replace("{subdomain}",a).replace("{culture}",f);return function(a){if(a)return $e(a[0],a[1],-a[2]-1,b),d.replace("{quadkey}",af(b))}}));if(c.imageryProviders){var g=Bc(yc("EPSG:4326"),this.f);a=c.imageryProviders.map(function(a){var b=a.attribution,c={};a.coverageAreas.forEach(function(a){var b=a.zoomMin,f=Math.min(a.zoomMax,d);a=a.bbox;a=pc([a[1],a[0],a[3],a[2]],g);var h,l;for(h=b;h<=f;++h)l=h.toString(),
+b=pf(e,a,h),l in c?c[l].push(b):c[l]=[b]});return new je({html:b,tileRanges:c})});a.push(qv);this.oa(a)}this.R=b;lf(this,"ready")}};function rv(a){a=a||{};var b=void 0!==a.projection?a.projection:"EPSG:3857",c=void 0!==a.tileGrid?a.tileGrid:yf({extent:wf(b),maxZoom:a.maxZoom,minZoom:a.minZoom,tileSize:a.tileSize});W.call(this,{attributions:a.attributions,cacheSize:a.cacheSize,crossOrigin:a.crossOrigin,logo:a.logo,opaque:a.opaque,projection:b,reprojectionErrorThreshold:a.reprojectionErrorThreshold,tileGrid:c,tileLoadFunction:a.tileLoadFunction,tilePixelRatio:a.tilePixelRatio,tileUrlFunction:a.tileUrlFunction,url:a.url,urls:a.urls,
+wrapX:void 0!==a.wrapX?a.wrapX:!0})}y(rv,W);function sv(a){this.v=a.account;this.A=a.map||"";this.c=a.config||{};this.j={};rv.call(this,{attributions:a.attributions,cacheSize:a.cacheSize,crossOrigin:a.crossOrigin,logo:a.logo,maxZoom:void 0!==a.maxZoom?a.maxZoom:18,minZoom:a.minZoom,projection:a.projection,state:"loading",wrapX:a.wrapX});tv(this)}y(sv,rv);k=sv.prototype;k.Tj=function(){return this.c};k.up=function(a){Ea(this.c,a);tv(this)};k.$o=function(a){this.c=a||{};tv(this)};
+function tv(a){var b=JSON.stringify(a.c);if(a.j[b])uv(a,a.j[b]);else{var c="https://"+a.v+".cartodb.com/api/v1/map";a.A&&(c+="/named/"+a.A);var d=new XMLHttpRequest;d.addEventListener("load",a.Nk.bind(a,b));d.addEventListener("error",a.Mk.bind(a));d.open("POST",c);d.setRequestHeader("Content-type","application/json");d.send(JSON.stringify(a.c))}}
+k.Nk=function(a,b){var c=b.target;if(200<=c.status&&300>c.status){var d;try{d=JSON.parse(c.responseText)}catch(e){lf(this,"error");return}uv(this,d);this.j[a]=d;lf(this,"ready")}else lf(this,"error")};k.Mk=function(){lf(this,"error")};function uv(a,b){a.Va("https://"+b.cdn_url.https+"/"+a.v+"/api/v1/map/"+b.layergroupid+"/{z}/{x}/{y}.png")};function Y(a){P.call(this,{attributions:a.attributions,extent:a.extent,logo:a.logo,projection:a.projection,wrapX:a.wrapX});this.C=void 0;this.ta=void 0!==a.distance?a.distance:20;this.A=[];this.ia=a.geometryFunction||function(a){return a.W()};this.v=a.source;this.v.I("change",Y.prototype.Sa,this)}y(Y,P);Y.prototype.Aa=function(){return this.v};Y.prototype.Pc=function(a,b,c){this.v.Pc(a,b,c);b!==this.C&&(this.clear(),this.C=b,vv(this),this.Jc(this.A))};
+Y.prototype.Sa=function(){this.clear();vv(this);this.Jc(this.A);this.u()};function vv(a){if(void 0!==a.C){a.A.length=0;for(var b=Lb(),c=a.ta*a.C,d=a.v.oe(),e={},f=0,g=d.length;f<g;f++){var h=d[f];w(h).toString()in e||!(h=a.ia(h))||(h=h.Z(),Xb(h,b),Ob(b,c,b),h=a.v.ef(b),h=h.filter(function(a){a=w(a).toString();return a in e?!1:e[a]=!0}),a.A.push(wv(a,h)))}}}
+function wv(a,b){for(var c=[0,0],d=b.length-1;0<=d;--d){var e=a.ia(b[d]);e?Bb(c,e.Z()):b.splice(d,1)}d=1/b.length;c[0]*=d;c[1]*=d;c=new Ik(new C(c));c.set("features",b);return c};function xv(a,b){var c=Object.keys(b).map(function(a){return a+"="+encodeURIComponent(b[a])}).join("&");a=a.replace(/[?&]$/,"");a=-1===a.indexOf("?")?a+"?":a+"&";return a+c};function yv(a){a=a||{};Ak.call(this,{attributions:a.attributions,logo:a.logo,projection:a.projection,resolutions:a.resolutions});this.Y=void 0!==a.crossOrigin?a.crossOrigin:null;this.i=a.url;this.j=void 0!==a.imageLoadFunction?a.imageLoadFunction:Gk;this.v=a.params||{};this.c=null;this.s=[0,0];this.T=0;this.S=void 0!==a.ratio?a.ratio:1.5}y(yv,Ak);k=yv.prototype;k.Sm=function(){return this.v};
+k.Mc=function(a,b,c,d){if(void 0===this.i)return null;b=Bk(this,b);var e=this.c;if(e&&this.T==this.g&&e.$()==b&&e.f==c&&Ub(e.H(),a))return e;e={F:"image",FORMAT:"PNG32",TRANSPARENT:!0};Ea(e,this.v);a=a.slice();var f=(a[0]+a[2])/2,g=(a[1]+a[3])/2;if(1!=this.S){var h=this.S*ic(a)/2,l=this.S*jc(a)/2;a[0]=f-h;a[1]=g-l;a[2]=f+h;a[3]=g+l}var h=b/c,l=Math.ceil(ic(a)/h),m=Math.ceil(jc(a)/h);a[0]=f-h*l/2;a[2]=f+h*l/2;a[1]=g-h*m/2;a[3]=g+h*m/2;this.s[0]=l;this.s[1]=m;f=a;g=this.s;d=d.cb.split(":").pop();e.SIZE=
+g[0]+","+g[1];e.BBOX=f.join(",");e.BBOXSR=d;e.IMAGESR=d;e.DPI=90*c;d=this.i.replace(/MapServer\/?$/,"MapServer/export").replace(/ImageServer\/?$/,"ImageServer/exportImage");e=xv(d,e);this.c=new eu(a,b,c,this.l,e,this.Y,this.j);this.T=this.g;B(this.c,"change",this.o,this);return this.c};k.Rm=function(){return this.j};k.Tm=function(){return this.i};k.Um=function(a){this.c=null;this.j=a;this.u()};k.Vm=function(a){a!=this.i&&(this.i=a,this.c=null,this.u())};k.Wm=function(a){Ea(this.v,a);this.c=null;this.u()};function zv(a){Ak.call(this,{projection:a.projection,resolutions:a.resolutions});this.Y=void 0!==a.crossOrigin?a.crossOrigin:null;this.s=void 0!==a.displayDpi?a.displayDpi:96;this.j=a.params||{};this.T=a.url;this.c=void 0!==a.imageLoadFunction?a.imageLoadFunction:Gk;this.ia=void 0!==a.hidpi?a.hidpi:!0;this.ta=void 0!==a.metersPerUnit?a.metersPerUnit:1;this.v=void 0!==a.ratio?a.ratio:1;this.Aa=void 0!==a.useOverlay?a.useOverlay:!1;this.i=null;this.S=0}y(zv,Ak);k=zv.prototype;k.Ym=function(){return this.j};
+k.Mc=function(a,b,c){b=Bk(this,b);c=this.ia?c:1;var d=this.i;if(d&&this.S==this.g&&d.$()==b&&d.f==c&&Ub(d.H(),a))return d;1!=this.v&&(a=a.slice(),oc(a,this.v));var e=[ic(a)/b*c,jc(a)/b*c];if(void 0!==this.T){var d=this.T,f=kc(a),g=this.ta,h=ic(a),l=jc(a),m=e[0],n=e[1],p=.0254/this.s,e={OPERATION:this.Aa?"GETDYNAMICMAPOVERLAYIMAGE":"GETMAPIMAGE",VERSION:"2.0.0",LOCALE:"en",CLIENTAGENT:"ol.source.ImageMapGuide source",CLIP:"1",SETDISPLAYDPI:this.s,SETDISPLAYWIDTH:Math.round(e[0]),SETDISPLAYHEIGHT:Math.round(e[1]),
+SETVIEWSCALE:n*h>m*l?h*g/(m*p):l*g/(n*p),SETVIEWCENTERX:f[0],SETVIEWCENTERY:f[1]};Ea(e,this.j);d=xv(d,e);d=new eu(a,b,c,this.l,d,this.Y,this.c);B(d,"change",this.o,this)}else d=null;this.i=d;this.S=this.g;return d};k.Xm=function(){return this.c};k.$m=function(a){Ea(this.j,a);this.u()};k.Zm=function(a){this.i=null;this.c=a;this.u()};function Av(a){var b=a.imageExtent,c=void 0!==a.crossOrigin?a.crossOrigin:null,d=void 0!==a.imageLoadFunction?a.imageLoadFunction:Gk;Ak.call(this,{attributions:a.attributions,logo:a.logo,projection:yc(a.projection)});this.c=new eu(b,void 0,1,this.l,a.url,c,d);this.i=a.imageSize?a.imageSize:null;B(this.c,"change",this.o,this)}y(Av,Ak);Av.prototype.Mc=function(a){return nc(a,this.c.H())?this.c:null};
+Av.prototype.o=function(a){if(2==this.c.V()){var b=this.c.H(),c=this.c.a(),d,e;this.i?(d=this.i[0],e=this.i[1]):(d=c.width,e=c.height);b=Math.ceil(ic(b)/(jc(b)/e));if(b!=d){var b=Oe(b,e),f=b.canvas;b.drawImage(c,0,0,d,e,0,0,f.width,f.height);this.c.g=f}}Ak.prototype.o.call(this,a)};function Bv(a){a=a||{};Ak.call(this,{attributions:a.attributions,logo:a.logo,projection:a.projection,resolutions:a.resolutions});this.ta=void 0!==a.crossOrigin?a.crossOrigin:null;this.j=a.url;this.S=void 0!==a.imageLoadFunction?a.imageLoadFunction:Gk;this.i=a.params||{};this.v=!0;Cv(this);this.ia=a.serverType;this.Aa=void 0!==a.hidpi?a.hidpi:!0;this.c=null;this.T=[0,0];this.Y=0;this.s=void 0!==a.ratio?a.ratio:1.5}y(Bv,Ak);var Dv=[101,101];k=Bv.prototype;
+k.fn=function(a,b,c,d){if(void 0!==this.j){var e=lc(a,b,0,Dv),f={SERVICE:"WMS",VERSION:"1.3.0",REQUEST:"GetFeatureInfo",FORMAT:"image/png",TRANSPARENT:!0,QUERY_LAYERS:this.i.LAYERS};Ea(f,this.i,d);d=Math.floor((e[3]-a[1])/b);f[this.v?"I":"X"]=Math.floor((a[0]-e[0])/b);f[this.v?"J":"Y"]=d;return Ev(this,e,Dv,1,yc(c),f)}};k.hn=function(){return this.i};
+k.Mc=function(a,b,c,d){if(void 0===this.j)return null;b=Bk(this,b);1==c||this.Aa&&void 0!==this.ia||(c=1);a=a.slice();var e=(a[0]+a[2])/2,f=(a[1]+a[3])/2,g=b/c,h=ic(a)/g,g=jc(a)/g,l=this.c;if(l&&this.Y==this.g&&l.$()==b&&l.f==c&&Ub(l.H(),a))return l;if(1!=this.s){var l=this.s*ic(a)/2,m=this.s*jc(a)/2;a[0]=e-l;a[1]=f-m;a[2]=e+l;a[3]=f+m}e={SERVICE:"WMS",VERSION:"1.3.0",REQUEST:"GetMap",FORMAT:"image/png",TRANSPARENT:!0};Ea(e,this.i);this.T[0]=Math.ceil(h*this.s);this.T[1]=Math.ceil(g*this.s);d=Ev(this,
+a,this.T,c,d,e);this.c=new eu(a,b,c,this.l,d,this.ta,this.S);this.Y=this.g;B(this.c,"change",this.o,this);return this.c};k.gn=function(){return this.S};
+function Ev(a,b,c,d,e,f){f[a.v?"CRS":"SRS"]=e.cb;"STYLES"in a.i||(f.STYLES="");if(1!=d)switch(a.ia){case "geoserver":d=90*d+.5|0;f.FORMAT_OPTIONS="FORMAT_OPTIONS"in f?f.FORMAT_OPTIONS+(";dpi:"+d):"dpi:"+d;break;case "mapserver":f.MAP_RESOLUTION=90*d;break;case "carmentaserver":case "qgis":f.DPI=90*d}f.WIDTH=c[0];f.HEIGHT=c[1];c=e.b;var g;a.v&&"ne"==c.substr(0,2)?g=[b[1],b[0],b[3],b[2]]:g=b;f.BBOX=g.join(",");return xv(a.j,f)}k.jn=function(){return this.j};k.kn=function(a){this.c=null;this.S=a;this.u()};
+k.ln=function(a){a!=this.j&&(this.j=a,this.c=null,this.u())};k.mn=function(a){Ea(this.i,a);Cv(this);this.c=null;this.u()};function Cv(a){a.v=0<=Ab(a.i.VERSION||"1.3.0")};function Fv(a){a=a||{};var b;void 0!==a.attributions?b=a.attributions:b=[Gv];rv.call(this,{attributions:b,cacheSize:a.cacheSize,crossOrigin:void 0!==a.crossOrigin?a.crossOrigin:"anonymous",opaque:void 0!==a.opaque?a.opaque:!0,maxZoom:void 0!==a.maxZoom?a.maxZoom:19,reprojectionErrorThreshold:a.reprojectionErrorThreshold,tileLoadFunction:a.tileLoadFunction,url:void 0!==a.url?a.url:"https://{a-c}.tile.openstreetmap.org/{z}/{x}/{y}.png",wrapX:a.wrapX})}y(Fv,rv);var Gv=new je({html:'&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors.'});(function(){var a={},b={ja:a};(function(c){if("object"===typeof a&&"undefined"!==typeof b)b.ja=c();else{var d;"undefined"!==typeof window?d=window:"undefined"!==typeof global?d=global:"undefined"!==typeof self?d=self:d=this;d.Sp=c()}})(function(){return function d(a,b,g){function h(m,p){if(!b[m]){if(!a[m]){var q="function"==typeof require&&require;if(!p&&q)return q(m,!0);if(l)return l(m,!0);q=Error("Cannot find module '"+m+"'");throw q.code="MODULE_NOT_FOUND",q;}q=b[m]={ja:{}};a[m][0].call(q.ja,function(b){var d=
+a[m][1][b];return h(d?d:b)},q,q.ja,d,a,b,g)}return b[m].ja}for(var l="function"==typeof require&&require,m=0;m<g.length;m++)h(g[m]);return h}({1:[function(a,b,f){a=a("./processor");f.$i=a},{"./processor":2}],2:[function(a,b){function f(a){var b=!0;try{new ImageData(10,10)}catch(d){b=!1}return function(d){var e=d.buffers,f=d.meta,g=d.width,h=d.height,l=e.length,m=e[0].byteLength;if(d.imageOps){m=Array(l);for(d=0;d<l;++d){var z=m,F=d,N;N=new Uint8ClampedArray(e[d]);var K=g,X=h;N=b?new ImageData(N,K,
+X):{data:N,width:K,height:X};z[F]=N}g=a(m,f).data}else{g=new Uint8ClampedArray(m);h=Array(l);z=Array(l);for(d=0;d<l;++d)h[d]=new Uint8ClampedArray(e[d]),z[d]=[0,0,0,0];for(e=0;e<m;e+=4){for(d=0;d<l;++d)F=h[d],z[d][0]=F[e],z[d][1]=F[e+1],z[d][2]=F[e+2],z[d][3]=F[e+3];d=a(z,f);g[e]=d[0];g[e+1]=d[1];g[e+2]=d[2];g[e+3]=d[3]}}return g.buffer}}function g(a,b){var d=Object.keys(a.lib||{}).map(function(b){return"var "+b+" = "+a.lib[b].toString()+";"}).concat(["var __minion__ = ("+f.toString()+")(",a.operation.toString(),
+");",'self.addEventListener("message", function(event) {',"  var buffer = __minion__(event.data);","  self.postMessage({buffer: buffer, meta: event.data.meta}, [buffer]);","});"]),d=URL.createObjectURL(new Blob(d,{type:"text/javascript"})),d=new Worker(d);d.addEventListener("message",b);return d}function h(a,b){var d=f(a.operation);return{postMessage:function(a){setTimeout(function(){b({data:{buffer:d(a),meta:a.meta}})},0)}}}function l(a){this.Re=!!a.ll;var b;0===a.threads?b=0:this.Re?b=1:b=a.threads||
+1;var d=[];if(b)for(var e=0;e<b;++e)d[e]=g(a,this.ig.bind(this,e));else d[0]=h(a,this.ig.bind(this,0));this.Ld=d;this.$c=[];this.oj=a.uo||Infinity;this.Jd=0;this.Ic={};this.Se=null}var m=a("./util").Fl;l.prototype.so=function(a,b,d){this.lj({Ac:a,Xg:b,qg:d});this.fg()};l.prototype.lj=function(a){for(this.$c.push(a);this.$c.length>this.oj;)this.$c.shift().qg(null,null)};l.prototype.fg=function(){if(0===this.Jd&&0<this.$c.length){var a=this.Se=this.$c.shift(),b=a.Ac[0].width,d=a.Ac[0].height,e=a.Ac.map(function(a){return a.data.buffer}),
+f=this.Ld.length;this.Jd=f;if(1===f)this.Ld[0].postMessage({buffers:e,meta:a.Xg,imageOps:this.Re,width:b,height:d},e);else for(var g=4*Math.ceil(a.Ac[0].data.length/4/f),h=0;h<f;++h){for(var l=h*g,m=[],z=0,F=e.length;z<F;++z)m.push(e[h].slice(l,l+g));this.Ld[h].postMessage({buffers:m,meta:a.Xg,imageOps:this.Re,width:b,height:d},m)}}};l.prototype.ig=function(a,b){this.Pp||(this.Ic[a]=b.data,--this.Jd,0===this.Jd&&this.pj())};l.prototype.pj=function(){var a=this.Se,b=this.Ld.length,d,e;if(1===b)d=new Uint8ClampedArray(this.Ic[0].buffer),
+e=this.Ic[0].meta;else{var f=a.Ac[0].data.length;d=new Uint8ClampedArray(f);e=Array(f);for(var f=4*Math.ceil(f/4/b),g=0;g<b;++g){var h=g*f;d.set(new Uint8ClampedArray(this.Ic[g].buffer),h);e[g]=this.Ic[g].meta}}this.Se=null;this.Ic={};a.qg(null,m(d,a.Ac[0].width,a.Ac[0].height),e);this.fg()};b.ja=l},{"./util":3}],3:[function(a,b,f){var g=!0;try{new ImageData(10,10)}catch(l){g=!1}var h=document.createElement("canvas").getContext("2d");f.Fl=function(a,b,d){if(g)return new ImageData(a,b,d);b=h.createImageData(b,
+d);b.data.set(a);return b}},{}]},{},[1])(1)});jl=b.ja})();function Hv(a){this.S=null;this.Aa=void 0!==a.operationType?a.operationType:"pixel";this.Sa=void 0!==a.threads?a.threads:1;this.c=Iv(a.sources);for(var b=0,c=this.c.length;b<c;++b)B(this.c[b],"change",this.u,this);this.i=Oe();this.ia=new Rh(function(){return 1},this.u.bind(this));for(var b=Jv(this.c),c={},d=0,e=b.length;d<e;++d)c[w(b[d].layer)]=b[d];this.j=this.s=null;this.Y={animate:!1,attributions:{},coordinateToPixelMatrix:Xc(),extent:null,focus:null,index:0,layerStates:c,layerStatesArray:b,logos:{},
+pixelRatio:1,pixelToCoordinateMatrix:Xc(),postRenderFunctions:[],size:[0,0],skippedFeatureUids:{},tileQueue:this.ia,time:Date.now(),usedTiles:{},viewState:{rotation:0},viewHints:[],wantedTiles:{}};Ak.call(this,{});void 0!==a.operation&&this.v(a.operation,a.lib)}y(Hv,Ak);Hv.prototype.v=function(a,b){this.S=new jl.$i({operation:a,ll:"image"===this.Aa,uo:1,lib:b,threads:this.Sa});this.u()};function Kv(a,b,c){var d=a.s;return!d||a.g!==d.Xo||c!==d.resolution||!$b(b,d.extent)}
+Hv.prototype.A=function(a,b,c,d){c=!0;for(var e,f=0,g=this.c.length;f<g;++f)if(e=this.c[f].a.ha(),"ready"!==e.V()){c=!1;break}if(!c)return null;a=a.slice();if(!Kv(this,a,b))return this.j;c=this.i.canvas;e=Math.round(ic(a)/b);f=Math.round(jc(a)/b);if(e!==c.width||f!==c.height)c.width=e,c.height=f;e=Ea({},this.Y);e.viewState=Ea({},e.viewState);var f=kc(a),g=Math.round(ic(a)/b),h=Math.round(jc(a)/b);e.extent=a;e.focus=kc(a);e.size[0]=g;e.size[1]=h;g=e.viewState;g.center=f;g.projection=d;g.resolution=
+b;this.j=d=new nk(a,b,1,this.l,c,this.T.bind(this,e));this.s={extent:a,resolution:b,Xo:this.g};return d};
+Hv.prototype.T=function(a,b){for(var c=this.c.length,d=Array(c),e=0;e<c;++e){var f;f=this.c[e];var g=a,h=a.layerStatesArray[e];if(f.l(g,h)){var l=g.size[0],m=g.size[1];if(Lv){var n=Lv.canvas;n.width!==l||n.height!==m?Lv=Oe(l,m):Lv.clearRect(0,0,l,m)}else Lv=Oe(l,m);f.i(g,h,Lv);f=Lv.getImageData(0,0,l,m)}else f=null;if(f)d[e]=f;else return}c={};this.b(new Mv(Nv,a,c));this.S.so(d,c,this.ta.bind(this,a,b));Sh(a.tileQueue,16,16)};
+Hv.prototype.ta=function(a,b,c,d,e){c?b(c):d&&(this.b(new Mv(Ov,a,e)),Kv(this,a.extent,a.viewState.resolution/a.pixelRatio)||this.i.putImageData(d,0,0),b(null))};var Lv=null;function Jv(a){return a.map(function(a){return jh(a.a)})}function Iv(a){for(var b=a.length,c=Array(b),d=0;d<b;++d){var e=d,f=a[d],g=null;f instanceof zf?(f=new dj({source:f}),g=new Bl(f)):f instanceof Ak&&(f=new cj({source:f}),g=new Al(f));c[e]=g}return c}
+function Mv(a,b,c){Wa.call(this,a);this.extent=b.extent;this.resolution=b.viewState.resolution/b.pixelRatio;this.data=c}y(Mv,Wa);var Nv="beforeoperations",Ov="afteroperations";var Pv={terrain:{tb:"jpg",opaque:!0},"terrain-background":{tb:"jpg",opaque:!0},"terrain-labels":{tb:"png",opaque:!1},"terrain-lines":{tb:"png",opaque:!1},"toner-background":{tb:"png",opaque:!0},toner:{tb:"png",opaque:!0},"toner-hybrid":{tb:"png",opaque:!1},"toner-labels":{tb:"png",opaque:!1},"toner-lines":{tb:"png",opaque:!1},"toner-lite":{tb:"png",opaque:!0},watercolor:{tb:"jpg",opaque:!0}},Qv={terrain:{minZoom:4,maxZoom:18},toner:{minZoom:0,maxZoom:20},watercolor:{minZoom:1,maxZoom:16}};
+function Rv(a){var b=a.layer.indexOf("-"),b=-1==b?a.layer:a.layer.slice(0,b),b=Qv[b],c=Pv[a.layer];rv.call(this,{attributions:Sv,cacheSize:a.cacheSize,crossOrigin:"anonymous",maxZoom:void 0!=a.maxZoom?a.maxZoom:b.maxZoom,minZoom:void 0!=a.minZoom?a.minZoom:b.minZoom,opaque:c.opaque,reprojectionErrorThreshold:a.reprojectionErrorThreshold,tileLoadFunction:a.tileLoadFunction,url:void 0!==a.url?a.url:"https://stamen-tiles-{a-d}.a.ssl.fastly.net/"+a.layer+"/{z}/{x}/{y}."+c.tb})}y(Rv,rv);
+var Sv=[new je({html:'Map tiles by <a href="http://stamen.com/">Stamen Design</a>, under <a href="http://creativecommons.org/licenses/by/3.0/">CC BY 3.0</a>.'}),Gv];function Tv(a){a=a||{};W.call(this,{attributions:a.attributions,cacheSize:a.cacheSize,crossOrigin:a.crossOrigin,logo:a.logo,projection:a.projection,reprojectionErrorThreshold:a.reprojectionErrorThreshold,tileGrid:a.tileGrid,tileLoadFunction:a.tileLoadFunction,url:a.url,urls:a.urls,wrapX:void 0!==a.wrapX?a.wrapX:!0});this.c=a.params||{};this.j=Lb()}y(Tv,W);Tv.prototype.v=function(){return this.c};Tv.prototype.bc=function(a){return a};
+Tv.prototype.vc=function(a,b,c){var d=this.tileGrid;d||(d=this.eb(c));if(!(d.b.length<=a[0])){var e=d.Ea(a,this.j),f=hf(d.Ja(a[0]),this.o);1!=b&&(f=gf(f,b,this.o));d={F:"image",FORMAT:"PNG32",TRANSPARENT:!0};Ea(d,this.c);var g=this.urls;g?(c=c.cb.split(":").pop(),d.SIZE=f[0]+","+f[1],d.BBOX=e.join(","),d.BBOXSR=c,d.IMAGESR=c,d.DPI=Math.round(d.DPI?d.DPI*b:90*b),a=(1==g.length?g[0]:g[xa((a[1]<<a[0])+a[2],g.length)]).replace(/MapServer\/?$/,"MapServer/export").replace(/ImageServer\/?$/,"ImageServer/exportImage"),
+a=xv(a,d)):a=void 0;return a}};Tv.prototype.A=function(a){Ea(this.c,a);this.u()};function Uv(a,b,c){df.call(this,a,2);this.l=b;this.c=c;this.g={}}y(Uv,df);Uv.prototype.$a=function(a){a=void 0!==a?w(a):-1;if(a in this.g)return this.g[a];var b=this.l,c=Oe(b[0],b[1]);c.strokeStyle="black";c.strokeRect(.5,.5,b[0]+.5,b[1]+.5);c.fillStyle="black";c.textAlign="center";c.textBaseline="middle";c.font="24px sans-serif";c.fillText(this.c,b[0]/2,b[1]/2);return this.g[a]=c.canvas};
+function Vv(a){zf.call(this,{opaque:!1,projection:a.projection,tileGrid:a.tileGrid,wrapX:void 0!==a.wrapX?a.wrapX:!0})}y(Vv,zf);Vv.prototype.ac=function(a,b,c){var d=this.Eb(a,b,c);if(Ze(this.a,d))return this.a.get(d);var e=hf(this.tileGrid.Ja(a));a=[a,b,c];b=(b=Cf(this,a))?Cf(this,b).toString():"";e=new Uv(a,e,b);this.a.set(d,e);return e};function Wv(a){this.c=null;W.call(this,{attributions:a.attributions,cacheSize:a.cacheSize,crossOrigin:a.crossOrigin,projection:yc("EPSG:3857"),reprojectionErrorThreshold:a.reprojectionErrorThreshold,state:"loading",tileLoadFunction:a.tileLoadFunction,wrapX:void 0!==a.wrapX?a.wrapX:!0});if(a.jsonp)kv(a.url,this.yh.bind(this),this.me.bind(this));else{var b=new XMLHttpRequest;b.addEventListener("load",this.pn.bind(this));b.addEventListener("error",this.nn.bind(this));b.open("GET",a.url);b.send()}}
+y(Wv,W);k=Wv.prototype;k.pn=function(a){a=a.target;if(200<=a.status&&300>a.status){var b;try{b=JSON.parse(a.responseText)}catch(c){this.me();return}this.yh(b)}else this.me()};k.nn=function(){this.me()};k.Ak=function(){return this.c};
+k.yh=function(a){var b=yc("EPSG:4326"),c=this.f,d;void 0!==a.bounds&&(d=pc(a.bounds,Bc(b,c)));var e=a.minzoom||0,f=a.maxzoom||22;this.tileGrid=c=yf({extent:wf(c),maxZoom:f,minZoom:e});this.tileUrlFunction=Fl(a.tiles,c);if(void 0!==a.attribution&&!this.l){b=void 0!==d?d:b.H();d={};for(var g;e<=f;++e)g=e.toString(),d[g]=[pf(c,b,e)];this.oa([new je({html:a.attribution,tileRanges:d})])}this.c=a;lf(this,"ready")};k.me=function(){lf(this,"error")};function Xv(a){zf.call(this,{projection:yc("EPSG:3857"),state:"loading"});this.s=void 0!==a.preemptive?a.preemptive:!0;this.j=Hl;this.i=void 0;this.c=a.jsonp||!1;if(a.url)if(this.c)kv(a.url,this.Bf.bind(this),this.ne.bind(this));else{var b=new XMLHttpRequest;b.addEventListener("load",this.tn.bind(this));b.addEventListener("error",this.sn.bind(this));b.open("GET",a.url);b.send()}else a.tileJSON&&this.Bf(a.tileJSON)}y(Xv,zf);k=Xv.prototype;
+k.tn=function(a){a=a.target;if(200<=a.status&&300>a.status){var b;try{b=JSON.parse(a.responseText)}catch(c){this.ne();return}this.Bf(b)}else this.ne()};k.sn=function(){this.ne()};k.xk=function(){return this.i};k.Ij=function(a,b,c,d,e){this.tileGrid?(b=this.tileGrid.Zd(a,b),Yv(this.ac(b[0],b[1],b[2],1,this.f),a,c,d,e)):!0===e?Tf(function(){c.call(d,null)}):c.call(d,null)};k.ne=function(){lf(this,"error")};
+k.Bf=function(a){var b=yc("EPSG:4326"),c=this.f,d;void 0!==a.bounds&&(d=pc(a.bounds,Bc(b,c)));var e=a.minzoom||0,f=a.maxzoom||22;this.tileGrid=c=yf({extent:wf(c),maxZoom:f,minZoom:e});this.i=a.template;var g=a.grids;if(g){this.j=Fl(g,c);if(void 0!==a.attribution){b=void 0!==d?d:b.H();for(d={};e<=f;++e)g=e.toString(),d[g]=[pf(c,b,e)];this.oa([new je({html:a.attribution,tileRanges:d})])}lf(this,"ready")}else lf(this,"error")};
+k.ac=function(a,b,c,d,e){var f=this.Eb(a,b,c);if(Ze(this.a,f))return this.a.get(f);a=[a,b,c];b=Cf(this,a,e);d=this.j(b,d,e);d=new Zv(a,void 0!==d?0:4,void 0!==d?d:"",this.tileGrid.Ea(a),this.s,this.c);this.a.set(f,d);return d};k.Yf=function(a,b,c){a=this.Eb(a,b,c);Ze(this.a,a)&&this.a.get(a)};function Zv(a,b,c,d,e,f){df.call(this,a,b);this.s=c;this.g=d;this.U=e;this.c=this.j=this.l=null;this.v=f}y(Zv,df);k=Zv.prototype;k.$a=function(){return null};
+k.getData=function(a){if(!this.l||!this.j)return null;var b=this.l[Math.floor((1-(a[1]-this.g[1])/(this.g[3]-this.g[1]))*this.l.length)];if("string"!==typeof b)return null;b=b.charCodeAt(Math.floor((a[0]-this.g[0])/(this.g[2]-this.g[0])*b.length));93<=b&&b--;35<=b&&b--;b-=32;a=null;b in this.j&&(b=this.j[b],this.c&&b in this.c?a=this.c[b]:a=b);return a};
+function Yv(a,b,c,d,e){0==a.state&&!0===e?(Pa(a,"change",function(){c.call(d,this.getData(b))},a),$v(a)):!0===e?Tf(function(){c.call(d,this.getData(b))},a):c.call(d,a.getData(b))}k.ib=function(){return this.s};k.ae=function(){this.state=3;ef(this)};k.zh=function(a){this.l=a.grid;this.j=a.keys;this.c=a.data;this.state=4;ef(this)};
+function $v(a){if(0==a.state)if(a.state=1,a.v)kv(a.s,a.zh.bind(a),a.ae.bind(a));else{var b=new XMLHttpRequest;b.addEventListener("load",a.rn.bind(a));b.addEventListener("error",a.qn.bind(a));b.open("GET",a.s);b.send()}}k.rn=function(a){a=a.target;if(200<=a.status&&300>a.status){var b;try{b=JSON.parse(a.responseText)}catch(c){this.ae();return}this.zh(b)}else this.ae()};k.qn=function(){this.ae()};k.load=function(){this.U&&$v(this)};function aw(a){a=a||{};var b=a.params||{};W.call(this,{attributions:a.attributions,cacheSize:a.cacheSize,crossOrigin:a.crossOrigin,logo:a.logo,opaque:!("TRANSPARENT"in b?b.TRANSPARENT:1),projection:a.projection,reprojectionErrorThreshold:a.reprojectionErrorThreshold,tileGrid:a.tileGrid,tileLoadFunction:a.tileLoadFunction,url:a.url,urls:a.urls,wrapX:void 0!==a.wrapX?a.wrapX:!0});this.v=void 0!==a.gutter?a.gutter:0;this.c=b;this.j=!0;this.A=a.serverType;this.T=void 0!==a.hidpi?a.hidpi:!0;this.S="";
+bw(this);this.Y=Lb();cw(this);Bf(this,dw(this))}y(aw,W);k=aw.prototype;
+k.vn=function(a,b,c,d){c=yc(c);var e=this.tileGrid;e||(e=this.eb(c));b=e.Zd(a,b);if(!(e.b.length<=b[0])){var f=e.$(b[0]),g=e.Ea(b,this.Y),e=hf(e.Ja(b[0]),this.o),h=this.v;0!==h&&(e=ff(e,h,this.o),g=Ob(g,f*h,g));h={SERVICE:"WMS",VERSION:"1.3.0",REQUEST:"GetFeatureInfo",FORMAT:"image/png",TRANSPARENT:!0,QUERY_LAYERS:this.c.LAYERS};Ea(h,this.c,d);d=Math.floor((g[3]-a[1])/f);h[this.j?"I":"X"]=Math.floor((a[0]-g[0])/f);h[this.j?"J":"Y"]=d;return ew(this,b,e,g,1,c,h)}};k.gf=function(){return this.v};
+k.Eb=function(a,b,c){return this.S+W.prototype.Eb.call(this,a,b,c)};k.wn=function(){return this.c};
+function ew(a,b,c,d,e,f,g){var h=a.urls;if(h){g.WIDTH=c[0];g.HEIGHT=c[1];g[a.j?"CRS":"SRS"]=f.cb;"STYLES"in a.c||(g.STYLES="");if(1!=e)switch(a.A){case "geoserver":c=90*e+.5|0;g.FORMAT_OPTIONS="FORMAT_OPTIONS"in g?g.FORMAT_OPTIONS+(";dpi:"+c):"dpi:"+c;break;case "mapserver":g.MAP_RESOLUTION=90*e;break;case "carmentaserver":case "qgis":g.DPI=90*e}f=f.b;a.j&&"ne"==f.substr(0,2)&&(a=d[0],d[0]=d[1],d[1]=a,a=d[2],d[2]=d[3],d[3]=a);g.BBOX=d.join(",");return xv(1==h.length?h[0]:h[xa((b[1]<<b[0])+b[2],h.length)],
+g)}}k.bc=function(a){return this.T&&void 0!==this.A?a:1};function bw(a){var b=0,c=[];if(a.urls){var d,e;d=0;for(e=a.urls.length;d<e;++d)c[b++]=a.urls[d]}a.S=c.join("#")}function dw(a){var b=0,c=[],d;for(d in a.c)c[b++]=d+"-"+a.c[d];return c.join("/")}
+k.vc=function(a,b,c){var d=this.tileGrid;d||(d=this.eb(c));if(!(d.b.length<=a[0])){1==b||this.T&&void 0!==this.A||(b=1);var e=d.$(a[0]),f=d.Ea(a,this.Y),d=hf(d.Ja(a[0]),this.o),g=this.v;0!==g&&(d=ff(d,g,this.o),f=Ob(f,e*g,f));1!=b&&(d=gf(d,b,this.o));e={SERVICE:"WMS",VERSION:"1.3.0",REQUEST:"GetMap",FORMAT:"image/png",TRANSPARENT:!0};Ea(e,this.c);return ew(this,a,d,f,b,c,e)}};k.xn=function(a){Ea(this.c,a);bw(this);cw(this);Bf(this,dw(this))};function cw(a){a.j=0<=Ab(a.c.VERSION||"1.3.0")};function fw(a){this.l=a.matrixIds;mf.call(this,{extent:a.extent,origin:a.origin,origins:a.origins,resolutions:a.resolutions,tileSize:a.tileSize,tileSizes:a.tileSizes,sizes:a.sizes})}y(fw,mf);fw.prototype.j=function(){return this.l};
+function gw(a,b){var c=[],d=[],e=[],f=[],g=[],h;h=yc(a.SupportedCRS.replace(/urn:ogc:def:crs:(\w+):(.*:)?(\w+)$/,"$1:$3"));var l=h.$b(),m="ne"==h.b.substr(0,2);a.TileMatrix.sort(function(a,b){return b.ScaleDenominator-a.ScaleDenominator});a.TileMatrix.forEach(function(a){d.push(a.Identifier);var b=2.8E-4*a.ScaleDenominator/l,h=a.TileWidth,r=a.TileHeight;m?e.push([a.TopLeftCorner[1],a.TopLeftCorner[0]]):e.push(a.TopLeftCorner);c.push(b);f.push(h==r?h:[h,r]);g.push([a.MatrixWidth,-a.MatrixHeight])});
+return new fw({extent:b,origins:e,resolutions:c,matrixIds:d,tileSizes:f,sizes:g})};function Z(a){function b(a){a="KVP"==d?xv(a,f):a.replace(/\{(\w+?)\}/g,function(a,b){return b.toLowerCase()in f?f[b.toLowerCase()]:a});return function(b){if(b){var c={TileMatrix:e.l[b[0]],TileCol:b[1],TileRow:-b[2]-1};Ea(c,g);b=a;return b="KVP"==d?xv(b,c):b.replace(/\{(\w+?)\}/g,function(a,b){return c[b]})}}}this.T=void 0!==a.version?a.version:"1.0.0";this.v=void 0!==a.format?a.format:"image/jpeg";this.c=void 0!==a.dimensions?a.dimensions:{};this.A=a.layer;this.j=a.matrixSet;this.S=a.style;var c=
+a.urls;void 0===c&&void 0!==a.url&&(c=Il(a.url));var d=this.Y=void 0!==a.requestEncoding?a.requestEncoding:"KVP",e=a.tileGrid,f={layer:this.A,style:this.S,tilematrixset:this.j};"KVP"==d&&Ea(f,{Service:"WMTS",Request:"GetTile",Version:this.T,Format:this.v});var g=this.c,h=c&&0<c.length?Gl(c.map(b)):Hl;W.call(this,{attributions:a.attributions,cacheSize:a.cacheSize,crossOrigin:a.crossOrigin,logo:a.logo,projection:a.projection,reprojectionErrorThreshold:a.reprojectionErrorThreshold,tileClass:a.tileClass,
+tileGrid:e,tileLoadFunction:a.tileLoadFunction,tilePixelRatio:a.tilePixelRatio,tileUrlFunction:h,urls:c,wrapX:void 0!==a.wrapX?a.wrapX:!1});Bf(this,hw(this))}y(Z,W);k=Z.prototype;k.Vj=function(){return this.c};k.yn=function(){return this.v};k.zn=function(){return this.A};k.hk=function(){return this.j};k.vk=function(){return this.Y};k.An=function(){return this.S};k.Ck=function(){return this.T};function hw(a){var b=0,c=[],d;for(d in a.c)c[b++]=d+"-"+a.c[d];return c.join("/")}
+k.vp=function(a){Ea(this.c,a);Bf(this,hw(this))};function iw(a){a=a||{};var b=a.size,c=b[0],d=b[1],e=[],f=256;switch(void 0!==a.tierSizeCalculation?a.tierSizeCalculation:"default"){case "default":for(;c>f||d>f;)e.push([Math.ceil(c/f),Math.ceil(d/f)]),f+=f;break;case "truncated":for(;c>f||d>f;)e.push([Math.ceil(c/f),Math.ceil(d/f)]),c>>=1,d>>=1}e.push([1,1]);e.reverse();for(var f=[1],g=[0],d=1,c=e.length;d<c;d++)f.push(1<<d),g.push(e[d-1][0]*e[d-1][1]+g[d-1]);f.reverse();var b=[0,-b[1],b[0],0],b=new mf({extent:b,origin:fc(b),resolutions:f}),h=a.url;
+W.call(this,{attributions:a.attributions,cacheSize:a.cacheSize,crossOrigin:a.crossOrigin,logo:a.logo,reprojectionErrorThreshold:a.reprojectionErrorThreshold,tileClass:jw,tileGrid:b,tileUrlFunction:function(a){if(a){var b=a[0],c=a[1];a=-a[2]-1;return h+"TileGroup"+((c+a*e[b][0]+g[b])/256|0)+"/"+b+"-"+c+"-"+a+".jpg"}}})}y(iw,W);function jw(a,b,c,d,e){fu.call(this,a,b,c,d,e);this.l={}}y(jw,fu);
+jw.prototype.$a=function(a){var b=void 0!==a?w(a).toString():"";if(b in this.l)return this.l[b];a=fu.prototype.$a.call(this,a);if(2==this.state){if(256==a.width&&256==a.height)return this.l[b]=a;var c=Oe(256,256);c.drawImage(a,0,0);return this.l[b]=c.canvas}return a};function kw(a){a=a||{};this.a=void 0!==a.initialSize?a.initialSize:256;this.g=void 0!==a.maxSize?a.maxSize:void 0!==la?la:2048;this.b=void 0!==a.space?a.space:1;this.c=[new lw(this.a,this.b)];this.f=this.a;this.i=[new lw(this.f,this.b)]}kw.prototype.add=function(a,b,c,d,e,f){if(b+this.b>this.g||c+this.b>this.g)return null;d=mw(this,!1,a,b,c,d,f);if(!d)return null;a=mw(this,!0,a,b,c,void 0!==e?e:na,f);return{offsetX:d.offsetX,offsetY:d.offsetY,image:d.image,Sg:a.image}};
+function mw(a,b,c,d,e,f,g){var h=b?a.i:a.c,l,m,n;m=0;for(n=h.length;m<n;++m){l=h[m];if(l=l.add(c,d,e,f,g))return l;l||m!==n-1||(b?(l=Math.min(2*a.f,a.g),a.f=l):(l=Math.min(2*a.a,a.g),a.a=l),l=new lw(l,a.b),h.push(l),++n)}}function lw(a,b){this.b=b;this.a=[{x:0,y:0,width:a,height:a}];this.f={};this.g=Oe(a,a);this.c=this.g.canvas}lw.prototype.get=function(a){return this.f[a]||null};
+lw.prototype.add=function(a,b,c,d,e){var f,g,h;g=0;for(h=this.a.length;g<h;++g)if(f=this.a[g],f.width>=b+this.b&&f.height>=c+this.b)return h={offsetX:f.x+this.b,offsetY:f.y+this.b,image:this.c},this.f[a]=h,d.call(e,this.g,f.x+this.b,f.y+this.b),a=g,b+=this.b,d=c+this.b,f.width-b>f.height-d?(c={x:f.x+b,y:f.y,width:f.width-b,height:f.height},b={x:f.x,y:f.y+d,width:b,height:f.height-d},nw(this,a,c,b)):(c={x:f.x+b,y:f.y,width:f.width-b,height:d},b={x:f.x,y:f.y+d,width:f.width,height:f.height-d},nw(this,
+a,c,b)),h;return null};function nw(a,b,c,d){b=[b,1];0<c.width&&0<c.height&&b.push(c);0<d.width&&0<d.height&&b.push(d);a.a.splice.apply(a.a,b)};function ow(a){this.A=this.s=this.f=null;this.o=void 0!==a.fill?a.fill:null;this.ya=[0,0];this.b=a.points;this.g=void 0!==a.radius?a.radius:a.radius1;this.c=void 0!==a.radius2?a.radius2:this.g;this.l=void 0!==a.angle?a.angle:0;this.a=void 0!==a.stroke?a.stroke:null;this.R=this.Ba=this.D=null;var b=a.atlasManager,c="",d="",e=0,f=null,g,h=0;this.a&&(g=ve(this.a.b),h=this.a.a,void 0===h&&(h=1),f=this.a.g,hg||(f=null),d=this.a.c,void 0===d&&(d="round"),c=this.a.f,void 0===c&&(c="round"),e=this.a.i,void 0===
+e&&(e=10));var l=2*(this.g+h)+1,c={strokeStyle:g,Bd:h,size:l,lineCap:c,lineDash:f,lineJoin:d,miterLimit:e};if(void 0===b){var m=Oe(l,l);this.s=m.canvas;b=l=this.s.width;this.Ih(c,m,0,0);this.o?this.A=this.s:(m=Oe(c.size,c.size),this.A=m.canvas,this.Hh(c,m,0,0))}else l=Math.round(l),(d=!this.o)&&(m=this.Hh.bind(this,c)),e=this.a?pj(this.a):"-",f=this.o?jj(this.o):"-",this.f&&e==this.f[1]&&f==this.f[2]&&this.g==this.f[3]&&this.c==this.f[4]&&this.l==this.f[5]&&this.b==this.f[6]||(this.f=["r"+e+f+(void 0!==
+this.g?this.g.toString():"-")+(void 0!==this.c?this.c.toString():"-")+(void 0!==this.l?this.l.toString():"-")+(void 0!==this.b?this.b.toString():"-"),e,f,this.g,this.c,this.l,this.b]),m=b.add(this.f[0],l,l,this.Ih.bind(this,c),m),this.s=m.image,this.ya=[m.offsetX,m.offsetY],b=m.image.width,this.A=d?m.Sg:this.s;this.D=[l/2,l/2];this.Ba=[l,l];this.R=[b,b];Ch.call(this,{opacity:1,rotateWithView:void 0!==a.rotateWithView?a.rotateWithView:!1,rotation:void 0!==a.rotation?a.rotation:0,scale:1,snapToPixel:void 0!==
+a.snapToPixel?a.snapToPixel:!0})}y(ow,Ch);k=ow.prototype;k.Yb=function(){return this.D};k.Fn=function(){return this.l};k.Gn=function(){return this.o};k.pe=function(){return this.A};k.jc=function(){return this.s};k.ld=function(){return this.R};k.td=function(){return 2};k.Ia=function(){return this.ya};k.Hn=function(){return this.b};k.In=function(){return this.g};k.uk=function(){return this.c};k.Fb=function(){return this.Ba};k.Jn=function(){return this.a};k.pf=na;k.load=na;k.Xf=na;
+k.Ih=function(a,b,c,d){var e;b.setTransform(1,0,0,1,0,0);b.translate(c,d);b.beginPath();this.c!==this.g&&(this.b*=2);for(c=0;c<=this.b;c++)d=2*c*Math.PI/this.b-Math.PI/2+this.l,e=0===c%2?this.g:this.c,b.lineTo(a.size/2+e*Math.cos(d),a.size/2+e*Math.sin(d));this.o&&(b.fillStyle=xe(this.o.b),b.fill());this.a&&(b.strokeStyle=a.strokeStyle,b.lineWidth=a.Bd,a.lineDash&&b.setLineDash(a.lineDash),b.lineCap=a.lineCap,b.lineJoin=a.lineJoin,b.miterLimit=a.miterLimit,b.stroke());b.closePath()};
+k.Hh=function(a,b,c,d){b.setTransform(1,0,0,1,0,0);b.translate(c,d);b.beginPath();this.c!==this.g&&(this.b*=2);var e;for(c=0;c<=this.b;c++)e=2*c*Math.PI/this.b-Math.PI/2+this.l,d=0===c%2?this.g:this.c,b.lineTo(a.size/2+d*Math.cos(e),a.size/2+d*Math.sin(e));b.fillStyle=ej;b.fill();this.a&&(b.strokeStyle=a.strokeStyle,b.lineWidth=a.Bd,a.lineDash&&b.setLineDash(a.lineDash),b.stroke());b.closePath()};t("ol.animation.bounce",function(a){var b=a.resolution,c=a.start?a.start:Date.now(),d=void 0!==a.duration?a.duration:1E3,e=a.easing?a.easing:be;return function(a,g){if(g.time<c)return g.animate=!0,g.viewHints[0]+=1,!0;if(g.time<c+d){var h=e((g.time-c)/d),l=b-g.viewState.resolution;g.animate=!0;g.viewState.resolution+=h*l;g.viewHints[0]+=1;return!0}return!1}},OPENLAYERS);t("ol.animation.pan",ce,OPENLAYERS);t("ol.animation.rotate",de,OPENLAYERS);t("ol.animation.zoom",ee,OPENLAYERS);
+t("ol.Attribution",je,OPENLAYERS);je.prototype.getHTML=je.prototype.g;ke.prototype.element=ke.prototype.element;t("ol.Collection",le,OPENLAYERS);le.prototype.clear=le.prototype.clear;le.prototype.extend=le.prototype.qf;le.prototype.forEach=le.prototype.forEach;le.prototype.getArray=le.prototype.Gl;le.prototype.item=le.prototype.item;le.prototype.getLength=le.prototype.dc;le.prototype.insertAt=le.prototype.ee;le.prototype.pop=le.prototype.pop;le.prototype.push=le.prototype.push;
+le.prototype.remove=le.prototype.remove;le.prototype.removeAt=le.prototype.Rf;le.prototype.setAt=le.prototype.Zo;t("ol.colorlike.asColorLike",xe,OPENLAYERS);t("ol.coordinate.add",Bb,OPENLAYERS);t("ol.coordinate.createStringXY",function(a){return function(b){return Jb(b,a)}},OPENLAYERS);t("ol.coordinate.format",Eb,OPENLAYERS);t("ol.coordinate.rotate",Gb,OPENLAYERS);t("ol.coordinate.toStringHDMS",function(a,b){return a?Db(a[1],"NS",b)+" "+Db(a[0],"EW",b):""},OPENLAYERS);
+t("ol.coordinate.toStringXY",Jb,OPENLAYERS);t("ol.DeviceOrientation",qn,OPENLAYERS);qn.prototype.getAlpha=qn.prototype.Oj;qn.prototype.getBeta=qn.prototype.Rj;qn.prototype.getGamma=qn.prototype.Yj;qn.prototype.getHeading=qn.prototype.Hl;qn.prototype.getTracking=qn.prototype.$g;qn.prototype.setTracking=qn.prototype.rf;t("ol.easing.easeIn",Yd,OPENLAYERS);t("ol.easing.easeOut",Zd,OPENLAYERS);t("ol.easing.inAndOut",$d,OPENLAYERS);t("ol.easing.linear",ae,OPENLAYERS);t("ol.easing.upAndDown",be,OPENLAYERS);
+t("ol.extent.boundingExtent",Kb,OPENLAYERS);t("ol.extent.buffer",Ob,OPENLAYERS);t("ol.extent.containsCoordinate",Sb,OPENLAYERS);t("ol.extent.containsExtent",Ub,OPENLAYERS);t("ol.extent.containsXY",Tb,OPENLAYERS);t("ol.extent.createEmpty",Lb,OPENLAYERS);t("ol.extent.equals",$b,OPENLAYERS);t("ol.extent.extend",ac,OPENLAYERS);t("ol.extent.getBottomLeft",cc,OPENLAYERS);t("ol.extent.getBottomRight",dc,OPENLAYERS);t("ol.extent.getCenter",kc,OPENLAYERS);t("ol.extent.getHeight",jc,OPENLAYERS);
+t("ol.extent.getIntersection",mc,OPENLAYERS);t("ol.extent.getSize",function(a){return[a[2]-a[0],a[3]-a[1]]},OPENLAYERS);t("ol.extent.getTopLeft",fc,OPENLAYERS);t("ol.extent.getTopRight",ec,OPENLAYERS);t("ol.extent.getWidth",ic,OPENLAYERS);t("ol.extent.intersects",nc,OPENLAYERS);t("ol.extent.isEmpty",hc,OPENLAYERS);t("ol.extent.applyTransform",pc,OPENLAYERS);t("ol.Feature",Ik,OPENLAYERS);Ik.prototype.clone=Ik.prototype.clone;Ik.prototype.getGeometry=Ik.prototype.W;Ik.prototype.getId=Ik.prototype.Xa;
+Ik.prototype.getGeometryName=Ik.prototype.$j;Ik.prototype.getStyle=Ik.prototype.Jl;Ik.prototype.getStyleFunction=Ik.prototype.ec;Ik.prototype.setGeometry=Ik.prototype.Ua;Ik.prototype.setStyle=Ik.prototype.sf;Ik.prototype.setId=Ik.prototype.mc;Ik.prototype.setGeometryName=Ik.prototype.Ec;t("ol.featureloader.tile",dl,OPENLAYERS);t("ol.featureloader.xhr",el,OPENLAYERS);t("ol.Geolocation",Ut,OPENLAYERS);Ut.prototype.getAccuracy=Ut.prototype.Mj;Ut.prototype.getAccuracyGeometry=Ut.prototype.Nj;
+Ut.prototype.getAltitude=Ut.prototype.Pj;Ut.prototype.getAltitudeAccuracy=Ut.prototype.Qj;Ut.prototype.getHeading=Ut.prototype.Ll;Ut.prototype.getPosition=Ut.prototype.Ml;Ut.prototype.getProjection=Ut.prototype.ah;Ut.prototype.getSpeed=Ut.prototype.wk;Ut.prototype.getTracking=Ut.prototype.bh;Ut.prototype.getTrackingOptions=Ut.prototype.Mg;Ut.prototype.setProjection=Ut.prototype.dh;Ut.prototype.setTracking=Ut.prototype.ge;Ut.prototype.setTrackingOptions=Ut.prototype.si;t("ol.Graticule",$t,OPENLAYERS);
+$t.prototype.getMap=$t.prototype.Pl;$t.prototype.getMeridians=$t.prototype.ik;$t.prototype.getParallels=$t.prototype.qk;$t.prototype.setMap=$t.prototype.setMap;t("ol.has.DEVICE_PIXEL_RATIO",gg,OPENLAYERS);t("ol.has.CANVAS",ig,OPENLAYERS);t("ol.has.DEVICE_ORIENTATION",jg,OPENLAYERS);t("ol.has.GEOLOCATION",kg,OPENLAYERS);t("ol.has.TOUCH",lg,OPENLAYERS);t("ol.has.WEBGL",bg,OPENLAYERS);eu.prototype.getImage=eu.prototype.a;eu.prototype.load=eu.prototype.load;fu.prototype.getImage=fu.prototype.$a;
+fu.prototype.load=fu.prototype.load;t("ol.Kinetic",Th,OPENLAYERS);t("ol.loadingstrategy.all",fl,OPENLAYERS);t("ol.loadingstrategy.bbox",function(a){return[a]},OPENLAYERS);t("ol.loadingstrategy.tile",function(a){return function(b,c){var d=a.Lb(c),e=pf(a,b,d),f=[],d=[d,0,0];for(d[1]=e.ca;d[1]<=e.ea;++d[1])for(d[2]=e.fa;d[2]<=e.ga;++d[2])f.push(a.Ea(d));return f}},OPENLAYERS);t("ol.Map",Q,OPENLAYERS);Q.prototype.addControl=Q.prototype.uj;Q.prototype.addInteraction=Q.prototype.vj;
+Q.prototype.addLayer=Q.prototype.kg;Q.prototype.addOverlay=Q.prototype.lg;Q.prototype.beforeRender=Q.prototype.Wa;Q.prototype.forEachFeatureAtPixel=Q.prototype.kd;Q.prototype.forEachLayerAtPixel=Q.prototype.Tl;Q.prototype.hasFeatureAtPixel=Q.prototype.kl;Q.prototype.getEventCoordinate=Q.prototype.Wj;Q.prototype.getEventPixel=Q.prototype.Td;Q.prototype.getTarget=Q.prototype.tf;Q.prototype.getTargetElement=Q.prototype.yc;Q.prototype.getCoordinateFromPixel=Q.prototype.Ma;Q.prototype.getControls=Q.prototype.Uj;
+Q.prototype.getOverlays=Q.prototype.nk;Q.prototype.getOverlayById=Q.prototype.mk;Q.prototype.getInteractions=Q.prototype.ak;Q.prototype.getLayerGroup=Q.prototype.xc;Q.prototype.getLayers=Q.prototype.eh;Q.prototype.getPixelFromCoordinate=Q.prototype.Ga;Q.prototype.getSize=Q.prototype.Za;Q.prototype.getView=Q.prototype.aa;Q.prototype.getViewport=Q.prototype.Dk;Q.prototype.renderSync=Q.prototype.Vo;Q.prototype.render=Q.prototype.render;Q.prototype.removeControl=Q.prototype.Oo;
+Q.prototype.removeInteraction=Q.prototype.Po;Q.prototype.removeLayer=Q.prototype.Ro;Q.prototype.removeOverlay=Q.prototype.So;Q.prototype.setLayerGroup=Q.prototype.ji;Q.prototype.setSize=Q.prototype.Wf;Q.prototype.setTarget=Q.prototype.fh;Q.prototype.setView=Q.prototype.kp;Q.prototype.updateSize=Q.prototype.Xc;Vg.prototype.originalEvent=Vg.prototype.originalEvent;Vg.prototype.pixel=Vg.prototype.pixel;Vg.prototype.coordinate=Vg.prototype.coordinate;Vg.prototype.dragging=Vg.prototype.dragging;
+We.prototype.map=We.prototype.map;We.prototype.frameState=We.prototype.frameState;db.prototype.key=db.prototype.key;db.prototype.oldValue=db.prototype.oldValue;t("ol.Object",eb,OPENLAYERS);eb.prototype.get=eb.prototype.get;eb.prototype.getKeys=eb.prototype.N;eb.prototype.getProperties=eb.prototype.O;eb.prototype.set=eb.prototype.set;eb.prototype.setProperties=eb.prototype.G;eb.prototype.unset=eb.prototype.P;t("ol.Observable",bb,OPENLAYERS);t("ol.Observable.unByKey",cb,OPENLAYERS);
+bb.prototype.changed=bb.prototype.u;bb.prototype.dispatchEvent=bb.prototype.b;bb.prototype.getRevision=bb.prototype.K;bb.prototype.on=bb.prototype.I;bb.prototype.once=bb.prototype.L;bb.prototype.un=bb.prototype.J;bb.prototype.unByKey=bb.prototype.M;t("ol.inherits",y,OPENLAYERS);t("ol.Overlay",Xm,OPENLAYERS);Xm.prototype.getElement=Xm.prototype.Sd;Xm.prototype.getId=Xm.prototype.Xa;Xm.prototype.getMap=Xm.prototype.he;Xm.prototype.getOffset=Xm.prototype.Kg;Xm.prototype.getPosition=Xm.prototype.gh;
+Xm.prototype.getPositioning=Xm.prototype.Lg;Xm.prototype.setElement=Xm.prototype.fi;Xm.prototype.setMap=Xm.prototype.setMap;Xm.prototype.setOffset=Xm.prototype.li;Xm.prototype.setPosition=Xm.prototype.uf;Xm.prototype.setPositioning=Xm.prototype.oi;t("ol.render.toContext",function(a,b){var c=a.canvas,d=b?b:{},e=d.pixelRatio||gg;if(d=d.size)c.width=d[0]*e,c.height=d[1]*e,c.style.width=d[0]+"px",c.style.height=d[1]+"px";c=[0,0,c.width,c.height];d=qh(Xc(),0,0,e,e,0,0,0);return new yj(a,e,c,d,0)},OPENLAYERS);
+t("ol.size.toSize",hf,OPENLAYERS);df.prototype.getTileCoord=df.prototype.i;df.prototype.load=df.prototype.load;Kk.prototype.getFormat=Kk.prototype.Ul;Kk.prototype.setFeatures=Kk.prototype.gi;Kk.prototype.setProjection=Kk.prototype.vf;Kk.prototype.setLoader=Kk.prototype.ki;t("ol.View",Rd,OPENLAYERS);Rd.prototype.constrainCenter=Rd.prototype.Pd;Rd.prototype.constrainResolution=Rd.prototype.constrainResolution;Rd.prototype.constrainRotation=Rd.prototype.constrainRotation;Rd.prototype.getCenter=Rd.prototype.ab;
+Rd.prototype.calculateExtent=Rd.prototype.Kc;Rd.prototype.getMaxResolution=Rd.prototype.Vl;Rd.prototype.getMinResolution=Rd.prototype.Wl;Rd.prototype.getProjection=Rd.prototype.Xl;Rd.prototype.getResolution=Rd.prototype.$;Rd.prototype.getResolutions=Rd.prototype.Yl;Rd.prototype.getRotation=Rd.prototype.La;Rd.prototype.getZoom=Rd.prototype.Fk;Rd.prototype.fit=Rd.prototype.cf;Rd.prototype.centerOn=Rd.prototype.Ej;Rd.prototype.rotate=Rd.prototype.rotate;Rd.prototype.setCenter=Rd.prototype.mb;
+Rd.prototype.setResolution=Rd.prototype.Ub;Rd.prototype.setRotation=Rd.prototype.ie;Rd.prototype.setZoom=Rd.prototype.np;t("ol.xml.getAllTextContent",Nk,OPENLAYERS);t("ol.xml.parse",Rk,OPENLAYERS);gm.prototype.getGL=gm.prototype.$n;gm.prototype.useProgram=gm.prototype.we;t("ol.tilegrid.TileGrid",mf,OPENLAYERS);mf.prototype.forEachTileCoord=mf.prototype.yg;mf.prototype.getMaxZoom=mf.prototype.Ig;mf.prototype.getMinZoom=mf.prototype.Jg;mf.prototype.getOrigin=mf.prototype.Ia;
+mf.prototype.getResolution=mf.prototype.$;mf.prototype.getResolutions=mf.prototype.Kh;mf.prototype.getTileCoordExtent=mf.prototype.Ea;mf.prototype.getTileCoordForCoordAndResolution=mf.prototype.Zd;mf.prototype.getTileCoordForCoordAndZ=mf.prototype.qd;mf.prototype.getTileSize=mf.prototype.Ja;mf.prototype.getZForResolution=mf.prototype.Lb;t("ol.tilegrid.createXYZ",yf,OPENLAYERS);t("ol.tilegrid.WMTS",fw,OPENLAYERS);fw.prototype.getMatrixIds=fw.prototype.j;
+t("ol.tilegrid.WMTS.createFromCapabilitiesMatrixSet",gw,OPENLAYERS);t("ol.style.AtlasManager",kw,OPENLAYERS);t("ol.style.Circle",qj,OPENLAYERS);qj.prototype.getFill=qj.prototype.Bn;qj.prototype.getImage=qj.prototype.jc;qj.prototype.getRadius=qj.prototype.Cn;qj.prototype.getStroke=qj.prototype.Dn;t("ol.style.Fill",ij,OPENLAYERS);ij.prototype.getColor=ij.prototype.g;ij.prototype.setColor=ij.prototype.f;t("ol.style.Icon",Dh,OPENLAYERS);Dh.prototype.getAnchor=Dh.prototype.Yb;Dh.prototype.getImage=Dh.prototype.jc;
+Dh.prototype.getOrigin=Dh.prototype.Ia;Dh.prototype.getSrc=Dh.prototype.En;Dh.prototype.getSize=Dh.prototype.Fb;Dh.prototype.load=Dh.prototype.load;t("ol.style.Image",Ch,OPENLAYERS);Ch.prototype.getOpacity=Ch.prototype.qe;Ch.prototype.getRotateWithView=Ch.prototype.Xd;Ch.prototype.getRotation=Ch.prototype.re;Ch.prototype.getScale=Ch.prototype.se;Ch.prototype.getSnapToPixel=Ch.prototype.Yd;Ch.prototype.setOpacity=Ch.prototype.te;Ch.prototype.setRotation=Ch.prototype.ue;Ch.prototype.setScale=Ch.prototype.ve;
+t("ol.style.RegularShape",ow,OPENLAYERS);ow.prototype.getAnchor=ow.prototype.Yb;ow.prototype.getAngle=ow.prototype.Fn;ow.prototype.getFill=ow.prototype.Gn;ow.prototype.getImage=ow.prototype.jc;ow.prototype.getOrigin=ow.prototype.Ia;ow.prototype.getPoints=ow.prototype.Hn;ow.prototype.getRadius=ow.prototype.In;ow.prototype.getRadius2=ow.prototype.uk;ow.prototype.getSize=ow.prototype.Fb;ow.prototype.getStroke=ow.prototype.Jn;t("ol.style.Stroke",oj,OPENLAYERS);oj.prototype.getColor=oj.prototype.Kn;
+oj.prototype.getLineCap=oj.prototype.dk;oj.prototype.getLineDash=oj.prototype.Ln;oj.prototype.getLineJoin=oj.prototype.ek;oj.prototype.getMiterLimit=oj.prototype.jk;oj.prototype.getWidth=oj.prototype.Mn;oj.prototype.setColor=oj.prototype.Nn;oj.prototype.setLineCap=oj.prototype.fp;oj.prototype.setLineDash=oj.prototype.On;oj.prototype.setLineJoin=oj.prototype.gp;oj.prototype.setMiterLimit=oj.prototype.hp;oj.prototype.setWidth=oj.prototype.lp;t("ol.style.Style",rj,OPENLAYERS);
+rj.prototype.getGeometry=rj.prototype.W;rj.prototype.getGeometryFunction=rj.prototype.Zj;rj.prototype.getFill=rj.prototype.Pn;rj.prototype.getImage=rj.prototype.Qn;rj.prototype.getStroke=rj.prototype.Rn;rj.prototype.getText=rj.prototype.Ha;rj.prototype.getZIndex=rj.prototype.Sn;rj.prototype.setGeometry=rj.prototype.Jh;rj.prototype.setZIndex=rj.prototype.Tn;t("ol.style.Text",Gp,OPENLAYERS);Gp.prototype.getFont=Gp.prototype.Xj;Gp.prototype.getOffsetX=Gp.prototype.kk;Gp.prototype.getOffsetY=Gp.prototype.lk;
+Gp.prototype.getFill=Gp.prototype.Un;Gp.prototype.getRotation=Gp.prototype.Vn;Gp.prototype.getScale=Gp.prototype.Wn;Gp.prototype.getStroke=Gp.prototype.Xn;Gp.prototype.getText=Gp.prototype.Ha;Gp.prototype.getTextAlign=Gp.prototype.yk;Gp.prototype.getTextBaseline=Gp.prototype.zk;Gp.prototype.setFont=Gp.prototype.bp;Gp.prototype.setOffsetX=Gp.prototype.mi;Gp.prototype.setOffsetY=Gp.prototype.ni;Gp.prototype.setFill=Gp.prototype.ap;Gp.prototype.setRotation=Gp.prototype.Yn;Gp.prototype.setScale=Gp.prototype.Zn;
+Gp.prototype.setStroke=Gp.prototype.ip;Gp.prototype.setText=Gp.prototype.pi;Gp.prototype.setTextAlign=Gp.prototype.ri;Gp.prototype.setTextBaseline=Gp.prototype.jp;t("ol.Sphere",sc,OPENLAYERS);sc.prototype.geodesicArea=sc.prototype.a;sc.prototype.haversineDistance=sc.prototype.b;t("ol.source.BingMaps",pv,OPENLAYERS);t("ol.source.BingMaps.TOS_ATTRIBUTION",qv,OPENLAYERS);t("ol.source.CartoDB",sv,OPENLAYERS);sv.prototype.getConfig=sv.prototype.Tj;sv.prototype.updateConfig=sv.prototype.up;
+sv.prototype.setConfig=sv.prototype.$o;t("ol.source.Cluster",Y,OPENLAYERS);Y.prototype.getSource=Y.prototype.Aa;t("ol.source.ImageArcGISRest",yv,OPENLAYERS);yv.prototype.getParams=yv.prototype.Sm;yv.prototype.getImageLoadFunction=yv.prototype.Rm;yv.prototype.getUrl=yv.prototype.Tm;yv.prototype.setImageLoadFunction=yv.prototype.Um;yv.prototype.setUrl=yv.prototype.Vm;yv.prototype.updateParams=yv.prototype.Wm;t("ol.source.ImageCanvas",Hk,OPENLAYERS);t("ol.source.ImageMapGuide",zv,OPENLAYERS);
+zv.prototype.getParams=zv.prototype.Ym;zv.prototype.getImageLoadFunction=zv.prototype.Xm;zv.prototype.updateParams=zv.prototype.$m;zv.prototype.setImageLoadFunction=zv.prototype.Zm;t("ol.source.Image",Ak,OPENLAYERS);Ck.prototype.image=Ck.prototype.image;t("ol.source.ImageStatic",Av,OPENLAYERS);t("ol.source.ImageVector",yl,OPENLAYERS);yl.prototype.getSource=yl.prototype.an;yl.prototype.getStyle=yl.prototype.bn;yl.prototype.getStyleFunction=yl.prototype.cn;yl.prototype.setStyle=yl.prototype.xh;
+t("ol.source.ImageWMS",Bv,OPENLAYERS);Bv.prototype.getGetFeatureInfoUrl=Bv.prototype.fn;Bv.prototype.getParams=Bv.prototype.hn;Bv.prototype.getImageLoadFunction=Bv.prototype.gn;Bv.prototype.getUrl=Bv.prototype.jn;Bv.prototype.setImageLoadFunction=Bv.prototype.kn;Bv.prototype.setUrl=Bv.prototype.ln;Bv.prototype.updateParams=Bv.prototype.mn;t("ol.source.OSM",Fv,OPENLAYERS);t("ol.source.OSM.ATTRIBUTION",Gv,OPENLAYERS);t("ol.source.Raster",Hv,OPENLAYERS);Hv.prototype.setOperation=Hv.prototype.v;
+Mv.prototype.extent=Mv.prototype.extent;Mv.prototype.resolution=Mv.prototype.resolution;Mv.prototype.data=Mv.prototype.data;t("ol.source.Source",jf,OPENLAYERS);jf.prototype.getAttributions=jf.prototype.wa;jf.prototype.getLogo=jf.prototype.ua;jf.prototype.getProjection=jf.prototype.xa;jf.prototype.getState=jf.prototype.V;jf.prototype.refresh=jf.prototype.sa;jf.prototype.setAttributions=jf.prototype.oa;t("ol.source.Stamen",Rv,OPENLAYERS);t("ol.source.TileArcGISRest",Tv,OPENLAYERS);
+Tv.prototype.getParams=Tv.prototype.v;Tv.prototype.updateParams=Tv.prototype.A;t("ol.source.TileDebug",Vv,OPENLAYERS);t("ol.source.TileImage",W,OPENLAYERS);W.prototype.setRenderReprojectionEdges=W.prototype.zb;W.prototype.setTileGridForProjection=W.prototype.Ab;t("ol.source.TileJSON",Wv,OPENLAYERS);Wv.prototype.getTileJSON=Wv.prototype.Ak;t("ol.source.Tile",zf,OPENLAYERS);zf.prototype.getTileGrid=zf.prototype.Na;Df.prototype.tile=Df.prototype.tile;t("ol.source.TileUTFGrid",Xv,OPENLAYERS);
+Xv.prototype.getTemplate=Xv.prototype.xk;Xv.prototype.forDataAtCoordinateAndResolution=Xv.prototype.Ij;t("ol.source.TileWMS",aw,OPENLAYERS);aw.prototype.getGetFeatureInfoUrl=aw.prototype.vn;aw.prototype.getParams=aw.prototype.wn;aw.prototype.updateParams=aw.prototype.xn;Jl.prototype.getTileLoadFunction=Jl.prototype.fb;Jl.prototype.getTileUrlFunction=Jl.prototype.gb;Jl.prototype.getUrls=Jl.prototype.hb;Jl.prototype.setTileLoadFunction=Jl.prototype.kb;Jl.prototype.setTileUrlFunction=Jl.prototype.Qa;
+Jl.prototype.setUrl=Jl.prototype.Va;Jl.prototype.setUrls=Jl.prototype.bb;t("ol.source.Vector",P,OPENLAYERS);P.prototype.addFeature=P.prototype.rb;P.prototype.addFeatures=P.prototype.Jc;P.prototype.clear=P.prototype.clear;P.prototype.forEachFeature=P.prototype.wg;P.prototype.forEachFeatureInExtent=P.prototype.ub;P.prototype.forEachFeatureIntersectingExtent=P.prototype.xg;P.prototype.getFeaturesCollection=P.prototype.Fg;P.prototype.getFeatures=P.prototype.oe;P.prototype.getFeaturesAtCoordinate=P.prototype.Eg;
+P.prototype.getFeaturesInExtent=P.prototype.ef;P.prototype.getClosestFeatureToCoordinate=P.prototype.Ag;P.prototype.getExtent=P.prototype.H;P.prototype.getFeatureById=P.prototype.Dg;P.prototype.getFormat=P.prototype.Ch;P.prototype.getUrl=P.prototype.Dh;P.prototype.removeFeature=P.prototype.nb;vl.prototype.feature=vl.prototype.feature;t("ol.source.VectorTile",Kl,OPENLAYERS);t("ol.source.WMTS",Z,OPENLAYERS);Z.prototype.getDimensions=Z.prototype.Vj;Z.prototype.getFormat=Z.prototype.yn;
+Z.prototype.getLayer=Z.prototype.zn;Z.prototype.getMatrixSet=Z.prototype.hk;Z.prototype.getRequestEncoding=Z.prototype.vk;Z.prototype.getStyle=Z.prototype.An;Z.prototype.getVersion=Z.prototype.Ck;Z.prototype.updateDimensions=Z.prototype.vp;
+t("ol.source.WMTS.optionsFromCapabilities",function(a,b){var c=ob(a.Contents.Layer,function(a){return a.Identifier==b.layer}),d=a.Contents.TileMatrixSet,e,f;e=1<c.TileMatrixSetLink.length?"projection"in b?sb(c.TileMatrixSetLink,function(a){return ob(d,function(b){return b.Identifier==a.TileMatrixSet}).SupportedCRS.replace(/urn:ogc:def:crs:(\w+):(.*:)?(\w+)$/,"$1:$3")==b.projection}):sb(c.TileMatrixSetLink,function(a){return a.TileMatrixSet==b.matrixSet}):0;0>e&&(e=0);f=c.TileMatrixSetLink[e].TileMatrixSet;
+var g=c.Format[0];"format"in b&&(g=b.format);e=sb(c.Style,function(a){return"style"in b?a.Title==b.style:a.isDefault});0>e&&(e=0);e=c.Style[e].Identifier;var h={};"Dimension"in c&&c.Dimension.forEach(function(a){var b=a.Identifier,c=a.Default;void 0===c&&(c=a.Value[0]);h[b]=c});var l=ob(a.Contents.TileMatrixSet,function(a){return a.Identifier==f}),m;m="projection"in b?yc(b.projection):yc(l.SupportedCRS.replace(/urn:ogc:def:crs:(\w+):(.*:)?(\w+)$/,"$1:$3"));var n=c.WGS84BoundingBox,p,q;void 0!==n&&
+(q=yc("EPSG:4326").H(),q=n[0]==q[0]&&n[2]==q[2],p=Sc(n,"EPSG:4326",m),(n=m.H())&&(Ub(n,p)||(p=void 0)));var l=gw(l,p),r=[];p=b.requestEncoding;p=void 0!==p?p:"";if("OperationsMetadata"in a&&"GetTile"in a.OperationsMetadata)for(var n=a.OperationsMetadata.GetTile.DCP.HTTP.Get,u=0,x=n.length;u<x;++u){var v=ob(n[u].Constraint,function(a){return"GetEncoding"==a.name}).AllowedValues.Value;""===p&&(p=v[0]);if("KVP"===p)jb(v,"KVP")&&r.push(n[u].href);else break}0===r.length&&(p="REST",c.ResourceURL.forEach(function(a){"tile"===
+a.resourceType&&(g=a.format,r.push(a.template))}));return{urls:r,layer:b.layer,matrixSet:f,format:g,projection:m,requestEncoding:p,tileGrid:l,style:e,dimensions:h,wrapX:q}},OPENLAYERS);t("ol.source.XYZ",rv,OPENLAYERS);t("ol.source.Zoomify",iw,OPENLAYERS);lh.prototype.vectorContext=lh.prototype.vectorContext;lh.prototype.frameState=lh.prototype.frameState;lh.prototype.context=lh.prototype.context;lh.prototype.glContext=lh.prototype.glContext;hk.prototype.get=hk.prototype.get;
+hk.prototype.getExtent=hk.prototype.H;hk.prototype.getGeometry=hk.prototype.W;hk.prototype.getProperties=hk.prototype.Nm;hk.prototype.getType=hk.prototype.X;t("ol.render.VectorContext",kh,OPENLAYERS);Cm.prototype.setStyle=Cm.prototype.sd;Cm.prototype.drawGeometry=Cm.prototype.sc;Cm.prototype.drawFeature=Cm.prototype.Ye;yj.prototype.drawCircle=yj.prototype.Rd;yj.prototype.setStyle=yj.prototype.sd;yj.prototype.drawGeometry=yj.prototype.sc;yj.prototype.drawFeature=yj.prototype.Ye;
+t("ol.proj.common.add",bj,OPENLAYERS);t("ol.proj.METERS_PER_UNIT",uc,OPENLAYERS);t("ol.proj.Projection",vc,OPENLAYERS);vc.prototype.getCode=vc.prototype.Sj;vc.prototype.getExtent=vc.prototype.H;vc.prototype.getUnits=vc.prototype.wb;vc.prototype.getMetersPerUnit=vc.prototype.$b;vc.prototype.getWorldExtent=vc.prototype.Ek;vc.prototype.isGlobal=vc.prototype.pl;vc.prototype.setGlobal=vc.prototype.ep;vc.prototype.setExtent=vc.prototype.Mm;vc.prototype.setWorldExtent=vc.prototype.mp;
+vc.prototype.setGetPointResolution=vc.prototype.cp;vc.prototype.getPointResolution=vc.prototype.getPointResolution;t("ol.proj.setProj4",function(a){xc=a},OPENLAYERS);t("ol.proj.addEquivalentProjections",zc,OPENLAYERS);t("ol.proj.addProjection",Lc,OPENLAYERS);t("ol.proj.addCoordinateTransforms",Ac,OPENLAYERS);t("ol.proj.fromLonLat",function(a,b){return Rc(a,"EPSG:4326",void 0!==b?b:"EPSG:3857")},OPENLAYERS);t("ol.proj.toLonLat",function(a,b){return Rc(a,void 0!==b?b:"EPSG:3857","EPSG:4326")},OPENLAYERS);
+t("ol.proj.get",yc,OPENLAYERS);t("ol.proj.equivalent",Oc,OPENLAYERS);t("ol.proj.getTransform",Pc,OPENLAYERS);t("ol.proj.transform",Rc,OPENLAYERS);t("ol.proj.transformExtent",Sc,OPENLAYERS);t("ol.layer.Heatmap",V,OPENLAYERS);V.prototype.getBlur=V.prototype.zg;V.prototype.getGradient=V.prototype.Gg;V.prototype.getRadius=V.prototype.ph;V.prototype.setBlur=V.prototype.di;V.prototype.setGradient=V.prototype.ii;V.prototype.setRadius=V.prototype.qh;t("ol.layer.Image",cj,OPENLAYERS);
+cj.prototype.getSource=cj.prototype.ha;t("ol.layer.Layer",mh,OPENLAYERS);mh.prototype.getSource=mh.prototype.ha;mh.prototype.setMap=mh.prototype.setMap;mh.prototype.setSource=mh.prototype.Fc;t("ol.layer.Base",ih,OPENLAYERS);ih.prototype.getExtent=ih.prototype.H;ih.prototype.getMaxResolution=ih.prototype.Nb;ih.prototype.getMinResolution=ih.prototype.Ob;ih.prototype.getOpacity=ih.prototype.Pb;ih.prototype.getVisible=ih.prototype.xb;ih.prototype.getZIndex=ih.prototype.Qb;ih.prototype.setExtent=ih.prototype.fc;
+ih.prototype.setMaxResolution=ih.prototype.nc;ih.prototype.setMinResolution=ih.prototype.oc;ih.prototype.setOpacity=ih.prototype.gc;ih.prototype.setVisible=ih.prototype.hc;ih.prototype.setZIndex=ih.prototype.ic;t("ol.layer.Group",Ti,OPENLAYERS);Ti.prototype.getLayers=Ti.prototype.Tc;Ti.prototype.setLayers=Ti.prototype.oh;t("ol.layer.Tile",dj,OPENLAYERS);dj.prototype.getPreload=dj.prototype.f;dj.prototype.getSource=dj.prototype.ha;dj.prototype.setPreload=dj.prototype.l;
+dj.prototype.getUseInterimTilesOnError=dj.prototype.c;dj.prototype.setUseInterimTilesOnError=dj.prototype.A;t("ol.layer.Vector",G,OPENLAYERS);G.prototype.getSource=G.prototype.ha;G.prototype.getStyle=G.prototype.C;G.prototype.getStyleFunction=G.prototype.D;G.prototype.setStyle=G.prototype.l;t("ol.layer.VectorTile",I,OPENLAYERS);I.prototype.getPreload=I.prototype.f;I.prototype.getUseInterimTilesOnError=I.prototype.c;I.prototype.setPreload=I.prototype.Y;I.prototype.setUseInterimTilesOnError=I.prototype.ia;
+t("ol.interaction.DoubleClickZoom",Zh,OPENLAYERS);t("ol.interaction.DoubleClickZoom.handleEvent",$h,OPENLAYERS);t("ol.interaction.DragAndDrop",hu,OPENLAYERS);t("ol.interaction.DragAndDrop.handleEvent",qc,OPENLAYERS);ku.prototype.features=ku.prototype.features;ku.prototype.file=ku.prototype.file;ku.prototype.projection=ku.prototype.projection;xi.prototype.coordinate=xi.prototype.coordinate;xi.prototype.mapBrowserEvent=xi.prototype.mapBrowserEvent;t("ol.interaction.DragBox",yi,OPENLAYERS);
+yi.prototype.getGeometry=yi.prototype.W;t("ol.interaction.DragPan",mi,OPENLAYERS);t("ol.interaction.DragRotateAndZoom",mu,OPENLAYERS);t("ol.interaction.DragRotate",qi,OPENLAYERS);t("ol.interaction.DragZoom",Di,OPENLAYERS);qu.prototype.feature=qu.prototype.feature;t("ol.interaction.Draw",ru,OPENLAYERS);t("ol.interaction.Draw.handleEvent",tu,OPENLAYERS);ru.prototype.removeLastPoint=ru.prototype.Qo;ru.prototype.finishDrawing=ru.prototype.jd;ru.prototype.extend=ru.prototype.rm;
+t("ol.interaction.Draw.createRegularPolygon",function(a,b){return function(c,d){var e=c[0],f=c[1],g=Math.sqrt(Hb(e,f)),h=d?d:Pd(new Vt(e),a);Qd(h,e,g,b?b:Math.atan((f[1]-e[1])/(f[0]-e[0])));return h}},OPENLAYERS);t("ol.interaction.Interaction",Vh,OPENLAYERS);Vh.prototype.getActive=Vh.prototype.f;Vh.prototype.getMap=Vh.prototype.l;Vh.prototype.setActive=Vh.prototype.i;t("ol.interaction.defaults",Si,OPENLAYERS);t("ol.interaction.KeyboardPan",Ei,OPENLAYERS);
+t("ol.interaction.KeyboardPan.handleEvent",Fi,OPENLAYERS);t("ol.interaction.KeyboardZoom",Gi,OPENLAYERS);t("ol.interaction.KeyboardZoom.handleEvent",Hi,OPENLAYERS);Hu.prototype.features=Hu.prototype.features;Hu.prototype.mapBrowserEvent=Hu.prototype.mapBrowserEvent;t("ol.interaction.Modify",Iu,OPENLAYERS);t("ol.interaction.Modify.handleEvent",Lu,OPENLAYERS);Iu.prototype.removePoint=Iu.prototype.ai;t("ol.interaction.MouseWheelZoom",Ii,OPENLAYERS);t("ol.interaction.MouseWheelZoom.handleEvent",Ji,OPENLAYERS);
+Ii.prototype.setMouseAnchor=Ii.prototype.D;t("ol.interaction.PinchRotate",Ki,OPENLAYERS);t("ol.interaction.PinchZoom",Oi,OPENLAYERS);t("ol.interaction.Pointer",ji,OPENLAYERS);t("ol.interaction.Pointer.handleEvent",ki,OPENLAYERS);Vu.prototype.selected=Vu.prototype.selected;Vu.prototype.deselected=Vu.prototype.deselected;Vu.prototype.mapBrowserEvent=Vu.prototype.mapBrowserEvent;t("ol.interaction.Select",Wu,OPENLAYERS);Wu.prototype.getFeatures=Wu.prototype.Bm;Wu.prototype.getLayer=Wu.prototype.Cm;
+t("ol.interaction.Select.handleEvent",Xu,OPENLAYERS);Wu.prototype.setMap=Wu.prototype.setMap;t("ol.interaction.Snap",Zu,OPENLAYERS);Zu.prototype.addFeature=Zu.prototype.rb;Zu.prototype.removeFeature=Zu.prototype.nb;cv.prototype.features=cv.prototype.features;cv.prototype.coordinate=cv.prototype.coordinate;t("ol.interaction.Translate",dv,OPENLAYERS);t("ol.geom.Circle",Vt,OPENLAYERS);Vt.prototype.clone=Vt.prototype.clone;Vt.prototype.getCenter=Vt.prototype.rd;Vt.prototype.getRadius=Vt.prototype.wf;
+Vt.prototype.getType=Vt.prototype.X;Vt.prototype.intersectsExtent=Vt.prototype.Ka;Vt.prototype.setCenter=Vt.prototype.jm;Vt.prototype.setCenterAndRadius=Vt.prototype.Vf;Vt.prototype.setRadius=Vt.prototype.km;Vt.prototype.transform=Vt.prototype.jb;t("ol.geom.Geometry",Tc,OPENLAYERS);Tc.prototype.getClosestPoint=Tc.prototype.vb;Tc.prototype.getExtent=Tc.prototype.H;Tc.prototype.rotate=Tc.prototype.rotate;Tc.prototype.simplify=Tc.prototype.Bb;Tc.prototype.transform=Tc.prototype.jb;
+t("ol.geom.GeometryCollection",Ln,OPENLAYERS);Ln.prototype.clone=Ln.prototype.clone;Ln.prototype.getGeometries=Ln.prototype.ff;Ln.prototype.getType=Ln.prototype.X;Ln.prototype.intersectsExtent=Ln.prototype.Ka;Ln.prototype.setGeometries=Ln.prototype.hi;Ln.prototype.applyTransform=Ln.prototype.rc;Ln.prototype.translate=Ln.prototype.Sc;t("ol.geom.LinearRing",zd,OPENLAYERS);zd.prototype.clone=zd.prototype.clone;zd.prototype.getArea=zd.prototype.nm;zd.prototype.getCoordinates=zd.prototype.Z;
+zd.prototype.getType=zd.prototype.X;zd.prototype.setCoordinates=zd.prototype.pa;t("ol.geom.LineString",R,OPENLAYERS);R.prototype.appendCoordinate=R.prototype.wj;R.prototype.clone=R.prototype.clone;R.prototype.forEachSegment=R.prototype.Lj;R.prototype.getCoordinateAtM=R.prototype.lm;R.prototype.getCoordinates=R.prototype.Z;R.prototype.getCoordinateAt=R.prototype.Bg;R.prototype.getLength=R.prototype.mm;R.prototype.getType=R.prototype.X;R.prototype.intersectsExtent=R.prototype.Ka;
+R.prototype.setCoordinates=R.prototype.pa;t("ol.geom.MultiLineString",S,OPENLAYERS);S.prototype.appendLineString=S.prototype.xj;S.prototype.clone=S.prototype.clone;S.prototype.getCoordinateAtM=S.prototype.om;S.prototype.getCoordinates=S.prototype.Z;S.prototype.getLineString=S.prototype.fk;S.prototype.getLineStrings=S.prototype.md;S.prototype.getType=S.prototype.X;S.prototype.intersectsExtent=S.prototype.Ka;S.prototype.setCoordinates=S.prototype.pa;t("ol.geom.MultiPoint",Bn,OPENLAYERS);
+Bn.prototype.appendPoint=Bn.prototype.zj;Bn.prototype.clone=Bn.prototype.clone;Bn.prototype.getCoordinates=Bn.prototype.Z;Bn.prototype.getPoint=Bn.prototype.rk;Bn.prototype.getPoints=Bn.prototype.je;Bn.prototype.getType=Bn.prototype.X;Bn.prototype.intersectsExtent=Bn.prototype.Ka;Bn.prototype.setCoordinates=Bn.prototype.pa;t("ol.geom.MultiPolygon",T,OPENLAYERS);T.prototype.appendPolygon=T.prototype.Aj;T.prototype.clone=T.prototype.clone;T.prototype.getArea=T.prototype.pm;
+T.prototype.getCoordinates=T.prototype.Z;T.prototype.getInteriorPoints=T.prototype.ck;T.prototype.getPolygon=T.prototype.tk;T.prototype.getPolygons=T.prototype.Wd;T.prototype.getType=T.prototype.X;T.prototype.intersectsExtent=T.prototype.Ka;T.prototype.setCoordinates=T.prototype.pa;t("ol.geom.Point",C,OPENLAYERS);C.prototype.clone=C.prototype.clone;C.prototype.getCoordinates=C.prototype.Z;C.prototype.getType=C.prototype.X;C.prototype.intersectsExtent=C.prototype.Ka;C.prototype.setCoordinates=C.prototype.pa;
+t("ol.geom.Polygon",E,OPENLAYERS);E.prototype.appendLinearRing=E.prototype.yj;E.prototype.clone=E.prototype.clone;E.prototype.getArea=E.prototype.qm;E.prototype.getCoordinates=E.prototype.Z;E.prototype.getInteriorPoint=E.prototype.bk;E.prototype.getLinearRingCount=E.prototype.gk;E.prototype.getLinearRing=E.prototype.Hg;E.prototype.getLinearRings=E.prototype.Vd;E.prototype.getType=E.prototype.X;E.prototype.intersectsExtent=E.prototype.Ka;E.prototype.setCoordinates=E.prototype.pa;
+t("ol.geom.Polygon.circular",Nd,OPENLAYERS);t("ol.geom.Polygon.fromExtent",Od,OPENLAYERS);t("ol.geom.Polygon.fromCircle",Pd,OPENLAYERS);t("ol.geom.SimpleGeometry",hd,OPENLAYERS);hd.prototype.getFirstCoordinate=hd.prototype.Ib;hd.prototype.getLastCoordinate=hd.prototype.Jb;hd.prototype.getLayout=hd.prototype.Kb;hd.prototype.applyTransform=hd.prototype.rc;hd.prototype.translate=hd.prototype.Sc;t("ol.format.EsriJSON",En,OPENLAYERS);En.prototype.readFeature=En.prototype.Rb;En.prototype.readFeatures=En.prototype.Fa;
+En.prototype.readGeometry=En.prototype.Vc;En.prototype.readProjection=En.prototype.Oa;En.prototype.writeGeometry=En.prototype.Zc;En.prototype.writeGeometryObject=En.prototype.Ke;En.prototype.writeFeature=En.prototype.Dd;En.prototype.writeFeatureObject=En.prototype.Yc;En.prototype.writeFeatures=En.prototype.Xb;En.prototype.writeFeaturesObject=En.prototype.Ie;t("ol.format.Feature",rn,OPENLAYERS);t("ol.format.GeoJSON",Pn,OPENLAYERS);Pn.prototype.readFeature=Pn.prototype.Rb;
+Pn.prototype.readFeatures=Pn.prototype.Fa;Pn.prototype.readGeometry=Pn.prototype.Vc;Pn.prototype.readProjection=Pn.prototype.Oa;Pn.prototype.writeFeature=Pn.prototype.Dd;Pn.prototype.writeFeatureObject=Pn.prototype.Yc;Pn.prototype.writeFeatures=Pn.prototype.Xb;Pn.prototype.writeFeaturesObject=Pn.prototype.Ie;Pn.prototype.writeGeometry=Pn.prototype.Zc;Pn.prototype.writeGeometryObject=Pn.prototype.Ke;t("ol.format.GPX",uo,OPENLAYERS);uo.prototype.readFeature=uo.prototype.Rb;
+uo.prototype.readFeatures=uo.prototype.Fa;uo.prototype.readProjection=uo.prototype.Oa;uo.prototype.writeFeatures=uo.prototype.Xb;uo.prototype.writeFeaturesNode=uo.prototype.a;t("ol.format.IGC",dp,OPENLAYERS);dp.prototype.readFeature=dp.prototype.Rb;dp.prototype.readFeatures=dp.prototype.Fa;dp.prototype.readProjection=dp.prototype.Oa;t("ol.format.KML",Hp,OPENLAYERS);Hp.prototype.readFeature=Hp.prototype.Rb;Hp.prototype.readFeatures=Hp.prototype.Fa;Hp.prototype.readName=Hp.prototype.Fo;
+Hp.prototype.readNetworkLinks=Hp.prototype.Go;Hp.prototype.readProjection=Hp.prototype.Oa;Hp.prototype.writeFeatures=Hp.prototype.Xb;Hp.prototype.writeFeaturesNode=Hp.prototype.a;t("ol.format.MVT",wr,OPENLAYERS);wr.prototype.readFeatures=wr.prototype.Fa;wr.prototype.readProjection=wr.prototype.Oa;wr.prototype.setLayers=wr.prototype.c;t("ol.format.OSMXML",Sr,OPENLAYERS);Sr.prototype.readFeatures=Sr.prototype.Fa;Sr.prototype.readProjection=Sr.prototype.Oa;t("ol.format.Polyline",qs,OPENLAYERS);
+t("ol.format.Polyline.encodeDeltas",rs,OPENLAYERS);t("ol.format.Polyline.decodeDeltas",ts,OPENLAYERS);t("ol.format.Polyline.encodeFloats",ss,OPENLAYERS);t("ol.format.Polyline.decodeFloats",us,OPENLAYERS);qs.prototype.readFeature=qs.prototype.Rb;qs.prototype.readFeatures=qs.prototype.Fa;qs.prototype.readGeometry=qs.prototype.Vc;qs.prototype.readProjection=qs.prototype.Oa;qs.prototype.writeGeometry=qs.prototype.Zc;t("ol.format.TopoJSON",ws,OPENLAYERS);ws.prototype.readFeatures=ws.prototype.Fa;
+ws.prototype.readProjection=ws.prototype.Oa;t("ol.format.WFS",Cs,OPENLAYERS);Cs.prototype.readFeatures=Cs.prototype.Fa;Cs.prototype.readTransactionResponse=Cs.prototype.o;Cs.prototype.readFeatureCollectionMetadata=Cs.prototype.l;Cs.prototype.writeGetFeature=Cs.prototype.j;Cs.prototype.writeTransaction=Cs.prototype.U;Cs.prototype.readProjection=Cs.prototype.Oa;t("ol.format.WKT",Ts,OPENLAYERS);Ts.prototype.readFeature=Ts.prototype.Rb;Ts.prototype.readFeatures=Ts.prototype.Fa;
+Ts.prototype.readGeometry=Ts.prototype.Vc;Ts.prototype.writeFeature=Ts.prototype.Dd;Ts.prototype.writeFeatures=Ts.prototype.Xb;Ts.prototype.writeGeometry=Ts.prototype.Zc;t("ol.format.WMSCapabilities",jt,OPENLAYERS);jt.prototype.read=jt.prototype.read;t("ol.format.WMSGetFeatureInfo",Gt,OPENLAYERS);Gt.prototype.readFeatures=Gt.prototype.Fa;t("ol.format.WMTSCapabilities",Ht,OPENLAYERS);Ht.prototype.read=Ht.prototype.read;t("ol.format.ogc.filter.and",yr,OPENLAYERS);
+t("ol.format.ogc.filter.or",function(a,b){return new Fr(a,b)},OPENLAYERS);t("ol.format.ogc.filter.not",function(a){return new Gr(a)},OPENLAYERS);t("ol.format.ogc.filter.bbox",Ar,OPENLAYERS);t("ol.format.ogc.filter.equalTo",function(a,b,c){return new Jr(a,b,c)},OPENLAYERS);t("ol.format.ogc.filter.notEqualTo",function(a,b,c){return new Kr(a,b,c)},OPENLAYERS);t("ol.format.ogc.filter.lessThan",function(a,b){return new Lr(a,b)},OPENLAYERS);
+t("ol.format.ogc.filter.lessThanOrEqualTo",function(a,b){return new Mr(a,b)},OPENLAYERS);t("ol.format.ogc.filter.greaterThan",function(a,b){return new Nr(a,b)},OPENLAYERS);t("ol.format.ogc.filter.greaterThanOrEqualTo",function(a,b){return new Or(a,b)},OPENLAYERS);t("ol.format.ogc.filter.isNull",function(a){return new Pr(a)},OPENLAYERS);t("ol.format.ogc.filter.between",function(a,b,c){return new Qr(a,b,c)},OPENLAYERS);
+t("ol.format.ogc.filter.like",function(a,b,c,d,e,f){return new Rr(a,b,c,d,e,f)},OPENLAYERS);t("ol.format.ogc.filter.Filter",Cr,OPENLAYERS);t("ol.format.ogc.filter.And",zr,OPENLAYERS);t("ol.format.ogc.filter.Or",Fr,OPENLAYERS);t("ol.format.ogc.filter.Not",Gr,OPENLAYERS);t("ol.format.ogc.filter.Bbox",Br,OPENLAYERS);t("ol.format.ogc.filter.Comparison",Hr,OPENLAYERS);t("ol.format.ogc.filter.ComparisonBinary",Ir,OPENLAYERS);t("ol.format.ogc.filter.EqualTo",Jr,OPENLAYERS);
+t("ol.format.ogc.filter.NotEqualTo",Kr,OPENLAYERS);t("ol.format.ogc.filter.LessThan",Lr,OPENLAYERS);t("ol.format.ogc.filter.LessThanOrEqualTo",Mr,OPENLAYERS);t("ol.format.ogc.filter.GreaterThan",Nr,OPENLAYERS);t("ol.format.ogc.filter.GreaterThanOrEqualTo",Or,OPENLAYERS);t("ol.format.ogc.filter.IsNull",Pr,OPENLAYERS);t("ol.format.ogc.filter.IsBetween",Qr,OPENLAYERS);t("ol.format.ogc.filter.IsLike",Rr,OPENLAYERS);t("ol.format.GML2",ko,OPENLAYERS);t("ol.format.GML3",lo,OPENLAYERS);
+lo.prototype.writeGeometryNode=lo.prototype.s;lo.prototype.writeFeatures=lo.prototype.Xb;lo.prototype.writeFeaturesNode=lo.prototype.a;t("ol.format.GML",lo,OPENLAYERS);lo.prototype.writeFeatures=lo.prototype.Xb;lo.prototype.writeFeaturesNode=lo.prototype.a;Xn.prototype.readFeatures=Xn.prototype.Fa;t("ol.events.condition.altKeyOnly",function(a){a=a.originalEvent;return a.altKey&&!(a.metaKey||a.ctrlKey)&&!a.shiftKey},OPENLAYERS);t("ol.events.condition.altShiftKeysOnly",ai,OPENLAYERS);
+t("ol.events.condition.always",qc,OPENLAYERS);t("ol.events.condition.click",function(a){return a.type==Zg},OPENLAYERS);t("ol.events.condition.never",rc,OPENLAYERS);t("ol.events.condition.pointerMove",ci,OPENLAYERS);t("ol.events.condition.singleClick",di,OPENLAYERS);t("ol.events.condition.doubleClick",function(a){return a.type==$g},OPENLAYERS);t("ol.events.condition.noModifierKeys",ei,OPENLAYERS);
+t("ol.events.condition.platformModifierKeyOnly",function(a){a=a.originalEvent;return!a.altKey&&(fg?a.metaKey:a.ctrlKey)&&!a.shiftKey},OPENLAYERS);t("ol.events.condition.shiftKeyOnly",fi,OPENLAYERS);t("ol.events.condition.targetNotEditable",gi,OPENLAYERS);t("ol.events.condition.mouseOnly",hi,OPENLAYERS);t("ol.events.condition.primaryAction",ii,OPENLAYERS);Wa.prototype.type=Wa.prototype.type;Wa.prototype.target=Wa.prototype.target;Wa.prototype.preventDefault=Wa.prototype.preventDefault;
+Wa.prototype.stopPropagation=Wa.prototype.stopPropagation;t("ol.control.Attribution",Ef,OPENLAYERS);t("ol.control.Attribution.render",Ff,OPENLAYERS);Ef.prototype.getCollapsible=Ef.prototype.$l;Ef.prototype.setCollapsible=Ef.prototype.cm;Ef.prototype.setCollapsed=Ef.prototype.bm;Ef.prototype.getCollapsed=Ef.prototype.Zl;t("ol.control.Control",Xe,OPENLAYERS);Xe.prototype.getMap=Xe.prototype.i;Xe.prototype.setMap=Xe.prototype.setMap;Xe.prototype.setTarget=Xe.prototype.c;t("ol.control.defaults",Kf,OPENLAYERS);
+t("ol.control.FullScreen",Lf,OPENLAYERS);t("ol.control.MousePosition",Qf,OPENLAYERS);t("ol.control.MousePosition.render",Rf,OPENLAYERS);Qf.prototype.getCoordinateFormat=Qf.prototype.Cg;Qf.prototype.getProjection=Qf.prototype.hh;Qf.prototype.setCoordinateFormat=Qf.prototype.ei;Qf.prototype.setProjection=Qf.prototype.ih;t("ol.control.OverviewMap",an,OPENLAYERS);t("ol.control.OverviewMap.render",bn,OPENLAYERS);an.prototype.getCollapsible=an.prototype.fm;an.prototype.setCollapsible=an.prototype.im;
+an.prototype.setCollapsed=an.prototype.hm;an.prototype.getCollapsed=an.prototype.em;an.prototype.getOverviewMap=an.prototype.pk;t("ol.control.Rotate",Hf,OPENLAYERS);t("ol.control.Rotate.render",If,OPENLAYERS);t("ol.control.ScaleLine",fn,OPENLAYERS);fn.prototype.getUnits=fn.prototype.wb;t("ol.control.ScaleLine.render",gn,OPENLAYERS);fn.prototype.setUnits=fn.prototype.D;t("ol.control.Zoom",Jf,OPENLAYERS);t("ol.control.ZoomSlider",kn,OPENLAYERS);t("ol.control.ZoomSlider.render",mn,OPENLAYERS);
+t("ol.control.ZoomToExtent",pn,OPENLAYERS);t("ol.color.asArray",te,OPENLAYERS);t("ol.color.asString",ve,OPENLAYERS);ke.prototype.type=ke.prototype.type;ke.prototype.target=ke.prototype.target;ke.prototype.preventDefault=ke.prototype.preventDefault;ke.prototype.stopPropagation=ke.prototype.stopPropagation;eb.prototype.changed=eb.prototype.u;eb.prototype.dispatchEvent=eb.prototype.b;eb.prototype.getRevision=eb.prototype.K;eb.prototype.on=eb.prototype.I;eb.prototype.once=eb.prototype.L;
+eb.prototype.un=eb.prototype.J;eb.prototype.unByKey=eb.prototype.M;le.prototype.get=le.prototype.get;le.prototype.getKeys=le.prototype.N;le.prototype.getProperties=le.prototype.O;le.prototype.set=le.prototype.set;le.prototype.setProperties=le.prototype.G;le.prototype.unset=le.prototype.P;le.prototype.changed=le.prototype.u;le.prototype.dispatchEvent=le.prototype.b;le.prototype.getRevision=le.prototype.K;le.prototype.on=le.prototype.I;le.prototype.once=le.prototype.L;le.prototype.un=le.prototype.J;
+le.prototype.unByKey=le.prototype.M;qn.prototype.get=qn.prototype.get;qn.prototype.getKeys=qn.prototype.N;qn.prototype.getProperties=qn.prototype.O;qn.prototype.set=qn.prototype.set;qn.prototype.setProperties=qn.prototype.G;qn.prototype.unset=qn.prototype.P;qn.prototype.changed=qn.prototype.u;qn.prototype.dispatchEvent=qn.prototype.b;qn.prototype.getRevision=qn.prototype.K;qn.prototype.on=qn.prototype.I;qn.prototype.once=qn.prototype.L;qn.prototype.un=qn.prototype.J;qn.prototype.unByKey=qn.prototype.M;
+Ik.prototype.get=Ik.prototype.get;Ik.prototype.getKeys=Ik.prototype.N;Ik.prototype.getProperties=Ik.prototype.O;Ik.prototype.set=Ik.prototype.set;Ik.prototype.setProperties=Ik.prototype.G;Ik.prototype.unset=Ik.prototype.P;Ik.prototype.changed=Ik.prototype.u;Ik.prototype.dispatchEvent=Ik.prototype.b;Ik.prototype.getRevision=Ik.prototype.K;Ik.prototype.on=Ik.prototype.I;Ik.prototype.once=Ik.prototype.L;Ik.prototype.un=Ik.prototype.J;Ik.prototype.unByKey=Ik.prototype.M;Ut.prototype.get=Ut.prototype.get;
+Ut.prototype.getKeys=Ut.prototype.N;Ut.prototype.getProperties=Ut.prototype.O;Ut.prototype.set=Ut.prototype.set;Ut.prototype.setProperties=Ut.prototype.G;Ut.prototype.unset=Ut.prototype.P;Ut.prototype.changed=Ut.prototype.u;Ut.prototype.dispatchEvent=Ut.prototype.b;Ut.prototype.getRevision=Ut.prototype.K;Ut.prototype.on=Ut.prototype.I;Ut.prototype.once=Ut.prototype.L;Ut.prototype.un=Ut.prototype.J;Ut.prototype.unByKey=Ut.prototype.M;fu.prototype.getTileCoord=fu.prototype.i;Q.prototype.get=Q.prototype.get;
+Q.prototype.getKeys=Q.prototype.N;Q.prototype.getProperties=Q.prototype.O;Q.prototype.set=Q.prototype.set;Q.prototype.setProperties=Q.prototype.G;Q.prototype.unset=Q.prototype.P;Q.prototype.changed=Q.prototype.u;Q.prototype.dispatchEvent=Q.prototype.b;Q.prototype.getRevision=Q.prototype.K;Q.prototype.on=Q.prototype.I;Q.prototype.once=Q.prototype.L;Q.prototype.un=Q.prototype.J;Q.prototype.unByKey=Q.prototype.M;We.prototype.type=We.prototype.type;We.prototype.target=We.prototype.target;
+We.prototype.preventDefault=We.prototype.preventDefault;We.prototype.stopPropagation=We.prototype.stopPropagation;Vg.prototype.map=Vg.prototype.map;Vg.prototype.frameState=Vg.prototype.frameState;Vg.prototype.type=Vg.prototype.type;Vg.prototype.target=Vg.prototype.target;Vg.prototype.preventDefault=Vg.prototype.preventDefault;Vg.prototype.stopPropagation=Vg.prototype.stopPropagation;Wg.prototype.originalEvent=Wg.prototype.originalEvent;Wg.prototype.pixel=Wg.prototype.pixel;
+Wg.prototype.coordinate=Wg.prototype.coordinate;Wg.prototype.dragging=Wg.prototype.dragging;Wg.prototype.preventDefault=Wg.prototype.preventDefault;Wg.prototype.stopPropagation=Wg.prototype.stopPropagation;Wg.prototype.map=Wg.prototype.map;Wg.prototype.frameState=Wg.prototype.frameState;Wg.prototype.type=Wg.prototype.type;Wg.prototype.target=Wg.prototype.target;db.prototype.type=db.prototype.type;db.prototype.target=db.prototype.target;db.prototype.preventDefault=db.prototype.preventDefault;
+db.prototype.stopPropagation=db.prototype.stopPropagation;Xm.prototype.get=Xm.prototype.get;Xm.prototype.getKeys=Xm.prototype.N;Xm.prototype.getProperties=Xm.prototype.O;Xm.prototype.set=Xm.prototype.set;Xm.prototype.setProperties=Xm.prototype.G;Xm.prototype.unset=Xm.prototype.P;Xm.prototype.changed=Xm.prototype.u;Xm.prototype.dispatchEvent=Xm.prototype.b;Xm.prototype.getRevision=Xm.prototype.K;Xm.prototype.on=Xm.prototype.I;Xm.prototype.once=Xm.prototype.L;Xm.prototype.un=Xm.prototype.J;
+Xm.prototype.unByKey=Xm.prototype.M;Kk.prototype.getTileCoord=Kk.prototype.i;Rd.prototype.get=Rd.prototype.get;Rd.prototype.getKeys=Rd.prototype.N;Rd.prototype.getProperties=Rd.prototype.O;Rd.prototype.set=Rd.prototype.set;Rd.prototype.setProperties=Rd.prototype.G;Rd.prototype.unset=Rd.prototype.P;Rd.prototype.changed=Rd.prototype.u;Rd.prototype.dispatchEvent=Rd.prototype.b;Rd.prototype.getRevision=Rd.prototype.K;Rd.prototype.on=Rd.prototype.I;Rd.prototype.once=Rd.prototype.L;Rd.prototype.un=Rd.prototype.J;
+Rd.prototype.unByKey=Rd.prototype.M;fw.prototype.forEachTileCoord=fw.prototype.yg;fw.prototype.getMaxZoom=fw.prototype.Ig;fw.prototype.getMinZoom=fw.prototype.Jg;fw.prototype.getOrigin=fw.prototype.Ia;fw.prototype.getResolution=fw.prototype.$;fw.prototype.getResolutions=fw.prototype.Kh;fw.prototype.getTileCoordExtent=fw.prototype.Ea;fw.prototype.getTileCoordForCoordAndResolution=fw.prototype.Zd;fw.prototype.getTileCoordForCoordAndZ=fw.prototype.qd;fw.prototype.getTileSize=fw.prototype.Ja;
+fw.prototype.getZForResolution=fw.prototype.Lb;qj.prototype.getOpacity=qj.prototype.qe;qj.prototype.getRotateWithView=qj.prototype.Xd;qj.prototype.getRotation=qj.prototype.re;qj.prototype.getScale=qj.prototype.se;qj.prototype.getSnapToPixel=qj.prototype.Yd;qj.prototype.setOpacity=qj.prototype.te;qj.prototype.setRotation=qj.prototype.ue;qj.prototype.setScale=qj.prototype.ve;Dh.prototype.getOpacity=Dh.prototype.qe;Dh.prototype.getRotateWithView=Dh.prototype.Xd;Dh.prototype.getRotation=Dh.prototype.re;
+Dh.prototype.getScale=Dh.prototype.se;Dh.prototype.getSnapToPixel=Dh.prototype.Yd;Dh.prototype.setOpacity=Dh.prototype.te;Dh.prototype.setRotation=Dh.prototype.ue;Dh.prototype.setScale=Dh.prototype.ve;ow.prototype.getOpacity=ow.prototype.qe;ow.prototype.getRotateWithView=ow.prototype.Xd;ow.prototype.getRotation=ow.prototype.re;ow.prototype.getScale=ow.prototype.se;ow.prototype.getSnapToPixel=ow.prototype.Yd;ow.prototype.setOpacity=ow.prototype.te;ow.prototype.setRotation=ow.prototype.ue;
+ow.prototype.setScale=ow.prototype.ve;jf.prototype.get=jf.prototype.get;jf.prototype.getKeys=jf.prototype.N;jf.prototype.getProperties=jf.prototype.O;jf.prototype.set=jf.prototype.set;jf.prototype.setProperties=jf.prototype.G;jf.prototype.unset=jf.prototype.P;jf.prototype.changed=jf.prototype.u;jf.prototype.dispatchEvent=jf.prototype.b;jf.prototype.getRevision=jf.prototype.K;jf.prototype.on=jf.prototype.I;jf.prototype.once=jf.prototype.L;jf.prototype.un=jf.prototype.J;jf.prototype.unByKey=jf.prototype.M;
+zf.prototype.getAttributions=zf.prototype.wa;zf.prototype.getLogo=zf.prototype.ua;zf.prototype.getProjection=zf.prototype.xa;zf.prototype.getState=zf.prototype.V;zf.prototype.refresh=zf.prototype.sa;zf.prototype.setAttributions=zf.prototype.oa;zf.prototype.get=zf.prototype.get;zf.prototype.getKeys=zf.prototype.N;zf.prototype.getProperties=zf.prototype.O;zf.prototype.set=zf.prototype.set;zf.prototype.setProperties=zf.prototype.G;zf.prototype.unset=zf.prototype.P;zf.prototype.changed=zf.prototype.u;
+zf.prototype.dispatchEvent=zf.prototype.b;zf.prototype.getRevision=zf.prototype.K;zf.prototype.on=zf.prototype.I;zf.prototype.once=zf.prototype.L;zf.prototype.un=zf.prototype.J;zf.prototype.unByKey=zf.prototype.M;Jl.prototype.getTileGrid=Jl.prototype.Na;Jl.prototype.refresh=Jl.prototype.sa;Jl.prototype.getAttributions=Jl.prototype.wa;Jl.prototype.getLogo=Jl.prototype.ua;Jl.prototype.getProjection=Jl.prototype.xa;Jl.prototype.getState=Jl.prototype.V;Jl.prototype.setAttributions=Jl.prototype.oa;
+Jl.prototype.get=Jl.prototype.get;Jl.prototype.getKeys=Jl.prototype.N;Jl.prototype.getProperties=Jl.prototype.O;Jl.prototype.set=Jl.prototype.set;Jl.prototype.setProperties=Jl.prototype.G;Jl.prototype.unset=Jl.prototype.P;Jl.prototype.changed=Jl.prototype.u;Jl.prototype.dispatchEvent=Jl.prototype.b;Jl.prototype.getRevision=Jl.prototype.K;Jl.prototype.on=Jl.prototype.I;Jl.prototype.once=Jl.prototype.L;Jl.prototype.un=Jl.prototype.J;Jl.prototype.unByKey=Jl.prototype.M;
+W.prototype.getTileLoadFunction=W.prototype.fb;W.prototype.getTileUrlFunction=W.prototype.gb;W.prototype.getUrls=W.prototype.hb;W.prototype.setTileLoadFunction=W.prototype.kb;W.prototype.setTileUrlFunction=W.prototype.Qa;W.prototype.setUrl=W.prototype.Va;W.prototype.setUrls=W.prototype.bb;W.prototype.getTileGrid=W.prototype.Na;W.prototype.refresh=W.prototype.sa;W.prototype.getAttributions=W.prototype.wa;W.prototype.getLogo=W.prototype.ua;W.prototype.getProjection=W.prototype.xa;
+W.prototype.getState=W.prototype.V;W.prototype.setAttributions=W.prototype.oa;W.prototype.get=W.prototype.get;W.prototype.getKeys=W.prototype.N;W.prototype.getProperties=W.prototype.O;W.prototype.set=W.prototype.set;W.prototype.setProperties=W.prototype.G;W.prototype.unset=W.prototype.P;W.prototype.changed=W.prototype.u;W.prototype.dispatchEvent=W.prototype.b;W.prototype.getRevision=W.prototype.K;W.prototype.on=W.prototype.I;W.prototype.once=W.prototype.L;W.prototype.un=W.prototype.J;
+W.prototype.unByKey=W.prototype.M;pv.prototype.setRenderReprojectionEdges=pv.prototype.zb;pv.prototype.setTileGridForProjection=pv.prototype.Ab;pv.prototype.getTileLoadFunction=pv.prototype.fb;pv.prototype.getTileUrlFunction=pv.prototype.gb;pv.prototype.getUrls=pv.prototype.hb;pv.prototype.setTileLoadFunction=pv.prototype.kb;pv.prototype.setTileUrlFunction=pv.prototype.Qa;pv.prototype.setUrl=pv.prototype.Va;pv.prototype.setUrls=pv.prototype.bb;pv.prototype.getTileGrid=pv.prototype.Na;
+pv.prototype.refresh=pv.prototype.sa;pv.prototype.getAttributions=pv.prototype.wa;pv.prototype.getLogo=pv.prototype.ua;pv.prototype.getProjection=pv.prototype.xa;pv.prototype.getState=pv.prototype.V;pv.prototype.setAttributions=pv.prototype.oa;pv.prototype.get=pv.prototype.get;pv.prototype.getKeys=pv.prototype.N;pv.prototype.getProperties=pv.prototype.O;pv.prototype.set=pv.prototype.set;pv.prototype.setProperties=pv.prototype.G;pv.prototype.unset=pv.prototype.P;pv.prototype.changed=pv.prototype.u;
+pv.prototype.dispatchEvent=pv.prototype.b;pv.prototype.getRevision=pv.prototype.K;pv.prototype.on=pv.prototype.I;pv.prototype.once=pv.prototype.L;pv.prototype.un=pv.prototype.J;pv.prototype.unByKey=pv.prototype.M;rv.prototype.setRenderReprojectionEdges=rv.prototype.zb;rv.prototype.setTileGridForProjection=rv.prototype.Ab;rv.prototype.getTileLoadFunction=rv.prototype.fb;rv.prototype.getTileUrlFunction=rv.prototype.gb;rv.prototype.getUrls=rv.prototype.hb;rv.prototype.setTileLoadFunction=rv.prototype.kb;
+rv.prototype.setTileUrlFunction=rv.prototype.Qa;rv.prototype.setUrl=rv.prototype.Va;rv.prototype.setUrls=rv.prototype.bb;rv.prototype.getTileGrid=rv.prototype.Na;rv.prototype.refresh=rv.prototype.sa;rv.prototype.getAttributions=rv.prototype.wa;rv.prototype.getLogo=rv.prototype.ua;rv.prototype.getProjection=rv.prototype.xa;rv.prototype.getState=rv.prototype.V;rv.prototype.setAttributions=rv.prototype.oa;rv.prototype.get=rv.prototype.get;rv.prototype.getKeys=rv.prototype.N;
+rv.prototype.getProperties=rv.prototype.O;rv.prototype.set=rv.prototype.set;rv.prototype.setProperties=rv.prototype.G;rv.prototype.unset=rv.prototype.P;rv.prototype.changed=rv.prototype.u;rv.prototype.dispatchEvent=rv.prototype.b;rv.prototype.getRevision=rv.prototype.K;rv.prototype.on=rv.prototype.I;rv.prototype.once=rv.prototype.L;rv.prototype.un=rv.prototype.J;rv.prototype.unByKey=rv.prototype.M;sv.prototype.setRenderReprojectionEdges=sv.prototype.zb;sv.prototype.setTileGridForProjection=sv.prototype.Ab;
+sv.prototype.getTileLoadFunction=sv.prototype.fb;sv.prototype.getTileUrlFunction=sv.prototype.gb;sv.prototype.getUrls=sv.prototype.hb;sv.prototype.setTileLoadFunction=sv.prototype.kb;sv.prototype.setTileUrlFunction=sv.prototype.Qa;sv.prototype.setUrl=sv.prototype.Va;sv.prototype.setUrls=sv.prototype.bb;sv.prototype.getTileGrid=sv.prototype.Na;sv.prototype.refresh=sv.prototype.sa;sv.prototype.getAttributions=sv.prototype.wa;sv.prototype.getLogo=sv.prototype.ua;sv.prototype.getProjection=sv.prototype.xa;
+sv.prototype.getState=sv.prototype.V;sv.prototype.setAttributions=sv.prototype.oa;sv.prototype.get=sv.prototype.get;sv.prototype.getKeys=sv.prototype.N;sv.prototype.getProperties=sv.prototype.O;sv.prototype.set=sv.prototype.set;sv.prototype.setProperties=sv.prototype.G;sv.prototype.unset=sv.prototype.P;sv.prototype.changed=sv.prototype.u;sv.prototype.dispatchEvent=sv.prototype.b;sv.prototype.getRevision=sv.prototype.K;sv.prototype.on=sv.prototype.I;sv.prototype.once=sv.prototype.L;
+sv.prototype.un=sv.prototype.J;sv.prototype.unByKey=sv.prototype.M;P.prototype.getAttributions=P.prototype.wa;P.prototype.getLogo=P.prototype.ua;P.prototype.getProjection=P.prototype.xa;P.prototype.getState=P.prototype.V;P.prototype.refresh=P.prototype.sa;P.prototype.setAttributions=P.prototype.oa;P.prototype.get=P.prototype.get;P.prototype.getKeys=P.prototype.N;P.prototype.getProperties=P.prototype.O;P.prototype.set=P.prototype.set;P.prototype.setProperties=P.prototype.G;P.prototype.unset=P.prototype.P;
+P.prototype.changed=P.prototype.u;P.prototype.dispatchEvent=P.prototype.b;P.prototype.getRevision=P.prototype.K;P.prototype.on=P.prototype.I;P.prototype.once=P.prototype.L;P.prototype.un=P.prototype.J;P.prototype.unByKey=P.prototype.M;Y.prototype.addFeature=Y.prototype.rb;Y.prototype.addFeatures=Y.prototype.Jc;Y.prototype.clear=Y.prototype.clear;Y.prototype.forEachFeature=Y.prototype.wg;Y.prototype.forEachFeatureInExtent=Y.prototype.ub;Y.prototype.forEachFeatureIntersectingExtent=Y.prototype.xg;
+Y.prototype.getFeaturesCollection=Y.prototype.Fg;Y.prototype.getFeatures=Y.prototype.oe;Y.prototype.getFeaturesAtCoordinate=Y.prototype.Eg;Y.prototype.getFeaturesInExtent=Y.prototype.ef;Y.prototype.getClosestFeatureToCoordinate=Y.prototype.Ag;Y.prototype.getExtent=Y.prototype.H;Y.prototype.getFeatureById=Y.prototype.Dg;Y.prototype.getFormat=Y.prototype.Ch;Y.prototype.getUrl=Y.prototype.Dh;Y.prototype.removeFeature=Y.prototype.nb;Y.prototype.getAttributions=Y.prototype.wa;Y.prototype.getLogo=Y.prototype.ua;
+Y.prototype.getProjection=Y.prototype.xa;Y.prototype.getState=Y.prototype.V;Y.prototype.refresh=Y.prototype.sa;Y.prototype.setAttributions=Y.prototype.oa;Y.prototype.get=Y.prototype.get;Y.prototype.getKeys=Y.prototype.N;Y.prototype.getProperties=Y.prototype.O;Y.prototype.set=Y.prototype.set;Y.prototype.setProperties=Y.prototype.G;Y.prototype.unset=Y.prototype.P;Y.prototype.changed=Y.prototype.u;Y.prototype.dispatchEvent=Y.prototype.b;Y.prototype.getRevision=Y.prototype.K;Y.prototype.on=Y.prototype.I;
+Y.prototype.once=Y.prototype.L;Y.prototype.un=Y.prototype.J;Y.prototype.unByKey=Y.prototype.M;Ak.prototype.getAttributions=Ak.prototype.wa;Ak.prototype.getLogo=Ak.prototype.ua;Ak.prototype.getProjection=Ak.prototype.xa;Ak.prototype.getState=Ak.prototype.V;Ak.prototype.refresh=Ak.prototype.sa;Ak.prototype.setAttributions=Ak.prototype.oa;Ak.prototype.get=Ak.prototype.get;Ak.prototype.getKeys=Ak.prototype.N;Ak.prototype.getProperties=Ak.prototype.O;Ak.prototype.set=Ak.prototype.set;
+Ak.prototype.setProperties=Ak.prototype.G;Ak.prototype.unset=Ak.prototype.P;Ak.prototype.changed=Ak.prototype.u;Ak.prototype.dispatchEvent=Ak.prototype.b;Ak.prototype.getRevision=Ak.prototype.K;Ak.prototype.on=Ak.prototype.I;Ak.prototype.once=Ak.prototype.L;Ak.prototype.un=Ak.prototype.J;Ak.prototype.unByKey=Ak.prototype.M;yv.prototype.getAttributions=yv.prototype.wa;yv.prototype.getLogo=yv.prototype.ua;yv.prototype.getProjection=yv.prototype.xa;yv.prototype.getState=yv.prototype.V;
+yv.prototype.refresh=yv.prototype.sa;yv.prototype.setAttributions=yv.prototype.oa;yv.prototype.get=yv.prototype.get;yv.prototype.getKeys=yv.prototype.N;yv.prototype.getProperties=yv.prototype.O;yv.prototype.set=yv.prototype.set;yv.prototype.setProperties=yv.prototype.G;yv.prototype.unset=yv.prototype.P;yv.prototype.changed=yv.prototype.u;yv.prototype.dispatchEvent=yv.prototype.b;yv.prototype.getRevision=yv.prototype.K;yv.prototype.on=yv.prototype.I;yv.prototype.once=yv.prototype.L;
+yv.prototype.un=yv.prototype.J;yv.prototype.unByKey=yv.prototype.M;Hk.prototype.getAttributions=Hk.prototype.wa;Hk.prototype.getLogo=Hk.prototype.ua;Hk.prototype.getProjection=Hk.prototype.xa;Hk.prototype.getState=Hk.prototype.V;Hk.prototype.refresh=Hk.prototype.sa;Hk.prototype.setAttributions=Hk.prototype.oa;Hk.prototype.get=Hk.prototype.get;Hk.prototype.getKeys=Hk.prototype.N;Hk.prototype.getProperties=Hk.prototype.O;Hk.prototype.set=Hk.prototype.set;Hk.prototype.setProperties=Hk.prototype.G;
+Hk.prototype.unset=Hk.prototype.P;Hk.prototype.changed=Hk.prototype.u;Hk.prototype.dispatchEvent=Hk.prototype.b;Hk.prototype.getRevision=Hk.prototype.K;Hk.prototype.on=Hk.prototype.I;Hk.prototype.once=Hk.prototype.L;Hk.prototype.un=Hk.prototype.J;Hk.prototype.unByKey=Hk.prototype.M;zv.prototype.getAttributions=zv.prototype.wa;zv.prototype.getLogo=zv.prototype.ua;zv.prototype.getProjection=zv.prototype.xa;zv.prototype.getState=zv.prototype.V;zv.prototype.refresh=zv.prototype.sa;
+zv.prototype.setAttributions=zv.prototype.oa;zv.prototype.get=zv.prototype.get;zv.prototype.getKeys=zv.prototype.N;zv.prototype.getProperties=zv.prototype.O;zv.prototype.set=zv.prototype.set;zv.prototype.setProperties=zv.prototype.G;zv.prototype.unset=zv.prototype.P;zv.prototype.changed=zv.prototype.u;zv.prototype.dispatchEvent=zv.prototype.b;zv.prototype.getRevision=zv.prototype.K;zv.prototype.on=zv.prototype.I;zv.prototype.once=zv.prototype.L;zv.prototype.un=zv.prototype.J;
+zv.prototype.unByKey=zv.prototype.M;Ck.prototype.type=Ck.prototype.type;Ck.prototype.target=Ck.prototype.target;Ck.prototype.preventDefault=Ck.prototype.preventDefault;Ck.prototype.stopPropagation=Ck.prototype.stopPropagation;Av.prototype.getAttributions=Av.prototype.wa;Av.prototype.getLogo=Av.prototype.ua;Av.prototype.getProjection=Av.prototype.xa;Av.prototype.getState=Av.prototype.V;Av.prototype.refresh=Av.prototype.sa;Av.prototype.setAttributions=Av.prototype.oa;Av.prototype.get=Av.prototype.get;
+Av.prototype.getKeys=Av.prototype.N;Av.prototype.getProperties=Av.prototype.O;Av.prototype.set=Av.prototype.set;Av.prototype.setProperties=Av.prototype.G;Av.prototype.unset=Av.prototype.P;Av.prototype.changed=Av.prototype.u;Av.prototype.dispatchEvent=Av.prototype.b;Av.prototype.getRevision=Av.prototype.K;Av.prototype.on=Av.prototype.I;Av.prototype.once=Av.prototype.L;Av.prototype.un=Av.prototype.J;Av.prototype.unByKey=Av.prototype.M;yl.prototype.getAttributions=yl.prototype.wa;
+yl.prototype.getLogo=yl.prototype.ua;yl.prototype.getProjection=yl.prototype.xa;yl.prototype.getState=yl.prototype.V;yl.prototype.refresh=yl.prototype.sa;yl.prototype.setAttributions=yl.prototype.oa;yl.prototype.get=yl.prototype.get;yl.prototype.getKeys=yl.prototype.N;yl.prototype.getProperties=yl.prototype.O;yl.prototype.set=yl.prototype.set;yl.prototype.setProperties=yl.prototype.G;yl.prototype.unset=yl.prototype.P;yl.prototype.changed=yl.prototype.u;yl.prototype.dispatchEvent=yl.prototype.b;
+yl.prototype.getRevision=yl.prototype.K;yl.prototype.on=yl.prototype.I;yl.prototype.once=yl.prototype.L;yl.prototype.un=yl.prototype.J;yl.prototype.unByKey=yl.prototype.M;Bv.prototype.getAttributions=Bv.prototype.wa;Bv.prototype.getLogo=Bv.prototype.ua;Bv.prototype.getProjection=Bv.prototype.xa;Bv.prototype.getState=Bv.prototype.V;Bv.prototype.refresh=Bv.prototype.sa;Bv.prototype.setAttributions=Bv.prototype.oa;Bv.prototype.get=Bv.prototype.get;Bv.prototype.getKeys=Bv.prototype.N;
+Bv.prototype.getProperties=Bv.prototype.O;Bv.prototype.set=Bv.prototype.set;Bv.prototype.setProperties=Bv.prototype.G;Bv.prototype.unset=Bv.prototype.P;Bv.prototype.changed=Bv.prototype.u;Bv.prototype.dispatchEvent=Bv.prototype.b;Bv.prototype.getRevision=Bv.prototype.K;Bv.prototype.on=Bv.prototype.I;Bv.prototype.once=Bv.prototype.L;Bv.prototype.un=Bv.prototype.J;Bv.prototype.unByKey=Bv.prototype.M;Fv.prototype.setRenderReprojectionEdges=Fv.prototype.zb;Fv.prototype.setTileGridForProjection=Fv.prototype.Ab;
+Fv.prototype.getTileLoadFunction=Fv.prototype.fb;Fv.prototype.getTileUrlFunction=Fv.prototype.gb;Fv.prototype.getUrls=Fv.prototype.hb;Fv.prototype.setTileLoadFunction=Fv.prototype.kb;Fv.prototype.setTileUrlFunction=Fv.prototype.Qa;Fv.prototype.setUrl=Fv.prototype.Va;Fv.prototype.setUrls=Fv.prototype.bb;Fv.prototype.getTileGrid=Fv.prototype.Na;Fv.prototype.refresh=Fv.prototype.sa;Fv.prototype.getAttributions=Fv.prototype.wa;Fv.prototype.getLogo=Fv.prototype.ua;Fv.prototype.getProjection=Fv.prototype.xa;
+Fv.prototype.getState=Fv.prototype.V;Fv.prototype.setAttributions=Fv.prototype.oa;Fv.prototype.get=Fv.prototype.get;Fv.prototype.getKeys=Fv.prototype.N;Fv.prototype.getProperties=Fv.prototype.O;Fv.prototype.set=Fv.prototype.set;Fv.prototype.setProperties=Fv.prototype.G;Fv.prototype.unset=Fv.prototype.P;Fv.prototype.changed=Fv.prototype.u;Fv.prototype.dispatchEvent=Fv.prototype.b;Fv.prototype.getRevision=Fv.prototype.K;Fv.prototype.on=Fv.prototype.I;Fv.prototype.once=Fv.prototype.L;
+Fv.prototype.un=Fv.prototype.J;Fv.prototype.unByKey=Fv.prototype.M;Hv.prototype.getAttributions=Hv.prototype.wa;Hv.prototype.getLogo=Hv.prototype.ua;Hv.prototype.getProjection=Hv.prototype.xa;Hv.prototype.getState=Hv.prototype.V;Hv.prototype.refresh=Hv.prototype.sa;Hv.prototype.setAttributions=Hv.prototype.oa;Hv.prototype.get=Hv.prototype.get;Hv.prototype.getKeys=Hv.prototype.N;Hv.prototype.getProperties=Hv.prototype.O;Hv.prototype.set=Hv.prototype.set;Hv.prototype.setProperties=Hv.prototype.G;
+Hv.prototype.unset=Hv.prototype.P;Hv.prototype.changed=Hv.prototype.u;Hv.prototype.dispatchEvent=Hv.prototype.b;Hv.prototype.getRevision=Hv.prototype.K;Hv.prototype.on=Hv.prototype.I;Hv.prototype.once=Hv.prototype.L;Hv.prototype.un=Hv.prototype.J;Hv.prototype.unByKey=Hv.prototype.M;Mv.prototype.type=Mv.prototype.type;Mv.prototype.target=Mv.prototype.target;Mv.prototype.preventDefault=Mv.prototype.preventDefault;Mv.prototype.stopPropagation=Mv.prototype.stopPropagation;
+Rv.prototype.setRenderReprojectionEdges=Rv.prototype.zb;Rv.prototype.setTileGridForProjection=Rv.prototype.Ab;Rv.prototype.getTileLoadFunction=Rv.prototype.fb;Rv.prototype.getTileUrlFunction=Rv.prototype.gb;Rv.prototype.getUrls=Rv.prototype.hb;Rv.prototype.setTileLoadFunction=Rv.prototype.kb;Rv.prototype.setTileUrlFunction=Rv.prototype.Qa;Rv.prototype.setUrl=Rv.prototype.Va;Rv.prototype.setUrls=Rv.prototype.bb;Rv.prototype.getTileGrid=Rv.prototype.Na;Rv.prototype.refresh=Rv.prototype.sa;
+Rv.prototype.getAttributions=Rv.prototype.wa;Rv.prototype.getLogo=Rv.prototype.ua;Rv.prototype.getProjection=Rv.prototype.xa;Rv.prototype.getState=Rv.prototype.V;Rv.prototype.setAttributions=Rv.prototype.oa;Rv.prototype.get=Rv.prototype.get;Rv.prototype.getKeys=Rv.prototype.N;Rv.prototype.getProperties=Rv.prototype.O;Rv.prototype.set=Rv.prototype.set;Rv.prototype.setProperties=Rv.prototype.G;Rv.prototype.unset=Rv.prototype.P;Rv.prototype.changed=Rv.prototype.u;Rv.prototype.dispatchEvent=Rv.prototype.b;
+Rv.prototype.getRevision=Rv.prototype.K;Rv.prototype.on=Rv.prototype.I;Rv.prototype.once=Rv.prototype.L;Rv.prototype.un=Rv.prototype.J;Rv.prototype.unByKey=Rv.prototype.M;Tv.prototype.setRenderReprojectionEdges=Tv.prototype.zb;Tv.prototype.setTileGridForProjection=Tv.prototype.Ab;Tv.prototype.getTileLoadFunction=Tv.prototype.fb;Tv.prototype.getTileUrlFunction=Tv.prototype.gb;Tv.prototype.getUrls=Tv.prototype.hb;Tv.prototype.setTileLoadFunction=Tv.prototype.kb;Tv.prototype.setTileUrlFunction=Tv.prototype.Qa;
+Tv.prototype.setUrl=Tv.prototype.Va;Tv.prototype.setUrls=Tv.prototype.bb;Tv.prototype.getTileGrid=Tv.prototype.Na;Tv.prototype.refresh=Tv.prototype.sa;Tv.prototype.getAttributions=Tv.prototype.wa;Tv.prototype.getLogo=Tv.prototype.ua;Tv.prototype.getProjection=Tv.prototype.xa;Tv.prototype.getState=Tv.prototype.V;Tv.prototype.setAttributions=Tv.prototype.oa;Tv.prototype.get=Tv.prototype.get;Tv.prototype.getKeys=Tv.prototype.N;Tv.prototype.getProperties=Tv.prototype.O;Tv.prototype.set=Tv.prototype.set;
+Tv.prototype.setProperties=Tv.prototype.G;Tv.prototype.unset=Tv.prototype.P;Tv.prototype.changed=Tv.prototype.u;Tv.prototype.dispatchEvent=Tv.prototype.b;Tv.prototype.getRevision=Tv.prototype.K;Tv.prototype.on=Tv.prototype.I;Tv.prototype.once=Tv.prototype.L;Tv.prototype.un=Tv.prototype.J;Tv.prototype.unByKey=Tv.prototype.M;Vv.prototype.getTileGrid=Vv.prototype.Na;Vv.prototype.refresh=Vv.prototype.sa;Vv.prototype.getAttributions=Vv.prototype.wa;Vv.prototype.getLogo=Vv.prototype.ua;
+Vv.prototype.getProjection=Vv.prototype.xa;Vv.prototype.getState=Vv.prototype.V;Vv.prototype.setAttributions=Vv.prototype.oa;Vv.prototype.get=Vv.prototype.get;Vv.prototype.getKeys=Vv.prototype.N;Vv.prototype.getProperties=Vv.prototype.O;Vv.prototype.set=Vv.prototype.set;Vv.prototype.setProperties=Vv.prototype.G;Vv.prototype.unset=Vv.prototype.P;Vv.prototype.changed=Vv.prototype.u;Vv.prototype.dispatchEvent=Vv.prototype.b;Vv.prototype.getRevision=Vv.prototype.K;Vv.prototype.on=Vv.prototype.I;
+Vv.prototype.once=Vv.prototype.L;Vv.prototype.un=Vv.prototype.J;Vv.prototype.unByKey=Vv.prototype.M;Wv.prototype.setRenderReprojectionEdges=Wv.prototype.zb;Wv.prototype.setTileGridForProjection=Wv.prototype.Ab;Wv.prototype.getTileLoadFunction=Wv.prototype.fb;Wv.prototype.getTileUrlFunction=Wv.prototype.gb;Wv.prototype.getUrls=Wv.prototype.hb;Wv.prototype.setTileLoadFunction=Wv.prototype.kb;Wv.prototype.setTileUrlFunction=Wv.prototype.Qa;Wv.prototype.setUrl=Wv.prototype.Va;Wv.prototype.setUrls=Wv.prototype.bb;
+Wv.prototype.getTileGrid=Wv.prototype.Na;Wv.prototype.refresh=Wv.prototype.sa;Wv.prototype.getAttributions=Wv.prototype.wa;Wv.prototype.getLogo=Wv.prototype.ua;Wv.prototype.getProjection=Wv.prototype.xa;Wv.prototype.getState=Wv.prototype.V;Wv.prototype.setAttributions=Wv.prototype.oa;Wv.prototype.get=Wv.prototype.get;Wv.prototype.getKeys=Wv.prototype.N;Wv.prototype.getProperties=Wv.prototype.O;Wv.prototype.set=Wv.prototype.set;Wv.prototype.setProperties=Wv.prototype.G;Wv.prototype.unset=Wv.prototype.P;
+Wv.prototype.changed=Wv.prototype.u;Wv.prototype.dispatchEvent=Wv.prototype.b;Wv.prototype.getRevision=Wv.prototype.K;Wv.prototype.on=Wv.prototype.I;Wv.prototype.once=Wv.prototype.L;Wv.prototype.un=Wv.prototype.J;Wv.prototype.unByKey=Wv.prototype.M;Df.prototype.type=Df.prototype.type;Df.prototype.target=Df.prototype.target;Df.prototype.preventDefault=Df.prototype.preventDefault;Df.prototype.stopPropagation=Df.prototype.stopPropagation;Xv.prototype.getTileGrid=Xv.prototype.Na;
+Xv.prototype.refresh=Xv.prototype.sa;Xv.prototype.getAttributions=Xv.prototype.wa;Xv.prototype.getLogo=Xv.prototype.ua;Xv.prototype.getProjection=Xv.prototype.xa;Xv.prototype.getState=Xv.prototype.V;Xv.prototype.setAttributions=Xv.prototype.oa;Xv.prototype.get=Xv.prototype.get;Xv.prototype.getKeys=Xv.prototype.N;Xv.prototype.getProperties=Xv.prototype.O;Xv.prototype.set=Xv.prototype.set;Xv.prototype.setProperties=Xv.prototype.G;Xv.prototype.unset=Xv.prototype.P;Xv.prototype.changed=Xv.prototype.u;
+Xv.prototype.dispatchEvent=Xv.prototype.b;Xv.prototype.getRevision=Xv.prototype.K;Xv.prototype.on=Xv.prototype.I;Xv.prototype.once=Xv.prototype.L;Xv.prototype.un=Xv.prototype.J;Xv.prototype.unByKey=Xv.prototype.M;aw.prototype.setRenderReprojectionEdges=aw.prototype.zb;aw.prototype.setTileGridForProjection=aw.prototype.Ab;aw.prototype.getTileLoadFunction=aw.prototype.fb;aw.prototype.getTileUrlFunction=aw.prototype.gb;aw.prototype.getUrls=aw.prototype.hb;aw.prototype.setTileLoadFunction=aw.prototype.kb;
+aw.prototype.setTileUrlFunction=aw.prototype.Qa;aw.prototype.setUrl=aw.prototype.Va;aw.prototype.setUrls=aw.prototype.bb;aw.prototype.getTileGrid=aw.prototype.Na;aw.prototype.refresh=aw.prototype.sa;aw.prototype.getAttributions=aw.prototype.wa;aw.prototype.getLogo=aw.prototype.ua;aw.prototype.getProjection=aw.prototype.xa;aw.prototype.getState=aw.prototype.V;aw.prototype.setAttributions=aw.prototype.oa;aw.prototype.get=aw.prototype.get;aw.prototype.getKeys=aw.prototype.N;
+aw.prototype.getProperties=aw.prototype.O;aw.prototype.set=aw.prototype.set;aw.prototype.setProperties=aw.prototype.G;aw.prototype.unset=aw.prototype.P;aw.prototype.changed=aw.prototype.u;aw.prototype.dispatchEvent=aw.prototype.b;aw.prototype.getRevision=aw.prototype.K;aw.prototype.on=aw.prototype.I;aw.prototype.once=aw.prototype.L;aw.prototype.un=aw.prototype.J;aw.prototype.unByKey=aw.prototype.M;vl.prototype.type=vl.prototype.type;vl.prototype.target=vl.prototype.target;
+vl.prototype.preventDefault=vl.prototype.preventDefault;vl.prototype.stopPropagation=vl.prototype.stopPropagation;Kl.prototype.getTileLoadFunction=Kl.prototype.fb;Kl.prototype.getTileUrlFunction=Kl.prototype.gb;Kl.prototype.getUrls=Kl.prototype.hb;Kl.prototype.setTileLoadFunction=Kl.prototype.kb;Kl.prototype.setTileUrlFunction=Kl.prototype.Qa;Kl.prototype.setUrl=Kl.prototype.Va;Kl.prototype.setUrls=Kl.prototype.bb;Kl.prototype.getTileGrid=Kl.prototype.Na;Kl.prototype.refresh=Kl.prototype.sa;
+Kl.prototype.getAttributions=Kl.prototype.wa;Kl.prototype.getLogo=Kl.prototype.ua;Kl.prototype.getProjection=Kl.prototype.xa;Kl.prototype.getState=Kl.prototype.V;Kl.prototype.setAttributions=Kl.prototype.oa;Kl.prototype.get=Kl.prototype.get;Kl.prototype.getKeys=Kl.prototype.N;Kl.prototype.getProperties=Kl.prototype.O;Kl.prototype.set=Kl.prototype.set;Kl.prototype.setProperties=Kl.prototype.G;Kl.prototype.unset=Kl.prototype.P;Kl.prototype.changed=Kl.prototype.u;Kl.prototype.dispatchEvent=Kl.prototype.b;
+Kl.prototype.getRevision=Kl.prototype.K;Kl.prototype.on=Kl.prototype.I;Kl.prototype.once=Kl.prototype.L;Kl.prototype.un=Kl.prototype.J;Kl.prototype.unByKey=Kl.prototype.M;Z.prototype.setRenderReprojectionEdges=Z.prototype.zb;Z.prototype.setTileGridForProjection=Z.prototype.Ab;Z.prototype.getTileLoadFunction=Z.prototype.fb;Z.prototype.getTileUrlFunction=Z.prototype.gb;Z.prototype.getUrls=Z.prototype.hb;Z.prototype.setTileLoadFunction=Z.prototype.kb;Z.prototype.setTileUrlFunction=Z.prototype.Qa;
+Z.prototype.setUrl=Z.prototype.Va;Z.prototype.setUrls=Z.prototype.bb;Z.prototype.getTileGrid=Z.prototype.Na;Z.prototype.refresh=Z.prototype.sa;Z.prototype.getAttributions=Z.prototype.wa;Z.prototype.getLogo=Z.prototype.ua;Z.prototype.getProjection=Z.prototype.xa;Z.prototype.getState=Z.prototype.V;Z.prototype.setAttributions=Z.prototype.oa;Z.prototype.get=Z.prototype.get;Z.prototype.getKeys=Z.prototype.N;Z.prototype.getProperties=Z.prototype.O;Z.prototype.set=Z.prototype.set;
+Z.prototype.setProperties=Z.prototype.G;Z.prototype.unset=Z.prototype.P;Z.prototype.changed=Z.prototype.u;Z.prototype.dispatchEvent=Z.prototype.b;Z.prototype.getRevision=Z.prototype.K;Z.prototype.on=Z.prototype.I;Z.prototype.once=Z.prototype.L;Z.prototype.un=Z.prototype.J;Z.prototype.unByKey=Z.prototype.M;iw.prototype.setRenderReprojectionEdges=iw.prototype.zb;iw.prototype.setTileGridForProjection=iw.prototype.Ab;iw.prototype.getTileLoadFunction=iw.prototype.fb;iw.prototype.getTileUrlFunction=iw.prototype.gb;
+iw.prototype.getUrls=iw.prototype.hb;iw.prototype.setTileLoadFunction=iw.prototype.kb;iw.prototype.setTileUrlFunction=iw.prototype.Qa;iw.prototype.setUrl=iw.prototype.Va;iw.prototype.setUrls=iw.prototype.bb;iw.prototype.getTileGrid=iw.prototype.Na;iw.prototype.refresh=iw.prototype.sa;iw.prototype.getAttributions=iw.prototype.wa;iw.prototype.getLogo=iw.prototype.ua;iw.prototype.getProjection=iw.prototype.xa;iw.prototype.getState=iw.prototype.V;iw.prototype.setAttributions=iw.prototype.oa;
+iw.prototype.get=iw.prototype.get;iw.prototype.getKeys=iw.prototype.N;iw.prototype.getProperties=iw.prototype.O;iw.prototype.set=iw.prototype.set;iw.prototype.setProperties=iw.prototype.G;iw.prototype.unset=iw.prototype.P;iw.prototype.changed=iw.prototype.u;iw.prototype.dispatchEvent=iw.prototype.b;iw.prototype.getRevision=iw.prototype.K;iw.prototype.on=iw.prototype.I;iw.prototype.once=iw.prototype.L;iw.prototype.un=iw.prototype.J;iw.prototype.unByKey=iw.prototype.M;lv.prototype.getTileCoord=lv.prototype.i;
+lv.prototype.load=lv.prototype.load;th.prototype.changed=th.prototype.u;th.prototype.dispatchEvent=th.prototype.b;th.prototype.getRevision=th.prototype.K;th.prototype.on=th.prototype.I;th.prototype.once=th.prototype.L;th.prototype.un=th.prototype.J;th.prototype.unByKey=th.prototype.M;Gm.prototype.changed=Gm.prototype.u;Gm.prototype.dispatchEvent=Gm.prototype.b;Gm.prototype.getRevision=Gm.prototype.K;Gm.prototype.on=Gm.prototype.I;Gm.prototype.once=Gm.prototype.L;Gm.prototype.un=Gm.prototype.J;
+Gm.prototype.unByKey=Gm.prototype.M;Jm.prototype.changed=Jm.prototype.u;Jm.prototype.dispatchEvent=Jm.prototype.b;Jm.prototype.getRevision=Jm.prototype.K;Jm.prototype.on=Jm.prototype.I;Jm.prototype.once=Jm.prototype.L;Jm.prototype.un=Jm.prototype.J;Jm.prototype.unByKey=Jm.prototype.M;Pm.prototype.changed=Pm.prototype.u;Pm.prototype.dispatchEvent=Pm.prototype.b;Pm.prototype.getRevision=Pm.prototype.K;Pm.prototype.on=Pm.prototype.I;Pm.prototype.once=Pm.prototype.L;Pm.prototype.un=Pm.prototype.J;
+Pm.prototype.unByKey=Pm.prototype.M;Rm.prototype.changed=Rm.prototype.u;Rm.prototype.dispatchEvent=Rm.prototype.b;Rm.prototype.getRevision=Rm.prototype.K;Rm.prototype.on=Rm.prototype.I;Rm.prototype.once=Rm.prototype.L;Rm.prototype.un=Rm.prototype.J;Rm.prototype.unByKey=Rm.prototype.M;Sl.prototype.changed=Sl.prototype.u;Sl.prototype.dispatchEvent=Sl.prototype.b;Sl.prototype.getRevision=Sl.prototype.K;Sl.prototype.on=Sl.prototype.I;Sl.prototype.once=Sl.prototype.L;Sl.prototype.un=Sl.prototype.J;
+Sl.prototype.unByKey=Sl.prototype.M;Tl.prototype.changed=Tl.prototype.u;Tl.prototype.dispatchEvent=Tl.prototype.b;Tl.prototype.getRevision=Tl.prototype.K;Tl.prototype.on=Tl.prototype.I;Tl.prototype.once=Tl.prototype.L;Tl.prototype.un=Tl.prototype.J;Tl.prototype.unByKey=Tl.prototype.M;Ul.prototype.changed=Ul.prototype.u;Ul.prototype.dispatchEvent=Ul.prototype.b;Ul.prototype.getRevision=Ul.prototype.K;Ul.prototype.on=Ul.prototype.I;Ul.prototype.once=Ul.prototype.L;Ul.prototype.un=Ul.prototype.J;
+Ul.prototype.unByKey=Ul.prototype.M;Wl.prototype.changed=Wl.prototype.u;Wl.prototype.dispatchEvent=Wl.prototype.b;Wl.prototype.getRevision=Wl.prototype.K;Wl.prototype.on=Wl.prototype.I;Wl.prototype.once=Wl.prototype.L;Wl.prototype.un=Wl.prototype.J;Wl.prototype.unByKey=Wl.prototype.M;Jj.prototype.changed=Jj.prototype.u;Jj.prototype.dispatchEvent=Jj.prototype.b;Jj.prototype.getRevision=Jj.prototype.K;Jj.prototype.on=Jj.prototype.I;Jj.prototype.once=Jj.prototype.L;Jj.prototype.un=Jj.prototype.J;
+Jj.prototype.unByKey=Jj.prototype.M;Al.prototype.changed=Al.prototype.u;Al.prototype.dispatchEvent=Al.prototype.b;Al.prototype.getRevision=Al.prototype.K;Al.prototype.on=Al.prototype.I;Al.prototype.once=Al.prototype.L;Al.prototype.un=Al.prototype.J;Al.prototype.unByKey=Al.prototype.M;Bl.prototype.changed=Bl.prototype.u;Bl.prototype.dispatchEvent=Bl.prototype.b;Bl.prototype.getRevision=Bl.prototype.K;Bl.prototype.on=Bl.prototype.I;Bl.prototype.once=Bl.prototype.L;Bl.prototype.un=Bl.prototype.J;
+Bl.prototype.unByKey=Bl.prototype.M;Dl.prototype.changed=Dl.prototype.u;Dl.prototype.dispatchEvent=Dl.prototype.b;Dl.prototype.getRevision=Dl.prototype.K;Dl.prototype.on=Dl.prototype.I;Dl.prototype.once=Dl.prototype.L;Dl.prototype.un=Dl.prototype.J;Dl.prototype.unByKey=Dl.prototype.M;Ol.prototype.changed=Ol.prototype.u;Ol.prototype.dispatchEvent=Ol.prototype.b;Ol.prototype.getRevision=Ol.prototype.K;Ol.prototype.on=Ol.prototype.I;Ol.prototype.once=Ol.prototype.L;Ol.prototype.un=Ol.prototype.J;
+Ol.prototype.unByKey=Ol.prototype.M;lh.prototype.type=lh.prototype.type;lh.prototype.target=lh.prototype.target;lh.prototype.preventDefault=lh.prototype.preventDefault;lh.prototype.stopPropagation=lh.prototype.stopPropagation;Wf.prototype.type=Wf.prototype.type;Wf.prototype.target=Wf.prototype.target;Wf.prototype.preventDefault=Wf.prototype.preventDefault;Wf.prototype.stopPropagation=Wf.prototype.stopPropagation;ih.prototype.get=ih.prototype.get;ih.prototype.getKeys=ih.prototype.N;
+ih.prototype.getProperties=ih.prototype.O;ih.prototype.set=ih.prototype.set;ih.prototype.setProperties=ih.prototype.G;ih.prototype.unset=ih.prototype.P;ih.prototype.changed=ih.prototype.u;ih.prototype.dispatchEvent=ih.prototype.b;ih.prototype.getRevision=ih.prototype.K;ih.prototype.on=ih.prototype.I;ih.prototype.once=ih.prototype.L;ih.prototype.un=ih.prototype.J;ih.prototype.unByKey=ih.prototype.M;mh.prototype.getExtent=mh.prototype.H;mh.prototype.getMaxResolution=mh.prototype.Nb;
+mh.prototype.getMinResolution=mh.prototype.Ob;mh.prototype.getOpacity=mh.prototype.Pb;mh.prototype.getVisible=mh.prototype.xb;mh.prototype.getZIndex=mh.prototype.Qb;mh.prototype.setExtent=mh.prototype.fc;mh.prototype.setMaxResolution=mh.prototype.nc;mh.prototype.setMinResolution=mh.prototype.oc;mh.prototype.setOpacity=mh.prototype.gc;mh.prototype.setVisible=mh.prototype.hc;mh.prototype.setZIndex=mh.prototype.ic;mh.prototype.get=mh.prototype.get;mh.prototype.getKeys=mh.prototype.N;
+mh.prototype.getProperties=mh.prototype.O;mh.prototype.set=mh.prototype.set;mh.prototype.setProperties=mh.prototype.G;mh.prototype.unset=mh.prototype.P;mh.prototype.changed=mh.prototype.u;mh.prototype.dispatchEvent=mh.prototype.b;mh.prototype.getRevision=mh.prototype.K;mh.prototype.on=mh.prototype.I;mh.prototype.once=mh.prototype.L;mh.prototype.un=mh.prototype.J;mh.prototype.unByKey=mh.prototype.M;G.prototype.setMap=G.prototype.setMap;G.prototype.setSource=G.prototype.Fc;G.prototype.getExtent=G.prototype.H;
+G.prototype.getMaxResolution=G.prototype.Nb;G.prototype.getMinResolution=G.prototype.Ob;G.prototype.getOpacity=G.prototype.Pb;G.prototype.getVisible=G.prototype.xb;G.prototype.getZIndex=G.prototype.Qb;G.prototype.setExtent=G.prototype.fc;G.prototype.setMaxResolution=G.prototype.nc;G.prototype.setMinResolution=G.prototype.oc;G.prototype.setOpacity=G.prototype.gc;G.prototype.setVisible=G.prototype.hc;G.prototype.setZIndex=G.prototype.ic;G.prototype.get=G.prototype.get;G.prototype.getKeys=G.prototype.N;
+G.prototype.getProperties=G.prototype.O;G.prototype.set=G.prototype.set;G.prototype.setProperties=G.prototype.G;G.prototype.unset=G.prototype.P;G.prototype.changed=G.prototype.u;G.prototype.dispatchEvent=G.prototype.b;G.prototype.getRevision=G.prototype.K;G.prototype.on=G.prototype.I;G.prototype.once=G.prototype.L;G.prototype.un=G.prototype.J;G.prototype.unByKey=G.prototype.M;V.prototype.getSource=V.prototype.ha;V.prototype.getStyle=V.prototype.C;V.prototype.getStyleFunction=V.prototype.D;
+V.prototype.setStyle=V.prototype.l;V.prototype.setMap=V.prototype.setMap;V.prototype.setSource=V.prototype.Fc;V.prototype.getExtent=V.prototype.H;V.prototype.getMaxResolution=V.prototype.Nb;V.prototype.getMinResolution=V.prototype.Ob;V.prototype.getOpacity=V.prototype.Pb;V.prototype.getVisible=V.prototype.xb;V.prototype.getZIndex=V.prototype.Qb;V.prototype.setExtent=V.prototype.fc;V.prototype.setMaxResolution=V.prototype.nc;V.prototype.setMinResolution=V.prototype.oc;V.prototype.setOpacity=V.prototype.gc;
+V.prototype.setVisible=V.prototype.hc;V.prototype.setZIndex=V.prototype.ic;V.prototype.get=V.prototype.get;V.prototype.getKeys=V.prototype.N;V.prototype.getProperties=V.prototype.O;V.prototype.set=V.prototype.set;V.prototype.setProperties=V.prototype.G;V.prototype.unset=V.prototype.P;V.prototype.changed=V.prototype.u;V.prototype.dispatchEvent=V.prototype.b;V.prototype.getRevision=V.prototype.K;V.prototype.on=V.prototype.I;V.prototype.once=V.prototype.L;V.prototype.un=V.prototype.J;
+V.prototype.unByKey=V.prototype.M;cj.prototype.setMap=cj.prototype.setMap;cj.prototype.setSource=cj.prototype.Fc;cj.prototype.getExtent=cj.prototype.H;cj.prototype.getMaxResolution=cj.prototype.Nb;cj.prototype.getMinResolution=cj.prototype.Ob;cj.prototype.getOpacity=cj.prototype.Pb;cj.prototype.getVisible=cj.prototype.xb;cj.prototype.getZIndex=cj.prototype.Qb;cj.prototype.setExtent=cj.prototype.fc;cj.prototype.setMaxResolution=cj.prototype.nc;cj.prototype.setMinResolution=cj.prototype.oc;
+cj.prototype.setOpacity=cj.prototype.gc;cj.prototype.setVisible=cj.prototype.hc;cj.prototype.setZIndex=cj.prototype.ic;cj.prototype.get=cj.prototype.get;cj.prototype.getKeys=cj.prototype.N;cj.prototype.getProperties=cj.prototype.O;cj.prototype.set=cj.prototype.set;cj.prototype.setProperties=cj.prototype.G;cj.prototype.unset=cj.prototype.P;cj.prototype.changed=cj.prototype.u;cj.prototype.dispatchEvent=cj.prototype.b;cj.prototype.getRevision=cj.prototype.K;cj.prototype.on=cj.prototype.I;
+cj.prototype.once=cj.prototype.L;cj.prototype.un=cj.prototype.J;cj.prototype.unByKey=cj.prototype.M;Ti.prototype.getExtent=Ti.prototype.H;Ti.prototype.getMaxResolution=Ti.prototype.Nb;Ti.prototype.getMinResolution=Ti.prototype.Ob;Ti.prototype.getOpacity=Ti.prototype.Pb;Ti.prototype.getVisible=Ti.prototype.xb;Ti.prototype.getZIndex=Ti.prototype.Qb;Ti.prototype.setExtent=Ti.prototype.fc;Ti.prototype.setMaxResolution=Ti.prototype.nc;Ti.prototype.setMinResolution=Ti.prototype.oc;
+Ti.prototype.setOpacity=Ti.prototype.gc;Ti.prototype.setVisible=Ti.prototype.hc;Ti.prototype.setZIndex=Ti.prototype.ic;Ti.prototype.get=Ti.prototype.get;Ti.prototype.getKeys=Ti.prototype.N;Ti.prototype.getProperties=Ti.prototype.O;Ti.prototype.set=Ti.prototype.set;Ti.prototype.setProperties=Ti.prototype.G;Ti.prototype.unset=Ti.prototype.P;Ti.prototype.changed=Ti.prototype.u;Ti.prototype.dispatchEvent=Ti.prototype.b;Ti.prototype.getRevision=Ti.prototype.K;Ti.prototype.on=Ti.prototype.I;
+Ti.prototype.once=Ti.prototype.L;Ti.prototype.un=Ti.prototype.J;Ti.prototype.unByKey=Ti.prototype.M;dj.prototype.setMap=dj.prototype.setMap;dj.prototype.setSource=dj.prototype.Fc;dj.prototype.getExtent=dj.prototype.H;dj.prototype.getMaxResolution=dj.prototype.Nb;dj.prototype.getMinResolution=dj.prototype.Ob;dj.prototype.getOpacity=dj.prototype.Pb;dj.prototype.getVisible=dj.prototype.xb;dj.prototype.getZIndex=dj.prototype.Qb;dj.prototype.setExtent=dj.prototype.fc;dj.prototype.setMaxResolution=dj.prototype.nc;
+dj.prototype.setMinResolution=dj.prototype.oc;dj.prototype.setOpacity=dj.prototype.gc;dj.prototype.setVisible=dj.prototype.hc;dj.prototype.setZIndex=dj.prototype.ic;dj.prototype.get=dj.prototype.get;dj.prototype.getKeys=dj.prototype.N;dj.prototype.getProperties=dj.prototype.O;dj.prototype.set=dj.prototype.set;dj.prototype.setProperties=dj.prototype.G;dj.prototype.unset=dj.prototype.P;dj.prototype.changed=dj.prototype.u;dj.prototype.dispatchEvent=dj.prototype.b;dj.prototype.getRevision=dj.prototype.K;
+dj.prototype.on=dj.prototype.I;dj.prototype.once=dj.prototype.L;dj.prototype.un=dj.prototype.J;dj.prototype.unByKey=dj.prototype.M;I.prototype.getSource=I.prototype.ha;I.prototype.getStyle=I.prototype.C;I.prototype.getStyleFunction=I.prototype.D;I.prototype.setStyle=I.prototype.l;I.prototype.setMap=I.prototype.setMap;I.prototype.setSource=I.prototype.Fc;I.prototype.getExtent=I.prototype.H;I.prototype.getMaxResolution=I.prototype.Nb;I.prototype.getMinResolution=I.prototype.Ob;
+I.prototype.getOpacity=I.prototype.Pb;I.prototype.getVisible=I.prototype.xb;I.prototype.getZIndex=I.prototype.Qb;I.prototype.setExtent=I.prototype.fc;I.prototype.setMaxResolution=I.prototype.nc;I.prototype.setMinResolution=I.prototype.oc;I.prototype.setOpacity=I.prototype.gc;I.prototype.setVisible=I.prototype.hc;I.prototype.setZIndex=I.prototype.ic;I.prototype.get=I.prototype.get;I.prototype.getKeys=I.prototype.N;I.prototype.getProperties=I.prototype.O;I.prototype.set=I.prototype.set;
+I.prototype.setProperties=I.prototype.G;I.prototype.unset=I.prototype.P;I.prototype.changed=I.prototype.u;I.prototype.dispatchEvent=I.prototype.b;I.prototype.getRevision=I.prototype.K;I.prototype.on=I.prototype.I;I.prototype.once=I.prototype.L;I.prototype.un=I.prototype.J;I.prototype.unByKey=I.prototype.M;Vh.prototype.get=Vh.prototype.get;Vh.prototype.getKeys=Vh.prototype.N;Vh.prototype.getProperties=Vh.prototype.O;Vh.prototype.set=Vh.prototype.set;Vh.prototype.setProperties=Vh.prototype.G;
+Vh.prototype.unset=Vh.prototype.P;Vh.prototype.changed=Vh.prototype.u;Vh.prototype.dispatchEvent=Vh.prototype.b;Vh.prototype.getRevision=Vh.prototype.K;Vh.prototype.on=Vh.prototype.I;Vh.prototype.once=Vh.prototype.L;Vh.prototype.un=Vh.prototype.J;Vh.prototype.unByKey=Vh.prototype.M;Zh.prototype.getActive=Zh.prototype.f;Zh.prototype.getMap=Zh.prototype.l;Zh.prototype.setActive=Zh.prototype.i;Zh.prototype.get=Zh.prototype.get;Zh.prototype.getKeys=Zh.prototype.N;Zh.prototype.getProperties=Zh.prototype.O;
+Zh.prototype.set=Zh.prototype.set;Zh.prototype.setProperties=Zh.prototype.G;Zh.prototype.unset=Zh.prototype.P;Zh.prototype.changed=Zh.prototype.u;Zh.prototype.dispatchEvent=Zh.prototype.b;Zh.prototype.getRevision=Zh.prototype.K;Zh.prototype.on=Zh.prototype.I;Zh.prototype.once=Zh.prototype.L;Zh.prototype.un=Zh.prototype.J;Zh.prototype.unByKey=Zh.prototype.M;hu.prototype.getActive=hu.prototype.f;hu.prototype.getMap=hu.prototype.l;hu.prototype.setActive=hu.prototype.i;hu.prototype.get=hu.prototype.get;
+hu.prototype.getKeys=hu.prototype.N;hu.prototype.getProperties=hu.prototype.O;hu.prototype.set=hu.prototype.set;hu.prototype.setProperties=hu.prototype.G;hu.prototype.unset=hu.prototype.P;hu.prototype.changed=hu.prototype.u;hu.prototype.dispatchEvent=hu.prototype.b;hu.prototype.getRevision=hu.prototype.K;hu.prototype.on=hu.prototype.I;hu.prototype.once=hu.prototype.L;hu.prototype.un=hu.prototype.J;hu.prototype.unByKey=hu.prototype.M;ku.prototype.type=ku.prototype.type;ku.prototype.target=ku.prototype.target;
+ku.prototype.preventDefault=ku.prototype.preventDefault;ku.prototype.stopPropagation=ku.prototype.stopPropagation;xi.prototype.type=xi.prototype.type;xi.prototype.target=xi.prototype.target;xi.prototype.preventDefault=xi.prototype.preventDefault;xi.prototype.stopPropagation=xi.prototype.stopPropagation;ji.prototype.getActive=ji.prototype.f;ji.prototype.getMap=ji.prototype.l;ji.prototype.setActive=ji.prototype.i;ji.prototype.get=ji.prototype.get;ji.prototype.getKeys=ji.prototype.N;
+ji.prototype.getProperties=ji.prototype.O;ji.prototype.set=ji.prototype.set;ji.prototype.setProperties=ji.prototype.G;ji.prototype.unset=ji.prototype.P;ji.prototype.changed=ji.prototype.u;ji.prototype.dispatchEvent=ji.prototype.b;ji.prototype.getRevision=ji.prototype.K;ji.prototype.on=ji.prototype.I;ji.prototype.once=ji.prototype.L;ji.prototype.un=ji.prototype.J;ji.prototype.unByKey=ji.prototype.M;yi.prototype.getActive=yi.prototype.f;yi.prototype.getMap=yi.prototype.l;yi.prototype.setActive=yi.prototype.i;
+yi.prototype.get=yi.prototype.get;yi.prototype.getKeys=yi.prototype.N;yi.prototype.getProperties=yi.prototype.O;yi.prototype.set=yi.prototype.set;yi.prototype.setProperties=yi.prototype.G;yi.prototype.unset=yi.prototype.P;yi.prototype.changed=yi.prototype.u;yi.prototype.dispatchEvent=yi.prototype.b;yi.prototype.getRevision=yi.prototype.K;yi.prototype.on=yi.prototype.I;yi.prototype.once=yi.prototype.L;yi.prototype.un=yi.prototype.J;yi.prototype.unByKey=yi.prototype.M;mi.prototype.getActive=mi.prototype.f;
+mi.prototype.getMap=mi.prototype.l;mi.prototype.setActive=mi.prototype.i;mi.prototype.get=mi.prototype.get;mi.prototype.getKeys=mi.prototype.N;mi.prototype.getProperties=mi.prototype.O;mi.prototype.set=mi.prototype.set;mi.prototype.setProperties=mi.prototype.G;mi.prototype.unset=mi.prototype.P;mi.prototype.changed=mi.prototype.u;mi.prototype.dispatchEvent=mi.prototype.b;mi.prototype.getRevision=mi.prototype.K;mi.prototype.on=mi.prototype.I;mi.prototype.once=mi.prototype.L;mi.prototype.un=mi.prototype.J;
+mi.prototype.unByKey=mi.prototype.M;mu.prototype.getActive=mu.prototype.f;mu.prototype.getMap=mu.prototype.l;mu.prototype.setActive=mu.prototype.i;mu.prototype.get=mu.prototype.get;mu.prototype.getKeys=mu.prototype.N;mu.prototype.getProperties=mu.prototype.O;mu.prototype.set=mu.prototype.set;mu.prototype.setProperties=mu.prototype.G;mu.prototype.unset=mu.prototype.P;mu.prototype.changed=mu.prototype.u;mu.prototype.dispatchEvent=mu.prototype.b;mu.prototype.getRevision=mu.prototype.K;
+mu.prototype.on=mu.prototype.I;mu.prototype.once=mu.prototype.L;mu.prototype.un=mu.prototype.J;mu.prototype.unByKey=mu.prototype.M;qi.prototype.getActive=qi.prototype.f;qi.prototype.getMap=qi.prototype.l;qi.prototype.setActive=qi.prototype.i;qi.prototype.get=qi.prototype.get;qi.prototype.getKeys=qi.prototype.N;qi.prototype.getProperties=qi.prototype.O;qi.prototype.set=qi.prototype.set;qi.prototype.setProperties=qi.prototype.G;qi.prototype.unset=qi.prototype.P;qi.prototype.changed=qi.prototype.u;
+qi.prototype.dispatchEvent=qi.prototype.b;qi.prototype.getRevision=qi.prototype.K;qi.prototype.on=qi.prototype.I;qi.prototype.once=qi.prototype.L;qi.prototype.un=qi.prototype.J;qi.prototype.unByKey=qi.prototype.M;Di.prototype.getGeometry=Di.prototype.W;Di.prototype.getActive=Di.prototype.f;Di.prototype.getMap=Di.prototype.l;Di.prototype.setActive=Di.prototype.i;Di.prototype.get=Di.prototype.get;Di.prototype.getKeys=Di.prototype.N;Di.prototype.getProperties=Di.prototype.O;Di.prototype.set=Di.prototype.set;
+Di.prototype.setProperties=Di.prototype.G;Di.prototype.unset=Di.prototype.P;Di.prototype.changed=Di.prototype.u;Di.prototype.dispatchEvent=Di.prototype.b;Di.prototype.getRevision=Di.prototype.K;Di.prototype.on=Di.prototype.I;Di.prototype.once=Di.prototype.L;Di.prototype.un=Di.prototype.J;Di.prototype.unByKey=Di.prototype.M;qu.prototype.type=qu.prototype.type;qu.prototype.target=qu.prototype.target;qu.prototype.preventDefault=qu.prototype.preventDefault;qu.prototype.stopPropagation=qu.prototype.stopPropagation;
+ru.prototype.getActive=ru.prototype.f;ru.prototype.getMap=ru.prototype.l;ru.prototype.setActive=ru.prototype.i;ru.prototype.get=ru.prototype.get;ru.prototype.getKeys=ru.prototype.N;ru.prototype.getProperties=ru.prototype.O;ru.prototype.set=ru.prototype.set;ru.prototype.setProperties=ru.prototype.G;ru.prototype.unset=ru.prototype.P;ru.prototype.changed=ru.prototype.u;ru.prototype.dispatchEvent=ru.prototype.b;ru.prototype.getRevision=ru.prototype.K;ru.prototype.on=ru.prototype.I;ru.prototype.once=ru.prototype.L;
+ru.prototype.un=ru.prototype.J;ru.prototype.unByKey=ru.prototype.M;Ei.prototype.getActive=Ei.prototype.f;Ei.prototype.getMap=Ei.prototype.l;Ei.prototype.setActive=Ei.prototype.i;Ei.prototype.get=Ei.prototype.get;Ei.prototype.getKeys=Ei.prototype.N;Ei.prototype.getProperties=Ei.prototype.O;Ei.prototype.set=Ei.prototype.set;Ei.prototype.setProperties=Ei.prototype.G;Ei.prototype.unset=Ei.prototype.P;Ei.prototype.changed=Ei.prototype.u;Ei.prototype.dispatchEvent=Ei.prototype.b;
+Ei.prototype.getRevision=Ei.prototype.K;Ei.prototype.on=Ei.prototype.I;Ei.prototype.once=Ei.prototype.L;Ei.prototype.un=Ei.prototype.J;Ei.prototype.unByKey=Ei.prototype.M;Gi.prototype.getActive=Gi.prototype.f;Gi.prototype.getMap=Gi.prototype.l;Gi.prototype.setActive=Gi.prototype.i;Gi.prototype.get=Gi.prototype.get;Gi.prototype.getKeys=Gi.prototype.N;Gi.prototype.getProperties=Gi.prototype.O;Gi.prototype.set=Gi.prototype.set;Gi.prototype.setProperties=Gi.prototype.G;Gi.prototype.unset=Gi.prototype.P;
+Gi.prototype.changed=Gi.prototype.u;Gi.prototype.dispatchEvent=Gi.prototype.b;Gi.prototype.getRevision=Gi.prototype.K;Gi.prototype.on=Gi.prototype.I;Gi.prototype.once=Gi.prototype.L;Gi.prototype.un=Gi.prototype.J;Gi.prototype.unByKey=Gi.prototype.M;Hu.prototype.type=Hu.prototype.type;Hu.prototype.target=Hu.prototype.target;Hu.prototype.preventDefault=Hu.prototype.preventDefault;Hu.prototype.stopPropagation=Hu.prototype.stopPropagation;Iu.prototype.getActive=Iu.prototype.f;Iu.prototype.getMap=Iu.prototype.l;
+Iu.prototype.setActive=Iu.prototype.i;Iu.prototype.get=Iu.prototype.get;Iu.prototype.getKeys=Iu.prototype.N;Iu.prototype.getProperties=Iu.prototype.O;Iu.prototype.set=Iu.prototype.set;Iu.prototype.setProperties=Iu.prototype.G;Iu.prototype.unset=Iu.prototype.P;Iu.prototype.changed=Iu.prototype.u;Iu.prototype.dispatchEvent=Iu.prototype.b;Iu.prototype.getRevision=Iu.prototype.K;Iu.prototype.on=Iu.prototype.I;Iu.prototype.once=Iu.prototype.L;Iu.prototype.un=Iu.prototype.J;Iu.prototype.unByKey=Iu.prototype.M;
+Ii.prototype.getActive=Ii.prototype.f;Ii.prototype.getMap=Ii.prototype.l;Ii.prototype.setActive=Ii.prototype.i;Ii.prototype.get=Ii.prototype.get;Ii.prototype.getKeys=Ii.prototype.N;Ii.prototype.getProperties=Ii.prototype.O;Ii.prototype.set=Ii.prototype.set;Ii.prototype.setProperties=Ii.prototype.G;Ii.prototype.unset=Ii.prototype.P;Ii.prototype.changed=Ii.prototype.u;Ii.prototype.dispatchEvent=Ii.prototype.b;Ii.prototype.getRevision=Ii.prototype.K;Ii.prototype.on=Ii.prototype.I;Ii.prototype.once=Ii.prototype.L;
+Ii.prototype.un=Ii.prototype.J;Ii.prototype.unByKey=Ii.prototype.M;Ki.prototype.getActive=Ki.prototype.f;Ki.prototype.getMap=Ki.prototype.l;Ki.prototype.setActive=Ki.prototype.i;Ki.prototype.get=Ki.prototype.get;Ki.prototype.getKeys=Ki.prototype.N;Ki.prototype.getProperties=Ki.prototype.O;Ki.prototype.set=Ki.prototype.set;Ki.prototype.setProperties=Ki.prototype.G;Ki.prototype.unset=Ki.prototype.P;Ki.prototype.changed=Ki.prototype.u;Ki.prototype.dispatchEvent=Ki.prototype.b;
+Ki.prototype.getRevision=Ki.prototype.K;Ki.prototype.on=Ki.prototype.I;Ki.prototype.once=Ki.prototype.L;Ki.prototype.un=Ki.prototype.J;Ki.prototype.unByKey=Ki.prototype.M;Oi.prototype.getActive=Oi.prototype.f;Oi.prototype.getMap=Oi.prototype.l;Oi.prototype.setActive=Oi.prototype.i;Oi.prototype.get=Oi.prototype.get;Oi.prototype.getKeys=Oi.prototype.N;Oi.prototype.getProperties=Oi.prototype.O;Oi.prototype.set=Oi.prototype.set;Oi.prototype.setProperties=Oi.prototype.G;Oi.prototype.unset=Oi.prototype.P;
+Oi.prototype.changed=Oi.prototype.u;Oi.prototype.dispatchEvent=Oi.prototype.b;Oi.prototype.getRevision=Oi.prototype.K;Oi.prototype.on=Oi.prototype.I;Oi.prototype.once=Oi.prototype.L;Oi.prototype.un=Oi.prototype.J;Oi.prototype.unByKey=Oi.prototype.M;Vu.prototype.type=Vu.prototype.type;Vu.prototype.target=Vu.prototype.target;Vu.prototype.preventDefault=Vu.prototype.preventDefault;Vu.prototype.stopPropagation=Vu.prototype.stopPropagation;Wu.prototype.getActive=Wu.prototype.f;Wu.prototype.getMap=Wu.prototype.l;
+Wu.prototype.setActive=Wu.prototype.i;Wu.prototype.get=Wu.prototype.get;Wu.prototype.getKeys=Wu.prototype.N;Wu.prototype.getProperties=Wu.prototype.O;Wu.prototype.set=Wu.prototype.set;Wu.prototype.setProperties=Wu.prototype.G;Wu.prototype.unset=Wu.prototype.P;Wu.prototype.changed=Wu.prototype.u;Wu.prototype.dispatchEvent=Wu.prototype.b;Wu.prototype.getRevision=Wu.prototype.K;Wu.prototype.on=Wu.prototype.I;Wu.prototype.once=Wu.prototype.L;Wu.prototype.un=Wu.prototype.J;Wu.prototype.unByKey=Wu.prototype.M;
+Zu.prototype.getActive=Zu.prototype.f;Zu.prototype.getMap=Zu.prototype.l;Zu.prototype.setActive=Zu.prototype.i;Zu.prototype.get=Zu.prototype.get;Zu.prototype.getKeys=Zu.prototype.N;Zu.prototype.getProperties=Zu.prototype.O;Zu.prototype.set=Zu.prototype.set;Zu.prototype.setProperties=Zu.prototype.G;Zu.prototype.unset=Zu.prototype.P;Zu.prototype.changed=Zu.prototype.u;Zu.prototype.dispatchEvent=Zu.prototype.b;Zu.prototype.getRevision=Zu.prototype.K;Zu.prototype.on=Zu.prototype.I;Zu.prototype.once=Zu.prototype.L;
+Zu.prototype.un=Zu.prototype.J;Zu.prototype.unByKey=Zu.prototype.M;cv.prototype.type=cv.prototype.type;cv.prototype.target=cv.prototype.target;cv.prototype.preventDefault=cv.prototype.preventDefault;cv.prototype.stopPropagation=cv.prototype.stopPropagation;dv.prototype.getActive=dv.prototype.f;dv.prototype.getMap=dv.prototype.l;dv.prototype.setActive=dv.prototype.i;dv.prototype.get=dv.prototype.get;dv.prototype.getKeys=dv.prototype.N;dv.prototype.getProperties=dv.prototype.O;dv.prototype.set=dv.prototype.set;
+dv.prototype.setProperties=dv.prototype.G;dv.prototype.unset=dv.prototype.P;dv.prototype.changed=dv.prototype.u;dv.prototype.dispatchEvent=dv.prototype.b;dv.prototype.getRevision=dv.prototype.K;dv.prototype.on=dv.prototype.I;dv.prototype.once=dv.prototype.L;dv.prototype.un=dv.prototype.J;dv.prototype.unByKey=dv.prototype.M;Tc.prototype.get=Tc.prototype.get;Tc.prototype.getKeys=Tc.prototype.N;Tc.prototype.getProperties=Tc.prototype.O;Tc.prototype.set=Tc.prototype.set;Tc.prototype.setProperties=Tc.prototype.G;
+Tc.prototype.unset=Tc.prototype.P;Tc.prototype.changed=Tc.prototype.u;Tc.prototype.dispatchEvent=Tc.prototype.b;Tc.prototype.getRevision=Tc.prototype.K;Tc.prototype.on=Tc.prototype.I;Tc.prototype.once=Tc.prototype.L;Tc.prototype.un=Tc.prototype.J;Tc.prototype.unByKey=Tc.prototype.M;hd.prototype.getClosestPoint=hd.prototype.vb;hd.prototype.getExtent=hd.prototype.H;hd.prototype.rotate=hd.prototype.rotate;hd.prototype.simplify=hd.prototype.Bb;hd.prototype.transform=hd.prototype.jb;hd.prototype.get=hd.prototype.get;
+hd.prototype.getKeys=hd.prototype.N;hd.prototype.getProperties=hd.prototype.O;hd.prototype.set=hd.prototype.set;hd.prototype.setProperties=hd.prototype.G;hd.prototype.unset=hd.prototype.P;hd.prototype.changed=hd.prototype.u;hd.prototype.dispatchEvent=hd.prototype.b;hd.prototype.getRevision=hd.prototype.K;hd.prototype.on=hd.prototype.I;hd.prototype.once=hd.prototype.L;hd.prototype.un=hd.prototype.J;hd.prototype.unByKey=hd.prototype.M;Vt.prototype.getFirstCoordinate=Vt.prototype.Ib;
+Vt.prototype.getLastCoordinate=Vt.prototype.Jb;Vt.prototype.getLayout=Vt.prototype.Kb;Vt.prototype.rotate=Vt.prototype.rotate;Vt.prototype.getClosestPoint=Vt.prototype.vb;Vt.prototype.getExtent=Vt.prototype.H;Vt.prototype.simplify=Vt.prototype.Bb;Vt.prototype.get=Vt.prototype.get;Vt.prototype.getKeys=Vt.prototype.N;Vt.prototype.getProperties=Vt.prototype.O;Vt.prototype.set=Vt.prototype.set;Vt.prototype.setProperties=Vt.prototype.G;Vt.prototype.unset=Vt.prototype.P;Vt.prototype.changed=Vt.prototype.u;
+Vt.prototype.dispatchEvent=Vt.prototype.b;Vt.prototype.getRevision=Vt.prototype.K;Vt.prototype.on=Vt.prototype.I;Vt.prototype.once=Vt.prototype.L;Vt.prototype.un=Vt.prototype.J;Vt.prototype.unByKey=Vt.prototype.M;Ln.prototype.getClosestPoint=Ln.prototype.vb;Ln.prototype.getExtent=Ln.prototype.H;Ln.prototype.rotate=Ln.prototype.rotate;Ln.prototype.simplify=Ln.prototype.Bb;Ln.prototype.transform=Ln.prototype.jb;Ln.prototype.get=Ln.prototype.get;Ln.prototype.getKeys=Ln.prototype.N;
+Ln.prototype.getProperties=Ln.prototype.O;Ln.prototype.set=Ln.prototype.set;Ln.prototype.setProperties=Ln.prototype.G;Ln.prototype.unset=Ln.prototype.P;Ln.prototype.changed=Ln.prototype.u;Ln.prototype.dispatchEvent=Ln.prototype.b;Ln.prototype.getRevision=Ln.prototype.K;Ln.prototype.on=Ln.prototype.I;Ln.prototype.once=Ln.prototype.L;Ln.prototype.un=Ln.prototype.J;Ln.prototype.unByKey=Ln.prototype.M;zd.prototype.getFirstCoordinate=zd.prototype.Ib;zd.prototype.getLastCoordinate=zd.prototype.Jb;
+zd.prototype.getLayout=zd.prototype.Kb;zd.prototype.rotate=zd.prototype.rotate;zd.prototype.getClosestPoint=zd.prototype.vb;zd.prototype.getExtent=zd.prototype.H;zd.prototype.simplify=zd.prototype.Bb;zd.prototype.transform=zd.prototype.jb;zd.prototype.get=zd.prototype.get;zd.prototype.getKeys=zd.prototype.N;zd.prototype.getProperties=zd.prototype.O;zd.prototype.set=zd.prototype.set;zd.prototype.setProperties=zd.prototype.G;zd.prototype.unset=zd.prototype.P;zd.prototype.changed=zd.prototype.u;
+zd.prototype.dispatchEvent=zd.prototype.b;zd.prototype.getRevision=zd.prototype.K;zd.prototype.on=zd.prototype.I;zd.prototype.once=zd.prototype.L;zd.prototype.un=zd.prototype.J;zd.prototype.unByKey=zd.prototype.M;R.prototype.getFirstCoordinate=R.prototype.Ib;R.prototype.getLastCoordinate=R.prototype.Jb;R.prototype.getLayout=R.prototype.Kb;R.prototype.rotate=R.prototype.rotate;R.prototype.getClosestPoint=R.prototype.vb;R.prototype.getExtent=R.prototype.H;R.prototype.simplify=R.prototype.Bb;
+R.prototype.transform=R.prototype.jb;R.prototype.get=R.prototype.get;R.prototype.getKeys=R.prototype.N;R.prototype.getProperties=R.prototype.O;R.prototype.set=R.prototype.set;R.prototype.setProperties=R.prototype.G;R.prototype.unset=R.prototype.P;R.prototype.changed=R.prototype.u;R.prototype.dispatchEvent=R.prototype.b;R.prototype.getRevision=R.prototype.K;R.prototype.on=R.prototype.I;R.prototype.once=R.prototype.L;R.prototype.un=R.prototype.J;R.prototype.unByKey=R.prototype.M;
+S.prototype.getFirstCoordinate=S.prototype.Ib;S.prototype.getLastCoordinate=S.prototype.Jb;S.prototype.getLayout=S.prototype.Kb;S.prototype.rotate=S.prototype.rotate;S.prototype.getClosestPoint=S.prototype.vb;S.prototype.getExtent=S.prototype.H;S.prototype.simplify=S.prototype.Bb;S.prototype.transform=S.prototype.jb;S.prototype.get=S.prototype.get;S.prototype.getKeys=S.prototype.N;S.prototype.getProperties=S.prototype.O;S.prototype.set=S.prototype.set;S.prototype.setProperties=S.prototype.G;
+S.prototype.unset=S.prototype.P;S.prototype.changed=S.prototype.u;S.prototype.dispatchEvent=S.prototype.b;S.prototype.getRevision=S.prototype.K;S.prototype.on=S.prototype.I;S.prototype.once=S.prototype.L;S.prototype.un=S.prototype.J;S.prototype.unByKey=S.prototype.M;Bn.prototype.getFirstCoordinate=Bn.prototype.Ib;Bn.prototype.getLastCoordinate=Bn.prototype.Jb;Bn.prototype.getLayout=Bn.prototype.Kb;Bn.prototype.rotate=Bn.prototype.rotate;Bn.prototype.getClosestPoint=Bn.prototype.vb;
+Bn.prototype.getExtent=Bn.prototype.H;Bn.prototype.simplify=Bn.prototype.Bb;Bn.prototype.transform=Bn.prototype.jb;Bn.prototype.get=Bn.prototype.get;Bn.prototype.getKeys=Bn.prototype.N;Bn.prototype.getProperties=Bn.prototype.O;Bn.prototype.set=Bn.prototype.set;Bn.prototype.setProperties=Bn.prototype.G;Bn.prototype.unset=Bn.prototype.P;Bn.prototype.changed=Bn.prototype.u;Bn.prototype.dispatchEvent=Bn.prototype.b;Bn.prototype.getRevision=Bn.prototype.K;Bn.prototype.on=Bn.prototype.I;
+Bn.prototype.once=Bn.prototype.L;Bn.prototype.un=Bn.prototype.J;Bn.prototype.unByKey=Bn.prototype.M;T.prototype.getFirstCoordinate=T.prototype.Ib;T.prototype.getLastCoordinate=T.prototype.Jb;T.prototype.getLayout=T.prototype.Kb;T.prototype.rotate=T.prototype.rotate;T.prototype.getClosestPoint=T.prototype.vb;T.prototype.getExtent=T.prototype.H;T.prototype.simplify=T.prototype.Bb;T.prototype.transform=T.prototype.jb;T.prototype.get=T.prototype.get;T.prototype.getKeys=T.prototype.N;
+T.prototype.getProperties=T.prototype.O;T.prototype.set=T.prototype.set;T.prototype.setProperties=T.prototype.G;T.prototype.unset=T.prototype.P;T.prototype.changed=T.prototype.u;T.prototype.dispatchEvent=T.prototype.b;T.prototype.getRevision=T.prototype.K;T.prototype.on=T.prototype.I;T.prototype.once=T.prototype.L;T.prototype.un=T.prototype.J;T.prototype.unByKey=T.prototype.M;C.prototype.getFirstCoordinate=C.prototype.Ib;C.prototype.getLastCoordinate=C.prototype.Jb;C.prototype.getLayout=C.prototype.Kb;
+C.prototype.rotate=C.prototype.rotate;C.prototype.getClosestPoint=C.prototype.vb;C.prototype.getExtent=C.prototype.H;C.prototype.simplify=C.prototype.Bb;C.prototype.transform=C.prototype.jb;C.prototype.get=C.prototype.get;C.prototype.getKeys=C.prototype.N;C.prototype.getProperties=C.prototype.O;C.prototype.set=C.prototype.set;C.prototype.setProperties=C.prototype.G;C.prototype.unset=C.prototype.P;C.prototype.changed=C.prototype.u;C.prototype.dispatchEvent=C.prototype.b;C.prototype.getRevision=C.prototype.K;
+C.prototype.on=C.prototype.I;C.prototype.once=C.prototype.L;C.prototype.un=C.prototype.J;C.prototype.unByKey=C.prototype.M;E.prototype.getFirstCoordinate=E.prototype.Ib;E.prototype.getLastCoordinate=E.prototype.Jb;E.prototype.getLayout=E.prototype.Kb;E.prototype.rotate=E.prototype.rotate;E.prototype.getClosestPoint=E.prototype.vb;E.prototype.getExtent=E.prototype.H;E.prototype.simplify=E.prototype.Bb;E.prototype.transform=E.prototype.jb;E.prototype.get=E.prototype.get;E.prototype.getKeys=E.prototype.N;
+E.prototype.getProperties=E.prototype.O;E.prototype.set=E.prototype.set;E.prototype.setProperties=E.prototype.G;E.prototype.unset=E.prototype.P;E.prototype.changed=E.prototype.u;E.prototype.dispatchEvent=E.prototype.b;E.prototype.getRevision=E.prototype.K;E.prototype.on=E.prototype.I;E.prototype.once=E.prototype.L;E.prototype.un=E.prototype.J;E.prototype.unByKey=E.prototype.M;ko.prototype.readFeatures=ko.prototype.Fa;lo.prototype.readFeatures=lo.prototype.Fa;lo.prototype.readFeatures=lo.prototype.Fa;
+Xe.prototype.get=Xe.prototype.get;Xe.prototype.getKeys=Xe.prototype.N;Xe.prototype.getProperties=Xe.prototype.O;Xe.prototype.set=Xe.prototype.set;Xe.prototype.setProperties=Xe.prototype.G;Xe.prototype.unset=Xe.prototype.P;Xe.prototype.changed=Xe.prototype.u;Xe.prototype.dispatchEvent=Xe.prototype.b;Xe.prototype.getRevision=Xe.prototype.K;Xe.prototype.on=Xe.prototype.I;Xe.prototype.once=Xe.prototype.L;Xe.prototype.un=Xe.prototype.J;Xe.prototype.unByKey=Xe.prototype.M;Ef.prototype.getMap=Ef.prototype.i;
+Ef.prototype.setMap=Ef.prototype.setMap;Ef.prototype.setTarget=Ef.prototype.c;Ef.prototype.get=Ef.prototype.get;Ef.prototype.getKeys=Ef.prototype.N;Ef.prototype.getProperties=Ef.prototype.O;Ef.prototype.set=Ef.prototype.set;Ef.prototype.setProperties=Ef.prototype.G;Ef.prototype.unset=Ef.prototype.P;Ef.prototype.changed=Ef.prototype.u;Ef.prototype.dispatchEvent=Ef.prototype.b;Ef.prototype.getRevision=Ef.prototype.K;Ef.prototype.on=Ef.prototype.I;Ef.prototype.once=Ef.prototype.L;Ef.prototype.un=Ef.prototype.J;
+Ef.prototype.unByKey=Ef.prototype.M;Lf.prototype.getMap=Lf.prototype.i;Lf.prototype.setMap=Lf.prototype.setMap;Lf.prototype.setTarget=Lf.prototype.c;Lf.prototype.get=Lf.prototype.get;Lf.prototype.getKeys=Lf.prototype.N;Lf.prototype.getProperties=Lf.prototype.O;Lf.prototype.set=Lf.prototype.set;Lf.prototype.setProperties=Lf.prototype.G;Lf.prototype.unset=Lf.prototype.P;Lf.prototype.changed=Lf.prototype.u;Lf.prototype.dispatchEvent=Lf.prototype.b;Lf.prototype.getRevision=Lf.prototype.K;
+Lf.prototype.on=Lf.prototype.I;Lf.prototype.once=Lf.prototype.L;Lf.prototype.un=Lf.prototype.J;Lf.prototype.unByKey=Lf.prototype.M;Qf.prototype.getMap=Qf.prototype.i;Qf.prototype.setMap=Qf.prototype.setMap;Qf.prototype.setTarget=Qf.prototype.c;Qf.prototype.get=Qf.prototype.get;Qf.prototype.getKeys=Qf.prototype.N;Qf.prototype.getProperties=Qf.prototype.O;Qf.prototype.set=Qf.prototype.set;Qf.prototype.setProperties=Qf.prototype.G;Qf.prototype.unset=Qf.prototype.P;Qf.prototype.changed=Qf.prototype.u;
+Qf.prototype.dispatchEvent=Qf.prototype.b;Qf.prototype.getRevision=Qf.prototype.K;Qf.prototype.on=Qf.prototype.I;Qf.prototype.once=Qf.prototype.L;Qf.prototype.un=Qf.prototype.J;Qf.prototype.unByKey=Qf.prototype.M;an.prototype.getMap=an.prototype.i;an.prototype.setMap=an.prototype.setMap;an.prototype.setTarget=an.prototype.c;an.prototype.get=an.prototype.get;an.prototype.getKeys=an.prototype.N;an.prototype.getProperties=an.prototype.O;an.prototype.set=an.prototype.set;an.prototype.setProperties=an.prototype.G;
+an.prototype.unset=an.prototype.P;an.prototype.changed=an.prototype.u;an.prototype.dispatchEvent=an.prototype.b;an.prototype.getRevision=an.prototype.K;an.prototype.on=an.prototype.I;an.prototype.once=an.prototype.L;an.prototype.un=an.prototype.J;an.prototype.unByKey=an.prototype.M;Hf.prototype.getMap=Hf.prototype.i;Hf.prototype.setMap=Hf.prototype.setMap;Hf.prototype.setTarget=Hf.prototype.c;Hf.prototype.get=Hf.prototype.get;Hf.prototype.getKeys=Hf.prototype.N;Hf.prototype.getProperties=Hf.prototype.O;
+Hf.prototype.set=Hf.prototype.set;Hf.prototype.setProperties=Hf.prototype.G;Hf.prototype.unset=Hf.prototype.P;Hf.prototype.changed=Hf.prototype.u;Hf.prototype.dispatchEvent=Hf.prototype.b;Hf.prototype.getRevision=Hf.prototype.K;Hf.prototype.on=Hf.prototype.I;Hf.prototype.once=Hf.prototype.L;Hf.prototype.un=Hf.prototype.J;Hf.prototype.unByKey=Hf.prototype.M;fn.prototype.getMap=fn.prototype.i;fn.prototype.setMap=fn.prototype.setMap;fn.prototype.setTarget=fn.prototype.c;fn.prototype.get=fn.prototype.get;
+fn.prototype.getKeys=fn.prototype.N;fn.prototype.getProperties=fn.prototype.O;fn.prototype.set=fn.prototype.set;fn.prototype.setProperties=fn.prototype.G;fn.prototype.unset=fn.prototype.P;fn.prototype.changed=fn.prototype.u;fn.prototype.dispatchEvent=fn.prototype.b;fn.prototype.getRevision=fn.prototype.K;fn.prototype.on=fn.prototype.I;fn.prototype.once=fn.prototype.L;fn.prototype.un=fn.prototype.J;fn.prototype.unByKey=fn.prototype.M;Jf.prototype.getMap=Jf.prototype.i;Jf.prototype.setMap=Jf.prototype.setMap;
+Jf.prototype.setTarget=Jf.prototype.c;Jf.prototype.get=Jf.prototype.get;Jf.prototype.getKeys=Jf.prototype.N;Jf.prototype.getProperties=Jf.prototype.O;Jf.prototype.set=Jf.prototype.set;Jf.prototype.setProperties=Jf.prototype.G;Jf.prototype.unset=Jf.prototype.P;Jf.prototype.changed=Jf.prototype.u;Jf.prototype.dispatchEvent=Jf.prototype.b;Jf.prototype.getRevision=Jf.prototype.K;Jf.prototype.on=Jf.prototype.I;Jf.prototype.once=Jf.prototype.L;Jf.prototype.un=Jf.prototype.J;Jf.prototype.unByKey=Jf.prototype.M;
+kn.prototype.getMap=kn.prototype.i;kn.prototype.setMap=kn.prototype.setMap;kn.prototype.setTarget=kn.prototype.c;kn.prototype.get=kn.prototype.get;kn.prototype.getKeys=kn.prototype.N;kn.prototype.getProperties=kn.prototype.O;kn.prototype.set=kn.prototype.set;kn.prototype.setProperties=kn.prototype.G;kn.prototype.unset=kn.prototype.P;kn.prototype.changed=kn.prototype.u;kn.prototype.dispatchEvent=kn.prototype.b;kn.prototype.getRevision=kn.prototype.K;kn.prototype.on=kn.prototype.I;
+kn.prototype.once=kn.prototype.L;kn.prototype.un=kn.prototype.J;kn.prototype.unByKey=kn.prototype.M;pn.prototype.getMap=pn.prototype.i;pn.prototype.setMap=pn.prototype.setMap;pn.prototype.setTarget=pn.prototype.c;pn.prototype.get=pn.prototype.get;pn.prototype.getKeys=pn.prototype.N;pn.prototype.getProperties=pn.prototype.O;pn.prototype.set=pn.prototype.set;pn.prototype.setProperties=pn.prototype.G;pn.prototype.unset=pn.prototype.P;pn.prototype.changed=pn.prototype.u;pn.prototype.dispatchEvent=pn.prototype.b;
+pn.prototype.getRevision=pn.prototype.K;pn.prototype.on=pn.prototype.I;pn.prototype.once=pn.prototype.L;pn.prototype.un=pn.prototype.J;pn.prototype.unByKey=pn.prototype.M;
+  return OPENLAYERS.ol;
+}));
+
diff --git a/themes/bootstrap3/js/vendor/rc4.js b/themes/bootstrap3/js/vendor/rc4.js
deleted file mode 100644
index df042c0eee83cd451e9a4fc6ee9f8c6d37b20ad3..0000000000000000000000000000000000000000
--- a/themes/bootstrap3/js/vendor/rc4.js
+++ /dev/null
@@ -1,100 +0,0 @@
-/* RC4 symmetric cipher encryption/decryption
- * Copyright (c) 2006 by Ali Farhadi.
- * released under the terms of the Gnu Public License.
- * see the GPL for details.
- *
- * Email: ali[at]farhadi[dot]ir
- * Website: http://farhadi.ir/
- */
-
-/**
- * Encrypt given plain text using the key with RC4 algorithm.
- * All parameters and return value are in binary format.
- *
- * @param string key - secret key for encryption
- * @param string pt - plain text to be encrypted
- * @return string
- */
-function rc4Encrypt(key, pt) {
-  s = new Array();
-  for (var i=0; i<256; i++) {
-    s[i] = i;
-  }
-  var j = 0;
-  var x;
-  for (i=0; i<256; i++) {
-    j = (j + s[i] + key.charCodeAt(i % key.length)) % 256;
-    x = s[i];
-    s[i] = s[j];
-    s[j] = x;
-  }
-  i = 0;
-  j = 0;
-  var ct = '';
-  for (var y=0; y<pt.length; y++) {
-    i = (i + 1) % 256;
-    j = (j + s[i]) % 256;
-    x = s[i];
-    s[i] = s[j];
-    s[j] = x;
-    ct += String.fromCharCode(pt.charCodeAt(y) ^ s[(s[i] + s[j]) % 256]);
-  }
-  return ct;
-}
-
-/**
- * Decrypt given cipher text using the key with RC4 algorithm.
- * All parameters and return value are in binary format.
- *
- * @param string key - secret key for decryption
- * @param string ct - cipher text to be decrypted
- * @return string
-*/
-function rc4Decrypt(key, ct) {
-  return rc4Encrypt(key, ct);
-}
-
-/* Hexadecimal conversion methods.
- * Copyright (c) 2006 by Ali Farhadi.
- * released under the terms of the Gnu Public License.
- * see the GPL for details.
- *
- * Email: ali[at]farhadi[dot]ir
- * Website: http://farhadi.ir/
- */
-
-//Encodes data to Hex(base16) format
-function hexEncode(data){
-  var b16_digits = '0123456789abcdef';
-  var b16_map = new Array();
-  for (var i=0; i<256; i++) {
-    b16_map[i] = b16_digits.charAt(i >> 4) + b16_digits.charAt(i & 15);
-  }
-
-  var result = new Array();
-  for (var i=0; i<data.length; i++) {
-    result[i] = b16_map[data.charCodeAt(i)];
-  }
-
-  return result.join('');
-}
-
-//Decodes Hex(base16) formated data
-function hexDecode(data){
-  var b16_digits = '0123456789abcdef';
-  var b16_map = new Array();
-  for (var i=0; i<256; i++) {
-    b16_map[b16_digits.charAt(i >> 4) + b16_digits.charAt(i & 15)] = String.fromCharCode(i);
-  }
-  if (!data.match(/^[a-f0-9]*$/i)) return false;// return false if input data is not a valid Hex string
-
-  if (data.length % 2) data = '0'+data;
-
-  var result = new Array();
-  var j=0;
-  for (var i=0; i<data.length; i+=2) {
-    result[j++] = b16_map[data.substr(i,2)];
-  }
-
-  return result.join('');
-}
\ No newline at end of file
diff --git a/themes/bootstrap3/js/vendor/recaptcha_ajax.js b/themes/bootstrap3/js/vendor/recaptcha_ajax.js
deleted file mode 100644
index 34ca67409589cc3ce6bb3a3d28d0491a5f7c794e..0000000000000000000000000000000000000000
--- a/themes/bootstrap3/js/vendor/recaptcha_ajax.js
+++ /dev/null
@@ -1,182 +0,0 @@
-(function(){var h,aa=aa||{},l=this,ba=function(a){a=a.split(".");for(var b=l,c;c=a.shift();)if(null!=b[c])b=b[c];else return null;return b},ca=function(){},da=function(a){var b=typeof a;if("object"==b)if(a){if(a instanceof Array)return"array";if(a instanceof Object)return b;var c=Object.prototype.toString.call(a);if("[object Window]"==c)return"object";if("[object Array]"==c||"number"==typeof a.length&&"undefined"!=typeof a.splice&&"undefined"!=typeof a.propertyIsEnumerable&&!a.propertyIsEnumerable("splice"))return"array";
-if("[object Function]"==c||"undefined"!=typeof a.call&&"undefined"!=typeof a.propertyIsEnumerable&&!a.propertyIsEnumerable("call"))return"function"}else return"null";else if("function"==b&&"undefined"==typeof a.call)return"object";return b},m=function(a){return"array"==da(a)},ea=function(a){var b=da(a);return"array"==b||"object"==b&&"number"==typeof a.length},n=function(a){return"string"==typeof a},fa=function(a){return"function"==da(a)},ga=function(a){var b=typeof a;return"object"==b&&null!=a||"function"==
-b},ha=function(a,b,c){return a.call.apply(a.bind,arguments)},ia=function(a,b,c){if(!a)throw Error();if(2<arguments.length){var d=Array.prototype.slice.call(arguments,2);return function(){var c=Array.prototype.slice.call(arguments);Array.prototype.unshift.apply(c,d);return a.apply(b,c)}}return function(){return a.apply(b,arguments)}},p=function(a,b,c){p=Function.prototype.bind&&-1!=Function.prototype.bind.toString().indexOf("native code")?ha:ia;return p.apply(null,arguments)},ja=Date.now||function(){return+new Date},
-q=function(a,b){var c=a.split("."),d=l;c[0]in d||!d.execScript||d.execScript("var "+c[0]);for(var e;c.length&&(e=c.shift());)c.length||void 0===b?d=d[e]?d[e]:d[e]={}:d[e]=b},r=function(a,b){function c(){}c.prototype=b.prototype;a.superClass_=b.prototype;a.prototype=new c;a.base=function(a,c,g){return b.prototype[c].apply(a,Array.prototype.slice.call(arguments,2))}};
-Function.prototype.bind=Function.prototype.bind||function(a,b){if(1<arguments.length){var c=Array.prototype.slice.call(arguments,1);c.unshift(this,a);return p.apply(null,c)}return p(this,a)};var s={};q("RecaptchaTemplates",s);s.VertHtml='<table id="recaptcha_table" class="recaptchatable" > <tr> <td colspan="6" class=\'recaptcha_r1_c1\'></td> </tr> <tr> <td class=\'recaptcha_r2_c1\'></td> <td colspan="4" class=\'recaptcha_image_cell\'><center><div id="recaptcha_image"></div></center></td> <td class=\'recaptcha_r2_c2\'></td> </tr> <tr> <td rowspan="6" class=\'recaptcha_r3_c1\'></td> <td colspan="4" class=\'recaptcha_r3_c2\'></td> <td rowspan="6" class=\'recaptcha_r3_c3\'></td> </tr> <tr> <td rowspan="3" class=\'recaptcha_r4_c1\' height="49"> <div class="recaptcha_input_area"> <input name="recaptcha_response_field" id="recaptcha_response_field" type="text" autocorrect="off" autocapitalize="off" placeholder="" /> <span id="recaptcha_privacy" class="recaptcha_only_if_privacy"></span> </div> </td> <td rowspan="4" class=\'recaptcha_r4_c2\'></td> <td><a id=\'recaptcha_reload_btn\'><img id=\'recaptcha_reload\' width="25" height="17" /></a></td> <td rowspan="4" class=\'recaptcha_r4_c4\'></td> </tr> <tr> <td><a id=\'recaptcha_switch_audio_btn\' class="recaptcha_only_if_image"><img id=\'recaptcha_switch_audio\' width="25" height="16" alt="" /></a><a id=\'recaptcha_switch_img_btn\' class="recaptcha_only_if_audio"><img id=\'recaptcha_switch_img\' width="25" height="16" alt=""/></a></td> </tr> <tr> <td><a id=\'recaptcha_whatsthis_btn\'><img id=\'recaptcha_whatsthis\' width="25" height="16" /></a></td> </tr> <tr> <td class=\'recaptcha_r7_c1\'></td> <td class=\'recaptcha_r8_c1\'></td> </tr> </table> ';s.CleanCss=".recaptchatable td img{display:block}.recaptchatable .recaptcha_image_cell center img{height:57px}.recaptchatable .recaptcha_image_cell center{height:57px}.recaptchatable .recaptcha_image_cell{background-color:white;height:57px;padding:7px!important}.recaptchatable,#recaptcha_area tr,#recaptcha_area td,#recaptcha_area th{margin:0!important;border:0!important;border-collapse:collapse!important;vertical-align:middle!important}.recaptchatable *{margin:0;padding:0;border:0;color:black;position:static;top:auto;left:auto;right:auto;bottom:auto}.recaptchatable #recaptcha_image{position:relative;margin:auto;border:1px solid #dfdfdf!important}.recaptchatable #recaptcha_image #recaptcha_challenge_image{display:block}.recaptchatable #recaptcha_image #recaptcha_ad_image{display:block;position:absolute;top:0}.recaptchatable a img{border:0}.recaptchatable a,.recaptchatable a:hover{cursor:pointer;outline:none;border:0!important;padding:0!important;text-decoration:none;color:blue;background:none!important;font-weight:normal}.recaptcha_input_area{position:relative!important;background:none!important}.recaptchatable label.recaptcha_input_area_text{border:1px solid #dfdfdf!important;margin:0!important;padding:0!important;position:static!important;top:auto!important;left:auto!important;right:auto!important;bottom:auto!important}.recaptcha_theme_red label.recaptcha_input_area_text,.recaptcha_theme_white label.recaptcha_input_area_text{color:black!important}.recaptcha_theme_blackglass label.recaptcha_input_area_text{color:white!important}.recaptchatable #recaptcha_response_field{font-size:11pt}.recaptcha_theme_blackglass #recaptcha_response_field,.recaptcha_theme_white #recaptcha_response_field{border:1px solid gray}.recaptcha_theme_red #recaptcha_response_field{border:1px solid #cca940}.recaptcha_audio_cant_hear_link{font-size:7pt;color:black}.recaptchatable{line-height:1em;border:1px solid #dfdfdf!important}.recaptcha_error_text{color:red}.recaptcha_only_if_privacy{float:right;text-align:right;margin-right:7px}#recaptcha-ad-choices{position:absolute;height:15px;top:0;right:0}#recaptcha-ad-choices img{height:15px}.recaptcha-ad-choices-collapsed{width:15px;height:15px;display:block}.recaptcha-ad-choices-expanded{width:75px;height:15px;display:none}#recaptcha-ad-choices:hover .recaptcha-ad-choices-collapsed{display:none}#recaptcha-ad-choices:hover .recaptcha-ad-choices-expanded{display:block}";s.CleanHtml='<table id="recaptcha_table" class="recaptchatable"> <tr height="73"> <td class=\'recaptcha_image_cell\' width="302"><center><div id="recaptcha_image"></div></center></td> <td style="padding: 10px 7px 7px 7px;"> <a id=\'recaptcha_reload_btn\'><img id=\'recaptcha_reload\' width="25" height="18" alt="" /></a> <a id=\'recaptcha_switch_audio_btn\' class="recaptcha_only_if_image"><img id=\'recaptcha_switch_audio\' width="25" height="15" alt="" /></a><a id=\'recaptcha_switch_img_btn\' class="recaptcha_only_if_audio"><img id=\'recaptcha_switch_img\' width="25" height="15" alt=""/></a> <a id=\'recaptcha_whatsthis_btn\'><img id=\'recaptcha_whatsthis\' width="25" height="16" /></a> </td> <td style="padding: 18px 7px 18px 7px;"> <img id=\'recaptcha_logo\' alt="" width="71" height="36" /> </td> </tr> <tr> <td style="padding-left: 7px;"> <div class="recaptcha_input_area" style="padding-top: 2px; padding-bottom: 7px;"> <input style="border: 1px solid #3c3c3c; width: 302px;" name="recaptcha_response_field" id="recaptcha_response_field" type="text" /> </div> </td> <td colspan=2><span id="recaptcha_privacy" class="recaptcha_only_if_privacy"></span></td> </tr> </table> ';s.VertCss=".recaptchatable td img{display:block}.recaptchatable .recaptcha_r1_c1{background:url('IMGROOT/sprite.png') 0 -63px no-repeat;width:318px;height:9px}.recaptchatable .recaptcha_r2_c1{background:url('IMGROOT/sprite.png') -18px 0 no-repeat;width:9px;height:57px}.recaptchatable .recaptcha_r2_c2{background:url('IMGROOT/sprite.png') -27px 0 no-repeat;width:9px;height:57px}.recaptchatable .recaptcha_r3_c1{background:url('IMGROOT/sprite.png') 0 0 no-repeat;width:9px;height:63px}.recaptchatable .recaptcha_r3_c2{background:url('IMGROOT/sprite.png') -18px -57px no-repeat;width:300px;height:6px}.recaptchatable .recaptcha_r3_c3{background:url('IMGROOT/sprite.png') -9px 0 no-repeat;width:9px;height:63px}.recaptchatable .recaptcha_r4_c1{background:url('IMGROOT/sprite.png') -43px 0 no-repeat;width:171px;height:49px}.recaptchatable .recaptcha_r4_c2{background:url('IMGROOT/sprite.png') -36px 0 no-repeat;width:7px;height:57px}.recaptchatable .recaptcha_r4_c4{background:url('IMGROOT/sprite.png') -214px 0 no-repeat;width:97px;height:57px}.recaptchatable .recaptcha_r7_c1{background:url('IMGROOT/sprite.png') -43px -49px no-repeat;width:171px;height:8px}.recaptchatable .recaptcha_r8_c1{background:url('IMGROOT/sprite.png') -43px -49px no-repeat;width:25px;height:8px}.recaptchatable .recaptcha_image_cell center img{height:57px}.recaptchatable .recaptcha_image_cell center{height:57px}.recaptchatable .recaptcha_image_cell{background-color:white;height:57px}#recaptcha_area,#recaptcha_table{width:318px!important}.recaptchatable,#recaptcha_area tr,#recaptcha_area td,#recaptcha_area th{margin:0!important;border:0!important;padding:0!important;border-collapse:collapse!important;vertical-align:middle!important}.recaptchatable *{margin:0;padding:0;border:0;font-family:helvetica,sans-serif;font-size:8pt;color:black;position:static;top:auto;left:auto;right:auto;bottom:auto}.recaptchatable #recaptcha_image{position:relative;margin:auto}.recaptchatable #recaptcha_image #recaptcha_challenge_image{display:block}.recaptchatable #recaptcha_image #recaptcha_ad_image{display:block;position:absolute;top:0}.recaptchatable img{border:0!important;margin:0!important;padding:0!important}.recaptchatable a,.recaptchatable a:hover{cursor:pointer;outline:none;border:0!important;padding:0!important;text-decoration:none;color:blue;background:none!important;font-weight:normal}.recaptcha_input_area{position:relative!important;width:153px!important;height:45px!important;margin-left:7px!important;margin-right:7px!important;background:none!important}.recaptchatable label.recaptcha_input_area_text{margin:0!important;padding:0!important;position:static!important;top:auto!important;left:auto!important;right:auto!important;bottom:auto!important;background:none!important;height:auto!important;width:auto!important}.recaptcha_theme_red label.recaptcha_input_area_text,.recaptcha_theme_white label.recaptcha_input_area_text{color:black!important}.recaptcha_theme_blackglass label.recaptcha_input_area_text{color:white!important}.recaptchatable #recaptcha_response_field{width:153px!important;position:relative!important;bottom:7px!important;padding:0!important;margin:15px 0 0 0!important;font-size:10pt}.recaptcha_theme_blackglass #recaptcha_response_field,.recaptcha_theme_white #recaptcha_response_field{border:1px solid gray}.recaptcha_theme_red #recaptcha_response_field{border:1px solid #cca940}.recaptcha_audio_cant_hear_link{font-size:7pt;color:black}.recaptchatable{line-height:1!important}#recaptcha_instructions_error{color:red!important}.recaptcha_only_if_privacy{float:right;text-align:right}#recaptcha-ad-choices{position:absolute;height:15px;top:0;right:0}#recaptcha-ad-choices img{height:15px}.recaptcha-ad-choices-collapsed{width:15px;height:15px;display:block}.recaptcha-ad-choices-expanded{width:75px;height:15px;display:none}#recaptcha-ad-choices:hover .recaptcha-ad-choices-collapsed{display:none}#recaptcha-ad-choices:hover .recaptcha-ad-choices-expanded{display:block}";var t={visual_challenge:"Get a visual challenge",audio_challenge:"Get an audio challenge",refresh_btn:"Get a new challenge",instructions_visual:"Type the text:",instructions_audio:"Type what you hear:",help_btn:"Help",play_again:"Play sound again",cant_hear_this:"Download sound as MP3",incorrect_try_again:"Incorrect. Try again.",image_alt_text:"reCAPTCHA challenge image",privacy_and_terms:"Privacy & Terms"},ka={visual_challenge:"\u0627\u0644\u062d\u0635\u0648\u0644 \u0639\u0644\u0649 \u062a\u062d\u062f\u064d \u0645\u0631\u0626\u064a",
-audio_challenge:"\u0627\u0644\u062d\u0635\u0648\u0644 \u0639\u0644\u0649 \u062a\u062d\u062f\u064d \u0635\u0648\u062a\u064a",refresh_btn:"\u0627\u0644\u062d\u0635\u0648\u0644 \u0639\u0644\u0649 \u062a\u062d\u062f\u064d \u062c\u062f\u064a\u062f",instructions_visual:"\u064a\u0631\u062c\u0649 \u0643\u062a\u0627\u0628\u0629 \u0627\u0644\u0646\u0635:",instructions_audio:"\u0627\u0643\u062a\u0628 \u0645\u0627 \u062a\u0633\u0645\u0639\u0647:",help_btn:"\u0645\u0633\u0627\u0639\u062f\u0629",play_again:"\u062a\u0634\u063a\u064a\u0644 \u0627\u0644\u0635\u0648\u062a \u0645\u0631\u0629 \u0623\u062e\u0631\u0649",
-cant_hear_this:"\u062a\u0646\u0632\u064a\u0644 \u0627\u0644\u0635\u0648\u062a \u0628\u062a\u0646\u0633\u064a\u0642 MP3",incorrect_try_again:"\u063a\u064a\u0631 \u0635\u062d\u064a\u062d. \u0623\u0639\u062f \u0627\u0644\u0645\u062d\u0627\u0648\u0644\u0629.",image_alt_text:"\u0635\u0648\u0631\u0629 \u0627\u0644\u062a\u062d\u062f\u064a \u0645\u0646 reCAPTCHA",privacy_and_terms:"\u0627\u0644\u062e\u0635\u0648\u0635\u064a\u0629 \u0648\u0627\u0644\u0628\u0646\u0648\u062f"},la={visual_challenge:"Obtener una pista visual",
-audio_challenge:"Obtener una pista sonora",refresh_btn:"Obtener una pista nueva",instructions_visual:"Introduzca el texto:",instructions_audio:"Escribe lo que oigas:",help_btn:"Ayuda",play_again:"Volver a reproducir el sonido",cant_hear_this:"Descargar el sonido en MP3",incorrect_try_again:"Incorrecto. Vu\u00e9lvelo a intentar.",image_alt_text:"Pista de imagen reCAPTCHA",privacy_and_terms:"Privacidad y condiciones"},ma={visual_challenge:"Kumuha ng pagsubok na visual",audio_challenge:"Kumuha ng pagsubok na audio",
-refresh_btn:"Kumuha ng bagong pagsubok",instructions_visual:"I-type ang teksto:",instructions_audio:"I-type ang iyong narinig",help_btn:"Tulong",play_again:"I-play muli ang tunog",cant_hear_this:"I-download ang tunog bilang MP3",incorrect_try_again:"Hindi wasto. Muling subukan.",image_alt_text:"larawang panghamon ng reCAPTCHA",privacy_and_terms:"Privacy at Mga Tuntunin"},na={visual_challenge:"Test visuel",audio_challenge:"Test audio",refresh_btn:"Nouveau test",instructions_visual:"Saisissez le texte\u00a0:",
-instructions_audio:"Qu'entendez-vous ?",help_btn:"Aide",play_again:"R\u00e9\u00e9couter",cant_hear_this:"T\u00e9l\u00e9charger l'audio au format MP3",incorrect_try_again:"Incorrect. Veuillez r\u00e9essayer.",image_alt_text:"Image reCAPTCHA",privacy_and_terms:"Confidentialit\u00e9 et conditions d'utilisation"},oa={visual_challenge:"Dapatkan kata pengujian berbentuk visual",audio_challenge:"Dapatkan kata pengujian berbentuk audio",refresh_btn:"Dapatkan kata pengujian baru",instructions_visual:"Ketik teks:",
-instructions_audio:"Ketik yang Anda dengar:",help_btn:"Bantuan",play_again:"Putar suara sekali lagi",cant_hear_this:"Unduh suara sebagai MP3",incorrect_try_again:"Salah. Coba lagi.",image_alt_text:"Gambar tantangan reCAPTCHA",privacy_and_terms:"Privasi & Persyaratan"},pa={visual_challenge:"\u05e7\u05d1\u05dc \u05d0\u05ea\u05d2\u05e8 \u05d7\u05d6\u05d5\u05ea\u05d9",audio_challenge:"\u05e7\u05d1\u05dc \u05d0\u05ea\u05d2\u05e8 \u05e9\u05de\u05e2",refresh_btn:"\u05e7\u05d1\u05dc \u05d0\u05ea\u05d2\u05e8 \u05d7\u05d3\u05e9",
-instructions_visual:"\u05d4\u05e7\u05dc\u05d3 \u05d0\u05ea \u05d4\u05d8\u05e7\u05e1\u05d8:",instructions_audio:"\u05d4\u05e7\u05dc\u05d3 \u05d0\u05ea \u05de\u05d4 \u05e9\u05d0\u05ea\u05d4 \u05e9\u05d5\u05de\u05e2:",help_btn:"\u05e2\u05d6\u05e8\u05d4",play_again:"\u05d4\u05e4\u05e2\u05dc \u05e9\u05d5\u05d1 \u05d0\u05ea \u05d4\u05e9\u05de\u05e2",cant_hear_this:"\u05d4\u05d5\u05e8\u05d3 \u05e9\u05de\u05e2 \u05db-3MP",incorrect_try_again:"\u05e9\u05d2\u05d5\u05d9. \u05e0\u05e1\u05d4 \u05e9\u05d5\u05d1.",
-image_alt_text:"\u05ea\u05de\u05d5\u05e0\u05ea \u05d0\u05ea\u05d2\u05e8 \u05e9\u05dc reCAPTCHA",privacy_and_terms:"\u05e4\u05e8\u05d8\u05d9\u05d5\u05ea \u05d5\u05ea\u05e0\u05d0\u05d9\u05dd"},qa={visual_challenge:"Obter um desafio visual",audio_challenge:"Obter um desafio de \u00e1udio",refresh_btn:"Obter um novo desafio",instructions_visual:"Digite o texto:",instructions_audio:"Digite o que voc\u00ea ouve:",help_btn:"Ajuda",play_again:"Reproduzir som novamente",cant_hear_this:"Fazer download do som no formato MP3",
-incorrect_try_again:"Incorreto. Tente novamente.",image_alt_text:"Imagem de desafio reCAPTCHA",privacy_and_terms:"Privacidade e Termos"},ra={visual_challenge:"Ob\u0163ine\u0163i un cod captcha vizual",audio_challenge:"Ob\u0163ine\u0163i un cod captcha audio",refresh_btn:"Ob\u0163ine\u0163i un nou cod captcha",instructions_visual:"Introduce\u021bi textul:",instructions_audio:"Introduce\u0163i ceea ce auzi\u0163i:",help_btn:"Ajutor",play_again:"Reda\u0163i sunetul din nou",cant_hear_this:"Desc\u0103rca\u0163i fi\u015fierul audio ca MP3",
-incorrect_try_again:"Incorect. \u00cencerca\u0163i din nou.",image_alt_text:"Imagine de verificare reCAPTCHA",privacy_and_terms:"Confiden\u0163ialitate \u015fi termeni"},sa={visual_challenge:"\u6536\u5230\u4e00\u4e2a\u89c6\u9891\u9080\u8bf7",audio_challenge:"\u6362\u4e00\u7ec4\u97f3\u9891\u9a8c\u8bc1\u7801",refresh_btn:"\u6362\u4e00\u7ec4\u9a8c\u8bc1\u7801",instructions_visual:"\u8f93\u5165\u6587\u5b57\uff1a",instructions_audio:"\u8bf7\u952e\u5165\u60a8\u542c\u5230\u7684\u5185\u5bb9\uff1a",help_btn:"\u5e2e\u52a9",
-play_again:"\u91cd\u65b0\u64ad\u653e",cant_hear_this:"\u4ee5 MP3 \u683c\u5f0f\u4e0b\u8f7d\u58f0\u97f3",incorrect_try_again:"\u4e0d\u6b63\u786e\uff0c\u8bf7\u91cd\u8bd5\u3002",image_alt_text:"reCAPTCHA \u9a8c\u8bc1\u56fe\u7247",privacy_and_terms:"\u9690\u79c1\u6743\u548c\u4f7f\u7528\u6761\u6b3e"},ta={en:t,af:{visual_challenge:"Kry 'n visuele verifi\u00ebring",audio_challenge:"Kry 'n klankverifi\u00ebring",refresh_btn:"Kry 'n nuwe verifi\u00ebring",instructions_visual:"",instructions_audio:"Tik wat jy hoor:",
-help_btn:"Hulp",play_again:"Speel geluid weer",cant_hear_this:"Laai die klank af as MP3",incorrect_try_again:"Verkeerd. Probeer weer.",image_alt_text:"reCAPTCHA-uitdagingprent",privacy_and_terms:"Privaatheid en bepalings"},am:{visual_challenge:"\u12e8\u12a5\u12ed\u1273 \u1270\u130b\u1323\u121a \u12a0\u130d\u129d",audio_challenge:"\u120c\u120b \u12a0\u12f2\u1235 \u12e8\u12f5\u121d\u133d \u1325\u12eb\u1244 \u12ed\u1245\u1228\u1265",refresh_btn:"\u120c\u120b \u12a0\u12f2\u1235 \u1325\u12eb\u1244 \u12ed\u1245\u1228\u1265",
-instructions_visual:"",instructions_audio:"\u12e8\u121d\u1275\u1230\u121b\u12cd\u1295 \u1270\u12ed\u1265\u1361-",help_btn:"\u12a5\u1308\u12db",play_again:"\u12f5\u121d\u1339\u1295 \u12a5\u1295\u12f0\u1308\u1293 \u12a0\u132b\u12cd\u1275",cant_hear_this:"\u12f5\u121d\u1339\u1295 \u1260MP3 \u1245\u122d\u133d \u12a0\u12cd\u122d\u12f5",incorrect_try_again:"\u1275\u12ad\u12ad\u120d \u12a0\u12ed\u12f0\u1208\u121d\u1362 \u12a5\u1295\u12f0\u1308\u1293 \u121e\u12ad\u122d\u1362",image_alt_text:"reCAPTCHA \u121d\u1235\u120d \u130d\u1320\u121d",
-privacy_and_terms:"\u130d\u120b\u12ca\u1290\u1275 \u12a5\u1293 \u12cd\u120d"},ar:ka,"ar-EG":ka,bg:{visual_challenge:"\u041f\u043e\u043b\u0443\u0447\u0430\u0432\u0430\u043d\u0435 \u043d\u0430 \u0432\u0438\u0437\u0443\u0430\u043b\u043d\u0430 \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0430",audio_challenge:"\u0417\u0430\u0440\u0435\u0436\u0434\u0430\u043d\u0435 \u043d\u0430 \u0430\u0443\u0434\u0438\u043e\u0442\u0435\u0441\u0442",refresh_btn:"\u0417\u0430\u0440\u0435\u0436\u0434\u0430\u043d\u0435 \u043d\u0430 \u043d\u043e\u0432 \u0442\u0435\u0441\u0442",
-instructions_visual:"\u0412\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u0442\u0435\u043a\u0441\u0442\u0430:",instructions_audio:"\u0412\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u0447\u0443\u0442\u043e\u0442\u043e:",help_btn:"\u041f\u043e\u043c\u043e\u0449",play_again:"\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u043f\u0443\u0441\u043a\u0430\u043d\u0435 \u043d\u0430 \u0437\u0432\u0443\u043a\u0430",cant_hear_this:"\u0418\u0437\u0442\u0435\u0433\u043b\u044f\u043d\u0435 \u043d\u0430 \u0437\u0432\u0443\u043a\u0430 \u0432\u044a\u0432 \u0444\u043e\u0440\u043c\u0430\u0442 MP3",
-incorrect_try_again:"\u041d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u043d\u043e. \u041e\u043f\u0438\u0442\u0430\u0439\u0442\u0435 \u043e\u0442\u043d\u043e\u0432\u043e.",image_alt_text:"\u0418\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u0435 \u043d\u0430 \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0430\u0442\u0430 \u0441 reCAPTCHA",privacy_and_terms:"\u041f\u043e\u0432\u0435\u0440\u0438\u0442\u0435\u043b\u043d\u043e\u0441\u0442 \u0438 \u041e\u0431\u0449\u0438 \u0443\u0441\u043b\u043e\u0432\u0438\u044f"},
-bn:{visual_challenge:"\u098f\u0995\u099f\u09bf \u09a6\u09c3\u09b6\u09cd\u09af\u09ae\u09be\u09a8 \u09aa\u09cd\u09b0\u09a4\u09bf\u09a6\u09cd\u09ac\u09a8\u09cd\u09a6\u09cd\u09ac\u09bf\u09a4\u09be \u09aa\u09be\u09a8",audio_challenge:"\u098f\u0995\u099f\u09bf \u0985\u09a1\u09bf\u0993 \u09aa\u09cd\u09b0\u09a4\u09bf\u09a6\u09cd\u09ac\u09a8\u09cd\u09a6\u09cd\u09ac\u09bf\u09a4\u09be  \u09aa\u09be\u09a8",refresh_btn:"\u098f\u0995\u099f\u09bf \u09a8\u09a4\u09c1\u09a8 \u09aa\u09cd\u09b0\u09a4\u09bf\u09a6\u09cd\u09ac\u09a8\u09cd\u09a6\u09cd\u09ac\u09bf\u09a4\u09be  \u09aa\u09be\u09a8",
-instructions_visual:"",instructions_audio:"\u0986\u09aa\u09a8\u09bf \u09af\u09be \u09b6\u09c1\u09a8\u099b\u09c7\u09a8 \u09a4\u09be \u09b2\u09bf\u0996\u09c1\u09a8:",help_btn:"\u09b8\u09b9\u09be\u09df\u09a4\u09be",play_again:"\u0986\u09ac\u09be\u09b0 \u09b8\u09be\u0989\u09a8\u09cd\u09a1 \u09aa\u09cd\u09b2\u09c7 \u0995\u09b0\u09c1\u09a8",cant_hear_this:"MP3 \u09b0\u09c2\u09aa\u09c7 \u09b6\u09ac\u09cd\u09a6 \u09a1\u09be\u0989\u09a8\u09b2\u09cb\u09a1 \u0995\u09b0\u09c1\u09a8",incorrect_try_again:"\u09ac\u09c7\u09a0\u09bf\u0995\u09f7 \u0986\u09ac\u09be\u09b0 \u099a\u09c7\u09b7\u09cd\u099f\u09be \u0995\u09b0\u09c1\u09a8\u09f7",
-image_alt_text:"reCAPTCHA \u099a\u09cd\u09af\u09be\u09b2\u09c7\u099e\u09cd\u099c \u099a\u09bf\u09a4\u09cd\u09b0",privacy_and_terms:"\u0997\u09cb\u09aa\u09a8\u09c0\u09af\u09bc\u09a4\u09be \u0993 \u09b6\u09b0\u09cd\u09a4\u09be\u09ac\u09b2\u09c0"},ca:{visual_challenge:"Obt\u00e9n un repte visual",audio_challenge:"Obteniu una pista sonora",refresh_btn:"Obteniu una pista nova",instructions_visual:"Escriviu el text:",instructions_audio:"Escriviu el que escolteu:",help_btn:"Ajuda",play_again:"Torna a reproduir el so",
-cant_hear_this:"Baixa el so com a MP3",incorrect_try_again:"No \u00e9s correcte. Torna-ho a provar.",image_alt_text:"Imatge del repte de reCAPTCHA",privacy_and_terms:"Privadesa i condicions"},cs:{visual_challenge:"Zobrazit vizu\u00e1ln\u00ed podobu v\u00fdrazu",audio_challenge:"P\u0159ehr\u00e1t zvukovou podobu v\u00fdrazu",refresh_btn:"Zobrazit nov\u00fd v\u00fdraz",instructions_visual:"Zadejte text:",instructions_audio:"Napi\u0161te, co jste sly\u0161eli:",help_btn:"N\u00e1pov\u011bda",play_again:"Znovu p\u0159ehr\u00e1t zvuk",
-cant_hear_this:"St\u00e1hnout zvuk ve form\u00e1tu MP3",incorrect_try_again:"\u0160patn\u011b. Zkuste to znovu.",image_alt_text:"Obr\u00e1zek reCAPTCHA",privacy_and_terms:"Ochrana soukrom\u00ed a smluvn\u00ed podm\u00ednky"},da:{visual_challenge:"Hent en visuel udfordring",audio_challenge:"Hent en lydudfordring",refresh_btn:"Hent en ny udfordring",instructions_visual:"Indtast teksten:",instructions_audio:"Indtast det, du h\u00f8rer:",help_btn:"Hj\u00e6lp",play_again:"Afspil lyden igen",cant_hear_this:"Download lyd som MP3",
-incorrect_try_again:"Forkert. Pr\u00f8v igen.",image_alt_text:"reCAPTCHA-udfordringsbillede",privacy_and_terms:"Privatliv og vilk\u00e5r"},de:{visual_challenge:"Captcha abrufen",audio_challenge:"Audio-Captcha abrufen",refresh_btn:"Neues Captcha abrufen",instructions_visual:"Geben Sie den angezeigten Text ein:",instructions_audio:"Geben Sie das Geh\u00f6rte ein:",help_btn:"Hilfe",play_again:"Wort erneut abspielen",cant_hear_this:"Wort als MP3 herunterladen",incorrect_try_again:"Falsch. Bitte versuchen Sie es erneut.",
-image_alt_text:"reCAPTCHA-Bild",privacy_and_terms:"Datenschutzerkl\u00e4rung & Nutzungsbedingungen"},el:{visual_challenge:"\u039f\u03c0\u03c4\u03b9\u03ba\u03ae \u03c0\u03c1\u03cc\u03ba\u03bb\u03b7\u03c3\u03b7",audio_challenge:"\u0397\u03c7\u03b7\u03c4\u03b9\u03ba\u03ae \u03c0\u03c1\u03cc\u03ba\u03bb\u03b7\u03c3\u03b7",refresh_btn:"\u039d\u03ad\u03b1 \u03c0\u03c1\u03cc\u03ba\u03bb\u03b7\u03c3\u03b7",instructions_visual:"\u03a0\u03bb\u03b7\u03ba\u03c4\u03c1\u03bf\u03bb\u03bf\u03b3\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf \u03ba\u03b5\u03af\u03bc\u03b5\u03bd\u03bf:",
-instructions_audio:"\u03a0\u03bb\u03b7\u03ba\u03c4\u03c1\u03bf\u03bb\u03bf\u03b3\u03ae\u03c3\u03c4\u03b5 \u03cc\u03c4\u03b9 \u03b1\u03ba\u03bf\u03cd\u03c4\u03b5:",help_btn:"\u0392\u03bf\u03ae\u03b8\u03b5\u03b9\u03b1",play_again:"\u0391\u03bd\u03b1\u03c0\u03b1\u03c1\u03b1\u03b3\u03c9\u03b3\u03ae \u03ae\u03c7\u03bf\u03c5 \u03be\u03b1\u03bd\u03ac",cant_hear_this:"\u039b\u03ae\u03c8\u03b7 \u03ae\u03c7\u03bf\u03c5 \u03c9\u03c2 \u039c\u03a13",incorrect_try_again:"\u039b\u03ac\u03b8\u03bf\u03c2. \u0394\u03bf\u03ba\u03b9\u03bc\u03ac\u03c3\u03c4\u03b5 \u03be\u03b1\u03bd\u03ac.",
-image_alt_text:"\u0395\u03b9\u03ba\u03cc\u03bd\u03b1 \u03c0\u03c1\u03cc\u03ba\u03bb\u03b7\u03c3\u03b7\u03c2 reCAPTCHA",privacy_and_terms:"\u0391\u03c0\u03cc\u03c1\u03c1\u03b7\u03c4\u03bf \u03ba\u03b1\u03b9 \u03cc\u03c1\u03bf\u03b9"},"en-GB":t,"en-US":t,es:la,"es-419":{visual_challenge:"Enfrentar un desaf\u00edo visual",audio_challenge:"Enfrentar un desaf\u00edo de audio",refresh_btn:"Enfrentar un nuevo desaf\u00edo",instructions_visual:"Escriba el texto:",instructions_audio:"Escribe lo que escuchas:",
-help_btn:"Ayuda",play_again:"Reproducir sonido de nuevo",cant_hear_this:"Descargar sonido en formato MP3",incorrect_try_again:"Incorrecto. Vuelve a intentarlo.",image_alt_text:"Imagen del desaf\u00edo de la reCAPTCHA",privacy_and_terms:"Privacidad y condiciones"},"es-ES":la,et:{visual_challenge:"Kuva kuvap\u00f5hine robotil\u00f5ks",audio_challenge:"Kuva helip\u00f5hine robotil\u00f5ks",refresh_btn:"Kuva uus robotil\u00f5ks",instructions_visual:"Tippige tekst:",instructions_audio:"Tippige, mida kuulete.",
-help_btn:"Abi",play_again:"Esita heli uuesti",cant_hear_this:"Laadi heli alla MP3-vormingus",incorrect_try_again:"Vale. Proovige uuesti.",image_alt_text:"reCAPTCHA robotil\u00f5ksu kujutis",privacy_and_terms:"Privaatsus ja tingimused"},eu:{visual_challenge:"Eskuratu ikusizko erronka",audio_challenge:"Eskuratu audio-erronka",refresh_btn:"Eskuratu erronka berria",instructions_visual:"",instructions_audio:"Idatzi entzuten duzuna:",help_btn:"Laguntza",play_again:"Erreproduzitu soinua berriro",cant_hear_this:"Deskargatu soinua MP3 gisa",
-incorrect_try_again:"Ez da zuzena. Saiatu berriro.",image_alt_text:"reCAPTCHA erronkaren irudia",privacy_and_terms:"Pribatutasuna eta baldintzak"},fa:{visual_challenge:"\u062f\u0631\u06cc\u0627\u0641\u062a \u06cc\u06a9 \u0645\u0639\u0645\u0627\u06cc \u062f\u06cc\u062f\u0627\u0631\u06cc",audio_challenge:"\u062f\u0631\u06cc\u0627\u0641\u062a \u06cc\u06a9 \u0645\u0639\u0645\u0627\u06cc \u0635\u0648\u062a\u06cc",refresh_btn:"\u062f\u0631\u06cc\u0627\u0641\u062a \u06cc\u06a9 \u0645\u0639\u0645\u0627\u06cc \u062c\u062f\u06cc\u062f",
-instructions_visual:"",instructions_audio:"\u0622\u0646\u0686\u0647 \u0631\u0627 \u06a9\u0647 \u0645\u06cc\u200c\u0634\u0646\u0648\u06cc\u062f \u062a\u0627\u06cc\u067e \u06a9\u0646\u06cc\u062f:",help_btn:"\u0631\u0627\u0647\u0646\u0645\u0627\u06cc\u06cc",play_again:"\u067e\u062e\u0634 \u0645\u062c\u062f\u062f \u0635\u062f\u0627",cant_hear_this:"\u062f\u0627\u0646\u0644\u0648\u062f \u0635\u062f\u0627 \u0628\u0647 \u0635\u0648\u0631\u062a MP3",incorrect_try_again:"\u0646\u0627\u062f\u0631\u0633\u062a. \u062f\u0648\u0628\u0627\u0631\u0647 \u0627\u0645\u062a\u062d\u0627\u0646 \u06a9\u0646\u06cc\u062f.",
-image_alt_text:"\u062a\u0635\u0648\u06cc\u0631 \u0686\u0627\u0644\u0634\u06cc reCAPTCHA",privacy_and_terms:"\u062d\u0631\u06cc\u0645 \u062e\u0635\u0648\u0635\u06cc \u0648 \u0634\u0631\u0627\u06cc\u0637"},fi:{visual_challenge:"Kuvavahvistus",audio_challenge:"\u00c4\u00e4nivahvistus",refresh_btn:"Uusi kuva",instructions_visual:"Kirjoita teksti:",instructions_audio:"Kirjoita kuulemasi:",help_btn:"Ohje",play_again:"Toista \u00e4\u00e4ni uudelleen",cant_hear_this:"Lataa \u00e4\u00e4ni MP3-tiedostona",
-incorrect_try_again:"V\u00e4\u00e4rin. Yrit\u00e4 uudelleen.",image_alt_text:"reCAPTCHA-kuva",privacy_and_terms:"Tietosuoja ja k\u00e4ytt\u00f6ehdot"},fil:ma,fr:na,"fr-CA":{visual_challenge:"Obtenir un test visuel",audio_challenge:"Obtenir un test audio",refresh_btn:"Obtenir un nouveau test",instructions_visual:"Saisissez le texte\u00a0:",instructions_audio:"Tapez ce que vous entendez\u00a0:",help_btn:"Aide",play_again:"Jouer le son de nouveau",cant_hear_this:"T\u00e9l\u00e9charger le son en format MP3",
-incorrect_try_again:"Erreur, essayez \u00e0 nouveau",image_alt_text:"Image reCAPTCHA",privacy_and_terms:"Confidentialit\u00e9 et conditions d'utilisation"},"fr-FR":na,gl:{visual_challenge:"Obter unha proba visual",audio_challenge:"Obter unha proba de audio",refresh_btn:"Obter unha proba nova",instructions_visual:"",instructions_audio:"Escribe o que escoitas:",help_btn:"Axuda",play_again:"Reproducir o son de novo",cant_hear_this:"Descargar son como MP3",incorrect_try_again:"Incorrecto. T\u00e9ntao de novo.",
-image_alt_text:"Imaxe de proba de reCAPTCHA",privacy_and_terms:"Privacidade e condici\u00f3ns"},gu:{visual_challenge:"\u0a8f\u0a95 \u0aa6\u0ac3\u0ab6\u0acd\u0aaf\u0abe\u0aa4\u0acd\u0aae\u0a95 \u0aaa\u0aa1\u0a95\u0abe\u0ab0 \u0aae\u0ac7\u0ab3\u0ab5\u0acb",audio_challenge:"\u0a8f\u0a95 \u0a91\u0aa1\u0abf\u0a93 \u0aaa\u0aa1\u0a95\u0abe\u0ab0 \u0aae\u0ac7\u0ab3\u0ab5\u0acb",refresh_btn:"\u0a8f\u0a95 \u0aa8\u0ab5\u0acb \u0aaa\u0aa1\u0a95\u0abe\u0ab0 \u0aae\u0ac7\u0ab3\u0ab5\u0acb",instructions_visual:"",
-instructions_audio:"\u0aa4\u0aae\u0ac7 \u0a9c\u0ac7 \u0ab8\u0abe\u0a82\u0aad\u0ab3\u0acb \u0a9b\u0acb \u0aa4\u0ac7 \u0ab2\u0a96\u0acb:",help_btn:"\u0ab8\u0ab9\u0abe\u0aaf",play_again:"\u0aa7\u0acd\u0ab5\u0aa8\u0abf \u0aab\u0ab0\u0ac0\u0aa5\u0ac0 \u0a9a\u0ab2\u0abe\u0ab5\u0acb",cant_hear_this:"MP3 \u0aa4\u0ab0\u0ac0\u0a95\u0ac7 \u0aa7\u0acd\u0ab5\u0aa8\u0abf\u0aa8\u0ac7 \u0aa1\u0abe\u0a89\u0aa8\u0ab2\u0acb\u0aa1 \u0a95\u0ab0\u0acb",incorrect_try_again:"\u0a96\u0acb\u0a9f\u0ac1\u0a82. \u0aab\u0ab0\u0ac0 \u0aaa\u0acd\u0ab0\u0aaf\u0abe\u0ab8 \u0a95\u0ab0\u0acb.",
-image_alt_text:"reCAPTCHA \u0aaa\u0aa1\u0a95\u0abe\u0ab0 \u0a9b\u0aac\u0ac0",privacy_and_terms:"\u0a97\u0acb\u0aaa\u0aa8\u0ac0\u0aaf\u0aa4\u0abe \u0a85\u0aa8\u0ac7 \u0ab6\u0ab0\u0aa4\u0acb"},hi:{visual_challenge:"\u0915\u094b\u0908 \u0935\u093f\u091c\u0941\u0905\u0932 \u091a\u0941\u0928\u094c\u0924\u0940 \u0932\u0947\u0902",audio_challenge:"\u0915\u094b\u0908 \u0911\u0921\u093f\u092f\u094b \u091a\u0941\u0928\u094c\u0924\u0940 \u0932\u0947\u0902",refresh_btn:"\u0915\u094b\u0908 \u0928\u0908 \u091a\u0941\u0928\u094c\u0924\u0940 \u0932\u0947\u0902",
-instructions_visual:"\u091f\u0947\u0915\u094d\u0938\u094d\u091f \u091f\u093e\u0907\u092a \u0915\u0930\u0947\u0902:",instructions_audio:"\u091c\u094b \u0906\u092a \u0938\u0941\u0928 \u0930\u0939\u0947 \u0939\u0948\u0902 \u0909\u0938\u0947 \u0932\u093f\u0916\u0947\u0902:",help_btn:"\u0938\u0939\u093e\u092f\u0924\u093e",play_again:"\u0927\u094d\u200d\u0935\u0928\u093f \u092a\u0941\u0928: \u091a\u0932\u093e\u090f\u0902",cant_hear_this:"\u0927\u094d\u200d\u0935\u0928\u093f \u0915\u094b MP3 \u0915\u0947 \u0930\u0942\u092a \u092e\u0947\u0902 \u0921\u093e\u0909\u0928\u0932\u094b\u0921 \u0915\u0930\u0947\u0902",
-incorrect_try_again:"\u0917\u0932\u0924. \u092a\u0941\u0928: \u092a\u094d\u0930\u092f\u093e\u0938 \u0915\u0930\u0947\u0902.",image_alt_text:"reCAPTCHA \u091a\u0941\u0928\u094c\u0924\u0940 \u091a\u093f\u0924\u094d\u0930",privacy_and_terms:"\u0917\u094b\u092a\u0928\u0940\u092f\u0924\u093e \u0914\u0930 \u0936\u0930\u094d\u0924\u0947\u0902"},hr:{visual_challenge:"Dohvati vizualni upit",audio_challenge:"Dohvati zvu\u010dni upit",refresh_btn:"Dohvati novi upit",instructions_visual:"Unesite tekst:",instructions_audio:"Upi\u0161ite \u0161to \u010dujete:",
-help_btn:"Pomo\u0107",play_again:"Ponovi zvuk",cant_hear_this:"Preuzmi zvuk u MP3 formatu",incorrect_try_again:"Nije to\u010dno. Poku\u0161ajte ponovno.",image_alt_text:"Slikovni izazov reCAPTCHA",privacy_and_terms:"Privatnost i odredbe"},hu:{visual_challenge:"Vizu\u00e1lis kih\u00edv\u00e1s k\u00e9r\u00e9se",audio_challenge:"Hangkih\u00edv\u00e1s k\u00e9r\u00e9se",refresh_btn:"\u00daj kih\u00edv\u00e1s k\u00e9r\u00e9se",instructions_visual:"\u00cdrja be a sz\u00f6veget:",instructions_audio:"\u00cdrja le, amit hall:",
-help_btn:"S\u00fag\u00f3",play_again:"Hang ism\u00e9telt lej\u00e1tsz\u00e1sa",cant_hear_this:"Hang let\u00f6lt\u00e9se MP3 form\u00e1tumban",incorrect_try_again:"Hib\u00e1s. Pr\u00f3b\u00e1lkozzon \u00fajra.",image_alt_text:"reCAPTCHA ellen\u0151rz\u0151 k\u00e9p",privacy_and_terms:"Adatv\u00e9delem \u00e9s Szerz\u0151d\u00e9si Felt\u00e9telek"},hy:{visual_challenge:"\u054d\u057f\u0561\u0576\u0561\u056c \u057f\u0565\u057d\u0578\u0572\u0561\u056f\u0561\u0576 \u056d\u0576\u0564\u056b\u0580",audio_challenge:"\u054d\u057f\u0561\u0576\u0561\u056c \u0571\u0561\u0575\u0576\u0561\u0575\u056b\u0576 \u056d\u0576\u0564\u056b\u0580",
-refresh_btn:"\u054d\u057f\u0561\u0576\u0561\u056c \u0576\u0578\u0580 \u056d\u0576\u0564\u056b\u0580",instructions_visual:"\u0544\u0578\u0582\u057f\u0584\u0561\u0563\u0580\u0565\u0584 \u057f\u0565\u0584\u057d\u057f\u0568\u055d",instructions_audio:"\u0544\u0578\u0582\u057f\u0584\u0561\u0563\u0580\u0565\u0584 \u0561\u0575\u0576, \u056b\u0576\u0579 \u056c\u057d\u0578\u0582\u0574 \u0565\u0584\u055d",help_btn:"\u0555\u0563\u0576\u0578\u0582\u0569\u0575\u0578\u0582\u0576",play_again:"\u0546\u057e\u0561\u0563\u0561\u0580\u056f\u0565\u056c \u0571\u0561\u0575\u0576\u0568 \u056f\u0580\u056f\u056b\u0576",
-cant_hear_this:"\u0532\u0565\u057c\u0576\u0565\u056c \u0571\u0561\u0575\u0576\u0568 \u0578\u0580\u057a\u0565\u057d MP3",incorrect_try_again:"\u054d\u056d\u0561\u056c \u0567: \u0553\u0578\u0580\u0571\u0565\u0584 \u056f\u0580\u056f\u056b\u0576:",image_alt_text:"reCAPTCHA \u057a\u0561\u057f\u056f\u0565\u0580\u0578\u057e \u056d\u0576\u0564\u056b\u0580",privacy_and_terms:"\u0533\u0561\u0572\u057f\u0576\u056b\u0578\u0582\u0569\u0575\u0561\u0576 & \u057a\u0561\u0575\u0574\u0561\u0576\u0576\u0565\u0580"},
-id:oa,is:{visual_challenge:"F\u00e1 a\u00f0gangspr\u00f3f sem mynd",audio_challenge:"F\u00e1 a\u00f0gangspr\u00f3f sem hlj\u00f3\u00f0skr\u00e1",refresh_btn:"F\u00e1 n\u00fdtt a\u00f0gangspr\u00f3f",instructions_visual:"",instructions_audio:"Sl\u00e1\u00f0u inn \u00fea\u00f0 sem \u00fe\u00fa heyrir:",help_btn:"Hj\u00e1lp",play_again:"Spila hlj\u00f3\u00f0 aftur",cant_hear_this:"S\u00e6kja hlj\u00f3\u00f0 sem MP3",incorrect_try_again:"Rangt. Reyndu aftur.",image_alt_text:"mynd reCAPTCHA a\u00f0gangspr\u00f3fs",
-privacy_and_terms:"Pers\u00f3nuvernd og skilm\u00e1lar"},it:{visual_challenge:"Verifica visiva",audio_challenge:"Verifica audio",refresh_btn:"Nuova verifica",instructions_visual:"Digita il testo:",instructions_audio:"Digita ci\u00f2 che senti:",help_btn:"Guida",play_again:"Riproduci di nuovo audio",cant_hear_this:"Scarica audio in MP3",incorrect_try_again:"Sbagliato. Riprova.",image_alt_text:"Immagine di verifica reCAPTCHA",privacy_and_terms:"Privacy e Termini"},iw:pa,ja:{visual_challenge:"\u753b\u50cf\u3067\u78ba\u8a8d\u3057\u307e\u3059",
-audio_challenge:"\u97f3\u58f0\u3067\u78ba\u8a8d\u3057\u307e\u3059",refresh_btn:"\u5225\u306e\u5358\u8a9e\u3067\u3084\u308a\u76f4\u3057\u307e\u3059",instructions_visual:"\u30c6\u30ad\u30b9\u30c8\u3092\u5165\u529b:",instructions_audio:"\u805e\u3053\u3048\u305f\u5358\u8a9e\u3092\u5165\u529b\u3057\u307e\u3059:",help_btn:"\u30d8\u30eb\u30d7",play_again:"\u3082\u3046\u4e00\u5ea6\u805e\u304f",cant_hear_this:"MP3 \u3067\u97f3\u58f0\u3092\u30c0\u30a6\u30f3\u30ed\u30fc\u30c9",incorrect_try_again:"\u6b63\u3057\u304f\u3042\u308a\u307e\u305b\u3093\u3002\u3082\u3046\u4e00\u5ea6\u3084\u308a\u76f4\u3057\u3066\u304f\u3060\u3055\u3044\u3002",
-image_alt_text:"reCAPTCHA \u78ba\u8a8d\u7528\u753b\u50cf",privacy_and_terms:"\u30d7\u30e9\u30a4\u30d0\u30b7\u30fc\u3068\u5229\u7528\u898f\u7d04"},kn:{visual_challenge:"\u0ca6\u0cc3\u0cb6\u0ccd\u0caf \u0cb8\u0cb5\u0cbe\u0cb2\u0cca\u0c82\u0ca6\u0ca8\u0ccd\u0ca8\u0cc1 \u0cb8\u0ccd\u0cb5\u0cc0\u0c95\u0cb0\u0cbf\u0cb8\u0cbf",audio_challenge:"\u0c86\u0ca1\u0cbf\u0caf\u0ccb \u0cb8\u0cb5\u0cbe\u0cb2\u0cca\u0c82\u0ca6\u0ca8\u0ccd\u0ca8\u0cc1 \u0cb8\u0ccd\u0cb5\u0cc0\u0c95\u0cb0\u0cbf\u0cb8\u0cbf",refresh_btn:"\u0cb9\u0cca\u0cb8 \u0cb8\u0cb5\u0cbe\u0cb2\u0cca\u0c82\u0ca6\u0ca8\u0ccd\u0ca8\u0cc1 \u0caa\u0ca1\u0cc6\u0caf\u0cbf\u0cb0\u0cbf",
-instructions_visual:"",instructions_audio:"\u0ca8\u0cbf\u0cae\u0c97\u0cc6 \u0c95\u0cc7\u0cb3\u0cbf\u0cb8\u0cc1\u0cb5\u0cc1\u0ca6\u0ca8\u0ccd\u0ca8\u0cc1 \u0c9f\u0cc8\u0caa\u0ccd\u200c \u0cae\u0cbe\u0ca1\u0cbf:",help_btn:"\u0cb8\u0cb9\u0cbe\u0caf",play_again:"\u0ca7\u0ccd\u0cb5\u0ca8\u0cbf\u0caf\u0ca8\u0ccd\u0ca8\u0cc1 \u0cae\u0ca4\u0ccd\u0ca4\u0cc6 \u0caa\u0ccd\u0cb2\u0cc7 \u0cae\u0cbe\u0ca1\u0cbf",cant_hear_this:"\u0ca7\u0ccd\u0cb5\u0ca8\u0cbf\u0caf\u0ca8\u0ccd\u0ca8\u0cc1 MP3 \u0cb0\u0cc2\u0caa\u0ca6\u0cb2\u0ccd\u0cb2\u0cbf \u0ca1\u0ccc\u0ca8\u0ccd\u200c\u0cb2\u0ccb\u0ca1\u0ccd \u0cae\u0cbe\u0ca1\u0cbf",
-incorrect_try_again:"\u0ca4\u0caa\u0ccd\u0caa\u0cbe\u0c97\u0cbf\u0ca6\u0cc6. \u0cae\u0ca4\u0ccd\u0ca4\u0cca\u0cae\u0ccd\u0cae\u0cc6 \u0caa\u0ccd\u0cb0\u0caf\u0ca4\u0ccd\u0ca8\u0cbf\u0cb8\u0cbf.",image_alt_text:"reCAPTCHA \u0cb8\u0cb5\u0cbe\u0cb2\u0cc1 \u0c9a\u0cbf\u0ca4\u0ccd\u0cb0",privacy_and_terms:"\u0c97\u0ccc\u0caa\u0ccd\u0caf\u0ca4\u0cc6 \u0cae\u0ca4\u0ccd\u0ca4\u0cc1 \u0ca8\u0cbf\u0caf\u0cae\u0c97\u0cb3\u0cc1"},ko:{visual_challenge:"\uadf8\ub9bc\uc73c\ub85c \ubcf4\uc548\ubb38\uc790 \ubc1b\uae30",
-audio_challenge:"\uc74c\uc131\uc73c\ub85c \ubcf4\uc548\ubb38\uc790 \ubc1b\uae30",refresh_btn:"\ubcf4\uc548\ubb38\uc790 \uc0c8\ub85c \ubc1b\uae30",instructions_visual:"\ud14d\uc2a4\ud2b8 \uc785\ub825:",instructions_audio:"\uc74c\uc131 \ubcf4\uc548\ubb38\uc790 \uc785\ub825:",help_btn:"\ub3c4\uc6c0\ub9d0",play_again:"\uc74c\uc131 \ub2e4\uc2dc \ub4e3\uae30",cant_hear_this:"\uc74c\uc131\uc744 MP3\ub85c \ub2e4\uc6b4\ub85c\ub4dc",incorrect_try_again:"\ud2c0\ub838\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574 \uc8fc\uc138\uc694.",
-image_alt_text:"reCAPTCHA \ubcf4\uc548\ubb38\uc790 \uc774\ubbf8\uc9c0",privacy_and_terms:"\uac1c\uc778\uc815\ubcf4 \ubcf4\ud638 \ubc0f \uc57d\uad00"},ln:na,lt:{visual_challenge:"Gauti vaizdin\u012f atpa\u017einimo test\u0105",audio_challenge:"Gauti garso atpa\u017einimo test\u0105",refresh_btn:"Gauti nauj\u0105 atpa\u017einimo test\u0105",instructions_visual:"\u012eveskite tekst\u0105:",instructions_audio:"\u012eveskite tai, k\u0105 girdite:",help_btn:"Pagalba",play_again:"Dar kart\u0105 paleisti gars\u0105",
-cant_hear_this:"Atsisi\u0173sti gars\u0105 kaip MP3",incorrect_try_again:"Neteisingai. Bandykite dar kart\u0105.",image_alt_text:"Testo \u201ereCAPTCHA\u201c vaizdas",privacy_and_terms:"Privatumas ir s\u0105lygos"},lv:{visual_challenge:"Sa\u0146emt vizu\u0101lu izaicin\u0101jumu",audio_challenge:"Sa\u0146emt audio izaicin\u0101jumu",refresh_btn:"Sa\u0146emt jaunu izaicin\u0101jumu",instructions_visual:"Ievadiet tekstu:",instructions_audio:"Ierakstiet dzirdamo:",help_btn:"Pal\u012bdz\u012bba",play_again:"V\u0113lreiz atska\u0146ot ska\u0146u",
-cant_hear_this:"Lejupiel\u0101d\u0113t ska\u0146u MP3\u00a0form\u0101t\u0101",incorrect_try_again:"Nepareizi. M\u0113\u0123iniet v\u0113lreiz.",image_alt_text:"reCAPTCHA izaicin\u0101juma att\u0113ls",privacy_and_terms:"Konfidencialit\u0101te un noteikumi"},ml:{visual_challenge:"\u0d12\u0d30\u0d41 \u0d26\u0d43\u0d36\u0d4d\u0d2f \u0d1a\u0d32\u0d1e\u0d4d\u0d1a\u0d4d \u0d28\u0d47\u0d1f\u0d41\u0d15",audio_challenge:"\u0d12\u0d30\u0d41 \u0d13\u0d21\u0d3f\u0d2f\u0d4b \u0d1a\u0d32\u0d1e\u0d4d\u0d1a\u0d4d \u0d28\u0d47\u0d1f\u0d41\u0d15",
-refresh_btn:"\u0d12\u0d30\u0d41 \u0d2a\u0d41\u0d24\u0d3f\u0d2f \u0d1a\u0d32\u0d1e\u0d4d\u0d1a\u0d4d \u0d28\u0d47\u0d1f\u0d41\u0d15",instructions_visual:"",instructions_audio:"\u0d15\u0d47\u0d7e\u0d15\u0d4d\u0d15\u0d41\u0d28\u0d4d\u0d28\u0d24\u0d4d \u0d1f\u0d48\u0d2a\u0d4d\u0d2a\u0d4d \u0d1a\u0d46\u0d2f\u0d4d\u0d2f\u0d42:",help_btn:"\u0d38\u0d39\u0d3e\u0d2f\u0d02",play_again:"\u0d36\u0d2c\u0d4d\u200c\u0d26\u0d02 \u0d35\u0d40\u0d23\u0d4d\u0d1f\u0d41\u0d02 \u0d2a\u0d4d\u0d32\u0d47 \u0d1a\u0d46\u0d2f\u0d4d\u0d2f\u0d41\u0d15",
-cant_hear_this:"\u0d36\u0d2c\u0d4d\u200c\u0d26\u0d02 MP3 \u0d06\u0d2f\u0d3f \u0d21\u0d57\u0d7a\u0d32\u0d4b\u0d21\u0d4d \u0d1a\u0d46\u0d2f\u0d4d\u0d2f\u0d41\u0d15",incorrect_try_again:"\u0d24\u0d46\u0d31\u0d4d\u0d31\u0d3e\u0d23\u0d4d. \u0d35\u0d40\u0d23\u0d4d\u0d1f\u0d41\u0d02 \u0d36\u0d4d\u0d30\u0d2e\u0d3f\u0d15\u0d4d\u0d15\u0d41\u0d15.",image_alt_text:"reCAPTCHA \u0d1a\u0d32\u0d1e\u0d4d\u0d1a\u0d4d \u0d07\u0d2e\u0d47\u0d1c\u0d4d",privacy_and_terms:"\u0d38\u0d4d\u0d35\u0d15\u0d3e\u0d30\u0d4d\u0d2f\u0d24\u0d2f\u0d41\u0d02 \u0d28\u0d3f\u0d2c\u0d28\u0d4d\u0d27\u0d28\u0d15\u0d33\u0d41\u0d02"},
-mr:{visual_challenge:"\u0926\u0943\u0936\u094d\u200d\u092f\u092e\u093e\u0928 \u0906\u0935\u094d\u0939\u093e\u0928 \u092a\u094d\u0930\u093e\u092a\u094d\u0924 \u0915\u0930\u093e",audio_challenge:"\u0911\u0921\u0940\u0913 \u0906\u0935\u094d\u0939\u093e\u0928 \u092a\u094d\u0930\u093e\u092a\u094d\u0924 \u0915\u0930\u093e",refresh_btn:"\u090f\u0915 \u0928\u0935\u0940\u0928 \u0906\u0935\u094d\u0939\u093e\u0928 \u092a\u094d\u0930\u093e\u092a\u094d\u0924 \u0915\u0930\u093e",instructions_visual:"",instructions_audio:"\u0906\u092a\u0932\u094d\u092f\u093e\u0932\u093e \u091c\u0947 \u0910\u0915\u0942 \u092f\u0947\u0908\u0932 \u0924\u0947 \u091f\u093e\u0907\u092a \u0915\u0930\u093e:",
-help_btn:"\u092e\u0926\u0924",play_again:"\u0927\u094d\u200d\u0935\u0928\u0940 \u092a\u0941\u0928\u094d\u0939\u093e \u092a\u094d\u200d\u0932\u0947 \u0915\u0930\u093e",cant_hear_this:"MP3 \u0930\u0941\u092a\u093e\u0924 \u0927\u094d\u200d\u0935\u0928\u0940 \u0921\u093e\u0909\u0928\u0932\u094b\u0921 \u0915\u0930\u093e",incorrect_try_again:"\u0905\u092f\u094b\u0917\u094d\u200d\u092f. \u092a\u0941\u0928\u094d\u200d\u0939\u093e \u092a\u094d\u0930\u092f\u0924\u094d\u200d\u0928 \u0915\u0930\u093e.",image_alt_text:"reCAPTCHA \u0906\u0935\u094d\u200d\u0939\u093e\u0928 \u092a\u094d\u0930\u0924\u093f\u092e\u093e",
-privacy_and_terms:"\u0917\u094b\u092a\u0928\u0940\u092f\u0924\u093e \u0906\u0923\u093f \u0905\u091f\u0940"},ms:{visual_challenge:"Dapatkan cabaran visual",audio_challenge:"Dapatkan cabaran audio",refresh_btn:"Dapatkan cabaran baru",instructions_visual:"Taipkan teksnya:",instructions_audio:"Taip apa yang didengari:",help_btn:"Bantuan",play_again:"Mainkan bunyi sekali lagi",cant_hear_this:"Muat turun bunyi sebagai MP3",incorrect_try_again:"Tidak betul. Cuba lagi.",image_alt_text:"Imej cabaran reCAPTCHA",
-privacy_and_terms:"Privasi & Syarat"},nl:{visual_challenge:"Een visuele uitdaging proberen",audio_challenge:"Een audio-uitdaging proberen",refresh_btn:"Een nieuwe uitdaging proberen",instructions_visual:"Typ de tekst:",instructions_audio:"Typ wat u hoort:",help_btn:"Help",play_again:"Geluid opnieuw afspelen",cant_hear_this:"Geluid downloaden als MP3",incorrect_try_again:"Onjuist. Probeer het opnieuw.",image_alt_text:"reCAPTCHA-uitdagingsafbeelding",privacy_and_terms:"Privacy en voorwaarden"},no:{visual_challenge:"F\u00e5 en bildeutfordring",
-audio_challenge:"F\u00e5 en lydutfordring",refresh_btn:"F\u00e5 en ny utfordring",instructions_visual:"Skriv inn teksten:",instructions_audio:"Skriv inn det du h\u00f8rer:",help_btn:"Hjelp",play_again:"Spill av lyd p\u00e5 nytt",cant_hear_this:"Last ned lyd som MP3",incorrect_try_again:"Feil. Pr\u00f8v p\u00e5 nytt.",image_alt_text:"reCAPTCHA-utfordringsbilde",privacy_and_terms:"Personvern og vilk\u00e5r"},pl:{visual_challenge:"Poka\u017c podpowied\u017a wizualn\u0105",audio_challenge:"Odtw\u00f3rz podpowied\u017a d\u017awi\u0119kow\u0105",
-refresh_btn:"Nowa podpowied\u017a",instructions_visual:"Przepisz tekst:",instructions_audio:"Wpisz us\u0142yszane s\u0142owa:",help_btn:"Pomoc",play_again:"Odtw\u00f3rz d\u017awi\u0119k ponownie",cant_hear_this:"Pobierz d\u017awi\u0119k jako plik MP3",incorrect_try_again:"Nieprawid\u0142owo. Spr\u00f3buj ponownie.",image_alt_text:"Zadanie obrazkowe reCAPTCHA",privacy_and_terms:"Prywatno\u015b\u0107 i warunki"},pt:qa,"pt-BR":qa,"pt-PT":{visual_challenge:"Obter um desafio visual",audio_challenge:"Obter um desafio de \u00e1udio",
-refresh_btn:"Obter um novo desafio",instructions_visual:"Introduza o texto:",instructions_audio:"Escreva o que ouvir:",help_btn:"Ajuda",play_again:"Reproduzir som novamente",cant_hear_this:"Transferir som como MP3",incorrect_try_again:"Incorreto. Tente novamente.",image_alt_text:"Imagem de teste reCAPTCHA",privacy_and_terms:"Privacidade e Termos de Utiliza\u00e7\u00e3o"},ro:ra,ru:{visual_challenge:"\u0412\u0438\u0437\u0443\u0430\u043b\u044c\u043d\u0430\u044f \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0430",
-audio_challenge:"\u0417\u0432\u0443\u043a\u043e\u0432\u0430\u044f \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0430",refresh_btn:"\u041e\u0431\u043d\u043e\u0432\u0438\u0442\u044c",instructions_visual:"\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0442\u0435\u043a\u0441\u0442:",instructions_audio:"\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0442\u043e, \u0447\u0442\u043e \u0441\u043b\u044b\u0448\u0438\u0442\u0435:",help_btn:"\u0421\u043f\u0440\u0430\u0432\u043a\u0430",play_again:"\u041f\u0440\u043e\u0441\u043b\u0443\u0448\u0430\u0442\u044c \u0435\u0449\u0435 \u0440\u0430\u0437",
-cant_hear_this:"\u0417\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044c MP3-\u0444\u0430\u0439\u043b",incorrect_try_again:"\u041d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u043e. \u041f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443.",image_alt_text:"\u041f\u0440\u043e\u0432\u0435\u0440\u043a\u0430 \u043f\u043e \u0441\u043b\u043e\u0432\u0443 reCAPTCHA",privacy_and_terms:"\u041f\u0440\u0430\u0432\u0438\u043b\u0430 \u0438 \u043f\u0440\u0438\u043d\u0446\u0438\u043f\u044b"},
-sk:{visual_challenge:"Zobrazi\u0165 vizu\u00e1lnu podobu",audio_challenge:"Prehra\u0165 zvukov\u00fa podobu",refresh_btn:"Zobrazi\u0165 nov\u00fd v\u00fdraz",instructions_visual:"Zadajte text:",instructions_audio:"Zadajte, \u010do po\u010dujete:",help_btn:"Pomocn\u00edk",play_again:"Znova prehra\u0165 zvuk",cant_hear_this:"Prevzia\u0165 zvuk v podobe s\u00faboru MP3",incorrect_try_again:"Nespr\u00e1vne. Sk\u00faste to znova.",image_alt_text:"Obr\u00e1zok zadania reCAPTCHA",privacy_and_terms:"Ochrana osobn\u00fdch \u00fadajov a Zmluvn\u00e9 podmienky"},
-sl:{visual_challenge:"Vizualni preskus",audio_challenge:"Zvo\u010dni preskus",refresh_btn:"Nov preskus",instructions_visual:"Vnesite besedilo:",instructions_audio:"Natipkajte, kaj sli\u0161ite:",help_btn:"Pomo\u010d",play_again:"Znova predvajaj zvok",cant_hear_this:"Prenesi zvok kot MP3",incorrect_try_again:"Napa\u010dno. Poskusite znova.",image_alt_text:"Slika izziva reCAPTCHA",privacy_and_terms:"Zasebnost in pogoji"},sr:{visual_challenge:"\u041f\u0440\u0438\u043c\u0438\u0442\u0435 \u0432\u0438\u0437\u0443\u0435\u043b\u043d\u0438 \u0443\u043f\u0438\u0442",
-audio_challenge:"\u041f\u0440\u0438\u043c\u0438\u0442\u0435 \u0430\u0443\u0434\u0438\u043e \u0443\u043f\u0438\u0442",refresh_btn:"\u041f\u0440\u0438\u043c\u0438\u0442\u0435 \u043d\u043e\u0432\u0438 \u0443\u043f\u0438\u0442",instructions_visual:"\u0423\u043d\u0435\u0441\u0438\u0442\u0435 \u0442\u0435\u043a\u0441\u0442:",instructions_audio:"\u041e\u0442\u043a\u0443\u0446\u0430\u0458\u0442\u0435 \u043e\u043d\u043e \u0448\u0442\u043e \u0447\u0443\u0458\u0435\u0442\u0435:",help_btn:"\u041f\u043e\u043c\u043e\u045b",
-play_again:"\u041f\u043e\u043d\u043e\u0432\u043e \u043f\u0443\u0441\u0442\u0438 \u0437\u0432\u0443\u043a",cant_hear_this:"\u041f\u0440\u0435\u0443\u0437\u043c\u0438 \u0437\u0432\u0443\u043a \u043a\u0430\u043e MP3 \u0441\u043d\u0438\u043c\u0430\u043a",incorrect_try_again:"\u041d\u0435\u0442\u0430\u0447\u043d\u043e. \u041f\u043e\u043a\u0443\u0448\u0430\u0458\u0442\u0435 \u043f\u043e\u043d\u043e\u0432\u043e.",image_alt_text:"\u0421\u043b\u0438\u043a\u0430 reCAPTCHA \u043f\u0440\u043e\u0432\u0435\u0440\u0435",
-privacy_and_terms:"\u041f\u0440\u0438\u0432\u0430\u0442\u043d\u043e\u0441\u0442 \u0438 \u0443\u0441\u043b\u043e\u0432\u0438"},sv:{visual_challenge:"H\u00e4mta captcha i bildformat",audio_challenge:"H\u00e4mta captcha i ljudformat",refresh_btn:"H\u00e4mta ny captcha",instructions_visual:"Skriv texten:",instructions_audio:"Skriv det du h\u00f6r:",help_btn:"Hj\u00e4lp",play_again:"Spela upp ljudet igen",cant_hear_this:"H\u00e4mta ljud som MP3",incorrect_try_again:"Fel. F\u00f6rs\u00f6k igen.",image_alt_text:"reCAPTCHA-bild",
-privacy_and_terms:"Sekretess och villkor"},sw:{visual_challenge:"Pata herufi za kusoma",audio_challenge:"Pata herufi za kusikiliza",refresh_btn:"Pata herufi mpya",instructions_visual:"",instructions_audio:"Charaza unachosikia:",help_btn:"Usaidizi",play_again:"Cheza sauti tena",cant_hear_this:"Pakua sauti kama MP3",incorrect_try_again:"Sio sahihi. Jaribu tena.",image_alt_text:"picha ya changamoto ya reCAPTCHA",privacy_and_terms:"Faragha & Masharti"},ta:{visual_challenge:"\u0baa\u0bbe\u0bb0\u0bcd\u0bb5\u0bc8 \u0b9a\u0bc7\u0bb2\u0b9e\u0bcd\u0b9a\u0bc8\u0baa\u0bcd \u0baa\u0bc6\u0bb1\u0bc1\u0b95",
-audio_challenge:"\u0b86\u0b9f\u0bbf\u0baf\u0bcb \u0b9a\u0bc7\u0bb2\u0b9e\u0bcd\u0b9a\u0bc8\u0baa\u0bcd \u0baa\u0bc6\u0bb1\u0bc1\u0b95",refresh_btn:"\u0baa\u0bc1\u0ba4\u0bbf\u0baf \u0b9a\u0bc7\u0bb2\u0b9e\u0bcd\u0b9a\u0bc8\u0baa\u0bcd \u0baa\u0bc6\u0bb1\u0bc1\u0b95",instructions_visual:"",instructions_audio:"\u0b95\u0bc7\u0b9f\u0bcd\u0baa\u0ba4\u0bc8 \u0b9f\u0bc8\u0baa\u0bcd \u0b9a\u0bc6\u0baf\u0bcd\u0b95:",help_btn:"\u0b89\u0ba4\u0bb5\u0bbf",play_again:"\u0b92\u0bb2\u0bbf\u0baf\u0bc8 \u0bae\u0bc0\u0ba3\u0bcd\u0b9f\u0bc1\u0bae\u0bcd \u0b87\u0baf\u0b95\u0bcd\u0b95\u0bc1",
-cant_hear_this:"\u0b92\u0bb2\u0bbf\u0baf\u0bc8 MP3 \u0b86\u0b95 \u0baa\u0ba4\u0bbf\u0bb5\u0bbf\u0bb1\u0b95\u0bcd\u0b95\u0bc1\u0b95",incorrect_try_again:"\u0ba4\u0bb5\u0bb1\u0bbe\u0ba9\u0ba4\u0bc1. \u0bae\u0bc0\u0ba3\u0bcd\u0b9f\u0bc1\u0bae\u0bcd \u0bae\u0bc1\u0baf\u0bb2\u0bb5\u0bc1\u0bae\u0bcd.",image_alt_text:"reCAPTCHA \u0b9a\u0bc7\u0bb2\u0b9e\u0bcd\u0b9a\u0bcd \u0baa\u0b9f\u0bae\u0bcd",privacy_and_terms:"\u0ba4\u0ba9\u0bbf\u0baf\u0bc1\u0bb0\u0bbf\u0bae\u0bc8 & \u0bb5\u0bbf\u0ba4\u0bbf\u0bae\u0bc1\u0bb1\u0bc8\u0b95\u0bb3\u0bcd"},
-te:{visual_challenge:"\u0c12\u0c15 \u0c26\u0c43\u0c36\u0c4d\u0c2f\u0c2e\u0c3e\u0c28 \u0c38\u0c35\u0c3e\u0c32\u0c41\u0c28\u0c41 \u0c38\u0c4d\u0c35\u0c40\u0c15\u0c30\u0c3f\u0c02\u0c1a\u0c02\u0c21\u0c3f",audio_challenge:"\u0c12\u0c15 \u0c06\u0c21\u0c3f\u0c2f\u0c4b \u0c38\u0c35\u0c3e\u0c32\u0c41\u0c28\u0c41 \u0c38\u0c4d\u0c35\u0c40\u0c15\u0c30\u0c3f\u0c02\u0c1a\u0c02\u0c21\u0c3f",refresh_btn:"\u0c15\u0c4d\u0c30\u0c4a\u0c24\u0c4d\u0c24 \u0c38\u0c35\u0c3e\u0c32\u0c41\u0c28\u0c41 \u0c38\u0c4d\u0c35\u0c40\u0c15\u0c30\u0c3f\u0c02\u0c1a\u0c02\u0c21\u0c3f",
-instructions_visual:"",instructions_audio:"\u0c2e\u0c40\u0c30\u0c41 \u0c35\u0c3f\u0c28\u0c4d\u0c28\u0c26\u0c3f \u0c1f\u0c48\u0c2a\u0c4d \u0c1a\u0c47\u0c2f\u0c02\u0c21\u0c3f:",help_btn:"\u0c38\u0c39\u0c3e\u0c2f\u0c02",play_again:"\u0c27\u0c4d\u0c35\u0c28\u0c3f\u0c28\u0c3f \u0c2e\u0c33\u0c4d\u0c32\u0c40 \u0c2a\u0c4d\u0c32\u0c47 \u0c1a\u0c47\u0c2f\u0c3f",cant_hear_this:"\u0c27\u0c4d\u0c35\u0c28\u0c3f\u0c28\u0c3f MP3 \u0c35\u0c32\u0c46 \u0c21\u0c4c\u0c28\u0c4d\u200c\u0c32\u0c4b\u0c21\u0c4d \u0c1a\u0c47\u0c2f\u0c3f",
-incorrect_try_again:"\u0c24\u0c2a\u0c4d\u0c2a\u0c41. \u0c2e\u0c33\u0c4d\u0c32\u0c40 \u0c2a\u0c4d\u0c30\u0c2f\u0c24\u0c4d\u0c28\u0c3f\u0c02\u0c1a\u0c02\u0c21\u0c3f.",image_alt_text:"reCAPTCHA \u0c38\u0c35\u0c3e\u0c32\u0c41 \u0c1a\u0c3f\u0c24\u0c4d\u0c30\u0c02",privacy_and_terms:"\u0c17\u0c4b\u0c2a\u0c4d\u0c2f\u0c24 & \u0c28\u0c3f\u0c2c\u0c02\u0c27\u0c28\u0c32\u0c41"},th:{visual_challenge:"\u0e23\u0e31\u0e1a\u0e04\u0e27\u0e32\u0e21\u0e17\u0e49\u0e32\u0e17\u0e32\u0e22\u0e14\u0e49\u0e32\u0e19\u0e20\u0e32\u0e1e",
-audio_challenge:"\u0e23\u0e31\u0e1a\u0e04\u0e27\u0e32\u0e21\u0e17\u0e49\u0e32\u0e17\u0e32\u0e22\u0e14\u0e49\u0e32\u0e19\u0e40\u0e2a\u0e35\u0e22\u0e07",refresh_btn:"\u0e23\u0e31\u0e1a\u0e04\u0e27\u0e32\u0e21\u0e17\u0e49\u0e32\u0e17\u0e32\u0e22\u0e43\u0e2b\u0e21\u0e48",instructions_visual:"\u0e1e\u0e34\u0e21\u0e1e\u0e4c\u0e02\u0e49\u0e2d\u0e04\u0e27\u0e32\u0e21\u0e19\u0e35\u0e49:",instructions_audio:"\u0e1e\u0e34\u0e21\u0e1e\u0e4c\u0e2a\u0e34\u0e48\u0e07\u0e17\u0e35\u0e48\u0e04\u0e38\u0e13\u0e44\u0e14\u0e49\u0e22\u0e34\u0e19:",
-help_btn:"\u0e04\u0e27\u0e32\u0e21\u0e0a\u0e48\u0e27\u0e22\u0e40\u0e2b\u0e25\u0e37\u0e2d",play_again:"\u0e40\u0e25\u0e48\u0e19\u0e40\u0e2a\u0e35\u0e22\u0e07\u0e2d\u0e35\u0e01\u0e04\u0e23\u0e31\u0e49\u0e07",cant_hear_this:"\u0e14\u0e32\u0e27\u0e42\u0e2b\u0e25\u0e14\u0e40\u0e2a\u0e35\u0e22\u0e07\u0e40\u0e1b\u0e47\u0e19 MP3",incorrect_try_again:"\u0e44\u0e21\u0e48\u0e16\u0e39\u0e01\u0e15\u0e49\u0e2d\u0e07 \u0e25\u0e2d\u0e07\u0e2d\u0e35\u0e01\u0e04\u0e23\u0e31\u0e49\u0e07",image_alt_text:"\u0e23\u0e2b\u0e31\u0e2a\u0e20\u0e32\u0e1e reCAPTCHA",
-privacy_and_terms:"\u0e19\u0e42\u0e22\u0e1a\u0e32\u0e22\u0e2a\u0e48\u0e27\u0e19\u0e1a\u0e38\u0e04\u0e04\u0e25\u0e41\u0e25\u0e30\u0e02\u0e49\u0e2d\u0e01\u0e33\u0e2b\u0e19\u0e14"},tr:{visual_challenge:"G\u00f6rsel sorgu al",audio_challenge:"Sesli sorgu al",refresh_btn:"Yeniden y\u00fckle",instructions_visual:"Metni yaz\u0131n:",instructions_audio:"Duydu\u011funuzu yaz\u0131n:",help_btn:"Yard\u0131m",play_again:"Sesi tekrar \u00e7al",cant_hear_this:"Sesi MP3 olarak indir",incorrect_try_again:"Yanl\u0131\u015f. Tekrar deneyin.",
-image_alt_text:"reCAPTCHA sorusu resmi",privacy_and_terms:"Gizlilik ve \u015eartlar"},uk:{visual_challenge:"\u041e\u0442\u0440\u0438\u043c\u0430\u0442\u0438 \u0432\u0456\u0437\u0443\u0430\u043b\u044c\u043d\u0438\u0439 \u0442\u0435\u043a\u0441\u0442",audio_challenge:"\u041e\u0442\u0440\u0438\u043c\u0430\u0442\u0438 \u0430\u0443\u0434\u0456\u043e\u0437\u0430\u043f\u0438\u0441",refresh_btn:"\u041e\u043d\u043e\u0432\u0438\u0442\u0438 \u0442\u0435\u043a\u0441\u0442",instructions_visual:"\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u0442\u0435\u043a\u0441\u0442:",
-instructions_audio:"\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u043f\u043e\u0447\u0443\u0442\u0435:",help_btn:"\u0414\u043e\u0432\u0456\u0434\u043a\u0430",play_again:"\u0412\u0456\u0434\u0442\u0432\u043e\u0440\u0438\u0442\u0438 \u0437\u0430\u043f\u0438\u0441 \u0449\u0435 \u0440\u0430\u0437",cant_hear_this:"\u0417\u0430\u0432\u0430\u043d\u0442\u0430\u0436\u0438\u0442\u0438 \u0437\u0430\u043f\u0438\u0441 \u044f\u043a MP3",incorrect_try_again:"\u041d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u043e. \u0421\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0449\u0435 \u0440\u0430\u0437.",
-image_alt_text:"\u0417\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u043d\u044f \u0437\u0430\u0432\u0434\u0430\u043d\u043d\u044f reCAPTCHA",privacy_and_terms:"\u041a\u043e\u043d\u0444\u0456\u0434\u0435\u043d\u0446\u0456\u0439\u043d\u0456\u0441\u0442\u044c \u0456 \u0443\u043c\u043e\u0432\u0438"},ur:{visual_challenge:"\u0627\u06cc\u06a9 \u0645\u0631\u0626\u06cc \u0686\u06cc\u0644\u0646\u062c \u062d\u0627\u0635\u0644 \u06a9\u0631\u06cc\u06ba",audio_challenge:"\u0627\u06cc\u06a9 \u0622\u0688\u06cc\u0648 \u0686\u06cc\u0644\u0646\u062c \u062d\u0627\u0635\u0644 \u06a9\u0631\u06cc\u06ba",
-refresh_btn:"\u0627\u06cc\u06a9 \u0646\u06cc\u0627 \u0686\u06cc\u0644\u0646\u062c \u062d\u0627\u0635\u0644 \u06a9\u0631\u06cc\u06ba",instructions_visual:"",instructions_audio:"\u062c\u0648 \u0633\u0646\u0627\u0626\u06cc \u062f\u06cc\u062a\u0627 \u06c1\u06d2 \u0648\u06c1 \u0679\u0627\u0626\u067e \u06a9\u0631\u06cc\u06ba:",help_btn:"\u0645\u062f\u062f",play_again:"\u0622\u0648\u0627\u0632 \u062f\u0648\u0628\u0627\u0631\u06c1 \u0686\u0644\u0627\u0626\u06cc\u06ba",cant_hear_this:"\u0622\u0648\u0627\u0632 \u06a9\u0648 MP3 \u06a9\u06d2 \u0628\u0637\u0648\u0631 \u0688\u0627\u0624\u0646 \u0644\u0648\u0688 \u06a9\u0631\u06cc\u06ba",
-incorrect_try_again:"\u063a\u0644\u0637\u06d4 \u062f\u0648\u0628\u0627\u0631\u06c1 \u06a9\u0648\u0634\u0634 \u06a9\u0631\u06cc\u06ba\u06d4",image_alt_text:"reCAPTCHA \u0686\u06cc\u0644\u0646\u062c \u0648\u0627\u0644\u06cc \u0634\u0628\u06cc\u06c1",privacy_and_terms:"\u0631\u0627\u0632\u062f\u0627\u0631\u06cc \u0648 \u0634\u0631\u0627\u0626\u0637"},vi:{visual_challenge:"Nh\u1eadn th\u1eed th\u00e1ch h\u00ecnh \u1ea3nh",audio_challenge:"Nh\u1eadn th\u1eed th\u00e1ch \u00e2m thanh",refresh_btn:"Nh\u1eadn th\u1eed th\u00e1ch m\u1edbi",
-instructions_visual:"Nh\u1eadp v\u0103n b\u1ea3n:",instructions_audio:"Nh\u1eadp n\u1ed9i dung b\u1ea1n nghe th\u1ea5y:",help_btn:"Tr\u1ee3 gi\u00fap",play_again:"Ph\u00e1t l\u1ea1i \u00e2m thanh",cant_hear_this:"T\u1ea3i \u00e2m thanh xu\u1ed1ng d\u01b0\u1edbi d\u1ea1ng MP3",incorrect_try_again:"Kh\u00f4ng ch\u00ednh x\u00e1c. H\u00e3y th\u1eed l\u1ea1i.",image_alt_text:"H\u00ecnh x\u00e1c th\u1ef1c reCAPTCHA",privacy_and_terms:"B\u1ea3o m\u1eadt v\u00e0 \u0111i\u1ec1u kho\u1ea3n"},"zh-CN":sa,"zh-HK":{visual_challenge:"\u56de\u7b54\u5716\u50cf\u9a57\u8b49\u554f\u984c",
-audio_challenge:"\u53d6\u5f97\u8a9e\u97f3\u9a57\u8b49\u554f\u984c",refresh_btn:"\u63db\u4e00\u500b\u9a57\u8b49\u554f\u984c",instructions_visual:"\u8f38\u5165\u6587\u5b57\uff1a",instructions_audio:"\u9375\u5165\u60a8\u6240\u807d\u5230\u7684\uff1a",help_btn:"\u8aaa\u660e",play_again:"\u518d\u6b21\u64ad\u653e\u8072\u97f3",cant_hear_this:"\u5c07\u8072\u97f3\u4e0b\u8f09\u70ba MP3",incorrect_try_again:"\u4e0d\u6b63\u78ba\uff0c\u518d\u8a66\u4e00\u6b21\u3002",image_alt_text:"reCAPTCHA \u9a57\u8b49\u6587\u5b57\u5716\u7247",
-privacy_and_terms:"\u79c1\u96b1\u6b0a\u8207\u689d\u6b3e"},"zh-TW":{visual_challenge:"\u53d6\u5f97\u5716\u7247\u9a57\u8b49\u554f\u984c",audio_challenge:"\u53d6\u5f97\u8a9e\u97f3\u9a57\u8b49\u554f\u984c",refresh_btn:"\u53d6\u5f97\u65b0\u7684\u9a57\u8b49\u554f\u984c",instructions_visual:"\u8acb\u8f38\u5165\u5716\u7247\u4e2d\u7684\u6587\u5b57\uff1a",instructions_audio:"\u8acb\u8f38\u5165\u8a9e\u97f3\u5167\u5bb9\uff1a",help_btn:"\u8aaa\u660e",play_again:"\u518d\u6b21\u64ad\u653e",cant_hear_this:"\u4ee5 MP3 \u683c\u5f0f\u4e0b\u8f09\u8072\u97f3",
-incorrect_try_again:"\u9a57\u8b49\u78bc\u6709\u8aa4\uff0c\u8acb\u518d\u8a66\u4e00\u6b21\u3002",image_alt_text:"reCAPTCHA \u9a57\u8b49\u6587\u5b57\u5716\u7247",privacy_and_terms:"\u96b1\u79c1\u6b0a\u8207\u689d\u6b3e"},zu:{visual_challenge:"Thola inselelo ebonakalayo",audio_challenge:"Thola inselelo yokulalelwayo",refresh_btn:"Thola inselelo entsha",instructions_visual:"",instructions_audio:"Bhala okuzwayo:",help_btn:"Usizo",play_again:"Phinda udlale okulalelwayo futhi",cant_hear_this:"Layisha umsindo njenge-MP3",
-incorrect_try_again:"Akulungile. Zama futhi.",image_alt_text:"umfanekiso oyinselelo we-reCAPTCHA",privacy_and_terms:"Okwangasese kanye nemigomo"},tl:ma,he:pa,"in":oa,mo:ra,zh:sa};var ua=function(a,b){for(var c in a)b.call(void 0,a[c],c,a)},va=function(a){var b=[],c=0,d;for(d in a)b[c++]=a[d];return b},wa=function(a){var b=[],c=0,d;for(d in a)b[c++]=d;return b},xa=function(a){for(var b in a)return!1;return!0},za=function(){var a=ya()?l.google_ad:null,b={},c;for(c in a)b[c]=a[c];return b},Aa="constructor hasOwnProperty isPrototypeOf propertyIsEnumerable toLocaleString toString valueOf".split(" "),Ba=function(a,b){for(var c,d,e=1;e<arguments.length;e++){d=arguments[e];for(c in d)a[c]=
-d[c];for(var g=0;g<Aa.length;g++)c=Aa[g],Object.prototype.hasOwnProperty.call(d,c)&&(a[c]=d[c])}};var w=function(a){if(Error.captureStackTrace)Error.captureStackTrace(this,w);else{var b=Error().stack;b&&(this.stack=b)}a&&(this.message=String(a))};r(w,Error);w.prototype.name="CustomError";var Ca;var Da=function(a,b){for(var c=a.split("%s"),d="",e=Array.prototype.slice.call(arguments,1);e.length&&1<c.length;)d+=c.shift()+e.shift();return d+c.join("%s")},x=function(a){if(!Ea.test(a))return a;-1!=a.indexOf("&")&&(a=a.replace(Fa,"&amp;"));-1!=a.indexOf("<")&&(a=a.replace(Ga,"&lt;"));-1!=a.indexOf(">")&&(a=a.replace(Ha,"&gt;"));-1!=a.indexOf('"')&&(a=a.replace(Ia,"&quot;"));-1!=a.indexOf("'")&&(a=a.replace(Ja,"&#39;"));return a},Fa=/&/g,Ga=/</g,Ha=/>/g,Ia=/"/g,Ja=/'/g,Ea=/[&<>"']/,Ka=function(a,
-b){return a<b?-1:a>b?1:0},La=function(a){return String(a).replace(/\-([a-z])/g,function(a,c){return c.toUpperCase()})},Ma=function(a){var b=n(void 0)?"undefined".replace(/([-()\[\]{}+?*.$\^|,:#<!\\])/g,"\\$1").replace(/\x08/g,"\\x08"):"\\s";return a.replace(RegExp("(^"+(b?"|["+b+"]+":"")+")([a-z])","g"),function(a,b,e){return b+e.toUpperCase()})};var Na=function(a,b){b.unshift(a);w.call(this,Da.apply(null,b));b.shift()};r(Na,w);Na.prototype.name="AssertionError";var y=function(a,b,c){if(!a){var d="Assertion failed";if(b)var d=d+(": "+b),e=Array.prototype.slice.call(arguments,2);throw new Na(""+d,e||[]);}},Oa=function(a,b){throw new Na("Failure"+(a?": "+a:""),Array.prototype.slice.call(arguments,1));};var z=Array.prototype,Pa=z.indexOf?function(a,b,c){y(null!=a.length);return z.indexOf.call(a,b,c)}:function(a,b,c){c=null==c?0:0>c?Math.max(0,a.length+c):c;if(n(a))return n(b)&&1==b.length?a.indexOf(b,c):-1;for(;c<a.length;c++)if(c in a&&a[c]===b)return c;return-1},Qa=z.forEach?function(a,b,c){y(null!=a.length);z.forEach.call(a,b,c)}:function(a,b,c){for(var d=a.length,e=n(a)?a.split(""):a,g=0;g<d;g++)g in e&&b.call(c,e[g],g,a)},Ra=z.filter?function(a,b,c){y(null!=a.length);return z.filter.call(a,
-b,c)}:function(a,b,c){for(var d=a.length,e=[],g=0,f=n(a)?a.split(""):a,k=0;k<d;k++)if(k in f){var u=f[k];b.call(c,u,k,a)&&(e[g++]=u)}return e},Sa=z.map?function(a,b,c){y(null!=a.length);return z.map.call(a,b,c)}:function(a,b,c){for(var d=a.length,e=Array(d),g=n(a)?a.split(""):a,f=0;f<d;f++)f in g&&(e[f]=b.call(c,g[f],f,a));return e},Ua=function(a){var b;t:{b=Ta;for(var c=a.length,d=n(a)?a.split(""):a,e=0;e<c;e++)if(e in d&&b.call(void 0,d[e],e,a)){b=e;break t}b=-1}return 0>b?null:n(a)?a.charAt(b):
-a[b]},Va=function(a,b){var c=Pa(a,b),d;if(d=0<=c)y(null!=a.length),z.splice.call(a,c,1);return d},Wa=function(a){var b=a.length;if(0<b){for(var c=Array(b),d=0;d<b;d++)c[d]=a[d];return c}return[]},Xa=function(a,b,c){y(null!=a.length);return 2>=arguments.length?z.slice.call(a,b):z.slice.call(a,b,c)};var A,Ya,Za,$a,ab=function(){return l.navigator?l.navigator.userAgent:null};$a=Za=Ya=A=!1;var B;if(B=ab()){var bb=l.navigator;A=0==B.lastIndexOf("Opera",0);Ya=!A&&(-1!=B.indexOf("MSIE")||-1!=B.indexOf("Trident"));Za=!A&&-1!=B.indexOf("WebKit");$a=!A&&!Za&&!Ya&&"Gecko"==bb.product}var cb=A,C=Ya,D=$a,E=Za,db=function(){var a=l.document;return a?a.documentMode:void 0},eb;
-t:{var fb="",gb;if(cb&&l.opera)var hb=l.opera.version,fb="function"==typeof hb?hb():hb;else if(D?gb=/rv\:([^\);]+)(\)|;)/:C?gb=/\b(?:MSIE|rv)[: ]([^\);]+)(\)|;)/:E&&(gb=/WebKit\/(\S+)/),gb)var ib=gb.exec(ab()),fb=ib?ib[1]:"";if(C){var jb=db();if(jb>parseFloat(fb)){eb=String(jb);break t}}eb=fb}
-var kb=eb,lb={},F=function(a){var b;if(!(b=lb[a])){b=0;for(var c=String(kb).replace(/^[\s\xa0]+|[\s\xa0]+$/g,"").split("."),d=String(a).replace(/^[\s\xa0]+|[\s\xa0]+$/g,"").split("."),e=Math.max(c.length,d.length),g=0;0==b&&g<e;g++){var f=c[g]||"",k=d[g]||"",u=RegExp("(\\d*)(\\D*)","g"),L=RegExp("(\\d*)(\\D*)","g");do{var v=u.exec(f)||["","",""],Q=L.exec(k)||["","",""];if(0==v[0].length&&0==Q[0].length)break;b=Ka(0==v[1].length?0:parseInt(v[1],10),0==Q[1].length?0:parseInt(Q[1],10))||Ka(0==v[2].length,
-0==Q[2].length)||Ka(v[2],Q[2])}while(0==b)}b=lb[a]=0<=b}return b},mb=l.document,nb=mb&&C?db()||("CSS1Compat"==mb.compatMode?parseInt(kb,10):5):void 0;var ob=!C||C&&9<=nb,pb=!D&&!C||C&&C&&9<=nb||D&&F("1.9.1");C&&F("9");var qb=function(a,b){var c;c=a.className;c=n(c)&&c.match(/\S+/g)||[];for(var d=Xa(arguments,1),e=c.length+d.length,g=c,f=0;f<d.length;f++)0<=Pa(g,d[f])||g.push(d[f]);a.className=c.join(" ");return c.length==e};var sb=function(a){return a?new rb(9==a.nodeType?a:a.ownerDocument||a.document):Ca||(Ca=new rb)},tb=function(a,b){return n(b)?a.getElementById(b):b},vb=function(a,b){ua(b,function(b,d){"style"==d?a.style.cssText=b:"class"==d?a.className=b:"for"==d?a.htmlFor=b:d in ub?a.setAttribute(ub[d],b):0==d.lastIndexOf("aria-",0)||0==d.lastIndexOf("data-",0)?a.setAttribute(d,b):a[d]=b})},ub={cellpadding:"cellPadding",cellspacing:"cellSpacing",colspan:"colSpan",frameborder:"frameBorder",height:"height",maxlength:"maxLength",
-role:"role",rowspan:"rowSpan",type:"type",usemap:"useMap",valign:"vAlign",width:"width"},xb=function(a,b,c){function d(c){c&&b.appendChild(n(c)?a.createTextNode(c):c)}for(var e=2;e<c.length;e++){var g=c[e];!ea(g)||ga(g)&&0<g.nodeType?d(g):Qa(wb(g)?Wa(g):g,d)}},yb=function(a){for(var b;b=a.firstChild;)a.removeChild(b)},zb=function(a){a&&a.parentNode&&a.parentNode.removeChild(a)},wb=function(a){if(a&&"number"==typeof a.length){if(ga(a))return"function"==typeof a.item||"string"==typeof a.item;if(fa(a))return"function"==
-typeof a.item}return!1},rb=function(a){this.document_=a||l.document||document};h=rb.prototype;h.getDomHelper=sb;h.getElement=function(a){return tb(this.document_,a)};h.$=rb.prototype.getElement;
-h.createDom=function(a,b,c){var d=this.document_,e=arguments,g=e[0],f=e[1];if(!ob&&f&&(f.name||f.type)){g=["<",g];f.name&&g.push(' name="',x(f.name),'"');if(f.type){g.push(' type="',x(f.type),'"');var k={};Ba(k,f);delete k.type;f=k}g.push(">");g=g.join("")}g=d.createElement(g);f&&(n(f)?g.className=f:m(f)?qb.apply(null,[g].concat(f)):vb(g,f));2<e.length&&xb(d,g,e);return g};h.createElement=function(a){return this.document_.createElement(a)};h.createTextNode=function(a){return this.document_.createTextNode(String(a))};
-h.appendChild=function(a,b){a.appendChild(b)};h.getChildren=function(a){return pb&&void 0!=a.children?a.children:Ra(a.childNodes,function(a){return 1==a.nodeType})};var Ab=function(){};Ab.prototype.disposed_=!1;Ab.prototype.dispose=function(){this.disposed_||(this.disposed_=!0,this.disposeInternal())};Ab.prototype.disposeInternal=function(){if(this.onDisposeCallbacks_)for(;this.onDisposeCallbacks_.length;)this.onDisposeCallbacks_.shift()()};var Bb=function(a){Bb[" "](a);return a};Bb[" "]=ca;var Cb=!C||C&&9<=nb,Db=C&&!F("9");!E||F("528");D&&F("1.9b")||C&&F("8")||cb&&F("9.5")||E&&F("528");D&&!F("8")||C&&F("9");var G=function(a,b){this.type=a;this.currentTarget=this.target=b;this.defaultPrevented=this.propagationStopped_=!1;this.returnValue_=!0};G.prototype.disposeInternal=function(){};G.prototype.dispose=function(){};G.prototype.preventDefault=function(){this.defaultPrevented=!0;this.returnValue_=!1};var H=function(a,b){G.call(this,a?a.type:"");this.relatedTarget=this.currentTarget=this.target=null;this.charCode=this.keyCode=this.button=this.screenY=this.screenX=this.clientY=this.clientX=this.offsetY=this.offsetX=0;this.metaKey=this.shiftKey=this.altKey=this.ctrlKey=!1;this.event_=this.state=null;if(a){var c=this.type=a.type;this.target=a.target||a.srcElement;this.currentTarget=b;var d=a.relatedTarget;if(d){if(D){var e;t:{try{Bb(d.nodeName);e=!0;break t}catch(g){}e=!1}e||(d=null)}}else"mouseover"==
-c?d=a.fromElement:"mouseout"==c&&(d=a.toElement);this.relatedTarget=d;this.offsetX=E||void 0!==a.offsetX?a.offsetX:a.layerX;this.offsetY=E||void 0!==a.offsetY?a.offsetY:a.layerY;this.clientX=void 0!==a.clientX?a.clientX:a.pageX;this.clientY=void 0!==a.clientY?a.clientY:a.pageY;this.screenX=a.screenX||0;this.screenY=a.screenY||0;this.button=a.button;this.keyCode=a.keyCode||0;this.charCode=a.charCode||("keypress"==c?a.keyCode:0);this.ctrlKey=a.ctrlKey;this.altKey=a.altKey;this.shiftKey=a.shiftKey;this.metaKey=
-a.metaKey;this.state=a.state;this.event_=a;a.defaultPrevented&&this.preventDefault()}};r(H,G);H.prototype.preventDefault=function(){H.superClass_.preventDefault.call(this);var a=this.event_;if(a.preventDefault)a.preventDefault();else if(a.returnValue=!1,Db)try{if(a.ctrlKey||112<=a.keyCode&&123>=a.keyCode)a.keyCode=-1}catch(b){}};H.prototype.disposeInternal=function(){};var Eb="closure_listenable_"+(1E6*Math.random()|0),Fb=function(a){try{return!(!a||!a[Eb])}catch(b){return!1}},Gb=0;var Hb=function(a,b,c,d,e){this.listener=a;this.proxy=null;this.src=b;this.type=c;this.capture=!!d;this.handler=e;this.key=++Gb;this.removed=this.callOnce=!1},Ib=function(a){a.removed=!0;a.listener=null;a.proxy=null;a.src=null;a.handler=null};var I=function(a){this.src=a;this.listeners={};this.typeCount_=0};I.prototype.add=function(a,b,c,d,e){var g=a.toString();a=this.listeners[g];a||(a=this.listeners[g]=[],this.typeCount_++);var f=Jb(a,b,d,e);-1<f?(b=a[f],c||(b.callOnce=!1)):(b=new Hb(b,this.src,g,!!d,e),b.callOnce=c,a.push(b));return b};
-I.prototype.remove=function(a,b,c,d){a=a.toString();if(!(a in this.listeners))return!1;var e=this.listeners[a];b=Jb(e,b,c,d);return-1<b?(Ib(e[b]),y(null!=e.length),z.splice.call(e,b,1),0==e.length&&(delete this.listeners[a],this.typeCount_--),!0):!1};var Kb=function(a,b){var c=b.type;if(!(c in a.listeners))return!1;var d=Va(a.listeners[c],b);d&&(Ib(b),0==a.listeners[c].length&&(delete a.listeners[c],a.typeCount_--));return d};
-I.prototype.removeAll=function(a){a=a&&a.toString();var b=0,c;for(c in this.listeners)if(!a||c==a){for(var d=this.listeners[c],e=0;e<d.length;e++)++b,Ib(d[e]);delete this.listeners[c];this.typeCount_--}return b};I.prototype.getListener=function(a,b,c,d){a=this.listeners[a.toString()];var e=-1;a&&(e=Jb(a,b,c,d));return-1<e?a[e]:null};var Jb=function(a,b,c,d){for(var e=0;e<a.length;++e){var g=a[e];if(!g.removed&&g.listener==b&&g.capture==!!c&&g.handler==d)return e}return-1};var Lb="closure_lm_"+(1E6*Math.random()|0),J={},Mb=0,Nb=function(a,b,c,d,e){if(m(b)){for(var g=0;g<b.length;g++)Nb(a,b[g],c,d,e);return null}c=Ob(c);return Fb(a)?a.listen(b,c,d,e):Pb(a,b,c,!1,d,e)},Pb=function(a,b,c,d,e,g){if(!b)throw Error("Invalid event type");var f=!!e,k=Qb(a);k||(a[Lb]=k=new I(a));c=k.add(b,c,d,e,g);if(c.proxy)return c;d=Rb();c.proxy=d;d.src=a;d.listener=c;a.addEventListener?a.addEventListener(b,d,f):a.attachEvent(b in J?J[b]:J[b]="on"+b,d);Mb++;return c},Rb=function(){var a=
-Sb,b=Cb?function(c){return a.call(b.src,b.listener,c)}:function(c){c=a.call(b.src,b.listener,c);if(!c)return c};return b},Tb=function(a,b,c,d,e){if(m(b)){for(var g=0;g<b.length;g++)Tb(a,b[g],c,d,e);return null}c=Ob(c);return Fb(a)?a.listenOnce(b,c,d,e):Pb(a,b,c,!0,d,e)},Ub=function(a,b,c,d,e){if(m(b))for(var g=0;g<b.length;g++)Ub(a,b[g],c,d,e);else c=Ob(c),Fb(a)?a.unlisten(b,c,d,e):a&&(a=Qb(a))&&(b=a.getListener(b,c,!!d,e))&&Vb(b)},Vb=function(a){if("number"==typeof a||!a||a.removed)return!1;var b=
-a.src;if(Fb(b))return Kb(b.eventTargetListeners_,a);var c=a.type,d=a.proxy;b.removeEventListener?b.removeEventListener(c,d,a.capture):b.detachEvent&&b.detachEvent(c in J?J[c]:J[c]="on"+c,d);Mb--;(c=Qb(b))?(Kb(c,a),0==c.typeCount_&&(c.src=null,b[Lb]=null)):Ib(a);return!0},Xb=function(a,b,c,d){var e=1;if(a=Qb(a))if(b=a.listeners[b])for(b=Wa(b),a=0;a<b.length;a++){var g=b[a];g&&g.capture==c&&!g.removed&&(e&=!1!==Wb(g,d))}return Boolean(e)},Wb=function(a,b){var c=a.listener,d=a.handler||a.src;a.callOnce&&
-Vb(a);return c.call(d,b)},Sb=function(a,b){if(a.removed)return!0;if(!Cb){var c=b||ba("window.event"),d=new H(c,this),e=!0;if(!(0>c.keyCode||void 0!=c.returnValue)){t:{var g=!1;if(0==c.keyCode)try{c.keyCode=-1;break t}catch(f){g=!0}if(g||void 0==c.returnValue)c.returnValue=!0}c=[];for(g=d.currentTarget;g;g=g.parentNode)c.push(g);for(var g=a.type,k=c.length-1;!d.propagationStopped_&&0<=k;k--)d.currentTarget=c[k],e&=Xb(c[k],g,!0,d);for(k=0;!d.propagationStopped_&&k<c.length;k++)d.currentTarget=c[k],
-e&=Xb(c[k],g,!1,d)}return e}return Wb(a,new H(b,this))},Qb=function(a){a=a[Lb];return a instanceof I?a:null},Yb="__closure_events_fn_"+(1E9*Math.random()>>>0),Ob=function(a){y(a,"Listener can not be null.");if(fa(a))return a;y(a.handleEvent,"An object listener must have handleEvent method.");return a[Yb]||(a[Yb]=function(b){return a.handleEvent(b)})};var K=function(a){this.handler_=a;this.keys_={}};r(K,Ab);var Zb=[];K.prototype.listen=function(a,b,c,d){m(b)||(Zb[0]=b,b=Zb);for(var e=0;e<b.length;e++){var g=Nb(a,b[e],c||this.handleEvent,d||!1,this.handler_||this);if(!g)break;this.keys_[g.key]=g}return this};K.prototype.listenOnce=function(a,b,c,d){return $b(this,a,b,c,d)};var $b=function(a,b,c,d,e,g){if(m(c))for(var f=0;f<c.length;f++)$b(a,b,c[f],d,e,g);else{b=Tb(b,c,d||a.handleEvent,e,g||a.handler_||a);if(!b)return a;a.keys_[b.key]=b}return a};
-K.prototype.unlisten=function(a,b,c,d,e){if(m(b))for(var g=0;g<b.length;g++)this.unlisten(a,b[g],c,d,e);else c=c||this.handleEvent,e=e||this.handler_||this,c=Ob(c),d=!!d,b=Fb(a)?a.getListener(b,c,d,e):a?(a=Qb(a))?a.getListener(b,c,d,e):null:null,b&&(Vb(b),delete this.keys_[b.key]);return this};K.prototype.removeAll=function(){ua(this.keys_,Vb);this.keys_={}};K.prototype.disposeInternal=function(){K.superClass_.disposeInternal.call(this);this.removeAll()};
-K.prototype.handleEvent=function(){throw Error("EventHandler.handleEvent not implemented");};var M=function(){this.eventTargetListeners_=new I(this);this.actualEventTarget_=this};r(M,Ab);M.prototype[Eb]=!0;h=M.prototype;h.parentEventTarget_=null;h.setParentEventTarget=function(a){this.parentEventTarget_=a};h.addEventListener=function(a,b,c,d){Nb(this,a,b,c,d)};h.removeEventListener=function(a,b,c,d){Ub(this,a,b,c,d)};
-h.dispatchEvent=function(a){ac(this);var b,c=this.parentEventTarget_;if(c){b=[];for(var d=1;c;c=c.parentEventTarget_)b.push(c),y(1E3>++d,"infinite loop")}c=this.actualEventTarget_;d=a.type||a;if(n(a))a=new G(a,c);else if(a instanceof G)a.target=a.target||c;else{var e=a;a=new G(d,c);Ba(a,e)}var e=!0,g;if(b)for(var f=b.length-1;!a.propagationStopped_&&0<=f;f--)g=a.currentTarget=b[f],e=bc(g,d,!0,a)&&e;a.propagationStopped_||(g=a.currentTarget=c,e=bc(g,d,!0,a)&&e,a.propagationStopped_||(e=bc(g,d,!1,a)&&
-e));if(b)for(f=0;!a.propagationStopped_&&f<b.length;f++)g=a.currentTarget=b[f],e=bc(g,d,!1,a)&&e;return e};h.disposeInternal=function(){M.superClass_.disposeInternal.call(this);this.eventTargetListeners_&&this.eventTargetListeners_.removeAll(void 0);this.parentEventTarget_=null};h.listen=function(a,b,c,d){ac(this);return this.eventTargetListeners_.add(String(a),b,!1,c,d)};h.listenOnce=function(a,b,c,d){return this.eventTargetListeners_.add(String(a),b,!0,c,d)};
-h.unlisten=function(a,b,c,d){return this.eventTargetListeners_.remove(String(a),b,c,d)};var bc=function(a,b,c,d){b=a.eventTargetListeners_.listeners[String(b)];if(!b)return!0;b=Wa(b);for(var e=!0,g=0;g<b.length;++g){var f=b[g];if(f&&!f.removed&&f.capture==c){var k=f.listener,u=f.handler||f.src;f.callOnce&&Kb(a.eventTargetListeners_,f);e=!1!==k.call(u,d)&&e}}return e&&!1!=d.returnValue_};M.prototype.getListener=function(a,b,c,d){return this.eventTargetListeners_.getListener(String(a),b,c,d)};
-var ac=function(a){y(a.eventTargetListeners_,"Event target is not initialized. Did you call the superclass (goog.events.EventTarget) constructor?")};var N=function(a){M.call(this);this.imageIdToRequestMap_={};this.imageIdToImageMap_={};this.handler_=new K(this);this.parent_=a};r(N,M);var cc=[C&&!F("11")?"readystatechange":"load","abort","error"],dc=function(a,b,c){(c=n(c)?c:c.src)&&(a.imageIdToRequestMap_[b]={src:c,corsRequestType:null})};
-N.prototype.start=function(){var a=this.imageIdToRequestMap_;Qa(wa(a),function(b){var c=a[b];if(c&&(delete a[b],!this.disposed_)){var d;d=this.parent_?sb(this.parent_).createDom("img"):new Image;c.corsRequestType&&(d.crossOrigin=c.corsRequestType);this.handler_.listen(d,cc,this.onNetworkEvent_);this.imageIdToImageMap_[b]=d;d.id=b;d.src=c.src}},this)};
-N.prototype.onNetworkEvent_=function(a){var b=a.currentTarget;if(b){if("readystatechange"==a.type)if("complete"==b.readyState)a.type="load";else return;"undefined"==typeof b.naturalWidth&&("load"==a.type?(b.naturalWidth=b.width,b.naturalHeight=b.height):(b.naturalWidth=0,b.naturalHeight=0));this.dispatchEvent({type:a.type,target:b});!this.disposed_&&(a=b.id,delete this.imageIdToRequestMap_[a],b=this.imageIdToImageMap_[a])&&(delete this.imageIdToImageMap_[a],this.handler_.unlisten(b,cc,this.onNetworkEvent_),
-xa(this.imageIdToImageMap_)&&xa(this.imageIdToRequestMap_)&&this.dispatchEvent("complete"))}};N.prototype.disposeInternal=function(){delete this.imageIdToRequestMap_;delete this.imageIdToImageMap_;var a=this.handler_;a&&"function"==typeof a.dispose&&a.dispose();N.superClass_.disposeInternal.call(this)};var ec="StopIteration"in l?l.StopIteration:Error("StopIteration"),fc=function(){};fc.prototype.next=function(){throw ec;};fc.prototype.__iterator__=function(){return this};var O=function(a,b){this.map_={};this.keys_=[];this.version_=this.count_=0;var c=arguments.length;if(1<c){if(c%2)throw Error("Uneven number of arguments");for(var d=0;d<c;d+=2)this.set(arguments[d],arguments[d+1])}else if(a){a instanceof O?(c=a.getKeys(),d=a.getValues()):(c=wa(a),d=va(a));for(var e=0;e<c.length;e++)this.set(c[e],d[e])}};O.prototype.getValues=function(){gc(this);for(var a=[],b=0;b<this.keys_.length;b++)a.push(this.map_[this.keys_[b]]);return a};
-O.prototype.getKeys=function(){gc(this);return this.keys_.concat()};O.prototype.remove=function(a){return Object.prototype.hasOwnProperty.call(this.map_,a)?(delete this.map_[a],this.count_--,this.version_++,this.keys_.length>2*this.count_&&gc(this),!0):!1};
-var gc=function(a){if(a.count_!=a.keys_.length){for(var b=0,c=0;b<a.keys_.length;){var d=a.keys_[b];Object.prototype.hasOwnProperty.call(a.map_,d)&&(a.keys_[c++]=d);b++}a.keys_.length=c}if(a.count_!=a.keys_.length){for(var e={},c=b=0;b<a.keys_.length;)d=a.keys_[b],Object.prototype.hasOwnProperty.call(e,d)||(a.keys_[c++]=d,e[d]=1),b++;a.keys_.length=c}};O.prototype.set=function(a,b){Object.prototype.hasOwnProperty.call(this.map_,a)||(this.count_++,this.keys_.push(a),this.version_++);this.map_[a]=b};
-O.prototype.__iterator__=function(a){gc(this);var b=0,c=this.keys_,d=this.map_,e=this.version_,g=this,f=new fc;f.next=function(){for(;;){if(e!=g.version_)throw Error("The map has changed since the iterator was created");if(b>=c.length)throw ec;var f=c[b++];return a?f:d[f]}};return f};var hc=function(a){if("function"==typeof a.getValues)return a.getValues();if(n(a))return a.split("");if(ea(a)){for(var b=[],c=a.length,d=0;d<c;d++)b.push(a[d]);return b}return va(a)},ic=function(a,b,c){if("function"==typeof a.forEach)a.forEach(b,c);else if(ea(a)||n(a))Qa(a,b,c);else{var d;if("function"==typeof a.getKeys)d=a.getKeys();else if("function"!=typeof a.getValues)if(ea(a)||n(a)){d=[];for(var e=a.length,g=0;g<e;g++)d.push(g)}else d=wa(a);else d=void 0;for(var e=hc(a),g=e.length,f=0;f<g;f++)b.call(c,
-e[f],d&&d[f],a)}};var kc=function(a){return jc(a||arguments.callee.caller,[])},jc=function(a,b){var c=[];if(0<=Pa(b,a))c.push("[...circular reference...]");else if(a&&50>b.length){c.push(lc(a)+"(");for(var d=a.arguments,e=0;d&&e<d.length;e++){0<e&&c.push(", ");var g;g=d[e];switch(typeof g){case "object":g=g?"object":"null";break;case "string":break;case "number":g=String(g);break;case "boolean":g=g?"true":"false";break;case "function":g=(g=lc(g))?g:"[fn]";break;default:g=typeof g}40<g.length&&(g=g.substr(0,40)+"...");
-c.push(g)}b.push(a);c.push(")\n");try{c.push(jc(a.caller,b))}catch(f){c.push("[exception trying to get caller]\n")}}else a?c.push("[...long stack...]"):c.push("[end]");return c.join("")},lc=function(a){if(mc[a])return mc[a];a=String(a);if(!mc[a]){var b=/function ([^\(]+)/.exec(a);mc[a]=b?b[1]:"[Anonymous]"}return mc[a]},mc={};var nc=function(a,b,c,d,e){this.reset(a,b,c,d,e)};nc.prototype.exception_=null;nc.prototype.exceptionText_=null;var oc=0;nc.prototype.reset=function(a,b,c,d,e){"number"==typeof e||oc++;d||ja();this.level_=a;this.msg_=b;delete this.exception_;delete this.exceptionText_};nc.prototype.setLevel=function(a){this.level_=a};var P=function(a){this.name_=a;this.handlers_=this.children_=this.level_=this.parent_=null},pc=function(a,b){this.name=a;this.value=b};pc.prototype.toString=function(){return this.name};var qc=new pc("SEVERE",1E3),rc=new pc("CONFIG",700),sc=new pc("FINE",500);P.prototype.getParent=function(){return this.parent_};P.prototype.getChildren=function(){this.children_||(this.children_={});return this.children_};P.prototype.setLevel=function(a){this.level_=a};
-var tc=function(a){if(a.level_)return a.level_;if(a.parent_)return tc(a.parent_);Oa("Root logger has no level set.");return null};P.prototype.log=function(a,b,c){if(a.value>=tc(this).value)for(fa(b)&&(b=b()),a=this.getLogRecord(a,b,c),b="log:"+a.msg_,l.console&&(l.console.timeStamp?l.console.timeStamp(b):l.console.markTimeline&&l.console.markTimeline(b)),l.msWriteProfilerMark&&l.msWriteProfilerMark(b),b=this;b;){c=b;var d=a;if(c.handlers_)for(var e=0,g=void 0;g=c.handlers_[e];e++)g(d);b=b.getParent()}};
-P.prototype.getLogRecord=function(a,b,c){var d=new nc(a,String(b),this.name_);if(c){d.exception_=c;var e;var g=arguments.callee.caller;try{var f;var k=ba("window.location.href");if(n(c))f={message:c,name:"Unknown error",lineNumber:"Not available",fileName:k,stack:"Not available"};else{var u,L,v=!1;try{u=c.lineNumber||c.line||"Not available"}catch(Q){u="Not available",v=!0}try{L=c.fileName||c.filename||c.sourceURL||l.$googDebugFname||k}catch(jd){L="Not available",v=!0}f=!v&&c.lineNumber&&c.fileName&&
-c.stack&&c.message&&c.name?c:{message:c.message||"Not available",name:c.name||"UnknownError",lineNumber:u,fileName:L,stack:c.stack||"Not available"}}e="Message: "+x(f.message)+'\nUrl: <a href="view-source:'+f.fileName+'" target="_new">'+f.fileName+"</a>\nLine: "+f.lineNumber+"\n\nBrowser stack:\n"+x(f.stack+"-> ")+"[end]\n\nJS stack traversal:\n"+x(kc(g)+"-> ")}catch(Yc){e="Exception trying to expose exception! You win, we lose. "+Yc}d.exceptionText_=e}return d};
-var uc={},vc=null,wc=function(a){vc||(vc=new P(""),uc[""]=vc,vc.setLevel(rc));var b;if(!(b=uc[a])){b=new P(a);var c=a.lastIndexOf("."),d=a.substr(c+1),c=wc(a.substr(0,c));c.getChildren()[d]=b;b.parent_=c;uc[a]=b}return b};var R=function(a,b){a&&a.log(sc,b,void 0)};var xc=function(a,b,c){if(fa(a))c&&(a=p(a,c));else if(a&&"function"==typeof a.handleEvent)a=p(a.handleEvent,a);else throw Error("Invalid listener argument");return 2147483647<b?-1:l.setTimeout(a,b||0)};var yc=RegExp("^(?:([^:/?#.]+):)?(?://(?:([^/?#]*)@)?([^/#?]*?)(?::([0-9]+))?(?=[/#?]|$))?([^?#]+)?(?:\\?([^#]*))?(?:#(.*))?$"),zc=E,Ac=function(a,b){if(zc){zc=!1;var c=l.location;if(c){var d=c.href;if(d&&(d=(d=Ac(3,d))&&decodeURIComponent(d))&&d!=c.hostname)throw zc=!0,Error();}}return b.match(yc)[a]||null};var Bc=function(){};Bc.prototype.cachedOptions_=null;var Dc=function(a){var b;(b=a.cachedOptions_)||(b={},Cc(a)&&(b[0]=!0,b[1]=!0),b=a.cachedOptions_=b);return b};var Ec,Fc=function(){};r(Fc,Bc);var Gc=function(a){return(a=Cc(a))?new ActiveXObject(a):new XMLHttpRequest},Cc=function(a){if(!a.ieProgId_&&"undefined"==typeof XMLHttpRequest&&"undefined"!=typeof ActiveXObject){for(var b=["MSXML2.XMLHTTP.6.0","MSXML2.XMLHTTP.3.0","MSXML2.XMLHTTP","Microsoft.XMLHTTP"],c=0;c<b.length;c++){var d=b[c];try{return new ActiveXObject(d),a.ieProgId_=d}catch(e){}}throw Error("Could not create ActiveXObject. ActiveX might be disabled, or MSXML might not be installed");}return a.ieProgId_};
-Ec=new Fc;var S=function(a){M.call(this);this.headers=new O;this.xmlHttpFactory_=a||null;this.active_=!1;this.xhrOptions_=this.xhr_=null;this.lastError_=this.lastMethod_=this.lastUri_="";this.inAbort_=this.inOpen_=this.inSend_=this.errorDispatched_=!1;this.timeoutInterval_=0;this.timeoutId_=null;this.responseType_="";this.useXhr2Timeout_=this.withCredentials_=!1};r(S,M);var Hc=S.prototype,Ic=wc("goog.net.XhrIo");Hc.logger_=Ic;
-var Jc=/^https?$/i,Kc=["POST","PUT"],Lc=[],Mc=function(a){var b=new S;Lc.push(b);b.listenOnce("ready",b.cleanupSend_);b.send(a,"POST",void 0,void 0)};S.prototype.cleanupSend_=function(){this.dispose();Va(Lc,this)};
-S.prototype.send=function(a,b,c,d){if(this.xhr_)throw Error("[goog.net.XhrIo] Object is active with another request="+this.lastUri_+"; newUri="+a);b=b?b.toUpperCase():"GET";this.lastUri_=a;this.lastError_="";this.lastMethod_=b;this.errorDispatched_=!1;this.active_=!0;this.xhr_=this.xmlHttpFactory_?Gc(this.xmlHttpFactory_):Gc(Ec);this.xhrOptions_=this.xmlHttpFactory_?Dc(this.xmlHttpFactory_):Dc(Ec);this.xhr_.onreadystatechange=p(this.onReadyStateChange_,this);try{R(this.logger_,T(this,"Opening Xhr")),
-this.inOpen_=!0,this.xhr_.open(b,String(a),!0),this.inOpen_=!1}catch(e){R(this.logger_,T(this,"Error opening Xhr: "+e.message));Nc(this,e);return}a=c||"";var g=new O(this.headers);d&&ic(d,function(a,b){g.set(b,a)});d=Ua(g.getKeys());c=l.FormData&&a instanceof l.FormData;!(0<=Pa(Kc,b))||d||c||g.set("Content-Type","application/x-www-form-urlencoded;charset=utf-8");ic(g,function(a,b){this.xhr_.setRequestHeader(b,a)},this);this.responseType_&&(this.xhr_.responseType=this.responseType_);"withCredentials"in
-this.xhr_&&(this.xhr_.withCredentials=this.withCredentials_);try{Oc(this),0<this.timeoutInterval_&&(this.useXhr2Timeout_=Pc(this.xhr_),R(this.logger_,T(this,"Will abort after "+this.timeoutInterval_+"ms if incomplete, xhr2 "+this.useXhr2Timeout_)),this.useXhr2Timeout_?(this.xhr_.timeout=this.timeoutInterval_,this.xhr_.ontimeout=p(this.timeout_,this)):this.timeoutId_=xc(this.timeout_,this.timeoutInterval_,this)),R(this.logger_,T(this,"Sending request")),this.inSend_=!0,this.xhr_.send(a),this.inSend_=
-!1}catch(f){R(this.logger_,T(this,"Send error: "+f.message)),Nc(this,f)}};var Pc=function(a){return C&&F(9)&&"number"==typeof a.timeout&&void 0!==a.ontimeout},Ta=function(a){return"content-type"==a.toLowerCase()};S.prototype.timeout_=function(){"undefined"!=typeof aa&&this.xhr_&&(this.lastError_="Timed out after "+this.timeoutInterval_+"ms, aborting",R(this.logger_,T(this,this.lastError_)),this.dispatchEvent("timeout"),this.abort(8))};
-var Nc=function(a,b){a.active_=!1;a.xhr_&&(a.inAbort_=!0,a.xhr_.abort(),a.inAbort_=!1);a.lastError_=b;Qc(a);Rc(a)},Qc=function(a){a.errorDispatched_||(a.errorDispatched_=!0,a.dispatchEvent("complete"),a.dispatchEvent("error"))};S.prototype.abort=function(){this.xhr_&&this.active_&&(R(this.logger_,T(this,"Aborting")),this.active_=!1,this.inAbort_=!0,this.xhr_.abort(),this.inAbort_=!1,this.dispatchEvent("complete"),this.dispatchEvent("abort"),Rc(this))};
-S.prototype.disposeInternal=function(){this.xhr_&&(this.active_&&(this.active_=!1,this.inAbort_=!0,this.xhr_.abort(),this.inAbort_=!1),Rc(this,!0));S.superClass_.disposeInternal.call(this)};S.prototype.onReadyStateChange_=function(){if(!this.disposed_)if(this.inOpen_||this.inSend_||this.inAbort_)Sc(this);else this.onReadyStateChangeEntryPoint_()};S.prototype.onReadyStateChangeEntryPoint_=function(){Sc(this)};
-var Sc=function(a){if(a.active_&&"undefined"!=typeof aa)if(a.xhrOptions_[1]&&4==Tc(a)&&2==Uc(a))R(a.logger_,T(a,"Local request error detected and ignored"));else if(a.inSend_&&4==Tc(a))xc(a.onReadyStateChange_,0,a);else if(a.dispatchEvent("readystatechange"),4==Tc(a)){R(a.logger_,T(a,"Request complete"));a.active_=!1;try{var b=Uc(a),c,d;t:switch(b){case 200:case 201:case 202:case 204:case 206:case 304:case 1223:d=!0;break t;default:d=!1}if(!(c=d)){var e;if(e=0===b){var g=Ac(1,String(a.lastUri_));
-if(!g&&self.location)var f=self.location.protocol,g=f.substr(0,f.length-1);e=!Jc.test(g?g.toLowerCase():"")}c=e}if(c)a.dispatchEvent("complete"),a.dispatchEvent("success");else{var k;try{k=2<Tc(a)?a.xhr_.statusText:""}catch(u){R(a.logger_,"Can not get status: "+u.message),k=""}a.lastError_=k+" ["+Uc(a)+"]";Qc(a)}}finally{Rc(a)}}},Rc=function(a,b){if(a.xhr_){Oc(a);var c=a.xhr_,d=a.xhrOptions_[0]?ca:null;a.xhr_=null;a.xhrOptions_=null;b||a.dispatchEvent("ready");try{c.onreadystatechange=d}catch(e){(c=
-a.logger_)&&c.log(qc,"Problem encountered resetting onreadystatechange: "+e.message,void 0)}}},Oc=function(a){a.xhr_&&a.useXhr2Timeout_&&(a.xhr_.ontimeout=null);"number"==typeof a.timeoutId_&&(l.clearTimeout(a.timeoutId_),a.timeoutId_=null)},Tc=function(a){return a.xhr_?a.xhr_.readyState:0},Uc=function(a){try{return 2<Tc(a)?a.xhr_.status:-1}catch(b){return-1}},T=function(a,b){return b+" ["+a.lastMethod_+" "+a.lastUri_+" "+Uc(a)+"]"};var U=function(){};U.getInstance=function(){return U.instance_?U.instance_:U.instance_=new U};U.prototype.nextId_=0;var V=function(a){M.call(this);this.dom_=a||sb()};r(V,M);h=V.prototype;h.idGenerator_=U.getInstance();h.id_=null;h.inDocument_=!1;h.element_=null;h.parent_=null;h.children_=null;h.childIndex_=null;h.wasDecorated_=!1;h.getElement=function(){return this.element_};h.getParent=function(){return this.parent_};h.setParentEventTarget=function(a){if(this.parent_&&this.parent_!=a)throw Error("Method not supported");V.superClass_.setParentEventTarget.call(this,a)};h.getDomHelper=function(){return this.dom_};
-h.createDom=function(){this.element_=this.dom_.createElement("div")};
-var Wc=function(a,b){if(a.inDocument_)throw Error("Component already rendered");a.element_||a.createDom();b?b.insertBefore(a.element_,null):a.dom_.document_.body.appendChild(a.element_);a.parent_&&!a.parent_.inDocument_||Vc(a)},Vc=function(a){a.inDocument_=!0;Xc(a,function(a){!a.inDocument_&&a.getElement()&&Vc(a)})},Zc=function(a){Xc(a,function(a){a.inDocument_&&Zc(a)});a.googUiComponentHandler_&&a.googUiComponentHandler_.removeAll();a.inDocument_=!1};
-V.prototype.disposeInternal=function(){this.inDocument_&&Zc(this);this.googUiComponentHandler_&&(this.googUiComponentHandler_.dispose(),delete this.googUiComponentHandler_);Xc(this,function(a){a.dispose()});!this.wasDecorated_&&this.element_&&zb(this.element_);this.parent_=this.element_=this.childIndex_=this.children_=null;V.superClass_.disposeInternal.call(this)};var Xc=function(a,b){a.children_&&Qa(a.children_,b,void 0)};
-V.prototype.removeChild=function(a,b){if(a){var c=n(a)?a:a.id_||(a.id_=":"+(a.idGenerator_.nextId_++).toString(36)),d;this.childIndex_&&c?(d=this.childIndex_,d=(c in d?d[c]:void 0)||null):d=null;a=d;if(c&&a){d=this.childIndex_;c in d&&delete d[c];Va(this.children_,a);b&&(Zc(a),a.element_&&zb(a.element_));c=a;if(null==c)throw Error("Unable to set parent component");c.parent_=null;V.superClass_.setParentEventTarget.call(c,null)}}if(!a)throw Error("Child is not in parent component");return a};var W=function(a,b,c){V.call(this,c);this.captchaImage_=a;this.adImage_=b&&300==b.naturalWidth&&57==b.naturalHeight?b:null};r(W,V);W.prototype.createDom=function(){W.superClass_.createDom.call(this);var a=this.getElement();this.captchaImage_.alt=X.image_alt_text;this.getDomHelper().appendChild(a,this.captchaImage_);this.adImage_&&(this.adImage_.alt=X.image_alt_text,this.getDomHelper().appendChild(a,this.adImage_),this.adImage_&&$c(this.adImage_)&&(a.innerHTML+='<div id="recaptcha-ad-choices"><div class="recaptcha-ad-choices-collapsed"><img height="15" width="15" alt="AdChoices" border="0" src="//pagead2.googlesyndication.com/pagead/images/adchoices/icon.png"/></div><div class="recaptcha-ad-choices-expanded"><a href="https://support.google.com/adsense/troubleshooter/1631343" target="_blank"><img height="15" width="75" alt="AdChoices" border="0" src="//pagead2.googlesyndication.com/pagead/images/adchoices/en.png"/></a></div></div>'))};
-var $c=function(a){var b=ad(a,"visibility");a=ad(a,"display");return"hidden"!=b&&"none"!=a},ad=function(a,b){var c;t:{c=9==a.nodeType?a:a.ownerDocument||a.document;if(c.defaultView&&c.defaultView.getComputedStyle&&(c=c.defaultView.getComputedStyle(a,null))){c=c[b]||c.getPropertyValue(b)||"";break t}c=""}if(!c&&!(c=a.currentStyle?a.currentStyle[b]:null)&&(c=a.style[La(b)],"undefined"===typeof c)){c=a.style;var d;t:if(d=La(b),void 0===a.style[d]){var e=(E?"Webkit":D?"Moz":C?"ms":cb?"O":null)+Ma(b);
-if(void 0!==a.style[e]){d=e;break t}}c=c[d]||""}return c};W.prototype.disposeInternal=function(){delete this.captchaImage_;delete this.adImage_;W.superClass_.disposeInternal.call(this)};var bd=function(a){return Sa(a,function(a){a=a.toString(16);return 1<a.length?a:"0"+a}).join("")};var cd=function(){this.blockSize=-1};var dd=function(){this.blockSize=-1;this.blockSize=64;this.chain_=Array(4);this.block_=Array(this.blockSize);this.totalLength_=this.blockLength_=0;this.reset()};r(dd,cd);dd.prototype.reset=function(){this.chain_[0]=1732584193;this.chain_[1]=4023233417;this.chain_[2]=2562383102;this.chain_[3]=271733878;this.totalLength_=this.blockLength_=0};
-var ed=function(a,b,c){c||(c=0);var d=Array(16);if(n(b))for(var e=0;16>e;++e)d[e]=b.charCodeAt(c++)|b.charCodeAt(c++)<<8|b.charCodeAt(c++)<<16|b.charCodeAt(c++)<<24;else for(e=0;16>e;++e)d[e]=b[c++]|b[c++]<<8|b[c++]<<16|b[c++]<<24;b=a.chain_[0];c=a.chain_[1];var e=a.chain_[2],g=a.chain_[3],f=0,f=b+(g^c&(e^g))+d[0]+3614090360&4294967295;b=c+(f<<7&4294967295|f>>>25);f=g+(e^b&(c^e))+d[1]+3905402710&4294967295;g=b+(f<<12&4294967295|f>>>20);f=e+(c^g&(b^c))+d[2]+606105819&4294967295;e=g+(f<<17&4294967295|
-f>>>15);f=c+(b^e&(g^b))+d[3]+3250441966&4294967295;c=e+(f<<22&4294967295|f>>>10);f=b+(g^c&(e^g))+d[4]+4118548399&4294967295;b=c+(f<<7&4294967295|f>>>25);f=g+(e^b&(c^e))+d[5]+1200080426&4294967295;g=b+(f<<12&4294967295|f>>>20);f=e+(c^g&(b^c))+d[6]+2821735955&4294967295;e=g+(f<<17&4294967295|f>>>15);f=c+(b^e&(g^b))+d[7]+4249261313&4294967295;c=e+(f<<22&4294967295|f>>>10);f=b+(g^c&(e^g))+d[8]+1770035416&4294967295;b=c+(f<<7&4294967295|f>>>25);f=g+(e^b&(c^e))+d[9]+2336552879&4294967295;g=b+(f<<12&4294967295|
-f>>>20);f=e+(c^g&(b^c))+d[10]+4294925233&4294967295;e=g+(f<<17&4294967295|f>>>15);f=c+(b^e&(g^b))+d[11]+2304563134&4294967295;c=e+(f<<22&4294967295|f>>>10);f=b+(g^c&(e^g))+d[12]+1804603682&4294967295;b=c+(f<<7&4294967295|f>>>25);f=g+(e^b&(c^e))+d[13]+4254626195&4294967295;g=b+(f<<12&4294967295|f>>>20);f=e+(c^g&(b^c))+d[14]+2792965006&4294967295;e=g+(f<<17&4294967295|f>>>15);f=c+(b^e&(g^b))+d[15]+1236535329&4294967295;c=e+(f<<22&4294967295|f>>>10);f=b+(e^g&(c^e))+d[1]+4129170786&4294967295;b=c+(f<<
-5&4294967295|f>>>27);f=g+(c^e&(b^c))+d[6]+3225465664&4294967295;g=b+(f<<9&4294967295|f>>>23);f=e+(b^c&(g^b))+d[11]+643717713&4294967295;e=g+(f<<14&4294967295|f>>>18);f=c+(g^b&(e^g))+d[0]+3921069994&4294967295;c=e+(f<<20&4294967295|f>>>12);f=b+(e^g&(c^e))+d[5]+3593408605&4294967295;b=c+(f<<5&4294967295|f>>>27);f=g+(c^e&(b^c))+d[10]+38016083&4294967295;g=b+(f<<9&4294967295|f>>>23);f=e+(b^c&(g^b))+d[15]+3634488961&4294967295;e=g+(f<<14&4294967295|f>>>18);f=c+(g^b&(e^g))+d[4]+3889429448&4294967295;c=
-e+(f<<20&4294967295|f>>>12);f=b+(e^g&(c^e))+d[9]+568446438&4294967295;b=c+(f<<5&4294967295|f>>>27);f=g+(c^e&(b^c))+d[14]+3275163606&4294967295;g=b+(f<<9&4294967295|f>>>23);f=e+(b^c&(g^b))+d[3]+4107603335&4294967295;e=g+(f<<14&4294967295|f>>>18);f=c+(g^b&(e^g))+d[8]+1163531501&4294967295;c=e+(f<<20&4294967295|f>>>12);f=b+(e^g&(c^e))+d[13]+2850285829&4294967295;b=c+(f<<5&4294967295|f>>>27);f=g+(c^e&(b^c))+d[2]+4243563512&4294967295;g=b+(f<<9&4294967295|f>>>23);f=e+(b^c&(g^b))+d[7]+1735328473&4294967295;
-e=g+(f<<14&4294967295|f>>>18);f=c+(g^b&(e^g))+d[12]+2368359562&4294967295;c=e+(f<<20&4294967295|f>>>12);f=b+(c^e^g)+d[5]+4294588738&4294967295;b=c+(f<<4&4294967295|f>>>28);f=g+(b^c^e)+d[8]+2272392833&4294967295;g=b+(f<<11&4294967295|f>>>21);f=e+(g^b^c)+d[11]+1839030562&4294967295;e=g+(f<<16&4294967295|f>>>16);f=c+(e^g^b)+d[14]+4259657740&4294967295;c=e+(f<<23&4294967295|f>>>9);f=b+(c^e^g)+d[1]+2763975236&4294967295;b=c+(f<<4&4294967295|f>>>28);f=g+(b^c^e)+d[4]+1272893353&4294967295;g=b+(f<<11&4294967295|
-f>>>21);f=e+(g^b^c)+d[7]+4139469664&4294967295;e=g+(f<<16&4294967295|f>>>16);f=c+(e^g^b)+d[10]+3200236656&4294967295;c=e+(f<<23&4294967295|f>>>9);f=b+(c^e^g)+d[13]+681279174&4294967295;b=c+(f<<4&4294967295|f>>>28);f=g+(b^c^e)+d[0]+3936430074&4294967295;g=b+(f<<11&4294967295|f>>>21);f=e+(g^b^c)+d[3]+3572445317&4294967295;e=g+(f<<16&4294967295|f>>>16);f=c+(e^g^b)+d[6]+76029189&4294967295;c=e+(f<<23&4294967295|f>>>9);f=b+(c^e^g)+d[9]+3654602809&4294967295;b=c+(f<<4&4294967295|f>>>28);f=g+(b^c^e)+d[12]+
-3873151461&4294967295;g=b+(f<<11&4294967295|f>>>21);f=e+(g^b^c)+d[15]+530742520&4294967295;e=g+(f<<16&4294967295|f>>>16);f=c+(e^g^b)+d[2]+3299628645&4294967295;c=e+(f<<23&4294967295|f>>>9);f=b+(e^(c|~g))+d[0]+4096336452&4294967295;b=c+(f<<6&4294967295|f>>>26);f=g+(c^(b|~e))+d[7]+1126891415&4294967295;g=b+(f<<10&4294967295|f>>>22);f=e+(b^(g|~c))+d[14]+2878612391&4294967295;e=g+(f<<15&4294967295|f>>>17);f=c+(g^(e|~b))+d[5]+4237533241&4294967295;c=e+(f<<21&4294967295|f>>>11);f=b+(e^(c|~g))+d[12]+1700485571&
-4294967295;b=c+(f<<6&4294967295|f>>>26);f=g+(c^(b|~e))+d[3]+2399980690&4294967295;g=b+(f<<10&4294967295|f>>>22);f=e+(b^(g|~c))+d[10]+4293915773&4294967295;e=g+(f<<15&4294967295|f>>>17);f=c+(g^(e|~b))+d[1]+2240044497&4294967295;c=e+(f<<21&4294967295|f>>>11);f=b+(e^(c|~g))+d[8]+1873313359&4294967295;b=c+(f<<6&4294967295|f>>>26);f=g+(c^(b|~e))+d[15]+4264355552&4294967295;g=b+(f<<10&4294967295|f>>>22);f=e+(b^(g|~c))+d[6]+2734768916&4294967295;e=g+(f<<15&4294967295|f>>>17);f=c+(g^(e|~b))+d[13]+1309151649&
-4294967295;c=e+(f<<21&4294967295|f>>>11);f=b+(e^(c|~g))+d[4]+4149444226&4294967295;b=c+(f<<6&4294967295|f>>>26);f=g+(c^(b|~e))+d[11]+3174756917&4294967295;g=b+(f<<10&4294967295|f>>>22);f=e+(b^(g|~c))+d[2]+718787259&4294967295;e=g+(f<<15&4294967295|f>>>17);f=c+(g^(e|~b))+d[9]+3951481745&4294967295;a.chain_[0]=a.chain_[0]+b&4294967295;a.chain_[1]=a.chain_[1]+(e+(f<<21&4294967295|f>>>11))&4294967295;a.chain_[2]=a.chain_[2]+e&4294967295;a.chain_[3]=a.chain_[3]+g&4294967295};
-dd.prototype.update=function(a,b){void 0===b&&(b=a.length);for(var c=b-this.blockSize,d=this.block_,e=this.blockLength_,g=0;g<b;){if(0==e)for(;g<=c;)ed(this,a,g),g+=this.blockSize;if(n(a))for(;g<b;){if(d[e++]=a.charCodeAt(g++),e==this.blockSize){ed(this,d);e=0;break}}else for(;g<b;)if(d[e++]=a[g++],e==this.blockSize){ed(this,d);e=0;break}}this.blockLength_=e;this.totalLength_+=b};var Y=function(){K.call(this);this.callback_=this.element_=null;this.md5_=new dd};r(Y,K);var fd=function(a,b,c,d,e){a.unwatch();a.element_=b;a.callback_=e;a.listen(b,"keyup",p(a.onChanged_,a,c,d))};Y.prototype.unwatch=function(){this.element_&&this.callback_&&(this.removeAll(),this.callback_=this.element_=null)};
-Y.prototype.onChanged_=function(a,b){var c;c=(c=this.element_.value)?c.replace(/[\s\xa0]+/g,"").toLowerCase():"";this.md5_.reset();this.md5_.update(c+"."+b);c=this.md5_;var d=Array((56>c.blockLength_?c.blockSize:2*c.blockSize)-c.blockLength_);d[0]=128;for(var e=1;e<d.length-8;++e)d[e]=0;for(var g=8*c.totalLength_,e=d.length-8;e<d.length;++e)d[e]=g&255,g/=256;c.update(d);d=Array(16);for(e=g=0;4>e;++e)for(var f=0;32>f;f+=8)d[g++]=c.chain_[e]>>>f&255;bd(d).toLowerCase()==a.toLowerCase()&&this.callback_()};
-Y.prototype.disposeInternal=function(){this.element_=null;Y.superClass_.disposeInternal.call(this)};var hd=function(a,b,c){this.adObject_=a;this.captchaImageUrl_=b;this.opt_successCallback_=c||null;gd(this)},gd=function(a){var b=new N;dc(b,"recaptcha_challenge_image",a.captchaImageUrl_);dc(b,"recaptcha_ad_image",a.adObject_.imageAdUrl);var c={};Nb(b,"load",p(function(a,b){a[b.target.id]=b.target},a,c));Nb(b,"complete",p(a.handleImagesLoaded_,a,c));b.start()};
-hd.prototype.handleImagesLoaded_=function(a){a=new W(a.recaptcha_challenge_image,a.recaptcha_ad_image);var b=tb(document,"recaptcha_image");yb(b);Wc(a,b);a.adImage_&&$c(a.adImage_)&&(Mc(this.adObject_.delayedImpressionUrl),a=new Y,fd(a,tb(document,"recaptcha_response_field"),this.adObject_.hashedAnswer,this.adObject_.salt,p(function(a,b){a.unwatch();Mc(b)},this,a,this.adObject_.engagementUrl)),this.opt_successCallback_&&this.opt_successCallback_("04"+this.adObject_.token))};var ya=function(){var a=l.google_ad;return!!(a&&a.token&&a.imageAdUrl&&a.hashedAnswer&&a.salt&&a.delayedImpressionUrl&&a.engagementUrl)};var X=t;q("RecaptchaStr",X);var Z=l.RecaptchaOptions;q("RecaptchaOptions",Z);var id={tabindex:0,theme:"red",callback:null,lang:null,custom_theme_widget:null,custom_translations:null};q("RecaptchaDefaultOptions",id);
-var $={widget:null,timer_id:-1,style_set:!1,theme:null,type:"image",ajax_verify_cb:null,$:function(a){return"string"==typeof a?document.getElementById(a):a},attachEvent:function(a,b,c){a&&a.addEventListener?a.addEventListener(b,c,!1):a&&a.attachEvent&&a.attachEvent("on"+b,c)},create:function(a,b,c){$.destroy();b&&($.widget=$.$(b));$._init_options(c);$._call_challenge(a)},destroy:function(){var a=$.$("recaptcha_challenge_field");a&&a.parentNode.removeChild(a);-1!=$.timer_id&&clearInterval($.timer_id);
-$.timer_id=-1;if(a=$.$("recaptcha_image"))a.innerHTML="";$.widget&&("custom"!=$.theme?$.widget.innerHTML="":$.widget.style.display="none",$.widget=null)},focus_response_field:function(){$.$("recaptcha_response_field").focus()},get_challenge:function(){return"undefined"==typeof RecaptchaState?null:RecaptchaState.challenge},get_response:function(){var a=$.$("recaptcha_response_field");return a?a.value:null},ajax_verify:function(a){$.ajax_verify_cb=a;a=$.get_challenge()||"";var b=$.get_response()||"";
-a=$._get_api_server()+"/ajaxverify?c="+encodeURIComponent(a)+"&response="+encodeURIComponent(b);$._add_script(a)},_ajax_verify_callback:function(a){$.ajax_verify_cb(a)},_get_overridable_url:function(a){var b=window.location.protocol;if("undefined"!=typeof _RecaptchaOverrideApiServer)a=_RecaptchaOverrideApiServer;else if("undefined"!=typeof RecaptchaState&&"string"==typeof RecaptchaState.server&&0<RecaptchaState.server.length)return RecaptchaState.server.replace(/\/+$/,"");return b+"//"+a},_get_api_server:function(){return $._get_overridable_url("www.google.com/recaptcha/api")},
-_get_static_url_root:function(){return $._get_overridable_url("www.gstatic.com/recaptcha/api")},_call_challenge:function(a){a=$._get_api_server()+"/challenge?k="+a+"&ajax=1&cachestop="+Math.random();$.getLang_()&&(a+="&lang="+$.getLang_());"undefined"!=typeof Z.extra_challenge_params&&(a+="&"+Z.extra_challenge_params);$._add_script(a)},_add_script:function(a){var b=document.createElement("script");b.type="text/javascript";b.src=a;$._get_script_area().appendChild(b)},_get_script_area:function(){var a=
-document.getElementsByTagName("head");return a=!a||1>a.length?document.body:a[0]},_hash_merge:function(a){for(var b={},c=0;c<a.length;c++)for(var d in a[c])b[d]=a[c][d];return b},_init_options:function(a){Z=$._hash_merge([id,a||{}])},challenge_callback:function(){$._reset_timer();X=$._hash_merge([t,ta[$.getLang_()]||{},Z.custom_translations||{}]);window.addEventListener&&window.addEventListener("unload",function(){$.destroy()},!1);$._is_ie()&&window.attachEvent&&window.attachEvent("onbeforeunload",
-function(){});if(0<navigator.userAgent.indexOf("KHTML")){var a=document.createElement("iframe");a.src="about:blank";a.style.height="0px";a.style.width="0px";a.style.visibility="hidden";a.style.border="none";a.appendChild(document.createTextNode("This frame prevents back/forward cache problems in Safari."));document.body.appendChild(a)}$._finish_widget()},_add_css:function(a){if(-1!=navigator.appVersion.indexOf("MSIE 5"))document.write('<style type="text/css">'+a+"</style>");else{var b=document.createElement("style");
-b.type="text/css";b.styleSheet?b.styleSheet.cssText=a:b.appendChild(document.createTextNode(a));$._get_script_area().appendChild(b)}},_set_style:function(a){$.style_set||($.style_set=!0,$._add_css(a+"\n\n.recaptcha_is_showing_audio .recaptcha_only_if_image,.recaptcha_isnot_showing_audio .recaptcha_only_if_audio,.recaptcha_had_incorrect_sol .recaptcha_only_if_no_incorrect_sol,.recaptcha_nothad_incorrect_sol .recaptcha_only_if_incorrect_sol{display:none !important}"))},_init_builtin_theme:function(){var a=
-$.$,b=$._get_static_url_root(),c=s.VertCss,d=s.VertHtml,e=b+"/img/"+$.theme,g="gif",b=$.theme;"clean"==b&&(c=s.CleanCss,d=s.CleanHtml,g="png");c=c.replace(/IMGROOT/g,e);$._set_style(c);$.widget.innerHTML='<div id="recaptcha_area">'+d+"</div>";c=$.getLang_();a("recaptcha_privacy")&&null!=c&&"en"==c.substring(0,2).toLowerCase()&&null!=X.privacy_and_terms&&0<X.privacy_and_terms.length&&(c=document.createElement("a"),c.href="http://www.google.com/intl/en/policies/",c.target="_blank",c.innerHTML=X.privacy_and_terms,
-a("recaptcha_privacy").appendChild(c));c=function(b,c,d,L){var v=a(b);v.src=e+"/"+c+"."+g;c=X[d];v.alt=c;b=a(b+"_btn");b.title=c;$.attachEvent(b,"click",L)};c("recaptcha_reload","refresh","refresh_btn",$.reload);c("recaptcha_switch_audio","audio","audio_challenge",function(){$.switch_type("audio")});c("recaptcha_switch_img","text","visual_challenge",function(){$.switch_type("image")});c("recaptcha_whatsthis","help","help_btn",$.showhelp);"clean"==b&&(a("recaptcha_logo").src=e+"/logo."+g);a("recaptcha_table").className=
-"recaptchatable recaptcha_theme_"+$.theme;b=function(b,c){var d=a(b);d&&(RecaptchaState.rtl&&"span"==d.tagName.toLowerCase()&&(d.dir="rtl"),d.appendChild(document.createTextNode(X[c])))};b("recaptcha_instructions_image","instructions_visual");b("recaptcha_instructions_audio","instructions_audio");b("recaptcha_instructions_error","incorrect_try_again");a("recaptcha_instructions_image")||a("recaptcha_instructions_audio")||(b="audio"==$.type?X.instructions_audio:X.instructions_visual,b=b.replace(/:$/,
-""),a("recaptcha_response_field").setAttribute("placeholder",b))},_finish_widget:function(){var a=$.$,b=Z,c=b.theme;c in{blackglass:1,clean:1,custom:1,red:1,white:1}||(c="red");$.theme||($.theme=c);"custom"!=$.theme?$._init_builtin_theme():$._set_style("");c=document.createElement("span");c.id="recaptcha_challenge_field_holder";c.style.display="none";a("recaptcha_response_field").parentNode.insertBefore(c,a("recaptcha_response_field"));a("recaptcha_response_field").setAttribute("autocomplete","off");
-a("recaptcha_image").style.width="300px";a("recaptcha_image").style.height="57px";$.should_focus=!1;$._set_challenge(RecaptchaState.challenge,"image");$.updateTabIndexes_();$.widget&&($.widget.style.display="");b.callback&&b.callback()},updateTabIndexes_:function(){var a=$.$,b=Z;b.tabindex&&(b=b.tabindex,a("recaptcha_response_field").tabIndex=b++,"audio"==$.type&&a("recaptcha_audio_play_again")&&(a("recaptcha_audio_play_again").tabIndex=b++,a("recaptcha_audio_download"),a("recaptcha_audio_download").tabIndex=
-b++),"custom"!=$.theme&&(a("recaptcha_reload_btn").tabIndex=b++,a("recaptcha_switch_audio_btn").tabIndex=b++,a("recaptcha_switch_img_btn").tabIndex=b++,a("recaptcha_whatsthis_btn").tabIndex=b,a("recaptcha_privacy").tabIndex=b++))},switch_type:function(a){$.type=a;$.reload("audio"==$.type?"a":"v");if("custom"!=$.theme){a=$.$;var b="audio"==$.type?X.instructions_audio:X.instructions_visual,b=b.replace(/:$/,"");a("recaptcha_response_field").setAttribute("placeholder",b)}},reload:function(a){var b=Z,
-c=RecaptchaState;"undefined"==typeof a&&(a="r");c=$._get_api_server()+"/reload?c="+c.challenge+"&k="+c.site+"&reason="+a+"&type="+$.type;$.getLang_()&&(c+="&lang="+$.getLang_());"undefined"!=typeof b.extra_challenge_params&&(c+="&"+b.extra_challenge_params);"audio"==$.type&&(c=b.audio_beta_12_08?c+"&audio_beta_12_08=1":c+"&new_audio_default=1");$.should_focus="t"!=a;$._add_script(c)},finish_reload:function(a,b,c){RecaptchaState.payload_url=c;RecaptchaState.is_incorrect=!1;$._set_challenge(a,b);$.updateTabIndexes_()},
-_set_challenge:function(a,b){var c=$.$,d=RecaptchaState;d.challenge=a;$.type=b;c("recaptcha_challenge_field_holder").innerHTML='<input type="hidden" name="recaptcha_challenge_field" id="recaptcha_challenge_field" value="'+d.challenge+'"/>';if("audio"==b)c("recaptcha_image").innerHTML=$.getAudioCaptchaHtml(),$._loop_playback();else if("image"==b){var e=d.payload_url;e||(e=$._get_api_server()+"/image?c="+d.challenge);ya()?(new hd(za(),e,function(a){RecaptchaState.challenge=a;c("recaptcha_challenge_field").value=
-a}),l.google_ad&&(l.google_ad=null)):c("recaptcha_image").innerHTML='<img id="recaptcha_challenge_image" alt="'+X.image_alt_text+'" height="57" width="300" src="'+e+'" />'}$._css_toggle("recaptcha_had_incorrect_sol","recaptcha_nothad_incorrect_sol",d.is_incorrect);$._css_toggle("recaptcha_is_showing_audio","recaptcha_isnot_showing_audio","audio"==b);$._clear_input();$.should_focus&&$.focus_response_field();$._reset_timer()},_reset_timer:function(){clearInterval($.timer_id);var a=Math.max(1E3*(RecaptchaState.timeout-
-60),6E4);$.timer_id=setInterval(function(){$.reload("t")},a);return a},showhelp:function(){window.open($._get_help_link(),"recaptcha_popup","width=460,height=580,location=no,menubar=no,status=no,toolbar=no,scrollbars=yes,resizable=yes")},_clear_input:function(){$.$("recaptcha_response_field").value=""},_displayerror:function(a){var b=$.$;b("recaptcha_image").innerHTML="";b("recaptcha_image").appendChild(document.createTextNode(a))},reloaderror:function(a){$._displayerror(a)},_is_ie:function(){return 0<
-navigator.userAgent.indexOf("MSIE")&&!window.opera},_css_toggle:function(a,b,c){var d=$.widget;d||(d=document.body);var e=d.className,e=e.replace(RegExp("(^|\\s+)"+a+"(\\s+|$)")," "),e=e.replace(RegExp("(^|\\s+)"+b+"(\\s+|$)")," ");d.className=e+(" "+(c?a:b))},_get_help_link:function(){var a=$._get_api_server().replace(/\/[a-zA-Z0-9]+\/?$/,"/help"),a=a+("?c="+RecaptchaState.challenge);$.getLang_()&&(a+="&hl="+$.getLang_());return a},playAgain:function(){$.$("recaptcha_image").innerHTML=$.getAudioCaptchaHtml();
-$._loop_playback()},_loop_playback:function(){var a=$.$("recaptcha_audio_play_again");a&&$.attachEvent(a,"click",function(){$.playAgain();return!1})},getAudioCaptchaHtml:function(){var a=RecaptchaState.payload_url;a||(a=$._get_api_server()+"/audio.mp3?c="+RecaptchaState.challenge);var b=$._get_static_url_root()+"/img/audiocaptcha.swf?v2",b=$._is_ie()?'<object classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000" id="audiocaptcha" width="0" height="0" codebase="https://fpdownload.macromedia.com/get/flashplayer/current/swflash.cab"><param name="movie" value="'+
-b+'" /><param name="quality" value="high" /><param name="bgcolor" value="#869ca7" /><param name="allowScriptAccess" value="always" /></object><br/>':'<embed src="'+b+'" quality="high" bgcolor="#869ca7" width="0" height="0" name="audiocaptcha" align="middle" play="true" loop="false" quality="high" allowScriptAccess="always" type="application/x-shockwave-flash" pluginspage="http://www.adobe.com/go/getflashplayer" /></embed>',c="";$.checkFlashVer()&&(c="<br/>"+$.getSpan_('<a id="recaptcha_audio_play_again" class="recaptcha_audio_cant_hear_link">'+
-X.play_again+"</a>"));c+="<br/>"+$.getSpan_('<a id="recaptcha_audio_download" class="recaptcha_audio_cant_hear_link" target="_blank" href="'+a+'">'+X.cant_hear_this+"</a>");return b+c},getSpan_:function(a){return"<span"+(RecaptchaState&&RecaptchaState.rtl?' dir="rtl"':"")+">"+a+"</span>"},gethttpwavurl:function(){if("audio"!=$.type)return"";var a=RecaptchaState.payload_url;a||(a=$._get_api_server()+"/image?c="+RecaptchaState.challenge);return a},checkFlashVer:function(){var a=-1!=navigator.appVersion.indexOf("MSIE"),
-b=-1!=navigator.appVersion.toLowerCase().indexOf("win"),c=-1!=navigator.userAgent.indexOf("Opera"),d=-1;if(null!=navigator.plugins&&0<navigator.plugins.length){if(navigator.plugins["Shockwave Flash 2.0"]||navigator.plugins["Shockwave Flash"])d=navigator.plugins["Shockwave Flash"+(navigator.plugins["Shockwave Flash 2.0"]?" 2.0":"")].description.split(" ")[2].split(".")[0]}else if(a&&b&&!c)try{d=(new ActiveXObject("ShockwaveFlash.ShockwaveFlash.7")).GetVariable("$version").split(" ")[1].split(",")[0]}catch(e){}return 9<=
-d},getLang_:function(){return"undefined"!=typeof RecaptchaState&&RecaptchaState.lang?RecaptchaState.lang:Z.lang?Z.lang:null}};q("Recaptcha",$);})()
diff --git a/themes/bootstrap3/js/vendor/validator.min.js b/themes/bootstrap3/js/vendor/validator.min.js
index ba7628243279965d8561ff8ce21630be817f607c..b82d0b12439c9459f19cf166379deec97eeb3f5b 100644
--- a/themes/bootstrap3/js/vendor/validator.min.js
+++ b/themes/bootstrap3/js/vendor/validator.min.js
@@ -1,9 +1,9 @@
 /*!
- * Validator v0.5.0 for Bootstrap 3, by @1000hz
- * Copyright 2014 Spiceworks, Inc.
+ * Validator v0.10.2 for Bootstrap 3, by @1000hz
+ * Copyright 2016 Cina Saffary
  * Licensed under http://opensource.org/licenses/MIT
  *
  * https://github.com/1000hz/bootstrap-validator
  */
 
-+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=a.extend({},c.DEFAULTS,d.data(),"object"==typeof b&&b),f=d.data("bs.validator");f||d.data("bs.validator",f=new c(this,e)),"string"==typeof b&&f[b]()})}var c=function(b,c){this.$element=a(b),this.options=c,this.$element.attr("novalidate",!0),this.toggleSubmit(),this.$element.on("input.bs.validator change.bs.validator focusout.bs.validator",a.proxy(this.validateInput,this)),this.$element.on("submit.bs.validator",a.proxy(this.onSubmit,this)),this.$element.find("[data-match]").each(function(){var b=a(this),c=b.data("match");a(c).on("input.bs.validator",function(){b.val()&&b.trigger("input")})})};c.DEFAULTS={delay:500,html:!1,errors:{match:"Does not match",minlength:"Not long enough"}},c.VALIDATORS={"native":function(a){var b=a[0];return b.checkValidity?b.checkValidity():!0},match:function(b){var c=b.data("match");return!b.val()||b.val()===a(c).val()},minlength:function(a){var b=a.data("minlength");return!a.val()||a.val().length>=b}},c.prototype.validateInput=function(b){var c=a(b.target),d=c.data("bs.validator.errors");if(c.is('[type="radio"]')&&(c=this.$element.find('input[name="'+c.attr("name")+'"]')),this.$element.trigger(b=a.Event("validate.bs.validator",{relatedTarget:c[0]})),!b.isDefaultPrevented()){var e=this;this.runValidators(c).done(function(f){c.data("bs.validator.errors",f),f.length?e.showErrors(c):e.clearErrors(c),d&&f.toString()===d.toString()||(b=f.length?a.Event("invalid.bs.validator",{relatedTarget:c[0],detail:f}):a.Event("valid.bs.validator",{relatedTarget:c[0],detail:d}),e.$element.trigger(b)),e.toggleSubmit(),e.$element.trigger(a.Event("validated.bs.validator",{relatedTarget:c[0]}))})}},c.prototype.runValidators=function(b){function d(a){return b.data(a+"-error")||b.data("error")||"native"==a&&b[0].validationMessage||g.errors[a]}var e=[],f=([c.VALIDATORS.native],a.Deferred()),g=this.options;return b.data("bs.validator.deferred")&&b.data("bs.validator.deferred").reject(),b.data("bs.validator.deferred",f),a.each(c.VALIDATORS,a.proxy(function(a,c){if((b.data(a)||"native"==a)&&!c.call(this,b)){var f=d(a);!~e.indexOf(f)&&e.push(f)}},this)),!e.length&&b.val()&&b.data("remote")?this.defer(b,function(){a.get(b.data("remote"),[b.attr("name"),b.val()].join("=")).fail(function(a,b,c){e.push(d("remote")||c)}).always(function(){f.resolve(e)})}):f.resolve(e),f.promise()},c.prototype.validate=function(){var a=this.options.delay;return this.options.delay=0,this.$element.find(":input").trigger("input"),this.options.delay=a,this},c.prototype.showErrors=function(b){var c=this.options.html?"html":"text";this.defer(b,function(){var d=b.closest(".form-group"),e=d.find(".help-block.with-errors"),f=b.data("bs.validator.errors");f.length&&(f=a("<ul/>").addClass("list-unstyled").append(a.map(f,function(b){return a("<li/>")[c](b)})),void 0===e.data("bs.validator.originalContent")&&e.data("bs.validator.originalContent",e.html()),e.empty().append(f),d.addClass("has-error"))})},c.prototype.clearErrors=function(a){var b=a.closest(".form-group"),c=b.find(".help-block.with-errors");c.html(c.data("bs.validator.originalContent")),b.removeClass("has-error")},c.prototype.hasErrors=function(){function b(){return!!(a(this).data("bs.validator.errors")||[]).length}return!!this.$element.find(":input:enabled").filter(b).length},c.prototype.isIncomplete=function(){function b(){return"checkbox"===this.type?!this.checked:"radio"===this.type?!a('[name="'+this.name+'"]:checked').length:""===a.trim(this.value)}return!!this.$element.find(":input[required]:enabled").filter(b).length},c.prototype.onSubmit=function(a){this.validate(),(this.isIncomplete()||this.hasErrors())&&a.preventDefault()},c.prototype.toggleSubmit=function(){var a=this.$element.find('input[type="submit"], button[type="submit"]');a.toggleClass("disabled",this.isIncomplete()||this.hasErrors()).css({"pointer-events":"all",cursor:"pointer"})},c.prototype.defer=function(a,b){window.clearTimeout(a.data("bs.validator.timeout")),a.data("bs.validator.timeout",window.setTimeout(b,this.options.delay))};var d=a.fn.validator;a.fn.validator=b,a.fn.validator.Constructor=c,a.fn.validator.noConflict=function(){return a.fn.validator=d,this},a(window).on("load",function(){a('form[data-toggle="validator"]').each(function(){var c=a(this);b.call(c,c.data())})})}(jQuery);
\ No newline at end of file
++function(a){"use strict";function b(b){return b.is('[type="checkbox"]')?b.prop("checked"):b.is('[type="radio"]')?!!a('[name="'+b.attr("name")+'"]:checked').length:a.trim(b.val())}function c(b){return this.each(function(){var c=a(this),e=a.extend({},d.DEFAULTS,c.data(),"object"==typeof b&&b),f=c.data("bs.validator");(f||"destroy"!=b)&&(f||c.data("bs.validator",f=new d(this,e)),"string"==typeof b&&f[b]())})}var d=function(c,e){this.options=e,this.$element=a(c),this.$inputs=this.$element.find(d.INPUT_SELECTOR),this.$btn=a('button[type="submit"], input[type="submit"]').filter('[form="'+this.$element.attr("id")+'"]').add(this.$element.find('input[type="submit"], button[type="submit"]')),e.errors=a.extend({},d.DEFAULTS.errors,e.errors);for(var f in e.custom)if(!e.errors[f])throw new Error("Missing default error message for custom validator: "+f);a.extend(d.VALIDATORS,e.custom),this.$element.attr("novalidate",!0),this.toggleSubmit(),this.$element.on("input.bs.validator change.bs.validator focusout.bs.validator",d.INPUT_SELECTOR,a.proxy(this.onInput,this)),this.$element.on("submit.bs.validator",a.proxy(this.onSubmit,this)),this.$element.find("[data-match]").each(function(){var c=a(this),d=c.data("match");a(d).on("input.bs.validator",function(){b(c)&&c.trigger("input.bs.validator")})})};d.INPUT_SELECTOR=':input:not([type="submit"], button):enabled:visible',d.FOCUS_OFFSET=20,d.DEFAULTS={delay:500,html:!1,disable:!0,focus:!0,custom:{},errors:{match:"Does not match",minlength:"Not long enough"},feedback:{success:"glyphicon-ok",error:"glyphicon-remove"}},d.VALIDATORS={"native":function(a){var b=a[0];return b.checkValidity?b.checkValidity():!0},match:function(b){var c=b.data("match");return!b.val()||b.val()===a(c).val()},minlength:function(a){var b=a.data("minlength");return!a.val()||a.val().length>=b}},d.prototype.onInput=function(b){var c=this,d=a(b.target),e="focusout"!==b.type;this.validateInput(d,e).done(function(){c.toggleSubmit()})},d.prototype.validateInput=function(c,d){var e=b(c),f=c.data("bs.validator.previous"),g=c.data("bs.validator.errors");if(f===e)return a.Deferred().resolve();c.data("bs.validator.previous",e),c.is('[type="radio"]')&&(c=this.$element.find('input[name="'+c.attr("name")+'"]'));var h=a.Event("validate.bs.validator",{relatedTarget:c[0]});if(this.$element.trigger(h),!h.isDefaultPrevented()){var i=this;return this.runValidators(c).done(function(b){c.data("bs.validator.errors",b),b.length?d?i.defer(c,i.showErrors):i.showErrors(c):i.clearErrors(c),g&&b.toString()===g.toString()||(h=b.length?a.Event("invalid.bs.validator",{relatedTarget:c[0],detail:b}):a.Event("valid.bs.validator",{relatedTarget:c[0],detail:g}),i.$element.trigger(h)),i.toggleSubmit(),i.$element.trigger(a.Event("validated.bs.validator",{relatedTarget:c[0]}))})}},d.prototype.runValidators=function(c){function e(a){return c.data(a+"-error")||c.data("error")||"native"==a&&c[0].validationMessage||h.errors[a]}var f=[],g=a.Deferred(),h=this.options;return c.data("bs.validator.deferred")&&c.data("bs.validator.deferred").reject(),c.data("bs.validator.deferred",g),a.each(d.VALIDATORS,a.proxy(function(a,d){if((b(c)||c.attr("required"))&&(c.data(a)||"native"==a)&&!d.call(this,c)){var g=e(a);!~f.indexOf(g)&&f.push(g)}},this)),!f.length&&b(c)&&c.data("remote")?this.defer(c,function(){var d={};d[c.attr("name")]=b(c),a.get(c.data("remote"),d).fail(function(a,b,c){f.push(e("remote")||c)}).always(function(){g.resolve(f)})}):g.resolve(f),g.promise()},d.prototype.validate=function(){var b=this;return a.when(this.$inputs.map(function(){return b.validateInput(a(this),!1)})).then(function(){b.toggleSubmit(),b.focusError()}),this},d.prototype.focusError=function(){if(this.options.focus){var b=a(".has-error:first :input");0!==b.length&&(a(document.body).animate({scrollTop:b.offset().top-d.FOCUS_OFFSET},250),b.focus())}},d.prototype.showErrors=function(b){var c=this.options.html?"html":"text",d=b.data("bs.validator.errors"),e=b.closest(".form-group"),f=e.find(".help-block.with-errors"),g=e.find(".form-control-feedback");d.length&&(d=a("<ul/>").addClass("list-unstyled").append(a.map(d,function(b){return a("<li/>")[c](b)})),void 0===f.data("bs.validator.originalContent")&&f.data("bs.validator.originalContent",f.html()),f.empty().append(d),e.addClass("has-error has-danger"),e.hasClass("has-feedback")&&g.removeClass(this.options.feedback.success)&&g.addClass(this.options.feedback.error)&&e.removeClass("has-success"))},d.prototype.clearErrors=function(a){var c=a.closest(".form-group"),d=c.find(".help-block.with-errors"),e=c.find(".form-control-feedback");d.html(d.data("bs.validator.originalContent")),c.removeClass("has-error has-danger"),c.hasClass("has-feedback")&&e.removeClass(this.options.feedback.error)&&b(a)&&e.addClass(this.options.feedback.success)&&c.addClass("has-success")},d.prototype.hasErrors=function(){function b(){return!!(a(this).data("bs.validator.errors")||[]).length}return!!this.$inputs.filter(b).length},d.prototype.isIncomplete=function(){function c(){return!b(a(this))}return!!this.$inputs.filter("[required]").filter(c).length},d.prototype.onSubmit=function(a){this.validate(),(this.isIncomplete()||this.hasErrors())&&a.preventDefault()},d.prototype.toggleSubmit=function(){this.options.disable&&this.$btn.toggleClass("disabled",this.isIncomplete()||this.hasErrors())},d.prototype.defer=function(b,c){return c=a.proxy(c,this,b),this.options.delay?(window.clearTimeout(b.data("bs.validator.timeout")),void b.data("bs.validator.timeout",window.setTimeout(c,this.options.delay))):c()},d.prototype.destroy=function(){return this.$element.removeAttr("novalidate").removeData("bs.validator").off(".bs.validator").find(".form-control-feedback").removeClass([this.options.feedback.error,this.options.feedback.success].join(" ")),this.$inputs.off(".bs.validator").removeData(["bs.validator.errors","bs.validator.deferred","bs.validator.previous"]).each(function(){var b=a(this),c=b.data("bs.validator.timeout");window.clearTimeout(c)&&b.removeData("bs.validator.timeout")}),this.$element.find(".help-block.with-errors").each(function(){var b=a(this),c=b.data("bs.validator.originalContent");b.removeData("bs.validator.originalContent").html(c)}),this.$element.find('input[type="submit"], button[type="submit"]').removeClass("disabled"),this.$element.find(".has-error, .has-danger").removeClass("has-error has-danger"),this};var e=a.fn.validator;a.fn.validator=c,a.fn.validator.Constructor=d,a.fn.validator.noConflict=function(){return a.fn.validator=e,this},a(window).on("load",function(){a('form[data-toggle="validator"]').each(function(){var b=a(this);c.call(b,b.data())})})}(jQuery);
\ No newline at end of file
diff --git a/themes/bootstrap3/js/vudl/canvas-zoomy.js b/themes/bootstrap3/js/vudl/canvas-zoomy.js
deleted file mode 100644
index 2fde8e14b407e050257b82887b5a15248c859aa6..0000000000000000000000000000000000000000
--- a/themes/bootstrap3/js/vudl/canvas-zoomy.js
+++ /dev/null
@@ -1,334 +0,0 @@
-var Zoomy = {
-  mouseDown: false,
-  mouseOnMap: null,
-  // Create
-  init: function(canvas) {
-    this.canvas  = canvas;
-    this.showMinimap = true;
-    this.canvas.width  = Math.floor(this.canvas.clientWidth);
-    this.canvas.height = Math.floor(this.canvas.clientHeight);
-    addEventListener('mousemove', Zoomy.mouseHandle, false);
-    addEventListener('touchmove', Zoomy.mouseHandle, false);
-    addEventListener('mouseup', function(e) {
-      Zoomy.mouse = undefined;
-      Zoomy.mouseDown = false;
-      Zoomy.mouseOnMap = null;
-    }, false);
-    addEventListener('touchend', function(e) {
-      Zoomy.mouse = undefined;
-      Zoomy.mouseDown = false;
-      Zoomy.mouseOnMap = null;
-    }, false);
-    this.canvas.addEventListener('mousedown', function(e) {
-      Zoomy.mouseDown = true;
-    }, false);
-    this.canvas.addEventListener('touchstart', function(e) {
-      Zoomy.mouseDown = true;
-    }, false);
-    this.canvas.addEventListener('mousewheel', function(e) {
-      e.preventDefault();
-      Zoomy.zoom(e);
-    }, false);
-    this.canvas.addEventListener('wheel', function(e) {
-      e.preventDefault();
-      Zoomy.zoom(e);
-    }, false);
-    this.context = canvas.getContext('2d');
-
-    Math.TWO_PI = Math.PI*2;
-    Math.HALF_PI = Math.PI/2;
-  },
-  resize: function() {
-    if(typeof this.canvas === "undefined") {
-      return;
-    }
-    this.canvas.width  = Math.floor(this.canvas.clientWidth);
-    this.canvas.height = Math.floor(this.canvas.clientHeight);
-    this.width = this.canvas.width;
-    this.height = this.canvas.height;
-    this.minimap = null;
-    this.rebound();
-    this.draw();
-  },
-  initMinimap: function() {
-    var aspectRatio = this.image.width / this.image.height;
-    var size = 150;
-    var mm = {
-      width  : this.image.width > this.image.height ? size : size * aspectRatio,
-      height : this.image.height > this.image.width ? size : size / aspectRatio,
-      size   : size
-    };
-    if(this.image.sideways) {
-      var t = mm.width;
-      mm.width = mm.height;
-      mm.height = t;
-    }
-    mm.x = this.width  - (size+mm.width)/2 - 10;
-    mm.y = this.height - (size+mm.height)/2 - 10;
-    return mm;
-  },
-  mouseHandle: function(e) {
-    if(!Zoomy.mouseDown) return;
-    e.preventDefault();
-    var bounds = Zoomy.canvas.getBoundingClientRect();
-    var mx = e.type.match("touch")
-      ? e.targetTouches[0].pageX
-      : e.pageX;
-    mx -= bounds.left;
-    var my = e.type.match("touch")
-      ? e.targetTouches[0].pageY
-      : e.pageY;
-    my -= bounds.top + window.scrollY;
-    if(typeof Zoomy.mouse !== "undefined") {
-      var xdiff = mx - Zoomy.mouse.x;
-      var ydiff = my - Zoomy.mouse.y;
-      if(Zoomy.mouseOnMap === null) {
-        Zoomy.mouseOnMap = Zoomy.minimap != null
-                        && mx > Zoomy.minimap.rect.x
-                        && mx < Zoomy.minimap.rect.x+Zoomy.minimap.rect.w
-                        && my > Zoomy.minimap.rect.y
-                        && my < Zoomy.minimap.rect.y+Zoomy.minimap.rect.h;
-      }
-      if(Zoomy.mouseOnMap) {
-        var ratio = Zoomy.image.rwidth / Zoomy.minimap.width;
-        if(Zoomy.image.rwidth < Zoomy.width)   xdiff = 0;
-        if(Zoomy.image.rheight < Zoomy.height) ydiff = 0;
-        switch(Zoomy.image.angle % Math.TWO_PI) {
-          case 0:
-            xdiff *= -ratio;
-            ydiff *= -ratio;
-            break;
-          case Math.HALF_PI: // On right side
-            var xtemp = xdiff;
-            xdiff = ydiff * ratio;
-            ydiff = xtemp * -ratio;
-            break;
-          case Math.PI: // Upside-down
-            xdiff *=  ratio;
-            ydiff *=  ratio;
-            break;
-          default: // On left side
-            var xtemp = xdiff;
-            xdiff = ydiff * -ratio;
-            ydiff = xtemp * ratio;
-            break;
-        }
-      }
-      Zoomy.image.x = Math.floor(Zoomy.image.x + xdiff);
-      Zoomy.image.y = Math.floor(Zoomy.image.y + ydiff);
-      Zoomy.enforceBounds();
-      Zoomy.draw();
-    }
-    Zoomy.mouse = {x: mx, y: my};
-  },
-  // Load image
-  load: function(src, callback) {
-    if(typeof this.canvas === "undefined") return;
-    var img = new Image();
-    img.onload = function() { Zoomy.finishLoad(img); if(typeof callback !== "undefined") callback(); }
-    img.src = src;
-  },
-  finishLoad: function(img) {
-    //console.log('Loaded.');
-    Zoomy.image = {
-      x: 0,
-      y: 0,
-      angle: 0,
-      sideways: false,
-      content: img,
-      transX: 0,
-      transY: 0,
-    }
-    Zoomy.minimap = null;
-    Zoomy.center();
-  },
-  draw: function() {
-    this.context.clearRect(0,0,this.width,this.height);
-    // Image
-    this.context.save();
-    this.context.translate(this.image.x, this.image.y);
-    this.context.rotate(this.image.angle);
-    this.context.translate(this.image.transX, this.image.transY);
-    this.context.drawImage(
-      this.image.content, 0, 0,
-      this.image.rwidth,
-      this.image.rheight
-    );
-    this.context.restore();
-
-    // Minimap
-    if(!this.showMinimap) return;
-    if(this.minimap == null) {
-      this.minimap = this.initMinimap();
-    }
-    this.context.drawImage(
-      this.image.content,
-      this.minimap.x,
-      this.minimap.y,
-      this.minimap.width,
-      this.minimap.height
-    );
-
-    var hLength = (this.width  / this.image.rwidth)  * this.minimap.width;
-    var vLength = (this.height / this.image.rheight) * this.minimap.height;
-    var drawWidth  = this.image.sideways ? vLength : hLength;
-    var drawHeight = this.image.sideways ? hLength : vLength;
-
-    var xdiff = this.image.sideways
-      ? -(this.image.y / this.image.height) * this.minimap.width
-      : -(this.image.x / this.image.width)  * this.minimap.width;
-    var ydiff = this.image.sideways
-      ? -(this.image.x / this.image.width)  * this.minimap.height
-      : -(this.image.y / this.image.height) * this.minimap.height;
-    switch(this.image.angle % Math.TWO_PI) {
-      case 0:
-        break;
-      case Math.HALF_PI: // On right side
-        ydiff = this.minimap.height - drawHeight - ydiff;
-        break;
-      case Math.PI: // Upside-down
-        xdiff = this.minimap.width  - drawWidth  - xdiff;
-        ydiff = this.minimap.height - drawHeight - ydiff;
-        break;
-      default: // On left side
-        xdiff = this.minimap.width  - drawWidth  - xdiff;
-        break;
-    }
-
-    if(drawWidth > this.minimap.width) {
-      xdiff = 0;
-      drawWidth = this.minimap.width;
-    }
-    if(drawHeight > this.minimap.height) {
-      ydiff = 0;
-      drawHeight = this.minimap.height;
-    }
-    this.minimap.rect = {
-      x: this.minimap.x+Math.floor(Math.max(0, xdiff)),
-      y: this.minimap.y+Math.floor(Math.max(0, ydiff)),
-      w: Math.ceil(drawWidth),
-      h: Math.ceil(drawHeight)
-    };
-    this.context.save();
-    this.context.strokeStyle = "#00F";
-    this.context.strokeRect(
-      this.minimap.rect.x,
-      this.minimap.rect.y,
-      this.minimap.rect.w,
-      this.minimap.rect.h
-    );
-    this.context.restore();
-  },
-  center: function() {
-    this.width = this.canvas.width;
-    this.height = this.canvas.height;
-    this.image.zoom = this.image.minZoom = Math.min(
-      (this.width-10)/this.image.content.width, 1,
-      (this.height-10)/this.image.content.height
-    );
-    this.zoom(0, this.image.zoom);
-    this.image.x = Math.floor((this.width-this.image.width)/2);
-    this.image.y = Math.floor((this.height-this.image.height)/2);
-    this.rebound();
-    this.draw();
-  },
-  turnLeft: function() {
-    var newx = this.width/2  + (this.image.y - this.height/2);
-    var newy = this.height/2 + (this.width/2 - this.image.x - this.image.width);
-    this.image.x = newx;
-    this.image.y = newy;
-
-    this.image.angle = (this.image.angle + Math.PI + Math.HALF_PI) % Math.TWO_PI;
-    this.image.width = [this.image.height, this.image.height=this.image.width][0];
-    this.image.sideways = !this.image.sideways;
-
-    this.rebound();
-    this.draw();
-  },
-  turnRight: function() {
-    var newx = this.width/2  + (this.height/2 - this.image.y - this.image.height);
-    var newy = this.height/2 + (this.image.x - this.width/2);
-    this.image.x = newx;
-    this.image.y = newy;
-
-    this.image.angle = (this.image.angle + Math.HALF_PI) % Math.TWO_PI;
-    this.image.width = [this.image.height, this.image.height=this.image.width][0];
-    this.image.sideways = !this.image.sideways;
-
-    this.rebound();
-    this.draw();
-  },
-  rebound: function() {
-    var xDiff = this.width-this.image.width;
-    var yDiff = this.height-this.image.height;
-    this.image.minX = Math.min(0, xDiff);
-    this.image.minY = Math.min(0, yDiff);
-    this.image.maxX = Math.max(xDiff, 0);
-    this.image.maxY = Math.max(yDiff, 0);
-    this.enforceBounds();
-    var rotation = this.image.angle / Math.HALF_PI;
-    this.image.transX = rotation == 2
-      ? -this.image.width
-      : rotation == 3
-        ? -this.image.height
-        : 0;
-    this.image.transY = rotation == 1
-      ? -this.image.width
-      : rotation == 2
-        ? -this.image.height
-        : 0;
-  },
-  enforceBounds: function() {
-    if(this.image.x < this.image.minX) this.image.x = this.image.minX;
-    if(this.image.y < this.image.minY) this.image.y = this.image.minY;
-    if(this.image.x > this.image.maxX) this.image.x = this.image.maxX;
-    if(this.image.y > this.image.maxY) this.image.y = this.image.maxY;
-  },
-  zoom: function(event, zoom) {
-    if (typeof zoom === "undefined") {
-      var delta = typeof event.deltaY === "undefined"
-        ? event.detail/Math.abs(event.detail)
-        : event.deltaY/Math.abs(event.deltaY);
-      this.image.zoom *= 1-(delta/12);
-    } else {
-      this.image.zoom = zoom;
-    }
-    if (this.image.zoom < this.image.minZoom) {
-      this.image.zoom = this.image.minZoom;
-    }
-
-    var mousex = this.width/2;
-    var mousey = this.height/2;
-    if (typeof event.offsetX !== "undefined") {
-      mousex = event.offsetX;
-      mousey = event.offsetY;
-    } else if (typeof event.layerX !== "undefined") {
-      mousex = event.layerX;
-      mousey = event.layerY;
-    }
-
-    var newWidth  = Math.floor(this.image.content.width * this.image.zoom);
-    var newHeight = Math.floor(this.image.content.height * this.image.zoom);
-
-    if ((this.image.angle/Math.HALF_PI) % 2 > 0) {
-      newWidth = [newHeight, newHeight = newWidth][0];
-      this.image.rwidth = newHeight;
-      this.image.rheight = newWidth;
-    } else {
-      this.image.rwidth = newWidth;
-      this.image.rheight = newHeight;
-    }
-
-    this.image.x -= Math.floor(((mousex-this.image.x)/this.image.width)*(newWidth-this.image.width));
-    this.image.y -= Math.floor(((mousey-this.image.y)/this.image.height)*(newHeight-this.image.height));
-
-    this.image.width = newWidth;
-    this.image.height = newHeight;
-    this.rebound();
-    this.draw();
-  },
-  toggleMap: function() {
-    this.showMinimap = !this.showMinimap;
-    this.draw();
-  }
-};
\ No newline at end of file
diff --git a/themes/bootstrap3/js/vudl/config.js b/themes/bootstrap3/js/vudl/config.js
deleted file mode 100644
index 8fc6d6bcac44bc5b683c8343a99327e35129ffd7..0000000000000000000000000000000000000000
--- a/themes/bootstrap3/js/vudl/config.js
+++ /dev/null
@@ -1,11 +0,0 @@
-var vudlSettings = {
-  'accordion': {
-    'bottom'      :10, // Pixels from the bottom to adjust accordion
-    'headerHeight':40  // Height of the headers, plus top-bottom margins
-  },
-  'scroll': {
-    'top'     :100, // Pixels above to of accordion to trigger loading
-    'bottom'  :300, // Pixels below
-    'selected':10   // When scrolling to selected, spacing from top
-  },
-}
\ No newline at end of file
diff --git a/themes/bootstrap3/js/vudl/grid.js b/themes/bootstrap3/js/vudl/grid.js
deleted file mode 100644
index 7c88850176b7ab33f743257f8b35aac1a09b0273..0000000000000000000000000000000000000000
--- a/themes/bootstrap3/js/vudl/grid.js
+++ /dev/null
@@ -1,42 +0,0 @@
-/**
- * The page loading, recursive function
- */
-var loading_pages = true;
-function ajaxLoadPages(response) {
-  if(!loading_pages) return;
-  // If we got any pages (length is set in the controller)
-  if(response.data !== false && response.data.length > 0) {
-    // For each page
-    var trueStart = response.data.start < 0 ? 0 : response.data.start;
-    for(var i=0;i<response.data.length;i++) {
-      var page = response.data.outline[response.data.start];
-      if(page == undefined) continue;
-      $('.page-grid#item'+response.data.start)
-        .attr('href','../Item/'+page.id)
-        .attr('title',page.id)
-        .attr('id', 'item'+response.data.start)
-        .html('<div class="imgc"><img src="'+page.thumbnail+'"/></div>'+page.label)
-        .removeClass('loading')
-        .removeClass('onscreen');
-      response.data.start++;
-    }
-    // Call the next round of page loading
-    if(loading_pages) {
-      //console.log(trueStart,response.data.start-1);
-      var target = Math.min(counts[0], Math.floor($('#list0').scrollTop()/159));
-      $.ajax({
-        url:'../VuDL/ajax?method=pageAjax&record='+documentID+'&start='+response.data.start+'&end='+(response.data.start+response.data.length),
-        dataType:'json',
-        success :ajaxLoadPages,
-        error   :function(d,e){
-          console.log(d.responseText);
-          console.log(e);
-        }
-      });
-    }
-  // When we're done (no pages returned)
-  } else {
-    //console.log('done');
-    loading_pages = false;
-  }
-};
diff --git a/themes/bootstrap3/js/vudl/record.js b/themes/bootstrap3/js/vudl/record.js
deleted file mode 100644
index 0068175f37c20d1cf26898d69e536aeac29e9c33..0000000000000000000000000000000000000000
--- a/themes/bootstrap3/js/vudl/record.js
+++ /dev/null
@@ -1,191 +0,0 @@
-// ====== GET VIEWS ====== //
-var currentType = 'imaginary';
-var currentTab = 'medium-tab';
-var updateFunction;
-var currentID = false;
-var viewLoadAjax = false;
-function ajaxGetView(pageObject) {
-  pageObject['counts'] = counts;
-  if (currentTab == 'master-tab' && currentID == pageObject['id']) {
-    // Trigger file download
-    $('#file-download').submit();
-  } else if (currentType != pageObject['filetype']) {
-    if(viewLoadAjax) {
-      viewLoadAjax.abort();
-    }
-    viewLoadAjax = $.ajax({
-      type: 'POST',
-      url : '../VuDL/ajax?method=viewLoad',
-      data: pageObject,
-      dataType: 'json'
-    })
-    .done(function(e) {
-      $('#view').html(e.data);
-      currentType = pageObject['filetype'];
-      var tab = $('#'+currentTab, e.data);
-      if(tab.length > 0) {
-        tab.click();
-      } else {
-        currentTab = $('.nav-tabs li a:eq(0)')[0].id;
-      }
-    })
-    .fail(function(response, textStatus) {
-      console.log(response, textStatus);
-    });
-  } else {
-    updateFunction(pageObject, currentTab);
-  }
-  updateTechInfo(pageObject);
-  currentID = pageObject['id'];
-}
-function updateTechInfo(record) {
-  $.ajax({dataType:'json',
-    type:'post',
-    url:path+'/VuDL/ajax?method=getTechInfo',
-    data:record
-  })
-  .done(function(d) {
-    $('#techinfo').html(d.data.div);
-    var downloadSrc = 'MASTER';
-    if(typeof d.data.type !== "undefined") {
-      $('#download-button .details').html(d.data.type+' ~ '+d.data.size);
-    }
-    $('#file-download').attr('action', VuFind.path+'/files/'+record.id+'/'+downloadSrc+'?download=true');
-  })
-  .fail(function(response, textStatus) {
-    console.log(response, textStatus);
-  });
-}
-// ====== GET MORE THUMBNAILS ====== //
-var loadWait = false;
-// AJAX load all records flagged as on screen
-function findVisible() {
-  var min = -1,max;
-  // Flag pages on screen
-  $('.page-link.unloaded').each(function(index, item) {
-    var listID = '#collapse'+currentList;
-    if($(item).offset().top > $(listID).position().top-vudlSettings.scroll.top
-    && $(item).offset().top < $(listID).position().top+$(listID).height()+vudlSettings.scroll.bottom
-    && $(item).hasClass('unloaded')) {
-      $(item).addClass('loading');
-      max = parseInt($(item).attr('title'));
-      if(min < 0) min = max;
-    }
-  });
-  if(min > -1) {
-    ajaxLoadPages(min, max+2);
-  } else {
-    loadWait = false;
-  }
-}
-// AJAX Handling
-function ajaxLoadPages(min, max) {
-  //console.log('ajax', min, max, counts);
-  $.ajax({
-    url:path+'/VuDL/ajax?method=pageAjax&record='+documentID+'&start='+min+'&end='+max,
-    dataType:'json'
-  })
-  .done(function(response) {
-    loadWait = false;
-    // For each page
-    for(var i=0;i<response.data.length;i++) {
-      var page = response.data.outline[response.data.start];
-      if(page == undefined) continue;
-      var img = $('<img src="'+page.thumbnail+'"/>');
-      $('.page-link#item'+response.data.start)
-        .attr('onClick','ajaxGetView('+JSON.stringify(page).replace(/"/g, "'")+', this)')
-        .attr('title',page.id)
-        .attr('alt',page.label)
-        .attr('id', 'item'+response.data.start)
-        .html('<br/>'+page.label)
-        .prepend(img)
-        .addClass('active')
-        .removeClass('loading')
-        .removeClass('unloaded');
-      response.data.start++;
-    }
-    findVisible();
-  })
-  .fail(function(response, textStatus) {
-    console.log(response, textStatus);
-  });
-}
-// Pages
-function prevPage() {
-  $('.page-link.selected').prev('.page-link').click();
-  scrollToSelected();
-}
-function nextPage() {
-  $('.page-link.selected').next('.page-link').click();
-  scrollToSelected();
-}
-function scrollToSelected() {
-  var listID = '#collapse'+currentList;
-  if($(listID).length > 0 && $(listID+' .selected').length > 0) {
-    $(listID).finish();
-    scrollAnimation = $(listID).animate({
-      scrollTop: $(listID+' .selected').offset().top-$(listID).offset().top+$(listID).scrollTop()-12
-    });
-  }
-}
-// Toggle side menu
-function toggleSideNav() {
-  $('#side-nav').toggle();
-  var opener = $('#view .nav-tabs li.opener a');
-  opener.toggleClass('hidden');
-  $('#view').toggleClass('col-sm-9').toggleClass('col-sm-12');
-}
-
-function resizeElements() {
-  var $height = $(window).height() + window.scrollY - $('.panel-collapse.in').offset().top - 50;
-  $('.panel-collapse').css('max-height', Math.max(300, Math.min($height, $(window).height() - 200)));
-}
-
-// Ready? Let's go
-$(document).ready(function() {
-  $('.page-link').click(function() {
-    $('.page-link.selected').removeClass('selected');
-    $(this).addClass('selected');
-    var list = parseInt($(this).parents('.item-list').attr('list-index'));
-    if(counts[list] > 1) {
-      $('.sibling-form .turn-button').removeClass('hidden');
-    } else {
-      $('.sibling-form .turn-button').addClass('hidden');
-    }
-  });
-  // Load clicked items
-  $('.unloaded').click(function() {
-    scrollToSelected();
-    findVisible();
-  });
-  // Scroll Event
-  $('.item-list').parent().scroll(function() {
-    if(loadWait) return;
-    loadWait = true;
-    findVisible();
-  });
-  // Side nav toggle
-  $('#side-nav-toggle').click(toggleSideNav);
-  setTimeout(findVisible, 1000);
-  ajaxGetView(initPage);
-  // Track top item in the scroll bar
-  $('.accordion-body').scroll(function(e){
-    var pageLinks = $('.page-link');
-    for(var i=0;i<pageLinks.length;i++) {
-      if($(pageLinks[i]).position().top >= 0) {
-        topScrollItem = pageLinks[i].id;
-        break;
-      }
-    }
-  });
-  $('.panel-title a').click(function() {
-    if($(this).attr('href') == "#collapse_details") {
-      return;
-    }
-    currentList = parseInt($(this).attr('href').substring(9));
-  });
-  scrollToSelected();
-  resizeElements();
-  $( window ).resize( resizeElements );
-  $( window ).scroll( resizeElements );
-});
diff --git a/themes/bootstrap3/less/bootstrap.less b/themes/bootstrap3/less/bootstrap.less
index 0b1f504ecf4a507429d60c10a461259e208961c7..96cbf57419abe79f7b1773cf0e46e1bf6f2d65ad 100644
--- a/themes/bootstrap3/less/bootstrap.less
+++ b/themes/bootstrap3/less/bootstrap.less
@@ -1,7 +1,10 @@
 @import "vendor/bootstrap/bootstrap";
+@import "vendor/font-awesome/font-awesome";
 @import "vendor/bootstrap-accessibility/bootstrap-accessibility";
 @import "vendor/a11y";
 
+@fa-font-path: "../../../../../../themes/bootstrap3/css/fonts";
+
 @import "components/advanced-search";
 @import "components/alphabrowse";
 @import "components/autocomplete";
@@ -21,8 +24,7 @@
   &:focus,
   &:hover {color: #000;}
 }
-[data-toggle~="dropdown"] {cursor: pointer;}
-.fa {cursor: default;}
+header .dropdown form { display: none; }
 .list-unstyled {margin: 0;}
 .highlight,mark {
   background: lighten(#FF0, 20%);
@@ -99,7 +101,7 @@ img {max-width: 100%;}
 #dateVisColorSettings {
   background-color: #fff; // background of box
   fill: rgb(234,234,234); // fillColor
-  outline-color: #e8cfac; // selection color
+  outline-color: #c38835; // selection color
   stroke: @brand-primary; // color
 }
 
diff --git a/themes/bootstrap3/less/compiled.less b/themes/bootstrap3/less/compiled.less
index b0692d898a36e6b48de6304e7dca597a249904c6..cbd46a7afcf4100bb70bf9028c89faed85e46397 100644
--- a/themes/bootstrap3/less/compiled.less
+++ b/themes/bootstrap3/less/compiled.less
@@ -1 +1 @@
-@import "bootstrap";
\ No newline at end of file
+@import "bootstrap";
diff --git a/themes/bootstrap3/less/components/js-tree.less b/themes/bootstrap3/less/components/js-tree.less
index bcfe6ac8df349f81e7dfc50e1290aea9bb59f319..110cecb21cac4648f1cb1574df49d7d731b9d626 100644
--- a/themes/bootstrap3/less/components/js-tree.less
+++ b/themes/bootstrap3/less/components/js-tree.less
@@ -93,3 +93,7 @@ li.jstree-node {
 }
 li.jstree-facet .badge {cursor: text;}
 li.jstree-facet ul {padding-left: 20px;}
+
+.list-group.facet .jstree-node.list-group-item { padding-left: 19px; }
+.list-group.facet .jstree-icon::before { margin-left: -12px; }
+.list-group.facet .jstree-facet li span.main { padding: 1px; }
diff --git a/themes/bootstrap3/less/components/lightbox.less b/themes/bootstrap3/less/components/lightbox.less
index 5fa7cb1fdae6003ec10f4b39d712103e882bea50..e793dc0b3120e5a88790d11008ff95ab9c1c740e 100644
--- a/themes/bootstrap3/less/components/lightbox.less
+++ b/themes/bootstrap3/less/components/lightbox.less
@@ -1,3 +1,6 @@
+.lightbox-only { display: none; }
+#modal .lightbox-only { display: initial; }
+
 #modal {background-color: rgba(0,0,0,.2);}
 #modal .modal-content > .close {
   position: absolute;
@@ -17,4 +20,5 @@
 
 #modal .cart-controls .btn {margin-bottom: 4px;}
 #modal .cart-controls .checkbox {padding-bottom: 1em;}
-#modal .cart-controls ~ hr {margin-top: 0;}
\ No newline at end of file
+#modal .cart-controls ~ hr {margin-top: 0;}
+.lightbox-scroll { overflow-y: auto; }
diff --git a/themes/bootstrap3/less/components/record.less b/themes/bootstrap3/less/components/record.less
index ed967a1a5335de46f0c0977aafd8c1589db54f22..bb5af6a238c711c1a4560349574ec4f2694d6ce1 100644
--- a/themes/bootstrap3/less/components/record.less
+++ b/themes/bootstrap3/less/components/record.less
@@ -37,3 +37,16 @@
   color: #999;
   text-decoration: none;
 }
+
+.record .format::after { content: ", "; }
+.result .record .format::after,
+.record .format:last-child::after { content: ""; }
+
+.marc-row-LEADER,
+.marc-row-006,
+.marc-row-007,
+.marc-row-008 {
+  white-space: pre-wrap;
+}
+
+.comment-form textarea.form-control { display: block; }
diff --git a/themes/bootstrap3/less/components/search.less b/themes/bootstrap3/less/components/search.less
index 0cc861eb8e8989b419aff7da42ac143a59d17ae9..53b7ccc70c36f28bc741a747946543f3b566469d 100644
--- a/themes/bootstrap3/less/components/search.less
+++ b/themes/bootstrap3/less/components/search.less
@@ -1,24 +1,116 @@
+@thumbnail-width-small: 60px;
+@thumbnail-width-medium: 100px;
+@thumbnail-width-large: 160px;
+
 .bulkActionButtons label {display: inline-block;}
 .bulkActionButtons label input {margin-top: 2px;}
 .grid { @media (max-width: 767px) {min-height: 250px;} }
 .result {
-  a.title {font-weight: bold;}
-  .left {
+  padding-top: 1.5rem;
+  .checkbox {
+    float: left;
+    padding-right: .5rem;
+  }
+  .media,
+  .media-body { overflow: visible; }
+  .media-body .row {
+    margin-left: 0;
+    margin-right: 0;
+  }
+  .media {
+    margin-top: 0;
+    margin-bottom: 1rem;
+    .clearfix();
+  }
+  .media-left,
+  .media-right {
+    padding: 0;
     text-align: center;
-    img {max-width: 100%;}
+    a {
+      display: inline-block;
+      text-align: center;
+    }
+    img {
+      max-width: none;
+      max-height: 300px;
+    }
+    &.small a,
+    &.small > img {
+      width: @thumbnail-width-small;
+      img { max-width: @thumbnail-width-small; }
+    }
+    &.medium a,
+    &.medium > img {
+      width: @thumbnail-width-medium;
+      img { max-width: @thumbnail-width-medium; }
+    }
+    &.large a,
+    &.large > img {
+      width: @thumbnail-width-large;
+      img { max-width: @thumbnail-width-large; }
+    }
   }
+  .media-left { padding-left: 10px; }
+  .media-right { padding-right: 10px; }
+  .title {font-weight: bold;}
+  .list-tab-content.record .img-col { display: none; }
+  .list-tab-content.record .info-col { width: 100%; }
+
   @media (max-width: 767px) {
-    a {text-decoration: underline;}
-    .middle,
-    .right {padding: 0;}
+    a { text-decoration: underline; }
   }
   @media (max-width: 530px) {
-    .checkbox {display: none !important;}
-    .left {width: 40%;}
-    .middle {width: 60%;}
-    .right {display: none;}
+    .checkbox { padding: 0; }
+    .media-left { padding-right: 0; }
+  }
+
+  .format {
+    &:extend(.label);
+    &:extend(.label-info);
+  }
+}
+
+.result.embedded {
+  .getFull {
+    display: block;
+    margin-left: 1.5rem;
+    padding-right: 30px;
+    border-left: 1px solid transparent;
+  }
+  .getFull.expanded {
+    &:extend(.list-group-item);
+    margin-top: -11px;
+    margin-left: .75rem;
+    padding-left: .75rem;
+    border-top-left-radius: @border-radius-base;
+    border-top-right-radius: @border-radius-base;
+    &::before {
+      content: '\25BC';
+      position: absolute;
+      right: 15px;
+      color: @gray;
+    }
+  }
+  .loading {
+    &:extend(.list-group-item);
+    margin-left: .75rem;
+    padding: 1rem;
+    background: #fff;
   }
+  .long-view {
+    margin-left: .75rem;
+    padding: .5rem;
+    border: 1px solid @list-group-border;
+    background-color: #fff;
+    border-bottom-left-radius: @border-radius-base;
+    border-bottom-right-radius: @border-radius-base;
+  }
+  .long-view .tab-content { padding: 0; }
+  .list-tabs { margin-bottom: 0; }
+  .list-tab-toggle { cursor: pointer; }
+  .list-tab-content { padding: 1rem; }
 }
+
 .search-controls .alert {margin-bottom: 0;}
 .searchtools a {padding: 0 .5em;}
 .title-in-heading {
@@ -26,4 +118,9 @@
   font-style: italic;
 }
 
-.wikipedia img { margin-right: 1rem; }
\ No newline at end of file
+.wikipedia img { margin-right: 1rem; }
+
+.geoItem {
+    font-size: .9em;
+    margin: 0px 0px 10px;
+}
diff --git a/themes/bootstrap3/less/components/sidebar.less b/themes/bootstrap3/less/components/sidebar.less
index 5247cc7f0aad9cdcc2689298bf64ea47addde384..e3c1603ab03aea0c5419e2ac68063021594751ee 100644
--- a/themes/bootstrap3/less/components/sidebar.less
+++ b/themes/bootstrap3/less/components/sidebar.less
@@ -35,12 +35,11 @@ label.list-group-item {
   border-radius: 0;
 }
 .list-group-item.title {font-weight: bold;}
-.list-group-item, .badge {
-  i.fa {
-    cursor: inherit;
-  }
-}
 .sidebar .facet a {text-decoration: none;}
+.sidebar .format {
+  &:extend(.label);
+  &:extend(.label-info);
+}
 .top-row .applied {
   font-weight: bold;
   &:hover {
@@ -50,3 +49,5 @@ label.list-group-item {
     }
   }
 }
+
+.full-facet-list { margin-top: 1rem; }
diff --git a/themes/bootstrap3/less/vendor/a11y.less b/themes/bootstrap3/less/vendor/a11y.less
index 6b42697518a8d7a147a7941b2da067765382602e..97ca12ca7920ad4997b186981076e90eee668b21 100644
--- a/themes/bootstrap3/less/vendor/a11y.less
+++ b/themes/bootstrap3/less/vendor/a11y.less
@@ -49,7 +49,7 @@ input, textarea {
   &:focus,
   &:active,
   &.active,
-  .open .dropdown-toggle& {
+  .open &.dropdown-toggle {
     color: @background;
     background-color: @color;
     border-color: darken(@border, 12%);
diff --git a/themes/bootstrap3/less/vendor/bootstrap/bootstrap.less b/themes/bootstrap3/less/vendor/bootstrap/bootstrap.less
index 1c0477805fa93fef01ccb5d6ed0ef3bce3bd84df..fff1594ce014ad988748afb90adc53392704d972 100644
--- a/themes/bootstrap3/less/vendor/bootstrap/bootstrap.less
+++ b/themes/bootstrap3/less/vendor/bootstrap/bootstrap.less
@@ -1,6 +1,6 @@
 /*!
- * Bootstrap v3.3.6 (http://getbootstrap.com)
- * Copyright 2011-2015 Twitter, Inc.
+ * Bootstrap v3.3.7 (http://getbootstrap.com)
+ * Copyright 2011-2016 Twitter, Inc.
  * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
  */
 
@@ -11,7 +11,7 @@
 // Reset and dependencies
 @import "normalize.less";
 @import "print.less";
-@import "glyphicons.less";
+// @import "glyphicons.less"; VuFind custom removal
 
 // Core CSS
 @import "scaffolding.less";
diff --git a/themes/bootstrap3/less/vendor/bootstrap/button-groups.less b/themes/bootstrap3/less/vendor/bootstrap/button-groups.less
index 293245a6503a02498dc2eab8765612fe3414d1bf..16db0c6135e79744f43b578edc0b79a10f74267c 100644
--- a/themes/bootstrap3/less/vendor/bootstrap/button-groups.less
+++ b/themes/bootstrap3/less/vendor/bootstrap/button-groups.less
@@ -59,7 +59,7 @@
     .border-right-radius(0);
   }
 }
-// Need .dropdown-toggle since :last-child doesn't apply given a .dropdown-menu immediately after it
+// Need .dropdown-toggle since :last-child doesn't apply, given that a .dropdown-menu is used immediately after it
 .btn-group > .btn:last-child:not(:first-child),
 .btn-group > .dropdown-toggle:not(:first-child) {
   .border-left-radius(0);
diff --git a/themes/bootstrap3/less/vendor/bootstrap/forms.less b/themes/bootstrap3/less/vendor/bootstrap/forms.less
index e8b071a138ea294d2ff35ad0e8a92903a21ea190..9377d3846b9f46eac257ca6e4600598bd9e12f71 100644
--- a/themes/bootstrap3/less/vendor/bootstrap/forms.less
+++ b/themes/bootstrap3/less/vendor/bootstrap/forms.less
@@ -181,7 +181,7 @@ input[type="search"] {
 // set a pixel line-height that matches the given height of the input, but only
 // for Safari. See https://bugs.webkit.org/show_bug.cgi?id=139848
 //
-// Note that as of 8.3, iOS doesn't support `datetime` or `week`.
+// Note that as of 9.3, iOS doesn't support `week`.
 
 @media screen and (-webkit-min-device-pixel-ratio: 0) {
   input[type="date"],
diff --git a/themes/bootstrap3/less/vendor/bootstrap/input-groups.less b/themes/bootstrap3/less/vendor/bootstrap/input-groups.less
index 5f73eec40ca9511f86bb390e046e21560dd702e6..d0763db7fff5db46e0d3cf92e454f126d3c0c663 100644
--- a/themes/bootstrap3/less/vendor/bootstrap/input-groups.less
+++ b/themes/bootstrap3/less/vendor/bootstrap/input-groups.less
@@ -29,7 +29,7 @@
 
     width: 100%;
     margin-bottom: 0;
-    
+
     &:focus {
       z-index: 3;
     }
diff --git a/themes/bootstrap3/less/vendor/bootstrap/mixins/tab-focus.less b/themes/bootstrap3/less/vendor/bootstrap/mixins/tab-focus.less
index 1f1f05ab054412684539a94423c097d6cdadd8ba..d12d23629f52f2d8bfb332b62627eee22a953b2b 100644
--- a/themes/bootstrap3/less/vendor/bootstrap/mixins/tab-focus.less
+++ b/themes/bootstrap3/less/vendor/bootstrap/mixins/tab-focus.less
@@ -1,9 +1,9 @@
 // WebKit-style focus
 
 .tab-focus() {
-  // Default
-  outline: thin dotted;
-  // WebKit
+  // WebKit-specific. Other browsers will keep their default outline style.
+  // (Initially tried to also force default via `outline: initial`,
+  // but that seems to erroneously remove the outline in Firefox altogether.)
   outline: 5px auto -webkit-focus-ring-color;
   outline-offset: -2px;
 }
diff --git a/themes/bootstrap3/less/vendor/bootstrap/panels.less b/themes/bootstrap3/less/vendor/bootstrap/panels.less
index 425eb5e642c15df5279493e542ad29b9195b7f93..65aa3a83f34356b0cee5f2bdd61afa976172ea22 100644
--- a/themes/bootstrap3/less/vendor/bootstrap/panels.less
+++ b/themes/bootstrap3/less/vendor/bootstrap/panels.less
@@ -214,7 +214,7 @@
 }
 
 
-// Collapsable panels (aka, accordion)
+// Collapsible panels (aka, accordion)
 //
 // Wrap a series of panels in `.panel-group` to turn them into an accordion with
 // the help of our collapse JavaScript plugin.
diff --git a/themes/bootstrap3/less/vendor/bootstrap/scaffolding.less b/themes/bootstrap3/less/vendor/bootstrap/scaffolding.less
index 1929bfc5cfa686a66b6f21c20b66e31d1d708e67..64a29c6a5e7f20920a7ab41baefdf342830be881 100644
--- a/themes/bootstrap3/less/vendor/bootstrap/scaffolding.less
+++ b/themes/bootstrap3/less/vendor/bootstrap/scaffolding.less
@@ -120,7 +120,7 @@ hr {
 
 // Only display content to screen readers
 //
-// See: http://a11yproject.com/posts/how-to-hide-content/
+// See: http://a11yproject.com/posts/how-to-hide-content
 
 .sr-only {
   position: absolute;
diff --git a/themes/bootstrap3/less/vendor/bootstrap/theme.less b/themes/bootstrap3/less/vendor/bootstrap/theme.less
index 8f51d913dc9db0990c2f24ce0d0305f9ec9478ba..fb6174427ba296a62cc334a24a618dcac0700505 100644
--- a/themes/bootstrap3/less/vendor/bootstrap/theme.less
+++ b/themes/bootstrap3/less/vendor/bootstrap/theme.less
@@ -1,6 +1,6 @@
 /*!
- * Bootstrap v3.3.6 (http://getbootstrap.com)
- * Copyright 2011-2015 Twitter, Inc.
+ * Bootstrap v3.3.7 (http://getbootstrap.com)
+ * Copyright 2011-2016 Twitter, Inc.
  * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
  */
 
diff --git a/themes/bootstrap3/less/vendor/bootstrap/variables.less b/themes/bootstrap3/less/vendor/bootstrap/variables.less
index b057ef5bf907b8373a9308553c1924f8a3668881..03b54980ae2ab00a8c6180dd709deb361697adec 100644
--- a/themes/bootstrap3/less/vendor/bootstrap/variables.less
+++ b/themes/bootstrap3/less/vendor/bootstrap/variables.less
@@ -111,7 +111,7 @@
 //** Global background color for active items (e.g., navs or dropdowns).
 @component-active-bg:       @brand-primary;
 
-//** Width of the `border` for generating carets that indicator dropdowns.
+//** Width of the `border` for generating carets that indicate dropdowns.
 @caret-width-base:          4px;
 //** Carets increase slightly in size for larger components.
 @caret-width-large:         5px;
diff --git a/themes/bootstrap3/less/vendor/font-awesome/animated.less b/themes/bootstrap3/less/vendor/font-awesome/animated.less
new file mode 100644
index 0000000000000000000000000000000000000000..66ad52a5ba052ced428dcc7ec7016bc25438a2ca
--- /dev/null
+++ b/themes/bootstrap3/less/vendor/font-awesome/animated.less
@@ -0,0 +1,34 @@
+// Animated Icons
+// --------------------------
+
+.@{fa-css-prefix}-spin {
+  -webkit-animation: fa-spin 2s infinite linear;
+          animation: fa-spin 2s infinite linear;
+}
+
+.@{fa-css-prefix}-pulse {
+  -webkit-animation: fa-spin 1s infinite steps(8);
+          animation: fa-spin 1s infinite steps(8);
+}
+
+@-webkit-keyframes fa-spin {
+  0% {
+    -webkit-transform: rotate(0deg);
+            transform: rotate(0deg);
+  }
+  100% {
+    -webkit-transform: rotate(359deg);
+            transform: rotate(359deg);
+  }
+}
+
+@keyframes fa-spin {
+  0% {
+    -webkit-transform: rotate(0deg);
+            transform: rotate(0deg);
+  }
+  100% {
+    -webkit-transform: rotate(359deg);
+            transform: rotate(359deg);
+  }
+}
diff --git a/themes/bootstrap3/less/vendor/font-awesome/bordered-pulled.less b/themes/bootstrap3/less/vendor/font-awesome/bordered-pulled.less
new file mode 100644
index 0000000000000000000000000000000000000000..f1c8ad75f52bbc6c84a6dc3a341fe39e8d3edfd4
--- /dev/null
+++ b/themes/bootstrap3/less/vendor/font-awesome/bordered-pulled.less
@@ -0,0 +1,25 @@
+// Bordered & Pulled
+// -------------------------
+
+.@{fa-css-prefix}-border {
+  padding: .2em .25em .15em;
+  border: solid .08em @fa-border-color;
+  border-radius: .1em;
+}
+
+.@{fa-css-prefix}-pull-left { float: left; }
+.@{fa-css-prefix}-pull-right { float: right; }
+
+.@{fa-css-prefix} {
+  &.@{fa-css-prefix}-pull-left { margin-right: .3em; }
+  &.@{fa-css-prefix}-pull-right { margin-left: .3em; }
+}
+
+/* Deprecated as of 4.4.0 */
+.pull-right { float: right; }
+.pull-left { float: left; }
+
+.@{fa-css-prefix} {
+  &.pull-left { margin-right: .3em; }
+  &.pull-right { margin-left: .3em; }
+}
diff --git a/themes/bootstrap3/less/vendor/font-awesome/core.less b/themes/bootstrap3/less/vendor/font-awesome/core.less
new file mode 100644
index 0000000000000000000000000000000000000000..c577ac84a6a55ef39ff7f4e25d60c3977a902ed1
--- /dev/null
+++ b/themes/bootstrap3/less/vendor/font-awesome/core.less
@@ -0,0 +1,12 @@
+// Base Class Definition
+// -------------------------
+
+.@{fa-css-prefix} {
+  display: inline-block;
+  font: normal normal normal @fa-font-size-base/@fa-line-height-base FontAwesome; // shortening font declaration
+  font-size: inherit; // can't have font-size inherit on line above, so need to override
+  text-rendering: auto; // optimizelegibility throws things off #1094
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+
+}
diff --git a/themes/bootstrap3/less/vendor/font-awesome/fixed-width.less b/themes/bootstrap3/less/vendor/font-awesome/fixed-width.less
new file mode 100644
index 0000000000000000000000000000000000000000..110289f2f4b5260ce04dd035408961f1e4f2785b
--- /dev/null
+++ b/themes/bootstrap3/less/vendor/font-awesome/fixed-width.less
@@ -0,0 +1,6 @@
+// Fixed Width Icons
+// -------------------------
+.@{fa-css-prefix}-fw {
+  width: (18em / 14);
+  text-align: center;
+}
diff --git a/themes/bootstrap3/less/vendor/font-awesome/font-awesome.less b/themes/bootstrap3/less/vendor/font-awesome/font-awesome.less
new file mode 100644
index 0000000000000000000000000000000000000000..c44e5f466a0de31751b647118a55dd563478815f
--- /dev/null
+++ b/themes/bootstrap3/less/vendor/font-awesome/font-awesome.less
@@ -0,0 +1,18 @@
+/*!
+ *  Font Awesome 4.6.3 by @davegandy - http://fontawesome.io - @fontawesome
+ *  License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License)
+ */
+
+@import "variables.less";
+@import "mixins.less";
+@import "path.less";
+@import "core.less";
+@import "larger.less";
+@import "fixed-width.less";
+@import "list.less";
+@import "bordered-pulled.less";
+@import "animated.less";
+@import "rotated-flipped.less";
+@import "stacked.less";
+@import "icons.less";
+@import "screen-reader.less";
diff --git a/themes/bootstrap3/less/vendor/font-awesome/icons.less b/themes/bootstrap3/less/vendor/font-awesome/icons.less
new file mode 100644
index 0000000000000000000000000000000000000000..ba21b222d6ff0e0f604d99acf34dca0ce2bd9066
--- /dev/null
+++ b/themes/bootstrap3/less/vendor/font-awesome/icons.less
@@ -0,0 +1,733 @@
+/* Font Awesome uses the Unicode Private Use Area (PUA) to ensure screen
+   readers do not read off random characters that represent icons */
+
+.@{fa-css-prefix}-glass:before { content: @fa-var-glass; }
+.@{fa-css-prefix}-music:before { content: @fa-var-music; }
+.@{fa-css-prefix}-search:before { content: @fa-var-search; }
+.@{fa-css-prefix}-envelope-o:before { content: @fa-var-envelope-o; }
+.@{fa-css-prefix}-heart:before { content: @fa-var-heart; }
+.@{fa-css-prefix}-star:before { content: @fa-var-star; }
+.@{fa-css-prefix}-star-o:before { content: @fa-var-star-o; }
+.@{fa-css-prefix}-user:before { content: @fa-var-user; }
+.@{fa-css-prefix}-film:before { content: @fa-var-film; }
+.@{fa-css-prefix}-th-large:before { content: @fa-var-th-large; }
+.@{fa-css-prefix}-th:before { content: @fa-var-th; }
+.@{fa-css-prefix}-th-list:before { content: @fa-var-th-list; }
+.@{fa-css-prefix}-check:before { content: @fa-var-check; }
+.@{fa-css-prefix}-remove:before,
+.@{fa-css-prefix}-close:before,
+.@{fa-css-prefix}-times:before { content: @fa-var-times; }
+.@{fa-css-prefix}-search-plus:before { content: @fa-var-search-plus; }
+.@{fa-css-prefix}-search-minus:before { content: @fa-var-search-minus; }
+.@{fa-css-prefix}-power-off:before { content: @fa-var-power-off; }
+.@{fa-css-prefix}-signal:before { content: @fa-var-signal; }
+.@{fa-css-prefix}-gear:before,
+.@{fa-css-prefix}-cog:before { content: @fa-var-cog; }
+.@{fa-css-prefix}-trash-o:before { content: @fa-var-trash-o; }
+.@{fa-css-prefix}-home:before { content: @fa-var-home; }
+.@{fa-css-prefix}-file-o:before { content: @fa-var-file-o; }
+.@{fa-css-prefix}-clock-o:before { content: @fa-var-clock-o; }
+.@{fa-css-prefix}-road:before { content: @fa-var-road; }
+.@{fa-css-prefix}-download:before { content: @fa-var-download; }
+.@{fa-css-prefix}-arrow-circle-o-down:before { content: @fa-var-arrow-circle-o-down; }
+.@{fa-css-prefix}-arrow-circle-o-up:before { content: @fa-var-arrow-circle-o-up; }
+.@{fa-css-prefix}-inbox:before { content: @fa-var-inbox; }
+.@{fa-css-prefix}-play-circle-o:before { content: @fa-var-play-circle-o; }
+.@{fa-css-prefix}-rotate-right:before,
+.@{fa-css-prefix}-repeat:before { content: @fa-var-repeat; }
+.@{fa-css-prefix}-refresh:before { content: @fa-var-refresh; }
+.@{fa-css-prefix}-list-alt:before { content: @fa-var-list-alt; }
+.@{fa-css-prefix}-lock:before { content: @fa-var-lock; }
+.@{fa-css-prefix}-flag:before { content: @fa-var-flag; }
+.@{fa-css-prefix}-headphones:before { content: @fa-var-headphones; }
+.@{fa-css-prefix}-volume-off:before { content: @fa-var-volume-off; }
+.@{fa-css-prefix}-volume-down:before { content: @fa-var-volume-down; }
+.@{fa-css-prefix}-volume-up:before { content: @fa-var-volume-up; }
+.@{fa-css-prefix}-qrcode:before { content: @fa-var-qrcode; }
+.@{fa-css-prefix}-barcode:before { content: @fa-var-barcode; }
+.@{fa-css-prefix}-tag:before { content: @fa-var-tag; }
+.@{fa-css-prefix}-tags:before { content: @fa-var-tags; }
+.@{fa-css-prefix}-book:before { content: @fa-var-book; }
+.@{fa-css-prefix}-bookmark:before { content: @fa-var-bookmark; }
+.@{fa-css-prefix}-print:before { content: @fa-var-print; }
+.@{fa-css-prefix}-camera:before { content: @fa-var-camera; }
+.@{fa-css-prefix}-font:before { content: @fa-var-font; }
+.@{fa-css-prefix}-bold:before { content: @fa-var-bold; }
+.@{fa-css-prefix}-italic:before { content: @fa-var-italic; }
+.@{fa-css-prefix}-text-height:before { content: @fa-var-text-height; }
+.@{fa-css-prefix}-text-width:before { content: @fa-var-text-width; }
+.@{fa-css-prefix}-align-left:before { content: @fa-var-align-left; }
+.@{fa-css-prefix}-align-center:before { content: @fa-var-align-center; }
+.@{fa-css-prefix}-align-right:before { content: @fa-var-align-right; }
+.@{fa-css-prefix}-align-justify:before { content: @fa-var-align-justify; }
+.@{fa-css-prefix}-list:before { content: @fa-var-list; }
+.@{fa-css-prefix}-dedent:before,
+.@{fa-css-prefix}-outdent:before { content: @fa-var-outdent; }
+.@{fa-css-prefix}-indent:before { content: @fa-var-indent; }
+.@{fa-css-prefix}-video-camera:before { content: @fa-var-video-camera; }
+.@{fa-css-prefix}-photo:before,
+.@{fa-css-prefix}-image:before,
+.@{fa-css-prefix}-picture-o:before { content: @fa-var-picture-o; }
+.@{fa-css-prefix}-pencil:before { content: @fa-var-pencil; }
+.@{fa-css-prefix}-map-marker:before { content: @fa-var-map-marker; }
+.@{fa-css-prefix}-adjust:before { content: @fa-var-adjust; }
+.@{fa-css-prefix}-tint:before { content: @fa-var-tint; }
+.@{fa-css-prefix}-edit:before,
+.@{fa-css-prefix}-pencil-square-o:before { content: @fa-var-pencil-square-o; }
+.@{fa-css-prefix}-share-square-o:before { content: @fa-var-share-square-o; }
+.@{fa-css-prefix}-check-square-o:before { content: @fa-var-check-square-o; }
+.@{fa-css-prefix}-arrows:before { content: @fa-var-arrows; }
+.@{fa-css-prefix}-step-backward:before { content: @fa-var-step-backward; }
+.@{fa-css-prefix}-fast-backward:before { content: @fa-var-fast-backward; }
+.@{fa-css-prefix}-backward:before { content: @fa-var-backward; }
+.@{fa-css-prefix}-play:before { content: @fa-var-play; }
+.@{fa-css-prefix}-pause:before { content: @fa-var-pause; }
+.@{fa-css-prefix}-stop:before { content: @fa-var-stop; }
+.@{fa-css-prefix}-forward:before { content: @fa-var-forward; }
+.@{fa-css-prefix}-fast-forward:before { content: @fa-var-fast-forward; }
+.@{fa-css-prefix}-step-forward:before { content: @fa-var-step-forward; }
+.@{fa-css-prefix}-eject:before { content: @fa-var-eject; }
+.@{fa-css-prefix}-chevron-left:before { content: @fa-var-chevron-left; }
+.@{fa-css-prefix}-chevron-right:before { content: @fa-var-chevron-right; }
+.@{fa-css-prefix}-plus-circle:before { content: @fa-var-plus-circle; }
+.@{fa-css-prefix}-minus-circle:before { content: @fa-var-minus-circle; }
+.@{fa-css-prefix}-times-circle:before { content: @fa-var-times-circle; }
+.@{fa-css-prefix}-check-circle:before { content: @fa-var-check-circle; }
+.@{fa-css-prefix}-question-circle:before { content: @fa-var-question-circle; }
+.@{fa-css-prefix}-info-circle:before { content: @fa-var-info-circle; }
+.@{fa-css-prefix}-crosshairs:before { content: @fa-var-crosshairs; }
+.@{fa-css-prefix}-times-circle-o:before { content: @fa-var-times-circle-o; }
+.@{fa-css-prefix}-check-circle-o:before { content: @fa-var-check-circle-o; }
+.@{fa-css-prefix}-ban:before { content: @fa-var-ban; }
+.@{fa-css-prefix}-arrow-left:before { content: @fa-var-arrow-left; }
+.@{fa-css-prefix}-arrow-right:before { content: @fa-var-arrow-right; }
+.@{fa-css-prefix}-arrow-up:before { content: @fa-var-arrow-up; }
+.@{fa-css-prefix}-arrow-down:before { content: @fa-var-arrow-down; }
+.@{fa-css-prefix}-mail-forward:before,
+.@{fa-css-prefix}-share:before { content: @fa-var-share; }
+.@{fa-css-prefix}-expand:before { content: @fa-var-expand; }
+.@{fa-css-prefix}-compress:before { content: @fa-var-compress; }
+.@{fa-css-prefix}-plus:before { content: @fa-var-plus; }
+.@{fa-css-prefix}-minus:before { content: @fa-var-minus; }
+.@{fa-css-prefix}-asterisk:before { content: @fa-var-asterisk; }
+.@{fa-css-prefix}-exclamation-circle:before { content: @fa-var-exclamation-circle; }
+.@{fa-css-prefix}-gift:before { content: @fa-var-gift; }
+.@{fa-css-prefix}-leaf:before { content: @fa-var-leaf; }
+.@{fa-css-prefix}-fire:before { content: @fa-var-fire; }
+.@{fa-css-prefix}-eye:before { content: @fa-var-eye; }
+.@{fa-css-prefix}-eye-slash:before { content: @fa-var-eye-slash; }
+.@{fa-css-prefix}-warning:before,
+.@{fa-css-prefix}-exclamation-triangle:before { content: @fa-var-exclamation-triangle; }
+.@{fa-css-prefix}-plane:before { content: @fa-var-plane; }
+.@{fa-css-prefix}-calendar:before { content: @fa-var-calendar; }
+.@{fa-css-prefix}-random:before { content: @fa-var-random; }
+.@{fa-css-prefix}-comment:before { content: @fa-var-comment; }
+.@{fa-css-prefix}-magnet:before { content: @fa-var-magnet; }
+.@{fa-css-prefix}-chevron-up:before { content: @fa-var-chevron-up; }
+.@{fa-css-prefix}-chevron-down:before { content: @fa-var-chevron-down; }
+.@{fa-css-prefix}-retweet:before { content: @fa-var-retweet; }
+.@{fa-css-prefix}-shopping-cart:before { content: @fa-var-shopping-cart; }
+.@{fa-css-prefix}-folder:before { content: @fa-var-folder; }
+.@{fa-css-prefix}-folder-open:before { content: @fa-var-folder-open; }
+.@{fa-css-prefix}-arrows-v:before { content: @fa-var-arrows-v; }
+.@{fa-css-prefix}-arrows-h:before { content: @fa-var-arrows-h; }
+.@{fa-css-prefix}-bar-chart-o:before,
+.@{fa-css-prefix}-bar-chart:before { content: @fa-var-bar-chart; }
+.@{fa-css-prefix}-twitter-square:before { content: @fa-var-twitter-square; }
+.@{fa-css-prefix}-facebook-square:before { content: @fa-var-facebook-square; }
+.@{fa-css-prefix}-camera-retro:before { content: @fa-var-camera-retro; }
+.@{fa-css-prefix}-key:before { content: @fa-var-key; }
+.@{fa-css-prefix}-gears:before,
+.@{fa-css-prefix}-cogs:before { content: @fa-var-cogs; }
+.@{fa-css-prefix}-comments:before { content: @fa-var-comments; }
+.@{fa-css-prefix}-thumbs-o-up:before { content: @fa-var-thumbs-o-up; }
+.@{fa-css-prefix}-thumbs-o-down:before { content: @fa-var-thumbs-o-down; }
+.@{fa-css-prefix}-star-half:before { content: @fa-var-star-half; }
+.@{fa-css-prefix}-heart-o:before { content: @fa-var-heart-o; }
+.@{fa-css-prefix}-sign-out:before { content: @fa-var-sign-out; }
+.@{fa-css-prefix}-linkedin-square:before { content: @fa-var-linkedin-square; }
+.@{fa-css-prefix}-thumb-tack:before { content: @fa-var-thumb-tack; }
+.@{fa-css-prefix}-external-link:before { content: @fa-var-external-link; }
+.@{fa-css-prefix}-sign-in:before { content: @fa-var-sign-in; }
+.@{fa-css-prefix}-trophy:before { content: @fa-var-trophy; }
+.@{fa-css-prefix}-github-square:before { content: @fa-var-github-square; }
+.@{fa-css-prefix}-upload:before { content: @fa-var-upload; }
+.@{fa-css-prefix}-lemon-o:before { content: @fa-var-lemon-o; }
+.@{fa-css-prefix}-phone:before { content: @fa-var-phone; }
+.@{fa-css-prefix}-square-o:before { content: @fa-var-square-o; }
+.@{fa-css-prefix}-bookmark-o:before { content: @fa-var-bookmark-o; }
+.@{fa-css-prefix}-phone-square:before { content: @fa-var-phone-square; }
+.@{fa-css-prefix}-twitter:before { content: @fa-var-twitter; }
+.@{fa-css-prefix}-facebook-f:before,
+.@{fa-css-prefix}-facebook:before { content: @fa-var-facebook; }
+.@{fa-css-prefix}-github:before { content: @fa-var-github; }
+.@{fa-css-prefix}-unlock:before { content: @fa-var-unlock; }
+.@{fa-css-prefix}-credit-card:before { content: @fa-var-credit-card; }
+.@{fa-css-prefix}-feed:before,
+.@{fa-css-prefix}-rss:before { content: @fa-var-rss; }
+.@{fa-css-prefix}-hdd-o:before { content: @fa-var-hdd-o; }
+.@{fa-css-prefix}-bullhorn:before { content: @fa-var-bullhorn; }
+.@{fa-css-prefix}-bell:before { content: @fa-var-bell; }
+.@{fa-css-prefix}-certificate:before { content: @fa-var-certificate; }
+.@{fa-css-prefix}-hand-o-right:before { content: @fa-var-hand-o-right; }
+.@{fa-css-prefix}-hand-o-left:before { content: @fa-var-hand-o-left; }
+.@{fa-css-prefix}-hand-o-up:before { content: @fa-var-hand-o-up; }
+.@{fa-css-prefix}-hand-o-down:before { content: @fa-var-hand-o-down; }
+.@{fa-css-prefix}-arrow-circle-left:before { content: @fa-var-arrow-circle-left; }
+.@{fa-css-prefix}-arrow-circle-right:before { content: @fa-var-arrow-circle-right; }
+.@{fa-css-prefix}-arrow-circle-up:before { content: @fa-var-arrow-circle-up; }
+.@{fa-css-prefix}-arrow-circle-down:before { content: @fa-var-arrow-circle-down; }
+.@{fa-css-prefix}-globe:before { content: @fa-var-globe; }
+.@{fa-css-prefix}-wrench:before { content: @fa-var-wrench; }
+.@{fa-css-prefix}-tasks:before { content: @fa-var-tasks; }
+.@{fa-css-prefix}-filter:before { content: @fa-var-filter; }
+.@{fa-css-prefix}-briefcase:before { content: @fa-var-briefcase; }
+.@{fa-css-prefix}-arrows-alt:before { content: @fa-var-arrows-alt; }
+.@{fa-css-prefix}-group:before,
+.@{fa-css-prefix}-users:before { content: @fa-var-users; }
+.@{fa-css-prefix}-chain:before,
+.@{fa-css-prefix}-link:before { content: @fa-var-link; }
+.@{fa-css-prefix}-cloud:before { content: @fa-var-cloud; }
+.@{fa-css-prefix}-flask:before { content: @fa-var-flask; }
+.@{fa-css-prefix}-cut:before,
+.@{fa-css-prefix}-scissors:before { content: @fa-var-scissors; }
+.@{fa-css-prefix}-copy:before,
+.@{fa-css-prefix}-files-o:before { content: @fa-var-files-o; }
+.@{fa-css-prefix}-paperclip:before { content: @fa-var-paperclip; }
+.@{fa-css-prefix}-save:before,
+.@{fa-css-prefix}-floppy-o:before { content: @fa-var-floppy-o; }
+.@{fa-css-prefix}-square:before { content: @fa-var-square; }
+.@{fa-css-prefix}-navicon:before,
+.@{fa-css-prefix}-reorder:before,
+.@{fa-css-prefix}-bars:before { content: @fa-var-bars; }
+.@{fa-css-prefix}-list-ul:before { content: @fa-var-list-ul; }
+.@{fa-css-prefix}-list-ol:before { content: @fa-var-list-ol; }
+.@{fa-css-prefix}-strikethrough:before { content: @fa-var-strikethrough; }
+.@{fa-css-prefix}-underline:before { content: @fa-var-underline; }
+.@{fa-css-prefix}-table:before { content: @fa-var-table; }
+.@{fa-css-prefix}-magic:before { content: @fa-var-magic; }
+.@{fa-css-prefix}-truck:before { content: @fa-var-truck; }
+.@{fa-css-prefix}-pinterest:before { content: @fa-var-pinterest; }
+.@{fa-css-prefix}-pinterest-square:before { content: @fa-var-pinterest-square; }
+.@{fa-css-prefix}-google-plus-square:before { content: @fa-var-google-plus-square; }
+.@{fa-css-prefix}-google-plus:before { content: @fa-var-google-plus; }
+.@{fa-css-prefix}-money:before { content: @fa-var-money; }
+.@{fa-css-prefix}-caret-down:before { content: @fa-var-caret-down; }
+.@{fa-css-prefix}-caret-up:before { content: @fa-var-caret-up; }
+.@{fa-css-prefix}-caret-left:before { content: @fa-var-caret-left; }
+.@{fa-css-prefix}-caret-right:before { content: @fa-var-caret-right; }
+.@{fa-css-prefix}-columns:before { content: @fa-var-columns; }
+.@{fa-css-prefix}-unsorted:before,
+.@{fa-css-prefix}-sort:before { content: @fa-var-sort; }
+.@{fa-css-prefix}-sort-down:before,
+.@{fa-css-prefix}-sort-desc:before { content: @fa-var-sort-desc; }
+.@{fa-css-prefix}-sort-up:before,
+.@{fa-css-prefix}-sort-asc:before { content: @fa-var-sort-asc; }
+.@{fa-css-prefix}-envelope:before { content: @fa-var-envelope; }
+.@{fa-css-prefix}-linkedin:before { content: @fa-var-linkedin; }
+.@{fa-css-prefix}-rotate-left:before,
+.@{fa-css-prefix}-undo:before { content: @fa-var-undo; }
+.@{fa-css-prefix}-legal:before,
+.@{fa-css-prefix}-gavel:before { content: @fa-var-gavel; }
+.@{fa-css-prefix}-dashboard:before,
+.@{fa-css-prefix}-tachometer:before { content: @fa-var-tachometer; }
+.@{fa-css-prefix}-comment-o:before { content: @fa-var-comment-o; }
+.@{fa-css-prefix}-comments-o:before { content: @fa-var-comments-o; }
+.@{fa-css-prefix}-flash:before,
+.@{fa-css-prefix}-bolt:before { content: @fa-var-bolt; }
+.@{fa-css-prefix}-sitemap:before { content: @fa-var-sitemap; }
+.@{fa-css-prefix}-umbrella:before { content: @fa-var-umbrella; }
+.@{fa-css-prefix}-paste:before,
+.@{fa-css-prefix}-clipboard:before { content: @fa-var-clipboard; }
+.@{fa-css-prefix}-lightbulb-o:before { content: @fa-var-lightbulb-o; }
+.@{fa-css-prefix}-exchange:before { content: @fa-var-exchange; }
+.@{fa-css-prefix}-cloud-download:before { content: @fa-var-cloud-download; }
+.@{fa-css-prefix}-cloud-upload:before { content: @fa-var-cloud-upload; }
+.@{fa-css-prefix}-user-md:before { content: @fa-var-user-md; }
+.@{fa-css-prefix}-stethoscope:before { content: @fa-var-stethoscope; }
+.@{fa-css-prefix}-suitcase:before { content: @fa-var-suitcase; }
+.@{fa-css-prefix}-bell-o:before { content: @fa-var-bell-o; }
+.@{fa-css-prefix}-coffee:before { content: @fa-var-coffee; }
+.@{fa-css-prefix}-cutlery:before { content: @fa-var-cutlery; }
+.@{fa-css-prefix}-file-text-o:before { content: @fa-var-file-text-o; }
+.@{fa-css-prefix}-building-o:before { content: @fa-var-building-o; }
+.@{fa-css-prefix}-hospital-o:before { content: @fa-var-hospital-o; }
+.@{fa-css-prefix}-ambulance:before { content: @fa-var-ambulance; }
+.@{fa-css-prefix}-medkit:before { content: @fa-var-medkit; }
+.@{fa-css-prefix}-fighter-jet:before { content: @fa-var-fighter-jet; }
+.@{fa-css-prefix}-beer:before { content: @fa-var-beer; }
+.@{fa-css-prefix}-h-square:before { content: @fa-var-h-square; }
+.@{fa-css-prefix}-plus-square:before { content: @fa-var-plus-square; }
+.@{fa-css-prefix}-angle-double-left:before { content: @fa-var-angle-double-left; }
+.@{fa-css-prefix}-angle-double-right:before { content: @fa-var-angle-double-right; }
+.@{fa-css-prefix}-angle-double-up:before { content: @fa-var-angle-double-up; }
+.@{fa-css-prefix}-angle-double-down:before { content: @fa-var-angle-double-down; }
+.@{fa-css-prefix}-angle-left:before { content: @fa-var-angle-left; }
+.@{fa-css-prefix}-angle-right:before { content: @fa-var-angle-right; }
+.@{fa-css-prefix}-angle-up:before { content: @fa-var-angle-up; }
+.@{fa-css-prefix}-angle-down:before { content: @fa-var-angle-down; }
+.@{fa-css-prefix}-desktop:before { content: @fa-var-desktop; }
+.@{fa-css-prefix}-laptop:before { content: @fa-var-laptop; }
+.@{fa-css-prefix}-tablet:before { content: @fa-var-tablet; }
+.@{fa-css-prefix}-mobile-phone:before,
+.@{fa-css-prefix}-mobile:before { content: @fa-var-mobile; }
+.@{fa-css-prefix}-circle-o:before { content: @fa-var-circle-o; }
+.@{fa-css-prefix}-quote-left:before { content: @fa-var-quote-left; }
+.@{fa-css-prefix}-quote-right:before { content: @fa-var-quote-right; }
+.@{fa-css-prefix}-spinner:before { content: @fa-var-spinner; }
+.@{fa-css-prefix}-circle:before { content: @fa-var-circle; }
+.@{fa-css-prefix}-mail-reply:before,
+.@{fa-css-prefix}-reply:before { content: @fa-var-reply; }
+.@{fa-css-prefix}-github-alt:before { content: @fa-var-github-alt; }
+.@{fa-css-prefix}-folder-o:before { content: @fa-var-folder-o; }
+.@{fa-css-prefix}-folder-open-o:before { content: @fa-var-folder-open-o; }
+.@{fa-css-prefix}-smile-o:before { content: @fa-var-smile-o; }
+.@{fa-css-prefix}-frown-o:before { content: @fa-var-frown-o; }
+.@{fa-css-prefix}-meh-o:before { content: @fa-var-meh-o; }
+.@{fa-css-prefix}-gamepad:before { content: @fa-var-gamepad; }
+.@{fa-css-prefix}-keyboard-o:before { content: @fa-var-keyboard-o; }
+.@{fa-css-prefix}-flag-o:before { content: @fa-var-flag-o; }
+.@{fa-css-prefix}-flag-checkered:before { content: @fa-var-flag-checkered; }
+.@{fa-css-prefix}-terminal:before { content: @fa-var-terminal; }
+.@{fa-css-prefix}-code:before { content: @fa-var-code; }
+.@{fa-css-prefix}-mail-reply-all:before,
+.@{fa-css-prefix}-reply-all:before { content: @fa-var-reply-all; }
+.@{fa-css-prefix}-star-half-empty:before,
+.@{fa-css-prefix}-star-half-full:before,
+.@{fa-css-prefix}-star-half-o:before { content: @fa-var-star-half-o; }
+.@{fa-css-prefix}-location-arrow:before { content: @fa-var-location-arrow; }
+.@{fa-css-prefix}-crop:before { content: @fa-var-crop; }
+.@{fa-css-prefix}-code-fork:before { content: @fa-var-code-fork; }
+.@{fa-css-prefix}-unlink:before,
+.@{fa-css-prefix}-chain-broken:before { content: @fa-var-chain-broken; }
+.@{fa-css-prefix}-question:before { content: @fa-var-question; }
+.@{fa-css-prefix}-info:before { content: @fa-var-info; }
+.@{fa-css-prefix}-exclamation:before { content: @fa-var-exclamation; }
+.@{fa-css-prefix}-superscript:before { content: @fa-var-superscript; }
+.@{fa-css-prefix}-subscript:before { content: @fa-var-subscript; }
+.@{fa-css-prefix}-eraser:before { content: @fa-var-eraser; }
+.@{fa-css-prefix}-puzzle-piece:before { content: @fa-var-puzzle-piece; }
+.@{fa-css-prefix}-microphone:before { content: @fa-var-microphone; }
+.@{fa-css-prefix}-microphone-slash:before { content: @fa-var-microphone-slash; }
+.@{fa-css-prefix}-shield:before { content: @fa-var-shield; }
+.@{fa-css-prefix}-calendar-o:before { content: @fa-var-calendar-o; }
+.@{fa-css-prefix}-fire-extinguisher:before { content: @fa-var-fire-extinguisher; }
+.@{fa-css-prefix}-rocket:before { content: @fa-var-rocket; }
+.@{fa-css-prefix}-maxcdn:before { content: @fa-var-maxcdn; }
+.@{fa-css-prefix}-chevron-circle-left:before { content: @fa-var-chevron-circle-left; }
+.@{fa-css-prefix}-chevron-circle-right:before { content: @fa-var-chevron-circle-right; }
+.@{fa-css-prefix}-chevron-circle-up:before { content: @fa-var-chevron-circle-up; }
+.@{fa-css-prefix}-chevron-circle-down:before { content: @fa-var-chevron-circle-down; }
+.@{fa-css-prefix}-html5:before { content: @fa-var-html5; }
+.@{fa-css-prefix}-css3:before { content: @fa-var-css3; }
+.@{fa-css-prefix}-anchor:before { content: @fa-var-anchor; }
+.@{fa-css-prefix}-unlock-alt:before { content: @fa-var-unlock-alt; }
+.@{fa-css-prefix}-bullseye:before { content: @fa-var-bullseye; }
+.@{fa-css-prefix}-ellipsis-h:before { content: @fa-var-ellipsis-h; }
+.@{fa-css-prefix}-ellipsis-v:before { content: @fa-var-ellipsis-v; }
+.@{fa-css-prefix}-rss-square:before { content: @fa-var-rss-square; }
+.@{fa-css-prefix}-play-circle:before { content: @fa-var-play-circle; }
+.@{fa-css-prefix}-ticket:before { content: @fa-var-ticket; }
+.@{fa-css-prefix}-minus-square:before { content: @fa-var-minus-square; }
+.@{fa-css-prefix}-minus-square-o:before { content: @fa-var-minus-square-o; }
+.@{fa-css-prefix}-level-up:before { content: @fa-var-level-up; }
+.@{fa-css-prefix}-level-down:before { content: @fa-var-level-down; }
+.@{fa-css-prefix}-check-square:before { content: @fa-var-check-square; }
+.@{fa-css-prefix}-pencil-square:before { content: @fa-var-pencil-square; }
+.@{fa-css-prefix}-external-link-square:before { content: @fa-var-external-link-square; }
+.@{fa-css-prefix}-share-square:before { content: @fa-var-share-square; }
+.@{fa-css-prefix}-compass:before { content: @fa-var-compass; }
+.@{fa-css-prefix}-toggle-down:before,
+.@{fa-css-prefix}-caret-square-o-down:before { content: @fa-var-caret-square-o-down; }
+.@{fa-css-prefix}-toggle-up:before,
+.@{fa-css-prefix}-caret-square-o-up:before { content: @fa-var-caret-square-o-up; }
+.@{fa-css-prefix}-toggle-right:before,
+.@{fa-css-prefix}-caret-square-o-right:before { content: @fa-var-caret-square-o-right; }
+.@{fa-css-prefix}-euro:before,
+.@{fa-css-prefix}-eur:before { content: @fa-var-eur; }
+.@{fa-css-prefix}-gbp:before { content: @fa-var-gbp; }
+.@{fa-css-prefix}-dollar:before,
+.@{fa-css-prefix}-usd:before { content: @fa-var-usd; }
+.@{fa-css-prefix}-rupee:before,
+.@{fa-css-prefix}-inr:before { content: @fa-var-inr; }
+.@{fa-css-prefix}-cny:before,
+.@{fa-css-prefix}-rmb:before,
+.@{fa-css-prefix}-yen:before,
+.@{fa-css-prefix}-jpy:before { content: @fa-var-jpy; }
+.@{fa-css-prefix}-ruble:before,
+.@{fa-css-prefix}-rouble:before,
+.@{fa-css-prefix}-rub:before { content: @fa-var-rub; }
+.@{fa-css-prefix}-won:before,
+.@{fa-css-prefix}-krw:before { content: @fa-var-krw; }
+.@{fa-css-prefix}-bitcoin:before,
+.@{fa-css-prefix}-btc:before { content: @fa-var-btc; }
+.@{fa-css-prefix}-file:before { content: @fa-var-file; }
+.@{fa-css-prefix}-file-text:before { content: @fa-var-file-text; }
+.@{fa-css-prefix}-sort-alpha-asc:before { content: @fa-var-sort-alpha-asc; }
+.@{fa-css-prefix}-sort-alpha-desc:before { content: @fa-var-sort-alpha-desc; }
+.@{fa-css-prefix}-sort-amount-asc:before { content: @fa-var-sort-amount-asc; }
+.@{fa-css-prefix}-sort-amount-desc:before { content: @fa-var-sort-amount-desc; }
+.@{fa-css-prefix}-sort-numeric-asc:before { content: @fa-var-sort-numeric-asc; }
+.@{fa-css-prefix}-sort-numeric-desc:before { content: @fa-var-sort-numeric-desc; }
+.@{fa-css-prefix}-thumbs-up:before { content: @fa-var-thumbs-up; }
+.@{fa-css-prefix}-thumbs-down:before { content: @fa-var-thumbs-down; }
+.@{fa-css-prefix}-youtube-square:before { content: @fa-var-youtube-square; }
+.@{fa-css-prefix}-youtube:before { content: @fa-var-youtube; }
+.@{fa-css-prefix}-xing:before { content: @fa-var-xing; }
+.@{fa-css-prefix}-xing-square:before { content: @fa-var-xing-square; }
+.@{fa-css-prefix}-youtube-play:before { content: @fa-var-youtube-play; }
+.@{fa-css-prefix}-dropbox:before { content: @fa-var-dropbox; }
+.@{fa-css-prefix}-stack-overflow:before { content: @fa-var-stack-overflow; }
+.@{fa-css-prefix}-instagram:before { content: @fa-var-instagram; }
+.@{fa-css-prefix}-flickr:before { content: @fa-var-flickr; }
+.@{fa-css-prefix}-adn:before { content: @fa-var-adn; }
+.@{fa-css-prefix}-bitbucket:before { content: @fa-var-bitbucket; }
+.@{fa-css-prefix}-bitbucket-square:before { content: @fa-var-bitbucket-square; }
+.@{fa-css-prefix}-tumblr:before { content: @fa-var-tumblr; }
+.@{fa-css-prefix}-tumblr-square:before { content: @fa-var-tumblr-square; }
+.@{fa-css-prefix}-long-arrow-down:before { content: @fa-var-long-arrow-down; }
+.@{fa-css-prefix}-long-arrow-up:before { content: @fa-var-long-arrow-up; }
+.@{fa-css-prefix}-long-arrow-left:before { content: @fa-var-long-arrow-left; }
+.@{fa-css-prefix}-long-arrow-right:before { content: @fa-var-long-arrow-right; }
+.@{fa-css-prefix}-apple:before { content: @fa-var-apple; }
+.@{fa-css-prefix}-windows:before { content: @fa-var-windows; }
+.@{fa-css-prefix}-android:before { content: @fa-var-android; }
+.@{fa-css-prefix}-linux:before { content: @fa-var-linux; }
+.@{fa-css-prefix}-dribbble:before { content: @fa-var-dribbble; }
+.@{fa-css-prefix}-skype:before { content: @fa-var-skype; }
+.@{fa-css-prefix}-foursquare:before { content: @fa-var-foursquare; }
+.@{fa-css-prefix}-trello:before { content: @fa-var-trello; }
+.@{fa-css-prefix}-female:before { content: @fa-var-female; }
+.@{fa-css-prefix}-male:before { content: @fa-var-male; }
+.@{fa-css-prefix}-gittip:before,
+.@{fa-css-prefix}-gratipay:before { content: @fa-var-gratipay; }
+.@{fa-css-prefix}-sun-o:before { content: @fa-var-sun-o; }
+.@{fa-css-prefix}-moon-o:before { content: @fa-var-moon-o; }
+.@{fa-css-prefix}-archive:before { content: @fa-var-archive; }
+.@{fa-css-prefix}-bug:before { content: @fa-var-bug; }
+.@{fa-css-prefix}-vk:before { content: @fa-var-vk; }
+.@{fa-css-prefix}-weibo:before { content: @fa-var-weibo; }
+.@{fa-css-prefix}-renren:before { content: @fa-var-renren; }
+.@{fa-css-prefix}-pagelines:before { content: @fa-var-pagelines; }
+.@{fa-css-prefix}-stack-exchange:before { content: @fa-var-stack-exchange; }
+.@{fa-css-prefix}-arrow-circle-o-right:before { content: @fa-var-arrow-circle-o-right; }
+.@{fa-css-prefix}-arrow-circle-o-left:before { content: @fa-var-arrow-circle-o-left; }
+.@{fa-css-prefix}-toggle-left:before,
+.@{fa-css-prefix}-caret-square-o-left:before { content: @fa-var-caret-square-o-left; }
+.@{fa-css-prefix}-dot-circle-o:before { content: @fa-var-dot-circle-o; }
+.@{fa-css-prefix}-wheelchair:before { content: @fa-var-wheelchair; }
+.@{fa-css-prefix}-vimeo-square:before { content: @fa-var-vimeo-square; }
+.@{fa-css-prefix}-turkish-lira:before,
+.@{fa-css-prefix}-try:before { content: @fa-var-try; }
+.@{fa-css-prefix}-plus-square-o:before { content: @fa-var-plus-square-o; }
+.@{fa-css-prefix}-space-shuttle:before { content: @fa-var-space-shuttle; }
+.@{fa-css-prefix}-slack:before { content: @fa-var-slack; }
+.@{fa-css-prefix}-envelope-square:before { content: @fa-var-envelope-square; }
+.@{fa-css-prefix}-wordpress:before { content: @fa-var-wordpress; }
+.@{fa-css-prefix}-openid:before { content: @fa-var-openid; }
+.@{fa-css-prefix}-institution:before,
+.@{fa-css-prefix}-bank:before,
+.@{fa-css-prefix}-university:before { content: @fa-var-university; }
+.@{fa-css-prefix}-mortar-board:before,
+.@{fa-css-prefix}-graduation-cap:before { content: @fa-var-graduation-cap; }
+.@{fa-css-prefix}-yahoo:before { content: @fa-var-yahoo; }
+.@{fa-css-prefix}-google:before { content: @fa-var-google; }
+.@{fa-css-prefix}-reddit:before { content: @fa-var-reddit; }
+.@{fa-css-prefix}-reddit-square:before { content: @fa-var-reddit-square; }
+.@{fa-css-prefix}-stumbleupon-circle:before { content: @fa-var-stumbleupon-circle; }
+.@{fa-css-prefix}-stumbleupon:before { content: @fa-var-stumbleupon; }
+.@{fa-css-prefix}-delicious:before { content: @fa-var-delicious; }
+.@{fa-css-prefix}-digg:before { content: @fa-var-digg; }
+.@{fa-css-prefix}-pied-piper-pp:before { content: @fa-var-pied-piper-pp; }
+.@{fa-css-prefix}-pied-piper-alt:before { content: @fa-var-pied-piper-alt; }
+.@{fa-css-prefix}-drupal:before { content: @fa-var-drupal; }
+.@{fa-css-prefix}-joomla:before { content: @fa-var-joomla; }
+.@{fa-css-prefix}-language:before { content: @fa-var-language; }
+.@{fa-css-prefix}-fax:before { content: @fa-var-fax; }
+.@{fa-css-prefix}-building:before { content: @fa-var-building; }
+.@{fa-css-prefix}-child:before { content: @fa-var-child; }
+.@{fa-css-prefix}-paw:before { content: @fa-var-paw; }
+.@{fa-css-prefix}-spoon:before { content: @fa-var-spoon; }
+.@{fa-css-prefix}-cube:before { content: @fa-var-cube; }
+.@{fa-css-prefix}-cubes:before { content: @fa-var-cubes; }
+.@{fa-css-prefix}-behance:before { content: @fa-var-behance; }
+.@{fa-css-prefix}-behance-square:before { content: @fa-var-behance-square; }
+.@{fa-css-prefix}-steam:before { content: @fa-var-steam; }
+.@{fa-css-prefix}-steam-square:before { content: @fa-var-steam-square; }
+.@{fa-css-prefix}-recycle:before { content: @fa-var-recycle; }
+.@{fa-css-prefix}-automobile:before,
+.@{fa-css-prefix}-car:before { content: @fa-var-car; }
+.@{fa-css-prefix}-cab:before,
+.@{fa-css-prefix}-taxi:before { content: @fa-var-taxi; }
+.@{fa-css-prefix}-tree:before { content: @fa-var-tree; }
+.@{fa-css-prefix}-spotify:before { content: @fa-var-spotify; }
+.@{fa-css-prefix}-deviantart:before { content: @fa-var-deviantart; }
+.@{fa-css-prefix}-soundcloud:before { content: @fa-var-soundcloud; }
+.@{fa-css-prefix}-database:before { content: @fa-var-database; }
+.@{fa-css-prefix}-file-pdf-o:before { content: @fa-var-file-pdf-o; }
+.@{fa-css-prefix}-file-word-o:before { content: @fa-var-file-word-o; }
+.@{fa-css-prefix}-file-excel-o:before { content: @fa-var-file-excel-o; }
+.@{fa-css-prefix}-file-powerpoint-o:before { content: @fa-var-file-powerpoint-o; }
+.@{fa-css-prefix}-file-photo-o:before,
+.@{fa-css-prefix}-file-picture-o:before,
+.@{fa-css-prefix}-file-image-o:before { content: @fa-var-file-image-o; }
+.@{fa-css-prefix}-file-zip-o:before,
+.@{fa-css-prefix}-file-archive-o:before { content: @fa-var-file-archive-o; }
+.@{fa-css-prefix}-file-sound-o:before,
+.@{fa-css-prefix}-file-audio-o:before { content: @fa-var-file-audio-o; }
+.@{fa-css-prefix}-file-movie-o:before,
+.@{fa-css-prefix}-file-video-o:before { content: @fa-var-file-video-o; }
+.@{fa-css-prefix}-file-code-o:before { content: @fa-var-file-code-o; }
+.@{fa-css-prefix}-vine:before { content: @fa-var-vine; }
+.@{fa-css-prefix}-codepen:before { content: @fa-var-codepen; }
+.@{fa-css-prefix}-jsfiddle:before { content: @fa-var-jsfiddle; }
+.@{fa-css-prefix}-life-bouy:before,
+.@{fa-css-prefix}-life-buoy:before,
+.@{fa-css-prefix}-life-saver:before,
+.@{fa-css-prefix}-support:before,
+.@{fa-css-prefix}-life-ring:before { content: @fa-var-life-ring; }
+.@{fa-css-prefix}-circle-o-notch:before { content: @fa-var-circle-o-notch; }
+.@{fa-css-prefix}-ra:before,
+.@{fa-css-prefix}-resistance:before,
+.@{fa-css-prefix}-rebel:before { content: @fa-var-rebel; }
+.@{fa-css-prefix}-ge:before,
+.@{fa-css-prefix}-empire:before { content: @fa-var-empire; }
+.@{fa-css-prefix}-git-square:before { content: @fa-var-git-square; }
+.@{fa-css-prefix}-git:before { content: @fa-var-git; }
+.@{fa-css-prefix}-y-combinator-square:before,
+.@{fa-css-prefix}-yc-square:before,
+.@{fa-css-prefix}-hacker-news:before { content: @fa-var-hacker-news; }
+.@{fa-css-prefix}-tencent-weibo:before { content: @fa-var-tencent-weibo; }
+.@{fa-css-prefix}-qq:before { content: @fa-var-qq; }
+.@{fa-css-prefix}-wechat:before,
+.@{fa-css-prefix}-weixin:before { content: @fa-var-weixin; }
+.@{fa-css-prefix}-send:before,
+.@{fa-css-prefix}-paper-plane:before { content: @fa-var-paper-plane; }
+.@{fa-css-prefix}-send-o:before,
+.@{fa-css-prefix}-paper-plane-o:before { content: @fa-var-paper-plane-o; }
+.@{fa-css-prefix}-history:before { content: @fa-var-history; }
+.@{fa-css-prefix}-circle-thin:before { content: @fa-var-circle-thin; }
+.@{fa-css-prefix}-header:before { content: @fa-var-header; }
+.@{fa-css-prefix}-paragraph:before { content: @fa-var-paragraph; }
+.@{fa-css-prefix}-sliders:before { content: @fa-var-sliders; }
+.@{fa-css-prefix}-share-alt:before { content: @fa-var-share-alt; }
+.@{fa-css-prefix}-share-alt-square:before { content: @fa-var-share-alt-square; }
+.@{fa-css-prefix}-bomb:before { content: @fa-var-bomb; }
+.@{fa-css-prefix}-soccer-ball-o:before,
+.@{fa-css-prefix}-futbol-o:before { content: @fa-var-futbol-o; }
+.@{fa-css-prefix}-tty:before { content: @fa-var-tty; }
+.@{fa-css-prefix}-binoculars:before { content: @fa-var-binoculars; }
+.@{fa-css-prefix}-plug:before { content: @fa-var-plug; }
+.@{fa-css-prefix}-slideshare:before { content: @fa-var-slideshare; }
+.@{fa-css-prefix}-twitch:before { content: @fa-var-twitch; }
+.@{fa-css-prefix}-yelp:before { content: @fa-var-yelp; }
+.@{fa-css-prefix}-newspaper-o:before { content: @fa-var-newspaper-o; }
+.@{fa-css-prefix}-wifi:before { content: @fa-var-wifi; }
+.@{fa-css-prefix}-calculator:before { content: @fa-var-calculator; }
+.@{fa-css-prefix}-paypal:before { content: @fa-var-paypal; }
+.@{fa-css-prefix}-google-wallet:before { content: @fa-var-google-wallet; }
+.@{fa-css-prefix}-cc-visa:before { content: @fa-var-cc-visa; }
+.@{fa-css-prefix}-cc-mastercard:before { content: @fa-var-cc-mastercard; }
+.@{fa-css-prefix}-cc-discover:before { content: @fa-var-cc-discover; }
+.@{fa-css-prefix}-cc-amex:before { content: @fa-var-cc-amex; }
+.@{fa-css-prefix}-cc-paypal:before { content: @fa-var-cc-paypal; }
+.@{fa-css-prefix}-cc-stripe:before { content: @fa-var-cc-stripe; }
+.@{fa-css-prefix}-bell-slash:before { content: @fa-var-bell-slash; }
+.@{fa-css-prefix}-bell-slash-o:before { content: @fa-var-bell-slash-o; }
+.@{fa-css-prefix}-trash:before { content: @fa-var-trash; }
+.@{fa-css-prefix}-copyright:before { content: @fa-var-copyright; }
+.@{fa-css-prefix}-at:before { content: @fa-var-at; }
+.@{fa-css-prefix}-eyedropper:before { content: @fa-var-eyedropper; }
+.@{fa-css-prefix}-paint-brush:before { content: @fa-var-paint-brush; }
+.@{fa-css-prefix}-birthday-cake:before { content: @fa-var-birthday-cake; }
+.@{fa-css-prefix}-area-chart:before { content: @fa-var-area-chart; }
+.@{fa-css-prefix}-pie-chart:before { content: @fa-var-pie-chart; }
+.@{fa-css-prefix}-line-chart:before { content: @fa-var-line-chart; }
+.@{fa-css-prefix}-lastfm:before { content: @fa-var-lastfm; }
+.@{fa-css-prefix}-lastfm-square:before { content: @fa-var-lastfm-square; }
+.@{fa-css-prefix}-toggle-off:before { content: @fa-var-toggle-off; }
+.@{fa-css-prefix}-toggle-on:before { content: @fa-var-toggle-on; }
+.@{fa-css-prefix}-bicycle:before { content: @fa-var-bicycle; }
+.@{fa-css-prefix}-bus:before { content: @fa-var-bus; }
+.@{fa-css-prefix}-ioxhost:before { content: @fa-var-ioxhost; }
+.@{fa-css-prefix}-angellist:before { content: @fa-var-angellist; }
+.@{fa-css-prefix}-cc:before { content: @fa-var-cc; }
+.@{fa-css-prefix}-shekel:before,
+.@{fa-css-prefix}-sheqel:before,
+.@{fa-css-prefix}-ils:before { content: @fa-var-ils; }
+.@{fa-css-prefix}-meanpath:before { content: @fa-var-meanpath; }
+.@{fa-css-prefix}-buysellads:before { content: @fa-var-buysellads; }
+.@{fa-css-prefix}-connectdevelop:before { content: @fa-var-connectdevelop; }
+.@{fa-css-prefix}-dashcube:before { content: @fa-var-dashcube; }
+.@{fa-css-prefix}-forumbee:before { content: @fa-var-forumbee; }
+.@{fa-css-prefix}-leanpub:before { content: @fa-var-leanpub; }
+.@{fa-css-prefix}-sellsy:before { content: @fa-var-sellsy; }
+.@{fa-css-prefix}-shirtsinbulk:before { content: @fa-var-shirtsinbulk; }
+.@{fa-css-prefix}-simplybuilt:before { content: @fa-var-simplybuilt; }
+.@{fa-css-prefix}-skyatlas:before { content: @fa-var-skyatlas; }
+.@{fa-css-prefix}-cart-plus:before { content: @fa-var-cart-plus; }
+.@{fa-css-prefix}-cart-arrow-down:before { content: @fa-var-cart-arrow-down; }
+.@{fa-css-prefix}-diamond:before { content: @fa-var-diamond; }
+.@{fa-css-prefix}-ship:before { content: @fa-var-ship; }
+.@{fa-css-prefix}-user-secret:before { content: @fa-var-user-secret; }
+.@{fa-css-prefix}-motorcycle:before { content: @fa-var-motorcycle; }
+.@{fa-css-prefix}-street-view:before { content: @fa-var-street-view; }
+.@{fa-css-prefix}-heartbeat:before { content: @fa-var-heartbeat; }
+.@{fa-css-prefix}-venus:before { content: @fa-var-venus; }
+.@{fa-css-prefix}-mars:before { content: @fa-var-mars; }
+.@{fa-css-prefix}-mercury:before { content: @fa-var-mercury; }
+.@{fa-css-prefix}-intersex:before,
+.@{fa-css-prefix}-transgender:before { content: @fa-var-transgender; }
+.@{fa-css-prefix}-transgender-alt:before { content: @fa-var-transgender-alt; }
+.@{fa-css-prefix}-venus-double:before { content: @fa-var-venus-double; }
+.@{fa-css-prefix}-mars-double:before { content: @fa-var-mars-double; }
+.@{fa-css-prefix}-venus-mars:before { content: @fa-var-venus-mars; }
+.@{fa-css-prefix}-mars-stroke:before { content: @fa-var-mars-stroke; }
+.@{fa-css-prefix}-mars-stroke-v:before { content: @fa-var-mars-stroke-v; }
+.@{fa-css-prefix}-mars-stroke-h:before { content: @fa-var-mars-stroke-h; }
+.@{fa-css-prefix}-neuter:before { content: @fa-var-neuter; }
+.@{fa-css-prefix}-genderless:before { content: @fa-var-genderless; }
+.@{fa-css-prefix}-facebook-official:before { content: @fa-var-facebook-official; }
+.@{fa-css-prefix}-pinterest-p:before { content: @fa-var-pinterest-p; }
+.@{fa-css-prefix}-whatsapp:before { content: @fa-var-whatsapp; }
+.@{fa-css-prefix}-server:before { content: @fa-var-server; }
+.@{fa-css-prefix}-user-plus:before { content: @fa-var-user-plus; }
+.@{fa-css-prefix}-user-times:before { content: @fa-var-user-times; }
+.@{fa-css-prefix}-hotel:before,
+.@{fa-css-prefix}-bed:before { content: @fa-var-bed; }
+.@{fa-css-prefix}-viacoin:before { content: @fa-var-viacoin; }
+.@{fa-css-prefix}-train:before { content: @fa-var-train; }
+.@{fa-css-prefix}-subway:before { content: @fa-var-subway; }
+.@{fa-css-prefix}-medium:before { content: @fa-var-medium; }
+.@{fa-css-prefix}-yc:before,
+.@{fa-css-prefix}-y-combinator:before { content: @fa-var-y-combinator; }
+.@{fa-css-prefix}-optin-monster:before { content: @fa-var-optin-monster; }
+.@{fa-css-prefix}-opencart:before { content: @fa-var-opencart; }
+.@{fa-css-prefix}-expeditedssl:before { content: @fa-var-expeditedssl; }
+.@{fa-css-prefix}-battery-4:before,
+.@{fa-css-prefix}-battery-full:before { content: @fa-var-battery-full; }
+.@{fa-css-prefix}-battery-3:before,
+.@{fa-css-prefix}-battery-three-quarters:before { content: @fa-var-battery-three-quarters; }
+.@{fa-css-prefix}-battery-2:before,
+.@{fa-css-prefix}-battery-half:before { content: @fa-var-battery-half; }
+.@{fa-css-prefix}-battery-1:before,
+.@{fa-css-prefix}-battery-quarter:before { content: @fa-var-battery-quarter; }
+.@{fa-css-prefix}-battery-0:before,
+.@{fa-css-prefix}-battery-empty:before { content: @fa-var-battery-empty; }
+.@{fa-css-prefix}-mouse-pointer:before { content: @fa-var-mouse-pointer; }
+.@{fa-css-prefix}-i-cursor:before { content: @fa-var-i-cursor; }
+.@{fa-css-prefix}-object-group:before { content: @fa-var-object-group; }
+.@{fa-css-prefix}-object-ungroup:before { content: @fa-var-object-ungroup; }
+.@{fa-css-prefix}-sticky-note:before { content: @fa-var-sticky-note; }
+.@{fa-css-prefix}-sticky-note-o:before { content: @fa-var-sticky-note-o; }
+.@{fa-css-prefix}-cc-jcb:before { content: @fa-var-cc-jcb; }
+.@{fa-css-prefix}-cc-diners-club:before { content: @fa-var-cc-diners-club; }
+.@{fa-css-prefix}-clone:before { content: @fa-var-clone; }
+.@{fa-css-prefix}-balance-scale:before { content: @fa-var-balance-scale; }
+.@{fa-css-prefix}-hourglass-o:before { content: @fa-var-hourglass-o; }
+.@{fa-css-prefix}-hourglass-1:before,
+.@{fa-css-prefix}-hourglass-start:before { content: @fa-var-hourglass-start; }
+.@{fa-css-prefix}-hourglass-2:before,
+.@{fa-css-prefix}-hourglass-half:before { content: @fa-var-hourglass-half; }
+.@{fa-css-prefix}-hourglass-3:before,
+.@{fa-css-prefix}-hourglass-end:before { content: @fa-var-hourglass-end; }
+.@{fa-css-prefix}-hourglass:before { content: @fa-var-hourglass; }
+.@{fa-css-prefix}-hand-grab-o:before,
+.@{fa-css-prefix}-hand-rock-o:before { content: @fa-var-hand-rock-o; }
+.@{fa-css-prefix}-hand-stop-o:before,
+.@{fa-css-prefix}-hand-paper-o:before { content: @fa-var-hand-paper-o; }
+.@{fa-css-prefix}-hand-scissors-o:before { content: @fa-var-hand-scissors-o; }
+.@{fa-css-prefix}-hand-lizard-o:before { content: @fa-var-hand-lizard-o; }
+.@{fa-css-prefix}-hand-spock-o:before { content: @fa-var-hand-spock-o; }
+.@{fa-css-prefix}-hand-pointer-o:before { content: @fa-var-hand-pointer-o; }
+.@{fa-css-prefix}-hand-peace-o:before { content: @fa-var-hand-peace-o; }
+.@{fa-css-prefix}-trademark:before { content: @fa-var-trademark; }
+.@{fa-css-prefix}-registered:before { content: @fa-var-registered; }
+.@{fa-css-prefix}-creative-commons:before { content: @fa-var-creative-commons; }
+.@{fa-css-prefix}-gg:before { content: @fa-var-gg; }
+.@{fa-css-prefix}-gg-circle:before { content: @fa-var-gg-circle; }
+.@{fa-css-prefix}-tripadvisor:before { content: @fa-var-tripadvisor; }
+.@{fa-css-prefix}-odnoklassniki:before { content: @fa-var-odnoklassniki; }
+.@{fa-css-prefix}-odnoklassniki-square:before { content: @fa-var-odnoklassniki-square; }
+.@{fa-css-prefix}-get-pocket:before { content: @fa-var-get-pocket; }
+.@{fa-css-prefix}-wikipedia-w:before { content: @fa-var-wikipedia-w; }
+.@{fa-css-prefix}-safari:before { content: @fa-var-safari; }
+.@{fa-css-prefix}-chrome:before { content: @fa-var-chrome; }
+.@{fa-css-prefix}-firefox:before { content: @fa-var-firefox; }
+.@{fa-css-prefix}-opera:before { content: @fa-var-opera; }
+.@{fa-css-prefix}-internet-explorer:before { content: @fa-var-internet-explorer; }
+.@{fa-css-prefix}-tv:before,
+.@{fa-css-prefix}-television:before { content: @fa-var-television; }
+.@{fa-css-prefix}-contao:before { content: @fa-var-contao; }
+.@{fa-css-prefix}-500px:before { content: @fa-var-500px; }
+.@{fa-css-prefix}-amazon:before { content: @fa-var-amazon; }
+.@{fa-css-prefix}-calendar-plus-o:before { content: @fa-var-calendar-plus-o; }
+.@{fa-css-prefix}-calendar-minus-o:before { content: @fa-var-calendar-minus-o; }
+.@{fa-css-prefix}-calendar-times-o:before { content: @fa-var-calendar-times-o; }
+.@{fa-css-prefix}-calendar-check-o:before { content: @fa-var-calendar-check-o; }
+.@{fa-css-prefix}-industry:before { content: @fa-var-industry; }
+.@{fa-css-prefix}-map-pin:before { content: @fa-var-map-pin; }
+.@{fa-css-prefix}-map-signs:before { content: @fa-var-map-signs; }
+.@{fa-css-prefix}-map-o:before { content: @fa-var-map-o; }
+.@{fa-css-prefix}-map:before { content: @fa-var-map; }
+.@{fa-css-prefix}-commenting:before { content: @fa-var-commenting; }
+.@{fa-css-prefix}-commenting-o:before { content: @fa-var-commenting-o; }
+.@{fa-css-prefix}-houzz:before { content: @fa-var-houzz; }
+.@{fa-css-prefix}-vimeo:before { content: @fa-var-vimeo; }
+.@{fa-css-prefix}-black-tie:before { content: @fa-var-black-tie; }
+.@{fa-css-prefix}-fonticons:before { content: @fa-var-fonticons; }
+.@{fa-css-prefix}-reddit-alien:before { content: @fa-var-reddit-alien; }
+.@{fa-css-prefix}-edge:before { content: @fa-var-edge; }
+.@{fa-css-prefix}-credit-card-alt:before { content: @fa-var-credit-card-alt; }
+.@{fa-css-prefix}-codiepie:before { content: @fa-var-codiepie; }
+.@{fa-css-prefix}-modx:before { content: @fa-var-modx; }
+.@{fa-css-prefix}-fort-awesome:before { content: @fa-var-fort-awesome; }
+.@{fa-css-prefix}-usb:before { content: @fa-var-usb; }
+.@{fa-css-prefix}-product-hunt:before { content: @fa-var-product-hunt; }
+.@{fa-css-prefix}-mixcloud:before { content: @fa-var-mixcloud; }
+.@{fa-css-prefix}-scribd:before { content: @fa-var-scribd; }
+.@{fa-css-prefix}-pause-circle:before { content: @fa-var-pause-circle; }
+.@{fa-css-prefix}-pause-circle-o:before { content: @fa-var-pause-circle-o; }
+.@{fa-css-prefix}-stop-circle:before { content: @fa-var-stop-circle; }
+.@{fa-css-prefix}-stop-circle-o:before { content: @fa-var-stop-circle-o; }
+.@{fa-css-prefix}-shopping-bag:before { content: @fa-var-shopping-bag; }
+.@{fa-css-prefix}-shopping-basket:before { content: @fa-var-shopping-basket; }
+.@{fa-css-prefix}-hashtag:before { content: @fa-var-hashtag; }
+.@{fa-css-prefix}-bluetooth:before { content: @fa-var-bluetooth; }
+.@{fa-css-prefix}-bluetooth-b:before { content: @fa-var-bluetooth-b; }
+.@{fa-css-prefix}-percent:before { content: @fa-var-percent; }
+.@{fa-css-prefix}-gitlab:before { content: @fa-var-gitlab; }
+.@{fa-css-prefix}-wpbeginner:before { content: @fa-var-wpbeginner; }
+.@{fa-css-prefix}-wpforms:before { content: @fa-var-wpforms; }
+.@{fa-css-prefix}-envira:before { content: @fa-var-envira; }
+.@{fa-css-prefix}-universal-access:before { content: @fa-var-universal-access; }
+.@{fa-css-prefix}-wheelchair-alt:before { content: @fa-var-wheelchair-alt; }
+.@{fa-css-prefix}-question-circle-o:before { content: @fa-var-question-circle-o; }
+.@{fa-css-prefix}-blind:before { content: @fa-var-blind; }
+.@{fa-css-prefix}-audio-description:before { content: @fa-var-audio-description; }
+.@{fa-css-prefix}-volume-control-phone:before { content: @fa-var-volume-control-phone; }
+.@{fa-css-prefix}-braille:before { content: @fa-var-braille; }
+.@{fa-css-prefix}-assistive-listening-systems:before { content: @fa-var-assistive-listening-systems; }
+.@{fa-css-prefix}-asl-interpreting:before,
+.@{fa-css-prefix}-american-sign-language-interpreting:before { content: @fa-var-american-sign-language-interpreting; }
+.@{fa-css-prefix}-deafness:before,
+.@{fa-css-prefix}-hard-of-hearing:before,
+.@{fa-css-prefix}-deaf:before { content: @fa-var-deaf; }
+.@{fa-css-prefix}-glide:before { content: @fa-var-glide; }
+.@{fa-css-prefix}-glide-g:before { content: @fa-var-glide-g; }
+.@{fa-css-prefix}-signing:before,
+.@{fa-css-prefix}-sign-language:before { content: @fa-var-sign-language; }
+.@{fa-css-prefix}-low-vision:before { content: @fa-var-low-vision; }
+.@{fa-css-prefix}-viadeo:before { content: @fa-var-viadeo; }
+.@{fa-css-prefix}-viadeo-square:before { content: @fa-var-viadeo-square; }
+.@{fa-css-prefix}-snapchat:before { content: @fa-var-snapchat; }
+.@{fa-css-prefix}-snapchat-ghost:before { content: @fa-var-snapchat-ghost; }
+.@{fa-css-prefix}-snapchat-square:before { content: @fa-var-snapchat-square; }
+.@{fa-css-prefix}-pied-piper:before { content: @fa-var-pied-piper; }
+.@{fa-css-prefix}-first-order:before { content: @fa-var-first-order; }
+.@{fa-css-prefix}-yoast:before { content: @fa-var-yoast; }
+.@{fa-css-prefix}-themeisle:before { content: @fa-var-themeisle; }
+.@{fa-css-prefix}-google-plus-circle:before,
+.@{fa-css-prefix}-google-plus-official:before { content: @fa-var-google-plus-official; }
+.@{fa-css-prefix}-fa:before,
+.@{fa-css-prefix}-font-awesome:before { content: @fa-var-font-awesome; }
diff --git a/themes/bootstrap3/less/vendor/font-awesome/larger.less b/themes/bootstrap3/less/vendor/font-awesome/larger.less
new file mode 100644
index 0000000000000000000000000000000000000000..c9d646770e2186c73632cbe042d82d1d1acaa25b
--- /dev/null
+++ b/themes/bootstrap3/less/vendor/font-awesome/larger.less
@@ -0,0 +1,13 @@
+// Icon Sizes
+// -------------------------
+
+/* makes the font 33% larger relative to the icon container */
+.@{fa-css-prefix}-lg {
+  font-size: (4em / 3);
+  line-height: (3em / 4);
+  vertical-align: -15%;
+}
+.@{fa-css-prefix}-2x { font-size: 2em; }
+.@{fa-css-prefix}-3x { font-size: 3em; }
+.@{fa-css-prefix}-4x { font-size: 4em; }
+.@{fa-css-prefix}-5x { font-size: 5em; }
diff --git a/themes/bootstrap3/less/vendor/font-awesome/list.less b/themes/bootstrap3/less/vendor/font-awesome/list.less
new file mode 100644
index 0000000000000000000000000000000000000000..0b440382f61f0bb1683e5b15c2a00e4e398f9980
--- /dev/null
+++ b/themes/bootstrap3/less/vendor/font-awesome/list.less
@@ -0,0 +1,19 @@
+// List Icons
+// -------------------------
+
+.@{fa-css-prefix}-ul {
+  padding-left: 0;
+  margin-left: @fa-li-width;
+  list-style-type: none;
+  > li { position: relative; }
+}
+.@{fa-css-prefix}-li {
+  position: absolute;
+  left: -@fa-li-width;
+  width: @fa-li-width;
+  top: (2em / 14);
+  text-align: center;
+  &.@{fa-css-prefix}-lg {
+    left: (-@fa-li-width + (4em / 14));
+  }
+}
diff --git a/themes/bootstrap3/less/vendor/font-awesome/mixins.less b/themes/bootstrap3/less/vendor/font-awesome/mixins.less
new file mode 100644
index 0000000000000000000000000000000000000000..beef231d0e176a47375d93fb7365270eea52f7b4
--- /dev/null
+++ b/themes/bootstrap3/less/vendor/font-awesome/mixins.less
@@ -0,0 +1,60 @@
+// Mixins
+// --------------------------
+
+.fa-icon() {
+  display: inline-block;
+  font: normal normal normal @fa-font-size-base/@fa-line-height-base FontAwesome; // shortening font declaration
+  font-size: inherit; // can't have font-size inherit on line above, so need to override
+  text-rendering: auto; // optimizelegibility throws things off #1094
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+
+}
+
+.fa-icon-rotate(@degrees, @rotation) {
+  -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=@{rotation})";
+  -webkit-transform: rotate(@degrees);
+      -ms-transform: rotate(@degrees);
+          transform: rotate(@degrees);
+}
+
+.fa-icon-flip(@horiz, @vert, @rotation) {
+  -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=@{rotation}, mirror=1)";
+  -webkit-transform: scale(@horiz, @vert);
+      -ms-transform: scale(@horiz, @vert);
+          transform: scale(@horiz, @vert);
+}
+
+
+// Only display content to screen readers. A la Bootstrap 4.
+//
+// See: http://a11yproject.com/posts/how-to-hide-content/
+
+.sr-only() {
+  position: absolute;
+  width: 1px;
+  height: 1px;
+  padding: 0;
+  margin: -1px;
+  overflow: hidden;
+  clip: rect(0,0,0,0);
+  border: 0;
+}
+
+// Use in conjunction with .sr-only to only display content when it's focused.
+//
+// Useful for "Skip to main content" links; see http://www.w3.org/TR/2013/NOTE-WCAG20-TECHS-20130905/G1
+//
+// Credit: HTML5 Boilerplate
+
+.sr-only-focusable() {
+  &:active,
+  &:focus {
+    position: static;
+    width: auto;
+    height: auto;
+    margin: 0;
+    overflow: visible;
+    clip: auto;
+  }
+}
diff --git a/themes/bootstrap3/less/vendor/font-awesome/path.less b/themes/bootstrap3/less/vendor/font-awesome/path.less
new file mode 100644
index 0000000000000000000000000000000000000000..835be41f8151b6439f799be29ec7921af1b72593
--- /dev/null
+++ b/themes/bootstrap3/less/vendor/font-awesome/path.less
@@ -0,0 +1,15 @@
+/* FONT PATH
+ * -------------------------- */
+
+@font-face {
+  font-family: 'FontAwesome';
+  src: url('@{fa-font-path}/fontawesome-webfont.eot?v=@{fa-version}');
+  src: url('@{fa-font-path}/fontawesome-webfont.eot?#iefix&v=@{fa-version}') format('embedded-opentype'),
+    url('@{fa-font-path}/fontawesome-webfont.woff2?v=@{fa-version}') format('woff2'),
+    url('@{fa-font-path}/fontawesome-webfont.woff?v=@{fa-version}') format('woff'),
+    url('@{fa-font-path}/fontawesome-webfont.ttf?v=@{fa-version}') format('truetype'),
+    url('@{fa-font-path}/fontawesome-webfont.svg?v=@{fa-version}#fontawesomeregular') format('svg');
+  // src: url('@{fa-font-path}/FontAwesome.otf') format('opentype'); // used when developing fonts
+  font-weight: normal;
+  font-style: normal;
+}
diff --git a/themes/bootstrap3/less/vendor/font-awesome/rotated-flipped.less b/themes/bootstrap3/less/vendor/font-awesome/rotated-flipped.less
new file mode 100644
index 0000000000000000000000000000000000000000..f6ba81475b92355723f0dc12767f715995a34162
--- /dev/null
+++ b/themes/bootstrap3/less/vendor/font-awesome/rotated-flipped.less
@@ -0,0 +1,20 @@
+// Rotated & Flipped Icons
+// -------------------------
+
+.@{fa-css-prefix}-rotate-90  { .fa-icon-rotate(90deg, 1);  }
+.@{fa-css-prefix}-rotate-180 { .fa-icon-rotate(180deg, 2); }
+.@{fa-css-prefix}-rotate-270 { .fa-icon-rotate(270deg, 3); }
+
+.@{fa-css-prefix}-flip-horizontal { .fa-icon-flip(-1, 1, 0); }
+.@{fa-css-prefix}-flip-vertical   { .fa-icon-flip(1, -1, 2); }
+
+// Hook for IE8-9
+// -------------------------
+
+:root .@{fa-css-prefix}-rotate-90,
+:root .@{fa-css-prefix}-rotate-180,
+:root .@{fa-css-prefix}-rotate-270,
+:root .@{fa-css-prefix}-flip-horizontal,
+:root .@{fa-css-prefix}-flip-vertical {
+  filter: none;
+}
diff --git a/themes/bootstrap3/less/vendor/font-awesome/screen-reader.less b/themes/bootstrap3/less/vendor/font-awesome/screen-reader.less
new file mode 100644
index 0000000000000000000000000000000000000000..11c188196d5ade55930b7b94fc5a1dd0f6d1a800
--- /dev/null
+++ b/themes/bootstrap3/less/vendor/font-awesome/screen-reader.less
@@ -0,0 +1,5 @@
+// Screen Readers
+// -------------------------
+
+.sr-only { .sr-only(); }
+.sr-only-focusable { .sr-only-focusable(); }
diff --git a/themes/bootstrap3/less/vendor/font-awesome/stacked.less b/themes/bootstrap3/less/vendor/font-awesome/stacked.less
new file mode 100644
index 0000000000000000000000000000000000000000..fc53fb0e7ab49594e1eac97208c32a290558efeb
--- /dev/null
+++ b/themes/bootstrap3/less/vendor/font-awesome/stacked.less
@@ -0,0 +1,20 @@
+// Stacked Icons
+// -------------------------
+
+.@{fa-css-prefix}-stack {
+  position: relative;
+  display: inline-block;
+  width: 2em;
+  height: 2em;
+  line-height: 2em;
+  vertical-align: middle;
+}
+.@{fa-css-prefix}-stack-1x, .@{fa-css-prefix}-stack-2x {
+  position: absolute;
+  left: 0;
+  width: 100%;
+  text-align: center;
+}
+.@{fa-css-prefix}-stack-1x { line-height: inherit; }
+.@{fa-css-prefix}-stack-2x { font-size: 2em; }
+.@{fa-css-prefix}-inverse { color: @fa-inverse; }
diff --git a/themes/bootstrap3/less/vendor/font-awesome/variables.less b/themes/bootstrap3/less/vendor/font-awesome/variables.less
new file mode 100644
index 0000000000000000000000000000000000000000..2b3381975e9e6ba8b0bf98ee86abf264ad158cb7
--- /dev/null
+++ b/themes/bootstrap3/less/vendor/font-awesome/variables.less
@@ -0,0 +1,744 @@
+// Variables
+// --------------------------
+
+@fa-font-path:        "../fonts";
+@fa-font-size-base:   14px;
+@fa-line-height-base: 1;
+//@fa-font-path:        "//netdna.bootstrapcdn.com/font-awesome/4.6.3/fonts"; // for referencing Bootstrap CDN font files directly
+@fa-css-prefix:       fa;
+@fa-version:          "4.6.3";
+@fa-border-color:     #eee;
+@fa-inverse:          #fff;
+@fa-li-width:         (30em / 14);
+
+@fa-var-500px: "\f26e";
+@fa-var-adjust: "\f042";
+@fa-var-adn: "\f170";
+@fa-var-align-center: "\f037";
+@fa-var-align-justify: "\f039";
+@fa-var-align-left: "\f036";
+@fa-var-align-right: "\f038";
+@fa-var-amazon: "\f270";
+@fa-var-ambulance: "\f0f9";
+@fa-var-american-sign-language-interpreting: "\f2a3";
+@fa-var-anchor: "\f13d";
+@fa-var-android: "\f17b";
+@fa-var-angellist: "\f209";
+@fa-var-angle-double-down: "\f103";
+@fa-var-angle-double-left: "\f100";
+@fa-var-angle-double-right: "\f101";
+@fa-var-angle-double-up: "\f102";
+@fa-var-angle-down: "\f107";
+@fa-var-angle-left: "\f104";
+@fa-var-angle-right: "\f105";
+@fa-var-angle-up: "\f106";
+@fa-var-apple: "\f179";
+@fa-var-archive: "\f187";
+@fa-var-area-chart: "\f1fe";
+@fa-var-arrow-circle-down: "\f0ab";
+@fa-var-arrow-circle-left: "\f0a8";
+@fa-var-arrow-circle-o-down: "\f01a";
+@fa-var-arrow-circle-o-left: "\f190";
+@fa-var-arrow-circle-o-right: "\f18e";
+@fa-var-arrow-circle-o-up: "\f01b";
+@fa-var-arrow-circle-right: "\f0a9";
+@fa-var-arrow-circle-up: "\f0aa";
+@fa-var-arrow-down: "\f063";
+@fa-var-arrow-left: "\f060";
+@fa-var-arrow-right: "\f061";
+@fa-var-arrow-up: "\f062";
+@fa-var-arrows: "\f047";
+@fa-var-arrows-alt: "\f0b2";
+@fa-var-arrows-h: "\f07e";
+@fa-var-arrows-v: "\f07d";
+@fa-var-asl-interpreting: "\f2a3";
+@fa-var-assistive-listening-systems: "\f2a2";
+@fa-var-asterisk: "\f069";
+@fa-var-at: "\f1fa";
+@fa-var-audio-description: "\f29e";
+@fa-var-automobile: "\f1b9";
+@fa-var-backward: "\f04a";
+@fa-var-balance-scale: "\f24e";
+@fa-var-ban: "\f05e";
+@fa-var-bank: "\f19c";
+@fa-var-bar-chart: "\f080";
+@fa-var-bar-chart-o: "\f080";
+@fa-var-barcode: "\f02a";
+@fa-var-bars: "\f0c9";
+@fa-var-battery-0: "\f244";
+@fa-var-battery-1: "\f243";
+@fa-var-battery-2: "\f242";
+@fa-var-battery-3: "\f241";
+@fa-var-battery-4: "\f240";
+@fa-var-battery-empty: "\f244";
+@fa-var-battery-full: "\f240";
+@fa-var-battery-half: "\f242";
+@fa-var-battery-quarter: "\f243";
+@fa-var-battery-three-quarters: "\f241";
+@fa-var-bed: "\f236";
+@fa-var-beer: "\f0fc";
+@fa-var-behance: "\f1b4";
+@fa-var-behance-square: "\f1b5";
+@fa-var-bell: "\f0f3";
+@fa-var-bell-o: "\f0a2";
+@fa-var-bell-slash: "\f1f6";
+@fa-var-bell-slash-o: "\f1f7";
+@fa-var-bicycle: "\f206";
+@fa-var-binoculars: "\f1e5";
+@fa-var-birthday-cake: "\f1fd";
+@fa-var-bitbucket: "\f171";
+@fa-var-bitbucket-square: "\f172";
+@fa-var-bitcoin: "\f15a";
+@fa-var-black-tie: "\f27e";
+@fa-var-blind: "\f29d";
+@fa-var-bluetooth: "\f293";
+@fa-var-bluetooth-b: "\f294";
+@fa-var-bold: "\f032";
+@fa-var-bolt: "\f0e7";
+@fa-var-bomb: "\f1e2";
+@fa-var-book: "\f02d";
+@fa-var-bookmark: "\f02e";
+@fa-var-bookmark-o: "\f097";
+@fa-var-braille: "\f2a1";
+@fa-var-briefcase: "\f0b1";
+@fa-var-btc: "\f15a";
+@fa-var-bug: "\f188";
+@fa-var-building: "\f1ad";
+@fa-var-building-o: "\f0f7";
+@fa-var-bullhorn: "\f0a1";
+@fa-var-bullseye: "\f140";
+@fa-var-bus: "\f207";
+@fa-var-buysellads: "\f20d";
+@fa-var-cab: "\f1ba";
+@fa-var-calculator: "\f1ec";
+@fa-var-calendar: "\f073";
+@fa-var-calendar-check-o: "\f274";
+@fa-var-calendar-minus-o: "\f272";
+@fa-var-calendar-o: "\f133";
+@fa-var-calendar-plus-o: "\f271";
+@fa-var-calendar-times-o: "\f273";
+@fa-var-camera: "\f030";
+@fa-var-camera-retro: "\f083";
+@fa-var-car: "\f1b9";
+@fa-var-caret-down: "\f0d7";
+@fa-var-caret-left: "\f0d9";
+@fa-var-caret-right: "\f0da";
+@fa-var-caret-square-o-down: "\f150";
+@fa-var-caret-square-o-left: "\f191";
+@fa-var-caret-square-o-right: "\f152";
+@fa-var-caret-square-o-up: "\f151";
+@fa-var-caret-up: "\f0d8";
+@fa-var-cart-arrow-down: "\f218";
+@fa-var-cart-plus: "\f217";
+@fa-var-cc: "\f20a";
+@fa-var-cc-amex: "\f1f3";
+@fa-var-cc-diners-club: "\f24c";
+@fa-var-cc-discover: "\f1f2";
+@fa-var-cc-jcb: "\f24b";
+@fa-var-cc-mastercard: "\f1f1";
+@fa-var-cc-paypal: "\f1f4";
+@fa-var-cc-stripe: "\f1f5";
+@fa-var-cc-visa: "\f1f0";
+@fa-var-certificate: "\f0a3";
+@fa-var-chain: "\f0c1";
+@fa-var-chain-broken: "\f127";
+@fa-var-check: "\f00c";
+@fa-var-check-circle: "\f058";
+@fa-var-check-circle-o: "\f05d";
+@fa-var-check-square: "\f14a";
+@fa-var-check-square-o: "\f046";
+@fa-var-chevron-circle-down: "\f13a";
+@fa-var-chevron-circle-left: "\f137";
+@fa-var-chevron-circle-right: "\f138";
+@fa-var-chevron-circle-up: "\f139";
+@fa-var-chevron-down: "\f078";
+@fa-var-chevron-left: "\f053";
+@fa-var-chevron-right: "\f054";
+@fa-var-chevron-up: "\f077";
+@fa-var-child: "\f1ae";
+@fa-var-chrome: "\f268";
+@fa-var-circle: "\f111";
+@fa-var-circle-o: "\f10c";
+@fa-var-circle-o-notch: "\f1ce";
+@fa-var-circle-thin: "\f1db";
+@fa-var-clipboard: "\f0ea";
+@fa-var-clock-o: "\f017";
+@fa-var-clone: "\f24d";
+@fa-var-close: "\f00d";
+@fa-var-cloud: "\f0c2";
+@fa-var-cloud-download: "\f0ed";
+@fa-var-cloud-upload: "\f0ee";
+@fa-var-cny: "\f157";
+@fa-var-code: "\f121";
+@fa-var-code-fork: "\f126";
+@fa-var-codepen: "\f1cb";
+@fa-var-codiepie: "\f284";
+@fa-var-coffee: "\f0f4";
+@fa-var-cog: "\f013";
+@fa-var-cogs: "\f085";
+@fa-var-columns: "\f0db";
+@fa-var-comment: "\f075";
+@fa-var-comment-o: "\f0e5";
+@fa-var-commenting: "\f27a";
+@fa-var-commenting-o: "\f27b";
+@fa-var-comments: "\f086";
+@fa-var-comments-o: "\f0e6";
+@fa-var-compass: "\f14e";
+@fa-var-compress: "\f066";
+@fa-var-connectdevelop: "\f20e";
+@fa-var-contao: "\f26d";
+@fa-var-copy: "\f0c5";
+@fa-var-copyright: "\f1f9";
+@fa-var-creative-commons: "\f25e";
+@fa-var-credit-card: "\f09d";
+@fa-var-credit-card-alt: "\f283";
+@fa-var-crop: "\f125";
+@fa-var-crosshairs: "\f05b";
+@fa-var-css3: "\f13c";
+@fa-var-cube: "\f1b2";
+@fa-var-cubes: "\f1b3";
+@fa-var-cut: "\f0c4";
+@fa-var-cutlery: "\f0f5";
+@fa-var-dashboard: "\f0e4";
+@fa-var-dashcube: "\f210";
+@fa-var-database: "\f1c0";
+@fa-var-deaf: "\f2a4";
+@fa-var-deafness: "\f2a4";
+@fa-var-dedent: "\f03b";
+@fa-var-delicious: "\f1a5";
+@fa-var-desktop: "\f108";
+@fa-var-deviantart: "\f1bd";
+@fa-var-diamond: "\f219";
+@fa-var-digg: "\f1a6";
+@fa-var-dollar: "\f155";
+@fa-var-dot-circle-o: "\f192";
+@fa-var-download: "\f019";
+@fa-var-dribbble: "\f17d";
+@fa-var-dropbox: "\f16b";
+@fa-var-drupal: "\f1a9";
+@fa-var-edge: "\f282";
+@fa-var-edit: "\f044";
+@fa-var-eject: "\f052";
+@fa-var-ellipsis-h: "\f141";
+@fa-var-ellipsis-v: "\f142";
+@fa-var-empire: "\f1d1";
+@fa-var-envelope: "\f0e0";
+@fa-var-envelope-o: "\f003";
+@fa-var-envelope-square: "\f199";
+@fa-var-envira: "\f299";
+@fa-var-eraser: "\f12d";
+@fa-var-eur: "\f153";
+@fa-var-euro: "\f153";
+@fa-var-exchange: "\f0ec";
+@fa-var-exclamation: "\f12a";
+@fa-var-exclamation-circle: "\f06a";
+@fa-var-exclamation-triangle: "\f071";
+@fa-var-expand: "\f065";
+@fa-var-expeditedssl: "\f23e";
+@fa-var-external-link: "\f08e";
+@fa-var-external-link-square: "\f14c";
+@fa-var-eye: "\f06e";
+@fa-var-eye-slash: "\f070";
+@fa-var-eyedropper: "\f1fb";
+@fa-var-fa: "\f2b4";
+@fa-var-facebook: "\f09a";
+@fa-var-facebook-f: "\f09a";
+@fa-var-facebook-official: "\f230";
+@fa-var-facebook-square: "\f082";
+@fa-var-fast-backward: "\f049";
+@fa-var-fast-forward: "\f050";
+@fa-var-fax: "\f1ac";
+@fa-var-feed: "\f09e";
+@fa-var-female: "\f182";
+@fa-var-fighter-jet: "\f0fb";
+@fa-var-file: "\f15b";
+@fa-var-file-archive-o: "\f1c6";
+@fa-var-file-audio-o: "\f1c7";
+@fa-var-file-code-o: "\f1c9";
+@fa-var-file-excel-o: "\f1c3";
+@fa-var-file-image-o: "\f1c5";
+@fa-var-file-movie-o: "\f1c8";
+@fa-var-file-o: "\f016";
+@fa-var-file-pdf-o: "\f1c1";
+@fa-var-file-photo-o: "\f1c5";
+@fa-var-file-picture-o: "\f1c5";
+@fa-var-file-powerpoint-o: "\f1c4";
+@fa-var-file-sound-o: "\f1c7";
+@fa-var-file-text: "\f15c";
+@fa-var-file-text-o: "\f0f6";
+@fa-var-file-video-o: "\f1c8";
+@fa-var-file-word-o: "\f1c2";
+@fa-var-file-zip-o: "\f1c6";
+@fa-var-files-o: "\f0c5";
+@fa-var-film: "\f008";
+@fa-var-filter: "\f0b0";
+@fa-var-fire: "\f06d";
+@fa-var-fire-extinguisher: "\f134";
+@fa-var-firefox: "\f269";
+@fa-var-first-order: "\f2b0";
+@fa-var-flag: "\f024";
+@fa-var-flag-checkered: "\f11e";
+@fa-var-flag-o: "\f11d";
+@fa-var-flash: "\f0e7";
+@fa-var-flask: "\f0c3";
+@fa-var-flickr: "\f16e";
+@fa-var-floppy-o: "\f0c7";
+@fa-var-folder: "\f07b";
+@fa-var-folder-o: "\f114";
+@fa-var-folder-open: "\f07c";
+@fa-var-folder-open-o: "\f115";
+@fa-var-font: "\f031";
+@fa-var-font-awesome: "\f2b4";
+@fa-var-fonticons: "\f280";
+@fa-var-fort-awesome: "\f286";
+@fa-var-forumbee: "\f211";
+@fa-var-forward: "\f04e";
+@fa-var-foursquare: "\f180";
+@fa-var-frown-o: "\f119";
+@fa-var-futbol-o: "\f1e3";
+@fa-var-gamepad: "\f11b";
+@fa-var-gavel: "\f0e3";
+@fa-var-gbp: "\f154";
+@fa-var-ge: "\f1d1";
+@fa-var-gear: "\f013";
+@fa-var-gears: "\f085";
+@fa-var-genderless: "\f22d";
+@fa-var-get-pocket: "\f265";
+@fa-var-gg: "\f260";
+@fa-var-gg-circle: "\f261";
+@fa-var-gift: "\f06b";
+@fa-var-git: "\f1d3";
+@fa-var-git-square: "\f1d2";
+@fa-var-github: "\f09b";
+@fa-var-github-alt: "\f113";
+@fa-var-github-square: "\f092";
+@fa-var-gitlab: "\f296";
+@fa-var-gittip: "\f184";
+@fa-var-glass: "\f000";
+@fa-var-glide: "\f2a5";
+@fa-var-glide-g: "\f2a6";
+@fa-var-globe: "\f0ac";
+@fa-var-google: "\f1a0";
+@fa-var-google-plus: "\f0d5";
+@fa-var-google-plus-circle: "\f2b3";
+@fa-var-google-plus-official: "\f2b3";
+@fa-var-google-plus-square: "\f0d4";
+@fa-var-google-wallet: "\f1ee";
+@fa-var-graduation-cap: "\f19d";
+@fa-var-gratipay: "\f184";
+@fa-var-group: "\f0c0";
+@fa-var-h-square: "\f0fd";
+@fa-var-hacker-news: "\f1d4";
+@fa-var-hand-grab-o: "\f255";
+@fa-var-hand-lizard-o: "\f258";
+@fa-var-hand-o-down: "\f0a7";
+@fa-var-hand-o-left: "\f0a5";
+@fa-var-hand-o-right: "\f0a4";
+@fa-var-hand-o-up: "\f0a6";
+@fa-var-hand-paper-o: "\f256";
+@fa-var-hand-peace-o: "\f25b";
+@fa-var-hand-pointer-o: "\f25a";
+@fa-var-hand-rock-o: "\f255";
+@fa-var-hand-scissors-o: "\f257";
+@fa-var-hand-spock-o: "\f259";
+@fa-var-hand-stop-o: "\f256";
+@fa-var-hard-of-hearing: "\f2a4";
+@fa-var-hashtag: "\f292";
+@fa-var-hdd-o: "\f0a0";
+@fa-var-header: "\f1dc";
+@fa-var-headphones: "\f025";
+@fa-var-heart: "\f004";
+@fa-var-heart-o: "\f08a";
+@fa-var-heartbeat: "\f21e";
+@fa-var-history: "\f1da";
+@fa-var-home: "\f015";
+@fa-var-hospital-o: "\f0f8";
+@fa-var-hotel: "\f236";
+@fa-var-hourglass: "\f254";
+@fa-var-hourglass-1: "\f251";
+@fa-var-hourglass-2: "\f252";
+@fa-var-hourglass-3: "\f253";
+@fa-var-hourglass-end: "\f253";
+@fa-var-hourglass-half: "\f252";
+@fa-var-hourglass-o: "\f250";
+@fa-var-hourglass-start: "\f251";
+@fa-var-houzz: "\f27c";
+@fa-var-html5: "\f13b";
+@fa-var-i-cursor: "\f246";
+@fa-var-ils: "\f20b";
+@fa-var-image: "\f03e";
+@fa-var-inbox: "\f01c";
+@fa-var-indent: "\f03c";
+@fa-var-industry: "\f275";
+@fa-var-info: "\f129";
+@fa-var-info-circle: "\f05a";
+@fa-var-inr: "\f156";
+@fa-var-instagram: "\f16d";
+@fa-var-institution: "\f19c";
+@fa-var-internet-explorer: "\f26b";
+@fa-var-intersex: "\f224";
+@fa-var-ioxhost: "\f208";
+@fa-var-italic: "\f033";
+@fa-var-joomla: "\f1aa";
+@fa-var-jpy: "\f157";
+@fa-var-jsfiddle: "\f1cc";
+@fa-var-key: "\f084";
+@fa-var-keyboard-o: "\f11c";
+@fa-var-krw: "\f159";
+@fa-var-language: "\f1ab";
+@fa-var-laptop: "\f109";
+@fa-var-lastfm: "\f202";
+@fa-var-lastfm-square: "\f203";
+@fa-var-leaf: "\f06c";
+@fa-var-leanpub: "\f212";
+@fa-var-legal: "\f0e3";
+@fa-var-lemon-o: "\f094";
+@fa-var-level-down: "\f149";
+@fa-var-level-up: "\f148";
+@fa-var-life-bouy: "\f1cd";
+@fa-var-life-buoy: "\f1cd";
+@fa-var-life-ring: "\f1cd";
+@fa-var-life-saver: "\f1cd";
+@fa-var-lightbulb-o: "\f0eb";
+@fa-var-line-chart: "\f201";
+@fa-var-link: "\f0c1";
+@fa-var-linkedin: "\f0e1";
+@fa-var-linkedin-square: "\f08c";
+@fa-var-linux: "\f17c";
+@fa-var-list: "\f03a";
+@fa-var-list-alt: "\f022";
+@fa-var-list-ol: "\f0cb";
+@fa-var-list-ul: "\f0ca";
+@fa-var-location-arrow: "\f124";
+@fa-var-lock: "\f023";
+@fa-var-long-arrow-down: "\f175";
+@fa-var-long-arrow-left: "\f177";
+@fa-var-long-arrow-right: "\f178";
+@fa-var-long-arrow-up: "\f176";
+@fa-var-low-vision: "\f2a8";
+@fa-var-magic: "\f0d0";
+@fa-var-magnet: "\f076";
+@fa-var-mail-forward: "\f064";
+@fa-var-mail-reply: "\f112";
+@fa-var-mail-reply-all: "\f122";
+@fa-var-male: "\f183";
+@fa-var-map: "\f279";
+@fa-var-map-marker: "\f041";
+@fa-var-map-o: "\f278";
+@fa-var-map-pin: "\f276";
+@fa-var-map-signs: "\f277";
+@fa-var-mars: "\f222";
+@fa-var-mars-double: "\f227";
+@fa-var-mars-stroke: "\f229";
+@fa-var-mars-stroke-h: "\f22b";
+@fa-var-mars-stroke-v: "\f22a";
+@fa-var-maxcdn: "\f136";
+@fa-var-meanpath: "\f20c";
+@fa-var-medium: "\f23a";
+@fa-var-medkit: "\f0fa";
+@fa-var-meh-o: "\f11a";
+@fa-var-mercury: "\f223";
+@fa-var-microphone: "\f130";
+@fa-var-microphone-slash: "\f131";
+@fa-var-minus: "\f068";
+@fa-var-minus-circle: "\f056";
+@fa-var-minus-square: "\f146";
+@fa-var-minus-square-o: "\f147";
+@fa-var-mixcloud: "\f289";
+@fa-var-mobile: "\f10b";
+@fa-var-mobile-phone: "\f10b";
+@fa-var-modx: "\f285";
+@fa-var-money: "\f0d6";
+@fa-var-moon-o: "\f186";
+@fa-var-mortar-board: "\f19d";
+@fa-var-motorcycle: "\f21c";
+@fa-var-mouse-pointer: "\f245";
+@fa-var-music: "\f001";
+@fa-var-navicon: "\f0c9";
+@fa-var-neuter: "\f22c";
+@fa-var-newspaper-o: "\f1ea";
+@fa-var-object-group: "\f247";
+@fa-var-object-ungroup: "\f248";
+@fa-var-odnoklassniki: "\f263";
+@fa-var-odnoklassniki-square: "\f264";
+@fa-var-opencart: "\f23d";
+@fa-var-openid: "\f19b";
+@fa-var-opera: "\f26a";
+@fa-var-optin-monster: "\f23c";
+@fa-var-outdent: "\f03b";
+@fa-var-pagelines: "\f18c";
+@fa-var-paint-brush: "\f1fc";
+@fa-var-paper-plane: "\f1d8";
+@fa-var-paper-plane-o: "\f1d9";
+@fa-var-paperclip: "\f0c6";
+@fa-var-paragraph: "\f1dd";
+@fa-var-paste: "\f0ea";
+@fa-var-pause: "\f04c";
+@fa-var-pause-circle: "\f28b";
+@fa-var-pause-circle-o: "\f28c";
+@fa-var-paw: "\f1b0";
+@fa-var-paypal: "\f1ed";
+@fa-var-pencil: "\f040";
+@fa-var-pencil-square: "\f14b";
+@fa-var-pencil-square-o: "\f044";
+@fa-var-percent: "\f295";
+@fa-var-phone: "\f095";
+@fa-var-phone-square: "\f098";
+@fa-var-photo: "\f03e";
+@fa-var-picture-o: "\f03e";
+@fa-var-pie-chart: "\f200";
+@fa-var-pied-piper: "\f2ae";
+@fa-var-pied-piper-alt: "\f1a8";
+@fa-var-pied-piper-pp: "\f1a7";
+@fa-var-pinterest: "\f0d2";
+@fa-var-pinterest-p: "\f231";
+@fa-var-pinterest-square: "\f0d3";
+@fa-var-plane: "\f072";
+@fa-var-play: "\f04b";
+@fa-var-play-circle: "\f144";
+@fa-var-play-circle-o: "\f01d";
+@fa-var-plug: "\f1e6";
+@fa-var-plus: "\f067";
+@fa-var-plus-circle: "\f055";
+@fa-var-plus-square: "\f0fe";
+@fa-var-plus-square-o: "\f196";
+@fa-var-power-off: "\f011";
+@fa-var-print: "\f02f";
+@fa-var-product-hunt: "\f288";
+@fa-var-puzzle-piece: "\f12e";
+@fa-var-qq: "\f1d6";
+@fa-var-qrcode: "\f029";
+@fa-var-question: "\f128";
+@fa-var-question-circle: "\f059";
+@fa-var-question-circle-o: "\f29c";
+@fa-var-quote-left: "\f10d";
+@fa-var-quote-right: "\f10e";
+@fa-var-ra: "\f1d0";
+@fa-var-random: "\f074";
+@fa-var-rebel: "\f1d0";
+@fa-var-recycle: "\f1b8";
+@fa-var-reddit: "\f1a1";
+@fa-var-reddit-alien: "\f281";
+@fa-var-reddit-square: "\f1a2";
+@fa-var-refresh: "\f021";
+@fa-var-registered: "\f25d";
+@fa-var-remove: "\f00d";
+@fa-var-renren: "\f18b";
+@fa-var-reorder: "\f0c9";
+@fa-var-repeat: "\f01e";
+@fa-var-reply: "\f112";
+@fa-var-reply-all: "\f122";
+@fa-var-resistance: "\f1d0";
+@fa-var-retweet: "\f079";
+@fa-var-rmb: "\f157";
+@fa-var-road: "\f018";
+@fa-var-rocket: "\f135";
+@fa-var-rotate-left: "\f0e2";
+@fa-var-rotate-right: "\f01e";
+@fa-var-rouble: "\f158";
+@fa-var-rss: "\f09e";
+@fa-var-rss-square: "\f143";
+@fa-var-rub: "\f158";
+@fa-var-ruble: "\f158";
+@fa-var-rupee: "\f156";
+@fa-var-safari: "\f267";
+@fa-var-save: "\f0c7";
+@fa-var-scissors: "\f0c4";
+@fa-var-scribd: "\f28a";
+@fa-var-search: "\f002";
+@fa-var-search-minus: "\f010";
+@fa-var-search-plus: "\f00e";
+@fa-var-sellsy: "\f213";
+@fa-var-send: "\f1d8";
+@fa-var-send-o: "\f1d9";
+@fa-var-server: "\f233";
+@fa-var-share: "\f064";
+@fa-var-share-alt: "\f1e0";
+@fa-var-share-alt-square: "\f1e1";
+@fa-var-share-square: "\f14d";
+@fa-var-share-square-o: "\f045";
+@fa-var-shekel: "\f20b";
+@fa-var-sheqel: "\f20b";
+@fa-var-shield: "\f132";
+@fa-var-ship: "\f21a";
+@fa-var-shirtsinbulk: "\f214";
+@fa-var-shopping-bag: "\f290";
+@fa-var-shopping-basket: "\f291";
+@fa-var-shopping-cart: "\f07a";
+@fa-var-sign-in: "\f090";
+@fa-var-sign-language: "\f2a7";
+@fa-var-sign-out: "\f08b";
+@fa-var-signal: "\f012";
+@fa-var-signing: "\f2a7";
+@fa-var-simplybuilt: "\f215";
+@fa-var-sitemap: "\f0e8";
+@fa-var-skyatlas: "\f216";
+@fa-var-skype: "\f17e";
+@fa-var-slack: "\f198";
+@fa-var-sliders: "\f1de";
+@fa-var-slideshare: "\f1e7";
+@fa-var-smile-o: "\f118";
+@fa-var-snapchat: "\f2ab";
+@fa-var-snapchat-ghost: "\f2ac";
+@fa-var-snapchat-square: "\f2ad";
+@fa-var-soccer-ball-o: "\f1e3";
+@fa-var-sort: "\f0dc";
+@fa-var-sort-alpha-asc: "\f15d";
+@fa-var-sort-alpha-desc: "\f15e";
+@fa-var-sort-amount-asc: "\f160";
+@fa-var-sort-amount-desc: "\f161";
+@fa-var-sort-asc: "\f0de";
+@fa-var-sort-desc: "\f0dd";
+@fa-var-sort-down: "\f0dd";
+@fa-var-sort-numeric-asc: "\f162";
+@fa-var-sort-numeric-desc: "\f163";
+@fa-var-sort-up: "\f0de";
+@fa-var-soundcloud: "\f1be";
+@fa-var-space-shuttle: "\f197";
+@fa-var-spinner: "\f110";
+@fa-var-spoon: "\f1b1";
+@fa-var-spotify: "\f1bc";
+@fa-var-square: "\f0c8";
+@fa-var-square-o: "\f096";
+@fa-var-stack-exchange: "\f18d";
+@fa-var-stack-overflow: "\f16c";
+@fa-var-star: "\f005";
+@fa-var-star-half: "\f089";
+@fa-var-star-half-empty: "\f123";
+@fa-var-star-half-full: "\f123";
+@fa-var-star-half-o: "\f123";
+@fa-var-star-o: "\f006";
+@fa-var-steam: "\f1b6";
+@fa-var-steam-square: "\f1b7";
+@fa-var-step-backward: "\f048";
+@fa-var-step-forward: "\f051";
+@fa-var-stethoscope: "\f0f1";
+@fa-var-sticky-note: "\f249";
+@fa-var-sticky-note-o: "\f24a";
+@fa-var-stop: "\f04d";
+@fa-var-stop-circle: "\f28d";
+@fa-var-stop-circle-o: "\f28e";
+@fa-var-street-view: "\f21d";
+@fa-var-strikethrough: "\f0cc";
+@fa-var-stumbleupon: "\f1a4";
+@fa-var-stumbleupon-circle: "\f1a3";
+@fa-var-subscript: "\f12c";
+@fa-var-subway: "\f239";
+@fa-var-suitcase: "\f0f2";
+@fa-var-sun-o: "\f185";
+@fa-var-superscript: "\f12b";
+@fa-var-support: "\f1cd";
+@fa-var-table: "\f0ce";
+@fa-var-tablet: "\f10a";
+@fa-var-tachometer: "\f0e4";
+@fa-var-tag: "\f02b";
+@fa-var-tags: "\f02c";
+@fa-var-tasks: "\f0ae";
+@fa-var-taxi: "\f1ba";
+@fa-var-television: "\f26c";
+@fa-var-tencent-weibo: "\f1d5";
+@fa-var-terminal: "\f120";
+@fa-var-text-height: "\f034";
+@fa-var-text-width: "\f035";
+@fa-var-th: "\f00a";
+@fa-var-th-large: "\f009";
+@fa-var-th-list: "\f00b";
+@fa-var-themeisle: "\f2b2";
+@fa-var-thumb-tack: "\f08d";
+@fa-var-thumbs-down: "\f165";
+@fa-var-thumbs-o-down: "\f088";
+@fa-var-thumbs-o-up: "\f087";
+@fa-var-thumbs-up: "\f164";
+@fa-var-ticket: "\f145";
+@fa-var-times: "\f00d";
+@fa-var-times-circle: "\f057";
+@fa-var-times-circle-o: "\f05c";
+@fa-var-tint: "\f043";
+@fa-var-toggle-down: "\f150";
+@fa-var-toggle-left: "\f191";
+@fa-var-toggle-off: "\f204";
+@fa-var-toggle-on: "\f205";
+@fa-var-toggle-right: "\f152";
+@fa-var-toggle-up: "\f151";
+@fa-var-trademark: "\f25c";
+@fa-var-train: "\f238";
+@fa-var-transgender: "\f224";
+@fa-var-transgender-alt: "\f225";
+@fa-var-trash: "\f1f8";
+@fa-var-trash-o: "\f014";
+@fa-var-tree: "\f1bb";
+@fa-var-trello: "\f181";
+@fa-var-tripadvisor: "\f262";
+@fa-var-trophy: "\f091";
+@fa-var-truck: "\f0d1";
+@fa-var-try: "\f195";
+@fa-var-tty: "\f1e4";
+@fa-var-tumblr: "\f173";
+@fa-var-tumblr-square: "\f174";
+@fa-var-turkish-lira: "\f195";
+@fa-var-tv: "\f26c";
+@fa-var-twitch: "\f1e8";
+@fa-var-twitter: "\f099";
+@fa-var-twitter-square: "\f081";
+@fa-var-umbrella: "\f0e9";
+@fa-var-underline: "\f0cd";
+@fa-var-undo: "\f0e2";
+@fa-var-universal-access: "\f29a";
+@fa-var-university: "\f19c";
+@fa-var-unlink: "\f127";
+@fa-var-unlock: "\f09c";
+@fa-var-unlock-alt: "\f13e";
+@fa-var-unsorted: "\f0dc";
+@fa-var-upload: "\f093";
+@fa-var-usb: "\f287";
+@fa-var-usd: "\f155";
+@fa-var-user: "\f007";
+@fa-var-user-md: "\f0f0";
+@fa-var-user-plus: "\f234";
+@fa-var-user-secret: "\f21b";
+@fa-var-user-times: "\f235";
+@fa-var-users: "\f0c0";
+@fa-var-venus: "\f221";
+@fa-var-venus-double: "\f226";
+@fa-var-venus-mars: "\f228";
+@fa-var-viacoin: "\f237";
+@fa-var-viadeo: "\f2a9";
+@fa-var-viadeo-square: "\f2aa";
+@fa-var-video-camera: "\f03d";
+@fa-var-vimeo: "\f27d";
+@fa-var-vimeo-square: "\f194";
+@fa-var-vine: "\f1ca";
+@fa-var-vk: "\f189";
+@fa-var-volume-control-phone: "\f2a0";
+@fa-var-volume-down: "\f027";
+@fa-var-volume-off: "\f026";
+@fa-var-volume-up: "\f028";
+@fa-var-warning: "\f071";
+@fa-var-wechat: "\f1d7";
+@fa-var-weibo: "\f18a";
+@fa-var-weixin: "\f1d7";
+@fa-var-whatsapp: "\f232";
+@fa-var-wheelchair: "\f193";
+@fa-var-wheelchair-alt: "\f29b";
+@fa-var-wifi: "\f1eb";
+@fa-var-wikipedia-w: "\f266";
+@fa-var-windows: "\f17a";
+@fa-var-won: "\f159";
+@fa-var-wordpress: "\f19a";
+@fa-var-wpbeginner: "\f297";
+@fa-var-wpforms: "\f298";
+@fa-var-wrench: "\f0ad";
+@fa-var-xing: "\f168";
+@fa-var-xing-square: "\f169";
+@fa-var-y-combinator: "\f23b";
+@fa-var-y-combinator-square: "\f1d4";
+@fa-var-yahoo: "\f19e";
+@fa-var-yc: "\f23b";
+@fa-var-yc-square: "\f1d4";
+@fa-var-yelp: "\f1e9";
+@fa-var-yen: "\f157";
+@fa-var-yoast: "\f2b1";
+@fa-var-youtube: "\f167";
+@fa-var-youtube-play: "\f16a";
+@fa-var-youtube-square: "\f166";
+
diff --git a/themes/bootstrap3/scss/bootstrap.scss b/themes/bootstrap3/scss/bootstrap.scss
new file mode 100644
index 0000000000000000000000000000000000000000..aad53f084f692285b9bc7a0c6cb7a72503eb8a6c
--- /dev/null
+++ b/themes/bootstrap3/scss/bootstrap.scss
@@ -0,0 +1,146 @@
+@import "vendor/bootstrap";
+$fa-font-path: "../../bootstrap3/css/fonts";
+@import "vendor/font-awesome/font-awesome";
+@import "vendor/bootstrap-accessibility/bootstrap-accessibility";
+@import "vendor/a11y";
+
+$fa-font-path: "../../../../../../themes/bootstrap3/css/fonts" !default;
+
+@import "components/advanced-search";
+@import "components/alphabrowse";
+@import "components/autocomplete";
+@import "components/icons";
+@import "components/js-tree";
+@import "components/lightbox";
+@import "components/offcanvas";
+@import "components/record";
+@import "components/search";
+@import "components/sidebar";
+@import "components/similar-carousel";
+@import "components/sliders";
+
+.alert.alert-info a {text-decoration: underline;}
+.btn.disabled {
+  &:active,
+  &:focus,
+  &:hover {color: #000;}
+}
+header .dropdown form { display: none; }
+.list-unstyled {margin: 0;}
+.highlight,mark {
+  background: lighten(#FF0, 20%);
+  padding: .1em .2em;
+}
+.icon-bar {background-color: #888;}
+img {max-width: 100%;}
+.popover {width: 250px;}
+.sub-breadcrumb {
+  padding: 5px 10px;
+  white-space: nowrap;
+  li {display: inline-block;}
+  li + li:before {
+    padding-left: 5px;
+    padding-right: 5px;
+    color: $breadcrumb-color;
+    content: "#{$breadcrumb-separator}\00a0";
+  }
+}
+.tab-content {padding: 4px;}
+
+@media (max-width: 991px) {
+  header .container.navbar {margin-bottom: 0;}
+  .searchForm {margin-top: 0;}
+}
+@media (min-width: 768px) {
+  h2 {
+    font-size: 23px;
+    font-weight: normal;
+  }
+  h3 {
+    font-size: 20px;
+    font-weight: normal;
+  }
+  .form-control { max-width: 400px; }
+}
+@media (max-width: 767px) {
+  h2 { font-size: 20px; }
+  h3 { font-size: 16px; }
+  .searchForm {padding-top: 0;}
+}
+
+/* --- Form Errors --- */
+.has-error {margin-bottom: 0;}
+.sms-error {@extend .has-error;}
+.help-block.with-errors {
+  margin: 0;
+  padding-top: $padding-base-vertical;
+  padding-bottom: $padding-base-vertical;
+  &:empty {padding: 0;}
+}
+
+/* --- Badges - blend the links in --- */
+.badge a {color: #fff;}
+
+/* --- Browse --- */
+.browse.list-group .list-group-item {
+  word-wrap: break-word;
+  &.view-record {
+    padding: 2px 4px;
+    font-size: 85%;
+    text-align: right;
+    border-top: 0;
+  }
+}
+
+/* --- Cart --- */
+.cart-controls .checkbox {
+  line-height: 2.5em;
+  padding-right: 1em;
+}
+
+/* --- PubDateVis --- */
+#dateVisColorSettings {
+  background-color: #fff; // background of box
+  fill: rgb(234,234,234); // fillColor
+  outline-color: #c38835; // selection color
+  stroke: $brand-primary; // color
+}
+
+/* --- Table wrapping to prevent horizontal overflow --- */
+.table {
+  table-layout: fixed;
+  word-wrap: break-word;
+}
+
+/* --- Visualization View --- */
+.node {
+  position: absolute;
+  box-sizing: content-box;
+  margin: -1px;
+  overflow: hidden;
+  font: 10px sans-serif;
+  line-height: 12px;
+  border: 1px solid white;
+}
+.node div {margin-top: 0px;}
+.toplevel {border: 2px solid black;}
+.node .label {
+  position: absolute;
+  bottom: 0;
+  left: 0;
+  min-height: 1px;
+  padding: 2px 4px;
+  font-size: 85%;
+  background-color: rgba(0,0,0,.5);
+  border-radius: 0;
+  text-shadow: none;
+}
+.notalabel {color: #000;}
+#viz-instructions {padding-top: 600px;}
+
+span[class^="services-"], span[class*=" services-"] span::before {
+  content: ", ";
+}
+span[class^="services-"], span[class*=" services-"] span:first-of-type::before {
+  content: "";
+}
diff --git a/themes/bootstrap3/scss/compiled.scss b/themes/bootstrap3/scss/compiled.scss
new file mode 100644
index 0000000000000000000000000000000000000000..cbd46a7afcf4100bb70bf9028c89faed85e46397
--- /dev/null
+++ b/themes/bootstrap3/scss/compiled.scss
@@ -0,0 +1 @@
+@import "bootstrap";
diff --git a/themes/bootstrap3/scss/components/advanced-search.scss b/themes/bootstrap3/scss/components/advanced-search.scss
new file mode 100644
index 0000000000000000000000000000000000000000..1f65e9da23ab43456797e9dc4b8dae226bea9376
--- /dev/null
+++ b/themes/bootstrap3/scss/components/advanced-search.scss
@@ -0,0 +1,51 @@
+.group {
+  position: relative;
+  background: $gray-lighter;
+  border-radius: $border-radius-base;
+  border: 1px solid darken($gray-lighter, 15%);
+  margin-top: 0;
+  margin-bottom: .5em !important;
+  .add_search_link {
+    display: inline-block;
+    margin-top: 4px;
+  }
+  & .group-close {
+    @extend .close;
+    position: absolute;
+    top: .3em;
+    right: .5em;
+    opacity: .4;
+    z-index: 2;
+  }
+  .search {
+    margin-bottom: 2px;
+    .close {opacity: .8;}
+  }
+  @media (min-width: 768px) {
+    padding: 10px 10px 10px 25px;
+    [class^=col-] {padding-left: 0;}
+  }
+  @media (max-width: 767px) {
+    .search  .middle {
+      float: left;
+      width: 90%;
+    }
+    & .group-close {
+      top: .5em;
+      right: 1em;
+      opacity: .6;
+    }
+  }
+  @media (max-width: 991px) {
+    .form-control {max-width: none;}
+  }
+}
+#groupPlaceHolder {
+  display:block;
+  padding:6px;
+}
+.template-dir-eds.template-name-advanced {
+  legend {margin-bottom: 0;}
+  .no-js .group:nth-child(n+3) {display: none;}
+  .search .close a {margin-left: -2em;}
+}
diff --git a/themes/bootstrap3/scss/components/alphabrowse.scss b/themes/bootstrap3/scss/components/alphabrowse.scss
new file mode 100644
index 0000000000000000000000000000000000000000..672066778870d7928f5ea6468c5bd4f1a173314e
--- /dev/null
+++ b/themes/bootstrap3/scss/components/alphabrowse.scss
@@ -0,0 +1,15 @@
+.alphabrowse {
+  border-collapse: separate;
+  .lcc {width: 20%;}
+  .titles {
+    width: 10%;
+    text-align: center;
+  }
+  /* highlighting the row makes ff bugs; operate on its children */
+  tr.browse-match td {
+    border-top: .2em solid $brand-primary;
+    border-bottom: .2em solid $brand-primary;
+    &:first-child {border-left: .2em solid $brand-primary;}
+    &:last-child {border-right: .2em solid $brand-primary;}
+  }
+}
diff --git a/themes/bootstrap3/scss/components/autocomplete.scss b/themes/bootstrap3/scss/components/autocomplete.scss
new file mode 100644
index 0000000000000000000000000000000000000000..1841dec17fc484dbd3ab1bbd0e2c1c83768ac2ca
--- /dev/null
+++ b/themes/bootstrap3/scss/components/autocomplete.scss
@@ -0,0 +1,39 @@
+/* crhallberg/autocomplete.js 0.14 */
+$autocomplete-item-bg   : #fff !default;
+$autocomplete-active-bg : $brand-primary !default;
+$autocomplete-hover-bg  : lighten($brand-primary, 60%) !default;
+$autocomplete-border    : lightgray !default;
+$autocomplete-secondary : darkgray !default; // item description
+
+.autocomplete-results {
+  position: absolute;
+  margin: 0;
+  margin-top: 2px;
+  padding: 0;
+  border: 1px solid $autocomplete-border;
+  background-color: $autocomplete-item-bg;
+  border-radius: $border-radius-base;
+  overflow: hidden;
+  z-index: 50;
+
+  .item {
+    display: block;
+    margin: 0;
+    padding: .75rem 1.25rem;
+    border-bottom: 1px solid $autocomplete-border;
+    cursor: pointer;
+
+    &:last-child { border: 0; }
+    &:hover { background-color: $autocomplete-hover-bg; }
+    &.loading { background-color: $autocomplete-item-bg; }
+    &.selected {
+      background-color: $autocomplete-active-bg;
+      color: contrast($autocomplete-active-bg);
+    }
+
+    small {
+      display: block;
+      color: $autocomplete-secondary;
+    }
+  }
+}
diff --git a/themes/bootstrap3/scss/components/icons.scss b/themes/bootstrap3/scss/components/icons.scss
new file mode 100644
index 0000000000000000000000000000000000000000..f706cd44692f4678b7f445d399403e513cf9f27e
--- /dev/null
+++ b/themes/bootstrap3/scss/components/icons.scss
@@ -0,0 +1,53 @@
+
+// Search Icons
+.fa-grid:before   {content: "\f00a";} // .fa-th
+.fa-visual:before {content: "\f008";} // .fa-film
+// Type Icons
+.fa-x:before              {content: "\f0f6";} // .fa-file-text-o
+.fa-atlas:before          {content: "\f14e";} // .fa-compass
+.fa-book:before           {content: "\f02d";} // .fa-book
+.fa-braille:before        {content: "\f0a6";} // .fa-hand-o-up
+.fa-cdrom:before          {content: "\f109";} // .fa-laptop
+.fa-chart:before          {content: "\f012";} // .fa-signal
+.fa-chipcartridge:before  {content: "\f109";} // .fa-laptop
+.fa-collage:before        {content: "\f03e";} // .fa-picture-o
+.fa-disccartridge:before  {content: "\f109";} // .fa-laptop
+.fa-drawing:before        {content: "\f03e";} // .fa-picture-o
+.fa-ebook:before          {content: "\f0f6";} // .fa-file-text-o
+.fa-electronic:before     {content: "\f1c6";} // .fa-file-archive-o
+.fa-filmstrip:before      {content: "\f008";} // .fa-film
+.fa-flashcard:before      {content: "\f0e7";} // .fa-bolt
+.fa-floppydisk:before     {content: "\f0c7";} // .fa-save
+.fa-globe:before          {content: "\f0ac";} // .fa-globe
+.fa-journal:before        {content: "\f0f6";} // .fa-file-text-o
+.fa-kit:before            {content: "\f0b1";} // .fa-briefcase
+.fa-manuscript:before     {content: "\f0f6";} // .fa-file-text-o
+.fa-map:before            {content: "\f14e";} // .fa-compass
+.fa-microfilm:before      {content: "\f008";} // .fa-film
+.fa-motionpicture:before  {content: "\f03d";} // .fa-video-camera
+.fa-musicalscore:before   {content: "\f001";} // .fa-music
+.fa-musicrecording:before {content: "\f001";} // .fa-music
+.fa-newspaper:before      {content: "\f0f6";} // .fa-file-text-o
+.fa-online:before         {content: "\f109";} // .fa-laptop
+.fa-painting:before       {content: "\f03e";} // .fa-picture-o
+.fa-photo:before          {content: "\f03e";} // .fa-picture-o
+.fa-photonegative:before  {content: "\f03e";} // .fa-picture-o
+.fa-physicalobject:before {content: "\f187";} // .fa-archive
+.fa-print:before          {content: "\f03e";} // .fa-picture-o
+.fa-sensorimage:before    {content: "\f03e";} // .fa-picture-o
+.fa-serial:before         {content: "\f0f6";} // .fa-file-text-o
+.fa-slide:before          {content: "\f008";} // .fa-film
+.fa-software:before       {content: "\f109";} // .fa-laptop
+.fa-soundcassette:before  {content: "\f025";} // .fa-headphones
+.fa-sounddisc:before      {content: "\f109";} // .fa-laptop
+.fa-soundrecording:before {content: "\f025";} // .fa-headphones
+.fa-tapecartridge:before  {content: "\f109";} // .fa-laptop
+.fa-tapecassette:before   {content: "\f025";} // .fa-headphones
+.fa-tapereel:before       {content: "\f008";} // .fa-film
+.fa-transparency:before   {content: "\f008";} // .fa-film
+.fa-unknown:before        {content: "\f128";} // .fa-question
+.fa-video:before          {content: "\f03d";} // .fa-video-camera
+.fa-videocartridge:before {content: "\f03d";} // .fa-video-camera
+.fa-videocassette:before  {content: "\f03d";} // .fa-video-camera
+.fa-videodisc:before      {content: "\f109";} // .fa-laptop
+.fa-videoreel:before      {content: "\f03d";} // .fa-video-camera
diff --git a/themes/bootstrap3/scss/components/js-tree.scss b/themes/bootstrap3/scss/components/js-tree.scss
new file mode 100644
index 0000000000000000000000000000000000000000..a53839e3155fa14aefc6848824204d447a83b684
--- /dev/null
+++ b/themes/bootstrap3/scss/components/js-tree.scss
@@ -0,0 +1,99 @@
+.hierarchy-tree {
+  /* jsTree arrows */
+  .jstree-ocl:before {
+    float: left;
+    width: 10px;
+    padding: 0;
+    margin-left: -10px;
+    font-family: 'FontAwesome';
+    font-style: normal;
+    font-weight: normal;
+    cursor: pointer;
+    text-decoration: inherit;
+    speak: none;
+  }
+  .jstree-open > .jstree-ocl:before {content: "\f0d7";}
+  .jstree-closed > .jstree-ocl:before {content: "\f0da";}
+  .jstree-leaf > .jstree-ocl:before {content: " ";}
+  .jstree-icon {
+    width: 16px;
+    color: #000;
+  }
+  .jstree-anchor {
+    padding: 2px 5px;
+    white-space: nowrap;
+  }
+  .jstree-container-ul,
+  .jstree-children {
+    padding-left: 16px;
+  }
+  .jstree-initial-node {display: none;}
+  .jstree-clicked {
+    color: $list-group-active-color;
+    background-color: $list-group-active-bg;
+    .jstree-icon {
+      color: #fff;
+    }
+  }
+  .jstree-search a {
+    font-style: italic;
+    color: #8b0000 ;
+    font-weight: bold;
+  }
+}
+
+/* --- Record --- */
+#hierarchyTreeHolder {
+  overflow-x: hidden;
+  border-right: 1px solid $gray-lighter;
+}
+#hierarchyTree .currentHierarchy > a,
+#hierarchyTree .currentRecord a {
+  font-weight: bold;
+  color: #000;
+}
+
+/* --- Facets --- */
+.facet .jstree-ocl:before {
+  float: left;
+  width: 10px;
+  padding: 0;
+  margin-left: -10px;
+  font-family: 'FontAwesome';
+  font-weight: normal;
+  font-style: normal;
+  text-decoration:inherit;
+  cursor: pointer;
+  speak: none;
+}
+.facet .jstree-default {
+  & .jstree-open > .jstree-ocl:before {content: "\f0d7";}
+  & .jstree-closed > .jstree-ocl:before {content: "\f0da";}
+  & .jstree-leaf > .jstree-ocl:before {content: " ";}
+}
+/* facet list styling */
+.jstree-facet li span.main {
+  display: block;
+  padding-left: 1px; /* Fix Firefox cutting the checkboxes */
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+.jstree-facet .jstree-container-ul {
+  padding: 0;
+  & > li.active,
+  & > li.active a.jstree-anchor {
+    background-color: #265680;
+    color: #fff;
+  }
+}
+li.jstree-facet,
+li.jstree-node {
+  list-style: none;
+}
+li.jstree-facet .badge {cursor: text;}
+li.jstree-facet ul {padding-left: 20px;}
+
+.list-group.facet .jstree-node.list-group-item { padding-left: 19px; }
+.list-group.facet .jstree-icon::before { margin-left: -12px; }
+.list-group.facet .jstree-facet li span.main { padding: 1px; }
diff --git a/themes/bootstrap3/scss/components/lightbox.scss b/themes/bootstrap3/scss/components/lightbox.scss
new file mode 100644
index 0000000000000000000000000000000000000000..e793dc0b3120e5a88790d11008ff95ab9c1c740e
--- /dev/null
+++ b/themes/bootstrap3/scss/components/lightbox.scss
@@ -0,0 +1,24 @@
+.lightbox-only { display: none; }
+#modal .lightbox-only { display: initial; }
+
+#modal {background-color: rgba(0,0,0,.2);}
+#modal .modal-content > .close {
+  position: absolute;
+  right: -50px;
+  top: 0;
+  z-index: 2;
+  font-size: 32pt;
+  color: #fff;
+  opacity: .7;
+}
+#modal .modal-content > .close:hover {opacity: 1;}
+#modal .modal-body h1,
+#modal .modal-body h2 {
+  margin-top: 0.3rem;
+  margin-bottom: 1.3rem;
+}
+
+#modal .cart-controls .btn {margin-bottom: 4px;}
+#modal .cart-controls .checkbox {padding-bottom: 1em;}
+#modal .cart-controls ~ hr {margin-top: 0;}
+.lightbox-scroll { overflow-y: auto; }
diff --git a/themes/bootstrap3/scss/components/offcanvas.scss b/themes/bootstrap3/scss/components/offcanvas.scss
new file mode 100644
index 0000000000000000000000000000000000000000..8a160dd13ce5b984420b5ef45c313f5c62fc1a03
--- /dev/null
+++ b/themes/bootstrap3/scss/components/offcanvas.scss
@@ -0,0 +1,92 @@
+$offcanvas-offset: 75% !default;
+$offcanvas-padding: 30px !default;
+
+.offcanvas-overlay,
+.offcanvas-toggle {display: none;}
+
+@media screen and (max-width: 767px) {
+  body.offcanvas {
+    overflow-x: hidden; /* Prevent scroll on narrow devices */
+    .sidebar {
+      position: fixed;
+      height: 100%;
+      top: 0;
+      width: $offcanvas-offset;
+      padding-left: 0;
+      padding-right: 0;
+      overflow-y: auto;
+      h4 {padding-left: $padding-base-horizontal;}
+      .checkbox {margin-left: 20px + $padding-base-horizontal;}
+      .list-group, .list-group-item {
+        border-left: 0;
+        border-right: 0;
+        border-radius: 0 !important;
+      }
+    }
+    &.active {overflow-y: hidden;}
+    &.offcanvas-left {
+      padding-left: $offcanvas-padding - $grid-gutter-width/2;
+      & .main {background: #FFF;}
+      &.active {
+        margin-left: $offcanvas-offset;
+        margin-right: -$offcanvas-offset;
+        .sidebar {left: 0;}
+        .offcanvas-overlay {right: -$offcanvas-offset;}
+        .offcanvas-toggle {left: $offcanvas-offset;}
+      }
+      .sidebar {left: -$offcanvas-offset;}
+      .offcanvas-overlay {right: -100%;}
+      .offcanvas-toggle {
+        border-radius: 0 $border-radius-small $border-radius-small 0; // top right and bottom right
+        left: 0;
+      }
+    }
+    &.offcanvas-right {
+      padding-right: $offcanvas-padding - $grid-gutter-width/2;
+      & .main > .container {background: #FFF;}
+      &.active {
+        margin-left: -$offcanvas-offset;
+        margin-right: $offcanvas-offset;
+        .sidebar {right: 0;}
+        .offcanvas-overlay {left: -$offcanvas-offset;}
+        .offcanvas-toggle {right: $offcanvas-offset;}
+      }
+      .sidebar {right: -$offcanvas-offset;}
+      .offcanvas-overlay {left: -100%;}
+      .offcanvas-toggle {
+        border-radius: $border-radius-small 0 0 $border-radius-small; // top left and bottom left
+        right: 0;
+      }
+    }
+    .offcanvas-overlay {
+      display: block;
+      position: fixed;
+      top: 0;
+      width: 100%;
+      height: 100%;
+      background-color: rgba(0,0,0,.3);
+      z-index: 3;
+    }
+    .offcanvas-toggle {
+      display: block;
+      position: fixed;
+      top: 50%;
+      width: calc($offcanvas-padding - 5px);
+      padding: 20px 0;
+      background: $brand-primary;
+      color: #EEE;
+      text-align: center;
+      z-index: 5;
+    }
+    .offcanvas-overlay,
+    .offcanvas-toggle,
+    .offcanvas-toggle * {cursor: pointer;}
+    &,.sidebar,
+    .offcanvas-overlay,
+    .offcanvas-toggle {
+      -webkit-transition: all .25s ease-out;
+           -o-transition: all .25s ease-out;
+              transition: all .25s ease-out;
+    }
+  }
+}
diff --git a/themes/bootstrap3/scss/components/record.scss b/themes/bootstrap3/scss/components/record.scss
new file mode 100644
index 0000000000000000000000000000000000000000..f6952443d5b42cf3ac960b7bc67725e720b0bef4
--- /dev/null
+++ b/themes/bootstrap3/scss/components/record.scss
@@ -0,0 +1,52 @@
+.citation .pace-car {
+  th,
+  td {
+    border: 0;
+    padding: 0;
+  }
+}
+.citation th {text-align: right;}
+.item-notes ul { padding-left: 2rem; }
+.recordcover {max-height: 300px;}
+.tagList {
+  .tag {
+    display: inline-block;
+    margin: 0 1px 1px;
+    border-radius: 4px;
+    &.selected {
+      background-color: $btn-primary-bg;
+      a {color: #fff;}
+      .badge {
+        color: $gray-darker;
+        background-color: #fff;
+        &:hover {color: #a94442;}
+      }
+    }
+
+    @include button-size($padding-base-vertical, $padding-base-vertical, $font-size-base, $line-height-base, $border-radius-base);
+    .badge .fa {width: 12px;}
+  }
+  button {border: 0;}
+  .tag-form {display: inline;}
+}
+.tagList.loggedin .tag:not(.selected) .badge:hover {background-color: $brand-success;}
+
+.subject-line:hover { color: #999; }
+.subject-line:hover a { color: $link-hover-color; }
+.subject-line a:hover ~ a {
+  color: #999;
+  text-decoration: none;
+}
+
+.record .format::after { content: ", "; }
+.result .record .format::after,
+.record .format:last-child::after { content: ""; }
+
+.marc-row-LEADER,
+.marc-row-006,
+.marc-row-007,
+.marc-row-008 {
+  white-space: pre-wrap;
+}
+
+.comment-form textarea.form-control { display: block; }
diff --git a/themes/bootstrap3/scss/components/search.scss b/themes/bootstrap3/scss/components/search.scss
new file mode 100644
index 0000000000000000000000000000000000000000..4ea08708a52140f707aa3ea9cedac4e42256f787
--- /dev/null
+++ b/themes/bootstrap3/scss/components/search.scss
@@ -0,0 +1,126 @@
+$thumbnail-width-small: 60px !default;
+$thumbnail-width-medium: 100px !default;
+$thumbnail-width-large: 160px !default;
+
+.bulkActionButtons label {display: inline-block;}
+.bulkActionButtons label input {margin-top: 2px;}
+.grid { @media (max-width: 767px) {min-height: 250px;} }
+.result {
+  padding-top: 1.5rem;
+  .checkbox {
+    float: left;
+    padding-right: .5rem;
+  }
+  .media,
+  .media-body { overflow: visible; }
+  .media-body .row {
+    margin-left: 0;
+    margin-right: 0;
+  }
+  .media {
+    margin-top: 0;
+    margin-bottom: 1rem;
+    @include clearfix();
+  }
+  .media-left,
+  .media-right {
+    padding: 0;
+    text-align: center;
+    a {
+      display: inline-block;
+      text-align: center;
+    }
+    img {
+      max-width: none;
+      max-height: 300px;
+    }
+    &.small a,
+    &.small > img {
+      width: $thumbnail-width-small;
+      img { max-width: $thumbnail-width-small; }
+    }
+    &.medium a,
+    &.medium > img {
+      width: $thumbnail-width-medium;
+      img { max-width: $thumbnail-width-medium; }
+    }
+    &.large a,
+    &.large > img {
+      width: $thumbnail-width-large;
+      img { max-width: $thumbnail-width-large; }
+    }
+  }
+  .media-left { padding-left: 10px; }
+  .media-right { padding-right: 10px; }
+  .title {font-weight: bold;}
+  .list-tab-content.record .img-col { display: none; }
+  .list-tab-content.record .info-col { width: 100%; }
+
+  @media (max-width: 767px) {
+    a { text-decoration: underline; }
+  }
+  @media (max-width: 530px) {
+    .checkbox { padding: 0; }
+    .media-left { padding-right: 0; }
+  }
+
+  .format {
+    @extend .label;
+    @extend .label-info;
+  }
+}
+
+.result.embedded {
+  .getFull {
+    display: block;
+    margin-left: 1.5rem;
+    padding-right: 30px;
+    border-left: 1px solid transparent;
+  }
+  .getFull.expanded {
+    @extend .list-group-item;
+    margin-top: -11px;
+    margin-left: .75rem;
+    padding-left: .75rem;
+    border-top-left-radius: $border-radius-base;
+    border-top-right-radius: $border-radius-base;
+    &::before {
+      content: '\25BC';
+      position: absolute;
+      right: 15px;
+      color: $gray;
+    }
+  }
+  .loading {
+    @extend .list-group-item;
+    margin-left: .75rem;
+    padding: 1rem;
+    background: #fff;
+  }
+  .long-view {
+    margin-left: .75rem;
+    padding: .5rem;
+    border: 1px solid $list-group-border;
+    background-color: #fff;
+    border-bottom-left-radius: $border-radius-base;
+    border-bottom-right-radius: $border-radius-base;
+  }
+  .long-view .tab-content { padding: 0; }
+  .list-tabs { margin-bottom: 0; }
+  .list-tab-toggle { cursor: pointer; }
+  .list-tab-content { padding: 1rem; }
+}
+
+.search-controls .alert {margin-bottom: 0;}
+.searchtools a {padding: 0 .5em;}
+.title-in-heading {
+  font-size: inherit;
+  font-style: italic;
+}
+
+.wikipedia img { margin-right: 1rem; }
+
+.geoItem {
+    font-size: .9em;
+    margin: 0px 0px 10px;
+}
diff --git a/themes/bootstrap3/scss/components/sidebar.scss b/themes/bootstrap3/scss/components/sidebar.scss
new file mode 100644
index 0000000000000000000000000000000000000000..73ba5087b2e0ae3156962346b5425eea30f971c7
--- /dev/null
+++ b/themes/bootstrap3/scss/components/sidebar.scss
@@ -0,0 +1,53 @@
+/* Sidebar rounded corners */
+.narrow-toggle { text-align: center; }
+.sidebar {
+  label:not(.list-group-item) {margin-left: 20px;}
+  .list-group.facet .list-group-item.title {
+    cursor: pointer;
+    &.collapsed {
+      border-radius: $border-radius-base;
+      &:after {content: '\25BC';}
+    }
+    &:after {
+      content: '\25B2';
+      float: right;
+    }
+  }
+  .collapse,.collapsing {
+    .list-group-item {
+      border-top-left-radius: 0px;
+      border-top-right-radius: 0px;
+      &[id^=more] {
+        border-bottom-left-radius: $border-radius-base;
+        border-bottom-right-radius: $border-radius-base;
+      }
+    }
+  }
+  #side-collapse-publishDate .list-group-item {
+    border-bottom-left-radius: $border-radius-base;
+    border-bottom-right-radius: $border-radius-base;
+  }
+}
+label.list-group-item {
+  margin-top: 0;
+  padding-left: 35px;
+  font-weight: normal;
+  border-radius: 0;
+}
+.list-group-item.title {font-weight: bold;}
+.sidebar .facet a {text-decoration: none;}
+.sidebar .format {
+  @extend .label;
+  @extend .label-info;
+}
+.top-row .applied {
+  font-weight: bold;
+  &:hover {
+    color: $state-danger-text;
+    .fa.fa-check:before {
+      content: "\f00d";
+    }
+  }
+}
+
+.full-facet-list { margin-top: 1rem; }
diff --git a/themes/bootstrap3/scss/components/similar-carousel.scss b/themes/bootstrap3/scss/components/similar-carousel.scss
new file mode 100644
index 0000000000000000000000000000000000000000..1493f611a9ab36a4a9659966000e0afc2a830a30
--- /dev/null
+++ b/themes/bootstrap3/scss/components/similar-carousel.scss
@@ -0,0 +1,37 @@
+#similar-items-carousel {
+  .carousel-indicators {
+    bottom: 0px;
+    li {
+      width: 8px;
+      height: 8px;
+      margin: 2px;
+      background-color: rgba(255,255,255,.3);
+      border-color: $gray-darker;
+    }
+  }
+  .hover-overlay {
+    position: relative;
+    display: block;
+    min-width: 150px;
+    min-height: 200px;
+    margin: auto;
+    text-align: center;
+    img {
+      max-width: 100%;
+      margin: 10px 0;
+    }
+    .content {
+      position: absolute;
+      top: 0;
+      left: 0;
+      display: none;
+      width: 100%;
+      height: 100%;
+      padding: .5em .5em 0;
+      color: #fff;
+      background-color: rgba(0,0,0,.5);
+    }
+    &:hover .content {display: block;}
+  }
+}
+#similar-items-carousel .item {padding: 0 4em;}
diff --git a/themes/bootstrap3/scss/components/sliders.scss b/themes/bootstrap3/scss/components/sliders.scss
new file mode 100644
index 0000000000000000000000000000000000000000..6caffdc44865b5edffc17fde5b7d7a942806683b
--- /dev/null
+++ b/themes/bootstrap3/scss/components/sliders.scss
@@ -0,0 +1,31 @@
+.slider-container {
+  padding:4px 10px;
+  text-align:center;
+  .slider.slider-horizontal {
+    width: 100%;
+  }
+  .slider-track {
+  background:$gray-light;
+  box-shadow:inset 0 1px 0 rgba(0, 0, 0, 0.4);
+  }
+  .slider-handle {
+    background:$brand-primary;
+    background-image:none;
+    border:1px solid $brand-primary;
+    box-shadow:none;
+    opacity:.9;
+    &:hover,&:active,&:focus {
+      opacity:1;
+      background:#FFF;
+      border-color:$gray-light;
+    }
+    &:active,&:focus {
+      border-color:$brand-primary;
+    }
+  }
+  .slider-selection {
+    background: #CCC;
+    box-shadow:inset 0 -1px 0 rgba(0,0,0,0.3);
+  }
+  input {display: none;}
+}
diff --git a/themes/bootstrap3/scss/test.scss b/themes/bootstrap3/scss/test.scss
new file mode 100644
index 0000000000000000000000000000000000000000..a8bbbc1276d3bffd26756c7540b7ae90aa7b0bcd
--- /dev/null
+++ b/themes/bootstrap3/scss/test.scss
@@ -0,0 +1 @@
+body { background-color: tan; }
diff --git a/themes/bootstrap3/scss/vendor/_bootstrap.scss b/themes/bootstrap3/scss/vendor/_bootstrap.scss
new file mode 100644
index 0000000000000000000000000000000000000000..e72d1def2a217b615f5a931d1d7e84d9332cbc6a
--- /dev/null
+++ b/themes/bootstrap3/scss/vendor/_bootstrap.scss
@@ -0,0 +1,56 @@
+/*!
+ * Bootstrap v3.3.7 (http://getbootstrap.com)
+ * Copyright 2011-2016 Twitter, Inc.
+ * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
+ */
+
+// Core variables and mixins
+@import "bootstrap/variables";
+@import "bootstrap/mixins";
+
+// Reset and dependencies
+@import "bootstrap/normalize";
+@import "bootstrap/print";
+@import "bootstrap/glyphicons";
+
+// Core CSS
+@import "bootstrap/scaffolding";
+@import "bootstrap/type";
+@import "bootstrap/code";
+@import "bootstrap/grid";
+@import "bootstrap/tables";
+@import "bootstrap/forms";
+@import "bootstrap/buttons";
+
+// Components
+@import "bootstrap/component-animations";
+@import "bootstrap/dropdowns";
+@import "bootstrap/button-groups";
+@import "bootstrap/input-groups";
+@import "bootstrap/navs";
+@import "bootstrap/navbar";
+@import "bootstrap/breadcrumbs";
+@import "bootstrap/pagination";
+@import "bootstrap/pager";
+@import "bootstrap/labels";
+@import "bootstrap/badges";
+@import "bootstrap/jumbotron";
+@import "bootstrap/thumbnails";
+@import "bootstrap/alerts";
+@import "bootstrap/progress-bars";
+@import "bootstrap/media";
+@import "bootstrap/list-group";
+@import "bootstrap/panels";
+@import "bootstrap/responsive-embed";
+@import "bootstrap/wells";
+@import "bootstrap/close";
+
+// Components w/ JavaScript
+@import "bootstrap/modals";
+@import "bootstrap/tooltip";
+@import "bootstrap/popovers";
+@import "bootstrap/carousel";
+
+// Utility classes
+@import "bootstrap/utilities";
+@import "bootstrap/responsive-utilities";
diff --git a/themes/bootstrap3/scss/vendor/a11y.scss b/themes/bootstrap3/scss/vendor/a11y.scss
new file mode 100644
index 0000000000000000000000000000000000000000..ee5ab6859c7ede4a9a7af9170d91cdffc64371f1
--- /dev/null
+++ b/themes/bootstrap3/scss/vendor/a11y.scss
@@ -0,0 +1,117 @@
+/* mixins */
+
+input, textarea {
+  &::-webkit-input-placeholder, // Chrome/Safari/Opera
+  &:-ms-input-placeholder,      // IE
+  &::-ms-input-placeholder,     // Edge
+  &::placeholder {
+    color: #888;
+  }
+}
+
+.sr-only
+{
+  clip: rect(1px, 1px, 1px, 1px);
+  position: absolute;
+  /* reset */
+  width: auto;
+  height: auto;
+  margin: 0;
+  padding: 0;
+  overflow: hidden;
+  border:0;
+}
+
+.sr-only:focus {
+  background-color: #fff; //$navbar-default-link-hover-bg;//watch out, transparent by default!
+  border-radius: $navbar-border-radius;
+  clip: auto;
+  color: $navbar-default-link-hover-color;
+  display: block;
+  font-size: $font-size-base;
+  height: $navbar-height;
+  line-height: $line-height-computed;
+  padding: $navbar-padding-vertical $navbar-padding-horizontal;
+  position: absolute;
+  left: 5px;
+  top: 5px;
+  text-decoration: none;
+  text-transform: none;
+  width: auto;
+  z-index: 100000; /* Above WP toolbar */
+}
+
+/* this mixins give output generate duplicate CSS code
+ * alternatively you will to have to rewrite the original mixin in buttons.less
+ */
+@mixin button-variant($color, $background, $border) {
+  &:hover,
+  &:focus,
+  &:active,
+  &.active,
+  .open &.dropdown-toggle {
+    color: $background;
+    background-color: $color;
+    border-color: darken($border, 12%);
+  }
+}
+
+// Navbar
+// -------------------------
+
+$navbar-default-color:             #fff;
+$navbar-default-bg:                #132531;
+$navbar-default-border:            darken($navbar-default-bg, 6.5%);
+
+// Navbar links
+$navbar-default-link-color:                #fff;
+$navbar-default-link-hover-color:          #132531;
+$navbar-default-link-hover-bg:             #fff;
+$navbar-default-link-active-color:         #132531;
+$navbar-default-link-active-bg:            #fff;
+$navbar-default-link-disabled-color:       #fff;
+$navbar-default-link-disabled-bg:          #068139;
+
+// Navbar brand label
+$navbar-default-brand-color:               $navbar-default-link-color;
+$navbar-default-brand-hover-color:         #068139; //contrast 3.15, so min 19px;
+.navbar-brand {
+  font-size: 20px;
+}
+$navbar-default-brand-hover-bg:            transparent;
+
+// Navbar toggle
+$navbar-default-toggle-hover-bg:           #ddd;
+$navbar-default-toggle-icon-bar-bg:        #888;
+$navbar-default-toggle-border-color:       #ddd;
+
+$brand-primary:         #265680;
+$brand-success:         #028302;
+$brand-info:            #1C5F74;
+$brand-warning:         #A56100;
+$brand-danger:          #A41915;
+
+/* buttons */
+$btn-primary-color:              #fff;
+$btn-primary-bg:                 $brand-primary;
+$btn-primary-border:             #fff;
+
+$btn-success-color:              #fff;
+$btn-success-bg:                 $brand-success;
+$btn-success-border:             #fff;
+
+$btn-info-color:              #fff;
+$btn-info-bg:                 $brand-info;
+$btn-info-border:             #fff;
+
+$btn-warning-color:              #fff;
+$btn-warning-bg:                 $brand-warning;
+$btn-warning-border:             #fff;
+
+$btn-danger-color:              #fff;
+$btn-danger-bg:                 $brand-danger;
+$btn-danger-border:             #fff;
+
+
+/* link on white background */
+$link-color:            #12538B;
diff --git a/themes/bootstrap3/scss/vendor/bootstrap-accessibility/bootstrap-accessibility.scss b/themes/bootstrap3/scss/vendor/bootstrap-accessibility/bootstrap-accessibility.scss
new file mode 100644
index 0000000000000000000000000000000000000000..73dae66c01c197bbf4813b7f5b7774c86ee466b1
--- /dev/null
+++ b/themes/bootstrap3/scss/vendor/bootstrap-accessibility/bootstrap-accessibility.scss
@@ -0,0 +1,17 @@
+// Import Compass Transition Module
+// @import "compass/css3/transition";
+
+// Import Variables
+@import "partials/variables";
+
+// Import Mixins
+@import "partials/mixins";
+
+// Import Partials Styles
+@import "partials/buttons";
+@import "partials/divs";
+@import "partials/links";
+@import "partials/close";
+@import "partials/navigation";
+@import "partials/carousel";
+@import "partials/alerts";
\ No newline at end of file
diff --git a/themes/bootstrap3/scss/vendor/bootstrap-accessibility/partials/_alerts.scss b/themes/bootstrap3/scss/vendor/bootstrap-accessibility/partials/_alerts.scss
new file mode 100644
index 0000000000000000000000000000000000000000..20333059b3b1b55d041b62d6c4d02cac10243e3a
--- /dev/null
+++ b/themes/bootstrap3/scss/vendor/bootstrap-accessibility/partials/_alerts.scss
@@ -0,0 +1,18 @@
+.alert-success {
+	color: $alert-success-color;
+}
+.alert-info {
+	color: $alert-info-color;
+}
+.alert-warning {
+  color: $alert-warning-color;
+  background-color: $alert-warning-bkg;
+}
+.alert-danger {
+  color: $alert-danger-color;
+}
+.alert-danger {
+  &:hover {
+  	color: $alert-danger-hover;
+  }
+}
\ No newline at end of file
diff --git a/themes/bootstrap3/scss/vendor/bootstrap-accessibility/partials/_buttons.scss b/themes/bootstrap3/scss/vendor/bootstrap-accessibility/partials/_buttons.scss
new file mode 100644
index 0000000000000000000000000000000000000000..e0cb64ae54d7d261a9347f16b5dac6777b443799
--- /dev/null
+++ b/themes/bootstrap3/scss/vendor/bootstrap-accessibility/partials/_buttons.scss
@@ -0,0 +1,5 @@
+.btn {
+  &:focus {
+	@include outline(2px);
+  }
+}
\ No newline at end of file
diff --git a/themes/bootstrap3/scss/vendor/bootstrap-accessibility/partials/_carousel.scss b/themes/bootstrap3/scss/vendor/bootstrap-accessibility/partials/_carousel.scss
new file mode 100644
index 0000000000000000000000000000000000000000..219077bbcb1feba1a4802c0defee05e9f337f12f
--- /dev/null
+++ b/themes/bootstrap3/scss/vendor/bootstrap-accessibility/partials/_carousel.scss
@@ -0,0 +1,54 @@
+.carousel-indicators li,
+.carousel-indicators li.active {
+  height: 18px;
+  width: 18px;
+  border-width: 2px;
+  position: relative;
+  box-shadow: 0px 0px 0px 1px #808080;
+}
+
+.carousel-indicators.active li {
+  background-color: rgba(100, 149, 253, 0.6);
+}
+
+.carousel-indicators.active li.active {
+  background-color: white;
+}
+
+.carousel-tablist-highlight {
+  display: block;
+  position: absolute;
+  outline: 2px solid transparent;
+  background-color: transparent;
+  box-shadow: 0px 0px 0px 1px transparent;
+}
+
+.carousel-tablist-highlight.focus {
+  outline: 2px solid #6495ED;
+  background-color: rgba(0, 0, 0, 0.4);
+}
+
+a.carousel-control:focus {
+  outline: 2px solid #6495ED;
+  background-image: linear-gradient(to right, transparent 0px, rgba(0, 0, 0, 0.5) 100%);
+  box-shadow: 0px 0px 0px 1px #000000;
+}
+
+.carousel-pause-button {
+  position: absolute;
+  top: -30em;
+  left: -300em;
+  display: block;
+}
+
+.carousel-pause-button.focus {
+  top: 0.5em;
+  left: 0.5em;
+}
+
+
+.carousel:hover .carousel-caption,
+.carousel.contrast .carousel-caption {
+    background-color: rgba(0, 0, 0, 0.5);
+    z-index: 10;
+}
diff --git a/themes/bootstrap3/scss/vendor/bootstrap-accessibility/partials/_close.scss b/themes/bootstrap3/scss/vendor/bootstrap-accessibility/partials/_close.scss
new file mode 100644
index 0000000000000000000000000000000000000000..ac5e20f801e096c95b1334ac30ddef6c34241c4c
--- /dev/null
+++ b/themes/bootstrap3/scss/vendor/bootstrap-accessibility/partials/_close.scss
@@ -0,0 +1,6 @@
+.close {
+  &:hover,
+  &:focus {
+  	@include outline(1px);
+  }
+}
\ No newline at end of file
diff --git a/themes/bootstrap3/scss/vendor/bootstrap-accessibility/partials/_divs.scss b/themes/bootstrap3/scss/vendor/bootstrap-accessibility/partials/_divs.scss
new file mode 100644
index 0000000000000000000000000000000000000000..e6a24e21a978378cc98f437c6af29ef88afb6889
--- /dev/null
+++ b/themes/bootstrap3/scss/vendor/bootstrap-accessibility/partials/_divs.scss
@@ -0,0 +1,5 @@
+div.active {
+	&:focus {
+		@include outline(1px);
+	}
+}
\ No newline at end of file
diff --git a/themes/bootstrap3/scss/vendor/bootstrap-accessibility/partials/_links.scss b/themes/bootstrap3/scss/vendor/bootstrap-accessibility/partials/_links.scss
new file mode 100644
index 0000000000000000000000000000000000000000..4d4584192cb15591766114a48efc12ba26613f69
--- /dev/null
+++ b/themes/bootstrap3/scss/vendor/bootstrap-accessibility/partials/_links.scss
@@ -0,0 +1,5 @@
+a {
+  &:focus {
+  	@include outline(1px);
+  }
+}
\ No newline at end of file
diff --git a/themes/bootstrap3/scss/vendor/bootstrap-accessibility/partials/_mixins.scss b/themes/bootstrap3/scss/vendor/bootstrap-accessibility/partials/_mixins.scss
new file mode 100644
index 0000000000000000000000000000000000000000..9866b06bc6130369a9f927a2159d30cc7c285979
--- /dev/null
+++ b/themes/bootstrap3/scss/vendor/bootstrap-accessibility/partials/_mixins.scss
@@ -0,0 +1,3 @@
+@mixin outline($size) {
+	outline: $outline-default-style $size $outline-default-color;
+}
\ No newline at end of file
diff --git a/themes/bootstrap3/scss/vendor/bootstrap-accessibility/partials/_navigation.scss b/themes/bootstrap3/scss/vendor/bootstrap-accessibility/partials/_navigation.scss
new file mode 100644
index 0000000000000000000000000000000000000000..ba5a8b8184f9da0c5a022a104bb08cebbb870ea2
--- /dev/null
+++ b/themes/bootstrap3/scss/vendor/bootstrap-accessibility/partials/_navigation.scss
@@ -0,0 +1,10 @@
+.nav {
+	> li {
+		> a {
+			&:hover,
+			&:focus {
+				@include outline(1px);
+			}
+		}
+	}
+}
\ No newline at end of file
diff --git a/themes/bootstrap3/scss/vendor/bootstrap-accessibility/partials/_variables.scss b/themes/bootstrap3/scss/vendor/bootstrap-accessibility/partials/_variables.scss
new file mode 100644
index 0000000000000000000000000000000000000000..3704b88ab1a1f2cb88ed940e598d3a2dd88894e5
--- /dev/null
+++ b/themes/bootstrap3/scss/vendor/bootstrap-accessibility/partials/_variables.scss
@@ -0,0 +1,21 @@
+//Set the variables for use in stylesheet
+
+// Colors
+$black: 		#000;
+$green-dark: 	#2d4821;
+$blue-dark: 	#214c62;
+$red-dark: 		#6c4a00;
+$red: 			#d2322d;
+$yellow: 		#f9f1c6;
+
+// Outline
+$outline-default-style: 	dotted;
+$outline-default-color: 	$black;
+
+// Alerts Colors
+$alert-success-color: 			$green-dark;
+$alert-info-color: 				$blue-dark;
+$alert-warning-color: 			$red-dark;
+$alert-warning-bkg: 			$yellow;
+$alert-danger-color: 			$red;
+$alert-danger-hover: 			darken($red, 10%);
diff --git a/themes/bootstrap3/scss/vendor/bootstrap/_alerts.scss b/themes/bootstrap3/scss/vendor/bootstrap/_alerts.scss
new file mode 100644
index 0000000000000000000000000000000000000000..7d1e1fddd15d81820e7bcfbd4b270381cf3b9377
--- /dev/null
+++ b/themes/bootstrap3/scss/vendor/bootstrap/_alerts.scss
@@ -0,0 +1,73 @@
+//
+// Alerts
+// --------------------------------------------------
+
+
+// Base styles
+// -------------------------
+
+.alert {
+  padding: $alert-padding;
+  margin-bottom: $line-height-computed;
+  border: 1px solid transparent;
+  border-radius: $alert-border-radius;
+
+  // Headings for larger alerts
+  h4 {
+    margin-top: 0;
+    // Specified for the h4 to prevent conflicts of changing $headings-color
+    color: inherit;
+  }
+
+  // Provide class for links that match alerts
+  .alert-link {
+    font-weight: $alert-link-font-weight;
+  }
+
+  // Improve alignment and spacing of inner content
+  > p,
+  > ul {
+    margin-bottom: 0;
+  }
+
+  > p + p {
+    margin-top: 5px;
+  }
+}
+
+// Dismissible alerts
+//
+// Expand the right padding and account for the close button's positioning.
+
+.alert-dismissable, // The misspelled .alert-dismissable was deprecated in 3.2.0.
+.alert-dismissible {
+  padding-right: ($alert-padding + 20);
+
+  // Adjust close link position
+  .close {
+    position: relative;
+    top: -2px;
+    right: -21px;
+    color: inherit;
+  }
+}
+
+// Alternate styles
+//
+// Generate contextual modifier classes for colorizing the alert.
+
+.alert-success {
+  @include alert-variant($alert-success-bg, $alert-success-border, $alert-success-text);
+}
+
+.alert-info {
+  @include alert-variant($alert-info-bg, $alert-info-border, $alert-info-text);
+}
+
+.alert-warning {
+  @include alert-variant($alert-warning-bg, $alert-warning-border, $alert-warning-text);
+}
+
+.alert-danger {
+  @include alert-variant($alert-danger-bg, $alert-danger-border, $alert-danger-text);
+}
diff --git a/themes/bootstrap3/scss/vendor/bootstrap/_badges.scss b/themes/bootstrap3/scss/vendor/bootstrap/_badges.scss
new file mode 100644
index 0000000000000000000000000000000000000000..70002e085b1867b14af58414c79074e444cb07ce
--- /dev/null
+++ b/themes/bootstrap3/scss/vendor/bootstrap/_badges.scss
@@ -0,0 +1,68 @@
+//
+// Badges
+// --------------------------------------------------
+
+
+// Base class
+.badge {
+  display: inline-block;
+  min-width: 10px;
+  padding: 3px 7px;
+  font-size: $font-size-small;
+  font-weight: $badge-font-weight;
+  color: $badge-color;
+  line-height: $badge-line-height;
+  vertical-align: middle;
+  white-space: nowrap;
+  text-align: center;
+  background-color: $badge-bg;
+  border-radius: $badge-border-radius;
+
+  // Empty badges collapse automatically (not available in IE8)
+  &:empty {
+    display: none;
+  }
+
+  // Quick fix for badges in buttons
+  .btn & {
+    position: relative;
+    top: -1px;
+  }
+
+  .btn-xs &,
+  .btn-group-xs > .btn & {
+    top: 0;
+    padding: 1px 5px;
+  }
+
+  // [converter] extracted a& to a.badge
+
+  // Account for badges in navs
+  .list-group-item.active > &,
+  .nav-pills > .active > a > & {
+    color: $badge-active-color;
+    background-color: $badge-active-bg;
+  }
+
+  .list-group-item > & {
+    float: right;
+  }
+
+  .list-group-item > & + & {
+    margin-right: 5px;
+  }
+
+  .nav-pills > li > a > & {
+    margin-left: 3px;
+  }
+}
+
+// Hover state, but only for links
+a.badge {
+  &:hover,
+  &:focus {
+    color: $badge-link-hover-color;
+    text-decoration: none;
+    cursor: pointer;
+  }
+}
diff --git a/themes/bootstrap3/scss/vendor/bootstrap/_breadcrumbs.scss b/themes/bootstrap3/scss/vendor/bootstrap/_breadcrumbs.scss
new file mode 100644
index 0000000000000000000000000000000000000000..b61f0c731caf2f99d3fba09e6643216c679984a6
--- /dev/null
+++ b/themes/bootstrap3/scss/vendor/bootstrap/_breadcrumbs.scss
@@ -0,0 +1,28 @@
+//
+// Breadcrumbs
+// --------------------------------------------------
+
+
+.breadcrumb {
+  padding: $breadcrumb-padding-vertical $breadcrumb-padding-horizontal;
+  margin-bottom: $line-height-computed;
+  list-style: none;
+  background-color: $breadcrumb-bg;
+  border-radius: $border-radius-base;
+
+  > li {
+    display: inline-block;
+
+    + li:before {
+      // [converter] Workaround for https://github.com/sass/libsass/issues/1115
+      $nbsp: "\00a0";
+      content: "#{$breadcrumb-separator}#{$nbsp}"; // Unicode space added since inline-block means non-collapsing white-space
+      padding: 0 5px;
+      color: $breadcrumb-color;
+    }
+  }
+
+  > .active {
+    color: $breadcrumb-active-color;
+  }
+}
diff --git a/themes/bootstrap3/scss/vendor/bootstrap/_button-groups.scss b/themes/bootstrap3/scss/vendor/bootstrap/_button-groups.scss
new file mode 100644
index 0000000000000000000000000000000000000000..4b385f569c491ac441fc4e518c38247f717e221f
--- /dev/null
+++ b/themes/bootstrap3/scss/vendor/bootstrap/_button-groups.scss
@@ -0,0 +1,244 @@
+//
+// Button groups
+// --------------------------------------------------
+
+// Make the div behave like a button
+.btn-group,
+.btn-group-vertical {
+  position: relative;
+  display: inline-block;
+  vertical-align: middle; // match .btn alignment given font-size hack above
+  > .btn {
+    position: relative;
+    float: left;
+    // Bring the "active" button to the front
+    &:hover,
+    &:focus,
+    &:active,
+    &.active {
+      z-index: 2;
+    }
+  }
+}
+
+// Prevent double borders when buttons are next to each other
+.btn-group {
+  .btn + .btn,
+  .btn + .btn-group,
+  .btn-group + .btn,
+  .btn-group + .btn-group {
+    margin-left: -1px;
+  }
+}
+
+// Optional: Group multiple button groups together for a toolbar
+.btn-toolbar {
+  margin-left: -5px; // Offset the first child's margin
+  @include clearfix;
+
+  .btn,
+  .btn-group,
+  .input-group {
+    float: left;
+  }
+  > .btn,
+  > .btn-group,
+  > .input-group {
+    margin-left: 5px;
+  }
+}
+
+.btn-group > .btn:not(:first-child):not(:last-child):not(.dropdown-toggle) {
+  border-radius: 0;
+}
+
+// Set corners individual because sometimes a single button can be in a .btn-group and we need :first-child and :last-child to both match
+.btn-group > .btn:first-child {
+  margin-left: 0;
+  &:not(:last-child):not(.dropdown-toggle) {
+    @include border-right-radius(0);
+  }
+}
+// Need .dropdown-toggle since :last-child doesn't apply, given that a .dropdown-menu is used immediately after it
+.btn-group > .btn:last-child:not(:first-child),
+.btn-group > .dropdown-toggle:not(:first-child) {
+  @include border-left-radius(0);
+}
+
+// Custom edits for including btn-groups within btn-groups (useful for including dropdown buttons within a btn-group)
+.btn-group > .btn-group {
+  float: left;
+}
+.btn-group > .btn-group:not(:first-child):not(:last-child) > .btn {
+  border-radius: 0;
+}
+.btn-group > .btn-group:first-child:not(:last-child) {
+  > .btn:last-child,
+  > .dropdown-toggle {
+    @include border-right-radius(0);
+  }
+}
+.btn-group > .btn-group:last-child:not(:first-child) > .btn:first-child {
+  @include border-left-radius(0);
+}
+
+// On active and open, don't show outline
+.btn-group .dropdown-toggle:active,
+.btn-group.open .dropdown-toggle {
+  outline: 0;
+}
+
+
+// Sizing
+//
+// Remix the default button sizing classes into new ones for easier manipulation.
+
+.btn-group-xs > .btn { @extend .btn-xs; }
+.btn-group-sm > .btn { @extend .btn-sm; }
+.btn-group-lg > .btn { @extend .btn-lg; }
+
+
+// Split button dropdowns
+// ----------------------
+
+// Give the line between buttons some depth
+.btn-group > .btn + .dropdown-toggle {
+  padding-left: 8px;
+  padding-right: 8px;
+}
+.btn-group > .btn-lg + .dropdown-toggle {
+  padding-left: 12px;
+  padding-right: 12px;
+}
+
+// The clickable button for toggling the menu
+// Remove the gradient and set the same inset shadow as the :active state
+.btn-group.open .dropdown-toggle {
+  @include box-shadow(inset 0 3px 5px rgba(0,0,0,.125));
+
+  // Show no shadow for `.btn-link` since it has no other button styles.
+  &.btn-link {
+    @include box-shadow(none);
+  }
+}
+
+
+// Reposition the caret
+.btn .caret {
+  margin-left: 0;
+}
+// Carets in other button sizes
+.btn-lg .caret {
+  border-width: $caret-width-large $caret-width-large 0;
+  border-bottom-width: 0;
+}
+// Upside down carets for .dropup
+.dropup .btn-lg .caret {
+  border-width: 0 $caret-width-large $caret-width-large;
+}
+
+
+// Vertical button groups
+// ----------------------
+
+.btn-group-vertical {
+  > .btn,
+  > .btn-group,
+  > .btn-group > .btn {
+    display: block;
+    float: none;
+    width: 100%;
+    max-width: 100%;
+  }
+
+  // Clear floats so dropdown menus can be properly placed
+  > .btn-group {
+    @include clearfix;
+    > .btn {
+      float: none;
+    }
+  }
+
+  > .btn + .btn,
+  > .btn + .btn-group,
+  > .btn-group + .btn,
+  > .btn-group + .btn-group {
+    margin-top: -1px;
+    margin-left: 0;
+  }
+}
+
+.btn-group-vertical > .btn {
+  &:not(:first-child):not(:last-child) {
+    border-radius: 0;
+  }
+  &:first-child:not(:last-child) {
+    @include border-top-radius($btn-border-radius-base);
+    @include border-bottom-radius(0);
+  }
+  &:last-child:not(:first-child) {
+    @include border-top-radius(0);
+    @include border-bottom-radius($btn-border-radius-base);
+  }
+}
+.btn-group-vertical > .btn-group:not(:first-child):not(:last-child) > .btn {
+  border-radius: 0;
+}
+.btn-group-vertical > .btn-group:first-child:not(:last-child) {
+  > .btn:last-child,
+  > .dropdown-toggle {
+    @include border-bottom-radius(0);
+  }
+}
+.btn-group-vertical > .btn-group:last-child:not(:first-child) > .btn:first-child {
+  @include border-top-radius(0);
+}
+
+
+// Justified button groups
+// ----------------------
+
+.btn-group-justified {
+  display: table;
+  width: 100%;
+  table-layout: fixed;
+  border-collapse: separate;
+  > .btn,
+  > .btn-group {
+    float: none;
+    display: table-cell;
+    width: 1%;
+  }
+  > .btn-group .btn {
+    width: 100%;
+  }
+
+  > .btn-group .dropdown-menu {
+    left: auto;
+  }
+}
+
+
+// Checkbox and radio options
+//
+// In order to support the browser's form validation feedback, powered by the
+// `required` attribute, we have to "hide" the inputs via `clip`. We cannot use
+// `display: none;` or `visibility: hidden;` as that also hides the popover.
+// Simply visually hiding the inputs via `opacity` would leave them clickable in
+// certain cases which is prevented by using `clip` and `pointer-events`.
+// This way, we ensure a DOM element is visible to position the popover from.
+//
+// See https://github.com/twbs/bootstrap/pull/12794 and
+// https://github.com/twbs/bootstrap/pull/14559 for more information.
+
+[data-toggle="buttons"] {
+  > .btn,
+  > .btn-group > .btn {
+    input[type="radio"],
+    input[type="checkbox"] {
+      position: absolute;
+      clip: rect(0,0,0,0);
+      pointer-events: none;
+    }
+  }
+}
diff --git a/themes/bootstrap3/scss/vendor/bootstrap/_buttons.scss b/themes/bootstrap3/scss/vendor/bootstrap/_buttons.scss
new file mode 100644
index 0000000000000000000000000000000000000000..6452b709f109420cf060147f5eb3d943eeb33b43
--- /dev/null
+++ b/themes/bootstrap3/scss/vendor/bootstrap/_buttons.scss
@@ -0,0 +1,168 @@
+//
+// Buttons
+// --------------------------------------------------
+
+
+// Base styles
+// --------------------------------------------------
+
+.btn {
+  display: inline-block;
+  margin-bottom: 0; // For input.btn
+  font-weight: $btn-font-weight;
+  text-align: center;
+  vertical-align: middle;
+  touch-action: manipulation;
+  cursor: pointer;
+  background-image: none; // Reset unusual Firefox-on-Android default style; see https://github.com/necolas/normalize.css/issues/214
+  border: 1px solid transparent;
+  white-space: nowrap;
+  @include button-size($padding-base-vertical, $padding-base-horizontal, $font-size-base, $line-height-base, $btn-border-radius-base);
+  @include user-select(none);
+
+  &,
+  &:active,
+  &.active {
+    &:focus,
+    &.focus {
+      @include tab-focus;
+    }
+  }
+
+  &:hover,
+  &:focus,
+  &.focus {
+    color: $btn-default-color;
+    text-decoration: none;
+  }
+
+  &:active,
+  &.active {
+    outline: 0;
+    background-image: none;
+    @include box-shadow(inset 0 3px 5px rgba(0,0,0,.125));
+  }
+
+  &.disabled,
+  &[disabled],
+  fieldset[disabled] & {
+    cursor: $cursor-disabled;
+    @include opacity(.65);
+    @include box-shadow(none);
+  }
+
+  // [converter] extracted a& to a.btn
+}
+
+a.btn {
+  &.disabled,
+  fieldset[disabled] & {
+    pointer-events: none; // Future-proof disabling of clicks on `<a>` elements
+  }
+}
+
+
+// Alternate buttons
+// --------------------------------------------------
+
+.btn-default {
+  @include button-variant($btn-default-color, $btn-default-bg, $btn-default-border);
+}
+.btn-primary {
+  @include button-variant($btn-primary-color, $btn-primary-bg, $btn-primary-border);
+}
+// Success appears as green
+.btn-success {
+  @include button-variant($btn-success-color, $btn-success-bg, $btn-success-border);
+}
+// Info appears as blue-green
+.btn-info {
+  @include button-variant($btn-info-color, $btn-info-bg, $btn-info-border);
+}
+// Warning appears as orange
+.btn-warning {
+  @include button-variant($btn-warning-color, $btn-warning-bg, $btn-warning-border);
+}
+// Danger and error appear as red
+.btn-danger {
+  @include button-variant($btn-danger-color, $btn-danger-bg, $btn-danger-border);
+}
+
+
+// Link buttons
+// -------------------------
+
+// Make a button look and behave like a link
+.btn-link {
+  color: $link-color;
+  font-weight: normal;
+  border-radius: 0;
+
+  &,
+  &:active,
+  &.active,
+  &[disabled],
+  fieldset[disabled] & {
+    background-color: transparent;
+    @include box-shadow(none);
+  }
+  &,
+  &:hover,
+  &:focus,
+  &:active {
+    border-color: transparent;
+  }
+  &:hover,
+  &:focus {
+    color: $link-hover-color;
+    text-decoration: $link-hover-decoration;
+    background-color: transparent;
+  }
+  &[disabled],
+  fieldset[disabled] & {
+    &:hover,
+    &:focus {
+      color: $btn-link-disabled-color;
+      text-decoration: none;
+    }
+  }
+}
+
+
+// Button Sizes
+// --------------------------------------------------
+
+.btn-lg {
+  // line-height: ensure even-numbered height of button next to large input
+  @include button-size($padding-large-vertical, $padding-large-horizontal, $font-size-large, $line-height-large, $btn-border-radius-large);
+}
+.btn-sm {
+  // line-height: ensure proper height of button next to small input
+  @include button-size($padding-small-vertical, $padding-small-horizontal, $font-size-small, $line-height-small, $btn-border-radius-small);
+}
+.btn-xs {
+  @include button-size($padding-xs-vertical, $padding-xs-horizontal, $font-size-small, $line-height-small, $btn-border-radius-small);
+}
+
+
+// Block button
+// --------------------------------------------------
+
+.btn-block {
+  display: block;
+  width: 100%;
+}
+
+// Vertically space out multiple block buttons
+.btn-block + .btn-block {
+  margin-top: 5px;
+}
+
+// Specificity overrides
+input[type="submit"],
+input[type="reset"],
+input[type="button"] {
+  &.btn-block {
+    width: 100%;
+  }
+}
diff --git a/themes/bootstrap3/scss/vendor/bootstrap/_carousel.scss b/themes/bootstrap3/scss/vendor/bootstrap/_carousel.scss
new file mode 100644
index 0000000000000000000000000000000000000000..753d881f455bc03321c48ada828687eb1ec3c2b5
--- /dev/null
+++ b/themes/bootstrap3/scss/vendor/bootstrap/_carousel.scss
@@ -0,0 +1,270 @@
+//
+// Carousel
+// --------------------------------------------------
+
+
+// Wrapper for the slide container and indicators
+.carousel {
+  position: relative;
+}
+
+.carousel-inner {
+  position: relative;
+  overflow: hidden;
+  width: 100%;
+
+  > .item {
+    display: none;
+    position: relative;
+    @include transition(.6s ease-in-out left);
+
+    // Account for jankitude on images
+    > img,
+    > a > img {
+      @include img-responsive;
+      line-height: 1;
+    }
+
+    // WebKit CSS3 transforms for supported devices
+    @media all and (transform-3d), (-webkit-transform-3d) {
+      @include transition-transform(0.6s ease-in-out);
+      @include backface-visibility(hidden);
+      @include perspective(1000px);
+
+      &.next,
+      &.active.right {
+        @include translate3d(100%, 0, 0);
+        left: 0;
+      }
+      &.prev,
+      &.active.left {
+        @include translate3d(-100%, 0, 0);
+        left: 0;
+      }
+      &.next.left,
+      &.prev.right,
+      &.active {
+        @include translate3d(0, 0, 0);
+        left: 0;
+      }
+    }
+  }
+
+  > .active,
+  > .next,
+  > .prev {
+    display: block;
+  }
+
+  > .active {
+    left: 0;
+  }
+
+  > .next,
+  > .prev {
+    position: absolute;
+    top: 0;
+    width: 100%;
+  }
+
+  > .next {
+    left: 100%;
+  }
+  > .prev {
+    left: -100%;
+  }
+  > .next.left,
+  > .prev.right {
+    left: 0;
+  }
+
+  > .active.left {
+    left: -100%;
+  }
+  > .active.right {
+    left: 100%;
+  }
+
+}
+
+// Left/right controls for nav
+// ---------------------------
+
+.carousel-control {
+  position: absolute;
+  top: 0;
+  left: 0;
+  bottom: 0;
+  width: $carousel-control-width;
+  @include opacity($carousel-control-opacity);
+  font-size: $carousel-control-font-size;
+  color: $carousel-control-color;
+  text-align: center;
+  text-shadow: $carousel-text-shadow;
+  background-color: rgba(0, 0, 0, 0); // Fix IE9 click-thru bug
+  // We can't have this transition here because WebKit cancels the carousel
+  // animation if you trip this while in the middle of another animation.
+
+  // Set gradients for backgrounds
+  &.left {
+    @include gradient-horizontal($start-color: rgba(0,0,0,.5), $end-color: rgba(0,0,0,.0001));
+  }
+  &.right {
+    left: auto;
+    right: 0;
+    @include gradient-horizontal($start-color: rgba(0,0,0,.0001), $end-color: rgba(0,0,0,.5));
+  }
+
+  // Hover/focus state
+  &:hover,
+  &:focus {
+    outline: 0;
+    color: $carousel-control-color;
+    text-decoration: none;
+    @include opacity(.9);
+  }
+
+  // Toggles
+  .icon-prev,
+  .icon-next,
+  .glyphicon-chevron-left,
+  .glyphicon-chevron-right {
+    position: absolute;
+    top: 50%;
+    margin-top: -10px;
+    z-index: 5;
+    display: inline-block;
+  }
+  .icon-prev,
+  .glyphicon-chevron-left {
+    left: 50%;
+    margin-left: -10px;
+  }
+  .icon-next,
+  .glyphicon-chevron-right {
+    right: 50%;
+    margin-right: -10px;
+  }
+  .icon-prev,
+  .icon-next {
+    width:  20px;
+    height: 20px;
+    line-height: 1;
+    font-family: serif;
+  }
+
+
+  .icon-prev {
+    &:before {
+      content: '\2039';// SINGLE LEFT-POINTING ANGLE QUOTATION MARK (U+2039)
+    }
+  }
+  .icon-next {
+    &:before {
+      content: '\203a';// SINGLE RIGHT-POINTING ANGLE QUOTATION MARK (U+203A)
+    }
+  }
+}
+
+// Optional indicator pips
+//
+// Add an unordered list with the following class and add a list item for each
+// slide your carousel holds.
+
+.carousel-indicators {
+  position: absolute;
+  bottom: 10px;
+  left: 50%;
+  z-index: 15;
+  width: 60%;
+  margin-left: -30%;
+  padding-left: 0;
+  list-style: none;
+  text-align: center;
+
+  li {
+    display: inline-block;
+    width:  10px;
+    height: 10px;
+    margin: 1px;
+    text-indent: -999px;
+    border: 1px solid $carousel-indicator-border-color;
+    border-radius: 10px;
+    cursor: pointer;
+
+    // IE8-9 hack for event handling
+    //
+    // Internet Explorer 8-9 does not support clicks on elements without a set
+    // `background-color`. We cannot use `filter` since that's not viewed as a
+    // background color by the browser. Thus, a hack is needed.
+    // See https://developer.mozilla.org/en-US/docs/Web/Events/click#Internet_Explorer
+    //
+    // For IE8, we set solid black as it doesn't support `rgba()`. For IE9, we
+    // set alpha transparency for the best results possible.
+    background-color: #000 \9; // IE8
+    background-color: rgba(0,0,0,0); // IE9
+  }
+  .active {
+    margin: 0;
+    width:  12px;
+    height: 12px;
+    background-color: $carousel-indicator-active-bg;
+  }
+}
+
+// Optional captions
+// -----------------------------
+// Hidden by default for smaller viewports
+.carousel-caption {
+  position: absolute;
+  left: 15%;
+  right: 15%;
+  bottom: 20px;
+  z-index: 10;
+  padding-top: 20px;
+  padding-bottom: 20px;
+  color: $carousel-caption-color;
+  text-align: center;
+  text-shadow: $carousel-text-shadow;
+  & .btn {
+    text-shadow: none; // No shadow for button elements in carousel-caption
+  }
+}
+
+
+// Scale up controls for tablets and up
+@media screen and (min-width: $screen-sm-min) {
+
+  // Scale up the controls a smidge
+  .carousel-control {
+    .glyphicon-chevron-left,
+    .glyphicon-chevron-right,
+    .icon-prev,
+    .icon-next {
+      width: ($carousel-control-font-size * 1.5);
+      height: ($carousel-control-font-size * 1.5);
+      margin-top: ($carousel-control-font-size / -2);
+      font-size: ($carousel-control-font-size * 1.5);
+    }
+    .glyphicon-chevron-left,
+    .icon-prev {
+      margin-left: ($carousel-control-font-size / -2);
+    }
+    .glyphicon-chevron-right,
+    .icon-next {
+      margin-right: ($carousel-control-font-size / -2);
+    }
+  }
+
+  // Show and left align the captions
+  .carousel-caption {
+    left: 20%;
+    right: 20%;
+    padding-bottom: 30px;
+  }
+
+  // Move up the indicators
+  .carousel-indicators {
+    bottom: 20px;
+  }
+}
diff --git a/themes/bootstrap3/scss/vendor/bootstrap/_close.scss b/themes/bootstrap3/scss/vendor/bootstrap/_close.scss
new file mode 100644
index 0000000000000000000000000000000000000000..3b74d8a973c2be2a1c5ae3002a5935de577dda30
--- /dev/null
+++ b/themes/bootstrap3/scss/vendor/bootstrap/_close.scss
@@ -0,0 +1,36 @@
+//
+// Close icons
+// --------------------------------------------------
+
+
+.close {
+  float: right;
+  font-size: ($font-size-base * 1.5);
+  font-weight: $close-font-weight;
+  line-height: 1;
+  color: $close-color;
+  text-shadow: $close-text-shadow;
+  @include opacity(.2);
+
+  &:hover,
+  &:focus {
+    color: $close-color;
+    text-decoration: none;
+    cursor: pointer;
+    @include opacity(.5);
+  }
+
+  // [converter] extracted button& to button.close
+}
+
+// Additional properties for button version
+// iOS requires the button element instead of an anchor tag.
+// If you want the anchor version, it requires `href="#"`.
+// See https://developer.mozilla.org/en-US/docs/Web/Events/click#Safari_Mobile
+button.close {
+  padding: 0;
+  cursor: pointer;
+  background: transparent;
+  border: 0;
+  -webkit-appearance: none;
+}
diff --git a/themes/bootstrap3/scss/vendor/bootstrap/_code.scss b/themes/bootstrap3/scss/vendor/bootstrap/_code.scss
new file mode 100644
index 0000000000000000000000000000000000000000..caa5f06304c8e575b3fabdfdc7639af6d4053cd2
--- /dev/null
+++ b/themes/bootstrap3/scss/vendor/bootstrap/_code.scss
@@ -0,0 +1,69 @@
+//
+// Code (inline and block)
+// --------------------------------------------------
+
+
+// Inline and block code styles
+code,
+kbd,
+pre,
+samp {
+  font-family: $font-family-monospace;
+}
+
+// Inline code
+code {
+  padding: 2px 4px;
+  font-size: 90%;
+  color: $code-color;
+  background-color: $code-bg;
+  border-radius: $border-radius-base;
+}
+
+// User input typically entered via keyboard
+kbd {
+  padding: 2px 4px;
+  font-size: 90%;
+  color: $kbd-color;
+  background-color: $kbd-bg;
+  border-radius: $border-radius-small;
+  box-shadow: inset 0 -1px 0 rgba(0,0,0,.25);
+
+  kbd {
+    padding: 0;
+    font-size: 100%;
+    font-weight: bold;
+    box-shadow: none;
+  }
+}
+
+// Blocks of code
+pre {
+  display: block;
+  padding: (($line-height-computed - 1) / 2);
+  margin: 0 0 ($line-height-computed / 2);
+  font-size: ($font-size-base - 1); // 14px to 13px
+  line-height: $line-height-base;
+  word-break: break-all;
+  word-wrap: break-word;
+  color: $pre-color;
+  background-color: $pre-bg;
+  border: 1px solid $pre-border-color;
+  border-radius: $border-radius-base;
+
+  // Account for some code outputs that place code tags in pre tags
+  code {
+    padding: 0;
+    font-size: inherit;
+    color: inherit;
+    white-space: pre-wrap;
+    background-color: transparent;
+    border-radius: 0;
+  }
+}
+
+// Enable scrollable blocks of code
+.pre-scrollable {
+  max-height: $pre-scrollable-max-height;
+  overflow-y: scroll;
+}
diff --git a/themes/bootstrap3/scss/vendor/bootstrap/_component-animations.scss b/themes/bootstrap3/scss/vendor/bootstrap/_component-animations.scss
new file mode 100644
index 0000000000000000000000000000000000000000..ca3b43ca788fff99fb8f46626fdb1ea2efbe9ffc
--- /dev/null
+++ b/themes/bootstrap3/scss/vendor/bootstrap/_component-animations.scss
@@ -0,0 +1,37 @@
+//
+// Component animations
+// --------------------------------------------------
+
+// Heads up!
+//
+// We don't use the `.opacity()` mixin here since it causes a bug with text
+// fields in IE7-8. Source: https://github.com/twbs/bootstrap/pull/3552.
+
+.fade {
+  opacity: 0;
+  @include transition(opacity .15s linear);
+  &.in {
+    opacity: 1;
+  }
+}
+
+.collapse {
+  display: none;
+
+  &.in      { display: block; }
+  // [converter] extracted tr&.in to tr.collapse.in
+  // [converter] extracted tbody&.in to tbody.collapse.in
+}
+
+tr.collapse.in    { display: table-row; }
+
+tbody.collapse.in { display: table-row-group; }
+
+.collapsing {
+  position: relative;
+  height: 0;
+  overflow: hidden;
+  @include transition-property(height, visibility);
+  @include transition-duration(.35s);
+  @include transition-timing-function(ease);
+}
diff --git a/themes/bootstrap3/scss/vendor/bootstrap/_dropdowns.scss b/themes/bootstrap3/scss/vendor/bootstrap/_dropdowns.scss
new file mode 100644
index 0000000000000000000000000000000000000000..aac84597a496ee873bed7e31498dce970da831ca
--- /dev/null
+++ b/themes/bootstrap3/scss/vendor/bootstrap/_dropdowns.scss
@@ -0,0 +1,216 @@
+//
+// Dropdown menus
+// --------------------------------------------------
+
+
+// Dropdown arrow/caret
+.caret {
+  display: inline-block;
+  width: 0;
+  height: 0;
+  margin-left: 2px;
+  vertical-align: middle;
+  border-top:   $caret-width-base dashed;
+  border-top:   $caret-width-base solid \9; // IE8
+  border-right: $caret-width-base solid transparent;
+  border-left:  $caret-width-base solid transparent;
+}
+
+// The dropdown wrapper (div)
+.dropup,
+.dropdown {
+  position: relative;
+}
+
+// Prevent the focus on the dropdown toggle when closing dropdowns
+.dropdown-toggle:focus {
+  outline: 0;
+}
+
+// The dropdown menu (ul)
+.dropdown-menu {
+  position: absolute;
+  top: 100%;
+  left: 0;
+  z-index: $zindex-dropdown;
+  display: none; // none by default, but block on "open" of the menu
+  float: left;
+  min-width: 160px;
+  padding: 5px 0;
+  margin: 2px 0 0; // override default ul
+  list-style: none;
+  font-size: $font-size-base;
+  text-align: left; // Ensures proper alignment if parent has it changed (e.g., modal footer)
+  background-color: $dropdown-bg;
+  border: 1px solid $dropdown-fallback-border; // IE8 fallback
+  border: 1px solid $dropdown-border;
+  border-radius: $border-radius-base;
+  @include box-shadow(0 6px 12px rgba(0,0,0,.175));
+  background-clip: padding-box;
+
+  // Aligns the dropdown menu to right
+  //
+  // Deprecated as of 3.1.0 in favor of `.dropdown-menu-[dir]`
+  &.pull-right {
+    right: 0;
+    left: auto;
+  }
+
+  // Dividers (basically an hr) within the dropdown
+  .divider {
+    @include nav-divider($dropdown-divider-bg);
+  }
+
+  // Links within the dropdown menu
+  > li > a {
+    display: block;
+    padding: 3px 20px;
+    clear: both;
+    font-weight: normal;
+    line-height: $line-height-base;
+    color: $dropdown-link-color;
+    white-space: nowrap; // prevent links from randomly breaking onto new lines
+  }
+}
+
+// Hover/Focus state
+.dropdown-menu > li > a {
+  &:hover,
+  &:focus {
+    text-decoration: none;
+    color: $dropdown-link-hover-color;
+    background-color: $dropdown-link-hover-bg;
+  }
+}
+
+// Active state
+.dropdown-menu > .active > a {
+  &,
+  &:hover,
+  &:focus {
+    color: $dropdown-link-active-color;
+    text-decoration: none;
+    outline: 0;
+    background-color: $dropdown-link-active-bg;
+  }
+}
+
+// Disabled state
+//
+// Gray out text and ensure the hover/focus state remains gray
+
+.dropdown-menu > .disabled > a {
+  &,
+  &:hover,
+  &:focus {
+    color: $dropdown-link-disabled-color;
+  }
+
+  // Nuke hover/focus effects
+  &:hover,
+  &:focus {
+    text-decoration: none;
+    background-color: transparent;
+    background-image: none; // Remove CSS gradient
+    @include reset-filter;
+    cursor: $cursor-disabled;
+  }
+}
+
+// Open state for the dropdown
+.open {
+  // Show the menu
+  > .dropdown-menu {
+    display: block;
+  }
+
+  // Remove the outline when :focus is triggered
+  > a {
+    outline: 0;
+  }
+}
+
+// Menu positioning
+//
+// Add extra class to `.dropdown-menu` to flip the alignment of the dropdown
+// menu with the parent.
+.dropdown-menu-right {
+  left: auto; // Reset the default from `.dropdown-menu`
+  right: 0;
+}
+// With v3, we enabled auto-flipping if you have a dropdown within a right
+// aligned nav component. To enable the undoing of that, we provide an override
+// to restore the default dropdown menu alignment.
+//
+// This is only for left-aligning a dropdown menu within a `.navbar-right` or
+// `.pull-right` nav component.
+.dropdown-menu-left {
+  left: 0;
+  right: auto;
+}
+
+// Dropdown section headers
+.dropdown-header {
+  display: block;
+  padding: 3px 20px;
+  font-size: $font-size-small;
+  line-height: $line-height-base;
+  color: $dropdown-header-color;
+  white-space: nowrap; // as with > li > a
+}
+
+// Backdrop to catch body clicks on mobile, etc.
+.dropdown-backdrop {
+  position: fixed;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  top: 0;
+  z-index: ($zindex-dropdown - 10);
+}
+
+// Right aligned dropdowns
+.pull-right > .dropdown-menu {
+  right: 0;
+  left: auto;
+}
+
+// Allow for dropdowns to go bottom up (aka, dropup-menu)
+//
+// Just add .dropup after the standard .dropdown class and you're set, bro.
+// TODO: abstract this so that the navbar fixed styles are not placed here?
+
+.dropup,
+.navbar-fixed-bottom .dropdown {
+  // Reverse the caret
+  .caret {
+    border-top: 0;
+    border-bottom: $caret-width-base dashed;
+    border-bottom: $caret-width-base solid \9; // IE8
+    content: "";
+  }
+  // Different positioning for bottom up menu
+  .dropdown-menu {
+    top: auto;
+    bottom: 100%;
+    margin-bottom: 2px;
+  }
+}
+
+
+// Component alignment
+//
+// Reiterate per navbar.less and the modified component alignment there.
+
+@media (min-width: $grid-float-breakpoint) {
+  .navbar-right {
+    .dropdown-menu {
+      right: 0; left: auto;
+    }
+    // Necessary for overrides of the default right aligned menu.
+    // Will remove come v4 in all likelihood.
+    .dropdown-menu-left {
+      left: 0; right: auto;
+    }
+  }
+}
diff --git a/themes/bootstrap3/scss/vendor/bootstrap/_forms.scss b/themes/bootstrap3/scss/vendor/bootstrap/_forms.scss
new file mode 100644
index 0000000000000000000000000000000000000000..ac26a6ad86b6516dbe332b847207d0fbc79660d5
--- /dev/null
+++ b/themes/bootstrap3/scss/vendor/bootstrap/_forms.scss
@@ -0,0 +1,617 @@
+//
+// Forms
+// --------------------------------------------------
+
+
+// Normalize non-controls
+//
+// Restyle and baseline non-control form elements.
+
+fieldset {
+  padding: 0;
+  margin: 0;
+  border: 0;
+  // Chrome and Firefox set a `min-width: min-content;` on fieldsets,
+  // so we reset that to ensure it behaves more like a standard block element.
+  // See https://github.com/twbs/bootstrap/issues/12359.
+  min-width: 0;
+}
+
+legend {
+  display: block;
+  width: 100%;
+  padding: 0;
+  margin-bottom: $line-height-computed;
+  font-size: ($font-size-base * 1.5);
+  line-height: inherit;
+  color: $legend-color;
+  border: 0;
+  border-bottom: 1px solid $legend-border-color;
+}
+
+label {
+  display: inline-block;
+  max-width: 100%; // Force IE8 to wrap long content (see https://github.com/twbs/bootstrap/issues/13141)
+  margin-bottom: 5px;
+  font-weight: bold;
+}
+
+
+// Normalize form controls
+//
+// While most of our form styles require extra classes, some basic normalization
+// is required to ensure optimum display with or without those classes to better
+// address browser inconsistencies.
+
+// Override content-box in Normalize (* isn't specific enough)
+input[type="search"] {
+  @include box-sizing(border-box);
+}
+
+// Position radios and checkboxes better
+input[type="radio"],
+input[type="checkbox"] {
+  margin: 4px 0 0;
+  margin-top: 1px \9; // IE8-9
+  line-height: normal;
+}
+
+input[type="file"] {
+  display: block;
+}
+
+// Make range inputs behave like textual form controls
+input[type="range"] {
+  display: block;
+  width: 100%;
+}
+
+// Make multiple select elements height not fixed
+select[multiple],
+select[size] {
+  height: auto;
+}
+
+// Focus for file, radio, and checkbox
+input[type="file"]:focus,
+input[type="radio"]:focus,
+input[type="checkbox"]:focus {
+  @include tab-focus;
+}
+
+// Adjust output element
+output {
+  display: block;
+  padding-top: ($padding-base-vertical + 1);
+  font-size: $font-size-base;
+  line-height: $line-height-base;
+  color: $input-color;
+}
+
+
+// Common form controls
+//
+// Shared size and type resets for form controls. Apply `.form-control` to any
+// of the following form controls:
+//
+// select
+// textarea
+// input[type="text"]
+// input[type="password"]
+// input[type="datetime"]
+// input[type="datetime-local"]
+// input[type="date"]
+// input[type="month"]
+// input[type="time"]
+// input[type="week"]
+// input[type="number"]
+// input[type="email"]
+// input[type="url"]
+// input[type="search"]
+// input[type="tel"]
+// input[type="color"]
+
+.form-control {
+  display: block;
+  width: 100%;
+  height: $input-height-base; // Make inputs at least the height of their button counterpart (base line-height + padding + border)
+  padding: $padding-base-vertical $padding-base-horizontal;
+  font-size: $font-size-base;
+  line-height: $line-height-base;
+  color: $input-color;
+  background-color: $input-bg;
+  background-image: none; // Reset unusual Firefox-on-Android default style; see https://github.com/necolas/normalize.css/issues/214
+  border: 1px solid $input-border;
+  border-radius: $input-border-radius; // Note: This has no effect on <select>s in some browsers, due to the limited stylability of <select>s in CSS.
+  @include box-shadow(inset 0 1px 1px rgba(0,0,0,.075));
+  @include transition(border-color ease-in-out .15s, box-shadow ease-in-out .15s);
+
+  // Customize the `:focus` state to imitate native WebKit styles.
+  @include form-control-focus;
+
+  // Placeholder
+  @include placeholder;
+
+  // Unstyle the caret on `<select>`s in IE10+.
+  &::-ms-expand {
+    border: 0;
+    background-color: transparent;
+  }
+
+  // Disabled and read-only inputs
+  //
+  // HTML5 says that controls under a fieldset > legend:first-child won't be
+  // disabled if the fieldset is disabled. Due to implementation difficulty, we
+  // don't honor that edge case; we style them as disabled anyway.
+  &[disabled],
+  &[readonly],
+  fieldset[disabled] & {
+    background-color: $input-bg-disabled;
+    opacity: 1; // iOS fix for unreadable disabled content; see https://github.com/twbs/bootstrap/issues/11655
+  }
+
+  &[disabled],
+  fieldset[disabled] & {
+    cursor: $cursor-disabled;
+  }
+
+  // [converter] extracted textarea& to textarea.form-control
+}
+
+// Reset height for `textarea`s
+textarea.form-control {
+  height: auto;
+}
+
+
+// Search inputs in iOS
+//
+// This overrides the extra rounded corners on search inputs in iOS so that our
+// `.form-control` class can properly style them. Note that this cannot simply
+// be added to `.form-control` as it's not specific enough. For details, see
+// https://github.com/twbs/bootstrap/issues/11586.
+
+input[type="search"] {
+  -webkit-appearance: none;
+}
+
+
+// Special styles for iOS temporal inputs
+//
+// In Mobile Safari, setting `display: block` on temporal inputs causes the
+// text within the input to become vertically misaligned. As a workaround, we
+// set a pixel line-height that matches the given height of the input, but only
+// for Safari. See https://bugs.webkit.org/show_bug.cgi?id=139848
+//
+// Note that as of 9.3, iOS doesn't support `week`.
+
+@media screen and (-webkit-min-device-pixel-ratio: 0) {
+  input[type="date"],
+  input[type="time"],
+  input[type="datetime-local"],
+  input[type="month"] {
+    &.form-control {
+      line-height: $input-height-base;
+    }
+
+    &.input-sm,
+    .input-group-sm & {
+      line-height: $input-height-small;
+    }
+
+    &.input-lg,
+    .input-group-lg & {
+      line-height: $input-height-large;
+    }
+  }
+}
+
+
+// Form groups
+//
+// Designed to help with the organization and spacing of vertical forms. For
+// horizontal forms, use the predefined grid classes.
+
+.form-group {
+  margin-bottom: $form-group-margin-bottom;
+}
+
+
+// Checkboxes and radios
+//
+// Indent the labels to position radios/checkboxes as hanging controls.
+
+.radio,
+.checkbox {
+  position: relative;
+  display: block;
+  margin-top: 10px;
+  margin-bottom: 10px;
+
+  label {
+    min-height: $line-height-computed; // Ensure the input doesn't jump when there is no text
+    padding-left: 20px;
+    margin-bottom: 0;
+    font-weight: normal;
+    cursor: pointer;
+  }
+}
+.radio input[type="radio"],
+.radio-inline input[type="radio"],
+.checkbox input[type="checkbox"],
+.checkbox-inline input[type="checkbox"] {
+  position: absolute;
+  margin-left: -20px;
+  margin-top: 4px \9;
+}
+
+.radio + .radio,
+.checkbox + .checkbox {
+  margin-top: -5px; // Move up sibling radios or checkboxes for tighter spacing
+}
+
+// Radios and checkboxes on same line
+.radio-inline,
+.checkbox-inline {
+  position: relative;
+  display: inline-block;
+  padding-left: 20px;
+  margin-bottom: 0;
+  vertical-align: middle;
+  font-weight: normal;
+  cursor: pointer;
+}
+.radio-inline + .radio-inline,
+.checkbox-inline + .checkbox-inline {
+  margin-top: 0;
+  margin-left: 10px; // space out consecutive inline controls
+}
+
+// Apply same disabled cursor tweak as for inputs
+// Some special care is needed because <label>s don't inherit their parent's `cursor`.
+//
+// Note: Neither radios nor checkboxes can be readonly.
+input[type="radio"],
+input[type="checkbox"] {
+  &[disabled],
+  &.disabled,
+  fieldset[disabled] & {
+    cursor: $cursor-disabled;
+  }
+}
+// These classes are used directly on <label>s
+.radio-inline,
+.checkbox-inline {
+  &.disabled,
+  fieldset[disabled] & {
+    cursor: $cursor-disabled;
+  }
+}
+// These classes are used on elements with <label> descendants
+.radio,
+.checkbox {
+  &.disabled,
+  fieldset[disabled] & {
+    label {
+      cursor: $cursor-disabled;
+    }
+  }
+}
+
+
+// Static form control text
+//
+// Apply class to a `p` element to make any string of text align with labels in
+// a horizontal form layout.
+
+.form-control-static {
+  // Size it appropriately next to real form controls
+  padding-top: ($padding-base-vertical + 1);
+  padding-bottom: ($padding-base-vertical + 1);
+  // Remove default margin from `p`
+  margin-bottom: 0;
+  min-height: ($line-height-computed + $font-size-base);
+
+  &.input-lg,
+  &.input-sm {
+    padding-left: 0;
+    padding-right: 0;
+  }
+}
+
+
+// Form control sizing
+//
+// Build on `.form-control` with modifier classes to decrease or increase the
+// height and font-size of form controls.
+//
+// The `.form-group-* form-control` variations are sadly duplicated to avoid the
+// issue documented in https://github.com/twbs/bootstrap/issues/15074.
+
+@include input-size('.input-sm', $input-height-small, $padding-small-vertical, $padding-small-horizontal, $font-size-small, $line-height-small, $input-border-radius-small);
+.form-group-sm {
+  .form-control {
+    height: $input-height-small;
+    padding: $padding-small-vertical $padding-small-horizontal;
+    font-size: $font-size-small;
+    line-height: $line-height-small;
+    border-radius: $input-border-radius-small;
+  }
+  select.form-control {
+    height: $input-height-small;
+    line-height: $input-height-small;
+  }
+  textarea.form-control,
+  select[multiple].form-control {
+    height: auto;
+  }
+  .form-control-static {
+    height: $input-height-small;
+    min-height: ($line-height-computed + $font-size-small);
+    padding: ($padding-small-vertical + 1) $padding-small-horizontal;
+    font-size: $font-size-small;
+    line-height: $line-height-small;
+  }
+}
+
+@include input-size('.input-lg', $input-height-large, $padding-large-vertical, $padding-large-horizontal, $font-size-large, $line-height-large, $input-border-radius-large);
+.form-group-lg {
+  .form-control {
+    height: $input-height-large;
+    padding: $padding-large-vertical $padding-large-horizontal;
+    font-size: $font-size-large;
+    line-height: $line-height-large;
+    border-radius: $input-border-radius-large;
+  }
+  select.form-control {
+    height: $input-height-large;
+    line-height: $input-height-large;
+  }
+  textarea.form-control,
+  select[multiple].form-control {
+    height: auto;
+  }
+  .form-control-static {
+    height: $input-height-large;
+    min-height: ($line-height-computed + $font-size-large);
+    padding: ($padding-large-vertical + 1) $padding-large-horizontal;
+    font-size: $font-size-large;
+    line-height: $line-height-large;
+  }
+}
+
+
+// Form control feedback states
+//
+// Apply contextual and semantic states to individual form controls.
+
+.has-feedback {
+  // Enable absolute positioning
+  position: relative;
+
+  // Ensure icons don't overlap text
+  .form-control {
+    padding-right: ($input-height-base * 1.25);
+  }
+}
+// Feedback icon (requires .glyphicon classes)
+.form-control-feedback {
+  position: absolute;
+  top: 0;
+  right: 0;
+  z-index: 2; // Ensure icon is above input groups
+  display: block;
+  width: $input-height-base;
+  height: $input-height-base;
+  line-height: $input-height-base;
+  text-align: center;
+  pointer-events: none;
+}
+.input-lg + .form-control-feedback,
+.input-group-lg + .form-control-feedback,
+.form-group-lg .form-control + .form-control-feedback {
+  width: $input-height-large;
+  height: $input-height-large;
+  line-height: $input-height-large;
+}
+.input-sm + .form-control-feedback,
+.input-group-sm + .form-control-feedback,
+.form-group-sm .form-control + .form-control-feedback {
+  width: $input-height-small;
+  height: $input-height-small;
+  line-height: $input-height-small;
+}
+
+// Feedback states
+.has-success {
+  @include form-control-validation($state-success-text, $state-success-text, $state-success-bg);
+}
+.has-warning {
+  @include form-control-validation($state-warning-text, $state-warning-text, $state-warning-bg);
+}
+.has-error {
+  @include form-control-validation($state-danger-text, $state-danger-text, $state-danger-bg);
+}
+
+// Reposition feedback icon if input has visible label above
+.has-feedback label {
+
+  & ~ .form-control-feedback {
+    top: ($line-height-computed + 5); // Height of the `label` and its margin
+  }
+  &.sr-only ~ .form-control-feedback {
+    top: 0;
+  }
+}
+
+
+// Help text
+//
+// Apply to any element you wish to create light text for placement immediately
+// below a form control. Use for general help, formatting, or instructional text.
+
+.help-block {
+  display: block; // account for any element using help-block
+  margin-top: 5px;
+  margin-bottom: 10px;
+  color: lighten($text-color, 25%); // lighten the text some for contrast
+}
+
+
+// Inline forms
+//
+// Make forms appear inline(-block) by adding the `.form-inline` class. Inline
+// forms begin stacked on extra small (mobile) devices and then go inline when
+// viewports reach <768px.
+//
+// Requires wrapping inputs and labels with `.form-group` for proper display of
+// default HTML form controls and our custom form controls (e.g., input groups).
+//
+// Heads up! This is mixin-ed into `.navbar-form` in navbars.less.
+
+// [converter] extracted from `.form-inline` for libsass compatibility
+@mixin form-inline {
+
+  // Kick in the inline
+  @media (min-width: $screen-sm-min) {
+    // Inline-block all the things for "inline"
+    .form-group {
+      display: inline-block;
+      margin-bottom: 0;
+      vertical-align: middle;
+    }
+
+    // In navbar-form, allow folks to *not* use `.form-group`
+    .form-control {
+      display: inline-block;
+      width: auto; // Prevent labels from stacking above inputs in `.form-group`
+      vertical-align: middle;
+    }
+
+    // Make static controls behave like regular ones
+    .form-control-static {
+      display: inline-block;
+    }
+
+    .input-group {
+      display: inline-table;
+      vertical-align: middle;
+
+      .input-group-addon,
+      .input-group-btn,
+      .form-control {
+        width: auto;
+      }
+    }
+
+    // Input groups need that 100% width though
+    .input-group > .form-control {
+      width: 100%;
+    }
+
+    .control-label {
+      margin-bottom: 0;
+      vertical-align: middle;
+    }
+
+    // Remove default margin on radios/checkboxes that were used for stacking, and
+    // then undo the floating of radios and checkboxes to match.
+    .radio,
+    .checkbox {
+      display: inline-block;
+      margin-top: 0;
+      margin-bottom: 0;
+      vertical-align: middle;
+
+      label {
+        padding-left: 0;
+      }
+    }
+    .radio input[type="radio"],
+    .checkbox input[type="checkbox"] {
+      position: relative;
+      margin-left: 0;
+    }
+
+    // Re-override the feedback icon.
+    .has-feedback .form-control-feedback {
+      top: 0;
+    }
+  }
+}
+// [converter] extracted as `@mixin form-inline` for libsass compatibility
+.form-inline {
+  @include form-inline;
+}
+
+
+
+// Horizontal forms
+//
+// Horizontal forms are built on grid classes and allow you to create forms with
+// labels on the left and inputs on the right.
+
+.form-horizontal {
+
+  // Consistent vertical alignment of radios and checkboxes
+  //
+  // Labels also get some reset styles, but that is scoped to a media query below.
+  .radio,
+  .checkbox,
+  .radio-inline,
+  .checkbox-inline {
+    margin-top: 0;
+    margin-bottom: 0;
+    padding-top: ($padding-base-vertical + 1); // Default padding plus a border
+  }
+  // Account for padding we're adding to ensure the alignment and of help text
+  // and other content below items
+  .radio,
+  .checkbox {
+    min-height: ($line-height-computed + ($padding-base-vertical + 1));
+  }
+
+  // Make form groups behave like rows
+  .form-group {
+    @include make-row;
+  }
+
+  // Reset spacing and right align labels, but scope to media queries so that
+  // labels on narrow viewports stack the same as a default form example.
+  @media (min-width: $screen-sm-min) {
+    .control-label {
+      text-align: right;
+      margin-bottom: 0;
+      padding-top: ($padding-base-vertical + 1); // Default padding plus a border
+    }
+  }
+
+  // Validation states
+  //
+  // Reposition the icon because it's now within a grid column and columns have
+  // `position: relative;` on them. Also accounts for the grid gutter padding.
+  .has-feedback .form-control-feedback {
+    right: floor(($grid-gutter-width / 2));
+  }
+
+  // Form group sizes
+  //
+  // Quick utility class for applying `.input-lg` and `.input-sm` styles to the
+  // inputs and labels within a `.form-group`.
+  .form-group-lg {
+    @media (min-width: $screen-sm-min) {
+      .control-label {
+        padding-top: ($padding-large-vertical + 1);
+        font-size: $font-size-large;
+      }
+    }
+  }
+  .form-group-sm {
+    @media (min-width: $screen-sm-min) {
+      .control-label {
+        padding-top: ($padding-small-vertical + 1);
+        font-size: $font-size-small;
+      }
+    }
+  }
+}
diff --git a/themes/bootstrap3/scss/vendor/bootstrap/_glyphicons.scss b/themes/bootstrap3/scss/vendor/bootstrap/_glyphicons.scss
new file mode 100644
index 0000000000000000000000000000000000000000..07a0fc91fe6dd8919fc4054e3134776b31170394
--- /dev/null
+++ b/themes/bootstrap3/scss/vendor/bootstrap/_glyphicons.scss
@@ -0,0 +1,307 @@
+//
+// Glyphicons for Bootstrap
+//
+// Since icons are fonts, they can be placed anywhere text is placed and are
+// thus automatically sized to match the surrounding child. To use, create an
+// inline element with the appropriate classes, like so:
+//
+// <a href="#"><span class="glyphicon glyphicon-star"></span> Star</a>
+
+@at-root {
+  // Import the fonts
+  @font-face {
+    font-family: 'Glyphicons Halflings';
+    src: url(if($bootstrap-sass-asset-helper, twbs-font-path('#{$icon-font-path}#{$icon-font-name}.eot'), '#{$icon-font-path}#{$icon-font-name}.eot'));
+    src: url(if($bootstrap-sass-asset-helper, twbs-font-path('#{$icon-font-path}#{$icon-font-name}.eot?#iefix'), '#{$icon-font-path}#{$icon-font-name}.eot?#iefix')) format('embedded-opentype'),
+         url(if($bootstrap-sass-asset-helper, twbs-font-path('#{$icon-font-path}#{$icon-font-name}.woff2'), '#{$icon-font-path}#{$icon-font-name}.woff2')) format('woff2'),
+         url(if($bootstrap-sass-asset-helper, twbs-font-path('#{$icon-font-path}#{$icon-font-name}.woff'), '#{$icon-font-path}#{$icon-font-name}.woff')) format('woff'),
+         url(if($bootstrap-sass-asset-helper, twbs-font-path('#{$icon-font-path}#{$icon-font-name}.ttf'), '#{$icon-font-path}#{$icon-font-name}.ttf')) format('truetype'),
+         url(if($bootstrap-sass-asset-helper, twbs-font-path('#{$icon-font-path}#{$icon-font-name}.svg##{$icon-font-svg-id}'), '#{$icon-font-path}#{$icon-font-name}.svg##{$icon-font-svg-id}')) format('svg');
+  }
+}
+
+// Catchall baseclass
+.glyphicon {
+  position: relative;
+  top: 1px;
+  display: inline-block;
+  font-family: 'Glyphicons Halflings';
+  font-style: normal;
+  font-weight: normal;
+  line-height: 1;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+}
+
+// Individual icons
+.glyphicon-asterisk               { &:before { content: "\002a"; } }
+.glyphicon-plus                   { &:before { content: "\002b"; } }
+.glyphicon-euro,
+.glyphicon-eur                    { &:before { content: "\20ac"; } }
+.glyphicon-minus                  { &:before { content: "\2212"; } }
+.glyphicon-cloud                  { &:before { content: "\2601"; } }
+.glyphicon-envelope               { &:before { content: "\2709"; } }
+.glyphicon-pencil                 { &:before { content: "\270f"; } }
+.glyphicon-glass                  { &:before { content: "\e001"; } }
+.glyphicon-music                  { &:before { content: "\e002"; } }
+.glyphicon-search                 { &:before { content: "\e003"; } }
+.glyphicon-heart                  { &:before { content: "\e005"; } }
+.glyphicon-star                   { &:before { content: "\e006"; } }
+.glyphicon-star-empty             { &:before { content: "\e007"; } }
+.glyphicon-user                   { &:before { content: "\e008"; } }
+.glyphicon-film                   { &:before { content: "\e009"; } }
+.glyphicon-th-large               { &:before { content: "\e010"; } }
+.glyphicon-th                     { &:before { content: "\e011"; } }
+.glyphicon-th-list                { &:before { content: "\e012"; } }
+.glyphicon-ok                     { &:before { content: "\e013"; } }
+.glyphicon-remove                 { &:before { content: "\e014"; } }
+.glyphicon-zoom-in                { &:before { content: "\e015"; } }
+.glyphicon-zoom-out               { &:before { content: "\e016"; } }
+.glyphicon-off                    { &:before { content: "\e017"; } }
+.glyphicon-signal                 { &:before { content: "\e018"; } }
+.glyphicon-cog                    { &:before { content: "\e019"; } }
+.glyphicon-trash                  { &:before { content: "\e020"; } }
+.glyphicon-home                   { &:before { content: "\e021"; } }
+.glyphicon-file                   { &:before { content: "\e022"; } }
+.glyphicon-time                   { &:before { content: "\e023"; } }
+.glyphicon-road                   { &:before { content: "\e024"; } }
+.glyphicon-download-alt           { &:before { content: "\e025"; } }
+.glyphicon-download               { &:before { content: "\e026"; } }
+.glyphicon-upload                 { &:before { content: "\e027"; } }
+.glyphicon-inbox                  { &:before { content: "\e028"; } }
+.glyphicon-play-circle            { &:before { content: "\e029"; } }
+.glyphicon-repeat                 { &:before { content: "\e030"; } }
+.glyphicon-refresh                { &:before { content: "\e031"; } }
+.glyphicon-list-alt               { &:before { content: "\e032"; } }
+.glyphicon-lock                   { &:before { content: "\e033"; } }
+.glyphicon-flag                   { &:before { content: "\e034"; } }
+.glyphicon-headphones             { &:before { content: "\e035"; } }
+.glyphicon-volume-off             { &:before { content: "\e036"; } }
+.glyphicon-volume-down            { &:before { content: "\e037"; } }
+.glyphicon-volume-up              { &:before { content: "\e038"; } }
+.glyphicon-qrcode                 { &:before { content: "\e039"; } }
+.glyphicon-barcode                { &:before { content: "\e040"; } }
+.glyphicon-tag                    { &:before { content: "\e041"; } }
+.glyphicon-tags                   { &:before { content: "\e042"; } }
+.glyphicon-book                   { &:before { content: "\e043"; } }
+.glyphicon-bookmark               { &:before { content: "\e044"; } }
+.glyphicon-print                  { &:before { content: "\e045"; } }
+.glyphicon-camera                 { &:before { content: "\e046"; } }
+.glyphicon-font                   { &:before { content: "\e047"; } }
+.glyphicon-bold                   { &:before { content: "\e048"; } }
+.glyphicon-italic                 { &:before { content: "\e049"; } }
+.glyphicon-text-height            { &:before { content: "\e050"; } }
+.glyphicon-text-width             { &:before { content: "\e051"; } }
+.glyphicon-align-left             { &:before { content: "\e052"; } }
+.glyphicon-align-center           { &:before { content: "\e053"; } }
+.glyphicon-align-right            { &:before { content: "\e054"; } }
+.glyphicon-align-justify          { &:before { content: "\e055"; } }
+.glyphicon-list                   { &:before { content: "\e056"; } }
+.glyphicon-indent-left            { &:before { content: "\e057"; } }
+.glyphicon-indent-right           { &:before { content: "\e058"; } }
+.glyphicon-facetime-video         { &:before { content: "\e059"; } }
+.glyphicon-picture                { &:before { content: "\e060"; } }
+.glyphicon-map-marker             { &:before { content: "\e062"; } }
+.glyphicon-adjust                 { &:before { content: "\e063"; } }
+.glyphicon-tint                   { &:before { content: "\e064"; } }
+.glyphicon-edit                   { &:before { content: "\e065"; } }
+.glyphicon-share                  { &:before { content: "\e066"; } }
+.glyphicon-check                  { &:before { content: "\e067"; } }
+.glyphicon-move                   { &:before { content: "\e068"; } }
+.glyphicon-step-backward          { &:before { content: "\e069"; } }
+.glyphicon-fast-backward          { &:before { content: "\e070"; } }
+.glyphicon-backward               { &:before { content: "\e071"; } }
+.glyphicon-play                   { &:before { content: "\e072"; } }
+.glyphicon-pause                  { &:before { content: "\e073"; } }
+.glyphicon-stop                   { &:before { content: "\e074"; } }
+.glyphicon-forward                { &:before { content: "\e075"; } }
+.glyphicon-fast-forward           { &:before { content: "\e076"; } }
+.glyphicon-step-forward           { &:before { content: "\e077"; } }
+.glyphicon-eject                  { &:before { content: "\e078"; } }
+.glyphicon-chevron-left           { &:before { content: "\e079"; } }
+.glyphicon-chevron-right          { &:before { content: "\e080"; } }
+.glyphicon-plus-sign              { &:before { content: "\e081"; } }
+.glyphicon-minus-sign             { &:before { content: "\e082"; } }
+.glyphicon-remove-sign            { &:before { content: "\e083"; } }
+.glyphicon-ok-sign                { &:before { content: "\e084"; } }
+.glyphicon-question-sign          { &:before { content: "\e085"; } }
+.glyphicon-info-sign              { &:before { content: "\e086"; } }
+.glyphicon-screenshot             { &:before { content: "\e087"; } }
+.glyphicon-remove-circle          { &:before { content: "\e088"; } }
+.glyphicon-ok-circle              { &:before { content: "\e089"; } }
+.glyphicon-ban-circle             { &:before { content: "\e090"; } }
+.glyphicon-arrow-left             { &:before { content: "\e091"; } }
+.glyphicon-arrow-right            { &:before { content: "\e092"; } }
+.glyphicon-arrow-up               { &:before { content: "\e093"; } }
+.glyphicon-arrow-down             { &:before { content: "\e094"; } }
+.glyphicon-share-alt              { &:before { content: "\e095"; } }
+.glyphicon-resize-full            { &:before { content: "\e096"; } }
+.glyphicon-resize-small           { &:before { content: "\e097"; } }
+.glyphicon-exclamation-sign       { &:before { content: "\e101"; } }
+.glyphicon-gift                   { &:before { content: "\e102"; } }
+.glyphicon-leaf                   { &:before { content: "\e103"; } }
+.glyphicon-fire                   { &:before { content: "\e104"; } }
+.glyphicon-eye-open               { &:before { content: "\e105"; } }
+.glyphicon-eye-close              { &:before { content: "\e106"; } }
+.glyphicon-warning-sign           { &:before { content: "\e107"; } }
+.glyphicon-plane                  { &:before { content: "\e108"; } }
+.glyphicon-calendar               { &:before { content: "\e109"; } }
+.glyphicon-random                 { &:before { content: "\e110"; } }
+.glyphicon-comment                { &:before { content: "\e111"; } }
+.glyphicon-magnet                 { &:before { content: "\e112"; } }
+.glyphicon-chevron-up             { &:before { content: "\e113"; } }
+.glyphicon-chevron-down           { &:before { content: "\e114"; } }
+.glyphicon-retweet                { &:before { content: "\e115"; } }
+.glyphicon-shopping-cart          { &:before { content: "\e116"; } }
+.glyphicon-folder-close           { &:before { content: "\e117"; } }
+.glyphicon-folder-open            { &:before { content: "\e118"; } }
+.glyphicon-resize-vertical        { &:before { content: "\e119"; } }
+.glyphicon-resize-horizontal      { &:before { content: "\e120"; } }
+.glyphicon-hdd                    { &:before { content: "\e121"; } }
+.glyphicon-bullhorn               { &:before { content: "\e122"; } }
+.glyphicon-bell                   { &:before { content: "\e123"; } }
+.glyphicon-certificate            { &:before { content: "\e124"; } }
+.glyphicon-thumbs-up              { &:before { content: "\e125"; } }
+.glyphicon-thumbs-down            { &:before { content: "\e126"; } }
+.glyphicon-hand-right             { &:before { content: "\e127"; } }
+.glyphicon-hand-left              { &:before { content: "\e128"; } }
+.glyphicon-hand-up                { &:before { content: "\e129"; } }
+.glyphicon-hand-down              { &:before { content: "\e130"; } }
+.glyphicon-circle-arrow-right     { &:before { content: "\e131"; } }
+.glyphicon-circle-arrow-left      { &:before { content: "\e132"; } }
+.glyphicon-circle-arrow-up        { &:before { content: "\e133"; } }
+.glyphicon-circle-arrow-down      { &:before { content: "\e134"; } }
+.glyphicon-globe                  { &:before { content: "\e135"; } }
+.glyphicon-wrench                 { &:before { content: "\e136"; } }
+.glyphicon-tasks                  { &:before { content: "\e137"; } }
+.glyphicon-filter                 { &:before { content: "\e138"; } }
+.glyphicon-briefcase              { &:before { content: "\e139"; } }
+.glyphicon-fullscreen             { &:before { content: "\e140"; } }
+.glyphicon-dashboard              { &:before { content: "\e141"; } }
+.glyphicon-paperclip              { &:before { content: "\e142"; } }
+.glyphicon-heart-empty            { &:before { content: "\e143"; } }
+.glyphicon-link                   { &:before { content: "\e144"; } }
+.glyphicon-phone                  { &:before { content: "\e145"; } }
+.glyphicon-pushpin                { &:before { content: "\e146"; } }
+.glyphicon-usd                    { &:before { content: "\e148"; } }
+.glyphicon-gbp                    { &:before { content: "\e149"; } }
+.glyphicon-sort                   { &:before { content: "\e150"; } }
+.glyphicon-sort-by-alphabet       { &:before { content: "\e151"; } }
+.glyphicon-sort-by-alphabet-alt   { &:before { content: "\e152"; } }
+.glyphicon-sort-by-order          { &:before { content: "\e153"; } }
+.glyphicon-sort-by-order-alt      { &:before { content: "\e154"; } }
+.glyphicon-sort-by-attributes     { &:before { content: "\e155"; } }
+.glyphicon-sort-by-attributes-alt { &:before { content: "\e156"; } }
+.glyphicon-unchecked              { &:before { content: "\e157"; } }
+.glyphicon-expand                 { &:before { content: "\e158"; } }
+.glyphicon-collapse-down          { &:before { content: "\e159"; } }
+.glyphicon-collapse-up            { &:before { content: "\e160"; } }
+.glyphicon-log-in                 { &:before { content: "\e161"; } }
+.glyphicon-flash                  { &:before { content: "\e162"; } }
+.glyphicon-log-out                { &:before { content: "\e163"; } }
+.glyphicon-new-window             { &:before { content: "\e164"; } }
+.glyphicon-record                 { &:before { content: "\e165"; } }
+.glyphicon-save                   { &:before { content: "\e166"; } }
+.glyphicon-open                   { &:before { content: "\e167"; } }
+.glyphicon-saved                  { &:before { content: "\e168"; } }
+.glyphicon-import                 { &:before { content: "\e169"; } }
+.glyphicon-export                 { &:before { content: "\e170"; } }
+.glyphicon-send                   { &:before { content: "\e171"; } }
+.glyphicon-floppy-disk            { &:before { content: "\e172"; } }
+.glyphicon-floppy-saved           { &:before { content: "\e173"; } }
+.glyphicon-floppy-remove          { &:before { content: "\e174"; } }
+.glyphicon-floppy-save            { &:before { content: "\e175"; } }
+.glyphicon-floppy-open            { &:before { content: "\e176"; } }
+.glyphicon-credit-card            { &:before { content: "\e177"; } }
+.glyphicon-transfer               { &:before { content: "\e178"; } }
+.glyphicon-cutlery                { &:before { content: "\e179"; } }
+.glyphicon-header                 { &:before { content: "\e180"; } }
+.glyphicon-compressed             { &:before { content: "\e181"; } }
+.glyphicon-earphone               { &:before { content: "\e182"; } }
+.glyphicon-phone-alt              { &:before { content: "\e183"; } }
+.glyphicon-tower                  { &:before { content: "\e184"; } }
+.glyphicon-stats                  { &:before { content: "\e185"; } }
+.glyphicon-sd-video               { &:before { content: "\e186"; } }
+.glyphicon-hd-video               { &:before { content: "\e187"; } }
+.glyphicon-subtitles              { &:before { content: "\e188"; } }
+.glyphicon-sound-stereo           { &:before { content: "\e189"; } }
+.glyphicon-sound-dolby            { &:before { content: "\e190"; } }
+.glyphicon-sound-5-1              { &:before { content: "\e191"; } }
+.glyphicon-sound-6-1              { &:before { content: "\e192"; } }
+.glyphicon-sound-7-1              { &:before { content: "\e193"; } }
+.glyphicon-copyright-mark         { &:before { content: "\e194"; } }
+.glyphicon-registration-mark      { &:before { content: "\e195"; } }
+.glyphicon-cloud-download         { &:before { content: "\e197"; } }
+.glyphicon-cloud-upload           { &:before { content: "\e198"; } }
+.glyphicon-tree-conifer           { &:before { content: "\e199"; } }
+.glyphicon-tree-deciduous         { &:before { content: "\e200"; } }
+.glyphicon-cd                     { &:before { content: "\e201"; } }
+.glyphicon-save-file              { &:before { content: "\e202"; } }
+.glyphicon-open-file              { &:before { content: "\e203"; } }
+.glyphicon-level-up               { &:before { content: "\e204"; } }
+.glyphicon-copy                   { &:before { content: "\e205"; } }
+.glyphicon-paste                  { &:before { content: "\e206"; } }
+// The following 2 Glyphicons are omitted for the time being because
+// they currently use Unicode codepoints that are outside the
+// Basic Multilingual Plane (BMP). Older buggy versions of WebKit can't handle
+// non-BMP codepoints in CSS string escapes, and thus can't display these two icons.
+// Notably, the bug affects some older versions of the Android Browser.
+// More info: https://github.com/twbs/bootstrap/issues/10106
+// .glyphicon-door                   { &:before { content: "\1f6aa"; } }
+// .glyphicon-key                    { &:before { content: "\1f511"; } }
+.glyphicon-alert                  { &:before { content: "\e209"; } }
+.glyphicon-equalizer              { &:before { content: "\e210"; } }
+.glyphicon-king                   { &:before { content: "\e211"; } }
+.glyphicon-queen                  { &:before { content: "\e212"; } }
+.glyphicon-pawn                   { &:before { content: "\e213"; } }
+.glyphicon-bishop                 { &:before { content: "\e214"; } }
+.glyphicon-knight                 { &:before { content: "\e215"; } }
+.glyphicon-baby-formula           { &:before { content: "\e216"; } }
+.glyphicon-tent                   { &:before { content: "\26fa"; } }
+.glyphicon-blackboard             { &:before { content: "\e218"; } }
+.glyphicon-bed                    { &:before { content: "\e219"; } }
+.glyphicon-apple                  { &:before { content: "\f8ff"; } }
+.glyphicon-erase                  { &:before { content: "\e221"; } }
+.glyphicon-hourglass              { &:before { content: "\231b"; } }
+.glyphicon-lamp                   { &:before { content: "\e223"; } }
+.glyphicon-duplicate              { &:before { content: "\e224"; } }
+.glyphicon-piggy-bank             { &:before { content: "\e225"; } }
+.glyphicon-scissors               { &:before { content: "\e226"; } }
+.glyphicon-bitcoin                { &:before { content: "\e227"; } }
+.glyphicon-btc                    { &:before { content: "\e227"; } }
+.glyphicon-xbt                    { &:before { content: "\e227"; } }
+.glyphicon-yen                    { &:before { content: "\00a5"; } }
+.glyphicon-jpy                    { &:before { content: "\00a5"; } }
+.glyphicon-ruble                  { &:before { content: "\20bd"; } }
+.glyphicon-rub                    { &:before { content: "\20bd"; } }
+.glyphicon-scale                  { &:before { content: "\e230"; } }
+.glyphicon-ice-lolly              { &:before { content: "\e231"; } }
+.glyphicon-ice-lolly-tasted       { &:before { content: "\e232"; } }
+.glyphicon-education              { &:before { content: "\e233"; } }
+.glyphicon-option-horizontal      { &:before { content: "\e234"; } }
+.glyphicon-option-vertical        { &:before { content: "\e235"; } }
+.glyphicon-menu-hamburger         { &:before { content: "\e236"; } }
+.glyphicon-modal-window           { &:before { content: "\e237"; } }
+.glyphicon-oil                    { &:before { content: "\e238"; } }
+.glyphicon-grain                  { &:before { content: "\e239"; } }
+.glyphicon-sunglasses             { &:before { content: "\e240"; } }
+.glyphicon-text-size              { &:before { content: "\e241"; } }
+.glyphicon-text-color             { &:before { content: "\e242"; } }
+.glyphicon-text-background        { &:before { content: "\e243"; } }
+.glyphicon-object-align-top       { &:before { content: "\e244"; } }
+.glyphicon-object-align-bottom    { &:before { content: "\e245"; } }
+.glyphicon-object-align-horizontal{ &:before { content: "\e246"; } }
+.glyphicon-object-align-left      { &:before { content: "\e247"; } }
+.glyphicon-object-align-vertical  { &:before { content: "\e248"; } }
+.glyphicon-object-align-right     { &:before { content: "\e249"; } }
+.glyphicon-triangle-right         { &:before { content: "\e250"; } }
+.glyphicon-triangle-left          { &:before { content: "\e251"; } }
+.glyphicon-triangle-bottom        { &:before { content: "\e252"; } }
+.glyphicon-triangle-top           { &:before { content: "\e253"; } }
+.glyphicon-console                { &:before { content: "\e254"; } }
+.glyphicon-superscript            { &:before { content: "\e255"; } }
+.glyphicon-subscript              { &:before { content: "\e256"; } }
+.glyphicon-menu-left              { &:before { content: "\e257"; } }
+.glyphicon-menu-right             { &:before { content: "\e258"; } }
+.glyphicon-menu-down              { &:before { content: "\e259"; } }
+.glyphicon-menu-up                { &:before { content: "\e260"; } }
diff --git a/themes/bootstrap3/scss/vendor/bootstrap/_grid.scss b/themes/bootstrap3/scss/vendor/bootstrap/_grid.scss
new file mode 100644
index 0000000000000000000000000000000000000000..b15ca27bb546898d56f9f13a8c600c593b7f824e
--- /dev/null
+++ b/themes/bootstrap3/scss/vendor/bootstrap/_grid.scss
@@ -0,0 +1,84 @@
+//
+// Grid system
+// --------------------------------------------------
+
+
+// Container widths
+//
+// Set the container width, and override it for fixed navbars in media queries.
+
+.container {
+  @include container-fixed;
+
+  @media (min-width: $screen-sm-min) {
+    width: $container-sm;
+  }
+  @media (min-width: $screen-md-min) {
+    width: $container-md;
+  }
+  @media (min-width: $screen-lg-min) {
+    width: $container-lg;
+  }
+}
+
+
+// Fluid container
+//
+// Utilizes the mixin meant for fixed width containers, but without any defined
+// width for fluid, full width layouts.
+
+.container-fluid {
+  @include container-fixed;
+}
+
+
+// Row
+//
+// Rows contain and clear the floats of your columns.
+
+.row {
+  @include make-row;
+}
+
+
+// Columns
+//
+// Common styles for small and large grid columns
+
+@include make-grid-columns;
+
+
+// Extra small grid
+//
+// Columns, offsets, pushes, and pulls for extra small devices like
+// smartphones.
+
+@include make-grid(xs);
+
+
+// Small grid
+//
+// Columns, offsets, pushes, and pulls for the small device range, from phones
+// to tablets.
+
+@media (min-width: $screen-sm-min) {
+  @include make-grid(sm);
+}
+
+
+// Medium grid
+//
+// Columns, offsets, pushes, and pulls for the desktop device range.
+
+@media (min-width: $screen-md-min) {
+  @include make-grid(md);
+}
+
+
+// Large grid
+//
+// Columns, offsets, pushes, and pulls for the large desktop device range.
+
+@media (min-width: $screen-lg-min) {
+  @include make-grid(lg);
+}
diff --git a/themes/bootstrap3/scss/vendor/bootstrap/_input-groups.scss b/themes/bootstrap3/scss/vendor/bootstrap/_input-groups.scss
new file mode 100644
index 0000000000000000000000000000000000000000..81f46f3e6161ce26fbfbdbddb278fdbd3a5275c4
--- /dev/null
+++ b/themes/bootstrap3/scss/vendor/bootstrap/_input-groups.scss
@@ -0,0 +1,171 @@
+//
+// Input groups
+// --------------------------------------------------
+
+// Base styles
+// -------------------------
+.input-group {
+  position: relative; // For dropdowns
+  display: table;
+  border-collapse: separate; // prevent input groups from inheriting border styles from table cells when placed within a table
+
+  // Undo padding and float of grid classes
+  &[class*="col-"] {
+    float: none;
+    padding-left: 0;
+    padding-right: 0;
+  }
+
+  .form-control {
+    // Ensure that the input is always above the *appended* addon button for
+    // proper border colors.
+    position: relative;
+    z-index: 2;
+
+    // IE9 fubars the placeholder attribute in text inputs and the arrows on
+    // select elements in input groups. To fix it, we float the input. Details:
+    // https://github.com/twbs/bootstrap/issues/11561#issuecomment-28936855
+    float: left;
+
+    width: 100%;
+    margin-bottom: 0;
+
+    &:focus {
+      z-index: 3;
+    }
+  }
+}
+
+// Sizing options
+//
+// Remix the default form control sizing classes into new ones for easier
+// manipulation.
+
+.input-group-lg > .form-control,
+.input-group-lg > .input-group-addon,
+.input-group-lg > .input-group-btn > .btn {
+  @extend .input-lg;
+}
+.input-group-sm > .form-control,
+.input-group-sm > .input-group-addon,
+.input-group-sm > .input-group-btn > .btn {
+  @extend .input-sm;
+}
+
+
+// Display as table-cell
+// -------------------------
+.input-group-addon,
+.input-group-btn,
+.input-group .form-control {
+  display: table-cell;
+
+  &:not(:first-child):not(:last-child) {
+    border-radius: 0;
+  }
+}
+// Addon and addon wrapper for buttons
+.input-group-addon,
+.input-group-btn {
+  width: 1%;
+  white-space: nowrap;
+  vertical-align: middle; // Match the inputs
+}
+
+// Text input groups
+// -------------------------
+.input-group-addon {
+  padding: $padding-base-vertical $padding-base-horizontal;
+  font-size: $font-size-base;
+  font-weight: normal;
+  line-height: 1;
+  color: $input-color;
+  text-align: center;
+  background-color: $input-group-addon-bg;
+  border: 1px solid $input-group-addon-border-color;
+  border-radius: $input-border-radius;
+
+  // Sizing
+  &.input-sm {
+    padding: $padding-small-vertical $padding-small-horizontal;
+    font-size: $font-size-small;
+    border-radius: $input-border-radius-small;
+  }
+  &.input-lg {
+    padding: $padding-large-vertical $padding-large-horizontal;
+    font-size: $font-size-large;
+    border-radius: $input-border-radius-large;
+  }
+
+  // Nuke default margins from checkboxes and radios to vertically center within.
+  input[type="radio"],
+  input[type="checkbox"] {
+    margin-top: 0;
+  }
+}
+
+// Reset rounded corners
+.input-group .form-control:first-child,
+.input-group-addon:first-child,
+.input-group-btn:first-child > .btn,
+.input-group-btn:first-child > .btn-group > .btn,
+.input-group-btn:first-child > .dropdown-toggle,
+.input-group-btn:last-child > .btn:not(:last-child):not(.dropdown-toggle),
+.input-group-btn:last-child > .btn-group:not(:last-child) > .btn {
+  @include border-right-radius(0);
+}
+.input-group-addon:first-child {
+  border-right: 0;
+}
+.input-group .form-control:last-child,
+.input-group-addon:last-child,
+.input-group-btn:last-child > .btn,
+.input-group-btn:last-child > .btn-group > .btn,
+.input-group-btn:last-child > .dropdown-toggle,
+.input-group-btn:first-child > .btn:not(:first-child),
+.input-group-btn:first-child > .btn-group:not(:first-child) > .btn {
+  @include border-left-radius(0);
+}
+.input-group-addon:last-child {
+  border-left: 0;
+}
+
+// Button input groups
+// -------------------------
+.input-group-btn {
+  position: relative;
+  // Jankily prevent input button groups from wrapping with `white-space` and
+  // `font-size` in combination with `inline-block` on buttons.
+  font-size: 0;
+  white-space: nowrap;
+
+  // Negative margin for spacing, position for bringing hovered/focused/actived
+  // element above the siblings.
+  > .btn {
+    position: relative;
+    + .btn {
+      margin-left: -1px;
+    }
+    // Bring the "active" button to the front
+    &:hover,
+    &:focus,
+    &:active {
+      z-index: 2;
+    }
+  }
+
+  // Negative margin to only have a 1px border between the two
+  &:first-child {
+    > .btn,
+    > .btn-group {
+      margin-right: -1px;
+    }
+  }
+  &:last-child {
+    > .btn,
+    > .btn-group {
+      z-index: 2;
+      margin-left: -1px;
+    }
+  }
+}
diff --git a/themes/bootstrap3/scss/vendor/bootstrap/_jumbotron.scss b/themes/bootstrap3/scss/vendor/bootstrap/_jumbotron.scss
new file mode 100644
index 0000000000000000000000000000000000000000..a27da4738fe71de9fa737c1cfadc92b11975b4d6
--- /dev/null
+++ b/themes/bootstrap3/scss/vendor/bootstrap/_jumbotron.scss
@@ -0,0 +1,54 @@
+//
+// Jumbotron
+// --------------------------------------------------
+
+
+.jumbotron {
+  padding-top:    $jumbotron-padding;
+  padding-bottom: $jumbotron-padding;
+  margin-bottom: $jumbotron-padding;
+  color: $jumbotron-color;
+  background-color: $jumbotron-bg;
+
+  h1,
+  .h1 {
+    color: $jumbotron-heading-color;
+  }
+
+  p {
+    margin-bottom: ($jumbotron-padding / 2);
+    font-size: $jumbotron-font-size;
+    font-weight: 200;
+  }
+
+  > hr {
+    border-top-color: darken($jumbotron-bg, 10%);
+  }
+
+  .container &,
+  .container-fluid & {
+    border-radius: $border-radius-large; // Only round corners at higher resolutions if contained in a container
+    padding-left:  ($grid-gutter-width / 2);
+    padding-right: ($grid-gutter-width / 2);
+  }
+
+  .container {
+    max-width: 100%;
+  }
+
+  @media screen and (min-width: $screen-sm-min) {
+    padding-top:    ($jumbotron-padding * 1.6);
+    padding-bottom: ($jumbotron-padding * 1.6);
+
+    .container &,
+    .container-fluid & {
+      padding-left:  ($jumbotron-padding * 2);
+      padding-right: ($jumbotron-padding * 2);
+    }
+
+    h1,
+    .h1 {
+      font-size: $jumbotron-heading-font-size;
+    }
+  }
+}
diff --git a/themes/bootstrap3/scss/vendor/bootstrap/_labels.scss b/themes/bootstrap3/scss/vendor/bootstrap/_labels.scss
new file mode 100644
index 0000000000000000000000000000000000000000..42ed6ea123273dc395095e96801d2dbefc4e53cb
--- /dev/null
+++ b/themes/bootstrap3/scss/vendor/bootstrap/_labels.scss
@@ -0,0 +1,66 @@
+//
+// Labels
+// --------------------------------------------------
+
+.label {
+  display: inline;
+  padding: .2em .6em .3em;
+  font-size: 75%;
+  font-weight: bold;
+  line-height: 1;
+  color: $label-color;
+  text-align: center;
+  white-space: nowrap;
+  vertical-align: baseline;
+  border-radius: .25em;
+
+  // [converter] extracted a& to a.label
+
+  // Empty labels collapse automatically (not available in IE8)
+  &:empty {
+    display: none;
+  }
+
+  // Quick fix for labels in buttons
+  .btn & {
+    position: relative;
+    top: -1px;
+  }
+}
+
+// Add hover effects, but only for links
+a.label {
+  &:hover,
+  &:focus {
+    color: $label-link-hover-color;
+    text-decoration: none;
+    cursor: pointer;
+  }
+}
+
+// Colors
+// Contextual variations (linked labels get darker on :hover)
+
+.label-default {
+  @include label-variant($label-default-bg);
+}
+
+.label-primary {
+  @include label-variant($label-primary-bg);
+}
+
+.label-success {
+  @include label-variant($label-success-bg);
+}
+
+.label-info {
+  @include label-variant($label-info-bg);
+}
+
+.label-warning {
+  @include label-variant($label-warning-bg);
+}
+
+.label-danger {
+  @include label-variant($label-danger-bg);
+}
diff --git a/themes/bootstrap3/scss/vendor/bootstrap/_list-group.scss b/themes/bootstrap3/scss/vendor/bootstrap/_list-group.scss
new file mode 100644
index 0000000000000000000000000000000000000000..7cb83aab05ca09849795a5d8e20527da0e0dc941
--- /dev/null
+++ b/themes/bootstrap3/scss/vendor/bootstrap/_list-group.scss
@@ -0,0 +1,130 @@
+//
+// List groups
+// --------------------------------------------------
+
+
+// Base class
+//
+// Easily usable on <ul>, <ol>, or <div>.
+
+.list-group {
+  // No need to set list-style: none; since .list-group-item is block level
+  margin-bottom: 20px;
+  padding-left: 0; // reset padding because ul and ol
+}
+
+
+// Individual list items
+//
+// Use on `li`s or `div`s within the `.list-group` parent.
+
+.list-group-item {
+  position: relative;
+  display: block;
+  padding: 10px 15px;
+  // Place the border on the list items and negative margin up for better styling
+  margin-bottom: -1px;
+  background-color: $list-group-bg;
+  border: 1px solid $list-group-border;
+
+  // Round the first and last items
+  &:first-child {
+    @include border-top-radius($list-group-border-radius);
+  }
+  &:last-child {
+    margin-bottom: 0;
+    @include border-bottom-radius($list-group-border-radius);
+  }
+}
+
+
+// Interactive list items
+//
+// Use anchor or button elements instead of `li`s or `div`s to create interactive items.
+// Includes an extra `.active` modifier class for showing selected items.
+
+a.list-group-item,
+button.list-group-item {
+  color: $list-group-link-color;
+
+  .list-group-item-heading {
+    color: $list-group-link-heading-color;
+  }
+
+  // Hover state
+  &:hover,
+  &:focus {
+    text-decoration: none;
+    color: $list-group-link-hover-color;
+    background-color: $list-group-hover-bg;
+  }
+}
+
+button.list-group-item {
+  width: 100%;
+  text-align: left;
+}
+
+.list-group-item {
+  // Disabled state
+  &.disabled,
+  &.disabled:hover,
+  &.disabled:focus {
+    background-color: $list-group-disabled-bg;
+    color: $list-group-disabled-color;
+    cursor: $cursor-disabled;
+
+    // Force color to inherit for custom content
+    .list-group-item-heading {
+      color: inherit;
+    }
+    .list-group-item-text {
+      color: $list-group-disabled-text-color;
+    }
+  }
+
+  // Active class on item itself, not parent
+  &.active,
+  &.active:hover,
+  &.active:focus {
+    z-index: 2; // Place active items above their siblings for proper border styling
+    color: $list-group-active-color;
+    background-color: $list-group-active-bg;
+    border-color: $list-group-active-border;
+
+    // Force color to inherit for custom content
+    .list-group-item-heading,
+    .list-group-item-heading > small,
+    .list-group-item-heading > .small {
+      color: inherit;
+    }
+    .list-group-item-text {
+      color: $list-group-active-text-color;
+    }
+  }
+}
+
+
+// Contextual variants
+//
+// Add modifier classes to change text and background color on individual items.
+// Organizationally, this must come after the `:hover` states.
+
+@include list-group-item-variant(success, $state-success-bg, $state-success-text);
+@include list-group-item-variant(info, $state-info-bg, $state-info-text);
+@include list-group-item-variant(warning, $state-warning-bg, $state-warning-text);
+@include list-group-item-variant(danger, $state-danger-bg, $state-danger-text);
+
+
+// Custom content options
+//
+// Extra classes for creating well-formatted content within `.list-group-item`s.
+
+.list-group-item-heading {
+  margin-top: 0;
+  margin-bottom: 5px;
+}
+.list-group-item-text {
+  margin-bottom: 0;
+  line-height: 1.3;
+}
diff --git a/themes/bootstrap3/scss/vendor/bootstrap/_media.scss b/themes/bootstrap3/scss/vendor/bootstrap/_media.scss
new file mode 100644
index 0000000000000000000000000000000000000000..8c835e861edf28f7c3c49f18caf09d626565e11d
--- /dev/null
+++ b/themes/bootstrap3/scss/vendor/bootstrap/_media.scss
@@ -0,0 +1,66 @@
+.media {
+  // Proper spacing between instances of .media
+  margin-top: 15px;
+
+  &:first-child {
+    margin-top: 0;
+  }
+}
+
+.media,
+.media-body {
+  zoom: 1;
+  overflow: hidden;
+}
+
+.media-body {
+  width: 10000px;
+}
+
+.media-object {
+  display: block;
+
+  // Fix collapse in webkit from max-width: 100% and display: table-cell.
+  &.img-thumbnail {
+    max-width: none;
+  }
+}
+
+.media-right,
+.media > .pull-right {
+  padding-left: 10px;
+}
+
+.media-left,
+.media > .pull-left {
+  padding-right: 10px;
+}
+
+.media-left,
+.media-right,
+.media-body {
+  display: table-cell;
+  vertical-align: top;
+}
+
+.media-middle {
+  vertical-align: middle;
+}
+
+.media-bottom {
+  vertical-align: bottom;
+}
+
+// Reset margins on headings for tighter default spacing
+.media-heading {
+  margin-top: 0;
+  margin-bottom: 5px;
+}
+
+// Media list variation
+//
+// Undo default ul/ol styles
+.media-list {
+  padding-left: 0;
+  list-style: none;
+}
diff --git a/themes/bootstrap3/scss/vendor/bootstrap/_mixins.scss b/themes/bootstrap3/scss/vendor/bootstrap/_mixins.scss
new file mode 100644
index 0000000000000000000000000000000000000000..78cd5aa0ff34a647fe9a99be3b8fe4e063cbe8af
--- /dev/null
+++ b/themes/bootstrap3/scss/vendor/bootstrap/_mixins.scss
@@ -0,0 +1,40 @@
+// Mixins
+// --------------------------------------------------
+
+// Utilities
+@import "mixins/hide-text";
+@import "mixins/opacity";
+@import "mixins/image";
+@import "mixins/labels";
+@import "mixins/reset-filter";
+@import "mixins/resize";
+@import "mixins/responsive-visibility";
+@import "mixins/size";
+@import "mixins/tab-focus";
+@import "mixins/reset-text";
+@import "mixins/text-emphasis";
+@import "mixins/text-overflow";
+@import "mixins/vendor-prefixes";
+
+// Components
+@import "mixins/alerts";
+@import "mixins/buttons";
+@import "mixins/panels";
+@import "mixins/pagination";
+@import "mixins/list-group";
+@import "mixins/nav-divider";
+@import "mixins/forms";
+@import "mixins/progress-bar";
+@import "mixins/table-row";
+
+// Skins
+@import "mixins/background-variant";
+@import "mixins/border-radius";
+@import "mixins/gradients";
+
+// Layout
+@import "mixins/clearfix";
+@import "mixins/center-block";
+@import "mixins/nav-vertical-align";
+@import "mixins/grid-framework";
+@import "mixins/grid";
diff --git a/themes/bootstrap3/scss/vendor/bootstrap/_modals.scss b/themes/bootstrap3/scss/vendor/bootstrap/_modals.scss
new file mode 100644
index 0000000000000000000000000000000000000000..823870f2a4daa5323adb6a0ae0c2355562674afe
--- /dev/null
+++ b/themes/bootstrap3/scss/vendor/bootstrap/_modals.scss
@@ -0,0 +1,150 @@
+//
+// Modals
+// --------------------------------------------------
+
+// .modal-open      - body class for killing the scroll
+// .modal           - container to scroll within
+// .modal-dialog    - positioning shell for the actual modal
+// .modal-content   - actual modal w/ bg and corners and shit
+
+// Kill the scroll on the body
+.modal-open {
+  overflow: hidden;
+}
+
+// Container that the modal scrolls within
+.modal {
+  display: none;
+  overflow: hidden;
+  position: fixed;
+  top: 0;
+  right: 0;
+  bottom: 0;
+  left: 0;
+  z-index: $zindex-modal;
+  -webkit-overflow-scrolling: touch;
+
+  // Prevent Chrome on Windows from adding a focus outline. For details, see
+  // https://github.com/twbs/bootstrap/pull/10951.
+  outline: 0;
+
+  // When fading in the modal, animate it to slide down
+  &.fade .modal-dialog {
+    @include translate(0, -25%);
+    @include transition-transform(0.3s ease-out);
+  }
+  &.in .modal-dialog { @include translate(0, 0) }
+}
+.modal-open .modal {
+  overflow-x: hidden;
+  overflow-y: auto;
+}
+
+// Shell div to position the modal with bottom padding
+.modal-dialog {
+  position: relative;
+  width: auto;
+  margin: 10px;
+}
+
+// Actual modal
+.modal-content {
+  position: relative;
+  background-color: $modal-content-bg;
+  border: 1px solid $modal-content-fallback-border-color; //old browsers fallback (ie8 etc)
+  border: 1px solid $modal-content-border-color;
+  border-radius: $border-radius-large;
+  @include box-shadow(0 3px 9px rgba(0,0,0,.5));
+  background-clip: padding-box;
+  // Remove focus outline from opened modal
+  outline: 0;
+}
+
+// Modal background
+.modal-backdrop {
+  position: fixed;
+  top: 0;
+  right: 0;
+  bottom: 0;
+  left: 0;
+  z-index: $zindex-modal-background;
+  background-color: $modal-backdrop-bg;
+  // Fade for backdrop
+  &.fade { @include opacity(0); }
+  &.in { @include opacity($modal-backdrop-opacity); }
+}
+
+// Modal header
+// Top section of the modal w/ title and dismiss
+.modal-header {
+  padding: $modal-title-padding;
+  border-bottom: 1px solid $modal-header-border-color;
+  @include clearfix;
+}
+// Close icon
+.modal-header .close {
+  margin-top: -2px;
+}
+
+// Title text within header
+.modal-title {
+  margin: 0;
+  line-height: $modal-title-line-height;
+}
+
+// Modal body
+// Where all modal content resides (sibling of .modal-header and .modal-footer)
+.modal-body {
+  position: relative;
+  padding: $modal-inner-padding;
+}
+
+// Footer (for actions)
+.modal-footer {
+  padding: $modal-inner-padding;
+  text-align: right; // right align buttons
+  border-top: 1px solid $modal-footer-border-color;
+  @include clearfix; // clear it in case folks use .pull-* classes on buttons
+
+  // Properly space out buttons
+  .btn + .btn {
+    margin-left: 5px;
+    margin-bottom: 0; // account for input[type="submit"] which gets the bottom margin like all other inputs
+  }
+  // but override that for button groups
+  .btn-group .btn + .btn {
+    margin-left: -1px;
+  }
+  // and override it for block buttons as well
+  .btn-block + .btn-block {
+    margin-left: 0;
+  }
+}
+
+// Measure scrollbar width for padding body during modal show/hide
+.modal-scrollbar-measure {
+  position: absolute;
+  top: -9999px;
+  width: 50px;
+  height: 50px;
+  overflow: scroll;
+}
+
+// Scale up the modal
+@media (min-width: $screen-sm-min) {
+  // Automatically set modal's width for larger viewports
+  .modal-dialog {
+    width: $modal-md;
+    margin: 30px auto;
+  }
+  .modal-content {
+    @include box-shadow(0 5px 15px rgba(0,0,0,.5));
+  }
+
+  // Modal sizes
+  .modal-sm { width: $modal-sm; }
+}
+
+@media (min-width: $screen-md-min) {
+  .modal-lg { width: $modal-lg; }
+}
diff --git a/themes/bootstrap3/scss/vendor/bootstrap/_navbar.scss b/themes/bootstrap3/scss/vendor/bootstrap/_navbar.scss
new file mode 100644
index 0000000000000000000000000000000000000000..11e5c01c15850511711675f0e5a009312e287db2
--- /dev/null
+++ b/themes/bootstrap3/scss/vendor/bootstrap/_navbar.scss
@@ -0,0 +1,662 @@
+//
+// Navbars
+// --------------------------------------------------
+
+
+// Wrapper and base class
+//
+// Provide a static navbar from which we expand to create full-width, fixed, and
+// other navbar variations.
+
+.navbar {
+  position: relative;
+  min-height: $navbar-height; // Ensure a navbar always shows (e.g., without a .navbar-brand in collapsed mode)
+  margin-bottom: $navbar-margin-bottom;
+  border: 1px solid transparent;
+
+  // Prevent floats from breaking the navbar
+  @include clearfix;
+
+  @media (min-width: $grid-float-breakpoint) {
+    border-radius: $navbar-border-radius;
+  }
+}
+
+
+// Navbar heading
+//
+// Groups `.navbar-brand` and `.navbar-toggle` into a single component for easy
+// styling of responsive aspects.
+
+.navbar-header {
+  @include clearfix;
+
+  @media (min-width: $grid-float-breakpoint) {
+    float: left;
+  }
+}
+
+
+// Navbar collapse (body)
+//
+// Group your navbar content into this for easy collapsing and expanding across
+// various device sizes. By default, this content is collapsed when <768px, but
+// will expand past that for a horizontal display.
+//
+// To start (on mobile devices) the navbar links, forms, and buttons are stacked
+// vertically and include a `max-height` to overflow in case you have too much
+// content for the user's viewport.
+
+.navbar-collapse {
+  overflow-x: visible;
+  padding-right: $navbar-padding-horizontal;
+  padding-left:  $navbar-padding-horizontal;
+  border-top: 1px solid transparent;
+  box-shadow: inset 0 1px 0 rgba(255,255,255,.1);
+  @include clearfix;
+  -webkit-overflow-scrolling: touch;
+
+  &.in {
+    overflow-y: auto;
+  }
+
+  @media (min-width: $grid-float-breakpoint) {
+    width: auto;
+    border-top: 0;
+    box-shadow: none;
+
+    &.collapse {
+      display: block !important;
+      height: auto !important;
+      padding-bottom: 0; // Override default setting
+      overflow: visible !important;
+    }
+
+    &.in {
+      overflow-y: visible;
+    }
+
+    // Undo the collapse side padding for navbars with containers to ensure
+    // alignment of right-aligned contents.
+    .navbar-fixed-top &,
+    .navbar-static-top &,
+    .navbar-fixed-bottom & {
+      padding-left: 0;
+      padding-right: 0;
+    }
+  }
+}
+
+.navbar-fixed-top,
+.navbar-fixed-bottom {
+  .navbar-collapse {
+    max-height: $navbar-collapse-max-height;
+
+    @media (max-device-width: $screen-xs-min) and (orientation: landscape) {
+      max-height: 200px;
+    }
+  }
+}
+
+
+// Both navbar header and collapse
+//
+// When a container is present, change the behavior of the header and collapse.
+
+.container,
+.container-fluid {
+  > .navbar-header,
+  > .navbar-collapse {
+    margin-right: -$navbar-padding-horizontal;
+    margin-left:  -$navbar-padding-horizontal;
+
+    @media (min-width: $grid-float-breakpoint) {
+      margin-right: 0;
+      margin-left:  0;
+    }
+  }
+}
+
+
+//
+// Navbar alignment options
+//
+// Display the navbar across the entirety of the page or fixed it to the top or
+// bottom of the page.
+
+// Static top (unfixed, but 100% wide) navbar
+.navbar-static-top {
+  z-index: $zindex-navbar;
+  border-width: 0 0 1px;
+
+  @media (min-width: $grid-float-breakpoint) {
+    border-radius: 0;
+  }
+}
+
+// Fix the top/bottom navbars when screen real estate supports it
+.navbar-fixed-top,
+.navbar-fixed-bottom {
+  position: fixed;
+  right: 0;
+  left: 0;
+  z-index: $zindex-navbar-fixed;
+
+  // Undo the rounded corners
+  @media (min-width: $grid-float-breakpoint) {
+    border-radius: 0;
+  }
+}
+.navbar-fixed-top {
+  top: 0;
+  border-width: 0 0 1px;
+}
+.navbar-fixed-bottom {
+  bottom: 0;
+  margin-bottom: 0; // override .navbar defaults
+  border-width: 1px 0 0;
+}
+
+
+// Brand/project name
+
+.navbar-brand {
+  float: left;
+  padding: $navbar-padding-vertical $navbar-padding-horizontal;
+  font-size: $font-size-large;
+  line-height: $line-height-computed;
+  height: $navbar-height;
+
+  &:hover,
+  &:focus {
+    text-decoration: none;
+  }
+
+  > img {
+    display: block;
+  }
+
+  @media (min-width: $grid-float-breakpoint) {
+    .navbar > .container &,
+    .navbar > .container-fluid & {
+      margin-left: -$navbar-padding-horizontal;
+    }
+  }
+}
+
+
+// Navbar toggle
+//
+// Custom button for toggling the `.navbar-collapse`, powered by the collapse
+// JavaScript plugin.
+
+.navbar-toggle {
+  position: relative;
+  float: right;
+  margin-right: $navbar-padding-horizontal;
+  padding: 9px 10px;
+  @include navbar-vertical-align(34px);
+  background-color: transparent;
+  background-image: none; // Reset unusual Firefox-on-Android default style; see https://github.com/necolas/normalize.css/issues/214
+  border: 1px solid transparent;
+  border-radius: $border-radius-base;
+
+  // We remove the `outline` here, but later compensate by attaching `:hover`
+  // styles to `:focus`.
+  &:focus {
+    outline: 0;
+  }
+
+  // Bars
+  .icon-bar {
+    display: block;
+    width: 22px;
+    height: 2px;
+    border-radius: 1px;
+  }
+  .icon-bar + .icon-bar {
+    margin-top: 4px;
+  }
+
+  @media (min-width: $grid-float-breakpoint) {
+    display: none;
+  }
+}
+
+
+// Navbar nav links
+//
+// Builds on top of the `.nav` components with its own modifier class to make
+// the nav the full height of the horizontal nav (above 768px).
+
+.navbar-nav {
+  margin: ($navbar-padding-vertical / 2) (-$navbar-padding-horizontal);
+
+  > li > a {
+    padding-top:    10px;
+    padding-bottom: 10px;
+    line-height: $line-height-computed;
+  }
+
+  @media (max-width: $grid-float-breakpoint-max) {
+    // Dropdowns get custom display when collapsed
+    .open .dropdown-menu {
+      position: static;
+      float: none;
+      width: auto;
+      margin-top: 0;
+      background-color: transparent;
+      border: 0;
+      box-shadow: none;
+      > li > a,
+      .dropdown-header {
+        padding: 5px 15px 5px 25px;
+      }
+      > li > a {
+        line-height: $line-height-computed;
+        &:hover,
+        &:focus {
+          background-image: none;
+        }
+      }
+    }
+  }
+
+  // Uncollapse the nav
+  @media (min-width: $grid-float-breakpoint) {
+    float: left;
+    margin: 0;
+
+    > li {
+      float: left;
+      > a {
+        padding-top:    $navbar-padding-vertical;
+        padding-bottom: $navbar-padding-vertical;
+      }
+    }
+  }
+}
+
+
+// Navbar form
+//
+// Extension of the `.form-inline` with some extra flavor for optimum display in
+// our navbars.
+
+.navbar-form {
+  margin-left: -$navbar-padding-horizontal;
+  margin-right: -$navbar-padding-horizontal;
+  padding: 10px $navbar-padding-horizontal;
+  border-top: 1px solid transparent;
+  border-bottom: 1px solid transparent;
+  $shadow: inset 0 1px 0 rgba(255,255,255,.1), 0 1px 0 rgba(255,255,255,.1);
+  @include box-shadow($shadow);
+
+  // Mixin behavior for optimum display
+  @include form-inline;
+
+  .form-group {
+    @media (max-width: $grid-float-breakpoint-max) {
+      margin-bottom: 5px;
+
+      &:last-child {
+        margin-bottom: 0;
+      }
+    }
+  }
+
+  // Vertically center in expanded, horizontal navbar
+  @include navbar-vertical-align($input-height-base);
+
+  // Undo 100% width for pull classes
+  @media (min-width: $grid-float-breakpoint) {
+    width: auto;
+    border: 0;
+    margin-left: 0;
+    margin-right: 0;
+    padding-top: 0;
+    padding-bottom: 0;
+    @include box-shadow(none);
+  }
+}
+
+
+// Dropdown menus
+
+// Menu position and menu carets
+.navbar-nav > li > .dropdown-menu {
+  margin-top: 0;
+  @include border-top-radius(0);
+}
+// Menu position and menu caret support for dropups via extra dropup class
+.navbar-fixed-bottom .navbar-nav > li > .dropdown-menu {
+  margin-bottom: 0;
+  @include border-top-radius($navbar-border-radius);
+  @include border-bottom-radius(0);
+}
+
+
+// Buttons in navbars
+//
+// Vertically center a button within a navbar (when *not* in a form).
+
+.navbar-btn {
+  @include navbar-vertical-align($input-height-base);
+
+  &.btn-sm {
+    @include navbar-vertical-align($input-height-small);
+  }
+  &.btn-xs {
+    @include navbar-vertical-align(22);
+  }
+}
+
+
+// Text in navbars
+//
+// Add a class to make any element properly align itself vertically within the navbars.
+
+.navbar-text {
+  @include navbar-vertical-align($line-height-computed);
+
+  @media (min-width: $grid-float-breakpoint) {
+    float: left;
+    margin-left: $navbar-padding-horizontal;
+    margin-right: $navbar-padding-horizontal;
+  }
+}
+
+
+// Component alignment
+//
+// Repurpose the pull utilities as their own navbar utilities to avoid specificity
+// issues with parents and chaining. Only do this when the navbar is uncollapsed
+// though so that navbar contents properly stack and align in mobile.
+//
+// Declared after the navbar components to ensure more specificity on the margins.
+
+@media (min-width: $grid-float-breakpoint) {
+  .navbar-left {
+    float: left !important;
+  }
+  .navbar-right {
+    float: right !important;
+  margin-right: -$navbar-padding-horizontal;
+
+    ~ .navbar-right {
+      margin-right: 0;
+    }
+  }
+}
+
+
+// Alternate navbars
+// --------------------------------------------------
+
+// Default navbar
+.navbar-default {
+  background-color: $navbar-default-bg;
+  border-color: $navbar-default-border;
+
+  .navbar-brand {
+    color: $navbar-default-brand-color;
+    &:hover,
+    &:focus {
+      color: $navbar-default-brand-hover-color;
+      background-color: $navbar-default-brand-hover-bg;
+    }
+  }
+
+  .navbar-text {
+    color: $navbar-default-color;
+  }
+
+  .navbar-nav {
+    > li > a {
+      color: $navbar-default-link-color;
+
+      &:hover,
+      &:focus {
+        color: $navbar-default-link-hover-color;
+        background-color: $navbar-default-link-hover-bg;
+      }
+    }
+    > .active > a {
+      &,
+      &:hover,
+      &:focus {
+        color: $navbar-default-link-active-color;
+        background-color: $navbar-default-link-active-bg;
+      }
+    }
+    > .disabled > a {
+      &,
+      &:hover,
+      &:focus {
+        color: $navbar-default-link-disabled-color;
+        background-color: $navbar-default-link-disabled-bg;
+      }
+    }
+  }
+
+  .navbar-toggle {
+    border-color: $navbar-default-toggle-border-color;
+    &:hover,
+    &:focus {
+      background-color: $navbar-default-toggle-hover-bg;
+    }
+    .icon-bar {
+      background-color: $navbar-default-toggle-icon-bar-bg;
+    }
+  }
+
+  .navbar-collapse,
+  .navbar-form {
+    border-color: $navbar-default-border;
+  }
+
+  // Dropdown menu items
+  .navbar-nav {
+    // Remove background color from open dropdown
+    > .open > a {
+      &,
+      &:hover,
+      &:focus {
+        background-color: $navbar-default-link-active-bg;
+        color: $navbar-default-link-active-color;
+      }
+    }
+
+    @media (max-width: $grid-float-breakpoint-max) {
+      // Dropdowns get custom display when collapsed
+      .open .dropdown-menu {
+        > li > a {
+          color: $navbar-default-link-color;
+          &:hover,
+          &:focus {
+            color: $navbar-default-link-hover-color;
+            background-color: $navbar-default-link-hover-bg;
+          }
+        }
+        > .active > a {
+          &,
+          &:hover,
+          &:focus {
+            color: $navbar-default-link-active-color;
+            background-color: $navbar-default-link-active-bg;
+          }
+        }
+        > .disabled > a {
+          &,
+          &:hover,
+          &:focus {
+            color: $navbar-default-link-disabled-color;
+            background-color: $navbar-default-link-disabled-bg;
+          }
+        }
+      }
+    }
+  }
+
+
+  // Links in navbars
+  //
+  // Add a class to ensure links outside the navbar nav are colored correctly.
+
+  .navbar-link {
+    color: $navbar-default-link-color;
+    &:hover {
+      color: $navbar-default-link-hover-color;
+    }
+  }
+
+  .btn-link {
+    color: $navbar-default-link-color;
+    &:hover,
+    &:focus {
+      color: $navbar-default-link-hover-color;
+    }
+    &[disabled],
+    fieldset[disabled] & {
+      &:hover,
+      &:focus {
+        color: $navbar-default-link-disabled-color;
+      }
+    }
+  }
+}
+
+// Inverse navbar
+
+.navbar-inverse {
+  background-color: $navbar-inverse-bg;
+  border-color: $navbar-inverse-border;
+
+  .navbar-brand {
+    color: $navbar-inverse-brand-color;
+    &:hover,
+    &:focus {
+      color: $navbar-inverse-brand-hover-color;
+      background-color: $navbar-inverse-brand-hover-bg;
+    }
+  }
+
+  .navbar-text {
+    color: $navbar-inverse-color;
+  }
+
+  .navbar-nav {
+    > li > a {
+      color: $navbar-inverse-link-color;
+
+      &:hover,
+      &:focus {
+        color: $navbar-inverse-link-hover-color;
+        background-color: $navbar-inverse-link-hover-bg;
+      }
+    }
+    > .active > a {
+      &,
+      &:hover,
+      &:focus {
+        color: $navbar-inverse-link-active-color;
+        background-color: $navbar-inverse-link-active-bg;
+      }
+    }
+    > .disabled > a {
+      &,
+      &:hover,
+      &:focus {
+        color: $navbar-inverse-link-disabled-color;
+        background-color: $navbar-inverse-link-disabled-bg;
+      }
+    }
+  }
+
+  // Darken the responsive nav toggle
+  .navbar-toggle {
+    border-color: $navbar-inverse-toggle-border-color;
+    &:hover,
+    &:focus {
+      background-color: $navbar-inverse-toggle-hover-bg;
+    }
+    .icon-bar {
+      background-color: $navbar-inverse-toggle-icon-bar-bg;
+    }
+  }
+
+  .navbar-collapse,
+  .navbar-form {
+    border-color: darken($navbar-inverse-bg, 7%);
+  }
+
+  // Dropdowns
+  .navbar-nav {
+    > .open > a {
+      &,
+      &:hover,
+      &:focus {
+        background-color: $navbar-inverse-link-active-bg;
+        color: $navbar-inverse-link-active-color;
+      }
+    }
+
+    @media (max-width: $grid-float-breakpoint-max) {
+      // Dropdowns get custom display
+      .open .dropdown-menu {
+        > .dropdown-header {
+          border-color: $navbar-inverse-border;
+        }
+        .divider {
+          background-color: $navbar-inverse-border;
+        }
+        > li > a {
+          color: $navbar-inverse-link-color;
+          &:hover,
+          &:focus {
+            color: $navbar-inverse-link-hover-color;
+            background-color: $navbar-inverse-link-hover-bg;
+          }
+        }
+        > .active > a {
+          &,
+          &:hover,
+          &:focus {
+            color: $navbar-inverse-link-active-color;
+            background-color: $navbar-inverse-link-active-bg;
+          }
+        }
+        > .disabled > a {
+          &,
+          &:hover,
+          &:focus {
+            color: $navbar-inverse-link-disabled-color;
+            background-color: $navbar-inverse-link-disabled-bg;
+          }
+        }
+      }
+    }
+  }
+
+  .navbar-link {
+    color: $navbar-inverse-link-color;
+    &:hover {
+      color: $navbar-inverse-link-hover-color;
+    }
+  }
+
+  .btn-link {
+    color: $navbar-inverse-link-color;
+    &:hover,
+    &:focus {
+      color: $navbar-inverse-link-hover-color;
+    }
+    &[disabled],
+    fieldset[disabled] & {
+      &:hover,
+      &:focus {
+        color: $navbar-inverse-link-disabled-color;
+      }
+    }
+  }
+}
diff --git a/themes/bootstrap3/scss/vendor/bootstrap/_navs.scss b/themes/bootstrap3/scss/vendor/bootstrap/_navs.scss
new file mode 100644
index 0000000000000000000000000000000000000000..9d369f307917e557b53e3aeb9e800708b3aadb6b
--- /dev/null
+++ b/themes/bootstrap3/scss/vendor/bootstrap/_navs.scss
@@ -0,0 +1,242 @@
+//
+// Navs
+// --------------------------------------------------
+
+
+// Base class
+// --------------------------------------------------
+
+.nav {
+  margin-bottom: 0;
+  padding-left: 0; // Override default ul/ol
+  list-style: none;
+  @include clearfix;
+
+  > li {
+    position: relative;
+    display: block;
+
+    > a {
+      position: relative;
+      display: block;
+      padding: $nav-link-padding;
+      &:hover,
+      &:focus {
+        text-decoration: none;
+        background-color: $nav-link-hover-bg;
+      }
+    }
+
+    // Disabled state sets text to gray and nukes hover/tab effects
+    &.disabled > a {
+      color: $nav-disabled-link-color;
+
+      &:hover,
+      &:focus {
+        color: $nav-disabled-link-hover-color;
+        text-decoration: none;
+        background-color: transparent;
+        cursor: $cursor-disabled;
+      }
+    }
+  }
+
+  // Open dropdowns
+  .open > a {
+    &,
+    &:hover,
+    &:focus {
+      background-color: $nav-link-hover-bg;
+      border-color: $link-color;
+    }
+  }
+
+  // Nav dividers (deprecated with v3.0.1)
+  //
+  // This should have been removed in v3 with the dropping of `.nav-list`, but
+  // we missed it. We don't currently support this anywhere, but in the interest
+  // of maintaining backward compatibility in case you use it, it's deprecated.
+  .nav-divider {
+    @include nav-divider;
+  }
+
+  // Prevent IE8 from misplacing imgs
+  //
+  // See https://github.com/h5bp/html5-boilerplate/issues/984#issuecomment-3985989
+  > li > a > img {
+    max-width: none;
+  }
+}
+
+
+// Tabs
+// -------------------------
+
+// Give the tabs something to sit on
+.nav-tabs {
+  border-bottom: 1px solid $nav-tabs-border-color;
+  > li {
+    float: left;
+    // Make the list-items overlay the bottom border
+    margin-bottom: -1px;
+
+    // Actual tabs (as links)
+    > a {
+      margin-right: 2px;
+      line-height: $line-height-base;
+      border: 1px solid transparent;
+      border-radius: $border-radius-base $border-radius-base 0 0;
+      &:hover {
+        border-color: $nav-tabs-link-hover-border-color $nav-tabs-link-hover-border-color $nav-tabs-border-color;
+      }
+    }
+
+    // Active state, and its :hover to override normal :hover
+    &.active > a {
+      &,
+      &:hover,
+      &:focus {
+        color: $nav-tabs-active-link-hover-color;
+        background-color: $nav-tabs-active-link-hover-bg;
+        border: 1px solid $nav-tabs-active-link-hover-border-color;
+        border-bottom-color: transparent;
+        cursor: default;
+      }
+    }
+  }
+  // pulling this in mainly for less shorthand
+  &.nav-justified {
+    @extend .nav-justified;
+    @extend .nav-tabs-justified;
+  }
+}
+
+
+// Pills
+// -------------------------
+.nav-pills {
+  > li {
+    float: left;
+
+    // Links rendered as pills
+    > a {
+      border-radius: $nav-pills-border-radius;
+    }
+    + li {
+      margin-left: 2px;
+    }
+
+    // Active state
+    &.active > a {
+      &,
+      &:hover,
+      &:focus {
+        color: $nav-pills-active-link-hover-color;
+        background-color: $nav-pills-active-link-hover-bg;
+      }
+    }
+  }
+}
+
+
+// Stacked pills
+.nav-stacked {
+  > li {
+    float: none;
+    + li {
+      margin-top: 2px;
+      margin-left: 0; // no need for this gap between nav items
+    }
+  }
+}
+
+
+// Nav variations
+// --------------------------------------------------
+
+// Justified nav links
+// -------------------------
+
+.nav-justified {
+  width: 100%;
+
+  > li {
+    float: none;
+    > a {
+      text-align: center;
+      margin-bottom: 5px;
+    }
+  }
+
+  > .dropdown .dropdown-menu {
+    top: auto;
+    left: auto;
+  }
+
+  @media (min-width: $screen-sm-min) {
+    > li {
+      display: table-cell;
+      width: 1%;
+      > a {
+        margin-bottom: 0;
+      }
+    }
+  }
+}
+
+// Move borders to anchors instead of bottom of list
+//
+// Mixin for adding on top the shared `.nav-justified` styles for our tabs
+.nav-tabs-justified {
+  border-bottom: 0;
+
+  > li > a {
+    // Override margin from .nav-tabs
+    margin-right: 0;
+    border-radius: $border-radius-base;
+  }
+
+  > .active > a,
+  > .active > a:hover,
+  > .active > a:focus {
+    border: 1px solid $nav-tabs-justified-link-border-color;
+  }
+
+  @media (min-width: $screen-sm-min) {
+    > li > a {
+      border-bottom: 1px solid $nav-tabs-justified-link-border-color;
+      border-radius: $border-radius-base $border-radius-base 0 0;
+    }
+    > .active > a,
+    > .active > a:hover,
+    > .active > a:focus {
+      border-bottom-color: $nav-tabs-justified-active-link-border-color;
+    }
+  }
+}
+
+
+// Tabbable tabs
+// -------------------------
+
+// Hide tabbable panes to start, show them when `.active`
+.tab-content {
+  > .tab-pane {
+    display: none;
+  }
+  > .active {
+    display: block;
+  }
+}
+
+
+// Dropdowns
+// -------------------------
+
+// Specific dropdowns
+.nav-tabs .dropdown-menu {
+  // make dropdown border overlap tab border
+  margin-top: -1px;
+  // Remove the top rounded corners here since there is a hard edge above the menu
+  @include border-top-radius(0);
+}
diff --git a/themes/bootstrap3/scss/vendor/bootstrap/_normalize.scss b/themes/bootstrap3/scss/vendor/bootstrap/_normalize.scss
new file mode 100644
index 0000000000000000000000000000000000000000..9dddf73ad2924561afa704701655f2cbc011d7df
--- /dev/null
+++ b/themes/bootstrap3/scss/vendor/bootstrap/_normalize.scss
@@ -0,0 +1,424 @@
+/*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */
+
+//
+// 1. Set default font family to sans-serif.
+// 2. Prevent iOS and IE text size adjust after device orientation change,
+//    without disabling user zoom.
+//
+
+html {
+  font-family: sans-serif; // 1
+  -ms-text-size-adjust: 100%; // 2
+  -webkit-text-size-adjust: 100%; // 2
+}
+
+//
+// Remove default margin.
+//
+
+body {
+  margin: 0;
+}
+
+// HTML5 display definitions
+// ==========================================================================
+
+//
+// Correct `block` display not defined for any HTML5 element in IE 8/9.
+// Correct `block` display not defined for `details` or `summary` in IE 10/11
+// and Firefox.
+// Correct `block` display not defined for `main` in IE 11.
+//
+
+article,
+aside,
+details,
+figcaption,
+figure,
+footer,
+header,
+hgroup,
+main,
+menu,
+nav,
+section,
+summary {
+  display: block;
+}
+
+//
+// 1. Correct `inline-block` display not defined in IE 8/9.
+// 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera.
+//
+
+audio,
+canvas,
+progress,
+video {
+  display: inline-block; // 1
+  vertical-align: baseline; // 2
+}
+
+//
+// Prevent modern browsers from displaying `audio` without controls.
+// Remove excess height in iOS 5 devices.
+//
+
+audio:not([controls]) {
+  display: none;
+  height: 0;
+}
+
+//
+// Address `[hidden]` styling not present in IE 8/9/10.
+// Hide the `template` element in IE 8/9/10/11, Safari, and Firefox < 22.
+//
+
+[hidden],
+template {
+  display: none;
+}
+
+// Links
+// ==========================================================================
+
+//
+// Remove the gray background color from active links in IE 10.
+//
+
+a {
+  background-color: transparent;
+}
+
+//
+// Improve readability of focused elements when they are also in an
+// active/hover state.
+//
+
+a:active,
+a:hover {
+  outline: 0;
+}
+
+// Text-level semantics
+// ==========================================================================
+
+//
+// Address styling not present in IE 8/9/10/11, Safari, and Chrome.
+//
+
+abbr[title] {
+  border-bottom: 1px dotted;
+}
+
+//
+// Address style set to `bolder` in Firefox 4+, Safari, and Chrome.
+//
+
+b,
+strong {
+  font-weight: bold;
+}
+
+//
+// Address styling not present in Safari and Chrome.
+//
+
+dfn {
+  font-style: italic;
+}
+
+//
+// Address variable `h1` font-size and margin within `section` and `article`
+// contexts in Firefox 4+, Safari, and Chrome.
+//
+
+h1 {
+  font-size: 2em;
+  margin: 0.67em 0;
+}
+
+//
+// Address styling not present in IE 8/9.
+//
+
+mark {
+  background: #ff0;
+  color: #000;
+}
+
+//
+// Address inconsistent and variable font size in all browsers.
+//
+
+small {
+  font-size: 80%;
+}
+
+//
+// Prevent `sub` and `sup` affecting `line-height` in all browsers.
+//
+
+sub,
+sup {
+  font-size: 75%;
+  line-height: 0;
+  position: relative;
+  vertical-align: baseline;
+}
+
+sup {
+  top: -0.5em;
+}
+
+sub {
+  bottom: -0.25em;
+}
+
+// Embedded content
+// ==========================================================================
+
+//
+// Remove border when inside `a` element in IE 8/9/10.
+//
+
+img {
+  border: 0;
+}
+
+//
+// Correct overflow not hidden in IE 9/10/11.
+//
+
+svg:not(:root) {
+  overflow: hidden;
+}
+
+// Grouping content
+// ==========================================================================
+
+//
+// Address margin not present in IE 8/9 and Safari.
+//
+
+figure {
+  margin: 1em 40px;
+}
+
+//
+// Address differences between Firefox and other browsers.
+//
+
+hr {
+  box-sizing: content-box;
+  height: 0;
+}
+
+//
+// Contain overflow in all browsers.
+//
+
+pre {
+  overflow: auto;
+}
+
+//
+// Address odd `em`-unit font size rendering in all browsers.
+//
+
+code,
+kbd,
+pre,
+samp {
+  font-family: monospace, monospace;
+  font-size: 1em;
+}
+
+// Forms
+// ==========================================================================
+
+//
+// Known limitation: by default, Chrome and Safari on OS X allow very limited
+// styling of `select`, unless a `border` property is set.
+//
+
+//
+// 1. Correct color not being inherited.
+//    Known issue: affects color of disabled elements.
+// 2. Correct font properties not being inherited.
+// 3. Address margins set differently in Firefox 4+, Safari, and Chrome.
+//
+
+button,
+input,
+optgroup,
+select,
+textarea {
+  color: inherit; // 1
+  font: inherit; // 2
+  margin: 0; // 3
+}
+
+//
+// Address `overflow` set to `hidden` in IE 8/9/10/11.
+//
+
+button {
+  overflow: visible;
+}
+
+//
+// Address inconsistent `text-transform` inheritance for `button` and `select`.
+// All other form control elements do not inherit `text-transform` values.
+// Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera.
+// Correct `select` style inheritance in Firefox.
+//
+
+button,
+select {
+  text-transform: none;
+}
+
+//
+// 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio`
+//    and `video` controls.
+// 2. Correct inability to style clickable `input` types in iOS.
+// 3. Improve usability and consistency of cursor style between image-type
+//    `input` and others.
+//
+
+button,
+html input[type="button"], // 1
+input[type="reset"],
+input[type="submit"] {
+  -webkit-appearance: button; // 2
+  cursor: pointer; // 3
+}
+
+//
+// Re-set default cursor for disabled elements.
+//
+
+button[disabled],
+html input[disabled] {
+  cursor: default;
+}
+
+//
+// Remove inner padding and border in Firefox 4+.
+//
+
+button::-moz-focus-inner,
+input::-moz-focus-inner {
+  border: 0;
+  padding: 0;
+}
+
+//
+// Address Firefox 4+ setting `line-height` on `input` using `!important` in
+// the UA stylesheet.
+//
+
+input {
+  line-height: normal;
+}
+
+//
+// It's recommended that you don't attempt to style these elements.
+// Firefox's implementation doesn't respect box-sizing, padding, or width.
+//
+// 1. Address box sizing set to `content-box` in IE 8/9/10.
+// 2. Remove excess padding in IE 8/9/10.
+//
+
+input[type="checkbox"],
+input[type="radio"] {
+  box-sizing: border-box; // 1
+  padding: 0; // 2
+}
+
+//
+// Fix the cursor style for Chrome's increment/decrement buttons. For certain
+// `font-size` values of the `input`, it causes the cursor style of the
+// decrement button to change from `default` to `text`.
+//
+
+input[type="number"]::-webkit-inner-spin-button,
+input[type="number"]::-webkit-outer-spin-button {
+  height: auto;
+}
+
+//
+// 1. Address `appearance` set to `searchfield` in Safari and Chrome.
+// 2. Address `box-sizing` set to `border-box` in Safari and Chrome.
+//
+
+input[type="search"] {
+  -webkit-appearance: textfield; // 1
+  box-sizing: content-box; //2
+}
+
+//
+// Remove inner padding and search cancel button in Safari and Chrome on OS X.
+// Safari (but not Chrome) clips the cancel button when the search input has
+// padding (and `textfield` appearance).
+//
+
+input[type="search"]::-webkit-search-cancel-button,
+input[type="search"]::-webkit-search-decoration {
+  -webkit-appearance: none;
+}
+
+//
+// Define consistent border, margin, and padding.
+//
+
+fieldset {
+  border: 1px solid #c0c0c0;
+  margin: 0 2px;
+  padding: 0.35em 0.625em 0.75em;
+}
+
+//
+// 1. Correct `color` not being inherited in IE 8/9/10/11.
+// 2. Remove padding so people aren't caught out if they zero out fieldsets.
+//
+
+legend {
+  border: 0; // 1
+  padding: 0; // 2
+}
+
+//
+// Remove default vertical scrollbar in IE 8/9/10/11.
+//
+
+textarea {
+  overflow: auto;
+}
+
+//
+// Don't inherit the `font-weight` (applied by a rule above).
+// NOTE: the default cannot safely be changed in Chrome and Safari on OS X.
+//
+
+optgroup {
+  font-weight: bold;
+}
+
+// Tables
+// ==========================================================================
+
+//
+// Remove most spacing between table cells.
+//
+
+table {
+  border-collapse: collapse;
+  border-spacing: 0;
+}
+
+td,
+th {
+  padding: 0;
+}
diff --git a/themes/bootstrap3/scss/vendor/bootstrap/_pager.scss b/themes/bootstrap3/scss/vendor/bootstrap/_pager.scss
new file mode 100644
index 0000000000000000000000000000000000000000..c2342174ff3713003e0417b18271ef3e609475c7
--- /dev/null
+++ b/themes/bootstrap3/scss/vendor/bootstrap/_pager.scss
@@ -0,0 +1,54 @@
+//
+// Pager pagination
+// --------------------------------------------------
+
+
+.pager {
+  padding-left: 0;
+  margin: $line-height-computed 0;
+  list-style: none;
+  text-align: center;
+  @include clearfix;
+  li {
+    display: inline;
+    > a,
+    > span {
+      display: inline-block;
+      padding: 5px 14px;
+      background-color: $pager-bg;
+      border: 1px solid $pager-border;
+      border-radius: $pager-border-radius;
+    }
+
+    > a:hover,
+    > a:focus {
+      text-decoration: none;
+      background-color: $pager-hover-bg;
+    }
+  }
+
+  .next {
+    > a,
+    > span {
+      float: right;
+    }
+  }
+
+  .previous {
+    > a,
+    > span {
+      float: left;
+    }
+  }
+
+  .disabled {
+    > a,
+    > a:hover,
+    > a:focus,
+    > span {
+      color: $pager-disabled-color;
+      background-color: $pager-bg;
+      cursor: $cursor-disabled;
+    }
+  }
+}
diff --git a/themes/bootstrap3/scss/vendor/bootstrap/_pagination.scss b/themes/bootstrap3/scss/vendor/bootstrap/_pagination.scss
new file mode 100644
index 0000000000000000000000000000000000000000..fecfa9c6421106f05dd37023620a477012f2e9e9
--- /dev/null
+++ b/themes/bootstrap3/scss/vendor/bootstrap/_pagination.scss
@@ -0,0 +1,89 @@
+//
+// Pagination (multiple pages)
+// --------------------------------------------------
+.pagination {
+  display: inline-block;
+  padding-left: 0;
+  margin: $line-height-computed 0;
+  border-radius: $border-radius-base;
+
+  > li {
+    display: inline; // Remove list-style and block-level defaults
+    > a,
+    > span {
+      position: relative;
+      float: left; // Collapse white-space
+      padding: $padding-base-vertical $padding-base-horizontal;
+      line-height: $line-height-base;
+      text-decoration: none;
+      color: $pagination-color;
+      background-color: $pagination-bg;
+      border: 1px solid $pagination-border;
+      margin-left: -1px;
+    }
+    &:first-child {
+      > a,
+      > span {
+        margin-left: 0;
+        @include border-left-radius($border-radius-base);
+      }
+    }
+    &:last-child {
+      > a,
+      > span {
+        @include border-right-radius($border-radius-base);
+      }
+    }
+  }
+
+  > li > a,
+  > li > span {
+    &:hover,
+    &:focus {
+      z-index: 2;
+      color: $pagination-hover-color;
+      background-color: $pagination-hover-bg;
+      border-color: $pagination-hover-border;
+    }
+  }
+
+  > .active > a,
+  > .active > span {
+    &,
+    &:hover,
+    &:focus {
+      z-index: 3;
+      color: $pagination-active-color;
+      background-color: $pagination-active-bg;
+      border-color: $pagination-active-border;
+      cursor: default;
+    }
+  }
+
+  > .disabled {
+    > span,
+    > span:hover,
+    > span:focus,
+    > a,
+    > a:hover,
+    > a:focus {
+      color: $pagination-disabled-color;
+      background-color: $pagination-disabled-bg;
+      border-color: $pagination-disabled-border;
+      cursor: $cursor-disabled;
+    }
+  }
+}
+
+// Sizing
+// --------------------------------------------------
+
+// Large
+.pagination-lg {
+  @include pagination-size($padding-large-vertical, $padding-large-horizontal, $font-size-large, $line-height-large, $border-radius-large);
+}
+
+// Small
+.pagination-sm {
+  @include pagination-size($padding-small-vertical, $padding-small-horizontal, $font-size-small, $line-height-small, $border-radius-small);
+}
diff --git a/themes/bootstrap3/scss/vendor/bootstrap/_panels.scss b/themes/bootstrap3/scss/vendor/bootstrap/_panels.scss
new file mode 100644
index 0000000000000000000000000000000000000000..6c568def47b5a219a8a661b52cb7fd72b8c61b32
--- /dev/null
+++ b/themes/bootstrap3/scss/vendor/bootstrap/_panels.scss
@@ -0,0 +1,271 @@
+//
+// Panels
+// --------------------------------------------------
+
+
+// Base class
+.panel {
+  margin-bottom: $line-height-computed;
+  background-color: $panel-bg;
+  border: 1px solid transparent;
+  border-radius: $panel-border-radius;
+  @include box-shadow(0 1px 1px rgba(0,0,0,.05));
+}
+
+// Panel contents
+.panel-body {
+  padding: $panel-body-padding;
+  @include clearfix;
+}
+
+// Optional heading
+.panel-heading {
+  padding: $panel-heading-padding;
+  border-bottom: 1px solid transparent;
+  @include border-top-radius(($panel-border-radius - 1));
+
+  > .dropdown .dropdown-toggle {
+    color: inherit;
+  }
+}
+
+// Within heading, strip any `h*` tag of its default margins for spacing.
+.panel-title {
+  margin-top: 0;
+  margin-bottom: 0;
+  font-size: ceil(($font-size-base * 1.125));
+  color: inherit;
+
+  > a,
+  > small,
+  > .small,
+  > small > a,
+  > .small > a {
+    color: inherit;
+  }
+}
+
+// Optional footer (stays gray in every modifier class)
+.panel-footer {
+  padding: $panel-footer-padding;
+  background-color: $panel-footer-bg;
+  border-top: 1px solid $panel-inner-border;
+  @include border-bottom-radius(($panel-border-radius - 1));
+}
+
+
+// List groups in panels
+//
+// By default, space out list group content from panel headings to account for
+// any kind of custom content between the two.
+
+.panel {
+  > .list-group,
+  > .panel-collapse > .list-group {
+    margin-bottom: 0;
+
+    .list-group-item {
+      border-width: 1px 0;
+      border-radius: 0;
+    }
+
+    // Add border top radius for first one
+    &:first-child {
+      .list-group-item:first-child {
+        border-top: 0;
+        @include border-top-radius(($panel-border-radius - 1));
+      }
+    }
+
+    // Add border bottom radius for last one
+    &:last-child {
+      .list-group-item:last-child {
+        border-bottom: 0;
+        @include border-bottom-radius(($panel-border-radius - 1));
+      }
+    }
+  }
+  > .panel-heading + .panel-collapse > .list-group {
+    .list-group-item:first-child {
+      @include border-top-radius(0);
+    }
+  }
+}
+// Collapse space between when there's no additional content.
+.panel-heading + .list-group {
+  .list-group-item:first-child {
+    border-top-width: 0;
+  }
+}
+.list-group + .panel-footer {
+  border-top-width: 0;
+}
+
+// Tables in panels
+//
+// Place a non-bordered `.table` within a panel (not within a `.panel-body`) and
+// watch it go full width.
+
+.panel {
+  > .table,
+  > .table-responsive > .table,
+  > .panel-collapse > .table {
+    margin-bottom: 0;
+
+    caption {
+      padding-left: $panel-body-padding;
+      padding-right: $panel-body-padding;
+    }
+  }
+  // Add border top radius for first one
+  > .table:first-child,
+  > .table-responsive:first-child > .table:first-child {
+    @include border-top-radius(($panel-border-radius - 1));
+
+    > thead:first-child,
+    > tbody:first-child {
+      > tr:first-child {
+        border-top-left-radius: ($panel-border-radius - 1);
+        border-top-right-radius: ($panel-border-radius - 1);
+
+        td:first-child,
+        th:first-child {
+          border-top-left-radius: ($panel-border-radius - 1);
+        }
+        td:last-child,
+        th:last-child {
+          border-top-right-radius: ($panel-border-radius - 1);
+        }
+      }
+    }
+  }
+  // Add border bottom radius for last one
+  > .table:last-child,
+  > .table-responsive:last-child > .table:last-child {
+    @include border-bottom-radius(($panel-border-radius - 1));
+
+    > tbody:last-child,
+    > tfoot:last-child {
+      > tr:last-child {
+        border-bottom-left-radius: ($panel-border-radius - 1);
+        border-bottom-right-radius: ($panel-border-radius - 1);
+
+        td:first-child,
+        th:first-child {
+          border-bottom-left-radius: ($panel-border-radius - 1);
+        }
+        td:last-child,
+        th:last-child {
+          border-bottom-right-radius: ($panel-border-radius - 1);
+        }
+      }
+    }
+  }
+  > .panel-body + .table,
+  > .panel-body + .table-responsive,
+  > .table + .panel-body,
+  > .table-responsive + .panel-body {
+    border-top: 1px solid $table-border-color;
+  }
+  > .table > tbody:first-child > tr:first-child th,
+  > .table > tbody:first-child > tr:first-child td {
+    border-top: 0;
+  }
+  > .table-bordered,
+  > .table-responsive > .table-bordered {
+    border: 0;
+    > thead,
+    > tbody,
+    > tfoot {
+      > tr {
+        > th:first-child,
+        > td:first-child {
+          border-left: 0;
+        }
+        > th:last-child,
+        > td:last-child {
+          border-right: 0;
+        }
+      }
+    }
+    > thead,
+    > tbody {
+      > tr:first-child {
+        > td,
+        > th {
+          border-bottom: 0;
+        }
+      }
+    }
+    > tbody,
+    > tfoot {
+      > tr:last-child {
+        > td,
+        > th {
+          border-bottom: 0;
+        }
+      }
+    }
+  }
+  > .table-responsive {
+    border: 0;
+    margin-bottom: 0;
+  }
+}
+
+
+// Collapsible panels (aka, accordion)
+//
+// Wrap a series of panels in `.panel-group` to turn them into an accordion with
+// the help of our collapse JavaScript plugin.
+
+.panel-group {
+  margin-bottom: $line-height-computed;
+
+  // Tighten up margin so it's only between panels
+  .panel {
+    margin-bottom: 0;
+    border-radius: $panel-border-radius;
+
+    + .panel {
+      margin-top: 5px;
+    }
+  }
+
+  .panel-heading {
+    border-bottom: 0;
+
+    + .panel-collapse > .panel-body,
+    + .panel-collapse > .list-group {
+      border-top: 1px solid $panel-inner-border;
+    }
+  }
+
+  .panel-footer {
+    border-top: 0;
+    + .panel-collapse .panel-body {
+      border-bottom: 1px solid $panel-inner-border;
+    }
+  }
+}
+
+
+// Contextual variations
+.panel-default {
+  @include panel-variant($panel-default-border, $panel-default-text, $panel-default-heading-bg, $panel-default-border);
+}
+.panel-primary {
+  @include panel-variant($panel-primary-border, $panel-primary-text, $panel-primary-heading-bg, $panel-primary-border);
+}
+.panel-success {
+  @include panel-variant($panel-success-border, $panel-success-text, $panel-success-heading-bg, $panel-success-border);
+}
+.panel-info {
+  @include panel-variant($panel-info-border, $panel-info-text, $panel-info-heading-bg, $panel-info-border);
+}
+.panel-warning {
+  @include panel-variant($panel-warning-border, $panel-warning-text, $panel-warning-heading-bg, $panel-warning-border);
+}
+.panel-danger {
+  @include panel-variant($panel-danger-border, $panel-danger-text, $panel-danger-heading-bg, $panel-danger-border);
+}
diff --git a/themes/bootstrap3/scss/vendor/bootstrap/_popovers.scss b/themes/bootstrap3/scss/vendor/bootstrap/_popovers.scss
new file mode 100644
index 0000000000000000000000000000000000000000..9b90a2e964e39dee9ad208b544a2a9b538c92b08
--- /dev/null
+++ b/themes/bootstrap3/scss/vendor/bootstrap/_popovers.scss
@@ -0,0 +1,131 @@
+//
+// Popovers
+// --------------------------------------------------
+
+
+.popover {
+  position: absolute;
+  top: 0;
+  left: 0;
+  z-index: $zindex-popover;
+  display: none;
+  max-width: $popover-max-width;
+  padding: 1px;
+  // Our parent element can be arbitrary since popovers are by default inserted as a sibling of their target element.
+  // So reset our font and text properties to avoid inheriting weird values.
+  @include reset-text;
+  font-size: $font-size-base;
+
+  background-color: $popover-bg;
+  background-clip: padding-box;
+  border: 1px solid $popover-fallback-border-color;
+  border: 1px solid $popover-border-color;
+  border-radius: $border-radius-large;
+  @include box-shadow(0 5px 10px rgba(0,0,0,.2));
+
+  // Offset the popover to account for the popover arrow
+  &.top     { margin-top: -$popover-arrow-width; }
+  &.right   { margin-left: $popover-arrow-width; }
+  &.bottom  { margin-top: $popover-arrow-width; }
+  &.left    { margin-left: -$popover-arrow-width; }
+}
+
+.popover-title {
+  margin: 0; // reset heading margin
+  padding: 8px 14px;
+  font-size: $font-size-base;
+  background-color: $popover-title-bg;
+  border-bottom: 1px solid darken($popover-title-bg, 5%);
+  border-radius: ($border-radius-large - 1) ($border-radius-large - 1) 0 0;
+}
+
+.popover-content {
+  padding: 9px 14px;
+}
+
+// Arrows
+//
+// .arrow is outer, .arrow:after is inner
+
+.popover > .arrow {
+  &,
+  &:after {
+    position: absolute;
+    display: block;
+    width: 0;
+    height: 0;
+    border-color: transparent;
+    border-style: solid;
+  }
+}
+.popover > .arrow {
+  border-width: $popover-arrow-outer-width;
+}
+.popover > .arrow:after {
+  border-width: $popover-arrow-width;
+  content: "";
+}
+
+.popover {
+  &.top > .arrow {
+    left: 50%;
+    margin-left: -$popover-arrow-outer-width;
+    border-bottom-width: 0;
+    border-top-color: $popover-arrow-outer-fallback-color; // IE8 fallback
+    border-top-color: $popover-arrow-outer-color;
+    bottom: -$popover-arrow-outer-width;
+    &:after {
+      content: " ";
+      bottom: 1px;
+      margin-left: -$popover-arrow-width;
+      border-bottom-width: 0;
+      border-top-color: $popover-arrow-color;
+    }
+  }
+  &.right > .arrow {
+    top: 50%;
+    left: -$popover-arrow-outer-width;
+    margin-top: -$popover-arrow-outer-width;
+    border-left-width: 0;
+    border-right-color: $popover-arrow-outer-fallback-color; // IE8 fallback
+    border-right-color: $popover-arrow-outer-color;
+    &:after {
+      content: " ";
+      left: 1px;
+      bottom: -$popover-arrow-width;
+      border-left-width: 0;
+      border-right-color: $popover-arrow-color;
+    }
+  }
+  &.bottom > .arrow {
+    left: 50%;
+    margin-left: -$popover-arrow-outer-width;
+    border-top-width: 0;
+    border-bottom-color: $popover-arrow-outer-fallback-color; // IE8 fallback
+    border-bottom-color: $popover-arrow-outer-color;
+    top: -$popover-arrow-outer-width;
+    &:after {
+      content: " ";
+      top: 1px;
+      margin-left: -$popover-arrow-width;
+      border-top-width: 0;
+      border-bottom-color: $popover-arrow-color;
+    }
+  }
+
+  &.left > .arrow {
+    top: 50%;
+    right: -$popover-arrow-outer-width;
+    margin-top: -$popover-arrow-outer-width;
+    border-right-width: 0;
+    border-left-color: $popover-arrow-outer-fallback-color; // IE8 fallback
+    border-left-color: $popover-arrow-outer-color;
+    &:after {
+      content: " ";
+      right: 1px;
+      border-right-width: 0;
+      border-left-color: $popover-arrow-color;
+      bottom: -$popover-arrow-width;
+    }
+  }
+}
diff --git a/themes/bootstrap3/scss/vendor/bootstrap/_print.scss b/themes/bootstrap3/scss/vendor/bootstrap/_print.scss
new file mode 100644
index 0000000000000000000000000000000000000000..66e54ab489ea278cab3ac847d59449b9bcea9020
--- /dev/null
+++ b/themes/bootstrap3/scss/vendor/bootstrap/_print.scss
@@ -0,0 +1,101 @@
+/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */
+
+// ==========================================================================
+// Print styles.
+// Inlined to avoid the additional HTTP request: h5bp.com/r
+// ==========================================================================
+
+@media print {
+    *,
+    *:before,
+    *:after {
+        background: transparent !important;
+        color: #000 !important; // Black prints faster: h5bp.com/s
+        box-shadow: none !important;
+        text-shadow: none !important;
+    }
+
+    a,
+    a:visited {
+        text-decoration: underline;
+    }
+
+    a[href]:after {
+        content: " (" attr(href) ")";
+    }
+
+    abbr[title]:after {
+        content: " (" attr(title) ")";
+    }
+
+    // Don't show links that are fragment identifiers,
+    // or use the `javascript:` pseudo protocol
+    a[href^="#"]:after,
+    a[href^="javascript:"]:after {
+        content: "";
+    }
+
+    pre,
+    blockquote {
+        border: 1px solid #999;
+        page-break-inside: avoid;
+    }
+
+    thead {
+        display: table-header-group; // h5bp.com/t
+    }
+
+    tr,
+    img {
+        page-break-inside: avoid;
+    }
+
+    img {
+        max-width: 100% !important;
+    }
+
+    p,
+    h2,
+    h3 {
+        orphans: 3;
+        widows: 3;
+    }
+
+    h2,
+    h3 {
+        page-break-after: avoid;
+    }
+
+    // Bootstrap specific changes start
+
+    // Bootstrap components
+    .navbar {
+        display: none;
+    }
+    .btn,
+    .dropup > .btn {
+        > .caret {
+            border-top-color: #000 !important;
+        }
+    }
+    .label {
+        border: 1px solid #000;
+    }
+
+    .table {
+        border-collapse: collapse !important;
+
+        td,
+        th {
+            background-color: #fff !important;
+        }
+    }
+    .table-bordered {
+        th,
+        td {
+            border: 1px solid #ddd !important;
+        }
+    }
+
+    // Bootstrap specific changes end
+}
diff --git a/themes/bootstrap3/scss/vendor/bootstrap/_progress-bars.scss b/themes/bootstrap3/scss/vendor/bootstrap/_progress-bars.scss
new file mode 100644
index 0000000000000000000000000000000000000000..343df6323c59464cf8088d9c136ace5c29637758
--- /dev/null
+++ b/themes/bootstrap3/scss/vendor/bootstrap/_progress-bars.scss
@@ -0,0 +1,87 @@
+//
+// Progress bars
+// --------------------------------------------------
+
+
+// Bar animations
+// -------------------------
+
+// WebKit
+@-webkit-keyframes progress-bar-stripes {
+  from  { background-position: 40px 0; }
+  to    { background-position: 0 0; }
+}
+
+// Spec and IE10+
+@keyframes progress-bar-stripes {
+  from  { background-position: 40px 0; }
+  to    { background-position: 0 0; }
+}
+
+
+// Bar itself
+// -------------------------
+
+// Outer container
+.progress {
+  overflow: hidden;
+  height: $line-height-computed;
+  margin-bottom: $line-height-computed;
+  background-color: $progress-bg;
+  border-radius: $progress-border-radius;
+  @include box-shadow(inset 0 1px 2px rgba(0,0,0,.1));
+}
+
+// Bar of progress
+.progress-bar {
+  float: left;
+  width: 0%;
+  height: 100%;
+  font-size: $font-size-small;
+  line-height: $line-height-computed;
+  color: $progress-bar-color;
+  text-align: center;
+  background-color: $progress-bar-bg;
+  @include box-shadow(inset 0 -1px 0 rgba(0,0,0,.15));
+  @include transition(width .6s ease);
+}
+
+// Striped bars
+//
+// `.progress-striped .progress-bar` is deprecated as of v3.2.0 in favor of the
+// `.progress-bar-striped` class, which you just add to an existing
+// `.progress-bar`.
+.progress-striped .progress-bar,
+.progress-bar-striped {
+  @include gradient-striped;
+  background-size: 40px 40px;
+}
+
+// Call animation for the active one
+//
+// `.progress.active .progress-bar` is deprecated as of v3.2.0 in favor of the
+// `.progress-bar.active` approach.
+.progress.active .progress-bar,
+.progress-bar.active {
+  @include animation(progress-bar-stripes 2s linear infinite);
+}
+
+
+// Variations
+// -------------------------
+
+.progress-bar-success {
+  @include progress-bar-variant($progress-bar-success-bg);
+}
+
+.progress-bar-info {
+  @include progress-bar-variant($progress-bar-info-bg);
+}
+
+.progress-bar-warning {
+  @include progress-bar-variant($progress-bar-warning-bg);
+}
+
+.progress-bar-danger {
+  @include progress-bar-variant($progress-bar-danger-bg);
+}
diff --git a/themes/bootstrap3/scss/vendor/bootstrap/_responsive-embed.scss b/themes/bootstrap3/scss/vendor/bootstrap/_responsive-embed.scss
new file mode 100644
index 0000000000000000000000000000000000000000..080a5118fe9ab2af331e6b1444fff4be840ecc03
--- /dev/null
+++ b/themes/bootstrap3/scss/vendor/bootstrap/_responsive-embed.scss
@@ -0,0 +1,35 @@
+// Embeds responsive
+//
+// Credit: Nicolas Gallagher and SUIT CSS.
+
+.embed-responsive {
+  position: relative;
+  display: block;
+  height: 0;
+  padding: 0;
+  overflow: hidden;
+
+  .embed-responsive-item,
+  iframe,
+  embed,
+  object,
+  video {
+    position: absolute;
+    top: 0;
+    left: 0;
+    bottom: 0;
+    height: 100%;
+    width: 100%;
+    border: 0;
+  }
+}
+
+// Modifier class for 16:9 aspect ratio
+.embed-responsive-16by9 {
+  padding-bottom: 56.25%;
+}
+
+// Modifier class for 4:3 aspect ratio
+.embed-responsive-4by3 {
+  padding-bottom: 75%;
+}
diff --git a/themes/bootstrap3/scss/vendor/bootstrap/_responsive-utilities.scss b/themes/bootstrap3/scss/vendor/bootstrap/_responsive-utilities.scss
new file mode 100644
index 0000000000000000000000000000000000000000..f3f0c839beca5d7d5064b49a0bbd48af7a0c8665
--- /dev/null
+++ b/themes/bootstrap3/scss/vendor/bootstrap/_responsive-utilities.scss
@@ -0,0 +1,179 @@
+//
+// Responsive: Utility classes
+// --------------------------------------------------
+
+
+// IE10 in Windows (Phone) 8
+//
+// Support for responsive views via media queries is kind of borked in IE10, for
+// Surface/desktop in split view and for Windows Phone 8. This particular fix
+// must be accompanied by a snippet of JavaScript to sniff the user agent and
+// apply some conditional CSS to *only* the Surface/desktop Windows 8. Look at
+// our Getting Started page for more information on this bug.
+//
+// For more information, see the following:
+//
+// Issue: https://github.com/twbs/bootstrap/issues/10497
+// Docs: http://getbootstrap.com/getting-started/#support-ie10-width
+// Source: http://timkadlec.com/2013/01/windows-phone-8-and-device-width/
+// Source: http://timkadlec.com/2012/10/ie10-snap-mode-and-responsive-design/
+
+@at-root {
+  @-ms-viewport {
+    width: device-width;
+  }
+}
+
+
+// Visibility utilities
+// Note: Deprecated .visible-xs, .visible-sm, .visible-md, and .visible-lg as of v3.2.0
+
+@include responsive-invisibility('.visible-xs');
+@include responsive-invisibility('.visible-sm');
+@include responsive-invisibility('.visible-md');
+@include responsive-invisibility('.visible-lg');
+
+.visible-xs-block,
+.visible-xs-inline,
+.visible-xs-inline-block,
+.visible-sm-block,
+.visible-sm-inline,
+.visible-sm-inline-block,
+.visible-md-block,
+.visible-md-inline,
+.visible-md-inline-block,
+.visible-lg-block,
+.visible-lg-inline,
+.visible-lg-inline-block {
+  display: none !important;
+}
+
+@media (max-width: $screen-xs-max) {
+  @include responsive-visibility('.visible-xs');
+}
+.visible-xs-block {
+  @media (max-width: $screen-xs-max) {
+    display: block !important;
+  }
+}
+.visible-xs-inline {
+  @media (max-width: $screen-xs-max) {
+    display: inline !important;
+  }
+}
+.visible-xs-inline-block {
+  @media (max-width: $screen-xs-max) {
+    display: inline-block !important;
+  }
+}
+
+@media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) {
+  @include responsive-visibility('.visible-sm');
+}
+.visible-sm-block {
+  @media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) {
+    display: block !important;
+  }
+}
+.visible-sm-inline {
+  @media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) {
+    display: inline !important;
+  }
+}
+.visible-sm-inline-block {
+  @media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) {
+    display: inline-block !important;
+  }
+}
+
+@media (min-width: $screen-md-min) and (max-width: $screen-md-max) {
+  @include responsive-visibility('.visible-md');
+}
+.visible-md-block {
+  @media (min-width: $screen-md-min) and (max-width: $screen-md-max) {
+    display: block !important;
+  }
+}
+.visible-md-inline {
+  @media (min-width: $screen-md-min) and (max-width: $screen-md-max) {
+    display: inline !important;
+  }
+}
+.visible-md-inline-block {
+  @media (min-width: $screen-md-min) and (max-width: $screen-md-max) {
+    display: inline-block !important;
+  }
+}
+
+@media (min-width: $screen-lg-min) {
+  @include responsive-visibility('.visible-lg');
+}
+.visible-lg-block {
+  @media (min-width: $screen-lg-min) {
+    display: block !important;
+  }
+}
+.visible-lg-inline {
+  @media (min-width: $screen-lg-min) {
+    display: inline !important;
+  }
+}
+.visible-lg-inline-block {
+  @media (min-width: $screen-lg-min) {
+    display: inline-block !important;
+  }
+}
+
+@media (max-width: $screen-xs-max) {
+  @include responsive-invisibility('.hidden-xs');
+}
+
+@media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) {
+  @include responsive-invisibility('.hidden-sm');
+}
+
+@media (min-width: $screen-md-min) and (max-width: $screen-md-max) {
+  @include responsive-invisibility('.hidden-md');
+}
+
+@media (min-width: $screen-lg-min) {
+  @include responsive-invisibility('.hidden-lg');
+}
+
+
+// Print utilities
+//
+// Media queries are placed on the inside to be mixin-friendly.
+
+// Note: Deprecated .visible-print as of v3.2.0
+
+@include responsive-invisibility('.visible-print');
+
+@media print {
+  @include responsive-visibility('.visible-print');
+}
+.visible-print-block {
+  display: none !important;
+
+  @media print {
+    display: block !important;
+  }
+}
+.visible-print-inline {
+  display: none !important;
+
+  @media print {
+    display: inline !important;
+  }
+}
+.visible-print-inline-block {
+  display: none !important;
+
+  @media print {
+    display: inline-block !important;
+  }
+}
+
+@media print {
+  @include responsive-invisibility('.hidden-print');
+}
diff --git a/themes/bootstrap3/scss/vendor/bootstrap/_scaffolding.scss b/themes/bootstrap3/scss/vendor/bootstrap/_scaffolding.scss
new file mode 100644
index 0000000000000000000000000000000000000000..362c7e2a13345957de5f40ca1891c1b44c22f8fb
--- /dev/null
+++ b/themes/bootstrap3/scss/vendor/bootstrap/_scaffolding.scss
@@ -0,0 +1,161 @@
+//
+// Scaffolding
+// --------------------------------------------------
+
+
+// Reset the box-sizing
+//
+// Heads up! This reset may cause conflicts with some third-party widgets.
+// For recommendations on resolving such conflicts, see
+// http://getbootstrap.com/getting-started/#third-box-sizing
+* {
+  @include box-sizing(border-box);
+}
+*:before,
+*:after {
+  @include box-sizing(border-box);
+}
+
+
+// Body reset
+
+html {
+  font-size: 10px;
+  -webkit-tap-highlight-color: rgba(0,0,0,0);
+}
+
+body {
+  font-family: $font-family-base;
+  font-size: $font-size-base;
+  line-height: $line-height-base;
+  color: $text-color;
+  background-color: $body-bg;
+}
+
+// Reset fonts for relevant elements
+input,
+button,
+select,
+textarea {
+  font-family: inherit;
+  font-size: inherit;
+  line-height: inherit;
+}
+
+
+// Links
+
+a {
+  color: $link-color;
+  text-decoration: none;
+
+  &:hover,
+  &:focus {
+    color: $link-hover-color;
+    text-decoration: $link-hover-decoration;
+  }
+
+  &:focus {
+    @include tab-focus;
+  }
+}
+
+
+// Figures
+//
+// We reset this here because previously Normalize had no `figure` margins. This
+// ensures we don't break anyone's use of the element.
+
+figure {
+  margin: 0;
+}
+
+
+// Images
+
+img {
+  vertical-align: middle;
+}
+
+// Responsive images (ensure images don't scale beyond their parents)
+.img-responsive {
+  @include img-responsive;
+}
+
+// Rounded corners
+.img-rounded {
+  border-radius: $border-radius-large;
+}
+
+// Image thumbnails
+//
+// Heads up! This is mixin-ed into thumbnails.less for `.thumbnail`.
+.img-thumbnail {
+  padding: $thumbnail-padding;
+  line-height: $line-height-base;
+  background-color: $thumbnail-bg;
+  border: 1px solid $thumbnail-border;
+  border-radius: $thumbnail-border-radius;
+  @include transition(all .2s ease-in-out);
+
+  // Keep them at most 100% wide
+  @include img-responsive(inline-block);
+}
+
+// Perfect circle
+.img-circle {
+  border-radius: 50%; // set radius in percents
+}
+
+
+// Horizontal rules
+
+hr {
+  margin-top:    $line-height-computed;
+  margin-bottom: $line-height-computed;
+  border: 0;
+  border-top: 1px solid $hr-border;
+}
+
+
+// Only display content to screen readers
+//
+// See: http://a11yproject.com/posts/how-to-hide-content
+
+.sr-only {
+  position: absolute;
+  width: 1px;
+  height: 1px;
+  margin: -1px;
+  padding: 0;
+  overflow: hidden;
+  clip: rect(0,0,0,0);
+  border: 0;
+}
+
+// Use in conjunction with .sr-only to only display content when it's focused.
+// Useful for "Skip to main content" links; see http://www.w3.org/TR/2013/NOTE-WCAG20-TECHS-20130905/G1
+// Credit: HTML5 Boilerplate
+
+.sr-only-focusable {
+  &:active,
+  &:focus {
+    position: static;
+    width: auto;
+    height: auto;
+    margin: 0;
+    overflow: visible;
+    clip: auto;
+  }
+}
+
+
+// iOS "clickable elements" fix for role="button"
+//
+// Fixes "clickability" issue (and more generally, the firing of events such as focus as well)
+// for traditionally non-focusable elements with role="button"
+// see https://developer.mozilla.org/en-US/docs/Web/Events/click#Safari_Mobile
+
+[role="button"] {
+  cursor: pointer;
+}
diff --git a/themes/bootstrap3/scss/vendor/bootstrap/_tables.scss b/themes/bootstrap3/scss/vendor/bootstrap/_tables.scss
new file mode 100644
index 0000000000000000000000000000000000000000..affcc58c0b0f80388f291c73451da8e38c8f9280
--- /dev/null
+++ b/themes/bootstrap3/scss/vendor/bootstrap/_tables.scss
@@ -0,0 +1,234 @@
+//
+// Tables
+// --------------------------------------------------
+
+
+table {
+  background-color: $table-bg;
+}
+caption {
+  padding-top: $table-cell-padding;
+  padding-bottom: $table-cell-padding;
+  color: $text-muted;
+  text-align: left;
+}
+th {
+  text-align: left;
+}
+
+
+// Baseline styles
+
+.table {
+  width: 100%;
+  max-width: 100%;
+  margin-bottom: $line-height-computed;
+  // Cells
+  > thead,
+  > tbody,
+  > tfoot {
+    > tr {
+      > th,
+      > td {
+        padding: $table-cell-padding;
+        line-height: $line-height-base;
+        vertical-align: top;
+        border-top: 1px solid $table-border-color;
+      }
+    }
+  }
+  // Bottom align for column headings
+  > thead > tr > th {
+    vertical-align: bottom;
+    border-bottom: 2px solid $table-border-color;
+  }
+  // Remove top border from thead by default
+  > caption + thead,
+  > colgroup + thead,
+  > thead:first-child {
+    > tr:first-child {
+      > th,
+      > td {
+        border-top: 0;
+      }
+    }
+  }
+  // Account for multiple tbody instances
+  > tbody + tbody {
+    border-top: 2px solid $table-border-color;
+  }
+
+  // Nesting
+  .table {
+    background-color: $body-bg;
+  }
+}
+
+
+// Condensed table w/ half padding
+
+.table-condensed {
+  > thead,
+  > tbody,
+  > tfoot {
+    > tr {
+      > th,
+      > td {
+        padding: $table-condensed-cell-padding;
+      }
+    }
+  }
+}
+
+
+// Bordered version
+//
+// Add borders all around the table and between all the columns.
+
+.table-bordered {
+  border: 1px solid $table-border-color;
+  > thead,
+  > tbody,
+  > tfoot {
+    > tr {
+      > th,
+      > td {
+        border: 1px solid $table-border-color;
+      }
+    }
+  }
+  > thead > tr {
+    > th,
+    > td {
+      border-bottom-width: 2px;
+    }
+  }
+}
+
+
+// Zebra-striping
+//
+// Default zebra-stripe styles (alternating gray and transparent backgrounds)
+
+.table-striped {
+  > tbody > tr:nth-of-type(odd) {
+    background-color: $table-bg-accent;
+  }
+}
+
+
+// Hover effect
+//
+// Placed here since it has to come after the potential zebra striping
+
+.table-hover {
+  > tbody > tr:hover {
+    background-color: $table-bg-hover;
+  }
+}
+
+
+// Table cell sizing
+//
+// Reset default table behavior
+
+table col[class*="col-"] {
+  position: static; // Prevent border hiding in Firefox and IE9-11 (see https://github.com/twbs/bootstrap/issues/11623)
+  float: none;
+  display: table-column;
+}
+table {
+  td,
+  th {
+    &[class*="col-"] {
+      position: static; // Prevent border hiding in Firefox and IE9-11 (see https://github.com/twbs/bootstrap/issues/11623)
+      float: none;
+      display: table-cell;
+    }
+  }
+}
+
+
+// Table backgrounds
+//
+// Exact selectors below required to override `.table-striped` and prevent
+// inheritance to nested tables.
+
+// Generate the contextual variants
+@include table-row-variant('active', $table-bg-active);
+@include table-row-variant('success', $state-success-bg);
+@include table-row-variant('info', $state-info-bg);
+@include table-row-variant('warning', $state-warning-bg);
+@include table-row-variant('danger', $state-danger-bg);
+
+
+// Responsive tables
+//
+// Wrap your tables in `.table-responsive` and we'll make them mobile friendly
+// by enabling horizontal scrolling. Only applies <768px. Everything above that
+// will display normally.
+
+.table-responsive {
+  overflow-x: auto;
+  min-height: 0.01%; // Workaround for IE9 bug (see https://github.com/twbs/bootstrap/issues/14837)
+
+  @media screen and (max-width: $screen-xs-max) {
+    width: 100%;
+    margin-bottom: ($line-height-computed * 0.75);
+    overflow-y: hidden;
+    -ms-overflow-style: -ms-autohiding-scrollbar;
+    border: 1px solid $table-border-color;
+
+    // Tighten up spacing
+    > .table {
+      margin-bottom: 0;
+
+      // Ensure the content doesn't wrap
+      > thead,
+      > tbody,
+      > tfoot {
+        > tr {
+          > th,
+          > td {
+            white-space: nowrap;
+          }
+        }
+      }
+    }
+
+    // Special overrides for the bordered tables
+    > .table-bordered {
+      border: 0;
+
+      // Nuke the appropriate borders so that the parent can handle them
+      > thead,
+      > tbody,
+      > tfoot {
+        > tr {
+          > th:first-child,
+          > td:first-child {
+            border-left: 0;
+          }
+          > th:last-child,
+          > td:last-child {
+            border-right: 0;
+          }
+        }
+      }
+
+      // Only nuke the last row's bottom-border in `tbody` and `tfoot` since
+      // chances are there will be only one `tr` in a `thead` and that would
+      // remove the border altogether.
+      > tbody,
+      > tfoot {
+        > tr:last-child {
+          > th,
+          > td {
+            border-bottom: 0;
+          }
+        }
+      }
+
+    }
+  }
+}
diff --git a/themes/bootstrap3/scss/vendor/bootstrap/_theme.scss b/themes/bootstrap3/scss/vendor/bootstrap/_theme.scss
new file mode 100644
index 0000000000000000000000000000000000000000..c1b0e9c609fda9a5ab22698e988506078a6d627f
--- /dev/null
+++ b/themes/bootstrap3/scss/vendor/bootstrap/_theme.scss
@@ -0,0 +1,291 @@
+/*!
+ * Bootstrap v3.3.7 (http://getbootstrap.com)
+ * Copyright 2011-2016 Twitter, Inc.
+ * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
+ */
+
+//
+// Load core variables and mixins
+// --------------------------------------------------
+
+@import "variables";
+@import "mixins";
+
+
+//
+// Buttons
+// --------------------------------------------------
+
+// Common styles
+.btn-default,
+.btn-primary,
+.btn-success,
+.btn-info,
+.btn-warning,
+.btn-danger {
+  text-shadow: 0 -1px 0 rgba(0,0,0,.2);
+  $shadow: inset 0 1px 0 rgba(255,255,255,.15), 0 1px 1px rgba(0,0,0,.075);
+  @include box-shadow($shadow);
+
+  // Reset the shadow
+  &:active,
+  &.active {
+    @include box-shadow(inset 0 3px 5px rgba(0,0,0,.125));
+  }
+
+  &.disabled,
+  &[disabled],
+  fieldset[disabled] & {
+    @include box-shadow(none);
+  }
+
+  .badge {
+    text-shadow: none;
+  }
+}
+
+// Mixin for generating new styles
+@mixin btn-styles($btn-color: #555) {
+  @include gradient-vertical($start-color: $btn-color, $end-color: darken($btn-color, 12%));
+  @include reset-filter; // Disable gradients for IE9 because filter bleeds through rounded corners; see https://github.com/twbs/bootstrap/issues/10620
+  background-repeat: repeat-x;
+  border-color: darken($btn-color, 14%);
+
+  &:hover,
+  &:focus  {
+    background-color: darken($btn-color, 12%);
+    background-position: 0 -15px;
+  }
+
+  &:active,
+  &.active {
+    background-color: darken($btn-color, 12%);
+    border-color: darken($btn-color, 14%);
+  }
+
+  &.disabled,
+  &[disabled],
+  fieldset[disabled] & {
+    &,
+    &:hover,
+    &:focus,
+    &.focus,
+    &:active,
+    &.active {
+      background-color: darken($btn-color, 12%);
+      background-image: none;
+    }
+  }
+}
+
+// Common styles
+.btn {
+  // Remove the gradient for the pressed/active state
+  &:active,
+  &.active {
+    background-image: none;
+  }
+}
+
+// Apply the mixin to the buttons
+.btn-default { @include btn-styles($btn-default-bg); text-shadow: 0 1px 0 #fff; border-color: #ccc; }
+.btn-primary { @include btn-styles($btn-primary-bg); }
+.btn-success { @include btn-styles($btn-success-bg); }
+.btn-info    { @include btn-styles($btn-info-bg); }
+.btn-warning { @include btn-styles($btn-warning-bg); }
+.btn-danger  { @include btn-styles($btn-danger-bg); }
+
+
+//
+// Images
+// --------------------------------------------------
+
+.thumbnail,
+.img-thumbnail {
+  @include box-shadow(0 1px 2px rgba(0,0,0,.075));
+}
+
+
+//
+// Dropdowns
+// --------------------------------------------------
+
+.dropdown-menu > li > a:hover,
+.dropdown-menu > li > a:focus {
+  @include gradient-vertical($start-color: $dropdown-link-hover-bg, $end-color: darken($dropdown-link-hover-bg, 5%));
+  background-color: darken($dropdown-link-hover-bg, 5%);
+}
+.dropdown-menu > .active > a,
+.dropdown-menu > .active > a:hover,
+.dropdown-menu > .active > a:focus {
+  @include gradient-vertical($start-color: $dropdown-link-active-bg, $end-color: darken($dropdown-link-active-bg, 5%));
+  background-color: darken($dropdown-link-active-bg, 5%);
+}
+
+
+//
+// Navbar
+// --------------------------------------------------
+
+// Default navbar
+.navbar-default {
+  @include gradient-vertical($start-color: lighten($navbar-default-bg, 10%), $end-color: $navbar-default-bg);
+  @include reset-filter; // Remove gradient in IE<10 to fix bug where dropdowns don't get triggered
+  border-radius: $navbar-border-radius;
+  $shadow: inset 0 1px 0 rgba(255,255,255,.15), 0 1px 5px rgba(0,0,0,.075);
+  @include box-shadow($shadow);
+
+  .navbar-nav > .open > a,
+  .navbar-nav > .active > a {
+    @include gradient-vertical($start-color: darken($navbar-default-link-active-bg, 5%), $end-color: darken($navbar-default-link-active-bg, 2%));
+    @include box-shadow(inset 0 3px 9px rgba(0,0,0,.075));
+  }
+}
+.navbar-brand,
+.navbar-nav > li > a {
+  text-shadow: 0 1px 0 rgba(255,255,255,.25);
+}
+
+// Inverted navbar
+.navbar-inverse {
+  @include gradient-vertical($start-color: lighten($navbar-inverse-bg, 10%), $end-color: $navbar-inverse-bg);
+  @include reset-filter; // Remove gradient in IE<10 to fix bug where dropdowns don't get triggered; see https://github.com/twbs/bootstrap/issues/10257
+  border-radius: $navbar-border-radius;
+  .navbar-nav > .open > a,
+  .navbar-nav > .active > a {
+    @include gradient-vertical($start-color: $navbar-inverse-link-active-bg, $end-color: lighten($navbar-inverse-link-active-bg, 2.5%));
+    @include box-shadow(inset 0 3px 9px rgba(0,0,0,.25));
+  }
+
+  .navbar-brand,
+  .navbar-nav > li > a {
+    text-shadow: 0 -1px 0 rgba(0,0,0,.25);
+  }
+}
+
+// Undo rounded corners in static and fixed navbars
+.navbar-static-top,
+.navbar-fixed-top,
+.navbar-fixed-bottom {
+  border-radius: 0;
+}
+
+// Fix active state of dropdown items in collapsed mode
+@media (max-width: $grid-float-breakpoint-max) {
+  .navbar .navbar-nav .open .dropdown-menu > .active > a {
+    &,
+    &:hover,
+    &:focus {
+      color: #fff;
+      @include gradient-vertical($start-color: $dropdown-link-active-bg, $end-color: darken($dropdown-link-active-bg, 5%));
+    }
+  }
+}
+
+
+//
+// Alerts
+// --------------------------------------------------
+
+// Common styles
+.alert {
+  text-shadow: 0 1px 0 rgba(255,255,255,.2);
+  $shadow: inset 0 1px 0 rgba(255,255,255,.25), 0 1px 2px rgba(0,0,0,.05);
+  @include box-shadow($shadow);
+}
+
+// Mixin for generating new styles
+@mixin alert-styles($color) {
+  @include gradient-vertical($start-color: $color, $end-color: darken($color, 7.5%));
+  border-color: darken($color, 15%);
+}
+
+// Apply the mixin to the alerts
+.alert-success    { @include alert-styles($alert-success-bg); }
+.alert-info       { @include alert-styles($alert-info-bg); }
+.alert-warning    { @include alert-styles($alert-warning-bg); }
+.alert-danger     { @include alert-styles($alert-danger-bg); }
+
+
+//
+// Progress bars
+// --------------------------------------------------
+
+// Give the progress background some depth
+.progress {
+  @include gradient-vertical($start-color: darken($progress-bg, 4%), $end-color: $progress-bg)
+}
+
+// Mixin for generating new styles
+@mixin progress-bar-styles($color) {
+  @include gradient-vertical($start-color: $color, $end-color: darken($color, 10%));
+}
+
+// Apply the mixin to the progress bars
+.progress-bar            { @include progress-bar-styles($progress-bar-bg); }
+.progress-bar-success    { @include progress-bar-styles($progress-bar-success-bg); }
+.progress-bar-info       { @include progress-bar-styles($progress-bar-info-bg); }
+.progress-bar-warning    { @include progress-bar-styles($progress-bar-warning-bg); }
+.progress-bar-danger     { @include progress-bar-styles($progress-bar-danger-bg); }
+
+// Reset the striped class because our mixins don't do multiple gradients and
+// the above custom styles override the new `.progress-bar-striped` in v3.2.0.
+.progress-bar-striped {
+  @include gradient-striped;
+}
+
+
+//
+// List groups
+// --------------------------------------------------
+
+.list-group {
+  border-radius: $border-radius-base;
+  @include box-shadow(0 1px 2px rgba(0,0,0,.075));
+}
+.list-group-item.active,
+.list-group-item.active:hover,
+.list-group-item.active:focus {
+  text-shadow: 0 -1px 0 darken($list-group-active-bg, 10%);
+  @include gradient-vertical($start-color: $list-group-active-bg, $end-color: darken($list-group-active-bg, 7.5%));
+  border-color: darken($list-group-active-border, 7.5%);
+
+  .badge {
+    text-shadow: none;
+  }
+}
+
+
+//
+// Panels
+// --------------------------------------------------
+
+// Common styles
+.panel {
+  @include box-shadow(0 1px 2px rgba(0,0,0,.05));
+}
+
+// Mixin for generating new styles
+@mixin panel-heading-styles($color) {
+  @include gradient-vertical($start-color: $color, $end-color: darken($color, 5%));
+}
+
+// Apply the mixin to the panel headings only
+.panel-default > .panel-heading   { @include panel-heading-styles($panel-default-heading-bg); }
+.panel-primary > .panel-heading   { @include panel-heading-styles($panel-primary-heading-bg); }
+.panel-success > .panel-heading   { @include panel-heading-styles($panel-success-heading-bg); }
+.panel-info > .panel-heading      { @include panel-heading-styles($panel-info-heading-bg); }
+.panel-warning > .panel-heading   { @include panel-heading-styles($panel-warning-heading-bg); }
+.panel-danger > .panel-heading    { @include panel-heading-styles($panel-danger-heading-bg); }
+
+
+//
+// Wells
+// --------------------------------------------------
+
+.well {
+  @include gradient-vertical($start-color: darken($well-bg, 5%), $end-color: $well-bg);
+  border-color: darken($well-bg, 10%);
+  $shadow: inset 0 1px 3px rgba(0,0,0,.05), 0 1px 0 rgba(255,255,255,.1);
+  @include box-shadow($shadow);
+}
diff --git a/themes/bootstrap3/scss/vendor/bootstrap/_thumbnails.scss b/themes/bootstrap3/scss/vendor/bootstrap/_thumbnails.scss
new file mode 100644
index 0000000000000000000000000000000000000000..da0e1e76cf01131006ae0c188d81d3f1470ad197
--- /dev/null
+++ b/themes/bootstrap3/scss/vendor/bootstrap/_thumbnails.scss
@@ -0,0 +1,38 @@
+//
+// Thumbnails
+// --------------------------------------------------
+
+
+// Mixin and adjust the regular image class
+.thumbnail {
+  display: block;
+  padding: $thumbnail-padding;
+  margin-bottom: $line-height-computed;
+  line-height: $line-height-base;
+  background-color: $thumbnail-bg;
+  border: 1px solid $thumbnail-border;
+  border-radius: $thumbnail-border-radius;
+  @include transition(border .2s ease-in-out);
+
+  > img,
+  a > img {
+    @include img-responsive;
+    margin-left: auto;
+    margin-right: auto;
+  }
+
+  // [converter] extracted a&:hover, a&:focus, a&.active to a.thumbnail:hover, a.thumbnail:focus, a.thumbnail.active
+
+  // Image captions
+  .caption {
+    padding: $thumbnail-caption-padding;
+    color: $thumbnail-caption-color;
+  }
+}
+
+// Add a hover state for linked versions only
+a.thumbnail:hover,
+a.thumbnail:focus,
+a.thumbnail.active {
+  border-color: $link-color;
+}
diff --git a/themes/bootstrap3/scss/vendor/bootstrap/_tooltip.scss b/themes/bootstrap3/scss/vendor/bootstrap/_tooltip.scss
new file mode 100644
index 0000000000000000000000000000000000000000..f0c165827434484c9121d320324284b265ace7f1
--- /dev/null
+++ b/themes/bootstrap3/scss/vendor/bootstrap/_tooltip.scss
@@ -0,0 +1,101 @@
+//
+// Tooltips
+// --------------------------------------------------
+
+
+// Base class
+.tooltip {
+  position: absolute;
+  z-index: $zindex-tooltip;
+  display: block;
+  // Our parent element can be arbitrary since tooltips are by default inserted as a sibling of their target element.
+  // So reset our font and text properties to avoid inheriting weird values.
+  @include reset-text;
+  font-size: $font-size-small;
+
+  @include opacity(0);
+
+  &.in     { @include opacity($tooltip-opacity); }
+  &.top    { margin-top:  -3px; padding: $tooltip-arrow-width 0; }
+  &.right  { margin-left:  3px; padding: 0 $tooltip-arrow-width; }
+  &.bottom { margin-top:   3px; padding: $tooltip-arrow-width 0; }
+  &.left   { margin-left: -3px; padding: 0 $tooltip-arrow-width; }
+}
+
+// Wrapper for the tooltip content
+.tooltip-inner {
+  max-width: $tooltip-max-width;
+  padding: 3px 8px;
+  color: $tooltip-color;
+  text-align: center;
+  background-color: $tooltip-bg;
+  border-radius: $border-radius-base;
+}
+
+// Arrows
+.tooltip-arrow {
+  position: absolute;
+  width: 0;
+  height: 0;
+  border-color: transparent;
+  border-style: solid;
+}
+// Note: Deprecated .top-left, .top-right, .bottom-left, and .bottom-right as of v3.3.1
+.tooltip {
+  &.top .tooltip-arrow {
+    bottom: 0;
+    left: 50%;
+    margin-left: -$tooltip-arrow-width;
+    border-width: $tooltip-arrow-width $tooltip-arrow-width 0;
+    border-top-color: $tooltip-arrow-color;
+  }
+  &.top-left .tooltip-arrow {
+    bottom: 0;
+    right: $tooltip-arrow-width;
+    margin-bottom: -$tooltip-arrow-width;
+    border-width: $tooltip-arrow-width $tooltip-arrow-width 0;
+    border-top-color: $tooltip-arrow-color;
+  }
+  &.top-right .tooltip-arrow {
+    bottom: 0;
+    left: $tooltip-arrow-width;
+    margin-bottom: -$tooltip-arrow-width;
+    border-width: $tooltip-arrow-width $tooltip-arrow-width 0;
+    border-top-color: $tooltip-arrow-color;
+  }
+  &.right .tooltip-arrow {
+    top: 50%;
+    left: 0;
+    margin-top: -$tooltip-arrow-width;
+    border-width: $tooltip-arrow-width $tooltip-arrow-width $tooltip-arrow-width 0;
+    border-right-color: $tooltip-arrow-color;
+  }
+  &.left .tooltip-arrow {
+    top: 50%;
+    right: 0;
+    margin-top: -$tooltip-arrow-width;
+    border-width: $tooltip-arrow-width 0 $tooltip-arrow-width $tooltip-arrow-width;
+    border-left-color: $tooltip-arrow-color;
+  }
+  &.bottom .tooltip-arrow {
+    top: 0;
+    left: 50%;
+    margin-left: -$tooltip-arrow-width;
+    border-width: 0 $tooltip-arrow-width $tooltip-arrow-width;
+    border-bottom-color: $tooltip-arrow-color;
+  }
+  &.bottom-left .tooltip-arrow {
+    top: 0;
+    right: $tooltip-arrow-width;
+    margin-top: -$tooltip-arrow-width;
+    border-width: 0 $tooltip-arrow-width $tooltip-arrow-width;
+    border-bottom-color: $tooltip-arrow-color;
+  }
+  &.bottom-right .tooltip-arrow {
+    top: 0;
+    left: $tooltip-arrow-width;
+    margin-top: -$tooltip-arrow-width;
+    border-width: 0 $tooltip-arrow-width $tooltip-arrow-width;
+    border-bottom-color: $tooltip-arrow-color;
+  }
+}
diff --git a/themes/bootstrap3/scss/vendor/bootstrap/_type.scss b/themes/bootstrap3/scss/vendor/bootstrap/_type.scss
new file mode 100644
index 0000000000000000000000000000000000000000..620796adc1e0a5c3a75a33a7175e238a92ee034f
--- /dev/null
+++ b/themes/bootstrap3/scss/vendor/bootstrap/_type.scss
@@ -0,0 +1,298 @@
+//
+// Typography
+// --------------------------------------------------
+
+
+// Headings
+// -------------------------
+
+h1, h2, h3, h4, h5, h6,
+.h1, .h2, .h3, .h4, .h5, .h6 {
+  font-family: $headings-font-family;
+  font-weight: $headings-font-weight;
+  line-height: $headings-line-height;
+  color: $headings-color;
+
+  small,
+  .small {
+    font-weight: normal;
+    line-height: 1;
+    color: $headings-small-color;
+  }
+}
+
+h1, .h1,
+h2, .h2,
+h3, .h3 {
+  margin-top: $line-height-computed;
+  margin-bottom: ($line-height-computed / 2);
+
+  small,
+  .small {
+    font-size: 65%;
+  }
+}
+h4, .h4,
+h5, .h5,
+h6, .h6 {
+  margin-top: ($line-height-computed / 2);
+  margin-bottom: ($line-height-computed / 2);
+
+  small,
+  .small {
+    font-size: 75%;
+  }
+}
+
+h1, .h1 { font-size: $font-size-h1; }
+h2, .h2 { font-size: $font-size-h2; }
+h3, .h3 { font-size: $font-size-h3; }
+h4, .h4 { font-size: $font-size-h4; }
+h5, .h5 { font-size: $font-size-h5; }
+h6, .h6 { font-size: $font-size-h6; }
+
+
+// Body text
+// -------------------------
+
+p {
+  margin: 0 0 ($line-height-computed / 2);
+}
+
+.lead {
+  margin-bottom: $line-height-computed;
+  font-size: floor(($font-size-base * 1.15));
+  font-weight: 300;
+  line-height: 1.4;
+
+  @media (min-width: $screen-sm-min) {
+    font-size: ($font-size-base * 1.5);
+  }
+}
+
+
+// Emphasis & misc
+// -------------------------
+
+// Ex: (12px small font / 14px base font) * 100% = about 85%
+small,
+.small {
+  font-size: floor((100% * $font-size-small / $font-size-base));
+}
+
+mark,
+.mark {
+  background-color: $state-warning-bg;
+  padding: .2em;
+}
+
+// Alignment
+.text-left           { text-align: left; }
+.text-right          { text-align: right; }
+.text-center         { text-align: center; }
+.text-justify        { text-align: justify; }
+.text-nowrap         { white-space: nowrap; }
+
+// Transformation
+.text-lowercase      { text-transform: lowercase; }
+.text-uppercase      { text-transform: uppercase; }
+.text-capitalize     { text-transform: capitalize; }
+
+// Contextual colors
+.text-muted {
+  color: $text-muted;
+}
+
+@include text-emphasis-variant('.text-primary', $brand-primary);
+
+@include text-emphasis-variant('.text-success', $state-success-text);
+
+@include text-emphasis-variant('.text-info', $state-info-text);
+
+@include text-emphasis-variant('.text-warning', $state-warning-text);
+
+@include text-emphasis-variant('.text-danger', $state-danger-text);
+
+// Contextual backgrounds
+// For now we'll leave these alongside the text classes until v4 when we can
+// safely shift things around (per SemVer rules).
+.bg-primary {
+  // Given the contrast here, this is the only class to have its color inverted
+  // automatically.
+  color: #fff;
+}
+@include bg-variant('.bg-primary', $brand-primary);
+
+@include bg-variant('.bg-success', $state-success-bg);
+
+@include bg-variant('.bg-info', $state-info-bg);
+
+@include bg-variant('.bg-warning', $state-warning-bg);
+
+@include bg-variant('.bg-danger', $state-danger-bg);
+
+
+// Page header
+// -------------------------
+
+.page-header {
+  padding-bottom: (($line-height-computed / 2) - 1);
+  margin: ($line-height-computed * 2) 0 $line-height-computed;
+  border-bottom: 1px solid $page-header-border-color;
+}
+
+
+// Lists
+// -------------------------
+
+// Unordered and Ordered lists
+ul,
+ol {
+  margin-top: 0;
+  margin-bottom: ($line-height-computed / 2);
+  ul,
+  ol {
+    margin-bottom: 0;
+  }
+}
+
+// List options
+
+// [converter] extracted from `.list-unstyled` for libsass compatibility
+@mixin list-unstyled {
+  padding-left: 0;
+  list-style: none;
+}
+// [converter] extracted as `@mixin list-unstyled` for libsass compatibility
+.list-unstyled {
+  @include list-unstyled;
+}
+
+
+// Inline turns list items into inline-block
+.list-inline {
+  @include list-unstyled;
+  margin-left: -5px;
+
+  > li {
+    display: inline-block;
+    padding-left: 5px;
+    padding-right: 5px;
+  }
+}
+
+// Description Lists
+dl {
+  margin-top: 0; // Remove browser default
+  margin-bottom: $line-height-computed;
+}
+dt,
+dd {
+  line-height: $line-height-base;
+}
+dt {
+  font-weight: bold;
+}
+dd {
+  margin-left: 0; // Undo browser default
+}
+
+// Horizontal description lists
+//
+// Defaults to being stacked without any of the below styles applied, until the
+// grid breakpoint is reached (default of ~768px).
+
+.dl-horizontal {
+  dd {
+    @include clearfix; // Clear the floated `dt` if an empty `dd` is present
+  }
+
+  @media (min-width: $dl-horizontal-breakpoint) {
+    dt {
+      float: left;
+      width: ($dl-horizontal-offset - 20);
+      clear: left;
+      text-align: right;
+      @include text-overflow;
+    }
+    dd {
+      margin-left: $dl-horizontal-offset;
+    }
+  }
+}
+
+
+// Misc
+// -------------------------
+
+// Abbreviations and acronyms
+abbr[title],
+// Add data-* attribute to help out our tooltip plugin, per https://github.com/twbs/bootstrap/issues/5257
+abbr[data-original-title] {
+  cursor: help;
+  border-bottom: 1px dotted $abbr-border-color;
+}
+.initialism {
+  font-size: 90%;
+  @extend .text-uppercase;
+}
+
+// Blockquotes
+blockquote {
+  padding: ($line-height-computed / 2) $line-height-computed;
+  margin: 0 0 $line-height-computed;
+  font-size: $blockquote-font-size;
+  border-left: 5px solid $blockquote-border-color;
+
+  p,
+  ul,
+  ol {
+    &:last-child {
+      margin-bottom: 0;
+    }
+  }
+
+  // Note: Deprecated small and .small as of v3.1.0
+  // Context: https://github.com/twbs/bootstrap/issues/11660
+  footer,
+  small,
+  .small {
+    display: block;
+    font-size: 80%; // back to default font-size
+    line-height: $line-height-base;
+    color: $blockquote-small-color;
+
+    &:before {
+      content: '\2014 \00A0'; // em dash, nbsp
+    }
+  }
+}
+
+// Opposite alignment of blockquote
+//
+// Heads up: `blockquote.pull-right` has been deprecated as of v3.1.0.
+.blockquote-reverse,
+blockquote.pull-right {
+  padding-right: 15px;
+  padding-left: 0;
+  border-right: 5px solid $blockquote-border-color;
+  border-left: 0;
+  text-align: right;
+
+  // Account for citation
+  footer,
+  small,
+  .small {
+    &:before { content: ''; }
+    &:after {
+      content: '\00A0 \2014'; // nbsp, em dash
+    }
+  }
+}
+
+// Addresses
+address {
+  margin-bottom: $line-height-computed;
+  font-style: normal;
+  line-height: $line-height-base;
+}
diff --git a/themes/bootstrap3/scss/vendor/bootstrap/_utilities.scss b/themes/bootstrap3/scss/vendor/bootstrap/_utilities.scss
new file mode 100644
index 0000000000000000000000000000000000000000..8c99c71643ed94ad7000f105faf4610095270d33
--- /dev/null
+++ b/themes/bootstrap3/scss/vendor/bootstrap/_utilities.scss
@@ -0,0 +1,55 @@
+//
+// Utility classes
+// --------------------------------------------------
+
+
+// Floats
+// -------------------------
+
+.clearfix {
+  @include clearfix;
+}
+.center-block {
+  @include center-block;
+}
+.pull-right {
+  float: right !important;
+}
+.pull-left {
+  float: left !important;
+}
+
+
+// Toggling content
+// -------------------------
+
+// Note: Deprecated .hide in favor of .hidden or .sr-only (as appropriate) in v3.0.1
+.hide {
+  display: none !important;
+}
+.show {
+  display: block !important;
+}
+.invisible {
+  visibility: hidden;
+}
+.text-hide {
+  @include text-hide;
+}
+
+
+// Hide from screenreaders and browsers
+//
+// Credit: HTML5 Boilerplate
+
+.hidden {
+  display: none !important;
+}
+
+
+// For Affix plugin
+// -------------------------
+
+.affix {
+  position: fixed;
+}
diff --git a/themes/bootstrap3/scss/vendor/bootstrap/_variables.scss b/themes/bootstrap3/scss/vendor/bootstrap/_variables.scss
new file mode 100644
index 0000000000000000000000000000000000000000..e04968529171ed730a99e741dea4a2d593be0ebb
--- /dev/null
+++ b/themes/bootstrap3/scss/vendor/bootstrap/_variables.scss
@@ -0,0 +1,874 @@
+$bootstrap-sass-asset-helper: false !default;
+//
+// Variables
+// --------------------------------------------------
+
+
+//== Colors
+//
+//## Gray and brand colors for use across Bootstrap.
+
+$gray-base:              #000 !default;
+$gray-darker:            lighten($gray-base, 13.5%) !default; // #222
+$gray-dark:              lighten($gray-base, 20%) !default;   // #333
+$gray:                   lighten($gray-base, 33.5%) !default; // #555
+$gray-light:             lighten($gray-base, 46.7%) !default; // #777
+$gray-lighter:           lighten($gray-base, 93.5%) !default; // #eee
+
+$brand-primary:         darken(#428bca, 6.5%) !default; // #337ab7
+$brand-success:         #5cb85c !default;
+$brand-info:            #5bc0de !default;
+$brand-warning:         #f0ad4e !default;
+$brand-danger:          #d9534f !default;
+
+
+//== Scaffolding
+//
+//## Settings for some of the most global styles.
+
+//** Background color for `<body>`.
+$body-bg:               #fff !default;
+//** Global text color on `<body>`.
+$text-color:            $gray-dark !default;
+
+//** Global textual link color.
+$link-color:            $brand-primary !default;
+//** Link hover color set via `darken()` function.
+$link-hover-color:      darken($link-color, 15%) !default;
+//** Link hover decoration.
+$link-hover-decoration: underline !default;
+
+
+//== Typography
+//
+//## Font, line-height, and color for body text, headings, and more.
+
+$font-family-sans-serif:  "Helvetica Neue", Helvetica, Arial, sans-serif !default;
+$font-family-serif:       Georgia, "Times New Roman", Times, serif !default;
+//** Default monospace fonts for `<code>`, `<kbd>`, and `<pre>`.
+$font-family-monospace:   Menlo, Monaco, Consolas, "Courier New", monospace !default;
+$font-family-base:        $font-family-sans-serif !default;
+
+$font-size-base:          14px !default;
+$font-size-large:         ceil(($font-size-base * 1.25)) !default; // ~18px
+$font-size-small:         ceil(($font-size-base * 0.85)) !default; // ~12px
+
+$font-size-h1:            floor(($font-size-base * 2.6)) !default; // ~36px
+$font-size-h2:            floor(($font-size-base * 2.15)) !default; // ~30px
+$font-size-h3:            ceil(($font-size-base * 1.7)) !default; // ~24px
+$font-size-h4:            ceil(($font-size-base * 1.25)) !default; // ~18px
+$font-size-h5:            $font-size-base !default;
+$font-size-h6:            ceil(($font-size-base * 0.85)) !default; // ~12px
+
+//** Unit-less `line-height` for use in components like buttons.
+$line-height-base:        1.428571429 !default; // 20/14
+//** Computed "line-height" (`font-size` * `line-height`) for use with `margin`, `padding`, etc.
+$line-height-computed:    floor(($font-size-base * $line-height-base)) !default; // ~20px
+
+//** By default, this inherits from the `<body>`.
+$headings-font-family:    inherit !default;
+$headings-font-weight:    500 !default;
+$headings-line-height:    1.1 !default;
+$headings-color:          inherit !default;
+
+
+//== Iconography
+//
+//## Specify custom location and filename of the included Glyphicons icon font. Useful for those including Bootstrap via Bower.
+
+//** Load fonts from this directory.
+
+// [converter] If $bootstrap-sass-asset-helper if used, provide path relative to the assets load path.
+// [converter] This is because some asset helpers, such as Sprockets, do not work with file-relative paths.
+$icon-font-path: if($bootstrap-sass-asset-helper, "bootstrap/", "../fonts/bootstrap/") !default;
+
+//** File name for all font files.
+$icon-font-name:          "glyphicons-halflings-regular" !default;
+//** Element ID within SVG icon file.
+$icon-font-svg-id:        "glyphicons_halflingsregular" !default;
+
+
+//== Components
+//
+//## Define common padding and border radius sizes and more. Values based on 14px text and 1.428 line-height (~20px to start).
+
+$padding-base-vertical:     6px !default;
+$padding-base-horizontal:   12px !default;
+
+$padding-large-vertical:    10px !default;
+$padding-large-horizontal:  16px !default;
+
+$padding-small-vertical:    5px !default;
+$padding-small-horizontal:  10px !default;
+
+$padding-xs-vertical:       1px !default;
+$padding-xs-horizontal:     5px !default;
+
+$line-height-large:         1.3333333 !default; // extra decimals for Win 8.1 Chrome
+$line-height-small:         1.5 !default;
+
+$border-radius-base:        4px !default;
+$border-radius-large:       6px !default;
+$border-radius-small:       3px !default;
+
+//** Global color for active items (e.g., navs or dropdowns).
+$component-active-color:    #fff !default;
+//** Global background color for active items (e.g., navs or dropdowns).
+$component-active-bg:       $brand-primary !default;
+
+//** Width of the `border` for generating carets that indicate dropdowns.
+$caret-width-base:          4px !default;
+//** Carets increase slightly in size for larger components.
+$caret-width-large:         5px !default;
+
+
+//== Tables
+//
+//## Customizes the `.table` component with basic values, each used across all table variations.
+
+//** Padding for `<th>`s and `<td>`s.
+$table-cell-padding:            8px !default;
+//** Padding for cells in `.table-condensed`.
+$table-condensed-cell-padding:  5px !default;
+
+//** Default background color used for all tables.
+$table-bg:                      transparent !default;
+//** Background color used for `.table-striped`.
+$table-bg-accent:               #f9f9f9 !default;
+//** Background color used for `.table-hover`.
+$table-bg-hover:                #f5f5f5 !default;
+$table-bg-active:               $table-bg-hover !default;
+
+//** Border color for table and cell borders.
+$table-border-color:            #ddd !default;
+
+
+//== Buttons
+//
+//## For each of Bootstrap's buttons, define text, background and border color.
+
+$btn-font-weight:                normal !default;
+
+$btn-default-color:              #333 !default;
+$btn-default-bg:                 #fff !default;
+$btn-default-border:             #ccc !default;
+
+$btn-primary-color:              #fff !default;
+$btn-primary-bg:                 $brand-primary !default;
+$btn-primary-border:             darken($btn-primary-bg, 5%) !default;
+
+$btn-success-color:              #fff !default;
+$btn-success-bg:                 $brand-success !default;
+$btn-success-border:             darken($btn-success-bg, 5%) !default;
+
+$btn-info-color:                 #fff !default;
+$btn-info-bg:                    $brand-info !default;
+$btn-info-border:                darken($btn-info-bg, 5%) !default;
+
+$btn-warning-color:              #fff !default;
+$btn-warning-bg:                 $brand-warning !default;
+$btn-warning-border:             darken($btn-warning-bg, 5%) !default;
+
+$btn-danger-color:               #fff !default;
+$btn-danger-bg:                  $brand-danger !default;
+$btn-danger-border:              darken($btn-danger-bg, 5%) !default;
+
+$btn-link-disabled-color:        $gray-light !default;
+
+// Allows for customizing button radius independently from global border radius
+$btn-border-radius-base:         $border-radius-base !default;
+$btn-border-radius-large:        $border-radius-large !default;
+$btn-border-radius-small:        $border-radius-small !default;
+
+
+//== Forms
+//
+//##
+
+//** `<input>` background color
+$input-bg:                       #fff !default;
+//** `<input disabled>` background color
+$input-bg-disabled:              $gray-lighter !default;
+
+//** Text color for `<input>`s
+$input-color:                    $gray !default;
+//** `<input>` border color
+$input-border:                   #ccc !default;
+
+// TODO: Rename `$input-border-radius` to `$input-border-radius-base` in v4
+//** Default `.form-control` border radius
+// This has no effect on `<select>`s in some browsers, due to the limited stylability of `<select>`s in CSS.
+$input-border-radius:            $border-radius-base !default;
+//** Large `.form-control` border radius
+$input-border-radius-large:      $border-radius-large !default;
+//** Small `.form-control` border radius
+$input-border-radius-small:      $border-radius-small !default;
+
+//** Border color for inputs on focus
+$input-border-focus:             #66afe9 !default;
+
+//** Placeholder text color
+$input-color-placeholder:        #999 !default;
+
+//** Default `.form-control` height
+$input-height-base:              ($line-height-computed + ($padding-base-vertical * 2) + 2) !default;
+//** Large `.form-control` height
+$input-height-large:             (ceil($font-size-large * $line-height-large) + ($padding-large-vertical * 2) + 2) !default;
+//** Small `.form-control` height
+$input-height-small:             (floor($font-size-small * $line-height-small) + ($padding-small-vertical * 2) + 2) !default;
+
+//** `.form-group` margin
+$form-group-margin-bottom:       15px !default;
+
+$legend-color:                   $gray-dark !default;
+$legend-border-color:            #e5e5e5 !default;
+
+//** Background color for textual input addons
+$input-group-addon-bg:           $gray-lighter !default;
+//** Border color for textual input addons
+$input-group-addon-border-color: $input-border !default;
+
+//** Disabled cursor for form controls and buttons.
+$cursor-disabled:                not-allowed !default;
+
+
+//== Dropdowns
+//
+//## Dropdown menu container and contents.
+
+//** Background for the dropdown menu.
+$dropdown-bg:                    #fff !default;
+//** Dropdown menu `border-color`.
+$dropdown-border:                rgba(0,0,0,.15) !default;
+//** Dropdown menu `border-color` **for IE8**.
+$dropdown-fallback-border:       #ccc !default;
+//** Divider color for between dropdown items.
+$dropdown-divider-bg:            #e5e5e5 !default;
+
+//** Dropdown link text color.
+$dropdown-link-color:            $gray-dark !default;
+//** Hover color for dropdown links.
+$dropdown-link-hover-color:      darken($gray-dark, 5%) !default;
+//** Hover background for dropdown links.
+$dropdown-link-hover-bg:         #f5f5f5 !default;
+
+//** Active dropdown menu item text color.
+$dropdown-link-active-color:     $component-active-color !default;
+//** Active dropdown menu item background color.
+$dropdown-link-active-bg:        $component-active-bg !default;
+
+//** Disabled dropdown menu item background color.
+$dropdown-link-disabled-color:   $gray-light !default;
+
+//** Text color for headers within dropdown menus.
+$dropdown-header-color:          $gray-light !default;
+
+//** Deprecated `$dropdown-caret-color` as of v3.1.0
+$dropdown-caret-color:           #000 !default;
+
+
+//-- Z-index master list
+//
+// Warning: Avoid customizing these values. They're used for a bird's eye view
+// of components dependent on the z-axis and are designed to all work together.
+//
+// Note: These variables are not generated into the Customizer.
+
+$zindex-navbar:            1000 !default;
+$zindex-dropdown:          1000 !default;
+$zindex-popover:           1060 !default;
+$zindex-tooltip:           1070 !default;
+$zindex-navbar-fixed:      1030 !default;
+$zindex-modal-background:  1040 !default;
+$zindex-modal:             1050 !default;
+
+
+//== Media queries breakpoints
+//
+//## Define the breakpoints at which your layout will change, adapting to different screen sizes.
+
+// Extra small screen / phone
+//** Deprecated `$screen-xs` as of v3.0.1
+$screen-xs:                  480px !default;
+//** Deprecated `$screen-xs-min` as of v3.2.0
+$screen-xs-min:              $screen-xs !default;
+//** Deprecated `$screen-phone` as of v3.0.1
+$screen-phone:               $screen-xs-min !default;
+
+// Small screen / tablet
+//** Deprecated `$screen-sm` as of v3.0.1
+$screen-sm:                  768px !default;
+$screen-sm-min:              $screen-sm !default;
+//** Deprecated `$screen-tablet` as of v3.0.1
+$screen-tablet:              $screen-sm-min !default;
+
+// Medium screen / desktop
+//** Deprecated `$screen-md` as of v3.0.1
+$screen-md:                  992px !default;
+$screen-md-min:              $screen-md !default;
+//** Deprecated `$screen-desktop` as of v3.0.1
+$screen-desktop:             $screen-md-min !default;
+
+// Large screen / wide desktop
+//** Deprecated `$screen-lg` as of v3.0.1
+$screen-lg:                  1200px !default;
+$screen-lg-min:              $screen-lg !default;
+//** Deprecated `$screen-lg-desktop` as of v3.0.1
+$screen-lg-desktop:          $screen-lg-min !default;
+
+// So media queries don't overlap when required, provide a maximum
+$screen-xs-max:              ($screen-sm-min - 1) !default;
+$screen-sm-max:              ($screen-md-min - 1) !default;
+$screen-md-max:              ($screen-lg-min - 1) !default;
+
+
+//== Grid system
+//
+//## Define your custom responsive grid.
+
+//** Number of columns in the grid.
+$grid-columns:              12 !default;
+//** Padding between columns. Gets divided in half for the left and right.
+$grid-gutter-width:         30px !default;
+// Navbar collapse
+//** Point at which the navbar becomes uncollapsed.
+$grid-float-breakpoint:     $screen-sm-min !default;
+//** Point at which the navbar begins collapsing.
+$grid-float-breakpoint-max: ($grid-float-breakpoint - 1) !default;
+
+
+//== Container sizes
+//
+//## Define the maximum width of `.container` for different screen sizes.
+
+// Small screen / tablet
+$container-tablet:             (720px + $grid-gutter-width) !default;
+//** For `$screen-sm-min` and up.
+$container-sm:                 $container-tablet !default;
+
+// Medium screen / desktop
+$container-desktop:            (940px + $grid-gutter-width) !default;
+//** For `$screen-md-min` and up.
+$container-md:                 $container-desktop !default;
+
+// Large screen / wide desktop
+$container-large-desktop:      (1140px + $grid-gutter-width) !default;
+//** For `$screen-lg-min` and up.
+$container-lg:                 $container-large-desktop !default;
+
+
+//== Navbar
+//
+//##
+
+// Basics of a navbar
+$navbar-height:                    50px !default;
+$navbar-margin-bottom:             $line-height-computed !default;
+$navbar-border-radius:             $border-radius-base !default;
+$navbar-padding-horizontal:        floor(($grid-gutter-width / 2)) !default;
+$navbar-padding-vertical:          (($navbar-height - $line-height-computed) / 2) !default;
+$navbar-collapse-max-height:       340px !default;
+
+$navbar-default-color:             #777 !default;
+$navbar-default-bg:                #f8f8f8 !default;
+$navbar-default-border:            darken($navbar-default-bg, 6.5%) !default;
+
+// Navbar links
+$navbar-default-link-color:                #777 !default;
+$navbar-default-link-hover-color:          #333 !default;
+$navbar-default-link-hover-bg:             transparent !default;
+$navbar-default-link-active-color:         #555 !default;
+$navbar-default-link-active-bg:            darken($navbar-default-bg, 6.5%) !default;
+$navbar-default-link-disabled-color:       #ccc !default;
+$navbar-default-link-disabled-bg:          transparent !default;
+
+// Navbar brand label
+$navbar-default-brand-color:               $navbar-default-link-color !default;
+$navbar-default-brand-hover-color:         darken($navbar-default-brand-color, 10%) !default;
+$navbar-default-brand-hover-bg:            transparent !default;
+
+// Navbar toggle
+$navbar-default-toggle-hover-bg:           #ddd !default;
+$navbar-default-toggle-icon-bar-bg:        #888 !default;
+$navbar-default-toggle-border-color:       #ddd !default;
+
+
+//=== Inverted navbar
+// Reset inverted navbar basics
+$navbar-inverse-color:                      lighten($gray-light, 15%) !default;
+$navbar-inverse-bg:                         #222 !default;
+$navbar-inverse-border:                     darken($navbar-inverse-bg, 10%) !default;
+
+// Inverted navbar links
+$navbar-inverse-link-color:                 lighten($gray-light, 15%) !default;
+$navbar-inverse-link-hover-color:           #fff !default;
+$navbar-inverse-link-hover-bg:              transparent !default;
+$navbar-inverse-link-active-color:          $navbar-inverse-link-hover-color !default;
+$navbar-inverse-link-active-bg:             darken($navbar-inverse-bg, 10%) !default;
+$navbar-inverse-link-disabled-color:        #444 !default;
+$navbar-inverse-link-disabled-bg:           transparent !default;
+
+// Inverted navbar brand label
+$navbar-inverse-brand-color:                $navbar-inverse-link-color !default;
+$navbar-inverse-brand-hover-color:          #fff !default;
+$navbar-inverse-brand-hover-bg:             transparent !default;
+
+// Inverted navbar toggle
+$navbar-inverse-toggle-hover-bg:            #333 !default;
+$navbar-inverse-toggle-icon-bar-bg:         #fff !default;
+$navbar-inverse-toggle-border-color:        #333 !default;
+
+
+//== Navs
+//
+//##
+
+//=== Shared nav styles
+$nav-link-padding:                          10px 15px !default;
+$nav-link-hover-bg:                         $gray-lighter !default;
+
+$nav-disabled-link-color:                   $gray-light !default;
+$nav-disabled-link-hover-color:             $gray-light !default;
+
+//== Tabs
+$nav-tabs-border-color:                     #ddd !default;
+
+$nav-tabs-link-hover-border-color:          $gray-lighter !default;
+
+$nav-tabs-active-link-hover-bg:             $body-bg !default;
+$nav-tabs-active-link-hover-color:          $gray !default;
+$nav-tabs-active-link-hover-border-color:   #ddd !default;
+
+$nav-tabs-justified-link-border-color:            #ddd !default;
+$nav-tabs-justified-active-link-border-color:     $body-bg !default;
+
+//== Pills
+$nav-pills-border-radius:                   $border-radius-base !default;
+$nav-pills-active-link-hover-bg:            $component-active-bg !default;
+$nav-pills-active-link-hover-color:         $component-active-color !default;
+
+
+//== Pagination
+//
+//##
+
+$pagination-color:                     $link-color !default;
+$pagination-bg:                        #fff !default;
+$pagination-border:                    #ddd !default;
+
+$pagination-hover-color:               $link-hover-color !default;
+$pagination-hover-bg:                  $gray-lighter !default;
+$pagination-hover-border:              #ddd !default;
+
+$pagination-active-color:              #fff !default;
+$pagination-active-bg:                 $brand-primary !default;
+$pagination-active-border:             $brand-primary !default;
+
+$pagination-disabled-color:            $gray-light !default;
+$pagination-disabled-bg:               #fff !default;
+$pagination-disabled-border:           #ddd !default;
+
+
+//== Pager
+//
+//##
+
+$pager-bg:                             $pagination-bg !default;
+$pager-border:                         $pagination-border !default;
+$pager-border-radius:                  15px !default;
+
+$pager-hover-bg:                       $pagination-hover-bg !default;
+
+$pager-active-bg:                      $pagination-active-bg !default;
+$pager-active-color:                   $pagination-active-color !default;
+
+$pager-disabled-color:                 $pagination-disabled-color !default;
+
+
+//== Jumbotron
+//
+//##
+
+$jumbotron-padding:              30px !default;
+$jumbotron-color:                inherit !default;
+$jumbotron-bg:                   $gray-lighter !default;
+$jumbotron-heading-color:        inherit !default;
+$jumbotron-font-size:            ceil(($font-size-base * 1.5)) !default;
+$jumbotron-heading-font-size:    ceil(($font-size-base * 4.5)) !default;
+
+
+//== Form states and alerts
+//
+//## Define colors for form feedback states and, by default, alerts.
+
+$state-success-text:             #3c763d !default;
+$state-success-bg:               #dff0d8 !default;
+$state-success-border:           darken(adjust-hue($state-success-bg, -10), 5%) !default;
+
+$state-info-text:                #31708f !default;
+$state-info-bg:                  #d9edf7 !default;
+$state-info-border:              darken(adjust-hue($state-info-bg, -10), 7%) !default;
+
+$state-warning-text:             #8a6d3b !default;
+$state-warning-bg:               #fcf8e3 !default;
+$state-warning-border:           darken(adjust-hue($state-warning-bg, -10), 5%) !default;
+
+$state-danger-text:              #a94442 !default;
+$state-danger-bg:                #f2dede !default;
+$state-danger-border:            darken(adjust-hue($state-danger-bg, -10), 5%) !default;
+
+
+//== Tooltips
+//
+//##
+
+//** Tooltip max width
+$tooltip-max-width:           200px !default;
+//** Tooltip text color
+$tooltip-color:               #fff !default;
+//** Tooltip background color
+$tooltip-bg:                  #000 !default;
+$tooltip-opacity:             .9 !default;
+
+//** Tooltip arrow width
+$tooltip-arrow-width:         5px !default;
+//** Tooltip arrow color
+$tooltip-arrow-color:         $tooltip-bg !default;
+
+
+//== Popovers
+//
+//##
+
+//** Popover body background color
+$popover-bg:                          #fff !default;
+//** Popover maximum width
+$popover-max-width:                   276px !default;
+//** Popover border color
+$popover-border-color:                rgba(0,0,0,.2) !default;
+//** Popover fallback border color
+$popover-fallback-border-color:       #ccc !default;
+
+//** Popover title background color
+$popover-title-bg:                    darken($popover-bg, 3%) !default;
+
+//** Popover arrow width
+$popover-arrow-width:                 10px !default;
+//** Popover arrow color
+$popover-arrow-color:                 $popover-bg !default;
+
+//** Popover outer arrow width
+$popover-arrow-outer-width:           ($popover-arrow-width + 1) !default;
+//** Popover outer arrow color
+$popover-arrow-outer-color:           fade_in($popover-border-color, 0.05) !default;
+//** Popover outer arrow fallback color
+$popover-arrow-outer-fallback-color:  darken($popover-fallback-border-color, 20%) !default;
+
+
+//== Labels
+//
+//##
+
+//** Default label background color
+$label-default-bg:            $gray-light !default;
+//** Primary label background color
+$label-primary-bg:            $brand-primary !default;
+//** Success label background color
+$label-success-bg:            $brand-success !default;
+//** Info label background color
+$label-info-bg:               $brand-info !default;
+//** Warning label background color
+$label-warning-bg:            $brand-warning !default;
+//** Danger label background color
+$label-danger-bg:             $brand-danger !default;
+
+//** Default label text color
+$label-color:                 #fff !default;
+//** Default text color of a linked label
+$label-link-hover-color:      #fff !default;
+
+
+//== Modals
+//
+//##
+
+//** Padding applied to the modal body
+$modal-inner-padding:         15px !default;
+
+//** Padding applied to the modal title
+$modal-title-padding:         15px !default;
+//** Modal title line-height
+$modal-title-line-height:     $line-height-base !default;
+
+//** Background color of modal content area
+$modal-content-bg:                             #fff !default;
+//** Modal content border color
+$modal-content-border-color:                   rgba(0,0,0,.2) !default;
+//** Modal content border color **for IE8**
+$modal-content-fallback-border-color:          #999 !default;
+
+//** Modal backdrop background color
+$modal-backdrop-bg:           #000 !default;
+//** Modal backdrop opacity
+$modal-backdrop-opacity:      .5 !default;
+//** Modal header border color
+$modal-header-border-color:   #e5e5e5 !default;
+//** Modal footer border color
+$modal-footer-border-color:   $modal-header-border-color !default;
+
+$modal-lg:                    900px !default;
+$modal-md:                    600px !default;
+$modal-sm:                    300px !default;
+
+
+//== Alerts
+//
+//## Define alert colors, border radius, and padding.
+
+$alert-padding:               15px !default;
+$alert-border-radius:         $border-radius-base !default;
+$alert-link-font-weight:      bold !default;
+
+$alert-success-bg:            $state-success-bg !default;
+$alert-success-text:          $state-success-text !default;
+$alert-success-border:        $state-success-border !default;
+
+$alert-info-bg:               $state-info-bg !default;
+$alert-info-text:             $state-info-text !default;
+$alert-info-border:           $state-info-border !default;
+
+$alert-warning-bg:            $state-warning-bg !default;
+$alert-warning-text:          $state-warning-text !default;
+$alert-warning-border:        $state-warning-border !default;
+
+$alert-danger-bg:             $state-danger-bg !default;
+$alert-danger-text:           $state-danger-text !default;
+$alert-danger-border:         $state-danger-border !default;
+
+
+//== Progress bars
+//
+//##
+
+//** Background color of the whole progress component
+$progress-bg:                 #f5f5f5 !default;
+//** Progress bar text color
+$progress-bar-color:          #fff !default;
+//** Variable for setting rounded corners on progress bar.
+$progress-border-radius:      $border-radius-base !default;
+
+//** Default progress bar color
+$progress-bar-bg:             $brand-primary !default;
+//** Success progress bar color
+$progress-bar-success-bg:     $brand-success !default;
+//** Warning progress bar color
+$progress-bar-warning-bg:     $brand-warning !default;
+//** Danger progress bar color
+$progress-bar-danger-bg:      $brand-danger !default;
+//** Info progress bar color
+$progress-bar-info-bg:        $brand-info !default;
+
+
+//== List group
+//
+//##
+
+//** Background color on `.list-group-item`
+$list-group-bg:                 #fff !default;
+//** `.list-group-item` border color
+$list-group-border:             #ddd !default;
+//** List group border radius
+$list-group-border-radius:      $border-radius-base !default;
+
+//** Background color of single list items on hover
+$list-group-hover-bg:           #f5f5f5 !default;
+//** Text color of active list items
+$list-group-active-color:       $component-active-color !default;
+//** Background color of active list items
+$list-group-active-bg:          $component-active-bg !default;
+//** Border color of active list elements
+$list-group-active-border:      $list-group-active-bg !default;
+//** Text color for content within active list items
+$list-group-active-text-color:  lighten($list-group-active-bg, 40%) !default;
+
+//** Text color of disabled list items
+$list-group-disabled-color:      $gray-light !default;
+//** Background color of disabled list items
+$list-group-disabled-bg:         $gray-lighter !default;
+//** Text color for content within disabled list items
+$list-group-disabled-text-color: $list-group-disabled-color !default;
+
+$list-group-link-color:         #555 !default;
+$list-group-link-hover-color:   $list-group-link-color !default;
+$list-group-link-heading-color: #333 !default;
+
+
+//== Panels
+//
+//##
+
+$panel-bg:                    #fff !default;
+$panel-body-padding:          15px !default;
+$panel-heading-padding:       10px 15px !default;
+$panel-footer-padding:        $panel-heading-padding !default;
+$panel-border-radius:         $border-radius-base !default;
+
+//** Border color for elements within panels
+$panel-inner-border:          #ddd !default;
+$panel-footer-bg:             #f5f5f5 !default;
+
+$panel-default-text:          $gray-dark !default;
+$panel-default-border:        #ddd !default;
+$panel-default-heading-bg:    #f5f5f5 !default;
+
+$panel-primary-text:          #fff !default;
+$panel-primary-border:        $brand-primary !default;
+$panel-primary-heading-bg:    $brand-primary !default;
+
+$panel-success-text:          $state-success-text !default;
+$panel-success-border:        $state-success-border !default;
+$panel-success-heading-bg:    $state-success-bg !default;
+
+$panel-info-text:             $state-info-text !default;
+$panel-info-border:           $state-info-border !default;
+$panel-info-heading-bg:       $state-info-bg !default;
+
+$panel-warning-text:          $state-warning-text !default;
+$panel-warning-border:        $state-warning-border !default;
+$panel-warning-heading-bg:    $state-warning-bg !default;
+
+$panel-danger-text:           $state-danger-text !default;
+$panel-danger-border:         $state-danger-border !default;
+$panel-danger-heading-bg:     $state-danger-bg !default;
+
+
+//== Thumbnails
+//
+//##
+
+//** Padding around the thumbnail image
+$thumbnail-padding:           4px !default;
+//** Thumbnail background color
+$thumbnail-bg:                $body-bg !default;
+//** Thumbnail border color
+$thumbnail-border:            #ddd !default;
+//** Thumbnail border radius
+$thumbnail-border-radius:     $border-radius-base !default;
+
+//** Custom text color for thumbnail captions
+$thumbnail-caption-color:     $text-color !default;
+//** Padding around the thumbnail caption
+$thumbnail-caption-padding:   9px !default;
+
+
+//== Wells
+//
+//##
+
+$well-bg:                     #f5f5f5 !default;
+$well-border:                 darken($well-bg, 7%) !default;
+
+
+//== Badges
+//
+//##
+
+$badge-color:                 #fff !default;
+//** Linked badge text color on hover
+$badge-link-hover-color:      #fff !default;
+$badge-bg:                    $gray-light !default;
+
+//** Badge text color in active nav link
+$badge-active-color:          $link-color !default;
+//** Badge background color in active nav link
+$badge-active-bg:             #fff !default;
+
+$badge-font-weight:           bold !default;
+$badge-line-height:           1 !default;
+$badge-border-radius:         10px !default;
+
+
+//== Breadcrumbs
+//
+//##
+
+$breadcrumb-padding-vertical:   8px !default;
+$breadcrumb-padding-horizontal: 15px !default;
+//** Breadcrumb background color
+$breadcrumb-bg:                 #f5f5f5 !default;
+//** Breadcrumb text color
+$breadcrumb-color:              #ccc !default;
+//** Text color of current page in the breadcrumb
+$breadcrumb-active-color:       $gray-light !default;
+//** Textual separator for between breadcrumb elements
+$breadcrumb-separator:          "/" !default;
+
+
+//== Carousel
+//
+//##
+
+$carousel-text-shadow:                        0 1px 2px rgba(0,0,0,.6) !default;
+
+$carousel-control-color:                      #fff !default;
+$carousel-control-width:                      15% !default;
+$carousel-control-opacity:                    .5 !default;
+$carousel-control-font-size:                  20px !default;
+
+$carousel-indicator-active-bg:                #fff !default;
+$carousel-indicator-border-color:             #fff !default;
+
+$carousel-caption-color:                      #fff !default;
+
+
+//== Close
+//
+//##
+
+$close-font-weight:           bold !default;
+$close-color:                 #000 !default;
+$close-text-shadow:           0 1px 0 #fff !default;
+
+
+//== Code
+//
+//##
+
+$code-color:                  #c7254e !default;
+$code-bg:                     #f9f2f4 !default;
+
+$kbd-color:                   #fff !default;
+$kbd-bg:                      #333 !default;
+
+$pre-bg:                      #f5f5f5 !default;
+$pre-color:                   $gray-dark !default;
+$pre-border-color:            #ccc !default;
+$pre-scrollable-max-height:   340px !default;
+
+
+//== Type
+//
+//##
+
+//** Horizontal offset for forms and lists.
+$component-offset-horizontal: 180px !default;
+//** Text muted color
+$text-muted:                  $gray-light !default;
+//** Abbreviations and acronyms border color
+$abbr-border-color:           $gray-light !default;
+//** Headings small color
+$headings-small-color:        $gray-light !default;
+//** Blockquote small color
+$blockquote-small-color:      $gray-light !default;
+//** Blockquote font size
+$blockquote-font-size:        ($font-size-base * 1.25) !default;
+//** Blockquote border color
+$blockquote-border-color:     $gray-lighter !default;
+//** Page header border color
+$page-header-border-color:    $gray-lighter !default;
+//** Width of horizontal description list titles
+$dl-horizontal-offset:        $component-offset-horizontal !default;
+//** Point at which .dl-horizontal becomes horizontal
+$dl-horizontal-breakpoint:    $grid-float-breakpoint !default;
+//** Horizontal line color.
+$hr-border:                   $gray-lighter !default;
diff --git a/themes/bootstrap3/scss/vendor/bootstrap/_wells.scss b/themes/bootstrap3/scss/vendor/bootstrap/_wells.scss
new file mode 100644
index 0000000000000000000000000000000000000000..b8657118a661cfb1f6119f7dccb169db06e4cd04
--- /dev/null
+++ b/themes/bootstrap3/scss/vendor/bootstrap/_wells.scss
@@ -0,0 +1,29 @@
+//
+// Wells
+// --------------------------------------------------
+
+
+// Base class
+.well {
+  min-height: 20px;
+  padding: 19px;
+  margin-bottom: 20px;
+  background-color: $well-bg;
+  border: 1px solid $well-border;
+  border-radius: $border-radius-base;
+  @include box-shadow(inset 0 1px 1px rgba(0,0,0,.05));
+  blockquote {
+    border-color: #ddd;
+    border-color: rgba(0,0,0,.15);
+  }
+}
+
+// Sizes
+.well-lg {
+  padding: 24px;
+  border-radius: $border-radius-large;
+}
+.well-sm {
+  padding: 9px;
+  border-radius: $border-radius-small;
+}
diff --git a/themes/bootstrap3/scss/vendor/bootstrap/mixins/_alerts.scss b/themes/bootstrap3/scss/vendor/bootstrap/mixins/_alerts.scss
new file mode 100644
index 0000000000000000000000000000000000000000..3faf0b5a507d799af422ac2b65f20ef155781830
--- /dev/null
+++ b/themes/bootstrap3/scss/vendor/bootstrap/mixins/_alerts.scss
@@ -0,0 +1,14 @@
+// Alerts
+
+@mixin alert-variant($background, $border, $text-color) {
+  background-color: $background;
+  border-color: $border;
+  color: $text-color;
+
+  hr {
+    border-top-color: darken($border, 5%);
+  }
+  .alert-link {
+    color: darken($text-color, 10%);
+  }
+}
diff --git a/themes/bootstrap3/scss/vendor/bootstrap/mixins/_background-variant.scss b/themes/bootstrap3/scss/vendor/bootstrap/mixins/_background-variant.scss
new file mode 100644
index 0000000000000000000000000000000000000000..4c7769e13a3665a5cb9b12884f3ed82cfb277f34
--- /dev/null
+++ b/themes/bootstrap3/scss/vendor/bootstrap/mixins/_background-variant.scss
@@ -0,0 +1,12 @@
+// Contextual backgrounds
+
+// [converter] $parent hack
+@mixin bg-variant($parent, $color) {
+  #{$parent} {
+    background-color: $color;
+  }
+  a#{$parent}:hover,
+  a#{$parent}:focus {
+    background-color: darken($color, 10%);
+  }
+}
diff --git a/themes/bootstrap3/scss/vendor/bootstrap/mixins/_border-radius.scss b/themes/bootstrap3/scss/vendor/bootstrap/mixins/_border-radius.scss
new file mode 100644
index 0000000000000000000000000000000000000000..ce1949987508574a8d31b1d688096ce8925ed051
--- /dev/null
+++ b/themes/bootstrap3/scss/vendor/bootstrap/mixins/_border-radius.scss
@@ -0,0 +1,18 @@
+// Single side border-radius
+
+@mixin border-top-radius($radius) {
+  border-top-right-radius: $radius;
+   border-top-left-radius: $radius;
+}
+@mixin border-right-radius($radius) {
+  border-bottom-right-radius: $radius;
+     border-top-right-radius: $radius;
+}
+@mixin border-bottom-radius($radius) {
+  border-bottom-right-radius: $radius;
+   border-bottom-left-radius: $radius;
+}
+@mixin border-left-radius($radius) {
+  border-bottom-left-radius: $radius;
+     border-top-left-radius: $radius;
+}
diff --git a/themes/bootstrap3/scss/vendor/bootstrap/mixins/_buttons.scss b/themes/bootstrap3/scss/vendor/bootstrap/mixins/_buttons.scss
new file mode 100644
index 0000000000000000000000000000000000000000..b93f84b2cb69d72278590188f862e06853451afd
--- /dev/null
+++ b/themes/bootstrap3/scss/vendor/bootstrap/mixins/_buttons.scss
@@ -0,0 +1,65 @@
+// Button variants
+//
+// Easily pump out default styles, as well as :hover, :focus, :active,
+// and disabled options for all buttons
+
+@mixin button-variant($color, $background, $border) {
+  color: $color;
+  background-color: $background;
+  border-color: $border;
+
+  &:focus,
+  &.focus {
+    color: $color;
+    background-color: darken($background, 10%);
+        border-color: darken($border, 25%);
+  }
+  &:hover {
+    color: $color;
+    background-color: darken($background, 10%);
+        border-color: darken($border, 12%);
+  }
+  &:active,
+  &.active,
+  .open > &.dropdown-toggle {
+    color: $color;
+    background-color: darken($background, 10%);
+        border-color: darken($border, 12%);
+
+    &:hover,
+    &:focus,
+    &.focus {
+      color: $color;
+      background-color: darken($background, 17%);
+          border-color: darken($border, 25%);
+    }
+  }
+  &:active,
+  &.active,
+  .open > &.dropdown-toggle {
+    background-image: none;
+  }
+  &.disabled,
+  &[disabled],
+  fieldset[disabled] & {
+    &:hover,
+    &:focus,
+    &.focus {
+      background-color: $background;
+          border-color: $border;
+    }
+  }
+
+  .badge {
+    color: $background;
+    background-color: $color;
+  }
+}
+
+// Button sizes
+@mixin button-size($padding-vertical, $padding-horizontal, $font-size, $line-height, $border-radius) {
+  padding: $padding-vertical $padding-horizontal;
+  font-size: $font-size;
+  line-height: $line-height;
+  border-radius: $border-radius;
+}
diff --git a/themes/bootstrap3/scss/vendor/bootstrap/mixins/_center-block.scss b/themes/bootstrap3/scss/vendor/bootstrap/mixins/_center-block.scss
new file mode 100644
index 0000000000000000000000000000000000000000..e06fb5e276e471ac4bd574068254808089695afd
--- /dev/null
+++ b/themes/bootstrap3/scss/vendor/bootstrap/mixins/_center-block.scss
@@ -0,0 +1,7 @@
+// Center-align a block level element
+
+@mixin center-block() {
+  display: block;
+  margin-left: auto;
+  margin-right: auto;
+}
diff --git a/themes/bootstrap3/scss/vendor/bootstrap/mixins/_clearfix.scss b/themes/bootstrap3/scss/vendor/bootstrap/mixins/_clearfix.scss
new file mode 100644
index 0000000000000000000000000000000000000000..dc3e2ab426edf4d5ae1f27d5c767ff396f359243
--- /dev/null
+++ b/themes/bootstrap3/scss/vendor/bootstrap/mixins/_clearfix.scss
@@ -0,0 +1,22 @@
+// Clearfix
+//
+// For modern browsers
+// 1. The space content is one way to avoid an Opera bug when the
+//    contenteditable attribute is included anywhere else in the document.
+//    Otherwise it causes space to appear at the top and bottom of elements
+//    that are clearfixed.
+// 2. The use of `table` rather than `block` is only necessary if using
+//    `:before` to contain the top-margins of child elements.
+//
+// Source: http://nicolasgallagher.com/micro-clearfix-hack/
+
+@mixin clearfix() {
+  &:before,
+  &:after {
+    content: " "; // 1
+    display: table; // 2
+  }
+  &:after {
+    clear: both;
+  }
+}
diff --git a/themes/bootstrap3/scss/vendor/bootstrap/mixins/_forms.scss b/themes/bootstrap3/scss/vendor/bootstrap/mixins/_forms.scss
new file mode 100644
index 0000000000000000000000000000000000000000..277aa5f8e1872166a784a188a36d90ab4ba6a3bc
--- /dev/null
+++ b/themes/bootstrap3/scss/vendor/bootstrap/mixins/_forms.scss
@@ -0,0 +1,88 @@
+// Form validation states
+//
+// Used in forms.less to generate the form validation CSS for warnings, errors,
+// and successes.
+
+@mixin form-control-validation($text-color: #555, $border-color: #ccc, $background-color: #f5f5f5) {
+  // Color the label and help text
+  .help-block,
+  .control-label,
+  .radio,
+  .checkbox,
+  .radio-inline,
+  .checkbox-inline,
+  &.radio label,
+  &.checkbox label,
+  &.radio-inline label,
+  &.checkbox-inline label  {
+    color: $text-color;
+  }
+  // Set the border and box shadow on specific inputs to match
+  .form-control {
+    border-color: $border-color;
+    @include box-shadow(inset 0 1px 1px rgba(0,0,0,.075)); // Redeclare so transitions work
+    &:focus {
+      border-color: darken($border-color, 10%);
+      $shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 6px lighten($border-color, 20%);
+      @include box-shadow($shadow);
+    }
+  }
+  // Set validation states also for addons
+  .input-group-addon {
+    color: $text-color;
+    border-color: $border-color;
+    background-color: $background-color;
+  }
+  // Optional feedback icon
+  .form-control-feedback {
+    color: $text-color;
+  }
+}
+
+
+// Form control focus state
+//
+// Generate a customized focus state and for any input with the specified color,
+// which defaults to the `$input-border-focus` variable.
+//
+// We highly encourage you to not customize the default value, but instead use
+// this to tweak colors on an as-needed basis. This aesthetic change is based on
+// WebKit's default styles, but applicable to a wider range of browsers. Its
+// usability and accessibility should be taken into account with any change.
+//
+// Example usage: change the default blue border and shadow to white for better
+// contrast against a dark gray background.
+@mixin form-control-focus($color: $input-border-focus) {
+  $color-rgba: rgba(red($color), green($color), blue($color), .6);
+  &:focus {
+    border-color: $color;
+    outline: 0;
+    @include box-shadow(inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px $color-rgba);
+  }
+}
+
+// Form control sizing
+//
+// Relative text size, padding, and border-radii changes for form controls. For
+// horizontal sizing, wrap controls in the predefined grid classes. `<select>`
+// element gets special love because it's special, and that's a fact!
+// [converter] $parent hack
+@mixin input-size($parent, $input-height, $padding-vertical, $padding-horizontal, $font-size, $line-height, $border-radius) {
+  #{$parent} {
+    height: $input-height;
+    padding: $padding-vertical $padding-horizontal;
+    font-size: $font-size;
+    line-height: $line-height;
+    border-radius: $border-radius;
+  }
+
+  select#{$parent} {
+    height: $input-height;
+    line-height: $input-height;
+  }
+
+  textarea#{$parent},
+  select[multiple]#{$parent} {
+    height: auto;
+  }
+}
diff --git a/themes/bootstrap3/scss/vendor/bootstrap/mixins/_gradients.scss b/themes/bootstrap3/scss/vendor/bootstrap/mixins/_gradients.scss
new file mode 100644
index 0000000000000000000000000000000000000000..a8939f5ae6411aa9f9dabce969e1a20760eb0ef2
--- /dev/null
+++ b/themes/bootstrap3/scss/vendor/bootstrap/mixins/_gradients.scss
@@ -0,0 +1,58 @@
+// Gradients
+
+
+
+// Horizontal gradient, from left to right
+//
+// Creates two color stops, start and end, by specifying a color and position for each color stop.
+// Color stops are not available in IE9 and below.
+@mixin gradient-horizontal($start-color: #555, $end-color: #333, $start-percent: 0%, $end-percent: 100%) {
+  background-image: -webkit-linear-gradient(left, $start-color $start-percent, $end-color $end-percent); // Safari 5.1-6, Chrome 10+
+  background-image: -o-linear-gradient(left, $start-color $start-percent, $end-color $end-percent); // Opera 12
+  background-image: linear-gradient(to right, $start-color $start-percent, $end-color $end-percent); // Standard, IE10, Firefox 16+, Opera 12.10+, Safari 7+, Chrome 26+
+  background-repeat: repeat-x;
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#{ie-hex-str($start-color)}', endColorstr='#{ie-hex-str($end-color)}', GradientType=1); // IE9 and down
+}
+
+// Vertical gradient, from top to bottom
+//
+// Creates two color stops, start and end, by specifying a color and position for each color stop.
+// Color stops are not available in IE9 and below.
+@mixin gradient-vertical($start-color: #555, $end-color: #333, $start-percent: 0%, $end-percent: 100%) {
+  background-image: -webkit-linear-gradient(top, $start-color $start-percent, $end-color $end-percent);  // Safari 5.1-6, Chrome 10+
+  background-image: -o-linear-gradient(top, $start-color $start-percent, $end-color $end-percent);  // Opera 12
+  background-image: linear-gradient(to bottom, $start-color $start-percent, $end-color $end-percent); // Standard, IE10, Firefox 16+, Opera 12.10+, Safari 7+, Chrome 26+
+  background-repeat: repeat-x;
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#{ie-hex-str($start-color)}', endColorstr='#{ie-hex-str($end-color)}', GradientType=0); // IE9 and down
+}
+
+@mixin gradient-directional($start-color: #555, $end-color: #333, $deg: 45deg) {
+  background-repeat: repeat-x;
+  background-image: -webkit-linear-gradient($deg, $start-color, $end-color); // Safari 5.1-6, Chrome 10+
+  background-image: -o-linear-gradient($deg, $start-color, $end-color); // Opera 12
+  background-image: linear-gradient($deg, $start-color, $end-color); // Standard, IE10, Firefox 16+, Opera 12.10+, Safari 7+, Chrome 26+
+}
+@mixin gradient-horizontal-three-colors($start-color: #00b3ee, $mid-color: #7a43b6, $color-stop: 50%, $end-color: #c3325f) {
+  background-image: -webkit-linear-gradient(left, $start-color, $mid-color $color-stop, $end-color);
+  background-image: -o-linear-gradient(left, $start-color, $mid-color $color-stop, $end-color);
+  background-image: linear-gradient(to right, $start-color, $mid-color $color-stop, $end-color);
+  background-repeat: no-repeat;
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#{ie-hex-str($start-color)}', endColorstr='#{ie-hex-str($end-color)}', GradientType=1); // IE9 and down, gets no color-stop at all for proper fallback
+}
+@mixin gradient-vertical-three-colors($start-color: #00b3ee, $mid-color: #7a43b6, $color-stop: 50%, $end-color: #c3325f) {
+  background-image: -webkit-linear-gradient($start-color, $mid-color $color-stop, $end-color);
+  background-image: -o-linear-gradient($start-color, $mid-color $color-stop, $end-color);
+  background-image: linear-gradient($start-color, $mid-color $color-stop, $end-color);
+  background-repeat: no-repeat;
+  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#{ie-hex-str($start-color)}', endColorstr='#{ie-hex-str($end-color)}', GradientType=0); // IE9 and down, gets no color-stop at all for proper fallback
+}
+@mixin gradient-radial($inner-color: #555, $outer-color: #333) {
+  background-image: -webkit-radial-gradient(circle, $inner-color, $outer-color);
+  background-image: radial-gradient(circle, $inner-color, $outer-color);
+  background-repeat: no-repeat;
+}
+@mixin gradient-striped($color: rgba(255,255,255,.15), $angle: 45deg) {
+  background-image: -webkit-linear-gradient($angle, $color 25%, transparent 25%, transparent 50%, $color 50%, $color 75%, transparent 75%, transparent);
+  background-image: -o-linear-gradient($angle, $color 25%, transparent 25%, transparent 50%, $color 50%, $color 75%, transparent 75%, transparent);
+  background-image: linear-gradient($angle, $color 25%, transparent 25%, transparent 50%, $color 50%, $color 75%, transparent 75%, transparent);
+}
diff --git a/themes/bootstrap3/scss/vendor/bootstrap/mixins/_grid-framework.scss b/themes/bootstrap3/scss/vendor/bootstrap/mixins/_grid-framework.scss
new file mode 100644
index 0000000000000000000000000000000000000000..16d038c04f3b1993608a76fd773eb39ec141dfa1
--- /dev/null
+++ b/themes/bootstrap3/scss/vendor/bootstrap/mixins/_grid-framework.scss
@@ -0,0 +1,81 @@
+// Framework grid generation
+//
+// Used only by Bootstrap to generate the correct number of grid classes given
+// any value of `$grid-columns`.
+
+// [converter] This is defined recursively in LESS, but Sass supports real loops
+@mixin make-grid-columns($i: 1, $list: ".col-xs-#{$i}, .col-sm-#{$i}, .col-md-#{$i}, .col-lg-#{$i}") {
+  @for $i from (1 + 1) through $grid-columns {
+    $list: "#{$list}, .col-xs-#{$i}, .col-sm-#{$i}, .col-md-#{$i}, .col-lg-#{$i}";
+  }
+  #{$list} {
+    position: relative;
+    // Prevent columns from collapsing when empty
+    min-height: 1px;
+    // Inner gutter via padding
+    padding-left:  ceil(($grid-gutter-width / 2));
+    padding-right: floor(($grid-gutter-width / 2));
+  }
+}
+
+
+// [converter] This is defined recursively in LESS, but Sass supports real loops
+@mixin float-grid-columns($class, $i: 1, $list: ".col-#{$class}-#{$i}") {
+  @for $i from (1 + 1) through $grid-columns {
+    $list: "#{$list}, .col-#{$class}-#{$i}";
+  }
+  #{$list} {
+    float: left;
+  }
+}
+
+
+@mixin calc-grid-column($index, $class, $type) {
+  @if ($type == width) and ($index > 0) {
+    .col-#{$class}-#{$index} {
+      width: percentage(($index / $grid-columns));
+    }
+  }
+  @if ($type == push) and ($index > 0) {
+    .col-#{$class}-push-#{$index} {
+      left: percentage(($index / $grid-columns));
+    }
+  }
+  @if ($type == push) and ($index == 0) {
+    .col-#{$class}-push-0 {
+      left: auto;
+    }
+  }
+  @if ($type == pull) and ($index > 0) {
+    .col-#{$class}-pull-#{$index} {
+      right: percentage(($index / $grid-columns));
+    }
+  }
+  @if ($type == pull) and ($index == 0) {
+    .col-#{$class}-pull-0 {
+      right: auto;
+    }
+  }
+  @if ($type == offset) {
+    .col-#{$class}-offset-#{$index} {
+      margin-left: percentage(($index / $grid-columns));
+    }
+  }
+}
+
+// [converter] This is defined recursively in LESS, but Sass supports real loops
+@mixin loop-grid-columns($columns, $class, $type) {
+  @for $i from 0 through $columns {
+    @include calc-grid-column($i, $class, $type);
+  }
+}
+
+
+// Create grid for specific class
+@mixin make-grid($class) {
+  @include float-grid-columns($class);
+  @include loop-grid-columns($grid-columns, $class, width);
+  @include loop-grid-columns($grid-columns, $class, pull);
+  @include loop-grid-columns($grid-columns, $class, push);
+  @include loop-grid-columns($grid-columns, $class, offset);
+}
diff --git a/themes/bootstrap3/scss/vendor/bootstrap/mixins/_grid.scss b/themes/bootstrap3/scss/vendor/bootstrap/mixins/_grid.scss
new file mode 100644
index 0000000000000000000000000000000000000000..59551dac1eda335f9d73112a4b414abadf232bfc
--- /dev/null
+++ b/themes/bootstrap3/scss/vendor/bootstrap/mixins/_grid.scss
@@ -0,0 +1,122 @@
+// Grid system
+//
+// Generate semantic grid columns with these mixins.
+
+// Centered container element
+@mixin container-fixed($gutter: $grid-gutter-width) {
+  margin-right: auto;
+  margin-left: auto;
+  padding-left:  floor(($gutter / 2));
+  padding-right: ceil(($gutter / 2));
+  @include clearfix;
+}
+
+// Creates a wrapper for a series of columns
+@mixin make-row($gutter: $grid-gutter-width) {
+  margin-left:  ceil(($gutter / -2));
+  margin-right: floor(($gutter / -2));
+  @include clearfix;
+}
+
+// Generate the extra small columns
+@mixin make-xs-column($columns, $gutter: $grid-gutter-width) {
+  position: relative;
+  float: left;
+  width: percentage(($columns / $grid-columns));
+  min-height: 1px;
+  padding-left:  ($gutter / 2);
+  padding-right: ($gutter / 2);
+}
+@mixin make-xs-column-offset($columns) {
+  margin-left: percentage(($columns / $grid-columns));
+}
+@mixin make-xs-column-push($columns) {
+  left: percentage(($columns / $grid-columns));
+}
+@mixin make-xs-column-pull($columns) {
+  right: percentage(($columns / $grid-columns));
+}
+
+// Generate the small columns
+@mixin make-sm-column($columns, $gutter: $grid-gutter-width) {
+  position: relative;
+  min-height: 1px;
+  padding-left:  ($gutter / 2);
+  padding-right: ($gutter / 2);
+
+  @media (min-width: $screen-sm-min) {
+    float: left;
+    width: percentage(($columns / $grid-columns));
+  }
+}
+@mixin make-sm-column-offset($columns) {
+  @media (min-width: $screen-sm-min) {
+    margin-left: percentage(($columns / $grid-columns));
+  }
+}
+@mixin make-sm-column-push($columns) {
+  @media (min-width: $screen-sm-min) {
+    left: percentage(($columns / $grid-columns));
+  }
+}
+@mixin make-sm-column-pull($columns) {
+  @media (min-width: $screen-sm-min) {
+    right: percentage(($columns / $grid-columns));
+  }
+}
+
+// Generate the medium columns
+@mixin make-md-column($columns, $gutter: $grid-gutter-width) {
+  position: relative;
+  min-height: 1px;
+  padding-left:  ($gutter / 2);
+  padding-right: ($gutter / 2);
+
+  @media (min-width: $screen-md-min) {
+    float: left;
+    width: percentage(($columns / $grid-columns));
+  }
+}
+@mixin make-md-column-offset($columns) {
+  @media (min-width: $screen-md-min) {
+    margin-left: percentage(($columns / $grid-columns));
+  }
+}
+@mixin make-md-column-push($columns) {
+  @media (min-width: $screen-md-min) {
+    left: percentage(($columns / $grid-columns));
+  }
+}
+@mixin make-md-column-pull($columns) {
+  @media (min-width: $screen-md-min) {
+    right: percentage(($columns / $grid-columns));
+  }
+}
+
+// Generate the large columns
+@mixin make-lg-column($columns, $gutter: $grid-gutter-width) {
+  position: relative;
+  min-height: 1px;
+  padding-left:  ($gutter / 2);
+  padding-right: ($gutter / 2);
+
+  @media (min-width: $screen-lg-min) {
+    float: left;
+    width: percentage(($columns / $grid-columns));
+  }
+}
+@mixin make-lg-column-offset($columns) {
+  @media (min-width: $screen-lg-min) {
+    margin-left: percentage(($columns / $grid-columns));
+  }
+}
+@mixin make-lg-column-push($columns) {
+  @media (min-width: $screen-lg-min) {
+    left: percentage(($columns / $grid-columns));
+  }
+}
+@mixin make-lg-column-pull($columns) {
+  @media (min-width: $screen-lg-min) {
+    right: percentage(($columns / $grid-columns));
+  }
+}
diff --git a/themes/bootstrap3/scss/vendor/bootstrap/mixins/_hide-text.scss b/themes/bootstrap3/scss/vendor/bootstrap/mixins/_hide-text.scss
new file mode 100644
index 0000000000000000000000000000000000000000..1767e029c53a5e817ce79e9d489fd2cc114d319c
--- /dev/null
+++ b/themes/bootstrap3/scss/vendor/bootstrap/mixins/_hide-text.scss
@@ -0,0 +1,21 @@
+// CSS image replacement
+//
+// Heads up! v3 launched with only `.hide-text()`, but per our pattern for
+// mixins being reused as classes with the same name, this doesn't hold up. As
+// of v3.0.1 we have added `.text-hide()` and deprecated `.hide-text()`.
+//
+// Source: https://github.com/h5bp/html5-boilerplate/commit/aa0396eae757
+
+// Deprecated as of v3.0.1 (has been removed in v4)
+@mixin hide-text() {
+  font: 0/0 a;
+  color: transparent;
+  text-shadow: none;
+  background-color: transparent;
+  border: 0;
+}
+
+// New mixin to use as of v3.0.1
+@mixin text-hide() {
+  @include hide-text;
+}
diff --git a/themes/bootstrap3/scss/vendor/bootstrap/mixins/_image.scss b/themes/bootstrap3/scss/vendor/bootstrap/mixins/_image.scss
new file mode 100644
index 0000000000000000000000000000000000000000..c8dcf5e9cd867de4d0ba528736719483266a1ae0
--- /dev/null
+++ b/themes/bootstrap3/scss/vendor/bootstrap/mixins/_image.scss
@@ -0,0 +1,33 @@
+// Image Mixins
+// - Responsive image
+// - Retina image
+
+
+// Responsive image
+//
+// Keep images from scaling beyond the width of their parents.
+@mixin img-responsive($display: block) {
+  display: $display;
+  max-width: 100%; // Part 1: Set a maximum relative to the parent
+  height: auto; // Part 2: Scale the height according to the width, otherwise you get stretching
+}
+
+
+// Retina image
+//
+// Short retina mixin for setting background-image and -size. Note that the
+// spelling of `min--moz-device-pixel-ratio` is intentional.
+@mixin img-retina($file-1x, $file-2x, $width-1x, $height-1x) {
+  background-image: url(if($bootstrap-sass-asset-helper, twbs-image-path("#{$file-1x}"), "#{$file-1x}"));
+
+  @media
+  only screen and (-webkit-min-device-pixel-ratio: 2),
+  only screen and (   min--moz-device-pixel-ratio: 2),
+  only screen and (     -o-min-device-pixel-ratio: 2/1),
+  only screen and (        min-device-pixel-ratio: 2),
+  only screen and (                min-resolution: 192dpi),
+  only screen and (                min-resolution: 2dppx) {
+    background-image: url(if($bootstrap-sass-asset-helper, twbs-image-path("#{$file-2x}"), "#{$file-2x}"));
+    background-size: $width-1x $height-1x;
+  }
+}
diff --git a/themes/bootstrap3/scss/vendor/bootstrap/mixins/_labels.scss b/themes/bootstrap3/scss/vendor/bootstrap/mixins/_labels.scss
new file mode 100644
index 0000000000000000000000000000000000000000..eda6dfd29ea1709380b27748ef5d082fed20903a
--- /dev/null
+++ b/themes/bootstrap3/scss/vendor/bootstrap/mixins/_labels.scss
@@ -0,0 +1,12 @@
+// Labels
+
+@mixin label-variant($color) {
+  background-color: $color;
+
+  &[href] {
+    &:hover,
+    &:focus {
+      background-color: darken($color, 10%);
+    }
+  }
+}
diff --git a/themes/bootstrap3/scss/vendor/bootstrap/mixins/_list-group.scss b/themes/bootstrap3/scss/vendor/bootstrap/mixins/_list-group.scss
new file mode 100644
index 0000000000000000000000000000000000000000..c478eeb31e321a4199329b653e06b2cb57ab3436
--- /dev/null
+++ b/themes/bootstrap3/scss/vendor/bootstrap/mixins/_list-group.scss
@@ -0,0 +1,32 @@
+// List Groups
+
+@mixin list-group-item-variant($state, $background, $color) {
+  .list-group-item-#{$state} {
+    color: $color;
+    background-color: $background;
+
+    // [converter] extracted a&, button& to a.list-group-item-#{$state}, button.list-group-item-#{$state}
+  }
+
+  a.list-group-item-#{$state},
+  button.list-group-item-#{$state} {
+    color: $color;
+
+    .list-group-item-heading {
+      color: inherit;
+    }
+
+    &:hover,
+    &:focus {
+      color: $color;
+      background-color: darken($background, 5%);
+    }
+    &.active,
+    &.active:hover,
+    &.active:focus {
+      color: #fff;
+      background-color: $color;
+      border-color: $color;
+    }
+  }
+}
diff --git a/themes/bootstrap3/scss/vendor/bootstrap/mixins/_nav-divider.scss b/themes/bootstrap3/scss/vendor/bootstrap/mixins/_nav-divider.scss
new file mode 100644
index 0000000000000000000000000000000000000000..2e6da02a4748b00cf67c21cc1735c26373e9c4c8
--- /dev/null
+++ b/themes/bootstrap3/scss/vendor/bootstrap/mixins/_nav-divider.scss
@@ -0,0 +1,10 @@
+// Horizontal dividers
+//
+// Dividers (basically an hr) within dropdowns and nav lists
+
+@mixin nav-divider($color: #e5e5e5) {
+  height: 1px;
+  margin: (($line-height-computed / 2) - 1) 0;
+  overflow: hidden;
+  background-color: $color;
+}
diff --git a/themes/bootstrap3/scss/vendor/bootstrap/mixins/_nav-vertical-align.scss b/themes/bootstrap3/scss/vendor/bootstrap/mixins/_nav-vertical-align.scss
new file mode 100644
index 0000000000000000000000000000000000000000..c8fbf1a7d67d35140c5de3b192fd917775ac8c18
--- /dev/null
+++ b/themes/bootstrap3/scss/vendor/bootstrap/mixins/_nav-vertical-align.scss
@@ -0,0 +1,9 @@
+// Navbar vertical align
+//
+// Vertically center elements in the navbar.
+// Example: an element has a height of 30px, so write out `.navbar-vertical-align(30px);` to calculate the appropriate top margin.
+
+@mixin navbar-vertical-align($element-height) {
+  margin-top: (($navbar-height - $element-height) / 2);
+  margin-bottom: (($navbar-height - $element-height) / 2);
+}
diff --git a/themes/bootstrap3/scss/vendor/bootstrap/mixins/_opacity.scss b/themes/bootstrap3/scss/vendor/bootstrap/mixins/_opacity.scss
new file mode 100644
index 0000000000000000000000000000000000000000..88e9a576abd795a9d0734542ff7e58e44a08d9a3
--- /dev/null
+++ b/themes/bootstrap3/scss/vendor/bootstrap/mixins/_opacity.scss
@@ -0,0 +1,8 @@
+// Opacity
+
+@mixin opacity($opacity) {
+  opacity: $opacity;
+  // IE8 filter
+  $opacity-ie: ($opacity * 100);
+  filter: alpha(opacity=$opacity-ie);
+}
diff --git a/themes/bootstrap3/scss/vendor/bootstrap/mixins/_pagination.scss b/themes/bootstrap3/scss/vendor/bootstrap/mixins/_pagination.scss
new file mode 100644
index 0000000000000000000000000000000000000000..d4a5404fce227fd7de1af1be1d979152d1a62694
--- /dev/null
+++ b/themes/bootstrap3/scss/vendor/bootstrap/mixins/_pagination.scss
@@ -0,0 +1,24 @@
+// Pagination
+
+@mixin pagination-size($padding-vertical, $padding-horizontal, $font-size, $line-height, $border-radius) {
+  > li {
+    > a,
+    > span {
+      padding: $padding-vertical $padding-horizontal;
+      font-size: $font-size;
+      line-height: $line-height;
+    }
+    &:first-child {
+      > a,
+      > span {
+        @include border-left-radius($border-radius);
+      }
+    }
+    &:last-child {
+      > a,
+      > span {
+        @include border-right-radius($border-radius);
+      }
+    }
+  }
+}
diff --git a/themes/bootstrap3/scss/vendor/bootstrap/mixins/_panels.scss b/themes/bootstrap3/scss/vendor/bootstrap/mixins/_panels.scss
new file mode 100644
index 0000000000000000000000000000000000000000..3ff31ae51ee0827d9a4dd3e14ff3fd18f90572fb
--- /dev/null
+++ b/themes/bootstrap3/scss/vendor/bootstrap/mixins/_panels.scss
@@ -0,0 +1,24 @@
+// Panels
+
+@mixin panel-variant($border, $heading-text-color, $heading-bg-color, $heading-border) {
+  border-color: $border;
+
+  & > .panel-heading {
+    color: $heading-text-color;
+    background-color: $heading-bg-color;
+    border-color: $heading-border;
+
+    + .panel-collapse > .panel-body {
+      border-top-color: $border;
+    }
+    .badge {
+      color: $heading-bg-color;
+      background-color: $heading-text-color;
+    }
+  }
+  & > .panel-footer {
+    + .panel-collapse > .panel-body {
+      border-bottom-color: $border;
+    }
+  }
+}
diff --git a/themes/bootstrap3/scss/vendor/bootstrap/mixins/_progress-bar.scss b/themes/bootstrap3/scss/vendor/bootstrap/mixins/_progress-bar.scss
new file mode 100644
index 0000000000000000000000000000000000000000..90a62afc2d6b33de932ff21247b9ef7eec258cf6
--- /dev/null
+++ b/themes/bootstrap3/scss/vendor/bootstrap/mixins/_progress-bar.scss
@@ -0,0 +1,10 @@
+// Progress bars
+
+@mixin progress-bar-variant($color) {
+  background-color: $color;
+
+  // Deprecated parent class requirement as of v3.2.0
+  .progress-striped & {
+    @include gradient-striped;
+  }
+}
diff --git a/themes/bootstrap3/scss/vendor/bootstrap/mixins/_reset-filter.scss b/themes/bootstrap3/scss/vendor/bootstrap/mixins/_reset-filter.scss
new file mode 100644
index 0000000000000000000000000000000000000000..bf73051200ec53b4fca0fcb50eaa8bf7b7c16d23
--- /dev/null
+++ b/themes/bootstrap3/scss/vendor/bootstrap/mixins/_reset-filter.scss
@@ -0,0 +1,8 @@
+// Reset filters for IE
+//
+// When you need to remove a gradient background, do not forget to use this to reset
+// the IE filter for IE9 and below.
+
+@mixin reset-filter() {
+  filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
+}
diff --git a/themes/bootstrap3/scss/vendor/bootstrap/mixins/_reset-text.scss b/themes/bootstrap3/scss/vendor/bootstrap/mixins/_reset-text.scss
new file mode 100644
index 0000000000000000000000000000000000000000..c9c28417fabaf6f190bd284e6e954b5dec5034d6
--- /dev/null
+++ b/themes/bootstrap3/scss/vendor/bootstrap/mixins/_reset-text.scss
@@ -0,0 +1,18 @@
+@mixin reset-text() {
+  font-family: $font-family-base;
+  // We deliberately do NOT reset font-size.
+  font-style: normal;
+  font-weight: normal;
+  letter-spacing: normal;
+  line-break: auto;
+  line-height: $line-height-base;
+  text-align: left; // Fallback for where `start` is not supported
+  text-align: start;
+  text-decoration: none;
+  text-shadow: none;
+  text-transform: none;
+  white-space: normal;
+  word-break: normal;
+  word-spacing: normal;
+  word-wrap: normal;
+}
diff --git a/themes/bootstrap3/scss/vendor/bootstrap/mixins/_resize.scss b/themes/bootstrap3/scss/vendor/bootstrap/mixins/_resize.scss
new file mode 100644
index 0000000000000000000000000000000000000000..83fa6379179cba67dbd3b3fb1b1d167380f361d4
--- /dev/null
+++ b/themes/bootstrap3/scss/vendor/bootstrap/mixins/_resize.scss
@@ -0,0 +1,6 @@
+// Resize anything
+
+@mixin resizable($direction) {
+  resize: $direction; // Options: horizontal, vertical, both
+  overflow: auto; // Per CSS3 UI, `resize` only applies when `overflow` isn't `visible`
+}
diff --git a/themes/bootstrap3/scss/vendor/bootstrap/mixins/_responsive-visibility.scss b/themes/bootstrap3/scss/vendor/bootstrap/mixins/_responsive-visibility.scss
new file mode 100644
index 0000000000000000000000000000000000000000..cbdf77723977ab2b76e6f54454534e9e6b3c5874
--- /dev/null
+++ b/themes/bootstrap3/scss/vendor/bootstrap/mixins/_responsive-visibility.scss
@@ -0,0 +1,21 @@
+// Responsive utilities
+
+//
+// More easily include all the states for responsive-utilities.less.
+// [converter] $parent hack
+@mixin responsive-visibility($parent) {
+  #{$parent} {
+    display: block !important;
+  }
+  table#{$parent}  { display: table !important; }
+  tr#{$parent}     { display: table-row !important; }
+  th#{$parent},
+  td#{$parent}     { display: table-cell !important; }
+}
+
+// [converter] $parent hack
+@mixin responsive-invisibility($parent) {
+  #{$parent} {
+    display: none !important;
+  }
+}
diff --git a/themes/bootstrap3/scss/vendor/bootstrap/mixins/_size.scss b/themes/bootstrap3/scss/vendor/bootstrap/mixins/_size.scss
new file mode 100644
index 0000000000000000000000000000000000000000..abbe2463ce8d7c315f8a2368f9301603315a35db
--- /dev/null
+++ b/themes/bootstrap3/scss/vendor/bootstrap/mixins/_size.scss
@@ -0,0 +1,10 @@
+// Sizing shortcuts
+
+@mixin size($width, $height) {
+  width: $width;
+  height: $height;
+}
+
+@mixin square($size) {
+  @include size($size, $size);
+}
diff --git a/themes/bootstrap3/scss/vendor/bootstrap/mixins/_tab-focus.scss b/themes/bootstrap3/scss/vendor/bootstrap/mixins/_tab-focus.scss
new file mode 100644
index 0000000000000000000000000000000000000000..f16ed6428aac35b4bc86d9c18501f651632d2adf
--- /dev/null
+++ b/themes/bootstrap3/scss/vendor/bootstrap/mixins/_tab-focus.scss
@@ -0,0 +1,9 @@
+// WebKit-style focus
+
+@mixin tab-focus() {
+  // WebKit-specific. Other browsers will keep their default outline style.
+  // (Initially tried to also force default via `outline: initial`,
+  // but that seems to erroneously remove the outline in Firefox altogether.)
+  outline: 5px auto -webkit-focus-ring-color;
+  outline-offset: -2px;
+}
diff --git a/themes/bootstrap3/scss/vendor/bootstrap/mixins/_table-row.scss b/themes/bootstrap3/scss/vendor/bootstrap/mixins/_table-row.scss
new file mode 100644
index 0000000000000000000000000000000000000000..136795081eb992d71b7a72dae19795bf71135316
--- /dev/null
+++ b/themes/bootstrap3/scss/vendor/bootstrap/mixins/_table-row.scss
@@ -0,0 +1,28 @@
+// Tables
+
+@mixin table-row-variant($state, $background) {
+  // Exact selectors below required to override `.table-striped` and prevent
+  // inheritance to nested tables.
+  .table > thead > tr,
+  .table > tbody > tr,
+  .table > tfoot > tr {
+    > td.#{$state},
+    > th.#{$state},
+    &.#{$state} > td,
+    &.#{$state} > th {
+      background-color: $background;
+    }
+  }
+
+  // Hover states for `.table-hover`
+  // Note: this is not available for cells or rows within `thead` or `tfoot`.
+  .table-hover > tbody > tr {
+    > td.#{$state}:hover,
+    > th.#{$state}:hover,
+    &.#{$state}:hover > td,
+    &:hover > .#{$state},
+    &.#{$state}:hover > th {
+      background-color: darken($background, 5%);
+    }
+  }
+}
diff --git a/themes/bootstrap3/scss/vendor/bootstrap/mixins/_text-emphasis.scss b/themes/bootstrap3/scss/vendor/bootstrap/mixins/_text-emphasis.scss
new file mode 100644
index 0000000000000000000000000000000000000000..3b446c41524883db963f728dbd5439ea993c8d64
--- /dev/null
+++ b/themes/bootstrap3/scss/vendor/bootstrap/mixins/_text-emphasis.scss
@@ -0,0 +1,12 @@
+// Typography
+
+// [converter] $parent hack
+@mixin text-emphasis-variant($parent, $color) {
+  #{$parent} {
+    color: $color;
+  }
+  a#{$parent}:hover,
+  a#{$parent}:focus {
+    color: darken($color, 10%);
+  }
+}
diff --git a/themes/bootstrap3/scss/vendor/bootstrap/mixins/_text-overflow.scss b/themes/bootstrap3/scss/vendor/bootstrap/mixins/_text-overflow.scss
new file mode 100644
index 0000000000000000000000000000000000000000..1593b25ea5c7b570188f99bbeda5f36b0d57e5eb
--- /dev/null
+++ b/themes/bootstrap3/scss/vendor/bootstrap/mixins/_text-overflow.scss
@@ -0,0 +1,8 @@
+// Text overflow
+// Requires inline-block or block for proper styling
+
+@mixin text-overflow() {
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
diff --git a/themes/bootstrap3/scss/vendor/bootstrap/mixins/_vendor-prefixes.scss b/themes/bootstrap3/scss/vendor/bootstrap/mixins/_vendor-prefixes.scss
new file mode 100644
index 0000000000000000000000000000000000000000..b3d0371fa866e3f5bb0d8671126087d2f862fc4d
--- /dev/null
+++ b/themes/bootstrap3/scss/vendor/bootstrap/mixins/_vendor-prefixes.scss
@@ -0,0 +1,222 @@
+// Vendor Prefixes
+//
+// All vendor mixins are deprecated as of v3.2.0 due to the introduction of
+// Autoprefixer in our Gruntfile. They have been removed in v4.
+
+// - Animations
+// - Backface visibility
+// - Box shadow
+// - Box sizing
+// - Content columns
+// - Hyphens
+// - Placeholder text
+// - Transformations
+// - Transitions
+// - User Select
+
+
+// Animations
+@mixin animation($animation) {
+  -webkit-animation: $animation;
+       -o-animation: $animation;
+          animation: $animation;
+}
+@mixin animation-name($name) {
+  -webkit-animation-name: $name;
+          animation-name: $name;
+}
+@mixin animation-duration($duration) {
+  -webkit-animation-duration: $duration;
+          animation-duration: $duration;
+}
+@mixin animation-timing-function($timing-function) {
+  -webkit-animation-timing-function: $timing-function;
+          animation-timing-function: $timing-function;
+}
+@mixin animation-delay($delay) {
+  -webkit-animation-delay: $delay;
+          animation-delay: $delay;
+}
+@mixin animation-iteration-count($iteration-count) {
+  -webkit-animation-iteration-count: $iteration-count;
+          animation-iteration-count: $iteration-count;
+}
+@mixin animation-direction($direction) {
+  -webkit-animation-direction: $direction;
+          animation-direction: $direction;
+}
+@mixin animation-fill-mode($fill-mode) {
+  -webkit-animation-fill-mode: $fill-mode;
+          animation-fill-mode: $fill-mode;
+}
+
+// Backface visibility
+// Prevent browsers from flickering when using CSS 3D transforms.
+// Default value is `visible`, but can be changed to `hidden`
+
+@mixin backface-visibility($visibility) {
+  -webkit-backface-visibility: $visibility;
+     -moz-backface-visibility: $visibility;
+          backface-visibility: $visibility;
+}
+
+// Drop shadows
+//
+// Note: Deprecated `.box-shadow()` as of v3.1.0 since all of Bootstrap's
+// supported browsers that have box shadow capabilities now support it.
+
+@mixin box-shadow($shadow...) {
+  -webkit-box-shadow: $shadow; // iOS <4.3 & Android <4.1
+          box-shadow: $shadow;
+}
+
+// Box sizing
+@mixin box-sizing($boxmodel) {
+  -webkit-box-sizing: $boxmodel;
+     -moz-box-sizing: $boxmodel;
+          box-sizing: $boxmodel;
+}
+
+// CSS3 Content Columns
+@mixin content-columns($column-count, $column-gap: $grid-gutter-width) {
+  -webkit-column-count: $column-count;
+     -moz-column-count: $column-count;
+          column-count: $column-count;
+  -webkit-column-gap: $column-gap;
+     -moz-column-gap: $column-gap;
+          column-gap: $column-gap;
+}
+
+// Optional hyphenation
+@mixin hyphens($mode: auto) {
+  word-wrap: break-word;
+  -webkit-hyphens: $mode;
+     -moz-hyphens: $mode;
+      -ms-hyphens: $mode; // IE10+
+       -o-hyphens: $mode;
+          hyphens: $mode;
+}
+
+// Placeholder text
+@mixin placeholder($color: $input-color-placeholder) {
+  // Firefox
+  &::-moz-placeholder {
+    color: $color;
+    opacity: 1; // Override Firefox's unusual default opacity; see https://github.com/twbs/bootstrap/pull/11526
+  }
+  &:-ms-input-placeholder { color: $color; } // Internet Explorer 10+
+  &::-webkit-input-placeholder  { color: $color; } // Safari and Chrome
+}
+
+// Transformations
+@mixin scale($ratio...) {
+  -webkit-transform: scale($ratio);
+      -ms-transform: scale($ratio); // IE9 only
+       -o-transform: scale($ratio);
+          transform: scale($ratio);
+}
+
+@mixin scaleX($ratio) {
+  -webkit-transform: scaleX($ratio);
+      -ms-transform: scaleX($ratio); // IE9 only
+       -o-transform: scaleX($ratio);
+          transform: scaleX($ratio);
+}
+@mixin scaleY($ratio) {
+  -webkit-transform: scaleY($ratio);
+      -ms-transform: scaleY($ratio); // IE9 only
+       -o-transform: scaleY($ratio);
+          transform: scaleY($ratio);
+}
+@mixin skew($x, $y) {
+  -webkit-transform: skewX($x) skewY($y);
+      -ms-transform: skewX($x) skewY($y); // See https://github.com/twbs/bootstrap/issues/4885; IE9+
+       -o-transform: skewX($x) skewY($y);
+          transform: skewX($x) skewY($y);
+}
+@mixin translate($x, $y) {
+  -webkit-transform: translate($x, $y);
+      -ms-transform: translate($x, $y); // IE9 only
+       -o-transform: translate($x, $y);
+          transform: translate($x, $y);
+}
+@mixin translate3d($x, $y, $z) {
+  -webkit-transform: translate3d($x, $y, $z);
+          transform: translate3d($x, $y, $z);
+}
+@mixin rotate($degrees) {
+  -webkit-transform: rotate($degrees);
+      -ms-transform: rotate($degrees); // IE9 only
+       -o-transform: rotate($degrees);
+          transform: rotate($degrees);
+}
+@mixin rotateX($degrees) {
+  -webkit-transform: rotateX($degrees);
+      -ms-transform: rotateX($degrees); // IE9 only
+       -o-transform: rotateX($degrees);
+          transform: rotateX($degrees);
+}
+@mixin rotateY($degrees) {
+  -webkit-transform: rotateY($degrees);
+      -ms-transform: rotateY($degrees); // IE9 only
+       -o-transform: rotateY($degrees);
+          transform: rotateY($degrees);
+}
+@mixin perspective($perspective) {
+  -webkit-perspective: $perspective;
+     -moz-perspective: $perspective;
+          perspective: $perspective;
+}
+@mixin perspective-origin($perspective) {
+  -webkit-perspective-origin: $perspective;
+     -moz-perspective-origin: $perspective;
+          perspective-origin: $perspective;
+}
+@mixin transform-origin($origin) {
+  -webkit-transform-origin: $origin;
+     -moz-transform-origin: $origin;
+      -ms-transform-origin: $origin; // IE9 only
+          transform-origin: $origin;
+}
+
+
+// Transitions
+
+@mixin transition($transition...) {
+  -webkit-transition: $transition;
+       -o-transition: $transition;
+          transition: $transition;
+}
+@mixin transition-property($transition-property...) {
+  -webkit-transition-property: $transition-property;
+          transition-property: $transition-property;
+}
+@mixin transition-delay($transition-delay) {
+  -webkit-transition-delay: $transition-delay;
+          transition-delay: $transition-delay;
+}
+@mixin transition-duration($transition-duration...) {
+  -webkit-transition-duration: $transition-duration;
+          transition-duration: $transition-duration;
+}
+@mixin transition-timing-function($timing-function) {
+  -webkit-transition-timing-function: $timing-function;
+          transition-timing-function: $timing-function;
+}
+@mixin transition-transform($transition...) {
+  -webkit-transition: -webkit-transform $transition;
+     -moz-transition: -moz-transform $transition;
+       -o-transition: -o-transform $transition;
+          transition: transform $transition;
+}
+
+
+// User select
+// For selecting text on the page
+
+@mixin user-select($select) {
+  -webkit-user-select: $select;
+     -moz-user-select: $select;
+      -ms-user-select: $select; // IE10+
+          user-select: $select;
+}
diff --git a/themes/bootstrap3/scss/vendor/font-awesome/_animated.scss b/themes/bootstrap3/scss/vendor/font-awesome/_animated.scss
new file mode 100644
index 0000000000000000000000000000000000000000..8a020dbfff7822bf57c7217eafdaa4884b8aa943
--- /dev/null
+++ b/themes/bootstrap3/scss/vendor/font-awesome/_animated.scss
@@ -0,0 +1,34 @@
+// Spinning Icons
+// --------------------------
+
+.#{$fa-css-prefix}-spin {
+  -webkit-animation: fa-spin 2s infinite linear;
+          animation: fa-spin 2s infinite linear;
+}
+
+.#{$fa-css-prefix}-pulse {
+  -webkit-animation: fa-spin 1s infinite steps(8);
+          animation: fa-spin 1s infinite steps(8);
+}
+
+@-webkit-keyframes fa-spin {
+  0% {
+    -webkit-transform: rotate(0deg);
+            transform: rotate(0deg);
+  }
+  100% {
+    -webkit-transform: rotate(359deg);
+            transform: rotate(359deg);
+  }
+}
+
+@keyframes fa-spin {
+  0% {
+    -webkit-transform: rotate(0deg);
+            transform: rotate(0deg);
+  }
+  100% {
+    -webkit-transform: rotate(359deg);
+            transform: rotate(359deg);
+  }
+}
diff --git a/themes/bootstrap3/scss/vendor/font-awesome/_bordered-pulled.scss b/themes/bootstrap3/scss/vendor/font-awesome/_bordered-pulled.scss
new file mode 100644
index 0000000000000000000000000000000000000000..d4b85a02f24adad8890ad69f7a1db6c7e3ec8a7d
--- /dev/null
+++ b/themes/bootstrap3/scss/vendor/font-awesome/_bordered-pulled.scss
@@ -0,0 +1,25 @@
+// Bordered & Pulled
+// -------------------------
+
+.#{$fa-css-prefix}-border {
+  padding: .2em .25em .15em;
+  border: solid .08em $fa-border-color;
+  border-radius: .1em;
+}
+
+.#{$fa-css-prefix}-pull-left { float: left; }
+.#{$fa-css-prefix}-pull-right { float: right; }
+
+.#{$fa-css-prefix} {
+  &.#{$fa-css-prefix}-pull-left { margin-right: .3em; }
+  &.#{$fa-css-prefix}-pull-right { margin-left: .3em; }
+}
+
+/* Deprecated as of 4.4.0 */
+.pull-right { float: right; }
+.pull-left { float: left; }
+
+.#{$fa-css-prefix} {
+  &.pull-left { margin-right: .3em; }
+  &.pull-right { margin-left: .3em; }
+}
diff --git a/themes/bootstrap3/scss/vendor/font-awesome/_core.scss b/themes/bootstrap3/scss/vendor/font-awesome/_core.scss
new file mode 100644
index 0000000000000000000000000000000000000000..7425ef85fc80ce6b035065906fc27490715e3733
--- /dev/null
+++ b/themes/bootstrap3/scss/vendor/font-awesome/_core.scss
@@ -0,0 +1,12 @@
+// Base Class Definition
+// -------------------------
+
+.#{$fa-css-prefix} {
+  display: inline-block;
+  font: normal normal normal #{$fa-font-size-base}/#{$fa-line-height-base} FontAwesome; // shortening font declaration
+  font-size: inherit; // can't have font-size inherit on line above, so need to override
+  text-rendering: auto; // optimizelegibility throws things off #1094
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+
+}
diff --git a/themes/bootstrap3/scss/vendor/font-awesome/_fixed-width.scss b/themes/bootstrap3/scss/vendor/font-awesome/_fixed-width.scss
new file mode 100644
index 0000000000000000000000000000000000000000..b221c98133a4d4a8449c848ccb69bf631d1c3e5d
--- /dev/null
+++ b/themes/bootstrap3/scss/vendor/font-awesome/_fixed-width.scss
@@ -0,0 +1,6 @@
+// Fixed Width Icons
+// -------------------------
+.#{$fa-css-prefix}-fw {
+  width: (18em / 14);
+  text-align: center;
+}
diff --git a/themes/bootstrap3/scss/vendor/font-awesome/_icons.scss b/themes/bootstrap3/scss/vendor/font-awesome/_icons.scss
new file mode 100644
index 0000000000000000000000000000000000000000..2944344350f3fab4741095812606ecb48be18fab
--- /dev/null
+++ b/themes/bootstrap3/scss/vendor/font-awesome/_icons.scss
@@ -0,0 +1,733 @@
+/* Font Awesome uses the Unicode Private Use Area (PUA) to ensure screen
+   readers do not read off random characters that represent icons */
+
+.#{$fa-css-prefix}-glass:before { content: $fa-var-glass; }
+.#{$fa-css-prefix}-music:before { content: $fa-var-music; }
+.#{$fa-css-prefix}-search:before { content: $fa-var-search; }
+.#{$fa-css-prefix}-envelope-o:before { content: $fa-var-envelope-o; }
+.#{$fa-css-prefix}-heart:before { content: $fa-var-heart; }
+.#{$fa-css-prefix}-star:before { content: $fa-var-star; }
+.#{$fa-css-prefix}-star-o:before { content: $fa-var-star-o; }
+.#{$fa-css-prefix}-user:before { content: $fa-var-user; }
+.#{$fa-css-prefix}-film:before { content: $fa-var-film; }
+.#{$fa-css-prefix}-th-large:before { content: $fa-var-th-large; }
+.#{$fa-css-prefix}-th:before { content: $fa-var-th; }
+.#{$fa-css-prefix}-th-list:before { content: $fa-var-th-list; }
+.#{$fa-css-prefix}-check:before { content: $fa-var-check; }
+.#{$fa-css-prefix}-remove:before,
+.#{$fa-css-prefix}-close:before,
+.#{$fa-css-prefix}-times:before { content: $fa-var-times; }
+.#{$fa-css-prefix}-search-plus:before { content: $fa-var-search-plus; }
+.#{$fa-css-prefix}-search-minus:before { content: $fa-var-search-minus; }
+.#{$fa-css-prefix}-power-off:before { content: $fa-var-power-off; }
+.#{$fa-css-prefix}-signal:before { content: $fa-var-signal; }
+.#{$fa-css-prefix}-gear:before,
+.#{$fa-css-prefix}-cog:before { content: $fa-var-cog; }
+.#{$fa-css-prefix}-trash-o:before { content: $fa-var-trash-o; }
+.#{$fa-css-prefix}-home:before { content: $fa-var-home; }
+.#{$fa-css-prefix}-file-o:before { content: $fa-var-file-o; }
+.#{$fa-css-prefix}-clock-o:before { content: $fa-var-clock-o; }
+.#{$fa-css-prefix}-road:before { content: $fa-var-road; }
+.#{$fa-css-prefix}-download:before { content: $fa-var-download; }
+.#{$fa-css-prefix}-arrow-circle-o-down:before { content: $fa-var-arrow-circle-o-down; }
+.#{$fa-css-prefix}-arrow-circle-o-up:before { content: $fa-var-arrow-circle-o-up; }
+.#{$fa-css-prefix}-inbox:before { content: $fa-var-inbox; }
+.#{$fa-css-prefix}-play-circle-o:before { content: $fa-var-play-circle-o; }
+.#{$fa-css-prefix}-rotate-right:before,
+.#{$fa-css-prefix}-repeat:before { content: $fa-var-repeat; }
+.#{$fa-css-prefix}-refresh:before { content: $fa-var-refresh; }
+.#{$fa-css-prefix}-list-alt:before { content: $fa-var-list-alt; }
+.#{$fa-css-prefix}-lock:before { content: $fa-var-lock; }
+.#{$fa-css-prefix}-flag:before { content: $fa-var-flag; }
+.#{$fa-css-prefix}-headphones:before { content: $fa-var-headphones; }
+.#{$fa-css-prefix}-volume-off:before { content: $fa-var-volume-off; }
+.#{$fa-css-prefix}-volume-down:before { content: $fa-var-volume-down; }
+.#{$fa-css-prefix}-volume-up:before { content: $fa-var-volume-up; }
+.#{$fa-css-prefix}-qrcode:before { content: $fa-var-qrcode; }
+.#{$fa-css-prefix}-barcode:before { content: $fa-var-barcode; }
+.#{$fa-css-prefix}-tag:before { content: $fa-var-tag; }
+.#{$fa-css-prefix}-tags:before { content: $fa-var-tags; }
+.#{$fa-css-prefix}-book:before { content: $fa-var-book; }
+.#{$fa-css-prefix}-bookmark:before { content: $fa-var-bookmark; }
+.#{$fa-css-prefix}-print:before { content: $fa-var-print; }
+.#{$fa-css-prefix}-camera:before { content: $fa-var-camera; }
+.#{$fa-css-prefix}-font:before { content: $fa-var-font; }
+.#{$fa-css-prefix}-bold:before { content: $fa-var-bold; }
+.#{$fa-css-prefix}-italic:before { content: $fa-var-italic; }
+.#{$fa-css-prefix}-text-height:before { content: $fa-var-text-height; }
+.#{$fa-css-prefix}-text-width:before { content: $fa-var-text-width; }
+.#{$fa-css-prefix}-align-left:before { content: $fa-var-align-left; }
+.#{$fa-css-prefix}-align-center:before { content: $fa-var-align-center; }
+.#{$fa-css-prefix}-align-right:before { content: $fa-var-align-right; }
+.#{$fa-css-prefix}-align-justify:before { content: $fa-var-align-justify; }
+.#{$fa-css-prefix}-list:before { content: $fa-var-list; }
+.#{$fa-css-prefix}-dedent:before,
+.#{$fa-css-prefix}-outdent:before { content: $fa-var-outdent; }
+.#{$fa-css-prefix}-indent:before { content: $fa-var-indent; }
+.#{$fa-css-prefix}-video-camera:before { content: $fa-var-video-camera; }
+.#{$fa-css-prefix}-photo:before,
+.#{$fa-css-prefix}-image:before,
+.#{$fa-css-prefix}-picture-o:before { content: $fa-var-picture-o; }
+.#{$fa-css-prefix}-pencil:before { content: $fa-var-pencil; }
+.#{$fa-css-prefix}-map-marker:before { content: $fa-var-map-marker; }
+.#{$fa-css-prefix}-adjust:before { content: $fa-var-adjust; }
+.#{$fa-css-prefix}-tint:before { content: $fa-var-tint; }
+.#{$fa-css-prefix}-edit:before,
+.#{$fa-css-prefix}-pencil-square-o:before { content: $fa-var-pencil-square-o; }
+.#{$fa-css-prefix}-share-square-o:before { content: $fa-var-share-square-o; }
+.#{$fa-css-prefix}-check-square-o:before { content: $fa-var-check-square-o; }
+.#{$fa-css-prefix}-arrows:before { content: $fa-var-arrows; }
+.#{$fa-css-prefix}-step-backward:before { content: $fa-var-step-backward; }
+.#{$fa-css-prefix}-fast-backward:before { content: $fa-var-fast-backward; }
+.#{$fa-css-prefix}-backward:before { content: $fa-var-backward; }
+.#{$fa-css-prefix}-play:before { content: $fa-var-play; }
+.#{$fa-css-prefix}-pause:before { content: $fa-var-pause; }
+.#{$fa-css-prefix}-stop:before { content: $fa-var-stop; }
+.#{$fa-css-prefix}-forward:before { content: $fa-var-forward; }
+.#{$fa-css-prefix}-fast-forward:before { content: $fa-var-fast-forward; }
+.#{$fa-css-prefix}-step-forward:before { content: $fa-var-step-forward; }
+.#{$fa-css-prefix}-eject:before { content: $fa-var-eject; }
+.#{$fa-css-prefix}-chevron-left:before { content: $fa-var-chevron-left; }
+.#{$fa-css-prefix}-chevron-right:before { content: $fa-var-chevron-right; }
+.#{$fa-css-prefix}-plus-circle:before { content: $fa-var-plus-circle; }
+.#{$fa-css-prefix}-minus-circle:before { content: $fa-var-minus-circle; }
+.#{$fa-css-prefix}-times-circle:before { content: $fa-var-times-circle; }
+.#{$fa-css-prefix}-check-circle:before { content: $fa-var-check-circle; }
+.#{$fa-css-prefix}-question-circle:before { content: $fa-var-question-circle; }
+.#{$fa-css-prefix}-info-circle:before { content: $fa-var-info-circle; }
+.#{$fa-css-prefix}-crosshairs:before { content: $fa-var-crosshairs; }
+.#{$fa-css-prefix}-times-circle-o:before { content: $fa-var-times-circle-o; }
+.#{$fa-css-prefix}-check-circle-o:before { content: $fa-var-check-circle-o; }
+.#{$fa-css-prefix}-ban:before { content: $fa-var-ban; }
+.#{$fa-css-prefix}-arrow-left:before { content: $fa-var-arrow-left; }
+.#{$fa-css-prefix}-arrow-right:before { content: $fa-var-arrow-right; }
+.#{$fa-css-prefix}-arrow-up:before { content: $fa-var-arrow-up; }
+.#{$fa-css-prefix}-arrow-down:before { content: $fa-var-arrow-down; }
+.#{$fa-css-prefix}-mail-forward:before,
+.#{$fa-css-prefix}-share:before { content: $fa-var-share; }
+.#{$fa-css-prefix}-expand:before { content: $fa-var-expand; }
+.#{$fa-css-prefix}-compress:before { content: $fa-var-compress; }
+.#{$fa-css-prefix}-plus:before { content: $fa-var-plus; }
+.#{$fa-css-prefix}-minus:before { content: $fa-var-minus; }
+.#{$fa-css-prefix}-asterisk:before { content: $fa-var-asterisk; }
+.#{$fa-css-prefix}-exclamation-circle:before { content: $fa-var-exclamation-circle; }
+.#{$fa-css-prefix}-gift:before { content: $fa-var-gift; }
+.#{$fa-css-prefix}-leaf:before { content: $fa-var-leaf; }
+.#{$fa-css-prefix}-fire:before { content: $fa-var-fire; }
+.#{$fa-css-prefix}-eye:before { content: $fa-var-eye; }
+.#{$fa-css-prefix}-eye-slash:before { content: $fa-var-eye-slash; }
+.#{$fa-css-prefix}-warning:before,
+.#{$fa-css-prefix}-exclamation-triangle:before { content: $fa-var-exclamation-triangle; }
+.#{$fa-css-prefix}-plane:before { content: $fa-var-plane; }
+.#{$fa-css-prefix}-calendar:before { content: $fa-var-calendar; }
+.#{$fa-css-prefix}-random:before { content: $fa-var-random; }
+.#{$fa-css-prefix}-comment:before { content: $fa-var-comment; }
+.#{$fa-css-prefix}-magnet:before { content: $fa-var-magnet; }
+.#{$fa-css-prefix}-chevron-up:before { content: $fa-var-chevron-up; }
+.#{$fa-css-prefix}-chevron-down:before { content: $fa-var-chevron-down; }
+.#{$fa-css-prefix}-retweet:before { content: $fa-var-retweet; }
+.#{$fa-css-prefix}-shopping-cart:before { content: $fa-var-shopping-cart; }
+.#{$fa-css-prefix}-folder:before { content: $fa-var-folder; }
+.#{$fa-css-prefix}-folder-open:before { content: $fa-var-folder-open; }
+.#{$fa-css-prefix}-arrows-v:before { content: $fa-var-arrows-v; }
+.#{$fa-css-prefix}-arrows-h:before { content: $fa-var-arrows-h; }
+.#{$fa-css-prefix}-bar-chart-o:before,
+.#{$fa-css-prefix}-bar-chart:before { content: $fa-var-bar-chart; }
+.#{$fa-css-prefix}-twitter-square:before { content: $fa-var-twitter-square; }
+.#{$fa-css-prefix}-facebook-square:before { content: $fa-var-facebook-square; }
+.#{$fa-css-prefix}-camera-retro:before { content: $fa-var-camera-retro; }
+.#{$fa-css-prefix}-key:before { content: $fa-var-key; }
+.#{$fa-css-prefix}-gears:before,
+.#{$fa-css-prefix}-cogs:before { content: $fa-var-cogs; }
+.#{$fa-css-prefix}-comments:before { content: $fa-var-comments; }
+.#{$fa-css-prefix}-thumbs-o-up:before { content: $fa-var-thumbs-o-up; }
+.#{$fa-css-prefix}-thumbs-o-down:before { content: $fa-var-thumbs-o-down; }
+.#{$fa-css-prefix}-star-half:before { content: $fa-var-star-half; }
+.#{$fa-css-prefix}-heart-o:before { content: $fa-var-heart-o; }
+.#{$fa-css-prefix}-sign-out:before { content: $fa-var-sign-out; }
+.#{$fa-css-prefix}-linkedin-square:before { content: $fa-var-linkedin-square; }
+.#{$fa-css-prefix}-thumb-tack:before { content: $fa-var-thumb-tack; }
+.#{$fa-css-prefix}-external-link:before { content: $fa-var-external-link; }
+.#{$fa-css-prefix}-sign-in:before { content: $fa-var-sign-in; }
+.#{$fa-css-prefix}-trophy:before { content: $fa-var-trophy; }
+.#{$fa-css-prefix}-github-square:before { content: $fa-var-github-square; }
+.#{$fa-css-prefix}-upload:before { content: $fa-var-upload; }
+.#{$fa-css-prefix}-lemon-o:before { content: $fa-var-lemon-o; }
+.#{$fa-css-prefix}-phone:before { content: $fa-var-phone; }
+.#{$fa-css-prefix}-square-o:before { content: $fa-var-square-o; }
+.#{$fa-css-prefix}-bookmark-o:before { content: $fa-var-bookmark-o; }
+.#{$fa-css-prefix}-phone-square:before { content: $fa-var-phone-square; }
+.#{$fa-css-prefix}-twitter:before { content: $fa-var-twitter; }
+.#{$fa-css-prefix}-facebook-f:before,
+.#{$fa-css-prefix}-facebook:before { content: $fa-var-facebook; }
+.#{$fa-css-prefix}-github:before { content: $fa-var-github; }
+.#{$fa-css-prefix}-unlock:before { content: $fa-var-unlock; }
+.#{$fa-css-prefix}-credit-card:before { content: $fa-var-credit-card; }
+.#{$fa-css-prefix}-feed:before,
+.#{$fa-css-prefix}-rss:before { content: $fa-var-rss; }
+.#{$fa-css-prefix}-hdd-o:before { content: $fa-var-hdd-o; }
+.#{$fa-css-prefix}-bullhorn:before { content: $fa-var-bullhorn; }
+.#{$fa-css-prefix}-bell:before { content: $fa-var-bell; }
+.#{$fa-css-prefix}-certificate:before { content: $fa-var-certificate; }
+.#{$fa-css-prefix}-hand-o-right:before { content: $fa-var-hand-o-right; }
+.#{$fa-css-prefix}-hand-o-left:before { content: $fa-var-hand-o-left; }
+.#{$fa-css-prefix}-hand-o-up:before { content: $fa-var-hand-o-up; }
+.#{$fa-css-prefix}-hand-o-down:before { content: $fa-var-hand-o-down; }
+.#{$fa-css-prefix}-arrow-circle-left:before { content: $fa-var-arrow-circle-left; }
+.#{$fa-css-prefix}-arrow-circle-right:before { content: $fa-var-arrow-circle-right; }
+.#{$fa-css-prefix}-arrow-circle-up:before { content: $fa-var-arrow-circle-up; }
+.#{$fa-css-prefix}-arrow-circle-down:before { content: $fa-var-arrow-circle-down; }
+.#{$fa-css-prefix}-globe:before { content: $fa-var-globe; }
+.#{$fa-css-prefix}-wrench:before { content: $fa-var-wrench; }
+.#{$fa-css-prefix}-tasks:before { content: $fa-var-tasks; }
+.#{$fa-css-prefix}-filter:before { content: $fa-var-filter; }
+.#{$fa-css-prefix}-briefcase:before { content: $fa-var-briefcase; }
+.#{$fa-css-prefix}-arrows-alt:before { content: $fa-var-arrows-alt; }
+.#{$fa-css-prefix}-group:before,
+.#{$fa-css-prefix}-users:before { content: $fa-var-users; }
+.#{$fa-css-prefix}-chain:before,
+.#{$fa-css-prefix}-link:before { content: $fa-var-link; }
+.#{$fa-css-prefix}-cloud:before { content: $fa-var-cloud; }
+.#{$fa-css-prefix}-flask:before { content: $fa-var-flask; }
+.#{$fa-css-prefix}-cut:before,
+.#{$fa-css-prefix}-scissors:before { content: $fa-var-scissors; }
+.#{$fa-css-prefix}-copy:before,
+.#{$fa-css-prefix}-files-o:before { content: $fa-var-files-o; }
+.#{$fa-css-prefix}-paperclip:before { content: $fa-var-paperclip; }
+.#{$fa-css-prefix}-save:before,
+.#{$fa-css-prefix}-floppy-o:before { content: $fa-var-floppy-o; }
+.#{$fa-css-prefix}-square:before { content: $fa-var-square; }
+.#{$fa-css-prefix}-navicon:before,
+.#{$fa-css-prefix}-reorder:before,
+.#{$fa-css-prefix}-bars:before { content: $fa-var-bars; }
+.#{$fa-css-prefix}-list-ul:before { content: $fa-var-list-ul; }
+.#{$fa-css-prefix}-list-ol:before { content: $fa-var-list-ol; }
+.#{$fa-css-prefix}-strikethrough:before { content: $fa-var-strikethrough; }
+.#{$fa-css-prefix}-underline:before { content: $fa-var-underline; }
+.#{$fa-css-prefix}-table:before { content: $fa-var-table; }
+.#{$fa-css-prefix}-magic:before { content: $fa-var-magic; }
+.#{$fa-css-prefix}-truck:before { content: $fa-var-truck; }
+.#{$fa-css-prefix}-pinterest:before { content: $fa-var-pinterest; }
+.#{$fa-css-prefix}-pinterest-square:before { content: $fa-var-pinterest-square; }
+.#{$fa-css-prefix}-google-plus-square:before { content: $fa-var-google-plus-square; }
+.#{$fa-css-prefix}-google-plus:before { content: $fa-var-google-plus; }
+.#{$fa-css-prefix}-money:before { content: $fa-var-money; }
+.#{$fa-css-prefix}-caret-down:before { content: $fa-var-caret-down; }
+.#{$fa-css-prefix}-caret-up:before { content: $fa-var-caret-up; }
+.#{$fa-css-prefix}-caret-left:before { content: $fa-var-caret-left; }
+.#{$fa-css-prefix}-caret-right:before { content: $fa-var-caret-right; }
+.#{$fa-css-prefix}-columns:before { content: $fa-var-columns; }
+.#{$fa-css-prefix}-unsorted:before,
+.#{$fa-css-prefix}-sort:before { content: $fa-var-sort; }
+.#{$fa-css-prefix}-sort-down:before,
+.#{$fa-css-prefix}-sort-desc:before { content: $fa-var-sort-desc; }
+.#{$fa-css-prefix}-sort-up:before,
+.#{$fa-css-prefix}-sort-asc:before { content: $fa-var-sort-asc; }
+.#{$fa-css-prefix}-envelope:before { content: $fa-var-envelope; }
+.#{$fa-css-prefix}-linkedin:before { content: $fa-var-linkedin; }
+.#{$fa-css-prefix}-rotate-left:before,
+.#{$fa-css-prefix}-undo:before { content: $fa-var-undo; }
+.#{$fa-css-prefix}-legal:before,
+.#{$fa-css-prefix}-gavel:before { content: $fa-var-gavel; }
+.#{$fa-css-prefix}-dashboard:before,
+.#{$fa-css-prefix}-tachometer:before { content: $fa-var-tachometer; }
+.#{$fa-css-prefix}-comment-o:before { content: $fa-var-comment-o; }
+.#{$fa-css-prefix}-comments-o:before { content: $fa-var-comments-o; }
+.#{$fa-css-prefix}-flash:before,
+.#{$fa-css-prefix}-bolt:before { content: $fa-var-bolt; }
+.#{$fa-css-prefix}-sitemap:before { content: $fa-var-sitemap; }
+.#{$fa-css-prefix}-umbrella:before { content: $fa-var-umbrella; }
+.#{$fa-css-prefix}-paste:before,
+.#{$fa-css-prefix}-clipboard:before { content: $fa-var-clipboard; }
+.#{$fa-css-prefix}-lightbulb-o:before { content: $fa-var-lightbulb-o; }
+.#{$fa-css-prefix}-exchange:before { content: $fa-var-exchange; }
+.#{$fa-css-prefix}-cloud-download:before { content: $fa-var-cloud-download; }
+.#{$fa-css-prefix}-cloud-upload:before { content: $fa-var-cloud-upload; }
+.#{$fa-css-prefix}-user-md:before { content: $fa-var-user-md; }
+.#{$fa-css-prefix}-stethoscope:before { content: $fa-var-stethoscope; }
+.#{$fa-css-prefix}-suitcase:before { content: $fa-var-suitcase; }
+.#{$fa-css-prefix}-bell-o:before { content: $fa-var-bell-o; }
+.#{$fa-css-prefix}-coffee:before { content: $fa-var-coffee; }
+.#{$fa-css-prefix}-cutlery:before { content: $fa-var-cutlery; }
+.#{$fa-css-prefix}-file-text-o:before { content: $fa-var-file-text-o; }
+.#{$fa-css-prefix}-building-o:before { content: $fa-var-building-o; }
+.#{$fa-css-prefix}-hospital-o:before { content: $fa-var-hospital-o; }
+.#{$fa-css-prefix}-ambulance:before { content: $fa-var-ambulance; }
+.#{$fa-css-prefix}-medkit:before { content: $fa-var-medkit; }
+.#{$fa-css-prefix}-fighter-jet:before { content: $fa-var-fighter-jet; }
+.#{$fa-css-prefix}-beer:before { content: $fa-var-beer; }
+.#{$fa-css-prefix}-h-square:before { content: $fa-var-h-square; }
+.#{$fa-css-prefix}-plus-square:before { content: $fa-var-plus-square; }
+.#{$fa-css-prefix}-angle-double-left:before { content: $fa-var-angle-double-left; }
+.#{$fa-css-prefix}-angle-double-right:before { content: $fa-var-angle-double-right; }
+.#{$fa-css-prefix}-angle-double-up:before { content: $fa-var-angle-double-up; }
+.#{$fa-css-prefix}-angle-double-down:before { content: $fa-var-angle-double-down; }
+.#{$fa-css-prefix}-angle-left:before { content: $fa-var-angle-left; }
+.#{$fa-css-prefix}-angle-right:before { content: $fa-var-angle-right; }
+.#{$fa-css-prefix}-angle-up:before { content: $fa-var-angle-up; }
+.#{$fa-css-prefix}-angle-down:before { content: $fa-var-angle-down; }
+.#{$fa-css-prefix}-desktop:before { content: $fa-var-desktop; }
+.#{$fa-css-prefix}-laptop:before { content: $fa-var-laptop; }
+.#{$fa-css-prefix}-tablet:before { content: $fa-var-tablet; }
+.#{$fa-css-prefix}-mobile-phone:before,
+.#{$fa-css-prefix}-mobile:before { content: $fa-var-mobile; }
+.#{$fa-css-prefix}-circle-o:before { content: $fa-var-circle-o; }
+.#{$fa-css-prefix}-quote-left:before { content: $fa-var-quote-left; }
+.#{$fa-css-prefix}-quote-right:before { content: $fa-var-quote-right; }
+.#{$fa-css-prefix}-spinner:before { content: $fa-var-spinner; }
+.#{$fa-css-prefix}-circle:before { content: $fa-var-circle; }
+.#{$fa-css-prefix}-mail-reply:before,
+.#{$fa-css-prefix}-reply:before { content: $fa-var-reply; }
+.#{$fa-css-prefix}-github-alt:before { content: $fa-var-github-alt; }
+.#{$fa-css-prefix}-folder-o:before { content: $fa-var-folder-o; }
+.#{$fa-css-prefix}-folder-open-o:before { content: $fa-var-folder-open-o; }
+.#{$fa-css-prefix}-smile-o:before { content: $fa-var-smile-o; }
+.#{$fa-css-prefix}-frown-o:before { content: $fa-var-frown-o; }
+.#{$fa-css-prefix}-meh-o:before { content: $fa-var-meh-o; }
+.#{$fa-css-prefix}-gamepad:before { content: $fa-var-gamepad; }
+.#{$fa-css-prefix}-keyboard-o:before { content: $fa-var-keyboard-o; }
+.#{$fa-css-prefix}-flag-o:before { content: $fa-var-flag-o; }
+.#{$fa-css-prefix}-flag-checkered:before { content: $fa-var-flag-checkered; }
+.#{$fa-css-prefix}-terminal:before { content: $fa-var-terminal; }
+.#{$fa-css-prefix}-code:before { content: $fa-var-code; }
+.#{$fa-css-prefix}-mail-reply-all:before,
+.#{$fa-css-prefix}-reply-all:before { content: $fa-var-reply-all; }
+.#{$fa-css-prefix}-star-half-empty:before,
+.#{$fa-css-prefix}-star-half-full:before,
+.#{$fa-css-prefix}-star-half-o:before { content: $fa-var-star-half-o; }
+.#{$fa-css-prefix}-location-arrow:before { content: $fa-var-location-arrow; }
+.#{$fa-css-prefix}-crop:before { content: $fa-var-crop; }
+.#{$fa-css-prefix}-code-fork:before { content: $fa-var-code-fork; }
+.#{$fa-css-prefix}-unlink:before,
+.#{$fa-css-prefix}-chain-broken:before { content: $fa-var-chain-broken; }
+.#{$fa-css-prefix}-question:before { content: $fa-var-question; }
+.#{$fa-css-prefix}-info:before { content: $fa-var-info; }
+.#{$fa-css-prefix}-exclamation:before { content: $fa-var-exclamation; }
+.#{$fa-css-prefix}-superscript:before { content: $fa-var-superscript; }
+.#{$fa-css-prefix}-subscript:before { content: $fa-var-subscript; }
+.#{$fa-css-prefix}-eraser:before { content: $fa-var-eraser; }
+.#{$fa-css-prefix}-puzzle-piece:before { content: $fa-var-puzzle-piece; }
+.#{$fa-css-prefix}-microphone:before { content: $fa-var-microphone; }
+.#{$fa-css-prefix}-microphone-slash:before { content: $fa-var-microphone-slash; }
+.#{$fa-css-prefix}-shield:before { content: $fa-var-shield; }
+.#{$fa-css-prefix}-calendar-o:before { content: $fa-var-calendar-o; }
+.#{$fa-css-prefix}-fire-extinguisher:before { content: $fa-var-fire-extinguisher; }
+.#{$fa-css-prefix}-rocket:before { content: $fa-var-rocket; }
+.#{$fa-css-prefix}-maxcdn:before { content: $fa-var-maxcdn; }
+.#{$fa-css-prefix}-chevron-circle-left:before { content: $fa-var-chevron-circle-left; }
+.#{$fa-css-prefix}-chevron-circle-right:before { content: $fa-var-chevron-circle-right; }
+.#{$fa-css-prefix}-chevron-circle-up:before { content: $fa-var-chevron-circle-up; }
+.#{$fa-css-prefix}-chevron-circle-down:before { content: $fa-var-chevron-circle-down; }
+.#{$fa-css-prefix}-html5:before { content: $fa-var-html5; }
+.#{$fa-css-prefix}-css3:before { content: $fa-var-css3; }
+.#{$fa-css-prefix}-anchor:before { content: $fa-var-anchor; }
+.#{$fa-css-prefix}-unlock-alt:before { content: $fa-var-unlock-alt; }
+.#{$fa-css-prefix}-bullseye:before { content: $fa-var-bullseye; }
+.#{$fa-css-prefix}-ellipsis-h:before { content: $fa-var-ellipsis-h; }
+.#{$fa-css-prefix}-ellipsis-v:before { content: $fa-var-ellipsis-v; }
+.#{$fa-css-prefix}-rss-square:before { content: $fa-var-rss-square; }
+.#{$fa-css-prefix}-play-circle:before { content: $fa-var-play-circle; }
+.#{$fa-css-prefix}-ticket:before { content: $fa-var-ticket; }
+.#{$fa-css-prefix}-minus-square:before { content: $fa-var-minus-square; }
+.#{$fa-css-prefix}-minus-square-o:before { content: $fa-var-minus-square-o; }
+.#{$fa-css-prefix}-level-up:before { content: $fa-var-level-up; }
+.#{$fa-css-prefix}-level-down:before { content: $fa-var-level-down; }
+.#{$fa-css-prefix}-check-square:before { content: $fa-var-check-square; }
+.#{$fa-css-prefix}-pencil-square:before { content: $fa-var-pencil-square; }
+.#{$fa-css-prefix}-external-link-square:before { content: $fa-var-external-link-square; }
+.#{$fa-css-prefix}-share-square:before { content: $fa-var-share-square; }
+.#{$fa-css-prefix}-compass:before { content: $fa-var-compass; }
+.#{$fa-css-prefix}-toggle-down:before,
+.#{$fa-css-prefix}-caret-square-o-down:before { content: $fa-var-caret-square-o-down; }
+.#{$fa-css-prefix}-toggle-up:before,
+.#{$fa-css-prefix}-caret-square-o-up:before { content: $fa-var-caret-square-o-up; }
+.#{$fa-css-prefix}-toggle-right:before,
+.#{$fa-css-prefix}-caret-square-o-right:before { content: $fa-var-caret-square-o-right; }
+.#{$fa-css-prefix}-euro:before,
+.#{$fa-css-prefix}-eur:before { content: $fa-var-eur; }
+.#{$fa-css-prefix}-gbp:before { content: $fa-var-gbp; }
+.#{$fa-css-prefix}-dollar:before,
+.#{$fa-css-prefix}-usd:before { content: $fa-var-usd; }
+.#{$fa-css-prefix}-rupee:before,
+.#{$fa-css-prefix}-inr:before { content: $fa-var-inr; }
+.#{$fa-css-prefix}-cny:before,
+.#{$fa-css-prefix}-rmb:before,
+.#{$fa-css-prefix}-yen:before,
+.#{$fa-css-prefix}-jpy:before { content: $fa-var-jpy; }
+.#{$fa-css-prefix}-ruble:before,
+.#{$fa-css-prefix}-rouble:before,
+.#{$fa-css-prefix}-rub:before { content: $fa-var-rub; }
+.#{$fa-css-prefix}-won:before,
+.#{$fa-css-prefix}-krw:before { content: $fa-var-krw; }
+.#{$fa-css-prefix}-bitcoin:before,
+.#{$fa-css-prefix}-btc:before { content: $fa-var-btc; }
+.#{$fa-css-prefix}-file:before { content: $fa-var-file; }
+.#{$fa-css-prefix}-file-text:before { content: $fa-var-file-text; }
+.#{$fa-css-prefix}-sort-alpha-asc:before { content: $fa-var-sort-alpha-asc; }
+.#{$fa-css-prefix}-sort-alpha-desc:before { content: $fa-var-sort-alpha-desc; }
+.#{$fa-css-prefix}-sort-amount-asc:before { content: $fa-var-sort-amount-asc; }
+.#{$fa-css-prefix}-sort-amount-desc:before { content: $fa-var-sort-amount-desc; }
+.#{$fa-css-prefix}-sort-numeric-asc:before { content: $fa-var-sort-numeric-asc; }
+.#{$fa-css-prefix}-sort-numeric-desc:before { content: $fa-var-sort-numeric-desc; }
+.#{$fa-css-prefix}-thumbs-up:before { content: $fa-var-thumbs-up; }
+.#{$fa-css-prefix}-thumbs-down:before { content: $fa-var-thumbs-down; }
+.#{$fa-css-prefix}-youtube-square:before { content: $fa-var-youtube-square; }
+.#{$fa-css-prefix}-youtube:before { content: $fa-var-youtube; }
+.#{$fa-css-prefix}-xing:before { content: $fa-var-xing; }
+.#{$fa-css-prefix}-xing-square:before { content: $fa-var-xing-square; }
+.#{$fa-css-prefix}-youtube-play:before { content: $fa-var-youtube-play; }
+.#{$fa-css-prefix}-dropbox:before { content: $fa-var-dropbox; }
+.#{$fa-css-prefix}-stack-overflow:before { content: $fa-var-stack-overflow; }
+.#{$fa-css-prefix}-instagram:before { content: $fa-var-instagram; }
+.#{$fa-css-prefix}-flickr:before { content: $fa-var-flickr; }
+.#{$fa-css-prefix}-adn:before { content: $fa-var-adn; }
+.#{$fa-css-prefix}-bitbucket:before { content: $fa-var-bitbucket; }
+.#{$fa-css-prefix}-bitbucket-square:before { content: $fa-var-bitbucket-square; }
+.#{$fa-css-prefix}-tumblr:before { content: $fa-var-tumblr; }
+.#{$fa-css-prefix}-tumblr-square:before { content: $fa-var-tumblr-square; }
+.#{$fa-css-prefix}-long-arrow-down:before { content: $fa-var-long-arrow-down; }
+.#{$fa-css-prefix}-long-arrow-up:before { content: $fa-var-long-arrow-up; }
+.#{$fa-css-prefix}-long-arrow-left:before { content: $fa-var-long-arrow-left; }
+.#{$fa-css-prefix}-long-arrow-right:before { content: $fa-var-long-arrow-right; }
+.#{$fa-css-prefix}-apple:before { content: $fa-var-apple; }
+.#{$fa-css-prefix}-windows:before { content: $fa-var-windows; }
+.#{$fa-css-prefix}-android:before { content: $fa-var-android; }
+.#{$fa-css-prefix}-linux:before { content: $fa-var-linux; }
+.#{$fa-css-prefix}-dribbble:before { content: $fa-var-dribbble; }
+.#{$fa-css-prefix}-skype:before { content: $fa-var-skype; }
+.#{$fa-css-prefix}-foursquare:before { content: $fa-var-foursquare; }
+.#{$fa-css-prefix}-trello:before { content: $fa-var-trello; }
+.#{$fa-css-prefix}-female:before { content: $fa-var-female; }
+.#{$fa-css-prefix}-male:before { content: $fa-var-male; }
+.#{$fa-css-prefix}-gittip:before,
+.#{$fa-css-prefix}-gratipay:before { content: $fa-var-gratipay; }
+.#{$fa-css-prefix}-sun-o:before { content: $fa-var-sun-o; }
+.#{$fa-css-prefix}-moon-o:before { content: $fa-var-moon-o; }
+.#{$fa-css-prefix}-archive:before { content: $fa-var-archive; }
+.#{$fa-css-prefix}-bug:before { content: $fa-var-bug; }
+.#{$fa-css-prefix}-vk:before { content: $fa-var-vk; }
+.#{$fa-css-prefix}-weibo:before { content: $fa-var-weibo; }
+.#{$fa-css-prefix}-renren:before { content: $fa-var-renren; }
+.#{$fa-css-prefix}-pagelines:before { content: $fa-var-pagelines; }
+.#{$fa-css-prefix}-stack-exchange:before { content: $fa-var-stack-exchange; }
+.#{$fa-css-prefix}-arrow-circle-o-right:before { content: $fa-var-arrow-circle-o-right; }
+.#{$fa-css-prefix}-arrow-circle-o-left:before { content: $fa-var-arrow-circle-o-left; }
+.#{$fa-css-prefix}-toggle-left:before,
+.#{$fa-css-prefix}-caret-square-o-left:before { content: $fa-var-caret-square-o-left; }
+.#{$fa-css-prefix}-dot-circle-o:before { content: $fa-var-dot-circle-o; }
+.#{$fa-css-prefix}-wheelchair:before { content: $fa-var-wheelchair; }
+.#{$fa-css-prefix}-vimeo-square:before { content: $fa-var-vimeo-square; }
+.#{$fa-css-prefix}-turkish-lira:before,
+.#{$fa-css-prefix}-try:before { content: $fa-var-try; }
+.#{$fa-css-prefix}-plus-square-o:before { content: $fa-var-plus-square-o; }
+.#{$fa-css-prefix}-space-shuttle:before { content: $fa-var-space-shuttle; }
+.#{$fa-css-prefix}-slack:before { content: $fa-var-slack; }
+.#{$fa-css-prefix}-envelope-square:before { content: $fa-var-envelope-square; }
+.#{$fa-css-prefix}-wordpress:before { content: $fa-var-wordpress; }
+.#{$fa-css-prefix}-openid:before { content: $fa-var-openid; }
+.#{$fa-css-prefix}-institution:before,
+.#{$fa-css-prefix}-bank:before,
+.#{$fa-css-prefix}-university:before { content: $fa-var-university; }
+.#{$fa-css-prefix}-mortar-board:before,
+.#{$fa-css-prefix}-graduation-cap:before { content: $fa-var-graduation-cap; }
+.#{$fa-css-prefix}-yahoo:before { content: $fa-var-yahoo; }
+.#{$fa-css-prefix}-google:before { content: $fa-var-google; }
+.#{$fa-css-prefix}-reddit:before { content: $fa-var-reddit; }
+.#{$fa-css-prefix}-reddit-square:before { content: $fa-var-reddit-square; }
+.#{$fa-css-prefix}-stumbleupon-circle:before { content: $fa-var-stumbleupon-circle; }
+.#{$fa-css-prefix}-stumbleupon:before { content: $fa-var-stumbleupon; }
+.#{$fa-css-prefix}-delicious:before { content: $fa-var-delicious; }
+.#{$fa-css-prefix}-digg:before { content: $fa-var-digg; }
+.#{$fa-css-prefix}-pied-piper-pp:before { content: $fa-var-pied-piper-pp; }
+.#{$fa-css-prefix}-pied-piper-alt:before { content: $fa-var-pied-piper-alt; }
+.#{$fa-css-prefix}-drupal:before { content: $fa-var-drupal; }
+.#{$fa-css-prefix}-joomla:before { content: $fa-var-joomla; }
+.#{$fa-css-prefix}-language:before { content: $fa-var-language; }
+.#{$fa-css-prefix}-fax:before { content: $fa-var-fax; }
+.#{$fa-css-prefix}-building:before { content: $fa-var-building; }
+.#{$fa-css-prefix}-child:before { content: $fa-var-child; }
+.#{$fa-css-prefix}-paw:before { content: $fa-var-paw; }
+.#{$fa-css-prefix}-spoon:before { content: $fa-var-spoon; }
+.#{$fa-css-prefix}-cube:before { content: $fa-var-cube; }
+.#{$fa-css-prefix}-cubes:before { content: $fa-var-cubes; }
+.#{$fa-css-prefix}-behance:before { content: $fa-var-behance; }
+.#{$fa-css-prefix}-behance-square:before { content: $fa-var-behance-square; }
+.#{$fa-css-prefix}-steam:before { content: $fa-var-steam; }
+.#{$fa-css-prefix}-steam-square:before { content: $fa-var-steam-square; }
+.#{$fa-css-prefix}-recycle:before { content: $fa-var-recycle; }
+.#{$fa-css-prefix}-automobile:before,
+.#{$fa-css-prefix}-car:before { content: $fa-var-car; }
+.#{$fa-css-prefix}-cab:before,
+.#{$fa-css-prefix}-taxi:before { content: $fa-var-taxi; }
+.#{$fa-css-prefix}-tree:before { content: $fa-var-tree; }
+.#{$fa-css-prefix}-spotify:before { content: $fa-var-spotify; }
+.#{$fa-css-prefix}-deviantart:before { content: $fa-var-deviantart; }
+.#{$fa-css-prefix}-soundcloud:before { content: $fa-var-soundcloud; }
+.#{$fa-css-prefix}-database:before { content: $fa-var-database; }
+.#{$fa-css-prefix}-file-pdf-o:before { content: $fa-var-file-pdf-o; }
+.#{$fa-css-prefix}-file-word-o:before { content: $fa-var-file-word-o; }
+.#{$fa-css-prefix}-file-excel-o:before { content: $fa-var-file-excel-o; }
+.#{$fa-css-prefix}-file-powerpoint-o:before { content: $fa-var-file-powerpoint-o; }
+.#{$fa-css-prefix}-file-photo-o:before,
+.#{$fa-css-prefix}-file-picture-o:before,
+.#{$fa-css-prefix}-file-image-o:before { content: $fa-var-file-image-o; }
+.#{$fa-css-prefix}-file-zip-o:before,
+.#{$fa-css-prefix}-file-archive-o:before { content: $fa-var-file-archive-o; }
+.#{$fa-css-prefix}-file-sound-o:before,
+.#{$fa-css-prefix}-file-audio-o:before { content: $fa-var-file-audio-o; }
+.#{$fa-css-prefix}-file-movie-o:before,
+.#{$fa-css-prefix}-file-video-o:before { content: $fa-var-file-video-o; }
+.#{$fa-css-prefix}-file-code-o:before { content: $fa-var-file-code-o; }
+.#{$fa-css-prefix}-vine:before { content: $fa-var-vine; }
+.#{$fa-css-prefix}-codepen:before { content: $fa-var-codepen; }
+.#{$fa-css-prefix}-jsfiddle:before { content: $fa-var-jsfiddle; }
+.#{$fa-css-prefix}-life-bouy:before,
+.#{$fa-css-prefix}-life-buoy:before,
+.#{$fa-css-prefix}-life-saver:before,
+.#{$fa-css-prefix}-support:before,
+.#{$fa-css-prefix}-life-ring:before { content: $fa-var-life-ring; }
+.#{$fa-css-prefix}-circle-o-notch:before { content: $fa-var-circle-o-notch; }
+.#{$fa-css-prefix}-ra:before,
+.#{$fa-css-prefix}-resistance:before,
+.#{$fa-css-prefix}-rebel:before { content: $fa-var-rebel; }
+.#{$fa-css-prefix}-ge:before,
+.#{$fa-css-prefix}-empire:before { content: $fa-var-empire; }
+.#{$fa-css-prefix}-git-square:before { content: $fa-var-git-square; }
+.#{$fa-css-prefix}-git:before { content: $fa-var-git; }
+.#{$fa-css-prefix}-y-combinator-square:before,
+.#{$fa-css-prefix}-yc-square:before,
+.#{$fa-css-prefix}-hacker-news:before { content: $fa-var-hacker-news; }
+.#{$fa-css-prefix}-tencent-weibo:before { content: $fa-var-tencent-weibo; }
+.#{$fa-css-prefix}-qq:before { content: $fa-var-qq; }
+.#{$fa-css-prefix}-wechat:before,
+.#{$fa-css-prefix}-weixin:before { content: $fa-var-weixin; }
+.#{$fa-css-prefix}-send:before,
+.#{$fa-css-prefix}-paper-plane:before { content: $fa-var-paper-plane; }
+.#{$fa-css-prefix}-send-o:before,
+.#{$fa-css-prefix}-paper-plane-o:before { content: $fa-var-paper-plane-o; }
+.#{$fa-css-prefix}-history:before { content: $fa-var-history; }
+.#{$fa-css-prefix}-circle-thin:before { content: $fa-var-circle-thin; }
+.#{$fa-css-prefix}-header:before { content: $fa-var-header; }
+.#{$fa-css-prefix}-paragraph:before { content: $fa-var-paragraph; }
+.#{$fa-css-prefix}-sliders:before { content: $fa-var-sliders; }
+.#{$fa-css-prefix}-share-alt:before { content: $fa-var-share-alt; }
+.#{$fa-css-prefix}-share-alt-square:before { content: $fa-var-share-alt-square; }
+.#{$fa-css-prefix}-bomb:before { content: $fa-var-bomb; }
+.#{$fa-css-prefix}-soccer-ball-o:before,
+.#{$fa-css-prefix}-futbol-o:before { content: $fa-var-futbol-o; }
+.#{$fa-css-prefix}-tty:before { content: $fa-var-tty; }
+.#{$fa-css-prefix}-binoculars:before { content: $fa-var-binoculars; }
+.#{$fa-css-prefix}-plug:before { content: $fa-var-plug; }
+.#{$fa-css-prefix}-slideshare:before { content: $fa-var-slideshare; }
+.#{$fa-css-prefix}-twitch:before { content: $fa-var-twitch; }
+.#{$fa-css-prefix}-yelp:before { content: $fa-var-yelp; }
+.#{$fa-css-prefix}-newspaper-o:before { content: $fa-var-newspaper-o; }
+.#{$fa-css-prefix}-wifi:before { content: $fa-var-wifi; }
+.#{$fa-css-prefix}-calculator:before { content: $fa-var-calculator; }
+.#{$fa-css-prefix}-paypal:before { content: $fa-var-paypal; }
+.#{$fa-css-prefix}-google-wallet:before { content: $fa-var-google-wallet; }
+.#{$fa-css-prefix}-cc-visa:before { content: $fa-var-cc-visa; }
+.#{$fa-css-prefix}-cc-mastercard:before { content: $fa-var-cc-mastercard; }
+.#{$fa-css-prefix}-cc-discover:before { content: $fa-var-cc-discover; }
+.#{$fa-css-prefix}-cc-amex:before { content: $fa-var-cc-amex; }
+.#{$fa-css-prefix}-cc-paypal:before { content: $fa-var-cc-paypal; }
+.#{$fa-css-prefix}-cc-stripe:before { content: $fa-var-cc-stripe; }
+.#{$fa-css-prefix}-bell-slash:before { content: $fa-var-bell-slash; }
+.#{$fa-css-prefix}-bell-slash-o:before { content: $fa-var-bell-slash-o; }
+.#{$fa-css-prefix}-trash:before { content: $fa-var-trash; }
+.#{$fa-css-prefix}-copyright:before { content: $fa-var-copyright; }
+.#{$fa-css-prefix}-at:before { content: $fa-var-at; }
+.#{$fa-css-prefix}-eyedropper:before { content: $fa-var-eyedropper; }
+.#{$fa-css-prefix}-paint-brush:before { content: $fa-var-paint-brush; }
+.#{$fa-css-prefix}-birthday-cake:before { content: $fa-var-birthday-cake; }
+.#{$fa-css-prefix}-area-chart:before { content: $fa-var-area-chart; }
+.#{$fa-css-prefix}-pie-chart:before { content: $fa-var-pie-chart; }
+.#{$fa-css-prefix}-line-chart:before { content: $fa-var-line-chart; }
+.#{$fa-css-prefix}-lastfm:before { content: $fa-var-lastfm; }
+.#{$fa-css-prefix}-lastfm-square:before { content: $fa-var-lastfm-square; }
+.#{$fa-css-prefix}-toggle-off:before { content: $fa-var-toggle-off; }
+.#{$fa-css-prefix}-toggle-on:before { content: $fa-var-toggle-on; }
+.#{$fa-css-prefix}-bicycle:before { content: $fa-var-bicycle; }
+.#{$fa-css-prefix}-bus:before { content: $fa-var-bus; }
+.#{$fa-css-prefix}-ioxhost:before { content: $fa-var-ioxhost; }
+.#{$fa-css-prefix}-angellist:before { content: $fa-var-angellist; }
+.#{$fa-css-prefix}-cc:before { content: $fa-var-cc; }
+.#{$fa-css-prefix}-shekel:before,
+.#{$fa-css-prefix}-sheqel:before,
+.#{$fa-css-prefix}-ils:before { content: $fa-var-ils; }
+.#{$fa-css-prefix}-meanpath:before { content: $fa-var-meanpath; }
+.#{$fa-css-prefix}-buysellads:before { content: $fa-var-buysellads; }
+.#{$fa-css-prefix}-connectdevelop:before { content: $fa-var-connectdevelop; }
+.#{$fa-css-prefix}-dashcube:before { content: $fa-var-dashcube; }
+.#{$fa-css-prefix}-forumbee:before { content: $fa-var-forumbee; }
+.#{$fa-css-prefix}-leanpub:before { content: $fa-var-leanpub; }
+.#{$fa-css-prefix}-sellsy:before { content: $fa-var-sellsy; }
+.#{$fa-css-prefix}-shirtsinbulk:before { content: $fa-var-shirtsinbulk; }
+.#{$fa-css-prefix}-simplybuilt:before { content: $fa-var-simplybuilt; }
+.#{$fa-css-prefix}-skyatlas:before { content: $fa-var-skyatlas; }
+.#{$fa-css-prefix}-cart-plus:before { content: $fa-var-cart-plus; }
+.#{$fa-css-prefix}-cart-arrow-down:before { content: $fa-var-cart-arrow-down; }
+.#{$fa-css-prefix}-diamond:before { content: $fa-var-diamond; }
+.#{$fa-css-prefix}-ship:before { content: $fa-var-ship; }
+.#{$fa-css-prefix}-user-secret:before { content: $fa-var-user-secret; }
+.#{$fa-css-prefix}-motorcycle:before { content: $fa-var-motorcycle; }
+.#{$fa-css-prefix}-street-view:before { content: $fa-var-street-view; }
+.#{$fa-css-prefix}-heartbeat:before { content: $fa-var-heartbeat; }
+.#{$fa-css-prefix}-venus:before { content: $fa-var-venus; }
+.#{$fa-css-prefix}-mars:before { content: $fa-var-mars; }
+.#{$fa-css-prefix}-mercury:before { content: $fa-var-mercury; }
+.#{$fa-css-prefix}-intersex:before,
+.#{$fa-css-prefix}-transgender:before { content: $fa-var-transgender; }
+.#{$fa-css-prefix}-transgender-alt:before { content: $fa-var-transgender-alt; }
+.#{$fa-css-prefix}-venus-double:before { content: $fa-var-venus-double; }
+.#{$fa-css-prefix}-mars-double:before { content: $fa-var-mars-double; }
+.#{$fa-css-prefix}-venus-mars:before { content: $fa-var-venus-mars; }
+.#{$fa-css-prefix}-mars-stroke:before { content: $fa-var-mars-stroke; }
+.#{$fa-css-prefix}-mars-stroke-v:before { content: $fa-var-mars-stroke-v; }
+.#{$fa-css-prefix}-mars-stroke-h:before { content: $fa-var-mars-stroke-h; }
+.#{$fa-css-prefix}-neuter:before { content: $fa-var-neuter; }
+.#{$fa-css-prefix}-genderless:before { content: $fa-var-genderless; }
+.#{$fa-css-prefix}-facebook-official:before { content: $fa-var-facebook-official; }
+.#{$fa-css-prefix}-pinterest-p:before { content: $fa-var-pinterest-p; }
+.#{$fa-css-prefix}-whatsapp:before { content: $fa-var-whatsapp; }
+.#{$fa-css-prefix}-server:before { content: $fa-var-server; }
+.#{$fa-css-prefix}-user-plus:before { content: $fa-var-user-plus; }
+.#{$fa-css-prefix}-user-times:before { content: $fa-var-user-times; }
+.#{$fa-css-prefix}-hotel:before,
+.#{$fa-css-prefix}-bed:before { content: $fa-var-bed; }
+.#{$fa-css-prefix}-viacoin:before { content: $fa-var-viacoin; }
+.#{$fa-css-prefix}-train:before { content: $fa-var-train; }
+.#{$fa-css-prefix}-subway:before { content: $fa-var-subway; }
+.#{$fa-css-prefix}-medium:before { content: $fa-var-medium; }
+.#{$fa-css-prefix}-yc:before,
+.#{$fa-css-prefix}-y-combinator:before { content: $fa-var-y-combinator; }
+.#{$fa-css-prefix}-optin-monster:before { content: $fa-var-optin-monster; }
+.#{$fa-css-prefix}-opencart:before { content: $fa-var-opencart; }
+.#{$fa-css-prefix}-expeditedssl:before { content: $fa-var-expeditedssl; }
+.#{$fa-css-prefix}-battery-4:before,
+.#{$fa-css-prefix}-battery-full:before { content: $fa-var-battery-full; }
+.#{$fa-css-prefix}-battery-3:before,
+.#{$fa-css-prefix}-battery-three-quarters:before { content: $fa-var-battery-three-quarters; }
+.#{$fa-css-prefix}-battery-2:before,
+.#{$fa-css-prefix}-battery-half:before { content: $fa-var-battery-half; }
+.#{$fa-css-prefix}-battery-1:before,
+.#{$fa-css-prefix}-battery-quarter:before { content: $fa-var-battery-quarter; }
+.#{$fa-css-prefix}-battery-0:before,
+.#{$fa-css-prefix}-battery-empty:before { content: $fa-var-battery-empty; }
+.#{$fa-css-prefix}-mouse-pointer:before { content: $fa-var-mouse-pointer; }
+.#{$fa-css-prefix}-i-cursor:before { content: $fa-var-i-cursor; }
+.#{$fa-css-prefix}-object-group:before { content: $fa-var-object-group; }
+.#{$fa-css-prefix}-object-ungroup:before { content: $fa-var-object-ungroup; }
+.#{$fa-css-prefix}-sticky-note:before { content: $fa-var-sticky-note; }
+.#{$fa-css-prefix}-sticky-note-o:before { content: $fa-var-sticky-note-o; }
+.#{$fa-css-prefix}-cc-jcb:before { content: $fa-var-cc-jcb; }
+.#{$fa-css-prefix}-cc-diners-club:before { content: $fa-var-cc-diners-club; }
+.#{$fa-css-prefix}-clone:before { content: $fa-var-clone; }
+.#{$fa-css-prefix}-balance-scale:before { content: $fa-var-balance-scale; }
+.#{$fa-css-prefix}-hourglass-o:before { content: $fa-var-hourglass-o; }
+.#{$fa-css-prefix}-hourglass-1:before,
+.#{$fa-css-prefix}-hourglass-start:before { content: $fa-var-hourglass-start; }
+.#{$fa-css-prefix}-hourglass-2:before,
+.#{$fa-css-prefix}-hourglass-half:before { content: $fa-var-hourglass-half; }
+.#{$fa-css-prefix}-hourglass-3:before,
+.#{$fa-css-prefix}-hourglass-end:before { content: $fa-var-hourglass-end; }
+.#{$fa-css-prefix}-hourglass:before { content: $fa-var-hourglass; }
+.#{$fa-css-prefix}-hand-grab-o:before,
+.#{$fa-css-prefix}-hand-rock-o:before { content: $fa-var-hand-rock-o; }
+.#{$fa-css-prefix}-hand-stop-o:before,
+.#{$fa-css-prefix}-hand-paper-o:before { content: $fa-var-hand-paper-o; }
+.#{$fa-css-prefix}-hand-scissors-o:before { content: $fa-var-hand-scissors-o; }
+.#{$fa-css-prefix}-hand-lizard-o:before { content: $fa-var-hand-lizard-o; }
+.#{$fa-css-prefix}-hand-spock-o:before { content: $fa-var-hand-spock-o; }
+.#{$fa-css-prefix}-hand-pointer-o:before { content: $fa-var-hand-pointer-o; }
+.#{$fa-css-prefix}-hand-peace-o:before { content: $fa-var-hand-peace-o; }
+.#{$fa-css-prefix}-trademark:before { content: $fa-var-trademark; }
+.#{$fa-css-prefix}-registered:before { content: $fa-var-registered; }
+.#{$fa-css-prefix}-creative-commons:before { content: $fa-var-creative-commons; }
+.#{$fa-css-prefix}-gg:before { content: $fa-var-gg; }
+.#{$fa-css-prefix}-gg-circle:before { content: $fa-var-gg-circle; }
+.#{$fa-css-prefix}-tripadvisor:before { content: $fa-var-tripadvisor; }
+.#{$fa-css-prefix}-odnoklassniki:before { content: $fa-var-odnoklassniki; }
+.#{$fa-css-prefix}-odnoklassniki-square:before { content: $fa-var-odnoklassniki-square; }
+.#{$fa-css-prefix}-get-pocket:before { content: $fa-var-get-pocket; }
+.#{$fa-css-prefix}-wikipedia-w:before { content: $fa-var-wikipedia-w; }
+.#{$fa-css-prefix}-safari:before { content: $fa-var-safari; }
+.#{$fa-css-prefix}-chrome:before { content: $fa-var-chrome; }
+.#{$fa-css-prefix}-firefox:before { content: $fa-var-firefox; }
+.#{$fa-css-prefix}-opera:before { content: $fa-var-opera; }
+.#{$fa-css-prefix}-internet-explorer:before { content: $fa-var-internet-explorer; }
+.#{$fa-css-prefix}-tv:before,
+.#{$fa-css-prefix}-television:before { content: $fa-var-television; }
+.#{$fa-css-prefix}-contao:before { content: $fa-var-contao; }
+.#{$fa-css-prefix}-500px:before { content: $fa-var-500px; }
+.#{$fa-css-prefix}-amazon:before { content: $fa-var-amazon; }
+.#{$fa-css-prefix}-calendar-plus-o:before { content: $fa-var-calendar-plus-o; }
+.#{$fa-css-prefix}-calendar-minus-o:before { content: $fa-var-calendar-minus-o; }
+.#{$fa-css-prefix}-calendar-times-o:before { content: $fa-var-calendar-times-o; }
+.#{$fa-css-prefix}-calendar-check-o:before { content: $fa-var-calendar-check-o; }
+.#{$fa-css-prefix}-industry:before { content: $fa-var-industry; }
+.#{$fa-css-prefix}-map-pin:before { content: $fa-var-map-pin; }
+.#{$fa-css-prefix}-map-signs:before { content: $fa-var-map-signs; }
+.#{$fa-css-prefix}-map-o:before { content: $fa-var-map-o; }
+.#{$fa-css-prefix}-map:before { content: $fa-var-map; }
+.#{$fa-css-prefix}-commenting:before { content: $fa-var-commenting; }
+.#{$fa-css-prefix}-commenting-o:before { content: $fa-var-commenting-o; }
+.#{$fa-css-prefix}-houzz:before { content: $fa-var-houzz; }
+.#{$fa-css-prefix}-vimeo:before { content: $fa-var-vimeo; }
+.#{$fa-css-prefix}-black-tie:before { content: $fa-var-black-tie; }
+.#{$fa-css-prefix}-fonticons:before { content: $fa-var-fonticons; }
+.#{$fa-css-prefix}-reddit-alien:before { content: $fa-var-reddit-alien; }
+.#{$fa-css-prefix}-edge:before { content: $fa-var-edge; }
+.#{$fa-css-prefix}-credit-card-alt:before { content: $fa-var-credit-card-alt; }
+.#{$fa-css-prefix}-codiepie:before { content: $fa-var-codiepie; }
+.#{$fa-css-prefix}-modx:before { content: $fa-var-modx; }
+.#{$fa-css-prefix}-fort-awesome:before { content: $fa-var-fort-awesome; }
+.#{$fa-css-prefix}-usb:before { content: $fa-var-usb; }
+.#{$fa-css-prefix}-product-hunt:before { content: $fa-var-product-hunt; }
+.#{$fa-css-prefix}-mixcloud:before { content: $fa-var-mixcloud; }
+.#{$fa-css-prefix}-scribd:before { content: $fa-var-scribd; }
+.#{$fa-css-prefix}-pause-circle:before { content: $fa-var-pause-circle; }
+.#{$fa-css-prefix}-pause-circle-o:before { content: $fa-var-pause-circle-o; }
+.#{$fa-css-prefix}-stop-circle:before { content: $fa-var-stop-circle; }
+.#{$fa-css-prefix}-stop-circle-o:before { content: $fa-var-stop-circle-o; }
+.#{$fa-css-prefix}-shopping-bag:before { content: $fa-var-shopping-bag; }
+.#{$fa-css-prefix}-shopping-basket:before { content: $fa-var-shopping-basket; }
+.#{$fa-css-prefix}-hashtag:before { content: $fa-var-hashtag; }
+.#{$fa-css-prefix}-bluetooth:before { content: $fa-var-bluetooth; }
+.#{$fa-css-prefix}-bluetooth-b:before { content: $fa-var-bluetooth-b; }
+.#{$fa-css-prefix}-percent:before { content: $fa-var-percent; }
+.#{$fa-css-prefix}-gitlab:before { content: $fa-var-gitlab; }
+.#{$fa-css-prefix}-wpbeginner:before { content: $fa-var-wpbeginner; }
+.#{$fa-css-prefix}-wpforms:before { content: $fa-var-wpforms; }
+.#{$fa-css-prefix}-envira:before { content: $fa-var-envira; }
+.#{$fa-css-prefix}-universal-access:before { content: $fa-var-universal-access; }
+.#{$fa-css-prefix}-wheelchair-alt:before { content: $fa-var-wheelchair-alt; }
+.#{$fa-css-prefix}-question-circle-o:before { content: $fa-var-question-circle-o; }
+.#{$fa-css-prefix}-blind:before { content: $fa-var-blind; }
+.#{$fa-css-prefix}-audio-description:before { content: $fa-var-audio-description; }
+.#{$fa-css-prefix}-volume-control-phone:before { content: $fa-var-volume-control-phone; }
+.#{$fa-css-prefix}-braille:before { content: $fa-var-braille; }
+.#{$fa-css-prefix}-assistive-listening-systems:before { content: $fa-var-assistive-listening-systems; }
+.#{$fa-css-prefix}-asl-interpreting:before,
+.#{$fa-css-prefix}-american-sign-language-interpreting:before { content: $fa-var-american-sign-language-interpreting; }
+.#{$fa-css-prefix}-deafness:before,
+.#{$fa-css-prefix}-hard-of-hearing:before,
+.#{$fa-css-prefix}-deaf:before { content: $fa-var-deaf; }
+.#{$fa-css-prefix}-glide:before { content: $fa-var-glide; }
+.#{$fa-css-prefix}-glide-g:before { content: $fa-var-glide-g; }
+.#{$fa-css-prefix}-signing:before,
+.#{$fa-css-prefix}-sign-language:before { content: $fa-var-sign-language; }
+.#{$fa-css-prefix}-low-vision:before { content: $fa-var-low-vision; }
+.#{$fa-css-prefix}-viadeo:before { content: $fa-var-viadeo; }
+.#{$fa-css-prefix}-viadeo-square:before { content: $fa-var-viadeo-square; }
+.#{$fa-css-prefix}-snapchat:before { content: $fa-var-snapchat; }
+.#{$fa-css-prefix}-snapchat-ghost:before { content: $fa-var-snapchat-ghost; }
+.#{$fa-css-prefix}-snapchat-square:before { content: $fa-var-snapchat-square; }
+.#{$fa-css-prefix}-pied-piper:before { content: $fa-var-pied-piper; }
+.#{$fa-css-prefix}-first-order:before { content: $fa-var-first-order; }
+.#{$fa-css-prefix}-yoast:before { content: $fa-var-yoast; }
+.#{$fa-css-prefix}-themeisle:before { content: $fa-var-themeisle; }
+.#{$fa-css-prefix}-google-plus-circle:before,
+.#{$fa-css-prefix}-google-plus-official:before { content: $fa-var-google-plus-official; }
+.#{$fa-css-prefix}-fa:before,
+.#{$fa-css-prefix}-font-awesome:before { content: $fa-var-font-awesome; }
diff --git a/themes/bootstrap3/scss/vendor/font-awesome/_larger.scss b/themes/bootstrap3/scss/vendor/font-awesome/_larger.scss
new file mode 100644
index 0000000000000000000000000000000000000000..41e9a8184aa287c5970cc8415e3c5a6310dc9f79
--- /dev/null
+++ b/themes/bootstrap3/scss/vendor/font-awesome/_larger.scss
@@ -0,0 +1,13 @@
+// Icon Sizes
+// -------------------------
+
+/* makes the font 33% larger relative to the icon container */
+.#{$fa-css-prefix}-lg {
+  font-size: (4em / 3);
+  line-height: (3em / 4);
+  vertical-align: -15%;
+}
+.#{$fa-css-prefix}-2x { font-size: 2em; }
+.#{$fa-css-prefix}-3x { font-size: 3em; }
+.#{$fa-css-prefix}-4x { font-size: 4em; }
+.#{$fa-css-prefix}-5x { font-size: 5em; }
diff --git a/themes/bootstrap3/scss/vendor/font-awesome/_list.scss b/themes/bootstrap3/scss/vendor/font-awesome/_list.scss
new file mode 100644
index 0000000000000000000000000000000000000000..7d1e4d54d6c293333eb638aa56feba7b62e15564
--- /dev/null
+++ b/themes/bootstrap3/scss/vendor/font-awesome/_list.scss
@@ -0,0 +1,19 @@
+// List Icons
+// -------------------------
+
+.#{$fa-css-prefix}-ul {
+  padding-left: 0;
+  margin-left: $fa-li-width;
+  list-style-type: none;
+  > li { position: relative; }
+}
+.#{$fa-css-prefix}-li {
+  position: absolute;
+  left: -$fa-li-width;
+  width: $fa-li-width;
+  top: (2em / 14);
+  text-align: center;
+  &.#{$fa-css-prefix}-lg {
+    left: -$fa-li-width + (4em / 14);
+  }
+}
diff --git a/themes/bootstrap3/scss/vendor/font-awesome/_mixins.scss b/themes/bootstrap3/scss/vendor/font-awesome/_mixins.scss
new file mode 100644
index 0000000000000000000000000000000000000000..c3bbd5745d35bebda3e16ce18aeff7a4a0ce5ae1
--- /dev/null
+++ b/themes/bootstrap3/scss/vendor/font-awesome/_mixins.scss
@@ -0,0 +1,60 @@
+// Mixins
+// --------------------------
+
+@mixin fa-icon() {
+  display: inline-block;
+  font: normal normal normal #{$fa-font-size-base}/#{$fa-line-height-base} FontAwesome; // shortening font declaration
+  font-size: inherit; // can't have font-size inherit on line above, so need to override
+  text-rendering: auto; // optimizelegibility throws things off #1094
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+
+}
+
+@mixin fa-icon-rotate($degrees, $rotation) {
+  -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=#{$rotation})";
+  -webkit-transform: rotate($degrees);
+      -ms-transform: rotate($degrees);
+          transform: rotate($degrees);
+}
+
+@mixin fa-icon-flip($horiz, $vert, $rotation) {
+  -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=#{$rotation}, mirror=1)";
+  -webkit-transform: scale($horiz, $vert);
+      -ms-transform: scale($horiz, $vert);
+          transform: scale($horiz, $vert);
+}
+
+
+// Only display content to screen readers. A la Bootstrap 4.
+//
+// See: http://a11yproject.com/posts/how-to-hide-content/
+
+@mixin sr-only {
+  position: absolute;
+  width: 1px;
+  height: 1px;
+  padding: 0;
+  margin: -1px;
+  overflow: hidden;
+  clip: rect(0,0,0,0);
+  border: 0;
+}
+
+// Use in conjunction with .sr-only to only display content when it's focused.
+//
+// Useful for "Skip to main content" links; see http://www.w3.org/TR/2013/NOTE-WCAG20-TECHS-20130905/G1
+//
+// Credit: HTML5 Boilerplate
+
+@mixin sr-only-focusable {
+  &:active,
+  &:focus {
+    position: static;
+    width: auto;
+    height: auto;
+    margin: 0;
+    overflow: visible;
+    clip: auto;
+  }
+}
diff --git a/themes/bootstrap3/scss/vendor/font-awesome/_path.scss b/themes/bootstrap3/scss/vendor/font-awesome/_path.scss
new file mode 100644
index 0000000000000000000000000000000000000000..bb457c23a8e4e0688ebd9383e34ab9f2b3acecab
--- /dev/null
+++ b/themes/bootstrap3/scss/vendor/font-awesome/_path.scss
@@ -0,0 +1,15 @@
+/* FONT PATH
+ * -------------------------- */
+
+@font-face {
+  font-family: 'FontAwesome';
+  src: url('#{$fa-font-path}/fontawesome-webfont.eot?v=#{$fa-version}');
+  src: url('#{$fa-font-path}/fontawesome-webfont.eot?#iefix&v=#{$fa-version}') format('embedded-opentype'),
+    url('#{$fa-font-path}/fontawesome-webfont.woff2?v=#{$fa-version}') format('woff2'),
+    url('#{$fa-font-path}/fontawesome-webfont.woff?v=#{$fa-version}') format('woff'),
+    url('#{$fa-font-path}/fontawesome-webfont.ttf?v=#{$fa-version}') format('truetype'),
+    url('#{$fa-font-path}/fontawesome-webfont.svg?v=#{$fa-version}#fontawesomeregular') format('svg');
+//  src: url('#{$fa-font-path}/FontAwesome.otf') format('opentype'); // used when developing fonts
+  font-weight: normal;
+  font-style: normal;
+}
diff --git a/themes/bootstrap3/scss/vendor/font-awesome/_rotated-flipped.scss b/themes/bootstrap3/scss/vendor/font-awesome/_rotated-flipped.scss
new file mode 100644
index 0000000000000000000000000000000000000000..a3558fd09ca7cb968166d5445f4df1a0bc2d5a7e
--- /dev/null
+++ b/themes/bootstrap3/scss/vendor/font-awesome/_rotated-flipped.scss
@@ -0,0 +1,20 @@
+// Rotated & Flipped Icons
+// -------------------------
+
+.#{$fa-css-prefix}-rotate-90  { @include fa-icon-rotate(90deg, 1);  }
+.#{$fa-css-prefix}-rotate-180 { @include fa-icon-rotate(180deg, 2); }
+.#{$fa-css-prefix}-rotate-270 { @include fa-icon-rotate(270deg, 3); }
+
+.#{$fa-css-prefix}-flip-horizontal { @include fa-icon-flip(-1, 1, 0); }
+.#{$fa-css-prefix}-flip-vertical   { @include fa-icon-flip(1, -1, 2); }
+
+// Hook for IE8-9
+// -------------------------
+
+:root .#{$fa-css-prefix}-rotate-90,
+:root .#{$fa-css-prefix}-rotate-180,
+:root .#{$fa-css-prefix}-rotate-270,
+:root .#{$fa-css-prefix}-flip-horizontal,
+:root .#{$fa-css-prefix}-flip-vertical {
+  filter: none;
+}
diff --git a/themes/bootstrap3/scss/vendor/font-awesome/_screen-reader.scss b/themes/bootstrap3/scss/vendor/font-awesome/_screen-reader.scss
new file mode 100644
index 0000000000000000000000000000000000000000..637426f0da6dcef3602d764d9e359dabb4a5a862
--- /dev/null
+++ b/themes/bootstrap3/scss/vendor/font-awesome/_screen-reader.scss
@@ -0,0 +1,5 @@
+// Screen Readers
+// -------------------------
+
+.sr-only { @include sr-only(); }
+.sr-only-focusable { @include sr-only-focusable(); }
diff --git a/themes/bootstrap3/scss/vendor/font-awesome/_stacked.scss b/themes/bootstrap3/scss/vendor/font-awesome/_stacked.scss
new file mode 100644
index 0000000000000000000000000000000000000000..aef7403660c9a2ccc02a264c62c6b105f7d8d532
--- /dev/null
+++ b/themes/bootstrap3/scss/vendor/font-awesome/_stacked.scss
@@ -0,0 +1,20 @@
+// Stacked Icons
+// -------------------------
+
+.#{$fa-css-prefix}-stack {
+  position: relative;
+  display: inline-block;
+  width: 2em;
+  height: 2em;
+  line-height: 2em;
+  vertical-align: middle;
+}
+.#{$fa-css-prefix}-stack-1x, .#{$fa-css-prefix}-stack-2x {
+  position: absolute;
+  left: 0;
+  width: 100%;
+  text-align: center;
+}
+.#{$fa-css-prefix}-stack-1x { line-height: inherit; }
+.#{$fa-css-prefix}-stack-2x { font-size: 2em; }
+.#{$fa-css-prefix}-inverse { color: $fa-inverse; }
diff --git a/themes/bootstrap3/scss/vendor/font-awesome/_variables.scss b/themes/bootstrap3/scss/vendor/font-awesome/_variables.scss
new file mode 100644
index 0000000000000000000000000000000000000000..a5a89ef97bcfa214b44b81d0cdb488cca416420e
--- /dev/null
+++ b/themes/bootstrap3/scss/vendor/font-awesome/_variables.scss
@@ -0,0 +1,744 @@
+// Variables
+// --------------------------
+
+$fa-font-path:        "../fonts" !default;
+$fa-font-size-base:   14px !default;
+$fa-line-height-base: 1 !default;
+//$fa-font-path:        "//netdna.bootstrapcdn.com/font-awesome/4.6.3/fonts" !default; // for referencing Bootstrap CDN font files directly
+$fa-css-prefix:       fa !default;
+$fa-version:          "4.6.3" !default;
+$fa-border-color:     #eee !default;
+$fa-inverse:          #fff !default;
+$fa-li-width:         (30em / 14) !default;
+
+$fa-var-500px: "\f26e";
+$fa-var-adjust: "\f042";
+$fa-var-adn: "\f170";
+$fa-var-align-center: "\f037";
+$fa-var-align-justify: "\f039";
+$fa-var-align-left: "\f036";
+$fa-var-align-right: "\f038";
+$fa-var-amazon: "\f270";
+$fa-var-ambulance: "\f0f9";
+$fa-var-american-sign-language-interpreting: "\f2a3";
+$fa-var-anchor: "\f13d";
+$fa-var-android: "\f17b";
+$fa-var-angellist: "\f209";
+$fa-var-angle-double-down: "\f103";
+$fa-var-angle-double-left: "\f100";
+$fa-var-angle-double-right: "\f101";
+$fa-var-angle-double-up: "\f102";
+$fa-var-angle-down: "\f107";
+$fa-var-angle-left: "\f104";
+$fa-var-angle-right: "\f105";
+$fa-var-angle-up: "\f106";
+$fa-var-apple: "\f179";
+$fa-var-archive: "\f187";
+$fa-var-area-chart: "\f1fe";
+$fa-var-arrow-circle-down: "\f0ab";
+$fa-var-arrow-circle-left: "\f0a8";
+$fa-var-arrow-circle-o-down: "\f01a";
+$fa-var-arrow-circle-o-left: "\f190";
+$fa-var-arrow-circle-o-right: "\f18e";
+$fa-var-arrow-circle-o-up: "\f01b";
+$fa-var-arrow-circle-right: "\f0a9";
+$fa-var-arrow-circle-up: "\f0aa";
+$fa-var-arrow-down: "\f063";
+$fa-var-arrow-left: "\f060";
+$fa-var-arrow-right: "\f061";
+$fa-var-arrow-up: "\f062";
+$fa-var-arrows: "\f047";
+$fa-var-arrows-alt: "\f0b2";
+$fa-var-arrows-h: "\f07e";
+$fa-var-arrows-v: "\f07d";
+$fa-var-asl-interpreting: "\f2a3";
+$fa-var-assistive-listening-systems: "\f2a2";
+$fa-var-asterisk: "\f069";
+$fa-var-at: "\f1fa";
+$fa-var-audio-description: "\f29e";
+$fa-var-automobile: "\f1b9";
+$fa-var-backward: "\f04a";
+$fa-var-balance-scale: "\f24e";
+$fa-var-ban: "\f05e";
+$fa-var-bank: "\f19c";
+$fa-var-bar-chart: "\f080";
+$fa-var-bar-chart-o: "\f080";
+$fa-var-barcode: "\f02a";
+$fa-var-bars: "\f0c9";
+$fa-var-battery-0: "\f244";
+$fa-var-battery-1: "\f243";
+$fa-var-battery-2: "\f242";
+$fa-var-battery-3: "\f241";
+$fa-var-battery-4: "\f240";
+$fa-var-battery-empty: "\f244";
+$fa-var-battery-full: "\f240";
+$fa-var-battery-half: "\f242";
+$fa-var-battery-quarter: "\f243";
+$fa-var-battery-three-quarters: "\f241";
+$fa-var-bed: "\f236";
+$fa-var-beer: "\f0fc";
+$fa-var-behance: "\f1b4";
+$fa-var-behance-square: "\f1b5";
+$fa-var-bell: "\f0f3";
+$fa-var-bell-o: "\f0a2";
+$fa-var-bell-slash: "\f1f6";
+$fa-var-bell-slash-o: "\f1f7";
+$fa-var-bicycle: "\f206";
+$fa-var-binoculars: "\f1e5";
+$fa-var-birthday-cake: "\f1fd";
+$fa-var-bitbucket: "\f171";
+$fa-var-bitbucket-square: "\f172";
+$fa-var-bitcoin: "\f15a";
+$fa-var-black-tie: "\f27e";
+$fa-var-blind: "\f29d";
+$fa-var-bluetooth: "\f293";
+$fa-var-bluetooth-b: "\f294";
+$fa-var-bold: "\f032";
+$fa-var-bolt: "\f0e7";
+$fa-var-bomb: "\f1e2";
+$fa-var-book: "\f02d";
+$fa-var-bookmark: "\f02e";
+$fa-var-bookmark-o: "\f097";
+$fa-var-braille: "\f2a1";
+$fa-var-briefcase: "\f0b1";
+$fa-var-btc: "\f15a";
+$fa-var-bug: "\f188";
+$fa-var-building: "\f1ad";
+$fa-var-building-o: "\f0f7";
+$fa-var-bullhorn: "\f0a1";
+$fa-var-bullseye: "\f140";
+$fa-var-bus: "\f207";
+$fa-var-buysellads: "\f20d";
+$fa-var-cab: "\f1ba";
+$fa-var-calculator: "\f1ec";
+$fa-var-calendar: "\f073";
+$fa-var-calendar-check-o: "\f274";
+$fa-var-calendar-minus-o: "\f272";
+$fa-var-calendar-o: "\f133";
+$fa-var-calendar-plus-o: "\f271";
+$fa-var-calendar-times-o: "\f273";
+$fa-var-camera: "\f030";
+$fa-var-camera-retro: "\f083";
+$fa-var-car: "\f1b9";
+$fa-var-caret-down: "\f0d7";
+$fa-var-caret-left: "\f0d9";
+$fa-var-caret-right: "\f0da";
+$fa-var-caret-square-o-down: "\f150";
+$fa-var-caret-square-o-left: "\f191";
+$fa-var-caret-square-o-right: "\f152";
+$fa-var-caret-square-o-up: "\f151";
+$fa-var-caret-up: "\f0d8";
+$fa-var-cart-arrow-down: "\f218";
+$fa-var-cart-plus: "\f217";
+$fa-var-cc: "\f20a";
+$fa-var-cc-amex: "\f1f3";
+$fa-var-cc-diners-club: "\f24c";
+$fa-var-cc-discover: "\f1f2";
+$fa-var-cc-jcb: "\f24b";
+$fa-var-cc-mastercard: "\f1f1";
+$fa-var-cc-paypal: "\f1f4";
+$fa-var-cc-stripe: "\f1f5";
+$fa-var-cc-visa: "\f1f0";
+$fa-var-certificate: "\f0a3";
+$fa-var-chain: "\f0c1";
+$fa-var-chain-broken: "\f127";
+$fa-var-check: "\f00c";
+$fa-var-check-circle: "\f058";
+$fa-var-check-circle-o: "\f05d";
+$fa-var-check-square: "\f14a";
+$fa-var-check-square-o: "\f046";
+$fa-var-chevron-circle-down: "\f13a";
+$fa-var-chevron-circle-left: "\f137";
+$fa-var-chevron-circle-right: "\f138";
+$fa-var-chevron-circle-up: "\f139";
+$fa-var-chevron-down: "\f078";
+$fa-var-chevron-left: "\f053";
+$fa-var-chevron-right: "\f054";
+$fa-var-chevron-up: "\f077";
+$fa-var-child: "\f1ae";
+$fa-var-chrome: "\f268";
+$fa-var-circle: "\f111";
+$fa-var-circle-o: "\f10c";
+$fa-var-circle-o-notch: "\f1ce";
+$fa-var-circle-thin: "\f1db";
+$fa-var-clipboard: "\f0ea";
+$fa-var-clock-o: "\f017";
+$fa-var-clone: "\f24d";
+$fa-var-close: "\f00d";
+$fa-var-cloud: "\f0c2";
+$fa-var-cloud-download: "\f0ed";
+$fa-var-cloud-upload: "\f0ee";
+$fa-var-cny: "\f157";
+$fa-var-code: "\f121";
+$fa-var-code-fork: "\f126";
+$fa-var-codepen: "\f1cb";
+$fa-var-codiepie: "\f284";
+$fa-var-coffee: "\f0f4";
+$fa-var-cog: "\f013";
+$fa-var-cogs: "\f085";
+$fa-var-columns: "\f0db";
+$fa-var-comment: "\f075";
+$fa-var-comment-o: "\f0e5";
+$fa-var-commenting: "\f27a";
+$fa-var-commenting-o: "\f27b";
+$fa-var-comments: "\f086";
+$fa-var-comments-o: "\f0e6";
+$fa-var-compass: "\f14e";
+$fa-var-compress: "\f066";
+$fa-var-connectdevelop: "\f20e";
+$fa-var-contao: "\f26d";
+$fa-var-copy: "\f0c5";
+$fa-var-copyright: "\f1f9";
+$fa-var-creative-commons: "\f25e";
+$fa-var-credit-card: "\f09d";
+$fa-var-credit-card-alt: "\f283";
+$fa-var-crop: "\f125";
+$fa-var-crosshairs: "\f05b";
+$fa-var-css3: "\f13c";
+$fa-var-cube: "\f1b2";
+$fa-var-cubes: "\f1b3";
+$fa-var-cut: "\f0c4";
+$fa-var-cutlery: "\f0f5";
+$fa-var-dashboard: "\f0e4";
+$fa-var-dashcube: "\f210";
+$fa-var-database: "\f1c0";
+$fa-var-deaf: "\f2a4";
+$fa-var-deafness: "\f2a4";
+$fa-var-dedent: "\f03b";
+$fa-var-delicious: "\f1a5";
+$fa-var-desktop: "\f108";
+$fa-var-deviantart: "\f1bd";
+$fa-var-diamond: "\f219";
+$fa-var-digg: "\f1a6";
+$fa-var-dollar: "\f155";
+$fa-var-dot-circle-o: "\f192";
+$fa-var-download: "\f019";
+$fa-var-dribbble: "\f17d";
+$fa-var-dropbox: "\f16b";
+$fa-var-drupal: "\f1a9";
+$fa-var-edge: "\f282";
+$fa-var-edit: "\f044";
+$fa-var-eject: "\f052";
+$fa-var-ellipsis-h: "\f141";
+$fa-var-ellipsis-v: "\f142";
+$fa-var-empire: "\f1d1";
+$fa-var-envelope: "\f0e0";
+$fa-var-envelope-o: "\f003";
+$fa-var-envelope-square: "\f199";
+$fa-var-envira: "\f299";
+$fa-var-eraser: "\f12d";
+$fa-var-eur: "\f153";
+$fa-var-euro: "\f153";
+$fa-var-exchange: "\f0ec";
+$fa-var-exclamation: "\f12a";
+$fa-var-exclamation-circle: "\f06a";
+$fa-var-exclamation-triangle: "\f071";
+$fa-var-expand: "\f065";
+$fa-var-expeditedssl: "\f23e";
+$fa-var-external-link: "\f08e";
+$fa-var-external-link-square: "\f14c";
+$fa-var-eye: "\f06e";
+$fa-var-eye-slash: "\f070";
+$fa-var-eyedropper: "\f1fb";
+$fa-var-fa: "\f2b4";
+$fa-var-facebook: "\f09a";
+$fa-var-facebook-f: "\f09a";
+$fa-var-facebook-official: "\f230";
+$fa-var-facebook-square: "\f082";
+$fa-var-fast-backward: "\f049";
+$fa-var-fast-forward: "\f050";
+$fa-var-fax: "\f1ac";
+$fa-var-feed: "\f09e";
+$fa-var-female: "\f182";
+$fa-var-fighter-jet: "\f0fb";
+$fa-var-file: "\f15b";
+$fa-var-file-archive-o: "\f1c6";
+$fa-var-file-audio-o: "\f1c7";
+$fa-var-file-code-o: "\f1c9";
+$fa-var-file-excel-o: "\f1c3";
+$fa-var-file-image-o: "\f1c5";
+$fa-var-file-movie-o: "\f1c8";
+$fa-var-file-o: "\f016";
+$fa-var-file-pdf-o: "\f1c1";
+$fa-var-file-photo-o: "\f1c5";
+$fa-var-file-picture-o: "\f1c5";
+$fa-var-file-powerpoint-o: "\f1c4";
+$fa-var-file-sound-o: "\f1c7";
+$fa-var-file-text: "\f15c";
+$fa-var-file-text-o: "\f0f6";
+$fa-var-file-video-o: "\f1c8";
+$fa-var-file-word-o: "\f1c2";
+$fa-var-file-zip-o: "\f1c6";
+$fa-var-files-o: "\f0c5";
+$fa-var-film: "\f008";
+$fa-var-filter: "\f0b0";
+$fa-var-fire: "\f06d";
+$fa-var-fire-extinguisher: "\f134";
+$fa-var-firefox: "\f269";
+$fa-var-first-order: "\f2b0";
+$fa-var-flag: "\f024";
+$fa-var-flag-checkered: "\f11e";
+$fa-var-flag-o: "\f11d";
+$fa-var-flash: "\f0e7";
+$fa-var-flask: "\f0c3";
+$fa-var-flickr: "\f16e";
+$fa-var-floppy-o: "\f0c7";
+$fa-var-folder: "\f07b";
+$fa-var-folder-o: "\f114";
+$fa-var-folder-open: "\f07c";
+$fa-var-folder-open-o: "\f115";
+$fa-var-font: "\f031";
+$fa-var-font-awesome: "\f2b4";
+$fa-var-fonticons: "\f280";
+$fa-var-fort-awesome: "\f286";
+$fa-var-forumbee: "\f211";
+$fa-var-forward: "\f04e";
+$fa-var-foursquare: "\f180";
+$fa-var-frown-o: "\f119";
+$fa-var-futbol-o: "\f1e3";
+$fa-var-gamepad: "\f11b";
+$fa-var-gavel: "\f0e3";
+$fa-var-gbp: "\f154";
+$fa-var-ge: "\f1d1";
+$fa-var-gear: "\f013";
+$fa-var-gears: "\f085";
+$fa-var-genderless: "\f22d";
+$fa-var-get-pocket: "\f265";
+$fa-var-gg: "\f260";
+$fa-var-gg-circle: "\f261";
+$fa-var-gift: "\f06b";
+$fa-var-git: "\f1d3";
+$fa-var-git-square: "\f1d2";
+$fa-var-github: "\f09b";
+$fa-var-github-alt: "\f113";
+$fa-var-github-square: "\f092";
+$fa-var-gitlab: "\f296";
+$fa-var-gittip: "\f184";
+$fa-var-glass: "\f000";
+$fa-var-glide: "\f2a5";
+$fa-var-glide-g: "\f2a6";
+$fa-var-globe: "\f0ac";
+$fa-var-google: "\f1a0";
+$fa-var-google-plus: "\f0d5";
+$fa-var-google-plus-circle: "\f2b3";
+$fa-var-google-plus-official: "\f2b3";
+$fa-var-google-plus-square: "\f0d4";
+$fa-var-google-wallet: "\f1ee";
+$fa-var-graduation-cap: "\f19d";
+$fa-var-gratipay: "\f184";
+$fa-var-group: "\f0c0";
+$fa-var-h-square: "\f0fd";
+$fa-var-hacker-news: "\f1d4";
+$fa-var-hand-grab-o: "\f255";
+$fa-var-hand-lizard-o: "\f258";
+$fa-var-hand-o-down: "\f0a7";
+$fa-var-hand-o-left: "\f0a5";
+$fa-var-hand-o-right: "\f0a4";
+$fa-var-hand-o-up: "\f0a6";
+$fa-var-hand-paper-o: "\f256";
+$fa-var-hand-peace-o: "\f25b";
+$fa-var-hand-pointer-o: "\f25a";
+$fa-var-hand-rock-o: "\f255";
+$fa-var-hand-scissors-o: "\f257";
+$fa-var-hand-spock-o: "\f259";
+$fa-var-hand-stop-o: "\f256";
+$fa-var-hard-of-hearing: "\f2a4";
+$fa-var-hashtag: "\f292";
+$fa-var-hdd-o: "\f0a0";
+$fa-var-header: "\f1dc";
+$fa-var-headphones: "\f025";
+$fa-var-heart: "\f004";
+$fa-var-heart-o: "\f08a";
+$fa-var-heartbeat: "\f21e";
+$fa-var-history: "\f1da";
+$fa-var-home: "\f015";
+$fa-var-hospital-o: "\f0f8";
+$fa-var-hotel: "\f236";
+$fa-var-hourglass: "\f254";
+$fa-var-hourglass-1: "\f251";
+$fa-var-hourglass-2: "\f252";
+$fa-var-hourglass-3: "\f253";
+$fa-var-hourglass-end: "\f253";
+$fa-var-hourglass-half: "\f252";
+$fa-var-hourglass-o: "\f250";
+$fa-var-hourglass-start: "\f251";
+$fa-var-houzz: "\f27c";
+$fa-var-html5: "\f13b";
+$fa-var-i-cursor: "\f246";
+$fa-var-ils: "\f20b";
+$fa-var-image: "\f03e";
+$fa-var-inbox: "\f01c";
+$fa-var-indent: "\f03c";
+$fa-var-industry: "\f275";
+$fa-var-info: "\f129";
+$fa-var-info-circle: "\f05a";
+$fa-var-inr: "\f156";
+$fa-var-instagram: "\f16d";
+$fa-var-institution: "\f19c";
+$fa-var-internet-explorer: "\f26b";
+$fa-var-intersex: "\f224";
+$fa-var-ioxhost: "\f208";
+$fa-var-italic: "\f033";
+$fa-var-joomla: "\f1aa";
+$fa-var-jpy: "\f157";
+$fa-var-jsfiddle: "\f1cc";
+$fa-var-key: "\f084";
+$fa-var-keyboard-o: "\f11c";
+$fa-var-krw: "\f159";
+$fa-var-language: "\f1ab";
+$fa-var-laptop: "\f109";
+$fa-var-lastfm: "\f202";
+$fa-var-lastfm-square: "\f203";
+$fa-var-leaf: "\f06c";
+$fa-var-leanpub: "\f212";
+$fa-var-legal: "\f0e3";
+$fa-var-lemon-o: "\f094";
+$fa-var-level-down: "\f149";
+$fa-var-level-up: "\f148";
+$fa-var-life-bouy: "\f1cd";
+$fa-var-life-buoy: "\f1cd";
+$fa-var-life-ring: "\f1cd";
+$fa-var-life-saver: "\f1cd";
+$fa-var-lightbulb-o: "\f0eb";
+$fa-var-line-chart: "\f201";
+$fa-var-link: "\f0c1";
+$fa-var-linkedin: "\f0e1";
+$fa-var-linkedin-square: "\f08c";
+$fa-var-linux: "\f17c";
+$fa-var-list: "\f03a";
+$fa-var-list-alt: "\f022";
+$fa-var-list-ol: "\f0cb";
+$fa-var-list-ul: "\f0ca";
+$fa-var-location-arrow: "\f124";
+$fa-var-lock: "\f023";
+$fa-var-long-arrow-down: "\f175";
+$fa-var-long-arrow-left: "\f177";
+$fa-var-long-arrow-right: "\f178";
+$fa-var-long-arrow-up: "\f176";
+$fa-var-low-vision: "\f2a8";
+$fa-var-magic: "\f0d0";
+$fa-var-magnet: "\f076";
+$fa-var-mail-forward: "\f064";
+$fa-var-mail-reply: "\f112";
+$fa-var-mail-reply-all: "\f122";
+$fa-var-male: "\f183";
+$fa-var-map: "\f279";
+$fa-var-map-marker: "\f041";
+$fa-var-map-o: "\f278";
+$fa-var-map-pin: "\f276";
+$fa-var-map-signs: "\f277";
+$fa-var-mars: "\f222";
+$fa-var-mars-double: "\f227";
+$fa-var-mars-stroke: "\f229";
+$fa-var-mars-stroke-h: "\f22b";
+$fa-var-mars-stroke-v: "\f22a";
+$fa-var-maxcdn: "\f136";
+$fa-var-meanpath: "\f20c";
+$fa-var-medium: "\f23a";
+$fa-var-medkit: "\f0fa";
+$fa-var-meh-o: "\f11a";
+$fa-var-mercury: "\f223";
+$fa-var-microphone: "\f130";
+$fa-var-microphone-slash: "\f131";
+$fa-var-minus: "\f068";
+$fa-var-minus-circle: "\f056";
+$fa-var-minus-square: "\f146";
+$fa-var-minus-square-o: "\f147";
+$fa-var-mixcloud: "\f289";
+$fa-var-mobile: "\f10b";
+$fa-var-mobile-phone: "\f10b";
+$fa-var-modx: "\f285";
+$fa-var-money: "\f0d6";
+$fa-var-moon-o: "\f186";
+$fa-var-mortar-board: "\f19d";
+$fa-var-motorcycle: "\f21c";
+$fa-var-mouse-pointer: "\f245";
+$fa-var-music: "\f001";
+$fa-var-navicon: "\f0c9";
+$fa-var-neuter: "\f22c";
+$fa-var-newspaper-o: "\f1ea";
+$fa-var-object-group: "\f247";
+$fa-var-object-ungroup: "\f248";
+$fa-var-odnoklassniki: "\f263";
+$fa-var-odnoklassniki-square: "\f264";
+$fa-var-opencart: "\f23d";
+$fa-var-openid: "\f19b";
+$fa-var-opera: "\f26a";
+$fa-var-optin-monster: "\f23c";
+$fa-var-outdent: "\f03b";
+$fa-var-pagelines: "\f18c";
+$fa-var-paint-brush: "\f1fc";
+$fa-var-paper-plane: "\f1d8";
+$fa-var-paper-plane-o: "\f1d9";
+$fa-var-paperclip: "\f0c6";
+$fa-var-paragraph: "\f1dd";
+$fa-var-paste: "\f0ea";
+$fa-var-pause: "\f04c";
+$fa-var-pause-circle: "\f28b";
+$fa-var-pause-circle-o: "\f28c";
+$fa-var-paw: "\f1b0";
+$fa-var-paypal: "\f1ed";
+$fa-var-pencil: "\f040";
+$fa-var-pencil-square: "\f14b";
+$fa-var-pencil-square-o: "\f044";
+$fa-var-percent: "\f295";
+$fa-var-phone: "\f095";
+$fa-var-phone-square: "\f098";
+$fa-var-photo: "\f03e";
+$fa-var-picture-o: "\f03e";
+$fa-var-pie-chart: "\f200";
+$fa-var-pied-piper: "\f2ae";
+$fa-var-pied-piper-alt: "\f1a8";
+$fa-var-pied-piper-pp: "\f1a7";
+$fa-var-pinterest: "\f0d2";
+$fa-var-pinterest-p: "\f231";
+$fa-var-pinterest-square: "\f0d3";
+$fa-var-plane: "\f072";
+$fa-var-play: "\f04b";
+$fa-var-play-circle: "\f144";
+$fa-var-play-circle-o: "\f01d";
+$fa-var-plug: "\f1e6";
+$fa-var-plus: "\f067";
+$fa-var-plus-circle: "\f055";
+$fa-var-plus-square: "\f0fe";
+$fa-var-plus-square-o: "\f196";
+$fa-var-power-off: "\f011";
+$fa-var-print: "\f02f";
+$fa-var-product-hunt: "\f288";
+$fa-var-puzzle-piece: "\f12e";
+$fa-var-qq: "\f1d6";
+$fa-var-qrcode: "\f029";
+$fa-var-question: "\f128";
+$fa-var-question-circle: "\f059";
+$fa-var-question-circle-o: "\f29c";
+$fa-var-quote-left: "\f10d";
+$fa-var-quote-right: "\f10e";
+$fa-var-ra: "\f1d0";
+$fa-var-random: "\f074";
+$fa-var-rebel: "\f1d0";
+$fa-var-recycle: "\f1b8";
+$fa-var-reddit: "\f1a1";
+$fa-var-reddit-alien: "\f281";
+$fa-var-reddit-square: "\f1a2";
+$fa-var-refresh: "\f021";
+$fa-var-registered: "\f25d";
+$fa-var-remove: "\f00d";
+$fa-var-renren: "\f18b";
+$fa-var-reorder: "\f0c9";
+$fa-var-repeat: "\f01e";
+$fa-var-reply: "\f112";
+$fa-var-reply-all: "\f122";
+$fa-var-resistance: "\f1d0";
+$fa-var-retweet: "\f079";
+$fa-var-rmb: "\f157";
+$fa-var-road: "\f018";
+$fa-var-rocket: "\f135";
+$fa-var-rotate-left: "\f0e2";
+$fa-var-rotate-right: "\f01e";
+$fa-var-rouble: "\f158";
+$fa-var-rss: "\f09e";
+$fa-var-rss-square: "\f143";
+$fa-var-rub: "\f158";
+$fa-var-ruble: "\f158";
+$fa-var-rupee: "\f156";
+$fa-var-safari: "\f267";
+$fa-var-save: "\f0c7";
+$fa-var-scissors: "\f0c4";
+$fa-var-scribd: "\f28a";
+$fa-var-search: "\f002";
+$fa-var-search-minus: "\f010";
+$fa-var-search-plus: "\f00e";
+$fa-var-sellsy: "\f213";
+$fa-var-send: "\f1d8";
+$fa-var-send-o: "\f1d9";
+$fa-var-server: "\f233";
+$fa-var-share: "\f064";
+$fa-var-share-alt: "\f1e0";
+$fa-var-share-alt-square: "\f1e1";
+$fa-var-share-square: "\f14d";
+$fa-var-share-square-o: "\f045";
+$fa-var-shekel: "\f20b";
+$fa-var-sheqel: "\f20b";
+$fa-var-shield: "\f132";
+$fa-var-ship: "\f21a";
+$fa-var-shirtsinbulk: "\f214";
+$fa-var-shopping-bag: "\f290";
+$fa-var-shopping-basket: "\f291";
+$fa-var-shopping-cart: "\f07a";
+$fa-var-sign-in: "\f090";
+$fa-var-sign-language: "\f2a7";
+$fa-var-sign-out: "\f08b";
+$fa-var-signal: "\f012";
+$fa-var-signing: "\f2a7";
+$fa-var-simplybuilt: "\f215";
+$fa-var-sitemap: "\f0e8";
+$fa-var-skyatlas: "\f216";
+$fa-var-skype: "\f17e";
+$fa-var-slack: "\f198";
+$fa-var-sliders: "\f1de";
+$fa-var-slideshare: "\f1e7";
+$fa-var-smile-o: "\f118";
+$fa-var-snapchat: "\f2ab";
+$fa-var-snapchat-ghost: "\f2ac";
+$fa-var-snapchat-square: "\f2ad";
+$fa-var-soccer-ball-o: "\f1e3";
+$fa-var-sort: "\f0dc";
+$fa-var-sort-alpha-asc: "\f15d";
+$fa-var-sort-alpha-desc: "\f15e";
+$fa-var-sort-amount-asc: "\f160";
+$fa-var-sort-amount-desc: "\f161";
+$fa-var-sort-asc: "\f0de";
+$fa-var-sort-desc: "\f0dd";
+$fa-var-sort-down: "\f0dd";
+$fa-var-sort-numeric-asc: "\f162";
+$fa-var-sort-numeric-desc: "\f163";
+$fa-var-sort-up: "\f0de";
+$fa-var-soundcloud: "\f1be";
+$fa-var-space-shuttle: "\f197";
+$fa-var-spinner: "\f110";
+$fa-var-spoon: "\f1b1";
+$fa-var-spotify: "\f1bc";
+$fa-var-square: "\f0c8";
+$fa-var-square-o: "\f096";
+$fa-var-stack-exchange: "\f18d";
+$fa-var-stack-overflow: "\f16c";
+$fa-var-star: "\f005";
+$fa-var-star-half: "\f089";
+$fa-var-star-half-empty: "\f123";
+$fa-var-star-half-full: "\f123";
+$fa-var-star-half-o: "\f123";
+$fa-var-star-o: "\f006";
+$fa-var-steam: "\f1b6";
+$fa-var-steam-square: "\f1b7";
+$fa-var-step-backward: "\f048";
+$fa-var-step-forward: "\f051";
+$fa-var-stethoscope: "\f0f1";
+$fa-var-sticky-note: "\f249";
+$fa-var-sticky-note-o: "\f24a";
+$fa-var-stop: "\f04d";
+$fa-var-stop-circle: "\f28d";
+$fa-var-stop-circle-o: "\f28e";
+$fa-var-street-view: "\f21d";
+$fa-var-strikethrough: "\f0cc";
+$fa-var-stumbleupon: "\f1a4";
+$fa-var-stumbleupon-circle: "\f1a3";
+$fa-var-subscript: "\f12c";
+$fa-var-subway: "\f239";
+$fa-var-suitcase: "\f0f2";
+$fa-var-sun-o: "\f185";
+$fa-var-superscript: "\f12b";
+$fa-var-support: "\f1cd";
+$fa-var-table: "\f0ce";
+$fa-var-tablet: "\f10a";
+$fa-var-tachometer: "\f0e4";
+$fa-var-tag: "\f02b";
+$fa-var-tags: "\f02c";
+$fa-var-tasks: "\f0ae";
+$fa-var-taxi: "\f1ba";
+$fa-var-television: "\f26c";
+$fa-var-tencent-weibo: "\f1d5";
+$fa-var-terminal: "\f120";
+$fa-var-text-height: "\f034";
+$fa-var-text-width: "\f035";
+$fa-var-th: "\f00a";
+$fa-var-th-large: "\f009";
+$fa-var-th-list: "\f00b";
+$fa-var-themeisle: "\f2b2";
+$fa-var-thumb-tack: "\f08d";
+$fa-var-thumbs-down: "\f165";
+$fa-var-thumbs-o-down: "\f088";
+$fa-var-thumbs-o-up: "\f087";
+$fa-var-thumbs-up: "\f164";
+$fa-var-ticket: "\f145";
+$fa-var-times: "\f00d";
+$fa-var-times-circle: "\f057";
+$fa-var-times-circle-o: "\f05c";
+$fa-var-tint: "\f043";
+$fa-var-toggle-down: "\f150";
+$fa-var-toggle-left: "\f191";
+$fa-var-toggle-off: "\f204";
+$fa-var-toggle-on: "\f205";
+$fa-var-toggle-right: "\f152";
+$fa-var-toggle-up: "\f151";
+$fa-var-trademark: "\f25c";
+$fa-var-train: "\f238";
+$fa-var-transgender: "\f224";
+$fa-var-transgender-alt: "\f225";
+$fa-var-trash: "\f1f8";
+$fa-var-trash-o: "\f014";
+$fa-var-tree: "\f1bb";
+$fa-var-trello: "\f181";
+$fa-var-tripadvisor: "\f262";
+$fa-var-trophy: "\f091";
+$fa-var-truck: "\f0d1";
+$fa-var-try: "\f195";
+$fa-var-tty: "\f1e4";
+$fa-var-tumblr: "\f173";
+$fa-var-tumblr-square: "\f174";
+$fa-var-turkish-lira: "\f195";
+$fa-var-tv: "\f26c";
+$fa-var-twitch: "\f1e8";
+$fa-var-twitter: "\f099";
+$fa-var-twitter-square: "\f081";
+$fa-var-umbrella: "\f0e9";
+$fa-var-underline: "\f0cd";
+$fa-var-undo: "\f0e2";
+$fa-var-universal-access: "\f29a";
+$fa-var-university: "\f19c";
+$fa-var-unlink: "\f127";
+$fa-var-unlock: "\f09c";
+$fa-var-unlock-alt: "\f13e";
+$fa-var-unsorted: "\f0dc";
+$fa-var-upload: "\f093";
+$fa-var-usb: "\f287";
+$fa-var-usd: "\f155";
+$fa-var-user: "\f007";
+$fa-var-user-md: "\f0f0";
+$fa-var-user-plus: "\f234";
+$fa-var-user-secret: "\f21b";
+$fa-var-user-times: "\f235";
+$fa-var-users: "\f0c0";
+$fa-var-venus: "\f221";
+$fa-var-venus-double: "\f226";
+$fa-var-venus-mars: "\f228";
+$fa-var-viacoin: "\f237";
+$fa-var-viadeo: "\f2a9";
+$fa-var-viadeo-square: "\f2aa";
+$fa-var-video-camera: "\f03d";
+$fa-var-vimeo: "\f27d";
+$fa-var-vimeo-square: "\f194";
+$fa-var-vine: "\f1ca";
+$fa-var-vk: "\f189";
+$fa-var-volume-control-phone: "\f2a0";
+$fa-var-volume-down: "\f027";
+$fa-var-volume-off: "\f026";
+$fa-var-volume-up: "\f028";
+$fa-var-warning: "\f071";
+$fa-var-wechat: "\f1d7";
+$fa-var-weibo: "\f18a";
+$fa-var-weixin: "\f1d7";
+$fa-var-whatsapp: "\f232";
+$fa-var-wheelchair: "\f193";
+$fa-var-wheelchair-alt: "\f29b";
+$fa-var-wifi: "\f1eb";
+$fa-var-wikipedia-w: "\f266";
+$fa-var-windows: "\f17a";
+$fa-var-won: "\f159";
+$fa-var-wordpress: "\f19a";
+$fa-var-wpbeginner: "\f297";
+$fa-var-wpforms: "\f298";
+$fa-var-wrench: "\f0ad";
+$fa-var-xing: "\f168";
+$fa-var-xing-square: "\f169";
+$fa-var-y-combinator: "\f23b";
+$fa-var-y-combinator-square: "\f1d4";
+$fa-var-yahoo: "\f19e";
+$fa-var-yc: "\f23b";
+$fa-var-yc-square: "\f1d4";
+$fa-var-yelp: "\f1e9";
+$fa-var-yen: "\f157";
+$fa-var-yoast: "\f2b1";
+$fa-var-youtube: "\f167";
+$fa-var-youtube-play: "\f16a";
+$fa-var-youtube-square: "\f166";
+
diff --git a/themes/bootstrap3/scss/vendor/font-awesome/font-awesome.scss b/themes/bootstrap3/scss/vendor/font-awesome/font-awesome.scss
new file mode 100644
index 0000000000000000000000000000000000000000..2308b14ca7c27e6ea02f874e9ab7456032a7d007
--- /dev/null
+++ b/themes/bootstrap3/scss/vendor/font-awesome/font-awesome.scss
@@ -0,0 +1,18 @@
+/*!
+ *  Font Awesome 4.6.3 by @davegandy - http://fontawesome.io - @fontawesome
+ *  License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License)
+ */
+
+@import "variables";
+@import "mixins";
+@import "path";
+@import "core";
+@import "larger";
+@import "fixed-width";
+@import "list";
+@import "bordered-pulled";
+@import "animated";
+@import "rotated-flipped";
+@import "stacked";
+@import "icons";
+@import "screen-reader";
diff --git a/themes/bootstrap3/templates/Auth/ILS/newpassword.phtml b/themes/bootstrap3/templates/Auth/AbstractBase/newpassword.phtml
similarity index 57%
rename from themes/bootstrap3/templates/Auth/ILS/newpassword.phtml
rename to themes/bootstrap3/templates/Auth/AbstractBase/newpassword.phtml
index 04c0fbce37ed963751cd4dbe8131ab1f727a5745..df98cd128a887da7765bc9a26a5b2088f9f7279a 100644
--- a/themes/bootstrap3/templates/Auth/ILS/newpassword.phtml
+++ b/themes/bootstrap3/templates/Auth/AbstractBase/newpassword.phtml
@@ -15,10 +15,29 @@
     </div>
   </div>
 <? endif; ?>
+<?
+  $pattern = '';
+  if (isset($this->passwordPolicy['pattern'])) {
+    if ($this->passwordPolicy['pattern'] == 'numeric') {
+      $pattern = '\d+';
+    } elseif ($this->passwordPolicy['pattern'] == 'alphanumeric') {
+      $pattern = '[\da-zA-Z]+';
+    } else {
+      $pattern = $this->passwordPolicy['pattern'];
+    }
+  }
+?>
 <div class="form-group">
   <label class="col-sm-3 control-label"><?=$this->transEsc('new_password') ?>:</label>
   <div class="col-sm-9">
-    <input type="password" id="password" name="password" class="form-control" required aria-required="true"<?=isset($this->passwordPolicy['minLength']) ? ' data-minlength="' . $this->passwordPolicy['minLength'] . '" data-minlength-error="' . $this->escapeHtmlAttr($this->translate('password_minimum_length', array('%%minlength%%' => $this->passwordPolicy['minLength']))) . '"' : ''?><?=isset($this->passwordPolicy['maxLength']) ? ' maxlength="' . $this->passwordPolicy['maxLength'] . '"' : ''?>/>
+    <input type="password" id="password" name="password" class="form-control" required aria-required="true"
+      <?=isset($this->passwordPolicy['minLength']) ? ' data-minlength="' . $this->passwordPolicy['minLength'] . '" data-minlength-error="' . $this->escapeHtmlAttr($this->translate('password_minimum_length', array('%%minlength%%' => $this->passwordPolicy['minLength']))) . '"' : '' ?>
+      <?=isset($this->passwordPolicy['maxLength']) ? ' maxlength="' . $this->passwordPolicy['maxLength'] . '"' : '' ?>
+      <?=$pattern ? ' pattern="' . $pattern . '"' : '' ?>
+    />
+    <? if ($this->passwordPolicy['hint']): ?>
+      <div class="help-block"><?=$this->transEsc($this->passwordPolicy['hint']) ?></div>
+    <? endif; ?>
     <div class="help-block with-errors"></div>
   </div>
 </div>
diff --git a/themes/bootstrap3/templates/Auth/Database/create.phtml b/themes/bootstrap3/templates/Auth/Database/create.phtml
index 187efa9070c5a899e6203f55983973417e4918ec..2afb1f1e2172280bb2f3e1add78f147b31a1c20a 100644
--- a/themes/bootstrap3/templates/Auth/Database/create.phtml
+++ b/themes/bootstrap3/templates/Auth/Database/create.phtml
@@ -1,3 +1,15 @@
+<?
+  $pattern = '';
+  if (isset($this->passwordPolicy['pattern'])) {
+    if ($this->passwordPolicy['pattern'] == 'numeric') {
+      $pattern = '\d+';
+    } elseif ($this->passwordPolicy['pattern'] == 'alphanumeric') {
+      $pattern = '[\da-zA-Z]+';
+    } else {
+      $pattern = $this->passwordPolicy['pattern'];
+    }
+  }
+?>
 <div class="form-group">
   <label class="col-sm-3 control-label" for="account_firstname"><?=$this->transEsc('First Name')?>:</label>
   <div class="col-sm-9">
@@ -21,12 +33,20 @@
   <label class="col-sm-3 control-label" for="account_username"><?=$this->transEsc('Desired Username')?>:</label>
   <div class="col-sm-9">
     <input id="account_username" type="text" name="username" required aria-required="true" value="<?=$this->escapeHtmlAttr($this->request->get('username'))?>" class="form-control"/>
+    <div class="help-block with-errors"></div>
   </div>
 </div>
 <div class="form-group">
   <label class="col-sm-3 control-label" for="account_password"><?=$this->transEsc('Password')?>:</label>
   <div class="col-sm-9">
-    <input id="account_password" type="password" name="password" required aria-required="true" class="form-control"<?=isset($this->passwordPolicy['minLength']) ? ' data-minlength="' . $this->passwordPolicy['minLength'] . '" data-minlength-error="' . $this->escapeHtmlAttr($this->translate('password_minimum_length', array('%%minlength%%' => $this->passwordPolicy['minLength']))) . '"' : ''?><?=isset($this->passwordPolicy['maxLength']) ? ' maxlength="' . $this->passwordPolicy['maxLength'] . '"' : ''?>/>
+    <input id="account_password" type="password" name="password" required aria-required="true" class="form-control"
+      <?=isset($this->passwordPolicy['minLength']) ? ' data-minlength="' . $this->passwordPolicy['minLength'] . '" data-minlength-error="' . $this->escapeHtmlAttr($this->translate('password_minimum_length', array('%%minlength%%' => $this->passwordPolicy['minLength']))) . '"' : ''?>
+      <?=isset($this->passwordPolicy['maxLength']) ? ' maxlength="' . $this->passwordPolicy['maxLength'] . '"' : ''?>
+      <?=$pattern ? ' pattern="' . $pattern . '"' : '' ?>
+    />
+    <? if ($this->passwordPolicy['hint']): ?>
+      <div class="help-block"><?=$this->transEsc($this->passwordPolicy['hint']) ?></div>
+    <? endif; ?>
     <div class="help-block with-errors"></div>
   </div>
 </div>
diff --git a/themes/bootstrap3/templates/Auth/Database/newpassword.phtml b/themes/bootstrap3/templates/Auth/Database/newpassword.phtml
deleted file mode 100644
index 1c6e7e2bfba16481a675fb3fd8a1b234f359c2d9..0000000000000000000000000000000000000000
--- a/themes/bootstrap3/templates/Auth/Database/newpassword.phtml
+++ /dev/null
@@ -1,30 +0,0 @@
-<? if (isset($this->username)): ?>
-  <div class="form-group">
-    <label class="col-sm-3 control-label"><?=$this->transEsc('Username') ?>:</label>
-    <div class="col-sm-9">
-      <p class="form-control-static"><?=$this->username ?></p>
-    </div>
-  </div>
-<? endif; ?>
-<? if (isset($this->verifyold) && $this->verifyold || isset($this->oldpwd)): ?>
-  <div class="form-group">
-    <label class="col-sm-3 control-label"><?=$this->transEsc('old_password') ?>:</label>
-    <div class="col-sm-9">
-      <input type="password" name="oldpwd" class="form-control"/>
-    </div>
-  </div>
-<? endif; ?>
-<div class="form-group">
-  <label class="col-sm-3 control-label"><?=$this->transEsc('new_password') ?>:</label>
-  <div class="col-sm-9">
-    <input type="password" id="password" name="password" class="form-control" required aria-required="true"<?=isset($this->passwordPolicy['minLength']) ? ' data-minlength="' . $this->passwordPolicy['minLength'] . '" data-minlength-error="' . $this->escapeHtmlAttr($this->translate('password_minimum_length', array('%%minlength%%' => $this->passwordPolicy['minLength']))) . '"' : ''?><?=isset($this->passwordPolicy['maxLength']) ? ' maxlength="' . $this->passwordPolicy['maxLength'] . '"' : ''?>/>
-    <div class="help-block with-errors"></div>
-  </div>
-</div>
-<div class="form-group">
-  <label class="col-sm-3 control-label"><?=$this->transEsc('confirm_new_password') ?>:</label>
-  <div class="col-sm-9">
-    <input type="password" name="password2" class="form-control" required aria-required="true" data-match="#password" data-match-error="<?=$this->escapeHtmlAttr($this->translate('Passwords do not match'))?>"/>
-    <div class="help-block with-errors"></div>
-  </div>
-</div>
diff --git a/themes/bootstrap3/templates/Auth/MultiILS/newpassword.phtml b/themes/bootstrap3/templates/Auth/MultiILS/newpassword.phtml
deleted file mode 100644
index 04c0fbce37ed963751cd4dbe8131ab1f727a5745..0000000000000000000000000000000000000000
--- a/themes/bootstrap3/templates/Auth/MultiILS/newpassword.phtml
+++ /dev/null
@@ -1,31 +0,0 @@
-<? if (isset($this->username)): ?>
-  <div class="form-group">
-    <label class="col-sm-3 control-label"><?=$this->transEsc('Username') ?>:</label>
-    <div class="col-sm-9">
-      <p class="form-control-static"><?=$this->username ?></p>
-    </div>
-  </div>
-<? endif; ?>
-<? if (isset($this->verifyold) && $this->verifyold || isset($this->oldpwd)): ?>
-  <div class="form-group">
-    <label class="col-sm-3 control-label"><?=$this->transEsc('old_password') ?>:</label>
-    <div class="col-sm-9">
-      <input type="password" name="oldpwd" class="form-control"/>
-      <div class="help-block with-errors"></div>
-    </div>
-  </div>
-<? endif; ?>
-<div class="form-group">
-  <label class="col-sm-3 control-label"><?=$this->transEsc('new_password') ?>:</label>
-  <div class="col-sm-9">
-    <input type="password" id="password" name="password" class="form-control" required aria-required="true"<?=isset($this->passwordPolicy['minLength']) ? ' data-minlength="' . $this->passwordPolicy['minLength'] . '" data-minlength-error="' . $this->escapeHtmlAttr($this->translate('password_minimum_length', array('%%minlength%%' => $this->passwordPolicy['minLength']))) . '"' : ''?><?=isset($this->passwordPolicy['maxLength']) ? ' maxlength="' . $this->passwordPolicy['maxLength'] . '"' : ''?>/>
-    <div class="help-block with-errors"></div>
-  </div>
-</div>
-<div class="form-group">
-  <label class="col-sm-3 control-label"><?=$this->transEsc('confirm_new_password') ?>:</label>
-  <div class="col-sm-9">
-    <input type="password" name="password2" class="form-control" required aria-required="true" data-match="#password" data-match-error="<?=$this->escapeHtmlAttr($this->translate('Passwords do not match'))?>"/>
-    <div class="help-block with-errors"></div>
-  </div>
-</div>
diff --git a/themes/bootstrap3/templates/Helpers/ils-offline.phtml b/themes/bootstrap3/templates/Helpers/ils-offline.phtml
new file mode 100644
index 0000000000000000000000000000000000000000..886aaa79b34098f3510bb38ab8be03614a324e64
--- /dev/null
+++ b/themes/bootstrap3/templates/Helpers/ils-offline.phtml
@@ -0,0 +1,7 @@
+<div class="alert alert-warning">
+  <h2><?=$this->transEsc('ils_offline_title')?></h2>
+  <p><strong><?=$this->transEsc('ils_offline_status')?></strong></p>
+  <p><?=$this->transEsc($this->offlineModeMsg)?></p>
+  <? $supportEmail = $this->escapeHtmlAttr($this->systemEmail()); ?>
+  <p><a href="mailto:<?=$supportEmail?>"><?=$supportEmail?></a></p>
+</div>
\ No newline at end of file
diff --git a/themes/bootstrap3/templates/Helpers/pagination.phtml b/themes/bootstrap3/templates/Helpers/pagination.phtml
index c7f104b7740025e30d6a03d22285228d7d48fcb7..584ac0d8234abe50dbb028a4f6d5460c57b55c09 100644
--- a/themes/bootstrap3/templates/Helpers/pagination.phtml
+++ b/themes/bootstrap3/templates/Helpers/pagination.phtml
@@ -4,10 +4,10 @@
     <li<? if (isset($this->previous)): ?>>
       <? $newParams = $this->params; $newParams['page'] = $this->previous; ?>
       <a href="<?= $this->currentPath() . '?' . http_build_query($newParams); ?>">
-        &laquo; <?=$this->translate('Previous')?>
+        &laquo; <?=$this->translate('Prev')?>
       </a>
     <? else: ?>
-       class="disabled"> <span>&laquo; <?=$this->translate('Previous')?></span>
+       class="disabled"> <span>&laquo; <?=$this->translate('Prev')?></span>
     <? endif; ?>
     </li>
 
diff --git a/themes/bootstrap3/templates/Recommend/FavoriteFacets.phtml b/themes/bootstrap3/templates/Recommend/FavoriteFacets.phtml
index 8ae7abb8163eb3a1f3746bb5513ed1be10791d8c..c01e8718b409ca5a5d8ebffa249aecb0f1419cf6 100644
--- a/themes/bootstrap3/templates/Recommend/FavoriteFacets.phtml
+++ b/themes/bootstrap3/templates/Recommend/FavoriteFacets.phtml
@@ -11,7 +11,7 @@
       <? foreach ($tagFilterList as $filter): ?>
         <? $removeLink = $this->currentPath().$results->getUrlQuery()->removeFacet($filter['field'], $filter['value']); ?>
         <a class="list-group-item active" href="<?=$removeLink?>">
-          <span class="pull-right flip"><i class="fa fa-minus-circle"></i></span>
+          <span class="pull-right flip"><i class="fa fa-minus-circle" aria-hidden="true"></i></span>
           <?=$this->escapeHtml($filter['displayText'])?>
         </a>
       <? endforeach; ?>
diff --git a/themes/bootstrap3/templates/Recommend/MapSelection.phtml b/themes/bootstrap3/templates/Recommend/MapSelection.phtml
new file mode 100644
index 0000000000000000000000000000000000000000..4170ec5e442b20bcddcb7c2f43dbae0a14c4787b
--- /dev/null
+++ b/themes/bootstrap3/templates/Recommend/MapSelection.phtml
@@ -0,0 +1,43 @@
+<? if ($this->recommend->getSearchResultCoordinates()) :?>
+<?
+  $this->headScript()->appendFile('vendor/ol/ol.js');
+  $this->headScript()->appendFile('map_selection.js');
+  $this->headLink()->appendStylesheet('vendor/ol/ol.css');
+
+  $resultsCoords = $this->recommend->getMapResultCoordinates();
+  $baseUrl = $this->url('home');
+  $geoField = $this->recommend->getGeoField();
+  $urlpath = $this->url('search-results');
+  $coordinates = $this->recommend->getSelectedCoordinates();
+  $showSelection = true;
+  $popupTitle = $this->transEsc('map_results_label');
+  if ($coordinates == null) {
+    $coordinates = $this->recommend->getDefaultCoordinates();
+    $showSelection = false;
+  }
+  $height = $this->recommend->getHeight();
+  $searchParams = $this->recommend->getSearchParams();
+  $params = array(json_encode($geoField), json_encode($coordinates), json_encode($urlpath),
+    json_encode($baseUrl), json_encode($searchParams), json_encode($showSelection),
+    json_encode($resultsCoords), json_encode($popupTitle));
+  $jsParams = implode(', ', $params);
+  $jsLoad = "loadMapSelection(" . $jsParams . ");";
+  $addSearchOption = <<<EOF
+    $('.search-query-options>.advanced').after('&nbsp;&nbsp;<a href="#" class="advanced" onclick=\\'{$jsLoad} $(this).remove(); return false;\\'>{$this->transEsc('Geographic Search')}</a>');
+EOF;
+?>
+<div class="authorbox">
+  <div id="geo_search" style="display: none;">
+    <button id="draw_box"><?=$this->transEsc('Draw Search Box')?></button>
+    <span class="geo_maphelp">&nbsp;<a href="<?=$this->url('help-home')?>?topic=geosearch" data-lightbox class="help-link"><?=$this->transEsc('Need Help?')?></a></span>
+    <div id="geo_search_map" style="height: <?=$height?>px;">
+      <div id="popup"></div>
+    </div>
+  </div>
+  <? if ($showSelection) :?>
+    <?=$this->inlineScript(\Zend\View\Helper\HeadScript::SCRIPT, $jsLoad, 'SET');?>
+  <? else: ?>
+    <?=$this->inlineScript(\Zend\View\Helper\HeadScript::SCRIPT, $addSearchOption, 'SET');?>
+  <? endif; ?>
+</div>
+<? endif; ?>
diff --git a/themes/bootstrap3/templates/Recommend/ResultGoogleMapAjax.phtml b/themes/bootstrap3/templates/Recommend/ResultGoogleMapAjax.phtml
index 6daed32b9a5a1fbccf6a034d0724d7daed06939c..790178cf70a9c9f4f5de7b130bd1b36030ddf090 100644
--- a/themes/bootstrap3/templates/Recommend/ResultGoogleMapAjax.phtml
+++ b/themes/bootstrap3/templates/Recommend/ResultGoogleMapAjax.phtml
@@ -1,8 +1,8 @@
 <?
   $searchParams = $this->recommend->getSearchParams();
 
-  $this->headScript()->appendFile('https://maps.googleapis.com/maps/api/js?v=3.8&sensor=false&language='.$this->layout()->userLang);
-  $this->headScript()->appendFile('https://google-maps-utility-library-v3.googlecode.com/svn/tags/markerclustererplus/2.0.9/src/markerclusterer_packed.js');
+  $this->headScript()->appendFile('https://maps.googleapis.com/maps/api/js?key=' . urlencode($this->recommend->getGoogleMapApiKey()) . '&sensor=false&language='.$this->layout()->userLang);
+  $this->headScript()->appendFile('vendor/markerclusterer.min.js');
 ?>
 <script type="text/javascript">
 /**
@@ -44,7 +44,7 @@ ClusterIcon.prototype.onAdd = function () {
   this.div_ = document.createElement("div");
   this.div_.className = "clusterDiv";
   if (this.visible_) {
-    this.removeClass('hidden');
+    $(this).removeClass('hidden');
   }
 
   this.getPanes().overlayMouseTarget.appendChild(this.div_);
@@ -78,16 +78,6 @@ ClusterIcon.prototype.onAdd = function () {
   });
 };
 
-/**
- * Overriding the image path for ssl
- *
- * The default root name for the marker cluster images.
- *
- * @type {string}
- * @constant
- */
-MarkerClusterer.IMAGE_PATH = "https://google-maps-utility-library-v3.googlecode.com/svn/trunk/markerclustererplus/images/m";
-
 var markers;
 var mc;
 var markersData;
@@ -95,112 +85,121 @@ var latlng;
 var myOptions;
 var map;
 var infowindow = new google.maps.InfoWindow({maxWidth: 480, minWidth: 480});
-  function initialize() {
-    var url = VuFind.path+'/AJAX/json?method=getMapData&<?=$searchParams ?>';
-    //alert('go: ' + url);
-    $.getJSON(url, function(data){
-      //alert(data);
-      markersData = data['data'];
-      if (markersData.length <= 0){
-            return;
-      }
-      latlng = new google.maps.LatLng(0, 0);
-      myOptions = {
-        zoom: 1,
-        center: latlng,
-        mapTypeControl: true,
-        mapTypeControlOptions: {
-            style: google.maps.MapTypeControlStyle.DROPDOWN_MENU
-          },
-        mapTypeId: google.maps.MapTypeId.ROADMAP
-      };
-      map = new google.maps.Map(document.getElementById("map_canvas"),
-          myOptions);
-      //mc = new MarkerClusterer(map);
-      showMarkers();
-      var checkbx = document.getElementById("useCluster");
-      var wrap = document.getElementById("mapWrap");
-      wrap.style.display = "block";
-      checkbx.style.display = "block";
-    });
-  }
-  function showMarkers(){
-    deleteOverlays();
-    if(mc != null) {
-      mc.clearMarkers();
-    }
-    markers = [];
 
-    for (var i = 0; i<markersData.length; i++){
-      var disTitle = markersData[i].title;
-      var iconSize = "0.5";
-      if (disTitle>99){
-          iconSize = "0.75";
-      }
-      var markerImg = "https://chart.googleapis.com/chart?chst=d_map_spin&chld="+iconSize+"|0|F44847|10|_|" +  disTitle;
-      var labelXoffset = 1 + disTitle.length * 4;
-      var latLng = new google.maps.LatLng(markersData[i].lat , markersData[i].lon)
-      var marker = new google.maps.Marker({//MarkerWithLabel
-        loc_facet: markersData[i].location_facet,
-        position: latLng,
-        map: map,
-        title: disTitle,
-        icon: markerImg
-      });
-      google.maps.event.addListener(marker, 'click', function() {
-        infowindow.close();
-        //infowindow.setContent(this.html);
-        //infowindow.open(map, this);
-        load_content(this);
-      });
-      markers.push(marker);
-    }
-    if (document.getElementById("usegmm").checked) {
-      mc = new MarkerClusterer(map, markers);
-    } else {
-      for (var i = 0; i < markers.length; i++) {
-        map.addOverlay(markers[i]);
-      }
+function initialize() {
+  var url = VuFind.path+'/AJAX/json?method=getMapData&<?=$searchParams ?>';
+  //alert('go: ' + url);
+  $.getJSON(url, function(data){
+    //alert(data);
+    markersData = data['data'];
+    if (markersData.length <= 0){
+      return;
     }
+    latlng = new google.maps.LatLng(0, 0);
+    myOptions = {
+      zoom: 1,
+      center: latlng,
+      mapTypeControl: true,
+      mapTypeControlOptions: {
+        style: google.maps.MapTypeControlStyle.DROPDOWN_MENU
+      },
+      mapTypeId: google.maps.MapTypeId.ROADMAP
+    };
+    map = new google.maps.Map(document.getElementById("map_canvas"), myOptions);
+
+    var useGmm = document.getElementById('usegmm');
+    google.maps.event.addDomListener(useGmm, 'click', refreshMap);
+    map.addListener('zoom_changed', refreshMap);
+
+    var checkbx = document.getElementById("useCluster");
+    var wrap = document.getElementById("mapWrap");
+    wrap.style.display = "block";
+    checkbx.style.display = "block";
+
+    showMarkers();
+  });
+}
+
+function showMarkers(){
+  markers = [];
+
+  deleteOverlays();
+  if (mc) {
+    mc.clearMarkers();
   }
-  function load_content(marker){
-    var xmlhttp;
-    if (window.XMLHttpRequest){// code for IE7+, Firefox, Chrome, Opera, Safarihttp://www.google.ie/search?hl=en&cp=10&gs_id=2i&xhr=t&q=php+cast+string+to+int&pq=php+int+to+string&gs_sm=&gs_upl=&bav=on.2,or.r_gc.r_pw.&biw=1876&bih=1020&um=1&ie=UTF-8&tbm=isch&source=og&sa=N&tab=wi
-      xmlhttp=new XMLHttpRequest();
+
+  for (var i = 0; i<markersData.length; i++){
+    var disTitle = markersData[i].title;
+    var iconSize = "0.5";
+    if (disTitle>99){
+      iconSize = "0.75";
     }
-    else{// code for IE6, IE5
-      xmlhttp=new ActiveXObject("Microsoft.XMLHTTP");
+    var markerImg = "https://chart.googleapis.com/chart?chst=d_map_spin&chld="+iconSize+"|0|F44847|10|_|" +  disTitle;
+    var labelXoffset = 1 + disTitle.length * 4;
+    var latLng = new google.maps.LatLng(markersData[i].lat , markersData[i].lon)
+    var marker = new google.maps.Marker({//MarkerWithLabel
+      loc_facet: markersData[i].location_facet,
+      position: latLng,
+      map: map,
+      title: disTitle,
+      icon: markerImg
+    });
+    google.maps.event.addListener(marker, 'click', function() {
+      infowindow.close();
+      //infowindow.setContent(this.html);
+      //infowindow.open(map, this);
+      load_content(this);
+    });
+    markers.push(marker);
+  }
+  if (document.getElementById("usegmm").checked) {
+    mc = new MarkerClusterer(map, markers);
+  } else {
+    for (var i = 0; i < markers.length; i++) {
+      markers[i].setMap(map);
     }
-    var ajaxUrl = VuFind.path+'/AJAX/ResultGoogleMapInfo?limit=5&filter[]=long_lat%3A"' + marker.loc_facet+'"&<?=$searchParams ?>';
-    xmlhttp.open("GET", ajaxUrl, false);
-    xmlhttp.send();
+  }
+}
 
-    infowindow.setContent(xmlhttp.responseText);
+function load_content(marker){
+  var ajaxUrl = VuFind.path+'/AJAX/ResultGoogleMapInfo?limit=5&filter[]=long_lat%3A"' + marker.loc_facet+'"&<?=$searchParams ?>';
+  $.ajax(ajaxUrl, { dataType: 'html' }).done(function (html) {
+    infowindow.setContent(html);
     infowindow.open(map, marker);
+  });
+}
+
+function deleteOverlays() {
+  if (markers) {
+    for (i in markers) {
+      markers[i].setMap(null);
+    }
+    markers.length = 0;
   }
-  function deleteOverlays() {
-      if (markers) {
-        for (i in markers) {
-          markers[i].setMap(null);
-        }
-        markers.length = 0;
-      }
-  }
-  function refreshMap() {
-    showMarkers();
-  }
+}
+
+function refreshMap() {
+  deleteOverlays();
+  showMarkers();
+  // The following two lines are a workaround for a library bug that causes
+  // clusters to disappear after zoom and other events; these may be able to be
+  // removed along with the zoom_changed event hook above in the future if the
+  // library is fixed.
+  mapcenter = map.getCenter();
+  map.setCenter(new google.maps.LatLng((mapcenter.lat() + 0.0000001), mapcenter.lng()));
+}
 
-  google.maps.event.addDomListener(window, 'load', initialize);
+google.maps.event.addDomListener(window, 'load', initialize);
 </script>
 <div id="mapWrap" onload="initialize()" style="height:479px;display:none;margin-bottom:1em" class="clearfix">
   <div id="map_canvas" style="width: 100%; height: 100%"></div>
   <div class="mapClusterToggle" id="useCluster">
     <div class="checkbox">
       <label for="usegmm">
-        <input type="checkbox" id="usegmm" checked="true" onclick="refreshMap();"></input>
+        <input type="checkbox" id="usegmm" checked="true"></input>
         <?=$this->transEsc('google_map_cluster_points') ?>
       </label>
     </div>
   </div>
 </div>
-<br/>
\ No newline at end of file
+<br/>
diff --git a/themes/bootstrap3/templates/Recommend/SideFacets.phtml b/themes/bootstrap3/templates/Recommend/SideFacets.phtml
index f8a35dee5158f67256610129e3bd647c05e6e209..bc3233564ae6896e543b5ae93cc7b434f5ec0f6e 100644
--- a/themes/bootstrap3/templates/Recommend/SideFacets.phtml
+++ b/themes/bootstrap3/templates/Recommend/SideFacets.phtml
@@ -1,4 +1,9 @@
-<? $results = $this->recommend->getResults(); ?>
+<?
+  $this->headScript()->appendFile('facets.js');
+
+  $results = $this->recommend->getResults();
+  $options = $this->searchOptions($this->searchClassId);
+?>
 <? if ($results->getResultTotal() > 0): ?>
   <h4><?=$this->transEsc(isset($this->overrideSideFacetCaption) ? $this->overrideSideFacetCaption : 'Narrow Search')?></h4>
 <? endif; ?>
@@ -43,8 +48,8 @@
             $filter['displayText'] = $this->translate('filter_wildcard');
           }
         ?>
-        <a class="list-group-item active" href="<?=$removeLink?>">
-          <span class="pull-right flip"><i class="fa fa-times"></i></span>
+        <a class="list-group-item active" href="<?=$removeLink?>" title="<?=$this->transEsc('clear_tag_filter') ?>">
+          <span class="pull-right flip"><i class="fa fa-times" aria-hidden="true"></i></span>
           <? if ($filter['operator'] == 'NOT') echo $this->transEsc('NOT') . ' '; if ($filter['operator'] == 'OR' && $i > 0) echo $this->transEsc('OR') . ' '; ?><?=$this->transEsc($field)?>: <?=$this->escapeHtml($filter['displayText'])?>
         </a>
       <? endforeach; ?>
@@ -57,6 +62,7 @@
   <? foreach ($sideFacetSet as $title => $cluster): ?>
     <? $allowExclude = $this->recommend->excludeAllowed($title); ?>
     <? $facets_before_more = $this->recommend->getShowMoreSetting($title); ?>
+    <? $showMoreInLightbox = $this->recommend->getShowInLightboxSetting($title); ?>
     <div class="list-group facet" id="side-panel-<?=$this->escapeHtmlAttr($title) ?>">
       <div class="list-group-item title<? if(in_array($title, $collapsedFacets)): ?> collapsed<? endif ?>" data-toggle="collapse" href="#side-collapse-<?=$this->escapeHtmlAttr($title) ?>" >
         <?=$this->transEsc($cluster['label'])?>
@@ -90,6 +96,7 @@
           </div>
           <? if ($rangeFacets[$title]['type'] == 'date'): ?>
             <? $this->headScript()->appendFile('vendor/bootstrap-slider.js'); ?>
+            <? $this->headLink()->appendStylesheet('vendor/bootstrap-slider.min.css'); ?>
             <?
               $min = !empty($rangeFacets[$title]['values'][0]) ? min($rangeFacets[$title]['values'][0], 1400) : 1400;
               $future = date('Y', time()+31536000);
@@ -113,7 +120,7 @@ $(document).ready(function() {
     'value':[{$low},{$high}],
     'reversed': {$reversed}
   })
-  .on('slide', fillTexts)
+  .on('change', fillTexts)
   .data('slider');
 });
 JS;
@@ -123,7 +130,6 @@ JS;
         <? else: ?>
           <? if (in_array($title, $hierarchicalFacets)): ?>
             <? $this->headScript()->appendFile('vendor/jsTree/jstree.min.js'); ?>
-            <? $this->headScript()->appendFile('facets.js'); ?>
             <? $sort = isset($hierarchicalFacetSortOptions[$title]) ? $hierarchicalFacetSortOptions[$title] : ''; ?>
             <? if (!in_array($title, $collapsedFacets)): ?>
               <?
@@ -162,14 +168,25 @@ JS;
               ?>
               <? $moreClass = 'narrowGroupHidden-'.$this->escapeHtmlAttr($title).' hidden'; ?>
             <? if ($i == $facets_before_more): ?>
-              <a id="more-narrowGroupHidden-<?=$this->escapeHtmlAttr($title)?>" class="list-group-item narrow-toggle" href="javascript:moreFacets('narrowGroupHidden-<?=$this->escapeHtmlAttr($title) ?>')"><?=$this->transEsc('more')?> ...</a>
+              <? $idAndClass = 'id="more-narrowGroupHidden-'.$this->escapeHtmlAttr($title).'" class="list-group-item narrow-toggle"'; ?>
+              <? if ($facetLightbox = $options->getFacetListAction()): ?>
+                <? $moreUrl = $this->url($facetLightbox) . $results->getUrlQuery()->getParams().'&amp;facet='.$title.'&amp;facetop='.$thisFacet['operator'].'&amp;facetexclude='.($allowExclude ? 1 : 0); ?>
+              <? else: ?>
+                <? $moreUrl = '#'; ?>
+              <? endif; ?>
+              <? if (($showMoreInLightbox && $showMoreInLightbox !== 'more') && $facetLightbox): ?>
+                <a <?=$idAndClass ?> data-lightbox href="<?=$moreUrl ?>" rel="nofollow"><?=$this->transEsc('more')?> ...</a>
+                <? break; ?>
+              <? else: ?>
+                <a <?=$idAndClass ?> href="<?=$moreUrl ?>" onclick="return moreFacets('narrowGroupHidden-<?=$this->escapeHtmlAttr($title) ?>')" rel="nofollow"><?=$this->transEsc('more')?> ...</a>
+              <? endif; ?>
             <? endif; ?>
             <? if ($thisFacet['isApplied']): ?>
-              <a class="list-group-item active<? if ($i >= $facets_before_more): ?><?=$moreClass ?><?endif ?><? if ($thisFacet['operator'] == 'OR'): ?> facetOR applied<? endif ?>" href="<?=$this->currentPath().$results->getUrlQuery()->removeFacet($title, $thisFacet['value'], true, $thisFacet['operator']) ?>">
+              <a class="list-group-item active<? if ($i >= $facets_before_more): ?><?=$moreClass ?><?endif ?><? if ($thisFacet['operator'] == 'OR'): ?> facetOR applied<? endif ?>" href="<?=$this->currentPath().$results->getUrlQuery()->removeFacet($title, $thisFacet['value'], true, $thisFacet['operator']) ?>" title="<?=$this->transEsc('applied_filter') ?>">
                 <? if ($thisFacet['operator'] == 'OR'): ?>
-                  <i class="fa fa-check-square-o"></i>
+                  <i class="fa fa-check-square-o" aria-hidden="true"></i>
                 <? else: ?>
-                  <span class="pull-right flip"><i class="fa fa-check"></i></span>
+                  <span class="pull-right flip"><i class="fa fa-check" aria-hidden="true"></i></span>
                 <? endif; ?>
                 <?=$this->escapeHtml($thisFacet['displayText'])?>
               </a>
@@ -183,14 +200,14 @@ JS;
               <span class="badge">
                 <?=$this->localizedNumber($thisFacet['count'])?>
                 <? if ($allowExclude): ?>
-                  <a href="<?=$this->currentPath().$results->getUrlQuery()->addFacet($title, $thisFacet['value'], 'NOT') ?>" title="<?=$this->transEsc('exclude_facet') ?>"><i class="fa fa-times"></i></a>
+                  <a href="<?=$this->currentPath().$results->getUrlQuery()->addFacet($title, $thisFacet['value'], 'NOT') ?>" title="<?=$this->transEsc('exclude_facet') ?>"><i class="fa fa-times" aria-hidden="true"></i></a>
                 <? endif; ?>
               </span>
               <? if ($allowExclude): ?>
                 <a href="<?=$addURL ?>">
               <? endif; ?>
               <? if($thisFacet['operator'] == 'OR'): ?>
-                <i class="fa fa-square-o"></i>
+                <i class="fa fa-square-o" aria-hidden="true"></i>
               <? endif; ?>
               <?=$this->escapeHtml($thisFacet['displayText'])?>
               <? if ($allowExclude): ?>
@@ -201,7 +218,11 @@ JS;
               <? endif; ?>
             <? endif; ?>
           <? endforeach; ?>
-          <? if ($i >= $facets_before_more): ?><a class="list-group-item narrow-toggle <?=$moreClass ?>" href="javascript:lessFacets('narrowGroupHidden-<?=$this->escapeHtmlAttr($title) ?>')"><?=$this->transEsc('less')?> ...</a><? endif; ?>
+          <? if ($showMoreInLightbox === 'more' && $facetLightbox = $options->getFacetListAction()): ?>
+            <? $moreUrl = $this->url($facetLightbox) . $results->getUrlQuery()->getParams().'&amp;facet='.$title.'&amp;facetop='.$thisFacet['operator'].'&amp;facetexclude='.($allowExclude ? 1 : 0); ?>
+            <a class="list-group-item narrow-toggle <?=$moreClass ?>" data-lightbox href="<?=$moreUrl ?>" rel="nofollow"><?=$this->transEsc('see all')?> ...</a>
+          <? endif; ?>
+          <? if ($i >= $facets_before_more): ?><a class="list-group-item narrow-toggle <?=$moreClass ?>" href="#" onclick="return lessFacets('narrowGroupHidden-<?=$this->escapeHtmlAttr($title) ?>')"><?=$this->transEsc('less')?> ...</a><? endif; ?>
         <? endif; ?>
         <? if (in_array($title, $hierarchicalFacets)): ?>
           </noscript>
@@ -209,4 +230,4 @@ JS;
       </div>
     </div>
   <? endforeach; ?>
-<? endif; ?>
\ No newline at end of file
+<? endif; ?>
diff --git a/themes/bootstrap3/templates/Recommend/SummonBestBetsDeferred.phtml b/themes/bootstrap3/templates/Recommend/SummonBestBetsDeferred.phtml
index ef315eb767c576ac22d981d59cca5af909863573..52acf074161dc152fd67284b523ade1bbe09de18 100644
--- a/themes/bootstrap3/templates/Recommend/SummonBestBetsDeferred.phtml
+++ b/themes/bootstrap3/templates/Recommend/SummonBestBetsDeferred.phtml
@@ -4,6 +4,6 @@
     . "\$('#SummonDeferredBestBets').load(url);";
 ?>
 <div id="SummonDeferredBestBets">
-  <p><?=$this->transEsc("Loading")?>... <i class="fa fa-spinner"></i></p>
+  <p><?=$this->transEsc("Loading")?>... <i class="fa fa-spinner" aria-hidden="true"></i></p>
   <?=$this->inlineScript(\Zend\View\Helper\HeadScript::SCRIPT, $loadJs, 'SET')?>
 </div>
\ No newline at end of file
diff --git a/themes/bootstrap3/templates/Recommend/SummonDatabasesDeferred.phtml b/themes/bootstrap3/templates/Recommend/SummonDatabasesDeferred.phtml
index 0b9f2caa70d20cfe1852ebfcf944a1b566964b51..ef58352d86afaab2311699f5e6f40103a9a749cc 100644
--- a/themes/bootstrap3/templates/Recommend/SummonDatabasesDeferred.phtml
+++ b/themes/bootstrap3/templates/Recommend/SummonDatabasesDeferred.phtml
@@ -4,6 +4,6 @@
     . "\$('#SummonDeferredDatabases').load(url);";
 ?>
 <div id="SummonDeferredDatabases">
-  <p><?=$this->transEsc("Loading")?>... <i class="fa fa-spinner"></i></p>
+  <p><?=$this->transEsc("Loading")?>... <i class="fa fa-spinner" aria-hidden="true"></i></p>
   <?=$this->inlineScript(\Zend\View\Helper\HeadScript::SCRIPT, $loadJs, 'SET')?>
 </div>
\ No newline at end of file
diff --git a/themes/bootstrap3/templates/Recommend/SummonResultsDeferred.phtml b/themes/bootstrap3/templates/Recommend/SummonResultsDeferred.phtml
index e97c79492b2c3998a02bef5818aefcc35a94feca..991f8baa00e5b6c1142c298d343eef650f1a2cd0 100644
--- a/themes/bootstrap3/templates/Recommend/SummonResultsDeferred.phtml
+++ b/themes/bootstrap3/templates/Recommend/SummonResultsDeferred.phtml
@@ -5,6 +5,6 @@
 ?>
 <div id="SummonDeferredRecommend">
   <h3><?=$this->transEsc("Summon Results")?></h3>
-  <p><?=$this->transEsc("Loading")?>... <i class="fa fa-spinner"></i></p>
+  <p><?=$this->transEsc("Loading")?>... <i class="fa fa-spinner" aria-hidden="true"></i></p>
   <?=$this->inlineScript(\Zend\View\Helper\HeadScript::SCRIPT, $loadJs, 'SET')?>
 </div>
\ No newline at end of file
diff --git a/themes/bootstrap3/templates/Recommend/TopFacets.phtml b/themes/bootstrap3/templates/Recommend/TopFacets.phtml
index 75ea5aacb9ec7527324698929d81d8491f5b4cac..c35d26b21a80197d788d62e17354bb22534dc8ce 100644
--- a/themes/bootstrap3/templates/Recommend/TopFacets.phtml
+++ b/themes/bootstrap3/templates/Recommend/TopFacets.phtml
@@ -27,12 +27,12 @@
             $removeLink = $this->currentPath().$results->getUrlQuery()->removeFacet($title, $thisFacet['value'], true, $thisFacet['operator']);
           } ?>
           <a href="<?=$removeLink ?>" class="applied">
-            <?=$this->escapeHtml($thisFacet['displayText'])?> <i class="fa fa-check"></i>
+            <?=$this->escapeHtml($thisFacet['displayText'])?> <i class="fa fa-check" aria-hidden="true"></i>
           </a>
         <? else: ?>
           <a href="<?=$this->currentPath().$results->getUrlQuery()->addFacet($title, $thisFacet['value'], $thisFacet['operator'])?>"><?=$this->escapeHtml($thisFacet['displayText'])?></a> <span class="badge"><?=$this->localizedNumber($thisFacet['count']) ?>
           <? if ($allowExclude): ?>
-            <a href="<?=$this->currentPath().$results->getUrlQuery()->addFacet($title, $thisFacet['value'], 'NOT')?>" title="<?=$this->transEsc('exclude_facet')?>"><i class="fa fa-times"></i></a>
+            <a href="<?=$this->currentPath().$results->getUrlQuery()->addFacet($title, $thisFacet['value'], 'NOT')?>" title="<?=$this->transEsc('exclude_facet')?>"><i class="fa fa-times" aria-hidden="true"></i></a>
           <? endif; ?>
           </span>
         <? endif; ?>
diff --git a/themes/bootstrap3/templates/RecordDriver/EDS/core.phtml b/themes/bootstrap3/templates/RecordDriver/EDS/core.phtml
index a5dfb0931484b7c6ac58f2e81047e5fd556f50f3..812ac1daa9edcedcbed94e9217b7056113f2e6b0 100644
--- a/themes/bootstrap3/templates/RecordDriver/EDS/core.phtml
+++ b/themes/bootstrap3/templates/RecordDriver/EDS/core.phtml
@@ -9,7 +9,7 @@
     $restrictedView = empty($accessLevel) ? false : true;
 ?>
 <div class="row" vocab="http://schema.org/" resource="#record" typeof="<?=$this->driver->getSchemaOrgFormats()?> Product">
-  <div class="col-sm-3">
+  <div class="col-sm-3 img-col">
     <? if ($thumb): ?>
         <img src="<?=$this->escapeHtmlAttr($thumb)?>" class="recordcover" alt="<?=$this->transEsc('Cover Image')?>"/>
     <? else: ?>
@@ -47,7 +47,7 @@
       <? endif; ?>
     </div>
   </div>
-  <div class="col-sm-9">
+  <div class="col-sm-9 info-col">
     <h3 property="name"><?=$this->driver->getTitle()?></h3>
 
     <table class="table table-striped" summary="<?=$this->transEsc('Bibliographic Details')?>">
diff --git a/themes/bootstrap3/templates/RecordDriver/EDS/result-list.phtml b/themes/bootstrap3/templates/RecordDriver/EDS/result-list.phtml
index c7ee425fce400e3ea98d365e7dd3238f8be63aeb..8534df4198a271c5dedfdae02de610c35e14f743 100644
--- a/themes/bootstrap3/templates/RecordDriver/EDS/result-list.phtml
+++ b/themes/bootstrap3/templates/RecordDriver/EDS/result-list.phtml
@@ -2,119 +2,136 @@
   $this->headLink()->appendStylesheet('EDS.css');
   $accessLevel = $this->driver->getAccessLevel();
   $restrictedView = empty($accessLevel) ? false : true;
+  $coverDetails = $this->record($this->driver)->getCoverDetails('result-list', 'medium', $this->recordLink()->getUrl($this->driver));
 ?>
-<div class="row col-xs-11 source<?=$this->escapeHtmlAttr($this->driver->getSourceIdentifier())?> recordId<?=$this->driver->supportsAjaxStatus()?' ajaxItemId':''?>">
-  <input type="hidden" value="<?=$this->escapeHtmlAttr($this->driver->getUniqueID())?>" class="hiddenId" />
-  <div class="col-sm-2 left">
-    <? if ($summThumb = $this->record($this->driver)->getThumbnail()): ?>
-        <a href="<?=$this->recordLink()->getUrl($this->driver)?>" class="_record_link">
-        <img src="<?=$this->escapeHtmlAttr($summThumb)?>" class="recordcover" alt="<?=$this->transEsc('Cover Image')?>"/>
-        </a>
+<?
+  $thumbnail = false;
+  $thumbnailAlignment = $this->record($this->driver)->getThumbnailAlignment('result');
+  ob_start(); ?>
+  <div class="media-<?=$thumbnailAlignment ?> <?=$this->escapeHtml($coverDetails['size'])?>">
+    <? if ($coverDetails['cover']): ?>
+      <a href="<?=$this->recordLink()->getUrl($this->driver)?>" class="_record_link">
+        <img src="<?=$this->escapeHtmlAttr($coverDetails['cover'])?>" class="recordcover" alt="<?=$this->transEsc('Cover Image')?>"/>
+      </a>
     <? else: ?>
-        <span class="recordcover pt-icon pt-<?=$this->driver->getPubTypeId()?>"></span>
-        <div><?=$this->driver->getPubType()?></div>
+      <span class="recordcover pt-icon pt-<?=$this->driver->getPubTypeId()?>"></span>
+      <div><?=$this->driver->getPubType()?></div>
     <? endif; ?>
   </div>
-  <div class="col-sm-7 middle">
-    <?  $items  =  $this->driver->getItems();
-      if (isset($items) && !empty($items)) :
-        foreach ($items as $item):
-          if (!empty($item)): ?>
+<? $thumbnail = ob_get_contents(); ?>
+<? ob_end_clean(); ?>
+<input type="hidden" value="<?=$this->escapeHtmlAttr($this->driver->getUniqueID())?>" class="hiddenId" />
+<input type="hidden" value="<?=$this->escapeHtmlAttr($this->driver->getSourceIdentifier())?>" class="hiddenSource" />
+<div class="media<?=$this->driver->supportsAjaxStatus()?' ajaxItem':''?>">
+  <? if ($thumbnail && $thumbnailAlignment == 'left'): ?>
+    <?=$thumbnail ?>
+  <? endif; ?>
+  <div class="media-body">
+    <div class="row short-view">
+      <div class="col-sm-8 middle">
+        <? $items = $this->driver->getItems();
+          if (isset($items) && !empty($items)):
+            foreach ($items as $item):
+              if (!empty($item)): ?>
+                <div class="resultItemLine1">
+                  <? if('Ti' == $item['Group']): ?>
+                    <a href="<?=$this->recordLink()->getUrl($this->driver)?>" class="title getFull _record_link"  data-view="<?=$this->params->getOptions()->getListViewOption()?>">
+                    <?=$item['Data']?> </a>
+                  <? else:?>
+                    <p>
+                      <b><?=$this->transEsc($item['Label'])?>:</b>
+                      <?=$item['Data']?>
+                    </p>
+                  <? endif;?>
+                </div>
+              <? endif;
+            endforeach;
+          elseif ($restrictedView): ?>
             <div class="resultItemLine1">
-              <?if('Ti' == $item['Group']): ?>
-                <a href="<?=$this->recordLink()->getUrl($this->driver)?>" class="title _record_link" >
-                <?=$item['Data']?> </a>
-              <?else:?>
-                <p>
-                  <b><?=$this->transEsc($item['Label'])?>:</b>
-                  <?=$item['Data']?>
-                </p>
-              <?endif;?>
+              <p>
+                <?=$this->transEsc('This result is not displayed to guests')?>
+                <br />
+                <a class="login" href="<?=$this->url('myresearch-home')?>">
+                  <strong><?=$this->transEsc('Login for full access')?></strong>
+                </a>
+              </p>
             </div>
-          <? endif;
-        endforeach;
-      elseif ($restrictedView): ?>
-        <div class="resultItemLine1">
-          <p>
-            <?=$this->transEsc('This result is not displayed to guests')?>
-            <br />
-            <a class="login" href="<?=$this->url('myresearch-home')?>">
-              <strong><?=$this->transEsc('Login for full access')?></strong>
-            </a>
-          </p>
-        </div>
-      <? endif; ?>
+          <? endif; ?>
 
-  <div class="resultItemLine4 custom-links">
-    <? $customLinks = array_merge($this->driver->getFTCustomLinks(), $this->driver->getCustomLinks());
-    if (!empty($customLinks)): ?>
-      <? foreach ($customLinks as $customLink): ?>
-      <? $url = isset($customLink['Url']) ? $customLink['Url'] : '';
-          $mot = isset($customLink['MouseOverText'])? $customLink['MouseOverText'] : '';
-          $icon = isset ($customLink['Icon']) ? $customLink['Icon'] : '';
-          $name = isset($customLink['Text']) ? $customLink['Text'] : '';
-      ?>
-      <span>
-        <a href="<?=$this->escapeHtmlAttr($url)?>" target="_blank" title="<?=$this->escapeHtmlAttr($mot)?>" class="custom-link">
-          <? if ($icon): ?><img src="<?=$this->escapeHtmlAttr($icon)?>" /> <? endif; ?><?=$this->escapeHtml($name)?>
-        </a>
-      </span>
-      <? endforeach; ?>
-    <? endif; ?>
-    </div>
+        <div class="resultItemLine4 custom-links">
+          <? $customLinks = array_merge($this->driver->getFTCustomLinks(), $this->driver->getCustomLinks());
+          if (!empty($customLinks)): ?>
+            <? foreach ($customLinks as $customLink): ?>
+            <? $url = isset($customLink['Url']) ? $customLink['Url'] : '';
+                $mot = isset($customLink['MouseOverText'])? $customLink['MouseOverText'] : '';
+                $icon = isset ($customLink['Icon']) ? $customLink['Icon'] : '';
+                $name = isset($customLink['Text']) ? $customLink['Text'] : '';
+            ?>
+            <span>
+              <a href="<?=$this->escapeHtmlAttr($url)?>" target="_blank" title="<?=$this->escapeHtmlAttr($mot)?>" class="custom-link">
+                <? if ($icon): ?><img src="<?=$this->escapeHtmlAttr($icon)?>" /> <? endif; ?><?=$this->escapeHtml($name)?>
+              </a>
+            </span>
+            <? endforeach; ?>
+          <? endif; ?>
+        </div>
 
-    <? if ($this->driver->hasHTMLFullTextAvailable()): ?>
-      <a href="<?= $this->recordLink()->getUrl($this->driver, 'fulltext') ?>#html" class="icon html fulltext _record_link" target="_blank">
-        <?=$this->transEsc('HTML Full Text')?>
-      </a>
-      &nbsp; &nbsp;
-    <? endif; ?>
+        <? if ($this->driver->hasHTMLFullTextAvailable()): ?>
+          <a href="<?= $this->recordLink()->getUrl($this->driver, 'fulltext') ?>#html" class="icon html fulltext _record_link" target="_blank">
+            <?=$this->transEsc('HTML Full Text')?>
+          </a>
+          &nbsp; &nbsp;
+        <? endif; ?>
 
-    <? if ($this->driver->hasPdfAvailable()): ?>
-      <a href="<?= $this->recordLink()->getUrl($this->driver).'/PDF'; ?>" class="icon pdf fulltext" target="_blank">
-        <?=$this->transEsc('PDF Full Text')?>
-      </a>
-    <? endif; ?>
-  </div>
+        <? if ($this->driver->hasPdfAvailable()): ?>
+          <a href="<?= $this->recordLink()->getUrl($this->driver).'/PDF'; ?>" class="icon pdf fulltext" target="_blank">
+            <?=$this->transEsc('PDF Full Text')?>
+          </a>
+        <? endif; ?>
+      </div>
+      <div class="col-sm-4 right hidden-print">
+        <? /* Display qrcode if appropriate: */ ?>
+        <? if ($QRCode = $this->record($this->driver)->getQRCode("results")): ?>
+          <?
+            // Add JS Variables for QrCode
+            $this->jsTranslations()->addStrings(array('qrcode_hide' => 'qrcode_hide', 'qrcode_show' => 'qrcode_show'));
+          ?>
+          <span class="hidden-xs">
+            <i class="fa fa-fw fa-qrcode" aria-hidden="true"></i> <a href="<?=$this->escapeHtmlAttr($QRCode);?>" class="qrcodeLink"><?=$this->transEsc('qrcode_show')?></a>
+            <div class="qrcode hidden">
+              <script type="text/template" class="qrCodeImgTag">
+                <img alt="<?=$this->transEsc('QR Code')?>" src="<?=$this->escapeHtmlAttr($QRCode);?>"/>
+              </script>
+            </div><br/>
+          </span>
+        <? endif; ?>
 
-  <div class="col-sm-2 right hidden-print">
-    <? /* Display qrcode if appropriate: */ ?>
-    <? if ($QRCode = $this->record($this->driver)->getQRCode("results")): ?>
-      <?
-        // Add JS Variables for QrCode
-        $this->jsTranslations()->addStrings(array('qrcode_hide' => 'qrcode_hide', 'qrcode_show' => 'qrcode_show'));
-      ?>
-      <span class="hidden-xs">
-        <i class="fa fa-fw fa-qrcode"></i> <a href="<?=$this->escapeHtmlAttr($QRCode);?>" class="qrcodeLink"><?=$this->transEsc('qrcode_show')?></a>
-        <div class="qrcode hidden">
-          <script type="text/template" class="qrCodeImgTag">
-            <img alt="<?=$this->transEsc('QR Code')?>" src="<?=$this->escapeHtmlAttr($QRCode);?>"/>
-          </script>
-        </div><br/>
-      </span>
-    <? endif; ?>
+        <? if ($this->userlist()->getMode() !== 'disabled'): ?>
+          <? /* Add to favorites */ ?>
+          <i class="fa fa-fw fa-star" aria-hidden="true"></i> <a href="<?=$this->recordLink()->getActionUrl($this->driver, 'Save')?>" class="save-record" data-lightbox id="<?=$this->driver->getUniqueId() ?>" title="<?=$this->transEsc('Add to favorites')?>"><?=$this->transEsc('Add to favorites')?></a><br/>
 
-    <? if ($this->userlist()->getMode() !== 'disabled'): ?>
-      <? /* Add to favorites */ ?>
-      <i class="fa fa-fw fa-star"></i> <a href="<?=$this->recordLink()->getActionUrl($this->driver, 'Save')?>" class="save-record" data-lightbox id="<?=$this->driver->getUniqueId() ?>" title="<?=$this->transEsc('Add to favorites')?>"><?=$this->transEsc('Add to favorites')?></a><br/>
+          <? /* Saved lists */ ?>
+          <div class="savedLists alert alert-info hidden">
+            <strong><?=$this->transEsc("Saved in")?>:</strong>
+          </div>
+        <? endif; ?>
 
-      <? /* Saved lists */ ?>
-      <div class="savedLists alert alert-info hidden">
-        <strong><?=$this->transEsc("Saved in")?>:</strong>
+        <? /* Hierarchy tree link */ ?>
+        <? $trees = $this->driver->tryMethod('getHierarchyTrees'); if (!empty($trees)): ?>
+          <? foreach ($trees as $hierarchyID => $hierarchyTitle): ?>
+            <div class="hierarchyTreeLink">
+              <input type="hidden" value="<?=$this->escapeHtmlAttr($hierarchyID)?>" class="hiddenHierarchyId" />
+              <i class="fa fa-fw fa-sitemap" aria-hidden="true"></i>
+              <a class="hierarchyTreeLinkText" data-lightbox href="<?=$this->recordLink()->getTabUrl($this->driver, 'HierarchyTree')?>?hierarchy=<?=urlencode($hierarchyID)?>#tabnav" title="<?=$this->transEsc('hierarchy_tree')?>">
+                <?=$this->transEsc('hierarchy_view_context')?><? if (count($trees) > 1): ?>: <?=$this->escapeHtml($hierarchyTitle)?><? endif; ?>
+              </a>
+            </div>
+          <? endforeach; ?>
+        <? endif; ?>
       </div>
-    <? endif; ?>
-
-    <? /* Hierarchy tree link */ ?>
-    <? $trees = $this->driver->tryMethod('getHierarchyTrees'); if (!empty($trees)): ?>
-      <? foreach ($trees as $hierarchyID => $hierarchyTitle): ?>
-        <div class="hierarchyTreeLink">
-          <input type="hidden" value="<?=$this->escapeHtmlAttr($hierarchyID)?>" class="hiddenHierarchyId" />
-          <i class="fa fa-fw fa-sitemap"></i>
-          <a class="hierarchyTreeLinkText" data-lightbox href="<?=$this->recordLink()->getTabUrl($this->driver, 'HierarchyTree')?>?hierarchy=<?=urlencode($hierarchyID)?>#tabnav" title="<?=$this->transEsc('hierarchy_tree')?>">
-            <?=$this->transEsc('hierarchy_view_context')?><? if (count($trees) > 1): ?>: <?=$this->escapeHtml($hierarchyTitle)?><? endif; ?>
-          </a>
-        </div>
-      <? endforeach; ?>
-    <? endif; ?>
+    </div>
   </div>
-</div>
\ No newline at end of file
+  <? if ($thumbnail && $thumbnailAlignment == 'right'): ?>
+    <?=$thumbnail ?>
+  <? endif; ?>
+</div>
diff --git a/themes/bootstrap3/templates/RecordDriver/LibGuides/result-list.phtml b/themes/bootstrap3/templates/RecordDriver/LibGuides/result-list.phtml
index eda74148212a3ea029104ff21488f616df98aad8..a422c928ad85f07472410cdeee6d8c50aae889b8 100644
--- a/themes/bootstrap3/templates/RecordDriver/LibGuides/result-list.phtml
+++ b/themes/bootstrap3/templates/RecordDriver/LibGuides/result-list.phtml
@@ -1,10 +1,12 @@
 <?
   $url = $this->driver->getUniqueId();
 ?>
-<div class="listentry col-xs-11">
-  <div class="resultItemLine1">
-    <a href="<?=$this->escapeHtmlAttr($url)?>" class="title">
-      <?=$this->record($this->driver)->getTitleHtml()?>
-    </a>
+<div class="media">
+  <div class="media-body">
+    <div class="resultItemLine1">
+      <a href="<?=$this->escapeHtmlAttr($url)?>" class="title">
+        <?=$this->record($this->driver)->getTitleHtml()?>
+      </a>
+    </div>
   </div>
-</div>
\ No newline at end of file
+</div>
diff --git a/themes/bootstrap3/templates/RecordDriver/Pazpar2/result-list.phtml b/themes/bootstrap3/templates/RecordDriver/Pazpar2/result-list.phtml
index 7bb554ef9ee0e479a49d679194d81f23c76a3dd7..f59217ae4f1620d3ae424252880b6b2fa82976dc 100644
--- a/themes/bootstrap3/templates/RecordDriver/Pazpar2/result-list.phtml
+++ b/themes/bootstrap3/templates/RecordDriver/Pazpar2/result-list.phtml
@@ -1,105 +1,112 @@
-<div class="result source<?=$this->escapeHtmlAttr($this->driver->getSourceIdentifier())?> recordId<?=$this->driver->supportsAjaxStatus()?' ajaxItemId':''?> col-xs-11">
-  <div class="row">
+<?
+  $coverDetails = $this->record($this->driver)->getCoverDetails('result-list', 'medium', $this->recordLink()->getUrl($this->driver));
+  $cover = $coverDetails['html'];
+  $thumbnail = false;
+  $thumbnailAlignment = $this->record($this->driver)->getThumbnailAlignment('result');
+  if ($cover):
+    ob_start(); ?>
+    <div class="media-<?=$thumbnailAlignment ?> <?=$this->escapeHtmlAttr($coverDetails['size'])?>">
+      <?=$cover ?>
+    </div>
+    <? $thumbnail = ob_get_contents(); ?>
+  <? ob_end_clean(); ?>
+<? endif; ?>
+<input type="hidden" value="<?=$this->escapeHtmlAttr($this->driver->getUniqueID())?>" class="hiddenId" />
+<input type="hidden" value="<?=$this->escapeHtmlAttr($this->driver->getSourceIdentifier())?>" class="hiddenSource" />
+<div class="media<?=$this->driver->supportsAjaxStatus()?' ajaxItem':''?>">
+  <? if ($thumbnail && $thumbnailAlignment == 'left'): ?>
+    <?=$thumbnail ?>
+  <? endif; ?>
+  <div class="media-body">
     <div>
-      <input type="hidden" value="<?=$this->escapeHtmlAttr($this->driver->getUniqueID())?>" class="hiddenId" />
-      <input type="hidden" value="<?=$this->escapeHtmlAttr($this->driver->getSourceIdentifier())?>" class="hiddenSource" />
+      <b>
+        <?=$this->record($this->driver)->getTitleHtml()?>
+      </b>
     </div>
-    <? $cover = $this->record($this->driver)->getCover('result-list', 'medium', $this->recordLink()->getUrl($this->driver)); ?>
-    <? if ($cover): ?>
-    <div class="col-sm-2 col-xs-3 left">
-      <?=$cover ?>
+
+    <div>
+      <? $summAuthors = $this->driver->getPrimaryAuthorsWithHighlighting(); if (!empty($summAuthors)): ?>
+        <?=$this->transEsc('by')?>
+        <? $authorCount = count($summAuthors); foreach ($summAuthors as $i => $summAuthor): ?>
+          <a href="<?=$this->record($this->driver)->getLink('author', $this->highlight($summAuthor, null, true, false))?>"><?=$this->highlight($summAuthor)?></a><?=$i + 1 < $authorCount ? ',' : ''?>
+        <? endforeach; ?>
+      <? endif; ?>
+
+      <? $journalTitle = $this->driver->getContainerTitle(); $summDate = $this->driver->getPublicationDates(); ?>
+      <? if (!empty($journalTitle)): ?>
+        <?=!empty($summAuthor) ? '<br />' : ''?>
+        <?=/* TODO: handle highlighting more elegantly here */ $this->transEsc('Published in') . ' <a href="' . $this->record($this->driver)->getLink('journaltitle', str_replace(array('{{{{START_HILITE}}}}', '{{{{END_HILITE}}}}'), '', $journalTitle)) . '">' . $this->highlight($journalTitle) . '</a>';?>
+        <?=!empty($summDate) ? ' (' . $this->escapeHtml($summDate[0]) . ')' : ''?>
+      <? elseif (!empty($summDate)): ?>
+        <?=!empty($summAuthor) ? '<br />' : ''?>
+        <?=$this->transEsc('Published') . ' ' . $this->escapeHtml($summDate[0])?>
+      <? endif; ?>
+      <? $summInCollection = $this->driver->getContainingCollections();
+      if (!empty($summInCollection)): ?>
+        <? foreach ($summInCollection as $collId => $collText): ?>
+          <div>
+            <b><?=$this->transEsc("in_collection_label")?></b>
+            <a class="collectionLinkText" href="<?=$this->url('collection', array('id' => $collId))?>?recordID=<?=urlencode($this->driver->getUniqueID())?>">
+              <?=$this->escapeHtml($collText)?>
+            </a>
+          </div>
+        <? endforeach; ?>
+      <? endif; ?>
     </div>
-    <div class="col-sm-7 col-xs-6 middle">
-    <? else : ?>
-    <div class="col-sm-9 col-xs-9 middle">
-    <? endif ?>
-      <div>
-        <b>
-          <?=$this->record($this->driver)->getTitleHtml()?>
-        </b>
-      </div>
 
-      <div>
-        <? $summAuthors = $this->driver->getPrimaryAuthorsWithHighlighting(); if (!empty($summAuthors)): ?>
-          <?=$this->transEsc('by')?>
-          <? $authorCount = count($summAuthors); foreach ($summAuthors as $i => $summAuthor): ?>
-            <a href="<?=$this->record($this->driver)->getLink('author', $this->highlight($summAuthor, null, true, false))?>"><?=$this->highlight($summAuthor)?></a><?=$i + 1 < $authorCount ? ',' : ''?>
-          <? endforeach; ?>
+    <div>
+      <div class="callnumAndLocation ajax-availability hidden">
+        <? if ($this->driver->supportsAjaxStatus()): ?>
+          <strong class="hideIfDetailed"><?=$this->transEsc('Call Number')?>:</strong>
+          <span class="callnumber ajax-availability hidden">
+            <?=$this->transEsc('Loading')?>...
+          </span><br class="hideIfDetailed"/>
+          <strong><?=$this->transEsc('Located')?>:</strong>
+          <span class="location ajax-availability hidden">
+            <?=$this->transEsc('Loading')?>...
+          </span>
+          <div class="locationDetails"></div>
+        <? else: ?>
+          <? $summCallNo = $this->driver->getCallNumber(); if (!empty($summCallNo)): ?>
+            <strong><?=$this->transEsc('Call Number')?>:</strong> <?=$this->escapeHtml($summCallNo)?>
+          <? endif; ?>
         <? endif; ?>
+      </div>
 
-        <? $journalTitle = $this->driver->getContainerTitle(); $summDate = $this->driver->getPublicationDates(); ?>
-        <? if (!empty($journalTitle)): ?>
-          <?=!empty($summAuthor) ? '<br />' : ''?>
-          <?=/* TODO: handle highlighting more elegantly here */ $this->transEsc('Published in') . ' <a href="' . $this->record($this->driver)->getLink('journaltitle', str_replace(array('{{{{START_HILITE}}}}', '{{{{END_HILITE}}}}'), '', $journalTitle)) . '">' . $this->highlight($journalTitle) . '</a>';?>
-          <?=!empty($summDate) ? ' (' . $this->escapeHtml($summDate[0]) . ')' : ''?>
-        <? elseif (!empty($summDate)): ?>
-          <?=!empty($summAuthor) ? '<br />' : ''?>
-          <?=$this->transEsc('Published') . ' ' . $this->escapeHtml($summDate[0])?>
+      <? /* We need to find out if we're supposed to display an OpenURL link ($openUrlActive),
+            but even if we don't plan to display the link, we still want to get the $openUrl
+            value for use in generating a COinS (Z3988) tag -- see bottom of file.
+          */
+        $openUrl = $this->openUrl($this->driver, 'results');
+        $openUrlActive = $openUrl->isActive();
+        // Account for replace_other_urls setting
+        $urls = $this->record($this->driver)->getLinkDetails($openUrlActive);
+        if ($openUrlActive || !empty($urls)): ?>
+        <? if ($openUrlActive): ?>
+          <br/>
+          <?=$openUrl->renderTemplate()?>
         <? endif; ?>
-        <? $summInCollection = $this->driver->getContainingCollections();
-        if (!empty($summInCollection)): ?>
-          <? foreach ($summInCollection as $collId => $collText): ?>
-            <div>
-              <b><?=$this->transEsc("in_collection_label")?></b>
-              <a class="collectionLinkText" href="<?=$this->url('collection', array('id' => $collId))?>?recordID=<?=urlencode($this->driver->getUniqueID())?>">
-                <?=$this->escapeHtml($collText)?>
-              </a>
-            </div>
+        <? if (!is_array($urls)) $urls = array();
+          if(!$this->driver->isCollection()):
+            foreach ($urls as $current): ?>
+              <a href="<?=$this->escapeHtmlAttr($this->proxyUrl($current['url']))?>" class="fulltext" target="new"><i class="fa fa-external-link" aria-hidden="true"></i> <?=($current['url'] == $current['desc']) ? $this->transEsc('Get full text') : $this->escapeHtml($current['desc'])?></a><br/>
           <? endforeach; ?>
         <? endif; ?>
-      </div>
+      <? endif; ?>
 
-      <div>
-        <div class="callnumAndLocation ajax-availability hidden">
-          <? if ($this->driver->supportsAjaxStatus()): ?>
-            <strong class="hideIfDetailed"><?=$this->transEsc('Call Number')?>:</strong>
-            <span class="callnumber ajax-availability hidden">
-              <?=$this->transEsc('Loading')?>...
-            </span><br class="hideIfDetailed"/>
-            <strong><?=$this->transEsc('Located')?>:</strong>
-            <span class="location ajax-availability hidden">
-              <?=$this->transEsc('Loading')?>...
-            </span>
-            <div class="locationDetails"></div>
-          <? else: ?>
-            <? $summCallNo = $this->driver->getCallNumber(); if (!empty($summCallNo)): ?>
-              <strong><?=$this->transEsc('Call Number')?>:</strong> <?=$this->escapeHtml($summCallNo)?>
-            <? endif; ?>
-          <? endif; ?>
-        </div>
+      <?=$this->record($this->driver)->getFormatList() ?>
 
-        <? /* We need to find out if we're supposed to display an OpenURL link ($openUrlActive),
-              but even if we don't plan to display the link, we still want to get the $openUrl
-              value for use in generating a COinS (Z3988) tag -- see bottom of file.
-            */
-          $openUrl = $this->openUrl($this->driver, 'results');
-          $openUrlActive = $openUrl->isActive();
-          // Account for replace_other_urls setting
-          $urls = $this->record($this->driver)->getLinkDetails($openUrlActive);
-          if ($openUrlActive || !empty($urls)): ?>
-          <? if ($openUrlActive): ?>
-            <br/>
-            <?=$openUrl->renderTemplate()?>
-          <? endif; ?>
-          <? if (!is_array($urls)) $urls = array();
-            if(!$this->driver->isCollection()):
-              foreach ($urls as $current): ?>
-                <a href="<?=$this->escapeHtmlAttr($this->proxyUrl($current['url']))?>" class="fulltext" target="new"><i class="fa fa-external-link"></i> <?=($current['url'] == $current['desc']) ? $this->transEsc('Get full text') : $this->escapeHtml($current['desc'])?></a><br/>
-            <? endforeach; ?>
-          <? endif; ?>
-        <? endif; ?>
+      <? if (!$openUrlActive && empty($urls) && $this->driver->supportsAjaxStatus()): ?>
+        <span class="status ajax-availability small">
+          <span class="label label-default"><?=$this->transEsc('Loading')?>...</span>
+        </span>
+      <? endif; ?>
+      <?=$this->record($this->driver)->getPreviews()?>
 
-        <?=str_replace('class="', 'class="label label-info ', $this->record($this->driver)->getFormatList())?>
-
-        <? if (!$openUrlActive && empty($urls) && $this->driver->supportsAjaxStatus()): ?>
-          <span class="status ajax-availability small">
-            <span class="label label-default"><?=$this->transEsc('Loading')?>...</span>
-          </span>
-        <? endif; ?>
-        <?=$this->record($this->driver)->getPreviews()?>
-
-        <?=$this->driver->supportsCoinsOpenUrl()?'<span class="Z3988" title="'.$this->escapeHtmlAttr($this->driver->getCoinsOpenUrl()).'"></span>':''?>
-      </div>
+      <?=$this->driver->supportsCoinsOpenUrl()?'<span class="Z3988" title="'.$this->escapeHtmlAttr($this->driver->getCoinsOpenUrl()).'"></span>':''?>
     </div>
   </div>
+  <? if ($thumbnail && $thumbnailAlignment == 'right'): ?>
+    <?=$thumbnail ?>
+  <? endif; ?>
 </div>
diff --git a/themes/bootstrap3/templates/RecordDriver/SolrAuth/result-list.phtml b/themes/bootstrap3/templates/RecordDriver/SolrAuth/result-list.phtml
index 15c285cadfdfaa684fdd95f4ee14638ee68d4902..274d8cb07ef5e55f1616c2702e0167d49a79549d 100644
--- a/themes/bootstrap3/templates/RecordDriver/SolrAuth/result-list.phtml
+++ b/themes/bootstrap3/templates/RecordDriver/SolrAuth/result-list.phtml
@@ -6,26 +6,28 @@
   $seeAlso = $this->driver->getSeeAlso();
   $useFor = $this->driver->getUseFor();
 ?>
-<div class="listentry col-xs-10">
-  <div class="resultItemLine1">
-    <a href="<?=$this->url('authority-record')?>?id=<?=urlencode($this->driver->getUniqueId())?>" class="title"><?=$this->escapeHtml($heading)?></a>
-  </div>
+<div class="media">
+  <div class="media-body">
+    <div class="resultItemLine1">
+      <a href="<?=$this->url('authority-record')?>?id=<?=urlencode($this->driver->getUniqueId())?>" class="title"><?=$this->escapeHtml($heading)?></a>
+    </div>
 
-  <div class="resultItemLine2">
-    <? if (!empty($seeAlso)): ?>
-      <?=$this->transEsc("See also")?>:<br/>
-      <? foreach ($seeAlso as $current): ?>
-        <a href="<?=$this->url('authority-search')?>?lookfor=%22<?=urlencode($current)?>%22&amp;type=MainHeading"><?=$this->escapeHtml($current)?></a><br/>
-      <? endforeach; ?>
-    <? endif; ?>
-  </div>
+    <div class="resultItemLine2">
+      <? if (!empty($seeAlso)): ?>
+        <?=$this->transEsc("See also")?>:<br/>
+        <? foreach ($seeAlso as $current): ?>
+          <a href="<?=$this->url('authority-search')?>?lookfor=%22<?=urlencode($current)?>%22&amp;type=MainHeading"><?=$this->escapeHtml($current)?></a><br/>
+        <? endforeach; ?>
+      <? endif; ?>
+    </div>
 
-  <div class="resultItemLine3">
-    <? if (!empty($useFor)): ?>
-      <?=$this->transEsc("Use for")?>:<br/>
-      <? foreach ($useFor as $current): ?>
-        <?=$this->escapeHtml($current)?><br/>
-      <? endforeach; ?>
-    <? endif; ?>
+    <div class="resultItemLine3">
+      <? if (!empty($useFor)): ?>
+        <?=$this->transEsc("Use for")?>:<br/>
+        <? foreach ($useFor as $current): ?>
+          <?=$this->escapeHtml($current)?><br/>
+        <? endforeach; ?>
+      <? endif; ?>
+    </div>
   </div>
-</div>
\ No newline at end of file
+</div>
diff --git a/themes/bootstrap3/templates/RecordDriver/SolrDefault/core.phtml b/themes/bootstrap3/templates/RecordDriver/SolrDefault/core.phtml
index e1b264671efd5c74f43ca859c29a90389bb344b6..0d411fb74252041c5e994c502fd6583c838022f2 100644
--- a/themes/bootstrap3/templates/RecordDriver/SolrDefault/core.phtml
+++ b/themes/bootstrap3/templates/RecordDriver/SolrDefault/core.phtml
@@ -5,13 +5,24 @@
   } else {
     $user_id = false;
   }
+
+  $formatRoles = function ($roles) {
+    if (count($roles) == 0) {
+      return '';
+    }
+    $that = $this;
+    $translate = function ($str) use ($that) {
+      return $that->transEsc('CreatorRoles::' . $str);
+    };
+    return ' (' . implode(', ', array_unique(array_map($translate, $roles))) . ')';
+  };
 ?>
 <div class="row" vocab="http://schema.org/" resource="#record" typeof="<?=$this->driver->getSchemaOrgFormats()?> Product">
   <? $QRCode = $this->record($this->driver)->getQRCode("core");
      $cover = $this->record($this->driver)->getCover('core', 'medium', $this->record($this->driver)->getThumbnail('large'));
      $preview = $this->record($this->driver)->getPreviews(); ?>
   <? if ($QRCode || $cover || $preview): ?>
-  <div class="col-sm-3">
+  <div class="col-sm-3 img-col">
     <div class="text-center">
       <? /* Display thumbnail if appropriate: */ ?>
       <? if($cover): ?>
@@ -33,7 +44,7 @@
     <? if ($preview): ?><?=$preview?><? endif; ?>
   </div>
 
-  <div class="col-sm-9">
+  <div class="col-sm-9 info-col">
   <? else: ?>
   <div class="col-sm-12">
   <? endif; ?>
@@ -100,7 +111,7 @@
         <tr>
           <th><?=$this->transEsc(count($authors['main']) > 1 ? 'Main Authors' : 'Main Author')?>: </th>
           <td>
-            <? $i = 0; foreach ($authors['main'] as $author => $roles): ?><?=($i++ == 0)?'':', '?><span property="author"><a href="<?=$this->record($this->driver)->getLink('author', $author)?>"><?=$this->escapeHtml($author)?></a><? if (count($roles) > 0): ?> (<? $j = 0; foreach ($roles as $role): ?><?=($j++ == 0)?'':', '?><?=$this->transEsc("CreatorRoles::" . $role)?><? endforeach; ?>)<? endif; ?></span><? endforeach; ?>
+            <? $i = 0; foreach ($authors['main'] as $author => $roles): ?><?=($i++ == 0)?'':', '?><span property="author"><a href="<?=$this->record($this->driver)->getLink('author', $author)?>"><?=$this->escapeHtml($author)?></a><?=$formatRoles($roles)?></span><? endforeach; ?>
           </td>
         </tr>
       <? endif; ?>
@@ -109,7 +120,7 @@
         <tr>
           <th><?=$this->transEsc(count($authors['corporate']) > 1 ? 'Corporate Author' : 'Corporate Authors')?>: </th>
           <td>
-            <? $i = 0; foreach ($authors['corporate'] as $corporate => $roles): ?><?=($i++ == 0)?'':', '?><span property="creator"><a href="<?=$this->record($this->driver)->getLink('author', $corporate)?>"><?=$this->escapeHtml($corporate)?></a><? if (count($roles) > 0): ?> (<? $j = 0; foreach ($roles as $role): ?><?=($j++ == 0)?'':', '?><?=$this->transEsc("CreatorRoles::" . $role)?><? endforeach; ?>)<? endif; ?></span><? endforeach; ?>
+            <? $i = 0; foreach ($authors['corporate'] as $corporate => $roles): ?><?=($i++ == 0)?'':', '?><span property="creator"><a href="<?=$this->record($this->driver)->getLink('author', $corporate)?>"><?=$this->escapeHtml($corporate)?></a><?=$formatRoles($roles)?></span><? endforeach; ?>
           </td>
         </tr>
       <? endif; ?>
@@ -118,12 +129,12 @@
         <tr>
           <th><?=$this->transEsc('Other Authors')?>: </th>
           <td>
-            <? $i = 0; foreach ($authors['secondary'] as $author => $roles): ?><?=($i++ == 0)?'':', '?><span property="contributor"><a href="<?=$this->record($this->driver)->getLink('author', $author)?>"><?=$this->escapeHtml($author)?></a><? if (count($roles) > 0): ?> (<? $j = 0; foreach ($roles as $role): ?><?=($j++ == 0)?'':', '?><?=$this->transEsc("CreatorRoles::" . $role)?><? endforeach; ?>)<? endif; ?></span><? endforeach; ?>
+            <? $i = 0; foreach ($authors['secondary'] as $author => $roles): ?><?=($i++ == 0)?'':', '?><span property="contributor"><a href="<?=$this->record($this->driver)->getLink('author', $author)?>"><?=$this->escapeHtml($author)?></a><?=$formatRoles($roles)?></span><? endforeach; ?>
           </td>
         </tr>
       <? endif; ?>
 
-      <? $formats = $this->driver->getFormats(); if (!empty($formats)): ?>
+      <? if (count($this->driver->getFormats()) > 0): ?>
         <tr>
           <th><?=$this->transEsc('Format')?>: </th>
           <td><?=$this->record($this->driver)->getFormatList()?></td>
@@ -265,7 +276,7 @@
           <th><?=$this->transEsc('Tags')?>: </th>
           <td>
             <a class="tag-record btn btn-link pull-right flip" href="<?=$this->recordLink()->getActionUrl($this->driver, 'AddTag')?>" data-lightbox>
-              <i class="fa fa-plus"></i> <?=$this->transEsc('Add Tag')?>
+              <i class="fa fa-plus" aria-hidden="true"></i> <?=$this->transEsc('Add Tag')?>
             </a>
             <?=$this->context($this)->renderInContext('record/taglist', array('tagList'=>$tagList, 'loggedin'=>$loggedin)) ?>
           </td>
diff --git a/themes/bootstrap3/templates/RecordDriver/SolrDefault/format-list.phtml b/themes/bootstrap3/templates/RecordDriver/SolrDefault/format-list.phtml
index 9ffc562f0d1c0490415099d24323b3a6e665c633..2b9a07e77b12d61c49a7566606b8283af9abc74d 100644
--- a/themes/bootstrap3/templates/RecordDriver/SolrDefault/format-list.phtml
+++ b/themes/bootstrap3/templates/RecordDriver/SolrDefault/format-list.phtml
@@ -1,3 +1,3 @@
 <? foreach ($this->driver->getFormats() as $format): ?>
-  <span class="iconlabel <?=$this->record($this->driver)->getFormatClass($format)?>"><?=$this->transEsc($format)?></span>
+  <span class="format <?=$this->record($this->driver)->getFormatClass($format) ?>"><?=$this->transEsc($format) ?></span>
 <? endforeach; ?>
diff --git a/themes/bootstrap3/templates/RecordDriver/SolrDefault/list-entry.phtml b/themes/bootstrap3/templates/RecordDriver/SolrDefault/list-entry.phtml
index 9b75791676d556f1d99b85d0a3cc8cb82dd3c515..5ca54d335fa2e006dd82971768594827629a8123 100644
--- a/themes/bootstrap3/templates/RecordDriver/SolrDefault/list-entry.phtml
+++ b/themes/bootstrap3/templates/RecordDriver/SolrDefault/list-entry.phtml
@@ -9,177 +9,192 @@
     $list_id = null;
     $user_id = $this->user ? $this->user->id : null;
   }
-?>
-<div class="row result<? if($this->driver->supportsAjaxStatus()): ?> ajaxItem<? endif ?>">
-  <? $cover=$this->record($this->driver)->getCover('list-entry', 'small', $this->recordLink()->getUrl($this->driver)); ?>
-  <? if ($cover): ?>
-  <div class="col-xs-2 left">
-  <? else: ?>
-  <div class="col-xs-1 left">
-  <? endif ?>
-    <label class="pull-left flip"><?=$this->record($this->driver)->getCheckbox() ?></label>
-    <input type="hidden" value="<?=$this->escapeHtmlAttr($id) ?>" class="hiddenId"/>
-  <? if ($cover): ?>
-    <?=$cover ?>
-  </div>
-  <div class="col-xs-6 middle">
-  <? else: ?>
+  // Thumbnail
+  $coverDetails = $this->record($this->driver)->getCoverDetails('list-entry', 'medium', $this->recordLink()->getUrl($this->driver));
+  $cover = $coverDetails['html'];
+  $thumbnail = false;
+  $thumbnailAlignment = $this->record($this->driver)->getThumbnailAlignment('list');
+  if ($cover):
+    ob_start(); ?>
+    <div class="media-<?=$thumbnailAlignment ?> <?=$this->escapeHtmlAttr($coverDetails['size'])?>">
+      <?=$cover ?>
+    </div>
+    <? $thumbnail = ob_get_contents(); ?>
+  <? ob_end_clean(); ?>
+<? endif; ?>
+<div class="result<? if($this->driver->supportsAjaxStatus()): ?> ajaxItem<? endif ?>">
+  <input type="hidden" value="<?=$this->escapeHtmlAttr($id) ?>" class="hiddenId"/>
+  <input type="hidden" value="<?=$this->escapeHtmlAttr($source) ?>" class="hiddenSource"/>
+  <div class="checkbox hidden-print">
+    <label><?=$this->record($this->driver)->getCheckbox()?></label>
   </div>
-  <div class="col-xs-7 middle">
-  <? endif ?>
+  <div class="media">
+    <? if ($thumbnail && $thumbnailAlignment == 'left'): ?>
+      <?=$thumbnail ?>
+    <? endif; ?>
+    <div class="media-body">
+      <div class="row short-view">
+        <div class="col-sm-8">
+          <div class="resultItemLine1">
+            <? $missing = $this->driver instanceof \VuFind\RecordDriver\Missing; ?>
+            <? if (!$missing): ?><a href="<?=$this->recordLink()->getUrl($this->driver)?>" class="getFull" data-view="<?=$this->params->getOptions()->getListViewOption() ?>"><? endif; ?>
+              <span class="title"><?=$this->record($this->driver)->getTitleHtml()?></span>
+            <? if (!$missing): ?></a><? endif; ?>
+          </div>
 
-    <div class="resultItemLine1">
-      <? $missing = $this->driver instanceof \VuFind\RecordDriver\Missing; ?>
-      <? if (!$missing): ?><a href="<?=$this->recordLink()->getUrl($this->driver)?>"><? endif; ?>
-        <?=$this->record($this->driver)->getTitleHtml()?>
-      <? if (!$missing): ?></a><? endif; ?>
-    </div>
+          <div class="resultItemLine2">
+            <? if($this->driver->isCollection()): ?>
+              <?=implode('<br>', array_map(array($this, 'escapeHtml'), $this->driver->getSummary())); ?>
+            <? else: ?>
+              <? $summAuthors = $this->driver->getPrimaryAuthors(); if (!empty($summAuthors)): ?>
+                <?=$this->transEsc('by')?>
+                <? $authorCount = count($summAuthors); foreach ($summAuthors as $i => $summAuthor): ?>
+                  <a href="<?=$this->record($this->driver)->getLink('author', $summAuthor)?>"><?=$this->escapeHtml($summAuthor)?></a><?=($i + 1 < $authorCount ? ';' : '') ?>
+                <? endforeach; ?>
+              <? endif; ?>
 
-    <div class="resultItemLine2">
-      <? if($this->driver->isCollection()): ?>
-        <?=implode('<br>', array_map(array($this, 'escapeHtml'), $this->driver->getSummary())); ?>
-      <? else: ?>
-        <? $summAuthors = $this->driver->getPrimaryAuthors(); if (!empty($summAuthors)): ?>
-          <?=$this->transEsc('by')?>
-          <? $authorCount = count($summAuthors); foreach ($summAuthors as $i => $summAuthor): ?>
-            <a href="<?=$this->record($this->driver)->getLink('author', $summAuthor)?>"><?=$this->escapeHtml($summAuthor)?></a><?=($i + 1 < $authorCount ? ';' : '') ?>
-          <? endforeach; ?>
-        <? endif; ?>
+              <? $journalTitle = $this->driver->getContainerTitle(); $summDate = $this->driver->getPublicationDates(); ?>
+              <? if (!empty($journalTitle)): ?>
+                <?=!empty($summAuthor) ? '<br/>' : ''?>
+                <?=/* TODO: handle highlighting more elegantly here */ $this->transEsc('Published in') . ' <a href="' . $this->record($this->driver)->getLink('journaltitle', str_replace(array('{{{{START_HILITE}}}}', '{{{{END_HILITE}}}}'), '', $journalTitle)) . '">' . $this->highlight($journalTitle) . '</a>';?>
+                <?=!empty($summDate) ? ' (' . $this->escapeHtml($summDate[0]) . ')' : ''?>
+              <? elseif (!empty($summDate)): ?>
+                <?=!empty($summAuthor) ? '<br/>' : ''?>
+                <?=$this->transEsc('Published') . ' ' . $this->escapeHtml($summDate[0])?>
+              <? endif; ?>
+              <? $summInCollection = $this->driver->getContainingCollections(); if (false && !empty($summInCollection)): ?>
+                <? foreach ($summInCollection as $collId => $collText): ?>
+                  <div>
+                    <b><?=$this->transEsc("in_collection_label")?></b>
+                    <a class="collectionLinkText" href="<?=$this->url('collection', array('id' => $collId))?>?recordID=<?=urlencode($this->driver->getUniqueID())?>">
+                      <?=$this->escapeHtml($collText)?>
+                    </a>
+                  </div>
+                <? endforeach; ?>
+              <? endif; ?>
+            <? endif; ?>
+          </div>
 
-        <? $journalTitle = $this->driver->getContainerTitle(); $summDate = $this->driver->getPublicationDates(); ?>
-        <? if (!empty($journalTitle)): ?>
-          <?=!empty($summAuthor) ? '<br/>' : ''?>
-          <?=/* TODO: handle highlighting more elegantly here */ $this->transEsc('Published in') . ' <a href="' . $this->record($this->driver)->getLink('journaltitle', str_replace(array('{{{{START_HILITE}}}}', '{{{{END_HILITE}}}}'), '', $journalTitle)) . '">' . $this->highlight($journalTitle) . '</a>';?>
-          <?=!empty($summDate) ? ' (' . $this->escapeHtml($summDate[0]) . ')' : ''?>
-        <? elseif (!empty($summDate)): ?>
-          <?=!empty($summAuthor) ? '<br/>' : ''?>
-          <?=$this->transEsc('Published') . ' ' . $this->escapeHtml($summDate[0])?>
-        <? endif; ?>
-        <? $summInCollection = $this->driver->getContainingCollections(); if (false && !empty($summInCollection)): ?>
-          <? foreach ($summInCollection as $collId => $collText): ?>
-            <div>
-              <b><?=$this->transEsc("in_collection_label")?></b>
-              <a class="collectionLinkText" href="<?=$this->url('collection', array('id' => $collId))?>?recordID=<?=urlencode($this->driver->getUniqueID())?>">
-                <?=$this->escapeHtml($collText)?>
-              </a>
-            </div>
-          <? endforeach; ?>
-        <? endif; ?>
-      <? endif; ?>
-    </div>
+          <div class="last">
+            <? if(!$this->driver->isCollection()) {
+                if ($snippet = $this->driver->getHighlightedSnippet()) {
+                  if (!empty($snippet['caption'])) {
+                    echo '<strong>' . $this->transEsc($snippet['caption']) . ':</strong> ';
+                  }
+                  if (!empty($snippet['snippet'])) {
+                    echo '<span class="quotestart">&#8220;</span>...' . $this->highlight($snippet['snippet']) . '...<span class="quoteend">&#8221;</span><br/>';
+                  }
+                }
+              } ?>
 
-    <div class="last">
-    <? if(!$this->driver->isCollection()) {
-        if ($snippet = $this->driver->getHighlightedSnippet()) {
-          if (!empty($snippet['caption'])) {
-            echo '<strong>' . $this->transEsc($snippet['caption']) . ':</strong> ';
-          }
-          if (!empty($snippet['snippet'])) {
-            echo '<span class="quotestart">&#8220;</span>...' . $this->highlight($snippet['snippet']) . '...<span class="quoteend">&#8221;</span><br/>';
-          }
-        }
-      } ?>
+            <? $listTags = ($this->usertags()->getMode() !== 'disabled') ? $this->driver->getTags(
+                null === $list_id ? true : $list_id, // get tags for all lists if no single list is selected
+                $user_id, 'tag'
+               ) : array();
+            ?>
+            <? if (count($listTags) > 0): ?>
+              <strong><?=$this->transEsc('Your Tags')?>:</strong>
+              <? foreach ($listTags as $tag): ?>
+                <a href="<?=$this->currentPath() . $results->getUrlQuery()->addFacet('tags', $tag->tag)?>"><?=$this->escapeHtml($tag->tag)?></a>
+              <? endforeach; ?>
+              <br/>
+            <? endif; ?>
+            <? $listNotes = $this->driver->getListNotes($list_id, $user_id); ?>
+            <? if (count($listNotes) > 0): ?>
+              <strong><?=$this->transEsc('Notes')?>:</strong>
+              <? if (count($listNotes) > 1): ?><br/><? endif; ?>
+              <? foreach ($listNotes as $note): ?>
+                <?=$this->escapeHtml($note)?><br/>
+              <? endforeach; ?>
+            <? endif; ?>
 
-    <? $listTags = ($this->usertags()->getMode() !== 'disabled') ? $this->driver->getTags(
-        null === $list_id ? true : $list_id, // get tags for all lists if no single list is selected
-        $user_id, 'tag'
-       ) : array();
-    ?>
-    <? if (count($listTags) > 0): ?>
-      <strong><?=$this->transEsc('Your Tags')?>:</strong>
-      <? foreach ($listTags as $tag): ?>
-        <a href="<?=$this->currentPath() . $results->getUrlQuery()->addFacet('tags', $tag->tag)?>"><?=$this->escapeHtml($tag->tag)?></a>
-      <? endforeach; ?>
-      <br/>
-    <? endif; ?>
-    <? $listNotes = $this->driver->getListNotes($list_id, $user_id); ?>
-    <? if (count($listNotes) > 0): ?>
-      <strong><?=$this->transEsc('Notes')?>:</strong>
-      <? if (count($listNotes) > 1): ?><br/><? endif; ?>
-      <? foreach ($listNotes as $note): ?>
-        <?=$this->escapeHtml($note)?><br/>
-      <? endforeach; ?>
-    <? endif; ?>
+            <? if (count($this->lists) > 0): ?>
+                <strong><?=$this->transEsc('Saved in')?>:</strong>
+                <? $i=0;foreach($this->lists as $current): ?>
+                    <a href="<?=$this->url('userList', array('id' => $current->id))?>"><?=$this->escapeHtml($current->title)?></a><? if($i++ < count($this->lists)-1): ?>,<? endif; ?>
+                <? endforeach; ?>
+                <br/>
+            <? endif; ?>
 
-    <? if (count($this->lists) > 0): ?>
-        <strong><?=$this->transEsc('Saved in')?>:</strong>
-        <? $i=0;foreach($this->lists as $current): ?>
-            <a href="<?=$this->url('userList', array('id' => $current->id))?>"><?=$this->escapeHtml($current->title)?></a><? if($i++ < count($this->lists)-1): ?>,<? endif; ?>
-        <? endforeach; ?>
-        <br/>
-    <? endif; ?>
-      <div class="callnumAndLocation">
-        <? if ($this->driver->supportsAjaxStatus()): ?>
-          <strong class="hideIfDetailed"><?=$this->transEsc('Call Number')?>:</strong>
-          <span class="callnumber ajax-availability hidden">
-            <?=$this->transEsc('Loading')?>...
-          </span><br class="hideIfDetailed"/>
-          <strong><?=$this->transEsc('Located')?>:</strong>
-          <span class="location ajax-availability hidden">
-            <?=$this->transEsc('Loading')?>...
-          </span>
-          <div class="locationDetails hidden"></div>
-        <? else: ?>
-          <? $summCallNo = $this->driver->getCallNumber(); if (!empty($summCallNo)): ?>
-            <strong><?=$this->transEsc('Call Number')?>:</strong> <?=$this->escapeHtml($summCallNo)?>
-          <? endif; ?>
-        <? endif; ?>
-      </div>
+            <div class="callnumAndLocation ajax-availability hidden">
+              <? if ($this->driver->supportsAjaxStatus()): ?>
+                <strong class="hideIfDetailed"><?=$this->transEsc('Call Number')?>:</strong>
+                <span class="callnumber ajax-availability hidden">
+                  <?=$this->transEsc('Loading')?>...<br/>
+                </span>
+                <strong><?=$this->transEsc('Located')?>:</strong>
+                <span class="location ajax-availability hidden">
+                  <?=$this->transEsc('Loading')?>...
+                </span>
+                <div class="locationDetails"></div>
+              <? else: ?>
+                <? $summCallNo = $this->driver->getCallNumber(); if (!empty($summCallNo)): ?>
+                  <strong><?=$this->transEsc('Call Number')?>:</strong> <?=$this->escapeHtml($summCallNo)?>
+                <? endif; ?>
+              <? endif; ?>
+            </div>
 
-      <? /* We need to find out if we're supposed to display an OpenURL link ($openUrlActive),
-            but even if we don't plan to display the link, we still want to get the $openUrl
-            value for use in generating a COinS (Z3988) tag -- see bottom of file.
-          */
-        $openUrl = $this->openUrl($this->driver, 'results');
-        $openUrlActive = $openUrl->isActive();
-        // Account for replace_other_urls setting
-        $urls = $this->record($this->driver)->getLinkDetails($openUrlActive);
+            <? /* We need to find out if we're supposed to display an OpenURL link ($openUrlActive),
+                  but even if we don't plan to display the link, we still want to get the $openUrl
+                  value for use in generating a COinS (Z3988) tag -- see bottom of file.
+                */
+              $openUrl = $this->openUrl($this->driver, 'results');
+              $openUrlActive = $openUrl->isActive();
+              // Account for replace_other_urls setting
+              $urls = $this->record($this->driver)->getLinkDetails($openUrlActive);
 
-        if ($openUrlActive || !empty($urls)):
-      ?>
-        <? if ($openUrlActive): ?>
-          <br/>
-          <?=$openUrl->renderTemplate()?>
-        <? endif;?>
+              if ($openUrlActive || !empty($urls)):
+            ?>
+              <? if ($openUrlActive): ?>
+                <br/>
+                <?=$openUrl->renderTemplate()?>
+              <? endif;?>
 
-        <? if (!is_array($urls)) { $urls = array(); }
-          if(!$this->driver->isCollection()):
-            foreach ($urls as $current): ?>
-              <a href="<?=$this->escapeHtmlAttr($this->proxyUrl($current['url']))?>" class="fulltext" target="new"><i class="fa fa-external-link"></i> <?=($current['url'] == $current['desc']) ? $this->transEsc('Get full text') : $this->escapeHtml($current['desc'])?></a>
-            <? endforeach; ?>
-          <? endif; ?>
-        <? endif; ?>
-      <br/>
-      <?=str_replace('class="', 'class="label label-info ', $this->record($this->driver)->getFormatList())?>
+              <? if (!is_array($urls)) { $urls = array(); }
+                if(!$this->driver->isCollection()):
+                  foreach ($urls as $current): ?>
+                    <a href="<?=$this->escapeHtmlAttr($this->proxyUrl($current['url']))?>" class="fulltext" target="new"><i class="fa fa-external-link" aria-hidden="true"></i> <?=($current['url'] == $current['desc']) ? $this->transEsc('Get full text') : $this->escapeHtml($current['desc'])?></a>
+                  <? endforeach; ?>
+                <? endif; ?>
+              <? endif; ?>
+            <br/>
 
-      <? if (!$openUrlActive && empty($urls) && $this->driver->supportsAjaxStatus()): ?>
-        <span class="status ajax-availability hidden"><?=$this->transEsc('Loading')?>...</span>
-        <br/><br/>
-      <? endif; ?>
-      <?=$this->record($this->driver)->getPreviews()?>
-    </div>
-  </div>
+            <?=$this->record($this->driver)->getFormatList() ?>
 
-  <div class="col-xs-2 right">
-    <i class="fa fa-fw fa-edit"></i> <a href="<?=$this->url('myresearch-edit')?>?id=<?=urlencode($id)?>&amp;source=<?=urlencode($source)?><? if (!is_null($list_id)):?>&amp;list_id=<?=urlencode($list_id)?><? endif; ?>" class="edit tool"><?=$this->transEsc('Edit')?></a><br/>
-    <? /* Use a different delete URL if we're removing from a specific list or the overall favorites: */
-      $deleteUrl = null === $list_id
-          ? $this->url('myresearch-favorites')
-          : $this->url('userList', array('id' => $list_id));
-      $deleteUrlGet = $deleteUrl . '?delete=' . urlencode($id) . '&amp;source=' . urlencode($source);
+            <? if (!$openUrlActive && empty($urls) && $this->driver->supportsAjaxStatus()): ?>
+              <span class="status ajax-availability hidden"><?=$this->transEsc('Loading')?>...</span>
+              <br/><br/>
+            <? endif; ?>
+            <?=$this->record($this->driver)->getPreviews()?>
+          </div>
+        </div>
 
-      $dLabel = 'delete-label-' . preg_replace('[\W]','-',$id);
-    ?>
-    <div class="dropdown">
-      <i class="fa fa-fw fa-trash-o"></i> <a class="dropdown-toggle" id="<?=$dLabel ?>" role="button" data-toggle="dropdown" data-target="#" href="<?=$deleteUrlGet ?>">
-        <?=$this->transEsc('Delete') ?>
-      </a>
-      <ul class="dropdown-menu" role="menu" aria-labelledby="<?=$dLabel ?>">
-        <li><a onClick="$.post('<?=$deleteUrl?>', {'delete':'<?=$this->escapeJs($id) ?>','source':'<?=$this->escapeJs($source) ?>','confirm':true},function(){location.reload(true)})" title="<?=$this->transEsc('confirm_delete_brief')?>"><?=$this->transEsc('confirm_dialog_yes')?></a></li>
-        <li><a><?=$this->transEsc('confirm_dialog_no')?></a></li>
-      </ul>
-    </div>
+        <div class="col-sm-4 right">
+          <i class="fa fa-fw fa-edit" aria-hidden="true"></i> <a href="<?=$this->url('myresearch-edit')?>?id=<?=urlencode($id)?>&amp;source=<?=urlencode($source)?><? if (!is_null($list_id)):?>&amp;list_id=<?=urlencode($list_id)?><? endif; ?>" class="edit tool"><?=$this->transEsc('Edit')?></a><br/>
+          <? /* Use a different delete URL if we're removing from a specific list or the overall favorites: */
+            $deleteUrl = null === $list_id
+                ? $this->url('myresearch-favorites')
+                : $this->url('userList', array('id' => $list_id));
+            $deleteUrlGet = $deleteUrl . '?delete=' . urlencode($id) . '&amp;source=' . urlencode($source);
 
-    <?=$this->driver->supportsCoinsOpenUrl()?'<span class="Z3988" title="'.$this->escapeHtmlAttr($this->driver->getCoinsOpenUrl()).'"></span>':''?>
+            $dLabel = 'delete-label-' . preg_replace('[\W]','-',$id);
+          ?>
+          <div class="dropdown">
+            <i class="fa fa-fw fa-trash-o" aria-hidden="true"></i> <a class="dropdown-toggle" id="<?=$dLabel ?>" role="button" data-toggle="dropdown" data-target="#" href="<?=$deleteUrlGet ?>">
+              <?=$this->transEsc('Delete') ?>
+            </a>
+            <ul class="dropdown-menu" role="menu" aria-labelledby="<?=$dLabel ?>">
+              <li><a onClick="$.post('<?=$deleteUrl?>', {'delete':'<?=$this->escapeJs($id) ?>','source':'<?=$this->escapeJs($source) ?>','confirm':true},function(){location.reload(true)})" title="<?=$this->transEsc('confirm_delete_brief')?>"><?=$this->transEsc('confirm_dialog_yes')?></a></li>
+              <li><a><?=$this->transEsc('confirm_dialog_no')?></a></li>
+            </ul>
+          </div>
+
+          <?=$this->driver->supportsCoinsOpenUrl()?'<span class="Z3988" title="'.$this->escapeHtmlAttr($this->driver->getCoinsOpenUrl()).'"></span>':''?>
+        </div>
+      </div>
+    </div>
+    <? if ($thumbnail && $thumbnailAlignment == 'right'): ?>
+      <?=$thumbnail ?>
+    <? endif; ?>
   </div>
 </div>
diff --git a/themes/bootstrap3/templates/RecordDriver/SolrDefault/result-grid.phtml b/themes/bootstrap3/templates/RecordDriver/SolrDefault/result-grid.phtml
index bfe3c8e7e281f06b92f46867fcf1ede0b06215cd..d0c012dae9fee972f67a50acd037d2a365d09448 100644
--- a/themes/bootstrap3/templates/RecordDriver/SolrDefault/result-grid.phtml
+++ b/themes/bootstrap3/templates/RecordDriver/SolrDefault/result-grid.phtml
@@ -33,7 +33,7 @@ $urls = $this->record($this->driver)->getLinkDetails($openUrlActive);
         <?=$openUrl->renderTemplate()?><br />
       <? endif; ?>
       <? if (!is_array($urls)) $urls = array(); foreach ($urls as $current): ?>
-        <a href="<?=$this->escapeHtmlAttr($this->proxyUrl($current['url']))?>" class="fulltext" target="new"><i class="fa fa-external-link"></i> <?=($current['url'] == $current['desc']) ? $this->transEsc('Get full text') : $this->escapeHtml($current['desc'])?></a>
+        <a href="<?=$this->escapeHtmlAttr($this->proxyUrl($current['url']))?>" class="fulltext" target="new"><i class="fa fa-external-link" aria-hidden="true"></i> <?=($current['url'] == $current['desc']) ? $this->transEsc('Get full text') : $this->escapeHtml($current['desc'])?></a>
         <br/>
       <? endforeach; ?>
     <? endif; ?>
diff --git a/themes/bootstrap3/templates/RecordDriver/SolrDefault/result-list.phtml b/themes/bootstrap3/templates/RecordDriver/SolrDefault/result-list.phtml
index 2617e7d64a693476ba343c92890c96e0d7d0959c..b1c1204e8c354288177164b672c5de555b168e38 100644
--- a/themes/bootstrap3/templates/RecordDriver/SolrDefault/result-list.phtml
+++ b/themes/bootstrap3/templates/RecordDriver/SolrDefault/result-list.phtml
@@ -1,180 +1,196 @@
-<div class="<?=$this->driver->supportsAjaxStatus()?'ajaxItem ':''?>col-xs-11">
-  <div class="row">
-    <div>
-      <input type="hidden" value="<?=$this->escapeHtmlAttr($this->driver->getUniqueID())?>" class="hiddenId" />
-      <input type="hidden" value="<?=$this->escapeHtmlAttr($this->driver->getSourceIdentifier())?>" class="hiddenSource" />
+<?
+  $coverDetails = $this->record($this->driver)->getCoverDetails('result-list', 'medium', $this->recordLink()->getUrl($this->driver));
+  $cover = $coverDetails['html'];
+  $thumbnail = false;
+  $thumbnailAlignment = $this->record($this->driver)->getThumbnailAlignment('result');
+  if ($cover):
+    ob_start(); ?>
+    <div class="media-<?=$thumbnailAlignment ?> <?=$this->escapeHtmlAttr($coverDetails['size'])?>">
+      <?=$cover ?>
     </div>
-    <? if ($cover = $this->record($this->driver)->getCover('result-list', 'medium', $this->recordLink()->getUrl($this->driver))): ?>
-      <div class="col-sm-2 col-xs-3 left"><?=$cover ?></div>
-    <? endif ?>
-    <div class="<?=$cover ? 'col-sm-7 col-xs-6' : 'col-sm-9 col-xs-9'?> middle">
-      <div>
-        <a href="<?=$this->recordLink()->getUrl($this->driver)?>" class="title">
-          <?=$this->record($this->driver)->getTitleHtml()?>
-        </a>
-      </div>
+    <? $thumbnail = ob_get_contents(); ?>
+  <? ob_end_clean(); ?>
+<? endif; ?>
+<input type="hidden" value="<?=$this->escapeHtmlAttr($this->driver->getUniqueID())?>" class="hiddenId" />
+<input type="hidden" value="<?=$this->escapeHtmlAttr($this->driver->getSourceIdentifier())?>" class="hiddenSource" />
+<div class="media">
+  <? if ($thumbnail && $thumbnailAlignment == 'left'): ?>
+    <?=$thumbnail ?>
+  <? endif ?>
+  <div class="media-body">
+    <div class="row short-view">
+      <div class="col-sm-8 middle">
+        <div>
+          <a href="<?=$this->recordLink()->getUrl($this->driver)?>" class="title getFull" data-view="<?=$this->params->getOptions()->getListViewOption() ?>">
+            <?=$this->record($this->driver)->getTitleHtml()?>
+          </a>
+        </div>
 
-      <div>
-        <? if($this->driver->isCollection()): ?>
-          <?=implode('<br>', array_map(array($this, 'escapeHtml'), $this->driver->getSummary())); ?>
-        <? else: ?>
-          <? $summAuthors = $this->driver->getPrimaryAuthorsWithHighlighting(); if (!empty($summAuthors)): ?>
-            <?=$this->transEsc('by')?>
-            <? $authorCount = count($summAuthors); foreach ($summAuthors as $i => $summAuthor): ?>
-              <a href="<?=$this->record($this->driver)->getLink('author', $this->highlight($summAuthor, null, true, false))?>"><?=$this->highlight($summAuthor)?></a><?=$i + 1 < $authorCount ? ',' : ''?>
-            <? endforeach; ?>
-          <? endif; ?>
+        <div>
+          <? if($this->driver->isCollection()): ?>
+            <?=implode('<br>', array_map(array($this, 'escapeHtml'), $this->driver->getSummary())); ?>
+          <? else: ?>
+            <? $summAuthors = $this->driver->getPrimaryAuthorsWithHighlighting(); if (!empty($summAuthors)): ?>
+              <?=$this->transEsc('by')?>
+              <? $authorCount = count($summAuthors); foreach ($summAuthors as $i => $summAuthor): ?>
+                <a href="<?=$this->record($this->driver)->getLink('author', $this->highlight($summAuthor, null, true, false))?>"><?=$this->highlight($summAuthor)?></a><?=$i + 1 < $authorCount ? ',' : ''?>
+              <? endforeach; ?>
+            <? endif; ?>
 
-          <? $journalTitle = $this->driver->getContainerTitle(); $summDate = $this->driver->getPublicationDates(); ?>
-          <? if (!empty($journalTitle)): ?>
-            <?=!empty($summAuthor) ? '<br />' : ''?>
-            <?=$this->transEsc('Published in')?>
-            <? $containerSource = $this->driver->getSourceIdentifier(); ?>
-            <? $containerID = $this->driver->getContainerRecordID(); ?>
-            <? /* TODO: handle highlighting more elegantly here: */?>
-            <a href="<?=($containerID ? $this->recordLink()->getUrl("$containerSource|$containerID") : $this->record($this->driver)->getLink('journaltitle', str_replace(array('{{{{START_HILITE}}}}', '{{{{END_HILITE}}}}'), '', $journalTitle)))?>"><?=$this->highlight($journalTitle) ?></a>
-            <?=!empty($summDate) ? ' (' . $this->escapeHtml($summDate[0]) . ')' : ''?>
-          <? elseif (!empty($summDate)): ?>
-            <?=!empty($summAuthor) ? '<br />' : ''?>
-            <?=$this->transEsc('Published') . ' ' . $this->escapeHtml($summDate[0])?>
+            <? $journalTitle = $this->driver->getContainerTitle(); $summDate = $this->driver->getPublicationDates(); ?>
+            <? if (!empty($journalTitle)): ?>
+              <?=!empty($summAuthor) ? '<br />' : ''?>
+              <?=$this->transEsc('Published in')?>
+              <? $containerSource = $this->driver->getSourceIdentifier(); ?>
+              <? $containerID = $this->driver->getContainerRecordID(); ?>
+              <? /* TODO: handle highlighting more elegantly here: */?>
+              <a href="<?=($containerID ? $this->recordLink()->getUrl("$containerSource|$containerID") : $this->record($this->driver)->getLink('journaltitle', str_replace(array('{{{{START_HILITE}}}}', '{{{{END_HILITE}}}}'), '', $journalTitle)))?>"><?=$this->highlight($journalTitle) ?></a>
+              <?=!empty($summDate) ? ' (' . $this->escapeHtml($summDate[0]) . ')' : ''?>
+            <? elseif (!empty($summDate)): ?>
+              <?=!empty($summAuthor) ? '<br />' : ''?>
+              <?=$this->transEsc('Published') . ' ' . $this->escapeHtml($summDate[0])?>
+            <? endif; ?>
+            <? $summInCollection = $this->driver->getContainingCollections(); if (!empty($summInCollection)): ?>
+              <? foreach ($summInCollection as $collId => $collText): ?>
+                <div>
+                  <b><?=$this->transEsc("in_collection_label")?></b>
+                  <a class="collectionLinkText" href="<?=$this->url('collection', array('id' => $collId))?>?recordID=<?=urlencode($this->driver->getUniqueID())?>">
+                    <?=$this->escapeHtml($collText)?>
+                  </a>
+                </div>
+              <? endforeach; ?>
+            <? endif; ?>
           <? endif; ?>
-          <? $summInCollection = $this->driver->getContainingCollections(); if (!empty($summInCollection)): ?>
-            <? foreach ($summInCollection as $collId => $collText): ?>
-              <div>
-                <b><?=$this->transEsc("in_collection_label")?></b>
-                <a class="collectionLinkText" href="<?=$this->url('collection', array('id' => $collId))?>?recordID=<?=urlencode($this->driver->getUniqueID())?>">
-                  <?=$this->escapeHtml($collText)?>
-                </a>
-              </div>
-            <? endforeach; ?>
-          <? endif; ?>
-        <? endif; ?>
-      </div>
+        </div>
 
-      <? if(!$this->driver->isCollection()): ?>
-        <? if ($snippet = $this->driver->getHighlightedSnippet()): ?>
-          <? if (!empty($snippet['caption'])): ?>
-            <strong><?=$this->transEsc($snippet['caption']) ?>:</strong> ';
-          <? endif; ?>
-          <? if (!empty($snippet['snippet'])): ?>
-            <span class="quotestart">&#8220;</span>...<?=$this->highlight($snippet['snippet']) ?>...<span class="quoteend">&#8221;</span><br/>
+        <? if(!$this->driver->isCollection()): ?>
+          <? if ($snippet = $this->driver->getHighlightedSnippet()): ?>
+            <? if (!empty($snippet['caption'])): ?>
+              <strong><?=$this->transEsc($snippet['caption']) ?>:</strong> ';
+            <? endif; ?>
+            <? if (!empty($snippet['snippet'])): ?>
+              <span class="quotestart">&#8220;</span>...<?=$this->highlight($snippet['snippet']) ?>...<span class="quoteend">&#8221;</span><br/>
+            <? endif; ?>
           <? endif; ?>
         <? endif; ?>
-      <? endif; ?>
 
-      <?
-      /* Display information on duplicate records if available */
-      if ($dedupData = $this->driver->getDedupData()): ?>
-        <div class="dedupInformation">
         <?
-          $i = 0;
-          foreach ($dedupData as $source => $current) {
-            if (++$i == 1) {
-              ?><span class="currentSource"><a href="<?=$this->recordLink()->getUrl($this->driver)?>"><?=$this->transEsc("source_$source", array(), $source)?></a></span><?
-            } else {
-              if ($i == 2) {
-                ?> <span class="otherSources">(<?=$this->transEsc('Other Sources')?>: <?
+        /* Display information on duplicate records if available */
+        if ($dedupData = $this->driver->getDedupData()): ?>
+          <div class="dedupInformation">
+          <?
+            $i = 0;
+            foreach ($dedupData as $source => $current) {
+              if (++$i == 1) {
+                ?><span class="currentSource"><a href="<?=$this->recordLink()->getUrl($this->driver)?>"><?=$this->transEsc("source_$source", array(), $source)?></a></span><?
               } else {
-                ?>, <?
+                if ($i == 2) {
+                  ?> <span class="otherSources">(<?=$this->transEsc('Other Sources')?>: <?
+                } else {
+                  ?>, <?
+                }
+                ?><a href="<?=$this->recordLink()->getUrl($current['id'])?>"><?=$this->transEsc("source_$source", array(), $source)?></a><?
               }
-              ?><a href="<?=$this->recordLink()->getUrl($current['id'])?>"><?=$this->transEsc("source_$source", array(), $source)?></a><?
             }
-          }
-          if ($i > 1) {
-            ?>)</span><?
-          }?>
+            if ($i > 1) {
+              ?>)</span><?
+            }?>
+          </div>
+        <? endif; ?>
+
+        <div class="callnumAndLocation ajax-availability hidden">
+          <? if ($this->driver->supportsAjaxStatus()): ?>
+            <strong class="hideIfDetailed"><?=$this->transEsc('Call Number')?>:</strong>
+            <span class="callnumber ajax-availability hidden">
+              <?=$this->transEsc('Loading')?>...<br/>
+            </span>
+            <strong><?=$this->transEsc('Located')?>:</strong>
+            <span class="location ajax-availability hidden">
+              <?=$this->transEsc('Loading')?>...
+            </span>
+            <div class="locationDetails"></div>
+          <? else: ?>
+            <? $summCallNo = $this->driver->getCallNumber(); if (!empty($summCallNo)): ?>
+              <strong><?=$this->transEsc('Call Number')?>:</strong> <?=$this->escapeHtml($summCallNo)?>
+            <? endif; ?>
+          <? endif; ?>
         </div>
-      <? endif; ?>
 
-      <div class="callnumAndLocation ajax-availability hidden">
-        <? if ($this->driver->supportsAjaxStatus()): ?>
-          <strong class="hideIfDetailed"><?=$this->transEsc('Call Number')?>:</strong>
-          <span class="callnumber ajax-availability hidden">
-            <?=$this->transEsc('Loading')?>...<br/>
-          </span>
-          <strong><?=$this->transEsc('Located')?>:</strong>
-          <span class="location ajax-availability hidden">
-            <?=$this->transEsc('Loading')?>...
-          </span>
-          <div class="locationDetails"></div>
-        <? else: ?>
-          <? $summCallNo = $this->driver->getCallNumber(); if (!empty($summCallNo)): ?>
-            <strong><?=$this->transEsc('Call Number')?>:</strong> <?=$this->escapeHtml($summCallNo)?>
+        <? /* We need to find out if we're supposed to display an OpenURL link ($openUrlActive),
+              but even if we don't plan to display the link, we still want to get the $openUrl
+              value for use in generating a COinS (Z3988) tag -- see bottom of file.
+            */
+          $openUrl = $this->openUrl($this->driver, 'results');
+          $openUrlActive = $openUrl->isActive();
+          // Account for replace_other_urls setting
+          $urls = $this->record($this->driver)->getLinkDetails($openUrlActive);
+
+          if ($openUrlActive || !empty($urls)): ?>
+          <? if ($openUrlActive): ?>
+            <br/>
+            <?=$openUrl->renderTemplate()?>
+          <? endif; ?>
+          <? if (!is_array($urls)) $urls = array();
+            if(!$this->driver->isCollection()):
+              foreach ($urls as $current): ?>
+                <a href="<?=$this->escapeHtmlAttr($this->proxyUrl($current['url']))?>" class="fulltext" target="new"><i class="fa fa-external-link" aria-hidden="true"></i> <?=($current['url'] == $current['desc']) ? $this->transEsc('Get full text') : $this->escapeHtml($current['desc'])?></a><br/>
+            <? endforeach; ?>
           <? endif; ?>
         <? endif; ?>
-      </div>
 
-      <? /* We need to find out if we're supposed to display an OpenURL link ($openUrlActive),
-            but even if we don't plan to display the link, we still want to get the $openUrl
-            value for use in generating a COinS (Z3988) tag -- see bottom of file.
-          */
-        $openUrl = $this->openUrl($this->driver, 'results');
-        $openUrlActive = $openUrl->isActive();
-        // Account for replace_other_urls setting
-        $urls = $this->record($this->driver)->getLinkDetails($openUrlActive);
+        <?=$this->record($this->driver)->getFormatList() ?>
 
-        if ($openUrlActive || !empty($urls)): ?>
-        <? if ($openUrlActive): ?>
-          <br/>
-          <?=$openUrl->renderTemplate()?>
+        <? if (!$openUrlActive && empty($urls) && $this->driver->supportsAjaxStatus()): ?>
+          <span class="status ajax-availability hidden">
+            <span class="label label-default"><?=$this->transEsc('Loading')?>...</span>
+          </span>
         <? endif; ?>
-        <? if (!is_array($urls)) $urls = array();
-          if(!$this->driver->isCollection()):
-            foreach ($urls as $current): ?>
-              <a href="<?=$this->escapeHtmlAttr($this->proxyUrl($current['url']))?>" class="fulltext" target="new"><i class="fa fa-external-link"></i> <?=($current['url'] == $current['desc']) ? $this->transEsc('Get full text') : $this->escapeHtml($current['desc'])?></a><br/>
-          <? endforeach; ?>
+        <?=$this->record($this->driver)->getPreviews()?>
+      </div>
+      <div class="col-sm-4 right hidden-print">
+        <? /* Display qrcode if appropriate: */ ?>
+        <? if ($QRCode = $this->record($this->driver)->getQRCode("results")): ?>
+          <?
+            // Add JS Variables for QrCode
+            $this->jsTranslations()->addStrings(array('qrcode_hide' => 'qrcode_hide', 'qrcode_show' => 'qrcode_show'));
+          ?>
+          <span class="hidden-xs">
+            <i class="fa fa-fw fa-qrcode" aria-hidden="true"></i> <a href="<?=$this->escapeHtmlAttr($QRCode);?>" class="qrcodeLink"><?=$this->transEsc('qrcode_show')?></a>
+            <div class="qrcode hidden">
+              <script type="text/template" class="qrCodeImgTag">
+                <img alt="<?=$this->transEsc('QR Code')?>" src="<?=$this->escapeHtmlAttr($QRCode);?>"/>
+              </script>
+            </div><br/>
+          </span>
         <? endif; ?>
-      <? endif; ?>
-
-      <?=str_replace('class="', 'class="label label-info ', $this->record($this->driver)->getFormatList())?>
 
-      <? if (!$openUrlActive && empty($urls) && $this->driver->supportsAjaxStatus()): ?>
-        <span class="status ajax-availability hidden">
-          <span class="label label-default"><?=$this->transEsc('Loading')?>...</span>
-        </span>
-      <? endif; ?>
-      <?=$this->record($this->driver)->getPreviews()?>
-    </div>
-    <div class="col-xs-3 right hidden-print">
-      <? /* Display qrcode if appropriate: */ ?>
-      <? if ($QRCode = $this->record($this->driver)->getQRCode("results")): ?>
-        <?
-          // Add JS Variables for QrCode
-          $this->jsTranslations()->addStrings(array('qrcode_hide' => 'qrcode_hide', 'qrcode_show' => 'qrcode_show'));
-        ?>
-        <span class="hidden-xs">
-          <i class="fa fa-fw fa-qrcode"></i> <a href="<?=$this->escapeHtmlAttr($QRCode);?>" class="qrcodeLink"><?=$this->transEsc('qrcode_show')?></a>
-          <div class="qrcode hidden">
-            <script type="text/template" class="qrCodeImgTag">
-              <img alt="<?=$this->transEsc('QR Code')?>" src="<?=$this->escapeHtmlAttr($QRCode);?>"/>
-            </script>
-          </div><br/>
-        </span>
-      <? endif; ?>
-
-      <? if ($this->userlist()->getMode() !== 'disabled'): ?>
-        <? /* Add to favorites */ ?>
-        <i class="fa fa-fw fa-star"></i> <a href="<?=$this->recordLink()->getActionUrl($this->driver, 'Save')?>" data-lightbox class="save-record" data-id="<?=$this->escapeHtmlAttr($this->driver->getUniqueId()) ?>"><?=$this->transEsc('Add to favorites')?></a><br/>
-        <? /* Saved lists */ ?>
-        <div class="savedLists alert alert-info hidden">
-          <strong><?=$this->transEsc("Saved in")?>:</strong>
-        </div>
-      <? endif; ?>
-
-      <? /* Hierarchy tree link */ ?>
-      <? $trees = $this->driver->tryMethod('getHierarchyTrees'); if (!empty($trees)): ?>
-        <? foreach ($trees as $hierarchyID => $hierarchyTitle): ?>
-          <div class="hierarchyTreeLink">
-            <input type="hidden" value="<?=$this->escapeHtmlAttr($hierarchyID)?>" class="hiddenHierarchyId" />
-            <i class="fa fa-fw fa-sitemap"></i>
-            <a class="hierarchyTreeLinkText" data-lightbox href="<?=$this->recordLink()->getTabUrl($this->driver, 'HierarchyTree')?>?hierarchy=<?=urlencode($hierarchyID)?>#tabnav" title="<?=$this->transEsc('hierarchy_tree')?>" data-lightbox-href="<?=$this->recordLink()->getTabUrl($this->driver, 'AjaxTab')?>?hierarchy=<?=urlencode($hierarchyID)?>" data-lightbox-post="tab=hierarchytree">
-              <?=$this->transEsc('hierarchy_view_context')?><? if (count($trees) > 1): ?>: <?=$this->escapeHtml($hierarchyTitle)?><? endif; ?>
-            </a>
+        <? if ($this->userlist()->getMode() !== 'disabled'): ?>
+          <? /* Add to favorites */ ?>
+          <i class="fa fa-fw fa-star" aria-hidden="true"></i> <a href="<?=$this->recordLink()->getActionUrl($this->driver, 'Save')?>" data-lightbox class="save-record" data-id="<?=$this->escapeHtmlAttr($this->driver->getUniqueId()) ?>"><?=$this->transEsc('Add to favorites')?></a><br/>
+          <? /* Saved lists */ ?>
+          <div class="savedLists alert alert-info hidden">
+            <strong><?=$this->transEsc("Saved in")?>:</strong>
           </div>
-        <? endforeach; ?>
-      <? endif; ?>
+        <? endif; ?>
 
-      <?=$this->driver->supportsCoinsOpenUrl()?'<span class="Z3988" title="'.$this->escapeHtmlAttr($this->driver->getCoinsOpenUrl()).'"></span>':''?>
+        <? /* Hierarchy tree link */ ?>
+        <? $trees = $this->driver->tryMethod('getHierarchyTrees'); if (!empty($trees)): ?>
+          <? foreach ($trees as $hierarchyID => $hierarchyTitle): ?>
+            <div class="hierarchyTreeLink">
+              <input type="hidden" value="<?=$this->escapeHtmlAttr($hierarchyID)?>" class="hiddenHierarchyId" />
+              <i class="fa fa-fw fa-sitemap" aria-hidden="true"></i>
+              <a class="hierarchyTreeLinkText" data-lightbox href="<?=$this->recordLink()->getTabUrl($this->driver, 'HierarchyTree')?>?hierarchy=<?=urlencode($hierarchyID)?>#tabnav" title="<?=$this->transEsc('hierarchy_tree')?>" data-lightbox-href="<?=$this->recordLink()->getTabUrl($this->driver, 'AjaxTab')?>?hierarchy=<?=urlencode($hierarchyID)?>" data-lightbox-post="tab=hierarchytree">
+                <?=$this->transEsc('hierarchy_view_context')?><? if (count($trees) > 1): ?>: <?=$this->escapeHtml($hierarchyTitle)?><? endif; ?>
+              </a>
+            </div>
+          <? endforeach; ?>
+        <? endif; ?>
+
+        <?=$this->driver->supportsCoinsOpenUrl()?'<span class="Z3988" title="'.$this->escapeHtmlAttr($this->driver->getCoinsOpenUrl()).'"></span>':''?>
+      </div>
     </div>
   </div>
+  <? if ($thumbnail && $thumbnailAlignment == 'right'): ?>
+    <?=$thumbnail ?>
+  <? endif ?>
 </div>
diff --git a/themes/bootstrap3/templates/RecordDriver/SolrDefault/toolbar.phtml b/themes/bootstrap3/templates/RecordDriver/SolrDefault/toolbar.phtml
index 6364402c81f7936e4be3e4c998236b46d9c72bf2..013497adf9ec58782a8b187889ed4ff6f8598581 100644
--- a/themes/bootstrap3/templates/RecordDriver/SolrDefault/toolbar.phtml
+++ b/themes/bootstrap3/templates/RecordDriver/SolrDefault/toolbar.phtml
@@ -5,22 +5,20 @@
   }
 
   // Set up some variables for convenience:
-  $id = $this->driver->getUniqueId();
-  $controllerClass = 'controller:' . $this->record($this->driver)->getController();
   $cart = $this->cart();
-  $cartId = $this->driver->getSourceIdentifier() . '|' . $id;
+  $cartId = $this->driver->getSourceIdentifier() . '|' . $this->driver->getUniqueId();
 ?>
 <ul class="nav nav-pills hidden-print">
   <? if (count($this->driver->getCitationFormats()) > 0): ?>
-    <li><a class="cite-record <?=$controllerClass?>" data-lightbox href="<?=$this->recordLink()->getActionUrl($this->driver, 'Cite')?>" rel="nofollow"><i class="fa fa-asterisk"></i> <?=$this->transEsc('Cite this')?></a></li>
+    <li><a class="cite-record" data-lightbox href="<?=$this->recordLink()->getActionUrl($this->driver, 'Cite')?>" rel="nofollow"><i class="fa fa-asterisk" aria-hidden="true"></i> <?=$this->transEsc('Cite this')?></a></li>
   <? endif; ?>
-  <li><a class="sms-record <?=$controllerClass?>" data-lightbox href="<?=$this->recordLink()->getActionUrl($this->driver, 'SMS')?>" rel="nofollow"><i class="fa fa-mobile"></i> <?=$this->transEsc('Text this')?></a></li>
-  <li><a class="mail-record <?=$controllerClass?>" data-lightbox href="<?=$this->recordLink()->getActionUrl($this->driver, 'Email')?>" rel="nofollow"><i class="fa fa-envelope"></i> <?=$this->transEsc('Email this')?></a></li>
+  <li><a class="sms-record" data-lightbox href="<?=$this->recordLink()->getActionUrl($this->driver, 'SMS')?>" rel="nofollow"><i class="fa fa-mobile" aria-hidden="true"></i> <?=$this->transEsc('Text this')?></a></li>
+  <li><a class="mail-record" data-lightbox href="<?=$this->recordLink()->getActionUrl($this->driver, 'Email')?>" rel="nofollow"><i class="fa fa-envelope" aria-hidden="true"></i> <?=$this->transEsc('Email this')?></a></li>
 
   <? $exportFormats = $this->export()->getFormatsForRecord($this->driver); ?>
   <? if(count($exportFormats) > 0): ?>
     <li class="dropdown">
-      <a class="export-toggle dropdown-toggle" data-toggle="dropdown" href="<?=$this->recordLink()->getActionUrl($this->driver, 'Export')?>" rel="nofollow"><i class="fa fa-list-alt"></i> <?=$this->transEsc('Export Record') ?></a>
+      <a class="export-toggle dropdown-toggle" data-toggle="dropdown" href="<?=$this->recordLink()->getActionUrl($this->driver, 'Export')?>" rel="nofollow"><i class="fa fa-list-alt" aria-hidden="true"></i> <?=$this->transEsc('Export Record') ?></a>
       <ul class="dropdown-menu" role="menu">
         <? foreach ($exportFormats as $exportFormat): ?>
           <li><a <? if ($this->export()->needsRedirect($exportFormat)): ?>target="<?=$this->escapeHtmlAttr($exportFormat)?>Main" <? endif; ?>href="<?=$this->recordLink()->getActionUrl($this->driver, 'Export')?>?style=<?=$this->escapeHtmlAttr($exportFormat)?>" rel="nofollow"><?=$this->transEsc('Export to')?> <?=$this->transEsc($this->export()->getLabelForFormat($exportFormat))?></a></li>
@@ -30,16 +28,16 @@
   <? endif; ?>
 
   <? if ($this->userlist()->getMode() !== 'disabled'): ?>
-    <li><a class="save-record <?=$controllerClass?>" data-lightbox href="<?=$this->recordLink()->getActionUrl($this->driver, 'Save')?>" rel="nofollow"><i class="fa fa-star"></i> <?=$this->transEsc('Add to favorites')?></a></li>
+    <li><a class="save-record" data-lightbox href="<?=$this->recordLink()->getActionUrl($this->driver, 'Save')?>" rel="nofollow"><i class="fa fa-star" aria-hidden="true"></i> <?=$this->transEsc('Add to favorites')?></a></li>
   <? endif; ?>
   <? if (!empty($addThis)): ?>
-    <li><a class="addThis addthis_button" href="https://www.addthis.com/bookmark.php?v=250&amp;pub=<?=urlencode($addThis)?>"><i class="fa fa-bookmark"></i> <?=$this->transEsc('Bookmark')?></a></li>
+    <li><a class="addThis addthis_button" href="https://www.addthis.com/bookmark.php?v=250&amp;pub=<?=urlencode($addThis)?>"><i class="fa fa-bookmark" aria-hidden="true"></i> <?=$this->transEsc('Bookmark')?></a></li>
   <? endif; ?>
   <? if ($cart->isActive()): ?>
     <li class="bookbag-menu">
       <input class="cartId" type="hidden" name="ids[]" value="<?=$this->escapeHtmlAttr($cartId)?>" />
-      <a class="cart-add hidden<? if(!$cart->contains($cartId)): ?> correct<? endif ?>" href="#"><i class="fa fa-plus"></i> <?=$this->transEsc('Add to Book Bag') ?></a>
-      <a class="cart-remove hidden<? if($cart->contains($cartId)): ?> correct<? endif ?>"href="#"><i class="fa fa-minus-circle"></i> <?=$this->transEsc('Remove from Book Bag') ?></a>
+      <a class="cart-add hidden<? if(!$cart->contains($cartId)): ?> correct<? endif ?>" href="#"><i class="fa fa-plus" aria-hidden="true"></i> <?=$this->transEsc('Add to Book Bag') ?></a>
+      <a class="cart-remove hidden<? if($cart->contains($cartId)): ?> correct<? endif ?>"href="#"><i class="fa fa-minus-circle" aria-hidden="true"></i> <?=$this->transEsc('Remove from Book Bag') ?></a>
       <noscript>
         <form method="post" name="addForm" action="<?=$this->url('cart-processor')?>">
           <input type="hidden" name="ids[]" value="<?=$this->escapeHtmlAttr($cartId)?>" />
diff --git a/themes/bootstrap3/templates/RecordDriver/SolrWeb/result-list.phtml b/themes/bootstrap3/templates/RecordDriver/SolrWeb/result-list.phtml
index 0e7e5c518cf906dd5c0d48e925e41eabe9ea174e..f6ca14f38305c92275774cc7ea773a89d5fa2442 100644
--- a/themes/bootstrap3/templates/RecordDriver/SolrWeb/result-list.phtml
+++ b/themes/bootstrap3/templates/RecordDriver/SolrWeb/result-list.phtml
@@ -1,27 +1,29 @@
 <?
   $url = $this->driver->getUrl();
 ?>
-<div class="listentry col-xs-11">
-  <div class="resultItemLine1">
-    <a href="<?=$this->escapeHtmlAttr($url)?>" class="title">
-      <?=$this->record($this->driver)->getTitleHtml()?>
-    </a>
-  </div>
+<div class="media">
+  <div class="media-body">
+    <div class="resultItemLine1">
+      <a href="<?=$this->escapeHtmlAttr($url)?>" class="title">
+        <?=$this->record($this->driver)->getTitleHtml()?>
+      </a>
+    </div>
 
-  <div class="resultItemLine2">
-    <? $snippet = $this->driver->getHighlightedSnippet(); ?>
-    <? $summary = $this->driver->getSummary(); ?>
-    <? if (!empty($snippet)): ?>
-      <?=$this->highlight($snippet['snippet'])?>
-    <? elseif (!empty($summary)): ?>
-      <?=$this->escapeHtml($summary[0])?>
-    <? endif; ?>
-  </div>
+    <div class="resultItemLine2">
+      <? $snippet = $this->driver->getHighlightedSnippet(); ?>
+      <? $summary = $this->driver->getSummary(); ?>
+      <? if (!empty($snippet)): ?>
+        <?=$this->highlight($snippet['snippet'])?>
+      <? elseif (!empty($summary)): ?>
+        <?=$this->escapeHtml($summary[0])?>
+      <? endif; ?>
+    </div>
 
-  <div class="resultItemLine3">
-    <?=$this->escapeHtml($url)?>
-    <? $lastMod = $this->driver->getLastModified(); if (!empty($lastMod)): ?>
-      <br /><?=$this->transEsc('Last Modified')?>: <?=$this->escapeHtml(trim(str_replace(array('T', 'Z'), ' ', $lastMod)))?>
-    <? endif; ?>
+    <div class="resultItemLine3">
+      <?=$this->escapeHtml($url)?>
+      <? $lastMod = $this->driver->getLastModified(); if (!empty($lastMod)): ?>
+        <br /><?=$this->transEsc('Last Modified')?>: <?=$this->escapeHtml(trim(str_replace(array('T', 'Z'), ' ', $lastMod)))?>
+      <? endif; ?>
+    </div>
   </div>
 </div>
\ No newline at end of file
diff --git a/themes/bootstrap3/templates/RecordTab/hierarchytree.phtml b/themes/bootstrap3/templates/RecordTab/hierarchytree.phtml
index fcac230f96122bc001ceb79dfc3ce644e2f6530c..710844ba4504c12787b9108f386ca7bdcccb9136 100644
--- a/themes/bootstrap3/templates/RecordTab/hierarchytree.phtml
+++ b/themes/bootstrap3/templates/RecordTab/hierarchytree.phtml
@@ -23,9 +23,9 @@
   <div id="treeSelector">
     <? foreach ($hierarchyTreeList as $hierarchy => $hierarchyTitle): ?>
       <? if($activeTree == $hierarchy): ?>
-        <i class="fa fa-sitemap"></i> <?=$this->escapeHtml($hierarchyTitle)?>
+        <i class="fa fa-sitemap" aria-hidden="true"></i> <?=$this->escapeHtml($hierarchyTitle)?>
       <? else: ?>
-        <i class="fa fa-sitemap text-muted"></i> <a href="<?=$this->recordLink()->getTabUrl($this->driver, 'HierarchyTree')?>?hierarchy=<?=urlencode($hierarchy)?>"><?=$this->escapeHtml($hierarchyTitle)?></a>
+        <i class="fa fa-sitemap text-muted" aria-hidden="true"></i> <a href="<?=$this->recordLink()->getTabUrl($this->driver, 'HierarchyTree')?>?hierarchy=<?=urlencode($hierarchy)?>"><?=$this->escapeHtml($hierarchyTitle)?></a>
       <? endif; ?>
     <? endforeach; ?>
   </div>
@@ -40,12 +40,12 @@
           <option value="Title"><?=$this->transEsc('Title')?></option>
         </select>
         <input type="submit" class="btn btn-default" value="<?=$this->transEsc('Search') ?>"/>
-        <i id="treeSearchLoadingImg" class="fa fa-spinner fa-spin hidden"></i>
+        <i id="treeSearchLoadingImg" class="fa fa-spinner fa-spin hidden" aria-hidden="true"></i>
       </div>
       <div id="treeSearchNoResults" class="alert alert-danger hidden"><?=$this->translate('nohit_heading')?></div>
       <div id="treeSearchLimitReached" class="alert alert-danger hidden"><?=$this->translate('tree_search_limit_reached_html', array('%%url%%' => $this->url('search-results'), '%%limit%%' => $this->tab->getSearchLimit()))?></div>
     <? endif; ?>
-    <div id="hierarchyLoading" class="hide"><i class="fa fa-spinner fa-spin"></i> <?=$this->transEsc("Loading")?>...</div>
+    <div id="hierarchyLoading" class="hide"><i class="fa fa-spinner fa-spin" aria-hidden="true"></i> <?=$this->transEsc("Loading")?>...</div>
     <div id="hierarchyTree" class="hierarchy-tree">
       <input type="hidden" value="<?=$this->escapeHtml($this->driver->getUniqueId())?>" class="hiddenRecordId" />
       <input type="hidden" value="<?=$this->escapeHtml($activeTree)?>" class="hiddenHierarchyId" />
diff --git a/themes/bootstrap3/templates/RecordTab/holdingsils.phtml b/themes/bootstrap3/templates/RecordTab/holdingsils.phtml
index 71aa0b283a7f7d893eac644bc44b640145532e91..683f14c74a5415fe374fe24c81c2415f9032d877 100644
--- a/themes/bootstrap3/templates/RecordTab/holdingsils.phtml
+++ b/themes/bootstrap3/templates/RecordTab/holdingsils.phtml
@@ -19,15 +19,7 @@
 
 <?=$this->context($this)->renderInContext('librarycards/selectcard.phtml', array('user' => $this->auth()->isLoggedIn())); ?>
 
-<? if ($offlineMode == "ils-offline"): ?>
-  <div class="alert alert-warning">
-    <h2><?=$this->transEsc('ils_offline_title')?></h2>
-    <p><strong><?=$this->transEsc('ils_offline_status')?></strong></p>
-    <p><?=$this->transEsc('ils_offline_holdings_message')?></p>
-    <? $supportEmail = $this->escapeHtmlAttr($this->systemEmail()); ?>
-    <p><a href="mailto:<?=$supportEmail?>"><?=$supportEmail?></a></p>
-  </div>
-<? endif; ?>
+<?=($offlineMode == "ils-offline") ? $this->render('Helpers/ils-offline.phtml', ['offlineModeMsg' => 'ils_offline_holdings_message']) : ''?>
 <? if (($this->ils()->getHoldsMode() == 'driver' && !empty($holdings)) || $this->ils()->getTitleHoldsMode() == 'driver'): ?>
   <? if ($account->loginEnabled() && $offlineMode != 'ils-offline'): ?>
     <? if (!$user): ?>
@@ -42,7 +34,7 @@
   <? endif; ?>
 <? endif; ?>
 <? $holdingTitleHold = $this->driver->tryMethod('getRealTimeTitleHold'); if (!empty($holdingTitleHold)): ?>
-    <a class="placehold" data-lightbox title="<?=$this->transEsc('request_place_text')?>" href="<?=$this->recordLink()->getRequestUrl($holdingTitleHold)?>"><i class="fa fa-flag"></i>&nbsp;<?=$this->transEsc('title_hold_place')?></a>
+    <a class="placehold" data-lightbox title="<?=$this->transEsc('request_place_text')?>" href="<?=$this->recordLink()->getRequestUrl($holdingTitleHold)?>"><i class="fa fa-flag" aria-hidden="true"></i>&nbsp;<?=$this->transEsc('title_hold_place')?></a>
 <? endif; ?>
 <? if (!empty($urls) || $openUrlActive): ?>
   <h3><?=$this->transEsc("Internet")?></h3>
@@ -68,7 +60,12 @@
     <th><?=$this->transEsc("Call Number")?>: </th>
     <td width="50%">
       <? foreach ($callNos as $callNo): ?>
-        <?=$this->escapeHtml($callNo)?><br />
+        <? if ($this->callnumberHandler): ?>
+          <a href="<?=$this->url('alphabrowse-home') ?>?source=<?=$this->escapeHtmlAttr($this->callnumberHandler) ?>&amp;from=<?=$this->escapeHtmlAttr($callNo) ?>"><?=$this->escapeHtml($callNo)?></a>
+        <? else: ?>
+          <?=$this->escapeHtml($callNo)?>
+        <? endif; ?>
+        <br />
       <? endforeach; ?>
     </td>
   </tr>
@@ -110,13 +107,13 @@
               <? /* Begin Available Items (Holds) */ ?>
                <span class="text-success"><?=$this->transEsc("Available")?><link property="availability" href="http://schema.org/InStock" /></span>
               <? if (!$block && isset($row['link']) && $row['link']): ?>
-                <a class="<?=$check ? 'checkRequest ' : ''?>placehold" data-lightbox href="<?=$this->recordLink()->getRequestUrl($row['link'])?>"><i class="fa fa-flag"></i>&nbsp;<?=$this->transEsc($check ? "Check Hold" : "Place a Hold")?></a>
+                <a class="<?=$check ? 'checkRequest ' : ''?>placehold" data-lightbox href="<?=$this->recordLink()->getRequestUrl($row['link'])?>"><i class="fa fa-flag" aria-hidden="true"></i>&nbsp;<?=$this->transEsc($check ? "Check Hold" : "Place a Hold")?></a>
               <? endif; ?>
               <? if (!$blockStorageRetrievalRequest && isset($row['storageRetrievalRequestLink']) && $row['storageRetrievalRequestLink']): ?>
-                <a class="<?=$checkStorageRetrievalRequest ? 'checkStorageRetrievalRequest ' : ''?> placeStorageRetrievalRequest" data-lightbox href="<?=$this->recordLink()->getRequestUrl($row['storageRetrievalRequestLink'])?>"><i class="fa fa-flag"></i>&nbsp;<?=$this->transEsc($checkStorageRetrievalRequest ? "storage_retrieval_request_check_text" : "storage_retrieval_request_place_text")?></a>
+                <a class="<?=$checkStorageRetrievalRequest ? 'checkStorageRetrievalRequest ' : ''?> placeStorageRetrievalRequest" data-lightbox href="<?=$this->recordLink()->getRequestUrl($row['storageRetrievalRequestLink'])?>"><i class="fa fa-flag" aria-hidden="true"></i>&nbsp;<?=$this->transEsc($checkStorageRetrievalRequest ? "storage_retrieval_request_check_text" : "storage_retrieval_request_place_text")?></a>
               <? endif; ?>
               <? if (!$blockILLRequest && isset($row['ILLRequestLink']) && $row['ILLRequestLink']): ?>
-                <a class="<?=$checkILLRequest ? 'checkILLRequest ' : ''?>placeILLRequest" data-lightbox href="<?=$this->recordLink()->getRequestUrl($row['ILLRequestLink'])?>"><i class="fa fa-flag"></i>&nbsp;<?=$this->transEsc($checkILLRequest ? "ill_request_check_text" : "ill_request_place_text")?></a>
+                <a class="<?=$checkILLRequest ? 'checkILLRequest ' : ''?>placeILLRequest" data-lightbox href="<?=$this->recordLink()->getRequestUrl($row['ILLRequestLink'])?>"><i class="fa fa-flag" aria-hidden="true"></i>&nbsp;<?=$this->transEsc($checkILLRequest ? "ill_request_check_text" : "ill_request_place_text")?></a>
               <? endif; ?>
             <? else: ?>
               <? /* Begin Unavailable Items (Recalls) */ ?>
@@ -129,7 +126,7 @@
                 <span><?=$this->transEsc("Requests")?>: <?=$this->escapeHtml($row['requests_placed'])?></span>
               <? endif; ?>
               <? if (!$block && isset($row['link']) && $row['link']): ?>
-                <a class="<?=$check ? 'checkRequest' : ''?> placehold" data-lightbox href="<?=$this->recordLink()->getRequestUrl($row['link'])?>"><i class="fa fa-flag"></i>&nbsp;<?=$this->transEsc($check ? "Check Recall" : "Recall This")?></a>
+                <a class="<?=$check ? 'checkRequest' : ''?> placehold" data-lightbox href="<?=$this->recordLink()->getRequestUrl($row['link'])?>"><i class="fa fa-flag" aria-hidden="true"></i>&nbsp;<?=$this->transEsc($check ? "Check Recall" : "Recall This")?></a>
               <? endif; ?>
             <? endif; ?>
             <? if (isset($row['item_notes'])): ?>
diff --git a/themes/bootstrap3/templates/RecordTab/map.phtml b/themes/bootstrap3/templates/RecordTab/map.phtml
index 374a6ef94f87b384a9b24bec932a10e323923df9..462bcbbfe9056ba87975088eeea2a8da8dbcdce0 100644
--- a/themes/bootstrap3/templates/RecordTab/map.phtml
+++ b/themes/bootstrap3/templates/RecordTab/map.phtml
@@ -1,13 +1,27 @@
-<script type="text/javascript" src="https://maps.googleapis.com/maps/api/js?v=3.5&sensor=false&language=<?=$this->layout()->userLang?>"></script>
-
-<script type="text/javascript">
-
-var markers;
-var markersData;
-var latlng;
-var myOptions;
-var map;
-var infowindow = new google.maps.InfoWindow({maxWidth: 480, minWidth: 480});
+<? $mapType = $this->tab->getMapType(); ?>
+<? if (isset($mapType) && ($mapType == 'openlayers')) : ?>
+  <? $this->headScript()->appendFile('vendor/ol/ol.js');
+  $this->headScript()->appendFile('map_tab_ol.js');
+  $this->headLink()->appendStylesheet('vendor/ol/ol.css');
+  $mapTabData = $this->tab->getMapTabData();
+  $popupTitle = $this->transEsc('map_results_label');
+  $params = array(json_encode($mapTabData), json_encode($popupTitle));
+  $jsParams = implode(', ', $params);
+  $jsLoad = "loadMapTab(" . $jsParams . ");"; ?>
+  <div id="wrap" style="width: 674px; height: 479px">
+    <div id="map-canvas" style="width: 100%; height: 100%"><div id="popup"></div></div>
+    <?=$this->inlineScript(\Zend\View\Helper\HeadScript::SCRIPT, $jsLoad, 'SET');?>
+  </div>
+<? endif; ?>
+<? if (isset($mapType) && ($mapType == 'google')) : ?>
+  <script type="text/javascript" src="https://maps.googleapis.com/maps/api/js?key=<?=$this->tab->getGoogleMapApiKey()?>&amp;language=<?=$this->layout()->userLang?>"></script>
+  <script type="text/javascript">
+  var markers;
+  var markersData;
+  var latlng;
+  var myOptions;
+  var map;
+  var infowindow = new google.maps.InfoWindow({maxWidth: 480, minWidth: 480});
   function initialize() {
     markersData = <?=$this->tab->getGoogleMapMarker()?>;
     latlng = new google.maps.LatLng(0, 0);
@@ -27,16 +41,15 @@ var infowindow = new google.maps.InfoWindow({maxWidth: 480, minWidth: 480});
   function showMarkers(){
     deleteOverlays();
     markers = [];
-
     for (var i = 0; i<markersData.length; i++){
-      var disTitle = markersData[i].title;
+      var disTitle = markersData[i][0].title ? markersData[i][0].title : '';
       var iconTitle = disTitle;
       if (disTitle.length>25){
           iconTitle = disTitle.substring(0,25) + "...";
       }
       var markerImg = "https://chart.googleapis.com/chart?chst=d_bubble_text_small&chld=edge_bc|" + iconTitle +"|EEEAE3|";
       var labelXoffset = 1 + disTitle.length * 4;
-      var latLng = new google.maps.LatLng(markersData[i].lat , markersData[i].lon)
+      var latLng = new google.maps.LatLng(markersData[i][0].lat , markersData[i][0].lon)
       var marker = new google.maps.Marker({
         position: latLng,
         map: map,
@@ -63,3 +76,4 @@ var infowindow = new google.maps.InfoWindow({maxWidth: 480, minWidth: 480});
 <div id="wrap" onload="initialize()" style="width: 674px; height: 479px">
   <div id="map_canvas" style="width: 100%; height: 100%"></div>
 </div>
+<? endif; ?>
diff --git a/themes/bootstrap3/templates/RecordTab/similaritemscarousel.phtml b/themes/bootstrap3/templates/RecordTab/similaritemscarousel.phtml
index 99cc5e98bd8c9afa1403be662426060bcfe405a4..c4f61873124ad969fdb2df935480e56673edf5ee 100644
--- a/themes/bootstrap3/templates/RecordTab/similaritemscarousel.phtml
+++ b/themes/bootstrap3/templates/RecordTab/similaritemscarousel.phtml
@@ -46,10 +46,10 @@
 
     <!-- Controls -->
     <a class="left carousel-control" href="#similar-items-carousel" role="button" data-slide="prev">
-      <span class="fa fa-chevron-left glyphicon-chevron-left"></span>
+      <span class="fa fa-chevron-left glyphicon-chevron-left" title="<?=$this->transEsc('Prev') ?>"></span>
     </a>
     <a class="right carousel-control" href="#similar-items-carousel" role="button" data-slide="next">
-      <span class="fa fa-chevron-right glyphicon-chevron-right"></span>
+      <span class="fa fa-chevron-right glyphicon-chevron-right" title="<?=$this->transEsc('Next') ?>"></span>
     </a>
   </div>
 <? else: ?>
diff --git a/themes/bootstrap3/templates/RecordTab/usercomments.phtml b/themes/bootstrap3/templates/RecordTab/usercomments.phtml
index b8ae275ca3e33bd4a6cec3c64fe436a3ebac8bf1..e00d5eaaffc30893cdbbf1b12b81efa4fca169e8 100644
--- a/themes/bootstrap3/templates/RecordTab/usercomments.phtml
+++ b/themes/bootstrap3/templates/RecordTab/usercomments.phtml
@@ -6,7 +6,7 @@
 <div class="comment-list">
   <?=$this->render('record/comments-list.phtml')?>
 </div>
-<form class="comment-form row" name="commentRecord" action="<?=$this->recordLink()->getActionUrl($this->driver, 'AddComment')?>" method="post">
+<form class="comment-form" name="commentRecord" action="<?=$this->recordLink()->getActionUrl($this->driver, 'AddComment')?>" method="post">
   <div class="row">
     <div class="col-sm-3 name">
       <input type="hidden" name="id" value="<?=$this->escapeHtmlAttr($this->driver->getUniqueId())?>"/>
@@ -17,9 +17,14 @@
       <? $user = $this->auth()->isLoggedIn() ?>
       <? if($user): ?>
         <textarea name="comment" class="form-control" rows="3" required></textarea><br/>
+        <? if ($this->tab->isRecaptchaActive()): ?>
+          <?=$this->recaptcha()->html(true, false) ?><br/>
+        <? else: ?>
+          <script>/* workaround for nested form bug */</script>
+        <? endif; ?>
         <input class="btn btn-primary" data-loading-text="<?=$this->transEsc('Submitting') ?>..." type="submit" value="<?=$this->transEsc("Add your comment")?>"/>
       <? else: ?>
-        <a href="<?=$this->url('myresearch-userlogin') ?>" class="btn btn-primary" data-lightbox title="Login"><i class="fa fa-sign-in"></i> <?=$this->transEsc("You must be logged in first") ?></a>
+        <a href="<?=$this->url('myresearch-userlogin') ?>" class="btn btn-primary" data-lightbox title="Login"><i class="fa fa-sign-in" aria-hidden="true"></i> <?=$this->transEsc("You must be logged in first") ?></a>
       <? endif; ?>
     </div>
   </div>
diff --git a/themes/bootstrap3/templates/ajax/status-full.phtml b/themes/bootstrap3/templates/ajax/status-full.phtml
index 992a216479e43f17b9ba980706dcab09304eeb10..a04420f3b63f42ea6a9a6737b5433a825845fadb 100644
--- a/themes/bootstrap3/templates/ajax/status-full.phtml
+++ b/themes/bootstrap3/templates/ajax/status-full.phtml
@@ -7,7 +7,7 @@
   <? $i = 0; foreach ($this->statusItems as $item): ?>
     <? if (++$i == 5) break; // Show no more than 5 items ?>
     <tr>
-      <td>
+      <td class="fullLocation">
         <? $locationText = $this->transEsc('location_' . $item['location'], array(), $item['location']); ?>
         <? if (isset($item['locationhref']) && $item['locationhref']): ?>
           <a href="<?=$item['locationhref']?>" target="_blank"><?=$locationText?></a>
@@ -15,8 +15,14 @@
           <?=$locationText?>
         <? endif; ?>
       </td>
-      <td><?=$this->escapeHtml($item['callnumber'])?></td>
-      <td>
+      <td class="fullCallnumber">
+        <? if ($this->callnumberHandler): ?>
+          <a href="<?=$this->url('alphabrowse-home') ?>?source=<?=$this->escapeHtmlAttr($this->callnumberHandler) ?>&amp;from=<?=$this->escapeHtmlAttr($item['callnumber']) ?>"><?=$this->escapeHtml($item['callnumber'])?></a>
+        <? else: ?>
+          <?=$this->escapeHtml($item['callnumber'])?>
+        <? endif; ?>
+      </td>
+      <td class="fullAvailability">
         <? if (isset($item['use_unknown_message']) && $item['use_unknown_message']): ?>
           <span><?=$this->transEsc("status_unknown_message")?></span>
         <? elseif ($item['availability']): ?>
diff --git a/themes/bootstrap3/templates/breadcrumbs/default.phtml b/themes/bootstrap3/templates/breadcrumbs/default.phtml
index 47e7561add0b7c025373334eadcc9bb4e514af4d..17d0d06fbcfdabfb2daa33ebcad2ff75b0deb186 100644
--- a/themes/bootstrap3/templates/breadcrumbs/default.phtml
+++ b/themes/bootstrap3/templates/breadcrumbs/default.phtml
@@ -1,5 +1,4 @@
 <li><a href="<?=$this->url('home')?>"><?=$this->transEsc('Home')?></a></li>
-<li><a href="<?=$this->url('vudl-default-collection')?>"><?=$this->transEsc('Collections') ?></a></li>
 <? $current = $this->layout()->breadcrumbs; $current = current($current); ?>
 <? foreach($current as $id=>$parent): ?>
   <li><a href="<?=$this->url('collection', array('id'=>$id)) ?>"><?=$parent ?></a></li>
diff --git a/themes/bootstrap3/templates/breadcrumbs/multi.phtml b/themes/bootstrap3/templates/breadcrumbs/multi.phtml
index 3c127eb2a3f7003c87ed997ed4b741f88d1d6b18..b1b633e9371b74ebd5cda213defdfbd811375944 100644
--- a/themes/bootstrap3/templates/breadcrumbs/multi.phtml
+++ b/themes/bootstrap3/templates/breadcrumbs/multi.phtml
@@ -1,12 +1,11 @@
 <li><a href="<?=$this->url('home') ?>">Home</a></li>
-<li><a href="<?=$this->url('vudl-default-collection') ?>">Collections</a></li>
 <li class="dropdown">
   <a data-toggle="dropdown" href="#">In <?=count($this->layout()->breadcrumbs) ?> Collections</a>
   <div class="dropdown-menu" role="menu" aria-labelledby="dLabel">
     <? foreach ($this->layout()->breadcrumbs as $trail): ?>
       <ul class="sub-breadcrumb">
       <? foreach ($trail as $id=>$title): ?>
-        <li><a href="<?=$this->url('vudl-record', array('id'=>$id))?>"><?=$title ?></a></li>
+        <li><a href="<?=$this->url('record', ['id'=>$id])?>"><?=$title ?></a></li>
       <? endforeach; ?>
       </ul>
     <? endforeach; ?>
diff --git a/themes/bootstrap3/templates/browse/home.phtml b/themes/bootstrap3/templates/browse/home.phtml
index 6cd5d8095d988a1f68a79c2f583c79642f440b87..4c6f2197d0c30a968ad973956cc4090bf62d4d08 100644
--- a/themes/bootstrap3/templates/browse/home.phtml
+++ b/themes/bootstrap3/templates/browse/home.phtml
@@ -15,7 +15,7 @@
     <? foreach ($this->browseOptions as $item=>$currentOption): ?>
       <a href="<?=$this->url('browse-' . strtolower($currentOption['action'])); ?>" class="list-group-item<? if($currentOption['action'] == $this->currentAction): ?> active<? endif; ?>">
         <?=$this->transEsc($currentOption['description']) ?>
-        <span class="pull-right flip"><i class="fa fa-angle-right"></i></span>
+        <span class="pull-right flip"><i class="fa fa-angle-right" title="<?=$this->transEsc('more') ?>"></i></span>
       </a>
     <? endforeach; ?>
   </div>
@@ -23,10 +23,10 @@
   <? if (!empty($this->categoryList)): ?>
     <div class="browse list-group col-sm-3<? if (!empty($this->secondaryList) || !empty($this->resultList)): ?> hidden-xs<? endif ?>" id="list2">
       <? foreach($this->categoryList as $findby=>$category): ?>
-        <a href="<?=$BROWSE_BASE ?>?findby=<?=urlencode($findby) ?>&query_field=<?=$this->browse()->getSolrField($findby, $this->currentAction) ?>" class="list-group-item clearfix<? if ($this->findby == $findby): ?> active<? endif; ?>">
+        <a href="<?=$BROWSE_BASE ?>?findby=<?=urlencode($findby) ?>&amp;query_field=<?=$this->browse()->getSolrField($findby, $this->currentAction) ?>" class="list-group-item clearfix<? if ($this->findby == $findby): ?> active<? endif; ?>">
           <? if(is_string($category)): ?>
             <?=$this->transEsc($category)?>
-            <span class="pull-right flip"><i class="fa fa-angle-right"></i></span>
+            <span class="pull-right flip"><i class="fa fa-angle-right" title="<?=$this->transEsc('more') ?>"></i></span>
           <? else: ?>
             <?=$this->transEsc($category['text'])?>
             <span class="badge"><?=number_format($category['count'])?></span>
@@ -40,14 +40,14 @@
     <div class="browse list-group col-sm-3<? if (!empty($this->resultList)): ?> hidden-xs<? endif ?>" id="list3">
     <? foreach($this->secondaryList as $secondary): ?>
       <? $url = $BROWSE_BASE . '?findby=' . urlencode($this->findby)
-          . '&category=' . urlencode($this->category)
-          . '&query=' . urlencode($secondary['value']);
+          . '&amp;category=' . urlencode($this->category)
+          . '&amp;query=' . urlencode($secondary['value']);
         if ($this->facetPrefix) {
-          $url .= '&facet_prefix=' . urlencode($secondary['displayText']);
+          $url .= '&amp;facet_prefix=' . urlencode($secondary['displayText']);
         }
         if ($this->secondaryParams) {
           foreach($this->secondaryParams as $var=>$val) {
-            $url .= '&' . $var .'=' . urlencode($val);
+            $url .= '&amp;' . $var .'=' . urlencode($val);
           }
         }
         $viewRecord = !empty($this->categoryList) && $this->currentAction != 'Tag' && $this->findby != 'alphabetical';
@@ -57,11 +57,11 @@
         <? if ($this->findby != 'alphabetical' && isset($secondary['count'])): ?>
           <span class="badge"><?=number_format($secondary['count']) ?></span>
         <? else: ?>
-          <span class="pull-right flip"><i class="fa fa-angle-right"></i></span>
+          <span class="pull-right flip"><i class="fa fa-angle-right" title="<?=$this->transEsc('more') ?>"></i></span>
         <? endif; ?>
       </a>
       <? if($viewRecord): ?>
-        <a class="list-group-item view-record" href="<?=$SEARCH_BASE ?>?lookfor=<? if ($this->filter): ?>&filter[]=<?=urlencode($this->filter) ?>%3A<?=str_replace('+AND+','&filter[]=', urlencode($secondary['value'])) ?><? endif; ?>&filter[]=<?=$this->browse()->getSolrField($this->currentAction) ?>%3A[* TO *]<? if($this->dewey_flag):?>&sort=dewey-sort<?endif;?>"><?=$this->transEsc('View Records') ?></a>
+        <a class="list-group-item view-record" href="<?=$SEARCH_BASE ?>?lookfor=<? if ($this->filter): ?>&amp;filter[]=<?=urlencode($this->filter) ?>%3A<?=str_replace('+AND+','&amp;filter[]=', urlencode($secondary['value'])) ?><? endif; ?>&amp;filter[]=<?=$this->browse()->getSolrField($this->currentAction) ?>%3A[* TO *]<? if($this->dewey_flag):?>&amp;sort=dewey-sort<?endif;?>"><?=$this->transEsc('View Records') ?></a>
       <? endif; ?>
     <? endforeach; ?>
     </div>
@@ -70,7 +70,7 @@
   <? if (!empty($this->resultList)): ?>
     <div class="browse list-group col-sm-3" id="list4">
     <? foreach($this->resultList as $result): ?>
-      <a class="list-group-item clearfix" href="<?=$SEARCH_BASE ?>?<?=$this->paramTitle ?><?=urlencode($result['value']) ?><? if ($this->searchParams): foreach($this->searchParams as $var=>$val): ?>&<?=$var ?>=<?=urlencode($val) ?><? endforeach;endif; ?>">
+      <a class="list-group-item clearfix" href="<?=$SEARCH_BASE ?>?<?=$this->paramTitle ?><?=urlencode($result['value']) ?><? if ($this->searchParams): foreach($this->searchParams as $var=>$val): ?>&amp;<?=$var ?>=<?=urlencode($val) ?><? endforeach;endif; ?>">
         <?=$this->escapeHtml($result['displayText'])?>
         <span class="badge"><?=number_format($result['count']) ?></span>
       </a>
diff --git a/themes/bootstrap3/templates/cart/cart.phtml b/themes/bootstrap3/templates/cart/cart.phtml
index e709d7017477755ff5f750fc3dfcb97f58025167..dfd5c6c2e57b915a21ab2fd605b8e1845c469b80 100644
--- a/themes/bootstrap3/templates/cart/cart.phtml
+++ b/themes/bootstrap3/templates/cart/cart.phtml
@@ -19,27 +19,27 @@
       </div>
       <? if ($this->userlist()->getMode() !== 'disabled'): ?>
         <button type="submit" class="btn btn-default" name="saveCart" title="<?=$this->transEsc('bookbag_save')?>" value="1">
-          <i class="fa fa-save"></i>
+          <i class="fa fa-save" aria-hidden="true"></i>
           <?=$this->transEsc('Save')?>
         </button>
       <? endif; ?>
       <button type="submit" class="btn btn-default" name="email" title="<?=$this->transEsc('bookbag_email')?>" value="1">
-        <i class="fa fa-envelope-o"></i>
+        <i class="fa fa-envelope-o" aria-hidden="true"></i>
         <?=$this->transEsc('Email')?>
       </button>
       <? $exportOptions = $this->export()->getActiveFormats('bulk'); if (count($exportOptions) > 0): ?>
         <button type="submit" class="btn btn-default" name="export" title="<?=$this->transEsc('bookbag_export')?>" value="1">
-          <i class="fa fa-list-alt"></i>
+          <i class="fa fa-list-alt" aria-hidden="true"></i>
           <?=$this->transEsc('Export')?>
         </button>
       <? endif; ?>
       <button type="submit" class="btn btn-default dropdown-toggle" name="print" title="<?=$this->transEsc('print_selected')?>" value="1">
-        <i class="fa fa-print"></i>
+        <i class="fa fa-print" aria-hidden="true"></i>
         <?=$this->transEsc('Print')?>
       </button>
       <div class="btn-group" id="cartDelete">
         <button type="submit" name="delete" class="btn btn-default dropdown-toggle" data-toggle="dropdown" id="cart-delete-label" value="1">
-          <i class="fa fa-trash"></i>
+          <i class="fa fa-trash" aria-hidden="true"></i>
           <?=$this->transEsc('Delete')?>
         </button>
         <ul class="dropdown-menu" role="menu" aria-labelledby="cart-delete-label">
@@ -49,7 +49,7 @@
       </div>
       <div class="btn-group">
         <button type="submit" class="btn btn-default dropdown-toggle" name="empty" data-toggle="dropdown" id="cart-empty-label" value="1">
-          <i class="fa fa-close"></i>
+          <i class="fa fa-close" aria-hidden="true"></i>
           <?=$this->transEsc('Empty Book Bag')?>
         </button>
         <ul class="dropdown-menu" role="menu" aria-labelledby="cart-empty-label">
diff --git a/themes/bootstrap3/templates/cart/export-success.phtml b/themes/bootstrap3/templates/cart/export-success.phtml
index 913f259893c3540b9e0fb7c7eb65246593c88e47..d3b57410399d54d82099b62798abc2ad4dcdd8f2 100644
--- a/themes/bootstrap3/templates/cart/export-success.phtml
+++ b/themes/bootstrap3/templates/cart/export-success.phtml
@@ -1,4 +1,4 @@
 <div class="text-center">
   <?=$this->transEsc('export_success')?>&nbsp;&mdash;&nbsp;
-  <a class="btn btn-primary" href="<?=$this->escapeHtmlAttr($this->url)?>"><?=$this->transEsc('export_download')?></a>
+  <a class="btn btn-primary <?=$this->exportType?>" href="<?=$this->escapeHtmlAttr($this->url)?>"><?=$this->transEsc('export_download')?></a>
 </div>
diff --git a/themes/bootstrap3/templates/cart/save.phtml b/themes/bootstrap3/templates/cart/save.phtml
index 90fb7d77956d55fc62b1e3bf59fb09750db3d0a8..a982ee7952ebfaf9ce43a6b3ecf1425c3fa8918f 100644
--- a/themes/bootstrap3/templates/cart/save.phtml
+++ b/themes/bootstrap3/templates/cart/save.phtml
@@ -48,7 +48,7 @@
           <option value=""><?=$this->transEsc('My Favorites') ?></option>
         <? endif; ?>
       </select>
-      <a class="btn btn-link" id="make-list"  href="<?=$this->url('editList', array('id' => 'NEW')) . '?' . implode('&', $idParams) ?>"><?=$this->transEsc('or create a new list'); ?></a>
+      <a class="btn btn-link" id="make-list"  href="<?=$this->url('editList', array('id' => 'NEW')) . '?' . implode('&amp;', $idParams) ?>"><?=$this->transEsc('or create a new list'); ?></a>
     </div>
   </div>
 
diff --git a/themes/bootstrap3/templates/collection/view.phtml b/themes/bootstrap3/templates/collection/view.phtml
index 0173e7b79afcb33c784604f23afcd98bff3edbe7..08b328f6ff679b60ca91fe9a6cdd901506b3c866 100644
--- a/themes/bootstrap3/templates/collection/view.phtml
+++ b/themes/bootstrap3/templates/collection/view.phtml
@@ -27,33 +27,47 @@
 <? if (isset($this->scrollData) && ($this->scrollData['previousRecord'] || $this->scrollData['nextRecord'])): ?>
   <ul class="pager">
     <? if ($this->scrollData['previousRecord']): ?>
+      <? if ($this->scrollData['firstRecord']): ?>
+        <li>
+          <a href="<?=$this->recordLink()->getUrl($this->scrollData['firstRecord'])?>" title="<?=$this->transEsc('First Search Result')?>" rel="nofollow">&laquo; <?=$this->transEsc('First')?></a>
+        </li>
+      <? endif; ?>
       <li>
-        <a href="<?=$this->recordLink()->getUrl($this->scrollData['previousRecord'])?>" title="<?=$this->transEsc('Previous Search Result')?>">&laquo; <?=$this->transEsc('Prev')?></a>
+        <a href="<?=$this->recordLink()->getUrl($this->scrollData['previousRecord'])?>" title="<?=$this->transEsc('Previous Search Result')?>" rel="nofollow">&laquo; <?=$this->transEsc('Prev')?></a>
       </li>
     <? else: ?>
+      <? if ($this->scrollData['firstRecord']): ?>
+        <li class="disabled"><a href="#">&laquo; <?=$this->transEsc('First')?></a></li>
+      <? endif; ?>
       <li class="disabled"><a href="#">&laquo; <?=$this->transEsc('Prev')?></a></li>
     <? endif; ?>
     #<?=$this->localizedNumber($this->scrollData['currentPosition']) . ' ' . $this->transEsc('of') . ' ' . $this->localizedNumber($this->scrollData['resultTotal']) . ' ' . $this->transEsc('results') ?>
     <? if ($this->scrollData['nextRecord']): ?>
       <li>
-        <a href="<?=$this->recordLink()->getUrl($this->scrollData['nextRecord'])?>" title="<?=$this->transEsc('Next Search Result')?>"><?=$this->transEsc('Next')?> &raquo;</a>
+        <a href="<?=$this->recordLink()->getUrl($this->scrollData['nextRecord'])?>" title="<?=$this->transEsc('Next Search Result')?>" rel="nofollow"><?=$this->transEsc('Next')?> &raquo;</a>
       </li>
-    <? else: ?>
+      <? if ($this->scrollData['lastRecord']): ?>
+        <li>
+          <a href="<?=$this->recordLink()->getUrl($this->scrollData['lastRecord'])?>" title="<?=$this->transEsc('Last Search Result')?>" rel="nofollow"><?=$this->transEsc('Last')?> &raquo;</a>
+        </li>
+      <? endif; ?>
+     <? else: ?>
       <li class="disabled"><a href="#"><?=$this->transEsc('Next')?> &raquo;</a></li>
+      <? if ($this->scrollData['lastRecord']): ?>
+        <li class="disabled"><a href="#"><?=$this->transEsc('Last')?> &raquo;</a></li>
+      <? endif; ?>
     <? endif; ?>
   </ul>
 <? endif; ?>
 
 <?=$this->record($this->driver)->getToolbar()?>
 
-<div class="row">
+<div class="record row">
   <div class="<?=$tree ? 'col-sm-12' : $this->layoutClass('mainbody') ?>">
-    <div class="record">
-      <input type="hidden" value="<?=$this->escapeHtmlAttr($this->driver->getUniqueId())?>" class="hiddenId" id="record_id" />
-      <input type="hidden" value="<?=$this->escapeHtmlAttr($this->driver->getSourceIdentifier())?>" class="hiddenSource" />
-      <?=$this->flashmessages()?>
-      <?=$this->record($this->driver)->getCollectionMetadata()?>
-    </div>
+    <input type="hidden" value="<?=$this->escapeHtmlAttr($this->driver->getUniqueId())?>" class="hiddenId" id="record_id" />
+    <input type="hidden" value="<?=$this->escapeHtmlAttr($this->driver->getSourceIdentifier())?>" class="hiddenSource" />
+    <?=$this->flashmessages()?>
+    <?=$this->record($this->driver)->getCollectionMetadata()?>
 
     <? if (count($this->tabs) > 0): ?>
       <a name="tabnav"></a>
@@ -75,7 +89,7 @@
               if (!$obj->supportsAjax()) { $tab_classes[] = 'noajax'; }
             ?>
             <li<?=count($tab_classes) > 0 ? ' class="' . implode(' ', $tab_classes) . '"' : ''?>>
-              <a class="<?=strtolower($tab) ?>" href="<?=$this->recordLink()->getTabUrl($this->driver, $tab)?>#tabnav"><?=$this->transEsc($desc)?></a>
+              <a class="<?=strtolower($tab) ?>" href="<?=$this->recordLink()->getTabUrl($this->driver, $tab)?>#tabnav"<? if ($obj->supportsAjax() && in_array($tab, $this->backgroundTabs)):?> data-background<? endif ?>><?=$this->transEsc($desc)?></a>
             </li>
           <? endforeach; ?>
         </ul>
diff --git a/themes/bootstrap3/templates/collections/list.phtml b/themes/bootstrap3/templates/collections/list.phtml
index c43957ef32ca65bc3dcf403fc4bb774eee1a1597..cc5fc011ae0d6f2a7bd44e6c33ae827900417afa 100644
--- a/themes/bootstrap3/templates/collections/list.phtml
+++ b/themes/bootstrap3/templates/collections/list.phtml
@@ -4,7 +4,7 @@
       <strong><?=$this->escapeHtml($item['displayText'])?></strong>
       <span class="badge">
         <?=$item['count']?> <?=$this->transEsc('items')?>
-        <i class="fa fa-angle-right"></i>
+        <i class="fa fa-angle-right" title="<?=$this->transEsc('Find More') ?>"></i>
       </span>
     </a>
   <? endforeach; ?>
diff --git a/themes/bootstrap3/templates/combined/results-ajax.phtml b/themes/bootstrap3/templates/combined/results-ajax.phtml
index d4e6bcabe6b64106220f9c144bc8aeba0ccc61f6..d2df4e75ff1933e2c756d4cd5a93001030a2bc51 100644
--- a/themes/bootstrap3/templates/combined/results-ajax.phtml
+++ b/themes/bootstrap3/templates/combined/results-ajax.phtml
@@ -21,6 +21,6 @@ JS;
 
 ?>
 <h2><?=$this->transEsc($currentSearch['label'])?></h2>
-<p><i class="fa fa-spinner fa-spin"></i> <?=$this->transEsc("Loading")?>...</p>
+<p><i class="fa fa-spinner fa-spin" aria-hidden="true"></i> <?=$this->transEsc("Loading")?>...</p>
 <?=$this->inlineScript(\Zend\View\Helper\HeadScript::SCRIPT, $loadJs, 'SET')?>
 <noscript><?=$this->transEsc('Please enable JavaScript.')?></noscript>
diff --git a/themes/bootstrap3/templates/combined/results-list.phtml b/themes/bootstrap3/templates/combined/results-list.phtml
index 905c167b9bf364afdc593e696dbf123dbce4ff22..5c8fe93ca8986066109068f25e041ee56bcb9aeb 100644
--- a/themes/bootstrap3/templates/combined/results-list.phtml
+++ b/themes/bootstrap3/templates/combined/results-list.phtml
@@ -13,7 +13,7 @@
 ?>
 <? if (isset($currentSearch['more_link']) && $currentSearch['more_link']): ?>
   <div class="pull-right flip">
-    <a href="<?=$moreUrl?>" class="btn btn-link"><i class="fa fa-gears"></i> <?=$this->transEsc('More options')?></a>
+    <a href="<?=$moreUrl?>" class="btn btn-link"><i class="fa fa-gears" aria-hidden="true"></i> <?=$this->transEsc('More options')?></a>
   </div>
   <h2><a href="<?=$moreUrl?>"><?=$this->transEsc($currentSearch['label'])?></a></h2>
 <? else: ?>
@@ -80,6 +80,6 @@
   ?>
   <?=$this->render('search/list-' . $viewType . '.phtml', $viewParams)?>
   <? if (isset($currentSearch['more_link']) && $currentSearch['more_link']): ?>
-    <p><a href="<?=$moreUrl?>"><?=$this->transEsc($currentSearch['more_link'])?> <i class="fa fa-long-arrow-right"></i></a></p>
+    <p><a href="<?=$moreUrl?>"><?=$this->transEsc($currentSearch['more_link'])?> <i class="fa fa-long-arrow-right" aria-hidden="true"></i></a></p>
   <? endif; ?>
 <? endif; ?>
diff --git a/themes/bootstrap3/templates/eds/advanced.phtml b/themes/bootstrap3/templates/eds/advanced.phtml
index 19177171e2928c74485cadf618bd7cc6877cc928..32be6c84edf7ce77165bc931e9cdafe0eee6427d 100644
--- a/themes/bootstrap3/templates/eds/advanced.phtml
+++ b/themes/bootstrap3/templates/eds/advanced.phtml
@@ -48,7 +48,7 @@
         </div>
       <? endif; ?>
     <? endfor; ?>
-    <i class="fa fa-plus-circle search_place_holder hidden"></i> <a href="#" class="add_search_link hidden"><?=$this->transEsc("add_search")?></a>
+    <i class="fa fa-plus-circle search_place_holder hidden" aria-hidden="true"></i> <a href="#" class="add_search_link hidden"><?=$this->transEsc("add_search")?></a>
   </div>
 </div>
 <?
diff --git a/themes/bootstrap3/templates/footer.phtml b/themes/bootstrap3/templates/footer.phtml
index cd0aa5fddd1e8aa6b881b4d22e3049d73ed81a7f..abdcdeeae069685e051fb6c3ee113dcb9a97c29e 100644
--- a/themes/bootstrap3/templates/footer.phtml
+++ b/themes/bootstrap3/templates/footer.phtml
@@ -19,7 +19,7 @@
   <div class="col-sm-4">
     <p><strong><?=$this->transEsc('Need Help?')?></strong></p>
     <ul>
-      <li><a href="<?=$this->url('help-home')?>?topic=search" data-lightbox class="help-link"><?=$this->transEsc('Search Tips')?></a></li>
+      <li><a href="<?=$this->url('help-home')?>?topic=search&amp;_=<?=time() ?>" data-lightbox class="help-link"><?=$this->transEsc('Search Tips')?></a></li>
       <li><a href="#"><?=$this->transEsc('Ask a Librarian')?></a></li>
       <li><a href="#"><?=$this->transEsc('FAQs')?></a></li>
     </ul>
diff --git a/themes/bootstrap3/templates/header.phtml b/themes/bootstrap3/templates/header.phtml
index 51e4b28db07289fe261e327f29e5b1168d3bfe6e..bdf98502dab6c0192a1641ee5a41aa18af618f74 100644
--- a/themes/bootstrap3/templates/header.phtml
+++ b/themes/bootstrap3/templates/header.phtml
@@ -2,7 +2,7 @@
 <div class="navbar-header">
   <button type="button" class="navbar-toggle" data-toggle="collapse" data-target="#header-collapse">
     <span class="sr-only">Toggle navigation</span>
-    <i class="fa fa-bars"></i>
+    <i class="fa fa-bars" aria-hidden="true"></i>
   </button>
   <a class="navbar-brand lang-<?=$this->layout()->userLang ?>" href="<?=$this->url('home')?>">VuFind</a>
 </div>
@@ -17,26 +17,29 @@
       <ul role="navigation" class="nav navbar-nav navbar-right flip">
         <? if ($this->feedback()->tabEnabled()): ?>
           <li>
-            <a id="feedbackLink" data-lightbox href="<?=$this->url('feedback-home') ?>"><i class="fa fa-envelope"></i> <?=$this->transEsc("Feedback")?></a>
+            <a id="feedbackLink" data-lightbox href="<?=$this->url('feedback-home') ?>"><i class="fa fa-envelope" aria-hidden="true"></i> <?=$this->transEsc("Feedback")?></a>
           </li>
         <? endif; ?>
         <? $cart = $this->cart(); if ($cart->isActive()): ?>
           <li id="cartSummary">
-            <a id="cartItems" data-lightbox title="<?=$this->transEsc('View Book Bag')?>" href="<?=$this->url('cart-home')?>"><i class="fa fa-suitcase"></i> <strong><?=count($cart->getItems())?></strong> <?=$this->transEsc('items')?><?=$cart->isFull() ? ' (' .  $this->transEsc('bookbag_full') . ')' : ''?></a>
+            <a id="cartItems" data-lightbox title="<?=$this->transEsc('View Book Bag')?>" href="<?=$this->url('cart-home')?>">
+              <i class="fa fa-suitcase" aria-hidden="true"></i> <strong><?=count($cart->getItems())?></strong> <?=$this->transEsc('items')?>
+              <span class="full<?=!$cart->isFull() ? ' hidden' : '' ?>">(<?=$this->transEsc('bookbag_full') ?>)</span>
+            </a>
           </li>
         <? endif; ?>
         <? if (is_object($account) && $account->loginEnabled()): // hide login/logout if unavailable ?>
           <li class="logoutOptions<? if(!$account->isLoggedIn()): ?> hidden<? endif ?>">
-            <a href="<?=$this->url('myresearch-home', array(), array('query' => array('redirect' => 0)))?>"><i class="fa fa-home"></i> <?=$this->transEsc("Your Account")?></a>
+            <a href="<?=$this->url('myresearch-home', array(), array('query' => array('redirect' => 0)))?>"><i class="fa fa-home" aria-hidden="true"></i> <?=$this->transEsc("Your Account")?></a>
           </li>
           <li class="logoutOptions<? if(!$account->isLoggedIn()): ?> hidden<? endif ?>">
-            <a href="<?=$this->url('myresearch-logout')?>" class="logout"><i class="fa fa-sign-out"></i> <?=$this->transEsc("Log Out")?></a>
+            <a href="<?=$this->url('myresearch-logout')?>" class="logout"><i class="fa fa-sign-out" aria-hidden="true"></i> <?=$this->transEsc("Log Out")?></a>
           </li>
           <li id="loginOptions"<? if($account->isLoggedIn()): ?> class="hidden"<? endif ?>>
             <? if ($account->getSessionInitiator($this->serverUrl($this->url('myresearch-home')))): ?>
-              <a href="<?=$this->url('myresearch-userlogin')?>"><i class="fa fa-sign-in"></i> <?=$this->transEsc("Institutional Login")?></a>
+              <a href="<?=$this->url('myresearch-userlogin')?>"><i class="fa fa-sign-in" aria-hidden="true"></i> <?=$this->transEsc("Institutional Login")?></a>
             <? else: ?>
-              <a href="<?=$this->url('myresearch-userlogin')?>" data-lightbox><i class="fa fa-sign-in"></i> <?=$this->transEsc("Login")?></a>
+              <a href="<?=$this->url('myresearch-userlogin')?>" data-lightbox><i class="fa fa-sign-in" aria-hidden="true"></i> <?=$this->transEsc("Login")?></a>
             <? endif; ?>
           </li>
         <? endif; ?>
diff --git a/themes/bootstrap3/templates/layout/layout.phtml b/themes/bootstrap3/templates/layout/layout.phtml
index f1ac62d08fceabeb783c0c022f701f92b683bc0c..acb94aa761405b30a51c40de71b6c9fcd5968f8d 100644
--- a/themes/bootstrap3/templates/layout/layout.phtml
+++ b/themes/bootstrap3/templates/layout/layout.phtml
@@ -34,6 +34,7 @@
             'bulk_noitems_advice' => 'bulk_noitems_advice',
             'bulk_save_success' => 'bulk_save_success',
             'close' => 'close',
+            'collection_empty' => 'collection_empty',
             'error_occurred' => 'An error has occurred',
             'go_to_list' => 'go_to_list',
             'libphonenumber_invalid' => 'libphonenumber_invalid',
@@ -44,8 +45,11 @@
             'libphonenumber_tooshort' => 'libphonenumber_tooshort',
             'libphonenumber_tooshortidd' => 'libphonenumber_tooshortidd',
             'loading' => 'Loading',
-            'sms_success' => 'sms_success',
-            'number_thousands_separator' => ['number_thousands_separator', null, ',']
+            'more' => 'more',
+            'number_thousands_separator' => [
+                'number_thousands_separator', null, ','
+            ],
+            'sms_success' => 'sms_success'
           )
         );
         // Add libphonenumber.js strings
@@ -71,6 +75,12 @@
               'VuFind.cart.setDomain("' . $domain . '");'
             );
           }
+          $cookiePath = $cart->getCookiePath();
+          if (!empty($cookiePath)) {
+            $this->headScript()->appendScript(
+              'VuFind.cart.setCookiePath("' . $cookiePath . '");'
+            );
+          }
           $this->jsTranslations()->addStrings(
             array(
               'addBookBag' => 'Add to Book Bag',
@@ -124,6 +134,9 @@ JS;
     ?>
     <header class="hidden-print">
       <div class="container navbar">
+        <? if (isset($this->layout()->srmessage)): // message for benefit of screen-reader users ?>
+          <span class="sr-only"><?=$this->layout()->srmessage ?></span>
+        <? endif; ?>
         <a class="sr-only" href="#content"><?=$this->transEsc('Skip to content') ?></a>
         <?=$this->render('header.phtml')?>
       </div>
@@ -176,9 +189,12 @@ JS;
         </div>
       </div>
     </div>
-    <div class="offcanvas-toggle" data-toggle="offcanvas"><i class="fa"></i></div>
+    <div class="offcanvas-toggle" data-toggle="offcanvas"><i class="fa" title="<?=$this->transEsc('sidebar_expand') ?>"></i></div>
     <div class="offcanvas-overlay" data-toggle="offcanvas"></div>
     <?=$this->googleanalytics()?>
     <?=$this->piwik()?>
+    <? if ($this->recaptcha()->active()): ?>
+      <?=$this->inlineScript(\Zend\View\Helper\HeadScript::FILE, "https://www.google.com/recaptcha/api.js?onload=recaptchaOnLoad&render=explicit&hl=" . $this->layout()->userLang, 'SET')?>
+    <? endif; ?>
   </body>
 </html>
diff --git a/themes/bootstrap3/templates/layout/lightbox.phtml b/themes/bootstrap3/templates/layout/lightbox.phtml
index 49913abe32ac9f9873079bad2d4b40a013114bb3..dbde8d57ff6f31fed0d44eb4995f0f334f921ac6 100644
--- a/themes/bootstrap3/templates/layout/lightbox.phtml
+++ b/themes/bootstrap3/templates/layout/lightbox.phtml
@@ -1 +1,3 @@
-<?=$this->layout()->content?>
\ No newline at end of file
+<?=$this->layout()->content?>
+<?=$this->piwik(['lightbox' => true])?>
+<?=$this->googleanalytics($this->serverUrl(true))?>
diff --git a/themes/bootstrap3/templates/librarycards/home.phtml b/themes/bootstrap3/templates/librarycards/home.phtml
index ed97650c237a5343f5ac0f60944ec060b13645e9..3d5eb9a978cf7f8f47081f7df3d48645b915215a 100644
--- a/themes/bootstrap3/templates/librarycards/home.phtml
+++ b/themes/bootstrap3/templates/librarycards/home.phtml
@@ -36,9 +36,9 @@
           <td><?=$this->escapeHtml($username)?></td>
           <td>
             <div class="btn-group">
-              <a class="btn btn-link" href="<?=$this->url('editLibraryCard') . $this->escapeHtmlAttr($record['id']) ?>" title="<?=$this->transEsc('Edit Library Card')?>"><i class="fa fa-edit"></i> <?=$this->transEsc('Edit')?></a>
+              <a class="btn btn-link" href="<?=$this->url('editLibraryCard') . $this->escapeHtmlAttr($record['id']) ?>" title="<?=$this->transEsc('Edit Library Card')?>"><i class="fa fa-edit" aria-hidden="true"></i> <?=$this->transEsc('Edit')?></a>
               <a class="btn btn-link dropdown-toggle" data-toggle="dropdown" href="<?=$this->url('librarycards-deletecard') ?>?cardID=<?=urlencode($record['id'])?>">
-                <i class="fa fa-trash-o"></i> <?=$this->transEsc('Delete')?>
+                <i class="fa fa-trash-o" aria-hidden="true"></i> <?=$this->transEsc('Delete')?>
               </a>
               <ul class="dropdown-menu">
                 <li><a href="<?=$this->url('librarycards-deletecard') ?>?cardID=<?=urlencode($record['id'])?>&amp;confirm=1"><?=$this->transEsc('confirm_dialog_yes') ?></a></li>
@@ -52,7 +52,7 @@
     <? endif; ?>
 
     <div class="btn-group">
-      <a href="<?=$this->url('editLibraryCard') ?>NEW" class="btn btn-link"><i class="fa fa-edit"></i> <?=$this->transEsc('Add a Library Card')?></a>
+      <a href="<?=$this->url('editLibraryCard') ?>NEW" class="btn btn-link"><i class="fa fa-edit" aria-hidden="true"></i> <?=$this->transEsc('Add a Library Card')?></a>
     </div>
   </div>
 
diff --git a/themes/bootstrap3/templates/myresearch/account.phtml b/themes/bootstrap3/templates/myresearch/account.phtml
index 22e742f9f2dc645ac81aac466b1c94d11b019a2a..aebfcbd6a1a80622f49da20432ae1df4c5308d42 100644
--- a/themes/bootstrap3/templates/myresearch/account.phtml
+++ b/themes/bootstrap3/templates/myresearch/account.phtml
@@ -13,7 +13,7 @@
   <?=$this->recaptcha()->html($this->useRecaptcha) ?>
   <div class="form-group">
     <div class="col-sm-3">
-      <a class="back-to-login btn btn-link" href="<?=$this->url('myresearch-userlogin') ?>"><i class="fa fa-chevron-left"></i> <?=$this->transEsc('Back')?></a>
+      <a class="back-to-login btn btn-link" href="<?=$this->url('myresearch-userlogin') ?>"><i class="fa fa-chevron-left" aria-hidden="true"></i> <?=$this->transEsc('Back')?></a>
     </div>
     <div class="col-sm-9">
       <input class="btn btn-primary" type="submit" name="submit" value="<?=$this->transEsc('Submit')?>"/>
diff --git a/themes/bootstrap3/templates/myresearch/cataloglogin.phtml b/themes/bootstrap3/templates/myresearch/cataloglogin.phtml
index ffa78e801c6c0a34ccf3445fc05c14a1c0b719a0..7dfc11a0b36158399251e3a60e6e034f9eebe637 100644
--- a/themes/bootstrap3/templates/myresearch/cataloglogin.phtml
+++ b/themes/bootstrap3/templates/myresearch/cataloglogin.phtml
@@ -9,13 +9,7 @@
     $offlineMode = $this->ils()->getOfflineMode();
 ?>
 <? if ($offlineMode == "ils-offline"): ?>
-  <div class="alert alert-warning">
-    <h2><?=$this->transEsc('ils_offline_title')?></h2>
-    <p><strong><?=$this->transEsc('ils_offline_status')?></strong></p>
-    <p><?=$this->transEsc('ils_offline_login_message')?></p>
-    <? $supportEmail = $this->escapeHtmlAttr($this->systemEmail()); ?>
-    <p><a href="mailto:<?=$supportEmail?>"><?=$supportEmail?></a></p>
-  </div>
+  <?=$this->render('Helpers/ils-offline.phtml', ['offlineModeMsg' => 'ils_offline_login_message'])?>
 <? else: ?>
   <h3><?=$this->transEsc('Library Catalog Profile')?></h3>
   <?=$this->flashmessages()?>
diff --git a/themes/bootstrap3/templates/myresearch/checkedout.phtml b/themes/bootstrap3/templates/myresearch/checkedout.phtml
index bbcaf47496a53d086cfa856514fb11591ee37687..0faacfc2aaaeda01dcc4681973dbcdec11948e96 100644
--- a/themes/bootstrap3/templates/myresearch/checkedout.phtml
+++ b/themes/bootstrap3/templates/myresearch/checkedout.phtml
@@ -57,7 +57,7 @@
       <? $i = 0; foreach ($this->transactions as $resource): ?>
         <hr/>
         <? $ilsDetails = $resource->getExtraDetail('ils_details'); ?>
-        <div id="record<?=$this->escapeHtmlAttr($resource->getUniqueId())?>" class="row">
+        <div id="record<?=$this->escapeHtmlAttr($resource->getUniqueId())?>" class="row result">
           <? if ($this->renewForm): ?>
             <? if (isset($ilsDetails['renewable']) && $ilsDetails['renewable'] && isset($ilsDetails['renew_details'])): ?>
               <? $safeId = preg_replace('/[^a-zA-Z0-9]/', '', $ilsDetails['renew_details']); ?>
@@ -100,8 +100,8 @@
               <?=$this->transEsc('by')?>:
               <a href="<?=$this->record($resource)->getLink('author', $listAuthors[0])?>"><?=$this->escapeHtml($listAuthors[0])?></a><? if (count($listAuthors) > 1): ?>, <?=$this->transEsc('more_authors_abbrev')?><? endif; ?><br/>
             <? endif; ?>
-            <? $formats = $resource->getFormats(); if (count($formats) > 0): ?>
-              <?=str_replace('class="', 'class="label label-info ', $this->record($resource)->getFormatList())?>
+            <? if (count($resource->getFormats()) > 0): ?>
+              <?=$this->record($resource)->getFormatList() ?>
               <br/>
             <? endif; ?>
             <? if (!empty($ilsDetails['volume'])): ?>
diff --git a/themes/bootstrap3/templates/myresearch/delete.phtml b/themes/bootstrap3/templates/myresearch/delete.phtml
index dc620723c239e377d0d36704c23932ef4974ecdc..6c138d756207ea36fa2e53f111e8fb4bf7ac43de 100644
--- a/themes/bootstrap3/templates/myresearch/delete.phtml
+++ b/themes/bootstrap3/templates/myresearch/delete.phtml
@@ -1,7 +1,7 @@
-<form action="<?=$this->url('myresearch-delete')?>" method="post" name="bulkDelete">
+<h2><?=$this->transEsc('delete_selected_favorites')?></h2>
+ <form action="<?=$this->url('myresearch-delete')?>" method="post" name="bulkDelete" data-lightbox-onclose="VuFind.refreshPage();">
   <div id="popupMessages"><?=$this->flashmessages()?></div>
   <div id="popupDetails">
-    <h2><?=$this->transEsc('delete_selected_favorites')?></h2>
     <? if (!$this->list): ?>
       <div class="alert alert-info"><?=$this->transEsc("fav_delete_warn") ?></div>
     <? else: ?>
diff --git a/themes/bootstrap3/templates/myresearch/holds.phtml b/themes/bootstrap3/templates/myresearch/holds.phtml
index ee7b2a245c0075f959c53997bc2655f7b749a394..7f0e5e95b8450eef11f4d7cb1b5ef35456269422 100644
--- a/themes/bootstrap3/templates/myresearch/holds.phtml
+++ b/themes/bootstrap3/templates/myresearch/holds.phtml
@@ -42,7 +42,7 @@
         <hr/>
         <? $iteration++; ?>
         <? $ilsDetails = $resource->getExtraDetail('ils_details'); ?>
-        <div id="record<?=$this->escapeHtmlAttr($resource->getUniqueId()) ?>" class="row">
+        <div id="record<?=$this->escapeHtmlAttr($resource->getUniqueId()) ?>" class="row result">
           <? if ($this->cancelForm && isset($ilsDetails['cancel_details'])): ?>
             <? $safeId = preg_replace('/[^a-zA-Z0-9]/', '', $resource->getUniqueId()); ?>
             <input type="hidden" name="cancelAllIDS[]" value="<?=$this->escapeHtmlAttr($ilsDetails['cancel_details']) ?>" />
@@ -83,8 +83,8 @@
               <a href="<?=$this->record($resource)->getLink('author', $listAuthors[0])?>"><?=$this->escapeHtml($listAuthors[0])?></a><? if (count($listAuthors) > 1): ?>, <?=$this->transEsc('more_authors_abbrev')?><? endif; ?><br/>
             <? endif; ?>
 
-            <? $formats = $resource->getFormats(); if (count($formats) > 0): ?>
-              <?=str_replace('class="', 'class="label label-info ', $this->record($resource)->getFormatList())?>
+            <? if (count($resource->getFormats()) > 0): ?>
+              <?=$this->record($resource)->getFormatList() ?>
               <br/>
             <? endif; ?>
             <? if (isset($ilsDetails['volume']) && !empty($ilsDetails['volume'])): ?>
@@ -98,7 +98,7 @@
             <? endif; ?>
 
             <? if (!empty($ilsDetails['requestGroup'])): ?>
-              <strong><?=$this->transEsc('hold_requested_group') ?>:</strong> <?=$this->transEsc('location_' . $ilsDetails['requestGroup'], array(), $ilsDetails['requestGroup'])?>
+              <strong><?=$this->transEsc('hold_requested_group') ?>:</strong> <?=$this->transEsc('request_group_' . $ilsDetails['requestGroup'], null, $ilsDetails['requestGroup'])?>
               <br />
             <? endif; ?>
 
@@ -121,7 +121,7 @@
             <? endif; ?>
             <? if (!empty($pickupDisplay)): ?>
               <strong><?=$this->transEsc('pick_up_location') ?>:</strong>
-              <?=$pickupTranslate ? $this->transEsc($pickupDisplay) : $this->escapeHtml($pickupDisplay)?>
+              <?=$pickupTranslate ? $this->transEsc('location_' . $pickupDisplay, null, $pickupDisplay) : $this->escapeHtml($pickupDisplay)?>
               <br />
             <? endif; ?>
 
diff --git a/themes/bootstrap3/templates/myresearch/illrequests.phtml b/themes/bootstrap3/templates/myresearch/illrequests.phtml
index 1124741eed1dd576354c8118c99a967b417d4e34..cfb1f3fd85472dc5dc2aff4655bdbabff441304c 100644
--- a/themes/bootstrap3/templates/myresearch/illrequests.phtml
+++ b/themes/bootstrap3/templates/myresearch/illrequests.phtml
@@ -43,7 +43,7 @@
         <hr/>
         <? $iteration++; ?>
         <? $ilsDetails = $resource->getExtraDetail('ils_details'); ?>
-        <div id="record<?=$this->escapeHtmlAttr($resource->getUniqueId()) ?>" class="row">
+        <div id="record<?=$this->escapeHtmlAttr($resource->getUniqueId()) ?>" class="row result">
           <? if ($this->cancelForm && isset($ilsDetails['cancel_details'])): ?>
             <? $safeId = preg_replace('/[^a-zA-Z0-9]/', '', $resource->getUniqueId()); ?>
             <input type="hidden" name="cancelAllIDS[]" value="<?=$this->escapeHtmlAttr($ilsDetails['cancel_details']) ?>" />
@@ -84,8 +84,8 @@
               <a href="<?=$this->record($resource)->getLink('author', $listAuthors[0])?>"><?=$this->escapeHtml($listAuthors[0])?></a><? if (count($listAuthors) > 1): ?>, <?=$this->transEsc('more_authors_abbrev')?><? endif; ?><br/>
             <? endif; ?>
 
-            <? $formats = $resource->getFormats(); if (count($formats) > 0): ?>
-              <?=str_replace('class="', 'class="label label-info ', $this->record($resource)->getFormatList())?>
+            <? if (count($resource->getFormats()) > 0): ?>
+              <?=$this->record($resource)->getFormatList() ?>
               <br/>
             <? endif; ?>
             <? if (isset($ilsDetails['volume']) && !empty($ilsDetails['volume'])): ?>
diff --git a/themes/bootstrap3/templates/myresearch/login.phtml b/themes/bootstrap3/templates/myresearch/login.phtml
index 71a66d049737ce4a56d9750a7746e380f731ef65..4f0e20bc1616cfc23f18b36a580450572c3e6aaf 100644
--- a/themes/bootstrap3/templates/myresearch/login.phtml
+++ b/themes/bootstrap3/templates/myresearch/login.phtml
@@ -12,13 +12,7 @@
 ?>
 
 <? if ($offlineMode == "ils-offline"): ?>
-  <div class="alert alert-warning">
-    <h2><?=$this->transEsc('ils_offline_title')?></h2>
-    <p><strong><?=$this->transEsc('ils_offline_status')?></strong></p>
-    <p><?=$this->transEsc('ils_offline_login_message')?></p>
-    <? $supportEmail = $this->escapeHtmlAttr($this->systemEmail()); ?>
-    <p><a href="mailto:<?=$supportEmail?>"><?=$supportEmail?></a></p>
-  </div>
+  <?=$this->render('Helpers/ils-offline.phtml', ['offlineModeMsg' => 'ils_offline_login_message'])?>
 <? endif; ?>
 
 <h2 class="lightbox-header"><?=$this->transEsc('Login')?></h2>
diff --git a/themes/bootstrap3/templates/myresearch/menu.phtml b/themes/bootstrap3/templates/myresearch/menu.phtml
index 913110484841c4ef040c52911ceca7ae50d01e4e..33dea93d5d457efd5e45cb1f7c45edb81ab85eda 100644
--- a/themes/bootstrap3/templates/myresearch/menu.phtml
+++ b/themes/bootstrap3/templates/myresearch/menu.phtml
@@ -2,54 +2,54 @@
 <div class="list-group">
   <? if ($this->userlist()->getMode() !== 'disabled'): ?>
     <a href="<?=$this->url('myresearch-favorites')?>" class="list-group-item<?=$this->active == 'favorites' ? ' active' : ''?>">
-      <i class="fa fa-fw fa-star"></i> <?=$this->transEsc('Favorites')?>
+      <i class="fa fa-fw fa-star" aria-hidden="true"></i> <?=$this->transEsc('Favorites')?>
     </a>
   <? endif; ?>
   <? if ('ils-none' !== $this->ils()->getOfflineMode()): ?>
     <? if ($this->ils()->checkCapability('getMyTransactions')): ?>
       <a href="<?=$this->url('myresearch-checkedout')?>" class="list-group-item<?=$this->active == 'checkedout' ? ' active' : ''?>">
-        <i class="fa fa-fw fa-book"></i> <?=$this->transEsc('Checked Out Items')?>
+        <i class="fa fa-fw fa-book" aria-hidden="true"></i> <?=$this->transEsc('Checked Out Items')?>
       </a>
     <? endif; ?>
     <? if ($this->ils()->checkCapability('getMyHolds')): ?>
       <a href="<?=$this->url('myresearch-holds')?>" class="list-group-item<?=$this->active == 'holds' ? ' active' : ''?>">
-        <i class="fa fa-fw fa-flag"></i> <?=$this->transEsc('Holds and Recalls')?>
+        <i class="fa fa-fw fa-flag" aria-hidden="true"></i> <?=$this->transEsc('Holds and Recalls')?>
       </a>
     <? endif; ?>
     <? if ($this->ils()->checkFunction('StorageRetrievalRequests')): ?>
       <a href="<?=$this->url('myresearch-storageretrievalrequests')?>" class="list-group-item<?=$this->active == 'storageRetrievalRequests' ? ' active' : ''?>">
-        <i class="fa fa-fw fa-archive"></i> <?=$this->transEsc('Storage Retrieval Requests')?>
+        <i class="fa fa-fw fa-archive" aria-hidden="true"></i> <?=$this->transEsc('Storage Retrieval Requests')?>
       </a>
     <? endif; ?>
     <? if ($this->ils()->checkFunction('ILLRequests')): ?>
     <a href="<?=$this->url('myresearch-illrequests')?>" class="list-group-item<?=$this->active == 'ILLRequests' ? ' active' : ''?>">
-      <i class="fa fa-fw fa-exchange"></i> <?=$this->transEsc('Interlibrary Loan Requests')?>
+      <i class="fa fa-fw fa-exchange" aria-hidden="true"></i> <?=$this->transEsc('Interlibrary Loan Requests')?>
     </a>
     <? endif; ?>
     <? if ($this->ils()->checkCapability('getMyFines')): ?>
       <a href="<?=$this->url('myresearch-fines')?>" class="list-group-item<?=$this->active == 'fines' ? ' active' : ''?>">
-        <i class="fa fa-fw fa-usd"></i> <?=$this->transEsc('Fines')?>
+        <i class="fa fa-fw fa-usd" aria-hidden="true"></i> <?=$this->transEsc('Fines')?>
       </a>
     <? endif; ?>
     <? if ($this->ils()->checkCapability('getMyProfile')): ?>
       <a href="<?=$this->url('myresearch-profile')?>" class="list-group-item<?=$this->active == 'profile' ? ' active' : ''?>">
-        <i class="fa fa-fw fa-user"></i> <?=$this->transEsc('Profile')?>
+        <i class="fa fa-fw fa-user" aria-hidden="true"></i> <?=$this->transEsc('Profile')?>
       </a>
     <? endif; ?>
     <? $user = $this->auth()->isLoggedIn(); if ($user && $user->libraryCardsEnabled()): ?>
       <a href="<?=$this->url('librarycards-home')?>" class="list-group-item<?=$this->active == 'librarycards' ? ' active' : ''?>">
-        <i class="fa fa-fw fa-barcode"></i> <?=$this->transEsc('Library Cards')?>
+        <i class="fa fa-fw fa-barcode" aria-hidden="true"></i> <?=$this->transEsc('Library Cards')?>
       </a>
     <? endif; ?>
   <? endif; ?>
   <? if ($this->accountCapabilities()->getSavedSearchSetting() === 'enabled'): ?>
     <a href="<?=$this->url('search-history')?>?require_login" class="list-group-item<?=$this->active == 'history' ? ' active' : ''?>">
-      <i class="fa fa-fw fa-search"></i> <?=$this->transEsc('history_saved_searches')?>
+      <i class="fa fa-fw fa-search" aria-hidden="true"></i> <?=$this->transEsc('history_saved_searches')?>
     </a>
   <? endif; ?>
   <? if ($user = $this->auth()->isLoggedIn()): ?>
     <a href="<?=$this->url('myresearch-logout')?>" class="list-group-item">
-      <i class="fa fa-fw fa-sign-out"></i> <?=$this->transEsc("Log Out")?>
+      <i class="fa fa-fw fa-sign-out" aria-hidden="true"></i> <?=$this->transEsc("Log Out")?>
     </a>
   <? endif; ?>
 </div>
@@ -57,7 +57,7 @@
   <h4><?=$this->transEsc('Preferences')?></h4>
   <div class="list-group">
     <a href="<?=$this->url('myresearch-changepassword') ?>" class="list-group-item<?=$this->active == 'newpassword' ? ' active' : ''?>">
-      <i class="fa fa-fw fa-lock"></i> <?=$this->transEsc('Change Password') ?>
+      <i class="fa fa-fw fa-lock" aria-hidden="true"></i> <?=$this->transEsc('Change Password') ?>
     </a>
   </div>
 <? endif; ?>
@@ -65,7 +65,7 @@
   <h4><?=$this->transEsc('Your Lists')?></h4>
   <div class="list-group">
     <a href="<?=$this->url('myresearch-favorites')?>" class="list-group-item<?=$this->active == 'favorites' ? ' active' : ''?>">
-      <i class="fa fa-fw fa-star"></i> <?=$this->transEsc('Your Favorites')?>
+      <i class="fa fa-fw fa-star" aria-hidden="true"></i> <?=$this->transEsc('Your Favorites')?>
     </a>
     <? $lists = $user->getLists() ?>
     <? foreach ($lists as $list): ?>
@@ -75,7 +75,7 @@
         </a>
     <? endforeach; ?>
     <a href="<?=$this->url('editList', array('id'=>'NEW'))?>" class="list-group-item">
-      <i class="fa fa-fw fa-plus"></i> <?=$this->transEsc('Create a List') ?>
+      <i class="fa fa-fw fa-plus" aria-hidden="true"></i> <?=$this->transEsc('Create a List') ?>
     </a>
   </div>
 <? endif ?>
diff --git a/themes/bootstrap3/templates/myresearch/mylist.phtml b/themes/bootstrap3/templates/myresearch/mylist.phtml
index c508549a8cde990ef43383a78823f43dc7eb141d..df9aae819b5691b0198f9019faf019024385f915 100644
--- a/themes/bootstrap3/templates/myresearch/mylist.phtml
+++ b/themes/bootstrap3/templates/myresearch/mylist.phtml
@@ -12,6 +12,12 @@
   // Load Javascript dependencies into header:
   $this->headScript()->appendFile("check_item_statuses.js");
 
+  // Load Javascript only if list view parameter is NOT full:
+  if ($this->params->getOptions()->getListViewOption()!="full") {
+    $this->headScript()->appendFile("record.js");
+    $this->headScript()->appendFile("embedded_record.js");
+  }
+
   $recordTotal = $this->results->getResultTotal();
 
   // Convenience variable:
@@ -28,10 +34,10 @@
       <div class="pull-right flip">
         <? if (isset($list)): ?>
           <? if ($list->editAllowed($account->isLoggedIn())): ?>
-            <a href="<?=$this->url('editList', array('id' => $list->id)) ?>" class="btn btn-link"><i class="fa fa-edit"></i> <?=$this->transEsc("edit_list")?></a>
+            <a href="<?=$this->url('editList', array('id' => $list->id)) ?>" class="btn btn-link"><i class="fa fa-edit" aria-hidden="true"></i> <?=$this->transEsc("edit_list")?></a>
             <div class="btn-group">
               <a class="btn btn-link dropdown-toggle" data-toggle="dropdown" href="<?=$this->url('myresearch-deletelist') ?>?listID=<?=urlencode($list->id)?>">
-                <i class="fa fa-trash-o"></i> <?=$this->transEsc("delete_list")?>
+                <i class="fa fa-trash-o" aria-hidden="true"></i> <?=$this->transEsc("delete_list")?>
               </a>
               <ul class="dropdown-menu">
                 <li><a href="<?=$this->url('myresearch-deletelist') ?>?listID=<?=urlencode($list->id)?>&amp;confirm=1"><?=$this->transEsc('confirm_dialog_yes') ?></a></li>
@@ -52,6 +58,7 @@
     <? if ($recordTotal > 0): ?>
       <div class="resulthead">
         <div class="pull-right flip">
+          <?=$this->render('search/controls/limit.phtml')?>
           <?=$this->render('search/controls/sort.phtml')?>
         </div>
       </div>
diff --git a/themes/bootstrap3/templates/myresearch/profile.phtml b/themes/bootstrap3/templates/myresearch/profile.phtml
index b8c25d720efa1f7c786741d683364713604597c0..24a25874fc6f3f278bfccbde3147b3968bd006b9 100644
--- a/themes/bootstrap3/templates/myresearch/profile.phtml
+++ b/themes/bootstrap3/templates/myresearch/profile.phtml
@@ -39,7 +39,7 @@
           <form id="profile_form" class="form-inline" method="post">
             <select id="home_library" name="home_library" class="form-control">
               <? foreach ($this->pickup as $lib): ?>
-                <option value="<?=$this->escapeHtmlAttr($lib['locationID'])?>"<?=($selected == $lib['locationID'])?' selected="selected"':''?>><?=$this->escapeHtml($lib['locationDisplay'])?></option>
+                <option value="<?=$this->escapeHtmlAttr($lib['locationID'])?>"<?=($selected == $lib['locationID'])?' selected="selected"':''?>><?=$this->transEsc('location_' . $lib['locationDisplay'], null, $lib['locationDisplay'])?></option>
               <? endforeach; ?>
             </select>
             <input class="btn btn-default" type="submit" value="<?=$this->transEsc('Save')?>" />
diff --git a/themes/bootstrap3/templates/myresearch/storageretrievalrequests.phtml b/themes/bootstrap3/templates/myresearch/storageretrievalrequests.phtml
index 2e47c9316c14d07ca26b4ceff33d5cac144b7e3f..fc8050b7e0c0c78d237f703f9c8ed0974d61a188 100644
--- a/themes/bootstrap3/templates/myresearch/storageretrievalrequests.phtml
+++ b/themes/bootstrap3/templates/myresearch/storageretrievalrequests.phtml
@@ -41,7 +41,7 @@
         <hr/>
         <? $iteration++; ?>
         <? $ilsDetails = $resource->getExtraDetail('ils_details'); ?>
-        <div id="record<?=$this->escapeHtmlAttr($resource->getUniqueId()) ?>" class="row">
+        <div id="record<?=$this->escapeHtmlAttr($resource->getUniqueId()) ?>" class="row result">
           <? if ($this->cancelForm && isset($ilsDetails['cancel_details'])): ?>
             <? $safeId = preg_replace('/[^a-zA-Z0-9]/', '', $resource->getUniqueId()); ?>
             <input type="hidden" name="cancelAllIDS[]" value="<?=$this->escapeHtmlAttr($ilsDetails['cancel_details']) ?>" />
@@ -81,8 +81,8 @@
               <a href="<?=$this->record($resource)->getLink('author', $listAuthors[0])?>"><?=$this->escapeHtml($listAuthors[0])?></a><? if (count($listAuthors) > 1): ?>, <?=$this->transEsc('more_authors_abbrev')?><? endif; ?><br/>
             <? endif; ?>
 
-            <? $formats = $resource->getFormats(); if (count($formats) > 0): ?>
-              <?=str_replace('class="', 'class="label label-info ', $this->record($resource)->getFormatList())?>
+            <? if (count($resource->getFormats()) > 0): ?>
+              <?=$this->record($resource)->getFormatList() ?>
               <br/>
             <? endif; ?>
             <? if (isset($ilsDetails['volume']) && !empty($ilsDetails['volume'])): ?>
diff --git a/themes/bootstrap3/templates/record/ajaxview-accordion.phtml b/themes/bootstrap3/templates/record/ajaxview-accordion.phtml
new file mode 100644
index 0000000000000000000000000000000000000000..c67b69aaa76762170bdc7cfd16fbc97517372d8a
--- /dev/null
+++ b/themes/bootstrap3/templates/record/ajaxview-accordion.phtml
@@ -0,0 +1,83 @@
+<?
+  $this->defaultTab = strtolower($this->defaultTab);
+  $idSuffix = $this->escapeHtmlAttr(md5($this->driver->getUniqueId() . '|' . $this->driver->getSourceIdentifier()));
+?>
+
+<div class="list-tabs panel-group" id="accordion_<?=$idSuffix?>">
+  <? $coreMetadata = $this->record($this->driver)->getCoreMetadata(); ?>
+  <? if (!empty($coreMetadata)): ?>
+    <div class="panel panel-default">
+      <div id="information_<?=$idSuffix?>" class="list-tab-toggle panel-heading loaded" data-toggle="collapse" data-parent="#accordion_<?=$idSuffix?>" data-target="#information_<?=$idSuffix?>-content">
+        <h4 class="panel-title">
+          <a class="accordion-toggle">
+            <?=$this->translate('ajaxview_label_information') ?>
+          </a>
+        </h4>
+      </div>
+      <div id="information_<?=$idSuffix?>-content" class="panel-collapse collapse<? if($this->defaultTab === 'information'): ?> in<? endif; ?>">
+        <div class="list-tab-content record panel-body">
+          <input type="hidden" value="<?=$this->escapeHtmlAttr($this->driver->getUniqueId())?>" class="hiddenId" id="record_id_<?=$idSuffix?>" />
+          <input type="hidden" value="<?=$this->escapeHtmlAttr($this->driver->getResourceSource()) ?>" class="hiddenSource" />
+          <?=$coreMetadata ?>
+        </div>
+      </div>
+    </div>
+  <? endif; ?>
+
+  <? $toolbar = $this->record($this->driver)->getToolbar(); ?>
+  <? if (!empty($toolbar)): ?>
+    <div class="panel panel-default">
+      <div id="tools_<?=$idSuffix?>" class="list-tab-toggle panel-heading loaded" data-toggle="collapse" data-parent="#accordion_<?=$idSuffix?>" data-target="#tools_<?=$idSuffix?>-content">
+        <h4 class="panel-title">
+          <a class="accordion-toggle">
+            <?=$this->translate('ajaxview_label_tools') ?>
+          </a>
+        </h4>
+      </div>
+      <div id="tools_<?=$idSuffix?>-content" class="panel-collapse collapse<? if($this->defaultTab === 'tools'): ?> in<? endif; ?>">
+        <div class="list-tab-content panel-body">
+          <?=$toolbar ?>
+        </div>
+      </div>
+    </div>
+  <? endif; ?>
+
+  <? $relatedList = $this->related()->getList($this->driver); ?>
+  <? if ($relatedList != null): ?>
+    <div class="panel panel-default">
+      <div id="related_<?=$idSuffix?>" class="list-tab-toggle panel-heading loaded" data-toggle="collapse" data-parent="#accordion_<?=$idSuffix?>" data-target="#related_<?=$idSuffix?>-content">
+        <h4 class="panel-title">
+          <a class="accordion-toggle">
+            <?=$this->transEsc("Related Items")?>
+          </a>
+        </h4>
+      </div>
+      <div id="related_<?=$idSuffix?>-content" class="panel-collapse collapse<? if($this->defaultTab === 'related'): ?> in<? endif; ?>">
+        <div class="list-tab-content panel-body">
+          <? foreach ($relatedList as $current): ?>
+            <?=$this->related()->render($current)?>
+          <? endforeach; ?>
+        </div>
+      </div>
+    </div>
+  <? endif; ?>
+  <? if (count($this->tabs) > 0): ?>
+    <? foreach ($this->tabs as $tab => $obj): ?>
+      <? // add current tab to breadcrumbs if applicable:
+        $desc = $obj->getDescription();
+        $tab_classes = array();
+        if (!$obj->isVisible()) { $tab_classes[] = 'hidden'; }
+        if (!$obj->supportsAjax()) { $tab_classes[] = 'noajax'; }
+        if ($this->defaultTab === strtolower($tab)) { $tab_classes[] = 'default'; }
+      ?>
+      <div class="panel panel-default <?=implode(' ', $tab_classes) ?>">
+        <div id="<?=strtolower($tab)?>_<?=$idSuffix?>" class="list-tab-toggle panel-heading" data-toggle="collapse" data-parent="#accordion_<?=$idSuffix?>" data-target="#<?=strtolower($tab)?>_<?=$idSuffix?>-content"<? if ($obj->supportsAjax() && in_array($tab, $this->backgroundTabs)):?> data-background<? endif ?>>
+          <h4 class="panel-title">
+            <a class="accordion-toggle" data-href="<?=$this->recordLink()->getTabUrl($this->driver, $tab)?>#tabnav"><?=$this->transEsc($desc) ?></a>
+          </h4>
+        </div>
+        <div id="<?=strtolower($tab)?>_<?=$idSuffix?>-content" class="list-tab-content panel-collapse collapse<? if($this->defaultTab === strtolower($tab)): ?> in<? endif; ?>"></div>
+      </div>
+    <? endforeach; ?>
+  <? endif; ?>
+</div>
diff --git a/themes/bootstrap3/templates/record/ajaxview-tabs.phtml b/themes/bootstrap3/templates/record/ajaxview-tabs.phtml
new file mode 100644
index 0000000000000000000000000000000000000000..a995b43151542b2ef4e59c86ded443ba0405e5c0
--- /dev/null
+++ b/themes/bootstrap3/templates/record/ajaxview-tabs.phtml
@@ -0,0 +1,73 @@
+<?
+  $this->defaultTab = strtolower($this->defaultTab);
+  $idSuffix = $this->escapeHtmlAttr(md5($this->driver->getUniqueId() . '|' . $this->driver->getSourceIdentifier()));
+?>
+<ul class="list-tabs nav nav-tabs">
+  <? $coreMetadata = trim($this->record($this->driver)->getCoreMetadata()); ?>
+  <? if (!empty($coreMetadata)): ?>
+    <li<? if($this->defaultTab === 'information'): ?> class="active"<? endif; ?>>
+      <a id="information_<?=$idSuffix?>" class="list-tab-toggle loaded" data-toggle="tab" data-target="#information_<?=$idSuffix?>-content" class="noajax">
+        <?=$this->translate('ajaxview_label_information') ?>
+      </a>
+    </li>
+  <? endif; ?>
+
+  <? $toolbar = trim($this->record($this->driver)->getToolbar()); ?>
+  <? if (!empty($toolbar)): ?>
+    <li<? if($this->defaultTab === 'tools'): ?> class="active"<? endif; ?>>
+      <a id="tools_<?=$idSuffix?>" class="list-tab-toggle loaded" data-toggle="tab" data-target="#tools_<?=$idSuffix?>-content" class="noajax">
+      <?=$this->translate('ajaxview_label_tools') ?>
+      </a>
+    </li>
+  <? endif; ?>
+
+  <? $list = $this->related()->getList($this->driver); ?>
+  <? if (!empty($list)): ?>
+    <li<? if($this->defaultTab === 'related'): ?> class="active"<? endif; ?>>
+      <a id="related_<?=$idSuffix?>" class="list-tab-toggle loaded" data-toggle="tab" data-target="#related_<?=$idSuffix?>-content" class="noajax">
+        <?=$this->transEsc("Related Items")?>
+      </a>
+    </li>
+  <? endif; ?>
+
+  <? foreach ($this->tabs as $tab => $obj): ?>
+    <? // add current tab to breadcrumbs if applicable:
+      $desc = $obj->getDescription();
+      $tab_classes = array();
+      if ($this->defaultTab === strtolower($tab)) {
+        if (!$this->ajaxTabs || !$obj->supportsAjax()) {
+          $tab_classes[] = 'active';
+        }
+      }
+      if (!$obj->isVisible()) { $tab_classes[] = 'hidden'; }
+      if (!$obj->supportsAjax()) { $tab_classes[] = 'noajax'; }
+    ?>
+    <li<?=count($tab_classes) > 0 ? ' class="' . implode(' ', $tab_classes) . '"' : ''?>>
+      <a id="<?=strtolower($tab)?>_<?=$idSuffix?>" class="list-tab-toggle" href="<?=$this->recordLink()->getTabUrl($this->driver, $tab)?>#tabnav" data-toggle="tab" data-target="#<?=strtolower($tab)?>_<?=$idSuffix?>-content"<? if ($obj->supportsAjax() && in_array($tab, $this->backgroundTabs)):?> data-background<? endif ?>><?=$this->transEsc($desc)?></a>
+    </li>
+  <? endforeach; ?>
+</ul>
+<div class="tab-content">
+  <? if (!empty($coreMetadata)): ?>
+    <div class="list-tab-content record tab-pane<? if($this->defaultTab === 'information'): ?> active<? endif; ?>" id="information_<?=$idSuffix?>-content">
+      <input type="hidden" value="<?=$this->escapeHtmlAttr($this->driver->getUniqueId())?>" class="hiddenId" id="record_id_<?=$idSuffix?>" />
+      <input type="hidden" value="<?=$this->escapeHtmlAttr($this->driver->getResourceSource()) ?>" class="hiddenSource" />
+      <?=$coreMetadata ?>
+    </div>
+  <? endif; ?>
+  <? if (!empty($toolbar)): ?>
+    <div class="list-tab-content tab-pane<? if($this->defaultTab === 'tools'): ?> active<? endif; ?>" id="tools_<?=$idSuffix?>-content">
+      <?=$toolbar ?>
+    </div>
+  <? endif; ?>
+  <? if (!empty($list)): ?>
+    <div class="list-tab-content tab-pane<? if($this->defaultTab === 'related'): ?> active<? endif; ?>" id="related_<?=$idSuffix?>-content">
+      <? foreach ($list as $current): ?>
+        <?=$this->related()->render($current)?>
+      <? endforeach; ?>
+    </div>
+  <? endif; ?>
+  <? foreach ($this->tabs as $tab => $obj): ?>
+    <div class="list-tab-content tab-pane<? if($this->defaultTab === strtolower($tab)): ?> active<? endif; ?>" id="<?=strtolower($tab)?>_<?=$idSuffix?>-content"></div>
+  <? endforeach; ?>
+</div>
diff --git a/themes/bootstrap3/templates/record/cover.phtml b/themes/bootstrap3/templates/record/cover.phtml
index 69295273c65b4ead89e78b5f1973b295151ba545..435cb53795041383fc3d79210076705b1f28c906 100644
--- a/themes/bootstrap3/templates/record/cover.phtml
+++ b/themes/bootstrap3/templates/record/cover.phtml
@@ -1,8 +1,8 @@
 <? /* Display thumbnail if appropriate: */ ?>
 <? if ($cover): ?>
   <? if ($this->link): ?><a href="<?=$this->escapeHtmlAttr($this->link)?>"><? endif; ?>
-  <img alt="<?=$this->transEsc('Cover Image')?>" class="recordcover" src="<?=$this->escapeHtmlAttr($cover); ?>"/>
+  <img alt="<?=$this->transEsc('Cover Image')?>" <? if ($linkPreview): ?>data-linkpreview="true" <? endif; ?>class="recordcover" src="<?=$this->escapeHtmlAttr($cover); ?>"/>
   <? if ($this->link): ?></a><? endif; ?>
 <? else: ?>
-  <img src="<?=$this->url('cover-unavailable')?>" class="recordcover" alt="<?=$this->transEsc('No Cover Image')?>"/>
+  <img src="<?=$this->url('cover-unavailable')?>" <? if ($linkPreview): ?>data-linkpreview="true" <? endif; ?>class="recordcover" alt="<?=$this->transEsc('No Cover Image')?>"/>
 <? endif; ?>
diff --git a/themes/bootstrap3/templates/record/hold.phtml b/themes/bootstrap3/templates/record/hold.phtml
index 649146efc7a7d16044be0e9ba0ddbfafa9d05378..5abbdf6c9b2472911a6106796e6d92cfba117183 100644
--- a/themes/bootstrap3/templates/record/hold.phtml
+++ b/themes/bootstrap3/templates/record/hold.phtml
@@ -15,6 +15,10 @@
 <div class="hold-form">
   <form class="form-horizontal" method="post" name="placeHold">
     <?=$this->flashmessages()?>
+    <label class="col-sm-3 control-label"><?=$this->transEsc('Title')?></label>
+    <div class="col-sm-9">
+      <p class="form-control-static"><?=$this->driver->getBreadcrumb() ?></p>
+    </div>
     <? if (in_array("comments", $this->extraHoldFields)): ?>
       <div class="form-group hold-comment">
         <label class="col-sm-3 control-label"><?=$this->transEsc("Comments")?>:</label>
diff --git a/themes/bootstrap3/templates/record/taglist.phtml b/themes/bootstrap3/templates/record/taglist.phtml
index 136e00d24f98424de690b6217a2ffec590738db1..aa6de6c4ed32013ef08bc4a365950f20f288333f 100644
--- a/themes/bootstrap3/templates/record/taglist.phtml
+++ b/themes/bootstrap3/templates/record/taglist.phtml
@@ -9,9 +9,9 @@
             <input type="hidden" name="tag" value="<?=$this->escapeHtmlAttr($tag['tag'])?>"/>
             <button type="submit" class="badge" onClick="ajaxTagUpdate(this, '<?=$this->escapeHtmlAttr($tag['tag'])?>', <?=$is_me ? 'true' : 'false' ?>);return false;"><?=$this->escapeHtml($tag['cnt']) ?>
             <? if($is_me): ?>
-              <i class="fa fa-close"></i>
+              <i class="fa fa-close" title="<?=$this->transEsc('delete_tag') ?>"></i>
             <? else: ?>
-              <i class="fa fa-plus"></i>
+              <i class="fa fa-plus" title="<?=$this->transEsc('Add Tag') ?>"></i>
             <? endif; ?>
             </button>
           </form>
diff --git a/themes/bootstrap3/templates/record/view.phtml b/themes/bootstrap3/templates/record/view.phtml
index c1ce3ff577992996e63c257d0054351ef73d7c1b..d17786fd5b43a7ddcd00221c1b919edda019ba75 100644
--- a/themes/bootstrap3/templates/record/view.phtml
+++ b/themes/bootstrap3/templates/record/view.phtml
@@ -21,10 +21,18 @@
 <? if (isset($this->scrollData) && ($this->scrollData['previousRecord'] || $this->scrollData['nextRecord'])): ?>
   <ul class="pager hidden-print">
     <? if ($this->scrollData['previousRecord']): ?>
+      <? if ($this->scrollData['firstRecord']): ?>
+        <li>
+          <a href="<?=$this->recordLink()->getUrl($this->scrollData['firstRecord'])?>" title="<?=$this->transEsc('First Search Result')?>" rel="nofollow">&laquo; <?=$this->transEsc('First')?></a>
+        </li>
+      <? endif; ?>
       <li>
         <a href="<?=$this->recordLink()->getUrl($this->scrollData['previousRecord'])?>" title="<?=$this->transEsc('Previous Search Result')?>" rel="nofollow">&laquo; <?=$this->transEsc('Prev')?></a>
       </li>
     <? else: ?>
+      <? if ($this->scrollData['firstRecord']): ?>
+        <li class="disabled"><a href="#">&laquo; <?=$this->transEsc('First')?></a></li>
+      <? endif; ?>
       <li class="disabled"><a href="#">&laquo; <?=$this->transEsc('Prev')?></a></li>
     <? endif; ?>
     #<?=$this->localizedNumber($this->scrollData['currentPosition']) . ' ' . $this->transEsc('of') . ' ' . $this->localizedNumber($this->scrollData['resultTotal']) . ' ' . $this->transEsc('results') ?>
@@ -32,22 +40,28 @@
       <li>
         <a href="<?=$this->recordLink()->getUrl($this->scrollData['nextRecord'])?>" title="<?=$this->transEsc('Next Search Result')?>" rel="nofollow"><?=$this->transEsc('Next')?> &raquo;</a>
       </li>
-    <? else: ?>
+      <? if ($this->scrollData['lastRecord']): ?>
+        <li>
+          <a href="<?=$this->recordLink()->getUrl($this->scrollData['lastRecord'])?>" title="<?=$this->transEsc('Last Search Result')?>" rel="nofollow"><?=$this->transEsc('Last')?> &raquo;</a>
+        </li>
+      <? endif; ?>
+     <? else: ?>
       <li class="disabled"><a href="#"><?=$this->transEsc('Next')?> &raquo;</a></li>
+      <? if ($this->scrollData['lastRecord']): ?>
+        <li class="disabled"><a href="#"><?=$this->transEsc('Last')?> &raquo;</a></li>
+      <? endif; ?>
     <? endif; ?>
   </ul>
 <? endif; ?>
 
 <?=$this->record($this->driver)->getToolbar()?>
 
-<div class="row">
+<div class="record source<?=$this->escapeHtmlAttr($this->driver->getSourceIdentifier())?> row">
   <div class="<?=$this->layoutClass('mainbody')?>">
-    <div class="record source<?=$this->escapeHtmlAttr($this->driver->getSourceIdentifier())?>">
-      <input type="hidden" value="<?=$this->escapeHtmlAttr($this->driver->getUniqueId())?>" class="hiddenId" />
-      <input type="hidden" value="<?=$this->escapeHtmlAttr($this->driver->getSourceIdentifier()) ?>" class="hiddenSource" />
-      <?=$this->flashmessages()?>
-      <?=$this->record($this->driver)->getCoreMetadata()?>
-    </div>
+    <input type="hidden" value="<?=$this->escapeHtmlAttr($this->driver->getUniqueId())?>" class="hiddenId" />
+    <input type="hidden" value="<?=$this->escapeHtmlAttr($this->driver->getSourceIdentifier()) ?>" class="hiddenSource" />
+    <?=$this->flashmessages()?>
+    <?=$this->record($this->driver)->getCoreMetadata()?>
 
     <? if (count($this->tabs) > 0): ?>
       <a name="tabnav"></a>
@@ -69,7 +83,7 @@
               if (!$obj->supportsAjax()) { $tab_classes[] = 'noajax'; }
             ?>
             <li<?=count($tab_classes) > 0 ? ' class="' . implode(' ', $tab_classes) . '"' : ''?>>
-              <a class="<?=strtolower($tab) ?>" href="<?=$this->recordLink()->getTabUrl($this->driver, $tab)?>#tabnav"><?=$this->transEsc($desc)?></a>
+              <a class="<?=strtolower($tab) ?>" href="<?=$this->recordLink()->getTabUrl($this->driver, $tab)?>#tabnav"<? if ($obj->supportsAjax() && in_array($tab, $this->backgroundTabs)):?> data-background<? endif ?>><?=$this->transEsc($desc)?></a>
             </li>
           <? endforeach; ?>
         </ul>
diff --git a/themes/bootstrap3/templates/search/advanced/eds.phtml b/themes/bootstrap3/templates/search/advanced/eds.phtml
index 6c953e9c71de8faaea80932b8c61a3de16ce11d0..a3fb6703c76984403a66acd6a91d66f9d2049ada 100644
--- a/themes/bootstrap3/templates/search/advanced/eds.phtml
+++ b/themes/bootstrap3/templates/search/advanced/eds.phtml
@@ -93,6 +93,7 @@
         </div>
         <?
           $this->headScript()->appendFile('vendor/bootstrap-slider.js');
+          $this->headLink()->appendStylesheet('vendor/bootstrap-slider.min.css');
           $min = !empty($current['values'][0]) ? min($current['values'][0], 1400) : 1400;
           $future = date('Y', time()+31536000);
           $max = !empty($current['values'][1]) ? max($future, $current['values'][1]) : $future;
@@ -118,7 +119,7 @@ $(document).ready(function() {
        'tooltip':"hide",
        'value':[{$low},{$high}]
     })
-    .on('slide', fillTexts)
+    .on('change', fillTexts)
     .data('slider');
   {$init}
 });
@@ -127,4 +128,4 @@ JS;
       <?=$this->inlineScript(\Zend\View\Helper\HeadScript::SCRIPT, $script, 'SET'); ?>
     </fieldset>
   <? endif; ?>
-</div>
\ No newline at end of file
+</div>
diff --git a/themes/bootstrap3/templates/search/advanced/layout.phtml b/themes/bootstrap3/templates/search/advanced/layout.phtml
index f1b0d28ff5622908723b80f69c3700a4e9accaae..4b68fd43938101432c7936da8be5b13cb4a1ece4 100644
--- a/themes/bootstrap3/templates/search/advanced/layout.phtml
+++ b/themes/bootstrap3/templates/search/advanced/layout.phtml
@@ -87,7 +87,7 @@
         </div>
         <? /* An empty div. This is the target for the javascript that builds this screen */ ?>
         <span id="groupPlaceHolder" class="hidden">
-          <i class="fa fa-plus-circle"></i> <a href="#" onClick="addGroup()"><?= $this->transEsc('add_search_group') ?></a>
+          <i class="fa fa-plus-circle" aria-hidden="true"></i> <a href="#" onClick="addGroup()"><?= $this->transEsc('add_search_group') ?></a>
         </span>
         <? /* fallback to a fixed set of search groups/fields if JavaScript is turned off */ ?>
         <div class="no-js">
@@ -129,7 +129,7 @@
                         </div>
                         <? if($group == 0 && $search == 0): ?>
                             </div>
-                          <i class="fa fa-plus-circle search_place_holder hidden"></i> <a href="#" class="add_search_link hidden"><?=$this->transEsc("add_search")?></a>
+                          <i class="fa fa-plus-circle search_place_holder hidden" aria-hidden="true"></i> <a href="#" class="add_search_link hidden"><?=$this->transEsc("add_search")?></a>
                         <? endif; ?>
                       <? endfor; ?>
                     </div>
@@ -187,8 +187,8 @@
         <div class="sidegroup">
           <h4><?=$this->transEsc("Search Tips")?></h4>
           <div class="list-group">
-            <a class="list-group-item help-link" data-lightbox href="<?=$this->url('help-home')?>?topic=advsearch"><?=$this->transEsc("Help with Advanced Search")?></a>
-            <a class="list-group-item help-link" data-lightbox href="<?=$this->url('help-home')?>?topic=search"><?=$this->transEsc("Help with Search Operators")?></a>
+            <a class="list-group-item help-link" data-lightbox href="<?=$this->url('help-home')?>?topic=advsearch&amp;_=<?=time() ?>"><?=$this->transEsc("Help with Advanced Search")?></a>
+            <a class="list-group-item help-link" data-lightbox href="<?=$this->url('help-home')?>?topic=search&amp;_=<?=time() ?>"><?=$this->transEsc("Help with Search Operators")?></a>
           </div>
         </div>
       </div>
diff --git a/themes/bootstrap3/templates/search/advanced/ranges.phtml b/themes/bootstrap3/templates/search/advanced/ranges.phtml
index f740da6d4ad96559cd67d98c02ebdfe220b1f814..44dfd9c3ade6a1ed4fdaf6678e6ef9db186f2016 100644
--- a/themes/bootstrap3/templates/search/advanced/ranges.phtml
+++ b/themes/bootstrap3/templates/search/advanced/ranges.phtml
@@ -21,6 +21,7 @@
         </div>
         <?
           $this->headScript()->appendFile('vendor/bootstrap-slider.js');
+          $this->headLink()->appendStylesheet('vendor/bootstrap-slider.min.css');
           $min = !empty($current['values'][0]) ? min($current['values'][0], 1400) : 1400;
           $future = date('Y', time()+31536000);
           $max = !empty($current['values'][1]) ? max($future, $current['values'][1]) : $future;
@@ -48,7 +49,7 @@ $(document).ready(function() {
        'value':[{$low},{$high}],
        'reversed': {$reversed}
     })
-    .on('slide', fillTexts)
+    .on('change', fillTexts)
     .data('slider');
   {$init}
 });
diff --git a/themes/bootstrap3/templates/search/facet-list.phtml b/themes/bootstrap3/templates/search/facet-list.phtml
new file mode 100644
index 0000000000000000000000000000000000000000..cb3a003e5760aa55ac4d36e6a6e6621f4f585ff7
--- /dev/null
+++ b/themes/bootstrap3/templates/search/facet-list.phtml
@@ -0,0 +1,75 @@
+<?
+  $options = $this->searchOptions($this->searchClassId);
+  $facetLightbox = $options->getFacetListAction();
+  if (empty($this->sortOptions)) {
+    $this->sort = 'default';
+    $this->sortOptions = [ 'default' => 'default' ];
+  }
+  $urlBase = $this->url($facetLightbox) . $results->getUrlQuery()->getParams() . '&amp;facet=' . urlencode($this->facet) . '&amp;facetexclude=' . $this->exclude . '&amp;facetop=' . $this->operator;
+?>
+<h2><?=$this->transEsc($this->facetLabel) ?></h2>
+<? if (count($this->sortOptions) > 1): ?>
+  <div class="full-facet-sort-options">
+    <label><?=$this->translate('Sort') ?></label>
+    <div class="btn-group">
+      <? foreach ($this->sortOptions as $key=>$sort): ?>
+        <a href="<?=$urlBase . '&amp;facetpage=1&amp;facetsort=' . urlencode($key) ?>" class="btn btn-default js-facet-sort<? if($this->sort == $key): ?> active<? endif; ?>" data-sort="<?=$key ?>" data-lightbox-ignore><?=$this->translate($sort) ?></a>
+      <? endforeach; ?>
+    </div>
+  </div>
+<? endif; ?>
+<div class="lightbox-scroll full-facets">
+  <? foreach ($this->sortOptions as $key=>$sort): ?>
+    <? $active = $this->sort == $key; ?>
+    <div class="full-facet-list list-group<? if(!$active): ?> hidden<? endif; ?>" id="facet-list-<?=$this->escapeHtmlAttr($key) ?>">
+      <? if ($active): ?>
+        <? if ($this->page > 1): ?>
+          <a href="<?=$urlBase . '&amp;facetpage=' . ($this->page-1) . '&amp;facetsort=' . $this->sort ?>" class="list-group-item js-facet-prev-page" data-page="<?=($this->page+1) ?>" data-sort="<?=$this->sort ?>" data-limit="<?=count($this->data) ?>" data-lightbox-ignore><?=$this->translate('prev') ?> ...</a>
+        <? endif; ?>
+        <? foreach ($this->data as $item): ?>
+          <? $toggleUrl = $item['isApplied']
+              ? $this->url($options->getSearchAction()) . $this->results->getUrlQuery()->removeFacet($this->facet, $item['value'], $item['operator'])
+              : $this->url($options->getSearchAction()) . $this->results->getUrlQuery()->addFacet($this->facet, $item['value'], $item['operator'])
+          ?>
+          <? $subLinks = $this->exclude && !$item['isApplied']; ?>
+          <? if ($subLinks): ?>
+            <li class="list-group-item js-facet-item">
+              <a href="<?=$toggleUrl ?>" data-lightbox-ignore data-title="<?=$this->escapeHtmlAttr($item['displayText']) ?>" data-count="<?=$item['count'] ?>"<? if($item['isApplied']): ?> title="<?=$this->transEsc('applied_filter') ?>"<? endif;?>>
+          <? else: ?>
+            <a href="<?=$toggleUrl ?>" data-lightbox-ignore class="js-facet-item list-group-item<? if($item['isApplied']): ?> active<?endif;?>" data-title="<?=$this->escapeHtmlAttr($item['displayText']) ?>" data-count="<?=$item['count'] ?>"<? if($item['isApplied']): ?> title="<?=$this->transEsc('applied_filter') ?>"<? endif;?>>
+          <? endif; ?>
+              <? if (!empty($item['displayText'])): ?>
+                <?=$this->escapeHtml($item['displayText']) ?>
+              <? else: ?>
+                <?=$this->escapeHtml($item['value']) ?>
+              <? endif; ?>
+            <? if ($subLinks): ?>
+              </a>
+            <? endif; ?>
+            <? if($item['isApplied']): ?>
+              <? if ($item['operator'] == 'OR'): ?>
+                <i class="fa fa-check-square-o" aria-hidden="true"></i>
+              <? else: ?>
+                <span class="pull-right flip">
+                  <i class="fa fa-check" aria-hidden="true"></i>
+                </span>
+              <? endif; ?>
+            <? else: ?>
+              <span class="badge">
+                <?=$this->localizedNumber($item['count']) ?>
+                <? if ($this->exclude): ?>
+                  <a href="<?=$this->url($options->getSearchAction()) . $this->results->getUrlQuery()->addFacet($this->facet, $item['value'], 'NOT') ?>" title="<?=$this->transEsc('exclude_facet') ?>" data-lightbox-ignore><i class="fa fa-times" aria-hidden="true"></i></a>
+                <? endif; ?>
+              </span>
+            <? endif; ?>
+          <?=$subLinks ? '</li>' : '</a>'; ?>
+        <? endforeach; ?>
+      <? endif; ?>
+      <? if ($this->anotherPage): ?>
+        <a href="<?=$urlBase . '&amp;facetpage=' . ($this->page+1) . '&amp;facetsort=' . urlencode($key) ?>" class="list-group-item js-facet-next-page" data-page="<?=($this->page+1) ?>" data-sort="<?=$this->escapeHtmlAttr($key) ?>" data-lightbox-ignore><?=$this->translate('more') ?> ...</a>
+      <? endif; ?>
+    </div>
+  <? endforeach; ?>
+</div>
+<button class="btn btn-default lightbox-only" data-dismiss="modal"><?=$this->translate('close') ?></button>
+<?=$this->inlineScript(\Zend\View\Helper\HeadScript::SCRIPT, '(typeof VuFind.lightbox_facets !== "undefined") && VuFind.lightbox_facets.setup();', 'SET'); ?>
diff --git a/themes/bootstrap3/templates/search/history-table.phtml b/themes/bootstrap3/templates/search/history-table.phtml
index 27126e25d9bc515c0411f387f6f79b1f3d17dace..a916c37c7c99dccb7af0a18cd8af6f9da0e4adfb 100644
--- a/themes/bootstrap3/templates/search/history-table.phtml
+++ b/themes/bootstrap3/templates/search/history-table.phtml
@@ -34,9 +34,9 @@
       <? if ($saveSupported): ?>
         <td>
           <? if ($this->showSaved): ?>
-            <a href="<?=$this->url('myresearch-savesearch')?>?delete=<?=urlencode($info->getSearchId())?>&amp;mode=history"><i class="fa fa-remove"></i> <?=$this->transEsc("history_delete_link")?></a>
+            <a href="<?=$this->url('myresearch-savesearch')?>?delete=<?=urlencode($info->getSearchId())?>&amp;mode=history"><i class="fa fa-remove" aria-hidden="true"></i> <?=$this->transEsc("history_delete_link")?></a>
           <? else: ?>
-            <a href="<?=$this->url('myresearch-savesearch')?>?save=<?=urlencode($info->getSearchId())?>&amp;mode=history"><i class="fa fa-save"></i> <?=$this->transEsc("history_save_link")?></a>
+            <a href="<?=$this->url('myresearch-savesearch')?>?save=<?=urlencode($info->getSearchId())?>&amp;mode=history"><i class="fa fa-save" aria-hidden="true"></i> <?=$this->transEsc("history_save_link")?></a>
           <? endif; ?>
         </td>
       <? endif; ?>
diff --git a/themes/bootstrap3/templates/search/history.phtml b/themes/bootstrap3/templates/search/history.phtml
index ae765b1f04c6fa55508b7455424aeade01cf7cd8..377a1020bfd6f482326c8120b2e864ac16499d3a 100644
--- a/themes/bootstrap3/templates/search/history.phtml
+++ b/themes/bootstrap3/templates/search/history.phtml
@@ -20,7 +20,7 @@
     <h2><?=$this->transEsc("history_recent_searches")?></h2>
     <? if (!empty($this->unsaved)): ?>
       <?=$this->context()->renderInContext('search/history-table.phtml', array('showSaved' => false));?>
-      <a href="?purge=true"><i class="fa fa-remove"></i> <?=$this->transEsc("history_purge")?></a>
+      <a href="?purge=true"><i class="fa fa-remove" aria-hidden="true"></i> <?=$this->transEsc("history_purge")?></a>
     <? else: ?>
       <?=$this->transEsc("history_no_searches")?>
     <? endif; ?>
diff --git a/themes/bootstrap3/templates/search/home.phtml b/themes/bootstrap3/templates/search/home.phtml
index a161235b597db1923fb7103411c572e82969aa10..0ff456b7b4899e80425ba97150b5d78b58308728 100644
--- a/themes/bootstrap3/templates/search/home.phtml
+++ b/themes/bootstrap3/templates/search/home.phtml
@@ -19,15 +19,7 @@
 ?>
 
 <div class="searchHomeContent">
-  <? if ($this->ils()->getOfflineMode() == "ils-offline"): ?>
-    <div class="alert alert-warning">
-      <h2><?=$this->transEsc('ils_offline_title')?></h2>
-      <p><strong><?=$this->transEsc('ils_offline_status')?></strong></p>
-      <p><?=$this->transEsc('ils_offline_home_message')?></p>
-      <? $supportEmail = $this->escapeHtmlAttr($this->systemEmail()); ?>
-      <p><a href="mailto:<?=$supportEmail?>"><?=$supportEmail?></a></p>
-    </div>
-  <? endif; ?>
+  <?=($this->ils()->getOfflineMode() == "ils-offline") ? $this->render('Helpers/ils-offline.phtml', ['offlineModeMsg' => 'ils_offline_home_message']) : ''?>
   <div class="well well-lg clearfix" role="search">
     <?=$this->context($this)->renderInContext("search/searchbox.phtml", ['ignoreHiddenFilterMemory' => true])?>
   </div>
diff --git a/themes/bootstrap3/templates/search/list-list.phtml b/themes/bootstrap3/templates/search/list-list.phtml
index 48418bfdda63490364fad4205dc99848d0b8c548..b6adf0cd9e9f3ab7262720834173953cdc6e12b0 100644
--- a/themes/bootstrap3/templates/search/list-list.phtml
+++ b/themes/bootstrap3/templates/search/list-list.phtml
@@ -3,14 +3,16 @@
   || (isset($this->showBulkOptions) && $this->showBulkOptions); ?>
 <? $i = $this->indexStart; foreach ($this->results->getResults() as $current):
   $recordNumber = $this->results->getStartRecord()+$i-$this->indexStart; ?>
-  <div id="result<?=$i++ ?>" class="row result clearfix">
-    <div class="col-xs-1 hidden-print<? if ($showCheckboxes): ?> checkbox<? endif; ?>">
-      <label>
-        <? if ($showCheckboxes): ?>
+  <div id="result<?=$i++ ?>" class="result<?=$current->supportsAjaxStatus()?' ajaxItem':''?>">
+    <div class="checkbox hidden-print">
+      <? if ($showCheckboxes): ?>
+        <label>
           <?=$this->record($current)->getCheckbox()?>
-        <? endif; ?>
-        <?=$recordNumber?>
-      </label>
+          <?=$recordNumber ?>
+        </label>
+      <? else: ?>
+        <?=$recordNumber ?>
+      <? endif; ?>
     </div>
     <?=$this->record($current)->getSearchResult('list')?>
   </div>
diff --git a/themes/bootstrap3/templates/search/reservessearch.phtml b/themes/bootstrap3/templates/search/reservessearch.phtml
index 02a1ee675fbb7653b74680886091857c65c7dd00..5157b1bf30b3bdd888e99e8f00f1e3370763cf6f 100644
--- a/themes/bootstrap3/templates/search/reservessearch.phtml
+++ b/themes/bootstrap3/templates/search/reservessearch.phtml
@@ -7,6 +7,8 @@
 
     // Convenience variables:
     $reservesLookfor = $this->params->getDisplayQuery();
+
+    $this->searchClassId = 'SolrReserves';
 ?>
 
 <div class="row">
@@ -26,15 +28,17 @@
           <strong><?=$this->localizedNumber($this->results->getStartRecord())?></strong> - <strong><?=$this->localizedNumber($this->results->getEndRecord())?></strong>
           <?=$this->transEsc('of')?> <strong><?=$this->localizedNumber($recordTotal)?></strong>
           <?=$this->transEsc('for search')?>: <strong>'<?=$this->escapeHtml($reservesLookfor)?>'</strong>,
-        <? endif; ?>
-        <? if ($qtime = $this->results->getQuerySpeed()): ?>
-          <?=$this->transEsc('query time')?>: <?=$this->localizedNumber($qtime, 2).$this->transEsc('seconds_abbrev')?>
+          <? if ($qtime = $this->results->getQuerySpeed()): ?>
+            <?=$this->transEsc('query time')?>: <?=$this->localizedNumber($qtime, 2).$this->transEsc('seconds_abbrev')?>
+          <? endif; ?>
         <? endif; ?>
       </div>
 
-      <div class="pull-right flip">
-        <?=$this->render('search/controls/sort.phtml')?>
-      </div>
+      <? if ($recordTotal > 0): ?>
+        <div class="pull-right flip">
+          <?=$this->render('search/controls/sort.phtml')?>
+        </div>
+      <? endif; ?>
     </div>
     <? if ($recordTotal < 1): ?>
       <p class="error"><?=$this->transEsc('nohit_prefix')?> - <strong><?=$this->escapeHtml($reservesLookfor)?></strong> - <?=$this->transEsc('nohit_suffix')?></p>
diff --git a/themes/bootstrap3/templates/search/results.phtml b/themes/bootstrap3/templates/search/results.phtml
index 7dbd281dcea14798a94af05e382c00f4b640ccd2..c0fb8203084d52e7b5cfe6565929bda832df295a 100644
--- a/themes/bootstrap3/templates/search/results.phtml
+++ b/themes/bootstrap3/templates/search/results.phtml
@@ -37,6 +37,12 @@
   // Enable bulk options if appropriate:
   $this->showBulkOptions = $this->params->getOptions()->supportsCart() && $this->showBulkOptions;
 
+  // Load Javascript only if list view parameter is NOT full:
+  if ($this->params->getOptions()->getListViewOption() != "full") {
+    $this->headScript()->appendFile("record.js");
+    $this->headScript()->appendFile("embedded_record.js");
+  }
+
   // Load Javascript dependencies into header:
   $this->headScript()->appendFile("check_item_statuses.js");
   $this->headScript()->appendFile("check_save_statuses.js");
@@ -56,6 +62,7 @@
           <?=$this->transEsc("Showing")?>
           <strong><?=$this->localizedNumber($this->results->getStartRecord())?></strong> - <strong><?=$this->localizedNumber($this->results->getEndRecord())?></strong>
           <? if (!isset($this->skipTotalCount)): ?>
+            <? $this->layout()->srmessage = $this->transEsc('result_count', ['%%count%%' => $this->localizedNumber($recordTotal)]); ?>
             <?=$this->transEsc('of')?> <strong><?=$this->localizedNumber($recordTotal)?></strong>
           <? endif; ?>
           <? if (isset($this->overrideSearchHeading)): ?>
@@ -86,7 +93,8 @@
         <? if (isset($this->overrideEmptyMessage)): ?>
           <?=$this->overrideEmptyMessage?>
         <? else: ?>
-          <?=$this->transEsc('nohit_prefix')?> - <strong><?=$this->escapeHtml($lookfor)?></strong> - <?=$this->transEsc('nohit_suffix')?>
+          <? $this->layout()->srmessage = $this->transEsc('nohit_prefix') . ' - ' . $this->escapeHtml($lookfor) . ' - ' . $this->transEsc('nohit_suffix'); ?>
+          <?=$this->layout()->srmessage ?>
         <? endif; ?>
       </p>
       <? if (isset($this->parseError)): ?>
@@ -110,18 +118,18 @@
 
       <div class="searchtools hidden-print">
         <strong><?=$this->transEsc('Search Tools')?>:</strong>
-        <a href="<?=$this->results->getUrlQuery()->setViewParam('rss')?>"><i class="fa fa-bell"></i> <?=$this->transEsc('Get RSS Feed')?></a>
+        <a href="<?=$this->results->getUrlQuery()->setViewParam('rss')?>"><i class="fa fa-bell" aria-hidden="true"></i> <?=$this->transEsc('Get RSS Feed')?></a>
         &mdash;
         <a href="<?=$this->url('search-email')?>" class="mailSearch" data-lightbox id="mailSearch<?=$this->escapeHtmlAttr($this->results->getSearchId())?>">
-          <i class="fa fa-envelope"></i> <?=$this->transEsc('Email this Search')?>
+          <i class="fa fa-envelope" aria-hidden="true"></i> <?=$this->transEsc('Email this Search')?>
         </a>
         <? if ($this->accountCapabilities()->getSavedSearchSetting() === 'enabled'): ?>
           &mdash;
           <? if (is_numeric($this->results->getSearchId())): ?>
             <? if ($this->results->isSavedSearch()): ?>
-              <a href="<?=$this->url('myresearch-savesearch')?>?delete=<?=urlencode($this->results->getSearchId())?>"><i class="fa fa-remove"></i> <?=$this->transEsc('save_search_remove')?></a>
+              <a href="<?=$this->url('myresearch-savesearch')?>?delete=<?=urlencode($this->results->getSearchId())?>"><i class="fa fa-remove" aria-hidden="true"></i> <?=$this->transEsc('save_search_remove')?></a>
             <? else: ?>
-              <a href="<?=$this->url('myresearch-savesearch')?>?save=<?=urlencode($this->results->getSearchId())?>"><i class="fa fa-save"></i> <?=$this->transEsc('save_search')?></a>
+              <a href="<?=$this->url('myresearch-savesearch')?>?save=<?=urlencode($this->results->getSearchId())?>"><i class="fa fa-save" aria-hidden="true"></i> <?=$this->transEsc('save_search')?></a>
             <? endif; ?>
           <? endif; ?>
         <? endif; ?>
diff --git a/themes/bootstrap3/templates/search/searchbox.phtml b/themes/bootstrap3/templates/search/searchbox.phtml
index 06b5f799d4532f1151cdee335d711961aa5339d6..e7d0c4ea19db9434d8d1cc4d473d032ece871cdb 100644
--- a/themes/bootstrap3/templates/search/searchbox.phtml
+++ b/themes/bootstrap3/templates/search/searchbox.phtml
@@ -52,10 +52,13 @@
     <? elseif ($handlerCount == 1): ?>
       <input type="hidden" name="type" value="<?=$this->escapeHtmlAttr($handlers[0]['value'])?>" />
     <? endif; ?>
-    <button type="submit" class="btn btn-primary"><i class="fa fa-search"></i> <?=$this->transEsc("Find")?></button>
+    <button type="submit" class="btn btn-primary"><i class="fa fa-search" aria-hidden="true"></i> <?=$this->transEsc("Find")?></button>
     <? if ($advSearch): ?>
       <a href="<?=$this->url($advSearch) . ((isset($this->searchId) && $this->searchId) ? '?edit=' . $this->escapeHtmlAttr($this->searchId) : $hiddenFilterParams) ?>" class="btn btn-link" rel="nofollow"><?=$this->transEsc("Advanced")?></a>
     <? endif; ?>
+    <? if ($geoUrl = $this->geocoords()->getSearchUrl($options)) : ?>
+        <a href="<?=$geoUrl ?>" class="btn btn-link"><?=$this->transEsc('Geographic Search')?></a>
+    <? endif; ?>
 
     <? $shards = $options->getShards(); if ($options->showShardCheckboxes() && !empty($shards)): ?>
       <?
diff --git a/themes/bootstrap3/templates/upgrade/fixduplicatetags.phtml b/themes/bootstrap3/templates/upgrade/fixduplicatetags.phtml
index 3b6188eddac43da29941cb9e5e0e2cc4d1f10211..c403778a3a4e1905ecfa24eba997e0ff1814f666 100644
--- a/themes/bootstrap3/templates/upgrade/fixduplicatetags.phtml
+++ b/themes/bootstrap3/templates/upgrade/fixduplicatetags.phtml
@@ -8,12 +8,14 @@
 <h2><?=$this->transEsc('Upgrade VuFind')?></h2>
 <?=$this->flashmessages()?>
 
-<p>Due to a bug in earlier versions of VuFind, you have some duplicate tags
-in your database.  It is recommended that you fix these.  Click Submit to proceed.</p>
+<p>Some duplicate tags have been detected in your database.  This may be due to a bug in an earlier
+version of VuFind, or it may be related to a change to your case_sensitive_tags setting in the [Social]
+section of VuFind.  If you want case-sensitive tags, make sure that the setting is on and try again;
+otherwise, it is recommended that you fix these.  Click Submit to proceed.</p>
 
 <p>If you do not wish to fix the problem at this time, click the Skip button.</p>
 
-<p>See <a target="_jira" href="https://vufind.org/jira/browse/VUFIND-805">https://vufind.org/jira/browse/VUFIND-805</a> for more details.</p>
+<p>See <a target="_jira" href="https://vufind.org/jira/browse/VUFIND-805">https://vufind.org/jira/browse/VUFIND-805</a> and <a target="_jira" href="https://vufind.org/jira/browse/VUFIND-1187">https://vufind.org/jira/browse/VUFIND-1187</a> for more details.</p>
 
 <br />
 
diff --git a/themes/bootstrap3/templates/vudl/about.phtml b/themes/bootstrap3/templates/vudl/about.phtml
deleted file mode 100644
index f453520acbd2d4cc333c784edecece951e5f0229..0000000000000000000000000000000000000000
--- a/themes/bootstrap3/templates/vudl/about.phtml
+++ /dev/null
@@ -1,24 +0,0 @@
-<?php
-  $this->headTitle($this->translate($this->title) .' - '. $this->translate('About'));
-  $this->layout()->breadcrumbs = false;
-?>
-
-<div class="row">
-  <div class="<?=$this->layoutClass('mainbody')?>">
-    <div class="alert alert-info">
-      <h2><?=$this->transEsc('Powered By') ?> <a href="http://vudl.org">VuDL</a></h2>
-    </div>
-      <li><strong>Total Collections:</strong> <?=$this->totals['folders'] ?></li>
-      <li><strong>Total Resources:</strong> <?=$this->totals['resources'] ?></li>
-      <!-- These counts aren't working yet --
-      <li><strong>Total Images:</strong> <?=$this->totals['images'] ?></li>
-      <li><strong>Total PDFs:</strong> <?=$this->totals['pdfs'] ?></li> -->
-  </div>
-
-  <div class="<?=$this->layoutClass('sidebar')?>">
-    <ul class="list-group">
-      <li class="list-group-item title"><?=$this->transEsc('Content') ?></li>
-      <li class="list-group-item"><a href="http://vudl.org">VuDL</a></li>
-    </ul>
-  </div>
-</div>
\ No newline at end of file
diff --git a/themes/bootstrap3/templates/vudl/denied.phtml b/themes/bootstrap3/templates/vudl/denied.phtml
deleted file mode 100644
index eb285ce7440630c018a06c4077b133ec9d920b25..0000000000000000000000000000000000000000
--- a/themes/bootstrap3/templates/vudl/denied.phtml
+++ /dev/null
@@ -1,7 +0,0 @@
-<?php
-  $this->headTitle($this->translate($this->title) .' - '. $this->translate('vudl_access_denied'));
-  $this->layout()->breadcrumbs = false;
-?>
-<div class="alert alert-danger">
-  <?=$this->transEsc('vudl_access_denied')?>
-</div>
diff --git a/themes/bootstrap3/templates/vudl/details.phtml b/themes/bootstrap3/templates/vudl/details.phtml
deleted file mode 100644
index 96449508f0a7cea2462d07ccc0449252326ba522..0000000000000000000000000000000000000000
--- a/themes/bootstrap3/templates/vudl/details.phtml
+++ /dev/null
@@ -1,81 +0,0 @@
-<?php
-  $skip = array(
-    'license', 'special_license', 'description', 'title'
-  );
-  $no_link = array('title', 'description');
-?>
-<div class="panel">
-  <div class="panel-heading">
-    <h4 class="panel-title">
-      <a data-toggle="collapse" data-parent="#side-nav" href="#collapse_details">
-        <?=$this->transEsc('Summary & Rights') ?>
-      </a>
-    </h4>
-  </div>
-  <div id="collapse_details" class="panel-collapse collapse">
-    <div class="panel-body details">
-      <table class="table">
-        <tr><td><?=$this->transEsc('Full Title') ?></td><td><?=$this->details['title']['value'] ?></td></tr>
-        <? foreach ($this->details as $attr=>$val):
-          // Skip items not placed in this table
-          if(in_array($attr, $skip)) continue; ?>
-          <tr><td><?=$this->transEsc($val['title']) ?></td><td>
-            <? // Special format for first_indexed ?>
-            <? if($attr == 'first_indexed'): ?>
-              <?=date_create($val['value'])->format('j F Y') ?>
-
-            <? // If we need exploding and backlinking ?>
-            <? elseif($attr == 'topic' || $attr == 'series'): ?>
-              <? if(!is_array($val['value'])) $val['value'] = array($val['value']); ?>
-              <? if(count($val['value']) > 1): ?><ul class="list-unstyled"><? endif ?>
-              <? foreach($val['value'] as $v): ?>
-                <? if(count($val['value']) > 1): ?><li><? endif ?>
-               <? $parts = explode(' -- ', $v);
-                  $filter = '';
-                  foreach($parts as $i=>$p):
-                    $filter .= ' '.$p; ?>
-                    <?=$i>0 ? ' &gt; ' : '' ?><a class="backlink" href="<?=$this->url('search-results') ?>?filter[]=<?=$attr ?>:<?=urlencode(trim($filter)) ?>"><?=$p ?></a>
-               <? endforeach; ?>
-                <? if(count($val['value']) > 1): ?></li><? endif ?>
-              <? endforeach; ?>
-              <? if(count($val['value']) > 1): ?></ul><? endif ?>
-
-            <? // Items not useful to link ?>
-            <? elseif(in_array($attr, $no_link)): ?>
-              <?=is_array($val['value']) ? implode('<br/>', $val['value']) : $val['value'] ?>
-
-            <? // Array handling ?>
-            <? elseif(is_array($val['value'])): ?>
-              <? if(count($val['value']) == 1): ?>
-                <a href="<?=$this->url('search-results') ?>?filter[]=<?=$attr ?>:<?=urlencode($val['value'][0]) ?>"><?=$val['value'][0] ?></a>
-              <? else: ?>
-                <ul class="list-unstyled">
-                <? foreach($val['value'] as $v): ?>
-                   <li><a href="<?=$this->url('search-results') ?>?filter[]=<?=$attr ?>:<?=urlencode($v) ?>"><?=$v ?></a></li>
-                <? endforeach; ?>
-                </ul>
-              <? endif; ?>
-
-            <? // The rest are linked ?>
-            <? else: ?>
-              <a href="<?=$this->url('search-results') ?>?filter[]=<?=$attr ?>:<?=urlencode($val['value']) ?>"><?=$val['value'] ?></a>
-            <? endif; ?>
-          </td></tr>
-        <? endforeach; ?>
-        <? if(isset($this->details['license'])): ?>
-          <? if(!$this->details['special_license']): ?>
-            <tr><td>License</td><td><a href="<?=$this->details['license'] ?>"><?=$this->details['license'] ?></a></td></tr>
-          <? endif; ?>
-        <? endif; ?>
-      </table>
-      <? if(isset($this->details['license'])): ?>
-        <div class="copyright">
-          <?=$this->render('/vudl/licenses/'.$this->details['special_license'], array('details'=>$this->details)); ?>
-        </div>
-      <? endif; ?>
-      <a href="<?=$this->url('record', array('id'=>$this->id))?>"><i class="fa fa-list"></i> <?=$this->transEsc('More Details') ?></a><br/>
-      <a href="<?=$this->url('vudl-record', array('id'=>$this->id))?>"><i class="fa fa-book"></i> <?=$this->transEsc('Permanent Link') ?></a>
-      <? if(isset($this->details['description'])): ?><p class="description"><?=html_entity_decode($this->details['description']['value'], 2 /*ENT_COMPAT|ENT_HTML401*/, 'UTF-8') ?></p><? endif ?>
-    </div>
-  </div>
-</div>
\ No newline at end of file
diff --git a/themes/bootstrap3/templates/vudl/grid.phtml b/themes/bootstrap3/templates/vudl/grid.phtml
deleted file mode 100644
index 46aad164f81e6552a416f177c2df4502b61059ba..0000000000000000000000000000000000000000
--- a/themes/bootstrap3/templates/vudl/grid.phtml
+++ /dev/null
@@ -1,80 +0,0 @@
-<?
-  $this->headTitle($this->translate($this->title) .' - '. $this->details['title']['value']);
-
-  // Multiple breadcrumbs
-  $this->layout()->breadcrumbs = $this->parents;
-  $this->layout()->title = $this->details['title']['value'];
-  $this->layout()->from = $this->from;
-
-  // Facebook image meta
-  $this->layout()->facebookImage = $this->outline['lists'][0][$this->initPage]['medium'];
-  $this->layout()->facebookTitle = $this->details['title']['value'];
-
-  // HEADER FILES
-  $this->headLink()->appendStylesheet('vudl.css');
-  $this->headScript()->appendFile('vudl/grid.js');
-
-  // Compact header
-  $this->layout()->headerType = 'grid';
-  $this->layout()->vudlID = $this->id;
-  $this->layout()->hierarchyID = $this->hierarchyID;
-  $this->layout()->siblings = $this->siblings;
-  $this->layout()->parents = $this->parents;
-
-  function json_php_encode($op, $quotes = false) {
-    if($quotes) {
-      return str_replace('"', "'", str_replace('\/', '/', json_encode($op)));
-    } else {
-      return str_replace('\/', '/', json_encode($op));
-    }
-  }
-?>
-<script>
-  var documentID = '<?=$this->id ?>';
-  var initPage = <?=isset($this->initPage) ? $this->initPage : 0 ?>;
-</script>
-<div class="vudl row">
-  <div class="col-sm-3">
-    <?=$this->context($this)->renderInContext('vudl/details.phtml', array())?>
-  </div>
-  <div class="col-sm-9">
-    <div class="row">
-      <? $index=0; foreach($this->outline['lists'] as $key=>$list): ?>
-        <!-- PRE LOADING PLACEHOLDERS -->
-        <? for($i=0;$i<current(array_keys($list))-1;$i++, $index++): ?>
-          <a class="col-sm-3 page-grid" id="item<?=$i ?>" title="<?=$i ?>">Loading...</a>
-        <? endfor; ?>
-        <!-- LOADED ITEMS -->
-        <? foreach($list as $i=>$item): ?>
-          <a class="col-sm-3 page-grid" href="<?=$this->url('vudl-record', array('id'=>$item['id'])) ?>" title="<?=$item['id'] ?>" id="item<?=$index?>">
-            <? if(isset($item['thumbnail'])): ?>
-              <img src="<?=$item['thumbnail'] ?>" alt="<?=$item['label'] ?>"/>
-            <? else: ?>
-              <img class="<?=$item['fulltype'] ?>" src="<?=$this->imageLink('vudl/'.$item['fulltype'].'.png') ?>"/>
-            <? endif; ?>
-            <br/><?=$this->transEsc($item['label']) ?>
-          </a>
-          <? $index++; ?>
-        <? endforeach; ?>
-        <!-- POST LOADING PLACEHOLDERS -->
-        <? if(isset($this->outline['counts'][$key])): ?>
-          <? for($i=$this->initPage+count($list);$i<$this->outline['counts'][$key];$i++, $index++): ?>
-            <a class="col-sm-3 page-grid" id="item<?=$i ?>" title="<?=$i ?>">Loading...</a>
-          <? endfor; ?>
-        <? endif; ?>
-      <? endforeach; ?>
-    </div>
-  </div>
-</div>
-<script>
-  $.ajax({dataType:'json',
-    url:'../VuDL/ajax?method=pageAjax&record=<?=$this->id ?>&start=0&end=16',
-    success:ajaxLoadPages,
-    error:function(d,e){
-      console.log(d);console.log(e)
-    }
-  });
-  counts = $.parseJSON('<?=json_encode($this->outline['counts'], JSON_HEX_APOS | JSON_HEX_AMP) ?>');
-  $('.accordion').removeClass('hidden');
-  $('#collapse_details').addClass('in');
-</script>
diff --git a/themes/bootstrap3/templates/vudl/home.phtml b/themes/bootstrap3/templates/vudl/home.phtml
deleted file mode 100644
index 648661b2c6b02f8134aa913f6259658b6d4f9a66..0000000000000000000000000000000000000000
--- a/themes/bootstrap3/templates/vudl/home.phtml
+++ /dev/null
@@ -1,39 +0,0 @@
-<?
-  // Set page title.
-  $this->headTitle($this->translate($this->translate($this->title)));
-
-  // Set default value if necessary:
-  if (!isset($this->searchClassId)) {
-    $this->searchClassId = 'Solr';
-  }
-
-  // Load search actions and settings (if any):
-  $options = $this->searchOptions($this->searchClassId);
-  $basicSearch = $options->getSearchAction();
-  $advSearch = $options->getAdvancedSearchAction();
-  $this->layout()->selectedTab = 'digital';
-  $this->layout()->showBreadcrumbs = false;
-?>
-<div class="hero-unit">
-  <div id="grid-container">
-    <div id="grid">
-      <?  foreach($this->thumbnails as $thumbnail): ?>
-        <a href="<?=$this->url('collection', array('id'=>$thumbnail['id'])) ?>" title="<?=$thumbnail['label'] ?>" class="item"><img style="width:<?=(40*rand(2,4))-4 ?>px;margin:2px 0" src="<?=$thumbnail['img'] ?>"></a>
-      <?  endforeach; ?>
-    </div>
-  </div>
-  <a class="btn btn-primary btn-lg" alt="<?=$this->transEsc('Browse the Collection')?>" href="<?=$this->url('vudl-default-collection') ?>"><?=$this->transEsc('Browse the Collection')?> <i class="fa fa-double-angle-right"></i></a>
-  <script src="<?=$this->imageLink('../js/vendor/masonry.min.js') ?>"></script>
-  <script>
-    window.onload = function() {
-      var container = document.getElementById('grid');
-      var limit = 10;
-      do {
-        new Masonry( container , {
-          columnWidth: 40,
-          isFitWidth: true
-        });
-      } while(limit-- && container.offsetHeight > 370);
-    };
-  </script>
-</div>
diff --git a/themes/bootstrap3/templates/vudl/licenses/CC.phtml b/themes/bootstrap3/templates/vudl/licenses/CC.phtml
deleted file mode 100644
index 4fd9532c30baa805f15b2288480a5a225b100728..0000000000000000000000000000000000000000
--- a/themes/bootstrap3/templates/vudl/licenses/CC.phtml
+++ /dev/null
@@ -1,6 +0,0 @@
-<?
-  $cctype = array();
-  preg_match('/licenses\/([^\/]*)/', $this->details['license'], $cctype);
-  $cctype = $cctype[1];
-?>
-This work is licensed under a <a target="_blank" rel="license" href="<?=$this->details['license'] ?>deed.en_US">Creative Commons <?=strpos($cctype, 'by') == 0 ? 'Attribution-' : '' ?><?=strpos($cctype, 'nc') ? 'NonCommercial-' : '' ?><?=strpos($cctype, 'nd') ? 'NoDerivs' : '' ?><?=strpos($cctype, 'sa') ? 'ShareAlike' : '' ?> 3.0 Unported License</a>.<br/><br/><a target="_blank" rel="license" href="<?=$this->details['license'] ?>deed.en_US"><img alt="Creative Commons License" style="border-width:0" src="http://i.creativecommons.org/l/<?=$cctype ?>/3.0/80x15.png" /></a>
\ No newline at end of file
diff --git a/themes/bootstrap3/templates/vudl/licenses/VU.phtml b/themes/bootstrap3/templates/vudl/licenses/VU.phtml
deleted file mode 100644
index ef41f3044ff2536beb7fba4ef504de770a0e0373..0000000000000000000000000000000000000000
--- a/themes/bootstrap3/templates/vudl/licenses/VU.phtml
+++ /dev/null
@@ -1 +0,0 @@
-This work is under <a target="_blank" href="http://digital.library.villanova.edu/copyright.html">Villanova University Copyright License</a> of the original publisher.
\ No newline at end of file
diff --git a/themes/bootstrap3/templates/vudl/master-tab.phtml b/themes/bootstrap3/templates/vudl/master-tab.phtml
deleted file mode 100644
index 36638bb4a75b8ef9a8e56338008d8ce2ff060c9d..0000000000000000000000000000000000000000
--- a/themes/bootstrap3/templates/vudl/master-tab.phtml
+++ /dev/null
@@ -1,14 +0,0 @@
-<? if(isset($this->master)): ?>
-  <form method="get" id="file-download" action="<?=$this->url('files', array('id'=>$this->id, 'type'=>'MASTER')) ?>?download=true">
-    <button id="download-button" class="btn btn-primary btn-lg">
-      <i class="fa fa-download"></i> <?=$this->transEsc('Download File') ?><br>
-      <span class="details"><?=$this->techinfo['type'] ?><?if(isset($this->techinfo['size'])):?> ~ <?=$this->techinfo['size'] ?><? endif ?></span>
-    </button>
-  </form>
-<? else: ?>
-  <br/><br/>
-  <p>Original Image File Not Available.</p>
-  <p>See below for all available downloads.</p>
-<? endif; ?>
-<br/>
-<div class="accordion" id="techinfo"><?=$this->techinfo['div'] ?></div>
\ No newline at end of file
diff --git a/themes/bootstrap3/templates/vudl/navigation.phtml b/themes/bootstrap3/templates/vudl/navigation.phtml
deleted file mode 100644
index 06a7d7f677918e92aefc0188e69d9eccf1d407c2..0000000000000000000000000000000000000000
--- a/themes/bootstrap3/templates/vudl/navigation.phtml
+++ /dev/null
@@ -1,81 +0,0 @@
-<?
-  // Parent collections for Prev/Next Item
-  $parents = array();
-  $parentKeys = array();
-  foreach($this->parents as $trail) {
-    if(is_array($trail)) {
-      end($trail);
-    }
-    if(!in_array(key($trail), $parentKeys)) {
-      $uniqueParents[] = array(
-        'id'    => key($trail),
-        'title' => current($trail)
-      );
-      $parentKeys[] = key($trail);
-    }
-  }
-  $siblingHref = $this->url('vudl-sibling') . '?id=' . $this->layout()->vudlID;
-  if(count($uniqueParents) <= 1) {
-    $siblingHref .= '&trail=' . $uniqueParents[0]['id'];
-  }
-?>
-<nav class="sibling-form navbar" role="navigation">
-  <ul class="nav navbar-nav">
-    <? if(count($uniqueParents) > 1): ?>
-      <li class="dropdown">
-        <a id="sibling-prev" class="btn btn-default" data-toggle="dropdown" title="<?=$this->transEsc('Prev Item in Collection')?>">
-          <?=$this->transEsc('Prev Item')?> <span class="caret"></span>
-        </a>
-        <ul class="dropdown-menu" role="menu" aria-labelledby="sibling-prev">
-          <li role="presentation" class="dropdown-header"><?=$this->transEsc('Choose a collection') ?></li>
-          <? foreach($uniqueParents as $trail): ?>
-            <li role="presentation">
-              <a role="menuitem" tabindex="-1" href="<?=$siblingHref ?>&trail=<?=$trail['id'] ?>&prev=1">
-                <?=$trail['title'] ?>
-              </a>
-            </li>
-          <? endforeach; ?>
-        </ul>
-      </li>
-    <? else: ?>
-      <li>
-        <a href="<?=$siblingHref ?>&prev=1" class="btn btn-default" title="<?=$this->transEsc('Prev Item in Collection')?>">
-          <i class="fa fa-angle-left"></i>
-          <?=$this->transEsc('Prev Item')?>
-        </a>
-      </li>
-    <? endif; ?>
-    <li class="btn-group">
-      <a href="javascript:prevPage()" class="turn-button btn btn-default<? if($this->outline['counts'][$this->initList] <= 1): ?> hidden<? endif; ?>">Prev Page</a>
-      <a href="<?=$this->url('vudl-grid', array('id'=>$this->id)) ?>" class="grid-btn btn btn-default">
-        <i class="fa fa-th hidden-xs"></i>
-        <span class="visible-xs-inline"><?=$this->transEsc('grid_view') ?></span>
-      </a>
-      <a href="javascript:nextPage()" class="turn-button btn btn-default<? if($this->outline['counts'][$this->initList] <= 1): ?> hidden<? endif; ?>">Next Page</a>
-    </li>
-    <? if(count($uniqueParents) > 1): ?>
-      <li class="dropdown">
-        <a id="sibling-next" class="btn btn-default" data-toggle="dropdown" title="<?=$this->transEsc('Next Item in Collection')?>">
-          <?=$this->transEsc('Next Item')?> <span class="caret"></span>
-        </a>
-        <ul class="dropdown-menu" role="menu" aria-labelledby="sibling-next">
-          <li role="presentation" class="dropdown-header"><?=$this->transEsc('Choose a collection') ?></li>
-          <? foreach($uniqueParents as $trail): ?>
-            <li role="presentation">
-              <a role="menuitem" tabindex="-1" href="<?=$siblingHref ?>&trail=<?=$trail['id'] ?>&next=1">
-                <?=$trail['title'] ?>
-              </a>
-            </li>
-          <? endforeach; ?>
-        </ul>
-      </li>
-    <? else: ?>
-      <li>
-        <a href="<?=$siblingHref ?>&next=1" class="btn btn-default" title="<?=$this->transEsc('Next Item in Collection')?>">
-          <?=$this->transEsc('Next Item')?>
-          <i class="fa fa-angle-right"></i>
-        </a>
-      </li>
-    <? endif; ?>
-  </ul>
-</nav>
\ No newline at end of file
diff --git a/themes/bootstrap3/templates/vudl/record.phtml b/themes/bootstrap3/templates/vudl/record.phtml
deleted file mode 100644
index c038ccc2bd287445c6c2bd187b33d0ac27e416bc..0000000000000000000000000000000000000000
--- a/themes/bootstrap3/templates/vudl/record.phtml
+++ /dev/null
@@ -1,100 +0,0 @@
-<?
-  $this->headTitle($this->translate($this->title) .' - '. $this->details['title']['value']);
-
-  // Multiple breadcrumbs
-  $this->layout()->breadcrumbs = $this->parents;
-  $this->layout()->title = $this->details['title']['value'];
-  $this->layout()->from = $this->from;
-
-  // Get first list name
-  foreach($this->outline as $list=>$v) {
-    if($list == 'counts') continue;
-    $listName = $list;
-    break;
-  }
-
-  // HEADER FILES
-  $this->headLink()->appendStylesheet('vudl.css');
-  $this->headScript()->appendFile('vudl/canvas-zoomy.js');
-  $this->headScript()->appendFile('vudl/config.js');
-  $this->headScript()->appendFile('vudl/record.js');
-
-  // Compact header
-  $this->layout()->headerType = 'record';
-  $this->layout()->vudlID = $this->id;
-  $this->layout()->hierarchyID = $this->hierarchyID;
-  $this->layout()->parents = $this->parents;
-  $this->layout()->siblings = $this->siblings;
-
-  function json_php_encode($op, $quotes = false) {
-    if($quotes) {
-      return str_replace('"', "'", str_replace("'", "\\'", str_replace('\/', '/', json_encode($op))));
-    } else {
-      return str_replace('\/', '/', json_encode($op));
-    }
-  }
-?>
-<script>
-  var documentID = '<?=$this->id ?>';
-  var initPage = $.parseJSON('<?=str_replace('\"', "\'", json_encode($this->outline['lists'][$this->initList][$this->initPage], JSON_HEX_APOS | JSON_HEX_AMP)) ?>');
-  var currentList = <?=$this->initList ?>;
-
-  counts = $.parseJSON('<?=json_encode($this->outline['counts'], JSON_HEX_APOS | JSON_HEX_AMP) ?>');
-  <? if(count($this->outline['lists'][$this->initList]) >= $this->outline['counts'][$this->initList]): ?>
-    loading_pages = false;
-  <? endif; ?>
-</script>
-<?=$this->context($this)->renderInContext('vudl/navigation.phtml', array()); ?>
-<div class="vudl row">
-  <div class="panel-group col-sm-3" id="side-nav">
-    <div class="panel">
-      <div class="panel-heading">
-        <h4 class="panel-title">
-          <a data-toggle="collapse" data-parent="#side-nav" id="side-nav-toggle">
-            <i class="fa fa-caret-left"></i>
-            <i class="fa fa-caret-left"></i>
-            <i class="fa fa-caret-left"></i>
-          </a>
-        </h4>
-      </div>
-    </div>
-    <?=$this->context($this)->renderInContext('vudl/details.phtml', array())?>
-    <? foreach($this->outline['lists'] as $key=>$list): ?>
-      <div class="panel">
-        <div class="panel-heading">
-          <h4 class="panel-title">
-            <a data-toggle="collapse" data-parent="#side-nav" href="#collapse<?=$key ?>">
-            <?=$this->outline['names'][$key] ?>
-            </a>
-          </h4>
-        </div>
-        <div id="collapse<?=$key ?>" class="panel-collapse collapse<? if($key==$this->initList): ?> in<? endif; ?>">
-          <div class="panel-body item-list" id="list<?=$key ?>" list-index="<?=$key ?>">
-            <!-- PRE LOADING PLACEHOLDERS -->
-            <? for($i=0;$i<current(array_keys($list))-1;$i++): ?>
-              <a class="page-link unloaded" id="item<?=$i ?>" title="<?=$i ?>">Loading...</a>
-            <? endfor; ?>
-            <!-- LOADED ITEMS -->
-            <? foreach($list as $j=>$item): ?>
-              <a title="<?=$item['id'] ?>" onClick="ajaxGetView(<?=json_php_encode($item, true) ?>, this)" class="page-link active<?=$key == $this->initList && $j == $this->initPage ?' selected':''?>" id="item<?=$j?>">
-              <? if(isset($item['thumbnail'])): ?>
-                <img src="<?=$item['thumbnail'] ?>" alt="<?=$item['label'] ?>"/><br/>
-              <? else: ?>
-                <i class="fa fa-file file-<?=$item['fulltype'] ?>"></i><br/>
-              <? endif; ?>
-                <?=$item['label'] ?>
-              </a>
-            <? endforeach; ?>
-            <!-- POST LOADING PLACEHOLDERS -->
-            <? if(isset($this->outline['counts'][$key])): ?>
-              <? for($i=$this->initPage+count($list);$i<$this->outline['counts'][$key];$i++): ?>
-                <a class="page-link unloaded" id="item<?=($i) ?>" title="<?=$i ?>">Loading...</a>
-              <? endfor; ?>
-            <? endif; ?>
-          </div>
-        </div>
-      </div>
-    <? endforeach; ?>
-  </div>
-  <div id="view" class="col-sm-9"></div>
-</div>
\ No newline at end of file
diff --git a/themes/bootstrap3/templates/vudl/techinfo.phtml b/themes/bootstrap3/templates/vudl/techinfo.phtml
deleted file mode 100644
index 1a2de431b3661c2f09add616072c9fb8b0f90954..0000000000000000000000000000000000000000
--- a/themes/bootstrap3/templates/vudl/techinfo.phtml
+++ /dev/null
@@ -1,74 +0,0 @@
-<!-- ALL FILES -->
-<div class="panel-group">
-  <div class="panel">
-    <div class="panel-heading">
-      <h4 class="panel-title">
-        <a data-toggle="collapse" data-parent="#techinfo" href="#allFiles">
-          <?=$this->transEsc('All Files')?>
-        </a>
-      </h4>
-    </div>
-    <div id="allFiles" class="panel-collapse collapse">
-      <div class="panel-body">
-        <? foreach ($this->record as $key=>$link): ?>
-          <? $mtKey = array_search(strToUpper($key), $this->record['datastreams']); ?>
-          <? if (
-            is_array($this->record[$key])
-            || strpos($this->record['mimetypes'][$mtKey], 'text') !== false
-            || strpos($this->record['mimetypes'][$mtKey], 'xml') !== false
-            || strpos($key, '-') !== false
-            || $key == 'techinfo'
-          ) continue; ?>
-          <a class="btn btn-default clearfix" href="<?=$this->url(
-            'files',
-            array(
-              'id'=>$this->record['id'],
-              'type'=>strtoupper($key)
-            )
-          )?>?download=true">
-            <span class="pull-left flip"><?=strToUpper($this->transEsc($key)) ?></span>
-            <? if (isset($this->record['mimetypes'])): ?>
-              <span class="pull-right flip small"><?=$this->record['mimetypes'][$mtKey] ?></span>
-            <? endif; ?>
-          </a>
-        <? endforeach; ?>
-      </div>
-    </div>
-  </div>
-
-  <!-- OCR --->
-  <? if (isset($this->record['ocr-dirty'])): ?>
-    <div class="panel">
-      <div class="panel-heading">
-        <h4 class="panel-title">
-          <a data-toggle="collapse" data-parent="#techinfo" href="#ocr">
-            <?=$this->transEsc('Computer Generated Transcription (OCR)')?>
-          </a>
-        </h4>
-      </div>
-      <div id="ocr" class="panel-collapse collapse">
-        <div class="panel-body">
-          <pre><?=$this->record['ocr-dirty'] ?></pre>
-        </div>
-      </div>
-    </div>
-  <? endif; ?>
-
-  <!-- Technical Information XML --->
-  <? if (isset($this->record['techinfo'])): ?>
-    <div class="panel">
-      <div class="panel-heading">
-        <h4 class="panel-title">
-          <a class="accordion-toggle" data-toggle="collapse" data-parent="#techinfo" href="#xml">
-            <?=$this->transEsc('Technical Information (Master File)')?>
-          </a>
-        </h4>
-      </div>
-      <div id="xml" class="panel-collapse collapse">
-        <div class="panel-body">
-          <?=$this->vudl()->formatTechInfo($this->record['techinfo'])?>
-        </div>
-      </div>
-    </div>
-  <? endif; ?>
-</div>
\ No newline at end of file
diff --git a/themes/bootstrap3/templates/vudl/views/audio.phtml b/themes/bootstrap3/templates/vudl/views/audio.phtml
deleted file mode 100644
index 7749d354cdde00098e5a78135769f085fdbc268f..0000000000000000000000000000000000000000
--- a/themes/bootstrap3/templates/vudl/views/audio.phtml
+++ /dev/null
@@ -1,39 +0,0 @@
-<script>
-  var mobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(navigator.userAgent);
-  var audioSupported = document.createElement('audio').canPlayType('audio/mpeg')
-    || document.createElement('audio').canPlayType('audio/ogg')
-    || document.createElement('audio').canPlayType(data['mimetype']);
-  updateFunction = mobile ? false : function(data, tab) {
-    if (audioSupported) {
-      var html = '<source src="'+data['ogg']+'?download=true"/>'
-      + '<source src="'+data['mp3']+'?download=true"/>'
-      + '<source src="'+data[data['master']]+'?download=true"/>';
-      var audio = $('audio');
-      audio.trigger('pause').html(html).removeClass('hidden');
-      audio[0].load();
-    }
-  };
-  $(document).ready(function() {
-    currentTab = "master-tab";
-    $('#view .nav-tabs li.opener a').addClass('hidden');
-  });
-</script>
-<ul class="nav nav-tabs">
-  <li class="static opener">
-    <a onClick="toggleSideNav()">
-      <i class="fa fa-caret-right"></i>
-      <i class="fa fa-caret-right"></i>
-      <i class="fa fa-caret-right"></i>
-    </a>
-  </li>
-  <li class="active"><a>Player and Downloads</a></li>
-</ul>
-<div class="tab-container text-center tab-content">
-  <audio controls preload="auto">
-    <? if(isset($this->ogg)): ?><source src="<?=$this->ogg ?>?download=true"/><?endif?>
-    <? if(isset($this->mp3)): ?><source src="<?=$this->mp3 ?>?download=true"/><?endif?>
-    <? if(!isset($this->mp3) && !isset($this->ogg)): ?><source src="<?=$this->master ?>?download=true"/><?endif?>
-  </audio>
-  <br/><br/>
-  <?=$this->context($this)->renderInContext('vudl/master-tab.phtml', array())?>
-</div>
diff --git a/themes/bootstrap3/templates/vudl/views/download.phtml b/themes/bootstrap3/templates/vudl/views/download.phtml
deleted file mode 100644
index 18ca9ca6f1afa4c02d53f0be1a78a9950bbe47e1..0000000000000000000000000000000000000000
--- a/themes/bootstrap3/templates/vudl/views/download.phtml
+++ /dev/null
@@ -1,23 +0,0 @@
-<script>
-  // Mandatory update function
-  updateFunction = function(data, tab) {
-    $('#file-download').attr("action", data['master']);
-  };
-  $(document).ready(function() {
-    currentTab = "master-tab";
-    $('#view .nav-tabs li.opener a').addClass('hidden');
-  });
-</script>
-<ul class="nav nav-tabs">
-  <li class="static opener">
-    <a onClick="toggleSideNav()">
-      <i class="fa fa-caret-right"></i>
-      <i class="fa fa-caret-right"></i>
-      <i class="fa fa-caret-right"></i>
-    </a>
-  </li>
-  <li class="active"><a href="#master" id="master-tab">Downloads</a></li>
-</ul>
-<div class="tab-container text-center tab-content">
-  <?=$this->context($this)->renderInContext('vudl/master-tab.phtml', array())?>
-</div>
\ No newline at end of file
diff --git a/themes/bootstrap3/templates/vudl/views/page.phtml b/themes/bootstrap3/templates/vudl/views/page.phtml
deleted file mode 100644
index c10122ef94b694154e1d499c3606e81841189bc8..0000000000000000000000000000000000000000
--- a/themes/bootstrap3/templates/vudl/views/page.phtml
+++ /dev/null
@@ -1,112 +0,0 @@
-<script>
-  // Mandatory update function
-  updateFunction = function(data, tab) {
-    $('.loading-bar').removeClass('hidden');
-    if(!data['zoom'] && data['large']) data['zoom'] = data['large'];
-    pageData = data;
-    //console.log(data);
-    $('#view .nav-tabs li:not(.static) a').addClass('hidden');
-    for(var i in data) {
-      $('#'+i+'-tab').removeClass('hidden');
-    }
-    $('.download').attr('href', data['master']);
-    Zoomy.load(data['large'], function() {$('.loading-bar').addClass('hidden')});
-    $('#'+tab).click();
-  };
-  // Load an image into the preview (to be deprecated by the new template system)
-  var currentPreview = "";
-  function showPreview(index, tab) {
-    if(currentPreview !== pageData[index]) {
-      currentPreview = pageData[index];
-      $('.loading-bar').removeClass('hidden');
-      $('#preview')
-        .load(function() {
-          $('#preview').css('opacity','1');
-          $('.loading-bar').addClass('hidden');
-        })
-        .css('opacity','.3')
-        .attr({
-          'src':pageData[index]
-        })
-    }
-  }
-
-  function resizeZoom() {
-    var $height = $(window).height() + window.scrollY - $('#zoomy').offset().top - 10;
-    if($('#side-nav').height() > 200) {
-      $height = Math.min($height, $('#side-nav').height() - 80);
-    } else {
-      $height = Math.min($height, $(window).height() - 270);
-    }
-    $('#zoomy').css({
-      'width' : '100%',
-      'height': Math.max(400, $height)
-    });
-  }
-
-  $(document).ready(function() {
-    $('#view .nav-tabs li.opener a').addClass('hidden');
-    $('#view .nav-tabs li:not(.static) a').click(function (e) {
-      e.preventDefault();
-      $(this).tab('show');
-      currentTab = $(this).attr('id');
-    });
-    // Initialize the size of Zoomy
-    $('#zoom-tab').on('shown.bs.tab', function() {
-      resizeZoom();
-      Zoomy.init(document.getElementById('zoomy'));
-      Zoomy.load(pageData['large'], function() { $('.loading-bar').addClass('hidden'); });
-    });
-    $(window).scroll(resizeZoom);
-    $(window).resize(resizeZoom);
-    resizeZoom();
-    updateFunction({
-      'id'       :'<?=$this->id ?>',
-      'medium'   :'<?=$this->medium ?>',
-      'large'    :'<?=$this->large ?>',
-      'zoom'     :'<?=$this->zoom ?: $this->large ?>',
-      'master'   :'<?=$this->master ?>',
-      'thumbnail':'<?=$this->thumbnail ?>',
-      'mimetype' :'<?=$this->mimetype ?>'
-    }, 'medium-tab');
-  });
-</script>
-<ul class="nav nav-tabs">
-  <li class="static opener">
-    <a onClick="toggleSideNav()">
-      <i class="fa fa-caret-right"></i>
-      <i class="fa fa-caret-right"></i>
-      <i class="fa fa-caret-right"></i>
-    </a>
-  </li>
-  <li><a href="#image" id="medium-tab" onClick="showPreview('medium', this)">Medium</a></li>
-  <li class="hidden-xs"><a href="#image" id="large-tab" onClick="showPreview('large', this)">Large</a></li>
-  <li><a href="#zoom" id="zoom-tab">Zoom</a></li>
-  <li><a href="#master" id="master-tab">Downloads</a></li>
-</ul>
-<div class="tab-content">
-  <div class="loading-bar front">
-    <div class="progress progress-striped active">
-      <div class="progress-bar"  role="progressbar" aria-valuenow="100" aria-valuemin="0" aria-valuemax="100" style="width:100%">
-        <span>Loading...</span>
-      </div>
-    </div>
-  </div>
-  <div class="tab-pane text-center" id="image">
-    <img id="preview" src="<?=isset($this->medium) ? $this->medium : '' ?>">
-  </div>
-  <div class="tab-pane text-center" id="zoom">
-    <a class="btn btn-default" onClick="Zoomy.turnLeft()"><i class="fa fa-rotate-left"></i></a>
-    <a class="btn btn-default" onClick="Zoomy.zoom({deltaY:1})"><i class="fa fa-search-minus"></i></a>
-    <a class="btn btn-default" onClick="Zoomy.zoom(0,1)">[1:1]</a>
-    <a class="btn btn-default" onClick="Zoomy.toggleMap();$(this).children('.fa').toggleClass('fa fa-toggle-down').toggleClass('fa fa-toggle-up');" title="<?=$this->transEsc('more_info_toggle') ?>">
-      <i class="fa fa-toggle-down"></i>
-    </a>
-    <a class="btn btn-default" onClick="Zoomy.zoom({deltaY:-1})"><i class="fa fa-search-plus"></i></a>
-    <a class="btn btn-default" onClick="Zoomy.turnRight()"><i class="fa fa-rotate-right"></i></a>
-    <canvas id="zoomy"></canvas>
-  </div>
-  <div class="tab-pane text-center" id="master">
-    <?=$this->context($this)->renderInContext('vudl/master-tab.phtml', array())?>
-  </div>
-</div>
\ No newline at end of file
diff --git a/themes/bootstrap3/templates/vudl/views/video.phtml b/themes/bootstrap3/templates/vudl/views/video.phtml
deleted file mode 100644
index 0d1de9196a5b1fe830a3ef64ba6015a655a94aab..0000000000000000000000000000000000000000
--- a/themes/bootstrap3/templates/vudl/views/video.phtml
+++ /dev/null
@@ -1,39 +0,0 @@
-<script>
-  var mobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(navigator.userAgent);
-  var videoSupported = document.createElement('video').canPlayType('video/mpeg')
-    || document.createElement('video').canPlayType('video/ogg')
-    || document.createElement('video').canPlayType(data['mimetype']);
-  updateFunction = mobile ? false : function(data, tab) {
-    if (videoSupported) {
-      var html = '<source src="'+data['ogg']+'?download=true"/>'
-      + '<source src="'+data['mp4']+'?download=true"/>'
-      + '<source src="'+data[data['master']]+'?download=true"/>';
-      var video = $('video');
-      video.trigger('pause').html(html).removeClass('hidden');
-      video[0].load();
-    }
-  };
-  $(document).ready(function() {
-    currentTab = "master-tab";
-    $('#view .nav-tabs li.opener a').addClass('hidden');
-  });
-</script>
-<ul class="nav nav-tabs">
-  <li class="static opener">
-    <a onClick="toggleSideNav()">
-      <i class="fa fa-caret-right"></i>
-      <i class="fa fa-caret-right"></i>
-      <i class="fa fa-caret-right"></i>
-    </a>
-  </li>
-  <li class="active"><a>Player and Downloads</a></li>
-</ul>
-<div class="tab-container text-center tab-content">
-  <video controls>
-    <? if(isset($this->ogg)): ?><source src="<?=$this->ogg ?>?download=true"/><?endif?>
-    <? if(isset($this->mp4)): ?><source src="<?=$this->mp4 ?>?download=true"/><?endif?>
-    <? if(!isset($this->ogg) && !isset($this->mp4)): ?><source src="<?=$this->master ?>?download=true"/><?endif?>
-  </video>
-  <br/><br/>
-  <?=$this->context($this)->renderInContext('vudl/master-tab.phtml', array())?>
-</div>
diff --git a/themes/bootstrap3/theme.config.php b/themes/bootstrap3/theme.config.php
index ba64ddfcc49e2af8bdf3a5325690e1dfb4af980e..8ee821b92dbc87e4ab490b053620725ed09bb6a1 100644
--- a/themes/bootstrap3/theme.config.php
+++ b/themes/bootstrap3/theme.config.php
@@ -4,10 +4,9 @@ return array(
     'css' => array(
         //'vendor/bootstrap.min.css',
         //'vendor/bootstrap-accessibility.css',
+        //'vendor/font-awesome.min.css',
         //'bootstrap-custom.css',
         'compiled.css',
-        'vendor/font-awesome.min.css',
-        'vendor/bootstrap-slider.min.css',
         'print.css:print',
     ),
     'js' => array(
@@ -15,10 +14,8 @@ return array(
         'vendor/jquery.min.js',
         'vendor/bootstrap.min.js',
         'vendor/bootstrap-accessibility.min.js',
-        //'vendor/bootlint.min.js',
-        'autocomplete.js',
         'vendor/validator.min.js',
-        'vendor/rc4.js',
+        'autocomplete.js',
         'common.js',
         'lightbox.js',
     ),
@@ -35,8 +32,7 @@ return array(
         ),
         'invokables' => array(
             'highlight' => 'VuFind\View\Helper\Bootstrap3\Highlight',
-            'search' => 'VuFind\View\Helper\Bootstrap3\Search',
-            'vudl' => 'VuDL\View\Helper\Bootstrap3\VuDL',
+            'search' => 'VuFind\View\Helper\Bootstrap3\Search'
         )
     )
 );
diff --git a/themes/jquerymobile/css/styles.css b/themes/jquerymobile/css/styles.css
index abd11c23fb0281fd348c9090e913e0e9ced8806d..1ceebec3224856b0520ca35547d8e79f8b877936 100644
--- a/themes/jquerymobile/css/styles.css
+++ b/themes/jquerymobile/css/styles.css
@@ -303,4 +303,11 @@ span[class^="services-"], span[class*=" services-"] span:first-of-type::before {
 }
 span[class^="services-"], span[class*=" services-"] span::before {
     content: ", ";
-}
\ No newline at end of file
+}
+
+.marc-row-LEADER,
+.marc-row-006,
+.marc-row-007,
+.marc-row-008 {
+  white-space: pre-wrap;
+}
diff --git a/themes/jquerymobile/js/check_item_statuses.js b/themes/jquerymobile/js/check_item_statuses.js
index 61cbf563ec354d4b33851ff96ff49f4ede90bf73..158e94419bbbeae6a61b82a6a7330fedc5757cc9 100644
--- a/themes/jquerymobile/js/check_item_statuses.js
+++ b/themes/jquerymobile/js/check_item_statuses.js
@@ -1,4 +1,14 @@
 /*global path*/
+function linkCallnumbers(callnumber, callnumber_handler) {
+  if (callnumber_handler) {
+    var cns = callnumber.split(',\t');
+    for (var i = 0; i < cns.length; i++) {
+      cns[i] = '<a href="' + path + '/Alphabrowse/Home?source=' + encodeURI(callnumber_handler) + '&amp;from=' + encodeURI(cns[i]) + '">' + cns[i] + '</a>';
+    }
+    return cns.join(',\t');
+  }
+  return callnumber;
+}
 
 function checkItemStatuses() {
     var id = $.map($('.ajaxItemId'), function(i) {
@@ -27,7 +37,7 @@ function checkItemStatuses() {
                             item.find('.status').hide();
                         } else {
                             // Default case -- load call number and location into appropriate containers:
-                            item.find('.callnumber').empty().append(result.callnumber);
+                            item.find('.callnumber').empty().append(linkCallnumbers(result.callnumber, result.callnumber_handler));
                             item.find('.location').empty().append(
                                 result.reserve == 'true'
                                 ? result.reserve_message
diff --git a/themes/jquerymobile/templates/Auth/ILS/newpassword.phtml b/themes/jquerymobile/templates/Auth/AbstractBase/newpassword.phtml
similarity index 91%
rename from themes/jquerymobile/templates/Auth/ILS/newpassword.phtml
rename to themes/jquerymobile/templates/Auth/AbstractBase/newpassword.phtml
index 7ac88888f698fbc481dc30346f432abdede4823c..62dd2c42cb21ba05a2d3967caaa95addbfbe1c3f 100644
--- a/themes/jquerymobile/templates/Auth/ILS/newpassword.phtml
+++ b/themes/jquerymobile/templates/Auth/AbstractBase/newpassword.phtml
@@ -10,6 +10,9 @@
   <? endif; ?>
   <label for="password" class="ui-input-text"><?=$this->transEsc('new_password') ?>:</label>
   <input type="password" name="password" id="password" class="ui-input-text ui-body-c ui-corner-all ui-shadow-inset"/><br/>
+  <? if ($this->passwordPolicy['hint']): ?>
+    <p><?=$this->transEsc($this->passwordPolicy['hint']) ?></p>
+  <? endif; ?>
   <label for="password2" class="ui-input-text"><?=$this->transEsc('confirm_new_password') ?>:</label>
   <input type="password" name="password2" id="password2" class="ui-input-text ui-body-c ui-corner-all ui-shadow-inset"/><br/>
 </div>
diff --git a/themes/jquerymobile/templates/Auth/Database/create.phtml b/themes/jquerymobile/templates/Auth/Database/create.phtml
index 21b371e8acf89db120d649cda6e46ca5fd49857a..ab79ddce8c52a9931abf43eaedbbd6907a45e50a 100644
--- a/themes/jquerymobile/templates/Auth/Database/create.phtml
+++ b/themes/jquerymobile/templates/Auth/Database/create.phtml
@@ -8,5 +8,8 @@
 <input id="account_username" type="text" name="username" value="<?=$this->escapeHtmlAttr($this->request->get('username'))?>" />
 <label for="account_password"><?=$this->transEsc('Password')?>:</label>
 <input id="account_password" type="password" name="password" />
+<? if ($this->passwordPolicy['hint']): ?>
+  <p><?=$this->transEsc($this->passwordPolicy['hint']) ?></p>
+<? endif; ?>
 <label for="account_password2"><?=$this->transEsc('Password Again')?>:</label>
 <input id="account_password2" type="password" name="password2" />
\ No newline at end of file
diff --git a/themes/jquerymobile/templates/Auth/Database/newpassword.phtml b/themes/jquerymobile/templates/Auth/Database/newpassword.phtml
deleted file mode 100644
index 4c0b2d107fd35a6b3f22b57495d085c510a06728..0000000000000000000000000000000000000000
--- a/themes/jquerymobile/templates/Auth/Database/newpassword.phtml
+++ /dev/null
@@ -1,15 +0,0 @@
-<div data-role="fieldcontain" class="ui-field-contain ui-body ui-br">
-  <? if (isset($this->username)): ?>
-    <input type="hidden" name="username" value="<?=$this->username ?>"/>
-    <label class="ui-input-text"><?=$this->transEsc('Username') ?>:</label>
-    <input type="text" name="username" id="username" value="<?=$this->username ?>" disabled class="ui-input-text ui-body-c ui-corner-all ui-shadow-inset" style="border:1px solid #CCC;box-shadow:rgba(0, 0, 0, 0.1) 0px 1px 4px 0px inset;color:#777"/><br/>
-  <? endif; ?>
-  <? if (isset($this->verifyold) && $this->verifyold || isset($this->oldpwd)): ?>
-    <label for="oldpwd" class="ui-input-text"><?=$this->transEsc('old_password') ?>:</label>
-    <input type="password" name="oldpwd" id="oldpwd" class="ui-input-text ui-body-c ui-corner-all ui-shadow-inset"/><br/>
-  <? endif; ?>
-  <label for="password" class="ui-input-text"><?=$this->transEsc('new_password') ?>:</label>
-  <input type="password" name="password" id="password" class="ui-input-text ui-body-c ui-corner-all ui-shadow-inset"/><br/>
-  <label for="password2" class="ui-input-text"><?=$this->transEsc('confirm_new_password') ?>:</label>
-  <input type="password" name="password2" id="password2" class="ui-input-text ui-body-c ui-corner-all ui-shadow-inset"/><br/>
-</div>
\ No newline at end of file
diff --git a/themes/jquerymobile/templates/Auth/MultiILS/newpassword.phtml b/themes/jquerymobile/templates/Auth/MultiILS/newpassword.phtml
deleted file mode 100644
index 7ac88888f698fbc481dc30346f432abdede4823c..0000000000000000000000000000000000000000
--- a/themes/jquerymobile/templates/Auth/MultiILS/newpassword.phtml
+++ /dev/null
@@ -1,15 +0,0 @@
-<div data-role="fieldcontain" class="ui-field-contain ui-body ui-br">
-  <? if (isset($this->username)): ?>
-    <input type="hidden" name="username" value="<?=$this->username ?>"/>
-    <label class="ui-input-text"><?=$this->transEsc('Username') ?>:</label>
-    <input type="text" name="username" id="username" value="<?=$this->username ?>" disabled class="ui-input-text ui-body-c ui-corner-all ui-shadow-inset" style="border:1px solid #CCC;box-shadow:rgba(0, 0, 0, 0.1) 0px 1px 4px 0px inset;color:#777"/><br/>
-  <? endif; ?>
-  <? if (isset($this->verifyold) && $this->verifyold || isset($this->oldpwd)): ?>
-    <label for="oldpwd" class="ui-input-text"><?=$this->transEsc('old_password') ?>:</label>
-    <input type="password" name="oldpwd" id="oldpwd" class="ui-input-text ui-body-c ui-corner-all ui-shadow-inset"/><br/>
-  <? endif; ?>
-  <label for="password" class="ui-input-text"><?=$this->transEsc('new_password') ?>:</label>
-  <input type="password" name="password" id="password" class="ui-input-text ui-body-c ui-corner-all ui-shadow-inset"/><br/>
-  <label for="password2" class="ui-input-text"><?=$this->transEsc('confirm_new_password') ?>:</label>
-  <input type="password" name="password2" id="password2" class="ui-input-text ui-body-c ui-corner-all ui-shadow-inset"/><br/>
-</div>
diff --git a/themes/jquerymobile/templates/RecordDriver/SolrDefault/core.phtml b/themes/jquerymobile/templates/RecordDriver/SolrDefault/core.phtml
index cf00bc8d31273fc4d2bce40f1c3dc0291996978e..f9a570094fbacc741b5cf9c50ba3fb91c029ab02 100644
--- a/themes/jquerymobile/templates/RecordDriver/SolrDefault/core.phtml
+++ b/themes/jquerymobile/templates/RecordDriver/SolrDefault/core.phtml
@@ -5,6 +5,17 @@
   } else {
     $user_id = false;
   }
+
+  $formatRoles = function ($roles) {
+    if (count($roles) == 0) {
+      return '';
+    }
+    $that = $this;
+    $translate = function ($str) use ($that) {
+      return $that->transEsc('CreatorRoles::' . $str);
+    };
+    return ' (' . implode(', ', array_unique(array_map($translate, $roles))) . ')';
+  };
 ?>
 <? /* Display thumbnail if appropriate: */ ?>
 <?=$this->record($this->driver)->getCover('core', 'medium', $this->record($this->driver)->getThumbnail('large')); ?>
@@ -50,7 +61,7 @@
   <? if (isset($authors['main']) && !empty($authors['main'])): ?>
     <dt><?=$this->transEsc(count($authors['main']) > 1 ? 'Main Authors' : 'Main Author')?>: </dt>
     <dd>
-      <p><? $i = 0; foreach ($authors['main'] as $author => $roles): ?><?=($i++ == 0)?'':', '?><a rel="external" href="<?=$this->record($this->driver)->getLink('author', $author)?>"><?=$this->escapeHtml($author)?></a><? if (count($roles) > 0): ?> (<? $j = 0; foreach ($roles as $role): ?><?=($j++ == 0)?'':', '?><?=$this->transEsc("CreatorRoles::" . $role)?><? endforeach; ?>)<? endif; ?><? endforeach; ?></p>
+      <p><? $i = 0; foreach ($authors['main'] as $author => $roles): ?><?=($i++ == 0)?'':', '?><a rel="external" href="<?=$this->record($this->driver)->getLink('author', $author)?>"><?=$this->escapeHtml($author)?></a><?=$formatRoles($roles)?><? endforeach; ?></p>
     </dd>
   <? endif; ?>
 
@@ -99,14 +110,14 @@
   <? if (isset($authors['corporate']) && !empty($authors['corporate'])): ?>
     <dt><?=$this->transEsc(count($authors['corporate']) > 1 ? 'Corporate Author' : 'Corporate Authors')?>: </dt>
     <dd>
-      <p><? $i = 0; foreach ($authors['corporate'] as $corporate => $roles): ?><?=($i++ == 0)?'':', '?><a rel="external" href="<?=$this->record($this->driver)->getLink('author', $corporate)?>"><?=$this->escapeHtml($corporate)?></a><? if (count($roles) > 0): ?> (<? $j = 0; foreach ($roles as $role): ?><?=($j++ == 0)?'':', '?><?=$this->transEsc("CreatorRoles::" . $role)?><? endforeach; ?>)<? endif; ?><? endforeach; ?></p>
+      <p><? $i = 0; foreach ($authors['corporate'] as $corporate => $roles): ?><?=($i++ == 0)?'':', '?><a rel="external" href="<?=$this->record($this->driver)->getLink('author', $corporate)?>"><?=$this->escapeHtml($corporate)?></a><?=$formatRoles($roles)?><? endforeach; ?></p>
     </dd>
   <? endif; ?>
 
   <? if (isset($authors['secondary']) && !empty($authors['secondary'])): ?>
     <dt><?=$this->transEsc('Other Authors')?>: </dt>
     <dd>
-      <p><? $i = 0; foreach ($authors['secondary'] as $author => $roles): ?><?=($i++ == 0)?'':', '?><a rel="external" href="<?=$this->record($this->driver)->getLink('author', $author)?>"><?=$this->escapeHtml($author)?></a><? if (count($roles) > 0): ?> (<? $j = 0; foreach ($roles as $role): ?><?=($j++ == 0)?'':', '?><?=$this->transEsc("CreatorRoles::" . $role)?><? endforeach; ?>)<? endif; ?><? endforeach; ?></p>
+      <p><? $i = 0; foreach ($authors['secondary'] as $author => $roles): ?><?=($i++ == 0)?'':', '?><a rel="external" href="<?=$this->record($this->driver)->getLink('author', $author)?>"><?=$this->escapeHtml($author)?></a><?=$formatRoles($roles)?><? endforeach; ?></p>
     </dd>
   <? endif; ?>
 
diff --git a/themes/jquerymobile/templates/RecordTab/holdingsils.phtml b/themes/jquerymobile/templates/RecordTab/holdingsils.phtml
index e9b2f5aa640d22704fec72f049a7942357775a45..3da187abb51a5ea1de8ecc724c674fd13056b832 100644
--- a/themes/jquerymobile/templates/RecordTab/holdingsils.phtml
+++ b/themes/jquerymobile/templates/RecordTab/holdingsils.phtml
@@ -56,7 +56,12 @@
     <th><?=$this->transEsc("Call Number")?>: </th>
     <td>
       <? foreach ($callNos as $callNo): ?>
-        <?=$this->escapeHtml($callNo)?><br />
+        <? if ($this->callnumberHandler): ?>
+          <a href="<?=$this->url('alphabrowse-home') ?>?source=<?=$this->escapeHtmlAttr($this->callnumberHandler) ?>&amp;from=<?=$this->escapeHtmlAttr($callNo) ?>"><?=$this->escapeHtml($callNo)?></a>
+        <? else: ?>
+          <?=$this->escapeHtml($callNo)?>
+        <? endif; ?>
+        <br />
       <? endforeach; ?>
     </td>
   </tr>
diff --git a/themes/jquerymobile/templates/RecordTab/usercomments.phtml b/themes/jquerymobile/templates/RecordTab/usercomments.phtml
index c172b0666861709c7782f8dd6487dda9529f012c..824f636bb5909125628be04aa93463fc34ea272f 100644
--- a/themes/jquerymobile/templates/RecordTab/usercomments.phtml
+++ b/themes/jquerymobile/templates/RecordTab/usercomments.phtml
@@ -13,6 +13,7 @@
     <label for="comments_form_comment"><?=$this->transEsc("Your Comment")?>:</label>
     <textarea id="comments_form_comment" name="comment"></textarea>
   </div>
+  <?=$this->recaptcha()->html($this->tab->isRecaptchaActive()) ?>
   <div data-role="fieldcontain">
     <input type="submit" value="<?=$this->transEsc("Add your comment")?>"/>
   </div>
diff --git a/themes/jquerymobile/templates/myresearch/holds.phtml b/themes/jquerymobile/templates/myresearch/holds.phtml
index 461b40065bf0e9170ce9bf7d41ffa54f64514d0e..3118692dda43b1238da157320bdde5da35fd16a4 100644
--- a/themes/jquerymobile/templates/myresearch/holds.phtml
+++ b/themes/jquerymobile/templates/myresearch/holds.phtml
@@ -61,7 +61,7 @@
             <? if (!empty($ilsDetails['requestGroup'])): ?>
               <p>
                 <strong><?=$this->transEsc('hold_requested_group') ?>:</strong>
-                <?=$this->transEsc('location_' . $ilsDetails['requestGroup'], array(), $ilsDetails['requestGroup'])?>
+                <?=$this->transEsc('request_group_' . $ilsDetails['requestGroup'], null, $ilsDetails['requestGroup'])?>
               </p>
             <? endif; ?>
 
@@ -85,7 +85,7 @@
             <? if (!empty($pickupDisplay)): ?>
               <p>
                 <strong><?=$this->transEsc('pick_up_location') ?>:</strong>
-                <?=$pickupTranslate ? $this->transEsc($pickupDisplay) : $this->escapeHtml($pickupDisplay)?>
+                <?=$pickupTranslate ? $this->transEsc('location_' . $pickupDisplay, null, $pickupDisplay) : $this->escapeHtml($pickupDisplay)?>
               </p>
             <? endif; ?>
 
diff --git a/themes/jquerymobile/templates/record/save.phtml b/themes/jquerymobile/templates/record/save.phtml
index 1eeacf1273e968f2c9a165baefbfd30a3b24a1e2..149feb4c13ab9842722739d6161349f2b39a773c 100644
--- a/themes/jquerymobile/templates/record/save.phtml
+++ b/themes/jquerymobile/templates/record/save.phtml
@@ -37,7 +37,7 @@
           </select>
         <? endif; ?>
 
-        <a rel="external" data-role="button" data-rel="dialog" href="<?=$this->url('editList', array('id' => 'NEW')) ?>?recordId=<?=urlencode($this->driver->getUniqueId())?>&amp;recordSource=<?=urlencode($this->driver->getSourceIdentifier())?>" class="listEdit controller<?=$this->record($this->driver)->getController()?>" title="<?=$this->transEsc('Create a List') ?>"><? if ($showLists) echo $this->transEsc('or create a new list'); else echo $this->transEsc('Create a List'); ?></a>
+        <a rel="external" data-role="button" data-rel="dialog" href="<?=$this->url('editList', array('id' => 'NEW')) ?>?recordId=<?=urlencode($this->driver->getUniqueId())?>&amp;recordSource=<?=urlencode($this->driver->getSourceIdentifier())?>" class="listEdit" title="<?=$this->transEsc('Create a List') ?>"><? if ($showLists) echo $this->transEsc('or create a new list'); else echo $this->transEsc('Create a List'); ?></a>
 
         <? if ($showLists): ?>
           <? if ($this->usertags()->getMode() !== 'disabled'): ?>
diff --git a/themes/jquerymobile/templates/vudl/record.phtml b/themes/jquerymobile/templates/vudl/record.phtml
deleted file mode 100644
index 3e86acd50ad36f23088b0993a42a54ba031d19ed..0000000000000000000000000000000000000000
--- a/themes/jquerymobile/templates/vudl/record.phtml
+++ /dev/null
@@ -1,48 +0,0 @@
-<?
-  // Set page title.
-  $this->headTitle($this->details['title']);
-  // More style
-  $this->headLink()->appendStylesheet('vudl.css');
-  $BACK_LINK = array(
-    'extraButtons'=>array('<a href="#grid" data-icon="arrow-l">'.$this->transEsc("navigate_back").'</a>')
-  );
-?>
-<div data-role="page" id="grid">
-  <?=$this->mobileMenu()->header()?>
-  <div data-role="content" class="ui-grid-c">
-    <? $i=0; foreach ($this->pages as $page): ?>
-      <div class="thumbnail ui-block-<?=substr('abcd', $i%4, 1) ?>">
-        <a href="#flow<?=$i ?>">
-            <span class="label"><?=$page['label'] ?></span>
-            <img src="<?=$page['thumbnail'] ?>" />
-        </a>
-      </div>
-    <? $i++; endforeach; ?>
-    <? foreach ($this->docs as $doc): ?>
-      <div class="thumbnail ui-block-<?=substr('abcd', $i%4, 1) ?>">
-        <a target="_new" href="<?=$doc['src'] ?>" class="label"><?=$doc['label'] ?></a>
-        <a target="_new" href="<?=$doc['src'] ?>" class="<?=$doc['img'] ?>"></a>
-      </div>
-    <? $i++; endforeach; ?>
-  </div>
-</div>
-<? foreach ($this->pages as $i=>$page): ?>
-  <div data-role="page" id="flow<?=$i ?>">
-    <?=$this->mobileMenu()->header($BACK_LINK)?>
-    <div data-role="content" class="content">  
-      <div class="ui-grid-a">
-        <div class="ui-block-a">
-          <? if($i > 0): ?>
-            <a data-role="button" data-theme="b" data-icon="arrow-l" href="#flow<?=($i-1)?>">Prev</a>
-          <? endif; ?>
-        </div>
-        <div class="ui-block-b">
-          <? if($i < count($this->pages)-1): ?>
-            <a data-role="button" data-theme="b" data-icon="arrow-r" data-iconpos="right" href="#flow<?=($i+1)?>">Next</a>
-          <? endif; ?>
-        </div>
-      </div>
-      <a href="<?=$this->pages[$i]['large'] ?>" class="preview"><img src="<?=$this->pages[$i]['medium'] ?>"/></a>
-    </div>
-  </div>
-<? endforeach; ?>
\ No newline at end of file
diff --git a/themes/root/images/europeana.eu.png b/themes/root/images/europeana.eu.png
new file mode 100644
index 0000000000000000000000000000000000000000..0e09113d028a16592e52667c2882d492fb5b98a1
Binary files /dev/null and b/themes/root/images/europeana.eu.png differ
diff --git a/themes/root/templates/Email/share-link.phtml b/themes/root/templates/Email/share-link.phtml
index 57d5f34c460955bb08081994a92c9c780e2bb6f9..955127043464558072af9b8fe621462e6ad33de2 100644
--- a/themes/root/templates/Email/share-link.phtml
+++ b/themes/root/templates/Email/share-link.phtml
@@ -7,7 +7,7 @@
 
 
 <? endif; ?>
-  <?=$this->translate('email_link')?>: <?=$this->msgUrl?>
+  <?=$this->translate('email_link')?>: <<?=$this->msgUrl?>>
 
 ------------------------------------------------------------
 
diff --git a/themes/root/templates/HelpTranslations/en/geosearch.phtml b/themes/root/templates/HelpTranslations/en/geosearch.phtml
new file mode 100644
index 0000000000000000000000000000000000000000..4872934a1d2ecedc46b6ad61035254de73c68e90
--- /dev/null
+++ b/themes/root/templates/HelpTranslations/en/geosearch.phtml
@@ -0,0 +1,23 @@
+<h1>Geographic Search Tips</h1>
+<dl class="Content">
+<dt></dt>
+  <dd>
+    <h3>To draw the search box:</h3>
+      <ol>
+        <li>Click on the 'Draw Search Box' button.</li>
+        <li>Click and release on starting point of the search box.</li>
+        <li>Drag the search box to the cover the area of the map you wish to search.</li>
+        <li>Click and release on the ending point of the search box.</li>
+      </ol>
+    <h3>Interacting with the search results</h3>
+    <p>Search results are displayed on the map as clusters (circular icons with numbers). The numbers represent
+        the number of records in each cluster. A list of results is displayed below the map view. </p>
+    <p>You can interact with the map by zooming by either using the zoom buttons (+/-) or by using the mouse to
+        double-click (zoom in) or shift + double-click (zoom out).</p>
+    <p>Clusters with less than 5 records will display pop-up information about the records at that location.
+        When you hover over a cluster with less than 5 records, the mouse pointer will change to a hand with a finger
+        pointing at the cluster. Click on the cluster, and a pop-up window will appear showing a list of titles for the
+        records at that location. Each title is hyperlinked to the associated record.
+        Click on the title to view the full record.</p>
+  </dd>
+</dl>
diff --git a/themes/root/theme.config.php b/themes/root/theme.config.php
index 7822da54141bb0154d9288f456d48cde82ec21ea..0de80f7ed5c50f93f94d52b7fd234a447944be53 100644
--- a/themes/root/theme.config.php
+++ b/themes/root/theme.config.php
@@ -15,6 +15,7 @@ return array(
             'export' => 'VuFind\View\Helper\Root\Factory::getExport',
             'feedback' => 'VuFind\View\Helper\Root\Factory::getFeedback',
             'flashmessages' => 'VuFind\View\Helper\Root\Factory::getFlashmessages',
+            'geocoords' => 'VuFind\View\Helper\Root\Factory::getGeoCoords',
             'googleanalytics' => 'VuFind\View\Helper\Root\Factory::getGoogleAnalytics',
             'helptext' => 'VuFind\View\Helper\Root\Factory::getHelpText',
             'historylabel' => 'VuFind\View\Helper\Root\Factory::getHistoryLabel',
diff --git a/util/commit.php b/util/commit.php
index 497223e97f312a701c1cd4debeccf9e03fc7cbdc..deb839887bceb3f90e9d9f53c352e01313a2ffad 100644
--- a/util/commit.php
+++ b/util/commit.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Utilities
diff --git a/util/createHierarchyTrees.php b/util/createHierarchyTrees.php
index 111685b0a1bed42782bb22b11a9b9df1a288b4b5..7b134cb725d33fc83d7f0c869702113cd4b23419 100644
--- a/util/createHierarchyTrees.php
+++ b/util/createHierarchyTrees.php
@@ -23,7 +23,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Utilities
diff --git a/util/cssBuilder.php b/util/cssBuilder.php
index f66e78dc7d74d214ab208bfb1212a631c9725c5d..1189659189102a1404607ed31d51b2637505ea22 100644
--- a/util/cssBuilder.php
+++ b/util/cssBuilder.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Utilities
diff --git a/util/dedupe.php b/util/dedupe.php
index 2d55ec8b86e757eca5a1e7c9293a0e9c59607482..b7de0743fc1af1db41f3a6aea445618bb1427717 100644
--- a/util/dedupe.php
+++ b/util/dedupe.php
@@ -19,7 +19,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Utilities
diff --git a/util/deletes.php b/util/deletes.php
index c46679b7e8c89783084b8b1c7c4fbc87a753689a..fd13511b0e1562029f92b02e04b76d1910f4bf58 100644
--- a/util/deletes.php
+++ b/util/deletes.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Utilities
diff --git a/util/expire_searches.php b/util/expire_searches.php
index 826b5704990b11a0ab6dca1cf540e2b4ad77743b..098a169e0d17111ad35f280ead34a6b329c9b355 100644
--- a/util/expire_searches.php
+++ b/util/expire_searches.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Utilities
diff --git a/util/expire_sessions.php b/util/expire_sessions.php
index 6072e7fe6d0ada851167ce88bb089c2369406707..06db2c4c6d43f9710788b524254c8868bb079401 100644
--- a/util/expire_sessions.php
+++ b/util/expire_sessions.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Utilities
diff --git a/util/index_reserves.php b/util/index_reserves.php
index f16203d6e2cebbceae2f4c0ec6bb8925c42d5d4e..02dcea582ec54b30061c4c150e0facf03688a240 100644
--- a/util/index_reserves.php
+++ b/util/index_reserves.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Utilities
diff --git a/util/optimize.php b/util/optimize.php
index 66aef01d867d504946777ce3a6cb70aaedbc84fd..5e8cef74d0abdcfee27fb30794c56e5278a689cb 100644
--- a/util/optimize.php
+++ b/util/optimize.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Utilities
diff --git a/util/sitemap.php b/util/sitemap.php
index db9f7a0a4aac235958549f5b28779db83dac463b..d54eeaebd594e4be6753749b7bc6c4c438596b70 100644
--- a/util/sitemap.php
+++ b/util/sitemap.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Utilities
diff --git a/util/suppressed.php b/util/suppressed.php
index 0f842ecd015dda0bd59f5bce22f2b33ca794b1c3..5ca75afcf16d05cd97f013910cc1811914d020db 100644
--- a/util/suppressed.php
+++ b/util/suppressed.php
@@ -17,7 +17,7 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  *
  * @category VuFind
  * @package  Utilities